Skip to content

[Refactor] 댓글 조회 API 분리#346

Open
seongjunnoh wants to merge 21 commits intodevelopfrom
refactor/#343-comment-show-api
Open

[Refactor] 댓글 조회 API 분리#346
seongjunnoh wants to merge 21 commits intodevelopfrom
refactor/#343-comment-show-api

Conversation

@seongjunnoh
Copy link
Collaborator

@seongjunnoh seongjunnoh commented Feb 18, 2026

#️⃣ 연관된 이슈

closes #343

📝 작업 내용

댓글 조회 API를 루트 댓글 조회와 자식 댓글 조회로 분리했습니다.

✨ 주요 변경 사항

  1. 댓글 조회 API 분리
  • 루트 댓글 조회 API: GET /comments/{postId} - 특정 게시글의 루트 댓글 목록 조회 (커서 기반 페이지네이션)
  • 자식 댓글 조회 API: GET /comments/replies/{rootCommentId} - 특정 루트 댓글의 모든 자식 댓글 조회 (depth 무관, 작성 시간순 정렬)
  1. 댓글 생성 API 분리
  • 루트 댓글 생성 API: POST /comments/{postId}
  • 자식 댓글 생성 API: POST /comments/replies/{parentCommentId}
  1. DB 스키마 변경
  • comments 테이블에 root_comment_id 컬럼 추가 (자식 댓글의 루트 댓글 참조)
  • comments 테이블에 descendant_count 컬럼 추가 (루트 댓글의 자식 댓글 수)
  • 조회 성능을 위한 복합 인덱스 추가
    • comments: (post_id, parent_id, comment_id)
    • comment_likes: (comment_id, user_id)
  1. 기타
  • Dockerfile 메모리 설정 최적화 (-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 마이그레이션

✅ 테스트

  • 루트 댓글 조회 API 통합 테스트
  • 자식 댓글 조회 API 통합 테스트
  • 루트/자식 댓글 생성 API 통합 테스트
  • 캐시 동작 테스트
  • k6 부하 테스트

📸 스크린샷

💬 리뷰 요구사항

  • 회의에서 언급한 것처럼 루트 댓글 조회 API의 service 메서드에서 spring transactional 을 제거했습니다.
    -> spring transactional 으로 인한 추가적인 DB Questions 을 줄이고, 커넥션 점유 시간 또한 줄이기 위함
    -> spring transactional 없음에도 애플리케이션 <-> DB select 쿼리 2번 사이 Phantom Read가 발생할 여지가 없으므로 문제 상황은 없을것이라 판단 ([1. 댓글 조회 -> 2. 내가 좋아한 댓글 확인] 사이에 '내가 좋아한 댓글 정보' 가 달라질 use case가 없으므로)
  • 루트 댓글 조회 시 캐시 도입을 고려했으나, 실제 코드에 반영하지는 않았습니다.
    -> 이번 pr의 범위가 너무 커지는 것을 우려해, 좀 더 고민해보고 다른 브랜치에서 추후에 진행할 수 있습니다.
  • 이번 pr에서는 comments 테이블 스키마 변경, 이에 따른 댓글 생성/조회 API의 분리를 위주로 리뷰해주시면 됩니다.

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

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

Summary by CodeRabbit

  • New Features

    • 댓글 API 분리: 루트 댓글과 대댓글을 별도 엔드포인트로 제공
    • 루트 댓글에 자식 개수(descendantCount) 노출 및 대댓글 커서 기반 페이지네이션 추가
  • 성능 개선

    • 루트 댓글 캐시 도입으로 DB 조회 감소
    • 댓글 관련 DB 인덱스 추가 및 조회 쿼리 최적화
  • 인프라

    • Docker 런타임 메모리 동적 설정 및 이미지 작업경로 개선
    • Redis 캐시 설정 및 CI 서비스에 Redis 추가
  • 테스트

    • 루트/대댓글 생성·조회 및 캐시 동작을 검증하는 통합 테스트·부하 테스트 스크립트 추가

- 게시글 별 루트 댓글 조회 성능을 위한 (post_id, parent_id) 복합 인덱스를 comments 테이블에 추가
- 루트 댓글 최신순 조회 시, 페이징 처리를 위한 cursor를 created_at -> PK 로 변경
- 루트 댓글 조회 시, postType에 대한 where 절 제거 (post_id 로만 검증해도 충분)
- root_comment_id : 현재 댓글의 루트 댓글 ID 값 (루트 댓글인 경우는 null)
- descendant_count : 현재 댓글의 모든 자손 댓글의 개수 (루트 댓글이 아닌 경우는 0)
- 루트 댓글 조회, 자손 댓글 조회 API 분리에 따른 use case 네이밍 수정
- 루트 댓글 조회 API에 spring transactional 제거 (for 조회 성능)
- 유저별 좋아하는 댓글 목록 조회를 위한 복합 인데스 추가
- persistence adapter 게층에서 '첫 페이지의 루트 댓글 조회' 시 캐시 조회하도록 설계
- 실제 코드에 반영하지는 않음 (CommentQueryPersistenceAdapter에서 주석 처리)
- 기존 service 에서 분기처리해서 수행하던 작업을 2개의 service로 분리
- comments 테이블 스키마 변경에 따른 comments 레코드 생성 로직 변경
- 부모/루트 댓글 조회 로직 추가
- 자손 수 변경 로직 추가
- jpa entity <-> domain entity의 매핑 오버헤드를 줄이고자, adapter 계층에서 [루트 댓글 설정, 자손 수 업데이트] 로직 수행
- controller 단위 테스트, API 통합 테스트, Comment 도메인 단위 테스트 수정
- 로컬 부하 테스트 환경 구성을 위한 /monitoring 디렉토리 추가 및 git ignore 설정
- 고정 크기 메모리 할당이 아니라, 비율 기반으로 메모리 할당하도록 수정
Copilot AI review requested due to automatic review settings February 18, 2026 02:20
@seongjunnoh seongjunnoh linked an issue Feb 18, 2026 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Walkthrough

댓글 기능을 루트 댓글과 자식(대댓글)으로 분리합니다. 엔드포인트·DTO·유스케이스·서비스가 분리되고 JPA·DB 스키마에 root_comment_id와 descendant_count가 추가되며 Redis 캐시·인덱스·부하테스트 스크립트가 도입됩니다.

Changes

Cohort / File(s) Summary
인프라/설정
/.gitignore, Dockerfile, src/main/java/konkuk/thip/config/CacheConfig.java, .github/workflows/ci-workflow.yml
Dockerfile JVM 옵션 변경 및 WORKDIR 추가, .gitignore에 /monitoring/ 추가, Redis 캐시를 위한 CacheConfig 추가, CI에 Redis 서비스 추가.
부하 테스트
loadtest/comment/root_comment_show.js
루트 댓글 조회용 k6 스크립트 추가(시나리오, 토큰 셋업, 태그 기반 Threshold).
DB 마이그레이션
src/main/resources/db/migration/V260107__Add_index_comments.sql, V260119__Add_root_comment_id_column.sql, V260123__Add_descendant_count.sql
comments 테이블에 (post_id,parent_id) 인덱스 추가, root_comment_id 칼럼 및 참조 마이그레이션, descendant_count 칼럼 및 초기화 쿼리 추가.
도메인 / 엔티티 / 매퍼
src/main/java/konkuk/thip/comment/domain/Comment.java, .../adapter/out/jpa/CommentJpaEntity.java, .../adapter/out/jpa/CommentLikeJpaEntity.java, .../adapter/out/mapper/CommentMapper.java
도메인에 descendantCount·rootCommentId 추가 및 createRoot/ createChild 팩토리 도입, JPA 엔티티에 root 관계·descendantCount 필드 및 증감 메서드 추가, 좋아요 테이블 인덱스 추가, 매퍼에 root/descendantCount 매핑 반영.
퍼시스턴스 리포지토리/어댑터
.../adapter/out/persistence/CommentQueryPersistenceAdapter.java, .../repository/CommentQueryRepository.java, .../repository/CommentQueryRepositoryImpl.java, .../adapter/out/persistence/CommentCommandPersistenceAdapter.java
루트 댓글 페이징을 lastRootCommentId 기반으로 변경, 자식 댓글 조회를 Cursor 기반 findAllDescendantComments로 통합, repository 구현에서 프로젝션에 descendantCount 추가, 생성시 루트 추적 및 descendantCount 증감 로직 추가.
캐시 어댑터 및 키
src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCacheAdapter.java, CommentCacheKey.java
루트 댓글 첫 페이지 캐시용 CommentCacheAdapter와 키 상수 추가(캐시 적중/실패 로깅 및 eviction).
컨트롤러 / DTO / 포트 / 서비스 분리
src/main/java/konkuk/thip/comment/adapter/in/web/..., src/main/java/konkuk/thip/comment/application/port/in/..., src/main/java/konkuk/thip/comment/application/service/...
CommentCreate/Show를 Root와 Child로 분리: 컨트롤러 경로·요청 DTO·커맨드·유스케이스 인터페이스(생성/조회) 분리, 서비스 구현(RootCommentCreateService, ChildCommentCreateService, RootCommentShowService, ChildCommentShowService) 추가 및 기존 통합형(CommentCreateUseCase, CommentShowAllUseCase) 제거.
응답 모델 변경
src/main/java/konkuk/thip/comment/adapter/in/web/response/RootCommentsResponse.java, ChildCommentsResponse.java, CommentForSinglePostResponse.java (삭제)
기존 단일 응답 구조 삭제 후 루트·자식 전용 응답 타입 도입(루트에 descendantCount 포함, 자식은 평탄한 리스트와 cursor 기반 페이징).
쿼리 DTO 변경
src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java
CommentQueryDto에 descendantCount 추가 및 관련 @QueryProjection 생성자 업데이트.
테스트 변경/추가
src/test/java/.../RootCommentCreateApiTest.java, ChildCommentCreateApiTest.java, RootCommentShowApiTest.java, ChildCommentShowApiTest.java, CommentCacheTest.java, CommentTest.java, TestEntityFactory.java, 기타 테스트 조정`
루트/자식 관련 통합/단위 테스트 추가·수정: 생성·조회·캐시·페이징 테스트 및 TestEntityFactory의 루트 추적 로직 업데이트.
기타 테스트 정리
src/test/java/.../CommentCreateControllerTest.java, FeedCreateControllerTest.java, RoomVerifyPasswordControllerTest.java
isReplyRequest/parentId 관련 테스트 제거 및 요청 포맷 간소화, 몇몇 테스트 요청 필드 정리.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

🍀 refactor, 🧸 현준, 👻 성준

Poem

나는 토끼, 땅굴에 깃든 코드야,
루트와 자식을 분리해 나눴지🐰,
캐시 춤추고 인덱스가 빛나면,
댓글 숲은 더 넓게 퍼지네🌿,
작은 당근으로 축하하자! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 주요 변경사항인 댓글 조회 API 분리를 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR이 연결된 #343 이슈의 핵심 요구사항(성능 측정, SQL 튜닝, 인덱스 설정)을 충족합니다.
Out of Scope Changes check ✅ Passed Dockerfile, .gitignore, CI 워크플로우 등의 변경사항이 있으나 모두 댓글 기능 개선을 지원하는 범위 내입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#343-comment-show-api

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

❤️ Share

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

@github-actions
Copy link

github-actions bot commented Feb 18, 2026

Test Results

499 tests   499 ✅  45s ⏱️
147 suites    0 💤
147 files      0 ❌

Results for commit 02c1df1.

♻️ This comment has been updated with latest results.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +21 to +23
@Configuration
@EnableCaching
public class CacheConfig {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

PR 설명에는 루트 댓글 조회 캐시는 “고려했으나 이번 PR에는 반영하지 않았다”고 되어 있는데, 이 PR에서 @EnableCaching + Redis CacheManager 설정과 CommentCacheAdapter/통합 테스트까지 추가되어 실제로는 캐싱 인프라가 포함되어 있습니다. 이번 PR 범위를 캐싱까지 포함할지(혹은 설정/테스트를 별도 PR로 분리할지) 명확히 정리해 주세요.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

@seongjunnoh seongjunnoh Feb 18, 2026

Choose a reason for hiding this comment

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

PR 설명에는 루트 댓글 조회 캐시는 “고려했으나 이번 PR에는 반영하지 않았다”고 되어 있는데, 이 PR에서 @EnableCaching + Redis CacheManager 설정과 CommentCacheAdapter/통합 테스트까지 추가되어 실제로는 캐싱 인프라가 포함되어 있습니다. 이번 PR 범위를 캐싱까지 포함할지(혹은 설정/테스트를 별도 PR로 분리할지) 명확히 정리해 주세요.

@copilot 루트 댓글 조회 시 이용할 캐시 관련 코드를 일부 구현했으나, 이를 댓글 조회 API에 엮지는 않았어.
따라서 현재 PR에서는 댓글 조회 시 캐시를 도입하지 않았다고 보는게 맞아.

Comment on lines +29 to +33
@Override
// @Transactional(readOnly = true)
// @Unfiltered
public RootCommentsResponse showRootCommentsOfPost(CommentShowAllQuery query) {
Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

RootCommentShowService에서 @Transactional을 제거하면, Spring Data Repository 메서드 자체가 @Transactional(readOnly=true)로 실행되면서 StatusFilterAspect가 기본 ACTIVE 필터를 활성화합니다. 그 결과 INACTIVE(삭제된) 루트 댓글이 조회되지 않아 findLatestRootCommentsWithDeleted/삭제 루트 처리 로직이 깨집니다. 이 서비스 메서드에 @Transactional(readOnly = true)를 다시 적용하고, 삭제 댓글을 포함해야 하므로 @Unfiltered 또는 @IncludeInactive를 함께 적용해 주세요.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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 명시하지 않으면 애플리케이션 단에서는 트랜잭션이 활성화되지 않는걸로 아는데, 관련해서 검토해봐

Comment on lines +1 to +2
CREATE INDEX idx_comments_post_parent
ON comments (post_id, parent_id); No newline at end of file
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

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 uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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를 포함하든, 포함하지 않든 문제가 없다고 생각)
관련해서 검토해봐

Comment on lines +37 to +46
// 타입 정보를 포함하여 직렬화/역직렬화
BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build();

mapper.activateDefaultTyping(
validator,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

Redis 캐시 직렬화에 activateDefaultTyping + allowIfBaseType(Object.class)는 매우 광범위한 다형 타입 역직렬화를 허용해, Redis 값이 오염될 경우(Jackson gadget chain 등) 역직렬화 취약점으로 이어질 수 있습니다. 캐시 값 타입을 명시적으로 제한(화이트리스트 패키지/타입)하거나, 타입 정보를 제거하고 필요한 DTO만 직렬화하는 방식으로 안전하게 구성해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +207 to +214
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());
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

이 테스트는 comment_id가 연속 증가한다고 가정하고 findById(comment1_1.getCommentId() + i - 1)로 다시 로드한 뒤 createdAt을 업데이트합니다. ID 생성 전략/시퀀스 allocation 등에 따라 연속성이 보장되지 않아 테스트가 간헐적으로 깨질 수 있습니다. 생성한 자식 댓글 엔티티를 리스트로 보관해 그 리스트를 순회하며 업데이트하는 방식으로 변경하는 편이 안정적입니다.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +27
* 삭제된 루트 댓글 생성용 정적 팩토리 메서드
* descendantCount와 isDeleted만 실제 값이고, 나머지는 모두 쓰레기 값
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

삭제된 루트 댓글 응답을 설명하면서 ‘쓰레기 값’이라는 표현을 사용하고 있는데, 공개 API DTO 주석으로는 다소 부적절합니다. placeholder, null-filled, redacted 등 중립적인 용어로 바꾸고 어떤 필드가 어떤 의미로 채워지는지(예: isDeleted/descendantCount만 유효) 명확히 기술해 주세요.

Suggested change
* 삭제된 루트 댓글 생성용 정적 팩토리 메서드
* descendantCount와 isDeleted만 실제 값이고, 나머지는 모두 쓰레기
* 삭제된 루트 댓글 생성용 정적 팩토리 메서드.
* 메서드가 반환하는 DTO에서는 {@code isDeleted} {@code descendantCount} 의미 있는 값이며,
* 필드들은 삭제된 댓글을 표현하기 위한 placeholder/null-filled (: {@code null}, {@code 0}, {@code false})으로 채워집니다.

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 메서드명이 반환 타입과 불일치합니다.

findRootCommentIdLong (ID 값)을 반환할 것처럼 보이지만, 실제로는 CommentQueryDto를 반환합니다. findRootCommentByIdfindRootCommentDtoByCommentId 등으로 변경하면 의도가 더 명확해집니다. 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: 인바운드 포트에서 어댑터 레이어 응답 타입 직접 참조

RootCommentsResponseadapter.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와 중복됨

ChildCommentCreateServicesendNotificationsToPostWriter와 시그니처 및 구현이 동일합니다. 공통 헬퍼 클래스나 상위 클래스로 추출하면 중복을 제거할 수 있습니다. 또한 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: findLatestRootCommentsWithDeletedfindAllDescendantComments의 커서 처리 및 CursorBasedList 변환 로직이 동일한 패턴을 반복합니다.

두 메서드 모두 다음과 같은 동일한 흐름을 따릅니다:

  1. cursor에서 lastIdsize 추출
  2. 레포지토리 호출
  3. 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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine build.gradle configuration
find . -name "build.gradle" -o -name "build.gradle.kts" | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 82


🏁 Script executed:

# Also check if there's a Dockerfile and examine it
find . -name "Dockerfile" | head -5

Repository: THIP-TextHip/THIP-Server

Length of output: 80


🏁 Script executed:

# Read build.gradle to check jar and bootJar configuration
cat -n ./build.gradle

Repository: THIP-TextHip/THIP-Server

Length of output: 4547


🏁 Script executed:

# Read Dockerfile to see the COPY statement
cat -n ./Dockerfile

Repository: 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).

Comment on lines +25 to +26
public final RootCommentShowUseCase rootCommentShowUseCase;
public final ChildCommentShowUseCase childCommentShowUseCase;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

필드 접근 제어자를 private으로 변경하세요

public final로 선언되어 있어 외부에서 직접 접근이 가능합니다. @RequiredArgsConstructorprivate 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.

Suggested change
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).

Comment on lines +5 to +9
public record ChildCommentsResponse(
List<ChildCommentDto> childComments,
String nextCursor,
boolean isLast
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the complete ChildCommentsResponse.java file
cat -n src/main/java/konkuk/thip/comment/adapter/in/web/response/ChildCommentsResponse.java

Repository: THIP-TextHip/THIP-Server

Length of output: 917


🏁 Script executed:

# Check if ChildCommentDto exists and examine it
find . -name "ChildCommentDto.java" -type f

Repository: 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 -5

Repository: 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 -20

Repository: 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 -5

Repository: 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 -3

Repository: 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/null

Repository: 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.

Comment on lines +20 to +22
int likeCount,
boolean isLike,
boolean isWriter
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 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")

  1. Explicitly name it

    • Java: @JsonProperty("isActive") on the field or getter (make sure you import com.fasterxml.jackson.annotation.JsonProperty). [2]
    • Kotlin: prefer @get:JsonProperty("isActive") on the property when needed. [3]
  2. Use getIsXxx() instead of isXxx()

    • If your getter is getIsActive(), Jackson will strip get and keep "isActive" as the property name. This also avoids the “boolean field named with is” confusion that can lead to double-properties / mismatches. [2]
  3. Best practice (avoid the issue): don’t name the field with is

    • Use private boolean active; with isActive() → JSON "active". This aligns with JavaBeans conventions and avoids edge cases. [2]

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:


🌐 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]

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 becomes isActive)

(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:


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.

Suggested change
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.

Comment on lines +32 to +41
@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);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +38 to +40
BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "CacheConfig.java" | head -5

Repository: 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.Object is 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 out Object (and Serializable) 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 (like Object) you need custom handling—highlighting that treating Object as allowed is not “safe by default.” (fasterxml.github.io)

Practical security implication

If you combine:

  • activateDefaultTyping(...) (or equivalent polymorphic typing), and
  • allowIfBaseType(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 than Object.class. (cowtowncoder.medium.com)
  • Consider base-type limiting approaches (block Object/Serializable as bases) and only allow polymorphism where you control the model. (fasterxml.github.io)

Citations:


🌐 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:

  1. You deserialize untrusted JSON, and
  2. You enabled default typing (or equivalent @JsonTypeInfo usage that lets input choose a class), and
  3. 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_CLASS where 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.java

Repository: 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.

Suggested change
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).

Comment on lines +1 to +2
CREATE INDEX idx_comments_post_parent
ON comments (post_id, parent_id); No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.sql" | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 835


🏁 Script executed:

fd --extension sql --path "*migration*" | grep -i comment

Repository: THIP-TextHip/THIP-Server

Length of output: 301


🏁 Script executed:

rg -n "INDEX.*comment" --type sql -i

Repository: THIP-TextHip/THIP-Server

Length of output: 171


🏁 Script executed:

rg -n "@Index" --type java -B 2 -A 3 | head -50

Repository: THIP-TextHip/THIP-Server

Length of output: 1334


🏁 Script executed:

rg -n "comment_id.*parent_id\|parent_id.*comment_id" --type sql -i

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

cat -n src/main/resources/db/migration/V260107__Add_index_comments.sql

Repository: 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 -80

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

cat -n src/main/resources/db/migration/V260119__Add_root_comment_id_column.sql

Repository: THIP-TextHip/THIP-Server

Length of output: 1445


🏁 Script executed:

cat -n src/main/resources/db/migration/V260123__Add_descendant_count.sql

Repository: 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 -100

Repository: 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 -60

Repository: 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 -100

Repository: 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 -100

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

git log --oneline --all | grep -i "index\|comment\|cursor" | head -20

Repository: 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 repository

Repository: THIP-TextHip/THIP-Server

Length of output: 467


🏁 Script executed:

rg -n "findRootComment\|findChildComment\|Slice\|Page\|cursor" -t java -B 2 -A 8 | head -150

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

git log --oneline -20 --all | head -20

Repository: 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.java

Repository: THIP-TextHip/THIP-Server

Length of output: 751


🏁 Script executed:

cat -n src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java

Repository: THIP-TextHip/THIP-Server

Length of output: 7397


🏁 Script executed:

cat -n src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java

Repository: THIP-TextHip/THIP-Server

Length of output: 2282


🏁 Script executed:

cat src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java | head -50

Repository: 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 -i

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

git log --all --oneline -- src/main/resources/db/migration/V260107* | head -5

Repository: 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 5

Repository: 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 -5

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

git log --all --oneline -- "*.sql" | grep -i "index\|comment" | head -10

Repository: 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 DESC

comment_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.

⚠️ Potential issue | 🟡 Minor

🧩 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 -20

Repository: 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 -10

Repository: 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 -20

Repository: 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 -30

Repository: 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 yaml

Repository: 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 -20

Repository: 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" \) -print

Repository: 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 yaml

Repository: 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 f

Repository: 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" \) -print

Repository: 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*" \) -print

Repository: 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 2

Repository: 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 -50

Repository: 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 -50

Repository: 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 ./Dockerfile

Repository: 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 -100

Repository: 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.java

Repository: 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 index

Repository: 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.sql

Repository: 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.sql

Repository: 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 1

Repository: 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 -50

Repository: 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.

Comment on lines +6 to +25
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;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 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 WITH clause is permitted at the beginning of SELECT, UPDATE, and DELETE statements, so WITH 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.

Comment on lines +207 to +214
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());
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

자식 댓글 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.

Suggested change
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.

Comment on lines +31 to +35
@SpringBootTest
@ActiveProfiles("test")
@Transactional
@DisplayName("[통합] CommentCacheAdapter 테스트")
class CommentCacheTest {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

@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를 실행하도록 변경
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 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.

Copy link

Copilot AI commented Feb 18, 2026

@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.

Copy link

Copilot AI commented Feb 18, 2026

@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.

This comment was marked as duplicate.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-400] [refactor] 댓글 조회 API 성능 측정 및 개선

2 participants

Comments