Spring IoC 컨테이너란?
객체 생성,의존성 주입,생명주기 관리를 개발자 대신 수행하는 Spring의 핵심엔진입니다
전통적인 객체 생성 방식과 어떤 차이가 있는지?
전통적인 방식에서는 개발자가 직접 new로 객체를 생성하고 의존성을 연결했습니다
IoC 방식에서는 컨테이너가 @Component,@Service 같은 어노테이션을 스캔해서 Bean을 자동생성하고 필요한곳에 주입합니다 이것이 제어의 역전입니다
제어의 역전 → 무엇이 역전되는가?
loc(Inversion of Control,제어의 역전)는 프로그램의 제어흐름을 개발자에서 프레임워크로 넘기는 설계원칙이다
전통적 방식 → 내 코드가 라이브러리를 호출
IoC 방식 → 프레임워크가 내 코드를 호출
제어가 역전 되는 3가지항목
| 전통적 방식 | IoC 방식 | |
| 객체생성 | new 직접 호출 | 컨테이너 자동 생성 |
| 의존성 연결 | 개발자가 직접 조립 | 자동주입(@Autowired) |
| 호출흐름 | 내 코드 → 라이브러리 | 프레임워크 → 내 코드 |
IoC VS DI 원칙과 구현 방법 차이
IoC → 무엇을 할지에 대한 설계원칙
DI(의존성 주입) → 어떻게 구현할지에 대한 구체척 방법
IoC 컨테이너의 3가지 핵심역할
객체 생성
@Service
public class UserService {
// 개발자는 new UserService() 하지 않음
// 컨테이너가 자동으로 인스턴스 생성
}
의존성 주입
@Service
public class UserService {
private final UserRepository userRepository;
// 컨테이너가 UserRepository Bean을 찾아 자동 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
생명주기 및 관리
@Service
public class CacheService {
@PostConstruct
void init() {
// 컨테이너 시작 시 자동 호출
// DB 커넥션 풀 초기화
}
@PreDestroy
void cleanup() {
// 컨테이너 종료 시 자동 호출
// 자원 해제
}
}
의존성 주입 3가지 방식 비교
| 생성자주입(권장) | 세터주입(선택적) | 필드주입(비권장) | |
| 장점 | 1.final 사용가능 → 불변성 보장 필수 의존성 명시적 표현 2. Spring 없이 new Service로 테스트 3. 순환 의존성 시작시점에 즉시 감지 4. NPE 원천 방지 |
1. 선택적 의존성 2. 나중에 의존성 변경 가능함 |
코드가 짧고 간결해짐 |
| 단점 | 의존성 많으면 생성자 복잡 (lombok @ReQuiredArgsConstructor로 해결 가능 ) |
1. final 사용 불가 2. 주입전 NPE 위험 3. 순환 의존성 숨김 |
1. new로 생성시 repo = null 이라서 테스트 불편함 2. final 사용불가 3. DI 컨테이너 없으면 동작 불가 4. 의존성이 외부에서 보이지 않음 |
코드 예시
생성자 주입
@Service
public class UserService {
private final UserRepository repo;
// Spring 4.3+ 단일 생성자는 @Autowired 생략 가능
public UserService(UserRepository repo) {
this.repo = repo;
}
}
세터주입
@Service
public class UserService {
private UserRepository repo; // final 불가
@Autowired
public void setRepo(UserRepository repo) {
this.repo = repo;
}
}
필드주입
@Service
public class UserService {
@Autowired
private UserRepository repo; // final 불가
// 생성자 없음 — 간결함
}
BeanFactory VS ApplicationContext
BeanFactory → 기본적인 의존성 주입(DI) 기능만 제공하는 최상위 인터페이스
ApplicationContext → BeanFactory 확장,엔터프라이즈 기능을 추가한 고급 컨테이너
실무에서는 ApplicationContext를 사용함 시작시점에 싱글톤 빈을 미리 생성해서 런타임오류를 조기 발견하고 국제화,이벤트 발생,AOP통합, 환경설정 등을 추가로 지원한다
Bean 스코프 Singleton과 동시성 문제
Bean은 기본적인 스코프가 싱글톤이다 컨테이너당 인스턴스가 1개만 생성되서 모든요청에서 공유된다
프로토타입 스코프는 getBean을 호출할때 마다 새 인스턴스를 만든다
싱글톤 빈은 무상태로 설계해야한다 인스턴스 변수에 요청별 데이터를 저장하면 여러 스레드가 같은 변수를 동시에 수정해서 Race Condition이 발생한다 따라서 상태 필요시 메서드 파라미터로 전달하거나 리퀘스트나 프로토타입스코프를 사용해야한다
동시성 버그 예시
❌ 잘못된 예 (상태 있는 Singleton)
@Service
public class PriceService {
private double discount; // 위험!
// Thread A → setDiscount(0.1)
// Thread B → setDiscount(0.2) 덮어씀
// Thread A → discount가 0.2 → 버그!
}
⚠ Race Condition → 데이터 오염
✅ 올바른 예 (무상태 설계)
@Service
public class PriceService {
// 상태 없음 — 모든 값을 파라미터로 전달
public double calculate(
double price,
double discount // 파라미터로 전달
) {
return price * (1 - discount);
}
}
✓ 스레드 안전 — 상태 없음
| 스코프 | 인스턴스수 | 생명주기 | 주 사용처 |
| Singleton | 컨테이너당 1개 | 전체 생명주기 | 무상태 서비스 |
| Prototype | 요청마다 새 인스턴스 | 컨테이너 관리 안 함 | 상태 있는 객체 |
| Request | HTTP 요청당 1개 | 요청 시작~종료 | 요청별 로깅 |
| Session | HTTP 세션당 1개 | 세션 시작~만료 | 로그인 사용자 정보 |
컨테이너 초기화 과정 - Bean 생성하기
BeanDefinition 로드 → BeanFactoryPostProcessor 실행 → Bean 인스턴스화 →
의존성 주입 → BeanPostProcessor 실행 → 컨테이너 준비 완료
자주발생하는 문제
두개의 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+에서는 시작 자체를 실패시킴
해결:
- 설계리팩토링(권장) → ServiceA와 ServiceB가 공유하는 로직을 ServiceC로 분리해 단방향 의존으로 전환
- @Lazy 사용 → 한쪽 생성자 파라미터에 @Lazy 붙여 프록시 주입으로 순환 회피
- 세터 주입(비권장) → 객체 생성 후 주입하여 우회
NoSuchBeanDefinitionException — 필요한 Bean을 찾을 수 없음
원인과 해결
- 어노테이션 누락 → 클래스에 @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의 계산이 잘못된 할인율 적용함
해결
- 무상태로 설계 → calculate(double price, double discount) 상태를 파라미터로 전달
- ThreadLocal 사용 → 스레드별로 데이터를 격리 (단, 요청 종료 후 remove() 필수)
- Prototype/Request 스코프 → 요청마다 인스턴스 분리
'🐢 꼬부기 LV.1 | 개념•기초 > 💧물대포(핵심개념)' 카테고리의 다른 글
| HTTP 메서드의 종류와 특징 (0) | 2026.03.26 |
|---|---|
| Spring Bean 등록 방법 4가지 (0) | 2026.03.25 |
| 리액트 설치후 실행에러 해결하기 (0) | 2026.03.23 |
| FileNotFoundError,NameError,PermissionError 해결하기 (0) | 2026.03.22 |
| 파이썬 데이터 분석 토이프로젝트 - 전기차 충전소 지역별 검색하기 (0) | 2026.03.21 |