[Spring] 검증1 - Validation

영우·2026년 9월 8일

BindingResult1

지금부터 스프링이 제공하는 검증 오류 처리 방법을 알아보자.

@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
                        RedirectAttributes redirectAttributes) {

    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.addError(
                new FieldError("item", "itemName", "상품 이름은 필수입니다.")
        );
    }

    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        bindingResult.addError(
                new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다.")
        );
    }

    if (item.getQuantity() == null || item.getQuantity() >= 10000) {
        bindingResult.addError(
                new FieldError("item", "quantity", "수량은 최대 9,999 까지 허용합니다.")
        );
    }

    // 특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();

        if (resultPrice < 10000) {
            bindingResult.addError(
                    new ObjectError("item",
                            "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice)
            );
        }
    }

    if (bindingResult.hasErrors()) {
        log.info("errors={}", bindingResult);
        return "validation/v2/addForm";
    }

    // 성공 로직
    Item savedItem = itemRepository.save(item);

    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);

    return "redirect:/validation/v2/items/{itemId}";
}

BindingResult bindingResult 파라미터의 위치는 @ModelAttribute Item item 다음에 와야 한다.

필드 오류 - FieldError

if (!StringUtils.hasText(item.getItemName())) {
  bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}

필드에 오류가 있으면 FieldError 객체를 생성해서 bindingResult 에 담아두면 된다.

  • objectName : @ModelAttribute 이름
  • field : 오류가 발생한 필드 이름
  • defaultMessage : 오류 기본 메시지

글로벌 오류 - ObjectError

bindingResult.addError(
        new ObjectError("item",
                "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice)
);

특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서 bindingResult 에 담아두면 된다.

  • objectName : @ModelAttribute 의 이름
  • defaultMessage : 오류 기본 메시지
<form action="item.html" th:action th:object="${item}" method="post">

    <div th:if="${#fields.hasGlobalErrors()}">
        <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메시지</p>
    </div>

    <div>
        <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
        <input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
        <div class="field-error" th:errors="*{itemName}">상품명 오류</div>
    </div>

    <div>
        <label for="price" th:text="#{label.item.price}">가격</label>
        <input type="text" id="price" th:field="*{price}" th:errorclass="field-error" class="form-control" placeholder="가격을 입력하세요">
        <div class="field-error" th:errors="*{price}">가격 오류</div>
    </div>

    <div>
        <label for="quantity" th:text="#{label.item.quantity}">수량</label>
        <input type="text" id="quantity" th:field="*{quantity}" th:errorclass="field-error" class="form-control" placeholder="수량을 입력하세요">
        <div class="field-error" th:errors="*{quantity}">수량 오류</div>
    </div>

</form>

타임리프 스프링 검증 오류 통합 기능
타임리프는 스프링의 BindingResult 를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공한다.

  • #fields : #fieldsBindingResult 가 제공하는 검증 오류에 접근할 수 있다.
  • th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전이다.
  • th:errorclass : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.

글로벌 오류 처리

<div th:if="${#fields.hasGlobalErrors()}">
  <p class="field-error" 
     th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
</div>

필드 오류 처리

<input type="text" id="itemName" th:field="*{itemName}"
	th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">
  상품명 오류
</div>

BindingResult2

  • 스프링이 제공하는 검증 오류를 보관하는 객체이다. 검증 오류가 발생하면 여기에 보관하면 된다.
  • BindingResult 가 있으면 @ModelAttribute 에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다!

예) @ModelAttribute에 바인딩 시 타입 오류가 발생하면?

  • BindingResult 가 없으면 -> 400 오류가 발생하면서 컨트롤러가 호출되지 않고, 오류 페이지로 이동한다.
  • BindingResult 가 있으면 오류 정보(FieldError )를 BindingResult 에 담아서 컨트롤러를 정상 호출한다.

타입 오류 확인
숫자가 입력되어야 할 곳에 문자를 입력해서 타입을 다르게 해서 BindingResult 를 호출하고 bindingResult 의 값을 확인해보자.

FieldError, ObjectError

사용자 입력 오류 메시지가 화면에 남도록 하자.
예) 가격을 1000원 미만으로 설정시 입력한 값이 남아있어야 한다.

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

    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.addError(new FieldError("item", "itemName",
                item.getItemName(), false, null, null,
                "상품 이름은 필수입니다."));
    }

    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        bindingResult.addError(new FieldError("item", "price",
                item.getPrice(), false, null, null,
                "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
    }

    if (item.getQuantity() == null || item.getQuantity() >= 10000) {
        bindingResult.addError(new FieldError("item", "quantity",
                item.getQuantity(), false, null, null,
                "수량은 최대 9,999 까지 허용합니다."));
    }

    // 특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();

        if (resultPrice < 10000) {
            bindingResult.addError(new ObjectError("item", null, null,
                    "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
        }
    }

    if (bindingResult.hasErrors()) {
        log.info("errors={}", bindingResult);
        return "validation/v2/addForm";
    }

    // 성공 로직
    Item savedItem = itemRepository.save(item);

    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);

    return "redirect:/validation/v2/items/{itemId}";
}

FieldError 생성자

public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage)

파라미터 목록

  • objectName : 오류가 발생한 객체 이름
  • field : 오류 필드
  • rejectedValue : 사용자가 입력한 값(거절된 값)
  • bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
  • codes : 메시지 코드
  • arguments : 메시지에서 사용하는 인자
  • defaultMessage : 기본 오류 메시지

오류 발생시 사용자 입력 값 유지
new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다.")

FieldError 는 오류 발생시 사용자 입력 값을 저장하는 기능을 제공한다.

여기서 rejectedValue 가 바로 오류 발생시 사용자 입력 값을 저장하는 필드다.
bindingFailure 는 타입 오류 같은 바인딩이 실패했는지 여부를 적어주면 된다. 여기서는 바인딩이 실패한 것은 아니기 때문에 false 를 사용한다.

타임리프의 사용자 입력 값 유지
th:field="*{price}"
정상 상황에는 모델 객체의 값을 사용하지만, 오류가 발생하면 FieldError 에서 보관한 값을 사용해서 값을 출력한다.

스프링의 바인딩 오류 처리
타입 오류로 바인딩에 실패하면 스프링은 FieldError 를 생성하면서 사용자가 입력한 값을 넣어둔다. 그리고 해당 오류를BindingResult 에 담아서 컨트롤러를 호출한다. 따라서 타입 오류 같은 바인딩 실패시에도 사용자의 오류 메시지를 정상 출력할 수 있다.

오류 코드와 메세지 처리

BindingResult 가 제공하는 rejectValue() , reject() 를 사용하면 FieldError , ObjectError 를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.

@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item,
                        BindingResult bindingResult,
                        RedirectAttributes redirectAttributes) {

    log.info("objectName={}", bindingResult.getObjectName());
    log.info("target={}", bindingResult.getTarget());

    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.rejectValue("itemName", "required");
    }

    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        bindingResult.rejectValue(
                "price",
                "range",
                new Object[]{1000, 1000000},
                null
        );
    }

    if (item.getQuantity() == null || item.getQuantity() > 10000) {
        bindingResult.rejectValue(
                "quantity",
                "max",
                new Object[]{9999},
                null
        );
    }

    // 특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();

        if (resultPrice < 10000) {
            bindingResult.reject(
                    "totalPriceMin",
                    new Object[]{10000, resultPrice},
                    null
            );
        }
    }

    if (bindingResult.hasErrors()) {
        log.info("errors={}", bindingResult);
        return "validation/v2/addForm";
    }

    // 성공 로직
    Item savedItem = itemRepository.save(item);

    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);

    return "redirect:/validation/v2/items/{itemId}";
}

ValidationUtils

ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");

스프링이 직접 만든 오류 메시지 처리

스프링은 타입 오류가 발생하면 typeMismatch 라는 오류 코드를 사용한다.

properties에 내용 추가

typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.

결과적으로 소스코드를 하나도 건들지 않고, 원하는 메시지를 단계별로 설정할 수 있다.

0개의 댓글