들어가며
Spring에서 의존성 주입(DI) 방법은 세 가지가 있습니다.
그 중 가장 권장되는 방식이 바로 생성자 주입입니다.
왜 생성자 주입이 권장되는지, 불변성과 테스트 용이성이 왜 중요한지 정리해보겠습니다.
의존성 주입(DI)이란?
의존성 주입은 객체가 필요로 하는 다른 객체를 직접 생성하지 않고 외부(Spring 컨테이너)에서 주입받는 것입니다.
// 직접 생성 - 의존성 주입 아님
public class MemberService {
private MemberRepository memberRepository = new MemberRepository(); // 직접 생성
}
// 의존성 주입 - Spring이 주입
public class MemberService {
private final MemberRepository memberRepository; // 외부에서 주입받음
}
세 가지 의존성 주입 방법
1. 필드 주입
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository; // 필드에 직접 주입
}
코드가 간결하지만 불변성 보장 안 됨, 테스트 어려움 등의 단점이 있습니다.
2. 수정자(Setter) 주입
@Service
public class MemberService {
private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
선택적 의존성에는 유용하지만, 주입이 누락되면 런타임 오류가 발생합니다.
3. 생성자 주입 (권장)
@Service
public class MemberService {
private final MemberRepository memberRepository; // final 키워드
@Autowired // 생성자가 하나면 생략 가능
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
객체 생성 시점에 모든 의존성을 주입받기 때문에 불변성 보장과 테스트 용이성이 뛰어납니다.
생성자 주입의 동작 원리
Spring 컨테이너가 Bean을 생성할 때 @Autowired가 붙은 생성자를 찾아 필요한 Bean을 자동으로 주입합니다.
[Spring 컨테이너 Bean 생성 과정]
1. MemberService Bean 생성 요청
↓
2. @Autowired 생성자 탐색
↓
3. 생성자 파라미터 타입(MemberRepository) 확인
↓
4. 컨테이너에서 MemberRepository Bean 조회
↓
5. MemberService 생성자 호출 (MemberRepository 주입)
↓
6. MemberService Bean 등록 완료
불변성 보장 (Immutability)
생성자 주입의 가장 큰 장점은 final 키워드를 사용할 수 있다는 것입니다.final 필드는 한 번 할당되면 변경할 수 없기 때문에, 객체가 생성된 이후 의존성이 바뀌지 않습니다.
// 필드 주입 - final 사용 불가
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository; // final 불가, 언제든 변경 가능
}
// 생성자 주입 - final 사용 가능
@Service
public class MemberService {
private final MemberRepository memberRepository; // final - 변경 불가
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository; // 생성 시점에 1번만 할당
}
}
필드 주입: 생성 → 주입 → 언제든 변경 가능 → 불안정
생성자 주입: 생성 시점에 주입 → 이후 변경 불가 (final) → 안정적
테스트 용이성
생성자 주입은 Spring 컨테이너 없이도 순수 Java 코드로 테스트할 수 있습니다.
필드 주입 - 테스트하기 어렵다
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository; // Spring이 주입해야만 사용 가능
}
// 테스트
class MemberServiceTest {
@Test
void test() {
MemberService service = new MemberService();
// memberRepository가 null! → 테스트 불가
// Spring 컨테이너 없이는 주입 방법이 없음
}
}
생성자 주입 - 테스트하기 쉽다
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
// 테스트 - Spring 없이 순수 Java로 가능
class MemberServiceTest {
@Test
void test() {
// Mock 객체나 가짜 객체를 직접 생성해서 주입
MemberRepository mockRepository = new FakeMemberRepository();
MemberService service = new MemberService(mockRepository); // 직접 주입 가능
// 테스트 실행
Member member = service.findById(1L);
assertNotNull(member);
}
}
생성자로 의존성을 받기 때문에 테스트 시 가짜 객체(Mock)를 직접 주입할 수 있습니다.
순환 참조 방지
생성자 주입은 순환 참조 문제를 컴파일 시점에 발견할 수 있습니다.
순환 참조: A → B → A → B → ... (무한 반복)
MemberService → OrderService → MemberService (순환!)
// 필드 주입: 순환 참조가 있어도 애플리케이션이 실행됨 → 런타임 오류
// 생성자 주입: 순환 참조 발생 시 애플리케이션 시작 자체가 실패 → 즉시 발견 가능
생성자 주입은 순환 참조를 런타임이 아닌 시작 시점에 바로 잡아줍니다.
@RequiredArgsConstructor로 더 간결하게
Lombok의 @RequiredArgsConstructor를 사용하면 final 필드에 대한 생성자를 자동으로 만들어줍니다.@Autowired도 생략 가능해 코드가 훨씬 간결해집니다.
// @RequiredArgsConstructor 사용 전
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final MailService mailService;
@Autowired
public MemberService(MemberRepository memberRepository, MailService mailService) {
this.memberRepository = memberRepository;
this.mailService = mailService;
}
}
// @RequiredArgsConstructor 사용 후
@Service
@RequiredArgsConstructor // final 필드 생성자 자동 생성
public class MemberService {
private final MemberRepository memberRepository;
private final MailService mailService;
// 생성자 코드 자동 생성됨 → 코드 간결
}
Creator-Flex 프로젝트에서도 @RequiredArgsConstructor를 적극 활용했습니다.
세 가지 주입 방식 비교
| 항목 | 필드 주입 | 수정자 주입 | 생성자 주입 |
|---|---|---|---|
| final 사용 | 불가 | 불가 | 가능 |
| 불변성 보장 | 안 됨 | 안 됨 | 보장됨 |
| 테스트 용이성 | 낮음 | 보통 | 높음 |
| 순환 참조 탐지 | 런타임 | 런타임 | 시작 시점 |
| 코드 간결성 | 간결 | 보통 | Lombok으로 간결 가능 |
| Spring 권장 여부 | 비권장 | 선택적 의존성에만 | 권장 |
정리
- 생성자 주입은
@Autowired가 붙은 생성자를 통해 객체 생성 시점에 의존성을 주입받는 방식이다. final키워드를 사용할 수 있어 한 번 주입된 의존성이 변경되지 않는 불변성이 보장된다.- Spring 컨테이너 없이 생성자에 직접 Mock 객체를 주입할 수 있어 테스트가 용이하다.
- 순환 참조 문제를 런타임이 아닌 애플리케이션 시작 시점에 발견할 수 있다.
@RequiredArgsConstructor를 함께 사용하면 생성자 코드 없이도 간결하게 생성자 주입을 구현할 수 있다.