아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.
하나 이상의 자원에 의존하는 클래스의 경우에는 다음과 같이 구현된 경우를 드물지 않게 볼 수 있다.
// 정적 유틸리티를 잘못 사용한 예
public clas SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} //객체 생성 방지
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String type) { ... }
}
// 싱글턴을 잘못 사용한 예
public clas SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static SpellChecker INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
위 두가지 클래스의 구현이 아주 훌륭하지는 못하다고 할 수 있는 이유는, 실전에서는 사전이 언어별로 따로 있고 특수 어휘용 사전을 별도로 두기도 하기 때문이다. 사전 하나(필드)로 이 모든 쓰임에 대응할 수 있기를 바라지 말자.
이를 개선하기 위해서 SpellChecker가 여러 사전을 사용할 수 있도록 만들어보자. final 한정자를 삭제하고 다른 사전으로 교체하는 메서드를 추가할 수 있지만 이 방법은 멀티스레드 환경에서는 쓸 수 없다.
🤔 이와 관련하여 우아한 테크코스의 첫번째 미션 중 내가 구현한 레이싱게임 클래스이 정적 유틸리티 클래스였고, 레이싱게임이 끝났는지를 판단하는 포인트가 되는 isRunning 필드가 static boolean으로 선언되어있었다. 이 부분에서 리뷰어분께 static으로 했을 때 멀티스레드 환경에서 잘 동작할 수 있을지 여부에 대해서 고민해보라고 하셨는데, 이 문제와 동일한 문제일 것이다. 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다. 스레드1에서 해당 isRunning 필드 값을 변경할 경우, 동시에 참조하던 스레드2에서 문제가 발생할 수 있다.
따라서 이 경우에는 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야한다. 이 조건을 만족시키기 위해서는 바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다. 이는 의존 객체 주입의 한 예이다.
public class SpellChecker {
private static final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String type) { ... }
}
이 의존 객체 주입 패턴의 쓸만한 변형으로는 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 즉, 팩터리 메서드 패턴을 구현하는 것이다. 이 방식을 사용하면 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }