
헬시플레이트의프로필 이미지를 Amazon S3에 업로드하는 기능을 구현하면서
직접 업로드 방식과 Presigned URL 방식을 모두 경험해보았다.
처음에는 S3 업로드에 대한 지식이 부족했기 때문에,
아무 고민 없이 가장 흔하고 직관적인 방식인 서버 직접 업로드를 선택했다.
MultipartFile을 받아
클라이언트 → 서버 → S3 로 업로드하는 구조
문제없이 잘 동작했고, 구현도 어렵지 않았다.
하지만 프로젝트를 진행하던 중, 프론트엔드 팀원이자 동네 친구에게 이런 제안을 받았다.
💬 “Presigned URL 방식으로 바꾸는 게 어때?”
솔직히 처음에는 “뭐가 그렇게 다른데?” 라는 생각이 먼저 들었다.
하지만 장점이 명확하다면 바꿀 이유는 충분하다고 생각했고, 공부 + 실제 구현을 해보면서
왜 많은 서비스들이 Presigned URL 방식을 선택하는지 몸소 이해하게 되었다.
이 글은
직접 업로드 → Presigned URL 방식으로 전환한 이유와 구현 과정,
그리고 사용하면서 느낀 장단점과 고민 포인트를 정리한 기록이다.
이 질문에 대한 답은 두 방식의 구조 차이를 보면 바로 이해된다.
클라이언트 → 서버(MultipartFile) → S3
클라이언트 → S3 (직접 업로드)
우리 서비스는 블로그형 서비스다. 즉, 이미지 업로드가 굉장히 잦다. 이 상황에서 모든 이미지가 서버를 거쳐 간다면?
👉 “굳이 서버가 파일을 중계해야 할까?”
이 질문에 대한 답이 Presigned URL이었다.
Presigned URL은 AWS S3에 특정 작업을 허용해주는 임시 URL이다.
조금 풀어서 말하면,
“이 URL을 가진 사람은
이 파일을,
이 시간 동안만,
이 권한으로 S3에 업로드(또는 다운로드)할 수 있다”
라는 정보를 서명(Sign) 해서 만든 URL이다.
즉, Presigned URL에는 이미 다음 정보들이 포함되어 있다.
그래서 클라이언트는 AWS Access Key를 전혀 몰라도 Presigned URL 하나만으로 S3에 직접 접근 할 수 있다.
Presigned URL은
“서버가 S3 업로드 권한을 잠깐 빌려주는 링크”다.
이 개념을 이해하고 나면,
왜 서버를 거치지 않고도 S3 업로드가 가능한지,
그리고 왜 많은 서비스들이 이 방식을 선택하는지가 자연스럽게 이어진다.
Presigned URL을 적용하면서 가장 크게 체감한 단점이 바로 이 부분이었다.
현재 플로우
// 1. Presigned URL 요청
const { presignedUrl, fileUrl } = await getPresignedUrl();
// 2. S3 업로드
await uploadToS3(presignedUrl, imageFile);
// 3. 회원가입
await register({ profileImageUrl: fileUrl });
❌ 3번에서 실패 or 사용자가 회원가입 프로세스에서 이미지 등록 후 페이지를 나간다면?
S3에는 파일이 있을것이다. 하지만 DB에는 해당 fileUrl이 존재하지 않을 것이다. 이렇게 되면 S3에만 올라가있고 DB에서 조회할 수 없는 고아파일이 발생하는 것이다.
if (!s3Service.fileExists(request.profileImageUrl())) {
throw new IllegalArgumentException("이미지가 존재하지 않습니다.");
}
@Scheduled(cron = "0 0 2 * * *")
public void cleanupOrphanFiles() {
// DB에 없는 S3 파일 삭제
}
장점
단점
장점
단점
Presigned URL은 “서버를 가볍게 만들고 싶은 서비스”라면 거의 필수에 가까운 선택지였다.
다만, 무조건 좋은 방식은 아니고 서비스 특성과 트랜잭션 요구사항에 따라 충분한 고민이 필요하다는 것도 느꼈다.
이번 경험을 통해 “왜 이렇게 설계했는지”를 이해하게 된 것이 가장 큰 수확이었다.