[F-Lab 모각코 챌린지 60일차] 로그 관리, Exception 정보

부추·2023년 7월 30일
0

F-Lab 모각코 챌린지

목록 보기
60/66

와!! 벌써 애프랩 60일차!! 뭐했죠?


1. 로그 관리

프로그램 동작 중 던져진 Exception을 관리하기 위해 아래와 같은 ExceptionHandler를 만들었다. 뭐가 문제인지 보이면 당신은 고수!

@RestControllerAdvice
@Slf4j
public class UserExceptionHandler {
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ExceptionResponse> handleUserNotFoundException(
            UserNotFoundException e) {
        log.error("UserNotFoundException for key {}", e.getKey());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ExceptionResponse("존재하지 않는 유저에 대한 요청입니다."));
    }

    @ExceptionHandler(InvalidUserRoleException.class)
    public ResponseEntity<ExceptionResponse> handleInvalidUserRoleException(
            InvalidUserRoleException e) {
        log.error("InvalidUserException for role {}", e.getRequiredRole());
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(new ExceptionResponse("권한이 없는 요청입니다."));
    }
}

UserExceptionHandler는 총 두 개의 예외를 관리하고 있다. 특정 key(Id 혹은 Email)로 유저를 찾았을 때, 해당 유저가 존재하지 않을 때 던지는 UserNotFoundException, 그리고 권한이 없는 유저가 요청을 날렸을 경우 던져지는 InvalidUserRoleException이다.

여기까진 ok이다.

일반적으로 예외가 던져지면 해당 예외의 정보를 로그로 찍어 남긴다. 트러블 슈팅을 위함인데, 따라서 예외를 던질 땐 해당 예외를 처리할 수 있도록 정보를 잘 남기는 것이 필요하다. (이와 관련해선 2번에 추가)


우리가 서비스를 운영하다보면 로그를 통해 개발자가 알람을 받아보도록 구성해야할 수도 있는데요, 가급적 개발자가 개입해줘야할만한 예외에 대해서만 error 레벨의 로그로 찍는게 좋습니다.

위대하신 멘토님 가로되, 로그는 운영의 편리함을 위함이라고 한다. 프로그램의 로그가 갑자기 많이 쌓였다면 시스템의 이상이 생겼을 확률이 크다. 심각한 오류일 수록 개발자가 빨리 알람을 받아야 하고, 그러기 위해선 "심각한 로그"와 "심각하지 않은 로그"의 단계를 나눠 해당 로그들이 얼마나 올라왔는지 모니터링하고, 평소와 다른 행보를 보였을 경우 이를 알 수 있게 알림을 보내는 일이 필요하다.

그런데 위의 코드 UserNotFoundExceptionInvalidUserRoleException은 시스템 운영단의 문제라기보단, 잘못된 사용자의 요청이다. 각 예외의 이름을 보면 예상 가능하듯 UserNotFoundException의 경우 특정 이메일이나 userId로 유저를 찾았을 때 존재하지 않는 유저일 경우 던져지는 예외이고, InvalidUserRoleException은 권한을 가지지 못한 유저가 어떠한 행위를 했을 때 던져지는 예외이다. 단순히 URL을 잘못 입력했거나, 로그인에 실패했거나, 권한이 없는 유저가 페이지에 접근했을 뿐인 "유저 실수"의 예외에 log.error 레벨의 에러 로그를 찍어버리면 더 fatal한 에러에 대한 로그와 구분하기 힘들어진다.

따라서 그렇게 중요해보이지 않는 4xx 에러에 대해선 그냥 log.warn선에서 마무리했다.

@RestControllerAdvice
@Slf4j
public class UserExceptionHandler {
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ExceptionResponse> handleUserNotFoundException(
            UserNotFoundException e) {
        log.warn("UserNotFoundException for key {}", e.getKey());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ExceptionResponse("존재하지 않는 유저에 대한 요청입니다."));
    }

    @ExceptionHandler(InvalidUserRoleException.class)
    public ResponseEntity<ExceptionResponse> handleInvalidUserRoleException(
            InvalidUserRoleException e) {
        log.warn("InvalidUserException for role {}", e.getRequiredRole());
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(new ExceptionResponse("권한이 없는 요청입니다."));
    }
}

그건 그렇고.. 저 많은 코드 중에서 log.error의 "ERROR"을 캐치한 멘토님도 정말 대단하다;;

로그 모니터링과 관련한 블로그 글에서도 간단하게 각 로그레벨에 대해 어떤 상황에서 해당 레벨을 쓰면 좋을지 정리되어있다.

  • DEBUG : 개발 혹은 테스트 단계에서 해당 기능들이 올바르게 작동하는지 확인하기 위한 로그 레벨. 운영 환경에서 노출되지 않는다.
  • INFO : 정상 작동에 대한 정보 즉, 어떤 일이 발생했음을 나타내는 표준 로그 레벨으로, 시스템을 파악하는데 유익한 정보여야만 한다. (쓸데없이 로그파일에 많은 기록이 남으면 데이터 낭비)
  • WARN : 애플리케이션에서 잠재적으로 문제가 될 수 있는 상황일때 남기는 로그 레벨
  • ERROR : 애플리케이션에서 발생한 심각한 오류나 예외 상황을 나타내는 로그 레벨. (조심해서 사용해라!!!!)

블로그 글엔 개략적으로 각 단계의 로그가 얼마나 찍혔는지에 따라 알림을 보낼지 여부를 결정하는 기준도 나와있다!
더더욱.. ERROR 사용을 자제해야겠다는 생각이 든다.

'시스템 <-> 시스템'처럼 내부 시스템간 통신에서 비슷한 문제가 벌어지는 경우에는 error 레벨로 간주하는게 더 적절한 상황도 있습니다. 이건 사용자가 요청하는게 아니라 내부적인 결함이 있을 수도 있기 때문이에요.

이런 상황은 또 언제를 말하는걸까? 바로..

@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionResponse> handleGlobalException(Exception e) {
    log.error("Unknown error ", e);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ExceptionResponse(
                    e.getMessage()));
}

이때다. 이 위에 SpringMvc와 관련한 Exception을 다 받고있기 때문에, 여기서 받는 "Unknown error"는 진짜로 뭔지 알 수 없는 종류의 에러이다.



2. Exception 정보

기존의 InvalidUserRoleException은 아래와 같았다. (1번 토픽의 예외 클래스와 같다)

@AllArgsConstructor
@Getter
public class InvalidUserRoleException extends RuntimeException {
    private Role requiredRole;
}

그렇기 때문에 로그에 관한 정보도 requiredRole과 관련해서만 찍혔다.


이때 위대하신 멘토님의 코멘트.

문제가 발생했을 때 트러블 슈팅에 도움이 될만한 정보가 어떤건지 고민해보시고, 그런 정보를 Exception에 파라미터로 넘겨줘서 로그로 찍는게 좋습니다!

그렇다. 위의 정보만으론, 누가 Role에 맞지 않는 요청을 날리고 있는지? 그 유저가 갖는 Role이 무엇인지? 알 수가 없다.

게다가 저렇게 불친절하게 예외를 던져놓으면 트러블 슈팅의 난이도도 굉장히 올라간다. 어떻게 해야 트러블 슈팅에 도움이 될지, 그 정보가 무엇일지 고민하면서 예외를 던지자!

InvalidUserRoleException은 앞서 언급했던 것처럼 "누가 Role에 맞지 않는 요청을 날리고 있는지", "그 유저가 갖는 Role이 무엇인지" 정보를 추가할 수 있다. 예외 클래스가 아래와 같이 바뀐다.

@AllArgsConstructor
@Getter
public class InvalidUserRoleException extends RuntimeException {
    private Long userId;
    private Role currentRole;
    private Role requiredRole;
}

그리고 ExceptionHandler도 아래와 같이 바뀐다.

@ExceptionHandler(InvalidUserRoleException.class)
public ResponseEntity<ExceptionResponse> handleInvalidUserRoleException(
        InvalidUserRoleException e) {
        
    log.warn("InvalidUserRoleException!! - " +
            "userId: {}, currentRole: {}, requiredRole: {}",
            e.getUserId(), e.getCurrentRole(), e.getRequiredRole());

    return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(new ExceptionResponse("권한이 없는 요청입니다."));
}

이제 로그를 보면 어떤 유저가 어째서 접근 denied가 떴는지 알 수 있다.

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

1개의 댓글

comment-user-thumbnail
2023년 7월 30일

글 잘 봤습니다.

답글 달기