2. Spring Bean 생명주기와 Scope

Spring Bean은 컨테이너가 만들고 관리하는 객체입니다. "언제 만들어지고, 어떻게 초기화되고, 언제 사라지는가" — 이 흐름을 모르면 DB 커넥션 풀 초기화 실패나 리소스 누수 같은 장애를 디버깅하기 어렵습니다. Scope는 Bean의 수명과 공유 범위를 결정합니다.

1. Bean 생명주기 전체 흐름

① Spring이: 클래스 탐색
@ComponentScan으로 @Component 계열 클래스 탐색 / @Bean 메서드 파싱
② Spring이: 인스턴스 생성
기본 생성자 또는 지정된 생성자로 객체 생성 (Reflection API 사용)
③ Spring이: 의존성 주입
생성자/세터/필드 주입 수행. 생성자 주입은 ②와 동시에 이뤄짐
④ 개발자: 초기화
@PostConstruct 메서드 실행 — 모든 의존성 주입 완료 후 실행됨. 커넥션 풀 초기화, 캐시 워밍 등
⑤ 사용 중
ApplicationContext에서 Bean을 꺼내 사용하는 단계 (대부분의 애플리케이션 동작)
⑥ 개발자: 소멸 전 처리
@PreDestroy 메서드 실행 — 컨테이너 종료 전. DB 커넥션 해제, 스케줄러 종료 등
⑦ Spring이: Bean 소멸
GC가 메모리 회수. Prototype Scope는 이 단계를 Spring이 관리하지 않음
@PostConstruct — 초기화
@Component
public class CacheWarmupService {
    private final RedisTemplate<String, Object> redis;
    private Map<String, Item> localCache;

    // 생성자 주입 (redis 확실히 있음)
    public CacheWarmupService(RedisTemplate redis) {
        this.redis = redis;
    }

    @PostConstruct
    public void init() {
        // 의존성 주입 완료 후 실행
        // 생성자에서 하면 redis가 null일 수 있음!
        localCache = loadFromRedis();
        log.info("Cache warmed up: {} items", localCache.size());
    }
}
@PreDestroy — 정리
@Component
public class SchedulerService {
    private ScheduledExecutorService executor;

    @PostConstruct
    public void start() {
        executor = Executors.newScheduledThreadPool(2);
        executor.scheduleAtFixedRate(
            this::processQueue, 0, 5, TimeUnit.SECONDS);
    }

    @PreDestroy
    public void shutdown() {
        // 컨테이너 종료 시 스케줄러 정상 종료
        // 없으면 JVM 종료까지 스레드가 살아있음
        executor.shutdown();
        log.info("Scheduler stopped");
    }
}
왜 생성자에서 초기화 로직을 쓰면 안 되나?

생성자 주입의 경우 의존성이 생성자와 동시에 주입되므로 생성자에서 써도 됩니다. 하지만 필드/세터 주입을 사용하면 생성자가 먼저 실행되고, 그 이후에 @Autowired 주입이 이뤄집니다. 생성자 안에서 아직 주입 안 된 필드를 참조하면 NPE가 발생합니다. @PostConstruct모든 주입이 완료된 이후에 실행되는 것이 보장되므로 초기화 로직은 항상 @PostConstruct에 두는 것이 안전합니다.

2. Bean Scope — 수명과 공유 범위

Scope인스턴스 생성 시점공유 범위주요 사용처
Singleton 컨테이너 시작 시 1회 ApplicationContext 전체에서 1개 공유 서비스, 레포지토리, 대부분의 Bean — 상태 없는(stateless) 객체에 적합
Prototype getBean() 호출마다 요청한 클라이언트마다 새 인스턴스 상태를 가지는(stateful) 객체, 매번 새 인스턴스가 필요할 때
Request HTTP 요청마다 하나의 HTTP 요청 내에서만 공유 요청별 컨텍스트 정보 (로그 트레이싱 ID, 요청 파라미터 보관)
Session HTTP 세션마다 하나의 HTTP 세션 내에서만 공유 로그인 사용자 정보, 장바구니 등 세션 단위 상태

Singleton Bean에 Prototype Bean을 주입하면 생기는 문제

문제 — 항상 같은 인스턴스
@Component
@Scope("prototype")
public class TaskContext { // 매번 새 인스턴스여야 함
    private String taskId;
}

@Service // Singleton
public class TaskService {
    @Autowired
    private TaskContext ctx; // 주입은 1번만 일어남!

    public void process(String id) {
        // ctx는 항상 최초 주입된 하나의 인스턴스
        // "prototype이지만 사실상 singleton처럼 동작"
        ctx.setTaskId(id); // 스레드 안전하지 않음!
    }
}
해결 — ApplicationContext 직접 조회
@Service
public class TaskService {
    // ObjectProvider (Spring 권장)
    @Autowired
    private ObjectProvider<TaskContext> ctxProvider;

    public void process(String id) {
        // 호출마다 새 Prototype 인스턴스 생성
        TaskContext ctx = ctxProvider.getObject();
        ctx.setTaskId(id);
        // ...
    }
}

// 또는 @Scope proxyMode 사용
@Component
@Scope(value = "prototype",
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TaskContext { ... }
// → 주입 시 프록시가 들어오고
//   실제 사용 시마다 새 인스턴스 반환
Singleton Scope와 Thread-safety

Singleton Bean은 모든 스레드가 공유합니다. 따라서 Singleton Bean은 상태를 가지면 안 됩니다(stateless). 인스턴스 변수에 요청별 데이터를 저장하면 다른 요청의 데이터를 덮어쓰거나 읽는 race condition이 발생합니다. 쿠팡처럼 수백만 TPS를 처리하는 서비스에서 이 실수는 심각한 데이터 오염으로 이어집니다. 상태가 필요하다면 메서드 로컬 변수에 담거나, ThreadLocal을 사용하거나, Prototype/Request Scope를 쓰십시오.

3. 면접 대비 핵심 Q&A

Q1. "@PostConstruct와 생성자 초기화의 차이는?"

생성자는 Spring이 의존성을 주입하기 전에 실행될 수 있습니다(필드/세터 주입 시). @PostConstructSpring이 모든 의존성 주입을 완료한 뒤 실행을 보장합니다. 생성자 주입만 사용하면 생성자 안에서도 안전하게 초기화할 수 있지만, DB 조회나 외부 API 호출처럼 부작용이 있는 초기화는 @PostConstruct에 두는 것이 관례입니다.

Q2. "Singleton Bean에서 상태를 가지면 왜 위험한가요?"

Singleton Bean은 하나의 인스턴스를 모든 스레드가 공유합니다. 인스턴스 변수에 값을 쓰면 동시에 다른 요청을 처리 중인 스레드도 그 값을 읽거나 덮어씁니다. 예를 들어 private String currentUser 필드를 두고 요청마다 설정하면, A 요청이 설정한 값을 B 요청이 읽는 race condition이 발생합니다. 상태는 메서드 파라미터나 로컬 변수로만 다루고, 인스턴스 필드는 설정값(불변)이나 의존성만 담아야 합니다.

Q3. "Request Scope Bean을 Singleton Service에서 쓰려면 어떻게 하나요?"

Request Scope Bean에 proxyMode = ScopedProxyMode.TARGET_CLASS를 지정하면 Singleton Bean에 주입되는 것은 실제 Bean이 아닌 프록시 객체입니다. 프록시는 메서드 호출 시마다 현재 HTTP 요청의 Request Scope Bean을 찾아서 위임합니다. 덕분에 Singleton Service가 Request Scope Bean을 주입받아도 요청마다 올바른 인스턴스를 사용할 수 있습니다.

이 챕터의 핵심 한 줄

Bean 생명주기는 "생성 → DI → @PostConstruct → 사용 → @PreDestroy → 소멸"이며, Singleton Scope(기본)은 stateless로만 사용해야 스레드 안전성이 보장된다.