이펙티브자바, 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
싱글턴과 정적유틸리티가 외부 객체를 사용할 때
- 아래는 정적유틸리티를 구현한 형태이다.
- 클래스 내부에서 필요한 객체의 인스턴스를 생성하고 static 매서드로 활용한다.
public class SpellCheckerV1 {
private static final Lexicon dictionary = new Lexicon();
public static boolean isValid(String word) {
return dictionary.contains(word);
}
}
- 아래는 싱글턴 형태이다.
- 싱글턴으로 사용할 객체만 static이며 나머지는 인스턴스를 통해 사용한다.
public class SpellCheckerV2 {
private final Lexicon dictionary = new Lexicon();
private static final SpellCheckerV2 INSTANCE = new SpellCheckerV2();
private SpellCheckerV2(){}
public static SpellCheckerV2 getInstance(){
return INSTANCE;
}
public boolean isValid(String word){
return dictionary.contains(word);
}
}
- 하지만 필드에서 초기화를 하는 방식은 유연하지 않다. Lexicon 객체를 상황에 따라 다른 값으로 넣는 것이 나을 수 있는데, 이러한 가능성을 없애버린다.
- 그러므로 싱글턴으로 사용할 객체 자체를 외부에서 주입하는 방식을 택한다. 이를 의존 객체 주입이라 한다.
의존 객체 주입 : 유연성과 테스트 용이성을 높여준다.
- 아래의 코드는 생성자로 객체를 구현할 때, 필요로 한 객체를 주입하는 형태로 진행한다.
- 생성자에서 필요한 데이터를 주입하기 때문에, final 로 구현할 수 있다.
- 해당 객체를 인터페이스나 서플라이어, 와일드 카드 등 다형성을 보장할 수 있다.
public class SpellCheckerV3 {
private final Lexicon dictionary;
public SpellCheckerV3(Lexicon dictionary){
this.dictionary = dictionary;
}
public boolean isValid(String word){
return dictionary.contains(word);
}
}
- 이에 대한 수많은 장점이 있지만, 나에게 가장 큰 장점은 테스트 코드 작성에 있었다.
- 싱글턴을 테스트 코드를 작성하면 아래와 같은 형태가 된다.
- 아래의 문제는 SpellCheckerV2 객체와 그 내부에 있는 Lexicon 객체에 대한 통제가 아예 불가능하다. 테스트 자체가 사실상 어렵다.
@Test
void test(){
final SpellCheckerV2 instance = SpellCheckerV2.getInstance();
final boolean kim = instance.isValid("kim");
System.out.println("kim = " + kim);
}
- 하지만 의존 형태로 할 경우 태스트가 매우 자유롭다.
- Lexicon에 대하여 구현할 수 있다.
@Test
void test(){
Lexicon dictionary = new Lexicon();
SpellCheckerV3 spellCheckerV3 = new SpellCheckerV3(dictionary);
final boolean kim = spellCheckerV3.isValid("kim");
System.out.println("kim = " + kim);
}
스프링과 테스트 코드 작성
- 사실, 이러한 패턴은 스프링에서 자주 봤다.
- DI를 필드주입 할 경우 일종의 싱글턴과 같은 형태로 소스코드가 작성된다.
@Service
public class TestService {
@Autowired
private TestRepository testRepository;
public boolean getOne(Long id){
return testRepository.getOne(id);
}
}
class TestServiceTest {
@Test
void test(){
TestService testService = new TestService();
testService.getOne(120l); // 예외가 발생한다. 왜냐하면 필드 TestRepository는 DB와 커넥션이 필요로 한다. 이는 SpringBoot가 로딩되지 않으면 동작하지 않는다. 유닛 테스트가 불가능하다.
}
}
- 하지만 생성자를 통해 주입할 경우 테스트 코드 작성이 가능하다. 자원으로 사용하는 객체에 대하여 목 데이터를 생성할 수 있다. 의존 객체 주입은 테스트 코드를 위해서 아주 좋은 형태이다.
@Service
@RequiredArgsConstructor
public class TestService {
private final TestRepository testRepository;
public boolean getOne(Long id){
return testRepository.getOne(id);
}
}
class TestServiceTest {
@Test
void test(){
// given
TestRepository testRepository = new TestRepository(){
@Override
public boolean getOne(Long id) {
if(id<=0){
throw new IllegalArgumentException();
}
return true;
}
}; // 유닛테스트가 가능하다. 적당한 형태로 해당 객체를 구현할 수 있다.
// when
TestService testService = new TestService(testRepository);
final boolean result = testService.getOne(120l);
// then
Assertions.assertThat(result).isTrue();
}
}