From 919028c3110b0473603c46246aad3d3d97ae9188 Mon Sep 17 00:00:00 2001 From: z3rotig4r Date: Thu, 30 Jul 2026 09:10:38 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20reactive=20=EA=B2=80=EC=A6=9D=EA=B8=B0?= =?UTF-8?q?=20=EC=A0=95=EA=B7=9C=EC=8B=9D=20=EA=B2=80=EC=82=AC=EB=A5=BC=20?= =?UTF-8?q?matches()=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=B4=20=ED=96=89?= =?UTF-8?q?=EC=A2=85=EA=B2=B0=EC=9E=90=20=EC=9A=B0=ED=9A=8C=20=EC=B0=A8?= =?UTF-8?q?=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reactive validation 패키지의 검증기 6종이 정규식 검사에 Matcher.find()를 사용한다. 패턴은 "^...$" 형태인데 Java 정규식의 $는 입력 끝 또는 최종 행종결자 앞에서 매치되므로, 행종결자(\n, \r\n, U+0085, U+2028, U+2029)로 끝나는 입력이 패턴 게이트를 통과한다. - EgovCnCheckValidation: "1101110000002\n" -> NumberFormatException 전파 - EgovRrnCheckValidation: "9913321123459\n" -> NumberFormatException 전파 - EgovMobilePhoneCheckValidation: "01012345678\n" -> true - EgovPhoneCheckValidation: "0212345678\n" -> true - EgovEnglishCheckValidation: "abc\n" -> true - EgovKoreanCheckValidation: "가나다\n" -> true Cn/Rrn은 패턴을 통과한 뒤 마지막 자리를 Integer.parseInt로 파싱하므로 NumberFormatException이 발생한다. ConstraintValidator가 예외를 던지면 false 반환이 아니라 ValidationException으로 전파되어 400이 아닌 500 응답이 된다. 입력은 사용자 제어값이다. find()를 전체 일치를 요구하는 matches()로 바꿔 행종결자로 끝나는 입력을 거부한다. 패턴 문자열은 변경하지 않았다. matches()에서는 ^/$ 앵커가 남아 있어도 무해하다. 수정 전후 판정을 실측 비교한 결과 정상값·하이픈 포함 값·앞뒤 공백·탭 종료· 길이 초과/미달·빈 문자열 판정은 모두 동일하고, 행종결자로 끝나는 입력만 false로 바뀐다. ReactiveValidatorsLineTerminatorTest로 6종의 행종결자 거부와 정상값 무회귀를 단언한다. --- .../validation/EgovCnCheckValidation.java | 2 +- .../EgovEnglishCheckValidation.java | 2 +- .../validation/EgovKoreanCheckValidation.java | 2 +- .../EgovMobilePhoneCheckValidation.java | 2 +- .../validation/EgovPhoneCheckValidation.java | 2 +- .../validation/EgovRrnCheckValidation.java | 2 +- .../ReactiveValidatorsLineTerminatorTest.java | 93 +++++++++++++++++++ 7 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsLineTerminatorTest.java diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCnCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCnCheckValidation.java index 611cd8a3..aa2c73cd 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCnCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCnCheckValidation.java @@ -48,7 +48,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { } String mValue = value.replaceAll("-", ""); Matcher matcher = CN_PATTERN.matcher(mValue); - boolean check = matcher.find(); + boolean check = matcher.matches(); if (!check) { return false; } diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovEnglishCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovEnglishCheckValidation.java index eac39d70..df05a2c3 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovEnglishCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovEnglishCheckValidation.java @@ -47,7 +47,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { return false; } Matcher matcher = ENGLISH_PATTERN.matcher(value); - return matcher.find(); + return matcher.matches(); } } diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovKoreanCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovKoreanCheckValidation.java index 92eb55eb..7276d0d5 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovKoreanCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovKoreanCheckValidation.java @@ -47,7 +47,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { return false; } Matcher matcher = KOREAN_PATTERN.matcher(value); - return matcher.find(); + return matcher.matches(); } } diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovMobilePhoneCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovMobilePhoneCheckValidation.java index 94e61487..102b3774 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovMobilePhoneCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovMobilePhoneCheckValidation.java @@ -48,7 +48,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { } String mValue = value.replaceAll("-", ""); Matcher matcher = MOBILE_PHONE_PATTERN.matcher(mValue); - return matcher.find(); + return matcher.matches(); } } diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovPhoneCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovPhoneCheckValidation.java index dd2cce15..b4b6a6a6 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovPhoneCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovPhoneCheckValidation.java @@ -48,7 +48,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { } String mValue = value.replaceAll("-", ""); Matcher matcher = PHONE_PATTERN.matcher(mValue); - return matcher.find(); + return matcher.matches(); } } diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovRrnCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovRrnCheckValidation.java index 30420496..050a4762 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovRrnCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovRrnCheckValidation.java @@ -48,7 +48,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { } String mValue = value.replaceAll("-", ""); Matcher matcher = RRN_PATTERN.matcher(mValue); - boolean check = matcher.find(); + boolean check = matcher.matches(); if (!check) { return false; } diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsLineTerminatorTest.java b/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsLineTerminatorTest.java new file mode 100644 index 00000000..9f69075f --- /dev/null +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsLineTerminatorTest.java @@ -0,0 +1,93 @@ +package org.egovframe.rte.ptl.reactive.validation; + +import jakarta.validation.ConstraintValidator; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Annotation; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 행종결자로 끝나는 입력을 예외 없이 false로 반환하는지 검증한다. + * + *

대상은 정규식 검사를 matches()로 전환한 검증기 6종(법인등록번호·주민등록번호· + * 휴대전화번호·일반전화번호·영문·한국어)이다. 이미 matches()를 사용하는 + * EgovCrnCheckValidation·EgovPwdCheckValidation·EgovEmailCheckValidation· + * EgovIPCheckValidation은 이 테스트의 대상이 아니다.

+ */ +class ReactiveValidatorsLineTerminatorTest { + + @Test + void validators_lineTerminatorInput_returnFalseWithoutException() { + assertAll( + () -> assertLineTerminatorRejected(new EgovCnCheckValidation(), "1101110000002"), + () -> assertLineTerminatorRejected(new EgovRrnCheckValidation(), "9913321123459"), + () -> assertLineTerminatorRejected(new EgovMobilePhoneCheckValidation(), "01012345678"), + () -> assertLineTerminatorRejected(new EgovPhoneCheckValidation(), "0212345678"), + () -> assertLineTerminatorRejected(new EgovEnglishCheckValidation(), "abc"), + () -> assertLineTerminatorRejected(new EgovKoreanCheckValidation(), "가나다")); + } + + @Test + void cnAndRrn_lineFeedInput_doNotThrowNumberFormatException() { + assertFalse(assertDoesNotThrow(() -> new EgovCnCheckValidation().isValid("1101110000002\n", null), + "법인등록번호 검증기는 개행 입력에서 NumberFormatException을 전파하면 안 된다"), + "법인등록번호 검증기는 개행으로 끝나는 입력에 false를 반환해야 한다"); + assertFalse(assertDoesNotThrow(() -> new EgovRrnCheckValidation().isValid("9913321123459\n", null), + "주민등록번호 검증기는 개행 입력에서 NumberFormatException을 전파하면 안 된다"), + "주민등록번호 검증기는 개행으로 끝나는 입력에 false를 반환해야 한다"); + } + + @Test + void cn_unicodeLineSeparatorInput_returnFalseWithoutException() { + boolean result = assertDoesNotThrow(() -> new EgovCnCheckValidation().isValid("1101110000002\u2028", null), + "법인등록번호 검증기는 유니코드 행종결자 입력에서 예외를 던지면 안 된다"); + assertFalse(result, "법인등록번호 검증기는 유니코드 행종결자로 끝나는 입력에 false를 반환해야 한다"); + } + + @Test + void validators_stillValidateNormalInput() { + assertTrue(new EgovCnCheckValidation().isValid("1101110000002", null), "법인등록번호 검증기는 정상값에 true"); + assertTrue(new EgovCnCheckValidation().isValid("110111-0000002", null), "법인등록번호 검증기는 하이픈 포함 정상값에 true"); + assertFalse(new EgovCnCheckValidation().isValid("1101110000003", null), "법인등록번호 검증기는 체크숫자 오류에 false"); + + assertTrue(new EgovRrnCheckValidation().isValid("9913321123459", null), "주민등록번호 검증기는 정상값에 true"); + assertTrue(new EgovRrnCheckValidation().isValid("991332-1123459", null), "주민등록번호 검증기는 하이픈 포함 정상값에 true"); + assertFalse(new EgovRrnCheckValidation().isValid("9913321123458", null), "주민등록번호 검증기는 체크숫자 오류에 false"); + + assertTrue(new EgovMobilePhoneCheckValidation().isValid("01012345678", null), "휴대전화번호 검증기는 정상값에 true"); + assertTrue(new EgovMobilePhoneCheckValidation().isValid("010-1234-5678", null), "휴대전화번호 검증기는 하이픈 포함 정상값에 true"); + assertFalse(new EgovMobilePhoneCheckValidation().isValid("01512345678", null), "휴대전화번호 검증기는 잘못된 식별번호에 false"); + + assertTrue(new EgovPhoneCheckValidation().isValid("0212345678", null), "일반전화번호 검증기는 정상값에 true"); + assertTrue(new EgovPhoneCheckValidation().isValid("02-1234-5678", null), "일반전화번호 검증기는 하이픈 포함 정상값에 true"); + assertFalse(new EgovPhoneCheckValidation().isValid("021234567890", null), "일반전화번호 검증기는 길이 오류에 false"); + + assertTrue(new EgovEnglishCheckValidation().isValid("abc", null), "영문 검증기는 소문자에 true"); + assertTrue(new EgovEnglishCheckValidation().isValid("ABC", null), "영문 검증기는 대문자에 true"); + assertFalse(new EgovEnglishCheckValidation().isValid("abc1", null), "영문 검증기는 숫자 포함 값에 false"); + assertFalse(new EgovEnglishCheckValidation().isValid("한글", null), "영문 검증기는 한글에 false"); + + assertTrue(new EgovKoreanCheckValidation().isValid("가나다", null), "한국어 검증기는 완성형 한글에 true"); + assertTrue(new EgovKoreanCheckValidation().isValid("ㄱㄴㄷ", null), "한국어 검증기는 한글 자모에 true"); + assertFalse(new EgovKoreanCheckValidation().isValid("가나1", null), "한국어 검증기는 숫자 포함 값에 false"); + assertFalse(new EgovKoreanCheckValidation().isValid("abc", null), "한국어 검증기는 영문에 false"); + } + + private static void assertLineTerminatorRejected( + ConstraintValidator validator, String validValue) { + assertRejected(validator, validValue + "\n"); + assertRejected(validator, validValue + "\r\n"); + } + + private static void assertRejected( + ConstraintValidator validator, String value) { + boolean result = assertDoesNotThrow(() -> validator.isValid(value, null), + validator.getClass().getSimpleName() + "는 행종결자 입력에서 예외를 던지면 안 된다"); + assertFalse(result, validator.getClass().getSimpleName() + "는 행종결자로 끝나는 입력에 false를 반환해야 한다"); + } + +}