[Effective Java] Item 5. 자원을 직접 명시하지 않고 의존 객체를 주입하라
사전에 의존하고 있는 맞춤법 검사기를 자바 클래스로 한 번 구현해봅시다.
정적 유틸리티를 사용한 첫 번째 예시
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 객체 생성 방지
public static boolean isValid(String word) { ... }
public List<String> suggestions(String type) { ... }
}
싱글턴을 사용한 두 번째 예시
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static SpellChecker INSTANCE = new SpellChecker(...);
public static boolean isValid(String word) { ... }
public List<String> suggestions(String type) { ... }
}
사실 위의 두 예시는 단 하나의 사전만 사용한다고 가정한다는 점에서 잘못된 예시라고 볼 수 있습니다.
그래서 이번에는 SpellChecker 클래스가 언어 별로 다양한 사전을 선택해 사용할 수 있도록 수정해봅시다.
final 한정자를 제거하고 사전 교체 메소드를 추가한 예시
public class SpellChecker {
private static Lexicon dictionary = ...;
private SpellChecker(...) {}
public void setDictionary(Lexicon dictionary) { this.dictionary = dictionary; }
public boolean isValid(String word) { ... }
public List<String> suggestions(String type) { ... }
}
메소드를 통해 사전을 교체하는 방식은 오류를 내기 쉬우며 특히 멀티스레드 환경에서는 쓸 수 없습니다.
결론적으로 사용하는 자원에 따라 동작이 달라지는 경우에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않습니다.
의존 객체 주입 패턴
클래스가 여러 자원을 지원해야 하며, 이 중 원하는 자원을 선택해 사용해야 하는 경우 의존 객체 주입을 적용할 수 있습니다.
의존 객체 주입 패턴은 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식
의존 객체 주입을 사용한 예시
public class SpellChecker {
private final Lexicon dictionary;
private SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String type) { ... }
}
의존 객체 주입의 장점
- 유연성 개선
- 테스트 용이성 개선
의존 객체 주입의 단점
- 의존성이 많은 코드의 가독성 저하
가독성이 저하되는 단점은 스프링과 같은 의존 객체 주입 프레임워크를 사용해 해결할 수 있습니다.
의존 객체 주입 방법
1. 생성자로 주입하기
2. 정적 팩터리로 주입하기
3. 빌더로 주입하기
의존 관계 주입 예시: 팩터리 메서드 패턴
생성자의 파라미터로 의존하는 객체가 아닌 의존 객체를 생성하는 자원 팩터리를 넘겨줄 수도 있습니다.
여기서 팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말합니다.
생성자를 통해 특정 타입의 자원을 만들 수 있는 팩터리를 넘겨주는 방식을 팩터리 메서드 패턴이라고 부릅니다.
팩터리 메서드 패턴에 대한 자세한 설명은 아래 글을 참고해주세요.
팩터리 메서드 패턴의 예시로 자바 8의 Supplier<T> 인터페이스가 있습니다.
Supplier<T> 인터페이스에 대해 짧게 알아보자면
Supplier는 원하는 객체를 반복해서 공급해주는 공급자 인터페이스입니다.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier<T> 인터페이스 내부 코드에는 특정 타입의 인스턴스를 반복해서 반환하는 추상 메서드 get() 하나만 존재합니다.
Supplier<String> supplier = () -> "Hyeonae";
System.out.println("I'm " + supplier.get());
Supplier<Car> supplier = () -> new Car();
System.out.println(supplier.get());
Supplier에는 원시 타입(Integer, Long)부터 시작해 문자열과 객체, 심지어 함수까지 담을 수 있습니다.
담아둔 객체는 get() 메소드를 통해 반복적으로 생성할 수 있습니다.
Supplier<T> 인터페이스로 팩터리 메서드 패턴 만들기
Supplier<T> 인터페이스는 생성하고자 하는 자원의 팩터리 역할을 수행할 수 있습니다.
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
위의 코드는 매개변수로 주입받은 타일 팩터리를 활용해 모자이크를 생성하는 메소드입니다.
이 때 매개변수로 주입 받는 Supplier 인터페이스의 타입은 Tile을 상속하고 있어야만 합니다.
제네릭 코드에서 물음표(?)로 표기된 것은 아직 알려지지 않은 타입(와일드카드)를 의미합니다.
와일드카드의 타입을 extends로 제한하여 Tile의 하위 타입이라면 모두 생성할 수 있는 팩터리를 받아 사용할 수 있습니다.