저는 현재 회사에서 WebFlux 를 활용한 PG 도메인 프로젝트를 진행하며, 커스텀 에러를 사용하고 있습니다.
이번 글에서는 WebFlux 뿐만 아니라 MVC 환경에서도 적용 가능한 커스텀 에러 설계 및 사용 방법을 공유하고자 합니다.
중간에 WebFlux 문법이 등장할 수 있지만, 사용된 어노테이션은 MVC 환경에서도 적용 가능하니 참고해 주세요 ☺️
🪴 들어가기 전
커스텀 에러에 대해 소개하기 전에 이 글에서 다뤄질 에러 종류에 대해 정리하고 가겠습니다.
이 글에서 언급하는 에러는 크게 HTTP 통신 에러와 애플리케이션 단계의 에러 두 가지로 나눌 수 있습니다.
- HTTP 통신 에러는 클라이언트가 직접적으로 받게 되는 에러로, 서버에서 응답한 HTTP 상태 코드와 메시지를 통해 전달됩니다.
- 애플리케이션 단계의 에러는 서버 내부 로직에서 발생하는 에러로, 주로 서버에서 확인하고 처리해야 하는 에러입니다.
🪴 커스텀 에러
커스텀 에러는 애플리케이션 내부 로직에서 발생하는 특정 예외를 처리하기 위해 개발자가 정의한 사용자 맞춤형 예외입니다.
이를 통해 클라이언트에게 더 정확한 에러 원인을 제공할 수 있어서 에러 처리의 효율성이 높아집니다.
예를 들어 이미 가입된 회원에 대해 단순히 HTTP 400 코드와 "잘못된 요청" 메시지를 반환하는 대신에
아래와 같이 클라이언트에게 더 구체적인 에러 메시지를 전달할 수 있습니다
// 400 Bad Request
MEMBER_ALREADY_EXIST(HttpStatus.BAD_REQUEST.value(), "이미 존재하는 회원입니다.")
🪴 전체적인 코드
1. ErrorCodes
- 커스텀 에러 코드와 메시지를 정의하는 곳입니다.
public enum ErrorCodes {
//400
MEMBER_ALREADY_EXIST(HttpStatus.BAD_REQUEST.value(), "이미 존재하는 회원입니다."),
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 회원입니다. 회원가입 진행해주세요."),
INVALID_MEMBER_LOGIN_INFO(HttpStatus.BAD_REQUEST.value(), "아이디 또는 비밀번호가 올바르지 않습니다."),
//403
FORBIDDEN(HttpStatus.FORBIDDEN.value(), "접근할 수 없습니다."),
//500
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 내부 오류입니다.");
private final int httpStatusCode;
private final String message;
ErrorCodes(int httpStatusCode, String message) {
this.httpStatusCode = httpStatusCode;
this.message = message;
}
public int httpStatusCode() {
return httpStatusCode;
}
public String message() {
return message;
}
@Override
public String toString() {
return message;
}
}
2. CustomException
- 커스텀 에러를 던질 수 있도록 RuntimeException을 확장한 예외 클래스입니다.
public class CustomException extends RuntimeException{
private final int httpStatusCode;
private final String body;
private final String message;
public CustomException(ErrorCodes errorCode){
super(errorCode.message());
this.httpStatusCode = errorCode.httpStatusCode();
this.body = errorCode.name();
this.message = errorCode.message();
}
public CustomException(ErrorCodes errorCode, String message){
super(errorCode.message());
this.httpStatusCode = errorCode.httpStatusCode();
this.body = errorCode.name();
this.message = message;
}
public CustomException(int httpStatusCode, String body, String message) {
this.httpStatusCode = httpStatusCode;
this.body = body;
this.message = message;
}
public int httpStatusCode() {
return httpStatusCode;
}
public String body() {
return body;
}
public String message() {
return message;
}
}
3. GlobalExceptionHandler
- 애플리케이션 전역에서 발생한 커스텀 예외를 처리하는 핸들러 클래스입니다.
@ControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ResultData<String>> handleCustomException(CustomException e){
return ResponseEntity.status(e.httpStatusCode())
.body(ResultData.<String>builder()
.body(e.body())
.message(e.message())
.build()
);
}
}
- @ControllerAdvice
- @ControllerAdvice는 전역적으로 예외를 처리할 수 있도록 하는 어노테이션입니다. 이를 사용하면 특정 예외를 애플리케이션 전역에서 처리할 수 있으며 여러 컨트롤러에서 발생하는 예외를 하나의 클래스로 처리할 수 있습니다.
- @ExceptionHandler
- @ExceptionHandler는 지정된 예외가 발생했을 때 해당 예외를 처리하는 메서드를 정의하는데 사용합니다.
4. 사용예시
//비밀번호 재설정 예시코드
public Mono<ResultData<ResultCodes>> resetPassword(
PasswordResetRequestDto requestDto,
ServerWebExchange exchange
) {
return memberRepository.updateMemberPassword(requestDto.memberId(), bCryptPasswordEncoder.encode(requestDto.newPassword()), exchange)
.switchIfEmpty(Mono.error(new CustomException(ErrorCodes.MEMBER_NOT_FOUND)))
.map(result -> new ResultData<>(ResultCodes.SUCCESS, "비밀번호가 재설정되었습니다."));
}
🪴 에러 처리 흐름
위의 코드에서 GlobalExceptionHandler 클래스가 존재하지 않는다면 모든 에러는 500 에러로 통신하게 됩니다.
핸들러가 존재하지 않으면 Spring 에서는 예외 발생 시, 자동으로 500 Internal Server Error를 반환하기 때문입니다.
따라서 커스텀 에러를 만들고자 할 때에는, @ControllerAdvice와 @ExceptionHandler 를 이용해서 예외 처리에 대한 핸들러 작업을 필수로 해주어야합니다.
스프링 프레임워크에서의 에러 흐름을 정리하면 아래와 같습니다.
- HTTP Client 에서 요청 시작
- DispatcherServlet 을 통한 요청 라우팅
- Controller 와 Service 처리
- 예외 발생 시 Global ExceptionHandler 에서 처리
- 커스텀 또는 기본 예외에 따른 응답 처리
- 최종 응답 HTTP Client 로 반환
'SpringBoot' 카테고리의 다른 글
[Spring] Spring Bean 생성 주기 및 어노테이션 비교 (1) | 2023.05.03 |
---|---|
[Spring] @Scheduled 스케쥴링 적용 방법 (0) | 2023.03.08 |
[Spring] Hikari 이용하여 데이터베이스 연결 관리하기 (0) | 2023.03.02 |
[Spring] Jpa 이용해서 db 에 entity 추가 (0) | 2023.01.18 |
[Spring] Spring 환경 IntelliJ에 구축 (0) | 2023.01.11 |