[Error Note] ErrorResponse에 stackTrace 등이 붙는 오류 해결

DEINGVELOP·2023년 1월 28일

🧾 오류 내용

의도했던 Response

{
    "code": "INVALID_TEAM_NAME",
    "message": "유효하지 않은 그룹(팀)명입니다."
}

현재 Response

{
    "cause": null,
    "stackTrace": [
        {
            "classLoaderName": "app",
            "moduleName": null,
            "moduleVersion": null,
            "methodName": "invalidGroupNameExceptionHandler",
            "fileName": "GlobalExceptionHandler.java",
            "lineNumber": 18,
            "className": "com.coderder.colorMeeting.exception.GlobalExceptionHandler",
            "nativeMethod": false
        },
        {
            "classLoaderName": null,
            "moduleName": "java.base",
            "moduleVersion": "11.0.16",
            "methodName": "invoke0",
            "fileName": "NativeMethodAccessorImpl.java",
            "lineNumber": -2,
            "className": "jdk.internal.reflect.NativeMethodAccessorImpl",
            "nativeMethod": true
        },
        ···,    // 축약
        {
            "classLoaderName": null,
            "moduleName": "java.base",
            "moduleVersion": "11.0.16",
            "methodName": "run",
            "fileName": "Thread.java",
            "lineNumber": 829,
            "className": "java.lang.Thread",
            "nativeMethod": false
        }
    ],
    "code": "INVALID_TEAM_NAME",
    "message": "유효하지 않은 그룹(팀) 이름입니다.",
    "suppressed": [],
    "localizedMessage": "유효하지 않은 그룹(팀) 이름입니다."
}

예상 문제 원인

  • jwt 도입 과정에서 AuthorizationFilter 등을 추가하면서, ControllerAdvice가 만들어준 response가 Dispatcher-Servlet을 벗어난 후 Filter 단에서 한 번 더 예외처리가 되면서 resopnse에 필드들이 추가되는 것으로 예상 - 아니었음. 아래에서 다시 이야기할 예정

🔧 해결 과정

해결 시도 1 - e.printStackTrace(); 주석처리 → 실패

  • JwtAuthenticationFilter

        // DB에 저장된 값과 일치하는가?
     @Override
     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
         throws AuthenticationException {
    
         try {
             // System.out.println("============== attempt Authentication starts ===============");
             // 1) request에서 username, password 받아오기
             // ObjectMapper는 json을 파싱한다
             // User.class를 넣으면 username, password 외에 기타 정보도 다 가져온다. loginDto를 추가로 만들자
             ObjectMapper om = new ObjectMapper();
             LoginRequestDto loginRequestDto = om.readValue(request.getInputStream(), LoginRequestDto.class);
    
             // 2) 토큰을 만들고 로그인을 시도한다
             // am.authenticate()는 토큰의 파라메터(getUsername, getPassword)를 차례대로 검증한다
             UsernamePasswordAuthenticationToken authenticationToken =
                     new UsernamePasswordAuthenticationToken(loginRequestDto.getUsername(), loginRequestDto.getPassword());
             Authentication authentication =
                     authenticationManager.authenticate(authenticationToken);
    
             // 로깅
             PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
             // System.out.println("================== Authentication : "+principalDetails.getMember().getUsername() + " ==================");
             // System.out.println("============== attempt Authentication ends ===============");
    
             // 3) authenticate를 성공하면 authentication을 만든다.
             // attemptAuthentication의 리턴값은 session 영역에 저장된다.
             // 권한 관리를 위해 session에 저장한다. 권한 관리를 안 하면 굳이 세션에 저장하지 않아도 된다.
             return authentication;
    
         } catch (StreamReadException e) {
             e.printStackTrace();
         } catch (DatabindException e) {
             e.printStackTrace();
         } catch (IOException e) {
             e.printStackTrace();
         }
    
         return null;
     };

해결 시도 2 - 추정원인 : throws 해준 exception을 catch해주는 코드가 빠져있다.

  • ex 1) Spring Security 설정시 참고한 레퍼런스(ChadDa - jwtUtilvalidateToken 메서드)
    public String validateToken( String token) {
          try {
              Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
              return TokenProperties.VALID;
          } catch (ExpiredJwtException e) {
              return TokenProperties.EXPIRED;
          } catch ( JwtException | IllegalArgumentException | NullPointerException e) {
              return TokenProperties.INVALID;
          }
      }
  • ex2) 작성한 코드 -> 해당 사항 없음

적용해볼 해결 방법

  1. ExceptionHandlingFilter를 별도로 하나 만들어 모든 Exception들을 핸들링할 수 있도록 한다.
  2. Spring Security 내에서 try ~ catch를 통해 에러를 컨트롤한다.

고민 및 풀리지 않는 의문점

  • Spring Security에서는 에러가 나지 않고, 오로지 Service에서 발생하는 오류인데 왜 Spring Security의 영향을 받아 그 방식대로 오류를 타는가? 왜 인증/인가 관련 오류로 한 번 더 래핑되는가??

    👉🏻 RuntimeException을 상속한 커스텀 Exception을 스프링 시큐리티에서 인증/인가 관련 exception으로 다뤄주고 있을 확률 있음

  • 왜 Spring Security를 타고 나면 stackTrace가 추가되는가???

    👉🏻 이 부분은 발견하지 못함. 심지어 application.properties에서 server.error.include-stacktrace=never를 굳이 설정해주어도 소용 없음. 어디선가 직접 친절히 추가하는 것으로 보이는데, 아직 해당 부분을 발견하지 못했다.

추가로 해보면 좋을 것

  • 오류 발생시 함께 나오는 stackTrace를 따라 한 번 스프링 시큐리티를 뜯어보면 어디서 문제가 발생하는지, 어떻게 이 에러가 타고 온 건지 확인할 수 있을지도!!?!1

🧨 문제의 진짜 원인

ErrorResponseJwtUtil을 거치며 String으로 바뀌며 무언가 추가로 붙는 줄 알았는데, 디버깅해보니 이미 ErrorResponse가 생성될 때부터 property로 stackTrace를 비롯한 이것 저것 생기더군요. 엥 이게 뭐지? 해서 ErrorResponse가보니.. ErrorResponse 클래스가 RuntimeException을 상속받게 하는, 어이없는 실수 때문이었습니다.

변경 전

@Getter
@Builder
@AllArgsConstructor
public class ErrorResponse extends RuntimeException {
    private String code;
    private String message;

    public ErrorResponse(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
    }
}

변경 후

@Getter
@Builder
@AllArgsConstructor
public class ErrorResponse {
    private String code;
    private String message;

    public ErrorResponse(ErrorCode errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
    }
}

💡 What I Learned

디버깅을 했을 때, 제대로 꼼꼼히 보지 않아서 (Ex. ErrorResponse의 property들까지 확인해보지 않아서) 오류의 발생 구역을 아예 잘못 파악했고, 그래서 헛삽질 하는 기간이 아주 길었다.

사실 아직 디버깅으로 오류를 찾는 방법에 익숙하지 못한 것 같다. 익숙해져야 하겠다.

0개의 댓글