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 생성 시나리오를 어떻게 다르게 처리하는지 확인해보세요.

제어 흐름 비교 — 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 패턴 등이 있지만, 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) 세 가지 역할을 수행합니다.

@Service, @Repository 같은 어노테이션이 붙은 클래스를 스캔해서 Bean으로 등록하고, 생성자에 선언된 의존성을 분석해 자동으로 주입하며,@PostConstruct부터 @PreDestroy까지 전체 생명주기를 관리합니다.

컨테이너가 의존성 그래프를 자동으로 분석하기 때문에, 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) 형태로 단위 테스트를 작성할 수 있습니다. 필드 주입은 코드가 간결해 보이지만 테스트하기 어렵고 순환 의존성을 숨깁니다.

Spring 4.3 이후 생성자가 하나뿐이면 @Autowired를 생략할 수 있습니다. Lombok @RequiredArgsConstructor를 사용하면 final 필드에 대한 생성자를 자동 생성해 코드를 더욱 간결하게 만들 수 있습니다.

탭을 클릭해 각 DI 방식의 코드 예시와 장단점을 비교해보세요.

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

계층 다이어그램에서 호버하거나 비교 테이블을 확인해보세요.

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 스코프를 사용해야 합니다.

Prototype Bean은 컨테이너가 소멸을 관리하지 않아 @PreDestroy가 호출되지 않습니다. DB 커넥션 같은 자원을 사용한다면 직접 해제해야 합니다.

"요청 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 프록시는 어느 시점에 생성되는지" 같은 심화 면접 질문에 답할 수 있습니다.

의존성 그래프가 복잡해도 컨테이너가 자동으로 순서를 계산합니다. OrderService → UserService → UserRepository → DataSource 순서로 의존한다면, DataSource부터 생성한 뒤 역순으로 조립합니다.

"자동 재생" 또는 "단계별 진행" 모드로 초기화 흐름을 직접 따라가 보세요.

컨테이너 초기화 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/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와 지연 초기화


퀴즈 1

생성자 주입 vs 필드 주입

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

    public OrderService() {
        paymentService.charge(100);  // 이 코드는 어떻게 될까요?
    }
}
퀴즈 2

싱글톤(Singleton) Bean의 동시성

@Service
public class UserService {
    private User currentUser;

    public void setCurrentUser(User user) { this.currentUser = user; }
    public User getCurrentUser() { return currentUser; }
}

// 10명의 사용자가 동시에 로그인했을 때 어떤 일이 발생할까요?
퀴즈 3

BeanFactory vs ApplicationContext

// Spring Boot 애플리케이션 시작 시 DB 커넥션 풀 초기화 실패가 발생했습니다.
// 그런데 애플리케이션은 정상적으로 시작되었고,
// 첫 번째 API 요청 시점에 에러가 발생했습니다.
// 이런 상황이 발생할 수 있는 경우는?
퀴즈 4

순환 의존성(Circular Dependency) 해결

@Service
public class OrderService {
    public OrderService(UserService userService) { ... }
}

@Service
public class UserService {
    public UserService(OrderService orderService) { ... }
}

// BeanCurrentlyInCreationException으로 시작 실패!
// 가장 좋은 해결 방법은?

참고 자료


의견을 들려주세요

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

0 / 1000