Spring Bean 생명주기와 InitializingBean 이해하기

Spring Bean의 생명주기는 Bean이 생성되고 초기화되어 사용 가능한 상태가 되고, 최종적으로 소멸되기까지의 전체 과정을 의미합니다. Spring Container가 이 전체 과정을 관리하며, 개발자는 생명주기의 특정 시점에 원하는 로직을 실행할 수 있습니다.
Spring Bean 생명주기 단계
① Spring Container 시작
② Bean 인스턴스 생성 (생성자 호출)
③ 의존성 주입 (Dependency Injection)
④ Bean 초기화 전 처리 (BeanPostProcessor.postProcessBeforeInitialization)
⑤ Bean 초기화 (@PostConstruct, InitializingBean, init-method)
⑥ Bean 초기화 후 처리 (BeanPostProcessor.postProcessAfterInitialization)
⑦ Bean 사용 가능 (애플리케이션 로직 실행)
⑧ Container 종료 시작
⑨ Bean 소멸 전 처리 (@PreDestroy, DisposableBean, destroy-method)
⑩ Spring Container 종료
Spring Container가 Bean 정의(Bean Definition)를 읽고 Java의 리플렉션을 사용하여 Bean 객체를 생성합니다. 이 시점에 생성자가 호출됩니다.
생성된 Bean에 필요한 의존성을 주입합니다. @Autowired, @Inject, Setter 메서드 등을 통해 다른 Bean이나 값을 주입합니다. 이 단계에서 Bean의 필드나 메서드에 의존 객체가 할당됩니다.
의존성 주입이 완료된 후 Bean이 사용 가능한 상태로 만들기 위한 초기화 작업을 수행합니다. 데이터베이스 연결, 외부 리소스 로딩, 캐시 초기화 등의 작업이 이 단계에서 이루어집니다.
초기화가 완료된 Bean은 애플리케이션 로직에서 자유롭게 사용할 수 있습니다. Spring Container는 Singleton Bean의 경우 하나의 인스턴스를 계속 관리합니다.
애플리케이션 종료 시점에 Bean이 사용하던 리소스를 정리하고 소멸합니다. 데이터베이스 연결 해제, 파일 닫기, 스레드 종료 등의 정리 작업이 수행됩니다.
Spring에서 Bean을 생성하는 방법은 여러 가지가 있습니다. 각 방법마다 생성 시점과 방식이 조금씩 다릅니다.
public class UserService {
// 생성자
public UserService() {
System.out.println("1. UserService 생성자 호출");
}
}
@Component, @Service, @Repository, @Controller 등의 어노테이션을 사용하면 Component Scan을 통해 자동으로 Bean이 등록됩니다.
public class AppConfig {
@Bean
public UserService userService() {
System.out.println("1. UserService 생성");
return new UserService();
}
}
@Configuration 클래스 내에서 @Bean 메서드를 정의하여 Bean을 생성합니다. 개발자가 직접 Bean 생성 로직을 제어할 수 있습니다.
Spring에서 의존성을 주입하는 방법은 크게 세 가지가 있으며, 각각 주입 시점이 다릅니다.
public class UserService {
private final UserRepository userRepository;
// 생성자 주입 - Bean 생성 시점에 주입
@Autowired
public UserService(UserRepository userRepository) {
System.out.println("2. 생성자를 통한 의존성 주입");
this.userRepository = userRepository;
}
}
생성자 주입은 Bean 인스턴스 생성 시점에 의존성이 주입됩니다. Spring에서 권장하는 방식이며, 불변성(final)을 보장할 수 있습니다.
| 장점 | 설명 |
|---|---|
| 불변성 보장 | final 키워드 사용 가능 |
| 순환 참조 방지 | 컴파일 타임에 순환 참조 발견 가능 |
| 테스트 용이 | 생성자를 통해 Mock 객체 주입 가능 |
| 필수 의존성 명시 | 생성자 파라미터로 필수 의존성 표현 |
public class UserService {
private UserRepository userRepository;
// Setter 주입 - Bean 생성 후 주입
@Autowired
public void setUserRepository(UserRepository userRepository) {
System.out.println("3. Setter를 통한 의존성 주입");
this.userRepository = userRepository;
}
}
Setter 주입은 Bean 인스턴스 생성 후 Setter 메서드를 호출하여 의존성을 주입합니다. 선택적 의존성이나 변경 가능한 의존성에 사용됩니다.
public class UserService {
// 필드 주입 - Bean 생성 후 리플렉션으로 주입
@Autowired
private UserRepository userRepository;
public UserService() {
System.out.println("2. 생성자 호출 (아직 의존성 주입 안됨)");
}
}
필드 주입은 Bean 생성 후 리플렉션을 사용하여 필드에 직접 주입합니다. 코드가 간결하지만 테스트가 어렵고 불변성을 보장할 수 없어 권장되지 않습니다.
⚠️ 주의: 필드 주입은 생성자에서 의존성을 사용할 수 없습니다. 생성자 실행 시점에는 아직 의존성이 주입되지 않았기 때문입니다.
public class UserService {
private final UserRepository userRepository; // 생성자 주입용
@Autowired
private EmailService emailService; // 필드 주입용
private SmsService smsService; // Setter 주입용
public UserService(UserRepository userRepository) {
System.out.println("1. 생성자 호출 및 생성자 주입");
this.userRepository = userRepository;
// 이 시점에 emailService는 null
}
// 2. 필드 주입 (생성자 호출 후)
// emailService에 자동 주입됨
@Autowired
public void setSmsService(SmsService smsService) {
System.out.println("3. Setter 주입");
this.smsService = smsService;
}
}
주입 순서는 ① 생성자 주입 → ② 필드 주입 → ③ Setter 주입 순서로 진행됩니다.
public class UserService {
@Value("${app.name}")
private String appName;
private final String version;
public UserService(@Value("${app.version}") String version) {
System.out.println("생성자 주입: version = " + version);
this.version = version;
// appName은 아직 null
}
}
@Value도 의존성 주입과 동일한 순서로 처리됩니다. 생성자 파라미터의 @Value가 먼저 주입되고, 필드의 @Value는 생성자 실행 후에 주입됩니다.
InitializingBean은 Spring이 제공하는 Bean 초기화 콜백 인터페이스입니다. 의존성 주입이 완료된 후 실행되어야 하는 초기화 로직을 정의할 수 있습니다.
public class UserService implements InitializingBean {
private final UserRepository userRepository;
@Autowired
private EmailService emailService;
public UserService(UserRepository userRepository) {
System.out.println("1. 생성자 호출");
this.userRepository = userRepository;
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("2. InitializingBean.afterPropertiesSet() 호출");
// 이 시점에 모든 의존성 주입 완료
System.out.println("userRepository: " + userRepository);
System.out.println("emailService: " + emailService);
// 초기화 로직 수행
validateDependencies();
loadInitialData();
}
private void validateDependencies() {
// 의존성 검증 로직
}
private void loadInitialData() {
// 초기 데이터 로딩
}
}
afterPropertiesSet() 메서드는 모든 프로퍼티(의존성)가 설정된 직후에 호출됩니다. 이 시점에는 생성자 주입, 필드 주입, Setter 주입이 모두 완료된 상태입니다.
Spring에서는 InitializingBean 외에도 여러 가지 초기화 방법을 제공합니다. 각 방법의 특징과 실행 순서를 이해하는 것이 중요합니다.
public class UserService {
@Autowired
private UserRepository userRepository;
public UserService() {
System.out.println("1. 생성자 호출");
}
@PostConstruct
public void init() {
System.out.println("2. @PostConstruct 메서드 호출");
// 의존성 주입 완료 후 실행
System.out.println("userRepository: " + userRepository);
}
}
@PostConstruct는 Java 표준(JSR-250) 어노테이션으로, Spring에 종속되지 않습니다. 가장 권장되는 초기화 방법입니다.
public class AppConfig {
@Bean(initMethod = "initialize")
public UserService userService() {
return new UserService();
}
}
public class UserService {
public void initialize() {
System.out.println("3. initMethod 호출");
// 초기화 로직
}
}
@Bean 어노테이션의 initMethod 속성을 사용하면 특정 메서드를 초기화 콜백으로 지정할 수 있습니다. 외부 라이브러리 클래스에 유용합니다.
여러 초기화 방법을 함께 사용하면 다음 순서로 실행됩니다.
public class UserService implements InitializingBean {
public UserService() {
System.out.println("1. 생성자 호출");
}
@PostConstruct
public void postConstruct() {
System.out.println("2. @PostConstruct 호출");
}
@Override
public void afterPropertiesSet() {
System.out.println("3. InitializingBean.afterPropertiesSet() 호출");
}
public void customInit() {
System.out.println("4. @Bean(initMethod) 호출");
}
}
| 순서 | 초기화 방법 | 특징 |
|---|---|---|
| 1 | 생성자 | Bean 인스턴스 생성 |
| 2 | 의존성 주입 | @Autowired, @Value 등 |
| 3 | @PostConstruct | 가장 먼저 실행되는 초기화 콜백 |
| 4 | InitializingBean.afterPropertiesSet() | Spring 인터페이스 기반 |
| 5 | @Bean(initMethod) | 가장 나중에 실행 |
| 방법 | 장점 | 단점 | 사용 시나리오 |
|---|---|---|---|
| @PostConstruct | Java 표준, 간결함 | Java 9+ 이상에서 별도 의존성 필요 | 일반적인 초기화 (가장 권장) |
| InitializingBean | Spring 네이티브 지원 | Spring에 종속 | Spring 기능이 필요한 경우 |
| @Bean(initMethod) | 코드 수정 불필요 | 설정이 분산됨 | 외부 라이브러리 Bean |
| 생성자 | 불변성 보장 | 의존성 주입 전 | 필수 검증만 |
💡 Best Practice: 대부분의 경우 @PostConstruct를 사용하는 것이 권장됩니다. Java 표준이며 가독성이 좋고, Spring에 종속되지 않습니다.
public class DatabaseConnectionPool implements InitializingBean {
@Value("${db.url}")
private String dbUrl;
@Value("${db.pool.size}")
private int poolSize;
private DataSource dataSource;
@Override
public void afterPropertiesSet() throws Exception {
// 프로퍼티 검증
if (dbUrl == null || dbUrl.isEmpty()) {
throw new IllegalStateException("Database URL is required");
}
// 연결 풀 초기화
HikariConfig config = new HikariConfig();
config.setJdbcUrl(dbUrl);
config.setMaximumPoolSize(poolSize);
this.dataSource = new HikariDataSource(config);
System.out.println("데이터베이스 연결 풀 초기화 완료");
}
}
public class ProductService {
@Autowired
private ProductRepository productRepository;
private Map<Long, Product> productCache;
@PostConstruct
public void initCache() {
System.out.println("제품 캐시 초기화 시작");
// 자주 사용되는 제품 데이터를 미리 로드
List<Product> popularProducts =
productRepository.findTop100ByOrderBySalesDesc();
productCache = popularProducts.stream()
.collect(Collectors.toMap(Product::getId, p -> p));
System.out.println("제품 캐시 초기화 완료: " + productCache.size());
}
}
public class TaskScheduler {
@Autowired
private TaskService taskService;
private ScheduledExecutorService scheduler;
@PostConstruct
public void startScheduler() {
scheduler = Executors.newScheduledThreadPool(5);
// 1분마다 작업 실행
scheduler.scheduleAtFixedRate(
() -> taskService.executePeriodicTask(),
0, 1, TimeUnit.MINUTES
);
System.out.println("스케줄러 시작 완료");
}
@PreDestroy
public void stopScheduler() {
if (scheduler != null) {
scheduler.shutdown();
}
}
}
public class EmailService implements InitializingBean {
@Value("${email.host}")
private String host;
@Value("${email.port}")
private Integer port;
@Value("${email.username}")
private String username;
@Override
public void afterPropertiesSet() throws Exception {
// 필수 프로퍼티 검증
validateProperty(host, "email.host");
validateProperty(port, "email.port");
validateProperty(username, "email.username");
// 포트 범위 검증
if (port < 1 || port > 65535) {
throw new IllegalStateException(
"Invalid port: " + port);
}
}
private void validateProperty(Object value, String name) {
if (value == null) {
throw new IllegalStateException(
"Required property '" + name + "' is missing");
}
}
}
public class OrderService {
@Autowired(required = false)
private PaymentService paymentService;
@Autowired(required = false)
private ShippingService shippingService;
@PostConstruct
public void validateDependencies() {
// 선택적 의존성이지만 둘 중 하나는 필수
if (paymentService == null && shippingService == null) {
throw new IllegalStateException(
"At least one service (Payment or Shipping) is required");
}
}
}
초기화만큼 중요한 것이 소멸 시 리소스 정리입니다. Spring은 다양한 소멸 콜백 메서드를 제공합니다.
public class FileProcessor {
private ExecutorService executorService;
private List<FileWriter> openWriters = new ArrayList<>();
@PostConstruct
public void init() {
executorService = Executors.newFixedThreadPool(10);
System.out.println("FileProcessor 초기화");
}
@PreDestroy
public void cleanup() {
System.out.println("리소스 정리 시작");
// 스레드 풀 종료
if (executorService != null) {
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
}
// 열린 파일 닫기
for (FileWriter writer : openWriters) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("리소스 정리 완료");
}
}
public class CacheManager implements DisposableBean {
private Map<String, Object> cache = new ConcurrentHashMap<>();
@Override
public void destroy() throws Exception {
System.out.println("캐시 정리 시작");
// 캐시 저장 (필요시)
persistCache();
// 캐시 클리어
cache.clear();
System.out.println("캐시 정리 완료");
}
}
✅ 권장: 생성자 주입을 사용하고, 초기화 로직은 @PostConstruct에 분리하세요.
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository,
EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
@PostConstruct
public void init() {
// 초기화 로직
}
}
public void init() {
try {
// 초기화 작업
connectToDatabase();
} catch (Exception e) {
// 명확한 에러 메시지와 함께 실패
throw new BeanInitializationException(
"Failed to initialize UserService: " + e.getMessage(), e);
}
}
@Lazy // 실제 사용 시점까지 Bean 생성 지연
public class HeavyService {
@PostConstruct
public void init() {
// 무거운 초기화 작업
loadLargeDataset();
}
}
A: 필드 주입이나 Setter 주입을 사용한 경우, 생성자 실행 시점에는 아직 의존성이 주입되지 않았습니다. 생성자 주입을 사용하거나, 초기화 로직을 @PostConstruct 메서드로 분리하세요.
@Service
public class UserService {
@Autowired
private UserRepository repository;
public UserService() {
repository.findAll(); // NullPointerException!
}
}
// ✅ 좋은 예
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
repository.findAll(); // 정상 동작
}
}
A: @PostConstruct 사용을 권장합니다. Java 표준(JSR-250)이며 Spring에 종속되지 않아 테스트와 이식성이 좋습니다. InitializingBean은 Spring Framework에 강하게 결합되므로 특별한 이유가 없다면 @PostConstruct를 사용하세요.
A: 초기화 메서드에서 예외가 발생하면 Bean 생성이 실패하고 애플리케이션 컨텍스트 로딩이 중단됩니다. 이는 의도된 동작으로, 잘못 초기화된 Bean이 사용되는 것을 방지합니다. 애플리케이션 시작 시점에 문제를 발견할 수 있어 런타임 오류를 예방할 수 있습니다.
A: 네, 가능합니다. 실행 순서는 @PostConstruct → InitializingBean.afterPropertiesSet() → @Bean(initMethod) 순서입니다. 하지만 일반적으로는 하나의 초기화 방법만 사용하는 것이 코드 가독성에 좋습니다.
A: 생성자 파라미터에 @Value를 사용하면 가능합니다. 하지만 필드에 @Value를 사용한 경우, 생성자 실행 시점에는 아직 주입되지 않았으므로 @PostConstruct에서 사용해야 합니다.
public UserService(@Value("${app.name}") String appName) {
System.out.println(appName); // 정상 동작
}
// ❌ 불가능
@Value("${app.name}")
private String appName;
public UserService() {
System.out.println(appName); // null
}
A: @Lazy로 표시된 Bean은 애플리케이션 시작 시점이 아니라 실제로 처음 사용될 때 초기화됩니다. 즉, 다른 Bean에서 해당 Bean을 주입받거나 ApplicationContext에서 getBean()으로 요청할 때 생성됩니다.
A: Prototype Bean은 요청할 때마다 새로운 인스턴스가 생성되며, 초기화 콜백은 실행되지만 소멸 콜백은 실행되지 않습니다. Spring Container가 Prototype Bean의 소멸을 관리하지 않기 때문에, 리소스 정리는 개발자가 직접 처리해야 합니다.
A: 생성자 주입으로 순환 의존성이 있으면 BeanCurrentlyInCreationException이 발생합니다. 필드 주입이나 Setter 주입을 사용하면 순환 의존성을 해결할 수 있지만, 근본적으로는 설계를 개선하는 것이 좋습니다.
A: 기술적으로는 가능하지만 권장되지 않습니다. 여러 @PostConstruct 메서드가 있을 때 실행 순서는 보장되지 않습니다. 하나의 @PostConstruct 메서드에서 순차적으로 초기화 로직을 실행하는 것이 좋습니다.
A: 네, 가능합니다. @PostConstruct나 afterPropertiesSet() 실행 시점에는 모든 의존성 주입이 완료된 상태이므로 주입받은 Bean을 자유롭게 사용할 수 있습니다. 이것이 초기화 메서드의 주요 목적입니다.
public class UserService {
@Autowired
private UserRepository repository;
@PostConstruct
public void init() {
// repository를 사용 가능
long count = repository.count();
System.out.println("Total users: " + count);
}
}
Spring Bean의 생명주기를 이해하는 것은 Spring Framework를 효과적으로 사용하는 데 필수적입니다. Bean 생성, 의존성 주입, 초기화, 사용, 소멸의 전체 흐름을 정확히 이해하면 애플리케이션의 동작 방식을 예측할 수 있고, 버그를 예방할 수 있습니다.
의존성 주입 방법 중에서는 생성자 주입을 사용하는 것이 Best Practice입니다. 불변성을 보장하고, 순환 참조를 방지하며, 테스트가 용이합니다. 필드 주입이나 Setter 주입이 필요한 경우는 매우 제한적입니다.
초기화 로직은 @PostConstruct를 사용하는 것을 권장합니다. Java 표준이며 Spring에 종속되지 않아 코드의 이식성과 테스트 용이성이 높습니다. InitializingBean은 Spring 프레임워크에 강하게 결합되므로 특별한 이유가 없다면 피하는 것이 좋습니다.
초기화 메서드에서는 의존성 검증, 리소스 로딩, 캐시 초기화 등의 작업을 수행할 수 있습니다. 마찬가지로 @PreDestroy를 사용하여 애플리케이션 종료 시 리소스를 정리하는 것도 중요합니다. 데이터베이스 연결, 파일 핸들, 스레드 풀 등은 반드시 정리해야 메모리 누수를 방지할 수 있습니다.
끝.
'Development > Java' 카테고리의 다른 글
| [Java] System.arraycopy vs Arrays.copyOfRange 차이점 비교 - 성능과 사용법 (0) | 2025.03.25 |
|---|---|
| [Java] Java throw와 throws 차이점 비교 - 예외 처리 핵심 정리 (0) | 2024.07.24 |
| [Java] Java Class 파일 DeCompile (0) | 2020.04.08 |
| [Java] StringBuffer vs StringBuilder (0) | 2019.09.26 |
| [Java] Java Handler 사용법과 메모리 누수 방지 방법 (0) | 2019.09.05 |