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

2023. 11. 16. 23:39Book/이펙티브 자바

 

객체 생성과 파괴

 

들어가기 전 요약

 - 사용하는 자원에 따라 동작이 달라지는 클래스는 정적 유틸리티 클래스나 싱글톤 방식이 적합하지 않다.

 - 의존 객체 주입이란 인스터스를 생성할 때 필요한 자원을 넘겨주는 방식이다.

 - 이 방식의 변형으로 생성자에 자원 팩토리를 넘겨줄 수 있다.

 - 의존 객체 주입을 사용하며 클래스의 유용성, 재사용성, 테스트 용이성을 개선할 수 있다.

 

 

많은 클래스가 하나 이상의 자원에 의존한다. 가령 맞춤법 검사기는 사전(Dictionary)에 의존하는데, 이런 클래스를 정적 유틸리티 클래스(아이템4)로 구현한 모습을 드물지 않게 볼 수 있다.

 

 

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

public class SpellChecker {

    //자원을 직접 명시
    private static final Dictionary dictionary = new Dictionary();
    private SpellChecker() {}
    public static final SpellChecker INSTANCE = new SpellChecker();
    public static boolean isValid(String word) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.contains(word);
    }

    public static List<String> suggestions(String typo) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.closeWordsTo(typo);
    }
}

 

 

싱글톤을 잘못 사용한 예)

public class SpellChecker {

    //자원을 직접 명시
    private final Dictionary dictionary = new Dictionary();
    private SpellChecker() {}
    public static final SpellChecker INSTANCE = new SpellChecker();
    public boolean isValid(String word) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.contains(word);
    }

    public List<String> suggestions(String typo) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.closeWordsTo(typo);
    }
}

============================================================================================

class SpellCheckerTest {

    @Test
    void isValid() {
        assertTrue(SpellChecker.INSTANCE.isValid("test"));
    }
}

 

두 방식 모두 Dictionary를 Mocking 하기 어려워 테스트하기 불편하다(static 한 클래스를 mocking 할 수는 있다. 권장x)

또한 실전에서는 언어별로 다른 사전이 있다. SpellChecker가 여러 사전을 사용할 수 있도록 해보자.

간단하게 Dictionary 필드에서 final 한정자를 제거하고 다른 사전으로 교체하는 메서드를 추가할 수 있다.

하지만 이 방식은 오류를 내기 쉽고 멀티스레드 환경에서는 쓸 수 없다.

  ==> 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글톤 방식이 적합하지 않다!

 

클래스(SpellChecker)가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원(dictionary)을 사용해야한다.

 ==> 이 조건을 만족하는 간단한 패턴이 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.(DI)

 

public class SpellChecker {

    private final Dictionary dictionary;

    public SpellChecker(DefaultDictionary dictionary) {
        this.dictionary = dictionary;
    }

    public boolean isValid(String word) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.contains(word);
    }

    public List<String> suggestions(String typo) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.closeWordsTo(typo);
    }
}

=================================================================

class SpellCheckerTest {

    @Test
    void isValid() {
        SpellChecker spellChecker = new SpellChecker(new DefaultDictionary());
    }
}

 

Dictionary를 인터페이스화 하고 원하는 구현체를 생성할 때 넘겨주면 유연하고 테스트하기 용이하게 할 수 있다.

이를 의존 객체 주입 패턴이라고 한다.

 

이 패턴의 쓸만한 변형으로, 생성자에 자원 팩토리를 넘겨주는 방식이 있다. 팩토리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다.(팩터리 메서드 패턴)

 

public class SpellChecker {
	//Dictionary 인터페이스
    private final Dictionary dictionary;
    //DictionalFactory 인터페이스
    public SpellChecker(DictionaryFactory dictionaryFactory) {
        this.dictionary = dictionaryFactory.getDictionary();
    }

    public boolean isValid(String word) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.contains(word);
    }

    public List<String> suggestions(String typo) {
        /*
         *  SpellCheck 코드 생략
         */
        return dictionary.closeWordsTo(typo);
    }
}

====================================================================================

public interface Dictionary {
    boolean contains(String word);

    List<String> closeWordsTo(String typo);
}


public interface DictionaryFactory {
    Dictionary getDictionary();
}

====================================================================================

public class DefaultDictionaryFactory implements DictionaryFactory{
    @Override
    public Dictionary getDictionary() {
        return new DefaultDictionary();
    }
}

public class MockDictionaryFactory implements DictionaryFactory{
    @Override
    public Dictionary getDictionary() {
        return new MockDictionary();
    }
}

=====================================================================================

 

Dictionary도 인터페이스를 사용하고 Factory도 인터페이스를 사용하기 때문에 클라이언트 코드의 변경 없이 제품을 변경할 수 있다.(개방 폐쇄 원칙)