Skip to content

Conversation

@hd0rable
Copy link
Member

@hd0rable hd0rable commented Dec 17, 2025

#️⃣ 연관된 이슈

closes #338

📝 작업 내용

게시글 좋아요 API의 동시성 문제를 해결하기 위해 기존에 적용했던 비관적 락(Pessimistic Lock) 방식을 제거하고, Redis의 원자적 연산(INCR/DECR) 을 활용하여 성능을 개선하였습니다

  • 비관적 락 제거: 단일 게시물에 요청이 몰릴 때 발생하는 DB Lock 경합 및 스레드 고갈 문제를 해결하기 위해 최초 조회 시의 X-Lock을 제거했습니다.
  • Redis 원자적 연산 도입: PostLikeCountRedisAdapter를 구현하여 좋아요 수 증감을 Redis 메모리 상에서 원자적으로 처리함으로써 응답 시간을 획기적으로 단축했습니다.
  • Write-Back 동기화 스케줄러: Redis에 임시 저장된 좋아요 증분 데이터를 주기적(1분 단위)으로 DB에 벌크 업데이트하는PostLikeCountSyncToDBService를 도입하여 데이터 최종 정합성을 확보했습니다.
  • 성능 검증: k6 부하 테스트를 통해 VU 1,000명 기준 P(95) Latency가 약 44% 개선됨을 확인했습니다.
  • 레디스 키 때문에 yml도 업데이트해두었습니다! pr 닫히기 전에 원상복귀 해두겠습니닷 노션에도 업데이트해두었습니다
  • 관련하여 노션에 문서화 해두었습니다 트러블슈팅 -> count 이슈 -> 피드(게시글) 좋아요 API 동시성 - 성능 최적화

📸 스크린샷

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

릴리스 노트

  • 성능 개선

    • 좋아요/좋아요 취소 기능의 응답 속도가 개선되었습니다.
    • 대규모 동시성 요청 처리 시 시스템 안정성이 강화되었습니다.
  • 테스트

    • 동시 사용자의 좋아요 상태 변경에 대한 스트레스 테스트를 추가했습니다.
    • 대규모 트래픽 시뮬레이션 테스트 도구를 도입했습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

…Counts 배치 좋아요 업데이트 함수 추가

(#338)
findPostIdsByIds,batchUpdateLikeCounts 추가 (#338)
…pdateLikeCounts 배치 좋아요 업데이트 함수 추가

(#338)
…Counts 배치 좋아요 업데이트 함수 추가

(#338)
Redis의 좋아요 수(증감)를 주기적으로 DB에 동기화하는 스케줄러추가 (#338)
- 게시글 좋아요 로직이 레디스 원자적 연산으로 수정되면서 게시글 엔티티의 좋아요 정보(likeCount)는 더이상 실시간정보가 아니므로 db 정합성을 확인하는 코드 주석 처리
(#324)
- updateLikeCount()의newLikeCount 매개변수가 추가되면서 테스트코드 수정
(#324)
@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

Redis 기반 원자적 연산을 사용하여 게시글 좋아요 기능을 개선합니다. 좋아요 수를 Redis에서 관리하고 주기적으로 데이터베이스에 동기화하는 구조로 변경되었습니다. k6 부하 테스트 스크립트도 추가되었습니다.

Changes

칸 / 파일 변경 사항
부하 테스트
loadtest/feed/feed-like-load-test.js
k6 기반 피드 좋아요 동시 요청 시뮬레이션 스크립트, 커스텀 메트릭 및 오류 분류 로직 추가
Redis 설정
src/main/java/konkuk/thip/config/RedisConfig.java
Integer 타입 RedisTemplate 빈 추가
Redis 포트 정의
src/main/java/konkuk/thip/post/application/port/out/PostLikeCountRedis*.java
Redis 좋아요 수 조회/명령 포트 인터페이스 2개 추가
Redis 어댑터
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCountRedisAdapter.java
Redis 캐시 기반 좋아요 수 관리, 일괄 조회/업데이트/초기화 메서드 구현
동기화 서비스
src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java
Redis의 좋아요 수를 주기적으로(1분) DB에 동기화하는 스케줄된 서비스
좋아요 서비스 개선
src/main/java/konkuk/thip/post/application/service/PostLikeService.java
Redis 포트 주입, Redis 기반 좋아요 수 조회/업데이트로 변경
도메인 모델 서명 변경
src/main/java/konkuk/thip/*/domain/{Feed,Record,Vote}.java
updateLikeCount 메서드에 newLikeCount 파라미터 추가, getLikeCount() 게터 추가
도메인 인터페이스
src/main/java/konkuk/thip/post/domain/CountUpdatable.java
updateLikeCount 서명 업데이트 및 getLikeCount() 메서드 추가
핸들러 확장
src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java
findPostIdsByIds, batchUpdateLikeCounts 메서드 추가
배치 업데이트 메서드
src/main/java/konkuk/thip/{feed,roompost}/adapter/out/persistence/{FeedCommandPersistenceAdapter,RecordCommandPersistenceAdapter,VoteCommandPersistenceAdapter}.java
JdbcTemplate 기반 배치 업데이트(batchUpdateLikeCounts, findByIds) 메서드 추가
배치 쿼리 메서드
src/main/java/konkuk/thip/{feed,roompost}/adapter/out/persistence/repository/*Repository.java
findByPostIds 쿼리 메서드 추가 (Feed, Record, Vote)
포트 인터페이스 확장
src/main/java/konkuk/thip/{feed,roompost}/application/port/out/{FeedCommandPort,RecordCommandPort,VoteCommandPort}.java
findByIds, batchUpdateLikeCounts 메서드 추가
테스트 동시성
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java
새 동시성 스트레스 테스트 추가 (스레드 2개, 반복 10회)
기존 테스트 업데이트
src/test/java/konkuk/thip/**/*Test.java
Redis 의존성 주입, 메서드 서명 업데이트 호출, 테스트 고정값 ISBN 변경

Sequence Diagram

sequenceDiagram
    participant Client
    participant PostLikeService
    participant PostLikeCountRedisQueryPort
    participant PostLikeCountRedisAdapter as Redis Adapter
    participant PostLikeCountSyncService
    participant FeedCommandPort
    participant Database
    
    rect rgba(200, 220, 255, 0.5)
    Note over Client,Database: 좋아요 요청 흐름 (실시간)
    Client->>PostLikeService: 좋아요/언좋아요 요청
    PostLikeService->>PostLikeCountRedisQueryPort: getLikeCount(postType, postId, dbCount)
    PostLikeCountRedisQueryPort->>PostLikeCountRedisAdapter: Redis 조회
    alt 캐시 히트
        PostLikeCountRedisAdapter-->>PostLikeCountRedisQueryPort: 캐시된 카운트 반환
    else 캐시 미스
        PostLikeCountRedisAdapter->>Database: DB에서 조회
        Database-->>PostLikeCountRedisAdapter: dbLikeCount 반환
        PostLikeCountRedisAdapter->>PostLikeCountRedisAdapter: Redis에 저장 (TTL 10분)
        PostLikeCountRedisAdapter-->>PostLikeCountRedisQueryPort: 카운트 반환
    end
    PostLikeCountRedisQueryPort-->>PostLikeService: 현재 좋아요 수
    PostLikeService->>PostLikeService: updateLikeCount(newCount)
    PostLikeService->>PostLikeCountRedisAdapter: updateLikeCount (INCR/DECR)
    PostLikeCountRedisAdapter->>PostLikeCountRedisAdapter: Redis 원자적 연산
    PostLikeCountRedisAdapter-->>PostLikeService: 완료
    PostLikeService-->>Client: 응답
    end
    
    rect rgba(220, 255, 200, 0.5)
    Note over PostLikeCountSyncService,Database: 주기적 동기화 (1분 간격)
    PostLikeCountSyncService->>PostLikeCountRedisAdapter: getAllLikeCounts()
    PostLikeCountRedisAdapter-->>PostLikeCountSyncService: 변경된 모든 카운트
    PostLikeCountSyncService->>PostLikeCountSyncService: PostType별 집계
    PostLikeCountSyncService->>FeedCommandPort: batchUpdateLikeCounts(Map)
    FeedCommandPort->>Database: 배치 업데이트 (UPDATE ... SET like_count = like_count + ?)
    Database-->>FeedCommandPort: 완료
    PostLikeCountSyncService->>PostLikeCountRedisAdapter: bulkResetLikeCounts()
    PostLikeCountRedisAdapter->>PostLikeCountRedisAdapter: Redis 파이프라인 초기화
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

주의 깊게 검토해야 할 영역:

  • Redis 어댑터 로직 (PostLikeCountRedisAdapter.java): 캐시 미스 시 DB 폴백, TTL 처리, 원자적 연산의 올바른 구현 확인 필요
  • 동기화 서비스 (PostLikeCountSyncToDBService.java): PostType별 집계 로직, 존재하는 게시글만 업데이트하는 필터링, 트랜잭션 관리 검토
  • 배치 업데이트 구현: 3개 어댑터(FeedCommandPersistenceAdapter, RecordCommandPersistenceAdapter, VoteCommandPersistenceAdapter)에서 유사한 배치 로직이 반복되므로 일관성 확인
  • 도메인 모델 서명 변경: updateLikeCount의 새 파라미터가 모든 호출처에서 올바르게 전달되는지 확인
  • Redis 캐시 일관성: 동기화 중 새 요청이 들어오는 경우의 레이스 컨디션, TTL 만료 시나리오 검토
  • 테스트 커버리지: 부하 테스트 스크립트와 동시성 테스트가 Redis 동작을 올바르게 검증하는지 확인

Possibly related issues

Possibly related PRs

Suggested reviewers

  • seongjunnoh
  • buzz0331

Poem

🐰 Redis의 마법으로 좋아요 날아다니고,
원자적 연산 춤을 추며 반짝이고,
주기적 동기화가 DB와 손잡고,
배치 업데이트로 효율 높여가고,
부하 테스트 k6으로 힘 자랑하네! 🚀✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.65% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 Redis 원자적 연산을 통한 게시글 좋아요 API 처리율 개선이라는 주요 변경사항을 명확하게 설명합니다.
Linked Issues check ✅ Passed PR은 #338의 모든 필수 요구사항을 충족합니다: Redis INCR/DECR 원자적 연산 도입(PostLikeCountRedisAdapter), 주기적 동기화 스케줄러(PostLikeCountSyncToDBService) 추가, k6 부하 테스트 스크립트 작성 완료.
Out of Scope Changes check ✅ Passed 모든 변경사항은 Redis 기반 원자적 연산 도입 및 주기적 동기화라는 정해진 범위 내에 있습니다. 테스트 데이터 변경(ISBN)은 테스트 격리를 위한 필수 조정이며, updateLikeCount 서명 변경은 Redis 캐시 기반 업데이트를 지원하기 위한 필수 리팩토링입니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test/#322-k6-feed-like-redis-INCR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link

Test Results

489 tests   489 ✅  44s ⏱️
146 suites    0 💤
146 files      0 ❌

Results for commit 38bfd73.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (13)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedApiTest.java (1)

62-62: 테스트 픽스처 데이터 변경을 확인했습니다.

ISBN 값이 "9788954682152"에서 "9788954682151"로 변경되었습니다. 다른 테스트 파일들도 유사하게 다양한 ISBN 값을 사용하도록 변경된 것으로 보입니다.

테스트 간 데이터 격리나 병렬 실행을 위한 의도적인 변경으로 보이지만, 이러한 변경의 목적이 명확하지 않습니다. 만약 테스트 데이터 충돌을 방지하기 위한 것이라면, 각 테스트에서 동적으로 고유한 ISBN을 생성하는 것이 더 명확한 의도를 전달할 수 있습니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java (1)

61-61: 테스트 픽스처 ISBN 변경을 확인했습니다.

이전 파일과 동일하게 ISBN 값이 변경되었습니다. 여러 테스트 파일에서 일관되게 ISBN을 다르게 설정하는 패턴이 보이는데, 이러한 변경이 PR의 주요 목적(Redis 원자적 연산 도입)과 직접적인 연관이 없어 보입니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (1)

49-49: 공백 줄 추가 및 ISBN 변경을 확인했습니다.

다른 테스트 파일들과 마찬가지로 ISBN 픽스처가 변경되었습니다. 여러 테스트 파일에서 이러한 변경이 반복되고 있는데, 이것이 의도적인 패턴인지 확인이 필요합니다.

Also applies to: 57-57

src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java (1)

62-62: 테스트 픽스처 ISBN이 변경되었습니다.

다른 테스트 파일들과 동일한 패턴으로 ISBN이 변경되었습니다. 이러한 변경 사항들이 PR 전체에 걸쳐 일관되게 나타나고 있으므로, 변경의 목적과 의도를 PR 설명이나 커밋 메시지에 명시하는 것이 좋습니다.

loadtest/feed/feed-like-load-test.js (3)

6-12: 설정 값을 환경 변수로 외부화하세요.

BASE_URL과 FEED_ID가 하드코딩되어 있어 다양한 환경(로컬, 스테이징, 프로덕션)에서 테스트하기 어렵습니다. k6의 환경 변수 또는 설정 파일을 사용하여 외부화하는 것을 권장합니다.

다음과 같이 환경 변수를 사용하도록 수정하세요:

-const BASE_URL = 'http://localhost:8080';
-const FEED_ID = 1; // 테스트할 피드 ID
+const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
+const FEED_ID = parseInt(__ENV.FEED_ID || '1'); // 테스트할 피드 ID

실행 시: k6 run -e BASE_URL=https://api.example.com -e FEED_ID=123 feed-like-load-test.js


92-92: 불필요한 배열 길이 조정 코드입니다.

Line 92의 길이 조정 로직은 불필요합니다. 루프가 이미 USERS_COUNT까지만 반복하므로 tokens.lengthUSERS_COUNT를 초과할 수 없습니다.

다음과 같이 제거할 수 있습니다:

-    if (tokens.length > USERS_COUNT) tokens.length = USERS_COUNT;

110-112: 토큰 발급 실패 시 로깅을 추가하세요.

토큰이 없는 VU가 조용히 스킵되어 setup 단계의 문제를 파악하기 어렵습니다. 테스트 결과 해석에 혼란을 줄 수 있으므로, 콘솔 로그를 추가하거나 별도의 메트릭으로 추적하는 것을 권장합니다.

     if (!token) {     // 토큰 발급 실패 -> 스킵
+        console.warn(`VU ${vuIdx} skipped: token issuance failed`);
         return;
     }
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java (1)

92-94: 좋아요 수 검증 로직이 주석 처리되어 있습니다.

Redis 기반 좋아요 수 관리로 전환하면서 즉시 DB에 반영되지 않기 때문에 카운트 검증을 주석 처리한 것으로 보입니다. 그러나 이는 테스트 커버리지 감소로 이어집니다.

다음 중 하나를 고려하세요:

  1. Redis에서 직접 좋아요 수를 조회하여 검증하는 로직으로 교체
  2. 스케줄러를 수동으로 트리거한 후 DB를 검증
  3. 주석에 왜 비활성화했는지, 언제 재활성화할지 명시

예시:

-//        // 좋아요 카운트 증가 확인
-//        FeedJpaEntity updatedFeed = feedJpaRepository.findById(feed.getPostId()).orElseThrow();
-//        assertThat(updatedFeed.getLikeCount()).isEqualTo(1);
+        // Redis에서 좋아요 수 검증
+        String redisKey = "post:like:count:FEED:" + feed.getPostId();
+        Integer likeCount = redisTemplate.opsForValue().get(redisKey);
+        assertThat(likeCount).isEqualTo(1);

Also applies to: 139-141

src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java (2)

35-36: @DisplayName이 테스트 유형과 맞지 않습니다.

@SpringBootTest를 사용하는 통합 테스트인데 [단위]로 표기되어 있습니다. [통합] 또는 [동시성]으로 수정하는 것이 더 정확합니다.

-@DisplayName("[단위] 피드 좋아요 상태변경 다중 스레드 테스트")
+@DisplayName("[통합] 피드 좋아요 상태변경 동시성 테스트")

99-106: ExecutorService 종료 처리 개선 및 최종 상태 검증 추가를 권장합니다.

  1. shutdown()awaitTermination()을 호출하는 것이 리소스 정리를 위한 모범 사례입니다.
  2. 동시성 테스트의 목적상 성공/실패 횟수뿐만 아니라 최종 좋아요 수가 예상대로인지 검증하면 더 강력한 테스트가 됩니다.
         latch.await();
-        executor.shutdown();
+        executor.shutdown();
+        executor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS);
 
         // then
         assertAll(
                 () -> assertThat(successCount.get()).isEqualTo(threadCount * repeat),
-                () -> assertThat(failCount.get()).isEqualTo(0)
+                () -> assertThat(failCount.get()).isEqualTo(0),
+                // 각 유저가 10번 토글 -> 최종 상태: 좋아요 상태 = true (홀수 번째 토글)
+                // 최종 좋아요 수 = 2 (두 유저 모두 좋아요 상태)
+                () -> {
+                    FeedJpaEntity updatedFeed = feedJpaRepository.findById(feed.getPostId()).orElseThrow();
+                    // Redis에서 동기화되기 전 상태일 수 있으므로 실제 검증 로직은 상황에 맞게 조정
+                }
         );
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

132-150: 빈 맵에 대한 early return 추가를 권장합니다.

idToLikeCount가 비어 있을 때 불필요한 JDBC 호출을 방지할 수 있습니다.

 @Override
 public void batchUpdateLikeCounts(Map<Long, Integer> idToLikeCount) {
+    if (idToLikeCount == null || idToLikeCount.isEmpty()) {
+        return;
+    }
     String sql = "UPDATE posts SET like_count = like_count + ? WHERE post_id = ?";
     List<Map.Entry<Long, Integer>> entries = new ArrayList<>(idToLikeCount.entrySet());
src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java (1)

208-226: 코드 중복 추출을 고려해 보세요.

batchUpdateLikeCounts 구현이 FeedCommandPersistenceAdapterRecordCommandPersistenceAdapter와 동일합니다. 향후 유지보수를 위해 공통 유틸리티 클래스로 추출하는 것을 고려해 볼 수 있습니다.

예시:

// PostLikeCountBatchUpdater.java
@Component
@RequiredArgsConstructor
public class PostLikeCountBatchUpdater {
    private final JdbcTemplate jdbcTemplate;
    
    public void batchUpdateLikeCounts(Map<Long, Integer> idToLikeCount) {
        if (idToLikeCount == null || idToLikeCount.isEmpty()) return;
        // ... 공통 구현
    }
}
src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)

102-119: 배치 업데이트 로직이 올바르게 구현되었습니다.

증분 업데이트(like_count = like_count + ?)를 사용하여 write-back 패턴에 적합하게 구현되었으며, BatchPreparedStatementSetter를 통한 배치 처리로 효율성도 확보되었습니다.

운영 안정성을 위해 다음 개선을 고려해보세요:

 @Override
 public void batchUpdateLikeCounts(Map<Long, Integer> idToLikeCount) {
+    if (idToLikeCount == null || idToLikeCount.isEmpty()) {
+        return;
+    }
+    
     String sql = "UPDATE posts SET like_count = like_count + ? WHERE post_id = ?";
     List<Map.Entry<Long, Integer>> entries = new ArrayList<>(idToLikeCount.entrySet());
 
     jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
         @Override
         public void setValues(PreparedStatement ps, int i) throws SQLException {
             Map.Entry<Long, Integer> entry = entries.get(i);
             ps.setInt(1, entry.getValue());
             ps.setLong(2, entry.getKey());
         }
 
         @Override
         public int getBatchSize() {
             return entries.size();
         }
     });
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4052fb7 and 38bfd73.

📒 Files selected for processing (32)
  • loadtest/feed/feed-like-load-test.js (1 hunks)
  • src/main/java/konkuk/thip/config/RedisConfig.java (2 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (5 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (3 hunks)
  • src/main/java/konkuk/thip/feed/domain/Feed.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCountRedisAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/port/out/PostLikeCountRedisCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/port/out/PostLikeCountRedisQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/service/PostLikeService.java (3 hunks)
  • src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java (2 hunks)
  • src/main/java/konkuk/thip/post/domain/CountUpdatable.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (5 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java (5 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java (3 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java (3 hunks)
  • src/main/java/konkuk/thip/roompost/domain/Record.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/domain/Vote.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java (6 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java (5 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/domain/FeedTest.java (3 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java (8 hunks)
  • src/test/java/konkuk/thip/roompost/domain/RecordTest.java (3 hunks)
  • src/test/java/konkuk/thip/roompost/domain/VoteTest.java (3 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Applied to files:

  • src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java
  • src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java
  • src/main/java/konkuk/thip/post/application/port/out/PostLikeCountRedisCommandPort.java
  • src/main/java/konkuk/thip/post/application/port/out/PostLikeCountRedisQueryPort.java
  • src/main/java/konkuk/thip/post/application/service/PostLikeService.java
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
📚 Learning: 2025-07-14T14:19:38.796Z
Learnt from: buzz0331
Repo: THIP-TextHip/THIP-Server PR: 75
File: src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java:50-83
Timestamp: 2025-07-14T14:19:38.796Z
Learning: Vote와 VoteItem 엔티티는 자주 함께 사용되므로, N+1 문제를 방지하기 위해 양방향 매핑과 fetch join을 고려하는 것이 좋습니다. 특히 기록장 조회 API 등에서도 함께 사용될 가능성이 높습니다.

Applied to files:

  • src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java
📚 Learning: 2025-06-29T09:17:39.787Z
Learnt from: hd0rable
Repo: THIP-TextHip/THIP-Server PR: 36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.

Applied to files:

  • src/main/java/konkuk/thip/config/RedisConfig.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java
📚 Learning: 2025-09-01T13:18:13.652Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 287
File: src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java:8-14
Timestamp: 2025-09-01T13:18:13.652Z
Learning: seongjunnoh는 JpaRepository의 findById 메서드 재정의보다는 도메인별 명시적 메서드(findByUserId, findByRoomId 등)를 정의하여 Hibernate Filter 적용을 보장하는 방식을 선호하며, 이를 통해 더 안전하고 의도가 명확한 코드 구조를 구축한다.

Applied to files:

  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java
📚 Learning: 2025-07-26T06:09:00.850Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.

Applied to files:

  • src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java
🧬 Code graph analysis (6)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (32)
src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java (3)

3-4: 필요한 import 추가를 확인했습니다.

새로운 메서드들에서 사용되는 ListMap 타입을 위한 import가 추가되었습니다.


52-58: 배치 조회 메서드가 올바르게 구현되었습니다.

findPostIdsByIds 메서드는 PostType에 따라 적절한 포트로 라우팅하여 여러 게시글 ID를 조회합니다. Redis에서 동기화할 게시글이 실제로 존재하는지 확인하는 용도로 사용될 것으로 보입니다.


60-66: 배치 업데이트 메서드가 올바르게 구현되었습니다.

batchUpdateLikeCounts 메서드는 Redis에 누적된 좋아요 증감 값을 데이터베이스에 일괄 반영하기 위한 메서드입니다. PostType별로 적절한 포트로 라우팅하는 패턴이 기존 메서드들과 일관성 있게 구현되어 있습니다.

src/main/java/konkuk/thip/config/RedisConfig.java (2)

11-11: 필요한 serializer import가 추가되었습니다.

GenericToStringSerializer는 새로운 redisIntegerTemplate 빈에서 Integer 값을 직렬화하기 위해 사용됩니다.


33-40: Redis Integer 전용 템플릿이 올바르게 구성되었습니다.

좋아요 카운트를 Redis에서 Integer로 관리하기 위한 전용 RedisTemplate<String, Integer> 빈입니다. GenericToStringSerializer<Integer>를 사용하여 INCR/DECR과 같은 Redis 원자적 연산과 호환되도록 구성되어 있습니다.

Based on learnings, Redis 의존성은 향후 사용을 위해 이미 추가되어 있었고, 이제 실제로 활용되고 있습니다.

src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java (2)

4-4: Redis 통합을 위한 import와 필드가 추가되었습니다.

테스트에서 Redis 상태를 관리하기 위한 RedisTemplate이 추가되었습니다.

Also applies to: 30-30, 62-62


76-80: Redis 초기화 로직이 추가되었습니다.

각 테스트 실행 전 Redis의 모든 키를 삭제하여 깨끗한 상태를 보장합니다. 테스트 격리를 위해 필요한 변경입니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java (1)

161-161: 동일한 ISBN 불일치 패턴이 여러 테스트에서 반복됩니다.

모든 테스트에서 데이터베이스에 저장되는 Book의 ISBN과 API 요청의 ISBN이 다릅니다. 이러한 패턴이 여러 테스트에 걸쳐 일관되게 나타나므로 의도적인 것으로 보이지만, 다음을 확인해야 합니다:

  1. 테스트가 실제로 통과하는지
  2. ISBN 조회 로직이 어떤 기준으로 Book을 찾는지 (정확한 매칭 vs 부분 매칭)
  3. 이러한 불일치가 실제 버그를 놓칠 가능성은 없는지

Also applies to: 164-164, 209-209, 212-212, 242-242, 245-245, 282-282, 285-285

loadtest/feed/feed-like-load-test.js (1)

33-45: 잘 작성된 에러 파싱 로직입니다.

JSON 파싱 실패 시 안전한 기본값을 반환하는 방어적인 코드가 잘 작성되어 있습니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (1)

32-33: 올바르게 구현된 배치 조회 메서드입니다.

IN 절을 사용한 배치 ID 조회 메서드가 올바르게 구현되었으며, @Param 어노테이션도 적절히 사용되었습니다. Redis 동기화 시 존재하는 게시글 ID를 확인하는 데 유용합니다.

src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java (1)

25-25: 배치 연산을 위한 포트 메서드가 잘 설계되었습니다.

findByIdsbatchUpdateLikeCounts 메서드 추가로 Redis에서 DB로의 write-back 동기화가 효율적으로 수행될 수 있습니다. CQRS CommandPort에 find 메서드를 추가하는 프로젝트 컨벤션에도 부합합니다.

Based on learnings, THIP 프로젝트에서는 CommandPort에 도메인 엔티티를 찾아오는 메서드를 추가하는 컨벤션을 따릅니다.

Also applies to: 57-57

src/test/java/konkuk/thip/feed/domain/FeedTest.java (1)

230-260: 테스트가 새로운 API 시그니처에 맞게 올바르게 업데이트되었습니다.

모든 updateLikeCount 호출이 세 번째 파라미터로 현재 좋아요 수를 명시적으로 전달하도록 수정되었습니다. 특히 Line 260의 underflow 테스트는 feed.getLikeCount()-1을 전달하여 언더플로우 상황을 정확히 시뮬레이션하고 있습니다.

src/main/java/konkuk/thip/feed/domain/Feed.java (1)

158-160: Redis 기반 좋아요 수 관리에 적합한 구현입니다.

updateLikeCount 메서드가 명시적인 newLikeCount 파라미터를 받아 PostCountService에 전달하는 방식으로 변경되었으며, PostCountService.updatePostLikeCount(boolean isLike, int likeCount) 메서드가 해당 시그니처에 맞게 구현되어 있습니다. 이는 Redis에서 관리되는 최신 좋아요 수를 도메인 엔티티에 반영할 수 있게 하여, 원자적 연산 기반 아키텍처에 적합합니다.

src/main/java/konkuk/thip/post/domain/CountUpdatable.java (1)

8-10: 인터페이스 변경이 적절하게 적용되었습니다.

updateLikeCount 메서드에 newLikeCount 파라미터를 추가하고 getLikeCount getter를 추가한 것은 Redis 기반 좋아요 수 관리 아키텍처에 적합합니다. Redis에서 관리되는 최신 값을 명시적으로 전달할 수 있게 되었으며, 모든 구현체(Feed, Vote, Record)에서 새로운 메서드 시그니처가 올바르게 적용되었습니다. Vote와 Record는 RoomPost 인터페이스를 통해 CountUpdatable을 간접 구현하고 있으며, 모두 @Getter 어노테이션으로 getLikeCount() 메서드가 자동 생성됩니다.

src/test/java/konkuk/thip/roompost/domain/RecordTest.java (1)

145-180: 테스트가 새로운 3-파라미터 시그니처에 맞게 적절히 업데이트되었습니다.

모든 updateLikeCount 호출이 현재 좋아요 수를 세 번째 인자로 전달하도록 일관되게 수정되었으며, 언더플로우 테스트(Line 176)도 명시적으로 getLikeCount()-1을 전달하여 예외 발생을 확인하고 있습니다.

src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1)

17-17: Feed에도 배치 연산 메서드가 일관되게 추가되었습니다.

findByIdsbatchUpdateLikeCounts 메서드가 Record, Vote와 동일한 패턴으로 추가되어 Redis 기반 좋아요 수 동기화를 지원합니다.

Based on learnings, CommandPort에 이러한 메서드를 추가하는 것은 프로젝트의 CQRS 컨벤션을 따릅니다.

Also applies to: 27-27

src/main/java/konkuk/thip/post/application/port/out/PostLikeCountRedisCommandPort.java (2)

8-8: 벌크 리셋 메서드가 추가되었습니다.

bulkResetLikeCounts는 DB 동기화 후 Redis 키를 정리하는 메서드로 보입니다. Set<String>을 사용하여 중복 없이 효율적으로 처리합니다.


7-7: Redis 좋아요 수 업데이트 메서드에 매개변수 설명이 필요합니다.

updateLikeCount 메서드의 likeCount 매개변수가 데이터베이스에서 가져온 초기값(캐시가 없을 때만 사용)이고, 실제 증감은 isLike 불린값으로 처리된다는 점이 메서드 시그니처만으로는 불명확합니다. JavaDoc을 추가하여 각 매개변수의 역할을 명시하는 것을 권장합니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java (1)

60-64: 테스트 격리를 위한 Redis 초기화가 적절합니다.

각 테스트 전에 모든 Redis 키를 삭제하여 테스트 간 간섭을 방지합니다.

src/main/java/konkuk/thip/roompost/domain/Vote.java (1)

82-84: 도메인 메서드 시그니처가 Redis 기반 카운트 관리에 맞게 업데이트되었습니다.

updateLikeCount가 명시적인 newLikeCount 파라미터를 받아 Redis에서 관리되는 좋아요 수를 반영할 수 있도록 변경되었습니다. Feed, Record와 일관된 패턴을 따릅니다.

src/main/java/konkuk/thip/post/application/port/out/PostLikeCountRedisQueryPort.java (1)

7-8: Redis 조회 포트가 적절하게 설계되었습니다.

getLikeCountdbLikeCount를 fallback으로 받아 Redis에 값이 없을 때 DB 값을 사용할 수 있도록 하고, getAllLikeCounts는 배치 동기화를 위한 전체 조회를 지원합니다.

Based on learnings, Command/Query Port를 분리하는 것은 프로젝트의 CQRS 컨벤션을 따릅니다.

src/test/java/konkuk/thip/roompost/domain/VoteTest.java (1)

145-180: 테스트가 새로운 시그니처에 맞게 업데이트되었습니다.

모든 updateLikeCount 호출이 올바르게 수정되었습니다. Line 176의 언더플로우 테스트는 현재 카운트(0)를 전달하고 isLike=false로 감소를 시도하여 예외를 발생시킵니다.

src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java (2)

20-20: 배치 조회 메서드가 올바르게 추가되었습니다.

findByIds 메서드는 여러 Record를 한 번에 조회하기 위한 배치 연산을 지원하며, 구현체에서 null 체크와 빈 리스트 처리를 통해 엣지 케이스를 적절히 처리합니다. batchUpdateLikeCounts 메서드도 JDBC 배치 작업을 활용해 효율적으로 Redis 동기화 후 DB 업데이트를 수행합니다.


31-31: 트랜잭션 처리와 에러 핸들링이 필요합니다.

batchUpdateLikeCounts 메서드가 구현되었으나, 배치 업데이트 작업에 @Transactional 애노테이션이 누락되었고 에러 핸들링이 없습니다. 배치 작업의 원자성을 보장하고 실패 시 적절히 처리하도록 개선이 필요합니다.

또한 RecordCommandPort가 조회 메서드(findById, findByIds)와 쓰기 메서드를 함께 포함하고 있어 CQRS 패턴 분리가 필요합니다. 조회는 QueryPort로 분리하시기 바랍니다.

⛔ Skipped due to learnings
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

55-60: 구현이 적절합니다.

findByIds 메서드가 null/empty 입력을 올바르게 처리하고 있습니다.

src/main/java/konkuk/thip/roompost/domain/Record.java (1)

87-90: Redis 기반 좋아요 수 관리를 위한 시그니처 변경이 적절합니다.

외부에서 최신 좋아요 수를 주입받아 도메인 상태를 갱신하는 방식으로, Feed 및 Vote 도메인과 일관성 있게 구현되었습니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java (1)

93-98: 구현이 적절합니다.

FeedCommandPersistenceAdapter와 일관된 방식으로 구현되었습니다.

src/main/java/konkuk/thip/post/application/service/PostLikeService.java (1)

64-68: Redis 연산 순서와 동시성 처리를 검토해주세요.

현재 로직의 순서:

  1. Redis에서 좋아요 수 조회 (getLikeCount)
  2. 도메인 상태 갱신 (post.updateLikeCount())
  3. Redis에 원자적 증감 (updateLikeCount with INCR/DECR)

동시성 환경에서 getLikeCount와 updateLikeCount 사이에 다른 스레드가 Redis를 수정할 수 있습니다. 각 INCR/DECR 명령은 원자적이지만, 이 두 단계 사이의 간격으로 인해 도메인 객체의 likeCount가 실제 Redis 값과 일시적으로 불일치할 수 있습니다. 반환값(PostIsLikeResult)에 정확한 좋아요 수가 필요하다면, Redis 갱신 후 최신 값을 다시 읽거나 Lua 스크립트로 연산을 원자적으로 처리하는 것을 검토하세요.

src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java (1)

32-34: 동기화 주기 설정이 적절합니다.

1분 단위의 write-back 동기화 주기는 성능과 데이터 정합성 간 균형을 잘 맞춘 설계입니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)

68-72: 적절한 방어 로직입니다.

null 및 빈 리스트 체크를 통해 불필요한 DB 조회를 방지하는 좋은 구현입니다.

src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCountRedisAdapter.java (2)

29-39: Cache-aside 패턴이 올바르게 구현되었습니다.

캐시 미스 시 DB 값을 저장하고 TTL을 설정하는 로직이 적절합니다.


75-87: 파이프라인을 활용한 효율적인 배치 리셋입니다.

여러 키를 0으로 리셋할 때 파이프라인을 사용하여 네트워크 왕복을 최소화한 좋은 구현입니다.

Comment on lines +42 to +61
public Map<String, Integer> getAllLikeCounts() {
// 모든 좋아요 카운트 키 검색
Set<String> keys = redisTemplate.keys(postLikeCountPrefix + "*");
if (keys == null || keys.isEmpty()) {
return Collections.emptyMap();
}

List<Integer> values = redisTemplate.opsForValue().multiGet(keys);

Map<String, Integer> result = new HashMap<>();
List<String> keyList = new ArrayList<>(keys);

// key와 value를 매핑하여 맵 생성
for (int i = 0; i < keyList.size(); i++) {
if (values.get(i) != null) {
result.put(keyList.get(i), values.get(i));
}
}
return result; //각 키의 좋아요 수를 맵으로 반환
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

프로덕션 환경에서 위험한 keys() 명령어를 사용하고 있습니다.

Line 44의 keys() 명령어는 Redis의 모든 키를 스캔하는 O(N) 블로킹 연산으로, 싱글 스레드인 Redis를 정지시켜 다른 모든 요청의 응답 시간을 지연시킵니다. 스케줄러가 1분마다 이 메서드를 호출하므로, 프로덕션 환경에서 심각한 성능 저하를 유발할 수 있습니다.

SCAN 명령어를 사용하도록 변경하세요:

 @Override
 public Map<String, Integer> getAllLikeCounts() {
-    // 모든 좋아요 카운트 키 검색
-    Set<String> keys = redisTemplate.keys(postLikeCountPrefix + "*");
-    if (keys == null || keys.isEmpty()) {
-        return Collections.emptyMap();
-    }
-
-    List<Integer> values = redisTemplate.opsForValue().multiGet(keys);
-
-    Map<String, Integer> result = new HashMap<>();
-    List<String> keyList = new ArrayList<>(keys);
-
-    // key와 value를 매핑하여 맵 생성
-    for (int i = 0; i < keyList.size(); i++) {
-        if (values.get(i) != null) {
-            result.put(keyList.get(i), values.get(i));
-        }
-    }
-    return result; //각 키의 좋아요 수를 맵으로 반환
+    Map<String, Integer> result = new HashMap<>();
+    String pattern = postLikeCountPrefix + "*";
+    
+    // SCAN을 사용하여 비블로킹 방식으로 키 조회
+    redisTemplate.execute((RedisCallback<Void>) connection -> {
+        try (Cursor<byte[]> cursor = connection.scan(
+                ScanOptions.scanOptions().match(pattern).count(100).build())) {
+            
+            while (cursor.hasNext()) {
+                String key = new String(cursor.next());
+                Integer value = redisTemplate.opsForValue().get(key);
+                if (value != null) {
+                    result.put(key, value);
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to scan Redis keys", e);
+        }
+        return null;
+    });
+    
+    return result;
 }

필요한 import 추가:

import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCountRedisAdapter.java
around lines 42 to 61, replace the usage of redisTemplate.keys(...) with a
non-blocking SCAN-based iteration: use redisTemplate.execute with
ScanOptions.scanOptions().match(postLikeCountPrefix + "*").count(1000).build()
to obtain a Cursor<byte[]> (or Cursor<String> depending on template) and iterate
the cursor to collect keys into a List, ensuring the cursor is closed after use;
then call opsForValue().multiGet(collectedKeys) and map results to the returned
Map as before. Also add imports for org.springframework.data.redis.core.Cursor
and org.springframework.data.redis.core.ScanOptions and handle potential
null/empty cases the same way as before.

Comment on lines +64 to +72
public void updateLikeCount(PostType postType, Long postId, Integer likeCount, boolean isLike) {
String redisKey = makeRedisKey(postType, postId);
// 키가 없으면 getLikeCount로 초기화
if (!redisTemplate.hasKey(redisKey)) {
getLikeCount(postType,postId,likeCount);
}
if(isLike) incrementLikeCount(postType,postId);
else decrementLikeCount(postType,postId);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition과 코드 포매팅 문제가 있습니다.

  1. Race condition (Major): Line 67의 hasKey() 체크와 Line 68-71의 후속 작업 사이에 다른 스레드가 개입하여 키를 생성하거나 만료시킬 수 있습니다. 이로 인해 중복 초기화가 발생할 수 있습니다.

  2. 코드 포매팅 (Minor): Line 70-71에서 if 키워드 뒤에 공백이 누락되었습니다.

다음과 같이 수정하세요:

 @Override
 public void updateLikeCount(PostType postType, Long postId, Integer likeCount, boolean isLike) {
     String redisKey = makeRedisKey(postType, postId);
-    // 키가 없으면 getLikeCount로 초기화
-    if (!redisTemplate.hasKey(redisKey)) {
-        getLikeCount(postType,postId,likeCount);
+    
+    // 키 존재 여부와 상관없이 INCR/DECR은 안전하게 작동
+    // 키가 없으면 0부터 시작하므로 초기화 후 증감
+    Integer currentCount = redisTemplate.opsForValue().get(redisKey);
+    if (currentCount == null) {
+        // 캐시 미스 시 DB 값으로 초기화
+        redisTemplate.opsForValue().set(redisKey, likeCount, TTL);
+        currentCount = likeCount;
     }
-    if(isLike) incrementLikeCount(postType,postId);
-    else decrementLikeCount(postType,postId);
+    
+    if (isLike) {
+        incrementLikeCount(postType, postId);
+    } else {
+        decrementLikeCount(postType, postId);
+    }
 }

또는 더 간단하게, Redis INCR/DECR이 키가 없을 때 0에서 시작하는 특성을 활용:

 @Override
 public void updateLikeCount(PostType postType, Long postId, Integer likeCount, boolean isLike) {
     String redisKey = makeRedisKey(postType, postId);
-    // 키가 없으면 getLikeCount로 초기화
-    if (!redisTemplate.hasKey(redisKey)) {
-        getLikeCount(postType,postId,likeCount);
+    
+    // 먼저 현재 값 확인하여 캐시 워밍
+    getLikeCount(postType, postId, likeCount);
+    
+    if (isLike) {
+        incrementLikeCount(postType, postId);
+    } else {
+        decrementLikeCount(postType, postId);
     }
-    if(isLike) incrementLikeCount(postType,postId);
-    else decrementLikeCount(postType,postId);
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +89 to +99
private void incrementLikeCount(PostType postType, Long postId) {
String redisKey = makeRedisKey(postType, postId);
redisTemplate.opsForValue().increment(redisKey);
redisTemplate.expire(redisKey, TTL);
}

private void decrementLikeCount(PostType postType, Long postId) {
String redisKey = makeRedisKey(postType, postId);
redisTemplate.opsForValue().decrement(redisKey);
redisTemplate.expire(redisKey, TTL);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

증감 연산과 TTL 설정이 원자적이지 않습니다.

Line 91-92와 Line 97-98에서 increment/decrement 연산과 expire 연산이 별도로 실행되어, 두 명령어 사이에 장애가 발생하면 TTL이 설정되지 않은 채로 키가 영구적으로 남게 됩니다. 이는 메모리 누수를 유발할 수 있습니다.

Lua 스크립트를 사용하여 원자적으로 처리하세요:

먼저, 클래스에 Lua 스크립트를 상수로 정의:

private static final String INCR_WITH_EXPIRE_SCRIPT = 
    "local count = redis.call('INCR', KEYS[1]) " +
    "redis.call('EXPIRE', KEYS[1], ARGV[1]) " +
    "return count";

private static final String DECR_WITH_EXPIRE_SCRIPT = 
    "local count = redis.call('DECR', KEYS[1]) " +
    "redis.call('EXPIRE', KEYS[1], ARGV[1]) " +
    "return count";

그리고 메서드를 다음과 같이 수정:

 private void incrementLikeCount(PostType postType, Long postId) {
     String redisKey = makeRedisKey(postType, postId);
-    redisTemplate.opsForValue().increment(redisKey);
-    redisTemplate.expire(redisKey, TTL);
+    redisTemplate.execute(
+        RedisScript.of(INCR_WITH_EXPIRE_SCRIPT, Long.class),
+        Collections.singletonList(redisKey),
+        TTL.getSeconds()
+    );
 }
 
 private void decrementLikeCount(PostType postType, Long postId) {
     String redisKey = makeRedisKey(postType, postId);
-    redisTemplate.opsForValue().decrement(redisKey);
-    redisTemplate.expire(redisKey, TTL);
+    redisTemplate.execute(
+        RedisScript.of(DECR_WITH_EXPIRE_SCRIPT, Long.class),
+        Collections.singletonList(redisKey),
+        TTL.getSeconds()
+    );
 }

필요한 import 추가:

import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCountRedisAdapter.java
around lines 89 to 99, the increment/decrement and expire calls are non-atomic
which can leave keys without TTL; fix by defining Lua scripts as class constants
for INCR+EXPIRE and DECR+EXPIRE, add imports for
org.springframework.data.redis.core.script.RedisScript and
java.util.Collections, and replace the separate
redisTemplate.opsForValue().increment/decrement + expire calls with
redisTemplate.execute(...) invoking the appropriate RedisScript, passing
Collections.singletonList(redisKey) as keys and TTL (as String or Number) as arg
so the increment/decrement and EXPIRE run atomically and the returned count is
handled appropriately.

Comment on lines +33 to +87
@Transactional
public void syncLikeCountsToDB() {
Map<String, Integer> allKeyToLikeCount = redisQueryPort.getAllLikeCounts();
if (allKeyToLikeCount.isEmpty()) {
return;
}

// 타입별 ID 및 좋아요수 맵으로 분리
Map<PostType, List<Long>> typeToIdList = new HashMap<>();
Map<String, Integer> keyToLikeCountToUpdate = new HashMap<>(); // DB에 업데이트할 키만 저장
boolean hasUpdates = false;

for (Map.Entry<String, Integer> entry : allKeyToLikeCount.entrySet()) {
String key = entry.getKey();
Integer likeCount = entry.getValue();

if (likeCount == null || likeCount.intValue() == 0) continue;
// 0보다 큰 값이 있으면 업데이트가 필요함을 기록
hasUpdates = true;

String[] parts = key.split(":");
if (parts.length != 4) continue;
PostType type = PostType.valueOf(parts[2]);
Long postId = Long.valueOf(parts[3]);

typeToIdList.computeIfAbsent(type, k -> new ArrayList<>()).add(postId);
keyToLikeCountToUpdate.put(key, likeCount);
}

if (!hasUpdates) {
return;
}

for (Map.Entry<PostType, List<Long>> entry : typeToIdList.entrySet()) {
PostType type = entry.getKey();
List<Long> ids = entry.getValue();

// 도메인별 id 리스트 중 실제 존재하는 id만 필터링
List<Long> existingIds = postHandler.findPostIdsByIds(type, ids);
if (existingIds.isEmpty()) continue;
// 해당 id와 좋아요 수만 맵으로 생성
Map<Long, Integer> idToLikeCount = existingIds.stream()
.collect(Collectors.toMap(id -> id, id -> {
String redisKey = postLikeCountPrefix + type.name() + ":" + id;
return keyToLikeCountToUpdate.getOrDefault(redisKey, 0);
}));

// 도메인별 벌크 좋아요 DB 업데이트
postHandler.batchUpdateLikeCounts(type, idToLikeCount);
}

// 레디스 리셋
Set<String> updatedKeys = keyToLikeCountToUpdate.keySet();
redisCommandPort.bulkResetLikeCounts(updatedKeys);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

트랜잭션 경계와 Redis 작업의 원자성 문제를 검토하세요.

현재 @Transactional 범위 내에서 DB 업데이트(Line 81)와 Redis 리셋(Line 86)이 모두 수행됩니다. 그러나 Redis는 Spring 트랜잭션 관리 대상이 아니므로, DB 커밋 후 Redis 리셋이 실패하면 다음 동기화 사이클에서 같은 증분 값이 중복 반영될 수 있습니다.

추가로, 동기화 성공/실패에 대한 로깅이 없어 운영 중 문제 발생 시 디버깅이 어려울 수 있습니다.

다음 개선 사항을 검토하세요:

  1. Redis 리셋을 별도 트랜잭션으로 분리하거나, 실패 시 보상 로직 추가
 @Scheduled(fixedRate = 60000)
 @Transactional
 public void syncLikeCountsToDB() {
+    log.info("Starting like count sync to DB");
     Map<String, Integer> allKeyToLikeCount = redisQueryPort.getAllLikeCounts();
     if (allKeyToLikeCount.isEmpty()) {
+        log.debug("No like counts to sync");
         return;
     }
 
     // ... (중간 로직)
 
     for (Map.Entry<PostType, List<Long>> entry : typeToIdList.entrySet()) {
         PostType type = entry.getKey();
         List<Long> ids = entry.getValue();
 
         List<Long> existingIds = postHandler.findPostIdsByIds(type, ids);
         if (existingIds.isEmpty()) continue;
         
         Map<Long, Integer> idToLikeCount = existingIds.stream()
                 .collect(Collectors.toMap(id -> id, id -> {
                     String redisKey = postLikeCountPrefix + type.name() + ":" + id;
                     return keyToLikeCountToUpdate.getOrDefault(redisKey, 0);
                 }));
 
         postHandler.batchUpdateLikeCounts(type, idToLikeCount);
+        log.info("Updated {} like counts for type {}", idToLikeCount.size(), type);
     }
+}
 
+// Redis 리셋을 별도 메서드로 분리 (트랜잭션 커밋 후 실행)
+@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+public void resetRedisAfterCommit(Set<String> updatedKeys) {
-    // 레디스 리셋
-    Set<String> updatedKeys = keyToLikeCountToUpdate.keySet();
-    redisCommandPort.bulkResetLikeCounts(updatedKeys);
+    try {
+        redisCommandPort.bulkResetLikeCounts(updatedKeys);
+        log.info("Successfully reset {} Redis keys", updatedKeys.size());
+    } catch (Exception e) {
+        log.error("Failed to reset Redis keys after DB sync. Manual intervention may be required.", e);
+        // 알림 또는 재시도 로직 추가 고려
+    }
 }
  1. 또는 idempotent 동기화 전략 사용: Redis 값을 증분이 아닌 절대값으로 관리하여 중복 반영 문제를 근본적으로 방지할 수 있습니다.

트랜잭션 분리 전략 또는 idempotent 동기화 로직 구현을 도와드릴까요?

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java
lines 33-87: The current method performs DB updates and then resets Redis inside
the same @Transactional scope, risking duplicate application if Redis reset
fails after DB commit; add clear logging and make Redis operations outside the
DB transaction or add compensation/retry logic. Refactor so the DB bulkUpdate
(postHandler.batchUpdateLikeCounts) remains inside a transactional method that
returns the set of keys/ids successfully updated, then perform
redisCommandPort.bulkResetLikeCounts(updatedKeys) in a separate
non-transactional call (or a post-commit callback) with try/catch, exponential
retries and error logs; alternatively persist a last-synced marker/absolute like
count per post in the DB and use it to make the sync idempotent so repeated
Redis resets/failed attempts cannot double-apply counts.

Comment on lines +39 to +40
@Query("SELECT r.postId FROM RecordJpaEntity r WHERE r.postId IN :postIds")
List<Long> findByPostIds(List<Long> postIds);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

@Param 어노테이션 누락으로 인한 잠재적 문제가 있습니다.

Line 40의 메서드 파라미터에 @Param("postIds") 어노테이션이 누락되어 있습니다. Java 8 이후 -parameters 컴파일 옵션이 활성화되어 있지 않으면 런타임 시 파라미터 이름을 인식하지 못해 오류가 발생할 수 있습니다.

명시적으로 @Param 어노테이션을 추가하세요:

     @Query("SELECT r.postId FROM RecordJpaEntity r WHERE r.postId IN :postIds")
-    List<Long> findByPostIds(List<Long> postIds);
+    List<Long> findByPostIds(@Param("postIds") List<Long> postIds);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Query("SELECT r.postId FROM RecordJpaEntity r WHERE r.postId IN :postIds")
List<Long> findByPostIds(List<Long> postIds);
@Query("SELECT r.postId FROM RecordJpaEntity r WHERE r.postId IN :postIds")
List<Long> findByPostIds(@Param("postIds") List<Long> postIds);
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java
around lines 39-40, the repository method parameter is missing the
@Param("postIds") annotation which can break parameter binding when the
-parameters compiler option is not enabled; add @Param("postIds") to the
List<Long> postIds parameter in the findByPostIds method and ensure the
org.springframework.data.repository.query.Param import is present.

Comment on lines +26 to +27
@Query("SELECT v.postId FROM VoteJpaEntity v WHERE v.postId IN :postIds")
List<Long> findByPostIds(List<Long> ids);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

파라미터 이름 불일치로 인한 런타임 오류가 발생합니다.

Line 27의 메서드 파라미터 이름이 ids이지만 Line 26의 @Query에서는 :postIds를 참조하고 있어 런타임 시 QueryParameterNotFoundException이 발생합니다.

다음과 같이 파라미터 이름을 일치시키세요:

     @Query("SELECT v.postId FROM VoteJpaEntity v WHERE v.postId IN :postIds")
-    List<Long> findByPostIds(List<Long> ids);
+    List<Long> findByPostIds(@Param("postIds") List<Long> postIds);
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java
around lines 26-27, the JPQL uses :postIds but the method parameter is named ids
which causes QueryParameterNotFoundException; fix by making the parameter name
match the query (rename the method parameter to postIds) or explicitly bind it
with @Param("postIds") on the parameter so the query parameter and method
parameter are consistent.

Comment on lines +49 to +56
@BeforeEach
void setUp() {
Alias alias = TestEntityFactory.createLiteratureAlias();
user1 = userJpaRepository.save(TestEntityFactory.createUser(alias));
user2 = userJpaRepository.save(TestEntityFactory.createUser(alias));
book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));
feed = feedJpaRepository.save(TestEntityFactory.createFeed(user1,book, true));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# 1. FeedChangeLikeStatusConcurrencyTest 파일 찾기 및 전체 내용 확인
find . -name "*FeedChangeLikeStatusConcurrencyTest*" -type f

# 2. FeedChangeLikeStatusApiTest 찾기 및 Redis 설정 확인
find . -name "*FeedChangeLikeStatusApiTest*" -type f

# 3. PostLikeService 찾기
find . -name "*PostLikeService*" -type f

Repository: THIP-TextHip/THIP-Server

Length of output: 311


🏁 Script executed:

# Redis 관련 어노테이션 확인 (테스트 파일에서)
rg -n "@.*Redis|redis|@.*Embed" --type java src/test/

Repository: THIP-TextHip/THIP-Server

Length of output: 2744


🏁 Script executed:

# FeedChangeLikeStatusConcurrencyTest의 클래스 어노테이션 및 전체 구조 확인
cat -n src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java | head -100

Repository: THIP-TextHip/THIP-Server

Length of output: 4899


🏁 Script executed:

# FeedChangeLikeStatusConcurrencyTest 전체 파일 읽기
wc -l src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java

Repository: THIP-TextHip/THIP-Server

Length of output: 158


🏁 Script executed:

# FeedChangeLikeStatusApiTest의 Redis 설정 확인 (특히 @BeforeEach, @AfterEach, 클래스 레벨 설정)
cat -n src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java | head -80

Repository: THIP-TextHip/THIP-Server

Length of output: 4167


🏁 Script executed:

# PostLikeService에서 Redis 사용 여부 확인
grep -n "redis\|Redis\|redisTemplate" src/main/java/konkuk/thip/post/application/service/PostLikeService.java

Repository: THIP-TextHip/THIP-Server

Length of output: 806


🏁 Script executed:

# PostLikeService 파일의 전체 구조 확인
wc -l src/main/java/konkuk/thip/post/application/service/PostLikeService.java

Repository: THIP-TextHip/THIP-Server

Length of output: 142


🏁 Script executed:

cat -n src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java | tail -15

Repository: THIP-TextHip/THIP-Server

Length of output: 457


🏁 Script executed:

# @BeforeEach, @AfterEach 메서드 확인
grep -n "@BeforeEach\|@AfterEach\|@Before\|@After" src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java

Repository: THIP-TextHip/THIP-Server

Length of output: 86


Redis 상태를 setUp() 메서드에서 초기화해주세요.

PostLikeService가 Redis를 사용하며, FeedChangeLikeStatusApiTest에서처럼 각 테스트 실행 전에 Redis 상태를 정리해야 합니다. 현재 코드에는 Redis 초기화 로직이 없어 테스트 간 상태 누적으로 인한 간섭이 발생할 수 있습니다.

@BeforeEach
void setUp() {
    // Redis 초기화
    Set<String> keys = redisTemplate.keys("*");
    if (keys != null && !keys.isEmpty()) {
        redisTemplate.delete(keys);
    }
    
    Alias alias = TestEntityFactory.createLiteratureAlias();
    user1 = userJpaRepository.save(TestEntityFactory.createUser(alias));
    user2 = userJpaRepository.save(TestEntityFactory.createUser(alias));
    book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));
    feed = feedJpaRepository.save(TestEntityFactory.createFeed(user1, book, true));
}

RedisTemplate<String, Integer> redisTemplate@Autowired로 주입 받아 사용하세요.

🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java
around lines 49 to 56, the setUp() method does not clear Redis state causing
test interference; inject a RedisTemplate<String, Integer> (e.g. @Autowired)
into the test class and, at the start of setUp(), delete all keys (check for
null/empty) so Redis is cleared before creating Alias/User/Book/Feed, then
proceed with the existing entity creations and saves.


// given
bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));
bookJpaRepository.save(TestEntityFactory.createBookWithISBN("3788954682152"));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Once we find the file, let's examine it
cat -n $(find . -name "FeedCreateApiTest.java" -type f | head -1) | sed -n '60,90p'

Repository: THIP-TextHip/THIP-Server

Length of output: 1543


🏁 Script executed:

# Also check line 161, 209, 242, 282 mentioned in scratchpad
cat -n $(find . -name "FeedCreateApiTest.java" -type f | head -1) | sed -n '155,165p'

Repository: THIP-TextHip/THIP-Server

Length of output: 589


🏁 Script executed:

# Find all ISBN patterns in the test file
rg "ISBN|Isbn|isbn|9788954682152|3788954682152|1788954682152|4788954682152|2788954682152|5788954682152" $(find . -name "FeedCreateApiTest.java" -type f | head -1) -n

Repository: THIP-TextHip/THIP-Server

Length of output: 1583


🏁 Script executed:

# Find Book repository methods and ISBN lookup logic
rg -A 5 "findBy.*Isbn|findBy.*ISBN" . -t java

Repository: THIP-TextHip/THIP-Server

Length of output: 9326


데이터베이스 저장 ISBN과 요청 ISBN이 일치하지 않습니다. 테스트 의도와 맞지 않습니다.

테스트 이름이 "isbn 에 해당하는 책이 DB에 존재할 때 피드를 생성할 수 있다"인데, 실제로는 "3788954682152"를 저장하고 "9788954682152"를 요청하고 있습니다. findByIsbn() 조회 시 저장된 책을 찾을 수 없어 외부 API 호출로 이어집니다.

라인 75를 bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));로 수정하여 저장 ISBN을 요청 ISBN과 일치시키세요. 이는 라인 161, 209, 242, 282에도 동일하게 적용됩니다.

🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java around
line 75 (and similarly at lines 161, 209, 242, 282), the saved test Book
entities use ISBN "3788954682152" which does not match the request ISBN
"9788954682152", causing DB lookups to miss and trigger external API calls;
update the Book saves at those lines to use ISBN "9788954682152" so the
persisted ISBN matches the request ISBN used in the test.

Comment on lines +113 to +115
// // 좋아요 카운트 증가 확인
// RecordJpaEntity updatedRecord = recordJpaRepository.findById(record.getPostId()).orElseThrow();
// assertThat(updatedRecord.getLikeCount()).isEqualTo(1);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# List the repository structure and find the test file
find . -type f -name "RoomPostChangeLikeStatusApiTest.java" 2>/dev/null | head -5

Repository: THIP-TextHip/THIP-Server

Length of output: 152


🏁 Script executed:

# If found, examine the file around the specified lines
if [ -f "src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java" ]; then
  wc -l "src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java"
fi

Repository: THIP-TextHip/THIP-Server

Length of output: 154


🏁 Script executed:

# Search for sync/scheduler patterns in the codebase related to like count or post
rg -n "Scheduled|sync|writeBack|writeback" --type java -i -C 2 | head -100

Repository: THIP-TextHip/THIP-Server

Length of output: 9950


🏁 Script executed:

# Search for PostLikeCount or similar patterns
rg -n "LikeCount|likeCount|PostLike" --type java -A 2 | head -80

Repository: THIP-TextHip/THIP-Server

Length of output: 7497


🏁 Script executed:

# Read the test file at the specified lines
sed -n '110,120p' "src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java"
echo "---"
sed -n '155,160p' "src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java"
echo "---"
sed -n '195,200p' "src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java"
echo "---"
sed -n '236,241p' "src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java"

Repository: THIP-TextHip/THIP-Server

Length of output: 1125


🏁 Script executed:

# Search for the like count sync/update implementation (scheduler, service methods)
rg -n "updatePostLikeCount|syncLikeCount|likeCountSync" --type java -B 2 -A 5

Repository: THIP-TextHip/THIP-Server

Length of output: 4220


🏁 Script executed:

# Search for Redis operations related to likes
rg -n "redis|Redis|cache.*like|like.*cache" --type java -i | grep -i "like\|sync\|schedule" | head -30

Repository: THIP-TextHip/THIP-Server

Length of output: 4795


🏁 Script executed:

# Look at the service that handles like status changes
find . -type f -name "*LikeStatusService*" -o -name "*PostLike*Service*" 2>/dev/null | head -10

Repository: THIP-TextHip/THIP-Server

Length of output: 228


좋아요 카운트 검증 누락으로 인한 테스트 커버리지 감소

Redis 기반 write-back 패턴 도입에 따라 PostLikeCountSyncToDBService의 스케줄러(@Scheduled(fixedRate = 60000))가 60초마다 Redis에서 데이터베이스로 좋아요 카운트를 동기화합니다. 이로 인해 기존 DB 직접 검증 로직(lines 113-115, 157-158, 197-198, 238-239)이 주석 처리되었습니다.

현재 테스트는 postLikeJpaRepository.existsByUserIdAndPostId()로 좋아요 관계 생성만 검증하며, 카운트 증감은 검증하지 않습니다. 다음 중 하나를 선택하여 커버리지를 복구해야 합니다:

  1. Redis 카운트 직접 검증: postLikeCountRedisQueryPort.getLikeCount()로 Redis 상태 확인
  2. 동기화 서비스 명시적 호출: 테스트에서 PostLikeCountSyncToDBService.syncLikeCountsToDB()를 호출한 후 DB 검증
  3. 스케줄러 조작: @DirtiesContext 또는 TaskScheduler 목 처리로 동기화 제어
🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java
around lines 113-115 (and the other commented blocks at 157-158, 197-198,
238-239), the test no longer verifies the like count because DB sync is deferred
to a Redis write-back scheduler; restore coverage by adding an assertion path
that validates counts — either (A) call
postLikeCountRedisQueryPort.getLikeCount(postId) and assert the Redis count, or
(B) invoke PostLikeCountSyncToDBService.syncLikeCountsToDB() in the test and
then load the RecordJpaEntity to assert its likeCount, or (C) mock/control the
scheduler (e.g., use @DirtiesContext or replace TaskScheduler with a testable
mock) to trigger the scheduled sync and then assert DB state; pick one approach
and add the corresponding assertions and any necessary test setup/teardown to
reliably verify the like count.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-393] [refactor] 게시글 좋아요 api 서버 처리율 개선 - Redis 원자적 연산 적용

2 participants