🔖 핵심 주제
Interceptor / AOP의 구조이해와 활용.
💡 배운 내용
1. 요청 처리 흐름 이해 (Filter → Interceptor → Controller → AOP)
- Filter는 인증(JWT 파싱)
- Interceptor는 인가(admin 권한 체크)
- AOP는 메서드 실행 전후 로깅, 트랜잭션, 예외 처리 등에 활용 될 수 있다

추가적으로 Interceptor 에서 발생한 예외가 Filter의 catch에서 잡히는 이유는 chain.doFilter() 내부에서 발생했기 때문이라는 점을 이해했다.
예를들어 jwt Filter 에서 이런 에러 처리가 있을때, Interceptor 에서 에러가 발생하면 해당 에러가 반환된다.
} catch (Exception e) {
log.error("예상치 못한 오류: URI={}", url, e);
sendErrorResponse(httpResponse, HttpStatus.INTERNAL_SERVER_ERROR, "요청 처리 중 오류가 발생했습니다.");
}
즉, doFilter 내부에서 interceptor가 동작한다는 것을 알 수 있다.
2. Admin API 실행 전후로 요청/응답을 JSON으로 로깅하는 AOP를 구현했다.
- @Aspect, @Pointcut, @Around 등의 에노테이션이 무엇인지 확인하고 사용 해보았다.
- Interceptor 와 다르게 webConfig에 등록하지 않고 메서드를 직접 지정 하는 방식.
- AOP 내부에서 요청정보를 가져오는 법을 학습하고 사용해보았다
-전체 소스 코드
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LoggingAspect {
private final ObjectMapper objectMapper;
@Pointcut("execution(* org.example.expert.domain.user.service.UserAdminService.*(..)) || " +
"execution(* org.example.expert.domain.comment.controller.CommentAdminController.*(..))")
private void adminApi() {}
@Around("adminApi()")
public Object logAdminApi(ProceedingJoinPoint joinPoint) throws Throwable {
UUID requestId = UUID.randomUUID();
// 요청 시간 확인
String requestTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// attributes 가져오기
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//attributes 가 없으면 에러 발생
if(attributes == null){
log.error("HTTP 요청 컨텍스트를 찾을 수 없습니다. (RequestAttributes is null)");
throw new IllegalStateException("해당 AOP는 HTTP 요청 내부에서만 실행 가능합니다.");
}
// 요청 정보 가져오기
HttpServletRequest request = attributes.getRequest();
//url 정보
String url = request.getRequestURI();
// attribute 에 저장된 userId가져오기
Long userId = (Long) request.getAttribute("userId");
// body args list
Object[] args = joinPoint.getArgs();
log.info(">>> 요청 ID= {} | 요청 데이터 userId= {} 시간= {} 주소= {} Body= {}",
requestId ,userId, requestTime, url, objectMapper.writeValueAsString(args));
Object result;
//서비스 실행
result = joinPoint.proceed();
// 결과 response 반환
String responseBody = (result != null) ? result.toString() : "No Response";
log.info("<<< 요청 ID= {} | API Response:[Body: {}]",requestId, responseBody);
return result;
}
}
🚀 문제 해결 및 트러블슈팅
- 문제 상황 : enum 검증을 위해 ValidEnum 작성중 적용 되지 않던 현상
1. 기존엔 enum 검증을 서비스 단에서 수행 -> dto단에서 검증하고 서비스에서는 바로 enum 사용을 하고자 함
-> ValidEnum 이라는 개념을 적용 시켜보려 시도 중 타입을 찾지 못하는 에러 발생.


-시도한 방법 : ConstraintValidator<ValidEnum, UserRole>으로 선언 하여 해결 시도 ->
UserRole 타입으로 선언된 순간 Jackson이 역직렬화할 때 이미 enum 값 검증을 해버리기 때문에, @ValidEnum까지 올 일이 없다,
유효하지 않은 값이 오면 그 전에 터집니다. -> 의미 없는 행위
- 해결 방안
단순 DTO에서 타입설정 및 NotNull -> 이후 globalException 에서 HttpMessageNotReadableException 를 관리
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SignupRequest {
@NotBlank @Email
private String email;
@NotBlank
private String password;
@NotNull
private UserRole userRole;
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> enumValidation(final HttpMessageNotReadableException e) {
HttpStatus status = HttpStatus.BAD_REQUEST;
String message = e.getMessage();
return getErrorResponse(status, message);
}
오늘 잘한 점
- 단순히 “왜 에러 나지?”에서 끝나지 않고 실행 흐름을 구조적으로 분석했다.
- Filter와 Interceptor의 역할을 명확히 분리했다.
- AOP를 실제 요구사항에 맞게 구현해봤다.
새로 알게 된 점
- Interceptor 예외가 Filter catch에 잡히는 건 정상 흐름이라는 것
- AOP는 단순 로깅 도구가 아니라 “관심사의 분리”를 구현하는 핵심 기술이라는 것
- ValidEnum의 기본 사용법과 사용 이유.
내일 더 학습하고 싶은 내용
- 테스트 커버리지 고려한 테스트 코드작성
- 과제 리팩토링
'TIL' 카테고리의 다른 글
| 유닛테스트와 통합테스트 (0) | 2026.03.05 |
|---|---|
| 테스트코드 (0) | 2026.03.04 |
| github - 협업준비하기. (0) | 2026.02.15 |
| SOLID 원칙 (1) | 2026.02.06 |
| ORM 과 JPA (1) | 2026.02.03 |