맞춤법 검사기 클래스를 구현한다고 가정하자. 검사기는 사전(Dictionary) 클래스에 의존하고, 이를 구현할 때 정적 유틸리티 클래스나 싱글톤 클래스로 구현하는 모습을 많이 볼 수 있다.
// Singleton
public class SpellChecker {
private final Dictionary dictionary = new Dictionary();
private SpellChecker() {
// 객체 외부 생성 방지
...
}
public static SpellChecker INSTANCE = new SpellChecker();
public boolean isValid(String word) {
...
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
...
return dictionary.closeWordsTo(typo);
}
}
// Static Utility
public class SpellChecker {
private static final Dictionary dictionary = new Dictionary();
private SpellChecker() {
// 객체 외부 생성 방지
}
public static boolean isValid(String word) {
...
return dictionary.contains(word);
}
public static List<String> suggestions(String typo) {
...
return dictionary.closeWordsTo(typo);
}
}
하지만 사전을 하나만 사용한다는 점에서 좋아보이지는 않는다. 한글, 영어 등 각 언어에 따라 다른 사전이 있다면 위의 클래스는 유연한 대응과 재사용이 불가능할 것이다. 또한 테스트 코드 작성시 Dictionary를 테스트용 모킹 객체로 변경할 수 없기 때문에 SpellChecker에 집중한 테스트 코드를 작성하기 힘들다. 물론 Mockito의 MockStatic을 사용해 모킹할 수 있지만 이보단 객체지향적으로 문제를 해결하는 방법이 권장된다.
간단하게 dictionary 필드에서 final 한정자를 제거하고 사전 교체 메소드를 추가할 수 있지만, 오류를 내기 쉬우며 멀티스레드 환경에서 쓸 수 없다. 여러 클라이언트가 동시에 사전을 교체하게 되면 동시성 문제로 인해 원하는 사전으로 교체되지 않을 가능성이 있다.
이와 같이 사용하는 자원에 따라 동작이 달라지는 클래스는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
이를 위한 가장 간단한 방법은 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다. 이는 의존 객체 주입의 한 형태로 Spring 프레임워크를 사용해본 개발자라면 익숙할 것 이다.
public class SpellChecker {
private final Dictionary dictionary;
public SpellChecker(Dictionary dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) {
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
return dictionary.closeWordsTo(typo);
}
}
생성자 의존 객체 주입을 사용해 의존 관계에 상관없이 유연하게 작동하고, 불변을 보장해 여러 클라이언트가 의존 객체를 안심하고 공유할 수있다. 또한 테스트 용이성도 개선했다. 이 패턴의 변형으로, 생성자에 자원 팩토리를 넘겨 중간 단계를 추상화 시킬 수 있다. 팩토리란 호출할 때마다 특정 타입의 인스턴스를 만들어주는 객체를 말하며 팩토리 메소드 패턴의 구현이라 할 수 있다. 자바의 Supplier<T> 인터페이스를 사용해 간단히 구현할 수 있다. Supplier<T>는 인자는 없고 리턴 타입만 존재하는 메소드를 제공하는 함수형 인터페이스다.
public class SpellChecker {
private final Dictionary dictionary;
public SpellChecker(Supplier<? extends Dictionary> factory) {
this.dictionary = Objects.requireNonNull(factory.get());
}
public boolean isValid(String word) {
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
return dictionary.closeWordsTo(typo);
}
}
public class App {
public static void main(String[] args) {
SpellChecker spellChecker = new SpellChecker(Dictionary::new);
}
}
현재 코드는 단순 객체 생성이기 때문에 불필요한 중간과정이 늘어난 것 같지만 Dictionary 객체 생성 과정에 추가적인 작업이 필요하거나, 매번 생성하기 부담스러운 무거운 객체여서 캐싱이 필요할 경우 팩토리 메소드 패턴을 활용하면 사용성 좋은 코드가 될 것 같다.
의존성이 수 천개나 되는 큰 프로젝트에서는 오히려 코드를 어지럽게 만들 수 있다. Dagger, Guice, Spring 같은 의존 객체 주입 프레임워크를 사용하면 이를 해소할 수 있다.
정리
- 클래스가 내부적으로 하나 이상의 자원에 의존하고, 자원이 클래스 동작에 영향을 준다면 정적 유틸리티 클래스나 싱글턴은 적합하지 않다.
- 생성자 의존 객체 주입을 이용해 클래스의 유연성, 재사용성, 테스트 용이성을 개선할 수 있다.
'java' 카테고리의 다른 글
mockStatic 사용시 자원해제 문제 (0) | 2023.10.03 |
---|---|
Jar 파일 배포시 FileNotFoundException 문제 (0) | 2023.08.06 |
JAVA - 기본 타입(primitive type) (0) | 2023.02.08 |