Conversation
- 게시글 별 루트 댓글 조회 성능을 위한 (post_id, parent_id) 복합 인덱스를 comments 테이블에 추가
- 루트 댓글 최신순 조회 시, 페이징 처리를 위한 cursor를 created_at -> PK 로 변경 - 루트 댓글 조회 시, postType에 대한 where 절 제거 (post_id 로만 검증해도 충분)
- query dto -> response dto 변환 로직 추가
- root_comment_id : 현재 댓글의 루트 댓글 ID 값 (루트 댓글인 경우는 null) - descendant_count : 현재 댓글의 모든 자손 댓글의 개수 (루트 댓글이 아닌 경우는 0)
- 루트 댓글 조회, 자손 댓글 조회 API 분리에 따른 use case 네이밍 수정 - 루트 댓글 조회 API에 spring transactional 제거 (for 조회 성능)
- 유저별 좋아하는 댓글 목록 조회를 위한 복합 인데스 추가
- persistence adapter 게층에서 '첫 페이지의 루트 댓글 조회' 시 캐시 조회하도록 설계 - 실제 코드에 반영하지는 않음 (CommentQueryPersistenceAdapter에서 주석 처리)
- API 엔드포인트 분리
- 기존 service 에서 분기처리해서 수행하던 작업을 2개의 service로 분리
- comments 테이블 스키마 변경에 따른 comments 레코드 생성 로직 변경 - 부모/루트 댓글 조회 로직 추가 - 자손 수 변경 로직 추가 - jpa entity <-> domain entity의 매핑 오버헤드를 줄이고자, adapter 계층에서 [루트 댓글 설정, 자손 수 업데이트] 로직 수행
- controller 단위 테스트, API 통합 테스트, Comment 도메인 단위 테스트 수정
- 로컬 부하 테스트 환경 구성을 위한 /monitoring 디렉토리 추가 및 git ignore 설정
- 고정 크기 메모리 할당이 아니라, 비율 기반으로 메모리 할당하도록 수정
Walkthrough댓글 기능을 루트 댓글과 자식(대댓글)으로 분리합니다. 엔드포인트·DTO·유스케이스·서비스가 분리되고 JPA·DB 스키마에 root_comment_id와 descendant_count가 추가되며 Redis 캐시·인덱스·부하테스트 스크립트가 도입됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant RootController as RootCommentController
participant RootService as RootCommentCreateService
participant CommandPort as CommentCommandPort
participant QueryPort as CommentQueryPort
participant Mapper as CommentQueryMapper
Client->>RootController: POST /comments/{postId}
RootController->>RootService: createRootComment(RootCommentCreateCommand)
RootService->>CommandPort: save(Comment)
CommandPort-->>RootService: savedCommentId
RootService->>QueryPort: findRootCommentById(commentId)
QueryPort-->>RootService: CommentQueryDto
RootService->>Mapper: toRootCommentResponse(dto)
Mapper-->>RootService: RootCommentDto
RootService-->>RootController: CommentCreateResponse
RootController-->>Client: 200 OK
sequenceDiagram
participant Client as Client
participant ChildController as ChildCommentController
participant ChildService as ChildCommentCreateService
participant CommandPort as CommentCommandPort
participant QueryPort as CommentQueryPort
participant Mapper as CommentQueryMapper
Client->>ChildController: POST /comments/replies/{parentCommentId}
ChildController->>ChildService: createChildComment(ChildCommentCreateCommand)
ChildService->>CommandPort: findCommentById(parentCommentId)
CommandPort-->>ChildService: parentComment
ChildService->>CommandPort: save(Comment.createChildComment(...))
CommandPort-->>ChildService: savedChildCommentId
ChildService->>QueryPort: findRootCommentById(rootCommentId)
QueryPort-->>ChildService: rootCommentDto
ChildService->>Mapper: toRootCommentResponseWithChildren(rootDto, childDto)
Mapper-->>ChildService: RootCommentDto(with child)
ChildService-->>ChildController: CommentCreateResponse
ChildController-->>Client: 200 OK
sequenceDiagram
participant Client as Client
participant RootShowController as RootCommentShowController
participant RootShowService as RootCommentShowService
participant Cache as CommentCacheAdapter
participant QueryPort as CommentQueryPort
participant LikePort as CommentLikeQueryPort
participant Mapper as CommentQueryMapper
Client->>RootShowController: GET /comments/{postId}?cursor=...
RootShowController->>RootShowService: showRootCommentsOfPost(query)
RootShowService->>Cache: findFirstPageRootCommentsFromCache(postId, size)
alt Cache Hit
Cache-->>RootShowService: List<CommentQueryDto>
else Cache Miss
Cache->>QueryPort: findLatestRootCommentsWithDeleted(postId, cursor)
QueryPort-->>Cache: List<CommentQueryDto>
Cache-->>RootShowService: List<CommentQueryDto>
end
RootShowService->>LikePort: findCommentIdsLikedByUser(userId, ids)
LikePort-->>RootShowService: Set<likedIds>
RootShowService->>Mapper: toRootCommentResponse(dto, likedIds, userId)
Mapper-->>RootShowService: RootCommentDto list
RootShowService-->>RootShowController: RootCommentsResponse
RootShowController-->>Client: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Results499 tests 499 ✅ 45s ⏱️ Results for commit 02c1df1. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Pull request overview
댓글 도메인의 조회/생성 API를 루트 댓글과 **자식 댓글(대댓글)**로 분리하고, 조회 성능을 위해 root_comment_id/descendant_count 기반 조회 구조를 도입한 PR입니다. (추가로 Redis 캐시 설정/어댑터/테스트 및 부하테스트 스크립트가 포함되어 있습니다.)
Changes:
- 댓글 조회 API 분리: 루트 댓글 조회(
GET /comments/{postId}) + 자식 댓글 조회(GET /comments/replies/{rootCommentId}) - 댓글 생성 API 분리: 루트 댓글 생성 + 자식 댓글 생성
- DB 스키마 변경:
root_comment_id,descendant_count및 인덱스/마이그레이션 추가, QueryDSL 조회 로직 개편
Reviewed changes
Copilot reviewed 40 out of 53 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordControllerTest.java | 요청 구성/검증 방식 일부 수정 |
| src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java | 피드 생성 요청 테스트 데이터 조정 |
| src/test/java/konkuk/thip/common/util/TestEntityFactory.java | 댓글/답글 생성 시 root 계산 로직 반영 |
| src/test/java/konkuk/thip/comment/domain/CommentTest.java | 도메인 팩토리 메서드 분리(createRoot/createChild) 테스트로 전환 |
| src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheTest.java | CommentCacheAdapter 캐싱 동작 통합 테스트 추가 |
| src/test/java/konkuk/thip/comment/adapter/in/web/RootCommentShowApiTest.java | 루트 댓글 조회 응답/descendantCount 검증으로 테스트 갱신 |
| src/test/java/konkuk/thip/comment/adapter/in/web/RootCommentCreateApiTest.java | 루트 댓글 생성 API 통합 테스트로 분리/정리 |
| src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java | 기존 isReplyRequest 기반 검증 테스트 일부 제거 |
| src/test/java/konkuk/thip/comment/adapter/in/web/ChildCommentShowApiTest.java | 자식 댓글 조회(평탄화/페이징/정렬/좋아요) 통합 테스트 추가 |
| src/test/java/konkuk/thip/comment/adapter/in/web/ChildCommentCreateApiTest.java | 자식 댓글 생성 API 통합 테스트 추가 |
| src/main/resources/db/migration/V260123__Add_descendant_count.sql | descendant_count 컬럼 추가 및 초기 집계 마이그레이션 |
| src/main/resources/db/migration/V260119__Add_root_comment_id_column.sql | root_comment_id 컬럼 추가 및 재귀 CTE 마이그레이션 + FK |
| src/main/resources/db/migration/V260107__Add_index_comments.sql | comments 인덱스 추가 |
| src/main/java/konkuk/thip/config/CacheConfig.java | Redis CacheManager/ObjectMapper 기반 캐시 설정 추가 |
| src/main/java/konkuk/thip/comment/domain/Comment.java | 도메인 생성 API 분리 + rootCommentId/descendantCount 필드 추가 |
| src/main/java/konkuk/thip/comment/application/service/RootCommentShowService.java | 루트 댓글 조회 전용 서비스 분리 |
| src/main/java/konkuk/thip/comment/application/service/RootCommentCreateService.java | 루트 댓글 생성 전용 서비스 추가 |
| src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java | 기존 “전체 댓글 조회” 서비스 삭제 |
| src/main/java/konkuk/thip/comment/application/service/ChildCommentShowService.java | 자식 댓글 조회 전용 서비스 추가 |
| src/main/java/konkuk/thip/comment/application/service/ChildCommentCreateService.java | 자식 댓글 생성 전용 서비스로 리팩터링 |
| src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java | descendantCount 포함하도록 DTO 확장 |
| src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java | 루트/자식 댓글 조회 포트 시그니처 변경 |
| src/main/java/konkuk/thip/comment/application/port/in/dto/RootCommentCreateCommand.java | 루트 댓글 생성 커맨드로 분리 |
| src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java | 주석/의미 보강 |
| src/main/java/konkuk/thip/comment/application/port/in/dto/ChildCommentsShowQuery.java | 자식 댓글 조회 쿼리 DTO 추가 |
| src/main/java/konkuk/thip/comment/application/port/in/dto/ChildCommentCreateCommand.java | 자식 댓글 생성 커맨드 DTO 추가 |
| src/main/java/konkuk/thip/comment/application/port/in/RootCommentShowUseCase.java | 루트 댓글 조회 유스케이스 추가 |
| src/main/java/konkuk/thip/comment/application/port/in/RootCommentCreateUseCase.java | 루트 댓글 생성 유스케이스 추가 |
| src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java | 기존 유스케이스 삭제 |
| src/main/java/konkuk/thip/comment/application/port/in/CommentCreateUseCase.java | 기존 유스케이스 삭제 |
| src/main/java/konkuk/thip/comment/application/port/in/ChildCommentShowUseCase.java | 자식 댓글 조회 유스케이스 추가 |
| src/main/java/konkuk/thip/comment/application/port/in/ChildCommentCreateUseCase.java | 자식 댓글 생성 유스케이스 추가 |
| src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java | 루트/자식 조회 DTO 매핑 분리 및 생성 응답 매핑 조정 |
| src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java | QueryDSL 조회 로직을 root_comment_id/descendantCount 기반으로 개편 |
| src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java | Query repository 인터페이스 시그니처 변경 |
| src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java | 커서 기반 루트/자식 조회 어댑터 개편 |
| src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java | 저장 시 root 설정 + descendant_count 증가 처리 |
| src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheKey.java | 댓글 캐시 키 상수 추가 |
| src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheAdapter.java | 루트 댓글 첫 페이지 캐싱 어댑터 추가 |
| src/main/java/konkuk/thip/comment/adapter/out/mapper/CommentMapper.java | JPA 매핑에 root/descendantCount 반영 |
| src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java | comment_likes 복합 인덱스 추가 |
| src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java | root/descendantCount 필드 + 인덱스 + 카운트 증감 메서드 추가 |
| src/main/java/konkuk/thip/comment/adapter/in/web/response/RootCommentsResponse.java | 루트 댓글 조회 전용 응답 DTO 추가 |
| src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java | 기존 루트+자식 동시 반환 응답 DTO 삭제 |
| src/main/java/konkuk/thip/comment/adapter/in/web/response/ChildCommentsResponse.java | 자식 댓글 조회 전용 응답 DTO 추가 |
| src/main/java/konkuk/thip/comment/adapter/in/web/request/RootCommentCreateRequest.java | 루트 댓글 생성 요청 DTO 추가 |
| src/main/java/konkuk/thip/comment/adapter/in/web/request/CommentCreateRequest.java | 기존 통합 생성 요청 DTO 삭제 |
| src/main/java/konkuk/thip/comment/adapter/in/web/request/ChildCommentCreateRequest.java | 자식 댓글 생성 요청 DTO 추가 |
| src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java | 조회 엔드포인트를 루트/자식으로 분리 |
| src/main/java/konkuk/thip/comment/adapter/in/web/CommentCommandController.java | 생성 엔드포인트를 루트/자식으로 분리 |
| loadtest/comment/root_comment_show.js | k6 루트 댓글 조회 부하 테스트 스크립트 추가 |
| Dockerfile | JVM 메모리 옵션/WORKDIR 등 조정 |
| .gitignore | 로컬 monitoring 디렉토리 제외 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Configuration | ||
| @EnableCaching | ||
| public class CacheConfig { |
There was a problem hiding this comment.
PR 설명에는 루트 댓글 조회 캐시는 “고려했으나 이번 PR에는 반영하지 않았다”고 되어 있는데, 이 PR에서 @EnableCaching + Redis CacheManager 설정과 CommentCacheAdapter/통합 테스트까지 추가되어 실제로는 캐싱 인프라가 포함되어 있습니다. 이번 PR 범위를 캐싱까지 포함할지(혹은 설정/테스트를 별도 PR로 분리할지) 명확히 정리해 주세요.
There was a problem hiding this comment.
PR 설명에는 루트 댓글 조회 캐시는 “고려했으나 이번 PR에는 반영하지 않았다”고 되어 있는데, 이 PR에서
@EnableCaching+ RedisCacheManager설정과CommentCacheAdapter/통합 테스트까지 추가되어 실제로는 캐싱 인프라가 포함되어 있습니다. 이번 PR 범위를 캐싱까지 포함할지(혹은 설정/테스트를 별도 PR로 분리할지) 명확히 정리해 주세요.
@copilot 루트 댓글 조회 시 이용할 캐시 관련 코드를 일부 구현했으나, 이를 댓글 조회 API에 엮지는 않았어.
따라서 현재 PR에서는 댓글 조회 시 캐시를 도입하지 않았다고 보는게 맞아.
| @Override | ||
| // @Transactional(readOnly = true) | ||
| // @Unfiltered | ||
| public RootCommentsResponse showRootCommentsOfPost(CommentShowAllQuery query) { | ||
| Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE); |
There was a problem hiding this comment.
RootCommentShowService에서 @Transactional을 제거하면, Spring Data Repository 메서드 자체가 @Transactional(readOnly=true)로 실행되면서 StatusFilterAspect가 기본 ACTIVE 필터를 활성화합니다. 그 결과 INACTIVE(삭제된) 루트 댓글이 조회되지 않아 findLatestRootCommentsWithDeleted/삭제 루트 처리 로직이 깨집니다. 이 서비스 메서드에 @Transactional(readOnly = true)를 다시 적용하고, 삭제 댓글을 포함해야 하므로 @Unfiltered 또는 @IncludeInactive를 함께 적용해 주세요.
There was a problem hiding this comment.
RootCommentShowService에서@Transactional을 제거하면, Spring Data Repository 메서드 자체가@Transactional(readOnly=true)로 실행되면서StatusFilterAspect가 기본 ACTIVE 필터를 활성화합니다. 그 결과 INACTIVE(삭제된) 루트 댓글이 조회되지 않아findLatestRootCommentsWithDeleted/삭제 루트 처리 로직이 깨집니다. 이 서비스 메서드에@Transactional(readOnly = true)를 다시 적용하고, 삭제 댓글을 포함해야 하므로@Unfiltered또는@IncludeInactive를 함께 적용해 주세요.
@copilot 현재 댓글 조회 시에는 Spring Data Repository 의 메서드를 활용하지 않고, QueryDSL로 커스텀한 sql 쿼리를 활용하고 있어. 이러면 spring transactional 명시하지 않으면 애플리케이션 단에서는 트랜잭션이 활성화되지 않는걸로 아는데, 관련해서 검토해봐
| CREATE INDEX idx_comments_post_parent | ||
| ON comments (post_id, parent_id); No newline at end of file |
There was a problem hiding this comment.
PR 설명의 인덱스는 (post_id, parent_id, comment_id)인데, 마이그레이션은 (post_id, parent_id)까지만 생성합니다. 현재 쿼리가 WHERE post_id=? AND parent_id IS NULL ORDER BY comment_id ... 형태라면 comment_id까지 포함한 복합 인덱스가 있어야 정렬/페이징이 효율적입니다. 마이그레이션 인덱스를 실제 쿼리 패턴에 맞게 보완해 주세요.
There was a problem hiding this comment.
PR 설명의 인덱스는
(post_id, parent_id, comment_id)인데, 마이그레이션은(post_id, parent_id)까지만 생성합니다. 현재 쿼리가WHERE post_id=? AND parent_id IS NULL ORDER BY comment_id ...형태라면comment_id까지 포함한 복합 인덱스가 있어야 정렬/페이징이 효율적입니다. 마이그레이션 인덱스를 실제 쿼리 패턴에 맞게 보완해 주세요.
@copilot 해당 인덱스는 comments 테이블에 설정한 인덱스이므로, PK인 comment_id는 굳이 복합인덱스에 포함하지 않더라도 문제없다고 생각했어 (InnoDB 세컨더리 인덱스의 리프는 PK 이므로, 굳이 복합 인덱스의 마지막 순번에 PK를 포함하든, 포함하지 않든 문제가 없다고 생각)
관련해서 검토해봐
| // 타입 정보를 포함하여 직렬화/역직렬화 | ||
| BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder() | ||
| .allowIfBaseType(Object.class) | ||
| .build(); | ||
|
|
||
| mapper.activateDefaultTyping( | ||
| validator, | ||
| ObjectMapper.DefaultTyping.NON_FINAL, | ||
| JsonTypeInfo.As.PROPERTY | ||
| ); |
There was a problem hiding this comment.
Redis 캐시 직렬화에 activateDefaultTyping + allowIfBaseType(Object.class)는 매우 광범위한 다형 타입 역직렬화를 허용해, Redis 값이 오염될 경우(Jackson gadget chain 등) 역직렬화 취약점으로 이어질 수 있습니다. 캐시 값 타입을 명시적으로 제한(화이트리스트 패키지/타입)하거나, 타입 정보를 제거하고 필요한 DTO만 직렬화하는 방식으로 안전하게 구성해 주세요.
| for (int i = 1; i <= 15; i++) { | ||
| CommentJpaEntity childComment = commentJpaRepository.findById((long)(comment1_1.getCommentId() + i - 1)).orElse(null); | ||
| if (childComment != null) { | ||
| jdbcTemplate.update( | ||
| "UPDATE comments SET created_at = ? WHERE comment_id = ?", | ||
| Timestamp.valueOf(base.minusMinutes(30 - i)), childComment.getCommentId()); | ||
| } | ||
| } |
There was a problem hiding this comment.
이 테스트는 comment_id가 연속 증가한다고 가정하고 findById(comment1_1.getCommentId() + i - 1)로 다시 로드한 뒤 createdAt을 업데이트합니다. ID 생성 전략/시퀀스 allocation 등에 따라 연속성이 보장되지 않아 테스트가 간헐적으로 깨질 수 있습니다. 생성한 자식 댓글 엔티티를 리스트로 보관해 그 리스트를 순회하며 업데이트하는 방식으로 변경하는 편이 안정적입니다.
| * 삭제된 루트 댓글 생성용 정적 팩토리 메서드 | ||
| * descendantCount와 isDeleted만 실제 값이고, 나머지는 모두 쓰레기 값 |
There was a problem hiding this comment.
삭제된 루트 댓글 응답을 설명하면서 ‘쓰레기 값’이라는 표현을 사용하고 있는데, 공개 API DTO 주석으로는 다소 부적절합니다. placeholder, null-filled, redacted 등 중립적인 용어로 바꾸고 어떤 필드가 어떤 의미로 채워지는지(예: isDeleted/descendantCount만 유효) 명확히 기술해 주세요.
| * 삭제된 루트 댓글 생성용 정적 팩토리 메서드 | |
| * descendantCount와 isDeleted만 실제 값이고, 나머지는 모두 쓰레기 값 | |
| * 삭제된 루트 댓글 생성용 정적 팩토리 메서드. | |
| * 이 메서드가 반환하는 DTO에서는 {@code isDeleted}와 {@code descendantCount}만 의미 있는 값이며, | |
| * 그 외 필드들은 삭제된 댓글을 표현하기 위한 placeholder/null-filled 값(예: {@code null}, {@code 0}, {@code false})으로 채워집니다. |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordControllerTest.java (1)
76-85:⚠️ Potential issue | 🟡 Minor
@DisplayName과 실제 assertion 불일치
@DisplayName("roomId가 없을 때 400 error")라고 명시하고 있지만,/rooms//password와 같이 path variable이 비어 있을 때 Spring MVC는 핸들러 매핑 실패로 404를 반환할 가능성이 높습니다. 현재is4xxClientError()는 400~499 모두를 수용하므로 테스트 자체는 통과하지만, DisplayName이 오해를 유발할 수 있습니다.DisplayName을 실제 동작과 일치하도록 수정하거나, 기대 동작을 명확히 하는 것을 권장합니다.
🔧 수정 제안
- `@DisplayName`("roomId가 없을 때 400 error") + `@DisplayName`("roomId가 없을 때 4xx error") void missing_roomId() throws Exception {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordControllerTest.java` around lines 76 - 85, Update the test missing_roomId to make the DisplayName and assertion consistent with Spring's behavior: change `@DisplayName`("roomId가 없을 때 400 error") to `@DisplayName`("roomId가 없을 때 404 error") and replace the generic .andExpect(status().is4xxClientError()) with .andExpect(status().isNotFound()) for the request to "/rooms//password" so the test explicitly asserts the expected 404 from handler mapping failure.
🧹 Nitpick comments (18)
Dockerfile (1)
6-8:MaxRAMPercentage=75.0비힙 메모리 여유 검토 권장컨테이너 메모리 기반 동적 힙 설정으로의 전환은 올바른 방향입니다.
-XX:+ExitOnOutOfMemoryError도 컨테이너 환경에서 권장되는 옵션입니다.다만, 75%는 힙에 할당되는 비율로, 나머지 25%는 메타스페이스·스레드 스택·코드 캐시·다이렉트 버퍼(Netty 등)·OS용으로 모두 사용됩니다. 앱이 스레드 수가 많거나 다이렉트 버퍼를 적극적으로 사용하는 경우(예: Spring WebFlux + Netty), 실제 컨테이너 메모리 한도 대비 여유가 충분한지 부하 테스트를 통해 검증하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Dockerfile` around lines 6 - 8, 환경변수 JAVA_TOOL_OPTIONS에 설정된 "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"는 힙을 컨테이너 메모리의 75%로 고정하므로 메타스페이스·스레드 스택·코드 캐시·다이렉트 버퍼(예: Netty) 등에 남는 여유가 25%로 제한됩니다; 소스에서 JAVA_TOOL_OPTIONS와 "-XX:MaxRAMPercentage=75.0" 설정을 찾아 애플리케이션의 스레드/다이렉트 버퍼 사용량을 기준으로 부하 테스트를 수행하고 필요하면 MaxRAMPercentage 값을 낮추거나 운영 문서에 테스트 결과와 권장값을 명시하도록 변경하세요 (또한 "-XX:+ExitOnOutOfMemoryError"는 유지하되, OOM 시 재시작 전략을 배포 설정에 반영하십시오).src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
264-278: 루트 계산 로직 중복 — 헬퍼 메서드 추출 고려Lines 267과 286에서 동일한 루트 계산 로직(
parentComment.getRoot() != null ? parentComment.getRoot() : parentComment)이 반복됩니다. private static 헬퍼로 추출하면 한 곳에서만 관리할 수 있습니다.♻️ 헬퍼 메서드 추출 제안
+ private static CommentJpaEntity resolveRoot(CommentJpaEntity parentComment) { + return parentComment.getRoot() != null ? parentComment.getRoot() : parentComment; + } + public static CommentJpaEntity createReplyComment(PostJpaEntity post, UserJpaEntity user, PostType postType, CommentJpaEntity parentComment) { - CommentJpaEntity root = parentComment.getRoot() != null ? parentComment.getRoot() : parentComment; + CommentJpaEntity root = resolveRoot(parentComment); // ... } public static CommentJpaEntity createReplyComment(PostJpaEntity post, UserJpaEntity user, PostType postType, CommentJpaEntity parentComment, String content, int likeCount) { - CommentJpaEntity root = parentComment.getRoot() != null ? parentComment.getRoot() : parentComment; + CommentJpaEntity root = resolveRoot(parentComment); // ... }Based on learnings: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/common/util/TestEntityFactory.java` around lines 264 - 278, Extract the duplicated root calculation into a private static helper in TestEntityFactory (e.g., private static CommentJpaEntity computeRoot(CommentJpaEntity parentComment)) that returns parentComment.getRoot() != null ? parentComment.getRoot() : parentComment, then replace both inline occurrences with calls to computeRoot(parentComment) and ensure the helper is used wherever root is resolved for reply CommentJpaEntity creation.src/main/resources/db/migration/V260123__Add_descendant_count.sql (1)
1-17: 마이그레이션 로직 적절합니다.
root_comment_id기반 집계와parent_id IS NULL조건을 통한 루트 댓글만 업데이트하는 방식이 올바릅니다. Flyway 버전 순서(V260119 → V260123)도 정확합니다.다만, 운영 환경에서
comments테이블 데이터가 매우 클 경우UPDATE ... JOIN구문이 장시간 테이블 락을 유발할 수 있습니다. 대량 데이터 환경이라면 배치 단위 업데이트(예:LIMIT활용 반복)를 고려해 보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/db/migration/V260123__Add_descendant_count.sql` around lines 1 - 17, The migration currently runs a single UPDATE ... JOIN against the comments table which can lock large tables; change the update to perform batched updates instead by iterating over root_comment_id groups (or using LIMIT with an indexed selection) and updating descendant_count in smaller chunks until complete; specifically modify the logic around the UPDATE comments parent JOIN (referencing comments, descendant_count, root_comment_id, parent_id, status) to apply batch-sized updates (e.g., select affected parent.comment_id values in pages and update those parents) rather than one monolithic UPDATE to avoid long table locks in production.src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java (1)
96-110:descendantCount증감 시 동시성 이슈 가능성
incrementDescendantCount()와decrementDescendantCount()는 JPA 엔티티의 인메모리 값을 변경하는 방식입니다. 동일한 루트 댓글에 동시에 자식 댓글이 생성/삭제되면 lost-update 문제가 발생할 수 있습니다.빈도가 낮다면 현재 방식도 수용 가능하지만, 정확도가 중요하다면 DB 레벨의 atomic update(
UPDATE comments SET descendant_count = descendant_count + 1 WHERE comment_id = ?)를 레포지토리에@Modifying쿼리로 추가하는 것을 권장합니다.♻️ Atomic update 쿼리 예시
Repository에 추가:
`@Modifying` `@Query`("UPDATE CommentJpaEntity c SET c.descendantCount = c.descendantCount + 1 WHERE c.commentId = :commentId") void incrementDescendantCount(`@Param`("commentId") Long commentId); `@Modifying` `@Query`("UPDATE CommentJpaEntity c SET c.descendantCount = c.descendantCount - 1 WHERE c.commentId = :commentId AND c.descendantCount > 0") void decrementDescendantCount(`@Param`("commentId") Long commentId);Based on learnings: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java` around lines 96 - 110, The current in-memory mutations in CommentJpaEntity.incrementDescendantCount() and decrementDescendantCount() can suffer lost-update under concurrent child create/delete; replace or supplement these by adding atomic DB-level updates in the repository: create `@Modifying` methods (e.g. incrementDescendantCount(commentId) and decrementDescendantCount(commentId) with a WHERE guard to prevent negative counts) and call those repository methods when adjusting descendant counts instead of—or immediately after—mutating the entity field so the count change is performed atomically at the database level.src/main/java/konkuk/thip/config/CacheConfig.java (1)
59-60: TTL 5분 하드코딩 — 환경별 설정값 외부화 권장TTL이 코드에 하드코딩되어 있어 프로파일별 조정(dev/prod 차이, 캐시 무효화 정책 변경 등)을 위해 재배포가 필요합니다.
♻️ application.yml 기반 설정 외부화 예시
+@Value("${cache.comment.ttl-minutes:5}") +private long cacheTtlMinutes; RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofMinutes(5)) + .entryTtl(Duration.ofMinutes(cacheTtlMinutes))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/config/CacheConfig.java` around lines 59 - 60, The Redis TTL is hardcoded in CacheConfig (the RedisCacheConfiguration config setup) — make it configurable via application configuration (e.g., application.yml) by introducing a property like cache.redis.ttlMinutes and reading it into CacheConfig (use `@Value` or a `@ConfigurationProperties` bean) and then use that value to set entryTtl(Duration.ofMinutes(...)) instead of the literal 5; ensure a sensible default is provided if the property is missing and update any tests or documentation accordingly.src/test/java/konkuk/thip/comment/domain/CommentTest.java (1)
34-44:descendantCount초기값 검증 추가를 권장합니다.루트 댓글 생성 테스트에서
descendantCount가 0으로 초기화되는지 확인하는 assertion을 추가하면, 새로 도입된 필드의 동작을 더 확실하게 보장할 수 있습니다.♻️ 제안
void createRootComment_valid() { Comment comment = Comment.createRootComment(CONTENT, POST_ID, CREATOR_ID, FEED); assertNotNull(comment); assertNull(comment.getParentCommentId()); assertEquals(FEED, comment.getPostType()); assertEquals(POST_ID, comment.getTargetPostId()); assertEquals(CONTENT, comment.getContent()); + assertEquals(0, comment.getDescendantCount()); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/konkuk/thip/comment/domain/CommentTest.java` around lines 34 - 44, Add an assertion to verify that a newly created root comment's descendantCount is initialized to 0: in the test method createRootComment_valid() after creating the Comment via Comment.createRootComment(CONTENT, POST_ID, CREATOR_ID, FEED), call comment.getDescendantCount() and assert it equals 0 so the new descendantCount field's default behavior is validated.src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java (1)
13-13:findRootCommentId메서드명이 반환 타입과 불일치합니다.
findRootCommentId는Long(ID 값)을 반환할 것처럼 보이지만, 실제로는CommentQueryDto를 반환합니다.findRootCommentById나findRootCommentDtoByCommentId등으로 변경하면 의도가 더 명확해집니다. Based on learnings, seongjunnoh는 메서드 네이밍 시 비즈니스 의도를 반영하는 것을 선호합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java` at line 13, The method name findRootCommentId is misleading because it returns a CommentQueryDto; rename the method to a name that reflects the DTO return (e.g., findRootCommentDtoByCommentId or findRootCommentById) in the CommentQueryRepository interface and update all implementations and call sites to use the new name; ensure method signatures, imports and any tests referencing findRootCommentId are updated accordingly to prevent compile errors.src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java (1)
38-70:save()메서드의 책임이 커졌습니다 — 루트 해석 + descendant 관리까지 포함현재
save()메서드가 단순 저장 외에 루트 댓글 탐색, descendant count 증가, JPA 엔티티 변환을 모두 수행합니다. 지금은 관리 가능한 수준이지만, 향후 로직이 추가될 경우 메서드 분리를 고려해 보세요 (예:resolveRootComment(parentCommentJpaEntity)헬퍼 추출).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java` around lines 38 - 70, The save() method currently handles multiple responsibilities (finding parent/root, updating descendant count, and mapping to JPA) — refactor by extracting helper methods: implement a private resolveRootComment(CommentJpaEntity parentCommentJpaEntity) that returns the correct root (parent.getRoot() != null ? parent.getRoot() : parent) and a private ensureIncrementDescendantCount(CommentJpaEntity root) that encapsulates rootCommentJpaEntity.incrementDescendantCount(); then update save() to call userJpaRepository.findByUserId(...), findPostJpaEntity(...), fetch parent via commentJpaRepository.findByCommentId(...), call resolveRootComment(...) and ensureIncrementDescendantCount(...), and finally call commentMapper.toJpaEntity(...) and commentJpaRepository.save(...).src/main/java/konkuk/thip/comment/application/port/in/RootCommentShowUseCase.java (1)
1-9: 인바운드 포트에서 어댑터 레이어 응답 타입 직접 참조
RootCommentsResponse는adapter.in.web.response패키지에 위치한 웹 어댑터 레이어 타입인데,application.port.in인바운드 포트에서 직접 반환 타입으로 사용하고 있습니다. 엄밀히 보면 포트 → 어댑터 방향 의존이 발생하지만, 이전 학습 내용에 따르면 실용적인 접근을 선호하시는 것으로 알고 있어 참고용으로 남깁니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/application/port/in/RootCommentShowUseCase.java` around lines 1 - 9, The inbound port RootCommentShowUseCase currently returns a web-adapter type RootCommentsResponse (from adapter.in.web.response), creating a port→adapter dependency; change the port to return an application-layer DTO (e.g., create a RootCommentsDto or RootCommentsResponse in application.port.in or application.dto package) and update RootCommentShowUseCase.showRootCommentsOfPost(CommentShowAllQuery) to use that DTO, then map between the application DTO and the web adapter RootCommentsResponse inside the web adapter (controller/mapper) instead of in the port.src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java (1)
57-64:children파라미터 네이밍이 단일 객체와 불일치
toRootCommentResponseWithChildren메서드의CommentQueryDto children파라미터는 단수 객체인데, 이름이 복수형입니다.child로 변경하면 가독성이 향상됩니다. 또한 record의 리스트를 외부에서 직접add()로 변경하는 패턴은 record의 불변성 의도와 어긋나지만,toRoot에서 명시적으로ArrayList를 생성하고 있어 의도된 동작으로 보입니다.제안: 파라미터명 수정
default CommentCreateResponse toRootCommentResponseWithChildren( - CommentQueryDto root, CommentQueryDto children, boolean isLikedParentComment, `@Context` Long userId) { - CommentCreateResponse.ReplyCommentCreateDto replyDto = toReply(children, userId); + CommentQueryDto root, CommentQueryDto child, boolean isLikedParentComment, `@Context` Long userId) { + CommentCreateResponse.ReplyCommentCreateDto replyDto = toReply(child, userId); CommentCreateResponse rootDto = toRoot(root, isLikedParentComment, userId); rootDto.replyList().add(replyDto); return rootDto; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java` around lines 57 - 64, Rename the misleading plural parameter in method toRootCommentResponseWithChildren from CommentQueryDto children to CommentQueryDto child and update all usages inside the method (use child when calling toReply) so names accurately reflect a single object; leave the creation behavior from toRoot and the subsequent rootDto.replyList().add(...) unchanged.src/main/java/konkuk/thip/comment/application/service/RootCommentCreateService.java (1)
68-83:sendNotificationsToPostWriter메서드가ChildCommentCreateService와 중복됨
ChildCommentCreateService의sendNotificationsToPostWriter와 시그니처 및 구현이 동일합니다. 공통 헬퍼 클래스나 상위 클래스로 추출하면 중복을 제거할 수 있습니다. 또한switch문에default분기가 없어 새로운PostType추가 시 알림이 누락될 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/application/service/RootCommentCreateService.java` around lines 68 - 83, The sendNotificationsToPostWriter method is duplicated in RootCommentCreateService and ChildCommentCreateService; extract this shared logic into a common helper or superclass (e.g., CommentNotificationHelper or abstract CommentCreateService base) and move the implementation there so both RootCommentCreateService and ChildCommentCreateService call the single method, referencing PostQueryDto, User, feedNotificationOrchestrator and roomNotificationOrchestrator; also add a safe default branch in the switch over PostType (or throw/ log an exception) so new PostType values won't silently drop notifications.src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1)
127-161: 메서드명findAllDescendantCommentsByCreatedAtAsc가 실제 정렬 기준과 불일치합니다Line 158에서
comment.commentId.asc()로 정렬하고 있으나, 메서드명은ByCreatedAtAsc로 되어 있어 혼란을 줄 수 있습니다. 커서 페이지네이션도commentId기반(line 146)이므로, 메서드명을 실제 동작에 맞게 수정하는 것을 권장합니다.♻️ 메서드명 수정 제안
- public List<CommentQueryDto> findAllDescendantCommentsByCreatedAtAsc(Long rootCommentId, Long lastCommentId, int size) { + public List<CommentQueryDto> findAllDescendantCommentsByCommentIdAsc(Long rootCommentId, Long lastCommentId, int size) {인터페이스(
CommentQueryRepository)의 메서드명도 함께 변경해 주세요.Based on learnings: seongjunnoh는 메서드 네이밍 시 구현 세부사항보다 비즈니스 의도를 반영하는 것을 선호합니다. 다만 이 경우는 정렬 기준이 명시적으로 메서드명에 포함되어 있어, 실제 구현과의 정합성이 중요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java` around lines 127 - 161, The method name findAllDescendantCommentsByCreatedAtAsc is misleading because the implementation sorts by comment.commentId.asc() and uses commentId for cursor pagination; rename the method to reflect the actual sort/key (e.g., findAllDescendantCommentsByCommentIdAsc) in CommentQueryRepositoryImpl and update the declaration in the CommentQueryRepository interface accordingly, then update all callers/usages to the new name to keep API and implementation consistent.src/main/java/konkuk/thip/comment/application/service/RootCommentShowService.java (1)
29-32: 주석 처리된 어노테이션과 미사용 import 정리 필요
@Transactional(readOnly = true)과@Unfiltered를 의도적으로 제거한 것은 PR 목적과 일치하지만, 주석으로 남겨두기보다는 완전히 삭제하는 것이 좋습니다. 관련된 미사용 import(Unfiltered,Transactional)도 함께 정리해 주세요.주석 처리된 코드는 git history로 추적 가능하므로, 향후 혼란을 줄이기 위해 제거하는 것을 권장합니다.
♻️ 정리 제안
-import konkuk.thip.common.annotation.persistence.Unfiltered; import konkuk.thip.common.util.Cursor; import konkuk.thip.common.util.CursorBasedList; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; ... `@Override` -// `@Transactional`(readOnly = true) -// `@Unfiltered` public RootCommentsResponse showRootCommentsOfPost(CommentShowAllQuery query) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/application/service/RootCommentShowService.java` around lines 29 - 32, Remove the commented-out annotations and unused imports: delete the commented lines "// `@Transactional`(readOnly = true)" and "// `@Unfiltered`" from the showRootCommentsOfPost method in RootCommentShowService and remove the corresponding unused import statements for Transactional and Unfiltered at the top of the file; ensure imports are cleaned so no unused symbols remain and run organiser/IDE optimizer or mvn/gradle build to confirm no compilation warnings.loadtest/comment/root_comment_show.js (1)
4-7: 하드코딩된 설정값에 대한 환경 변수 사용 고려
BASE_URL,TARGET_POST_ID,POST_TYPE이 하드코딩되어 있습니다. k6는__ENV를 통한 환경 변수를 지원하므로, 다양한 환경에서 재사용하기 쉽도록 환경 변수화를 고려해 보세요.const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; const TARGET_POST_ID = __ENV.TARGET_POST_ID || 1;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@loadtest/comment/root_comment_show.js` around lines 4 - 7, Replace the hardcoded constants BASE_URL, TARGET_POST_ID, and POST_TYPE with environment-backed values using __ENV so the script is reusable across environments; read BASE_URL and POST_TYPE from __ENV with sensible defaults, and read TARGET_POST_ID from __ENV and convert it to a number (e.g., parseInt) with a fallback to 1 to avoid string/number issues when the env var is provided. Ensure you update any code that uses TARGET_POST_ID to expect a number and keep the same constant names (BASE_URL, TARGET_POST_ID, POST_TYPE) so references remain unchanged.src/main/java/konkuk/thip/comment/adapter/out/mapper/CommentMapper.java (1)
12-24:toJpaEntity메서드의 파라미터 수 증가 관련메서드 파라미터가 5개로 늘어났습니다. 현재는 수용 가능한 수준이지만, 향후 필드가 더 추가된다면 파라미터 객체(e.g.,
CommentEntityContext)로의 리팩토링을 고려해 볼 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/mapper/CommentMapper.java` around lines 12 - 24, The toJpaEntity method currently accepts five parameters (Comment comment, PostJpaEntity postJpaEntity, UserJpaEntity userJpaEntity, CommentJpaEntity parentCommentJpaEntity, CommentJpaEntity rootCommentJpaEntity); to make it extensible, introduce a parameter object (e.g., CommentEntityContext) that aggregates these fields and change the signature to toJpaEntity(CommentEntityContext ctx); inside, read ctx.getComment(), ctx.getPostJpaEntity(), ctx.getUserJpaEntity(), ctx.getParentCommentJpaEntity(), ctx.getRootCommentJpaEntity() and use them to build CommentJpaEntity via CommentJpaEntity.builder(); update all callers to construct and pass CommentEntityContext instead of multiple parameters.src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java (3)
20-41: 주석 처리된 캐시 코드와 Javadoc이 불일치합니다.Lines 22-26의 Javadoc에서 "첫 페이지: CommentCacheAdapter를 통한 캐시 조회"를 설명하고 있지만, 실제 캐시 로직(Lines 33-41)과
commentCacheAdapter참조(Line 20)가 모두 주석 처리되어 있어 문서와 구현이 불일치합니다.PR 목표에서 캐싱은 이번 PR에서 적용하지 않는다고 명시되어 있으므로, 주석 처리된 코드와 관련 Javadoc을 정리하고, 캐싱 적용 시점을 별도 이슈로 추적하는 것을 권장합니다.
♻️ 주석 코드 정리 및 Javadoc 업데이트 제안
`@Slf4j` `@Repository` `@RequiredArgsConstructor` public class CommentQueryPersistenceAdapter implements CommentQueryPort { private final CommentJpaRepository commentJpaRepository; -// private final CommentCacheAdapter commentCacheAdapter; - /** - * 루트 댓글 조회 (페이징) - * - 첫 페이지: CommentCacheAdapter를 통한 캐시 조회 - * - 2페이지 이후: DB 직접 조회 (캐싱하지 않음) - */ + // 루트 댓글 조회 (페이징, DB 직접 조회) `@Override` public CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, Cursor cursor) { int size = cursor.getPageSize(); Long lastRootCommentId = cursor.isFirstRequest() ? null : cursor.getLong(0); List<CommentQueryDto> commentQueryDtos = commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, lastRootCommentId, size); -// List<CommentQueryDto> commentQueryDtos; -// if (cursor.isFirstRequest()) { -// // 첫 페이지: 캐시 조회 -// commentQueryDtos = commentCacheAdapter.findFirstPageRootCommentsFromCache(postId, size); -// } else { -// // 2페이지 이후: DB 직접 조회 -// Long lastRootCommentId = cursor.getLong(0); -// commentQueryDtos = commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, lastRootCommentId, size); -// } - return CursorBasedList.of(commentQueryDtos, size, commentQueryDto -> { Cursor nextCursor = new Cursor(List.of(commentQueryDto.commentId().toString())); return nextCursor.toEncodedString(); }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java` around lines 20 - 41, Update the implementation and docs for findLatestRootCommentsWithDeleted to reflect that caching is not used in this PR: remove the commented-out commentCacheAdapter field and the commented cache-branch in the method, and change the Javadoc on the method to state that root comments are always fetched from the DB via commentJpaRepository (use lastRootCommentId logic with Cursor and size); optionally add a short TODO referencing a future issue to reintroduce CommentCacheAdapter-based first-page caching when implemented.
27-60:findLatestRootCommentsWithDeleted와findAllDescendantComments의 커서 처리 및CursorBasedList변환 로직이 동일한 패턴을 반복합니다.두 메서드 모두 다음과 같은 동일한 흐름을 따릅니다:
cursor에서lastId와size추출- 레포지토리 호출
CursorBasedList.of(...)+commentId기반 nextCursor 생성커서 변환 로직(특히
commentQueryDto -> nextCursor람다)을 공통 헬퍼로 추출하면 중복을 줄일 수 있습니다. 다만, 현재 메서드가 2개이므로 향후 유사 패턴이 추가될 때 리팩토링해도 무방합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java` around lines 27 - 60, Both findLatestRootCommentsWithDeleted and findAllDescendantComments duplicate cursor handling and CursorBasedList conversion (extracting size/lastId, calling repository, then CursorBasedList.of(...) with commentQueryDto -> new Cursor(...)). Create a small private helper (e.g., toCursorBasedList or buildCursorBasedList) that accepts the fetched List<CommentQueryDto> and page size (and/or a Function<CommentQueryDto,String> if needed) and returns CursorBasedList by encapsulating the lambda that builds the nextCursor from commentQueryDto.commentId(), then replace the inline CursorBasedList.of(...) usages in findLatestRootCommentsWithDeleted and findAllDescendantComments to call that helper; keep repository calls and cursor lastId/size extraction in each method.
9-14:@Slf4j어노테이션이 추가되었으나log가 사용되지 않습니다.현재 활성 코드에서
log를 참조하는 곳이 없습니다. 캐시 로직 제거 시 함께 정리하거나, 향후 사용 계획이 있다면 그대로 두셔도 됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java` around lines 9 - 14, CommentQueryPersistenceAdapter 클래스에 추가된 `@Slf4j와` lombok.extern.slf4j.Slf4j import가 실제로 사용되지 않으므로 정리하세요: CommentQueryPersistenceAdapter에서 `@Slf4j` 어노테이션과 관련 import를 제거하거나(캐시 로직 제거에 따라 불필요하면) 향후 사용할 계획이 있어 남겨둘 경우에는 log를 참조하는 메서드(예: 캐시 관련 메서드명 또는 생성자)에 적절한 로그 호출을 추가해 사용되도록 하십시오.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Dockerfile`:
- Line 11: The Dockerfile's COPY line "COPY ./build/libs/*.jar app.jar" can
match both the fat executable JAR and the plain JAR, causing nondeterministic
copy and "no main manifest attribute" runtime failures; fix by either disabling
plain JAR generation in the Gradle build via adding a jar { enabled = false }
configuration in build.gradle, or make the Docker COPY explicit to target the
executable JAR (e.g., replace the wildcard COPY ./build/libs/*.jar app.jar with
a pattern or explicit name that only matches the boot jar, e.g., COPY
./build/libs/thip-*.jar app.jar).
In
`@src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java`:
- Around line 25-26: Change the field access from public final to private final
for rootCommentShowUseCase and childCommentShowUseCase in CommentQueryController
to enforce encapsulation; keep them final so `@RequiredArgsConstructor` still
generates the constructor and no other changes are needed to usages (update any
direct external accesses to use methods if present).
In
`@src/main/java/konkuk/thip/comment/adapter/in/web/response/ChildCommentsResponse.java`:
- Around line 5-9: The record ChildCommentsResponse currently exposes a boolean
field isLast which Jackson 2.17.x will serialize as "last"; to fix, annotate the
boolean field with `@JsonProperty`("isLast") in the ChildCommentsResponse
declaration (record ChildCommentsResponse(... boolean isLast ...)) so the JSON
key remains "isLast"; also apply the same fix inside ChildCommentDto for its
boolean fields isLike and isWriter by adding `@JsonProperty`("isLike") and
`@JsonProperty`("isWriter") to those record fields so their JSON names keep the
"is" prefix.
- Around line 20-22: The two boolean components in the ChildCommentsResponse
record, isLike and isWriter, will be serialized by Jackson as "like" and
"writer" due to JavaBeans `is` stripping; update the record so the JSON property
names match the API contract by either annotating the boolean components with
`@JsonProperty`("isLike") and `@JsonProperty`("isWriter") on the record components
(or their accessor methods), or rename the components to like and writer; apply
the chosen change to the ChildCommentsResponse record definition and import
com.fasterxml.jackson.annotation.JsonProperty if you use annotations.
In
`@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheAdapter.java`:
- Around line 32-41: The cache key only uses `#postId` while
findFirstPageRootCommentsFromCache(Long postId, int pageSize) accepts pageSize,
so calls for the same postId but different pageSize can return wrong cached
data; either remove the pageSize parameter and hard-code the page size (e.g., a
private static final DEFAULT_PAGE_SIZE used in
findFirstPageRootCommentsFromCache and passed to
commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc) or include
pageSize in the `@Cacheable` key (update the key expression on the `@Cacheable` on
findFirstPageRootCommentsFromCache to incorporate pageSize, e.g. combine `#postId`
and `#pageSize`) and update any callers accordingly; ensure
CommentCacheKey.ROOT_COMMENTS is still used as the cache name and adjust method
signature/usages if you choose the constant approach.
In
`@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java`:
- Around line 56-64: 현재 CommentCommandPersistenceAdapter에서
rootCommentJpaEntity.incrementDescendantCount()로 메모리상 값을 증가시켜 JPA dirty
checking에 맡기는데, 동시 생성 시 lost update가 발생할 수 있으니 수정하세요: (1) 빠른 해결 —
CommentRepository에 원자적 업데이트 메서드(예: `@Modifying` `@Query`("UPDATE Comment c SET
c.descendantCount = c.descendantCount + 1 WHERE c.id = :id"))를 추가하고
CommentCommandPersistenceAdapter에서 increment 대신 이 저장소 메서드를 호출하도록 변경하거나, (2) 낙관적
잠금 적용 — Comment JPA 엔티티에 `@Version` 필드를 추가하고 incrementDescendantCount()/save 시
OptimisticLockException을 잡아 재시도 로직을 구현해 동시성 충돌을 처리하도록 하세요; 대상 식별자:
CommentCommandPersistenceAdapter, rootCommentJpaEntity,
incrementDescendantCount().
In
`@src/main/java/konkuk/thip/comment/application/service/ChildCommentCreateService.java`:
- Around line 60-73: The notification is sent before the comment is actually
persisted, which can cause stray notifications on save failure; move the call to
sendNotificationsToPostWriter so it runs after Comment.createChildComment(...)
and commentCommandPort.save(...), and pass the persisted comment information
(e.g., savedCommentId or the saved Comment) to the notifier; ensure the sequence
becomes: create child Comment, save via commentCommandPort.save, update post
(post.increaseCommentCount and postHandler.updatePost), then call
sendNotificationsToPostWriter using the saved entity, so notifications are only
sent on successful persistence within the transaction.
- Around line 75-85: The code incorrectly treats command.parentCommentId() as a
root id; instead resolve the actual root id of the parent comment first and use
that for root-scoped queries. Add or call a method on CommentQueryPort to get
the root id for a given comment (e.g.,
findRootIdByCommentId(command.parentCommentId()) or traverse parents until
parentId is null), then call CommentQueryPort.findRootCommentById(rootId) to get
the true root and call CommentQueryPort.findChildCommentById(rootId,
savedCommentId) (or fetch the saved reply directly by id and attach it under the
resolved root) so responses use the real root and child lookups use the resolved
root id rather than command.parentCommentId(); update usages of parentCommentDto
accordingly (in sendNotificationsToParentCommentWriter and
commentQueryMapper.toRootCommentResponseWithChildren).
In
`@src/main/java/konkuk/thip/comment/application/service/RootCommentCreateService.java`:
- Around line 41-66: Move the notification send so it occurs after the comment
is persisted to avoid notifying for a failed save: in
RootCommentCreateService.createRootComment, call commentCommandPort.save(...)
first (store savedCommentId), then update
post.increaseCommentCount()/postHandler.updatePost(...), then fetch
savedCommentDto and only after successful save and updates invoke
sendNotificationsToPostWriter(postQueryDto, actorUser) (or move the earlier
actorUser lookup as needed), and apply the same exact reordering in
ChildCommentCreateService.createChildComment so notifications are sent only
after the comment is successfully saved and related post state updated.
In
`@src/main/java/konkuk/thip/comment/application/service/RootCommentShowService.java`:
- Around line 62-71: The descendantCount is never decremented on child deletion;
update CommentDeleteService so that after calling commentCommandPort.delete(...)
it finds the deleted comment's root/parent and calls decrementDescendantCount()
on that parent (or root) and persists the change via the same command port (or
an existing update method) so descendantCount is accurate; specifically invoke
decrementDescendantCount() for the parent/root entity when a child is deleted
(handle the case where deleted comment is itself a root vs non-root and ensure
no-op when parent is null), ensuring the persisted entity reflects the
decremented descendantCount used by RootCommentShowService.
In `@src/main/java/konkuk/thip/config/CacheConfig.java`:
- Around line 38-40: The BasicPolymorphicTypeValidator currently uses
allowIfBaseType(Object.class) which effectively disables type checks; replace
this broad allowance with a package-restricted or specific-base-class policy so
only your application classes are permitted. Update the
BasicPolymorphicTypeValidator builder used in CacheConfig (the variable named
validator and the place where you call allowIfBaseType(Object.class) and later
pass it to enableDefaultTyping/DefaultTyping.NON_FINAL) to use
allowIfSubType("konkuk.thip") or allowIfBaseType(YourAppBaseClass.class) (or add
explicit allowIfSubType entries for all trusted packages), and remove/avoid
using a global Object.class allowance so Jackson will only deserialize types
from the trusted package(s).
In `@src/main/resources/db/migration/V260107__Add_index_comments.sql`:
- Around line 1-2: Change the DB index created in
V260107__Add_index_comments.sql from CREATE INDEX idx_comments_post_parent ON
comments (post_id, parent_id) to include comment_id (i.e., make it (post_id,
parent_id, comment_id)) so the root-comment cursor pagination WHERE/post_id = ?
AND parent_id IS NULL AND comment_id < ? can use the index for range scans; also
remove or disable the duplicate `@Index` annotation on CommentJpaEntity (the
`@Index` declared around line 17) so the JPA entity does not create a
conflicting/duplicate index and the Flyway migration remains the single source
of truth.
- Around line 1-2: There is a duplicate index definition: remove the `@Index` on
the JPA entity or make the Flyway migration idempotent; either delete the
`@Index`(name = "idx_comments_post_parent", columnList = "post_id, parent_id")
from CommentJpaEntity so Flyway migration V260107__Add_index_comments.sql is the
single source of truth, or modify the SQL in V260107__Add_index_comments.sql to
create the index safely (e.g., use a “CREATE INDEX IF NOT EXISTS” style
statement supported by your DB) to avoid duplicate creation errors.
In `@src/main/resources/db/migration/V260119__Add_root_comment_id_column.sql`:
- Around line 6-25: The migration uses a recursive CTE (comment_path) and an
UPDATE ... JOIN which requires MySQL 8.0.1+; update the migration so it either
(A) asserts or checks the server version before running (and fails fast with a
clear message if version < 8.0.1), or (B) provides a fallback non-CTE
implementation that populates comments.root_comment_id (e.g., iterative
processing via a temporary table/loop over parent_id levels or multiple JOINs)
and targets the same table/column names (comments, root_comment_id, parent_id,
comment_id) to produce equivalent results for older MySQL. Ensure the migration
logic explicitly excludes root rows (parent_id IS NOT NULL) like the original
and documents the version requirement if you choose the assert approach.
In
`@src/test/java/konkuk/thip/comment/adapter/in/web/ChildCommentShowApiTest.java`:
- Around line 207-214: The loop assumes sequential auto-increment IDs by using
comment1_1.getCommentId() + i - 1 for findById, which is brittle; instead fetch
the actual child comment list (e.g., via commentJpaRepository query or a getter
on comment1_1) and iterate that List to update created_at with
jdbcTemplate.update for each child's getCommentId(); replace the
arithmetic-based findById lookup with direct list indexing/streaming over the
retrieved child entities to ensure correct IDs are used.
In
`@src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheTest.java`:
- Around line 31-35: The test class CommentCacheTest uses `@Transactional` which
rolls back DB state but not Spring Cache, so cached entries can leak between
tests; add an explicit cache cleanup in an `@AfterEach` method (use the existing
cacheManager to clear the cache keyed by CommentCacheKey.ROOT_COMMENTS) or
annotate the class with `@DirtiesContext` to reset caches between tests; implement
one of these in CommentCacheTest to ensure
cacheManager.getCache(CommentCacheKey.ROOT_COMMENTS).clear() runs after each
test.
---
Outside diff comments:
In
`@src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordControllerTest.java`:
- Around line 76-85: Update the test missing_roomId to make the DisplayName and
assertion consistent with Spring's behavior: change `@DisplayName`("roomId가 없을 때
400 error") to `@DisplayName`("roomId가 없을 때 404 error") and replace the generic
.andExpect(status().is4xxClientError()) with .andExpect(status().isNotFound())
for the request to "/rooms//password" so the test explicitly asserts the
expected 404 from handler mapping failure.
---
Nitpick comments:
In `@Dockerfile`:
- Around line 6-8: 환경변수 JAVA_TOOL_OPTIONS에 설정된 "-XX:MaxRAMPercentage=75.0
-XX:+ExitOnOutOfMemoryError"는 힙을 컨테이너 메모리의 75%로 고정하므로 메타스페이스·스레드 스택·코드 캐시·다이렉트
버퍼(예: Netty) 등에 남는 여유가 25%로 제한됩니다; 소스에서 JAVA_TOOL_OPTIONS와
"-XX:MaxRAMPercentage=75.0" 설정을 찾아 애플리케이션의 스레드/다이렉트 버퍼 사용량을 기준으로 부하 테스트를 수행하고
필요하면 MaxRAMPercentage 값을 낮추거나 운영 문서에 테스트 결과와 권장값을 명시하도록 변경하세요 (또한
"-XX:+ExitOnOutOfMemoryError"는 유지하되, OOM 시 재시작 전략을 배포 설정에 반영하십시오).
In `@loadtest/comment/root_comment_show.js`:
- Around line 4-7: Replace the hardcoded constants BASE_URL, TARGET_POST_ID, and
POST_TYPE with environment-backed values using __ENV so the script is reusable
across environments; read BASE_URL and POST_TYPE from __ENV with sensible
defaults, and read TARGET_POST_ID from __ENV and convert it to a number (e.g.,
parseInt) with a fallback to 1 to avoid string/number issues when the env var is
provided. Ensure you update any code that uses TARGET_POST_ID to expect a number
and keep the same constant names (BASE_URL, TARGET_POST_ID, POST_TYPE) so
references remain unchanged.
In `@src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java`:
- Around line 96-110: The current in-memory mutations in
CommentJpaEntity.incrementDescendantCount() and decrementDescendantCount() can
suffer lost-update under concurrent child create/delete; replace or supplement
these by adding atomic DB-level updates in the repository: create `@Modifying`
methods (e.g. incrementDescendantCount(commentId) and
decrementDescendantCount(commentId) with a WHERE guard to prevent negative
counts) and call those repository methods when adjusting descendant counts
instead of—or immediately after—mutating the entity field so the count change is
performed atomically at the database level.
In `@src/main/java/konkuk/thip/comment/adapter/out/mapper/CommentMapper.java`:
- Around line 12-24: The toJpaEntity method currently accepts five parameters
(Comment comment, PostJpaEntity postJpaEntity, UserJpaEntity userJpaEntity,
CommentJpaEntity parentCommentJpaEntity, CommentJpaEntity rootCommentJpaEntity);
to make it extensible, introduce a parameter object (e.g., CommentEntityContext)
that aggregates these fields and change the signature to
toJpaEntity(CommentEntityContext ctx); inside, read ctx.getComment(),
ctx.getPostJpaEntity(), ctx.getUserJpaEntity(), ctx.getParentCommentJpaEntity(),
ctx.getRootCommentJpaEntity() and use them to build CommentJpaEntity via
CommentJpaEntity.builder(); update all callers to construct and pass
CommentEntityContext instead of multiple parameters.
In
`@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java`:
- Around line 38-70: The save() method currently handles multiple
responsibilities (finding parent/root, updating descendant count, and mapping to
JPA) — refactor by extracting helper methods: implement a private
resolveRootComment(CommentJpaEntity parentCommentJpaEntity) that returns the
correct root (parent.getRoot() != null ? parent.getRoot() : parent) and a
private ensureIncrementDescendantCount(CommentJpaEntity root) that encapsulates
rootCommentJpaEntity.incrementDescendantCount(); then update save() to call
userJpaRepository.findByUserId(...), findPostJpaEntity(...), fetch parent via
commentJpaRepository.findByCommentId(...), call resolveRootComment(...) and
ensureIncrementDescendantCount(...), and finally call
commentMapper.toJpaEntity(...) and commentJpaRepository.save(...).
In
`@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java`:
- Around line 20-41: Update the implementation and docs for
findLatestRootCommentsWithDeleted to reflect that caching is not used in this
PR: remove the commented-out commentCacheAdapter field and the commented
cache-branch in the method, and change the Javadoc on the method to state that
root comments are always fetched from the DB via commentJpaRepository (use
lastRootCommentId logic with Cursor and size); optionally add a short TODO
referencing a future issue to reintroduce CommentCacheAdapter-based first-page
caching when implemented.
- Around line 27-60: Both findLatestRootCommentsWithDeleted and
findAllDescendantComments duplicate cursor handling and CursorBasedList
conversion (extracting size/lastId, calling repository, then
CursorBasedList.of(...) with commentQueryDto -> new Cursor(...)). Create a small
private helper (e.g., toCursorBasedList or buildCursorBasedList) that accepts
the fetched List<CommentQueryDto> and page size (and/or a
Function<CommentQueryDto,String> if needed) and returns CursorBasedList by
encapsulating the lambda that builds the nextCursor from
commentQueryDto.commentId(), then replace the inline CursorBasedList.of(...)
usages in findLatestRootCommentsWithDeleted and findAllDescendantComments to
call that helper; keep repository calls and cursor lastId/size extraction in
each method.
- Around line 9-14: CommentQueryPersistenceAdapter 클래스에 추가된 `@Slf4j와`
lombok.extern.slf4j.Slf4j import가 실제로 사용되지 않으므로 정리하세요:
CommentQueryPersistenceAdapter에서 `@Slf4j` 어노테이션과 관련 import를 제거하거나(캐시 로직 제거에 따라
불필요하면) 향후 사용할 계획이 있어 남겨둘 경우에는 log를 참조하는 메서드(예: 캐시 관련 메서드명 또는 생성자)에 적절한 로그 호출을
추가해 사용되도록 하십시오.
In
`@src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java`:
- Line 13: The method name findRootCommentId is misleading because it returns a
CommentQueryDto; rename the method to a name that reflects the DTO return (e.g.,
findRootCommentDtoByCommentId or findRootCommentById) in the
CommentQueryRepository interface and update all implementations and call sites
to use the new name; ensure method signatures, imports and any tests referencing
findRootCommentId are updated accordingly to prevent compile errors.
In
`@src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java`:
- Around line 127-161: The method name findAllDescendantCommentsByCreatedAtAsc
is misleading because the implementation sorts by comment.commentId.asc() and
uses commentId for cursor pagination; rename the method to reflect the actual
sort/key (e.g., findAllDescendantCommentsByCommentIdAsc) in
CommentQueryRepositoryImpl and update the declaration in the
CommentQueryRepository interface accordingly, then update all callers/usages to
the new name to keep API and implementation consistent.
In
`@src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java`:
- Around line 57-64: Rename the misleading plural parameter in method
toRootCommentResponseWithChildren from CommentQueryDto children to
CommentQueryDto child and update all usages inside the method (use child when
calling toReply) so names accurately reflect a single object; leave the creation
behavior from toRoot and the subsequent rootDto.replyList().add(...) unchanged.
In
`@src/main/java/konkuk/thip/comment/application/port/in/RootCommentShowUseCase.java`:
- Around line 1-9: The inbound port RootCommentShowUseCase currently returns a
web-adapter type RootCommentsResponse (from adapter.in.web.response), creating a
port→adapter dependency; change the port to return an application-layer DTO
(e.g., create a RootCommentsDto or RootCommentsResponse in application.port.in
or application.dto package) and update
RootCommentShowUseCase.showRootCommentsOfPost(CommentShowAllQuery) to use that
DTO, then map between the application DTO and the web adapter
RootCommentsResponse inside the web adapter (controller/mapper) instead of in
the port.
In
`@src/main/java/konkuk/thip/comment/application/service/RootCommentCreateService.java`:
- Around line 68-83: The sendNotificationsToPostWriter method is duplicated in
RootCommentCreateService and ChildCommentCreateService; extract this shared
logic into a common helper or superclass (e.g., CommentNotificationHelper or
abstract CommentCreateService base) and move the implementation there so both
RootCommentCreateService and ChildCommentCreateService call the single method,
referencing PostQueryDto, User, feedNotificationOrchestrator and
roomNotificationOrchestrator; also add a safe default branch in the switch over
PostType (or throw/ log an exception) so new PostType values won't silently drop
notifications.
In
`@src/main/java/konkuk/thip/comment/application/service/RootCommentShowService.java`:
- Around line 29-32: Remove the commented-out annotations and unused imports:
delete the commented lines "// `@Transactional`(readOnly = true)" and "//
`@Unfiltered`" from the showRootCommentsOfPost method in RootCommentShowService
and remove the corresponding unused import statements for Transactional and
Unfiltered at the top of the file; ensure imports are cleaned so no unused
symbols remain and run organiser/IDE optimizer or mvn/gradle build to confirm no
compilation warnings.
In `@src/main/java/konkuk/thip/config/CacheConfig.java`:
- Around line 59-60: The Redis TTL is hardcoded in CacheConfig (the
RedisCacheConfiguration config setup) — make it configurable via application
configuration (e.g., application.yml) by introducing a property like
cache.redis.ttlMinutes and reading it into CacheConfig (use `@Value` or a
`@ConfigurationProperties` bean) and then use that value to set
entryTtl(Duration.ofMinutes(...)) instead of the literal 5; ensure a sensible
default is provided if the property is missing and update any tests or
documentation accordingly.
In `@src/main/resources/db/migration/V260123__Add_descendant_count.sql`:
- Around line 1-17: The migration currently runs a single UPDATE ... JOIN
against the comments table which can lock large tables; change the update to
perform batched updates instead by iterating over root_comment_id groups (or
using LIMIT with an indexed selection) and updating descendant_count in smaller
chunks until complete; specifically modify the logic around the UPDATE comments
parent JOIN (referencing comments, descendant_count, root_comment_id, parent_id,
status) to apply batch-sized updates (e.g., select affected parent.comment_id
values in pages and update those parents) rather than one monolithic UPDATE to
avoid long table locks in production.
In `@src/test/java/konkuk/thip/comment/domain/CommentTest.java`:
- Around line 34-44: Add an assertion to verify that a newly created root
comment's descendantCount is initialized to 0: in the test method
createRootComment_valid() after creating the Comment via
Comment.createRootComment(CONTENT, POST_ID, CREATOR_ID, FEED), call
comment.getDescendantCount() and assert it equals 0 so the new descendantCount
field's default behavior is validated.
In `@src/test/java/konkuk/thip/common/util/TestEntityFactory.java`:
- Around line 264-278: Extract the duplicated root calculation into a private
static helper in TestEntityFactory (e.g., private static CommentJpaEntity
computeRoot(CommentJpaEntity parentComment)) that returns
parentComment.getRoot() != null ? parentComment.getRoot() : parentComment, then
replace both inline occurrences with calls to computeRoot(parentComment) and
ensure the helper is used wherever root is resolved for reply CommentJpaEntity
creation.
|
|
||
| COPY ./build/libs/*.jar ./app.jar | ||
| # 빌드된 JAR 파일 복사 | ||
| COPY ./build/libs/*.jar app.jar |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine build.gradle configuration
find . -name "build.gradle" -o -name "build.gradle.kts" | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 82
🏁 Script executed:
# Also check if there's a Dockerfile and examine it
find . -name "Dockerfile" | head -5Repository: THIP-TextHip/THIP-Server
Length of output: 80
🏁 Script executed:
# Read build.gradle to check jar and bootJar configuration
cat -n ./build.gradleRepository: THIP-TextHip/THIP-Server
Length of output: 4547
🏁 Script executed:
# Read Dockerfile to see the COPY statement
cat -n ./DockerfileRepository: THIP-TextHip/THIP-Server
Length of output: 504
와일드카드 COPY로 인한 plain JAR 복사 위험: 컨테이너 실행 오류 가능
build.gradle에서 jar { enabled = false }를 명시하지 않았으므로, Spring Boot 3.5.0은 기본적으로 build/libs/thip-0.0.1-SNAPSHOT.jar(실행 가능한 fat JAR)와 build/libs/thip-0.0.1-SNAPSHOT-plain.jar(plain JAR) 두 파일을 모두 생성합니다.
Dockerfile의 COPY ./build/libs/*.jar app.jar은 두 파일 모두에 매칭되지만 대상(app.jar)이 디렉토리가 아니므로 Docker 명세에 위배됩니다. 빌더 버전에 따라 비결정적으로 plain JAR이 복사될 수 있으며, 이 경우 ENTRYPOINT에서 "no main manifest attribute" 오류로 컨테이너 기동이 실패합니다.
해결 방법:
build.gradle에 다음을 추가하여 plain JAR 생성을 비활성화하세요:
jar {
enabled = false
}또는 Dockerfile의 COPY를 명시적 파일명으로 변경하세요:
COPY ./build/libs/thip-*.jar app.jar🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Dockerfile` at line 11, The Dockerfile's COPY line "COPY ./build/libs/*.jar
app.jar" can match both the fat executable JAR and the plain JAR, causing
nondeterministic copy and "no main manifest attribute" runtime failures; fix by
either disabling plain JAR generation in the Gradle build via adding a jar {
enabled = false } configuration in build.gradle, or make the Docker COPY
explicit to target the executable JAR (e.g., replace the wildcard COPY
./build/libs/*.jar app.jar with a pattern or explicit name that only matches the
boot jar, e.g., COPY ./build/libs/thip-*.jar app.jar).
| public final RootCommentShowUseCase rootCommentShowUseCase; | ||
| public final ChildCommentShowUseCase childCommentShowUseCase; |
There was a problem hiding this comment.
필드 접근 제어자를 private으로 변경하세요
public final로 선언되어 있어 외부에서 직접 접근이 가능합니다. @RequiredArgsConstructor는 private final 필드에 대해서도 생성자를 생성하므로, 캡슐화를 위해 private final로 변경해야 합니다.
🔒 수정 제안
- public final RootCommentShowUseCase rootCommentShowUseCase;
- public final ChildCommentShowUseCase childCommentShowUseCase;
+ private final RootCommentShowUseCase rootCommentShowUseCase;
+ private final ChildCommentShowUseCase childCommentShowUseCase;📝 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 final RootCommentShowUseCase rootCommentShowUseCase; | |
| public final ChildCommentShowUseCase childCommentShowUseCase; | |
| private final RootCommentShowUseCase rootCommentShowUseCase; | |
| private final ChildCommentShowUseCase childCommentShowUseCase; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java`
around lines 25 - 26, Change the field access from public final to private final
for rootCommentShowUseCase and childCommentShowUseCase in CommentQueryController
to enforce encapsulation; keep them final so `@RequiredArgsConstructor` still
generates the constructor and no other changes are needed to usages (update any
direct external accesses to use methods if present).
| public record ChildCommentsResponse( | ||
| List<ChildCommentDto> childComments, | ||
| String nextCursor, | ||
| boolean isLast | ||
| ) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the complete ChildCommentsResponse.java file
cat -n src/main/java/konkuk/thip/comment/adapter/in/web/response/ChildCommentsResponse.javaRepository: THIP-TextHip/THIP-Server
Length of output: 917
🏁 Script executed:
# Check if ChildCommentDto exists and examine it
find . -name "ChildCommentDto.java" -type fRepository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check build configuration for Jackson and Spring Boot versions
fd "build.gradle|pom.xml" -type f | head -5Repository: THIP-TextHip/THIP-Server
Length of output: 238
🏁 Script executed:
# Search for Jackson configuration or annotations in the codebase
rg "@JsonProperty|@JsonSerialize|ObjectMapper" --type java | grep -i "comment\|response" | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 1547
🏁 Script executed:
# Find build configuration files
find . -maxdepth 3 -name "build.gradle" -o -name "pom.xml" | head -5Repository: THIP-TextHip/THIP-Server
Length of output: 82
🏁 Script executed:
# Read build.gradle if it exists
cat src/../build.gradle 2>/dev/null || cat build.gradle 2>/dev/null || echo "Not found"Repository: THIP-TextHip/THIP-Server
Length of output: 3630
🏁 Script executed:
# Search for Spring Boot and Jackson version declarations
rg "spring-boot|jackson|org.springframework" --type-list | head -3Repository: THIP-TextHip/THIP-Server
Length of output: 457
🏁 Script executed:
# Look for any application.yml or application.properties with Jackson config
find . -name "application*.yml" -o -name "application*.properties" | xargs cat 2>/dev/nullRepository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check for any ObjectMapper configuration beans
rg "ObjectMapper|@Configuration" --type-add "gradle:build.gradle" src/main/java -A 3 | grep -A 3 "ObjectMapper\|Jackson"Repository: THIP-TextHip/THIP-Server
Length of output: 8424
boolean isLast, isLike, isWriter 필드: Jackson에서 is 접두사 제거 문제 확인됨
Spring Boot 3.5.0에 포함된 Jackson 2.17.x는 Java record의 boolean isLast 등의 필드에서 생성된 접근자 메서드(isLast(), isLike(), isWriter())를 JavaBeans boolean getter로 인식합니다. 이로 인해 JSON 직렬화 시 is 접두사가 자동으로 제거되어, API 응답에서 last, like, writer로 반환됩니다.
코드베이스의 다른 응답 클래스들(BaseResponse.java, ErrorResponse.java)은 이미 @JsonProperty("isSuccess")를 명시적으로 사용하여 이 문제를 해결하고 있습니다. ChildCommentsResponse.java도 동일한 패턴을 따라 각 boolean 필드에 @JsonProperty 어노테이션을 추가해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/konkuk/thip/comment/adapter/in/web/response/ChildCommentsResponse.java`
around lines 5 - 9, The record ChildCommentsResponse currently exposes a boolean
field isLast which Jackson 2.17.x will serialize as "last"; to fix, annotate the
boolean field with `@JsonProperty`("isLast") in the ChildCommentsResponse
declaration (record ChildCommentsResponse(... boolean isLast ...)) so the JSON
key remains "isLast"; also apply the same fix inside ChildCommentDto for its
boolean fields isLike and isWriter by adding `@JsonProperty`("isLike") and
`@JsonProperty`("isWriter") to those record fields so their JSON names keep the
"is" prefix.
| int likeCount, | ||
| boolean isLike, | ||
| boolean isWriter |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Jackson boolean field "is" prefix serialization JSON property name
💡 Result:
Jackson derives JSON property names from JavaBean accessor names, not the raw field name. By default it removes get/set/is prefixes from methods, so a boolean getter isActive() becomes JSON property "active" (not "isActive"). [1]
How to keep the is in JSON (e.g., "isActive")
-
Explicitly name it
- Java:
@JsonProperty("isActive")on the field or getter (make sure you importcom.fasterxml.jackson.annotation.JsonProperty). [2] - Kotlin: prefer
@get:JsonProperty("isActive")on the property when needed. [3]
- Java:
-
Use
getIsXxx()instead ofisXxx()- If your getter is
getIsActive(), Jackson will stripgetand keep"isActive"as the property name. This also avoids the “boolean field named withis” confusion that can lead to double-properties / mismatches. [2]
- If your getter is
-
Best practice (avoid the issue): don’t name the field with
is- Use
private boolean active;withisActive()→ JSON"active". This aligns with JavaBeans conventions and avoids edge cases. [2]
- Use
Kotlin-specific “isXxx” edge cases
is-prefixed properties have had known quirks in jackson-module-kotlin (including stripping is and breaking round-trip serialization) tracked in the project issues. [3]
Sources:
[1] Jackson databind Javadoc: default naming removes get/set/is prefixes (PropertyNamingStrategy)
[2] Stack Overflow: @JsonProperty + isEnabled naming confusion / correct import
[3] FasterXML jackson-module-kotlin issue #80 (“is” prefix stripped / serialization-deserialization problems)
🌐 Web query:
Java record Jackson serialization isLike isWriter boolean fields
💡 Result:
Jackson follows JavaBeans rules for boolean getters: an accessor named isXxx() is treated as the JSON property "xxx" (it strips the is prefix). (stackoverflow.com)
With Java records, the accessor method name is the component name, so:
record Reaction(boolean isLike, boolean isWriter) {}generates isLike() / isWriter(), and Jackson will typically serialize as:
{"like":true,"writer":false}Fix options
A) Recommended: don’t name boolean components with is
record Reaction(boolean like, boolean writer) {}(Produces "like" / "writer".)
B) If your JSON must be "isLike" / "isWriter": rename via @JsonProperty
record Reaction(
`@com.fasterxml.jackson.annotation.JsonProperty`("isLike") boolean like,
`@com.fasterxml.jackson.annotation.JsonProperty`("isWriter") boolean writer
) {}C) Config-based (global): stop auto-detecting isXxx() as getters
Disable AUTO_DETECT_IS_GETTERS so Jackson won’t apply the is-getter naming rule unless you explicitly annotate. (cnblogs.com)
Citations:
- 1: https://stackoverflow.com/questions/32270422/jackson-renames-primitive-boolean-field-by-removing-is?utm_source=openai
- 2: https://www.cnblogs.com/huey/p/5626781.html?utm_source=openai
🌐 Web query:
Jackson MapperFeature USE_STD_BEAN_NAMING boolean field serialization
💡 Result:
MapperFeature.USE_STD_BEAN_NAMING does not change the fact that Jackson strips the is/get prefix when it derives a JSON property name from a getter. For a boolean getter like isActive(), Jackson still treats the logical property name as active (so JSON becomes "active": true), because that’s standard JavaBean property naming.
What USE_STD_BEAN_NAMING does change is the decapitalization rule for the remainder of the name (mainly affecting acronym/leading-caps cases). Jackson’s default historically lowercased leading caps aggressively; standard JavaBeans keeps leading acronyms intact (e.g., getURL() → URL, not url). The feature exists to force that standard behavior. [1]
Why this matters for boolean serialization
Example (Kotlin/Java-style naming):
- Getter
isUSDListing():- default Jackson naming may produce
"usdlisting"(lowercased) - with
USE_STD_BEAN_NAMING=true, it becomes"USDListing"(standard bean rule) [1][2]
- default Jackson naming may produce
But in both cases, the leading is is still treated as a getter prefix (so you won’t get "isUSDListing" just by enabling this feature).
How to get "isXxx" in JSON
Use an explicit name override, e.g.:
@JsonProperty("isActive")on field/getter, or- change the accessor to
getIsActive()(so the logical bean name becomesisActive)
(Manual override is also the common recommendation when you need a specific casing like aBoolean vs ABoolean.) [3]
Enabling the feature
ObjectMapper mapper = JsonMapper.builder()
.enable(MapperFeature.USE_STD_BEAN_NAMING)
.build();Sources:
[1] Jackson MapperFeature.USE_STD_BEAN_NAMING Javadoc (behavior + URL example) (javadoc.io)
[2] Example showing isUSDListing serialization and workaround via USE_STD_BEAN_NAMING (thecodinganalyst.com)
[3] Explanation of USE_STD_BEAN_NAMING casing impact and using @JsonProperty to force a desired name (stackoverflow.com)
Citations:
- 1: https://javadoc.io/static/com.fasterxml.jackson.core/jackson-databind/2.16.1/com/fasterxml/jackson/databind/MapperFeature.html?utm_source=openai
- 2: https://www.thecodinganalyst.com/issue%20and%20workaround/issue-with-jackson-serialization-of-is-getter/?utm_source=openai
- 3: https://stackoverflow.com/questions/53108660/inconsistent-jackson-unrecognized-field-due-to-capitalization?utm_source=openai
boolean isLike, boolean isWriter: Jackson이 is 접두사를 제거해 like, writer로 직렬화될 수 있음
Jackson은 JavaBeans 규칙에 따라 isLike(), isWriter() 접근자 메서드에서 is 접두사를 제거하며, 이로 인해 JSON 프로퍼티명이 like, writer로 직렬화됩니다. Java record에서도 동일하게 동작하여 API 계약이 클라이언트와 불일치할 수 있습니다.
아래 중 하나를 적용해 주세요:
@JsonProperty("isLike")/@JsonProperty("isWriter")어노테이션 명시- 필드명을
like,writer(접두사 없이) 로 변경
🛠️ `@JsonProperty` 적용 방법
public record ChildCommentDto(
Long commentId,
String parentCommentCreatorNickname,
Long creatorId,
String creatorProfileImageUrl,
String creatorNickname,
String aliasName,
String aliasColor,
String postDate,
String content,
int likeCount,
+ `@JsonProperty`("isLike")
boolean isLike,
+ `@JsonProperty`("isWriter")
boolean isWriter
) {}📝 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.
| int likeCount, | |
| boolean isLike, | |
| boolean isWriter | |
| int likeCount, | |
| `@JsonProperty`("isLike") | |
| boolean isLike, | |
| `@JsonProperty`("isWriter") | |
| boolean isWriter |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/konkuk/thip/comment/adapter/in/web/response/ChildCommentsResponse.java`
around lines 20 - 22, The two boolean components in the ChildCommentsResponse
record, isLike and isWriter, will be serialized by Jackson as "like" and
"writer" due to JavaBeans `is` stripping; update the record so the JSON property
names match the API contract by either annotating the boolean components with
`@JsonProperty`("isLike") and `@JsonProperty`("isWriter") on the record components
(or their accessor methods), or rename the components to like and writer; apply
the chosen change to the ChildCommentsResponse record definition and import
com.fasterxml.jackson.annotation.JsonProperty if you use annotations.
| @Cacheable( | ||
| value = CommentCacheKey.ROOT_COMMENTS, | ||
| key = "#postId", | ||
| unless = "#result == null || #result.isEmpty()" | ||
| ) | ||
| public List<CommentQueryDto> findFirstPageRootCommentsFromCache(Long postId, int pageSize) { | ||
| log.debug("Cache miss - Loading first page root comments from DB for postId: {}", postId); | ||
|
|
||
| return commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, null, pageSize); | ||
| } |
There was a problem hiding this comment.
pageSize가 캐시 키에 포함되지 않아 잘못된 캐시 데이터 반환 가능
@Cacheable의 key가 #postId만 사용하지만, 메서드 파라미터로 pageSize를 받고 있습니다. 동일한 postId에 대해 다른 pageSize로 호출될 경우, 이전에 캐시된 다른 크기의 결과가 반환됩니다.
현재 첫 페이지 전용으로 pageSize가 고정이라면 문제가 없겠지만, 파라미터로 노출하는 이상 방어적으로 처리하는 것이 좋습니다. pageSize를 메서드 파라미터에서 제거하고 상수로 관리하거나, 캐시 키에 포함시키는 것을 고려해 주세요.
옵션 1: pageSize를 캐시 키에서 제외하고 상수 사용
+ private static final int FIRST_PAGE_SIZE = 10;
+
`@Cacheable`(
value = CommentCacheKey.ROOT_COMMENTS,
key = "#postId",
unless = "#result == null || `#result.isEmpty`()"
)
- public List<CommentQueryDto> findFirstPageRootCommentsFromCache(Long postId, int pageSize) {
+ public List<CommentQueryDto> findFirstPageRootCommentsFromCache(Long postId) {
log.debug("Cache miss - Loading first page root comments from DB for postId: {}", postId);
- return commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, null, pageSize);
+ return commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, null, FIRST_PAGE_SIZE);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheAdapter.java`
around lines 32 - 41, The cache key only uses `#postId` while
findFirstPageRootCommentsFromCache(Long postId, int pageSize) accepts pageSize,
so calls for the same postId but different pageSize can return wrong cached
data; either remove the pageSize parameter and hard-code the page size (e.g., a
private static final DEFAULT_PAGE_SIZE used in
findFirstPageRootCommentsFromCache and passed to
commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc) or include
pageSize in the `@Cacheable` key (update the key expression on the `@Cacheable` on
findFirstPageRootCommentsFromCache to incorporate pageSize, e.g. combine `#postId`
and `#pageSize`) and update any callers accordingly; ensure
CommentCacheKey.ROOT_COMMENTS is still used as the cache name and adjust method
signature/usages if you choose the constant approach.
| BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder() | ||
| .allowIfBaseType(Object.class) | ||
| .build(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "CacheConfig.java" | head -5Repository: THIP-TextHip/THIP-Server
Length of output: 119
🌐 Web query:
Jackson BasicPolymorphicTypeValidator allowIfBaseType Object.class security implications
💡 Result:
Using BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class) is effectively equivalent to “allow polymorphic deserialization for anything” and largely defeats the point of having a validator.
Why it’s dangerous
- With Jackson polymorphic typing (“default typing” /
@JsonTypeInfo(use = Id.CLASS)), the JSON can carry a class name to instantiate. If the input is attacker-controlled, this can enable “deserialization gadget” attacks, including remote code execution (RCE) when suitable gadget classes exist on the classpath. Jackson explicitly warns about this risk for global default typing with untrusted input. (github.com) java.lang.Objectis a fundamentally unsafe base type because every class is assignable to it; so “allow if base type is Object” means the base-type check returns “allowed” and Jackson proceeds with subtype resolution with no meaningful restriction. Jackson’s own guidance calls outObject(andSerializable) as unsafe bases that require careful validation. (cowtowncoder.medium.com)- Jackson even provides a built-in validator concept specifically to block unsafe base types like
Object; the Javadoc notes that for unsafe bases (likeObject) you need custom handling—highlighting that treatingObjectas allowed is not “safe by default.” (fasterxml.github.io)
Practical security implication
If you combine:
activateDefaultTyping(...)(or equivalent polymorphic typing), andallowIfBaseType(Object.class), and- you deserialize untrusted JSON,
then you are back in the classic risk zone that has produced many Jackson polymorphic-typing CVEs over time (gadget chains, blacklist bypasses, etc.). (github.com)
Safer alternatives (typical)
- Avoid global default typing for untrusted inputs; prefer explicit DTO types.
- If you truly need polymorphism, whitelist narrowly, e.g.
allowIfSubType("com.mycompany.allowed.")or explicit subtype classes, rather thanObject.class. (cowtowncoder.medium.com) - Consider base-type limiting approaches (block
Object/Serializableas bases) and only allow polymorphism where you control the model. (fasterxml.github.io)
Citations:
- 1: https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization?utm_source=openai
- 2: https://cowtowncoder.medium.com/jackson-2-10-safe-default-typing-2d018f0ce2ba?utm_source=openai
- 3: https://fasterxml.github.io/jackson-databind/javadoc/2.11/com/fasterxml/jackson/databind/jsontype/DefaultBaseTypeLimitingValidator.html?utm_source=openai
- 4: https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization?utm_source=openai
- 5: https://cowtowncoder.medium.com/jackson-2-10-safe-default-typing-2d018f0ce2ba?utm_source=openai
- 6: https://fasterxml.github.io/jackson-databind/javadoc/2.11/com/fasterxml/jackson/databind/jsontype/DefaultBaseTypeLimitingValidator.html?utm_source=openai
🌐 Web query:
Jackson DefaultTyping NON_FINAL polymorphic deserialization gadget chain risk
💡 Result:
ObjectMapper DefaultTyping.NON_FINAL turns on global polymorphic deserialization for all non-final types (and arrays of them), meaning Jackson will accept attacker-supplied type metadata and try to instantiate that class during deserialization. This is explicitly called out as a security risk with untrusted input. [1], [2]
Why it’s risky (gadget chains)
If an attacker can send JSON to an endpoint that is deserialized with default typing enabled, they may be able to specify a class on your classpath that acts as a deserialization “gadget” (a class whose construction/setters trigger dangerous behavior), enabling outcomes up to remote code execution (RCE). Jackson historically tried to mitigate this with blocklists, but new gadget types have repeatedly been found (many CVEs). [1], [3], [4]
When exploitation is typically possible
You generally have risk when all of these are true:
- You deserialize untrusted JSON, and
- You enabled default typing (or equivalent
@JsonTypeInfousage that lets input choose a class), and - Your runtime includes a usable gadget class in application/third‑party libraries. [1], [4], [5]
Why NON_FINAL is particularly high-risk
NON_FINAL applies polymorphic typing extremely broadly (“all non-final types”), which greatly increases the number of places an attacker can inject a chosen type compared to narrower modes like JAVA_LANG_OBJECT / OBJECT_AND_NON_CONCRETE. [1], [2]
Mitigations (practical)
- Do not use global default typing for data from untrusted sources. [1], [2]
- If you must support polymorphism, prefer allowlist-based validation via
activateDefaultTyping(PolymorphicTypeValidator, …)(introduced to address these issues more robustly than blocklists). [1], [2] - Use explicit, narrow polymorphism (e.g., annotate specific base types) rather than global defaults; avoid
Id.CLASS/MINIMAL_CLASSwhere possible. [1] - Upgrade Jackson: older lines relied heavily on blocklists; vendor guidance and Jackson’s own CVE criteria emphasize that default typing + untrusted input is the core risk pattern. [4], [6]
Sources: Jackson polymorphic deserialization docs (incl. security warning + NON_FINAL meaning) [1], Jackson DefaultTyping Javadoc security note [2], Spring’s CVE writeup on default typing enabling gadget-based code execution [3], Jackson’s CVE criteria for polymorphic deserialization (default typing + gadgets) [4], Snyk summary of prerequisites (untrusted input + polymorphic type handling) [5], Red Hat explanation of gadget-chain exposure when polymorphic unmarshalling is enabled [6].
🏁 Script executed:
cat -n ./src/main/java/konkuk/thip/config/CacheConfig.javaRepository: THIP-TextHip/THIP-Server
Length of output: 3426
allowIfBaseType(Object.class) — 모든 Java 클래스를 허용하는 과도하게 넓은 타입 허용 범위
Object.class는 모든 Java 타입의 슈퍼타입이므로 사실상 타입 검증을 비활성화하는 것과 동일합니다. 이 설정을 DefaultTyping.NON_FINAL과 함께 사용하면 Jackson은 Redis에서 역직렬화된 JSON의 타입 정보를 검증 없이 수락하게 되며, 임의의 비정적 클래스를 인스턴스화할 수 있습니다. Redis가 침해되거나 외부에서 캐시 값을 조작할 수 있는 경우, 공격자가 가젯 클래스를 통해 원격 코드 실행(RCE)을 포함한 심각한 공격을 수행할 수 있습니다.
애플리케이션 패키지로 제한하여 인가된 클래스만 역직렬화되도록 하세요.
🔒 수정 제안 — 패키지 기반 허용 목록 사용
BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
- .allowIfBaseType(Object.class)
+ .allowIfSubType("konkuk.thip.")
+ .allowIfSubType("java.util.") // List, Map 등 컬렉션 지원
+ .allowIfSubType("java.lang.") // String, Integer 등
.build();📝 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.
| BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder() | |
| .allowIfBaseType(Object.class) | |
| .build(); | |
| BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder() | |
| .allowIfSubType("konkuk.thip.") | |
| .allowIfSubType("java.util.") // List, Map 등 컬렉션 지원 | |
| .allowIfSubType("java.lang.") // String, Integer 등 | |
| .build(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/konkuk/thip/config/CacheConfig.java` around lines 38 - 40, The
BasicPolymorphicTypeValidator currently uses allowIfBaseType(Object.class) which
effectively disables type checks; replace this broad allowance with a
package-restricted or specific-base-class policy so only your application
classes are permitted. Update the BasicPolymorphicTypeValidator builder used in
CacheConfig (the variable named validator and the place where you call
allowIfBaseType(Object.class) and later pass it to
enableDefaultTyping/DefaultTyping.NON_FINAL) to use
allowIfSubType("konkuk.thip") or allowIfBaseType(YourAppBaseClass.class) (or add
explicit allowIfSubType entries for all trusted packages), and remove/avoid
using a global Object.class allowance so Jackson will only deserialize types
from the trusted package(s).
| CREATE INDEX idx_comments_post_parent | ||
| ON comments (post_id, parent_id); No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.sql" | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 835
🏁 Script executed:
fd --extension sql --path "*migration*" | grep -i commentRepository: THIP-TextHip/THIP-Server
Length of output: 301
🏁 Script executed:
rg -n "INDEX.*comment" --type sql -iRepository: THIP-TextHip/THIP-Server
Length of output: 171
🏁 Script executed:
rg -n "@Index" --type java -B 2 -A 3 | head -50Repository: THIP-TextHip/THIP-Server
Length of output: 1334
🏁 Script executed:
rg -n "comment_id.*parent_id\|parent_id.*comment_id" --type sql -iRepository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
cat -n src/main/resources/db/migration/V260107__Add_index_comments.sqlRepository: THIP-TextHip/THIP-Server
Length of output: 157
🏁 Script executed:
rg -n "comment_id.*parent_id\|ORDER BY.*comment_id\|cursor" --type java -i -A 3 | head -80Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
cat -n src/main/resources/db/migration/V260119__Add_root_comment_id_column.sqlRepository: THIP-TextHip/THIP-Server
Length of output: 1445
🏁 Script executed:
cat -n src/main/resources/db/migration/V260123__Add_descendant_count.sqlRepository: THIP-TextHip/THIP-Server
Length of output: 827
🏁 Script executed:
rg -n "WHERE.*post_id\|findByPostId" --type java -A 5 | grep -A 5 "comment" | head -100Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
grep -r "comment_id <\|comment_id >\|ORDER BY.*comment" src/main --include="*.java" -n -A 2 | head -60Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
fd --extension java -path "*/comment/*" -name "*Repository*" -o -name "*Repository.java"Repository: THIP-TextHip/THIP-Server
Length of output: 236
🏁 Script executed:
rg -n "class CommentRepository\|findByPost\|@Query.*comment" --type java -A 10 | head -100Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
rg -n "Slice<\|Page<\|cursor\|pagination" --type java -B 3 -A 5 -i | grep -i comment -B 5 -A 5 | head -100Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
git log --oneline --all | grep -i "index\|comment\|cursor" | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 1590
🏁 Script executed:
find . -type f -name "*CommentQueryRepository*" -o -name "*CommentCommandRepository*"Repository: THIP-TextHip/THIP-Server
Length of output: 269
🏁 Script executed:
find . -type f -name "*.java" -path "*/comment/*" | grep -i repositoryRepository: THIP-TextHip/THIP-Server
Length of output: 467
🏁 Script executed:
rg -n "findRootComment\|findChildComment\|Slice\|Page\|cursor" -t java -B 2 -A 8 | head -150Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
git log --oneline -20 --all | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 1207
🏁 Script executed:
git diff HEAD~10..HEAD -- "*V260107*" 2>/dev/null || echo "Recent commits not available"Repository: THIP-TextHip/THIP-Server
Length of output: 96
🏁 Script executed:
cat -n src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.javaRepository: THIP-TextHip/THIP-Server
Length of output: 751
🏁 Script executed:
cat -n src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.javaRepository: THIP-TextHip/THIP-Server
Length of output: 7397
🏁 Script executed:
cat -n src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.javaRepository: THIP-TextHip/THIP-Server
Length of output: 2282
🏁 Script executed:
cat src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java | head -50Repository: THIP-TextHip/THIP-Server
Length of output: 1433
🏁 Script executed:
rg -n "post_id.*parent_id.*comment_id\|comment_id.*post_id.*parent_id" --type sql -iRepository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
git log --all --oneline -- src/main/resources/db/migration/V260107* | head -5Repository: THIP-TextHip/THIP-Server
Length of output: 105
🏁 Script executed:
git show --stat 2>/dev/null | head -100 || echo "Git context unavailable"Repository: THIP-TextHip/THIP-Server
Length of output: 5927
🏁 Script executed:
rg -n "idx_comments_post_parent" --type java -B 5 -A 5Repository: THIP-TextHip/THIP-Server
Length of output: 1140
🏁 Script executed:
find . -name "*.md" -o -name "*.txt" | xargs grep -l "index\|comment" 2>/dev/null | head -5Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
git log --all --oneline -- "*.sql" | grep -i "index\|comment" | head -10Repository: THIP-TextHip/THIP-Server
Length of output: 50
루트 댓글 커서 기반 페이지네이션 쿼리를 위해 comment_id 컬럼을 인덱스에 추가 필요
현재 마이그레이션은 (post_id, parent_id) 2-컬럼 인덱스만 생성하지만, 루트 댓글 조회 쿼리는 다음과 같은 형태로 실행됩니다:
WHERE post_id = ? AND parent_id IS NULL AND comment_id < lastRootCommentId
ORDER BY comment_id DESCcomment_id가 인덱스에 포함되지 않으면, DB는 (post_id, parent_id) 조건까지만 인덱스를 활용할 수 있고, comment_id < ? 범위 조건에 대해서는 인덱스 범위 스캔을 수행하지 못해 추가 테이블 액세스가 발생합니다. 성능 최적화를 위해 인덱스를 (post_id, parent_id, comment_id)로 수정해야 합니다.
🛠️ 수정 제안
CREATE INDEX idx_comments_post_parent
- ON comments (post_id, parent_id);
+ ON comments (post_id, parent_id, comment_id);또한 CommentJpaEntity.java 라인 17에 동일한 이름의 @Index 어노테이션이 정의되어 있어 Flyway 마이그레이션과 중복됩니다. JPA 엔티티의 @Index 정의를 제거하거나 마이그레이션에서 생성한 인덱스만 유지하도록 통일이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/db/migration/V260107__Add_index_comments.sql` around lines
1 - 2, Change the DB index created in V260107__Add_index_comments.sql from
CREATE INDEX idx_comments_post_parent ON comments (post_id, parent_id) to
include comment_id (i.e., make it (post_id, parent_id, comment_id)) so the
root-comment cursor pagination WHERE/post_id = ? AND parent_id IS NULL AND
comment_id < ? can use the index for range scans; also remove or disable the
duplicate `@Index` annotation on CommentJpaEntity (the `@Index` declared around line
17) so the JPA entity does not create a conflicting/duplicate index and the
Flyway migration remains the single source of truth.
🧩 Analysis chain
🏁 Script executed:
# First, find the CommentJpaEntity class and check for `@Index` annotation
find . -type f -name "*CommentJpaEntity*" -o -name "*Comment*Entity*.java" | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 219
🏁 Script executed:
# Search for `@Index` annotation related to comments table
rg -n "@Index|@Table" --type java -B 2 -A 5 | grep -A 5 -B 5 "comments\|Comment"Repository: THIP-TextHip/THIP-Server
Length of output: 3141
🏁 Script executed:
# Check test profile configuration for ddl-auto settings
fd --extension "properties|yml|yaml" | xargs grep -l "test" | head -10Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Find all ddl-auto configurations in the project
fd --extension "properties|yml|yaml" -x grep -l "ddl-auto\|ddl_auto" {}Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Search for application configuration files more thoroughly
find . -type f \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \) | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check if there's a test profile configuration
find . -type f -path "*/test/*" \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \)Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# View the CommentJpaEntity file to see the full context
cat -n "./src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java"Repository: THIP-TextHip/THIP-Server
Length of output: 3828
🏁 Script executed:
# Search for all application configuration files in the entire project
find . -type f -name "application*" | grep -E "\.(yml|yaml|properties)$"Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check in resources directory more broadly
find ./src -type f -name "*.yml" -o -name "*.yaml" -o -name "*.properties" | head -30Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Look for the migration file to see its exact content
cat -n "./src/main/resources/db/migration/V260107__Add_index_comments.sql"Repository: THIP-TextHip/THIP-Server
Length of output: 157
🏁 Script executed:
# Search for any Flyway-related configuration or properties
rg -i "flyway\|migration" --type properties --type yamlRepository: THIP-TextHip/THIP-Server
Length of output: 102
🏁 Script executed:
# Check if there's a pom.xml or build.gradle to understand the project structure
find . -maxdepth 2 -type f \( -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" \)Repository: THIP-TextHip/THIP-Server
Length of output: 82
🏁 Script executed:
# Look for any application-test.yml or test-specific configuration
find ./src/test -type f -name "*.yml" -o -name "*.yaml" -o -name "*.properties" 2>/dev/null | head -20Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check the build.gradle for Flyway configuration
cat -n "./build.gradle"Repository: THIP-TextHip/THIP-Server
Length of output: 4547
🏁 Script executed:
# Look for application configuration files without type specification
find . -path ./build -prune -o -type f \( -name "application.yml" -o -name "application.yaml" -o -name "application.properties" \) -printRepository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Search for ddl-auto configuration anywhere in the codebase
rg "ddl-auto|ddl_auto" --type yamlRepository: THIP-TextHip/THIP-Server
Length of output: 216
🏁 Script executed:
# Search for all yaml files in the project
find . -type f -name "*.yaml" -o -name "*.yml"Repository: THIP-TextHip/THIP-Server
Length of output: 427
🏁 Script executed:
# Search for all properties files
find . -type f -name "*.properties"Repository: THIP-TextHip/THIP-Server
Length of output: 110
🏁 Script executed:
# Check main resources directory
ls -la ./src/main/resources/Repository: THIP-TextHip/THIP-Server
Length of output: 298
🏁 Script executed:
# Check what's in the db directory
find ./src/main/resources/db -type fRepository: THIP-TextHip/THIP-Server
Length of output: 835
🏁 Script executed:
# Look for test resources
ls -la ./src/test/resources/ 2>/dev/null || echo "No test resources directory found"Repository: THIP-TextHip/THIP-Server
Length of output: 101
🏁 Script executed:
# Search recursively for any application configuration files
find . -path ./build -prune -o -path ./.git -prune -o -type f \( -iname "application*.yml" -o -iname "application*.yaml" \) -printRepository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check if there's a nested application.yml anywhere in src
find ./src -type f -name "*.yml" -o -name "*.yaml"Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check if there's any explicit Spring Boot configuration in the Dockerfile or deployment configs
find . -path ./build -prune -o -type f \( -name "Dockerfile*" -o -name "docker-compose*" \) -printRepository: THIP-TextHip/THIP-Server
Length of output: 80
🏁 Script executed:
# Look for any environment-specific configurations or profiles
find ./src -type d -name "*profile*" -o -type d -name "*config*"Repository: THIP-TextHip/THIP-Server
Length of output: 137
🏁 Script executed:
# Search for any references to ddl-auto in Java code (like in configuration classes)
rg "ddl.auto|ddl-auto|hibernate.hbm2ddl" --type java -B 2 -A 2Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check the test configuration directory
find ./src/test/java/konkuk/thip/config -type f -name "*.java"Repository: THIP-TextHip/THIP-Server
Length of output: 245
🏁 Script executed:
# Check the main configuration directory
find ./src/main/java/konkuk/thip/config -type f -name "*.java"Repository: THIP-TextHip/THIP-Server
Length of output: 1033
🏁 Script executed:
# Look for TestConfiguration or any test setup files
rg "class.*Test.*Config|@TestConfiguration|ddl|flywaydb" --type java -B 2 -A 2 | head -50Repository: THIP-TextHip/THIP-Server
Length of output: 1340
🏁 Script executed:
# Check for `@SpringBootTest` or integration test setup
rg "@SpringBootTest|@DataJpaTest|setUp\(\)|beforeEach" --type java -B 1 -A 1 | head -50Repository: THIP-TextHip/THIP-Server
Length of output: 3615
🏁 Script executed:
# Look for any Spring Boot properties/configuration in test files or main application resources
find . -path ./build -prune -o -type f -path "*/resources/*" -name "*.yml" -o -name "*.yaml" -o -name "*.properties" | grep -v ".git"Repository: THIP-TextHip/THIP-Server
Length of output: 110
🏁 Script executed:
# Check the Dockerfile to see what configuration it uses
cat -n ./DockerfileRepository: THIP-TextHip/THIP-Server
Length of output: 504
🏁 Script executed:
# Look for application configuration files in src/main/resources
ls -la ./src/main/resources/Repository: THIP-TextHip/THIP-Server
Length of output: 298
🏁 Script executed:
# Check if there's any default Spring Boot configuration being applied implicitly
# Look for any `@Configuration` classes that might set ddl-auto
rg "@Configuration|@EnableJpaRepositories|hibernateProperties|spring.jpa" --type java -B 2 -A 2 | head -100Repository: THIP-TextHip/THIP-Server
Length of output: 6771
🏁 Script executed:
# Search for any BaseJpaEntity that might have configuration
cat -n ./src/main/java/konkuk/thip/common/entity/BaseJpaEntity.javaRepository: THIP-TextHip/THIP-Server
Length of output: 2250
🏁 Script executed:
# Check the actual migration files to see if there are other index definitions
ls -n ./src/main/resources/db/migration/ | grep -i indexRepository: THIP-TextHip/THIP-Server
Length of output: 140
🏁 Script executed:
# Check if there are other migration files that might interact with this index
cat -n ./src/main/resources/db/migration/V260119__Add_root_comment_id_column.sql
cat -n ./src/main/resources/db/migration/V260123__Add_descendant_count.sqlRepository: THIP-TextHip/THIP-Server
Length of output: 2204
🏁 Script executed:
# Check if the migration uses IF NOT EXISTS clause
grep -n "IF NOT EXISTS\|IF\|exists" ./src/main/resources/db/migration/V260107__Add_index_comments.sqlRepository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Verify the exact Spring Boot version and Hibernate version
rg "spring-boot-starter-data-jpa|hibernate-core" ./build.gradle -A 1 -B 1Repository: THIP-TextHip/THIP-Server
Length of output: 226
🏁 Script executed:
# Check if there's a baseline migration or initial schema creation
cat -n ./src/main/resources/db/migration/V1__BaseLine.sql | head -50Repository: THIP-TextHip/THIP-Server
Length of output: 50
🏁 Script executed:
# Check if MySQL CREATE INDEX IF NOT EXISTS syntax is used anywhere
grep -r "CREATE INDEX" ./src/main/resources/db/migration/Repository: THIP-TextHip/THIP-Server
Length of output: 171
🏁 Script executed:
# Look for any application profile-specific configuration
find . -type f \( -name "application-*.yml" -o -name "application-*.yaml" -o -name "application-*.properties" \)Repository: THIP-TextHip/THIP-Server
Length of output: 50
JPA 엔티티 @Index 어노테이션 제거 권장
CommentJpaEntity에서 정의한 @Index(name = "idx_comments_post_parent", columnList = "post_id, parent_id")와 Flyway 마이그레이션 파일 V260107__Add_index_comments.sql에서 동일한 인덱스가 중복으로 정의되어 있습니다.
Flyway가 데이터베이스 스키마 관리의 주요 도구인 경우, JPA 엔티티의 @Index 어노테이션을 제거하고 마이그레이션 파일에서만 관리하는 것이 일관성 있는 방식입니다. 또는 마이그레이션 파일을 CREATE INDEX IF NOT EXISTS로 수정하여 중복 생성을 방지할 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/db/migration/V260107__Add_index_comments.sql` around lines
1 - 2, There is a duplicate index definition: remove the `@Index` on the JPA
entity or make the Flyway migration idempotent; either delete the `@Index`(name =
"idx_comments_post_parent", columnList = "post_id, parent_id") from
CommentJpaEntity so Flyway migration V260107__Add_index_comments.sql is the
single source of truth, or modify the SQL in V260107__Add_index_comments.sql to
create the index safely (e.g., use a “CREATE INDEX IF NOT EXISTS” style
statement supported by your DB) to avoid duplicate creation errors.
| WITH RECURSIVE comment_path AS ( | ||
| -- Anchor: 부모가 없는 루트 댓글들 | ||
| -- 주의: 루트 댓글의 root_comment_id 컬럼은 NULL이지만, 자식들에게는 이 루트의 ID(comment_id)가 root_id가 됨 | ||
| SELECT comment_id, comment_id as root_id, 1 as depth | ||
| FROM comments | ||
| WHERE parent_id IS NULL | ||
|
|
||
| UNION ALL | ||
|
|
||
| -- Recursive: 부모를 따라가며 자식 찾기 | ||
| SELECT c.comment_id, cp.root_id, cp.depth + 1 | ||
| FROM comments c | ||
| INNER JOIN comment_path cp ON c.parent_id = cp.comment_id | ||
| WHERE cp.depth < 1000 -- 무한 루프 방지용 깊이 제한 | ||
| ) | ||
| UPDATE comments c | ||
| INNER JOIN comment_path path ON c.comment_id = path.comment_id | ||
| SET c.root_comment_id = path.root_id | ||
| -- [중요] 루트 댓글(parent_id IS NULL)은 root_comment_id가 NULL이어야 하므로 업데이트 대상에서 제외 | ||
| WHERE c.parent_id IS NOT NULL; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
MySQL WITH RECURSIVE CTE UPDATE statement support version requirements
💡 Result:
MySQL support for WITH RECURSIVE (recursive CTEs) in an UPDATE requires MySQL 8.0.1 or later:
- CTEs (both non-recursive and recursive) were added in MySQL 8.0.1. [1]
- The MySQL manual states a
WITHclause is permitted at the beginning ofSELECT,UPDATE, andDELETEstatements, soWITH RECURSIVE ... UPDATE ...is valid syntax (on versions that support CTEs). [2]
If you’re on MySQL 5.7 (or earlier), WITH / WITH RECURSIVE is not supported at all. [1]
Sources:
[1] MySQL 8.0.1 release notes (CTEs, including recursive, introduced)
[2] MySQL Reference Manual: WITH (Common Table Expressions) (allowed before UPDATE)
Recursive CTE를 활용한 데이터 마이그레이션 — MySQL 버전 호환성 확인 필요
WITH RECURSIVE ... UPDATE ... JOIN 구문은 MySQL 8.0.1 이상에서만 지원됩니다. MySQL 5.7 이하에서는 CTE 자체를 사용할 수 없으므로, 운영 DB의 MySQL 버전이 8.0.1 이상인지 반드시 확인해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/db/migration/V260119__Add_root_comment_id_column.sql`
around lines 6 - 25, The migration uses a recursive CTE (comment_path) and an
UPDATE ... JOIN which requires MySQL 8.0.1+; update the migration so it either
(A) asserts or checks the server version before running (and fails fast with a
clear message if version < 8.0.1), or (B) provides a fallback non-CTE
implementation that populates comments.root_comment_id (e.g., iterative
processing via a temporary table/loop over parent_id levels or multiple JOINs)
and targets the same table/column names (comments, root_comment_id, parent_id,
comment_id) to produce equivalent results for older MySQL. Ensure the migration
logic explicitly excludes root rows (parent_id IS NOT NULL) like the original
and documents the version requirement if you choose the assert approach.
| for (int i = 1; i <= 15; i++) { | ||
| CommentJpaEntity childComment = commentJpaRepository.findById((long)(comment1_1.getCommentId() + i - 1)).orElse(null); | ||
| if (childComment != null) { | ||
| jdbcTemplate.update( | ||
| "UPDATE comments SET created_at = ? WHERE comment_id = ?", | ||
| Timestamp.valueOf(base.minusMinutes(30 - i)), childComment.getCommentId()); | ||
| } | ||
| } |
There was a problem hiding this comment.
자식 댓글 ID를 순차적이라고 가정하여 산술 연산으로 조회하는 부분이 취약함
comment1_1.getCommentId() + i - 1로 ID를 계산하여 findById를 호출하고 있는데, auto-increment ID가 반드시 연속적이라는 보장이 없습니다 (다른 엔티티 삽입, 트랜잭션 롤백 등으로 gap 발생 가능). 각 댓글을 리스트에 담아 인덱스로 접근하는 방식이 더 안정적입니다.
제안: 리스트 기반 접근
+ List<CommentJpaEntity> childComments = List.of(
+ comment1_1, comment1_2, comment1_3, comment1_4, comment1_5,
+ comment1_6, comment1_7, comment1_8, comment1_9, comment1_10,
+ comment1_11, comment1_12, comment1_13, comment1_14, comment1_15
+ );
+
for (int i = 1; i <= 15; i++) {
- CommentJpaEntity childComment = commentJpaRepository.findById((long)(comment1_1.getCommentId() + i - 1)).orElse(null);
- if (childComment != null) {
- jdbcTemplate.update(
- "UPDATE comments SET created_at = ? WHERE comment_id = ?",
- Timestamp.valueOf(base.minusMinutes(30 - i)), childComment.getCommentId());
- }
+ CommentJpaEntity childComment = childComments.get(i - 1);
+ jdbcTemplate.update(
+ "UPDATE comments SET created_at = ? WHERE comment_id = ?",
+ Timestamp.valueOf(base.minusMinutes(30 - i)), childComment.getCommentId());
}📝 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.
| for (int i = 1; i <= 15; i++) { | |
| CommentJpaEntity childComment = commentJpaRepository.findById((long)(comment1_1.getCommentId() + i - 1)).orElse(null); | |
| if (childComment != null) { | |
| jdbcTemplate.update( | |
| "UPDATE comments SET created_at = ? WHERE comment_id = ?", | |
| Timestamp.valueOf(base.minusMinutes(30 - i)), childComment.getCommentId()); | |
| } | |
| } | |
| List<CommentJpaEntity> childComments = List.of( | |
| comment1_1, comment1_2, comment1_3, comment1_4, comment1_5, | |
| comment1_6, comment1_7, comment1_8, comment1_9, comment1_10, | |
| comment1_11, comment1_12, comment1_13, comment1_14, comment1_15 | |
| ); | |
| for (int i = 1; i <= 15; i++) { | |
| CommentJpaEntity childComment = childComments.get(i - 1); | |
| jdbcTemplate.update( | |
| "UPDATE comments SET created_at = ? WHERE comment_id = ?", | |
| Timestamp.valueOf(base.minusMinutes(30 - i)), childComment.getCommentId()); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/test/java/konkuk/thip/comment/adapter/in/web/ChildCommentShowApiTest.java`
around lines 207 - 214, The loop assumes sequential auto-increment IDs by using
comment1_1.getCommentId() + i - 1 for findById, which is brittle; instead fetch
the actual child comment list (e.g., via commentJpaRepository query or a getter
on comment1_1) and iterate that List to update created_at with
jdbcTemplate.update for each child's getCommentId(); replace the
arithmetic-based findById lookup with direct list indexing/streaming over the
retrieved child entities to ensure correct IDs are used.
| @SpringBootTest | ||
| @ActiveProfiles("test") | ||
| @Transactional | ||
| @DisplayName("[통합] CommentCacheAdapter 테스트") | ||
| class CommentCacheTest { |
There was a problem hiding this comment.
@Transactional 롤백 시 캐시 데이터가 정리되지 않음
@Transactional 테스트는 종료 시 DB를 롤백하지만, Spring Cache는 롤백되지 않습니다. 이 테스트에서 캐시에 저장된 데이터가 다른 테스트에 영향을 줄 수 있습니다. @AfterEach에서 캐시를 명시적으로 비우거나, @DirtiesContext를 사용하는 것이 안정적입니다.
제안: `@AfterEach에서` 캐시 정리
`@AfterEach`
void clearCache() {
cacheManager.getCache(CommentCacheKey.ROOT_COMMENTS).clear();
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheTest.java`
around lines 31 - 35, The test class CommentCacheTest uses `@Transactional` which
rolls back DB state but not Spring Cache, so cached entries can leak between
tests; add an explicit cache cleanup in an `@AfterEach` method (use the existing
cacheManager to clear the cache keyed by CommentCacheKey.ROOT_COMMENTS) or
annotate the class with `@DirtiesContext` to reset caches between tests; implement
one of these in CommentCacheTest to ensure
cacheManager.getCache(CommentCacheKey.ROOT_COMMENTS).clear() runs after each
test.
- REDIS 실행 방식을 외부 액션에서 Service 컨테이너로 변경 - 기존 `supercharge` 액션 내부의 Docker 클라이언트 버전(1.40) 노후화로 인해 CI 빌드가 실패하는 문제가 발생 - 이에 외부 액션 의존성을 제거하고, GitHub Actions의 native 기능인 `services`를 사용하여 Redis를 실행하도록 변경
There was a problem hiding this comment.
🧹 Nitpick comments (1)
.github/workflows/ci-workflow.yml (1)
22-33: Redis 서비스 컨테이너 전환 — LGTM, 이미지 태그 고정 권장
supercharge/redis-github-action서드파티 액션을 네이티브services블록으로 대체한 것은 공급망 의존성을 줄이고 GitHub Actions의 관용적인 방식을 따르는 좋은 변경입니다. 헬스체크 설정(redis-cli ping, 10s/5s/5회)도 적절합니다.다만 이미지 태그
redis:7은 메이저 버전만 고정되어 있어 마이너·패치 업데이트 시 빌드 재현성이 떨어질 수 있습니다. 재현 가능한 CI 환경을 위해 태그를 특정 마이너 버전으로 고정하거나, 이미지 다이제스트를 함께 사용하는 방법을 고려해 보세요.🔧 이미지 태그 고정 예시
- image: redis:7 + image: redis:7.2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci-workflow.yml around lines 22 - 33, Update the Redis service definition (services: redis) so the image: redis:7 line pins a reproducible reference instead of the floating major tag; replace it with a specific minor/patch tag (e.g., redis:7.X.Y) or the image digest (sha256:...) to ensure CI build reproducibility and retain the existing healthcheck options and ports unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In @.github/workflows/ci-workflow.yml:
- Around line 22-33: Update the Redis service definition (services: redis) so
the image: redis:7 line pins a reproducible reference instead of the floating
major tag; replace it with a specific minor/patch tag (e.g., redis:7.X.Y) or the
image digest (sha256:...) to ensure CI build reproducibility and retain the
existing healthcheck options and ports unchanged.
|
@seongjunnoh I've opened a new pull request, #347, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@seongjunnoh I've opened a new pull request, #348, to work on those changes. Once the pull request is ready, I'll request review from you. |
#️⃣ 연관된 이슈
📝 작업 내용
댓글 조회 API를 루트 댓글 조회와 자식 댓글 조회로 분리했습니다.
✨ 주요 변경 사항
GET /comments/{postId}- 특정 게시글의 루트 댓글 목록 조회 (커서 기반 페이지네이션)GET /comments/replies/{rootCommentId}- 특정 루트 댓글의 모든 자식 댓글 조회 (depth 무관, 작성 시간순 정렬)POST /comments/{postId}POST /comments/replies/{parentCommentId}comments테이블에root_comment_id컬럼 추가 (자식 댓글의 루트 댓글 참조)comments테이블에descendant_count컬럼 추가 (루트 댓글의 자식 댓글 수)comments:(post_id, parent_id, comment_id)comment_likes:(comment_id, user_id)-XX:MaxRAMPercentage=75.0)/loadtest/comment/root_comment_show.js)/monitoring디렉토리 gitignore 추가📁 주요 변경 파일
CommentQueryController.java- 조회 API 엔드포인트CommentCommandController.java- 생성 API 엔드포인트 분리CommentJpaEntity.java- root, descendantCount 필드 추가CommentQueryRepositoryImpl.java- QueryDSL 조회 로직RootCommentShowService.java,ChildCommentShowService.java- 조회 서비스 분리V250218__Add_root_comment_id.sql- DB 마이그레이션✅ 테스트
📸 스크린샷
💬 리뷰 요구사항
-> spring transactional 으로 인한 추가적인 DB Questions 을 줄이고, 커넥션 점유 시간 또한 줄이기 위함
-> spring transactional 없음에도 애플리케이션 <-> DB select 쿼리 2번 사이 Phantom Read가 발생할 여지가 없으므로 문제 상황은 없을것이라 판단 ([1. 댓글 조회 -> 2. 내가 좋아한 댓글 확인] 사이에 '내가 좋아한 댓글 정보' 가 달라질 use case가 없으므로)
-> 이번 pr의 범위가 너무 커지는 것을 우려해, 좀 더 고민해보고 다른 브랜치에서 추후에 진행할 수 있습니다.
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
New Features
성능 개선
인프라
테스트