인터페이스를 구현하는 구현체를 빈으로 등록할 때 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를 조합해서 사용하기도 하고 필드명 매칭만을 사용하기도 한다. 아니면 애시당초에 컴포넌트 스캔 자동 빈 등록에서 원하는것만 등록해버려도 된다.
'Spring, Apache, Java' 카테고리의 다른 글
Bean LifeCycle Callback (0) | 2024.05.27 |
---|---|
타입으로 조회한 빈 여러개가 모두 필요할 때 (List, Map) (0) | 2024.05.27 |
스프링에서 의존관계 자동 주입하는 방법들 (0) | 2024.05.25 |
@ComponentScan (0) | 2024.05.24 |
스프링 빈은 무상태로 설계해야 한다 ❗️ (0) | 2024.05.24 |