오늘은 회원가입 기능을 구현해보도록 하겠습니다
회원가입시에 고려해야할 부분을 생각해보면 일단 사용자로부터
입력받은 데이터에 대해 유효성 검사를 시행해야합니다
사용자가 아이디와 패스워드 같은 부분에 아무것도 입력하지 않거나
이메일부분에 올바른 형식으로 입력하지 않는 경우 이에 대해 유효성 검사를 시행하고
사용자에게 어느 부분을 잘못 입력했는지에 대해 알려줘야 합니다
위에 대한 부분은 이전 포스팅에서 추가한 validation 라이브러리를 이용할것이고
또한 사용자가 이미 존재하는 아이디를 입력했을 경우에 대해서도 고려해야 합니다
이경우는 validation을 통해 검증이 불가능하므로 사용자가 입력한 아이디로
DB를 검색해 이미 존재하는 경우 사용자에게 중복된 아이디라고 알려줄것입니다
일단 회원가입 기능을 구현하기 위해 오라클DB에 유저 정보를 저장할 테이블을
하나 생성해주도록 합시다. ID값은 시퀀스를 통해 자동생성하도록 해줍니다
CREATE TABLE DL_USER (
ID NUMBER PRIMARY KEY,
USERNAME VARCHAR2(100) NOT NULL,
PASSWORD VARCHAR2(100) NOT NULL,
EMAIL VARCHAR2(50) ,
NICKNAME VARCHAR2(50),
POINT NUMBER DEFAULT 0,
PHONE VARCHAR2(20) ,
RATING VARCHAR2(50) DEFAULT 0,
ROLE VARCHAR2(20) DEFAULT 'ROLE_USER'
);
-- 시퀀스(USER테이블의 ID 자동생성)
CREATE SEQUENCE USER_ID_SEQ
INCREMENT BY 1 //증가값
START WITH 1 //시작값
MINVALUE 1 //최솟값
MAXVALUE 99999999999 //최댓값
NOCYCLE // 최댓값 도달시 시작값 부터 다시 반복
NOCACHE //CACHE를 사용할지 여부
NOORDER; // 요청 순서대로 값을 생성할지 여부
이제 회원가입 페이지를 만들어주기 위해 views에 auth폴더를 추가하고
signup.jsp를 생성하여 밑의 코드를 넣어주도록 합시다
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="/WEB-INF/views/layout/link.jsp" %>
<link rel="stylesheet" href="/css/auth/signin.css">
</head>
<body>
<main>
<div class="login_box">
<a href="/"><img src="/img/bamin2.png" alt="이미지" class="bm_img"></a>
<form action="/auth/signup" method="post" >
<div class="input_aera">
<input type="text" name="username" class="username" maxlength="20" placeholder="아이디를 입력해 주세요" value="${signupDto.username}" >
<span class="msg_box">${valid_username}</span>
</div>
<div class="input_aera">
<input type="password" class="password1" name="password" maxlength="20" placeholder="비밀번호를 입력해 주세요" value="${signupDto.password}">
</div>
<div class="input_aera">
<input type="password" class="password2" maxlength="20" placeholder="비밀번호를 한번더 입력해 주세요" value="${signupDto.password}">
<span class="msg_box">${valid_password}</span>
</div>
<div class="input_aera">
<input type="text" name="email" class="email" placeholder="이메일을 입력해 주세요" value="${signupDto.email}" >
<span class="msg_box">${valid_email}</span>
</div>
<div class="input_aera">
<input type="text" class="nickname" name="nickname" maxlength="20" placeholder="사용하실 닉네임을 입력해 주세요" value="${signupDto.nickname}">
<span class="msg_box">${valid_nickname}</span>
</div>
<div class="input_aera">
<input type=number name="phone" class="phone" placeholder="'-' 없이 입력해 주세요" maxlength="11" value="${signupDto.phone}" >
<span class="msg_box">${valid_phone}</span>
</div>
<input type="submit" value="회원가입" class="login_btn" >
</form>
</div>
</main>
</body>
</html>
사용자로부터 입력받은 데이터를 form으로 감싸 서버로 보내고 서버에서는 이에 대한
정보를 Dto에 담아 받을겁니다 각 input값에 value는 처음에는 아무것도 존재하지 않지만
유효성검사에 실패시 el태그를 사용하여 사용자가 입력한 정보를 유지하도록 하기 위함이고
msg_box에는 유효성검사가 실패한 이유를 나타낼겁니다
이제 최상위 패키지안에 dto패키지를 생성하고 signupDto클래스를 추가해줍니다
package com.han.delivery.dto;
import lombok.Data;
@Data
public class SignupDto {
@NotBlank(message = "아이디를 입력해주세요")
@Pattern(regexp = "^[ㄱ-ㅎ가-힣a-z0-9-_]{5,30}$", message = "아이디는 특수문자를 제외한 5자이상이여야 합니다")
private String username;
@NotBlank(message = "비밀번호를 입력해주세요")
@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z]).{8,16}", message = "최소 하나의 문자 및 숫자를 포함한 8~16자이여야 합니다")
private String password;
@NotBlank(message = "이메일을 입력해주세요")
@Pattern(regexp = "^(?:\\w+\\.?)*\\w+@(?:\\w+\\.)+\\w+$", message = "이메일 형식이 올바르지 않습니다")
private String email;
@NotBlank
@Pattern(regexp = "^[ㄱ-ㅎ가-힣a-z]{2,30}$", message = "숫자 또는 특수문자를 제외한 2자이상 입력해주세요")
private String nickname;
@Pattern(regexp = "^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$", message = "휴대폰번호를 확인해 주세요")
private String phone;
}
@NotBlank를 사용하면 null 과 "" 과 " "를 모두 허용하지 않으며
이를 위배하면 message에 설정한 값을 응답해줍니다 Pattern도 마찬가지로
우리가 정해놓은 패턴을 벗어날시 해당 message를 signup.jsp에 보내줄겁니다
다음으로 최상위패키지안에 controller패키지를 생성하고 AuthController클래스를
추가해줍시다
package com.han.delivery.controller;
@Controller
public class AuthController {
@Autowired
AuthService authService;
@GetMapping("/auth/signin")
public String signin() {
return "auth/signin";
}
@GetMapping("/auth/signup")
public String signup() {
return "auth/signup";
}
//로그인 실패시 에러메시지를 보여주기 위함
@GetMapping("/auth/failed")
public String failedSignin(Model model) {
return Script.locationMsg("/auth/signin", "아이디 또는 비밀번호를 잘못 입력하셨습니다", model);
}
//Errors는 반드시 Request객체 바로뒤에 위치해야함
@PostMapping("/auth/signup")
public String signup(@Valid SignupDto signupDto, Errors errors, Model model) {
//유효성 검사 실패
if(errors.hasErrors()) {
//사용자로부터 입력받은 데이터를 유지하기 위함
model.addAttribute("signupDto", signupDto);
//유효성검사에 실패한 필드와 메시지를 저장
Map<String, String> validResult = authService.validHandling(errors);
//필드를 key값으로 에러메시지 저장
for(String key : validResult.keySet()) {
System.out.println(key + validResult.get(key));
model.addAttribute(key, validResult.get(key));
}
//사용자로부터 입력받은 데이터와 에러메시지를 가지고 회원가입페이지로 다시이동
return "auth/signup";
}
//username이 이미 존재할시 키값에 오류메시지 저장
if(authService.usernameChk(signupDto.getUsername()) != 0 ) {
model.addAttribute("valid_username","이미 등록된 아이디입니다");
return "auth/signup";
}
//유효성 검사 성공시 회원가입 서비스 로직 실행
authService.signup(signupDto);
//이때 회원가입이 성공하였다는 메시지 출력 후 로그인페이지 이동
return Script.locationMsg("/auth/signin", "회원가입에 성공하였습니다", model);
}
}
Errors는 반드시 @Valid를 선언한 Request객체 바로 뒤에 와야합니다
errors.hasErrors()는 유효성검사에 실패했을시 true값을 반환하는데
만약 유효성검사에 실패했을시 우리는 기존에 사용자가 입력한 데이터값을 유지한채
회원가입페이지를 재호출해야하므로 입력받은 데이터를 담은 Dto을 다시 Model에
심도록 합시다
errors는 유효성검사에 실패한 필드를 키값으로 우리가 지정해준 message를 저장하는데
우리는 이 message를 사용자에게 보여줘야하기 때문에 model에 실패한 필드명을
key값으로 우리가 Dto에 미리 설정한 message를 value값으로 심어주도록 합니다
여기서 문제가 하나 있습니다 유저의 아이디는 유니크값이기에 중복될수가 없는데 유효성검사로는 중복된 아이디인지를 확인할수가 없습니다 따라서 이를 위해 Dto에서 username을 뽑아
usernameChk라는 메서드를 만들어 DB에 존재하는지를 확인할겁니다
자 이제 최상위패키지안에 service패키지를 생성하고 AuthService클래스를 추가해줍시다
package com.han.delivery.service;
@Service
public class AuthService {
@Autowired
AuthMapper authMapper;
@Transactional
public Map<String, String> validHandling(Errors errors) {
Map<String, String> validResult = new HashMap<>();
for(FieldError error : errors.getFieldErrors()) {
validResult.put("valid_"+error.getField(), error.getDefaultMessage());
}
return validResult;
}
//아이디 중복확인
@Transactional
public int usernameChk(String username) {
return authMapper.usernameChk(username);
}
//회원가입
public void signup(SignupDto signupDto) {
authMapper.signup(signupDto);
}
}
validHandling메서드는 Error객체에서 모든 오류메시지를 꺼내어
Map에 담기 위한 메서드입니다 유효성검사에 실패한 필드를 key값 , 메시지를 value로
가지고 있으며 우리는 key값앞에 valid를 붙여서 넘겨줄겁니다
이제 DB와 통신하기 위한 Mapper 인터페이스를 추가해주도록 하겠습니다
마찬가지로 dao패키지를 생성하고 AuthMapper 인터페이스를 추가해줍니다
package com.han.delivery.dao;
import org.apache.ibatis.annotations.Mapper;
import com.han.delivery.dto.SignupDto;
@Mapper
public interface AuthMapper {
public int usernameChk(String username);
public void signup(SignupDto signupDto);
}
mapper는 mybatis가 제공하는 기능으로 기존에 Dao인터페이스와 Dao구현체중에
오직 인터페이스만으로 실제 sql문을 담당하는 xml파일과 연결이 가능합니다
따라서 구현체를 만들 필요가 없고 *.xml
파일에 sql문 id와 mapper의 메서드이름만
맞춰주면 됩니다
이제 실제 DB에 쿼리를 날려줄 mapper를 작성해줘야 합니다
src/main/resource에 mappers폴더를 생성하고 AuthMapper.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.han.delivery.dao.AuthMapper">
<select id="usernameChk" resultType="int">
SELECT COUNT(*) FROM DL_USER WHERE USERNAME = #{username}
</select>
<insert id="signup">
INSERT INTO DL_USER (ID, USERNAME, PASSWORD, EMAIL, NICKNAME, PHONE)
VALUES (USER_ID_SEQ.NEXTVAL, #{username}, #{password}, #{email}, #{nickname},#{phone} )
</insert>
</mapper>
usernameChk는 아이디 중복체크를 위한 쿼리로 COUNT를 통해 사용자가 입력한
아이디가 현재 존재하는지를 확인합니다 이때 존재한다면 1 존재하지 않는다면 0이
나올것이며 이때 우리는 1아니면 0의 값을 다시 Controller까지 보내줘야하므로 반드시
resultType을 int로 해줘야합니다
위에서 한가지 설명을 안하고 내려온게 있는데 보통 스프링+Mybatis조합을 보면
Service/Servicelmp , DAO/DAOlmp와 같은식으로 인터페이스와 구현체를 나눠놓는걸
볼수있습니다.
이런 경우는 객체 간의 결합도를 낮추어 유연한 개발을 하고 하나의
인터페이스를 구현하는 여러 구현체가 있을때 적절한 구현체를 넣을수 있도록 다형성을
주기 위해서입니다
또한 AOP Proxy가 인터페이스 기반으로 프록시 객체를 만들게 되어있어서 이런식으로
사용했었지만 지금은 클래스 기반으로 AOP Proxy를 만들수 있고
우리가 현재 진행중인 프로젝트에서는 하나의 인터페이스에 여러 구현체가 필요하지 않아
오히려 복잡도만 높아지게 되므로 인터페이스와 구현체를 나누지 않을겁니다
이제 마지막으로 유효성검사를 통과했을경우 회원가입이 완료되었다는 메시지를 출력해준후
로그인화면으로 이동시켜줘야 합니다
여러가지 방법이 있는데 @Responsebody을 통해 스크립트를 넘겨주는 방법이 제일 간단하지만
우리는 현재 유효성검사 실패시에 회원가입 화면을 뿌려주고 있습니다 따라서 현재 함수에 @Responsebody를 붙여주게 될 경우 유효성검사 기능이 제대로 작동하지 않으므로
새로운 클래스를 하나 만들어서 이를 처리할겁니다
utils패키지를 하나 생성한후 Script.java 클래스를 하나 추가해줍시다
package com.han.delivery.utils;
public class Script {
public static String locationMsg(String locationUrl, String msg, Model model) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("<script>");
stringBuilder.append("alert('"+msg+"');");
stringBuilder.append("location.href='"+locationUrl+"';");
stringBuilder.append("</script>");
stringBuilder.toString();
model.addAttribute("locationMsg",stringBuilder);
return "utils/locationMsg";
}
}
마지막으로 views안에 utils폴더를 생성하고 locationMsg.jsp를 추가해줍니다
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
${locationMsg}
</body>
</html>
Controller의 제일 마지막부분의
return Script.locationMsg("/auth/signin", "회원가입에 성공하셨습니다", model)
를 보면 알수있듯이 이동시키길 원하는 주소 / 출력할 메시지 / model을 적어주면
Script.java의 locationMsg에서 스크립트를 조합하여 Model에 심어 locationMsg.jsp
로 보내줍니다 이때 이 jsp파일의 body에는 el문으로 ${locationMsg}가 들어가있으므로
<script>
구문이 실행되어 alert
를 통해 회원가입이 완료되었다는 메시지를
보여주며 그 직후 로그인페이지로 이동시켜줍니다
이것으로 회원가입 기능 구현은 끝이났고 유효성검사와 회원가입이 잘 작동하는지 확인해봅시다
감사합니다. 잘 배우고 갑니다.