의존관계 자동 주입 - 2

hoonie·2023년 2월 20일
0

인프런 김영한님의 강의 '스프링 핵심 원리 - 기본편'의 강의 내용을 참고하여 작성한 글입니다.

(문제점) 조회 빈이 2개 이상인 경우

@Autowired는 타입(Type)으로 조회한다.

참고: 설명의 편의성을 위해 이전 포스트에서 적용했던 lombok설정을 해제하고 기존에 사용하던 코드로 진행함.

@Autowired
private DiscountPolicy discountPolicy

타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작한다.(실제로는 더 많은 기능을 제공)
ac.getBean(DiscountPolicy.class)

스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy 둘 다 스프링 빈으로 선언해보자.

@Component
public class FixDiscountPolicy implements DiscountPolicy{}
@Component
public class RateDiscountPolicy implements DiscountPolicy{}

이렇게 한 뒤 의존관계 자동 주입을 실행(DiscountPolicy를 자동주입 받는 코드실행)하면

@Autowired
private DiscountPolicy discountPolicy

NoUniqueBeanDefinitionException 오류가 발생한다.

No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: 
expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy

오류메세지에서도 알 수 있듯이 하나의 빈을 기대했는데 fixDiscountPolicy, rateDiscountPolicy 2개의 빈이 발견되었다고 알려준다.
이때, 주입받는 필드를 하위 타입으로 구체화해서 지정할 수 있는데 이렇게 코드를 짜게되면 DIP를 위배하는 결과와 함께 코드의 유연성이 떨어지게 된다. 또한 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 2개 이상 있을때는 하위 타입으로 지정하는 방법으로도 해결할 수 었다.
스프링 빈을 수동등록해서 문제를 해결해도 되지만, 의존관계 자동주입에서 이를 해결하는 방법이 여러개 있으니 한 번 알아보자.

@Autowired 필드 명, @Qualifier, @Primary

해결 방법을 하나씩 알아보자.
조회 대상 빈이 2개 이상일 때 해결 방법

  • @Autowired 필드명 매칭
  • @Qualifier -> @Qualifier끼리 매칭 -> 빈 이름 매칭
  • @Primary 사용

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
기존 코드

@Autowired
private DiscountPolicy discountPolicy;

필드 명을 빈 이름으로 변경

@Autowired
private DiscountPolicy rateDiscountPolicy

필드 명이 rateDiscountPolicy이므로 정상 주입이 된다.
필드 명 매칭은 먼저 타입 매칭을 시도하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.

참고: 위 코드는 필드 주입을 활용하여 필드명 매칭을 구현한 코드이다. 같은 원칙을 적용하여 생성자 주입을 활용해보면

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = rateDiscountPolicy;
}

와 같이 파라미터 명에 빈 이름을 명시해주면 타입으로 조회를 하더라도 원하는 빈을 주입받을 수 있다.

@Autowired 매칭 정리
1. 타입 매칭
2. 타입 매칭의 결과가 2개 이상일 때 필드 명 파라미터 명으로 빈 이름 매칭

@Qualifier 사용

@Qualifier는 추가 구분자를 붙여주는 방법이다. 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

적용방법 - 빈 등록 시 @Qualifier를 붙여준다.

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

주입 시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.
생성자 자동 주입 예시

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

수정자 자동 주입 예시

@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy) DiscountPolicy discountPolicy) {
	this.discountPolicy = discountPolicy;
}

@Qualifier로 주입할 때 @Qualifier("mainDiscountPolicy")를 못 찾으면 어떻게 될까? 그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 하지만 개발자가 정말 의도를 갖고 짠 코드가 아니고 실수로 mainDiscountPolicy를 못 찾는 코드를 작성했을 확률이 높기 때문에 이런 경우는 바람직하지 않다고 생각한다. 따라서 @Qualifier@Qualifier를 찾는 용도로만 사용되는게 명확할 것이다.
다음과 같이 직접 빈 등록시에도 @Qualifier를 동일하게 사용할 수 있다.

@Bean
@Qualifer("mainDiscountPolicy)
public DiscountPolicy discountPolicy () {
	return new ...
}

@Qualifier 정리
1. @Qualifer끼리 매칭
2. 빈 이름 매칭
3. NoSuchBeanDefinitionException예외 발생

@Primary 사용

@Primary는 우선순위를 정하는 방법이다. @Autowired시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.
rateDiscountPolicy가 우선권을 가지도록 하자.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

코드를 실행해보면 원하는 빈이 주입받아지는 것을 확인 할 수 있다는 것을 알 수 있다.

여기까지 보면 @Primary@Qualifier 중에 어떤 것이 좋을지 고민이 될 것이다. @Qualifier의 단점은 주입 받기 위해 모든 코드에 어노테이션을 붙여주어야한다는 점이다. 반면에 @Primary는 빈에만 붙여주면 된다.

@Primary, @Qualifier 활용

예를 들어 코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다. 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 사용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 사용할 수 있도록 하고 서브 데이터베이스 커넥션을 획득할 때에는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 코드를 작성하면 코드를 깔끔하게 유지할 수 있다. 물론 이때 메인 데이터베이스의 커넥션을 획득하는 스프링 빈을 등록할 때 @Qualifier를 지정해주는것은 상관이 없다.

우선순위
@Primary는 기본값처럼 동작을 하고 @Qualifier는 매우 상세하게 동작한다. 이런 경우에 어떤 것이 우선권을 가져갈까? 스프링은 자동보다는 수동이, 넓은 범위의 선택권보단 좁은 범위의 선택권이 우선순위가 높다. 따라서 여기서도 @Qualifier가 우선권이 높다.

어노테이션 직접 만들기

@Qualifier("mainDiscountPolicy") 이렇게 문자열를 적으면 컴파일시 타입 체크가 안된다. 다음과 같은 어노테이션을 직접 만들어서 문제를 해결할 수 있다.

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")

public @interface MainDiscountPolicy {
}

생성자 자동 주입

@Autowired // 생성자가 딱 1개만 있으면 생략가능.
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
    }

어노테이션의 경우 상속이라는 개념이 없다. 이렇게 여러 어노테이션을 모아서 사용하는 기능은 스프링이 지원하는 기능이다. @Qualifier뿐만 아니라 다른 어노테이션들도 함께 조합해서 사용할 수 있다. 단적으로 @Autowired도 재정의할 수 있다. 하지만 스프링이 제공하는 기능을 뚜렷한 목적없이 무분별하게 재정의하는 것은 유지보수에 혼란만 가중할 수 있으므로 주의해야한다.

조회한 빈이 모두 필요할 때 - List, Map

의도적으로 해당 타입의 빈이 모두 필요한 경우가 있다. 예를 들어 할인 서비스를 제공하는 과정에서 사용자가 rate, fix 중 원하는 것을 선택할 수 있어야하는 경우가 있다. 이때, 스프링을 사용하면 소위 말하는 전략패턴을 매우 간단하게 구현할 수 있다.

test코드

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {

    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);

        assertThat(discountService).isInstanceOf(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);

        int fixDiscountPrice = discountService.discount(member, 20000, "fixDiscountPolicy");
        assertThat(fixDiscountPrice).isEqualTo(1000);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);

    }
    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 discountPolicy) {
            DiscountPolicy selectedPolicy = policyMap.get(discountPolicy);

            System.out.println("discountPolicy = " + discountPolicy);
            System.out.println("selectedPolicy = " + selectedPolicy);

            return selectedPolicy.discount(member, price);
        }
    }

}

로직 분석

  • DiscountService는 Map으로 모든 DiscountPolicy를 주입받는다. 이때 fixDiscountPolicy, rateDiscountPolicyAutoAppConfig.class로 주입받는다.
  • static으로 정의한 DiscountService의 경우 @Component 어노테이션이 없으므로 컴포넌트 스캔 대상이 아니므로 명시적으로 스프링 빈에 등록해준다.
  • discount() 메서드는 discountPolicy로 사용자가 선택한 할인 정책을 받고 해당 객체를 스프링 빈에서 찾아서 실행한다.

주입 분석

  • Map<String, DiscountPolicy>: map의 key에는 스프링 빈의 이름을 넣어주고 value에는 DiscountPolicy로 조회한 모든 스프링 빈을 담아준다.

참고 - 스프링 컨테이너를 생성하면서 스프링 빈 등록하기
스프링 컨테이너는 생성자에 클래스 정보를 받는다. 여기에 클래스 정보를 넘기면 해당 클래스가 스프링 빈으로 자동 등록된다.
new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

위 코드는 2가지로 나누어 이해할 수 있다.

  • new AnnotationConfigApplicationContext()를 통해 스프링 컨테이너를 생성한다.
  • AutoAppConfig.class, DiscountService.class를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링 빈으로 등록한다.
    정리하면 스프링 컨테이너를 생성하면서, 해당 컨테이너에 동시에 AutoAppConfig, DiscountService를 스프링 빈으로 자동 등록한다.
profile
사우루스 팡팡!

0개의 댓글