DABOM μ€μκ° κ°μ‘± λ°μ΄ν° ν΅ν© κ΄λ¦¬ μμ€ν μ μλ¦Ό μλΉμ€. Kafka μ΄λ²€νΈ κΈ°λ°μΌλ‘ μλ¦Όμ μμ νμ¬ DB μμν, SSE μ€μκ° μ€νΈλ¦Ό, Web Push μΈ μ±λλ‘ λμ λ°°ν¬νλ€.
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
- Upstream:
processor-usage,api-coreκ°notification-eventsKafka ν ν½μΌλ‘ μ΄λ²€νΈ λ°ν - Downstream:
web-service(www.dabom.site) ν΄λΌμ΄μΈνΈκ° SSE μ€νΈλ¦Ό ꡬλ λ° REST API νΈμΆ
| μμ | κΈ°μ | λ²μ |
|---|---|---|
| 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 | - |
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β νμ± ν΄λΌμ΄μΈνΈ λΈλ‘λμΊμ€νΈ"]
μλ¦Ό λΆλ°° κ·μΉ:
customerIdκ° μ§μ λ μ΄λ²€νΈ β ν΄λΉ κ°μΈμκ²λ§ DB μ μ₯ + Push μ μ‘customerIdκ° null βfamilyIdλ‘ κ°μ‘± μ 체 ꡬμ±μμκ² κ°κ° DB μ μ₯ + κ°μ‘± λ¨μ Push μ μ‘- SSEλ νμ
familyIdλ¨μλ‘ λΈλ‘λμΊμ€νΈ
Consumer Group: dabom-api-notification (application.yml μ€μ )
| μΉ΄ν κ³ λ¦¬ | νμ | λ°μ μμ |
|---|---|---|
| μ¬μ©λ | 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 |
λ°±μ€νΌμ€μμ μ§μ λ°μ‘ |
ν΄λΌμ΄μΈνΈκ° 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β μ΄ν μ΄λ²€νΈ μμ λκΈ°"]
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μ΄ μ£ΌκΈ°λ‘
family_quotaν μ΄λΈμμ νμ± κ°μ‘±μ μ¬μ©λμ IN μΏΌλ¦¬λ‘ μΌκ΄ μ‘°ν (N+1 λ°©μ§) ConcurrentHashMapμΌλ‘ μ΄μ usedBytesμ λΉκ΅νμ¬ λ³κ²½λ κ°μ‘±λ§ νν°λ§- λ³κ²½ κ°μ§ μ
usage-updated+usage-updated-by-memberμ΄λ²€νΈ μ μ‘ - SSE μ°κ²°μ΄ μμΌλ©΄ (
activeFamilyIdsλΉμ΄μμΌλ©΄) ν΄λ§ μ체λ₯Ό 건λλ
μ°κ²° μλͺ μ£ΌκΈ°:
- Emitter timeout: 60μ΄ β ν΄λΌμ΄μΈνΈκ° μ¬μ°κ²°
- Heartbeat: 25μ΄ μ£ΌκΈ°λ‘
pingμ μ‘νμ¬ νλ‘μ/λ‘λλ°Έλ°μ μ ν΄ νμμμ λ°©μ§ - μλ¬ μ½λ°±: broken pipe / client abort / EOF κ°μ§ μ μλ μ 리
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"]
보μ μ‘°μΉ:
- ꡬλ μλν¬μΈνΈ HTTPS νμ κ²μ¦
- SSRF λ°©μ§:
NetworkValidator.isInternalAddress()λ‘ λ΄λΆ IP μ£Όμ μ°¨λ¨ - HTTP ν΄λΌμ΄μΈνΈ νμμμ: connect 5μ΄, socket 5μ΄ (νκ²½λ³μλ‘ μ‘°μ κ°λ₯)
ꡬλ κ΄λ¦¬ (Upsert):
- λμΌ endpoint μ¬λ±λ‘ β ν€ μ 보 κ°±μ
- λ€λ₯Έ customerκ° μ μ ν endpoint β κΈ°μ‘΄ ꡬλ μμ ν μ¬ν λΉ
- κ³ κ°λΉ 1κ° κ΅¬λ μ μ§
| 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 κΈ°λ° νν°λ§)
| Method | Path | κΆν | μ€λͺ |
|---|---|---|---|
GET |
/events/stream |
member | SSE μ€μκ° μ΄λ²€νΈ μ€νΈλ¦Ό |
GET |
/events/stream/test/{customerId} |
μμ | ν μ€νΈμ© SSE (μΈμ¦ λΆνμ) |
| Method | Path | κΆν | μ€λͺ |
|---|---|---|---|
GET |
/push/vapid-public-key |
μμ | VAPID 곡κ°ν€ μ‘°ν |
POST |
/push/subscribe |
member | νΈμ ꡬλ λ±λ‘/κ°±μ (Upsert) |
DELETE |
/push/subscribe |
member | νΈμ ꡬλ ν΄μ |
POST |
/push/send |
admin | κ΄λ¦¬μ μ§μ νΈμ λ°μ‘ |
κ³΅ν΅ λνΌ 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)).
| μ½λ | HTTP | μ€λͺ |
|---|---|---|
NOTIFICATION_001 |
500 | μλ¦Ό μ μ₯ μ€ν¨ |
NOTIFICATION_002 |
403 | ν΄λΉ μλ¦Όμ λν κΆν μμ |
NOTIFICATION_003 |
404 | μλ¦Όμ μ°Ύμ μ μμ |
NOTIFICATION_004 |
400 | ν΄λΉ κ³ κ°μ κ°μ‘± μ 보λ₯Ό μ°Ύμ μ μμ |
| μ½λ | HTTP | μ€λͺ |
|---|---|---|
SUBSCRIPTION_001 |
404 | ꡬλ μ 보λ₯Ό μ°Ύμ μ μμ |
SUBSCRIPTION_002 |
500 | νΈμ μλ¦Ό μ μ‘ μ€ν¨ |
SUBSCRIPTION_003 |
400 | μ ν¨νμ§ μμ ꡬλ μλν¬μΈνΈ URL |
μλ¦Ό μ΄λ ₯ μ μ₯ ν μ΄λΈ. 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 = νμ±) |
Web Push ꡬλ μ 보. κ³ κ°λΉ μ΅λ 1건.
| μ»¬λΌ | νμ | μ€λͺ |
|---|---|---|
id |
BIGINT PK | μλ μ¦κ° |
endpoint |
VARCHAR (UNIQUE) | λΈλΌμ°μ Push μλν¬μΈνΈ URL |
p256dh |
VARCHAR | P-256 ECDH 곡κ°ν€ |
auth |
VARCHAR | μΈμ¦ μν¬λ¦Ώ |
customer_id |
BIGINT | ꡬλ μμ κ³ κ° ID |
μ΄ μλΉμ€λ μλ ν
μ΄λΈμ μ‘°νλ§ μννλ€ (μ°κΈ° μ±
μμ api-core / processor-usageμ μμ):
| ν μ΄λΈ | μ©λ |
|---|---|
customer |
κ³ κ° κΈ°λ³Έ μ 보 |
customer_quota |
κ³ κ°λ³ μκ° μ¬μ©λ/νλ (SSE member λ°μ΄ν°) |
family |
κ°μ‘± κ·Έλ£Ή μ 보 |
family_member |
κ°μ‘± ꡬμ±μ λ§€ν β customerId β familyId, role |
family_quota |
κ°μ‘± μκ° μ΄ μ¬μ©λ/μΏΌν° (SSE 1μ΄ ν΄λ§ λμ) |
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) |
| λ³μ | κΈ°λ³Έκ° | μ€λͺ |
|---|---|---|
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 λΉνμ±ν μ¬λΆ |
| Profile | μ©λ |
|---|---|
local |
λ‘컬 κ°λ° (κΈ°λ³Έ) |
dev |
κ°λ° μλ² |
prod |
μ΄μ νκ²½ |
.env νμΌμ νλ‘μ νΈ λ£¨νΈμ λλ©΄ bootRun μ μλμΌλ‘ νκ²½λ³μκ° λ‘λ©λλ€.
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/
# νκ²½λ³μ μ€μ (.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 κΈ°λ³Έ)
| μμ | λꡬ | μ€μ |
|---|---|---|
| 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