Spring validation - BindingResult
2021-12-30 20:11:01
BindingResult
spring은 입력데이터에 대한 validation과 예외처리를 지원해준다. org.springframework.validation.BindingResult 가 validation 기능을 지원해주는 주요 객체 중 하나이다. BindingResult는 입력 form의 필드값 중에 오류가 있으면 오류정보를 담아둔다.
예제
아래와 같은 간단히 사람의 이름과 나이를 입력하고 입력한 정보를 조회할 수 있는 controller가 있다고 가정하자
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {
private final MemberRepository repository;
@GetMapping
public String getMember(Model model){
model.addAttribute(new Member());
return "member";
}
@PostMapping
public String addMember(@ModelAttribute Member member , RedirectAttributes redirectAttributes){
log.info("member : {}",member);
Member savedMember = repository.save(member);
Long id = savedMember.getId();
redirectAttributes.addAttribute("id",id);
return "redirect:/member/{id}";
}
@GetMapping("/{id}")
public String detailMember(@PathVariable(name = "id") Long id ,Model model ){
Member foundMember = repository.findById(id);
model.addAttribute("member",foundMember);
return "memberDetail";
}
}
getMember method 로 사람의 정보를 등록할 수 있는 member page가 호출된다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="eg">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/member" method="POST" th:object="${member}" >
<div>
<label th:for="name">Enter your name: </label>
<input type="text" th:field="*{name}">
</div>
<div>
<label th:for="age">Enter your age: </label>
<input type="text" th:field="*{age}">
</div>
<button>submit</button>
</form>
addMember 를 호출하면 name=value&name=value 형태 (Content-Type: application/x-www-form-urlencoded) 로 데이터가 서버에 넘어가고, 서버에서 memberDetail page로 redirect 한다. 이를 PRG 패턴이라고 한다.
PRG pattern - Post/Redirect/Get
(ref - https://en.wikipedia.org/wiki/Post/Redirect/Get)
Binding Result 적용 1
bidingResult는 에러를 포함한 객체 바로 다음에 와야 한다. 객체의 필드값별로 로직에 따라 추가할 에러를 설정할 수 있다.
- bindingResult.addError(ObjectError)
참고로 FieldError는 Object Error의 자식임으로 addError parameter에 넘겨줄 수 있다.
public class FieldError extends ObjectError{}

field Error와 object error는 2개의 생성자를 overloading 하고 있는데 첫번쨰는 defaultMessage (view에 보여줄 메시지) 를 바로 명시하는 생성자 방식이 있고, 두번째는 messageCodeResolver에 의해 설정파일로부터 값을 읽어와 view에 보여줄 수 있는 생성자 방식이 있다. 두 방식에서 공통 매개변수는 객체와 해당 객체에 오류를 가졌다고 명시할 필드이다.
참고로 messageCodeResolver와 messages.properties 관련 내용은 spring internalization 관련 내용으로 , boot에서는 /resources/messages.properties 파일을 생성하고, message code를 입력하면 자동으로 등록해준다.

Object error는 해당 객체에 특정 필드가 가진 오류라기보다 global 오류 정보를 의미한다. 예를 들어 특정 필드간의 조합이 x 범위 이내를 만족하지 못할 경우이다.
@PostMapping
public String addMember(@ModelAttribute Member member , BindingResult bindingResult , RedirectAttributes redirectAttributes){
// binding result는 에러 필드를 가진 객체 바로 뒤에 와야한다.
// fieldErrors
log.info("member :{}",member);
if(!StringUtils.hasText(member.getName())){
bindingResult.addError(new FieldError("member","name",member.getName(),false,new String[]{"empty.name"},null,null));
}
if(member.getAge() == null){
bindingResult.addError(new FieldError("member","age",member.getAge(),false,new String[]{"empty.age"},null,null));
}
// objectErrors
if(member.getAge() != null && member.getAge() <= 10 && member.getName().startsWith("Kim")){
bindingResult.addError(new ObjectError("member",new String[]{"limit.member"},null,null));
}
// error가 있다면 바로 view 반환
if(bindingResult.hasErrors()){
return "member";
}
Member savedMember = repository.save(member);
Long id = savedMember.getId();
redirectAttributes.addAttribute("id",id);
return "redirect:/member/{id}";
}
위와 같이 controller에서 binding result에 error 정보를 포함해주고, 바로 view를 반환한다. template engine마다 다르겠지만 thymeleaf template engine은 spring binding result에 포함된 오류정보를 꺼내서 보여주는 기능을 가지고 있다.
<form action="/member" method="POST" th:object="${member}" >
<div th:if="${#fields.hasGlobalErrors()}">
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}"></p>
</div>
<div>
<label th:for="name">Enter your name: </label>
<input type="text" th:field="*{name}">
<div th:errors="*{name}"></div>
</div>
<div>
<label th:for="age">Enter your age: </label>
<input type="text" th:field="*{age}">
<div th:errors="*{age}"></div>
</div>
<button>submit</button>
</form>
th:object="${객체명}" th:errors="*{필드명}" 을 사용하면 해당 필드에 오류가 있으면 bindingResult에 저장된 오류 메시지를 출력해준다. thymeleaf에서는 th:object에 명시될 객체를 command object라고 한다.
(ref - https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#validation-and-error-messages)
Binding Result 적용 2
앞선 BindingResult는 오류를 검사할 객체 바로 뒤에 온다, 즉 오류를 검사할 target 객체를 알고 있으므로, bindingResult는 오류메시지를 간편하게 추가할 수 있는 method를 제공한다.
- rejectValue(...) : FieldError 와 유사한 기능
- reject(...) : ObjectError와 유사한 기능
@PostMapping
public String addMember(@ModelAttribute Member member , BindingResult bindingResult , RedirectAttributes redirectAttributes){
// binding result는 에러 필드를 가진 객체 바로 뒤에 와야한다.
log.info("member :{}",member);
if(!StringUtils.hasText(member.getName())){
bindingResult.rejectValue("name","empty.name");
}
if(member.getAge() == null){
bindingResult.rejectValue("age","empty.age");
}
if(member.getAge() != null && member.getAge() <= 10 && member.getName().startsWith("Kim")){
bindingResult.reject("limit.member");
}
if(bindingResult.hasErrors()){
return "member";
}
Member savedMember = repository.save(member);
Long id = savedMember.getId();
redirectAttributes.addAttribute("id",id);
return "redirect:/member/{id}";
}
Valdiation 로직 분리
위의 검증 로직들은 별도의 validator에 의해 분리될 수 있다. spring은 별도의 validator interface를 제공한다. (org.springframework.validation.Validator)
public interface Validator {
/**
* Can this {@link Validator} {@link #validate(Object, Errors) validate}
* instances of the supplied {@code clazz}?
*/
boolean supports(Class<?> clazz);
/**
* Validate the supplied {@code target} object, which must be
* of a {@link Class} for which the {@link #supports(Class)} method
* typically has (or would) return {@code true}.
* <p>The supplied {@link Errors errors} instance can be used to report
* any resulting validation errors.
* @param target the object that is to be validated
* @param errors contextual state about the validation process
* @see ValidationUtils
*/
void validate(Object target, Errors errors);
}
인터페이스 설명에 보면 2가지 method를 구현해야 하는데, 다음과 같이 정리하였다.
- supports(...) : 해당 instance가 검증할 class의 instance가 맞는가?
- validate(...) : 검증 후 , report할 오류는 errors parameter에 추가한다.
위 interface를 만든 예제에 적용하면 다음과 같다.
@Component // 굳이 검증기를 client요청시마다 생성할 이유가 없다, 동시성 이슈가 있는 것도 아니기 떄문에 싱글톤으로 관리하는게 적합하다고 한다.
public class MemberValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Member.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Member member = (Member) target;
if(!StringUtils.hasText(member.getName())){
errors.rejectValue("name","empty.name");
}
if(member.getAge() == null){
errors.rejectValue("age","empty.age");
}
if(member.getAge() != null && member.getAge() <= 10 && member.getName().startsWith("Kim")){
errors.reject("limit.member");
}
}
}
별도로 validator를 생성자 DI 받으면 controller의 책임과 validation 책임을 분리할수있다 (SRP)
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {
private final MemberRepository repository;
private final MemberValidator validator;
@GetMapping
public String getMember(Model model){
model.addAttribute(new Member());
return "member";
}
@PostMapping
public String addMember(@ModelAttribute Member member , BindingResult bindingResult , RedirectAttributes redirectAttributes){
// binding result는 에러 필드를 가진 객체 바로 뒤에 와야한다.
log.info("member :{}",member);
validator.validate(member,bindingResult);
if(bindingResult.hasErrors()){
return "member";
}
Member savedMember = repository.save(member);
Long id = savedMember.getId();
redirectAttributes.addAttribute("id",id);
return "redirect:/member/{id}";
}
@GetMapping("/{id}")
public String detailMember(@PathVariable(name = "id") Long id ,Model model ){
Member foundMember = repository.findById(id);
model.addAttribute("member",foundMember);
return "memberDetail";
}
}
추가로 해당 controller에 있는 method들에 validator를 적용하고 싶으면 다음과 같이 설정하면 된다.
public class MemberController {
private final MemberRepository repository;
private final MemberValidator validator;
// controller 마다 적용
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(validator);
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
//global (전체 controller)에 해당 검증기 적용
@Override
public Validator getValidator() {
return new MemberValidator();
}
}
정리
spring 이 제공하는 binding result를 활용하면 간결하게 검증 로직을 추가할 수 있다. 오류 메시지는 하드코딩하지말고 messages.propeties 에 설정정보를 가져오는 messageCodeResolver를 활용하면 오류 메시지 변경이 생길떄 빠르게 대처할 수 있다. spring 외에 다른 framework 적용시에도 위와 같은 흐름으로 오류 처리를 하면 좋을 것같다.
참고자료
인프런 - 김영한 개발자님의 강의를 듣고, 궁금한 부분은 추가로 레퍼런스를 찾고 정리한 글입니다. https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
댓글
이 게시글에 대한 의견을 공유해주세요!
