Spring

Spring Boot Validation(검증)의 모든 것: 더 나은 코드를 위한 필수 가이드

Written by 개발자서동우 · 1 min read >
Spring Boot 검증(Validation)의 모든 것

안녕하세요! Devloo 입니다. 🙂 데이터 검증은 데이터의 품질을 확인하는 중요한 과정입니다. 마치 선생님이 시험 답안을 채점하듯이, 프로그램에 입력된 정보가 올바르고 규칙에 맞는지 꼼꼼히 확인하는 것이죠.

스프링 부트(Spring Boot)는 이를 위해 어노테이션, 사용자 정의 검증기(Custom Validators), 오류 처리, 그룹 검증 등 다양한 검증 방법을 제공합니다. 이 글에서는 이러한 스프링 부트의 검증 기능을 통해 데이터를 어떻게 신뢰할 수 있게 만드는지 알아보겠습니다. 자 그럼 시작해볼까요 🙂 ?

검증 어노테이션

스프링 부트에서는 어노테이션을 사용해 필드에 특정 검증 규칙을 적용하여 검증을 쉽게 수행할 수 있습니다. 예를 들어, 사용자의 간단한 등록 폼을 검증하는 코드를 살펴보겠습니다.

public class UserRegistrationForm {
    @NotBlank(message = "사용자 이름을 입력해 주세요")
    private String username;

    @Email(message = "유효한 이메일 주소를 입력해 주세요")
    private String email;

    @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다")
    private String password;

    // Getters and setters
}
  • @NotNull: 필드가 null이 아닌지 확인합니다.
  • @NotBlank: null이 아니고, 적어도 하나의 공백이 아닌 문자가 포함되어 있는지 확인합니다.
  • @NotEmpty: 컬렉션이나 배열이 비어 있지 않은지 확인합니다.
  • @Min(value): 숫자 필드가 지정된 최소값 이상인지 확인합니다.
  • @Max(value): 숫자 필드가 지정된 최대값 이하인지 확인합니다.
  • @Size(min, max): 문자열 또는 컬렉션의 크기가 특정 범위 내에 있는지 확인합니다.
  • @Pattern(regex): 필드가 제공된 정규 표현식과 일치하는지 확인합니다.
  • @Email: 필드에 유효한 이메일 주소 형식이 포함되어 있는지 확인합니다.
  • @Digits(integer, fraction): 숫자 필드가 지정된 정수 및 소수 자릿수를 가지고 있는지 확인합니다.
  • @Past@Future: 날짜 또는 시간이 과거 또는 미래인지 확인합니다.
  • @AssertTrue@AssertFalse: 부울 필드가 각각 true와 false인지 확인합니다.
  • @CreditCardNumber: 필드에 유효한 신용카드 번호가 포함되어 있는지 확인합니다.
  • @Valid: 중첩된 객체나 속성의 검증을 트리거합니다.
  • @Validated: 클래스 또는 메서드 수준에서 적용할 검증 그룹을 지정합니다.
@Valid

메서드 파라미터에 @Valid 어노테이션을 적용하면, 스프링 부트는 해당 파라미터를 메서드 호출 전에 자동으로 검증합니다. 이 어노테이션은 객체 앞에 위치하여 해당 객체가 검증 대상임을 나타냅니다. 즉, 이 파라미터로 전달되는 데이터는 지정된 검증 규칙에 따라 검증됩니다.

@RestController
@RequestMapping("/users")
public class ApiController {
    
    private final UserService userService;
    
    public ApiController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<String> createUser(@RequestBody @Valid User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            String errorMessages = bindingResult.getAllErrors()
                                                .stream()
                                                .map(ObjectError::getDefaultMessage)
                                                .collect(Collectors.joining(", "));
            return ResponseEntity.badRequest().body("검증 실패: " + errorMessages);
        }
        userService.saveUser(user);
        return ResponseEntity.ok("사용자가 성공적으로 생성되었습니다");
    }
}

데이터 검증에 실패하면, 스프링 부트는 자동으로 검증 오류 메시지를 생성하고 이를 입력 데이터의 적절한 필드에 연결합니다. 이러한 검증 오류는 일반적으로 BindingResult 객체에 캡처되며, 이를 통해 검증 실패를 분석하고 처리할 수 있습니다.

@Validated

@Validated는 검증 그룹을 지원하기 위해 도입된 어노테이션으로, 특정 필드 그룹에 대한 검증 규칙을 적용할 수 있는 메커니즘을 제공합니다. 표준 @Valid 어노테이션이 전체 빈 객체를 검증하는 것과 달리, @Validated를 사용하면 검증 과정에서 특정 검증 그룹을 지정할 수 있습니다.


중첩 속성 검증

복잡한 객체를 다룰 때, 중첩된 속성에도 검증이 필요한 경우가 있습니다. 이때 @Valid 어노테이션을 사용하여 상위 객체와 중첩된 속성이 모두 올바르게 검증되도록 할 수 있습니다.

public class Order {
    @NotNull
    private String orderId;

    @Valid
    private ShippingAddress shippingAddress;

    // 기타 속성, getters, setters...
}

public class ShippingAddress {
    @NotNull
    private String street;

    @NotNull
    @Size(min = 2, max = 50)
    private String city;

    @NotNull
    private String zipCode;
}

표현 계층(Presentation Layer)의 검증

검증은 주로 사용자 입력을 받는 컨트롤러에서 이루어집니다.

컨트롤러에서의 검증#1
@Controller
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/register")
    public String registerUser(@Valid UserRegistrationForm form, BindingResult result, Model model) {
        if (result.hasErrors()) {
            model.addAttribute("errors", result.getAllErrors());
            return "registrationForm"; // 오류 메시지와 함께 폼으로 돌아감
        }
        
        userService.registerUser(form);
        return "redirect:/login";
    }
}
컨트롤러에서의 검증#2
@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<String> createUser(@RequestBody @Valid User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            String errorMessages = bindingResult.getAllErrors()
                                                .stream()
                                                .map(ObjectError::getDefaultMessage)
                                                .collect(Collectors.joining(", "));
            return ResponseEntity.badRequest().body("검증 오류가 발견되었습니다: " + errorMessages);
        }
        
        userService.saveUser(user);
        return ResponseEntity.ok("사용자가 성공적으로 생성되었습니다.");
    }
}

서비스 계층(Server Layer)의 검증

서비스 계층에서 유효성 검증을 수행하는 것은 애플리케이션의 데이터 무결성을 유지하고 비즈니스 로직의 일관성을 보장하는 중요한 방법입니다. 서비스 계층은 주로 비즈니스 로직을 처리하며, 입력 데이터가 올바른지 확인하는 것은 이러한 로직이 올바르게 동작하도록 합니다. 아래는 코드 예시입니다 :

import org.springframework.stereotype.Service;
import javax.validation.Validator;
import javax.validation.ConstraintViolation;
import java.util.Set;

@Service
@AllArgsConstructor
public class UserService {
    private final Validator validator;

    public void createUser(User user) {
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        if (!violations.isEmpty()) {
            // 검증 오류 처리
        }
    }
}

이 예제에서 @Valid 어노테이션은 createUser 메서드에 사용되어 요청 본문으로 받은 User 객체를 검증합니다. BindingResult 객체는 검증 오류를 캡처하는 데 사용됩니다.

글로벌 예외 처리 (Global Exception Handling)

검증 오류는 피할 수 없습니다. 스프링 부트는 이를 전역적으로 처리할 수 있는 방법을 제공합니다:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationException(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);
    }
}

@ControllerAdvice 어노테이션은 클래스를 전역 예외 처리기로 표시하며, MethodArgumentNotValidException 발생 시 이를 처리합니다. (자세한 설명은 해당 글을 참고하세요 !)

사진: UnsplashLiana S

사용자 정의 검증 (Custom Validation)

1. 사용자 정의 검증 어노테이션 (Custom Validation Annotation)

새로운 어노테이션을 정의하여 사용자 정의 검증 어노테이션을 생성합니다. 이 어노테이션은 클래스의 필드나 메서드에 적용할 검증 규칙을 지정합니다.

  • @Target: 어노테이션이 적용될 위치를 정의합니다. 예제에서는 필드와 메서드에 지정됩니다.
  • @Retention: 어노테이션이 유지되는 기간을 지정합니다. RUNTIME은 실행 시에 검증을 위해 어노테이션이 사용 가능함을 의미합니다.
  • @Constraint: 검증 로직을 구현하는 검증기 클래스(Validator Class)를 지정합니다.
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomValidator.class)
public @interface CustomValidation {
    String message() default "유효하지 않은 값입니다";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
2. 사용자 정의 검증기 (Custom Validator)

사용자 정의 검증기는 ConstraintValidator 인터페이스를 구현하는 클래스입니다. 예제에서 검증기 클래스는 CustomValidator이며, 이는 ConstraintValidator<CustomValidation, String>을 구현합니다. 이는 @CustomValidation 어노테이션이 적용된 문자열 필드를 검증하는 역할을 합니다.

  • initialize(): 검증기를 초기화합니다. 필요한 경우 어노테이션 속성에 접근할 수 있습니다.
  • isValid(): 실제 검증 로직을 수행합니다. 검증 중인 필드 값(이 경우 문자열)과 검증 동작을 커스터마이징하기 위한 ConstraintValidatorContext를 받습니다.
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CustomValidator implements ConstraintValidator<CustomValidation, String> {
    @Override
    public void initialize(CustomValidation constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 여기서 검증 로직을 구현합니다
        // 검증이 통과하면 true를, 그렇지 않으면 false를 반환합니다
        return value != null && value.startsWith("ABC"); // 예시 검증 조건
    }
}
3. 사용자 정의 검증 어노테이션 사용

클래스의 필드에 사용자 정의 검증 어노테이션을 적용합니다. 예제에서 Data 클래스는 customField라는 필드를 가지고 있으며, @CustomValidation 어노테이션이 적용되어 있습니다. 이 어노테이션은 관련된 검증기(CustomValidator)에 정의된 검증 로직을 트리거합니다.

public class Data {
    @CustomValidation
    private String customField;
}

이렇게 하면, Data 클래스의 customField 필드는 @CustomValidation 어노테이션에 정의된 검증 규칙을 따르게 됩니다.


검증 그룹 (Validation Groups)

검증 그룹을 사용하면 다양한 상황에 맞게 특정 검증 규칙을 적용할 수 있습니다. 여기서는 기본 정보와 고급 정보를 포함한 사용자 등록 폼을 예로 들어 설명하겠습니다.

1단계: 검증 그룹 정의

먼저, 각기 다른 검증 그룹을 나타내는 마커 인터페이스를 생성합니다:

//아래는 마커 인터페이스 입니다.
public interface BasicInfo {}
public interface AdvancedInfo {}


public class User {

    @NotNull(groups = BasicInfo.class)
    @NotNull(groups = AdvancedInfo.class)
    private String username;

    @NotNull(groups = AdvancedInfo.class)
    private String email;

    // 기타 필드, getters, setters
}
2단계: 검증 그룹 적용

컨트롤러 메서드에서 원하는 검증 그룹과 함께 @Validated 어노테이션을 사용합니다:

import org.springframework.validation.annotation.Validated;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.validation.BindingResult;

@Controller
@Validated
public class UserController {

    @PostMapping("/registerBasicInfo")
    public String registerBasicInfo(@Validated(BasicInfo.class) @ModelAttribute UserRegistrationForm form, BindingResult result) {
        if (result.hasErrors()) {
            return "basicInfoForm";
        }
        // 기본 정보 등록 처리
        return "redirect:/success";
    }

    @PostMapping("/registerAdvancedInfo")
    public String registerAdvancedInfo(@Validated(AdvancedInfo.class) @ModelAttribute UserRegistrationForm form, BindingResult result) {
        if (result.hasErrors()) {
            return "advancedInfoForm";
        }
        // 고급 정보 등록 처리
        return "redirect:/success";
    }
}

이렇게 하면 각 메서드가 특정 검증 그룹에 맞는 검증 규칙을 적용받아 검증을 수행할 수 있습니다.


마무리

데이터 검증은 우리가 사용하는 데이터가 올바르고 신뢰할 수 있는지를 확인하는 중요한 과정입니다. 스프링 부트(Spring Boot)는 다양한 검증 방법을 제공하여 개발자가 더 쉽고 효과적으로 데이터 검증을 수행할 수 있도록 돕습니다.

이 글에서 살펴본 것처럼, 검증 어노테이션을 사용하여 간단한 검증을 수행하거나, 사용자 정의 검증기를 만들어 특정 요구사항에 맞는 검증을 할 수 있습니다. 또한, 검증 오류를 처리하고, 글로벌 예외 처리기를 통해 애플리케이션 전체의 검증 오류를 관리할 수 있습니다.

데이터 검증을 통해 잘못된 데이터가 시스템에 들어오는 것을 방지하면, 프로그램의 안정성과 신뢰성을 높일 수 있습니다. 이러한 검증 절차는 단순히 개발자의 편의를 넘어서, 사용자에게 더 나은 서비스를 제공하는 데 중요한 역할을 합니다.

스프링 부트의 검증 기능을 잘 활용하여 여러분의 애플리케이션이 더 견고하고 안정적으로 동작할 수 있도록 해보세요. 데이터 검증은 복잡하지 않으며, 오히려 프로그램의 품질을 높이는 중요한 단계임을 기억하시기 바랍니다.

끝까지 읽어주셔서 정말 감사합니다 🙂 . 궁금하신 점은 댓글로 남겨주세요 ~!! (_ _)

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *