grabPT
지역 기반으로 트레이너와 사용자를 연결하고, 상담부터 계약·결제까지 지원하는 1:1 PT 매칭 플랫폼입니다.
Period
2025.06 — Present
Tech stack
Overview
grabPT는 지역을 기반으로 다양한 운동 종목의 트레이너와 사용자를 1:1로 연결하는 웹 플랫폼입니다. 사용자가 PT 요청서를 작성하면 트레이너가 제안서를 보내고, 실시간 채팅으로 조건을 조율한 뒤 계약서 작성과 결제까지 하나의 흐름에서 진행할 수 있습니다.
초기 개발은 2025년 6월부터 8월까지 진행했으며, 현재 운영 경험과 성능 측정 결과를 바탕으로 리팩터링하고 있습니다.
Architecture
Web Client
├─ REST API ───────────────→ Spring Boot ─→ MySQL
├─ WebSocket / STOMP ──────→ Chat Service ─→ Redis
└─ Contract & Asset Upload ────────────────→ S3
GitHub Actions → Docker Image → AWS Deployment핵심 비즈니스 데이터는 MySQL에 저장하고, 실시간 채팅과 안 읽은 메시지 집계에는 Redis를 사용했습니다. 계약서와 서비스 자산은 S3에 저장하며, GitHub Actions와 Docker를 이용해 배포 과정을 자동화했습니다.
Tech Stack
| 영역 | 기술 | 활용 목적 |
|---|---|---|
| Backend | Java, Spring Boot | 요청서·제안서·계약·결제 도메인 API 구현 |
| Database | MySQL | 관계형 비즈니스 데이터 관리 |
| Realtime | WebSocket, STOMP, Redis | 1:1 채팅 및 안 읽은 메시지 집계 |
| Infrastructure | AWS, Docker, S3 | 애플리케이션 실행 환경과 파일 저장소 구성 |
| CI/CD & Test | GitHub Actions, k6 | 배포 자동화와 부하 테스트 |
Team
- Product Manager 1명
- Designer 1명
- Frontend 2명
- Backend 3명
My Role
- 서비스 도메인에 맞는 REST API 설계 및 구현
- WebSocket과 STOMP 기반 1:1 실시간 채팅 기능 구현
- k6 부하 테스트를 통한 채팅방 목록 조회 병목 분석
- Redis 카운터 기반의 안 읽은 메시지 집계 구조 설계 및 적용
Key Features
- 사용자 PT 요청서와 트레이너 제안서 작성
- 트레이너–사용자 1:1 매칭 및 실시간 채팅
- 계약서 작성 및 PDF 변환
- 결제 및 정산
Problem 1. 데이터베이스 운영 비용
프로젝트 운영 중 AWS RDS 프리 티어 한도를 초과하면서 데이터베이스 고정 비용이 서비스 유지에 부담이 되었습니다. 트래픽과 팀 규모를 고려했을 때, 관리형 데이터베이스의 편의성보다 당장의 운영 지속 가능성을 우선할 필요가 있었습니다.
Troubleshooting
RDS에서 사용하던 MySQL을 EC2 내부의 Docker 컨테이너로 이전했습니다. 애플리케이션과 데이터베이스의 실행 환경을 코드로 관리해 개발 환경과 운영 환경의 차이를 줄이고, 기존 데이터와 스키마를 유지하며 이전했습니다.
Docker 기반 MySQL은 비용을 줄이는 대신 백업, 복구, 모니터링과 장애 대응의 책임이 서비스 팀으로 이동합니다. 운영 규모가 커지면 관리형 데이터베이스로의 재전환을 포함해 가용성 전략을 다시 검토할 계획입니다.
Result
기존 서비스 데이터를 유지하면서 데이터베이스 운영 비용을 낮췄고, Docker를 기준으로 실행 환경을 통일해 배포와 장애 재현 과정을 단순화했습니다.
Problem 2. 채팅방 목록 조회 병목
채팅방 목록에는 각 방의 안 읽은 메시지 수가 함께 표시됩니다. 초기 구현은 조회할 때마다 GROUP BY와 상관 서브쿼리로 이 값을 계산했습니다. 메시지 한 건마다 독립적인 서브쿼리가 실행되고 그룹화를 위한 스캔까지 발생해, 채팅방과 메시지가 늘어날수록 응답 시간이 급격히 증가했습니다.
k6로 가상 사용자 50명이 25초 동안 요청하는 시나리오를 실행했습니다. 채팅방이 1개인 사용자에게서는 p95가 59ms였지만, 채팅방이 4개인 사용자에게서는 p95가 4.63초까지 증가했습니다.
Troubleshooting
안 읽은 메시지 수를 조회 시점에 계산하지 않고, 메시지가 생성되는 시점에 미리 집계하는 구조로 변경했습니다.
- 새 메시지 수신 시 Redis
INCR로 수신자의 채팅방 카운터를 원자적으로 증가시켰습니다. - 사용자가 채팅방에 입장해 메시지를 읽으면
SET으로 해당 카운터를 초기화했습니다. - 채팅방 목록 조회 시
MGET으로 여러 방의 카운터를 한 번에 가져왔습니다. - 병목의 원인이었던
MessageRepository의COUNT ... GROUP BY집계 쿼리를 제거했습니다.
Result
동일한 조건에서 다시 측정한 결과입니다.
| 지표 | 개선 전 | 개선 후 | 변화 |
|---|---|---|---|
| p95 응답 시간 | 4.63초 | 0.76초 | 약 83% 단축 |
| 평균 응답 시간 | 1.63초 | 0.21초 | 약 87% 단축 |
| 처리량 | 15.1 req/s | 32.8 req/s | 약 2.1배 증가 |
Retrospective
정확성만 보면 문제가 없어 보이는 로직도 동시 요청과 데이터 누적이 발생하는 운영 환경에서는 구조적인 한계를 드러낼 수 있다는 점을 배웠습니다. 특히 병목을 추측에 의존하지 않고 부하 테스트 수치로 재현하고, 데이터 접근 방식을 변경한 뒤 같은 조건에서 다시 검증하는 과정의 중요성을 체감했습니다.
또한 데이터베이스 비용 최적화처럼 단기적인 이점이 명확한 선택에도 운영 책임이라는 반대급부가 있음을 경험했습니다. 앞으로는 성능뿐 아니라 백업, 복구, 모니터링까지 포함한 운영 관점에서 기술적 결정을 내리고자 합니다.