본문으로 바로가기
반응형

본 예제는 Spring 5.x & Spring boot 2.3.0 버전을 사용합니다.

또한 Kotlin (v1.3.72)로 예제를 작성하며 IntelliJ CE를 사용합니다.

스프링에서는 form에 대한 유효성 체크를 간단하게 할수 있도록 도와주는 library들이 존재합니다.

java bean의 validation check와 추가적인 체크 기능을 구현한 org.hibernate.validator spring-boot-starter-web에 dependency로 지정되어 있습니다.


먼저 테스트를 하기 위해 신용카드 체크를 하는 페이지를 하나 만듭니다.


신용카드 체크 페이지 구성

model 구성

기본적으로 신용카드 정보는 카드번호, 유효기간, CVV값 세개를 사용합니다.

따라서 이 세개의 field를 갖는 data class를 생성합니다. (PayInfo.kt)

import org.hibernate.validator.constraints.CreditCardNumber
import javax.validation.constraints.Digits
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Pattern

data class PayInfo(
        @NotNull
        @NotBlank
        @CreditCardNumber(message = "card number is wrong")
        var cardNumber: String? = null,

        @NotNull
        @Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message = "Check format. MM/YY")
        var expiredDate: String? = null,

        @NotNull
        @Digits(integer = 3, fraction = 0, message = "Wrong CVV")
        var cvv: String? = null
)


hibernate에서 신용카드 자체의 유효성 검증을 위한 기능을 제공 합니다.

따라서 간단하게 @CrediCardNumber annotation을 달아놓으면 신용카드 번호형식이 맞는지를 확인할 수 있습니다.


아쉽게도 유효기간체크 기능은 지원하지 않으므로 정규 표현식으로 체크 하도록 @Pattern annotation을 사용합니다.


마지막으로 cvv값은 숫자 세자리 이므로 숫자 & 자리수를 체크하도록 합니다.

그외에도 이 예제에는 없지만 이전 글에서 사용했던 @Size같이 개수를 체크해 볼수도 있습니다.


Annotation이 직관적이기 때문에 쉽게 해당 기능을 유추해 볼수 있습니다.


controller 구성

@Controller
@RequestMapping("/order/pay")
class PayController {
    companion object {
        private const val PAY_VIEW = "pay"
        private const val ORDER_VIEW = "/order"
    }

    private val log = LoggerFactory.getLogger("PayController")

    @GetMapping
    fun showPayView(model: Model): String {
        model.addAttribute("payinfo", PayInfo())
        return PAY_VIEW
    }

    @PostMapping
    fun processForm(@ModelAttribute("payinfo") @Valid payinfo: PayInfo, errors: Errors): String {
        if (errors.hasErrors()) {
            log.error("pay info is not invalid")
            return PAY_VIEW
        }

        log.info("Pay info: $payinfo")

        return "redirect:$ORDER_VIEW"
    }
}


해당 페이지가 호출되면 key로 "payinfo" 사용하여 빈 PayInfo 클래스를 생성하여 Model에 등록시킵니다.

그리고 유저가 화면에서 값을 입력후 form을 전송하면 @PostMapping 함수인 processForm 함수로 해당 객체가 인자로 넘어 옵니다.


processForm에서 @ModelAttribute로 넘어오는 객체의 key값을 지정하며 @Valid를 이용하여 processForm() 함수를 실행시키 전에 유효성 검사를 진행합니다.

따라서 에러가 발생했다면 에러 역시 함수의 인자로 넘겨 줍니다.


view 구성

view는 간단하게 생성하여 아래와 같이 보이도록 구성 합니다.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
  <head>
<meta charset="UTF-8">
    <title>Buy My Car!!</title>
    <link rel="stylesheet" th:href="@{/styles.css}" />
  </head>

  <body>

    <form method="POST" th:object="${payinfo}">
      <h1>Insert your payment info.</h1>

      <label for="cardNumber">Credit Card #: </label>
      <input type="text" th:field="*{cardNumber}"/>
      <span class="validationError"
            th:if="${#fields.hasErrors('cardNumber')}"
            th:errors="*{cardNumber}">CC Num Error</span>
      <br/>

      <label for="expiredDate">Expiration: </label>
      <input type="text" th:field="*{expiredDate}"/>
      <span class="validationError"
            th:if="${#fields.hasErrors('expiredDate')}"
            th:errors="*{expiredDate}">CC Num Error</span>
      <br/>

      <label for="cvv">CVV: </label>
      <input type="text" th:field="*{cvv}"/>
      <span class="validationError"
            th:if="${#fields.hasErrors('cvv')}"
            th:errors="*{cvv}">CC Num Error</span>
      <br/>

      <input type="submit" value="Submit payment info."/>
    </form>

  </body>
</html>

form 태그 내부에 th:object="${payinfo}" 를 사용하여 model의 payinfo 속성을 사용한다는것을 명시합니다.


각 항목별로 input을 넣고 th:field를 사용하여 입력된 값을 payinfo 객체의 각 멤버번수에 담습니다.

그리고 유효성 검사를 통과하지 못할경우 th:if, th:errors를 이용하여 메시지를 화면에 찍습니다.


Kotlin에서 유효성 Annotation 미동작 이슈

실제로 실행시켜 아무값이나 넣을경우 에러가 떠야 하지만 정상적으로 처리되어 controller에서 지정한 다음페이지로 넘어갑니다.

해결 방법은 아래와 같이 두가지중 하나를 사용하면 해결 할 수 있습니다.

1. @get:xxx 형태로 변경
위 data lcass를 아래와 같이 변경합니다.

import org.hibernate.validator.constraints.CreditCardNumber
import javax.validation.constraints.Digits
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Pattern

data class PayInfo(
        @get:NotNull
        @get:NotBlank
        @get:CreditCardNumber(message = "card number is wrong")
        var cardNumber: String? = null,

        @get:NotNull
        @get:Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message = "Check format. MM/YY")
        var expiredDate: String? = null,

        @get:NotNull
        @get:Digits(integer = 3, fraction = 0, message = "Wrong CVV")
        var cvv: String? = null
)


2. Model에 담을 클래스는 java로 생성 (PayInfoInJava.java)

kotlin을 이용할 수는 없지만 Kotlin 코드와 java는 100%(??) 호환성을 갖기 때문에 유효성 체크를 해야 하는 class만 따로 java class로 생성하여 사용 합니다.

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

import javax.validation.constraints.Digits;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Data
public class PayInfoInJava {
    @NotNull
    @CreditCardNumber(message = "card number is wrong")
    private String cardNumber;

    @NotNull
    @Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message = "Check format. MM/YY")
    public String  expiredDate;

    @NotNull
    @Digits(integer = 3, fraction = 0, message = "Wrong CVV")
    public String cvv;
}


물론 getter/setter의 자동 생성을 위해 @Data annotation (lombok library)을 이용해야 합니다.

lombok을 안쓴다면 전부 코드로 getter/setter를 모두 표기해야 겠죠?




참고: https://jsonobject.tistory.com/459

반응형