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 모드가 추가되었다.
TimeTickService가 FeatureTimeMachineModule에 하드코딩된 결과,
이 모듈을 임포트하는 모든 서비스(dta-wide-api 포함)에서 자동 시작된다.
dta-wide-api의 authJWT는 TimeMachine 가상 시간을 JWT 검증용 clockTimestamp로 사용하므로,
가상 시간이 토큰 만료 시각을 넘어서면 TokenExpiredError가 발생한다.
Goals
dta-wide-api에서TimeTickService가 실행되지 않도록 격리dha-sleep-api에서만 flowing 모드가 정상 동작하도록 보장- 변경 범위를 최소화하여 다른 서비스의 동작에 영향 없음 보장
Non-Goals
authJWT의clockTimestamp로직 변경 — JWT ↔ TimeMachine 분리는 별도 플랜으로dha-metabolic-api의 flowing 지원 — toggleFlow 엔드포인트 없음, 별도 플랜으로- DevToolsModule의 production 환경 가드 추가 — 기존 DebugApiGuard로 충분
- 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-api | auth.module.ts, time-machine.module.ts | ❌ 불필요 |
dha-sleep-api | dev-tools.module.ts 등 7곳 | ✅ flowing 관리 서비스 |
dha-metabolic-api | dev-tools.module.ts 등 5곳 | ⚠️ toggleFlow 엔드포인트 없음 |
해결 방안
TimeTickService를 FeatureTimeMachineModule에서 분리하여 별도 FeatureTimeMachineTickModule을 만들고,
오직 dha-sleep-api의 DevToolsModule에서만 임포트한다.
[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-api | TimeTickService 실행 ❌ | TimeTickService 미실행 ✅ |
dha-sleep-api | TimeTickService 실행 (의도치 않게 여러 곳에서) | DevToolsModule 1곳에서만 실행 ✅ |
dha-metabolic-api | TimeTickService 실행 ❌ | TimeTickService 미실행 (변경 없음) |
분산 락:
TimeTickService는 Redislock:time-tick으로 인스턴스 간 중복 tick을 방지한다. dha-sleep-api 내부에서도 1개 인스턴스만 실제로 tick을 실행한다.
설계 결정
Q1. TimeTickService를 FeatureTimeMachineModule에서 제거 vs 조건부 실행
Context:
FeatureTimeMachineModule은dta-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-api의DevToolsModule은 현재FeatureTimeMachineModule을 임포트하여UserTimeManager,UserClockService를 DI로 사용DevToolsModule을FeatureTimeMachineModule→FeatureTimeMachineTickModule으로 교체하면 DI가 끊길 수 있음
Decision: FeatureTimeMachineTickModule의 exports에 FeatureTimeMachineModule을 포함하여 re-export.
// FeatureTimeMachineTickModule
exports: [FeatureTimeMachineModule]
Consequences:
- ✅
DevToolsModule이FeatureTimeMachineTickModule만 임포트해도UserTimeManager등 모든 exports가 available - ✅
DevToolsModule내부 코드 변경 불필요 (imports 교체만)
Alternatives Considered:
- DevToolsModule에서 FeatureTimeMachineModule과 FeatureTimeMachineTickModule 둘 다 임포트 — 중복 임포트, NestJS가 중복을 허용하지만 의미가 불명확, 채택하지 않음
Rabbit Holes
- ❌
authJWT의clockTimestamp제거 — JWT ↔ TimeMachine 분리는 더 넓은 영향 범위를 가지며 별도 설계 필요. 이 플랜의 범위 초과 - ❌
dha-metabolic-api에FeatureTimeMachineTickModule추가 — toggleFlow 엔드포인트가 없음. 지금 추가하면 사용하지 않는 tick이 실행됨 - ❌
DevToolsModule에 production guard 추가 — DebugApiGuard가 이미 존재. 이 플랜 범위 초과
구현 계획
| 문서 | 내용 | 변경 파일 | 우선순위 |
|---|---|---|---|
| 01-timetick-isolation.md | Phase 1: lib 변경 — TimeTickService 격리 | 1 NEW + 2 EDIT | P0 |
| 02-dha-sleep-apply.md | Phase 2: app 변경 — dha-sleep-api 적용 | 1 EDIT | P0 |
| 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.ts | TimeTickService 로직 자체는 수정하지 않음 |
dha-sleep-api의 다른 모듈 | analytics.module, mcp.module 등 6곳은 FeatureTimeMachineModule을 직접 임포트하므로 DI 영향 없음 |
⚠️ Known Risk — Production TimeTickService 실행:
DevToolsModule은 production에서도 unconditionally 로드된다. 따라서 이 변경 후dha-sleep-apiproduction에서TimeTickService가 실행된다.flowing=true로 전환하려면DebugApiGuard로 보호된 DevTools API를 의도적으로 호출해야 하므로 의도된 동작으로 수용한다.
🚨 배포 전 필수 조치:
flowing=true상태인 유저가 있으면 배포 전에 초기화해야 한다. 99-verification.md의 "DB 즉시 조치" 섹션 참조.
예상 효과
| 시나리오 | Before | After |
|---|---|---|
| flowing=true 유저가 dta-wide-api API 호출 | TokenExpiredError ❌ | 정상 응답 ✅ |
| dha-sleep-api에서 flowing 모드 활성화 | TimeTickService 실행 (중복 위험) | DevToolsModule에서만 실행 ✅ |
| 새 서비스가 FeatureTimeMachineModule 임포트 | TimeTickService 자동 시작 ❌ | TimeTickService 미실행 (안전) ✅ |