개요
HTTP 요청과 함께 특정 데이터가 전달될 경우, 개발자는 이런 데이터가 올바른지 검증하는 과정이 필요하다. Java에서는 이런 데이터를 어노테이션으로 편리하게 검증할 수 있는 표준 기술을 제공하는데, 이 기술을 Bean Validation이라고 부른다.
보통 아래와 같이 Controller의 메서드에서 파라미터로 들어오는 데이터에 @Valid 혹은 @Validated(Spring에서 제공) 라는 어노테이션을 붙여서 데이터 검증을 시작한다.
@ResponseStatus(HttpStatus.CREATED)
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CategoryResponse save(@Validated @ModelAttribute CategorySaveRequest requestDto) {
return categoryService.save(requestDto);
}
그리고 DTO 클래스를 만들어 각 필드에 Bean Validation을 구현한 Hibernate Validator가 제공하는 @NotBlank, @NotNull, @Min, @Max 같은 검증 어노테이션을 붙여 데이터 검증을 진행할 수 있다.
@Getter
@Builder
public static class CategorySaveRequest {
@NotBlank
private final String name;
private final MultipartFile iconFile;
}
그런데, 위 DTO의 name 처럼 범용적으로 사용되는 타입(String, Long, LocalDate etc.)들을 검증하는 어노테이션을 제공하지만, iconFile처럼 MultipartFile 같은 타입의 데이터를 검증할 수 있는 어노테이션은 제공하지 않는다. 그래서 해당 타입의 데이터 검증을 위해서는 별도의 방법을 찾아야 한다.
이번 글에서는 개인 프로젝트 개발 중에 MultipartFile 타입의 데이터를 검증하는 방법을 찾아냈고, 이를 어떻게 적용시켰는지 적어보려 한다.
MultipartFile 검증 방법
1. 직접 검증
사실, MultipartFile 타입의 데이터를 검증하는 제일 간단한 방법은 controller에서 해당 데이터를 직접 확인하는 것이다.
먼저, 아래와 같이 MultipartFile 데이터가 올바른지 확인하는 메서드가 담긴 추상 클래스를 만들었다.
public abstract class FileUtils {
public static boolean isValidFile(MultipartFile file) {
return file != null && !file.isEmpty();
}
}
그리고, MultipartFile 타입의 데이터를 전달 받는 controller마다 이를 적용해주면 된다.
@ResponseStatus(HttpStatus.CREATED)
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CategoryResponse save(@Validated @ModelAttribute CategorySaveRequest requestDto) {
// 검증 실패
if (!FileUtils.isValidFile(requestDto.getIconFile()) {
// 검증 실패 로직 적용
}
return categoryService.save(requestDto);
}
이렇게 아주 간단하게 MultipartFile 타입의 데이터가 올바른지 검증할 수 있다. 하지만, 해당 검증 코드를 MultipartFile을 받는 모든 controller의 메서드에 적용시켜야 하는 것은 매우 비효율적이다. 그리고, 이러한 방식보다는 Bean Validation을 이용한 검증을 진행하고 싶었다.
2. ConstraintValidator
Java의 표준 검증 라이브러리인 jakarta.validation은 ConstraintValidator라는 인터페이스를 제공한다.
이 인터페이스는 어노테이션과 해당 어노테이션이 적용된 타입을 통해 검증을 진행하는 기능을 제공한다. 우리가 @NotBlank, @NotNull, @NotEmpty, @Min, @Max 등의 검증 어노테이션들을 사용하면, 이 인터페이스가 제공하는 기능을 통해 검증이 이루어진다.
하지만, Hibernate Validator는 MultipartFile을 검증하는 어노테이션을 구현하지 않았기 때문에, ConstraintValidator를 적용하기 위해서는 직접 어노테이션을 만들어야 한다. 아래와 같이 어노테이션을 직접 생성하자.
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidFile {
String message() default "올바르지 않은 형태의 파일입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
그리고, 위의 어노테이션에 적용시킬 ConstraintValidator를 구현하자.
public class MultipartFileValidator implements ConstraintValidator<ValidFile, MultipartFile> {
@Override
public boolean isValid(MultipartFile file, ConstraintValidatorContext context) {
return FileUtils.isValidFile(file);
}
}
마지막으로 방금 만든 @ValidFile 어노테이션에 @Constraint 어노테이션을 추가하고 위의 구현체를 설정하자.
@Documented
@Constraint(validatedBy = MultipartFileValidator.class) // ConstraintValidator 설정
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidFile {
String message() default "올바르지 않은 형태의 파일입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
이렇게 MultipartFile 검증을 위한 어노테이션 준비는 끝났다. 이제 이 어노테이션을 DTO에 있는 MultipartFile 타입에 적용하면 검증이 적용된다.
@Getter
@Builder
public static class CategorySaveRequest {
@NotBlank
private final String name;
@ValidFile
private final MultipartFile iconFile;
}
테스트
위에서 제작한 @ValidFile이 제대로 작동하는지 테스트해보자.
@Test
@DisplayName("Category Request DTO 검증 실패 테스트")
void categoryDtoFailed() throws Exception {
// given
MockPart name = new MockPart("name", "categoryName".getBytes());
MockMultipartFile iconFile = new MockMultipartFile("iconFile", (byte[]) null);
// when
ResultActions saveResult = mockMvc.perform(multipart("/api/categories")
.file(iconFile)
.part(name));
// then
saveResult
.andExpect(status().is4xxClientError())
.andExpect(jsonPath("error.status").value(NOT_VALID_ARGUMENT.getStatus()))
.andExpect(jsonPath("error.code").value(NOT_VALID_ARGUMENT.getCode()))
.andExpect(jsonPath("error.message").value(NOT_VALID_ARGUMENT.getMessage()))
.andExpect(jsonPath("error.fieldErrors").isNotEmpty());
log.info(saveResult.andReturn()
.getResponse()
.getContentAsString(StandardCharsets.UTF_8)); // 검증 실패 오류 메시지 확인
}
MockMultipartFile을 사용하여 유효하지 않은 이미지 파일을 생성하고 이를 전달하는 테스트 코드를 작성했다. 그리고 마지막에 검증 실패 오류 메시지를 확인하기 위해 로그로 응답을 출력하였다. 테스트를 실행하면 아래와 같이 검증 실패 오류 메시지를 확인할 수 있다.
{
"error": {
"status":400,
"code":"002",
"message":"Arguments Not Validated (데이터 검증에 실패했습니다).",
"fieldErrors":[
{
"field":"iconFile",
"message":"올바르지 않은 형태의 파일입니다."
}
]
}
}
위의 오류 메시지("올바르지 않은 형태의 파일입니다.")는 @ValidFile 어노테이션을 제작할 때 message()에 설정한 기본 메시지이다. 따라서, @ValidFile을 이용한 MultipartFile 타입 검증이 잘 작동한다는 것을 확인할 수 있다.
후기
이렇게 ConstraintValidator 인터페이스와 어노테이션을 활용하여 MultipartFile 타입의 데이터를 검증하는 방법을 알아보았다. MultipartFile 이외에도, 다른 타입들도 이와 같이 어노테이션을 새로 제작하고 ConstraintValidator를 구현한다면 Bean Validation을 적용할 수 있다.
'Spring' 카테고리의 다른 글
[Spring / AWS] Spring Boot 3 + AWS Lambda 사용하기 (0) | 2024.04.26 |
---|---|
[Spring] Pagination 기본값 설정하기 (0) | 2024.04.03 |
[Spring] MultipartFile 테스트 시 405 Error가 생기는 이유 (0) | 2024.01.18 |
[Spring] MultipartFile 테스트하는 방법 (0) | 2024.01.17 |
[Spring] Spring REST Docs 상세 설정 (0) | 2024.01.02 |