다양한 의존관계 주입 방법
- 생성자 주입: 한 번만 실행되므로, 누락을 방지할 수 있고 불변(final)함. 생성자가 하나면@Autowired를 생략 가능.
- 세터 주입: 세터 메서드에 @Autowired를 통해 자동 주입. 선택, 변경 가능성이 있는 의존관계에 사용하지만 거의 X
- 필드 주입: 필드에 @Autowired를 통해 바로 필드에 자동 주입. 외부에서 변경이 불가능해서 테스트 하기 힘듦.
- 일반 메서드 주입: init 등의 초기화 함수에 @Autowired를 통해 일반 메서드로도 주입하는 것. 한 번에 여러 필드에 의존관계를 자동 주입 가능. 굳이 사용 X
옵션 처리
- @Autowired의 기본 값인 required = true가 되어있으면, 등록된 스프링 빈이 없을 때 오류가 발생한다.
- 그래서 required = false로 설정함으로써, 자동 주입할 대상이 없으면 메서드 자체가 호출이 안되게 한다.
- 매개변수에 @Nullable로 해두면 자동 주입할 대상이 없을 때 null이 입력된다.
- Optional<>은 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
public class AutowiredTest {
@Test
void AutowiredOption() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
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("nobean3 = " + nobean3);
}
}
}
롬복
- Getter, Setter, 생성자 등을 어노테이션(@Getter, @Setter 등)을 통해 자동 생성해주는 라이브러리.
- @RequiredArgsConstructor를 통해 final 필드를, 매개변수로 받아서 생성자를 통해 주입해주는 생성자를 생성해줌.
- build.gradle에 롬복 설정을 추가해준다. 스프링을 처음 초기화 할 때 롬복 라이브러리를 추가해서 만들어도 된다.
- 인텔리제이에 롬복 플러그인을 설치하고, File->Settings에서 Annotation Processors 기능을 활성화 한다.
Autowired 매칭
- 타입 매칭: 해당 타입의 빈이 하나만 등록되어 있으면 그대로 가져다 씀
- 반면 타입 매칭의 결과가 2개 이상이라면, 파라미터 명으로 빈 이름을 매칭.
- @Qualifer를 통해서도 가능. @Qualifer는 @Qualifer를 찾는 용도로만 사용하는게 명확하고 좋다.
- @Primary를 통해, 해당 어노테이션이 달린 클래스를 우선적으로 매칭. 이거는 많이 씀.
- 우선순위는 @Qualifer가 @Primary보다 높다. @Qualifer가 더 상세하기 때문.
// 파라미터 명으로 빈 이름 매칭
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
// ------------------------@Qualifer로--------------------------------------
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
}
else {
return 0;
}
}
}
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
}
else {
return 0;
}
}
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// ---------------------------@Primary------------------------------
// DiscountPolicy 빈이 두 개 이상 찾아지면, Primary인 RateDiscountPolicy가 매칭됨.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
}
else {
return 0;
}
}
}
어노테이션 직접 만들기
- 무분별하게 사용하면 유지보수에 혼란 유발 가능.
package hello.core.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 {
}
조회한 빈이 모두 필요할 때, List, Map
- 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있을 때, 이러한 전략 패턴을 적용.
- 즉 할인을 적용받는 모든 상품들에 대해, 할인률을 동적으로 변경하기 위한 것임.
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");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
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;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}
자동, 수동의 올바른 실무 운영 기준
- 업무 로직(비즈니스 로직) 빈은 자동 기능을 적극 사용하는 것이 좋다. 컨트롤러, 서비스처럼 계층이 명확하고 그 수가 많고, 보통 문제가 발생하기 명확하게 파악하기 쉽기 때문.
- 기술 지원 빈(공통 기술, AOP, DB 커넥션 등)에 수동 빈 등록을 사용하는 것이 좋다. 업무 로직에 비해 수가 매우 적고, 애플리케이션 전반에 걸쳐 영향을 미치기 때문. 적용이 잘 되고 있는지 명확하게 보이지 않기 때문.
- DiscountPolicy 처럼 어떤 빈이 들어올지 파악하기 위해서 여러 코드를 찾아봐야 하는 경우 수동 빈으로 등록하거나, 자동으로 할려면 특정 패키지에 같이 묶어두는게 좋다.
- 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다.
// 자동 등록을 사용하지만, 같은 패키지로 묶어서 관리
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}