SPARTA 과제/SPRING

심화) LV 5 API 로깅

kjw81024 2026. 3. 4. 23:29

5. API 로깅

AOP -> 어떤 요청을 실행시켰는지

Interceptor -> Http 요청에 대한 기록

둘 다 해보기

 

 

(1) AOP

 

식별자가 필요하다는 에러

 

강의 자료에서 봤던 것 처럼 형식에 맞춰서 패키지.패키지. . . 클래스 순서로 작성

강의 자료에서는 한 클래스 내부의 메소드만 체크해줬지만 과제는 다른 두 클래스를 사용해야한다.

 

찾아보니까 AND 와 OR 연산자를 사용할 수 있는 듯 하여 추가

@Around("execution(* org.example.expert.domain.user.service..*(..)) ||" +
        "execution(* org.example.expert.domain.comment.service..*(..))")

 

설정한 클래스와 메서드에서 값이 정상적으로 실행됨

댓글을 삭제하는 요청을 보냈고 정상적으로 삭제되었다

execution 을 잘 설정해준것을 확인

로그가 찍히는 걸 확인 후 로그에 포함되어야 할 내용을 추가해줘야한다...

 

 

 

(첫 코드)

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class ApiLogRecordAop {
    private final JwtUtil jwtUtil;
    private final ObjectMapper objectMapper;


    @Around("execution(* org.example.expert.domain.user.service..*(..)) ||" +
            "execution(* org.example.expert.domain.comment.service..*(..))")
    public Object recordApiLog(ProceedingJoinPoint joinPoint) throws Throwable{
        HttpServletRequest request = ((ServletRequestAttributes) 
        	RequestContextHolder.currentRequestAttributes()).getRequest();

        log.info("timeStamp: "+ LocalDateTime.now()); # api 요청이 들어온 시각

        log.info("package: {}", joinPoint.getThis()); # 패키지 위치 
        log.info("요청 method: {} {}", request.getMethod() ,request.getRequestURI()); # URL
        String jwt = jwtUtil.substringToken(request.getHeader("Authorization")); 
        log.info("해당 요청에 접근한 사용자 Id: {}", jwtUtil.extractClaims(jwt).getSubject()); # ID
        
        # Object[] 로 반복문을 돌려서
        for (Object obj : joinPoint.getArgs()){
        # 사용된 형태가 ./dto/request 형태인 것만 추출 
            if (obj.getClass().getPackageName().contains(".dto.request")){
            # json 형태로 변환하여 출력
                log.info("요청 본문: {}", objectMapper.writeValueAsString(obj));
                break;
            }
        }

        # -------얘 기준으로 method 앞인지 뒤인지 결정됨---------
        Object result = joinPoint.proceed();
        # -----------------------------------------------------


        log.info("timeStamp: "+ LocalDateTime.now());
        
        # 사용된 형태가 ./dto/response 형태인 것만 추출
        for (Object obj : joinPoint.getArgs()){
            if (obj.getClass().getPackageName().contains(".dto.response")){
                log.info("응답 본문: {}", objectMapper.writeValueAsString(obj));
                break;
            }
        }

        return result;
    }
}

 

과장 좀 보태서 3시간은 쓴 거 같다

 

리팩토링 할 부분이 너무 많아서 코드 수정 과정 lv6 TIL로 추가 예정

 

결과적으로 필요한 값은 출력이 되었지만 request.getXXX 를 20개는 써본 것 같다 너무 어거지로 만든 느낌 ㅠㅠ


일단 어려웠던게 HttpServletRequest 가 무슨 값을 들고있고 어떻게 참조해야할지 잘 몰랐고, 동시에 ProceedingJoinPoint 도 생소해서 어떻게 그 안 에서 원하는 정보만 빼서 log 로 출력해야할지에 대해 고민이 많았던거같음 

수많은 메소드들 중 언젠가 쓸 거 같은 애들만 정리하고 넘어가려고 한다.

 

<HttpServletRequest>

method Name 의미 결과값 예시
getRequestURI() 요청이 들어간 URL /admin/comments/3
getMethod() HTTP method  GET, POST, PUT 등
getParameter(String name) parameter 이름을 넣어서 해당하는 쿼리 파라미터의 value 값을 가져옴 /login?email=tako@email.com
getHeader(String name) Header 의 name을 넣어서 value 값을 가져옴 ex) "Authorization" Bearer ey어쩌고...
getAttribute(String name) request 에 저장되어있는 값 꺼내기  
setAttribute(String name, Object value) request 에 값 저장
인증 객체를 저장함
 
getSession() 세션기반 로그인 할 때 사용  

 

<JoinPoint>

method Name  의미 결과값 예시
getSigniture().getName() 현재 실행중인 method 정보 createTodo
getArgs() DTO 값 로깅   
getTarget().getClass().getName() 실행중인 class 정보 (객체) org.example.expert.domain.todo.service.TodoService
getSigniture().getMethod().XXX 어노테이션을 읽음 
MethodSigniture 를 받고
Method method = sig.getMethod() 로 씀

method.isAnnotationPresent(xxx.class)
 

 

다음엔 헤매지 말아야지

 


(2) Interceptor

Interceptor 과제에서는 오류가 발생하면 오류처리를 하는 부분도 포함되어있기 때문에 일부러 ADMIN 권한이 없는 버전으로 실행

 

구현 순서 

1) HandlerInterceptor 를 implements 한 CustomInterceptor 만들기 

@Slf4j
@Component
@RequiredArgsConstructor
public class CheckAdminIntercepotor implements HandlerInterceptor {
	# 2) 
}

Bean 으로 등록해주기 위해 @Component 를 추가 

 

2) Prehandle 또는 Posthandle 내용을 구현

private final JwtUtil jwtUtil;

public boolean preHandle (HttpServletRequest request,HttpServletResponse response,
        Object handler) throws IOException{

    String jwt = jwtUtil.substringToken(request.getHeader("Authorization"));
    String role = (String) request.getAttribute("userRole");
    log.info(role +" "+UserRole.ADMIN);

    if (UserRole.of(role) != UserRole.ADMIN){
        log.warn("관리자 아님, 접근 거부 {}", jwtUtil.extractClaims(jwt).getSubject());
        throw new InvalidRequestException("관리자 아닌디?");
    }

    log.info("timestamp: {}", LocalDateTime.now());
    log.info("요청 method: {} {}", request.getMethod() ,request.getRequestURI());

    return true;
}

토큰을 파싱해서 현재 로그인한 유저의 역할을 role 에 저장하고 ADMIN 인지 확인하여 exception 을 던지는 로직 

만약 exception 이 던져지지 않았다면 해당 요청이 온 시각과 요청 Method URI 를 출력

 

3) WebConfig 에 addInterceptors 메서드를 추가 

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(checkAdminIntercepotor)		# 어떤 인터셉터를
            .addPathPatterns("/admin/**");			# 어떤 URL 에 연결할지
}

admin 이 붙은 메서드들만 interceptor 를 실시할 수 있다 

** 은 그 이후에 나오는 endpoint 는 별개로 /admin 까지만 확인한다는 의미

InterceptorRegistry 는 addInterceptors의 매개변수로 사용되며 어떤 Interceptor 를 어떤 URL 패턴에 적용할지 등록 

-> Interceptor -----InterceptorRegistry -----> SpringMVC (연결해주는 역할)

 

코드를 실행시켰다

근데 내가 설정한 log 메시지인 관리자 아님, 접근 거부 가 아닌 권한부족 log 가 발생

-> JwtFilter 에 이미 ADMIN 역할 필터링이 되는 부분이 존재해서 이미 Filter 단에서 걸려서 Interceptor 가 수행되기 전에 return 또는 throw 됨

 

주석처리 후 실행시키니 아래처럼 내가 설정한 값들로 제대로 return 되는 모습


결론

강의에서 최근에는 Interceptor 대신 AOP 를 많이 사용한다고 해서 이번 과제에서는 AOP 를 한 번 제대로 사용해보고싶은 마음이 컸다! 최근에 많이 사용되는게 제일 성능이나 편리함 측면에서 좋을 것이라고 생각했었던 것 같다

 

하지만 방법 둘 다 구현을 하는 과정에서 

AOP 코드가 위치한곳과 사용하면 안 되는 로직... 들에 대해서 알고 리팩토링 하는 과정에서 많은 어려움을 겪었다

Interceptor Http요청 인증, 인가 -> 웹 요청 제어니까 Interceptor 쓰는게 55555배 나음 
AOP 특정 메서드 공통 비즈니스 로직 (ex 로깅, 시간재기)

AOP 는 Http 개념을 몰라야하므로 ContextHolder 까지 가서 꺼내야함

-> 너무 번거로운 작업 ....

 

Request 를 분석해야하는 경우에는 AOP 말고 interceptor 쓰기......

AOP 에서 requestBody 꺼내거나 컨트롤러 흐름 제어하려고 하는짓 하지말기 

최신꺼 말고 무조건 상황에 맞는거 쓰기 

 


참고자료

https://yijoon009.tistory.com/entry/Spring-Boot-AOP%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B9%85-%EA%B5%AC%ED%98%84-EnableAspectJAutoProxy%EC%99%80-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EC%9D%98-Aspect-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%B9%84%EA%B5%90-%EA%B0%80%EC%9D%B4%EB%93%9C#:~:text=%EC%97%AC%EB%9F%AC%20%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80%20%EB%A9%94%EC%86%8C%EB%93%9C%EC%97%90%20%EA%B1%B8%EC%B3%90%20%EB%B0%98%EB%B3%B5%EC%A0%81%EC%9C%BC%EB%A1%9C%20%ED%95%84

 

[Spring Boot] AOP를 활용한 로깅 구현: @EnableAspectJAutoProxy와 사용자 정의 Aspect 클래스 비교 가이드

AOP를 활용한 로깅 구현: @EnableAspectJAutoProxy와 사용자 정의 Aspect 클래스 비교 가이드 스프링 부트 프로젝트에 로직의 이해를 돕기 위해 AOP를 로깅에 적용하는 방법에 대해서 설명해 보겠다. 1. 왜 A

yijoon009.tistory.com