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

NAKTA·2023년 11월 17일
0

effective java 3e

목록 보기
5/5
post-thumbnail
post-custom-banner

🌱 들어가면서

프로그래밍을 하다보면, 하나의 객체가 다른 객체를 포함하는 경우가 많이 있다.

public class Car {
    private final Engine engine = new Engine();
}

예를 들어, Car 클래스 내부에 Engine 클래스를 필드로 가지고 있는 경우를 생각해보자.

Car 클래스 내부 메서드는 Engine 클래스의 속성을 가져와서 쓰고 있다면 Car 클래스는 Engine 클래스에 의존한다. 라고 말할 수 있다.

객체간의 의존 관련 용어에 대해 알고싶다면 결합도와 응집도 관련 내용을 참고해보면 좋을 것 같다.

이번 내용은 이러한 의존 객체를 초기화 하는 작업인
의존 객체 주입 (Dependency Injection, DI) 에 대해서 다뤄볼 예정이다.


💡 *의존 객체 주입 (Dependency Injection, DI) 이란?
객체 지향 프로그래밍에서 한 객체가 다른 객체에 의존성을 직접 생성하는 것이 아니라, 외부에서 의존 객체를 주입받아 사용하는 디자인 패턴이다.



🙃 의존 객체 주입 잘못된 예시

유틸리티 클래스 예제 코드

public class SpellChecker
{
	private static final Lexicon dictionary = new Dictionary();
	
  	private SpellChecker() {}

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

싱글톤 (Singleton) 패턴 예제 코드

public class SpellChecker {

  	private static final Lexion dictionary = new Dictionary();
      
    private SpellChecker() {}
    public static SpellChecker INSTANCE = new SpellChecker();
    
    public boolean isValid() {...}
    public List<String> suggestions(String typo) {...}

}

다음과 같이 맞춤법 검사 기능을 지원해주는 SpellChecker 클래스를 유틸리티 클래스싱글톤 (Singleton) 패턴 으로 예시를 들어보자.

해당 클래스들은 맞춤법이 맞는지 확인할 사전 Lexicon 이 필요하기에 내부에 의존 객체로써 포함하려고 한다.

따라서, private 생성자 를 사용하므로 의존 객체 초기화를 미리 해둔 상태이다.

겉보기에는 아무런 문제가 없어보이는 코드지만,
실제로 맞춤법 검사를 수행하는데 있어서는 어떨까?
각 나라별 사전이나 특수 어휘용 사전 등 다양한 사전을 활용하여 다양한 경우에서 맞춤법 검사를 해야하지 않을까?


물론, 해당 예제 코드 클래스가 단 하나의 경우 에서만 사용할 것이라면,
해당 코드는 아무런 문제가 없다.

하지만, 확장성을 고려해야 하는 객체지향적 코드로 봤을 때,
해당 코드는 결코 잘 짜여진 코드라고 볼 수 없다.


그렇다면, Lexicon 필드의 final 키워드를 제거하고 static method 를 이용해 외부에서 Lexicon 을 바꿀 수 있게 변경하면 어떨까?

public class SpellChecker
{
	private static Lexicon dictionary = ...;
	
  	private SpellChecker()
  	{
  	}
    
    public static void changeDictionary(Lexicon dictionary) {
    	this.dictionary = dictionary;
    }

  	public static boolean isValid(String word)
  	{
  		...
  	{

  	public static List<String> suggestions(String type)
  	{
  		...
  	}
}

싱글톤 (Singleton) 방식과 비슷하니 유틸리티 클래스 예제 코드만 수정했다.

이렇게 코드를 수정하면 외부에서 changeDictionary() 메서드를 호출하여 사전을 필요에 따라 바꿀 수 있으므로 다양한 사전을 넣어줄 수 있다.

하지만, 이렇게 되면 유틸리티 클래스는 결국 상태 를 가지게 된다.
즉, 다음과 같이 외부에서 dictionary 를 변경할 수 있게 되버리면 해당 유틸리티 클래스의 기능을 이용하는 모든 클래스는 변경된 상태가 공유된다는 말이 된다.

따라서, 멀티 스레드 환경에서는 동시성 문제를 야기할 수 있어 동시성 처리를 따로 해줘야 한다는 단점이 있다.


다음과 같은 이유 때문에 상태 를 가지면 안되는 클래스에는 의존 객체 사용하면 안된다는 사실을 알 수 있었다.


📑 정리

  • 상태를 가지면 안되는 클래스 에는 의존 객체를 사용하면 확장성 에 좋지 않다.
  • 의존 객체를 외부에서 변경할 수 있게 바꾸면 상태 를 가지게 되어 동시성 문제를 야기할 수 있다.



🙃 의존 객체 주입 올바른 예시

public class SpellChecker {

  	private final Lexicon dictionary = ...;
    
    public SpellChecker(Lexicon dictionary) {
    	this.dictionary = dictionary;
    }
    
    public boolean isValid() {...}
    public List<String> suggestions(String typo) {...}
}

다음과 같이 객체를 생성할 때마다 내부 의존 객체를 주입하는 방식은 여러가지 이점이 있다.


첫 번째로, 확장성이 높아진다.

public interface Lexicon {
    void printText();
}
public class EnglishDictionary implements Lexicon{
    @Override
    public void printText() {
        System.out.println("ENGLISH");
    }
}
public class KoreanDictionary implements Lexicon{
    @Override
    public void printText() {
        System.out.println("KOREAN");
    }
}
public class Main {
    public static void main(String[] args) {
        SpellChecker koreanSpellChecker = new SpellChecker(new KoreanDictionary());
        SpellChecker englishSpellChecker = new SpellChecker(new EnglishDictionary());
    }
}

다음과 같이 Lexicon 인터페이스에서 하위 타입을 구현해 놓으면 여러 하위 타입을 넣고 객체를 만들어 사용이 가능하다.


두 번째로, 테스트에 용이하다.

import org.junit.Test;
import static org.mockito.Mockito.*;

public class SpellCheckerTest {

    @Test
    public void testSpellCheckerWithEnglishDictionary() {
        // given
        Lexicon englishDictionary = mock(EnglishDictionary.class);
        SpellChecker spellChecker = new SpellChecker(englishDictionary);

        // when
        spellChecker.performSpellCheck();

		// then
        verify(englishDictionary).printText();
    }

    @Test
    public void testSpellCheckerWithKoreanDictionary() {
        // given
        Lexicon koreanDictionary = mock(KoreanDictionary.class);
        SpellChecker spellChecker = new SpellChecker(koreanDictionary);

        // when
        spellChecker.performSpellCheck();

        // then
        verify(koreanDictionary).printText();
    }
}

다음과 같이 의존하는 객체를 외부에서 생성하여 직접 넣어줄 수 있기 때문에 각각의 하위 타입에 대한 테스트 코드를 짜기가 수월해진다.


📑 정리

  • 의존 객체를 생성자로 넘겨주게 되면 확장성 이 높아진다.
  • 외부에서 객체를 제공하기 때문에 테스트 가 용이해진다.


자원 팩토리 방식

패턴을 응용해서 자원 팩토리 를 생성자에 넘겨주는 방식이 있다.

아래는 자바8 에서부터 사용가능한 Supplier<T> 인터페이스가 팩토리를 표현한 예이다.

import java.util.function.Supplier;

public class SpellChecker {
    private final Lexicon dictionary;

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

    public void print() {
        dictionary.printText();
    }
}
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Type type = Type.ENGLISH;

        Supplier<Lexicon> dictionaryFactory = () -> {
            Lexicon dictionary = null;

            switch (type) {
                case KOREAN: dictionary = new KoreanDictionary(); break;
                case ENGLISH: dictionary = new EnglishDictionary(); break;
            }
            return dictionary;
        };

        SpellChecker spellChecker = new SpellChecker(dictionaryFactory);
        spellChecker.print();
    }
}

참고로, Supplier<T> 인터페이스는 함수형 인터페이스 이므로, 람다식으로 표현이 가능하다.


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

주로, 팩토리 방식한정적 와일드 카드 타입을 사용하여 팩터리의 타입 매개 변수를 제한한다. 타입을 제한하면 사용자는 명시된 타입의 하위 타입이면 무엇이든 팩토리를 만들어 넘길 수 있게 된다.


물론 해당 방식은 좋지만,
의존성이 매우 높은 실제 프로젝트 에서는 각각 팩토리로 만들거나 생성자 매개변수로 넘겨주면 프로그래머 입장에서는 큰 부담이 된다.

이러한 부담을 줄여주기 위해서 실전에서는 의존 객체 프레임워크 를 활용하여 해결한다.

예를 들어, 스프링 프레임워크 에서는 @Autowired 와 같은 Annotation 을 활용하여 의존성을 주입해주는 방식을 지원해준다.

💡 의존 객체 프레임워크란?
의존성 주입을 지원하는 소프트웨어 프레임워크로, 이러한 프레임워크는 객체 간의 의존성을 효과적으로 관리하고 주입할 수 있는 방법을 제공하여 코드의 유연성과 유지보수성을 향상시켜준다.



profile
느려도 확실히
post-custom-banner

0개의 댓글