본문은 Effective Java를 읽고 정리한 내용을 기반으로 작성된 글입니다.
많은 클래스가 하나 이상의 자원에 의존한다. 하지만 의존관계를 잘못 사용하면 유연하지 않고 테스트가 어렵다.
- 정적 유틸리티를 잘못 사용한 경우
public class SpellChecker {
private static final Lexicon dictionary = ...; // 의존하는 리소스 (의존성)
private SpellChecker() {} // 객체 생성 방지
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
- 싱글톤을 잘못 사용한 경우
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {}
public static SpellChecker INSTANCE = new Spellheker(...);
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
두 방식 모두 단 하나의 사전만 사용한다고 가정한다는 점에서 좋지 않다. (언어별 사전, 특수 어휘 사전 등등)
→ SpellChecker가 여러개의 사전을 사용할 수 있도록 만들자.
☑️ 필드에서 final 한정자를 제거하고 다른 사전으로 교체하는 메서드를 추가한다면?
- 이 방법은 어색하고, 오류를 내기 쉬움
- 멀티스레드 환경에선 쓸 수 없음
- final일 때는 한 번 초기화 → 변경 불가 → 스레드가 동일 객체를 읽는 동안에는 안전하지만, final을 제거한다면 그렇지 않으므로
결론은, 사용하는 자원에 따라 동작이 달라지는 클래스 ↔ 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
대신, 클래스(SpellChecker)가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원(dictionary)를 사용해야 한다.
→ 이 조건을 만족하는 패턴 : 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식 (의존 객체 주입의 한 형태)
[의존 객체 주입]
public class SpellChecker {
private final Lexicon dictionary;
private SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { return true; }
public List<String> suggestions(String typo) { return null; }
}
- dictionary라는 딱 하나의 자원을 사용하지만 자원 개수, 의존관계와 상관 없이 잘 작동함
- 불변(item17)임을 보장함 → 여러 클라이언트가 의존 객체들을 공유할 수 있음
[팩터리 메서드 패턴]
: 위의 예제를 발전시켜, 자원을 바로 받는게 아니라 팩터리를 통해 가져오는 방식으로 중간 단계를 한번 더 추상화할 수 있다.
☑️ 팩터리
: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
☑️ Supplier<T> 인터페이스
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get(); // 매개변수가 없고 T를 반환하는 추상 메서드
}
- Supplier<T>를 입력받는 메서드는 일반적으로 한정적 와일드카드 타입을 사용해서 팩터리의 타입 매개변수를 제한해야 한다.
- 클라이언트는 자신이 명시한 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.
public class SpellChecker {
private final Dictionary dictionary;
public SpellChecker(Supplier<Dictionary> dictionarySupplier) {
this.dictionary = dictionarySupplier.get();
}
public boolean isValid(String word) {
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
return dictionary.closeWordsTo(typo);
}
}
→ Supplier 인터페이스를 통해 구현한 예시 코드가 많이 복잡해지므로 스프링같은 의존 객체 주입 프레임워크를 사용하자~
[핵심 정리]
- 클래스가 내부적으로 클래스 동작에 영향을 주는 하나 이상의 자원에 의존한다면, 싱글톤 & 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
- 대신 필요한 자원/자원을 만들어주는 팩터리를 생성자/정적 팩터리/빌더에 넘겨주자.
참고
https://velog.io/@alkwen0996/이펙티브-자바-아이템5-자원을-직접-명시하지-말고-의존-객체-주입을-사용하라