객체지향 설계에서 많은 클래스가 하나 이상의 자원에 의존한다. item5는 의존상태의 잘못된 예로 두 가지를 든다.

자원을 직접 명시한 경우

// 정적 유틸리티 클래스
public class SpellChecker1 {

    private static final Lexicon dictionary = new KoreanDictionary();

    private SpellChecker1() {
    }

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

    public static List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }
}
  • item4의 정적 유틸리티 클래스로 구현한 SpellChecker1 클래스이다.
  • SpellChecker1은 KoreanDictionary 클래스를 직접 생성하여 dictionary 변수에 할당하고 있다.
  • 이는 다른 종류의 사전으로 사용한다고 했을 때, 교체하기 힘들며, SpellChecker1을 테스트하고자 할 때, dictionary도 같이 테스트하게 되는 문제점이 있다.
// 싱글톤
public class SpellChecker2 {

    private final Lexicon dictionary = new KoreanDictionary();

    private SpellChecker2() {
    }
    
    public static SpellChecker2 INSTANCE = new SpellChecker2();

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

    public static List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }
}
  • item3의 싱글톤 방식을 사용한 SpellChecker2 클래스이다.
  • 인스턴스 생성없이 static으로 직접 할당을 하는 위의 두 방식은 dictionary를 바꾸기 쉽지않다.
  • setDictionary()를 만들어 정적 멤버를 변경한다면 멀티 스레드 환경에서 버그를 유발하기 쉽다.
  • 이처럼 어떤 클래스가 사용하는 리소스에 따라 행동을 달리해야 하는 경우 위의 두 방법은 부적절하다.

의존 객체 주입

public class SpellChecker3 {
    private final Lexicon dictionary;

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

    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 KoreanDictionary();
    
    SpellChecker3 spellChecker = new SpellChecker3(lexicon);
    spellChecker.isValid("test");
}
  • 의존성 주입방식은 위의 두 방법의 문제점을 해결한다.
  • 인스턴스 생성 시, 생성자에 필요한 자원을 넘겨받아 사용하면되고, dictionary가 final 키워드로 설정되어있어 새로운 참조값을 받을 수 없는 불변의 상태를 만들 수 있다.
  • 또한 lexicon이라는 인스턴스를 테스트하려면 테스트용 lexicon으로 교체할 수 있다.

      class KoreanDictionary implements Lexicon {}
        
      class TestDictionary implements Lexicon {}
    
    • SpellChecker3 안에 있는 내용만 유닛 테스트가 가능해진다.

Spring의 의존성 주입

// Lexicon interface
public interface Lexicon {}

// SpellChecker
@Component
public class SpellChecker {
    
    private Lexicon lexicon;
    ...
    }
}

// KoreanDictionary
@Component
public class KoreanDictionary implements Lexicon {...} 

// config
@Configuration
@ComponentScan(basePackageClasses = Config.class)
public class Config {...}

// 사용
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Config.class);
SpellChecker spellChecker = applicationContext.getBean(SpellChecker.class);
spellChecker.isValid("test");
  • Spring boot를 사용한다면 위처럼 직접 작성하여 사용하지는 않지만, Spring의 의존성 주입 방식은 다음과 같다.
  • @Component를 통해 Spring Container에 Bean을 등록하고 Config 설정의 ComponentScan을 통해 인스턴스를 사용하는 쪽에서는 Bean을 받아(getBean) 사용한다.

참고자료

  • 이펙티브 자바 3판
  • https://www.youtube.com/watch?v=24scqT2_m4U&list=PLfI752FpVCS8e5ACdi5dpwLdlVkn0QgJJ&index=5&ab_channel=%EB%B0%B1%EA%B8%B0%EC%84%A0