[Spring] 스프링에서 의존성 주입을 하는 3가지 방법
다양한 의존관계 주입 방법
생성자 주입
수정자 주입(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 같은 곳이 아니라면
사용하지 말자.
그리고 필드 주입은 스프링에서 권장하고 있지 않는 방법
입니다.
필드 주입을 지양해야 하는 이유는 무엇일까?
너무나 대충 만든 예제 코드이지만 지양해야 하는 이유를 코드를 보면서 알아보겠습니다.
@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
을 예방할 수 있습니다.