MemberDTO 유효성 검사
build.gradle -> dependencies에 Spring Validation 의존성 주입
- Validation : 클라이언트로부터 전달된 데이터를 서버에서 검증할 때 사용되는 라이브러리
// 유효성 검사
implementation 'org.springframework.boot:spring-boot-starter-validation'
MemberDTO
@Getter
@Setter
public class MemberDTO {
private Long userNo;
//@NotBlank : null X, 공백이 아닌 문자 포함
@NotBlank(message = "아이디는 필수 입력값입니다")
@Pattern(regexp = "^(?=.*[a-z])[a-z0-9]{6,12}$", message = "영문 소문자 + 숫자 6 ~ 12자리여야 합니다.")
private String userId;
@NotBlank(message = "비밀번호는 필수 입력값입니다.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,20}$", message = "영문+숫자+특수문자를 포함하여 8~20자로 입력하세요.")
private String userPwd;
@NotBlank(message = "비밀번호 확인은 필수입니다.")
private String confirmPassword;
@NotBlank(message = "이름은 필수 입력값입니다.")
@Pattern(regexp = "^[가-힣]{2,10}$", message = "한글만 가능 합니다.")
private String userName;
@NotBlank(message = "전화번호는 필수 입력값입니다")
@Pattern(regexp = "^010\\d{8}$", message = "'-' 없이 입력하세요.")
private String userPhone;
private LocalDateTime createdDate; // 생성일
private LocalDateTime deletedDate; // 탈퇴일
}
자주 사용하는 Validation 어노테이션
- @NotBlank : null, 빈 문자열(공백)을 허용하지 않음
- @Size(min = 최소 길이, max = 최대 길이, message = "출력할 메세지")
- @Email(message = "유효한 이메일 주소를 입력하세요")
- @Pattern(regexp = " 적용할 정규 표현식", message = " 출력할 메세지 ")
위의 어노테이션보다 훨씬 종류가 많다!!
MemberController
MemberDTO에 대한 유효성 검사 어노테이션(위에서 적용한) 실행 -> @Valid
BindingResult -> @Valid에서 수행된 유효성 검사 결과를 저장
bindingResult 객체를 바인딩 -> 발생한 메시지를 타임리프(사용자)에 보여주기 위함
*** BindingResult는 @Valid 매개변수의 바로 뒤에 와서 사용되어야 한다!
// 회원가입
@PostMapping("/signup")
public String signUp(@Valid MemberDTO memberDTO, BindingResult bindingResult) {
// bindingResult -> model.Attribute 하지 않아도 Model 객체에 자동 바인딩 (생략가능)
if (bindingResult.hasErrors()) { // 유효성 검사에서 발생한 에러확인 -> 있을시 true, 없을시 false
return "member/sign";
}
int result = memberService.setSignup(memberDTO);
return "member/sign";
}
아이디 중복 검사
일단 바로 이전 블로그에서 작성한 JavaScript 전체 코드에서 아이디 중복검사 함수를 살펴 보도록 하겠다.
클라이언트 측 : JavaScript
// 아이디 중복체크 함수
function idCheck() {
const signUpId = document.getElementById("signUpId").value; // 입력된 아이디 값
const idMsg = document.getElementById("idMsg"); // 아이디 메시지 출력 요소
// 영문 소문자와 숫자만 허용, 6자리 이상 12자리 이하
const idPattern = /^(?=.*[a-z])[a-z0-9]{6,12}$/;
// 아이디 입력란이 비어있는지 확인
if (!signUpId) {
alert('아이디를 입력해 주세요.');
return; // 아이디가 없으면 함수를 종료
}
// 유효성 검사
if (!idPattern.test(signUpId)) {
idMsg.innerHTML = "영어 소문자 + 숫자 6~12자리로 입력하세요.";
idMsg.style.color = "red";
isIdValid = false;
return;
}
console.log(signUpId);
console.log("Checking ID");
// AJAX를 통해 서버에 중복 체크 요청
$.ajax({
type: "POST",
url: "/idcheck",
data: JSON.stringify({ userId: signUpId }), // JSON 형식으로 데이터를 전송
contentType: "application/json", // 서버가 JSON 형식을 인식하도록 지정
success: function(response) {
console.log(response);
if (response.result > 0) {
alert('이미 사용 중인 아이디입니다.');
idMsg.innerHTML = "아이디가 이미 사용 중입니다.";
idMsg.style.color = "red";
isIdValid = false;
} else {
alert('사용 가능한 아이디입니다.');
idMsg.innerHTML = "사용 가능한 아이디입니다.";
idMsg.style.color = "#aaa";
isIdValid = true;
}
},
error: function(err) {
console.error('AJAX 요청 실패:', err);
idMsg.textContent = '서버 오류가 발생했습니다. 나중에 다시 시도해 주세요.';
idMsg.style.color = "red";
isIdValid = false;
}
});
}
ajax의 역할
사용자가 입력한 아이디가 유효한 형식인지 클라이언트 측에서 먼저 검증
서버에 아이디 중복체크 요청 -> POST 방식, /idcheck 요청을 보낼 URL
서버가 응답을 반환 -> success 콜백 : 사용자에게 중복 여부 전달
서버가 응답을 반환 -> 요청 실패시 error 콜백 : 에러 처리
서버 측
: Controller 클라이언트에서 전달된 아이디 -> Service에 전달 -> 실제 데이터베이스에 있는 아이디와 중복 여부 확인
SignController
// 아이디 중복 체크 요청 처리
@PostMapping("/idcheck")
@ResponseBody
public Map<String, Object> idCheck(@RequestBody Map<String, String> param) {
Map<String, Object> response = new HashMap<>();
String userId = param.get("userId"); // 클라이언트에서 전달된 아이디
int result = memberService.idCheck(userId); // 서비스에서 아이디 중복 체크
response.put("result", result); // 응답 결과를 Map에 담아 반환
return response;
}
MemberService
// 호출된 idCheck 메서드
// memberMapper의 idCheck 메서드를 호출하여 데이터베이스에서 아이디의 존재 여부를 확인
public int idCheck(String userId) {
return memberMapper.idCheck(userId); // Mapper를 통해 아이디 중복 체크
}
MemberMapper
// MyBatis 매퍼로, SQL 쿼리와 java 메서드를 매핑
@Mapper
public interface MemberMapper {
public int idCheck(@Param("userId") String userId);
}
member-mapper.xml
// 실제 SQL 쿼리를 정의하여 데이터베이스에서 아이디의 중복 여부를 확인
// userId를 가진 레코드의 개수 조회 --> 1 : 아이디 존재, 0 : 아이디 없음
<select id="idCheck">
SELECT count(*) FROM USER
WHERE USER_ID = #{userId}
</select>
이렇게 중복되는 아이디가 있을 경우 아래와 같이 alert창이 나온다.
비밀번호 일치 여부 검증
validator 패키지 생성
PasswordMatches 어노테이션 생성
아래 코드는 비밀번호와 비밀번호 확인이 일치하는지 검증하는 커스텀 유효성 검사 어노테이션 생성 코드이다.
// 어노테이션을 ElementType.TYPE : 클래스, 인터페이스, enum에 적용할 수 있도록 지정
@Target({ElementType.TYPE})
// 어노테이션 유지 여부, RetentionPolicy.RUNTIME : 런타임동안 유지
@Retention(RetentionPolicy.RUNTIME)
// 유효성 검사 로직이 구현된 클래스 지정
@Constraint(validatedBy = PasswordMatchesValidator.class)
public @interface PasswordMatches {
// 유효성 검사 실패 시 반환될 기본 에러 메세지
String message() default "비밀번호와 비밀번호 확인이 일치하지 않습니다.";
Class<?>[] groups() default {}; // 유효성 검사 그룹 지정 (기본값은 그룹 지정 없음)
Class<? extends Payload>[] payload() default {}; // 추가적인 메타데이터를 전달할 때 사용 (기본값은 빈 배열)
}
*** 추가 설명 : @Constraint(validatedBy = PasswordMatchesValidator.class)
-> @PasswordMatches가 지정된 클래스가 유효성 검사를 할 때,
사용될 클래스(PasswordMatchesValidator) 정의
즉, 실제 검사 로직은 PasswordMatchesValidator라는 클래스에서 수행
PasswordMatchesValidator 클래스 생성
아래 코드는 사용자 정의 유효성 검사를 위한 클래스로, @PasswordMatches 어노테이션을 처리하는 역할을 한다.
public class PasswordMatchesValidator
// ConstraintValidator : 특정 어노테이션에 대한 유효성 검사를 수행
// PasswordMatches : 검사할 어노테이션 타입
// MemberDTO : 유효성 검사를 수행할 대상의 타입
implements ConstraintValidator<PasswordMatches, MemberDTO> {
@Override
public void initialize(PasswordMatches constraintAnnotation) {
// 초기화 메서드
}
@Override
public boolean isValid(MemberDTO memberDTO, ConstraintValidatorContext constraintValidatorContext) {
// 비밀번호, 비밀번호 확인 필드가 null이 아닌지 확인
if (memberDTO.getUserPwd() == null || memberDTO.getConfirmPassword() == null) {
return false;
}
// 두 비밀번호가 일치하는지 검사
boolean isValid = memberDTO.getUserPwd().equals(memberDTO.getConfirmPassword());
if (!isValid) {
// 기본 에러 메세지 비활성화
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext
// 에러 메세지 설정
.buildConstraintViolationWithTemplate("비밀번호가 일치하지 않습니다.")
// 설정한 에러 등록
.addConstraintViolation();
}
return isValid; // isValid 값 반환 (일치 true, 불일치 false)
}
}
MemberDTO : 생성한 유효성 검증 어노테이션 추가
member/sign.html
백엔드 회원가입 유효성 검사 -> th:object로 바인딩된 객체에 사용
<div th:if="${#fields.hasErrors('userId')}" th:errors="*{userId}"></div>
- ${#fields.hasErrors('userId')} : #fields를 사용하여 bindingResult가 제공하는 검증 오류에 접근
- th:errors="*{userId}" : id 필드에 에러가 있는지 확인하고, 있다면 에러 메시지를 div 태그에 추가를 해준다.
<div class="serverValidate" th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></div>
<div th:if="${#fields.hasGlobalErrors()}">
<div th:each="err : ${#fields.globalErrors()}">
<div th:if="${err.contains('비밀번호가 일치하지 않습니다.')}">
<div th:text="${err}"></div>
</div>
</div>
</div>
- ${#fields.hasGlobalErrors()} : 현재 memberDTO에 전역 오류가 있으면 true
- th:each="err : ${#fields.globalErrors()}" : 전역 오류 순회하면서 err 변수에 바인딩
- th:if="${err.contains('비밀번호가 일치하지 않습니다.')}"
: error 문자열에 ‘비밀번호가 일치하지 않습니다.‘ 텍스트가 포함되면 true
- th:text="${err}" : th:if가 true되면서 text 렌더링
<!--회원가입 시작-->
<div class="sign-up-htm">
<form th:action="@{/signup}" th:object="${memberDTO}" method="POST" id="signUpForm">
<!-- 회원가입 폼 -->
<div class="group">
<label for="signUpId" class="label">아이디</label>
<div class="id-group">
<input id="signUpId" type="text" class="input signUpId-input" th:field="*{userId}"
placeholder="영어소문자+숫자만 허용,6~12자리" required>
<button type="button" class="button idCheck-bnt" onclick="idCheck()">중복확인</button>
</div>
<div th:if="${#fields.hasErrors('userId')}" th:errors="*{userId}"></div>
<span id="idMsg" class="validate"></span>
</div>
<div class="group">
<label for="signUpPwd" class="label">비밀번호</label>
<input id="signUpPwd" type="password" class="input" th:field="*{userPwd}"
onkeyup="validatePassword()" placeholder="영문+숫자+특수문자 8~20자" required>
<div th:if="${#fields.hasErrors('userPwd')}" th:errors="*{userPwd}"></div>
<span id="pwdMsg" class="validate"></span>
</div>
<div class="group">
<label for="confirmPassword" class="label">비밀번호 확인</label>
<input id="confirmPassword" type="password" class="input" th:field="*{confirmPassword}"
onkeyup="validatePassword()" placeholder="비밀번호 확인">
<div class="serverValidate" th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></div>
<div th:if="${#fields.hasGlobalErrors()}">
<div th:each="err : ${#fields.globalErrors()}">
<div th:if="${err.contains('비밀번호가 일치하지 않습니다.')}">
<div th:text="${err}"></div>
</div>
</div>
</div>
<span id="pwdConfirmMsg" class="validate"></span>
</div>
<div class="group">
<label for="signUpName" class="label">이름</label>
<input id="signUpName" type="text" class="input" th:field="*{userName}"
onkeyup="validName()" placeholder="최소 2자, 최대 10자 한글" required>
<div class="serverValidate" th:if="${#fields.hasErrors('userName')}" th:errors="*{userName}"></div>
<span id="nameMsg" class="validate"></span>
</div>
<!--휴대전화번호 입력-->
<div class="group">
<label for="phone" class="label">휴대전화번호</label>
<div class="phone-group">
<input id="phone" type="text" class="input phone-input" th:field="*{userPhone}"
placeholder="'-' 없이 입력" required>
<button type="button" class="button requestSMS-bnt" onclick="requestSMS()">인증요청</button>
</div>
<div class="serverValidate" th:if="${#fields.hasErrors('userPhone')}" th:errors="*{userPhone}"></div>
<span id="phoneMsg" class="validate"></span>
<!--인증번호 입력-->
<div class="group">
<label for="Authentication" class="label"></label>
<input id="Authentication" type="text" class="input" placeholder="인증번호를 입력하세요" required>
<!--서버에서 발송된 인증번호-->
<input type="hidden" id="randomNum" value="">
<button type="button" class="button checkSMS-bnt"
onclick="checkSMS()">인증번호 확인
</button>
<span id="authMsg" class="validate"></span>
</div>
</div>
<div class="group">
<button type="submit" class="button signUpBnt" >회원가입</button>
</div>
</form>
<!--로그인 화면 이동-->
<div class="foot-lnk">
<label for="tab-1">
<a>이미 회원인가요?</a>
</label>
</div>
</div>
<!--회원가입 끝-->
앞의 블로그에서 진행했던 프론트 유효성 검사를 모두 주석 처리를 한 뒤 테스트를 하면 아래와 같이 나온다.
이제 회원가입은 완료 되었다
다음 블로그에서는 문자인증에 대해 적어보도록 하겠다!!