사용자의 요청에 응답하는 서버는 사용자의 메시지가 목적에 맞게 잘 전달되었는지 확인하는 절차가 필요하다.
이러한 검증은 수시로 변경될 수 있고, 사용자의 편의성을 위해 클라리언트 검증과 서버 검증, 데이터베이스 검증... 등으로 나누어질 수 있다.
- 타입 검증
- 필드 검증
- 특정 필드의 범위를 넘어서는 검증
실제 보낸 요청이 프로그램 언어에 맞는 타입으로 변환될 수 있는지 확인하는 타입 검증 부분과, 필드의 값이 의도한 필드 값의 형식에 맞는지 검증하며, 특정 필드의 값의 범위를 제한하여 처리하는 검증이 도 있고 이 데이터가 데이터베이스에 저장될 때 데이터베이스에 저장할 수 있는지 여부조차도 검증한다.
클라이언트에서 자바스크립트를 통해 검증을 완벽하게 구현하면 서버에서 검증이 필요 없다고 생각하는 사람을 실제로 보기도 했다. 서버는 착한 클라이언트들이 완벽한 가이드대로 입력하는 경우도 있지만 실수로 서버를 공격하거나 악의적인 해커가 공격을 할 수 있다는 사실은 큰 가능성으로 두어야 한다고 생각한다.
클라이언트를 통해 서버에 요청하지만 우회하여 클라이언트의 요청을 조작할 수 있기 때문에 클라이언트 검증만으로는 완벽하지 않고 이는 고객 사용성에 초점을 맞추어 검증하는 경우가 대부분이다. 그렇기에 서버에서는 필수적으로 검증해야 한다. 서버에서만 검증한다면 클라이언트의 UX경험이 매우 안 좋기 때문에 이 둘을 적적 힐 섞어 사용하는 것이 좋다.
API 방식으로 통신하는 경우, 마치 서버to서버, Open API 서버들은 요청의 대한 검증을 진행한 후 그에 해당되는 적절한 대응을 API 응답 결과에 잘 넣어서 보내주면 된다.
검증이 실패한 경우 ?
특정 사용자의 요청이 검증이 실패했다면 그에 대한 대응은 어떻게 이루어져야 할까?
만약 사용자의 입력이 필수 입력 값을 입력하지 않고 서버에 요청을 하여 검증이 실패한 경우 서버의 검증 로직이 실패하고, 사용자에게 재입력할 수 있는 화면을 제공하면서, 어떠한 값이 누락됐는지 알려주어야 한다.
@PostMapping("/add")
public String addItem(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() >= 9999) {
errors.put("quantity", "수량은 최대 9,999까지 허용합니다.");
}
//복합 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격과 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
}
}
//검증 실패 시
if (hasError(errors)){
model.addAttribute("errors", errors);
return "validation/addForm";
}
//성공 로직
저장하고, redirect에 해당 item, status를 전달한다.
PRG를 사용
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/items/{itemId}";
}
* Model에 값을 담지 않는 이유
스프링 MVC 구조에서 dispathcerServlet이 요청을 처리할 Adapter를 찾고 그에 대응되는 Method를 처리할 때 HandlerArgumentResolver의 구현체들이 해당 인자를 생성 가능하면 생성하여 제공해주는데 스프링 부트는 기본적으로 사용자 정의 타입은 @ModelAttribute가 붙은 것과 동일하게 처리한다. 해당 요청을 Item에 맵핑을 해준후 넘겨준다. 또한 이 @ModelAttribute가 적용되는 범위는 Model에 담지 않아도 view 템플릿에서 접근할 수 있다.
별도의 웹 프런트 프레임워크를 사용한다면 응답 값에 정해진 Content-type에 맞게 처리하면 되지만
서버 사이드 렌더링에 사용되는 jsp, thymeleaf, velocity.. 를 사용한다면 템플릿 내부에서 처리해야 한다.
Thymeleaf 처리
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</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>
* Safe Navigation Operator
첫 화면에 진입한 경우에는 errors라는 HashMap이 존재하지 않는다. 저장 버튼을 눌렀을 때 검증이 이루어지기 때문이다. 그렇기에 errors에 접근할 때 NullPointerException이 발생할 것이다.
Sage Navigation Operator는 해당 문제를 해결하는 방법을 제시했는데 예시 코드에서
errors?. 은 errors가 null 일 때 NullPointerException을 발생하는 대신 null을 반환하는 문법이다.
th:if에서 null인 경우는 false로 처리되기 때문에 해당 에러 메시지 출력하는 html이 렌더링 되지 않는다.
이것은 스프링의 SpringEL이 제공하는 문법이다.
'Spring|Spring-boot' 카테고리의 다른 글
Spring MessageCodesResolver (0) | 2022.01.08 |
---|---|
Spring Web Validation (2) (0) | 2022.01.08 |
Spring 메시지, 국제화 (0) | 2022.01.06 |
Spring MVC HTTP Header 처리와 Arguments, Return (0) | 2021.12.29 |
뷰 리졸버 (0) | 2021.12.25 |
댓글