DB 설계한 내용을 바탕으로 회원가입과 로그인을 담당하는 UserController를 구현해보자.
MyBatis를 사용하는 Spring project는 이게 첫 글이라서 전체적으로 플로우대로 작성하려고 노력했다.

가장 중요한 것은 어느 부분을 비동기로, 어느 부분을 동기로 할 것인가였다.
회원가입 버튼을 submit으로 한 뒤 각 필드를 form으로 감싸는 방법은 어떨까?
필수 항목을 한 번 더 검증하기 위해서 버튼을 클릭했을 시 각 태그의 값을 클라이언트단에서 가져와 어느정도 검증할 수 있는 JS 이벤트를 구성했다.
<button type="button" id="signup-button">회원가입</button>
$(document).on("click", "#signup-button", () => {
const USER_EMAIL = $("#USER_EMAIL").val();
const USER_NAME = $("#USER_NAME").val();
const USER_PWD = $("#USER_PWD").val();
const USER_PWD2 = $("#USER_PWD2").val();
const MARKET_CHK = $("#MARKET_CHK").prop("checked");
const PRIVAC_CHK = $("#PRIVAC_CHK").prop("checked");
// 패스워드 동일한지 검사
if (USER_PWD != USER_PWD2) {
}
// 필수항목 전부 체크됐는지 검사
let allChecked = true;
$(".essential-chk").each(function () {
if (!this.checked) {
allChecked = false;
return false; // 반복문 탈출
}
});
if (!allChecked) {
}
axios({
method: "post",
url: "/user/validate-signup",
data: {USER_EMAIL, USER_NAME, USER_PWD, MARKET_CHK, PRIVAC_CHK},
dataType: "String",
headers: {"Content-Type": "application/json"}
}).then(res => {
console.log(res.data);
if (res.data == true) {
window.location.replace("joinConfirm");
}
});
});
axios로 서버의 /user/validate-signup에 post 요청을 보낸 후 성공적이라면 회원가입에 성공했다는 페이지로 이동하도록 설계했다.
html의 구분자와 id, class 등에 대한 설명은 생략하겠다. 이 방법 말고도 input 태그에 required 속성을 추가하는 등 방법은 많다는 것에 유의하자. 우선은 에러가 났을 때 별다른 대처를 하지 않도록 했다.
// UserContoller method
@ResponseBody
@RequestMapping("validate-signup")
public boolean validateSignup(@RequestBody HashMap<String, Object> user) {
String userEmail = (String) user.get("USER_EMAIL");
String userName = (String) user.get("USER_NAME");
String userPwd = (String) user.get("USER_PWD");
boolean marketChk = (boolean) user.get("MARKET_CHK");
boolean privacChk = (boolean) user.get("PRIVAC_CHK");
// 비밀번호 정규식 기준: 숫자, 영문, 특수문자 허용 8~20 글자. 섞어서 안써도 됨.
if (userName.length() < nameMinLen || userName.length() > nameMaxLen
|| !emailPattern.matcher(userEmail).matches()
|| !pwdPattern.matcher(userPwd).matches()
|| userService.userSelectByEmail(userEmail) != null
|| userService.userSelectByName(userName) != null) {
return false;
}
// 검증 성공 후 USER DB에 데이터 삽입
try {
userService.userInsert(user);
long userIdNo = (long) userService.userSelectByEmail(userEmail).get("USER_ID_NO");
List<HashMap<String, Object>> terms = new ArrayList<>();
addTerm(terms, userIdNo, "MARKET_CHK", marketChk);
addTerm(terms, userIdNo, "PRIVAC_CHK", privacChk);
System.out.println(terms);
termService.insertAll(terms);
}
catch (Exception e) {
e.printStackTrace();
}
System.out.println(user);
return true;
}
현재 회원가입을 검증하는 컨트롤러의 메소드이다. 자바의 HashMap형태로 json을 받아서 검증 후 데이터베이스에 유저와 권한 데이터를 삽입한다.
MyBatis에서는 데이터를 삽입한 뒤 id 같은 값을 반환하도록 할 수 있지만 나중에 안 부분이라 이런건 기능구현 이후 나중에 여유있을 때 리팩토링하는 것으로 결정했다...
아래는 userService가 DB에 접근할 때 사용하는 코드의 일부이다.
<!-- user.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.ecommerce.projectec.user.UserMapper">
<select id="userSelectByEmail" resultType="HashMap">
SELECT * FROM USER WHERE USER_EMAIL = #{USER_EMAIL}
</select>
<insert id="userInsert" parameterType="HashMap">
INSERT INTO USER(USER_EMAIL, USER_NAME, USER_PWD)
VALUES (#{USER_EMAIL}, #{USER_NAME}, #{USER_PWD})
</insert>
</mapper>
@Mapper
public interface UserMapper {
void userInsert(HashMap<String, Object> user);
HashMap<String, Object> userSelectByEmail(@Param("USER_EMAIL") String userEmail);
}
MyBatis는 위와 같이 xml 파일로 쿼리를 날린 후 Mapper에 매핑하는 방식으로 관리하는 것을 추천한다. 이 방식은 SQL을 자유롭게 사용하기 편하고 직관적이지만 재사용이 어렵다는 느낌이 든다.
@RequiredArgsConstructor
@Service
public class UserService{
private final UserMapper userMapper;
@Override
public void userInsert(HashMap<String, Object> user) {
if (user == null)
return;
// 비밀번호 인코딩
// user.put("userPwd", passwordEncoder.encode((String)user.get("userPwd")));
userMapper.userInsert(user);
return;
}
@Override
public HashMap<String, Object> userSelectByEmail(String userEmail) {
HashMap<String, Object> user = userMapper.userSelectByEmail(userEmail);
if (user == null)
return null;
return user;
}
}
MyBatis에서 성공적으로 Mapper와 xml을 매핑했다면 이렇게 매퍼 인터페이스임에도 불구하고 구현체로 사용할 수 있다. 비밀번호는 단방향함수(Hash)로 인코딩하여 DB에 저장하는 방법이면 안전하다. 자세한 설명은 나중에 한다.
프로젝트 내용과는 별개지만, 자바는 null safety에 취약하므로 nullable 변수들은 항상 유의하면서 사용하자.
어쨌든 이런 식으로 회원버튼을 눌렀을 때의 구현을 마치겠다.
뭐 이 정도로도 괜찮지만 전 글에서 말했던 것처럼 요새 웹페이지들은 이메일 필드를 입력할 때 깔쌈하게 비동기로 '이 이메일은 사용 가능합니다'같은 문구가 출력된다. 기획서에도 비슷한 내용을 요구했으므로 이를 구현해보도록 한다. 예시로 이메일 필드만 보도록 하자.

<!-- 회원가입 input html 부분 -->
<div class="inputboxs">
<label for="joinUserEmail">
<p>이메일주소(ID)</p>
<input type="textbox" name="USER_EMAIL" placeholder="이메일 주소 입력" id="joinUserEmail">
<p class="wrong__pw" id="invalidEmail" style="display: none;"> 이메일이 이미 존재하거나 불가능한 형식입니다. </p>
<p class="right__pw" id="validEmail" style="display: none;"> 이 이메일은 사용 가능합니다. </p>
</label>
이메일 필드를 이런식으로 작성 후 invalidEmail, validEmail id 속성을 담자. input 값이 바뀔 때마다 검증 후 해당 p 태그가 보이도록 할 것이다.
// jQuery
$(document).on("propertychange change keyup paste input", "#joinUserEmail", () => {
const USER_EMAIL = $("#joinUserEmail").val();
axios({
method: "post",
url: "/user/validate-email",
data: {USER_EMAIL},
dataType: "String",
headers: {"Content-Type": "application/json"}
}).then(res => {
if (res.data == false) {
$("#invalidEmail").show();
$("#validEmail").hide();
}
else {
$("#invalidEmail").hide();
$("#validEmail").show();
}
});
});
별다른 설명이 필요없다. JQuery(js)에서 해당 input 태그에 맞는 이벤트를 등록 후 서버에서 검증 받은 결과를 이용한다.
컨트롤러에서 이 이메일을 받아 DB에 값이 있는지 없는지 검증하는, /user/validate-email에 매핑된 메소드만 만들면 끝이다.
// UserController 내부임
@ResponseBody
@RequestMapping("validate-email")
public boolean validateEmail(@RequestBody HashMap<String, Object> user) {
String userEmail = (String) user.get("USER_EMAIL");
return isEmailValid(userEmail);
}
private boolean isEmailValid(String userEmail) {
return userEmail != null && emailPattern.matcher(userEmail).matches()
&& userService.userSelectByEmail(userEmail) == null;
}
회원가입과 비교해서 로그인에 주의해야 할 점으로 떠오른 건 두 개 있다.
사용자가 입력한 비밀번호를 암호화된 DB안의 비밀번호와 비교하기 위해선 같은 방법으로 단방향 암호화를 진행한 뒤 비교하면 안전할 것이다. 그리고 로그인 성공시에는 세션을 이용했다. 짧게 짚고 넘어가자.

// UserController 내부 메소드
@PostMapping("login")
public String login(@RequestParam("USER_EMAIL") String userEmail,
@RequestParam("USER_PWD") String userPwd,
HttpSession httpSession,
Model model) {
HashMap<String, Object> user = userService.userSelectByEmail(userEmail);
if (user == null) {
model.addAttribute("errorMessage", "이메일이 잘못 입력되었습니다.");
return "user/signin";
}
// important: 비밀번호 인코딩 비교로 교체 필요
if (!(userPwd.equals(user.get("USER_PWD")))) {
model.addAttribute("errorMessage", "비밀번호가 잘못 입력되었습니다.");
return "user/signin";
}
httpSession.setAttribute("USER_EMAIL" , user.get("USER_EMAIL"));
httpSession.setAttribute("USER_NAME", user.get("USER_NAME"));
return "redirect:/";
}
로그인은 굳이 비동기를 사용하지 않고 새로고침하도록 했다. 그리고 로그인 성공시에는 httpSession을 사용해 세션에 이메일과 이름 값을 담도록 설정했다.
부족한 점이 많다. 항상 이런 구현에는 trade-off가 따르는 것 같다.
회원가입 검증에서 DTO, VO를 사용하지 않았지만 UserDto라는 클래스를 만든 후 받아와 BindingResult 어노테이션을 사용한다면 더 깔끔하게 구현할 수 있을 것이다. 개발할 때 HashMap은 멤버 변수가 없어서 넘어오는 내용을 알아야 한다는 점이 불편하다. 즉 클래스를 만들어서 매핑하는 방식보다 유지보수상 불리하다. 하지만 DTO를 사용하면 비슷하지만 약간씩 다른 객체들을 관리할 때 그 비슷한 DTO들을 만들어야 한다는 단점이 있다. 여기서는 코드를 나 혼자 보기도 하고 DTO를 굳이 사용해야 할 필요성을 느끼지 않아서 대부분 HashMap을 사용했다.
장단점이 있으므로 필요할 경우 섞어서 쓰도록 해보자.
그리고 항상 Controller와 Service를 설계할 때 객체지향을 고려하자. 기능 구현에 있어서 결합도가 너무 높아지면 재사용이 어려운 컨트롤러 내부의 메소드들이 하는 게 너무 많아지는 것 같다. 그러나 당연하게도 너무 얽매여서도 안 된다. 항상 주어진 시간안에 최대한 가독성과 재사용에 염두하자.
이 프로젝트의 목표가 기능 구현이지만 끝나고 난 후에는 부족한 부분인 리팩토링과 테스트 코드 작성에 집중해보고 싶다.
예를 들어서 현재 코드에선 트랜잭션 처리를 하지 않았기 때문에 테스트 코드를 작성한다면 이런 문제를 어디가 구현이 되지 않은건지 따로 고려하지 않아도 나중에 고치기 편하지 않을까라는 생각이 든다.
다음 글에서는 상품을 등록하고 조회하는 어드민 페이지를 어떻게 구현했는지 적자.
잘 봤습니다. 좋은 글 감사합니다.