Bean Validation

JeongHoHyun·2025년 2월 11일

Spring MVC

목록 보기
12/21

Bean Validation

매번 검증 기능을 코드로 작성하는 것이 번거롭기 때문에, 자주 사용하는 검증 로직(빈 값체크, 크기 체크 등)을 모든 프로젝트에 적용할 수 있게 공통화하고 표준화 한 것이다.
애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.

📌 의존관계 추가

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

예제

package hello.itemservice.domain.item;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Item {

    private Long id;
    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

검증 애노테이션

  • @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않음.
  • @NotNull : null을 허용하지 않는다.
  • @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
  • @Max(9999) : 최대 9999까지만 허용된다.

📌 에러 코드

Bean Validation이 제공하는 오류 메시지를 좀더 자세히 변경하고싶을때.

메시지 등록

errors.properties

#Bean Validation 추가
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
  • {0}은 필드명이고, {1}, {2}... 은 각 애노테이션마다 다르다.

메시지 찾는 순서

    1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
    1. 애노테이셔의 message속성 사용 (ex : @NotBlank(message = "공백! {0}"))
    1. 라이브러리가 제공하는 기본값 사용 -> 공백일 수 없습니다.

📌 오브젝트 오류

Field Error가 아닌 ObjectError 처리방법은 다음과 같이 @ScriptAssert()를 사용하면 된다.

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총 합이 10,000원이 넘게 입력해 주세요")
public class Item {

    private Long id;
    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}
  • 실제로 사용하면 제약이 많고 복잡하다.
  • 실무에서는 검증이 해당 객체의 범위를 넘어서는 경우들도 있기 때문에 사용하지 않는 것을 권장한다.
  • 직접 자바 코드를 사용하여 검증을 하는것이 유리하다.

📌 groups

데이터를 등록할 때와 수정할 때는 요구사항이 다를 수 있다.
동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법을 사용해야한다.

방법

    1. BeanValidation의 groups 기능을 사용한다.
    1. Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용한다.

groups 기능 사용

저장용 groups 생성

package hello.itemservice.domain.item;

public interface SaveCheck {
}

수정용 groups 생성

package hello.itemservice.domain.item;

public interface UpdateCheck {
}

Item - groups 적용

package hello.itemservice.domain.item;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Item {

    @NotNull(groups = UpdateCheck.class)
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String itemName;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Max(value = 9999, groups = SaveCheck.class)
    private Integer quantity;

    .
    .
    .
}

컨트롤러에 groups 적용

@PostMapping("/add")
    public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    	// 저장 로직
        ...
    }
.
.
.

@PostMapping("/{itemId}/edit")
    public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
    	// 업데이트 로직
        ...
    }

🧹 정리

  • groups 기능을 사용해 등록과 수정시 각각 다르게 검증을 할 수 있었다.
  • 하지만 groups기능을 사용하니 전반족으로 복잡도가 올라갔다.
  • 따라서 실무에서는 groups를 잘 사용하지 않고, 등록용 객체, 수정용 객체를 분리해서 사용한다.

⭐️ Form 전송 객체 분리

실무에서는 groups기능을 사용하지 않고 전송객체를 분리하여 사용한다.

저장용 폼 객체

package hello.itemservice.web.validation.form;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class ItemSaveForm {

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;
}

수정용 폼 객체

package hello.itemservice.web.validation.form;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ItemUpdateForm {

    @NotBlank
    private Long id;

    @NotNull
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000)
    private Integer price;

    private Integer quantity;
}

컨트롤러 수정

@PostMapping("/add")
    public String addItemV2(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        // 특정 필드가 아닌 복합 검증 룰
        if (form.getPrice() != null && form.getQuantity() != null) {
            int resultPrice = form.getPrice() * form.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
            }
        }

        // 검증에 실패하면 다시 입력 폼으로
        if (bindingResult.hasErrors()) {
            log.error("errors = {}", bindingResult);
            // bindingResult 는 model에 담지 않아도 뷰로 넘어간다.
            return "validation/v4/addForm";
        }

        // 성공 로직
        // 아이템 객체에 담아주고 아이템 객체를 넘겨주어야한다.
        Item item = new Item();
        item.setItemName(form.getItemName());
        item.setPrice(form.getPrice());
        item.setQuantity(form.getQuantity());

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v4/items/{itemId}";
    }
    .
    .
    .
@PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId, @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {

        // 특정 필드가 아닌 복합 검증 룰
        if (form.getPrice() != null && form.getQuantity() != null) {
            int resultPrice = form.getPrice() * form.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
            }
        }

        if (bindingResult.hasErrors()) {
            log.error("errors = {}", bindingResult);
            return "validation/v4/editForm";
        }

		// 아이템 객체에 담아주고 아이템 객체를 넘겨주어야한다.
        Item itemParam = new Item();
        itemParam.setItemName(form.getItemName());
        itemParam.setPrice(form.getPrice());
        itemParam.setQuantity(form.getQuantity());

        itemRepository.update(itemId, itemParam);
        return "redirect:/validation/v4/items/{itemId}";
    }

HTTP 메시지 컨버터 (@RequestBody)

@Valid, @ValidatedHttpMessageConverter(@RequestBody)에도 적용할 수 있다.

profile
Java Back-End 2022.11.01 💻~ing

0개의 댓글