Spring Boot에서 예외처리 할 때에는 단순하게 @ExceptionHandler에서 로직을 다 구현하여 처리해도 되지만 Front-End와 협업을 해야하는 입장에서 Error Message는 예쁘게, 담아야 하는 내용이 다 담겨있게 만들 수록 좋다.
나름 예쁘게 나올 수 있도록 코드를 순서대로 작성해 보았다.
1. Error 코드를 Enum으로 정의
전역적으로 사용되는 GlobalErrorCode와 특정 도메인에 대해 구체적으로 내려가는 UserErrorCode로 나누고, 인터페이스를 이용해 추상화한다.
public interface ErrorCode {
HttpStatus getHttpStatus();
String getMessage();
}
// Global Error Code Enum
@AllArgsConstructor
@Getter
public enum GlobalErrorCode implements ErrorCode{
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류가 발생하였습니다."),
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
;
private final HttpStatus httpStatus;
private final String message;
}
// User Error Code enum
@AllArgsConstructor
@Getter
public enum UserErrorCode implements ErrorCode{
NOT_FOUND_USER(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."),
;
private final HttpStatus httpStatus;
private final String message;
}
2. Error Response 객체를 생성
팀에 맞는 Error메시지를 생성한다.
record를 활용해서 작성해보았다.
// Error Response | record활용
@Builder
public record ErrorResponse(
// 에러 발생 시간
LocalDateTime timeStamp,
// 상태 코드
String statusCode,
// 상태 메시지
String statusName,
// 요청 uri
String requestUrl,
// 에러 메시지
String message,
// Field 에러 내용
List<ValidationError> validationErrorList
) {
// Field Valid Error
@Builder
public record ValidationError(String field, String message, String invalidValue) {
public static ValidationError of(final FieldError fieldError) {
return ValidationError.builder()
.field(fieldError.getField())
.message(fieldError.getDefaultMessage())
.invalidValue(Optional.ofNullable(fieldError.getRejectedValue())
.map(Object::toString)
.orElse("null"))
.build();
}
}
}
3. ErrorResponseService 인터페이스 생성
Error메시지를 내려주는 형태가 여러가지 있을 때, 알맞은 반환 방법을 정의한다.
// Error Service interface
public interface ErrorResponseService {
ErrorResponse failure(ErrorCode errorCode, List<ErrorResponse.ValidationError> errorMessage, String uri);
ErrorResponse failureWithNoContent(ErrorCode errorCode, String uri);
}
3-1. ErrorResponseServiceImpl 객체 생성
인터페이스에서 정의한 것을 토대로 구체화한다.
// Error Service impl
@Service
public class ErrorResponseServiceImpl implements ErrorResponseService {
@Override
public ErrorResponse failure(ErrorCode errorCode, List<ErrorResponse.ValidationError> errorMessage, String uri) {
return ErrorResponse.builder()
.timeStamp(LocalDateTime.now())
.statusCode(errorCode.getHttpStatus().value() + "")
.statusName(errorCode.name())
.requestUrl(uri)
.message(errorCode.getMessage())
.validationErrorList(errorMessage)
.build();
}
@Override
public ErrorResponse failureWithNoContent(ErrorCode errorCode, String uri) {
return failure(errorCode, new ArrayList<>(), uri);
}
}
4. Custom Exception 생성(1)
이 클래스는 Custom Exception을 만들 때 ErrorCode의 get메서드를 강제해주기 위해 생성 하였다.
Custom Exception을 만들 때 GlobalException을 상속 받아서 사용하면 된다.
// GlobalException
@RequiredArgsConstructor
@Getter
public abstract class GlobalException extends RuntimeException {
private final ErrorCode errorCode;
}
Custom Exception 생성(2)
위에서 만든 GlobalException을 상속받아서 실질적으로 사용한 CustomException을 생성하였다.
// UserException
@Getter
public class UserException extends GlobalException{
private final ErrorCode errorCode;
public UserException(ErrorCode errorCode) {
super(errorCode);
this.errorCode = errorCode;
}
}
5. Exception 처리
@RestControllerAdvice를 사용하여 발생한 Exception들을 처리한다.
// RestControllerAdvice
@RequiredArgsConstructor
@RestControllerAdvice(basePackageClasses = RestApiController.class) // RestApiController에 대해서만
public class ApiControllerAdvice {
// DI
private final ErrorResponseService responseService;
// Global Exception
@ExceptionHandler(value = Exception.class)
public ResponseEntity<?> exception(Exception e, HttpServletRequest request) {
return returnDetails(INTERNAL_SERVER_ERROR, request.getRequestURI());
}
// UserException
@ExceptionHandler(value = UserException.class)
public ResponseEntity<?> userNotFoundException(UserException e, HttpServletRequest request) {
return returnDetails(e.getErrorCode(), request.getRequestURI());
}
// Validation Exception
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<?> methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
return returnDetails(INVALID_REQUEST, e, request.getRequestURI());
}
// content없는 Error ResponseEntity 생성
private ResponseEntity<?> returnDetails(ErrorCode errorCode, String url) {
ErrorResponse failure = responseService.failureWithNoContent(errorCode, url);
return ResponseEntity.status(errorCode.getHttpStatus()).body(failure);
}
// content있는 Error ResponseEntity 생성
private ResponseEntity<?> returnDetails(ErrorCode errorCode, BindingResult bindingResult, String url) {
List<ErrorResponse.ValidationError> validationErrors =
bindingResult.getFieldErrors()
.stream()
.map(ErrorResponse.ValidationError::of)
.toList();
ErrorResponse failure = responseService.failure(errorCode, validationErrors, url);
return ResponseEntity.status(errorCode.getHttpStatus()).body(failure);
}
}
6. Exception 결과
@RestController
@RequestMapping("/api")
@Validated
public class RestApiController {
@GetMapping("/user/{id}")
public String getUser(@PathVariable Long id) {
if(id != 1){
throw new UserException(NOT_FOUND_USER);
}
return id + "";
}
/*
{
"timeStamp": "2024-06-10T03:28:41.3866596",
"statusCode": "404",
"statusName": "NOT_FOUND_USER",
"requestUrl": "/api/user/2",
"message": "유저를 찾을 수 없습니다.",
// 빈 배열 또한 중요한 정보가 될 수 있다고 생각해서 빈 배열로 리턴
"validationErrorList": []
}
*/
@PostMapping("/post")
public User post(@RequestBody @Valid User user) {
return user;
}
/*
{
"timeStamp": "2024-06-10T03:27:53.5418522",
"statusCode": "400",
"statusName": "INVALID_REQUEST",
"requestUrl": "/api/post",
"message": "잘못된 요청입니다.",
"validationErrorList": [
{
"field": "pw",
"message": "\"^/d$\"와 일치해야 합니다",
"invalidValue": "12d34"
}
]
}
*/
}
'Back-End > Spring Boot' 카테고리의 다른 글
| Spring Boot Interceptor란? (0) | 2024.06.17 |
|---|---|
| Spring Boot Filter란 (2) | 2024.06.16 |
| AOP (0) | 2024.06.08 |
| IoC와 DI (0) | 2024.06.08 |
| Spring Boot Custom Validation만들기 (0) | 2024.06.07 |