2025. 6. 18. 14:56ㆍSpring Framework/Spring IoC
의존성 주입은 객체가 자신이 필요한 의존성(즉, 함께 작업할 다른 객체들)을 생성자 아규먼트, 팩토리 메서드 아규먼트, 또는 객체가 생성되거나 팩토리 메서드에서 반환된 후 설정되는 프로퍼티(properties)를 통해 정의하는 방식이다.
Spring IoC 컨테이너는 Bean을 생성할 때 이러한 의존성들을 주입힌다.
이 과정을 제어의 역전(Inversion of Control, IoC)라고 한다.
💡 의존성 주입의 장점
| 결합도 감소 | 클래스 간 의존성이 약해져, 변경에 유연해진다. |
| 유지보수 용이 | 의존 객체를 쉽게 교체하거나 확장할 수 있다. |
| 단위 테스트 용이 | 실존 의존 객체 대신 Mock, Stub 등을 주입하여 테스트가 가능하다. |
| 가독성과 명확성 | 어떤 의존성이 필요한지 코드만 보고도 명확히 알 수 있다. |
💡 의존성 주입의 주요 두 가지 방식
1. 생성자 기반 의존성 주입 (Constructor-based Dependency Injection)
2. Setter 기반 의존성 주입(Setter-based Dependency Injection)
💥 필드 기반 의존성 주입도 있지만, 추천하지 않는다
Spring 공식 문서에서도 "필드 주입은 권장되지 않음" 적혀있다
⚙️ 생성자 기반 의존성 주입
생성자 기반 DI는 컨테이너가 여러 아규먼트를 가진 생성자를 호출하여 Bean을 생성할 때 이 아규먼트들 각각을 의존성으로 전달함으로써 이루어진다.
특정 아규먼트를 전달하여 정적 팩토리 메서드(static factory method)를 호출해 Bean을 생성하는 것도 거의 동일한 방식이다.
Spring이 가장 권장하는 방식이며, 불변 객체를 만들고, 필수 의존성을 강제할 수 있다
📃 예제
config 패키지
@Configuration
public class AppConfig {
@Bean
public MovieFinder movieFinder() {
return new MemoryMovieFinder();
}
@Bean
public MovieLister movieLister(MovieFinder movieFinder) {
return new MovieLister(movieFinder);
}
}
model 패키지
public interface MovieFinder {
List<String> findAll();
}
public class MemoryMovieFinder implements MovieFinder {
@Override
public List<String> findAll() {
List<String> movieList = new ArrayList<>();
movieList.add("Inception");
movieList.add("Interstellar");
movieList.add("Harry Potter");
return movieList;
}
}
public class MovieLister {
private MovieFinder movieFinder;
public MovieLister() {}
// 생성자 기반 DI
public MovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovie() {
System.out.println(movieFinder.findAll());
}
}
Main 클래스
public class Main {
public static void main(String... args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
MovieLister movieLister = context.getBean(MovieLister.class);
movieLister.listMovie();
}
}
출력시
[Inception, Interstellar, Harry Poter]
💡 Constructor-based Dependency Injection 장단점
| 장점 | 설명 |
| 🔐 불변성 보장 | 생성자에서 final 필드로 초기화 → 이후 변경 불가 (Immutable) |
| 📌 필수 의존성 보장 | 객체 생성 시 반드시 필요한 의존성을 명시해야 하므로 주입 누락 방지 |
| 🧪 단위 테스트 용이 | 테스트 시 생성자를 통해 Mock 객체 쉽게 전달 가능 |
| 🔍 의존성 명확화 | 생성자를 보면 어떤 의존성이 필요한지 한눈에 보임 → 가독성 향상 |
| 🚫 순환 참조 빠르게 감지 | 생성자 주입은 순환 참조 시 런타임에서 명확한 예외 발생 |
| 🔧 프레임워크 의존도 낮음 | 일반 Java 환경에서도 작동 (Spring 없어도 가능) |
| 🧼 @Autowired 생략 가능 | 생성자가 1개뿐이면 Spring이 자동으로 DI 처리해줌 |
| 단점 | 설명 |
| 📈 의존성 많을 경우 생성자 길어짐 | 의존성이 많은 클래스는 생성자의 파라미터 수가 과도하게 길어져 가독성 저하 |
| 🔄 순환 참조 발생 시 런타임 실패 | A → B → A 와 같은 구조에서는 Spring이 Bean 생성 중 에러 발생 |
| 🔨 빌더 패턴과 충돌 가능성 | 복잡한 객체 생성에 Builder 패턴을 함께 사용하면 코드 구조가 복잡해질 수 있음 |
| 📉 선택적 의존성 처리 어려움 | 필수가 아닌 의존성을 처리할 때 유연성이 부족함 (세터 주입이 더 적합) |
⚙️ Setter 기반 의존성 주입
Setter 기반 DI는 컨테이너가 기본 생성자 또는 아규먼트가 없는 static 팩토리 메서드를 호출하여 빈을 인스턴스화 한 후, Setter method를 통해 의존성을 주입함으로써 이루어진다.
특징으로 선택적 의존성 주입 가능, 객체 생성 후 의존성을 변경 가능하다.
❗ 문제로는 객체의 불완전한 상태(incomplete state) 발생할 수 있다.
📃 예제
config 패키지
@Configuration
@ComponentScan(basePackages = "dependency.dependencysetterinjection.model")
public class AppConfig {}
model 패키지
public interface MovieFinder {
List<String> findAll();
}
@Component
public class InMemoryMovieFinder implements MovieFinder {
@Override
public List<String> findAll() {
List<String> list = new ArrayList<>();
list.add("Harry Potter");
list.add("Inception");
list.add("Interstellar");
return list;
}
}
@Component
public class SimpleMovieLister {
private MovieFinder movieFinder;
public MovieFinder getMovieFinder() {
return movieFinder;
}
// Setter 주입
@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void printMovieList() {
System.out.println(movieFinder.findAll());
}
}
Main 클래스
public class Main {
public static void main(String... args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
SimpleMovieLister movieLister = context.getBean(SimpleMovieLister.class);
movieLister.printMovieList();
}
}
출력시
[Harry Potter, Inception, Interstellar]
🔗 ApplicationContext와 다양한 DI 지원
ApplicationContext는 관리하는 Bean에 대해 생성자 기반 DI와 Setter 기반 DI 모두를 지원한다. 또한 일부 의존성이 생성자 주입으로 먼저 주입된 이후, 나머지를 Setter 주입 방식으로 이어서 주입하는 것도 가능하다.
의존성은 BeanDefinition 형태로 구성되며, 이를 통해 PropertyEditor 인스턴스와 함께 속성 값을 다른 형식으로 변환할 수 있다. 하지만 대부분의 Spring 사용자들은 이러한 클래스를 직접(프로그래밍적으로) 다루지 않고, 다음과 같은 방식을 사용한다.
- XML 기반 Bean 설정
- @Component 등으로 어노테이션 기반 구성 요소
- @Configuration 클래스 안의 @Bean 메서드
이러한 구성 메타데이터들은 내부적으로 BeanDefinition 인스턴스로 변환되어, 전체 Spring IoC 컨테이너 인스턴스를 로드하는 데 사용된다.
⚔️ 생성자 기반 DI vs Setter 기반 DI
Spring에서는 생성자 기반 DI와 Setter 기반 DI를 혼합하여 사용할 수 있다. 하지만 일반적인 권장 규칙은 다음과 같다:
필수적인 의존성은 생성자를 통해 주입하고
선택적인 의존성은 Setter 메서드나 configuration 메서드를 통해 주입하라.
Setter 메서드에 @Autowired 어노테이션을 사용하면 해당 속성을 필수 의존성으로 지정할 수 있다.
🧐 스프링 팀의 권장 사항
Spring 팀은 생성자 주입(Constructor Injection)을 일반적으로 더 권장한다.
- 어플리케이션 구성 요소를 불변 객체(Immutable Object)로 만들 수 있음
- 필수 의존성이 null이 아님을 보장
- 생성자 주입을 통해 주입된 컴포넌트는 항상 완전히 초기화된 상태로 클라이언트 코드에 반환됨
참고 : 생성자 인자가 너무 많으면 안 좋은 코드 냄새(Bad Code Smell)다.
💥 순환 참조(Circular Dependencies) 문제
두 개 이상의 Bean이 서로를 의존하면서 끝나지 않는 순환 구조를 만들 때 발생하는 문제다.
📃 예제 : 생성자 주입으로 인한 순환 의존성 발생
A → B → A → B ...
@Component
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
- 생성자 주입(Constructor Injection)을 주로 사용하는 경우, 해결할 수 없는 순환 참조 상황이 발생할 수 있다.
현재 코드를 보면
▶ 클래스 A가 생성자에서 클래스 B를 요구
▶ 클래스 B가 생성자에서 클래스 A를 요구
→ Spring이 어느 것도 먼저 생성할 수 없어 오류 발생
💡 해결 방법
1. Setter 주입으로 변경
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
- Setter 주입은 객체가 완전히 생성된 후 주입됨
- Spring이 A와 B 중 하나를 먼저 생성할 수 있음
2. @Lazy 사용
@Component
public class A {
private B b;
@Autowired
public A(@Lazy B b) {
this.b = b;
}
}
- @Lazy를 사용하여 B의 객체 생성을 지연 → 즉시 객체를 만들지 않고 필요할 때 생성
'Spring Framework > Spring IoC' 카테고리의 다른 글
| Spring의 Method Injection (0) | 2025.06.20 |
|---|---|
| DI에서 Depend on, Lazy, Autowiring 사용 (0) | 2025.06.18 |
| Bean의 인스턴스화 방법 (0) | 2025.06.11 |
| Bean 이름 지정 (Naming Beans) (0) | 2025.06.11 |
| Bean Definition(정의) (0) | 2025.06.11 |