목표
안정성있고 유연한 예외처리를 가진 애플리케이션 설계 방법에 대한 방법을 고민하고 정리한다.
예외(Exception)
Java에서 예외는 비정상적인 상황을 나타내는 객체이며, 다음과 같이 Throwable 클래스 하위의 다음 두가지로 나뉜다.
Checked Exception
컴파일 시점에 처리를 강제하는 예외이다.
만약 해당 예외에 대해 try-catch, throws로 처리되어 있지 않다면 컴파일 시점에 에러가 발생한다.
반드시 예외처리를 해야하며, 개발자에게 안정성을 강제한다.
Unchecked Exception
런타임 시점에 발생하는 예외이다.
컴파일러가 예외처리를 강제하지 않기때문에, 예외처리를 강제하지 않아도 된다.
컴파일 시점에는 에러가 발생하지 않으나, 런타임 시점에 해당 예외처리가 되어있지 않다면 에러가 발생한다.
발생을 예측하기 어렵거나, 프로그래밍의 논리적 오류를 나타낼때 사용한다.
예외에 대한 처리에 대한 문법을 Java에서는 try-catch-finally, throw로 제공하고 있다.
예외처리 전략에 관한 정리글이기에 문법은 자세히 다루지 않는다.
예외 처리 기본 원칙
첫째, 구체적인 예외를 잡아야 한다.
이는 예외의 원인을 명확히 파악하고 적절한 처리를 할 수 있다.
둘째, 예외를 무시하지 않아야 한다.
예외가 발생했을 때 적절한 처리를 하지 않고 넘어가면 프로그램의 안정성이 저하된다.
셋째, 예외를 남발하지 않아야 한다.
예외는 정말 예외적인 상황에서만 사용해야 한다. 예외 처리는 비용이 많이 드는 작업이다.
넷째, 예외 메시지는 가능한 한 유용하게 작성해야 한다. 예외 메시지에는 예외의 원인과 해결 방법에 대한 정보가 포함되어야 한다.
예외 처리 전략
첫째, 예외를 로깅해야 한다.
예외가 발생했을 때 로그를 남기면 문제의 원인을 파악하고 해결하는데 도움이 된다.
둘째, 커스텀 예외 클래스를 사용한다.
비즈니스 로직에 맞는 커스텀 예외를 작성하면 예외의 의미를 명확히 할 수 있다.
셋째, 애플리케이션 전역에서 예외를 처리할 수 있도록 @ControllerAdvice와 @ExceptionHandler를 활용한다.
넷째, 예외 발생 시 적절한 수준(ERROR, WARN, INFO)으로 로깅하여 문제를 추적 가능하게 한다.
다섯째, 예외 발생 상황에 대한 모니터링과 알림 설정을 한다.
여섯째, 예외 처리시 불필요한 try-catch를 줄이고, 가능한 한 높은 계층에서 Exception을 처리한다.
더불어 적절한 HTTP 상태 코드를 반환하여 클라이언트가 응답을 이해할 수 있게 한다.
또한 민감한 정보(예: 비밀번호, API 키)는 예외 메시지에 포함하지 않는다.
예외 처리 전략에 따른 예제 코드
1. 비즈니스 로직에 맞는 커스텀 Exception 클래스를 사용한다.
더불어 예외에는 명확한 원인과 에러 해결에 도움이 될 수 있는 정보를 담는다.
공통 Exception 클래스
공통 Exception 클래스는 예외 처리에 대한 일관성을 높이고 유지보수를 쉽게 하기 위해 중요하다.
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
private final HttpStatus httpStatus; // Http 상태값
private final String errorCode; // 에러 코드
private final String errorMessage; // 에러 메시지
public CustomException(ApiExceptionCode errorCode) {
this.httpStatus = errorCode.getHttpStatus();
this.errorCode = errorCode.getErrorCode();
this.errorMessage = errorCode.getErrorMessage();
}
}
Enum 기반 에러 코드 관리
위 CustomException 클래스는 ApiExceptionCode라는 Enum 기반으로 Exception 객체를 생성하고 있다.
매 Exception 마다 errorCode, errorMessage를 임의로 지정하기보다 미리 Enum 기반으로 Exception 객체를 생성하면 일관성과 재사용성 그리고 좋은 가독성을 가질 수 있다.
API Exception Enum
@Getter
@AllArgsConstructor
public enum ApiExceptionCode {
INVALID_INPUT("4001", "Invalid input provided.", HttpStatus.BAD_REQUEST),
NOT_FOUND("4041", "Resource not found.", HttpStatus.NOT_FOUND),
SERVER_ERROR("5001", "Internal server error.", HttpStatus.INTERNAL_SERVER_ERROR);
private final String code; // 에러 코드
private final String message; // 에러 메시지
private final HttpStatus httpStatus; // HTTP 상태 코드
}
2. 예외 처리시 가능한 try-catch를 줄이고 가장 높은 계층에서 예외를 처리한다.
try-catch를 줄이고 가장 높은 계층에서 예외를 처리하기 위해서는 @ControllerAdvice, @ExceptionHandler 를 이용하면 애플리케이션 전역에서 발생한 Exception에 대해 최상단에서 공통 처리 해줄 수 있다.
이를 위한 GlobaclExceptionHandler 클래스를 만들고 Exception 내용을 공통 에러 응답 클래스(CommonExceptionResponse.class)에 매핑하여 클라이언트에게 전달한다.
공통 에러 응답 클래스
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(staticName = "of")
public class ErrorResponse {
private final String errorCode;
private final String errorMessage;
}
전역 Exception Hanldler 클래스
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
// 예외 로깅 (ERROR 레벨)
log.error("CustomException occurred: ErrorCode = {}, Message = {}", ex.getErrorCode(), ex.getErrorMessage(), ex);
ErrorResponse errorResponse = ErrorResponse.of(ex.getErrorCode(), ex.getErrorMessage());
return new ResponseEntity<>(errorResponse, ex.getHttpStatus());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
// 예외 로깅 (ERROR 레벨)
log.error("Unexpected error occurred: {}", ex.getMessage(), ex);
ErrorResponse errorResponse = ErrorResponse.of("UNKNOWN_ERROR", "An unexpected error occurred");
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
추가적으로, 에러 응답에 StackTraceID값을 함께 전달해주면 클라이언트에서 에러를 인지하고 문제를 전달할 때 TraceID를 기반으로 문제를 빠르게 식별하는데 유용하게 사용할 수 있다.
* RestControllerAdvice와 ControllerAdvice 어노테이션의 차이
특징 | @ControllerAdvice | @RestControllerAdvice |
응답 형식 | 뷰 이름, ModelAndView, 또는 HTTP 응답 | 객체(JSON/XML 등) 응답 |
포함 기능 | @ResponseBody 없음 | @ResponseBody 있음 |
사용 목적 | HTML 뷰 기반 응답 (전통적인 MVC) | RESTful API 응답 |
주된 사용 사례 | 웹 페이지 렌더링 컨트롤러 예외 처리 | JSON 형식의 API 에러 처리 |
3. 예외를 적절한 레벨로 로깅하며, 모니터링과 더불어 알림처리를 통해 개발자가 인지할 수 있도록 처리한다.
Slf4j를 이용하여 ERROR 레벨로 로깅될 수 있도록 처리한다.
모니터링 툴에서는 ERROR 레벨 로그를 통해 모니터링 되도록 처리할 수 있으며, 특정 키워드를 기반으로 정해진 메신저툴로 에러 발생 알림을 보내도록 후속 처리를 진행한다.
마무리
Spring에서의 효율적인 예외 처리는 명확한 계층 구조, 전역 처리, 로깅, 사용자 친화적인 응답, 그리고 모니터링의 조화로운 활용에 달려 있다, 이를 통해 애플리케이션의 안정성과 가독성을 극대화 할 수 있다.
'Spring' 카테고리의 다른 글
[Spring] 카카오 로그인 REST API 방식 적용 및 구현 (0) | 2024.11.26 |
---|---|
[Spring] 스프링 부트(Spring Boot) - H2 DB 연동하여 사용하기(with.JPA) (0) | 2024.11.22 |
[Spring] Junit 테스트시 Atomikos 커넥션풀의 Connection Name 중복 오류 (0) | 2024.07.08 |
IntelliJ에서 Spring MVC 세팅하기 (0) | 2024.03.17 |
[Spring] Spring Boot 3 마이그레이션 시작하기 (0) | 2023.12.13 |