스프링의 의존성 주입 방법은 세 가지로 나뉜다.
수정자 주입 (Setter Injection)
수정자 주입은 말 그대로 setter 메서드를 통해 의존성을 주입받는 방식이다. 선택적인 의존성을 주입할 때 주로 사용되는데, 이는 마치 자동차에 엔진을 나중에 교체할 수 있게 만드는 것과 비슷하다. ( 하지만 그런일은 드물 것이다.)
public class Car {
private Engine engine;
@Autowired
public void setEngine(Engine engine) {
this.engine = engine; // 엔진을 나중에 교체할 수 있음
}
}
런타임 의존성 변경
애플리케이션 실행 중에도 의존성을 동적으로 변경할 수 있다는 장점이 있다. 하지만 이는 양날의 검이 될 수 있는데, 객체의 불변성을 보장할 수 없어 예기치 않은 동작이 발생할 수 있는 위험이 있다. 자동차를 운행 중에도 엔진을 교체하는 것 처럼 말이다.
순환 참조 허용
아래의 코드 처럼 두 객체가 서로를 필요로 하는 상황을 만들 수 있다. 프로그램은 실행되지만, 이는 마치 "닭이 먼저냐 달걀이 먼저냐"와 같은 순환 논리를 만들어 유지보수를 어렵게 만들 수 있다.
// 닭이 먼저인가, 달걀이 먼저인가?
public class Chicken {
private Egg egg;
@Autowired
public void setEgg(Egg egg) {
this.egg = egg;
}
}
public class Egg {
private Chicken chicken;
@Autowired
public void setChicken(Chicken chicken) {
this.chicken = chicken;
}
}
불변성 보장 불가
불변성 보장 불가에 대한 내용은 아래 필드 주입에서 다룬다.
수정자 주입은 유연하지만 그만큼 위험할 수 있다. 마치 자동차 부품을 쉽게 교체할 수 있지만, 그만큼 잘못된 부품을 끼울 수 있는 위험도 있는 것처럼 말이다.
필드 주입 (Field Injection)
필드 주입은 필드에 직접 @Autowired 어노테이션을 사용하여 의존성을 주입하는 방식이다. 가장 간단하지만, 권장하지 않는 방식이다.
public class Car {
@Autowired
private Engine engine; // 필드에 직접 @Autowired 적용
}
테스트의 어려움
필드 주입을 사용하면 테스트 코드 작성이 매우 어려워진다. Spring 컨테이너 없이는 의존성을 주입할 방법이 없기 때문이다. 예를 들어, Car 클래스를 테스트할 때 engine 객체를 주입할 수 없어 NPE가 발생하게 된다. 이를 해결하기 위해서는 리플렉션을 사용하거나 Spring 컨테이너를 구동해야 하는데, 이는 단위 테스트의 본질에서 벗어나며 테스트 실행 시간도 크게 증가시킨다.
public class Car {
@Autowired
private Engine engine; // private 필드에 직접 주입
}
// 테스트 코드 작성 시
public class CarTest {
@Test
void testCar() {
Car car = new Car();
// engine을 주입할 방법이 없음
// Spring 컨테이너 없이는 테스트 불가능
}
}
불변성 보장 불가
필드 주입과 수정자 주입은 final 키워드를 사용할 수 없어 객체의 불변성을 보장할 수 없다. 이는 런타임에 의존성이 변경될 수 있다는 것을 의미하며, 예측하지 못한 버그와 동시성 문제를 일으킬 수 있다.
public class Car {
@Autowired
private Engine engine; // final 키워드 사용 불가
public void changeEngine(Engine newEngine) {
this.engine = newEngine; // 언제든지 변경 가능
}
}
필드 주입은 코드가 간단하지만 단위 테스트가 어렵고 의존성을 파악하기 힘들며, 수정자 주입은 런타임에 의존성이 변경될 수 있어 애플리케이션의 안정성을 해칠 수 있다. 또한 위의 두 가지 방식은 객체가 생성 후 의존성이 주입되므로 final 키워드를 사용할 수 없다. 때문에 Spring에서는 생성자 주입 방식을 가장 권장하고 있다.
생성자 주입 (Constructor Injection)
생성자 주입은 Spring Framework에서 가장 권장하는 의존성 주입 방식이다. 이 방식은 생성자를 통해 의존성을 주입받으며, 객체 생성 시점에 모든 의존성을 주입받을 수 있다.
public class Car {
private final Engine engine;
@AutoWired // 생성자가 1개만 있을 경우에 @Autowired를 생략가능
public Car(Engine engine) {
this.engine= engine;
}
}
불변성(Immutability) 보장
생성자 주입은 final 키워드를 사용할 수 있어 의존성 객체의 불변성을 보장한다. 객체가 생성된 후에는 의존성이 변경될 수 없으므로, 특히 멀티스레드 환경에서 안전한 코드를 작성할 수 있다.
순환 참조 방지
애플리케이션 시작 시점에 에러가 발생하므로 순환 참조를 감지할 수 있다. 이는 런타임에서 발생할 수 있는 심각한 문제를 사전에 방지할 수 있게 해준다. (스프링 부트 2.6 부터는 순환 참조가 기본적으로 허용되지 않도록 변경되었다.)
// 순환 참조는 컴파일은 되지만, 애플리케이션 실행 시점에 에러가 발생
public class Chicken {
private final Egg egg;
@Autowired
public Chicken(Egg egg) {
this.egg = egg;
}
}
public class Egg {
private final Chicken chicken;
@Autowired
public Egg(Chicken chicken) {
this.chicken = chicken;
}
}
테스트 용이성
테스트 코드에서 명시적으로 의존성을 주입할 수 있어, 테스트하고자 하는 클래스를 쉽게 인스턴스화할 수 있다. Mock 객체를 사용한 테스트도 매우 자연스럽게 구현할 수 있으며, 이는 각 컴포넌트를 독립적으로 테스트할 수 있게 해준다. 특히 Spring 컨테이너 없이도 POJO로 테스트가 가능하므로, 테스트 실행 속도도 향상된다.
public class CarTest {
@Test
void carShouldStart() {
// Given
Engine mockEngine = mock(Engine.class);
Car car = new Car(mockEngine); // 테스트용 Mock 객체 주입 가능
// When
car.start();
}
}
@RequiredArgsConstructor과의 결합
@RequiredArgsConstructor는 Lombok에서 제공하는 어노테이션으로, final 또는 @NonNull이 붙은 필드에 대한 생성자를 자동으로 생성해주는 어노테이션이다. 따라서, 불변성을 유지하고자 final 키워드를 붙인 필드들과 함께 생성자 주입 방식을 더욱 간단하게 구현할 수 있다.
@RequiredArgsConstructor
public class Car {
private final Engine engine;
public void start() {
engine.start();
}
}
참고
Dependencies :: Spring Framework
Dependencies :: Spring Framework
A typical enterprise application does not consist of a single object (or bean in the Spring parlance). Even the simplest application has a few objects that work together to present what the end-user sees as a coherent application. This next section explain
docs.spring.io
Dependency Injection :: Spring Framework
Dependency Injection :: Spring Framework
Constructor-based DI is accomplished by the container invoking a constructor with a number of arguments, each representing a dependency. Calling a static factory method with specific arguments to construct the bean is nearly equivalent, and this discussion
docs.spring.io
'Spring' 카테고리의 다른 글
Spring Boot에서 JPA 사용하기 (0) | 2024.09.06 |
---|---|
JPA란? (3) | 2024.09.01 |
Spring boot 3 버전 이상 swagger 사용법 (0) | 2024.08.24 |
IoC란? (0) | 2024.08.20 |
DI (0) | 2024.08.18 |