Java/Effective Java

[이펙티브 자바] 아이템 05.자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

메성 2020. 7. 28. 17:33
반응형

많은 클래스들은 하나 이상의 자원에 의존하고 있다.

예를 통해 살펴보자. 보통 맞춤법 검사기(SpellChecker)는 사전(Lexicon)에 의존하고 있다.


정적 유틸리티 클래스와 싱글턴을 활용한 맞춤법 검사기

그럼 아이템03에서 언급한 싱글턴아이템04에서 언급한 정적 유틸리티 클래스을 활용하여 소스를 구성해보자.

//아이템 03_싱글턴
public class SpellChecker {
  private static final Lexicon dictionary = ...;    //사전에 의존

  private SpellChecker(){}

  public static SpellChecker INSTANCE = new SpellChecker(...);    //싱글턴 활용

  public static boolean isValid(String word) {...}
  public static List<String> suggestions(String typo) {...}
}

//아이템 04_정적 유틸리티 클래스
public class SpellChecker {
  private static final Lexicon dictionary = ...;    //사전에 의존

  private SpellChecker(){}

  public static boolean isValid(String word) {...}
  public static List<String> suggestions(String typo) {...}
}

위 소스를 보는 바와 같이 두 가지의 방법의 SpellChecker 클래스는 단 하나의 사전만 의존하고 있는 것을 볼 수 있다.

  • Lexicon dictionary가 final로 선언되어 있다.

하지만 실전에서 맞춤법 검사기는 하나의 사전만 사용하는 것이 아니라, 사전이 언어별로 따로 있을 수 있고, 어휘용 사전도 별도로 사용할 수도 있기 때문에 여러 사전이 필요하다는 것을 알 수 있을 것이다. 또한, 사전에 따라 맞춤법 검사의 방법이 달라지므로 사전이 맞춤법 검사 동작에 영향이 있는 것도 알 수 있다.

즉, 클래스가 내부적으로 하나 이상의 자원을 의존하고, 자원에 따라 클래스 동작에 영향을 주었을 때는 아이템 03과 아이템 04의 방식은 효율적인 방법이 아닌 것이다.

 

 

여러 사전을 사용할 수 있는 맞춤법 검사기

그럼 이제 SpellChecker가 여러 사전을 사용할 수 있도록 소스를 재구성해보자.

소스를 재구성하기 전에 변경해야할 부분을 생각해보면, dictionary 필드의 final을 제거하고 다른 사전으로 교체할 수 있는 메서드를 추가하는 방법으로 수정하면 되겠지만 해당 방법은 멀티 스레드 환경에서 사용할 수 없다는 단점이 있다.

멀티 스레드 환경에서는 다른 스레드에 의해 사용하려는 사전이 아닌 다른 사전으로 변경이 가능하다.

여러 사전을 사용할 수 있는 맞춤법 검사기(SpellChecker) 클래스의 경우 하나 이상의 자원을 의존하고 있고, SpellChecker가 원하는 자원을 사용해야 한다는 특징을 가지고 있다. 이런 경우에 가장 만족하는 패턴이 있으니, 그것이 바로 의존 객체 주입 패턴 이다.

의존 객체 주입 패턴 : 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.

public class SpellChecker {
  private final Lexicon dictionary;

  //의존 객체 주입 패턴 활용
  public SpellChecker(Lexicon dictionary) {
    this.dictionary = Objects.requireNonNull(dictionary);
  }

  public boolean isValid(String word) {...}
  public List<String> suggestions(String typo) {...}
}

의존 객체 주입 패턴의 경우 여러 자원을 생성자를 통해 받을 수 있고, final로 인해 불변을 보장하고 있는 것을 확인할 수 있다. 의존 객체 주입 같은 경우 위에서 보듯 생성자 뿐만 아니라 정적 팩터리 메서드, 빌더 모두에 똑같이 응용할 수 있다.

 

 

팩터리 메서드 패턴을 활용한 맞춤법 검사기

위의 의존 객체 주입 패턴의 변형으로 사용할 만한 것은 생성자에 자원 팩터리를 넘겨주는 방식이다. 여기서 말하는 팩터리는 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말하는 것이다. 즉, 팩터리 메서드 패턴을 구현한 것이다.

//팩터리 메서드 패턴 예

//Lexicon
public class Lexicon {
    public void print() {
    }
}
//다른 종류의 사전1
public class Dictionary1 extends Lexicon {
    @Override
    public void print() {
        System.out.println("Dictionary1");
    }
}
//다른 종류의 사전2
public class Dictionary2 extends Lexicon {
    @Override
    public void print() {
        System.out.println("Dictionary2");
    }
}

//인스턴스를 생성할 팩토리 추상 클래스
abstract class Factory {
    public final Lexicon create(String dic) {
        return createProduct(dic);
    }
    protected abstract Lexicon createProduct(String dic);
}

//실질적으로 인스턴스를 생성할 팩토리 클래스
public class LexiconFactory extends Factory{
    @Override
    protected Lexicon createProduct(String dic) {
        if(dic.equals("no1")) {
            return new Dictionary1();
        } else if(dic.equals("no2")) {
            return new Dictionary2();
        }
    }
}

//SpellChecker 사용 예
public class SpellChecker {
    private Lexicon dictionary = null;

    //의존 객체 주입 패턴 활용
    public SpellChecker(String dic) {
        Factory factory = new LexiconFactory();
        this.dictionary = factory.create(dic);
        this.dictionary.print();
    }

    ...
}

위에서 보는 것 처럼, 팩터리 메서드 패턴을 사용하게 되면 클라이언트는 생성하고자 하는 사전의 키 값(String : no1, no2)을 알면 팩터리에 의해 클래스를 대신 인스턴스화할 수 있는 것이다.

하지만, 이 보다도 Java8에서 소개한 Supplier를 활용하면 더욱 완벽한 팩터리를 표현한 것이다.

public class SpellChecker {
    private Lexicon dictionary = null;

    public SpellChecker(Supplier<? extends Lexicon> lexiconFac) {
        this.dictionary = lexiconFac.get();
        this.dictionary.print();
    }

        ...
}

Supplier를 간단히 설명하면, Supplier를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입을 사용해 팩터리의 타입 매개변수를 제한하는데, 위에서는 Lexicon의 하위 타입들만 생성하고 있는 것이다.

 


의존 객체 주입 패턴의 경우 유연성과 테스트 용이성을 개선해주지만 실무 프로젝트의 경우 한 클래스에서 의존성이 많아 코드를 매우 어지럽게 만들기도 한다. 이런 경우 의존 객체 주입 프레임워크를 사용하면 위 같은 단점을 해소할 수 있다. 스프링과 같은...

전체적으로 정리하면, 클래스가 내부적으로 하나 이상의 자원을 의존하고 있고, 해당 자원에 의해서 클래스 동작에 영향을 받을 때에는 싱글턴이나 정적 유틸리티 클래스 형식으로 구성하는 것은 옳지 않다. 또한, 이 자원들을 사용하는 클래스가 직접 만드는 것도 옳지 않다(멀티 스레드 환경에서의 단점). 해당 클래스에서 필요한 자원을 생성하는 역할은 외부 클라이언트에게 넘겨주도록 하자!!

그저 역할을 넘겨주는 것만으로도 우리는 클래스의 유연성, 재사용성, 테스트 용이성이라는 장점을 얻을 수 있다.


이펙티브 자바 Effective Java 3/E
국내도서
저자 : 조슈아 블로크(Joshua Bloch) / 이복연(개앞맵시)역
출판 : 인사이트 2018.11.01
상세보기
반응형