스프링

스프링 - GCS 이미지 저장/덮어쓰기/삭제

infobox503 2025. 2. 3. 18:07

<요약>

  • 목적
    • GCS로 이미지 저장, 삭제, 수정 구현
  • 설명
    • GCS는 Storage 객체를 통해 이미지 저장소(bucket)에 접근 가능
    • Storage를 통해 이미지 저장 및 삭제 가능(수정은 불가)
  • 보완 계획
    • GCS 이미지 등록을 비동기적으로 수행
      • Storage 객체를 비동기적 또는 멀티 스레드 방식으로 해결 예정
      • (현재는 ADMIN 관리자만 상품 등록이 가능하므로, GCS를 스프링 빈으로 선언해서 싱글톤 객체로 쓰고 있음)

 

QnA)

1. GCS 용어

  • 용어

https://techblog-history-younghunjo1.tistory.com/27

  • Bucket
    • 데이터 저장소
    • Bucket안에 Bucket이 있을 수 없음
    • 각 Bucket 안에 각 데이터(이미지)를 저장 가능
  • Object
    • Bucket 안에 있는 각 데이터
    • 구성 요소
      • 객체 데이터
        • 실제 데이터 내용
        • ex) 이미지
      • 객체 메타 데이터
        • 객체 데이터 부연 설명
        • key-value 형태로 객체 데이터를 설명함
        • ex)
{
  "name": "images/cat.jpg",
  "contentType": "image/jpeg",
	 ... 
 }

 

 

 

2. GCS 업로드 권한

  • 요약
    • 목적
      • GCS에 업로드한 이미지를 인터넷의 모든 사람들이 볼 수 있도록 한다.
    • 방법
      • GCS 버킷 사용 시, 접근이 가능한 사용자를 지정할 수 있다.
  • 적용
    • GCS → 버킷 → 권한 → 액세스 권한 부여로 이동
    • 해당 액세스 권한 부여에서 구성원 및 역할 지정이 가능하다
    • 구성원
    • 역할 지정
      • https://cloud.google.com/iam/docs/understanding-roles
      • 버킷의 접근 권한을 지정한다.
      • 본인은 “저장소 개체 뷰어”를 선택했다.
        • 저장소 개체 뷰어
          • 객체, 객체 메타데이터 읽기 가능
          • ⇒ 이미지 읽기 가능
          <설명에 나온 글>
        • 저장소 개체 뷰어 ACL을 제외한 객체와 객체의 메타데이터를 볼 수 있는 액세스 권한을 부여합니다. 버킷의 객체를 나열할 수도 있습니다

 

3. Bucket 사용 권한 획득

https://choo.oopy.io/35bffd94-7a41-4cfa-812c-b8aaf148604a

  • 목적
    • 스프링으로 Bucket에 이미지를 등록, 수정, 삭제하는 것
      • 코드 상에서 Bucket을 수정할 수 있는 권한을 획득한다.
  • 방법
    1. 액세스 권한 획득
      • 원격으로 Bucket에 접속하기 위해, 접속 권한을 획득한다.
      • IAM 및 관리자 → 서비스 계정 → 서비스 계정 만들기

 

  • 접속 권한 등록
    • 사용 권한
      • 저장소 개체 관리자
        • Bucket 안에 있는 각 객체에 대한 CRUD 권한 획득
      • 저장소 관리자
        • Bucket 제어 권한 획득(=Bucket에 접속 권한 획득)

 

  • 키 획득
    • 위에서 설정한 권한을 가진 키를 획득
    • (본인은 JSON으로 획득)
     

 

 

4. Spring에서 Bucket 수정

  • 목적
    • Spring에서 이미지 업로드
  1. gradle.yml 추가
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. 사용

  • 절차
    1. Storage 획득
      • Storage : GCS 전체를 관할하는 객체 → 즉, 각 Bucket에 접속 가능
      • Storage를 사용하기 위해서는 2가지 입력 필요
        1. GCS 액세스 권한(위에서 가져온 JSON 키 활용)
        2. project-id
          • 구글 클라우드에서 현재 사용하고 있는 프로젝트 id
          • (1. GCS 액세스 권한 파일에 project-id 란으로 확인 가능)
        • ⇒ 해당 구글 클라우드 프로젝트에 GCS 사용 권한을 획득한 Storage 객체를 사용가능한 것
//액세스 권한 키가 있는 절대 위치
String location = C:\Windows\System32\MyProject\sportsshop-44.json;
String projectId = google-cloud-project-id

GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(location));

Storage storage = StorageOptions.newBuilder()
        .setCredentials(credentials)
        .setProjectId(projectId)
        .build()
        .getService();

 

2. Storage 사용

  • bucketName : 사용할 Bucket 이름
  • BlobId : 버킷의 객체 id
  • BlobInfo : BlobId 객체의 메타 데이터
String bucketName = google-cloud-gcs-bucket-name;

//1번에서 언급된 대로 storage 생성
Storage storage = ...;

//저장할 이미지 파일
MultipartFile image

//버킷에 저장할 이미지 이름
String imageName = bucket-object-name;
String ext = image.getContentType(); // 파일의 형식 ex) JPG

// 객체 id 생성(= imageName)
BlobId blobId = BlobId.of(bucketName, imageName);
// 객체 메타 데이터
BlobInfo blobInfo = BlobInfo.newBuilder(blobId)
						       .setContentType(ext)
							     .build();

//객체 쓰기 객체
WriteChannel writer = storage.writer(blobInfo)
//image 쓰기
byte[] imageData = image.getBytes();
writer.write(ByteBuffer.wrap(imageData));

//=> bucketName에 image 쓰기 완료

 

 

5. 이미지 업로드 예시

https://velog.io/@julia2039/SpringBoot-프로젝트에서-GCS로-이미지-업로드-Google-Cloud-StorageGCP

  • 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'

 

 

yml

spring:
  cloud:
    gcp:
      storage:
        credentials:
          location: C:\Windows\System32\MyProject\~~~~.json
        project-id: ~~~~
        bucket: ~~~~

 

 

  • 저장
    • 사진은 MultipartFile 타입
    • blocking 방식으로 이미지 업로드가 진행됨
      • 1번에 1번의 요청 처리 및 그 외의 요청은 대기 상태에 빠짐
public class ImageUpLoad {

    @Value("${spring.cloud.gcp.storage.bucket}") // application.yml에 써둔 bucket 이름
    private String bucketName;

    @Value("${spring.cloud.gcp.storage.project-id}")
    String projectId;

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


    // 회원 정보 수정
    public void updateMemberInfo(MultipartFile file) throws IOException {
        GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(location));

        Storage storage = StorageOptions.newBuilder()
                .setCredentials(credentials)
                .setProjectId(projectId)
                .build()
                .getService();


        String uuid = UUID.randomUUID().toString(); // Google Cloud Storage 에 저장될 파일 이름
        String ext = file.getContentType(); // 파일의 형식 ex) JPG

        // Cloud에 이미지 업로드
        BlobId blobId = BlobId.of(bucketName, uuid);
        BlobInfo blobInfo = BlobInfo.newBuilder(blobId)
                .setContentType(ext)
                .build();
        try (WriteChannel writer = storage.writer(blobInfo)) {
            byte[] imageData = file.getBytes();
            writer.write(ByteBuffer.wrap(imageData));
        } catch (Exception ex) {
            // 예외 처리 코드
            ex.printStackTrace();
        }
    }

}

 

 

  • 이미지 삭제
    • 이미지는 id값으로 삭제 가능
    • 해당 id값은 BlobId 타입
    • BlobId는 bucketName + uuid로 이루어짐
    • 따라서, imageUrl에 있는 bucketName과 uuid로 삭제 가능
public void deleteImage(String imageUrl){
    Storage storage = createStorage();
    String uuid = parseUuid(imageUrl);
    BlobId blobId = BlobId.of(bucketName, uuid);

    if(storage == null){
        log.info("Storage 생성 실패");
        return;
    }

    if(!storage.delete(blobId)){
        log.info("GCS 이미지 삭제 실패");
        return;
    }
    log.info("GCS 이미지 삭제 성공");
}

private String parseUuid(String imageUrl){
    int lastSlashIndex = imageUrl.lastIndexOf('/');
    if (lastSlashIndex == -1) {
        throw new IllegalArgumentException("올바르지 않은 URL 형식입니다.");
    }
    return imageUrl.substring(lastSlashIndex + 1);
}

private Storage createStorage(){
    GoogleCredentials credentials = null;
    try{
        credentials = GoogleCredentials.fromStream(new FileInputStream(location));
    } catch (Exception e){
        log.error("이미지 인증 정보 생성 실패");
        return null;
    }

    return StorageOptions.newBuilder()
            .setCredentials(credentials)
            .setProjectId(projectId)
            .build()
            .getService();
}

 

 

  • 이미지 수정
    • GCS는 이미지 덮어쓰기 기능이 존재함
    • 덮어쓰기는 기존 URL을 유지하되, 새로운 이미지로 대체되는 것
      • (원래 이미지로 복구도 가능하다고 하는 데 더 디테일한 조사 필요)
    public String overWriteImage(MultipartFile newImage, String originalImageUrl){
        Storage storage = createStorage();
        if(storage == null){
            log.info("Storage 생성 실패");
            return null;
        }

        String uuid = parseUuid(originalImageUrl);
        String ext = newImage.getContentType();

        // Gcs 이미지 업로드
        BlobId blobId = BlobId.of(bucketName, uuid);
        BlobInfo blobInfo = BlobInfo.newBuilder(blobId)
                .setContentType(ext)
                .build();

        try (WriteChannel writer = storage.writer(blobInfo)) {
            byte[] imageData = newImage.getBytes();
            writer.write(ByteBuffer.wrap(imageData));
        } catch (Exception e) {
            log.error("이미지 전송 실패");
            return null;
        }

        return originalImageUrl;
    }

 

 

※ 주의

'스프링' 카테고리의 다른 글

테스트 코드  (0) 2025.02.05
JWT_사용자 정의 로그인  (0) 2025.01.31