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

Loopy·2022년 5월 21일
0

이펙티브 자바

목록 보기
5/76
post-thumbnail

우리는 주로 사전과 같은 경우 정적 유틸리티 클래스 혹은 싱글턴으로 구현하고는 한다. 하지만, 이는 잘못된 방법인데 많은 클래스가 하나 이상의 자원에 의존하는 경우가 많기 때문이다. 아래의 예시를 통해 자세히 알아보자.

// 정적 유틸리티를 잘못 사용한 예시
public class SpellChecker {
	private final Lexion dictionary = ...;
    
    private SpellChecker(..) {}
    
    public static booolean isValid(String word) {...}
    public static List<String> suggestions(String type){...}
}
// 싱글톤을 잘못 사용한 예시
public class SpellChecker {
	private final Lexion dictionary = ...;
    
    private SpellChecker(..) {}
    public static SpellChecker INSTANCE = new SpellChecker(...);  // 객체 생성
    
    public booolean isValid(String word) {...}
    public List<String> suggestions(String type){...}
}

앞선 두 방식은, 유연하지 않고 테스트하기 어렵다는 단점이 존재한다.
dictionary 종류를 외부에서 바꾸는 것이 불가능하므로 이는 모두 사전을 단 하나만 사용한다고 가정한 코드이지, 다른 종류의 사전들이 들어오는 경우는 고려하지 않았기 때문이다.

그렇다면 SpellChecker 가 여러 사전을 사용할 수 있도록 하려면 어떻게 해야할까?

필드에서 final 한정자를 제거하고 사전을 교체할 수 있도록 지원하는 setter 메서드는 멀티 스레드 환경에서 사용이 불가능하며 에러를 일으킨다.

public static void setDictionary(Lexicon new) {
   dictionary = new;
}

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

☁️ 의존 객체 주입이란?

위 문제점의 해결책은, 바로 의존 객체 주입이다. 의존 객체 주입이란 인스턴스를 생성할때 생성자에 필요한 자원을 넘겨주는 방식 이다. 대표적으로 스프링은 의존 객체 주입 프레임워크이다.

단, 다음의 2가지 조건을 만족할때 사용 가능하다.

  1. 클래스(SpellChecker)가 여러 자원 인스턴스를 지원
  2. 클라이언트가 원하는 자원(dictionary)을 사용
public class SpellChecker{
	private final lexicon dictionary;
    
    public SpellChecker(Lexion dictionary){  //생성자
    	this.dictionary = Objects.requireNonNull(dictionary);
   }
    public booolean isValid(String word) {...}
    public List<String> suggestions(String type){...}
}

이처럼 의존 객체 주입 패턴은, 자원이 몇 개든 의존 관계가 어떻든 상관없이 잘 동작한다.

interface Lexicon {}

public class KoreanDictionary implements Lexicon {}
public class EnglishDictionary implements Lexicon {}

Lexicon kDic = new KoreanDictionary();
Lexicon eDic = new EnglishDictionary();
SpellChecker spellChecker = new SpellChecker(kDic);
SpellChecker spellChecker = new SpellChecker(eDic);

☁️ 의존 객체 주입의 장점

또한, final 로 자원의 변경을 막음으로써 불변을 보장하여 같은 자원을 사용하려는 여러 클라이언트가 의존 객체들을 안심하고 공유 가능하다.

☁️ 의존 객체 주입 패턴 변형: 팩터리 메서드 패턴

이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 존재하며 이를 팩터리 메서드 패턴이라 한다. 자바 8의 Supplier<T> 을 사용하면, 자신이 명시한 타입의 하위 타입이라면 모두 생성할 수 있는 팩터리를 넘길 수 있다.

@FunctionalInterface
public interface Supplier<T> {
  	T get(); // 매개변수가 없고, 단순히 무엇인가를 반환할 때 사용
}

Supplier<T> 를 입력으로 받는 메서드는, 한정적 와일드카드 타입을 사용해 팩터리의 타입 매개변수를 제한할 수 있기 때문이다. 예시를 봐보자.

class Tile {
    // 기본 타일 클래스
}

class GrassTile extends Tile {
    // 잔디 타일 클래스
}
public class GameMap {
    private Supplier<? extends Tile> tileFactory;

    public GameMap(Supplier<? extends Tile> tileFactory) {
        this.tileSupplier = tileSupplier;
    }
    ...
}

Supplier<Tile> grassTileSupplier = () -> new GrassTile(); // GrassTile::new; 
GameMap grassMap = new GameMap(grassTileSupplier);

💡핵심 정리
클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 대신, 필요한 자원을(or 그 자원을 만들어주는 팩터리를) 생성자에(or 정적 팩터리나 빌더)넘겨주자. 의존 객체 주입은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글