ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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 값이 출력되는 걸 확인할 수 있다. 

     

    db

     

     

     

     


    [참고] https://dev-coco.tistory.com/125

     

    반응형

    댓글

Designed by Tistory.