Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
318 changes: 58 additions & 260 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 커버리지 리포트

<details>
<summary>목차</summary>

- [실행 방법](#실행-방법)
- [주요 기능](#주요-기능)
- [사용 기술](#사용-기술)
- [성능 테스트](#성능-테스트)
- [아키텍처](#아키텍처)
- [ERD](#erd)

</details>

<table>
<tr>
<td align="center">
<img src="https://github.com/user-attachments/assets/29880bf4-fe0b-4d6e-8473-3ffd1f1deaaf" alt="앨범 생성" width="300"><br>
<b>프로젝트 시작</b>
</td>
<td align="center">
<img src="https://github.com/user-attachments/assets/e109d07f-ef0c-4594-ad6b-9a2661512717" alt="이미지 업로드" width="300"><br>
<b>회원가입 & 로그인</b>
</td>
</tr>
<tr>
<td align="center">
<img src="https://github.com/user-attachments/assets/a6753028-b951-4c40-8da6-1b573d482818" alt="편지 작성" width="300"><br>
<b>앨범 생성</b>
</td>
<td align="center">
<img src="https://github.com/user-attachments/assets/17f478c4-8d78-4df3-9562-41607eef195d" alt="스티커 관리" width="300"><br>
<b>편지 생성</b>
</td>
</tr>
</table>
## 주요 기능

---
- 회원가입, 로그인, 로그아웃과 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<Object[]> 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
Expand All @@ -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 }
```

---

## 시퀀스 다이어그램

<details>
<summary>로그인</summary>

```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
```
</details>
기존 README와 `load-test/README.md`에는 편지 목록 조회의 photo count 계산을 서브쿼리 기반 조회로 줄인 비교 기록이 정리되어 있습니다.

<details>
<summary>앨범 생성</summary>

```mermaid
sequenceDiagram
C->>API: POST /api/albums/create
API->>AS: hasAlbum, createAlbum
AS->>AR: save
API-->>C: 201 { Album }
```
</details>

<details>
<summary>편지 작성</summary>

```mermaid
sequenceDiagram
C->>API: POST /api/albums/{id}/create
API->>LS: createLetter
LS->>LR: save
API-->>C: 201 { Letter }
```
</details>

<details>
<summary>사진 업로드</summary>

```mermaid
sequenceDiagram
C->>API: POST /api/letters/{id}/photos
API->>PS: addPhoto
PS->>S3: uploadFile
PS->>PR: save
API-->>C: 201 { Photo }
```
</details>
## 참고 사항

- 로컬 기본 설정은 S3 접근 키가 없어도 애플리케이션이 기동되도록 더미 값을 둡니다.
- 실제 S3 업로드를 사용하려면 `CLOUD_AWS_S3_BUCKET`, `CLOUD_AWS_REGION`, `CLOUD_AWS_ACCESS_KEY`, `CLOUD_AWS_SECRET_KEY`를 환경에 맞게 지정해야 합니다.
Loading