반응형

Validation(검증) 이란?

  • 올바르지 않은 데이터(타입에러, 범위에러 등)가 입력되었을 때 걸러내는 작업
  • validation을 할 때 Client Side 뿐아닌 Server Side에서의 validation도 중요

  • validation이 없다면 입력을 마친 상품 등록 폼을 Controller에서 전송 받고 이 정보들로 별도의 검증 없이 바로 상품 객체로 만들어 등록 후 다음화면으로 이동시킴
  • validation이 있다면 입력을 마친 상품 등록 폼이 Controller에 전송 되었을 때 Controller에서 이 입력값들에 대한 validation을 진행함
    • validation을 통과해야 객체 생성, 등록 후 다음 화면으로 이동
    • validation을 통과하지 못하면 전에 사용자의 입력 + 에러메세지를 들고 다시 상품 등록 폼으로 이동시킴

@Valid를 활용한 Validation 진행 예제

  • 상품을 등록하는 예제를 통해 Validation을 진행해 봄
  • 사용자가 상품을 등록할 때, 아래와 같은 Validation 조건들이 있음
    1. 이름 : 비어있으면 안되고, 최대 10글자까지 허용 (필드 에러)
    2. 판매자 이메일 : 비어있으면 안되고, 이메일 형식이여야 함 (필드 에러)
    3. 가격 : 비어있으면 안되고, 100원 이상 1,000,000원 이하여야 함 (필드 에러)
    4. 수량 : 비어있으면 안되고, 최대 999개까지 허용 (필드 에러)
    5. 추가적으로 가격 * 수량이 500,000,000원을 넘으면 안됨 (글로벌 에러)

spring-boot-starter-validation 라이브러리 추가

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

Item 객체

  • Item 객체
@Data
@AllArgsConstructor
public class Item {
    private int id;
    private String name;
    private String sellerEmail;
    private int price;
    private int quantity;
}

AddItemForm 객체 생성

  • AddItemForm 객체
  • 입력을 Item 객체로 받는것이 아닌 AddItemForm 객체로 받음
  • Item 객체 자체로 입력받지 않고 따로 입력용 객체를 만들어 입력받았을 때의 장점
    1. AddItemForm 객체에는 Item 객체의 모든 변수가 있을 필요 없이 원하는 값만 받을 수 있음
    2. 오직 이 페이지에서 입력을 받기 위한 객체이므로 더 직관적임
    3. 다른 페이지에서도 Item을 생성할 수 있는데 이런 경우와 구분 가능
    4. AddItemForm에 validation annotation들을 넣어줌으로써 이 페이지의 들어오는 입력에 대한 validation만을 따로 지정할 수 있음
@Data
public class AddItemForm {

    @NotBlank(message = "상품명이 비어있을 수 없습니다!")
    @Length(max = 10, message = "상품명의 최대 길이는 10글자 입니다!")
    private String name;

    @NotBlank
    @Email
    private String sellerEmail;

    @NotNull
    @Range(min = 100, max = 1000000)
    private int price;

    @NotNull
    @Max(999)
    private int quantity;
}
  • 위에서 적어두었던 필드 에러들을 모두 어노테이션을 사용해 validation 진행 예정
  • error message를 따로 설정해 줄 수도 있음

validation 관련 어노테이션들이 정리되어 있는 사이트

ItemController

@Slf4j
@Controller
@RequestMapping("/validation")
public class ItemController {

    @GetMapping("/add")
    public String addItemForm(Model model) {
        model.addAttribute("addItemForm",new AddItemForm());
        return "validation_practice/addForm";
    }

    @PostMapping("/add")
    public String addItem(@Valid @ModelAttribute("addItemForm") AddItemForm form, BindingResult bindingResult) {

        // 글로벌 에러 처리
        Long totalPrice = Long.valueOf(form.getPrice() * form.getQuantity());
        if(totalPrice > 500000000) {
            bindingResult.reject("overMaxTotalPrice",
                    "글로벌 에러 발생 : 가격 * 수량이 5억이 넘을 수 없습니다. 입력값 : " + totalPrice);
        }

        // 모든 에러 처리
        if(bindingResult.hasErrors()) {
            log.info("상품 등록 실패");
            return "validation_practice/addForm";
        }

        log.info("상품 등록 완료");
        return "redirect:/validation/add";
    }
}
  • GetMapping 에서 model에 Item이 아닌 AddItemForm을 담아줘서 전송
  • PostMapping 에서는 @ModelAttribute를 사용해 입력을 받음
  • 먼저 가격 * 수량이 5억이 넘으면 안된다는 글로벌 에러를 처리해줌
  • 글로벌 에러 발생 시, bindingResult에 error을 추가시켜 아래의 조건문에서 걸리게 함
  • @Valid 어노테이션이 필드 에러들을 검증해서 조건이 맞지 않다면 error을 발생시킴
  • 필드 에러, 글로벌 에러 중 하나라도 존재한다면 조건문에 걸리게 되고 다시 addForm.html로 돌아가게 됨
  • 이 때, 전에 사용자가 입력했던 값들은 자동으로 같이 넘어감

addForm.html

  • th:errors는 th:field로 연결된 변수에 field-error가 발생하면 해당 태그를 나타나게끔 처리
  • th:errorclass는 th:field로 연결된 변수에 에러가 발생한다면 해당 태그에 "error-class"라는 클래스를 추가시켜줌
  • error-class는 별다른 의미는 없고 마지막에 style을 통해 테두리와 글자색을 빨간색으로 적용시켜주기 위해 사용
  • 필드 에러는 th:field를 사용해 별도의 추가 코드 없이 간편하게 사용했다면 글로벌 에러는 마지막 div와 같이 직접 작성해줘야 함
<!DOCTYPE html>
<html lang="ko">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>상품 추가 Form</h1>
    <form action="/validation/add" th:object="${addItemForm}" method="post" style="width: 400px">
        <div>
            <label th:for="name">상품명 : </label>
            <input type="text" th:field="*{name}" placeholder="상품명을 입력하세요"
                    th:errorclass="error-class">
            <div class="error-class" th:errors="*{name}"></div>
        </div> <br/>
        <div>
            <label th:for="sellerEmail">이메일 : </label>
            <input type="text" th:field="*{sellerEmail}" placeholder="판매자 이메일을 입력하세요"
                    th:errorclass="error-class">
            <div class="error-class" th:errors="*{sellerEmail}"></div>
        </div> <br/>
        <div>
            <label th:for="price">가격 : </label>
            <input type="text" th:field="*{price}" th:errorclass="error-class">
            <div class="error-class" th:errors="*{price}"></div>
        </div> <br/>
        <div>
            <label th:for="quantity">수량 : </label>
            <input type="text" th:field="*{quantity}" th:errorclass="error-class">
            <div class="error-class" th:errors="*{quantity}"></div>
        </div>

        <!-- 글로벌 에러 처리 부분 -->
        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="error-class" th:each="err : ${#fields.globalErrors()}" th:text="${err}"/>
        </div> <br/>

        <button type="submit">상품 등록</button>
    </form>
</body>
</html>
<style>
input {
    width: 200px;
}
.error-class {
    color: red;
    border-color: red;
}
</style>

결과

  • 기본 페이지

  • 필드 에러 발생 1

  • 필드 에러 발생 2

  • 글로벌 에러 발생

에러 메세지 설정의 필요성

  • 글로벌 에러와 name에 관한 에러 메세지들은 직접 작성한 에러 메세지가 출력되었음
  • 이를 제외한 메세지들은 (ex) 공백일 수 없습니다) 모두 스프링에서 자동으로 출력해주는 메세지들임
  • name과 같이 에러 메세지를 모두 작성해 줄 수 있지만 에러 메세지들이 너무 많아진다면 일일이 작성하기 어려움
  • 따라서 에러 메세지들을 한번에 설정, 관리하는 방법이 필요

에러 메세지 관리 방법

  1. 프로젝트의 main/resources 폴더 안에 'errors.properties' 파일 생성
  2. application.properties에 다음 코드 추가
spring.messages.basename = errors
  1. 이제 error.properties에 각각의 에러에 맡게 원하는 메세지들을 넣어주면 됨

에러 메세지 작성 방법

Global Error

  • Global Error은 errorCode = errorMessage 형식으로 작성하면 됨
    • 위에서 Global Error Code를 overMaxTotalPrice로 지정

Field Error

  • Field Error에는 우선순위가 존재함
  • Field Error 우선 순위 (아래로 갈수록 우선순위가 낮아짐)
    1. NotBlank.addItemForm.name = 상품명은 필수입니다!
    2. NotBlank.name = 이름은 필수입니다!
    3. NotBlank.java.lang.String = 빈 문자열 일 수 없습니다!
    4. NotBlank = 공백일 수 없습니다!
  • 위와 같이 코드를 작성했다면
    • addItemForm의 name이 빈 칸이라면 "상품명은 필수입니다!"가 출력됨
    • addItemForm이 아닌 다른 객체의 name이 빈 칸이라면 "이름은 필수입니다!"가 출력됨
    • addItemFrom의 email이 빈 칸이라면 String이기 때문에 "빈 문자열 일 수 없습니다!"가 출력됨
    • String 타입도 아닌 필드가 빈 칸이라면 "공백일 수 없습니다!"가 출력됨

필드명, parameter 출력

  • {0} : 필드명, {1}, {2}, ..은 파라미터를 의미
  • price 필드에 @Range(min = 100, max = 1000000) validation을 걸어줬음
  • 이 값들이 화면에 출력되고 싶다면 {} 활용
  • "Range.item.price = {0}는 {2}원 ~ {1}원 사이 값이여야 합니다!" 와 같이 작성해주면 "price는 100원 ~ 1,000,000원 사이 값이여야 합니다!"로 출력됨

Type Error

  • Type Error는 typeMismatch 사용
  • TypeError 우선 순위 (아래로 갈수록 우선순위가 낮아짐)
    1. typeMismatch.addItemForm.price = 가격은 숫자여야 합니다!
    2. typeMismatch.java.lang.Integer = 숫자가 입력되야 합니다!
    3. typeMismatch = 입력타입이 잘못되었습니다!
  • price, quantity 필드에 문자를 넣었다면 price는 "가격은 숫자여야 합니다!"을 출력하고 quantity는 "숫자가 입력되야 합니다!"을 출력함
  • 이 특성들을 이용해 에러 메세지를 세분화 할 수 있고, 범용성 있게 쓸 수도 있음

최종 errors.properties

# Field Error
NotBlank.item.name = 상품명은 필수입니다!
NotNull.item.price = 가격을 입력해주세요!
Range.item.price = {0}는 {2}원 ~ {1}원 사이 값이여야 합니다!
Email.item.sellerEmail = 판매자 이메일이 이메일 형식이 아닙니다!

Email = Email 형식이 아닙니다!
NotNull = {0}은 공백일 수 없습니다!
NotBlank = {0}은 공백일 수 없습니다!
Range = {2} ~ {1} 허용
Max = 최대 {1}

# Global Error
overMaxTotalPrice = 상품 가격 * 수량의 최대값은 500,000,000 입니다! 현재 값은 {1} 입니다!

# Type Error
typeMismatch.item.price = 가격은 숫자여야 합니다!
typeMismatch.java.lang.Integer = 숫자가 입력되야 합니다!
typeMismatch = 입력타입이 잘못되었습니다!

결과

  • 글로벌 에러 발생시키는 코드를 아래와 같이 수정 후 실행
bindingResult.reject("overMaxTotalPrice", new Object[]{500000000, totalPrice}, "글로벌 에러 메세지");
  • 필드 에러 메세지 적용 결과 1

  • 필드 에러 메세지 적용 결과 2

  • 글로벌 에러 메세지 적용 결과

반응형

↓ 클릭시 이동

복사했습니다!