[Spring MVC] DTO 유효성 검증

최우형·2023년 4월 12일
1

Spring MVC

목록 보기
3/4

📌DTO 유효성 검증(Validation)이 필요한 이유

잘못된 형식의 이메일 주소를 입력할 수 있기에 검증해야한다.

DTO 클래스에 유효성 검증 적용하기

유효성 검증을 위한 의존 라이브러리 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	...
	...
}

build.gradle 파일의 dependencies 항목에 org.springframework.boot:spring-boot-stater-validation을 추가해야 한다.

MEmberPostDto 유효성 검증

✔️ MemberPostDto 유효성 검증 제약 사항

  • email (이메일 주소)
    • 값이 비어있지 않거나 공백이 아니어야 한다.
    • 유효한 이메일 주소 형식이어야 한다.
  • name (이름)
    • 값이 비어있지 않거나 공백이 아니어야 한다.
  • 'phone` (휴대폰 번호)
    • 값이 비어있지 않거나 공백이 아니어야 한다.
    • 아래와 같이 010으로 시작하는 11자리 숫자와 '-'로 구성된 문자열이어야 한다.
      • 예) 010-1234-5678
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

public class MemberPostDto {
    @NotBlank
    @Email
    private String email;

    @NotBlank(message = "이름은 공백이 아니어야 합니다.")
    private String name;

    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
            message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
    private String phone;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
}

MemberPostDto의 멤버 변수에 적용된 유효성 검증 내용은 다음과 같다.

  • email
    • @NotBlank
      • 이메일 정보가 비어있지 않은 지를 검증한다.
      • null 값이나 공백(""), 스페이스(" ")같은 값들을 모두 허용하지 않는다.
      • 유효성 검증에 실패하면 에러 메시지가 콘솔에 출력된다.
    • @Email
      • 유효한 이메일 주소인지 검증한다.
      • 유효성 검증에 실패하면 내장된 디폴트 에러 메시지가 콘솔에 출력된다.
  • name
    • @NotBlank
      • 이름 정보가 비어있지 않은지를 검증한다.
      • null 값이나 공백(""), 스페이스(" ") 같은 값들을 모두 혀용하지 않는다.
      • 유효성 검증에 실패하면 @NotBlank의 message 애트리뷰트에 지정한 문자열이 에러 메시지로 콘솔에 출력된다.
  • phone
    • @Pattern
      • 휴대폰 정보가 정규표현식(Regular Expression)에 매치되는 유효한 번호인지를 검증한다.
      • 유효성 검증에 실패하면 내장된 디폴트 에러 메시지가 콘솔에 출력된다.
MemberController@Valid 애너테이션을 추가하면 된다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
        return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
    }
		...
		...
}


이러면 유효성 검사에 걸리게 된다.


MemberPatchDto 유효성 검증

✔️ MemberPatchDto 유효성 검증 제약 사항

  • name (이름)
    • 값이 비어있을 수 있다.
    • 값이 비어있지 않다면 공백이 아니어야 한다.
  • phone (휴대폰 번호)
    • 값이 비어있을 수 있다.
    • 아래와 같이 010으로 시작하는 11자리 숫자와 '-'로 구성된 문자열이어야 한다.
      • 예) 010-1234-5678
import javax.validation.constraints.Pattern;

public class MemberPatchDto {
    private long memberId;

    // 공백 아닌 문자 1개 이상((공백인 문자 0개 또는 1개)(공백이 아닌 문자 1개 이상)) -> 마지막 맨 바깥 쪽 괄호 조건이 0개 이상(즉, 있어도 되고 없어도 된다)
    @Pattern(regexp = "^\\S+(\\s?\\S+)*$", message = "회원 이름은 공백이 아니어야 합니다.")
    private String name;

    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
            message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
    private String phone;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public long getMemberId() {
        return memberId;
    }

    public void setMemberId(long memberId) {
        this.memberId = memberId;
    }
}

MemberPatchDto 클래스의 멤버 변수에 적용된 유효성 검증 내용은 다음과 같다.

  • memberId

    • Request Body에 포함되는 데이터가 아니므로 유효성 검증이 필요하지 않습니다.
  • name

    • @Pattern
      • 정규 표현식으로 다음 내용을 체크합니다.
        • 이름 정보가 비어있으면(null) 유효성 검증을 하지 않습니다.
        • 이름 정보가 비어 있지 않고(not null), 공백 문자열이라면 검증에 실패합니다.
        • 시작 문자가 공백이면 검증에 실패합니다.
        • 끝 문자가 공백이면 검증에 실패합니다.
        • 문자와 문자 사이 공백이 1개를 초과하면 검증에 실패합니다.
  • phone

    • @Pattern
    • 정규 표현식으로 다음 내용을 체크합니다.
    • 휴대폰 정보가 비어있으면(null) 유효성 검증을 하지 않습니다.
    • 휴대폰 정보가 비어 있지 않고, 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이 아니라면 검증에 실패합니다.

MemberPostDto 클래스와 달리 MemberPatchDto에서는 모두 정규 표현식을 사용했다.

다양한 조건을 선택적으로 검증하고자 할 때 유용한 방법 중 하나가 바로 정규 표현식(Regular Experssion) 이다.

name 멤버 변수에 사용한 “^\S+(\s?\S+)*$” 정규 표현식에서

‘^’은 문자열의 시작을 의미한다.
‘$’는 문자열의 끝을 의미한다.
’는 ‘’ 앞에 평가할 대상이 0개 또는 1개 이상인지를 평가한다.
‘\s’는 공백 문자열을 의미한다.
‘\S’ 공백 문자열이 아닌 나머지 문자열을 의미한다.
‘?’는 ‘?’ 앞에 평가할 대상이 0개 또는 1개인지를 의미한다.
‘+’는 ‘+’ 앞에 평가할 대상이 1개인지를 의미한다.

MemberController 클래스에 patchMember() 메서드를 바꾼다.

@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {
    ...
		...

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") @Min(2) long memberId,
                                    @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        // No need Business logic

        return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
    }
}

이렇게 하면

유효하지 않은 정보를 입력하면 Bad Request를 받을 수 있다.


쿼리 파라미터 (Query Parameter 또는 Query String) 및 @Pathvariable에 대한 유효성 검증

지금까지 @PathVaribale("member-id") long memberId에 대한 검증은 하지 않았다.

1 이상의 숫자여야한다 라는 제약을 걸어보자
@RestController
@RequestMapping("/v1/members")
@Validated   // (1)
public class MemberController {
		...
		...

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember(@PathVariable("member-id") @Min(1) long memberId,
                                    @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);

        // No need Business logic

        return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
    }
}

Jakarta Bean Validation

지금까지 DTO 클래스의 유효성 검증을 위해 사용한 애너테이션은 Jakarta Bean Validation이라는 유효성 검증을 위한 표준 스펙에서 지원하는 내장 애너테이션들이다.

Jakarta Bean Validation 스펙을 구현한 구현체가 바로 Hibernate Validator이다.


Custom Validator를 사용한 유효성 검증

Jakarta Bean Validation에 내장된 애너테이션 중에 목적에 맞는 애너테이션이 존재하지 않을 수 있다.

Custom Validator를 구현하기 위한 절차는 다음과 같다.

  1. Custom Validator를 사용하기 위한 Custom Annotation을 정의한다.
  2. 정의한 Custom Annotation에 바인딩 되는 Custom Validator를 구현한다.
  3. 유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다.

✔️ Custom Annotation을 정의

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {NotSpaceValidator.class}) // (1)
public @interface NotSpace {
    String message() default "공백이 아니어야 합니다"; // (2)
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

NotSpace 애너테이션이 멤버 변수에 추가되었을 때, 동작 할 Custom Validator를 (1)과 같이 추가했다.

(2)는 애너테이션 추가 시 별도로 정의하지 않으면 유효성 검증 실패 시, 표시되는 디폴트 메시지이다.

✔️ Custom Validator 구현

import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class NotSpaceValidator implements ConstraintValidator<NotSpace, String> {

    @Override
    public void initialize(NotSpace constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null || StringUtils.hasText(value);
    }
}

✔️ 유효성 검증을 위해 Custom Annotation 추가

import javax.validation.constraints.Pattern;

public class MemberPatchDto {
    private long memberId;

    @NotSpace(message = "회원 이름은 공백이 아니어야 합니다") // (1)
    private String name;

    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
            message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다")
    private String phone;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public long getMemberId() {
        return memberId;
    }

    public void setMemberId(long memberId) {
        this.memberId = memberId;
    }
}

⭐핵심 포인트

  • 프론트엔드 쪽에서 유효성 검증을 진행했다 하더라도 서버 쪽에서 추가적으로 유효성 검증을 반드시 진행해야 한다.

  • 프론트엔드 쪽에서의 유효성 검증 프로세스는 사용자 편의성 측면에서 필요한 작업니다.

  • Jakarta Bean Validation의 애터네이션을 이용하면 Controller 로직에서 유효성 검증 로직을 분리할 수 있다.

  • Jakarta Bean Validation은 애너테이션 기반 유효성 검증을 위한 표준 스펙이다.

  • Hibernate Validator는 Jakrta Bean Validation 스펙을 구현한 구현체이다.

  • SPring에서 지원하는 @Validated 애너테이션을 사용하면 쿼리 파라미터 (Query Parameter 또는 Query String) 및 @Pathvariable 에 대한 유효성 검증을 진행할 수 있다.

  • Jakarta Bean Validation에서 빌트인 (Built-in)으로 지원하지 않는 애너테이션은 Custom Validator를 통해 Custom Annotation을 구현한 후, 적용할 수 있다.

profile
프로젝트, 오류, CS 공부, 코테 등을 꾸준히 기록하는 저만의 기술 블로그입니다!

0개의 댓글