의존 관게 주입의 종류
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
1. 생성자 주입
여태 우리가 했던게 생성자 주입이다.
불변, 필수 의존관계일 때 사용
@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 생략 가능.(스프링 빈에서만)
2. 수정자 주입
우리가 아는 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;
}
}
3. 필드 주입
필드에 바로 주입하는 방법
외부에서 변경 불가능, 테스트하기 어려움.
DI프레임워크가 없으면 아무것도 할 수 없다.
그냥 사용하지 말자.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
4. 일반 메서드 주입
일반 메서드를 통해 주입
한번에 여러 필드 주입받을 수 있느나 잘 사용 X
@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(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
- org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
//호출 X
@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);
}
setNoBean2 = null
setNoBean3 = Optional.empty
setNoBean1() 은 @Autowired(required=false) 이므로 호출 자체가 안된다.
생성자 주입을 사용하자.
수정자 의존관계일 때
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;
}
//...
}
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
NPE(Null Point Exception)이 발생하는데, memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문
생성자 주입을 사용하면 컴파일 오류가 발생하고 IDE에서 어떤 값을 주입해야하는지 알 수 있다.
final키워드
생성자 주입에서 필드에 final을 사용할 수 있는데, 값 설정되지 않는 오류를 컴파일 시점에서 막아준다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
}
//...
}
필수인 DiscountPolicy 값 설정을 안한 것을 볼 수 있는데, 컴파일 시점에서 오류를 발생시킨다.
컴파일 오류가 세상에서 제일 좋은 오류임을 기억하자.
롬복과 최신 트렌드
저 위에 DiscountPolicy 값을 설정했다고 했을 때 이를 최적화해보자.
생성자가 1개이면 Autowired를 생략할 수 있다.
따라서 코드는
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
여기서 이제 롬복을 적용하자.
롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
이게 최종이다. 간결하다.
. 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class 를 열어보면 다음 코드가 추가되어 있는 것을 확인할 수 있다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
최근엔 생성자 1개로하고 Lombok 라이브러리를 추가하는 방법을 사용해 깔끔한 코드를 쓴다.
조회 빈이 2개 이상이면?
@Autowired 는 타입(Type)으로 조회한다.
스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
DiscountPolicy 의 하위 타입인 FixDiscountPolicy , RateDiscountPolicy 둘다 @Component를 통해 스프링 빈으로 선언해 보고
@Autowired
private DiscountPolicy discountPolicy
로 의존관계 주입을 실행하면
NoUniqueBeanDefinitionException 오류가 발생한다.
말 그대로 하나의 빈이 아닌 2개가 발견되었다는 것으로, 이를 어떻게 해결하는지 차근차근 알아보자.
1. @Autowired 필드 명 매칭
@Autowired
private DiscountPolicy rateDiscountPolicy
필드 명이 rateDiscountPolicy 이므로 정상 주입된다.
@Autowired 매칭 정리
1) 타입 매칭
2) 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭
2. @Qualifier
@Qualifier 는 추가 구분자를 붙여주는 방법
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
생성자 자동 주입 예시
@Autowired
public OrderServiceImpl(MemberRepository memberRepositor,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
수정자 자동 주입을 사용할 때도 동일하게 하면 된다.
@Bean을 사용할 때도 동일하게 하면 된다.
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
return new ...
}
3. @Primary
@Primary 는 우선순위를 정하는 방법
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
이렇게 사용하면 RateDiscountPolicy에 우선권이 있는 것이다.
이렇게 하고 실행하면 별 문제 없이 동작한다.
애노테이션 직접 만들기
@Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다. 다음과 같은 애노테이션을 만들어서 문제를 해결할 수 있다
package hello.core.annotataion;
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)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {} ```
```java
//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy
discountPolicy) {
this.discountPolicy = discountPolicy;
}
이렇게 사용하면 @Qualifier를 @MainDiscountPolicy로 사용하여 타입 체크를 하도록 할 수 있다.
조회한 빈이 모두 필요할 때, List, Map
해당 타입의 스프링 빈이 다 필요한 경우
ex) 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.
스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다
(사실 List, Map은 이해가 조금 되지 않지만 이해한 바로 최대한 써보자.)
package hello.core.autowired;
import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import
org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
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);
}
}
}
DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다.
( fixDiscountPolicy , rateDiscountPolicy 주입)
discount () : discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행
Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다
'Back-end' 카테고리의 다른 글
09. 빈 스코프 (1) | 2023.11.14 |
---|---|
08. 빈 생명주기 콜백 (0) | 2023.11.05 |
06. 컴포넌트 스캔 (1) | 2023.10.31 |
05. 싱글톤 컨테이너 (0) | 2023.10.31 |
04. 스프링 컨테이너와 스프링 빈 (1) | 2023.10.07 |