Spring Boot 2.3 ๋ฒ์ ์ดํ๋ถํฐ๋ ๋ถํ์ํ ์์กด์ฑ์ ์ค์ด๊ธฐ ์ํด ์ ํจ์ฑ ๊ฒ์ฌ ๊ธฐ๋ฅ์ด ๊ธฐ๋ณธ ์น ์คํํฐ(spring-boot-starter-web)์์ ๋ถ๋ฆฌ๋์๋ค. ๋ฐ๋ผ์ @Valid ๊ธฐ๋ฐ์ Bean Validation์ ์ฌ์ฉํ๋ ค๋ฉด ๋ณ๋์ ์์กด์ฑ์ ์ถ๊ฐํด์ผ ํ๋ค.
// build.gradle (Gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
// pom.xml (Maven)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
ํด๋น ์คํํฐ์๋ Jakarta Bean Validation ํ์ค API์ ์ด๋ฅผ ๊ตฌํํ Hibernate Validator๊ฐ ํฌํจ๋์ด ์์ด ๋ณ๋์ ์ค์ ์์ด ๋ฐ๋ก ๊ฒ์ฆ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๋ค.
์์กด์ฑ์ ์ถ๊ฐํ ํ์๋ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์ ๋ฌ๋ฐ์ ๋ฐ์ดํฐ ๊ฐ์ฒด(DTO)์ ํ๋์ ๊ฒ์ฆ ๊ท์น์ ์ด๋
ธํ
์ด์
์ผ๋ก ์ ์ํ๋ค.
(์ฐธ๊ณ : Spring Boot 3.x ๋ฒ์ ๋ถํฐ๋ javax.validation ๋์ jakarta.validation ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ค.)
import lombok.Getter;
import lombok.NoArgsConstructor;
โ
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
@Getter
@NoArgsConstructor
public class CommentCreateRequest {
@NotNull(message = "postId๋ ํ์์ ๋๋ค.")
@Positive(message = "postId๋ 1 ์ด์์ด์ด์ผ ํฉ๋๋ค.")
private Long postId;
// null ํ์ฉ
private Long parentId;
@NotBlank(message = "๋๋ค์์ ์ ๋ ฅํ์ธ์.")
@Size(max = 10, message = "๋๋ค์์ ์ต๋ 10์์ ๋๋ค.")
private String nickname;
@NotBlank(message = "๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํ์ธ์.")
@Pattern(regexp = "^\\d{4}$", message = "๋น๋ฐ๋ฒํธ๋ 4์๋ฆฌ ์ซ์์ฌ์ผ ํฉ๋๋ค.")
private String password;
@NotBlank(message = "๋๊ธ ๋ด์ฉ์ ์ ๋ ฅํด์ฃผ์ธ์.")
@Size(max = 250, message = "๋๊ธ์ ์ต๋ 250์๊น์ง ์ ๋ ฅํ ์ ์์ต๋๋ค.")
private String content;
}
@NotNull @NotEmpty @NotBlank @Size(min, max) @Min(value) / @Max(value) @DecimalMin(value) / @DecimalMax(value) @Positive / @PositiveOrZero @Past / @PastOrPresent @Future / @FutureOrPresent @Pattern(regexp) @Email ๊ฐ ํ๋์ ํ์
์ ๋ฐ๋ผ ์ ์ฉ ๊ฐ๋ฅํ ๊ฒ์ฆ ์ด๋
ธํ
์ด์
๊ณผ ์์ฑ ์ต์
์ด ๋ค๋ฅด๋ฏ๋ก ๋ฐ์ดํฐ์ ํน์ฑ๊ณผ ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ๊ณ ๋ คํ์ฌ ์ ์ ํ ์ ์ฝ ์กฐ๊ฑด์ ์ ํํ๋ ๊ฒ์ด ์ค์ํ๋ค.
ํนํ ๋ฌธ์์ด, ์ซ์, ๋ ์ง, ์ปฌ๋ ์
๋ฑ ํ์
๋ณ๋ก ์ง์ํ๋ ์ด๋
ธํ
์ด์
์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋จ์ํ ํ์๊ฐ ์ฌ๋ถ๋ง์ด ์๋๋ผ ๊ฐ์ ๋ฒ์, ํ์, ๊ธธ์ด, ์์ ์กฐ๊ฑด๊น์ง ํจ๊ป ๊ฒํ ํด์ผ ํ๋ค.
์ฒ์ ์ฌ์ฉํ ๋ ๊ฐ์ฅ ํท๊ฐ๋ฆฌ๋ ๋ถ๋ถ์ด๊ธฐ ๋๋ฌธ์ 3๊ฐ์ง ์ด๋
ธํ
์ด์
์ ๋ํด ๋ค์ ํ ๋ฒ ์ ๋ฆฌํด๋ณด์. @NotNull
1. ๋ชจ๋ ํ์
์ ์ ์ฉ ๊ฐ๋ฅ
2. ๊ฐ์ด null์ด ์๋์ง๋ง ๊ฒ์ฌ
3. ๋ฌธ์์ด์ ๊ฒฝ์ฐ ๋น ๋ฌธ์์ด("")๊ณผ ๊ณต๋ฐฑ(" ")์ ํ์ฉ @NotEmpty
1. String, ๋ฐฐ์ด, Collection, Map ๋ฑ์ ์ ์ฉ ๊ฐ๋ฅ
2. null์ด ์๋๊ณ ๊ธธ์ด/ํฌ๊ธฐ๊ฐ 0์ด ์๋์ง ๊ฒ์ฌ
3. ๋ฌธ์์ด์ ๊ฒฝ์ฐ ๊ณต๋ฐฑ(" ")์ ํ์ฉ @NotBlank
1. String ํ์
์ ์ฉ ์ด๋
ธํ
์ด์
2. null, ๋น ๋ฌธ์์ด(""), ๊ณต๋ฐฑ๋ง ์๋ ๋ฌธ์์ด ๋ชจ๋ ํ์ฉํ์ง ์๋๋ค.
3. ๋ฌธ์์ด ํ์๊ฐ ๊ฒ์ฆ ์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ค.
DTO ์์ ๋ ๋ค๋ฅธ DTO๊ฐ ํฌํจ๋์ด ์๋ ๊ฒฝ์ฐ ์์ DTO ํ๋์ @Valid๋ฅผ ๋ถ์ฌ์ผ ๋ด๋ถ ๊ฐ์ฒด๊น์ง ํจ๊ป ๊ฒ์ฆ๋๋ค.
@Getter
public class OrderRequest {
@Valid
@NotNull
private AddressRequest address;
}
@Getter
public class AddressRequest {
@NotBlank
private String city;
@NotBlank
private String street;
}
Bean Validation์ ๊ธฐ๋ณธ์ ์ผ๋ก ํด๋น ๊ฐ์ฒด์ ์ง์ ์ ์ธ ํ๋๊น์ง๋ง ๊ฒ์ฌํ๋ค. ๋ฐ๋ผ์ ์ค์ฒฉ ๊ฐ์ฒด์ ๋ด๋ถ ํ๋๊น์ง ๊ฒ์ฆํ๋ ค๋ฉด ๊ฒ์ฆ์ด ํ์ ๊ฐ์ฒด๋ก ์ ํ๋๋๋ก @Valid๋ฅผ ๋ช
์์ ์ผ๋ก ์ ์ธํด์ผ ํ๋ค.
๋ง์ฝ OrderRequest์์ address ํ๋์ @Valid๋ฅผ ๋ถ์ด์ง ์์ผ๋ฉด AddressRequest ๋ด๋ถ์ city, street ํ๋์ ๋ํ ๊ฒ์ฆ์ ์ํ๋์ง ์๋๋ค.
์ปฌ๋ ์ ์์ฒด๋ฟ๋ง ์๋๋ผ ๋ด๋ถ ์์๊น์ง ํจ๊ป ๊ฒ์ฆํ ์ ์๋ค.
@Getter
public class PostRequest {
@NotEmpty
private List<@NotBlank String> tags;
}
์ด ๋ฌธ๋ฒ์ Java 8 ์ดํ์ ์ปจํ
์ด๋ ์์ ์ ์ฝ ๊ธฐ๋ฅ์ด๋ฉฐ @NotEmpty๋ฅผ ํตํด ๋ฆฌ์คํธ๊ฐ null์ด ์๋๊ณ ๋น์ด์์ง ์์์ง๋ฅผ ์ฐ์ ๊ฒ์ฌํ๊ณ @NotBlank๋ฅผ ํตํด ๋ฆฌ์คํธ์ ํฌํจ๋ ๊ฐ ์์๊ฐ null, ๋น ๋ฌธ์์ด, ๊ณต๋ฐฑ ๋ฌธ์์ด์ด ์๋์ง ์ถ๊ฐ ๊ฒ์ฆํ๋ค.
์ฆ ์ปฌ๋ ์
์ ๊ตฌ์กฐ์ ๊ฐ ์์์ ๊ฐ์ ๋์์ ๊ฒ์ฆํ ์ ์๋ค.
DTO์ ์ ํจ์ฑ ๊ฒ์ฌ ์ด๋
ธํ
์ด์
์ ์ค์ ํ ํ์๋ ์ค์ ๋ก ๊ฒ์ฆ์ด ์ํ๋๋๋ก ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ์ @Valid๋ฅผ ๋ถ์ฌ์ผ ํ๋ค.
๋ง์ฝ @Valid๋ฅผ ๋ถ์ด์ง ์์ผ๋ฉด DTO์ ๊ฒ์ฆ ์ด๋
ธํ
์ด์
์ด ์ ์๋์ด ์์ด๋ ๊ฒ์ฆ์ ์ํ๋์ง ์๋๋ค. ์ฆ DTO์ ์ ์ฝ ์กฐ๊ฑด์ ์ ์ธํ๋ ๊ฒ๊ณผ ์ค์ ๊ฒ์ฆ ์คํ์ ๋ณ๊ฐ์ ๋จ๊ณ์ด๋ค. @Valid๋ ์ ๋ฌ๋ฐ์ ๊ฐ์ฒด์ ์ ์๋ ์ ์ฝ ์กฐ๊ฑด์ ๊ธฐ๋ฐ์ผ๋ก ์์ฒญ ๋ฐ์ดํฐ๊ฐ ์ ํจํ์ง ๊ฒ์ฌํ๋ ์ญํ ์ ํ๋ค. ์ฃผ๋ก @RequestBody๋ @ModelAttribute์ ํจ๊ป ์ฌ์ฉ๋๋ค.
@PostMapping
public PostCreateResponse createPost(@Valid @RequestBody PostCreateRequest request) {
// ...์ปจํธ๋กค๋ฌ ๋ก์ง
}
ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญ์ ๋ณด๋ด๋ฉด Spring์ด ์์ฒญ ๋ฐ์ดํฐ๋ฅผ DTO๋ก ๋ฐ์ธ๋ฉํ๊ณ @Valid๊ฐ ๋ถ์ด ์๋ค๋ฉด Bean Validation์ ์ํํ๋ค. ์ด๋ ๊ฒ์ฆ ๊ณผ์ ์์ ํ๋๋ผ๋ ์ ์ฝ ์กฐ๊ฑด์ ๋ง์กฑํ์ง ๋ชปํ๋ฉด ๊ฒ์ฆ์ด ์คํจํ๋ฉฐ ์ปจํธ๋กค๋ฌ ๋ฉ์๋๊ฐ ์คํ๋๊ธฐ ์ ์ ์์ธ๊ฐ ๋ฐ์ํ๋ค.
@Valid์ ์ํ ์ ํจ์ฑ ๊ฒ์ฌ๊ฐ ์คํจํ๋ฉด ๋ํ์ ์ผ๋ก ๋ ๊ฐ์ง ์์ธ๊ฐ ๋ฐ์ํ๋ค.
@RequestBody๋ก ์ ๋ฌ๋ DTO์ ๊ฒ์ฆ์ด ์คํจํ ๊ฒฝ์ฐ MethodArgumentNotValidException @ModelAttribute ๋๋ @RequestParam ๋ฐฉ์์ผ๋ก ์ ๋ฌ๋ ๋ฐ์ดํฐ์ ๊ฒ์ฆ์ด ์คํจํ ๊ฒฝ์ฐ BindException ์ด ์์ธ๋ฅผ ๋ณ๋๋ก ์ฒ๋ฆฌํ์ง ์์ผ๋ฉด Spring์ด ๊ธฐ๋ณธ ์๋ฌ ์๋ต์ ๋ฐํํ๋ค. ํ์ง๋ง ํ์์ ๋ฐ๋ผ ์ปจํธ๋กค๋ฌ์์ ์ง์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๊ฑฐ๋ @RestControllerAdvice๋ฅผ ํ์ฉํด ์ ์ญ์์ ๊ณตํต์ ์ผ๋ก ์ฒ๋ฆฌํ ์๋ ์๋ค.
์ปจํธ๋กค๋ฌ ๋ฉ์๋์์ @Valid ๋ฐ๋ก ๋ค์ BindingResult๋ฅผ ์ ์ธํ๋ฉด ๊ฒ์ฆ ์คํจ ์ ์์ธ๋ฅผ ๋ฐ์์ํค์ง ์๊ณ ์ค๋ฅ ์ ๋ณด๋ฅผ ์ง์ ์ฒ๋ฆฌํ ์ ์๋ค.
@PostMapping
public ResponseEntity<?> createPost(
@Valid @RequestBody PostCreateRequest request,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(bindingResult.getAllErrors());
}
return ResponseEntity.ok().build();
}โ
@Valid๊ฐ ๊ฒ์ฆ์ ์ํํ ํ ๊ฒ์ฆ ์คํจ ์ ์์ธ๋ฅผ ๋์ง๋ ๋์ ์ค๋ฅ ์ ๋ณด๋ฅผ BindingResult ๊ฐ์ฒด์ ์ ์ฅํ๋ค. ์ดํ bindingResult.hasErrors()๋ฅผ ํตํด ๊ฒ์ฆ ์คํจ ์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์๋ค.
์ด ๋ฐฉ์์ ๊ฐ๋จํ API์์ ๋น ๋ฅด๊ฒ ๊ตฌํํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ง๋ง ์ปจํธ๋กค๋ฌ๋ง๋ค ์ ์ฌํ ๊ฒ์ฆ ์ฒ๋ฆฌ ์ฝ๋๊ฐ ๋ฐ๋ณต๋ ์ ์์ผ๋ฉฐ ๊ณตํต ์๋ต ํฌ๋งท์ ์ ์งํ๊ธฐ ์ด๋ ต๋ค๋ ๋จ์ ์ด ์๋ค.
์ค๋ฌด์์๋ ๋ณดํต ์ ์ญ ์์ธ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ฌ์ฉํ๋ค.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidationException(
MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.toList();
return ResponseEntity.badRequest().body(errors);
}
}
๋ชจ๋ ์ปจํธ๋กค๋ฌ์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ๊ณตํต์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ผ๋ฉฐ ์ผ๊ด๋ ์๋ฌ ์๋ต ํฌ๋งท์ ์ ์งํ ์ ์์ด ์ ์ง๋ณด์ ์ธก๋ฉด์์ ์ ๋ฆฌํ๋ค.
