perf: 성능 보고서 기반 최적화 — 쪽지 이메일 비동기화 + Redis 왕복 축소 - #349
Conversation
OTel 전수 측정(109 API)에서 확인된 상위 병목 3건을 개선한다.
1. 쪽지 이메일 발송 비동기화 — 응답 3,068ms → 21ms (실측 148배)
- 요청 스레드의 동기 SMTP 발송을 LetterEmailRequestedEvent +
@async @TransactionalEventListener(AFTER_COMMIT) 리스너로 분리 (R6 패턴)
- 발송 실패는 응답에 영향 없이 서버 로그로 기록 (쪽지는 이미 커밋됨)
- LetterMailSendFailException 제거 (더 이상 요청 흐름에서 발생하지 않음)
2. 게시글 조회수 Redis 왕복 축소 — 첫 조회 5회/재조회 2회 → 항상 1회
- EXISTS·INCR·EXPIRE·SET·GET 시퀀스를 Lua 스크립트 단일 왕복으로 통합
- 델타 방식·DB 폴백 동작은 동일 (단위 테스트 갱신)
3. 최근 검색어 저장 Redis 왕복 축소 — 4회 → 1회
- LREM·LPUSH·LTRIM·EXPIRE를 Lua 스크립트 단일 왕복으로 통합
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthrough쪽지 저장 후 이메일 발송 정보를 담은 이벤트를 발행하고, 커밋 이후 비동기 리스너가 SMTP 발송을 수행하도록 변경했습니다. 생성 흐름은 메일 발송을 직접 수행하지 않습니다. 최근 검색어와 조회수 갱신은 Lua 스크립트 단일 실행으로 통합했으며, 조회수 Redis 오류 시 DB 증가 폴백과 관련 테스트를 갱신했습니다. Estimated code review effort: 3 (Moderate) | ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/tavemakers/surf/application/letter/usecase/LetterCreateService.java (1)
14-27: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win트랜잭션 경계를
LetterUsecase.createLetter로 이동해 주세요.현재 경계는
save에만 적용되어 Usecase 전체 작업을 조율하지 못합니다.createLetter에@Transactional을 선언하고 여기서는 제거해도AFTER_COMMIT이벤트는 외부 트랜잭션에 정상 등록됩니다.권장 변경
- `@Transactional` public Letter save(Letter letter) {
LetterUsecase:+ `@Transactional` public LetterResDTO createLetter(Long senderId, LetterCreateReqDTO req) {코딩 지침에 따라 “Manage
@Transactionalannotation at the Usecase layer, not at Service level” 규칙을 적용했습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/tavemakers/surf/application/letter/usecase/LetterCreateService.java` around lines 14 - 27, Move the transaction boundary from LetterCreateService.save to the use case layer: add `@Transactional` to LetterUsecase.createLetter and remove it from LetterCreateService.save. Keep save responsible for persistence and event publication so its AFTER_COMMIT event registers with the enclosing createLetter transaction.Source: Coding guidelines
🧹 Nitpick comments (2)
src/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.java (2)
43-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value🏄 코치의 제안: HTTP 액션 기반 메서드 네이밍
RESTful 컨벤션에 맞게
increaseViewCount대신patchViewCount로 이름을 바꿔보는 건 어떨까요?As per coding guidelines, Action naming in classes and DTOs must use:
Create(POST),Get(GET),Patch(PATCH),Delete(DELETE) following REST conventions.💡 코드 수정 제안
- public int increaseViewCount(Post post, Long viewerId) { + public int patchViewCount(Post post, Long viewerId) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.java` at line 43, Rename the ViewCountService method increaseViewCount to patchViewCount to follow the PATCH action naming convention, and update every caller and related reference to use the new method name.Source: Coding guidelines
28-37: 🩺 Stability & Availability | 🔵 Trivial🏄 코치의 조언: Redis Cluster 환경을 위한
CROSSSLOT대비단일 왕복으로 동시성을 해결한 Lua 스크립트 구조가 아주 훌륭합니다! 견고하게 잘 작성하셨네요. 😎
한 가지 팁을 드리자면, 현재 스크립트는
KEYS[1]과KEYS[2]라는 두 개의 키를 동시에 다루고 있습니다. Standalone 환경에서는 완벽하게 동작하지만, 향후 Redis Cluster 환경으로 확장하게 되면 두 키가 다른 해시 슬롯에 배정되어CROSSSLOT에러가 발생할 수 있습니다.
나중에 클러스터 환경을 도입하신다면 공식 문서의 Hash Tag 기능을 참고하여, 키 생성 시"post:{%d}:view:count"처럼{}로 묶어 같은 슬롯에 안전하게 배치되도록 개선해 보세요!🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.java` around lines 28 - 37, Update the key-generation logic used by ViewCountService so the two Redis keys passed to INCREASE_AND_GET_SCRIPT share the same Redis Cluster hash tag, such as the post identifier wrapped in braces. Keep the existing key suffixes and script behavior unchanged while ensuring KEYS[1] and KEYS[2] always map to the same hash slot.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java`:
- Around line 38-42: Update RecentSearchService.saveQuery around the
redis.execute call to catch Redis failures, log the exception using the existing
logging pattern from ViewCountService, and return normally so
PostSearchService.search continues returning results when recent-search
persistence is unavailable.
- Line 32: Remove the `@Transactional` annotation from RecentSearchService,
specifically the method shown in the diff. Keep transaction management at the
Usecase layer; the Redis Lua operation should remain unchanged.
In
`@src/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.java`:
- Around line 42-43: Move transaction ownership out of
ViewCountService.increaseViewCount by removing its `@Transactional` annotation,
and delete the now-unused Transactional import in ViewCountService.java at lines
42-43 and 9 respectively; retain transaction management at the Usecase layer.
In
`@src/test/java/com/tavemakers/surf/application/letter/usecase/LetterUsecaseCreateLetterTest.java`:
- Around line 72-79: 레터 이벤트 프로브 메서드인 onLetterSent와 onEmailRequested를 변경해 각
public 메서드 바로 위에 /** */ 형식의 한국어 한 줄 Javadoc을 추가하세요.
---
Outside diff comments:
In
`@src/main/java/com/tavemakers/surf/application/letter/usecase/LetterCreateService.java`:
- Around line 14-27: Move the transaction boundary from LetterCreateService.save
to the use case layer: add `@Transactional` to LetterUsecase.createLetter and
remove it from LetterCreateService.save. Keep save responsible for persistence
and event publication so its AFTER_COMMIT event registers with the enclosing
createLetter transaction.
---
Nitpick comments:
In
`@src/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.java`:
- Line 43: Rename the ViewCountService method increaseViewCount to
patchViewCount to follow the PATCH action naming convention, and update every
caller and related reference to use the new method name.
- Around line 28-37: Update the key-generation logic used by ViewCountService so
the two Redis keys passed to INCREASE_AND_GET_SCRIPT share the same Redis
Cluster hash tag, such as the post identifier wrapped in braces. Keep the
existing key suffixes and script behavior unchanged while ensuring KEYS[1] and
KEYS[2] always map to the same hash slot.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3f245b0c-d0cb-4086-a5c2-fb0dead460c0
📒 Files selected for processing (9)
src/main/java/com/tavemakers/surf/application/letter/event/LetterEmailListener.javasrc/main/java/com/tavemakers/surf/application/letter/usecase/LetterCreateService.javasrc/main/java/com/tavemakers/surf/application/letter/usecase/LetterUsecase.javasrc/main/java/com/tavemakers/surf/domain/letter/event/LetterEmailRequestedEvent.javasrc/main/java/com/tavemakers/surf/domain/letter/exception/LetterMailSendFailException.javasrc/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.javasrc/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.javasrc/test/java/com/tavemakers/surf/application/letter/usecase/LetterUsecaseCreateLetterTest.javasrc/test/java/com/tavemakers/surf/domain/post/service/support/ViewCountServiceTest.java
💤 Files with no reviewable changes (1)
- src/main/java/com/tavemakers/surf/domain/letter/exception/LetterMailSendFailException.java
- RecentSearchService·ViewCountService의 @transactional 제거 (Redis 전용 로직, Lua 스크립트가 원자성 보장 — R4 부채 청산) - ViewCountService DB 폴백의 더티체킹 전제(호출자 트랜잭션)를 Javadoc으로 명시 - saveQuery에 try-catch 추가 — Redis 장애가 검색 API 응답을 실패시키지 않도록 격리 - 테스트 프로브 public 메서드에 한 줄 Javadoc 추가
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java (1)
12-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff서비스 클래스 네이밍 컨벤션 준수 제안
Surf 프로젝트 코치입니다! 🏄♂️ Redis 예외 처리 격리와 Lua 스크립트 도입으로 성능과 안정성을 훌륭하게 챙기셨네요! 아주 멋집니다.
다만, 프로젝트 가이드라인에 따르면 Service 계층의 클래스명은
{Domain}{Action}Service형식을 따라야 합니다. 현재RecentSearchService는 Action(HTTP 동작)이 명시되어 있지 않으며, 내부적으로 조회(Get)와 생성/수정(Create/Patch) 로직이 혼재되어 있습니다.향후 유지보수 시 단일 책임 원칙(SRP)과 가이드라인을 모두 충족할 수 있도록, 이 클래스를
RecentSearchCreateService,RecentSearchGetService등으로 분리하는 리팩토링을 고려해 보세요!🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java` around lines 12 - 15, 분리 제안에 따라 RecentSearchService의 조회 로직과 생성·수정 로직을 각각 RecentSearchGetService와 RecentSearchCreateService로 나누고, 각 클래스가 단일 책임을 갖도록 관련 의존성과 호출부도 함께 갱신하세요.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@dump.rdb`:
- Line 1: Remove the tracked dump.rdb Redis database dump and add dump.rdb to
the repository root .gitignore so future local Redis dumps are excluded from
version control.
In
`@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java`:
- Line 33: Update the RecentSearchService method saveQuery to createQuery,
reflecting the create action in the method name, and update all call sites
accordingly while preserving its existing behavior.
---
Nitpick comments:
In
`@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java`:
- Around line 12-15: 분리 제안에 따라 RecentSearchService의 조회 로직과 생성·수정 로직을 각각
RecentSearchGetService와 RecentSearchCreateService로 나누고, 각 클래스가 단일 책임을 갖도록 관련
의존성과 호출부도 함께 갱신하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9c28c9e4-eb95-41c1-bb0c-a477ace05f98
📒 Files selected for processing (5)
dump.rdbsrc/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.javasrc/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.javasrc/test/java/com/tavemakers/surf/application/letter/usecase/LetterUsecaseCreateLetterTest.javasrc/test/resources/archunit-store/ee56f0a9-759f-46a0-81a5-3dbf57c83976
💤 Files with no reviewable changes (1)
- src/test/resources/archunit-store/ee56f0a9-759f-46a0-81a5-3dbf57c83976
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.java
- src/test/java/com/tavemakers/surf/application/letter/usecase/LetterUsecaseCreateLetterTest.java
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java (1)
12-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff서비스 클래스 네이밍 컨벤션 준수 제안
Surf 프로젝트 코치입니다! 🏄♂️ Redis 예외 처리 격리와 Lua 스크립트 도입으로 성능과 안정성을 훌륭하게 챙기셨네요! 아주 멋집니다.
다만, 프로젝트 가이드라인에 따르면 Service 계층의 클래스명은
{Domain}{Action}Service형식을 따라야 합니다. 현재RecentSearchService는 Action(HTTP 동작)이 명시되어 있지 않으며, 내부적으로 조회(Get)와 생성/수정(Create/Patch) 로직이 혼재되어 있습니다.향후 유지보수 시 단일 책임 원칙(SRP)과 가이드라인을 모두 충족할 수 있도록, 이 클래스를
RecentSearchCreateService,RecentSearchGetService등으로 분리하는 리팩토링을 고려해 보세요!🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java` around lines 12 - 15, 분리 제안에 따라 RecentSearchService의 조회 로직과 생성·수정 로직을 각각 RecentSearchGetService와 RecentSearchCreateService로 나누고, 각 클래스가 단일 책임을 갖도록 관련 의존성과 호출부도 함께 갱신하세요.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@dump.rdb`:
- Line 1: Remove the tracked dump.rdb Redis database dump and add dump.rdb to
the repository root .gitignore so future local Redis dumps are excluded from
version control.
In
`@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java`:
- Line 33: Update the RecentSearchService method saveQuery to createQuery,
reflecting the create action in the method name, and update all call sites
accordingly while preserving its existing behavior.
---
Nitpick comments:
In
`@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java`:
- Around line 12-15: 분리 제안에 따라 RecentSearchService의 조회 로직과 생성·수정 로직을 각각
RecentSearchGetService와 RecentSearchCreateService로 나누고, 각 클래스가 단일 책임을 갖도록 관련
의존성과 호출부도 함께 갱신하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9c28c9e4-eb95-41c1-bb0c-a477ace05f98
📒 Files selected for processing (5)
dump.rdbsrc/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.javasrc/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.javasrc/test/java/com/tavemakers/surf/application/letter/usecase/LetterUsecaseCreateLetterTest.javasrc/test/resources/archunit-store/ee56f0a9-759f-46a0-81a5-3dbf57c83976
💤 Files with no reviewable changes (1)
- src/test/resources/archunit-store/ee56f0a9-759f-46a0-81a5-3dbf57c83976
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/com/tavemakers/surf/domain/post/service/support/ViewCountService.java
- src/test/java/com/tavemakers/surf/application/letter/usecase/LetterUsecaseCreateLetterTest.java
🛑 Comments failed to post (2)
dump.rdb (1)
1-1: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
의도치 않은 데이터베이스 덤프 파일 커밋 방지
Surf 프로젝트 코치입니다! 🏄♂️ 테스트 중 로컬 환경에서 생성된 Redis 덤프 파일(
dump.rdb)이 의도치 않게 커밋된 것으로 보입니다.이러한 바이너리 덤프 파일은 Git 저장소 용량을 불필요하게 차지할 뿐만 아니라, 파일 내부에 민감한 정보(JWT 토큰 등)가 포함될 경우 심각한 보안 및 개인정보 유출 원인이 될 수 있습니다. (현재 덤프 내부에 JWT 페이로드 흔적이 관찰됩니다.) 공식 보안 모범 사례에서도 운영/테스트 DB 덤프는 버전 관리에서 반드시 제외하도록 강력하게 권고하고 있습니다.
저장소에서 해당 파일을 즉시 삭제해 주시고, 프로젝트 루트의
.gitignore파일에dump.rdb를 추가하여 향후 동일한 문제가 발생하지 않도록 든든하게 방어해 주세요! 🛡️🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dump.rdb` at line 1, Remove the tracked dump.rdb Redis database dump and add dump.rdb to the repository root .gitignore so future local Redis dumps are excluded from version control.src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java (1)
33-33: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
메서드 네이밍 컨벤션 준수 (Action 반영)
가이드라인에 따라 메서드명에는 HTTP 동작(
Create,Get,Patch,Delete)을 나타내는 키워드가 포함되어야 합니다.save대신create를 사용하면 팀 내 네이밍 일관성이 한층 더 탄탄해집니다.♻️ 메서드명 변경 제안
- public void saveQuery(Long memberId, String raw) { + public void createQuery(Long memberId, String raw) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.public void createQuery(Long memberId, String raw) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/tavemakers/surf/domain/post/service/search/RecentSearchService.java` at line 33, Update the RecentSearchService method saveQuery to createQuery, reflecting the create action in the method name, and update all call sites accordingly while preserving its existing behavior.Source: Coding guidelines
배경
OpenTelemetry Java agent로 전체 122개 API 중 109개를 실측한 성능 보고서에서 확인된 상위 병목을 개선합니다.
핵심 원칙: 로컬에서 DB·Redis 왕복은 싸지만 운영(원거리)에서는 왕복당 수십 ms — 요청당 왕복 수를 줄이는 것이 운영 지연 개선의 본질.
변경 사항
1. 🚨 쪽지 이메일 발송 비동기화 — 응답 3,068ms → 21ms (실측 148배)
측정 최대 발견:
POST /v1/user/letters가 요청 스레드에서 SMTP 발송을 동기로 수행해 응답이 3초였습니다 (DB는 3ms).LetterEmailRequestedEvent+@Async @TransactionalEventListener(AFTER_COMMIT)리스너로 분리 (기존 알림 리스너와 동일한 R6 패턴)LetterMailSendFailException제거 (요청 흐름에서 더 이상 발생하지 않음)2. 게시글 조회수 Redis 왕복 — 첫 조회 5회/재조회 2회 → 항상 1회
ViewCountService의 EXISTS→INCR→EXPIRE→SET→GET 시퀀스를 Lua 스크립트 단일 왕복으로 통합 (RTR의 CAS 스크립트와 동일 패턴). 델타 방식·Redis 장애 시 DB 폴백 동작은 그대로.3. 최근 검색어 저장 Redis 왕복 — 4회 → 1회
RecentSearchService.saveQuery의 LREM→LPUSH→LTRIM→EXPIRE를 Lua 단일 왕복으로 통합. 게시글 검색 API의 부수 왕복 -3.검증
남은 권고 (별도 작업)
member.kakao_idNOT NULL 여부 (로컬에서 퇴출 500 재현됨),board.typeenum 정의🤖 Generated with Claude Code
Summary by CodeRabbit