[SPRING] Bean Validation

๋ฆผ๋ฏผ์ง€ยท2025๋…„ 3์›” 28์ผ

Today I Learn

๋ชฉ๋ก ๋ณด๊ธฐ
35/62

๐Ÿฅœ Bean Validation

๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ๋งค๋ฒˆ BingdingResult์ฒ˜๋Ÿผ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•˜๋Š”๊ฑด ์ข€ ํž˜๋“ค๋‹ค... ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์ž‘์€ ํ”„๋กœ์ ํŠธ๋Š” ๊ดœ์ฐฎ์ง€๋งŒ, ์กฐ๊ธˆ๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ ธ๋„ Controller์˜ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด๋„ˆ๋ฌด ์ปค์ง€๋ฉฐ ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์— ์œ„๋ฐฐ๋œ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‚˜์˜จ ๋ฐฉ๋ฒ•์ด ๋ฐ”๋กœ โญ๏ธBean Validationโญ๏ธ
: ์ œ์•ฝ ์กฐ๊ฑด์„ ์„ค์ •ํ•ด ์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ‘œ์ค€ํ™”๋œ ๋ฐฉ๋ฒ•

  1. Bean Validation์€ ๊ธฐ์ˆ  ํ‘œ์ค€ ์ธํ„ฐํŽ˜์ด์Šค
  2. ๋‹ค์–‘ํ•œ Annotation๋“ค๊ณผ ์—ฌ๋Ÿฌ๊ฐ€์ง€ Interface๋กœ ๊ตฌ์„ฑ
    : Bean Validation(์ธํ„ฐํŽ˜์ด์Šค) ๊ตฌํ˜„์ฒด์ธ Hibernate Validator๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์ž˜ ๋ชจ๋ฅผ๋• ์–ธ์ œ๋‚˜ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์ž!
๐Ÿ“„ Hibernate Validator ๊ณต์‹ ๋ฌธ์„œ

๋จผ์ € ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผํ•œ๋‹ค

//build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'

Annotation๋“ค

Annotation์กฐ๊ฑด
@NotNullnull โŒ / ๊ณต๋ฐฑ(โ€ โ€œ) โŒ / ๋นˆ๊ฐ’(โ€โ€) โŒ / CharSequence ํƒ€์ž… ํ—ˆ์šฉ
@NotBlanknull โŒ / ๋ชจ๋“  ํƒ€์ž…
@NotEmptynull โŒ / ๋นˆ๊ฐ’(โ€โ€) โŒ / CharSequence, Collection, Map, Array ํ—ˆ์šฉ

@Range(min = 1, max = 120) ์™€ ๊ฐ™์€
Annotation์„ ์ ์šฉ์‹œํ‚ค๋Š”๊ฒƒ ๋งŒ์œผ๋กœ Validation์„ ํ•ต์‰ฝ๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค!!( ๊ฐธ๋€ฐ)

@Data
public class TestDto{
	@notBlank
    private String stringField;
    
    @NotNull
    @Range(min = 1, max = 9999)
    private Integer integerField;

๋‹จ์ˆœํžˆ Annotation์„ ์„ ์–ธํ•ด์ฃผ๋ฉด ๊ฒ€์ฆ์ด ์™„๋ฃŒ๋˜๋Š” ์ด์œ ๋Š” Validator๊ฐ€ ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ
Spring Boot๋Š” validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์ •ํ•˜๋ฉด
'org.springframework.boot:spring-boot-starter-validation' ์ž๋™์œผ๋กœ Bean Validator๋ฅผ
Spring์— ํ†ตํ•ฉ๋˜๋„๋ก ์„ค์ •ํ•ด์ค€๋‹ค!

@Valid vs @Validated

AnnotationํŠน์ง•
@ValidJava ํ‘œ์ค€ ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜
@ValidatedSpring์—์„œ ์ œ๊ณตํ•˜๋Š” ํ™•์žฅ ์–ด๋…ธํ…Œ์ด์…˜, Group Validation ์ง€์›

@Validated๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŠน์ • ๊ทธ๋ฃน์„ ์ง€์ •ํ•˜์—ฌ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ ๊ฐ€๋Šฅ!

๐Ÿ’ญ ์˜ค๋ฅ˜ Message ์ˆ˜์ •ํ•˜๊ธฐ

Bean Validation์„ ์ ์šฉํ•œ ํ›„ ๊ฒ€์ฆ์˜ค๋ฅ˜ ํ™•์ธํ•˜๋ฉด ์˜ค๋ฅ˜๊ฐ€ ์–ด๋…ธํ…Œ์ด์…˜ ์ด๋ฆ„์œผ๋กœ ๋“ฑ๋ก๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋”ฐ.
๊ทผ๋ฐ ๋‚˜๋Š” ๋‹ค๋ฅธ ๋ฉ”์„ธ์ง€๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ์‹ถ์„ ์ˆ˜๋„ ์žˆ์ž๋‚˜..?

๊ทธ๋Ÿด๋• message ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์ž

@Data
public class TestDto {
	@NotBlank(message = "๋ฉ”์„ธ์ง€ ์ˆ˜์ • ๊ฐ€๋Šฅ")
	private String stringField;
}

groups vs DTO ๋ถ„๋ฆฌ

๋งŒ์•ฝ postํ• ๋•Œ๋Š” ๊ฐ€๊ฒฉ์˜ ๋ฒ”์œ„๊ฐ€ 100์ด์ƒ์ด์–ด์•ผํ•˜๋Š”๋ฐ, put(์ˆ˜์ •)ํ• ๋•Œ๋Š” ๊ฐ€๊ฒฉ ๋ฒ”์œ„๊ฐ€ ์ƒ๊ด€์—†์–ด๋„ ๋œ๋‹ค๋ฉด,, ๋ฒ”์œ„ ์„ค์ •์„ ๋”ฐ๋กœ ํ•ด์ฃผ์–ด์•ผํ•  ๊ฒƒ์ด๋‹ค

์ด๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ Spring์˜ groups ๊ธฐ๋Šฅ๊ณผ DTO๋ฅผ ๋”ฐ๋กœ ๋ถ„๋ฆฌํ•ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค!

1. groups ์‚ฌ์šฉํ•˜๊ธฐ

// ์ €์žฅ์šฉ group
public interface SaveCheck {
}
// ์ˆ˜์ •์šฉ group
public interface UpdateCheck {
}
@Data
public class ProductRequestDtoV2 {
	// ์ €์žฅ, ์ˆ˜์ • @NotBlank Validation ์ ์šฉ
	@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
	private String name;
	// ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“ ๊ณณ์—์„œ @NotNull Validation ์ ์šฉ, ์ €์žฅ๋งŒ @Range ๋ฐ˜์˜
    @NotNull
	@Range(min = 10, max = 10000, groups = SaveCheck.class)
	private Integer price;
 ...
 
@RestController
public class ProductController {
	@PostMapping("/v2/product")
	public String save(
	// ์ €์žฅ ์†์„ฑ๊ฐ’ ์„ค์ •
	@Validated(SaveCheck.class) @ModelAttribute ProductReques
) {
		log.info("์ƒ์„ฑ API๊ฐ€ ํ˜ธ์ถœ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
		// Validation ์„ฑ๊ณต์‹œ repository ์ €์žฅ๋กœ์ง ํ˜ธ์ถœ
		return "์ƒํ’ˆ ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค";
	}
	@PutMapping("/v2/product/{id}")
	public String update(
		@PathVariable Long id,
		// ์ˆ˜์ • ์†์„ฑ๊ฐ’ ์„ค์ •
		@Validated(UpdateCheck.class) 
        ...

groups๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋”ฐ๋กœ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ์ง€๋งŒ, UpdateCheck, SaveCheck์ฒ˜๋Ÿผ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์„œ ๊ฐ๊ฐ ์ ์šฉํ•ด์ฃผ์–ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์ด ๋งค์šฐ๋งค์šฐ ๋–จ์–ด์ง„๋‹คใ… ใ… 

๋งŒ์•ฝ groups ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด @Valid๊ฐ€ ์•„๋‹Œ@Validated๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜์ž (groups๋Š” ์ž๋ฐ”๊ฐ€ ์•„๋‹ˆ๋ผ ์Šคํ”„๋ง์˜ ๊ธฐ๋Šฅ์ด๊ธฐ ๋•Œ๋ฌธ)

๋  ์ˆ˜ ์žˆ์œผ๋ฉด DTO๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜์ž

2. ๐Ÿ‘ DTO ๋ถ„๋ฆฌ
๋„ค์ด๋ฐ์€ ์ผ๊ด€์„ฑ์žˆ๊ฒŒ ์ž‘์„ฑํ•˜๊ธฐ(SaveRequestDto, UpdateRequestDto)
์›ฌ๋งŒํ•˜๋ฉด DTO๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ž‘์„ฑํ•˜๋Š”๊ฒŒ ์ œ์ผ ์ข‹๋‹ค!!

โœ… @ModelAttribute vs @RequestBody ๊ฒ€์ฆ ํ๋ฆ„ ์ฐจ์ด

๊ตฌ๋ถ„@ModelAttribute@RequestBody
๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉํผ ๋ฐ์ดํ„ฐ (x-www-form-urlencoded, multipart/form-data)JSON (application/json)
๊ฒ€์ฆ๋ฐฉ์‹BindingResult ์‚ฌ์šฉ ๊ฐ€๋Šฅ (FieldError, ObjectError ์ง์ ‘ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ)@Valid ๋˜๋Š” @RequestBody ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ
์ปจํŠธ๋กค๋Ÿฌ ํ˜ธ์ถœ ์—ฌ๋ถ€์˜ค๋ฅ˜ ์žˆ์–ด๋„ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ ์‹คํ–‰๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋ฉด ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Œ
์˜ค๋ฅ˜ ๋‹ด๋Š” ๊ณณBindingResult์— ์ €์žฅMethodArgumentNotValidException ๋ฐœ์ƒ
์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•bindingResult.hasErrors()๋กœ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ@ExceptionHandler ๋˜๋Š” ControllerAdvice๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•„์š”

@RequestBody์˜ ๊ฒฝ์šฐ ์˜ค๋ฅ˜๊ฐ€ ์ƒ๊ธฐ๋ฉด ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค!
โ†’ @RequestBody์˜ ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด @ExceptionHandler ๋˜๋Š” @ControllerAdvice๊ฐ€ ํ•„์š”ํ•จ.

โœ” @ModelAttribute์™€ @RequestBody ๋ชจ๋‘ FieldError์™€ ObjectError๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ

FieldError
: ํŠน์ • ํ•„๋“œ(์†์„ฑ) ์— ๋Œ€ํ•œ ๊ฒ€์ฆ ์˜ค๋ฅ˜.
์ œ์•ฝ ์กฐ๊ฑด(@NotNull, @NotBlank, @Size ๋“ฑ)์„ ์œ„๋ฐ˜ํ•˜๋ฉด ๋ฐœ์ƒ.

ObjectError

ํŠน์ • ํ•„๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฐ์ฒด ์ „์ฒด์— ๋Œ€ํ•œ ์˜ค๋ฅ˜.
๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฒ€์ฆ์— ๋งŽ์ด ์‚ฌ์šฉ๋จ (์˜ˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ, ๋‚ ์งœ ๊ฒ€์ฆ ๋“ฑ).
BindingResult.getGlobalErrors()๋กœ ํ™•์ธ ๊ฐ€๋Šฅ.

0๊ฐœ์˜ ๋Œ“๊ธ€