Light Blue Pointer
본문 바로가기
Developing/TIL(Develop)

Custom annotation으로 Bean Validation + Parameter Validation 하기 (어노테이션 정의해서 검증하기)

by Greedy 2024. 3. 25.

1. Bean Validation

참고 자료: 

https://www.baeldung.com/spring-mvc-custom-validator

baeldung 을 읽으면서 따라해봄

3단계 과정

1. Annotation 만들기

2. Validator 만들기

3. 사용하기

 

나는 들어오는 값이 정해진 값 중의 하나인지 검증하고 싶었다

1) The New Annotation

@Documented
@Constraint(validatedBy = CategoryValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Category {
    String message() default "Invalid category";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

그래서 이렇게 만들어줌

 

2) Creating a Validator

public class CategoryValidator implements ConstraintValidator<Category, String> {

    @Override
    public void initialize(Category category) {
    }

    @Override
    public boolean isValid(String categoryEnum,
        ConstraintValidatorContext cxt) {
        return categoryEnum.equals(CategoryEnum.ALL.toString())
            ||categoryEnum.equals(CategoryEnum.ASIAN.toString())
            ||categoryEnum.equals(CategoryEnum.BURGER.toString())
            ||categoryEnum.equals(CategoryEnum.CHICKEN.toString())
            ||categoryEnum.equals(CategoryEnum.CHINESE.toString())
            ||categoryEnum.equals(CategoryEnum.JAPANESE.toString())
            ||categoryEnum.equals(CategoryEnum.KOREAN.toString())
            ||categoryEnum.equals(CategoryEnum.LUNCHBOX.toString())
            ||categoryEnum.equals(CategoryEnum.PIZZA.toString())
            ||categoryEnum.equals(CategoryEnum.SNACK.toString())
            ||categoryEnum.equals(CategoryEnum.WESTERN.toString())
            ;
    }

}

 

 

3) Applying Validation Annotation , Controller에 적용

baeldung에서는 이렇게 사용한다!

@ContactNumberConstraint
private String phone;
@PostMapping("/addValidatePhone")
    public String submitForm(@Valid ValidatedPhone validatedPhone,
      BindingResult result, Model m) {
        if(result.hasErrors()) {
            return "phoneHome";
        }
        m.addAttribute("message", "Successfully saved phone: "
          + validatedPhone.toString());
        return "phoneHome";
    }

 

하지만 우리 프로젝트는 BindingResult 없이 GlobalExceptionHandler 에서 MethodArgumentNotValidException을 AOP로 처리를 해서 저 부분은 안 넣었다.

GlobalHandler로 에러처리를 하는 부분)

@ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<List<String>> handleMethodArgumentNotValidException(
        BindingResult bindingResult) {
        List<String> errors = bindingResult.getFieldErrors().stream()
            .map((FieldError fieldError) -> fieldError.getField() +" "+ fieldError.getDefaultMessage())
            .toList();
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "입력값이 잘못되었습니다", errors);
    }

 

 

내가 프로젝트에 적용한 코드는 다음과 같다 

DTO에 적용했다

public record PostCategoryRequest (
    @Category
    String category
){
}

 

 

4) PostMan으로 테스트해봄

되는 카테고리를 날리면 잘 됨

{
    "category":"ALL"
}
{
    "status": 200,
    "message": "글 카테고리별 조회에 성공했습니다.",
    "data": [
        {
            "id": 1,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 5000,
            "deadline": "2024-01-11T00:41:48"
        },
        {
            "id": 2,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 10000,
            "deadline": "2024-01-11T00:41:49"
        },
        {
            "id": 3,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 5000,
            "deadline": "2024-01-11T00:41:50"
        }
    ]
}

 

존재하지 않는 카테고리를 날리면

{
    "category":"PLS"
}
{
    "status": 400,
    "message": "입력값이 잘못되었습니다",
    "data": [
        "category Invalid category"
    ]
}

에러처리가 잘 된다!

 

 

2) Parameter Validation

RequestBody방식이 아니라 RequestParam 방식으로 변경해야 할 일이 있어서

DTO가 아니라 Parameter에서도 검증할 수 있을지 찾아보고 적용해봤다

 

1) Annotation을 정의할때 target에 PARAMETER를 추가해줌

@Documented
@Constraint(validatedBy = CategoryValidator.class)
@Target( {ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Category {
    String message() default "has to be one of the CategoryEnum";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

 

2) Validator는 Bean validation 때와 같다

public class CategoryValidator implements ConstraintValidator<Category, String> {

    @Override
    public void initialize(Category category) {
    }

    @Override
    public boolean isValid(String categoryEnum,
        ConstraintValidatorContext cxt) {
        return categoryEnum.equals(CategoryEnum.ALL.toString())
            ||categoryEnum.equals(CategoryEnum.ASIAN.toString())
            ||categoryEnum.equals(CategoryEnum.BURGER.toString())
            ||categoryEnum.equals(CategoryEnum.CHICKEN.toString())
            ||categoryEnum.equals(CategoryEnum.CHINESE.toString())
            ||categoryEnum.equals(CategoryEnum.JAPANESE.toString())
            ||categoryEnum.equals(CategoryEnum.KOREAN.toString())
            ||categoryEnum.equals(CategoryEnum.LUNCHBOX.toString())
            ||categoryEnum.equals(CategoryEnum.PIZZA.toString())
            ||categoryEnum.equals(CategoryEnum.SNACK.toString())
            ||categoryEnum.equals(CategoryEnum.WESTERN.toString())
            ||categoryEnum.equals("CAFE")
            ;
    }

}

 

3) Controller의 Parameter에 annotation 적용

//글 카테고리별 조회
    @GetMapping("/posts/category/page/{page}")
    public ApiResponse<List<BriefPostResponse>> getPostsByCategory(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable(name = "page") int page,
        @RequestParam(name = "category") @Category String category
    ) {
        return new ApiResponse<>(HttpStatus.OK.value(), "글 카테고리별 조회에 성공했습니다.",
            postReadService.getPostsByCategory(page, category, userDetails.getUser()));
    }