Spring

IoC 컨테이너에 대해서 설명해주세요

Spring의 심장인 IoC 컨테이너가 어떻게 객체를 생성하고 의존성을 주입하며 생명주기를 관리하는지. 전통적인 객체 생성과의 차이부터 DI 방식, Bean 스코프, 컨테이너 초기화 과정까지 인터랙티브 시각화로 완전 정복합니다.

2026-03-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 애플리케이션의 동시성 버그를 예방할 수 있습니다.

면접관이 정말 듣고 싶은 건 "IoC가 제어의 역전이다"라는 정의가 아니라, 왜 Spring이 이 방식을 선택했고 실무에서 어떤 문제를 해결하는지입니다.

직접 new로 객체를 만들면 간단해 보이지만, 프로젝트 규모가 커지면 의존성 그래프가 복잡해지고 테스트마다 Mock을 주입하기 어려워집니다. IoC 컨테이너는 이 모든 문제를 "객체 생성 권한을 프레임워크에게 넘긴다"는 아이디어로 해결합니다. 이 아티클에서는 전통적 방식과 IoC 방식을 인터랙티브 시각화로 비교하며 차이를 직관적으로 이해해 봅시다.


1. 제어의 역전 — 무엇이 역전되는가

꼬리질문: “제어의 역전(IoC)이라는 말에서 구체적으로 무엇이 역전된다는 건가요?”

IoC(Inversion of Control, 제어의 역전)는 프로그램의 제어 흐름을 개발자에서 프레임워크로 넘기는 설계 원칙입니다. 전통적 방식에서는 내 코드가 라이브러리를 호출하지만, IoC 방식에서는 프레임워크가 내 코드를 호출합니다.

"Don't call us, we'll call you"라는 할리우드 원칙으로 비유됩니다. 배우(개발자)는 자신의 연기(비즈니스 로직)만 준비하고, 언제 출연할지는 감독(프레임워크)이 결정합니다.

아래 시각화에서 "단계별 실행" 버튼을 눌러 전통적 방식과 IoC 방식을 비교해 보세요.

제어 흐름 비교 — UserService 생성 시나리오
전통적 방식개발자 제어
1main() 시작
public static void main(String[] args)
2new DataSource()
DataSource ds = new DataSource()
3new UserRepository(ds)
UserRepository repo = new UserRepository(ds)
4new UserService(repo)
UserService svc = new UserService(repo)
5userService.createUser() 호출
svc.createUser(...)

개발자가 생성 순서와 연결을 직접 제어

IoC 방식컨테이너 자동 관리
1main() → SpringApplication.run()
SpringApplication.run(App.class, args)
2IoC Container 초기화
ApplicationContext ctx 생성
3@Component 스캔 + 의존성 그래프 분석
@ComponentScan → BeanDefinition 등록
4Bean 생성 및 의존성 자동 주입
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 패턴, Strategy 패턴 등이 있지만, Spring은 DI를 핵심 메커니즘으로 채택하고 있습니다.

아래 다이어그램에서 각 영역을 클릭해 IoC와 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) 세 가지 역할을 수행합니다.

컨테이너가 의존성 그래프를 자동으로 분석하기 때문에, OrderService → UserService → UserRepository → DataSource와 같은 복잡한 의존 관계도 올바른 순서로 생성합니다. 개발자는 생성 순서를 신경 쓸 필요가 없습니다.

각 카드를 클릭하거나 "자동 재생" 버튼을 눌러 컨테이너의 역할을 순서대로 확인해 보세요.

IoC 컨테이너의 3가지 핵심 역할

IoC Container

🏭1객체 생성 (Instantiation)

@Component·@Service·@Repository 스캔

🔗2의존성 주입 (Dependency Injection)

생성자 파라미터 분석 → Bean 탐색 → 자동 주입

⏱️3생명주기 관리 (Lifecycle Management)

@PostConstruct 초기화 → 사용 → @PreDestroy 정리

각 카드를 클릭하면 코드 예시가 펼쳐집니다


4. 의존성 주입 3가지 방식 비교

꼬리질문: “의존성 주입 방식 중 생성자 주입을 권장하는 이유는 무엇인가요?”

Spring은 생성자 주입, 세터 주입, 필드 주입 세 가지 DI 방식을 지원하지만, 생성자 주입이 압도적으로 권장됩니다.

생성자 주입은 final 필드로 불변성을 보장하고, 생성자 파라미터로 필수 의존성을 명시하며, Spring 없이도 new Service(mockRepo) 형태로 단위 테스트를 작성할 수 있습니다. 필드 주입은 코드가 간결해 보이지만 테스트하기 어렵고 순환 의존성을 숨깁니다.

각 탭을 클릭해 3가지 주입 방식의 코드와 장단점을 비교해 보세요.

의존성 주입 3가지 방식 비교

코드 예시

@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를 사용합니다. Eager Loading으로 시작 시점에 Singleton Bean을 미리 생성해 런타임 오류를 조기 발견하고, 국제화(i18n), 이벤트 발행, AOP 통합, 환경 설정(@Profile) 등을 추가로 지원합니다.

계층 다이어그램에 마우스를 올려 ApplicationContext가 추가하는 기능을 확인해 보세요.

BeanFactory vs ApplicationContext — 계층 구조 & 기능 비교

ApplicationContext

엔터프라이즈 기능 포함

MessageSourceApplicationEventPublisher@Profile 지원AOP 자동 프록시

BeanFactory

기본 DI 기능

Bean 생성Bean 조회DI 지원

마우스를 올려보세요

기능BeanFactoryApplicationContext
로딩 방식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 스코프를 사용해야 합니다.

"요청 3개 동시 보내기" 버튼을 눌러 Singleton과 Prototype의 차이를 시뮬레이션해 보세요.

Bean 스코프 시뮬레이터
Singleton컨테이너당 1개

UserService Bean

ID: #1234

기본 스코프 (@Service, @Repository)

Prototype요청마다 신규 생성

요청 시 각각 새 인스턴스 생성

@Scope("prototype") 필요

스코프인스턴스 수생명주기주 사용처
Singleton컨테이너당 1개전체 생명주기무상태 서비스
Prototype요청마다 새 인스턴스컨테이너 관리 안 함상태 있는 객체
RequestHTTP 요청당 1개요청 시작~종료요청별 로깅
SessionHTTP 세션당 1개세션 시작~만료로그인 사용자 정보

7. 컨테이너 초기화 과정 — Bean은 어떻게 생성되나

꼬리질문: “Spring 컨테이너가 시작될 때 내부에서 어떤 일이 일어나나요?”

ApplicationContext가 시작될 때 내부에서 BeanDefinition 로드 → BeanFactoryPostProcessor 실행 → Bean 인스턴스화 → 의존성 주입 → BeanPostProcessor 실행 → 컨테이너 준비 완료순서로 6단계가 진행됩니다.

이 과정을 이해하면 "왜 @PostConstruct가 생성자보다 늦게 실행되는지", "AOP 프록시는 어느 시점에 생성되는지" 같은 심화 면접 질문에 답할 수 있습니다.

"자동 재생" 또는 "단계별 진행" 버튼으로 초기화 과정을 직접 따라가 보세요.

컨테이너 초기화 6단계
📄1

1. BeanDefinition 로드

@ComponentScan으로 @Component 클래스 탐색 + @Configuration의 @Bean 메서드 파싱

⚙️2

2. BeanFactoryPostProcessor 실행

BeanDefinition 수정 기회. ${} 플레이스홀더를 실제 값으로 치환

🏗️3

3. Bean 인스턴스화

Singleton만 Eager Loading — 지금 생성. 생성자 호출·리플렉션으로 인스턴스 생성

🔗4

4. 의존성 주입

생성자 주입: 인스턴스화와 동시에 주입. 세터·필드 주입: 인스턴스화 후 별도 주입

🔧5

5. BeanPostProcessor 실행

@PostConstruct 실행 + AOP 프록시 생성 (@Transactional 등)

6

6. 컨테이너 준비 완료

ContextRefreshedEvent 발행 — 애플리케이션 사용 가능 상태

💡 각 단계를 클릭하면 상세 동작과 로그 예시를 볼 수 있습니다


자주 발생하는 문제

실무에서 IoC 컨테이너 관련 자주 만나는 예외와 해결책

순환 의존성 — ServiceA와 ServiceB가 서로를 의존

상황: 두 Bean이 서로를 의존하면 BeanCurrentlyInCreationException 발생

@Service public class ServiceA { public ServiceA(ServiceB b) { ... } }
@Service public class ServiceB { public ServiceB(ServiceA a) { ... } }

원인: 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 = ...) 범위 지정
  • 동일 타입 Bean 여러 개: @Primary 또는 @Qualifier로 명시적 지정
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. 무상태로 설계 (권장): calculate(double price, double discount) — 상태를 파라미터로 전달
  2. ThreadLocal 사용: 스레드별로 데이터를 격리 (단, 요청 종료 후 remove() 필수)
  3. Prototype 또는 Request 스코프: 요청마다 인스턴스 분리

면접 체크리스트

이것만 확실히 이해하면 IoC 컨테이너 질문은 완벽하게 답할 수 있습니다

  • - IoC의 핵심: 제어 흐름을 프레임워크에게 넘겨 결합도 감소, 테스트 용이성 증가
  • - IoC ≠ DI: IoC는 설계 원칙(What), DI는 그 구현 방법(How) — Spring은 DI로 IoC를 구현
  • - 생성자 주입 권장: 불변성·필수 의존성 명시·NPE 방지·순환 의존성 조기 발견·테스트 용이
  • - ApplicationContext: BeanFactory + 엔터프라이즈 기능(국제화, 이벤트, AOP, Profile) — 실무 표준
  • - Singleton Bean: 반드시 무상태로 설계, 상태 필요 시 파라미터 전달 또는 다른 스코프 선택
  • - 순환 의존성: 설계 리팩토링이 최선, @Lazy는 임시 해결책
  • - 더 알아볼 주제: Spring Bean 라이프사이클 콜백, BeanPostProcessor와 AOP 연결, @Lazy와 지연 초기화

실전 퀴즈

시나리오 기반 문제로 IoC 컨테이너 이해도를 점검해 보세요

1 / 5현재 점수: 0

케이스 1

생성자에서 의존성을 사용할 수 있을까?

@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService;

    public OrderService() {
        paymentService.charge(100); // 어떻게 될까?
    }
}

참고 자료


의견을 들려주세요

서비스 개선에 큰 도움이 됩니다. 익명으로 자유롭게 남겨주세요.

0 / 1000