Spring DI(의존관계 주입)

최준호·2021년 7월 6일
0

Spring

목록 보기
6/47

DI (의존관계 주입)

의존관계 주입의 방법은 크게 4가지가 존재합니다.

  1. 생성자 주입
  2. 수정자(setter) 주입
  3. 필드 주입
  4. 일반 메서드 주입
  • 생성자 주입

    • 특징
    1. 생성자 호출 시점에 1번만 호출되는 것을 보장합니다.

    2. 불변, 필수 의존관계에 사용됩니다.만약 spring bean class에 생성자가 단 1개라면 @Autowired가 생략되어도 spring이 자동으로 주입해줍니다.위 코드와 아래 코드와 동일한 실행 결과가 나옵니다.

      @Component 
      public class OrderServiceImpl implements OrderService { 
        private final MemberRepository memberRepository; 
        private final DiscountPolicy discountPolicy; 
      
        public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { 
          this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; 
        } 
      }
    @Component 
    public class OrderServiceImpl implements OrderService { 
      private final MemberRepository memberRepository; 
      private final DiscountPolicy discountPolicy; 
    
      @Autowired 
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy; 
      } 
    }
  • 생성자 주입은 생성자를 통해서 의존관계를 주입하는 방법입니다. 지금까지 우리가 진행했던 방법이 생성자 주입입니다.

  • 수정자 주입 (setter 주입)

    • 특징
    1. 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법입니다.
    2. 선택, 변경 가능성이 있는 의존관계에 사용합니다.
    3. 변경이 가능하기때문에 무분별하게 사용할시 장애가 발생할 확률이 높습니다.자바빈 프로퍼티 규약
    4. 필드의 값을 직접 변경하지 않고 setA, getA 라는 메서를드 통해 필드값을 수정하거나 읽는 자바 규약
    @Component 
    public class OrderServiceImpl implements OrderService { 
        private MemberRepository memberRepository; 
        private DiscountPolicy discountPolicy; 
    
        @Autowired public void setMemberRepository(MemberRepository memberRepository) { 
            this.memberRepository = memberRepository; 
        } 
    
        @Autowired public void setDiscountPolicy(DiscountPolicy discountPolicy) { 
            this.discountPolicy = discountPolicy; 
        } 
    }
  • setter라는 수정자 메서드를 통해서 의존관계를 주입하는 방법입니다.

  • 필드 주입

    • 특징
    1. 코드가 간단하다는 장점이 있지만 단점이 더 큰 방법입니다.
    2. 외부에서 의존관계를 컨트롤하는 것이 불가능하기 때문에 테스트 코드를 작성하기 어렵습니다.
    3. 하지만 테스트 코드에서는 간단한 작성을 통해 의존관계가 주입이 가능하기 때문에 테스트 코드에서 사용하는 경우도 있습니다.
    @Component 
    public class OrderServiceImpl implements OrderService { 
      @Autowired 
      private MemberRepository memberRepository; 
    
      @Autowired 
      private DiscountPolicy discountPolicy; 
    }
  • 필드에 바로 주입하는 방법입니다.

  • 일반 메서드 주입

    • 특징
    1. 일반적으로 잘 사용하지 않는 방법입니다.
    @Component
    public class OrderServiceImpl implements OrderService {
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
    
        @Autowired
        public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }
  • 메서드를 작성하여 의존관계를 주입하는 방법입니다.

가장 선호되는 DI

과거에는 setter 주입과 필드 주입을 주로 사용했지만 요즘 테스트 코드를 중요시 하는 프로젝트에서는 생성자 주입을 권장합니다. 그 이유는

  1. 테스트 코드 작성시 생성자를 통해 개발자가 원하는 DI를 정할 수 있습니다.
  2. 어플리케이션 입장에서는 server가 실행되었을 때 의존관계를 주입을 단 1번만 하는 것을 보장하기 때문에 불변하게 설계할 수 있기 때문입니다.
  3. 코드를 작성시 생성자 주입은 매개변수를 모두 입력하지 않으면 오류가 나지만 setter 주입의 경우 개발자가 실제로 코드를 실행해서 error 코드를 보기전까지 알수가 없습니다.
  • final을 사용한 생성자 주입위와 같은 코드를 작성하게되면 에러가 납니다!

    @Component
    public class OrderServiceImpl implements OrderService {
      private final MemberRepository memberRepository; 
      private final DiscountPolicy discountPolicy; 
    
      @Autowired
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
      	this.memberRepository = memberRepository; 
      } 
    }
  • 생성자 주입은 server 작동시 필드에 단 한번만 주입을 하기때문에 final을 붙여줄 수 있습니다. final을 붙였을때의 장점은 final로 선언한 뒤 2번 이상 필드에 값을 주입하거나 1번도 주입하지 않는다면 오류가 발생하기때문에 개발자가 코드를 작성하면서 바로 확인할 수 있습니다.

최신 트렌드 (feat. lombok)

가장 선호되는 DI 방법은 생성자 주입이란걸 알았는데 당장 프로젝트를 시작하려니 DI를 적용시킬때마다 생성자를 만들어주는게 귀찮다고 느끼면서 나도 모르게 필드 주입의 유혹을 받을 수도 있습니다. 하지만 lombok을 사용하시면 훨씬 간편하게 코드를 짜실 수 있습니다! (lombok적용법은 인터넷 검색을 해주세요!)

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

lombok을 사용하면 위와같이 코드를 작성하면 우리가 지금까지 했던 코드와 동일하게 작동하는데요! 그 이유는 먼저 lombok이 아닌 spring의 도움있습니다. 위에서 말한것처럼 생성자 주입시 bean class에 생성자가 단 1개라면 @Autowired를 생략해도 spring이 알아서 생성자를 찾아서 등록해준다고 했었습니다. 그 이유 1개와 lombok의 @RequiredArgsConstructor를 사용하게되면 현재 class에 private final이 붙은 필드를 모두 가지고 생성자 하나를 자동으로 생성하는 annotation입니다. 이 두개가 조합되어 개발자는 필드 주입처럼 생성자 주입을 사용할수 있게됩니다. 혹시 난 못믿겠는데? 눈으로 봐야 믿겠다고 하시는 분들은 컴파일하신 뒤 class를 디컴파일하셔서 확인해보세요!

조회 bean이 2개 이상일 경우

bean을 조회했는데 2개 이상이 조회될 경우가 있습니다. 현재 프로젝트에서는 DiscountPolicy의 경우 Rate와 Fix 2개가 존재하는데 개발자가 어떤걸 사용할지 @Autowired에서는 알아낼수가 없습니다. 그래서

  1. @Autowired 필드명 매칭
  2. @Qualifier
  3. @Primary

다음과 같은 3가지 방법이 있습니다. 다음 방법을 자세히 알아보겠습니다.

  • @Autowired 필드명 매칭필드명 자체를 bean에 등록되는 이름과 동일하게 작성하면 spring에서 자동으로 등록해줍니다. @Autowired를 사용하면

    1. type으로 매칭을 하고
    2. 매칭 결과가 2개 이상일 경우 필드명으로 bean을 가져옵니다.
    3. 하지만 필드명을 설정해주지 않아서 2개 이상이 조회된다면 NoSuchBeanDefinitionException 예외가 발생합니다.
    @Autowired 
    private DiscountPolicy rateDiscountPolicy
  • @Qualifier 위와같이 2개의 bean class에 @Qualifier를 통해 이름을 붙여주고다음과 같이 bean을 조회하는 부분에 @Qulifier("bean이름")으로 조회를 해주면됩니다.@Qualifier는

    1. @Qualifier끼리 서로 매칭하고
    2. 없다면 매개변수 부분에 입력된 @Qualifier("bean이름")을 기준으로 bean을 검색하고
    3. 그래도 없다면 NoSuchBeanDefinitionException 예외가 발생합니다.
  • @Component @Qualifier("fixDiscountPolicy") public class FixDiscountPolicy implements DiscountPolicy {}

  • @Component @Qualifier("rateDiscountPolicy") public class RateDiscountPolicy implements DiscountPolicy {}

  • @Qualifier는 위와 같은 방법으로 구분자 역할을 제공해주어 bean들을 개발자가 좀더 정확하게 구분하여 사용할 수 있도록 해줍니다. 하지만 만약에 @Qualifier를 까먹고 매개변수에만 붙이고 bean에 입력해주지 않았다면 spring은 알아서 @Qualifier에 입력된 이름의 spring bean을 검색하지만 그래도 양쪽에 붙여서 명확하게 사용하는 편이 유지보수과 코드리딩 부분에서 더 좋습니다.

    @Autowired public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("rateDiscountPolicy") DiscountPolicy discountPolicy) {
    	this.memberRepository = memberRepository; 
    	this.discountPolicy = discountPolicy; 
    }
  • @Qualifier는 bean을 구분할 수 있도록 구분자 역할을 제공합니다. @Qualifier("bean이름") 으로 bean의 이름을 붙여주면 생성자에서 설정한 이름으로 bean을 1개만 선택하여 불러올 수 있습니다.

  • @Qualifier("bean이름")를 annotation으로 직접 만들어 사용해보기@Qualifier의 코드입니다. 위의 코드에서 어노테이션을 그대로 복사해서다음과 같이 작성하면 @Qualifier("bean이름") 대신 @mainDiscountPolicy을 사용하여 @Qualifier("bean이름")과 동일하게 동작하게 만들 수 있습니다.어노테이션으로 설정되어있기 때문에 오타가 나면 바로 오류를 확인할 수 있습니다.

    @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) 
    @Retention(RetentionPolicy.RUNTIME) 
    @Inherited @Documented 
    public @interface Qualifier {
    	String value() default ""; 
    }
    @Component 
    @MainDiscountPolicy 
    public class RateDiscountPolicy implements DiscountPolicy {}
    @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) 
    @Retention(RetentionPolicy.RUNTIME) 
    @Documented 
    @Qualifier("rateDiscountPolicy") 
    public @interface MainDiscountPolicy { }
  • @Qualifier("bean이름")으로 사용하게되면 컴파일에서 오류로 확인할 수 없습니다. 하지만 어노테이션으로 개발자가 직접 만들어서 사용하게된다면 오타로 인한 오류는 바로 확인할 수 있게됩니다.

  • @PrimaryRateDiscount에 @Primary를 사용하여 준다면다음과 같은 코드에서 RateDiscount로 우선적으로 bean을 조회하게 됩니다.

    @Component 
    public class FixDiscountPolicy implements DiscountPolicy {}
    @Component 
    @Primary 
    public class RateDiscountPolicy implements DiscountPolicy {}
  • @Qualifier와 @Primary를 동시에 사용했을 경우에는 좀 더 수동적이고 자세한 @Qualifier를 우선순위를 가져가게 됩니다. @Qualifier와 @Primary의 장점을 이해하고 적절하게 사용하면 좋을거 같습니다!

    @Autowired 
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository; 
      this.discountPolicy = discountPolicy; 
    }
  • @Primary는 우선순위를 정해주는 방법입니다. @Primary를 붙인 bean을 우선적으로 불러와서 사용한다고 생각하시면 편하실거 같습니다.

조회한 bean이 2개 이상이고 조회된 bean이 모두 필요할때

위에서는 2개 이상의 빈을 조회했을때 한개만 강제로 주입하게 했지만 2개이상 조회된 빈을 상황에 맞게 사용해야할 경우도 있다. 현재 프로젝트에서 예시를 들면 사용자가 쇼핑 후 상품의 가격에따라 1000원 즉시 할인을 선택할지 10% 할인을 받을지 선택할지 정한다면 그 상황에 맞게 할인되는 구현체를 적용해줘야한다.

  • List와 Map을 사용하여 적용다음과 같은 test code를 실행해보면

    1. DiscountPolicy 대신 DiscountService라는 class를 만들어서 테스트를 진행했습니다.
    2. DiscountService는 Map으로 DiscountPolicy를 주입받습니다. 이때 println으로 찍히는 내용을 확인하시면 map과 list모두 fixDiscount와 rateDiscount를 주입 받습니다.
    3. discount method discountCode는 주입받은 구현체를 선택할 수 있는 매개변수로 map에서 get()을 통해 구현체를 선택할 수 있도록 구현되어 있습니다. list에서도 index값으로 가져오던 스타일에 맞게 사용하시면 될거 같습니다.
    4. @Autowired가 작동한 방식을 분석해보면
    5. Map<String, DiscountPolicy>의 map key값으로 스프링 빈의 이름을 넣어주고 value에는 DiscountPolicy로 조회된 모든 bean을 등록해줍니다.
    6. List는 DiscountPolicy로 조회된 모든 bean을 list에 담아줍니다.
    7. 만약 해당 타입의 빈이 없으면 빈 컬렉션이나 map을 주입합니다.
    public class AllBeanTest { 
    
      @Test 
      void findAllBean(){ 
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class); 
        DiscountService discountService = ac.getBean(DiscountService.class); 
        Member member = new Member(1L, "userA", Grade.VIP); int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        Assertions.assertThat(discountService).isInstanceOf(DiscountService.class); 
        Assertions.assertThat(discountPrice).isEqualTo(1000); 
      } 
    
      static class DiscountService{ 
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;
      
        @Autowired 
        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
          this.policyMap = policyMap; 
          this.policies = policies;
          System.out.println("policyMap = " + policyMap); 
          System.out.println("policies = " + policies);
        } 
      
        public int discount(Member member, int price, String discountCode) { 
          DiscountPolicy discountPolicy = policyMap.get(discountCode); 
          return discountPolicy.discount(member, price); 
        } 
      } 
    }

자동과 수동의 기준

지금까지 spring으로 자동화와 수동으로 bean을 등록하고 컨트롤하는 방법을 배웠습니다. 자동화도 편하고 좋지만 수동으로 등록해서 사용해야할 경우도 존재합니다. 그럼 어떻게 구분해서 사용해야할까요?

  1. 업무 로직 빈은 자동화로 등록해서 사용하는 것이 좋다.
  2. 기술 지원 빈은 수동으로 등록해서 사용하는 것이 좋다.

1번의 업무 로직은 어떤 것일까요? 우리가 사용하는 controller, servic, repository 등이 모두 업무 로직입니다. 이런 로직들은 한번 개발할때 방대한 양의 코드들이 추가되지만 패턴또한 유사하게 반복되므로 자동화로 등록해도 다른 개발자들이 유지보수에서 편하게 찾아볼 수 있습니다. 또한 방대한 양의 코드들을 자동화로 등록할 수 있기 때문에 작성하는 개발자 또한 편하게 작성이 가능합니다.

2번의 기술 지원 로직은 기술적인 문제 해결이나 AOP 처리를 말합니다. DB 연결이나 공통 로그 처리같은 기술을 기술 지원 로직이라 생각하시면 될것 같습니다. 이런 로직들은 기술에 따라 코드 작성법이 달라질 수도 있어 자동화로 등록하게되면 코드를 파악하기 어려울 수 있습니다. 그래서 이런 기술 지원 로직들은 수동 빈 등록을 사용하여 명확하게 표시해주는 것이 좋습니다.

  1. 비즈니스 로직 중 다형성을 활용한 코드

위에서 작성한 Map과 List로 다수의 bean을 조회하는 코드에서 DiscountService에 주입되는 bean들을 다른 개발자가 쉽게 찾아낼 수 있을까요? 우리는 방금 우리가 만든 코드이기 때문에 rate와 fix가 있다는걸 떠올릴 수 있지만 처음 본 개발자는 해당 코드를 이해하기 위해서는 print로 찍히는 console를 확인하거나 DiscountPolicy를 impliment한 class들을 모두 찾아봐야할것입니다. 하지만

@Configuration
public class DiscountPolicyConfig {

    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }

    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}

이렇게 선언해놓는 class를 만들어놓는다면 bean을 수동으로 등록하는 코드를 추가적으로 입력해야하지만 유지보수하는 개발자 입장에서는 DiscountService 대신 주입되는 DiscountPolicyConfig class에서 한눈에 어떤 DiscountPolicy가 존재하는지 확인할 수 있으며 추가 개발이나 수정에서 쉽게 확인할 수 있습니다.

출처 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글