์ž์นด๋ฅดํƒ€ ๋ฒจ๋ฆฌ๋ฐ์ด์…˜

์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฒจ๋ฆฌ๋ฐ์ด์…˜์€ spring-boot-starter-validation ๋ชจ๋“ˆ์— ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ Jakarta Bean Validation์„ ๋”ฐ๋ฅด๋Š” Hibhernate Validator๋ฅผ ์˜์กดํ•˜๊ณ  ์žˆ๋‹ค. ์Šคํ”„๋ง ๋ถ€ํŠธ์— ์˜ํ•œ ์ž๋™ ๊ตฌ์„ฑ์€ ValidationAutoConfiguration์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋ฉฐ LocalValidatorFactoryBean๋ฅผ ๊ธฐ๋ณธ Validator ๋นˆ์œผ๋กœ์จ ๋“ฑ๋กํ•จ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

LocalValidatorFactoryBean

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ์— ์กด์žฌํ•˜๋Š” ๋ฉ”์‹œ์ง€ ์†Œ์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณด๊ฐ„์„ ์ฒ˜๋ฆฌํ•˜๋Š” MessageSourceMessageInterpolator๊ฐ€ ์‚ฌ์šฉ๋˜๋„๋ก ๊ตฌํ˜„๋˜์–ด์žˆ๋‹ค. MessageInterpolatorFactory์˜ ๋‚ด๋ถ€ ๊ตฌํ˜„์„ ์‚ดํŽด๋ณด๋ฉด ๋ฉ”์‹œ์ง€ ์†Œ์Šค๊ฐ€ ์—†๋‹ค๋ฉด ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ๋ฒจ๋ฆฌ๋ฐ์ดํ„ฐ์˜ ResourceBundleMessageInterpolator๊ฐ€ ์‚ฌ์šฉ๋˜๋‚˜ ์Šคํ”„๋ง ๋ถ€ํŠธ์— ์˜ํ•ด ๋ฉ”์‹œ์ง€ ์†Œ์Šค ์ž์ฒด๋„ ์ž๋™ ๊ตฌ์„ฑ๋˜๋ฏ€๋กœ ๊ฒฐ๊ตญ MessageSourceMessageInterpolator๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค๊ณ  ์ธ์ง€ํ•˜๋ฉด ๋œ๋‹ค.

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext,
        ObjectProvider<ValidationConfigurationCustomizer> customizers) {
    LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
    factoryBean.setConfigurationInitializer((configuration) -> customizers.orderedStream()
        .forEach((customizer) -> customizer.customize(configuration)));
    MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
    factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
    return factoryBean;
}

์ตœ์ข…์ ์œผ๋กœ๋Š” MessageSourceMessageInterpolator๊ฐ€ LocaleContextMessageInterpolator๋กœ ๋ž˜ํ•‘๋˜๋ฉฐ LocaleContextHolder.getLocale()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํ”„๋ง ์›น ์š”์ฒญ์— ์˜ํ•œ ์Šค๋ ˆ๋“œ์— ๋Œ€ํ•ด ๋กœ์ผ€์ผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฉ”์‹œ์ง€ ๋ณด๊ฐ„์ด ์ฒ˜๋ฆฌ๋˜๋„๋ก ์ง€์›ํ•œ๋‹ค.

๊ธฐ๋ณธ ๋ฒจ๋ฆฌ๋ฐ์ด์…˜ ๋ฉ”์‹œ์ง€

ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ๋ฒจ๋ฆฌ๋ฐ์ดํ„ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋‚ด์—๋Š” ๊ธฐ๋ณธ์œผ๋กœ ์ •์˜๋œ ValidationMessages๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ, @NotEmpty ๋˜๋Š” @Length, @Email ๊ณผ ๊ฐ™์€ ์ž์นด๋ฅดํƒ€ ๋ฒจ๋ฆฌ๋ฐ์ด์…˜์— ์ •์˜๋˜์–ด์žˆ๋Š” Constraints์— ๋Œ€ํ•ด์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฉ”์‹œ์ง€ ๋ณด๊ฐ„์œผ๋กœ ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ๋‹ค.

org.hibernate.validator.ValidationMessages

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ

Validation ์–ด๋””๊นŒ์ง€ ํ•ด๋ดค๋‹ˆ?์—์„œ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด ์ž์นด๋ฅดํƒ€ ๋ฒจ๋ฆฌ๋ฐ์ด์…˜์˜ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฒจ๋ฆฌ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ž๋ฐ” ๋นˆ ํด๋ž˜์Šค์— ์ €์žฅ๋œ ์ •๋ณด๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ•„๋“œ ๊ฒ€์ฆ์„ ์‰ฝ๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ์Šคํ”„๋ง ๋ถ€ํŠธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ž๋™ ๊ตฌ์„ฑ๋˜๋Š” LocalValidatorFactoryBean๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

@Service
public class LoginService {
    public boolean validate(Account.Login login) {
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Account.Login>> constraintViolations = validator.validate(login);
        return constraintViolations.isEmpty();
    }
}

์‚ฌ์šฉ์ž ์ •์˜ ๋ฉ”์‹œ์ง€ ์ œ์•ฝ์‚ฌํ•ญ

์Šคํ”„๋ง ๋ฉ”์‹œ์ง€ ์†Œ์Šค์— ์‚ฌ์šฉ์ž ์ •์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ง€์ •ํ•˜๋”๋ผ๋„ ๋ฉ”์‹œ์ง€ ํŒจํ„ด์—์„œ {value}์™€ ๊ฐ™์€ ํŒŒ๋ผ๋ฏธํ„ฐ ํ‘œํ˜„์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์–ด๋–ค ๊ฐ’์— ๋Œ€ํ•œ ๊ธธ์ด๋ฅผ ์ œํ•œํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์—ฌ @Min ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ€์—ฌํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž.

<messages>
    <entry key="javax.validation.constraints.Min.message">
        <ko_KR><![CDATA[(์‚ฌ์šฉ์ž ์ •์˜) {value} ๋ณด๋‹ค ๊ฐ™๊ฑฐ๋‚˜ ์ปค์•ผํ•ฉ๋‹ˆ๋‹ค]]></ko_KR>
        <en_US><![CDATA[(Custom) must be greater than or equal to {value}]]></en_US>
    </entry>
</messages>
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is javax.validation.ValidationException: HV000149: An exception occurred during message interpolation
...
Caused by: java.lang.IllegalArgumentException: can't parse argument number: value
	at java.base/java.text.MessageFormat.makeFormat(MessageFormat.java:1451) ~[na:na]
	at java.base/java.text.MessageFormat.applyPattern(MessageFormat.java:491) ~[na:na]
	at java.base/java.text.MessageFormat.<init>(MessageFormat.java:390) ~[na:na]
...
Caused by: java.lang.NumberFormatException: For input string: "value"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:na]
	at java.base/java.lang.Integer.parseInt(Integer.java:652) ~[na:na]
	at java.base/java.lang.Integer.parseInt(Integer.java:770) ~[na:na]
	at java.base/java.text.MessageFormat.makeFormat(MessageFormat.java:1449) ~[na:na]

์ด๋Ÿฌํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ํ‘œํ˜„์€ Hibernate Validator์—์„œ ์ง€์›ํ•˜๋Š” ํ‘œํ˜„์‹์ด๋ฏ€๋กœ MessageFormat์œผ๋กœ ์ง€์›ํ•˜๋Š” ํ‘œํ˜„์‹๊ณผ๋Š” ๋‹ค๋ฅด๋‹ค. MessageFormat๋Š” {0}, {1} ์ด๋ ‡๊ฒŒ ์ˆซ์ž ๊ธฐ๋ฐ˜์˜ ํ‘œํ˜„์‹์„ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ‘œํ˜„์‹ ์ œ์•ฝ์ด ๋ฐœ์ƒํ•œ๋‹ค. ์•„์‰ฌ์šด ๋ถ€๋ถ„์ด์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •์˜ ๋ฉ”์‹œ์ง€์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค๋ฉด ValidationMessages ๋ฆฌ์†Œ์Šค ๋ฒˆ๋“ค์„ ํด๋ž˜์ŠคํŒจ์Šค์— ์žฌ์ •์˜ํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ๋ฐ–์— ์—†์„ ๊ฒƒ ๊ฐ™๋‹ค.