첨부파일들을 관리하는데 모든 역할을 서버가 담당한다면, 서버의 부하가 엄청 크게 발생할것이라고 봅니다.
그래서 Signed URL
방식을 통해 보안성을 유지하며 서버에 대한 부하를 줄이고자 합니다.
해당글은 Google Cloud Platform을 기준으로 작성되었음을 알려드립니다.
대략적인 순서는 이렇습니다
GCP 접속
IAM 및 관리자 접속
서비스 계정 생정
서비스계정 키 만들기
Cloud Storage에서 버킷을 생성해줍니다.
appilication.yml 설정
spring:
application:
name: file-api
cloud:
gcp:
credentials:
location: classpath:gcs-key.json # gcs-key.json 파일 경로
project-id: [프로젝트 명]
bucket: gcs-test-bucket123 // 버킷 이름
server:
port: 8888
build.gradle 작성
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudGcpVersion', "6.0.0")
set('springCloudVersion', "2024.0.0")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.google.cloud:spring-cloud-gcp-starter-storage'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
dependencyManagement {
imports {
mavenBom "com.google.cloud:spring-cloud-gcp-dependencies:${springCloudGcpVersion}"
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
gcs-key.json 파일 예시
{
"type": "service_account",
"project_id": "",
"private_key_id": "",
"private_key": "-----BEGIN PRIVATE KEY-----\n",
"client_email": "",
"client_id": "",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "",
"universe_domain": "googleapis.com
}
GcpProperties.java - properties 받을 클래스 정의
package com.example.fileapi.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "spring.cloud.gcp") // 👈 YAML의 해당 부분 매핑
public class GcpProperties {
private Credentials credentials;
private String projectId;
private String bucket;
@Getter
@Setter
public static class Credentials {
private String location;
}
}
GcpConfig.java - Cloud Storage 설정 클래스
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ResourceUtils;
import java.io.IOException;
import java.io.InputStream;
@Configuration
@RequiredArgsConstructor
public class GcpConfig {
private final GcpProperties gcpProperties;
@Bean
public Storage googleCloudStorage() throws IOException {
InputStream keyFile = ResourceUtils.getURL(gcpProperties.getCredentials().getLocation())
.openStream(); // keyfile
return StorageOptions.newBuilder()
.setProjectId(gcpProperties.getProjectId())
.setCredentials(GoogleCredentials.fromStream(keyFile))
.build()
.getService();
}
}
Controller
package com.example.fileapi.controller;
import com.example.fileapi.dto.SignedUrlRequest;
import com.example.fileapi.service.SignedUrlService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/files")
@RequiredArgsConstructor
public class FileController {
private final SignedUrlService signedUrlService;
@PostMapping("/signed-url/upload")
public ResponseEntity<?> generateUpLoadSignedUrl(@RequestBody SignedUrlRequest signedUrlRequest) {
return ResponseEntity.ok(signedUrlService.generateUploadSignedUrl( signedUrlRequest));
}
@PostMapping("/signed-url/download")
public ResponseEntity<?> generateDownLoadSignedUrl(@RequestBody SignedUrlRequest signedUrlRequest) {
return ResponseEntity.ok(signedUrlService.generateDownloadSignedUrl( signedUrlRequest));
}
}
SignedUrlRequest.java - 서명된 URL을 받을 Request DTO
public record SignedUrlRequest(String filePath) {
}
SignedUrlService.java - 실제 동작하는 서비스
package com.example.fileapi.service;
import com.example.fileapi.config.GcpProperties;
import com.example.fileapi.dto.SignedUrlRequest;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.HttpMethod;
import com.google.cloud.storage.Storage;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
public class SignedUrlService {
private final Storage storage;
private final GcpProperties gcpProperties;
/**
* 📌 업로드용 Signed URL 생성 (PUT 요청)
* @param signedUrlRequest 파일 경로
* @return Signed URL (5분간 유효)
*/
public String generateUploadSignedUrl(SignedUrlRequest signedUrlRequest) {
try {
BlobInfo blobInfo = BlobInfo.newBuilder(gcpProperties.getBucket(), signedUrlRequest.filePath()).build();
URL signedUrl = storage.signUrl(
blobInfo,
5,
TimeUnit.MINUTES,
Storage.SignUrlOption.httpMethod(HttpMethod.PUT), // upload는 PUT or POST
Storage.SignUrlOption.withV4Signature() // V4 서명 방식 사용,
);
return signedUrl.toString();
} catch (Exception e) {
throw new RuntimeException("서명된 URL 생성 실패", e);
}
}
/**
* 📌 다운로드용 Signed URL 생성 (GET 요청)
* @param signedUrlRequest 파일 경로
* @return Signed URL (5분간 유효, 브라우저에서 다운로드 가능)
*/
public String generateDownloadSignedUrl(SignedUrlRequest signedUrlRequest) {
try {
BlobInfo blobInfo = BlobInfo.newBuilder(gcpProperties.getBucket(), signedUrlRequest.filePath()).build();
URL signedUrl = storage.signUrl(
blobInfo,
5,
TimeUnit.MINUTES,
Storage.SignUrlOption.httpMethod(HttpMethod.GET), // download는 GET
Storage.SignUrlOption.withV4Signature(), // V4 서명 방식 사용,
Storage.SignUrlOption.withQueryParams(Map.of("response-content-disposition", "attachment")) // 브라우저 접속시 다운로드
);
return signedUrl.toString();
} catch (Exception e) {
throw new RuntimeException("서명된 URL 생성 실패", e);
}
}
}
API 요청-업로드용 URL
업로드
업로드 확인
다운로드 URL 요청
다운로드
브라우저에서 URL 입력
글 올려주세요 기다리는중...