프로젝트/도서 관리 시스템

[Spring Boot] 2-7. Validation 회원가입 중복 체크

s워니얌 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

 

반응형