SPRING MVC - Validation

Sungjin·2021년 8월 4일
0

Spring

목록 보기
11/23
post-thumbnail

validation이 필요한 이유

  • 회원 가입의 경우를 생각 해 봅시다.
    나이를 문자로 작성을 하거나 형식에 맞지 않는 데이터를 입력시, 바로 그냥 오류 페이지가 나오거나 예외 페이지로 이동 해 버린다면 어떨까요??
    사용자는 '아 씨, 이거 뭐야' 이럴 것입니다.
    그리하여 저희가 제공하는 웹 서비스는 저러한 오류가 발생할 때, 어디가 잘못되었는지를 알려주어야 하고 또 오류가 발생하더라도 사용자가 입력했던 데이터는 유지를 시켜줘야 할 것 입니다..

  • 그렇다면 Validation은 어느 쪽에서 이뤄져야하나요??

    • 클라이언트 쪽에서만 검증?? : 물론 사용자들이 보기에도 편하고 즉각즉각 사용성이 좋을테지만 임의로 조작이 가능하므로 보안에 취약할 것 입니다.
    • 서버에서만 검증? : 클라이언트 쪽의 장점을 하나도 가져가지 못할 것입니다.
      So, 둘을 적절히 섞어서 사용합니다.

먼저, BindingResult
validation의 핵심

@PostMapping("/signup")
public String signup(@ModelAttribute SignupForm form, BindingResult bindingresult){
...
return "redirect:/"
}

이런 식으로 사용하게 됩니다. BindingResult는 필드에 오류가 있으면 FieldError를 담아 두게 됩니다. 타임리프에서는 #fields를 활용에 BindingResult가 담고 있는 오류에 접근 가능 합니다.
BindingResult는 @ModelAttribute에서 데이터 바인딩 시 타입의 mismatch로 인한 오류 상황에서도 예외가 발생하지 않고, 그 field에 대해서 오류룰 담아 보관하고 Controller를 정상적으로 호출하게 됩니다.
이러한 일을 하기 위해 스프링은 간단한 규칙을 부여하는데 Validation을 처리하기 위한 Target 객체의 뒤에 바로 BindingResult가 와야한다는 것입니다!

회원 가입 상황에서의 Validation 적용 예시

회원 가입을 위한 Form

@Data
public class SignupForm {

    private String name;

    private String password;

    private Integer age;
}

검증을 하기 위한 Validator

@Component
public class SignupValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return SignupForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        SignupForm form=(SignupForm) target;

        if(!StringUtils.hasText(form.getName())){
            errors.rejectValue("name","required");

        }
        if(!StringUtils.hasText(form.getPassword())){
            errors.rejectValue("password","required");

        }
    }
}

회원 가입 시, name 과 password를 적지 않으면 그 부분이 비었다는 메시지를 보여주도록 하기위한 코드입니다.
여기서 중요하게 볼 수 있는것은 validate 메소드의 errors 파라미터 입니다.
errors.rejectValue로 들어가는 파라미터를 보시면 {"name","password"} 와 "required"를 보실 수 있습니다.
"name"과 "password"는 오류가 난 필드의 이름이라고 보시면 됩니다.
"required"는 메시지 코드를 의미합니다. (스프링이 MessageSource를 활용하여 messages.properties를 읽어 코드에 맞는 메시지를 불러옵니다.)
메시지 코드를 만들 때는
required.signupform.name:이름은 필수 입니다.
와 같이 자세히 만들 수도 있고
required:필수 입니다.
처럼 단순히 만들 수도 있습니다.

오류 메시지를 이렇게 정의 해 놓는다면 어떤 오류 코드를 찾아서 출력 할까요??
required.signupform.name과 같이 세밀하게 정의 해 놓은 것을 우선 순위로 하여 갖고 오게 됩니다.
그런데 위의 코드를 보시면 오류 메시지 코드를 required라고만 설정을 해 놨는데 어떻게 저런 식으로 우선 순위에 따라 오류 메시지를 불러올 수 있을 지 의문이 드실 수 있습니다.

이러한 의문점의 해답은 MessageCodesResolver에 있습니다!
MessageCodesResolver

  • 전달 받은 오류 코드로 메시지 코드를 생성
  • 필드 오류 시 생성 방법
    • 우선 순위
      1. code.object name.field
      2. code.field
      3. code.type(field)
      4. code
        이 순서대로 메시지를 찾아서 출력하게 됩니다!!

회원 가입을 위한 form을 보시면 age는 Integer타입인데 만일 문자가 입력된다면 바인딩 오류가 생겨 예외가 터질 것입니다.
글 초기에 BindingResult를 설명드릴 때, 데이터 바인딩 시 타입이 mismatch되면 그 필드에 대한 오류를 BindingResult에 담는다고 하였습니다.
이때 스프링은 typeMismatch라는 오류 코드를 생성 시켜 주어 BindingResult에 담아 줍니다.
typeMismatch Code또한 위의 우선 순위를 토대로 메시지 코드를 생성합니다!

application.properties

errors.properties

SignupController

@Controller
@RequiredArgsConstructor
public class SignupController {

    private final SignupValidator signupValidator;

    @InitBinder
    public void init(WebDataBinder webDataBinder){
        webDataBinder.addValidators(signupValidator);
    }

    @GetMapping("/signup")
    public String signupForm(Model model){
        model.addAttribute("signupForm",new SignupForm());

        return "signup";
    }

    @PostMapping("/signup")
    public String signup(@Validated @ModelAttribute SignupForm form, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "signup";
        }

        return "redirect:/";
    }
}

webDataBinder에 검증기를 추가함으로서 @Validated annotation을 사용할 수 있습니다!
검증이 되나 확인만을 위한 코드 입니다!

signup.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <style>
        .field-error{
            border-color:red;
            color:red;
        }
    </style>
</head>
<body>

<div >
    <div >
        <h2 >회원 가입</h2>
    </div>

    <form  th:action th:object="${signupForm}" method="post">
        <div>
            <label for="name" >이름</label>
            <input type="text" id="name" th:field="*{name}"  placeholder="이름을 입력하세요"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{name}">
                이름 오류
            </div>
        </div>
        <div>
            <label for="password" >비밀번호</label>
            <input type="text" id="password" th:field="*{password}" 
                   placeholder="가격을 입력하세요" th:errorclass="field-error">
            <div class="field-error" th:errors="*{password}">
                비밀번호 오류
            </div>
        </div>
        <div>
            <label for="age" >나이</label>
            <input type="text" id="age" th:field="*{age}"
                   placeholder="가격을 입력하세요" th:errorclass="field-error">
            <div class="field-error" th:errors="*{age}">
                나이 오류
            </div>
        </div>

        <div >
            <input type="submit">
        </div>
    </form>
</div> 
</body>
</html>

th:errors : 해당 필드에 오류가 있는 경우 태그를 출력 (th:if 와 th:text의 통합 버전(?) 처럼 볼 수 있을듯)
th:errorclass : th:field에 지정한 오류가 존재하면 class 추가

실행 결과

의도한 대로 잘 실행되는 것을 확인할 수 있습니다!!

이상으로 포스팅을 마치겠습니다. 감사합니다 :)

이 글은 인프런 김영한님의 '스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술'을 수강하고 작성합니다.
출처:https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

profile
WEB STUDY & etc.. HELLO!

0개의 댓글