Skip to content

[refactor] Post 관련 상속 전략 변경#247

Merged
seongjunnoh merged 7 commits intodevelopfrom
refactor/#246-post-entity
Aug 20, 2025
Merged

[refactor] Post 관련 상속 전략 변경#247
seongjunnoh merged 7 commits intodevelopfrom
refactor/#246-post-entity

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Aug 17, 2025

#️⃣ 연관된 이슈

closes #246

📝 작업 내용

기존 Post <-> Vote, Record, Feed 간의 상속 전략을 JOINED에서 SINGLE_TABLE로 변경하였습니다.

상속 전략을 변경하는 이유는 다음과 같습니다.

  1. 구현 난이도 감소
  2. 조회 성능 개선
    구체적인 내용은 노션 참고해주세요~

전략 변경을 위해, 다음과 같은 작업을 수행했습니다.

  1. 하위 필드 모두 nullable = true로 변경 (서비스 로직에게 null 검증 책임을 완전히 위임)
  2. 기록장 조회시 명시적인 left join 제거 (left join을 유지하면 내부에서 서브 테이블을 만들어서 여전히 left join이 발생하기 때문에 제거했습니다.)
  3. treat 기반 다운캐스트 (하위 엔티티 record, vote에 있는 필드에 접근하기 위해서 treat 문을 사용해서 entity를 다운캐스트했습니다.)

추가적으로 현재 운영서버에 있는 DB를 모두 drop 시키지 않고 DB 마이그레이션을 통해 데이터를 옮길 생각입니다!

📸 스크린샷

💬 리뷰 요구사항

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

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

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

Summary by CodeRabbit

  • New Features

    • 기록·투표 항목에 page 정보가 추가되어 목록 표시·정렬·필터링에 활용 가능합니다.
  • Refactor

    • 게시물 유형을 단일 테이블 구조로 통합해 조회 일관성과 성능을 개선했습니다.
    • 일부 필드의 null 허용으로 생성·수정 시 유연성이 향상되었습니다.
  • Bug Fixes

    • 기록 관련 조회 로직의 조인/처리 방식을 변경해 결과 일관성과 안정성을 개선했습니다.

@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

엔티티 매핑과 상속 전략을 변경했습니다. Post의 JPA 상속 전략을 JOINED → SINGLE_TABLE로 전환했고, Record/Vote에 page 필드 추가 및 isOverview의 primitive→wrapper/nullable 제약 완화, Feed의 일부 컬럼 nullable 허용 및 @table 주석 처리, RecordQueryRepositoryImpl에서 post를 treat(...)로 처리하도록 쿼리 변경이 포함됩니다.

Changes

Cohort / File(s) Change Summary
Post 상속 전략 변경
src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java
상속 전략을 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)로 변경 (기존 JOINED → SINGLE_TABLE).
Feed 엔티티 매핑 완화
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java
클래스 레벨 @Table(name = "feeds") 주석 처리; is_public, report_count 컬럼의 nullable=false 제거; book_id JoinColumn의 nullable 제거(선택적 관계).
Record 엔티티 확장/제약 변경
src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java
page(Integer) 컬럼 추가; isOverview 타입 booleanBoolean으로 변경 및 nullable=false 제거; @Table 주석 처리; 빌더 및 updateFrom에 page 반영.
Vote 엔티티 확장/제약 변경
src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java
page(Integer) 컬럼 추가; isOverviewnullable=false 제거; @Table 주석 처리.
매퍼/쿼리 적응
src/main/java/konkuk/thip/roompost/adapter/out/mapper/RecordMapper.java,
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java
RecordMapper에서 getIsOverview() 접근으로 변경. RecordQueryRepositoryImpl은 record/vote 직접 leftJoin 제거하고 treat(post, QRecordJpaEntity.class) / treat(post, QVoteJpaEntity.class) 사용하도록 쿼리 및 표현식(page/isOverview) 수정.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Repository as RecordQueryRepositoryImpl
  participant QueryDSL
  participant DB
  Client->>Repository: findMyRecords(...)
  Repository->>QueryDSL: build query using from(post) + treat(post, QRecord/ QVote) for page/isOverview
  QueryDSL->>DB: execute SQL (single-table posts with dtype discriminator)
  DB-->>QueryDSL: rows
  QueryDSL-->>Repository: mapped results
  Repository-->>Client: paged domain DTOs
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Post 및 파생 엔티티 상속 전략 JOINED → SINGLE_TABLE로 변경 (#246)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Feed의 @Table 주석 처리 및 컬럼 nullable 변경 (src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java) 이슈는 상속 전략 변경이 목적이므로 Feed의 테이블 주석/nullable 완화는 명시된 목표와 직접적으로 연결되지 않음.
Record/Vote에 page 컬럼 추가 (.../RecordJpaEntity.java, .../VoteJpaEntity.java) 상속 전략 전환 이슈에서 요구한 변경으로 명시되지 않은 신규 영속 필드 추가임.
Record/Vote의 isOverview 제약 완화(boolean→Boolean, nullable 제거/허용) (.../RecordJpaEntity.java, .../VoteJpaEntity.java) 상속 전략 변경 목표와 직접 관련이 없으며 스키마 제약을 변경함.
RecordQueryRepositoryImpl에서 기존 record/vote leftJoin 제거 및 treat 기반 전환 (.../RecordQueryRepositoryImpl.java) 상속 전략 변경 목적과 연관될 수 있으나 쿼리 구조 변경(조인 제거)은 별도 동작 변경으로 이슈에 명시된 범위를 벗어날 가능성이 있음.

Possibly related PRs

Suggested reviewers

  • seongjunnoh
  • hd0rable

Poem

토끼가 말하네, 테이블을 한 장으로 폈네 —
dtype 깃발 아래 포스트들이 모이고,
페이지를 품은 기록이 조용히 깃들고,
개요는 살짝 빈 칸으로 숨을 쉬네,
깡총—리팩터의 밭에 당근 한 움큼! 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#246-post-entity

🪧 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.
    • 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.
  • 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 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/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@github-actions
Copy link

github-actions bot commented Aug 17, 2025

Test Results

407 tests   407 ✅  31s ⏱️
121 suites    0 💤
121 files      0 ❌

Results for commit 322ddd8.

♻️ This comment has been updated with latest results.

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

🔭 Outside diff range comments (4)
src/main/java/konkuk/thip/roompost/adapter/out/mapper/RecordMapper.java (1)

24-31: Boolean → boolean 매핑 시 NPE 위험: null-safe 변환 필요

RecordJpaEntity.isOverview가 Boolean로 변경되어 null일 수 있습니다. 현 코드에서 auto-unboxing이 일어나면 NPE 발생 가능. 안전 변환을 권장합니다.

-                .isOverview(recordJpaEntity.getIsOverview())
+                .isOverview(Boolean.TRUE.equals(recordJpaEntity.getIsOverview()))

도메인 Record가 Boolean를 받도록 허용한다면(정책 변경 시) DTO/빌더 시그니처도 Boolean로 바꾸는 방법이 있습니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (2)

96-124: isOverview 필터 Boolean null 처리 및 페이지 범위 파라미터 안전성 보강

isOverview 파라미터가 Boolean인데 null 케이스 처리가 없습니다. 또한 pageStart/pageEnd가 null일 경우 between 사용 시 NPE 위험이 있습니다.

-        if (isOverview) {
-            voteCondition.and(vote.isOverview.isTrue());
-        } else {
-            voteCondition.and(vote.isOverview.isFalse())
-                    .and(vote.page.between(pageStart, pageEnd));
-        }
+        if (Boolean.TRUE.equals(isOverview)) {
+            voteCondition.and(vote.isOverview.isTrue());
+        } else if (Boolean.FALSE.equals(isOverview)) {
+            voteCondition.and(vote.isOverview.isFalse());
+            if (pageStart != null && pageEnd != null) {
+                voteCondition.and(vote.page.between(pageStart, pageEnd));
+            } else if (pageStart != null) {
+                voteCondition.and(vote.page.goe(pageStart));
+            } else if (pageEnd != null) {
+                voteCondition.and(vote.page.loe(pageEnd));
+            }
+        }
...
-        if (isOverview) {
-            recordCondition.and(record.isOverview.isTrue());
-        } else {
-            recordCondition.and(record.isOverview.isFalse())
-                    .and(record.page.between(pageStart, pageEnd));
-        }
+        if (Boolean.TRUE.equals(isOverview)) {
+            recordCondition.and(record.isOverview.isTrue());
+        } else if (Boolean.FALSE.equals(isOverview)) {
+            recordCondition.and(record.isOverview.isFalse());
+            if (pageStart != null && pageEnd != null) {
+                recordCondition.and(record.page.between(pageStart, pageEnd));
+            } else if (pageStart != null) {
+                recordCondition.and(record.page.goe(pageStart));
+            } else if (pageEnd != null) {
+                recordCondition.and(record.page.loe(pageEnd));
+            }
+        }

비즈니스 상 isOverview 필수 파라미터라면 컨트롤러/서비스 단 유효성 검증을 추가해 주세요.


126-141: CASE 표현식 null 안전성: 정렬/투영 일관성을 위해 coalesce 추가

dtype 일치 시 서브타입 컬럼 값이 null이면 CASE 결과가 null이 됩니다. 정렬(특히 desc)과 select 투영 시 데이터베이스마다 null 정렬 규칙이 달라 예측성이 떨어집니다. coalesce로 기본값(0)을 보장하세요.

     private NumberExpression<Integer> pageExpr() {
-        return new CaseBuilder()
+        return new CaseBuilder()
                 .when(post.dtype.eq(RECORD.getType())).then(record.page)
                 .when(post.dtype.eq(VOTE.getType())).then(vote.page)
-                .otherwise(0);
+                .otherwise(0)
+                .coalesce(0);
     }
...
     private NumberExpression<Integer> isOverviewExpr() {
-        return new CaseBuilder()
+        return new CaseBuilder()
                 .when(post.dtype.eq(RECORD.getType())).then(record.isOverview.castToNum(Integer.class))
                 .when(post.dtype.eq(VOTE.getType())).then(vote.isOverview.castToNum(Integer.class))
-                .otherwise(0);
+                .otherwise(0)
+                .coalesce(0);
     }

이렇게 하면 MINE 정렬과 select 시 Boolean 변환(eq(1)) 모두 안정적으로 동작합니다.

src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java (1)

31-37: Builder가 primitive boolean을 받아 null 표현 불가 — PR의 “nullable 위임” 의도와 충돌

Builder에서 boolean(primitive)을 받으면 null을 표현할 수 없고, 값 미지정 시 false가 흘러들어가 데이터가 의도치 않게 채워질 수 있습니다. nullable 전략에 맞추어 Boolean으로 받는 것이 안전합니다.

-    public RecordJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) {
+    public RecordJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, Boolean isOverview, RoomJpaEntity roomJpaEntity) {
         super(content, likeCount, commentCount, userJpaEntity);
         this.page = page;
         this.isOverview = isOverview;
         this.roomJpaEntity = roomJpaEntity;
     }
🧹 Nitpick comments (4)
src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1)

21-23: SINGLE_TABLE 전환 방향성은 타당. dtype/room_id 인덱스 고려 권장

단일 테이블 전략으로 조회 부하를 줄이려는 의도는 명확하고 적절합니다. 다만 이후 조회 조건에서 dtype과 room_id, status, created_at 등을 자주 사용할 것으로 보여, 운영 DB 마이그레이션 시 아래 인덱스를 함께 고려해 주세요.

  • posts(dtype)
  • posts(room_id)
  • posts(status)
  • posts(created_at desc, post_id desc) 또는 필요한 정렬 조합

데이터 규모가 커질수록 효과가 큽니다.

src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java (1)

21-26: 서브타입 컬럼 nullable 정책과 타입 일관성: isOverview를 Boolean으로 전환 제안

PR 설명대로 하위 엔티티 필드를 nullable 허용으로 가져가려면 primitive boolean보다는 wrapper(Boolean)가 일관됩니다. 현재 VoteJpaEntity만 boolean이라, RecordJpaEntity(Boolean)와 불일치합니다. 쿼리/정렬 시 null 안전성도 좋아집니다.

적용 예시:

-    @Column(name = "is_overview")
-    private boolean isOverview;
+    @Column(name = "is_overview")
+    private Boolean isOverview;

-    public VoteJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, boolean isOverview, RoomJpaEntity roomJpaEntity) {
+    public VoteJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Integer page, Boolean isOverview, RoomJpaEntity roomJpaEntity) {

-        this.isOverview = isOverview;
+        this.isOverview = isOverview;

도메인 Vote가 primitive를 유지한다면(예: vote.isOverview()), 오토박싱이 적용되므로 빌더 인자는 Boolean 유지해도 무방합니다. 반대로 도메인이 Boolean이면 vote.getIsOverview()로 맞춰 주세요.

Also applies to: 31-37, 39-46

src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)

25-30: reportCount 타입 일관성(도메인 Integer) 맞추기

도메인 Feed.reportCount는 Integer인데 JPA는 int입니다. SINGLE_TABLE에서 컬럼이 null일 수 있음을 고려하면 Integer가 더 안전합니다. 타입을 맞추면 마이그레이션/데이터 일관성 면에서도 유리합니다.

-    @Column(name = "report_count")
-    private int reportCount = 0;
+    @Column(name = "report_count")
+    private Integer reportCount;

빌더/업데이트 메서드 시그니처도 Integer로 맞추면 이상적입니다.

src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java (1)

15-15: SINGLE_TABLE 전환에 따른 하위 엔티티 @table 제거는 맞습니다 — 주석 보관 대신 완전 삭제 권장

주석으로 남겨두면 혼란을 줄 수 있습니다. 상위(PostJpaEntity)의 테이블에만 매핑되므로 이 라인은 제거해도 무방합니다.

-//@Table(name = "records")
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 93d182a and 47b5394.

📒 Files selected for processing (6)
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1 hunks)
  • src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteJpaEntity.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/mapper/RecordMapper.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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 등에서도 함께 사용될 가능성이 높습니다.
⏰ 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 (5)
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)

18-21: @table 주석 처리 OK

SINGLE_TABLE 하위 엔티티에서 @table 제거(또는 주석 처리)는 올바른 설정입니다. 상위(PostJpaEntity)의 posts 테이블 하나만 사용하게 됩니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (1)

179-186: 정렬 키 null 처리 일관성 유지

위 coalesce 적용 후에는 MINE 정렬의 isOverviewExpr().desc(), pageExpr().desc()가 안정적으로 동작합니다. 추가 변경은 불필요하나, page에 null이 많다면 필요에 따라 asc/desc 정책을 도메인 규칙으로 명시해 주세요.

src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java (3)

21-23: page 컬럼 추가 및 nullable 처리 적절

SINGLE_TABLE에서는 서브타입 전용 컬럼이 공용 테이블에 NULL 허용으로 들어가는 것이 자연스럽습니다. 마이그레이션에서 상위 테이블(Posts 등)에 page 컬럼 추가만 확인하면 됩니다.


39-46: null 보존 우려 불필요
Record.isOverview 필드는 primitive boolean으로 정의되어 있어 null이 존재하지 않습니다. 따라서 updateFrom에서 record.isOverview()를 사용하는 현재 구현은 올바르며, getIsOverview()로 변경할 필요가 없습니다.

Likely an incorrect or invalid review comment.


24-26: RecordJpaEntity Boolean 필드 Lombok getter 변경 영향 없음 확인

– RecordJpaEntity의 private Boolean isOverview; 에 대해 Lombok이 getIsOverview()를 생성하는 점을 검토했습니다.
– 전체 코드베이스에서 recordJpaEntity.isOverview() 호출은 없으며, RecordMapper 등 접근부는 모두 getIsOverview()로 작성되어 있습니다.
– VoteJpaEntity는 primitive boolean isOverview이기 때문에 기존대로 isOverview()가 제공됩니다.

따라서 컴파일 오류나 런타임 이슈는 발생하지 않습니다.

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

🧹 Nitpick comments (1)
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (1)

94-116: isOverview/page의 null 처리 정책 명확화 필요 (where 절 필터링 누락 가능성)

nullable 전환 이후, 아래 분기에서:

  • isOverview가 null인 레코드는 isTrue()/isFalse() 어느 쪽에도 매칭되지 않습니다.
  • page가 null이면 between(pageStart, pageEnd)에서 제외됩니다.

이게 의도(미완성/비정상 데이터는 제외)라면 OK입니다. 만약 null을 각각 false/0으로 취급하고 포함시키려면 다음처럼 보완을 고려해 주세요.

-            voteCondition.and(treat(post, QVoteJpaEntity.class).isOverview.isFalse())
-                    .and(treat(post, QVoteJpaEntity.class).page.between(pageStart, pageEnd));
+            voteCondition.and(
+                    treat(post, QVoteJpaEntity.class).isOverview.isFalse()
+                            .or(treat(post, QVoteJpaEntity.class).isOverview.isNull())
+            ).and(
+                    treat(post, QVoteJpaEntity.class).page.coalesce(0).between(pageStart, pageEnd)
+            )
-            recordCondition.and(treat(post, QRecordJpaEntity.class).isOverview.isFalse())
-                    .and(treat(post, QRecordJpaEntity.class).page.between(pageStart, pageEnd));
+            recordCondition.and(
+                    treat(post, QRecordJpaEntity.class).isOverview.isFalse()
+                            .or(treat(post, QRecordJpaEntity.class).isOverview.isNull())
+            ).and(
+                    treat(post, QRecordJpaEntity.class).page.coalesce(0).between(pageStart, pageEnd)
+            )

추가로, 해당 컬럼들(dtype, room_id, is_overview, page)을 중심으로 복합/보조 인덱스를 점검하면 SINGLE_TABLE 전환 후 조회 성능 안정화에 도움이 됩니다.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 47b5394 and 322ddd8.

📒 Files selected for processing (1)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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 등에서도 함께 사용될 가능성이 높습니다.
⏰ 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 (3)
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (3)

23-23: treat 정적 임포트 사용 호환성 확인 (Querydsl/JPA 버전)

com.querydsl.jpa.JPAExpressions.treat 사용은 버전 의존적입니다. 현재 의존성(Querydsl JPA, Hibernate/JPA 2.1+)이 해당 API를 지원하는지 확인해 주세요. 만약 호환 이슈가 있다면, 대안으로 alias 공유 방식(new QRecordJpaEntity(post.getMetadata()))도 고려 가능합니다.


59-66: SINGLE_TABLE + treat 다운캐스트 접근 방식 적절함

JOIN 없이 단일 테이블에서 하위 타입 필드 접근을 위해 treat(post, QVoteJpaEntity.class) / treat(post, QRecordJpaEntity.class)를 사용하는 접근은 의도에 부합합니다. dtype 가드(post.dtype.eq(...))도 함께 있어 안전합니다.


166-173: 키셋 커서 비교식과 정렬 기준의 방향성이 일치합니다 (desc 정렬 기준).

MINE의 정렬 키가 isOverview desc, page desc, postId desc인 상황에서, 커서 비교식이 모두 lt를 사용해 다음 페이지를 잘라내는 형태로 정합적입니다. 위에서 제안한 coalesce(0) 적용을 전제하면 안정적으로 동작할 것으로 보입니다.

Comment on lines 123 to 131
// Case: pageExpr (Record, Vote 분기)
private NumberExpression<Integer> pageExpr() {
return new CaseBuilder()
.when(post.dtype.eq(RECORD.getType())).then(record.page)
.when(post.dtype.eq(VOTE.getType())).then(vote.page)
.when(post.dtype.eq(RECORD.getType()))
.then(treat(post, QRecordJpaEntity.class).page)
.when(post.dtype.eq(VOTE.getType()))
.then(treat(post, QVoteJpaEntity.class).page)
.otherwise(0);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

pageExpr가 null을 반환할 수 있어 정렬/커서 비교가 깨집니다 — coalesce(0) 적용 권장

page가 Integer(Nullable)로 변경되었기 때문에 현재 구현은 null을 그대로 반환합니다. ORDER BY와 키셋 커서 비교(lt/eq)에서 null은 3값 논리로 처리되어 페이지네이션이 불안정해질 수 있습니다. 0으로 정규화(coalesce)해 주세요.

                 return new CaseBuilder()
-                .when(post.dtype.eq(RECORD.getType()))
-                .then(treat(post, QRecordJpaEntity.class).page)
-                .when(post.dtype.eq(VOTE.getType()))
-                .then(treat(post, QVoteJpaEntity.class).page)
+                .when(post.dtype.eq(RECORD.getType()))
+                .then(treat(post, QRecordJpaEntity.class).page.coalesce(0))
+                .when(post.dtype.eq(VOTE.getType()))
+                .then(treat(post, QVoteJpaEntity.class).page.coalesce(0))
                 .otherwise(0);

이 개선은 select DTO의 page 값과 Cursor.getInteger(…) 처리에서도 NPE/불일치 위험을 줄여줍니다.

📝 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
// Case: pageExpr (Record, Vote 분기)
private NumberExpression<Integer> pageExpr() {
return new CaseBuilder()
.when(post.dtype.eq(RECORD.getType())).then(record.page)
.when(post.dtype.eq(VOTE.getType())).then(vote.page)
.when(post.dtype.eq(RECORD.getType()))
.then(treat(post, QRecordJpaEntity.class).page)
.when(post.dtype.eq(VOTE.getType()))
.then(treat(post, QVoteJpaEntity.class).page)
.otherwise(0);
}
// Case: pageExpr (Record, Vote 분기)
private NumberExpression<Integer> pageExpr() {
return new CaseBuilder()
.when(post.dtype.eq(RECORD.getType()))
.then(treat(post, QRecordJpaEntity.class).page.coalesce(0))
.when(post.dtype.eq(VOTE.getType()))
.then(treat(post, QVoteJpaEntity.class).page.coalesce(0))
.otherwise(0);
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java
around lines 123 to 131, the CaseBuilder can yield null for page (since page is
now nullable) which breaks ORDER BY and keyset cursor comparisons; wrap the case
expression with a coalesce returning 0 so nulls are normalized. Concretely:
build the CaseBuilder as you have, assign it to a NumberExpression<Integer>
variable, then return a coalesced expression that returns 0 when the case result
is null (e.g., use QueryDSL/Expressions.coalesce(caseExpr, 0) or equivalent API
in your codebase) so pagination comparisons never see null.

Comment on lines 133 to 141
// Case: isOverviewExpr (총평 여부를 정렬 기준으로 사용)
private NumberExpression<Integer> isOverviewExpr() {
return new CaseBuilder()
.when(post.dtype.eq(RECORD.getType())).then(record.isOverview.castToNum(Integer.class))
.when(post.dtype.eq(VOTE.getType())).then(vote.isOverview.castToNum(Integer.class))
.when(post.dtype.eq(RECORD.getType()))
.then(treat(post, QRecordJpaEntity.class).isOverview.castToNum(Integer.class))
.when(post.dtype.eq(VOTE.getType()))
.then(treat(post, QVoteJpaEntity.class).isOverview.castToNum(Integer.class))
.otherwise(0);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

isOverviewExpr도 null 가능성이 있어 비교/정렬이 불안정합니다 — coalesce(0) 적용

Boolean(Nullable) → Number로 cast한 뒤 null이 그대로 유지됩니다. eq/lt 비교와 ORDER BY에서 3값 논리로 변질되므로 0으로 coalesce 하는 편이 안전합니다.

         return new CaseBuilder()
-                .when(post.dtype.eq(RECORD.getType()))
-                .then(treat(post, QRecordJpaEntity.class).isOverview.castToNum(Integer.class))
-                .when(post.dtype.eq(VOTE.getType()))
-                .then(treat(post, QVoteJpaEntity.class).isOverview.castToNum(Integer.class))
+                .when(post.dtype.eq(RECORD.getType()))
+                .then(treat(post, QRecordJpaEntity.class).isOverview.castToNum(Integer.class).coalesce(0))
+                .when(post.dtype.eq(VOTE.getType()))
+                .then(treat(post, QVoteJpaEntity.class).isOverview.castToNum(Integer.class).coalesce(0))
                 .otherwise(0);

이 변경은 selectPostQueryDto의 isOverviewExpr().eq(1)에서도 null 비교를 피하게 되어 투영 시 Boolean/boolean 매핑 문제를 예방합니다.

📝 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
// Case: isOverviewExpr (총평 여부를 정렬 기준으로 사용)
private NumberExpression<Integer> isOverviewExpr() {
return new CaseBuilder()
.when(post.dtype.eq(RECORD.getType())).then(record.isOverview.castToNum(Integer.class))
.when(post.dtype.eq(VOTE.getType())).then(vote.isOverview.castToNum(Integer.class))
.when(post.dtype.eq(RECORD.getType()))
.then(treat(post, QRecordJpaEntity.class).isOverview.castToNum(Integer.class))
.when(post.dtype.eq(VOTE.getType()))
.then(treat(post, QVoteJpaEntity.class).isOverview.castToNum(Integer.class))
.otherwise(0);
}
// Case: isOverviewExpr (총평 여부를 정렬 기준으로 사용)
private NumberExpression<Integer> isOverviewExpr() {
return new CaseBuilder()
.when(post.dtype.eq(RECORD.getType()))
.then(treat(post, QRecordJpaEntity.class)
.isOverview
.castToNum(Integer.class)
.coalesce(0))
.when(post.dtype.eq(VOTE.getType()))
.then(treat(post, QVoteJpaEntity.class)
.isOverview
.castToNum(Integer.class)
.coalesce(0))
.otherwise(0);
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java
around lines 133 to 141, the CaseBuilder expression returns nullable Integer
(from casting Boolean) which leaves nulls in comparisons and ORDER BY; update
the expression to wrap the cast result with a coalesce(0) so any null becomes 0
(apply coalesce to each then-branch result or to the whole case result) to
ensure stable eq/lt checks and ordering and to avoid null projection when
calling isOverviewExpr().eq(1).

Copy link
Collaborator

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

굳굳 좋습니다!! 생각보다 수월하게 마무리된 것 같네요!!

@Table(name = "posts")
@Getter
@Inheritance(strategy = InheritanceType.JOINED)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Comment on lines -25 to +32
@Column(name = "is_public", nullable = false)
@Column(name = "is_public")
private Boolean isPublic;

@Column(name = "report_count", nullable = false)
@Column(name = "report_count")
private int reportCount = 0;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id", nullable = false)
@JoinColumn(name = "book_id")
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳

Comment on lines -62 to +65
BooleanBuilder voteCondition = new BooleanBuilder();
voteCondition.and(post.dtype.eq(VOTE.getType()))
.and(vote.roomJpaEntity.roomId.eq(roomId));
BooleanBuilder voteCondition = new BooleanBuilder()
.and(post.dtype.eq(VOTE.getType()))
.and(treat(post, QVoteJpaEntity.class).roomJpaEntity.roomId.eq(roomId));
Copy link
Collaborator

Choose a reason for hiding this comment

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

오호 이렇게 jpa treat 로 post, record, vote jpa entity 간의 명시적인 left join을 없앨 수 있군요!! 좋습니다!
성능이 한층 개선될 것 같네요!!

Copy link
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.

너무좋습니다

@Table(name = "posts")
@Getter
@Inheritance(strategy = InheritanceType.JOINED)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
Copy link
Member

Choose a reason for hiding this comment

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

@seongjunnoh seongjunnoh merged commit d2a1fee into develop Aug 20, 2025
4 checks passed
@seongjunnoh seongjunnoh deleted the refactor/#246-post-entity branch August 20, 2025 13:35
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-292] [refactor] Post 관련 상속 전략 변경

3 participants