티스토리 뷰
반응형
의존성 주입 (Dependency Injection) 의 종류
1. 생성자 주입 방식 (Constructor Injection)
- 생성자를 통해 의존성을 주입하는 방식이다.
- 총 3가지 주입 방식 중 가장 권장되는 주입 방식이다. (아래에서 자세히 설명)
public class ExampleClass {
// final을 사용할 수 있다.
private final Example example;
@Autowired
public ExampleClass(Example example) {
this.example = example;
}
}
2. 필드 주입 방식 (Field Injection)
- @Autowired 어노테이션을 통해 의존성을 주입하는 방식이다.
- 사용법이 매우 간단하다.
public class ExampleClass {
@Autowired
private Example example;
}
3. 수정자 주입 방식 (Setter Injection)
- 수정자 (setXX) 를 통해 의존성을 주입하는 방식이다.
public class ExampleClass {
private Example example;
// 함수 이름이 setXX일 필요는 없지만 일관성을 위해 setXX을 사용하는 것이 좋다.
@Autowired
public void setExample(Example example) {
this.example = example;
}
}
생성자 주입 방식이 권장되는 이유
의존성을 주입하는 방법은 3가지인데, 왜 생성자 주입 방식이 권장되는 것일까?
1. 순환 참조 방지
- 예를 들어, 아래와 같이 두 개의 클래스와 메인 클래스가 있다고 해보자.
@Component
public class Example1 {
@Autowired
private Example2 example2;
public void playMusic() {
example2.playGame();
}
}
@Component
public class Example2 {
@Autowired
private Example1 example1;
public void playGame() {
example1.playMusic();
}
}
@SpringBootApplication
public class TestApplication implements CommandLineRunner {
@Autowired
private Example1 example1;
@Autowired
private Example2 example2;
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
example1.playMusic();
example2.playGame();
}
}
- 위 코드를 실행하면 과연 어떤 결과가 나올까?
.....
.....
.....
.....
.....
.....
.....
.....
.....
- 위 이미지를 통해 런타임 에러가 발생한 것을 알 수 있으며, 이는 애플리케이션이 정상적으로 구동되었지만 프로그램이 수행할 수 없는 동작을 수행하려고 했다는 것이다.
- 그렇다면 왜 이러한 결과가 나온 것인지 필드 주입 방식을 생성자 주입 방식으로 수정하여 실행해보면 아래와 같은 결과를 얻을 수 있다.
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
testApplication (field private com.example.test.Example1 com.example.test.TestApplication.example1)
┌─────┐
| example1 defined in file [.../com/example/test/Example1.class]
↑ ↓
| example2 defined in file [.../com/example/test/Example2.class]
└─────┘
필드 주입 방식과 생성자 주입 방식의 결과가 왜 다른 것일까?
- 이는 의존성을 주입하는 시점이 (생성자) - (필드, 수정자) 방식이 다르기 때문이다.
- 필드, 수정자 주입 방식의 경우, 빈 객체를 먼저 생성한 뒤에 의존성을 주입 하기 때문에 빈 객체를 생성하는 시점에는 순환 참조가 발생하는지 알 수 있는 방법이 없다. (생성 -> 주입)
- 생성자 주입 방식의 경우, 빈 객체를 생성하는 시점에 생성자의 파라미터 빈 객체를 찾아 먼저 주입한 뒤에 주입받은 빈 객체를 이용하여 생성한다. (주입 -> 생성)
- 이 때문에 런타임 시점이 아닌 애플리케이션 구동 시점에 순환 참조를 발견할 수 있다.
2. final 키워드 사용으로 불변성 (Immutable) 을 통한 오류 방지, Lombok 활용
- 필드, 수정자 주입 방식은 예기치 못한 상황으로 빈 객체가 변경될 수 있어 이로 인한 오류 (ex. NPE) 가 발생할 수 있다.
- 생성자 주입 방식의 경우, final을 사용할 수 있고 이를 통해 객체 생성 시점에 필수적으로 빈 객체 초기화를 수행해야 하기 때문에 null이 아님을 보장할 수 있고, 초기화된 객체는 변경될 수 없다.
- 또한, 생성자 주입 방식은 Lombok의 어노테이션 (@RequiredArgsConstructor) 을 활용하여 간단히 작성할 수 있다.
@Component
public class Example1 {
private final Example2 example2;
public Example1(Example2 example2) {
this.example2 = example2;
}
public void playMusic() {
example2.playGame();
}
}
// Lombok 어노테이션을 통한 의존성 주입
@Component
@RequiredArgsConstructor
public class Example2 {
private final Example1 example1;
public void playGame() {
example1.playMusic();
}
}
3. 테스트 코드를 작성하기 쉽다.
- 필드 주입 방식으로 코드를 작성한 경우, 순수한 자바 코드로 단위 테스트를 작성하는 것은 불가능하다.
- 예를 들어, 아래와 같은 코드가 있다고 해보자.
@Service
public class ExampleService {
@Autowired
private ExampleRepository exampleRepository;
public void save(int seq) {
exampleRepository.save(seq);
}
}
public class ExampleTest {
@Test
public void test() {
ExampleService exampleService = new ExampleService();
exampleService.save(1);
}
}
ExampleTest 코드를 실행하면 어떻게 될까?
- Spring과 같은 DI (Dependency Injection) 프레임워크 위에서 동작하지 않기 때문에 의존성 주입이 되지 않을 것이다.
- 그렇다면 exampleService = null일 것이고, exampleService.add(1)은 NPE를 발생시킬 것이다.
- 수정자 주입 방식을 사용하는 경우, SOLID (객체 지향 설계 원칙) 의 OCP (개방-폐쇄 원칙, Open-Closed Principle) 을 위반하게 된다.
- 생성자 주입 방식을 이용하면 아래와 같이 테스트 코드를 작성할 수 있다.
- 이는 컴파일 시점에 빈 객체를 주입받아 테스트 코드를 작성할 수 있고, 빈 객체를 주입하지 못한 경우 컴파일 시점에서 오류를 발견할 수 있다는 것이다.
@Service
public class ExampleService {
private final ExampleRepository exampleRepository;
public ExampleService (ExampleRepository exampleRepository) {
this.exampleRepository = exampleRepository;
}
public void save(int seq) {
exampleRepository.save(seq);
}
}
public class ExampleTest {
@Test
public void test() {
ExampleRepository exampleRepository = new ExampleRepository();
ExampleService exampleService = new ExampleService(exampleRepository);
exampleService.save(1);
}
}
반응형
'Spring' 카테고리의 다른 글
Spring MVC (Model, View, Controller) (0) | 2022.04.15 |
---|---|
스프링의 읽기 전용 조회시 성능 향상 (스칼라 타입, org.hibernate.readOnly, @Transactional(readOnly = true)) (0) | 2021.08.13 |
Spring Boot + ELK 를 이용한 로그 수집 (0) | 2021.02.07 |
댓글