-
Notifications
You must be signed in to change notification settings - Fork 0
[refactor] 게시글 좋아요 api 서버 처리율 개선 - 레디스 비동기 큐잉 적용 #341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
…Counts 배치 좋아요 업데이트 함수 추가 (#338)
findPostIdsByIds,batchUpdateLikeCounts 추가 (#338)
…pdateLikeCounts 배치 좋아요 업데이트 함수 추가 (#338)
…Counts 배치 좋아요 업데이트 함수 추가 (#338)
- 게시글 좋아요 로직이 레디스 원자적 연산으로 수정되면서 게시글 엔티티의 좋아요 정보(likeCount)는 더이상 실시간정보가 아니므로 db 정합성을 확인하는 코드 주석 처리 (#324)
- updateLikeCount()의newLikeCount 매개변수가 추가되면서 테스트코드 수정 (#324)
- 이벤트 발행: Service 계층의 요청을 받아 PostLikeChangedEvent 발행
- 이벤트 리스너: DB 커밋 성공 후 전용 스레드('postLikeAsyncExecutor')에서 후속 작업 처리
리스너 내부에서 Redis Set/Count 갱신, Queue 삽입 순차적으로 실행
…r 구현 (#339) - Redis List를 활용한 비동기 좋아요 큐 Adapter 구현 - 발행 (Enqueue): enqueueFromEvent()에서 PostLikeChangedEvent를 받아 PostLikeQueueMessage로 변환하고, JSON 직렬화 후 Redis List에 LPUSH하여 메시지 삽입. - 소비 (Consume): consumeOne 메서드에서 Redis List의 Tail에서 메시지를 BRPOP으로 꺼내 역직렬화
- @scheduled(fixedDelay = 1000)를 통해 1초마다 트랜잭션을 시작하고 큐 소비 시도 - PostLikeQueueConsumerPort를 통해 메시지를 가져옴, 큐가 비어있지 않은 동안 루프를 반복하여 메시지 일괄처리 -메시지의 action 필드에 따라 PostLikeCommandPort를 호출하여 DB에 INSERT 또는 DELETE 작업 실행
- WorkerThreadConfig에 PostLike 이벤트 전용 실행기 추가 - 비동기 이벤트 리스너 안정화를 위한 CGLIB 프록시 강제 적용 (#339)
- updateLikeCount()의newLikeCount 매개변수가 추가되면서 테스트코드 수정
- TestAsyncConfig에 PostLike 이벤트 전용 실행기 추가 - 비동기 이벤트 리스너 안정화를 위한 CGLIB 프록시 강제 적용 (#339)
…R' into test/#322-k6-feed-like-redis-event # Conflicts: # src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java
# Conflicts: # src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java
Walkthrough이 PR은 게시글 좋아요 API의 처리량을 개선하기 위한 대규모 리팩토링입니다. Redis 기반의 즉시 검증(원자적 연산), 이벤트 기반 비동기 큐잉, 그리고 예약된 DB 동기화를 도입하여 좋아요 기록 영속화를 API 트랜잭션 경로에서 제거합니다. Changes
Sequence DiagramsequenceDiagram
actor Client
participant API as Like API
participant Service as PostLikeService
participant Redis as Redis
participant EventBus as EventBus
participant Queue as Redis Queue
participant Scheduler as LikeRecordSyncService
participant DB as Database
rect rgb(200, 220, 255)
Note over Client,DB: 기존 흐름 (동기적 DB 영속화)
Client->>API: POST /like
API->>DB: Check isLikedByUser (DB Query)
DB-->>API: Result
API->>DB: INSERT/UPDATE PostLike
DB-->>API: Persisted
API-->>Client: 200 OK
end
rect rgb(200, 255, 220)
Note over Client,DB: 신규 흐름 (Redis 비동기 큐잉)
Client->>API: POST /like
API->>Redis: SISMEMBER userId:postId (O(1) Check)
Redis-->>API: Result
API->>Service: updateLikeCount()
Service->>Redis: INCR/DECR count (Atomic)
Redis-->>Service: New Count
Service->>EventBus: PublishEvent (afterCommit)
EventBus->>Redis: Add to Set + Enqueue
Redis-->>EventBus: Done
API-->>Client: 200 OK (즉시 응답)
par Async Background Processing
Scheduler->>Queue: BRPOP (Consume Message)
Queue-->>Scheduler: Message
Scheduler->>DB: INSERT/UPDATE PostLike
DB-->>Scheduler: Persisted
Scheduler->>Redis: bulkResetLikeCounts
Redis-->>Scheduler: Done
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 특별히 주의 깊게 검토해야 할 부분:
Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 Results489 tests 489 ✅ 45s ⏱️ Results for commit 0c0deb0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
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/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java (1)
3-3: 잘못된@Param어노테이션 import를 사용하고 있습니다.Line 3에서
io.lettuce.core.dynamic.annotation.Param을 import하고 있는데, JPA 쿼리 메서드에는org.springframework.data.repository.query.Param을 사용해야 합니다. Lettuce의@Param은 Redis 명령어용이며 JPA와 호환되지 않습니다.다음 diff를 적용하여 올바른 import로 수정하세요:
-import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param;
🟡 Minor comments (5)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java-57-57 (1)
57-57: 테스트 데이터의 ISBN 형식이 유효하지 않습니다.ISBN이 "9788954682152"(13자리)에서 "8788954682152"(12자리)로 변경되었는데, 12자리는 유효한 ISBN 형식이 아닙니다. ISBN-13은 13자리, ISBN-10은 10자리여야 합니다. 선행 "9"가 실수로 제거된 것으로 보입니다.
테스트 데이터라도 실제와 유사한 형식을 사용하는 것이 좋으며, ISBN 검증 로직이 존재할 경우 테스트가 실패할 수 있습니다.
다음 diff를 적용하여 유효한 ISBN 형식으로 복원하세요:
- BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("8788954682152")); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java-62-62 (1)
62-62: ISBN 테스트 데이터가 표준 형식을 따르지 않으며 PR 목적과 무관함이 변경은 다음과 같은 문제가 있습니다:
- 유효한 ISBN-13은 978 또는 979 접두사로 시작해야 합니다. 새 ISBN "6788954682152"는 이 요구사항을 충족하지 않습니다. 기존 값 "9788954682152"는 올바른 형식이었습니다.
- 이 PR의 목적은 게시글 좋아요 API의 Redis 비동기 큐잉 적용인데, 테스트 데이터 변경이 포함된 이유가 불명확합니다.
- 응용 프로그램의 ISBN 검증 로직(
@Pattern(regexp = "\\d{13}"))은 13자리 숫자만 확인하므로 이 형식 위반이 직접적인 검증 실패를 유발하지는 않지만, 표준을 준수하지 않는 테스트 데이터입니다.- 이는 다른 피드 관련 테스트 파일에서 "9788954682152"를 사용하는 패턴과 일관성이 없습니다.
유효한 ISBN-13 형식(978 또는 979 접두사)을 사용하거나, 이 변경의 의도를 설명해야 합니다.
loadtest/feed/feed-like-load-test.js-120-120 (1)
120-120:type필드는 불린값으로 전달해야 합니다Line 120에서
type: 'true'를 문자열로 전달하고 있습니다. API는 Boolean 타입을 기대하므로type: true로 변경해야 합니다.const body = JSON.stringify({ type: true });src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java-208-226 (1)
208-226: 빈 맵에 대한 처리가 필요합니다.
idToLikeCount가 비어있을 때 불필요한 JDBC 배치 연산이 실행됩니다.findByIds처럼 early return을 추가하세요.@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 = ?";src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java-132-150 (1)
132-150: 빈 맵 처리 추가 및 중복 코드 추출을 권장합니다.
VoteCommandPersistenceAdapter와 동일하게 빈 맵에 대한 early return이 필요합니다.또한
batchUpdateLikeCounts구현이VoteCommandPersistenceAdapter와 완전히 동일합니다. 공통 유틸리티 클래스나 상위 추상 클래스로 추출하면 유지보수성이 향상됩니다.@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 = ?";
🧹 Nitpick comments (21)
loadtest/feed/feed-like-load-test.js (3)
6-7: 환경 변수를 통한 설정 가능성 고려
BASE_URL과FEED_ID가 하드코딩되어 있어 다른 환경(스테이징, 프로덕션 등)에서 테스트하기 어렵습니다. k6의__ENV객체를 사용하여 환경 변수로 설정하는 것을 권장합니다.-const BASE_URL = 'http://localhost:8080'; -const FEED_ID = 1; // 테스트할 피드 ID +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; +const FEED_ID = __ENV.FEED_ID || 1; // 테스트할 피드 ID
92-92: 불필요한 배열 길이 조정
tokens배열은 루프에서 최대USERS_COUNT개의 요소만 추가하므로, 이 조건은 절대 참이 될 수 없습니다. 불필요한 코드입니다.- if (tokens.length > USERS_COUNT) tokens.length = USERS_COUNT;
155-161: 외부 의존성 및 import 위치
- Import 문이 파일 하단에 있어 가독성이 저하됩니다. 파일 상단으로 이동하는 것을 권장합니다.
- 외부 URL에서 직접 가져오는 방식은 네트워크 불안정 시 테스트 실패 원인이 될 수 있습니다. 특정 커밋 해시나 버전 태그를 사용하여 재현성을 확보하세요.
+// 파일 상단으로 이동 권장 +import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/2.4.0/dist/bundle.js"; + // feed-like-load-test.js import http from 'k6/http';// 테스트 결과 html 리포트로 저장 -import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js"; export function handleSummary(data) {src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java (1)
26-27: 명시적인@Param어노테이션 추가를 권장합니다.Line 27의 메서드 파라미터에
@Param("postIds")어노테이션이 누락되어 있습니다. 파라미터명이 JPQL 플레이스홀더와 일치하여 작동할 수 있지만, 같은 파일의 다른 쿼리 메서드들(Line 20 참조)과의 일관성 및 명시성을 위해@Param을 추가하는 것이 좋습니다.다음 diff를 적용하세요:
@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);src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java (2)
52-58: 입력 검증 추가를 고려하세요.
postIds가 null이거나 비어있는 경우의 처리가 명시되어 있지 않습니다. 빈 리스트를 조기에 반환하거나, 최소한 JavaDoc으로 동작을 문서화하는 것을 권장합니다.+ /** + * 주어진 postIds 중 실제로 DB에 존재하는 ID만 필터링하여 반환 + * @param type 게시글 타입 + * @param postIds 검증할 게시글 ID 목록 (null이거나 빈 리스트인 경우 빈 리스트 반환) + * @return 실제 존재하는 게시글 ID 목록 + */ public List<Long> findPostIdsByIds(PostType type, List<Long> postIds) { + if (postIds == null || postIds.isEmpty()) { + return List.of(); + } return switch(type) { case FEED -> feedCommandPort.findByIds(postIds); case RECORD -> recordCommandPort.findByIds(postIds); case VOTE -> voteCommandPort.findByIds(postIds); }; }
60-66: 입력 검증 추가를 고려하세요.
idToLikeCount가 null이거나 비어있는 경우의 처리가 명시되어 있지 않습니다. 불필요한 DB 호출을 방지하기 위해 조기 반환을 추가하거나, JavaDoc으로 전제 조건을 문서화하는 것을 권장합니다.+ /** + * 게시글 좋아요 카운트를 배치로 업데이트 + * @param type 게시글 타입 + * @param idToLikeCount 게시글 ID와 새로운 좋아요 수의 매핑 (null이거나 빈 맵인 경우 작업 수행 안 함) + */ public void batchUpdateLikeCounts(PostType type, Map<Long, Integer> idToLikeCount) { + if (idToLikeCount == null || idToLikeCount.isEmpty()) { + return; + } switch(type) { case FEED -> feedCommandPort.batchUpdateLikeCounts(idToLikeCount); case RECORD -> recordCommandPort.batchUpdateLikeCounts(idToLikeCount); case VOTE -> voteCommandPort.batchUpdateLikeCounts(idToLikeCount); } }src/main/java/konkuk/thip/post/adapter/out/event/dto/PostLikeChangedEvent.java (1)
7-13: 필드 유효성 검증 추가를 권장합니다.이벤트 DTO의 필드들(
userId,postId,finalLikeCount)에 대한 유효성 검증이 없어, null 값이나 음수 카운트가 전파될 수 있습니다. 이벤트 기반 아키텍처에서 잘못된 데이터가 큐를 통해 전파되면 다운스트림 컴포넌트에서 데이터 정합성 문제나 예외가 발생할 수 있습니다.다음과 같이 compact constructor에서 검증을 추가하는 것을 고려해보세요:
@Builder public record PostLikeChangedEvent( Long userId, Long postId, boolean isLike, PostType postType, int finalLikeCount -) { +) { + public PostLikeChangedEvent { + if (userId == null || postId == null || postType == null) { + throw new IllegalArgumentException("userId, postId, postType는 null일 수 없습니다"); + } + if (finalLikeCount < 0) { + throw new IllegalArgumentException("finalLikeCount는 음수일 수 없습니다"); + } + } }src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusConcurrencyTest.java (1)
73-96: 실제 경쟁 상태를 테스트하도록 개선이 필요합니다.현재 테스트는 각 스레드가 서로 다른 userId를 사용하므로 진짜 race condition을 테스트하지 않습니다. Redis Set 키가
userId:postId조합이라면, user1과 user2는 완전히 독립적인 키를 사용하여 동시성 충돌이 발생하지 않습니다.실제 동시성 문제를 검증하려면:
- 같은 사용자가 동시에 토글하는 케이스 (최종 상태가 하나만 반영되어야 함)
- 다른 사용자들이 동시에 좋아요하는 케이스 (카운트가 정확히 증가해야 함)
추가 테스트 메서드를 작성하세요:
@Test public void concurrentSameUserLikeToggleTest() throws InterruptedException { // 같은 사용자가 동시에 여러 번 토글 시도 // 최종 상태는 1회만 반영되어야 함 (Redis의 원자적 연산 검증) } @Test public void concurrentMultipleUsersLikeTest() throws InterruptedException { // 100명의 사용자가 동시에 좋아요 // 최종 likeCount가 정확히 100이어야 함 }src/main/java/konkuk/thip/roompost/domain/Record.java (1)
88-90: 파라미터명이 실제 의도와 일치하지 않습니다.
newLikeCount라는 이름은 "업데이트 후의 새로운 값"을 의미하는 것처럼 보이지만, 실제로는 "현재 좋아요 수"를 전달받아postCountService가isLike에 따라 증감 계산을 수행합니다.명확성을 위해 파라미터명을
currentLikeCount로 변경하는 것을 권장합니다. 이는 Vote.java와 Feed.java에도 동일하게 적용되어야 합니다.- public void updateLikeCount(PostCountService postCountService, boolean isLike, int newLikeCount) { - likeCount = postCountService.updatePostLikeCount(isLike, newLikeCount); + public void updateLikeCount(PostCountService postCountService, boolean isLike, int currentLikeCount) { + likeCount = postCountService.updatePostLikeCount(isLike, currentLikeCount); }src/main/java/konkuk/thip/post/application/port/out/PostLikeRedisCommandPort.java (1)
6-11: Redis 연산의 원자성 보장에 대한 문서화가 필요합니다.이 포트의 메서드들이 Redis에서 원자적으로 실행되는지, 아니면 별도 트랜잭션이 필요한지 명확하지 않습니다. 특히:
updateLikeCount와addLikeRecordToSet/removeLikeRecordFromSet이 함께 호출될 때 원자성이 보장되는가?- 여러 메서드 호출 사이에 데이터 불일치가 발생할 수 있는가?
- Redis 파이프라인이나 트랜잭션(MULTI/EXEC)을 사용하는가?
PR 설명에서 "정합성 보장"을 언급했으므로, 인터페이스에 원자성 보장 정책을 JavaDoc으로 명시하는 것을 권장합니다.
+/** + * Redis 기반 좋아요 상태 관리 Command Port + * + * <p>주의: 개별 메서드는 원자적으로 실행되지만, 여러 메서드를 순차 호출할 경우 + * Redis Pipeline 또는 Transaction을 사용하여 원자성을 보장해야 합니다. + * (예: updateLikeCount + addLikeRecordToSet을 함께 실행) + */ public interface PostLikeRedisCommandPort {src/main/java/konkuk/thip/roompost/domain/Vote.java (1)
82-84: 파라미터명이 실제 의도와 일치하지 않습니다.Record.java와 동일한 이슈입니다.
newLikeCount는 "업데이트 후의 새로운 값"처럼 읽히지만, 실제로는 현재 좋아요 수를 받아서isLike플래그에 따라 증감 계산합니다.일관성과 명확성을 위해 Record.java, Vote.java, Feed.java 모두에서 파라미터명을
currentLikeCount로 변경하는 것을 권장합니다.- public void updateLikeCount(PostCountService postCountService, boolean isLike, int newLikeCount) { - likeCount = postCountService.updatePostLikeCount(isLike, newLikeCount); + public void updateLikeCount(PostCountService postCountService, boolean isLike, int currentLikeCount) { + likeCount = postCountService.updatePostLikeCount(isLike, currentLikeCount); }src/main/java/konkuk/thip/post/application/service/PostLikeRecordSyncToDBService.java (1)
21-22: 스케줄링 간격 설정을 외부화하고 조정 고려
fixedDelay = 1000(1초)는 프로덕션 환경에서 너무 짧을 수 있습니다. 특히 큐가 자주 비어있는 경우 불필요한 오버헤드가 발생합니다.다음을 권장합니다:
- 스케줄링 간격을
application.yml로 외부화- 초기 설정값을 실제 부하 패턴에 따라 조정 (예: 5-10초)
- 큐 크기 기반 동적 조정 고려
- @Scheduled(fixedDelay = 1000) + @Scheduled(fixedDelayString = "${post.like.sync.delay:5000}")src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java (3)
63-67: 테스트 환경에서는 허용되나, 프로덕션 코드에서는 사용 금지 패턴입니다.
keys("*")명령은 O(N) 복잡도로 모든 키를 스캔하므로 프로덕션에서는 Redis 블로킹을 유발할 수 있습니다. 테스트 환경에서는 괜찮지만, 테스트용 Redis 유틸리티로 분리하거나@DirtiesContext또는 테스트 전용 Redis 인스턴스 사용을 권장합니다.
106-109: 주석 처리된 코드 정리가 필요합니다.비동기 Write-Back 패턴 도입으로 DB 좋아요 카운트 검증이 불가능해진 것으로 보입니다. 주석 처리된 코드는 제거하거나, 비동기 처리 완료 후 검증하는 별도 통합 테스트로 분리하는 것이 좋습니다.
91-100: 중복되는 트랜잭션 커밋/정리 패턴을 헬퍼 메서드로 추출하는 것을 권장합니다.동일한 패턴이 여러 테스트에서 반복됩니다. 테스트 유지보수성을 위해 공통 헬퍼 메서드로 추출하면 좋겠습니다.
private void commitAndCleanup() { TestTransaction.flagForCommit(); TestTransaction.end(); feedJpaRepository.deleteAllInBatch(); bookJpaRepository.deleteAllInBatch(); userJpaRepository.deleteAllInBatch(); TestTransaction.start(); }src/main/java/konkuk/thip/post/adapter/out/event/PostLikeEventSyncAdapter.java (1)
39-39: 이벤트 리스너 메서드의 가시성을public으로 변경하는 것을 권장합니다.
protected메서드는 Spring의 CGLIB 프록시에서 동작할 수 있지만, 인터페이스 기반 프록시나 향후 설정 변경 시 문제가 발생할 수 있습니다. Spring 이벤트 리스너는 일반적으로public으로 선언합니다.- protected void handlePostLikeChangedEvent(PostLikeChangedEvent event) { + public void handlePostLikeChangedEvent(PostLikeChangedEvent event) {src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java (2)
75-79:FeedChangeLikeStatusApiTest와 동일한 패턴이 반복됩니다.Redis
keys("*")초기화 및 트랜잭션 커밋/정리 패턴이FeedChangeLikeStatusApiTest와 중복됩니다. 테스트 베이스 클래스나 공통 유틸리티로 추출하면 테스트 코드 유지보수가 용이해집니다.
125-128: 주석 처리된 DB 좋아요 카운트 검증 코드를 정리하세요.
FeedChangeLikeStatusApiTest와 마찬가지로, 비동기 Write-Back 패턴으로 인해 즉시 DB 검증이 불가능합니다. 주석 코드를 제거하거나 별도 통합 테스트로 분리하세요.src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java (1)
32-38:fixedRate대신fixedDelay사용을 권장합니다.
fixedRate는 이전 작업 완료 여부와 관계없이 일정 간격으로 실행되므로, 대량 데이터 처리 시 작업이 겹칠 수 있습니다.PostLikeRecordSyncToDBService와 동일하게fixedDelay를 사용하면 이전 작업 완료 후 대기 시간을 보장합니다.- @Scheduled(fixedRate = 60000)// 1분마다 실행 + @Scheduled(fixedDelay = 60000) // 이전 작업 완료 후 1분 대기src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)
101-119: 빈 맵에 대한 조기 반환 처리를 추가하세요.
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/post/adapter/out/persistence/PostLikeRedisAdapter.java (1)
117-127:increment와expire가 원자적으로 실행되지 않습니다.두 연산 사이에 장애가 발생하면 TTL 없이 키가 남을 수 있습니다. Lua 스크립트를 사용하거나 Redis 7+의
INCR와EXPIRE조합 대신INCRBY ... EXAT옵션을 고려하세요.
📜 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.
📒 Files selected for processing (42)
loadtest/feed/feed-like-load-test.js(1 hunks)src/main/java/konkuk/thip/config/RedisConfig.java(2 hunks)src/main/java/konkuk/thip/config/WorkerThreadConfig.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/event/PostLikeEventSyncAdapter.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/event/dto/PostLikeChangedEvent.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueueRedisAdapter.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeRedisAdapter.java(1 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeEventCommandPort.java(1 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeQueueCommandPort.java(1 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeQueueConsumerPort.java(1 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeRedisCommandPort.java(1 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeRedisQueryPort.java(1 hunks)src/main/java/konkuk/thip/post/application/port/out/dto/PostLikeQueueMessage.java(1 hunks)src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java(1 hunks)src/main/java/konkuk/thip/post/application/service/PostLikeRecordSyncToDBService.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/config/TestAsyncConfig.java(2 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java(7 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(10 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/post/application/port/out/PostLikeRedisQueryPort.javasrc/main/java/konkuk/thip/post/application/port/out/PostLikeEventCommandPort.javasrc/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.javasrc/main/java/konkuk/thip/post/application/port/out/PostLikeRedisCommandPort.javasrc/main/java/konkuk/thip/post/application/service/PostLikeService.javasrc/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.javasrc/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.javasrc/main/java/konkuk/thip/post/application/port/out/PostLikeQueueCommandPort.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.javasrc/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.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
📚 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-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 (9)
src/main/java/konkuk/thip/post/application/port/out/PostLikeEventCommandPort.java (1)
src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java (1)
PostLikeCommandPort(5-10)
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/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/main/java/konkuk/thip/post/application/service/PostLikeRecordSyncToDBService.java (1)
src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java (1)
Service(20-88)
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)
src/main/java/konkuk/thip/post/application/service/PostLikeCountSyncToDBService.java (1)
src/main/java/konkuk/thip/post/application/service/PostLikeRecordSyncToDBService.java (1)
Service(13-40)
src/main/java/konkuk/thip/config/WorkerThreadConfig.java (1)
src/test/java/konkuk/thip/config/TestAsyncConfig.java (1)
EnableAsync(13-37)
⏰ 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 (26)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedApiTest.java (1)
62-62: PR 목표와 무관한 변경사항을 확인해주세요.이 테스트는 피드 저장 기능에 대한 것으로, PR의 주요 목표인 좋아요 API의 Redis 비동기 큐잉 리팩토링과 관련이 없습니다. ISBN 변경(9788954682152 → 9788954682151)이 의도적인 것인지 확인이 필요합니다.
- 이 변경이 의도적이라면, 별도의 커밋으로 분리하거나 커밋 메시지에 변경 이유를 명시하는 것이 좋습니다.
- 만약 실수로 포함된 변경이라면, 제거하거나 별도 PR로 분리해주세요.
src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java (1)
61-61: 테스트 데이터 ISBN 변경이 PR 목표와 관련 없어 보입니다.이 PR의 핵심 목표는 Redis 기반 비동기 큐잉을 통한 좋아요 API 성능 개선인데, 테스트 데이터의 ISBN 값이 "9788954682152"에서 "7788954682152"로 변경되었습니다.
추가로, 새로운 ISBN "7788954682152"는 유효한 ISBN-13 형식(978 또는 979로 시작)을 따르지 않습니다. 테스트 데이터임을 명확히 하기 위한 의도적 변경인지, 아니면 다른 이유가 있는지 확인이 필요합니다.
다른 테스트 파일들(FeedCreateApiTest, FeedChangeSavedApiTest)에서도 유사한 변경이 있다고 하니, 혹시 테스트 데이터 표준화 작업의 일환인가요? 이 변경의 목적과 PR에 포함된 이유를 명확히 해주시면 좋을 것 같습니다.
loadtest/feed/feed-like-load-test.js (3)
14-31: LGTM!커스텀 메트릭 정의와 에러 코드 매핑이 잘 구성되어 있습니다. 특정 에러 코드별 카운터를 통해 실패 원인 분석이 용이합니다.
33-45: LGTM!방어적 코딩이 잘 적용되어 있으며, JSON 파싱 실패 시 안전한 기본값을 반환합니다.
47-64: LGTM!
per-vu-iterationsexecutor를 사용한 시나리오 설계가 적절합니다. 모든 VU가 정확히 1회 실행되어 동시 요청 시뮬레이션에 적합하며, 임계값 설정도 합리적입니다.src/main/java/konkuk/thip/config/RedisConfig.java (1)
33-40: 좋아요 카운트용 Integer 전용 RedisTemplate 설정이 적절합니다.
GenericToStringSerializer<Integer>를 사용하여 정수값을 문자열로 직렬화하는 방식은 Redis 카운터 연산에 적합하며, 가독성도 좋습니다. Redis의 INCR/DECR 명령어와도 호환되는 구성입니다.src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (1)
32-33: 배치 조회 메서드가 올바르게 구현되었습니다.
@Param어노테이션과 파라미터명이 정확히 일치하며, JPQL의 IN 절을 사용한 배치 조회 패턴이 적절합니다. 다른 repository들도 이 구현 방식을 따르도록 해야 합니다.src/main/java/konkuk/thip/post/application/port/out/PostLikeEventCommandPort.java (1)
5-9: 이벤트 발행 포트 설계가 적절합니다.메서드 시그니처가 명확하며 이벤트 기반 아키텍처 패턴에 적합합니다.
isLike플래그와finalLikeCount를 통해 최종 정합성을 보장하는 설계가 잘 반영되어 있습니다.src/main/java/konkuk/thip/config/WorkerThreadConfig.java (1)
16-16:proxyTargetClass = true설정이 적절합니다.CGLIB 기반 프록시로 변경하여 인터페이스 없이도
@Async메서드 호출이 가능하도록 구성되었습니다. 테스트 설정과도 일관성이 유지됩니다.src/test/java/konkuk/thip/config/TestAsyncConfig.java (1)
13-13: 테스트용 비동기 설정이 올바르게 구성되었습니다.
proxyTargetClass = true설정이 프로덕션 설정과 일치하며,SyncTaskExecutor를 사용하여 테스트 환경에서 동기 실행을 강제함으로써 테스트의 결정성과 안정성을 보장합니다.Also applies to: 28-31
src/test/java/konkuk/thip/roompost/domain/VoteTest.java (1)
145-180: 테스트 시그니처 업데이트가 올바르게 적용되었습니다.
updateLikeCount메서드의 세 번째 파라미터로vote.getLikeCount()를 전달하여 현재 좋아요 수를 명시적으로 제공하는 것은 Redis 기반 동기화 아키텍처에서 명시적 상태 전달을 위한 적절한 변경입니다.다만, 도메인 메서드의 파라미터명이
newLikeCount인데 실제로는currentLikeCount를 전달하고 있어 혼란스러울 수 있습니다. 이는 Vote.java, Record.java 도메인 파일에서 파라미터명을currentLikeCount로 변경하면 더 명확할 것입니다.src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1)
27-27: 배치 업데이트 메서드가 적절하게 추가되었습니다.Redis 비동기 큐잉에서 워커가 여러 게시글의 좋아요 수를 한 번에 DB에 동기화하기 위한
batchUpdateLikeCounts메서드는 성능 최적화에 효과적입니다. Write-Back 패턴 구현에 적합한 설계입니다.src/main/java/konkuk/thip/post/application/port/out/PostLikeQueueConsumerPort.java (1)
1-8: LGTM!큐 소비를 위한 명확하고 간결한 인터페이스입니다.
Optional반환 타입을 사용하여 메시지가 없는 경우를 적절히 처리하고 있습니다.src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java (2)
31-31: LGTM!배치 좋아요 카운트 업데이트 메서드가 적절하게 추가되었습니다. Redis에서 DB로의 Write-Back 동기화 패턴을 효율적으로 지원합니다. 프로젝트 학습 내용에 따라 CommandPort에 적합한 메서드입니다.
20-20: 이 메서드는 의도적으로List<Long>을 반환하고 있습니다.findByIds는 입력된 ID 목록 중 데이터베이스에 실제로 존재하는 ID들을 필터링하여 반환하는 메서드로,PostHandler.findPostIdsByIds()메서드에서 사용 중입니다. 유사한 목적으로FeedCommandPort,VoteCommandPort도 동일하게List<Long>을 반환하고 있으며, 이는 엔티티 조회가 아닌 ID 존재 여부 검증을 위한 설계입니다.Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java (2)
57-57: LGTM!배치 좋아요 카운트 업데이트 메서드가 일관되게 추가되었습니다.
RecordCommandPort및FeedCommandPort와 동일한 패턴을 따릅니다.
25-25: 메서드 반환 타입은 정확함 - 재검토 불필요
findByIds메서드는 List을 반환하는 것이 올바른 설계입니다. 이 메서드는 벌크 엔티티 조회가 아닌 ID 검증/필터링 목적으로 설계되었습니다. FeedCommandPort, RecordCommandPort, VoteCommandPort 모두 동일한 패턴을 따르고 있으며, 데이터베이스에 존재하는 ID들을 필터링하여 반환합니다. 엔티티 객체 조회가 필요한 경우findById(Long id)같은 별도의 메서드를 사용합니다.Likely an incorrect or invalid review comment.
src/test/java/konkuk/thip/roompost/domain/RecordTest.java (1)
145-176: LGTM!테스트가 새로운
updateLikeCount시그니처에 맞춰 올바르게 업데이트되었습니다. 특히 Line 176의 언더플로우 테스트에서record.getLikeCount()-1을 전달하여 검증 로직을 적절히 테스트하고 있습니다.src/main/java/konkuk/thip/feed/domain/Feed.java (1)
158-160: Redis 기반 좋아요 수 업데이트는 설계 의도대로 구현됨
updateLikeCount메서드 변경사항을 검증했습니다. PostLikeService에서는 Redis 캐시를 통해 최신 좋아요 수를 조회한 후 정확하게 전달하고 있으며, 캐시 미스 시 데이터베이스 값을 fallback으로 사용합니다. PostCountService는 언더플로우 검증을 수행하므로 데이터 일관성이 보장됩니다.src/main/java/konkuk/thip/post/domain/CountUpdatable.java (1)
8-10: 인터페이스 변경사항 확인 완료
CountUpdatable인터페이스 변경이 구현체에서 올바르게 반영되었습니다:
updateLikeCount메서드의newLikeCount파라미터 추가 - Feed에서 정상 구현됨 (lines 157-160)getLikeCount()메서드 - Feed의 @Getter 어노테이션에 의해 자동 생성됨 (likeCount 필드에서)Feed 클래스가 유일한 직접 구현체이며, 모든 인터페이스 계약을 정상적으로 이행하고 있습니다.
src/main/java/konkuk/thip/post/application/port/out/PostLikeRedisQueryPort.java (1)
6-10: LGTM!인터페이스 설계가 CQRS QueryPort 컨벤션을 잘 따르고 있습니다. Redis 기반 좋아요 조회를 위한 책임이 명확하게 정의되어 있습니다.
getAllLikeCounts()의 반환 타입이Map<String, Integer>인 것은 Redis 키 형식(예:"FEED:123")을 그대로 사용하기 위한 것으로 보입니다. 필요시 호출부에서 파싱하여 사용하면 됩니다.src/test/java/konkuk/thip/feed/domain/FeedTest.java (1)
225-264: LGTM!
updateLikeCountAPI 시그니처 변경에 맞춰 테스트가 올바르게 업데이트되었습니다. 증가, 감소, 언더플로우 케이스가 모두 적절히 검증되고 있습니다.src/main/java/konkuk/thip/post/application/service/PostLikeService.java (2)
51-52: 알림 전송 코드가 주석 처리되어 있습니다.좋아요 푸시 알림 기능이 비활성화되어 있습니다. 의도적인 변경이라면 코드를 완전히 제거하거나, 임시 조치라면 TODO 주석과 함께 관련 이슈를 추적할 수 있도록 해주세요.
57-64: Redis 기반 비동기 아키텍처가 올바르게 구현되었습니다.
PostLikeEventSyncAdapter에서 이벤트 발행 시@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)어노테이션을 통해 트랜잭션 커밋 후에만 이벤트 리스너가 실행되도록 보장합니다. 추가로@Async어노테이션으로 비동기 처리를 통해 도메인 상태 갱신(Redis 카운트 주입), 이벤트 발행, 후속 처리(Redis 업데이트, 큐 삽입)의 순서가 안전하게 구분되어 있습니다.src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)
67-72: LGTM!null/empty 입력에 대한 방어 로직이 적절히 구현되어 있습니다.
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueueRedisAdapter.java (1)
27-42: 이벤트 큐잉 구현이 적절합니다.leftPush/rightPop 패턴으로 FIFO 순서가 보장되며, 직렬화 실패 시 RuntimeException으로 명확히 실패를 전파합니다.
| /** | ||
| * PostLike 이벤트 전용 실행기 | ||
| * - 좋아요/취소 시 Redis Set/Count 갱신 및 큐 발행 처리 | ||
| * - 순간적인 높은 트래픽에 대비하여 큐 용량 확보 | ||
| */ | ||
| @Bean(name = "postLikeAsyncExecutor") | ||
| public Executor postLikeAsyncExecutor() { | ||
| ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | ||
| executor.setCorePoolSize(5); | ||
| executor.setMaxPoolSize(30); | ||
| executor.setQueueCapacity(300); | ||
| executor.setThreadNamePrefix("like-async-"); | ||
| executor.setWaitForTasksToCompleteOnShutdown(true); | ||
| executor.initialize(); | ||
| return executor; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
종료 시 태스크 완료 대기 시간 및 거부 정책을 명시적으로 설정하세요.
현재 구성에 두 가지 잠재적 데이터 유실 위험이 있습니다:
-
awaitTerminationSeconds미설정:waitForTasksToCompleteOnShutdown만으로는 무한정 대기할 수 있습니다. 애플리케이션 종료 시 작업 완료를 위한 최대 대기 시간을 설정해야 합니다. -
RejectedExecutionHandler미설정: 큐가 가득 차고 최대 스레드 수에 도달하면 기본AbortPolicy가 작동하여 새 작업이 예외와 함께 거부됩니다. 좋아요 이벤트 유실을 방지하려면CallerRunsPolicy를 사용하는 것을 권장합니다.
다음 diff를 적용하세요:
@Bean(name = "postLikeAsyncExecutor")
public Executor postLikeAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(30);
executor.setQueueCapacity(300);
executor.setThreadNamePrefix("like-async-");
executor.setWaitForTasksToCompleteOnShutdown(true);
+ executor.setAwaitTerminationSeconds(60);
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/config/WorkerThreadConfig.java around lines 57 to
72, set an explicit shutdown wait time and a rejection policy to avoid silent
task loss: call executor.setAwaitTerminationSeconds(...) with a sensible timeout
(e.g., 30–60 seconds) and set the rejected execution handler to CallerRunsPolicy
via executor.setRejectedExecutionHandler(new
java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()), then reinitialize;
this ensures the pool will wait a bounded time for in-flight tasks and will
execute rejected submissions on the calling thread instead of aborting.
| Long save(Feed feed); | ||
| Long update(Feed feed); | ||
| Optional<Feed> findById(Long id); | ||
| List<Long> findByIds(List<Long> ids); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find and examine FeedCommandPort interface
find . -name "FeedCommandPort.java" -type fRepository: THIP-TextHip/THIP-Server
Length of output: 142
🏁 Script executed:
#!/bin/bash
# Search for FeedCommandPersistenceAdapter
find . -name "FeedCommandPersistenceAdapter.java" -type fRepository: THIP-TextHip/THIP-Server
Length of output: 159
🏁 Script executed:
#!/bin/bash
# Look for Feed entity definition
find . -path "*/feed/*" -name "Feed.java" -type fRepository: THIP-TextHip/THIP-Server
Length of output: 117
🏁 Script executed:
#!/bin/bash
# Search for usages of findByIds method
rg "findByIds" --type javaRepository: THIP-TextHip/THIP-Server
Length of output: 1691
🏁 Script executed:
#!/bin/bash
# Examine FeedCommandPort interface
cat -n ./src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.javaRepository: THIP-TextHip/THIP-Server
Length of output: 1179
🏁 Script executed:
#!/bin/bash
# Examine FeedCommandPersistenceAdapter implementation
cat -n ./src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.javaRepository: THIP-TextHip/THIP-Server
Length of output: 7510
🏁 Script executed:
#!/bin/bash
# Check context where findByIds is called in PostHandler
rg -A 5 -B 5 "findByIds" ./src/main/java/konkuk/thip/post/application/service/handler/PostHandler.javaRepository: THIP-TextHip/THIP-Server
Length of output: 508
🏁 Script executed:
#!/bin/bash
# Check UserCommandPort for comparison (it returns entities)
cat -n ./src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.javaRepository: THIP-TextHip/THIP-Server
Length of output: 507
findByIds 메서드의 반환 타입을 수정하세요.
List<Long> findByIds(List<Long> ids)는 프로젝트의 CQRS CommandPort 컨벤션과 맞지 않습니다. CommandPort의 findByXXX 메서드는 도메인 엔티티를 반환해야 하는데(예: UserCommandPort의 Map<Long, User> findByIds), 현재 구현은 ID만 반환합니다.
다음 중 하나로 수정하세요:
- 메서드명을
findIdsByIds등으로 변경하거나 - 반환 타입을
List<Feed>로 변경하고 호출 코드 조정
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
around line 17, the method signature List<Long> findByIds(List<Long> ids)
violates the CommandPort CQRS convention (should return domain entities); either
rename it to something like findIdsByIds to reflect it returns IDs, or change
its return type to List<Feed> and update all callers to expect Feed entities
(and adjust imports, tests, and any mapping logic where IDs were used); pick one
approach and make consistent changes across the codebase so CommandPort methods
return domain entities as required.
| @Async("postLikeAsyncExecutor") | ||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| protected void handlePostLikeChangedEvent(PostLikeChangedEvent event) { | ||
| // 1. Redis Set 기록 (좋아요 상태 반영) | ||
| if (event.isLike()) { | ||
| postLikeRedisCommandPort.addLikeRecordToSet(event.userId(), event.postId()); | ||
| } else { | ||
| postLikeRedisCommandPort.removeLikeRecordFromSet(event.userId(), event.postId()); | ||
| } | ||
|
|
||
| // 2. Redis 카운트 갱신 (INCR/DECR) | ||
| postLikeRedisCommandPort.updateLikeCount(event.postType(), event.postId(), | ||
| event.finalLikeCount(), event.isLike()); | ||
|
|
||
| // 3. 비동기 DB 기록 메시지를 큐에 삽입 (Redis List LPUSH) | ||
| try { | ||
| postLikeQueueCommandPort.enqueueFromEvent(event); | ||
| } catch (Exception e) { | ||
| log.error("CRITICAL: Failed to publish Like Record to Queue. Event Data: {}", event, e); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
부분 실패 시 데이터 정합성 문제가 발생할 수 있습니다.
현재 구현에서 Redis Set 업데이트(1단계) 또는 카운트 갱신(2단계)이 성공한 후 큐 삽입(3단계)이 실패하면, 사용자에게는 좋아요가 반영된 것처럼 보이지만 DB에는 영구 기록되지 않습니다.
최소한의 복구 전략으로:
- 재시도 로직 추가 (예: Spring Retry)
- 또는 Dead Letter Queue 패턴 적용
- 또는 실패한 이벤트를 별도 저장소에 기록하여 수동 복구 지원
try {
postLikeQueueCommandPort.enqueueFromEvent(event);
} catch (Exception e) {
log.error("CRITICAL: Failed to publish Like Record to Queue. Event Data: {}", event, e);
+ // 복구를 위해 실패한 이벤트를 별도로 저장하거나 재시도 큐에 추가
+ // failedEventRepository.save(event);
}| } catch (Exception e) { | ||
| log.error("Failed to deserialize like record: {}", jsonRecord, e); | ||
| // 디시리얼라이즈 실패 메시지는 무시하고 다음 메시지를 시도하거나, DLQ로 보내야 함. | ||
| // 여기서는 유실을 가정하고 Optional.empty() 반환 | ||
| return Optional.empty(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
역직렬화 실패 시 메시지 유실 문제가 있습니다.
주석에서 DLQ(Dead Letter Queue) 처리의 필요성을 언급하고 있으나 실제 구현이 없습니다. 역직렬화 실패 메시지를 별도 키(예: post-like-queue:dlq)에 보관하여 추후 분석 및 복구가 가능하도록 해주세요. 좋아요 기록의 정합성 보장을 위해 중요합니다.
try {
PostLikeQueueMessage message = objectMapper.readValue(jsonRecord, PostLikeQueueMessage.class);
return Optional.of(message);
} catch (Exception e) {
log.error("Failed to deserialize like record: {}", jsonRecord, e);
- // 디시리얼라이즈 실패 메시지는 무시하고 다음 메시지를 시도하거나, DLQ로 보내야 함.
- // 여기서는 유실을 가정하고 Optional.empty() 반환
+ // DLQ에 실패 메시지 저장
+ stringRedisTemplate.opsForList().leftPush(postLikeQueuePrefix + ":dlq", jsonRecord);
return Optional.empty();
}📝 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("Failed to deserialize like record: {}", jsonRecord, e); | |
| // 디시리얼라이즈 실패 메시지는 무시하고 다음 메시지를 시도하거나, DLQ로 보내야 함. | |
| // 여기서는 유실을 가정하고 Optional.empty() 반환 | |
| return Optional.empty(); | |
| } | |
| } catch (Exception e) { | |
| log.error("Failed to deserialize like record: {}", jsonRecord, e); | |
| // DLQ에 실패 메시지 저장 | |
| stringRedisTemplate.opsForList().leftPush(postLikeQueuePrefix + ":dlq", jsonRecord); | |
| return Optional.empty(); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueueRedisAdapter.java
around lines 56 to 61, the catch block currently logs deserialization failures
and drops the message; instead, push the failed jsonRecord into a Redis DLQ key
(e.g., "post-like-queue:dlq") before returning Optional.empty(). Implement this
by using the existing Redis client/redisTemplate (or inject StringRedisTemplate
if not present), call the appropriate leftPush/rightPush to store the raw
jsonRecord and include a log.info/log.error that records the DLQ key and
original payload; ensure any Redis errors are caught and logged but do not
rethrow so the method still returns Optional.empty().
| public Map<String, Integer> getAllLikeCounts() { | ||
| // 모든 좋아요 카운트 키 검색 | ||
| Set<String> keys = redisTemplate.keys(postLikeCountPrefix + "*"); | ||
| if (keys == null || keys.isEmpty()) { | ||
| return Collections.emptyMap(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 환경에서 keys() 명령어 사용은 위험합니다.
keys()는 O(N) 복잡도로 Redis를 블로킹하며, 키가 많을 경우 심각한 성능 저하를 유발합니다. SCAN 명령어로 대체하여 점진적으로 키를 조회해야 합니다.
@Override
public Map<String, Integer> getAllLikeCounts() {
- // 모든 좋아요 카운트 키 검색
- Set<String> keys = redisTemplate.keys(postLikeCountPrefix + "*");
- if (keys == null || keys.isEmpty()) {
- return Collections.emptyMap();
- }
+ Set<String> keys = new HashSet<>();
+ ScanOptions scanOptions = ScanOptions.scanOptions()
+ .match(postLikeCountPrefix + "*")
+ .count(100)
+ .build();
+ try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) {
+ while (cursor.hasNext()) {
+ keys.add(cursor.next());
+ }
+ }
+ if (keys.isEmpty()) {
+ return Collections.emptyMap();
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public Map<String, Integer> getAllLikeCounts() { | |
| // 모든 좋아요 카운트 키 검색 | |
| Set<String> keys = redisTemplate.keys(postLikeCountPrefix + "*"); | |
| if (keys == null || keys.isEmpty()) { | |
| return Collections.emptyMap(); | |
| } | |
| public Map<String, Integer> getAllLikeCounts() { | |
| Set<String> keys = new HashSet<>(); | |
| ScanOptions scanOptions = ScanOptions.scanOptions() | |
| .match(postLikeCountPrefix + "*") | |
| .count(100) | |
| .build(); | |
| try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) { | |
| while (cursor.hasNext()) { | |
| keys.add(cursor.next()); | |
| } | |
| } | |
| if (keys.isEmpty()) { | |
| return Collections.emptyMap(); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeRedisAdapter.java
around lines 46 to 51, the method uses redisTemplate.keys(postLikeCountPrefix +
"*") which is blocking and unsafe in production; replace it with a non-blocking
SCAN-based iteration (Redis SCAN via RedisTemplate.scan or RedisConnection.scan
with a ScanOptions match pattern) to progressively fetch matching keys, iterate
the Cursor, collect key names, read their values and aggregate into the Map, and
ensure the cursor/connection is closed after use; also handle empty results and
any Redis exceptions gracefully.
| public void syncRecordsFromQueue() { | ||
| Optional<PostLikeQueueMessage> messageOptional = postLikeQueueConsumerPort.consumeOne(); | ||
|
|
||
| while (messageOptional.isPresent()) { | ||
| PostLikeQueueMessage command = messageOptional.get(); | ||
|
|
||
| // DB에 실제 INSERT/DELETE 작업 수행 | ||
| if ("SAVE".equals(command.action())) { | ||
| postLikeCommandPort.save(command.userId(), command.postId(), command.postType()); | ||
| } else if ("DELETE".equals(command.action())) { | ||
| postLikeCommandPort.delete(command.userId(), command.postId()); | ||
| } | ||
|
|
||
| // 다음 메시지 가져오기 | ||
| messageOptional = postLikeQueueConsumerPort.consumeOne(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
트랜잭션 경계 및 에러 처리 개선 필요
현재 구현에 여러 심각한 문제가 있습니다:
Critical:
- 무제한 트랜잭션: 큐에 메시지가 많을 경우 하나의 긴 트랜잭션에서 모두 처리되어 락 경합 및 성능 저하 발생
- 에러 처리 부재: 하나의 메시지 처리 실패 시 전체 배치가 롤백됨
- 배치 크기 제한 없음: 메모리 및 성능 문제 가능성
Major:
4. 문자열 비교: "SAVE"/"DELETE" 대신 enum 사용 권장
5. 관찰성 부족: 로깅/메트릭 없음
다음과 같은 개선을 제안합니다:
@Scheduled(fixedDelay = 1000)
- @Transactional
public void syncRecordsFromQueue() {
+ int batchSize = 100; // 설정 가능하게 만들기
+ int processedCount = 0;
+
Optional<PostLikeQueueMessage> messageOptional = postLikeQueueConsumerPort.consumeOne();
- while (messageOptional.isPresent()) {
+ while (messageOptional.isPresent() && processedCount < batchSize) {
- PostLikeQueueMessage command = messageOptional.get();
-
- // DB에 실제 INSERT/DELETE 작업 수행
- if ("SAVE".equals(command.action())) {
- postLikeCommandPort.save(command.userId(), command.postId(), command.postType());
- } else if ("DELETE".equals(command.action())) {
- postLikeCommandPort.delete(command.userId(), command.postId());
+ try {
+ processOneMessage(messageOptional.get());
+ processedCount++;
+ } catch (Exception e) {
+ log.error("Failed to process like message: {}", messageOptional.get(), e);
+ // DLQ로 이동 또는 재시도 로직 추가
}
// 다음 메시지 가져오기
messageOptional = postLikeQueueConsumerPort.consumeOne();
}
+
+ if (processedCount > 0) {
+ log.info("Processed {} like records from queue", processedCount);
+ }
+ }
+
+ @Transactional
+ private void processOneMessage(PostLikeQueueMessage command) {
+ // enum 사용 권장
+ if ("SAVE".equals(command.action())) {
+ postLikeCommandPort.save(command.userId(), command.postId(), command.postType());
+ } else if ("DELETE".equals(command.action())) {
+ postLikeCommandPort.delete(command.userId(), command.postId());
+ }
}각 메시지를 별도 트랜잭션으로 처리하여 격리성을 확보하고, 에러 처리 및 배치 크기 제한을 추가합니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/post/application/service/PostLikeRecordSyncToDBService.java
around lines 23-38, refactor the loop so each queue message is processed in its
own transaction and add per-message error handling and a batch-size cap: consume
up to a configurable maxBatch (e.g. loop counter) instead of unlimited, for each
message invoke a transactional helper (or annotate a small private method
@Transactional) that switches on an Action enum (replace raw "SAVE"/"DELETE"
string checks by parsing into an enum) and performs save/delete, wrap that call
in try/catch to log the error/metric and continue to the next message (do not
let one failure roll back other messages), and ensure basic logging/metrics are
emitted for success and failure.
| if ("SAVE".equals(command.action())) { | ||
| postLikeCommandPort.save(command.userId(), command.postId(), command.postType()); | ||
| } else if ("DELETE".equals(command.action())) { | ||
| postLikeCommandPort.delete(command.userId(), command.postId()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
액션 타입을 enum으로 변경 권장
문자열 리터럴 "SAVE"와 "DELETE" 대신 enum을 사용하면 타입 안전성과 유지보수성이 향상됩니다. 또한 알 수 없는 액션에 대한 처리도 추가해야 합니다.
public enum PostLikeAction {
SAVE, DELETE
}
// PostLikeQueueMessage에서 사용
public record PostLikeQueueMessage(
Long userId,
Long postId,
PostLikeAction action, // String -> enum
PostType postType
) {}
// 처리 로직
switch (command.action()) {
case SAVE -> postLikeCommandPort.save(command.userId(), command.postId(), command.postType());
case DELETE -> postLikeCommandPort.delete(command.userId(), command.postId());
}🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/post/application/service/PostLikeRecordSyncToDBService.java
around lines 30-34, replace the string literal action checks with a
PostLikeAction enum to make actions type-safe: add a PostLikeAction enum with
SAVE and DELETE, update PostLikeQueueMessage (and any deserialization/mapping)
to use PostLikeAction instead of String, change the if/else to a switch on
command.action() calling postLikeCommandPort.save(...) for SAVE and
postLikeCommandPort.delete(...) for DELETE, and add a default branch that logs
or throws for unknown/null actions to ensure explicit handling.
| @Query("SELECT v.postId FROM VoteJpaEntity v WHERE v.postId IN :postIds") | ||
| List<Long> findByPostIds(List<Long> ids); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JPQL 파라미터명과 메서드 파라미터명이 불일치하여 런타임 오류가 발생합니다.
Line 26의 JPQL은 :postIds를 참조하지만, Line 27의 메서드 파라미터명은 ids로 선언되어 있습니다. 이로 인해 메서드 호출 시 org.springframework.dao.InvalidDataAccessApiUsageException이 발생합니다. 또한 @Param 어노테이션이 누락되어 있습니다.
다음 diff를 적용하여 수정하세요:
@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
and lacks @Param, causing a runtime InvalidDataAccessApiUsageException; fix it
by either renaming the method parameter to postIds or annotate the existing
parameter with @Param("postIds") so the JPQL parameter and method parameter
match, then run tests to verify the query executes without error.
| @Test | ||
| public void concurrentLikeToggleTest() throws InterruptedException { | ||
|
|
||
| int threadCount = 2; | ||
| int repeat = 10; // 스레드별 몇 번 반복할지 | ||
| ExecutorService executor = Executors.newFixedThreadPool(threadCount); | ||
| CountDownLatch latch = new CountDownLatch(threadCount * repeat); | ||
|
|
||
| AtomicInteger successCount = new AtomicInteger(); | ||
| AtomicInteger failCount = new AtomicInteger(); | ||
|
|
||
| // 각 스레드별로 현재 상태(true/false)를 관리하기 위한 배열 | ||
| boolean[] likeStatus = new boolean[threadCount]; | ||
|
|
||
| for (int i = 0; i < threadCount; i++) { | ||
| final int userIndex = i; | ||
| executor.submit(() -> { | ||
| likeStatus[userIndex] = true; | ||
| for (int r = 0; r < repeat; r++) { | ||
| boolean isLike = likeStatus[userIndex]; | ||
| try { | ||
| // 각 스레드별로 서로 다른 user를 사용하도록 user1, user2 분기 처리 | ||
| Long userId = (userIndex == 0) ? user1.getUserId() : user2.getUserId(); | ||
|
|
||
| postLikeService.changeLikeStatusPost( | ||
| new PostIsLikeCommand(userId, feed.getPostId(), PostType.FEED, isLike) | ||
| ); | ||
| successCount.getAndIncrement(); | ||
| // 성공했을 때만 현재 상태를 반전 | ||
| likeStatus[userIndex] = !likeStatus[userIndex]; | ||
| } catch (Exception e) { | ||
| log.error(e.getMessage(), e); | ||
| failCount.getAndIncrement(); | ||
| } finally { | ||
| latch.countDown(); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| latch.await(); | ||
| executor.shutdown(); | ||
|
|
||
| // then | ||
| assertAll( | ||
| () -> assertThat(successCount.get()).isEqualTo(threadCount * repeat), | ||
| () -> assertThat(failCount.get()).isEqualTo(0) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
최종 상태 검증이 누락되었습니다.
동시성 테스트에서 예외 발생 여부만 확인하고 있으나, 실제 데이터 정합성은 검증하지 않고 있습니다. 각 사용자가 독립적으로 토글하므로(user1과 user2가 서로 다른 userId 사용) 진짜 race condition을 테스트하지 않습니다.
Redis 비동기 큐잉 아키텍처의 핵심 검증 사항:
- 최종 좋아요 상태(user1, user2 각각 좋아요 여부)가 예상과 일치하는가?
- Redis Set의 상태와 DB의 최종 상태가 일관성 있는가?
- 게시글의 최종 likeCount가 정확한가?
테스트 종료 후 다음 검증을 추가하세요:
latch.await();
executor.shutdown();
+
+ // 백그라운드 워커가 큐를 처리할 시간 대기
+ Thread.sleep(2000);
// then
assertAll(
() -> assertThat(successCount.get()).isEqualTo(threadCount * repeat),
- () -> assertThat(failCount.get()).isEqualTo(0)
+ () -> assertThat(failCount.get()).isEqualTo(0),
+ // 각 사용자의 최종 좋아요 상태 검증 (짝수번 토글이므로 초기값과 동일)
+ () -> assertThat(postLikeService.isLikedPostByUser(
+ new PostIsLikeCommand(user1.getUserId(), feed.getPostId(), PostType.FEED, true)
+ )).isFalse(),
+ () -> assertThat(postLikeService.isLikedPostByUser(
+ new PostIsLikeCommand(user2.getUserId(), feed.getPostId(), PostType.FEED, true)
+ )).isFalse(),
+ // 최종 좋아요 카운트 검증
+ () -> {
+ FeedJpaEntity updatedFeed = feedJpaRepository.findById(feed.getPostId()).orElseThrow();
+ assertThat(updatedFeed.getLikeCount()).isEqualTo(0);
+ }
);Committable suggestion skipped: line range outside the PR's diff.
|
|
||
| // given | ||
| bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); | ||
| bookJpaRepository.save(TestEntityFactory.createBookWithISBN("3788954682152")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 데이터 불일치로 인한 테스트 로직 오류
다섯 개의 테스트 모두에서 저장된 Book의 ISBN과 요청 시 사용하는 ISBN이 일치하지 않습니다:
- 75번 라인: DB에
"3788954682152"저장, 요청은"9788954682152"사용, 검증은"9788954682152"기대 (106번 라인) - 161번 라인: DB에
"1788954682152"저장, 요청은"9788954682152"사용, 검증은"9788954682152"기대 (190번 라인) - 209, 242, 282번 라인도 동일한 패턴
특히 createFeedWithBookExistsInDB 테스트는 "DB에 책이 존재할 때"를 테스트한다고 명시하지만, 실제로는 요청한 ISBN의 책이 DB에 존재하지 않는 상황을 만들고 있습니다. 이 경우 외부 API를 호출하거나 실패해야 하는데, 검증은 요청한 ISBN을 가진 책이 저장되었다고 기대합니다.
또한 이 변경사항들은 PR의 목적(좋아요 API 리팩토링)과 무관해 보입니다.
모든 테스트에서 저장하는 Book의 ISBN을 요청 시 사용하는 ISBN과 동일하게 수정하세요:
- bookJpaRepository.save(TestEntityFactory.createBookWithISBN("3788954682152"));
+ bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));- bookJpaRepository.save(TestEntityFactory.createBookWithISBN("1788954682152"));
+ bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));- bookJpaRepository.save(TestEntityFactory.createBookWithISBN("4788954682152"));
+ bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));- bookJpaRepository.save(TestEntityFactory.createBookWithISBN("2788954682152"));
+ bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));- bookJpaRepository.save(TestEntityFactory.createBookWithISBN("5788954682152"));
+ bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152"));만약 테스트 간 격리를 위해 서로 다른 ISBN이 필요하다면, 요청 payload의 ISBN도 함께 변경하여 일관성을 유지해야 합니다.
Also applies to: 161-161, 209-209, 242-242, 282-282
🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java around
lines 75, 161, 209, 242, 282, the tests save Books with ISBNs that do not match
the ISBN used in the request payloads and assertions (e.g., line 75 saves
"3788954682152" while the test requests and expects "9788954682152"), causing
incorrect test logic; update each saved
TestEntityFactory.createBookWithISBN(...) call so the ISBN exactly matches the
ISBN used in that test's request and assertion (or alternatively change the test
request payloads to match the saved ISBNs) to ensure each test truly reflects
the intended DB-existing vs non-existing scenarios and maintains isolation.
#️⃣ 연관된 이슈
📝 작업 내용
게시글 좋아요 API의 처리량 극대화와 시스템 안정성을 위해, 기존의 동기적 DB 쓰기 방식을 Redis 기반의 비동기 이벤트 기반 아키텍처(EDA) 로 리팩토링하였습니다.
대신 Redis List를 활용한 메시지 큐 발행 방식으로 대체하여 API 응답 시간을 최소화했습니다.
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
릴리스 노트
새로운 기능
테스트
✏️ Tip: You can customize this high-level summary in your review settings.