From 3ac715c2756ab47cb0480d2dc82d84588437d7b0 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 7 Mar 2026 01:46:52 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[refactor]=20BaseEntity=EC=97=90=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=ED=95=9C=20filter=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - statusFilter는 'status = active' 쿼리로 고정 - 굳이 in 절 필요없다고 판단 --- .../java/konkuk/thip/common/entity/BaseJpaEntity.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java b/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java index 9f33e2410..bf2b1609f 100644 --- a/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java +++ b/src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java @@ -9,7 +9,6 @@ import lombok.Getter; import org.hibernate.annotations.Filter; import org.hibernate.annotations.FilterDef; -import org.hibernate.annotations.ParamDef; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -19,14 +18,8 @@ @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -@FilterDef( - name = "statusFilter", - parameters = @ParamDef(name = "statuses", type = String.class) -) -@Filter( - name = "statusFilter", - condition = "status in (:statuses)" -) +@FilterDef(name = "statusFilter") +@Filter(name = "statusFilter", condition = "status = 'ACTIVE'") public abstract class BaseJpaEntity { @CreatedDate From 3123fd48b9495b97f39b4685394d09ce42cfc3ff Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 7 Mar 2026 01:48:48 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[feat]=20filter=20mode=20=EB=AA=85=EC=8B=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20ThreadLocal=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - active_only, unfiltered(필터 적용 X == status 조건 제거) 2가지 mode - active_only mode가 default --- .../thip/common/aop/FilterContextHolder.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/aop/FilterContextHolder.java diff --git a/src/main/java/konkuk/thip/common/aop/FilterContextHolder.java b/src/main/java/konkuk/thip/common/aop/FilterContextHolder.java new file mode 100644 index 000000000..6772f2559 --- /dev/null +++ b/src/main/java/konkuk/thip/common/aop/FilterContextHolder.java @@ -0,0 +1,26 @@ +package konkuk.thip.common.aop; + +public class FilterContextHolder { + + public enum FilterMode { + ACTIVE_ONLY, + UNFILTERED + } + + private static final ThreadLocal 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(); + } + + private FilterContextHolder() {} +} From 8e1a94a11592a70ca757cd9b55220dd4336bc539 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 7 Mar 2026 01:57:05 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[refactor]=20StatusFilterAspect=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 트랜잭션 경계에서 status filter 자동으로 동작하도록 설정 - @Unfiltered 동작 시 ThreadLocal mode 변경 & 필터 동작 X - 사용하지 않는 @IncludeInactive 제거 --- .../persistence/IncludeInactive.java | 11 -- .../thip/common/aop/StatusFilterAspect.java | 111 +++++------------- .../thip/common/exception/code/ErrorCode.java | 1 - 3 files changed, 30 insertions(+), 93 deletions(-) delete mode 100644 src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java diff --git a/src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java b/src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java deleted file mode 100644 index d4faebbda..000000000 --- a/src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java +++ /dev/null @@ -1,11 +0,0 @@ -package konkuk.thip.common.annotation.persistence; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.METHOD, ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface IncludeInactive { -} diff --git a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java index 0c00fab6e..11d561fbe 100644 --- a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java +++ b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java @@ -1,8 +1,6 @@ package konkuk.thip.common.aop; import jakarta.persistence.EntityManager; -import konkuk.thip.common.entity.StatusType; -import konkuk.thip.common.exception.InternalServerException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; @@ -10,11 +8,9 @@ import org.aspectj.lang.annotation.Aspect; import org.hibernate.Session; import org.springframework.stereotype.Component; -import org.springframework.transaction.support.TransactionSynchronizationManager; -import java.util.List; - -import static konkuk.thip.common.exception.code.ErrorCode.PERSISTENCE_TRANSACTION_REQUIRED; +import static konkuk.thip.common.aop.FilterContextHolder.FilterMode.ACTIVE_ONLY; +import static konkuk.thip.common.aop.FilterContextHolder.FilterMode.UNFILTERED; @Slf4j @Aspect @@ -24,92 +20,51 @@ public class StatusFilterAspect { private final EntityManager em; - /** - * Hibernate Session은 thread-not-safe 하므로 반드시 트랜잭션 경계 내에서만 사용해야함 - * 현재 스레드에 바인딩된 EntityManager를 통해 세션을 획득하도록 강제 - */ - private Session currentTxSession() { - if (!TransactionSynchronizationManager.isActualTransactionActive()) { - throw new InternalServerException(PERSISTENCE_TRANSACTION_REQUIRED); - } - return session(); - } - - private Session session() { - return em.unwrap(Session.class); // 현재 스레드의 em에서 Hibernate 세션 얻기 - } - private static final String FILTER_NAME = "statusFilter"; - private static final String PARAM_STATUSES = "statuses"; private static final String ANN_TX = "org.springframework.transaction.annotation.Transactional"; - private static final String ANN_INCLUDE_INACTIVE = "konkuk.thip.common.annotation.persistence.IncludeInactive"; private static final String ANN_UNFILTERED = "konkuk.thip.common.annotation.persistence.Unfiltered"; - // 기본: ACTIVE만 (트랜잭션 경계 진입 시) - // 1) @Transactional 이고 - // 2) @IncludeInactive, @Unfiltered 가 붙어있지 않은 경우에만 적용 + /** + * @Unfiltered: @Transactional 없이도 동작. + * ThreadLocal에 UNFILTERED 의도를 기록하고 반환. + * 실제 Session 조작은 트랜잭션 경계(PCUT_TX_DEFAULT)에서 수행. + */ + 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 + ")"; - // @IncludeInactive: 트랜잭션 컨텍스트가 보장된 경우에만 동작 - private static final String PCUT_INCLUDE_INACTIVE = - "@annotation(" + ANN_INCLUDE_INACTIVE + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; - - // @Unfiltered: 트랜잭션 컨텍스트가 보장된 경우에만 동작 - private static final String PCUT_UNFILTERED = - "@annotation(" + ANN_UNFILTERED + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; - - // 기본: ACTIVE만 - @Around(PCUT_TX_DEFAULT) - public Object enableActiveByDefault(ProceedingJoinPoint pjp) throws Throwable { - var s = currentTxSession(); - var wasEnabled = isFilterEnabled(s); - if (!wasEnabled) { - enableFilterWith(s, List.of(StatusType.ACTIVE.name())); - } + @Around(PCUT_UNFILTERED) + public Object unfiltered(ProceedingJoinPoint pjp) throws Throwable { + FilterContextHolder.FilterMode prev = FilterContextHolder.get(); + FilterContextHolder.set(UNFILTERED); try { return pjp.proceed(); } finally { - if (!wasEnabled) { - disableFilter(s); - } + FilterContextHolder.set(prev); } } - // Include Inactive: ACTIVE, INACTIVE 모두 + 종료 시 active-only 로 복귀 - @Around(PCUT_INCLUDE_INACTIVE) - public Object includeInactive(ProceedingJoinPoint pjp) throws Throwable { - var s = currentTxSession(); - var prevEnabled = isFilterEnabled(s); - - enableFilterWith(s, List.of(StatusType.ACTIVE.name(), StatusType.INACTIVE.name())); + @Around(PCUT_TX_DEFAULT) + public Object enableActiveByDefault(ProceedingJoinPoint pjp) throws Throwable { + Session s = em.unwrap(Session.class); + boolean wasEnabled = isFilterEnabled(s); - try { - return pjp.proceed(); - } finally { - restoreToActive(s); - if (!prevEnabled) { - disableFilter(s); - } + if (FilterContextHolder.get() == ACTIVE_ONLY && !wasEnabled) { + enableFilter(s); } - } - // Unfiltered: 필터 해제 + 종료 시 active-only 로 복귀 - @Around(PCUT_UNFILTERED) - public Object unfiltered(ProceedingJoinPoint pjp) throws Throwable { - var s = currentTxSession(); - var wasEnabled = isFilterEnabled(s); - if (wasEnabled) { - disableFilter(s); - } try { return pjp.proceed(); } finally { - if (wasEnabled) { - restoreToActive(s); + if (!wasEnabled) { + disableFilter(s); } } } @@ -118,19 +73,13 @@ private boolean isFilterEnabled(Session s) { return s.getEnabledFilter(FILTER_NAME) != null; } - private void enableFilterWith(Session s, List statuses) { - s.enableFilter(FILTER_NAME).setParameterList(PARAM_STATUSES, statuses); - log.debug("statusFilter -> ENABLED [statuses={}]", statuses); - } - - private void restoreToActive(Session s) { - var restored = List.of(StatusType.ACTIVE.name()); - s.enableFilter(FILTER_NAME).setParameterList(PARAM_STATUSES, restored); - log.debug("statusFilter -> RESTORED [statuses={}]", restored); + private void enableFilter(Session s) { + s.enableFilter(FILTER_NAME); + log.debug("statusFilter -> ENABLED [ACTIVE only]"); } private void disableFilter(Session s) { s.disableFilter(FILTER_NAME); log.debug("statusFilter -> DISABLED"); } -} +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 38af91a62..ac0f6226d 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -30,7 +30,6 @@ public enum ErrorCode implements ResponseCode { AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."), WEB_DOMAIN_ORIGIN_EMPTY(HttpStatus.INTERNAL_SERVER_ERROR, 50102, "허용된 웹 도메인 설정이 비어있습니다."), - PERSISTENCE_TRANSACTION_REQUIRED(HttpStatus.INTERNAL_SERVER_ERROR, 50110, "@Transactional 컨텍스트가 필요합니다. 트랜잭션 범위 내에서만 사용할 수 있습니다."), RESOURCE_LOCKED(HttpStatus.LOCKED, 50200, "자원이 잠겨 있어 요청을 처리할 수 없습니다."), /* 60000부터 비즈니스 예외 */ From 6ce4938596e4b2d4e4bfe660c6fbbd25a119a052 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 7 Mar 2026 02:04:47 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[refactor]=20QueryPersistenceAdapter=20?= =?UTF-8?q?=EC=97=90=20read=20only=20transactional=20=EB=AA=85=EC=8B=9C=20?= =?UTF-8?q?(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - status filter는 트랜잭션이 열려있어야 동작 가능 - 조회 시 사용하는 영속성 adapter 클래스에 read only 트랜잭션 명시 - QueryPersistenceAdapter 하위의 QueryDSL, spring data jpa repository 인터페이스를 활용하는 DB access 코드 모두 status filter 동작 가능 - 트랜잭션 명시로 인해 DB에 추가적인 command가 날라가지만, 이는 감수할만한 트레이드 오프라고 판단 --- .../adapter/out/persistence/BookQueryPersistenceAdapter.java | 2 ++ .../out/persistence/CommentLikeQueryPersistenceAdapter.java | 2 ++ .../adapter/out/persistence/CommentQueryPersistenceAdapter.java | 2 ++ .../adapter/out/persistence/FeedQueryPersistenceAdapter.java | 2 ++ .../out/persistence/NotificationQueryPersistenceAdapter.java | 2 ++ .../out/persistence/PostLikeQueryPersistenceAdapter.java | 2 ++ .../adapter/out/persistence/PostQueryPersistenceAdapter.java | 2 ++ .../out/persistence/RecentSearchQueryPersistenceAdapter.java | 2 ++ .../out/persistence/RoomParticipantQueryPersistenceAdapter.java | 2 ++ .../adapter/out/persistence/RoomQueryPersistenceAdapter.java | 2 ++ .../out/persistence/AttendanceCheckQueryPersistenceAdapter.java | 2 ++ .../adapter/out/persistence/RecordQueryPersistenceAdapter.java | 2 ++ .../adapter/out/persistence/VoteQueryPersistenceAdapter.java | 2 ++ .../out/persistence/FollowingQueryPersistenceAdapter.java | 2 ++ .../adapter/out/persistence/UserQueryPersistenceAdapter.java | 2 ++ 15 files changed, 30 insertions(+) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java index aa9b2bd6c..3870eca70 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java @@ -9,12 +9,14 @@ import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; import java.util.Set; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class BookQueryPersistenceAdapter implements BookQueryPort { diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java index fb4ed6b36..e6e14aca5 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java @@ -6,10 +6,12 @@ import konkuk.thip.comment.application.port.out.CommentLikeQueryPort; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.util.Set; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class CommentLikeQueryPersistenceAdapter implements CommentLikeQueryPort { diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java index 3c10850db..530ed5a46 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java @@ -7,6 +7,7 @@ import konkuk.thip.common.util.Cursor; import konkuk.thip.common.util.CursorBasedList; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; @@ -14,6 +15,7 @@ import java.util.Map; import java.util.Set; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class CommentQueryPersistenceAdapter implements CommentQueryPort { diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java index a0ffbe84a..934c48d0a 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java @@ -10,12 +10,14 @@ import konkuk.thip.feed.application.port.out.dto.FeedQueryDto; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; import java.util.Set; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class FeedQueryPersistenceAdapter implements FeedQueryPort { diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java index ab90570a3..73a82474c 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java @@ -8,10 +8,12 @@ import konkuk.thip.notification.application.port.out.NotificationQueryPort; import konkuk.thip.notification.application.port.out.dto.NotificationQueryDto; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.util.List; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class NotificationQueryPersistenceAdapter implements NotificationQueryPort { diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java index 51c10edbe..e6509b215 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java @@ -3,10 +3,12 @@ import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.post.application.port.out.PostLikeQueryPort; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.util.Set; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class PostLikeQueryPersistenceAdapter implements PostLikeQueryPort { diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java index 966a04f04..520a41c96 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java @@ -4,8 +4,10 @@ import konkuk.thip.post.application.port.out.PostQueryPort; import konkuk.thip.post.application.port.out.dto.PostQueryDto; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class PostQueryPersistenceAdapter implements PostQueryPort { diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java index 780dadb30..1401eb947 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java @@ -6,11 +6,13 @@ import konkuk.thip.recentSearch.application.port.out.RecentSearchQueryPort; import konkuk.thip.recentSearch.domain.RecentSearch; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class RecentSearchQueryPersistenceAdapter implements RecentSearchQueryPort { diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java index 31a2f6579..445282a23 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantQueryPersistenceAdapter.java @@ -3,8 +3,10 @@ import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.application.port.out.RoomParticipantQueryPort; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class RoomParticipantQueryPersistenceAdapter implements RoomParticipantQueryPort { diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java index b706a517b..ff835ff54 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java @@ -13,12 +13,14 @@ import konkuk.thip.room.domain.value.Category; import konkuk.thip.room.domain.Room; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class RoomQueryPersistenceAdapter implements RoomQueryPort { diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java index 1b1c07e60..c5b48e014 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java @@ -7,6 +7,7 @@ import konkuk.thip.roompost.application.port.out.AttendanceCheckQueryPort; import konkuk.thip.roompost.application.port.out.dto.AttendanceCheckQueryDto; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; @@ -14,6 +15,7 @@ import static konkuk.thip.common.entity.StatusType.ACTIVE; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class AttendanceCheckQueryPersistenceAdapter implements AttendanceCheckQueryPort { diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java index 4c97976aa..cc7b87b65 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordQueryPersistenceAdapter.java @@ -8,10 +8,12 @@ import konkuk.thip.roompost.application.port.out.dto.RoomPostQueryDto; import konkuk.thip.roompost.domain.Record; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.util.List; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class RecordQueryPersistenceAdapter implements RecordQueryPort { diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java index 97ab89043..225ac29ca 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java @@ -6,6 +6,7 @@ import konkuk.thip.roompost.application.port.out.VoteQueryPort; import konkuk.thip.roompost.application.port.out.dto.VoteItemQueryDto; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.util.List; @@ -16,6 +17,7 @@ import static java.util.stream.Collectors.groupingBy; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class VoteQueryPersistenceAdapter implements VoteQueryPort { diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java index bff694526..ee62bf86e 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java @@ -9,11 +9,13 @@ import konkuk.thip.user.application.port.out.dto.UserQueryDto; import konkuk.thip.user.domain.value.Alias; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class FollowingQueryPersistenceAdapter implements FollowingQueryPort { diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java index 8e4535ece..aa9c707b2 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java @@ -10,12 +10,14 @@ import konkuk.thip.user.application.port.out.dto.UserQueryDto; import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; import java.util.Set; +@Transactional(readOnly = true) @Repository @RequiredArgsConstructor public class UserQueryPersistenceAdapter implements UserQueryPort { From 8d1e35cba15cd5dc79c3887b1c9343dcfd16e570 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 8 Mar 2026 16:46:03 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[test]=20=EC=88=98=EC=A0=95=ED=95=9C=20filt?= =?UTF-8?q?er=20=EB=8F=99=EC=9E=91=20=EA=B2=80=EC=A6=9D=20(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/persistence/StatusFilterTest.java | 220 +++++------------- .../thip/config/StatusFilterTestConfig.java | 129 +++------- .../thip/config/TestUserJpaRepository.java | 10 + 3 files changed, 103 insertions(+), 256 deletions(-) create mode 100644 src/test/java/konkuk/thip/config/TestUserJpaRepository.java diff --git a/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java index 8277ec669..9f7dce58d 100644 --- a/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java +++ b/src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java @@ -1,231 +1,137 @@ package konkuk.thip.common.persistence; -import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; -import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; -import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.config.StatusFilterTestConfig; +import konkuk.thip.config.TestUserJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +/** + * Hibernate Filter (statusFilter) 동작 검증 테스트. + * + * 검증 범위: + * 1. findById 는 PK 조회로 Hibernate filter 가 적용되지 않는다. + * 2. @Transactional(readOnly=true) 가 명시된 adapter 경계에서 filter 가 활성화된다. + * 3. adapter 내부 구현(JPA repo)과 무관하게 filter 가 동작한다. + * 4. service 메서드에 @Unfiltered 가 있으면 @Transactional 유무와 관계없이 filter 가 비활성화된다. + */ @SpringBootTest @ActiveProfiles("test") -@Transactional public class StatusFilterTest { - @Autowired private UserJpaRepository userJpaRepository; - @Autowired private BookJpaRepository bookJpaRepository; - @Autowired private SavedBookJpaRepository savedBookJpaRepository; - - @Autowired private StatusFilterTestConfig.TestUserIdFindService testUserIdFindService; - @Autowired private StatusFilterTestConfig.TestUserQueryService testUserQueryService; - @Autowired private StatusFilterTestConfig.TestUserJpqlService testUserJpqlService; - @Autowired private StatusFilterTestConfig.TestUserQuerydslService testUserQuerydslService; + @Autowired private TestUserJpaRepository testUserJpaRepository; + @Autowired private StatusFilterTestConfig.TestUserPersistenceAdapter testUserPersistenceAdapter; + @Autowired private StatusFilterTestConfig.TestUnfilteredService testUnfilteredService; @Autowired private JdbcTemplate jdbcTemplate; - private void saveActiveUser(int count) { - for (int i = 1; i <= count; i++) { - UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "activeUser" + i); - userJpaRepository.save(user); - } + @AfterEach + void cleanup() { + jdbcTemplate.update("DELETE FROM users"); } - private void saveInactiveUser(int count) { - for (int i = 1; i <= count; i++) { - UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "inactiveUser" + i); - userJpaRepository.save(user); - - jdbcTemplate.update( - "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", - user.getUserId() - ); - } + private UserJpaEntity saveActiveUser(String nickname) { + return testUserJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, nickname)); } - @Test - @DisplayName("spring data jpa의 기본 findById 메서드는 PK를 기준으로만 조회하므로 status 필터링이 적용되지 않는다.") - void default_find_by_id_method_does_not_execute_filtering() throws Exception { - //given - UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); - UserJpaEntity inactiveUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); - jdbcTemplate.update( - "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", - inactiveUser.getUserId() - ); - - //when - Optional findActiveUser = testUserIdFindService.defaultFindById(activeUser.getUserId()); - Optional findInactiveUser = testUserIdFindService.defaultFindById(inactiveUser.getUserId()); - - //then - assertThat(findActiveUser).isPresent(); - assertThat(findInactiveUser).isPresent(); // status 필터링이 적용되지 않아서 INACTIVE 엔티티도 조회됨 + private UserJpaEntity saveInactiveUser(String nickname) { + UserJpaEntity user = testUserJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, nickname)); + jdbcTemplate.update("UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", user.getUserId()); + return user; } @Test - @DisplayName("jpa repository에 정의한 custom 메서드는 status 필터링이 적용된다.") - void custom_find_active_by_id_method_does_execute_filtering() throws Exception { + @DisplayName("findById 는 PK 조회이므로 Hibernate filter 가 적용되지 않아 INACTIVE 엔티티도 조회된다.") + void findById_bypasses_hibernate_filter() { //given - UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); - UserJpaEntity inactiveUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); - jdbcTemplate.update( - "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", - inactiveUser.getUserId() - ); + UserJpaEntity activeUser = saveActiveUser("activeUser"); + UserJpaEntity inactiveUser = saveInactiveUser("inactiveUser"); //when - Optional findActiveUser = testUserIdFindService.customFindById(activeUser.getUserId()); - Optional findInactiveUser = testUserIdFindService.customFindById(inactiveUser.getUserId()); + Optional findActive = testUserJpaRepository.findById(activeUser.getUserId()); + Optional findInactive = testUserJpaRepository.findById(inactiveUser.getUserId()); //then - assertThat(findActiveUser).isPresent(); - assertThat(findInactiveUser).isNotPresent(); // status 필터링이 적용되어 INACTIVE 엔티티는 조회되지 않음 + assertThat(findActive).isPresent(); + assertThat(findInactive).isPresent(); // PK 조회라 filter 미적용 → INACTIVE 도 조회됨 } @Test - @DisplayName("[jpa 쿼리 메서드] active 상태인 엔티티만 조회하는 것이 기본 동작이다.") - void jpa_query_method_default_find_active_entities() throws Exception { + @DisplayName("adapter 의 @Transactional(readOnly=true) 경계에서 filter 가 활성화되어 ACTIVE 엔티티만 조회된다.") + void adapter_transactional_applies_filter() { //given - saveActiveUser(3); - saveInactiveUser(2); + saveActiveUser("activeUser1"); + saveActiveUser("activeUser2"); + saveInactiveUser("inactiveUser1"); //when - List userJpaEntities = testUserQueryService.findAllActiveOnly(); + List users = testUserPersistenceAdapter.findAll(); //then - assertThat(userJpaEntities).hasSize(3) + assertThat(users).hasSize(2) .extracting(UserJpaEntity::getNickname) - .containsExactlyInAnyOrder( - "activeUser1", "activeUser2", "activeUser3" - ); + .containsExactlyInAnyOrder("activeUser1", "activeUser2"); } @Test - @DisplayName("[jpa 쿼리 메서드] IncludeInactive 어노테이션이 붙은 메서드는 active, inactive 상태인 모든 엔티티를 조회한다.") - void jpa_query_method_specific_find_active_and_inactive_entities() throws Exception { + @DisplayName("adapter 내부에서 JPQL 기반 메서드를 호출해도 filter 가 적용되어 INACTIVE 엔티티는 조회되지 않는다.") + void adapter_jpql_method_applies_filter() { //given - saveActiveUser(3); - saveInactiveUser(2); + UserJpaEntity activeUser = saveActiveUser("activeUser"); + UserJpaEntity inactiveUser = saveInactiveUser("inactiveUser"); //when - List userJpaEntities = testUserQueryService.findAllIncludingInactive(); + Optional findActive = testUserPersistenceAdapter.findByUserId(activeUser.getUserId()); + Optional findInactive = testUserPersistenceAdapter.findByUserId(inactiveUser.getUserId()); //then - assertThat(userJpaEntities).hasSize(5) - .extracting(UserJpaEntity::getNickname) - .containsExactlyInAnyOrder( - "activeUser1", "activeUser2", "activeUser3", "inactiveUser1", "inactiveUser2" - ); + assertThat(findActive).isPresent(); + assertThat(findInactive).isNotPresent(); // filter 적용 → INACTIVE 조회 안 됨 } @Test - @DisplayName("[jpql] active 상태인 엔티티만 조회하는 것이 기본 동작이다.") - void jpql_default_find_active_entities() throws Exception { + @DisplayName("service 메서드에 @Unfiltered + @Transactional 이 함께 있으면 filter 가 비활성화되어 모든 엔티티가 조회된다.") + void unfiltered_with_transactional_disables_filter() { //given - saveActiveUser(3); - saveInactiveUser(2); + saveActiveUser("activeUser1"); + saveActiveUser("activeUser2"); + saveInactiveUser("inactiveUser1"); //when - List userJpaEntities = testUserJpqlService.findAllByJpql(); + List users = testUnfilteredService.findAllWithTx(); //then - assertThat(userJpaEntities).hasSize(3) + assertThat(users).hasSize(3) .extracting(UserJpaEntity::getNickname) - .containsExactlyInAnyOrder( - "activeUser1", "activeUser2", "activeUser3" - ); + .containsExactlyInAnyOrder("activeUser1", "activeUser2", "inactiveUser1"); } @Test - @DisplayName("[jpql] IncludeInactive 어노테이션이 붙은 메서드는 active, inactive 상태인 모든 엔티티를 조회한다.") - void jpql_specific_find_active_and_inactive_entities() throws Exception { + @DisplayName("service 메서드에 @Transactional 없이 @Unfiltered 만 있어도 filter 가 비활성화되어 모든 엔티티가 조회된다.") + void unfiltered_without_transactional_disables_filter() { //given - saveActiveUser(3); - saveInactiveUser(2); + saveActiveUser("activeUser1"); + saveActiveUser("activeUser2"); + saveInactiveUser("inactiveUser1"); //when - List userJpaEntities = testUserJpqlService.findAllIncludingInactiveByJpql(); + List users = testUnfilteredService.findAllWithoutTx(); //then - assertThat(userJpaEntities).hasSize(5) + assertThat(users).hasSize(3) .extracting(UserJpaEntity::getNickname) - .containsExactlyInAnyOrder( - "activeUser1", "activeUser2", "activeUser3", "inactiveUser1", "inactiveUser2" - ); - } - - @Test - @DisplayName("[querydsl] active 상태인 엔티티만 조회하는 것이 기본 동작이다.") - void query_dsl_default_find_active_entities() throws Exception { - //given - saveActiveUser(3); - saveInactiveUser(2); - - //when - List userJpaEntities = testUserQuerydslService.findAllByQuerydsl(); - - //then - assertThat(userJpaEntities).hasSize(3) - .extracting(UserJpaEntity::getNickname) - .containsExactlyInAnyOrder( - "activeUser1", "activeUser2", "activeUser3" - ); - } - - @Test - @DisplayName("[querydsl 쿼리 메서드] IncludeInactive 어노테이션이 붙은 메서드는 active, inactive 상태인 모든 엔티티를 조회한다.") - void query_dsl_specific_find_active_and_inactive_entities() throws Exception { - //given - saveActiveUser(3); - saveInactiveUser(2); - - //when - List userJpaEntities = testUserQuerydslService.findAllIncludingInactiveByQuerydsl(); - - //then - assertThat(userJpaEntities).hasSize(5) - .extracting(UserJpaEntity::getNickname) - .containsExactlyInAnyOrder( - "activeUser1", "activeUser2", "activeUser3", "inactiveUser1", "inactiveUser2" - ); - } - - @Test - @DisplayName("[join 테스트] 루트=User + SavedBook ON-조인 시: 기본은 ACTIVE만, @IncludeInactive 적용 시 ACTIVE+INACTIVE 모두 집계한다.") - void join_filter_propagation_on_user() throws Exception { - //given - UserJpaEntity activeUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); - UserJpaEntity inactiveUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); - jdbcTemplate.update( - "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", - inactiveUser.getUserId() - ); - - BookJpaEntity bookJpaEntity = bookJpaRepository.save(TestEntityFactory.createBook()); - - savedBookJpaRepository.save(TestEntityFactory.createSavedBook(activeUser, bookJpaEntity)); - savedBookJpaRepository.save(TestEntityFactory.createSavedBook(inactiveUser, bookJpaEntity)); - - //when - long defCount = testUserQuerydslService.countSaversByBook(bookJpaEntity.getBookId()); - long incCount = testUserQuerydslService.countSaversByBookIncludingInactive(bookJpaEntity.getBookId()); - - //then - assertThat(defCount).isEqualTo(1); // active user만 카운트 - assertThat(incCount).isEqualTo(2); // active + inactive user 모두 카운트 + .containsExactlyInAnyOrder("activeUser1", "activeUser2", "inactiveUser1"); } } diff --git a/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java b/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java index 7341c8e26..7f1a6f9c5 100644 --- a/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java +++ b/src/test/java/konkuk/thip/config/StatusFilterTestConfig.java @@ -1,16 +1,12 @@ package konkuk.thip.config; -import com.querydsl.jpa.impl.JPAQueryFactory; -import jakarta.persistence.EntityManager; -import konkuk.thip.book.adapter.out.jpa.QSavedBookJpaEntity; -import konkuk.thip.common.annotation.persistence.IncludeInactive; -import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; +import konkuk.thip.common.annotation.persistence.Unfiltered; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; import java.util.List; import java.util.Optional; @@ -18,113 +14,48 @@ @Configuration public class StatusFilterTestConfig { - // jpa repository PK 조회 메서드 테스트 - @Component - @RequiredArgsConstructor - public static class TestUserIdFindService { - - private final UserJpaRepository userJpaRepository; - - /** jpa repository 기본 findById 메서드 */ - @Transactional(readOnly = true) - public Optional defaultFindById(Long userId) { - return userJpaRepository.findById(userId); - } - - /** jpa repository custom 메서드 */ - @Transactional(readOnly = true) - public Optional customFindById(Long userId) { - return userJpaRepository.findByUserId(userId); - } - } - - // jpa query 메서드 - @Component + /** + * 테스트용 영속성 Adapter. + * 실제 *QueryPersistenceAdapter 와 동일하게 class-level @Transactional(readOnly=true) 를 가진다. + * adapter 경계에서 filter 가 활성화되므로, 내부 구현(JPA repo / QueryDSL)과 무관하게 filter 가 적용된다. + */ + @Transactional(readOnly = true) + @Repository @RequiredArgsConstructor - public static class TestUserQueryService { - - private final UserJpaRepository userJpaRepository; - - /** 기본: ACTIVE만 (Aspect가 트랜잭션 경계에서 statusFilter를 ACTIVE로 enable) */ - @Transactional(readOnly = true) - public List findAllActiveOnly() { - return userJpaRepository.findAll(); - } - - /** IncludeInactive: ACTIVE + INACTIVE 모두 */ - @IncludeInactive - @Transactional(readOnly = true) - public List findAllIncludingInactive() { - return userJpaRepository.findAll(); - } - } + public static class TestUserPersistenceAdapter { - // jpql - @Component - @RequiredArgsConstructor - public static class TestUserJpqlService { - private final EntityManager em; + private final TestUserJpaRepository testUserJpaRepository; - /** 기본: ACTIVE만 */ - @Transactional(readOnly = true) - public List findAllByJpql() { - return em.createQuery( - "select u from UserJpaEntity u", UserJpaEntity.class - ).getResultList(); + public Optional findByUserId(Long userId) { + return testUserJpaRepository.findByUserId(userId); } - /** IncludeInactive: ACTIVE + INACTIVE */ - @IncludeInactive - @Transactional(readOnly = true) - public List findAllIncludingInactiveByJpql() { - return em.createQuery( - "select u from UserJpaEntity u", UserJpaEntity.class - ).getResultList(); + public List findAll() { + return testUserJpaRepository.findAll(); } } - // querydsl + /** + * @Unfiltered 동작 검증용 서비스. + * service 메서드에서 @Transactional 유무와 관계없이 filter 가 비활성화됨을 검증한다. + */ @Component @RequiredArgsConstructor - public static class TestUserQuerydslService { - private final JPAQueryFactory qf; - - /** 기본: ACTIVE만 */ - @Transactional(readOnly = true) - public List findAllByQuerydsl() { - QUserJpaEntity u = QUserJpaEntity.userJpaEntity; - return qf.selectFrom(u).fetch(); - } + public static class TestUnfilteredService { - /** IncludeInactive: ACTIVE + INACTIVE */ - @IncludeInactive - @Transactional(readOnly = true) - public List findAllIncludingInactiveByQuerydsl() { - QUserJpaEntity u = QUserJpaEntity.userJpaEntity; - return qf.selectFrom(u).fetch(); - } + private final TestUserPersistenceAdapter testUserPersistenceAdapter; + /** @Unfiltered + @Transactional: filter 비활성화 */ + @Unfiltered @Transactional(readOnly = true) - public long countSaversByBook(Long bookId) { - QSavedBookJpaEntity sb = QSavedBookJpaEntity.savedBookJpaEntity; - QUserJpaEntity u = QUserJpaEntity.userJpaEntity; - return qf.select(u.userId.countDistinct()) - .from(u) - .join(sb).on(sb.userJpaEntity.eq(u)) - .where(sb.bookJpaEntity.bookId.eq(bookId)) - .fetchOne(); + public List findAllWithTx() { + return testUserPersistenceAdapter.findAll(); } - @IncludeInactive - @Transactional(readOnly = true) - public long countSaversByBookIncludingInactive(Long bookId) { - QSavedBookJpaEntity sb = QSavedBookJpaEntity.savedBookJpaEntity; - QUserJpaEntity u = QUserJpaEntity.userJpaEntity; - return qf.select(u.userId.countDistinct()) - .from(u) - .join(sb).on(sb.userJpaEntity.eq(u)) - .where(sb.bookJpaEntity.bookId.eq(bookId)) - .fetchOne(); + /** @Unfiltered only (no @Transactional): filter 비활성화 */ + @Unfiltered + public List findAllWithoutTx() { + return testUserPersistenceAdapter.findAll(); } } } diff --git a/src/test/java/konkuk/thip/config/TestUserJpaRepository.java b/src/test/java/konkuk/thip/config/TestUserJpaRepository.java new file mode 100644 index 000000000..a46c3ef4b --- /dev/null +++ b/src/test/java/konkuk/thip/config/TestUserJpaRepository.java @@ -0,0 +1,10 @@ +package konkuk.thip.config; + +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TestUserJpaRepository extends JpaRepository { + Optional findByUserId(Long userId); +} From 68d2fb44b99f289ec3ab187de30d6fa6cfe9188f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 8 Mar 2026 16:47:36 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[chore]=20@Transactional=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=EC=8B=9C,=20TransactionInterceptor=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=ED=9B=84,=20StatusFilterAspect=20=EB=8F=99=EC=9E=91=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=9C=EC=84=9C=20=EA=B0=95=EC=A0=9C=20?= =?UTF-8?q?(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TransactionInterceptor 동작으로 Hibernate Session이 열려야, StatusFilterAspect 에서 Session에 Hibernate Filter 활성화 가능 --- .../java/konkuk/thip/common/aop/StatusFilterAspect.java | 3 +++ src/main/java/konkuk/thip/config/AppConfig.java | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java index 11d561fbe..f739a0403 100644 --- a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java +++ b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java @@ -7,12 +7,15 @@ import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.hibernate.Session; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import static konkuk.thip.common.aop.FilterContextHolder.FilterMode.ACTIVE_ONLY; import static konkuk.thip.common.aop.FilterContextHolder.FilterMode.UNFILTERED; @Slf4j +@Order(Ordered.LOWEST_PRECEDENCE) // aspect order 명시 (우선순위 최하위 -> transaction aspect 이후에 동작) @Aspect @Component @RequiredArgsConstructor diff --git a/src/main/java/konkuk/thip/config/AppConfig.java b/src/main/java/konkuk/thip/config/AppConfig.java index 06f72663b..567a01861 100644 --- a/src/main/java/konkuk/thip/config/AppConfig.java +++ b/src/main/java/konkuk/thip/config/AppConfig.java @@ -1,4 +1,10 @@ package konkuk.thip.config; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE - 1) public class AppConfig { }