Spring AOP와 Cache 실행 순서로 인한 문제 해결기

DeadWhale·2025년 1월 30일
0

Spring AOP와 Cache 실행 순서로 인한 문제 해결기

Spring AOP와 Cache 기능을 함께 사용하면서 발생한 실행 순서 문제와 그 해결 과정을 공유합니다.

목차

문제 발견의 여정

처음 발견한 이상 현상

비속어 필터 API를 개발하면서 차단(BLOCK)된 클라이언트와 폐기(DISCARD)된 클라이언트의 테스트를 진행하고 있었습니다. 테스트 시나리오는 간단했습니다:

# 차단된 클라이언트 테스트
curl -X POST 'http://localhost:9999/api/v1/filter' \
-H 'Content-Type: application/json' \
-H 'x-api-key: CWAAh7v3Z-********F3k' \
-d '{"text": "테스트", "mode": "FILTER"}'

# 폐기된 클라이언트 테스트
curl -X POST 'http://localhost:9999/api/v1/filter' \
-H 'Content-Type: application/json' \
-H 'x-api-key: 7RhvsEuI********Fa1o' \
-d '{"text": "테스트", "mode": "FILTER"}'

그런데 이상한 점을 발견했습니다. 두 요청 모두 동일한 응답이 반환되었고, 특히 첫 번째 요청의 응답이 두 번째 요청에서도 그대로 반환되었습니다.

디버깅 과정

처음에는 SecurityContext나 권한 체크 로직에 문제가 있다고 생각했습니다. DB를 확인해보니 각 클라이언트의 권한은 정확했습니다:

{
  "api_key": "7RhvsEuI********Fa1o",
  "permissions": "DISCARD"
},
{
  "api_key": "CWAAh7v3Z-********F3k",
  "permissions": "BLOCK"
}

로그를 자세히 살펴보니 더 이상한 점을 발견했습니다:

2025-01-31T00:23:22.999+09:00  INFO 72577 --- [nio-9999-exec-1] : ip : 127.0.0.1, path : /api/v1/filter, method : POST
2025-01-31T00:23:23.150+09:00  INFO 72577 --- [nio-9999-exec-1] : permissions: [BLOCK]
2025-01-31T00:23:23.150+09:00  INFO 72577 --- [nio-9999-exec-1] : 차단된 클라이언트의 접근이 감지되었습니다.
2025-01-31T00:23:40.891+09:00  INFO 72577 --- [nio-9999-exec-2] : ip : 127.0.0.1, path : /api/v1/filter, method : POST

두 번째 요청에서는 권한 체크 로그가 아예 찍히지 않았습니다. 이는 권한 체크 로직 자체가 실행되지 않았다는 것을 의미했습니다.

원인 분석

문제의 원인은 Spring AOP의 실행 순서에 있었습니다. 코드를 살펴보면:

@VerifiedClientOnly
@Cacheable(value = "request_filter", key = "#request.text + '_' + #request.mode")
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> basicProfanity(...) {
    // 메소드 구현
}

@Cacheable 어노테이션이 @VerifiedClientOnly 보다 먼저 실행되어, 첫 번째 권한 체크 결과가 캐시에 저장되고 이후 모든 요청에 대해 동일한 응답이 반환되고 있었습니다.

해결 과정

1. AOP 우선순위 이해

Spring AOP에서 어노테이션의 우선순위는 중요합니다:

  • 캐시: Ordered.LOWEST_PRECEDENCE - 1
  • 트랜잭션: Ordered.LOWEST_PRECEDENCE - 1

2. 해결 방법 구현

권한 체크 Aspect에 @Order 어노테이션을 추가하여 우선순위를 조정했습니다:

@Slf4j
@Aspect
@Component
@Order(1)
public class ClientVerificationAspect {
    
    @Around("@annotation(app.security.annotation.VerifiedClientOnly)")
    public Object verifyClient(ProceedingJoinPoint joinPoint) throws Throwable {
        // 권한 체크 로직
        if (annotation.checkBlocked() && SecurityContextUtil.isBlockedClient()) {
            return ApiResponse.error(Status.of(FORBIDDEN, "차단된 클라이언트입니다."));
        }
        if (annotation.checkDiscarded() && SecurityContextUtil.isDiscardedClient()) {
            return ApiResponse.error(Status.of(FORBIDDEN, "폐기된 클라이언트입니다."));
        }
        return joinPoint.proceed();
    }
}

결과

이제 각 클라이언트의 상태에 따라 적절한 응답이 반환됩니다:
1. BLOCK 상태 클라이언트: "차단된 클라이언트입니다." 메시지 반환
2. DISCARD 상태 클라이언트: "폐기된 클라이언트입니다." 메시지 반환

학습 포인트

  1. Spring AOP의 실행 순서의 중요성

    • 여러 AOP가 적용될 때는 실행 순서를 신중히 고려해야 함
    • @Order 어노테이션으로 우선순위 제어 가능
  2. 캐시 관련 주의사항

    • 캐시는 성능 향상에 도움이 되지만, 다른 기능과의 상호작용을 고려해야 함
    • 권한 체크와 같은 중요한 로직이 캐시에 의해 무시되지 않도록 주의
  3. AOP 설계 시 고려사항

    • Aspect의 책임과 실행 순서를 명확히 정의
    • 여러 Aspect 간의 상호작용을 고려한 설계 필요

결론

이번 문제 해결을 통해 Spring AOP의 동작 방식과 실행 순서의 중요성을 다시 한번 깨달을 수 있었습니다. 특히 캐시와 같은 성능 최적화 기능을 도입할 때는 기존 비즈니스 로직에 영향을 주지 않도록 신중한 설계가 필요하다는 것을 배웠습니다.

0개의 댓글

관련 채용 정보