[Spring Boot] DTO를 이용한 유효성 검증

BAEJUN SON·2024년 7월 30일

싸피 공통 프로젝트

목록 보기
18/23

DTO?

Data Transfer Object의 약자로, 데이터를 전달하기 위해서 사용하는 객체를 의미합니다.

DTO를 사용하게 되면 다음과 같은 장점이 있습니다.

  1. Entity의 값 중 일부를 보여주지 않을 수도 있고, Entity에 없는 값이더라도 DTO를 생성할 때 만들어서 넣어주면 되므로 Entity에 의존적이지 않을 수 있다.
  2. Entity는 Entity Manager가 관리하므로, 값의 변화가 수정을 가져올 수 있다. 만약 Service에서 해당 Entity를 변경하게 된다면, 생각하지 못한 Side Effect가 발생할 수 있다. DTO를 따로 두고 관리한다면 가져온 값들에 대해서 수정이 있더라도 이 영향이 Entity에 미치지 않게 된다.
  3. 들어오는 데이터에 대한 유효성 검증을 진행하는 역할을 외부로 넘길 수 있다

DTO 유효성 검증이 필요한 이유

일반적으로 3번의 검증과 예외처리를 진행한다.

  1. 프론트에서 입력값에 대한 검증
  2. 백엔드에서 유효성 검증
  3. 서비스 로직 진행 중 발생하는 예외에 대한 처리

왜 이렇게 여러번 검증해야 할까?

일반적으로 프론트엔드 쪽 웹사이트에서는 JS를 이용하여 사용자가 입력하는 입력 폼 필드의 값에 대해 1차적으로 유효성 검증을 진행한다.

하지만 JS로 전송되는 데이터는 브라우저의 개발자 도구를 사용해서 breakpoint를 추가한 뒤에 얼마든지 그 값을 조작할 수 있기 때문에 프론트엔드 쪽에서 유효성 검사를 진행했다고 하더라도 서버 쪽에서 한번 더 유효성 검사를 진행해야 한다.

한마디로, 프론트엔드에서 하는 검증은 사용자에게 편의를 주기위해(의미 없는 요청이 진행되지 않도록) 검증하고, 백엔드에서 정말 그 값에 대해 검증을 진행하여야 한다는 의미다!


유효성 검증 진행하기

1. 가장 먼저 Build.gradle에서 의존성을 추가해줘야 합니다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

2. DTO에 @Validation 어노테이션을 사용 해 값을 검증한다.

@Data
@Builder
public class ChildRequestDTO {

    private String profileImage;

    @NotNull(message = "닉네임은 필수 값입니다.")
    @Size(max = 10, message = "닉네임은 최대 10자까지 입력 가능합니다.")
    private String nickname;

    @NotNull(message = "성별은 필수 값입니다.")
    private Gender gender;

    @NotNull(message = "생일은 필수 값입니다.")
    private LocalDate birth;

    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
  • 위 코드처럼 필요한 Validation을 적용하면 됩니다.
  • Null체크는 @NotNull 검증, 문자열의 길이 검증은 @Size, 값의 최소/최대값 검증은 @Min/@Max 등이 존재합니다.

3. 해당 DTO를 사용하는 Controller 에서 @Valid를 적용한다.

@RestController
@RequestMapping("/api/child")
@RequiredArgsConstructor
public class ChildController {

    private final ChildService childService;

    @PostMapping
    public ResponseEntity<Void> addChild(@Valid @RequestBody ChildRequestDTO childRequestDTO) {
        childService.addChild(childRequestDTO);
        return new ResponseEntity<>(HttpStatus.OK);
    }
    
}
  • @Valid가 붙어있는 DTO에 대해 검증이 들어가게 됩니다.
  • 검증에 성공하면 문제없지만, 검증에 실패한다면 ?

예외처리를 해주어야 합니다.

4. MethodArgumentNotValidException 예외처리 진행

Validation Exception이 발생한다면 MethodArgumentNotValidException이 발생하게 됩니다. 이를 GlobalExceptionHandler에서 처리해주어 검증에 실패한 필드에 대한 메시지를 출력합니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    // 다른 예외 핸들러도 추가할 수 있습니다.
}

결론

위와 같이 유효성 검증을 추가하고, 예외처리를 해준다면

이렇게 잘못된 요청이 들어왔을떄(위 예시는 score, playtime필드가 @NotNull인데, Null이 들어온 케이스) 값을 검증해주어 유효성을 판단하여 줍니다.

DTO를 사용하여 들어오는 값에 대한 검증을 진행하고, 이를 통해 유효성 검사의 역할을 분리하여 결합도를 떨어트리는 결과를 가져옵니다.

0개의 댓글