질문, 피드백 등 모든 댓글 환영합니다.
지금까지 작성한 코드를 되돌아 보던 중 HomeController에서 직접 작성한 정규식 검증 로직을 @Pattern으로 처리할 수 있음을 알았습니다. 코드를 그에 맞춰 리팩토링하고 하드코딩으로 작성한 에러 메시지들을 errors.properties로 메시지 소스를 분리하여 사용하겠습니다.
MemberDto
public class MemberDto {
@Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
@Pattern(regexp = "^[a-z0-9]*$", message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
private String loginId;
@Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
@Pattern(regexp = "^[a-z0-9]*$", message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
private String password;
private String checkPassword;
@Length(max = 20, min = 2, message = "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.")
@Pattern(regexp = "^[ㄱ-ㅎㅏ-ㅣ가-힣a-z0-9\\s]*$", message = "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.")
private String name;
}
@Pattern 어노테이션에서 정규식 검증 로직 처리
HomeController
@PostMapping("/add")
public String save(@Validated @ModelAttribute MemberDto memberDto, BindingResult bindingResult) {
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:/";
}
@Pattern을 적용하여 컨트롤러를 단순하게 변경
검증 시연 장면
@Length와 @Pattern 에서 동시에 오류가 발생하면 에러메시지가 두 번 출력됨
때문에 검증 어노테이션에서 기본 메시지를 정의하여 사용하기 보단 에러메시지를 분리해 사용
이 예제에서는 기존의 방식으로는 에러메시지가 중복으로 출력되었기에 메시지 소스를 분리하였지만 이보다 더 중요한 이유가 있습니다.
메시지가 java파일과 html 등에 하드코딩 되어 있다면 추후에 수정이 필요할 때 모든 소스코드를 뒤져가며 일일히 수정해야 하지만 메시지 소스를 분리하여 사용한다면 그 파일만 수정하면 모든 부분에 적용이 되므로 시간을 절약할 수 있고 원치않는 오타를 방지할 수 있습니다.
또한 메시지 작성시 디테일에 단계를 두어 범용적으로 사용하거나 세밀하게 사용하기 쉽습니다.
이 프로젝트는 굉장히 작기 때문에 에러 메시지만 분리하여 사용하고 메시지 작성 시 단계를 나누지 않겠습니다.
errors.properties
required.memberDto.loginId=5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.
required.memberDto.password=5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.
required.memberDto.name=2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.
duplication.memberDto=이미 존재하는 ID 입니다.
wrong.memberDto.checkPassword=비밀번호가 일치하지 않습니다.
loginFail.loginDto=id 혹은 비밀번호가 정확하지 않습니다.
required.toDoDto.title=1~20자까지 가능합니다.
required.toDoDto.description=최대 100자까지 가능합니다.
required.toDoDto.dueDate=오늘 이전의 날짜는 선택할 수 없습니다.
application.yml
spring:
messages:
basename: errors
스프링부트가 에러메시지 소스를 적용할 수 있도록 설정 추가 (default = messages)
MemberDto
public class MemberDto {
@Length(max = 20, min = 5)
@Pattern(regexp = "^[a-z0-9]*$")
private String loginId;
@Length(max = 20, min = 5)
@Pattern(regexp = "^[a-z0-9]*$")
private String password;
private String checkPassword;
@Length(max = 20, min = 2)
@Pattern(regexp = "^[ㄱ-ㅎㅏ-ㅣ가-힣a-z0-9\\s]*$")
private String name;
}
디폴트 메시지를 사용하면 th:errors에서 경우에 따라 에러 메시지가 모두 출력되므로 디폴트 메시지 제거
ToDoDto
public class ToDoDto {
private Long id;
@Length(max = 20, min = 1, message = "{required.toDoDto.title}")
private String title;
@Length(max = 100, message = "{required.toDoDto.description}")
private String description;
private Boolean isCompleted;
private LocalDateTime createdDateTime;
@DateTimeFormat(pattern = "yyyy-MM-dd") @FutureOrPresent(message = "{required.toDoDto.dueDate}")
private LocalDate dueDate;
String title 필드의 어노테이션을 @NotEmpty -> @Length 로 변경하여 한 번에 처리
HomeController
@PostMapping("/add")
public String save(@Validated @ModelAttribute MemberDto memberDto, BindingResult bindingResult) {
if (!memberDto.getPassword().equals(memberDto.getCheckPassword()))
bindingResult.rejectValue("checkPassword", "wrong");
if (bindingResult.hasErrors()) return "/member/add";
Member member = new Member(memberDto.getLoginId(), memberDto.getPassword(), memberDto.getName().strip());
if (memberService.save(member) == null) {
bindingResult.reject("duplication");
return "/member/add";
}
return "redirect:/";
}
로그인id의 중복 여부를 처리하는 부분은 에러 출력 시 loginId 필드가 겹쳐 global 에러를 처리하는 BindingResult.reject로 변경하여 처리
ToDoController
@PostMapping("/todo/add")
public String addToDo(@Validated @ModelAttribute("toDoDto") ToDoDto toDoDto, BindingResult bindingResult, HttpServletRequest request) {
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";
}
ToDoDto에서 @Length로 바꾸었기에 길이 검증 로직 제거
add.html (member/add)
<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:if="${#fields.hasErrors('name')}" th:text="#{required.memberDto.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:if="${#fields.hasErrors('loginId')}" th:text="#{required.memberDto.loginId}"></div>
<div th:if="${#fields.hasGlobalErrors()}" th:text="#{duplication.memberDto}"></div>
<label for="password" class="form-label">비밀번호</label>
<input type="password" id="password" class="w-100 form-control"
placeholder="5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다." th:field="*{password}">
<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>
#{}
문법으로 메시지 소스 조회 가능
form.html
<div th:if="${#fields.hasGlobalErrors()}" th:text="#{loginFail.loginDto}"></div>
이로써 모든 todo list 개발이 모두 끝났습니다. 다음 시간에는 프로젝트를 되돌아보고 정리하며 마치도록 하겠습니다.