의존관계 주입은 다음 네가지 방법으로 가능하다.
생성자를 통해 의존관계를 주입받는 방법
@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;
}
}
특징
@Autowired
생략 가능setter라고 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계 주입
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
특징
필드에 바로 주입하는 방법
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
}
특징
일반 메서드를 통해 주입받는다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy, discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
특징
참고>
- 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
일반 클래스에서는 @Autowired가 동작하지 않는다.
주입 받을 객체 타입의 스프링빈이 존재하지 않아도 동작해야하는 경우가 있다.
@Autowired
만 사용한다면 기본 값은 @Autowired(required=true)
이므로 오류가 발생한다.
자동 주입 대상을 옵션으로 처리하는 방법
@Autowired(required=false)
: 주입받을 스프링빈이 없으면 수정자 메서드 자체를 호출하지 않음@Nullable
: 주입 받을 스프링 빈이 없으면 null을 주입Optional<>
: 주입 받을 스프링 빈이 없으면 Optional.empty
를 주입static class TestBean {
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean2 = " + noBean3);
}
}
TestBean
을 스프링 빈으로 등록할 때 의존관계 주입을 위해 @Autowired
를 호출한다.
출력 결과를 보면 Memeber
클래스는 스프링 빈으로 등록되지 않았기 때문에
setNoBean1()
: @Autowired(required=false)
이므로 호출 xsetNoBean2()
: 파라미터에 @Nullable
을 사용하면 주입받을 스프링 빈이 없는 경우 null
이 들어온다.setNoBean3()
: 파라미터 타입이 Optional이면 주입받을 스프링 빈이 없는 경우 Optional.empty
가 들어온다.스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.
생성자 주입을 사용하면 필드에 fianl
을 사용할 수 있다.
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;
}
}
final
을 사용하면 생성자에서 초기화 하는 부분이 없는 경우 오류를 컴파일 단계에서 알 수 있다. 또한 이후에 필드를 변경할 수 없다.
수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 호출 이후에 호출되므로 필드에 final
을 사용할 수 없다.
정리
- 생성자 주입 방식은 프레임워크에 의존하지 않고 자바 언어의 특징을 잘 살리는 방법이다.
- 기본적으로 생성자 주입을 사용하고 필수 값이 아닌 경우에 수정자 주입을 옵션으로 부여하면 된다.
- 필드 주입은 사용하지 않는게 좋다.
@Autowired
는 의존 관계를 주입할 스프링 빈을 타입으로 조회한다. 스프링 빈을 조회할 때 같은 타입의 빈이 두 개 이상일 때 NoUniqueBeanDefinitionException
오류가 발생한다.
해결 방법
@Autowired
필드명 매칭@Qualifier
사용@Primary
사용@Autowired
필드명 매칭@Autowired
는 타입 매칭을 시도하고 이때 여러 빈이 있으면 필드이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
타입으로 조회한 스프링빈이 두개인 경우 파라미터의 이름과 일치하는 이름의 스프링 빈을 주입한다.
@Qualifier
@Qualifier
는 추가 구분자를 붙여주는 방법이다. 빈 이름을 바꾸는 것은 아니다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
}
빈 등록시에 @Qualify
로 이름을 부여한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
주입할 때에도 같은 이름의 @Qualifier
를 파라미터에 지정하여 조회한 스프링 빈이 여러개일 때 한가지를 매칭할 수 있다.
같은 이름의 @Qualifier
가 사용된 스프링빈 매칭에 실패하면 해당 이름을 가진 스프링 빈을 추가로 찾는다.
다음과 같이 애노테이션을 직접 만들어서 사용할 수도 있다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
이렇게 여러 애노테이션을 모아서 새로운 애노테이션을 만들어 사용하는 기능은 스프링이 지원하는 기능이다.
@Primary
@Primary
는 우선순위를 정하는 방법이다. @Autowired
시에 여러 빈이 매칭되면 @Primary
가 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
같은 타입의 스프링 빈이 두개 등록되고 생성자에서 주입하는 경우
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Primary
가 사용된 빈이 주입된다.
@Primary
는 아무것도 지정되지 않은 필드나 생성자 파라미터에 주입할 스프링 빈이 여러개인 경우 우선적으로@Primary
가 주입되는 것이다.
@Primary
가 사용된 빈이 있고@Qualifier
사용된 빈이 있을 때
필드나 생성자에서@Qualifier
를 지정하면@Qualifier
가 우선권을 가진다.
어떤 타입의 스프링빈을 모두 조회하여 사용해야하는 경우가 있다.
예를들어 할인 서비스를 제공하는데 클라이언트가 그 종류를 선택하는 경우
스프링을 사용하면 전략 패턴(Strategy Pattern)을 매우 간단하게 구현할 수 있다.
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class,DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member,10000,"fixDiscountPolicy");
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
}
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;
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}
DiscountService
는 스프링 빈으로 등록될 때
DiscountPolicy
타입의 모든 스프링 빈을 <스프링빈 이름, 스프링빈 인스턴스> 를 <key,value>로 가지는 Map을 주입받는다. DiscountService
의 discount
메서드를 활용하면 클라이언트 코드에서 어떤 할인 정책의 스프링 빈 객체를 사용할지를 정할 수 있다.
이렇게 동적으로 객체의 행위를 정하여 행위를 유연하게 확장할 수 있도록 하는 것을 전략 패턴이라고 한다.