목차
Validation : 데이터 검사
Validation이란 데이터의 값이 유효한지 검사하는 것이다.
유효성검사를 통해 올바르지 않은 데이터가 서버로 전송되거나, DB에 저장되지 않도록 한다.
의존성 설정
spring-boot-starter-validation은 가장 유명한 hibernate validator를 포함하고있다
Maven이용 시 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Gradle이용 시 build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
Controller에 검증로직 직접 구현
Map<String, String> errors 객체를 생성하여 에러에 해당되는 필드와 에러메시지를 저장하여 view에 넘겨준다
타임리프 TIP
th:if="${errors?.containsKey( 'key값' )}" : model을 통해 받은 errors 객체에 'key값'이 있다면 해당 태그를 표시한다
th:text="${errors[ 'key값' ]}" : errors 객체의 'key값'에 해당되는 value값을 꺼낸다
@PostMapping("/form")
public String formSave(@ModelAttribute Board board, RedirectAttributes redirectAttributes, Model model){
// 검증 오류 결과 저장
Map<String, String> errors = new HashMap<>();
//검증 로직
if (!StringUtils.hasText(board.getTitle())) {
errors.put("title", "제목은 필수이고, 2~10글자를 입력해주세요");
}
if (!StringUtils.hasText(board.getContent())){
// str이 null이거나 길이가 0이면 true 반환.
errors.put("content", "내용은 필수입니다");
}
// 오류 발생 시 입력폼으로
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "board/form";
}
// 성공 시
Board savedBoard = boardRepository.save(board);
redirectAttributes.addAttribute("boardId", savedBoard.getId());
return "redirect:/board/form?id={boardId}";
}
<form method="post" th:action="@{/board/form}" th:object="${board}">
<div class="mb-3">
<label for="title" class="form-label">제목</label>
<input type="text" class="form-control" id="title" th:field="*{title}">
<div class="invalid-feedback"
th:if="${errors?.containsKey('title')}" th:text="${errors['title']}"
th:style="${errors?.containsKey('title')} ? 'display:block'">
Please select a valid state.
</div>
</div>
</form>
문제점
view에서 각 항목마다 비슷한 로직을 넣어야한다
타입 오류는 스프링 컨트롤러에 오기도 전에 예외가 발생하기때문에 컨트롤러 호출 불가하다 (오류페이지 못띄워줌)
아예 오류페이지가 떠버리니 기존에 입력했던 값을 보여줄 수 없다
스프링의 BindingResult 객체 사용
Map<String, String> errors = new HashMap<>(); 에 오류메시지를 담는것이 아닌 BindingResult객체를 사용한다
@ModelAttribute에서 데이터가 바인딩될 때 오류가 발생하면, 해당 오류 객체는 BindingResult에 저장된다.
= BindingResult객체를 사용하면 데이터 검증 결과를 담고 있다.
@ModelAttribute에 오류가 발생해도 Controller가 호출될 수 있다.
BindingResult객체는 Model처럼 view로 자동 전달된다.
BindingResult객체는 반드시 @ModelAttribute 바로 옆에 작성한다!!
bindingResult.addError(
new FieldError( "오류 발생 객체명",
"필드명",
사용자가 입력한 값,
"바인딩실패인지, 검증실패인지에 대한 구분값",
"에러코드",
"메시지에서 사용하는 인자"
"에러메시지" ) //필드에 오류가 있을 때
OR
new ObjectError( "오류 발생 객체명", "에러메시지" ) //복합적으로 오류가 있을 때 (객체수준)
);
(사실 BindingResult는 @ModelAttribute의 객체를 참고하고있기때문에 "오류 발생 객체명"은 생략 가능하다)
타임리프 TIP
th:errors="*{필드명}" : th:if와 th:text를 합친 것이다. 에러가 있으면 해당 필드명으로 저장된 에러메시지를 출력한다
@PostMapping("/form")
public String formSave(@ModelAttribute Board board,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
//검증 로직
if (!StringUtils.hasText(board.getTitle())) {
bindingResult.addError(new FieldError(
"board", "title", "제목은 필수이고, 2~10글자를 입력해주세요"
));
}
if (!StringUtils.hasText(board.getContent())){
// str이 null이거나 길이가 0이면 true 반환.
bindingResult.addError(new FieldError(
"board", "content", "내용은 필수입니다"
));
}
// 오류 발생 시 입력폼으로
if (bindingResult.hasErrors()) {
return "board/form";
}
// 성공 시
Board savedBoard = boardRepository.save(board);
redirectAttributes.addAttribute("boardId", savedBoard.getId());
return "redirect:/board/form?id={boardId}";
}
<form method="post" th:action="@{/board/form}" th:object="${board}">
<div class="mb-3">
<label for="title" class="form-label">제목</label>
<input type="text" class="form-control" id="title" th:field="*{title}">
<div class="invalid-feedback"
th:errors="*{title}"
th:style="${#fields.hasErrors('title')} ? 'display:block'">
Please select a valid state.
</div>
</div>
</form>
⭐만약 오류 발생하면
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors Field error in object 'board' on field 'title': rejected value
만약 이런 오류가 발생하거나, 오류 발생하는 이유를 모르겠다 싶으면
@ModelAttribute 바로 다음 인자가 BindingResult인지 확인해보자!!
(나는 @ModelAttribute 바로 다음 인자가 BindingResult가 아닌 RedirectAttribute여서 오류가 발생했다..😅)
오류코드와 메시지 (rejectValue, reject)
FieldError 객체의 에러코드와 메시지에서 사용하는 인자가 많아졌을 때를 대비하여 별도의 파일로 관리하자
MessageCodesResolver 인터페이스를 사용하자.
application.properties
spring.messages.basename=messages,errors
이렇게 하면 errors.properties, messages.properties 파일을 인식할 수 있게된다
erros.properties
⚠ 인텔리제이의 경우 properties파일의 인코딩은 ISO8859이므로 한글을 사용하고자하면
settings > Editor > File Encodings > Transparent native-to-ascii conversion을 체크하고 저장해주자
required=필수값입니다.
typeMismatch=타입오류입니다
#typeMismatch는 스프링이 만든 오류이다
#필드오류 메시지 생성규칙
#오류코드.객체명.필드명
#오류코드.필드명
#오류코드.필드타입
#오류코드
#객체오류 메시지 생성규칙
#오류코드.객체명
#오류코드
FieldError는 rejectValue("오류필드명", "오류코드", 사용자 입력값)
ObjectError는 reject("오류코드", 사용자 입력값)으로 대체한다.
@PostMapping("/form")
public String formSave(@ModelAttribute Board board,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
//검증 로직
if (!StringUtils.hasText(board.getTitle())) {
bindingResult.rejectValue("title", "required");
}
if (!StringUtils.hasText(board.getContent())){
bindingResult.rejectValue("content", "required");
}
// 오류 발생 시 입력폼으로
if (bindingResult.hasErrors()) {
return "board/form";
}
// 성공 시
Board savedBoard = boardRepository.save(board);
redirectAttributes.addAttribute("boardId", savedBoard.getId());
return "redirect:/board/form?id={boardId}";
}
<form method="post" th:action="@{/board/form}" th:object="${board}">
...
<div class="invalid-feedback" th:errors="*{content}" style="display:block;">
Please select a valid state.
</div>
...
</form>
rejectValue(), reject() 메서드는 내부적으로 MessageCodesResolver를 사용하여
다음과 같은 메시지 코드들의 String배열을 자동으로 생성한다.
#필드오류 메시지 생성규칙 #오류코드.객체명.필드명 #오류코드.필드명 #오류코드.필드타입 #오류코드 |
rejectValue("title", "required") required.board.title required.title required.java.lang.String required |
#객체오류 메시지 생성규칙 #오류코드.객체명 #오류코드 |
reject("required") required.board required |
Validator 인터페이스
컨트롤러에서 검증로직의 비율이 너무 무거워지고있다
boolean supports(Class<?> clazz) : 파라미터의 클래스가 해당 validator에서 처리할 수 있는지 확인
validate(Object target, Errors errors) : 검증대상(target)과 대상의 Error객체(bindingResult객체)를 받아서 검증로직을 처리한다
BindingResult클래스는 errors를 구현하는 객체이다.
@Component
public class BoardValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Board.class.isAssignableFrom(clazz);
//isAssignableForm : 인스턴스를 현재 형식의 인스턴스에 할당할 수 있는지 여부 확인
}
@Override
public void validate(Object target, Errors errors) {
Board board = (Board) target;
//검증 로직
if (!org.springframework.util.StringUtils.hasText(board.getTitle())) {
errors.rejectValue("title", "required");
}
if (!StringUtils.hasText(board.getContent())){
errors.rejectValue("content", "required");
}
}
}
@Autowired
private BoardValidator boardValidator;
@PostMapping("/form")
public String formSave(@ModelAttribute Board board,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
boardValidator.validate(board, bindingResult);
// 오류 발생 시 입력폼으로
if (bindingResult.hasErrors()) {
return "board/form";
}
// 성공 시
Board savedBoard = boardRepository.save(board);
redirectAttributes.addAttribute("boardId", savedBoard.getId());
return "redirect:/board/form?id={boardId}";
}
<form method="post" th:action="@{/board/form}" th:object="${board}">
...
<div class="invalid-feedback" th:errors="*{content}" style="display:block;">
Please select a valid state.
</div>
...
</form>
Bean Validation (어노테이션 사용)
아주 간단한 검증로직은 어노테이션으로 대체할 수 있다.
타겟 객체에 @Validated를 넣고, 검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담는다
(=@Validated가 붙은 객체 바로뒤에는 BindingResult를 매개변수로 추가해주어야한다)
필드는 어노테이션으로 처리가 가능하지만 특정 필드가 아닌 객체수준의 검증은 어노테이션으로는 한계가 있기때문에 Validator를 구현해서 직접 검증로직을 작성해주자
public class Board {
private Long id;
@NotBlank
@Size(min=2, max=10, message = "제목은 2자 이상 30자 이하입니다")
private String title;
// @NotNull //null비허용, "" " " 허용됨
// @NotEmpty //null, "" 비허용, " " 허용됨
@NotBlank //null, "", " " 모두 비허용
private String content;
}
@PostMapping("/form")
public String formSave(@Validated @ModelAttribute Board board,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
// 오류 발생 시 입력폼으로
if (bindingResult.hasErrors()) {
return "board/form";
}
// 성공 시
Board savedBoard = boardRepository.save(board);
redirectAttributes.addAttribute("boardId", savedBoard.getId());
return "redirect:/board/form?id={boardId}";
}
<form method="post" th:action="@{/board/form}" th:object="${board}">
...
<div class="invalid-feedback" th:errors="*{content}" style="display:block;">
Please select a valid state.
</div>
...
</form>
@Null | Null값만 허용됨 |
@NotNull | null (X), "" (O), " " (O) |
@NotEmpty | null (X), "" (X), " " (O) |
@NotBlank | null (X), "" (X), " " (X) |
@Pattern(regexp="정규표현식", message="에러메시지") | 입력값이 정규표현식에 해당되어야함 |
@Range(min=최소값, max=최대값) | 입력값이 범위 내의 값이어야함. |
@Size(min=최소값, max=최대값) | 입력크기가 범위 내의 크기이어야함. |
@Max(최대값) | 입력값이 최대값보다 작아야함 |
@Min(최소값) | 입력값이 최소값보다 커야함 |
유효한 이메일인지 확인함 | |
@DateTimeFormat(pattern="YYYYMMDD") | 패턴에 맞는 날짜형식이어야함 |
한계점
각 view마다 다른 검증을 적용할 수 없다
해결방법 1 : view별 다른Board 클래스를 만들어 적용한다
해결방법 2 : BeanValidation의 groups 기능 사용 (크게 추천하지않음)
groups 기능
객체의 검증 어노테이션에 (groups = 허용할클래스명.class) 으로 그룹을 지정한다.
@PostMapping(/주소)에 따라 @Validated(그룹명.class)를 다르게 적어주면된다.
public interface SaveForm { }
public interface UpdateForm { }
public class Board {
@NotNull(groups = UpdateForm.class) //updateForm클래스만 해당
private Long id;
@NotBlank(groups = {SaveForm.class, UpdateForm.class})
@Size(min=2, max=10, message = "제목은 2자 이상 30자 이하입니다")
private String title;
@NotBlank(groups = {SaveForm.class, UpdateForm.class})
private String content;
}
@PostMapping("/form")
public String formSave(@Validated(SaveForm.class) @ModelAttribute Board board,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
}
@PostMapping("/edit")
public String formEdit(@Validated(UpdateForm.class) @ModelAttribute Board board,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
}
@ModelAttribute와 @RequestBody차이점
@ModelAttribute는 HTTP 요청 파라미터(쿼리스트링, form)를 가져올 때
@RequestBody는 HTTP Body의 데이터(JSON)를 가져올 때 사용
@Validated @ModelAttribute Board board
@RequestBody @Validated Board board
@ModelAttribute는 필드 단위로 적용되기때문에
TypeMismatch가 발생해도 다른 필드는 정상처리 가능하다 => @Validated 검증 가능
@RequestBody는 객체 단위로 적용되기때문에
TypeMismatch가 발생하면 HttpMessageConverter에서 JSON 객체로 변환 자체가 안된다 => 예외 발생 (컨트롤러조차 호출 안됨)
정리
필드 수준의 검증(어노테이션 이용)
1. Board 객체에 @NotNull 등의 어노테이션 추가
2. Controller의 메서드 매개변수에 @Valiedated 추가 (@ModelAttribute 앞에)
3. @ModelAttribute 다음인자로 반드시 BindingResult 추가
4. 타임리프 th:object="${board}", th:errors="*{content}" 로 에러 출력
객체 수준의 검증(Validator 인터페이스 이용)
1. Validator 인터페이스를 구현한 BoardValidator 클래스 생성 후 @Component 선언
2. supports 메서드에 Board.class.isAssignableForm 입력
3. validate 메서드에 검증로직 작성 (StringUtils 사용하면 편리)
errors.rejectValue("에러필드", "에러코드") / errors.reject("에러코드")
4. Controller 파일에 @Autowired private BoardValidator boardValidator;
5. 메서드에 boardValidator.validate(객체, bindingResult) 작성하여 메서드 수행
에러메시지 설정
1. application.properties 파일에 "spring.messages.basename=errors" 추가
2. errors.properties 파일에 "에러코드=에러메시지" 작성 (인코딩 꼭 확인)
'Backend' 카테고리의 다른 글
홈페이지 최초접속 시 url에 jsessionid 자동으로 안붙게하기 (0) | 2021.12.20 |
---|---|
Cookie와 Session, 그리고 Token (0) | 2021.12.09 |
스프링MVC 기본기능 (0) | 2021.12.03 |
스프링 MVC 구조, DispatcherServlet, @RequestMapping.. (0) | 2021.12.01 |
FrontController, View, Model, Adapter, Handler (0) | 2021.12.01 |