s3 버킷 API 로직 개선
안녕하세요! 이 글은 현재 제가 참여 중인 사이드 프로젝트에서 사용 중인 s3 버킷을 사용하는 API를 개선하는 작업을 바탕으로 작성하게 되었습니다.
현재 프로젝트에서는 이미지를 s3 버킷에 저장하고 있습니다. 하지만 API를 통해 s3에 이미지를 삽입 및 내보내기 위해서는 특정 key값과 그때마다 다른 s3 URL을 지급받아 사용하여 비효율적이었습니다.
이러한 비효율적인 방식을 개선하는 작업이 필요하다고 생각하였고, 실제로 비효율적인 부분을 개선하여 코드를 단순화하여 다른 개발자들이 코드를 이해하기 편하고, 응용하는 데 도움이 되었습니다.
현재 로직 방식
블로그에 포함되어 있는 API 관련 글들은 실제 API URL을 포함하고 있지 않습니다.
현재 로직을 프로필 이미지 업로드 기능을 예시로 들자면,
- 클라이언트 → 서버에 업로드 요청 (filename, type 전달)
- 서버 → 클라우드 스토리지에 임시 업로드 URL(Presigned URL) 발급 요청
- 클라우드 스토리지 → 서버에
{key, preSignedUrl}반환- 클라이언트 → 발급받은 Presigned URL로 클라우드 스토리지에 직접 파일 업로드
- 클라이언트 → 서버에 프로필 업데이트 요청 (스토리지 키 전달하여 프로필에 연결)
방식으로 s3의 presigned URL을 생성하고 key값과 preSignedUrl을 받아와 값을 store로 저장하여, 프론트가 다른 API에서 사용할 수 있도록 하는 방식입니다.
현재 방식은 총 5번의 작업을 거쳐 1개의 기능이 이루어지는 형태로, 이 부분이 비효율적이라고 생각이 되어 개선을 시작하였습니다. 개선을 하기에 앞서, 개선을 위해 현재의 문제점이 무엇인지 알아 보았습니다.
현재 로직 방식의 문제점
저는 현재 로직의 가장 큰 문제점은 orchestration, 즉 복잡하게 얽힌 워크플로우에 있습니다. 하나의 기능을 완성하기 위해 여러 API가 꼬리에 꼬리를 물고 연쇄적으로 호출되는 구조입니다.
클라이언트 기준 로직 방식
- 서버에 업로드 요청 함수로 filename과 type 전달
- 서버에서 클라우드 스토리지에 임시 업로드 후 preSignedURL + key 반환
- preSignedURL로 직접 put
이러한 다단계 의존성은 코드의 결합도를 높이고 유지보수를 어렵게 만듭니다. 그럼 이 orchestration을 어떻게 하면 좋을까요?
해결 방식 제안
저는 백엔드에 통합 엔드포인트 요청 드렸습니다. 현재 클라이언트가 orchestration을 담당하는데, 이를 백엔드 한 곳에서 처리하게 하는 것 입니다.
기존 방식과 달리 3번의 과정 없이, multipart/form-data로 파일과 메타데이터를 한번에 보내는 방식으로 변경하는 것입니다. 이런 식으로 3번의 API 호출 과정을 1번으로 줄여 근본적인 코드 단순화에는 이 방법이 제일 좋을 것이라고 생각하였습니다.
해결 방식 | 첫 번째로는 캡슐화에 대한 이야기입니다.
프론트엔드가 S3와 직접 통신하지 않고, 백엔드가 파일 업로드와 DB 저장을 한 번에 처리하는 방식인데, 기존에는 프론트엔드가 S3와 직접 통신했기 때문에 버킷 경로, Key 구조 등이 노출되었습니다. 이제는 서버 내부 로직으로 감춥니다.
- 기존 (Exposed): 프론트가
images/user1/profile_v1.png라는 경로를 직접 알고 업로드. - 변경 (Encapsulated): 프론트는
File객체만 던집니다. 서버가 내부적으로 UUID를 생성하고, 날짜별 폴더 구조를 결정하여 S3에 저장합니다.
해결 방식 | 두번째는 백엔드 구현 가이드입니다.
프론트엔드 개발자인 제가 AI를 통해 알아본 가이드입니다.(정확하지 않을 수 있습니다.)
1. API 스펙 구성 (multipart/form-data)
JSON 데이터와 파일을 한 번에 받기 위해 multipart/form-data 형식을 사용합니다.
- DTO 구조: 텍스트 데이터(String, Number)와 파일(MultipartFile/Stream)을 혼합하여 정의합니다.
- Spring (Java) 예시:
@RequestPart를 사용하여 메타데이터 JSON과 파일을 분리해서 받을 수 있습니다.
2. 파일 스트리밍 처리 (메모리 관리)
파일을 서버가 직접 받으면 서버 메모리 부하가 발생할 수 있습니다.
- 작은 파일: 메모리에 적재 후 S3 업로드.
- 큰 파일: 서버 메모리에 다 올리지 않고,
InputStream을 통해 S3로 바로 스트리밍(Transfer Manager 등 활용)하여 메모리 점유율을 최소화합니다.
3. 트랜잭션 관리
파일 업로드와 DB 저장이 한 세트이므로 실패 시 처리가 중요합니다.
- DB 저장 실패 시: S3에 이미 업로드된 파일을 삭제하는 Rollback 로직을 구현하거나, 주기적으로 '주인 없는 파일'을 청소하는 배치 작업을 고려해야 합니다.
개선 후
s3 관련 API들의 작동 방식에 대한 설명입니다.
- 클라이언트 (브라우저) │ FormData (텍스트 필드 + 파일)
- Next.js API Route (프록시) │ 원본 multipart body를 그대로 전달 (boundary 유지)
- 백엔드 서버 │ 파일 수신 → S3 업로드 + DB 저장을 백엔드가 일괄 처리
클라이언트는 FormData에 텍스트 데이터와 파일을 함께 담아 한 번만 요청합니다.
Next.js API Route는 proxyToBackend() 헬퍼를 통해 원본 multipart/form-data body를 백엔드로 중계합니다.
S3 업로드 로직은 전부 백엔드에서 처리합니다.
이렇게 기존에는 3번의 API 호출을 하여 기능 1개를 실행하였지만, 개선 후 Formdata를 통해 정보를 한 번에 보내고, 기존에 s3 버킷 관련 작업은 백엔드에서 처리하는 방식으로 개선하였습니다.
마무리하며
s3 버킷 API의 기존 구조를 재설계하고 개선하는 과정을 하면서 개발에 대한 응용력이 좋아진 것 같습니다. 단순히 개발을 하는 것 뿐만 아니라 어떻게 더 효율적이게, 전체적인 코드에 도움이 되는 방향성 등을 생각하며 작업을 하니 개발에 대한 응용력을 늘릴 수 있었습니다.
개발을 하시는 분들은 개발만이 아닌 기존 기능을 개선하고, 기존 기능에 대한 재설계 작업을 한 번 해보시는 것을 개인적으로 추천드립니다!