spring은 입력데이터에 대한 validation과 예외처리를 지원해준다. org.springframework.validation.BindingResult 가 validation 기능을 지원해주는 주요 객체 중 하나이다. BindingResult는 입력 form의 필드값 중에 오류가 있으면 오류정보를 담아둔다.
예제
아래와 같은 간단히 사람의 이름과 나이를 입력하고 입력한 정보를 조회할 수 있는 controller가 있다고 가정하자
@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가 호출된다.
<formaction="/member"method="POST"th:object="${member}" > <div> <labelth:for="name">Enter your name: </label> <inputtype="text"th:field="*{name}"> </div> <div> <labelth:for="age">Enter your age: </label> <inputtype="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
POST 방식으로 client에서 server로 데이터 전송 이후, Server가 바로 특정 page를 forward하게 되면 refresh 시 데이터가 중복 재전송된다. 이를 방지하기 위해서 다른 페이지로 GET (redirect) 한다.
bidingResult는 에러를 포함한 객체 바로 다음에 와야 한다. 객체의 필드값별로 로직에 따라 추가할 에러를 설정할 수 있다.
bindingResult.addError(ObjectError)
참고로 FieldError는 Object Error의 자식임으로 addError parameter에 넘겨줄 수 있다.
1
publicclassFieldErrorextendsObjectError{}
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에 포함된 오류정보를 꺼내서 보여주는 기능을 가지고 있다.
th:object=”${객체명}” th:errors=”*{필드명}” 을 사용하면 해당 필드에 오류가 있으면 bindingResult에 저장된 오류 메시지를 출력해준다. thymeleaf에서는 th:object에 명시될 객체를 command object라고 한다.
Command object is the name Spring MVC gives to form-backing beans, this is, to objects that model a form’s fields and provide getter and setter methods that will be used by the framework for establishing and obtaining the values input by the user at the browser side.
/** * Can this {@link Validator} {@link #validate(Object, Errors) validate} * instances of the supplied {@code clazz}? */ booleansupports(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 */ voidvalidate(Object target, Errors errors);
}
인터페이스 설명에 보면 2가지 method를 구현해야 하는데, 다음과 같이 정리하였다.
supports(…) : 해당 instance가 검증할 class의 instance가 맞는가?
validate(…) : 검증 후 , report할 오류는 errors parameter에 추가한다.
@Component// 굳이 검증기를 client요청시마다 생성할 이유가 없다, 동시성 이슈가 있는 것도 아니기 떄문에 싱글톤으로 관리하는게 적합하다고 한다. publicclassMemberValidatorimplementsValidator{ @Override publicbooleansupports(Class<?> clazz){ return Member.class.isAssignableFrom(clazz); }
@Override publicvoidvalidate(Object target, Errors errors){ Member member = (Member) target;
@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를 적용하고 싶으면 다음과 같이 설정하면 된다.
//global (전체 controller)에 해당 검증기 적용 @Override public Validator getValidator(){ returnnew MemberValidator(); } }
정리
spring 이 제공하는 binding result를 활용하면 간결하게 검증 로직을 추가할 수 있다. 오류 메시지는 하드코딩하지말고 messages.propeties 에 설정정보를 가져오는 messageCodeResolver를 활용하면 오류 메시지 변경이 생길떄 빠르게 대처할 수 있다. spring 외에 다른 framework 적용시에도 위와 같은 흐름으로 오류 처리를 하면 좋을 것같다.