ep07. 회원가입 기능 구현 - 중복검사
화면 구현과 유효성검사를 마쳤으니 이제 회원가입 기능을 구현해보고자 한다. SpringSecurity와 JWT 설정, 패스워드 암호화, 실명인증, 중복검사 등 해야 할 게 많다...... SpringSecurity + JWT 적용은 다른 기능들을 적용시키고 마지막에 진행해보려 한다. 이번 포스팅에서는 회원가입 기능 구현 그리고 계정과 닉네임에 대한 중복검사를 먼저 진행해 보겠다.
MVC 패턴 적용
유저가 정보를 입력하고 회원가입 버튼을 클릭하면 그 정보가 DB에 저장되는 것을 가장 먼저 구현해 보자. 아키텍쳐는 MVC 패턴을 선택하였고 흐름은 아래와 같다(draw.io를 이용해서 작성해 보았다). 클라이언트의 요청(request)이 뷰로 들어오면 컨트롤러와 모델을 거쳐 데이터베이스에 값을 저장하거나 조회해 와서 다시 응답(response)을 주는 방식이다. 이제 코드를 작성해 보자.

1. 회원정보 작성 유도
유효성 검사를 진행하는 JavaScript 파일에 전역변수를 선언하고 기본값을 false로 초기화했다. 이후 유효성검사가 통과되면 전역변수의 값을 true로 변경시켜 주는 로직을 작성했다. 이와 같이 작성하여 유저가 정보를 입력하지 않았을 때 회원가입이 진행되지 않게 설정했다.
<button type="button" th:onclick="register()">회원가입</button>
let nameFlag = false;
function idValidation() {
// 유저가 입력한 값
const value = document.getElementById("userId").value;
// 검사 결과를 나타낸 영역
const showText = document.getElementById("showId");
// 정규식
const checkId = /^(?=.*[a-z])[A-Za-z\d]{5,}$/;
if(!checkId.test(value)){
failedValidation(showText, "알파벳 대,소문자와 숫자를 조합하여 5자리 이상 작성해주세요.");
} else {
successValidation(showText);
idFlag = true;
}
}
// 회원가입
function register() {
if(nameFlag && birthDateFlag && idFlag && passwordFlag && rePasswordFlag && nickNameFlag && phoneFlag && postCodeFlag && detailAddressFlag ){
// 회원가입 로직 작성 필요
} else {
swal("회원정보를 모두 입력해주세요.", "", "error");
}
}

2. 회원가입 비즈니스 로직
2-1. ajax를 이용하여 Contoller 호출
// 회원가입
function register() {
const userName = document.getElementById("userName").value;
const userBirthDate = document.getElementById("birthDate").value;
const userId = document.getElementById("userId").value;
const userPassword = document.getElementById("password").value;
const userNickName = document.getElementById("nickName").value;
const userPhone = document.getElementById("phone").value;
const userPostCode = document.getElementById("postCode").value;
const userAddress = document.getElementById("address").value;
const userDetailAddress = document.getElementById("detailAddress").value;
const userExtraAddress = document.getElementById("extraAddress").value;
if(nameFlag && birthDateFlag && idFlag && passwordFlag && rePasswordFlag && nickNameFlag && phoneFlag && postCodeFlag && detailAddressFlag ){
$.ajax({
type: "POST",
url: "/register",
data: {
userName : userName,
userBirthDate : userBirthDate,
userId : userId,
userPassword : userPassword,
userNickName : userNickName,
userPhone : userPhone,
userPostCode : userPostCode,
userAddress : userAddress,
userDetailAddress : userDetailAddress,
userExtraAddress : userExtraAddress,
},
success: function success(res) {
if(res === "성공") {
swal("회원가입 성공", "반갑습니다." , "success");
} else {
swal("회원가입 실패", "입력하신 정보를 확인해주세요.", "error");
}
},
error: function error(err) {
}
});
} else {
swal("회원정보를 모두 입력해주세요.", "", "error");
}
}
2-2. Controller 작성
Spring은 ajax에서 전송한 데이터를 userDTO 변수에 자동으로 저장해 준다. 정말 편리하다.. 최고다.. Controller는 서비스를 호출하고 서비스에서 실행되는 결과를 result라는 변수에 초기화한다. 그리고 조건문을 통해 ajax에게 '성공', '실패' 둘 중 하나의 값을 다시 전송한다.
* @ResponseBody: Thymeleaf를 뷰 템플릿으로 적용시킨 상태라 ResponseBody 어노테이션이 없다면 return값을 화면의 경로로 인식하기 때문에 사용해준다.
package kr.co.vibevillage.user.controller;
import kr.co.vibevillage.user.dto.UserDTO;
import kr.co.vibevillage.user.service.RegisterServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor // 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대한 생성자를 만들어줌
public class RegisterController {
// 서비스 객체 생성
private final RegisterServiceImpl registerService;
// 회원가입
@PostMapping("/register")
@ResponseBody
public String register(UserDTO userDTO){
int result = registerService.register(userDTO);
if(result == 1){
return "성공";
} else {
return "실패";
}
}
}
2-3. Service 작성
package kr.co.vibevillage.user.service;
import kr.co.vibevillage.user.dto.UserDTO;
public interface RegisterService {
// 회원가입
public int register(UserDTO userDTO);
}
package kr.co.vibevillage.user.service;
import kr.co.vibevillage.user.dto.UserDTO;
import kr.co.vibevillage.user.mapper.RegisterMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor // 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대한 생성자를 만들어줌
public class RegisterServiceImpl implements RegisterService{
// Mapper 객체 생성
private final RegisterMapper registerMapper;
// 회원가입
@Override
public int register(UserDTO userDTO) {
return registerMapper.register(userDTO);
}
}
2-4. Mapper 작성
package kr.co.vibevillage.user.mapper;
import kr.co.vibevillage.user.dto.UserDTO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RegisterMapper {
// 회원가입
public int register(UserDTO userDTO);
}
<?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="kr.co.vibevillage.user.mapper.RegisterMapper">
<insert id="register" parameterType="kr.co.vibevillage.user.dto.UserDTO">
INSERT INTO USER_VIBEVILLAGE VALUES (USER_VIBEVILLAGE_SEQ.nextval,
#{userName},
#{userId},
#{userPassword},
#{userNickName},
#{userPhone},
#{userPostCode},
#{userAddress},
#{userDetailAddress},
#{userExtraAddress},
default,
#{userBirthDate},
default,
default
)
</insert>
</mapper>
3. 회원가입 테스트
정보를 입력하고 회원가입 버튼을 눌렀을 때 DB에 저장되는지 확인해 보자.



3. 계정, 닉네임 중복검사
테이블 생성할 때 계정, 닉네임 컬럼에 unique 제약조건을 걸어두었기 때문에 정보를 입력할 때 중복검사를 할 수 있도록 코드를 작성해 보겠다. 유저가 입력한 값을 가져와 데이터베이스에 조회를 하고 그 결과에 따라 Alert창을 띄워준다.
3-1. ajax를 이용하여 Controller 호출
// 계정 중복검사
function checkId() {
const userId = document.getElementById("userId").value;
if(userId === "") {
swal("계정을 입력해주세요.", "", "error");
} else {
$.ajax({
type: "GET",
url: "/checkId",
data: {
userId : userId,
},
success: function success(res){
if(res === "중복") {
swal("이미 사용중인 계정입니다.", "", "error")
} else if (res === "가능") {
swal("사용 가능한 계정입니다.", "", "success")
}
},
error: function error(err) {
}
});
}
}
// 닉네임 중복검사
function checkNickName() {
const nickName = document.getElementById("nickName").value;
if(nickName === ""){
swal("닉네임을 입력해주세요.", "", "error");
} else {
$.ajax({
type: "GET",
url: "/checkNickName",
data: {
userNickName : nickName,
},
success: function success(res){
if(res === "중복") {
swal("이미 사용중인 닉네임입니다.", "", "error")
} else if (res === "가능") {
swal("사용 가능한 닉네임입니다.", "", "success")
}
},
error: function error(err) {
}
});
}
}
3-2. Controller 작성
// 계정 중복검사
function checkId() {
const userId = document.getElementById("userId").value;
if(userId === "") {
swal("계정을 입력해주세요.", "", "error");
} else {
$.ajax({
type: "GET",
url: "/checkId",
data: {
userId : userId,
},
success: function success(res){
if(res === "중복") {
swal("이미 사용중인 계정입니다.", "", "error")
} else if (res === "가능") {
swal("사용 가능한 계정입니다.", "", "success")
}
},
error: function error(err) {
}
});
}
}
// 닉네임 중복검사
function checkNickName() {
const nickName = document.getElementById("nickName").value;
if(nickName === ""){
swal("닉네임을 입력해주세요.", "", "error");
} else {
$.ajax({
type: "GET",
url: "/checkNickName",
data: {
userNickName : nickName,
},
success: function success(res){
if(res === "중복") {
swal("이미 사용중인 닉네임입니다.", "", "error")
} else if (res === "가능") {
swal("사용 가능한 닉네임입니다.", "", "success")
}
},
error: function error(err) {
}
});
}
}
3-3. Service 작성
package kr.co.vibevillage.user.service;
import kr.co.vibevillage.user.dto.UserDTO;
public interface RegisterService {
public int register(UserDTO userDTO);
// 닉네임 중복검사
public int checkNickName(String userNickName);
// 계정 중복검사
public int checkId(String userId);
}
package kr.co.vibevillage.user.service;
import kr.co.vibevillage.user.dto.UserDTO;
import kr.co.vibevillage.user.mapper.RegisterMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RegisterServiceImpl implements RegisterService{
private final RegisterMapper registerMapper;
@Override
public int register(UserDTO userDTO) {
return registerMapper.register(userDTO);
}
// 닉네임 중복검사
@Override
public int checkNickName(String userNickName){
return registerMapper.checkNickName(userNickName);
}
// 계정 중복검사
@Override
public int checkId(String userId) {
return registerMapper.checkId(userId);
}
}
3-4. Mapper 작성
package kr.co.vibevillage.user.mapper;
import kr.co.vibevillage.user.dto.UserDTO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RegisterMapper {
public int register(UserDTO userDTO);
// 닉네임 중복검사
public int checkNickName(String userNickName);
// 계정 중복검사
public int checkId(String userId);
}
<?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="kr.co.vibevillage.user.mapper.RegisterMapper">
<insert id="register" parameterType="kr.co.vibevillage.user.dto.UserDTO">
INSERT INTO USER_VIBEVILLAGE VALUES (USER_VIBEVILLAGE_SEQ.nextval,
#{userName},
#{userId},
#{userPassword},
#{userNickName},
#{userPhone},
#{userPostCode},
#{userAddress},
#{userDetailAddress},
#{userExtraAddress},
default,
#{userBirthDate},
default,
default
)
</insert>
<select id="checkNickName">
SELECT COUNT(U_NICKNAME) FROM USER_VIBEVILLAGE WHERE U_NICKNAME = #{userNickName}
</select>
<select id="checkId">
SELECT COUNT(U_ID) FROM USER_VIBEVILLAGE WHERE U_ID = #{userId}
</select>
</mapper>
코드는 다 작성했으니.. 아까 회원가입했던 정보를 다시 입력하고 중복검사 테스트를 진행한다.



다음 포스팅에는 bcrypt를 이용한 패스워드 암호화, 실명인증에 대하여 작성해 보겠다.