TimeMachine 도메인 모델
1. 개요
TimeMachine 도메인은 시스템 내에서 시간 관리를 담당하는 핵심 컴포넌트입니다. 개발 및 테스트 환경에서 사용자가 특정 시간을 지정하여 기능을 검증할 수 있도록 하며, 시간에 민감한 비즈니스 로직을 테스트하는 데 활용됩니다.
데이터 저장 규칙
- 개인정보 관련 데이터는 private 스키마에 저장되어야 합니다.
- 비개인정보 데이터는 time_machine 스키마에 저장되어야 합니다.
- 개인정보 포함 여부에 따라 위 규칙을 우선적으로 적용합니다.
- TimeMachine 도메인에서는 VirtualTime, TimeChangeRecord, RollbackRecord, TimeChangeEvent 등 사용자 ID와 연결된 데이터는 모두 개인정보로 취급하여 private 스키마에 저장합니다.
- 사용자와 연결되지 않은 설정 데이터(CacheConfig 등)만 time_machine 스키마에 저장합니다.
2. 엔티티 관계도 (ERD)
3. 엔티티
3.1 VirtualTime
interface VirtualTime {
id: string; // 고유 식별자
userId: string; // 소유 사용자 ID
currentTime: Date; // 현재 가상 시간
lastChangeTimestamp: Date; // 마지막 변경 시간
changedById: string; // 마지막 변경자 ID
changeReason: string; // 변경 사유
active: boolean; // 활성화 상태
createdAt: Date; // 생성 시간
updatedAt: Date; // 수정 시간
}
3.2 TimeChangeRecord
interface TimeChangeRecord {
id: string; // 고유 식별자
virtualTimeId: string; // 가상 시간 ID
previousTime: Date; // 이전 시간
newTime: Date; // 새 시간
changeType: ChangeType; // 변경 유형
timestamp: Date; // 변경 시간
changedById: string; // 변경자 ID
reason: string; // 변경 사유
createdAt: Date; // 생성 시간
}
enum ChangeType {
SET = 'SET', // 직접 설정
MOVE_FORWARD = 'MOVE_FORWARD', // 미래로 이동
MOVE_BACKWARD = 'MOVE_BACKWARD', // 과거로 이동
RESET = 'RESET' // 실제 시간으로 재설정
}
3.3 RollbackRecord
interface RollbackRecord {
id: string; // 고유 식별자
timeChangeId: string; // 시간 변경 기록 ID
affectedEntityType: string; // 영향받은 엔티티 유형
affectedEntityId: string; // 영향받은 엔티티 ID
rollbackAction: RollbackAction; // 롤백 액션
rollbackTimestamp: Date; // 롤백 시간
createdAt: Date; // 생성 시간
}
enum RollbackAction {
HIDE = 'HIDE', // 숨김 처리
PERMANENT_DELETE = 'PERMANENT_DELETE', // 영구 삭제
MARK_INVALID = 'MARK_INVALID' // 무효 표시
}
3.4 TimeChangeEvent
interface TimeChangeEvent {
id: string; // 고유 식별자
timeChangeId: string; // 시간 변경 기록 ID
previousDate: Date; // 이전 날짜
newDate: Date; // 새 날짜
timestamp: Date; // 이벤트 발생 시간
changedById: string; // 변경자 ID
metadata: Record<string, any>; // 메타데이터
publishStatus: PublishStatus; // 발행 상태
publishedAt?: Date; // 발행 시간
publishErrorMessage?: string; // 발행 오류 메시지
createdAt: Date; // 생성 시간
}
enum PublishStatus {
PENDING = 'PENDING', // 대기 중
PUBLISHED = 'PUBLISHED', // 발행됨
FAILED = 'FAILED', // 실패
RETRYING = 'RETRYING' // 재시도 중
}
3.5 User
interface User {
id: string; // 고유 식별자
name: string; // 사용자 이름
createdAt: Date; // 생성 시간
updatedAt: Date; // 수정 시간
}
4. 값 객체
4.1 Duration
interface Duration {
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
4.2 TimeChangePayload
interface TimeChangePayload {
newTime: Date; // 변경할 새 시간
changeType: ChangeType; // 변경 유형
reason: string; // 변경 사유
changedById: string; // 변경자 ID
}
4.3 DateChangeEventPayload
interface DateChangeEventPayload {
previousDate: Date; // 이전 날짜
newDate: Date; // 새 날짜
timestamp: Date; // 이벤트 발생 시간
changedById: string; // 변경자 ID
reason: string; // 변경 사유
metadata?: Record<string, any>; // 추가 메타데이터
}
4.4 RollbackOptions
interface RollbackOptions {
entityTypes: string[]; // 롤백할 엔티티 유형 목록
defaultAction: RollbackAction; // 기본 롤백 액션
preserveLogs: boolean; // 로그 보존 여부
}
5. 집계 (Aggregates)
-
VirtualTime Aggregate
- Root: VirtualTime
- Entities: TimeChangeRecord
- Value Objects: Duration, TimeChangePayload
-
RollbackData Aggregate
- Root: RollbackRecord
- Value Objects: RollbackOptions
-
TimeChangeEvent Aggregate
- Root: TimeChangeEvent
- Value Objects: DateChangeEventPayload
6. 도메인 서비스
6.1 VirtualTimeService
interface VirtualTimeService {
getCurrentTime(userId: string): Promise<Date>;
getUserVirtualTime(userId: string): Promise<VirtualTime>;
setVirtualTime(userId: string, newTime: Date, changedById: string, reason: string): Promise<VirtualTime>;
moveTimeForward(userId: string, duration: Duration, changedById: string, reason: string): Promise<VirtualTime>;
moveTimeBackward(userId: string, duration: Duration, changedById: string, reason: string): Promise<VirtualTime>;
resetToRealTime(userId: string, changedById: string, reason: string): Promise<VirtualTime>;
enableTimeMachine(userId: string, changedById: string, reason?: string): Promise<VirtualTime>;
disableTimeMachine(userId: string, changedById: string, reason?: string): Promise<VirtualTime>;
getTimeChangeHistory(userId: string): Promise<TimeChangeRecord[]>;
isDateChanged(previousTime: Date, newTime: Date): boolean;
listAllUserVirtualTimes(page?: number, limit?: number): Promise<{ items: VirtualTime[], totalCount: number }>;
}
6.2 DataRollbackService
interface DataRollbackService {
rollbackFutureData(fromTime: Date, timeChangeId: string, options?: RollbackOptions): Promise<RollbackRecord[]>;
getRollbackRecords(timeChangeId: string): Promise<RollbackRecord[]>;
getRollbackStatistics(timeChangeId: string): Promise<RollbackStatistics>;
}
interface RollbackStatistics {
totalEntities: number;
entitiesByType: Record<string, number>;
actionCounts: Record<RollbackAction, number>;
}
6.3 EventPublishService
interface EventPublishService {
publishDateChangeEvent(payload: DateChangeEventPayload): Promise<TimeChangeEvent>;
retryFailedEvents(): Promise<void>;
getEventStatus(eventId: string): Promise<PublishStatus>;
getEventsByTimeChangeId(timeChangeId: string): Promise<TimeChangeEvent[]>;
}
6.4 TimeSyncService
interface TimeSyncService {
synchronizeWithMobileApp(userId: string): Promise<boolean>;
synchronizeWithBackend(userId: string): Promise<boolean>;
getTimeSyncStatus(userId: string): Promise<TimeSyncStatus>;
broadcastTimeChange(userId: string, newTime: Date): Promise<void>;
}
interface TimeSyncStatus {
userId: string;
lastSyncedAt: Date;
syncSuccessful: boolean;
syncError?: string;
clientTimeDrift: number; // milliseconds
}
6.5 InternalEventHandlerService
interface InternalEventHandlerService {
handleTimeMachineEnabled(event: TimeMachineEnabledEvent): Promise<void>;
handleTimeMachineDisabled(event: TimeMachineDisabledEvent): Promise<void>;
invalidateUserCaches(userId: string): Promise<void>;
triggerDataCleanup(userId: string, cutoffTime: Date): Promise<boolean>;
}
7. 도메인 이벤트
7.1 가상 시간 관련 이벤트
interface VirtualTimeCreatedEvent {
virtualTimeId: string;
initialTime: Date;
createdById: string;
timestamp: Date;
}
interface VirtualTimeChangedEvent {
virtualTimeId: string;
previousTime: Date;
newTime: Date;
changeType: ChangeType;
changedById: string;
reason: string;
timestamp: Date;
}
interface VirtualTimeDateChangedEvent {
virtualTimeId: string;
previousDate: Date;
newDate: Date;
changedById: string;
reason: string;
timestamp: Date;
}
interface VirtualTimeResetEvent {
virtualTimeId: string;
previousTime: Date;
resetToTime: Date;
changedById: string;
reason: string;
timestamp: Date;
}
interface TimeMachineEnabledEvent {
userId: string;
enabledBy: string;
initialVirtualTime: string;
reason?: string;
enabledAt: Date;
}
interface TimeMachineDisabledEvent {
userId: string;
disabledBy: string;
finalVirtualTime: string;
resetToRealTime: string;
reason?: string;
disabledAt: Date;
}
7.2 데이터 롤백 관련 이벤트
interface FutureDataRollbackStartedEvent {
timeChangeId: string;
fromTime: Date;
options: RollbackOptions;
timestamp: Date;
}
interface FutureDataRollbackCompletedEvent {
timeChangeId: string;
totalEntitiesRolledBack: number;
entitiesByType: Record<string, number>;
duration: number; // milliseconds
timestamp: Date;
}
interface FutureDataRollbackFailedEvent {
timeChangeId: string;
reason: string;
affectedEntityType?: string;
affectedEntityId?: string;
timestamp: Date;
}
7.3 이벤트 발행 관련 이벤트
interface TimeChangeEventPublishedEvent {
eventId: string;
timeChangeId: string;
topic: string;
publishedAt: Date;
timestamp: Date;
}
interface TimeChangeEventPublishFailedEvent {
eventId: string;
timeChangeId: string;
reason: string;
retryCount: number;
timestamp: Date;
}
interface TimeChangeEventRetryScheduledEvent {
eventId: string;
timeChangeId: string;
retryCount: number;
nextRetryAt: Date;
timestamp: Date;
}
8. 도메인 규칙
본 문서에서는 모델 구조에 대한 설명에 중점을 두고 있으며, 주요 규칙 카테고리는 다음과 같습니다:
-
가상 시간 관리 규칙:
- 각 사용자는 독립적인 자신만의 가상 시간을 가짐
- 한 사용자의 가상 시간 변경이 다른 사용자에게 영향을 미치지 않음
- 사용자당 한 번에 하나의 활성화된 가상 시간만 존재할 수 있음
- 가상 시간 변경 시 변경 사유는 필수적으로 기록되어야 함
- 가상 시간 변경은 IAM 도메인에서 관리하는 적절한 권한(예: 'tester' 그룹 또는 역할)을 가진 사용자만 가능함
- 가상 시간은 유효한 날짜/시간 형식이어야 함
- TimeMachine 활성화/비활성화 시 internal event가 발생하여 캐시 무효화 등의 후속 처리가 자동으로 수행됨
- TimeMachine 비활성화 시 가상 시간은 현재 실제 시간으로 재설정되며, 해당 시점 이후의 데이터는 정리됨
-
데이터 롤백 규칙:
- 과거 시점으로 이동 시 이동된 시점 이후의 데이터는 존재하지 않아야 함
- 롤백된 데이터는 복구하지 않고 영구적으로 제거함
- 롤백 작업은 원자적으로 수행되어야 함 (부분적 롤백 허용하지 않음)
- 데이터 롤백은 트랜잭션으로 처리되어야 함
-
이벤트 발행 규칙:
- 날짜가 변경된 경우에만 GCP Pub/Sub으로 이벤트 발행
- 이벤트 발행 실패 시 지수 백오프 방식으로 재시도
- 일정 횟수 이상 재시도 실패 시 관리자에게 알림
- 모든 이벤트는 고유 식별자를 가짐
- Internal event는 시스템 내부 처리용으로 사용되며, cross-domain event와 구분됨
- TimeMachine 상태 변경 시 internal event를 통해 캐시 무효화, 데이터 정리 등이 비동기적으로 처리됨
-
시간 동기화 규칙:
- Mobile App과 Backend는 항상 동일한 가상 시간 참조
- 시간 조회는 캐시를 통해 빠르게 응답
- 캐시 무효화는 가상 시간 변경 시 자동으로 수행
-
데이터 개인정보 처리 규칙:
- 개인정보 데이터는 반드시 private 스키마에 저장
- 비개인정보 데이터는 access_code 스키마에 저장
- 스키마 간 데이터 조인 시 개인정보 보호 정책 준수
- 개인정보 데이터의 로깅, 캐싱 금지
- 개인정보 관련 로그는 익명화 처리 필수
- 개인정보 관련 작업은 감사 로그에 기록되어야 함
9. 데이터베이스 스키마 (Prisma)
스키마 구분 원칙
- 개인정보 관련 데이터:
private스키마에 저장 - 비개인정보 데이터:
time_machine스키마에 저장 - 도메인 특화 데이터:
time_machine스키마에 저장 (개인정보 포함 여부에 따라 위 규칙 우선 적용)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["time_machine", "private", "access_code"]
}
// 변경 유형 열거형
enum ChangeType {
SET
MOVE_FORWARD
MOVE_BACKWARD
RESET
}
// 롤백 액션 열거형
enum RollbackAction {
HIDE
PERMANENT_DELETE
MARK_INVALID
}
// 발행 상태 열거형
enum PublishStatus {
PENDING
PUBLISHED
FAILED
RETRYING
}
// 가상 시간 모델
model VirtualTime {
id String @id @default(uuid())
userId String @unique @map("user_id")
virtualTime BigInt @map("virtual_time")
timezoneId String? @map("timezone_id")
changedById String? @map("changed_by_id")
changeReason String? @map("change_reason")
active Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
timeChangeRecords TimeChangeRecord[]
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@map("virtual_times")
@@schema("private")
}
// 시간 변경 기록 모델
model TimeChangeRecord {
id String @id @default(uuid())
virtualTimeId String @map("virtual_time_id")
previousTime DateTime @map("previous_time")
newTime DateTime @map("new_time")
changeType ChangeType @map("change_type")
timestamp DateTime
changedById String @map("changed_by_id")
reason String
createdAt DateTime @default(now()) @map("created_at")
// 관계
virtualTime VirtualTime @relation(fields: [virtualTimeId], references: [id])
rollbackRecords RollbackRecord[]
timeChangeEvents TimeChangeEvent[]
@@index([virtualTimeId])
@@index([timestamp])
@@map("time_change_records")
@@schema("private")
}
// 롤백 기록 모델
model RollbackRecord {
id String @id @default(uuid())
timeChangeId String @map("time_change_id")
affectedEntityType String @map("affected_entity_type")
affectedEntityId String @map("affected_entity_id")
rollbackAction RollbackAction @map("rollback_action")
rollbackTimestamp DateTime @map("rollback_timestamp")
createdAt DateTime @default(now()) @map("created_at")
// 관계
timeChangeRecord TimeChangeRecord @relation(fields: [timeChangeId], references: [id])
@@index([timeChangeId])
@@index([affectedEntityType, affectedEntityId])
@@map("rollback_records")
@@schema("private")
}
// 시간 변경 이벤트 모델
model TimeChangeEvent {
id String @id @default(uuid())
timeChangeId String @map("time_change_id")
previousDate DateTime @map("previous_date") @db.Date
newDate DateTime @map("new_date") @db.Date
timestamp DateTime
changedById String @map("changed_by_id")
metadata Json?
publishStatus PublishStatus @map("publish_status")
publishedAt DateTime? @map("published_at")
publishErrorMessage String? @map("publish_error_message")
retryCount Int @default(0) @map("retry_count")
nextRetryAt DateTime? @map("next_retry_at")
createdAt DateTime @default(now()) @map("created_at")
// 관계
timeChangeRecord TimeChangeRecord @relation(fields: [timeChangeId], references: [id])
@@index([timeChangeId])
@@index([publishStatus])
@@index([nextRetryAt])
@@map("time_change_events")
@@schema("private")
}
// 캐시 설정 모델
model CacheConfig {
id String @id @default(uuid())
ttlSeconds Int @map("ttl_seconds")
maxEntries Int @map("max_entries")
enabled Boolean @default(true)
updatedAt DateTime @updatedAt @map("updated_at")
@@map("cache_config")
@@schema("time_machine")
}
10. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-04-01 | bok@weltcorp.com | 최초 작성 |
| 0.2.0 | 2025-04-10 | bok@weltcorp.com | 개인정보/비개인정보 스키마 분리 규칙 추가 |
| 0.2.1 | 2025-04-11 | bok@weltcorp.com | 비개인정보 저장소를 access_code에서 time_machine 스키마로 변경 |
| 0.3.0 | 2025-04-12 | bok@weltcorp.com | 사용자별 가상시간 관리를 위한 VirtualTime 엔티티에 userId 필드 추가 |
| 0.3.1 | 2025-04-12 | bok@weltcorp.com | 개인정보 포함 엔티티들의 스키마를 private으로 변경 |
| 0.4.0 | 2025-01-19 | bok@weltcorp.com | TimeMachine enable/disable internal event 추가, 사용자 ID 타입 string으로 통일 |