[스프링] DI, 혹은 getBean을 하는데 조회된 빈이 2개 이상인 경우

June Lee·2021년 6월 14일
0

Spring

목록 보기
2/9

🌱 김영한님의 스프링 핵심 원리 - 기본편을 수강한 후 학습한 내용을 정리하고 기록하기 위해 작성하는 포스팅입니다.



전통적인 프로그래밍에서는 프로그래머가 프로그램의 실행을 관장하고 외부 라이브러리는 호출해서 사용하는 형식으로만 사용했는데, 최근 다양한 프레임워크들은 프로그램의 실행과 그 속에서 다양한 객체들의 라이프사이클 전체를 관리해주기 때문에 프레임워크 없이는 프로그램이 동작하지 않는다. 이와 같은 기능을 하는 프레임워크를 IoC(Inversion of Control) 컨테이너라고 한다. 그 예시로는 스프링 프레임워크, Junit 등이 있다.
한편, 스프링 프레임워크는 제어의 역전 외에도 다양한 기능들을 수행하는데, 그 중에서도 가장 중요한 개념 중 하나가 바로 의존관계 주입 이다. 이런 이유로 스프링 프레임워크를 DI Container(Dependency Injection Container)라고도 부른다.


스프링 컨테이너에서 빈(bean: 컨테이너가 관리하는 자바 객체)을 찾을 때에는 1)타입으로 찾는 방법과 2)빈 이름으로 찾는 방법이 있다. 빈 이름의 경우, 따로 명시해주지 않으면 기본적으로 클래스 이름의 첫글자를 소문자로 바꾼 것이 빈 이름으로 설정된다.

빈 이름 지정 방법

// 자동 빈 등록 
@Component(value=“member")
public class Member() {
	...
}
// 수동 빈 등록
@Configuration
public class AppConfig() {
    @Bean(name="member")
	public Member member(){
    	return new Member();
    }
}
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class); // 이름으로 조회
MemberService memberService = ac.getBean(MemberService.class); // 타입으로 조회

단 타입으로 조회할 때에 한 가지 주의할 점은, 구체 타입으로 조회하는 코드는 자칫 다형성을 해칠 수 있다는 것이다. 그런 이유로 부모 타입을 구현한 자식 타입으로 조회하는 것보다는 부모 타입 자체로 조회하는 것이 좋은데, 만약 부모 타입을 구현한 자식 클래스의 갯수가 2개 이상이 되면(상속과 구현은 확장을 전제로 설계하는 것이니 사실 당연히 2개 이상이 될 수밖에 없다.) 문제가 발생하게 된다.

위의 예시 코드에서는 ac.getBean()을 해서 빈을 스프링 컨테이너에서 조회해서 가져오는 예시였지만, 사실 의존성 주입을 할 때에도 스프링 컨테이너는 ac.getBean()과 같은 동작을 내부적으로 거쳐야하기 때문에, 의존성 주입의 경우에도 똑같이 적용되는 해결방법이다. 또 현실적으로 스프링컨테이너에서 직접 bean을 가져와서 사용하는 경우는 거의 없기 때문에, DI에서 조회된 빈이 여러 개여서 발생할 수 있는 문제 상황과 이것을 해결하기 위한 방법이라고 보는 것이 더 맞을 것 같다.

이럴 때 사용할 수 있는 방법은 크게 3가지가 있다.


조회된 빈이 2개 이상일 때 해결 방법

  1. 필드명(멤버 변수명 or 매개변수명)을 빈 이름으로 변경
    : 스프링 컨테이너에서는 조회된 빈이 2개 이상일 때, 그 다음으로 필드명을 확인해서 일치하는 빈을 자동으로 찾아온다. 그러나 이 방법은 이후 같은 부모를 상속받은 다른 빈으로 바꿔주고 싶을 때 클라이언트 코드를 수정해야한다는 단점이 있다. (OCP 위반)

  2. @Qualifier 사용

@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
	...
}

위와 같이 붙여준 후 사용을 원하는 곳에 똑같이 @Qualifier 어노테이션을 붙여 원하는 빈을 가져와줄 수 있다.

  1. @Primary 사용
    여러 클래스 중 @Primary가 붙은 빈이 우선시되어 가져와진다.

  2. 커스텀 어노테이션 사용

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

Qualifier는 문자열이라는 단점이 있기 때문에, Qualifier를 포함한 커스텀 어노테이션을 만들어서 Qualifier를 붙여줄 위치에 대신 붙여줄수도 있다. 그러나 대부분의 경우에는 스프링에서 제공해주는 어노테이션만으로도 해결이 가능하기 때문에 무분별하게 만들어 사용하는 것은 좋지 않다.

cf)
자동 빈 등록과 수동 빈 등록이 출동하는 경우, 스프링에서는 수동 빈 등록이 우선시된다. 그런데 이런 애매한 기준으로 생기는 버그가 훨씬 잡기 힘들기 때문에, 스프링 부트에서는 그냥 컴파일 에러를 띄워버린다.
(application.properties에서 컴파일 에러가 뜨지 않도록 설정도 가능하다)


조회된 빈이 2개 이상일 때, 전부 다 가져오는 방법

때로는 조회된 빈들 중 하나를 선택하는 것이 아니라, 모두 가져와서 담아두고 실행 시점에 필요에 맞게 빈을 선택해서 건네주는 방식의 동작이 필요할 때도 있다.
그럴 때에는 Map 혹은 List 자료구조에 빈을 담아줄 수 있다.

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

        // {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@5b218417, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@645aa696}
        System.out.println(discountService.getPolicyMap());
        // [hello.core.discount.FixDiscountPolicy@5b218417, hello.core.discount.RateDiscountPolicy@645aa696]
        System.out.println(discountService.getPolicies());

        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        Assertions.assertTrue(discountService instanceof DiscountService);
        Assertions.assertEquals(discountPrice, 1000);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        Assertions.assertEquals(rateDiscountPrice, 2000);
    }

    @RequiredArgsConstructor
    @Getter
    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;

        public int discount(Member member, int price, String discountCode){
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
@Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType(){
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        Assertions.assertEquals(beansOfType.size(), 2);
        for (String key: beansOfType.keySet()){
            System.out.println("key= " + key + "value = " + beansOfType.get(key));
        }
    }
profile
📝 dev wiki

0개의 댓글