SpringBoot Server/API

AWS Java SDK를 이용하여 S3에 파일 업로드 REST API 구현하기

ssury94 2025. 1. 3. 23:43
 
 
 

 

지금까지는 문자열만 전달했기 때문에 Body - JSON문으로 리퀘스트를 보냈었습니다.

이번엔 문자열이 아닌, 사진파일을 저장해봅시다.

 

사진파일은 DB에 저장 될까요?

 

모든 데이터를 구분없이 전부 DB에 저장하면 관리가 어렵기때문에

Storage라는 파일 저장용 서버를 이용하고, DB에는 Storage에 저장된 URL을 저장하여 관리합니다.

 

DB는 구조화된 데이터를 저장하고, 테이블 형식으로  사용하는 논리적으로 정리된 공간,

Storage는 여러 파일, 객체를 담는 바구니 같은 물리적인 공간이라고 보면 되겠습니다.

바구니가 가득 차게되면 새 바구니로 확장하면 되기 때문에 효율적입니다.

S3 버킷 생성

우선 파일 저장을 위한 Storage를 만들겠습니다.

 

AWS에서는 S3이라는 클라우드 기반의 객체 스토리지 서비스를 제공하고 있습니다.

프리티어 이용자도 최대 5기가 까지 무료로 이용가능하며,

유료로 사용시 비용도 저렴한 편에 저장가능한 데이터 양이 무한에 가깝고, 파일 저장에 특화되어있어 유지관리가 편하여 전 세계적으로 많이 사용하는 클라우드 스토리지 서비스입니다.

 

 

S3 콘솔 화면

 

 

 

버킷 이름은 모든 Region에서 고유한 이름이여야하기때문에 아이디와 사용목적에 맞는 단어로 조합하는 쪽이 좋습니다.

AWS의 버킷 명명 규칙 레퍼런스

https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html?icmpid=docs_amazons3_console

 

General purpose bucket naming rules - Amazon Simple Storage Service

Before March 1, 2018, buckets created in the US East (N. Virginia) Region could have names that were up to 255 characters long and included uppercase letters and underscores. Beginning March 1, 2018, new buckets in US East (N. Virginia) must conform to the

docs.aws.amazon.com

 

내용을 참고해서 버킷을 만들어봅시다.

버킷 설정하기

 

ACL이란?

Access Control List. 즉 말 그대로 접근 제어 목록입니다.

AWS에서는 버킷소유자 외에는 각 객체에 대해 액세스를 개별적으로 관리하도록 비활성화 하는 것을 권장하고있으나

테스트를 위해 다른 AWS계정에서도 접근 및 소유가 가능하도록 ACL를 활성화 하고 진행하겠습니다.

 

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/managing-acls.html

 

ACL 구성 - Amazon Simple Storage Service

ACL 구성 이 섹션에서는 ACL(액세스 제어 목록)을 사용하여 S3 버킷에 대한 액세스 권한을 관리하는 방법을 설명합니다. AWS Management Console, AWS Command Line Interface(CLI), REST API 또는 AWS SDK를 사용하면 리

docs.aws.amazon.com

 

 

외부에서 S3 버킷으로 바로 액세스가 가능한지 확인을 위해 퍼블릭 액세스도 퍼블릭으로 진행 하겠습니다.

실 사용시에는 보안 상황에 맞게 설정하여 사용합시다.

 

 

AWS IAM 에서 사용자 설정하기

IAM 이란?

AWS 상의 리소스에 대한 액세스를 제어하는 서비스입니다.

보안상 일반적인 작업에 루트 사용자를 사용하는 것은 위험할 수 있으므로 권한을 그룹, 또는 사용자를 생성해서 관리할 수 있습니다.

사람 뿐만 아니라 접근하고자하는 서버에 대해서도 권한설정이 가능합니다.

 

 

https://docs.aws.amazon.com/ko_kr/IAM/latest/UserGuide/introduction.html

 

IAM이란 무엇입니까? - AWS Identity and Access Management

IAM이란 무엇입니까? AWS Identity and Access Management(IAM)은 AWS 리소스에 대한 액세스를 안전하게 제어할 수 있는 웹 서비스입니다. IAM을 사용하면 사용자가 액세스할 수 있는 AWS 리소스를 제어하는 권

docs.aws.amazon.com

 

 

서버가 S3 버킷에서 객체에 대한 읽기, 쓰기, 삭제 및 기타 모든 작업을 할 수 있도록 권한 설정을 하겠습니다.

사용자 권한 설정

 

 

다양하게 세분화되어있는 정책들 중에서

S3 전체에 액세스가 가능한 S3FullAccess를 선택하고 생성하면

 

 

AmazonS3FullAccess 권한을 가진 사용자가 생성 되었습니다.

 

 

Access-key, Secret-key 발급

 

 

주의! 여기서 끝이 아니라 access-key, secret-key 두가지를 발급받아야합니다.

액세스키는 처음 한번만 확인 및 저장이 가능하며, 분실시에는 새로 발급받아야하고

외부유출시 해킹의 위험이 있으니 주의하여 보관합니다.

 

AWS의 액세스 키 권장사항
-액세스 키를 일반 텍스트, 코드 리포지토리 또는 코드로 저장해서는 안됩니다.
-더 이상 필요 없는 경우 액세스 키를 비활성화하거나 삭제합니다.
-최소 권한을 활성화합니다.
-액세스 키를 정기적으로 교체합니다.

 

 

 

AWS SDK를 사용하기위한 IntelliJ 설정

이제 IntelliJ에서 마저 설정을 진행합니다.

 

 

application.yml 설정

spring:
  profiles:
    active: dev

cloud:
  aws:
    credentials:
      access-key:  // 액세스 키 입력
      secret-key:  // 시크릿 키 입력
    s3:
      bucket: //버킷 이름 입력
      region: ap-northeast-2 // 한국 리전

 

pom.xml에 AWS SDK 의존성 추가

</properties>
<!-- AWS SDK -->
<dependencyManagement>
    <dependencies>
       <dependency>
          <groupId>software.amazon.awssdk</groupId>
          <artifactId>bom</artifactId>
          <version>2.25.60</version>
          <type>pom</type>
          <scope>import</scope>
       </dependency>
    </dependencies>
</dependencyManagement>
<!-- AWS SDK -->
<dependencies>

    <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>s3</artifactId>
    </dependency>

 

AWS SDK 의존성을 추가합니다.

AWS SDK란?

Amazon Web Services(AWS)에서 제공하는 소프트웨어 개발 키트(SDK)로, Java 애플리케이션에서 AWS 서비스에 쉽게 접근하고 사용할 수 있도록 돕는 라이브러리입니다. Java SDK를 사용하면 S3와 같은 AWS 서비스에 대한 API 호출을 간편하게 수행할 수 있으며, 인증, 요청 형식화, 응답 처리 등의 복잡한 작업을 자동으로 처리해 줍니다.

 

S3에 파일을 업로드 하는 예시

S3Client s3Client = S3Client.builder()
    .region(Region.of("ap-northeast-2"))
    .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("accessKey", "secretKey")))
    .build();

PutObjectRequest putObjectRequest = PutObjectRequest.builder()
    .bucket("your-bucket-name")
    .key("your-file-key")
    .acl(ObjectCannedACL.PUBLIC_READ)
    .build();

s3Client.putObject(putObjectRequest, RequestBody.fromFile(new File("path/to/your/file")));

 

S3Config 클래스 추가

@Configuration
public class S3Config {

    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;
    //${ yml 에 적은 값들을 가져옴

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

    @Value("${cloud.aws.s3.region}")
    private String region;

    @Bean
    public S3Client amazonS3Client() {
        AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKey, secretKey);
        return S3Client.builder()
                .region(Region.of(region))
                .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
                .build();
    }
}

 

UniqueFileNameGenerator 유틸클래스 추가

마지막으로, 

유저들이 올리는 파일 이름을 유저가 보낸 이름 그대로 사용하게 될 경우 유니크처리가 제대로 되지 않을 수 있기때문에

유저아이디와 업로드 시간을 조합한 파일이름으로 변환해주는 Util UniqueFileNameGenerator을 추가하겠습니다.

public class UniqueFileNameGenerator {
    public static String generateUniqueFileName(long userId, String extension) {
        LocalDateTime now = LocalDateTime.now();
        String timestamp = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
        return userId + "_" + timestamp + "_" + System.nanoTime() + extension;
    }
}

 

 

 

사진 업로드 API 만들기

포스트맨에 리퀘스트 준비

 

 

파일 리퀘스트는 Body에 Form-Data로 전송합니다.

POST HTTP 메서드를 사용하여

api/v1/reviews/restaurant/{restaurantId}/menu/{menuId} 경로에서 사진을 첨부한 리뷰를 작성할 수 있는 API를 구현하겠습니다.

 

- rating  (text) : 5

- content (text) : 이 음식 정말 맛있게 잘 먹었습니다~ 굿!

- image   (file) : 사진파일

 

Form-data는 문자(text)와 파일 (File) 두 형식으로 전달됩니다.

숫자도 텍스트(String)로 들어오기 때문에, 로직을 담당하는 Service클래스에서 int로 변환처리합니다.

 

 

DB에 이미지 주소 저장할 컬럼 만들기

 

이미지 첨부없이 데이터를 작성하는 경우도 있으므로 디폴트는 ''로 설정합니다.

 

 

API 쿼리 구현

스토리지를 이용한 파일저장시 API 로직은 위와 같습니다.

 

Controller)

1)HTTP Post 요청을 처리합니다.

JWT토큰과 이미지 파일(required=false '선택사항')을 서비스 클래스에 전달합니다.

 

Service)

JWT 토큰에서 유저아이디 추출,

파일이 있는 경우

파일이름을 UniqueFileNameGenerator 유니크하게 변경한 후, 2)S3에 이미지를 업로드 하고 3)이미지 URL을 저장합니다.

그리고 4)이미지 URL을 DAO에 전달합니다.

파일이 없는 경우 imageUrl은 초기화한대로 빈 문자열로 전달됩니다.

 

DAO)

5)서비스클래스에서 전달받은  user_id,restaurant_id,menu_id,rating,content,image_url를 SQL쿼리를 통해 DB에 저장합니다.

6)저장된 결과를 서비스 클래스에 반환합니다.

 

Service)

6)서비스클래스도 DAO에서 받은 결과를 바탕으로 컨트롤러클래스에 반환합니다.

 

Controller)

6)리뷰 저장이 잘 되었으면 201, 실패했을경우 400으로 결과를 반환합니다.

 

Controller 클래스

@RestController
public class ReviewController {
    @Autowired
    ReviewService reviewService;

    // 사진, 별점, 내용 보내기

    @PostMapping("/api/v1/reviews/restaurant/{restaurantId}/menu/{menuId}")
    public ResponseEntity<Void> createReviewPhoto(
            @RequestHeader("Authorization") String token,
            @PathVariable long restaurantId,
            @PathVariable long menuId,
            @RequestParam("rating") String strRating,
            @RequestParam("content") String content,
            @RequestParam(required = false,value = "image") MultipartFile image){

        try {
            reviewService.createReviewPhoto(token, restaurantId, menuId,
                    strRating, content, image);
        } catch (Exception e) {
            return ResponseEntity.status(400).build();
        }
        return ResponseEntity.status(201).build();
    }

Service 클래스

@Value("${cloud.aws.s3.bucket}")
String bucketName;
// 중요한것은 Class 에 바로 쓰지 않는다. > @Value로 가져와서 사용한다.
@Autowired
S3Client s3Client; //업로드 담당



// 사진, 별점, 내용 보내기
public int createReviewPhoto(String token, long restaurantId, long menuId, String strRating, String content, MultipartFile image) {
    //1. 토큰에서 유저아이디 추출
    long userId = Long.parseLong(jwtConfig.getTokenClaims(token.substring(7)).getSubject());

    //String rating 으로 온다. < strRating 로 변수 처리 Form-data
    int rating = Integer.parseInt(strRating);
    // 먼저 text String 으로 온 rating 을 int 로 바꿔줌

    String imageUrl = "";
    // imageUrl 을 빈 문자열로 초기화

    if (image != null && !image.isEmpty()) {
        //a. 이미지가 없으면 null
        //b. 이미지가 있으면 S3에 업로드
        //2. 이미지를 S3에 저장
        //2-1 유저들이 저장한 사진 이름으로 하면 중복되는 등 문제가 생길 수 있다. 유니크하게 바꿔줘야함.(업로드 시간, 유저아이디 조합) -UniqueFileNameGenerator 이용
        String fileName = UniqueFileNameGenerator.generateUniqueFileName(userId, ".jpg");
        //버킷 변수를 가져옴 < @Value("${cloud.aws.s3.bucket}") String bucketName;
        //2-2 S3에 업로드를 위한 Request 객체를 생성한다.
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(fileName)
                .contentLength(image.getSize())
                .contentType(image.getContentType())
                .acl(ObjectCannedACL.PUBLIC_READ)
                // ACL : Access Control List public read 로 해야 다른 사람들도 볼 수 있음
                .build();
        //2-3 S3에 이미지를 업로드한다.
        try {
            s3Client.putObject(putObjectRequest,
                    RequestBody.fromInputStream(image.getInputStream(), image.getSize()));
            // s3Client 더러 putObject 를 실행하라하기 위해 s3Client 멤버변수 만듬 @Autowired S3Client s3Client;

            //빨간줄뜨는 이유 > 에러날 수도 있다 > try catch로 감싸준다 < 자동완성됨
            //3. 이미지 URL 을 가져온다.
            imageUrl = String.format("https://%s.s3.amazonaws.com/%s", bucketName, fileName);
            // String.format : 문자열을 만들어주는 메소드
            // URL 형식: "https://{버킷이름}.s3.amazonaws.com/{파일이름}"
            // %s는 문자열 형식 지정자로, 순서대로 bucketName과 fileName 변수의 값으로 대체
        } catch (IOException e) {
            System.out.println("S3 업로드 실패");
            throw new RuntimeException(e);
            // Exception 은 controller 에서 처리하도록 던져준다.
            //.getInputStream() 은 IOException 을 던지기 때문에 try catch로 감싸준다.
            // 에러 발생시 컨트롤러에게 던저준다 > 컨트롤러에서 500 에러 처리
        }
    }
    //4. DB 저장
    reviewDAO.createReviewPhoto(userId, restaurantId, menuId, rating, content, imageUrl);
    return 0;
    //성공시 0 반환
}

DAO 클래스

@Repository
public class ReviewDAO {
    @Autowired
    JdbcTemplate jdbcTemplate;


    // todo 4. 사진, 별점, 내용 보내기
    public int createReviewPhoto(long userId, long restaurantId, long menuId, int rating, String content, String imageUrl) {


        String sql = "INSERT into review (user_id,restaurant_id,menu_id,rating,content,image_url)\n" +
                "values (?,?,?,?,?,?);";
        return jdbcTemplate.update(sql, userId, restaurantId, menuId, rating, content, imageUrl);
    }
    // 사진, 별점, 내용 보내기 < 사진없이
    public int createReviewPhoto(long userId, long restaurantId, long menuId,
                                 int rating, String content){
        String sql = "insert INTO review (user_id, restaurant_id, menu_id, rating, content)\n" +
                "values (? , ? , ? , ? , ? );";
        return jdbcTemplate.update(sql, userId, restaurantId, menuId, rating, content);
    }

 

 

* 기존에 createReview(String token, ReviewRequest reviewRequest) 라는 사진없이 리뷰를 작성하는 API를 만들어둔 상태여서

새로 만든 API와의 구분을 위해

쿼리파라미터에 이미지가 없는 경우, 있는 경우로 Method Overloading 구현했습니다.