Spring의 핵심 기능 DI(Dependency Injection), IoC(Inversion of Control).
오늘은 그 중 Spring에서 DI를 하는 3가지 방법에 대해 알아보고자 합니다.
그리고 마지막으로 생성자 주입 방식이 왜 가장 추천되는지 코드를 통해 살펴 보며 마무리 하겠습니다.
목차
- DI 란?
- DI의 3가지 방법
- 필드 주입
- Setter 주입
- 생성자 주입
- BEST 방법 -> 생성자 주입 방식
생성자 주입을 사용해야 하는 이유로 넘어 가고 싶다면 바로 밑으로 쭉 내리시면 됩니다.
DI 란?
구체적인 DI방식에 대해 알아보기 전에, 간단히 DI의 정의에 대해 알아 봅시다.
DI(dependency injection), 의존성 주입은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉을 의미합니다.
어렵게 생각하지 말고, 바로 코드를 봅시다.
아래 코드를 보면,
1. TestService라는 객체는 testRepository 객체와 의존성을 가지고 있고,
2. 그 의존성을 생성자를 통해 주입 하는 것을 볼 수 있습니다.
이렇게 하나의 객체(TestService)가 의존관계를 가지고 있는 다른 객체(TestRepository)를 주입해주는 것이 바로 DI입니다!
public class TestService{
//1. TestService, TestRepository 의존성 있음
private final TestRepository testRepository;
//2. 의존성을 생성자를 통해 주입
public TestService(TestRepository testRepository){
this.testRepository = testRepository;
}
}
의존성 주입의 의도는 객체의 생성과 사용의 관심을 분리하는 것이며, 이는 가독성과 코드 재사용을 높혀줍니다.
DI의 3가지 방법
의존성 주입에는 아래 3가지 방법이 있으며 이에 대해 코드를 통해 알아 봅시다.
- 필드 주입
- setter 주입
- 생성자 주입
1. 필드 주입
가장 간결한 방법 입니다.
public class FieldDI{
//1. 이렇게 Autowired 어노테이션을 달아줌으로써 자동으로 스프링부트가 DI를 해줍니다.
@Autowired
private TestDependency testdependency;
}
2. setter 주입
수정자를 이용한 방법입니다.
public class FieldDI{
private TestDependency testDependency;
//1. 이렇게 수정자에 Autowired 어노테이션을 달아줌으로써 자동으로 스프링부트가 DI를 해줍니다.
@Autowired
public void setTestDependency(TestDependency testDependency){
this.testDependency = testDependency;
}
}
3. 생성자(constructor) 주입
생성자를 이용한 방법입니다.
public class FieldDI{
private TestDependency testDependency;
//1. 이렇게 생성자에 Autowired 어노테이션을 달아줌으로써 자동으로 스프링부트가 DI를 해줍니다.
@Autowired
public FieldDI(TestDependency testDependency){
this.testDependency = testDependency;
}
}
단, 해당 클래스에 생성자가 하나 밖에 없다면 @Autowired를 생략 할 수 있습니다. (공식 문서)
Best Way ? -> 생성자 주입 방식
생성자 주입 방식이 가장 좋은 방식이라는 것은 많은 개발자들 사이에서 기정 사실화 되어 있습니다.
예를 들어, 스프링 contributor중 한명인 Oliver가 쓴 블로그 에서도, 스택오버플로우에서도 생성자 주입이 좋다고 합니다.
또한, 스프링팀 내에서도 생성자 주입방식을 가장 선호하는 것을 알 수 있습니다. (공식 문서2 - setter vs constructor)
인텔리제이에서도 필드 인젝션을 하면 아래와 같은 경고 문구를 출력하며 생성자 주입방식을 권고합니다.
Field injection is not recommended … Always use constructor based dependency injection in your beans
코드만 봐서는 너무 간단하고 편해보이는 필드 인젝션인데, 왜 생성자 인젝션 방식을 사용하라고 하는 것 일까요?
가장 큰 이유는 아래 2가지로 정리할 수 있습니다.
- Null Exception이 발생하기 너무 쉽다.
- Test 코드를 작성하기 힘들다.
NPE(Null Point Exception) 발생 가능성
코드 예시를 통해 설명해보도록 하겠습니다.
public class MyComponent{
@Autowired
MyDependency myDependency;
public void doSth(){
myDependency.doSth();
}
}
public class TestFieldNPE{
MyComponent myComponent = new MyComponent();
myComponent.doSth(); // -> Null Point Exception 발생
}
위 코드를 보면 NPE가 발생하기 매우 쉬운 구조라는 것을 알 수 있습니다. 심지어 해당 에러는 컴파일시에 알 수 없고, 런타임에러로만 잡을 수 있기에 더 귀찮아진다.
테스트 코드 작성의 어려움
이 역시 바로 코드 예시를 보면서 알아보도록 하겠습니다.
@Service
public class MyService {
@Autowired
private MyDependency myDependency;
public String doSomething() {
return myDependency.doSomethingElse();
}
}
public class MyServiceTest {
private MyService myService;
@Test
public void testDoSomething() {
String expected = "Hello, world!";
// myDependency를 주입해줄 수 있는 방법이 없다.
// 방법 아래처럼 myDependency를 mock해서,
// myService 클래스에 직접 set해주면 되긴 하지만. 매우 귀찮다.
// mock 방법
// myService = new MyService();
// MyDependency mockDependency = mock(MyDependency.class);
// Field field = MyService.class.getDeclaredField("myDependency");
// field.setAccessible(true);
// field.set(myService, mockDependency);
String actual = myService.doSomething(); // -> Error 발생
assertEquals(expected, actual);
}
}
이에 반해, 생성자 방식을 이용하면 아래처럼 매우 간단하게 테스트 코드에서도 DI를 해줄 수 있습니다.
@Service
public class MyService {
private MyDependency myDependency;
@Autowired
public MyService(MyDependency myDependency){
this.myDependency = myDependency;
}
public String doSomething() {
return myDependency.doSomethingElse();
}
}
public class MyServiceTest {
// 생성자를 이용해서 바로 DI가 가능하다.
private MyService myService = new MyService(new MyDependency());
@Test
public void testDoSomething() {
String expected = "Hello, world!";
String actual = myService.doSomething();
assertEquals(expected, actual);
}
}
얘기는 길었지만, 결론은 "생성자 DI을 사용하자" 입니다.
'java,springboot' 카테고리의 다른 글
[Springboot] Error:IllegalStateException: Required property b not found for class (0) | 2023.04.21 |
---|---|
mariaDB(mysql) Java(R2dbc) 타입 매핑 (TinyInt(1) -> Boolean) (1) | 2023.04.20 |
[springboot] 부모 클래스 생성자 에러 해결 - 생성자 주입 방식 (0) | 2023.04.18 |
Spring - Bean으로 등록하는 2가지 방법(컴포넌트 스캔, 자바 코드) (0) | 2023.04.07 |
vscode에서 gradle로 springboot 개발환경 설정 (0) | 2023.03.01 |