[Java][Effective Java] item 5, 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

2022. 5. 17. 16:55JAVA/Effective Java

많은 클래스가 하나 이상의 자원에 의존합니다. 예를 들어 맞춤법 검사기는 사전(dictionary)에 의존하는데, 이러한 의존성을 가지는 클래스를 다음과 같이 적절하지 못하게 정의한 사례가 있습니다.

1. 의존성을 가지고 있는 클래스를 적절치 못하게 정의한 사례 : 정적 유틸리티 클래스로 정의

public class SpellChecker {
	private static final Lexicon dictionary = new Lexicon();
	
	// 객체 생성 방지
	private SpellChecker() {
		throw new AssertionError();
	}
	
	public static boolean isValid(String word) {
		return false;
	}
	
	public static List<String> suggestions(String type){
		return null;
	}
}

2. 의존성을 가지고 있는 클래스를 적절치 못하게 정의한 사례 : 싱글턴 클래스로 정의

public class SpellChecker2 {
	private final Lexicon dictionary = new Lexicon();
	public static SpellChecker2 INSTANCE = new SpellChecker2();
	
	// 객체 생성 방지
	private SpellChecker2() {
		
	}
	
	public boolean isValid(String word) {
		return false;
	}
	
	public List<String> suggestions(String type){
		return null;
	}
	
	public static void main(String[] args) {
		SpellChecker2 spellChecker2 = SpellChecker2.INSTANCE;
		spellChecker2.isValid("test");
	}
}

맞춤법 검사기(SpellCheck) 클래스가 정적 유틸리티 클래스 또는 싱클턴 클래스로는 적절하지 않은 이유는 다음과 같습니다.

  • 사전(Lexicon)은 언어별로 존재할 수 있고 특수한 용도의 어휘용 사전이 별도로 필요할 수 있음
  • 단순히 사전 클래스 한가지만으로는 모든 단어를 대응할 수는 없음

위와 같은 이유로 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않을 수 있습니다.

 

3. 의존 객체 주입 패턴

의존 객체 주입 패턴은 인스턴스를 생성할때 생성자에 필요한 자원을 넘겨주는 방식을 의미합니다.

public class SpellChecker3 {
	private final Lexicon dictionary;

	// 생성자를 이용해서 의존성 주입 수행
	public SpellChecker3(Lexicon dictionary) {
		this.dictionary = dictionary;
	}
	...
	
	public boolean isValid(String word) {
		System.out.println("call is Valid");
		return false;
	}
	
	public List<String> suggestions(String type){
		System.out.println("call suggestions");
		return null;
	}
	...
}
class KoreaDictionary extends Lexicon{}

Lexicon koreaLexicon = new KoreaDictionary();
SpellChecker3 sc1 = new SpellChecker3(koreaLexicon);

위와 같이 맞춤법 검사기 인스턴스를 생성할때 생성자로 필요한 Lexicon 인스턴스를 전달하여 주입할 수 있습니다.

 

3. 의존 객체 주입 패턴의 응용 : 팩토리 메서드 패턴

팩토리 메서드 패턴은 의존 객체 주입의 응용으로써 생성자에 자원 팩토리를 넘겨주는 방식입니다. 여기서 팩토리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말합니다.

public class SpellChecker3 {
	private final Lexicon dictionary;

	...
    
	// 생성자에 자원 팩토리를 전달하여 의존 주입 수행
	// 한정적 와일드카드 타입을 사용해 팩토리의 타입 매개변수를 제한함
	// 예제의 팩토리의 타입 매개변수는 팩토리 타입이 Lexicon 또는 Lexicon의 하위 클래스만을 받음
	public SpellChecker3(Supplier<? extends Lexicon> lexiconFactory) {
		this.dictionary = lexiconFactory.get();
	}
	
	public boolean isValid(String word) {
		System.out.println("call is Valid");
		return false;
	}
	
	public List<String> suggestions(String type){
		System.out.println("call suggestions");
		return null;
	}
	
	public static void main(String[] args) {
		Lexicon koreaLexicon = new KoreanDictionary();
		Lexicon englishLexicon = new EnglishDictionary();
		String lang = "eng";
		
		SpellChecker3 sc1 = new SpellChecker3(koreaLexicon);
		SpellChecker3 sc2 = new SpellChecker3(new Supplier<Lexicon>() {
			@Override
			public Lexicon get() {
				if(lang.equals("kor")) {
					return koreaLexicon;
				}
				else {
					return englishLexicon;
				}
				
			}
		});
		SpellChecker3 sc3 = new SpellChecker3(() -> koreaLexicon);	// 람다 표현식	
	}
}

위의 예제에서 Supplier<T> 인터페이스가 팩토리를 표현할 수 있습니다. 이 팩토리 인스턴스는 람다 표현식처럼 넘길수 있고 조건문을 통해서 문자열의 내용에 따라서 다른 Lexicon 인스턴스를 전달할 수 있습니다. 그리고 생성자로 팩토리를 받을 때 한정적 와일드카드 타입을 사용하여 특정 타입의 팩토리만을 받을 수 있습니다.

public SpellChecker3(Supplier<? extends Lexicon> lexiconFactory) {
		this.dictionary = lexiconFactory.get();
}

 

정리하며

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋습니다. 이 자원들을 클래스가 직접 만들게 해서도 안됩니다. 대신 필요한 자원을(혹은 그 자원을 만들어주는 팩토리를) 생성자에(혹은 정적 팩터리나 빌더)에 넘겨줍니다. 의존 객체 주입 기법은 클래스의 유연성 재사용성, 테스트 용이성을 개선해줍니다.

 

References

source code : https://github.com/yonghwankim-dev/effective_java/tree/master/src/role5
[도서] effective java
[Effective Java] 아이템5 - 자원은 직접 명시하지 말고 의존 객체 주입을 사용하라