저는 그동안 메서드 내부에서 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);
}
가독성이 좋아짐과 동시에 좀더 역할에 충실한 느낌인 것 같습니다.