프로젝트 목록으로 돌아가기
Recruiter Briefrunning-app

Running App

러닝 기록 앱에서 활동 저장 후속 작업을 비동기로 분리하고 Redis 캐시를 적용해 조회 응답 병목을 낮췄습니다.

Role
풀스택 및 백엔드 개선
Period
2025
Team
개인 프로젝트
Theme
Performance
Java 17Spring Boot 3Spring SecurityJWTSpring Data JPAPostgreSQLRedisSpring Events

30초 요약

문제
활동 저장 후 레벨·챌린지·계획 갱신이 요청 경로를 길게 만들 수 있었습니다.
해결
@TransactionalEventListener(AFTER_COMMIT), @Async, Redis TTL 캐시로 요청 책임을 분리했습니다.
검증
문서화된 after 측정에서 activityCreation 평균 3~5ms, summary 조회 7.43ms → 1.02ms를 확인했습니다.

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 수준의 측정 근거를 확인했습니다.

3~5ms
Measured
활동 저장 API

비동기 이벤트 적용 후 activityCreation after 평균

7.43ms → 1.02ms
Measured
요약 API

Redis 캐싱 적용 전후 summary 평균 응답

Async
Designed
후속 작업 분리

레벨·챌린지·훈련 계획 갱신을 AFTER_COMMIT 이후 처리

Measured

avg 3~5ms after

활동 저장

Scenario
활동 저장 후 레벨·챌린지·훈련 계획 업데이트
Method
@TransactionalEventListener(AFTER_COMMIT)와 @Async 적용 후 k6 결과 확인
Result
문서화된 after 측정에서 activityCreation 평균 3~5ms 수준을 확인했습니다.
Measured

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. 1. REST API → JWT 인증
  2. 2. REST API → Spring Events
  3. 3. Spring Events → Async Listeners
  4. 4. React Web → Nginx
  5. 5. SwiftUI iOS → Nginx
  6. 6. Nginx → REST API
  7. 7. REST API → PostgreSQL
  8. 8. REST API → Redis
  9. 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 --> PG

ERD

전체 ERD

erd

전체 ERD

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

users
bigint
id
PK
varchar
email
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. #1 users ||--o{ running_activity · 기록
  2. #2 users ||--o{ user_challenge · 참여
  3. #3 users ||--o{ user_plan · 등록
  4. #4 challenge ||--o{ user_challenge · 포함
  5. #5 training_plan ||--o{ plan_week · 구성
  6. #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
ClientControllerActivityServiceEventPublisherUserLevelListenerChallengeListenerPlanListener
  1. 1요청

    Client → Controller

    POST /api/activities

  2. 2처리

    Controller → ActivityService

    createActivity

  3. 3저장

    ActivityService → ActivityService

    save to DB

  4. 4이벤트

    ActivityService → EventPublisher

    publish ActivityCreatedEvent

  5. 5처리

    ActivityService → Controller

    Activity

  6. 6처리

    Controller → Client

    201 Created

  7. 7노트

    Note · EventPublisher

    AFTER_COMMIT 비동기 처리

  8. 8par 병렬 실행

    control

    par 병렬 실행

    조건: par 병렬 실행

+ 8개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1Client → ControllerPOST /api/activities-
2Controller → ActivityServicecreateActivity-
3ActivityService → ActivityServicesave to DB-
4ActivityService → EventPublisherpublish ActivityCreatedEvent-
5ActivityService → ControllerActivity-
6Controller → Client201 Created-
7Note · EventPublisherAFTER_COMMIT 비동기 처리-
8controlpar 병렬 실행par 병렬 실행
9EventPublisher → UserLevelListeneronActivityCreatedpar 병렬 실행
10UserLevelListener → UserLevelListenerupdateUserLevelpar 병렬 실행
11controlandand
12EventPublisher → ChallengeListeneronActivityCreatedand
13ChallengeListener → ChallengeListenerupdateChallengeProgressand
14controlandand
15EventPublisher → PlanListeneronActivityCreatedand
16PlanListener → PlanListenerupdatePlanProgressand
원본 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
CAPICSUC
  1. 1요청

    C → API

    POST /api/challenges/{id}/join

  2. 2처리

    API → CS

    joinChallenge

  3. 3검증

    CS → CS

    validate eligibility

  4. 4저장

    CS → UC

    save UserChallenge

  5. 5처리

    API → C

    200 OK

전체 메시지 상세
StepFrom → ToMessageCondition
1C → APIPOST /api/challenges/{id}/join-
2API → CSjoinChallenge-
3CS → CSvalidate eligibility-
4CS → UCsave UserChallenge-
5API → C200 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
CAPIPSUP
  1. 1요청

    C → API

    POST /api/plans/{id}/start

  2. 2처리

    API → PS

    startPlan

  3. 3응답

    PS → PS

    check not already enrolled

  4. 4저장

    PS → UP

    save UserPlan

  5. 5처리

    API → C

    200 OK

전체 메시지 상세
StepFrom → ToMessageCondition
1C → APIPOST /api/plans/{id}/start-
2API → PSstartPlan-
3PS → PScheck not already enrolled-
4PS → UPsave UserPlan-
5API → C200 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