Skip to content

[refactor] #62 커뮤니티 조회 API Redis 캐싱 적용 및 성능 검증#63

Merged
seokjun01 merged 14 commits intodevelopfrom
refactor/#62/캐싱-최적화
Feb 28, 2026

Hidden character warning

The head ref may contain hidden characters: "refactor/#62/\uce90\uc2f1-\ucd5c\uc801\ud654"
Merged

[refactor] #62 커뮤니티 조회 API Redis 캐싱 적용 및 성능 검증#63
seokjun01 merged 14 commits intodevelopfrom
refactor/#62/캐싱-최적화

Conversation

@seokjun01
Copy link
Copy Markdown
Collaborator

@seokjun01 seokjun01 commented Feb 26, 2026

개요

커뮤니티 목록·상세·댓글 조회 API가 매 요청마다 DB를 다중 조회하는 구조였습니다.
이미 인프라에 올라와 있는 Redis를 활용해 Spring Cache 추상화를 적용하고, 부하 테스트로 성능 개선을 수치로 검증했습니다.


구현 내용

1. CacheConfig

RedisCacheManager 빈을 등록하고 Jackson 직렬화(GenericJackson2JsonRedisSerializer)와 TTL 5분을 설정합니다.
캐시 키 전략상 userId를 포함하지 않아 사용자 수에 무관하게 캐시 엔트리가 일정하게 유지됩니다.

직렬화 이슈 해결
Java recordfinal 클래스라 DefaultTyping.NON_FINAL 설정에서 타입 정보가 직렬화되지 않아
역직렬화 시 MismatchedInputException이 발생했습니다.
CacheDto 3종에 @JsonTypeInfo(use = CLASS, include = WRAPPER_ARRAY)를 명시해 해결했습니다.

2. CommunityCacheService (별도 빈 분리)

@Cacheable / @CacheEvict를 한 곳에서 관리합니다.
같은 클래스 내부에서 호출하면 AOP 프록시가 동작하지 않는 self-invocation 문제를 피하기 위해 별도 빈으로 분리했습니다.

메서드 캐시 이름 KEY
getAllCommunitiesCache() community-list "all"
getCommunitiesByFieldCache(field) community-list-by-field field.name()
getCommunityDetailCache(communityId) community-detail communityId
getCommentsCache(communityId) comment-list communityId

3. CacheDto 3종

relativeTime("40분 전" 등)은 캐시에 저장하면 시간이 고정되므로 raw LocalDateTime으로 저장하고,
응답 변환 시점에 재계산합니다.
isLiked / isWriter는 사용자마다 다르므로 캐시에는 저장하지 않고 캐시 조회 후 별도 계산합니다.

4. 캐시 무효화 전략

트리거 무효화 대상
글 작성 community-list, community-list-by-field
글 수정 위 2개 + community-detail::{id}
글 삭제 위 3개 + comment-list::{id}
댓글 생성/수정/삭제 community-detail::{communityId}, comment-list::{communityId}
좋아요/취소 community-detail::{communityId}, 목록 캐시 전체

부하 테스트 결과

배경 — JMeter 설정 실수와 수정

첫 번째 JMeter 실행 (잘못된 설정)

Threads: 100 / Ramp-Up: 130초 / Loop Count: 10 / Duration: 120초

Ramp-Up(130초) > Duration(120초) 로 설정되어 있어, 테스트가 끝날 때까지 100개 스레드가 다 뜨지 않습니다.
루프 횟수도 10회로 제한되어 실제 동시 사용자는 약 46명 수준, 처리량은 30 req/s에 불과했습니다.
이 조건에서는 DB에 가해지는 압박이 작아 캐싱 효과가 희석돼 평균 응답시간 개선이 23% 수준에 그쳤습니다.
-캐싱 적용 전
스크린샷 2026-02-26 오후 4 46 38

-캐싱 적용 후
스크린샷 2026-02-26 오후 5 12 09

두 번째 JMeter 실행 (수정된 설정)

Threads: 100 / Ramp-Up: 30초 / Loop Count: Infinite / Duration: 120초

Ramp-Up을 Duration 이하로 줄이고 Loop를 무한으로 설정해 실제 100 VU가 120초 동안 지속적으로 부하를 줍니다.

지표 캐싱 전 캐싱 후 개선율
Average 130ms 103ms -20.8%
p90 220ms 172ms -21.8%
p95 260ms 204ms -21.5%
p99 358ms 286ms -20.1%
Max 796ms 726ms -8.8%
Throughput 668/s 839/s +25.5%
Std Dev 73.8 58.8 -20.3%

API별 평균 응답시간 (캐싱 전 → 후)

API 개선율
getAllCommunities 128ms 102ms -20.3%
getByField 128ms 101ms -21.1%
getCommunityDetail 135ms 106ms -21.5%
getComments 127ms 104ms -18.1%

-캐싱 전
스크린샷 2026-02-26 오후 6 14 37

-캐싱 후
스크린샷 2026-02-26 오후 6 18 40


k6 병행 검증

JMeter와 독립적으로 k6를 이용해 같은 시나리오를 검증했습니다.

k6 설정 설명

stages: [
  { duration: '10s', target: 50 },   // 예열: 10초에 걸쳐 VU를 0→50명으로 증가
  { duration: '30s', target: 100 },  // 본 측정: 30초에 걸쳐 100명까지 증가 후 유지
  { duration: '10s', target: 0 },    // 종료: 10초에 걸쳐 VU를 0으로 감소
]
  • VU(Virtual User): 동시에 요청을 보내는 가상 사용자 수입니다. JMeter의 Thread 수와 동일한 개념입니다.
  • Stages: 부하를 단계적으로 올려 서버가 갑작스러운 트래픽 충격 없이 스케일업 과정을 거치게 합니다. Ramp-Up을 Duration 안에 포함시키는 구조라 JMeter의 설정 실수가 발생하지 않습니다.
  • 각 VU의 동작: 전체 목록 → 필드별 목록 → 상세 조회 → 댓글 조회 순서로 4개 API를 반복하며, 요청 사이에 0.1초씩 대기합니다. 실제 사용자가 페이지를 탐색하는 패턴을 모방합니다.

k6 결과

지표 캐싱 전 (develop) 캐싱 후 개선율
Average 32.2ms 18.5ms -43%
p95 120.7ms 53.0ms -56%
Throughput 379 req/s 413 req/s +9%
에러율 0% 0% -

k6는 JMeter보다 더 높은 실효 부하(~413 req/s)를 주었기 때문에 DB 커넥션 경쟁이 심해져 캐싱 효과가 더 크게 나타났습니다.


결론 — 캐싱 적용 효과가 있는가?

있습니다.

두 도구 모두에서 일관되게 개선이 확인됐습니다.

  • 평균 응답시간: JMeter 21% / k6 43% 감소
  • p95 응답시간: JMeter 22% / k6 56% 감소 — 트래픽이 몰릴수록 꼬리 지연 개선 효과가 더 큽니다
  • 처리량: JMeter 25% / k6 9% 증가
  • 안정성(Std Dev): 20% 감소 — 응답시간 편차가 줄어 서비스 일관성이 향상됐습니다

현재 로컬 환경(DB가 같은 머신)에서 측정한 수치입니다. 실제 EC2 환경에서는 RDS 네트워크 왕복 레이턴시(~2–5ms × 쿼리 수)가 추가로 붙기 때문에 개선 폭이 더 크게 나타날 것으로 예상됩니다.

Max 스파이크 잔존: JMeter 기준 Max 796ms → 726ms로 8.8% 감소에 그쳤습니다.
서버 최초 기동 시 캐시가 비어 있어 첫 요청이 DB 조회 + Redis 직렬화 비용을 모두 부담하기 때문입니다.
운영 환경에서는 ApplicationRunner를 통한 Cache Warm-up으로 제거 가능합니다.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced caching for community data (lists, details, and comments) with Redis-backed storage and 5-minute cache expiration.
    • Added load and debug testing scripts for performance validation.
  • Chores

    • Added Spring Cache dependency to support caching abstractions.
  • Tests

    • Updated test suites to reflect cache-driven data retrieval patterns.

seokjun01 and others added 11 commits February 25, 2026 16:33
…to, CommentCacheDto (relativeTime 문제 해결, isLiked/isWriter 분리)
- CommunityUseCaseImplTest: UserRepository/CommentUsecase 목 제거, CommunityCacheService 목 추가
  - getCommunity: communityCacheService.getCommunityDetailCache() 목으로 교체
  - getAllCommunities/getCommunitiesByField: 캐시 서비스 위임 방식으로 재작성
  - 쓰기 메서드(create/update/delete): evict 호출 검증 추가
- CommentUseCaseImplTest: CommunityCacheService 목 추가
  - getComments: communityCacheService.getCommentsCache() 목으로 교체
  - buildsNestedTree: CommentCacheDto 트리 구조로 직접 구성
  - writerNotInMap 테스트 제거 (책임이 CommunityCacheService로 이전)
  - 쓰기 메서드(create/update/delete): evictCommentsAndDetail 호출 검증 추가
- LikeUseCaseImplTest: CommunityCacheService 목 추가, evictCommunityAll 호출 검증 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

The PR introduces a Redis-backed caching layer for community data using Spring Cache. A new CommunityCacheService provides cacheable methods for retrieving communities and comments with strategic eviction on mutations. Utility classes extract time formatting logic to a shared TimeUtils module. Use cases refactor to delegate data retrieval to the cache service. Load test scripts are added for performance validation.

Changes

Cohort / File(s) Summary
Cache Configuration & Setup
build.gradle, src/main/java/.../global/config/CacheConfig.java
Adds Spring Cache starter dependency and configures Redis-backed cache manager with 5-minute TTL and four cache regions (community-list, community-detail, etc.).
Cache Service & DTOs
src/main/java/.../application/community/cache/CommunityCacheService.java, src/main/java/.../application/community/dto/cache/*
Introduces CommunityCacheService with cacheable methods for communities/comments and eviction strategies; adds three cache DTO records (CommunitySummaryCacheDto, CommunityDetailCacheDto, CommentCacheDto) with factory and response conversion methods.
Use Case Refactoring
src/main/java/.../application/community/usecase/CommentUseCaseImpl.java, src/main/java/.../application/community/usecase/CommunityUseCaseImpl.java, src/main/java/.../application/community/usecase/LikeUseCaseImpl.java
Refactors use cases to delegate data retrieval to CommunityCacheService; adds cache eviction calls after mutations using TransactionUtils; removes duplicate utility methods.
Response DTO Updates
src/main/java/.../dto/response/CommunityDetailResponse.java, src/main/java/.../dto/response/CommunitySummaryResponse.java, src/main/java/.../dto/response/MyCommentResponse.java, src/main/java/.../dto/response/MyCommunityResponse.java, src/main/java/.../dto/response/MyLikedCommunityResponse.java
Migrates time/preview formatting from static imports of LikeUseCaseImpl to centralized TimeUtils utility.
Shared Utilities
src/main/java/.../global/common/TimeUtils.java, src/main/java/.../global/common/TransactionUtils.java
Adds TimeUtils for relative-time formatting and content preview; adds TransactionUtils for deferred cache eviction after transaction commit.
Test Updates
src/test/java/.../usecase/CommentUseCaseImplTest.java, src/test/java/.../usecase/CommunityUseCaseImplTest.java, src/test/java/.../usecase/LikeUseCaseImplTest.java
Updates test mocks/stubs to use CommunityCacheService instead of direct repository access; adds cache eviction verifications.
Load Testing
k6/debug_test.js, k6/load_test.js
Adds k6 load-test scripts for debugging and performance benchmarking with configurable stages, custom metrics, and simulated API workflows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • [refactor] #62 캐싱 최적화 #62: Implements the Redis-backed caching refactor for community data with CommunityCacheService, cache DTOs, and cache-driven eviction strategies as planned.

Possibly related PRs

Suggested reviewers

  • jinisim

Poem

🐰 A cache is born, with Redis so bright,
Communities served in less than a bite,
Eviction on change keeps data fresh and true,
TimeUtils hops through, making time anew!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: applying Redis caching to community query APIs and verifying performance, which aligns with the core refactoring work across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#62/캐싱-최적화

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seokjun01 seokjun01 self-assigned this Feb 26, 2026
@seokjun01 seokjun01 requested a review from jinisim February 26, 2026 09:27
@seokjun01 seokjun01 added the enhancement New feature or request label Feb 26, 2026
@seokjun01 seokjun01 changed the title refactor: 커뮤니티 조회 API Redis 캐싱 적용 및 성능 검증 [refactor] #62 커뮤니티 조회 API Redis 캐싱 적용 및 성능 검증 Feb 26, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java (1)

43-47: ⚠️ Potential issue | 🟠 Major

Cache is not evicted when creating a new like.

When a new like is created (first time a user likes a community), the method returns early at line 46 without calling evictCommunityAll. This leaves stale data in the cache with an outdated likeCount.

🐛 Proposed fix
     if (like == null) {
         likeRepository.save(Like.of(userId, communityId));
         community.increaseLikeCount();
+        communityCacheService.evictCommunityAll(communityId);
         return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java`
around lines 43 - 47, In LikeUseCaseImpl, when like == null the code saves the
new like via likeRepository.save(...) and increments
community.increaseLikeCount() but returns without clearing the cache; after
creating the Like (and after community.increaseLikeCount()) call
evictCommunityAll(communityId) to evict stale cached community data before
returning so the updated likeCount is visible; update the early-return branch to
invoke evictCommunityAll with the relevant communityId.
src/test/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImplTest.java (1)

55-72: ⚠️ Potential issue | 🔴 Critical

Add cache eviction verification for new like creation.

When a user first likes a community, evictCommunityAll is not called, causing stale cache entries. The test at lines 55-72 should verify cache invalidation occurs. Add verify(communityCacheService).evictCommunityAll(communityId); to the test, and update the implementation to call communityCacheService.evictCommunityAll(communityId); before returning in the new like path (after community.increaseLikeCount()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImplTest.java`
around lines 55 - 72, Update the test
LikeUseCaseImplTest.newLike_createsAndIncrements to verify cache eviction by
adding verify(communityCacheService).evictCommunityAll(communityId); after
asserting like save and like count, and modify the implementation of
LikeUseCaseImpl.like(userId, communityId) so that in the "new like" branch
(after creating/saving the Like and calling community.increaseLikeCount()) it
calls communityCacheService.evictCommunityAll(communityId) before returning;
reference the LikeUseCaseImpl class and evictCommunityAll on
communityCacheService to locate the change.
🧹 Nitpick comments (7)
k6/load_test.js (2)

4-4: Unused import: open is never used.

The open function from k6/experimental/fs is imported but not utilized.

Proposed fix
-import { open } from 'k6/experimental/fs';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k6/load_test.js` at line 4, Remove the unused import of open from
k6/experimental/fs: locate the import statement that reads "import { open } from
'k6/experimental/fs';" and delete it (or replace with any actually used exports
if intended), ensuring no other code references the symbol open so the module
builds without the unused import.

26-34: Consider validating TOKEN before defining headers.

If TOKEN is not provided via __ENV.TOKEN, the Authorization header will be "Bearer undefined", causing silent 401 failures across all requests. Adding a setup check or guard would improve debuggability.

Proposed approach
 const BASE_URL = 'http://localhost:8080';
 const TOKEN = __ENV.TOKEN;
+
+if (!TOKEN) {
+  console.error('TOKEN environment variable is required. Run with: k6 run -e TOKEN=<jwt> load_test.js');
+}
+
 const COMMUNITY_IDS = [1, 3, 4, 5, 6, 8, 10, 11];
 const FIELDS = ['RECOMMEND', 'TIP', 'COLLAB'];

This provides a clear error message when the test is misconfigured rather than producing confusing 401 errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k6/load_test.js` around lines 26 - 34, Validate __ENV.TOKEN before building
the headers constant: check TOKEN (the variable set from __ENV.TOKEN) at the top
of k6/load_test.js and fail early with a clear message if it's missing or empty
(e.g., throw or call fail in setup) so you don't create an Authorization header
like "Bearer undefined"; update the place where headers is defined (the headers
constant) to assume a valid TOKEN after this guard and include guidance text in
the error to indicate the env var name expected.
k6/debug_test.js (1)

2-2: Unused import: check is never used.

The check function is imported but not utilized in this script.

Proposed fix
-import { check } from 'k6';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k6/debug_test.js` at line 2, Remove the unused import `check` from the module
import list in k6/debug_test.js (the `import { check } from 'k6';` statement)
or, if the intent was to assert response values, replace its unused import with
actual calls to `check()` where appropriate; ensure no other code references
`check` before deleting to avoid breaking intended assertions.
src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java (1)

106-116: Consider extracting utility methods to a dedicated class.

preview and convertToRelativeTime are static utility methods that are now being called from DTO classes. These methods don't belong in LikeUseCaseImpl and should be moved to a utility class to avoid coupling DTOs to use case implementations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java`
around lines 106 - 116, The static utility methods preview and
convertToRelativeTime currently defined in LikeUseCaseImpl should be moved into
a dedicated utility class (e.g., CommunityUtils or TimeUtils) to decouple DTOs
from use-case implementation; create a new final utility class with public
static methods preview(...) and convertToRelativeTime(LocalDateTime) containing
the existing logic, update all callers (DTOs and any other classes) to import
and call CommunityUtils.preview(...) and
CommunityUtils.convertToRelativeTime(...), and remove the static methods from
LikeUseCaseImpl to keep that class focused on use-case logic.
src/main/java/tave/crezipsa/crezipsa/global/config/CacheConfig.java (1)

29-31: Consider security implications of LaissezFaireSubTypeValidator.

LaissezFaireSubTypeValidator accepts any class for polymorphic deserialization without validation. While Redis cache is typically internal, this could be a vector for deserialization attacks if cache data is ever compromised.

Consider using a more restrictive validator like BasicPolymorphicTypeValidator with explicit allowed packages/classes if stricter security is needed.

♻️ Proposed alternative
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;

 ObjectMapper objectMapper = new ObjectMapper()
     .registerModule(new JavaTimeModule())
     .activateDefaultTyping(
-        LaissezFaireSubTypeValidator.instance,
+        BasicPolymorphicTypeValidator.builder()
+            .allowIfBaseType("tave.crezipsa.crezipsa.")
+            .allowIfBaseType("java.util.")
+            .allowIfBaseType("java.time.")
+            .build(),
         ObjectMapper.DefaultTyping.NON_FINAL
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/tave/crezipsa/crezipsa/global/config/CacheConfig.java` around
lines 29 - 31, The current use of LaissezFaireSubTypeValidator in CacheConfig
(via ObjectMapper.activateDefaultTyping) is overly permissive; replace it with a
stricter validator by configuring a BasicPolymorphicTypeValidator that
explicitly allows only your application's package(s) or trusted classes (e.g.,
build a BasicPolymorphicTypeValidator.withBasePackage("your.base.pkg") or
allowIfSubType checks) and wire that into ObjectMapper.activateDefaultTyping
instead of LaissezFaireSubTypeValidator.instance so polymorphic deserialization
is constrained to known-safe types.
src/main/java/tave/crezipsa/crezipsa/application/community/cache/CommunityCacheService.java (1)

42-53: 요약 DTO 매핑 로직 중복은 헬퍼로 합치면 관리가 쉬워집니다.

두 메서드의 매핑/카운트 조합이 거의 동일합니다. toSummaryCacheDtos(List<Community>) 같은 내부 헬퍼로 묶으면 변경 시 드리프트를 줄일 수 있습니다.

Also applies to: 57-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/cache/CommunityCacheService.java`
around lines 42 - 53, Extract the duplicate mapping/count combination in
getAllCommunitiesCache() (and the similar block at lines 57-68) into a private
helper like toSummaryCacheDtos(List<Community>) that accepts the list of
Community, calls loadCommentCounts(communities), and returns
List<CommunitySummaryCacheDto> by mapping each Community to
CommunitySummaryCacheDto.from(c, c.getLikeCount(),
commentCounts.getOrDefault(c.getCommunityId(), 0L)); then replace the mapping
logic in getAllCommunitiesCache() and the other method to delegate to this new
helper to remove duplication and centralize DTO creation.
src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java (1)

8-8: DTO에서 UseCase 구현체 정적 메서드 의존을 분리하세요.

CommunitySummaryCacheDtoLikeUseCaseImpl에 직접 의존해서 계층 결합도가 올라갔습니다. preview/relativeTime 포맷팅은 별도 유틸/formatter로 분리하고 DTO는 결과값만 다루는 구조가 유지보수에 안전합니다.

Also applies to: 32-32, 48-48

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java`
at line 8, CommunitySummaryCacheDto currently directly references the
LikeUseCaseImpl static methods (and uses preview/relativeTime formatting),
creating an unwanted dependency; remove any direct reference to LikeUseCaseImpl
from CommunitySummaryCacheDto and move preview/relativeTime logic into a
separate formatter/utility (e.g., CommunityPreviewFormatter or DateTimeFormatter
util). Change CommunitySummaryCacheDto to accept already-formatted preview and
relativeTime (or accept a simple interface/functional formatter) via its
constructor/factory or DTO mapper so the DTO only holds values, and update
callers (where LikeUseCaseImpl was used) to call the new formatter/utility or
inject the formatter before constructing the DTO.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/dev-cicd.yml:
- Line 1: The workflow YAML has an invalid key `계획name`; rename this key to the
correct `name` in .github/workflows/dev-cicd.yml so the top-level workflow key
reads `name: CI/CD Dev` (remove the accidental Korean prefix) to restore valid
YAML parsing and allow GitHub Actions to run.

In `@k6/debug_test.js`:
- Around line 23-24: The current log calls use res.body.substring(0, 300) which
will throw if res.body is null/undefined or not a string; update the logging to
defensively check res.body (and its type) before slicing — e.g., compute a
safeBody = (typeof res.body === "string" && res.body.length) ?
res.body.substring(0, 300) : "<empty or non-string body>" and log safeBody
instead of calling substring directly on res.body; look for the console.log
lines referencing res.status and res.body.substring and replace the latter with
this guarded value.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommentCacheDto.java`:
- Line 10: CommentCacheDto currently depends on the use-case implementation
LikeUseCaseImpl for convertToRelativeTime, creating tight coupling; extract that
logic into a new utility class (e.g.,
tave.crezipsa.crezipsa.global.util.TimeUtils) with a public static method
convertToRelativeTime(LocalDateTime) implementing the relative-time calculation,
remove the import/use of LikeUseCaseImpl from CommentCacheDto and any other DTOs
(e.g., other occurrences around line 39), and update those classes to call
TimeUtils.convertToRelativeTime(createdAt) instead.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.java`:
- Line 11: CommunityDetailCacheDto currently depends on the use-case
implementation LikeUseCaseImpl for the convertToRelativeTime method (same as
CommentCacheDto), creating inappropriate coupling; extract convertToRelativeTime
into a new shared utility (e.g., TimeUtils or DateTimeFormatterUtil) as a static
method, update CommunityDetailCacheDto (and CommentCacheDto) to import and call
that utility instead of LikeUseCaseImpl, and remove the LikeUseCaseImpl import
from CommunityDetailCacheDto so DTOs no longer depend on use-case
implementations.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java`:
- Around line 93-96: The current getAllCommunities and getCommunitiesByField in
CommunityUseCaseImpl return unpaged Lists causing scalability issues; change
their signatures to accept a Pageable and return a
Page<CommunitySummaryResponse> (or a paged DTO), update calls to
communityCacheService to use paged caches (e.g.,
communityCacheService.getCommunitiesCache(page,size,field)) and include
page/size/(field) in cache keys, and adjust communityCacheService to store and
retrieve pages rather than the entire list; also update the underlying
repository calls used by these methods to use pageable queries and fetch related
entities in batches or with joins to avoid N+1 (refer to repository methods used
by CommunityUseCaseImpl and ensure they support Pageable and use fetch joins or
batch fetching).
- Around line 32-33: Reorder the import statements in CommunityUseCaseImpl so
they follow the project's import grouping: place all java.* imports (e.g.,
java.util.Collections, java.util.Map) first, then jakarta/javax, then org, then
lombok, and finally project-specific packages; adjust the import block around
the CommunityUseCaseImpl class so the java.util imports are moved above any
project imports to match the required order.
- Around line 56-58: Replace direct cache eviction calls inside transactional
methods with a transactional event that triggers eviction after commit: in
CommunityUseCaseImpl remove calls to communityCacheService.evictCommunityLists()
from createCommunity, updateCommunity, and deleteCommunity and instead publish a
lightweight event (e.g., CommunityEvictEvent) via ApplicationEventPublisher;
implement a separate listener (e.g., CommunityCacheEvictListener) with
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT) that calls
communityCacheService.evictCommunityLists(); apply the same pattern to
LikeUseCaseImpl and CommentUseCaseImpl for their evict* calls so eviction always
runs after the transaction commits.

---

Outside diff comments:
In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java`:
- Around line 43-47: In LikeUseCaseImpl, when like == null the code saves the
new like via likeRepository.save(...) and increments
community.increaseLikeCount() but returns without clearing the cache; after
creating the Like (and after community.increaseLikeCount()) call
evictCommunityAll(communityId) to evict stale cached community data before
returning so the updated likeCount is visible; update the early-return branch to
invoke evictCommunityAll with the relevant communityId.

In
`@src/test/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImplTest.java`:
- Around line 55-72: Update the test
LikeUseCaseImplTest.newLike_createsAndIncrements to verify cache eviction by
adding verify(communityCacheService).evictCommunityAll(communityId); after
asserting like save and like count, and modify the implementation of
LikeUseCaseImpl.like(userId, communityId) so that in the "new like" branch
(after creating/saving the Like and calling community.increaseLikeCount()) it
calls communityCacheService.evictCommunityAll(communityId) before returning;
reference the LikeUseCaseImpl class and evictCommunityAll on
communityCacheService to locate the change.

---

Nitpick comments:
In `@k6/debug_test.js`:
- Line 2: Remove the unused import `check` from the module import list in
k6/debug_test.js (the `import { check } from 'k6';` statement) or, if the intent
was to assert response values, replace its unused import with actual calls to
`check()` where appropriate; ensure no other code references `check` before
deleting to avoid breaking intended assertions.

In `@k6/load_test.js`:
- Line 4: Remove the unused import of open from k6/experimental/fs: locate the
import statement that reads "import { open } from 'k6/experimental/fs';" and
delete it (or replace with any actually used exports if intended), ensuring no
other code references the symbol open so the module builds without the unused
import.
- Around line 26-34: Validate __ENV.TOKEN before building the headers constant:
check TOKEN (the variable set from __ENV.TOKEN) at the top of k6/load_test.js
and fail early with a clear message if it's missing or empty (e.g., throw or
call fail in setup) so you don't create an Authorization header like "Bearer
undefined"; update the place where headers is defined (the headers constant) to
assume a valid TOKEN after this guard and include guidance text in the error to
indicate the env var name expected.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/cache/CommunityCacheService.java`:
- Around line 42-53: Extract the duplicate mapping/count combination in
getAllCommunitiesCache() (and the similar block at lines 57-68) into a private
helper like toSummaryCacheDtos(List<Community>) that accepts the list of
Community, calls loadCommentCounts(communities), and returns
List<CommunitySummaryCacheDto> by mapping each Community to
CommunitySummaryCacheDto.from(c, c.getLikeCount(),
commentCounts.getOrDefault(c.getCommunityId(), 0L)); then replace the mapping
logic in getAllCommunitiesCache() and the other method to delegate to this new
helper to remove duplication and centralize DTO creation.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java`:
- Line 8: CommunitySummaryCacheDto currently directly references the
LikeUseCaseImpl static methods (and uses preview/relativeTime formatting),
creating an unwanted dependency; remove any direct reference to LikeUseCaseImpl
from CommunitySummaryCacheDto and move preview/relativeTime logic into a
separate formatter/utility (e.g., CommunityPreviewFormatter or DateTimeFormatter
util). Change CommunitySummaryCacheDto to accept already-formatted preview and
relativeTime (or accept a simple interface/functional formatter) via its
constructor/factory or DTO mapper so the DTO only holds values, and update
callers (where LikeUseCaseImpl was used) to call the new formatter/utility or
inject the formatter before constructing the DTO.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java`:
- Around line 106-116: The static utility methods preview and
convertToRelativeTime currently defined in LikeUseCaseImpl should be moved into
a dedicated utility class (e.g., CommunityUtils or TimeUtils) to decouple DTOs
from use-case implementation; create a new final utility class with public
static methods preview(...) and convertToRelativeTime(LocalDateTime) containing
the existing logic, update all callers (DTOs and any other classes) to import
and call CommunityUtils.preview(...) and
CommunityUtils.convertToRelativeTime(...), and remove the static methods from
LikeUseCaseImpl to keep that class focused on use-case logic.

In `@src/main/java/tave/crezipsa/crezipsa/global/config/CacheConfig.java`:
- Around line 29-31: The current use of LaissezFaireSubTypeValidator in
CacheConfig (via ObjectMapper.activateDefaultTyping) is overly permissive;
replace it with a stricter validator by configuring a
BasicPolymorphicTypeValidator that explicitly allows only your application's
package(s) or trusted classes (e.g., build a
BasicPolymorphicTypeValidator.withBasePackage("your.base.pkg") or allowIfSubType
checks) and wire that into ObjectMapper.activateDefaultTyping instead of
LaissezFaireSubTypeValidator.instance so polymorphic deserialization is
constrained to known-safe types.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1ae26a and 9de56ee.

📒 Files selected for processing (15)
  • .github/workflows/dev-cicd.yml
  • build.gradle
  • k6/debug_test.js
  • k6/load_test.js
  • src/main/java/tave/crezipsa/crezipsa/application/community/cache/CommunityCacheService.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommentCacheDto.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java
  • src/main/java/tave/crezipsa/crezipsa/global/config/CacheConfig.java
  • src/test/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImplTest.java
  • src/test/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImplTest.java
  • src/test/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImplTest.java

Comment thread .github/workflows/dev-cicd.yml Outdated
Comment thread k6/debug_test.js
Comment on lines +23 to +24
console.log(`status: ${res.status}`);
console.log(`body: ${res.body.substring(0, 300)}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against null/empty response body.

If the server returns an error or an empty body, res.body.substring(0, 300) will throw. Add a defensive check.

Proposed fix
   console.log(`status: ${res.status}`);
-  console.log(`body: ${res.body.substring(0, 300)}`);
+  console.log(`body: ${res.body ? res.body.substring(0, 300) : '(empty)'}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@k6/debug_test.js` around lines 23 - 24, The current log calls use
res.body.substring(0, 300) which will throw if res.body is null/undefined or not
a string; update the logging to defensively check res.body (and its type) before
slicing — e.g., compute a safeBody = (typeof res.body === "string" &&
res.body.length) ? res.body.substring(0, 300) : "<empty or non-string body>" and
log safeBody instead of calling substring directly on res.body; look for the
console.log lines referencing res.status and res.body.substring and replace the
latter with this guarded value.

Comment on lines +32 to +33
import java.util.Collections;
import java.util.Map;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

import 그룹 순서를 가이드에 맞춰 정리해 주세요.

java.util.* import가 프로젝트 import 뒤에 위치해 있습니다. java → jakarta/javax → org → lombok → project 순서로 정렬해 주세요.

As per coding guidelines **/*.java: Import grouping follows typical order: javajakarta/javaxorglombok → project packages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java`
around lines 32 - 33, Reorder the import statements in CommunityUseCaseImpl so
they follow the project's import grouping: place all java.* imports (e.g.,
java.util.Collections, java.util.Map) first, then jakarta/javax, then org, then
lombok, and finally project-specific packages; adjust the import block around
the CommunityUseCaseImpl class so the java.util imports are moved above any
project imports to match the required order.

Comment on lines +56 to +58
CommunityResponse response = CommunityResponse.of(communityRepository.save(community));
communityCacheService.evictCommunityLists();
return response;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the file structure and read the CommunityUseCaseImpl.java file
wc -l src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java

Repository: CREJIPSA/CreJipsa-BE

Length of output: 160


🏁 Script executed:

# Read the file to see the context around the flagged lines
cat -n src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java

Repository: CREJIPSA/CreJipsa-BE

Length of output: 8170


🏁 Script executed:

# Search for `@TransactionalEventListener` usage in the codebase (pattern mentioned in suggestion)
rg -n '@TransactionalEventListener' --type=java

Repository: CREJIPSA/CreJipsa-BE

Length of output: 46


🏁 Script executed:

# Search for domain event patterns in the community usecase and related files
rg -n 'DomainEvent|@DomainEvent|TransactionalEventListener' --type=java src/main/java/tave/crezipsa/crezipsa/application/community/

Repository: CREJIPSA/CreJipsa-BE

Length of output: 46


🏁 Script executed:

# Check how communityCacheService is defined and used
rg -n 'communityCacheService|CommunityCacheService' --type=java -A 2

Repository: CREJIPSA/CreJipsa-BE

Length of output: 18459


🏁 Script executed:

# Read CommunityCacheService to understand what eviction does
cat -n src/main/java/tave/crezipsa/crezipsa/application/community/cache/CommunityCacheService.java

Repository: CREJIPSA/CreJipsa-BE

Length of output: 8148


🏁 Script executed:

# Check LikeUseCaseImpl for similar patterns
cat -n src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java | head -100

Repository: CREJIPSA/CreJipsa-BE

Length of output: 4222


🏁 Script executed:

# Check CommentUseCaseImpl for similar patterns  
cat -n src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java | head -100

Repository: CREJIPSA/CreJipsa-BE

Length of output: 4714


🏁 Script executed:

# Search for event listener or event bus patterns in application layer
rg -n 'EventListener|ApplicationEvent|publish|Event' --type=java src/main/java/tave/crezipsa/crezipsa/application/ | head -20

Repository: CREJIPSA/CreJipsa-BE

Length of output: 46


🏁 Script executed:

# Search more broadly for event infrastructure anywhere in the codebase
rg -n 'TransactionalEventListener|ApplicationEvent|DomainEvent|EventPublisher|EventListener' --type=java

Repository: CREJIPSA/CreJipsa-BE

Length of output: 46


🏁 Script executed:

# Check if there's any global event or transaction listener configuration
rg -n '@TransactionalEventListener|ApplicationEventPublisher|TransactionSynchronizationManager' --type=java

Repository: CREJIPSA/CreJipsa-BE

Length of output: 46


🏁 Script executed:

# Verify the transaction boundary for the three flagged methods
grep -n '@Transactional' src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java

Repository: CREJIPSA/CreJipsa-BE

Length of output: 264


Defer cache eviction to after-transaction commit to eliminate race condition.

Calling evict* methods inside @Transactional write operations creates a window where concurrent requests can regenerate stale cache data before the transaction commits. The sequence is:

  1. Cache evicted (now empty)
  2. Concurrent request reads empty cache, queries uncommitted DB state, and repopulates with stale data
  3. Original transaction commits

This occurs at lines 56–58 (createCommunity), 70–71 (updateCommunity), and 108–109 (deleteCommunity). Similar patterns exist in LikeUseCaseImpl and CommentUseCaseImpl.

Use @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) to defer eviction to after the transaction commits, ensuring cache consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java`
around lines 56 - 58, Replace direct cache eviction calls inside transactional
methods with a transactional event that triggers eviction after commit: in
CommunityUseCaseImpl remove calls to communityCacheService.evictCommunityLists()
from createCommunity, updateCommunity, and deleteCommunity and instead publish a
lightweight event (e.g., CommunityEvictEvent) via ApplicationEventPublisher;
implement a separate listener (e.g., CommunityCacheEvictListener) with
`@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT) that calls
communityCacheService.evictCommunityLists(); apply the same pattern to
LikeUseCaseImpl and CommentUseCaseImpl for their evict* calls so eviction always
runs after the transaction commits.

Comment on lines 93 to 96
public List<CommunitySummaryResponse> getAllCommunities() {
List<Community> communities = communityRepository.findAll();
Map<Long, Long> commentCounts = loadCommentCounts(communities);

return communities.stream()
.map(c -> CommunitySummaryResponse.of(
c,
c.getLikeCount(),
commentCounts.getOrDefault(c.getCommunityId(), 0L)
))
return communityCacheService.getAllCommunitiesCache().stream()
.map(dto -> dto.toResponse())
.toList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

목록 API가 무제한 List 반환이라 확장성 리스크가 큽니다.

getAllCommunities, getCommunitiesByField 모두 전체 목록을 반환합니다. 데이터 증가 시 캐시 미스/웜업 시점 부하가 커지므로 Pageable 기반으로 전환하고 캐시 키도 page/size/field 기준으로 분리하는 편이 안전합니다.

As per coding guidelines **/application/**/usecase/**Impl.java: Use Pageable for list endpoints and watch for N+1 query problems by using batch/join queries.

Also applies to: 133-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java`
around lines 93 - 96, The current getAllCommunities and getCommunitiesByField in
CommunityUseCaseImpl return unpaged Lists causing scalability issues; change
their signatures to accept a Pageable and return a
Page<CommunitySummaryResponse> (or a paged DTO), update calls to
communityCacheService to use paged caches (e.g.,
communityCacheService.getCommunitiesCache(page,size,field)) and include
page/size/(field) in cache keys, and adjust communityCacheService to store and
retrieve pages rather than the entire list; also update the underlying
repository calls used by these methods to use pageable queries and fetch related
entities in batches or with joins to avoid N+1 (refer to repository methods used
by CommunityUseCaseImpl and ensure they support Pageable and use fetch joins or
batch fetching).

seokjun01 and others added 3 commits February 26, 2026 18:34
convertToRelativeTime, preview 가 LikeUseCaseImpl 정적 메서드로 존재하여
DTO/CacheDto 계층이 UseCase 구현체를 직접 import하는 레이어 침범 문제 해결.
global/common/TimeUtils 로 이관하고 모든 참조(9개 파일) 업데이트.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@transactional 내부에서 evict*() 를 즉시 호출하면,
캐시가 비워진 직후 다른 요청이 아직 커밋되지 않은 스냅샷을
캐시에 채워 낡은 데이터가 남는 레이스 컨디션이 발생한다.
TransactionUtils.afterCommit()으로 evict 시점을 커밋 이후로 연기해 해결.
CommunityUseCaseImpl, LikeUseCaseImpl, CommentUseCaseImpl 3개 클래스 적용.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.java (1)

50-64: Avoid shadowing comments in toResponse(...).

Line 50 uses a parameter name identical to the record component, which is easy to misread during maintenance. A distinct name improves clarity.

Proposed patch
-	public CommunityDetailResponse toResponse(boolean isWriter, boolean isLiked, List<CommentResponse> comments) {
+	public CommunityDetailResponse toResponse(boolean isWriter, boolean isLiked, List<CommentResponse> commentResponses) {
 		return new CommunityDetailResponse(
@@
-			comments
+			commentResponses
 		);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.java`
around lines 50 - 64, The toResponse method in CommunityDetailCacheDto currently
has a parameter named comments which shadows the record component comments;
rename the parameter (e.g., commentResponses or responseComments) in the
CommunityDetailCacheDto.toResponse(...) signature and update the single usage
inside the method (the argument passed to new CommunityDetailResponse) so the
record component and parameter are distinct, ensuring callers and imports are
updated accordingly.
src/main/java/tave/crezipsa/crezipsa/global/common/TransactionUtils.java (1)

15-16: Fail fast on null callback input.

action is not validated, so a null input fails later with a less actionable NPE path. Adding a boundary null-check makes failures immediate and clearer.

Proposed patch
+import java.util.Objects;
+
 public final class TransactionUtils {
@@
 	public static void afterCommit(Runnable action) {
+		Objects.requireNonNull(action, "action must not be null");
 		if (TransactionSynchronizationManager.isActualTransactionActive()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/tave/crezipsa/crezipsa/global/common/TransactionUtils.java`
around lines 15 - 16, The afterCommit method in TransactionUtils should validate
its Runnable parameter and fail-fast on null; add a null-check at the start of
afterCommit(action) that throws a clear NullPointerException (or
IllegalArgumentException) with a descriptive message (e.g., "action must not be
null") so callers get an immediate, actionable error instead of a later NPE when
registering the callback.
src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java (1)

95-97: Consider using method reference for cleaner stream mapping.

♻️ Minor style improvement
 	public List<CommunitySummaryResponse> getAllCommunities() {
 		return communityCacheService.getAllCommunitiesCache().stream()
-			.map(dto -> dto.toResponse())
+			.map(CommunitySummaryCacheDto::toResponse)
 			.toList();
 	}

Same applies to getCommunitiesByField at line 136.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java`
around lines 95 - 97, Replace the lambda mapping in the stream with a method
reference for clarity: change the .map(dto -> dto.toResponse()) call inside the
chain returned by communityCacheService.getAllCommunitiesCache() to use the
DTO's toResponse method reference, and apply the same change in the
getCommunitiesByField flow at the corresponding .map usage; ensure the method
reference matches the DTO class that defines toResponse.
src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java (1)

105-114: Missing @Transactional(readOnly = true) on read-only method.

getMyComments only reads data but lacks the readOnly annotation that getComments has. This misses potential read-replica routing and JPA flush optimization.

♻️ Proposed fix
 	`@Override`
+	`@Transactional`(readOnly = true)
 	public List<MyCommentResponse> getMyComments(Long userId, CommunityField field, int page, int size) {

As per coding guidelines: Use @Transactional(readOnly = true) for query-only operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java`
around lines 105 - 114, Add the read-only transactional annotation to the
query-only method getMyComments in CommentUseCaseImpl: mark the method with
`@Transactional`(readOnly = true) (matching getComments) so JPA can optimize flush
behavior and enable read-replica routing; ensure the import for
org.springframework.transaction.annotation.Transactional is present if missing.
src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java (1)

23-37: Add null check before calling TimeUtils.preview(community.getContent()).

The content field in the Community entity has no @NotNull annotation and the create() method does not validate it, allowing null values. Since TimeUtils.preview() calls substring() directly on the content, it will throw NullPointerException if content is null.

Proposed fix
 	public static CommunitySummaryCacheDto from(Community community, long likeCount, long commentCount) {
 		String thumbnail = (community.getImageUrls() != null && !community.getImageUrls().isEmpty())
 			? community.getImageUrls().get(0)
 			: null;
+		String content = community.getContent();
+		String preview = (content != null) ? TimeUtils.preview(content) : "";

 		return new CommunitySummaryCacheDto(
 			community.getCommunityId(),
 			community.getField(),
 			community.getTitle(),
-			TimeUtils.preview(community.getContent()),
+			preview,
 			likeCount,
 			commentCount,
 			community.getCreatedAt(),
 			thumbnail
 		);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java`
around lines 23 - 37, CommunitySummaryCacheDto.from should guard against null
community content before calling TimeUtils.preview; update the method
(CommunitySummaryCacheDto.from) to check community.getContent() for null and
pass a safe default (e.g., empty string) or handle accordingly so
TimeUtils.preview is never invoked with null (reference Community.getContent and
TimeUtils.preview) — ensure the thumbnail logic and other parameters remain
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java`:
- Around line 72-73: The updateComment method currently calls
communityCacheService.evictCommentsAndDetail(comment.getCommunityId()) directly
causing a race; change it to schedule eviction after transaction commit using
TransactionUtils.afterCommit(() ->
communityCacheService.evictCommentsAndDetail(comment.getCommunityId())),
mirroring how createComment and deleteComment perform eviction, so cache is only
invalidated once the DB transaction has successfully committed.

In `@src/main/java/tave/crezipsa/crezipsa/global/common/TimeUtils.java`:
- Around line 21-22: The preview method in TimeUtils risks NPE when content is
null; update the preview(String content) implementation to guard for null (e.g.,
return an empty string or other defined fallback) before calling
content.substring, preserving the existing behavior for non-null values by
returning content.substring(0, Math.min(50, content.length())). Ensure the
null-check is performed at the start of the preview method so callers across DTO
mappings are protected.

---

Nitpick comments:
In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.java`:
- Around line 50-64: The toResponse method in CommunityDetailCacheDto currently
has a parameter named comments which shadows the record component comments;
rename the parameter (e.g., commentResponses or responseComments) in the
CommunityDetailCacheDto.toResponse(...) signature and update the single usage
inside the method (the argument passed to new CommunityDetailResponse) so the
record component and parameter are distinct, ensuring callers and imports are
updated accordingly.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java`:
- Around line 23-37: CommunitySummaryCacheDto.from should guard against null
community content before calling TimeUtils.preview; update the method
(CommunitySummaryCacheDto.from) to check community.getContent() for null and
pass a safe default (e.g., empty string) or handle accordingly so
TimeUtils.preview is never invoked with null (reference Community.getContent and
TimeUtils.preview) — ensure the thumbnail logic and other parameters remain
unchanged.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java`:
- Around line 105-114: Add the read-only transactional annotation to the
query-only method getMyComments in CommentUseCaseImpl: mark the method with
`@Transactional`(readOnly = true) (matching getComments) so JPA can optimize flush
behavior and enable read-replica routing; ensure the import for
org.springframework.transaction.annotation.Transactional is present if missing.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java`:
- Around line 95-97: Replace the lambda mapping in the stream with a method
reference for clarity: change the .map(dto -> dto.toResponse()) call inside the
chain returned by communityCacheService.getAllCommunitiesCache() to use the
DTO's toResponse method reference, and apply the same change in the
getCommunitiesByField flow at the corresponding .map usage; ensure the method
reference matches the DTO class that defines toResponse.

In `@src/main/java/tave/crezipsa/crezipsa/global/common/TransactionUtils.java`:
- Around line 15-16: The afterCommit method in TransactionUtils should validate
its Runnable parameter and fail-fast on null; add a null-check at the start of
afterCommit(action) that throws a clear NullPointerException (or
IllegalArgumentException) with a descriptive message (e.g., "action must not be
null") so callers get an immediate, actionable error instead of a later NPE when
registering the callback.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9de56ee and d61e065.

📒 Files selected for processing (13)
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommentCacheDto.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/response/CommunityDetailResponse.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/response/CommunitySummaryResponse.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/response/MyCommentResponse.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/response/MyCommunityResponse.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/response/MyLikedCommunityResponse.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java
  • src/main/java/tave/crezipsa/crezipsa/global/common/TimeUtils.java
  • src/main/java/tave/crezipsa/crezipsa/global/common/TransactionUtils.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommentCacheDto.java
  • src/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.java

Comment on lines +72 to +73
communityCacheService.evictCommentsAndDetail(comment.getCommunityId());
return commentMapper.toCommentResponse(comment, writer, true, relativeTime, List.of());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition: updateComment evicts cache without afterCommit.

Unlike createComment (line 56) and deleteComment (line 92) which correctly use TransactionUtils.afterCommit(), updateComment calls eviction directly. This creates the same race condition that was fixed elsewhere: concurrent requests can repopulate stale cache before the transaction commits.

🐛 Proposed fix
 		comment.update(request.content());
 		User writer = findUserOrThrow(userId);
 		String relativeTime = TimeUtils.convertToRelativeTime(comment.getCreatedAt());

-		communityCacheService.evictCommentsAndDetail(comment.getCommunityId());
-		return commentMapper.toCommentResponse(comment, writer, true, relativeTime, List.of());
+		CommentResponse response = commentMapper.toCommentResponse(comment, writer, true, relativeTime, List.of());
+		TransactionUtils.afterCommit(() -> communityCacheService.evictCommentsAndDetail(comment.getCommunityId()));
+		return response;
 	}
📝 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.

Suggested change
communityCacheService.evictCommentsAndDetail(comment.getCommunityId());
return commentMapper.toCommentResponse(comment, writer, true, relativeTime, List.of());
CommentResponse response = commentMapper.toCommentResponse(comment, writer, true, relativeTime, List.of());
TransactionUtils.afterCommit(() -> communityCacheService.evictCommentsAndDetail(comment.getCommunityId()));
return response;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.java`
around lines 72 - 73, The updateComment method currently calls
communityCacheService.evictCommentsAndDetail(comment.getCommunityId()) directly
causing a race; change it to schedule eviction after transaction commit using
TransactionUtils.afterCommit(() ->
communityCacheService.evictCommentsAndDetail(comment.getCommunityId())),
mirroring how createComment and deleteComment perform eviction, so cache is only
invalidated once the DB transaction has successfully committed.

Comment on lines +21 to +22
public static String preview(String content) {
return content.substring(0, Math.min(50, content.length()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard preview against null input.

At Line 22, content == null will throw NPE. Since this utility is shared across multiple DTO mappings, a null-safe fallback avoids broad endpoint failures.

Proposed patch
 	public static String preview(String content) {
+		if (content == null) return "";
 		return content.substring(0, Math.min(50, content.length()));
 	}
📝 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.

Suggested change
public static String preview(String content) {
return content.substring(0, Math.min(50, content.length()));
public static String preview(String content) {
if (content == null) return "";
return content.substring(0, Math.min(50, content.length()));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/tave/crezipsa/crezipsa/global/common/TimeUtils.java` around
lines 21 - 22, The preview method in TimeUtils risks NPE when content is null;
update the preview(String content) implementation to guard for null (e.g.,
return an empty string or other defined fallback) before calling
content.substring, preserving the existing behavior for non-null values by
returning content.substring(0, Math.min(50, content.length())). Ensure the
null-check is performed at the start of the preview method so callers across DTO
mappings are protected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant