@Valid์™€ @Validated

์ •๋ฏผ๊ตยท2024๋…„ 11์›” 4์ผ

๐Ÿ“’๋ชฉํ‘œ

@Valid์™€ @Validated์˜ ์ฐจ์ด์  ๋ฐ ๋™์ž‘ ํ๋ฆ„์— ๋Œ€ํ•ด ์•Œ์•„๋ณธ๋‹ค.

  • Spring Boot: 3.3.5
    • Spring MVC
    • Data JPA: MySQL(latest)

๐Ÿ“’๋„์ปค ์ปดํฌ์ฆˆ๋กœ MySQL ์‹คํ–‰

ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๊ฒฝ๋กœ์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด 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 boot์™€ MySQL ์—ฐ๊ฒฐ

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์—์„œ ํƒ€์ž„์กด ์„ค์ •์„ ์ง„ํ–‰ํ•œ๋‹ค. ๋‚˜์ค‘์— ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹๋‹ค.

์‹คํ–‰ ์„ฑ๊ณตํ–ˆ์œผ๋ฉด ์ค€๋น„ ๋

๐Ÿ“’Member Entity ์ž‘์„ฑ

@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;
}

๋”ฑํžˆ ์ œ์•ฝ์กฐ๊ฑด์ด ํ•„์š”ํ•œ ๊ฑด ์•„๋‹ˆ์ง€๋งŒ ๊ทธ๋ƒฅ ๊ตฌ์ƒ‰ ๋งž์ถ”๊ธฐ ์šฉ์œผ๋กœ ์ข€ ์จ๋ดค๋‹ค.

๐Ÿ“’Controller, Service, Repository ์ž‘์„ฑ

@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> {
}

ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ปจํ‹€๋กค๋Ÿฌ, ์„œ๋น„์Šค, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋„ ์ผ๋‹จ ์ ์–ด๋‘”๋‹ค.

โœ”๏ธํฌ์ŠคํŠธ๋งจ์œผ๋กœ api๊ฐ€ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ

์—ฌ๊ธฐ๊นŒ์ง€ ์˜ค์ผ€์ด

โœ”๏ธPOST /members api๋ฅผ ์œ„ํ•œ DTO๋ฅผ ๋งŒ๋“ค๊ณ  ๊ฒ€์ฆํ•˜๊ธฐ

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;
}

๐Ÿ“Œ@Validated๋กœ @RequestBody ๊ฒ€์ฆ ๋ฐ ๋””๋ฒ„๊น…

{
    "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์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฑฐ๋กœ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.

์ข€ ๋” ๋งŽ์€ ํ…Œ์ŠคํŠธ ๊ณผ์ •์„ ํฌ์ŠคํŒ…ํ•˜๋ฉด์„œ ๊ธ€์„ ๋งˆ๋ฌด๋ฆฌํ•˜๊ณ  ์‹ถ์—ˆ์ง€๋งŒ ์‹œ๊ฐ„์ด ์—†์–ด์„œ ์ค„์ด๊ณ , ๋‚˜์ค‘์— ์ข€ ๋” ๋ณด์ถฉํ•˜๋„๋ก ํ•ด์•ผํ•  ๊ฒƒ ๊ฐ™๋‹ค.

profile
๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž

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