1. gradle 의존성 추가

//gcp cloud storage
implementation group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter', version: '1.2.5.RELEASE'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-gcp-storage', version: '1.2.5.RELEASE'
2. yaml에 storage 정보 등록
keyfile을 프로젝트 resources 안에 넣어 놓고 yaml에 정보를 등록한다.
# gcp storage 설정
spring:
cloud:
gcp:
storage:
bucket: {파일용 버킷이름}
image-bucket: {이미지용 버킷이름}
credentials:
location: classpath:{keyfile이름}.json
project-id: {key파일에 있는 프로젝트 id} (프로젝트 이름 뒤에 숫자로 구성돼있다)
3. GCPStorageHandler 클래스 생성
편의를 위해 utill 폴더에 클래스를 생성했다.
우리 사이트에서 이미지 관련 작업은 3개로 구성되어있다.
1. 파일을 업로드(여러 파일을 한번에 올릴수 있다. 기능상으론 GCS로부터 따로 리턴받을건 없고 성공, 실패정도만 알면됨)
2. 게시글 사이사이의 이미지 (한번에 이미지 1건만 받을수있다 리턴받을때 media링크를 받아야함. 확장자가 이미지가 아니면 에러남)
3. 파일 다운로드 (프론트에서 blob 또는 url을 받아서 a링크에 셋팅하면 되는듯?)
GCPStorageHandler.class
@Service
@RequiredArgsConstructor
public class GCPStorageHandler {
@Value("${spring.cloud.gcp.storage.bucket}")
private String bucketName;
@Value("${spring.cloud.gcp.storage.credentials.location}")
private String keyFileName;
@Value("${spring.cloud.gcp.storage.project-id}")
private String projectId;
//GCS에 파일 업로드
public Blob makeBlob(String category, MultipartFile multipartFile) throws IOException {
String uuid = UUID.randomUUID().toString();
InputStream keyFile = ResourceUtils.getURL(keyFileName).openStream();
Storage storage = StorageOptions.newBuilder()
.setCredentials(GoogleCredentials.fromStream(keyFile))
.build()
.getService();
String filePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
String blobName = category + "/" + filePath + "/" + uuid + "_" + multipartFile.getOriginalFilename();
BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, blobName)
.setContentType(multipartFile.getContentType()).build();
return storage.create(blobInfo, multipartFile.getInputStream());
}
//파일 업로드 (다건 가능)
public List<FileResDto> uploadToStorage(FileReqDto fileReqDto) throws IOException {
List<FileResDto> fileResDtos = new ArrayList<>();
for(MultipartFile multipartFile : fileReqDto.getFileList()){
Blob blob = makeBlob(fileReqDto.getCategory(), multipartFile);
FileResDto fileResDto = FileResDto.builder()
.realFileName(multipartFile.getOriginalFilename())
.filePath(blob.getName())
.contentType(blob.getContentType())
.mediaLink(blob.getMediaLink())
.build();
fileResDtos.add(fileResDto);
}
return fileResDtos;
}
//이미지 업로드 (단건만 가능)
public FileResDto uploadImageToStorage(String category, MultipartFile multipartFile) throws IOException {
String contentType = multipartFile.getContentType();
if(contentType == null || !contentType.startsWith("image")) {
throw new CustomException("이미지 파일이 아닙니다.");
}
Blob blob = makeBlob(category, multipartFile);
FileResDto fileResDto = FileResDto.builder()
.realFileName(multipartFile.getOriginalFilename())
.filePath(blob.getName())
.contentType(blob.getContentType())
.mediaLink(blob.getMediaLink())
.build();
return fileResDto;
}
public Blob downloadFromStorage(String objectName) throws IOException {
InputStream keyFile = ResourceUtils.getURL(keyFileName).openStream();
Storage storage = StorageOptions.newBuilder()
.setCredentials(GoogleCredentials.fromStream(keyFile))
.build()
.getService();
Blob blob = storage.get(bucketName, objectName);
return blob;
}
FileReqDto.class
@AllArgsConstructor
@Getter
@Builder
public class FileReqDto {
private List<MultipartFile> fileList;
private String category;
}
1번과 2번의 gcs에 올리는 작업이 중복되어서 makeBlob이라는 메소드를 공통으로 빼주었다.
category는 폴더 이름이다.
Storage를 @Autowired로 주입받아서 사용하는 블로그가 많았는데 계속 찾을수 없다는 오류가 나고 의존성을 다른걸로 바꿔보아도 계속 안돼서 조금 고생을 했다. 확실히 aws보다 공식사이트 문서, 블로그 등 정보가 부족한것 같다.
InputStream keyFile = ResourceUtils.getURL(keyFileName).openStream();
Storage storage = StorageOptions.newBuilder()
.setCredentials(GoogleCredentials.fromStream(keyFile))
.build()
.getService();
이런식으로 Storage 객체를 생성해 주었다.
4. 컨트롤러 생성
아직 기능과 연결을 안해서 테스트용 컨트롤러를 만들었다. 파일을 받는 컨트롤러 설정은 항상 잊게되는거같다ㅎㅎ
서비스는 특별한 내용없고 GCPStorageHandler와 연결해주는 작업만 한다.
// 파일 다건 업로드
@PostMapping(value ="/gcs/upload/{category}", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<?> uploadToStorage(@PathVariable("category") String category,
@Parameter(
description = "multipart/form-data 형식의 파일 리스트를 받습니다. 이때 key 값은 fileList 입니다.",
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
)
@RequestPart("multipartFiles") List<MultipartFile> fileList) throws IOException {
return responseDto.success(gcpStorageService.uploadToStorage(category, fileList));
}
// 파일 단건 업로드
@PostMapping(value ="/gcs/upload/image/{category}", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<?> uploadImageToStorage(@PathVariable("category") String category, @RequestPart("multipartFile") MultipartFile multipartFile) throws IOException {
return responseDto.success(gcpStorageService.uploadImageToStorage(category, multipartFile));
// 파일 다운로드
@GetMapping(value ="/gcs/download")
public ResponseEntity<ByteArrayResource> downloadToStorage(@Parameter(name = "파일경로+이름", description="파일경로+이름") String objectName) throws IOException {
Blob blob = gcpStorageService.downloadFromStorage(objectName);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.contentLength(blob.getContent().length)
.body(new ByteArrayResource(
blob.getContent()));
}

swagger에서 다량 업로드를 테스트하면 아래와 같이 결과를 확인 할 수 있다.
이후 실제 로직에 적용할 때에는 이 결과값을 리턴받아서 DB에 넣어주는 작업도 해야한다.
{
"completed": true,
"message": "정상적으로 조회 되었습니다.",
"data": [
{
"realFileName": "테스트파일1.xls",
"filePath": "Request/202401/2b179508-c474-4072-b7f0-804304b0919c_테스트파일1.xls",
"contentType": "application/vnd.ms-excel",
"mediaLink": "https://storage.googleapis.com/download/storage/v1/b/bkt/o/Request%2F202401%2F2b179508-c474-4072-b7f0-804304b0919c_%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%8C%8C%EC%9D%BC1.xls?generation=1705983228087886&alt=media"
},
{
"realFileName": "테스트파일2.xls",
"filePath": "Request/202401/a6e0f5de-f9ac-446b-9d06-060b42ec5b23_테스트파일2.xls",
"contentType": "application/vnd.ms-excel",
"mediaLink": "https://storage.googleapis.com/download/storage/v1/b/bkt/o/Request%2F202401%2Fa6e0f5de-f9ac-446b-9d06-060b42ec5b23_%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%8C%8C%EC%9D%BC2.xls?generation=1705983231132903&alt=media"
},
{
"realFileName": "테스트파일3.xls",
"filePath": "Request/202401/44a36898-66d7-4eee-b04f-59a470c7b335_테스트파일3.xls",
"contentType": "application/vnd.ms-excel",
"mediaLink": "https://storage.googleapis.com/download/storage/v1/b/bkt/o/Request%2F202401%2F44a36898-66d7-4eee-b04f-59a470c7b335_%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%8C%8C%EC%9D%BC3.xls?generation=1705983233019162&alt=media"
}
]
}
validation을 추가하고 결과값등을 좀 더 바꿔야할 부분이 있을 것 같다.
'DevOps' 카테고리의 다른 글
| [GCP] 스프링부트 + GCP Storage 연동하기 / Cloud CDN란? - 1 (1) | 2024.01.23 |
|---|---|
| AWS Free tier 스펙으로 바꿨는데도 계속 과금된다면? =>"다음달까지 기다려보자" (0) | 2023.02.01 |
| [az-900] Azure Fundamentals 불합격 할뻔한 후기(examtopics만 공부하면 안되는 이유), 덤프 추천, 시험보기전준비사항 (0) | 2022.02.20 |
| [Exam AZ-900] 할인적용 중 오류가 발생할 때 해결법 (0) | 2022.02.07 |
| [MSsql] Azure SQL db timezone 시간이 UTC로 나올때 해결 (0) | 2021.12.09 |