ํด๋ผ์ด์ธํธ ์์ฒญ ๊ฐ์ ์๋น์ค ๋ก์ง์์ if ๋ฌธ์ผ๋ก ํ๋์ฉ ๊ฒ์ฌํ๋ฉด ๊ฒ์ฆ ์ฝ๋๊ฐ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ค์์ด๊ณ , ์ปจํธ๋กค๋ฌ๋ง๋ค ๋น์ทํ ์ฝ๋๊ฐ ๋ฐ๋ณต๋๋ค.
Spring Boot์์๋ @Valid์ Bean Validation์ ์ฌ์ฉํ์ฌ DTO์ ๊ฒ์ฆ ๊ท์น์ ์ ์ธํ๋ ๊ฒ๋ง์ผ๋ก ์๋ชป๋ ์์ฒญ์ ์ปจํธ๋กค๋ฌ ์ง์
์ ์ ๊ฑธ๋ฌ๋ด๊ณ , ์ผ๊ด๋ ํ์์ ์๋ฌ ์๋ต์ ๋ฐํํ ์ ์๋ค.
Spring Boot 2.3๋ถํฐ ์ ํจ์ฑ ๊ฒ์ฌ ๊ธฐ๋ฅ์ด spring-boot-starter-web์์ ๋ถ๋ฆฌ๋์๋ค. @Valid ๊ธฐ๋ฐ ๊ฒ์ฆ์ ์ฌ์ฉํ๋ ค๋ฉด spring-boot-starter-validation ์์กด์ฑ์ ์ถ๊ฐํด์ผ ํ๋ค.
[Gradle]
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
[Maven]
// pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
์ด ์คํํฐ์๋ Jakarta Bean Validation ํ์ค API์ ๊ตฌํ์ฒด์ธ Hibernate Validator๊ฐ ํฌํจ๋์ด ์์ด ๋ณ๋ ์ค์ ์์ด ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ค.
Spring Boot 3.x๋ถํฐ๋ javax.validation ๋์ jakarta.validation ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ค.
ํด๋ผ์ด์ธํธ ์์ฒญ์ ๋ฐ์ DTO ํ๋์ ๊ฒ์ฆ ๊ท์น์ ์ด๋ ธํ ์ด์ ์ผ๋ก ์ ์ธํ๋ค.
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;โ
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
@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๋ ๋ชจ๋ ํ์ ๊ฐ์ ๊ฒ์ฆํ์ง๋ง ๋น ๋ฌธ์์ด๊ณผ ๊ณต๋ฐฑ ๋ฌธ์์ด์ ๋ค๋ฃจ๋ ๋ฐฉ์์ด ๋ค๋ฅด๋ค.
์ด๋ ธํ ์ด์ | null | "" | " " | ์ ์ฉ ํ์ |
|---|---|---|---|---|
| ๊ฑฐ๋ถ | ํ์ฉ | ํ์ฉ | ๋ชจ๋ ํ์ |
| ๊ฑฐ๋ถ | ๊ฑฐ๋ถ | ํ์ฉ | String, ๋ฐฐ์ด, ์ปฌ๋ ์ , Map |
| ๊ฑฐ๋ถ | ๊ฑฐ๋ถ | ๊ฑฐ๋ถ | String |
๋ฌธ์์ด ํ์ ๊ฐ์๋ ๊ณต๋ฐฑ ์
๋ ฅ๊น์ง ๋ง๋ @NotBlank๋ฅผ ์ฃผ๋ก ์ฌ์ฉํ๋ค.
๊ฐ์ ํฌ๊ธฐ, ๋ฒ์, ํ์, ์์ ์ ์ ํํ ๋๋ ํ๋ ํ์ ์ ๋ง๋ ์ด๋ ธํ ์ด์ ์ ์ ํํ๋ค.
์ด๋ ธํ ์ด์ | ๊ฒ์ฆ ๋ด์ฉ | ์ ์ฉ ํ์ |
|---|---|---|
| ๊ธธ์ด, ํฌ๊ธฐ๊ฐ ๋ฒ์ ์์ ์๋์ง | String, ๋ฐฐ์ด, ์ปฌ๋ ์ , Map |
| ์ง์ ํ ๊ฐ ์ด์ / ์ดํ์ธ์ง | ์ ์ ํ์ , BigDecimal |
| 0๋ณด๋ค ํฐ์ง / 0 ์ด์์ธ์ง | ์ซ์ ํ์ |
| ๊ณผ๊ฑฐ์ธ์ง / ๊ณผ๊ฑฐ ๋๋ ํ์ฌ์ธ์ง | ๋ ์ง, ์๊ฐ ํ์ |
| ๋ฏธ๋์ธ์ง / ๋ฏธ๋ ๋๋ ํ์ฌ์ธ์ง | ๋ ์ง, ์๊ฐ ํ์ |
| ์ ๊ท ํํ์๊ณผ ์ผ์นํ๋์ง | String |
| ์ด๋ฉ์ผ ํ์์ธ์ง | String |
์ด ์ด๋
ธํ
์ด์
๋ค์ null์ ์ ํจํ ๊ฐ์ผ๋ก ํ๋จํ๋ค. ๊ฐ์ด ๋ฐ๋์ ์์ด์ผ ํ๋ ํ๋๋ผ๋ฉด @NotNull์ด๋ @NotBlank๋ฅผ ํจ๊ป ์ ์ธํ๋ค.
DTO์ ์ ์ฝ ์กฐ๊ฑด์ ์ ์ธํด๋ ๊ทธ ์์ฒด๋ก๋ ๊ฒ์ฆ์ด ์คํ๋์ง ์๋๋ค. ์ปจํธ๋กค๋ฌ ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ @Valid๋ฅผ ๋ถ์ฌ์ผ Spring์ด ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฆํ๋ฉฐ, ์ฃผ๋ก @RequestBody / @ModelAttribute์ ํจ๊ป ์ฌ์ฉํ๋ค.
@PostMapping
public CommentCreateResponse createComment(
@Valid @RequestBody CommentCreateRequest request
) {
// ...์ปจํธ๋กค๋ฌ ๋ก์ง
}
์์ฒญ์ด ๋ค์ด์ค๋ฉด Spring์ ์์ฒญ ๋ฐ์ดํฐ๋ฅผ DTO๋ก ๋ฐ์ธ๋ฉํ ๋ค Bean Validation์ ์ํํ๋ค. ์ ์ฝ ์กฐ๊ฑด์ ํ๋๋ผ๋ ์๋ฐํ๋ฉด ์ปจํธ๋กค๋ฌ ๋ฉ์๋๊ฐ ์คํ๋๊ธฐ ์ ์ ์์ธ๊ฐ ๋ฐ์ํ๋ค.
Bean Validation์ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ์ฒด์ ์ง์ ํ๋๋ง ๊ฒ์ฌํ๋ค. 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;
}
address ํ๋์์ @Valid๋ฅผ ๋นผ๋ฉด AddressRequest์ city, street ๊ฒ์ฆ์ ์ํ๋์ง ์๋๋ค. @NotNull์ address ์์ฒด๊ฐ null์ธ ์์ฒญ์ ๋ง๋๋ค.
์ ๋ค๋ฆญ ํ์ ์ธ์์ ์ ์ฝ ์กฐ๊ฑด์ ์ ์ธํ๋ฉด ์ปฌ๋ ์ ์ ๊ฐ ์์๊น์ง ๊ฒ์ฆํ ์ ์๋ค. Bean Validation 2.0๋ถํฐ ์ง์ํ๋ ์ปจํ ์ด๋ ์์ ์ ์ฝ(container element constraint) ๊ธฐ๋ฅ์ด๋ค.
@Getter
public class PostRequest {
@NotEmpty
private List<@NotBlank String> tags;
}
@NotEmpty๋ ๋ฆฌ์คํธ๊ฐ null์ด๊ฑฐ๋ ๋น์ด ์์ง ์์์ง ๊ฒ์ฌํ๊ณ , @NotBlank๋ ๋ฆฌ์คํธ์ ๊ฐ ์์๊ฐ null, ๋น ๋ฌธ์์ด, ๊ณต๋ฐฑ ๋ฌธ์์ด์ด ์๋์ง ๊ฒ์ฌํ๋ค.
@Valid ๊ฒ์ฆ์ด ์คํจํ๋ฉด ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ ๋ฐฉ์์ ๋ฐ๋ผ ๋ค๋ฅธ ์์ธ๊ฐ ๋ฐ์ํ๋ค.
๋ฐ์ธ๋ฉ ๋ฐฉ์ | ๋ฐ์ ์์ธ |
|---|---|
|
|
|
|
@RequestParam, @PathVariable ๊ฐ์ ๋จ์ผ ๊ฐ ๊ฒ์ฆ์ @Valid๊ฐ ์๋ ๋ฉ์๋ ๊ฒ์ฆ์ผ๋ก ๋์ํ๋ฉฐ, ๋ฐ์ํ๋ ์์ธ๋ ๋ค๋ฅด๋ค.
์์ธ๋ฅผ ๋ฐ๋ก ์ฒ๋ฆฌํ์ง ์์ผ๋ฉด Spring Boot๊ฐ 400 Bad Request์ ๊ธฐ๋ณธ ์๋ฌ ์๋ต์ ๋ฐํํ๋ค. ํด๋ผ์ด์ธํธ์ ์ํ๋ ํ์์ ์๋ฌ ์๋ต์ ์ฃผ๋ ค๋ฉด ์ด ์์ธ๋ฅผ ์ง์ ์ฒ๋ฆฌํด์ผ ํ๋ค.
@Valid ํ๋ผ๋ฏธํฐ ๋ฐ๋ก ๋ค์ BindingResult๋ฅผ ์ ์ธํ๋ฉด ๊ฒ์ฆ์ด ์คํจํด๋ ์์ธ๊ฐ ๋ฐ์ํ์ง ์๋๋ค. ๋์ ์ค๋ฅ ์ ๋ณด๊ฐ BindingResult์ ๋ด๊ธฐ๊ณ , hasErrors()๋ก ์คํจ ์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์๋ค.
@PostMapping
public ResponseEntity<?> createComment(
@Valid @RequestBody CommentCreateRequest request,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(bindingResult.getAllErrors());
}
return ResponseEntity.ok().build();
}
๊ฐ๋จํ API์์๋ ๋น ๋ฅด๊ฒ ๊ตฌํํ ์ ์์ง๋ง, ์ปจํธ๋กค๋ฌ๋ง๋ค ๊ฐ์ ์ฒ๋ฆฌ ์ฝ๋๊ฐ ๋ฐ๋ณต๋๊ณ ์๋ฌ ์๋ต ํ์์ ํต์ผํ๊ธฐ ์ด๋ ต๋ค.
@RestControllerAdvice๋ก ๊ฒ์ฆ ์์ธ๋ฅผ ํ๊ณณ์์ ์ฒ๋ฆฌํ๋ฉด ๋ชจ๋ ์ปจํธ๋กค๋ฌ๊ฐ ๊ฐ์ ํ์์ ์๋ฌ ์๋ต์ ๋ฐํํ๊ณ , ์ปจํธ๋กค๋ฌ์์ ๊ฒ์ฆ ์ฒ๋ฆฌ ์ฝ๋๋ฅผ ์์จ ์ ์๋ค.
@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);
}
}
Spring 5.3๋ถํฐ MethodArgumentNotValidException์ BindException์ ์์ํ๋ค. @ModelAttribute ์์ฒญ์ ๊ฒ์ฆ ์์ธ๊น์ง ํจ๊ป ์ฒ๋ฆฌํ๋ ค๋ฉด BindException์ ํธ๋ค๋ฌ ๋์์ผ๋ก ์ง์ ํ๋ค.
