ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 유용한 Custom Validation을 구현해보자 (숫자, 특수문자, 영어, 한글, 날짜, url, ip, uuid, YN...)
    카테고리 없음 2022. 11. 16. 19:48

    Jakarta Bean Validation

     

    이전 글 까지, Spring에서 제공하는 “Bean Validation” 기능에 대해서 알아보고 Custom Validation을 구현하는 방법을 알아봤습니다.

    이번 글에서는, 유용하게 사용할 수 있는 “Custom Validation”을 구현하여, 샘플을 정리 및 공유하겠습니다.

     


    해당 글을 작성하면서, 개발된 스프링 부트 기반의 프로젝트입니다.

    https://github.com/rojae/Spring-Validation-Check-Sample/tree/v3

     

    GitHub - rojae/Spring-Validation-Check-Sample

    Contribute to rojae/Spring-Validation-Check-Sample development by creating an account on GitHub.

    github.com

     

     


    Package Tree

    패키지 구성에 대한 내용입니다.

    유효성 검증 기능의 시작은 “validator”라는 패키지를 시작으로 구성됩니다.

    └── validator   # validation 구현의 시작이며, 프로젝트 전용 파일들
        └── common  # 공통적으로 사용될 수 있는 모듈
            ├── extension  # 다른 패키지에서 사용하는 공통적인 확장 모듈
            ├── format     # 날짜, URL, IP와 같은 포맷 기반의 모듈
            └── match      # 특수문자, 영어, 한글과 같은 문자열 매칭 기반의 모듈
            └── utils      # 정규식 활용을 위한 유틸리티들

    패키지는 위와 같은 기준으로 분리 구분하여, 실질적으로 common 내부의 모듈은 프로젝트에서 건들이지 않고

    사용이 가능하게 됩니다.

    아래는 샘플로 구현된 목록입니다.

    (자세한 소스는 “아래 글”과 “Github Link”를 참고해주세요)

    └── validator    # validation 구현의 시작이며, 프로젝트 전용 파일들
        ├── EmailValid.java
        ├── EmailValidator.java
        ├── LoginIdValid.java
        ├── MovieCategoryValid.java
        ├── MovieCategoryValidator.java
        └── common   # 공통적으로 사용될 수 있는 모듈
            ├── extension  # 다른 패키지에서 사용하는 공통적인 확장 모듈
            │   ├── StringValid.java
            │   └── StringValidator.java
            ├── format     # 날짜, URL, IP와 같은 포맷 기반의 모듈
            │   ├── IsDateValid.java
            │   ├── IsDateValidator.java
            │   ├── IsIpValid.java
            │   ├── IsIpValidator.java
            │   ├── IsUrlPathValid.java
            │   ├── IsUrlPathValidator.java
            │   ├── IsUrlValid.java
            │   ├── IsUrlValidator.java
            │   ├── IsUuidValid.java
            │   ├── IsUuidValidator.java
            │   └── IsYnValid.java
            └── match     # 특수문자, 영어, 한글과 같은 문자열 매칭 기반의 모듈
                ├── NoSpecialValid.java
                ├── NoSpecialValidator.java
                ├── OnlyAlphabetValid.java
                ├── OnlyAlphabetValidator.java
                ├── OnlyDownerValid.java
                ├── OnlyDownerValidator.java
                ├── OnlyKoreanValid.java
                ├── OnlyKoreanValidator.java
                ├── OnlyNumericValid.java
                ├── OnlyNumericValidator.java
                ├── OnlyNumericWithAlphabetValid.java
                ├── OnlyNumericWithAlphabetValidator.java
                ├── OnlyUpperValid.java
                └── OnlyUpperValidator.java
            └── utils      # 정규식을 위한 유틸리티
                └── RegexUtils.java

    설명과 동시에 정리하면 아래와 같은 구조가 될 것 입니다.

    • 프로젝트 전용 (validator)
      • @EmailValid
      • @LoginIdValid
      • @MovieCategoryValid
      • 공통 모듈 (common)
        • 확장 가능 (extension)
          • @StringValid
        • 포맷 기반 (format)
          • @IsDateValid (날짜 포맷)
          • @IsIpValid (IP 포맷)
          • @IsUrlValid (URL 포맷)
          • @IsUrlPathValid (URL 경로 포맷)
          • @IsUuidValid (UUID 포맷)
          • @IsYnValid (단일 문자열 Y, N 포맷)
        • 문자열 검사 기반 (match)
          • @NoSpecialValid (특수문자 검사)
          • @OnlyAlphabetValid (영문 검사)
          • @OnlyDownerValid (영문 소문자 검사)
          • @OnlyUpperValid (영문 대문자 검사)
          • @OnlyKoreanValid (한글 검사)
          • @OnlyNumericValid (숫자 검사)
          • @OnlyNumericWithAlphabetValid (한글과 숫자 검사)
        • 정규식 유틸리티
          • RegexUtils

    Implementation (공통 패키지)

    실질적인 구현 부분이지만, 크게 설명하는 부분보다 소스코드를 공유하면 다들 이해가 되실 거라고 생각합니다.

    이전에 설명한 글과 동일하게 “ConstraintValidator”를 구현하여, Annotation에 validatedBy를 붙이는 방식입니다.

    정규식 기반으로 동작하기 때문에, “RegexUtils”라는 클래스를 추가 구현했습니다.

     


    1. validator.common.utils

     

    RegexUtils

    해당 유틸리티를 사용하여, Annotation 구현부에서 Validation Check를 합니다.
    public class RegexUtils {
    
        public static boolean hasSpecialChar(String str){
            return str.matches ("[0-9|a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힝]*");
        }
    
        public static boolean onlyNumericWithAlphabet(String str){
            return str.matches("^[a-zA-Z0-9]*");
        }
    
        public static boolean isNumeric(String str){
            return str.matches("^[0-9]*$");
        }
    
        public static boolean isAlphabet(String str){
            return str.matches("^[a-zA-Z]*$");
        }
    
        public static boolean isKorean(String str){
            return str.matches("[가-힣]*$");
        }
    
        public static boolean isUpper(String str){
            return str.matches("^[A-Z]*$");
        }
    
        public static boolean isDowner(String str){
            return str.matches("^[a-z]*$");
        }
    
        public static boolean isUrl(String str){
            return str.matches("(http[s]?:\\/\\/)([a-zA-Z0-9]+)\\.[a-z]+([a-zA-Z0-9.?#]+)?");
        }
    
        public static boolean isUrlPath(String str){
            return str.matches("(http[s]?:\\/\\/)([^\\/\\s]+\\/)(.*)");
        }
    
        public static boolean isIp(String str){
            return str.matches("([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})");
        }
    
        public static boolean isDate(String str){
            return str.matches("^\\d{4}.\\d{2}.\\d{2}$");
        }
    
        public static boolean isUUID(String str){
            return str.matches("[a-f0-9]{8}(?:-[a-f0-9]{4}){4}[a-f0-9]{8}");
        }
    }
    
    

     


    2. validator.common.extension

     

    @StringValid

    정의된 String과 일치하는지 체크합니다. 대소문자 무시를 위해서 ignoreLetterCase라는 boolean 변수도 추가하였습니다.

     

    StringValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = StringValidator.class)
    public @interface StringValid {
        String[] acceptedList();
        String message() default "Invalid Movie Category Type";
        Class[] groups() default {};
        Class[] payload() default {};
        boolean ignoreLetterCase() default false;
    }
    
    
    

     

    StringValidator.java

    public class StringValidator implements ConstraintValidator<StringValid, String> {
    
        private List<String> valueList;
        private StringValid stringValid;
    
        @Override
        public void initialize(StringValid constraintAnnotation) {
            valueList = new ArrayList<>();
            valueList.addAll(List.of(constraintAnnotation.acceptedList()));
            this.stringValid = constraintAnnotation;
        }
    
        @Override
        public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
            boolean ignoreLetterCase = stringValid.ignoreLetterCase();
    
            if (ignoreLetterCase) {
                for (String value : valueList) {
                    if (value.equalsIgnoreCase(s))
                        return true;
                }
                return false;
            } else {
                return valueList.contains(s);
            }
        }
    }
    
    

     


    3. validator.common.format

    @IsDateValid

    정의된 날짜 포맷과 일치하는 지 검사합니다. (YYYY.MM.DD)

     

    IsDateValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = IsDateValidator.class)
    public @interface IsDateValid {
        String message() default "Required only date Format (yyyy.mm.dd)";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    IsDateValidator.java

    public class IsDateValidator implements ConstraintValidator<IsDateValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isDate(value);
        }
    }
    
    

     


    @IsIpValid

    정의된 IP형식과 일치하는지 검사합니다. (숫자 1~3개 사이마다 '.' 포함)

     

    IsIpValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = IsIpValidator.class)
    public @interface IsIpValid {
        String message() default "Required only IP Address Format";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    IsIpValidator.java

    public class IsIpValidator implements ConstraintValidator<IsIpValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isIp(value);
        }
    }
    
    

     


    @IsUrlPathValid

    URL Path 형식이 맞는지 확인합니다.

     

    IsUrlPathValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = IsUrlPathValidator.class)
    public @interface IsUrlPathValid {
        String message() default "Required only url path Format";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    IsUrlPathValidator.java

    public class IsUrlPathValidator implements ConstraintValidator<IsUrlPathValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isUrlPath(value);
        }
    }
    
    

     


    @IsUrlValid

    URL 형식이 맞는지 확인합니다. (Path는 혀용하지 않습니다)
    https://google.com -> O
    https://google.com/books -> X

     

    IsUrlValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = IsUrlValidator.class)
    public @interface IsUrlValid {
        String message() default "Required only URL Format";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

    IsUrlValidator.java

    public class IsUrlValidator implements ConstraintValidator<IsUrlValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isUrl(value);
        }
    }
    
    

     


    @IsUuidValid

    UUID 규약을 준수하여, 포맷을 검사합니다.

    IsUuidValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = IsUuidValidator.class)
    public @interface IsUuidValid {
        String message() default "Required only UUID Format";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

    IsUuidValidator.java

    public class IsUuidValidator implements ConstraintValidator<IsUuidValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isUUID(value);
        }
    }
    
    

     


    @IsYnValid

    StringValid Annotation을 사용하여, Y 혹은 N 문자열만 허용하는 Annotation입니다.

     

    IsYnValid.java

    StringValid Annotation을 활용하여 개발하기 때문에, 실제 구현부는 공백으로 둡니다.

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RetentionPolicy.RUNTIME)
    @StringValid(acceptedList = {"Y", "N"})
    @Constraint(validatedBy = {})
    public @interface IsYnValid {
        String message() default "Required only character (Y, N)";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     


    3. validator.common.match

     

    @NoSpecialValid

    특수문자가 아닌 경우, 허용되는 Annotation입니다.

     

    NoSpecialValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = NoSpecialValidator.class)
    public @interface NoSpecialValid {
        String message() default "Required no Special Character";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    NoSpecialValidator.java

    public class NoSpecialValidator implements ConstraintValidator<NoSpecialValid, String> {
    
        @Override
        public void initialize(NoSpecialValid constraintAnnotation) {
            ConstraintValidator.super.initialize(constraintAnnotation);
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return !RegexUtils.hasSpecialChar(value);
        }
    
    }
    
    

     


    @OnlyAlphabetValid

    영문자 알파벳으로만 이루어진 경우 허용되는 Annotation입니다.

     

    OnlyAlphabetValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = OnlyAlphabetValidator.class)
    public @interface OnlyAlphabetValid {
        String message() default "Required only Alphabet";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    OnlyAlphabetValidator.java

    public class OnlyAlphabetValidator implements ConstraintValidator<OnlyAlphabetValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isAlphabet(value);
        }
    }
    
    

     


    @OnlyDownerValid

    영어 소문자인 경우만 허용되는 Annotation입니다.

     

    OnlyDownerValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = OnlyDownerValidator.class)
    public @interface OnlyDownerValid {
        String message() default "Required only alphabet downercase";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    OnDownerValidator.java

    public class OnlyDownerValidator implements ConstraintValidator<OnlyDownerValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isDowner(value);
        }
    }
    
    

     


    @OnlyKoreanValid

    한글인 경우만 허용되는 Annotation입니다.

     

    OnlyKoreanValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = OnlyKoreanValidator.class)
    public @interface OnlyKoreanValid {
        String message() default "Required only Korean";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    OnlyKoreanValidator.java

    public class OnlyKoreanValidator implements ConstraintValidator<OnlyKoreanValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isKorean(value);
        }
    }
    
    

     


    @OnlyNumericValid

    문자열이 숫자인 경우만 허용되는 Annotation입니다.

     

    OnlyNumericValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = OnlyNumericValidator.class)
    public @interface OnlyNumericValid {
        String message() default "Required only Number";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    OnlyNumericValidator.java

    public class OnlyNumericValidator implements ConstraintValidator<OnlyNumericValid, String> {
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isNumeric(value);
        }
    }
    
    

     


    @OnlyNumericWithAlphabetValid

    문자열이 숫자이거나 영문자인 경우에만, 허용되는 Annotation입니다.

     

    OnlyNumericWithAlphabetValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = OnlyNumericWithAlphabetValidator.class)
    public @interface OnlyNumericWithAlphabetValid {
        String message() default "Required only digit or alphabet";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    OnlyNumericWithAlphabetValidator.java

    public class OnlyNumericWithAlphabetValidator implements ConstraintValidator<OnlyNumericWithAlphabetValid, String>{
        @Override
        public void initialize(OnlyNumericWithAlphabetValid constraintAnnotation) {
            ConstraintValidator.super.initialize(constraintAnnotation);
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.onlyNumericWithAlphabet(value);
        }
    }
    
    

     


    @OnlyUpperValid

    영문자가 대문자로만 이루어진 경우 허용되는 Annotation입니다.

     

    OnlyUpperValid.java

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = OnlyUpperValidator.class)
    public @interface OnlyUpperValid {
        String message() default "Required only alphabet uppercase";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    OnlyUpperValidator.java

    public class OnlyUpperValidator implements ConstraintValidator<OnlyUpperValid, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return RegexUtils.isUpper(value);
        }
    }
    
    

     

     


    Implementation (for project)

    위에서 구현한 common 패키지를 상속받아서, 구현하였습니다.

    또한 유효성 체크를 위한 열거형 (enum) 같은 값이 있는 경우, 별도로 구현하였습니다.

    프로젝트마다 “Validation Check”는 상이하기 때문에, 이 부분은 샘플로서 참고해주시길 바랍니다.

     


    validator (패키지명)

    @EmailValid

    프로젝트에서 사용할 이메일 유효성 체크 Annotation 입니다.
    acppectedRegexList는 정규식을 가지고 있어
    이메일 중에서 gmail.com와 naver.com만 허용하도록 하였습니다.

     

    EmailValid.java

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @NotBlank(message = "email은 빈 공간을 가질 수 없습니다.")
    @NoSpecialValid(message = "email에 특수문자가 들어갈 수 없습니다.")
    @Constraint(validatedBy = EmailValidator.class)
    public @interface EmailValid {
        String[] acceptedRegexList() default {
            "\\w+@gmail.com",
            "\\w+@naver.com"
        };
    
        String message() default "허용되지 않는 이메일 주소입니다";
    
        Class[] groups() default {};
    
        Class[] payload() default {};
    }
    
    

     

    EmailValidator.java

    public class EmailValidator implements ConstraintValidator<EmailValid, String> {
        private List<String> acceptedRegexList;
    
        @Override
        public void initialize(EmailValid constraintAnnotation) {
            ConstraintValidator.super.initialize(constraintAnnotation);
            this.acceptedRegexList = new ArrayList<>();
            this.acceptedRegexList.addAll(List.of(constraintAnnotation.acceptedRegexList()));
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            for(String format : this.acceptedRegexList){
                if(value.matches(format))
                    return true;
            }
            return false;
        }
    }
    
    

     


    @LoginIdValid

    로그인 아이디에 대한 유효성 검사 Annotation입니다.
    이미 공통에서 구현한 Annotation과 Hibernate Validation을 상속받아서 사용하기 때문에
    구현부는 별도로 존재하지 않습니다.

     

    LoginIdValid.java

    이미 구현된 common 내부의 OnlyNumbericWithAlphabetValid를 사용하고 있기 때문에, 별도의 구현부는 존재하지 않습니다.

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Length(min = 8, max = 15, message = "loginId는 8자 이상, 15자 이하로 가능합니다.")
    @NotBlank(message = "loginId는 빈 공간을 가질 수 없습니다.")
    @OnlyNumericWithAlphabetValid(message = "loginId는 숫자와 영어로만 이루어집니다.")
    @Constraint(validatedBy = {})
    public @interface LoginIdValid {
        String message() default "";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    

     


    @MovieCategoryValid

    이 Annotation은 MovieCategory라는 Enum의 Validation Check를 위해서 사용합니다.
    클라이언트에서 입력받은 String 문자열이, 미리 정의 된 Enum에 존재하는 경우 허용하며, 없는 경우 허용하지 않습니다.

     

    MovieCategoryValid.java

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = MovieCategoryValidator.class)
    public @interface MovieCategoryValid {
        String message() default "Invalid Movie Category Type";
        Class[] groups() default {};
        Class[] payload() default {};
    }
    
    

     

    MovieCategoryValidator.java

    public class MovieCategoryValidator implements ConstraintValidator<MovieCategoryValid, String> {
    
        @Override
        public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
            if (MovieCategory.UNKNOWN.equals(MovieCategory.valueOfName(s))) {
                return false;
            } else {
                return true;
            }
        }
    }
    
    

     

    이제 계획한 모든 구현이 끝났습니다.

    실제 Request DTO 파라미터에 “Validation Check”를 연결해봅시다. 📎

     

     


    Usage

    AccountAddRequest.java

    message를 Annotation에서 정의를 했지만, DTO 단에서 Override가 가능합니다.

    즉, 경우에 따라 한글로 잘 맞춰주어 경우마다 대응이 가능합니다. 😄

    @Data
    public class AccountAddRequest {
        @LoginIdValid(message = "loginId이 사용 불가능한 값입니다")
        private String loginId;
    
        @EmailValid(message = "email이 사용 불가능한 값입니다")
        private String email;
    
        @OnlyKoreanValid(message = "name은 한글만 입력이 가능합니다")
        private String name;
    
        @IsDateValid(message = "birthDate는 날짜 형식만 가능합니다 (yyyy.mm.dd)")
        private String birthDate;
    
        @OnlyNumericValid(message = "age는 숫자만 입력이 가능합니다")
        private String age;
    
        @IsUrlPathValid(message = "profileUrl은 URL 형식만 가능합니다")
        private String profileUrl;
    
        @IsYnValid(message = "isEnable이 유효한 값이 아닙니다. (Y, N)")
        private String isEnable;
    
        @IsIpValid(message = "clientIp는 IP 형식만 가능합니다")
        private String clientIp;
    
        @IsUuidValid(message = "reqId은 UUID 형식만 가능합니다")
        private String reqId;
    }
    
    

     

    ValidController.java

    이제 AccountAddRequest를 요청 DTO로 가지는 Annotation Controller를 추가합니다.

    @PostMapping("/account")
    public ResponseEntity<ApiBase> addAccount(@RequestBody @Valid AccountAddRequest request){
        AccountAddResponse response = new AccountAddResponse();
        response.setLoginId(request.getLoginId());
        return ResponseEntity.status(HttpStatus.CREATED).body(ApiBase.builder().message("").response(response).build());
    }
    

     

     


    Test Run

    curl --location --request POST 'http://localhost:8080/account' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "loginId": "testid123",
        "email": "test@gmail.com",
        "name": "김철수",
        "birthDate": "2001.10.30",
        "age": "25",
        "profileUrl": "ns-profile.company.com/image/2f48f241-9d64-4d16-bf56-70b9d4e0e79a.jpg",
        "isEnable": "N",
        "clientIp": "203.133.203.103",
        "reqId": "2f48f241-9d64-4d16-bf56-70b9d4e0e79a"
    }'
    
    

    localhost:8080에 서버를 띄우고 위처럼 요청을 날려보면, 오류가 발생합니다.

    (profileUrl에 http, https와 같은 프로토콜이 빠졌기 때문입니다 😁)

     

    결과는 아래와 같습니다.

    {
        "message": "profileUrl은 URL 형식만 가능합니다",
        "response": ""
    }
    
    

    정상적으로 Exception Handle가 작동한 모습입니다.


    혹시 Exception Handler를 구현하지 않았다면, 아래 코드를 확인하고 추가해보세요.

    GlobalErrorHandler.java

    @ControllerAdvice
    @Slf4j
    public class GlobalErrorHandler {
    
        @ExceptionHandler({MethodArgumentNotValidException.class})
        public ResponseEntity<ApiBase> handleResponseBodyError(MethodArgumentNotValidException ex) {
            log.error("Exception Caught in handleRequestBodyError : {}", ex.getMessage());
    
            var error = ex.getBindingResult().getAllErrors().stream()
                    .map(DefaultMessageSourceResolvable::getDefaultMessage)
                    .sorted()
                    .collect(Collectors.joining(", "));
    
            log.error("Error is : {}", error);
    
            return ResponseEntity.badRequest()
                    .body(ApiBase.builder().message(error).response("").build());
        }
    
    }
    
    

     

     


    이번 글에서는 "유용하게 사용할 수 있는 Custom Validation Annotation"을 구현했습니다.

    다음 글은 열거형 Enum 형식이 아닌, 데이터베이스의 테이블에 저장되어 있는 컬럼 값을 참조하여, "Bean Validation Annotation"을 초기화가 시키고 이어서 "Validation Check"가 가능한지 검토하고 직접 구현하는 글을 작성하겠습니다. 🔆

    (즉 유효성 체크의 기준이 DB 테이블이 가능한가에 대한 검토와 구현입니다)

    긴 글 읽어주셔서 감사합니다. 😁

     

     

    반응형
Designed by Tistory.