Server/Spring

[Spring] 스프링에서 의존성 주입을 하는 3가지 방법

백엔드 규니 2021. 11. 29. 01:32
728x90
반응형

다양한 의존관계 주입 방법

  • 생성자 주입
  • 수정자 주입(setter 주입)
  • 필드 주입
  • 일반 메소드 주입

 

주입 방법은 크게 4가지가 있는데 하나씩 알아보겠습니다.



생성자 주입

  • 이름 그대로 생성자를 통해 의존 관계를 주입하는 것입니다.
  • 생성자 1개이면 @Autowired를 생략할 수 있습니다.

 

@Component
public class TestComponent {

    private TestRepository testRepository;

    // 생성자 주입
    public TestComponent(TestRepository testRepository) {
        this.testRepository = testRepository;
    }

    @PostConstruct
    public void test() {
        System.out.println(testRepository.getClass());
    }
}

위와 같이 TestComponent와 TestRepository가 Bean으로 등록이 되었을 때 스프링 IoC 컨테이너가 의존 관계 주입을 해줍니다. 생성자 주입은 setter, 필드 주입과는 다르게 TestComponent 객체가 생길 때 TestRepository와의 의존 관계 주입이 동시에 발생합니다.

 

TestComponent가 Bean 으로 등록되면 싱글톤으로 관리되기 때문에 생성자는 단 한번만 호출된다는 것을 알 수 있습니다. (불변을 보장할 수 있습니다.) 그리고 필수 의존 관계일 때 사용할 수도 있습니다. 예를들어 필드에 final 키워드를 사용하면 생성자에서 반드시 초기화를 시켜줘야 합니다. (final로 바꾸면 생성자 외에서 다른 곳에서 바꿀 수 없기 때문에 불변도 유지할 수 있습니다.)



setter 주입

@Component
public class TestComponent {

    private TestRepository testRepository;

    @Autowired
    public void setTestRepository(TestRepository testRepository) {
        this.testRepository = testRepository;
    }
}
  • setter라 불리는 필드의 값을 변경하는 수정자 메소드를 통해서 의존관계를 주입하는 방법입니다.
  • 선택, 변경 가능성이 있는 의존관계에 사용합니다.(클라이언트에게 setter를 노출하기 때문에 불변을 유지하지 못해 약간의 불안함이 존재합니다.)



필드 주입

  • 이름 그대로 필드에 바로 주입하는 방법입니다.
  • 코드가 간결해서 많은 개발자들이 편하게 사용할 수는 있지만 변경이 불가능해서 테스트 하기 힘들다면 치명적인 단점이 있습니다.
  • DI 프레임워크가 없다면 아무 것도 할 수 없습니다.
  • 애플리케이션과 관련 없는 테스트 코드, 스프링 설정을 목적으로 하는 @Configuration 같은 곳이 아니라면 사용하지 말자.

 

스크린샷 2021-04-24 오후 5 47 17

그리고 필드 주입은 스프링에서 권장하고 있지 않는 방법입니다.



필드 주입을 지양해야 하는 이유는 무엇일까?

너무나 대충 만든 예제 코드이지만 지양해야 하는 이유를 코드를 보면서 알아보겠습니다.

@Component
public class TestComponent {

    @Autowired
    private TestRepository testRepository;

    public void createTest() {
        Test test = testRepository.findByTest(1);

    }
}
public class Test {
    private String name;
    private String address;

    public Test(String name, String address) {
        this.name = name;
        this.address = address;
    }
}
@Repository
public class TestRepository {

    public Test findByTest(int testId) {
        return new Test("Gyunny", "Korea");
    }
}

위와 같이 아주 간단한 코드가 있습니다. 이 코드에 대해서 테스트 코드를 작성해보면 아래와 같습니다.

 

import org.junit.jupiter.api.Test;

class TestComponentTest {

    @Test
    public void fieldInjectionTest() {
        TestComponent testComponent = new TestComponent();
        testComponent.createTest();
    }
}

여기서는 TestComponent을 직접 new를 사용해서 객체를 만들었습니다. 이렇게 직접 객체를 만들면 @Autowired가 적용이 되지 않기 때문에 TestComponent의 TestRepository는 주입을 받지 못하게 됩니다.

 

그리고 testRepository.createTest()를 하면 어떻게 될까요? TestRepository가 주입을 받지 못했기 때문에 당연히 testRepository.findByTest(1); 이 부분에서 NullPointerException이 발생하게 됩니다. 그래서 NullPointerException의 발생을 막고자 setter를 만들 수 밖에 없습니다. 그러면 그냥 setter 인젝션을 쓰는게 낫습니다.



생성자 주입을 사용하는 것이 좋다.

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

위와 같이 setter 인젝션을 사용한 후에 테스트 코드를 작성해보겠습니다.

 

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

위의 테스트를 실행하면 어떻게 될까요? 위에서 계속 말했던 것처럼 NullPointerException이 발생합니다. 위의 코드로만 봐서는 해당 테스트가 제대로 작동하는지 안하는지 파악하는데 한계가 있습니다. OrderServiceImpl 내부 코드를 봐야 알 수 있습니다. 즉, setter 주입은 이러한 단점을 가지고 있기에, 생성자 주입을 사용해야 합니다.

 

생성자 주입을 사용하면 어떤 이점이 있을까요?

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

생성자를 이용하면 OrderServiceImpl 객체를 만들 때 어떤 객체가 반드시 필요한지 알 수 있기 때문에 NullPointerException을 예방할 수 있습니다.

반응형