조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)
1. 개요
많은 클래스가 하나 이상의 자원에 의존한다. 가령 맞춤법 검사기는 사전에 의존하는데, 이런 클래스를 정적 유틸리티 클래스나 싱글톤 클래스 구현한 모습을 드물지 않게 볼 수 있다.
1) 정적 유틸리티 클래스로 구현한 예
public class SpellChecker {
// 의존 객체 생성
private static final KoreaDictionary dictionary = new KoreaDictionary();
private SpellChecker(){} // 외부 객체 생성 방지
public static boolean isValid(String word){
...
}
public static List<String> suggestions(String typo){
...
}
}
2) 싱글톤 클래스로 구현한 예
public class SpellChecker {
// 의존 객체 생성
private final KoreaDictionary dictionary = new KoreaDictionary();
// 싱글턴 패턴을 사용해 생성한 인스턴스
public static SpellChecker INSTANCE = new SpellChecker();
private SpellChecker(){
}
public static boolean isValid(String word){
...
}
public static List<String> suggestions(String typo){
...
}
}
2. 위 방식의 문제
두 방식 모두 한국어 사전만 사용한다면 큰 문제가 되지 않지만 다른 종류의 사전을 사용해야한다면 변경이 필요할 때마다 의존 객체 생성 부분 코드를 수정해야한다.
3. 수정자 메서드를 통한 문제 해결?
수정자 메서드를 추가하여 의존객체를 변경하는 방법이 있다. 이를 사용하면 사전의 종류가 바뀌는 건 맞지만, 멀티쓰레드 환경에서는 변경되는 상태 값을 공유하면 안되므로 사용해선 안된다.
결국 멀티쓰레드 환경에서는 의존객체가 변경되는 클래스를 싱글톤이나 정적 유틸리티 클래스로 구현하면 안된다.
그렇다면 이러한 클래스는 어떻게 구현해야할까? 바로 생성자를 통해 의존 객체를 주입해야한다.
4. 생성자를 통한 의존 객체 주입
public class SpellChecker {
private final Dictionary dictionary;
// 생성자를 통한 의존 객체 주입
public SpellChecker(Dictionary dictionary){
this.dictionary = dictionary;
}
public static boolean isValid(String word){
...
}
public static List<String> suggestions(String typo){
...
}
}
생성자를 통해 객체를 생성할 때만 의존 객체를 주입하고 있다. 의존 객체는 불변성을 갖게 되어 멀티 쓰레드 환경에서도 안심하고 사용할 수 있다.
KoreaDictionary 클래스 대신 Dictionary 인터페이스를 사용하고, KoreaDictionary와 같은 여러 사전 클래스들이 Dictionary 인터페이스를 구현하도록 했다. 만약 영어사전에 대한 맞춤법 검사 기능을 제공해야한다면 Dictionary 인터페이스를 구현한 EnglishDictionary 클래스를 만들고, 이를 외부에서 생성자를 통해 주입하면 된다. 이로써 의존 객체가 바뀌더라도 SpellChecker의 코드는 변경할 필요가 없게 되었다.
이렇게 의존성을 외부로부터 주입받는 패턴을 의존 객체 주입 패턴이라고 하며 이 패턴의 변형으로 자바 8에서 등장한 Supplier<T> 인터페이스가 있다. 한정적 와일드 카드 타입을 사용해 팩터리의 타입 매개변수를 제한하여 사용할 수 있으며, 클라이언트에서는 해당 타입의 하위 타입을 포함하여 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.
public class SpellChecker {
...
public SpellChecker(Supplier <? extends Dictionary> factory){
this.dictionary = factory.get();
}
...
}
SpellChecker spellChecker = new SpellChecker(() -> new KoreaDictionary());
※ 개방 폐쇄 원칙
SpellChecker의 맞춤법 검사 기능이 영어까지 '확장' 되었지만, SpellChecker의 코드 '변경'은 일어나지 않는다. 이처럼 '확장'에는 열려있고, '변경'에는 닫혀있는 것을 '개방 폐쇄 원칙' 이라고 한다.
5. 정리
클래스가 하나 이상의 객체에 의존한다면 확장성을 고려하여 싱글턴과 정적 유틸리티 클래스 형태로 사용하지 않는 것이 좋다. 의존 객체는 내부에서 직접 생성하지 말고 생성자 혹은 정적 팩터리 메서드를 통해 넘겨주는 것이 좋다. 의존 객체 주입은 변경에 대한 유연성, 재사용성, 테스트 용이성을 제공한다.
'공부 > Effective Java' 카테고리의 다른 글
[Effective Java] Item 13. clone 재정의는 주의해서 진행하라. (0) | 2023.09.15 |
---|---|
[Effective Java] Item 12. toString을 항상 재정의하라 (0) | 2023.09.14 |
[Effective Java] Item 6. 불필요한 객체 생성을 피하라 (2) | 2023.09.07 |
[Effective Java] Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2023.09.02 |
[Effective Java] Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라. (0) | 2023.09.02 |