
자원이 여러 개 필요할 때 정적 유틸리티 클래스나 싱글턴을 사용하는 것은 좋지 않다.
필요한 자원이 여러 개일 수 있다
필드에서 final 한정자를 제거하고 자원을 변경하는 메서드를 추가한다면?
public class SpellChecker {
private Dictionary dictionary;
public void setDictionary(Dictionary dictionary) { this.dictionary = dictionary; }
}
setDictionary()를 동시에 호출하면 데이터 불일치 문제가 발생할 수 있다.setDictionary(new KoreanDictionary())를 호출한 후,setDictionary(new EnglishDictionary())를 호출하게 된다면dictionary 필드가 KoreanDictionary일 수도 있고, EnglishDictionary일 수도 있게되버린다.dictionary가 null이 될 가능성이 높아 NullPointerException 발생 위험이 생긴다.final을 사용하면 dictionary를 생성자에서 반드시 초기화해야 하므로 오류가 발생할 위험이 없어지는데 final을 없애버린다면setDictionary()를 통해서 값을 주입하지 않고 다른 메서드를 사용하게 되면 dictionary가 null이 될 수 있는 것이다.public class SpellChecker {
private final Dictionary dictionary;
// 생성자 주입
public SpellChecker(Dictionary dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
}
Dictionary 구현체를 주입할 수 있..final로 선언하여 dictionary가 변경될 위험이 없다.MockDictionary 등을 주입해서 테스트가 가능하다.public class TextProcessor {
private final SpellChecker spellChecker;
private final GrammarChecker grammarChecker;
public TextProcessor(SpellChecker spellChecker, GrammarChecker grammarChecker) {
this.spellChecker = spellChecker;
this.grammarChecker = grammarChecker;
}
}
// 정적 팩터리에 의존 객체 주입 적용
public class SpellChecker {
private final Dictionary dictionary;
// private 생성자: 직접 생성 막음
private SpellChecker(Dictionary dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public static SpellChecker from(Dictionary dictionary) {
return new SpellChecker(dictionary); // 의존 객체 주입
}
}
// 사용
Dictionary koreanDictionary = new KoreanDictionary();
SpellChecker spellChecker = SpellChecker.from(koreanDictionary);
// 빌더에 의존 객체 주입 적용
public class SpellChecker {
private final Dictionary dictionary;
private final GrammarChecker grammarChecker;
private SpellChecker(Builder builder) {
this.dictionary = Objects.requireNonNull(builder.dictionary);
this.grammarChecker = Objects.requireNonNull(builder.grammarChecker);
}
public static class Builder {
private Dictionary dictionary;
private GrammarChecker grammarChecker;
public Builder dictionary(Dictionary dictionary) {
this.dictionary = dictionary;
return this;
}
public Builder grammarChecker(GrammarChecker grammarChecker) {
this.grammarChecker = grammarChecker;
return this;
}
public SpellChecker build() {
return new SpellChecker(this); // 의존 객체 주입
}
}
}
// 사용
SpellChecker spellChecker = new SpellChecker.Builder()
.dictionary(new KoreanDictionary())
.grammarChecker(new BasicGrammarChecker())
.build();
팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체이다.
Supplier<T> 인터페이스를 사용한 의존 객체 주입public class SpellChecker {
private final Supplier<Dictionary> dictionarySupplier;
public SpellChecker(Supplier<? extends Dictionary> dictionarySupplier) {
this.dictionarySupplier = dictionarySupplier;
}
public void checkSpelling() {
Dictionary dictionary = dictionarySupplier.get(); // 필요할 때 사전 가져오기
}
}
? extends T)을 사용Supplier<? extends Dictionary>와 같이 한정적 와일드 카드 타입을 사용해서 Dictionary를 **상속하는 모든 객체(Supplier 등)를 받을 수 있도록 허용한다.Dictionary를 다룰 수 있다.
Supplier<T>란?
- 호출될 때마다 새로운 객체를 반환하는 함수형 인터페이스
- 매번 새로운 객체를 만들거나 미리 생성된 객체를 제공하는 데 유용
앞에서 계속 설명한 것처럼 의존 객체 주입을 사용하면 클래스의 유연성, 테스트 용이성이 증가한다.
Dictionary 구현체를 쉽게 교체할 수 있게되어서 다양한 환경에서 활용이 가능해진다.SpellChecker spellChecker = new SpellChecker(new MockDictionary());SpellChecker의 생성자에 Dictionary를 직접 생성한다면 맞춤법 검사 뿐만아니고 객체 생성과 교체 기능까지 책임을 지게 된다. Dictionary를 변경하려면 내부 코드를 변경해야 되는 것이다.SpellChecker는 Dictionary가 어떤 구현체인지 신경 쓰지 않고, 오직 맞춤법 검사만 담당하게 되어서 단일 책임 원칙이 유지된다.“코드를 어지럽게 만든다”는 말의 의미가 뭘까?
프로젝트가 커질수록 의존성이 많아지면 매번 new 키워드로 객체를 생성하고 주입하는 것이 번거롭고 관리하기 어렵다라는 뜻인것 같다.
아래는 Spring 프레임워크 코드 예시이다.
@Component
public class SpellChecker {
private final Dictionary dictionary;
@Autowired
public SpellChecker(Dictionary dictionary) {
this.dictionary = dictionary;
}
}
@Component 애너테이션SpellChecker는 @Component로 등록되어 있어서 Spring 컨테이너가 관리하는 빈(Bean)이 된다.@Autowired@Autowired가 붙은 생성자는 Spring이 자동으로 매개변수에 알맞은 빈을 찾아서 주입해준다.Dictionary 객체를 찾는다.Dictionary도 빈으로 등록되어 있어야한다)Dictionary 타입의 객체를 찾는다.Supplier<T>)를 활용하면 더 유연한 객체 생성이 가능하다.