목록으로
FastAPI Storages로 파일 업로드 구현
FastAPI에서 파일 업로드를 처리하는 건 간단하지만, SQLAlchemy ORM 모델에도 편리하게 적용해 사용하려면 조금 손을 써야 해요. 이걸 편리하게 해주는 도구가 FastAPI Storages 패키지죠. 로컬에서 파일을 처리하거나 AWS S3를 이용해 파일을 처리하는 스토리지를 기본 제공하며, 기본 제공되는 스토리지 구현체를 참고하면 자신만의 스토리지를 구현할 수도 있죠. 또한 SQLAlchemy의 ORM 모델에 사용할 수 있는 모델 필드를 제공해주죠. FastAPI Storages를 사용해 파일 업로드를 구현할게요.
설치와 설정
fastapi-storages 패키지를 설치하고요.
따로 설정할 건 없어요. 파일을 업로드 할 디렉터리 정도만 만들면 돼요. pyproject.toml 파일이 있는 경로에 _uploads 디렉터리를 생성하세요.
Attachment 모델 만들기
할 일 하나에 여러 개 파일을 첨부하도록 첨부 파일 정보를 관리하는 모델을 만들게요.
Attachment 모델은 Todo 모델을 향해 다수 대 1(N:1) 관계를 맺어요. FastAPI Storages가 SQLAlchemy용으로 제공하는 모델 필드인 FileType을 사용해서 SQLAlchemy 열(Column)을 만들어 사용해야 해요. SQLModel는 SQLModel에서 SQLAlchemy 필드형으로 처리하지 못하는 자료형이 있을 것에 대응하도록 Field에 sa_column 인자를 제공해요. 이 인자로 SQLAlchemy Column 객체를 전달하면 SQLModel이 직접 모델 필드를 만들지 않고 전달된 Column 객체를 사용하죠.
FastAPI Storages는 로컬 파일 스토리지(FileSystemStorage)와 AWS S3 스토리지(S3Storage)를 제공하는데, 우리는 로컬 파일 스토리지를 사용할게요. 파일이 저장될 경로를 path 인자로 전달하며 FileSystemStorage 객체를 만들어서 FileType에 주입해주면 돼요.
모델 필드의 자료형 각주엔 StorageFile을 사용해야 해요. 공식 문서에는 이 내용이 없어서 조금 헤맸어요. 😅 이 자료형 각주는 Pydantic을 위한 것이죠. SQLModel을 사용하지 않고 SQLAlchemy를 사용했다면 굳이 자료형 각주를 할 필요는 없거든요.
마지막으로 Pydantic으로 모델로써 StorageFile 자료형을 사용할 수 있도록 arbitrary_types_allowed 설정을 활성화합니다. Pydantic이 StorageFile을 다루지 못하거든요.
Alembic 마이그레이션 스크립트
Attachment 모델을 데이터베이스 테이블로 매핑하기 위해 마이그레이션을 진행할게요. 먼저 마이그레이션 스크립트를 만들어요.
마이그레이션 스크립트 파일이 db/alembic/versions 디렉터리에 만들어지는데요. 바로 마이그레이션을 수행하지 마세요. 마이그레이션 스크립트를 수정해야 해요. 만들어진 스크립트 파일에서 file 열에 대해 다음과 같이 정의되어 있을 거예요.
두 가지 문제가 있는데, 첫 번째 문제는 fastapi_storages 모듈을 임포트 하지 않아 오류가 나요. 그래서 직접 import fastapi_storages 구문을 추가해야 하죠. 두 번째 문제는 FileType 클래스를 아무 인자없이 호출해 객체를 만든다는 점이예요. 이건 직접 인자를 지정해줘야 하죠.
두 문제를 해결하는 최종 마이그레이션 스크립트는 다음과 같아요.
이제 마이그레이션합니다.
여기까지 진행한 코드 커밋 : 8130340
AttachmentService에 파일 업로드 API 구현
첨부 파일을 다루는 AttachmentService를 만들게요. 저는 되도록 서비스와 모델을 1:1로 관계지어요.
UploadFile은 FastAPI가 넘겨받은 업로드 파일 객체예요. 이 객체를 Attachment 모델의 file 모델 필드로 전달하면 FastAPI Storages가 알아서 스토리지에 파일을 저장하죠. AWS S3 스토리지를 사용하도록 지정하면 AWS S3에 파일을 업로드해요.
이렇게 만든 서비스를 종단점 함수에서 의존성 주입받아 사용하도록 의존성 각주도 만듭니다.
FastAPI에 Static 경로 Mount 설정
이제 파일을 저장할 경로를 만들고, 이 경로를 FastAPI에 지정해 연결할게요. pyproject.toml 파일이 있는 곳에 _uploads 디렉터리를 만들고요.
이 경로를 URL 경로로 _uploads에 연결해줍니다.
app.mount()로 연결하면 돼요. 첫 번째 인자는 URL 경로예요. 만약 /puddingcamp/uploads라고 지정하면 파일을 내려받는 기본(base) 경로는 /puddingcamp/uploads가 되죠. 하지만 아쉽게도 FastAPI Storages의 스토리지 경로와 FastAPI의 마운트 URL 경로가 불일치하면 HTTP 404 응답이 발생하고, 이를 해결하려면 코드를 좀 손봐야하니 여기에선 스토리지 경로와 URL 경로를 _uploads로 일치시킬게요.
여기까지 진행한 코드 커밋 : 09057bf
파일 첨부 종단점 구현
그동안 FastAPI로 구현한 게 있어서 이제는 설명을 간단하게 할 수 있어서 내용 전개가 빠르네요. 😁
파일 첨부 종단점 HTTP API 경로는 /todos/{pk}/attachments로 할게요. 특정 할 일에 파일을 첨부하는 것이니 /todos/{pk}로 기본 URL 경로를 잡은거죠.
새로운 코드 패턴은 upload_file: UploadFile 정도네요. FastAPI는 UploadFile 자료형 각주가 있으면 업로드 파일을 주입해야 한다는 걸 알아채요. 간단하고 직관적이죠?
파일을 새로 업로드하면 htmx로 부분 렌더링한 HTML를 응답해요. 코드는 간단해요. todo-detail.jinja2와 거의 같은 패턴이거든요. 먼저 pudding_todo/apps/todo/templates/component.jinja2 템플릿 파일을 먼저 만들어요.
render 매크로 인자로 Attachment 객체를 받고, 이 객체의 file 모델 필드의 값을 활용하죠. 이 모델 필드의 자료형은 fastapi_storages.StorageFile인데, 이 객체에는 파일의 경로가 담긴 path 속성이, 파일명이 담긴 name 속성이 있죠.
components/attachment.jinja2 템플릿 파일은 매크로 파일이므로 직접 렌더링이 안 돼요. 그래서 부분 렌더링을 위한 attachment.jinja2 파일을 pudding_todo/apps/todo/templates/partial 디렉터리에 만들어서 이 매크로를 사용해 렌더링 합니다.
add_attachment_to_todo() 종단점 함수는 바로 이 템플릿 파일을 사용해 렌더링하는 거예요.
파일 첨부 UI
거의 다 왔어요! pudding_todo/apps/todo/templates/components/todo-detail.jinja2 템플릿 파일을 열고 파일 첨부 폼을 추가합니다.
두 개 요소를 추가했어요. 하나씩 살펴보죠.
먼저 <div id="todo-attachment-list-{{ todo.id }}"> 태그부터 볼게요. 이 태그 안엔 각 할 일에 첨부된 파일을 나열해요. 나열하는 데 사용하는 템플릿은 앞서 만든 components/attachment.jinja2 템플릿 파일이죠.
폼 태그는 파일 업로드에 필요한 태그가 있는데, 이것도 htmx로 처리하고 있어요. 앞서 만든 파일 업로드 API를 지정하는데, 이 API는 업로드한 파일에 대한 HTML 을 반환하잖아요. 응답받은 그 HTML을 <div id="todo-attachment-list-{{ todo.id }}"> 안에 추가해 넣기 위해 hx-swap="beforeend" 속성으로 지정했어요. 그리고 파일 업로드이니 폼 인코딩을 multipart/form-data로 지정했지요.
이제 파일 첨부를 해보면 잘 동작합니다!
여기까지 진행한 코드 커밋 : 73f3e0e
FastAPI 연재 컨텐츠를 마치며
2024년 5월 중순에 연재 시작하여 8주 동안 함께 FastAPI를 학습하시느라 모두 고생하셨어요.
분량과 난이도를 조절하느라 꽤 많은 소재를 쳐냈어요. SQLite와 PostgreSQL의 JSON 필드 차이도 다루고, AWS Lightsail에 배포도 하고, 현재 인증 절차 없이 접근 가능한 SQLAdmin에 인증 기능도 붙이려 했거든요. 가령 PostgreSQL의 JSON 컬럼에서 문자열이 들어가있는 배열에 대해 검색을 하는 다음 SQL 쿼리를 SQLAlchemy로 어떻게 작성하는지에 대한 내용이죠.
이런 내용은 별도 컨텐츠로 다루겠습니다.
푸딩캠프 뉴스레터를 구독하면 학습과 성장, 기술에 관해 요약된 컨텐츠를 매주 편하게 받아보실 수 있습니다.
목차