본문으로 건너뛰기

003. TimeMachine TimeTickService 격리 — JWT 만료 버그 수정

Status: 📋 계획 수립 작성일: 2026-03-31 담당자: mook@weltcorp.com 우선순위: high 발견 계기: dta-wide-api 0.0.20-rc4(PR #2826) 배포 후 TokenExpiredError: jwt expired 오류 급증 참조 구현: libs/feature/time-machine/src/lib/feature-time-machine.module.ts 연관 문서: timemachine-flowing-jwt-expiry.md


배경

Plan 126 Phase 1(eaff75ed5)에서 TimeMachine flowing 모드가 추가되었다. TimeTickServiceFeatureTimeMachineModule에 하드코딩된 결과, 이 모듈을 임포트하는 모든 서비스(dta-wide-api 포함)에서 자동 시작된다. dta-wide-apiauthJWT는 TimeMachine 가상 시간을 JWT 검증용 clockTimestamp로 사용하므로, 가상 시간이 토큰 만료 시각을 넘어서면 TokenExpiredError가 발생한다.


Goals

  1. dta-wide-api에서 TimeTickService가 실행되지 않도록 격리
  2. dha-sleep-api에서만 flowing 모드가 정상 동작하도록 보장
  3. 변경 범위를 최소화하여 다른 서비스의 동작에 영향 없음 보장

Non-Goals

  1. authJWTclockTimestamp 로직 변경 — JWT ↔ TimeMachine 분리는 별도 플랜으로
  2. dha-metabolic-api의 flowing 지원 — toggleFlow 엔드포인트 없음, 별도 플랜으로
  3. DevToolsModule의 production 환경 가드 추가 — 기존 DebugApiGuard로 충분
  4. DB에서 flowing=true 유저 초기화 — 운영자가 직접 처리

문제 분석

현상

JWT verification failed for kid: e57432a3db...
TokenExpiredError: jwt expired
at JwkManagementService.verifyWithJwk
at AccessTokenService.authJWT
at ValidateTokenHandler.execute
at JwtAuthGuard.canActivate

근본 원인

FeatureTimeMachineModule (libs/feature/time-machine)
└── services = [UserTimeManager, RollbackCoordinator,
UserClockService, TimeTickService] ← 하드코딩

├── dta-wide-api/auth.module.ts → FeatureTimeMachineModule 임포트
│ → TimeTickService.onModuleInit() 실행 ← 의도치 않음
│ → 10초마다 flowing=true 유저의 virtualTime += 10,000ms

└── authJWT
→ clockTimestamp = virtualTime (점점 증가)
→ verifyWithJwk(token, jwk, clockTimestamp)
→ virtualTime > token.exp → TokenExpiredError ❌

기존 현황

서비스FeatureTimeMachineModule 임포트 위치TimeTickService 의도
dta-wide-apiauth.module.ts, time-machine.module.ts❌ 불필요
dha-sleep-apidev-tools.module.ts 등 7곳✅ flowing 관리 서비스
dha-metabolic-apidev-tools.module.ts 등 5곳⚠️ toggleFlow 엔드포인트 없음

해결 방안

TimeTickServiceFeatureTimeMachineModule에서 분리하여 별도 FeatureTimeMachineTickModule을 만들고, 오직 dha-sleep-apiDevToolsModule에서만 임포트한다.

[Before]
FeatureTimeMachineModule
└── providers: [..., TimeTickService] ← 모든 임포터에서 자동 시작

[After]
FeatureTimeMachineModule
└── providers: [UserTimeManager, RollbackCoordinator, UserClockService]

FeatureTimeMachineTickModule ← NEW
└── imports: [FeatureTimeMachineModule]
└── providers: [TimeTickService]
└── exports: [FeatureTimeMachineModule] ← re-export for DI 연결

dha-sleep-api/DevToolsModule
└── imports: [CqrsModule, FeatureTimeMachineTickModule] ← CHANGED

서비스별 영향

서비스변경 전변경 후
dta-wide-apiTimeTickService 실행 ❌TimeTickService 미실행 ✅
dha-sleep-apiTimeTickService 실행 (의도치 않게 여러 곳에서)DevToolsModule 1곳에서만 실행 ✅
dha-metabolic-apiTimeTickService 실행 ❌TimeTickService 미실행 (변경 없음)

분산 락: TimeTickService는 Redis lock:time-tick으로 인스턴스 간 중복 tick을 방지한다. dha-sleep-api 내부에서도 1개 인스턴스만 실제로 tick을 실행한다.


설계 결정

Q1. TimeTickService를 FeatureTimeMachineModule에서 제거 vs 조건부 실행

Context:

  • FeatureTimeMachineModuledta-wide-api, dha-sleep-api, dha-metabolic-api 등 여러 서비스에서 임포트됨
  • TimeTickService는 현재 모듈 providers에 하드코딩(line 60)

Decision: FeatureTimeMachineModule에서 완전히 제거하고 별도 FeatureTimeMachineTickModule로 분리.

Consequences:

  • ✅ 임포터가 tick 실행 여부를 명시적으로 선택
  • ✅ dta-wide-api, dha-metabolic-api는 코드 변경 없이 tick에서 분리
  • ⚠️ 새 서비스가 flowing을 지원하려면 FeatureTimeMachineTickModule을 명시적으로 추가해야 함

Alternatives Considered:

  • 환경변수 조건부 실행 (process.env.ENABLE_TIME_TICK) — 서비스마다 env 설정 필요, 배포 복잡도 증가, 채택하지 않음
  • dta-wide-api에서만 TimeTickService를 providers에서 override — 서비스 레이어에서 라이브러리 내부를 패치하는 패턴, 유지보수 어려움, 채택하지 않음

Q2. FeatureTimeMachineTickModule에서 FeatureTimeMachineModule을 re-export해야 하는가

Context:

  • dha-sleep-apiDevToolsModule은 현재 FeatureTimeMachineModule을 임포트하여 UserTimeManager, UserClockService를 DI로 사용
  • DevToolsModuleFeatureTimeMachineModuleFeatureTimeMachineTickModule으로 교체하면 DI가 끊길 수 있음

Decision: FeatureTimeMachineTickModuleexportsFeatureTimeMachineModule을 포함하여 re-export.

// FeatureTimeMachineTickModule
exports: [FeatureTimeMachineModule]

Consequences:

  • DevToolsModuleFeatureTimeMachineTickModule만 임포트해도 UserTimeManager 등 모든 exports가 available
  • DevToolsModule 내부 코드 변경 불필요 (imports 교체만)

Alternatives Considered:

  • DevToolsModule에서 FeatureTimeMachineModule과 FeatureTimeMachineTickModule 둘 다 임포트 — 중복 임포트, NestJS가 중복을 허용하지만 의미가 불명확, 채택하지 않음

Rabbit Holes

  • authJWTclockTimestamp 제거 — JWT ↔ TimeMachine 분리는 더 넓은 영향 범위를 가지며 별도 설계 필요. 이 플랜의 범위 초과
  • dha-metabolic-apiFeatureTimeMachineTickModule 추가 — toggleFlow 엔드포인트가 없음. 지금 추가하면 사용하지 않는 tick이 실행됨
  • DevToolsModule에 production guard 추가 — DebugApiGuard가 이미 존재. 이 플랜 범위 초과

구현 계획

문서내용변경 파일우선순위
01-timetick-isolation.mdPhase 1: lib 변경 — TimeTickService 격리1 NEW + 2 EDITP0
02-dha-sleep-apply.mdPhase 2: app 변경 — dha-sleep-api 적용1 EDITP0
99-verification.md빌드/기능 검증P0
총계1 NEW + 3 EDIT

영향 범위

변경 대상 파일

libs/feature/time-machine/
└── src/
├── index.ts [EDIT] FeatureTimeMachineTickModule export 추가
└── lib/
├── feature-time-machine.module.ts [EDIT] TimeTickService 제거
└── feature-time-machine-tick.module.ts [NEW] TimeTickService 전용 래퍼 모듈

apps/dha-sleep-api/
└── src/app/dev-tools/
└── dev-tools.module.ts [EDIT] FeatureTimeMachineTickModule으로 교체

변경하지 않는 것

대상이유
apps/dta-wide-api/**FeatureTimeMachineModule 임포트 유지, TimeTickService가 제거되므로 코드 수정 불필요
apps/dha-metabolic-api/**toggleFlow 엔드포인트 없음, 현재 flowing 미지원
libs/feature/auth/**JWT clockTimestamp 로직 유지 — 별도 플랜
libs/feature/time-machine/src/lib/application/services/time-tick.service.tsTimeTickService 로직 자체는 수정하지 않음
dha-sleep-api의 다른 모듈analytics.module, mcp.module 등 6곳은 FeatureTimeMachineModule을 직접 임포트하므로 DI 영향 없음

⚠️ Known Risk — Production TimeTickService 실행: DevToolsModule은 production에서도 unconditionally 로드된다. 따라서 이 변경 후 dha-sleep-api production에서 TimeTickService가 실행된다. flowing=true로 전환하려면 DebugApiGuard로 보호된 DevTools API를 의도적으로 호출해야 하므로 의도된 동작으로 수용한다.

🚨 배포 전 필수 조치: flowing=true 상태인 유저가 있으면 배포 전에 초기화해야 한다. 99-verification.md의 "DB 즉시 조치" 섹션 참조.


예상 효과

시나리오BeforeAfter
flowing=true 유저가 dta-wide-api API 호출TokenExpiredError ❌정상 응답 ✅
dha-sleep-api에서 flowing 모드 활성화TimeTickService 실행 (중복 위험)DevToolsModule에서만 실행 ✅
새 서비스가 FeatureTimeMachineModule 임포트TimeTickService 자동 시작 ❌TimeTickService 미실행 (안전) ✅