Problem
Problem
러닝 기록 저장 후 레벨, 챌린지, 훈련 계획 갱신까지 같은 요청에서 처리하면 사용자 입력 응답이 후속 작업에 묶이는 문제가 있었습니다.
Decision
Decision
활동 저장의 핵심 트랜잭션과 후속 갱신을 Spring Event 기반으로 분리하고, 반복 조회는 Redis 캐시와 JPA 조회 최적화로 낮추는 방향을 택했습니다.
Build
Implementation
- @TransactionalEventListener(AFTER_COMMIT)와 @Async로 후속 갱신을 커밋 이후 비동기 처리했습니다.
- 활동 요약, 챌린지, 계획 조회에는 목적별 TTL을 둔 Redis 캐시를 적용하고 저장 이후 무효화 경계를 고려했습니다.
- JOIN FETCH와 인덱스 정리로 활동 조회 시 연관 데이터 접근 비용을 낮췄습니다.
사용자 체감 성능은 전체 작업 완료 시간이 아니라 요청의 책임 경계를 어디까지 잡는지에 크게 좌우된다는 점을 설명할 수 있는 사례입니다.
Proof
Verification
docs/PERFORMANCE.md와 k6 결과 파일에서 activityCreation after 평균 3~5ms, 요약 조회 7.43ms에서 1.02ms 수준의 측정 근거를 확인했습니다.
비동기 이벤트 적용 후 activityCreation after 평균
Redis 캐싱 적용 전후 summary 평균 응답
레벨·챌린지·훈련 계획 갱신을 AFTER_COMMIT 이후 처리
avg 3~5ms after
활동 저장
- Scenario
- 활동 저장 후 레벨·챌린지·훈련 계획 업데이트
- Method
- @TransactionalEventListener(AFTER_COMMIT)와 @Async 적용 후 k6 결과 확인
- Result
- 문서화된 after 측정에서 activityCreation 평균 3~5ms 수준을 확인했습니다.
7.43ms → 1.02ms
요약 조회
- Scenario
- 활동 요약 반복 조회
- Method
- Redis TTL 캐시 적용 전후 응답 시간 측정
- Result
- summary 조회 평균 응답 시간이 7.43ms에서 1.02ms로 낮아졌습니다.
Boundaries
Trade-offs & Limitations
Trade-offs
- 비동기 이벤트는 응답 시간을 줄이지만 후속 작업 실패를 재시도·관찰하는 장치가 필요합니다.
- 캐시는 조회 속도를 높이지만 활동 저장 후 무효화 시점을 놓치면 오래된 요약을 보여줄 수 있습니다.
Limitations
- 개인 사용 흐름 중심으로 검증해 대규모 사용자 트래픽은 별도 검증이 필요합니다.
- HealthKit 연동은 iOS 클라이언트 범위이며 백엔드 지표와 분리해 해석해야 합니다.
아키텍처
전체 아키텍처▼
architecture
전체 아키텍처
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Client
사용자 진입과 실시간 상태 확인
React Web
Clients
SwiftUI iOS
Clients
Application
API, 도메인 로직, 워커 처리
REST API
Backend
JWT 인증
Backend
Spring Events
Backend
Async Listeners
Backend
Nginx
Infra
NCP Cloud
Infra
Data / Messaging
상태 저장, 캐시, 이벤트 전달
PostgreSQL
Data
Redis
Data
핵심 연결 흐름
- 1. REST API → JWT 인증
- 2. REST API → Spring Events
- 3. Spring Events → Async Listeners
- 4. React Web → Nginx
- 5. SwiftUI iOS → Nginx
- 6. Nginx → REST API
- 7. REST API → PostgreSQL
- 8. REST API → Redis
- 9. Async Listeners → PostgreSQL
원본 Mermaid 보기
flowchart LR
subgraph Clients
Web[React Web]
iOS[SwiftUI iOS]
end
subgraph Backend["Spring Boot"]
API[REST API]
JWT[JWT 인증]
Event[Spring Events]
Async[Async Listeners]
API --> JWT
API --> Event
Event --> Async
end
subgraph Data
PG[(PostgreSQL)]
Redis[(Redis)]
end
subgraph Infra
Nginx[Nginx]
NCP[NCP Cloud]
end
Web --> Nginx
iOS --> Nginx
Nginx --> API
API --> PG
API --> Redis
Async --> PGERD
전체 ERD▼
erd
전체 ERD
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
users
- bigint
- id
- PK
- varchar
- UK
- varchar
- password
- varchar
- nickname
- int
- level
- double
- total_distance
+ 2개 필드
running_activity
- bigint
- id
- PK
- bigint
- user_id
- FK
- double
- distance
- int
- duration
- double
- pace
- int
- heart_rate
+ 4개 필드
challenge
- bigint
- id
- PK
- varchar
- name
- enum
- type
- double
- target_distance
- int
- target_count
- date
- start_date
+ 2개 필드
user_challenge
- bigint
- id
- PK
- bigint
- user_id
- FK
- bigint
- challenge_id
- FK
- double
- current_distance
- int
- current_count
- datetime
- joined_at
+ 1개 필드
training_plan
- bigint
- id
- PK
- varchar
- name
- enum
- goal_type
- enum
- difficulty
plan_week
- bigint
- id
- PK
- bigint
- plan_id
- FK
- int
- week_number
- double
- target_distance
- int
- target_run_count
user_plan
- bigint
- id
- PK
- bigint
- user_id
- FK
- bigint
- plan_id
- FK
- int
- current_week
- datetime
- started_at
- datetime
- completed_at
관계 요약
- #1 users ||--o{ running_activity · 기록
- #2 users ||--o{ user_challenge · 참여
- #3 users ||--o{ user_plan · 등록
- #4 challenge ||--o{ user_challenge · 포함
- #5 training_plan ||--o{ plan_week · 구성
- #6 training_plan ||--o{ user_plan · 포함
원본 Mermaid 보기
erDiagram
users ||--o{ running_activity : "기록"
users ||--o{ user_challenge : "참여"
users ||--o{ user_plan : "등록"
challenge ||--o{ user_challenge : "포함"
training_plan ||--o{ plan_week : "구성"
training_plan ||--o{ user_plan : "포함"
users {
bigint id PK
varchar email UK
varchar password
varchar nickname
int level
double total_distance
double weight
double height
}
running_activity {
bigint id PK
bigint user_id FK
double distance
int duration
double pace
int heart_rate
int cadence
json route
varchar memo
datetime created_at
}
challenge {
bigint id PK
varchar name
enum type
double target_distance
int target_count
date start_date
date end_date
int recommended_level
}
user_challenge {
bigint id PK
bigint user_id FK
bigint challenge_id FK
double current_distance
int current_count
datetime joined_at
datetime completed_at
}
training_plan {
bigint id PK
varchar name
enum goal_type
enum difficulty
}
plan_week {
bigint id PK
bigint plan_id FK
int week_number
double target_distance
int target_run_count
}
user_plan {
bigint id PK
bigint user_id FK
bigint plan_id FK
int current_week
datetime started_at
datetime completed_at
}시퀀스 다이어그램
활동 기록 (이벤트 비동기)▼
sequence
활동 기록 (이벤트 비동기)
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
Client → Controller
POST /api/activities
- 2처리
Controller → ActivityService
createActivity
- 3저장
ActivityService → ActivityService
save to DB
- 4이벤트
ActivityService → EventPublisher
publish ActivityCreatedEvent
- 5처리
ActivityService → Controller
Activity
- 6처리
Controller → Client
201 Created
- 7노트
Note · EventPublisher
AFTER_COMMIT 비동기 처리
- 8par 병렬 실행
control
par 병렬 실행
조건: par 병렬 실행
+ 8개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Client → Controller | POST /api/activities | - |
| 2 | Controller → ActivityService | createActivity | - |
| 3 | ActivityService → ActivityService | save to DB | - |
| 4 | ActivityService → EventPublisher | publish ActivityCreatedEvent | - |
| 5 | ActivityService → Controller | Activity | - |
| 6 | Controller → Client | 201 Created | - |
| 7 | Note · EventPublisher | AFTER_COMMIT 비동기 처리 | - |
| 8 | control | par 병렬 실행 | par 병렬 실행 |
| 9 | EventPublisher → UserLevelListener | onActivityCreated | par 병렬 실행 |
| 10 | UserLevelListener → UserLevelListener | updateUserLevel | par 병렬 실행 |
| 11 | control | and | and |
| 12 | EventPublisher → ChallengeListener | onActivityCreated | and |
| 13 | ChallengeListener → ChallengeListener | updateChallengeProgress | and |
| 14 | control | and | and |
| 15 | EventPublisher → PlanListener | onActivityCreated | and |
| 16 | PlanListener → PlanListener | updatePlanProgress | and |
원본 Mermaid 보기
sequenceDiagram
participant C as Client
participant API as Controller
participant AS as ActivityService
participant E as EventPublisher
participant UL as UserLevelListener
participant CL as ChallengeListener
participant PL as PlanListener
C->>API: POST /api/activities
API->>AS: createActivity
AS->>AS: save to DB
AS->>E: publish ActivityCreatedEvent
AS-->>API: Activity
API-->>C: 201 Created
Note over E,PL: AFTER_COMMIT 비동기 처리
par 병렬 실행
E->>UL: onActivityCreated
UL->>UL: updateUserLevel
and
E->>CL: onActivityCreated
CL->>CL: updateChallengeProgress
and
E->>PL: onActivityCreated
PL->>PL: updatePlanProgress
end챌린지 참여▼
sequence
챌린지 참여
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
C → API
POST /api/challenges/{id}/join
- 2처리
API → CS
joinChallenge
- 3검증
CS → CS
validate eligibility
- 4저장
CS → UC
save UserChallenge
- 5처리
API → C
200 OK
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | C → API | POST /api/challenges/{id}/join | - |
| 2 | API → CS | joinChallenge | - |
| 3 | CS → CS | validate eligibility | - |
| 4 | CS → UC | save UserChallenge | - |
| 5 | API → C | 200 OK | - |
원본 Mermaid 보기
sequenceDiagram
C->>API: POST /api/challenges/{id}/join
API->>CS: joinChallenge
CS->>CS: validate eligibility
CS->>UC: save UserChallenge
API-->>C: 200 OK훈련 계획 시작▼
sequence
훈련 계획 시작
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
C → API
POST /api/plans/{id}/start
- 2처리
API → PS
startPlan
- 3응답
PS → PS
check not already enrolled
- 4저장
PS → UP
save UserPlan
- 5처리
API → C
200 OK
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | C → API | POST /api/plans/{id}/start | - |
| 2 | API → PS | startPlan | - |
| 3 | PS → PS | check not already enrolled | - |
| 4 | PS → UP | save UserPlan | - |
| 5 | API → C | 200 OK | - |
원본 Mermaid 보기
sequenceDiagram
C->>API: POST /api/plans/{id}/start
API->>PS: startPlan
PS->>PS: check not already enrolled
PS->>UP: save UserPlan
API-->>C: 200 OK