Spring
IoC 컨테이너에 대해서 설명해주세요
Spring의 심장인 IoC 컨테이너가 어떻게 객체를 생성하고 의존성을 주입하며 생명주기를 관리하는지. 전통적인 객체 생성과의 차이부터 DI 방식, Bean 스코프, 컨테이너 초기화 과정까지 인터랙티브 시각화로 완전 정복합니다.
2026년 3월 21일 · 약 12분 읽기
Q. "Spring의 IoC 컨테이너가 무엇인지 설명하고, 전통적인 객체 생성 방식과 어떤 차이가 있는지 말씀해주세요."
예상 꼬리질문
답변 가이드
IoC 컨테이너는 객체 생성, 의존성 주입, 생명주기 관리를 개발자 대신 수행하는 Spring의 핵심 엔진입니다. 전통적인 방식에서는 개발자가 직접 new로 객체를 생성하고 의존성을 연결했지만, IoC 방식에서는 컨테이너가 @Component, @Service 같은 어노테이션을 스캔해서 Bean을 자동으로 생성하고 필요한 곳에 주입합니다. 이것이 바로 "제어의 역전"입니다.
구체적으로 역전되는 것은 프로그램의 제어 흐름입니다. 전통적인 방식에서는 내 코드가 라이브러리를 호출하지만, IoC 방식에서는 프레임워크가 내 코드를 호출합니다. 이 원칙을 구현하는 가장 대표적인 방법이 DI(의존성 주입)이며, Spring은 생성자 주입을 권장합니다. 생성자 주입은 final 필드로 불변성을 보장하고, 필수 의존성을 명시하며, Spring 없이도 new Service(mockRepo) 형태로 테스트할 수 있습니다.
실무에서 가장 중요한 포인트는 Singleton Bean의 무상태 설계입니다. Spring Bean의 기본 스코프는 Singleton으로, 모든 요청이 같은 인스턴스를 공유합니다. 인스턴스 변수에 요청별 데이터를 저장하면 여러 스레드가 동시에 접근하여 Race Condition이 발생합니다. 이를 이해하면 Spring 애플리케이션의 동시성 버그를 예방할 수 있습니다.
핵심 요약 (TL;DR)
- - IoC(제어의 역전)는 프로그램의 제어 흐름을 개발자가 아닌 프레임워크가 담당하게 하는 설계 원칙이다
- - DI(의존성 주입)는 IoC를 구현하는 구체적인 방법이다 — IoC가 "무엇"이라면 DI는 "어떻게"
- - 생성자 주입이 권장되는 이유는 불변성 보장, 필수 의존성 명시, 순환 의존성 조기 발견, 테스트 용이성 때문이다
- - ApplicationContext는 BeanFactory를 확장해 국제화, 이벤트 발행, AOP 통합 등 엔터프라이즈 기능을 추가한 실무 표준 컨테이너다
- - Singleton Bean은 무상태(Stateless)로 설계해야 하며, 상태가 필요하면 메서드 파라미터로 전달하거나 다른 스코프를 사용해야 한다
면접관이 정말 듣고 싶은 건 "IoC가 제어의 역전이다"라는 정의가 아니라, 왜 Spring이 이 방식을 선택했고 실무에서 어떤 문제를 해결하는지입니다.
직접 new로 객체를 만들면 간단해 보이지만, 프로젝트 규모가 커지면 의존성 그래프가 복잡해지고 테스트마다 Mock을 주입하기 어려워집니다. IoC 컨테이너는 이 모든 문제를 "객체 생성 권한을 프레임워크에게 넘긴다"는 아이디어로 해결합니다. 이 아티클에서는 전통적 방식과 IoC 방식을 인터랙티브 시각화로 비교하며 차이를 직관적으로 이해해 봅시다.
1. 제어의 역전 — 무엇이 역전되는가
꼬리질문: "제어의 역전(IoC)이라는 말에서 구체적으로 무엇이 역전된다는 건가요?"
IoC(Inversion of Control, 제어의 역전)는 프로그램의 제어 흐름을 개발자에서 프레임워크로 넘기는 설계 원칙입니다. 전통적 방식에서는 내 코드가 라이브러리를 호출하지만, IoC 방식에서는 프레임워크가 내 코드를 호출합니다.
"Don't call us, we'll call you"라는 할리우드 원칙으로 비유됩니다. 배우(개발자)는 자신의 연기(비즈니스 로직)만 준비하고, 언제 출연할지는 감독(프레임워크)이 결정합니다.
아래 시각화에서 전통적 방식과 IoC 방식이 동일한 UserService 생성 시나리오를 어떻게 다르게 처리하는지 확인해보세요.
public static void main(String[] args)DataSource ds = new DataSource()UserRepository repo = new UserRepository(ds)UserService svc = new UserService(repo)svc.createUser(...)개발자가 생성 순서와 연결을 직접 제어
SpringApplication.run(App.class, args)ApplicationContext ctx 생성@ComponentScan → BeanDefinition 등록DataSource → UserRepository → UserService컨테이너가 순서와 연결을 자동 관리
제어가 역전되는 3가지 항목
| 항목 | 전통적 방식 | IoC 방식 |
|---|---|---|
| 객체 생성 | new 직접 호출 | 컨테이너 자동 생성 |
| 의존성 연결 | 개발자가 직접 조립 | 자동 주입 (@Autowired) |
| 호출 흐름 | 내 코드 → 라이브러리 | 프레임워크 → 내 코드 |
2. IoC vs DI — 원칙과 구현 방법의 차이
꼬리질문: "IoC와 DI(의존성 주입)는 같은 개념인가요, 다른 개념인가요?"
IoC는 "무엇을 할지"에 대한 설계 원칙이고, DI(의존성 주입)는 "어떻게 구현할지"의 구체적 방법입니다. Martin Fowler는 IoC라는 용어가 너무 범용적이어서 더 구체적인 이름인 "Dependency Injection"을 제안했습니다.
IoC를 구현하는 방법에는 DI 외에도 Service Locator 패턴, Template Method 패턴 등이 있지만, Spring은 DI를 핵심 메커니즘으로 채택하고 있습니다.
각 영역을 클릭하면 IoC와 DI의 관계를 상세히 확인할 수 있습니다.
IoC
설계 원칙
Service Locator
Template Method
Strategy 패턴
DI
구현 패턴
Spring 핵심
↑ 다이어그램의 원을 클릭하면
상세 설명이 표시됩니다
“IoC라는 이름은 너무 일반적이어서 DI라는 더 구체적인 이름을 제안했다.” — Martin Fowler
3. IoC 컨테이너의 3가지 핵심 역할
꼬리질문: "Spring IoC 컨테이너는 구체적으로 어떤 일을 하나요?"
Spring IoC 컨테이너는 객체 생성(Instantiation), 의존성 주입(Dependency Injection), 생명주기 관리(Lifecycle Management) 세 가지 역할을 수행합니다.
@Service, @Repository 같은 어노테이션이 붙은 클래스를 스캔해서 Bean으로 등록하고, 생성자에 선언된 의존성을 분석해 자동으로 주입하며,@PostConstruct부터 @PreDestroy까지 전체 생명주기를 관리합니다.
컨테이너가 의존성 그래프를 자동으로 분석하기 때문에, OrderService → UserService → UserRepository → DataSource와 같은 복잡한 의존 관계도 올바른 순서로 생성합니다. 개발자는 생성 순서를 신경 쓸 필요가 없습니다.
"전체 프로세스 자동 재생" 버튼을 클릭하거나 각 카드를 클릭해 실제 코드 예시를 확인하세요.
IoC Container
@Component·@Service·@Repository 스캔
생성자 파라미터 분석 → Bean 탐색 → 자동 주입
@PostConstruct 초기화 → 사용 → @PreDestroy 정리
각 카드를 클릭하면 코드 예시가 펼쳐집니다
4. 의존성 주입 3가지 방식 비교
꼬리질문: "의존성 주입 방식 중 생성자 주입을 권장하는 이유는 무엇인가요?"
Spring은 생성자 주입, 세터 주입, 필드 주입 세 가지 DI 방식을 지원하지만, 생성자 주입이 압도적으로 권장됩니다.
생성자 주입은 final 필드로 불변성을 보장하고, 생성자 파라미터로 필수 의존성을 명시하며, Spring 없이도 new Service(mockRepo) 형태로 단위 테스트를 작성할 수 있습니다. 필드 주입은 코드가 간결해 보이지만 테스트하기 어렵고 순환 의존성을 숨깁니다.
Spring 4.3 이후 생성자가 하나뿐이면 @Autowired를 생략할 수 있습니다. Lombok @RequiredArgsConstructor를 사용하면 final 필드에 대한 생성자를 자동 생성해 코드를 더욱 간결하게 만들 수 있습니다.
탭을 클릭해 각 DI 방식의 코드 예시와 장단점을 비교해보세요.
코드 예시
@Service
public class UserService {
private final UserRepository repo;
// Spring 4.3+ 단일 생성자는 @Autowired 생략 가능
public UserService(UserRepository repo) {
this.repo = repo;
}
}장점
- ✓final 사용 가능 → 불변성 보장
- ✓필수 의존성 명시적 표현
- ✓Spring 없이 new Service(mockRepo)로 테스트
- ✓순환 의존성 시작 시점에 즉시 감지
- ✓NPE 원천 방지
단점
- ✗의존성 많으면 생성자 복잡 (Lombok @RequiredArgsConstructor로 해결)
💡 Spring 4.3+ 단일 생성자는 @Autowired 생략 가능 • Lombok @RequiredArgsConstructor로 생성자 자동 생성
5. BeanFactory vs ApplicationContext
꼬리질문: "BeanFactory와 ApplicationContext의 차이는 무엇인가요?"
Spring IoC 컨테이너는 두 가지 레벨이 있습니다. BeanFactory는 기본적인 DI 기능만 제공하는 최상위 인터페이스이고, ApplicationContext는 BeanFactory를 확장하여 엔터프라이즈 기능을 추가한 고급 컨테이너입니다.
실무에서는 거의 항상 ApplicationContext를 사용합니다. Lazy Loading 대신 Eager Loading으로 시작 시점에 Singleton Bean을 미리 생성해 런타임 오류를 조기 발견하고, 국제화(i18n), 이벤트 발행, AOP 통합, 환경 설정(@Profile) 등을 추가로 지원합니다.
계층 다이어그램에서 호버하거나 비교 테이블을 확인해보세요.
ApplicationContext
엔터프라이즈 기능 포함
BeanFactory
기본 DI 기능
마우스를 올려보세요
| 기능 | BeanFactory | ApplicationContext |
|---|---|---|
| 로딩 방식 | Lazy Loading — getBean() 시 생성 | Eager Loading — 시작 시 Singleton 미리 생성 |
| 국제화(i18n) | ✗ 미지원 | ✓ MessageSource로 다국어 메시지 |
| 이벤트 발행 | ✗ 미지원 | ✓ ApplicationEventPublisher |
| AOP 통합 | 수동 설정 필요 | ✓ @Transactional 등 자동 프록시 |
| 환경 설정 | ✗ 미지원 | ✓ @Profile, @PropertySource |
| 사용 대상 | IoT·임베디드(리소스 제한) | 일반 엔터프라이즈 앱 (실무 표준) |
| 대표 사용법 | new DefaultListableBeanFactory() | SpringApplication.run() |
💡 실무에서는 99% ApplicationContext 사용. Spring Boot에서 SpringApplication.run()이 자동으로 ApplicationContext를 생성합니다.
6. Bean 스코프 — Singleton과 동시성 문제
꼬리질문: "Singleton Bean 사용 시 반드시 무상태로 설계해야 하는 이유는 무엇인가요?"
Bean의 기본 스코프는 Singleton입니다. 컨테이너당 인스턴스가 1개만 생성되어 모든 요청에서 공유됩니다. Prototype 스코프는 getBean() 호출 시마다 새 인스턴스를 생성합니다.
Singleton Bean은 반드시 무상태(Stateless)로 설계해야 합니다. 인스턴스 변수에 요청별 데이터를 저장하면 여러 스레드가 같은 변수를 동시에 수정해 Race Condition이 발생합니다. 상태가 필요하면 메서드 파라미터로 전달하거나 Request/Prototype 스코프를 사용해야 합니다.
Prototype Bean은 컨테이너가 소멸을 관리하지 않아 @PreDestroy가 호출되지 않습니다. DB 커넥션 같은 자원을 사용한다면 직접 해제해야 합니다.
"요청 3개 동시 보내기" 버튼으로 Singleton과 Prototype의 차이를 직접 체험해보세요.
UserService Bean
ID: #1234
기본 스코프 (@Service, @Repository)
요청 시 각각 새 인스턴스 생성
@Scope("prototype") 필요
| 스코프 | 인스턴스 수 | 생명주기 | 주 사용처 |
|---|---|---|---|
| Singleton | 컨테이너당 1개 | 전체 생명주기 | 무상태 서비스 |
| Prototype | 요청마다 새 인스턴스 | 컨테이너 관리 안 함 | 상태 있는 객체 |
| Request | HTTP 요청당 1개 | 요청 시작~종료 | 요청별 로깅 |
| Session | HTTP 세션당 1개 | 세션 시작~만료 | 로그인 사용자 정보 |
7. 컨테이너 초기화 과정 — Bean은 어떻게 생성되나
꼬리질문: "Spring 컨테이너가 시작될 때 내부적으로 어떤 순서로 초기화가 이뤄지나요?"
ApplicationContext가 시작될 때 내부에서 BeanDefinition 로드 → BeanFactoryPostProcessor 실행 → Bean 인스턴스화 → 의존성 주입 → BeanPostProcessor 실행 → 컨테이너 준비 완료 순서로 6단계가 진행됩니다.
이 과정을 이해하면 "왜 @PostConstruct가 생성자보다 늦게 실행되는지", "AOP 프록시는 어느 시점에 생성되는지" 같은 심화 면접 질문에 답할 수 있습니다.
의존성 그래프가 복잡해도 컨테이너가 자동으로 순서를 계산합니다. OrderService → UserService → UserRepository → DataSource 순서로 의존한다면, DataSource부터 생성한 뒤 역순으로 조립합니다.
"자동 재생" 또는 "단계별 진행" 모드로 초기화 흐름을 직접 따라가 보세요.
1. BeanDefinition 로드
@ComponentScan으로 @Component 클래스 탐색 + @Configuration의 @Bean 메서드 파싱
2. BeanFactoryPostProcessor 실행
BeanDefinition 수정 기회. ${} 플레이스홀더를 실제 값으로 치환
3. Bean 인스턴스화
Singleton만 Eager Loading — 지금 생성. 생성자 호출·리플렉션으로 인스턴스 생성
4. 의존성 주입
생성자 주입: 인스턴스화와 동시에 주입. 세터·필드 주입: 인스턴스화 후 별도 주입
5. BeanPostProcessor 실행
@PostConstruct 실행 + AOP 프록시 생성 (@Transactional 등)
6. 컨테이너 준비 완료
ContextRefreshedEvent 발행 — 애플리케이션 사용 가능 상태
💡 각 단계를 클릭하면 상세 동작과 로그 예시를 볼 수 있습니다
자주 발생하는 문제
실무에서 자주 마주치는 IoC/DI 관련 오류와 해결 방법입니다.
순환 의존성 — ServiceA와 ServiceB가 서로를 의존
상황: 두 Bean이 서로를 의존하면 BeanCurrentlyInCreationException 발생
@Service
public class ServiceA {
public ServiceA(ServiceB serviceB) { ... }
}
@Service
public class ServiceB {
public ServiceB(ServiceA serviceA) { ... }
}원인: ServiceA 생성 시 ServiceB 필요 → ServiceB 생성 시 ServiceA 필요 → 무한 루프. Spring Boot 2.6+에서는 기본적으로 시작 자체를 실패시킵니다.
해결:
- 1. 설계 리팩토링(권장): ServiceA와 ServiceB가 공유하는 로직을 ServiceC로 분리해 단방향 의존으로 전환
- 2. @Lazy 사용: 한쪽 생성자 파라미터에
@Lazy붙여 프록시 주입으로 순환 회피 - 3. 세터 주입: 객체 생성 후 주입하여 우회 (비권장)
NoSuchBeanDefinitionException — 필요한 Bean을 찾을 수 없음
상황: No qualifying bean of type 'UserRepository' available 에러 발생
원인과 해결:
- - 어노테이션 누락: 클래스에
@Repository또는@Component추가 - - 컴포넌트 스캔 범위 밖:
@ComponentScan(basePackages = {"com.example", "com.other"})범위 지정 - - 동일 타입 Bean 여러 개:
@Primary또는@Qualifier("beanName")로 명시적 지정
Singleton Bean의 동시성 문제 — 인스턴스 변수에 요청별 데이터 저장
상황: Singleton Bean에 인스턴스 변수로 요청별 데이터를 저장
@Service
public class PriceCalculator {
private double discount; // 위험: 모든 스레드가 공유
public void setDiscount(double d) { this.discount = d; }
public double calculate(double price) { return price * (1 - discount); }
}원인: Thread A가 discount=0.1 설정 → Thread B가 discount=0.2로 덮어씀 → Thread A의 계산이 잘못된 할인율 적용
해결:
- 1. 무상태로 설계(권장):
public double calculate(double price, double discount)— 상태를 파라미터로 전달 - 2. ThreadLocal 사용: 스레드별로 데이터를 격리 (단, 요청 종료 후
remove()필수) - 3. Prototype 또는 Request 스코프: 요청마다 인스턴스 분리
마무리
- - IoC의 핵심: 제어 흐름을 프레임워크에게 넘겨 결합도 감소, 테스트 용이성 증가
- - IoC ≠ DI: IoC는 설계 원칙(What), DI는 그 구현 방법(How) — Spring은 DI로 IoC를 구현
- - 생성자 주입 권장: 불변성·필수 의존성 명시·NPE 방지·순환 의존성 조기 발견·테스트 용이
- - ApplicationContext: BeanFactory + 엔터프라이즈 기능(국제화, 이벤트, AOP, Profile) — 실무 표준
- - Singleton Bean: 반드시 무상태로 설계, 상태 필요 시 파라미터 전달 또는 다른 스코프 선택
- - 순환 의존성: 설계 리팩토링이 최선, @Lazy는 임시 해결책
더 알아볼 주제: Spring Bean 라이프사이클 콜백, BeanPostProcessor와 AOP 연결, @Lazy와 지연 초기화
생성자 주입 vs 필드 주입
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
public OrderService() {
paymentService.charge(100); // 이 코드는 어떻게 될까요?
}
}싱글톤(Singleton) Bean의 동시성
@Service
public class UserService {
private User currentUser;
public void setCurrentUser(User user) { this.currentUser = user; }
public User getCurrentUser() { return currentUser; }
}
// 10명의 사용자가 동시에 로그인했을 때 어떤 일이 발생할까요?BeanFactory vs ApplicationContext
// Spring Boot 애플리케이션 시작 시 DB 커넥션 풀 초기화 실패가 발생했습니다. // 그런데 애플리케이션은 정상적으로 시작되었고, // 첫 번째 API 요청 시점에 에러가 발생했습니다. // 이런 상황이 발생할 수 있는 경우는?
순환 의존성(Circular Dependency) 해결
@Service
public class OrderService {
public OrderService(UserService userService) { ... }
}
@Service
public class UserService {
public UserService(OrderService orderService) { ... }
}
// BeanCurrentlyInCreationException으로 시작 실패!
// 가장 좋은 해결 방법은?참고 자료
- Martin Fowler — Inversion of Control Containers and the Dependency Injection pattern — IoC와 DI 개념을 처음 정리한 고전 아티클. "왜 DI라는 이름을 제안했는지" 배경과 주입 방식 비교까지 담겨 있습니다.
- Baeldung — Inversion of Control and Dependency Injection with Spring — Spring에서 IoC와 DI를 어떻게 구현하는지 예제 코드와 함께 단계별로 설명하는 튜토리얼입니다.
- Baeldung — Spring Bean Scopes — Singleton, Prototype, Request, Session 등 Bean 스코프를 실전 예제로 설명합니다.
- Baeldung — Circular Dependencies in Spring — 순환 의존성의 원인과 해결 방법(@Lazy, 세터 주입, 설계 리팩토링)을 코드와 함께 상세히 설명합니다.
- Baeldung — How Does the Spring Singleton Bean Serve Concurrent Requests? — Singleton Bean이 동시 요청을 어떻게 처리하는지, 동시성 문제를 어떻게 예방하는지 설명합니다.
- HowToDoInJava — Spring IoC Container: BeanFactory and ApplicationContext — BeanFactory와 ApplicationContext의 차이를 계층 구조 그림과 함께 쉽게 설명합니다.
의견을 들려주세요
서비스 개선에 큰 도움이 됩니다. 익명으로 자유롭게 남겨주세요.