강의에서 받은 validation 프로젝트 파일을 기반으로 검증에 대해서 공부한다. 초기 세팅은 이전 프로젝트들과 동일하게 해주도록 한다.
1. 검증 내용
검증 예제를 바탕으로 학습을 해볼 것이다. 검증 요구사항이 아래와 같다고 가정한다.
1. 타입 검증
가격, 수량에 문자가 들어가면 에러
2. 필드 검증
상품명 : 필수 입력, 공백X
가격 : 1,000원 이상, 1,000,000원 이하
수량 : 최대 9,999개
3. 전체 검증
가격*수량의 값은 10,000원 이상
프론트 VS 백 검증
검증에는 클라이언트(프론트)상에서 입력값을 바로 검증하는 방법이 있고, 입력값을 서버(백)로 가져와서 검증하는 방법이 있다. 그러나 결론은 프론트, 백 모두에서 검증해야한다. 왜냐하면 프론트에서만 검증할 경우, 입력창에서 검증을 막는다하더라도, 직접 http 메시지를 postman 등으로 만들어서 보내버리면 검증이 안된 데이터가 서버로 넘어와버릴 수 있기 때문이다. 이런 보안상의 취약점으로 인해 반드시 서버에서 검증을 해야한다. 반대로 서버에서만 검증을 하면 사용자에게 즉각적인 피드백을 주기가 어렵기 때문에 두 가지 방식을 적절히 섞어서 적용해야하며, 아무리 여의치 못한 상황이라도 반드시 서버에서는 검증 코드가 있어야 한다.
2. 검증 로직 추가하기
컨트롤러에서 검증로직을 추가하도록 한다. 컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
프로젝트에서 검증이 들어갈 부분은 상품 등록 및 수정부분이다. 사용자의 입력 부분을 검증 내용에 맞게 검증한다. 기존에는
등록,GET -> 등록,POST -> 등록,REDIRECT -> 상세,GET으로 저장 성공 시 상품상세 GET으로 PRG 패턴을 이용하는 형태였다면,
검증 에러 발생 시 등록,POST -> 등록,GET으로 상품 등록화면에 머물러 있도록 바꾸어야 한다.
우선 컨트롤러에 로직을 추가하고 View를 다르게 처리할 수 있도록 하자.
컨트롤러에 검증 내용 추가
검증 오류 결과를 보관할 수 있는 Map<String, String> errors 객체를 추가한다. 그리고 검증로직을 실행하여 검증 로직에 걸리게 되면 errors 객체에 에러 메시지 내용을 추가하는 방식으로 검증을 한다. 여기서, 각 필드값이 null 인 경우를 기본적으로 검사한다는 것을 기억하자. 그리고 사용자에게 에러 정보를 알려줄 수 있도록 View단으로 이동 전, Model 객체에 errors 객체를 담고 View단으로 전달한다.
StringUtils.hasText() 메서드를 통해 특정 객체가 null인지 문자열을 포함한 String인지 검사할 수 있다는 것도 기억하자. 그리고 28번줄과 같이 이중부정문이 나와서 논리가 혼란이 오는 경우, 리팩토링 [Ctrl + Alt + M]을 통해 긍정문으로 바꿔주는 것이 권장된다.
java/hello/itemservice/web/validation/ValidationItemControllerV1.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
//검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
//검증 로직
if(!StringUtils.hasText(item.getItemName())) {
errors.put("itemName", "상품 이름은 필수입니다.");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.put("price", "가격은 1,000원 ~ 1,000,000 까지 허용합니다.");
}
if (item.getQuantity() == null || item.getQuantity() >= 10000) {
errors.put("quantity", "수량은 최대 9,999까지 허용합니다.");
}
if (item.getPrice() != null && item.getQuantity() != null) {
int totalPrice = item.getQuantity() * item.getPrice();
if (totalPrice < 10000) {
errors.put("global", "가격*수량의 합은 10,000원 이상이여야 합니다. 현재 값 = " + totalPrice);
}
}
log.info("errors ={}", errors);
//검증 로직 실패 시, 다시 입력 폼으로
//이중 부정이므로 리팩토링으로 hasError 등으로 바꾸는게 좋다.
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
//검증 로직 통과 시, 상품 저장
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
}
|
cs |
검증 로직 실패, 새로 고침 시 사용자의 입력값 유지
에러가 나거나 새로고침을 하더라도 사용자가 입력한 입력값은 유지되어야 한다. 이것은 위 코드에서 @ModelAttribute Item item 으로 해당 컨트롤러 메서드로 요청 시에 항상 item 객체의 정보를 담아서 View로 전달해주기 때문이다. 다심 말해서, 요청이 올때마다 사용자의 입력값을 item 객체에 담고, model.addAttribute("item", item)을 실행하여 model 객체로 View단에 사용자의 입력값을 전달하기 때문에 입력값이 유지될 수 있는 것이다. 새로고침을 해서 이전 요청을 재실행하게 되더라도 같은 원리로 입력값이 유지될 수 있다.
View단에 에러 메시지 표시 추가
이제 컨트롤러에서 넘겨받은 model 객체의 정보를 바탕으로 View단에 에러메시지를 표시하자.
기본적으로는 3~5번줄의 global 에러 메시지를 표시하는 방식과 같이 th:if 구문을 사용한다. 조건문에서 errors.containsKey() 메서드로 객체를 검사해서 errors Map 객체 내부에 컨트롤러에서 지정해준 에러 요소가 있는지 검사하고, 있을때만 요소를 표기하는 방식이다. 상품명, 가격, 수량부분도 기본적으로 같은 문법을 사용하는 것을 볼 수 있다.
th:class를 사용하여 에러가 있을때만 field-error 클래스를 추가하도록 해주어서, 에러 메시지 및 에러 박스가 빨간색으로 표기되도록 하였다. 해당 View 파일의 <style> 태그에 field-error 클래스의 border-color, color 부분을 추가해주어서 빨간색으로 표시되게 한 것이다(생략). 가격부분에 작성한 것처럼, th:classappend 문법을 사용해서 조건을 만족했을 때만 field-error 클래스를 추가하고, 아니면 No operation을 의미하는 언더바 _ 연산자를 적용하는 방법도 있다.
Safe Navigation Operator
3번줄에서 errors?. 로 물음표를 포함한 문법을 사용했는데, 이것은 errors 객체가 null인 경우에 null.containsKey가 되어 NullPointerException 에러가 나는 것을 방지하는 문법이다. Controller 코드를 보면, Post에서는 errors 객체를 새로 생성하여 errors 객체 자체가 null이 될 경우는 없지만, GET에서는 errors 객체가 없어서 null로 들어올 수도 있으므로 이런 코드를 적용할 수 있다는 것을 예시로 보여준다. 이 문법은 SpringEL의 문법이다.
PostMapping 에서는 errors 객체를 항상 만들어주지만, GetMapping에서는 errors 객체가 null인채로 addForm.html View로 넘어갈 수도 있다.
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v1/addForm";
}
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
//검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
//검증 로직
if(!StringUtils.hasText(item.getItemName())) {
resources/templates/validation/v1/addForm.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${errors?.containsKey('global')}">
<p class="field-error" th:text="${errors['global']}">총액 오류 메시지</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">상품명 오류</div>
</div>
<div>
<label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price" th:field="*{price}"
th:classappend="${errors?.containsKey('price')} ? 'field-error' : _"
class="form-control" placeholder="가격을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('price')}" th:text="${errors['price']}">가격 오류</div>
</div>
<div>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:field="*{quantity}"
th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">수량 오류</div>
</div>
... 중략
|
cs |
이제 다음 글에서부터 입력값의 타입에러를 검사하거나, 중복되는 부분을 스프링이 지원하는 방식을 활용하고, 메시지를 좀 더 효율적으로 처리할 수 있는 방법을 배우게 된다.
참조
1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC-2] 7. 검증(Validation) - 오류코드, 메시지 처리 (0) | 2021.08.22 |
---|---|
[스프링 웹MVC-2] 6. 검증(Validation) 개선 : BindingError, FieldError, ObjectError (0) | 2021.08.22 |
[스프링 웹MVC-2] 4. 메시지, 국제화 (0) | 2021.08.20 |
[스프링 웹MVC-2] 3. 타임리프/스프링으로 폼 양식 만들기 - th:object, th:field, 체크박스, 라디오 버튼, 셀렉트 박스 (0) | 2021.08.16 |
[스프링 웹MVC-2] 2. 타임리프 기본 - 2 : 대표 문법 공부 계속 - 반복, 조건, 주석, 블록, 자바스크립트, 템플릿, 레이아웃 (0) | 2021.08.16 |