의존 관계 주입(DI)

의존관계 주입 종류

의존관계 주입에는 크게 4가지 방법이 있다.

  1. 생성자 주입

  2. 수정자 주입(setter)

  3. 필드 주입

  4. 일반 메서드 주입

수정자 주입

@Component
public class OrderServiceImpl implements OrderService{
    
    private  MemberRepository memberRepository;
    private  DiscountPolicy discountPolicy;

  	@Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
		
  	@Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

수정자 메서드(setter)를 통해서 의존관계를 주입한다. 선택, 변경 가능성이 있는 의존관계에서 사용된다.

@Component
public class OrderServiceImpl implements OrderService{
    
    private  MemberRepository memberRepository;
    private  DiscountPolicy discountPolicy;

  	@Autowired(required = false)
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
		
  	@Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

@Autowired 의 default는 주입할 대상이 없으면 오류가 발생하지만, @Autowired(required = false) 로 설정하면, 주입할 대상이 없어도 동작하게 할 수 있다.

필드 주입

@Component
public class OrderServiceImpl implements OrderService{

    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private final DiscountPolicy discountPolicy;

}
  • 코드가 간결

  • 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점

  • DI 프레임워크가 없으면 아무것도 할 수 없다.

  • 실무에서는 사용하지 않는 것을 권장

    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드

    • 스프링 설정을 목적으로 하는 @Configuration 같이 특별한 용도로 사용

일반 메서드 주입

@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;
    }
}
  • 한번에 여러 필드를 주입받을 수 있다.

  • 일반적으로 잘 사용하지 않는다.

생성자 주입

생성자를 통해서 의존 관계를 주입받는 방법이다. 생성자 호출시점에 딱 1번만 호출되는 것이 보장되며, 불편, 필수 의존관계에 사용된다.

@Component
public class OrderServiceImpl implements OrderService{

  	// final은 반드시 값이 있어야한다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

생성자가 1개만 있으면, @Autowired를 생략해도 자동 주입된다.

과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 대부분이 생성자 주입을 권장한다.

불변

  • 대부분의 의존 관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.

    • 대부분 의존 관계는 애플리케이션 종료 전까지 변하면 안된다.

  • 수정자 주입의 경우 setXxx 메서드를 public 으로 선언해야한다.

    • public으로 설정하게 되는 경우, 누군가가 실수로 변경할 수도 있을뿐더러, 변경이 되면 안되는 메서드를 public으로 설정하는 것은 좋은 설계가 아니다.

  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로, 불변하게 설계가 가능하다.

누락

프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에는

@Component
public class OrderServiceImpl implements OrderService{
    
    private  MemberRepository memberRepository;
    private  DiscountPolicy discountPolicy;

  	@Autowired(required = false)
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
		
  	@Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
class OrderServiceImplTest {
	@Test
  void createOrder() {
      OrderServiceImpl orderService = new OrderServiceImpl();
      orderService.createOrder(1L, "itemA", 10000);
  }  
}
java.lnag.NullPointerException
//  ...

memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문에 실행은 되나 NullPointException 이 발생한다.

@Component
public class OrderServiceImpl implements OrderService{

  	// final은 반드시 값이 있어야한다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
class OrderServiceImplTest {
	@Test
  void createOrder() {
      OrderServiceImpl orderService = new OrderServiceImpl();
      orderService.createOrder(1L, "itemA", 10000);
  }  
}

생성자 주입을 사용하면 주입 데이터를 누락 했을 때 컴파일 오류가 발생한다. 필요로 하는 타입을 바로 알 수 있기때문에 누락되는 경우가 없다.

java: constructor OrderServiceImpl in class dh0023.springcore.order.service.OrderServiceImpl cannot be applied to given types;
  required: dh0023.springcore.member.repository.MemberRepository,dh0023.springcore.discount.service.DiscountPolicy
  found: no arguments
  reason: actual and formal argument lists differ in length

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;
          // this.discountPolicy = discountPolicy;
        }
}

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.

java: variable discountPolicy might not have been initialized

컴파일 단계에서 오류를 발견하는 것은 가장 빠르고 좋은 오류이다.

수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없으며, 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.

  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.

옵션 처리

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

@Autowired(require = false)

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
        TestBean bean = ac.getBean(TestBean.class);
    }

    static class TestBean {

        @Autowired
        public void setNoBean(Member member) {
            System.out.println("member = " + member);
        }
    }
}

bean으로 등록되지 않은 Member 클래스를 @Autowired를 하면 다음과 같은 오류가 발생한다.

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: {}

	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:768)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:720)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1413)
	....

다음과 같이 @Autowired(required = false)로 자동주입 대상을 주입할 수 있다.

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
        TestBean bean = ac.getBean(TestBean.class);
    }

    static class TestBean {

        @Autowired(required = false)
        public void setNoBean(Member member) {
            System.out.println("member = " + member);
        }
    }
}

이 경우에는 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안된다. (로그가 출력이 안되는 것을 확인 가능)

23:25:53.088 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@223aa2f7
23:25:53.165 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
23:25:53.630 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
23:25:53.637 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
23:25:53.640 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
23:25:53.643 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
23:25:53.819 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autowiredTest.TestBean'


Process finished with exit code 0

org.springframework.lang.@Nullable

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
        TestBean bean = ac.getBean(TestBean.class);
    }

    static class TestBean {

        @Autowired
        public void setNoBean(@Nullable Member member) {
            System.out.println("member = " + member);
        }

    }
}

@Nullable 로 설정할 경우 자동 주입할 대상이 없으면 null이 입력된다.

23:27:46.560 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@223aa2f7
23:27:46.591 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
23:27:46.721 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
23:27:46.730 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
23:27:46.733 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
23:27:46.736 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
23:27:46.757 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autowiredTest.TestBean'
member = null

Process finished with exit code 0

Optional

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
        TestBean bean = ac.getBean(TestBean.class);
    }

    static class TestBean {

        @Autowired
        public void setNoBean(Optional<Member> member) {
            System.out.println("member = " + member);
        }

    }
}

Optional자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

23:27:46.560 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@223aa2f7
23:27:46.591 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
23:27:46.721 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
23:27:46.730 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
23:27:46.733 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
23:27:46.736 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
23:27:46.757 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autowiredTest.TestBean'
member = Optional.empty

Process finished with exit code 0

조회 빈이 2개 이상인 경우

기존에는 RateDiscountPolicy@Component로 등록을 했었는데, FixDiscountPolicy까지 @Component로 등록하게 되면 NoUniqueBeanDefinitionException 오류가 발생한다.

@Component
public class OrderServiceImpl implements OrderService{

    // final은 반드시 값이 있어야한다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자 의존관계 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
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

이때 하위 타입을 지정할 수 있지만, 이는 DIP를 위배하고 유연성이 떨어진다. 이름만 다르고 완전히 똑같은 타입의 스프링빈이 여러개(상속, 구현)인 경우 해결이 안된다.

이때, 자동 의존 주입으로 해결할 수 있는 방법이 약 3가지 정도 있다.

@Autowired 필드명

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

생성자 주입

  • AS-IS

@Component
public class OrderServiceImpl implements OrderService{

    // final은 반드시 값이 있어야한다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자 의존관계 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
  • TO-BE

@Component
public class OrderServiceImpl implements OrderService{

    // final은 반드시 값이 있어야한다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자 의존관계 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }
}

필드 주입

  • AS-IS

@Component
public class OrderServiceImpl implements OrderService{

    // 필드 주입
    @Autowired private MemberRepository memberRepository;
    @Autowired private DiscountPolicy rateDiscountPolicy;
}
  • TO-BE

@Component
public class OrderServiceImpl implements OrderService{

    // 필드 주입
    @Autowired private MemberRepository memberRepository;
    @Autowired private DiscountPolicy disRcountPolicy;
}

@Quilifier

@Quilifier 는 추가 구분자를 붙여주는 방법이다.

주입시 추가적인 방법을 제공하는 것이며, 빈 이름을 변경하는 것은 아니다.

/**
 * @Component 어노테이션 추가로 빈설정
 * 이때 빈이름을 설정하고 싶은 경우에는 @Component("빈이름")으로 설정할 수 있다.
 * @Qualifier : 추가 구분자 설정 
 */
@Component
@Qualifier("rateDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{

    private final static int DIS_PER = 10;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP){
            return price * DIS_PER / 100;
        } else {
            return 0;
        }
    }
}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{

    private static final int DISCOUNT_AMT = 1000;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP) {
            return DISCOUNT_AMT;
        } else {
            return 0;
        }
    }
}
@Component
public class OrderServiceImpl implements OrderService{

    // final은 반드시 값이 있어야한다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자 의존관계 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("rateDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

다음과 같이 추가 구분자로 설정을 할 수 있다. 같은 @Qulifier 를 찾아서 주입해주는 것을 알 수 있다.

만약에 해당 이름으로 구분자를 못찾는 경우에는, 해당명으로 생성된 스프링 빈을 추가로 찾는다! @Qulifier@Qulifier 를 찾는 용도로만 사용하는 것이 가장 명확하다.

추가적으로 @Qualifier("fixDiscountPolicy") 의 명칭은 문자열이므로, 컴파일시 타입 체크가 불가능하다.

@Primary

@Primary 는 우선순위를 지정하는 방법이다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{}
@Component
public class FixDiscountPolicy implements DiscountPolicy{}

다음과 같이 RateDiscountPolicy@Primary 를 설정하면, 의존성 주입시 우선권을 갖게된다.

@Component
public class OrderServiceImpl implements OrderService{

    // final은 반드시 값이 있어야한다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자 의존관계 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

예를들어, 코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있는 경우에 편리하게 사용할 수 있다.

@Primary , @Qulifier

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

  • 즉, @Qulifier가 더 높은 우선순위를 갖게된다.

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

package dh0023.springcore.autowired;

import dh0023.springcore.config.AutoAppConfig;
import dh0023.springcore.discount.service.DiscountPolicy;
import dh0023.springcore.member.domain.Grade;
import dh0023.springcore.member.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.*;

public class AllBeanTest {

    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

    }


    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policyList;

        @Autowired
        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
            this.policyList = policyList;
            this.policyMap = policyMap;

            System.out.println("policyMap = " + policyMap);
            System.out.println("policyList = " + policyList);
        }
    }
}
policyMap = {fixDiscountPolicy=dh0023.springcore.discount.service.FixDiscountPolicy@c055c54, rateDiscountPolicy=dh0023.springcore.discount.service.RateDiscountPolicy@25e2ab5a}
policyList = [dh0023.springcore.discount.service.FixDiscountPolicy@c055c54, dh0023.springcore.discount.service.RateDiscountPolicy@25e2ab5a]

다음과 같이 MapList로 모든 DiscountPolicy를 받아 올 수 있다. 각각 policyMap과 policyList에 FixDiscountPolicy와 RateDiscountPolicy가 들어가 잇는 것을 알 수 있다.

참고

Last updated