GDSC 솔루션 챌린지를 진행하면서 이미지를 저장해야 했다.
프로젝트에서 이미지는 어떻게 저장할까? 보통은 AWS S3나 Google Cloud Storage와 같은 클라우드 저장소에 저장한다. 이번 포스팅에서는 이미지를 Google Cloud Storage에 저장하고, 꺼내 쓰는 법을 다뤄보겠다.
클라이언트 측에서 보낸 파일을 바로 Storage에 올리는 방식이 필요했다.
찾아보니 감사하게도 잘 정리된 글이 있어서 다행히 잘 따라갈 수 있었다.
GCS로 이미지를 업로드하는 방식의 기본 원리는 AWS S3에 업로드하는 원리와 동일하다. 다만, 설정 관련한 부분이 상이하다고 한다.
Google Cloud 에 어떤 형식의 파일이든 저장하게 해주는 서비스이다. 정확히는 버킷이라는 컨테이너에 파일을 저장한다. AWS S3 버킷과 유사하다.
AWS vs GCP
클라우드 서비스 | 스토리지 | 하나의 버킷에 하나의 객체(파일)을 저장함 |
---|---|---|
AWS | S3 | S3의 버킷 |
GCP | Cloud Storage | Cloud Storage의 버킷 |
[1] 콘솔로 이동
https://cloud.google.com/?hl=ko
[2] Cloud Storage - 버킷으로 이동
[3] 버킷 만들기
파일 업로드를 누르고, 내 로컬 PC의 이미지를 업로드해본다. 업로드한 파일 이름(hawaii.jpg)를 클릭하면 인증된 URL 이라는 게 뜬다. 이 URL 로 접속해보면 아래처럼 업로드했던 사진이 뜬다!
다만, 공개 액세스가 아니기에 다른 구글 계정에선 해당 URL로 업로드한 사진을 볼 수 없다. 우리는 클라이언트가 이 URL만 가지고 화면단에 이미지를 띄워줄 수 있게 하기 위해 공개 액세스를 적용해주겠다!
공개 액세스 적용
a. 버킷 선택 후, 권한 탭으로 이동
b. 액세스 권한 부여
c. 버킷 새로고침 후 확인
이제 해당 Bucket의 모든 객체들은 모든 사용자들에게 공개된 상태로 변경되었다. 실제로, URL을 복사하셔서 다른 구글 계정이나 혹은 시크릿 모드에서 URL 접속하면 업로드한 사진을 볼 수 있다.
<img src={image_url} alt=""/>
와 같이 링크로 넣어주어 화면에 띄운다.
[4] 접근 권한 허용, key 파일 생성
위에서 공개 액세스를 적용했는데, 접근 권한 허용을 왜 또 하는지 궁금했다. 허나, 이 과정은 key를 만들어서 스프링부트에서 key를 이용해 storage에 접근할 수 있도록 해주는 것이다!
a. IAM 및 관리자 - 서비스 계정으로 이동
b. 서비스 계정 만들기 클릭
아래와 같이 입력해 만들어줬다.
c. 키 만들기
만든 서비스 계정을 클릭하고서, 키 탭으로 이동해 키 추가를 클릭한다.
만든 json 키 파일은 컴퓨터에 저장해둡니다. 나중에 프로젝트의 resources 폴더 안으로 옮길거에요! 위치 기억해두기!
아래는 내가 받은 key파일 내부이다.
[1] 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'
[2] application.yml에 key 등록해 storage 변수에 의존성 주입
위에서 만든 json key file을 resources 폴더에 넣고, application.yml에 아래 코드를 추가해준다.
spring:
cloud:
gcp:
storage:
credentials:
location: classpath:{위에서 만든 key 파일 이름}.json
project-id: {key 파일 안에 있는 project_id 값}
bucket: {버킷 이름}
key 파일에는 type, project_id, private_key_id, private_key 등 storage를 사용하는데 필요한 내용이 저장되어있다. 따라서 application.yml에 키 파일의 경로를 적어주면 스프링 부트는 키 파일의 내용을 바탕으로 stroage 변수에 자동으로 의존성을 부여한다.
주의할 점은 키 파일의 경로 외에도 project-id를 꼭! 넣어주어야 한다는 것이다. 넣지 않으면 의존성이 제대로 부여되지 않는다. 버킷 이름은 Cloud Storage 콘솔 들어가시면 나오는 버킷 이름이다.
[3] Security Configuration 설정
@Bean
public Storage storage() {
return StorageOptions.getDefaultInstance().getService();
}
[4] 이미지 업로드 코드
GCS에 사용자로부터 넘어온 이미지 파일을 저장하는 코드이다.
ImageController
package com.app.premom.controller;
import com.app.premom.dto.ImageSaveRequestDto;
import com.app.premom.entity.User;
import com.app.premom.service.ImageService;
import com.app.premom.service.LoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PatchMapping;
import java.io.IOException;
@RequiredArgsConstructor
@Controller
public class ImageController {
private final ImageService imageService;
private final LoginService loginService;
@PatchMapping("")
public ResponseEntity<Void> updateImageInfo(Authentication auth, ImageSaveRequestDto dto) throws IOException {
User user = loginService.getLoginUserByLoginId(auth.getName());
imageService.updateImageInfo(user, dto);
return new ResponseEntity<>(HttpStatus.OK);
}
}
ImageService
package com.app.premom.service;
import com.app.premom.dto.ImageSaveRequestDto;
import com.app.premom.entity.User;
import com.google.cloud.WriteChannel;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class ImageService {
@Value("${spring.cloud.gcp.storage.bucket}") //application.u=yml에 써둔 bucket 이름
private String bucketName;
@Value("${spring.cloud.gcp.storage.project-id}")
private String projectId;
Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
//이미지 정보 수정
public void updateImageInfo(User user, ImageSaveRequestDto dto) throws IOException {
// !!! 이미지 업로드 관련 부분 !!!
String uuid = UUID.randomUUID().toString(); // Google Cloud Storage 에 저장될 파일 이름
String ext = dto.getImage().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 = dto.getImage().getBytes(); // 이미지 데이터를 byte 배열로 읽어옵니다.
writer.write(ByteBuffer.wrap(imageData));
} catch (Exception ex) {
// 예외 처리 코드
ex.printStackTrace();
}
// DB에 반영
user.updateProfileImage(uuid);
}
}
(📌Google Cloud Storage의 create 메서드가 deprecated되었음!)
deprecated된 내용 @Value("${spring.cloud.gcp.storage.bucket}") // application.yml에 써둔 bucket 이름
private String bucketName;
// 회원 정보 수정
public void updateMemberInfo(MemberUpdateRequest dto) throws IOException {
// ...
// !!!!!!!!!!!이미지 업로드 관련 부분!!!!!!!!!!!!!!!
String uuid = UUID.randomUUID().toString(); // Google Cloud Storage에 저장될 파일 이름
String ext = dto.getImage().getContentType(); // 파일의 형식 ex) JPG
// Cloud에 이미지 업로드
BlobInfo blobInfo = storage.create(
BlobInfo.newBuilder(bucketName, uuid)
.setContentType(ext)
.build(),
dto.getImage().getInputStream()
);
// ...
}
⛔에러
'create(com.google.cloud.storage.BlobInfo,java.io.InputStream,com.google.cloud.storage.Storage.BlobWriteOption...)' is deprecated
create 메서드가 deprecate(사용이 권장되지 않음) 되었음, 대신 write 메서드를 사용하는 것이 좋다. BlobInfo와 InputStream을 사용하여 새로운 Blob을 생성하고 위의 ImageService내의 코드와 같이 사용했다.
</div>
ImageSaveRequestDto
package com.app.premom.dto;
import lombok.*;
import org.springframework.web.multipart.MultipartFile;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ImageSaveRequestDto {
private String nickname;
private MultipartFile image; // 업로드할 이미지
}
Postman 테스트 성공!