들어가기 전
이 글은 인프런 백기선님의 강의를 공부하며 정리한 글입니다
아이템5. 자원을 직접 명시하지 말고 의존객체 주입을 사용하라.
아이템 5의 핵심을 정리하면 다음과 같다.
- 사용하는 자원에 따라 동작이 달라지는 클래스는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
- 의존 객체 주입(Dependency Injection)이란 인스턴스를 생성할 때 필요한 자원을 넘겨주는 방식이다.
- DI의 변형으로 생성자에 자원 팩터리를 넘겨줄 수 있다.
- DI를 사용하면 클래스의 유연성, 재사용성, 테스트 용이성을 개선할 수 있다.
모든 경우에 대해서 DI를 사용해야하는 것은 아니다. DI를 사용하면 좋은 경우는 다음과 같다.
- 자원을 직접 명시해서 사용한다.
- 사용하는 자원의 종류에 따라 다른 동작을 한다.
위 두 가지 조건을 만족하는 경우라면 DI를 이용해서 코드를 개선해 볼 수 있다.
사용하는 자원에 따라 동작이 달라지는 클래스
여러 종류의 사전이 존재하고, 사전의 정보를 읽어서 필요한 처리를 해주는 SpellChecker 클래스가 존재한다고 가정해보자. SpellChecker의 코드를 확인해보면 다음과 같다.
// static 형태의 SpellChecker
public class SpellChecker {
private static final Dictionary dictionary = new Dictionary();
public static boolean isValid(String word) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.contains(word);
}
public static List<String> suggestions(String typo) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.closeWordsTo(typo);
}
}
위 코드를 살펴보면 다음 사실을 알 수 있다.
- SpellChecker는 의존 객체로 Dictionary를 가진다.
- SpellChecker는 어떤 사전이냐에 따라 하는 동작이 달라진다.
- 예를 들어 영어 사전, 독일어 사전, 한국어 사전에 따라서 SpellChecker는 서로 다른 결과를 내놓게 될 것이다.
가장 먼저 테스트 코드를 작성할 수 있는지를 확인해보고자 한다.
class SpellCheckerTest {
@Test
void isValid() {
// 이 경우, SpellChecker 내부에서 사용하는 딕셔너리를 바꿀 수 있는 방법이 없다.
// SpellChecker의 의존 객체와 테스트에서 격리성이 필요한데, 그럴 수 없어서 문제가 된다.
Assertions.assertTrue(SpellChecker.isValid("test"));
}
}
결론부터 이야기하면 테스트 코드를 작성하기가 어렵다.
SpellChecker는 내부에서 Dictionary()를 직접 생성해서 가지고 있다. 이것은 사용하는 자원을 직접 명시하는 경우다. 따라서 테스트 코드에서 다른 Dictonary를 사용해보거나, 혹은 Mock 객체를 생성해서 SpellChecker를 테스트 할 수가 없게 된다. 테스트 코드의 유연성이 떨어진다는 것을 의미한다. 이것은 의존 객체의 종류에 따라 다른 동작을 하는 클래스가 내부에서 자원을 직접 명시(생성)해서 사용하기 때문에 발생하는 문제다.
다시 한번 정리하지만 자원을 명시한다는 것은 내부에서 직접 자신이 사용할 자원을 결정해서 생성하는 것을 의미하기도 한다. 이처럼 자신이 쓸 자원을 내부에서 명시한다는 것은 코드의 유연성과 테스트의 용이성을 떨어뜨리는 행위가 된다.
// 싱글톤 형태의 SpellChecker
public class SpellChecker {
private final Dictionary dictionary = new Dictionary();
public static final SpellChecker spellChecker = new SpellChecker();
private SpellChecker() {
}
public boolean isValid(String word) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.closeWordsTo(typo);
}
}
이것은 SpellChecker를 싱글톤 형태로 사용하더라도 동일하다. 이 문제는 자신이 사용하는 의존 객체를 자신이 직접 생성(명시)하기 때문에 발생한 것이기 때문이다.
코드의 유연성이 떨어진다?
코드의 유연성은 왜 떨어진다고 하는 것일까? 만약 SpellChecker가 KoreanDictionary를 사용하다가, EnglishDictionary를 사용한다고 가정해보자. 그렇다면 SpellChecker 클래스에서 바껴야 할 부분은 얼마나 있을까?
public class SpellChecker {
private static final Dictionary dictionary = new Dictionary();
public static boolean isValid(String word) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.contains(word);
}
public static List<String> suggestions(String typo) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.closeWordsTo(typo);
}
}
위 스펠체커 클래스 코드가 존재한다. 이 때, Dictionary를 koreanDictionary로 바꾸면 어떤 코드가 바뀌어야 할지 생각해보자.
public class SpellChecker {
// private static final Dictionary dictionary = new Dictionary();
private static final KoreanDictionary dictionary = new KoreanDictionary();
public static boolean isValid(String word) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.containsKorean(word);
// return dictionary.contains(word);
}
public static List<String> suggestions(String typo) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.closeWordsToKorean(typo);
// return dictionary.closeWordsTo(typo);
}
}
위와 같이 많은 변경점이 발생할 수 있다.
- KoreanDictionary의 내부 메서드는 Dictionary 클래스의 내부 메서드와 다른 이름과 다른 매개변수를 받아서 동작할 수도 있다.
- KoreanDictionary는 Dictionary와 다른 클래스 일 수 있기 때문에 이 부분 역시 수정이 필요하다.
즉, 의존 객체 하나를 바꾸는데에 있어서 정말 많은 내부 코드의 수정이 필요한 상황이 된다. 이런 의미 때문에 코드의 재사용성, 코드의 유연성이 떨어진다고 표현했다. 그리고 다시 한번 이야기 하지만 이 문제는 자신이 사용할 의존 객체를 스스로 내부에서 정해서 사용했기 때문에 발생했다. 그렇다면 이 부분은 어떻게 해결할 수 있을까?
의존성 주입(DI)을 사용해라.
SpellChecker는 가지고 있는 의존 객체의 타입에 따라서 동작이 바뀌는 클래스다. 따라서 이 경우 DI를 이용하면 코드의 재사용성, 유연성, 테스트의 용이성이 개선된다. 이 때의 전제는 의존 객체가 반드시 인터페이스여야 한다는 점이다. 가장 먼저 의존 객체를 인터페이스로 생성한다. 인터페이스를 생성하면 DI를 할 때도 편리하며, 테스트 코드를 작성할 때 Mock 객체 역시 손쉽게 생성할 수 있다.
public interface Dictionary {
boolean contains(String word);
List<String> closeWordsTo(String typo);
}
public class DefaultDictionary implements Dictionary{
public boolean contains(String word) {
return false;
}
public List<String> closeWordsTo(String typo) {
return null;
}
}
의존 객체에 인터페이스를 넣어주었다면, 이제는 DI (의존성 주입)을 하도록 코드를 변경하면 된다.
- 생성자를 통해서 의존 객체를 주입받도록 했다.
- DI는 생성자를 통해서 주입 받을 수도 있지만, Setter를 이용하는 방식도 있을 수 있다. 외부에서 의존 객체를 주입 받을 수 있도록 통로를 열어주기만 하면 된다.
public class SpellChecker {
private final Dictionary dictionary;
public SpellChecker(Dictionary dictionary) {
this.dictionary = dictionary;
}
public boolean isValid(String word) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.contains(word);
}
public List<String> suggestions(String typo) {
// TODO : SpellChecker 코드 로직 구현
return dictionary.closeWordsTo(typo);
}
}
위와 같이 코드를 작성하게 되면 코드의 재사용성, 확장성, 테스트 용이성이 모두 개선된다.
- 인터페이스 타입으로 의존 객체를 받기 때문에 공통된 메서드가 사용된다. 따라서 내부 클래스에서 코드의 변경점이 최소화 된다.
- 인터페이스 타입을 도입했기 때문에 Mock 객체를 생성해서 의존성 주입할 때도 매우 편리하게 동작한다.
정리
IOC (Inversion of Controll)은 제어의 역전을 의미한다. 이것은 SpellChecker에서 사용할 리소스를 SpellChecker의 밖에서 주입해주는 것을 의미한다. 이렇게 작성하면 SpellChecker의 코드를 그대로 사용하면서, 다양한 타입의 의존 객체를 사용할 수도 있다. 또한 테스트 코드도 작성하기 편리해진다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Effective Java : 아이템5 완벽공략 (0) | 2023.02.26 |
---|---|
Effective Java : 아이템2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2023.02.26 |
Effective Java : 아이템4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2023.02.25 |
Effective Java : 아이템3 완벽 공략 (0) | 2023.02.25 |
Effective Java : 아이템3. 생성자나 열거 타입으로 싱글턴임을 보증하라. (0) | 2023.02.25 |