스프링 IoC와 DI 개념 정리: 빈, 주입 방식, 스코프
IoC(제어의 역전)는 객체의 생성과 연결을 개발자가 아니라 프레임워크(컨테이너)가 맡는 것이고, DI(의존성 주입)는 그 IoC를 구현하는 방법으로, 객체가 필요한 의존성을 외부에서 넣어주는 것이다.
1. 의존성(Dependency)이란
A가 동작하려면 B가 필요할 때 "A는 B에 의존한다"고 한다. OrderService가 결제를 위해 PaymentClient를 쓴다면 OrderService는 PaymentClient에 의존한다. 문제는 누가 B를 만들고 고르느냐다.
2. 직접 생성 vs 주입
| 구분 | 직접 생성 (new) | 의존성 주입 (DI) |
| 구현체 선택 | 사용하는 클래스가 결정 | 외부(컨테이너, 설정)가 결정 |
| 결합도 | 높음 (구현체에 묶임) | 낮음 (인터페이스에만 의존) |
| 구현 교체 | 사용하는 쪽 코드 수정 필요 | 설정만 변경 |
| 테스트 | 가짜 객체로 바꾸기 어려움 | 생성자로 가짜 객체 전달 가능 |
| 객체 수명 관리 | 개발자가 직접 | 컨테이너가 관리 |
DI는 SOLID의 DIP(의존 역전 원칙), "구체가 아니라 추상에 의존하라"를 실천하는 수단이기도 하다.
3. IoC 컨테이너와 빈
- IoC 컨테이너: 스프링에서는
ApplicationContext. 객체를 만들고, 의존성을 연결하고, 생명주기를 관리한다. - 빈(Bean): 컨테이너가 생성하고 관리하는 객체.
| 등록 방법 | 방식 | 언제 쓰나 |
| 컴포넌트 스캔 | 클래스에 @Component, @Service, @Repository, @Controller | 직접 만든 클래스 |
| 자바 설정 | @Configuration 클래스의 @Bean 메서드 | 외부 라이브러리 객체, 생성 로직이 복잡할 때 |
@Service, @Repository, @Controller는 모두 @Component의 특수화로, 역할을 드러내는 이름표다. @Repository는 DB 예외를 스프링 예외로 변환하는 기능이 추가된다.
4. 빈 생명주기
- 컨테이너 생성
- 빈 인스턴스 생성
- 의존성 주입
- 초기화 콜백 (
@PostConstruct) - 사용
- 소멸 전 콜백 (
@PreDestroy) - 컨테이너 종료
의존성은 3단계에서 들어오므로, 생성자 안에서는 생성자 주입으로 받은 것만 쓸 수 있고, 필드·수정자 주입 값은 @PostConstruct 이후에 써야 한다.
5. 주입 방식 비교
| 구분 | 생성자 주입 | 수정자(Setter) 주입 | 필드 주입 |
| 형태 | 생성자 파라미터 | setXxx 메서드 + @Autowired | 필드에 @Autowired |
| final 사용 | 가능 (불변 보장) | 불가 | 불가 |
| 누락 시 | 객체 생성 자체가 실패 (빠른 발견) | null로 남을 수 있음 | null로 남을 수 있음 |
| 테스트 | new로 쉽게 생성 | 가능 | 스프링 없이 넣을 방법이 없음 |
| 순환 참조 | 기동 시점에 감지 | 늦게 발견 | 늦게 발견 |
| 권장 | 기본 | 선택적 의존성 | 테스트 코드 외에는 지양 |
생성자가 하나면 @Autowired를 생략할 수 있고(Spring 4.3+), 롬복 @RequiredArgsConstructor와 함께 쓰면 final 필드만 선언하면 된다.
6. 같은 타입의 빈이 여러 개일 때
| 방법 | 설명 |
| @Primary | 여러 후보 중 기본으로 쓸 빈 지정 |
| @Qualifier("이름") | 주입받는 쪽에서 특정 빈을 이름으로 선택 |
| List / Map으로 받기 | 해당 타입의 빈 전부를 주입 (Map은 빈 이름이 key) |
아무것도 지정하지 않으면 NoUniqueBeanDefinitionException이 발생한다.
7. 빈 스코프
| 스코프 | 생성 단위 | 비고 |
| singleton | 컨테이너당 1개 | 기본값 |
| prototype | 요청(주입)할 때마다 새로 | 생성 후에는 컨테이너가 관리하지 않음 (소멸 콜백 없음) |
| request | HTTP 요청당 1개 | 웹 환경 |
| session | HTTP 세션당 1개 | 웹 환경 |
싱글톤 빈은 여러 요청(스레드)이 동시에 같은 객체를 공유한다. 그래서 싱글톤 빈에 요청별로 바뀌는 값을 필드로 저장하면 다른 사용자의 값과 섞인다. 싱글톤 빈은 무상태(stateless)로 설계해야 한다.
8. 실제로 어떻게 적용되나
- 결제 수단별 전략 패턴:
PaymentClient인터페이스의 구현(Toss, 카드사 등)을List<PaymentClient>로 전부 주입받아 Map으로 만들어 두면, 결제 수단이 추가돼도 구현 클래스 하나만 추가하면 된다. "같은 타입의 빈 여러 개를 컬렉션으로 주입"을 활용한 것이다. - 다중 DataSource: DB가 두 개면 같은 타입(
DataSource)의 빈이 두 개가 된다. 하나에@Primary를 붙이고 다른 쪽은@Qualifier로 골라 쓴다. - 테스트: 생성자 주입이면
new OrderService(fakePaymentClient)처럼 스프링 없이 가짜 객체로 유닛 테스트할 수 있다. - @Transactional과 프록시: 컨테이너는 원본 빈 대신 프록시를 주입할 수 있다. 트랜잭션, 캐시 같은 기능이 이 방식으로 붙는다. 그래서 같은 클래스 안에서 자기 메서드를 직접 호출하면 프록시를 거치지 않아
@Transactional이 적용되지 않는다.
9. 순환 참조
A가 B를, B가 A를 생성자로 받으면 어느 쪽도 먼저 만들 수 없다. Spring Boot 2.6부터는 순환 참조를 기본적으로 금지해 기동 시점에 실패한다. 주입 방식을 바꿔 우회하기보다 공통 로직을 별도 클래스로 분리해 구조를 고치는 게 원칙이다.
10. 면접 질문으로 정리
- Q. IoC와 DI의 차이는? IoC는 객체 생성과 의존 관계 관리의 제어권을 개발자에서 컨테이너로 넘기는 원칙이고, DI는 필요한 의존성을 외부에서 주입하는 방식으로 IoC를 구현하는 방법이다.
- Q. 생성자 주입을 권장하는 이유는? final로 불변성을 보장하고, 의존성이 누락되면 객체 생성 시점에 바로 실패하며, 스프링 없이도 테스트에서 직접 생성할 수 있고, 순환 참조를 기동 시점에 발견할 수 있기 때문이다.
- Q. 스프링 빈은 기본적으로 싱글톤인데 주의할 점은? 여러 스레드가 같은 인스턴스를 공유하므로 요청마다 바뀌는 상태를 필드에 두면 안 된다. 무상태로 설계해야 한다.
- Q. 같은 타입 빈이 여러 개면? @Primary로 기본 빈을 지정하거나, @Qualifier로 이름을 지정하거나, List·Map으로 전부 주입받는다.