Frame Work/FastAPI

FastAPI의 이상한 파일 업로드

j4ko 2024. 7. 15. 00:43
728x90
반응형

목차

     

    개요 

    최근 FastAPI를 이용해 파일 업로드 기능을 다뤄야 했다. 대용량 파일 업로드 처리나 분산 파일 업로드 서버를 구현하는 게 아닌 "단순" 파일 업로드가 가능한 API만 구현하면 끝인 상황이었다.  FastAPI 문서나 구글 검색을 참조해 이리저리 구현하던 도중에 Type Hinting을 통해 생성되는 Swagger 문서에서 특징을 발견할 수 있었는데 이 글은 겪었던 그러한 특징에 대해 정리한 글이다.

     

     

    1.  단건 파일업로드 처리

    1.1 UploadFile 객체와 bytes를 이용한 파일 업로드 예제

    다음은 FastAPI 문서에서 제공되는 파일업로드를 구현하기 위한 기본 코드 예제이다.

     

    from fastapi import UploadFile
    
    fileupload_router = APIRouter(tags=['FILE_UPLOAD_OBJECT'], prefix='/api/v1')
    
    @fileupload_router.post(path="")
    def file_upload_example(
            upload_files: UploadFile
    ):
        print(upload_files)
        return JSONResponse(status_code=200, content={})

    fastapi를 설치하면 제공되는 UploadFile 객체를 이용한 방식이다. "단건" 파일 업로드에 해당하며 이 경우 Swagger Docs는 다음과 같이 제공된다.

    UploadFile을 이용한 Swagger Docs

    File은 Bytes로 처리되기에 UploadFile 대신 bytes를 사용해 Type Hinting을 걸어줘도 가능하다. 

    from fastapi import File
    
    @fileupload_router.post(path="")
    def file_upload_example(
            upload_files: bytes = File()
    ):
        print(upload_files)
        return JSONResponse(status_code=200, content={})

    단 위의 코드 예시처럼 FastAPI에서 제공하는 "File" 객체를 함께 사용해줘야 한다. 그렇지 않은 경우 다음과 같이 입력값 검증단계에서부터 실패한다.

    bytes를 이용한 경우도 UploadFile과 같은 Swagger Docs가 제공된다.

    bytes를 이용한 Swagger Docs

    1.2. UploadFile 객체와 bytes를 사용하는 파일 업로드 차이

    UploadFile과 bytes를 사용하는 방식의 차이점은 UploadFile을 사용하는 쪽이 다루기 더 편하다는 점이다. UploadFile을 사용하는 경우 다음과 같이 Wrapping이 되기 때문에 객체처럼 다룰 수 있게 된다.

    @fileupload_router.post(path="")
    def file_upload_example(
            upload_files: UploadFile
    ):
        print(upload_files.file)
        print(upload_files.filename)
    
        return JSONResponse(status_code=200, content={})

    UploadFile은 FastAPI에서 제공해 주는 객체이기 때문에 Type Hinting이 편리하다는 것이 장점이다.

    bytes를 이용해 파일을 다룰 경우에는 다음과 같이 추가적인 코드가 발생할 수 있다.

    @fileupload_router.post(path="")
    def file_upload_example(
            upload_files: bytes = File()
    ):
        import tempfile
        import os
    
        with tempfile.NamedTemporaryFile(delete=True) as temp_file:
            temp_file.write(upload_files)
    
            print(temp_file.file)
            print(temp_file.name)
            print(os.path.getsize(temp_file.name) / 1024 / 1024)
    
        return JSONResponse(status_code=200, content={})

    upload_files가 bytes이기 때문에 Type Hinting이 File 객체를 통하지 않는다.

     

     

    2.  다중 파일 업로드 처리

     

    2.1 UploadFile을 이용한 다중 파일 업로드 처리하기

    파일을 단건이 아닌 리스트로 받아서 처리할 경우 UploadFile을 typing 모듈의 List를 이용해서 다음과 같이 Type Hinting을 걸어줄 수 있다.

    @fileupload_router.post(path="")
    def file_upload_example(
            upload_files: List[UploadFile]
    ):
        return JSONResponse(status_code=200, content={})

     

    이 경우 Swagger Docs가 다음과 같이 여러 파일을 입력받을 수 있는 UI를 제공할 수 있도록 변경된다.

     

    2.2 UploadFile을 처리하고 싶지 않은 경우

     

    어떤 요청이 파일이 필요로 하지 않는다면 다음과 같이 변경해야 한다.

    @fileupload_router.post(path="")
    def file_upload_example(
            upload_files: List[UploadFile] = File(default=[])
    ):
        return JSONResponse(status_code=200, content={})

    위와 같이 변경하고 Swagger Docs를 확인해 보면 '2.1'에서는 required라고 명시된 부분이 사라져 있단 것을 알 수 있다.

    그러나 이는 파일을 아예 보내지 않는 경우를 처리한다. 즉, 파일을 보내고 싶지 않은 경우 key 값마저 빼버려야 한다.

     

    2.3 UploadFile을 None으로 처리가능한가?

    이 토픽은 UploadFile에 "Send Empty Value"를 체크해서 요청을 전송하거나 값을 입력하지 않은 경우 기본값이 "None"으로 할당된 경우를 말한다. 그러나 아쉽게도 이 방법은 찾지 못했다. 막연히 생각했을 때 Type Hinting을 다음과 같이 줄 수 있을 것이다.

    @fileupload_router.post(path="")
    def file_upload_example(
            upload_files: Optional[List[bytes]] = File(default=[])
    ):
        return JSONResponse(status_code=200, content={})

    그러나 위와 같이 사용하는 경우 Swagger Docs에서 파일 업로드가 제공되지 않는다.

     

     

    마치며

    파일 업로드를 다루면서 알게 된 짤막한 사실을 짧게 기록해 봤다. 본문에 기재한 내용 외에도 조금 더 많은 방법을 시도해 봤는데 해당 방법은 추후 이 글을 수정하는 형태로 추가해야겠다.

     

    728x90
    반응형