# MongoDB Nanosecond Precision POC > 작성일: 2026-01-21 ## 개요 MongoDB BSON Date는 millisecond 정밀도만 지원하므로, nanosecond 정밀도를 보존하기 위한 POC 구현. ## 최종 저장 형식 ```javascript // 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 설정 ```nginx location / { proxy_set_header X-Request-Start-Time $msec; proxy_pass http://backend; } ``` ### Kubernetes Ingress ```yaml 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 예시 ```bash 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 예시 ```json { "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 쿼리 예시 ```javascript // 이벤트별 참여자 조회 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 ``` --- ## 테스트 실행 ```bash # 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 |