2022. 5. 15. 13:45ㆍLanguage`/Spring
@Component & @ComponentScan에 의해서 자동으로 빈을 등록해준다면 스프링 빈들의 DI를 굳이 수동으로 해줄 필요도 없고 수동으로 하면 복잡해진다.
따라서 자동 빈 등록에 대한 "의존관계 자동 주입"도 더불어서 가져가주는게 낫다
DI 방식
1. 생성자 주입
우리가 지금까지 사용하고 있던 방식이 "생성자 주입"이고 이 방식을 대부분 사용한다
생성자 주입의 특징은 다음과 같다
생성자 호출시점에 딱 1번만 호출되는 것이 보장된다
불변/필수 의존관계에 사용된다
생성자는 객체가 생성될 때 딱 1번만 호출되기 때문에 만약 그때 DI가 된다면 이후에 변경될 일이 아예 없다
그리고 대부분 주입 대상을 필드 상에서 final로 지정하는데 final로 지정하면 초기화를 해주든 생성자에 넣어주든 해줘야 하기 때문에 필수적 의존관계상에서 많이 사용한다
그리고 어차피 대부분 현실에서는 처음 DI를 해주고 종료할 때까지 변경될 일이 없기 때문에 생성자 주입을 default로 사용하면 된다
일반적으로 스프링 컨테이너의 라이프사이클은 2가지로 나뉘게 된다
- 스프링 빈 등록
- 의존관계 주입
생성자 주입을 사용할 경우 스프링 빈으로 등록이 될 때 의존관계도 같이 주입이 된다. 따라서 라이프사이클 첫번째 단계에서 동시에 이루어진다고 볼 수 있다
@Component로 자동 빈 등록을 할 경우 의존관계에 대해서도 자동으로 주입해야 하는데 이 때 사용하는 것이 @Autowired이다
생성자가 1개만 존재할 경우 @Autowired는 생략이 가능하지만 그냥 쓰는 것이 좋은 것 같다
빈으로 등록이 잘 되었고 의존관계도 @Autowired에 의해서 잘 된것을 볼 수 있다
2. 수정자 주입 (setter 주입)
수정자 주입의 경우 setter로 의존관계를 주입하는 방식이다
수정자 주입의 특징은 다음과 같다
선택/변경 가능성이 있는 의존관계에 사용
생성자 주입의 경우 처음 딱 1번 호출됨에 따라서 절대로 변경될 수 없는 것에 비해서 수정자 주입의 경우 그냥 setter내부를 변경하면 의존관계도 변경된다
@Autowired의 경우 기본 동작은 "주입할 대상이 없으면 오류가 발생"한다
여기서 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정하면 된다
빈 등록이 잘 된 것을 볼 수 있다
※ 라이프사이클 비교 (생성자 vs setter)
<생성자 주입>
생성자 주입의 로그를 보면 memberServiceImpl에 대한 Autowiring 로그가 없는 것을 확인할 수 있다
→ 빈으로 생성되고 등록될 때 DI도 동시에 이루어진다
<setter 주입>
setter 주입의 로그를 보면 memberServiceImpl에 대한 Autowiring 로그가 따로 존재한다
→ 1. 빈으로 등록
→ 2. DI
이렇게 setter 주입은 빈 등록과 DI 라이프사이클이 나뉘어져 있다는 사실을 알 수 있다
3. 필드 주입
필드 주입은 그냥 필드에 @Autowired를 붙이면 되는 굉장히 간단한 DI방식이다. 하지만 이 방식을 IDE상에서도 권장하지 않고 되도록이면 쓰지 않는 것이 좋다
왜냐하면 필드 주입은 DI 프레임워크가 없다면 아무것도 할 수 없고 외부에서 변경도 불가능하기 때문에 테스트하기가 힘들다는 치명적인 단점이 존재한다
4. 일반 메소드 주입
이 경우도 한번에 여러 필드를 주입받을 수 있지만 굳이 사용할 필요는 없다
옵션 처리
주입할 스프링 빈이 없어도 동작을 해야하는 순간이 있다
하지만 @Autowired의 경우 주입 대상이 없다면 자동으로 오류가 발생하게 된다 (default → required = true)
따라서 자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다
1. Autowired(required = false) : 자동 주입 대상이 없으면 setter 메소드 자체가 호출 X
2. @Nullable : 자동 주입할 대상이 없다면 null 입력
3. Optional<> : 자동 주입할 대상이 없다면 Optional.empty 입력
@Autowired(required = false)
public void setNoBean1(Member member){
System.out.println("setNoBean1 = " + member);
}
@Autowired
public void setNoBean2(@Nullable Member member){
System.out.println("setNoBean2 = " + member);
}
@Autowired
public void setNoBean3(Optional<Member> member){
System.out.println("setNoBean3 = " + member);
}
Member라는 스프링 빈이 없기 때문에 setNoBean1의 경우 아예 호출이 되지 않은 것을 볼 수 있다
Lombok
Lombok은 자동으로 생성자를 만들어주고 거의 필드 주입처럼 편리하게 코딩을 할 수 있는 환경을 제공해준다
Lombok을 사용하려면 2가지 과정이 필요하다
- 일단 Lombok 플러그인이 없으면 기본적으로 설치를 해야 한다
1. build.gradle 설정
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
2. Annotation Processor 설정
이 설정을 끝냈다면 Lombok을 사용할 준비가 완료되었다
@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;
}
OrderServiceImpl에 대한 생성자 주입을 위와 같이 해주고 있다
여기에 Lombok을 적용해준다면 훨씬 편리하고 간결한 코드를 생성해낼 수 있다
이렇게 하면 생성자 DI가 완료되었다
@RequiredArgsConstructor는 "final이 붙은 필드"에 대해서 알아서 생성자를 만들어주기 때문에 생성자 주입을 활용할 경우 Lombok은 굉장히 편리한 수단이 될 수 있다
만약 의존관계를 추가할 경우 @RequiredArgsConstructor를 사용하지 않는다면 필드도 새로 만들고 그에 따라서 생성자에도 추가해야 하지만 @RequiredArgsConstructor을 사용하면 그냥 필드만 새로 만들면 생성자는 알아서 Lombok이 생성해준다
조회 빈 중복
@Autowired는 기본적으로 "타입"으로 조회를 한다
- getBean(<타입>)과 유사하게 동작
타입으로 조회할 때 "동일한 타입에 대해서 여러개의 빈이 존재하면"문제가 발생하게 된다
물론 이 경우 스프링 빈을 수동 등록해서 우선순위를 확보할 수도 있지만 자동 주입상에서 해결하는 방법은 다음과 같다
- 결국 "private final DiscountPolicy discountPolic"에서 DiscountPolicy를 구현한 2개의 정책 중 어느 것을 선택할지 몰라서 발생하는 오류
1. Autowired 필드명
@Autowired의 매칭 단계는 다음과 같다
1. "타입 매칭"을 시도한다
2. 타입 매칭에서 여러개의 빈이 존재한다면 그 다음에 "필드 이름 & 파라미터 이름"을 통해서 빈 이름을 추가로 매칭한다
이렇게 여러개이 빈이 존재할 경우 직접적으로 "필드명"을 통해서 지정해주는 것이다
rateDiscountPolicy로 필드명을 지정했으면 "rateDiscountPolicy 스프링 빈"이 매칭이 된다
fixDiscountPolicy로 필드명을 지정했으면 "fixDiscountPolicy 스프링 빈"이 매칭이 된다
그런데 이 방법은 다른 것들이 변경될 가능성이 존재하기 때문에 잘 사용되지 않는다
2. @Qualifier
@Qualifier는 "추가 구분자"를 붙여주는 방식이다
이것은 그냥 DI할 때 구분하기 위한 별명같은거지 실제로 빈 이름을 변경하는 것은 아니다
이렇게 각 빈에 대해서 Qualifier를 설정해준다
그리고 DI할 때 해당 타입에 대해서 @Qualifier를 지정해주면 된다
그런데 만약 "rateDiscountPolicy"라는 구분자를 찾지 못한다면 그 다음으로 빈 이름에 대해서 매칭을 시도하게 된다
근데 빈 이름에 대해서도 못찾는다면 "NoSuchBeanDefinitionException" 예외가 발생하게 된다
- @Qualifier의 구분자를 통해서 스프링 빈 매칭 시도 (rateDiscountPolicy)
- (1)과정에서 매칭 못한다면 빈 이름에 대해서 매칭 시도 (discountPolicy)
- (2)과정까지 찾지 못했다면 "NoSuchBeanDefinitionException" 예외 발생
@Qualifier의 단점은 DI할 때 모든 코드에 대해서 @Qualifier를 붙여줘야 한다는 점이다
3. @Primary
또 다른 방법은 여러가지 빈 가운데 우선순위를 정하는 방식이다
가장 우선순위가 높은 빈 클래스에 대해서 @Primary를 붙여주면 되는 간단한 방식이다
둘 중에서 RateDiscountPolicy한테 우선순위를 부여하게 된다면 DiscountPolicy 타입으로 조회할 때 RateDiscountPolicy로 지정이 된다