개발 알다가도 모르겠네요

[Spring] AWS S3 + CloudFront를 활용해서 음악 공유 URL을 만들어보자 본문

웹/Spring

[Spring] AWS S3 + CloudFront를 활용해서 음악 공유 URL을 만들어보자

이재빵 2024. 5. 27. 23:42
728x90

배경

인생네컷에는 QR코드를 통해 촬영 과정을 담은 비디오, 사진 이미지를 다운받을 수 있는 유효시간 72시간의 URL을 제공한다.

현재 개발중인 Daytune 앱 내에도 사용자가 자신의 음악을 주위 친구들에게 자랑함으로써 참여도를 높이고, 유입자 수를 증가시키기 위해 공유 URL 기능을 추가하기로 했다. 현재는 사용자가 일기를 작성하면, 그 일기의 감정에 맞는 음악 2개를 생성해주고 사용자는 2개 중 하나를 골라 소유하게 된다. 그 다음 과정으로 72시간동안 접근 가능한 음악공유 URL 을 생성하여 제공하는 방식이다.

 

그렇다면 유효시간이 설정된 url은 어떻게 만들어야 할까? 답은 AWS S3의 Presigned Url 기능에 있다.

 

Pre-Signed URL 이란?

Pre-Signed URL은 AWS S3의 객체에 대한 제한된 시간 동안의 접근 권한을 부여하는 URL이다. 이 URL을 통해, S3 버킷에 저장된 객체에 대해 제한된 시간 동안 읽기 또는 쓰기 권한을 부여할 수 있다. Pre-Signed URL을 생성하는 주체는 해당 객체에 접근할 권한을 가진 사용자여야 하며, URL을 생성할 때 특정 기간 동안만 유효하도록 설정할 수 있다고 한다.

 

AWS CloudFront 란?

CloudFront는 글로벌 콘텐츠 배포 네트워크(CDN) 서비스이다. 웹 콘텐츠, 동영상, 애플리케이션 데이터 등의 정적 및 동적 콘텐츠를 보다 빠르게 제공할 수 있고, 지리적으로 분산된 엣지 로케이션을 활용하여 사용자에게 가장 가까운 서버에서 콘텐츠를 제공함으로써 성능을 최적화시킨다고 한다.
특히 SSL/TLS 암호화를 통해 데이터 전송의 보안을 보장하고, Origin Access Identity(OAI)를 사용하면 S3 버킷을 CloudFront를 통해서만 접근 가능하도록 설정할 수 있다.

 

구현과정

AWS S3과 Cloud Front를 활용하여 유효시간이 있는 공유 URL을 생성해 보자.

build.gradle 추가

	implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

yml파일 내 AWS S3 관련 정보 추가

cloud:
  aws:
    credentials:
      accessKey: 액세스키
      secretKey: 비밀키
    region:
      static: 지역
    stack:
      auto: false
    s3:
      bucket: S3 버킷명
    cloudfront:
      domain: 설정한 cloudfront 도메인명

 

File Util 구현

/**
 * 파일 다운로드 유틸리티
 * 주어진 URL에서 파일을 다운로드하여 임시 파일로 저장하는 기능
 */
public class FileUtil {
    /**
     * 주어진 URL에서 파일을 다운로드하여 임시 파일로 저장
     * @param fileUrl 다운로드할 파일의 URL
     * @return 다운로드한 임시 파일
     * @throws IOException 파일 다운로드 중 오류가 발생한 경우
     */
    public static File downloadFile(String fileUrl) throws IOException {
        // URL 객체 생성
        URL url = new URL(fileUrl);

        // URL에 대한 HttpURLConnection 객체 생성
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // HTTP 요청 메서드를 GET으로 설정
        connection.setRequestMethod("GET");

        // 연결 시도
        connection.connect();

        // 서버 응답 코드가 HTTP OK(200)인지 확인
        if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
            // 응답 코드가 HTTP OK가 아닌 경우 예외 발생
            throw new IOException("파일 다운로드 실패: " + connection.getResponseMessage());
        }

        // 서버로부터 파일 데이터를 읽기 위한 입력 스트림 열기
        InputStream inputStream = connection.getInputStream();

        // 임시 파일 생성 (접두사 "temp", 접미사 ".mp3" 사용)
        File tempFile = File.createTempFile("temp", ".mp3");

        // 임시 파일에 데이터를 쓰기 위한 파일 출력 스트림 생성
        FileOutputStream outputStream = new FileOutputStream(tempFile);

        // 데이터 읽기 및 쓰기를 위한 버퍼 생성 (버퍼 크기: 4096 바이트)
        byte[] buffer = new byte[4096];
        int bytesRead;

        // 입력 스트림으로부터 데이터를 읽어와 버퍼에 저장하고, 출력 스트림으로 씀
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }

        // 출력 스트림 닫기 (자원 해제)
        outputStream.close();

        // 입력 스트림 닫기 (자원 해제)
        inputStream.close();

        // 다운로드한 임시 파일 반환
        return tempFile;
    }
}

 

S3 Config 추가

/**
 * S3 연동을 위한 환경 설정
 */
@Slf4j
@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;

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

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

    @Bean
    public AmazonS3 amazonS3() {
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(Regions.fromName(region))
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }
}

 

S3Service 구현

AWS S3에 파일을 업로드하고 CloudFront URL을 생성하는 서비스가 필요하다. 이 때 CloudFront URL를 이용하는 이유는 s3에서 생성된 url을 그대로 사용할 경우, s3의 주소가 그대로 노출된 뿐 아니라 url길이가 아래처럼 너무 길어지기 때문이다.

 

https://daytune-share-url-s3.s3.ap-northeast-2.amazonaws.com/pages/9662c047-e9b2-4358-8d60-c3643df50a3f.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240523T151412Z&X-Amz-SignedHeaders=host&X-Amz-Expires=259199&X-Amz-Credential=AKIA6GBMES42FVAAMYOK%2F20240523%2Fap-northeast-2%2Fs3%2Faws4_request&X-Amz-Signature=af0d750532e5501c06c2bf13310a7c624ed9ff2275ce2ab9e02bfb0b559dba62

 

 

CloudFront 를 통해 Url을 생성할 경우 아래처럼 설명한 도메인명, 짧은 길이의 주소가 생성된다.

 

https://cdn.chewthecud.site/pages/ab9b9130-e520-44db-8d65-8e7b74e173c6.html

 

S3Service 클래스는 파일을 S3에 업로드하고, 업로드된 파일의 URL을 반환하는 역할을 한다. 이를 통해 사용자들이 업로드한 파일을 클라우드에서 직접 접근할 수 있게 한다.

@Service
public class S3Service {
    @Autowired
    private AmazonS3 amazonS3;

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

    @Value("${cloud.aws.cloudfront.domain}")
    private String cloudFrontDomain;

    /**
     * File 객체를 S3에 업로드
     * @param fileName 업로드할 파일의 이름
     * @param file 업로드할 파일 객체
     * @return 업로드된 파일의 CloudFront URL
     */
    public String uploadFile(String fileName, File file) {
        try (InputStream inputStream = new FileInputStream(file)) {
            // 파일 메타데이터 설정
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentLength(file.length());
            metadata.setContentType("audio/mpeg");

            // S3에 파일 업로드
            amazonS3.putObject(new PutObjectRequest(bucketName, fileName, inputStream, metadata));
        } catch (Exception e) {
            // 업로드 중 예외 발생 시 RuntimeException 발생
            throw new RuntimeException(e.getMessage());
        }

        // 업로드된 파일의 CloudFront URL 반환
        return "https://" + cloudFrontDomain + "/" + fileName;
    }

    /**
     * 바이트 배열을 S3에 업로드
     * @param fileName 업로드할 파일의 이름
     * @param content 업로드할 파일 내용 (바이트 배열)
     * @param contentType 파일의 콘텐츠 타입
     * @return 업로드된 파일의 CloudFront URL
     */
    public String uploadFile(String fileName, byte[] content, String contentType) {
        // 바이트 배열을 입력 스트림으로 변환
        InputStream inputStream = new ByteArrayInputStream(content);

        // 파일 메타데이터 설정
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(content.length);
        metadata.setContentType(contentType);

        // S3에 파일 업로드
        amazonS3.putObject(new PutObjectRequest(bucketName, fileName, inputStream, metadata));

        // 업로드된 파일의 CloudFront URL 반환
        return "https://" + cloudFrontDomain + "/" + fileName;
    }
}

 

공유 HTML 페이지 생성 및 업로드

사용자가 AI음악 2개 중 최종 선택한 음악을 72시간동안 공유할 수 있는 url을 S3를 통해 생성하는 부분이다.

  1. 공유 HTML 페이지 생성 및 업로드
    • S3에서 생성한 Pre-Signed URL을 사용하여 HTML 페이지를 생성
    • 생성된 HTML 페이지를 S3에 업로드하고 해당 URL을 반환
  2. 공유 URL 저장 및 반환
    • 공유 URL을 생성하고 유효기간(3일)을 설정한 후 데이터베이스에 저장
    • 최종적으로 생성된 공유 URL을 포함한 응답 객체를 반환
    @Transactional
    public MusicSelectionResponseDto selectMusic(Long diaryId, Long musicId) {
        // 일기 엔티티 조회
        ...
        // 선택된 음악 엔티티를 조회
        ...
        // 이미 선택된 음악인지 확인 (이미 선택된 음악일 경우 fileUrl이 https://cdn1.suno.ai/~ -> https://storage.googleapis.com/~ 로 변경되어있음)
        ...

        // 선택된 음악을 제외한 나머지 음악 목록을 조회
        ...
        // 나머지 음악들을 삭제
        ...
        try {
            // 원격 파일을 다운로드
            File tempFile = FileUtil.downloadFile(selectedMusic.getFileUrl());

            // 선택된 음악을 Firebase Storage와 AWS S3에 저장
            String firebaseUrl = firebaseService.uploadFileToFirebaseStorage(tempFile, diary.getId().toString());
            String s3Url = s3Service.uploadFile("music/diaryId_" + diary.getId() + ".mp3", tempFile);

            // 임시 파일 삭제
            tempFile.delete();

            // 선택된 음악의 URL을 업데이트
            selectedMusic.setFileUrl(firebaseUrl);
            musicRepository.save(selectedMusic);

            String thumbnailUrl = "https://via.placeholder.com/300";  // 예시 썸네일 URL
            String htmlUrl = generateMusicSharePage(s3Url, thumbnailUrl);

            // 공유 URL 생성 및 저장
            ShareUrl shareUrl = new ShareUrl();
            shareUrl.setMusic(selectedMusic);
            shareUrl.setUrl(s3Url);
            shareUrl.setExpirationDate(LocalDateTime.now().plusDays(3)); // 유효 기간 3일 설정
            shareUrlRepository.save(shareUrl);

            return MusicSelectionResponseDto.builder().shareUrl(htmlUrl).build();
        } catch (IOException e) {
            throw new RuntimeException("파일 업로드 중 오류가 발생했습니다.", e);
        }
    }

 

아래 generateMusicSharePage 메서드는 음악 파일의 Pre-Signed URL을 사용하여 HTML 페이지를 생성하고, S3에 업로드하는 부분이다.

  1. HTML 페이지 생성
    • 제공된 Pre-Signed URL과 썸네일 이미지를 사용하여 HTML 페이지 콘텐츠를 생성
    • HTML내 오디오 플레이어를 포함하여 사용자가 음악을 재생할 수 있도록 함 (추후 앱스토어 링크, 음악 다운로드 버튼 추가 예정)
  2. HTML 페이지 업로드
    • 생성된 HTML 페이지를 S3에 업로드
    • 업로드된 HTML 페이지의 S3 URL을 반환
    /**
     * 음악 파일의 Presigned URL을 사용하여 HTML 페이지를 생성하고, S3에 업로드
     * @param presignedUrl 음악 파일의 Presigned URL
     * @return HTML 페이지의 Presigned URL
     */
    private String generateMusicSharePage(String presignedUrl, String thumbnailUrl) {
        String htmlContent = "<!DOCTYPE html>\n"
                + "<html lang=\"en\">\n"
                + "<head>\n"
                + "    <meta charset=\"UTF-8\">\n"
                + "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
                + "    <title>Daytune</title>\n"
                + "    <meta property=\"og:image\" content=\"" + thumbnailUrl + "\" />\n"
                + "    <meta property=\"og:title\" content=\"Daytune\" />\n"
                + "    <meta property=\"og:description\" content=\"오늘의 감정을 음악으로\" />\n"
                + "</head>\n"
                + "<body>\n"
                + "    <audio controls>\n"
                + "        <source src=\"" + presignedUrl + "\" type=\"audio/mpeg\">\n"
                + "        Your browser does not support the audio element.\n"
                + "    </audio>\n"
                + "</body>\n"
                + "</html>";

        // S3에 HTML 페이지 업로드
        String pageFileName = "pages/" + UUID.randomUUID().toString() + ".html";
        return s3Service.uploadFile(pageFileName, htmlContent.getBytes(StandardCharsets.UTF_8), "text/html");
    }

 

결과물

 

참고

Cloud Front와 S3 버킷, Rout53 설정이 필요하다.

 

[Spring Boot & AWS] AWS S3 bucket을 활용한 이미지 업로드

프로젝트를 진행하면서 부가적인 회원 서비스를 깔끔하게 마무리짓기 위해 프로필 사진 업로드를 진행하고자 하였습니다.로컬 환경의 상대 경로를 활용하는 것은 이전에 시도해보아서 익숙했

velog.io

 

 

aws S3, CloudFront, Route53 연동하기

자체 개발한 플랫폼을 실제 도메인과 서버와 연결하기 위해 S3를 처음 사용해 보았다. 대략적 흐름은 이렇다. - 로컬에서 파일 build (npm build / yarn build) - 생성된 build 폴더 안의 파일들을 S3에 업로

whoyoung90.tistory.com

 

 

마치며

AWS S3를 활용하여 설정한 시간동안만 유효한 URL을 생성하는 과정을 설명했다.

Pre-Signed URL을 사용하면, S3 버킷에 저장된 파일에 대한 접근 권한을 시간 설정과 함께 부여할 수 있다는 점이 매력적이었다.

물론 공유페이지 자체는 리액트와 같은 프론트엔드 프레임워크를 사용한게 아니고, 백단에서 한땀한땀 html 코드를 한줄씩 쳐서 구현하는 방식이라 유지보수, 확장성 측면에서는 많이 부족한 부분이 있다. 하지만 단순 공유 목적의 페이지를 사용자에게 제공하는 것이었기 때문에 공수가 가장 덜 드는 방법이었다.