easycode

[Spring] GCS(Google Cloud Storage)를 이용하여 MultipartFile(이미지 파일) 업로드 및 포스트맨으로 테스트 해보기(MultipartFile&JSON) (feat. Spring, flutter) 본문

Spring

[Spring] GCS(Google Cloud Storage)를 이용하여 MultipartFile(이미지 파일) 업로드 및 포스트맨으로 테스트 해보기(MultipartFile&JSON) (feat. Spring, flutter)

ez() 2023. 9. 18. 16:19

API명세서에 따라 프런트엔드에서 Multipart 형태의 이미지 파일과 json 형태의 데이터를 받아서 이미지는 GCS에, json 형태의 데이터는 DB에 저장해 보자.

사용 스택 : Springboot 2.7, JPA

 

 

예를 들어 아래와 같은 API가 있다고 하자.

POST	/users/register/profile

해당 api는 유저의 프로필 정보를 받아 등록하는 POST 형식의 api이다.

프런트엔드에선 해당 api로 List<MultiPartFile> 이미지와 유저의 정보를 보내준다.

그럼 백엔드에선 controller를 통해 해당 데이터를 잘 처리해서 DB에 저장해야 한다.

 

 

 

시작 전, GCS 설정과 spring에 연동하는 방법은 아래 글을 참고해 주세요.

https://jyami.tistory.com/54

 

GCP Cloud Storage + Springboot 연동하기

이번 외주를 맡은 내용이 Google Cloud Storage를 이용해서 file을 업로드, 다운로드하는 API 기능을 구현해서 이 내용을 정리하고자 한다. Cloud Storage를 다루는 방법으로 Google Cloud Console, Cloud SDK를 이용

jyami.tistory.com

https://dncjf64.tistory.com/313

 

Springboot + GCP Cloud Storage 연동(파일 업로드, 다운로드)

회원 정보 수정에서 썸네일 업로드가 필요했습니다. 파일을 Gloud에 저장하여 업로드 및 다운이 필요했기에 현재 사용하고 있는 Google Cloud Platform을 선택하였습니다. 적용하는데 발생한 이슈위주

dncjf64.tistory.com

https://jsikim1.tistory.com/27

 

Google Cloud Storage Bucket 개체 공개 액세스 변경 (allUsers 뷰어 권한 부여)

Google Cloud Storage Bucket 개체 공개 액세스 변경 (allUsers 뷰어 권한 부여) Google Data Studio의 Community Visualizations 을 직접 만들어 사용하거나 공개하고 싶을때와 같이 Google Cloud Platform(GCP)의 Google Cloude Stor

jsikim1.tistory.com


Properties 설정

 

먼저, MultipartFile을 다루기 위해선 .properties 파일에 설정을 해줘야 한다.

application.properties에 아래 코드를 추가해 주자.

spring.servlet.multipart.enabled=true  // MultipartFile을 사용
spring.servlet.multipart.max-file-size=10MB  // 각 파일의 최대 크기는 10MB
spring.servlet.multipart.max-request-size=20MB  // 서버로 전송되는 모든 데이터는 20MB를 초과할 수 없음

이제 MultipartFile을 사용할 수 있게 됐다.

 

다음으로 GCS를 연동해 주자.

spring.cloud.gcp.storage.credentials.location=classpath:{GCS key 값을 저장한 json 파일}
spring.cloud.gcp.project-id={GCS project id}
spring.cloud.gcp.storage.bucket={bucket 이름}

 

 


DTO

 

그럼 api로 받아오는 requestData의 DTO를 만들어 주자.

나는 예시코드에 List<MultiPartFile>는 해당 dto에 넣지 않고 따로 받아 왔지만, dto 안에 넣어도 된다.

(아래는 예시 코드입니다)

// ProfileRequestDto.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class ProfileRequestDto {
    private char gender;
    private String nickname;
    private String birthday;
    private String intro;
}

 

 

 


Service

 

 

다음으로 이미지 파일을 업로드할 service를 작성해 보자.

여기서 신경 써야 할 점은
1. 다중 이미지 파일인 점(List<MultipartFile> 형태)

2. DB에 저장되는 이미지 파일 이름이 겹치지 않도록 신경 쓰기

3. 이미지를 GCS에 업로드 후, https://~ 형태로 DB에 저장하기

이다.

2번의 경우, 파일명을 uuid로 저장하면 된다. (uuid란?  네트워크상에서 고유성을 보장하는 ID를 만들기 위한 표준 규약으로서, 쉽게 말하자면 중복되는 확률이 0에 가까운, 최소한의 확률을 가지는 ID를 만드는 것이다)

 

(아래는 예시 코드입니다)

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

private Storage storage; // import com.google.cloud.storage.Storage;


...


public <return값> insertProfile(ProfileRequestDto profileRequestDto, List<MultipartFile> profilePhotoUpload) throws IOException {

	// Profile 테이블에 정보 저장
    	...

	String uuid; // 이미지 이름대신 uuid를 사용
    String ext; // 파일의 contentType

	// 다중 이미지기 때문에 for문 사용
    for (MultipartFile multipartFile : profilePhotoUpload) { // MultipartFile형인 변수 multipartFile
        uuid = UUID.randomUUID().toString();
		ext = multipartFile.getContentType();
        
        // GCS에 이미지 업로드
        BlobInfo blobInfo = storage.create(
                    BlobInfo.newBuilder(bucketName, uuid)
                            .setContentType(ext)
                            .build(),
                    multipartFile.getInputStream()
            );
        
        // profilePhoto 테이블에 해당 사진의 url 저장
        ProfilePhoto profilePhoto = ProfilePhoto.builder()
                .photoUrl("https://storage.googleapis.com/"+ buketName + "/" + uuid) // GCS에 업로드된 이미지 파일을 바로 볼 수 있도록 https://~ 를 붙여 설정
                .build();
        profilePhotoRepository.save(profilePhoto);
        }

        ... 
    }

DB에 https://storage.googleapis.com/버킷명/GCS에 저장된 이름으로 넣은 이유는 GCS에 업로드된 이미지를 보기 위해선 앞서 말한 형식으로 지정해야 사진을 볼 수 있기 때문이다.

나는 이후에 클라이언트로 값을 넘겨줄 때 클라이언트에서 해당 값을 받은 후, 따로 처리 과정을 거치지 않고 바로 뷰에 뿌리는 게 낫다고 생각해서 DB에 해당 형식으로 넣어 주었다.

굳이 https://storage.googleapis.com/~를 붙이고 싶지 않다면, 버킷명과 GCS에 업로드할 때 사용했던 이름만 넣어 프런트엔드에게 넘겨주면 프런트엔드가 앞에 https://storage.googleapis.com/를 붙여 보여주는 방법도 있다.

 

 

 


Controller

 

 

이제 마지막으로 controller를 짜 보자.

(아래는 예시 코드입니다)

    // 프로필 등록
    @PostMapping(value = "/users/register/profile")
    public ResponseEntity<ProfileResponseDto> insertProfile(@RequestPart(value = "profileInfo") ProfileRequestDto profileRequestDto,
                                                            @RequestPart(value = "profilePhoto") List<MultipartFile> profilePhotoUpload) throws IOException {
        ProfileResponseDto profileResponseDto = profileService.insertProfile(profileRequestDto, profilePhotoUpload);
        return ResponseEntity.ok(profileResponseDto);
    }

1. 데이터를 저장하는 것이므로 POST 형식을 사용하기 (value엔 설정한 api 값을 넣어준다)

2. 데이터를 받을 때는 꼭!!!!!!!!!! @RequestPart 어노테이션으로 받기 (@RequestBody 어노테이션과 중복 사용 절대 금지)

3. ResponseEntity<return값>으로 설정 (필자는 위 service에서 response값을 따로 설정해 뒀다)

 

특히 2번 같은 경우, @RequestBody 어노테이션과 중복해서 사용하면 절대 안 된다.

body로 데이터를 받으려면 @RequestBody를 써야 하긴 하지만, multipart/form-data 파일을 받아야 하는데 해당 어노테이션을 쓸 경우 에러가 나므로 꼭 @RequestPart를 사용하자.

@RequestPart 옆 value값엔 requestData의 key값을 쓴다.

 

 


Postman 테스트

 

이제 세팅은 모두 끝났다!

포스트맨으로 먼저 테스트해보자.

postman 세팅은 다음과 같이 해줬다.

1. 메서드는 POST

2. 로컬에서 테스트 시, localhost:포트번호/api

3. body - form/data로 설정

4. key 값엔 아까 @RequestPart 옆에 지정해 줬던 value 값 입력

5. json으로 받는 경우, value엔 json 형식으로 데이터 입력

6. 이미지 파일은 key 옆에 뜨는 text를 file로 형식 변경, select files를 클릭해서 업로드할 이미지 파일 선택

7. content type은 application/json과 multipart/form-data로 설정해 줬다.

 

 

 

그리고 테스트를 해보면

이렇게 성공적으로 된다!

 

 


(번외) 분명 포스트맨에선 잘 됐는데...

프런트엔드와 실제로 테스트하는 과정에서 오류가 났다. 에러 코드는 415...

보통 이미지 파일 업로드 과정에서 나오는 415 코드의 원인은 content type이 제대로 설정되지 않아서이다.

프런트엔드 쪽에선 MultipartFile로 이미지를 보내고 있었는데, 그게 아니라 form-data 형식으로 보내줘야 했다. 그리고 json으로 보내는 데이터도 json으로 encode 후, content type을 application/json으로 설정해줘야 했다.

해결 방법을 구글링 해보던 중, flutter 쪽에서 content type을 지정해 주는 방법이 있어 해당 방법으로 시도했더니 됐다!!

 

(아래는 flutter 예시 코드입니다)

// json으로 encode 후, content type 지정
jsonEncode(Pref.instance.profileData?.toJson()),
contentType: MediaType('application', 'json'),

// content type을 form-data로 보내기
 FormData formData = FormData.fromMap({
        'profileInfo': profileInfoData,
        'profilePhoto': profilePhotos
      });

 

(예시 코드 전문)

더보기
// flutter 코드

lass ProfileRegister {
  Future<bool> sendProfileRequest(List<MultipartFile> profilePhotos) async {
    const url = '${Config.domain}/users/register/profile';

    try {
      Dio dio = Dio();

      log("[JSON]\n${jsonEncode(Pref.instance.profileData?.toJson())}");

      final profileInfoData = MultipartFile.fromString(
        jsonEncode(Pref.instance.profileData?.toJson()),
        contentType: MediaType('application', 'json'),
      );

      FormData formData = FormData.fromMap({
        'profileInfo': profileInfoData,
        'profilePhoto': profilePhotos
      });

      Response response = await dio.post(url, data: formData);

      if (response.statusCode == 200) {
        log("Upload successful");
        return true;
      } else {
        log("Error during upload: ${response.statusCode}");
        return false;
      }
    } catch (e) {
      log("Exception occured: $e");
      return false;
    }
  }
}

 

이중 MediaType을 사용해 주기 위해선 flutter에서 따로 dependency를 추가해줘야 한다.

 

 


자료 조사를 많이 했던 덕분인지 생각보다 수월하게 끝낼 수 있었다.

아래는 GCS 연동에 정말 큰 도움을 준 글이다(만약 제 글을 읽고 이해가 안 가거나 조금 더 자세히 알고 싶다면 아래 글을 참고해주세요)

https://choo.oopy.io/35bffd94-7a41-4cfa-812c-b8aaf148604a#04fe20b4-5dcf-43cc-9ffb-faf3aa0e8aff

 

Springboot에서 GCS로 이미지 파일 업로드 총정리(multipart/form-data)(Google Cloud Storage)(GCP)

1. 서론

choo.oopy.io

GCS는 한글로 된 문서가 잘 없는데 위 글을 발견해서 너무 행복했다... 정말로 감사합니다.