저는 그동안 메서드 내부에서 try-catch 블록을 사용하여 각 예외를 개별적으로 처리했습니다. 예를 들어, 댓글 업데이트 기능에서 발생할 수 있는 다양한 예외를 직접 잡아 각각에 맞는 응답을 반환했습니다.

ex)

@PatchMapping
public ResponseEntity<?> updateOrDeleteComment(
        @Validated(UpdateGroup.class) @RequestBody CommentRequest commentRequest) {

    try {
        CommentResponse updatedComment = commentService.updateComment(commentRequest);
        return ResponseEntity.ok(updatedComment);
    } catch (NoSuchElementException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("댓글을 찾을 수 없습니다: " + e.getMessage());
    } catch (IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("잘못된 요청: " + e.getMessage());
    } catch (DataAccessException e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("데이터베이스 접근 오류: " + e.getMessage());
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("요청 처리 오류: " + e.getMessage());
    }
}

 

이 접근법은 직관적이고 간단한 경우에는 효과적일 수 있으나, 점점 예외 처리 로직이 여러 군데에서 중복되기 시작하고, 가독성 좋은 코드와 거리가 멀어졌습니다.

이런 문제를 1편에서 한번 출현했던 @ControllerAdvice를 이용한 전략으로 해결하고자 했습니다.

@ControllerAdvice를 사용하면 애플리케이션 전반에 걸쳐 발생할 수 있는 예외를 중앙에서 관리하고 처리할 수 있으므로, 코드의 중복을 줄이고 일관된 예외 처리 정책을 구현할 수 있습니다.

참고로 예외 핸들러는 주로 발생한 예외에 대응하여 적절한 HTTP 응답을 생성하고 로깅하는 역할을 합니다.

트랜잭션의 롤백 등은 일반적으로 서비스 레이어의 메서드에 적용됩니다.

다음은 구현 예제입니다.

 

@ControllerAdvice
public class GlobalExceptionHandler {

    private final EmailService emailService;

    public GlobalExceptionHandler(EmailService emailService) {
        this.emailService = emailService;
    }

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // 벨리데이션 관련 예외 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.badRequest().body("입력값 검증에 실패했습니다: " + errors);
    }

    //noSuch 예외처리
    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<Object> handleNoSuchElementException(NoSuchElementException ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", "요청한 항목을 찾을 수 없습니다.");

        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }

    //데이터베이스 관련 예외처리
    @ExceptionHandler(DataAccessException.class)
    public ResponseEntity<Object> handleDatabaseException(DataAccessException ex, WebRequest request) {
        String errorId = UUID.randomUUID().toString();
        log.error("Error ID: {}, Request URL: {}, Database operation exception occurred - Exception: {}, Message: {}", errorId, request.getDescription(false), ex.getClass().getName(), ex.getMessage());

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("errorId", errorId);
        body.put("message", "데이터베이스 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");

        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    //입출력 에러 관련 예외처리
    @ExceptionHandler(IOException.class)
    public ResponseEntity<Object> handleIOException(IOException ex, WebRequest request) {
        String errorId = UUID.randomUUID().toString();
        log.error("Error ID: {}, Request URL: {}, I/O exception occurred - Exception: {}, Message: {}", errorId, request.getDescription(false), ex.getClass().getName(), ex.getMessage());

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("errorId", errorId);
        body.put("message", "파일 입출력 중 오류가 발생했습니다.");

        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    //메모리 부족에 대한 처리
    @ExceptionHandler(OutOfMemoryError.class)
    public ResponseEntity<Object> handleOutOfMemoryError(OutOfMemoryError error, WebRequest request) {
        String errorId = UUID.randomUUID().toString();
        log.error("Error ID: {}, Request URL: {}, System out of memory - Error: {}", errorId, request.getDescription(false), error.getMessage());

        emailService.sendErrorEmail("시스템 메모리 부족", "시스템에 메모리가 부족합니다. 즉각적인 조치가 필요합니다.");

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("errorId", errorId);
        body.put("message", "시스템 오류가 발생했습니다. 관리자에게 문의해주세요.");

        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }


    // 처리되지 않은 모든 예외에 대한 핸들러
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllUncaughtException(Exception ex, WebRequest request) {
        String errorId = UUID.randomUUID().toString();
        log.error("Error ID: {}, Request URL: {}, An unexpected error occurred - Exception: {}, Message: {}", errorId, request.getDescription(false), ex.getClass().getName(), ex.getMessage());

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("errorId", errorId);
        body.put("message", "알 수 없는 오류가 발생했습니다. 지원팀에 문의해주세요.");

        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 

코드는 크게 설명 드릴게 없는 것 같습니다. 원하는 예외를 잡아서, 로그를 남기고, 사용자에게 전달할 메세지를 정의합니다. 저는 빠른 오류 해결을 위해, UUID로 고유한 에러 번호를 만들어서 사용자에게 전달하고 로그에 남겼습니다. 이렇게하면 에러ID로 어떤 현상이 일어났었는지 금방 매치할 수 있을 것입니다. 그리고 어떤 요청에서 에러가 발생하는지(URL) 남기는 것도 하나의 노하우일 수 있을 것입니다.  또, 메모리 부족 등 크리티컬한 에러에 관해서는 저에게 메일로 알림을 보내게 하였습니다. 이제 처음의 제 updateOrDeleteComment메서드는 이렇게 변하게 되었습니다.

 

@PatchMapping
public ResponseEntity<?> updateOrDeleteComment(
        @Validated(UpdateGroup.class) @RequestBody CommentRequest commentRequest) {
        CommentResponse updatedComment = commentService.updateComment(commentRequest);
        return ResponseEntity.ok(updatedComment);
}

 

가독성이 좋아짐과 동시에 좀더 역할에 충실한 느낌인 것 같습니다.

 

 

+ Recent posts