클라우드/Firebase

[Firebase] Firebase SDK(Java)를 사용하여 이미지 업로드/다운로드

worldcenter 2024. 10. 22. 01:15

 

 

 

뉴스피드 앱에서 게시물 기능을 개발하면서 이미지와 글을 게시물로 등록, 조회, 수정, 삭제하는 기능을 구현해야 했습니다. 글을 등록하는 것은 문제가 없었지만 이미지를 등록하는 방법에 대해서 고민했습니다. 첫번째는 이미지를 어디에 저장할까 였습니다.

 

1. 서버 데이터 디스크에 이미지 보관

2. 클라우드 스토리지에 이미지 보관

 

저는 위 2가지 방법 중 '클라우드 스토리지에 이미지를 보관' 하는 방법을 채택 했습니다. 그 이유는 앱이 배포되어 있는 서버가 1대면 상관없지만 트래픽에 대한 관리가 중요한 만큼 부하 분산을 위한 서버가 여러 대 일 경우 공유해서 사용할 스토리지가 필요할 것이라 생각했습니다.

 

클라우드 스토리지에 이미지 보관

해당 프로젝트에서는 Firebase Blob Storage를 사용하여 이미지를 보관하는 방법을 채택했습니다. 이 과정에서 발생한 몇 가지 이슈에 대해서 어떻게 해결하였는지 설명드리겠습니다.

 

처음에는 Controller에서 이미지 파일을 받아올 때 MultipartFile을 사용하려 했으나 파일을 가져오지 못하는 문제가 지속되었고 튜터님에게 문의드리니 해당 방법은 과거에 사용하던 방법으로 최근 이미지 파일 업로드/다운로드 방법은 다음 2가지를 사용한다고 답변주셨습니다.

 

1. Frontend에서 이미지를 업로드 받아 클라우드에 업로드하고 접속 URL만 백엔드로 전달

2. 이미지 파일을 String으로 백엔드에 전달

 

이 중 첫 번재 방법은 해당 프로젝트에서 Frontend를 구현하지 않으므로 제외하고 두 번째 방법을 사용했습니다.

이진파일을 Base64로 인코딩하여 백엔드로 전달하고 백엔드에서 다시 이를 디코딩하여 스토리지에 저장하면 되지 않을까? 라는 생각으로 몇 단계에 걸쳐 개발을 진행하였습니다.

 

 

1. Base64 디코딩

이미지를 클라우드 스토리지로 업로드 하기 위해서는 이미지 이름, Content-Type, 이미지 데이터가 필요합니다. 이를 위해서 디코딩을 필수적입니다.

 

우선 firebase SDK를 사용해야 하기에 관련 라이브러리를 build.gradle에 추가합니다.

dependencies {
    implementation 'com.google.firebase:firebase-admin:9.2.0'
}

 

Controller에서 이미지 데이터와 게시글을 하나의 Body에 담아서 Service로 전달합니다.

@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    // 게시물 등록
    @PostMapping()
    public ResponseEntity<ResponsePostDto> createPost(@RequestBody @Valid RequestPostDto requestDto) {

        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(postService.createPost(requestDto));

    }

}
@Getter
@NoArgsConstructor
public class RequestPostDto {

    @NotBlank
    private String imgData;

    @NotBlank
    @Size(max = 255)
    private String caption;


}

 

Service에서 디코딩을 처리해주는 객체를 생성하고 인코딩 되어 있는 이미지 데이터를 전달합니다.
임시로 return 값은 null로 처리하였습니다.

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public ResponsePostDto createPost(RequestPostDto requestDto) {

        try {

            // Base64 -> Image
            MultipartFile multipartFile = new Base64ToImage(requestDto.getImgData());

            return null;


        } catch (IOException e) {
            return null;

        }


    }

}

 

이미지를 인코딩 하게 되면 아래와 같은 데이터 형식을 가지게 되며, 해당 데이터에서 MIME 타입과 이미지 데이터를 디코딩해야 하기 때문에 별도의 역할을 수행할 Base64ToImage 클래스를 생성하였습니다.

data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/
public class Base64ToImage implements MultipartFile {

    private String[] parts = new String[2];

    public Base64ToImage(String imgData) {
        this.parts = imgData.split(",");
    }

	// 랜덤ID로 이미지 이름 생성
    @Override
    public String getName() {
        return UUID.randomUUID().toString();
    }

	// metadata에서 MIME 타입 추출
    @Override
    public String getContentType() {

        String metadata = parts[0];
        String mimeType = metadata.split(":")[1].split(";")[0];
        return mimeType;

    }

	// 이미지 디코딩
    @Override
    public byte[] getBytes() throws IOException {
        String base64Image = parts[1];
        return Base64.getDecoder().decode(base64Image);

    }

	// bytearray -> InputStream
    @Override
    public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(getBytes());
    }


}

 

이미지를 Base64로 인코딩하면 HTTP 요청을 할 필요 없이 이미지 데이터를 문자열로 포함시킬 수 있습니다. 일반적으로 이미지를 HTTP 요청을 통해 전송하는 경우, 클라이언트는 서버에게 이미지 파일을 URL을 제공하고 서버는 해당 URL로부터 이미지를 다운로드하여 응답으로 전송합니다. 

그러나 이미지를 Base64로 인코딩하면 이미지 데이터가 문자열로 변환되어 있으므로 별도의 HTTP 요청 없이 해당 문자열을 클라이언트로 전송할 수 있습니다. 

Base64로 이미지를 인코딩하면 주요 이점은 다음과 같습니다.

 

1. 간편한 전송 : 이미지를 Base64 문자열로 인코딩하면 이미지 데이터를 HTTP 요청 없이 직접 전송
2. 단일 요청 : 이미지를 Base64로 인코딩하면 이미지 데이터를 별도의 HTTP 요청 없이 HTML, CSS, JSON 등의 텍스트 기반 형식으로 포함시킬 수 있어 리소스 요청 수를 줄이고 응답 시간을 개선할 수 있음
3. 캐싱 : 이미지를 Base64로 인코딩하여 HTML, CSS 등에 포함시키면, 클라이언트는 이미지를 따로 다운로드 하지 않아도 되므로 캐싱을 활용할 수 있음. 캐싱은 성능을 향상시키고 대역폭을 절약할 수 있는 장점을 제공
4. 외부 종속성 제거 : 이미지를 Base64로 인코딩하면 이미지 파일 자체에 대한 의존성을 제거할 수 있음. 따라서 이미지 파일을 따로 관리하거나 로드하는 작업을 건너뛸 수 있음

 

Base64로 이미지를 인코딩하면 데이터 크기가 커지는 단점도 있을 수 있습니다. 이미지를 문자열로 변환하기 때문에 데이터 크기가 약 1.37배로 증가하게 됩니다. 따라서 대용량 이미지의 경우 데이터 전송 및 처리에 부하가 발생할 수 있습니다.
또한, Base64 인코딩된 이미지를 표시하려면 디코딩 작업이 필요하므로 추가적인 처리 시간이 소요될 수 있습니다.

 

 

 

2. Firebase Storage 업로드/다운로드

Firebase Storage에 이미지를 업로드/다운로드 하기 위해서는 몇 가지 작업들이 선행되어야 합니다.

 

1) Firebase Storage 생성

 

2) Firebase Service Account 얻기

Firebase Storage에 접근하기 위해서는 적법한 권한을 가지고 있어야 합니다. firebase console > 프로젝트 설정 > 서비스 계정으로 이동하여 '새 비공개 키를 생성' 합니다.

'새 비공개 키 생성' 버튼을 누르게 되면 권한 정보가 담긴 firebase.json 파일이 다운로드 되고 이를 프로젝트 루트 계정에 위치시킵니다.

 

3) Firebase SDK 초기화

Firebase Storage에 접속하기 위한 권한까지 얻었으면 이제는 접속을 위한 firebase SDK 초기화 과정이 필요합니다.

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    @EventListener
    public void init(ApplicationReadyEvent event) {
        try {
        	
            // firebase.json에 있는 권한 정보 읽어오기
            FileInputStream serviceAccount = new FileInputStream("firebase.json");

		// firebase storage 정보 빌드
            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                    .setStorageBucket("nuri-8c58d.appspot.com")
                    .build();

		// firebase 초기화
            FirebaseApp.initializeApp(options);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public ResponsePostDto createPost(RequestPostDto requestDto) {

        try {

            // Base64 -> Image
            MultipartFile multipartFile = new Base64ToImage(requestDto.getImgData());

            return null;


        } catch (IOException e) {
            return null;

        }


    }

}

 

https://firebase.google.com/docs/storage/admin/start?hl=ko

 

Admin Cloud Storage API 소개  |  Cloud Storage for Firebase

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 의견 보내기 Admin Cloud Storage API 소개 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Cloud Storage for Fireba

firebase.google.com

 

4) Storage에 이미지 업로드/다운로드

 

마지막으로 디코딩한 이미지 파일에서 이미지 정보, MIME 타입, 이미지 이름을 가져와 bucket에 업로드하고 접속 URL과 토큰 정보를 가져오는 기능을 구현하였습니다.

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    @EventListener
    public void init(ApplicationReadyEvent event) {
        try {
            FileInputStream serviceAccount = new FileInputStream("firebase.json");

            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                    .setStorageBucket("nuri-8c58d.appspot.com")
                    .build();

            FirebaseApp.initializeApp(options);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public ResponsePostDto createPost(RequestPostDto requestDto) {

        try {

            // Base64 -> Image
            MultipartFile multipartFile = new Base64ToImage(requestDto.getImgData());

            // firebase storage upload
            String imageName = multipartFile.getName();
            String contentType = multipartFile.getContentType();
            InputStream image = multipartFile.getInputStream();

            Bucket bucket = StorageClient.getInstance().bucket();
            Blob blob = bucket.create(imageName, image, contentType);

            // download image url
            String downloadToken = UUID.randomUUID().toString();
            String downloadLink = String.format("https://firebasestorage.googleapis.com/v0/b/%s/o/%s?alt=media&token=%s",
                    bucket.getName(),
                    blob.getName(),
                    downloadToken
            );

            return null;


        } catch (IOException e) {
            return null;

        }


    }

}

 

https://medium.com/@bhanuprasadbodasingi1234/spring-boot-file-uploads-with-firebase-storage-integration-ef1e61e72d07

 

Spring Boot File Uploads with Firebase Storage Integration

I want to explain how to upload files into firebase storage with Spring Boot and get downloaded link as a response.

medium.com

 

 

기능 테스트

이제는 정상적으로 이미지가 업로드 되고 다운로드 url까지 받아오는지 확인해보겠습니다. Postman을 통해 이미지와 게시글을 앱으로 POST 해보았습니다.

POST http://localhost:8080/api/posts
{
    "imgData": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/...,
    "caption": "너무 즐거운 시간~"
}

 

firebase storage를 확인해보니 정상적으로 이미지가 업로드 된 것을 확인할 수 있습니다.

 

또한, 이미지 업로드 후 접속 URL도 확인할 수 있었으며 해당 링크로 접속하면 정상적으로 이미지를 볼 수 있습니다.

 

firebase SDK를 사용하여 이미지를 업로드/다운로드는 하는 기능을 구현할 때 공식 자료로는 정상적으로 작동하지 않아 많은 시행 착오가 있었기에 해당 내용 공유합니다.