|
| 1 | +# TrainUs Backend |
| 2 | +> 프로그래머스 클라우드 기반 백엔드 데브코스 최종 프로젝트 최우수상 수상 프로젝트 |
| 3 | +
|
| 4 | +TrainUs는 지역 기반으로 운동 메이트와 트레이너를 찾고 레슨 개설부터 신청, 결제, 리뷰까지 연결하는 운동 클래스 매칭 플랫폼입니다. |
| 5 | + |
| 6 | +## 사용 스택 |
| 7 | + |
| 8 | +### Backend |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +### Database / Cache |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +### Infra / CI/CD |
| 22 | + |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +### Monitoring / Test |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | + |
| 34 | +## 프로젝트 소개 |
| 35 | + |
| 36 | +운동을 꾸준히 하고 싶어도 함께 운동할 사람을 찾기 어렵거나 지역 기반으로 신뢰할 수 있는 트레이너와 레슨을 찾기 어려운 문제가 있습니다. |
| 37 | + |
| 38 | +TrainUs는 사용자가 자신의 지역을 기준으로 운동 메이트 또는 트레이너를 찾고 레슨 개설과 신청, 결제, 후기 작성까지 하나의 흐름으로 이용할 수 있도록 설계한 서비스입니다. |
| 39 | + |
| 40 | +### 주요 목표 |
| 41 | + |
| 42 | +- 혼자 하기 어려운 운동을 함께할 운동 메이트를 지역 기반으로 찾을 수 있도록 합니다. |
| 43 | +- 축구, 배드민턴 등 다인원 운동의 참여자를 쉽게 모집할 수 있도록 합니다. |
| 44 | +- 무료/유료 운동 레슨을 탐색하고 신청할 수 있는 흐름을 제공합니다. |
| 45 | +- 리뷰, 평점, 랭킹을 통해 트레이너와 레슨에 대한 신뢰도 판단 기준을 제공합니다. |
| 46 | +- 쿠폰과 결제를 통해 유료 레슨 신청 흐름을 확장할 수 있도록 합니다. |
| 47 | + |
| 48 | +### 개선 완료 목표 |
| 49 | + |
| 50 | +- 선착순 레슨 신청처럼 순간 트래픽이 몰리는 시나리오를 Redis Stream 기반 비동기 처리 구조로 고도화했습니다. |
| 51 | +- 법정동 중심 검색을 사용자 좌표 기반 반경 검색과 가까운 순 정렬까지 확장했습니다. |
| 52 | +- API 서버와 Consumer 서버를 분리해 요청 접수와 DB 반영 작업의 리소스 경합을 줄였습니다. |
| 53 | + |
| 54 | +## 팀 구성 |
| 55 | + |
| 56 | +| 구분 | 내용 | |
| 57 | +| --- | --- | |
| 58 | +| 팀명 | 3성 | |
| 59 | +| 협업 방식 | WBS 기반 일정 관리, API 명세 공유, Daily Scrum, PR 리뷰, 피어 리뷰어 지정 | |
| 60 | + |
| 61 | +<table> |
| 62 | + <tr> |
| 63 | + <th width="10%"><div align="center">항목</div></th> |
| 64 | + <th width="18%"><div align="center">지민혁</div></th> |
| 65 | + <th width="18%"><div align="center">김태호</div></th> |
| 66 | + <th width="18%"><div align="center">임창인</div></th> |
| 67 | + <th width="18%"><div align="center">김지은</div></th> |
| 68 | + <th width="18%"><div align="center">나상연</div></th> |
| 69 | + </tr> |
| 70 | + <tr align="center"> |
| 71 | + <td><strong>프로필</strong></td> |
| 72 | + <td><a href="https://github.com/Ji-minhyeok"><img src="https://github.com/Ji-minhyeok.png" width="64" /></a></td> |
| 73 | + <td><a href="https://github.com/taeho4523"><img src="https://github.com/taeho4523.png" width="64" /></a></td> |
| 74 | + <td><a href="https://github.com/cba700"><img src="https://github.com/cba700.png" width="64" /></a></td> |
| 75 | + <td><a href="https://github.com/iamjieunkim"><img src="https://github.com/iamjieunkim.png" width="64" /></a></td> |
| 76 | + <td><a href="https://github.com/ense333"><img src="https://github.com/ense333.png" width="64" /></a></td> |
| 77 | + </tr> |
| 78 | + <tr align="center"> |
| 79 | + <td><strong>역할</strong></td> |
| 80 | + <td>PO, BE</td> |
| 81 | + <td>BE 팀장</td> |
| 82 | + <td>BE</td> |
| 83 | + <td>BE</td> |
| 84 | + <td>BE</td> |
| 85 | + </tr> |
| 86 | + <tr align="center"> |
| 87 | + <td><strong>주요 담당 영역</strong></td> |
| 88 | + <td>유저 레슨<br />CI/CD<br />검색/레슨 선착순 고도화</td> |
| 89 | + <td>유저 쿠폰<br />CI/CD<br />쿠폰 선착순 고도화</td> |
| 90 | + <td>로그인/회원가입<br />프로필<br />랭킹</td> |
| 91 | + <td>강사용 레슨<br />관리자 쿠폰</td> |
| 92 | + <td>댓글<br />리뷰<br />Toss Payments 결제</td> |
| 93 | + </tr> |
| 94 | +</table> |
| 95 | + |
| 96 | +## 주요 기능 |
| 97 | + |
| 98 | +### 회원 및 프로필 |
| 99 | + |
| 100 | +- 이메일 인증 기반 회원가입과 로그인 |
| 101 | +- JWT 기반 인증 |
| 102 | +- 내 프로필 조회/수정 |
| 103 | +- 다른 사용자 프로필 조회 |
| 104 | + |
| 105 | +### 레슨 |
| 106 | + |
| 107 | +- 레슨 생성, 수정, 삭제 |
| 108 | +- 레슨 상세 조회 |
| 109 | +- 지역, 키워드, 카테고리 기반 검색 |
| 110 | +- 개설자가 신청자를 승인/거절하는 수락제 레슨 관리 |
| 111 | + |
| 112 | +**고도화/개선 사항:** |
| 113 | + |
| 114 | +- 기존 법정동 기반 검색을 사용자 좌표 기반 반경 검색으로 확장했습니다. |
| 115 | +- PostGIS `geography` 타입과 GiST 인덱스를 활용해 반경 검색과 가까운 순 정렬을 지원했습니다. |
| 116 | + |
| 117 | +### 레슨 신청 |
| 118 | + |
| 119 | +- 수락제 레슨 신청 |
| 120 | +- 선착순 레슨 신청 |
| 121 | +- 비동기 신청 상태 조회 |
| 122 | +- 내 신청 목록 조회 |
| 123 | + |
| 124 | +**고도화/개선 사항:** |
| 125 | + |
| 126 | +- 정원이 제한된 선착순 레슨에 다수의 사용자가 동시에 신청하는 상황을 병목 시나리오로 정의했습니다. |
| 127 | +- API 서버는 Redis에서 중복 신청과 재고를 먼저 확인한 뒤 접수 ID를 반환하고 DB 반영은 비동기 처리로 분리했습니다. |
| 128 | +- Consumer 서버가 Redis Stream 메시지를 batch 단위로 DB에 반영하고 사용자는 접수 ID로 처리 상태를 조회합니다. |
| 129 | + |
| 130 | + |
| 131 | + |
| 132 | +### 결제 및 쿠폰 |
| 133 | + |
| 134 | +- Toss Payments 결제 승인/취소 |
| 135 | +- 쿠폰 발급, 적용, 복원 |
| 136 | +- 쿠폰 상태 스케줄링 |
| 137 | + |
| 138 | +### 커뮤니티 |
| 139 | + |
| 140 | +- 댓글/대댓글 |
| 141 | +- 리뷰와 평점 |
| 142 | +- 리뷰와 평점 기반 트레이너 랭킹 |
| 143 | + |
| 144 | +## 주요 API |
| 145 | + |
| 146 | +| 기능 | Method | Endpoint | |
| 147 | +| --- | --- | --- | |
| 148 | +| 레슨 검색 | GET | `/api/v1/lessons` | |
| 149 | +| 위치 기반 레슨 검색 | GET | `/api/v1/lessons/search/nearby` | |
| 150 | +| 레슨 상세 조회 | GET | `/api/v1/lessons/{lessonId}` | |
| 151 | +| 수락제 레슨 신청 | POST | `/api/v1/lessons/{lessonId}/applications/approval` | |
| 152 | +| 선착순 레슨 신청 | POST | `/api/v1/lessons/{lessonId}/applications/open-run` | |
| 153 | +| 비동기 신청 상태 조회 | GET | `/api/v1/lessons/apply/status/{requestId}` | |
| 154 | +| 레슨 신청 취소 | DELETE | `/api/v1/lessons/{lessonId}/application` | |
| 155 | +| 내 신청 목록 조회 | GET | `/api/v1/lessons/my-applications` | |
| 156 | +| 개설 레슨 신청자 조회 | GET | `/api/v1/lessons/{lessonId}/applications` | |
| 157 | +| 레슨 신청 승인/거절 | POST | `/api/v1/lessons/applications/{lessonApplicationId}` | |
| 158 | +| 랭킹 조회 | GET | `/api/v1/rankings` | |
| 159 | +| 결제 승인 | POST | `/api/v1/payments/confirm` | |
| 160 | +| S3 업로드 URL 발급 | GET | `/api/v1/s3/posturl` | |
| 161 | + |
| 162 | +Swagger 설정이 포함되어 있어 실행 환경에서 API 문서를 확인할 수 있습니다. |
| 163 | + |
| 164 | +## 시스템 구조 |
| 165 | + |
| 166 | + |
| 167 | + |
| 168 | +운영 환경에서는 API 서버와 Consumer 서버를 역할별 profile로 분리합니다. |
| 169 | + |
| 170 | +| Role | App Port | Actuator Port | 주요 책임 | |
| 171 | +| --- | ---: | ---: | --- | |
| 172 | +| `api` | 8080 | 8082 | HTTP 요청 수신, 인증, 레슨 검색, 신청 접수 | |
| 173 | +| `consumer` | 8081 | 8083 | Redis Stream 소비, DB 반영, 보정 스케줄링 | |
| 174 | + |
| 175 | +Redis는 목적에 따라 Core Redis와 MQ Redis로 분리해 사용합니다. |
| 176 | + |
| 177 | +| Redis | 기본 포트 | 용도 | |
| 178 | +| --- | ---: | --- | |
| 179 | +| `redis-core` | 6379 | 재고, 중복 신청, 대기열, 분산 락 | |
| 180 | +| `redis-mq` | 6380 | Redis Stream, 신청 처리 상태 | |
| 181 | + |
| 182 | +## 배포 자동화 |
| 183 | + |
| 184 | +배포는 GitHub Actions, S3, CodeDeploy를 사용합니다. |
| 185 | +GitHub Actions가 빌드 산출물을 S3에 업로드하고 CodeDeploy가 EC2에서 배포 hook을 실행하는 Pull 방식으로 구성했습니다. |
| 186 | + |
| 187 | +```text |
| 188 | +develop push |
| 189 | + -> GitHub Actions build |
| 190 | + -> deploy.zip 생성 |
| 191 | + -> S3 업로드 |
| 192 | + -> CodeDeploy 배포 생성 |
| 193 | + -> EC2에서 appspec hook 실행 |
| 194 | +``` |
| 195 | + |
| 196 | +CodeDeploy hook: |
| 197 | + |
| 198 | +| Script | 역할 | |
| 199 | +| --- | --- | |
| 200 | +| `scripts/setup.sh` | 배포 디렉터리 준비, S3에서 환경변수 파일 동기화 | |
| 201 | +| `scripts/stop.sh` | 기존 애플리케이션 종료 | |
| 202 | +| `scripts/start.sh` | 서버 역할에 맞는 Spring profile로 애플리케이션 실행 | |
| 203 | + |
| 204 | +## 협업 방식 |
| 205 | + |
| 206 | +- Notion 기반 WBS와 작업 보드로 기능 단위 진행 상황을 관리했습니다. |
| 207 | +- API 명세를 공유하며 프론트엔드와 요청/응답 형식을 조율했습니다. |
| 208 | +- Git commit convention, code convention, class naming convention을 사전에 정리했습니다. |
| 209 | +- PR 리뷰와 피어 리뷰어 지정 방식을 통해 코드 변경 사항을 검토했습니다. |
| 210 | +- Daily Scrum과 회의록을 통해 이슈와 의사결정 내용을 기록했습니다. |
| 211 | + |
| 212 | +## 주요 도메인 구조 |
| 213 | + |
| 214 | +```text |
| 215 | +src/main/java/com/threestar/trainus |
| 216 | + ├── domain |
| 217 | + │ ├── user # 회원, 이메일 인증 |
| 218 | + │ ├── profile # 프로필 |
| 219 | + │ ├── lesson # 레슨 생성/검색/신청/선착순 처리 |
| 220 | + │ ├── coupon # 쿠폰 생성/발급/사용 |
| 221 | + │ ├── payment # Toss 결제/취소 |
| 222 | + │ ├── review # 리뷰 |
| 223 | + │ ├── comment # 댓글/대댓글 |
| 224 | + │ ├── ranking # 랭킹 |
| 225 | + │ └── file # S3 URL 발급 |
| 226 | + └── global |
| 227 | + ├── config # Security, Redis, S3, Swagger, Scheduler |
| 228 | + ├── resolver # @LoginUser |
| 229 | + ├── aop # Redisson distributed lock |
| 230 | + └── exception # 공통 예외 처리 |
| 231 | +``` |
0 commit comments