728x90
다양한 의존관계 주입 방법
의존관계 주입은 생성자 주입, setter 주입, 필드 주입, 일반 메서드 주입이 있다.
생성자 주입
- 생성자를 통해 의존 관계를 주입 받는 방법
- 생성자가 딱 1개만 있으면 @Autowired를 붙이지 않아도 자동 주입 된다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
- 왜 필드에 "final 키워드를 작성할까?"
- final 변수는 재할당 불가, 생성자나 초기화 block을 통해 "무조건" 값을 가져야만 함
- 불변, 필수 의존관계에 사용
setter 주입
- 생성자 주입
- 불변/필수
- setter
- 변경/선택
- setXXX를 사용하면 메서드를 통해 변경할 수 있음
- setXXX를 "선택적"으로 호출하여 주입하여 "선택"할 수 있음
Q.
setter주입은 선택, 변경
생성자 주입은 불변, 필수 라고 말씀하셨습니다.
setter주입은 setXXX 메서드를 통하여 변경할 수 있고, 파라미터마다 setXXX를 만들면 선택적으로 주입할 수 있다.
생성자 주입은 생성자가 1번 호출 되는 것이 보장되기 때문에 불변이라 할 수 있다.
그러나 필수 이 부분은 어느 부분때문에 필수라고 말할 수 있을까?
A.
생성자로 설정한 객체는 별도의 수정자가 없으면 변경이 불가하므로 불변이다.
필수는 생성자의 특징을 말하는 것이 아니라 파라미터가 필수적으로 들어와야한다.
왜?
setter주입은 해당 set 메서드를 사용하지 않으면 상관이 없지만, 생성자는 클래스를 생성할 때 당연하게 실행이 되니까 파라미터가 빈으로 존재하지 않으면, 클래스의 객체를 생성하지 못한다.
필드 주입
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
- 장점
- 매우 편함!
- 단점
- 테스트를 돌릴 때에 순수 자바 코드로 작성하기 어렵다.
- 외부에서 변경이 어려움
- memberRepository 나 discountPolicy 에 접근할 수 있는 메서드가 없다면 해당 객체에 접근할 수 없다!!
- 테스트
- 순수 자바 코드로 작성한 테스트
- 일반적으로 new 로 객체를 생성
- 당연히 @Autowired 작동하지 않음
- 당연히 의존관계 주입 X, 해당 필드 NPE 발생!
- 스프링 컨테이너를 띄워서 실행하는 테스트 (feat. @SpringBootTest)
- Bean을 사용하여 의존관계 주입
- @Autowired 작동하기 위해서는 스프링 컨테이너가 "관리"해야함
- 즉, @SpringBootTest가 없다면 @Autowired 사용할 수 없다!!
- 순수 자바 코드로 작성한 테스트
- 그래서 필드 주입은 "테스트 코드"를 작성할 때 많이 사용함!
- 왜? 애플리케이션의 실제 코드와 관계가 없기 때문~
Q.
setter주입은 단계가 나눠져있고, 생성자 의존관계 주입은 빈을 등록하면서 의존관계 주입이 동시에 일어난다고 말씀해주셨습니다.
단계로 나눈다는 말씀은 빈을 등록하고, 의존관계 주입을 할 때, 컨테이너에서 빈을 조회하여 주입시킨다. 이 부분은 이해가 됩니다.
결국에는 setter이든 생성자이든 단계가 나눠지거나 동시에 일어난다인가요?
A.
setter주입은 빈을 생성할 때 의존관계에 있는 빈들을 주입하지 않고 나중에 별도로 set 메서드를 사용하여
주입해야한다. 그래서 빈 등록 단계와 의존관계 주입이 분리가 된다고 말할 수 있다.
출처 : QnA
생성자 주입을 택해야 하는 이유
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
@Test
public void setterInjection() throws Exception {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "a", 1000);
}
위와 같이 테스트를 작성하고 돌리면 결과가 어떻게 될까??
- 결과적으로 NPE 발생
- 왜????????
- OrderServiceImpl MemberRepository와 DiscountPolicy가 필요함
- 그러나 setter 주입으로 각각의 객체가 자동으로 의존관계 주입이 되지 않고 setter를 호출해야함
- 따라서 OrderServiceImpl 코드를 보고나서 "아 이건 setter로 주입을 해줘야하는구나"라고 알 수 있음
정리
- setter주입을 사용할 경우에 테스트를 순수 자바 코드로 실행할 경우
- setter를 사용하여 MemberRepository와 DiscountPolicy 객체를 생성하지 않을 경우 NPE가 발생
- setter 주입은 직접 OrderServiceImpl 코드를 보고 나서 알 수 있음
Lombok
해당 강의에서는 코드를 최적화할 수 있는 롬복 플러그인을 써보았다.
@Getter
@Setter
private 변수를 생성하게 되면 우리는 getter setter 메서드를 이용했다.
항상 해당 코드를 작성하기 번거롭기 때문에 롬복에서는 위와 같은 어노테이션을 사용해 편하게
getter setter 메서드를 만들어준다.
@ToString
위 어노테이션은 toString을 만들어준다.
@NoArgsConstructor
파라미터가 없는 기본 생성자를 생성
@RequiredArgsConstructor
final이 붙은 필드 값만 파라미터로 생성자를 생성
@AllArgsConstructor
모든 필드를 파라미터로 생성자를 생성
@Autowired를 사용하는데 조회 빈이 2개 이상 나올 때
- DiscountPolicy의 하위 타입인 RateDiscountPolicy, FixDiscountPolicy 모두 빈에 등록
- OrderServiceImpl 생성자 주입 시 DiscountPolicy에 2개의 빈이 조회됨
- 발생
- NoUniqueBeanDefinitionException
- 로그 : expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
- 발생
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
그렇다면 위와 같은 예외를 발생시키지 않기 위해서는 3가지 방법이 있다.
@Autowired 필드 명 매칭
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
- Autowired는 처음에 타입 매칭 시도
- 조회된 빈이 여러 개일 경우
- 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭함
- 즉, 처음엔 DiscountPolicy로 매칭 시도
- RateDiscountPolicy, FixDiscountPolicy 조회
- rateDiscountPolicy, fixDiscountPolicy으로 "빈 이름"이 등록되어 있기 때문에
- rateDiscountPolicy에 RateDiscountPolicy 빈이 주입
@Qualifier
//OrderServiceImpl.class
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//RateDiscountPolicy.class
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy{
//code
}
- 빈 이름을 변경 X (헷갈리지 말자)
- 클래스 이름을 그대로 사용하면서, 이 해당 빈이 대표하는(?) 이름이라고 생각하면 된다.
- Autowired와 비슷하게 처음엔 @Qualifier끼리 매칭
- 없으면 빈 이름 매칭
- 또 없으면 NoSuchBeanDefinitionException 발생
- 단점
- 사용하는 모든 곳에 @Qualifier 붙여야함
@Primary
- 조회된 여러 빈 중에 우선순위가 제일 높음
- RateDiscountPolicy 클래스에 @Primary 어노테이션을 넣으면 DiscountPolicy에 RateDiscountPolicy 주입
- Qualifier와 다르게 생성자 파라미터에 Primary 넣지 않아도 됨
Qualifier vs Primary
Qualfiier
- 우선순위가 더 높음
- 사용하는 곳마다 Qualifier
- 코드 지저분
Primary
- 우선순위를 높일 클래스에만 붙이면 됨
- 코드 깔끔
Primary, Qualifier는 어디에 사용할까?
- 자주 사용하는 메인 DB의 커넥션을 획득하는 스프링 빈
- 특별한 기능으로 가끔 사용하는 서브 DB의 커넥션을 획득하는 스프링 빈
- 메인 DB의 스프링 빈에 @Primary를 적용하여 편리하게 조회 가능
- 여러 개의 DB가 있을 경우 @Qualifier를 이용하여 "secondDB", "thirdDB" 등 별칭을 이용하여 쉽게 구분 가능!
AnnotationConfigApplicationContext는 스프링 컨테이너다
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
- new AnnotationConfigApplicationContext() 을 통해 스프링 컨테이너 생성
- AutoAppConfig.class, DiscountService를 파라미터로 넘기면서 해당 클래스를 스프링 빈으로 등록
Q.
DiscountService는 @Component,
@Configuration이
없는데 어떻게 빈으로 등록됐으며, 생성자를 이용한 의존관계 주입을 할 수 있는걸까?
A.
스프링 컨테이너(AnnotationConfigApplicationContext)를 만들 때 파라미터를 넘기면, 해당 클래스를 특별하게 자동으로 스프링 빈으로 등록해준다고 한다.
출처 : QnA
조회한 빈이 모두 필요할 때, List || Map
조회한 빈이 모두 필요할 경우가 있을까?
- 할인 서비스에서 해당 클라이언트에 해당하는 할인의 종류가 여러 개 있다고 생각해보자
- 할인이라는 "전략"이 다양하게 있는 것
- 전략 패턴을 매우 간단하게 구현할 수 있음
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);
return discountPolicy.discount(member, price);
}
}
/*
policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@64a8c844, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@3f6db3fb}
policies = [hello.core.discount.FixDiscountPolicy@64a8c844, hello.core.discount.RateDiscountPolicy@3f6db3fb]
*/
- Map
- key : 등록된 빈의 이름
- value : 빈 저장
- List
- 빈 저장
이렇게도 의존관계 주입이 가능하다는 것을 보고 매우 신기했다!!!
언젠가 유용하게 쓸 것 같다
REFERENCES
출처 : 인프런 스프링 핵심 원리 - 기본편 (김영한 강사님)
728x90
'Spring 강의 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
스프링 핵심 원리 - 빈 스코프(2022.03.21 수정) (0) | 2021.08.09 |
---|---|
스프링 핵심 원리 - 빈 생명주기 콜백(2022.03.12 수정) (0) | 2021.08.09 |
스프링 핵심 원리 - 컴포넌트 스캔 (0) | 2021.08.06 |
스프링 핵심 원리 - 싱글톤 컨테이너 (0) | 2021.08.04 |
스프링 핵심 원리 - 스프링 컨테이너와 스프링 빈 (0) | 2021.08.02 |