프로그래밍/Spring

@Validated 적용기

hwangsehee 2025. 5. 3. 16:51

 

 

상황


1차 프로젝트를 진행하면서 

@Validated로 유효성 로직을 구현하였다. 

 

내가 체크해야될 필드는 

아래 사진에서 보이는 필드 값들을

모두 not null + jpa 기본 컬럼 사이즈인 varchar(255)를 넘지 않는지 사이즈 체크를 해야했다. 

 

우리 팀은 이미지 등록을 DB에 byte [] 로 저장하게 구현했으므로 

이미지 validate 는 커스텀 어노테이션을 만들어야만했다. 

 

(상품당 이미지는 1개만 들어가기도했고, 이미지의 크기가 작아서 이런 저장방식으로 구현했는데

멘토님,강사님이 이렇게 이미지 구현하는 것은 좋지 않다고 말씀해주셨다. 

사실 팀원들도 모두 알고있었던건데 워낙 간단한 프로젝트라서 방심(?)했던 것 같다)

상품 등록 입력 폼

 

유효성 검사 로직 구현 화면

 

구현 코드 


 

Request용 DTO 

@Getter
@Setter
@NoArgsConstructor
public class ProductCreateRequest {

    @NotBlank(message = "상품명은 필수입니다.")
    @ByteSize(max = 255)
    private String name;

    private Category category;

    @NotBlank(message = "상품설명은 필수입니다.")
    @ByteSize(max = 255)
    private String description;

    @NotBlank(message = "가격 입력은 필수입니다.")
    @ByteSize(max = 255)
    private String price;

    @ProductValidFile
    private MultipartFile file;

    @Builder
    public ProductCreateRequest(String name, Category category, String description, String price,
        MultipartFile file) {
        this.name = name;
        this.category = category;
        this.description = description;
        this.price = price;
        this.file = file;
    }
}

 

우선 DTO 에서 

@NotBlank를 통해 not null 유효성 체크를 걸어준다.

이때 메세지를 설정할 수 있는데,

이 메세지는 화면에서 보여주기 위한 메세지이다.

 

@ByteSize@ProductValidFile은 

내가 만든 커스텀 어노테이션이다. 

마지막에 설명할 예정이다. 

 

 @PostMapping
    public String saveProduct(
        @Validated @ModelAttribute("productForm") ProductCreateRequest productCreateRequest,
        BindingResult bindingResult, Model model) {
        if (bindingResult.hasErrors()) {
            model.addAttribute("categories", Category.values());
            model.addAttribute("isNew", true);

            if (!productCreateRequest.getFile().isEmpty()) {
                model.addAttribute("imageReUploadNotice", "다른 항목 오류로 인해 이미지는 다시 업로드해 주세요.");
            }
            return "admin/view_save_products";
        }
        Product product = productService.saveProduct(productCreateRequest);

        return "redirect:/admin/products/" + product.getId();
    }

 

Controller 에서

화면에서 넘어오는 값인 ProductCreateRequuest(request용 DTO)에 

@Validtaed 어노테이션을 붙여주었다.

 

바로 뒤에는 BindingResult 객체를 받아온다. 

이 객체는 DTO 에 선언해놓은 

유효성 체크를 통과하지 못할 때

.hasErrors()가 true를 반환한다. 

 

<div id="nameError" class="errorMsg" th:if="${#fields.hasErrors('name')}"
               th:errors="*{name}" style="padding-top:5px;color:red">
          </div>

 

 

화면에서 에러 메세지를 보여주기 위해

div 요소를 위와 같이 작성하였다. 

(타임리프 기준)

 

커스텀 어노테이션 (@ByteSize, @ProductValidFile) 


@ByteSize 어노테이션을 만든 이유는 

 

한글은 3byte, 영어는 1byte로 

@Size 어노테이션만으로는 정확한 유효성 체크를 할 수 없다고 생각했다. 

 

@Size 어노테이션만으로 체크를 한다면 

max = 255 로 설정했을 때 

이 값은 byte 값으로 검사하는것이 아니기 때문에 

(글자 수를 체크한다)

이 유효성 로직을 통과하여도 실제 DB 저장시에는 

필드 사이즈를 초과하여 저장 오류가 발생할 수 있다.

 

 

우선 Validator를 만들어주었다. 

public class ByteSizeValidator implements ConstraintValidator<ByteSize, String> {

    private int max;

    @Override
    public void initialize(ByteSize constraintAnnotation) {
        this.max = constraintAnnotation.max();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if(value == null){
            return true;
        }
        int byteLength = value.getBytes(StandardCharsets.UTF_8).length;

        return byteLength <= max;
    }
}

 

이 내용이 실질적인 검증로직인 셈 이다. 

최소값은 not null로 설정했으니 

max 값만 설정해주었다. 

 

입력받은 string값을 utf-8 byte 값으로 변환한 후 

max 값을 넘지 않는다면 true, 그렇지 않으면 false 를 반환하게 구현하였다. 

 

ConstraintValidator<A, T> 

이 부분 설정은 A에는 내가 만든 어노테이션 타입을,

T에는 검증할 타입을 명시해주면 된다.

 

ByteSize Annotation 생성

@Documented
@Constraint(validatedBy = ByteSizeValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ByteSize {

    String message() default "바이트 길이가 허용 범위를 초과했습니다.";

    int max();

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

    Class<? extends Payload>[] payload() default {};

}

 

위와 같이 어노테이션을 생성해주었다. 

허용 글자수가 넘었을 때 

화면에서 보여줄 메세지를 설정하였고 

@Constraint(validatedBy = ByteSizeValidator.class)를 통해
@ByteSize 어노테이션에 ByteSizeValidator를 지정하여 유효성 검사를 수행하도록 구성하였다.

 

@Target 으로 필드 레벨에 적용할 수 있도록 명시한다. 

@Retention(RetentionPolicy.RUNTIME) 으로 

컴파일 시, class 파일에도 기록되며 runtime 도중에도 어노테이션이 유효할 수 있게 설정했다. 

 

 

다음으로는 @ProductValidFile 어노테이션을 만들었다.

이 어노테이션은 파일 객체가 비어있는지는 

@NotNull로 체크가 가능하지만, 

파일 사이즈를 제한하기 위해 만들었다. 

 

만드는 방식은 위와 동일하다. 

 

우선 Validator 를 만들어주고 

public class ProductFileValidator implements ConstraintValidator<ProductValidFile, MultipartFile> {

    private long maxSize;

    @Override
    public void initialize(ProductValidFile constraintAnnotation) {
        this.maxSize = constraintAnnotation.maxSize();
    }

    @Override
    public boolean isValid(MultipartFile file,
        ConstraintValidatorContext context) {

        if (file == null || file.isEmpty()) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("파일은 필수입니다.").addConstraintViolation();
            return false;
        }

        if (file.getSize() > maxSize) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("파일은 최대 " + (maxSize / 1024 / 1024) + "MB까지 업로드 가능합니다.").addConstraintViolation();
            return false;
        }

        return true;
    }
}

 

내가 설정하고 싶은 값을 체크하는 로직을 구현한다. 

 

나는 파일 사이즈를 1MB 로 제한하였고,

(아래 어노테이션에서 설정)

파일 객체가 비어있는지 체크를 하였다. 

 

@ProductValidFile 어노테이션

@Documented
@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ProductFileValidator.class)
public @interface ProductValidFile {

    String message() default "파일을 선택해주세요.";

    long maxSize() default 1048576;

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

    Class<? extends Payload>[] payload() default {};
}

 

 

maxSize를 1M 로 설정하였다.

 

여기서 spring boot는 기본적으로 

 

MultipartFile에 대한 크기를 1MB로 제한하고있다. 

 

그래서 내가 구현한 유효성 로직에 도달하기도 전에 에러 페이지를 응답한다.

 

따라서 applicatioin.yml 에 다음과 같은 설정값을 추가하였다. 

  spring:
      servlet:
        multipart:
          max-request-size: 10MB
          max-file-size: 10MB

 

 

이미지가 제한 크기를 초과했을 때 에러 메세지