-
[Spring Boot] 2-7. Validation 회원가입 중복 체크프로젝트/도서 관리 시스템 2023. 7. 17. 14:29
validation 어노테이션으로는 단일 필드에 대한 유효성 검증만 처리가 가능하기 때문에, 중복체크 같은 경우는 validation 어노테이션으로 해결이 불가능하다. 이전 포스팅에서는 js로 중복 검사를 하였는데 이번엔 validation을 커스터마이징하는 방법을 사용하려고 한다.
📑 1. Mapper
회원가입 시 사용자가 입력한 이메일 또는 닉네임에 해당하는 회원 정보가 db에 있을 경우 boolean 타입으로 true를 반환한다. 즉) false를 반환되어야 회원 가입이 가능한 것이다.
<UserMapper.xml>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.wish.library.security.mapper.UserMapper"> <select id="getOneByEmail" resultType="boolean"> select count(*) from members where email = #{email} </select> <select id="getOneByNickname" resultType="boolean"> select count(*) from members where nickname = #{nickname} </select> </mapper>
<UserMapper.java>
package com.wish.library.security.mapper; import com.wish.library.security.domain.MemberSaveForm; import com.wish.library.security.domain.UserDTO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper { /*중복 검사, 이메일 조회*/ boolean getOneByEmail(String email); /*중복 검사, 닉네임 조회*/ boolean getOneByNickname(String nickname); }
📑 2. Validator 구현한 AbstractValidator 생성
<AbstractValidator.java>
package com.wish.library.security.controller.validation; import jdk.jshell.ErroneousSnippet; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @Slf4j public abstract class AbstractValidator<T> implements Validator { @Override public boolean supports(Class<?> clazz) { return true; } @SuppressWarnings("unchecked") @Override public void validate(Object target, Errors errors) { try{ doValidate((T) target, errors); }catch(RuntimeException e){ log.error("중복 검증 에러", e); throw e; } } protected abstract void doValidate(final T dto, final Errors errors); }
validate를 구현하고 검증로직이 들어갈 부분을 doValidate로 따로 빼주었다.
@SupperssWarnings("unchecked")는 컴파일러에서 경고하지 않도록 하기 위해 사용했다.
validate를 구현한 AbstractValidator를 구현하는 이메일 체크, 닉네임 체크 클래스를 생성한다.
<CheckEmailValidator.java>
package com.wish.library.security.controller.validation; import com.wish.library.security.domain.MemberSaveForm; import com.wish.library.security.service.MemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; @Component @Slf4j @RequiredArgsConstructor public class CheckEmailValidator extends AbstractValidator<MemberSaveForm> { private final MemberService memberService; @Override protected void doValidate(MemberSaveForm dto, Errors errors) { log.info("이메일 중복체크 중"); if(memberService.checkEmailDuplication(dto.getEmail())) { errors.rejectValue("email", "이메일 중복 오류", "이미 사용중인 이메일입니다."); } } }
errors에 rejectValue 메서드로 에러 메세지를 전달한다.
각각 파라미터는 field, errorCode, defaultMessegae를 나타낸다.
<CheckNicknameValidator.java>
package com.wish.library.security.controller.validation; import com.wish.library.security.domain.MemberSaveForm; import com.wish.library.security.service.MemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; @Component @Slf4j @RequiredArgsConstructor public class CheckNicknameValidator extends AbstractValidator<MemberSaveForm> { private final MemberService memberService; @Override protected void doValidate(MemberSaveForm dto, Errors errors) { log.info("닉네임 중복체크 중"); if(memberService.checkNicknameDuplication(dto.getNickname())) { errors.rejectValue("nickname", "닉네임 중복 오류", "이미 사용중인 닉네임입니다."); } } }
📑 Controller
package com.wish.library.security.controller; import com.wish.library.security.controller.validation.CheckEmailValidator; import com.wish.library.security.controller.validation.CheckNicknameValidator; import com.wish.library.security.domain.MemberSaveForm; import com.wish.library.security.domain.UserDTO; import com.wish.library.security.service.CustomUserService; import com.wish.library.security.service.MemberService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import java.util.Map; /** * GET 방식, 화면 조회시 */ @Controller @RequiredArgsConstructor @Slf4j public class UserController { private final CustomUserService customUserService; private final MemberService memberService; private final CheckEmailValidator checkEmailValidator; private final CheckNicknameValidator checkNicknameValidator; /*커스텀 유효성 검증을 위해 추가*/ @InitBinder public void validatorBinder(WebDataBinder binder){ binder.addValidators(checkEmailValidator); binder.addValidators(checkNicknameValidator); } @GetMapping("/join") public String join(){ return "join"; } @PostMapping("/join") public String join(@Validated @ModelAttribute("memberSaveForm") MemberSaveForm memberSaveForm , BindingResult bindingResult , RedirectAttributes rttr ,Model model) { log.info("=================join Controller"); if(bindingResult.hasErrors()){ model.addAttribute("memberDto", memberSaveForm); log.info("errors={}", bindingResult); /*중복 검사 통과 못한 메시지를 핸들링*/ /* Map<String, String> validatorResult = memberService.validateHandling(bindingResult); //keySet() : Map의 key값을 가져올때 사용. for(String key:validatorResult.keySet()){ model.addAttribute(key, validatorResult.get(key)); }*/ return "join"; } boolean saveResult = memberService.save(memberSaveForm); if(saveResult){ rttr.addFlashAttribute("result", "회원 가입 완료"); return "redirect:/login"; } return "join"; } }
Validator 사용을 위해 @InitBinder이 붙은 WebDataBinder를 인자로 받는 메서드를 작성해주었다.
📌 @InitBinder는 특정 컨트롤러에서 바인딩 또는 검증 설정을 변경하고 싶을때 사용한다. 즉) @Valid 어노테이션으로 검증이 필요한 객체를 가져오기 전에 수행할 메서드를 지정해주는 어노테이션이다.
📌 WebDataBinder는 HTTP 요청 정보를 컨트롤러 메서드의 파라미터나 모델에 바인딩할 때 사용되는 바인딩 객체이다. addValidators를 통해 새로운 유효성을 정의한 객체를 binder에 추가해주는 작업이다.
화면단에서 에러 메세지를 출력하기 위해 따로 validateHandling 메서드를 서비스 단에서 만들었는데, 사용할 필요가 없는 듯 하다. 이미 <form:errors path="">로 에러 값들을 출력하고 있기에 메시지가 중복된다.
📑 에러 출력 화면 (jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <!doctype html> <html lang="en" data-bs-theme="auto"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors"> <meta name="generator" content="Hugo 0.111.3"> <meta name="_csrf" content="${_csrf.token}"/> <meta name="_csrf_header" content="${_csrf.headerName}"/> <title>회원가입</title> <link rel="canonical" href="https://getbootstrap.com/docs/5.3/examples/sign-in/"> <link href="assets/dist/css/sign-in.css" rel="stylesheet"> <jsp:include page="./includes/common_includes.jsp"></jsp:include> <%-- <script type="text/javascript" src="assets/dist/js/commonAjax.js"></script> <script type="text/javascript" src="assets/dist/js/member.js"></script>--%> <style> @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; } } .nav-scroller .nav { display: flex; flex-wrap: nowrap; padding-bottom: 1rem; margin-top: -1px; overflow-x: auto; text-align: center; white-space: nowrap; -webkit-overflow-scrolling: touch; } </style> </head> <script> </script> <body class="text-center"> <main class="form-signin w-100 m-auto"> <form:form name="join_form" action="/join" method="post" modelAttribute="memberSaveForm"> <%--<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />--%> <img class="mb-4" src="assets/brand/bootstrap-logo.svg" alt="" width="72" height="57" onclick="location.href='/'"> <h1 class="h3 mb-3 fw-normal">Please join in</h1> <div class="form-floating"> <input type="email" class="form-control" name="email" id="email" value="${memberDto.email}" placeholder="name@example.com"> <label for="email">Email address</label><%--<button class="btn btn-light rounded-pill px-3" type="button" name="btn" id="btnEmailCheck">Duplicate check</button>--%> <%--<span id="id-check"></span>--%> <span class="text-danger"><%--${valid_email}--%><form:errors path="email" /></span> </div> <div class="form-floating"> <input type="password" class="form-control" name="password" id="password" placeholder="Password" autoComplete="off"> <label for="password">Password</label> <span class="text-danger"><form:errors path="password" /></span> </div> <%-- <div class="form-floating"> <input type="password" class="form-control" name="passwordConfirm" id="passwordConfirm" placeholder="passwordConfirm" autoComplete="off"> <label for="passwordConfirm">passwordConfirm</label> <form:errors path="passwordConfirm" /><br> </div>--%> <div class="form-floating"> <input type="text" class="form-control" name="name" id="name" value="${memberDto.name}" placeholder="name"> <label for="name">name</label> <span class="text-danger"><form:errors path="name" /></span> </div> <div class="form-floating"> <input type="text" class="form-control" name="nickname" id="nickname" value="${memberDto.nickname}" placeholder="nickname"> <label for="nickname">nickname</label> <%--<button class="btn btn-light rounded-pill px-3" type="button" name="btn" id="btnNicknameCheck">Duplicate check</button>--%> <%--<span id="nickname-check"></span>--%> <span class="text-danger"><%--${valid_nickname}--%> <form:errors path="nickname" /></span> </div> <div class="form-floating"> <div class="form-check"> <input id="men" name="mfCode" value="MEN" type="radio" class="form-check-input" checked> <label class="form-check-label" for="men">Men</label> </div> <div class="form-check"> <input id="female" name="mfCode" value="FEMALE" type="radio" class="form-check-input"> <label class="form-check-label" for="female">Female</label> </div> </div> <div class="form-floating"> <input type="text" class="form-control" name="cellNo" id="cellNo" value="${memberDto.cellNo}" placeholder="cellNo"> <label for="cellNo">cellNo</label> <span class="text-danger"><form:errors path="cellNo" /></span> </div> <button class="w-50 btn btn-lg btn-dark" type="button" onclick="history.back();" >이전페이지로가기</button> <%--<button class="w-50 btn btn-lg btn-primary" type="button" name="btn" id="btnSingUp" >Sign up</button>--%> <button type="submit" class="w-50 btn btn-lg btn-primary">Sign up</button> </form:form> </main> </body> </html>
이미 db에 저장되어 있는 이메일과 닉네임 값을 넣어봤다. CheckEmailValidator와 CheckNicknameValidator에서 지정해둔 defaultMessage 값이 출력되는 걸 확인할 수 있다.
[참고] https://dev-coco.tistory.com/125
반응형'프로젝트 > 도서 관리 시스템' 카테고리의 다른 글
[Spring Boot] 2-6. Validation을 이용한 회원가입 구현 (jsp) (0) 2023.07.05 [Spring Boot] 2-5. 스프링 시큐리티, 로그인 실패시 메시지 출력 (0) 2023.07.05 [Spring Boot] 2-4. 스프링 시큐리티, 회원 가입 구현하기 (0) 2023.07.05 [Spring Boot] 2-3. 스프링 시큐리티, 로그인/로그아웃 구현 (0) 2023.07.05 [Spring Boot] 2-2. 세션을 사용하는 회원 가입, 회원 정보 수정 (1) 2023.06.19