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
보다 먼저 실행되어, 첫 번째 권한 체크 결과가 캐시에 저장되고 이후 모든 요청에 대해 동일한 응답이 반환되고 있었습니다.
Spring AOP에서 어노테이션의 우선순위는 중요합니다:
Ordered.LOWEST_PRECEDENCE - 1
Ordered.LOWEST_PRECEDENCE - 1
권한 체크 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 상태 클라이언트: "폐기된 클라이언트입니다." 메시지 반환
Spring AOP의 실행 순서의 중요성
@Order
어노테이션으로 우선순위 제어 가능캐시 관련 주의사항
AOP 설계 시 고려사항
이번 문제 해결을 통해 Spring AOP의 동작 방식과 실행 순서의 중요성을 다시 한번 깨달을 수 있었습니다. 특히 캐시와 같은 성능 최적화 기능을 도입할 때는 기존 비즈니스 로직에 영향을 주지 않도록 신중한 설계가 필요하다는 것을 배웠습니다.