Programming/Java

[Youtube][이팩티브 자바] #5 의존성 주입

bisi 2020. 5. 21. 10:36

백기선님의 유투브 강의 내용을 정리하였습니다.

 

백기선님 Github 바로가기

Youtube 바로가기

 

 

강의내용 필기 


주제 5 : 리소스를 엮을 때는 의존성 주입을 선호하라

 

대부분의 클래스는 여러 리소스에 의존한다. 이책에서는 SpellChecker와 Dictionary를 예로 들고 있다. 즉, SpellChecker가 Dictionary를 사용하고 이를 의존하는 리소스 또는 의존성이라고 부른다. 이때 SpellChecker를 다음과 같이 구현하는 경우가 있다. 

 

부적절한 구현

  • static 유틸 클래스
    • 유틸클래스 -> 인스턴스로 만들지 않기 때문에 pivate한 생성자로 만들어준다. (지난 강의 #4 참고)
package me.whiteship.effectivejava3rd.item05.usecase1;

import java.util.List;

// 스펠링이 맞는지 확인하는 클래스 
public class SpellChecker {

    private static final Lexicon dictionary = new KoreanDicationry();

	// spelling check method
    private SpellChecker() {
        // Noninstantiable
    }

	// 단어가 들어왔을때, 제안할 수 있는지 확인하는 것.
    public static boolean isValid(String word) {
        throw new UnsupportedOperationException();
    }

    public static List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }

    public static void main(String[] args) {
        SpellChecker.isValid("hello");
    }
}

//이 클래스는 Lexicon이라는 Dictionary를 사용한다.
interface Lexicon {}

class KoreanDicationry implements Lexicon {}

 

  • 싱글톤으로 구현하기
    • Dictionary를 바뀌어 넣을 부분이 유연하지 않다. 
public class SpellChecker {

    private final Lexicon dictionary = new KoreanDicationry();

    private SpellChecker() {
    }

    public static final SpellChecker INSTANCE = new SpellChecker() {
    };

    public boolean isValid(String word) {
        throw new UnsupportedOperationException();
    }


    public List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }
    
    public static void main(String[] args){
    	SpellChecker.INSTANCE.isValid("hello");
    }

}

interface Lexicon{}

class KoreanDictionary implements Lexicon {}

사전을 하나만 사용할꺼라면 위와 같은 구현도 만족스러울 수 있겠지만, 실제로는 각 언어의 맞춤법 검사기는 사용하는 사전이 각기 다르다. 또한 테스트 코드에서는 테스트용 사전을 사용하고 싶을 수도 있다.

 

어떤 클래스가 사용하는 리소스에 따라 행동을 달리해야하는 경우에는 static 유틸리티 클래스와 싱글톤을 사용하는 것은 부적절하다.

 

그런 요구 사항을 만족할 수 있는 간단한 패턴으로 생성자를 사용해서 새 인스턴스를 생성할 때 사용할 리소스를 넘겨주는 방법이 있다.  (-> 의존성 주입을 사용한다!)

 

적절한 구현 

public class SpellChecker {

    private final Lexicon dictionary;

    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }

    public boolean isValid(String word) {
        throw new UnsupportedOperationException();
    }
    
    public List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }

}

class Lexicon {}

생성자를 통한 의존성 주입, 세터를 통한 의존성 주입과 같은 방법이 적절한다.

(=어떤 언어의 사전이 오든 바꿀수 있도록 하는 것이 좋다.)

 

위와 같은 의존성 주입은 생성자 static 팩토리 그리고 빌더에도 적용할 수 잇다. 

Test할때도 의존성 주입을 사용하면 재 사용하여 UNIT 테스트가 가능하다.

import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

public class SpellChecker {

    private final Lexicon dictionary;

    public SpellChecker(Supplier<Lexicon> dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary.get());
    }

    public boolean isValid(String word) {
        throw new UnsupportedOperationException();
    }

    public List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }

    public static void main(String[] args) {
        Lexicon lexicon = new TestDictionary();
        SpellChecker spellChecker = new SpellChecker(() -> lexicon);
        spellChecker.isValid("hello");
    }

}

interface Lexicon {}

class KoreanDictionary implements Lexicon {}

class TestDictionary implements Lexicon {}

 

이 패턴의 변종으로 리소스의 팩토리를 생성자에 전달하는 방법도 있다. 이방법은 자바 8에 들어온 Supplier<T> 인터페이스가 그런 팩토리로 쓰기에 완벽하다. Supplier<T>를 인자로 받는 메서드는 보통 bounded wildcard type으로 입력을 제한해야한다. 

 

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

 

의존성 주입이 유연함과 테스트 용이함을 크게 향상 시켜주지만, 의존성이 많은 큰 프로젝트인 경웅에는 코드가 장황해 질 수 있다. 그점은 대거, 쥬스, 스프링 같은 프레임웍을 사용해서 해결할 수 있다.

 

요약하자면 의존하는 리소스에 따라 행동을 달리하는 클래스를 만들 때는 싱글톤이나 스태틱 유틸 클래스를 사용하지 말자. 그런 경우에는 리소스를 생성자나 팩토리로 전달하는 의존성 주입을 사용하여 유연함, 재사용성, 테스트 용이성을 향상 시키자.