Initialize projecet
This commit is contained in:
228
docs/20260121_195722_mongodb_nanosecond_precision_poc.md
Normal file
228
docs/20260121_195722_mongodb_nanosecond_precision_poc.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user