@Valid์ @Validated์ ์ฐจ์ด์ ๋ฐ ๋์ ํ๋ฆ์ ๋ํด ์์๋ณธ๋ค.
ํ๋ก์ ํธ ๋ฃจํธ ๊ฒฝ๋ก์ ๋ค์๊ณผ ๊ฐ์ด docker-compose.yml ํ์ผ์ ์์ฑํด์ค๋ค.
services:
mysql:
image: mysql
restart: always
environment:
- MYSQL_ROOT_PASSWORD=test
- MYSQL_DATABASE=validation
- MYSQL_USER=alsry
- MYSQL_PASSWORD=alsry
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
TZ ๊ฐ์ ๋ฆฌ๋
์ค ํ์ค ํ๊ฒฝ๋ณ์์ ๋ํ ๊ฐ๋ environment ํ๋์ ์์ฑ ๊ฐ๋ฅํ๋ค. ์ฌ๊ธฐ์ ๋ณ๋๋ก ์์ฑํ์ง ์์๋ค.
ํ์ค ํ๊ฒฝ๋ณ์์ ๋ํ ๋ด์ฉ์ gnu, posix docs ๊ฐ์ ๊ณณ์ ์ฐพ์๋ณด๋ฉด ์ ๋์์๋ค.
docker compose up -d
๋ช ๋ น์ด๋ฅผ ์คํํ์ฌ ์ปจํ ์ด๋๋ฅผ ๋์ด๋ค.

์ฐ๊ฒฐ ํ ์คํธ๋ ์ฑ๊ณตํ์๋ค.
spring:
application:
name: validation
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/validation?useSSL=false&characterEncoding=UTF-8
username: alsry
password: alsry
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
application.yml ํ์ผ์ ์์ฑํ๊ณ ์คํ๋ง ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํด๋ณธ๋ค.
jdbc url์ ํ๋กํผํฐ ์ข
๋ฅ๋ค์ mysql connectorJ ๊ณต์ ๋ฌธ์์์ ํ์ธ ๊ฐ๋ฅํ๋ฉฐ, com.mysql.cj.conf ํจํค์ง์ PropertyKey enum ์์ฃ ํ์ธ ๊ฐ๋ฅํ๋ค.
์ฐธ๊ณ ๋ก url ํ๋กํผํฐ ์ค์ serverTimezone ํ๋กํผํฐ ์ค์ ์ ํ๋ฉด com.mysql.cj.protocol.a.NativeProtocol#configureTimeZone์์ ํ์์กด ์ค์ ์ ์งํํ๋ค. ๋์ค์ ์ฐธ๊ณ ํ๋ฉด ์ข๋ค.

์คํ ์ฑ๊ณตํ์ผ๋ฉด ์ค๋น ๋
@Entity
@Getter
@Table(uniqueConstraints = {@UniqueConstraint(name = "USERNAME_UNIQUE", columnNames = {"username"})})
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String username;
@Column(nullable = false, length = 24)
private String password;
@Enumerated(value = EnumType.STRING)
private MemberRole role;
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
๋ฑํ ์ ์ฝ์กฐ๊ฑด์ด ํ์ํ ๊ฑด ์๋์ง๋ง ๊ทธ๋ฅ ๊ตฌ์ ๋ง์ถ๊ธฐ ์ฉ์ผ๋ก ์ข ์จ๋ดค๋ค.
@RequestMapping("/member")
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping
public String getMembers() {
return "getMembers";
}
@GetMapping("/{id}")
public String getMember(@PathVariable Long id) {
return "getMember";
}
@PostMapping
public String postMembers() {
return "postMembers";
}
@PatchMapping
public String patchMembers() {
return "patchMembers";
}
@DeleteMapping
public String deleteMembers() {
return "deleteMembers";
}
}
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
}
public interface MemberRepository extends JpaRepository<Member, Long> {
}
ํ ์คํธ๋ฅผ ์ํด ์ปจํ๋กค๋ฌ, ์๋น์ค, ๋ฆฌํฌ์งํ ๋ฆฌ๋ ์ผ๋จ ์ ์ด๋๋ค.

์ฌ๊ธฐ๊น์ง ์ค์ผ์ด
MemberPostDto
@Getter
public class MemberPostDto {
@NotBlank
@Length(min = 3, max = 20)
private String username;
@Length(min = 8, max = 24)
@Pattern(regexp = "^(?=.*[!@#$%^&*(),.?\":{}|<>])(?=.*[a-zA-Z])(?=.*\\d)\\S+$",
message = "ํน์๋ฌธ์ ํ๋๋ฅผ ๋ฐ๋์ ํฌํจํด์ผ ํ๊ณ , ๊ณต๋ฐฑ์ด ์์ด์ผ ํฉ๋๋ค.")
private String password;
}
{
"username": "aa",
"password": "1a2s3d4f"
}
์ ๋ฐ์ดํฐ๋ก @Validated๋ก ๊ฒ์ฆํด๋ณด์
์๋ต ๋ฉ์ธ์ง
"message": "Validation failed for object='memberPostDto'. Error count: 2",
"errors": [
{
"codes": [
"Length.memberPostDto.username",
"Length.username",
"Length.java.lang.String",
"Length"
],
"arguments": [
{
"codes": [
"memberPostDto.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
},
20,
3
],
"defaultMessage": "๊ธธ์ด๊ฐ 3์์ 20 ์ฌ์ด์ฌ์ผ ํฉ๋๋ค",
"objectName": "memberPostDto",
"field": "username",
"rejectedValue": "aa",
"bindingFailure": false,
"code": "Length"
}
...
]
๋ฐ์ ์๋ฌ๋ MethodArgumentNotValidException์ด๊ณ ์๋ต ๋ฉ์ธ์ง๋ ๋์ถฉ ์๋ฐ์์ด๋ค.
codes ํ๋๋ฅผ ์ดํด๋ณด๋ฉด "Length.memberPostDto.username" ํ์์ธ๋ฐ
1. ์ฝ๋
2. ๋งคํํ ๊ฐ์ฒด
3. ํ๋
์ด๋ฐ์์ผ๋ก ๊ตฌ์ฑ๋์ด ์๋ค.
๋๋จธ์ง๋ ๋น์ทํ๋ค.
๋๋ฒ๊น ์ผ๋ก ์ด๋์ ์๋ฌ๋ฅผ ๋ฐ์์ํค๋์ง ๋ฐ๋ผ๊ฐ๋ณด์.

RequestResponseBodyMethodProcessor(HandlerMethodArgumentResolver)์ resolveArgument ๋ฉ์๋๊ฐ ์คํ๋๋ค.

AbstractMessageConverterMethodArgumentResolver#validateIfApplicable์ด ์คํ๋๋ฉด์ ํ๋ผ๋ฏธํฐ์ ๋ถ์ ์ด๋
ธํ
์ด์
๋ค์ ํ์
ํ๋ค.

ValidationAnnotationUtils#determineValidationHints๊ฐ ์คํ๋๋ฉด์ ๊ฒ์ฆ ์ด๋
ธํ
์ด์
์ด @Validated์ธ์ง @Valid์ธ์ง ํ์
ํ๊ณ @Validated๋ผ๋ฉด ์ด๋
ธํ
์ด์
์ ์ ๋ฌ๋ ํ๋ผ๋ฏธํฐ(์ด๋
ธํ
์ด์
ํํธ)๋ชฉ๋ก๋ค์ ๋ฐํํ๋ค.
์ ๋ฉ์๋์์ ๋ฐํ๋ ๊ฐ์ด null์ด ์๋๋ผ๋ฉด WebDataBinder#validate๊ฐ ์คํ๋๋ค.

DataBinder#validate์์ ์ค์ ๊ฒ์ฆ์์
์ ํ๋๋ฐ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ validatorHints๊ฐ ๋น์ด์์ง ์๊ณ (@Validated ์ด๋
ธํ
์ด์
์ ๊ทธ๋ฃน์ด ์ ์ฉ๋๋ ๋ถ๋ถ์ ๋งํ๋ ๊ฒ์ผ๋ก ๋ณด์), ์ ์ฉ๋ ๊ฒ์ฆ๊ธฐ๊ฐ SmartValidator๋ฉด
if ์กฐ๊ฑด์ ํด๋น๋๋ฉฐ ConstrationViolationException ์ด ๋ฐ์ํ๊ณ ,
๊ทธ๊ฒ ์๋๋ผ๋ฉด else if ์กฐ๊ฑด์ ํด๋น๋์ด MethodArgumentNotValidException์ด ๋ฐ์ํ๋ค.
MethodArugmentNotValidException์ด ๋ฐ์ํ๋ ์ด์ ๋, HandlerMethodArgumentResolver์ธ RequestResponseBodyMethodProcessor์์ ๊ฒ์ฆ์ ์งํํ๊ณ ์ฌ๊ธฐ์ ํด๋น ์์ธ๋ฅผ ๋ฐ์์ํค๊ธฐ ๋๋ฌธ์ด๋ค.
์ด ๊ณผ์ ์์์ @Valid๋ @Validated๋ ๊ฐ์ ์์ธ๋ฅผ ๋ฐ์์ํค๋ฉฐ, ์์ธ๊ฐ ์๋ค๋ฉด group ์ ์ฉ์ ํตํด @Validated๋ฅผ ์ ์ฉํ์๋ค๋ฉด ConstraintViolationException์ด ๋ฐ์ํ๋ค.
ConstraintViolationException์ @Validated๋ฅผ ํด๋์ค ๋ ๋ฒจ์ ์ ์ฉํ์ฌ AOP๊ฐ ๋์ํ๋ฉด์, MethodValidationInterceptor๊ฐ ๋์ํ๋๋ฐ, ์ด ๋ ๋ฐ์์ํค๋ ์์ธ๊ฐ ConstraintViolationException ์์ธ์ด๋ค.
์ฆ HandlerMethodArgumentResolver์์ ๋์ํ๋ ๊ฒฝ์ฐ๋ฉด MethodArgumentNotValidException์ด ๋ฐ์ํ๋ ๊ฑฐ๊ณ , @Validated๋ก AOP๋ฅผ ํตํด ๊ฒ์ฆ๊ธฐ๊ฐ ๋์ํ๋ ๊ฒฝ์ฐ ConstraintViolationException์ด ๋ฐ์ํ๋ ๊ฑฐ๋ก ์ดํดํ ์ ์๋ค.
์ข ๋ ๋ง์ ํ ์คํธ ๊ณผ์ ์ ํฌ์คํ ํ๋ฉด์ ๊ธ์ ๋ง๋ฌด๋ฆฌํ๊ณ ์ถ์์ง๋ง ์๊ฐ์ด ์์ด์ ์ค์ด๊ณ , ๋์ค์ ์ข ๋ ๋ณด์ถฉํ๋๋ก ํด์ผํ ๊ฒ ๊ฐ๋ค.