의존성 주입(Dependency Injection)은 자바 계통 언어로 개발을 해본 사람이라면 정말 익히 들어봤을 내용이지만, 난 지금까지 정확하게 이해를 하진 못했었다.
글의 내용을 훑어보고 내가 이해한 내용을 요약하자면 "상황에 맞게 클래스가 의존하는 자원을 변경하여, 유연성과 재사용성을 높이자"가 된다.
📌 유틸리티의 잘못 사용한 예
1️⃣ 정적 유틸리티를 잘못 사용한 예
public class SpellChecker {
private static final Lexicon dictionary = ...; // 의존하는 자원
private SpellChecker() {} // 객체 생성 방지
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
2️⃣ 싱글턴을 잘못 사용한 예
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static SpellChecker INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) {... }
}
두 방법의 공통적인 문제점은 자원을 얻어오는 'private final Lexicon dictionary = ...;'에 있다.
맞춤법 검사기는 사전(dictionary)에 의존하는데, 사전이란 건 언어별로 다르고, 특수 어휘용 사전을 별도로 두기도 하는데 자원을 하나로 고정하고 있으므로 유연하지 않고 테스트가 어려워진다.
필드에서 final 키워드를 제거하고, 사전을 교체하는 메서드를 사용할 수는 있지만 Thread-safe를 얻지 못하므로 멀티 스레드 환경에서 사용할 수 없다.
따라서, 사용하는 자원에 따라 동작이 달라지는 클래스의 경우 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
이를 해결하기 위한 방식은 바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 의존성 주입(Dependency Injection)이다.
📌 의존성 주입(Dependency Injection)
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 typo) { ... }
}
final 한정자를 제거하지 않았으므로 불변을 보장하므로 여러 클라이언트가 의존 객체들을 안심하고 공유할 수 있다.
또한 자원이 몇 개든 의존 관계가 어떻게 되는 상관없이 잘 동작한다.
의존성 주입의 생성자, 정적 팩터리, 빌더 모두 똑같이 적용 가능하다.
📌 팩터리 메서드 패턴(Factory Method pattern)
생성자에 자원 팩터리를 넘겨주는 방식으로 의존 객체 주입 패턴의 변형이다.
• Factory: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
• Resource Factory: 객체 생성을 위해서 별도의 팩터리 객체를 사용하는 패턴. 객체 생성 과정에서 다양한 설정 정보를 이용할 수 있다.
(ex. DB 연결 객체 생성 과정에서 DB 정보, 연결 방식 등을 동적으로 변경할 수 있다.)
즉, Factory Method 패턴은 생성자나 메서드를 통해 직접적으로 생성하는 것이 아니라, 객체 생성에 대한 책임을 서브 클래스로 분리하여 구현한다.
이렇게 되면 추상 클래스나 인터페이스에서는 객체 생성 메서드의 시그니처만 정의하고, 생성에 대한 구체적인 구현은 서브 클래스가 처리하므로 유연하게 객체를 생성할 수 있게 된다.
대표적인 예로 Java8에서 Supplier<T> 인터페이스가 Factory를 표현한 완벽한 예다.
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get(); // T 타입 객체를 찍어낸다
}
T타입의 객체를 제공하는 함수형 인터페이스이므로, 이를 활용하여 객체 생성을 위한 Resource Factory를 구현할 수 있다.
public class Foo {
private final Resource resource;
public Foo(Supplier<Resource> resourceFactory) { // 객체 생성에 대한 책임을 Supplier로 분리
this.resource = resourceFactory.get();
}
public void doSomething() {
resource.use();
}
}
Foo 클래스는 Resource 클래스를 의존하고 있다.
Foo 클래스 생성자에서는 Supplier<Resource> 타입의 매개변수를 받아 Resource Factory를 넘겨받는다.
이후 doSomething() 메서드에서는 Resource Factory를 통해 생성된 Resource 객체를 사용한다.
이를 통해서 Foo 클래스는 외부에서 Resource 객체를 제공받아 의존성을 낮추는 효과를 얻을 수 있다.
책에서는 Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드 타입(bounded wildcard type, Item 31)을 사용하여 팩터리의 타입 매개변수를 제한해야 한다고 나와 있다.
이 방식을 이용하면 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 Factory를 넘길 수 있게 된다.
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
의존 객체 주입이 유연성과 테스트 용이성을 높여주는 것은 사실이지만, 의존성이 너무 많으면 코드를 어지럽게 하여 디버깅이 굉장히 까다로워진다.
Dagger, Guice, Spring과 같은 의존 객체 주입 프레임워크를 사용하면 이러한 문제를 해소할 수 있다.
책에서는 다음과 같이 핵심 정리를 해놓았다.
"클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 필요한 자원 혹은 팩토리를 생성자 혹은 정적 팩터리나 빌더에 넘겨주자. 의존 객체 주입은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다."