스프링, 다양한 DI 방법

    스프링의 다양한 의존관계 주입


    스프링의 의존관계 주입은 아래 네 가지 방법이 있다. 그 중에서 주로 사용되는 것은 생성자 주입, 수정자 주입이다.

    • 생성자 주입
    • 수정자 주입(Setter 주입)
    • 필드 주입
    • 일반 메서드 주입 

     

    스프링 DI : 생성자 주입


    생성자 주입은 생성자를 호출할 때 의존관계 주입까지 된다. 다른 의존관계 주입은 빈 라이프 사이클에 따라 의존관계 주입 단계에서 일괄적으로 이루어진다. 

    1. @ComponentScan을 하면 @Component가 붙은 OrderserviceImpl.class가 스프링 빈에 등록된다. 
    2. 스프링 빈 등록되면 생성자 호출이 된다.
    3. 생성자 호출 시, @Autowired가 있는 것을 스프링이 확인한다. 이 때 스프링 컨테이너의 빈 저장소에서 MemberRepository, DiscountPolicy 빈 객체를 꺼내온 후 의존관계 주입(DI)를 한다.

    생성자 주입은 클래스 내에 생성자가 딱 1개만 있으면, @Autowired가 없어도 스프링 빈에 자동으로 등록된다. 

     

    언제 생성자 주입?

    • 불변 의존관계 : 처음에 셋팅한 후 더 이상 값 변경이 없는 경우에 사용 
    • 필수 의존관계 : 생성자 주입은 private final로 필드변수 선언이 가능하다. final의 특성 상, 값이 없으면 에러가 발생한다.

     

    생성자 주입 테스트

    좌 : Before / 우 : 변경 후

    생성자 주입 테스트를 하게 되면서 필드 변수를 private final로 변경할 수 있게 되었다. @Component 클래스 내에 생성자가 하나만 있으며, @Autowired를 생략할 수 있다. 

    public class OrderApp {
        public static void main(String[] args) {
     		//ComponentScan 설정으로 AutoAppConfig.Class 정보 읽어오기 (스프링 빈으로 등록함) 
    		ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
            // OrderserviceImpl에 대해 @Autowired 없이도 등록되는지 확인. 
            OrderServiceImpl orderService = ac.getBean("orderServiceImpl", OrderServiceImpl.class);
            System.out.println("memberRepository = " + orderService.getMemberRepository());
            System.out.println("discountPolicy = " + orderService.getDiscountPolicy());

    위의 코드는 @ComponentScan으로 생성자 주입을 했을 때, 생성자가 1개인 경우 @Autowired가 없어도 자동적으로 의존관계가 주입되어서 스프링 빈에 등록되어있는 것을 검증하기 위한 메인 테스트 코드이다. 실행 결과는 아래와 같이 나온다. 

    위 처럼 OrderServiceImpl에서 여러 생성자가 있다면, @Autowired를 붙이지 않으면 "BeanCreationException"이 발생한다. 

    생성자 주입에 대해서 정리하면 다음과 같다.

    1. 생성자 주입 시, 필드 변수는 private final로 지정 가능하며, 컴파일 단계에서 에러를 미리 방지할 수 있다.
    2. 생성자가 1개면,  @Autowired 없이도 의존관계 주입이 된다. 생성자가 2개 이상일 때, @Autowired를 붙이지 않으면 자동으로 빈을 등록해주지 않기 때문에 "BeanCreationException"이 발생한다.

     

    스프링 DI : 수정자 주입


    수정자 주입은 생성자가 아닌 Setter를 활용해 의존관계를 주입한다. Setter는 필드의 값을 변경한다. 단, 선택 가능성이 있는 의존관계에 주로 사용되기 때문에 private final은 사용하지 못한다. 따라서 컴파일 단계의 누락 방지는 할 수 없다. 

    1. 선택 가능성이 있는 의존관계 주입에 사용한다. 이 말은 의존관계 주입에 필요한 빈이 항상 등록되어있다는 것이 보장되지 않을 수 있다는 것을 의미한다. (MemberRepository가 스프링 빈에 등록이 안 되어 있을 수도 있다) 선택적인 수정자 주입하기 위해서는 @Autowired(required = false) 어노테이션을 달아주면 필수가 아님을 알 수 있다.

    2. 변경 가능성이 있는 의존관계 주입에 사용된다. 예를 들어, 중간에 배역을 바꾸고 싶은 경우가 생길 것으로 생각한다면 수정자 주입을 통해 인스턴스를 바꿀 수 있다.

     

    수정자 주입 테스트

    1. 생성자를 삭제 혹은, Default 생성자를 OrderServiceImpl에 생성해둔다.
    2. 필드 변수를 private로 바꾼 후, Setter를 등록한다.
    3. 각 Setter에 @Autowired를 하고, souvm을 통해 호출되었는지 확인을 해둔다.
     // OrderServiceImpl 코드
     private DiscountPolicy discountPolicy;
        private MemberRepository memberRepository;
    
        @Autowired(required = false)
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            System.out.println("OrderServiceImpl.setDiscountPolicy");
            this.discountPolicy = discountPolicy;
        }
    
        @Autowired(required = false)
        public void setMemberRepository(MemberRepository memberRepository) {
            System.out.println("OrderServiceImpl.setMemberRepository");
            this.memberRepository = memberRepository;
        }

    아래 메인 코드를 테스트로 실행해본다.

    코드 실행 결과는 아래에서 확인이 가능하다. 생성자에 @Autowired 어노테이션을 붙이면, 아래와 같은 코드 결과가 나온다. 

    위의 결과를 미루어보건데, 내 뇌피셜로는 OrderServiceImpl은 다음 방식으로 스프링 빈 생성 + 의존관계가 생성되었을 것으로 예측된다. 오른쪽 그림에서 볼 수 있듯이, 스프링 컨테이너에 있는 모든 빈을 확인했을 때, 생성자 주입과 관련된 객체는 없고 orderServiceImpl 빈 객체만 있을 뿐이다. 

    1. OrderServiceImpl에 @Component가 붙은 것을 보고 @ComponentScan이 클래스 전체를 스캔한다.
    2. OrderServiceImpl에서 Default 생성자에 의해 OrderServiceImpl의 빈 객체가 생성되어, 스프링 빈 저장소에 저장된다.
    3. 의존관계 주입 단계에서 @Autowired 명령어를 읽어 생성자 주입을 통해 의존관계 주입이 완료된다.

     

    생성자 주입, 수정자 주입의 동작 방식의 차이


    기본적으로 스프링 컨테이너의 빈 라이프 사이클은 2단계로 구분된다.

    1. 스프링 빈 객체를 모두 생성해서 스프링 빈 저장소에서 저장한다.
    2. 스프링 빈 객체에 대한 의존관계를 일괄적으로 설정한다.
    // 생성자 주입
    new public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy);

    빈 라이프 사이클에서 생성자 주입과 수정자 주입의 의존관계 주입은 각기 다른 단계에서 일어난다. 생성자 주입은 빈이 생성될 때 의존관계까지 주입이 완료된다. 스프링이 OrderServiceImpl 빈 객체를 생성하기 위해서는 위 코드처럼 생성자를 호출해야한다. 생성자 주입은 @Autowired가 붙어있기 때문에, @Autowired 생성자가 빈 객체를 생성하면서 바로 의존관계 주입까지 완료한다. 

    // 수정자 주입
    	@Autowired(required = false)
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            System.out.println("OrderServiceImpl.setDiscountPolicy");
            this.discountPolicy = discountPolicy;
        }
    
        @Autowired(required = false)
        public void setMemberRepository(MemberRepository memberRepository) {
            System.out.println("OrderServiceImpl.setMemberRepository");
            this.memberRepository = memberRepository;
        }

    이와는 다르게 수정자 주입은 @Autowired가 나중에 동작하는 방식이기 때문에 2단계인 빈 객체에 대한 의존관계를 일괄적으로 설정할 때 동작하게 된다. 또한, 이 경우에는 생성자를 통해 이미 빈 객체가 다 생성이 되어 있기 떄문에 생성자가 필요 없어진다. 

     

    필드 주입


    필드 주입은 이름 그대로 필드 변수에 바로 주입을 하는 방법이다. 

    • 장점 : 코드가 간결해진다(생성자 주입, 수정자 주입 생각해보면..)
    • 단점 (안티 패턴임)
         1. DI 프레임워크가 없으면 아무것도 할 수 없다. (Autowired 상태기 때문)
         2. 외부에서 변경이 불가능해 테스트가 어렵다.
        @Autowired
        private DiscountPolicy discountPolicy;
        @Autowired
        private MemberRepository memberRepository;

    위에서 볼 수 있듯이, 필드 주입은 생성자와 수정자에 @Autowired를 넣는 것이 아니다. 필드 변수에 바로 @Autowired를 넣은 후, 추후에 의존관계를 바로 지정해주는 방식으로 동작한다. 따라서 코드가 매우 간결해진다. 그렇지만 독버섯이다.

        @Test
        void test2(){
            OrderServiceImpl orderService = new OrderServiceImpl();
            orderService.createOrder(1L, "itemA", 10000);
        }

    프레임워크 없이 Java만 사용하는 테스트를 할 수 없다. 위의 테스트 코드를 작성해서 돌릴 경우, DiscountPolicy, MemberRepository 멤버 변수는 'null'이기 때문에 createOrder 메서드를 실행하면 에러가 발생한다. 또한, private로 되어있기 때문에 외부에서 값을 수정할 수도 없다.

    수정 코드

    외부에서 값을 수정하기 위해서는 결국 Setter를 만들어야 하는데, 그럴 바에야 그냥 수정자 주입을 하는 것이 낫다는 것이다. 어차피 Setter를 만들어야 하기 때문이다.


    메서드 주입


    일반 메서드에 @Autowired 어노테이션을 달고, 그것을 통해서 의존관계를 주입하는 방법이다. 한번에 여러 멤버 변수(필드 변수)를 주입 받을 수 있지만, 사실살 수정자 주입과 다를 것이 없다. 아래코드를 보면 알 수 있다.

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

     

     

    어떤 의존관계 주입을 선택해야할까?


    결론부터 말하면, "항상 생성자 주입을 선택한다. 가끔 옵션이 필요한 경우(필수적이지 않은 경우) 수정자 주입을 선택하자. 필드 주입은 사용하지 않는 것이 좋다."이다. 

     

    수정자 주입을 하게 되면 Setter 메서드를 Public으로 열어두게 된다. Public으로 열려있을 경우 다른 사람들이 사용해도 되는 것으로 이해를 하고, Setter를 통해 수정이 이루어질 수 있다. 즉, 의도한 것과 다른 방식으로 동작할 가능성이 있다. 또한 생성자 주입으로 의존관계를 주입할 경우, 순수한 자바 코드 만으로 테스트할 때 NullPointerException이 발생할 수 있다. 

        @Test
        void test1(){
            OrderServiceImpl orderService = new OrderServiceImpl();
            orderService.createOrder(1L, "itemA",10000);
        }
        
        
    @Component
    public class OrderServiceImpl implements OrderService{
    
        private DiscountPolicy discountPolicy;
        private MemberRepository memberRepository;
    
        @Autowired
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }
    
        @Autowired
        public void setMemberRepository(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    
        @Override
        public Order createOrder(Long memberId, String itemName, int itemPrice) {
            Member member = memberRepository.findById(memberId);
            int discountPriceAmount = discountPolicy.discount(member, itemName, itemPrice);
            return new Order(memberId, itemName, itemPrice, discountPriceAmount);
        }
    
    }

    위 코드를 살펴보면 테스트 코드가 순수 자바코드다. OrderService 객체를 하나 생성해서, createOrder()가 정상 동작하는지 확인한다. 당연한 이야기지만, @Autowired는 스프링 컨테이너를 이용할 때만 의존관계가 주입되기 때문에 DiscountPolicy, MemberRepository 필드 변수에는 어떠한 의존관계도 없다. 따라서 Null Pointer Exception이 발생한다. 

     

    의존관계 주입이라는 것은 "공연이 시작할 때, 배역을 완전히 정해지는 것"과 동일하다고 볼 수 있다. 따라서, 스프링이 시작될 때 의존관계 주입을 마치고, 끝날 때까지 그 의존관계를 사용하는 것이 좋다. 이런 관점에서는 생성자 주입을 사용하는 것이 좋다. 

        void test1(){
            MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository();
            Member member = new Member(1L, "nameA", Grade.VIP);
            memoryMemberRepository.join(1L, member);
    
            OrderServiceImpl orderService = new OrderServiceImpl(new RateDiscountPolicy(), memoryMemberRepository);
            Order order = orderService.createOrder(1L, "itemA", 10000);
            Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
            
            
    @Component
    public class OrderServiceImpl implements OrderService{
    
    
        private final DiscountPolicy discountPolicy;
        private final MemberRepository memberRepository;
    
    
        public OrderServiceImpl(DiscountPolicy discountPolicy, MemberRepository memberRepository) {
            this.discountPolicy = discountPolicy;
            this.memberRepository = memberRepository;
        }
    
    }

    위의 코드를 살펴보면 순수 자바 코드만 사용해도 테스트가 가능한 것을 확인할 수 있다. 또한, private final로 선언되기 때문에 생성자 단계에서 강제적으로 누락이 방지가 되며, 이 때문에 자바 코드에서만으로도 훌륭하게 사용할 수 있는 것이다.

    생성자 주입을 선택할 때의 장점은 세 가지가 있다.

    1. 불변한다는 점 (final 키워드, 배역은 연극이 시작될 때 정해지고, 끝날 때까지 유지되어야 함)
    2. 누락을 막을 수 있다는 점(final 키워드)
    3. 필드 변수에 final 키워드를 사용할 수 있다는 점.

    즉, 이 세 가지 장점 때문에 생성자 주입으로 의존관계를 설정하는 것이 권유된다고 한다. 위의 내용들을 정리하면 다음과 같다.

    1. 생성자 주입 방입을 선택하면 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살린다.
    2. 기본으로 생성자 주입 선택하고, 필수 값이 안니 경우에는 수정자 주입 방식으로 옵션으로 부여하면 된다. (동시에 사용 가능함)
    3. 항상 생성자 주입을 선택하고, 가끔 옵션이 필요하면 수정자 주입을 선택하자. 필드 주입은 사용하지 않는 것이 좋다.

     

     

    @Autowired


    1. Autowire의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)를 입력해준다. 

    댓글

    Designed by JB FACTORY