OPEN HYPER STEP
← 목록으로 (Java+Spring)
JAVA · 78 / 99
java
CHAPTER 78 / 99
읽기 약 2
FUNCTION

파일 업로드와 S3 연동


핵심 개념

MultipartFile·AWS S3 SDK·pre-signed URL·이미지 리사이징 — 프로필 이미지 + CDN.

본문

단순 파일 업로드

JAVA📋 코드 (23줄)
@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 서비스

JAVA📋 코드 (35줄)
// 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 — 클라이언트 직접 업로드

JAVA📋 코드 (41줄)
@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());
    }
}

이미지 리사이징

JAVA📋 코드 (41줄)
// 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