java
CHAPTER 78 / 99
읽기 약 2분
FUNCTION
파일 업로드와 S3 연동
핵심 개념
MultipartFile·AWS S3 SDK·pre-signed URL·이미지 리사이징 — 프로필 이미지 + CDN.
본문
단순 파일 업로드
@RestController
@RequiredArgsConstructor
public class FileController {
private final S3Service s3Service;
@PostMapping("/upload")
public ResponseEntity<UploadResponse> upload(
@RequestParam MultipartFile file,
@RequestParam(required = false) Long userId
) {
if (file.isEmpty() || file.getSize() > 10 * 1024 * 1024) {
throw new InvalidFileException("파일 크기 10MB 이하");
}
String contentType = file.getContentType();
if (!List.of("image/jpeg", "image/png", "image/webp").contains(contentType)) {
throw new InvalidFileException("이미지 파일만 허용");
}
String key = "uploads/" + userId + "/" + UUID.randomUUID() + "_" + file.getOriginalFilename();
String url = s3Service.upload(file, key);
return ResponseEntity.ok(new UploadResponse(url));
}
}S3 서비스
// build.gradle: implementation 'software.amazon.awssdk:s3:2.25.0'
@Service
@RequiredArgsConstructor
public class S3Service {
private final S3Client s3Client;
@Value("${aws.s3.bucket}") private String bucket;
@Value("${aws.s3.cdn-url}") private String cdnUrl;
public String upload(MultipartFile file, String key) {
try {
PutObjectRequest req = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(file.getContentType())
.contentLength(file.getSize())
.acl(ObjectCannedACL.PUBLIC_READ)
.cacheControl("public, max-age=31536000") // 1년
.build();
s3Client.putObject(req,
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
return cdnUrl + "/" + key;
} catch (IOException e) {
throw new FileUploadException("업로드 실패", e);
}
}
public void delete(String key) {
s3Client.deleteObject(DeleteObjectRequest.builder()
.bucket(bucket).key(key).build());
}
}Pre-signed URL — 클라이언트 직접 업로드
@Service
@RequiredArgsConstructor
public class S3PresignService {
private final S3Presigner presigner;
@Value("${aws.s3.bucket}") private String bucket;
// 클라이언트가 이 URL로 직접 PUT — 서버 부하 감소
public PresignedUploadResponse generateUploadUrl(String filename, String contentType) {
String key = "uploads/" + UUID.randomUUID() + "_" + filename;
PutObjectRequest objReq = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(contentType)
.build();
PutObjectPresignRequest req = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5))
.putObjectRequest(objReq)
.build();
PresignedPutObjectRequest presigned = presigner.presignPutObject(req);
return new PresignedUploadResponse(
presigned.url().toString(),
key
);
}
}
@RestController
@RequiredArgsConstructor
public class UploadController {
private final S3PresignService presignService;
@PostMapping("/upload-url")
public PresignedUploadResponse getUploadUrl(@RequestBody UploadRequest req) {
return presignService.generateUploadUrl(req.filename(), req.contentType());
}
}이미지 리사이징
// build.gradle: implementation 'net.coobird:thumbnailator:0.4.20'
@Service
public class ImageProcessor {
public byte[] resize(MultipartFile file, int maxWidth, int maxHeight) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Thumbnails.of(file.getInputStream())
.size(maxWidth, maxHeight)
.outputFormat("webp")
.outputQuality(0.85)
.toOutputStream(out);
return out.toByteArray();
}
}
// 업로드 후 여러 사이즈 생성
@Service
@RequiredArgsConstructor
public class ProfileImageService {
private final S3Service s3;
private final ImageProcessor imageProcessor;
public ProfileImage upload(MultipartFile file, Long userId) throws IOException {
String basekey = "profile/" + userId + "/" + UUID.randomUUID();
// 원본 + 3개 사이즈
byte[] original = file.getBytes();
byte[] large = imageProcessor.resize(file, 1024, 1024);
byte[] medium = imageProcessor.resize(file, 512, 512);
byte[] thumb = imageProcessor.resize(file, 128, 128);
return new ProfileImage(
s3.uploadBytes(basekey + "_original.webp", original),
s3.uploadBytes(basekey + "_large.webp", large),
s3.uploadBytes(basekey + "_medium.webp", medium),
s3.uploadBytes(basekey + "_thumb.webp", thumb)
);
}
}다음 챕터
CH.10 "API 문서화: Swagger" — 자동 생성.
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 Spring 코드의 파일 업로드 부분을 분석해서 S3 비용·CDN 최적화와 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
파일 업로드 vs 다른 패턴 비교를 실전 사례 5개로 보여주고 직접 업로드 vs Pre-signed를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스 전체를 분석해서 파일 업로드 관련 업로드 보안 위반를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 기업의 파일 업로드 채택률과 한국 SaaS 이미지 처리 패턴를 솔직히 알려줘.
⭐ 이것만 기억하세요
파일 업로드와 S3 연동은 이 3가지만 확실히 잡으세요
1.S3 업로드는 직접(MultipartFile) vs Pre-signed URL — 후자가 서버 부하 적음
2.Thumbnailator로 리사이징 + WebP 압축 + CDN 캐싱 = 빠른 이미지 서비스
3.다음 챕터 CH.10에서 Swagger — API 문서 자동 생성
공유하기
진행도 78 / 99