우리는 주로 사전과 같은 경우 정적 유틸리티 클래스 혹은 싱글턴으로 구현하고는 한다. 하지만, 이는 잘못된 방법인데 많은 클래스가 하나 이상의 자원에 의존하는 경우가 많기 때문이다. 아래의 예시를 통해 자세히 알아보자.
// 정적 유틸리티를 잘못 사용한 예시
public class SpellChecker {
private final Lexion dictionary = ...;
private SpellChecker(..) {}
public static booolean isValid(String word) {...}
public static List<String> suggestions(String type){...}
}
// 싱글톤을 잘못 사용한 예시
public class SpellChecker {
private final Lexion dictionary = ...;
private SpellChecker(..) {}
public static SpellChecker INSTANCE = new SpellChecker(...); // 객체 생성
public booolean isValid(String word) {...}
public List<String> suggestions(String type){...}
}
앞선 두 방식은, 유연하지 않고 테스트하기 어렵다는 단점이 존재한다.
dictionary
종류를 외부에서 바꾸는 것이 불가능하므로 이는 모두 사전을 단 하나만 사용한다고 가정한 코드이지, 다른 종류의 사전들이 들어오는 경우는 고려하지 않았기 때문이다.
그렇다면 SpellChecker
가 여러 사전을 사용할 수 있도록 하려면 어떻게 해야할까?
필드에서 final
한정자를 제거하고 사전을 교체할 수 있도록 지원하는 setter
메서드는 멀티 스레드 환경에서 사용이 불가능하며 에러를 일으킨다.
public static void setDictionary(Lexicon new) {
dictionary = new;
}
즉, 사용하는 자원에 따라, 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않은 것이다.
위 문제점의 해결책은, 바로 의존 객체 주입이다. 의존 객체 주입이란 인스턴스를 생성할때 생성자에 필요한 자원을 넘겨주는 방식 이다. 대표적으로 스프링은 의존 객체 주입 프레임워크이다.
단, 다음의 2가지 조건을 만족할때 사용 가능하다.
- 클래스(SpellChecker)가 여러 자원 인스턴스를 지원
- 클라이언트가 원하는 자원(dictionary)을 사용
public class SpellChecker{
private final lexicon dictionary;
public SpellChecker(Lexion dictionary){ //생성자
this.dictionary = Objects.requireNonNull(dictionary);
}
public booolean isValid(String word) {...}
public List<String> suggestions(String type){...}
}
이처럼 의존 객체 주입 패턴은, 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 동작한다.
interface Lexicon {}
public class KoreanDictionary implements Lexicon {}
public class EnglishDictionary implements Lexicon {}
Lexicon kDic = new KoreanDictionary();
Lexicon eDic = new EnglishDictionary();
SpellChecker spellChecker = new SpellChecker(kDic);
SpellChecker spellChecker = new SpellChecker(eDic);
또한, final
로 자원의 변경을 막음으로써 불변을 보장하여 같은 자원을 사용하려는 여러 클라이언트가 의존 객체들을 안심하고 공유 가능하다.
이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 존재하며 이를 팩터리 메서드 패턴이라 한다. 자바 8의 Supplier<T>
을 사용하면, 자신이 명시한 타입의 하위 타입이라면 모두 생성할 수 있는 팩터리를 넘길 수 있다.
@FunctionalInterface
public interface Supplier<T> {
T get(); // 매개변수가 없고, 단순히 무엇인가를 반환할 때 사용
}
Supplier<T>
를 입력으로 받는 메서드는, 한정적 와일드카드 타입을 사용해 팩터리의 타입 매개변수를 제한할 수 있기 때문이다. 예시를 봐보자.
class Tile {
// 기본 타일 클래스
}
class GrassTile extends Tile {
// 잔디 타일 클래스
}
public class GameMap {
private Supplier<? extends Tile> tileFactory;
public GameMap(Supplier<? extends Tile> tileFactory) {
this.tileSupplier = tileSupplier;
}
...
}
Supplier<Tile> grassTileSupplier = () -> new GrassTile(); // GrassTile::new;
GameMap grassMap = new GameMap(grassTileSupplier);
💡핵심 정리
클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 대신, 필요한 자원을(or 그 자원을 만들어주는 팩터리를) 생성자에(or 정적 팩터리나 빌더)넘겨주자. 의존 객체 주입은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.