[VUE3 + JAVA] 비밀번호 벨리데이션 체크 예제 :PasswordValidator 클래스를 만들자!
FRONTEND
userPwd: yup
.string()
.required('비밀번호를 입력해 주세요.')
.min(8, '8자리 이상으로 입력해 주세요.')
.matches(regExpForPw, '영문, 숫자 및 특수문자 포함, 8자 이상으로 입력해 주세요.')
.test(
'no-sequential-numbers',
'연속된 숫자(예: 123, 321)는 사용할 수 없습니다.',
(value) => !value || !hasSequentialNumbers(value)
)
.test(
'no-repeated-characters',
'동일한 문자를 3자리 이상 반복할 수 없습니다.',
(value) => !value || !hasRepeatedCharacters(value)
),
// * 비밀번호 정규식 패턴 (영문, 숫자, 특수문자 포함 8자리 이상)
export const regExpForPw =
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+~`\-={}[\]:;"'<>,.?/|\\])[A-Za-z\d!@#$%^&*()_+~`\-={}[\]:;"'<>,.?/|\\]{8,}$/;
/**
* 연속된 숫자(정방향 또는 역방향)가 포함되어 있는지 검사
*
* @param value 검사할 문자열
* @param limit 연속으로 허용하지 않는 숫자의 개수 (기본값: 3)
* @returns 연속된 숫자가 있는 경우 true, 그렇지 않으면 false
*/
export function hasSequentialNumbers(value: string, limit: number = 3): boolean {
if (!value || limit < 2) return false;
for (let i = 0; i <= value.length - limit; i++) {
const chunk = value.slice(i, i + limit);
if (/^\d+$/.test(chunk)) {
const digits = chunk.split('').map(Number);
let isAscending = true;
let isDescending = true;
for (let j = 1; j < digits.length; j++) {
if (digits[j] !== digits[j - 1] + 1) isAscending = false;
if (digits[j] !== digits[j - 1] - 1) isDescending = false;
}
if (isAscending || isDescending) return true;
}
}
return false;
}
/**
* 문자열에 지정된 개수 이상의 반복된 문자가 있는지 검사.
* ex> 반복 문자 검사 (예: aaa, $$$, 1111)
*
* @param value 검사할 문자열
* @param limit 반복을 허용하는 문자 개수 (기본값: 3)
* @returns 반복된 문자가 있는 경우 true, 그렇지 않으면 false
*/
export function hasRepeatedCharacters(value: string, limit = 3): boolean {
const pattern = new RegExp(`(.)\\1{${limit - 1},}`);
return pattern.test(value);
}
frontend 규칙 그대로 Backend(Java)에서 1:1로 동일하게 적용하는 형태로 구현!
Frontend 규칙 요약
- 최소 8자
- 영문 포함
- 숫자 포함
- 특수문자 포함 (frontend에서 정의한 전체 특수문자 집합)
- 연속 숫자 3자리 이상 금지 (123, 321 등)
- 동일 문자 3회 이상 반복 금지 (aaa, $$$, 111)
Java 정규식 (Frontend와 동일하게 맞춤)
private static final String REGEX_PW =
"^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+~`\\-={}\\[\\]:;\"'<>,.?/|\\\\])" +
"[A-Za-z\\d!@#$%^&*()_+~`\\-={}\\[\\]:;\"'<>,.?/|\\\\]{8,}$";
✔ frontend 정규식 그대로 이스케이프만 Java 맞춤
✅ 연속 숫자 검사 (Frontend hasSequentialNumbers 동일 로직)
public static boolean hasSequentialNumbers(String value, int limit) {
if (value == null || value.length() < limit || limit < 2) return false;
for (int i = 0; i <= value.length() - limit; i++) {
String chunk = value.substring(i, i + limit);
if (chunk.matches("\\d+")) {
boolean isAscending = true;
boolean isDescending = true;
for (int j = 1; j < chunk.length(); j++) {
int prev = Character.getNumericValue(chunk.charAt(j - 1));
int curr = Character.getNumericValue(chunk.charAt(j));
if (curr != prev + 1) isAscending = false;
if (curr != prev - 1) isDescending = false;
}
if (isAscending || isDescending) return true;
}
}
return false;
}
✅ 반복 문자 검사 (Frontend hasRepeatedCharacters 동일)
public static boolean hasRepeatedCharacters(String value, int limit) {
if (value == null || limit < 2) return false;
String regex = "(.)\\1{" + (limit - 1) + ",}";
return value.matches(".*" + regex + ".*");
}
✅ 최종 비밀번호 검증 메서드
public static String validatePassword(String password) {
if (password == null || password.isEmpty()) {
return "비밀번호를 입력해 주세요.";
}
if (password.length() < 8) {
return "8자리 이상으로 입력해 주세요.";
}
if (!password.matches(REGEX_PW)) {
return "영문, 숫자 및 특수문자 포함, 8자 이상으로 입력해 주세요.";
}
if (hasSequentialNumbers(password, 3)) {
return "연속된 숫자(예: 123, 321)는 사용할 수 없습니다.";
}
if (hasRepeatedCharacters(password, 3)) {
return "동일한 문자를 3자리 이상 반복할 수 없습니다.";
}
return null; // 정상
}
✅ Controller 적용 예시
String pwd = StringUtil.nvl(params.getUserPwd(), "");
String errorMsg = validatePassword(pwd);
if (errorMsg != null) {
result.put("msg", errorMsg);
modelMap.addAttribute(CmnConst.RECORD, result);
return modelMap;
}
PasswordValidator 클래스 생성
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@Component
public class PasswordValidator implements Validator {
private static final String REGEX_PW =
"^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+~`\\-={}\\[\\]:;\"'<>,.?/|\\\\])" +
"[A-Za-z\\d!@#$%^&*()_+~`\\-={}\\[\\]:;\"'<>,.?/|\\\\]{8,}$";
@Override
public boolean supports(Class<?> clazz) {
return String.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
String password = (String) target;
if (password == null || password.isEmpty()) {
errors.reject("password.required", "비밀번호를 입력해 주세요.");
return;
}
if (password.length() < 8) {
errors.reject("password.min", "8자리 이상으로 입력해 주세요.");
return;
}
if (!password.matches(REGEX_PW)) {
errors.reject("password.pattern", "영문, 숫자 및 특수문자 포함, 8자 이상으로 입력해 주세요.");
return;
}
if (hasSequentialNumbers(password, 3)) {
errors.reject("password.sequential", "연속된 숫자(예: 123, 321)는 사용할 수 없습니다.");
return;
}
if (hasRepeatedCharacters(password, 3)) {
errors.reject("password.repeat", "동일한 문자를 3자리 이상 반복할 수 없습니다.");
}
}
private boolean hasSequentialNumbers(String value, int limit) {
if (value == null || value.length() < limit || limit < 2) return false;
for (int i = 0; i <= value.length() - limit; i++) {
String chunk = value.substring(i, i + limit);
if (chunk.matches("\\d+")) {
boolean isAscending = true;
boolean isDescending = true;
for (int j = 1; j < chunk.length(); j++) {
int prev = Character.getNumericValue(chunk.charAt(j - 1));
int curr = Character.getNumericValue(chunk.charAt(j));
if (curr != prev + 1) isAscending = false;
if (curr != prev - 1) isDescending = false;
}
if (isAscending || isDescending) return true;
}
}
return false;
}
private boolean hasRepeatedCharacters(String value, int limit) {
String regex = "(.)\\1{" + (limit - 1) + ",}";
return value.matches(".*" + regex + ".*");
}
}
Controller 적용
@PostMapping("/user/save")
public ResponseEntity<?> saveUser(
@RequestBody UserRequest req,
Errors errors) {
passwordValidator.validate(req.getUserPwd(), errors);
if (errors.hasErrors()) {
return ResponseEntity.badRequest().body(errors.getAllErrors().get(0).getDefaultMessage());
}
return ResponseEntity.ok("SUCCESS");
}
Bean 자동 주입
@Autowired
private PasswordValidator passwordValidator;
@Component 없이 사용하는 방법
PasswordValidator 클래스에서 @Component만 제거 후 사용
Controller에서 직접 사용
@PostMapping("/user/save")
public ResponseEntity<?> saveUser(@RequestBody UserRequest req, Errors errors) {
PasswordValidator validator = new PasswordValidator();
validator.validate(req.getUserPwd(), errors);
if (errors.hasErrors()) {
return ResponseEntity.badRequest()
.body(errors.getAllErrors().get(0).getDefaultMessage());
}
return ResponseEntity.ok("SUCCESS");
}
여러곳에서 공통 사용 시 (권장)
public class PasswordValidators {
private static final PasswordValidator INSTANCE = new PasswordValidator();
public static PasswordValidator get() {
return INSTANCE;
}
}
PasswordValidators.get().validate(req.getUserPwd(), errors);
@InitBinder 방식 (Bean 등록 없이 자동연결)
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new PasswordValidator());
}
@PostMapping("/user/save")
public ResponseEntity<?> saveUser(@RequestBody String userPwd, Errors errors) {
if (errors.hasErrors()) {
return ResponseEntity.badRequest()
.body(errors.getAllErrors().get(0).getDefaultMessage());
}
return ResponseEntity.ok("SUCCESS");
}
Errors error은 초기화를 어떻게 해?
Spring Validator에서 Errors는 개발자가 new로 생성하는 객체가 아니다.
👉 Spring이 자동으로 만들어서 주입해 주거나,
👉 직접 만들려면 BindingResult 구현체를 사용해야 한다.
상황별로 정확히 정리해줄게.
✅ 1. Controller에서 자동 주입 방식 (가장 권장)
@PostMapping("/user/save")
public ResponseEntity<?> saveUser(
@RequestBody UserRequest req,
Errors errors) {
PasswordValidator validator = new PasswordValidator();
validator.validate(req.getUserPwd(), errors);
if (errors.hasErrors()) {
return ResponseEntity.badRequest()
.body(errors.getAllErrors().get(0).getDefaultMessage());
}
return ResponseEntity.ok("SUCCESS");
}
👉 Spring MVC가 Errors(BindingResult) 를 자동 생성해서 주입해줌
👉 개발자는 절대 new 안함
✅ 2. 수동 생성해야 하는 경우 (Service, 테스트 등)
Controller 밖에서 Validator 쓰면 이렇게 만든다.
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
Errors errors = new BeanPropertyBindingResult(password, "password");
PasswordValidator validator = new PasswordValidator();
validator.validate(password, errors);
if (errors.hasErrors()) {
System.out.println(errors.getAllErrors().get(0).getDefaultMessage());
}
✅ 3. 단순 문자열 검증 전용 util 방식 (Errors 없이)
PasswordValidator validator = new PasswordValidator();
Errors errors = new BeanPropertyBindingResult(pwd, "userPwd");
validator.validate(pwd, errors);
❌ 하면 안 되는 방식
Errors errors = new Errors(); // ❌ 인터페이스라 불가
Errors errors = null; // ❌ NPE



