아이템05: 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
작성자: 프람
작성일시: 2024_04_18
내용: Effective Java 3/E 2장-아이템5
01. 자원 직접 명시란?
예를 들어 특정 구분자를 통해 문자열을 파싱하는 역할을 가진 클래스가 있다고 가장하자.
우선 같은 역할을 하는 Util class와 Singleton class, 두 가지 예시를 들어보겠다.
parse util class
public class ParseUtil { private static Delimiter delimiter = new DefaultDelimiter(); private ParseUtil() { throw new AssertionError("인스턴스화 불가능한 클래스입니다."); } public static List<String> parse(final String plaintext) { return Arrays.stream(plaintext .strip() .split(delimiter.getValue())) .toList(); } 생략... }
singleton class
public class SingletonParseUtil { private final Delimiter delimiter = new DefaultDelimiter(); private SingletonParseUtil() { throw new AssertionError("인스턴스화 불가능한 클래스입니다."); } public static WrongSingleton INSTANCE = new WrongSingleton(); public List<String> parse(final String plaintext) { return Arrays.stream(plaintext.strip().split(delimiter.getValue())) .toList(); } 생략... }
위 두 클래스에서 구분자 역할을 하는 멤버변수인, Delimiter를 할당하는 방법에 주목해보자.
둘 경우 모두 즉시 초기화(Eager initialization)을 하고 있다.
즉, 내부에서 자신의 멤버를 직접적으로 명시하고 있다고 볼 수 있다.
이러한 방법의 단점은 유연하지 않다는 점과 테스트가 어렵다는 점이 있다.
- 유연하지 않다: 예를 들어 멤버 변수인 Delimiter의 type이 DefaultDelimiter가 아닌 SpecialDelimiter 등 또 다른 경우의 구분자로 대체하고 싶다면?
- 테스트가 어렵다: Mock test하려면?
이러한 어려움 때문에 해당 아이템에서는 의존 객체 주입
을 권장하고 있다.
02. 의존 객체 주입이란?
자원 직접 명시와는 다르게 사용하는 되는 사용(의존)할 객체를 외부에서 받아오는 방법을 말한다.
위의 예시를 수정한 의존 객체 주입 예시를 살표보자.
public class ParseUtil {
private final Delimiter delimiter;
public ParseUtil(Delimiter delimiter) {
this.delimiter = Objects.requireNonNull(delimiter);
}
public List<String> parse(final String plaintext) {
return Arrays.stream(plaintext.strip().split(delimiter.getValue()))
.toList();
}
public boolean isValid(final String plaintext) {
return !plaintext.contains(" ");
}
}
의존할 객체를 외부로 부터 받아와 사용하는 방법은 아래와 같다.
- 생성자를 통한 주입
- Setter를 통한 주입
- Interface를 통한 주입
위 코드에서는 생성자를 통한 주입을 사용있는 예시이다.
즉, ParseUtil에서 사용(의존)하고 있는 Delimiter 객체를 외부에서 받아오는 것을 의존 객체 주입(Dedenpency Injection)[1]이라 한다.
이렇게 코드를 작성하게 되면 직접 자원 명시의 문제점을 보완할 수 있다.
유연성 보완
Delimiter 인터페이스를 외부에서 주입받게 된다면 컴파일 타임이 아닌 런타임에 구체 타입이 정해짐으로 유연성 문제 보완![2]테스트의 어려움
ParseUtil테스트 시, 테스트 용도의 Delimiter를 별도로 의존성 주입을 통해 테스트.
03. 응용
지금부터 의존 객체 주입을 효과적으로 사용하는 방법에 대해 소개하겠다.
팩토리 메서드 패턴[3]에서 부터 Supplier(Functional Interface[4]) 적용까지 점진적으로 효과적인 코드로 개선하는 예시를 보이겠다.
우선 전체적으로 예시 코드에 대해 설명하겠다.
Delimiter interface
public interface Delimiter { //생략... }
DefaultDelimiter Impl class
public class DefaultDelimiter implements Delimiter { //생략... }
SpecialDelimiter Impl class
public class SpecialDelimiter implements Delimiter { //생략... }
Parser Client class
public class Parser { private final Delimiter delimiter; public Parser(Delimiter delimiter) { this.delimiter = delimiter; } //생략... }
03-01. 팩터리 메서드 패턴 응용
추가된 코드
- DelimiterFactory abstract class
public abstract class DelimiterFactory {
public abstract Delimiter create();
}
DefaultDelimiterFactoy concrete class
public class DefaultDelimiterFactory extends DelimiterFactory{ @Override public Delimiter create() { return new DefaultDelimiter(); } }
SpecialDelimiterFactory concrete class
public class SpecialDelimiterFactory extends DelimiterFactory { @Override public Delimiter create() { return new SpecialDelimiter(); } }
이렇게 추상 팩토리 상속하는 구체 클래스들이 객체의 생성을 책임 지게 된다면, Client code는 아래처럼 수정된다.
public class Parser {
//생략...
private final Delimiter delimiter;
public Parser(DelimiterFactory factory) {
this.delimiter = factory.create();
}
//생략...
}
이러써 우리는 더 유연한(결합도가 낮은) 코드를 얻을 수 있게 되었다.
03-02. Supplier
적용
Factory를 사용하는 코드 대신 Supplier를 매개변수로 하여 단순화 하는 방법도 소개하고 있다.
public class Parser {
//생략...
private final Delimiter delimiter;
public Parser(DelimiterFactory factory) {
this.delimiter = factory.create();
}
public Parser(Supplier<? extends Delimiter> delimiterSupplier) {
this.delimiter = delimiterSupplier.get();
}
//생략...
}
생성 예시
@Test void create_parse() { Parser parser = new Parser(() -> new DefaultDelimiterFactory().create()); }
04. 결론
다형성을 이용해서 객체간의 결합도를 줄어주자. 그 방법 중 하나가 의존성 외부 주입이다.
참고자료
[1] Dependency Injection
[2] 의존성(Dependency)이란? 컴파일타임 의존성과 런타임 의존성의 차이 및 비교
[3] 팩토리 메서드 패턴
[4] Functional Interface
'독서 감상문 > Effective Java' 카테고리의 다른 글
로 타입은 사용하지 말라 (0) | 2024.05.06 |
---|---|
태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2024.05.03 |
상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (2) | 2024.05.02 |
Comparable을 구현할지 고려하라 (0) | 2024.04.25 |
인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.04.18 |