Chapter - 비즈니스 로직에 대한 예외 처리
이번 챕터에서는 서비스 계층에서 발생하는 비즈니스 로직의 상황에 맞게 개발자가 의도적으로 예외(Exception)를 던지는(throw) 방법에 대한 학습을 해보도록 하겠습니다.
그리고 이렇게 의도적으로 던져진 예외(Exception)를 API 계층에서 처리하는 방법 역시 학습해 보도록 하겠습니다.
학습 목표
- 서비스 계층에서 의도적으로 예외를 던지는 방법과 상황을 이해할 수 있다.
- 사용자 정의 예외(Custom Exception)를 만들 수 있다.
- 서비스 계층에서 던져진 예외를 API 계층에서 처리할 수 있다.
비즈니스적인 예외 던지기(throw) 및 예외 처리
체크 예외(Checked Exception)와 언체크 예외(Unchecked Exception)
애플리케이션에서 발생하는 예외(Exception)는 크게 체크 예외(Checked Exception)와 언체크 예외(Unchecked Exception)로 구분할 수 있습니다.
체크 예외는 말 그대로 발생한 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피하든가 등의 어떤 구체적인 처리를 해야 하는 예외입니다.
Java에서 흔히 볼 수 있는 대표적인 체크 예외로는 ClassNotFoundException 등을 들 수 있습니다.
반면에 언체크 예외는 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외를 의미합니다. 따라서 언체크 예외는 명시적으로 잡아서(catch) 어떤 처리를 할 필요가 없습니다.
대표적인 언체크 예외로는 NullPointerException, ArrayIndexOutOfBoundsException 등이 있습니다.
흔히 개발자가 코드를 잘못 작성해서 발생하는 이런 오류들은 모두 RuntimeException을 상속한 예외들입니다.
그런데 Java나 Spring에서 수많은 RuntimeException을 지원해 주지만 이 RuntimeException을 이용해서 개발자가 직접 예외(Exception)를 만들어야 할 경우도 있습니다.
체크 예외와 언체크 예외에 대해서 더 알아보고 싶다면 [심화 학습]을 참고하세요.
개발자가 의도적으로 예외를 던질 수(throw) 있는 상황
개발자가 의도적으로 예외를 던져야 되는 경우는 어떤 것들이 있을까요?
몇 가지 예를 들어 보도록 하겠습니다.
- 백엔드 서버와 외부 시스템과의 연동에서 발생하는 에러 처리
여러분들이 암호 화폐 지갑과 연동하는 백엔드 서비스를 만드는 상상을 해 봅시다.
암호 화폐 지갑은 웹 지갑이 될 수도 있고, 안드로이드나 iOS 기반의 앱 지갑이 될 수도 있으며, 데스크톱 애플리케이션이 될 수도 있습니다.
일반적으로 암호 화폐 지갑은 블록체인과 직접적으로 API 통신을 거치는 경우가 많지만 백엔드 서버를 한 번 거쳐서 블록체인과 통신하는 경우도 있을 수 있습니다.
만일 A라는 사용자가 B라는 사용자에게 코인을 전송하기 위해 백엔드 서버가 블록체인과 API 통신을 하는 과정에서 블록체인으로부터 A 사용자의 코인 잔고가 부족하다는 메시지를 전달받고, 프로세스가 중단되었습니다.
백엔드 서버 쪽에서 이런 예외가 발생했다면 이 예외를 복구하려고 시도해 봤자 할 수 있는 게 없습니다.
이럴 땐 잔고가 부족한 상황을 클라이언트 쪽에 즉시 알려서 클라이언트가 지갑에 잔고를 채우는 게 최선의 방법입니다.
이 경우, 백엔드 서버 쪽에서 예외를 의도적으로 던져서 클라이언트 쪽에 에러가 발생한 정보를 알려줄 수 있습니다.
- 시스템 내부에서 조회하려는 리소스(자원, Resource)가 없는 경우
여러분들이 학습하고 있는 샘플 애플리케이션인 커피 주문 애플리케이션을 생각해 봅시다.
회원 정보를 조회하려고 클라이언트 쪽에서 Controller의 getMember() 핸들러 메서드에 요청을 보냈습니다. 그런데 DB에 조회를 하니 해당하는 회원 정보가 없을 수 있습니다.
이런 경우 서비스 계층에서 해당 회원 정보가 없다는 예외를 의도적으로 전송해서 클라이언트 쪽에 알려줄 수 있습니다.
의도적인 예외 던지기/받기(throw/catch)
그렇다면 어떻게 하면 예외를 의도적으로 던질 수 있을까요?
여러분들이 이미 알고 있다시피 Java에서는 throw 키워드를 사용해서 예외를 메서드 바깥으로 던질 수 있습니다.
던져진 예외는 메서드 바깥 즉, 메서드를 호출한 지점으로 던져지게 되는 것입니다.
서비스 계층에서 예외를 던지면 이 예외는 어디로 던져질까요?
네, 맞습니다. 서비스 계층의 메서드는 API 계층인 Controller의 핸들러 메서드가 이용하므로 서비스 계층에서 던져진 예외는 Controller의 핸들러 메서드 쪽에서 잡아서 처리할 수 있습니다.
그런데 우리는 이미 Controller에서 발생하는 예외를 Exception Advice에서 처리하도록 공통화해두었으니 서비스 계층에서 던진 예외 역시 Exception Advice에서 처리하면 됩니다.
✔ 서비스 계층에서 예외(Exception) 던지기(throw)
@Service
public class MemberService {
...
...
public Member findMember(long memberId) {
// TODO should business logic
// (1)
throw new RuntimeException("Not found member");
}
...
...
}
[코드 3-63] 서비스 계층에서 예외를 던지는(throw) 예
여러분들이 아직 데이터 액세스 계층을 학습하지 않았기 때문에 회원 정보를 DB에서 조회할 수 없습니다.
코드 3-63에서는 DB에서 회원 정보를 조회했는데 조회되는 회원이 없다고 가정하고, (1)에서 throw 키워드를 사용하여 RuntimeException 객체에 적절한 예외 메시지를 포함한 후에 메서드 밖으로 던졌습니다.
✔ GlobalExceptionAdvice 예외 잡기(catch)
이제 이렇게 던져진 RuntimeException을 우리가 이 전 챕터에서 구현한 GlobalExceptionAdvice 클래스에서 받아보겠습니다.
@RestControllerAdvice
public class GlobalExceptionAdvice {
...
...
// (1)
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleResourceNotFoundException(RuntimeException e) {
System.out.println(e.getMessage());
// ErrorResponse 수정은 실습 과제로 대신합니다.
return null;
}
}
[코드 3-64] 서비스 계층에서 던져진(throw) RuntimeException을 잡아서 처리하는 예
코드 3-64에서는 이 전 챕터에서 구현한 GlobalExceptionAdvice(v4)에 (1)과 같이 RuntimeException을 잡아서(catch) 처리하기 위한 handleResourceNotFoundException() 메서드를 추가했습니다.
Postman으로 MemberController의 getMember() 핸들러 메서드에 요청을 보내면 MemberService에서 RuntimeException을 던지고, GlobalExceptionAdvice의 handleResourceNotFoundException() 메서드가 이 RuntimeException을 잡아서 예외 메시지인 “Not found member”를 콘솔에 출력할 것입니다.
handleResourceNotFoundException() 메서드에서 클라이언트 쪽으로 에러 메시지를 전송하는 부분은 실습 과제에 포함을 하도록 하고 이번 챕터에서는 콘솔에 메시지만 출력하도록 하겠습니다.
✔ handleResourceNotFoundException() 메서드의 문제점
그런데 서비스 계층에서 의도적으로 던질 수 있는 예외는 회원 정보가 존재하지 않는 경우 한 가지뿐일까요?
- 회원 등록 시 이미 존재하는 회원일 경우
- 로그인 패스워드 검증에서 패스워드가 일치하지 않는 경우 등
이처럼 서비스 계층에서 의도적으로 던질 수 있는 예외 상황은 다양하게 존재할 수 있기 때문에 handleResourceNotFoundException()의 메서드 이름은 적절하지 않습니다.
또한 추상적인 RuntimeException을 그대로 전달받는 것 역시 바람직하지 않아 보입니다.
서비스 계층에서 RuntimeException을 그대로 던지고(throw), Exception Advice에서 RuntimeException을 그대로 잡는 것(catch)은 예외의 의도가 명확하지 않으며, 구체적으로 어떤 예외가 발생했는지에 대한 예외 정보를 얻는 것이 어렵습니다.
사용자 정의 예외(Custom Exception) 사용
앞에서 백엔드 서버와 블록체인 간의 통신에서 블록체인 쪽에서 잔고 부족으로 인한 에러 메시지를 백엔드 서버 쪽에 전송한 경우를 다시 생각해 봅시다.
이 경우, 서버 쪽에서는 RuntimeException과 같은 추상적인 예외가 아닌 InsufficentBalanceException 같은 해당 예외를 조금 더 구체적으로 표현할 수 있는 Custom Exception을 만들어서 예외를 던질 수 있습니다.
우리도 이런 Custom Exception을 샘플 애플리케이션에 적용해 보도록 하겠습니다.
✔ 예외 코드 정의
package com.springboot.exception;
import lombok.Getter;
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member Not Found");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int status, String message) {
this.status = status;
this.message = message;
}
}
[코드 3-65] 예외 코드 정의
먼저 코드 3-65와 같이 서비스 계층에서 던질 Custom Exception에 사용할 ExceptionCode를 enum으로 정의합니다.
이처럼 ExceptionCode를 enum으로 정의하면 비즈니스 로직에서 발생하는 다양한 유형의 예외를 enum에 추가해서 사용할 수 있습니다.
✔ BusinessLogicException 구현
package com.springboot.exception;
import lombok.Getter;
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
[코드 3-66] 서비스 계층에서 사용할 Custom Exception 정의
다음으로 코드 3-66과 같이 서비스 계층에서 사용할 BusinessLogicException이라는 Custom Exception을 정의합니다.
BusinessLogicException은 RuntimeException을 상속하고 있으며 ExceptionCode를 멤버 변수로 지정하여 생성자를 통해서 조금 더 구체적인 예외 정보들을 제공해 줄 수 있습니다.
그리고 상위 클래스인 RuntimeException의 생성자(super)로 예외 메시지를 전달해 줍니다.
BusinessLogicException은 서비스 계층에서 개발자가 의도적으로 예외를 던져야 하는 다양한 상황에서 ExceptionCode 정보만 바꿔가며 던질 수 있습니다.
✔ 서비스 계층에 BusinessLogicException 적용
이제 서비스 계층에서 이 BusinessLogicException을 사용하여 예외를 던져봅시다.
@Service
public class MemberService {
...
...
public Member findMember(long memberId) {
// TODO should business logic
// (1)
throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
}
...
...
}
[코드 3-67] 서비스 계층에서 Custom Exception 던지기
코드 3-63에서 RuntimeException을 던지던 것을 BusinessLogicException에 구체적인 예외 정보(ExceptionCode)를 던지도록 변경했습니다.
여기서는 회원 정보가 존재하지 않는다는 MEMBER_NOT_FOUND를 BusinessLogicException 생성자의 파라미터로 전달했습니다.
✔ Exception Advice에서 BusinessLogicException 처리
이제 서비스 계층에서 던진 BusinessLogicException을 Exception Advice에서 처리하면 됩니다.
@RestControllerAdvice
public class GlobalExceptionAdvice {
...
...
@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
System.out.println(e.getExceptionCode().getStatus());
System.out.println(e.getMessage());
// ErrorResponse 수정은 실습 과제로 대신합니다.
return new ResponseEntity<>(HttpStatus.valueOf(e.getExceptionCode().getStatus()));
}
}
[코드 3-68] BusinessLogicException을 처리하도록 수정된 GlobalExceptionAdvice(v5)
코드 3-64의 GlobalExceptionAdvice(v4)에서 변경된 부분은 다음과 같습니다.
- 메서드 명 변경
먼저 메서드 명이 서비스 계층의 비즈니스 로직 처리에서 발생하는 예외를 처리하는 것을 목적으로 하기 때문에 메서드 명이 handleBusinessLogicException으로 변경되었습니다.
- 메서드 파라미터 변경
RuntimeException을 파라미터로 전달받던 것을 BusinessLogicException을 전달받는 것으로 변경되었습니다.
- @ResponseStatus(HttpStatus.NOT_FOUND) 제거
@ResponseStatus 애너테이션은 고정된 HttpStatus를 지정하기 때문에 BusinessLogicException과 같이 다양한 Status를 동적으로 처리할 수 없으므로 ResponseEntity를 사용해서 HttpStatus를 동적으로 지정하도록 변경했습니다.
변경된 코드를 테스트하기 위해 Postman에서 MemberController의 getMember() 핸들러 메서드에 요청을 전송하면 다음과 같은 출력 결과를 콘솔에서 확인할 수 있습니다.
404
Member Not Found
이처럼 BusinessLogicException 클래스를 통해 전달받은 구체적인 예외 정보는 여러분들이 ErrorResponse에 적절히 포함해서 클라이언트의 응답으로 전달하면 됩니다.(실습 과제가 될 예정입니다. ^^)
@RestControllerAdvice에서 @ResponseStatus를 쓸까? ResponseEntity를 쓸까?
앞에서 잠깐 언급을 했지만 한 가지 유형으로 고정된 예외를 처리할 경우에는 @ResponseStatus로 HttpStatus를 지정해서 사용하면 되고, BusinessLogicException처럼 다양한 유형의 Custom Exception을 처리하고자 할 경우에는 ResponseEntity를 사용하면 됩니다.
핵심 포인트
- 체크 예외(Checked Exception)는 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피를 하든가 등의 어떤 구체적인 처리를 해야 하는 예외이다.
- 언체크 예외(Unchecked Exception)는 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외를 의미한다.
- RuntimeException을 상속한 예외는 모두 언체크 예외(Unchked Exception)이다.
- RuntimeException을 상속해서 개발자가 직접 사용자 정의 예외(Custom Exception)를 만들 수 있다.
- 사용자 정의 예외(Custom Exception)를 정의해서 서비스 계층의 비즈니스 로직에서 발생하는 다양한 예외를 던질 수 있고, 던져진 예외는 Exception Advice에서 처리할 수 있다.
- @ResponseStatus 애너테이션은 고정된 예외를 처리할 경우에 사용할 수 있다.
- HttpStatus가 동적으로 변경되는 경우에는 ResponseEntity를 사용한다.
심화 학습
- 체크 예외(Checked Exception)와 언체크(Unchecked Exception) 예외에 대해서 더 알아보고 싶다면 아래 링크를 참고하세요.
- https://docs.oracle.com/javase/specs/jls/se7/html/jls-11.html
- https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html
- https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/RuntimeException.html
- https://www.geeksforgeeks.org/checked-vs-unchecked-exceptions-in-java/
- https://rollbar.com/blog/how-to-handle-checked-unchecked-exceptions-in-java/
[실습] 애플리케이션 예외 처리 실습
서비스 계층과 API 계층의 연동 실습 개요
- 이번 실습은 실습용 샘플 프로젝트에 포함되어 있는 MemberService 클래스의 메서드에서 던져진 사용자 정의 예외(Custom Exception)를 Exception Advice에서 처리하는 실습입니다.
- 지난 챕터까지 학습했던 구현 코드들이 기본적으로 포함이 되어 있으며, 이를 기반으로 요구 사항에 맞게 MemberService 클래스에서 발생하는 예외 처리를 위한 실습 코드를 구현하면 됩니다.
- 실습에 필요한 클래스는 ‘com.springboot’ 내의 패키지에 포함되어 있습니다.
- 실습용 프로젝트 패키지는 아래와 같이 구성되어 있습니다.
- advice
- 예외 공통 처리를 위한 GlobalExceptionAdvice
- coffee
- exception
- BusinessLogicException 클래스
- ExceptionCode 클래스
- member
- order
- response
- ErrorResponse
- validator
- advice
실습 사전 준비
- 실습용 샘플 프로젝트 복제
- 아래 github 링크에서 실습용 repository를 fork합니다.
- fork한 repository를 여러분의 PC에서 git clone 명령으로 local repository에 복제합니다.
- IntelliJ IDE로 clone 받은 forked local repository 디렉토리의 프로젝트를 Open합니다.
- 아래 실습 요구 사항에 따라 실습을 진행합니다.
- 작성한 코드는 main branch에 작성해 주세요.
- main branch가 아닌 별도의 branch를 생성해서 작업을 했다면 작업이 끝난 후, 반드시 main branch로 merge 해야 합니다.
실습 종료 시 요청 사항
- 실습 코드 작성이 완료되었으면 아래의 실습 과제 제출 안내에 따라 실습 과제를 제출해 주세요.
GlobalExceptionAdvice 기능 추가 1
구현 내용
- MemberController의 getMember() 핸들러 메서드에 요청 전송 시, 아래와 같은 에러 응답 및 HttpStatus를 볼 수 있어야 합니다. 현재는 요청을 전송해도 콘솔에서 로그만 출력됩니다.
[그림 h-5] MemberController getMember() 핸들러 메서드 요청 결과
- 실습 프로젝트에 포함된 GlobalExceptionAdvice 클래스의 handleBusinessLogicException() 메서드에서 클라이언트에게 [그림 h-5]와 같이 ErrorResponse를 전송할 수 있도록 추가 구현하세요.
- ⭐ MemberService의 findMember() 메서드에서 특정 예외를 던지도록 구현해 두었으니 MemberService는 별도로 건드릴 부분이 없습니다.
힌트
- 실습 프로젝트에 포함된 현재 버전의 ErrorResponse 클래스로는 handleBusinessLogicException 클래스에서 ErrorResponse를 전송할 수 없습니다.
- ErrorResponse에 추가 구현이 필요합니다.
- Postman에서 표시되는 응답 메시지의 프로퍼티를 ErrorResponse 클래스의 멤버 변수와 서로 매핑해 보세요. 해결책이 보일 가능성이 높습니다.
제한 사항
- GlobalExceptionAdvice 클래스와 ErrorResponse 클래스 이외에 실습 프로젝트에 이미 작성되어 있는 코드들은 수정하지 않아야 합니다.
- HTTP Status 정보는 ExceptionCode 클래스에 이미 정의된 정보 또는 org.springframework.http.HttpStatus 클래스를 이용해야 합니다.
GlobalExceptionAdvice 기능 추가 2
구현 내용
- HttpRequestMethodNotSupportedException을 처리하기 위한 handleHttpRequestMethodNotSupportedException() 메서드를 구현하세요.
- MemberController의 postMember() 핸들러 메서드를 POST가 아닌 PATCH로 변경한 후 요청 전송 시, 아래와 같은 에러 응답 및 HTTP Status를 받을 수 있어야 합니다.
[그림 h-6] MemberController postMmber() 핸들러 메서드 요청 결과
제한 사항
- GlobalExceptionAdvice 클래스와 ErrorResponse 클래스 이외에 실습 프로젝트에 이미 작성되어 있는 코드들은 수정하지 않아야 합니다.
- HTTP Status 정보는 ExceptionCode 클래스에 이미 정의된 정보 또는 org.springframework.http.HttpStatus 클래스를 이용해야 합니다.
GlobalExceptionAdvice 기능 추가 3
구현 내용
- NullpointerException 등과 같이 개발자가 구현 상의 실수로 발생하는 Exception을 처리하기 위한 handleException() 메서드를 구현하세요.
- 구현 후, MemberController의 deleteMember() 핸들러 메서드를 DELETE로 요청 전송 시, 아래와 같은 에러 응답 및 HTTP Status를 받을 수 있어야 합니다.
- ⭐ MemberService의 deleteMember() 메서드에는 의도적으로 NullpointerException이 발생하도록 구현이 되어 있는 상태입니다.
[그림 h-7] MemberController deleteMmber() 핸들러 메서드 요청 결과
제한 사항
- GlobalExceptionAdvice 클래스와 ErrorResponse 클래스, ExceptionCode enum 이외에 실습 프로젝트에 이미 작성되어 있는 코드들은 수정하지 않아야 합니다.
- HTTP Status 정보는 ExceptionCode 클래스에 이미 정의된 정보 또는 org.springframework.http.HttpStatus 클래스를 이용해야 합니다.
GlobalExceptionAdvice
package com.springboot.advice;
import com.springboot.exception.BusinessLogicException;
import com.springboot.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleBusinessLogicException(BusinessLogicException e) {
// TODO GlobalExceptionAdvice 기능 추가 1
return ErrorResponse.of(e.getExceptionCode());
}
// TODO GlobalExceptionAdvice 기능 추가 2
@ExceptionHandler
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
return ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED);
}
// TODO GlobalExceptionAdvice 기능 추가 3
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(NullPointerException e) {
return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Error
package com.springboot.response;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public interface Error {
String getField();
String getRejectedValue();
String getReason();
static <V, T> List<T> from(Collection<V> collection, Function<? super V, ? extends T> mapper) {
return collection.stream()
.map(mapper)
.collect(Collectors.toList());
}
}
ConstraintViolationError
package com.springboot.response;
import lombok.Getter;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
@Getter
public class ConstraintViolationError implements Error{
private String field;
private String rejectedValue;
private String reason;
private ConstraintViolationError(String field, String rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<Error> of(Set<ConstraintViolation<?>> constraintViolations) {
return Error.from(constraintViolations,
constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()
));
}
}
FieldError
package com.springboot.response;
import lombok.Getter;
import org.springframework.validation.BindingResult;
import java.util.List;
@Getter
public class FieldError implements Error{
private String field;
private String rejectedValue;
private String reason;
public FieldError(String field, String rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<Error> of(BindingResult bindingResult) {
List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
return Error.from(fieldErrors, error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
error.getDefaultMessage()
));
}
}
ErrorResponse
package com.springboot.response;
import com.springboot.exception.ExceptionCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
@Getter
public class ErrorResponse {
private Integer status;
private String message;
private List<Error> errors;
public ErrorResponse(List<Error> errors) {
this.errors = errors;
}
private ErrorResponse(Integer status, String message) {
this.status = status;
this.message = message;
}
public static ErrorResponse of(BindingResult bindingResult) {
List<Error> errors = FieldError.of(bindingResult);
return new ErrorResponse(errors);
}
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
List<Error> errors = ConstraintViolationError.of(violations);
return new ErrorResponse(errors);
}
public static ErrorResponse of(ExceptionCode exceptionCode) {
return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
}
public static ErrorResponse of(HttpStatus httpStatus) {
return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase());
}
}