컨트롤러의 중요한 역할 중 하나는 HTTP요청이 정상인지 검증하는 것이다.
따라서 클라이언트 검증과 서버 검증을 적절하게 섞어서 사용해야한다.
또 검증을 할 때에 검증 오류 발생했다면 오류가 발생한 폼에 사용자가 입력한 값을 유지시키고, 오류 메시지를 같이 띄워 사용자가 인식 할 수 있게 하는 것이 좋은 방법이라고 생각한다.
필드 오류란 말 그대로 필드 값이 요구되는 범위 밖으로 벗어난 오류를 말한다.
예를 들면 상품 개수를 1000개까지만 입력 받는데, 이때 1001개를 입력 했다면 이는 필드 오류에 해당한다.
객체 오류란 필드 오류와 다르게 하나의 필드 값으로만 발생하는 오류가 아니라 이를 넘어서는 오류를 말한다.
예륻 들면 주문을 받을때 최소 주문금액을 15,000원이라고 했을 때 5천원짜리 짜장면 한 그릇과 6천원짜리 짬뽕 한 그릇만 주문했다면 이는 객체 오류에 해당한다.
바인딩 오류란 HTTP 요청시 들어온 데이터 값이 제대로 바인딩 되지 않았을때 발생하는 오류로 예를 들자면 상품 개수가 1,2 와 같은 int 타입이아니라 한개, 두개와 같은 string 타입이 들어온다면 이는 바인딩 오류에 해당한다.
만약 위에서 말한 바인딩 오류가 발생한다면, 컨트롤러가 호출 되기도 전에 오류로 인해서 예외처리가 실행 될 것이다. 이렇게 되면 오류가 난 값을 따로 관리 할 수 없다. 이럴 때 사용하는 것이 BindingResult
이다.
BindingResult
이란 검증 오류를 보관하는 객체로 오류를 낸 값까지 같이 보관을 하고 있다.
이 BindingResult
를 사용한다면 바인딩 오류가 발생한다고 해도 컨트롤러가 호출된다.
다만 api방식에서는 컨트롤러가 호출 되지 않는다.
api방식은 http 컨버터가 메시지를 변환하지 못하는 오류로 처리를 하기 때문이다.
BindingResult
는 반드시 검증할 객체 뒤에 위치해야한다.
@ModelAttribute
의 객체가 바인딩 실패하면 spring이 자동으로 FieldError
를 생성해서 BindingResult
에 넣어준다.@validated
를 사용한다.오류 메시지를 원하는 방식으로 정의 할 수있다. 이럴때 메시지를 정의하는 방법은 errors.properties
파일을 만들어 원하는 오류 메시지를 적으면 된다.
그리고 이 파일을 적용시키기 위해서는 application.properties
파일에 설정을 하나 해줘야한다.
spring.messages.basename=errors
이렇게 만들어진 파일이름을 적는 것이다. 만약 국제화를 위한 message.properties
파일도 있다면 ,
로 구분해서 여러개를 적으면 된다. 또 에러 메시지도 국제화를 하고 싶다면 errors_en.properties
등의 파일을 만들어 국제화를 하면 된다.
또 오류 메시지에는 우선순위가 있는데, 디테일한 것이 제일 우선순위가 높다.
만약 오류 메시지 파일에 내용이
required=필요한 값이 제대로 입력되지 않았습니다.
required.item.itemName=아이템 이름이 입력되지 않았습니다.
이런식으로 있다면, 아이템 이름이 입력되지 않은 상황에서는 2번째 줄인 아이템 이름이 입력되지 않았습니다.
가 오류 메시지로 나오고, 나머지는 필요한 값이 제대로 입력되지 않았습니다.
가 출력될 것이다.
이를 이용해 덜 디테일한 오류메시지를 기본으로 쓰고, 자세하게 오류 메시지를 알려줘야할 경우에는 해당 조건을 적어 그 부분은 자세히 따로 정의하면 된다.
이렇게 errors.properties
를 작성할 때에 앞에 key값으로 적혀 있는 것들은 오류 코드라고하는데, 이러한 오류코드는 BindingResult
에 같이 저장된다. 그래서 errors.properties
를 작성 할 때에 이 규칙에 맞추어서 정의한다면 따로 기본 메시지라고 지정해 주지 않아도 spring이 읽는다.
만약 다르게 한다고 해서 사용 할 수 없는 것이 아니라 개발자가 직접 해당 오류에 이러한 메시지가 나와야한다고 지정해야한다.( 검증 오류 적용 방법에 따라 달라진다 )
Bean validation방법은 위에서 말한 검증 오류 적용 방법중 @validated
를 이용하는 방법이다. 그 외의 방법도 있지만 이 곳에서는 이 방법만 설명하려고 한다.
이 방식은 간단하다. @Validated
어노테이션을 검증할 객체 앞에 붙여주기만 하면된다. 또 이 검증 결과 또한 BindingResult
에 담기기 때문에 검증할 객체 뒤에 당연히 있어야한다.그리고 각 필드의 검증 조건은 해당 객체에 정의한다.
@PostMapping("/test")
pulbic String test(@Validated @ModelAttribute("item") Item item, BindingResult bindingResult){
if(bindingResult.hasResult()){
//검증 실패로직
}
// 검증 성공 로직
,,,
}
Public class Item{
public Integer itemNum;
@NotBlank
public String itemName;
@Range(min=100,max=9900)
public Integer price;
}
이렇게 각 필드에 어노테이션을 통해서 필드 검증 조건을 설정 할 수 있다.
예를 들어서 상품 등록시 검증 조건과 상품 수정시에 검증 조건이 다르면 어떻게 처리할 것인가? 이렇게 검증을 한다면, 간편하지만 같은 객체에 다른 검증 조건을 요구한다면 해결하기가 어렵다. 왜냐면 Item
이라는 객체에 아예 검증 조건을 정의했기 때문이다.
이를 극복하기위해 groups
라는 기능을 사용하는 방법도 있지만, 코드가 복잡해지는 것 같아 추천하지 않는다. 대신 이런 경우에는 각 화면 별로 주고받는 데이터를 따로 구현해 검증을 한다.
예를 들어서 Item
을 수정하거나 삭제한다고 할 때 SaveItemForm
과UpdateItemForm
을 따로 만들어 클라이언트에게 입력 받은 데이터는 이러한 폼 객체를 사용해 검증하고, 폼 객체를 통해서 Item
을 만드는 방식이다.
물론 컨트롤러에 이부분을 작성하는 것이 조금 번거롭겠지만 조금 더 직관적이라고 생각한다.
지금까지는 @ModelAttribute
로 받는 폼 데이터에 대해 주로 검증했다. 만약 폼 데이터가 아닌 @RequestBody
로 받는 HTTP body 메시지라면 어떻게 검증을 적용해야할까?
이 검증은 위에서 말한 것과 달리 HTTP body 메시지는 HTTP 컨버터가 실행되어 객체로 변환이되는데, 만약 여기서 오류가 난다면 위에서 바인딩 오류에 대해 처리한 것과 달리 HTTP 컨버터 예외처리로 들어가기 때문에 컨트롤러가 호출되기 이전에 끝난다. 때문에 BindingResult에 값이 담기지 않는다.
이러한 차이가 나는 이유는 폼 객체가 바인딩되는 과정과 HTTP body 메시지가 HTTP 컨버터를 통해 변환이 되는 과정이 다르기 때문이다.
차이
@ModelAttribute
는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.
@RequestBody
는HttpMessageConverter 단계
에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.
이 부분 이외에 컨트롤러가 호출된 이후부터는 똑같이 실행이 된다.