5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
안녕하세요, Item 04를 쓰고 7일만에 다시 포스팅을 하게 되었습니다.
그간, 스프링의 묘리를 깨닫기 위해 많이 노력을 한 것 같습니다 ㅎㅎ
프록시라는 개념을 알고, 다시 포스팅을 하니 무언가 유기적으로 연결되어 가는 느낌(?)이 드네요...
금일 포스팅은, 의존 객체 주입입니다.
대게 많은 클래스는 하나 이상의 자원에 의존합니다.
책의 예시를 들어, 가령 맞춤법 검사기는 사전(dictionary)에 의존하는데, 이러한 클래스를 정적 유틸리티 클래스(Item 4)로 구현한 모습을 드물지 않게 볼 수 있습니다.
정적 유틸리티를 잘못 사용한 예 - 유연하고 테스트하기 어려움
package item5;
import java.util.List;
public class SpellChecker {
private static final RrineauDictionary dictionary = new RrineauDictionary();
private SpellChecker() {} // private 생성자로 객체 생성 방지
public static boolean isValid(String word) {
/**
* ....
*/
return true;
}
public static List<String> suggestions(String typo) {
return List.of("Apple");
}
}
싱글턴을 잘못 사용한 예 - 유연하지 않고, 테스트하기 어려움
package item5;
import java.util.List;
public class SpellChecker2 {
private static final RrineauDictionary dictionary = new RrineauDictionary();
private SpellChecker2() {} // private 생성자로 객체 생성 방지
public static SpellChecker2 INSTANCE = new SpellChecker2();
public static boolean isValid(String word) {
/**
* ....
*/
return true;
}
public static List<String> suggestions(String typo) {
return List.of("Apple");
}
}
두 방식 모두 사전을 단 하나만 사용하고 있다는 것에, 그리 유용하지 않아보입니다.
왜냐하면, 사전은 여러 종류(언어, 전문용어 등)가 있을 수 있고, 심지어 테스트용 사전도 필요할 수 있기 때문입니다. 단지 사전 하나로 이 모든 쓰임에 대응한다는 것은 어느정도 한계가 있습니다.
그리하여, SpellChecker가 여러 사전을 사용할 수 있도록 만들어 보겠습니다.
간단히 dictionary 필드에서 final을 제거하고, 다른 사전으로 교체하는 메서드를 추가할 수도 있겠지만, 이는 멀티스레드 환경에서 사용이 불가합니다. (동시성 이슈)
여기서 중요한 의미가 내포되는데, 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않습니다.
앞서 예시를 들었듯, SpellChecker가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원(dictionary)를 사용해야 합니다.
이것을 슬기롭게 해결하기 위해, 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이 있습니다.
이는, 의존 객체 주입의 한 형태로, 맞춤법 검사기 생성 시 의존 객체인 사전을 주입해주면 됩니다. (대개 많이 언급되는 Spring의 의존주입을 떠올린다면 이해하기 쉬울 것 같습니다.)
의존 객체 주입의 예시 - 유연성과 테스트 용이성을 높여 줌
package item5;
import java.util.List;
import java.util.Objects;
public class SpellChecker3 {
private final RrineauDictionary dictionary;
public SpellChecker3(RrineauDictionary dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public static boolean isValid(String word) {
/**
* ....
*/
return true;
}
public static List<String> suggestions(String typo) {
return List.of("Apple");
}
}
사실 위 코드는, 아주 단순하여 수많은 사람이 이 방식에 이름이 있다는 사실도 모른 채 사용했습니다. 또한 불변(Item 17)을 보장하여, 여러 클라이언트가 안심하고 공유할 수 있기도 합니다. 의존 객체 주입은 생성자, 정적 팩터리(item 1), 빌더(item 2) 모두에 똑같이 응용할 수 있습니다.
쓸만한 응용으로, 생성자에 자원 팩터리 자체를 넘겨주는 반식이 존재하는데,
여기서 팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어 주는 객체를 말합니다.
즉, 팩터리 메서트 패턴을 구현한 것입니다.
자바 8에 출시한 Supplier<T> 인터페이스가 팩터리를 완벽하게 표현한 예시입니다.
Supplier<T>를 입력으로 받는 메서드는, 일반적으로 한정적 와일드카드 타입(item 31)을 사용해 팩터리의 타입 매개변수를 제한한다고 하는데, 추후 포스팅에 다시 언급 하도록 하겠습니다.
정리하여, 클랴스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면.. 싱글턴이나 정적 유틸리티 클래스는 사용하지 않는 것이 좋습니다. 예시를 들었지만, 다시 강조하여 필요한 자원을 생성자 (혹은 정적 팩터리나 빌더에) 넘겨주는 것이 좋습니다.
의존 객체 주입이라 하는 기법은, 클래스의 유연성, 재사용성, 테스트 용이성을 아주 눈에 띄게 개선합니다.
마찬가지로 그저 사용하던 당연한 방식이 '의존 주입' 방식이란것이 꽤나 흥미로웠습니다.
그저... final 필드가 있으면 해당 필드를 받는 생성자를 만들어줘서 소위 말하는 빨간줄을 없애는 당연한 처사라고 생각했는데, 여기에는 꽤나 중요한 의미와 이유가 있었습니다.