- 기존의 Controller에서 발생하는 예외들은 @ControllerAdvice 어노테이션이 붙은 클래스에서 처리가 가능하다.
- @ControllerAdvice는 전역 예외 처리가 가능하고 예외가 발생했을 때 처리할 수 있는 공통 로직을 제공한다.
- PostCreate.java
.....
@NotBlank(message = "title을 입력해주세요")
private String title;
@NotBlank(message = "content를 입력해주세요")
private String content;
.....
- Post 엔티티에 대한 DTO에 해당하는 PostCreate는 @NotBlank 어노테이션으로 인해 스프링이 제공하는 기본적인 검증이 이루어 진다.
- 하지만 기본적으로 제공하는 검증이기 때문에 특정한 검증(ex) title에 특정 단어가 포함되지 않게 검증해라) 에 대해선 검증 할 수 없다.
- 때문에 추가적인 검증을 위한 메서드 및 해당 검증에 대한 예외를 생성해주어야 한다.
- PostCreate.java
.....
public void validate() {
if (title.contains("바보")) {
throw new InvalidRequest("title", "제목에 바보를 포함할 수 없습니다.");
}
}
- PostCreate 클래스에 검증을 위한 메서드를 추가한다.
- 위 코드는 넘어온 title에 대해 "바보"라는 단어를 포함하게 되면 예외를 던지는 코드이다.
- PostController.java
@PostMapping("/posts")
public Post post(@RequestBody @Valid PostCreate request) {
request.validate();
return postService.write(request);
}
- 검증을 위해 validate() 메서드를 실행하는 코드를 추가한다.
- 여러 검증을 위한 예외 클래스를 생성하고 클래스의 수 만큼 @ControllerAdvice에 각 클래스에 대한 예외 처리 코드를 별도로 작성해 주어야 한다.
- 하지만 예외 클래스 마다 코드를 작성하는 것은 예외 클래스에 대한 에러 코드가 발생할 가능성이 크기 때문에 여러 예외 클래스에 대한 (개별적인)최상위 클래스를 생성하여 해당 클래스로 예외를 던지게 만드는 것이 좋다.
- IslogException.java ((개별적)최상위 예외 클래스)
public abstract class IslogException extends RuntimeException {
public IslogException(String message) {
super(message);
}
public IslogException(String message, Throwable cause) {
super(message, cause);
}
public abstract int getStatuscode();
}
- 위의 예외 클래스는 JAVA에서 기본적으로 제공하는 예외 클래스가 아니기 때문에 실제 예외를 상속 받아야 하며 예외를 실제 예외 클래스로 던져야 한다.
- 예외는 대체로 런타임, 언체크 예외인 RuntimeException으로 던지는 것이 좋다.
- getStatuscode()의 추상 메서드를 작성한 이유는 각 예외 클래스의 상태 코드가 다르기 때문에 각 예외로 부터 코드를 받아오는 것이 코드 중복이 발생하지 않아 좋기 때문이다.
- InvalidRequest.java
@Getter
public class InvalidRequest extends IslogException {
private static final String MESSAGE = "잘못된 요청입니다.";
public String fieldName;
public String message;
public InvalidRequest() {
super(MESSAGE);
}
public InvalidRequest(String fieldName, String message) {
super(MESSAGE);
this.fieldName = fieldName;
this.message = message;
}
@Override
public int getStatuscode() {
return 400;
}
}
- 위 클래스는 예외로 사용하기 위해 생성한 클래스이다.
- ExceptionController.java
.....
@ResponseBody
@ExceptionHandler(IslogException.class)
public ResponseEntity<ErrorResponse> islogException(IslogException e) {
int statusCode = e.getStatuscode();
ErrorResponse body = ErrorResponse.builder()
.code(String.valueOf(statusCode))//애초에 받아올 때 String으로 받아오던가 반환값을 뭐 알아서 변경하면 된다.
.message(e.getMessage())
.build();
// 응답 json validation -> title : 제목에 바보를 포함할 수 없습니다.
if (e instanceof InvalidRequest) {
InvalidRequest invalidRequest = (InvalidRequest) e;
String fieldName = invalidRequest.getFieldName();
String message = invalidRequest.getMessage();
body.addValidation(fieldName, message);
}
ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
.body(body);
return response;
}
.....
- 전역 예외를 처리하기 위한 @ControllerAdvice가 붙은 클래스이다.
- 예외 테스트
- Test 실행 -> Controller에서 InvalidException 발생 (파라미터로 "title", "제목에 바보를 포함할 수 없습니다"를 포함) -> ExceptionController.java의 @ExceptionHandler(IslogException.class)가 붙은 메서드 실행(IslogException.class는 추가 예외 클래스 중 최상위 클래스)
- PostControllerTest.java
@Test
@DisplayName("게시글 작성시 제목에 '바보'는 포함될 수 없다.")
void test11() throws Exception {
PostCreate request = PostCreate.builder()
.title("나는 바보입니다.")
.content("내용입니다.")
.build();
String json = objectMapper.writeValueAsString(request);
//when
mockMvc.perform(MockMvcRequestBuilders.post("/posts")
.contentType(APPLICATION_JSON)
.content(json)
)
.andExpect(status().isBadRequest())
.andDo(print());
}
- MockMvc를 통해 임시로 요청을 생성하는데 전달하는 엔티티의 title 값에 "바보"가 포함되어 있고,
Controller에서 "바보"가 포함된 title의 경우 InvalidRequest이 발생하도록 코드를 작성하였기 때문에 예외가 발생하여 InvalidRequest로 예외를 던진다.
- InvalidRequest의 생성자에 Controller에서 넘어온 값이 들어가고 최상위 예외인 IslogException로 예외를 전달하기 위해 super() 메서드를 호출한다.
public abstract class IslogException extends RuntimeException {
public IslogException(String message) {
super(message);
}
public IslogException(String message, Throwable cause) {
super(message, cause);
}
public abstract int getStatuscode();
}
- 최종적으로 IslogException는 RuntimeException을 상속 받는다. 즉, 언체크, 런타임 예외로 처리된다.
- 예외가 발생했으므로 @ControllerAdvice가 붙은 클래스에서 처리 된다.
- ExceptionController.java
@ResponseBody
@ExceptionHandler(IslogException.class)
public ResponseEntity<ErrorResponse> islogException(IslogException e) {
int statusCode = e.getStatuscode();
ErrorResponse body = ErrorResponse.builder()
.code(String.valueOf(statusCode))//애초에 받아올 때 String으로 받아오던가 반환값을 뭐 알아서 변경하면 된다.
.message(e.getMessage())
.build();
// 응답 json validation -> title : 제목에 바보를 포함할 수 없습니다.
if (e instanceof InvalidRequest) {
InvalidRequest invalidRequest = (InvalidRequest) e;
String fieldName = invalidRequest.getFieldName();
String message = invalidRequest.getMessage();
body.addValidation(fieldName, message);
}
ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
.body(body);
return response;
}
- @ExceptionHandler에 IslogException클래스에 대한 처리 메서드가 있기 때문에 해당 메서드에서 위에서 발생한 예외를 처리하게 된다.
- 반환 타입을 ResponseEntity<ErrorResponse>로 하게되면 ErrorResponse를 통한 HTTP 응답 필드에 대한 설정이 가능하다.
- ErrorResponse 엔티티에 있는 필드들을 builder 패턴을 이용해 객체를 생성한다.
- 매개변수로 IslogException을 받으므로 IslogException의 하위 클래스들을 타입 캐스팅을 통해 사용할 수 있다.
- if문을 통해 InvalidRequest 예외가 넘어온 경우 ErrorResponse의 validation 필드에 값을 넣기 위한 fieldName과 message 필드를 생성하여 InvalidRequest로부터 넘어온 값들을 넣고, ErrorResponse의 validatoin 필드에 Map 형태로 값을 집어 넣는다.
- ResponseEntity<ErrorResponse>의 객체를 생성하고, 응답코드 및, Response body에 넣을 값을 지정하여 해당 객체를 메서드에 반환한다.
- Test 코드의 결과로 위와 같이 HttpResponse 필드에 각 데이터가 입력되어 보여지는 것을 확인할 수 있다.
- 코드 리팩토링
- ExceptionController.java
if (e instanceof InvalidRequest) {
InvalidRequest invalidRequest = (InvalidRequest) e;
String fieldName = invalidRequest.getFieldName();
String message = invalidRequest.getMessage();
body.addValidation(fieldName, message);
}
- ExceptionController 클래스의 위 코드는 예외가 발생하는 field와 message를 전달하기 위해서 예외 클래스마다 if문을 작성해야 한다는 단점이 있다.
@ResponseBody
@ExceptionHandler(IslogException.class)
public ResponseEntity<ErrorResponse> islogException(IslogException e) {
int statusCode = e.getStatuscode();
ErrorResponse body = ErrorResponse.builder()
.code(String.valueOf(statusCode))//애초에 받아올 때 String으로 받아오던가 반환값을 뭐 알아서 변경하면 된다.
.message(e.getMessage())
.validation(e.getValidation())
.build();
ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
.body(body);
return response;
}
}
.validation(e.getValidation())
- 위 코드와 같이 ErrorResponse 클래스에 예외로부터 값을 받아오게 되면 Controller에서 예외 클래스마다 코드를 작성해야 하는 번거로움이 사라진다.
- IslogException.java
@Getter
public abstract class IslogException extends RuntimeException {
public final Map<String, String> validation = new HashMap<>();
public IslogException(String message) {
super(message);
}
public IslogException(String message, Throwable cause) {
super(message, cause);
}
public abstract int getStatuscode();
public void addValidation(String fieldName, String message) {
validation.put(fieldName, message);
}
}
- ErrorResponse의 vallidation 필드에 값을 IslogException 필드로부터 받아오기 때문에 IslogException 클래스에 DTO처럼 사용하기 위한 validation 필드를 하나 생성해준다.
- InvalidRequest.java
@Getter
public class InvalidRequest extends IslogException {
private static final String MESSAGE = "잘못된 요청입니다.";
// public String fieldName;
// public String message;
// 필드를 직접적으로 설정하는 것은 좋지 않다...
public InvalidRequest() {
super(MESSAGE);
}
public InvalidRequest(String fieldName, String message) {
super(MESSAGE);
// this.fieldName = fieldName;
// this.message = message;
addValidation(fieldName, message);
}
@Override
public int getStatuscode() {
return 400;
}
}
- InvalidRequest 클래스에 있던 기존 필드를 없애고 파라미터로 필드를 받아서 전달하는 방식으로 변경한다.
- InvalidRequest의 부모 클래스는 IslogException이기 때문에 자식 클래스에서 부모 클래스의 메서드를 사용할 수 있다.
- Controller에서 발생한 예외로 부터 넘어온 fieldName, message가 InvalidRequest를 통해 IslogException으로 넘어가고 IslogException에서 validation 필드에 해당 값들이 들어가고 해당 값은 최종적으로 ErrorResponse 필드에 주입되어 ExceptionController의 HttpResponse로 반환된다.
'Programming > Spring' 카테고리의 다른 글
Spring - API 인증 (파라미터, Header, Interceptor) (0) | 2023.10.04 |
---|---|
Spring - 문서화 (Rest Docs 문서) (0) | 2023.09.08 |
Spring, Java - Builder 패턴 분석 (0) | 2023.09.06 |
Spring - 게시글 수정 / 삭제 (0) | 2023.09.05 |
Spring - 페이징 (Jpa, Querydsl) (0) | 2023.08.31 |