spring_2기[본캠프]/과제

[파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 9

minwoo95 2026. 4. 24. 19:24

https://github.com/Hot6-NovelCraft/Hot6-NovelCraft

 

GitHub - Hot6-NovelCraft/Hot6-NovelCraft

Contribute to Hot6-NovelCraft/Hot6-NovelCraft development by creating an account on GitHub.

github.com

 

 

https://github.com/MinWoo1995/Hot6-NovelCraft-local

 

GitHub - MinWoo1995/Hot6-NovelCraft-local

Contribute to MinWoo1995/Hot6-NovelCraft-local development by creating an account on GitHub.

github.com

 

 

1. 오늘 한 일
오늘은 중간 발표 준비를 진행했다. 발표 주제는 mentor/mentoring 도메인 고도화 내용으로 선정했고, 의사결정 1개와 문제해결 2개 구조로 구성했다.

발표 주제 선정 및 구성 (의사결정 1개 + 문제해결 2개)
발표 문서 작성 (배경, 요구사항, 의사결정, 장단점)
발표 대본 작성 (5분 분량)
팀 목표 재설정 논의

또한 팀 회의에서 프로젝트 방향을 재설정했다. 기존에는 도메인별 기능 구현 완성도에 집중했는데, 앞으로는 프로젝트 디테일보다 고도화와 부하테스트 쪽에 더 집중하기로 팀 목표를 수정했다. 기능이 돌아가는 것보다 왜 이렇게 설계했는가, 실제 트래픽에서 얼마나 버티는가를 증명하는 방향으로 진행할 예정이다.

 

2. 발표 내용 정리
의사결정 - 멘토 조회 API v1/v2 분리 (QueryDSL N+1 해결)
getMyMentees API가 반복문 안에서 멘티, 소설, 피드백을 각각 개별 조회하는 N+1 구조였다. 멘티가 N명이면 쿼리가 1+3N번 나가는 문제였다. 이를 해결하기 위해 기존 v1은 유지하고 QueryDSL JOIN 단일 쿼리로 구현한 v2 엔드포인트를 추가했다. 팀 컨벤션상 엔티티 연관관계를 걸지 않아 Fetch Join을 쓸 수 없었고, QueryDSL로 직접 JOIN 쿼리를 짜는 방식을 선택했다.
문제해결 1 - 스케줄러 메모리 부하
매일 자정에 멘토 등급을 자동 조정하는 배치가 승급 대상 멘토를 한 번에 전부 List로 가져오는 구조였다. 코드래빗 리뷰에서 두 가지 문제를 잡아줬다. 첫째, 멘토 수가 늘어나면 메모리가 터질 수 있다. 둘째, @Transactional 안에서 처리한 엔티티가 배치 종료까지 persistence context에 쌓인다. 청크 100명 단위 페이지네이션, ID ASC 정렬 고정, 청크마다 flush()/clear() 호출로 해결했다.
문제해결 2 - QueryDSL LEFT JOIN null 노출
v1은 반복문 안에서 orElse("알 수 없는 사용자")로 null을 방어했는데, v2로 QueryDSL 단일 쿼리로 전환하면서 이 방어 로직을 놓쳤다. 탈퇴한 유저나 삭제된 소설 조회 시 응답에 null이 그대로 들어가는 문제였다. Expressions.cases()로 쿼리 단에서 직접 fallback 처리했다.

 

3. 느낀 점
발표 준비를 하면서 코드를 짜는 것과 그걸 말로 설명하는 건 완전히 다른 작업이라는 걸 느꼈다. 코드는 짰는데 왜 그렇게 짰는지 말로 정리하려니 생각보다 어려웠다.
팀 목표를 고도화와 부하테스트 중심으로 바꾼 것도 의미있는 결정이었다. 발표 준비를 하면서 "개선 전후 수치가 있으면 훨씬 설득력 있을 텐데"라는 생각이 계속 들었다. 앞으로는 N+1 개선 전후 쿼리 수 비교, 인덱스 적용 전후 실행 계획 비교, 부하테스트 결과를 직접 측정하고 정리하는 작업을 병행할 예정이다.