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 요청만 허용한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT", "GET"],
"AllowedOrigins": ["*"],
"ExposeHeaders": ["ETag"]
}
]
S3 → 버킷 → 권한 → CORS 에 추가하면 된다.
implementation 'software.amazon.awssdk:s3:2.25.25'
implementation 'software.amazon.awssdk:s3-presigner:2.25.25'
@Configuration
@RequiredArgsConstructor
public class S3Config {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Bean
public S3Presigner s3Presigner() {
return S3Presigner.create();
}
}
@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();
}
}
@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
));
}
}
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();
}
async function uploadToS3(uploadUrl: string, file: File) {
await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": file.type
},
body: file
});
}
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"
]
}