Skip to content

[feat] 댓글 조회 api 개발#165

Merged
seongjunnoh merged 23 commits into
developfrom
feat/#136/comment-show-for-single-post
Aug 8, 2025
Merged

[feat] 댓글 조회 api 개발#165
seongjunnoh merged 23 commits into
developfrom
feat/#136/comment-show-for-single-post

Conversation

@seongjunnoh
Copy link
Copy Markdown
Collaborator

@seongjunnoh seongjunnoh commented Aug 7, 2025

#️⃣ 연관된 이슈

closes #136

📝 작업 내용

게시글(= 피드, 기록, 투표) 의 댓글 목록을 조회하는 api 를 개발하였습니다

댓글 조회 api 의 요구사항은 아래와 같습니다

  1. 댓글 조회시 화면상에 보이는 댓글 목록은
  • 게시글에 직접 단 댓글 (= 루트 댓글들)
  • 이 댓글에 대한 답글, 그리고 답글에 대한 답글 등등 ,,, (= 자식 댓글들)
    이렇게 2개의 계층으로 구성되어야 한다
  1. 루트 댓글들은 최신순으로 정렬되어야 한다
  2. 자식 댓글들은 작성된 시각 순(= 최신순 역순) 으로 정렬되어야 한다
  3. 이때 자식 댓글들 사이에는 계층구조가 존재하지 않는다.
    단지 자신이 어떤 댓글(혹은 답글)에 대한 답글인지를 알 수 있도록, 부모 댓글(혹은 답글) 작성자의 닉네임을 같이 반환해야 한다
  4. 페이징 처리는 루트 댓글의 개수를 기준으로 한다.
    루트 댓글에 포함되는 자식 댓글들은 개수를 고려하지 않고 전부 반환한다
  5. 삭제된 루트 댓글일 경우,
  • 자식 댓글들이 존재하면, 자식 댓글들을 전부 반환하고, 루트 댓글의 isDeleted = true 로 반환한다
  • 자식 댓글들이 존재하지 않으면, 반환하지 않는다
image

-> 위 화면을 참고하시면 됩니다

  • 주요 코드 로직
    • service
      1. 루트 댓글들을 최신순으로 조회 (active, inactive 댓글 모두 조회)
      2. 조회한 댓글에 대해 모든 자식 댓글들을(= 깊이 고려 X, 하위의 모든 자식들) 작성 시각순으로 조회
      3. 조회한 모든 댓글들 중 유저가 좋아하는 댓글을 조회
      4. mapper 를 통해 response 로 매핑
    • QueryDSL 구현체
      • CommentQueryDto 를 활용해 프로젝션 기능 활용
      • 이때 루트 댓글, 자식 댓글을 위해 2개의 QueryProjection 생성자 정의
    • CommentQueryMapper
      • 반환할 [1개의 루트 댓글 + 이에 속하는 모든 자식 댓글들] 으로 매핑하는 메서드 정의
      • map struct 구현체가 사용할 default 메서드 정의

📸 스크린샷

💬 리뷰 요구사항

댓글 목록 조회의 요구사항이 생각보다 복잡한데, pr 메시지에 작성된 요구사항을 참고해 리뷰해주시면 감사하겠습니다!!

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

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

Summary by CodeRabbit

  • 신규 기능

    • 게시글의 모든 댓글을 계층 구조로 조회하는 REST API가 추가되었습니다. 루트 댓글과 대댓글, 작성자 정보, 좋아요 수 및 여부, 삭제 여부가 포함됩니다.
    • 댓글 목록은 최신순으로 커서 기반 페이지네이션되어 다음 페이지 커서와 마지막 페이지 여부를 제공합니다.
    • 사용자가 좋아요한 댓글을 식별하는 기능이 추가되었습니다.
  • 버그 수정

    • 삭제된 루트 댓글은 자식 댓글이 있을 경우 주요 정보가 비워진 상태로 표시되고, 자식 댓글이 없으면 목록에서 제외됩니다.
  • 테스트

    • 댓글 전체 조회, 정렬, 삭제 댓글 처리, 페이지네이션 등 다양한 시나리오를 검증하는 통합 테스트가 추가되었습니다.

@seongjunnoh seongjunnoh linked an issue Aug 7, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 7, 2025

Walkthrough

댓글 목록을 조회하는 새로운 API 기능이 도입되었습니다. 루트 댓글과 대댓글을 포함하여, 특정 게시글의 댓글을 커서 기반 페이지네이션 방식으로 조회할 수 있도록 서비스, 매퍼, 포트, 어댑터, 응답 DTO, 쿼리 DTO, 레포지토리, 통합 테스트 등이 추가 및 확장되었습니다.

Changes

Cohort / File(s) Change Summary
댓글 목록 조회 API 컨트롤러 및 응답 DTO
src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java, src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java
댓글 목록 조회 API 엔드포인트 및 Swagger 문서화, 응답 DTO 및 중첩 DTO, 삭제 댓글 응답 생성 메서드 추가
댓글 목록 조회 서비스 및 매퍼
src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java, src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java
댓글 목록 조회 서비스 구현, MapStruct 기반 매퍼 인터페이스 및 변환 로직 추가
댓글 목록 조회 유스케이스 및 쿼리 DTO
src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java, src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java, src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java
유스케이스 인터페이스, 쿼리용 DTO, QueryDSL용 생성자 및 정적 팩토리 메서드 추가
댓글/좋아요 쿼리 포트 및 어댑터
src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java, src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java, src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java, src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java
커서 기반 루트 댓글, 자식 댓글, 좋아요 여부 쿼리 메서드 추가 및 필드명 명확화
댓글/좋아요 JPA 레포지토리 및 쿼리 레포지토리
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java, src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java, src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java, src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
QueryDSL 기반 쿼리 레포지토리 인터페이스/구현체, 좋아요 쿼리 메서드, 루트/자식 댓글 조회 쿼리, 인터페이스 확장
테스트 및 테스트 유틸
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java, src/test/java/konkuk/thip/common/util/TestEntityFactory.java
댓글 목록 조회 API 통합 테스트, 댓글/대댓글 생성 유틸 메서드 오버로드 추가
불필요 클래스 삭제
src/main/java/konkuk/thip/comment/application/port/in/dto/DummyQuery.java
불필요한 DummyQuery DTO 완전 삭제
컨트롤러 예외 처리 테스트
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java
잘못된 postType 파라미터에 대한 400 응답 테스트 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant Service
    participant CommentQueryPort
    participant CommentLikeQueryPort
    participant Mapper

    Client->>Controller: GET /comments/{postId}?cursor=...
    Controller->>Service: showAllCommentsOfPost(query)
    Service->>CommentQueryPort: findLatestRootCommentsWithDeleted(postId, cursor)
    Service->>CommentQueryPort: findAllActiveChildrenComments(rootCommentId)
    Service->>CommentLikeQueryPort: findCommentIdsLikedByUser(commentIds, userId)
    Service->>Mapper: toRootCommentResponseWithChildren(...)
    Service->>Controller: CommentForSinglePostResponse
    Controller->>Client: BaseResponse<CommentForSinglePostResponse>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
댓글 목록(대댓글 포함) 조회 API 개발 (#136)
커서 기반 페이지네이션 및 삭제 댓글 처리 (#136)
댓글/대댓글 정렬 및 좋아요 여부 포함 (#136)
통합 테스트 및 API 문서화 (#136)

Assessment against linked issues: Out-of-scope changes

(해당 변경사항은 모두 명시된 이슈 목표 내에 있습니다.)

Suggested labels

🐶 희진, 🍀 refactor

Suggested reviewers

  • hd0rable
  • buzz0331

Poem

토끼가 깡총, 댓글을 모아
루트와 대댓글, 한눈에 보여
커서로 넘기며 페이지를 달려
삭제도, 좋아요도, 모두 담았죠
리뷰어님, 이 코드를 살펴
🐇✨ 댓글 숲에 봄바람 불어요!

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#136/comment-show-for-single-post

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@seongjunnoh seongjunnoh requested review from buzz0331 and hd0rable and removed request for hd0rable August 7, 2025 11:43
Copy link
Copy Markdown

@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: 0

🧹 Nitpick comments (7)
src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java (1)

3-3: 아키텍처 의존성 검토가 필요합니다.

Use Case 인터페이스에서 adapter layer의 response DTO를 직접 import하고 있습니다. Clean Architecture 원칙에 따르면 application layer는 adapter layer에 의존하지 않아야 합니다. response DTO를 application layer로 이동하거나 별도의 응답 모델을 고려해보세요.

src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java (2)

10-12: 정적 팩토리 메서드가 불필요합니다.

Record 클래스는 이미 생성자를 제공하므로 of 메서드가 추가적인 가치를 제공하지 않습니다. 단순히 생성자를 호출하는 것과 동일합니다.

-    public static CommentShowAllQuery of(Long postId, Long userId, String postType, String cursorStr) {
-        return new CommentShowAllQuery(postId, userId, postType, cursorStr);
-    }

4-9: 입력 검증 고려를 권장합니다.

postIduserId는 필수값이며 postType은 제한된 값들만 허용되어야 할 것 같습니다. 비즈니스 로직에서 검증하고 있다면 문제없지만, DTO 레벨에서도 기본적인 검증을 고려해볼 수 있습니다.

src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java (1)

27-27: postType 파라미터 검증을 고려해보세요.

postType이 선택적 파라미터이지만 유효하지 않은 값이 전달될 경우를 대비한 검증이 필요할 수 있습니다. 서비스 레이어에서 검증하고 있다면 문제없습니다.

src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java (1)

38-41: 주석의 오타 및 표현 개선 필요

주석에 다음과 같은 개선이 필요합니다:

  • isDeleteisDeleted 오타 수정
  • "쓰레기 값" → "null 또는 기본값" 같은 더 전문적인 표현 사용
         /**
          * 삭제된 루트 댓글에 매핑되는 response dto
-         * isDelete 제외 나머지 데이터는 모두 쓰레기 값으로
+         * isDeleted 제외 나머지 데이터는 모두 null 또는 기본값으로 설정
          */
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (2)

67-118: 대댓글 조회 로직 검토 필요

구현은 정확하지만 몇 가지 개선 사항을 제안합니다:

  1. 깊이 제한 추가 권장: 무한 중첩 댓글 구조에서 과도한 DB 쿼리를 방지하기 위해 최대 깊이 제한을 고려해보세요.

  2. 정렬 코드 간소화 가능 (Line 116):

-        allDescendants.sort(Comparator.comparing(CommentQueryDto::createdAt));
+        allDescendants.sort(Comparator.comparing(CommentQueryDto::createdAt));
  1. 성능 고려사항: 각 루트 댓글마다 이 메서드가 호출되면 N+1 쿼리 문제가 발생할 수 있습니다. 대량의 루트 댓글이 있는 경우 성능 모니터링이 필요합니다.

16-16: 와일드카드 import 사용 지양

명시적인 import 사용을 권장합니다.

-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d96ec46 and db77f03.

📒 Files selected for processing (18)
  • src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/in/dto/DummyQuery.java (0 hunks)
  • src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java (1 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/common/util/TestEntityFactory.java (2 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/konkuk/thip/comment/application/port/in/dto/DummyQuery.java
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
📚 Learning: spring data jpa에서 findby{fieldname} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, optional 반환 타입을 사용하는 것이 nu...
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.

Applied to files:

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

Applied to files:

  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java
  • src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java
  • src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java
  • src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java
📚 Learning: commentcontrollertest는 댓글 생성 api의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(commentcreateapi...
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.

Applied to files:

  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java
  • src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java
📚 Learning: thip 프로젝트에서는 query api(조회 api)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response dto를 직접 ...
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

Applied to files:

  • src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java
  • src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java
🧬 Code Graph Analysis (2)
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (33-345)
src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
  • DateUtil (12-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (28)
src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java (1)

3-3: 새로운 배치 조회 메서드가 잘 설계되었습니다.

Set을 이용한 배치 처리 방식으로 성능상 이점을 가져올 수 있으며, 메서드 네이밍도 Spring Data 컨벤션을 잘 따르고 있습니다.

Also applies to: 8-8

src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (1)

9-9: 커스텀 쿼리 레포지토리 확장이 올바르게 구현되었습니다.

Spring Data JPA의 표준 패턴을 따라 기본 JPA 기능과 커스텀 쿼리 메서드를 깔끔하게 결합했습니다.

src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java (1)

8-8: Use Case 메서드 시그니처가 잘 설계되었습니다.

Query 객체 패턴을 사용하여 파라미터를 캡슐화하고, 메서드명도 직관적으로 명명되었습니다.

src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java (1)

11-11: 새로운 메서드 구현이 기존 패턴을 잘 따르고 있습니다.

Repository로의 단순한 위임 방식으로 persistence adapter의 역할을 명확히 하고, 기존 코드와 일관성 있는 구조를 유지했습니다.

Also applies to: 27-30

src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java (1)

10-10: 배치 조회를 위한 JPQL 쿼리가 효율적으로 구현되었습니다.

IN 절을 사용한 배치 처리와 named parameter를 적절히 활용하여 성능과 가독성을 모두 고려한 구현입니다.

Also applies to: 31-32

src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java (1)

8-13: 인터페이스 설계가 적절합니다.

메서드명이 명확하고 CQRS 패턴을 잘 따르고 있습니다. 루트 댓글과 자식 댓글을 분리하여 조회하는 설계가 논리적이며, 삭제된 댓글 처리와 페이지네이션을 고려한 파라미터 구성이 적절합니다.

src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java (1)

11-13: 포트 인터페이스가 잘 설계되었습니다.

메서드명이 명확하고 CQRS 패턴을 잘 따르고 있습니다. 루트 댓글에는 커서 기반 페이지네이션을, 자식 댓글에는 전체 조회를 적용한 것이 요구사항에 적합합니다. CursorBasedListList를 적절히 구분하여 사용하고 있습니다.

src/test/java/konkuk/thip/common/util/TestEntityFactory.java (2)

188-200: 테스트 팩토리 메서드가 잘 구현되었습니다.

기존 패턴을 일관성있게 따르면서 댓글 내용과 좋아요 수를 커스터마이징할 수 있도록 하여 다양한 테스트 시나리오를 지원합니다. JavaDoc 주석으로 목적이 명확히 표시되어 있습니다.


214-227: 대댓글 팩토리 메서드도 적절히 구현되었습니다.

루트 댓글과 동일한 패턴으로 일관성을 유지하면서 부모 댓글 참조를 포함하고 있어 계층형 댓글 테스트에 유용합니다.

src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java (2)

21-33: 컨트롤러 구현이 적절합니다.

REST API 설계가 명확하고 Swagger 문서화가 잘 되어 있습니다. 컨트롤러는 HTTP 관심사만 처리하고 비즈니스 로직은 use case에 적절히 위임하고 있습니다.


19-19: final 필드가 적절히 선언되었습니다.

의존성 주입을 위한 final 필드 선언이 올바르게 되어 있고 @RequiredArgsConstructor와 함께 사용되어 불변성을 보장합니다.

src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java (2)

32-46: root comment 생성자 구현이 적절합니다.

root comment의 경우 부모 댓글 관련 필드를 null로 설정하여 canonical 생성자를 호출하는 방식이 깔끔하고 올바릅니다.


25-26: 빈 생성자에 올바른 구현이 필요합니다.

child comment용 생성자가 빈 구현체로 되어 있습니다. QueryDSL projection을 위해서는 실제 구현이 필요합니다.

다음과 같이 수정하세요:

-    @QueryProjection
-    public CommentQueryDto {}
+    @QueryProjection
+    public CommentQueryDto(
+            Long commentId,
+            Long parentCommentId,
+            String parentCommentCreatorNickname,
+            Long creatorId,
+            String creatorProfileImageUrl,
+            String creatorNickname,
+            String alias,
+            String aliasColor,
+            LocalDateTime createdAt,
+            String content,
+            int likeCount,
+            Boolean isDeleted
+    ) {
+        this.commentId = commentId;
+        this.parentCommentId = parentCommentId;
+        this.parentCommentCreatorNickname = parentCommentCreatorNickname;
+        this.creatorId = creatorId;
+        this.creatorProfileImageUrl = creatorProfileImageUrl;
+        this.creatorNickname = creatorNickname;
+        this.alias = alias;
+        this.aliasColor = aliasColor;
+        this.createdAt = createdAt;
+        this.content = content;
+        this.likeCount = likeCount;
+        this.isDeleted = isDeleted;
+    }
⛔ Skipped due to learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java (2)

27-59: 비즈니스 로직이 요구사항을 잘 구현했습니다.

댓글 조회 서비스의 핵심 로직이 잘 구현되어 있습니다:

  • 커서 기반 페이지네이션
  • 루트 댓글과 자식 댓글 분리 조회
  • 사용자별 좋아요 정보 포함
  • 적절한 응답 매핑

68-71: 삭제된 루트 댓글 처리 로직이 명확합니다.

삭제된 루트 댓글이 자식 댓글이 없으면 건너뛰는 로직이 요구사항에 맞게 구현되었습니다.

src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java (4)

22-27: 루트 댓글 매핑이 적절하게 구현되었습니다.

MapStruct 매핑 설정이 올바르고, 좋아요 상태 확인과 날짜 포맷팅이 적절히 처리되었습니다.


32-34: 답글 매핑이 올바르게 구현되었습니다.

자식 댓글의 좋아요 상태와 날짜 포맷팅이 적절히 처리되었습니다.


39-46: null-safe한 리스트 처리가 잘 구현되었습니다.

null이나 빈 리스트에 대한 처리가 적절하고, stream을 통한 매핑도 효율적입니다.


48-58: 삭제된 댓글 처리 로직이 명확합니다.

삭제된 루트 댓글에 대한 특별한 처리와 일반 댓글에 대한 리스트 병합이 적절히 구현되었습니다.

src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java (3)

19-21: 필드명 변경이 명확성을 개선했습니다.

jpaRepositorycommentJpaRepository로, userMappercommentMapper로 변경하여 코드의 가독성이 향상되었습니다.


23-33: 커서 기반 페이지네이션이 올바르게 구현되었습니다.

LocalDateTime을 커서로 사용하는 구현이 적절하고, CursorBasedList를 통한 다음 커서 생성도 올바릅니다. 이는 이전 학습 내용에서 확인한 seongjunnoh의 선호도와도 일치합니다.


36-38: 자식 댓글 조회 메서드가 간단명료합니다.

리포지토리에 적절히 위임하는 간단한 구현이 좋습니다.

src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java (5)

52-60: 테스트 정리가 적절히 구현되었습니다.

배치 삭제를 통한 효율적인 테스트 데이터 정리와 올바른 삭제 순서가 구현되었습니다.


64-115: 기본 댓글 조회 테스트가 포괄적입니다.

루트 댓글과 자식 댓글의 모든 필드를 검증하고, 좋아요 상태까지 확인하는 완전한 테스트입니다.


118-186: 정렬 요구사항 검증이 철저합니다.

루트 댓글의 최신순 정렬과 자식 댓글의 작성 시간순 정렬을 모두 검증하며, 부모 댓글 작성자 정보도 올바르게 확인합니다.


189-247: 삭제된 댓글 처리 테스트가 요구사항을 정확히 반영합니다.

자식 댓글이 있는 삭제된 루트 댓글은 반환하고, 없는 경우는 제외하는 로직을 정확히 테스트합니다.


250-359: 페이지네이션 테스트가 실제 사용 시나리오를 잘 시뮬레이션합니다.

첫 페이지와 다음 페이지를 모두 테스트하여 커서 기반 페이지네이션의 동작을 완전히 검증합니다.

src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1)

31-65: 루트 댓글 조회 메서드 구현이 적절합니다

커서 기반 페이지네이션과 삭제된 댓글 처리가 잘 구현되었습니다. Left join을 사용하여 사용자나 별칭이 없는 경우도 안전하게 처리됩니다.

Copy link
Copy Markdown
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~ 전체적으로 엄청 복잡한 요구사항이였는데 코드 깔끔하네요!! 주석이 아주 친절해서 코드 이해하는데 많은 도움이 되었습니다!! 리뷰 몇가지 적었는데 확인부탁드릴게여~~

// 최상위 댓글(size+1) 프로젝션 생성
QCommentQueryDto proj = new QCommentQueryDto(
comment.commentId,
user.userJpaEntity.userId,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p2: comment.userJpaEntity.userId로 바꾸는게 좋을 것 같습니다!!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

오호 이게 네이밍이 이상하네요 좀 다듬어 보겠습니다

comment.commentId,
comment.parent.commentId,
parentUser.nickname,
user.userJpaEntity.userId,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p2: 이것도 마찬가지로 comment.userJpaEntity.userId가 좋을 것 같아요~

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

관련해서 변수 네이밍 수정해서 push 했습니다!

Comment on lines +39 to +58
default List<CommentForSinglePostResponse.RootCommentDto.ReplyDto> mapReplies(List<CommentQueryDto> children, @Context Set<Long> likedCommentIds) {
if (children == null || children.isEmpty()) {
return Collections.emptyList();
}
return children.stream()
.map(child -> toReply(child, likedCommentIds))
.toList();
}

default CommentForSinglePostResponse.RootCommentDto toRootCommentResponseWithChildren(CommentQueryDto root, List<CommentQueryDto> children, @Context Set<Long> likedCommentIds) {
List<CommentForSinglePostResponse.RootCommentDto.ReplyDto> replyDtos = mapReplies(children, likedCommentIds);

if (root.isDeleted()) { // 삭제된 루트 & children 이 존재하는 경우
return CommentForSinglePostResponse.RootCommentDto.createDeletedRootCommentDto(replyDtos);
}

CommentForSinglePostResponse.RootCommentDto rootDto = toRoot(root, likedCommentIds);
rootDto.replyList().addAll(replyDtos);
return rootDto;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: 이 메서드도 @Named 메서드를 이용해서 어떤 메서드에서 매핑 역할을 하는지 명시하면 가독성이 좋아질 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

음 현재 CommentQueryMapper 는

toRoot : Abstract mapping → default 메서드와 연결 없음
toReply : Abstract mapping → default 메서드와 연결 없음
mapReplies : default 헬퍼 → toReply() 호출
toRootCommentResponseWithChildren : default 헬퍼 → toRoot() + mapReplies() 호출

위와 같이 구성되어 있고, 서비스에서는 toRootCommentResponseWithChildren 메서드를 호출하여 response 를 구성하는 하나의 inner class 를 반환하도록 구성되어 있습니다

따라서 추상 메서드가 default 메서드를 활용하는것이 아니어서 @nAmed 어노테이션을 붙이지 않았긴 한데, 가독성이 떨어질까요??

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

아 제가 잘못봤네요! 내부적으로 호출하고 있었군요. 그렇다면 굳이 필요 없을 것 같습니다. 죄송

Comment on lines +34 to +46
// 2. 조회한 루트 댓글의 전체 active 답글들 작성순 조회 -> map 구조로 저장
Set<Long> allCommentIds = new HashSet<>(); // 반환할 모든 댓글들의 id set
Map<Long, List<CommentQueryDto>> childrenMap = new HashMap<>();
for (CommentQueryDto root : rootsInOrder) {
List<CommentQueryDto> allActiveChildrenInOrder = commentQueryPort.findAllActiveChildrenComments(root.commentId());

childrenMap.put(root.commentId(), allActiveChildrenInOrder);

allCommentIds.add(root.commentId());
allActiveChildrenInOrder.stream()
.map(CommentQueryDto::commentId)
.forEach(allCommentIds::add);
}
Copy link
Copy Markdown
Contributor

@buzz0331 buzz0331 Aug 7, 2025

Choose a reason for hiding this comment

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

현재 코드에서는 최대 10개의 루트 댓글에 대해 for문을 반복하며 자식 댓글을 조회하고 있어, 총 10번의 쿼리가 발생하고 있습니다.
실제 로그를 확인해보면 다음과 같이 1 + N + 1 문제가 발생하고 있음을 확인할 수 있었습니다.
• 루트 댓글 조회 쿼리: 1회
• 각 루트 댓글마다 자식 댓글 조회 쿼리: 10회
• 좋아요 여부 조회 쿼리: 1회

이에 따라 다음과 같이 쿼리 최적화를 위한 리팩토링을 제안드려요!

  1. 자식 댓글 조회 최적화
    • 루트 댓글의 commentId를 Set으로 모아 한 번의 쿼리로 자식 댓글들을 조회하도록 개선
    • IN 절을 활용하여 Querydsl로 처리
  2. 서비스 로직에서 자식 댓글 그룹핑
    • 자식 댓글들을 조회한 뒤, stream().collect(groupingBy(...))를 활용하여 Map<Long, List> 형태로 그룹핑
    • 자식 댓글은 서비스 단에서 처리하는 것이 가독성과 유연성 측면에서 유리하다고 판단했습니다
  3. 좋아요 여부 조회 최적화
    • 루트 댓글과 자식 댓글의 ID를 하나의 Set으로 합쳐, 좋아요 여부를 단일 쿼리로 조회

제가 생각한 예시 코드입니다!

CommentShowAllService

// 2. 조회한 루트 댓글의 전체 active 답글들 작성순 조회 -> map 구조로 저장
       Set<Long> rootCommentIds = rootsInOrder.stream()
               .map(CommentQueryDto::commentId)
               .collect(Collectors.toSet());

       List<CommentQueryDto> allChildren = commentQueryPort.findAllActiveChildrenCommentsByPostId(query.postId(), rootCommentIds);

       Map<Long, List<CommentQueryDto>> childrenMap = allChildren.stream()
               .filter(child -> child.parentCommentId() != null) // 부모 댓글 ID가 있는 자식 댓글만 필터링
               .collect(Collectors.groupingBy(CommentQueryDto::parentCommentId));

       Set<Long> allCommentIds = Stream.concat(
               rootsInOrder.stream().map(CommentQueryDto::commentId),
               allChildren.stream().map(CommentQueryDto::commentId)
       ).collect(Collectors.toSet());

CommentQueryRepositoryImpl

@Override
    public List<CommentQueryDto> findAllActiveChildrenCommentsByPostIdOrderByCreatedAt(Long postId, Set<Long> rootCommentIds) {
        QCommentQueryDto childProj = new QCommentQueryDto(
                comment.commentId,
                comment.parent.commentId,
                parentUser.nickname,
                user.userJpaEntity.userId,
                alias.imageUrl,
                user.nickname,
                alias.value,
                alias.color,
                comment.createdAt,
                comment.content,
                comment.likeCount,
                comment.status.eq(StatusType.INACTIVE)
        );

        return queryFactory
                .select(childProj)
                .from(comment)
                .leftJoin(comment.parent, parentComment)
                .leftJoin(parentComment.userJpaEntity, parentUser)
                .leftJoin(comment.userJpaEntity, user)
                .leftJoin(user.aliasForUserJpaEntity, alias)
                .where(
                        comment.parent.commentId.in(rootCommentIds),
                        comment.status.eq(StatusType.ACTIVE),
                        comment.postJpaEntity.postId.eq(postId)
                )
                .fetch();
    }

실제 다음과 같이 리팩토링하고 쿼리 로그를 확인해보니 다음과 같이 출력되는 것을 확인했습니다.

  select
       cje1_0.comment_id,
       uje1_0.user_id,
       afuje1_0.image_url,
       uje1_0.nickname,
       afuje1_0.alias_value,
       afuje1_0.alias_color,
       cje1_0.created_at,
       cje1_0.content,
       cje1_0.like_count,
       cje1_0.status=cast(? as varchar(255)) 
   from
       comments cje1_0 
   left join
       users uje1_0 
           on uje1_0.user_id=cje1_0.user_id 
   left join
       aliases afuje1_0 
           on afuje1_0.alias_id=uje1_0.user_alias_id 
   where
       cje1_0.post_id=? 
       and cje1_0.parent_id is null 
       and true 
   order by
       cje1_0.created_at desc 
   fetch
       first ? rows only
Hibernate: 
   select
       cje1_0.comment_id,
       p1_0.comment_id,
       uje1_0.nickname,
       uje2_0.user_id,
       afuje1_0.image_url,
       uje2_0.nickname,
       afuje1_0.alias_value,
       afuje1_0.alias_color,
       cje1_0.created_at,
       cje1_0.content,
       cje1_0.like_count,
       cje1_0.status=cast(? as varchar(255)) 
   from
       comments cje1_0 
   left join
       comments p1_0 
           on p1_0.comment_id=cje1_0.parent_id 
   left join
       users uje1_0 
           on uje1_0.user_id=p1_0.user_id 
   left join
       users uje2_0 
           on uje2_0.user_id=cje1_0.user_id 
   left join
       aliases afuje1_0 
           on afuje1_0.alias_id=uje2_0.user_alias_id 
   where
       p1_0.comment_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 
       and cje1_0.status=? 
       and cje1_0.post_id=?
Hibernate: 
   select
       clje1_0.comment_id 
   from
       comment_likes clje1_0 
   where
       clje1_0.user_id=? 
       and clje1_0.comment_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

• 루트 댓글 조회: 1회
• 자식 댓글 조회: 1회
• 좋아요 여부 조회: 1회

즉, 기존 1 + N + 1 구조에서 1 + 2로 개선되었으며, 총 쿼리 수 12회 → 3회로 크게 감소하였습니다.
남아있는 2개의 쿼리는 댓글 구조상 자연스러운 분리로 판단되어, 사실상 N+1 문제는 해소된 것 같습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

현준님은

  1. 게시글에 직접 달린 루트 댓글들 페이징 처리하여 조회
  2. 1에서 조회한 모든 루트 댓글들에 대하여 이 친구들의 모든 자식 댓글들(깊이 무관) 을 1번의 쿼리로 전부 조회
  3. 이걸 서비스에서 다시 [루트 댓글 -> 자식 댓글들] 로 매핑
  4. 그 다음에 조회한 전체 댓글에 대해 유저가 좋아하는지 조회

이런 로직을 생각하신 것 같네요

저는 2번 과정에서 1에서 조회한 루트댓글들에 대하여 모든 자식노드들을 따로 조회한 이유가

  • 루트 댓글들은 최신순으로 정렬되어야 한다
  • 자식 댓글들은 작성된 시각 순(= 최신순 역순) 으로 정렬되어야 한다

위와 같은 요구사항이 명시되어 있는데, 이때 2번 과정에서 전체 루트 댓글들에 대하여 모든 자식노드들을 조회하면 정렬 순서를 어떻게 유지해야하나 라는 고민이 있어서 일단 현재 코드처럼 1번에서 조회한 루트 댓글에 대해서 모든 자식 댓글들을 따로 조회하도록 구현하였습니다

현준님이 제안한 방식은 2번 과정에서 모든 루트댓글들의 자식댓글들을 작성시간순으로 정렬해서 조회하고, 이를 각각 루트댓글들에게 매핑하면 최종 Map 구조에서는 루트 댓글은 최신순, 그 내부에 자식댓글들은 작성시간순 으로 정렬이 되어있다! 라는 의도가 맞을까요??

Copy link
Copy Markdown
Contributor

@buzz0331 buzz0331 Aug 8, 2025

Choose a reason for hiding this comment

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

루트 댓글에 대한 정렬은 지켜졌는데 자식 댓글에 대한 정렬은 누락했네요! 저는 자식댓글은 가져온 후에 루트 댓글에 매핑지을때 서비스 로직에서 정렬하면 된다고 생각했습니다! (DB 댓글 테이블의 createdAt에 인덱스가 걸려있지 않으니 사실상 DB의 정렬 효과는 미미하다고 판단)

자식 댓글들을 루트 댓글로 한번 그룹핑을 지어준 후, collectingAndThen()을 활용하여 List에서 다시 createdAt으로 정렬해주면 될 것 같다고 생각합니다.!

Map<Long, List<CommentQueryDto>> childrenMap = allChildren.stream()
    .filter(child -> child.parentCommentId() != null)
    .collect(Collectors.groupingBy(
        CommentQueryDto::parentCommentId,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparing(CommentQueryDto::createdAt))
                        .collect(Collectors.toList())
        )
    ));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

모든 자식들도 한번의 쿼리로 충분히 가져올 수 있을것 같네요. DB에서 잘만 조회하면 서비스에서 정렬과정이 필요없을 거 같긴합니다
한번 수정해서 push 해보겠습니다 날카로운 리뷰 감사합니닷!!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@buzz0331 현준님 예시코드에서는 루트 댓글의 바로 하위 계층의 자식 댓글만을 조회하는 로직으로 나와있는데, 요구사항은 루트 댓글의 트리에 속하는 모든 하위 댓글들을 전부 조회 & 이들을 작성시간순으로 조회 해야해서 재귀적 쿼리호출이 필수적으로 필요하므로 아쉽게도 1번의 쿼리 호출로는 구현하지 못하는 이슈가 있습니다.

그래도 수정한 코드에서는 기존 코드보다 DB 쿼리 호출 횟수를 줄였는데, 코드와 코멘트 참고해서 확인해주시면 감사하겠습니다!

코래말처럼 어플리케이션단에서 쿼리를 재귀적으로 호출하는 방법이 아니라, DB 차원에서 재귀 CTE 와 같이 내부적으로 재귀 sql을 실행하는 방법이 있는거같은데,

  1. queryDsl 이 아니라 native query를 활용해야함
  2. 관련 지식 부족

이슈로 도입하지는 못하였습니다 하하

public record CommentShowAllQuery(
Long postId,
Long userId,
String postType,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p2: 현재 PostType은 사용되지 않는 것 같은데 따로 의도가 있으신건가요??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

아닙니다 밑에 희진님 리뷰의 코멘트로 언급했듯이 PostType 은 제가 처음에 유효성 검증을 할까싶어서 추가햇다가 이후에 삭제하지 못한 거네요 허허

Copy link
Copy Markdown
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

정말 복집한 로직 수고하셧습니닷!! 🥇🥇 고수시네요 구현하신것처럼 postType관련해서 짤막한 리뷰 남겨봤는데 확인 부탁들빈디아

public record CommentShowAllQuery(
Long postId,
Long userId,
String postType,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

요청으로 들어온 postId와 그 postType이 동일한 지 확인할때 사용하시려고 아마 쓰신거같은데 그 검증이 누락된 것 같네요..! 예를들어 클라이언트가 피드 댓글 조회를 하려고 피드 postId를 넘기고, 클라이언트가 예측하는 게시물타입인 FEED를 같이 보내면 정확하게 피드의 댓글이 조회되지만, 클라이언트가 실제 넘긴 postId로 관련 댓글을 조회했을때 그 댓글의 포스트타입이 피드가아닌 RECORD라면 이에 해당하는 where절 조건에 comment.postType의 조건을 하나더 추가하여 클라이언트가 요청하고자한 postType만 조회할수있도록 해야할것같습니닷

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

아 이 부분은 처음에 PostType 에 대한 유효성 검증을 진행할까 하다가, QueryDSL 을 사용해서 postId 와 연관되는 댓글을 바로 조회하면 된다고 생각하여 PostType의 유효성 검증을 제외하였습니다!
그런데 Query dto 에서는 깜빡하고 제외하지 않았네요!
저는 굳이 지금 댓글을 조회할 대상이 feed, record, vote 인지를 검증하는 로직이 필요가 없다고 생각하는데, 혹시 @buzz0331 @hd0rable 님의 생각은 어떠신가요??

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

음 근데 댓글 조회가 한 api로 넘겨받은 postId로 피드,투표,기록에 대한 댓글들을 조회하는건데 정확히 클라이언트가 어떤 게시물의 댓글을 조회하려고하는지 측면에서 필요하다고생각합니닷

Copy link
Copy Markdown
Contributor

@buzz0331 buzz0331 Aug 8, 2025

Choose a reason for hiding this comment

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

오호,, 저도 마찬가지로 dtype을 통해 한번 검증하는 것이 나아보이긴 하네욥. 저희가 사실상 3개의 종류를 갖는 게시글의 댓글 조회 API를 하나로 합친거니, 클라이언트가 요청을 보낼때 정말 의도한 게시글에 대한 댓글들이 맞는지 확인하는 것도 좋아보입니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 확인했습니다! 두분 의견 반영해서 댓글 조회 api의 필수 request param으로 postType 을 지정하였고,

  1. 댓글 조회 use case 의 입력 모델인 Query dto 내부에서 PostType enum 을 통해 입력값인 String postType 의 유효성 검증
  2. QueryDSL을 활용해 게시글의 루트 댓글들 조회 시, comment.PostJpaEntity.dType.eq(postTypeStr) 코드를 통해 where 조건 추가

하도록 수정해서 추가 커밋 날렸습니다!

);

// WHERE 절 분리
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

위에말한것처럼 여기에 postType에 대한 조건도 추가되어야할것같습니다! comment.postType.eq(searchPostType) 이런식으루..

void tearDown() {
commentLikeJpaRepository.deleteAllInBatch();
commentJpaRepository.deleteAllInBatch();
feedJpaRepository.deleteAllInBatch();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

feed 소프트 전략이라 .deleteAllInBatch(); 로바꿔주시면 감사하겠습니다!! 또한 추가적으로 앞으로 테스트코드 작성하실때 소프트 딜리트인 것들은 .deleteAllInBatch();로 달아주시면 감사하겠습니다라랏

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

오 덕분에 deleteAll, deleteAllInBatch 메서드의 차이점을 한번 확인하고 갑니다잇~~
https://dkswnkk.tistory.com/757

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이미 현준띠니가 문귀답게 노션에 정리해두셧ㅇ어옄ㅋㅋㅋ

- adapter 로부터 정렬된 value를 가지는 Map 구조를 반환받아 이후 로직을 이어나가도록 수정
Copy link
Copy Markdown

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

♻️ Duplicate comments (1)
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1)

48-53: postType 조건 누락 — 과거 리뷰와 동일 지적사항입니다

루트 댓글 조회 where 절에 postType 조건이 없습니다. Feed/Record/Vote를 구분해야 한다면 반드시 포함되어야 합니다.

다음과 같이 수정 제안드립니다(메서드 시그니처에 postType 추가 전제).

- BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
+ BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
+         .and(comment.postType.eq(postType))
          .and(comment.parent.isNull())
          .and(lastCreatedAt != null
                  ? comment.createdAt.lt(lastCreatedAt)
                  : Expressions.TRUE
          );

메서드 선언부도 아래처럼 갱신되어야 합니다.

-public List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long postId, LocalDateTime lastCreatedAt, int size) {
+public List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long postId, PostType postType, LocalDateTime lastCreatedAt, int size) {
🧹 Nitpick comments (5)
src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java (1)

13-17: Cursor 의미 명확화 및 타임스탬프 타이 처리 검증 필요

본 PR은 LocalDateTime 단일 커서를 선호한다는 팀 컨벤션과 맞지만, 동일 createdAt이 다수인 경우 페이지 경계에서 누락/중복이 생길 수 있습니다. 현 Cursor가 createdAt 단일 기준인지 확인 부탁드립니다.

가능한 대응:

  • 현 방식을 유지하되, createdAt 타이 빈도가 낮은 전제 확인(인덱스/DB 정밀도).
  • 또는 내부적으로 보조 정렬키(commentId 등)를 추가만 하고 커서는 그대로 유지(타이 시 안정 정렬은 향상되지만, 경계 문제는 여전함). 필요 시 상세 대안 제안 가능합니다.
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (4)

62-64: 동일 createdAt 타이 시 페이지 경계 누락/중복 가능성

단일 createdAt 커서에 orderBy(createdAt desc)만 사용하는 경우, 경계값과 동일한 createdAt을 가진 레코드가 size를 초과해 앞 페이지에 일부만 실렸다면, 다음 페이지에서 lt(lastCreatedAt) 조건으로 인해 누락될 수 있습니다.

권장:

  • 팀 컨벤션을 유지하되, createdAt 타이 빈도(인덱스/DB 정밀도)를 계측해 허용 가능한지 확인.
  • 보조 정렬키를 추가하여 안정 정렬성(orderBy(comment.createdAt.desc(), comment.commentId.desc()))은 확보.
- .orderBy(comment.createdAt.desc())
+ .orderBy(comment.createdAt.desc(), comment.commentId.desc())

참고: 보조 정렬키 추가만으로 경계 누락 문제 자체는 해결되지 않습니다. 이를 완전히 해소하려면 커서에 보조 키를 포함하거나(복합 커서) 서비스에서 타이 보정 로직이 필요합니다.


121-187: 다중 루트 자식 조회도 동일 — 단일 쿼리 화로 N+1(깊이) 제거 가능

여러 루트에 대해 BFS를 수행하고 마지막에 루트별로 정렬하고 있습니다. root_comment_id 역정규화 컬럼이 있다면 IN (:rootIds) 한 번으로 모두 가져와 그룹핑하는 편이 DB/네트워크 비용을 크게 줄입니다.

추가로 최종 정렬은 이미 수행 중이지만, 페치 단계에서 orderBy를 맞춰 두면 JVM 정렬 비용도 줄일 수 있습니다.


95-104: 자식 조회 조건에 postType이 없는지 점검 필요

자식 댓글은 parentId 기반 필터로 충분할 가능성이 높지만, 모델에 따라 postType을 추가하면 계획 안정성과 불필요 스캔을 줄일 수 있습니다(특히 다중 타입 단일 테이블 전략일 때).

가능하면 다음 조건 추가 고려:

  • comment.postType.eq(root의 postType)
  • 또는 parentComment.postType과의 일치 조건

1-187: 접근 제어(Record/Vote의 Room 참가자 여부) — 서비스 레이어에서 검증되었는지 확인 필요

리포지토리 레벨에선 불가하지만, 요구사항상 Record/Vote는 Room 소속이며 열람에도 참가자 검증이 필요할 수 있습니다. 서비스에서 이를 보장하는지 확인 부탁드립니다.

해당 검증이 없다면, 서비스에서 postType이 Record/Vote일 때 Room 참가자 여부를 확인하는 가드를 추가하는 패치를 제안드릴 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8668ec7 and 8f4651d.

📒 Files selected for processing (5)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java
  • src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Applied to files:

  • src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java (1)

11-18: CQRS 컨벤션에 부합하는 포트 분리 — 전반적으로 방향 LGTM

조회 응답 전용 DTO를 반환하는 QueryPort 구성과 메서드 네이밍이 요구사항(루트 최신, 자식 오름차순, 삭제 루트 포함/자식은 활성만)에 부합합니다.

Comment on lines +91 to +117
// 3) 단계별 자식 댓글 조회
while (!parentIds.isEmpty()) {
List<CommentQueryDto> children = queryFactory
.select(childProj)
.from(comment)
.leftJoin(comment.parent, parentComment)
.leftJoin(parentComment.userJpaEntity, parentCommentCreator)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회
)
.fetch();

if (children.isEmpty()) break;

// 4) 누적 및 다음 단계 부모 ID 집합 갱신
allDescendants.addAll(children);
parentIds = children.stream()
.map(CommentQueryDto::commentId)
.collect(Collectors.toSet());
}

// 5) 전체 자손 댓글을 깊이와 상관없이 작성 순으로 재정렬
allDescendants.sort(Comparator.comparing(CommentQueryDto::createdAt));
return allDescendants;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

자식 댓글 BFS 반복 쿼리 — 스키마 여건에 따라 단일 쿼리로 최적화 가능

현재는 깊이마다 쿼리를 반복(BFS)합니다. 최대 깊이에 비례해 쿼리 수가 증가합니다.

스키마에 comment.rootCommentId(루트 참조)가 존재한다면, 다음과 같이 단일 쿼리로 대체해 성능을 크게 개선할 수 있습니다.

  • where: comment.rootCommentId.eq(:rootId) AND comment.status = ACTIVE
  • orderBy: createdAt asc, commentId asc
  • parent 작성자 닉네임은 self join 한 번으로 해결

스키마에 해당 컬럼이 없다면 현재 방식이 합리적입니다. 대안으로는 DB가 지원한다면 재귀 CTE(WITH RECURSIVE) 사용이 있으나, JPA/QueryDSL 표준에서의 이식성은 떨어집니다. 필요 시 JPA NativeQuery로 한 번에 가져오는 구현도 제안 가능합니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
around lines 91 to 117, the current implementation fetches child comments using
a BFS loop with repeated queries per depth level, which can degrade performance
as depth increases. If the schema includes a rootCommentId field referencing the
root comment, refactor this to a single query filtering by comment.rootCommentId
and status ACTIVE, ordering by createdAt and commentId ascending. Use a single
self join to fetch parent author nicknames. If rootCommentId does not exist,
keep the current approach or consider a recursive CTE or native query for
optimization.


public interface CommentQueryPort {

CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, Cursor cursor);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

postType 매개변수 누락 가능성 — 포트 시그니처에 반영 권장

동일 ID가 서로 다른 PostType(Feed/Record/Vote)에 존재할 수 있는 모델이라면, 루트 댓글 조회에 postType 조건이 필요합니다. 과거 리뷰에서도 postType 조건 추가가 지적되었습니다. Port 단계에서 명시적으로 받도록 설계하는 편이 안전합니다.

아래처럼 시그니처 확장을 제안합니다(호출부/어댑터/리포지토리 일괄 반영 필요).

- CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, Cursor cursor);
+ CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, PostType postType, Cursor cursor);

필요 시 전체 영향 범위에 대한 리팩터링 패치도 도와드릴 수 있습니다.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java
at line 13, the method findLatestRootCommentsWithDeleted is missing a postType
parameter, which is necessary to distinguish comments belonging to different
post types with the same ID. Modify the method signature to include a postType
argument, then update all related call sites, adapters, and repository
implementations accordingly to handle this new parameter consistently.

Copy link
Copy Markdown
Collaborator Author

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

@buzz0331 @hd0rable 댓글 조회 api 중 루트 댓글의 모든 자식댓글들을 조회하는 로직을 수정하였습니다

  • 기존 로직
    • 1차적으로 조회한 모든 루트 댓글들에 대하여, 하나의 루트 댓글에 해당하는 모든 자식 댓글들을 재구적으로 조회하는 로직을 반복
    • 따라서 총 쿼리 호출 횟수는 각 루트 댓글에 해당하는 트리의 깊이만큼의 합
    • ex) 첫번째 루트 댓글의 트리 깊이 = 3, 두번쨰 루트 댓글의 트리 깊이 = 2 인 경우 : 총 쿼리 횟수 = 5
  • 수정된 로직
    • 1차적으로 조회한 모든 루트 댓글들에 대하여, 전체 루트 댓글에 대해 하위의 모든 자식 댓글들을 조회
    • 즉 하나의 큰 트리를 생각하여 재귀적으로 쿼리 호출을 실행하도록 수정
    • 따라서 가장 깊은 트리의 깊이만큼 쿼리 호출이 발생함
    • ex) 첫번째 루트 댓글의 트리 깊이 = 3, 두번쨰 루트 댓글의 트리 깊이 = 2 인 경우 : 총 쿼리 횟수 = 3
    • 또한 서비스에서는 조회한 자식 댓글들이 어떤 루트댓글에 속하는지를 알아야 하므로, adapter 에서 Map<루트 댓글 id, List<모든 자식 댓글들>> 을 매핑지어 반환하도록 구현
      • 영속성 계층에서 DB 조회뿐만아니라 비즈니스 로직까지 들어있는거 같은 느낌이 있지만, 쿼리 호출 횟수를 줄이기 위해 감수해야한다고 생각

- QueryDSL 코드까지 postTypeStr을 전달
- 이를 받아 필터링하도록 QueryDSL 코드 수정
Copy link
Copy Markdown

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

♻️ Duplicate comments (3)
src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java (1)

12-14: 쿼리 where 절에 postType 필터가 실제로 적용되는지 확인 필요

이 DTO에 postType을 포함한 목적이 “요청한 게시물 타입만” 필터링하기 위함이라면, Repository/QueryDSL 레이어의 where 조건에도 반드시 포함되어야 합니다. (이 이슈는 과거 코멘트에서도 거론됨)

다음 스크립트로 코드베이스에서 사용 여부를 빠르게 점검할 수 있습니다:

#!/bin/bash
# postType/dtype 기반 필터 조건 사용 여부 검색
rg -n --hidden --glob '!**/build/**' --glob '!**/out/**' 'postType|dtype' -A 5 -B 3

# Comment 조회 쿼리에서 where절에 postType이 포함되는지 확인
rg -n --hidden --glob '!**/build/**' --glob '!**/out/**' -e 'where\s+.*postType' -e '\.and\(\s*.*postType'
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java (1)

12-13: Enum 미사용으로 인한 타입 안정성 저하

Port 층과 동일하게 postTypeStr가 문자열로 선언되어 있습니다.
Enum 교체를 일관되게 적용해 주세요.

src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1)

92-118: BFS 반복 쿼리로 인한 퍼포먼스 저하 우려

이전 리뷰에서도 동일 지적이 있었습니다. 스키마에 rootCommentId 컬럼이 있으면 단일 쿼리로 대체해 I/O를 크게 줄일 수 있습니다. 재귀 CTE·네이티브 쿼리도 대안입니다.

🧹 Nitpick comments (5)
src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java (3)

6-11: LocalDateTime 커서 접근자 추가 제안 (팀 선호 반영, 파싱 중복 제거)

팀 러닝에 따르면 LocalDateTime 단일 커서 선호. String 커서를 유지하되, 내부 서비스에서 공통 파싱을 재사용할 수 있도록 접근자를 제공합니다.

 import konkuk.thip.common.post.PostType;
 
 public record CommentShowAllQuery(
         Long postId,
         Long userId,
         PostType postType,
         String cursorStr
 ) {
+    public java.util.Optional<java.time.LocalDateTime> cursorAsLocalDateTime() {
+        if (cursorStr == null || cursorStr.isBlank()) return java.util.Optional.empty();
+        try {
+            return java.util.Optional.of(java.time.LocalDateTime.parse(cursorStr));
+        } catch (java.time.format.DateTimeParseException e) {
+            throw new IllegalArgumentException("Invalid cursor format. Expecting ISO-8601 LocalDateTime.", e);
+        }
+    }

필요 시 커서 포맷(예: ISO-8601)도 API 문서에 명시해 주세요.


6-11: of 오버로드 추가: PostType 직접 전달 경로 제공

호출 측에서 이미 PostType을 보유한 경우 중복 변환을 줄입니다.

 ) {
+    public static CommentShowAllQuery of(Long postId, Long userId, PostType postType, String cursorStr) {
+        return new CommentShowAllQuery(postId, userId, postType, cursorStr);
+    }

13-13: 인라인 주석 제거/이동 (코드 클린업)

구현 라인 끝의 주석은 Javadoc/메서드 주석으로 옮기는 편이 가독성에 유리합니다.

-        return new CommentShowAllQuery(postId, userId, PostType.from(postTypeStr), cursorStr);     // 내부에서 PostType string 값 유효성 검증
+        return new CommentShowAllQuery(postId, userId, PostType.from(postTypeStr), cursorStr);
src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java (1)

15-17: 메서드명에 정렬 조건을 명확히 포함해 주세요

findAllActiveChildCommentsOldestFirstfindActiveChildCommentsOrderByCreatedAtAsc처럼
“정렬 기준(createdAt asc)”을 직관적으로 드러내면 다른 개발자가 바로 이해할 수 있습니다.

src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1)

155-168: DB에서 정렬해 가져오면 메모리 정렬 비용을 줄일 수 있습니다

쿼리 결과를 메모리에서 다시 정렬하기보다는 orderBy(comment.createdAt.asc())를 추가해 DB가 정렬하도록 위임하는 편이 효율적입니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f4651d and c4f90f8.

📒 Files selected for processing (6)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Applied to files:

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

Applied to files:

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

Applied to files:

  • src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java
📚 Learning: 2025-07-14T18:22:56.538Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

Applied to files:

  • src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java
📚 Learning: 2025-07-23T17:41:55.507Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.

Applied to files:

  • src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java (1)

7-9: userId의 필수 여부 계약 명확화

비로그인 사용자의 댓글 조회를 지원하지 않는다면 userId는 null이 될 수 없으므로 불변식(Null/양수) 체크가 필요합니다. 지원한다면 null 허용을 문서화하고, 좋아요 여부 조회 로직에서 null-safe 처리가 되어있는지 점검해 주세요.

Comment on lines +32 to +55
public List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long postId, String postTypeStr, LocalDateTime lastCreatedAt, int size) {
// 최상위 댓글(size+1) 프로젝션 생성
QCommentQueryDto proj = new QCommentQueryDto(
comment.commentId,
commentCreator.userId,
aliasOfCommentCreator.imageUrl,
commentCreator.nickname,
aliasOfCommentCreator.value,
aliasOfCommentCreator.color,
comment.createdAt,
comment.content,
comment.likeCount,
comment.status.eq(StatusType.INACTIVE) // 루트 댓글이 삭제된 상태인지 아닌지
);

// WHERE 절 분리
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

postTypeStr 문자열 비교는 Enum으로 교체하는 편이 안전합니다

comment.postJpaEntity.dtype.eq(postTypeStr)
comment.postJpaEntity.dtype.eq(postType.getDtype()) 형태로 변경하면 오타·케이스 민감성 문제를 제거할 수 있습니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
around lines 32 to 55, replace the string comparison of postTypeStr with an
Enum-based comparison to avoid typos and case sensitivity issues. Change the
condition comment.postJpaEntity.dtype.eq(postTypeStr) to use
comment.postJpaEntity.dtype.eq(postType.getDtype()) where postType is an Enum
representing the post type. This requires passing or converting postTypeStr to
the corresponding Enum before the query.

Comment on lines +48 to +66
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
);

// 조회 및 반환
return queryFactory
.select(proj)
.from(comment)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(whereClause)
.orderBy(comment.createdAt.desc())
.limit(size + 1) // size + 1 개 조회
.fetch();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

커서 정렬 컬럼이 하나뿐이라 페이지 중복/누락 위험이 있습니다

orderBy(comment.createdAt.desc())createdAt < :lastCreatedAt 조건만 사용하면
같은 createdAt 값을 가진 댓글이 다수일 때 다음 페이지에서 중복이 발생하거나 일부가 건너뛰어질 수 있습니다.

createdAt 동 tie-breaker로 comment.commentId를 함께 사용해 안정적인 순서를 보장하세요.

.where(whereClause)
-.orderBy(comment.createdAt.desc())
+.orderBy(comment.createdAt.desc(), comment.commentId.desc())

커서 로직도 (createdAt, commentId) 복합 비교로 바꿔야 합니다.

📝 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
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
);
// 조회 및 반환
return queryFactory
.select(proj)
.from(comment)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(whereClause)
.orderBy(comment.createdAt.desc())
.limit(size + 1) // size + 1 개 조회
.fetch();
}
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
);
// 조회 및 반환
return queryFactory
.select(proj)
.from(comment)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(whereClause)
- .orderBy(comment.createdAt.desc())
+ .orderBy(comment.createdAt.desc(), comment.commentId.desc())
.limit(size + 1) // size + 1 개 조회
.fetch();
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
between lines 48 and 66, the current cursor pagination uses only createdAt for
ordering and filtering, which can cause duplicates or missing entries when
multiple comments share the same createdAt timestamp. To fix this, modify the
orderBy clause to order by createdAt descending and then commentId descending as
a tie-breaker. Also, update the whereClause to implement a composite cursor
condition that compares (createdAt, commentId) pairs, ensuring stable and
consistent pagination results.

Comment on lines +6 to +11
public record CommentShowAllQuery(
Long postId,
Long userId,
PostType postType,
String cursorStr
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

레코드 불변식 보강: postId/postType 필수, postId 양수 체크

도메인 계약을 DTO 레이어에서 보장하면 하위 계층의 방어코드가 줄어듭니다.

 public record CommentShowAllQuery(
         Long postId,
         Long userId,
         PostType postType,
         String cursorStr
 ) {
+    public CommentShowAllQuery {
+        java.util.Objects.requireNonNull(postId, "postId must not be null");
+        if (postId <= 0) {
+            throw new IllegalArgumentException("postId must be positive");
+        }
+        java.util.Objects.requireNonNull(postType, "postType must not be null");
+        // userId: 비로그인 허용 여부에 따라 null 허용/불허 결정 필요
+    }

userId가 반드시 로그인 사용자만 허용되는 API라면 동일하게 null/양수 체크를 추가해 주세요. 비로그인을 지원한다면 null 허용을 명시적으로 문서화하는 것을 권장합니다.


레코드 생성자에 불변식 검사 로직 추가 제안

DTO 레이어에서 postIdpostType에 대한 불변식을 검증하면, 하위 계층의 방어 코드가 줄어들고 도메인 계약이 보다 명확해집니다.

• 대상 파일

  • src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java (레코드 선언부)

• 제안된 수정사항

 public record CommentShowAllQuery(
         Long postId,
         Long userId,
         PostType postType,
         String cursorStr
 ) {
+    public CommentShowAllQuery {
+        java.util.Objects.requireNonNull(postId, "postId must not be null");
+        if (postId <= 0) {
+            throw new IllegalArgumentException("postId must be positive");
+        }
+        java.util.Objects.requireNonNull(postType, "postType must not be null");
+        // userId: 로그인 사용자만 허용한다면 null/양수 검사 추가,
+        // 비로그인 지원 시 null 허용 여부를 명시적으로 문서화 권장
+    }
 }

userId 처리 정책에 따라 적절한 null/양수 검사를 추가하거나, null 허용 범위를 주석 혹은 API 문서에 명시해 주세요.

📝 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 record CommentShowAllQuery(
Long postId,
Long userId,
PostType postType,
String cursorStr
) {
public record CommentShowAllQuery(
Long postId,
Long userId,
PostType postType,
String cursorStr
) {
public CommentShowAllQuery {
java.util.Objects.requireNonNull(postId, "postId must not be null");
if (postId <= 0) {
throw new IllegalArgumentException("postId must be positive");
}
java.util.Objects.requireNonNull(postType, "postType must not be null");
// userId: 로그인 사용자만 허용한다면 null/양수 검사 추가,
// 비로그인 지원 시 null 허용 여부를 명시적으로 문서화 권장
}
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java
around lines 6 to 11, add validation logic in the record constructor to enforce
invariants for postId and postType, such as non-null and positive checks. Also,
clarify the nullability and validation rules for userId by either adding
appropriate null or positivity checks or documenting its allowed values in
comments or API documentation. This will strengthen the DTO's contract and
reduce defensive checks downstream.

Comment on lines +12 to +14
public static CommentShowAllQuery of(Long postId, Long userId, String postTypeStr, String cursorStr) {
return new CommentShowAllQuery(postId, userId, PostType.from(postTypeStr), cursorStr); // 내부에서 PostType string 값 유효성 검증
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

postTypeStr null/공백 처리 및 정상화 필요 (NPE/유효성 강화)

null/공백 입력 시 NPE/잘못된 요청으로 이어질 수 있습니다. 최소한 trim/빈값 체크를 추가해 주세요.

다음 변경을 제안드립니다:

+import java.util.Objects;
-    public static CommentShowAllQuery of(Long postId, Long userId, String postTypeStr, String cursorStr) {
-        return new CommentShowAllQuery(postId, userId, PostType.from(postTypeStr), cursorStr);     // 내부에서 PostType string 값 유효성 검증
-    }
+    public static CommentShowAllQuery of(Long postId, Long userId, String postTypeStr, String cursorStr) {
+        Objects.requireNonNull(postTypeStr, "postType must not be null");
+        final String normalized = postTypeStr.trim();
+        if (normalized.isEmpty()) {
+            throw new IllegalArgumentException("postType must not be blank");
+        }
+        return new CommentShowAllQuery(postId, userId, PostType.from(normalized), cursorStr);
+    }

또한 PostType.from가 대소문자 무시를 지원하지 않으면, controller 단에서 upper-case 정규화(예: toUpperCase(Locale.ROOT))를 고려해 주세요.


PostType.from 메소드의 null/빈 문자열 처리 로직을 확인하기 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# PostType.java 위치 검색 및 from 메소드 구현 출력
file=$(fd PostType.java)
echo "PostType 파일 위치: $file"
rg -n "static .*from" "$file" -A 5 -B 2

Length of output: 660


postTypeStr null 및 공백 처리 추가 요망
PostType.from 호출 시 null 입력은 NPE, 공백 입력은 InvalidStateException 으로 처리됩니다. of() 메소드에서 null/blank 체크 및 trim 후 전달하도록 수정해 주세요.

대상 위치

  • 파일: src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java
  • 메소드: public static CommentShowAllQuery of(...)

제안하는 변경 내용:

+ import java.util.Objects;
  
     public static CommentShowAllQuery of(Long postId, Long userId, String postTypeStr, String cursorStr) {
-        return new CommentShowAllQuery(postId, userId, PostType.from(postTypeStr), cursorStr);
+        Objects.requireNonNull(postTypeStr, "postType must not be null");
+        String normalized = postTypeStr.trim();
+        if (normalized.isEmpty()) {
+            throw new IllegalArgumentException("postType must not be blank");
+        }
+        return new CommentShowAllQuery(postId, userId, PostType.from(normalized), cursorStr);
     }

참고: PostType.from 메소드 내부에서 equalsIgnoreCase를 사용해 대소문자를 구분하지 않으므로, toUpperCase 변환은 별도 적용할 필요가 없습니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java
around lines 12 to 14, the of() method calls PostType.from with postTypeStr
without checking for null or blank values, which can cause NullPointerException
or InvalidStateException. Modify the of() method to first check if postTypeStr
is null or blank, trim it if not blank, and only then pass the cleaned string to
PostType.from. This prevents exceptions by ensuring valid input is always
provided.


public interface CommentQueryPort {

CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, String postTypeStr, Cursor cursor);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

postType는 String 대신 명시적 Enum 사용을 권장합니다

문자열로 타입을 전달하면 오타·케이싱 오류를 컴파일 타임에 잡지 못합니다.
PostType(또는 동일한 역할의 Enum)을 도입해 시그니처를 아래처럼 교체하면 안정성과 가독성이 모두 향상됩니다.

-CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, String postTypeStr, Cursor cursor);
+CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, PostType postType, Cursor cursor);

호출부와 구현부(Repository/Adapter) 전체 수정이 필요합니다.

📝 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
CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, String postTypeStr, Cursor cursor);
CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(
Long postId,
PostType postType,
Cursor cursor
);
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java
at line 13, replace the postTypeStr parameter of type String with a specific
Enum type such as PostType to improve type safety and prevent runtime errors
from typos. Update the method signature accordingly and ensure all calling code
and implementations (Repository/Adapter) are modified to use the Enum type
instead of String for postType.

Copy link
Copy Markdown

@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: 0

🔭 Outside diff range comments (1)
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java (1)

1-41: 통합 테스트 검증 결과 및 보강 요청

  • CommentShowAllApiTest.java 파일이 존재하며, 주요 시나리오가 아래와 같이 커버됩니다.

    • 정렬: comment_show_all_ordering_test에서 루트 댓글 최신순, 자식 댓글 생성 시각 순 검증
    • 삭제 처리: comment_show_all_deleted_root_comment_test에서 삭제된 루트 댓글의 표시/생략 규칙 검증
    • 페이징: comment_show_all_page_test에서 cursor 기반 페이징 동작 검증
  • 보강이 필요한 항목

    • 사용자 좋아요 여부 표시(liked 또는 isLiked 필드) 검증 로직 추가
    • size 파라미터를 조절한 페이징 경계값(최소/최대 크기) 테스트 추가

위 두 케이스를 CommentShowAllApiTest.java에 보강해주세요.

🧹 Nitpick comments (4)
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java (4)

17-21: 단위 테스트 의도라면 @WebMvcTest로 경량화 권장

파라미터 검증/예외 매핑만 확인하는 목적이라면 전체 컨텍스트를 올리는 @SpringBootTest 대신 @WebMvcTest(controllers = CommentQueryController.class)가 더 적합합니다. 테스트 속도/격리가 개선되고 실패 원인도 국소화됩니다. 예외 핸들러(@ControllerAdvice)는 @Import(...)로 함께 로드하고, 서비스 포트는 목 주입하면 됩니다.

-@SpringBootTest
+@WebMvcTest(controllers = CommentQueryController.class)
 @ActiveProfiles("test")
 @AutoConfigureMockMvc(addFilters = false)

33-35: 요청 Accept/기본 파라미터 명시로 플래키 방지

컨트롤러에서 size 등의 필수 파라미터 기본값이 없을 경우, postType 검증 이전에 다른 검증으로 400이 날 수 있습니다. 안전하게 Accept와 기본 size를 명시해 주세요.

-        mockMvc.perform(get("/comments/{postId}", 1L)
+        mockMvc.perform(get("/comments/{postId}", 1L)
+                        .accept(org.springframework.http.MediaType.APPLICATION_JSON)
                         .requestAttr("userId", 1L)
-                        .param("postType", invalidPostType))
+                        .param("postType", invalidPostType)
+                        .param("size", "10"))

확인 필요: 컨트롤러에서 size 기본값이 설정돼 있다면 추가하지 않아도 됩니다(예: @RequestParam(defaultValue="10") int size).


38-38: 에러 메시지 문자열 매칭은 최소화 권장

메시지는 i18n/운영 환경에서 변경될 가능성이 있어 취약합니다. 이미 $.code를 검증하고 있으므로 메시지 검증은 생략하거나, 변경 안정적인 키(예: error, reason)가 있다면 그 필드로 검증하는 것을 권장합니다.

-                .andExpect(jsonPath("$.message", containsString(POST_TYPE_NOT_MATCH.getMessage())));
+                // 메시지는 변동 가능성이 있으므로 code 검증만 유지하는 것을 권장

26-31: 여러 invalid postType 케이스를 파라미터라이즈드 테스트로 확대

빈 문자열, 공백, 소문자 등 경계값을 함께 검증하면 신뢰도가 높아집니다. 원하시면 아래 형태로 보강 가능합니다.

@ParameterizedTest
@ValueSource(strings = {"", " ", "invalid", "feed", "RECORDS"})
@DisplayName("postType 유효하지 않으면 400")
void comment_show_all_post_type_invalid_cases(String invalid) throws Exception {
    mockMvc.perform(get("/comments/{postId}", 1L)
                    .accept(MediaType.APPLICATION_JSON)
                    .requestAttr("userId", 1L)
                    .param("postType", invalid)
                    .param("size", "10"))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.code").value(POST_TYPE_NOT_MATCH.getCode()));
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4f90f8 and 720b0f1.

📒 Files selected for processing (3)
  • src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java (1 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
📚 Learning: 2025-07-23T17:41:55.507Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.

Applied to files:

  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java (1)

26-39: 부적절한 postType에 대한 400 매핑 검증: LGTM

컨트롤러 레벨의 입력값 검증/예외 응답 매핑을 잘 커버합니다. 통합 테스트에서 정상 흐름을 다루는 팀 컨벤션(학습 노트)과도 일치합니다.

Copy link
Copy Markdown
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

수고하셨습니닷!!


// WHERE 절 분리
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
Copy link
Copy Markdown
Member

@hd0rable hd0rable Aug 8, 2025

Choose a reason for hiding this comment

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

어근데 저희 post는 dtype으로 문자열이고 댓글의 postType은 enum인데 이거 나중에 통일해야할것같네욥 수정하면서 PostType.from()으로 파라미터 검증도 추가하면 좋을것같습니다 ! 일단은 이렇게 해도 좋을것같네여

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

파라미터 검증은 QueryDto 내부에서 수행하고 있긴 합니다!
희진님 말대로 현재 dType 이 String 이어서 일단 서비스에서 enum의 String value 를 파싱해서 영속성 계층까지 던지는 식으로 구현하였습니다

Copy link
Copy Markdown
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

엄청 고민한 흔적이 보이네요!! 정말 고생하셨습니다~!!! 👍🏻 👍🏻

Comment on lines +121 to +188
@Override
public Map<Long, List<CommentQueryDto>> findAllActiveChildCommentsByCreatedAtAsc(Set<Long> rootCommentIds) {
// 1) 루트 ID별로 최상위 매핑 초기화
Map<Long, Long> idToRoot = new HashMap<>();
for (Long rootId : rootCommentIds) {
idToRoot.put(rootId, rootId); // 초기화
}

// 2) 결과 맵 초기화
Map<Long, List<CommentQueryDto>> resultMap = new HashMap<>();
for (Long rootId : rootCommentIds) {
resultMap.put(rootId, new ArrayList<>());
}

// 3) 단계별 조회용 parentIds 초기화
Set<Long> parentIds = new HashSet<>(rootCommentIds);

// 4) 자손 댓글용 프로젝션 정의
QCommentQueryDto childProj = new QCommentQueryDto(
comment.commentId,
comment.parent.commentId,
parentCommentCreator.nickname,
commentCreator.userId,
aliasOfCommentCreator.imageUrl,
commentCreator.nickname,
aliasOfCommentCreator.value,
aliasOfCommentCreator.color,
comment.createdAt,
comment.content,
comment.likeCount,
comment.status.eq(StatusType.INACTIVE)
);

// 5) 루프를 돌며 모든 깊이의 자식 댓글 조회 및 매핑
while (!parentIds.isEmpty()) {
List<CommentQueryDto> children = queryFactory
.select(childProj)
.from(comment)
.leftJoin(comment.parent, parentComment)
.leftJoin(parentComment.userJpaEntity, parentCommentCreator)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회
)
.fetch();

if (children.isEmpty()) break;

Set<Long> nextParentIds = new HashSet<>();
for (CommentQueryDto child : children) { // 조회한 자식 댓글들에 대하여
Long rootId = idToRoot.get(child.parentCommentId()); // 현재 자식댓글의 루트 댓글(부모 아님, 루트임)

resultMap.get(rootId).add(child); // 해당 루트 ID의 리스트에 자식 댓글 추가

// 현재 자식 댓글도 다음 단계의 parentIds로 사용하기 위해 매핑 저장
idToRoot.put(child.commentId(), rootId);
nextParentIds.add(child.commentId());
}
parentIds = nextParentIds; // 한단계 아래 계층에서 활용할 부모 댓글들
}

// 6) 각 루트별 value 리스트를 작성시간순으로 정렬
resultMap.values().forEach(list -> list.sort(Comparator.comparing(CommentQueryDto::createdAt)));

return resultMap;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

오호 굉장히 복잡하네요.. 우선 쿼리 횟수는 줄었으니 현 상황에서는 최선인 것 같습니다! 추후에 카톡에서 이야기 한 것처럼 Comment의 parentComment 대신 rootComment의 fk를 갖도록 하여 부모-자식 관계 => 루트-하위 관계로 풀어나가면 Querydsl의 로직이 조금 간단해질 것 같습니다!

저희가 초기에 Comment를 트리 구조로 잡은 이유가 '언급'이라는 기능에 따른 요구사항 때문이였는데, 이것도 다시보니 Comment에 'mentionUser'라는 User 테이블의 fk를 하나 가지고 있으면 해결 될 것 같네요! 추후 리팩토링에서 고려해봅시다!!
정말 수고하셨습니다~~!!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵넵 좋습니다! 현재 답글을 보여주는 로직이 자신의 부모 댓글 자체 보다는 부모 댓글을 작성한 작성자와 자식 댓글의 최상위 루트 댓글이 중요하니 엔티티 구조를 수정하는 것도 좋은 것 같습니다!
이러면 query dsl 에서 복잡한 로직을 수행하지 않아도 쿼리 호출 횟수를 줄일수 있을 것 같네요!

Comment on lines +12 to 14
public static CommentShowAllQuery of(Long postId, Long userId, String postTypeStr, String cursorStr) {
return new CommentShowAllQuery(postId, userId, PostType.from(postTypeStr), cursorStr); // 내부에서 PostType string 값 유효성 검증
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍🏻

@seongjunnoh seongjunnoh merged commit 7111010 into develop Aug 8, 2025
2 checks passed
@seongjunnoh seongjunnoh deleted the feat/#136/comment-show-for-single-post branch August 8, 2025 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-181] [feat] 댓글 목록 조회 api 개발

3 participants