Spring, Apache, Java

스프링에서 의존관계 자동 주입하는 방법들

cwchoiit 2024. 5. 25. 14:52
728x90
반응형
SMALL

우선 DI가 무엇인지 어떤 장점이 있는지 왜 스프링에서 핵심 중 하나인지는 이해했다.

그럼 스프링에서 이 의존관계를 주입하는 방법에 대해서 알아보려고 하는데 다음과 같은 방법들이 있다.

  • 생성자 주입
  • setter 주입
  • 필드 주입
  • 일반 메서드 주입

생성자 주입

생성자를 통해서 주입하는 방법이다. 가장 중요하고 가장 많이 사용되며 가장 안전한 주입 방식이다.

 

OrderServiceImpl

@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`를 생략할 수 있다.

 

OrderServiceImpl

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

 

미리 내는 결론이다. 의존관계 주입은 생성자 주입을 사용하자. 세터 주입은 결국 세터라는 굉장히 위험한 메서드를 `public`으로 열어야 하고 필드 주입은 순수 자바로 테스트 자체가 불가능하다. 그래서 그냥 생성자 주입을 사용하면 된다.

setter 주입

수정자를 통해 주입하는 방식이다.

OrderServiceImpl

@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;
    }

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

이 문제의 단점은 수정자를 만드는 것 그 자체에 있다. setter를 만든 순간부터 버그가 생길 수 있는 어마어마한 문을 활짝 열어놓는 것이다.

난 절대 사용하지 않는 방식이다. 정말 필요한 상황이 아니라면.

 

필드 주입

이 얘기하려고 이 게시글 만들었다. 필드 주입 절대 사용하지 않기로 한다.

OrderServiceImpl

@Component
public class OrderServiceImpl implements OrderService {

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

 

"근데 너무 간결해서 사용하고 싶은데요?"

 

그렇다. 진짜 깔끔해 보이고 왜 문제가 생기는 걸까? 하는 생각이 든다. 문제는 테스트할 때 발생한다.

스프링 컨테이너를 띄우는 테스트가 아니라 순수 자바로 테스트할 때 이 필드에 값을 넣어줄 방법이 없다.

 

다음 코드를 보자.

@Test
void fieldInjection() {
    OrderServiceImpl orderService = new OrderServiceImpl();

    // OrderServiceImpl이 사용하는 MemberRepository, DiscountPolicy를 어떻게 초기화 해주지?

    orderService.createOrder(1L, "itemA", 10000); // NullPointerException 발생
}

createOrder()는 내부적으로 MemberRepositoryDiscountPolicy를 사용한다. 그럼 그 둘은 초기화 된 상태여야한다. 

스프링이 띄워질 때 빈으로 등록될 것들을 전부 찾아 빈으로 등록하고 @Autowired를 찾아 자동 주입을 하는데 스프링이 없는 순수 자바 테스트는 어떻게 이 필드에 값을 채워넣겠는가? 못한다. 해결할 수 있는 방법은 두가지가 있다.

  • 생성자를 만든다: 생성자를 만들고 순수 자바 테스트할 때 저 두개의 필드를 초기화하면 된다.  => 그럼 처음부터 생성자 주입을 하면 된다.
  • 세터를 만든다: 수정자를 만들고 순수 자바 테스트할 때 세터로 값 넣어주면 된다 => 그럼 처음부터 setter 주입을 하면 된다.

필드 주입은 어떻게 생각을 해도 필요가 없다. 하지말자. 근데 해도 되는 경우가 있다.

  • 스프링을 사용해서 테스트 하는 경우
package org.example.springcore;

import org.example.springcore.order.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringCoreApplicationTests {
    
    @Autowired
    OrderService orderService;

    @Test
    void contextLoads() {
    }

}

이렇게 애시당초에 `@SpringBootTest` 애노테이션이 붙은 테스트는 스프링 컨테이너를 만들어서 테스트를 하는건데 이 경우에는 필드 주입을 하는게 오히려 더 간결하고 좋을 수 있다.

 

일반 메서드 주입

아무 메서드에 그냥 주입을 하는 방식인데, 이 또한 사용하지 말자.

OrderServiceImpl

@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;
    }
}

이렇게 일반 메서드로도 주입이 가능한데, 생성자로 주입하면 될 것을 굳이 이렇게 할 이유가 없다.

 

중간 결론

그래서 하고싶은 말은 거의 대부분의 경우 생성자 주입으로 의존관계를 주입하면 된다. 정말 가끔가다 위 설명대로 필드 주입을 사용해도 되는 경우가 있다. 그때는 사용하되 일반적으로는 사용하지말자. 이제 생성자 주입을 더 편리하게 사용하는 방법을 알아보자. Lombok의 도움을 받아 훨씬 더 편하게 사용할 수 있다.

 

참고로, 자동 주입이 될 수 있는 것은 OrderServiceImpl이 스프링이 관리하는 빈이기 때문이다! @Component 애노테이션이 붙어있다. 스프링이 컴포넌트 스캔으로 해당 클래스를 빈으로 등록했기 때문에 자동 주입도 가능한 것이다. 빈으로 관리하는 대상이 아니면 자동 주입을 위해 사용하는 @Autowired는 아무런 효력이 없다.

 

@Autowired 옵션 설정

스프링 빈을 자동 주입하려고 @Autowired를 사용했는데, 자동 주입 대상이 스프링 빈이 아닌 경우 기본값은 에러가 발생한다.

근데, 스프링 빈이면 자동 주입하고 빈이 아니면 그냥 주입이 안된 상태로 에러는 발생하지 않게 막는 방법이 크게 3가지가 있다.

 

  • `required = false` 옵션
  • @Nullable
  • Optional

세 가지가 있다. 솔직히 크게 중요한 내용은 아니라고 보는데 정말 가끔은 이런 경우가 있을 수도 있기 때문에 한번은 보고 넘어가는게 좋을듯하다.

package org.example.springcore;

import org.example.springcore.member.Member;
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 org.springframework.lang.Nullable;

import java.util.Optional;

public class AutowiredTest {

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

    static class TestBean {

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

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

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

위 코드를 보면 Member를 자동으로 주입하는데 Member는 스프링 빈 대상이 아니다. 그래서 자동 주입이 일어나지 않는다. 그럼 이 경우 required = false 옵션을 줘서 자동 주입을 아예 실행조차 하지 않게 하여 에러를 막는 방법이 있고 @Nullable을 사용해서 해당 값에 null을 넣는 방법이 있고, Optional을 사용해서 있으면 받고 없으면 Optional.empty로 반환하게 하는 방법이 있다.

 

@Test 실행결과

member = Optional.empty
member = null

 

Lombok을 사용해서 매우 깔끔하게 생성자 주입하기

나는 이 lombok 없이는 살 수 없다. 위 생성자 주입 예제 코드로 만든 코드를 한번 보자.

혹시 lombok에 대해 잘 알지 못한다면 Java lombok을 검색하면 문서를 통해 쉽게 이해할 수 있다.

 

OrderServiceImpl

@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`이 붙은 즉 초기화가 반드시 필요한 필드들을 가지고 생성자를 알아서 만들어준다. 그러니까 위에 코드와 이 코드가 백퍼센트 동일한 코드인거다. 필드 주입보다 훨씬 간단하다. 그래서 생성자 주입보단 필드 주입이 더 깔끔해 보이는 그런 유혹까지도 뿌리칠 수 있게 됐다. 

 

이제 생성자 주입을 잘 사용하면 된다.

728x90
반응형
LIST