@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;
}
}
⚠️ 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 된다.
setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다....
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
...
⚠️ @Autowired 는 주입할 대상이 없으면 오류가 발생한다. 오류가 발생하지 않게 하려면 @Autowired(required = false)로 지정해야한다.
@Autowired)도 관리도 해주기 때문에 new로 직접 객체를 생성하면 스프링은 몰라서 null이 될 수도 있다. ...
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
...
@Configuration 같은 곳...
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 @Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.
해결방법은 다음 3가지이다.
@Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력됨Optional<> : 자동 주입 대상이 없으면 Optional.empty가 입력됨(@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.)
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}
Member는 스프링 빈이 아니기 때문에 아래와 같이 setNoBean1 자체는 호출이 되지 않는 결과가 나온다.
java: variable discountPolicy might not have been initialized// 생성자 주입은 테스트에서도 명확하게 어떤 의존 객체가 필요한지 알 수 있음
OrderServiceImpl service = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
✅ 항상 생성자 주입을 선택하자! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택하도록 하자.
롬복 라이브러리가 제공하는@RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 컴파일 시점에 생성자를 자동으로 만들어준다. (자바의 애노테이션 프로세서라는 기능을 이용)
➕ 생성자가 하나일 경우 @Autowired 생략이 가능하기 때문에 아래와 같이 코드가 훨씬 간단해진다.
@RequiredArgsConstructor
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@Autowired는 타입 기준으로 주입을 시도하여 같은 타입에 등록된 빈이 2개 이상일 경우 NoUniqueBeanDefinitionException 오류가 발생한다.
@Autowired
private DiscountPolicy discountPolicy;
// DiscountPolicy 빈이 2개라면? (FixDiscountPolicy, RateDiscountPolicy)
이 문제를 해결하는 방법 3가지는 아래와 같다.
@Autowired는 먼저 타입 매칭을 시도하고 여러 빈이 있을 때 추가로 동작하는 기능이다. 아래와 같이 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
@Autowired
private DiscountPolicy rateDiscountPolicy; // 빈 이름과 일치
추가 구분자를 붙여주는 방법이다.
주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아니다! 주의하자!
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(
MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
...
}
만약 @Qualifier("mainDiscountPolicy")을 찾지 못한다면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
추가로 찾을 시에도 없다면 NoUniqueBeanDefinitionException 오류가 발생한다.
우선 순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
✅ @Qualifier vs @Primary
@Primary는 기본(default) 빈으로 지정하는 것이고 별다른 지정이 없을 때 주입된다.@Qualifier는 명시적으로 특정 빈을 지정하는 것이고 @Primary보다 우선순위가 높다.@Primary, 가끔 사용하는 빈은 @Qualifier로 지정하면 깔끔하게 관리할 수 있다.@Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {}
자바 애노테이션에는 상속이 없고, 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
//빈 정의 시
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
//생성자 자동 주입시
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
But, 명확한 목적 없이 무분별하게 사용하면 유지보수에 혼란이 생기므로 주의하자!
할인 서비스를 제공하는데 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자!
이럴 땐 모든 DiscountPolicy 구현체를 한번에 주입받고 클라이언트 선택에 따라 전략적으로 실행하는 방법이 있다.
@Component
public class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
// 모든 DiscountPolicy 빈이 Map과 List로 자동 주입됨
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy policy = policyMap.get(discountCode);
// 클라이언트가 고른 빈의 discount 메서드 실행
return policy.discount(member, price);
}
}
DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. 1. 편리한 자동 기능을 기본으로 사용하자
2. 그럼 언제 빈 등록을 사용할까 ❓
3. 비지니스 로직 중 다형성을 적극 활용할 때는 ❓
Map<String, DiscountPolicy> 에 어떤 빈들이 주입될지 파악하기 어렵다.