DI(Dependency Injection): 의존성 주입
**의존성(Dependency)이란 한 객체가 다른 객체를 사용하는 것
public class UserService {
private UserRepository repository; // UserService는 UserRepository에 의존한다
public User findUser(Long id) {
return repository.findById(id);
}
}
UserService는 UserRepository 없이는 동작할 수 없다. 이게 의존성입니다
**주입(Injection)이란 이 의존성을 외부에서 넣어주는 것
public UserService(UserRepository repository) {
this.repository = repository;
}
객체를 직접 생성하지 않고 외부에서 받는다. 이게 DI(Dependency Injection) 의존성 주입입니다
3가지 주입 방법
1. 생성자 주입 (권장)
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
장점:
- final 키워드 사용 가능 (불변 보장)
- 순환 참조 즉시 발견
- 테스트 코드 작성 쉬움
- Spring 4.3부터 생성자가 하나면 @Autowired 생략 가능
언제 사용? 항상. 특별한 이유 없으면 무조건 생성자 주입.
2. 필드 주입
@Service
public class UserService {
@Autowired
private UserRepository repository;
}
단점:
- final 못 씀
- 순환 참조를 늦게 발견함
- 테스트할 때 Spring 컨테이너 필요
- 의존성이 숨겨져서 클래스가 비대해질 수 있음
언제 사용? 테스트 코드에서만. 그것도 @SpringBootTest 쓸 때만.
3. 세터 주입
@Service
public class UserService {
private UserRepository repository;
@Autowired
public void setRepository(UserRepository repository) {
this.repository = repository;
}
}
장점:
- 선택적 의존성일 때 유용 (required = false 설정 가능)
단점:
- final 못 씀
- 객체 생성 후 setter 호출 전까지는 불완전한 상태
언제 사용? 거의 안 씀. 정말 선택적 의존성일 때만.
비교 정리
| 방식 | final | 순환참조 | 테스트 |
| 생성자 | O | 빠름 | 쉬움 |
| 필드 | X | 느림 | 어려움 |
| 세터 | X | 느림 | 보통 |
@Autowired 동작 원리
Spring이 어떻게 의존성을 찾아서 주입할까?
1. 타입으로 찾기
@Service
public class UserService {
public UserService(UserRepository repository) { // UserRepository 타입으로 찾음
this.repository = repository;
}
}
ApplicationContext에서 UserRepository 타입의 Bean을 찾아서 넣어줍니다
2. 주입 시점
디버거로 확인해보자.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); // 브레이크포인트
}
}
Step Into로 따라가면:
- @Component, @Service 등이 붙은 클래스 스캔
- Bean 정의 정보 생성
- 의존성이 없는 Bean부터 생성 (UserRepository 먼저)
- 의존성이 있는 Bean 생성 (UserService)
- 생성자 파라미터 확인
- 타입에 맞는 Bean 검색
- 찾은 Bean을 생성자에 넣어서 호출
- ApplicationContext에 등록
모두 애플리케이션 시작 시점에 일어납니다
같은 타입 Bean이 여러 개일 때
public interface UserRepository {
}
@Repository
public class JpaUserRepository implements UserRepository {
}
@Repository
public class MemoryUserRepository implements UserRepository {
}
이러면 어떻게 될까?
@Service
public class UserService {
public UserService(UserRepository repository) { // 어떤 걸 넣어줘야 하지?
this.repository = repository;
}
}
실행하면:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in UserService required a single bean, but 2 were found:
- jpaUserRepository
- memoryUserRepository
에러 발생. Spring이 어떤 걸 넣어줘야 할지 모르기 때문입니다
해결 방법 1: @Primary
@Repository
@Primary // 기본으로 이걸 쓰겠다
public class JpaUserRepository implements UserRepository {
}
@Repository
public class MemoryUserRepository implements UserRepository {
}
여러 개 중에서 기본으로 사용할 Bean을 지정합니다
해결 방법 2: @Qualifier
@Service
public class UserService {
public UserService(@Qualifier("jpaUserRepository") UserRepository repository) {
this.repository = repository;
}
}
Bean 이름을 직접 지정해서 주입받습니다
해결 방법 3: 파라미터 이름으로 매칭
@Service
public class UserService {
public UserService(UserRepository jpaUserRepository) { // 파라미터 이름과 Bean 이름 일치
this.repository = jpaUserRepository;
}
}
타입으로 찾고 → 이름으로 한 번 더 필터링합니다
해결 방법 4: 모두 받기
@Service
public class UserService {
private final List<UserRepository> repositories;
public UserService(List<UserRepository> repositories) {
this.repositories = repositories; // 모든 UserRepository를 List로
}
}
같은 타입 Bean을 전부 받을 수도 있습니다
우선순위
- @Qualifier로 명시한 것
- @Primary 붙은 Bean
- 파라미터 이름과 일치하는 Bean
- 안 되면 에러
순환 참조 문제
A가 B를 참조하고, B가 A를 참조하면?
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
@Service
public class BService {
private final AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
생성자 주입: 즉시 에러
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| aService defined in file [AService.class]
↑ ↓
| bService defined in file [BService.class]
└─────┘
왜?
- AService 생성하려면 BService 필요
- BService 생성하려면 AService 필요
- 둘 다 생성 못 함
- 애플리케이션 시작 실패
빨리 알 수 있어서 좋습니다!
필드 주입: 나중에 에러
@Service
public class AService {
@Autowired
private BService bService; // 일단 null로 객체 생성
}
@Service
public class BService {
@Autowired
private AService aService; // 일단 null로 객체 생성
}
객체는 일단 만들어지고, 나중에 필드에 주입하려다가 실패합니다. 근데 Spring 2.6부터는 필드 주입도 순환 참조를 감지해서 시작 시 에러를 냅니다
해결 방법
1. 설계 재검토 (가장 좋음)
순환 참조는 보통 설계 문제다. 둘 중 하나를 다른 방향으로 의존하도록 변경하거나, 제3의 클래스로 분리합니다
@Service
public class AService {
private final CService cService;
}
@Service
public class BService {
private final CService cService;
}
@Service
public class CService {
// 공통 로직
}
2. @Lazy 사용 (임시방편)
@Service
public class AService {
private final BService bService;
public AService(@Lazy BService bService) { // 필요할 때 생성
this.bService = bService;
}
}
프록시를 주입하고, 실제 호출 시점에 Bean을 가져옵니다. 이것은 근본적인 해결법은 아닙니다
실전 팁
1. Lombok @RequiredArgsConstructor
@Service
@RequiredArgsConstructor // final 필드로 생성자 자동 생성
public class UserService {
private final UserRepository repository;
private final EmailService emailService;
// 생성자 안 써도 됨
}
@RequiredArgsConstructor 애너테이션은 final 필드를 생성자 파라미터로 하는 생성자를 자동으로 만들어줍니다
2. Optional 의존성
@Service
public class UserService {
private final UserRepository repository;
private final EmailService emailService; // 없어도 됨
public UserService(UserRepository repository,
@Autowired(required = false) EmailService emailService) {
this.repository = repository;
this.emailService = emailService; // null일 수 있음
}
}
또는
@Service
public class UserService {
private final UserRepository repository;
private final Optional<EmailService> emailService;
public UserService(UserRepository repository,
Optional<EmailService> emailService) {
this.repository = repository;
this.emailService = emailService;
}
}
3. 생성자가 여러 개일 때
@Service
public class UserService {
private final UserRepository repository;
private final EmailService emailService;
@Autowired // 이 생성자를 사용하라고 명시
public UserService(UserRepository repository, EmailService emailService) {
this.repository = repository;
this.emailService = emailService;
}
public UserService(UserRepository repository) {
this(repository, null);
}
}
생성자가 여러 개면 @Autowired를 명시해야 합니다
정리
- DI: 의존성을 외부에서 주입받는 것
- 생성자 주입을 써라: final, 순환참조 감지, 테스트 편함
- @Autowired: 타입으로 Bean을 찾아서 주입
- 같은 타입 여러 개: @Primary, @Qualifier, 파라미터 이름, List로 받기
- 순환 참조: 생성자 주입은 시작 시 에러, 설계 재검토가 답이다
- 실전: Lombok @RequiredArgsConstructor 활용
'Spring' 카테고리의 다른 글
| [Spring] Servlet와 DispatcherServlet (0) | 2026.03.31 |
|---|---|
| [Spring] Bean이란? (0) | 2026.01.24 |
| [Spring] IoC(Inversion of Control) (0) | 2025.12.31 |
| [Spring] 스프링의 3계층 구조 : Controller, Service, Repository (1) | 2025.12.11 |
| [Spring] @WebMvcTest 401 에러 해결: @AutoConfigureMockMvc vs excludeFilters (0) | 2025.12.04 |