[Spring MVC] 검증 - 직접 처리
검증
컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
클라이언트 검증, 서버 검증
- 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
- 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
- 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수.
- API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함
검증 직접 처리
고객이 등록 폼에서 필드에 값을 입력하지 않거나, 수량이 적은 등 문제 발생시 검증에 실패하게 된다. 실패한 경우 고객에게 다시 상품 등록 폼을 보여주고 문제된 부분을 알려줘야 한다.
직접 처리 개발
Controller
@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() >= 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 (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
// 성공 로직은 생략..
}
필드 오류
검증시 오류가 발생하면 errors
에 담아두고, 오류를 구분하기 위해 발생한 필드명을 key
로 사용했다.
복합 룰 검증
특정 필드를 넘어서 오류를 처리해야 할 수도 있지만, 이때는 필드명을 넣을 수 없으므로 globalError
라는 key
를 사용했다.
addForm.html
<!-- 오류시 사용할 style 추가 -->
.field-error {
border-color: #dc3545;
color: #dc3545;
}
<!-- 글로벌 오류 메시지 -->
<form action="item.html" th:action th:object="${item}" method="post">
<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>
<!-- 나머지 필드 생략... -->
글로벌 오류 메시지
오류 메시지는 errors
에 내용이 있을 때만 출력하면 된다.
[참고] Safe Navigation Operator
등록폼에 진입한 시점(GET)에는errors
가 생성되지 않았다.
따라서,errors.containsKey()
를 호출하게 되면NullPointerException
이 발생한다.
errors?
는errors
가null
일때NullPointerException
이 발생하는 대신,null
을 반환하는 문법이다.
th:if
에서null
은 실패로 처리되므로 오류 메시지가 출력되지 않는다.
필드 오류 처리
위 코드는 조건문을 사용해서 오류가 있을 시에 class명 뒤에 field-error
붙여서 구분해주었지만 classappend
를 사용하면 간편하게 수정할 수 있다.
<input type="text" th:classappend="${errors?.containsKey('itemName')} ? 'field-error' : _"
class="form-control">
classappend
를 사용해서 해당 필드에 오류가 있으면 field-error
라는 클래스 정보를 더해서 색깔을 빨간색으로 강조. 만약 오류가 없다면 _
(No-Operation)을 사용해서 아무것도 하지 않는다.
정리
- 만약 검증 오류가 발생하면 입력 폼을 다시 보여준다.
- 검증 오류들을 고객에게 안내해서 다시 입력할 수 있게 한다.
- 검증 오류가 발생해도 고객이 입력한 데이터가 유지된다.
@ModelAttribute Item item
문제점
- 뷰 템플릿에서 중복 처리가 많다.
- 타입 오류 처리가 안된다.(
Integer
에String
값 입력)- 이런 오류는 컨트롤러에 진입하기도 전에 예외가 발생하여 컨트롤러가 호출되지도 않고, 400예외가 발생하게 된다.
- 만약 컨트롤러가 호출된다고 해도
Integer
이므로 문자를 보관할 수가 없다. - 해결 : 고객이 입력한 값이 어딘가에 별도로 관리가 되어야 한다.
<출처 : 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한)>
댓글남기기