ApplicationContext와 Bean 생명주기
앞 장에서 SpringApplication.run()이 시작한 객체 저장소가 ApplicationContext입니다. 컨텍스트는 Bean 정의를 읽고, 객체를 만들고, 생성자 의존성을 연결한 뒤 준비된 애플리케이션을 외부 요청에 공개합니다. 프로세스가 종료될 때는 자신이 관리한 Bean의 종료 콜백도 호출합니다.
이 생명주기를 알면 생성자에서 아직 준비되지 않은 외부 시스템을 호출하거나, 종료할 때 자원을 반납하지 않는 코드를 피할 수 있습니다. Bean은 단순한 전역 객체가 아니라 컨테이너가 생성부터 폐기까지 책임지는 객체입니다.
Bean은 컨테이너 안에서 생명주기를 가진다
컴포넌트 스캔과 @Bean 메서드는 먼저 Bean 정의를 등록합니다. 컨테이너는 이 정의를 토대로 생성자 인자를 해결하고 실제 인스턴스를 만듭니다. 모든 필수 의존성이 연결된 뒤 초기화 콜백을 실행하며, 컨텍스트가 닫힐 때는 역순에 가까운 흐름으로 종료 콜백을 호출합니다.
기본 scope인 singleton은 컨텍스트마다 한 인스턴스를 공유한다는 뜻이지, JVM 전체에 하나뿐이라는 뜻은 아닙니다. 같은 애플리케이션 안에서도 별도 컨텍스트를 만들면 각 컨텍스트가 자기 singleton을 관리합니다.
명시적 구성으로 Clock과 서비스를 조립한다
직접 만들기 어려운 외부 타입 Clock은 구성 클래스의 @Bean으로 등록하고, 우리 서비스는 @Service로 발견하게 합니다. 어느 방식이든 컨테이너에 들어온 뒤에는 같은 Bean으로 주입됩니다.
package com.andongmin.studylog;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class TimeConfiguration {
@Bean
Clock studyClock() {
return Clock.systemUTC();
}
}package com.andongmin.studylog;
import java.time.Clock;
import java.time.LocalDate;
import org.springframework.stereotype.Service;
@Service
public class TodayService {
private final Clock clock;
public TodayService(Clock clock) {
this.clock = clock;
}
public LocalDate today() {
return LocalDate.now(clock);
}
}package com.andongmin.studylog;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class LifecycleProbe {
@PostConstruct
void started() {
System.out.println("lifecycle=started");
}
@PreDestroy
void stopping() {
System.out.println("lifecycle=stopping");
}
}TodayService는 시간을 직접 생성하지 않으므로 테스트에서 고정 Clock으로 바꿀 수 있습니다. LifecycleProbe는 교육용으로 콜백 순서를 눈에 보이게 만들지만, 실제 자원 정리 코드는 종료가 여러 원인으로 호출될 수 있음을 고려해 반복 호출에도 안전하게 작성해야 합니다.
시작과 종료 콜백을 로그로 검증한다
먼저 컴파일로 타입 연결을 확인한 뒤 서버를 실행합니다. lifecycle=started가 보이면 Ctrl+C로 정상 종료해 lifecycle=stopping까지 확인합니다.
./mvnw -q -DskipTests compile
./mvnw -q spring-boot:runClock Bean이 하나 등록되고 TodayService 생성자에 주입되어야 합니다.같은 타입의 Clock Bean을 이름만 달리해 하나 더 등록하면 TodayService 생성자에서 어느 객체를 주입할지 결정할 수 없어 시작이 실패합니다. 이 실패는 다음 절에서 다룰 "필수 협력자를 생성자에 드러내기"와 후보 선택 규칙으로 이어집니다.
이제 서비스가 구체 저장 기술이 아니라 필요한 기능의 계약에 의존하도록 방향을 정합니다.