1. 🖼️ 프론트엔드에만 의존하는 Validation
프론트에서만 validation이 이루어지는 상황에서는 JavaScript로 직접 form을 전송하거나 input 검사가 제대로 작동하지 않으면 백엔드의 별도 처리가 없다면 데이터가 그대로 넘어가 서비스가 실행됩니다.
2. ✍️ 백엔드 검증의 부족
백엔드에서 기본적인 검증조차 이루어지지 않아 필수 필드가 누락될 경우 NullPointerException(NPE), 심지어는 DB Exception과 같은 오류가 발생합니다. 이는 서비스의 안정성을 크게 저해할 수 있습니다. 뿐만 아니라 아무리 간단한 쿼리라도 디비에 불필요한 부하(query exception or 검증되지 않은 데이터 insert)를 가하는 것입니다.
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;
@Valid는 Java Bean Validation의 표준으로 DTO, 엔티티의 필드에 유효성 검사를 적용하는 데 주로 사용됩니다.
컬렉션이나 객체의 중첩된 필드에 대해 재귀적으로 유효성 검사를 수행합니다.
🧐 유효성 검사 실패 시 MethodArgumentNotValidException이 발생
@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와 그룹을 함께 사용하여 다양한 유효성 검사 조건을 적용할 수 있습니다.
이 과정을 통해 백엔드에서 일관된 유효성 검사를 구현하여 데이터 누락이나 잘못된 입력으로 인한 오류를 방지하고 사용자 경험을 향상시킬 수 있었습니다.
특히 백엔드에서 에러 메시지를 맞춤형으로 전달하여 프론트엔드에서 상황에 맞는 메시지를 보여줄 수 있도록 개선했습니다.