Published on

스프링 프레임워크 첫걸음 - 8장 : 유효성 검사 기능 알아보기

Authors
  • avatar
    Name
    손예지(Liv)

7장에서는 요청 파라미터로 서버에 값을 전달하는 방법에 대해 살펴봤습니다. 8장에서는 입력한 값을 어떻게 검증하는지에 대해서 살펴보고 유효성 검사 기능에 대해 살펴보겠습니다.

1. 유효성 검사

스프링부트에서는 유효성 검사 기능을 통해 매개변수나 반환 값을 검증할 수 있습니다. 유효성 검사는 입력한 내용을 확인해 해당 값이 올바르게 입력되었는지 확인합니다. 유효성 검사 기능은 클래스경로에 Hibernate validater과 같은 어노테이션을 통해 사용할 수 있습니다. 저자는 유효성 검사를 단일 항목 검사와 상관 항목 검사로 구분해 설명합니다.

단일 항목 검사

단일 항목 검사는 입력한 항목 한가지에 대해서 설정하는 기능입니다. 단일 항목 검사에 사용되는 주요 어노테이션은 아래와 같습니다. (p199 - 200)

어노테이션기능 설명
@NotNullNull 값이 아닌 것을 검증
@NotEmpty문자열이 null 혹은 공백문자("")가 아닌 것을 검증합니다
@Max지정한 숫자 이하인 것을 검증
@Min지정한 숫자 이상인 것을 검증
@Email문자열이 이메일 주소 형식인지 검증
@Pattern지정한 정규 표현과 일치하는 것 검증
@Range지정한 숫자 범위 안에 있는 것 검증

커스텀 유효성 검사

여러 필드에서 검증하는 것을 상관 항목(서로 관련이 있는 항목) 검사라고 합니다. 상관 항목 검사를 수행하는 방법에는 Bean Validation을 사용하거나, 스프링 프레임워크에서 제공하는 Validator 인터페이스를 구현하는 방법이 있습니다.

2. 단일 항목 검사를 사용하는 프로그램 만들기

숫자를 입력하고, 올바른 값을 입력하지 않았을때 메세지를 전달하는 유효성 검사를 할 수 있는 간단한 프로그램을 만들어보겠습니다.

클래스 생성하기

사용자가 화면에서 값을 입력했을때 대응할 수 있는 클래스를 만들어줍니다. 유효성 검사에서 확인할 메세지를 message 속성으로 입력합니다. 아래 예시에서는 Null값이 아닌 값을 사용하는 @NotNull 조건과 범위를 설정하는 @Range 조건을 사용했습니다.

import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Range;

@Data
public class CalcForm {
    @NotNull(message = "숫자를 입력해주세요.")
    @Range(min = 1, max = 10, message = "왼쪽: {min}~{max} 범위위 숫자를 입력해주세요.")
    private Integer leftNum;

    @NotNull(message = "숫자를 입력해주세요.")
    @Range(min = 1, max = 10, message = "오른쪽: {min}~{max} 범위위 숫자를 입력해주세요.")
    private Integer rightNum;
}

컨트롤러 생성하기

화면과 연결될 컨트롤러를 생성해주겠습니다. 유효성 검사를 위해서는 form-back bean의 설정이 필요합니다. form-back bean은 사용자가 폼에 입력한 데이터를 바인딩 하거나, 데이터 유효성 검사를 하는것과 같이 폼과 관련된 비즈니스 로직을 수행하는데 사용됩니다. form-back bean은 @ModelAttribute 어노테이션을 통해 초기화 할 수 있습니다. 어노테이션을 부여하게 되면 form 데이터를 컨트롤러 메서드로 전달합니다. 여기서는 addAttribute() 메서드를 사용해 명시적으로 CalcForm 클래스와 연결하는 방식을 사용했습니다.


import livoi.mvctest.form.CalcForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ValidationController {

    @ModelAttribute
    public CalcForm setUpForm(){ // form-backing bean 초기화
        return new CalcForm();
    }

    @GetMapping("show") 
    public String showView(Model model) { // 입력 화면 표시
        model.addAttribute("calcForm", new CalcForm());
        return "entry";
    }

    @PostMapping("calc")
    public String confirmView(@Validated CalcForm form, BindingResult bindingResult, Model model){

        if (bindingResult.hasErrors()){
            return "entry";
        }

        Integer result = form.getLeftNum() + form.getRightNum();

        // Model에 저장
        model.addAttribute("result", result);

        return "confirm";
    }
}

뷰 생성하기

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>입력화면</title>
</head>
<body>
<!--/*@thymesVar id="calcForm" type="java"*/-->
<form method="post" th:action="@{/calc}" th:object="${calcForm}">
    <div>
        <input th:field="*{leftNum}" type="text">
        +
        <input th:field="*{rightNum}" type="text">
    </div>
    <input type="submit" value="계산">
    <ul th:if="${#fields.hasErrors('*')}">
        <li th:each="err:${#fields.errors('*')}" th:text="${err}"></li>
    </ul>
</form>
</body>
</html>

컨트롤러에서 showView 메서드를 통해서 반환해주는 entry 뷰 페이지를 만들어 보겠습니다. 여기서 제가 생각한 핵심은 타임리프에서 th:object 속성을 설정하고, th:field 속성을 설정하는 것 입니다. th:object는 CalcForm 객체를 모델에 전달하고 각각 필드(여기서는 leftNum, rightNum)을 바인딩합니다. 이때 객체가 올바르게 연결되지 않으면 필드 값을 인식하지 못하고 아래와 같은 parseError가 발생할 수 있습니다.

Caused by: org.attoparser.ParseException: Could not bind form errors using expression "*". 

오류 메세지를 표시하는 부분은 form 태그 안에 위치해야 합니다. #fields.hasErrors 메서드를 통해 에러 존재 여부를 확인한 후, 에러메세지가 있다면 출력해줍니다. * 는 모든 에러를 가져온다는 의미이고, 모든 에러를 출력해줘야 하기 때문에 th:each 속성을 사용했습니다.

입력 화면의 뷰를 생성했다면, 결과값을 출력해줄 뷰도 생성해보겠습니다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>확인 화면</title>
</head>
<body>
<h2>계산 결과</h2>
<h3>[[${calcForm.leftNum}]] + [[${calcForm.rightNum}]] = [[${result}]]</h3>
</body>
</html>

결과값을 출력해주는 <h3> 태그를 살펴보면, calcForm 객체와 필드에 저장된 값과 결과 값을 표시해줍니다. 타임리프의 표현식인 [[ ]] 또는 ${ } 를 잘 지켜 저와 같이 오류원인을 오랬동안 찾는 일이 없기를 바랍니다..ㅎㅎ

실행

이제 스프링부트를 실행하고 http://localhost:8080/show URL에서 계산하는 메인 화면을 확인합니다.

먼저 1에서 10사이의 올바른 값을 넣어서 계산버튼을 클릭합니다. 이때 calc URL로 변경되며 계산 결과가 노출됩니다.

허용되지 않는 값인 -1을 입력해 계산을 실행해봅니다. 계산버튼 하단에 예상한대로 출력되는 것을 확인할 수 있습니다.

2. 메세지 관리

스프링부트에서는 유효성 검사를 포함한 메세지를 프로그램과 별도로 관리합니다. 메세지는 messages.properties에서 관리할 수 있는데, 유효성 검사를 위한 메세지와 국제화(internationalization)를 위한 i18n 적용에 사용됩니다. 스프링부트에서 메세지를 어떻게 관리하는지 살펴보겠습니다.

인코딩 설정하기

먼저 인코딩 설정이 필요합니다. 메세지에 담는 언어가 한글인 경우, 사용하는 IDE에 따라 기본 인코딩 설정이 UTF-8로 되어있어도 한글은 인코딩이 적용되지 않을 수 있습니다. 이 설정은 Settings > File Encoding 설정에서 확인할 수 있습니다. Default encoding 영역을 살펴보면 ISO-8859-1 표준을 사용한다고 설정되어 있는데, ISO-8859-1은 8비트 문자 인코딩 표준으로 서유럽 언어 문자들을 포함하고 있어 한글은 포함하고 있지 않습니다.

Transparent native-to-ascii conversion 체크박스에 체크해 적용 하면, properties에서 설정한 한글도 출력할 수 있습니다. 단, messages.properties에 인코딩 변경 전에 저장한 한글이 있다면 설정을 적용하는 순간 ???의 깨진 형태로 바뀌어 노출되기 때문에 먼저 설정 후에 properties 파일을 작성하는 것을 추천합니다.

messages.properties 생성하기

messages.properties 파일은 html 파일처럼 main > src > resources 폴더 하단에 위치해야 생성해야 합니다. properties 는 key=value 형태로 정의합니다. 이때 key 값은 일반적으로는 객체.필드명 형태로 입력하지만, 일관된 방식으로 사용자가 정의할 수도 있습니다. 여기서는 title 태그와 button 태그 안의 값을 각각 입력과 보내기로 지정해줬습니다.

title.entry=입력
button.send=보내기

그렇다면 messages.properties 에서 입력한 값을 뷰에서는 어떻게 인식할까요? 타임리프에서는 #{키} 형태로 값을 가지고 옵니다. 앞서 작성했던 entry.html 파일에서 직접 값을 삽입했던 부분의 수정이 필요합니다. messages.properties에 입력한 부분을 받아올 수 있도록 수정해주었습니다.

  • title 태그
<!-- AS-IS -->
<head>
    <meta charset="UTF-8">
    <title>입력화면</title>
</head>

<!-- TO-BE -->
<head>
    <meta charset="UTF-8">
    <title th:text="#{title.entry}">제목</title>
</head>
  • submit 태그
<!-- AS-IS -->
<input type="submit" value="계산">

<!-- TO-BE -->
<button type="submit" th:text="#{button.send}"></button>

ValidationMessages.properties 생성

유효성 체크 결과로 노출해줄 값들을 저장하는 ValidationMessages.properties를 생성해보겠습니다. 조금 전 작성했던 messages.properties와 동일한 경로에 저장합니다. 이때 오류 메세지를 매핑하지 않는 경우 기본적으로 hibernate가 노출해주는 메세지를 출력해줍니다.

javax.validation.constraints.NotNull={0}: 숫자를 입력해주세요.
org.hibernate.validator.constraints.Range={0}: {min}~{max} 범위의 숫자를 입력해주세요.

form 클래스 수정하기

마지막으로 제일 처음 작성했던 form 클래스에서 messages 부분을 제거해 줍니다. 이렇게 수정하면 결과는 동일하지만 필요한 메세지 값을 관리할 수 있습니다.

정리

유효성 검사의 종류를 알아보고, 어떻게 객체를 바인딩 해서 출력해주는지 알아봤습니다. 입력값 같은 경우에는 데이터베이스에 저장되는 형식이 정해져있기 때문에, 개발자가 의도한 방식으로 저장되고, 저장된 값을 불러오기 위해서 꼭 필요한 생각이라는 생각이 들었습니다.

참고자료:
스프링프레임워크 첫걸음, 후루네스 키노시타 마사아키
https://docs.spring.io/spring-boot/reference/io/validation.html
https://docs.spring.io/spring-boot/docs/2.1.13.RELEASE/reference/html/boot-features-internationalization.html