본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 5 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 - 완벽 공략

by 개발인생 2022. 9. 22.
반응형

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

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

public class SpellChecker {

   private final Dictionary dictionary;

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

   public boolean isValid(String word) {
      // TODO 여기 SpellChecker 코드
      return dictionary.contains(word);
   }

   public List<String> suggestions(String typo) {
      // TODO 여기 SpellChecker 코드
      return dictionary.closeWordsTo(typo);
   }
}

SpellChecker 가 사용하는 의존성 대신에 팩토리를 넘겨주는 방법이 있다.

public class DictionaryFactory {
   public Dictionary get() {
      return new DefaultDictionary();
   }
}

DictionaryFactory 클래스를 생성한 뒤

public class SpellChecker {

   private final Dictionary dictionary;

//   public SpellChecker(Dictionary dictionary) {
//      
//      this.dictionary = dictionary;
//   }

   public SpellChecker(DictionaryFactory dictionaryFactory) {

      this.dictionary = dictionaryFactory.get();
   }

   public boolean isValid(String word) {
      // TODO 여기 SpellChecker 코드
      return dictionary.contains(word);
   }

   public List<String> suggestions(String typo) {
      // TODO 여기 SpellChecker 코드
      return dictionary.closeWordsTo(typo);
   }
}

이러한 예제를 발전시키면 팩터리 메서드 패턴이 된다.

자원을 바로 받는게 아니라 팩터리를 통해 자원을 가져오는 방식으로

중간 단계를 한번 더 추상화 시킨 것이다.

자원을 생성하는 과정이 복잡하다면 팩터리를 통해 자원을 받아울 수 있다.

DictionaryFactory 는 어떠한 뭔가를 가져오는 역할을 한다.

마치 Supplier 인터페이스 처럼 매개변수 필요없이 뭔가를 가져오는 메서드이다.

public class SpellChecker {

   private final Dictionary dictionary;


   public SpellChecker(Supplier<Dictionary> dictionarySupplier) {

      this.dictionary = dictionarySupplier.get();
   }


   public boolean isValid(String word) {
      // TODO 여기 SpellChecker 코드
      return dictionary.contains(word);
   }

   public List<String> suggestions(String typo) {
      // TODO 여기 SpellChecker 코드
      return dictionary.closeWordsTo(typo);
   }
}

위처럼 Supplier 인터페이스 를 통해 구현할 수 있다.

Supplier 인터페이스 는 팩터리를 표현한 완벽한 예가 될 수 있다.

class SpellCheckerTest {

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

      assertTrue(spellChecker.isValid("test"));
   }
}

위처럼 Supplier 인터페이스 를 사용한 생성자를 사용할 수 있다.

public class SpellChecker {

   private final Dictionary dictionary;


   public SpellChecker(Supplier<? extends Dictionary> dictionarySupplier) {

      this.dictionary = dictionarySupplier.get();
   }


   public boolean isValid(String word) {
      // TODO 여기 SpellChecker 코드
      return dictionary.contains(word);
   }

   public List<String> suggestions(String typo) {
      // TODO 여기 SpellChecker 코드
      return dictionary.closeWordsTo(typo);
   }
}

위처럼 Dictionary 의 하위타입도 받을 수 있다.

   public SpellChecker(Supplier<Dictionary> dictionarySupplier) {

      this.dictionary = dictionarySupplier.get();
   }

하지만 위처럼 작성해도 Dictionary 에 관련된 타입을 받을 수 있다.


팩터리 메서드 패턴

팩터리 메서드 패턴은 만들어야하는 인스턴스를 생성하는 과정이 복잡한 경우에 사용한다.

새로운 Product 를 제공하는 팩토리를 추가하더라도, 팩토리르 사용하는 클라이언트 코드는 변경할 필요가 없다.

public class SpellChecker {

   private Dictionary dictionary;

   public SpellChecker(DictionaryFactory dictionaryFactory) {
      this.dictionary = dictionaryFactory.getDictionary();
   }

   public boolean isValid(String word) {
      // TODO 여기 SpellChecker 코드
      return dictionary.contains(word);
   }

   public List<String> suggestions(String typo) {
      // TODO 여기 SpellChecker 코드
      return dictionary.closeWordsTo(typo);
   }
}

SpellCheckerDictionary 를 사용하는 클라이언트 코드이다.

public interface DictionaryFactory {

   Dictionary getDictionary();

}

팩토리 메소드 패턴에서 Creator 에 해당 하는 DictionaryFactory 인터페이스를 만든다.

public class DefaultDictionary implements Dictionary{

   @Override
   public boolean contains(String word) {
      return false;
   }

   @Override
   public List<String> closeWordsTo(String typo) {
      return null;
   }
}
public class DefaultDictionaryFactory implements DictionaryFactory {
    @Override
    public Dictionary getDictionary() {
        return new DefaultDictionary();
    }
}

DefaultDictionary 를 만들어주는 DefaultDictionaryFactory 이다.

구체적인 팩토리에서 구체적인 인터페이스를 리턴하는 것이다.

SpellChecker 는 인터페이스만 사용하고 있다.

만약 새로운 팩토리가 생기더라도 코드에 변경은 없다.

이러한 구조를 객체지향 원칙에서는 확장에 열려있고, 변경에 닫혀있는 상태 (OCP) 라고 한다.

스프링의 Bean Factory 는 팩토리 메서드 패턴의 대표적인 예이다.


스프링 Ioc

스프링 Ioc 는 Inversion of Control 의 약자이다.

Inversion of Control 은 제어가 역전되었다는 뜻이다.

여기서 말하는 제어권은 본인의 인스턴스를 만든다거나, 본인이 가지고 있는 메서드를 호출하거나

필요한 인스턴스에 대한 의존성을 설정하는 등의 활동을 말한다.

서블릿을 예로들면

doGet, doPost 메서드를 재 정의해 사용하는데 그 어디에서도 우리가 직접

doGet, doPost 를 호출하지 않는다.

doGet, doPost 는 서블릿 컨테이너가 호출하게 되어있다.

이게 바로 제어권 역전이다.

doGet, doPost 메서드를 호출하는 제어권이 서블릿 컨테이너에게 있다.

스프링 Ioc 를 사용하면 인스턴스를 직접 만들 필요가 없다.

스프링을 직접 관리하는 객체를 Bean 이라고 한다.

Bean 은 스프링이 직접 인스턴스를 만들고, Bean 에게 필요한 의존성들은

스프링 Ioc 컨테이너 안에 들어있는 다른 Bean 들을 가져다 알아서 넣어줄 수 있다.

Ioc 나 의존성 주입은 스프링이 없어도 쓸 수 있는 개념이고 직접 구현할 수 있다.

그럼에도 스프링을 사용하는 이유는 3가지가 있다.

  • 전 세계 수많은 개발자들에 의해 오랜기간 동안 검증되었고, 관리가 되고있는 오픈소스이다.
    • 굳이 직접 만들 필요는 없다.
  • 싱글톤 Scope 을 사용하기가 쉽다.
    • Scope 은 객체의 유효범위이다. 여러번 만들어지면 프로토타입, 한번만 만들어지는 인스턴스를 재사용하면 싱글턴 scope 이라한다.
    • 스프링 컨테이너 내부에 하나만 생기는 거지 외부에서는 얼마든지 여러개 만들 수 있다.
  • 인스턴스의 라이프사이클을 제공해준다.
    • 자바에서 기본적으로 제공하는 객체 라이프싸이클에 대한 메서드는 권장하지 않는다. 사실상 사용 불가하다.
    • Bean 은 스프링이 관리하기 때문에 훨씬 더 안정적으로 라이프싸이클에 기능을 끼워넣을 수 있다.

코드를 보면서 살펴보자.

public class SpellChecker {
   private Dictionary dictionary;

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

   public boolean isValid(String word) {
      // TODO 여기 SpellChecker 코드
      return dictionary.contains(word);
   }

   public List<String> suggestions(String typo) {
      // TODO 여기 SpellChecker 코드
      return dictionary.closeWordsTo(typo);
   }
}

SpellChecker 클래스를 만든다.

여기서 중요한건 위의 코드는 스프링을 사용하지 않는 일반적인 클래스이다.

이런 일반적인 클래스를 pojo 라고 한다.

스프링은 스프링의 코드가 노출되길 원하지 않는다.

스프링을 쓴다고해서 스프링이 제공하는 인터페이스를 구현하거나, 상속받아야 한다거나 하는 경우를 원하지 않는다.

프레임워크를 씀으로써 프레임워크의 코드가 우리의 코드에 침투하는 경우가 있다.

이런 경우를 침투적인 프레임워크 라고 한다.

스프링 프레임워크의 철학은 비침투적인 프레임워크이다.

public interface Dictionary {
   boolean contains(String word);
   List<String> closeWordsTo(String typo);
}
public class SpringDictionary implements Dictionary {

   @Override
   public boolean contains(String word) {
      System.out.println("contains " + word);
      return false;
   }

   @Override
   public List<String> closeWordsTo(String typo) {
      return null;
   }
}

Dictionary 인터페이스를 구현한 SpringDictionary 클래스를 만든다.

역시나 스프링과 관련한 코드는 존재하지 않는다.

@Configuration // 스프링 설정 파일 명시
public class AppConfig {

   @Bean
   public SpellChecker spellChecker(Dictionary dictionary) {
      return new SpellChecker(dictionary);
   }

   @Bean
   public Dictionary dictionary() {
      return new SpringDictionary();
   }
}

AppConfig 클래스를 만든다.

여기서는 스프링과 관련한 코드가 들어간다.

이렇게하면 SpellCheckerDictionaryBean 으로 등록되게 된다.


public class App {

   public static void main(String[] args) {
      ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
      SpellChecker spellChecker = applicationContext.getBean(SpellChecker.class); // 스프링이 싱글턴으로 만들어준 Bean
      spellChecker.isValid("test");

      SpellChecker spellChecker1 = new SpellChecker(new SpringDictionary()); // 스프링이 관리하지 않는 객체
   }
}

위처럼 ApplicationContext 를 통해 AppConfig 에 있던 설정을 등록하고

applicationContext.getBean 을 통해 Bean 을 가져온다.

스프링에서 만든 인스턴스를 꺼내서 사용하기만 하면 된다.

더 간단한 방법으로₩

@Component
public class SpellChecker {
   private Dictionary dictionary;

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

   public boolean isValid(String word) {
      // TODO 여기 SpellChecker 코드
      return dictionary.contains(word);
   }

   public List<String> suggestions(String typo) {
      // TODO 여기 SpellChecker 코드
      return dictionary.closeWordsTo(typo);
   }
}
@Component
public class SpringDictionary implements Dictionary {

   @Override
   public boolean contains(String word) {
      System.out.println("contains " + word);
      return false;
   }

   @Override
   public List<String> closeWordsTo(String typo) {
      return null;
   }
}

Bean 으로 등록하고 싶은 클래스에 @Component 어노테이션을 붙이고,

@Configuration
@ComponentScan(basePackageClasses = AppConfig.class)
public class AppConfig {

}

스프링 설정 파일에서 @ComponentScan(basePackageClasses = AppConfig.class) 를 붙이면 된다.

AppConfig.class 가 있는 패키지부터 @Component 어노테이션이 붙은 클래스들을 bean 으로 등록하는 설정이다.


public class App {

   public static void main(String[] args) {
      ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
      SpellChecker spellChecker = applicationContext.getBean(SpellChecker.class); // 스프링이 싱글턴으로 만들어준 Bean
      spellChecker.isValid("test");

      SpellChecker spellChecker1 = new SpellChecker(new SpringDictionary()); // 스프링이 관리하지 않는 객체
   }
}

@Component 를 붙인 클래스들이 bean 으로 잘 주입된 걸 확인할 수 있다.

반응형

댓글1