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

김대협·2023년 1월 3일
0

Effective Java 3rd

목록 보기
5/9

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

자원을 직접 명시하지 않고 의존 객체 주입을 사용해야 하는 클래스는 사용하는 자원에 따라 동작이 달라지는 클래스의 경우에 사용할 수 있다.

의존 객체 주입이 필요한 이유


단순한 예제로 사전의 경우를 예를 들어 잘못 사용된 사례는 아래와 같다.
사전은 다양한 언어를 사용할 수 있다고 가정해보자.

정적 유틸리티를 잘못 사용한 예

public class StaticUtilitySpellChecker {
    // 자원을 직접 명시하여 사용
    private static final Dictionary dictionary = new DefaultDictionary();

    // 객체 생성 방지
    private StaticUtilitySpellChecker() {
    }

    public static boolean isValid( String word ) {
        return dictionary.isValid( word );
    }

    public static List<String> suggestions( String typo ) {
        return Arrays.asList( "1", "2" ); // 임시 구현
    }
}

싱글턴을 잘못 사용한 예

public class SingletonSpellChecker {
    // 자원을 직접 명시하여 사용
    private final Dictionary dictionary = new DefaultDictionary();
    public static SingletonSpellChecker INSTANCE = new SingletonSpellChecker();

    private SingletonSpellChecker() {
    }

    public boolean isValid( String word ) {
        return dictionary.isValid( word );
    }

    public List<String> suggestions( String typo ) {
        return Arrays.asList( "1", "2" ); // 임시 구현
    }
}

위에 제시한 두 가지 구현 방식의 문제점

  • 유연하지 않다. (다른 자원을 효율적으로 교체할 수 없고 재생성하여야 한다.)
  • 테스트하기 어렵다.

유연성을 위해 필드에서 final을 제거하고 교체할 수 있지만 그 방식에서도 문제점이 발생한다.

  • 멀티스레드 환경에서 문제 발생 (Mutable 객체 참조 시점에 따라 변화한다.)

의존 객체 주입을 사용한 예


의존 객체 주입의 한 가지 예로 클라이언트가 원하는 자원을 제공하고 인스턴스 생성 시점에 필요한 자원을 넘겨주는 방식이다.

public class SpellChecker {

    // 의존 객체를 주입하여 사용.
    private static final Dictionary dictionary;
    public SpellChecker(Dictionary dictionary) {
        this.dictionary = dictionary;
    }

    public boolean isValid( String word ) {
        return dictionary.isValid( word );
    }

    public List<String> suggestions( String typo ) {
        return Arrays.asList( "1", "2" ); // 임시 구현
    }
}

의존 객체 주입은 유연성과 재사용성, 테스트 용이성을 높여준다.
의존 객체 주입은 생성자, 정적 팩터리, 빌더에 모두 응용할 수 있다.

생성자에 자원 팩토리를 넘겨주는 방식

쓸만한 변형 방식으로 생성자에 자원 팩터리를 넘겨주는 방식이 있고, Supplier 인터페이스가 팩터리를 표현한 완벽한 예이다.

설명의 팩터리는 Factory Method Pattern 을 일컫는 설명으로 이해하기 바란다.

public class SpellChecker {
    Dictionary dictionary;

    // 의존 객체 주입을 사용한 예
    public SpellChecker( Dictionary dictionary ) {
        this.dictionary = dictionary;
    }

    // 자원 팩터리를 넘겨주는 방식
    public SpellChecker( DictionaryFactory dictionaryFactory ) {
        this.dictionary = dictionaryFactory.get();
    }

    // Supplier<T> 인터페이스 사용한 예 (Bounded Wildcard 를 사용한다.)
    public SpellChecker( Supplier<? extends Dictionary> dictionarySupplier ) {
        this.dictionary = dictionarySupplier.get();
    }

    public boolean isValid( String word ) {
        return dictionary.isValid( word );
    }
}

Dependency Injection 과 Inversion of Control

두 용어는 의존 관계 성립 시에 매번 찾아볼 수 있는 용어이다.
엄격하게 두 가지의 구분은 다르다.

DI는 객체가 의존하는 또 다른 객체를 외부에서 선언하고 이를 주입받아 사용하는 방식이고,
이로 인해 A가 B에 의존할 경우 의존 대상이 변하면 A에 영향을 미친다.
오브젝트 레퍼런스를 외부로부터 제공(주입)받고 이를 통해
여타 오브젝트와 다이내믹하게 의존 관계가 만들어지는 것이 핵심이다.

우리는 DI를 통해 모든 제어를 클라이언트 코드가 가지도록 구현하였다.
기존 설계 방향과 달리 프레임워크(Container)가 제어를 나누어 가져가
의존 관계의 방향이 달라지는 것을 제어가 반전되었다고 한다.

앞서 살펴본 것처럼 의존 객체 주입이 갖는 장점이 있지만, 의존성이 수천 개나 되는
큰 프로젝트에서는 코드를 어지럽게 만들기도 한다. 
대거(Dagger), 주스(Guice), 스프링(Spring) 과 같은 의존 객체 주입 프레임워크를 사용하면 
이를 어느정도? 해소할 수 있다.

© 2023.1 Written by Boseong Kim.
profile
기록하는 개발자

0개의 댓글