diff --git a/README.md b/README.md index b079c4a..5c031ef 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,17 @@ # Ling Level Spring API +## 프로젝트 소개 + +Ling Level API는 학습 콘텐츠, 단어 학습, 스트릭, 추천, 알림 기능을 포함하는 Spring Boot 기반 백엔드 서버입니다. + +이 프로젝트는 단순 기능 구현용 저장소가 아니라, 구조 개선, 성능 최적화, 안정성 강화, 의사결정 기록을 함께 관리하는 운영형 API 프로젝트를 목표로 합니다. + +## 문서 + +- [프로젝트 문서 허브](docs/README.md) +- [아키텍처 문서 모음](docs/architecture/) +- [의사결정 기록 모음](docs/decisions/) + ## 사전 요구사항 - JDK 17 @@ -40,4 +52,4 @@ docker-compose -f monitoring/docker-compose.monitoring-prod.yml up -d # 접속 정보 # Prometheus: http://localhost:9090 # Grafana: http://localhost:3000 (admin/admin123 또는 환경변수) -``` \ No newline at end of file +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..2b41d12 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# Project Documentation + +이 디렉터리는 프로젝트 구조와 주요 의사결정을 정리하는 문서 허브이자, 기술 블로그에 가까운 기록을 모으는 공간이다. + +## 문서 구성 + +- [아키텍처 문서 모음](architecture/) +- [의사결정 기록 모음](decisions/) +- [템플릿 모음](templates/) + +## 언제 무엇을 쓰는가 + +- 현재 구조, 도메인 관계, 대표 흐름을 정리할 때: [architecture](architecture/) +- 구조, 성능, 안정성, 테스트 전략에 영향을 주는 큰 결정을 남길 때: [decisions](decisions/) +- 새 문서를 시작할 때: [templates](templates/) + +## 기본 원칙 + +- 문서는 길게 쓰기보다 빠르게 읽히는 수준으로 유지한다. +- 자잘한 구현 선택보다 구조와 판단이 드러나는 내용만 기록한다. +- 같은 설명을 여러 문서에 반복하지 않고, 허브에서는 링크 중심으로 정리한다. +- 단순 변경 내역보다 문제 인식, 선택 이유, 결과와 남은 이슈가 드러나도록 정리한다. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..e97b1f0 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,28 @@ +# Architecture Documents + +이 디렉터리는 프로젝트의 구조와 핵심 흐름을 정리하는 문서 모음이다. + +## 포함 대상 + +- 시스템 개요 +- 도메인 관계 +- 대표 요청 흐름 +- 상태 전이 +- 외부 시스템 연결 구조 + +## 작성 기준 + +- 클래스 전체 나열보다 도메인과 흐름을 우선 정리한다. +- Mermaid 다이어그램은 핵심 구조와 흐름만 표현한다. +- 문서는 실제 리팩터링과 성능 개선 판단에 도움이 되는 수준으로 유지한다. + +## 템플릿 + +- [아키텍처 템플릿](../templates/architecture-template.md) + +## 현재 문서 + +- [프로젝트 전체 구조 개요](overview.md) +- [Streak 도메인 구조](streak.md) +- [Word 도메인 구조](word.md) +- [Book 도메인 구조](content-book.md) diff --git a/docs/architecture/content-book.md b/docs/architecture/content-book.md new file mode 100644 index 0000000..8df73e8 --- /dev/null +++ b/docs/architecture/content-book.md @@ -0,0 +1,120 @@ +# Book 도메인 구조 + +## 목적 + +이 문서는 책 콘텐츠 도메인이 조회, 진행도, import, 이미지 처리를 어떻게 함께 다루는지 설명한다. + +## 범위 + +- `BookService` +- `ChapterService` +- `ProgressService` +- 책 import 및 이미지 처리 + +## 핵심 구성 요소 + +- `BooksController`, `BooksProgressController` +- `BookService` +- `ChapterService` +- `ProgressService` +- `BookRepository`, `ChapterRepository`, `ChunkRepository`, `BookProgressRepository` + +## 구조 요약 + +Book 도메인은 사용자에게 보이는 조회 API와 운영성 있는 import 파이프라인이 함께 들어 있는 구조다. +책 기본 정보와 챕터/청크는 MongoDB에 저장되고, import 시에는 AI 결과 파일 다운로드, 이미지 이동, 썸네일 생성이 같이 수행된다. +사용자 진행도는 책 단위가 아니라 챕터와 청크를 기반으로 계산되며, 읽기 완료는 스트릭 갱신으로 이어진다. + +## Mermaid 다이어그램 + +### 구조 관계 + +```mermaid +flowchart TD + Client[Client] + Admin[Admin Import] + BooksController[BooksController] + ProgressController[BooksProgressController] + BookService[BookService] + ChapterService[ChapterService] + ProgressService[ProgressService] + Mongo[(MongoDB)] + S3[(S3 / R2)] + Streak[StreakService] + + Client --> BooksController + Client --> ProgressController + Admin --> BooksController + + BooksController --> BookService + BooksController --> ChapterService + ProgressController --> ProgressService + + BookService --> Mongo + ChapterService --> Mongo + ProgressService --> Mongo + + BookService --> S3 + ProgressService --> Streak +``` + +### 대표 흐름: 진행도 업데이트와 스트릭 연결 + +```mermaid +sequenceDiagram + participant Client + participant ProgressService + participant ChunkRepo + participant ChapterRepo + participant BookProgressRepo + participant StreakService + participant Mongo + + Client->>ProgressService: updateProgress(bookId, chunkId) + ProgressService->>ChunkRepo: load chunk + ProgressService->>ChapterRepo: resolve chapter + ProgressService->>BookProgressRepo: load or create progress + ProgressService->>Mongo: update chapter progress / normalized progress + alt last chunk in chapter + ProgressService->>StreakService: updateStreak(...) + end + ProgressService-->>Client: ProgressResponse +``` + +## 주요 흐름 설명 + +1. `BookService`는 책 조회와 import를 함께 담당한다. +2. 조회 시에는 사용자 진행도를 합쳐 `BookResponse`를 만들고, import 시에는 AI 결과 파일 다운로드와 이미지 후처리까지 수행한다. +3. `ChapterService`는 챕터 목록 조회와 탐색을 맡고, 챕터별 청크 수와 사용자 진행도를 조합해 응답을 만든다. +4. `ProgressService`는 청크 단위 요청을 챕터 단위 진행도와 책 완료 상태로 변환하고, 마지막 청크를 읽은 경우 `StreakService`를 호출한다. + +## 핵심 데이터 + +- `Book` + - 책 메타데이터, 표지 이미지, 난이도, 챕터 수 +- `Chapter` + - 챕터 번호, 설명, 읽기 시간 +- `Chunk` + - 난이도별 세부 텍스트 조각 +- `BookProgress` + - 사용자별 현재 청크, 챕터별 진행도, 완료 여부 + +## 이 도메인의 특징 + +- 조회 응답이 단순 조회가 아니라 사용자 진행도와 이미지 URL 조합을 포함한다. +- import와 조회가 가까운 서비스에 있어 운영 기능과 사용자 API가 한 도메인 아래 모여 있다. +- 챕터 완료 판단은 청크 수 기준으로 계산되고, 책 완료 여부는 챕터 진행도 배열을 기반으로 계산된다. + +## 개선 포인트 + +- `BookService`는 import, 이미지 처리, 조회 응답 조립이 함께 있어 책임 분리가 가능하다. +- `ProgressService`는 검증, 진행도 계산, 읽기 완료 처리, 스트릭 연계를 한 번에 수행한다. +- `ChapterService`는 조회 응답 조립과 view count 증가, backward compatibility 로직이 같이 들어 있다. + +## 참고 코드 + +- `src/main/java/com/linglevel/api/content/book/service/BookService.java` +- `src/main/java/com/linglevel/api/content/book/service/ChapterService.java` +- `src/main/java/com/linglevel/api/content/book/service/ProgressService.java` +- `src/main/java/com/linglevel/api/content/book/entity/Book.java` +- `src/main/java/com/linglevel/api/content/book/entity/BookProgress.java` diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..1d8630a --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,134 @@ +# 프로젝트 전체 구조 개요 + +## 목적 + +이 문서는 `llv-api`의 상위 구조를 한 장으로 설명하기 위한 문서다. +세부 구현보다 어떤 도메인이 핵심이고, 어떤 저장소와 외부 시스템이 붙어 있는지 빠르게 파악하는 데 초점을 둔다. + +## 범위 + +- 주요 사용자 요청 경로 +- 핵심 도메인 묶음 +- 공통 인프라와 외부 시스템 +- 우선 문서화 대상 도메인 + +## 핵심 구성 요소 + +- API 진입점: `controller` +- 핵심 도메인: `streak`, `word`, `content/book` +- 보조 도메인: `content/recommendation`, `fcm`, `crawling` +- 공통 인프라: MongoDB, Redis, S3/R2, Spring AI, FCM + +## 구조 요약 + +이 프로젝트는 하나의 Spring Boot 애플리케이션 안에 학습 콘텐츠, 단어 분석, 스트릭, 추천, 알림, 크롤링, 파일 처리 기능을 함께 두고 있다. +데이터 저장은 MongoDB를 중심으로 하고, Redis는 읽기 세션과 rate limit 같은 짧은 상태 관리에 사용한다. +외부 연동은 AI 모델, FCM, S3/R2, 크롤링 대상 사이트가 중심이며, 도메인 서비스가 이 인프라를 직접 조합하는 구조가 많다. + +## Mermaid 다이어그램 + +```mermaid +flowchart TD + Client[Client App] + Admin[Admin] + Api[Spring Boot API] + + Streak[Streak Domain] + Word[Word Domain] + Book[Book Domain] + Recommend[Recommendation / Notification] + Crawl[Crawling / Feed] + + Mongo[(MongoDB)] + Redis[(Redis)] + S3[(S3 / R2)] + AI[AI Model] + FCM[Firebase Cloud Messaging] + External[External Content Sites] + + Client --> Api + Admin --> Api + + Api --> Streak + Api --> Word + Api --> Book + Api --> Recommend + Api --> Crawl + + Streak --> Mongo + Streak --> Redis + Streak --> FCM + + Word --> Mongo + Word --> AI + + Book --> Mongo + Book --> S3 + Book --> Streak + + Recommend --> Mongo + Recommend --> FCM + + Crawl --> Mongo + Crawl --> External +``` + +## 주요 흐름 설명 + +1. `book`, `word`, `streak` 요청은 각각 전용 서비스로 들어가지만, 실제 사용자 학습 흐름에서는 서로 연결된다. +2. `content/book`의 읽기 완료는 `streak` 갱신과 이어지고, 읽기 로그는 `content/recommendation`에서 선호도 집계에 사용된다. +3. `word`는 MongoDB 캐시와 AI 호출을 조합해 결과를 만들고, `streak`는 Redis 읽기 세션과 MongoDB 리포트를 함께 사용한다. +4. `crawling`과 `feed`는 외부 사이트 구조 변화에 영향을 많이 받는 별도 리스크 영역이다. + +## 핵심 도메인 + +### `streak` + +- 사용자 학습 연속성, 프리즈, 보상, 알림을 담당한다. +- Redis 기반 읽기 세션과 MongoDB 기반 누적 리포트를 함께 사용한다. +- 스케줄러와 알림이 얽혀 있어 구조적으로 가장 복잡한 영역 중 하나다. + +### `word` + +- 단어 조회, 원형/변형 매핑, AI 분석, 유효하지 않은 단어 차단을 담당한다. +- 캐시와 AI 호출, 응답 검증이 한 흐름에 들어가 있어 비용과 안정성 측면에서 중요하다. + +### `content/book` + +- 책 조회, 챕터/청크, 진행도, 이미지 처리, 가져오기(import)까지 맡는다. +- 조회 성능과 진행도 계산, 다른 도메인과의 연결 지점이 함께 모여 있다. + +## 공통 인프라 + +### MongoDB + +- 주요 도메인 엔티티와 로그, 추천 데이터를 저장한다. +- 도메인 서비스는 Mongo 문서 구조를 직접 전제로 동작하는 경우가 많다. + +### Redis + +- `streak` 읽기 세션과 `common/ratelimit` 같은 짧은 상태 관리에 사용된다. + +### S3 / R2 + +- 책 이미지와 AI 생성 결과 파일 처리를 담당한다. +- `content/book`는 import 이후 이미지 이동과 썸네일 생성까지 이어진다. + +### AI / FCM / External Sites + +- AI는 `word` 분석의 핵심 의존성이다. +- FCM은 `streak`, `notification` 쪽에서 사용된다. +- 외부 사이트는 `crawling`, `feed` 영역의 가장 큰 불안정 요소다. + +## 현재 문서화 우선순위 + +- [Streak 도메인 구조](streak.md) +- [Word 도메인 구조](word.md) +- [Book 도메인 구조](content-book.md) + +## 개선 포인트 + +- `streak`는 상태 계산, 보상, 통계, 알림 관련 책임이 큰 서비스에 집중돼 있다. +- `word`는 캐시 정책과 AI 실패 처리, 응답 검증이 서비스 흐름 안에 함께 들어가 있다. +- `content/book`는 조회, import, 이미지 처리, 진행도 계산이 서로 가까이 있어 변경 영향 범위가 넓다. +- 외부 의존성이 큰 `crawling`, `feed`는 이후 안정성 문서에서 별도로 다루는 편이 맞다. diff --git a/docs/architecture/streak.md b/docs/architecture/streak.md new file mode 100644 index 0000000..5d5fce5 --- /dev/null +++ b/docs/architecture/streak.md @@ -0,0 +1,116 @@ +# Streak 도메인 구조 + +## 목적 + +이 문서는 스트릭 도메인이 어떻게 학습 완료, 보상, 프리즈, 알림을 처리하는지 설명한다. + +## 범위 + +- `StreakService` +- `ReadingSessionService` +- 스트릭 관련 스케줄러 +- `UserStudyReport`, `DailyCompletion`, `FreezeTransaction` + +## 핵심 구성 요소 + +- `StreakController` +- `StreakService` +- `ReadingSessionService` +- `StreakProtectionScheduler` +- `UserStudyReportRepository`, `DailyCompletionRepository`, `FreezeTransactionRepository` + +## 구조 요약 + +스트릭 도메인은 사용자의 학습 연속성을 계산하는 핵심 서비스다. +읽기 세션은 Redis에 짧게 저장하고, 실제 누적 리포트와 완료 기록은 MongoDB에 저장한다. +책 읽기 완료나 다른 콘텐츠 완료 흐름에서 `StreakService`를 호출해 스트릭을 갱신하고, 스케줄러는 밤 시간대에 보호 알림을 보낸다. + +## Mermaid 다이어그램 + +### 구조 관계 + +```mermaid +flowchart TD + Client[Client] + Book[Book Progress] + Controller[StreakController] + Service[StreakService] + Session[ReadingSessionService] + Scheduler[Streak Schedulers] + Mongo[(MongoDB)] + Redis[(Redis)] + FCM[FCM] + + Client --> Controller + Client --> Book + Book --> Service + Controller --> Service + Service --> Session + Session --> Redis + Service --> Mongo + Scheduler --> Mongo + Scheduler --> FCM +``` + +### 대표 흐름: 읽기 완료 후 스트릭 갱신 + +```mermaid +sequenceDiagram + participant Client + participant ProgressService + participant ReadingCompletionService + participant StreakService + participant Mongo + + Client->>ProgressService: chapter progress update + ProgressService->>ReadingCompletionService: processReadingCompletion(...) + ProgressService->>StreakService: addStudyTime(...) + ProgressService->>StreakService: updateStreak(...) + StreakService->>Mongo: update report / completion / rewards + Mongo-->>StreakService: saved + StreakService-->>ProgressService: streakUpdated + ProgressService-->>Client: progress response +``` + +### 상태 관점 + +```mermaid +stateDiagram-v2 + [*] --> Active + Active --> CompletedToday: 오늘 학습 완료 + Active --> AtRisk: 학습 없이 하루 종료 + AtRisk --> Protected: freeze로 스트릭 보호 + AtRisk --> Reset: 보호 수단 없음 + Protected --> Active: 다음 학습일에 연속 유지 + CompletedToday --> Active: 다음 날짜로 이동 + Reset --> Active: 새 스트릭 시작 +``` + +## 주요 흐름 설명 + +1. 사용자가 학습을 시작하면 `ReadingSessionService`가 Redis에 읽기 세션을 저장한다. +2. 읽기 완료 시 `ProgressService` 같은 상위 도메인이 `StreakService`를 호출해 학습 시간, 스트릭, 완료 콘텐츠를 갱신한다. +3. `StreakService`는 오늘/어제 상태, 누락 일수, 프리즈 사용 여부를 계산하고 보상 지급 여부도 함께 판단한다. +4. `StreakProtectionScheduler`는 밤 9시에 오늘 미완료 사용자를 찾아 FCM 보호 알림을 보낸다. + +## 핵심 데이터 + +- `UserStudyReport` + - 현재 스트릭, 최장 스트릭, 사용 가능 프리즈, 총 학습 시간 등 누적 상태 +- `DailyCompletion` + - 일자별 완료 상태 +- `FreezeTransaction` + - 프리즈 지급/사용 내역 + +## 개선 포인트 + +- `StreakService`에 상태 계산, 보상 지급, 통계 응답 조립이 많이 모여 있어 분리 여지가 크다. +- Redis 세션 검증, 읽기 시간 계산, 콘텐츠 완료 처리 경계가 다른 도메인과 섞여 있다. +- 스케줄러 알림 정책과 도메인 규칙이 점점 가까워지면 테스트 경계가 흐려질 수 있다. + +## 참고 코드 + +- `src/main/java/com/linglevel/api/streak/service/StreakService.java` +- `src/main/java/com/linglevel/api/streak/service/ReadingSessionService.java` +- `src/main/java/com/linglevel/api/streak/scheduler/StreakProtectionScheduler.java` +- `src/main/java/com/linglevel/api/content/book/service/ProgressService.java` diff --git a/docs/architecture/word.md b/docs/architecture/word.md new file mode 100644 index 0000000..132f364 --- /dev/null +++ b/docs/architecture/word.md @@ -0,0 +1,117 @@ +# Word 도메인 구조 + +## 목적 + +이 문서는 단어 조회와 AI 분석 흐름이 어떻게 결합돼 있는지 설명한다. + +## 범위 + +- `WordService` +- `WordAiService` +- `WordVariant`, `InvalidWord`, `Word` +- 단어 조회 및 생성 흐름 + +## 핵심 구성 요소 + +- `WordsController` +- `WordService` +- `WordAiService` +- `WordVariantRepository`, `WordRepository`, `InvalidWordRepository` + +## 구조 요약 + +Word 도메인은 사용자가 입력한 단어를 바로 조회하지 않고, 먼저 원형/변형 관계를 확인한 뒤 필요한 경우 AI 분석으로 보완한다. +MongoDB에는 단어 본문, 변형 형태, 실패 캐시를 따로 저장하고, AI 결과는 검증과 필터링을 거친 뒤 저장한다. +즉 이 도메인은 조회 API처럼 보이지만 실제로는 캐시, 분석, 검증, 저장이 한 흐름에 묶인 구조다. + +## Mermaid 다이어그램 + +### 구조 관계 + +```mermaid +flowchart TD + Client[Client] + Controller[WordsController] + Service[WordService] + AI[WordAiService] + VariantRepo[WordVariantRepository] + WordRepo[WordRepository] + InvalidRepo[InvalidWordRepository] + Mongo[(MongoDB)] + Model[AI Model] + + Client --> Controller + Controller --> Service + Service --> VariantRepo + Service --> WordRepo + Service --> InvalidRepo + Service --> AI + AI --> Model + VariantRepo --> Mongo + WordRepo --> Mongo + InvalidRepo --> Mongo +``` + +### 대표 흐름: 단어 조회 및 생성 + +```mermaid +sequenceDiagram + participant Client + participant WordService + participant VariantRepo + participant InvalidRepo + participant WordAiService + participant AI + participant Mongo + + Client->>WordService: getOrCreateWords(word) + WordService->>VariantRepo: findAllByWord(word) + alt variant exists + VariantRepo-->>WordService: variants + WordService->>Mongo: load original words + else variant missing + WordService->>InvalidRepo: findByWord(word) + WordService->>WordAiService: analyzeWord(word, language) + WordAiService->>AI: prompt + schema + AI-->>WordAiService: analysis result + WordAiService-->>WordService: validated results + WordService->>Mongo: save word + variants + end + WordService-->>Client: WordSearchResponse +``` + +## 주요 흐름 설명 + +1. 먼저 `WordVariantRepository`에서 입력 단어가 이미 다른 원형에 연결된 변형인지 확인한다. +2. 데이터가 없으면 `InvalidWordRepository`를 확인해 반복 실패 단어를 빠르게 차단한다. +3. AI 호출이 필요하면 `WordAiService`가 강한 프롬프트, Bean schema, validation, enum 필터링, homograph 병합을 적용한다. +4. 성공 결과는 `Word`와 `WordVariant`로 나눠 저장하고, 이후 요청에서는 캐시처럼 재사용한다. + +## 핵심 데이터 + +- `Word` + - 원형 단어, 번역, 의미, 활용형 정보 +- `WordVariant` + - 입력 단어와 원형 단어 연결 +- `InvalidWord` + - 반복 실패한 단어에 대한 차단 캐시 + +## 이 도메인의 특징 + +- AI 응답을 그대로 신뢰하지 않고 validation과 enum 정리를 한 번 더 거친다. +- 같은 원형으로 합쳐야 하는 homograph/variant 처리를 서비스 쪽에서 보정한다. +- 실패한 단어를 `InvalidWord`로 캐시해 불필요한 재호출을 줄인다. + +## 개선 포인트 + +- `WordService`가 캐시 판단, 예외 전략, 저장 규칙까지 많이 알고 있어 책임이 크다. +- `WordAiService`는 프롬프트, 비용 로깅, 응답 검증을 함께 갖고 있어 분리 후보가 될 수 있다. +- AI 실패 정책과 사용자 응답 정책을 더 명확히 나누면 테스트가 쉬워질 수 있다. + +## 참고 코드 + +- `src/main/java/com/linglevel/api/word/service/WordService.java` +- `src/main/java/com/linglevel/api/word/service/WordAiService.java` +- `src/main/java/com/linglevel/api/word/entity/Word.java` +- `src/main/java/com/linglevel/api/word/entity/WordVariant.java` +- `src/main/java/com/linglevel/api/word/entity/InvalidWord.java` diff --git a/docs/decisions/001-chapter-query-aggregation.md b/docs/decisions/001-chapter-query-aggregation.md new file mode 100644 index 0000000..0e32d12 --- /dev/null +++ b/docs/decisions/001-chapter-query-aggregation.md @@ -0,0 +1,31 @@ +# Chapter 조회 N+1 문제를 Aggregation으로 해소 + +관련 PR: [#197](https://github.com/SWM16-ASAP/back-server/pull/197) + +## 문제 + +`GET /api/books/{bookId}/chapters` 조회 시 챕터 목록을 가져온 뒤 각 챕터별 청크 개수를 다시 조회하는 구조였다. +이 방식은 챕터 수만큼 추가 조회가 발생해 N+1 문제가 생기고, 책 규모가 커질수록 응답 시간이 불필요하게 늘어날 수 있었다. + +## 선택 + +`ChunkRepository`에서 MongoDB Aggregation Pipeline으로 여러 챕터의 난이도별 청크 개수를 한 번에 조회하도록 바꿨다. +`ChapterService`는 개별 `count` 호출 대신 집계 결과를 맵으로 받아 응답 조합에 사용하도록 정리했다. + +## 이유 + +목록 조회에서 가장 비싼 부분은 반복적인 청크 개수 조회였다. +이 문제는 캐시보다 먼저 조회 패턴을 바로잡는 편이 단순하고, 데이터 정합성도 유지하기 쉽다. +Aggregation은 현재 MongoDB 구조를 크게 바꾸지 않으면서도 쿼리 수를 줄일 수 있는 가장 직접적인 선택이었다. + +## 검증 + +- `ChapterServiceTest`에서 집계 결과를 기반으로 응답이 조합되는지 확인한다. +- 목록 조회 시 챕터 수에 비례해 청크 개수 조회가 반복되지 않는지 로그와 쿼리 패턴으로 확인한다. +- 동일한 요청에서 기존 응답 필드와 진행률 계산이 유지되는지 회귀 테스트로 확인한다. + +## 결과와 남은 이슈 + +- `content/book` 영역 전체에서 비슷한 반복 조회가 더 있는지 추가 점검이 필요하다. +- 실제 트래픽 기준 성능 개선 폭은 쿼리 로그나 부하 테스트로 별도 측정해두는 편이 좋다. +- 집계 결과가 더 복잡해지면 응답 조합 로직을 별도 조회 모델로 분리할지 다시 검토할 수 있다. diff --git a/docs/decisions/002-rate-limiting-with-bucket4j.md b/docs/decisions/002-rate-limiting-with-bucket4j.md new file mode 100644 index 0000000..3527266 --- /dev/null +++ b/docs/decisions/002-rate-limiting-with-bucket4j.md @@ -0,0 +1,31 @@ +# Bucket4j와 Redis 기반 Rate Limiting 도입 + +관련 PR: [#191](https://github.com/SWM16-ASAP/back-server/pull/191) + +## 문제 + +짧은 시간 안에 특정 사용자나 IP가 많은 요청을 보내면 서버 부하가 커지고, 악의적 요청에도 취약해질 수 있었다. +기존 구조에는 전역적으로 요청량을 제어하는 장치가 없었고, 민감한 API마다 제한 정책을 다르게 주기도 어려웠다. + +## 선택 + +Redis를 저장소로 사용하는 Bucket4j 기반 rate limiting을 도입했다. +Security filter chain 안에 `RateLimitFilter`를 배치하고, 전역 제한 설정과 `@RateLimit` 어노테이션 기반 개별 오버라이드를 함께 지원하도록 구성했다. + +## 이유 + +요청 제한은 애플리케이션 인스턴스별 메모리가 아니라 공유 저장소 기준으로 동작해야 서버가 늘어나도 정책이 유지된다. +Redis는 이미 운영 중인 인프라였고, Bucket4j는 토큰 버킷 알고리즘과 분산 저장소 연동이 검증되어 있어 도입 비용이 낮았다. +또한 `USER`, `IP`, `AUTO` 키 전략을 분리하면 로그인 사용자와 비로그인 사용자를 같은 방식으로 다루지 않아도 된다. + +## 검증 + +- `RateLimitFilterTest`로 사용자 기준, IP 기준, 어노테이션 오버라이드, 한도 초과 응답을 검증한다. +- 실제 컨트롤러에 `@RateLimit`을 적용해 전역 설정보다 더 엄격한 정책이 반영되는지 확인한다. +- Redis를 공유하는 환경에서 여러 요청이 들어와도 동일 키 기준으로 제한이 일관되게 동작하는지 확인한다. + +## 결과와 남은 이슈 + +- 현재 429 응답은 단순 문자열 JSON이라 공통 예외 응답 형식과 통일할 여지가 있다. +- 경로별 정책, 관리자 API 예외, burst 허용량 같은 세부 전략은 운영 데이터 기준으로 조정이 필요하다. +- rate limit hit 수를 메트릭으로 노출하면 운영성과 튜닝 근거가 더 좋아진다. diff --git a/docs/decisions/003-migrate-dev-infrastructure-to-on-premise.md b/docs/decisions/003-migrate-dev-infrastructure-to-on-premise.md new file mode 100644 index 0000000..9379bbc --- /dev/null +++ b/docs/decisions/003-migrate-dev-infrastructure-to-on-premise.md @@ -0,0 +1,31 @@ +# 개발 배포 환경을 온프레미스 기반으로 전환 + +관련 PR: [#312](https://github.com/SWM16-ASAP/back-server/pull/312) + +## 문제 + +기존 개발 배포 환경은 비용과 운영 방식 측면에서 현재 사용 패턴에 비해 무겁고 비효율적이었다. +배포 파이프라인, 이미지 빌드 대상 플랫폼, 정적 파일 저장소, 실행 환경 설정이 새 인프라 구조에 맞게 다시 정리될 필요가 있었다. + +## 선택 + +개발 배포 기준을 온프레미스 서버 중심으로 바꾸고, GitHub Actions의 배포 워크플로도 그 구조에 맞게 수정했다. +동시에 정적 파일 저장은 Cloudflare R2로, AI 관련 버킷은 AWS S3로 분리해 스토리지 역할도 나눴다. + +## 이유 + +개발 환경은 운영 환경과 완전히 같을 필요보다, 비용 대비 빠르게 운영 가능한 구조가 더 중요할 때가 있다. +온프레미스 전환은 서버 비용을 줄이면서도 현재 프로젝트 규모에서 충분한 제어권을 확보할 수 있는 선택이었다. +또한 정적 파일과 AI 처리 파일의 특성이 달라, 저장소를 분리하는 편이 비용과 운영 측면에서 더 합리적이었다. + +## 검증 + +- `dev-deploy.yml` 기준으로 이미지 빌드와 원격 배포가 새 서버에서 정상 동작하는지 확인한다. +- 애플리케이션이 새 환경 변수 집합으로 기동되고, 정적 파일과 AI 파일이 각 저장소로 올바르게 접근되는지 확인한다. +- `local`, `dev`, `prod` 프로필별 설정 차이가 새 구조와 충돌하지 않는지 점검한다. + +## 결과와 남은 이슈 + +- 개발 배포와 운영 배포의 경계가 더 명확해야 하므로 브랜치 트리거와 환경 문서를 계속 정리할 필요가 있다. +- 온프레미스 서버의 백업, 재시작 정책, 모니터링 기준은 runbook 형태로 추가 정리하는 편이 좋다. +- R2 호환성 이슈처럼 저장소별 세부 설정 차이는 별도 기록으로 분리해 관리하는 것이 낫다. diff --git a/docs/decisions/004-disable-r2-chunked-encoding.md b/docs/decisions/004-disable-r2-chunked-encoding.md new file mode 100644 index 0000000..e3aeeab --- /dev/null +++ b/docs/decisions/004-disable-r2-chunked-encoding.md @@ -0,0 +1,31 @@ +# R2 서명 불일치를 피하기 위해 Chunked Encoding 비활성화 + +관련 PR: [#314](https://github.com/SWM16-ASAP/back-server/pull/314) + +## 문제 + +Cloudflare R2를 정적 파일 저장소로 사용하면서 S3 호환 API 요청의 서명이 맞지 않는 문제가 발생했다. +특히 Java SDK 기본 설정에 가까운 요청 방식이 R2와 완전히 호환되지 않아 업로드 시점 실패로 이어졌다. + +## 선택 + +R2용 `S3Client`에 별도 `S3Configuration`을 주고, `pathStyleAccessEnabled(true)`와 `chunkedEncodingEnabled(false)`를 명시했다. +즉 AWS S3와 동일한 기본값을 그대로 쓰지 않고, R2 호환 설정을 클라이언트 레벨에서 분리했다. + +## 이유 + +이 문제는 애플리케이션 로직보다 스토리지 호환성 문제라서, 서비스 레이어가 아니라 클라이언트 설정에서 해결하는 편이 맞다. +R2는 S3 호환이지만 완전 동일 구현이 아니므로, 서명 방식에 영향을 주는 옵션은 명시적으로 고정하는 것이 안전하다. +이렇게 해야 업로드 호출부에 예외 처리 분기를 늘리지 않고도 문제를 국소적으로 막을 수 있다. + +## 검증 + +- R2 대상 업로드가 동일 입력에서 더 이상 서명 불일치로 실패하지 않는지 확인한다. +- AWS S3용 AI 버킷 클라이언트에는 같은 설정이 불필요하게 섞이지 않았는지 확인한다. +- 정적 파일 업로드 경로와 URL 생성 흐름이 기존과 동일하게 유지되는지 확인한다. + +## 결과와 남은 이슈 + +- R2 전용 설정 항목이 늘어나면 `S3Config`를 저장소별 설정 클래스로 더 분리할 필요가 있다. +- 멀티파트 업로드나 큰 파일 업로드에서 추가 호환성 이슈가 없는지 별도 확인이 필요하다. +- 외부 스토리지 장애나 응답 지연에 대한 fallback 전략은 아직 별도 정리되지 않았다. diff --git a/docs/decisions/005-ai-word-analysis-cost-and-reliability.md b/docs/decisions/005-ai-word-analysis-cost-and-reliability.md new file mode 100644 index 0000000..c79677e --- /dev/null +++ b/docs/decisions/005-ai-word-analysis-cost-and-reliability.md @@ -0,0 +1,33 @@ +# AI 단어 분석 파이프라인의 비용과 안정성 개선 + +관련 PR: [#185](https://github.com/SWM16-ASAP/back-server/pull/185), [#209](https://github.com/SWM16-ASAP/back-server/pull/209), [#211](https://github.com/SWM16-ASAP/back-server/pull/211) + +## 문제 + +영어 학습 서비스의 핵심 기능인 단어 검색과 단어장 생성은 AI 응답 품질에 직접 의존했다. +초기 구조에서는 프롬프트가 흔들리면 품사나 변형 정보가 잘못 들어오고, 실패한 단어를 반복 호출하면서 비용도 계속 누적될 수 있었다. + +## 선택 + +Spring AI와 Bedrock 기반 단어 분석 파이프라인을 도입하고, `homograph` 같은 까다로운 케이스를 기준으로 프롬프트를 강화했다. +동시에 `InvalidWord` 컬렉션으로 실패 단어를 기록하고, 3회 재시도 후 차단하는 방식으로 불필요한 재호출을 막았다. + +## 이유 + +단어 검색은 단순 번역보다 원형, 변형, 품사, 예문이 함께 맞아야 실제 학습 기능으로 쓸 수 있다. +그래서 프롬프트를 튜닝하는 것만으로 끝내지 않고, 잘못된 enum 후처리와 실패 단어 캐시까지 묶어서 파이프라인 전체를 안정화하는 편이 맞았다. +비용 측면에서도 실패 단어를 계속 AI로 보내는 구조는 손해가 크기 때문에, 실패를 기록하고 차단하는 정책이 필요했다. + +## 검증 + +- [WordAiService.java](../../src/main/java/com/linglevel/api/word/service/WordAiService.java) 에서 homograph, variant, 품사 예외 케이스를 포함한 구조화 프롬프트와 토큰 비용 로깅을 확인한다. +- [WordService.java](../../src/main/java/com/linglevel/api/word/service/WordService.java) 에서 `InvalidWord` 기반 3회 재시도 정책과 성공 시 캐시 제거 흐름을 확인한다. +- [WordServiceTest.java](../../src/test/java/com/linglevel/api/word/service/WordServiceTest.java) 로 원형/변형 저장과 AI 호출 경로를 검증한다. +- 비용 비교는 테스트 코드를 통해 10개 단어의 평균 input/output 토큰 소비량을 측정한 뒤, 모델별 단가로 환산하는 방식으로 잡았다. +- 그 기준에서 단어 1개 생성 비용은 약 10원에서 0.5원 수준으로 줄었고, 실패 단어 재호출까지 차단해 비용 누수를 줄였다. + +## 결과와 남은 이슈 + +- 모델 선택과 비용 비교 결과는 코드가 아니라 실험 기록 성격이 강하므로 별도 측정 자료와 함께 관리하는 편이 좋다. +- 번역 fallback 같은 사용자 경험 보완 로직은 현재 저장소 문서와 별도로 다시 정리할 필요가 있다. +- 단어 분석 실패 유형을 더 세분화하면 프롬프트 수정과 운영 대응이 더 빨라질 수 있다. diff --git a/docs/decisions/006-personalized-push-notification-and-tracking.md b/docs/decisions/006-personalized-push-notification-and-tracking.md new file mode 100644 index 0000000..dbc0109 --- /dev/null +++ b/docs/decisions/006-personalized-push-notification-and-tracking.md @@ -0,0 +1,33 @@ +# 개인화된 추천 PUSH와 캠페인 추적 체계 도입 + +관련 근거: [#222](https://github.com/SWM16-ASAP/back-server/pull/222), [#253](https://github.com/SWM16-ASAP/back-server/pull/253), [#288](https://github.com/SWM16-ASAP/back-server/pull/288) + +## 문제 + +기존 PUSH 알림은 일괄 리마인드 성격이 강해서 사용자별 선호나 학습 패턴을 충분히 반영하지 못했다. +또한 캠페인 단위로 송신 성공률과 오픈율을 추적하기 어려워, 어떤 알림이 실제 리텐션에 기여하는지 판단할 근거도 약했다. + +## 선택 + +사용자의 콘텐츠 접근 로그를 모아 카테고리 선호도를 계산하고, 그 결과를 알림 대상 선정과 메시지 구성에 활용하는 구조를 도입했다. +동시에 FCM 전송 경로에 `campaignId`, 송신 로그, 오픈 리포트, 통계 API를 넣어 캠페인 단위 성과를 추적하도록 정리했다. + +## 이유 + +리텐션을 높이려면 푸시를 많이 보내는 것보다, 어떤 사용자가 무엇에 반응하는지 추적 가능한 구조가 먼저 필요하다. +선호도 집계와 캠페인 추적이 같이 있어야 추천 알림의 효과를 비교할 수 있고, 운영 중 정책을 바꿔도 근거 있는 조정이 가능하다. +또한 단건 위주 전송이 아니라 배치 전송과 로그 저장을 같이 최적화해야 실제 대량 발송에서도 병목이 덜 생긴다. + +## 검증 + +- [UserPreferenceAggregationScheduler.java](../../src/main/java/com/linglevel/api/content/recommendation/scheduler/UserPreferenceAggregationScheduler.java) 에서 최근 90일 로그 기반 선호도 집계와 시간/읽기시간 가중치를 확인한다. +- [NotificationService.java](../../src/main/java/com/linglevel/api/admin/service/NotificationService.java) 에서 선호 카테고리 기반 알림 발송과 국가별 메시지 분기를 확인한다. +- [FcmMessagingService.java](../../src/main/java/com/linglevel/api/fcm/service/FcmMessagingService.java), [PushLogService.java](../../src/main/java/com/linglevel/api/fcm/service/PushLogService.java), [PushCampaignService.java](../../src/main/java/com/linglevel/api/fcm/service/PushCampaignService.java) 에서 `campaignId`, 배치 전송, 송신/오픈 로깅, 통계 집계를 확인한다. +- 리텐션 수치는 AppsFlyer 기준으로 확인했고, 2025년 10월 27일부터 11월 2일까지의 전체 유저 주간 리텐션을 비교 지표로 사용했다. +- 그 기준에서 주간 리텐션은 16.67%에서 32.24%까지 개선되었고, FCM 배치 전송 속도와 캠페인 단위 분석 가능성도 함께 좋아졌다. + +## 결과와 남은 이슈 + +- 리텐션 상승은 제품 전체 변화의 영향도 섞일 수 있으므로, 캠페인별 실험 기준을 더 분리할 필요가 있다. +- 추천 점수와 메시지 전략을 실험할 A/B 구조는 아직 별도 체계가 없다. +- 사용자 피로도와 알림 빈도 제한 정책은 추후 rate limit 성격으로 다시 정리할 수 있다. diff --git a/docs/decisions/007-choose-mongodb-for-early-flexibility.md b/docs/decisions/007-choose-mongodb-for-early-flexibility.md new file mode 100644 index 0000000..9fe7953 --- /dev/null +++ b/docs/decisions/007-choose-mongodb-for-early-flexibility.md @@ -0,0 +1,30 @@ +# 서비스 초기 데이터 저장소를 MongoDB 중심으로 고정 + +## 문제 + +서비스 초기에는 기능 요구사항과 응답 형태가 빠르게 바뀌는 반면, 운영 비용은 제한적이었다. +이 시점에 관계형 DB와 문서형 DB를 함께 운영하면 관리 비용이 커지고, 빠른 스키마 변경에도 부담이 생길 수 있었다. + +## 선택 + +초기 주 데이터 저장소를 MongoDB로 고정하고, 콘텐츠·단어·로그·추천 데이터까지 문서형 모델 중심으로 설계했다. +즉 익숙한 MySQL을 병행하기보다, 초기 단계에서는 MongoDB 단일 축으로 빠르게 전개하는 방향을 택했다. + +## 이유 + +초기 제품에서는 정규화보다 요구사항 변화에 빠르게 적응하는 편이 더 중요할 때가 많다. +이 프로젝트는 콘텐츠 구조, AI 결과 구조, 로그 구조가 자주 바뀌는 편이라 문서형 저장소가 자연스럽게 맞았다. +또한 초기에 저장소를 하나로 단순화하면 인프라 복잡도와 운영 비용을 동시에 낮출 수 있다. + +## 검증 + +- [application.properties](../../src/main/resources/application.properties) 와 각 프로필 설정에서 MongoDB가 주 저장소로 사용되는 구성을 확인한다. +- [AbstractDatabaseTest.java](../../src/test/java/com/linglevel/api/common/AbstractDatabaseTest.java) 기준으로 MongoDB 로컬/테스트 환경이 먼저 정착된 흐름을 확인한다. +- 콘텐츠, 단어, 추천, 로그 관련 엔티티와 리포지토리들이 MongoDB 문서 모델을 중심으로 구성된 현재 구조를 통해 초기 선택의 방향성을 확인할 수 있다. +- 실제 운영 관점에서는 스키마 변화와 신규 기능 추가 시 테이블 재설계 부담 없이 빠르게 적응할 수 있었다. + +## 결과와 남은 이슈 + +- 조회가 복잡해질수록 Aggregation, 인덱스, 문서 구조 튜닝이 더 중요해지므로 MongoDB의 비용이 뒤늦게 커질 수 있다. +- 관계형 쿼리에 더 잘 맞는 영역이 생기면 저장소를 분리할지 다시 판단해야 한다. +- TTL, 벡터 저장, 로그 저장 등 MongoDB 활용 범위는 실제 운영 패턴에 맞춰 더 명확히 구분할 필요가 있다. diff --git a/docs/decisions/008-image-delivery-optimization.md b/docs/decisions/008-image-delivery-optimization.md new file mode 100644 index 0000000..3bd7ecf --- /dev/null +++ b/docs/decisions/008-image-delivery-optimization.md @@ -0,0 +1,32 @@ +# 글로벌 이미지 전달 성능 최적화 + +관련 PR: [#160](https://github.com/SWM16-ASAP/back-server/pull/160) + +## 문제 + +정적 이미지가 미국 리전에 가까운 저장소 기준으로 제공되면서 아시아 사용자 입장에서는 로딩 지연이 컸다. +원본 이미지를 그대로 내려주면 파일 크기도 커서, 커버 이미지와 섬네일이 많은 화면에서 체감 지연이 더 커질 수 있었다. + +## 선택 + +CDN과 Lambda@Edge 기반 WebP 변환 전략을 도입하고, 자주 쓰는 썸네일 크기는 서버에서 미리 256x256 WebP로 전처리해 저장하도록 구성했다. +즉 요청 시점의 동적 처리와, 자주 쓰는 규격의 사전 생성 전략을 함께 가져가는 혼합형 구조를 택했다. + +## 이유 + +글로벌 사용자에게는 저장소 위치보다 최종 전달 경로 최적화가 더 중요하다. +동적 리사이징만 쓰면 유연하지만 cold start와 초기 변환 비용이 생기고, 전처리만 쓰면 다양한 크기 요구를 다 감당하기 어렵다. +그래서 일반 이미지는 CDN 경로 최적화를 쓰고, 반복적으로 많이 쓰는 썸네일은 별도 생성하는 편이 균형이 좋았다. + +## 검증 + +- [#160](https://github.com/SWM16-ASAP/back-server/pull/160) 에서 CDN + Lambda@Edge + WebP 전환 방향과 `ImageResizeService` 도입 의도를 확인한다. +- [ImageResizeService.java](../../src/main/java/com/linglevel/api/s3/service/ImageResizeService.java) 와 [ImageResizeServiceTest.java](../../src/test/java/com/linglevel/api/s3/service/ImageResizeServiceTest.java) 에서 256x256 WebP 썸네일 생성과 업로드 경로를 확인한다. +- 성능 비교는 로컬 환경에서 k6로 동일 시나리오를 반복 실행하고, Grafana로 응답시간과 처리량을 모니터링하는 방식으로 측정했다. +- 그 기준에서 파일 크기는 1,473KB에서 13KB 수준까지 줄었고, 평균 응답시간은 883ms에서 27ms, 처리량은 23.7 RPS에서 734.3 RPS 수준까지 개선됐다. + +## 결과와 남은 이슈 + +- CDN, Lambda@Edge, 저장소 구성은 코드 저장소 밖 인프라 설정도 포함하므로 별도 운영 문서와 함께 관리하는 편이 좋다. +- 이미지 종류별로 전처리 규격을 더 세분화할지, 혹은 WebP 외 포맷 대응을 늘릴지는 다시 검토할 수 있다. +- 현재 R2 호환성 같은 저장소 세부 이슈는 별도 기록으로 분리되어 있으므로, 상위 전략과 하위 호환성 기록을 함께 유지해야 한다. diff --git a/docs/decisions/009-dsl-driven-crawling.md b/docs/decisions/009-dsl-driven-crawling.md new file mode 100644 index 0000000..32dd0ca --- /dev/null +++ b/docs/decisions/009-dsl-driven-crawling.md @@ -0,0 +1,32 @@ +# DSL 기반 크롤링 규칙 관리 구조 도입 + +관련 근거: [#100](https://github.com/SWM16-ASAP/back-server/pull/100), [#120](https://github.com/SWM16-ASAP/back-server/pull/120), [#284](https://github.com/SWM16-ASAP/back-server/pull/284) + +## 문제 + +특정 사이트를 크롤링해 콘텐츠를 재가공하는 구조에서는 사이트 DOM이 바뀌면 곧바로 장애가 발생할 수 있다. +게다가 학습용 자료는 저작권과 앱 구조 제약 때문에 클라이언트가 직접 크롤링해야 하는 경우가 있어, 앱 배포 없이 규칙을 바꿀 수 있어야 했다. + +## 선택 + +HTML 추출 규칙을 코드에 하드코딩하지 않고 자체 DSL로 표현해 저장하고, 백엔드에서 도메인별 DSL을 관리하는 구조를 도입했다. +클라이언트와 서버는 같은 규칙 표현을 공유하고, 백엔드는 DSL 조회·검증·관리 API를 제공하는 방식으로 역할을 나눴다. + +## 이유 + +사이트별 셀렉터를 코드에 직접 넣으면 사이트 변경 때마다 앱이나 서버를 다시 배포해야 한다. +반면 DSL을 저장하고 조회하는 구조로 가면, 규칙 자체를 데이터처럼 바꿔서 빠르게 대응할 수 있다. +이 방식은 크롤링 로직을 범용 엔진과 도메인별 규칙으로 분리하므로, 신규 사이트 추가와 장애 대응 속도 모두에 유리하다. + +## 검증 + +- [CrawlerDsl.java](../../src/main/java/com/linglevel/api/crawling/dsl/CrawlerDsl.java) 에서 셀렉터, 속성, fallback을 포함한 DSL 인터프리터 구조를 확인한다. +- [CrawlingController.java](../../src/main/java/com/linglevel/api/crawling/controller/CrawlingController.java) 와 [AdminCrawlingController.java](../../src/main/java/com/linglevel/api/admin/crawling/AdminCrawlingController.java) 에서 조회/검증/관리 API를 확인한다. +- [CrawlerDslTest.java](../../src/test/java/com/linglevel/api/crawling/dsl/CrawlerDslTest.java) 로 규칙 해석과 fallback 추출이 동작하는지 검증한다. +- 운영 기준으로는 사이트 인터페이스가 바뀌어도 앱 강제 업데이트 없이 규칙만 바꿔 대응할 수 있는 구조를 확보했다. + +## 결과와 남은 이슈 + +- DSL 문법이 커질수록 검증기와 에러 메시지 품질도 같이 좋아져야 운영이 편해진다. +- 외부 사이트 구조 변화 탐지와 규칙 실패 알림은 아직 더 자동화할 수 있다. +- 서버 크롤링, 클라이언트 크롤링, RSS fallback이 섞이는 영역은 추후 책임 경계를 더 선명하게 나눌 수 있다. diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..361119a --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,33 @@ +# Decision Records + +이 디렉터리는 큰 기술적 선택과 개선 판단을 기록하는 문서 모음이며, 기술 블로그형 기록에 가까운 톤으로 유지한다. + +## 기록 대상 + +- 구조를 바꾸는 결정 +- 성능에 영향을 주는 결정 +- 안정성과 운영성에 영향을 주는 결정 +- 테스트 전략을 바꾸는 결정 + +## 작성 기준 + +- 자잘한 구현 선택은 기록하지 않는다. +- 문제, 선택, 이유, 검증, 결과와 남은 이슈 중심으로 짧게 정리한다. +- 하나의 문서에는 하나의 큰 결정만 기록한다. +- 구현 자체보다 왜 그런 선택을 했는지와 그 결과를 이해할 수 있게 적는다. + +## 템플릿 + +- [의사결정 기록 템플릿](../templates/decision-record-template.md) + +## 기록된 사례 + +- [001. Chapter 조회 N+1 문제를 Aggregation으로 해소](001-chapter-query-aggregation.md) +- [002. Bucket4j와 Redis 기반 Rate Limiting 도입](002-rate-limiting-with-bucket4j.md) +- [003. 개발 배포 환경을 온프레미스 기반으로 전환](003-migrate-dev-infrastructure-to-on-premise.md) +- [004. R2 서명 불일치를 피하기 위해 Chunked Encoding 비활성화](004-disable-r2-chunked-encoding.md) +- [005. AI 단어 분석 파이프라인의 비용과 안정성 개선](005-ai-word-analysis-cost-and-reliability.md) +- [006. 개인화된 추천 PUSH와 캠페인 추적 체계 도입](006-personalized-push-notification-and-tracking.md) +- [007. 서비스 초기 데이터 저장소를 MongoDB 중심으로 고정](007-choose-mongodb-for-early-flexibility.md) +- [008. 글로벌 이미지 전달 성능 최적화](008-image-delivery-optimization.md) +- [009. DSL 기반 크롤링 규칙 관리 구조 도입](009-dsl-driven-crawling.md) diff --git a/docs/templates/architecture-template.md b/docs/templates/architecture-template.md new file mode 100644 index 0000000..44dc6d1 --- /dev/null +++ b/docs/templates/architecture-template.md @@ -0,0 +1,91 @@ +# Architecture Template + +## 제목 + +짧고 명확한 문서 제목 + +## 목적 + +이 문서가 어떤 구조나 흐름을 설명하기 위한 것인지 적는다. + +## 범위 + +어떤 도메인, 기능, 요청 흐름을 다루는지 적는다. + +## 핵심 구성 요소 + +- 구성 요소 1 +- 구성 요소 2 +- 구성 요소 3 + +## 구조 요약 + +현재 구조를 짧게 설명한다. + +## Mermaid 다이어그램 + +필요한 경우 아래 예시 중 하나를 복사해서 사용한다. + +### 시스템/도메인 관계 예시 + +```mermaid +flowchart TD + Client[Client] + Api[Spring API] + Mongo[MongoDB] + Redis[Redis] + External[External Services] + + Client --> Api + Api --> Mongo + Api --> Redis + Api --> External +``` + +### 요청 흐름 예시 + +```mermaid +sequenceDiagram + participant Client + participant Controller + participant Service + participant Repository + participant Mongo + + Client->>Controller: Request + Controller->>Service: Call use case + Service->>Repository: Query or save + Repository->>Mongo: Access data + Mongo-->>Repository: Result + Repository-->>Service: Result + Service-->>Controller: Response DTO + Controller-->>Client: HTTP Response +``` + +### 상태 전이 예시 + +```mermaid +stateDiagram-v2 + [*] --> Pending + Pending --> InProgress + InProgress --> Completed + InProgress --> Failed +``` + +## 주요 흐름 설명 + +다이어그램만으로 부족한 핵심 흐름을 짧게 설명한다. + +1. 요청이 어디서 시작되는가 +2. 어떤 서비스가 핵심 규칙을 담당하는가 +3. 어떤 저장소나 외부 시스템에 의존하는가 + +## 개선 포인트 + +- 현재 구조의 문제 +- 리팩터링 후보 +- 성능 또는 안정성 리스크 + +## 참고 코드 + +- 관련 패키지 또는 파일 경로 diff --git a/docs/templates/decision-record-template.md b/docs/templates/decision-record-template.md new file mode 100644 index 0000000..a64eca1 --- /dev/null +++ b/docs/templates/decision-record-template.md @@ -0,0 +1,30 @@ +# Decision Record Template + +## 제목 + +짧고 명확한 의사결정 제목 + +## 문제 + +현재 어떤 구조적, 성능적, 안정성 문제를 해결하려는지 적는다. + +## 선택 + +무엇을 하기로 했는지 한두 문장으로 적는다. + +## 이유 + +왜 이 선택이 현재 상황에서 가장 적절한지 적는다. + +## 검증 + +무엇으로 이 선택이 유효한지 확인할지 적는다. + +- 테스트 +- 로그 +- 메트릭 +- 성능 측정 + +## 결과와 남은 이슈 + +무엇이 좋아졌는지와 아직 남아 있는 리스크, 후속 작업, 추후 다시 검토할 지점을 함께 적는다.