본문 바로가기
Spring

스프링 AOP는 항상 주의하여 사용하자

by woo00oo 2024. 5. 27.

이번 포스팅에서는 스프링 AOP 사용으로 삽질한 경험을 기록해두려고 한다.

 


사건의 계기

사용자들의 추가 요구사항으로 신규 API를 개발해야하는 경우가 많아졌다.

여러 REST API를 추가 개발하면서 중복 코드가 발생하였는데, 바로 요청 파라미터와 응답 파라미터에 대한 로깅 처리였다. 이를 스프링 AOP로 중복 코드를 제거하였고, 코드는 대략적으로 아래와 같았다.

 

JoinPoint 객체에서 CodeSignature 객체를 얻어와 요청 파라미터 네임과 어떤 데이터가 넘어왔는지 Map 형식으로 변환 시켰다.

private Map<String, Object> params(JoinPoint joinPoint) throws JsonProcessingException {

    CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
    String[] parameterNames = codeSignature.getParameterNames();
    Object[] args = joinPoint.getArgs();
    Map<String, Object> params = new HashMap<>();
    
    for (int = 0; i < parameterNames.length; i++) {
    	params.put(parameterNames[i], mapper.writeValuseAsString(args[i]));
    }
    
    return params;
 }

 

그리고 api 패키지 하위에 있는 모든 컨트롤러에서 위 AOP 객체가 호출 되도록 지정하였다.

@Pointcut("execution(....presentation.api..*(..))")
private void onRequest() {}

 

이후 해당 @Aspect 객체에 대해서 새까맣게 잊고 있었다..


문제 발생

국내 간편 결제 마이그레이션 개발 진행 중 예상 치 못한 에러가 발생 하였다.

결제 승인 후 결제사로부터 서버 콜백을 처리해야 한다. 바로 이 요청을 처리하는 컨트롤러에서 문제가 발생하였다.

컨트롤러 클래스 코드는 대략 아래와 같다. HttpServletRequest 객체에 setAttribute를 지정해야 하기에(세션키 저장) 컨트롤러 메소드에 인자로 선언해두었다.

@PostMapping
public String processServerCallback(
	@ReqeustParam String paramKey,
    ...
    HttpServletRequest request
 ) {
    ...
 }

 

에러 로그는 다음과 같았다. 
에러 내용으로는 ' requested_session_id_from_url'이라는 속성에 대해 두개의 서로 충돌하는 getter 메소드가 존재한다고..?

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Conflicting getter definitions for property "requested_session_id_from_url": org.apache.catalina.connector.RequestFacade#isRequestedSessionIdFromURL() vs org.apache.catalina.connector.RequestFacade#isRequestedSessionIdFromUrl()

 

결제사 콜백을 받고 비즈니스 로직을 처리하는데 문제가 없었지만 위 에러로 Http 500 응답이 반환되고 있었다.

jackson 예외인 걸 봐선 응답 DTO 파싱에 문제가 있었나? requested_session_id_from_url 라는 필드는 뭐지? 바로 삽실 시작..

 

응답 DTO 객체에 @AllArgsConstructor 도 붙여 보고, @NoArgsConstructor, @Setter 도 추가해보았지만 여전히 문제가 발생하였다. 스프링 AOP가 적용되어 Aspect 객체가 호출 되는지 전혀 모른체 내가 만들어둔 컨트롤러 클래스 자체에 문제가 있는 지 알고 헛고생을 하였다.

 

그렇게 비즈니스 로직에는 문제가 없는 것인지 중간중간 디버깅 로그를 추가하여 결제사로부터 서버 콜백이 오면 정상적으로 처리되고 있는지 확인하였다. 로그를 확인하던 중 내가 찍지 않았던 로그가 출력된 것을 확인할 수 있었다..

아..차 예전에 만들어둔 LoggingAspect가 작동하고 있었구나.. 해당 클래스의 소스코드를 보던 중 ObjectMapper의 writeValuseAsString 메소드 호출로 위 에러가 발생하였던 것이였다.

 

HttpServletRequest 클래스를 열어보면 다음 두 메소드가 존재한다

  • isRequestedSessionIdFromURL()
  • isRequestedSessionIdFromUrl()

위 메소드는 서로 다른 이름을 가지고 있지만, Jackson은 기본적으로 대소문자를 구분하지 않고 메서드 이름을 속성으로 매핑하려고 시도하기에 getter가 충돌한다는 예외가 발생했던 것이다.


해결 방법

문제 원인을 파악한 후로 해결 방법은 아주 간단하였다. 서버 콜백을 받는 컨트롤러 클래스는 AOP가 적용되지 않아도 되기에 포인트컷을 수정하였다. 추가적으로 타 API 컨트롤러 클래스에서도 HttpServletRequest 객체를 인자로 선언하게 되면 동일한 문제가 발생할 것이기에 요청 파라미터를 Map 형식으로 변환하는 LoggingAspect 객체의 params 메소드를 수정 하였다. (HttpServletRequest 객체는 ObjectMapper로 json 파싱되지 않도록..)