"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.
📝의존관계 주입 (@Autowired)
의존관계 주입은 크게 4가지가 있습니다.
- 생성자 주입
- 수정자 주입 (setter)
- 필드 주입
- 일반 메서드 주입 (많이 안 쓰기 때문에 패스)
생성자 주입
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 생성자 1개일 때 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
생성자를 통해 의존관계를 주입 받습니다. 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 됩니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.
수정자 주입
@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;
}
}
setter를 통해 의존관계를 주입 받습니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.
필드 주입
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
필드에 바로 주입하는 방법입니다. 코드가 간별하지만 외부에서 변경이 불가능해서 테스트 하기 어렵다는 단점이 있다. 예를 들면 setDiscountPolicy, setMemberRepository같은 Setter가 존재하면 DI 컨테이너(스프링)를 따로 안 띄워도 직접 넣어서 테스트할 때 사용이 가능한데 필드만 있는 경우는 스프링을 다 띄우지 않는 이상 테스트하기 어렵다.
📝어떤 걸 사용해야할까?
생성자 주입을 사용해야합니다. 필드 주입의 경우 테스트가 어려우며 Setter의 경우 변경가능성이 있기 때문에 객체 생성시 1번만 호출되게 설계할 수 있습니다.(불변) 또한 순수 자바코드로 단위테스트도 가능합니다.
final 키워드 사용시 생성자에 값이 없으면 컴파일 오류를 발생시켜 장애를 방지할 수 있습니다.
📝빈이 2개 이상일 때 해결 (@Qulifier, @Primary)
@Autowired
- 타입 매칭
- 타입 매칭의 결과가 2개이상일 때 필드명, 파라미터명으로 빈 이름 매칭
@Autowired
private DiscountPolicy rateDiscountPolicy
DiscountPolicy를 상속받은 RateDiscountPolicy, FixDiscountPolicy가 있으면 위 코드와 같이 했을 때 필드명을 보고 주입하게 된다. 즉, RateDiscountPolicy를 주입하게 된다.
@Qualifier
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Qualifier의 경우 @Qualifier에 등록한 Bean이름을 찾아서 주입합니다.
// 어노테이션 생성
@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 {}
위 같이 @Qualifier지정할 때 따로 어노테이션을 따로 만들어서 사용할 수 있는데 이렇게 사용하면 컴파일시기에 에러를 잡아낼 수 있는 장점이 있다.
@Primary
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Primary의 경우 여러개 빈이 있어도 Primary 등록한 최상위 것이 먼저 등록됩니다. Qualifier의 경우 코드가 지저분해지기 때문에 @Primray를 더 잘 사용합니다.
📝등록된 Bean 다 가져오기
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);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
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);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
DiscountPolicy 인터페이스를 상속받은 고정할인(fixDiscountPolicy)하고 퍼센티지할인(rateDiscountPolicy)이 Bean으로 만들어져있습니다. 여기에 사용자가 넘긴 정보에 따라 고정 또는 퍼센티지를 결정해야할 경우 DiscounyPolicy구현체인 Bean으로 만들어진 걸 가져와서 넘긴 파라미터에 따라 다르게 처리할 수 있습니다.
Map의 경우 키에 Bean의 이름이 들어가고 값에는 구현체가 들어가게 됩니다. List의 경우는 키값이 없기 때문에 구현체만 들어가게 됩니다.
📝 수동 등록 vs 자동 등록
수동등록
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
Configuration을 쓰고 Bean을 그 안에서 직접 생성해서 관리합니다. 한 곳에 모여있기 때문에 보기가 편합니다.
자동등록
@Component
public class DiscountPolicy {
}
Component어노테이션 기반으로 만들어진 어노테이션을 붙이면 Component Scan범위에 있으면 찾아서 Bean 등록해줍니다
요즘은 자동등록을 많이 쓰지만 수동등록의 경우도 필요할 때 써야한다. 보통 업무로직(Controller, Service, Repository)의 경우 자동 등록을 사용하고 기술지원(로깅, AOP 등)의 경우 Configuration으로 수동관리하는 편이다.