Cloud

Spring Boot에서 Google Cloud Storage(GCS) 다루기

Tae4an 2024. 11. 8. 13:24
반응형

이번 포스팅에서는 Spring Boot 애플리케이션에서 Google Cloud Storage(GCS)를 사용하여 파일을 업로드, 다운로드, 삭제하는 방법을 알아보겠습니다. GCS는 Google Cloud Platform에서 제공하는 확장성 있는 객체 스토리지 서비스입니다.


 

먼저 아래의 구글 클라우드 콘솔에 들어가서 프로젝트를 생성하면

 

https://cloud.google.com/cloud-console

 

https://cloud.google.com/cloud-console

 

cloud.google.com

 

 

아래의 사진 처럼 90일동안 무료 크레딧을 사용가능합니다. 저희는 이 크레딧을 사용할 겁니다.

아 그리고 일반 계정 활성화는 무료 크레딧을 전부 사용하면 이후에 사용한 만큼 비용을 지불하는 방식이라 따로 설정은 아하셔도 됩니다.(90일 또는 크레딧 전부 다 사용 후 이어서 계속 사용하시고 싶은 분들만)

그리고 좌측 상단의 메뉴바를 클릭해서 클라우드 스토리지를 클릭 후 버킷으로 이동합니다.

버킷을 눌러서 들어오시면 아래의 화면이 뜨는데 버킷 만들기를 선택해주시면 됩니다.

아래 화면에서 이름 정해주시고, 데이터 저장 위치는 Region 서울 선택하시면 됩니다. 데이터 스토리지 클래스는 Standard, 그리고 아래의 화면의 파란색 표시된 곳의 체크를 해제합니다. 그리고 이후엔 계속을 눌러서 버킷을 만들어주시면 됩니다.

아래와 같이 생성된 것이 확인이 됩니다.

그리고 다시 좌측 상단의 메뉴에서  IAM 및 관리자의 서비스 계정을 선택.

 

서비스 계정에 들어오면 현재 화면이 뜨는데, 아래는 예전에 생성해둔 서비스 계정이고, 새로 만들어줄 것이다. 파란색으로 표시된 서비스 계정 추가 클릭.

아래의 세 가지 역할을 추가.

 

이제 완료해서 생성하면 아래 새로운 서비스 계정이 생성된 것이 보일 것이다. 클릭하여 들어가줍니다.

클릭하여 서비스 계정에 들어와서 키를 선택 후 새 키 만들기를 선택. JSON으로 만들면 됩니다.

그러면 이제 키가 생성이 되고 자동으로 다운로드가 된다.

이 파일은 나중에 resources 디렉토리에 보관해야 하니 파일 경로를 기억해두록 합시다.

 

환경 설정 ⚙️

build.gradle 의존성 추가

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'

 

버킷 이름은 시작할 때 생성했던 이름, 프로젝트 ID는 클라우드 콘솔에서 확인하거나 다운로드 받은 제이슨 키 파일 안에서 확인 가능합니다.

application.yml 설정

spring
   cloud:
     gcp:
       storage:
         credentials:
             location: classpath:[키 이름].json
         project-id: [프로젝트 ID]
         bucket: [버킷 이름]

프로젝트 구조 📁

src/main/java/com/office/
├── controller
│   └── GCSController.java
├── service
│   └── GCSService.java
├── dto
│   ├── GCSRequest.java
│   └── GCSResponse.java
└── exception
    └── GCSException.java
└── resources
    └── [키 파일 붙여넣기]

구현하기 💻

예외 처리

먼저 GCS 관련 작업 중 발생할 수 있는 예외를 처리하기 위한 커스텀 예외 클래스를 만듭니다.

public class GCSException extends RuntimeException {
    public GCSException(String message) {
        super(message);
    }

    public GCSException(String message, Throwable cause) {
        super(message, cause);
    }
}

Controller 구현 🎮

REST API 엔드포인트를 제공하는 컨트롤러를 구현합니다.

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/gcs")
public class GCSController {
    private final GCSService gcsService;

    @PostMapping("/upload")
    public ResponseEntity<GCSResponse> uploadObject(@ModelAttribute GCSRequest gcsRequest) throws GCSException {
        return ResponseEntity.ok(gcsService.uploadObject(gcsRequest));
    }

    @GetMapping("/download/{fileName}")
    public ResponseEntity<GCSResponse> downloadObject(@PathVariable String fileName) throws GCSException {
        return ResponseEntity.ok(gcsService.downloadObject(fileName));
    }

    @DeleteMapping("/{fileName}")
    public ResponseEntity<Void> deleteObject(@PathVariable String fileName) throws GCSException {
        gcsService.deleteObject(fileName);
        return ResponseEntity.ok().build();
    }

    @GetMapping("/list")
    public ResponseEntity<List<GCSResponse>> listObjects() throws GCSException {
        return ResponseEntity.ok(gcsService.listObjects());
    }

    @ExceptionHandler(GCSException.class)
    public ResponseEntity<String> handleGCSException(GCSException e) {
        log.error("GCS operation failed", e);
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

Service 구현 ⚡

GCS와의 실제 상호작용을 담당하는 서비스 클래스입니다.

@Slf4j
@Service
public class GCSService {
    @Value("${spring.cloud.gcp.storage.bucket}")
    private String bucketName;

    @Value("${spring.cloud.gcp.storage.credentials.location}")
    private String keyFileName;

    private Storage storage;

    @PostConstruct
    public void init() throws GCSException {
        try {
            InputStream keyFile = ResourceUtils.getURL(keyFileName).openStream();
            storage = StorageOptions.newBuilder()
                    .setCredentials(GoogleCredentials.fromStream(keyFile))
                    .build()
                    .getService();
            log.info("GCS Service initialized successfully");
        } catch (IOException e) {
            log.error("Failed to initialize GCS Service", e);
            throw new GCSException("Failed to initialize GCS service", e);
        }
    }

    // 파일 업로드 메서드
    public GCSResponse uploadObject(GCSRequest request) throws GCSException {
        validateUploadRequest(request);
        try {
            MultipartFile file = request.getFile();
            String fileName = generateUniqueFileName(file.getOriginalFilename());
            BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, fileName)
                    .setContentType(file.getContentType())
                    .build();
            Blob blob = storage.create(blobInfo, file.getInputStream());
            return createGCSResponse(blob);
        } catch (IOException e) {
            throw new GCSException("Failed to upload file", e);
        }
    }

    // 기타 메서드 구현...
}

DTO 구현 📦

데이터 전송을 위한 DTO 클래스들입니다.

@Data
public class GCSRequest {
    private String name;
    private MultipartFile file;
}

@Data
@Builder
public class GCSResponse {
    private String fileName;
    private String downloadUrl;
    private Long fileSize;
    private String contentType;
    private String uploadTime;
}

주요 기능

  1. 파일 업로드 📤
    • 유니크한 파일명 생성
    • 파일 메타데이터 설정
    • 업로드 결과 반환
  2. 파일 다운로드 📥
    • 파일명으로 객체 조회
    • 다운로드 URL 제공
  3. 파일 삭제 🗑️
    • 지정된 파일 삭제
    • 삭제 결과 확인
  4. 파일 목록 조회 📋
    • 버킷 내 모든 파일 목록 조회
    • 파일 메타데이터 포함

API 테스트하기

Postman을 사용하여 구현된 API를 테스트해보겠습니다.

1. 파일 업로드 테스트 📤

Request:

  • Method: POST
  • URL: http://localhost:8080/api/gcs/upload
  • Headers:
    • Content-Type: multipart/form-data
  • Body (form-data):
    • Key: file (Type: File)
    • Key: name (Type: Text)

 

POST /api/gcs/upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="test.pdf"
Content-Type: application/pdf

[파일 내용]
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="name"

test-document
------WebKitFormBoundary7MA4YWxkTrZu0gW--

 

Response Example:

{
    "fileName": "6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
    "downloadUrl": "https://storage.googleapis.com/[bucket-name]/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
    "fileSize": 15234,
    "contentType": "application/pdf",
    "uploadTime": "2024-01-20T09:30:15.123Z"
}

2. 파일 목록 조회 테스트 📋

Request:

  • Method: GET
  • URL: http://localhost:8080/api/gcs/list
GET /api/gcs/list HTTP/1.1
Host: localhost:8080

Response Example:

[
    {
        "fileName": "6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
        "downloadUrl": "https://storage.googleapis.com/[bucket-name]/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
        "fileSize": 15234,
        "contentType": "application/pdf",
        "uploadTime": "2024-01-20T09:30:15.123Z"
    },
    {
        "fileName": "7d2b5e9f-8g4d-5c3b-0d1e-8e0c0f234567.jpg",
        "downloadUrl": "https://storage.googleapis.com/[bucket-name]/7d2b5e9f-8g4d-5c3b-0d1e-8e0c0f234567.jpg",
        "fileSize": 45678,
        "contentType": "image/jpeg",
        "uploadTime": "2024-01-20T10:15:30.456Z"
    }
]

3. 파일 다운로드 테스트 📥

Request:

  • Method: GET
  • URL: http://localhost:8080/api/gcs/download/{fileName}
GET /api/gcs/download/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf HTTP/1.1
Host: localhost:8080

Response Example:

{
    "fileName": "6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
    "downloadUrl": "https://storage.googleapis.com/[bucket-name]/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
    "fileSize": 15234,
    "contentType": "application/pdf",
    "uploadTime": "2024-01-20T09:30:15.123Z"
}

4. 파일 삭제 테스트 🗑️

Request:

  • Method: DELETE
  • URL: http://localhost:8080/api/gcs/delete/{fileName}
DELETE /api/gcs/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf HTTP/1.1
Host: localhost:8080

Response:

  • Status: 200 OK
  • Body: Empty

에러 응답 예시 ⚠️

파일이 존재하지 않는 경우:

{
    "error": "File not found: non-existent-file.pdf",
    "status": 400
}

파일 업로드 실패 시:

{
    "error": "Failed to upload file: Invalid file format",
    "status": 400
}

Postman 환경 설정 팁 💡

  1. 환경 변수 설정
  2. baseUrl: http://localhost:8080 bucketName: your-bucket-name
  3. Collection 변수 설정
    • API 경로: /api/gcs
    • 공통 헤더 설정
  4. 테스트 스크립트 예시
  5. // 파일 업로드 후 테스트 pm.test("Upload successful", function () { phttp://m.response.to.have.status(200); phttp://m.response.to.have.jsonSchema({ required: ['fileName', 'downloadUrl', 'fileSize'] }); });

주의사항 🚨

  1. 파일 업로드 시 최대 파일 크기 제한을 확인하세요.
  2. multipart/form-data 형식으로 전송해야 합니다.
  3. 파일명에 특수문자가 포함된 경우 URL 인코딩이 필요할 수 있습니다.
  4. 큰 파일 업로드 시 타임아웃 설정을 적절히 조정하세요.

마치며

이렇게 Spring Boot에서 Google Cloud Storage를 사용하는 방법을 알아보았습니다. 위 코드는 기본적인 파일 관리 기능을 구현한 것으로, 실제 프로젝트에서는 보안, 성능, 에러 처리 등을 고려하여 추가적인 구현이 필요할 수 있습니다.

반응형