Skip to content

Latest commit

 

History

History
125 lines (96 loc) · 9.42 KB

File metadata and controls

125 lines (96 loc) · 9.42 KB

04. 라이프사이클·스케줄링·비동기

무엇을 / 왜

빈은 "생성-주입-소멸"만으로 충분하지 않을 때가 있다. 메시지 리스너 컨테이너나 내장 서버는 컨테이너가 완전히 떠오른 뒤에 시작해야 하고, 내려갈 때는 역순으로 멈춰야 한다. 또 반복 작업(@Scheduled)이나 백그라운드 실행(@Async)도 컨테이너의 통제 아래 두고 싶다.

이 챕터는 세 가지를 묶는다. (1) Lifecycle/SmartLifecycle로 표현되는 시작·종료 순서 제어, (2) @ScheduledTrigger/TaskScheduler로 실행되는 경로, (3) @Async가 AOP 프록시로 비동기화되는 경로. 셋 다 "어노테이션/인터페이스를 컨테이너가 감지해 인프라에 연결한다"는 동일한 패턴을 공유한다.

핵심 타입

 라이프사이클                          스케줄링                         비동기
 ───────────                          ────────                         ──────
 Lifecycle                            TaskScheduler                    @Async
   start()/stop()/isRunning()           schedule(task, Trigger)          ↓ AOP
        ▲                                schedule(task, Instant)        AsyncAnnotationBeanPostProcessor
        │ extends                        scheduleAtFixedRate/Delay        └ AsyncAnnotationAdvisor
 SmartLifecycle (+ Phased)                   ▲                              └ AnnotationAsyncExecutionInterceptor
   isAutoStartup()                           │ 다음 실행시각 계산              → AsyncTaskExecutor로 위임
   getPhase()  stop(Runnable)           Trigger.nextExecution(ctx)
   isPauseable()                          ├ CronTrigger (CronExpression)
        ▲                                 └ PeriodicTrigger (고정 주기)
        │ 컨테이너가 관리
 LifecycleProcessor (= SmartLifecycle)
   onRefresh()/onClose()/onPause()/onRestart()
        │ 기본 구현
 DefaultLifecycleProcessor
   - phase 별로 묶어 startBeans/stopBeans

LifecycleProcessorlifecycleProcessor라는 이름의 빈이 있으면 그것을, 없으면 DefaultLifecycleProcessor를 쓴다. 이것 자체가 컨텍스트의 start()/stop()/close() 호출을 받아 개별 Lifecycle 빈으로 위임하는 오케스트레이터다.

동작 흐름 1: SmartLifecycle 시작/종료 순서

refresh()의 마지막(12단계 finishRefresh)에서 lifecycleProcessor.onRefresh()가 호출된다.

onRefresh()
   └ startBeans(autoStartupOnly = true)
        1. 모든 Lifecycle 빈 수집
        2. SmartLifecycle 중 isAutoStartup()==true 인 것만 대상
        3. getPhase() 값으로 그룹핑 (LifecycleGroup)
        4. phase "낮은 값 → 높은 값" 순으로 그룹별 start()
              (같은 phase 안에서는 depends-on 순서 보장)

종료(close()onClose())는 stopBeans()정확히 역순(높은 phase → 낮은 phase)으로 멈춘다. 핵심 규칙:

  • SmartLifecycle.getPhase() 기본값은 Integer.MAX_VALUE — 가장 늦게 시작, 가장 먼저 종료. 일반 Lifecycle은 phase 0으로 간주된다.
  • stop(Runnable callback) 변형은 비동기 종료를 지원한다. 같은 phase의 컴포넌트들을 동시에 멈추되, 각자 종료가 끝나면 콜백을 실행해야 한다. DefaultLifecycleProcessor는 phase별로 CountDownLatch와 타임아웃(timeoutPerShutdownPhase)을 두어, 한 컴포넌트가 늦어도 전체 종료가 무한정 막히지 않게 한다.
  • 7.0부터 onPause()/onRestart()isPauseable()이 추가되어, 컨텍스트를 닫지 않고 일시정지/재시작할 수 있다(CRaC 체크포인트 등과 연계).
phase 순서 예시
  시작:  [-100: 인프라] → [0: 일반 Lifecycle] → [MAX: 스케줄러/리스너]
  종료:  [MAX] → [0] → [-100]   (역순)

동작 흐름 2: @Scheduled 등록과 발화

@EnableSchedulingSchedulingConfigurationScheduledAnnotationBeanPostProcessor(SABPP) 등록. SABPP는 BeanPostProcessor이자 SmartLifecycle이며, 다음처럼 동작한다.

1) 각 빈 초기화 후: postProcessAfterInitialization(bean)
      └ @Scheduled / @Schedules 메서드를 찾아 processScheduled() 호출
           └ cron/fixedRate/fixedDelay에 따라 Task 종류 결정,
             registrar(ScheduledTaskRegistrar)에 "보류 작업"으로 등록
             (아직 실제 스케줄링 X — TaskScheduler가 아직 없을 수 있으므로)

2) ContextRefreshedEvent 수신 시: finishRegistration()
      └ TaskScheduler 확정 (명시 빈 → SchedulingConfigurer 콜백 → 없으면 단일스레드 기본)
      └ registrar.afterPropertiesSet() 가 보류 작업을 실제로 스케줄링

작업은 Trigger로 추상화된다. cron이면 CronTrigger, 고정 주기면 PeriodicTrigger. TaskScheduler.schedule(task, trigger)는 실행이 끝날 때마다 trigger.nextExecution(triggerContext)를 물어 다음 실행 시각을 계산하고 그때 다시 깨운다.

ThreadPoolTaskScheduler            CronTrigger
        │ schedule(task, trigger)       │
        ▼                               │
   1회 실행 ── 완료 ──▶ trigger.nextExecution(lastCompletion) ──▶ 다음 시각 t
        ▲                                                            │
        └────────────────── t 시점에 재실행 ◀───────────────────────┘

CronExpression(scheduling/support)은 cron 필드를 비트셋(BitsCronField)으로 파싱해, 주어진 시각으로부터 다음 매칭 시각을 효율적으로 계산한다. TaskScheduler가 없을 때를 대비한 TaskSchedulerRouter는 적절한 스케줄러를 런타임에 골라준다.

동작 흐름 3: @Async 프록시

@EnableAsyncAsyncConfigurationSelectorAdviceMode에 따라 ProxyAsyncConfiguration(기본) 또는 AspectJ 설정을 import → AsyncAnnotationBeanPostProcessor 등록.

AsyncAnnotationBeanPostProcessor (AbstractAdvisingBeanPostProcessor)
   setBeanFactory() 시점에 AsyncAnnotationAdvisor 생성
        │  이 Advisor의 포인트컷 = @Async 붙은 클래스/메서드
        ▼
   대상 빈을 AOP 프록시로 감쌈
        │
        ▼  프록시 메서드 호출 시
   AnnotationAsyncExecutionInterceptor.invoke()
        ├ 해당 메서드의 Executor 결정 (@Async("qualifier") → 이름으로 빈 조회,
        │   없으면 기본 AsyncConfigurer/TaskExecutor)
        └ executor.submit( () -> 실제 메서드 호출 )
              반환타입이 void / Future / CompletableFuture 면 그대로 비동기

@Async는 별도 BPP가 아니라 AOP advisor로 구현된다. 호출자는 즉시 반환받고, 실제 작업은 풀의 다른 스레드에서 돈다. 예외는 void 반환이면 AsyncUncaughtExceptionHandler로, Future면 future를 통해 전달된다.

핵심 메서드

  • DefaultLifecycleProcessor.startBeans()/stopBeans() — Lifecycle 빈을 phase로 그룹핑해 정렬 시작/역순 종료. stopBeans()는 phase별 latch와 타임아웃으로 비동기 종료를 조율한다.
  • ScheduledAnnotationBeanPostProcessor.processScheduled()@Scheduled의 cron/fixedRate/fixedDelay/initialDelay를 해석해 적절한 Task로 변환하고 registrar에 보류 등록. 리액티브/코틀린 suspend 메서드는 ScheduledAnnotationReactiveSupport로 위임된다.
  • Trigger.nextExecution(TriggerContext) — 다음 실행 시각을 반환하는 단일 추상화. cron과 고정주기를 같은 인터페이스 뒤로 통일한다. null을 반환하면 더 이상 발화하지 않는다.
  • AnnotationAsyncExecutionInterceptor.invoke()@Async 메서드 호출을 가로채 Executor로 제출하고, 반환 타입에 맞춰 Future를 돌려준다.

설계 포인트 / 확장점

  • 순서 보장의 일원화: 시작/종료 순서는 오직 Phased.getPhase()와 depends-on으로 결정된다. 스케줄러·메시지 리스너가 일반 빈보다 늦게 시작/먼저 종료되도록 기본 phase가 MAX_VALUE인 것이 그 의도다.
  • "보류 후 일괄 활성화": SABPP가 빈 초기화 시점엔 작업만 모으고 ContextRefreshedEvent에서야 스케줄링을 시작하는 것은, 모든 TaskScheduler/설정이 준비된 뒤에만 실제 실행을 켜기 위함이다.
  • AOP로서의 @Async: 비동기를 BeanPostProcessor가 아니라 advisor로 구현해, 트랜잭션·캐시 같은 다른 advisor와 동일한 프록시 체인에 자연스럽게 합류한다.
  • 확장점: SchedulingConfigurer(registrar에 직접 작업 등록), AsyncConfigurer(기본 Executor·예외 핸들러 지정), 커스텀 Trigger, LifecycleProcessor 빈 교체, taskScheduler/taskExecutor 빈 정의.

정리

Lifecycle/SmartLifecycle은 phase 기반의 정렬 시작·역순 종료를 제공하고, DefaultLifecycleProcessor가 이를 refresh/close 시점에 오케스트레이션한다. @Scheduled는 SABPP가 메서드를 Task로 모았다가 컨텍스트 준비 후 Trigger+TaskScheduler로 실제 발화시키며, @AsyncAsyncAnnotationBeanPostProcessor가 만든 AOP advisor가 호출을 Executor로 넘겨 비동기화한다. 세 기능 모두 "감지 → 인프라 빈 연결 → 컨테이너 라이프사이클에 동기화"라는 같은 골격을 따른다.