Files
mongodb-nanosec/docs/20260121_195722_mongodb_nanosecond_precision_poc.md
2026-01-23 11:45:18 +09:00

8.0 KiB

MongoDB Nanosecond Precision POC

작성일: 2026-01-21

개요

MongoDB BSON Date는 millisecond 정밀도만 지원하므로, nanosecond 정밀도를 보존하기 위한 POC 구현.

최종 저장 형식

// MongoDB Document
{
  "_id": "...",
  "eventId": "event-001",
  "userId": "user-001",
  "participatedAt": ISODate("2024-01-21T06:30:45.123Z"),  // BSON Date (ms)
  "nanoAdjustment": 456789,                               // Int (0-999999)
  "metadata": { ... }
}
필드 타입 설명
participatedAt BSON Date 밀리초 정밀도, UTC 저장
nanoAdjustment Int 0-999999 (서브밀리초 나노)

설계 결정 사항

1. Offset 저장 안함

  • Client가 i18n parameter (country code)를 요청 시 전달
  • Server에서 응답 시 해당 timezone으로 변환하여 반환
  • 별도 offset 저장 불필요

2. BSON Date 사용 이유

  • Native MongoDB 타입 → Shell 쿼리 용이 (ISODate())
  • 효율적인 인덱싱 및 범위 쿼리
  • Custom Converter 불필요 (Zero overhead)

3. 나노초 분리 저장

  • BSON Date는 ms까지만 지원
  • nanoAdjustment 필드로 sub-ms 나노초 보존
  • 복원: Instant.ofEpochMilli(ms).plusNanos(nanoAdjustment)

요청 시간 캡처 아키텍처

문제

  • Controller나 Service에서 Instant.now() 사용 시 실제 클라이언트 클릭 시간과 차이 발생
  • 클레임 발생 가능성

해결: 계층별 타임스탬프 캡처

┌─────────────────────────────────────────────────────────────────┐
│                        Client Click                              │
│                             │                                    │
│                      ~50-500ms 지연                              │
│                             ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  Ingress/LB (Nginx, ALB)                                  │  │
│  │  → X-Request-Start-Time: $msec 헤더 추가 ⭐ 가장 정확     │  │
│  └───────────────────────────────────────────────────────────┘  │
│                             │                                    │
│                             ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  RequestTimestampFilter (Servlet Filter)                  │  │
│  │  1. X-Request-Start-Time 헤더 파싱                        │  │
│  │  2. 없으면 Instant.now() (차선책)                         │  │
│  │  3. ThreadLocal + MDC 저장                                │  │
│  └───────────────────────────────────────────────────────────┘  │
│                             │                                    │
│                             ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  Service                                                   │  │
│  │  → RequestTimestampHolder.get() 으로 시간 조회             │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Filter vs Interceptor

구분 Filter Interceptor
실행 시점 DispatcherServlet 이전 DispatcherServlet 이후
소속 Servlet 스펙 Spring MVC
정확도 더 정확 덜 정확

결론: Filter가 더 빠르므로 Filter 사용 권장

Nginx 설정

location / {
    proxy_set_header X-Request-Start-Time $msec;
    proxy_pass http://backend;
}

Kubernetes Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header X-Request-Start-Time $msec;

API 엔드포인트

Method Path 설명
POST /api/v1/event/participation 이벤트 참여 등록
GET /api/v1/event/participation/{id} 참여 정보 조회
GET /api/v1/event/participation/event/{eventId} 이벤트별 참여 목록
GET /api/v1/event/participation/user/{userId} 사용자별 참여 목록
GET /api/v1/event/participation/check 참여 여부 확인
GET /api/v1/event/participation/count/{eventId} 참여자 수

POST Request 예시

curl -X POST http://localhost:8080/api/v1/event/participation \
  -H "Content-Type: application/json" \
  -H "X-Timezone: Asia/Seoul" \
  -d '{
    "eventId": "event-001",
    "userId": "user-001",
    "metadata": {
      "source": "mobile"
    }
  }'

Response 예시

{
  "id": "678f...",
  "eventId": "event-001",
  "userId": "user-001",
  "participatedAt": "2024-01-21T15:30:45.123456789+09:00[Asia/Seoul]",
  "participatedAtEpochMilli": 1705819845123,
  "nanoAdjustment": 456789,
  "metadata": { "source": "mobile" }
}

MongoDB Shell 쿼리 예시

// 이벤트별 참여자 조회
db.event_participations.find({ eventId: "event-123" })

// 사용자별 참여 이벤트 조회
db.event_participations.find({ userId: "user-456" })

// 특정 기간 참여자 조회
db.event_participations.find({
    participatedAt: {
        $gte: ISODate("2024-01-21T00:00:00Z"),
        $lt: ISODate("2024-01-22T00:00:00Z")
    }
})

// 참여 시간순 정렬
db.event_participations.find({ eventId: "event-123" }).sort({ participatedAt: 1 })

// 이벤트별 참여자 수
db.event_participations.countDocuments({ eventId: "event-123" })

// 전체 나노초 계산 (JavaScript)
db.event_participations.find().forEach(function(doc) {
    var msNanos = (doc.participatedAt.getTime() % 1000) * 1000000;
    var fullNanos = msNanos + doc.nanoAdjustment;
    print(doc.userId + ": " + fullNanos + " ns");
});

파일 구조

src/main/kotlin/com/myoauniverse/lab/mongodb/
├── MongodbNanosecApplication.kt
├── config/
│   └── MongoConfig.kt                    # MongoDB 설정 + Custom Converters
├── domain/
│   └── EventParticipation.kt             # 도메인 엔티티
├── repository/
│   └── EventParticipationRepository.kt   # Repository
├── service/
│   └── EventParticipationService.kt      # 비즈니스 로직
├── filter/
│   └── RequestTimestampFilter.kt         # 요청 시간 캡처 Filter
└── controller/
    ├── EventParticipationController.kt   # REST Controller
    └── dto/
        └── EventParticipationDto.kt      # Request/Response DTO

테스트 실행

# MongoDB 실행 필요 (localhost:27017)
./gradlew test --tests "NanosecondPrecisionTest"

성능 고려사항

항목
필드 수 2개 (participatedAt + nanoAdjustment)
메모리 오버헤드 ~12 bytes (Date 8 + Int 4)
Custom Converter 불필요 (native BSON types)
대상 TPS 10,000 ~ 50,000