s
s
spring
Search…
의존 관계 주입(DI)

의존관계 주입 종류

의존관계 주입에는 크게 4가지 방법이 있다.
  1. 1.
    생성자 주입
  2. 2.
    수정자 주입(setter)
  3. 3.
    필드 주입
  4. 4.
    일반 메서드 주입

수정자 주입

1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
private MemberRepository memberRepository;
5
private DiscountPolicy discountPolicy;
6
7
@Autowired
8
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
9
this.discountPolicy = discountPolicy;
10
}
11
12
@Autowired
13
public void setMemberRepository(MemberRepository memberRepository) {
14
this.memberRepository = memberRepository;
15
}
16
}
Copied!
수정자 메서드(setter)를 통해서 의존관계를 주입한다. 선택, 변경 가능성이 있는 의존관계에서 사용된다.
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
private MemberRepository memberRepository;
5
private DiscountPolicy discountPolicy;
6
7
@Autowired(required = false)
8
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
9
this.discountPolicy = discountPolicy;
10
}
11
12
@Autowired
13
public void setMemberRepository(MemberRepository memberRepository) {
14
this.memberRepository = memberRepository;
15
}
16
}
Copied!
@Autowired 의 default는 주입할 대상이 없으면 오류가 발생하지만, @Autowired(required = false) 로 설정하면, 주입할 대상이 없어도 동작하게 할 수 있다.

필드 주입

1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
@Autowired
5
private MemberRepository memberRepository;
6
@Autowired
7
private final DiscountPolicy discountPolicy;
8
9
}
Copied!
  • 코드가 간결
  • 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
  • 실무에서는 사용하지 않는 것을 권장
    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드
    • 스프링 설정을 목적으로 하는 @Configuration 같이 특별한 용도로 사용

일반 메서드 주입

1
@Component
2
public class OrderServiceImpl implements OrderService{
3
private MemberRepository memberRepository;
4
private DiscountPolicy discountPolicy;
5
6
@Autowired
7
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
8
this.memberRepository = memberRepository;
9
this.discountPolicy = discountPolicy;
10
}
11
}
Copied!
  • 한번에 여러 필드를 주입받을 수 있다.
  • 일반적으로 잘 사용하지 않는다.

생성자 주입

생성자를 통해서 의존 관계를 주입받는 방법이다. 생성자 호출시점에 딱 1번만 호출되는 것이 보장되며, 불편, 필수 의존관계에 사용된다.
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// final은 반드시 값이 있어야한다.
5
private final MemberRepository memberRepository;
6
private final DiscountPolicy discountPolicy;
7
8
@Autowired
9
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
10
this.memberRepository = memberRepository;
11
this.discountPolicy = discountPolicy;
12
}
13
}
Copied!
생성자가 1개만 있으면, @Autowired를 생략해도 자동 주입된다.
과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 대부분이 생성자 주입을 권장한다.

불변

  • 대부분의 의존 관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
    • 대부분 의존 관계는 애플리케이션 종료 전까지 변하면 안된다.
  • 수정자 주입의 경우 setXxx 메서드를 public 으로 선언해야한다.
    • public으로 설정하게 되는 경우, 누군가가 실수로 변경할 수도 있을뿐더러, 변경이 되면 안되는 메서드를 public으로 설정하는 것은 좋은 설계가 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로, 불변하게 설계가 가능하다.

누락

프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에는
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
private MemberRepository memberRepository;
5
private DiscountPolicy discountPolicy;
6
7
@Autowired(required = false)
8
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
9
this.discountPolicy = discountPolicy;
10
}
11
12
@Autowired
13
public void setMemberRepository(MemberRepository memberRepository) {
14
this.memberRepository = memberRepository;
15
}
16
}
Copied!
1
class OrderServiceImplTest {
2
@Test
3
void createOrder() {
4
OrderServiceImpl orderService = new OrderServiceImpl();
5
orderService.createOrder(1L, "itemA", 10000);
6
}
7
}
Copied!
1
java.lnag.NullPointerException
2
// ...
Copied!
memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문에 실행은 되나 NullPointException 이 발생한다.
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// final은 반드시 값이 있어야한다.
5
private final MemberRepository memberRepository;
6
private final DiscountPolicy discountPolicy;
7
8
@Autowired
9
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
10
this.memberRepository = memberRepository;
11
this.discountPolicy = discountPolicy;
12
}
13
}
Copied!
1
class OrderServiceImplTest {
2
@Test
3
void createOrder() {
4
OrderServiceImpl orderService = new OrderServiceImpl();
5
orderService.createOrder(1L, "itemA", 10000);
6
}
7
}
Copied!
생성자 주입을 사용하면 주입 데이터를 누락 했을 때 컴파일 오류가 발생한다. 필요로 하는 타입을 바로 알 수 있기때문에 누락되는 경우가 없다.
1
java: constructor OrderServiceImpl in class dh0023.springcore.order.service.OrderServiceImpl cannot be applied to given types;
2
required: dh0023.springcore.member.repository.MemberRepository,dh0023.springcore.discount.service.DiscountPolicy
3
found: no arguments
4
reason: actual and formal argument lists differ in length
Copied!

final 키워드

1
@Component
2
public class OrderServiceImpl implements OrderService {
3
private final MemberRepository memberRepository;
4
private final DiscountPolicy discountPolicy;
5
6
@Autowired
7
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
8
this.memberRepository = memberRepository;
9
// this.discountPolicy = discountPolicy;
10
}
11
}
Copied!
생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
1
java: variable discountPolicy might not have been initialized
Copied!
컴파일 단계에서 오류를 발견하는 것은 가장 빠르고 좋은 오류이다.
수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없으며, 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.

옵션 처리

주입할 스프링 빈이 없어도 동작해야할 때가 있다.

@Autowired(require = false)

1
public class AutowiredTest {
2
3
@Test
4
void AutowiredOption() {
5
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
6
TestBean bean = ac.getBean(TestBean.class);
7
}
8
9
static class TestBean {
10
11
@Autowired
12
public void setNoBean(Member member) {
13
System.out.println("member = " + member);
14
}
15
}
16
}
Copied!
bean으로 등록되지 않은 Member 클래스를 @Autowired를 하면 다음과 같은 오류가 발생한다.
1
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'autowiredTest.TestBean': Unsatisfied dependency expressed through method 'setNoBean' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'dh0023.springcore.member.domain.Member' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
2
3
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:768)
4
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:720)
5
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119)
6
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399)
7
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1413)
8
....
Copied!
다음과 같이 @Autowired(required = false)로 자동주입 대상을 주입할 수 있다.
1
public class AutowiredTest {
2
3
@Test
4
void AutowiredOption() {
5
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
6
TestBean bean = ac.getBean(TestBean.class);
7
}
8
9
static class TestBean {
10
11
@Autowired(required = false)
12
public void setNoBean(Member member) {
13
System.out.println("member = " + member);
14
}
15
}
16
}
Copied!
이 경우에는 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안된다. (로그가 출력이 안되는 것을 확인 가능)
1
23:25:53.088 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.spring[email protected]223aa2f7
2
23:25:53.165 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
3
23:25:53.630 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
4
23:25:53.637 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
5
23:25:53.640 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
6
23:25:53.643 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
7
23:25:53.819 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autowiredTest.TestBean'
8
9
10
Process finished with exit code 0
Copied!
1
public class AutowiredTest {
2
3
@Test
4
void AutowiredOption() {
5
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
6
TestBean bean = ac.getBean(TestBean.class);
7
}
8
9
static class TestBean {
10
11
@Autowired
12
public void setNoBean(@Nullable Member member) {
13
System.out.println("member = " + member);
14
}
15
16
}
17
}
Copied!
@Nullable 로 설정할 경우 자동 주입할 대상이 없으면 null이 입력된다.
1
23:27:46.560 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.spring[email protected]223aa2f7
2
23:27:46.591 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
3
23:27:46.721 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
4
23:27:46.730 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
5
23:27:46.733 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
6
23:27:46.736 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
7
23:27:46.757 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autowiredTest.TestBean'
8
member = null
9
10
Process finished with exit code 0
Copied!

Optional

1
public class AutowiredTest {
2
3
@Test
4
void AutowiredOption() {
5
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
6
TestBean bean = ac.getBean(TestBean.class);
7
}
8
9
static class TestBean {
10
11
@Autowired
12
public void setNoBean(Optional<Member> member) {
13
System.out.println("member = " + member);
14
}
15
16
}
17
}
Copied!
Optional자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
1
23:27:46.560 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.spring[email protected]223aa2f7
2
23:27:46.591 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
3
23:27:46.721 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
4
23:27:46.730 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
5
23:27:46.733 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
6
23:27:46.736 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
7
23:27:46.757 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autowiredTest.TestBean'
8
member = Optional.empty
9
10
Process finished with exit code 0
Copied!

조회 빈이 2개 이상인 경우

기존에는 RateDiscountPolicy@Component로 등록을 했었는데, FixDiscountPolicy까지 @Component로 등록하게 되면 NoUniqueBeanDefinitionException 오류가 발생한다.
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// final은 반드시 값이 있어야한다.
5
private final MemberRepository memberRepository;
6
private final DiscountPolicy discountPolicy;
7
8
// 생성자 의존관계 주입
9
@Autowired
10
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
11
this.memberRepository = memberRepository;
12
this.discountPolicy = discountPolicy;
13
}
14
}
Copied!
1
Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'dh0023.springcore.discount.service.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
Copied!
이때 하위 타입을 지정할 수 있지만, 이는 DIP를 위배하고 유연성이 떨어진다. 이름만 다르고 완전히 똑같은 타입의 스프링빈이 여러개(상속, 구현)인 경우 해결이 안된다.
이때, 자동 의존 주입으로 해결할 수 있는 방법이 약 3가지 정도 있다.

@Autowired 필드명

@Autowired 는 최초에 타입 매칭을 시도하는데, 이때 빈이 여러개라면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

생성자 주입

  • AS-IS
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// final은 반드시 값이 있어야한다.
5
private final MemberRepository memberRepository;
6
private final DiscountPolicy discountPolicy;
7
8
// 생성자 의존관계 주입
9
@Autowired
10
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
11
this.memberRepository = memberRepository;
12
this.discountPolicy = discountPolicy;
13
}
14
}
Copied!
  • TO-BE
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// final은 반드시 값이 있어야한다.
5
private final MemberRepository memberRepository;
6
private final DiscountPolicy discountPolicy;
7
8
// 생성자 의존관계 주입
9
@Autowired
10
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
11
this.memberRepository = memberRepository;
12
this.discountPolicy = rateDiscountPolicy;
13
}
14
}
Copied!

필드 주입

  • AS-IS
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// 필드 주입
5
@Autowired private MemberRepository memberRepository;
6
@Autowired private DiscountPolicy rateDiscountPolicy;
7
}
Copied!
  • TO-BE
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// 필드 주입
5
@Autowired private MemberRepository memberRepository;
6
@Autowired private DiscountPolicy disRcountPolicy;
7
}
Copied!

@Quilifier

@Quilifier 는 추가 구분자를 붙여주는 방법이다.
주입시 추가적인 방법을 제공하는 것이며, 빈 이름을 변경하는 것은 아니다.
1
/**
2
* @Component 어노테이션 추가로 빈설정
3
* 이때 빈이름을 설정하고 싶은 경우에는 @Component("빈이름")으로 설정할 수 있다.
4
* @Qualifier : 추가 구분자 설정
5
*/
6
@Component
7
@Qualifier("rateDiscountPolicy")
8
public class RateDiscountPolicy implements DiscountPolicy{
9
10
private final static int DIS_PER = 10;
11
12
@Override
13
public int discount(Member member, int price) {
14
if (member.getGrade() == Grade.VIP){
15
return price * DIS_PER / 100;
16
} else {
17
return 0;
18
}
19
}
20
}
Copied!
1
@Component
2
@Qualifier("fixDiscountPolicy")
3
public class FixDiscountPolicy implements DiscountPolicy{
4
5
private static final int DISCOUNT_AMT = 1000;
6
7
@Override
8
public int discount(Member member, int price) {
9
if(member.getGrade() == Grade.VIP) {
10
return DISCOUNT_AMT;
11
} else {
12
return 0;
13
}
14
}
15
}
Copied!
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// final은 반드시 값이 있어야한다.
5
private final MemberRepository memberRepository;
6
private final DiscountPolicy discountPolicy;
7
8
// 생성자 의존관계 주입
9
@Autowired
10
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("rateDiscountPolicy") DiscountPolicy discountPolicy) {
11
this.memberRepository = memberRepository;
12
this.discountPolicy = discountPolicy;
13
}
14
}
Copied!
다음과 같이 추가 구분자로 설정을 할 수 있다. 같은 @Qulifier 를 찾아서 주입해주는 것을 알 수 있다.
만약에 해당 이름으로 구분자를 못찾는 경우에는, 해당명으로 생성된 스프링 빈을 추가로 찾는다! @Qulifier@Qulifier 를 찾는 용도로만 사용하는 것이 가장 명확하다.
추가적으로 @Qualifier("fixDiscountPolicy") 의 명칭은 문자열이므로, 컴파일시 타입 체크가 불가능하다.

@Primary

@Primary 는 우선순위를 지정하는 방법이다.
1
@Component
2
@Primary
3
public class RateDiscountPolicy implements DiscountPolicy{}
4
@Component
5
public class FixDiscountPolicy implements DiscountPolicy{}
Copied!
다음과 같이 RateDiscountPolicy@Primary 를 설정하면, 의존성 주입시 우선권을 갖게된다.
1
@Component
2
public class OrderServiceImpl implements OrderService{
3
4
// final은 반드시 값이 있어야한다.
5
private final MemberRepository memberRepository;
6
private final DiscountPolicy discountPolicy;
7
8
// 생성자 의존관계 주입
9
@Autowired
10
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
11
this.memberRepository = memberRepository;
12
this.discountPolicy = discountPolicy;
13
}
14
}
Copied!
예를들어, 코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있는 경우에 편리하게 사용할 수 있다.

@Primary , @Qulifier

  • @Primary 는 기본값 처럼 동작하는 것이고, @Qulifier는 매우 상세하게 동작한다. 스프링은 자동보다 수동이, 넓은 범위보다 좁은 범위의 선택권이 우선순위가 높다.
  • 즉, @Qulifier가 더 높은 우선순위를 갖게된다.

조회한 빈이 모두 필요한 경우(List, Map)

1
package dh0023.springcore.autowired;
2
3
import dh0023.springcore.config.AutoAppConfig;
4
import dh0023.springcore.discount.service.DiscountPolicy;
5
import dh0023.springcore.member.domain.Grade;
6
import dh0023.springcore.member.domain.Member;
7
import org.assertj.core.api.Assertions;
8
import org.junit.jupiter.api.Test;
9
import org.springframework.beans.factory.annotation.Autowired;
10
import org.springframework.context.ApplicationContext;
11
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
12
13
import java.util.List;
14
import java.util.Map;
15
16
import static org.assertj.core.api.Assertions.*;
17
18
public class AllBeanTest {
19
20
@Test
21
void findAllBean() {
22
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
23
24
}
25
26
27
static class DiscountService {
28
private final Map<String, DiscountPolicy> policyMap;
29
private final List<DiscountPolicy> policyList;
30
31
@Autowired
32
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
33
this.policyList = policyList;
34
this.policyMap = policyMap;
35
36
System.out.println("policyMap = " + policyMap);
37
System.out.println("policyList = " + policyList);
38
}
39
}
40
}
Copied!
1
policyMap = {fixDisc[email protected]c055c54, rateDisco[email protected]25e2ab5a}
Copied!
다음과 같이 MapList로 모든 DiscountPolicy를 받아 올 수 있다. 각각 policyMap과 policyList에 FixDiscountPolicy와 RateDiscountPolicy가 들어가 잇는 것을 알 수 있다.

참고