본문 바로가기
Computer Sience/Java

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

by 제우제우 2024. 10. 26.

Chapter 02: 객체 생성과 파괴

객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 법

올바른 객체 생성 방법에 대한 설명

잘못된 사례1

많은 클래스는 하나 이상의 자원에 의존한다. 

책에서는 예시로 맞춤법 검사기(Spell Checker)와 맞춤법 검사기가 의존하는 사전(Dictionary)을 예시로 들고 있다. 

 

맞춤법 검사기가 정적 유틸리티 클래스 구조를 가진다면?

public interface Lexicon {
}

public class KoreanDictionary implements Lexicon{
}

public class SpellChecker {
    private static final Lexicon dictionary = new KoreanDictionary();
    private SpellChecker () {}
    public static boolean isValid(String word){
        return true;
    }
    public static List<String> suggestions(String type){
        return List.of();
    }
}

 

맞춤법 검사를 하는 대상이 한국어가 아니라면?

현재 SpellChecker는 특정 사전에 강하게 결합되어 있다.

이는 유연하지 않고 테스트하기 어렵다.

여기서 테스트란 SpellChecker 기능만을 테스트를 의미한다. 

잘못된 사례2

맞춤법 검사기가 싱글톤으로 구현 

public class SpellChecker {
    private final Lexicon dictionary = new KoreanDictionary();
    
    private SpellChecker () {}
    
    public static SpellChecker INSTANCE = new SpellChecker();
    
    public boolean isValid(String word){
        return true;
    }
    public List<String> suggestions(String type){
        return List.of();
    }
}

 

이 방식도 마찬가지로  SpellChecker가 특정 사전에 강하게 결합되어 있다.

실전에서는 사전이 언어별로 따로 있다. 또한 테스트용 사전이 필요할 수 있다. 

의존 객체 주입 방식 

어떤 클래스가 사용하는 리소스에 따라 행동을 달리 해야 하는 경우에는 스태틱 유틸리티 클래스와 싱글톤을 사용하는 것은 부적절하다.

public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker (Lexicon dictionary) {
        this.dictionary = dictionary;
    }
    public boolean isValid(String word){
        return true;
    }
    public List<String> suggestions(String type){
        return List.of();
    }
}

 

SpellChecker 인스턴스를 생성할 때 생성자로 필요한 자원을 넘겨주는 방식 

맞춤법 검사기를 생성할 때 의존 객체인 사전을 주입해주면 된다.

 

이런 의존성 주입은 생성자, 정적 팩토리(아이템1) 그리고 빌더(아이템2)에도 적용할 수 있다.

 

이 패턴의 변형으로 리소스의 팩토리를 생성자에 전달하는 방법도 있다.

자바 8에 들어온 함수형 인터페이스인 Supplier<T> 인터페이스가 그런 팩토리로 쓰기에 완벽하다. 

public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker (Supplier<? extends Lexicon> dictionary) {
        this.dictionary = dictionary.get();
    }
    public boolean isValid(String word){
        return true;
    }
    public List<String> suggestions(String type){
        return List.of();
    }

    public static void main(String[] args) {
        SpellChecker spellChecker = new SpellChecker(() -> new KoreanDictionary());
    }
}

 

Supplier<T>를 입력으로 받는 메소드는 한정적 와일드카드 타입(bounded wildcard type)을 사용해 

팩토리 타입의 매개변수를 제한해야 한다. 

 

지연 초기화(Lazy Initialization)

Supplier는 필요한 시점에 객체를 생성하게 한다.
따라서 SpellChecker 객체가 생성될 때 Lexicon 객체가 즉시 생성되지 않고, Supplier의 get() 메서드를 호출하는 시점에 실제 객체가 생성된다. 이는 리소스가 필요한 순간까지 객체 생성을 미루므로 성능이나 리소스 관리 면에서 유리할 수 있다.

특히 객체 생성에 리소스가 많이 사용되는 경우 

정리

의존하는 리소스에 따라 행동을 달리하는 클래스를 만들 때는 싱글톤이나 스태틱 유틸 클래스를 사용하지 말자.

그런 경우에는 리소스를 생성자나 팩토리로 전달하는 의존성 주입을 사용하여 유연함, 재사용성, 테스트 용이성을 향상 시키자.

 

의존성 주입이 유연함과 테스트 용이함을 크게 향상 시켜주지만, 의존성이 많은 큰 프로젝트인 경우에는 코드가 장황해 질 수 있다. 그점은 대거, 쥬스, 스프링 같은 프레임웍을 사용해서 해결할 수 있다.

참고 자료

백기선님 이펙티브 자바 깃허브

 

study/effective-java/item5.md at master · whiteship/study

Learn, Share and Grow. Contribute to whiteship/study development by creating an account on GitHub.

github.com