개요
Spring Boot의 유효성 검증 중 한 방법인 org.hibernate.validator.constraints
사용하면서 유효성 검증에 실패할 때 던져지는 MethodArgumentNotValidException
를 효과적으로 처리할 방법을 찾던 중 발견한 어노테이션
ControllerAdvice
는 컨트롤러에 AOP를 적용시켜주는 어노테이션으로써 부가 기능을 더해줄 수 있다.
RestControllerAdvice
는 RestController
와 동일하게 ControllerAdvice
+ ResponseBody
의 역할을 한다. 즉, 뷰를 렌더링하는 과정 없이 JSON 형태의 응답을 반환한다.
활용
ControllerAdvice를 통해서 예외에 대한 처리 핸들러를 구현하였다.
먼저 유효성 검증에 대한 실패 예외를 핸들링해주기 위해서 사용했으며 예외를 핸들링할 수 있도록 도움을 주는 @ExceptionHandler()
를 사용하였다.
유효성 검증 핸들러
@RestControllerAdvice
public class ExceptionAdvisor {
// MethodArgumentNotValidException, 유효성 처리 발생시 던져지는 Exception을 핸들링
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> validationHandler(MethodArgumentNotValidException e){
List<FieldError> errors = e.getBindingResult().getFieldErrors();
StringBuilder sb = new StringBuilder();
for(FieldError error : errors ){
sb
.append(error.getDefaultMessage())
.append("\n")
.append("입력된 값 : ")
.append(error.getRejectedValue())
.append("\n");
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
}
org.hibernate.validator.constraints
의 유효성 검증에 실패하면 던져지는 MethodArgumentNotValidException
에 대해서 예외를 잡아서 처리할 수 있다.
해당 예외가 컨트롤러에서 발생하면 @ExceptionHandler(MethodArgumentNotValidException.class)가 붙은 메소드가 실행되고 응답을 처리한다.
결과
DTO에서 적용시킨 content의 NotBlank 유효성이 제대로 검증되었음을 볼 수 있다.
DTO
@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PostDTO {
private Long id;
@NotBlank(message = "게시글 제목이 없습니다.")
private String title;
@NotBlank(message = "게시글 내용이 없습니다.")
private String content;
@NotBlank(message = "작성자 이메일은 필수 값입니다.")
@Email(message = "작성자 이메일의 형식이 잘못되었습니다.")
private String email;
private String writeDate;
@Builder.Default
private List<String> like = new ArrayList<>();
private Long views;
컨트롤러에서는 @Valid
만 추가해주면 RequestBody에 대한 유효성 검증이 실행된다.
Controller
@PostMapping
public ResponseEntity<PostDTO> createPost(
@Valid @RequestBody PostDTO postDTO
){
return ResponseEntity.status(HttpStatus.CREATED).body(this.postService.createPost(postDTO));
}
유효성 검증에 대한 예외 처리가 똑바로 진행되는지 테스트를 통해 알아보았다.
Test
@Test
@DisplayName("post title blank create test")
public void postTitleBlankCreateTest() throws Exception {
//given
post1.setTitle("");
//when
mockMvc.perform(
post("/post")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(post1))
)
.andExpect(status().isBadRequest())
.andDo(print());
//then
}
Response
컨트롤러에서의 예외처리에 대한 AOP 적용
컨트롤러에서 예외에 대한 처리를 각 엔드포인트에 일일히 작성해줄 필요 없이 ExceptionHandler로 관심사를 분리해낼 수 있다.
@RestController
@RequestMapping("/post")
public class PostController {
private final static Logger logger = LoggerFactory.getLogger(PostController.class);
private final PostService postService;
public PostController(
@Autowired PostService postService
){
this.postService = postService;
}
// PostNotFoundException은 ExceptionAdvisor에서 모두 처리
@PostMapping
public ResponseEntity<PostDTO> createPost(
@Valid @RequestBody PostDTO postDTO
){
return ResponseEntity.status(HttpStatus.CREATED).body(this.postService.createPost(postDTO));
}
@GetMapping("/{id}")
public ResponseEntity<PostDTO> readPost(
@PathVariable("id") Long id
){
return ResponseEntity.status(HttpStatus.OK).body(this.postService.readPost(id));
}
@GetMapping
public ResponseEntity<List<PostDTO>> readAllPost(){
return ResponseEntity.status(HttpStatus.OK).body(this.postService.readAllPost());
}
@PutMapping
public ResponseEntity<PostDTO> updatePost(
@RequestBody PostDTO postDTO
){
return ResponseEntity.status(HttpStatus.OK).body(this.postService.updatePost(postDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Boolean> deletePost(
@PathVariable("id") Long id
){
this.postService.deletePost(id);
return ResponseEntity.status(HttpStatus.OK).body(true);
}
}
PostController는 서비스 계층에서 던져지는 PostNotFoundException에 대한 예외 처리를 각 엔드포인트마다 작성해주어야 했지만 관심사의 분리를 통해서 묶어낼 수 있다.
결과
@RestControllerAdvice
public class ExceptionAdvisor {
// CustomException Service 계층에서 던져진 예외에 대해 핸들링
@ExceptionHandler({PostNotFoundException.class})
public ResponseEntity<String> customExceptionHandler(Exception e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
Test
@Test
@DisplayName("not found post update test")
public void notFoundPostUpdateTest() throws Exception {
PostDTO after1 = PostDTO.entityToDTO(PostEntity.initEntity(post1));
after1.setId(1L);
Throwable e = new PostNotFoundException();
//given
given(postService.updatePost(refEq(after1)))
.willThrow(e);
//when
mockMvc.perform(
put("/post")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(after1))
)
.andExpect(status().isBadRequest())
.andExpect(content().string(e.getMessage()))
.andDo(print());
//then
verify(postService).updatePost(refEq(after1));
}
Controller에 예외처리를 작성해주지 않았지만 성공적으로 예외처리되어 응답이 반환된 것을 볼 수 있다.
Response
결론
ControllerAdvice, RestControllerAdvice와 ExceptionHandler 어노테이션을 통하여 컨트롤러 계층의 예외들에 대해서 쉽게 처리할 수 있었다.
AOP를 통한 관심사의 분리를 쉽게 적용해줄 수 있었다.
각각의 엔드포인트에서 중복으로 작성되던 예외 처리를 모두 묶어내어 변경이 필요할 때는 ExceptionAdvisor의 메소드만 수정해주면되고 컨트롤러 계층의 코드도 가독성이 높아지는 효과가 있었다.
'Spring' 카테고리의 다른 글
[Spring Boot] 환경변수로 .jar와 docker run 프로파일 결정 (0) | 2024.02.05 |
---|---|
[Spring JPA] 일대다 연관관계 매핑 (0) | 2024.02.04 |
[Spring Test] Spring Boot Controller 단위 테스트 (1) | 2024.01.31 |
[Spring Test] Mockito when()과 given() 차이 (0) | 2024.01.29 |
[Spring Boot] 스프링 부트 HTTPS 적용 방법 (1) | 2024.01.26 |