ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 5: Prefer dependency injection to hardwiring resources
    독서/Effective Java 2021. 12. 4. 19:48

    맞춤법 검사기는 사전을 사용해 해당 맞춤법이 올바른지를 체크한다.

    정적 유틸리티 클래스(Item 4)로 생성한다면 다음과 같다.

    // Inappropriate use of static utility - inflexible & untestable!
    public class SpellChecker {
      private static final Lexicon dictionary = ...;
      private SpellChecker() {} // Noninstantiable
      public static boolean isValid(String word) { ... }
      public static List<String> suggestions(String typo) { ... }
    }

    싱글턴으로 구현한다면?(Item 3)

    // Inappropriate use of singleton - inflexible & untestable!
    public class SpellChecker {
      private final Lexicon dictionary = ...;
      private SpellChecker(...) {}
      public static INSTANCE = new SpellChecker(...);
      public boolean isValid(String word) { ... }
      public List<String> suggestions(String typo) { ... }
    }

    잘못된 접근: 사전이 단 하나의 종류만 있다고 가정

    유틸리티 클래스나 싱글턴 패턴으로 맞춤법 검사기를 만들 경우 다양한 사전(각 언어 -> 자체 사전 존재, 특수 어휘 -> 특수 사전이 사용됨)에 대응할 수 없다.

    또한 직접 명시되어 고정되어 있는 변수는 테스트를 하기 힘들게 만든다.
    해당 클래스는 사용하는 자원에 따라 동작이 달라지고, 이런 경우 정적 유틸리티 클래스나 싱글턴 방식은 사용하기 어렵다. 해결 방법은 의존성을 바깥으로 분리하여 외부로부터 주입받도록 해야 한다. (의존 객체 주입 패턴)

    해당 예제에서의 예: 사전을 외부로부터 주입받는다.

     

    의존 객체 주입

    코드로 보자.

    // Dependency injection provides flexibility and testability
    public class SpellChecker {
      private final Lexicon dictionary;
      public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
      }
      public boolean isValid(String word) { ... }
      public List<String> suggestions(String typo) { ... }
    }

    불변성을 유지(Item 17)하기 때문에 여러 클라이언트가 의존 개체를 공유할 수 있다.

    의존성 주입은 생성자, 정적 팩토리(Item 1) 및 빌더(Item 2)에 동일하게 적용할 수 있다.

     

    변형: pass a resource factory to the constructor

    패턴의 유용한 변형은 리소스 팩토리를 생성자에 전달하는 것이다. 

    (팩토리: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 의미한다.)

    @FunctionalInterface
    public interface Supplier<T> {
      T get();
    }
    
    // 실제 사용 예
    Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

     

    실제 맞춤법 검사기에 적용하면 다음과 같게 변형 가능하다.

    public class SpellChecker { 
    	private final Lexicon dictionary; 
    
    	private SpellChecker(Supplier<Lexicon> dictionary) { 
        		this.dictionary = Objects.requireNonNull(dictionary); 
     	} 
    
    	public boolean isValid(String word) { ... } 
    	public List<String> suggestions(String typo) { ... } 
    
    }
    
    // 사용 예
    Lexicon lexicon = new KoreanDictionary();
    SpellChecker spellChecker = new SpellChecker(() -> lexicon);
    spellChecker.isValid("hello");

    Supplier 자체가 Functional Interface이기 때문에 람다로도 바로 사용이 가능하다.

     

    의존 개체 주입은 유연성과 테스트 가능성을 크게 향상시키지만 일반적으로 수천 개의 종속성을 포함하는 대규모 프로젝트를 복잡하게 만든다.

    일반적으로 Dagger, Guice 또는 Spring과 같은 종속성 주입 프레임워크를 사용해 대규모 프로젝트를 구현하게 되면 해당 프레임워크에서 자동으로 의존 개체 주입을 활성화해주기 때문에 장황해지는 코드에서 벗어날 수 있다.

     

    정리

    클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다(특히 내부 자원을 직접 생성하지 않도록 하는 것이 포인트).
    대신 필요한 자원 또는 자원 생성 팩토리를 생성자에 (혹은 정적 팩터리나 빌더에) 전달하자.
    의존 개체 주입이라고 하는 이 방법은 클래스의 유연성, 재사용성 및 테스트 가능성을 크게 향상시킨다.

    댓글

Designed by Tistory.