아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
교재 : Effective java, 강의 : inflearn 백기선
사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
(1) 정적 유틸리티를 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다.
public class SpellChecker {
private static final Dictionary dictionary = new Dictionary();
private SpellChecker() {}
public static boolean isValid(String word) {
// TODO 여기 SpellChecker 코드
return dictionary.contains(word);
}
public static List<String> suggestions(String typo) {
// TODO 여기 SpellChecker 코드
return dictionary.closeWordsTo(typo);
}
}
(2) 싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다.
public class SpellChecker {
private final Dictionary dictionary = new Dictionary();
private SpellChecker() {}
public static final SpellChecker INSTANCE = new SpellChecker();
public boolean isValid(String word) {
// TODO 여기 SpellChecker 코드
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
// TODO 여기 SpellChecker 코드
return dictionary.closeWordsTo(typo);
}
}
SpellChecker의 기능을 확인하려면 Dictionary를 반드시 생성할 수밖에 없다.
이처럼 자원을 직접 명시하게 되면 유연성이 떨어지고 재사용성이 떨어진다.
SpellChecker
가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원(dictionary
)를 사용해야 한다. 이 조건을 만족하는 간단한 패턴이 바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.
Dictionary
를 interface
로 바꾸고 재사용 가능한 코드를 작성해보자.
public interface Dictionary {
boolean contains(String word);
List<String> closeWordsTo(String typo);
}
public class SpellChecker {
private final Dictionary dictionary;
public SpellChecker(Dictionary dictionary) {
this.dictionary = dictionary;
}
public boolean isValid(String word) {
// TODO 여기 SpellChecker 코드
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
// TODO 여기 SpellChecker 코드
return dictionary.closeWordsTo(typo);
}
}
새로운 사전이 추가되었을 때 재사용 가능하며, 코드를 테스트하는 것도 유연해졌다.
class SpellCheckerTest {
@Test
void isValid() {
SpellChecker spellChecker = new SpellChecker(new DefaultDictionary());
spellChecker.isValid("test");
}
}
이 패턴의 쓸만한 변형으로 생성자에 자원 팩터리를 넘겨주는 방식이 있다.
팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다. 즉, 팩터리 메서드 패턴(Factory Method Pattern)을 구현한 것이다. 자바 8에서 소개한 Supplier<T>
인터페이스가 팩터리를 표현한 완벽한 예다.
public SpellChecker(Supplier<Dictionary> dictionarySupplier) {
this.dictionary = dictionarySupplier.get();
}
Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입(bounded wildcard type)을 사용해 팩터리의 타입 매개변수를 제한해야 한다.
public SpellChecker(Supplier<? extends Dictionary> dictionarySupplier) {
this.dictionary = dictionarySupplier.get();
}
이 방식을 사용해 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.
@Test
void isValid() {
SpellChecker spellChecker = new SpellChecker(MockDictionary::new);
spellChecker.isValid("test");
}