DI(Dependency Injection, 의존성 주입) 가이드
DI는 객체가 직접 자신의 의존성을 관리하지 않고, 외부에서 필요한 의존성을 주입받는 설계 패턴을 의미합니다. 이는 객체 간의 결합도를 낮추어 코드의 유지보수성과 확장성을 높여주며, IoC의 핵심적인 구현 방식 중 하나입니다. 스프링 프레임워크에서는 IoC 컨테이너가 객체를 생성하고 의존성을 주입해 주며, 이를 통해 객체 간의 강한 결합을 피할 수 있습니다.
- IoC와 DI의 관계 : IoC는 객체의 제어권을 외부로 넘김으로써 객체 간의 결합도를 낮추는 원리를 의미합니다. DI는 이 IoC의 구체적인 구현 방법으로, 객체가 직접 의존성을 생성하지 않고 스프링 IoC 컨테이너가 대신 객체를 생성하고 주입해 주는 방식입니다.
DI 방식의 차이점과 성능 이슈
스프링에서 제공하는 세 가지 DI 방식은 상황에 따라 서로 다른 장단점이 있습니다. 각 방식의 특성과 성능을 비교해서 애플리케이션 설계에 적합한 방식을 선택하는 것이 중요합니다. 이 중에 생성자 주입이 가장 권장되는 방식입니다.
1) 생성자 주입
생성자 주입은 클래스의 생성자를 통해 의존성을 주입하는 방식입니다. 주입해야 할 의존성을 생성자 매개변수로 받아 클래스 내부에 할당합니다. 생성자 주입은 객체를 생성하는 시점에 의존성이 완전히 주입되므로, 클래스의 불변성을 유지할 수 있습니다. 또한, 필수적인 의존성을 강제할 수 있어 객체의 일관성을 보장할 수 있습니다.
- 장점
- 불변성 유지 : 의존성이 객체 생성 시 주입되므로, 주입된 의존성을 변경할 수 없어 클래스의 불변성을 유지할 수 있습니다.
- 필수 의존성 강제 : 객체가 생성될 때 필수 의존성을 주입하지 않으면 애플리케이션이 실행되지 않으므로, 클래스의 일관성을 보장합니다.
- NullPointerException 방지 : 객체 생성 시 의존성이 주입되지 않으면 애플리케이션이 실행되지 않으므로, 런타임 에러를 사전에 방지할 수 있습니다.
- 테스트 용이성 : 의존성을 생성자 매개변수로 받으므로, 테스트 시 Mock 객체를 쉽게 주입할 수 있습니다.
- 단점
- 매개변수 과다 : 의존성이 많은 경우 생성자의 매개변수가 길어져 가독성이 떨어질 수 있습니다. 이 경우 Builder 패턴을 고려할 수 있습니다.
@Component
public class ConstructorInjectionService {
private final Dependency dependency;
// 생성자를 통한 의존성 주입
public ConstructorInjectionService(Dependency dependency) {
this.dependency = dependency;
}
public void serve() {
dependency.doSomething();
}
}
2) Setter 주입
Setter 메서드를 통해 의존성을 주입하는 방식입니다. 의존성을 주입하는 Setter 메서드를 정의하고, 필요에 따라 의존성을 주입할 수 있습니다. Setter 주입은 선택적인 의존성을 처리할 때 유용하며, 객체 생성 후에도 의존성을 주입하거나 변경할 수 있는 유연성을 제공합니다. 하지만 필수 의존성을 보장하기 어렵다는 단점이 있습니다.
- 장점
- 유연성 제공 : 객체 생성 이후에도 의존성을 변경할 수 있어, 동적으로 의존성을 주입하는 데 유리합니다.
- 선택적 의존성 : 반드시 필요하지 않은 의존성을 선택적으로 주입할 수 있습니다.
- 단점
- 불변성 보장 불가 : 생성 이후에도 의존성을 변경할 수 있어, 객체의 일관성이 깨질 수 있습니다.
- 필수 의존성 주입 강제 불가 : 필수 의존성을 주입하지 않아도 애플리케이션이 실행될 수 있어, 일관성 저하의 위험이 있습니다.
@Component
public class SetterInjectionService {
private Dependency dependency;
// Setter 메서드를 통한 의존성 주입
@Autowired
public void setDependencyA(Dependency dependency) {
this.dependency = dependency;
}
public void serve() {
dependency.doSomething();
}
}
3) 필드 주입
필드에 직접 @Autowired 애노테이션을 붙여 의존성을 주입하는 방식입니다. 클래스의 필드에 직접 의존성을 주입받습니다. 코드가 간결해지고 설정이 간단하지만, 테스트 시 의존성 주입이 어려워질 수 있으며, DI 원칙을 위반하는 방식으로 간주될 수 있습니다. 스프링에서는 필드 주입을 권장하지 않습니다.
- 장점
- 간결한 코드 : 필드에 바로 의존성을 주입받을 수 있어 코드가 간결해집니다.
- 단점
- 테스트 어려움 : 필드 주입은 Mock 객체 주입이 어려워 테스트 코드 작성 시 불리할 수 있습니다.
- 불변성 보장 불가 : 주입된 의존성을 변경할 수 있어 클래스의 불변성을 보장할 수 없습니다.
- 결합도 증가 : 외부에서 명시적으로 주입 과정을 확인할 수 없어 결합도가 증가할 수 있습니다.
@Component
public class FieldInjectionService {
@Autowired
private Dependency dependency;
public void serve() {
dependency.doSomething();
}
}
DI를 통한 장점
- 결합도 낮추기 : 객체가 직접 의존성을 관리하지 않기 때문에, 의존성 주입을 통해 서로 독립적인 객체를 만들 수 있습니다. 이는 결합도를 낮춰 코드의 유연성과 확장성을 증가시킵니다.
- 테스트 용이성 : DI를 사용하면 테스트를 위한 가짜 객체(Mock)를 주입할 수 있어 유닛 테스트를 쉽게 작성할 수 있습니다. 특히, 생성자 주입 방식은 테스트 시 의존성을 쉽게 주입할 수 있는 장점이 있습니다.
- 가독성과 유지보수성 증가 : 각 클래스는 각자 역할만을 수행하며, 외부 의존성을 명확하게 관리할 수 있기 때문에 코드의 가독성이 향상됩니다. 또한, 의존성이 변경되더라도 수정해야 할 부분이 최소화됩니다.
순환 의존성 문제와 해결 방법
순환 의존성 문제란?
순환 의존성은 두 개 이상의 빈이 서로를 의존할 때 발생하는 문제입니다. 예를 들어, A 빈이 B 빈을 의존하고, B 빈이 다시 A 빈을 의존하는 상황에서 서로 의존을 주입하기 전에 빈이 생성되어야 하기 때문에 문제가 발생합니다.
- 스프링에서의 해결 방법 : 스프링은 필드 주입 또는 Setter 주입을 사용하는 경우 프록시를 사용해 빈을 먼저 생성한 후 의존성을 주입해 순환 의존성 문제를 해결할 수 있습니다. 그러나 생성자 주입에서는 순환 의존성 문제를 해결하지 못하고 예외를 던지게 됩니다.
순환 의존성 문제를 피하기 위한 설계 원칙
- 의존성 최소화 : 의존성 구조를 설계할 때 각 객체 간의 의존성을 최소화하는 것이 중요합니다.
- 인터페이스 도입 : 인터페이스를 도입해서 객체 간의 결합을 줄이고 순환 의존성을 피할 수 있습니다.
- 디자인 패턴 활용 : 팩토리 패턴 또는 전략 패턴을 사용해서 객체 생성과 의존성 관리를 유연하게 할 수 있습니다.
Java Config와 XML 설정 비교
1) Java Config 기반 설정
- 장점
- 타입 안전성 : 컴파일 시점에서 오류를 찾을 수 있어, 타입 안전성을 제공합니다.
- IDE 지원 : 코드 완성 및 리팩토링 기능을 활용할 수 있어 유지보수가 편리합니다.
- 가독성 : 코드 형태로 설정을 관리하기 때문에 로직을 쉽게 이해할 수 있습니다.
@Configuration
public class AppConfig {
@Bean public Service service() {
return new ServiceImpl();
}
}
2) XML 기반 설정
- 장점
- 설정 분리 : 설정을 자바 코드와 분리할 수 있어 코드 의존성을 줄일 수 있습니다.
- 외부 시스템과의 통합 용이성 : XML은 외부 시스템과의 통합에서 유연하게 사용될 수 있습니다.
<beans>
<bean id="service" class="com.example.ServiceImpl"/>
</beans>
선택 시기
- 자바 Config 기반 설정 : 대부분의 현대 애플리케이션에서는 자바 Config 기반 설정이 권장됩니다. 가독성, 타입 안전성, IDE 지원 측면에서 이점이 있기 때문입니다.
- XML 기반 설정 : 레거시 시스템이나 외부 시스템과의 통합이 필요할 때 여전히 XML 설정이 유용할 수 있습니다.
결론
DI를 사용한 설계는 객체 간의 결합도를 낮추고 유연성을 제공하는 동시에, 잘못된 설계로 인해 순환 의존성 문제가 발생할 수 있습니다. 또한, 자바의 Config와 XML 설정 방식은 각각의 장단점이 있으며 상황에 맞는 방법을 선택해야 합니다. DI 방식과 설정 방식의 선택은 애플리케이션의 성능과 유지보수성을 결정짓는 중요한 요소이므로, 프로젝트 특성에 맞는 전략을 사용해야 합니다.
'🌱프레임워크 & 라이브러리 > 스프링부트' 카테고리의 다른 글
[Spring] 빈 등록과 관리 방법의 다양한 패턴 (0) | 2024.10.02 |
---|---|
[Spring] 스프링의 다양한 빈 스코프 가이드 : 싱글톤과 프로토타입 (0) | 2024.10.01 |
[Spring] IoC 컨테이너의 동장 방식과 빈의 생명주기 관리 (0) | 2024.09.30 |
[Spring] IoC 컨테이너의 계층 구조 [루트와 서블릿] (0) | 2024.09.29 |
[Spring] 전통과 현재의 IoC 컨테이너 개요 (0) | 2024.09.26 |