프로젝트의 객체에 다양한 제약사항을 걸고 이를 만족하지 않을떄 날릴 예외처리를 내 입맛대로 만들어보자
클라이언트의 요청이 들어가면
→ Servlet 컨테이너 → Spring DispatcherServlet → Controller
이런 순서로 전달이 되고, 예외가 생기면 기본적으로는 Spring 에 내재되어있는 에러처리가 된다!
service, repository 등 에러가 발생하면 에러를 처리해줄 도구를 찾아 하나씩 위로위로 올라가며 찾는다
예외상황에 대처할 수 있는 핸들러를 만드는 것!
: 컨트롤러에서 발생하는 예외상황을 직접 다루고 싶을 때 쓰는 어노테이션
~~이런 예외가 터지면 작동하렴 이라고 스프링에게 알려주는 것
@ExceptionHandler(요기에 적용하고 싶은 예외상황 넣어주기)
@ExceptionHandler(IllegalArgumentException.class)@ExceptionHandler(NullPointerException.class)@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
log.error("CustomException: {}", e.getMessage());
Errors errorCode = e.getError();
ErrorResponse response = ErrorResponse.of(errorCode, e.getMessage());
return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus()));
}
만약 개발자가 처리방법을 명시한 @ExceptionHandler가 있다면 여기서 처리되고, 없으면 Servlet 컨테이너에서 자체적으로 에러 페이지 응답하게된다 (50x 40x 등등)
근데 만약 이메일을 입력받았지만 중복된 이메일이라서 에러가난건데 500이 반환된다면 이거 백엔드 탓도 아닌데 겁나 억울하자나!!!!!!!!!!
내가 잘못한거 아니고 이미 DB에 같은 값이 있을뿐인데~!!!!
이런거 발생 못하게 하려고 CustomException 만드는거임 ㅋ
(억울하기 짱시룸)
특정 상황에서만 발생하는 예외를 별도로 구분하고 싶을 때
(비즈니스 로직에서 중요한 예외를 나타내기 위함)
1. RuntimeException 을 상속한다
그러면 이렇게 번개 표시가 뜨는 모습을 볼 수 있다
public class CustomException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
이렇게 만든 후, 에러처리를 원하는 부분 위에
@ExceptionHandler(CustomException.class) 이 어노테이션을 붙여주면 에러를 잡아낼 수 있게되는 것이다.
@Valid로 조건을 잘 만족하는지 알아보기 위해서,
Entity에서 값을 받을 때 제약조건을 걸어보자
Todo 엔티티
@NotBlank(message = "제목은 공백일 수 없습니다")
@Size(max = 2, message = "제목은 최대 10글자로 작성할 수 있습니다")
@Column(nullable = false)
private String title;
@NotNull(message = "내용은 공백이라도 존재해야합니다")
@Column(columnDefinition = "longtext")
private String content;
Member 엔티티
@NotBlank(message = "이름은 공백일 수 없습니다. 4글자 이하로 입력하세요")
@Size(min = 2, message = "2글자 이상 입력하세요")
@Column(nullable = false, unique = true)
private String name;
@NotBlank //공백도 안됨
@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{4,10}"
, message = "비밀번호는 4~10자, 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
@Column(nullable = false)
private String password;
@Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,10}$"
, message = "이메일 형식이 올바르지 않습니다.")
@NotBlank(message = "이메일은 공백일 수 없습니다")
@Column(nullable = false, unique = true)
private String email;
회원가입 게시글 작성 이렇게 두가지 경우에 제약조건을 걸어야하므로
각각에 맞는 controller에 @Valid를 걸어주자
//회원가입
@PostMapping("/signup")
public ResponseEntity<SignUpResponseDto> signUp(@Valid @RequestBody SignUpRequestDto dto) {
SignUpResponseDto signUpResponseDto = memberService.signUp(dto.getName(), dto.getPassword(), dto.getEmail());
return new ResponseEntity<>(signUpResponseDto, HttpStatus.CREATED);
}
//일정 생성하기
@PostMapping
public ResponseEntity<TodoResponseDto> postTodo(@Valid @RequestBody TodoRequestDto dto, Error error){
TodoResponseDto todoResponseDto = todoService.postTodo(dto.getName(), dto.getTitle(), dto.getContent());
return new ResponseEntity<>(todoResponseDto, HttpStatus.CREATED);
}
@Getter
@AllArgsConstructor
public enum Errors {
INVALID_LOGIN(401 , "Bad Request","C001", "아이디 또는 비밀번호가 일치하지 않습니다."),
REQUIRED_USEREMAIL(400,"Bad Request","C002", "이메일 입력은 필수입니다."),
INVALID_INPUT_VALUE(400, "", "C003", "입력값이 잘못되었습니다");
private final int status;
private final String error;
private final String code;
private final String message;
}
status error code message 순으로 에러를 만들어줬다
enum 타입으로 관리하면 가독성도 높고 관리하기에도 용이하다!
에러를 반환할때 어떤 형식으로 어떤 값들을 담아 내보낼지 지정해주어야한다
@Getter
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {
private final LocalDateTime timestamp = LocalDateTime.now();
private final int status;
private final String error;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<FieldError> fieldErrors;
public static ErrorResponse of(Errors error) {
return ErrorResponse.builder()
.status(error.getStatus())
.error(error.getError())
.code(error.getCode())
.message(error.getMessage())
.fieldErrors(new ArrayList<>())
.build();
}
이렇게하면
{
"status": 401,
"error": "Bad Request",
"code": "C001",
"message": "아이디 또는 비밀번호가 일치하지 않습니다.",
"timestamp": "2025-04-03T17:41:49.853727"
}
이런 형식으로 에러가 반환될 것이다!
@Getter
public class CustomException extends RuntimeException {
private final Errors error;
public CustomException(Errors error) {
super(error.getMessage());
this.error = error;
}
RuntimeException를 상속받아서 커스텀 에러를 만들자
아까 만들어둔 Errors enum을 가져와서 부모타입에서 가져온 메세지를 할당한다
이제 모든 세팅은 끝났다!
프로젝트 전체에 관여할 수 있는 글로벌 핸들러에 설정해주면 된다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
log.error("CustomException: {}", e.getMessage());
Errors errorCode = e.getError();
ErrorResponse response = ErrorResponse.of(errorCode, e.getMessage());
return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus()));
}
@RestControllerAdvice & @ExceptionHandler 를 통해 여기가 에러처리하는 곳이다!!!!!!!!!!라고 알려주자
그리고 어떤 에러를 핸들링할건지 넣어준다 (CustomException.class)