Skip to content

da-bom/dabom-api-notification

Repository files navigation

dabom-api-notification

DABOM μ‹€μ‹œκ°„ κ°€μ‘± 데이터 톡합 관리 μ‹œμŠ€ν…œμ˜ μ•Œλ¦Ό μ„œλΉ„μŠ€. Kafka 이벀트 기반으둜 μ•Œλ¦Όμ„ μˆ˜μ‹ ν•˜μ—¬ DB μ˜μ†ν™”, SSE μ‹€μ‹œκ°„ 슀트림, Web Push μ„Έ μ±„λ„λ‘œ λ™μ‹œ λ°°ν¬ν•œλ‹€.


1. πŸ—οΈ μ‹œμŠ€ν…œ λ‚΄ μœ„μΉ˜

flowchart TB
    SIM["simulator-usage"] -->|usage-events| KF["Kafka Cluster"]
    KF -->|usage-events| PU["processor-usage"]
    PU -->|usage_event_outbox 적재| DB[("MySQL")]
    DB -->|배치 μ„œλ²„ 쑰회 ν›„ λ°œν–‰| KF
    AC["api-core"] -->|notification-events λ°œν–‰| KF

    KF -->|notification-events| AN

    subgraph AN["api-notification (이 μ„œλΉ„μŠ€)"]
        direction LR
        S1["1. DB μ €μž₯"]
        S2["2. SSE Push"]
        S3["3. Web Push"]
    end

    AN -->|SSE + REST| WS["web-service (PWA)"]

    style AN fill:#E8F5E9,stroke:#43A047,stroke-width:2px
Loading
  • Upstream: processor-usage, api-coreκ°€ notification-events Kafka ν† ν”½μœΌλ‘œ 이벀트 λ°œν–‰
  • Downstream: web-service(www.dabom.site) ν΄λΌμ΄μ–ΈνŠΈκ°€ SSE 슀트림 ꡬ독 및 REST API 호좜

2. πŸ› οΈ 기술 μŠ€νƒ

μ˜μ—­ 기술 버전
Runtime Java 21
Framework Spring Boot 3.4.0
Database MySQL + Spring Data JPA + QueryDSL 5.1.0
Cache Spring Data Redis -
Messaging Spring Kafka + lib-kafka (사내 곡톡 라이브러리) v1.0.1
Real-Time Spring MVC SSE (SseEmitter) -
Web Push nl.martijndwars/web-push + BouncyCastle 5.1.2
Auth JJWT (JWT 자체 κ΅¬ν˜„) 0.11.2
Docs SpringDoc OpenAPI (Swagger UI) 2.7.0
Observability OpenTelemetry + Micrometer + Prometheus 1.44.1
Code Quality Checkstyle (Naver) + Spotless + JaCoCo + SonarQube -

3. ⚑ 핡심 κΈ°λŠ₯

3.1 πŸ“¨ Kafka Consumer β€” μ•Œλ¦Ό 이벀트 μˆ˜μ‹ 

NotificationKafkaConsumerκ°€ notification-events 토픽을 μ†ŒλΉ„ν•œλ‹€.

flowchart TD
    KF["Kafka\nnotification-events"] --> KC["NotificationKafkaConsumer\nEventEnvelope<NotificationPayload> 역직렬화\nlib-kafka KafkaEventMessageSupport"]
    KC --> SVC["NotificationEventServiceImpl\n.handleNotificationEvent()"]

    SVC --> DB["1. DB μ €μž₯\nNotificationLog μ˜μ†ν™”"]
    SVC --> WP["2. Web Push\nVAPID μ„œλͺ… β†’ λΈŒλΌμš°μ € 전솑"]
    SVC --> SSE["3. SSE\nSsePublisher β†’ EmitterRegistry\nβ†’ ν™œμ„± ν΄λΌμ΄μ–ΈνŠΈ λΈŒλ‘œλ“œμΊμŠ€νŠΈ"]
Loading

μ•Œλ¦Ό λΆ„λ°° κ·œμΉ™:

  • customerIdκ°€ μ§€μ •λœ 이벀트 β†’ ν•΄λ‹Ή κ°œμΈμ—κ²Œλ§Œ DB μ €μž₯ + Push 전솑
  • customerIdκ°€ null β†’ familyId둜 κ°€μ‘± 전체 κ΅¬μ„±μ›μ—κ²Œ 각각 DB μ €μž₯ + κ°€μ‘± λ‹¨μœ„ Push 전솑
  • SSEλŠ” 항상 familyId λ‹¨μœ„λ‘œ λΈŒλ‘œλ“œμΊμŠ€νŠΈ

Consumer Group: dabom-api-notification (application.yml μ„€μ •)

3.2 πŸ”” μ•Œλ¦Ό νƒ€μž… (14μ’…)

μΉ΄ν…Œκ³ λ¦¬ νƒ€μž… λ°œμƒ μ‹œμ 
μ‚¬μš©λŸ‰ QUOTA_UPDATED κ°€μ‘± μΏΌν„° μž”μ—¬λŸ‰ λ³€κ²½
μ‚¬μš©λŸ‰ THRESHOLD_ALERT μž”μ—¬λŸ‰ 50%/30%/10% μž„κ³„μΉ˜ 도달
차단 CUSTOMER_BLOCKED 개인 ν•œλ„ 초과 / μ‹œκ°„λŒ€ 차단 / μˆ˜λ™ 차단
차단 CUSTOMER_UNBLOCKED 차단 ν•΄μ œ
μ •μ±… POLICY_CHANGED μ •μ±… μˆ˜μ • 반영
λ―Έμ…˜ MISSION_CREATED μƒˆ λ―Έμ…˜ 생성 (Owner β†’ κ°€μ‘±)
보상 REWARD_REQUESTED μžλ…€κ°€ 보상 μš”μ²­ (β†’ Owner)
보상 REWARD_APPROVED Ownerκ°€ 보상 승인 (β†’ μžλ…€)
보상 REWARD_REJECTED Ownerκ°€ 보상 거절 (β†’ μžλ…€)
이의제기 APPEAL_CREATED 이의제기 μš”μ²­ (β†’ Owner)
이의제기 APPEAL_APPROVED 이의제기 승인 (β†’ μžλ…€)
이의제기 APPEAL_REJECTED 이의제기 거절 (β†’ μžλ…€)
κΈ΄κΈ‰μš”μ²­ EMERGENCY_APPROVED κΈ΄κΈ‰ μΏΌν„° μžλ™μŠΉμΈ (β†’ Owner μ‚¬ν›„μ•Œλ¦Ό)
κ΄€λ¦¬μž ADMIN_PUSH λ°±μ˜€ν”ΌμŠ€μ—μ„œ 직접 λ°œμ†‘

3.3 πŸ“‘ SSE μ‹€μ‹œκ°„ 슀트림

ν΄λΌμ΄μ–ΈνŠΈκ°€ GET /events/stream으둜 SSE 연결을 맺으면, κ°€μ‘± λ‹¨μœ„λ‘œ 이벀트λ₯Ό μ‹€μ‹œκ°„ μˆ˜μ‹ ν•œλ‹€.

λ‚΄λΆ€ ꡬ쑰:

flowchart TD
    EC["EventStreamController\nGET /events/stream"] -->|"@CustomerId\nJWTμ—μ„œ customerId μΆ”μΆœ"| SS
    SS["SseSubscriber\n.subscribe(customerId)"] -->|"customerId β†’ familyId 쑰회\n(FamilyMember ν…Œμ΄λΈ”)"| ER
    ER["EmitterRegistry\n.register(familyId)"] -->|"ConcurrentHashMap<Long, List<SseEmitter>>\ntimeout: 60초"| CON["μ—°κ²° μ™„λ£Œ\n'connected' 이벀트 μ¦‰μ‹œ 전솑\nβ†’ 이후 이벀트 μˆ˜μ‹  λŒ€κΈ°"]
Loading

SSE 이벀트 μ’…λ₯˜:

SSE 이벀트λͺ… 데이터 λ°œμƒ 쑰건
connected μ—°κ²° 확인 λ©”μ‹œμ§€ 졜초 μ—°κ²° μ‹œ
heartbeat "ping" 25초 주기
usage-updated {familyId, totalUsedBytes, totalLimitBytes, remainingBytes} κ°€μ‘± μ‚¬μš©λŸ‰ λ³€κ²½ (1초 폴링)
usage-updated-by-member {familyId, customerId, monthlyUsedBytes} ꡬ성원별 μ‚¬μš©λŸ‰ λ³€κ²½
14μ’… μ•Œλ¦Ό νƒ€μž…λͺ… NotificationPayload Kafka μ•Œλ¦Ό 이벀트 μˆ˜μ‹  μ‹œ

Usage Polling λ©”μ»€λ‹ˆμ¦˜ (PollingService):

  1. 1초 주기둜 family_quota ν…Œμ΄λΈ”μ—μ„œ ν™œμ„± κ°€μ‘±μ˜ μ‚¬μš©λŸ‰μ„ IN 쿼리둜 일괄 쑰회 (N+1 λ°©μ§€)
  2. ConcurrentHashMap으둜 이전 usedBytes와 λΉ„κ΅ν•˜μ—¬ λ³€κ²½λœ κ°€μ‘±λ§Œ 필터링
  3. λ³€κ²½ 감지 μ‹œ usage-updated + usage-updated-by-member 이벀트 전솑
  4. SSE 연결이 μ—†μœΌλ©΄ (activeFamilyIds λΉ„μ–΄μžˆμœΌλ©΄) 폴링 자체λ₯Ό κ±΄λ„ˆλœ€

μ—°κ²° 수λͺ… μ£ΌκΈ°:

  • Emitter timeout: 60초 β†’ ν΄λΌμ΄μ–ΈνŠΈκ°€ μž¬μ—°κ²°
  • Heartbeat: 25초 주기둜 ping μ „μ†‘ν•˜μ—¬ ν”„λ‘μ‹œ/λ‘œλ“œλ°ΈλŸ°μ„œ 유휴 νƒ€μž„μ•„μ›ƒ λ°©μ§€
  • μ—λŸ¬ 콜백: broken pipe / client abort / EOF 감지 μ‹œ μžλ™ 정리

3.4 🌐 Web Push (VAPID)

RFC 8291 기반 Web Push Protocol을 κ΅¬ν˜„ν•˜μ—¬ PWA λΈŒλΌμš°μ €μ— ν‘Έμ‹œ μ•Œλ¦Όμ„ λ°œμ†‘ν•œλ‹€.

flowchart LR
    A["sendPushNotification()"] --> B["Payload 직렬화\n{title, body}"]
    B --> C["VAPID μ„œλͺ…\nBouncyCastle + jose4j"]
    C --> D["AES128GCM μ•”ν˜Έν™”\nRFC 8291"]
    D --> E["CloseableHttpClient\nβ†’ λΈŒλΌμš°μ € Push μ—”λ“œν¬μΈνŠΈ POST"]
Loading

λ³΄μ•ˆ 쑰치:

  • ꡬ독 μ—”λ“œν¬μΈνŠΈ HTTPS ν•„μˆ˜ 검증
  • SSRF λ°©μ§€: NetworkValidator.isInternalAddress()둜 λ‚΄λΆ€ IP μ£Όμ†Œ 차단
  • HTTP ν΄λΌμ΄μ–ΈνŠΈ νƒ€μž„μ•„μ›ƒ: connect 5초, socket 5초 (ν™˜κ²½λ³€μˆ˜λ‘œ μ‘°μ • κ°€λŠ₯)

ꡬ독 관리 (Upsert):

  • 동일 endpoint μž¬λ“±λ‘ β†’ ν‚€ 정보 κ°±μ‹ 
  • λ‹€λ₯Έ customerκ°€ μ μœ ν•œ endpoint β†’ κΈ°μ‘΄ ꡬ독 μ‚­μ œ ν›„ μž¬ν• λ‹Ή
  • 고객당 1개 ꡬ독 μœ μ§€

4. πŸ”Œ REST API

4.1 πŸ”” Notifications (/notifications)

Method Path κΆŒν•œ μ„€λͺ…
GET /notifications member μ•Œλ¦Ό λͺ©λ‘ 쑰회 (μ»€μ„œ 기반 λ¬΄ν•œμŠ€ν¬λ‘€)
GET /notifications/unread-count member 읽지 μ•Šμ€ μ•Œλ¦Ό 수
PATCH /notifications/{id}/read member 단건 읽음 처리
PATCH /notifications/read-all member 전체 읽음 처리
DELETE /notifications/{id} member μ•Œλ¦Ό μ‚­μ œ (soft delete)

μ•Œλ¦Ό λͺ©λ‘ 쑰회 νŒŒλΌλ―Έν„°:

νŒŒλΌλ―Έν„° νƒ€μž… ν•„μˆ˜ μ„€λͺ…
cursor string N λ‹€μŒ νŽ˜μ΄μ§€ μ»€μ„œ (Base64 인코딩 ID)
size int N 쑰회 크기 (기본 20)
isRead boolean N 읽음 μƒνƒœ ν•„ν„°
types NotificationType[] N μ•Œλ¦Ό νƒ€μž… ν•„ν„° (콀마 ꡬ뢄)
  • 보쑴 κΈ°κ°„: 30일 (μ„€μ •: app.notification.retention-days)
  • 30일 이전 μ•Œλ¦Όμ€ 쑰회 κ²°κ³Όμ—μ„œ μžλ™ μ œμ™Έ
  • 본인 μ•Œλ¦Όλ§Œ 쑰회 κ°€λŠ₯ (customerId JWT 기반 필터링)

4.2 πŸ“‘ Events (/events)

Method Path κΆŒν•œ μ„€λͺ…
GET /events/stream member SSE μ‹€μ‹œκ°„ 이벀트 슀트림
GET /events/stream/test/{customerId} μ—†μŒ ν…ŒμŠ€νŠΈμš© SSE (인증 λΆˆν•„μš”)

4.3 🌐 Push (/push)

Method Path κΆŒν•œ μ„€λͺ…
GET /push/vapid-public-key μ—†μŒ VAPID κ³΅κ°œν‚€ 쑰회
POST /push/subscribe member ν‘Έμ‹œ ꡬ독 등둝/κ°±μ‹  (Upsert)
DELETE /push/subscribe member ν‘Έμ‹œ ꡬ독 ν•΄μ œ
POST /push/send admin κ΄€λ¦¬μž 직접 ν‘Έμ‹œ λ°œμ†‘

4.4 πŸ“¦ 응닡 ν˜•μ‹

곡톡 래퍼 ApiResponse<T> μ‚¬μš©. api-core와 λ™μΌν•œ ꡬ쑰.

성곡:

{
  "success": true,
  "data": { ... },
  "timestamp": "2026-03-20T10:30:00Z"
}

μ—λŸ¬:

{
  "success": false,
  "error": {
    "code": "NOTIFICATION_003",
    "message": "μ•Œλ¦Όμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."
  },
  "timestamp": "2026-03-20T10:30:00Z"
}

dataλŠ” 성곡 μ‹œμ—λ§Œ, errorλŠ” μ‹€νŒ¨ μ‹œμ—λ§Œ ν¬ν•¨λœλ‹€ (@JsonInclude(NON_NULL)).


5. ❌ μ—λŸ¬ μ½”λ“œ

Notification

μ½”λ“œ HTTP μ„€λͺ…
NOTIFICATION_001 500 μ•Œλ¦Ό μ €μž₯ μ‹€νŒ¨
NOTIFICATION_002 403 ν•΄λ‹Ή μ•Œλ¦Όμ— λŒ€ν•œ κΆŒν•œ μ—†μŒ
NOTIFICATION_003 404 μ•Œλ¦Όμ„ 찾을 수 μ—†μŒ
NOTIFICATION_004 400 ν•΄λ‹Ή 고객의 κ°€μ‘± 정보λ₯Ό 찾을 수 μ—†μŒ

Subscription (Push)

μ½”λ“œ HTTP μ„€λͺ…
SUBSCRIPTION_001 404 ꡬ독 정보λ₯Ό 찾을 수 μ—†μŒ
SUBSCRIPTION_002 500 ν‘Έμ‹œ μ•Œλ¦Ό 전솑 μ‹€νŒ¨
SUBSCRIPTION_003 400 μœ νš¨ν•˜μ§€ μ•Šμ€ ꡬ독 μ—”λ“œν¬μΈνŠΈ URL

6. πŸ’Ύ 데이터 λͺ¨λΈ

6.1 notification_log

μ•Œλ¦Ό 이λ ₯ μ €μž₯ ν…Œμ΄λΈ”. Kafka둜 μˆ˜μ‹ λœ λͺ¨λ“  μ•Œλ¦Όμ΄ κ³ κ°λ³„λ‘œ 1건씩 μ €μž₯λœλ‹€.

컬럼 νƒ€μž… μ„€λͺ…
id BIGINT PK μžλ™ 증가
customer_id BIGINT μˆ˜μ‹  λŒ€μƒ 고객 ID
family_id BIGINT κ°€μ‘± κ·Έλ£Ή ID
type VARCHAR (ENUM) μ•Œλ¦Ό νƒ€μž… (14μ’…)
title VARCHAR(100) μ•Œλ¦Ό 제λͺ©
message TEXT μ•Œλ¦Ό λ³Έλ¬Έ
payload JSON μ•Œλ¦Ό λΆ€κ°€ 데이터
is_read BOOLEAN 읽음 μ—¬λΆ€ (κΈ°λ³Έ false)
sent_at DATETIME Kafka 이벀트 λ°œμƒ μ‹œκ°
created_at DATETIME λ ˆμ½”λ“œ 생성 μ‹œκ°
updated_at DATETIME μ΅œμ’… μˆ˜μ • μ‹œκ°
deleted_at DATETIME Soft Delete μ‹œκ° (null = ν™œμ„±)

6.2 push_subscription

Web Push ꡬ독 정보. 고객당 μ΅œλŒ€ 1건.

컬럼 νƒ€μž… μ„€λͺ…
id BIGINT PK μžλ™ 증가
endpoint VARCHAR (UNIQUE) λΈŒλΌμš°μ € Push μ—”λ“œν¬μΈνŠΈ URL
p256dh VARCHAR P-256 ECDH κ³΅κ°œν‚€
auth VARCHAR 인증 μ‹œν¬λ¦Ώ
customer_id BIGINT ꡬ독 μ†Œμœ  고객 ID

6.3 μ°Έμ‘° μ—”ν‹°ν‹° (읽기 μ „μš©)

이 μ„œλΉ„μŠ€λŠ” μ•„λž˜ ν…Œμ΄λΈ”μ„ 쑰회만 μˆ˜ν–‰ν•œλ‹€ (μ“°κΈ° μ±…μž„μ€ api-core / processor-usage에 있음):

ν…Œμ΄λΈ” μš©λ„
customer 고객 κΈ°λ³Έ 정보
customer_quota 고객별 μ›”κ°„ μ‚¬μš©λŸ‰/ν•œλ„ (SSE member 데이터)
family κ°€μ‘± κ·Έλ£Ή 정보
family_member κ°€μ‘± ꡬ성원 λ§€ν•‘ β€” customerId ↔ familyId, role
family_quota κ°€μ‘± μ›”κ°„ 총 μ‚¬μš©λŸ‰/μΏΌν„° (SSE 1초 폴링 λŒ€μƒ)

7. πŸ” 인증/인가

JWT 토큰 기반. LoginInterceptorκ°€ λͺ¨λ“  μš”μ²­μ— λŒ€ν•΄ Authorization: Bearer <token> 헀더λ₯Ό κ²€μ¦ν•œλ‹€.

JWT Payload:

{
  "customerId": 12345,
  "familyId": 100,
  "role": "OWNER",
  "exp": 1705312200
}

인증 μ œμ™Έ 경둜:

  • /push/vapid-public-key
  • /events/stream/test/**
  • /swagger-ui/**, /v3/api-docs/**

μ—­ν•  기반 μ ‘κ·Ό μ œμ–΄:

μ–΄λ…Έν…Œμ΄μ…˜ μ΅œμ†Œ μ—­ν•  μš©λ„
@CustomerId member JWTμ—μ„œ customerIdλ₯Ό ArgumentResolver둜 μžλ™ μ£Όμž…
@OwnerOnly owner AOP Aspect둜 Owner μ—­ν•  검증
@AdminOnly admin AOP Aspect둜 Admin μ—­ν•  검증 (예: POST /push/send)

8. βš™οΈ μ„€μ •

ν™˜κ²½λ³€μˆ˜

λ³€μˆ˜ κΈ°λ³Έκ°’ μ„€λͺ…
SERVER_PORT 8080 μ„œλ²„ 포트
DATABASE_URL jdbc:mysql://localhost:3310/app_db?... MySQL 접속 URL
DATABASE_USER app_user DB μ‚¬μš©μž
DATABASE_PASSWORD app_password DB λΉ„λ°€λ²ˆν˜Έ
REDIS_HOST localhost Redis 호슀트
REDIS_PORT 6379 Redis 포트
KAFKA_BOOTSTRAP_SERVERS localhost:9092 Kafka 브둜컀 μ£Όμ†Œ
KAFKA_CONSUMER_GROUP_ID dabom-api-notification Kafka Consumer Group
JWT_SECRET_KEY (개발용 κΈ°λ³Έκ°’) JWT μ„œλͺ… ν‚€
VAPID_PUBLIC_KEY (ν•„μˆ˜) VAPID κ³΅κ°œν‚€
VAPID_PRIVATE_KEY (ν•„μˆ˜) VAPID λΉ„λ°€ν‚€
FRONTEND_URL http://localhost:3000 CORS ν—ˆμš© Origin
NOTIFICATION_RETENTION_DAYS 30 μ•Œλ¦Ό 보쑴 κΈ°κ°„ (일)
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4318 OpenTelemetry μˆ˜μ§‘κΈ° μ£Όμ†Œ
OTEL_SDK_DISABLED true OTel SDK λΉ„ν™œμ„±ν™” μ—¬λΆ€

Profiles

Profile μš©λ„
local 둜컬 개발 (기본)
dev 개발 μ„œλ²„
prod 운영 ν™˜κ²½

.env νŒŒμΌμ„ ν”„λ‘œμ νŠΈ λ£¨νŠΈμ— 두면 bootRun μ‹œ μžλ™μœΌλ‘œ ν™˜κ²½λ³€μˆ˜κ°€ λ‘œλ”©λœλ‹€.


9. πŸ“‚ ν”„λ‘œμ νŠΈ ꡬ쑰

src/main/java/com/project/
β”œβ”€β”€ Application.java                          # @EnableAsync, @EnableScheduling
β”‚
β”œβ”€β”€ common/
β”‚   β”œβ”€β”€ api/response/ApiResponse.java         # 곡톡 응닡 래퍼
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ JwtTokenUtil.java                 # JWT 검증/νŒŒμ‹±
β”‚   β”‚   β”œβ”€β”€ LoginInterceptor.java             # 인증 인터셉터
β”‚   β”‚   β”œβ”€β”€ AuthorizationExtractor.java       # Bearer 토큰 μΆ”μΆœ
β”‚   β”‚   └── aop/
β”‚   β”‚       β”œβ”€β”€ CustomerId.java               # @CustomerId μ–΄λ…Έν…Œμ΄μ…˜
β”‚   β”‚       β”œβ”€β”€ CustomerArgumentResolver.java # customerId μžλ™ μ£Όμž…
β”‚   β”‚       β”œβ”€β”€ AdminOnly.java                # @AdminOnly μ–΄λ…Έν…Œμ΄μ…˜
β”‚   β”‚       β”œβ”€β”€ AdminOnlyAspect.java          # Admin μ—­ν•  검증 AOP
β”‚   β”‚       β”œβ”€β”€ OwnerOnly.java                # @OwnerOnly μ–΄λ…Έν…Œμ΄μ…˜
β”‚   β”‚       └── OwnerOnlyAspect.java          # Owner μ—­ν•  검증 AOP
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ WebConfig.java                    # CORS, 인터셉터, ArgumentResolver
β”‚   β”‚   β”œβ”€β”€ KafkaCommonConfig.java            # lib-kafka 곡톡 μ„€μ • Import
β”‚   β”‚   β”œβ”€β”€ PushConfig.java                   # VAPID PushService, HttpClient (SSRF λ°©μ–΄)
β”‚   β”‚   β”œβ”€β”€ JpaConfig.java                    # JPA Auditing ν™œμ„±ν™”
β”‚   β”‚   β”œβ”€β”€ QueryDslConfig.java               # JPAQueryFactory 빈
β”‚   β”‚   β”œβ”€β”€ SwaggerConfig.java                # OpenAPI JWT 인증 μŠ€ν‚€λ§ˆ
β”‚   β”‚   β”œβ”€β”€ ThreadPoolConfig.java             # 비동기 μŠ€λ ˆλ“œν’€
β”‚   β”‚   └── CacheProperties.java              # μΊμ‹œ μ„€μ •
β”‚   β”œβ”€β”€ exception/
β”‚   β”‚   β”œβ”€β”€ ExceptionAdvice.java              # @RestControllerAdvice (SSE μ•ˆμ „ 처리)
β”‚   β”‚   β”œβ”€β”€ BaseException.java
β”‚   β”‚   β”œβ”€β”€ ApplicationException.java
β”‚   β”‚   └── code/                             # 도메인별 μ—λŸ¬ μ½”λ“œ Enum
β”‚   β”‚       β”œβ”€β”€ NotificationErrorCode.java
β”‚   β”‚       β”œβ”€β”€ SubscriptionErrorCode.java
β”‚   β”‚       └── ...
β”‚   └── util/
β”‚       β”œβ”€β”€ BaseEntity.java                   # created_at, updated_at, deleted_at
β”‚       β”œβ”€β”€ CursorUtil.java                   # Base64 μ»€μ„œ 인코딩/λ””μ½”λ”©
β”‚       └── NetworkValidator.java             # λ‚΄λΆ€ IP 차단 (SSRF λ°©μ–΄)
β”‚
β”œβ”€β”€ domain/
β”‚   β”œβ”€β”€ notification/                         # ── μ•Œλ¦Ό 도메인 ──
β”‚   β”‚   β”œβ”€β”€ controller/
β”‚   β”‚   β”‚   └── NotificationController.java   # REST μ•Œλ¦Ό API (5개 μ—”λ“œν¬μΈνŠΈ)
β”‚   β”‚   β”œβ”€β”€ service/
β”‚   β”‚   β”‚   β”œβ”€β”€ NotificationService[Impl]     # μ•Œλ¦Ό 쑰회/읽음/μ‚­μ œ
β”‚   β”‚   β”‚   └── NotificationEventService[Impl]# Kafka 이벀트 β†’ DB + Push + SSE
β”‚   β”‚   β”œβ”€β”€ entity/
β”‚   β”‚   β”‚   β”œβ”€β”€ NotificationLog.java          # μ•Œλ¦Ό 이λ ₯ μ—”ν‹°ν‹°
β”‚   β”‚   β”‚   └── NotificationType.java         # 14μ’… μ•Œλ¦Ό νƒ€μž… Enum
β”‚   β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”‚   β”œβ”€β”€ NotificationLogRepository.java
β”‚   β”‚   β”‚   └── ...RepositoryImpl.java        # QueryDSL μ»€μ„œ 기반 νŽ˜μ΄μ§•
β”‚   β”‚   β”œβ”€β”€ dto/                              # μš”μ²­/응닡 DTO
β”‚   β”‚   └── infra/messaging/
β”‚   β”‚       └── NotificationKafkaConsumer.java# Kafka Consumer
β”‚   β”‚
β”‚   β”œβ”€β”€ event/                                # ── SSE μ—”λ“œν¬μΈνŠΈ ──
β”‚   β”‚   └── controller/
β”‚   β”‚       └── EventStreamController.java    # GET /events/stream (2개)
β”‚   β”‚
β”‚   β”œβ”€β”€ usagerecord/                          # ── SSE 인프라 ──
β”‚   β”‚   β”œβ”€β”€ infra/sse/
β”‚   β”‚   β”‚   β”œβ”€β”€ EmitterRegistry.java          # familyId별 SseEmitter 관리
β”‚   β”‚   β”‚   β”œβ”€β”€ SseSubscriber.java            # SSE ꡬ독 처리
β”‚   β”‚   β”‚   β”œβ”€β”€ SsePublisher.java             # μ•Œλ¦Ό 이벀트 SSE 전솑
β”‚   β”‚   β”‚   └── PollingService.java           # μ‚¬μš©λŸ‰ 1초 폴링 + 25초 heartbeat
β”‚   β”‚   └── dto/response/                     # μ‚¬μš©λŸ‰ SSE 응닡 DTO
β”‚   β”‚
β”‚   β”œβ”€β”€ webpush/                              # ── Web Push 도메인 ──
β”‚   β”‚   β”œβ”€β”€ controller/
β”‚   β”‚   β”‚   └── WebPushController.java        # Push API (4개 μ—”λ“œν¬μΈνŠΈ)
β”‚   β”‚   β”œβ”€β”€ service/
β”‚   β”‚   β”‚   └── WebPushServiceImpl.java       # VAPID μ„œλͺ… + AES128GCM μ•”ν˜Έν™”
β”‚   β”‚   β”œβ”€β”€ entity/PushSubscription.java
β”‚   β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   └── dto/                              # ꡬ독 μš”μ²­/응닡 DTO
β”‚   β”‚
β”‚   β”œβ”€β”€ customer/                             # ── 고객 (읽기 μ „μš©) ──
β”‚   β”‚   β”œβ”€β”€ entity/Customer.java, CustomerQuota.java
β”‚   β”‚   └── repository/
β”‚   β”‚
β”‚   └── family/                               # ── κ°€μ‘± (읽기 μ „μš©) ──
β”‚       β”œβ”€β”€ entity/Family.java, FamilyMember.java, FamilyQuota.java
β”‚       └── repository/

10. πŸš€ μ‹€ν–‰

# ν™˜κ²½λ³€μˆ˜ μ„€μ • (.env 파일 λ˜λŠ” 직접 export)
cp .env.example .env
# VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY ν•„μˆ˜ μ„€μ •

# 둜컬 μ‹€ν–‰
./gradlew bootRun

# λΉŒλ“œ
./gradlew build

# ν…ŒμŠ€νŠΈ
./gradlew test

ν•„μˆ˜ 인프라:

  • MySQL 8.x (포트 3310 κΈ°λ³Έ)
  • Redis (포트 6379 κΈ°λ³Έ)
  • Kafka (포트 9092 κΈ°λ³Έ)

11. πŸ“Š Observability

μ˜μ—­ 도ꡬ μ„€μ •
Metrics Micrometer β†’ Prometheus + OTLP /actuator/prometheus λ…ΈμΆœ, 15초 μ£ΌκΈ° OTLP push
Traces OpenTelemetry β†’ OTLP 둜그 νŒ¨ν„΄μ— traceId/spanId μžλ™ 포함
Logs Logstash JSON Encoder ꡬ쑰화 λ‘œκΉ… (net.logstash.logback)
Health Spring Actuator /actuator/health (liveness/readiness probes)

Actuator λ…ΈμΆœ μ—”λ“œν¬μΈνŠΈ: health, info, prometheus, metrics, loggers

λ©”νŠΈλ¦­ νžˆμŠ€ν† κ·Έλž¨ λŒ€μƒ: http.server.requests, spring.data.repository.invocations, kafka.consumer.processing.time

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors