1. DTO 유효성 검증(Validation)

 

 1) DTO 유효성 검증(Validation) 필요성

  • 애플리케이션과 REST API 통신을 하는 프런트엔드(Frontend) 쪽 웹 앱이 아래 그림과 같다
  • 일반적으로 프런트엔드의 웹 사이트에서는 자바스크립트를 통해서 사용자가 입력하는 입력 폼 필드의 값에 대한 유효성 검증을 진행한다
  • 프런트엔드에서 검증에 통과되었다고 그 값이 반드시 유효한 값이라고 보장할 수는 없다
    - 유효성 검사를 통과한 뒤 [등록] 버튼을 누르면 서버 쪽으로 HTTP Request 요청이 전송된다
    - 자바스크립트로 전송되는 데이터는 브라우저의 개발자 도구를 사용하면 브레이크 포인트(break point)를 이용해서 값을 조작할 수 있다
    - 프런트엔드에서 유효성 검사를 진행해도 서버에서 한번 더 유효성 검사를 진행해야 한다
    - 프런트엔드에서 진행하는 유효성 검사는 사용자 편의성을 위해 진행하는 절차이다
 

2. DTO 클래스에 유효성 검증 적용

  • MemberController에서 사용된 MemberPostDto 클래스와 MemberPatchDto 클래스에 유효성 검증을 적용해 본다

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

DTO 클래스에 유효성 검증을 적용하기 위해서는 Spring Boot에서 지원하는 Starter가 필요하다

build.gradle 파일의 dependencies 항목에 'org.springframework.boot:spring-boot-starter-validation’을 추가한다

//Spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성이 분리되서 @validation 사용을 하기 위해 추가 해줘야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'

 

 2) MemberPostDto 유효성 검증 제약 사항

  • email (이메일 주소)
    - 값이 비어있지 않거나 공백이 아니어야 한다
    - 유효한 이메일 주소 형식이어야 한다
  • name (이름)
    - 값이 비어있지 않거나 공백이 아니어야 한다
  • phone (휴대폰 번호)
    - 값이 비어있지 않거나 공백이 아니어야 한다
    - 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이어야 한다
      : 010-1234-5678
  • 아래는 유효성 검증을 위해 제약사항을 적용한 코드이다
    - 회원 등록을 위해 클라이언트에서 전달 받는 Request Body의 데이터인 emil, name, phone 정보에 유효성 검증을 위한 애너테이션을 추가하였다
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

public class MemberPostDto {
    @NotBlank(message = "공백없이 작성해 주시기 바랍니다")
    @Email
    private String email;
    @NotBlank(message = "공백없이 작성해 주시기 바랍니다")
    private String name;

    @NotBlank(message = "공백없이 작성해 주시기 바랍니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    private String phone;
    private long memberId;

    public String getEmail() {
        return email;
    }
    public String getName() {
        return name;
    }
    public String getPhone() {
        return phone;
    }
    //--------------------------------- 회원정보 Dto class 작성
    //--------------------------------- RequestBody를 전달받기 위한 Dto class 작성
    public void setEmail(String email) {
        this.email = email;
    }

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

    public void setPhone(String phone) {
        this.phone = phone;
    }
    public long getMemberId(){
        return memberId;
    }
    public void setMemberId(long memberId) {
        this.memberId = memberId;
    }
}

 

 2-1) MemberPostDto의 멤버 변수에 적용된 유효성 검증 내용

  • email
    - @NotBlank
      : 이메일 정보가 비어있지 않은지를 검증한다
      : null 값이나 공백(””), 스페이스(” “) 같은 값들을 허용하지 않는다
      : 유효성 검증에 실패하면 에러 메시지를 콘솔에 출력한다

    - @Email
      : 유효한 이메일 주소인지 검증한다
      : 유효성 검증에 실패하면 내장된 디폴트 에러 메시지를 콘솔에 출력한다
  • name
    - @NotBlank
      : 이름 정보가 비어있지 않은지를 검증한다
      : null 값이나 공백(””), 스페이스(” “) 같은 값들을 모두 허용하지 않는다
      : 유효성 검증에 실패하면 message 애트리뷰트에 지정한 문자열 에러 메시지를 콘솔에 출력한다
  • phone
    - @Pattern
      : 휴대폰 정보가 정규표현식(Reqular Expression)에 매치되는 유효한 번호인지를 검증한다
      : 유효성 검증에 실패하면 내장된 디폴트 에러 메시지를 콘솔에 출력한다
  • 요청으로 전달 받는 MemberPostDto 클래스의 각 멤버 변수에 유효성 검증을 위한 애너테이션을 추가하여 MemberController의 핸들러 메서드에 별도의 유효성 검증을 추가하지 않고 유효성 검증 로직을 분리하였다

 

 2-2) Controller에 유효성 검증 적용

  • 유효성 검증 애너테이션이 추가된 DTO 클래스에서 유효성 검증 로직이 실행되게 하기 위해서 postMember()와 같이 DTO 클래스에 @Valid 애너테이션을 추가한다
@PostMapping
//Dto에 유효성 검증 조건을 적용하였으므로 @Valid 를 적용하여 준다
public ResponseEntity postMember (@Valid @RequestBody MemberPostDto memberPostDto) {
    return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
}

postman에서 유효하지 않은 정보를 전송하면 Error이 발생한다

  • Spring MVC에서 출력한 로그를 IntelliJ IDE에서 확인하면 아래와 같이 출력된다
    - email의 오류를 출력한다
    - name는 공백이 없으므로 정상적으로 전송된다
    - phone은 '010'이 아니므로 오류 메시지를 출력한다

 

3) MemberPatchDto 유효성 검증 제약 사항

  • name (이름)
    - 값이 비어있지 않거나 공백이 아니어야 한다
  • phone (휴대폰 번호)
    - 값이 비어있지 않거나 공백이 아니어야 한다
    - 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이어야 한다
      : 010-1234-5678
  • 아래는 유효성 검증을 위해 제약사항을 적용한 코드이다
    - 회원 정보 수정을 위해 클라이언트에서 전달 받는 Request Body의 데이터인 name, phone 정보에 유효성 검증을 위한 애너테이션을 추가하였다
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;

public class MemberPatchDto {
    @Email
    private String email;

    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    private String name;

    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    private String phone;
    private long memberId;

    public String getEmail() {
        return email;
    }
    public String getName() {
        return name;
    }
    public String getPhone() {
        return phone;
    }
    //--------------------------------- 회원정보 Dto class 작성
    //--------------------------------- RequestBody를 전달받기 위한 Dto class 작성
    public void setEmail(String email) {
        this.email = email;
    }

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

    public void setPhone(String phone) {
        this.phone = phone;
    }
    public long getMemberId(){
        return memberId;
    }
    public void setMemberId(long memberId) {
        this.memberId = memberId;
    }
}

 3-1) MemberPatchDto 클래스의 멤버 변수에 적용된 유효성 검증 내용

  • memberId
    - Request Body에 포함되는 데이터가 아니므로 유효성 검증이 필요하지 않다
  • name
    - @Pattern
      : 정규 표현식으로 다음 내용을 체크한다
        -> 이름 정보가 비어있으면 검증에 성공
        -> 이름 정보가 비어 있지 않고, 공백 문자열이라면 검증에 실패
  • phone
    - @Pattern
      : 정규 표현식으로 다음 내용을 체크한다
        -> 휴대폰 정보가 비어있으면 검증에 성공
        -> 휴대폰 정보가 비어 있지 않고, 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이 아니라면 검증에 실패
  • MemberPostDto 클래스와 달리 MemberPatchDto 에서는 모두 정규 표현식을 사용했다
  • 웹 브라우저에서 회원 가입 후에 이름(또는 닉네임)만 수정하거나 휴대폰 번호만 수정할 수도 있고 모두 수정할 수도 있다
  • 이름이나 휴대폰 번호의 값으로 공백 문자열(”” 또는 “ “)만 포함이 되어 있을 경우를 검증해야할 수 있다

 3-2) Controller 클래스에 patchMember() 핸들러 메서드의 코드 적용

    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember (@PathVariable("member-id")long memberId,
                                       @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);
//        memberPatchDto.setName("홍길동");
        // No need Business logic
        return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
    }

 

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

  • Request Body의 유효성 검사 검증 대상에서 빠진 항목이 있다
  • patchMember() 핸들러 메서드의 URI path에서 사용되는 @PathVariable("member-id") long memberId 변수이다
  • 일반적으로 수정이 필요한 데이터의 식별자는 0이상의 숫자로 표현을 한다
  • patchMember() 핸들러 메서드에서 사용되는 memberId에 "1 이상의 숫자여야 한다"라는 조건을 부여하도록 한다
    - 1 이상의 숫자일 경우에만 유효성 검증에 통과하도록 @Min(1) 이라는 검증 애너테이션을 추가한다
    @PatchMapping("/{member-id}")
    public ResponseEntity patchMember (@PathVariable("member-id") @Min(1) long memberId,
                                       @Valid @RequestBody MemberPatchDto memberPatchDto) {
        memberPatchDto.setMemberId(memberId);
//        memberPatchDto.setName("홍길동");
        // No need Business logic
        return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
    }
  • 위와 같이 수정하여도 Postman으로 URI path에 유효하지 않은 값을 전송하면 유효성 검증이 정상적으로 수행되지 않는다
  • 유효성 검증이 정상적으로 수행되려면 클래스 레벨에 @Validated 애너테이션을 붙여주어야 한다
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
@Validated
  • postman으로 실행해 보면 응답 결과는 Response Satus가 500인 ‘Internal Server Error’를 전달 받는다
  • @Request Body에 대한 유효성 검증 실패 메시지와 조금 다르지만 유효성 검증은 정상적으로 이루어진 것이다

 

4. Jakarta Bean Validation

  • DTO 클래스의 유효성 검증을 위해서 사용한 애너테이션은 Jakarta Bean Validation라는 유효성 검증을 위한 표준 스펙에서 지원하는 내장 애너테이션이다
  • Jakarta Bean Validation은 라이브러리처럼 사용할 수 있는 API가 아닌 스펙(사양, Specification) 자체이다
    - Jakarta Bean Validation은 일종의 기능 명세와 같다
    - Jakarta Bean Validation 스펙을 구현한 구현체가 Hibernate Validator이다
  • Jakarta Bean Validation의 애너테이션을 DTO 클래스에만 사용할 수 있는 것은 아니다
    - Java Bean 스펙을 준수하는 Java 클래스라면 Jakarta Bean Validation의 애너테이션으로 유효성 검증을 할 수 있다

 

5. Custom Validator를 사용한 유효성 검증

  • DTO 클래스에 유효성 검증 시 Jakarta Bean Validation에 내장된 애너테이션 중에 목적에 맞는 애너테이션이 없을 수 있다
    - 목적에 맞는 애너테이션을 직접 만들어서 유효성 검증에 적용할 수 있다
  • MemberPatchDto 클래스의 name과 phone 멤버 변수에서 공백 여부를 검증하는 @Pattern(regexp = "^(?=\\s*\\S).*$") 애너테이션을 Custom Validator를 사용하여 바꿔 본다
  • 정규 표현식(Regular Expression)은 성능적인 면에서 때로는 비싼 비용을 치뤄야 될 가능성이 있다
    - 모든 로직을 정규표현식 위주로 작성하는 것은 좋은 개발 방식이 아니다

 

 1) Custom Validator를 구현하기 위한 절차

  • Custom Validator를 사용하기 위한 Custom Annotation을 정의한다
    - 공백을 허용하지 않는 Custom Annotation 'Notspace'를 생성한다
    - 중요한 부분은 NotSpace 애너테이션을 멤버 변수에 추가할 경우 동작하는 Custom Validator를 추가하는 것이다
     : @Constraint(validatedBy = {NotSpaceValidator.class})
    - 유효성 검증 실패 시 표시되는 디폴트 메시지를 애너테이션에서 정의해 준다
     : String message() default "공백이 아니어야 합니다"
     : 애너테이션에서 정의한 디폴트 메시지는 적용 시 공통적으로 사용된다
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 {};
}
  • 정의한 Custom Annotation에 바인딩 되는 Custom Validator를 구현한다
    - CustomValidator를 구현하기 위해서는 ConstraintValidator 인터페이스를 먼저 구현해야 한다
    - ConstraintValidator<NotSpace, String>에서
     : NotSpace는 CustomValidator와 매핑된 Custom Annotation을 의미한다
     : String은 Custom Annotation으로 검증할 대상 멤버 변수의 타입을 의미한다
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);
    }
}
  • 유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다
    - name과 phone 등의 멤버 변수에서 적용된 @Pattern(regexp = "^(?=\\s*\\S).*$") 애너테이션 대신 Custom Annotation인 @NotSpace를 추가한다
    - 애플리케이션을 재시작하고 patchMember() 핸들러 메서드에 해당되는 URI로 요청을 전송하여 확인한다
public class MemberPostDto {
//    @NotBlank(message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    @Email
    private String email;
//    @NotBlank(message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    private String name;

//    @NotBlank(message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    private String phone;
public class MemberPatchDto {
    @Email
    private String email;

//    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    private String name;

//    @Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
    @Notspace(message = "공백이 아니어야 합니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
    private String phone;

 

 

■ 참조 링크

정규 표현식 관련 자료

 : https://www.w3schools.com/java/java_regex.asp 

 

Java Regular Expressions

W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

www.w3schools.com

 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions

 

정규 표현식 - JavaScript | MDN

정규 표현식, 또는 정규식은 문자열에서 특정 문자 조합을 찾기 위한 패턴입니다. JavaScript에서는 정규 표현식도 객체로서, RegExp의 exec()와 test() 메서드를 사용할 수 있습니다. String의 match(), matchA

developer.mozilla.org

정규 표현식 모범 사례 자료

 : https://learn.microsoft.com/ko-kr/dotnet/standard/base-types/best-practices 

 

.NET의 정규식에 대한 모범 사례

.NET에서 효율적이고 효과적인 정규식을 만드는 방법을 알아봅니다.

learn.microsoft.com

 Jakarta Bean Validation Specification

 : https://beanvalidation.org/2.0/spec/ 

 

Jakarta Bean Validation specification

BeanNode, PropertyNode and ContainerElementNode host getContainerClass() and getTypeArgumentIndex(). If the node represents an element that is contained in a container such as Optional, List or Map, the former returns the declared type of the container and

beanvalidation.org

Jakarta Bean Validation Built-in Constraint definitions

 : https://beanvalidation.org/2.0/spec/#builtinconstraints 

 

Jakarta Bean Validation specification

BeanNode, PropertyNode and ContainerElementNode host getContainerClass() and getTypeArgumentIndex(). If the node represents an element that is contained in a container such as Optional, List or Map, the former returns the declared type of the container and

beanvalidation.org

Hibernate Validator

 : https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#preface 

 

Hibernate Validator 6.2.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org

Java Bean 관련 자료

 : https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94%EB%B9%88%EC%A6%88 

 

자바빈즈 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 자바빈즈(JavaBeans)는 자바로 작성된 소프트웨어 컴포넌트이다. 자바빈즈의 사양은 썬 마이크로시스템즈에서 다음과 같이 정의되었다. "빌더 형식의 개발도구에

ko.wikipedia.org

 

+ Recent posts