Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.example.solidconnection.auth.dto;

import com.example.solidconnection.auth.dto.validation.Password;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record EmailSignInRequest(

@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "유효한 이메일 주소를 입력해주세요.")
String email,

@Password
@NotBlank(message = "비밀번호를 입력해주세요.")
String password
Comment on lines +13 to 15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

비밀번호 정책 추가는 OK, “어노테이션 순서 민감도”는 그룹 시퀀스로 해결하세요.

    1. 현상 원인 설명: Bean Validation은 동일 필드의 제약 평가 “순서”를 보장하지 않으며, Hibernate Validator에 fail_fast가 켜져 있으면 첫 위반에서 즉시 중단합니다. 코드상 어노테이션 배치 순서가 검증 순서처럼 보일 수 있으나 이는 구현/리플렉션 차원에서 우연히 고정된 것처럼 보이는 효과입니다.
    1. 재현 조건 추정: PasswordValidator가 null/blank를 허용하지 않거나, fail_fast=true인 경우 어노테이션 배치에 따라 위반 메시지가 달라지거나 기대와 다른 동작처럼 보일 수 있습니다.
    1. 해법 1(권장): 검증 순서를 “그룹 시퀀스”로 고정합니다. RequiredChecks(입력 필수) → PasswordPolicyChecks(복잡도) 순으로 강제하면 어노테이션 배치와 무관해집니다.
    1. 해법 2(보완): PasswordValidator가 null/blank 입력일 때 true를 반환하도록 하여 “입력 여부는 @notblank, 복잡도는 @password”로 역할을 명확히 분리합니다.
    1. 해법 3(환경 설정): fail_fast를 사용하지 않거나(기본 false), 사용한다면 그룹 시퀀스를 반드시 함께 적용합니다.

아래는 DTO 필드의 그룹 지정 예시입니다.

-        @Password
-        @NotBlank(message = "비밀번호를 입력해주세요.")
+        @NotBlank(message = "비밀번호를 입력해주세요.", groups = RequiredChecks.class)
+        @Password(groups = PasswordPolicyChecks.class)
         String password

그리고 그룹/시퀀스 정의 예시입니다(패키지 내 공용 인터페이스로 두고 재사용 권장).

// 공용 그룹
public interface RequiredChecks {}
public interface PasswordPolicyChecks {}

// 검증 순서 고정
import jakarta.validation.GroupSequence;
@GroupSequence({RequiredChecks.class, PasswordPolicyChecks.class})
public interface ValidationSequence {}

컨트롤러 바인딩 시 그룹 시퀀스를 사용하세요.

@PostMapping("/sign-in")
public ResponseEntity<?> signIn(@Validated(ValidationSequence.class) @RequestBody EmailSignInRequest req) { ... }

PasswordValidator 보완 예시(입력 공백 위임):

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null || value.isBlank()) return true; // 입력 여부는 @NotBlank에 위임
    return PATTERN.matcher(value).matches();
}
    1. 테스트 제안: 어노테이션 순서를 바꿔도 결과가 동일함을 보장하는 파라미터라이즈드 테스트를 추가하세요.

다음 스크립트로 환경/구현 상태를 점검해 주세요.

원하시면 그룹/시퀀스 도입 PR 보조 커밋과 테스트 케이스(순서 의존성 제거)를 바로 생성해드리겠습니다.


🏁 Script executed:

#!/bin/bash
# 1) fail-fast 설정 확인
rg -n -S 'hibernate\.validator\.fail_fast\s*[:=]\s*true' -g 'application*.{yml,properties}'

# 2) PasswordValidator의 공백 처리 확인
rg -n --hidden -S "class\s+PasswordValidator|isValid\(|isBlank\(" -A 8 -B 2

# 3) @Validated에서 그룹/시퀀스 사용 여부 확인
rg -n "@Validated\(" -A 2

# 4) @Password 사용 위치 전수 스캔
rg -n "@Password" -A 2

Length of output: 12161


그룹 시퀀스로 검증 순서를 고정하세요

  1. 그룹/시퀀스 정의
    RequiredChecksPasswordPolicyChecks 인터페이스를 만들고
    @GroupSequence({RequiredChecks.class, PasswordPolicyChecks.class})로 순서를 지정합니다.

  2. DTO 필드에 그룹 지정
    EmailSignInRequestEmailSignUpTokenRequestpassword 필드에
    diff - @Password - @NotBlank(message = "비밀번호를 입력해주세요.") + @NotBlank(message = "비밀번호를 입력해주세요.", groups = RequiredChecks.class) + @Password(groups = PasswordPolicyChecks.class)
    를 적용하세요.

  3. 컨트롤러에 시퀀스 적용
    /sign-in 등 관련 엔드포인트 메서드에
    @Validated(ValidationSequence.class)를 붙여 그룹 시퀀스를 활성화합니다.

  4. 순서 의존성 제거 테스트 추가
    • 어노테이션 배치 순서를 바꿔도 동일한 검증 결과가 나오는지 검증하는 파라미터라이즈드 테스트를 작성하세요.

추가 참고

  • PasswordValidator는 이미 null·빈 문자열을 허용하도록(return true) 구현되어 있어 @NotBlank에 입력 여부 체크를 위임하고 있습니다.
  • hibernate.validator.fail_fast 설정은 기본값(false)으로 확인되어 있으며, 그룹 시퀀스 도입으로 모든 순서 이슈를 완벽히 제거할 수 있습니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Password
@NotBlank(message = "비밀번호를 입력해주세요.")
String password
@NotBlank(message = "비밀번호를 입력해주세요.", groups = RequiredChecks.class)
@Password(groups = PasswordPolicyChecks.class)
String password
🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java
around lines 13 to 15, you need to enforce a fixed validation order using group
sequences. Define two interfaces, RequiredChecks and PasswordPolicyChecks, then
create a ValidationSequence interface annotated with
@GroupSequence({RequiredChecks.class, PasswordPolicyChecks.class}). Annotate the
password field in EmailSignInRequest and EmailSignUpTokenRequest with the
appropriate groups to reflect these interfaces. In the controller methods
handling endpoints like /sign-in, add @Validated(ValidationSequence.class) to
activate the group sequence validation. Finally, add parameterized tests to
verify that changing the annotation order does not affect validation results.

) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.example.solidconnection.auth.dto;

import com.example.solidconnection.auth.dto.validation.Password;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record EmailSignUpTokenRequest(

@Email(message = "이메일을 입력해주세요.")
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "유효한 이메일 주소를 입력해주세요.")
String email,

@Password
@NotBlank(message = "비밀번호를 입력해주세요.")
String password
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.solidconnection.auth.dto.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
public @interface Password {

String message() default "비밀번호는 영문, 숫자, 특수문자를 포함한 8자리 이상이어야 합니다.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.solidconnection.auth.dto.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class PasswordValidator implements ConstraintValidator<Password, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isBlank()) {
return true;
}

return value.matches("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-={}\\[\\]|:;\"'<>,.?/`~])\\S{8,}$");
}
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

[3] 정규식 퍼포먼스 개선과 정책 명확화 제안(사전 컴파일 + 유니코드 옵션)

  • String.matches는 호출마다 패턴을 컴파일합니다. 성능과 가독성을 위해 Pattern을 static final로 사전 컴파일하는 편이 좋습니다.
  • 현재 [A-Za-z]는 라틴 알파벳만 허용합니다. 한국어 등 유니코드 문자를 ‘문자’로 인정하려면 \p{L}을 고려하세요.
  • 공백 허용 여부도 정책으로 확정 필요합니다. 지금은 \S 사용으로 공백(스페이스/탭 등)을 금지합니다.
  • 참고: String.matches는 전체 매치를 전제하므로 ^/$는 중복이지만, 가독성 차원에서 유지해도 무방합니다.

적용 diff(ASCII 정책 유지, 사전 컴파일):

@@
-import jakarta.validation.ConstraintValidator;
-import jakarta.validation.ConstraintValidatorContext;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import java.util.regex.Pattern;
@@
-public class PasswordValidator implements ConstraintValidator<Password, String> {
+public class PasswordValidator implements ConstraintValidator<Password, String> {
+
+    private static final Pattern PASSWORD_PATTERN = Pattern.compile(
+        "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-={}\\[\\]|:;\\\"'<>,.?/`~])\\S{8,}$"
+    );
@@
-        return value.matches("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-={}\\[\\]|:;\"'<>,.?/`~])\\S{8,}$");
+        return PASSWORD_PATTERN.matcher(value).matches();

유니코드 ‘문자’ 허용(선택사항) 예시:

  • 최소 하나의 유니코드 문자: (?=.*\p{L})
  • 최소 하나의 숫자: (?=.*\d)
  • 최소 하나의 특수문자: (?=.*[\p{Punct}\p{S}])
  • 공백 금지 및 길이: \S{8,}
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
  "^(?=.*\\p{L})(?=.*\\d)(?=.*[\\p{Punct}\\p{S}])\\S{8,}$"
);

추가로 테스트 케이스 보강을 권장합니다.

  • 길이 경계(정확히 8자).
  • 공백 포함/미포함.
  • 한글/이모지 포함 시 정책 의도에 맞는지.
  • 백슬래시(\) 등 현재 특수문자 집합에 없는 문자의 취급.

정규식 성능 최적화 및 정책 명확화 제안

  1. 정규식 패턴 사전 컴파일
    • String.matches는 호출할 때마다 패턴을 컴파일하므로 매번 오버헤드가 발생합니다.
    • 이를 방지하려면 static final Pattern으로 사전 컴파일하세요.
    • 예시 diff:
      -import jakarta.validation.ConstraintValidator;
      -import jakarta.validation.ConstraintValidatorContext;
      +import jakarta.validation.ConstraintValidator;
      +import jakarta.validation.ConstraintValidatorContext;
      +import java.util.regex.Pattern;
       
       public class PasswordValidator implements ConstraintValidator<Password, String> {
       
      -    public boolean isValid(String value, ConstraintValidatorContext context) {
      -        return value.matches("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-={}\\[\\]|:;\"'<>,.?/`~])\\S{8,}$");
      +    private static final Pattern PASSWORD_PATTERN = Pattern.compile(
      +        "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-={}\\[\\]|:;\\\"'<>,.?/`~])\\S{8,}$"
      +    );
      +
      +    public boolean isValid(String value, ConstraintValidatorContext context) {
      +        return PASSWORD_PATTERN.matcher(value).matches();
          }
  2. 유니코드 문자 허용 고려
    • [A-Za-z]는 라틴 알파벳만 허용합니다.
    • 한국어·한자·이모지 등 모든 언어 문자를 포함하려면 \p{L}을 사용하세요.
    • 예시:
      private static final Pattern PASSWORD_PATTERN = Pattern.compile(
        "^(?=.*\\p{L})(?=.*\\d)(?=.*[\\p{Punct}\\p{S}])\\S{8,}$"
      );
  3. 공백 처리 정책 확정
    • 현재 \S를 사용해 공백(스페이스·탭 등)을 금지하고 있습니다.
    • 실제 서비스 정책에 따라 공백 허용 여부를 명확히 결정하세요.
  4. 테스트 케이스 보강
    • 길이 경계(정확히 8자) 확인 테스트
    • 공백 포함/미포함 시나리오
    • 한글·이모지 포함 시 정책 일치 여부
    • 백슬래시(\) 등 특수문자 처리 검증

위 제안을 반영해 보시고, 패턴 변경이 전체 검증 로직에 미치는 영향을 함께 확인하세요.

🤖 Prompt for AI Agents
In
src/main/java/com/example/solidconnection/auth/dto/validation/PasswordValidator.java
around lines 14-15, the inline String.matches call compiles the regex on every
invocation and restricts letters to ASCII; precompile the regex as a static
final Pattern and use Pattern.matcher(value).matches() instead, replace [A-Za-z]
with \p{L} and use \p{Punct}\p{S} (or an agreed set of allowed special chars) to
allow Unicode letters and symbols, decide and document whether whitespace is
allowed (currently \S forbids it) and adjust the pattern accordingly, and add
unit tests for length boundary (exactly 8), whitespace allowed/forbidden,
Unicode letters (Korean/Chinese), emojis, and tricky special chars to validate
the new policy.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example.solidconnection.auth.dto.validation;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("비밀번호 유효성 검사 테스트")
class PasswordValidatorTest {

private final PasswordValidator validator = new PasswordValidator();

@Test
void 정상_패턴이면_true를_반환한다() {
assertThat(validator.isValid("abcd123!", null)).isTrue();
}

@Test
void 숫자가_없으면_false를_반환한다() {
assertThat(validator.isValid("abcdefg!", null)).isFalse();
}

@Test
void 영문자가_없으면_false를_반환한다() {
assertThat(validator.isValid("1234567!", null)).isFalse();
}

@Test
void 특수문자가_없으면_false를_반환한다() {
assertThat(validator.isValid("abcd1234", null)).isFalse();
}

@Test
void 공백을_포함하면_false를_반환한다() {
assertThat(validator.isValid("abcd123! ", null)).isFalse();
}

@Test
void 길이가_8자_미만이면_false를_반환한다() {
assertThat(validator.isValid("ab1!ab", null)).isFalse();
}
}
Loading