[Refactor] 피드 도메인 캐싱 도입 & 피드 전체 조회 api 캐싱 로직 추가#349
[Refactor] 피드 도메인 캐싱 도입 & 피드 전체 조회 api 캐싱 로직 추가#349
Conversation
- FeedCreatedEvent, FeedUpdatedEvent, FeedDeletedEvent, UserWithdrawnEvent 처리 - 트랜잭션 커밋 이후 캐시 갱신하도록 TransactionalEventListener 적용 (#345)
- feedIdTop 및 feedDetail 캐시 조회, 갱신, 삭제 기능 - 신규 피드 추가 시 topId 리스트 업데이트 및 상세 캐시 적재 - 피드 수정 시 feedDetail 캐시 무효화 후 재적재 - 피드 삭제 및 사용자 탈퇴 시 관련 캐시 일괄 제거 및 인덱스 갱신 - 캐시 워밍업 기능 (#345)
- ApplicationReadyEvent 수신 시 상위 피드 ID 및 상세 데이터 캐시 적재 (#345)
-FeedCacheHandler를 이용해 상위 피드 ID와 개별 피드 상세 캐시 우선 조회 -캐시 범위 벗어나거나 데이터 없을 경우 DB(FeedQueryPersistenceAdapter)로 위임 (#345)
-findTopFeedIds -findFeedDetailById -findFeedDetailsByIds (#345)
-findTopFeedIds -findFeedDetailById -findFeedDetailsByIds (#345)
Walkthrough스프링 캐시 인프라를 도입하여 피드 조회 성능을 개선하고, 피드 생성/수정/삭제 시 캐시 무효화 이벤트를 발행하는 구조를 구축했습니다. Caffeine 기반 두 개의 캐시(상위 피드 ID, 피드 상세)와 이벤트 기반 캐시 관리 체계를 추가했으며, 페이지 네이션 방식을 생성시간 기반에서 피드 ID 기반으로 변경했습니다. Changes
Sequence DiagramsequenceDiagram
participant Client
participant FeedController
participant FeedQueryCacheAdapter
participant CacheManager
participant FeedQueryPersistenceAdapter
participant Database
Client->>FeedController: GET /feeds?cursor=...
FeedController->>FeedQueryCacheAdapter: findLatestFeedsByFeedId(userId, cursor)
FeedQueryCacheAdapter->>CacheManager: getCache("feedIdTop").get(key)
alt Cache Hit - Sufficient Data
CacheManager-->>FeedQueryCacheAdapter: topIds[...]
FeedQueryCacheAdapter->>CacheManager: getCache("feedDetail").get(feedId)
CacheManager-->>FeedQueryCacheAdapter: FeedQueryDto[...]
FeedQueryCacheAdapter-->>FeedController: CursorBasedList<FeedQueryDto>
else Cache Miss or Insufficient Data
FeedQueryCacheAdapter->>FeedQueryPersistenceAdapter: findLatestFeedsByFeedId(userId, lastPostId, size)
FeedQueryPersistenceAdapter->>Database: SELECT feeds WHERE ... ORDER BY feedId DESC
Database-->>FeedQueryPersistenceAdapter: FeedQueryDto[...]
FeedQueryPersistenceAdapter-->>FeedQueryCacheAdapter: CursorBasedList<FeedQueryDto>
FeedQueryCacheAdapter-->>FeedController: CursorBasedList<FeedQueryDto>
end
FeedController-->>Client: 200 OK + Feed List JSON
Client->>FeedController: POST /feeds (create feed)
FeedController->>FeedCreateService: createFeed(request)
FeedCreateService->>Database: INSERT feed
FeedCreateService->>ApplicationEventPublisher: publishEvent(FeedCreatedEvent)
ApplicationEventPublisher->>FeedCacheEventListener: handleFeedCreatedEvent(event)
FeedCacheEventListener->>FeedCacheHandler: updateTopIdsWithNewId(feedId)
FeedCacheHandler->>CacheManager: getCache("feedIdTop").put(...)
FeedCacheEventListener->>FeedCacheHandler: warmUpFeedDetails([feedId])
FeedCacheHandler->>CacheManager: getCache("feedDetail").put(feedId, detail)
FeedCacheHandler-->>FeedCacheEventListener: done
FeedCacheEventListener-->>FeedCreateService: done
FeedCreateService-->>FeedController: FeedQueryDto
FeedController-->>Client: 201 Created + Feed JSON
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분 Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
Test Results514 tests 514 ✅ 46s ⏱️ Results for commit 86886de. |
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
103-122:⚠️ Potential issue | 🔴 Critical
null반환 대신 빈 리스트를 반환하세요.Line 108에서 피드가 없을 때
null을 반환하면 호출 측에서 null 체크 없이 직접 사용될 위험이 있습니다.UserDeleteService84줄에서deletedFeedIds를 받은 후 97줄의 이벤트 발행 시 null 체크 없이 전달되고 있으므로, NPE 가능성이 존재합니다.List.of()또는Collections.emptyList()를 반환하는 것이 안전합니다. 또한 Line 104와 Line 106에 동일한 주석이 중복되어 있습니다.수정 제안
public List<Long> deleteAllFeedByUserId(Long userId) { // 1. 유저가 작성한 피드 게시글 ID 리스트 조회 List<Long> feedIds = feedJpaRepository.findFeedIdsByUserId(userId); - // 1. 유저가 작성한 피드 게시글 ID 리스트 조회 if (feedIds == null || feedIds.isEmpty()) { - return null; // early return + return List.of(); // early return }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java` around lines 103 - 122, In deleteAllFeedByUserId (FeedCommandPersistenceAdapter) replace the null early-return with returning an empty immutable list (e.g., List.of() or Collections.emptyList()) so callers like UserDeleteService can safely iterate without NPE; also remove the duplicated comment above feedIds retrieval so the method has a single correct comment for step 1.
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListener.java (1)
25-28:handleFeedDeletedEvent가markAsDeleted만 호출하여feedIdTop캐시에 삭제된 피드 ID가 잔존합니다.이 문제의 근본 원인은
FeedCacheHandler.markAsDeleted에서 상위 ID 목록을 갱신하지 않는 것이며, 해당 파일 리뷰에서 상세히 기술했습니다. 이 핸들러에서도 top IDs 갱신 로직 추가를 검토해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListener.java` around lines 25 - 28, handleFeedDeletedEvent only calls feedCacheHandler.markAsDeleted which leaves the feedIdTop cache stale; after calling markAsDeleted you must also update the top-IDs cache—add a call like feedCacheHandler.removeFromTopIds(event.feedId()) (or feedCacheHandler.refreshTopIds()/updateTopIdsAfterDeletion(event.feedId())) and implement that method in FeedCacheHandler to remove the deleted feed ID from the top IDs cache and persist the updated top list.
🧹 Nitpick comments (22)
loadtest/feed/feed_home_scrolling_test.js (2)
34-43:token이 null/undefined일 경우 별도 처리가 없습니다
setup()에서 특정 userId의 토큰 발급이 실패하면 해당 VU(vuIdx)는 유효하지 않은 토큰으로 모든 요청을 보내게 됩니다. VU 시나리오 초반에 토큰 유효성을 확인하고 조기 종료하면 실패 노이즈를 줄일 수 있습니다.✏️ 수정 제안
export default function (data) { const vuIdx = __VU - 1; const token = data.tokens[vuIdx]; + if (!token) { + console.warn(`VU ${__VU}: no valid token, skipping scenario`); + return; + } + const params = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@loadtest/feed/feed_home_scrolling_test.js` around lines 34 - 43, The test does not handle a missing/invalid token (token from data.tokens[vuIdx]) so a VU may send unauthorized requests; before building params in the default exported function, check if token is null/undefined and bail out early: detect token using the existing vuIdx/token variables and either abort the VU scenario (throw or return) or call the appropriate k6 stop/sleep to avoid sending requests, and log the failure for visibility; update any logic that uses params/headers (the params object and Authorization header construction) to only run when token is present.
14-17:p(95)<80ms임계값은 500 VU 부하 시나리오에서 과도하게 엄격할 수 있습니다로컬 환경(localhost:8000)에서 캐시 미스, DB 접근, JVM GC 등을 고려하면 500 VU 피크 부하 시 p95 80ms 달성이 어려울 수 있으며, 테스트 자체가 항상 실패로 기록되어 실질적인 성능 비교 지표로서의 의미가 희석될 수 있습니다. 캐시 도입 전/후 비교 측정이 목적이라면, 현실적인 기준선을 먼저 측정 후 임계값을 설정하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@loadtest/feed/feed_home_scrolling_test.js` around lines 14 - 17, 현재 thresholds 객체의 http_req_duration: ['p(95)<80'] 임계값은 500 VU 시나리오에서 과도하게 엄격해 테스트가 항상 실패할 수 있으니, feed_home_scrolling_test.js의 thresholds.http_req_duration 설정을 현실적인 값으로 완화하거나(예: p(95)<200ms 또는 환경변수로 조정 가능하게) 임계값을 제거해 먼저 베이스라인을 측정하도록 변경하세요; 수정할 위치는 파일의 thresholds 블록(키명: http_req_duration, http_req_failed)이며 필요하면 환경변수 기반 구성(예: PROCESS_ENV 또는 옵션으로 전달)으로 만들고 문서에 새로운 기본값을 기록해 주세요.src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (2)
21-22: 수동 타임스탬프 조작 코드 제거 후 남은 불필요한 임포트·필드를 정리해야 합니다.
JdbcTemplate주입(Line 65–66) 및Timestamp,LocalDateTime임포트(Line 26–27),JdbcTemplate임포트(Line 21)는 이전의created_at직접 수정 로직이 제거된 이후 사용되지 않습니다.♻️ 제안하는 정리
-import org.springframework.jdbc.core.JdbcTemplate; ... -import java.sql.Timestamp; -import java.time.LocalDateTime; ... - `@Autowired` - private JdbcTemplate jdbcTemplate;Also applies to: 26-27, 65-66
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java` around lines 21 - 22, Remove leftover unused timestamp-manipulation artifacts in FollowingPriorityFeedShowAllApiTest: delete the unused imports for JdbcTemplate, java.sql.Timestamp, and java.time.LocalDateTime, and remove the injected JdbcTemplate field (and any related unused fields) from the test class so the compiler/IDE no longer reports unused imports/fields; search for symbols JdbcTemplate, Timestamp, and LocalDateTime in FollowingPriorityFeedShowAllApiTest to locate and delete the dead code.
247-251: 커서 형식을 하드코딩하면 형식 변경 시 테스트가 오동작할 수 있습니다.
FeedQueryPersistenceAdapter.findFeedsByFollowingPriority()에서는Cursor를List.of("1", feedId)로 생성하여 인코딩하는데, 이 테스트는 같은 형식을 수동으로 조합하고 있습니다. 커서 포맷이 변경되면 테스트가 조용히 깨질 위험이 있습니다.
Cursor또는CursorUtils에 테스트 전용 팩토리 메서드를 추가하거나, 최소한 "1|" 포맷을 상수로 추출하여 변경 지점을 단일화하는 것을 검토하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java` around lines 247 - 251, The test hardcodes the cursor string "1|"+f3.getPostId() which will break if the Cursor encoding changes; update the test to build the cursor using the Cursor/CursorUtils API instead of manual concatenation (e.g., call the existing Cursor factory/encoder used by FeedQueryPersistenceAdapter.findFeedsByFollowingPriority or add a small test-only factory method on Cursor/CursorUtils and use that to produce nextCursor from List.of("1", f3.getPostId().toString())); replace the manual nextCursor construction in FollowingPriorityFeedShowAllApiTest with that encoded value so the test remains correct if the cursor format changes.src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1)
29-29: 인터페이스 계약에@Nullable여부를 명시하거나, 구현체에서 빈 리스트를 반환하도록 보장하세요.
FeedCommandPersistenceAdapter에서 현재null을 반환할 수 있으므로, 구현체 수정(이미 위에서 제안)과 함께 이 인터페이스의 반환값이 non-null임을 보장하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java` at line 29, The interface method FeedCommandPort.deleteAllFeedByUserId currently allows a null return implicitly; either annotate the contract explicitly or make it non-null. Update FeedCommandPort.deleteAllFeedByUserId to declare a non-null contract (add your project’s `@NotNull/`@NonNull annotation or Javadoc stating non-null) and ensure FeedCommandPersistenceAdapter.deleteAllFeedByUserId implementation returns an empty List (e.g., Collections.emptyList()) instead of null so callers can rely on a non-null List<Long>.src/main/java/konkuk/thip/feed/adapter/out/event/dto/FeedUpdatedEvent.java (1)
1-8: 패키지 선언 앞의 빈 줄을 제거하세요.Line 1에 불필요한 빈 줄이 있습니다. 다른 이벤트 DTO(
FeedCreatedEvent,FeedDeletedEvent)와 일관성을 위해 제거하는 것이 좋습니다.♻️ 수정 제안
- package konkuk.thip.feed.adapter.out.event.dto;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/out/event/dto/FeedUpdatedEvent.java` around lines 1 - 8, Remove the leading blank line before the package declaration so the file starts immediately with "package konkuk.thip.feed.adapter.out.event.dto;" to match the style of the other event DTOs (FeedCreatedEvent, FeedDeletedEvent); update the top of the FeedUpdatedEvent record to have no empty line above the package statement.src/main/java/konkuk/thip/user/adapter/out/event/dto/UserWithdrawnEvent.java (1)
5-8: 팩토리 메서드 네이밍 불일치
FeedCreatedEvent.from(),FeedDeletedEvent.from()등 다른 이벤트 DTO들이from()을 사용하는 반면, 이 클래스만of()를 사용하고 있습니다. 코드베이스 내 일관성을 위해from()으로 통일하는 것을 고려해 주세요.♻️ 네이밍 통일 제안
- public static UserWithdrawnEvent of(Long userId, List<Long> deletedFeedIds) { - return new UserWithdrawnEvent(userId, deletedFeedIds); - } + public static UserWithdrawnEvent from(Long userId, List<Long> deletedFeedIds) { + return new UserWithdrawnEvent(userId, deletedFeedIds); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/user/adapter/out/event/dto/UserWithdrawnEvent.java` around lines 5 - 8, The factory method in UserWithdrawnEvent is named of(), which is inconsistent with other event DTOs like FeedCreatedEvent.from() and FeedDeletedEvent.from(); rename the static method UserWithdrawnEvent.of(Long userId, List<Long> deletedFeedIds) to UserWithdrawnEvent.from(Long userId, List<Long> deletedFeedIds) and update any call sites to use UserWithdrawnEvent.from(...) to keep factory naming consistent across the codebase.src/main/java/konkuk/thip/config/cache/CacheConfig.java (2)
33-43:caffeineBuilder메서드명이 반환 타입과 불일치메서드명
caffeineBuilder는Caffeine<Object, Object>빌더를 반환하는 것처럼 읽히지만 실제로는.build()까지 호출한 완성된Cache<Object, Object>인스턴스를 반환합니다.♻️ 메서드명 수정 제안
- private Cache<Object, Object> caffeineBuilder(CacheType cacheType) { + private Cache<Object, Object> buildCaffeineCache(CacheType cacheType) {- caffeineBuilder(cacheType) + buildCaffeineCache(cacheType)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/config/cache/CacheConfig.java` around lines 33 - 43, The method caffeineBuilder returns a built Cache<Object,Object> but its name implies it returns a Caffeine builder; rename the method caffeineBuilder to a name that reflects it returns a Cache (e.g., buildCache, createCache, or cacheForType) and update all callers accordingly; ensure the signature remains private Cache<Object,Object> caffeineBuilder(...) → private Cache<Object,Object> buildCache(...) (or chosen name) and keep the internal logic (Caffeine.newBuilder()... .build()) unchanged so callers receive the constructed Cache instance.
22-27:Collectors.toList()대신Stream.toList()사용 권장Java 16 이상 환경에서는
Stream.toList()가 더 간결하고 불변 리스트를 반환하는 관용적 표현입니다.♻️ 리팩토링 제안
- List<CaffeineCache> caches = Arrays.stream(CacheType.values()) - .map(cacheType -> new CaffeineCache( - cacheType.getCacheName(), - caffeineBuilder(cacheType) - )) - .collect(Collectors.toList()); + List<CaffeineCache> caches = Arrays.stream(CacheType.values()) + .map(cacheType -> new CaffeineCache( + cacheType.getCacheName(), + buildCaffeineCache(cacheType) + )) + .toList();(
import java.util.stream.Collectors;불필요해지므로 제거 가능)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/config/cache/CacheConfig.java` around lines 22 - 27, 현재 CacheConfig의 캐시 리스트 생성 코드에서 Arrays.stream(...).collect(Collectors.toList())를 사용하고 있는데 Java 16+ 환경에서는 더 간결하고 불변 리스트를 반환하는 Stream.toList()로 교체하세요: CacheConfig에서 Arrays.stream(CacheType.values()).map(...).toList()로 바꾸고 불필요해진 import java.util.stream.Collectors를 제거하며 caffeineBuilder 및 CacheType.getCacheName 참조는 그대로 유지해 주세요.src/main/java/konkuk/thip/config/cache/CacheType.java (2)
9-13:FEED_ID_TOP주석과maximumSize불일치주석에 "100개 인덱스 캐시"라고 명시되어 있으나
maximumSize=1입니다. 이는 캐시 엔트리 1개 안에 ID 리스트 전체를 담는 설계일 경우 올바르지만, 주석만 보면 100개의 캐시 엔트리가 있는 것으로 오해할 수 있습니다. 주석을 명확히 해주세요.♻️ 주석 명확화 제안
- // 최신순 피드 ID 100개 인덱스 캐시 - FEED_ID_TOP( - "feedIdTop", - 0, // 만료 시간 없음 - 1), // + // 최신순 피드 ID 목록 전체를 단일 엔트리로 저장하는 인덱스 캐시 (maximumSize=1: 키 1개, 값은 ID 리스트) + FEED_ID_TOP( + "feedIdTop", + 0, // 만료 시간 없음 + 1),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/config/cache/CacheType.java` around lines 9 - 13, FEED_ID_TOP의 주석과 실제 설정(maximumSize=1)이 불일치합니다; 주석을 수정하여 의도한 설계를 명확히 하세요 — 만약 설계가 "각 캐시 엔트리에 최신 100개 ID 리스트를 담는 단일 엔트리"라면 주석을 "최신 100개 ID를 하나의 캐시 엔트리에 저장"처럼 명확히 바꾸고, 반대로 의도가 "최신 피드 ID 100개를 각각의 캐시 엔트리로 보관"이라면 FEED_ID_TOP의 maximumSize를 100으로 변경하세요; 관련 식별자는 FEED_ID_TOP과 해당 maximumSize 설정입니다.
20-22:expireAfterWrite=0의 의미가 타입에서 드러나지 않음
int타입과0값으로 "만료 없음"을 표현하는 것은CacheConfig구현 내부 컨벤션에 의존합니다. 향후 유지보수 시 다른 개발자가0의 의미를 오해할 수 있습니다.♻️ 명시적 표현 제안 (선택 사항)
- private final int expireAfterWrite; + private final long expireAfterWriteSeconds; // 0 이하: 만료 없음(무한)또는 Java Duration을 사용해
null을 "만료 없음"으로 표현하는 방법도 고려할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/config/cache/CacheType.java` around lines 20 - 22, The field expireAfterWrite in CacheType currently uses int with 0 meaning "no expiry", which is unclear; change expireAfterWrite to an explicit type (e.g., Optional<Long> / OptionalInt or java.time.Duration / `@Nullable` Integer) or introduce a named sentinel constant (e.g., NO_EXPIRE = -1) and update CacheType's constructor, getters and any code that reads expireAfterWrite (including CacheConfig consumer code) to use the new representation and treat Optional.empty()/Duration.ZERO or NO_EXPIRE consistently as "no expiry"; also add a short javadoc on CacheType.expireAfterWrite to document the meaning and update any callers (e.g., where CacheType.expireAfterWrite is checked) to handle the new semantics.src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (1)
30-30:findFeedDetailById의 반환 타입을Optional<FeedQueryDto>로 변경 권장현재 구현에서
fetchOne()이 null을 반환할 수 있으며, 이를 명시적으로 반환하고 있습니다.FeedCacheHandler와FeedQueryPersistenceAdapter의 호출부 모두에서 null 체크 없이 반환값을 직접 사용하고 있어, 존재하지 않는 feedId 조회 시 NPE 또는 null 캐싱 문제가 발생할 수 있습니다. Java 관용 표현인Optional<FeedQueryDto>반환으로 null 안전성을 개선하세요.♻️ Optional 적용 제안
- FeedQueryDto findFeedDetailById(Long feedId); + Optional<FeedQueryDto> findFeedDetailById(Long feedId);인터페이스 상단에
import java.util.Optional;추가 필요, 구현체 및 호출부(FeedCacheHandler,FeedQueryPersistenceAdapter등)에도 동일하게 반영해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java` at line 30, Change the signature of findFeedDetailById to return Optional<FeedQueryDto> instead of nullable FeedQueryDto and update its implementation to wrap fetchOne() result with Optional.ofNullable; add import java.util.Optional and adjust all callers (FeedCacheHandler, FeedQueryPersistenceAdapter, etc.) to handle the Optional (e.g., orElse / orElseThrow / isPresent) instead of assuming non-null, ensuring no direct null usage or caching of null values.src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1)
13-13:getFeedDetail네이밍이 인터페이스 내 다른 메서드(findXxx)와 일관되지 않습니다.다른 QueryPort 메서드들은
find접두사를 사용하고 있습니다. 일관성을 위해findFeedDetail로 변경을 고려해 볼 수 있습니다. 다만 기능적 문제는 아닙니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java` at line 13, 인터페이스 FeedQueryPort의 메서드명 getFeedDetail이 다른 메서드들(findXxx)과 일관성이 없으니 getFeedDetail을 findFeedDetail로 변경하고 관련 구현체와 호출부 모두에서 메서드명을 동일하게 갱신하세요; 변경 대상 식별자: FeedQueryPort 인터페이스의 getFeedDetail, 반환형 FeedQueryDto, 그리고 이를 구현하는 클래스들 및 서비스/컨트롤러에서 호출하는 모든 참조를 찾아 findFeedDetail로 리네임하고 컴파일/테스트 통과 여부를 확인하면 됩니다.src/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheWarmupListenerTest.java (1)
24-29:@SpringBootTest를 사용하므로 "단위 테스트"보다는 "통합 테스트"로 분류하는 것이 적절합니다.전체 Spring 컨텍스트를 로드하는
@SpringBootTest기반 테스트는 일반적으로 통합 테스트로 간주됩니다.@DisplayName의 "[단위]" 표기가 혼동을 줄 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheWarmupListenerTest.java` around lines 24 - 29, The test class FeedCacheWarmupListenerTest is annotated with `@SpringBootTest` (loads full Spring context) but its `@DisplayName` labels it as "[단위]" (unit); update the `@DisplayName` to indicate it's an integration test (e.g., replace "[단위]" with "[통합]" or "[통합] FeedCacheWarmupListener 통합 테스트") so the test classification matches the use of `@SpringBootTest` and avoids confusion.src/test/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandlerTest.java (1)
102-119:refreshCacheAfterBulkDelete테스트에서feedDetail캐시 mock 설정이 누락되어, 상세 캐시 제거(evictFeeds) 및 워밍업 경로가 검증되지 않습니다.
refreshCacheAfterBulkDelete내부에서evictFeeds는cacheManager.getCache("feedDetail")을 호출하는데, 이 테스트에서는"feedDetail"에 대한 stub이 없어null이 반환되고 eviction이 무시됩니다. 또한warmUpFeedDetails에서도 마찬가지입니다. 상세 캐시 제거와 신규 진입 피드 워밍업까지 검증하려면detailCachestub을 추가해 주세요.제안: detailCache stub 추가
void refresh_when_contains_deleted() { List<Long> currentTop = new ArrayList<>(List.of(5L, 4L, 3L)); List<Long> deleted = List.of(5L); String cacheKey = "top" + DEFAULT_CACHE_SIZE; when(cacheManager.getCache("feedIdTop")).thenReturn(topCache); + when(cacheManager.getCache("feedDetail")).thenReturn(detailCache); when(topCache.get(cacheKey, List.class)) .thenReturn(currentTop); when(feedJpaRepository.findTopFeedIds(DEFAULT_CACHE_SIZE)) .thenReturn(List.of(4L, 3L, 2L)); + when(feedJpaRepository.findFeedDetailsByIds(List.of(2L))) + .thenReturn(List.of()); handler.refreshCacheAfterBulkDelete(deleted); verify(topCache).put(cacheKey, List.of(4L, 3L, 2L)); + verify(detailCache).evict(5L); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandlerTest.java` around lines 102 - 119, The test is missing a stub for the "feedDetail" cache so evictFeeds and warmUpFeedDetails paths inside refreshCacheAfterBulkDelete are not exercised; update the test to mock cacheManager.getCache("feedDetail") to return a mocked detailCache (e.g., detailCache) and set expected behaviors on detailCache (such as allowing evict calls and any get/put used by warmUpFeedDetails) so you can verify detailCache.evict(...) and any detailCache.put(...) interactions when handler.refreshCacheAfterBulkDelete(deleted) is invoked.src/main/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandler.java (1)
23-26:@Cacheable애노테이션과CacheManager직접 조작을 혼용하고 있어, 캐시 키 불일치 시 사일런트 버그 위험이 있습니다.
getTopIds()는@Cacheable로 SpEL 키'top' + 100→"top100"을 사용하고,updateTopIdsWithNewId()는"top" + DEFAULT_CACHE_SIZE→"top100"을 프로그래밍 방식으로 사용합니다. 현재는 일치하지만,DEFAULT_CACHE_SIZE를 변경하거나 SpEL 표현식을 수정하면 두 곳이 분리되어 키 불일치가 발생할 수 있습니다.캐시 키를 상수로 추출하거나,
@Cacheable을 제거하고CacheManager기반으로 일원화하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandler.java` around lines 23 - 26, Extract a single cache-key constant (e.g., CACHE_KEY_TOP = "top" + DEFAULT_CACHE_SIZE) in FeedCacheHandler and use that constant everywhere instead of hard-coded SpEL or string concatenation: update the `@Cacheable` on getTopIds() to reference the constant (or replace the annotation with a programmatic cache lookup using CacheManager and the same constant) and change updateTopIdsWithNewId() to use the same CACHE_KEY_TOP when reading/writing via CacheManager; ensure all usages (getTopIds, updateTopIdsWithNewId, DEFAULT_CACHE_SIZE references) are unified to avoid key drift.src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListener.java (1)
19-40: 이벤트 핸들러에 에러 처리가 없어, 캐시 조작 실패 시 예외가 전파됩니다.
@TransactionalEventListener(AFTER_COMMIT)핸들러에서 예외가 발생하면 트랜잭션에는 영향이 없지만, 로그 없이 실패하거나 Spring의 기본 예외 처리에 의존하게 됩니다. PR 설명에 이벤트 유실/재처리 미고려 상태라고 명시되어 있으므로, 최소한 try-catch로 예외를 로깅하는 방어 코드를 추가하면 운영 시 추적이 용이해집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListener.java` around lines 19 - 40, Wrap each transactional event handler (handleFeedCreatedEvent, handleFeedDeletedEvent, handleFeedUpdatedEvent, handleUserWithdrawn) in a try-catch that catches Exception and logs an error with contextual identifiers (e.g., event.feedId() or event.deletedFeedIds()) and the exception stacktrace, then swallow or handle the exception so it does not propagate; use the class logger (SLF4J org.slf4j.Logger) to emit a clear message per handler indicating which operation failed (updateTopIdsWithNewId, getFeedDetail, markAsDeleted, evictFeedDetailIfPresent, refreshCacheAfterBulkDelete) to aid production tracing.src/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListenerTest.java (1)
71-90:FeedUpdatedEvent테스트에서evictFeedDetailIfPresent가false를 반환하는 경우(캐시에 없는 피드 수정)의 검증이 누락되어 있습니다.
handleFeedUpdatedEvent는evictFeedDetailIfPresent가false이면getFeedDetail을 호출하지 않는 분기가 있습니다. 이 경로도 테스트하면 핸들러 로직의 완전성을 확보할 수 있습니다.추가 테스트 케이스 예시
`@Test` `@Transactional` `@DisplayName`("FeedUpdatedEvent 발행 → 캐시에 없는 피드면 getFeedDetail을 호출하지 않는다") void handleFeedUpdatedEvent_NotCached() { Long feedId = 1L; FeedUpdatedEvent event = FeedUpdatedEvent.from(feedId); given(feedCacheHandler.evictFeedDetailIfPresent(feedId)).willReturn(false); publisher.publishEvent(event); TestTransaction.flagForCommit(); TestTransaction.end(); verify(feedCacheHandler, times(1)).evictFeedDetailIfPresent(feedId); verify(feedCacheHandler, never()).getFeedDetail(feedId); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListenerTest.java` around lines 71 - 90, Add a new test in FeedCacheEventListenerTest to cover the branch where evictFeedDetailIfPresent returns false: create a FeedUpdatedEvent (use FeedUpdatedEvent.from(feedId)), stub feedCacheHandler.evictFeedDetailIfPresent(feedId) to return false, publish the event with publisher.publishEvent(event) and commit via TestTransaction.flagForCommit()/TestTransaction.end(), then verify evictFeedDetailIfPresent was called once and that feedCacheHandler.getFeedDetail(feedId) was never called; this ensures the handleFeedUpdatedEvent path where the cache miss skips getFeedDetail is exercised.src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllPersistenceApiTest.java (1)
69-93: Mock 설정이FeedQueryCacheAdapter의 일부 메서드만 커버합니다.
@MockitoBean은 stub 되지 않은 메서드에 대해 기본값(null,0,false등)을 반환합니다. 현재findLatestFeedsByFeedId와findSavedFeedIdsByUserIdAndFeedIds두 메서드만 stub 되어 있으므로,basic전략의 플로우가 변경되어 다른FeedQueryPort메서드를 호출하게 되면 테스트가NullPointerException으로 실패할 수 있습니다.Mockito의
delegatesTo기능이나@Autowired로 실제 persistence adapter를 직접 주입하는 방식을 고려해보세요.🔧 보다 안전한 대안: Mockito delegatesTo 사용
`@MockitoBean`(answers = Answers.CALLS_REAL_METHODS) // 또는 아래처럼 delegatesTo 활용 private FeedQueryCacheAdapter feedQueryCacheAdapter; `@BeforeEach` void setup() { // 모든 호출을 persistenceAdapter로 위임 given(feedQueryCacheAdapter.findLatestFeedsByFeedId(any(), any())) .willAnswer(invocation -> persistenceAdapter.findLatestFeedsByFeedId( invocation.getArgument(0), invocation.getArgument(1))); given(feedQueryCacheAdapter.findSavedFeedIdsByUserIdAndFeedIds(any(), any())) .willAnswer(invocation -> persistenceAdapter.findSavedFeedIdsByUserIdAndFeedIds( invocation.getArgument(0), invocation.getArgument(1))); // ... 필요 시 추가 stub }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllPersistenceApiTest.java` around lines 69 - 93, The test stubs only two methods on the MockitoBean FeedQueryCacheAdapter (findLatestFeedsByFeedId and findSavedFeedIdsByUserIdAndFeedIds), leaving other methods returning null and risking NPEs; change the mock to delegate all calls to the real persistence adapter by configuring the FeedQueryCacheAdapter mock to use delegatesTo/preset Answers.CALLS_REAL_METHODS or otherwise delegate to persistenceAdapter (persistenceAdapter) so every call falls through to persistenceAdapter.findLatestFeedsByFeedId / persistenceAdapter.findSavedFeedIdsByUserIdAndFeedIds and avoid partial stubbing issues.src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (1)
152-164:fetchFeedIdsLatest는 사용되지 않는 데드 코드입니다. 제거하세요.
findLatestFeedsByFeedId(Line 87-109)가 inline fetch join을 활용한 통합 쿼리로 리팩토링되면서, 이전에 ID 목록만 조회하던fetchFeedIdsLatest는 더 이상 호출되지 않습니다. 클래스 내 어디에서도 사용되지 않는 private 메서드이므로 삭제해도 안전합니다.🧹 dead code 제거
- /** - * ID 목록만 최신순 커서 페이징으로 조회 - */ - private List<Long> fetchFeedIdsLatest(Long userId, Long lastPostId , int size) { - return jpaQueryFactory - .select(feed.postId) - .from(feed) - .where( - // ACTIVE 인 feed & (내가 작성한 글 or 다른 유저가 작성한 공개글) & cursorCondition - feed.userJpaEntity.userId.eq(userId).or(feed.isPublic.eq(true)), - lastPostId != null ? feed.postId.lt(lastPostId) : null - ) - .orderBy(feed.postId.desc()) - .limit(size + 1) - .fetch(); - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java` around lines 152 - 164, Remove the dead private method fetchFeedIdsLatest(Long userId, Long lastPostId, int size) from FeedQueryRepositoryImpl: locate the method definition and delete it, ensure there are no remaining references to fetchFeedIdsLatest in the class, and run a build/tests to confirm compilation; keep the refactored findLatestFeedsByFeedId intact (it replaces the old ID-list flow).src/main/java/konkuk/thip/feed/adapter/out/cache/FeedQueryCacheAdapter.java (2)
37-37:creatorId()null 시 NPE 가능성
dto.creatorId().equals(userId)—creatorId()가null을 반환하면NullPointerException이 발생합니다. 실제로 피드의 작성자 ID가 null일 가능성은 낮지만, 방어적으로Objects.equals(dto.creatorId(), userId)를 사용하면 안전합니다.🛡️ 방어적 null 처리
- .filter(dto -> dto != null && (dto.isPublic() || dto.creatorId().equals(userId))) + .filter(dto -> dto != null && (dto.isPublic() || Objects.equals(dto.creatorId(), userId)))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedQueryCacheAdapter.java` at line 37, The filter predicate in FeedQueryCacheAdapter uses dto.creatorId().equals(userId) which can throw NPE if creatorId() is null; update the lambda to perform a null-safe equality check using Objects.equals(dto.creatorId(), userId) (or invert to userId.equals(...) after null-checking userId) so the expression becomes defensive against null creatorId values while preserving the original public-or-owner logic.
14-20:FeedQueryPersistenceAdapter구체 클래스 직접 의존
FeedQueryPort인터페이스 대신 구체 구현체에 의존하고 있습니다. 두 어댑터 모두 같은 포트를 구현하기 때문에 인터페이스를 주입하면 순환/모호 문제가 발생하므로 현 구조는 이해됩니다만, 향후 DB 어댑터 구현 변경 시 이 클래스도 수정해야 하는 커플링이 존재합니다.대안으로 DB 전용 포트 인터페이스를 분리하거나,
@Qualifier를 통해 인터페이스 주입하는 방식을 고려할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedQueryCacheAdapter.java` around lines 14 - 20, FeedQueryCacheAdapter currently depends on the concrete FeedQueryPersistenceAdapter causing tight coupling; change injection to use an abstraction by either (1) introduce a DB-specific port interface (e.g., FeedQueryDbPort) implemented by FeedQueryPersistenceAdapter and inject that into FeedQueryCacheAdapter instead of the concrete class, or (2) keep the existing FeedQueryPort but mark the persistence implementation with a `@Qualifier` and inject by that qualifier into FeedQueryCacheAdapter; update the constructor/field types in FeedQueryCacheAdapter (and add the new interface or `@Qualifier` annotation to FeedQueryPersistenceAdapter) and adjust any wiring so FeedCacheHandler and FeedQueryCacheAdapter remain decoupled from the concrete persistence class.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@loadtest/feed/feed_home_scrolling_test.js`:
- Around line 1-18: Update the misleading comments to match the actual constants
and stage targets: change the top-line comment referencing "100명의 사용자" to
reflect MAX_VUS = 500 (or set MAX_VUS to 100 if you prefer to keep the comment),
and update the stage comments near the options.stages entries to correctly
describe target: 200 ("200명까지 증가") and target: MAX_VUS (500) ("500명까지 증가");
refer to the MAX_VUS constant and the options object (stages -> target) when
making the edits so comments and code remain consistent.
- Around line 47-68: The top comment is inaccurate: the for-loop (for (let i =
1; i <= 3; i++)) with the early-exit check on responseData.isLast ||
!currentCursor can result in fewer than 3 requests; update the comment to say
"최대 3번의 요청(1~3페이지) 수행" or similar to reflect that currentCursor and
responseData.isLast may end the loop early (references: currentCursor,
responseData, responseData.isLast, the for-loop).
- Around line 20-32: The setup() function currently pushes res.body into tokens
even when the request failed or returned a non-string JSON; change it to
validate each HTTP response before pushing: for each userId (1..MAX_VUS) call
http.get(BASE_URL + '/api/test/token/access?userId=' + userId), check res.status
=== 200 and that the response body contains a usable token (if the API returns
JSON like {"token":"..."}, parse JSON and extract the token field); only push
the extracted token string into tokens when validation succeeds, otherwise
log/throw or skip pushing so failed responses (non-200 or network errors) are
not stored; ensure callers expecting "Authorization: Bearer <token>" receive the
raw token string to format the header later.
In
`@src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheWarmupListener.java`:
- Around line 38-40: The catch block in FeedCacheWarmupListener (inside the
method handling cache warmup) logs only e.getMessage(), losing the stack trace;
change the log.error call to pass the exception object as the second parameter
(e.g., use log.error("캐시 워밍업 중 오류가 발생했습니다", e)) so the full stack trace is
recorded for FeedCacheWarmupListener's error handling.
In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandler.java`:
- Around line 33-36: The current markAsDeleted(Long feedId) only writes a null
into the "feedDetail" cache and does not remove the deleted ID from the
"feedIdTop" cache, so FeedCacheEventListener.handleFeedDeletedEvent leaving it
causes stale IDs to remain; update markAsDeleted to (1) keep the `@CachePut` for
"feedDetail" but also remove the feedId from the top-ID cache (feedIdTop) —
either by invoking a cache operation to evict that id from feedIdTop or by
loading, filtering and writing back the updated top-ID list; locate
markAsDeleted and modify it to perform the additional eviction/update to
feedIdTop so deleted IDs are not returned later.
- Around line 38-57: The read-modify-write in updateTopIdsWithNewId (and
likewise in refreshCacheAfterBulkDelete) needs cache-key-level synchronization
to avoid lost updates: introduce a per-cache-key lock (e.g., a
ConcurrentHashMap<String, ReentrantLock> or similar) and acquire the lock for
the computed cacheKey ("top" + DEFAULT_CACHE_SIZE) before calling cache.get,
mutating the List, and cache.put, ensuring the lock is released in a finally
block; apply the same locking pattern to refreshCacheAfterBulkDelete so both
methods serialize modifications to the same cache key and avoid race conditions
with feedJpaRepository-backed fallback reads.
In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedQueryCacheAdapter.java`:
- Around line 31-44: The code filters topIds using
feedCacheHandler.getFeedDetail and silently drops items when getFeedDetail
returns null, then returns CursorBasedList.of(...) if feedQueryDtos.size() >=
size, causing permanently missing feeds; update the logic in the block handling
topIds/lastPostId (the stream that builds feedQueryDtos) to detect any
cache-miss (null from feedCacheHandler.getFeedDetail) and on any miss either (A)
perform a DB fallback for the whole request or (B) fetch only the missing
FeedQueryDto(s) from the DB to fill feedQueryDtos before applying .limit(size +
1) so that CursorBasedList.of(...) is only returned when the result is truly
complete; ensure to reference feedCacheHandler.getFeedDetail, FeedQueryDto,
topIds, size, and CursorBasedList.of when locating and changing the code.
In
`@src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java`:
- Around line 87-109: The queries in findLatestFeedsByFeedId and
findFeedDetailById are missing the explicit active-status filter so soft-deleted
feeds can be returned (the BaseJpaEntity `@Filter` isn't enabled here); update
both methods to include feed.status.eq(ACTIVE) in their where clauses (same
approach used in findTopFeedIds) so only feeds with ACTIVE status are returned.
In `@src/main/java/konkuk/thip/user/application/service/UserDeleteService.java`:
- Line 84: deletedFeedIds can be null if
feedCommandPort.deleteAllFeedByUserId(userId) returns null, which would pass a
null into the UserWithdrawnEvent and cause NPEs in FeedCacheEventListener when
iterating; after calling feedCommandPort.deleteAllFeedByUserId(userId) in
UserDeleteService, defensively replace a null result with
Collections.emptyList() (or use
Optional.ofNullable(...).orElse(Collections.emptyList())) before constructing
UserWithdrawnEvent, or alternatively ensure the FeedCommandPort implementation
always returns Collections.emptyList() instead of null.
In
`@src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllPersistenceApiTest.java`:
- Around line 131-132: 이 테스트의 주석이 실제 정렬 로직과 불일치합니다: Basic
전략(feed.show.strategy=basic)은 팔로잉 우선순위가 아니라 최신순(postId desc) 정렬이므로 "// 1순위: 팔로잉
글 f2"를 수정하세요; 예를 들어 BasicFeedShowAllPersistenceApiTest의 해당 assertion
라인(.andExpect(jsonPath("$.data.feedList[0].feedId",
is(f2.getPostId().intValue()))) 및 관련 f1/f2 변수 참조를 찾아 주석을 "최신 글 f2 (postId가 더 큼)"
또는 유사한 설명으로 바꿔 혼동을 제거하십시오.
In `@src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java`:
- Around line 315-361: The test createFeed_shouldRefreshCache assumes
synchronous cache updates; change it to poll the caches with a short retry loop
and timeout after committing (replace the immediate checks after
TestTransaction.flagForCommit() / TestTransaction.end() with a loop that sleeps
briefly and re-fetches both cacheManager.getCache("feedDetail") -> get(feedId,
...) and cacheManager.getCache("feedIdTop") -> get("top100", List.class) until
the expected values appear or a timeout is reached; if timeout expires, fail the
test with the same assertions to preserve diagnostics.
In `@src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java`:
- Around line 109-131: The test deleteFeed_shouldPutNullInDetailCache asserts
the detail cache immediately after committing but cache-refresh is asynchronous;
modify the test to poll the feedDetail cache for up to a short timeout (e.g.,
1–2s) with a small sleep/retry interval until detailCache.get(feedId) returns
null or timeout expires, using feedCacheHandler/cacheManager to read the cache
and failing the test if timeout is reached; keep existing TestTransaction commit
but replace the direct assertThat(cachedObject).isNull() with this retry loop to
make the assertion robust to async cache updates.
In `@src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java`:
- Around line 176-210: The test updateFeed_shouldRefreshDetailCache assumes
synchronous cache refresh; change it to retry/wait for the updated cache value
instead of asserting immediately after TestTransaction.end(): after performing
the patch and committing (TestTransaction.flagForCommit()/end()), poll the
feedDetail cache obtained via cacheManager.getCache("feedDetail") (or call
feedCacheHandler.getFeedDetail(feedId) if that triggers load) in a short loop
with a small sleep and overall timeout (or use Awaitility) until the cached
FeedQueryDto.contentBody() equals "캐시 테스트용", then assert; fail if timeout
elapses.
In
`@src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java`:
- Around line 107-122: The comments above the assertions in
FollowingPriorityFeedShowAllApiTest are reversed: the comment "// 1순위: 팔로잉 글 f2"
is placed over assertions for feedList[1] (which assert f1), and "// 2순위: 내 글
f1" is placed over assertions for feedList[0] (which assert f2); update the
comments to match the assertions (either swap the two comment lines or rewrite
them so "// 1순위: 팔로잉 글 f2" sits above feedList[0] assertions and "// 2순위: 내 글
f1" above feedList[1] assertions) so the comments align with the assertions in
FollowingPriorityFeedShowAllApiTest.
In `@src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java`:
- Around line 428-488: The test deleteUser_shouldRefreshFeedCache assumes
synchronous cache invalidation; add a short retry/wait loop after
TestTransaction.flagForCommit()/TestTransaction.end() and before accessing
cacheManager.getCache(...) to poll until the expected cache state appears (or
timeout) to account for asynchronous event processing. Specifically, after
committing the transaction (TestTransaction.flagForCommit();
TestTransaction.end();) call feedCacheHandler.getTopIds() or read from
cacheManager.getCache("feedIdTop") in a small loop (sleep ~50-200ms between
attempts) and re-evaluate assertions (topIds and detailCache reads) until they
match the expected post-deletion state or a max wait is reached, failing the
test if timed out; update assertions to use the final polled values. Ensure you
reference the existing symbols: deleteUser_shouldRefreshFeedCache,
TestTransaction.flagForCommit, TestTransaction.end, feedCacheHandler.getTopIds,
cacheManager.getCache("feedIdTop"), and cacheManager.getCache("feedDetail").
---
Outside diff comments:
In
`@src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java`:
- Around line 103-122: In deleteAllFeedByUserId (FeedCommandPersistenceAdapter)
replace the null early-return with returning an empty immutable list (e.g.,
List.of() or Collections.emptyList()) so callers like UserDeleteService can
safely iterate without NPE; also remove the duplicated comment above feedIds
retrieval so the method has a single correct comment for step 1.
---
Duplicate comments:
In `@src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListener.java`:
- Around line 25-28: handleFeedDeletedEvent only calls
feedCacheHandler.markAsDeleted which leaves the feedIdTop cache stale; after
calling markAsDeleted you must also update the top-IDs cache—add a call like
feedCacheHandler.removeFromTopIds(event.feedId()) (or
feedCacheHandler.refreshTopIds()/updateTopIdsAfterDeletion(event.feedId())) and
implement that method in FeedCacheHandler to remove the deleted feed ID from the
top IDs cache and persist the updated top list.
---
Nitpick comments:
In `@loadtest/feed/feed_home_scrolling_test.js`:
- Around line 34-43: The test does not handle a missing/invalid token (token
from data.tokens[vuIdx]) so a VU may send unauthorized requests; before building
params in the default exported function, check if token is null/undefined and
bail out early: detect token using the existing vuIdx/token variables and either
abort the VU scenario (throw or return) or call the appropriate k6 stop/sleep to
avoid sending requests, and log the failure for visibility; update any logic
that uses params/headers (the params object and Authorization header
construction) to only run when token is present.
- Around line 14-17: 현재 thresholds 객체의 http_req_duration: ['p(95)<80'] 임계값은 500
VU 시나리오에서 과도하게 엄격해 테스트가 항상 실패할 수 있으니, feed_home_scrolling_test.js의
thresholds.http_req_duration 설정을 현실적인 값으로 완화하거나(예: p(95)<200ms 또는 환경변수로 조정 가능하게)
임계값을 제거해 먼저 베이스라인을 측정하도록 변경하세요; 수정할 위치는 파일의 thresholds 블록(키명: http_req_duration,
http_req_failed)이며 필요하면 환경변수 기반 구성(예: PROCESS_ENV 또는 옵션으로 전달)으로 만들고 문서에 새로운 기본값을
기록해 주세요.
In `@src/main/java/konkuk/thip/config/cache/CacheConfig.java`:
- Around line 33-43: The method caffeineBuilder returns a built
Cache<Object,Object> but its name implies it returns a Caffeine builder; rename
the method caffeineBuilder to a name that reflects it returns a Cache (e.g.,
buildCache, createCache, or cacheForType) and update all callers accordingly;
ensure the signature remains private Cache<Object,Object> caffeineBuilder(...) →
private Cache<Object,Object> buildCache(...) (or chosen name) and keep the
internal logic (Caffeine.newBuilder()... .build()) unchanged so callers receive
the constructed Cache instance.
- Around line 22-27: 현재 CacheConfig의 캐시 리스트 생성 코드에서
Arrays.stream(...).collect(Collectors.toList())를 사용하고 있는데 Java 16+ 환경에서는 더 간결하고
불변 리스트를 반환하는 Stream.toList()로 교체하세요: CacheConfig에서
Arrays.stream(CacheType.values()).map(...).toList()로 바꾸고 불필요해진 import
java.util.stream.Collectors를 제거하며 caffeineBuilder 및 CacheType.getCacheName 참조는
그대로 유지해 주세요.
In `@src/main/java/konkuk/thip/config/cache/CacheType.java`:
- Around line 9-13: FEED_ID_TOP의 주석과 실제 설정(maximumSize=1)이 불일치합니다; 주석을 수정하여 의도한
설계를 명확히 하세요 — 만약 설계가 "각 캐시 엔트리에 최신 100개 ID 리스트를 담는 단일 엔트리"라면 주석을 "최신 100개 ID를
하나의 캐시 엔트리에 저장"처럼 명확히 바꾸고, 반대로 의도가 "최신 피드 ID 100개를 각각의 캐시 엔트리로 보관"이라면
FEED_ID_TOP의 maximumSize를 100으로 변경하세요; 관련 식별자는 FEED_ID_TOP과 해당 maximumSize
설정입니다.
- Around line 20-22: The field expireAfterWrite in CacheType currently uses int
with 0 meaning "no expiry", which is unclear; change expireAfterWrite to an
explicit type (e.g., Optional<Long> / OptionalInt or java.time.Duration /
`@Nullable` Integer) or introduce a named sentinel constant (e.g., NO_EXPIRE = -1)
and update CacheType's constructor, getters and any code that reads
expireAfterWrite (including CacheConfig consumer code) to use the new
representation and treat Optional.empty()/Duration.ZERO or NO_EXPIRE
consistently as "no expiry"; also add a short javadoc on
CacheType.expireAfterWrite to document the meaning and update any callers (e.g.,
where CacheType.expireAfterWrite is checked) to handle the new semantics.
In `@src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListener.java`:
- Around line 19-40: Wrap each transactional event handler
(handleFeedCreatedEvent, handleFeedDeletedEvent, handleFeedUpdatedEvent,
handleUserWithdrawn) in a try-catch that catches Exception and logs an error
with contextual identifiers (e.g., event.feedId() or event.deletedFeedIds()) and
the exception stacktrace, then swallow or handle the exception so it does not
propagate; use the class logger (SLF4J org.slf4j.Logger) to emit a clear message
per handler indicating which operation failed (updateTopIdsWithNewId,
getFeedDetail, markAsDeleted, evictFeedDetailIfPresent,
refreshCacheAfterBulkDelete) to aid production tracing.
In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandler.java`:
- Around line 23-26: Extract a single cache-key constant (e.g., CACHE_KEY_TOP =
"top" + DEFAULT_CACHE_SIZE) in FeedCacheHandler and use that constant everywhere
instead of hard-coded SpEL or string concatenation: update the `@Cacheable` on
getTopIds() to reference the constant (or replace the annotation with a
programmatic cache lookup using CacheManager and the same constant) and change
updateTopIdsWithNewId() to use the same CACHE_KEY_TOP when reading/writing via
CacheManager; ensure all usages (getTopIds, updateTopIdsWithNewId,
DEFAULT_CACHE_SIZE references) are unified to avoid key drift.
In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedQueryCacheAdapter.java`:
- Line 37: The filter predicate in FeedQueryCacheAdapter uses
dto.creatorId().equals(userId) which can throw NPE if creatorId() is null;
update the lambda to perform a null-safe equality check using
Objects.equals(dto.creatorId(), userId) (or invert to userId.equals(...) after
null-checking userId) so the expression becomes defensive against null creatorId
values while preserving the original public-or-owner logic.
- Around line 14-20: FeedQueryCacheAdapter currently depends on the concrete
FeedQueryPersistenceAdapter causing tight coupling; change injection to use an
abstraction by either (1) introduce a DB-specific port interface (e.g.,
FeedQueryDbPort) implemented by FeedQueryPersistenceAdapter and inject that into
FeedQueryCacheAdapter instead of the concrete class, or (2) keep the existing
FeedQueryPort but mark the persistence implementation with a `@Qualifier` and
inject by that qualifier into FeedQueryCacheAdapter; update the
constructor/field types in FeedQueryCacheAdapter (and add the new interface or
`@Qualifier` annotation to FeedQueryPersistenceAdapter) and adjust any wiring so
FeedCacheHandler and FeedQueryCacheAdapter remain decoupled from the concrete
persistence class.
In `@src/main/java/konkuk/thip/feed/adapter/out/event/dto/FeedUpdatedEvent.java`:
- Around line 1-8: Remove the leading blank line before the package declaration
so the file starts immediately with "package
konkuk.thip.feed.adapter.out.event.dto;" to match the style of the other event
DTOs (FeedCreatedEvent, FeedDeletedEvent); update the top of the
FeedUpdatedEvent record to have no empty line above the package statement.
In
`@src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java`:
- Line 30: Change the signature of findFeedDetailById to return
Optional<FeedQueryDto> instead of nullable FeedQueryDto and update its
implementation to wrap fetchOne() result with Optional.ofNullable; add import
java.util.Optional and adjust all callers (FeedCacheHandler,
FeedQueryPersistenceAdapter, etc.) to handle the Optional (e.g., orElse /
orElseThrow / isPresent) instead of assuming non-null, ensuring no direct null
usage or caching of null values.
In
`@src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java`:
- Around line 152-164: Remove the dead private method fetchFeedIdsLatest(Long
userId, Long lastPostId, int size) from FeedQueryRepositoryImpl: locate the
method definition and delete it, ensure there are no remaining references to
fetchFeedIdsLatest in the class, and run a build/tests to confirm compilation;
keep the refactored findLatestFeedsByFeedId intact (it replaces the old ID-list
flow).
In `@src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java`:
- Line 29: The interface method FeedCommandPort.deleteAllFeedByUserId currently
allows a null return implicitly; either annotate the contract explicitly or make
it non-null. Update FeedCommandPort.deleteAllFeedByUserId to declare a non-null
contract (add your project’s `@NotNull/`@NonNull annotation or Javadoc stating
non-null) and ensure FeedCommandPersistenceAdapter.deleteAllFeedByUserId
implementation returns an empty List (e.g., Collections.emptyList()) instead of
null so callers can rely on a non-null List<Long>.
In `@src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java`:
- Line 13: 인터페이스 FeedQueryPort의 메서드명 getFeedDetail이 다른 메서드들(findXxx)과 일관성이 없으니
getFeedDetail을 findFeedDetail로 변경하고 관련 구현체와 호출부 모두에서 메서드명을 동일하게 갱신하세요; 변경 대상
식별자: FeedQueryPort 인터페이스의 getFeedDetail, 반환형 FeedQueryDto, 그리고 이를 구현하는 클래스들 및
서비스/컨트롤러에서 호출하는 모든 참조를 찾아 findFeedDetail로 리네임하고 컴파일/테스트 통과 여부를 확인하면 됩니다.
In
`@src/main/java/konkuk/thip/user/adapter/out/event/dto/UserWithdrawnEvent.java`:
- Around line 5-8: The factory method in UserWithdrawnEvent is named of(), which
is inconsistent with other event DTOs like FeedCreatedEvent.from() and
FeedDeletedEvent.from(); rename the static method UserWithdrawnEvent.of(Long
userId, List<Long> deletedFeedIds) to UserWithdrawnEvent.from(Long userId,
List<Long> deletedFeedIds) and update any call sites to use
UserWithdrawnEvent.from(...) to keep factory naming consistent across the
codebase.
In
`@src/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListenerTest.java`:
- Around line 71-90: Add a new test in FeedCacheEventListenerTest to cover the
branch where evictFeedDetailIfPresent returns false: create a FeedUpdatedEvent
(use FeedUpdatedEvent.from(feedId)), stub
feedCacheHandler.evictFeedDetailIfPresent(feedId) to return false, publish the
event with publisher.publishEvent(event) and commit via
TestTransaction.flagForCommit()/TestTransaction.end(), then verify
evictFeedDetailIfPresent was called once and that
feedCacheHandler.getFeedDetail(feedId) was never called; this ensures the
handleFeedUpdatedEvent path where the cache miss skips getFeedDetail is
exercised.
In
`@src/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheWarmupListenerTest.java`:
- Around line 24-29: The test class FeedCacheWarmupListenerTest is annotated
with `@SpringBootTest` (loads full Spring context) but its `@DisplayName` labels it
as "[단위]" (unit); update the `@DisplayName` to indicate it's an integration test
(e.g., replace "[단위]" with "[통합]" or "[통합] FeedCacheWarmupListener 통합 테스트") so
the test classification matches the use of `@SpringBootTest` and avoids confusion.
In
`@src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllPersistenceApiTest.java`:
- Around line 69-93: The test stubs only two methods on the MockitoBean
FeedQueryCacheAdapter (findLatestFeedsByFeedId and
findSavedFeedIdsByUserIdAndFeedIds), leaving other methods returning null and
risking NPEs; change the mock to delegate all calls to the real persistence
adapter by configuring the FeedQueryCacheAdapter mock to use delegatesTo/preset
Answers.CALLS_REAL_METHODS or otherwise delegate to persistenceAdapter
(persistenceAdapter) so every call falls through to
persistenceAdapter.findLatestFeedsByFeedId /
persistenceAdapter.findSavedFeedIdsByUserIdAndFeedIds and avoid partial stubbing
issues.
In
`@src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java`:
- Around line 21-22: Remove leftover unused timestamp-manipulation artifacts in
FollowingPriorityFeedShowAllApiTest: delete the unused imports for JdbcTemplate,
java.sql.Timestamp, and java.time.LocalDateTime, and remove the injected
JdbcTemplate field (and any related unused fields) from the test class so the
compiler/IDE no longer reports unused imports/fields; search for symbols
JdbcTemplate, Timestamp, and LocalDateTime in
FollowingPriorityFeedShowAllApiTest to locate and delete the dead code.
- Around line 247-251: The test hardcodes the cursor string "1|"+f3.getPostId()
which will break if the Cursor encoding changes; update the test to build the
cursor using the Cursor/CursorUtils API instead of manual concatenation (e.g.,
call the existing Cursor factory/encoder used by
FeedQueryPersistenceAdapter.findFeedsByFollowingPriority or add a small
test-only factory method on Cursor/CursorUtils and use that to produce
nextCursor from List.of("1", f3.getPostId().toString())); replace the manual
nextCursor construction in FollowingPriorityFeedShowAllApiTest with that encoded
value so the test remains correct if the cursor format changes.
In `@src/test/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandlerTest.java`:
- Around line 102-119: The test is missing a stub for the "feedDetail" cache so
evictFeeds and warmUpFeedDetails paths inside refreshCacheAfterBulkDelete are
not exercised; update the test to mock cacheManager.getCache("feedDetail") to
return a mocked detailCache (e.g., detailCache) and set expected behaviors on
detailCache (such as allowing evict calls and any get/put used by
warmUpFeedDetails) so you can verify detailCache.evict(...) and any
detailCache.put(...) interactions when
handler.refreshCacheAfterBulkDelete(deleted) is invoked.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (37)
build.gradleloadtest/feed/feed_home_scrolling_test.jssrc/main/java/konkuk/thip/ThipServerApplication.javasrc/main/java/konkuk/thip/config/cache/CacheConfig.javasrc/main/java/konkuk/thip/config/cache/CacheType.javasrc/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListener.javasrc/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheWarmupListener.javasrc/main/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandler.javasrc/main/java/konkuk/thip/feed/adapter/out/cache/FeedQueryCacheAdapter.javasrc/main/java/konkuk/thip/feed/adapter/out/event/dto/FeedCreatedEvent.javasrc/main/java/konkuk/thip/feed/adapter/out/event/dto/FeedDeletedEvent.javasrc/main/java/konkuk/thip/feed/adapter/out/event/dto/FeedUpdatedEvent.javasrc/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.javasrc/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.javasrc/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.javasrc/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.javasrc/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.javasrc/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.javasrc/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.javasrc/main/java/konkuk/thip/feed/application/service/FeedCreateService.javasrc/main/java/konkuk/thip/feed/application/service/FeedDeleteService.javasrc/main/java/konkuk/thip/feed/application/service/FeedUpdateService.javasrc/main/java/konkuk/thip/user/adapter/out/event/dto/UserWithdrawnEvent.javasrc/main/java/konkuk/thip/user/application/service/UserDeleteService.javasrc/test/java/konkuk/thip/common/util/TestEntityFactory.javasrc/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheEventListenerTest.javasrc/test/java/konkuk/thip/feed/adapter/in/event/FeedCacheWarmupListenerTest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllCacheApiTest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllPersistenceApiTest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.javasrc/test/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandlerTest.javasrc/test/java/konkuk/thip/feed/adapter/out/cache/FeedQueryCacheAdapterTest.javasrc/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java
| //100명의 사용자가 동시에 각자 다른 속도로 홈 피드를 1페이지부터 3페이지까지 탐색하는 시나리오 | ||
| import http from 'k6/http'; | ||
| import { sleep,check } from 'k6'; | ||
|
|
||
| const BASE_URL = 'http://localhost:8000'; | ||
| const MAX_VUS = 500; | ||
|
|
||
| export let options = { | ||
| stages: [ | ||
| { duration: '20s', target: 200 }, // 20초 동안 100명까지 증가 | ||
| { duration: '40s', target: MAX_VUS }, // 40초 동안 300명까지 증가하며 피크 부하 | ||
| { duration: '20s', target: 0 }, // 20초 동안 0명으로 하강 | ||
| ], | ||
| thresholds: { | ||
| http_req_duration: ['p(95)<80'], | ||
| http_req_failed: ['rate<0.01'], | ||
| }, | ||
| }; |
There was a problem hiding this comment.
주석과 실제 설정값 불일치
주석에 기술된 사용자 수와 실제 설정값이 세 곳에서 맞지 않습니다.
| 위치 | 주석 | 실제 값 |
|---|---|---|
| Line 1 | "100명의 사용자" | MAX_VUS = 500 |
| Line 10 | "100명까지 증가" | target: 200 |
| Line 11 | "300명까지 증가" | target: MAX_VUS (500) |
✏️ 수정 제안
-//100명의 사용자가 동시에 각자 다른 속도로 홈 피드를 1페이지부터 3페이지까지 탐색하는 시나리오
+//최대 500명의 사용자가 동시에 각자 다른 속도로 홈 피드를 1페이지부터 3페이지까지 탐색하는 시나리오
...
- { duration: '20s', target: 200 }, // 20초 동안 100명까지 증가
- { duration: '40s', target: MAX_VUS }, // 40초 동안 300명까지 증가하며 피크 부하
+ { duration: '20s', target: 200 }, // 20초 동안 200명까지 증가
+ { duration: '40s', target: MAX_VUS }, // 40초 동안 500명까지 증가하며 피크 부하🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@loadtest/feed/feed_home_scrolling_test.js` around lines 1 - 18, Update the
misleading comments to match the actual constants and stage targets: change the
top-line comment referencing "100명의 사용자" to reflect MAX_VUS = 500 (or set
MAX_VUS to 100 if you prefer to keep the comment), and update the stage comments
near the options.stages entries to correctly describe target: 200 ("200명까지 증가")
and target: MAX_VUS (500) ("500명까지 증가"); refer to the MAX_VUS constant and the
options object (stages -> target) when making the edits so comments and code
remain consistent.
| // 테스트 전 사용자 별 토큰 발급 | ||
| export function setup() { | ||
| let tokens = []; | ||
|
|
||
| // 유저 ID에 대해 토큰을 미리 발급 | ||
| for (let userId = 1; userId <= MAX_VUS; userId++) { | ||
| const res = http.get(`${BASE_URL}/api/test/token/access?userId=${userId}`); | ||
| check(res, { 'token received': (r) => r.status === 200 && r.body.length > 0 }); | ||
| tokens.push(res.body); | ||
| } | ||
|
|
||
| return { tokens: tokens }; | ||
| } |
There was a problem hiding this comment.
setup()에서 토큰 발급 실패 시에도 tokens에 그대로 push됩니다
check()는 k6 메트릭 기록용이며 실행을 중단하거나 push를 건너뛰지 않습니다. 토큰 요청이 실패(비-200 또는 네트워크 오류)할 경우 오류 응답 본문이 토큰으로 사용되어, 해당 VU의 모든 요청이 401로 실패하고 테스트 결과가 오염됩니다.
또한 res.body를 그대로 사용하는데, API가 JSON({"token":"..."} 형태)을 반환한다면 Authorization: Bearer {...} 형태가 되어 인증이 깨집니다. API 응답 형식을 확인하고, JSON이라면 파싱이 필요합니다.
🛡️ 수정 제안
export function setup() {
let tokens = [];
for (let userId = 1; userId <= MAX_VUS; userId++) {
const res = http.get(`${BASE_URL}/api/test/token/access?userId=${userId}`);
- check(res, { 'token received': (r) => r.status === 200 && r.body.length > 0 });
- tokens.push(res.body);
+ const ok = check(res, { 'token received': (r) => r.status === 200 && r.body.length > 0 });
+ if (!ok) {
+ console.error(`Failed to get token for userId=${userId}: status=${res.status}`);
+ tokens.push(null);
+ continue;
+ }
+ // API가 JSON을 반환하는 경우: res.json().token 등으로 파싱 필요
+ tokens.push(res.body);
}
return { tokens: tokens };
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@loadtest/feed/feed_home_scrolling_test.js` around lines 20 - 32, The setup()
function currently pushes res.body into tokens even when the request failed or
returned a non-string JSON; change it to validate each HTTP response before
pushing: for each userId (1..MAX_VUS) call http.get(BASE_URL +
'/api/test/token/access?userId=' + userId), check res.status === 200 and that
the response body contains a usable token (if the API returns JSON like
{"token":"..."}, parse JSON and extract the token field); only push the
extracted token string into tokens when validation succeeds, otherwise log/throw
or skip pushing so failed responses (non-200 or network errors) are not stored;
ensure callers expecting "Authorization: Bearer <token>" receive the raw token
string to format the header later.
| // 모든 유저가 정확히 3번의 요청(1~3페이지) 수행 | ||
| for (let i = 1; i <= 3; i++) { | ||
| let url = `${BASE_URL}/feeds`; | ||
| if (currentCursor) { | ||
| url += `?cursor=${encodeURIComponent(currentCursor)}`; | ||
| } | ||
|
|
||
| let res = http.get(url, params); | ||
|
|
||
| if (check(res, { 'status is 200': (r) => r.status === 200 })) { | ||
| const responseData = res.json().data; | ||
| currentCursor = responseData.nextCursor; | ||
|
|
||
| // 만약 3페이지가 되기 전에 데이터가 끝났다면 루프 탈출 | ||
| if (responseData.isLast || !currentCursor) break; | ||
| } else { | ||
| // 요청 실패 시 해당 유저 시나리오 중단 | ||
| break; | ||
| } | ||
|
|
||
| sleep(Math.random() * 1 + 0.5); // 0.5초 ~ 1.5초 사이 랜덤하게 쉬기 | ||
| } |
There was a problem hiding this comment.
"정확히 3번" 주석이 실제 동작과 다릅니다
Line 47의 주석은 "모든 유저가 정확히 3번의 요청(1~3페이지) 수행"이라고 명시하지만, Line 61의 early break 조건(isLast || !currentCursor)으로 인해 3회 미만으로 종료될 수 있습니다.
✏️ 수정 제안
- // 모든 유저가 정확히 3번의 요청(1~3페이지) 수행
+ // 최대 3번의 요청(1~3페이지) 수행, 데이터가 끝나면 조기 종료🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@loadtest/feed/feed_home_scrolling_test.js` around lines 47 - 68, The top
comment is inaccurate: the for-loop (for (let i = 1; i <= 3; i++)) with the
early-exit check on responseData.isLast || !currentCursor can result in fewer
than 3 requests; update the comment to say "최대 3번의 요청(1~3페이지) 수행" or similar to
reflect that currentCursor and responseData.isLast may end the loop early
(references: currentCursor, responseData, responseData.isLast, the for-loop).
| } catch (Exception e) { | ||
| log.error("캐시 워밍업 중 오류가 발생했습니다: {}", e.getMessage()); | ||
| } |
There was a problem hiding this comment.
예외 로깅 시 스택 트레이스가 누락됩니다.
e.getMessage()만 기록하면 예외의 근본 원인을 파악하기 어렵습니다. 두 번째 인자로 예외 객체를 전달해 주세요.
수정 제안
- log.error("캐시 워밍업 중 오류가 발생했습니다: {}", e.getMessage());
+ log.error("캐시 워밍업 중 오류가 발생했습니다: {}", e.getMessage(), e);📝 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.
| } catch (Exception e) { | |
| log.error("캐시 워밍업 중 오류가 발생했습니다: {}", e.getMessage()); | |
| } | |
| } catch (Exception e) { | |
| log.error("캐시 워밍업 중 오류가 발생했습니다: {}", e.getMessage(), e); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/konkuk/thip/feed/adapter/in/event/FeedCacheWarmupListener.java`
around lines 38 - 40, The catch block in FeedCacheWarmupListener (inside the
method handling cache warmup) logs only e.getMessage(), losing the stack trace;
change the log.error call to pass the exception object as the second parameter
(e.g., use log.error("캐시 워밍업 중 오류가 발생했습니다", e)) so the full stack trace is
recorded for FeedCacheWarmupListener's error handling.
| @CachePut(cacheNames = "feedDetail", key = "#feedId") | ||
| public FeedQueryDto markAsDeleted(Long feedId) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
markAsDeleted는 feedDetail을 null로 캐싱하지만, 상위 ID 목록(feedIdTop)에서 해당 ID를 제거하지 않습니다.
FeedCacheEventListener.handleFeedDeletedEvent가 markAsDeleted만 호출하므로, 삭제된 피드의 ID가 feedIdTop 캐시에 계속 남아있게 됩니다. 이후 조회 시 해당 ID의 상세 데이터가 null로 반환되어, 클라이언트에 빈 피드가 노출되거나 NPE가 발생할 수 있습니다.
삭제 시 상위 ID 목록에서도 해당 ID를 제거하는 로직이 필요합니다.
제안: 삭제 시 top IDs에서도 제거
- `@CachePut`(cacheNames = "feedDetail", key = "#feedId")
- public FeedQueryDto markAsDeleted(Long feedId) {
- return null;
- }
+ public void markAsDeleted(Long feedId) {
+ // 1. feedDetail 캐시에서 제거
+ evictFeedDetailIfPresent(feedId);
+
+ // 2. feedIdTop 캐시에서 해당 ID 제거
+ Cache cache = cacheManager.getCache("feedIdTop");
+ if (cache == null) return;
+
+ String cacheKey = "top" + DEFAULT_CACHE_SIZE;
+ List<Long> currentIds = cache.get(cacheKey, List.class);
+ if (currentIds != null && currentIds.contains(feedId)) {
+ List<Long> updatedIds = new ArrayList<>(currentIds);
+ updatedIds.remove(feedId);
+ cache.put(cacheKey, updatedIds);
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/konkuk/thip/feed/adapter/out/cache/FeedCacheHandler.java`
around lines 33 - 36, The current markAsDeleted(Long feedId) only writes a null
into the "feedDetail" cache and does not remove the deleted ID from the
"feedIdTop" cache, so FeedCacheEventListener.handleFeedDeletedEvent leaving it
causes stale IDs to remain; update markAsDeleted to (1) keep the `@CachePut` for
"feedDetail" but also remove the feedId from the top-ID cache (feedIdTop) —
either by invoking a cache operation to evict that id from feedIdTop or by
loading, filtering and writing back the updated top-ID list; locate
markAsDeleted and modify it to perform the additional eviction/update to
feedIdTop so deleted IDs are not returned later.
| @Test | ||
| @DisplayName("피드 생성 후 캐시(feedIdTop, feedDetail)가 갱신된다.") | ||
| void createFeed_shouldRefreshCache() throws Exception { | ||
|
|
||
| // given | ||
| bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); | ||
|
|
||
| Map<String, Object> request = new HashMap<>(); | ||
| request.put("isbn", "9788954682152"); | ||
| request.put("contentBody", "캐시 갱신 테스트"); | ||
| request.put("isPublic", true); | ||
| request.put("tagList", List.of()); | ||
|
|
||
| // when | ||
| ResultActions result = mockMvc.perform(post("/feeds") | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .requestAttr("userId", user.getUserId()) | ||
| .content(objectMapper.writeValueAsString(request))); | ||
|
|
||
| result.andExpect(status().isOk()); | ||
|
|
||
| String json = result.andReturn().getResponse().getContentAsString(); | ||
| Long feedId = objectMapper.readTree(json) | ||
| .path("data") | ||
| .path("feedId") | ||
| .asLong(); | ||
|
|
||
| // 캐시 갱신 확인하기위해 강제 커밋 | ||
| TestTransaction.flagForCommit(); | ||
| TestTransaction.end(); | ||
|
|
||
| // then | ||
| // 1. feedDetail 캐시 확인 | ||
| Cache detailCache = cacheManager.getCache("feedDetail"); | ||
| assertThat(detailCache).isNotNull(); | ||
| Object cachedDetail = detailCache.get(feedId, Object.class); | ||
| assertThat(cachedDetail).isNotNull(); | ||
|
|
||
| // 2. feedIdTop 캐시 확인 | ||
| Cache topCache = cacheManager.getCache("feedIdTop"); | ||
| assertThat(topCache).isNotNull(); | ||
|
|
||
| List<Long> topIds = topCache.get("top100", List.class); | ||
|
|
||
| assertThat(topIds).isNotNull(); | ||
| assertThat(topIds).contains(feedId); | ||
| } |
There was a problem hiding this comment.
캐시 갱신이 비동기라면 대기/재시도 보강이 필요합니다.
커밋 직후 즉시 캐시를 단정하고 있어, 이벤트 리스너가 비동기로 처리되면 간헐적으로 실패할 수 있습니다. 캐시가 갱신될 때까지 짧게 재시도/대기하는 방식으로 안정화해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java` around
lines 315 - 361, The test createFeed_shouldRefreshCache assumes synchronous
cache updates; change it to poll the caches with a short retry loop and timeout
after committing (replace the immediate checks after
TestTransaction.flagForCommit() / TestTransaction.end() with a loop that sleeps
briefly and re-fetches both cacheManager.getCache("feedDetail") -> get(feedId,
...) and cacheManager.getCache("feedIdTop") -> get("top100", List.class) until
the expected values appear or a timeout is reached; if timeout expires, fail the
test with the same assertions to preserve diagnostics.
| @Test | ||
| @DisplayName("피드 삭제 시 삭제한 피드가 캐시에 적재되어있다면 해당하는 상세 캐시에 null이 저장된다.") | ||
| void deleteFeed_shouldPutNullInDetailCache() throws Exception { | ||
| // given | ||
| Long feedId = feed.getPostId(); | ||
|
|
||
| feedCacheHandler.getFeedDetail(feedId); //캐시에 먼저 적재 | ||
|
|
||
| // when | ||
| mockMvc.perform(delete("/feeds/{feedId}", feedId) | ||
| .requestAttr("userId", user.getUserId())) | ||
| .andExpect(status().isOk()); | ||
|
|
||
| // 캐시 갱신 확인하기위해 강제 커밋 | ||
| TestTransaction.flagForCommit(); | ||
| TestTransaction.end(); | ||
|
|
||
| // then | ||
| Cache detailCache = cacheManager.getCache("feedDetail"); | ||
| Object cachedObject = detailCache.get(feedId, Object.class); | ||
|
|
||
| assertThat(cachedObject).isNull(); | ||
| } |
There was a problem hiding this comment.
캐시 갱신이 비동기라면 대기/재시도 보강이 필요합니다.
커밋 직후 바로 캐시를 단정하고 있어 비동기 갱신 시 테스트가 흔들릴 수 있습니다. 캐시 갱신 완료 시점까지 짧게 재시도/대기하도록 보강해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java` around
lines 109 - 131, The test deleteFeed_shouldPutNullInDetailCache asserts the
detail cache immediately after committing but cache-refresh is asynchronous;
modify the test to poll the feedDetail cache for up to a short timeout (e.g.,
1–2s) with a small sleep/retry interval until detailCache.get(feedId) returns
null or timeout expires, using feedCacheHandler/cacheManager to read the cache
and failing the test if timeout is reached; keep existing TestTransaction commit
but replace the direct assertThat(cachedObject).isNull() with this retry loop to
make the assertion robust to async cache updates.
| @Test | ||
| @DisplayName("피드 수정 시 수정한 피드가 캐시에 적재되어있다면 해당하는 상세 캐시가 갱신된다.") | ||
| void updateFeed_shouldRefreshDetailCache() throws Exception { | ||
|
|
||
| // given | ||
| Long feedId = feed.getPostId(); | ||
|
|
||
| feedCacheHandler.getFeedDetail(feedId); //캐시에 먼저 적재 | ||
| Map<String, Object> request = new HashMap<>(); | ||
| request.put("contentBody", "캐시 테스트용"); | ||
| request.put("isPublic", true); | ||
|
|
||
|
|
||
| // when | ||
| ResultActions result = mockMvc.perform(patch("/feeds/{feedId}", feedId) | ||
| .requestAttr("userId", user.getUserId()) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(request))); | ||
|
|
||
| result.andExpect(status().isOk()); | ||
|
|
||
| // 캐시 갱신 확인하기위해 강제 커밋 | ||
| TestTransaction.flagForCommit(); | ||
| TestTransaction.end(); | ||
|
|
||
| // then | ||
| Cache detailCache = cacheManager.getCache("feedDetail"); | ||
| assertThat(detailCache).isNotNull(); | ||
|
|
||
| Object cachedObject = detailCache.get(feedId, Object.class); | ||
| assertThat(cachedObject).isNotNull(); | ||
|
|
||
| FeedQueryDto cached = (FeedQueryDto) cachedObject; | ||
| assertThat(cached.contentBody()).isEqualTo("캐시 테스트용"); | ||
| } |
There was a problem hiding this comment.
캐시 갱신이 비동기라면 대기/재시도 보강이 필요합니다.
커밋 직후 즉시 캐시를 단정하고 있어, 비동기 갱신일 경우 테스트가 흔들릴 수 있습니다. 캐시 갱신 완료 시점까지 짧게 재시도/대기하도록 보강해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java` around
lines 176 - 210, The test updateFeed_shouldRefreshDetailCache assumes
synchronous cache refresh; change it to retry/wait for the updated cache value
instead of asserting immediately after TestTransaction.end(): after performing
the patch and committing (TestTransaction.flagForCommit()/end()), poll the
feedDetail cache obtained via cacheManager.getCache("feedDetail") (or call
feedCacheHandler.getFeedDetail(feedId) if that triggers load) in a short loop
with a small sleep and overall timeout (or use Awaitility) until the cached
FeedQueryDto.contentBody() equals "캐시 테스트용", then assert; fail if timeout
elapses.
| // 1순위: 팔로잉 글 f2 | ||
| .andExpect(jsonPath("$.data.feedList[1].feedId", is(f1.getPostId().intValue()))) | ||
| .andExpect(jsonPath("$.data.feedList[1].creatorNickname", is("me"))) | ||
| .andExpect(jsonPath("$.data.feedList[1].contentUrls", hasSize(2))) | ||
| .andExpect(jsonPath("$.data.feedList[1].likeCount", is(10))) | ||
| .andExpect(jsonPath("$.data.feedList[1].commentCount", is(5))) | ||
| .andExpect(jsonPath("$.data.feedList[1].isSaved", is(true))) | ||
| .andExpect(jsonPath("$.data.feedList[1].isLiked", is(false))) | ||
| // 2순위: 내 글 f1 | ||
| .andExpect(jsonPath("$.data.feedList[0].feedId", is(f2.getPostId().intValue()))) | ||
| .andExpect(jsonPath("$.data.feedList[0].creatorNickname", is("user1"))) | ||
| .andExpect(jsonPath("$.data.feedList[0].contentUrls", hasSize(0))) | ||
| .andExpect(jsonPath("$.data.feedList[0].likeCount", is(50))) | ||
| .andExpect(jsonPath("$.data.feedList[0].commentCount", is(10))) | ||
| .andExpect(jsonPath("$.data.feedList[0].isSaved", is(false))) | ||
| .andExpect(jsonPath("$.data.feedList[0].isLiked", is(true))); |
There was a problem hiding this comment.
주석과 어설션 블록의 순서가 뒤바뀌어 있습니다.
// 1순위: 팔로잉 글 f2 주석이 feedList[1] (f1) 블록 위에, // 2순위: 내 글 f1 주석이 feedList[0] (f2) 블록 위에 위치하고 있습니다. 어설션 자체의 논리(f2가 index 0)는 맞지만, 주석이 반대로 붙어 있어 유지보수 시 혼란을 초래합니다.
🐛 제안하는 주석 수정
- // 1순위: 팔로잉 글 f2
- .andExpect(jsonPath("$.data.feedList[1].feedId", is(f1.getPostId().intValue())))
- .andExpect(jsonPath("$.data.feedList[1].creatorNickname", is("me")))
- .andExpect(jsonPath("$.data.feedList[1].contentUrls", hasSize(2)))
- .andExpect(jsonPath("$.data.feedList[1].likeCount", is(10)))
- .andExpect(jsonPath("$.data.feedList[1].commentCount", is(5)))
- .andExpect(jsonPath("$.data.feedList[1].isSaved", is(true)))
- .andExpect(jsonPath("$.data.feedList[1].isLiked", is(false)))
- // 2순위: 내 글 f1
- .andExpect(jsonPath("$.data.feedList[0].feedId", is(f2.getPostId().intValue())))
- .andExpect(jsonPath("$.data.feedList[0].creatorNickname", is("user1")))
- .andExpect(jsonPath("$.data.feedList[0].contentUrls", hasSize(0)))
- .andExpect(jsonPath("$.data.feedList[0].likeCount", is(50)))
- .andExpect(jsonPath("$.data.feedList[0].commentCount", is(10)))
- .andExpect(jsonPath("$.data.feedList[0].isSaved", is(false)))
- .andExpect(jsonPath("$.data.feedList[0].isLiked", is(true)));
+ // 1순위: 팔로잉 글 f2 (f2가 f1보다 최신이므로 index 0)
+ .andExpect(jsonPath("$.data.feedList[0].feedId", is(f2.getPostId().intValue())))
+ .andExpect(jsonPath("$.data.feedList[0].creatorNickname", is("user1")))
+ .andExpect(jsonPath("$.data.feedList[0].contentUrls", hasSize(0)))
+ .andExpect(jsonPath("$.data.feedList[0].likeCount", is(50)))
+ .andExpect(jsonPath("$.data.feedList[0].commentCount", is(10)))
+ .andExpect(jsonPath("$.data.feedList[0].isSaved", is(false)))
+ .andExpect(jsonPath("$.data.feedList[0].isLiked", is(true)))
+ // 2순위: 내 글 f1 (f1이 더 오래됐으므로 index 1)
+ .andExpect(jsonPath("$.data.feedList[1].feedId", is(f1.getPostId().intValue())))
+ .andExpect(jsonPath("$.data.feedList[1].creatorNickname", is("me")))
+ .andExpect(jsonPath("$.data.feedList[1].contentUrls", hasSize(2)))
+ .andExpect(jsonPath("$.data.feedList[1].likeCount", is(10)))
+ .andExpect(jsonPath("$.data.feedList[1].commentCount", is(5)))
+ .andExpect(jsonPath("$.data.feedList[1].isSaved", is(true)))
+ .andExpect(jsonPath("$.data.feedList[1].isLiked", is(false)));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java`
around lines 107 - 122, The comments above the assertions in
FollowingPriorityFeedShowAllApiTest are reversed: the comment "// 1순위: 팔로잉 글 f2"
is placed over assertions for feedList[1] (which assert f1), and "// 2순위: 내 글
f1" is placed over assertions for feedList[0] (which assert f2); update the
comments to match the assertions (either swap the two comment lines or rewrite
them so "// 1순위: 팔로잉 글 f2" sits above feedList[0] assertions and "// 2순위: 내 글
f1" above feedList[1] assertions) so the comments align with the assertions in
FollowingPriorityFeedShowAllApiTest.
| @Test | ||
| @DisplayName("회원탈퇴 시 탈퇴 유저의 피드가 캐시에 적재되어있다면 해당하는 캐시들이 삭제된다.") | ||
| void deleteUser_shouldRefreshFeedCache() throws Exception { | ||
|
|
||
| // given | ||
| UserJpaEntity withdrawUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); | ||
| UserJpaEntity otherUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); | ||
| BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); | ||
|
|
||
| // 탈퇴 유저 피드 2개 | ||
| FeedJpaEntity feed1 = feedJpaRepository.save( | ||
| TestEntityFactory.createFeed(withdrawUser, book, true) | ||
| ); | ||
| FeedJpaEntity feed2 = feedJpaRepository.save( | ||
| TestEntityFactory.createFeed(withdrawUser, book, true) | ||
| ); | ||
|
|
||
| // 다른 유저 피드 1개 | ||
| FeedJpaEntity feed3 = feedJpaRepository.save( | ||
| TestEntityFactory.createFeed(otherUser, book, true) | ||
| ); | ||
|
|
||
| // 캐시 강제 적재 | ||
| feedCacheHandler.getTopIds(); | ||
| feedCacheHandler.getFeedDetail(feed1.getPostId()); | ||
| feedCacheHandler.getFeedDetail(feed2.getPostId()); | ||
| feedCacheHandler.getFeedDetail(feed3.getPostId()); | ||
|
|
||
|
|
||
| // when | ||
| String accessToken = jwtUtil.createAccessToken(withdrawUser.getUserId()); | ||
| mockMvc.perform(delete("/users") | ||
| .header("Authorization", "Bearer " + accessToken) | ||
| .contentType(MediaType.APPLICATION_JSON)) | ||
| .andExpect(status().isOk()); | ||
|
|
||
| // 캐시 갱신 확인하기위해 강제 커밋 | ||
| TestTransaction.flagForCommit(); | ||
| TestTransaction.end(); | ||
|
|
||
| // then | ||
| Cache topCache = cacheManager.getCache("feedIdTop"); | ||
| assertThat(topCache).isNotNull(); | ||
| Cache detailCache = cacheManager.getCache("feedDetail"); | ||
| assertThat(detailCache).isNotNull(); | ||
|
|
||
| List<Long> topIds = topCache.get("top100", List.class); | ||
|
|
||
| // 1. 탈퇴 유저 피드 제거 확인 | ||
| assertThat(topIds) | ||
| .doesNotContain(feed1.getPostId(), feed2.getPostId()); | ||
| // 2. 다른 유저 피드는 유지 | ||
| assertThat(topIds) | ||
| .contains(feed3.getPostId()); | ||
| // 3. detail 캐시에서 탈퇴 유저 피드만 제거 | ||
| assertThat(detailCache.get(feed1.getPostId())).isNull(); | ||
| assertThat(detailCache.get(feed2.getPostId())).isNull(); | ||
|
|
||
| // 4. 다른 유저 피드는 여전히 존재 | ||
| assertThat(detailCache.get(feed3.getPostId())).isNotNull(); | ||
| } |
There was a problem hiding this comment.
캐시 갱신이 비동기라면 대기/재시도 보강이 필요합니다.
커밋 직후 즉시 캐시를 단정하고 있어, 비동기 이벤트 처리 시 간헐적 실패 가능성이 있습니다. 캐시가 갱신될 때까지 짧게 재시도/대기하는 방식으로 안정화해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java` around
lines 428 - 488, The test deleteUser_shouldRefreshFeedCache assumes synchronous
cache invalidation; add a short retry/wait loop after
TestTransaction.flagForCommit()/TestTransaction.end() and before accessing
cacheManager.getCache(...) to poll until the expected cache state appears (or
timeout) to account for asynchronous event processing. Specifically, after
committing the transaction (TestTransaction.flagForCommit();
TestTransaction.end();) call feedCacheHandler.getTopIds() or read from
cacheManager.getCache("feedIdTop") in a small loop (sleep ~50-200ms between
attempts) and re-evaluate assertions (topIds and detailCache reads) until they
match the expected post-deletion state or a max wait is reached, failing the
test if timed out; update assertions to use the final polled values. Ensure you
reference the existing symbols: deleteUser_shouldRefreshFeedCache,
TestTransaction.flagForCommit, TestTransaction.end, feedCacheHandler.getTopIds,
cacheManager.getCache("feedIdTop"), and cacheManager.getCache("feedDetail").
#️⃣ 연관된 이슈
📝 작업 내용
게시글 조회 성능 및 시스템 안정성을 위해 전체 피드 조회 로직을 로컬 캐시 기반 이벤트 드리븐 아키텍처로 리팩토링했습니다.
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
릴리스 노트
New Features
Tests
Chores