@Valid와 @Validate로 해결하는 Validation 처리

최정환·2024년 11월 10일
0

1. 👿 문제 상황

1. 🖼️ 프론트엔드에만 의존하는 Validation

프론트에서만 validation이 이루어지는 상황에서는 JavaScript로 직접 form을 전송하거나 input 검사가 제대로 작동하지 않으면 백엔드의 별도 처리가 없다면 데이터가 그대로 넘어가 서비스가 실행됩니다.

2. ✍️ 백엔드 검증의 부족

백엔드에서 기본적인 검증조차 이루어지지 않아 필수 필드가 누락될 경우 NullPointerException(NPE), 심지어는 DB Exception과 같은 오류가 발생합니다. 이는 서비스의 안정성을 크게 저해할 수 있습니다. 뿐만 아니라 아무리 간단한 쿼리라도 디비에 불필요한 부하(query exception or 검증되지 않은 데이터 insert)를 가하는 것입니다.

2. 🧶 문제 해결 과정

1) @Valid를 DTO에 추가

각 DTO 필드에 @NotBlank, @Size, @Pattern과 같은 제약 조건을 설정하고 각각의 오류 메시지를 함께 추가했습니다.

예를 들어:

@NotBlank(message = "문의유형을 확인해 주세요.")
private String type;

@NotBlank(message = "제목을 확인해 주세요.")
@Size(max = 50)
private String title;

@NotBlank(message = "내용을 확인해 주세요.")
@Size(min = 10, max = 1500)
private String contents;

@NotBlank
@Pattern(regexp = "^\\d+$", message = "전화번호는 숫자만 입력 가능합니다.")
private String phoneNumber;

2) @Valid와 컨트롤러에서의 적용

컨트롤러 메서드에서 @Valid를 사용하여 전달된 DTO 객체가 유효성 검사를 통과하는지 확인했습니다.
유효성 검사를 통과하지 못하는 경우 프론트엔드에 에러 코드를 반환하여 사용자가 어떤 입력을 수정해야 하는지 알 수 있게 했습니다.

예시:


@PostMapping("/inquiry")
public ResponseEntity<?> createInquiry(@Valid @RequestBody InquiryDTO inquiryDTO) {
    // 서비스 로직 수행
    return ResponseEntity.ok("문의가 등록되었습니다.");
}

3) 🌎 controllerAdvice 적용

전역적으로 설정한 메시지를 반환하기 위해 @ControllerAdvice를 활용하여 전역 예외 처리를 추가합니다.

@ControllerAdvice
public class GlobalExceptionHandler {

    // @Valid 유효성 검사 실패 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();

        // 유효성 검사 실패한 필드와 메시지를 맵에 담음
        for (FieldError error : ex.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

이제 프론트엔드에 일관된 JSON 형태의 오류 응답을 제공합니다.

📉 반환값

{
  "type": "문의유형을 확인해 주세요.",
  "title": "제목을 확인해 주세요.",
  "contents": "내용을 확인해 주세요.",
  "phoneNumber": "전화번호는 숫자만 입력 가능합니다."
}

4) @Pattern 직접 구현해보기

@Valid는 DTO 레벨에서 각 필드에 지정된 제약 조건을 검증하고 에러 메시지를 생성하여 사용자에게 알려주는 데 주로 사용됩니다.

예를 들어 특수문자를 거르는 어노테이션을 직접 만들어봅니다.


1. 어노테이션 인터페이스 정의

@Documented
@Constraint(validatedBy = NoSpecialCharactersValidator.class)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface NoSpecialCharacters {
    String message() default "특수 문자는 사용할 수 없습니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}


2. 검증 로직 구현

public class NoSpecialCharactersValidator implements ConstraintValidator<NoSpecialCharacters, String> {
    private static final String SPECIAL_CHAR_REGEX = ".*[^a-zA-Z0-9].*"; // 특수문자를 포함하는 패턴

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // null은 허용
        }
        return !value.matches(SPECIAL_CHAR_REGEX); // 특수문자가 포함되면 false 반환
    }
}

3.	사용 예시:

@NoSpecialCharacters(message = "특수 문자가 포함된 제목은 사용할 수 없습니다.")
private String title;

물론 간단한 정규식의 경우 @Pattern으로 대체할 수 있습니다.

@Pattern(regexp = "^[a-zA-Z0-9]*$", message = "특수 문자가 포함된 제목은 사용할 수 없습니다.")
private String title;

3. @Valid vs @Validated: 차이점 및 활용 방안

☕️ @Valid

@Valid는 Java Bean Validation의 표준으로 DTO, 엔티티의 필드에 유효성 검사를 적용하는 데 주로 사용됩니다.

컬렉션이나 객체의 중첩된 필드에 대해 재귀적으로 유효성 검사를 수행합니다.

🧐 유효성 검사 실패 시 MethodArgumentNotValidException이 발생

🍃 @Validated

@Validated는 Spring Framework에서 제공하는 어노테이션으로 특정 그룹을 설정할 수 있습니다.

주로 서비스 계층에서 유효성 검사를 수행할 때 사용하며 메서드 파라미터에서도 유효성 검사를 적용할 수 있습니다.

🧐 유효성 검사 실패시 ConstraintViolationException이 발생


예시

@Valid 예시

컨트롤러 레벨에서 DTO 유효성 검사를 적용할 때 주로 사용합니다:

@PostMapping("/create")
public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDTO) {
    // 유효성 검사를 통과하면 로직을 수행
    return ResponseEntity.ok("User created");
}

@Validated 예시

서비스 계층에서 메서드 단위로 유효성 검사를 적용할 때 유용하며 특정 조건별로 검증 그룹을 설정할 수 있습니다.

@Service
@Validated
public class UserService {

    public void createUser(@Validated(UserGroup.Create.class) UserDTO userDTO) {
        // 유효성 검사를 통과하면 로직을 수행
    }
}

@Validated와 그룹을 함께 사용하여 다양한 유효성 검사 조건을 적용할 수 있습니다.

4. 결과

이 과정을 통해 백엔드에서 일관된 유효성 검사를 구현하여 데이터 누락이나 잘못된 입력으로 인한 오류를 방지하고 사용자 경험을 향상시킬 수 있었습니다.

특히 백엔드에서 에러 메시지를 맞춤형으로 전달하여 프론트엔드에서 상황에 맞는 메시지를 보여줄 수 있도록 개선했습니다.

https://www.junglog.xyz/blogs/post/27

0개의 댓글