저번 포스팅에서 회원가입 폼 DTO 객체인 MemberSignUpForm
을 분리하고 Java Bean Validation을 사용하여 컨트롤러 파라미터로 폼 데이터가 바인딩되는 시점에 객체를 검증하도록 했다.
이번에는 아이디와 닉네임 중복 검사를 추가하고, 사용자가 입력한 데이터에 문제가 있는 겅우 BindingResult
객체에 오류 내용을 Model
에 담아 JSP에 표시하도록 했다. 또한 완전한 데이터가 들어오는 경우 데이터베이스에 회원을 저장하고 축하 페이지를 보여주도록 기능을 완성했다.
@PostMapping("/signup")
public String signUp(@Validated @ModelAttribute MemberSignUpForm form, BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes) {
// 아이디 중복 검증 (아이디는 가입 후 수정 불가하므로 메서드 분리 X)
String loginName = form.getLoginName();
if (memberService.isDuplicatedLoginName(loginName)) {
bindingResult.rejectValue("loginName", "duplicated", "이미 사용중인 아이디입니다.");
}
// 닉네임 중복 검증
validateUniqueNickname(form.getNickname(), bindingResult);
// 비밀번호 확인 검증
validatePasswordCheck(form.getPassword(), form.getPasswordCheck(), bindingResult);
// 검증 실패 => 회원가입 폼으로 이동
if (bindingResult.hasErrors()) {
form.setPassword(""); // 비밀번호는 초기화
model.addAttribute("form", form); // 사용자가 입력했던 값 다시 전달
return "signUpForm";
}
// 검증 통과 => 회원가입 로직
MemberLogin memberLogin = new MemberLogin();
memberLogin.setLoginName(form.getLoginName());
memberLogin.setPassword(form.getPassword());
Member member = new Member();
member.setNickname(form.getNickname());
member.setGrade(form.getGrade());
member.setRegion(form.getRegion());
member.setTransferred(form.getTransferred());
Member newMember = memberService.createMember(memberLogin, member);
// 회원가입 로직 성공
if (newMember != null) {
redirectAttributes.addFlashAttribute("newMember", newMember);
return "redirect:/signup/celebration";
}
return "redirect:/";
}
String loginName = form.getLoginName();
if (memberService.isDuplicatedLoginName(loginName)) {
bindingResult.rejectValue("loginName", "duplicated", "이미 사용중인 아이디입니다.");
}
사용자가 입력한 아이디(loginName
) 값이 @Validated
를 통과한 값일 경우 중복 검사를 진행한다. 아이디는 가입 후 변경 불가능하므로 아이디 중복검사는 회원가입 시에만 필요하기 때문에 별도 메서드로 분리하지 않았다.
public boolean isDuplicatedLoginName(String loginName) {
MemberLogin memberLogin = memberRepository.selectUserByLoginName(loginName);
if (memberLogin == null) {
return false;
}
return true;
}
isDuplicatedLoginName()
는 '아이디가 중복되었는가?'를 알려주는 서비스 계층의 메서드이다. 중복이라면 true
, 중복이 아니라면 false
를 반환한다. 중복이라면 검증 오류이기 때문에 rejectValue()
메서드로 BindingResult
에 loginName
필드 오류를 추가해준다.
private void validateUniqueNickname(String nickname, BindingResult bindingResult) {
if (memberService.isDuplicatedNickname(nickname)) {
bindingResult.rejectValue("nickname", "duplicated", "이미 사용중인 닉네임입니다.");
}
}
private void validatePasswordCheck(String password, String passwordCheck, BindingResult bindingResult) {
if (!passwordCheck.equals(password)) {
bindingResult.rejectValue("passwordCheck", "passwordCheck", "비밀번호가 일치하지 않습니다.");
}
}
닉네임 중복 검증과 비밀번호 확인 검증은 프로필 수정 등에서 재사용될 수 있으므로 별도 메소드로 분리했다.
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
BindingResult
오류 메세지를 표현하기 위해 JSTL의 form
태그를 사용하는 방법도 있지만, 태그가 제한적이라 UI를 세밀하게 적용할 수 없어 불편했다.
그보다는 spring
태그가 훨씬 편했다. 위와 같이 JSP 페이지에 spring
태그 라이브러리를 가져와준다.
<spring:hasBindErrors name="memberSignUpForm">
<c:if test="${not empty errors.getFieldError('loginName')}">
<div class="mt-1 x-field-error">
<i class="bi bi-exclamation-circle"></i>
<span><spring:message message="${errors.getFieldError('loginName')}"/></span>
</div>
</c:if>
</spring:hasBindErrors>
사용 예시는 위와 같다. 주의할 부분은 <spring:hasBindErrors>
의 name
속성값이다.
나는 컨트롤러에서 @ModelAttribute
바인딩할 때도 "form"이라고 이름을 주었고 Model
에 객체를 담을 때 "form"으로 보냈기 때문에 <spring:hasBindErrors name>
속성값도 "form"으로 지정했는데 오류메세지가 뜨지 않아서 한참을 헤맸다.
bindingResult = org.springframework.validation.BeanPropertyBindingResult: 6 errors
Field error in object 'memberSignUpForm' on field 'grade'
name
에는 클래스명을 딴 이름을 지정해주어야 한다. @Slf4j
로 bindingResult
를 출력해보면 클래스명의 맨 앞 글자를 소문자로 바꾼 이름으로 오류 객체가 등록되어있는 것을 확인할 수 있다(MemberSignUpForm
-> memberSignUpForm
).
사용자가 입력한 값에 오류가 있어 BindingResult
에 오류 내역을 담아 다시 사용자에게 폼을 보여줄 때, 사용자가 입력한 값을 그대로 유지하는 것이 좋다.
// 검증 실패 => 회원가입 폼으로 이동
if (bindingResult.hasErrors()) {
form.setPassword(""); // 비밀번호는 초기화
model.addAttribute("form", form); // 사용자가 입력했던 값 다시 전달
return "signUpForm";
}
@ModelAttribute
로 바인딩했던 객체를 그대로 model
에 보내고, JSP에서 JSTL로 값을 꺼내 표시하면 된다. 단, 비밀번호는 보안을 위해 서버에서 값을 지워서 보낸다.
<input type="text" class="form-control" name="loginName" id="loginName"
placeholder="4~15자 이내로 입력해주세요" value="${form.loginName}">
일반 <input>
의 경우 JSTL 태그로 form
객체의 값을 꺼내서 value
에 넣어주면 된다.
<div>
<label class="form-label">지역</label>
<select class="form-select form-select-md" name="region" id="region">
<option selected disabled>지역</option>
<c:forEach var="region" items="${regions}">
<option value="${region}" <c:if
test="${form.region eq region}"> selected</c:if>>${region.description}</option>
</c:forEach>
</select>
</div>
셀렉트 박스의 <option>
의 경우 해당하는 값이 체크된 상태로 보이도록 selected
값을 주어야 하기 때문에 <c:if>
를 사용한다.
<label class="form-label">편입여부</label>
<div class="x-form-check-group">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="transferred" id="transferredY"
value="true" <c:if test="${form.transferred eq true}"> checked</c:if>>
<label class="form-check-label" for="transferredY">예</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="transferred" id="transferredN"
value="false" <c:if test="${form.transferred eq false}"> checked</c:if>>
<label class="form-check-label" for="transferredN">아니오</label>
</div>
</div>
라디오 버튼의 경우는 셀렉트 박스와 비슷하지만 <c:if>
를 사용해 checked
속성을 준다.
@PostMapping("/signup")
public String signUp(@Validated @ModelAttribute MemberSignUpForm form, BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes) {
Member newMember = memberService.createMember(memberLogin, member);
// 회원가입 로직 성공
if (newMember != null) {
redirectAttributes.addFlashAttribute("newMember", newMember);
return "redirect:/signup/celebration";
}
}
회원가입이 성공한 경우 newMember
에 값이 들어온다. 가입한 회원의 닉네임을 표시할 수 있도록 RedirectAttributes
addFlashAttribute
에 newMember
를 담아 회원 축하 페이지로 리다이렉트한다.
@GetMapping("/signup/celebration")
public String signUpCelebration(Model model) {
// 회원가입 성공 후 리다이렉트된 경우
if (model.containsAttribute("newMember")) {
return "signUpCelebration";
}
// URL을 통한 접근의 경우
return "redirect:/";
}
회원 가입을 통한 리다이렉트가 아니라 직접 URL로 접근하는 경우, 즉 model
에 newMember
객체가 없는 경우는 홈으로 리다이렉트시킨다.
<head>
<meta http-equiv="Refresh" content="3; /">
<title>Bangcom - 가입을 환영합니다.</title>
</head>
3초 후 홈으로 자동 리다이렉트 되도록 <meta>
를 설정해주었다.
<div class="fs-2 text-center">
<p class="mb-0 x-font-bold">가입을 환영합니다!</p>
<p class="x-font-bold"><span class="text-primary">${newMember.nickname}</span>님</p>
</div>
FlashAttribute
에서 값을 꺼내는 것은 Model
과 똑같다.