Spring, Apache, Java

@Autowired로 자동 주입을 할 때 빈이 2개 이상 조회된 경우

cwchoiit 2024. 5. 26. 15:34
728x90
반응형
SMALL

인터페이스를 구현하는 구현체를 빈으로 등록할 때 2개 이상 등록하는 경우가 있을 수 있다.

예를 들어, OrderService에서 사용하는 DiscountPolicy를 구현한 구현체가 fixDiscountPolicy, rateDiscountPolicy 이렇게 두개가 존재하고 이 두개가 모두 빈으로 등록되는 경우는 생각보다 많다.

 

그럼 자동 주입을 하는 경우 타입을 통해 빈을 조회한다. 즉, 구체클래스인 fixDiscountPolicy, rateDiscountPolicy로 조회하는게 아니라 타입인 DiscountPolicy로 조회한다는 뜻이다. 이 경우 유니크하지 않다는 에러가 발생한다.

 

해결하는 방법은 가장 심플하게 구체 클래스로 빈을 조회하면 되는데 이는 DIP(Dependency Inversion Principle, 의존관계 역전 법칙)를 위반하는 행위이다. 그래서 이 방법 말고 다음 3가지 방법을 소개한다.

 

  • 필드명, 파라미터 명 매칭
  • @Qualifier
  • @Primary

필드 명 매칭

다음 코드를 보자.

OrderServiceImpl

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);

        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

문제가 되는 부분은 저 DiscountPolicy 필드이다. 이 타입으로 빈을 조회하게 되면 두 개의 빈이 조회가 되니까 문제가 된다. 이때 필드명을 구체적으로 작성해주면 된다.

수정된 OrderServiceImpl

package org.example.springcore.order;

import lombok.RequiredArgsConstructor;
import org.example.springcore.discount.DiscountPolicy;
import org.example.springcore.member.Member;
import org.example.springcore.member.MemberRepository;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy rateDiscountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);

        int discountPrice = rateDiscountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

이번엔 필드명을 구체적으로 rateDiscountPolicy로 작성했다. 이러면 타입으로 조회한 결과에서 여러 빈 중 이 필드명과 일치하는 빈을 스프링이 자동으로 주입해준다.

 

그래서, 필드명 매칭은 먼저 타입으로 조회해서 매칭을 시도하고, 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.

 

@Qualifier

이건 뭐냐면 추가적으로 애노테이션 정보를 넣어서 원하는것을 찾게 해주는 기능이다.바로 코드로 보자.

FixDiscountPolicy

package org.example.springcore.discount;

import org.example.springcore.member.Grade;
import org.example.springcore.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

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

    private static final int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        }
        return 0;
    }
}

RateDiscountPolicy

package org.example.springcore.discount;

import org.example.springcore.member.Grade;
import org.example.springcore.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

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

    private static final int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        }
        return 0;
    }
}

 

두 구체 클래스에 붙은 @Qualifier 애노테이션을 보자. 각각의 구체 클래스에 추가적인 이름을 부여한 것이다.

그리고 주입받는 쪽은 다음과 같이 그 중에서 무엇을 넣어줄건지를 결정하면 된다. 대신에 이건 lombok이랑 같이 사용할 경우 설정 파일을 추가적으로 작업해줘야 하는데 솔직히 그 시간에 그냥 생성자 만들어서 쓰는게 맞다고 생각이 든다. 그래서 다음 코드를 보자.

 

OrderServiceImpl

package org.example.springcore.order;

import org.example.springcore.discount.DiscountPolicy;
import org.example.springcore.member.Member;
import org.example.springcore.member.MemberRepository;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);

        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

 

음.. 근데 @Qualifier를 사용하기보단 필드명 매칭이 훨씬 더 간결해 보인다. 그리고 @Qualifier는 같은 @Qualifier를 찾는 메커니즘이다. 그러니까 @Qualifier("rateDiscountPolicy")라고 찾는다면 이 애노테이션이 달려있는 구현체가 있어야 맞다. 근데 그런 구현체가 없으면 빈 이름이 rateDiscountPolicy인 빈을 찾으려고 시도한다. 그러니까 어떻게 보면 예측하기 어려워지는 코드가 될 수 있다.

 

근데 이제 좀 재밌는게 있다. @Qualifier("rateDiscountPolicy") 이건 문자열을 집어 넣는다. 그 말은? 잘못된 문자열을 넣어도 컴파일 단계에서 에러를 잡아낼 수 없다는 소리다. 단적인 예로 내가 @Qualifier("rtaeDiscountPolicy") 이렇게 작성해도 전혀 에러로 잡아주지 않는다. 그리고 런타임 시에 문제가 딱 터질거다. 최악의 에러라고 볼 수 있다. 이것을 방지하기 위해 커스텀 애노테이션을 만들 수도 있다.

MainDiscountPolicy

package org.example.springcore.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)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

 

이렇게 나만의 애노테이션을 만들고 그 안에 @Qualifier("mainDiscountPolicy") 이렇게 만들어 넣으면 이 애노테이션이 저 @Qualifier 역할까지 하게 된다. 그리고 가져다가 사용하는 쪽은 이렇게 문자열을 잘못 입력 할 걱정없이 사용할 수 있다.

 

RateDiscountPolicy

package org.example.springcore.discount;

import org.example.springcore.annotation.MainDiscountPolicy;
import org.example.springcore.member.Grade;
import org.example.springcore.member.Member;
import org.springframework.stereotype.Component;

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {

    private static final int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        }
        return 0;
    }
}

 

OrderServiceImpl

package org.example.springcore.order;

import lombok.RequiredArgsConstructor;
import org.example.springcore.annotation.MainDiscountPolicy;
import org.example.springcore.discount.DiscountPolicy;
import org.example.springcore.member.Member;
import org.example.springcore.member.MemberRepository;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);

        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

 

그래서 이런 식으로도 가능하다. 근데 중요한 건 남용은 오히려 유지보수에 독이 된다. 스프링이 기본적으로 제공하는 거의 대부분의 애노테이션으로 다 해결이 가능하다. 그래서 정말 사용할 필요가 있는 경우에 사용하는 것을 고려해보자.

 

@Primary

이게 오히려 @Qualifier보다 편하고 깔끔하다. 타입으로 빈을 조회했을 때 여러개의 결과가 나올 수 있는데 그 중 @Primary가 붙은 빈을 주입하는 방식이다.

 

RateDiscountPolicy

package org.example.springcore.discount;

import org.example.springcore.member.Grade;
import org.example.springcore.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {

    private static final int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        }
        return 0;
    }
}

FixDiscountPolicy

package org.example.springcore.discount;

import org.example.springcore.member.Grade;
import org.example.springcore.member.Member;
import org.springframework.stereotype.Component;

@Component
public class FixDiscountPolicy implements DiscountPolicy {

    private static final int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        }
        return 0;
    }
}

 

위 두 구현체 중 RateDiscountPolicy@Primary 애노테이션이 붙었다. 이 경우 이 구현체를 주입한다는 그런 내용이다.

 

OrderServiceImpl

package org.example.springcore.order;

import lombok.RequiredArgsConstructor;
import org.example.springcore.discount.DiscountPolicy;
import org.example.springcore.member.Member;
import org.example.springcore.member.MemberRepository;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);

        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

그래서 코드도 다시 lombok을 사용해서 더 깔끔하고 군더더기 없게 됐다.

그럼 만약, @Qualifier, @Primary 둘 다 있을때 우선순위가 뭐가 더 높을까? @Primary는 기본값처럼 동작하는 것이고 @Qualifier는 매우 상세하게 동작한다. 그러니까 @Primary는 있으면 사용하는거고 없어도 그만이다의 느낌을 받으면 되는데 @Qualifier를 지정하면 막 필드 옆에다가도 @Qualifier를 붙여서 두 개를 매칭시키고 하는 이런 상세한 동작을 요한다. 스프링에선? 상세한게 더 우선순위다. 즉, @Qualifier가 더 우선순위가 높다.

 

결론

그래서 결론은 3개 모두 적절히 잘 사용하면 된다. 빈의 중복은 발생할 가능성이 꽤나 농후하기 때문에 그때 적절한 방법으로 풀어나가면 된다. @Primary@Qualifier를 조합해서 사용하기도 하고 필드명 매칭만을 사용하기도 한다. 아니면 애시당초에 컴포넌트 스캔 자동 빈 등록에서 원하는것만 등록해버려도 된다. 

728x90
반응형
LIST