본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
2장 : Bean과 의존성 주입

ApplicationContext와 Bean 생명주기

앞 장에서 SpringApplication.run()이 시작한 객체 저장소가 ApplicationContext입니다. 컨텍스트는 Bean 정의를 읽고, 객체를 만들고, 생성자 의존성을 연결한 뒤 준비된 애플리케이션을 외부 요청에 공개합니다. 프로세스가 종료될 때는 자신이 관리한 Bean의 종료 콜백도 호출합니다.

이 생명주기를 알면 생성자에서 아직 준비되지 않은 외부 시스템을 호출하거나, 종료할 때 자원을 반납하지 않는 코드를 피할 수 있습니다. Bean은 단순한 전역 객체가 아니라 컨테이너가 생성부터 폐기까지 책임지는 객체입니다.


Bean은 컨테이너 안에서 생명주기를 가진다

컴포넌트 스캔과 @Bean 메서드는 먼저 Bean 정의를 등록합니다. 컨테이너는 이 정의를 토대로 생성자 인자를 해결하고 실제 인스턴스를 만듭니다. 모든 필수 의존성이 연결된 뒤 초기화 콜백을 실행하며, 컨텍스트가 닫힐 때는 역순에 가까운 흐름으로 종료 콜백을 호출합니다.

기본 scope인 singleton은 컨텍스트마다 한 인스턴스를 공유한다는 뜻이지, JVM 전체에 하나뿐이라는 뜻은 아닙니다. 같은 애플리케이션 안에서도 별도 컨텍스트를 만들면 각 컨텍스트가 자기 singleton을 관리합니다.


명시적 구성으로 Clock과 서비스를 조립한다

직접 만들기 어려운 외부 타입 Clock은 구성 클래스의 @Bean으로 등록하고, 우리 서비스는 @Service로 발견하게 합니다. 어느 방식이든 컨테이너에 들어온 뒤에는 같은 Bean으로 주입됩니다.

src/main/java/com/andongmin/studylog/TimeConfiguration.java
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();
    }
}
src/main/java/com/andongmin/studylog/TodayService.java
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);
    }
}
src/main/java/com/andongmin/studylog/LifecycleProbe.java
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:run
Lifecycle 확인 결과
Clock Bean이 하나 등록되고 TodayService 생성자에 주입되어야 합니다.

같은 타입의 Clock Bean을 이름만 달리해 하나 더 등록하면 TodayService 생성자에서 어느 객체를 주입할지 결정할 수 없어 시작이 실패합니다. 이 실패는 다음 절에서 다룰 "필수 협력자를 생성자에 드러내기"와 후보 선택 규칙으로 이어집니다.


이제 서비스가 구체 저장 기술이 아니라 필요한 기능의 계약에 의존하도록 방향을 정합니다.