직접 Custom 어노테이션을 만들어서 사용해 보자
들어가며
프로젝트 진행 중에 기본적으로 제공되지 않는 @Valid 외에도 추가적인 검증이 필요한 필드가 있었습니다. 따라서 직접 검증을 수행할 수 있는 커스텀 어노테이션을 만들어 보겠습니다. 이를 통해 @Valid 어노테이션을 사용하는 방법을 알아보겠습니다.
@Valid
@Valid 어노테이션은 자바에서 Bean Validation API에서 제공되는 어노테이션 중 하나로, 주로 객체 그래프 내의 객체들에 대한 유효성 검사를 수행할 때 사용됩니다.
기본적으로 지원하는 어노테이션은
- @NotNull: 필드가 null이 아닌지 확인
- @Size: 문자열, 컬렉션 또는 배열의 크기를 검사
- @Min, @Max: 숫자 필드의 최솟값 및 최댓값을 검사
- @Pattern: 문자열 필드가 정규식 패턴과 일치하는지 확인
등과같이 여러 개의 어노테이션을 지원합니다. 하지만 현재 검증하려고 하는 필드의 대해서는 지원하지 않아 직접 어노테이션을 구현해 사용해야 합니다.
private List<Map<String,String>> cookeSteps;
현재 진행 중인 프로젝트에서 위와 같은 List <Map>의 필드값의 Map.value의 대해서 값을 입력했는지 안 했는지 검증을 해야 하나. 기본적으로 지원하지 않아 직접 커스텀한 에노테이션을 사용해야 합니다.
ConstraintValidator
커스텀한 @Valid를 사용하기 위해서는 Jakarta Bean Validation API의 중요한 부분 중 하나인 ConstraintValidator 인터페이스를 구현해 직접 어노테이션을 만들어야 합니다.
먼저 ConstraintValidator의 인터페이스는 다음과 같이 구성되어 있습니다.
ConstraintValidator 인터페이스는 제약조건 'A'와 검증할 객체 유형 'T' 에 대한 유효성 검사를 구현할 수 있습니다.
- initialize(초기회 메서드)
- Validator를 초기화하기 위해 호출
- Annotation에 대한 정보를 받아 초기화를 진행
- isValid(유효성 검사)
- value의 대한 값에 유효성 검사를 진행
이제 ConstraintValidator를 구현해 직접 에노테이션을 구현해 보도록 하겠습니다.
CustomValid
mapListValidator
public class mapListValidator implements ConstraintValidator<NotEmptyMapValue, List<Map<String,String>>> {
@Override
public boolean isValid(List<Map<String, String>> maps, ConstraintValidatorContext context) {
if (maps==null || maps.isEmpty()){
return false;
}
for (Map<String, String> map : maps) {
if (map==null|| map.isEmpty()){
return false;
}
for (String value : map.values()){
if (value==null || value.trim().isEmpty()){
return false;
}
}
}
return true;
}
}
- ConstraintValidator ( A' 부분의 생성할 에노테이션 명, 'T'에 검증 대생의 객체)를 구현한 mapListValidator 클래스를 하나 생성해 줍니다.
- isValid를 Override를 통해 구현할 검증 로직을 작성해 줍니다.(해당 로직은 map의 대해서 모든 값의 대해서 검증)
NotEmptyMapValue
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = mapListValidator.class)
public @interface NotEmptyMapValue {
String message() default "List<Map>의 값이 빈 값입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- @Valid를 사용한 커스텀 어노테이션을 만들어줍니다.
- @Target(ElementType.FIELD) : 필드에 적용
- @Retention(RetentionPolicy.RUNTIME) : 런타임에도 유지되야 함
- @Constraint(validatedBy = mapListValidator.class): 이 어노테이션의 유효성을 검사하기 위해 사용될 mapListValidator 클래스를 지정
- String message() default "List <Map>의 값이 빈 값입니다.";: 유효성 검사 실패 시 반환될 기본 메시지를 설정
- Class <?>[] groups() default {};: 어노테이션 그룹을 지정
- Class<? extends Payload>[] payload() default {};: 유효성 검사 결과를 전달하는 데 사용될 부가적인 메타데이터를 지정
이렇게 생성해 준 @NotEmptyMapValue를 이제 사용할 Dto 필드값의 사용해 줄 수 있다.
@NotEmptyMapValue(message = "변경할 레시피의 조리순서를 입력해주세요")
private List<Map<String,String>> cookeSteps;
@NotEmptyMapValue 테스트
RecipeUpdateRequest
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RecipeUpdateRequest {
@NotEmpty(message = "변경할 레시피의 제목를 입력해주세요")
private String title;
@NotEmpty(message = "변경할 레시피의 난이도를 입력해주세요")
private String cookLevel;
@NotEmpty(message = "변경할 레시피의 인원수를 입력해주세요")
private String people;
@NotEmpty(message = "변경할 레시피의 재료를 입력해주세요")
@Schema(example = "{\"ingredients\":[\"재료1\", \"재료2\"]}")
private List<String> ingredients;
@NotEmpty(message = "변경할 레시피의 시간를 입력해주세요")
private String cookTime;
@NotEmptyMapValue(message = "변경할 레시피의 조리순서를 입력해주세요")
private List<Map<String,String>> cookeSteps;
}
수정 API
@PostMapping(value = "/admin/update/{recipe-id}",consumes= MediaType.MULTIPART_FORM_DATA_VALUE ,produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> updateRecipe(@PathVariable(name = "recipe-id")Long recipeId ,
@Valid @RequestPart RecipeUpdateRequest recipeUpdateRequest, BindingResult bindingResult,
@RequestPart MultipartFile file){
try {
if (bindingResult.hasErrors()){
List<String> errors = new ArrayList<>();
for (FieldError error : bindingResult.getFieldErrors()){
errors.add(error.getDefaultMessage());
}
return ResponseEntity.badRequest().body(new ErrorResponse<>(false,"모든 값을 입력해주세요",errors));
}
recipeService.updateRecipe(recipeId,recipeUpdateRequest,file);
return ResponseEntity.ok(new ControllerApiResponse<>(true,"레시피 수정 성공"));
}catch (NoSuchElementException e){
throw new BadRequestException(e.getMessage());
} catch (BadRequestException e){
throw new BadRequestException(e.getMessage());
} catch (Exception e){
e.printStackTrace();
throw new ServerErrorException("서버오류");
}
}
위와 같이 Reuqest의 사용한 Custom에노테이션을 사용해서 작동이 잘되는지 한번 봐보도록 하겠습니다.
응답을 보시면 cook_steps의 빈값을 넣고 API의 요청을 보내면 400 Bad Request를 반환해 직접 지정한 message의 내용이 반환되는 것을 볼 수 있습니다.
'Spring > SpringBoot' 카테고리의 다른 글
[SpringBoot] @Qualifier 사용하기 (0) | 2024.06.18 |
---|---|
[SpringBoot] SSE를 이용한 실시간 알림 구현하기! (0) | 2024.06.13 |
[Spring-Boot] Srpingdoc OpenApi 스웨거(Swagger) 사용하기 (0) | 2024.04.24 |
[SpringBoot] 스프링 클라우트 볼트(Vault)를 활용하여 정보관리 (1) | 2024.01.04 |
[SpringBoot] JAR(JAVA Archive) 파일 생성 하는법 (0) | 2023.12.08 |