질문, 피드백 등 모든 댓글 환영합니다.
지난시간에 개발한 Controller에 세부적인 검증 로직을 개발하고 에러 메시지를 추가하겠습니다.
@Length : 해당 필드의 값의 길이 지정
@NotEmpty : null, 공백("") 허용 x
@FutureOrPresent : Date 등의 타입에서 현재, 미래만 가능
@Validated : 이 어노테이션이 붙은 필드를 검증
th:field는 렌더링 후 html의 id, name는 field의 식별자로, value는 그 값으로 자동으로 생성합니다.
또한 에러가 발생하여 다시 html이 렌더링 될 때 해당 필드의 값을 유지하여 보여주는 기능을 가지고 있습니다.
MemberDto
@Getter @Setter
public class MemberDto {
@Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
private String loginId;
@Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
private String password;
private String checkPassword;
@Length(max = 20, min = 2, message = "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.")
private String name;
}
HomeController
class HomeController {
@PostMapping("/add")
public String save(@Validated @ModelAttribute MemberDto memberDto, BindingResult bindingResult) {
if (!bindingResult.hasFieldErrors("name") && !validateKorean(memberDto.getName()))
bindingResult.rejectValue("name", "name", "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.");
if (!bindingResult.hasFieldErrors("loginId") && !validateString(memberDto.getLoginId()))
bindingResult.rejectValue("loginId", "loginId", "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.");
if (!bindingResult.hasFieldErrors("password")) {
if (!validateString(memberDto.getPassword()))
bindingResult.rejectValue("password", "password", "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.");
else if (!memberDto.getPassword().equals(memberDto.getCheckPassword()))
bindingResult.rejectValue("checkPassword", "", "비밀번호가 일치하지 않습니다.");
}
if (bindingResult.hasErrors()) return "/member/add";
Member member = new Member(memberDto.getLoginId(), memberDto.getPassword(), memberDto.getName().strip());
if (memberService.save(member) == null) {
bindingResult.rejectValue("loginId", "duplication", "이미 존재하는 ID 입니다.");
return "/member/add";
}
return "redirect:/";
}
private boolean validateString(String str) {
String temp = "";
for (int i = 0; i < str.length(); i++) {
if (!String.valueOf(str.charAt(i)).matches("[^a-z0-9]")) { // 영소문자나 숫자면
temp += str.charAt(i);
}
}
return str.equals(temp) ? true : false;
}
private boolean validateKorean(String str) {
String temp = "";
for (int i = 0; i < str.length(); i++) {
if (!String.valueOf(str.charAt(i)).matches("[^ㄱ-ㅎㅏ-ㅣ가-힣a-z0-9\\s]")) { //소문자거나 한글이면 공백이면
temp += str.charAt(i);
}
}
return str.equals(temp) ? true : false;
}
@Length에서 값 길이를 검증하고 validateString(), validateKorean()에서 정규식을 활용해 특수문자 여부 등 확인 정규식은 이 블로그를 참고했습니다.
에러 메시지가 중복되어 생성되지 않도록 이미 생성된 에러가 없을 경우에 이후 로직 적용하는 방식으로 개발
validation 어노테이션으로 1차 검증 후 로그인 Id 중복 검사
BindingResult의 void rejectValue()를 이용해 필드 오류를 생성하고 view로 전달
rejectValue(@Nullable String field, String errorCode)
rejectValue(@Nullable String field, String errorCode, String defaultMessage)
rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable Strign defaultMessage)
add.html
<form th:object="${memberDto}" method="post">
<div class="d-grid gap-2 col-6 mx-auto">
<label for="name" class="form-label">이름</label>
<input type="text" id="name" class="w-100 form-control"
placeholder="2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다." th:field="*{name}">
<div th:errors="*{name}"></div> <!--추가-->
<label for="loginId" class="form-label">로그인 id</label>
<input type="text" id="loginId" class="w-100 form-control"
placeholder="5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다." th:field="*{loginId}">
<div th:errors="*{loginId}"></div> <!--추가-->
<label for="password" class="form-label">비밀번호</label>
<input type="password" id="password" class="w-100 form-control"
placeholder="5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다." th:field="*{password}">
<div th:errors="*{password}"></div> <!--추가-->
<label for="checkPassword" class="form-label">비밀번호 확인</label>
<input type="password" id="checkPassword" class="w-100 form-control" th:field="*{checkPassword}">
<div th:errors="*{checkPassword}"></div> <!--추가-->
</div>
</form>
html에 타임리프 에러 메시지 추가
th:error는 해당 필드에 에러가 존재하면 에러 메시지를 찾아 출력
LoginDto
public class LoginDto {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
@NotEmpty로 1차 검증 후 로그인 Id와 비밀번호 일치 여부 검증
LoginController
class LoginController {
@PostMapping("/login")
public String login(@Validated @ModelAttribute LoginDto loginDto, BindingResult bindingResult, HttpServletRequest request) {
if (bindingResult.hasErrors())
return "/login/form";
Optional<Member> loginMember = loginService.login(loginDto.getLoginId(), loginDto.getPassword());
if (loginMember.isEmpty()) {
bindingResult.reject("loginFail", "id password 에러");
return "/login/form";
}
HttpSession session = request.getSession();
session.setAttribute("loginMember", loginMember.get());
return "redirect:/todo";
}
}
BindingResult의 void reject()로 글로벌 에러 생성, view로 전달
reject(String errorCode)
reject(String errorCode, String defaultMessage)
reject(String errorCode, @Nullable Object[] errorArgs, @Nullable Strign defaultMessage)
form.html
<form th:object="${loginDto}" method="post">
<div th:if="${#fields.hasGlobalErrors()}"> <!--추가-->
<p>id 혹은 비밀번호가 정확하지 않습니다.</p> <!--추가-->
</div> <!--추가-->
<div class="d-grid gap-2 col-6 mx-auto">
<label for="loginId" class="form-label">로그인 ID</label>
<input type="text" id="loginId" class="w-100 form-control" th:field="*{loginId}">
<div th:errors="*{loginId}"></div> <!--추가-->
<label for="password" class="form-label">비밀번호</label>
<input type="password" id="password" class="w-100 form-control" th:field="*{password}">
<div th:errors="*{password}"></div> <!--추가-->
#fields.hasGlobalErrors()로 글로벌 에러 여부 확인 가능
ToDoDto
public class ToDoDto {
private Long id;
@NotEmpty
private String title;
@Length(max = 100, message = "최대 100자까지 가능합니다")
private String description;
private Boolean isCompleted = false;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate createdDate;
@DateTimeFormat(pattern = "yyyy-MM-dd") @FutureOrPresent(message = "오늘 이전의 날짜는 선택할 수 없습니다.")
private LocalDate dueDate;
}
ToDoController
class ToDoController {
@PostMapping("/todo/add")
public String addToDo(@Validated @ModelAttribute("toDoDto") ToDoDto toDoDto, BindingResult bindingResult, HttpServletRequest request) {
if (!bindingResult.hasErrors() && toDoDto.getTitle().length() >= 20)
bindingResult.rejectValue("title", "length", "최대 20자까지 가능합니다.");
if (bindingResult.hasErrors()) return "/todo/add";
Optional<Member> findMember = memberService.findById(getSessionMember(request).getId());
Optional<ToDo> createToDo = findMember.map(member -> ToDo.createToDo(
toDoDto.getTitle(), toDoDto.getDescription(), toDoDto.getDueDate(),
member));
createToDo.ifPresent(toDo -> toDoService.save(toDo));
return "redirect:/todo";
}
@PostMapping("/todo/update/{id}")
public String update(@PathVariable Long id, @Validated @ModelAttribute("toDoDto") ToDoDto toDoDto, BindingResult bindingResult) {
if (!bindingResult.hasErrors() && toDoDto.getTitle().length() >= 20)
bindingResult.rejectValue("title", "length", "최대 20자까지 가능합니다.");
if (bindingResult.hasErrors()) return "/todo/edit";
if (id != null)
toDoService.update(id, toDoDto.getTitle(), toDoDto.getDescription(), toDoDto.getDueDate());
return "redirect:/todo";
}
}
title의 길이 검증 로직 추가
add.html
<div class="row">
<div class="col">
<label class="form-label" th:for="*{title}">제목</label>
<input type="text" class="w-100 form-control" placeholder="최대 20자까지 가능합니다" th:field="*{title}"><br>
<div th:errors="*{title}"></div> <!--추가-->
</div>
<div class="col">
<label class="form-label" th:for="*{dueDate}">마감일</label>
<input type="date" class="w-100 form-control" th:field="*{dueDate}">
<div th:errors="*{dueDate}"></div> <!--추가-->
</div>
</div>
<label class="form-label" th:for="*{description}">설명</label>
<textarea class="w-100 form-control" placeholder="최대 100자까지 가능합니다" th:field="*{description}"></textarea>
<div th:errors="*{description}"></div> <!--추가-->
}
edit.html은 add.html과 동일
스프링 부트의 검증 기능과 타임리프를 이용하면 일반적인 검증 로직과 에러 메시지를 쉽게 처리할 수 있습니다.
다음 시간에는 생각하지도 못하고 있던 로그아웃 기능과 스프링 인터셉터를 활용해 공통 관심사인 로그인 여부 등을 체크하는 기능을 개발하겠습니다.