Effective Java : 아이템5 완벽공략

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다.


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

    이 챕터와 관련된 완벽공략은 다음과 같다

    • p29. 이 패턴의 쓸만한 변형으로 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 
    • p29. 자바 8에서 소개한 Supplier<T> 인터페이스가 팩토리를 표현한 완벽한 예시다
    • p29. 한정적 와일드카드 타입을 사용해 팩토리의 타입 매개변수를 제한해야 한다
    • p29. 팩토리 메서드 패턴
    • p30. 의존 객체가 많은 경우에 Dagger, Guice, 스프링 같은 프레임워크 도입을 고려할 수 있다.

    아래에서 설명을 함께 보고자 한다. 


    생성자에 자원 팩터리를 넘겨주는 방식  + Supplier 함수형 인터페이스가 팩토리를 표현한 완벽한 예시다

    Item5에서 공부했던 SpellChecker가 사용하는 의존객체(자원)은 Dictionary 인스턴스다. 아래는 리팩토링한 코드인데, SpellChecker에서 Dictionary를 주입 받는 것을 볼 수 있다. 그런데 여기서 이야기하는 것은 자원을 생성하는 팩토리를 넘겨줘서 자원을 생성하는 과정을 한번 더 팩토리를 통해 추상화 하는 것을 의미한다. 

    public class SpellChecker {
    
        private final Dictionary dictionary;
    
        public SpellChecker(Dictionary dictionary) {
            this.dictionary = dictionary;
        }
    
        ...
    }

    기존의 리팩토링된 코드가 위와 같다면, 자원 생성 팩토리를 직접 넘겨주는 코드는 다음과 같이 변경된다. Factory를 넘겨주고, Factory에서 자원을 직접 획득해서 필드 변수로 셋팅하는 작업을 진행한다. 

    public class SpellCheckerWithSupplierFactory {
    
        private final Dictionary dictionary;
    
        public SpellCheckerWithSupplierFactory(DictionaryFactory factory) {
            this.dictionary = factory.getDictionary();
        }
    
        ...
    }

    "Supplier 함수형 인터페이스가 팩토리를 표현한 완벽한 예시다" 라는 말은 여기서 사용이 가능하다. Factory는 아무런 매개변수를 전달받지 않고 자원을 생성해서 넘겨주는 인터페이스다. 이것은 자바가 기본으로 제공하는 함수형 인터페이스 중 하나인 Supplier<T>와 정확히 매칭된다. 따라서 Supplier<Dictionary>로도 매개변수를 전달받아 동일한 기능을 수행할 수 있다. 예시 코드는 아래에서 살펴볼 수 있다.

    public class SpellCheckerWithSupplierFactory {
    
        private final Dictionary dictionary;
    
        public SpellCheckerWithSupplierFactory(Supplier<Dictionary> factory) {
            this.dictionary = factory.get();
        }
    	
        ...
    
        public static void main(String[] args) {
            SpellCheckerWithSupplierFactory spellChecker
            	= new SpellCheckerWithSupplierFactory(DictionaryFactory::getDictionaryStatic);
        }
    
    }

    위의 main() 메서드를 살펴보면 메서드 레퍼런스 형태로 Factory를 전달해서 spellChecker 인스턴스를 생성했다. 메서드 레퍼런스의 타겟 타입은 Supplier<Dictionary>가 될 것이다. 

     

    정리

    • 자원을 생성하는 과정이 복잡하다면 자원 생성 팩토리를 전달하고, 자원을 팩토리에서 얻는 방식으로 추상화 할 수 있다. 
    • 자원 생성 팩토리는 이 경우 Supplier 함수형 인터페이스와 일치한다. 

     


    한정적 와일드카드 타입을 사용해 팩토리의 타입 매개변수를 제한해야 한다

    이것은 Bounded 와일드 카드를 설정하는 것을 의미한다. 제네릭 타입으로 값을 받는다면 어떠한 타입도 올 수 있다. 하지만 해당 팩토리가 모든 종류의 자원을 생성하는 것은 아니다. 따라서 한정된 와일드 카드를 이용해서 생성할 수 있는 자원의 범위를 한정하는 것이다. 

    public SpellCheckerWithSupplierFactory(Supplier<? extends Dictionary> factory) {
        this.dictionary = factory.get();
    }

    예를 들어 위와 같이 설정한다면 Dictionary 하위 호환 타입까지 받을 수 있음을 의미한다. 

     


    팩터리 메서드 패턴 

    앞선 내용에서는 자원을 직접 DI 해주는 방식을 사용했다. 일반적인 경우에는 그것으로 충분할 수 있다. 하지만 자원 타입의 객체를 만들 때, 여러 타입의 객체가 있고 각 객체를 만드는 방법이 복잡해서 통일할 수 없는 경우가 있다. 이런 경우라면 팩터리 메서드 패턴을 통해 "자원을 만드는 과정"을 한번 더 추상화해서 OCP (확장에 열려있고, 변경에 닫혀있는)를 강화할 수 있다. 

    위 그림이 전형적인 형태의 팩터리 메서드 패턴이다.

    • Creator : 자원을 만드는 팩토리 역할을 한다. Interface 타입으로 생성하고, Product 인터페이스에 의존한다.
    • Product : 자원이다. Interface 타입으로 생성한다. 
    • Client는 Creator 인터페이스에 의존한다.

    이렇게 작성한다면 새로운 자원과 자원 팩토리가 생성되더라도, Client의 코드는 변경되지 않는다. 즉, 확장에는 열려있고 변경에는 닫혀있게 되는 것이다. 이것이 팩터리 메서드 패턴의 장점이 된다. 

    실생활에서 살펴볼 수 있는 예시는 다음과 같다. 

    • 자동차가 존재한다. 자동차는 추상적이다. 왜냐하면 펠리세이드 20년식, 코나 18년식 등등 자동차에 대한 더 구체적인 타입이 존재하기 때문이다.
    • 공장이 존재한다. 이 공장은 추상적이다. 이 공장이 펠리세이드를 만드는 공장일 수도 있고, 코나를 만드는 공장일 수도 있기 때문이다. 

    이런 상황에서 펠리세이드를 만들 때는 뒷좌석에 뭔가를 더 추가하는 공정이 있을 수 있고, 코나를 만들 때는 그렇지 않을 수 있다. 즉, 팩토리마다 생성하는 인스턴스가 다르고, 인스턴스마다 생성되는 방법이 다를 수 있다. 

    팩터리 메서드 패턴은 이렇게 추상화 된 자원을 가지고, 생성을 함에 있어서 공통으로 사용할 수 있는 부분은 재사용하고, 그렇지 않은 부분은 각각 구현한 후에 생성된 인스턴스를 반환하도록 디자인 된다.

     

    코드로 알아보기

    • Product - Dictionary
    • Creator - DictionaryFactory
    • Client - SpellChecker

    Product, Creator, Client에 대응되는 클래스는 각각 다음과 같다. 이 녀석을 이용해서 아래에 하나씩 구현해본다. 

    public interface Dictionary {
        boolean contains(String word);
        List<String> closeWordsTo(String typo);
    }
    
    
    public class KoreanDictionary implements Dictionary {
        public boolean contains(String word) {
            return false;
        }
    
        public List<String> closeWordsTo(String typo) {
            return null;
        }
    }
    • Dictionary는 인터페이스로 작성한다. 
    • Dictionary의 구현체로 KoreanDictionary를 사용한다. 
    public interface DictionaryFactory {
        Dictionary getDictionary();
    }
    
    public class KoreanDictionaryFactory implements DictionaryFactory{
    
        @Override
        public Dictionary getDictionary() {
            return new KoreanDictionary();
        }
    }
    • DictionaryFactory는 인터페이스로 작성한다. 이 녀석은 Dictionary 인터페이스에 의존한다. 
    • Dictionary 팩토리의 구현체로 KoreanDefaultDictionary를 구현한다. 
    public class SpellCheckerWithFactoryMethod {
    
        private Dictionary dictionary;
    
        public SpellCheckerWithFactoryMethod(DictionaryFactory factory) {
            this.dictionary = factory.getDictionary();
        }
    
        public boolean isValid(String word) {
            // TODO : SpellChecker 코드 로직 구현
            return dictionary.contains(word);
        }
    
        public List<String> suggestions(String typo) {
            // TODO : SpellChecker 코드 로직 구현
            return dictionary.closeWordsTo(typo);
        }
    
    }
    • SpellChecker(클라이언트)를 구현한다.
      • SpellChecker는 DictionaryFactory 인터페이스에 대한 객체를 주입받아서 Dictionary를 획득한다.
      • SpellChecker는 Dictionary 인터페이스에 의존한다. 

     

    정리

    • 팩터리 메서드 패턴은 자원 생성이 복잡할 때, 자원 생성 과정을 추상화 하는 작업을 한다. 
    • 팩터리 메서드 패턴은 팩터리, 자원이 각각 인터페이스로 추상화 된다. 따라서 클라이언트 코드에서는 새로운 팩터리, 자원이 추가되어도 확장에는 열려있고 변화에는 닫혀있다. 
    • 팩터리 메서드 패턴을 좀 더 일반적인 형태로 확장하면 스프링 IoC의 핵심적인 요소인 Spring Bean Factory가 된다. 

    스프링 IoC

    앞서 이야기 한 것처럼 팩터리 메서드 패턴이 좀 더 일반적인 형태로 확장되면 스프링 IoC의 핵심적인 요소인 Spring Bean Factory가 된다. Spring Bean Factory는 BeanFactory, ApplicationContext 형태로 사용된다. 

    • Inversion of Control - 뒤집힌 제어권 
      • 자기 코드에 대한 제어권을 자기 자신이 가지고 있지 않고 외부에서 제어하는 경우
      • 제어권? 인스턴스를 만들거나, 어떤 메서드를 실행하거나, 필요로 하는 의존성을 주입 받는 등
    • 스프링 IOC 컨테이너 사용 장점
      • 수많은 개발자에게 검증되었으며 자바 표준 스팩 (@Inject)도 지원한다.
      • 손쉽게 싱글톤 Scope를 사용할 수 있다.
      • 객체 생성 (Bean) 관련 라이프사이클 인터페이스를 제공한다. 
      • -> 인스턴스를 만들자마자 데이터를 넣거나, 뭔가 작업을 해야하는 경우도 있고, 객체가 소멸되기 전에 자원의 회수 등을 확인해야 하는 작업이 있을 수 있다. 스프링은 이런 작업을 할 수 있도록 라이프 사이클 인터페이스를 제공해준다. 

     

    IoC : 뒤집힌 제어권

    제어권이 역전되지 않은 것은 무엇을 의미할까? 개발자가 직접 인스턴스 내부에서 필요로 하는 의존 객체를 new로 만들어주는 것은 제어권이 역전되지 않은 것을 의미한다. 제어권이 역전되었다는 것은 인스턴스 내부에서 필요로 하는 의존 객체를 외부에서 주입해주는 것을 의미한다. 즉, 어떤 인스턴스가 내부에서 어떤 의존 객체를 사용할지 직접 결정할 수 없고 외부에서 결정을 해주는 것이다.

    이것의 대표적인 예로는 Servlet이 있다. doGet(), doPost() 메서드는 개발자가 직접 구현하지만 실제로 구현한 메서드를 호출하는 것은 개발자가 아니라 ServletContainer다. 즉, Servlet의 제어 권한이 Servlet 스스로가 아니라 Servlet 컨테이너에게 넘어간 상태가 된다. 

     

    스프링을 통해 손쉽게 싱글톤 Scope를 사용할 수 있다.

    Scope은 객체의 유효 범위를 의미한다. Singleton Scope은 어플리케이션 전체에서 하나의 인스턴스만 존재하는 것을 보장한다. 만약 필요한만큼 객체가 계속 만들어진다면 이 객체의 Scope은 Prototype Scope이 된다. 

     

    스프링을 통해 Bean 라이프 사이클 인터페이스를 사용할 수 있다.

    객체가 생성되고 소멸되는 과정에서 반드시 호출되어야 하는 작업들이 존재할 수 있다. 예를 들면 이런 작업들이 존재한다. 

    • 객체가 생성되자마자 값을 셋팅해야한다. 
    • 객체가 소멸되기 전 가지고 있던 자원 반납 작업이 필요하다. 

    이런 작업들이 존재하는데, 스프링은 빈의 생명 주기(Life Cycle) 인터페이스를 제공하고, 이 인터페이스는 어노테이션을 이용해 간편하게 사용할 수 있다. 

     

    스프링을 이용한 리팩토링

    팩토리 메서드 패턴을 이용해서 리팩토링 했던 코드가 있다. 이 코드를 다시 한번 스프링을 이용한 형태로 리팩토링 해보고자 한다. 

    @Configuration
    public class AppConfig {
    
        @Bean
        public DictionaryFactory dictionaryFactory(Dictionary dictionary) {
            return new KoreanDictionaryFactory(dictionary);
        }
        @Bean
        public Dictionary dictionary() {
            return new KoreanDictionary();
        }
    }
    • 먼저 스프링 빈 등록을 위한 AppConfig 클래스를 추가한다. 
    • KoreanDictionary, KoreanDictionaryFactory를 스프링 빈으로 생성해서 스프링 빈 컨테이너에 등록한다. 
    public class DefaultDictionaryFactory implements DictionaryFactory {
    
        private final Dictionary dictionary;
    
        public DefaultDictionaryFactory(Dictionary dictionary) {
            this.dictionary = dictionary;
        }
    
        @Override
        public Dictionary getDictionary() {
            return this.dictionary;
        }
    
    }
    • DictionaryFactory도 살짝 수정한다.
      • 기존에는 항상 new 키워드를 이용해서 새로운 dictionary를 생성해서 전달했다.
      • 지금부터는 DictionaryFactory는 스프링 빈으로 주입받은 Dictionary를 반환하도록 한다. 
    @Component
    public class SpellCheckerWithSpring {
    
    
        private final Dictionary dictionary;
    
        public SpellCheckerWithSpring(DictionaryFactory factory) {
            this.dictionary = factory.getDictionary();
        }
    
        public boolean isValid(String word) {
            // TODO : SpellChecker 코드 로직 구현
            return dictionary.contains(word);
        }
    
        public List<String> suggestions(String typo) {
            // TODO : SpellChecker 코드 로직 구현
            return dictionary.closeWordsTo(typo);
        }
    }
    • SpellChecker는 이제 Factory를 스프링 빈으로 주입 받아서 사용한다. 
    • 스프링 빈으로 주입 받을 땐, 생성자로 주입 받았다. 

    댓글

    Designed by JB FACTORY