Skip to content

[Refactor] StatusFilterAspect 동작 수정#352

Open
seongjunnoh wants to merge 7 commits intodevelopfrom
refactor/#351/status-filter-aspect
Open

[Refactor] StatusFilterAspect 동작 수정#352
seongjunnoh wants to merge 7 commits intodevelopfrom
refactor/#351/status-filter-aspect

Conversation

@seongjunnoh
Copy link
Copy Markdown
Collaborator

@seongjunnoh seongjunnoh commented Mar 8, 2026

#️⃣ 연관된 이슈

closes #351

📝 작업 내용

1. Hibernate Filter 조건 단순화

  • status IN (:statuses)status = 'ACTIVE' 로 변경
  • @IncludeInactive 어노테이션 제거, FilterModeACTIVE_ONLY / UNFILTERED 2가지로 단순화

2. FilterContextHolder (ThreadLocal) 도입

  • @Unfiltered@Transactional 의 결합 문제 해소
  • @Unfiltered AOP 는 ThreadLocal 에 UNFILTERED 모드만 기록 (Session 접근 없음)
  • @Transactional 경계의 StatusFilterAspect 가 ThreadLocal 을 읽어 filter 활성 여부 결정
  • @Unfiltered@Transactional 없이도 독립적으로 동작

3. Query Persistence Adapter 에 @Transactional(readOnly = true) 명시

  • SimpleJpaRepository 는 Spring AOP 가 intercept 하지 못하는 구조 → 서비스에 @Transactional 이 없으면 filter 미적용 경로 존재
  • 모든 데이터 접근이 영속성 어댑터를 경유하는 헥사고날 아키텍처 활용
  • 어댑터 경계에서 서비스 @Transactional 유무와 무관하게 filter 동작 보장
  • spring transactional 명시할 경우 DB에 부가적인 command가 나가지만, 서비스 트래픽 규모상 이는 무시해도 될만한 수준이라고 판단

4. AOP 실행 순서 명시적 보장

  • StatusFilterAspectem.unwrap(Session.class) 호출 시 트랜잭션이 먼저 열려있어야 함
  • @EnableTransactionManagement(order = LOWEST_PRECEDENCE - 1) 로 TransactionInterceptor 를 outer 로 고정
  • @Order(LOWEST_PRECEDENCE) 로 StatusFilterAspect 를 inner 로 명시

5. 테스트 정비

  • StatusFilterTest 재작성: adapter 경계 기준으로 filter 동작 검증
  • application-test.ymlopen-in-view: false 추가하여 production 과 동일한 환경에서 검증
  • dev, prod yml도 open-in-view: false 설정값 추가
  • REST API 애플리케이션 서버에서는 open-in-view 설정이 true 일 필요 X

📸 스크린샷

image (기존에 개발한 Hibernate Filter 자동화에 대한 플로우 차트)

💬 리뷰 요구사항

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

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

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

Summary by CodeRabbit

릴리스 노트

  • 리팩터링

    • 쿼리 계층의 트랜잭션 처리 체계를 일관되게 정비해 조회 시 데이터 일관성과 안정성을 강화했습니다.
    • 상태 필터링 로직을 단순화하고 필터 제어 수단을 도입해 비활성 엔터티 처리 흐름을 명확히 했습니다.
    • 불필요한 레거시 요소를 제거하여 코드 가독성과 유지보수성을 향상시켰습니다.
  • 테스트

    • 테스트 설정과 케이스를 재구성해 필터 및 트랜잭션 경계 동작을 보다 넓고 명확하게 검증하도록 개선했습니다.

- statusFilter는 'status = active' 쿼리로 고정
- 굳이 in 절 필요없다고 판단
- active_only, unfiltered(필터 적용 X == status 조건 제거) 2가지 mode
- active_only mode가 default
- 트랜잭션 경계에서 status filter 자동으로 동작하도록 설정
- @unfiltered 동작 시 ThreadLocal mode 변경 & 필터 동작 X
- 사용하지 않는 @IncludeInactive 제거
- status filter는 트랜잭션이 열려있어야 동작 가능
- 조회 시 사용하는 영속성 adapter 클래스에 read only 트랜잭션 명시
- QueryPersistenceAdapter 하위의 QueryDSL, spring data jpa repository 인터페이스를 활용하는 DB access 코드 모두 status filter 동작 가능
- 트랜잭션 명시로 인해 DB에 추가적인 command가 날라가지만, 이는 감수할만한 트레이드 오프라고 판단
…sFilterAspect 동작하도록 순서 강제 (#351)

- TransactionInterceptor 동작으로 Hibernate Session이 열려야, StatusFilterAspect 에서 Session에 Hibernate Filter 활성화 가능
Copilot AI review requested due to automatic review settings March 8, 2026 08:38
@seongjunnoh seongjunnoh linked an issue Mar 8, 2026 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 8, 2026

Walkthrough

트랜잭션 경계와 무관하게 상태 필터를 일관되게 적용하도록 시스템을 재구성했습니다. 여러 QueryPersistenceAdapter에 읽기 전용 트랜잭션 어노테이션을 추가하고, ThreadLocal 기반 FilterContextHolder를 도입하여 StatusFilterAspect를 간소화했으며 BaseJpaEntity의 Hibernate 필터 정의를 단순화했습니다.

Changes

Cohort / File(s) Summary
읽기 전용 트랜잭션 어노테이션 추가
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java, src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java, src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java, src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java, src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java, src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java, src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java, src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java, src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java, src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java, src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java, src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java, src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java, src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java, src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java
각 QueryPersistenceAdapter 클래스에 @Transactional(readOnly = true) 및 관련 import 추가 — 조회용 트랜잭션 경계 통일.
필터 모드 관리 인프라 추가
src/main/java/konkuk/thip/common/aop/FilterContextHolder.java
ThreadLocal 기반 FilterContextHolder 추가: enum {ACTIVE_ONLY, UNFILTERED}, get/set/clear 메서드 제공.
상태 필터 AOP 재구조화
src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java
Aspect 단순화: @Order 지정, IncludeInactive 경로 제거, unfilteredenableActiveByDefault 두 어드바이스로 필터 on/off 제어; FilterContextHolder와 트랜잭션 경계 기반 처리로 변경.
Hibernate 필터 정의 단순화
src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java
파라미터화된 status in (:statuses) 필터 제거 및 고정조건 status = 'ACTIVE'로 변경 (FilterDef 단순화).
기존 어노테이션/에러 코드 제거
src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java, src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
더 이상 사용되지 않는 IncludeInactive 어노테이션 파일 삭제 및 ErrorCode에서 관련 PERSISTENCE_TRANSACTION_REQUIRED 상수 제거.
트랜잭션 관리 설정 변경
src/main/java/konkuk/thip/config/AppConfig.java
@Configuration@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE - 1) 추가로 트랜잭션 관리 활성화 및 우선순위 설정.
테스트 인프라 및 테스트 변경
src/test/java/konkuk/thip/config/StatusFilterTestConfig.java, src/test/java/konkuk/thip/config/TestUserJpaRepository.java, src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java, src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java
테스트용 persistence adapter 및 Unfiltered 서비스 추가, 기존 테스트 서비스군 통합/리팩터링, 테스트 케이스 및 헬퍼 정리, FlushOrderTest 신규 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Service as Service / Controller
    participant Aspect as StatusFilterAspect
    participant FilterCtx as FilterContextHolder
    participant Tx as TransactionManager
    participant EM as EntityManager / Session
    participant DB as Database

    Service->>Aspect: 호출 (메서드, `@Unfiltered` 여부)
    Aspect->>FilterCtx: 기존 모드 저장 및 필요시 set(UNFILTERED)
    Aspect->>Service: proceed()
    Service->>Tx: 트랜잭션 시작 (optional)
    Tx->>EM: em.unwrap(Session) / 세션 획득
    EM->>DB: 쿼리 실행 (Hibernate filter state 적용 여부에 따라 필터 적용)
    DB-->>EM: 결과 반환
    Tx-->>Service: 트랜잭션 종료 (optional)
    Service-->>Aspect: 반환
    Aspect->>FilterCtx: 이전 모드 복원
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • buzz0331
  • hd0rable

Poem

🐰
필터를 품은 스레드 속 내 작은 집,
ACTIVE_only로 깡충깡충 달려가네.
트랜잭션 경계 넘어도 나는 기억해,
AOP가 살짝 춤추면 모드 바뀌네.
당근 한 입, 코드 한 줄 — 깔끔해졌네!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경 사항인 StatusFilterAspect 동작 수정을 명확하게 설명하고 있으며, 리팩토링 범위와 의도를 잘 전달합니다.
Linked Issues check ✅ Passed 코드 변경 사항이 연결된 이슈 #351의 모든 주요 요구사항을 충족합니다: osiv false 설정, 트랜잭션 의존성 제거, QueryDSL 어댑터에 @Transactional(readOnly=true) 적용 완료.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 이슈 #351의 범위 내에 있습니다. FilterContextHolder, @Unfiltered 어노테이션, StatusFilterAspect 리팩토링, 어댑터 트랜잭션 추가는 모두 필터 동작 개선을 위한 필수 변경입니다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#351/status-filter-aspect

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

❤️ Share

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 8, 2026

Test Results

487 tests   487 ✅  45s ⏱️
146 suites    0 💤
146 files      0 ❌

Results for commit a2e0d6b.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the StatusFilterAspect and related infrastructure to fix several correctness and robustness issues with the Hibernate statusFilter mechanism in the hexagonal (ports & adapters) architecture.

Changes:

  • Simplifies the Hibernate filter from a parameterized status IN (:statuses) to a hardcoded status = 'ACTIVE' condition, removing the @IncludeInactive annotation in favor of a single @Unfiltered mode.
  • Introduces FilterContextHolder (ThreadLocal) to decouple @Unfiltered intent (set at service level) from actual Session manipulation (performed at @Transactional adapter boundary), allowing @Unfiltered to work without requiring @Transactional on the service method itself.
  • Adds @Transactional(readOnly = true) to all *QueryPersistenceAdapter classes and configures explicit AOP ordering (@EnableTransactionManagement(order = LOWEST_PRECEDENCE - 1)) to guarantee the transaction is open before StatusFilterAspect fires.

Reviewed changes

Copilot reviewed 9 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
FilterContextHolder.java New ThreadLocal holder for FilterMode (ACTIVE_ONLY / UNFILTERED) to propagate filter intent across AOP boundaries
StatusFilterAspect.java Refactored to use ThreadLocal; unfiltered() records intent, enableActiveByDefault() reads it; simplified filter enable/disable logic
AppConfig.java Adds @EnableTransactionManagement(order = LOWEST_PRECEDENCE - 1) to ensure transaction opens before StatusFilterAspect runs
BaseJpaEntity.java Simplifies @FilterDef / @Filter to parameterless status = 'ACTIVE'
IncludeInactive.java Deleted — no longer supported
ErrorCode.java Removes PERSISTENCE_TRANSACTION_REQUIRED (no longer needed)
All *QueryPersistenceAdapter files (14) Adds @Transactional(readOnly = true) at class level to guarantee filter activation
StatusFilterTestConfig.java, StatusFilterTest.java, TestUserJpaRepository.java Rewrites tests to validate adapter-boundary filter behavior and @Unfiltered with/without @Transactional

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to 44
private static final String PCUT_UNFILTERED = "@annotation(" + ANN_UNFILTERED + ")";

/**
* 기본: @Transactional 경계에서 ThreadLocal을 읽어 filter를 활성화.
* @Unfiltered가 붙은 메서드는 제외 (PCUT_UNFILTERED에서 처리).
*/
private static final String PCUT_TX_DEFAULT =
"(" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")" +
" && !" + "@annotation(" + ANN_INCLUDE_INACTIVE + ")" +
" && !" + "@annotation(" + ANN_UNFILTERED + ")";
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

The @Unfiltered annotation (declared in Unfiltered.java) uses @Target({ElementType.METHOD, ElementType.TYPE}), which allows class-level usage. However, neither PCUT_UNFILTERED nor the exclusion in PCUT_TX_DEFAULT handles class-level @Unfiltered:

  • PCUT_UNFILTERED = "@annotation(ANN_UNFILTERED)" — only matches method-level annotations.
  • PCUT_TX_DEFAULT exclusion && !@annotation(ANN_UNFILTERED) — also only checks method-level annotations.

If @Unfiltered is placed on a class, its methods would still be intercepted by PCUT_TX_DEFAULT (which uses @within for @Transactional), causing the filter to be incorrectly activated. The AOP implementation should either remove ElementType.TYPE from @Unfiltered's @Target to prevent unsupported class-level usage, or extend both pointcuts with @within handling (e.g., || @within(ANN_UNFILTERED)) to properly support class-level annotation.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +24
static void clear() {
context.remove();
}

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

The clear() method is defined with package-private visibility, making it inaccessible from outside the konkuk.thip.common.aop package. It is currently not called from anywhere in the codebase (neither production code nor tests). If it's intended for use as a cleanup mechanism (e.g., after each HTTP request via a filter), it needs to be made public. If it's intended only for unit tests of the StatusFilterAspect, it should be called from within the test. Otherwise, this is dead code and should be removed.

Suggested change
static void clear() {
context.remove();
}

Copilot uses AI. Check for mistakes.
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: 1

🧹 Nitpick comments (3)
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java (1)

6-10: 조회 어댑터 전용 메타 어노테이션으로 묶어두는 편이 안전합니다.

이 PR부터 상태 필터의 정상 동작이 @Transactional(readOnly = true) 누락 여부에 달리게 됐는데, 지금처럼 각 QueryPersistenceAdapter에 수동으로 붙이면 신규 어댑터에서 빠뜨리기 쉽습니다. @Repository@Transactional(readOnly = true)를 합성한 공통 어노테이션으로 의도를 고정해 두는 쪽을 권합니다.

Based on learnings, THIP 팀은 Transactional 어노테이션을 서비스 메서드에 붙이는 것으로 통일했다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java`
around lines 6 - 10, Create a composed annotation (e.g., `@ReadOnlyRepository`)
that meta-annotates `@Repository` and `@Transactional`(readOnly = true), apply it to
RoomParticipantQueryPersistenceAdapter (replacing the existing `@Repository` and
`@Transactional` annotations), and update other QueryPersistenceAdapter classes to
use this composed annotation so new adapters won’t miss the readOnly
transactional intent; remove the manual `@Repository/`@Transactional usage from
those classes.
src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java (2)

62-72: 필터가 활성화되지 않은 경우에도 disableFilter() 호출됨

FilterContextHolder.get() == UNFILTERED인 경우 필터가 활성화되지 않지만, wasEnabled가 false이면 finally 블록에서 disableFilter(s)가 호출됩니다. Hibernate의 disableFilter는 이미 비활성화된 필터에 대해 호출해도 안전하지만, 불필요한 호출을 피하는 것이 좋습니다.

♻️ 제안된 수정
     `@Around`(PCUT_TX_DEFAULT)
     public Object enableActiveByDefault(ProceedingJoinPoint pjp) throws Throwable {
         Session s = em.unwrap(Session.class);
         boolean wasEnabled = isFilterEnabled(s);
+        boolean enabledByThisAdvice = false;

         if (FilterContextHolder.get() == ACTIVE_ONLY && !wasEnabled) {
             enableFilter(s);
+            enabledByThisAdvice = true;
         }

         try {
             return pjp.proceed();
         } finally {
-            if (!wasEnabled) {
+            if (enabledByThisAdvice) {
                 disableFilter(s);
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java` around lines 62
- 72, The finally block is disabling the filter even when it was never enabled
(e.g., when FilterContextHolder.get() == UNFILTERED); fix by tracking whether
this method actually enabled the filter and only disabling in finally when we
enabled it here. Concretely, inside StatusFilterAspect (around the code that
checks FilterContextHolder.get() == ACTIVE_ONLY and calls enableFilter(s)), set
a local boolean (e.g., enabledHere) to true when enableFilter(s) is invoked, and
in the finally block replace the current condition with a check that disables
only if enabledHere is true (or if wasEnabled was true when your logic requires
it). This uses the existing symbols enableFilter(s), disableFilter(s),
FilterContextHolder, ACTIVE_ONLY, wasEnabled and pjp.proceed() to locate the
change.

36-44: 클래스 레벨 @Unfiltered 지원 부족

PCUT_UNFILTERED 포인트컷이 @annotation()만 사용하고 @within()을 포함하지 않아, 클래스 레벨에 선언된 @Unfiltered는 인터셉트되지 않습니다. @Unfiltered 어노테이션이 @Target({ElementType.METHOD, ElementType.TYPE})으로 정의되어 있는 것과 불일치합니다.

PCUT_TX_DEFAULT의 제외 조건(44줄)도 @annotation(ANN_UNFILTERED)만 체크하므로, 클래스 레벨 @Unfiltered 적용 시 해당 클래스의 메서드들이 올바르게 제외되지 않습니다.

현재 코드베이스에는 클래스 레벨 사용이 없어 즉각적인 문제는 없으나, 일관성 있는 설계를 위해 포인트컷에 @within()을 추가하는 것이 권장됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java` around lines 36
- 44, PCUT_UNFILTERED currently checks only method-level
`@annotation`(ANN_UNFILTERED); update the pointcut to also include class-level
`@within`(ANN_UNFILTERED) so class-scoped `@Unfiltered` is matched. Similarly,
modify PCUT_TX_DEFAULT's exclusion (the !"@annotation(" + ANN_UNFILTERED + ")")
to exclude methods when either `@annotation`(ANN_UNFILTERED) OR
`@within`(ANN_UNFILTERED) is present. Apply these changes to the constants
PCUT_UNFILTERED and PCUT_TX_DEFAULT in StatusFilterAspect so they reference
ANN_UNFILTERED in both `@annotation`(...) and `@within`(...).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/konkuk/thip/common/aop/FilterContextHolder.java`:
- Around line 10-22: The ThreadLocal in FilterContextHolder currently only
supports set(FilterMode) and clear(), which loses the outer caller's mode when
nested `@Unfiltered` calls occur; modify FilterContextHolder to save and restore
previous values by adding a method like setAndGetPrevious(FilterMode) or a
restore(FilterMode) (or make set return the previous FilterMode) so callers can
capture the prior mode, and change StatusFilterAspect's around advice to call
get() before setting the new mode and then in finally restore(previous) (or call
clear() only when previous was null) so nested invocations correctly revert to
the outer FilterMode instead of defaulting back to ACTIVE_ONLY.

---

Nitpick comments:
In `@src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java`:
- Around line 62-72: The finally block is disabling the filter even when it was
never enabled (e.g., when FilterContextHolder.get() == UNFILTERED); fix by
tracking whether this method actually enabled the filter and only disabling in
finally when we enabled it here. Concretely, inside StatusFilterAspect (around
the code that checks FilterContextHolder.get() == ACTIVE_ONLY and calls
enableFilter(s)), set a local boolean (e.g., enabledHere) to true when
enableFilter(s) is invoked, and in the finally block replace the current
condition with a check that disables only if enabledHere is true (or if
wasEnabled was true when your logic requires it). This uses the existing symbols
enableFilter(s), disableFilter(s), FilterContextHolder, ACTIVE_ONLY, wasEnabled
and pjp.proceed() to locate the change.
- Around line 36-44: PCUT_UNFILTERED currently checks only method-level
`@annotation`(ANN_UNFILTERED); update the pointcut to also include class-level
`@within`(ANN_UNFILTERED) so class-scoped `@Unfiltered` is matched. Similarly,
modify PCUT_TX_DEFAULT's exclusion (the !"@annotation(" + ANN_UNFILTERED + ")")
to exclude methods when either `@annotation`(ANN_UNFILTERED) OR
`@within`(ANN_UNFILTERED) is present. Apply these changes to the constants
PCUT_UNFILTERED and PCUT_TX_DEFAULT in StatusFilterAspect so they reference
ANN_UNFILTERED in both `@annotation`(...) and `@within`(...).

In
`@src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java`:
- Around line 6-10: Create a composed annotation (e.g., `@ReadOnlyRepository`)
that meta-annotates `@Repository` and `@Transactional`(readOnly = true), apply it to
RoomParticipantQueryPersistenceAdapter (replacing the existing `@Repository` and
`@Transactional` annotations), and update other QueryPersistenceAdapter classes to
use this composed annotation so new adapters won’t miss the readOnly
transactional intent; remove the manual `@Repository/`@Transactional usage from
those classes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4286f2ee-3757-47d6-a364-4cd4d3c89c2d

📥 Commits

Reviewing files that changed from the base of the PR and between 9e87db0 and 68d2fb4.

📒 Files selected for processing (24)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java
  • src/main/java/konkuk/thip/common/aop/FilterContextHolder.java
  • src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java
  • src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
  • src/main/java/konkuk/thip/config/AppConfig.java
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java
  • src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java
  • src/test/java/konkuk/thip/config/StatusFilterTestConfig.java
  • src/test/java/konkuk/thip/config/TestUserJpaRepository.java
💤 Files with no reviewable changes (2)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
  • src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java

Comment on lines +10 to +22
private static final ThreadLocal<FilterMode> context =
ThreadLocal.withInitial(() -> FilterMode.ACTIVE_ONLY);

public static FilterMode get() {
return context.get();
}

public static void set(FilterMode mode) {
context.set(mode);
}

static void clear() {
context.remove();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

중첩 @Unfiltered 호출에서 상태가 유실됩니다.

지금 API는 set()clear()만 가능해서, @Unfiltered 메서드 A 안에서 또 다른 @Unfiltered 메서드 B를 호출하면 B의 finally에서 clear()한 뒤 A의 남은 구간이 다시 ACTIVE_ONLY로 돌아갑니다. 이전 모드를 저장하고 finally에서 복원하는 방식으로 바꿔야 합니다.

🔧 예시 방향
-    public static void set(FilterMode mode) {
-        context.set(mode);
-    }
-
-    static void clear() {
-        context.remove();
-    }
+    static FilterMode push(FilterMode mode) {
+        FilterMode previous = context.get();
+        context.set(mode);
+        return previous;
+    }
+
+    static void restore(FilterMode previous) {
+        if (previous == null) {
+            context.remove();
+            return;
+        }
+        context.set(previous);
+    }

StatusFilterAspect 쪽에서는 around advice에서 이전 값을 받아 finally에서 restore(previous) 하도록 맞추면 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/konkuk/thip/common/aop/FilterContextHolder.java` around lines
10 - 22, The ThreadLocal in FilterContextHolder currently only supports
set(FilterMode) and clear(), which loses the outer caller's mode when nested
`@Unfiltered` calls occur; modify FilterContextHolder to save and restore previous
values by adding a method like setAndGetPrevious(FilterMode) or a
restore(FilterMode) (or make set return the previous FilterMode) so callers can
capture the prior mode, and change StatusFilterAspect's around advice to call
get() before setting the new mode and then in finally restore(previous) (or call
clear() only when previous was null) so nested invocations correctly revert to
the outer FilterMode instead of defaulting back to ACTIVE_ONLY.

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

🧹 Nitpick comments (1)
src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java (1)

20-40: PR 범위와 무관한 테스트로 보입니다 — 별도 PR 분리 권장.

이 PR(#352)의 목표는 StatusFilterAspect 동작 수정, FilterContextHolder 도입, open-in-view: false 적용, Query Persistence Adapter에 @Transactional(readOnly = true) 부여입니다. 반면 본 파일은 Hibernate ActionQueue의 INSERT/UPDATE flush 순서를 검증하는 학습/문서성 테스트로, AI 요약에서도 "이 테스트는 PR의 필터/트랜잭션 경계 변경과 독립적"이라고 명시하고 있습니다. 리뷰 범위와 변경 의도를 분명히 하기 위해 별도 PR(또는 별도 커밋)로 분리하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java` around
lines 20 - 40, The FlushOrderTest (class FlushOrderTest) is unrelated to this
PR’s scope and should be removed from this change set and submitted separately;
revert or remove the file/changes that add or modify FlushOrderTest from this
branch (or move them to a new branch/PR) so the current PR only contains the
StatusFilterAspect, FilterContextHolder, open-in-view, and Query Persistence
Adapter transaction changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java`:
- Around line 71-137: The tests
update_then_insert_in_code__insert_first_at_flush,
update_then_force_flush_then_insert, and
insert_then_update_in_code__insert_first_at_flush lack assertions and only rely
on manual SQL log inspection; add automated verification by registering a
Hibernate StatementInspector (hibernate.session_factory.statement_inspector) in
the test configuration to capture executed SQL statements during em.flush(),
then assert ordering (e.g. assertThat(indexOf("insert ...
신규유저")).isLessThan(indexOf("update ... 수정된유저"))) for the first and third tests
and assert the two separate flush phases for update_then_force_flush_then_insert
(or alternatively enable hibernate.generate_statistics and use Statistics +
OutputCaptureExtension to capture SQL lines and assert their order); update the
test setup to install this inspector or statistics capture and add the
corresponding assertThat checks in each test method to fail CI when ordering
regresses.
- Around line 20-31: Update the class Javadoc on FlushOrderTest to remove the
absolute claim that "INSERT always runs before UPDATE at flush" and instead
state that INSERT runs before UPDATE only when they are in the same flush
operation; explicitly call out that Test 2 demonstrates the different behavior
when an explicit flush() separates operations (in that case UPDATE is executed
in the first flush and INSERT in the subsequent flush). Adjust the summary
sentence and add a short note describing the three test cases (1: same-flush
UPDATE→INSERT => INSERT first, 2: UPDATE→flush()→INSERT => separated flushes
follow code order, 3: INSERT→UPDATE => baseline) so the Javadoc matches the
tests' intent.
- Around line 50-52: The test relies on sequence-style ID allocation but
currently uses a bare `@GeneratedValue` on the id field in FlushOrderTest, which
behaves differently across dialects; change the annotation to
`@GeneratedValue`(strategy = GenerationType.SEQUENCE) on the private Long id field
to guarantee SEQUENCE allocation during tests, and add a short Javadoc on the
test class or test method explaining that this assertion depends on SEQUENCE
semantics and will not hold on IDENTITY-based databases (e.g., MySQL), so the
behavior is environment-specific.

---

Nitpick comments:
In `@src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java`:
- Around line 20-40: The FlushOrderTest (class FlushOrderTest) is unrelated to
this PR’s scope and should be removed from this change set and submitted
separately; revert or remove the file/changes that add or modify FlushOrderTest
from this branch (or move them to a new branch/PR) so the current PR only
contains the StatusFilterAspect, FilterContextHolder, open-in-view, and Query
Persistence Adapter transaction changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 78539dd0-adf2-4fb5-a567-6846db6416ce

📥 Commits

Reviewing files that changed from the base of the PR and between 68d2fb4 and a2e0d6b.

📒 Files selected for processing (1)
  • src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java

Comment on lines +20 to +31
/**
* flush 시점에 INSERT가 UPDATE보다 항상 먼저 실행되는지 검증하는 테스트.
*
* 로그에서 직접 SQL 순서를 눈으로 확인할 것.
* application-test.yml 또는 test/resources에서
* spring.jpa.show-sql=true 또는 logging.level.org.hibernate.SQL=debug 설정 필요.
*
* [테스트 3종]
* 1. 코드 순서 UPDATE → INSERT : flush 시 INSERT 먼저 (핵심 케이스)
* 2. 코드 순서 UPDATE → flush() → INSERT : 강제 flush로 분리된 케이스
* 3. 코드 순서 INSERT → UPDATE : 자연스러운 순서 기준선
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

클래스 Javadoc 요약이 테스트 2와 모순됩니다.

상단 주석은 "flush 시점에 INSERT가 UPDATE보다 항상 먼저 실행되는지 검증"이라 단언하지만, 테스트 2는 사이에 강제 flush()를 끼워 첫 flush에서 UPDATE만, 두 번째 flush에서 INSERT가 나가는 케이스(즉 UPDATE가 먼저 실행되는 시나리오)입니다. "동일 flush 내"라는 조건을 명시하거나, 테스트 2를 "강제 flush로 분리되면 코드 순서대로 나간다"는 별개 명제로 표현해 주세요.

제안 diff
-/**
- * flush 시점에 INSERT가 UPDATE보다 항상 먼저 실행되는지 검증하는 테스트.
+/**
+ * 동일 flush 내에서 Hibernate ActionQueue가 INSERT를 UPDATE보다 먼저 실행하는지 검증하는 테스트.
+ * (단, 중간에 강제 flush()가 들어가면 각 flush 경계 단위로 분리되어 처리됨 — 테스트 2 참고)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java` around
lines 20 - 31, Update the class Javadoc on FlushOrderTest to remove the absolute
claim that "INSERT always runs before UPDATE at flush" and instead state that
INSERT runs before UPDATE only when they are in the same flush operation;
explicitly call out that Test 2 demonstrates the different behavior when an
explicit flush() separates operations (in that case UPDATE is executed in the
first flush and INSERT in the subsequent flush). Adjust the summary sentence and
add a short note describing the three test cases (1: same-flush UPDATE→INSERT =>
INSERT first, 2: UPDATE→flush()→INSERT => separated flushes follow code order,
3: INSERT→UPDATE => baseline) so the Javadoc matches the tests' intent.

Comment on lines +50 to +52
@Id
@GeneratedValue
private Long id;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 운영/테스트 DB 방언 및 ddl-auto, identity 사용 여부 확인
fd -e yml -e yaml . src/main/resources src/test/resources 2>/dev/null | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'
rg -nP '@GeneratedValue|GenerationType\.' --type=java -C1

Repository: THIP-TextHip/THIP-Server

Length of output: 6227


🏁 Script executed:

#!/bin/bash
# Find and display test configuration files
find src/test/resources src/main/resources -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" 2>/dev/null | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Read the FlushOrderTest to understand what it's testing
cat -n src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java

Repository: THIP-TextHip/THIP-Server

Length of output: 5399


🏁 Script executed:

#!/bin/bash
# Search for test configuration files in all common locations
find src/test/resources -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.properties" \) 2>/dev/null
find . -maxdepth 3 -path "*test*" -name "application*" 2>/dev/null | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check if there's a pom.xml or build.gradle to see test database dependency
if [ -f "pom.xml" ]; then
  echo "=== pom.xml (searching for DB dependencies) ==="
  grep -A2 -B2 "h2\|mysql\|mariadb\|postgresql" pom.xml | head -40
fi
if [ -f "build.gradle" ]; then
  echo "=== build.gradle (searching for DB dependencies) ==="
  grep -A2 -B2 "h2\|mysql\|mariadb\|postgresql" build.gradle | head -40
fi

Repository: THIP-TextHip/THIP-Server

Length of output: 744


🏁 Script executed:

#!/bin/bash
# Search more thoroughly for test configuration
find . -name "*.yml" -o -name "*.yaml" -o -name "*.properties" | grep -E "(test|application)" | head -30

Repository: THIP-TextHip/THIP-Server

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Check src/test/resources directory structure
ls -la src/test/resources/ 2>/dev/null || echo "src/test/resources not found"
find src/test/resources -type f 2>/dev/null | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Check main resources for application configuration
ls -la src/main/resources/ 2>/dev/null || echo "src/main/resources not found"
find src/main/resources -type f 2>/dev/null | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 881


🏁 Script executed:

#!/bin/bash
# Read application.yml if it exists
if [ -f "src/main/resources/application.yml" ]; then
  echo "=== application.yml ==="
  cat -n src/main/resources/application.yml
fi

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Search for test configuration classes or profiles
rg -l "@TestConfiguration|@ActiveProfiles|@DataJpaTest" --type=java src/test | head -10
rg "spring.jpa.database|hibernate.dialect" --type-list --type=yaml --type=xml | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 1242


🏁 Script executed:

#!/bin/bash
# Check for application.yml and related files
find src/main/resources -name "application*" -type f 2>/dev/null

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Search for dialect configuration in the codebase
rg "hibernate.dialect|spring.jpa.database" --type=yaml --type=properties --type=java | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Check if application.yml exists in root or elsewhere
find . -maxdepth 4 -name "application.yml" -o -name "application.yaml" 2>/dev/null | head -10

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Look for all yaml files in the repo
find . -name "*.yml" -o -name "*.yaml" | grep -v ".git" | head -30

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check the structure of the entire repository
ls -la src/
ls -la src/main/

Repository: THIP-TextHip/THIP-Server

Length of output: 499


🏁 Script executed:

#!/bin/bash
# Search for Spring configuration in source code
rg "spring.jpa" --type=java | head -20
rg "DataSource|JpaRepository" --type=java | head -10

Repository: THIP-TextHip/THIP-Server

Length of output: 1730


🏁 Script executed:

#!/bin/bash
# Check for any Spring configuration or properties in Java code
rg "@Configuration|@Bean.*DataSource|@Bean.*EntityManagerFactory" --type=java -A3 | head -40

Repository: THIP-TextHip/THIP-Server

Length of output: 2547


🏁 Script executed:

#!/bin/bash
# Check build.gradle for any test configuration details
head -100 build.gradle

Repository: THIP-TextHip/THIP-Server

Length of output: 2963


🏁 Script executed:

#!/bin/bash
# Search for any profile-specific configuration or application.yml with profiles
find . -name "application-test*" -o -name "application.yml" -o -name "application.yaml" 2>/dev/null | grep -v ".git"

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check if there are any resource files with database or spring.jpa configuration
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.properties" \) 2>/dev/null | xargs grep -l "spring.jpa\|hibernate\|datasource" 2>/dev/null | head -10

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check for any Spring Boot configuration in src/main
cat src/main/java/konkuk/thip/ThipServerApplication.java

Repository: THIP-TextHip/THIP-Server

Length of output: 666


🏁 Script executed:

#!/bin/bash
# Check the complete directory structure to find config files
find . -type f -name "*.yml" -o -name "*.yaml" | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 428


🏁 Script executed:

#!/bin/bash
# Look at JpaRepositoryMethodTest to see if it has similar issues
cat -n src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java | head -80

Repository: THIP-TextHip/THIP-Server

Length of output: 3235


🏁 Script executed:

#!/bin/bash
# Check Hibernate and Spring documentation patterns - check for any explicit configuration in test files
rg "@DataJpaTest|@TestPropertySource|application.properties" --type=java src/test | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 897


🏁 Script executed:

#!/bin/bash
# Check the complete FlushOrderTest to see if it has assertions or is just for log inspection
cat -n src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java | tail -50

Repository: THIP-TextHip/THIP-Server

Length of output: 2026


🏁 Script executed:

#!/bin/bash
# Look at the `@DataJpaTest` documentation behavior - check Spring Boot version
grep -A2 "org.springframework.boot" build.gradle | head -5

Repository: THIP-TextHip/THIP-Server

Length of output: 257


🏁 Script executed:

#!/bin/bash
# Check if H2 is available at test runtime and verify default dialect
rg "H2Dialect|datasource.url" --type=java | head -10

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Verify H2 is being used for `@DataJpaTest` by checking Spring Boot default behavior
# Check if there's any explicit database configuration
rg "spring.datasource|spring.jpa.database|@TestPropertySource" --type=java src/test | head -20

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Look at how Spring Boot 3.5.0 configures `@DataJpaTest` with H2
# Check if there's any additional DataSource or test configuration
rg "TestConfiguration|@Bean|DataSource|datasource" --type=java src/test | head -30

Repository: THIP-TextHip/THIP-Server

Length of output: 520


🏁 Script executed:

#!/bin/bash
# Verify that production code consistently uses IDENTITY and check if there's any mixed usage
rg "@GeneratedValue" --type=java src/main/java | wc -l
rg "@GeneratedValue\(strategy = GenerationType.IDENTITY\)" --type=java src/main/java | wc -l

Repository: THIP-TextHip/THIP-Server

Length of output: 74


🏁 Script executed:

#!/bin/bash
# Verify that H2 is the default test database by checking Spring Data JPA behavior
# H2 with AUTO `@GeneratedValue` uses SEQUENCE, MySQL uses IDENTITY
# Let's confirm what `@DataJpaTest` actually does in Spring Boot 3.5.0 docs context

# Check if there are any other test profiles or database configs we missed
find . -type f \( -name "*.properties" -o -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | xargs grep -l "database\|datasource\|dialect" 2>/dev/null | head -10

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Verify production runs on MySQL and test runs on H2
# Check the migration scripts which suggest what DB schema is expected
head -50 src/main/resources/db/migration/V1__BaseLine.sql | grep -i "auto_increment\|sequence" | head -5

Repository: THIP-TextHip/THIP-Server

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Final verification: Check if there are any comments or documentation about test vs production DB
rg "H2|MySQL|MariaDB|IDENTITY|SEQUENCE" --type=java src/test | grep -i "test\|prod\|mysql\|h2" | head -10

Repository: THIP-TextHip/THIP-Server

Length of output: 50


테스트의 전제를 명시적으로 보장하기 위해 @GeneratedValue 전략을 지정하세요.

현재 bare @GeneratedValue는 데이터베이스 방언에 따라 다르게 동작합니다:

  • H2(테스트 DB): AUTOSEQUENCE → ID는 persist 시 할당되고, INSERT는 flush 시까지 지연
  • MySQL(운영 DB): AUTOIDENTITY → persist 시점에 즉시 INSERT 실행

테스트 주석 "flush 시 INSERT가 먼저"라는 전제는 SEQUENCE 동작에 의존하므로, ActionQueue 순서 검증 의도를 명확히 하려면 strategy = GenerationType.SEQUENCE를 명시하고, 운영 환경이 MySQL/IDENTITY라면 해당 환경에서 이 보장이 적용되지 않음을 Javadoc에 기록하는 것이 좋습니다.

제안 diff
         `@Id`
-        `@GeneratedValue`
+        `@GeneratedValue`(strategy = GenerationType.SEQUENCE)
         private Long id;
📝 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
@Id
@GeneratedValue
private Long id;
`@Id`
`@GeneratedValue`(strategy = GenerationType.SEQUENCE)
private Long id;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java` around
lines 50 - 52, The test relies on sequence-style ID allocation but currently
uses a bare `@GeneratedValue` on the id field in FlushOrderTest, which behaves
differently across dialects; change the annotation to `@GeneratedValue`(strategy =
GenerationType.SEQUENCE) on the private Long id field to guarantee SEQUENCE
allocation during tests, and add a short Javadoc on the test class or test
method explaining that this assertion depends on SEQUENCE semantics and will not
hold on IDENTITY-based databases (e.g., MySQL), so the behavior is
environment-specific.

Comment on lines +71 to +137
@Test
@DisplayName("[코드 순서: UPDATE → INSERT] flush 시 INSERT가 항상 먼저 실행된다")
void update_then_insert_in_code__insert_first_at_flush() {
/**
* 코드 작성 순서는 UPDATE → INSERT 이지만,
* flush 시점에는 Hibernate ActionQueue가 INSERT를 먼저 처리한다.
*
* 예상 SQL 로그:
* insert into test_user (name) values ('신규유저')
* update test_user set name='수정된유저' where id=?
*/

// UPDATE 대상 설정 (코드 상 먼저)
TestUser existing = em.find(TestUser.class, existingId);
existing.setName("수정된유저"); // dirty → flush 시 UPDATE

// INSERT 대상 설정 (코드 상 나중)
em.persist(new TestUser("신규유저"));

em.flush(); // ← 여기서 SQL 로그 확인: INSERT(신규유저) 가 UPDATE(수정된유저) 보다 먼저 나와야 함
}

@Test
@DisplayName("[코드 순서: UPDATE → flush() → INSERT] 강제 flush 이후 INSERT는 별도 flush에서 실행된다")
void update_then_force_flush_then_insert() {
/**
* 강제 flush를 중간에 끼워 넣으면, 그 시점까지 누적된 변경사항만 반영된다.
* INSERT 대상은 강제 flush 이후에 등록되었으므로 다음 flush에서 처리된다.
*
* 예상 SQL 로그:
* [첫 번째 flush] update test_user set name='수정된유저' where id=?
* [두 번째 flush] insert into test_user (name) values ('신규유저')
*/

// UPDATE 대상 설정
TestUser existing = em.find(TestUser.class, existingId);
existing.setName("수정된유저");

em.flush(); // ← 첫 번째 flush: UPDATE(수정된유저) 만 나감

// INSERT 대상 설정 (강제 flush 이후)
em.persist(new TestUser("신규유저"));

em.flush(); // ← 두 번째 flush: INSERT(신규유저) 만 나감
}

@Test
@DisplayName("[코드 순서: INSERT → UPDATE] 자연스러운 코드 순서에서도 INSERT가 먼저 실행된다 (기준선)")
void insert_then_update_in_code__insert_first_at_flush() {
/**
* 코드 작성 순서가 INSERT → UPDATE 인 자연스러운 케이스.
* 테스트 1과 결과가 동일해야 한다.
*
* 예상 SQL 로그:
* insert into test_user (name) values ('신규유저')
* update test_user set name='수정된유저' where id=?
*/

// INSERT 대상 설정 (코드 상 먼저)
em.persist(new TestUser("신규유저"));

// UPDATE 대상 설정 (코드 상 나중)
TestUser existing = em.find(TestUser.class, existingId);
existing.setName("수정된유저");

em.flush(); // ← 여기서 SQL 로그 확인: INSERT(신규유저) 가 UPDATE(수정된유저) 보다 먼저 나와야 함
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

자동 검증 누락 — 테스트가 항상 통과합니다.

세 테스트 모두 assert* 호출이 없고 "SQL 로그를 눈으로 확인"하는 방식이라, ActionQueue 동작이 깨지거나 누군가 코드 순서를 바꿔도 CI는 절대 실패를 잡아내지 못합니다. 사실상 회귀 방지 기능이 없는 문서성 코드입니다. 다음 중 하나로 자동 검증을 추가해 주세요.

  • 권장: Hibernate StatementInspector(hibernate.session_factory.statement_inspector)를 테스트 설정에 등록해 실행 SQL을 캡처하고, assertThat(insertIdx).isLessThan(updateIdx) 형태로 순서를 단언.
  • 또는 @DataJpaTest 로깅에 의존하지 말고, Statistics(hibernate.generate_statistics=true) + 로그 캡처(OutputCaptureExtensionCapturedOutput)로 SQL 라인 순서를 검증.

자동 검증이 어렵다면, 이 클래스는 테스트가 아니라 docs/ 또는 README의 예제로 옮기는 것이 명확합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/konkuk/thip/common/persistence/FlushOrderTest.java` around
lines 71 - 137, The tests update_then_insert_in_code__insert_first_at_flush,
update_then_force_flush_then_insert, and
insert_then_update_in_code__insert_first_at_flush lack assertions and only rely
on manual SQL log inspection; add automated verification by registering a
Hibernate StatementInspector (hibernate.session_factory.statement_inspector) in
the test configuration to capture executed SQL statements during em.flush(),
then assert ordering (e.g. assertThat(indexOf("insert ...
신규유저")).isLessThan(indexOf("update ... 수정된유저"))) for the first and third tests
and assert the two separate flush phases for update_then_force_flush_then_insert
(or alternatively enable hibernate.generate_statistics and use Statistics +
OutputCaptureExtension to capture SQL lines and assert their order); update the
test setup to install this inspector or statistics capture and add the
corresponding assertThat checks in each test method to fail CI when ordering
regresses.

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.

[Refactor] status filter aspect 수정

2 participants