생성자 주입
수정자 주입(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로 바꾸면 생성자 외에서 다른 곳에서 바꿀 수 없기 때문에 불변도 유지할 수 있습니다.)
@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
을 예방할 수 있습니다.