Skip to content

파일 확장자 체크 with custom validator

업로드 파일의 타입을 체크하는 로직을 커스텀 밸리데이터로 구현한다.

  1. 파일업로드는 Multipart 업로드를 사용한다.
  2. 요청 DTO 클래스의 컬럼에 어노테이션을 적용하는 방식으로 제약사항을 설정한다.
  3. 허가되는 확장자는 신규 어노테이션에 문자열 목록으로 설정한다.
  1. 어노테이션 생성
    FileExtension.kt
    @Target(AnnotationTarget.FIELD)
    @Retention(AnnotationRetention.RUNTIME)
    @Constraint(validatedBy = [FileExtensionValidator::class])
    annotation class FileExtension (
    val message: String = "확장자가 일치하지 않습니다.",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = [],
    val allowedExtension: Array<String> = []
    )
  2. ConstraintValidator 구현제 생성
    FileExtension.kt
    class FileExtensionValidator : ConstraintValidator<FileExtension, MultipartFile> {
    private lateinit var allowedExtension: List<String>
    override fun initialize(constraintAnnotation: FileExtension) {
    allowedExtension = constraintAnnotation
    .allowedExtension
    .map { it.lowercase(Locale.ROOT) }
    .toList()
    }
    override fun isValid(file: MultipartFile, context: ConstraintValidatorContext): Boolean {
    //필수입력 여부는 별도의 NotNull 어노테이션으로 제약을 설정한다.
    if (file.isEmpty) {
    return true
    }
    val extension: String = getExtension(file.originalFilename).lowercase(Locale.ROOT)
    return allowedExtension.contains(extension)
    }
    }
  3. 요청 DTO 클래스의 멤버에 제약 설정
    any request dto class
    @field:Schema(description = "사진파일", example = "사진파일")
    @field:NotNull
    @field:FileExtension(allowedExtension = ["GIF", "JPEG", "JPG", "PNG"])
    var photo: MultipartFile,
  4. Controller 메소드에 Valid(Validated) 어노테이션 추가
    any controller class
    @Operation(summary = "사진게시판 등록", description = "사진게시판 등록 API")
    @PostMapping
    fun insertPhotoBoard(@Valid @ModelAttribute param: InsertPhotoBoardRequest): ResponseEntity<ResponseDto<Unit>> {
    log.info("insertPhotoBoard param: $param")
    val model = param.toModel()
    photoBoardService.insertPhotoBoard(model)
    return ResponseEntity.status(HttpStatus.CREATED).body((ResponseDto(Unit)))
    }
  5. Validation 오류가 발생하면 MethodArgumentNotValidException 예외가 던져지는데, 이 예외가 발생시 405 Bad Request 오류가 발생하도록 설정
    RestApiExceptionAdvice.kt
    @ExceptionHandler(MethodArgumentNotValidException::class)
    @ResponseBody
    fun methodArgumentNotValidExceptionHandler(e: MethodArgumentNotValidException): ResponseEntity<ResponseDto<Unit>> {
    log.error("### MethodArgumentNotValidException occurred", e)
    val anyItem = e.bindingResult.fieldErrors[0]
    return ResponseEntity
    .badRequest()
    .body(ResponseDto(ApiResultCode.WRONG_PARAMETER, String.format("%s (%s)", anyItem.defaultMessage, anyItem.field)))
    }
  6. 테스트
    any test class
    @DisplayName("신규 등록시 유효성 체크 - 첨부파일 실패 케이스")
    @Test
    fun insertValidationTest() {
    return Given {
    spec(multipartRequestSpecification)
    formParam("title", "test")
    formParam("content", "test")
    multiPart("photo", "sample1.webp", readResourceFileAsBytes("sample/image/sample1.webp"), "image/webp")
    } When {
    post("/photo-board")
    } Then {
    statusCode(HttpStatus.SC_BAD_REQUEST)
    } Extract {
    println(body().asString())
    }
    }
  7. 로그 확인
    2026-02-11 14:25:27,749 [ERROR] [http-nio-auto-1-exec-2] b.m.c.c.e.a.RestApiExceptionAdvice: ### MethodArgumentNotValidException occurred
    org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<biz.mintchoco.carrot.common.dto.ResponseDto<kotlin.Unit>> biz.mintchoco.carrot.api.photo_board.controller.PhotoBoardController.insertPhotoBoard(biz.mintchoco.carrot.api.photo_board.dto.InsertPhotoBoardRequest): [Field error in object 'insertPhotoBoardRequest' on field 'photo': rejected value [org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@697db0a3]; codes [FileExtension.insertPhotoBoardRequest.photo,FileExtension.photo,FileExtension.org.springframework.web.multipart.MultipartFile,FileExtension]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [insertPhotoBoardRequest.photo,photo]; arguments []; default message [photo],[Ljava.lang.String;@3115bef]; default message [확장자가 일치하지 않습니다.]]
    at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:158)
    ...
    at java.base/java.lang.Thread.run(Thread.java:1583)
    {"payload":null,"isSuccess":false,"resultCode":"WRONG_PARAMETER","message":"확장자가 일치하지 않습니다. (photo)"}