Bean Validation
검증 기능을 매번 코드로 작성하는 것은 상당히 번거롭다. 예를 들어 특히 특정 필드에 대한 검증 로직은 대부분 빈 값인지 아닌지, 특정 크기를 넘는지 아닌지와 같이 사이즈를 측정한던가, 해당 값이 공백을 포함하고 있다던가 하는 매우 기본적인 검증방식이다.
이러한 것들이 웹 프로그래밍을 설계하는데 매우 자주 사용되고 중복되다보니 하나의 틀(Frame
)이 만들어졌고, 해당 틀을 아예 스프링 프레임워크가 간단한 애노테이션으로 제공할 수 있도록 제공하게 되었다.
BindingResult
BindingResult란 스프링이 제공하는 검증 오류 처리 보관하는 객체로써, Model에서 요청 데이터가 바인딩 될 때, 에러 발생 시 해당 에러에 대한 내용을 보관한다.
BindingResult는 2가지의 에러를 보관하는 기능을 제공해준다.
FiledError
: Model에 바인딩 되는 데이터의 Field에러를 보관하는 객체.
ObjectError
: FiledError를 제외한 Error를 보관하는 객체.
요청 데이터 바인딩 시 에러가 발생하게 된다면 FieldError or ObjectError 객체를 생성하여 에러에 관련된 내용을 담아두고 객체를 생성해서 BindingResult에 담아두고 에러를 핸들링할 수 있게 도와준다.
기본적인 흐름implementation 'org.springframework.boot:spring-boot-starter-validation'
의존성을 먼저 추가하고 스프링 프레임워크에서 제공하는 @Validated
애노테이션을 Model에 바인딩한다, 그 뒤 적용된 Model 객체 뒤에 BindingResult를 이용해 에러 발생 시 에러의 내용을 저장한 뒤 사용자에게 알려주기 위해 뷰로 보내준다.
의존성 추가 -> Bean Validation 적용 -> @Validated -> BindingResult -> 타임리프의 th:errors
의존성을 추가해줬으면, 검증을 원하는 도메인 객체에 Bean validation
애노테이션을 원하는 필드에 붙여주면 된다.
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductRequest {
private Long id;
@NotBlank
private String name;
@NotNull(message = "값을 입력해주세요.")
@Range(min = 50, max = 1_000_000)
private Integer sellingPrice;
@NotNull
@Range(min = 1_000, max = 1_000_000)
private Integer supplyingPrice;
@NotBlank
private String description;
@NotNull
private MultipartFile image;
@NotNull
private List<MultipartFile> images;
private String regDate;
@NotNull
private Long categoryId;
@NotNull
private Long deliveryType;
위와 같이 Bean Validation
이 제공하는 애노테이션에 파라미터로 message를 입력해서 해당 속성이 null일 경우 defaultMeesage를 생성할 수 있도록 @NotNull(message = "값을 입력해주세요.")
와 같이 입력해 줄 수 있다.
허나 프로젝트 규모에 따라 다르겠지만, 대규모 프로젝트의 경우 application.properties
에 spring.messages.basename=errors
를 추가해주고errors.properties
를 만들어서 에러 메시지를 관리할 수 있다.
기본적으로 스프링 부트에서는 spring.messages.basename=messages
가 defualtValue이다. (만약 messages.properties
와 errors.properties
를 같이 사용할 경우 spring.messages.basename=messages,errors
로 등록해주자.)
@PostMapping("/new")
public String reg(@Validated @ModelAttribute("product") ProductRequest regRequest, BindingResult bindingResult) throws IOException {
if (bindingResult.hasErrors()) {
log.info("Reg Form Error : {}", bindingResult + "\n");
return "/admin/products/reg";
}
service.reg(regRequest);
log.info("ProductRegRequest = {}", regRequest);
return "redirect:/admin/products";
}
그 뒤 @Validated
애노테이션을 Bean Validation
을 적용시킨 DTO 앞에 붙여준뒤, 바로 뒤에 BindingResult
를 붙여준다.
BindingResult
는 @Validated
애노테이션과 Bean Validation
애노테이션을 붙여준 클래스에서 에러가 날 경우 에러의 내용과 에러가 발생한 특정 필드를 저장하는 역할을 수행한다.
저장 한 뒤 뷰로 데이터를 보내는데 뷰로 데이터를 보낸 곳에서 errors.properties
에서 정의한 에러 메시지를 사용하려면 th:errors*{}
를 사용해주면 된다.(타임리프 사용 시)
타임리프의 사용자 입력 값 유지th:field="*{}"
타임리프의 th:field="*{}"
는 정상 상황에서는 모델 객체의 값을 사용하지만, 오류 발생시에는 FieldError
에서 보관한 값을 사용해서 값을 출력한다.
스프링의 바인딩 오류 처리
타입 오류로 인한 바인딩 실패 시 스프링은 FieldError
를 생성하면서 사용자가 입력한 값을 넣어둔다. 그리고 해당 오류를 BindingResult
에 담아서 컨트롤러를 호출하는데, 따라서 타입 오류 같은 바인딩 실패시에도 사용자의 오류 메시지를 정상 출력할 수 있다.
<form method="post" th:action="@{/admin/products/new}" class="n-form n-form-type:outline-box px:10 w:100p"
th:object="${product}" enctype="multipart/form-data">
<div>
<label>
<span>상품 이름</span>
<input type="text" th:field="*{name}">
<div th:errorclass="field-error" th:errors="*{name}"></div>
</label>
</div>
<div>
<label>
<span>판매가</span>
<input type="text" th:field="*{sellingPrice}"/>
<div th:errorclass="field-error" th:errors="*{sellingPrice}"></div>
</label>
</div>
<div>
<label>
<span>공급가</span>
<input type="text" th:field="*{supplyingPrice}"/>
<div th:errorclass="field-error" th:errors="*{supplyingPrice}"></div>
</label>
</div>
<div>
만약 name 필드에서 에러가 날 경우 <div th:errorclass="field-error" th:errors="*{name}"></div>
해당 속성이 출력된다.
BindingResult
가 제공하는 rejectValue()
, reject()
를 사용하면 FieldError
, ObjectError
를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.
추가
@ModelAttribute에 바인딩 시 타입 오류가 발생하게 된다면
-> BindingResult
가 없으면 400오류가 나오며 컨트롤러 자체가 호출되지 않지만, BindingResult
가 있으면 오류 정보를400Erroor
를 FieldError
에 담아서 컨트롤러를 정상 호출한다. 에러가 나서 스프링 부트가 보여주는 페이지를 보여주고 끝내는게 아니라, 에러 정보를 보관하기 때문에, BindingResult
를 활용하여 에러를 핸들링 할 수 있다.
컨트롤러가 정상 호출 될 수 있는 이유Http Method
요청이 호출 된 이후에 Dispatcher Servlet이 호출 되는데, 요청 URL을 처리할 수 있는 핸들러를 찾을 HandlerAdapter
를 찾게 된다.
찾게 되면 해당 반횐 된 Handler
를 통해 메서드를 호출하는데 이 때 해당 메서드의 파라미터에 있는 애노테이션들을 읽어서 정보를 넣어주는 역할을 수행해주는 ArgumentResolver
가 존재한다.
ArgumentResolver
를 통해 파라미터의 인자값들이 존재하면 Handler
가 호출 되고 이 떄 @Validation
이 적용된다. 적용이 실패할 경우 -> Error가 발생할 경우 BindingResult
에 마치 try-catch가 작동하는 것처럼 catch 부분에서 에러를 잡아내서 BindingResult
에 보관한다. 그러므로 Controller가 정상적으로 호출되는 것!!!
Bean Validation 애노테이션 모음
'스프링' 카테고리의 다른 글
[Spring] 서블릿의 예외 처리와 ExceptionHandler, ControllerAdvice 알아보기 (0) | 2024.05.17 |
---|---|
[Spring] 서블릿의 Filter, 스프링의 Interceptor 예제 코드로 알아보기 (0) | 2024.05.16 |
[Spring] 로그인 기능 구현으로 알아보는 쿠키 및 세션 (2) | 2024.05.13 |
[Spring] 애노테이션 파라미터를 처리하는 ArgumentResolver (1) | 2024.05.13 |
[Spring] MultipartFile을 이용한 파일 업로드 및 수정, 삭제 (1) | 2024.04.29 |