[Effective Java] 아이템5 - 자원은 직접 명시하지 말고 의존 객체 주입을 사용하라

Note: 이 글은 Effective Java 책을 읽으면서 중요한 내용 및 알아야 하는 지식들을 정리한 글입니다.
또한, 백기선님의 Youtube 강의영상도 참고 했습니다.

대부분의 클래스는 하나 이상의 자원에 의존한다.

책에서는 Dictionary에 의존하는 SpellChecker를 예로 들고 있다. 이렇게 의존성을 가지고 있는 클래스를 다음과 같이 구현하는 경우가 있다.

정적 유틸리 클래스 [아이템4]

// 부적절한 static 유틸리티 사용 예 - 유연하지 않고 테스트 할 수 없다.
public class SpellChecker {
    private static final Lexicon dictionary = new KoreanDicationry();

    private SpellChecker() {
        // Noninstantiable
    }

    public static boolean isValid(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

싱글턴 [아이템3]

// 부적절한 싱글톤 사용 예 - 유연하지 않고 테스트 할 수 없다.
public class SpellChecker {
    private final Lexicon dictionary = new KoreanDicationry();

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

    public boolean isValid(String word) {...}
    public List<String> suggestions(String typo) {...}
}

위 두가지 방식 모두 Lexicon을 다른 사전으로 변경하려면 코드를 수정해야 한다.

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱클턴 방식은 적합하지 않다.

대신 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 의존 객체 주입 패턴을 사용한다.

의존성 주입 구현

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 {}

위와 같은 의존객체 주입은 생성자, 정적 팩터리아이템1, 빌더아이템2에도 응용할 수 있다.

팩터리 메서드 패턴 (Factorty Method Pattern)

의존 객체 주입의 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다.

  • 팩터리 : 호출할 때마다 특정 타입의 인스턴스를 만들어주는 객체
  • 팩토리 메소드 패턴 : 서브클래스에서 구체적인 오브젝트 생성 방법을 경정하게 하는 것

자바8에서부터 사용가능한 Supplier<T> 인터페이스가 팩터리를 표현한 예이다.

// 유연하게 Lexicon 을 replace 할 수 있다.
Lexicon lexicon = new KoreanDictionary();
SpellChecker spellChecker = new SpellChecker(new Supplier<Lexicon>() {
  @Override
  public Lexicon get() {
    return lexicon;
  }
});
Lexicon testLexicon = new TestDictionary();
SpellChecker testChecker = new SpellChecker(() -> testLexicon); // 람다 표현식

Supplier<T> 인터페이스는 함수형 인터페이스이므로 람다 표현식으로 표현 가능하다.

This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference. Interface Supplier API 명세서

한정적 와일드카드 타입(bounded wildcard type) 아이템31 를 사용해 팩터리의 타입 매개변수를 제한할 수도 있다.

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

의존성 주입은 스프링 프레임워크 (특히 토피의 스프링 프레임워크)에 익숙한 사람은 읽기 쉬운 주제이다.

Summary: 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴, 정적 유틸리티 클래스로 구현하지 말고 의존성을 주입하자

의존성 객체를 활용한 예제

나이에 따라 검색어 자동완성 기능에 노출될 검색어를 다르게 하는 간단한 예제를 만들어 봤다.

  • 19세 미만일 경우 “담요”, “술빵”, “와우”, “그날들” 노출
  • 19세 이상일 경우에는 “담배”, “술”, “와인”, “그레이의 55가지 그림자” 노출
SpellChecker spellChecker = new SpellChecker(new Supplier<Lexicon>() {
  @Override
  public Lexicon get() {
    if(age < 20) {
      return new ChildDictionary();
    } else {
      return new GeneralDictionary();
    }				
  }
});		
return spellChecker.suggestions();

Supplier<T>를 활용해서, 조건에 따라 객체를 return 하게 팩터리 메서드 패턴을 적용했다.

결과

댓글남기기