[refactor] #62 커뮤니티 조회 API Redis 캐싱 적용 및 성능 검증#63
Conversation
…to, CommentCacheDto (relativeTime 문제 해결, isLiked/isWriter 분리)
…cation 방지를 위한 별도 빈 분리
- 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>
📝 WalkthroughWalkthroughThe 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 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: 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 | 🟠 MajorCache 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 outdatedlikeCount.🐛 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 | 🔴 CriticalAdd cache eviction verification for new like creation.
When a user first likes a community,
evictCommunityAllis not called, causing stale cache entries. The test at lines 55-72 should verify cache invalidation occurs. Addverify(communityCacheService).evictCommunityAll(communityId);to the test, and update the implementation to callcommunityCacheService.evictCommunityAll(communityId);before returning in the new like path (aftercommunity.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:openis never used.The
openfunction fromk6/experimental/fsis 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
TOKENis 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:checkis never used.The
checkfunction 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.
previewandconvertToRelativeTimeare static utility methods that are now being called from DTO classes. These methods don't belong inLikeUseCaseImpland 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 ofLaissezFaireSubTypeValidator.
LaissezFaireSubTypeValidatoraccepts 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
BasicPolymorphicTypeValidatorwith 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 구현체 정적 메서드 의존을 분리하세요.
CommunitySummaryCacheDto가LikeUseCaseImpl에 직접 의존해서 계층 결합도가 올라갔습니다. 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
📒 Files selected for processing (15)
.github/workflows/dev-cicd.ymlbuild.gradlek6/debug_test.jsk6/load_test.jssrc/main/java/tave/crezipsa/crezipsa/application/community/cache/CommunityCacheService.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommentCacheDto.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.javasrc/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.javasrc/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.javasrc/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.javasrc/main/java/tave/crezipsa/crezipsa/global/config/CacheConfig.javasrc/test/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImplTest.javasrc/test/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImplTest.javasrc/test/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImplTest.java
| console.log(`status: ${res.status}`); | ||
| console.log(`body: ${res.body.substring(0, 300)}`); |
There was a problem hiding this comment.
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.
| import java.util.Collections; | ||
| import java.util.Map; |
There was a problem hiding this comment.
import 그룹 순서를 가이드에 맞춰 정리해 주세요.
java.util.* import가 프로젝트 import 뒤에 위치해 있습니다. java → jakarta/javax → org → lombok → project 순서로 정렬해 주세요.
As per coding guidelines **/*.java: Import grouping follows typical order: java → jakarta/javax → org → lombok → 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.
| CommunityResponse response = CommunityResponse.of(communityRepository.save(community)); | ||
| communityCacheService.evictCommunityLists(); | ||
| return response; |
There was a problem hiding this comment.
🧩 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.javaRepository: 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.javaRepository: CREJIPSA/CreJipsa-BE
Length of output: 8170
🏁 Script executed:
# Search for `@TransactionalEventListener` usage in the codebase (pattern mentioned in suggestion)
rg -n '@TransactionalEventListener' --type=javaRepository: 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 2Repository: 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.javaRepository: 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 -100Repository: 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 -100Repository: 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 -20Repository: 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=javaRepository: 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=javaRepository: 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.javaRepository: 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:
- Cache evicted (now empty)
- Concurrent request reads empty cache, queries uncommitted DB state, and repopulates with stale data
- 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.
| 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(); |
There was a problem hiding this comment.
목록 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).
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>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.java (1)
50-64: Avoid shadowingcommentsintoResponse(...).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.
actionis 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
getCommunitiesByFieldat 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.
getMyCommentsonly reads data but lacks thereadOnlyannotation thatgetCommentshas. 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 callingTimeUtils.preview(community.getContent()).The
contentfield in the Community entity has no@NotNullannotation and thecreate()method does not validate it, allowing null values. SinceTimeUtils.preview()callssubstring()directly on the content, it will throwNullPointerExceptionif 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
📒 Files selected for processing (13)
src/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommentCacheDto.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunityDetailCacheDto.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/cache/CommunitySummaryCacheDto.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/response/CommunityDetailResponse.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/response/CommunitySummaryResponse.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/response/MyCommentResponse.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/response/MyCommunityResponse.javasrc/main/java/tave/crezipsa/crezipsa/application/community/dto/response/MyLikedCommunityResponse.javasrc/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommentUseCaseImpl.javasrc/main/java/tave/crezipsa/crezipsa/application/community/usecase/CommunityUseCaseImpl.javasrc/main/java/tave/crezipsa/crezipsa/application/community/usecase/LikeUseCaseImpl.javasrc/main/java/tave/crezipsa/crezipsa/global/common/TimeUtils.javasrc/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
| communityCacheService.evictCommentsAndDetail(comment.getCommunityId()); | ||
| return commentMapper.toCommentResponse(comment, writer, true, relativeTime, List.of()); |
There was a problem hiding this comment.
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.
| 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.
| public static String preview(String content) { | ||
| return content.substring(0, Math.min(50, content.length())); |
There was a problem hiding this comment.
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.
| 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.
개요
커뮤니티 목록·상세·댓글 조회 API가 매 요청마다 DB를 다중 조회하는 구조였습니다.
이미 인프라에 올라와 있는 Redis를 활용해 Spring Cache 추상화를 적용하고, 부하 테스트로 성능 개선을 수치로 검증했습니다.
구현 내용
1. CacheConfig
RedisCacheManager빈을 등록하고 Jackson 직렬화(GenericJackson2JsonRedisSerializer)와 TTL 5분을 설정합니다.캐시 키 전략상 userId를 포함하지 않아 사용자 수에 무관하게 캐시 엔트리가 일정하게 유지됩니다.
2. CommunityCacheService (별도 빈 분리)
@Cacheable/@CacheEvict를 한 곳에서 관리합니다.같은 클래스 내부에서 호출하면 AOP 프록시가 동작하지 않는 self-invocation 문제를 피하기 위해 별도 빈으로 분리했습니다.
getAllCommunitiesCache()community-list"all"getCommunitiesByFieldCache(field)community-list-by-fieldfield.name()getCommunityDetailCache(communityId)community-detailcommunityIdgetCommentsCache(communityId)comment-listcommunityId3. CacheDto 3종
relativeTime("40분 전" 등)은 캐시에 저장하면 시간이 고정되므로 rawLocalDateTime으로 저장하고,응답 변환 시점에 재계산합니다.
isLiked/isWriter는 사용자마다 다르므로 캐시에는 저장하지 않고 캐시 조회 후 별도 계산합니다.4. 캐시 무효화 전략
community-list,community-list-by-fieldcommunity-detail::{id}comment-list::{id}community-detail::{communityId},comment-list::{communityId}community-detail::{communityId}, 목록 캐시 전체부하 테스트 결과
배경 — JMeter 설정 실수와 수정
첫 번째 JMeter 실행 (잘못된 설정)
Ramp-Up(130초) > Duration(120초)로 설정되어 있어, 테스트가 끝날 때까지 100개 스레드가 다 뜨지 않습니다.루프 횟수도 10회로 제한되어 실제 동시 사용자는 약 46명 수준, 처리량은 30 req/s에 불과했습니다.
이 조건에서는 DB에 가해지는 압박이 작아 캐싱 효과가 희석돼 평균 응답시간 개선이 23% 수준에 그쳤습니다.
-캐싱 적용 전
-캐싱 적용 후

두 번째 JMeter 실행 (수정된 설정)
Ramp-Up을 Duration 이하로 줄이고 Loop를 무한으로 설정해 실제 100 VU가 120초 동안 지속적으로 부하를 줍니다.
API별 평균 응답시간 (캐싱 전 → 후)
-캐싱 전

-캐싱 후

k6 병행 검증
JMeter와 독립적으로 k6를 이용해 같은 시나리오를 검증했습니다.
k6 설정 설명
k6 결과
k6는 JMeter보다 더 높은 실효 부하(~413 req/s)를 주었기 때문에 DB 커넥션 경쟁이 심해져 캐싱 효과가 더 크게 나타났습니다.
결론 — 캐싱 적용 효과가 있는가?
있습니다.
두 도구 모두에서 일관되게 개선이 확인됐습니다.
현재 로컬 환경(DB가 같은 머신)에서 측정한 수치입니다. 실제 EC2 환경에서는 RDS 네트워크 왕복 레이턴시(~2–5ms × 쿼리 수)가 추가로 붙기 때문에 개선 폭이 더 크게 나타날 것으로 예상됩니다.
Summary by CodeRabbit
Release Notes
New Features
Chores
Tests