Devendency

person

s3 버킷 API 로직 개선

안녕하세요! 이 글은 현재 제가 참여 중인 사이드 프로젝트에서 사용 중인 s3 버킷을 사용하는 API를 개선하는 작업을 바탕으로 작성하게 되었습니다.

현재 프로젝트에서는 이미지를 s3 버킷에 저장하고 있습니다. 하지만 API를 통해 s3에 이미지를 삽입 및 내보내기 위해서는 특정 key값과 그때마다 다른 s3 URL을 지급받아 사용하여 비효율적이었습니다.

이러한 비효율적인 방식을 개선하는 작업이 필요하다고 생각하였고, 실제로 비효율적인 부분을 개선하여 코드를 단순화하여 다른 개발자들이 코드를 이해하기 편하고, 응용하는 데 도움이 되었습니다.

현재 로직 방식

블로그에 포함되어 있는 API 관련 글들은 실제 API URL을 포함하고 있지 않습니다.

현재 로직을 프로필 이미지 업로드 기능을 예시로 들자면,

  1. 클라이언트 → 서버에 업로드 요청 (filename, type 전달)
  2. 서버 → 클라우드 스토리지에 임시 업로드 URL(Presigned URL) 발급 요청
  3. 클라우드 스토리지 → 서버에 {key, preSignedUrl} 반환
  4. 클라이언트 → 발급받은 Presigned URL로 클라우드 스토리지에 직접 파일 업로드
  5. 클라이언트 → 서버에 프로필 업데이트 요청 (스토리지 키 전달하여 프로필에 연결)

방식으로 s3의 presigned URL을 생성하고 key값과 preSignedUrl을 받아와 값을 store로 저장하여, 프론트가 다른 API에서 사용할 수 있도록 하는 방식입니다.

현재 방식은 총 5번의 작업을 거쳐 1개의 기능이 이루어지는 형태로, 이 부분이 비효율적이라고 생각이 되어 개선을 시작하였습니다. 개선을 하기에 앞서, 개선을 위해 현재의 문제점이 무엇인지 알아 보았습니다.

현재 로직 방식의 문제점

저는 현재 로직의 가장 큰 문제점은 orchestration, 즉 복잡하게 얽힌 워크플로우에 있습니다. 하나의 기능을 완성하기 위해 여러 API가 꼬리에 꼬리를 물고 연쇄적으로 호출되는 구조입니다.

클라이언트 기준 로직 방식

  1. 서버에 업로드 요청 함수로 filename과 type 전달
  2. 서버에서 클라우드 스토리지에 임시 업로드 후 preSignedURL + key 반환
  3. 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들의 작동 방식에 대한 설명입니다.

  1. 클라이언트 (브라우저) │ FormData (텍스트 필드 + 파일)
  2. Next.js API Route (프록시) │ 원본 multipart body를 그대로 전달 (boundary 유지)
  3. 백엔드 서버 │ 파일 수신 → S3 업로드 + DB 저장을 백엔드가 일괄 처리

클라이언트는 FormData에 텍스트 데이터와 파일을 함께 담아 한 번만 요청합니다. Next.js API Route는 proxyToBackend() 헬퍼를 통해 원본 multipart/form-data body를 백엔드로 중계합니다. S3 업로드 로직은 전부 백엔드에서 처리합니다.

이렇게 기존에는 3번의 API 호출을 하여 기능 1개를 실행하였지만, 개선 후 Formdata를 통해 정보를 한 번에 보내고, 기존에 s3 버킷 관련 작업은 백엔드에서 처리하는 방식으로 개선하였습니다.

마무리하며

s3 버킷 API의 기존 구조를 재설계하고 개선하는 과정을 하면서 개발에 대한 응용력이 좋아진 것 같습니다. 단순히 개발을 하는 것 뿐만 아니라 어떻게 더 효율적이게, 전체적인 코드에 도움이 되는 방향성 등을 생각하며 작업을 하니 개발에 대한 응용력을 늘릴 수 있었습니다.

개발을 하시는 분들은 개발만이 아닌 기존 기능을 개선하고, 기존 기능에 대한 재설계 작업을 한 번 해보시는 것을 개인적으로 추천드립니다!

댓글 0