From 809a583d6df21485e5a47659784ea4e4c7eb005a Mon Sep 17 00:00:00 2001 From: jinhyuk9714 Date: Wed, 6 May 2026 23:42:28 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=ED=95=9C=EA=B5=AD=EC=96=B4=20README=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 318 ++++++++++-------------------------------------------- 1 file changed, 58 insertions(+), 260 deletions(-) diff --git a/README.md b/README.md index 0223ecb..b3d3745 100644 --- a/README.md +++ b/README.md @@ -1,172 +1,84 @@ -# Memory of Year (백엔드) +# Memory of Year -[![CI](https://github.com/jinhyuk9714/memory_of_year/actions/workflows/ci.yml/badge.svg)](https://github.com/jinhyuk9714/memory_of_year/actions) -![Java 17](https://img.shields.io/badge/Java-17-ED8B00?logo=openjdk) -![Spring Boot 3](https://img.shields.io/badge/Spring%20Boot-3.3-6DB33F?logo=springboot) -![MySQL](https://img.shields.io/badge/MySQL-8.0-4479A1?logo=mysql) -![Docker](https://img.shields.io/badge/Docker-Compose-2496ED?logo=docker) +Memory of Year는 앨범, 편지, 사진, 스티커를 관리하는 Spring Boot REST API 백엔드입니다. JWT 인증, S3 파일 업로드, MySQL/H2 실행 환경, Swagger 문서, k6 부하 테스트 스크립트를 포함합니다. -앨범·편지·사진·스티커를 관리하는 REST API 백엔드입니다. +## 문제 의식 -**멋쟁이사자처럼 12기** 팀 **멋삼핑** | 디자인 1명, 프론트 2명, 백엔드 4명 +추억을 앨범 단위로 모으는 서비스에서는 사용자 인증, 미디어 업로드, 앨범별 편지/사진 조회가 함께 동작해야 합니다. 이 저장소는 그 흐름을 REST API로 나누고, 목록 조회에서 발생할 수 있는 N+1 쿼리를 줄이는 방식까지 함께 정리합니다. -### 포트폴리오 핵심 - -- **N+1 최적화**: 편지 목록 API 쿼리 31회 → 1회, p95 응답시간 **40% 개선** -- **부하 테스트**: k6로 회원가입·로그인·앨범·편지 API 성능 측정 -- **Docker·MySQL**: `docker compose up` 한 번에 실행 환경 구성 -- **CI/CD**: GitHub Actions로 push 시 자동 테스트, JaCoCo 커버리지 리포트 - -
-목차 - -- [실행 방법](#실행-방법) -- [주요 기능](#주요-기능) -- [사용 기술](#사용-기술) -- [성능 테스트](#성능-테스트) -- [아키텍처](#아키텍처) -- [ERD](#erd) - -
- - - - - - - - - - -
- 앨범 생성
- 프로젝트 시작 -
- 이미지 업로드
- 회원가입 & 로그인 -
- 편지 작성
- 앨범 생성 -
- 스티커 관리
- 편지 생성 -
+## 주요 기능 ---- +- 회원가입, 로그인, 로그아웃과 JWT 필터 기반 인증 +- 앨범 생성, 조회, 수정 +- 앨범 안의 편지 작성, 목록, 상세 조회 +- 사진 업로드와 조회, 스티커 업로드와 목록 조회 +- AWS S3 연동 설정과 로컬 기본값 +- 공통 응답 형식과 전역 예외 처리 +- Swagger UI, Actuator health/metrics/info +- k6 기반 회원가입, 로그인, 앨범 조회, 편지 목록 부하 테스트 + +## 기술 스택 + +| 영역 | 기술 | +| --- | --- | +| Backend | Java 17, Spring Boot 3.3.5, Spring Web, Spring Security | +| Persistence | Spring Data JPA, MySQL 8, H2 | +| Storage | AWS SDK S3 | +| API Docs | springdoc-openapi | +| Test / Quality | JUnit 5, Spring Security Test, JaCoCo, k6 | +| Infra | Docker, Docker Compose, Gradle | + +## 구조 + +```text +src/main/java/com/demo/album/ +├── config/ # Security, Swagger, S3, Web 설정 +├── controller/ # User, Album, Letter, Photo, Sticker API +├── dto/ # 요청/응답 DTO와 공통 응답 +├── entity/ # User, Album, Letter, Photo, Sticker +├── exception/ # 전역 예외 처리 +├── filter/ # JWT 필터 +├── repository/ # JPA Repository +├── service/ # 비즈니스 로직 +└── util/ # JwtTokenProvider +``` ## 실행 방법 -### 로컬 (Gradle) - -- **Java 17**, **Gradle** 필요 +기본 프로필은 H2 인메모리 DB를 사용합니다. ```bash -# H2 (기본) ./gradlew bootRun - -# MySQL -./gradlew bootRun --args='--spring.profiles.active=mysql' -# 또는 MySQL 실행 후 MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD 환경변수 설정 ``` -### Docker +MySQL 프로필로 실행하려면 Docker Compose를 사용하거나 MySQL 환경 변수를 설정합니다. ```bash -# MySQL 연동 (기본) docker compose up --build - -# H2 전용 (MySQL 없이) -docker compose -f docker-compose.h2.yml up --build ``` -실행 후 API 문서: http://localhost:8080/swagger-ui.html - ---- - -## 주요 기능 - -| 기능 | 설명 | -|------|------| -| 인증 | JWT 회원가입·로그인·로그아웃(토큰 무효화) | -| 앨범 | 생성·조회·수정 (제목, 색상, 공개 여부, 스티커) | -| 편지 | 작성·목록·상세 (익명/작성자명, photoCount 포함) | -| 사진 | S3 업로드·목록·상세 (코멘트, 스티커 URL) | -| 홈 | 내 앨범/타인 앨범 구분, 액션 정보 | - -- 공통 응답 형식 `{ success, data, error }` · 전역 예외 처리 -- Swagger API 문서 · AWS S3 미디어 저장 · MySQL - ---- - -## 사용 기술 - -| 기술 | 용도 | -|------|------| -| Spring Boot 3 | REST API, 비즈니스 로직 | -| Spring Security + JWT | 인증·인가, 토큰 블랙리스트 | -| Spring Data JPA | 엔티티, 리포지토리 | -| MySQL / H2 | DB (로컬 기본 H2) | -| AWS S3 | 사진·스티커 저장 | -| Swagger, Lombok, Gradle | 문서화, 생산성, 빌드 | -| Docker, GitHub Actions | 컨테이너화, CI | -| JaCoCo | 테스트 커버리지 | - ---- - -## 프로젝트 구조 - -``` -src/main/java/com/demo/album/ -├── config/ Security, S3, Swagger, Web -├── controller/ REST API (auth, albums, letters, photos, stickers, home) -├── dto/ ApiResponse, 요청·응답 DTO -├── entity/ User, Album, Letter, Photo, Sticker -├── exception/ GlobalExceptionHandler -├── filter/ JwtTokenFilter -├── repository/ JPA Repository -├── service/ 비즈니스 로직 -└── util/ JwtTokenProvider +```bash +./gradlew bootRun --args='--spring.profiles.active=mysql' ``` ---- - -## 성능 테스트 - -### 부하 테스트 결과 (k6, H2 로컬) - -| API | VU | RPS | p95 | 실패율 | -|-----|-----|-----|-----|--------| -| 회원가입 | 10 | 16.5 | 115ms | 0% | -| 로그인 | 20 | 49.7 | 116ms | 0% | -| 앨범 조회 | 30 | 143.7 | 12.9ms | 0% | -| 편지 목록 (30개) | 20 | 96.7 | 13.8ms | 0% | - -### N+1 해결: 편지 목록 API - -편지 30개 + photoCount 조회 시: - -| 구분 | Before | After | -|------|--------|-------| -| DB 쿼리 | 31회 (1+N) | 1회 | -| p95 | 22.8ms | **13.8ms** (↓40%) | -| 평균 | 11.5ms | **6.7ms** (↓42%) | +H2 전용 Compose 파일도 포함되어 있습니다. -```java -// LetterRepository - 서브쿼리로 한 번에 조회 -@Query("SELECT l, (SELECT COUNT(p) FROM Photo p WHERE p.letter = l) FROM Letter l WHERE l.album.albumId = :albumId ORDER BY l.createdAt") -List findLettersWithPhotoCountByAlbumId(@Param("albumId") Long albumId); +```bash +docker compose -f docker-compose.h2.yml up --build ``` -### 앨범 조회 N+1 +테스트와 커버리지 리포트는 Gradle로 실행합니다. -- Before: 3회 (findById → owner → letters) -- After: 1회 (`@EntityGraph`) +```bash +./gradlew test +./gradlew test jacocoTestReport +``` -### 인덱스 +실행 후 API 문서는 `http://localhost:8080/swagger-ui.html`에서 확인할 수 있습니다. -`album.user_id` · `letter.album_id` · `letter.created_at` · `photo.letter_id` +## 성능 테스트 -### 테스트 실행 +`load-test/`에는 다음 k6 스크립트가 포함되어 있습니다. ```bash k6 run load-test/01-register.js @@ -175,123 +87,9 @@ k6 run load-test/03-album-get.js k6 run load-test/04-letters.js ``` -Before/After 비교: `APP_PERF_USE_N1_LETTERS=true ./gradlew bootRun` 후 04-letters 실행 → [load-test/README.md](load-test/README.md) - -### Actuator - -`/actuator/health` · `/actuator/metrics` · `/actuator/info` - -### CI / 테스트 커버리지 - -- **GitHub Actions**: push 시 `./gradlew test` 자동 실행 -- **JaCoCo**: `./gradlew test jacocoTestReport` → `build/reports/jacoco/test/html/index.html` - ---- - -## 아키텍처 - -### 시스템 구성 - -```mermaid -flowchart LR - subgraph Client - FE[프론트엔드] - end - subgraph Backend["Spring Boot"] - API[REST API] - JWT[JWT 검증] - API --> JWT - end - subgraph Data - MySQL[(MySQL)] - S3[(S3)] - end - FE --> API - JWT --> MySQL - API --> MySQL - API --> S3 -``` - -### 레이어 - -| 레이어 | 역할 | -|--------|------| -| Controller | HTTP, DTO 변환 | -| Service | 비즈니스 로직, 트랜잭션 | -| Repository | JPA CRUD | -| JwtTokenFilter | 토큰 검증, SecurityContext | - ---- - -## ERD - -```mermaid -erDiagram - user ||--o{ album : "소유" - album ||--o{ letter : "포함" - letter ||--o{ photo : "포함" - - user { bigint user_id PK varchar username UK varchar nickname UK varchar email UK } - album { bigint album_id PK bigint user_id FK varchar title varchar album_color } - letter { bigint letter_id PK bigint album_id FK varchar letter_title text content datetime created_at } - photo { bigint photo_id PK bigint letter_id FK varchar url } -``` - ---- - -## 시퀀스 다이어그램 - -
-로그인 - -```mermaid -sequenceDiagram - C->>API: POST /api/auth/login - API->>US: authenticateUser - US->>UR: findByUsername - alt 성공 - API->>JWT: createToken - API-->>C: 200 { token } - else 실패 - API-->>C: 401 - end -``` -
+기존 README와 `load-test/README.md`에는 편지 목록 조회의 photo count 계산을 서브쿼리 기반 조회로 줄인 비교 기록이 정리되어 있습니다. -
-앨범 생성 - -```mermaid -sequenceDiagram - C->>API: POST /api/albums/create - API->>AS: hasAlbum, createAlbum - AS->>AR: save - API-->>C: 201 { Album } -``` -
- -
-편지 작성 - -```mermaid -sequenceDiagram - C->>API: POST /api/albums/{id}/create - API->>LS: createLetter - LS->>LR: save - API-->>C: 201 { Letter } -``` -
- -
-사진 업로드 - -```mermaid -sequenceDiagram - C->>API: POST /api/letters/{id}/photos - API->>PS: addPhoto - PS->>S3: uploadFile - PS->>PR: save - API-->>C: 201 { Photo } -``` -
+## 참고 사항 +- 로컬 기본 설정은 S3 접근 키가 없어도 애플리케이션이 기동되도록 더미 값을 둡니다. +- 실제 S3 업로드를 사용하려면 `CLOUD_AWS_S3_BUCKET`, `CLOUD_AWS_REGION`, `CLOUD_AWS_ACCESS_KEY`, `CLOUD_AWS_SECRET_KEY`를 환경에 맞게 지정해야 합니다.