Java

[TIL] AWS S3를 이용하여 Spring JPA에 미디어 API 구현하기

손가든 2024. 1. 20. 02:29

요근래 B형 독감에 걸려서 3일간 공부를 못했다.

 

나만무 프로젝트 기간의 시작이다 보니 3일동안의 공백이 너무 컸다.

 

이제 다시 빡세게 좀 마음을 다잡고 프젝 기간에 불태워보려고 한다.

 

우리 나만무 프로젝트가 미디어 파일을 업로드하고 로드해야하는 플랫폼이다 보니 S3를 활용해보려 한다.

 

Spring JPA에서 어떻게 AWS S3를 활용할 수 있는지 알아보자.

 


 

AWS S3는 5GB의 용량까지 프리 티어로 사용할 수 있다.

따라서 해당 프리 티어 S3를 사용해보자.

 

S3에서 버킷을 생성하면 보안 키를 생성해줄 것이다.

 

그 이후에 우리 project에서 AWS SDK를 지원하는 dependency를 추가해준다.

<dependencies>
    <!-- AWS SDK -->
    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>s3</artifactId>
    </dependency>

    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

 

더보기

SDK는 무엇인가?

 

AWS 홈페이지에서는 SDK를 다음과 같이 설명하고 있다.

 

소프트웨어 개발 키트(SDK)는 개발자를 위한 플랫폼별 구축 도구 세트입니다. 특정 플랫폼, 운영 체제 또는 프로그래밍 언어에서 실행되는 코드를 만들려면 디버거, 컴파일러 및 라이브러리와 같은 구성 요소가 필요합니다. SDK는 소프트웨어를 개발하고 실행하는 데 필요한 모든 것을 한 곳에서 제공합니다. 또한 SDK에는 문서, 튜토리얼 및 가이드와 같은 리소스와 더 빠른 애플리케이션 개발을 위한 API 및 프레임워크가 포함됩니다.

 

SDK는 S3를 사용할 때 사용되며, SDK가 도와주는 클라우드 서비스 환경에서 S3를 용이하게 사용하기 위해 SDK를 설치하는 대신 dependency를 설치하는 것으로 이해하자.

 

SDK가 사용되는 예 중 하나

 

 

그리고 생성한 버킷의 key와 함께 S3에 관한 메타데이터를 application.yml 파일에 작성한다.

aws.accessKey=YOUR_ACCESS_KEY
aws.secretKey=YOUR_SECRET_KEY
aws.s3.bucketName=YOUR_BUCKET_NAME
aws.region=YOUR_REGION

 

자 이제 서비스 코드를 작성해보자.

 

 

Upload media

미디어 파일을 업로드하는 래퍼를 포함한 서비스 코드를 작성하면 다음과 같다.

 

import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.File;

@Service
public class S3Service {

    private final S3Client s3Client;
    private final String bucketName;

    public S3Service(S3Client s3Client,
                    @Value("${aws.s3.bucketName}") String bucketName) {
        this.s3Client = s3Client;
        this.bucketName = bucketName;
    }

    public void uploadFile(String key, File file) {
        s3Client.putObject(PutObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build(), file.toPath());
    }
}

 

S3Client 클래스를 통해 요청받은 file을 S3 스토리지에 업로드하는 코드를 작성했다.

 

해당 컨트롤러에서는 다음과 같이 S3Service를 생성자 주입 받는다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;

@RestController
@RequestMapping("/api/files")
public class FileController {

    private final S3Service s3Service;

    @Autowired
    public FileController(S3Service s3Service) {
        this.s3Service = s3Service;
    }

    @PostMapping("/upload")
    public void uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        s3Service.uploadFile(file.getOriginalFilename(), convertMultiPartToFile(file));
    }

    // 추가적으로 다운로드 API를 작성할 수 있습니다.
}

 

Post 메소드를 사용하여 인자로 MultipartFile file 받은 뒤 -> convertMultiPartToFile 메소드로 해당 파일을 File 객체로 변환한다.

 

이렇게 변환하는 이유는 클라이언트에서 해당 api를 통해 요청할 때 보내는 HTTP request 때문인데,

 

원래 dummy data를 보낼 때는 그저 Json 형태로 requestbody에 데이터를 담아 보냈지만, 업로드 파일은 다르다.

 

업로드한 파일을 백 서버에 전달할 때는 Multipart 형태로 제공하는데 다음과 같다.

 

POST /api/upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123

------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain

...file content goes here...
------WebKitFormBoundaryABC123--

 

이 데이터를 알아서 잘 파싱해서 데이터를 잘 추출하도록 하는 클래스가 multipartData인 것이다.

 

이 데이터는 java에서 S3 버킷에 데이터를 저장할 때는 multipart 형태가 아닌 file 객체 형태라고 가정된다.

 

따라서 우리는 다음과 같은 multipart 타입의 제공받은 미디어 파일을 file 형태로 변환한 것이다.

 

Download media

미디어를 다운로드 하는 api도 알아보자.

 

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/media")
public class MediaController {

    private final S3Service s3Service;

    // S3Service 주입
    @Autowired
    public MediaController(S3Service s3Service) {
        this.s3Service = s3Service;
    }

    @GetMapping("/video/{fileName}")
    public ResponseEntity<String> getVideoUrl(@PathVariable String fileName) {
        String videoUrl = s3Service.getPreSignedUrl(fileName);
        return ResponseEntity.ok(videoUrl);
    }
}

 

fileName을 통해 해당되는 스토리지의 파일을 불러오는 controller의 형태부터 확인해보자.

 

@PathVariable 을 사용하여 fileName을 받은 뒤 그 fileName을 통해 getPreSignedUrl 메소드를 통해 해당 파일의 Url을 가져온다.

 

import software.amazon.awssdk.services.s3.model.GetUrlRequest;
import java.time.Duration;

@Service
public class S3Service {

    // ... (이전 코드)

    public String getPreSignedUrl(String key) {
        // 예시: 유효 기간을 1시간으로 설정
        Duration expiration = Duration.ofHours(1);

        GetUrlRequest getUrlRequest = GetUrlRequest.builder()
                .bucket(bucketName)
                .key(key)
                .expiration(expiration)
                .build();

        return s3Client.utilities().getUrl(getUrlRequest).toString();
    }
}

 

이 Url은 1시간의 유효 기간을 설정하여 1시간이 지나면 해당 경로로 다운로드를 받을 수 없도록 기간을 둠으로써, 보안을 강화했다.

 

Stream media

유투브와 같은 영상 시청 플랫폼등은 동영상 재생 플레이어를 사용한다.

 

이때, 동영상을 재생하면 해당 영상이 처음부터 끝까지 모두 다운로드가 된 후 재생되는 것이 아니라, 동적 로딩을 통해 시청자가 보고 있는 구간만 로딩하여 재생하는 방식을 활용한다.

 

이 로직은 프론트엔드에서 HTTP Live Streaming(HLS)이나 Dynamic Adaptive Streaming over Http(DASH) 과 같은 스트리밍 프로토콜을 사용하는데, 이 기반을 백엔드에서 협력하기 위해서 stream의 데이터로 제공해줘야 할 필요가 있다.

 

그때 백엔드에서는 streaming할 데이터가 제공되는 길을 열어주는데 이게 바로 S3의 데이터를 접근하는 URL을 생성해주는 것이다.

 

이 URL은 위의 download service와 똑같은 메소드를 사용한다.

 

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class VideoController {

    private final S3Service s3Service;

    public VideoController(S3Service s3Service) {
        this.s3Service = s3Service;
    }

    @GetMapping("/api/videos/stream-url")
    public ResponseEntity<String> getVideoStreamingUrl(@RequestParam String key) {
        String streamingUrl = s3Service.getPresignedUrl(key);

        // 클라이언트에게 스트리밍 URL을 반환
        return ResponseEntity.ok(streamingUrl);
    }
}

 

 

유투브 동영상과 같은 재생 플레이어를 위한 로딩은 동적으로 수행하는 로직이 프론트엔드에서 진행되며, 백엔드에서는 다운로드 받을 URL을 열어주기만 하면 된다.

 

이때, URL을 통한 다운로드의 만료시간은 항상 두어야 하는데, 그 이유는 무분별한 로딩을 통한 서버 공격, 그리고 무한 배포와 같은 저작권 문제, URL을 탈취당할 시 해당 동영상의 다운로드 제어권을 영원히 탈취당하는 등의 문제 때문이다.