신입 때 진짜 끔찍한 경험을 해봤다.
시에서 운영하는 전산프로그램이었는데 납품하고 1년쯤 지나니까 오류가 난다는 연락이 왔다.
신입 시절 첫 SI로 진짜 긴장 많이 했었는데 참... ㅋㅋ
하여튼 원인을 분석하니 유니크해야 하는 칼럼 값이 중복으로 박혀있었다.
당연히 단위테스트 통합테스트를 진행했었다.
담당자들이 진짜 상상도 못 한 방법으로 DB의 무결성과 정합성을 깨버렸다.
(개발자가 콘센트에 플러그 꽂으라고 했는데 유저가 젓가락 꽂는 짤.jpg)
아무튼...
팀원(선임)이 퇴사하면서 내가... 해야 했었고 전산실에서 남들 퇴근한 시간에 암호화된 데이터를 일일이 까서
며칠 뜯어고쳤다 ㅠㅠ 진짜 두 번 다시는 겪고 싶지 않은 일이다.
그 일이 있고 나서 개발할 때는 불편하니까 논리적으로 한다 쳐도 프로덕트에 올릴 때는 제약조건을 꼭 걸어야 한다는 부분을 몸으로 느꼈다.
그래도 신입 때 몸으로 때려 맞으며 겪은 경험이라 습관은 잘 배어 있는 편인 것 같다 ㅋㅋ
유효성 검증은 과유불급? 그딴 거 없다.
최종 방어선은 서버가 되어야 시스템 장애를 야기시키지 않는다 ㅜㅜ
많으면 많을수록 좋다...
오늘은 Controller Layer의 유효성 검증 방법에 대해 정리하려고 한다.
유효성 검증은 Controller, Service, Repository의 레이어 별로 각각 흐름에 맞게 작성한다.
jakarta.validation -> javax.validation 패키지를 활용할 예정이다.
이 라이브러리는 스프링부트 스타터 패키지에 포함되어있다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
Data Transfer Object : DTO Class
public class BookDTO {
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class BookRequestDTO {
@NotBlank
@Size(min = 1, max = 50)
private String title;
@NotBlank
@Size(min = 2, max = 20)
private String author;
public Book toEntity() {
return Book.createBook(title, author);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class BookResponseDTO {
private Long id;
private String title;
private String author;
public static BookResponseDTO fromEntity(Book book) {
return BookResponseDTO.builder()
.id(book.getId())
.title(book.getTitle())
.author(book.getAuthor())
.build();
}
}
}
도메인 별로 내려줘야하는 데이터 포맷이 다른 경우가 많기 때문에 BookDTO 클래스 안에
inner static class로 요청용, 응답용 DTO로 분할해서 쓰는 편이다.
RestController : BookApiController
@RequestMapping("/api/v1/book")
@RestController
public class BookApiController {
@PostMapping
public ResponseEntity<GlobalCommonResponseDTO> saveBook(@RequestBody @Valid BookRequestDTO bookRequestDTO, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
HashMap<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
errorMap.put(error.getField(), error.getDefaultMessage());
});
GlobalCommonResponseDTO responseDTO = GlobalCommonResponseDTO.builder()
.code(GlobalCommonResponseCode.FAIL.getCode())
.message("validation error")
.data(errors)
.build();
return ResponseEntity.badRequest().body(responseDTO);
} else {
// todo service...
return ResponseEntity.ok();
}
}
}
@Valid
어노테이션이 태깅된 객체에 유효성 검증을 하게 되는데 여기서 에러가 있다면
bindingResult.hasErrors()에 걸리게 된다.
이후 에러 데이터를 잡아내면 되는데, 요청을 받아야 하는 컨트롤러가 몇 개인데 이거를 복붙하는 것도 일이고
수정사항이 생겼을 때 적용하려면 죽어난다.
그래서 해볼 수 있는 게
@ExceptionHandler
라는 것을 활용해볼 수 있다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidationException(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getAllErrors()
.forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
GlobalCommonResponseDTO responseDTO = GlobalCommonResponseDTO.builder()
.code(GlobalCommonResponseCode.FAIL.getCode())
.message("validation error")
.data(errors)
.build();
return ResponseEntity.badRequest().body(responseDTO);
}
MethodArgumentNotValidException.class에서 터진 예외를 핸들링하는 메서드를 생성한다.
이와 같은 맥락으로 RuntimeException
이나 IllegalStateException
등 각종 Exception을 적
용 할 수도 있고 상속받아 만드는 Custom Exception도 가능하다.
이렇게 되면 @Valid
가 태깅된 파라미터에 유효성 검증을 실행하고 에러가 터지면
handleValidationException
메서드를 수행한다.
요청에 대해 문제가 발생하면 예외로 던져 핸들러에서 처리하고 각 레이어에서는 각자 자기의 일만 충실히 할 수 있게 구현하는것이 좋을 것 같다.
@PostMapping("/api/v1/book")
public ResponseEntity<GlobalCommonResponseDTO> saveBook(@RequestBody @Valid BookRequestDTO bookRequestDTO/*, BindingResult bindingResult*/) {
// request do
BookResponseDTO bookDTO = bookService.saveBook(bookRequestDTO);
// send response
GlobalCommonResponseDTO body = GlobalCommonResponseDTO.builder()
.code(GlobalCommonResponseCode.SUCCESS.getCode())
.message("book list")
.data(bookDTO)
.build();
return new ResponseEntity(body, HttpStatus.CREATED);
}
컨트롤러는 클라이언트의 요청을 입력받아 각 기능으로 분배하는 역할을 해주고 있다.
BookRequestDTO
안에 태깅된 유효성 검사를 통과하면 자연스레 Service Layer로 전달되고
요청에 대한 응답을 처리한다.