[Spring] 회원가입 폼 검증하기 − ② (JSP)

dondonee·2024년 5월 21일
0
post-thumbnail

회원가입 폼 검증하기 (JSP)

저번 포스팅에서 회원가입 폼 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() 메서드로 BindingResultloginName 필드 오류를 추가해준다.


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", "비밀번호가 일치하지 않습니다.");
    }
}

닉네임 중복 검증과 비밀번호 확인 검증은 프로필 수정 등에서 재사용될 수 있으므로 별도 메소드로 분리했다.



JSP 오류 메시지 표시하기


<%@ 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에는 클래스명을 딴 이름을 지정해주어야 한다. @Slf4jbindingResult를 출력해보면 클래스명의 맨 앞 글자를 소문자로 바꾼 이름으로 오류 객체가 등록되어있는 것을 확인할 수 있다(MemberSignUpForm -> memberSignUpForm).



JSP 사용자 입력값 유지하기

사용자가 입력한 값에 오류가 있어 BindingResult에 오류 내역을 담아 다시 사용자에게 폼을 보여줄 때, 사용자가 입력한 값을 그대로 유지하는 것이 좋다.


컨트롤러

// 검증 실패 => 회원가입 폼으로 이동
if (bindingResult.hasErrors()) {
    form.setPassword("");  // 비밀번호는 초기화
    model.addAttribute("form", form);  // 사용자가 입력했던 값 다시 전달
    return "signUpForm";
}

@ModelAttribute로 바인딩했던 객체를 그대로 model에 보내고, JSP에서 JSTL로 값을 꺼내 표시하면 된다. 단, 비밀번호는 보안을 위해 서버에서 값을 지워서 보낸다.


JSP 뷰

<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 addFlashAttributenewMember를 담아 회원 축하 페이지로 리다이렉트한다.


@GetMapping("/signup/celebration")
public String signUpCelebration(Model model) {

    // 회원가입 성공 후 리다이렉트된 경우
    if (model.containsAttribute("newMember")) {
        return "signUpCelebration";
    }

    // URL을 통한 접근의 경우
    return "redirect:/";
}

회원 가입을 통한 리다이렉트가 아니라 직접 URL로 접근하는 경우, 즉 modelnewMember 객체가 없는 경우는 홈으로 리다이렉트시킨다.


JSP 뷰

<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과 똑같다.

0개의 댓글