본문 바로가기

ETC/AWS

S3 Presigned URL로 파일 업로드 구현

추천캐릭터 2026. 7. 17. 13:00
728x90

S3 Presigned URL로 파일 업로드 구현

결론부터

클라이언트가 서버를 거치지 않고 S3에 직접 파일을 올리게 하려면 Presigned URL을 쓴다. 서버는 "이 URL로 10분간 이 키에 업로드해도 좋다"는 서명된 URL만 발급하고, 실제 바이트는 클라이언트가 S3로 직접 전송한다. 서버 대역폭과 메모리를 아끼고, 대용량 업로드에서 특히 유리하다. 자바에서는 AWS SDK for Java 2.xS3Presigner로 발급한다.

왜 Presigned URL인가

파일을 서버가 받아서 다시 S3로 올리면, 업로드 트래픽이 서버를 두 번 통과한다. 파일이 크거나 동시 업로드가 많으면 서버 메모리와 네트워크가 병목이 된다. Presigned URL은 이 경로를 없앤다. 서버는 인증·권한만 책임지고, 데이터 전송은 클라이언트와 S3가 직접 처리한다.

흐름은 세 단계다. ① 클라이언트가 서버에 업로드 URL 요청 → ② 서버가 Presigned URL 발급 → ③ 클라이언트가 그 URL로 S3에 직접 PUT.

의존성과 설정

dependencies {
    // 버전은 최신 릴리스로 확인 후 사용
    implementation platform('software.amazon.awssdk:bom:2.31.0')
    implementation 'software.amazon.awssdk:s3'
}
aws.s3.bucket=my-upload-bucket
aws.region=ap-northeast-2

서버: URL 발급

S3Presigner로 PUT용 Presigned URL을 만든다.

@Service
public class S3UploadService {

    private final S3Presigner presigner;
    private final String bucket;

    public S3UploadService(
            @Value("${aws.s3.bucket}") String bucket,
            @Value("${aws.region}") String region) {
        this.bucket = bucket;
        this.presigner = S3Presigner.builder()
                .region(Region.of(region))
                .build();
    }

    public String createUploadUrl(String key) {
        PutObjectRequest objectRequest = PutObjectRequest.builder()
                .bucket(bucket)
                .key(key)
                .build();

        PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(10))  // URL 유효 시간
                .putObjectRequest(objectRequest)
                .build();

        return presigner.presignPutObject(presignRequest).url().toString();
    }
}

컨트롤러는 키를 만들어 URL을 돌려준다.

@RestController
@RequestMapping("/uploads")
public class UploadController {

    private final S3UploadService service;

    public UploadController(S3UploadService service) {
        this.service = service;
    }

    @PostMapping("/presign")
    public Map<String, String> presign(@RequestParam String filename) {
        String key = "uploads/" + UUID.randomUUID() + "-" + filename;
        return Map.of("uploadUrl", service.createUploadUrl(key), "key", key);
    }
}

클라이언트: 직접 업로드

발급받은 URL로 PUT 하면 끝이다.

const res = await fetch("/uploads/presign?filename=" + file.name, { method: "POST" });
const { uploadUrl } = await res.json();

await fetch(uploadUrl, { method: "PUT", body: file });

주의사항

  • CORS: 브라우저에서 직접 PUT 하려면 S3 버킷 CORS 설정에 해당 오리진과 PUT 메서드를 허용해야 한다.
  • Content-Type: 발급 시 지정했다면 업로드 요청도 같은 헤더를 보내야 서명이 일치한다. 불일치는 SignatureDoesNotMatch로 조용히 실패한다.
  • 키 검증: 클라이언트가 임의 키를 못 쓰도록, 키 생성은 반드시 서버에서 한다.
  • 유효 시간: 짧게(수 분~십 분) 잡아 URL 유출 위험을 줄인다.

마무리

Presigned URL은 '권한은 서버가, 전송은 클라이언트가'라는 역할 분리로 서버 부하를 크게 줄인다. 업로드가 잦거나 파일이 큰 서비스라면, 서버를 경유하는 방식보다 이 패턴이 대부분 낫다. 같은 방식으로 다운로드용(presignGetObject)도 발급할 수 있다.

728x90