2025. 5. 15. 23:57ㆍSpringBoot

이번 포스팅에서 SpringBoot 프로젝트에서 S3에 업로드하는 세 가지 방법을 비교하고, 각 방법의 장단점을 알아보겠습니다.
S3에 파일을 업로드하는 세 가지 방법
파일을 S3에 업로드하는 방법은 크게 세 가지가 존재합니다.
- Stream을 이용한 업로드 방식
- MultipartFile을 이용한 업로드 방식
- AWS Multipart을 이용한 업로드 방식
설명하기에 앞서 이해하면 좋은 내용들을 짚고 넘어가겠습니다.
Stream이란?
스트림이란 데이터의 흐름을 의미합니다.
이러한 스트림을 프로그램 입장에서 생각하면 다음 두 가지로 구분할 수 있습니다.
- 데이터가 외부로부터 출발하여 프로그램으로 들어오는 스트림
- 데이터가 프로그램에서 출발하여 프로그램 외부로 빠져나가는 스트림
이때 전자를 InputStream, 후자를 OutputStream이라 부릅니다.

프로그램은 InputStream을 통해 데이터를 읽고, OutputStream을 통해 데이터를 씁니다.
이때 데이터는 바이트를 기반으로 읽고 쓰여지게 됩니다.
그 중 이번 이미지 업로드에 쓰일 HttpServletRequest의 InputStream은
HttpServletRequest에서 HTTP 요청 본문 전체를 읽어오는 스트림입니다.
따라서 이번 Stream 업로드 방식의 특징은 1회 API 호출에 1개 파일만을 업로드할 수 있습니다. 요청 본문 바이너리에서 여러 개 파일 데이터를 구분할 수 있는 기준이 없기 때문입니다.
Multipart란?
Multipart는 HTTP 요청에서 여러 종류의 데이터를 한 번에 전송할 수 있도록 도와주는 콘텐츠 전송 방식입니다.
form-data는 HTML 폼에서 파일과 텍스트 데이터를 함께 전송할 수 있도록 하는 HTTP 요청의 Content-Type 형식입니다.
예를 들어 이러한 html 폼을 브라우저에서 제출한다면
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="text" name="username" />
<input type="file" name="profileImage" />
<button type="submit">Upload</button>
</form>
다음과 같이 HTTP 요청이 이루어집니다.
POST /upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"
myname
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="profileImage"; filename="myphoto.jpg"
Content-Type: image/jpeg
<이미지 바이너리 데이터>
------WebKitFormBoundaryABC123--
위 HTTP Body에서 boundary로 각 파트를 구분합니다. (Text, File)
그리고 서버에서는 이걸 파싱해서 MultipartFile, @RequestParam 등으로 나눠서 처리합니다.
프로젝트 세팅
build.gradle
다음과 같은 의존성이 필요합니다.
implementation 'org.springframework.boot:spring-boot-starter-web'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// AWS
implementation 'io.awspring.cloud:spring-cloud-aws-starter:3.1.1'
implementation 'software.amazon.awssdk:s3'
// S3
implementation 'software.amazon.awssdk:s3:2.31.0'
// Monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
application.yml (AWS ACCESS KEY, SECRET KEY, REGION, BUCKET을 환경변수로 설정해주세요!)
spring:
application:
name: s3uploadtest
servlet:
multipart:
enabled: true
max-file-size: 200MB
max-request-size: 200MB
cloud:
aws:
credentials:
access-key: ${AWS_S3_ACCESS_KEY}
secret-key: ${AWS_S3_SECRET_KEY}
s3:
bucket: ${AWS_S3_IMAGE_BUCKET}
region:
static: ${AWS_REGION}
stack:
auto: false
# monitoring
management:
endpoints:
web:
exposure:
include: health, prometheus
server:
tomcat:
mbeanregistry:
enabled: true
docker-compose.yml (메모리 사용량을 확인하기 위해 필요한 promethus와 grafana를 컨테이너로 띄우는 코드입니다!)
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
user: root
volumes:
- prometheus_data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
configs:
- source: prometheus_config
target: /etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:latest
container_name: grafana
volumes:
- grafana_data:/var/lib/grafana
ports:
- "3000:3000"
restart: unless-stopped
configs:
- source: grafana_datasource
target: /etc/grafana/provisioning/datasources/datasource.yml
configs:
prometheus_config:
content: |
global:
scrape_interval: 1s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: [ 'prometheus:9090' ]
- job_name: 'spring-actuator'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: [ 'host.docker.internal:8080' ]
grafana_datasource:
content: |
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
access: proxy
isDefault: true
volumes:
prometheus_data:
grafana_data:
S3Config.class
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public S3Client s3Client() {
AwsCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey);
return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.build();
}
}
Stream 업로드

Stream Upload는 HttpServletRequest의 InputStream을 사용하여 AWS S3에 다이렉트로 파일은 전송하는 방식입니다.
동작 방식
- 클라이언트가 서버로 파일 데이터를 전송합니다.
- 서버가 HttpServletRequest의 InputStream을 이용하여 파일 데이터를 InputStream으로 읽습니다.
- putObject를 호출하여 해당 스트림 데이터를 바로 S3에 단일 요청으로 업로드합니다.
stream 업로드 방식 특징
- 업로드할 파일의 바이너리 전체를 Spring Boot 애플리케이션을 실행하고 있는 서버의 디스크나 힙 메모리에 저장하지 않습니다.
- 애플리케이션 로직을 통해 모든 바이너리를 메모리에 로드하지 않는 이상 전 전처리(ex. 이미지 리사이징)가 불가합니다.
- 1회 API 호출에 1개 파일만을 업로드할 수 있습니다.
- HTTP 요청 본문 바이너리에서 여러 개 파일 데이터를 구분할 수 있는 기준이 없기 때문입니다.
- 업로드할 파일 용량이 커질수록 우리는 업로드 속도에 유의해야 합니다.
- Stream 업로드 방식은 구조적으로 클라이언트에게 업로드 현황을 제공할 수 없습니다.
- 따라서 클라이언트가 기다릴 수 있을만큼 요청 시 적당한 파일사이즈 제한이 필요합니다.
- 파일을 업로드 중 오류가 발생했을 때 전체 파일을 처음부터 다시 업로드해야하기 때문에 시간과 대역폭이 낭비될 수 있습니다.
따라서 Stream 업로드 방식은 서버의 리소스가 한정적이고 작은 용량의 파일을 업로드할 때 효과적입니다.
Controller 코드
@Controller
@RequiredArgsConstructor
public class StreamController {
private final StreamService streamService;
@PostMapping("/stream")
public ResponseEntity<String> uploadStream(HttpServletRequest request) throws IOException {
return ResponseEntity.ok(streamService.uploadViaStream(request, request.getHeader("File-Name")) + "초");
}
}
Service 코드
@Service
@RequiredArgsConstructor
public class StreamService {
private final S3Client s3Client;
@Getter
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
@Value("${cloud.aws.region.static}")
private String region;
public Double uploadViaStream(HttpServletRequest request, String fileName) throws IOException {
double startTime = System.currentTimeMillis();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(fileName)
.contentType(request.getContentType())
.contentLength(request.getContentLengthLong())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(request.getInputStream(), request.getContentLengthLong()));
double endTime = System.currentTimeMillis();
return (endTime - startTime) / 1000;
}
}
실제로 약 8MB정도 되는 크키의 파일을 Stream 방식으로 업로드 해보겠습니다.


메모리 사용량에서 거의 변화가 없는 모습을 볼 수 있습니다.
약 155MB 크기의 파일을 올려보겠습니다.


응답시간이 컸지만 여전히 메모리 사용량에는 큰 변화가 없는 것을 볼 수 있습니다.
MultipartFile 업로드

MultipartFile Upload는 Spring에서 제공하는 MultipartFile 인터페이스를 사용하여 AWS S3에 파일을 업로드하는 방식입니다.
동작 방식
- 클라이언트가 서버로 파일 데이터를 전송합니다.
- 서버가 binary파일을 MultipartFile로 변환합니다. (이 때 설정에 따라 디스크에 잠시 파일을 저장하거나 바로 메모리에 올려서 S3로 업로드 합니다.)
- S3의 putObject를 호출하여 S3에 단일 요청으로 업로드합니다.
Mutlipart 업로드를 사용할 경우 application.yml에 꼭 다음과 같이 명시해주어야 합니다.
spring:
servlet:
multipart:
enabled: true # 멀티파트 업로드 지원여부 (default: true)
file-size-threshold: 0B # 파일을 디스크에 저장하지 않고 메모리에 저장하는 최소 크기 (default: 0B)
location: C:\Users\ASUS\yubin\temp # 업로드된 파일이 임시로 저장되는 디스크 위치 (default: WAS가 결정)
max-file-size: 200MB # 한개 파일의 최대 사이즈 (default: 1MB)
max-request-size: 200MB # 한개 요청의 최대 사이즈 (default: 10MB)
클라이언트가 파일을 업로드했을 때 WAS(Tomcat)가 해당 파일을 임시 디렉터리(location)에 저장합니다.
여기서 임시 디렉터리에 저장된 파일은 힙 메모리에 올라가는 것이 아닌 Servlet Container Disk(컨테이너가 실행되고 있는 서버의 디스크)에 저장됨을 의미합니다.
요청 처리가 끝나면 임시 저장된 파일이 삭제됩니다. 하지만 업로드 중 배포가 되었다든지, 장애가 발생했다든지 등 가끔 삭제되지 않고 남아있을 수 있습니다.
그럴 경우에 삭제 작업을 별도로 해야 하기 때문에 작업과 관리가 용이하도록 경로를 직접 설정해 주는 것이 좋습니다.
따라서 Spring은 임시 디렉터리에 저장된 파일을 MultipartFile 변수에 매핑함으로써 업로드된 파일의 바이너리를 힙 메모리에 할당하지 않고 해당 콘텐츠 메타데이터에 액세스할 수 있습니다.
그리고 업로드된 파일의 크기가 fize-size-threshold 값 이하라면 WAS가 임시파일을 생성하지 않고 파일 바이너리를 메모리에 다이렉트로 할당하게 됩니다.
파일 처리 속도는 더 빠르겠지만, 스레드가 작업을 수행하는 동안 부담이 될 수 있기 때문에(스레드가 파일 업로드와 관련된 작업을 처리하는 동안, 그 스레드가 해당 메모리를 계속 점유하고 있기 때문입니다.), 충분한 검토가 필요합니다.
파일 1개 업로드와 여러개 업로드가 가능한 컨트롤러 코드입니다.
@Controller
@RequiredArgsConstructor
public class MultipartController {
private final MultipartService multipartService;
@PostMapping("/multipart")
public ResponseEntity<String> uploadMultipart(@RequestParam("file") MultipartFile file) throws IOException {
return ResponseEntity.ok(multipartService.uploadViaMultipart(file) + "초");
}
@PostMapping("/multipart/multiple")
public ResponseEntity<List<String>> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) throws IOException {
List<String> result = multipartService.uploadMultipleViaMultipart(files);
return ResponseEntity.ok(result);
}
}
서비스 코드입니다.
@Service
@RequiredArgsConstructor
public class MultipartService {
private final S3Client s3Client;
@Getter
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
public Double uploadViaMultipart(MultipartFile file) throws IOException {
double startTime = System.currentTimeMillis();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(file.getOriginalFilename())
.contentType(file.getContentType())
.contentLength(file.getSize())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
double endTime = System.currentTimeMillis();
return (endTime - startTime) / 1000;
}
public List<String> uploadMultipleViaMultipart(MultipartFile[] files) throws IOException {
return Arrays.stream(files)
.map(file -> {
try {
double startTime = System.currentTimeMillis();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(file.getOriginalFilename())
.contentType(file.getContentType())
.contentLength(file.getSize())
.build();
s3Client.putObject(putObjectRequest,
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
double endTime = System.currentTimeMillis();
double duration = (endTime - startTime) / 1000;
return file.getOriginalFilename() + " 업로드 완료 (" + duration + "초)";
} catch (IOException e) {
throw new UncheckedIOException("파일 업로드 실패: " + file.getOriginalFilename(), e);
}
})
.collect(Collectors.toList());
}
}
테스트로 약 4MB, 8MB 크기의 파일 2개를 업로드해보았습니다.


메모리 사용량 변화가 크게 없는 모습을 볼 수 있습니다.
AWS Multipart 업로드

AWS Multipart Upload는 AWS S3에서 제공하는 파일 업로드 방식입니다.
Multipart Upload는 총 4단계 프로세스로 구성되어 있습니다.
1. Multipart 업로드 시작
멀티파트 업로드 시작(initiate-upload)을 요청하면 서버는 멀티파트 업로드에 대한 고유 식별자인 Upload ID를 응답합니다. 부분 업로드, 업로드 완료 또는 업로드 중단 요청 시 항상 Upload ID를 포함해야 하기 때문에 클라이언트는 이 값을 잘 저장해야 합니다.
2. PresignedURL 발급
업로드를 위한 AWS의 서명된 URL을 발급받는 요청입니다. 멀티파트 시작 요청에서 받은 Upload ID 그리고 PartNumber 값을 함께 요청해야 합니다. PartNumber는 1부터 10,000까지 파트 번호 지정이 필요합니다. AWS에서 파트 번호를 이용하여 업로드하는 객체의 각 부분과 그 위치를 고유하게 식별하기 때문입니다. 파트 번호는 연속적인 시퀀스로 선택할 필요는 없습니다 (예를 들면 1, 5 및 14를 선택해도 됩니다).
만약 이전에 업로드한 부분과 동일한 부분 번호로 새 부분을 업로드할 경우 이전에 업로드한 부분을 덮어쓰게 됩니다.
3. PresignedURL part 업로드
발급받은 PresignedURL에 PUT 메소드로 파트의 바이너리를 실어서 요청합니다. 이때 파트의 용량은 클라이언트에서 결정하여 업로드합니다.
분할된 파트는 5MB~5GB의 크기만 가능합니다. 다만 마지막 파트는 5MB 이하도 괜찮습니다. 만약 업로드할 파일의 크기가 5MB 이하라면 파트 업로드는 한번 수행되어야 합니다. 즉, 첫 번째 파트가 마지막 파트이기도 하다면 위 규칙은 위반되지 않으며 AWS S3는 멀티파트 업로드로 수락하여 객체를 생성하게 됩니다. 파트는 최대 5GB까지 10,000개까지 업로드할 수 있으니 이론상 5TB 크기의 파일까지 업로드할 수 있습니다.
파트를 업로드 후 받는 응답 헤더에 ETag(Entity Tag)가 포함되어 있습니다. 클라이언트는 각 파트 업로드 시 PartNumber와 ETag 값을 매칭하여 보관해야 합니다. 이후 멀티파트 업로드 완료 요청에 이러한 값을 포함해야 하기 때문입니다.
4. Multipart 업로드 완료
멀티파트 업로드 완료는 Upoad ID, 각 PartNumber와 매칭되는 ETag 값이 배열로 포함되어야 합니다. 업로드 완료가 수행되어야 AWS S3에서 PartNumber와 ETag를 기준으로 객체를 재조립합니다. 객체가 매우 클 경우 이 프로세스는 몇 분 정도 걸릴 수 있습니다.
만약 업로드 완료를 하지 않을 경우 S3 버킷에 파일이 생성되지 않습니다.
이 방식은 업로드할 파일을 작은 part로 나누어 각 부분을 개별적으로 업로드하고, 발급된 URL을 통하여 직접 S3에 업로드하기 때문에 Spring 서버를 거치지 않고 업로드가 가능합니다. (따라서 서버의 부하를 고려하지 않아도 된다는 큰 장점이 있습니다.)
모든 part가 업로드되고 난 후, AWS에서 하나의 객체로 조립하여 저장됩니다.

이 방법의 특징은 대용량 파일을 서버의 부담없이 업로드 할 수 있으며, 위 ③번~④번 단계를 반복하면서 클라이언트는 사용자에게 업로드가 어디까지 수행되었는지 사용자에게 제공할 수 있다는 것입니다.
코드로 살펴보겠습니다. 컨트롤러 코드입니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/s3/multipart")
public class AwsMultipartController {
private final AwsMultipartService awsMultipartService;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
@GetMapping("/initiate")
public String initiate(@RequestParam String filename) {
return awsMultipartService.initiateMultipartUpload(bucketName, "uploads/" + filename);
}
@GetMapping("/presigned-url")
public String getPartUploadUrl(@RequestParam String filename,
@RequestParam String uploadId,
@RequestParam int partNumber) {
URL url = awsMultipartService.generatePresignedUrlForPart(bucketName, "uploads/" + filename, uploadId, partNumber);
return url.toString();
}
@PostMapping("/complete")
public String completeUpload(@RequestBody CompleteRequest request) {
List<CompletedPart> parts = request.getParts().stream()
.map(p -> software.amazon.awssdk.services.s3.model.CompletedPart.builder()
.partNumber(p.getPartNumber())
.eTag(p.getETag().replaceAll("^\"|\"$", ""))
.build())
.toList();
awsMultipartService.completeMultipartUpload(bucketName, "uploads/" + request.getFilename(), request.getUploadId(), parts);
return "Upload Complete";
}
}
DTO 코드 입니다.
@Getter
@Setter
public class CompleteRequest {
private String uploadId;
private String filename;
private List<PartETag> parts;
}
@Getter
@Setter
public class PartETag {
private int partNumber;
@JsonProperty("eTag")
private String eTag;
}
서비스 코드입니다.
@Service
@RequiredArgsConstructor
public class AwsMultipartService {
private final S3Client s3Client;
// 1. 멀티파트 업로드 시작
public String initiateMultipartUpload(String bucket, String key) {
CreateMultipartUploadRequest request = CreateMultipartUploadRequest.builder()
.bucket(bucket)
.key(key)
.build();
return s3Client.createMultipartUpload(request).uploadId();
}
// 2. 각 파트별 presigned URL 생성
public URL generatePresignedUrlForPart(String bucket, String key, String uploadId, int partNumber) {
S3Presigner presigner = S3Presigner.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(s3Client.serviceClientConfiguration().credentialsProvider())
.build();
UploadPartRequest partRequest = UploadPartRequest.builder()
.bucket(bucket)
.key(key)
.uploadId(uploadId)
.partNumber(partNumber)
.build();
UploadPartPresignRequest presignRequest = UploadPartPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(15))
.uploadPartRequest(partRequest)
.build();
URL url = presigner.presignUploadPart(presignRequest).url();
presigner.close();
return url;
}
// 3. 완료 요청 (ETag 목록 필요)
public void completeMultipartUpload(String bucket, String key, String uploadId, List<CompletedPart> parts) {
CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
.parts(parts)
.build();
CompleteMultipartUploadRequest completeRequest = CompleteMultipartUploadRequest.builder()
.bucket(bucket)
.key(key)
.uploadId(uploadId)
.multipartUpload(completedMultipartUpload)
.build();
s3Client.completeMultipartUpload(completeRequest);
}
}
테스트 도중 Etag가 헤더로 넘어오지 않는 문제가 발생했었습니다. 또한 클라이언트 도메인(example.com)과 요청받는 도메인(amazonaws.com)이 다르기 때문에 브라우저에서 CORS 에러가 발생합니다.
따라서 S3 버킷 권한의 CORS 설정 부분에 다음과 같이 설정해줍니다.

[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT"
],
"AllowedOrigins": [
"테스트 환경에 맞추어 바꿔주세요"
],
"ExposeHeaders": [
"ETag"
]
}
]
먼저 uploadId를 발급합니다.
쿼리파라미터로 filename을 작성합니다.

Presigned URL을 발급합니다.
쿼리파라미터로 filename, uploadId, partNumber를 작성합니다.

해당 URL로 PUT 요청을 보냅니다. (파일 조각들을 업로드하는 단계입니다.)
응답으로 넘어온 Presigned URL을 url 경로로 설정합니다.
헤더의 Content-Type을 image/jpeg로 설정합니다.
--upload-file로 요청 본문에 파일 조각을 보냅니다.
(필자는 포스트맨 사용 시 ETag를 볼 수가 없어 curl 명령으로 실행하였습니다.)

응답 값에 ETag가 포함되어 있음을 볼 수 있습니다.
이제 Upload complete 요청을 보냅니다.
바디 값에 JSON으로 이전에 발급받고 요청에 사용되던 uploadId, filename, partNumber와 그에 해당하는 eTag를 넘겨줍니다.

S3 버킷에 성공적으로 파일이 올라간 모습을 확인할 수 있습니다.

그리고 업로드가 취소되지 않고 남아있는 불완전한 멀티파트(incomplete multipart) 파일들은 계속해서 스토리지 비용이 부과됩니다. 따라서 AWS CLI를 통해 직접 삭제해야 하는 번거로움이 생길 수 있습니다. 하지만 아래와 같은 S3 LifeCycle 설정으로 자동으로 삭제되게 설정할 수 있습니다.
해당 버킷 -> 관리 -> 수명 주기 관리 로 이동하여 아래와 같이 설정할 수 있습니다.





결론
| 특징 | Stream | MultipartFile | AWS Multipart |
| 파일 크기 제한 | 이론상 없음 | 디스크/메모리에 의존(설정에 따라 제한 가능) | 최대 5TB |
| 파일 바이너리 서버 경유 유무 | △ (Buffer) | ○ (설정에 따라 발생 가능) | ✕ |
| 구현 복잡성 | 단순 | 중간 | 복잡 |
| 업로드 과정의 복잡성 | 단순 | 중간 | 복잡 |
| AWS S3 의존성 | 일반적 | 일반적 | 높음 |
| 진행 상태 표시 가능 여부 | ✕ | ✕ | ○ |
| CORS 설정 필요 여부 | ✕ | ✕ | ○ |
| 운영 유지보수 | ✕ | ○ (주기적인 임시파일 삭제) | ✕ (S3 Lifecycle) |
만약 최대 20MB 프로필 이미지 업로드 기능을 개발하면서 AWS Multipart 업로드 방식으로 구현한다면 오버 엔지니어링이라 볼 수 있습니다.
따라서 주어진 시간 내에 구현하는 것을 최우선 목표로 하고 그 상황 속에서 타협하여 방식을 선택하는 것이 옳은 방법 같습니다.
블로그 포스팅에 사용된 코드는 아래 GitHub Repository에서 확인하실 수 있습니다!
https://github.com/YUBIN-githubb/s3uploadtest.git
GitHub - YUBIN-githubb/s3uploadtest
Contribute to YUBIN-githubb/s3uploadtest development by creating an account on GitHub.
github.com
아래 블로그를 참고하였습니다!
https://techblog.woowahan.com/11392/
Spring Boot에서 S3에 파일을 업로드하는 세 가지 방법 | 우아한형제들 기술블로그
Spring Boot에서 S3에 파일을 업로드하는 세 가지 방법 | 안녕하세요. 세일즈서비스팀에서 전자계약서 시스템을 개발하고 있는 박민규입니다. 최근 저는 Spring Boot + Kotlin을 활용한 프로젝트에서
techblog.woowahan.com
'SpringBoot' 카테고리의 다른 글
| [SpringBoot] 뉴스피드 시스템 아키텍처 디자인하기 (0) | 2025.12.11 |
|---|---|
| [SpringBoot] 피드에 좋아요 구현하기 (0) | 2025.12.03 |
| [SpringBoot] Bulk insert를 활용한 속도 개선 (0) | 2025.05.09 |
| [SpringBoot] Facade 패턴을 활용한 동시성 트랜잭션 범위 문제 해결하기 (1) | 2025.05.08 |
| [SpringBoot] 토스페이먼츠 결제 연동하기 (1) | 2025.04.29 |