[Spring] Interceptor와 AOP를 활용한 API 로깅

말하는 감자·2025년 4월 17일

내일배움캠프

목록 보기
43/73
post-thumbnail

에라이 도전과제 할라는데 뭔소린지 하나도 모르것네 진심;;

🛂 Interceptor

자바에서 인터셉터는 애플리케이션의 요청과 응답을 가로채고 처리하는 기능을 제공하는 인터페이스

인증, 권한 확인, 요청 로그 남기기 하는 용도로 사용하기 좋다.

서버에 들어온 Request객체를 컨트롤러의 핸들러(url에 매핑되어있는 매서드)로 도달하기 전에 낚아채서 개발자가 원하는 추가적인 작업을 한 후 핸들러로 보낼 수 있도록 해주는 용도로 사용된다.

인터셉터는 언제쓸까?

상황설명
로그인 체크로그인 안 한 사용자는 요청 차단
관리자 권한 체크어드민 API 접근 시 권한 검사
요청 로깅어떤 API에 언제 누가 접근했는지 기록
공통 헤더 설정응답에 공통 헤더 붙이기 등





HandlerInterceptor 인터페이스

인터셉터는 HandlerInterceptor 인터페이스를 implements 해서 구현할 수 있다.
해당 인터페이스는 preHandle( ), postHandle( )
afterCompletion( ) 총 세 개의 추상 메서드를 포함하고 있음.


public class LoggerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		// 필요 로직~~
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //필요 로직~~~~
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //필요 로직~~~~
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}

여기서 필요한 메서드만 impl로 구현하면된다는 소리 ㅇㅇ
이제 preHandle( ), postHandle( )afterCompletion( ) 메서드가 각각 언제 어떻게 호출되나 봅시다..

🔢 호출순서

아주 좋은 사진이 있어서 가져왓다.!

  • 1️⃣ preHandle(): 컨트롤러 실행 전에 호출되는 메서드
    요청을 가로채고 사전 작업을 수행할 수 있다.
    이 메서드가 true를 반환하면 요청은 계속 진행되고, false를 반환하면 요청 처리가 중단됨

  • 2️⃣ afterCompletion(): 뷰 렌더링까지 완료된 후에 호출되는 메서드최종적인 작업 처리를 수행할 수 있다.

  • 3️⃣ postHandle(): 컨트롤러 실행 이후에 호출되는 메서드
    컨트롤러의 실행 결과에 대한 후처리 작업을 수행할 수 있다.
    → 예외 여부 상관없이 모든 처리가 완전히 끝난 후






이제 LoggerInterceptor 클래스가 작동할 수 있도록 클래스를 빈(Bean)으로 등록해 주어야 한다.

인터셉터 적용하기

빈으로 등록하기 위해서는 WebMvcConfigurer에 등록해야하는데,
대충 이 작업은 Spring에게 "이 URL에는 이 인터셉터 써줘!" 라고 알려주는 행위다.

과제 예시 코드에서는 이미 WebConfig가 있다 굿

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggerInterceptor()) // 우리가 만든 인터셉터
                .addPathPatterns("/admin/**")        // 이 URL에는 이 인터셉터 써줘!!!
                .excludePathPatterns("/admin/login"); // 여기는빼줘!!!
    }
}

이런너ㅡ낌?

addInterceptors( )

애플리케이션 내에 인터셉터를 등록해 준다.

  • excludePathPatterns( ) : 인자로 전달하는 주소(URI)와 경로(Path)는 인터셉터 호출에서 제외시킴.
  • addPathPatterns( ) : 인터셉터를 호출하는 주소와 경로를 추가

ㅊㅊ
https://velog.io/@dnflekf2748/스프링-인터셉터Interceptor
https://congsong.tistory.com/24
https://madinthe90.tistory.com/63#google_vignette






이제 과제할떄 필요한 기본은 알거같고

로깅은 또 Logger클래스를 활용하라고하네 첨듣는다 얘도

AOP도 대충 개념만 알고있는데 저것들이 같이있으니깐 말이 엄청 헷갈린다..








Logger 클래스

이때까지 로깅할때 @Slf4j만 써본거같은데 이번엔 다른거다

인줄알았는데
거의똑같네

private final Logger log = 
LoggerFactory.getLogger(클래스이름.class);

그외에 사용메서드는 다 똑같은거같다.

        String name = "Spring";
        log.trace("trace log = {}", name);
        log.debug("debug log = {}", name);
        log.info("info log = {}", name);
        log.warn("warn log = {}", name);
        log.error("error log = {}", name);
 

ㅊㅊ
https://growth-coder.tistory.com/121
https://dkswnkk.tistory.com/445









어드민 사용자만 접근할 수 있는 특정 API에는 접근할 때마다 접근 로그를 기록하기

에................
그러니깐 과제에서 제공해주는 프로젝트를 보면
Filter자체를 통과 -> 로그인 성공 으로 생각해도되는데,
만약에 로그인해서 들어온 경로가 어드민 사용자만 접근 가능한 api인 경우에 로그를 기록하기..............다.

필터를 잠깐 살펴보면

권한 체크랑 다해놨다.
결국엔 인증된 사용자만 필터를 넘어올 수 있으니
1. 넘어온 요청의 URI 확인 -> URI에 /admin 붙었나 확인 ㅇㅇ
2. /admin 붙으면 로깅

이걸 해주면 되는 것 같다.

<어드민 사용자만 접근할 수 있는 컨트롤러 메서드>

deleteComment()

  • uri : @DeleteMapping("/admin/comments/{commentId}")


    changeUserRole()
  • uri : @PatchMapping("/admin/users/{userId}")





인스펙터 사용하기

package org.example.expert.config;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;


public class LoggerInterceptor implements HandlerInterceptor {
    private final Logger log = LoggerFactory.getLogger(getClass());

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("RequestURL = {}", request.getRequestURL());
        log.info("Request time : {}", LocalDateTime.now());
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

}

uri검증은 인스펙터에서에서 필요가없는게
등록할때 특정 uri에서만 작동하게 설정가능해서그럼

바로 테스트 갈겨


잘뜬다 ㅎㅎ





AOP 사용하기

요구사항

AOP를 사용하여 구현하기

  • 어드민 API 메서드 실행 전후에 요청/응답 데이터를 로깅합니다.
    • @Around 어노테이션을 사용하여 어드민 API 메서드 실행 전후에 요청/응답 데이터를 로깅합니다.
  • 로깅 내용에는 다음이 포함되어야 합니다:
    • 요청한 사용자의 ID
    • API 요청 시각
    • API 요청 URL
    • 요청 본문(RequestBody)
    • 응답 본문(ResponseBody)
      • 요청 본문과 응답 본문은 JSON 형식으로 기록하세요.



의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-aop'
ㅇㅇ



AOP 클래스 형태

joinPoint.proceed()는 비즈니스 메서드의 실행을 제어한다.
다시 말해, joinPoint.proceed() 호출 전은 비즈니스 메서드 호출 전이고, joinPoint.proceed() 호출 후는 비즈니스 메서드 호출 후라고 생각하면 된다.

JointPointSignature에는 클라이언트가 호출한 메서드의 시그니처(리턴 타입, 이름, 매개변수 등) 정보가 담겨 있어 이를 활용하여 호출된 메서드의 정보를 가져올 수 있다.

그럼 필요한 정보를 가져와야겠지?


필터에보면 Attribute란에 userId를 넣어놔서 getAttribute로 손쉽게 가져올 수 있다.



🔍 joinPoint.getArgs()

request랑 response내용을 꺼내오는데 ProceedingJoinPoint라는 AOP객체를 사용하기 때문에 안에있는 내용들을 getArgs()으로 꺼내올 수 있다.

getArgs()는 실제로 호출된 메서드에 전달된 인자 목록을 배열 형태로 리턴해준다!! (공식문서 밑에있음)

예를들어서 들어오는 함수가

@PatchMapping("/admin/users/{userId}")
public void changeUserRole(@PathVariable long userId, @RequestBody UserRoleChangeRequest userRoleChangeRequest)

이거라고 치면
getArgs() 안에는 총 2개의 값이 배열로 저장됨

  • args[0] = (Long) @PathVariable long userId 값
  • args[1] = (UserRoleChangeRequest) @RequestBody로 들어온 DTO 인스턴스

이걸그냥 json으로 매핑해줘서 출력하믄된다잉

정리

  • joinPoint.getArgs()는 타겟 메서드에 전달된 모든 인자를 배열로 반환
  • 파라미터의 순서, 타입, 값은 실제 메서드 시그니처를 그대로 반영
  • @PathVariable, @RequestBody 같은 구분은 getArgs()만으로는 알 수 없음 → 타입으로 추론하거나, HandlerMethod를 따로 사용해야 함
    (타입 추론은 insteadof로 받아올 때 타입형태를 보고 분기나누기 해야함)

지금 해야하는 부분에는 dto 형식도 엄청 간단해서 그냥 냅다 출력갈겼다!
근데 여기서는 값만 가져오고 변수이름은 가져옴..



🔍 joinPoint.getSignature()

PathVariable값이라고 표기를 해서 Body랑 같이 JSON으로 묶어보고싶어서 좀 찾아봤다.
근데 joinPoint얘가 getSignature()라고하는 좋은 함수를 갖고있음.
반환형태는 MethodSignature라는 클래스인데 파라미터 이름들을 가지고있다!!

예를 들어서

@PatchMapping("/admin/users/{userId}")
public void changeUserRole(@PathVariable long userId, @RequestBody UserRoleChangeRequest userRoleChangeRequest)

이거라면 괄호안에 잇는
(@PathVariable long userId, @RequestBody UserRoleChangeRequest userRoleChangeRequest)
이것들을 가져와준다 완전대박 ㅋ
그럼 배열은 총 2개에

  • 0번은 String으로 "userId"
  • 1번도 String으로 "UserRoleChangeRequest"

가 저장이 됨

생각한대로 된 것 같당

ㅊㅊ
https://leeeeeyeon-dev.tistory.com/51
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.html

profile
대충 데굴데굴 굴러가는 개발?자

0개의 댓글