본문 바로가기
독서 감상문/Effective Java

자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

by 프람6 2024. 4. 18.

아이템05: 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

작성자: 프람

작성일시: 2024_04_18
내용: Effective Java 3/E 2장-아이템5

01. 자원 직접 명시란?

예를 들어 특정 구분자를 통해 문자열을 파싱하는 역할을 가진 클래스가 있다고 가장하자.

우선 같은 역할을 하는 Util class와 Singleton class, 두 가지 예시를 들어보겠다.

  1. 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();
    }
    생략...
    }
  2. 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)을 하고 있다.

즉, 내부에서 자신의 멤버를 직접적으로 명시하고 있다고 볼 수 있다.

이러한 방법의 단점은 유연하지 않다는 점과 테스트가 어렵다는 점이 있다.

  1. 유연하지 않다: 예를 들어 멤버 변수인 Delimiter의 type이 DefaultDelimiter가 아닌 SpecialDelimiter 등 또 다른 경우의 구분자로 대체하고 싶다면?
  2. 테스트가 어렵다: 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]이라 한다.

이렇게 코드를 작성하게 되면 직접 자원 명시의 문제점을 보완할 수 있다.

  1. 유연성 보완
    Delimiter 인터페이스를 외부에서 주입받게 된다면 컴파일 타임이 아닌 런타임에 구체 타입이 정해짐으로 유연성 문제 보완![2]

  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