검증1 - Validation1
현재 학습의 방향이 API 서버를 만드는 방향으로 가고 있기 때문에, 뷰 템플릿에 관련된 코드 설명은 없을 수 있다.
1. 검증 요구사항
상품 관리 시스템에 새로운 요구사항이 추가되었다.
요구 사항 : 검증 로직 추가
타입 검증
가격, 수량에 문자가 들어가면 검증 오류 처리
필드 검증
상품명: 필수, 공백X
가격: 1000원 이상, 1백만원 이하
수량: 최대 9999
특정 필드의 범위를 넘어서는 검증
가격 * 수량의 합은 10,000원 이상
컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다. 그리고 정상 로직보다 이런 검증 로직을 잘 개발하는 것이 어쩌면 더 어려울 수 있다.
참고 - 클라이언트 검증, 서버 검증
클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함
2. 검증 직접 처리 - 소개
서버 사이드 랜더링 기준임을 참고하자 (HTTP API 서버 기준이 아니다)
상품 저장 성공

사용자가 상품 등록 폼에서 정상 범위의 데이터를 입력하면, 서버에서는 검증 로직이 통과하고, 상품을 저장하고, 상품 상세 화면으로 redirect 한다.
상품 저장 실패

고객이 상품 등록 폼에서 상품을 입력하지 않거나, 가격, 수량 등이 너무 작거나 커서 검증 범위를 넘어서면 서버 금증 로직이 실패해야 한다. 이렇게 검증에 실패한 경우 고객에게 다시 상품 등록 폼을 보여주고, 어떤 값을 잘못 입력했는지 친절하게 알려주어야 한다.
3. 검증 직접 처리 - 개발
상품 등록 검증
먼저 상품 등록 검증 코드를 작성해보자.
ValidationItemControllerV1 - addItem() 수정
검증 오류 보관
Map<String, String> errors = new HashMap<>();
만약 검증시 오류가 발생하면 어떤 검증에서 오류가 발생했는지 정보를 담아둔다.
검증 로직
검증시 오류가 발생하면 errors 에 담아둔다. 이때 어떤 필드에서 오류가 발생했는지 구분하기 위해 오류가 발생한 필드명을 key 로 사용한다. 이후 뷰에서 이 데이터를 사용해서 고객에게 친절한 오류 메시지를 출력할 수 있다.
특정 필드의 범위를 넘어서는 검증 로직
특정 필드를 넘어서는 오류를 처리해야 할 수도 있다. 이떄는 필드 이름을 넣을 수 없으므로 globalError 라는 key 를 사용한다.
검증에 실패하면 다시 입력 폼으로
만약 검증에서 오류 메시지가 하나라도 있으면 오류 메시지를 출력하기 위해 model 에 errors 를 담고, 입력 폼이 있는 뷰 템플릿으로 보낸다.
addForm.html
뷰 템플릿이 학습의 목적이 아니기에 패스..
정리
만약 검증 오류가 발생하면 입력 폼을 다시 보여준다.
검증 오류들을 고객에게 친절하게 안내해서 다시 입력할 수 있게 한다.
검증 오류가 발생해도 고객이 입력한 데이터가 유지된다.
남은 문제점
뷰 템플릿에서 중복 처리가 많다. 뭔가 비슷하다.
타입 오류 처리가 안된다.
Item의price,quantity같은 숫자 필드는 타입이Integer이므로 문자 타입으로 설정하는 것이 불가능하다. 숫자 타입에 문자가 들어오면 오류가 발생한다. 그런데 이러한 오류는 스프링
MVC에서 컨트롤러에 진입하기도 전에 예외가 발생하기 때문에, 컨트롤러가 호출되지도 않고, 400 예외가 발생
하면서 오류 페이지를 띄워준다.
Item의price에 문자를 입력하는 것 처럼 타입 오류가 발생해도 고객이 입력한 문자를 화면에 남겨야 한다.만약 컨트롤러가 호출된다고 가정해도
Item의price는Integer이므로 문자를 보관할 수가 없다. 결국 문자는 바인딩이 불가능하므로 고객이 입력한 문자가 사라지게 되고, 고객은 본인이 어떤 내용을 입력해서 오류가 발생했는지 이해하기 어렵다.
결국 고객이 입력한 값도 어딘가에 별도로 관리가 되어야 한다.
지금부터 스프링이 제공하는 검증 방법을 하나씩 알아보자.
4. BindingResult1
지금부터 스프링이 제공하는 검증 오류 처리 방법을 알아보자. 여기서 핵심은 BindingResult 이다. 우선 코드로 확인해보자.
ValiationItemControllerV2 - addItemV1
주의
BindingResult bindingResult 파라미터의 위치는 @ModelAttribute Item item 다음에 와야 한다.
필드 오류 - FieldError
FieldError 생성자 요약
필드에 오류가 있으면 FieldError 객체를 생성해서 bindingResult 에 담아두면 된다.
objectName:@ModelAttribute이름field: 오류가 발생한 필드 이름defaultMessage: 오류 기본 메시지
글로벌 오류 - ObjectError
ObjectError 생성자 요약
특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서 bindingResult 에 담아두면 된다.
objectName:@ModelAttribute의 이름defaultMessage: 오류 기본 메시지
validation/v2/addForm.html 수정
뷰 템플릿이 학습의 목적이 아니기에 패스..
5. BindingResult2
스프링이 제공하는 검증 오류를 보관하는 객체이다. 검증 오류가 발생하면 여기에 보관하면 된다.
BindingResult가 있으면@ModelAttribute에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다!
예) @ModelAttribute에 바인딩 시 타입 오류가 발생하면?
BindingResult가 없으면 -> 400 오류가 발생하면서 컨트롤러가 호출되지 않고, 오류 페이지로 이동한다.BindingResult가 있으면 -> 오류 정보(FieldError)를BindingResult에 담아서 컨트롤러를 정상 호출한다.
예) @RequestBody에 바인딩 시 타입 오류가 발생하면?
BindingResult가 없으면 ->@RequestBody를 통해 객체로 변환하는HttpMessageConverter단계에서 타입 오류가 발생하면,ItemSaveForm과 같은 객체를 만들지 못하기 때문에, 컨트롤러 자체가 호출되지 않고 그 전에HttpMessageNotReadableException과 같은 예외가 발생한다.BindingResult가 있으면 ->HttpMessageConverter단계에서 타입 오류가 발생하면,@RequestBody의BindingResult유무와 관계없이 동일하게HttpMessageNotReadableException과 같은 예외가 발생하고 컨트롤러는 호출되지 않는다.
BindingResult에 검증 오류를 적용하는 3가지 방법
@ModelAttribute의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이FieldError생성해서BindingResult에 넣어준다.(
@RequestBody의 경우 컨트롤러를 호출하기 이전에 튕겨버린다)컨트롤러 내부에서 개발자가 직접 넣어준다.
Validator사용 이것은 뒤에서 설명
BindingResult 와 Errors
org.springframework.validation.Errorsorg.springframework.validation.BindingResult
BindingResult 는 인터페이스이고, Errors 인터페이스를 상속받고 있다.
실제 넘어오는 구현체는 BeanPropertyBindingResult 라는 것인데, 둘다 구현하고 있으므로 BindingResult
대신에 Errors 를 사용해도 된다.
Errors 인터페이스는 단순한 오류 저장과 조회 기능을 제공한다.
BindingResult 는 여기에 더해서 추가적인 기능들을 제공한다. addError() 도 BindingResult 가 제공하므로 여기서는 BindingResult 를 사용하자. 주로 관례상 BindingResult 를 많이 사용한다.
정리
BindingResult , FieldError , ObjectError 를 사용해서 오류 메시지를 처리하는 방법을 알아보았다.
그런데 오류가 발생하는 경우 고객이 입력한 내용이 모두 사라진다. 이 문제를 해결해보자.
6. FieldError, ObjectError
목표
사용자 입력 오류 메시지가 화면에 남도록 하자.
예) 가격을 1000원 미만으로 설정시 입력한 값이 남아있어야 한다.
FieldError,ObjectError에 대해서 더 자세히 알아보자.
ValidationItemControllerV2 - addItemV2
FieldError 생성자
파라미터 목록
objectName: 오류가 발생한 객체 이름field: 오류 필드rejectedValue: 사용자가 입력한 값(거절된 값)bindingFailure: 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값codes: 메시지 코드arguments: 메시지에서 사용하는 인자defaultMessage: 기본 오류 메시지
ObjectError 생성자
ObjectError 도 유사하게 두 가지 생성자를 제공한다.
오류 발생시 사용자 입력 값 유지
사용자의 입력 데이터가 컨트롤러의 @ModelAttribute 에 바인딩되는 시점에 오류가 발생하면 모델 객체에 사용자 입력 값을 유지하기 어렵다. 예를 들어, 가격에 숫자가 아닌 문자가 입력된다면 가격은 Integer 타입이므로 문자를 보관할 수 있는 방법이 없다. 그래서 오류가 발생한 경우 사용자 입력 값을 보관하는 별도의 방법이 필요하다. 그리고 이렇게 보관한 사용자 입력 값을 검증 오류 발생시 화면에 다시 출력하면 된다.
FieldError 는 오류 발생시 사용자 입력 값을 저장하는 기능을 제공한다.
여기서 rejectedValue 가 바로 오류 발생시 사용자 입력 값을 저장하는 필드다.
bindingFailure 는 타입 오류 같은 바인딩이 실패했는지 여부를 적어주면 된다. 여기서는 바인딩이 실패한 것은 아니기 때문에 false 를 사용한다.
스프링의 바인딩 오류 처리
타입 오류로 바인딩에 실패하면 스프링은 FieldError 를 생성하면서 사용자가 입력한 값을 넣어둔다. 그리고 해당 오류를 BindingResult에 담아서 컨트롤러를 호출한다. 따라서 타입 오류 같은 바인딩 실패시에도 사용자의 오류 메시지를 정상 출력할 수 있다.
Last updated