본문 바로가기

Reading/Effective Java

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

사전을 이용해서 맞춤법 검사기를 만든다고 해보자. 맞춤법 검사기는 사전에 의존하게 된다.

 

 

1. 정적 유틸리티를 사용한 클래스

 

public class SpellCheckerStaticUtility {
    private static final Lexicon dictionary = new KoreanDictionary();
    private SpellCheckerStaticUtility() { }
    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

 

2. 싱글톤을 사용한 클래스

 

public class SpellCheckerSingleton {
    private final Lexicon dictionary = new KoreanDictionary();
    private SpellCheckerSingleton() { }
    public static final SpellCheckerSingleton INSTANCE = new SpellCheckerSingleton() { };
    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

 

3. 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식(의존 객체 주입)

 

public class SpellCheckerDI {
    private final Lexicon dictionary;
    public SpellCheckerDI(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }
    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

 

사전은 언어별, 특수 어휘용 사전이 있을 수 있고 테스트용 사전이 있을 수 있다. 사전 하나로 이 모든 쓰임에 대응하기 어렵다.

즉, 1과 2의 방식은 사용하기 어렵다는 것이다. dictionary 필드에서 final 한정자를 제거하고 다른 사전으로 교체하는 메소드를 추가할 수 있지만 이 방식은 어색하고 오류를 내기 쉽고, 멀티스레드 환경에서 쓸 수 없다.

 

따라서

 

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

 

클래스(spellchecker)가 여러 자원 인스턴스를 지원해야하고, 클라이언트가 원하는 자원(dictionary)을 사용해야한다.

의존 객체 주입은 불변을 보장한다. 그리고 생성자, 정적 팩토리 메소드, 빌더 모두에 똑같이 응용할 수 있다.

 


생성자에 자원 팩토리를 넘겨주는 방식

 

팩토리: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체 --> 팩토리 메서드 패턴(Factory Method pattern)

Supplier<T> 인터페이스가 팩토리를 표현한 완벽한 예다.

 

Supplier<T>를 입력으로 받는 메소드는 일반적으로 한정적 와일드카드 타입을 사용해 팩토리의 타입 매개변수를 제한해야한다.

이 방식을 사용해 자신이 명시한 타입의 하위타입이면 뭐든 생성할 수 있는 팩토리를 넘길 수 있다.

 

//클라이언트가 제공한 팩토리가 생성한 타일들로 구성된 모자이크를 만드는 메소드
Mosaic create(Supplier<? extend Tile> tileFactory) { ... }

 

즉, Supplier에 Tile과 Tile을 상속받은 객체만 올 수 있게 한정하는 것이다. 

아래는 테스트의 예다.

 

public class SupplierWildCard {
    private List<Supplier<? extends A>> test;
    public void add(){
        test.add(A::new);
        test.add(B::new);
        test.add(C::new);
    }
    public void print(){
        test.forEach(System.out::println);
    }
}
class A { }
class B extends A { }
class C extends A { }

 

의존 객체주입이 유연성과 테스트 용이성을 개선해주긴 하지만, 의존성이 수없이 많다면 코드를 어지럽게 만든다.

대거(Dagger), 주스(Guice), 스프링(Spring)같은 의존 객체 주입 프레임워크가 이를 도와준다.

 

요약:

클래스가 내부적으로 하나 이상의 자원이 의존하고 그 자원이 클래스 동작에 영향을 준다면, 싱글톤과 정적 유틸리티 클래스를 사용하지말고, 이 자원을 클래스가 직접 만들게 하지도 말고 필요한 자원을 생성자에 넘겨주자. 

DI 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.

 

 

스프링에서의 DI와 ApplicationContext를 활용한 참조: https://github.com/keesun/study/tree/master/effective-java-3rd/src/main/java/me/whiteship/effectivejava3rd/item05/usercase4

 

 

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.