의존성 주입 방식 4가지 — 어떤 차이가 있을까?
스프링에서 객체 간 의존성을 주입하는 방법은 크게 4가지가 있다.
| 구분 | 설명 |
|---|---|
| 생성자 주입 | 생성자를 통해 필요한 객체를 주입 |
| 필드 주입 | 클래스 내부 필드에 직접 주입 |
| 수정자(Setter) 주입 | 세터 메서드를 통해 주입 |
| 일반 메서드 주입 | 일반 메서드를 통해 주입 |
1. 생성자 주입 — 가장 권장되는 방식
@Component
public class OrderService {
private final MemberService memberService;
public OrderService(MemberService memberService) {
this.memberService = memberService;
}
}
- 객체를 생성할 때 필수 의존성을 한 번에 받아서 주입
- 필드를
final로 선언할 수 있어 불변 객체로 만들 수 있다 - 생성자가 1개일 경우 @Autowired 생략 가능
장점
- 불변성 보장
- 테스트 용이 (스프링 없이도 생성 가능)
- 컴파일 또는 실행 시점에 의존성 누락 감지
- 순환 참조 발생 시 즉시 오류 발생 (fail-fast)
2. 필드 주입 — 사용은 가능하지만 권장되지 않음
@Component
public class OrderService {
@Autowired
private MemberService memberService;
}
@Autowired를 필드에 붙여 객체를 직접 주입- 코드가 짧아보이지만, 스프링 없이는 사용할 수 없다
단점
- 테스트에서 직접 주입 불가 → 테스트 코드 작성 어려움
- 의존성 누락 여부를 코드만 보고 파악하기 힘듦
- 순환 참조가 감춰져 실행 중 문제가 생길 수 있음
실무에선 거의 사용하지 않거나, 아주 제한적으로만 사용
3. 수정자(Setter) 주입 — 선택적 의존성에 적합
@Component
public class OrderService {
private MemberService memberService;
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
- 객체 생성 이후, 세터 메서드를 통해 의존성 주입
- 선택적으로 필요한 경우에 유용 (있어도 되고 없어도 되는 의존성)
장점
- 의존성 변경이 가능 (동적으로 바꿀 수 있음)
- 테스트 코드에서 일부만 주입 가능
단점
- 불변성 보장 불가
- 누가 언제 의존성을 넣었는지 알기 어려움
- 순환 참조 시 필드 주입과 동일하게 지연되어 늦게 에러 발생
4. 일반 메서드 주입 — 거의 쓰지 않음
@Component
public class OrderService {
private MemberService memberService;
@Autowired
public void init(MemberService memberService) {
this.memberService = memberService;
}
}
- 세터 주입과 유사하지만, 이름이 꼭
setXxx()일 필요는 없음 - 특정 라이프사이클 타이밍에만 쓰이는 예외적 주입 방식
사용 예
- 여러 의존성을 한 번에 주입해야 할 때
- 명확하게 "초기화 단계"임을 드러내고 싶을 때
일반 개발에서는 거의 사용되지 않음. 특수한 상황에만 사용
왜 생성자 주입을 써야 할까?
생성자 주입은 위 4가지 방식 중에서도 다음 이유로 가장 우수하다.
1. 불변성 보장
- 주입된 객체는
final로 선언할 수 있다. - 실행 도중 실수로 의존성이 바뀌는 일을 방지한다.
2. 의존성 누락 방지
- 생성자에 없으면 객체 생성이 불가능
- 필수 의존성이 빠지면 컴파일 또는 실행 시 바로 오류가 발생
3. 테스트 용이
OrderService service = new OrderService(mockMemberService);
- 스프링 없이도 객체를 생성해 테스트할 수 있다.
- Mockito와 함께 단위 테스트를 쉽게 작성 가능
4. 순환 참조 탐지 가능
순환 참조란?
@Component
public class A {
public A(B b) {}
}
@Component
public class B {
public B(A a) {}
}
A는B가 필요하고,B는 다시A가 필요- 이 구조는 무한 루프처럼 작동해 앱 실행을 막는다
생성자 주입 vs 필드/세터 주입
| 주입 방식 | 순환 참조 발생 시 | 결과 |
|---|---|---|
| 생성자 주입 | 즉시 에러 발생 | 빠르게 감지 가능 (fail-fast) |
| 필드/세터 주입 | 실행됨 → 나중에 오류 | 문제 원인 파악 어려움, 위험 |
5. 명확한 설계
- 생성자를 보면 이 클래스가 무엇을 필요로 하는지 한눈에 보인다
- 유지보수 및 협업 시 구조 이해가 쉬워진다
생성자 주입은 스프링 개발에서의 표준이라고 봐도 무방하다.
처음부터 생성자 주입을 쓰면 구조가 단단해지고, 나중에 유지보수도 훨씬 쉬워진다.
'SpringBoot' 카테고리의 다른 글
| 웹 애플리케이션의 이해 (0) | 2025.05.13 |
|---|---|
| RequestScope로 요청별 로그 분리하기 (0) | 2025.05.07 |
| 스프링 빈 자동 등록과 의존성 주입 (1) | 2025.05.01 |
| 싱글톤 패턴과 스프링의 싱글톤 관리 (0) | 2025.04.30 |
| Spring Boot: 데이터 검증(Validation) (1) | 2024.11.22 |