/ #JAVA#SPRING

스프링 레거시(Spring legacy) - 회원가입

이번 글에서는 bean validation을 통해 회원가입을 구현하겠습니다.

회원가입폼 작성

우선 회원가입폼을 작성하겠습니다.

/src/main/webapp/WEB-INF/views/member/include/signUpFormCSS.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<style type="text/tailwindcss">
    main {@apply text-gray-600 container px-5 py-16 mx-auto}
    article {@apply lg:w-2/3 w-full mx-auto overflow-auto}
    form {@apply flex flex-col max-w-2xl mx-auto pb-2 gap-3 xl:gap-6 before:text-lg before:xl:text-2xl before:font-bold before:text-gray-600 before:text-center}
    form > label {@apply flex flex-col w-full text-sm sm:text-base}
    form > label > font {@apply before:font-medium before:text-gray-600 before:mr-4}
    form > button[type=submit] {@apply relative flex justify-center rounded-md border border-transparent bg-slate-600 py-2 px-4 mt-4 text-sm font-medium text-white 
        hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2}
    form input {@apply mt-1 w-full px-3 py-2 bg-white border border-violet-300 rounded-md text-sm shadow-sm placeholder-slate-400
        focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 disabled:bg-slate-50 disabled:text-slate-500 
        disabled:border-slate-200 disabled:shadow-none}
</style>
/src/main/webapp/WEB-INF/views/member/signUpForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
    <%@ include file="./include/signUpFormCSS.jsp" %>
    <title>게시글 목록</title>
</head>
<body>
<main>
    <article>
        <form class="before:content-['회원가입']" action="/member" method="post" onsubmit="return false">
            <input type="hidden" name="act" value="signUp" />
            <label>
                <font class="before:content-['이메일']" color="red">
                    <c:if test="${errors.hasFieldErrors('email')}">* ${errors.getMessage('email')}</c:if>
                </font>
                <c:choose>
                    <c:when test="${!errors.hasFieldErrors('email')}">
                        <c:set var="value" value="${errors.getFieldValue('email')}" />
                    </c:when>
                    <c:otherwise><c:remove var="value" /></c:otherwise>
                </c:choose>
                <input type="email" name="email" required placeholder="Email" value="${value}" />
            </label>
            <label>
                <font class="before:content-['비밀번호']" color="red">
                    <c:if test="${errors.hasFieldErrors('password')}">* ${errors.getMessage('password')}</c:if>
                </font>
                <input type="password" name="password" minlength="8" maxlength="20" required placeholder="Password" />
            </label>
            <label>
                <font class="before:content-['비밀번호_확인']" color="red">
                    <c:if test="${errors.hasFieldErrors('rePassword')}">* ${errors.getMessage('rePassword')}</c:if>
                </font>
                <input type="password" name="rePassword" minlength="8" maxlength="20" required placeholder="Password" />
            </label>
            <label>
                <font class="before:content-['이름']" color="red">
                    <c:if test="${errors.hasFieldErrors('name')}">* ${errors.getMessage('name')}</c:if>
                </font>
                <c:choose>
                    <c:when test="${!errors.hasFieldErrors('name')}">
                        <c:set var="value" value="${errors.getFieldValue('name')}" />
                    </c:when>
                    <c:otherwise><c:remove var="value" /></c:otherwise>
                </c:choose>
                <input type="text" name="name" required placeholder="Name" value="${value}" />
            </label>
            <label>
                <font class="before:content-['닉네임']" color="red">
                    <c:if test="${errors.hasFieldErrors('nickName')}">* ${errors.getMessage('nickName')}</c:if>
                </font>
                <c:choose>
                    <c:when test="${!errors.hasFieldErrors('nickName')}">
                        <c:set var="value" value="${errors.getFieldValue('nickName')}" />
                    </c:when>
                    <c:otherwise><c:remove var="value" /></c:otherwise>
                </c:choose>
                <input type="text" name="nickName" required placeholder="Nickname" value="${value}" />
            </label>
            <button type="submit" class="before:content-['회원가입']"></button>
        </form>
    </article>
</main>

<script>
document.querySelector('button[type=submit]').addEventListener('click', function() {
    if(document.querySelector('input[name=password]').value != document.querySelector('input[name=rePassword]').value) {
        return document.querySelector('input[name=rePassword]').setCustomValidity("패스워드가 일치하지 않습니다.");
    }
    document.querySelector('form').submit();
});
</script>
</body>
</html>

tailwindcss를 통해 클래스로 스타일을 작성했으며, HTML5의 폼검증 API를 통해 클라이언트에서 1차적으로 검증을 하도록 작성했습니다.

Mapper, DAO, Service 수정

이메일이 중복되었는지 확인하기 위해 MemberDAO에 getMemberByEmail 메소드를 추가합니다.

/kro/rubisco/dao/MemberDAO.java
package kro.rubisco.dao;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import kro.rubisco.dto.MemberDTO;

@Mapper
public interface MemberDAO {

    public void create(MemberDTO member) throws Exception;
    
    public MemberDTO read(Long memberId) throws Exception;
    
    public MemberDTO getMemberByEmail(String email) throws Exception;

    public void update(MemberDTO member) throws Exception;

    public void delete(Long memberId) throws Exception;

    public List<MemberDTO> listAll() throws Exception;
}

memberMapper.xml 파일에 getMemberByEmail라는 id를 가지는 select 쿼리문을 작성하여 MemberDAO의 getMemberByEmail 메소드에 매핑하도록 하겠습니다.

memberMapper.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="kro.rubisco.dao.MemberDAO">

<resultMap id="getMember" type="MemberDTO">
    <association property="group" column="group_id" select="getGroup" />
</resultMap>

<insert id="create">
insert into member (password, email, name, nick_name, group_id)
values (#{password}, #{email}, #{name}, #{nickName}, #{groupId})
</insert>

<select id="read" resultMap="getMember">
select * from member where member_id = #{memberId}
</select>

<select id="getMemberByEmail" resultMap="getMember">
select * from member where email = #{email}
</select>

<select id="getGroup" resultType="GroupDTO">
select * from member_group where group_id=#{groupId}
</select>

<update id="update">
update member 
set password=#{password},
    email=#{email}, 
    name=#{name}, 
    nick_name= #{nickName},
where member_id = #{memberId}
</update>

<delete id="delete"> delete from member where member_id = #{memberId} </delete>

<select id="listAll" resultMap="getMember">
<![CDATA[ select * from member where m.member_id > 0 order by m.member_id desc ]]>
</select>

</mapper>

MemberService 인터페이스에 email을 매개변수로 받는 read 메소드를 오버로드하여 작성합니다.

/kro/rubisco/service/MemberService.java
package kro.rubisco.service;

import java.util.List;

import kro.rubisco.dto.MemberDTO;

public interface MemberService {
    
      public void regist(MemberDTO member) throws Exception;

      public MemberDTO read(Long memberId) throws Exception;
      
      public MemberDTO read(String email) throws Exception;

      public void modify(MemberDTO member) throws Exception;

      public void remove(Long memberId) throws Exception;

      public List<MemberDTO> listAll() throws Exception;
}

해당 인터페이스의 구현체를 작성합니다.

/kro/rubisco/service/impl/MemberServiceImpl.java
package kro.rubisco.service.impl;

import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import kro.rubisco.dao.MemberDAO;
import kro.rubisco.dto.MemberDTO;
import kro.rubisco.service.MemberService;

@Service
@Transactional(readOnly = true)
public class MemberServiceImpl implements MemberService {

    private final MemberDAO memberDAO;
    
    @Autowired
    public MemberServiceImpl(SqlSession sqlSession) {
        this.memberDAO = sqlSession.getMapper(MemberDAO.class);
    }
    
    @Override
    public void regist(MemberDTO member) throws Exception {
        memberDAO.create(member);
    }

    @Override
    public MemberDTO read(Long memberId) throws Exception {
        return memberDAO.read(memberId);
    }
    
    @Override
    public MemberDTO read(String email) throws Exception {
        return memberDAO.getMemberByEmail(email);
    }

    @Override
    public void modify(MemberDTO member) throws Exception {
        memberDAO.update(member);
    }

    @Override
    public void remove(Long memberId) throws Exception {
        memberDAO.delete(memberId);
    }

    @Override
    public List<MemberDTO> listAll() throws Exception {
        return memberDAO.listAll();
    }

}

DTO, Controller 수정

1차적으로 스크립트를 통해 클라이언트에서 유효성 검사를 하게 되지만, 클라이언트에서는 스크립트 수정을 통해 서버로 부정적인 접근을 할 수 있습니다. 그렇기때문에 컨트롤러에서 2차인 유효성검사를 하게 됩니다.

bean validation을 위해 MemberDTO에 다음과 같이 어노테이션을 붙여줍니다.

/kro/rubisco/dto/MemberDTO.java
package kro.rubisco.dto;

import java.util.Date;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberDTO {
    
    private Long memberId;
    
    @NotBlank(message = "패스워드를 입력하세요.")
    @Size(min = 8, max = 20, message = "패스워드는 8글자 이상 20글자 이하로 입력하세요.")
    private String password;
    
    @NotBlank(message = "이메일을 입력하세요.")
    @Email
    private String email;
    
    @NotBlank(message = "이름을 입력하세요.")
    private String name;
    
    @NotBlank(message = "닉네임을 입력하세요.")
    private String nickName;
    
    private Long groupId;
    private GroupDTO group;
    private Date createDate;
    private Date lastLogin;
}

마지막으로 MemberController의 insertMember 메소드를 수정해주세요.

/kro/rubisco/controller/MemberController.java
package kro.rubisco.controller;

import java.util.Locale;

import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kro.rubisco.config.BindExceptionWithViewName;
import kro.rubisco.dto.MemberDTO;
import kro.rubisco.service.MemberService;
import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {

    private final MessageSource messageSource;
    private final MemberService memberService;
    
    @GetMapping()
    public String getMemberInfo() {
        return "member/getMemberInfo";
    }
    
    @GetMapping(params="act=signUp")
    public String getSignUpView() throws Exception {
        return "member/signUpForm";
    }
    
    @PostMapping()
    public String insertMember(
        @Validated @ModelAttribute("member") MemberDTO member, 
        BindingResult bindingResult, 
        @RequestParam("rePassword") String rePassword,
        Locale locale
    ) throws Exception {
        
        if(!member.getPassword().equals(rePassword)) {
            bindingResult.addError(new FieldError("java.lang.String", "rePassword", "패스워드가 일치하지 않습니다."));
        }

        if(memberService.read(member.getEmail()) != null) {
            bindingResult.rejectValue("email", "overlap.email", "동일한 이메일이 존재합니다.");
        }
        
        if(bindingResult.hasErrors()) {
            throw new BindExceptionWithViewName(bindingResult, "member/signUpForm", messageSource, locale);
        }
        
        member.setGroupId(1L);
        memberService.regist(member);
        
        return "redirect:/";
    }
    
    @PatchMapping()
    public String updateMember(MemberDTO member) throws Exception {
        memberService.modify(member);
        return "redirect:/member";
    }
    
    @DeleteMapping()
    public String deleteMember() throws Exception {
        return "redirect:/";
    }
}

검증기를 따로 작성하지 않고 메소드 자체에 패스워드와 이메일 중복 검증 로직을 작성했습니다. 아직 그룹을 생성하는 폼이 없으므로, 이전에 DB에 입력한 Admin 그룹의 groupId인 1L을 주입하여 회원가입이 되도록 했습니다.

아래 화면은 이메일을 중복하여 가입했을 경우 나타나는 화면입니다.

img34