의존관계 자동 주입
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;
}
}
말 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.
생성자 호출시점에 딱 한번만 호출되는 것이 보장된다.
따라서 불변, 필수 의존관계에 사용된다.
※ 생성자가 단 하나일 때는 @Autowired는 생략 할 수 있다.
[수정자 주입 예시]
@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라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
선택, 변경 가능성이 있는 의존관계에 사용된다.
[필드 주입 예시]
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
특별한 메서드 없이 필드에 바로 주입하는 방식이다.
코드가 간결해지지만 DI 프레임워크에 매우 의존적이고 외부에서 변경이 불가능하기 때문에 테스트 하기 힘들다는 치명적인 단점이 있다.
애플리케이션의 실제 코드와 관계없는 테스트 코드 등에 쓰인다.
[일반 메서드 주입]
@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;
}
}
일반 메서드를 통해서 주입 받을 수 있다.
한번에 여러 필드를 주입 받을 수 있지만 잘 쓰이진 않는다.
■ 생성자 주입을 선택해야 하는 이유
- 불변
대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 변경할 일이 없다(불변해야 한다.).
수정자 주입을 사용하면 setxxx 메서드를 public으로 열어두어야 한다.
변경하면 안되는 메서드를 열어 놓는 것은 좋은 설계 방법이 아니다.
- 누락
수정자 주입의 경우 set메서드로 의존관계 주입을 해야하는데 이를 누락 할 경우 실행은 되지만 NPE(Null Point Exception)이 발생한다.
반면 생성자 주입의 경우 주입 데이터를 누락할 경우 바로 컴파일 오류가 발생한다.
※생성자 주입을 사용하면 필드에 final 키워드를 사용할수 있고, 이를 이용하면 생성자에 값이 설정되지 않는 오류를 컴파일 시점에 막아줄 수 있다.
2. 롬북 라이브러리
롬북 라이브러리를 이용해서 의존관계 자동 주입시 코드를 더욱 간결하게 만들 수 있다.
[build.gradle에 롬북 라이브러리 환경 추가]
//lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//lombok 설정 추가 끝
dependencies {
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
}
build.gradle에 위와 같은 코드를 추가하면 롬북 라이브러리가 추가된다.
이후 다음과 같은 과정을 따라 lombok을 설치하고 실행한다.
- Prefrences -> plugin -> lombok 검색 설치 실행 (재시작)
- Prefrences -> Anntation Processors 검색 -> Enable annotation processing 체크 (재시작)
- 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인
[롬북을 적용전]
@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;
}
}
위의 코드는 롬북을 이용해 아래와 같이 변경 할 수 있다.
[롬북 적용]
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@RequiredArgsConstructor은 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
따라서 롬북 적용전과 후의 코드는 크게 다르지만 실제로는 완전 같은 기능을 하는 코드라고 할 수 있다.
최근에는 생성자를 딱 1개 두고 @RequiredArgsConstrutor를 사용하는 방법을 주로 사용한다고 한다.
3. 옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
자동 주입 대상을 처리하는 방법은 아래와 같은 3가지 방법이 있다.
[@Autowired(required = false)]
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
이 방식은 자동 주입할 대상이 없으면 메서드 자체가 호출이 되지 않는다.
member 인스턴스는 스프링 컨테이너에 등록되지 않았기 때문에 setNoBean1 메서드는 실행되지 않는다.
[org.springframework.lang.@Nullable]
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
이 방식은 자동 주입할 대상이 없으면 null이 입력된다.
[Optional<>]
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}
자동 주입할 대상이 없으면 Optional.empty가 입력된다.
4. 조회 되는 빈이 복수일 경우
@Autowired는 타입으로 조회하기 때문에 선택된 빈이 2개 이상일 때 오류가 발생한다(NoUniqueBeanDefinitionException).
이런 문제를 해결하기 위한 방법은 아래와 같다.
[필드 명 매칭]
//@Autowired
//private DiscountPolicy discountPolicy
@Autowired
private DiscountPolicy rateDiscountPolicy
@Autowired는 타입 매칭을 시도하고, 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
따라서 필드 명과 의존관계를 주입할 빈의 이름을 매칭 시키면 된다.
[@Qualifier 사용]
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
의존관계 주입 시 구분 되어야 하는 빈에 @Qualifier(명칭)를 붙여준다.
그 뒤, 의존관계를 주입할 시 주입할 타입 앞에 @Qualifier(명칭)을 붙여주면 복수의 빈이 조회될 시 명칭과 매치 되는 빈을 조회한다.
[@Primary 사용]
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Primary는 우선순위를 정하는 방법이다. @Autowired에서 여러개의 빈이 매칭되면 @Primary가 우선권을 갖는다.
※@Qualifier과 @Primary는 @Qualifier가 우선권을 갖는다.
5. 복수의 빈 조회
경우에 따라 복수의 빈을 주입받고 사용해야 할 때가 있다.
[Map과 List를 이용한 복수의 빈 조회]
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;
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 i, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, i);
}
}
}
DiscountService는 Map으로 빈 이름과 DiscountPolicy를, List로 DiscountPolicy를 주입받는다.
DiscountService를 스프링 컨테이너에 등록한 후 getBean을 이용해서 Map 또는 List를 이용해 필요한 DiscountPolicy를 선택해 사용하면 된다.
위의 예제에서는 discount 라는 메서드를 이용해 필요한 DiscountPolicy 객체를 가져 올 수 있다.
■ 복수의 빈을 이용할 때 주의 사항
복수의 빈을 사용할 때, Map과 List만 보고 어떤 빈들이 주입될 지, 각 빈들의 이름을 무엇일지를 쉽게 파악하기 어렵다.
따라서 이런 경우 주입되는 빈들을 특정 패키지에 따로 묶어놓는 것이 좋다.
[별도의 설정정보 생성]
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
위의 예제 처럼 할인 정책에 관한 객체를 별도의 설정정보에 묶어놓으면 복수의 빈을 사용할 때 어떤 빈들이 주입될지를 설정정보만으로 쉽게 파악할 수 있다.
6. 자동, 수동 빈 등록 기준
기본적으로 자동 빈을 이용하도록 한다.
다만 기술 지원 로직의 경우 수가 적고 애플리케이션 전반에 걸쳐 영향을 미치는 반면, 문제가 발생했을 때 정확히 들어나지 않는 문제가 있다.
따라서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.
왜냐하면 설정 정보에서 바로 확인할 수 있고 유지보수에 용이하기 때문이다.
'Spring > Spring 핵심 원리' 카테고리의 다른 글
#8 빈 스코프 (0) | 2021.06.14 |
---|---|
#7 빈 생명주기 콜백 (0) | 2021.06.10 |
#5 컴포넌트 스캔 (0) | 2021.05.14 |
#4 싱글톤 컨테이너 (0) | 2021.05.14 |
#3 스프링 빈 조회 (0) | 2021.05.14 |
댓글