AWS S3 적용하는 방법

신정범·2026년 9월 2일

개인 프로젝트를 진행하던 중에 기존 서버에 저장해놓았던 인터넷에서 다운받아놓은 저품질 이미지들을 고화질로 바꿔서 사이트에 적용해보고 싶어서 구상하게 되었습니다.

먼저 서버에 직접 저장하게되면 여러대의 서버중에 어떤 서버에 어떤 이미지가 있는지 알수 없으며, 서버가 죽게되면 이미지도 함께 날라가고, 서버 디스크 용량도 한계가 있기 때문입니다.

S3를 사용하게되면 이미지 저장공간을 서버와 완전히 분리할 수 있고 , 용량 제한에 대해서도 신경을 안써도 되며, 이미지 URL 하나로 어디서든 접근이 가능해지기 때문입니다.

S3 핵심 용어

  • Bucket : 이미지를 저장하는 저장소
  • Object : S3에 저장되는 파일 1개, 즉 이미지 파일
  • Key : S3안에서 파일을 구분하는 경로 이름 ex) product/123/main.jpg

업로드 방식엔 두가지 방법이 있습니다

방식 1. 백엔드 서버 경유 업로드

흐름은 :

프론트엔드
프론트엔드
  → 이미지 파일을 백엔드로 전송
  → 백엔드가 S3에 업로드
  → S3 URL 또는 key를 DB에 저장

장점:

  • 구현이 단순함
  • 백엔드에서 파일 검증, 리사이징, 권한 체크를 하기 쉽다.

단점:

  • 이미지 파일이 백엔드를 거쳐서 서버 트래픽 부하가 증가함
  • 대용량 이미지나 다중 업로드에 불리함

방식 2.Presigned URL 직접 업로드

흐름:

프론트엔드
  → 백엔드에 업로드 URL 요청
백엔드
  → 권한 확인 후 S3 Presigned URL 생성
프론트엔드
  → Presigned URL로 S3에 직접 이미지 업로드
프론트엔드
  → 업로드 완료 후 백엔드에 key/URL 저장 요청
백엔드
  → DB에 이미지 정보 저장

장점:

  • 서버 부하가 적음
  • 실제 파일 전송은 프론트엔드 → S3로 바로 감
  • 실무에서 많이 쓰는 방식

단점:

  • 업로드 완료 검증 로직이 필요함
  • Presigned URL 만료 시간, 파일 타입 제한 등을 신경 써야 함

일반적인 실무 흐름

1. 사용자가 이미지 선택
2. 프론트엔드가 파일명, 타입, 크기를 백엔드에 전달
3. 백엔드가 로그인/권한 확인
4. 백엔드가 S3 object key 생성
5. 백엔드가 Presigned URL 반환
6. 프론트엔드가 해당 URL로 PUT 업로드
7. 업로드 성공 후 백엔드에 imageKey 저장 요청
8. 백엔드가 DB에 이미지 경로 저장
9. 화면에서는 CloudFront URL 또는 S3 URL로 이미지 표시

Key 설계할때 주의할점
파일명을 그대로 사용하는것 보다는 uuid를 붙이는것이 좋습니다
ex) users/{userID}/profile/{uuid}.jpg

시작

먼저 AWS 홈페이지에 들어가서 회원가입을 한 후

S3를 클릭한다

버킷 만들기를 누르고

버킷 이름을 설정 한 후
엑세스 차단은 모두 비활성화 해주고 넘어간다

버킷이 생성되면 권한으로 들어가

버킷 정책을 추가하고 변경사항 저장을 누르면 된다.
그럼 버킷 생성이 완료 되었고

다시 홈으로 가서 IAM을 검색한다

IAM을 클릭하고

권한 설정을 하고

사용자 생성을 하면
AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY값을 얻을수 있다 이 값들은 메모장에 적어두어야 한다.

이제 Spring에 작성한 코드들을 살펴 보자

.env 파일에 위 값들을 입력하고

properties에는 외부에서는 보이지 않게 작성해준다.

그다음 이미지 파일에 대한 패키지를 생성하고

S3Config

S3Config는 Spring에서 AWS S3와 통신 할수 있도록 AmazonS3 객체를 Bean으로 등록하는 설정 클래스이다.

package com.jeongbeom.ecommerce.global.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration  // 설정 클래스 Spring 시작시 가장 먼저 읽음
public class S3Config {
    @Value("${cloud.aws.credentials.access-key:local-access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key:local-secret-key}")
    private String secretKey;

    @Value("${cloud.aws.region.static:ap-northeast-2}")
    private String region;

    @Bean
    public AmazonS3 amazonS3(){
        // 1단계: 아이디/비밀번호 묶기
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        // 2단계: 인증 정보 + 리전으로 S3 클라이언트 생성
        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();       // 완성된 S3 클라이언트 반환
    }

}

Controller

package com.jeongbeom.ecommerce.image;

import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController // json 응답을 반환
@RequiredArgsConstructor    //final 필드를 매개변수로 받는 생성자들을 자동 생성하여 의존성 주입
@RequestMapping("/products/images")
public class ProductImageController {

    private final ProductImageService productImageService;

    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<ProductImageUploadResponse> upload(   // 이미지 업로드 결과를 HTTP 응답으로 반환
            @RequestParam("image") MultipartFile image          // 요청 파라미터중 Image라는 이름의 파일을 받음
    ) {
        ProductImageUploadResponse response = productImageService.uploadProductImage(image);    //서비스에 이미지 업로드를 맡기고 결과를 DTO를 받음
        return ResponseEntity.ok(response); // 200oK, 업로드된 이미지 반환
    }
}

Service

package com.jeongbeom.ecommerce.image;

import com.jeongbeom.ecommerce.common.exception.CustomException;
import com.jeongbeom.ecommerce.common.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

@Service
@RequiredArgsConstructor // final 필드를 매개변수로 받는 생성자를 자동 생성
public class ProductImageService {

    //업로드를 허용할 이미지 MINE 타입 목록
    private static final List<String> ALLOWED_CONTENT_TYPES = List.of(
            "image/jpeg",
            "image/png",
            "image/webp"
    );

    private final S3Uploader s3Uploader;    //S3 업로드를 수행하는 클래스

    public ProductImageUploadResponse uploadProductImage(MultipartFile image) {
        validateImage(image);   //파일이 비어있지 않은지, 허용된 이미지 타입인지 검증

        try {
            String imageUrl = s3Uploader.upload(image); // S3에 파일을 업로드 하고 접근 가능한 이미지 URL 받음
            return new ProductImageUploadResponse(imageUrl);    //업로드된 이미지 URL을 을답 DTO에 반환
        } catch (IOException e) {
            throw new CustomException(ErrorCode.FILE_UPLOAD_FAIL_EXCEPTION);
        }
    }
    // 업로드 파일의 유효성을 검증하는 메서드
    private void validateImage(MultipartFile image) {
        //파일이 없거나 비어있는 경우를 확인
        if (image == null || image.isEmpty()) {
            throw new CustomException(ErrorCode.FILE_UPLOAD_FAIL_EXCEPTION);
        }
        //이미지가 아닌 파일이면 예외 처리
        if (!ALLOWED_CONTENT_TYPES.contains(image.getContentType())) {
            throw new CustomException(ErrorCode.FILE_UPLOAD_FAIL_EXCEPTION);
        }
    }
}

Response

package com.jeongbeom.ecommerce.image;

import lombok.Getter;

@Getter
public class ProductImageUploadResponse {
    private final String imageUrl;

    public ProductImageUploadResponse(String imageUrl) {
        this.imageUrl = imageUrl;
    }
}

S3Uploader

S3Uploader는 실제 S3 업로드 기능을 수행하는 클래스이다. 이 프로젝트에서 실질적인 S3 업로드는 이 클래스의 upload() 메서드에서 이루어진다.

package com.jeongbeom.ecommerce.image;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.UUID;

@Service
@RequiredArgsConstructor    // final 필드 생성자 자동 생성 (Lombok)
public class S3Uploader {

    private final AmazonS3 amazonS3;  // S3Config에서 만든 빈 자동 주입

    @Value("${cloud.aws.s3.bucket:local-ecommerce-bucket}")
    private String bucket;         // .env에서 버킷명 주입

    public String upload(MultipartFile file) throws IOException {

        // 중복 방지 파일명 생성
        String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();

        // S3에 전달할 파일 정보 세팅
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(file.getSize());
        metadata.setContentType(file.getContentType());

        // 실제 업로드
        amazonS3.putObject(bucket, fileName, file.getInputStream(), metadata);

        // 업로드된 파일 URL 반환
        return amazonS3.getUrl(bucket, fileName).toString();
    }
}

코드들을 작성하여 이미지를 업로드 하면 해당 S3 버킷에 객체가 생성되는걸 볼수 있다.

profile
성장하는 개발자가 되겠습니다

0개의 댓글