-
Notifications
You must be signed in to change notification settings - Fork 0
[Refactor] StatusFilterAspect 동작 수정 #352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
3ac715c
3123fd4
8e1a94a
6ce4938
8d1e35c
68d2fb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||
| package konkuk.thip.common.aop; | ||||||||
|
|
||||||||
| public class FilterContextHolder { | ||||||||
|
|
||||||||
| public enum FilterMode { | ||||||||
| ACTIVE_ONLY, | ||||||||
| UNFILTERED | ||||||||
| } | ||||||||
|
|
||||||||
| 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(); | ||||||||
| } | ||||||||
|
|
||||||||
|
Comment on lines
+21
to
+24
|
||||||||
| static void clear() { | |
| context.remove(); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,115 +1,73 @@ | ||
| 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; | ||
| 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 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 | ||
| @Order(Ordered.LOWEST_PRECEDENCE) // aspect order 명시 (우선순위 최하위 -> transaction aspect 이후에 동작) | ||
| @Aspect | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| 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 + ")"; | ||
|
Comment on lines
+36
to
44
|
||
|
|
||
| // @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 +76,13 @@ private boolean isFilterEnabled(Session s) { | |
| return s.getEnabledFilter(FILTER_NAME) != null; | ||
| } | ||
|
|
||
| private void enableFilterWith(Session s, List<String> 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"); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
중첩
@Unfiltered호출에서 상태가 유실됩니다.지금 API는
set()후clear()만 가능해서,@Unfiltered메서드 A 안에서 또 다른@Unfiltered메서드 B를 호출하면 B의finally에서clear()한 뒤 A의 남은 구간이 다시ACTIVE_ONLY로 돌아갑니다. 이전 모드를 저장하고finally에서 복원하는 방식으로 바꿔야 합니다.🔧 예시 방향
StatusFilterAspect쪽에서는 around advice에서 이전 값을 받아finally에서restore(previous)하도록 맞추면 됩니다.🤖 Prompt for AI Agents