Spring Framework 핵심 기술 - Validation, Data binding

h.Im·2024년 8월 27일

Springboot 기초

목록 보기
4/17
post-thumbnail

Validation

개발자는 값에 대한 검증을 여러 방면에서 수행해야 합니다.

데이터 검증

  • 필수 데이터의 존재 여부
  • 문자열의 길이나 숫자형 데이터의 경우 값의 범위
  • email, 신용카드 번호 등 특정 형식에 맞춘 데이터

비즈니스 검증

  • 서비스 정책에 따라 데이터를 확인하여 검증
  • 경우에 따라 외부 api를 호출하거나 DB의 데이터까지 조회하여 검증하는 경우도 존재

Spring의 검증

스프링은 웹 레이어에 종속적이지 않은 방법으로 Validation을 하려고 의도하고 있으며 주로 아래 두 가지 방법을 활용하여 Validationd을 진행합니다.

Java Bean Validation

@NotBlank
@Size
@Min
@Email
등의 어노테이션을 이용한 방법입니다. 요청 dto에 어노테이션으로 명시한 후 @Valid 어노테이션을 @RequestBody에 달게 되면, Java Bean Validation을 수행한 후 문제가 없을 때만 메서드 내부로 진입합니다. 검증 중 실패가 발생하면 MethodArgumentNotValidException이 발생합니다.

예시 코드

import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

public class UserRequestDTO {

    @NotBlank(message = "Name must not be blank")
    @Size(min = 2, max = 30, message = "Name must be between 2 and 30 characters")
    private String name;

    @NotBlank(message = "Email must not be blank")
    @Email(message = "Email should be valid")
    private String email;

    @Min(value = 18, message = "Age must be at least 18")
    private int age;
    ...
}
 --------------------------------------------------------

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;

@RestController
public class UserController {

    @PostMapping("/users")
    public ResponseEntity<String> createUser(@Valid @RequestBody UserRequestDTO userRequestDTO) {
        // 정상적인 검증이 통과되면 이 메서드가 실행됩니다.
        return ResponseEntity.ok("User created successfully");
    }

    // Validation 실패 시 MethodArgumentNotValidException이 발생하며, 아래의 핸들러가 호출됩니다.
    @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);
    }
}

Spring Validator 인터페이스 구현을 통한 validation

Java Bean Validation 보다 복잡한 검증이 필요할 때 사용할 수 있는 방법입니다.

예시 코드

import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

@Component
public class UserRequestValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return UserRequestDTO.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        UserRequestDTO userRequest = (UserRequestDTO) target;

        // 공백 체크
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.empty", "Name must not be empty");
        
        // 이메일 형식 체크
        if (userRequest.getEmail() != null && !userRequest.getEmail().contains("@")) {
            errors.rejectValue("email", "email.invalid", "Email should be valid");
        }

        // 나이 체크 (예: 최소 나이 18세)
        if (userRequest.getAge() < 18) {
            errors.rejectValue("age", "age.invalid", "Age must be at least 18");
        }
    }
}

-----------------------------------------
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    private final UserRequestValidator userRequestValidator;

    @Autowired
    public UserController(UserRequestValidator userRequestValidator) {
        this.userRequestValidator = userRequestValidator;
    }

    @PostMapping("/users")
    public ResponseEntity<String> createUser(@RequestBody UserRequestDTO userRequestDTO, BindingResult result) {
        // 커스텀 검증 로직 수행
        userRequestValidator.validate(userRequestDTO, result);

        if (result.hasErrors()) {
            // 검증 실패 시, 에러 메시지를 반환
            return ResponseEntity.badRequest().body(result.getAllErrors().toString());
        }

        // 정상 처리
        return ResponseEntity.ok("User created successfully");
    }
}

Spring Validator 인터페이스 사용 시 주의 사항은 아래와 같습니다.

  • validation이 너무 여러 군데에 흩어져 있으면 테스트 및 유지보수성이 떨어짐(중복으로 검증하거나, 서로 다른 검증 로직으로 검증될 가능성 존재)
  • validation은 로직 초기에 수행하고, 실패 시에는 exception을 던져주는 것이 유지보수에 용이

Data Binding

데이터 바인딩을 위해 데이터 컨버터가 필요할 수 있습니다. S 타입을 T 타입으로 변환해주는 컨버터에 대해 알아보겠습니다.

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
public class UserRequestDTOToUserConverter implements Converter<UserRequestDTO, User> {

    @Override
    public User convert(UserRequestDTO source) {
        User user = new User();
        user.setName(source.getName());
        user.setEmail(source.getEmail());

        // 나이는 String에서 int로 변환합니다.
        try {
            user.setAge(Integer.parseInt(source.getAge()));
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Invalid age format: " + source.getAge());
        }

        return user;
    }
}


-------------------------
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    private final ConversionService conversionService;

    @Autowired
    public UserController(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    @PostMapping("/users")
    public ResponseEntity<String> createUser(@RequestBody UserRequestDTO userRequestDTO) {
        // ConversionService를 이용하여 UserRequestDTO를 User로 변환합니다.
        User user = conversionService.convert(userRequestDTO, User.class);

        if (user != null) {
            return ResponseEntity.ok("User created successfully: " + user.getName());
        } else {
            return ResponseEntity.badRequest().body("Conversion failed");
        }
    }
}
  • Converter를 만들어서 Spring에 Bean으로 등록
  • 스프링 내에 ConversionService라는 내장된 서비스에서 Converter 구현체 Bean들을 Converter 리스트에 등록
  • 외부데이터가 들어오고, Source Class Type -> Target Class Type이 Converter에 등록된 형식과 일치하면 해당 Converter가 동작하는 원리

0개의 댓글