11 min read

<Spring> 스프링 핵심원리 이해 7 - 의존관계 자동 주입

다양한 의존관계 주입 방법

의존관계 주입에는 아래와 같이 크게 4가지 방법이 존재한다.아래 네가지 방법이 어떤식으로 의존관계를 주입하는지 알아보자.

  1. 생성자 주입
  2. 수정자 주입(setter 주입)
  3. 필드 주입
  4. 일반 메서드 주입

생성자 주입

  • 이름 그대로 생성자를 통해서 의존관계를 주입받는 방법이다.
  • 기존에 우리가 진행했던 DI가 생성자 주입 방식을 이용했다.
  • 이는 생성자 호출 시점에 단 한번만 호출이 되기 때문에, 불변하고 필수적인 의존관계에 사용한다.
  • 아래 코드처럼 생성자가 한개만 있는 경우는 @Autowired를 생략해도 자동으로 스프링 빈을 통해 DI를 진행한다.
@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;
      }
}

수정자 주입(setter 주입)

  • setter라는 불리는 필드의 값을 변경하는 수정자 메서드를 이용하여, 의존관계를 주입하는 방식이다.
  • 선택,변경의 가능성이 있는 의존관계에 사용한다.
  • 기존의 코드에서 주입할 인스턴스들을 선언하는 부분에서 final 키워드를 없애주고 setter 코드를 작성해야한다.(변경에 용이)
@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;
        }
}

생성자와 수정자가 모두 있는 경우의 의존관계 주입 순서는?

  • 생성자의 경우 스프링 빈이 생성되는 동시에 의존관계 주입을 진행해 주기 때문에 수정자를 통한 의존관계 주입보다 먼저 해당 동작을 수행한다.

자바빈 프로퍼티 규약이란?

  • 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXX 또는 getXX라는 메서드를 이용해서 값을 수정하거나 읽는 규칙을 만들었는데, 이가 바로 자바빈 프로퍼티 규약이다.

필드 주입

  • 말 그대로 필드에 그대로 주입해버리는 방식이다.
  • 주입할 인스턴스 앞에 @Autowired를 붙이면 코드가 끝나버리기에 굉장히 간결한 장점을 가지고 있다.
  • 하지만 외부에서 해당 인스턴스에 대한 변경이 불가하다는 큰 단점이 존재한다.
@Component
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private MemberRepository memberRepository;
        @Autowired
        private DiscountPolicy discountPolicy;
  }

일반 메서드 주입

  • 일반 메서드를 통해서 주입 받을 수 있다.
  • 한번에 여러개의 필드 값을 주입 받을 수 있다.
@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;
        }
}

옵션처리

  • 주입할 대상이 없을 경우에 대해서 @Autowiredrequired 옵션을 처리하는 방식을 알아보자.
  • 먼저 required 는 디폴트 값이 true로 설정되어 있기 때문에, 자동 주입 대상이 없을 경우 오류를 발생시킨다.
  • 테스트에 autowired 패키지를 생성하고 아래에 AutowiredTest를 생성한다
  • 이후 내부 클래스로 스프링빈 등록이 되지 않은 Member 객체를 연결하는 코드를 작성해준다.
  • setNoBean1()의 required 옵션을 디폴트 값으로 지정하면 아래와 같은 오류가 발생한다.
  • setNoBean1()의 옵션을 다시 false로 하고 테스트를 진행해보면 아래와 같은 출력 결과를 얻을 수 있다.
  • 주입할 스프링 빈이 없지만 동작시켜야 할때는 아래의 3가지 방식을 이용하면 된다.
public class AutowiredTest {

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

    }

    static class TestBean {
        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }
        @Autowired
        public void setNoBean2(@Nullable Member noBean2) {
            System.out.println("noBean2 = " + noBean2);
        }
        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }
}
AutowiredTest 결과
AutowiredTest 결과

생성자 주입

최근의 DI 프레임워크들은 생성자를 통한 의존관계 주입을 권장하고 있다.위에서 읽었듯이 필드 주입 또는 메서드 주입은 자명한 단점이 있는데, 수정자 주입은 어떠한 단점이 있어서 권장되지 않는지 그리고 왜 생성자 주입을 사용해야 하는지 그에 대한 이유는 다음과 같다.

불변

  • 대부분의 의존관계 주입은 한번 발생하면 애플리케이션 종료시점까지 의존관계가 변경되는 일이 없다. 오히려 이러한 의존관계들은 애플리케이션이 구동하는 동안 동일하게 유지되어야한다
  • 만약 수정자 주입을 사용하면 public으로 set객체() 등등의 메서드를 오픈해놓아야한다.
  • 불변한 상태를 유지해야하는 인스턴스를 구지 public으로 열어둘 필요도 없고, 누군가가 실수로 변경할 여지도 있기에 해당 방식은 좋은 설계가 아니라 생각된다.
  • 생성자 주입의 경우, 스프링 빈이 생성될때 딱 한번만 의존관계를 주입해주고 접근 할 수 있는 문 자체를 닫아버리기에 불변성 유지 측면에서 완벽하다고 볼 수 있다.

누락

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

  • 위와 같은 수정자 주입으로 의존관계를 설정하는 경우 아래의 테스트 코드를 실행하게 되는 어떤결과가 나오게 될까?
@Test
  void createOrder() {
      OrderServiceImpl orderService = new OrderServiceImpl();
      orderService.createOrder(1L, "itemA", 10000);
  }
  • 테스트는 이상없이 돌아간다. 하지만 보이는 바와 같이 주입할 인스턴스를 생략(누락)했기 때문에 NPE가 발생한다.
  • 하지만 위의 테스트를 생성자 주입을 통해 의존관계를 넣어주면, OrderService 인스턴스를 선언하는 라인에서 바로 빨간줄을 띄우며, 컴파일 에러를 발생시킨다.
  • 즉, IDE에서 어떤 값을 필수로 주입해야하는지 알려준다.

final 사용

  • 위와 같이 생성자 주입을 사용하는 경우 final은 사용하지 못한다.
  • 그러면 당연히 위와 같이 인스턴스 누락을 컴파일 타임에서 잡아주지 못하는 단점이 생긴다.
  • 그래서 유일하게 final 키워드를 사용할 수 있는 생성자 주입이 권장된다.
  • 생성자 주입을 제외한 나머지 경우는 생성자 이후에 호출되기에 final을 사용할수 없다.

생성자 주입에 롬복 적용하기

롬복 설치하기

  • build.gradle에 적용한다.
  • Prefrences -> plugin -> lombok 검색 후 설치
  • Prefrences -> Annotation Processors 검색 -> Enable annotation processing 활성화

@RequiredArgsConstructor 적용하기

<기존 코드>

@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이 붙은 필드들을 모아서 생성자를 자동으로 만들어준다.(적용코드에는 보이지 않지만 실제 호출 가능함)

조회되는 빈이 2개이상인 경우의 해결

  • 다음 상황을 OrderServiceImpl로 설명하자면, 주입할 인스턴스인 DiscountPolicy를 찾기위해 컨테이너에서 컴포넌트 스캔을 진행할 텐데, 이 때 DiscountPolicy을 가진 동일한 두개의 스프링 빈(RateDiscountPolicy, FixedDiscountPolicy)이 등록되어있으면 아래와 같이 중복된다는 내용의 오류가 발생한다.
빈 중복
빈 중복
  • 해당 문제를 해결할 수 있는 방법은 3가지가 존재한다.
  1. @Autowired 필드 명 매칭
  • 다음과 같이 정의된 인스턴스의 이름을 @Autowired private DiscountPolicy discountPolicy @Autowired private DiscountPolicy rateDiscountPolicy로 바꿔준다.
  • 즉, 중복되는 빈이 존재할 경우 필드 이름(변수명)으로 빈 이름을 추가해준다.
  • 그러면 변경된 변수명을 가진 스프링 빈을 의존 관계 주입의 대상으로 선정한다.
  1. @Qualifier
  • @Qualifier는 추가 구분자를 붙여주는 방식이다.
  • 아래 코드와 같이 빈 등록 시 @Qualifier를 붙여준다

<RateDiscountPolicy>

@Component
  @Qualifier("mainDiscountPolicy")
  public class RateDiscountPolicy implements DiscountPolicy {}

<FixedDiscountPolicy>

@Component
  @Qualifier("fixDiscountPolicy")
  public class FixDiscountPolicy implements DiscountPolicy {}
  • 이후 생성자 의존관계 주입 시 @Qualifier를 붙여주고 이름을 적어준다
  @Autowired
  public OrderServiceImpl(MemberRepository memberRepository,
                          @Qualifier("mainDiscountPolicy") DiscountPolicy
  discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
}
  1. @Primary
  • 어노테이션 이름과 같이 중복된 타입의 빈들 사이에서 우선순위를 정해주는 방식이다.
  • 간단하게 사용할 빈의 클래스에 @Primary만 붙여주면 해당 스프링 빈이 의존관계 주입 시 사용된다.

<rateDiscountPolicy가 우선순위를 가지게 하는 코드>

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

동일한 타입의 빈을 모두 조회

의도적으로 동일한 타입의 모든 스프링 빈이 필요한 경우도 존재한다. 예를 들어 현재 할인 정책은 ratePolicy와 fixedPolicy로 나뉘는데 클라이언트가 해당 정책을 선택 할 수 있는 상황이면 두개의 정책 모두 필요한 상황이다.아래의 테스트 코드를 통해 동일한 타입을 모두 조회하는 코드를 구현한다.

package hello.core.autowired;

import static org.assertj.core.api.Assertions.*;

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, "brido", Grade.VIP);
        int discountPrice = discountService.discount(member,10000,"fixDiscountPolicy");
        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);
    }

    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.policyMap = policyMap;
            this.policyList = policyList;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policyList = " + policyList);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member,price);
        }
    }
}
  • DiscountService는 @Autowired로 인해 Map으로 모든 DiscountPolicy를 주입받는다.이때 fixDiscountPolicy, rateDiscountPolicy가 주입된다.
  • discount()메서드는 discountCode의 인자로 ‘fixDiscountPolicy’가 넘어오면 map에서 fixDiscountPolicy를 스프링 빈을 찾아서 실행한다.
  • Map<String, DiscountPolicy> policyMap은 map의 키에 스프링 빈의 이름을 넣어주고, value값으로는 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • 아래와 같이 해당 타입의 모든 스프링 빈이 담긴 것을 알 수 있다.

소스코드 : https://github.com/brido4125/core-spring

해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.