2023년 1월 1일
08:00 AM
Buffering ...

최근 글 👑

Spring Bean 생명주기

2026. 4. 27. 10:15ㆍ스프링

데일리 CS 학습

더보기

Q. List, Set, Map의 차이를 설명해주세요. 

A. List는 순서가 있고 중복을 허용합니다. Set은 순서가 없고 중복을 허용하지 않습니다. Map은 key-valu 쌍으로 저장하고 key는 중복 불가입니다. 

Q. ArrayList와 LinkedList의 차이를 설명해주세요.

A. ArrayList는 내부적으로 배열을 사용해 인덱스 조회가 O(1)로 빠르지만, 중간 삽입, 삭제 시 요소를 밀어야 해서 O(n)입니다. LinkedList는 노드를 포인터로 연결해 중간 삽입, 삭제가 O(1)이지만 특정 위치 조회는 O(n)입니다.

Q. 제네릭이란 무엇이고 왜 사용하나요?

A. 클래스나 메서드를 정의할 때 타입을 미리 고정하지 않고, 사용 시점에 지정할 수 있게 하는 기능입니다. 가장 큰 장점은 두 가지로 첫째 타입 안정성입니다. 컴파일 타임에 타입을 체크하므로 잘못된 타입이 들어오면 즉시 에러가 납니다. 둘째로 형변환 생략입니다. Object로 받아서 매번 캐스팅하는 번거로움이 없어집니다.

Q. Optional이란 무엇이고 어떻게 활용하나요?

A. null을 직접 다루는 대신 값이 있을 수도, 없을 수도 있다는 것을 명시적으로 표현하는 컨테이너 클래스입니다. NullPointerException을 방지하기 위해 사용합니다.

 

 

Today I Learned · 2026 · Spring Backend

Spring Bean 생명주기
등록 · 의존성 주입 · 초기화 · 소멸

스프링 컨테이너가 빈을 만들고 없애기까지 — 각 단계에서 무슨 일이 일어나는지, 콜백을 어떻게 활용하는지 완전 정리

Java Spring Boot IoC DI 생명주기

01 Bean 생명주기 전체 흐름

스프링 빈은 컨테이너가 시작되면서 생성되고, 컨테이너가 종료되면 소멸합니다. 그 사이에 의존성 주입과 초기화·소멸 콜백이 순서대로 호출됩니다.

1
스프링 컨테이너 생성
ApplicationContext 초기화. @Configuration, @ComponentScan 설정 파싱
2
빈 정의(BeanDefinition) 등록
@Bean, @Component로 등록된 빈의 메타정보(클래스, 스코프, 생성 방식) 수집
3
빈 인스턴스 생성
생성자 호출로 객체 생성. 이 시점에 의존성 주입은 아직 완료되지 않음
4
의존성 주입 (DI)
@Autowired, 생성자/setter 주입으로 의존 객체 주입. 생성자 주입은 3단계와 동시에 처리
5
초기화 콜백 — @PostConstruct
DI 완료 후 호출. DB 커넥션 확인, 캐시 로드 등 초기화 작업 수행
6
빈 사용
애플리케이션이 동작하는 동안 빈이 사용됨
7
소멸 콜백 — @PreDestroy
컨테이너 종료 직전 호출. 커넥션 반환, 리소스 해제 등 정리 작업 수행
8
스프링 컨테이너 종료 / 빈 소멸
컨테이너와 함께 빈이 소멸됨
핵심 포인트 — 생성과 초기화 분리
객체 생성(생성자)과 초기화는 분리하는 것이 좋습니다. 생성자는 객체를 만드는 역할에만 집중하고, DB 연결 같은 무거운 초기화 작업은 @PostConstruct에서 합니다. 이렇게 해야 모든 의존성이 주입된 상태에서 안전하게 초기화할 수 있습니다.

02 스프링 컨테이너와 빈 등록

IoC 컨테이너(ApplicationContext)는 빈의 생성·관리·소멸을 담당합니다. 빈을 등록하는 방법은 크게 세 가지입니다.

// 방법 1 — @Component 계열 (컴포넌트 스캔) ✅ 가장 일반적 @Service // 비즈니스 로직 레이어 @Repository // 데이터 접근 레이어 (DataAccessException 변환 포함) @Controller // 웹 요청 처리 레이어 @Component // 위 셋에 해당하지 않는 일반 컴포넌트 // → 모두 내부에 @Component를 포함, 자동 스캔 대상 // 방법 2 — @Bean (수동 등록) — 외부 라이브러리, 세밀한 설정 필요 시 @Configuration public class AppConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } // 지연 초기화 — @Lazy: 처음 요청할 때 생성 (컨테이너 시작 시 아님) @Service @Lazy public class HeavyService { // 무거운 초기화 → 실제 사용 전까지 생성 지연 }Java

03 의존성 주입 (DI) 3가지 방식

// ✅ 방법 1 — 생성자 주입 (가장 권장) @Service @RequiredArgsConstructor // final 필드 생성자 자동 생성 (Lombok) public class OrderService { private final MemberRepository memberRepository; // final → 불변 보장 private final OrderRepository orderRepository; // 생성자가 하나면 @Autowired 생략 가능 } // ❌ 방법 2 — 필드 주입 (지양) @Service public class OrderService { @Autowired private MemberRepository memberRepository; // 테스트 시 주입 불가, 불변 보장 안 됨 } // △ 방법 3 — setter 주입 (선택적 의존관계에만) @Service public class OrderService { private MemberRepository memberRepository; @Autowired(required = false) // 빈이 없어도 무시 public void setMemberRepository(MemberRepository memberRepository) { this.memberRepository = memberRepository; } }Java
생성자 주입
✅ 권장
  • final로 불변 보장
  • 순환 참조 조기 감지
  • 테스트 시 직접 주입 가능
  • 필수 의존관계 명확
필드 주입
❌ 지양
  • 코드 간결 (유일한 장점)
  • 테스트 시 주입 어려움
  • 순환 참조 런타임 발견
  • 불변성 보장 안 됨
setter 주입
△ 선택적 의존만
  • 선택적 의존관계 표현
  • 주입 후 변경 가능
  • 불변성 보장 안 됨
  • 실무 사용 드묾

같은 타입 빈이 여러 개 — @Primary, @Qualifier

@Component @Primary // 우선순위 높음 — 설정 없으면 이 빈 주입 public class RateDiscountPolicy implements DiscountPolicy { ... } @Component @Qualifier("fixDiscount") public class FixDiscountPolicy implements DiscountPolicy { ... } // 주입 시 명시적으로 지정 public OrderService(@Qualifier("fixDiscount") DiscountPolicy discountPolicy) { this.discountPolicy = discountPolicy; }Java

04 초기화 콜백 — @PostConstruct

@PostConstructDI 완료 직후 딱 한 번 호출됩니다. 모든 의존 객체가 주입된 상태이므로 안전하게 초기화 작업을 수행할 수 있습니다.

@Service @RequiredArgsConstructor public class CacheService { private final ProductRepository productRepository; private Map<Long, Product> cache; // 생성자에서 하면 안 되는 이유: // 필드/setter 주입 시 생성자 실행 시점엔 의존 객체가 아직 null @PostConstruct // DI 완료 후 호출 — productRepository 안전하게 사용 가능 public void init() { cache = productRepository.findAll().stream() .collect(Collectors.toMap(Product::getId, p -> p)); log.info("상품 캐시 초기화 완료: {}건", cache.size()); } } // DB 연결 확인 — 시작 시 즉시 문제 감지 @Component @RequiredArgsConstructor public class DatabaseHealthChecker { private final DataSource dataSource; @PostConstruct public void checkConnection() { try (Connection conn = dataSource.getConnection()) { log.info("DB 연결 확인 완료: {}", conn.getMetaData().getURL()); } catch (SQLException e) { throw new IllegalStateException("DB 연결 실패", e); } } }Java
⚠ @PostConstruct에서 예외 발생 시
예외가 발생하면 애플리케이션 컨텍스트 로딩이 실패합니다. DB 연결 실패처럼 치명적인 문제를 조기에 발견하는 장점이 있지만, 너무 무거운 작업을 넣으면 서버 시작이 느려집니다.

05 소멸 콜백 — @PreDestroy

@PreDestroy스프링 컨테이너가 종료되기 직전 호출됩니다. 커넥션 반환, 스레드 종료, 외부 리소스 해제 등 정리 작업에 사용합니다.

@Component public class NetworkClient { private Connection connection; @PostConstruct public void connect() { connection = openConnection(); log.info("커넥션 연결"); } @PreDestroy // 컨테이너 종료 직전 — 리소스 정리 public void disconnect() { if (connection != null) { connection.close(); log.info("커넥션 해제"); } } } // 스레드 풀 종료 예시 @Component public class SchedulerManager { private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); @PreDestroy public void shutdown() { scheduler.shutdown(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); } } }Java
⚠ @PreDestroy가 호출되지 않는 경우
프로토타입 스코프 빈은 @PreDestroy가 호출되지 않습니다. 생성 후 컨테이너가 더 이상 관리하지 않기 때문입니다. 또한 JVM이 강제 종료(kill -9)되면 소멸 콜백이 호출되지 않을 수 있습니다.

06 3가지 생명주기 콜백 방법 비교

// ✅ 방법 1 — @PostConstruct / @PreDestroy (권장) // JSR-250 표준 어노테이션 → 스프링 종속 아님, 코드 간결 @PostConstruct public void init() { ... } @PreDestroy public void destroy() { ... } // △ 방법 2 — InitializingBean / DisposableBean 인터페이스 (비권장) // 스프링 인터페이스에 강하게 결합 → 외부 라이브러리 적용 불가 public class NetworkClient implements InitializingBean, DisposableBean { @Override public void afterPropertiesSet() { connect(); } // 초기화 @Override public void destroy() { disconnect(); } // 소멸 } // △ 방법 3 — @Bean(initMethod, destroyMethod) (외부 라이브러리에 유용) @Bean(initMethod = "init", destroyMethod = "close") public DataSource dataSource() { HikariDataSource ds = new HikariDataSource(); ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); return ds; // → ds.init() 초기화, ds.close() 소멸 시 자동 호출 }Java
방법 스프링 종속 외부 라이브러리 권장
@PostConstruct/@PreDestroy ❌ 없음 (JSR-250) 적용 불가 ✅ 기본 사용
InitializingBean / DisposableBean ✅ 있음 적용 불가 ❌ 비권장
@Bean(initMethod, destroyMethod) ❌ 없음 ✅ 가능 △ 외부 라이브러리만

07 빈 스코프 — Singleton · Prototype · Request

스코프는 빈이 존재할 수 있는 범위입니다. 기본값은 싱글톤입니다.

singleton
기본값

컨테이너에 딱 1개만 생성. 모든 요청에서 같은 인스턴스 공유.

prototype
요청마다 새 인스턴스

빈 요청 시마다 새 객체 생성. 스프링이 생성까지만 관여. @PreDestroy 호출 안 됨.

request
HTTP 요청 단위

HTTP 요청 1개마다 1개 생성. 요청 종료 시 소멸. 웹 스코프(session, application도 있음).

@Component @Scope("prototype") public class PrototypeBean { ... } @Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MyLogger { // proxyMode: 싱글톤 빈에 request 스코프 빈 주입 시 프록시로 해결 private String requestURL; } // ⚠ Singleton 빈에 Prototype 빈 주입 시 문제 @Service public class SingletonService { @Autowired private PrototypeBean prototypeBean; // 싱글톤 생성 시 한 번만 주입 → 매번 같은 객체! (prototype 의미 없음) // ✅ 해결: ObjectProvider 사용 @Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider; public void logic() { PrototypeBean bean = prototypeBeanProvider.getObject(); // 매번 새 객체 } }Java

08 BeanPostProcessor — 빈 후처리기

BeanPostProcessor빈이 생성되고 초기화되는 전·후에 끼어들어 빈을 조작할 수 있는 확장 포인트입니다. 스프링이 내부적으로 AOP 프록시 생성, @Autowired 주입 등에 활용합니다.

// BeanPostProcessor 실행 순서 // 빈 생성 → postProcessBeforeInitialization() → @PostConstruct → postProcessAfterInitialization() @Component public class LoggingBeanPostProcessor implements BeanPostProcessor { // @PostConstruct 실행 전 @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { log.debug("초기화 전: {}", beanName); return bean; // 원본 빈 또는 다른 객체 반환 가능 } // @PostConstruct 실행 후 @Override public Object postProcessAfterInitialization(Object bean, String beanName) { log.debug("초기화 후: {}", beanName); // 여기서 프록시 객체로 교체하면 AOP 동작 return bean; } } // 스프링이 내부적으로 사용하는 BeanPostProcessor들 // AutowiredAnnotationBeanPostProcessor → @Autowired 처리 // CommonAnnotationBeanPostProcessor → @PostConstruct, @PreDestroy 처리 // AnnotationAwareAspectJAutoProxyCreator → @Transactional, @Async 등 AOP 프록시 생성Java
@PostConstruct가 동작하는 원리
@PostConstruct는 스프링이 직접 처리하는 것이 아니라, CommonAnnotationBeanPostProcessor가 빈의 메서드를 스캔해서 호출합니다. 마찬가지로 @Transactional의 AOP 프록시도 BeanPostProcessor를 통해 생성됩니다.

09 실무 함정 패턴

함정 1 — 생성자에서 의존 빈 사용 (필드 주입 시)

// ❌ 필드 주입 시 생성자에서 의존 빈 사용 — NPE 발생 @Service public class BadService { @Autowired private MemberRepository memberRepository; // 생성자 호출 후 주입됨 public BadService() { memberRepository.findAll(); // NPE! 아직 null } } // ✅ 해결 1 — 생성자 주입 (주입과 생성이 동시에) @Service @RequiredArgsConstructor public class GoodService { private final MemberRepository memberRepository; } // ✅ 해결 2 — @PostConstruct로 초기화 분리 @Service public class GoodService2 { @Autowired private MemberRepository memberRepository; @PostConstruct public void init() { memberRepository.findAll(); // 안전 — DI 완료 후 호출 } }Java

함정 2 — 싱글톤 빈의 상태 공유

// ❌ 싱글톤 빈에 상태(인스턴스 변수) 저장 — 동시성 문제 발생 @Service public class StatefulService { private int price; // 여러 스레드가 공유 → 데이터 꼬임! public void order(int price) { this.price = price; // Thread-A 10000 저장 중 Thread-B가 20000 덮어씀 } } // ✅ 싱글톤 빈은 무상태(stateless)로 설계 @Service public class StatelessService { public int order(int price) { return price; // 상태를 지역변수로 처리 → 스레드 안전 } }Java
⚠ 싱글톤 빈 설계 원칙
싱글톤 빈은 여러 스레드가 동시에 접근합니다. 인스턴스 변수에 상태를 저장하면 동시성 버그가 발생합니다. 스프링 빈은 항상 무상태(stateless)로 설계하고, 상태가 필요하면 지역변수나 파라미터를 사용하거나 prototype/request 스코프를 사용하세요.

10 면접 예상 질문

Q1. 스프링 빈의 생명주기를 설명하세요.

스프링 빈의 생명주기는 컨테이너 생성 → 빈 등록 → 객체 생성 → 의존성 주입 → @PostConstruct → 빈 사용 → @PreDestroy → 컨테이너 종료 순서로 진행됩니다. 객체 생성과 초기화는 분리하는 것이 원칙으로, 생성자는 객체 생성에만 집중하고 DB 연결 같은 무거운 초기화는 @PostConstruct에서 수행합니다. 이렇게 해야 DI가 완료된 상태에서 안전하게 초기화할 수 있습니다.

Q2. 생성자 주입을 권장하는 이유는?

네 가지 이유가 있습니다. 첫째 불변성 — final 키워드로 주입 후 변경되지 않음을 보장합니다. 둘째 순환 참조 조기 감지 — 순환 참조가 있으면 애플리케이션 시작 시점에 오류가 발생합니다(Spring Boot 2.6+). 셋째 테스트 용이성 — new로 객체 생성 시 생성자에 직접 Mock 객체를 넣을 수 있습니다. 넷째 필수 의존관계 명확 — 어떤 의존성이 반드시 필요한지 시그니처에서 명확히 알 수 있습니다.

Q3. @PostConstruct와 생성자의 차이는?

생성자는 객체가 생성되는 시점에 호출됩니다. 필드/setter 주입을 사용하면 생성자 실행 시점에 의존 객체가 아직 null입니다. @PostConstructDI가 완전히 완료된 후 호출되므로 주입된 의존 객체를 안전하게 사용해 초기화할 수 있습니다. DB 연결 확인, 캐시 로드 등 의존 객체가 필요한 초기화 작업은 반드시 @PostConstruct에서 해야 합니다.

Q4. 싱글톤 빈이 무상태여야 하는 이유는?

싱글톤 빈은 스프링 컨테이너에 단 하나만 존재하고 모든 요청 스레드가 공유합니다. 인스턴스 변수에 상태를 저장하면 Thread-A가 저장한 값을 Thread-B가 덮어쓰는 동시성 버그가 발생합니다. 따라서 싱글톤 빈은 상태를 인스턴스 변수가 아닌 지역변수나 파라미터로 처리해야 스레드 안전성을 보장할 수 있습니다.

Q5. BeanPostProcessor란 무엇이고 어디에 쓰이나요?

BeanPostProcessor는 빈이 생성된 후 초기화 전·후에 끼어들어 빈을 조작할 수 있는 확장 포인트입니다. 스프링 자체도 내부적으로 활용합니다. AutowiredAnnotationBeanPostProcessor@Autowired 처리, CommonAnnotationBeanPostProcessor@PostConstruct/@PreDestroy 처리, AnnotationAwareAspectJAutoProxyCreator는 AOP 프록시 생성을 담당합니다.
핵심 요약
  • 생명주기 순서 — 컨테이너 생성 → 빈 등록 → 객체 생성 → DI → @PostConstruct → 사용 → @PreDestroy → 소멸
  • 생성자 주입 권장 — final 불변 보장, 순환 참조 조기 감지, 테스트 용이, 필수 의존관계 명확
  • @PostConstruct — DI 완료 후 호출. 의존 빈을 사용하는 초기화 위치. 예외 시 앱 시작 실패
  • @PreDestroy — 컨테이너 종료 직전 호출. 리소스 해제. 프로토타입 빈은 호출 안 됨
  • 싱글톤 무상태 — 모든 스레드가 공유하므로 인스턴스 변수 상태 저장 금지
  • BeanPostProcessor — @Autowired, @PostConstruct, AOP 프록시가 모두 이것을 통해 동작하는 스프링 핵심 확장 포인트
오늘의 핵심 — 빈 생명주기의 핵심은 생성·주입·초기화의 순서다. 생성자에서 의존 빈을 쓰지 말고 반드시 @PostConstruct에서 초기화하라. 싱글톤은 무상태로.
728x90