S3에 이미지 업로드하기

김희영·2025년 12월 9일

spring

목록 보기
24/26

AWS에는 이미지를 올릴 수 있는 버킷이 있다.
EC2에 올려도 되긴하지만, 파일이 너무 많이 쌓이는 건 좋지 않고 비효율적이니 S3에 올려보자.

일단 기본적으로 2가지 방법이 있다.
1) 이미지 서버에 업로드 → 서버에서 S3에 업로드
2) S3에 바로 업로드 → 메타 데이터만 서버에서 저장

딱 봐도 2번이 효율적이다.
그럼 2번을 위해 정책을 추가할 필요가 있다.

대충 다음과 같은 방식이다.

[클라이언트] 
   ↓ Presigned URL 요청
[백엔드 서버]
   ↓ Presigned URL 생성 (IAM 권한 필요)
[클라이언트]
   ↓ 해당 URL로 이미지 PUT 업로드
[S3]
   ↓ 업로드 성공 응답
[클라이언트 → 백엔드]
   "S3 key" 전달 → 메타데이터 저장

정책 부여

그럼 이미지 업로드를 위해 정책부터 세팅하자.

우선 서버는 Presigned URL을 생성해야 한다. 그러니 IAM Role에 s3:PutObject, s3:GetObject 권한을 부여한다.
그리고 S3 버킷의 CORS 설정에서 퍼블릭 접근은 모두 차단하고, Presigned URL의 PUT 요청만 허용한다.

IAM Role

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}
  • s3:PutObject: 클라이언트가 업로드하도록 Presigned URL 생성
  • s3:GetObject: 업로드된 파일을 조회

S3 CORS 설정

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": ["ETag"]
  }
]

S3 → 버킷 → 권한 → CORS 에 추가하면 된다.

Spring Boot에서 Presigned URL 발급

(1) build.gradle 설정

implementation 'software.amazon.awssdk:s3:2.25.25'
implementation 'software.amazon.awssdk:s3-presigner:2.25.25'

(2) S3Config

@Configuration
@RequiredArgsConstructor
public class S3Config {

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    @Bean
    public S3Presigner s3Presigner() {
        return S3Presigner.create();
    }
}

(3) Presigned URL 발급 서비스

@Service
@RequiredArgsConstructor
public class S3Service {

    private final S3Presigner s3Presigner;

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    public String generateUploadUrl(String key) {

        PutObjectRequest objectRequest = PutObjectRequest.builder()
                .bucket(bucket)
                .key(key)
                .contentType("image/jpeg") // 필요시 변경
                .build();

        PresignRequest request = PutObjectPresignRequest.builder()
                .putObjectRequest(objectRequest)
                .signatureDuration(Duration.ofMinutes(5)) // URL 유효시간
                .build();

        PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(request);

        return presignedRequest.url().toString();
    }
}

(4) 컨트롤러

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

    private final S3Service s3Service;

    @PostMapping("/presigned")
    public ResponseEntity<Map<String, String>> createPresignedUrl(
            @RequestParam String directory,
            @RequestParam String filename
    ) {
        String key = directory + "/" + filename;

        String url = s3Service.generateUploadUrl(key);

        return ResponseEntity.ok(Map.of(
                "uploadUrl", url,
                "key", key
        ));
    }
}

프론트엔드(React/Next.js) 파일 업로드

(1) Presigned URL 요청

async function requestUploadUrl(file: File) {
  const filename = crypto.randomUUID() + "." + file.name.split(".").pop();

  const res = await fetch(
    `/api/uploads/presigned?directory=auction&filename=${filename}`,
    { method: "POST" }
  );

  return await res.json();
}

(2) 실제 업로드 (PUT 요청)

async function uploadToS3(uploadUrl: string, file: File) {
  await fetch(uploadUrl, {
    method: "PUT",
    headers: {
      "Content-Type": file.type
    },
    body: file
  });
}

(3) 전체 업로드 흐름

async function handleImageUpload(file: File) {
  const { uploadUrl, key } = await requestUploadUrl(file);

  await uploadToS3(uploadUrl, file);

  // 이제 key를 서버에 저장하면 끝
  return key;
}

이런식으로!

POST /api/auctions
{
  "name": "...",
  "description": "...",
  "images": [
      "auction/uuid1.jpg",
      "auction/uuid2.jpg"
  ]
}
profile
내는 반드시 엄청난 개발자가 되고 말것어

0개의 댓글