항상 개발하고 내용 정리는 Notion으로만 정리하다가 아이디 중복체크에 대한 로직을
쉽게 따라 할 수 있는 코드를 공유했으면 좋겠다는 생각에 Tistory를 시작하게 되었다.
왜냐하면,
아이디 중복체크에 대해서 정말 많은 검색을 했지만
쉽게 따라 할 수 있고 프론트+백이 같이 설명되어있는 게시글은 못 봤던 것 같다.
회원가입에서 아이디 중복체크를 할 때,
프론트에서 자바스크립트로 경고 문구를 띄어서 체크해준다고 해도
경고 문구를 무시하고 회원가입을 진행했다면 어떻게 될까?
당연히 백에선 처리를 안 해줬기 때문에 등록이 되는 오류가 발생할 것이다.
그렇기 때문에 프론트+백에서 전부 체크를 해줘야 한다.
아래 코드를 천천히 파악하고 구현한다면 회원가입 아이디 중복체크는 프론트+백에서 손쉽게 할 수 있을 것 같다.
1. MemberDto.java
package data.dto;
// 임포트 생략
@Setter
@Getter
@Alias("mdto")
public class MemberDto {
private int num;
@NotBlank(message = "필수 입력 항목입니다.")
@Pattern(regexp = "^[a-zA-Z0-9]{4,20}$", message = "4자~20자의 아이디여야 합니다.")
private String id;
@NotBlank(message = "필수 입력 항목입니다.")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$",
message = "숫자+영문자+특수문자 조합으로 8자리 이상 사용해야 합니다.")
private String pw;
@NotBlank(message = "필수 입력 항목입니다.")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$",
message = "숫자+영문자+특수문자 조합으로 8자리 이상 사용해야 합니다.")
private String pw_check;
// 나머지 생략
...
}
- @NotBlank는 Bean Validation에서 제공하는 표준 Validation이며, 기본적으로 3가지 어노테이션이 있다.
- @NotNull은 null만 허용하지 않고, @NotEmpty은 null과 "" 둘 다 허용하지 않고,
@NotBlank는 null, "", " " 전부 허용하지 않는다고 한다. - @Pattern은 정규식을 만족하는지 검사하는 어노테이션이라고 쉽게 생각하면 된다.
2. JoinController.java
package data.controller;
// 임포트 생략
@Controller
public class JoinController {
private final MemberService service;
// 생성자 주입
public JoinController(MemberService service) {
this.service = service;
}
// 회원가입
@PostMapping("/join/insert")
public String insert(@Valid MemberDto dto, Errors errors, Model model)
throws IOException {
if (errors.hasErrors()) {
// 회원가입 실패시 작성한 정보 유지!
model.addAttribute("MemberDto", dto);
Map<String, String> validatorResult = service.validateHandling(errors);
for (String key : validatorResult.keySet()) {
model.addAttribute(key, validatorResult.get(key));
}
// 유효성 검사가 맞지 않으면 폼으로 이동
return "/users/join_form";
}
// 유효성 검사를 통과하면 등록 후 페이지 이동
service.insertMember(dto);
return "/users/join_success";
}
// 아이디 중복체크
@ResponseBody
@RequestMapping("/idCheck")
public int idCheck(@RequestBody String id) {
// id 입력칸에 작성한 id가 db에 있는지 확인
int cnt = service.idCheck(id); // 있으면 1, 없으면 0
return cnt;
}
}
- @Valid는 객체에 명시된 조건을 검사하는 Annotation으로 검사해야 할 항목 앞에 붙여줘야 한다.
3. JoinService.java
package data.service;
// 임포트 생략
@RequiredArgsConstructor
@Service
public class JoinService {
private final JoinMapper mapper;
// 회원가입
public void insertMember(MemberDto dto) {
mapper.insertMember(dto);
}
public Map<String, String> validateHandling(Errors errors) {
Map<String, String> validatorResult = new HashMap<>();
for (FieldError error : errors.getFieldErrors()) {
String validKeyName = String.format("valid_%s", error.getField());
validatorResult.put(validKeyName, error.getDefaultMessage());
}
return validatorResult;
}
// 아이디 중복체크
public int idCheck(String id) {
return mapper.idCheck(id);
}
// 나머지 생략
...
}
4. JoinMapper (인터페이스로 생성)
package data.mapper;
import org.apache.ibatis.annotations.Mapper;
import data.dto.MemberDto;
@Mapper
public interface JoinMapper {
// 회원가입
public void insertMember(MemberDto dto);
// 아이디 중복체크
public int idCheck(String id);
// 나머지 생략
...
}
5. JoinSql.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="data.mapper.JoinMapper">
<insert id="insertMember" parameterType="mdto">
insert into member (id,pw)
values (#{id},#{pw})
</insert>
<select id="idCheck" parameterType="String" resultType="int">
select count(*) from member where id = #{id}
</select>
</mapper>
6. Join.jsp
<tr>
<th><span>*</span>아이디</th>
<td>
<input type="text" name="id" id="id" value="${MemberDto.id}"/>
<p id="id_tag">${valid_id}</p>
</td>
</tr>
<tr>
<th><span>*</span>비밀번호</th>
<td>
<input type="password" name="pw" id="pw_form" value="${MemberDto.pw}"/>
<p id="pw_tag">${valid_pw}</p>
</td>
</tr>
<tr>
<th><span>*</span>비밀번호 확인</th>
<td>
<input type="password" name="pw_check" id="pwcheck_form" value="${MemberDto.pw_check}"/>
<p id="pwcheck_tag">${valid_pw_check}</p>
</td>
</tr>
- input의 value 값을 넣는 이유는 유효성 검사가 실패한 경우를 대비하여 기존 작성 내용을 불러온다.
- ${valid_id}는 타임리프가 아닌 Jstl에서 사용할 때 에러 문구를 출력할 수 있다. (DTO의 Pattern Message 출력)
여기까지 Validation(Back) 유효성 검사와 ID 중복체크 로직까지만 완료되었다.
이제 아래 2가지만 더 진행하면 완벽하게 구현될 것이다.
- ID 중복체크는 제이쿼리로 입력하는 값에 따라 실시간 유효성 검사 (DB에 존재 여부에 따라 문구 출력)
- 제이쿼리로 중복체크 이후 중복된 값으로 회원가입 시 DB 저장 안 되게 처리
7. script.js
$(document).ready(function(){
// 정규식 변수
var idJ = /^[a-zA-Z0-9]{4,20}$/;
// 아이디 정규식 체크 및 중복확인
$('#id_form').on('keyup', function(){
// 한글 입력 안 되게 처리
$(this).val( $(this).val().replace( /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g, '' ) );
// 입력한 값의 길이 가져옴
var id_length = $(this).val().length;
// 입력한 값에서 공백 제거 후 id에 넣음
var id = $('#id_form').val().replace(/\s/gi,'');
// 정규식이 맞지 않거나, 입력값이 0보다 클 때
if(!idJ.test(id) && id_length >0){
$('#id_tag').text('특수문자를 제외한 2~10자리여야 합니다.');
$('#id_tag').css('color','#F63805');
// 정규식이 맞고, 입력값이 0보다 클 때
} else if(idJ.test(id) && id_length >0){
$.ajax({
type: 'post',
data: id,
url: '/idCheck',
contentType: 'application/json; charset=UTF-8',
success: function(cnt) {
if(cnt > 0) {
$('#id_tag').text('중복된 아이디입니다.');
$('#id_tag').css('color','#F63805');
} else {
$('#id_tag').text('사용 가능한 아이디입니다.');
$('#id_tag').css('color','#000');
}
}
});
} else{
$('#id_tag').text('');
}
});
});
- .replace()는 괄호 안의 앞에는 찾는 문자열, 뒤에는 바꿀 문자열을 넣으면 된다.
- .test()는 찾는 문자열이 들어있는지, 아닌지를 알려준다.
위 코드처럼 제이쿼리 처리까지 하면 아래 검사는 완료된 것으로 이제 한 개만 남았다.
ID 중복체크는 제이쿼리로 입력하는 값에 따라 실시간 유효성 검사 (DB에 존재 여부에 따라 문구 출력)
8. AbstractValidator.java
package data.validator;
// 임포트 생략
public abstract class AbstractValidator<T> implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
@SuppressWarnings("unchecked")
public void validate(Object target, Errors errors) {
try {
doValidate((T) target, errors);
} catch (RuntimeException e) {
throw e;
}
}
protected abstract void doValidate(final T dto, final Errors errors);
}
- Validator를 implements 하여 validate의 구현부 try안에 추상 메서드인 doValidate를 만들어서 검증 로직 클래스를
별도로 생성하여 doValidate 추상 메서드를 구현하면 된다. (아래 IdCheckValidator.class가 검증 로직 클래스)
9. IdCheckValidator.java
package data.validator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import data.dto.MemberDto;
import data.mapper.JoinMapper;
@Component
public class IdCheckValidator extends AbstractValidator<MemberDto> {
private final JoinMapper mapper;
public IdCheckValidator(JoinMapper mapper) {
this.mapper = mapper;
}
@Override
protected void doValidate(MemberDto dto, Errors errors) {
if (mapper.existsById(dto.getId())) {
errors.rejectValue("id", "아이디 중복 오류", "중복된 아이디입니다.");
}
}
}
10. JoinMapper 추가
<select id="existsById" parameterType="String" resultType="boolean">
select if(count(*) > 0, 'true', 'false') from member where id = #{id, jdbcType=VARCHAR}
</select>
- 아이디가 존재하면 count가 0보다 크기 때문에 true를 반환하고 아닐 경우 false를 반환하는 sql문!!
여기까지 만들었다면 아래 검사까지 완벽하게 완료된 것이다.
제이쿼리로 중복체크 이후 중복된 값으로 회원가입 시 DB 저장 안 되게 처리
중복된 아이디인 상태로 회원가입 버튼을 클릭해도 화면은 넘어가지 않고 해당 문구가 계속 표시된다.
이후 아이디를 사용 가능한 아이디로 수정을 하면 그때 회원가입이 제대로 된다.
이 게시글은 아이디 중복체크를 위주로 작성했지만 해당 방법으로 여러 가지 항목에 적용할 수 있다.
※ 만약에 따라하다가 잘 안되거나 궁금한 사항이 있으시면 댓글로 남겨주세요.
※ 해당 게시글은 Springboot + Maven + Mybatis + Jstl를 사용하여 개발하고 있는 프로젝트의 일부입니다.
'Development Log' 카테고리의 다른 글
[React/NestJS/TypeScript] 로그인 시 JWT Token 발급 및 인증(Token sessionStorage 저장) (0) | 2023.09.15 |
---|---|
choco 설치와 mkcert 설치 후 React 적용(with. React + TypeScript) (2) | 2023.09.12 |
[Development Log] TypeScript에서 http-proxy-middleware 사용법 (0) | 2023.08.21 |
[Development Log] React + NestJS 연동하는 방법(with. TypeScript) (0) | 2023.08.21 |
Spring boot - Enum 개념 및 Enum Mybatis 사용방법 (0) | 2022.02.06 |