J2ong 님의 블로그

Firebase Storage를 활용한 파일 업로드 구현 방법 본문

개인 프로젝트

Firebase Storage를 활용한 파일 업로드 구현 방법

J2ong 2025. 4. 22. 17:29

안녕하세요!
이번 글은 이전 게시글인 파일 업로드 구현 방식에 이어서, Firebase Storage를 활용해 배포 환경에서도 안정적으로 동작하는 파일 업로드 방식을 소개합니다.

 

파일 업로드 통합 구현 방식은 이전 게시물을 참고해주세요.

2025.04.21 - [개인 프로젝트] - Spring + React 파일 업로드 통합 구현 (korean-romanizer 라이브러리를 활용한 한글 파일명 오류 해결)

 

Spring + React 파일 업로드 통합 구현 (korean-romanizer 라이브러리를 활용한 한글 파일명 오류 해결)

안녕하세요!이번 글에서는 Spring Boot와 React 환경에서 파일 업로드 기능을 구현하는 방법을 공유합니다. 특히 한글 파일명을 업로드할 때 생길 수 있는 문제를 해결하는 방법도 함께 다룹니다.📌

talkhub.co.kr


📌 Firebase Storage를 사용한 이유

기존에는 파일을 file.path 기반으로 로컬 서버의 특정 폴더에 저장했는데, 다음과 같은 문제가 있었습니다.

  • 배포 환경에서 경로 인식 문제 발생
  • 서버 업데이트 시마다 mkdir로 폴더를 생성해야 함
  • 이전에 업로드된 파일이 삭제되거나 유지 관리가 어려움

SNS 서비스처럼 지속적인 배포와 기술 업데이트가 필요한 환경에서는 이러한 방식이 매우 불편하죠.
그래서 외부 스토리지(Firebase Storage) 를 도입하게 되었습니다.

 

📌  Firebase Storage 설정

1. Firebase 프로젝트 생성

 

Firebase | Google's Mobile and Web App Development Platform

개발자가 사용자가 좋아할 만한 앱과 게임을 빌드하도록 지원하는 Google의 모바일 및 웹 앱 개발 플랫폼인 Firebase에 대해 알아보세요.

firebase.google.com

  • Go to console → 원하는 프로젝트 선택 or 새로 생성
  • 좌측 사이드바에서 빌드 → Storage 이동

 

2. Cloud Billing 활성화

Firebase Storage를 사용하려면 Billing 계정 등록이 필요합니다.

 

  • 프로젝트 업그레이드 클릭

  • Clound Blilling 계정 만들기를 통해 결제 수단 등록

 

3. 보안 모드 & 스토리지 위치 설정

  • 배포용이면 프로덕션 모드 권장 (보안 규칙은 나중에도 수정 가능)
  • 리전 설정 예) us-central1

해당 이미지가 나오면 Storage가 성공적으로 생성된 것입니다.

이미지의 빨간 줄이 그어진 gs://를 제외한 부분이 이후에 스토리지에 접근할 버킷입니다.

 

4. 보안 규칙 설정

  • 보안 규칙을 설정하기 위해 규칙 탭으로 이동

보안 규칙 설정 예시
rules_version = '2';

service firebase.storage {
  match /b/{bucket}/o {
    match /uploads/{fileName} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}

 

5. 서비스 계정 키 생성

  • Firebase 콘솔 → 설정 → 서비스 계정
  • 새 비공개 키 생성 → 다운로드
  • 이 JSON 파일을 Spring 서버 설정에 포함

 


 

📌  Spring을 통한 Firebase Storage 연동

Configuration
    @Value("${firebase.config.json}")
    private String firebaseConfigJson;
    @Value("${firebase.storage-bucket}")
    private String firebaseStorageBucket;

    @PostConstruct
    public void initializeFirebase() {
        try {
    
            if (firebaseConfigJson == null || firebaseConfigJson.isEmpty()) {
                throw new RuntimeException("Firebase configuration is missing or empty!");
            }
    
            GoogleCredentials credentials = GoogleCredentials.fromStream(
                    new java.io.ByteArrayInputStream(firebaseConfigJson.getBytes())
            );
    
            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(credentials)
                    .setStorageBucket(firebaseStorageBucket)
                    .build();
    
            FirebaseApp.initializeApp(options);

            // CORS 설정 추가
            configureCors(credentials);
    
        } catch (IOException exception) {
            exception.printStackTrace();
            throw new RuntimeException("Firebase initialization failed", exception);
        }
    }

    private void configureCors(GoogleCredentials credentials) {
        try {
            // 스토리지 서비스 생성
            Storage storage = StorageOptions.newBuilder()
                .setCredentials(credentials)
                .build()
                .getService();
            
            // CORS 설정
            List<Cors> corsSettings = Arrays.asList(
                Cors.newBuilder()
                    .setOrigins(Arrays.asList(Cors.Origin.of({서버 URL})))
                    .setMethods(Arrays.asList(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.OPTIONS))
                    .setResponseHeaders(Arrays.asList("Content-Type", "Access-Control-Allow-Origin"))
                    .setMaxAgeSeconds(3600)
                    .build()
            );
            
            // 버킷 이름에서 gs:// 접두사 제거
            String bucketName = firebaseStorageBucket;
            if (bucketName.startsWith("gs://")) {
                bucketName = bucketName.substring(5);
            }
            
            // 버킷에 CORS 설정 적용
            storage.update(
                BucketInfo.newBuilder(bucketName)
                    .setCors(corsSettings)
                    .build()
            );
            
            System.out.println("CORS configuration applied to Firebase Storage bucket.");
            
        } catch (Exception e) {
            System.err.println("Failed to configure CORS for Firebase Storage: " + e.getMessage());
            e.printStackTrace();
        }
    }
  • firebaseConfigJson firebaseStorageBucket 은 application.propertice 설정에 변수로 지정합니다.
  • firebaseConfigJson : 5. 서비스 계정 키 생성 에서 생성한 Json 파일
  • firebaseStorageBucket : 3. 보안모드 & 스토리지 위치 설정 후 생성된 스토리지 버킷

 

Service (파일 업로드)
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Bucket;
import com.google.firebase.cloud.StorageClient;

    @Override
    @Transactional
    public String upload(MultipartFile file) {
        if (file.isEmpty()) return null;

        try {
            String originalFileName = file.getOriginalFilename();
            String fileNameWithoutExt = originalFileName.substring(0, originalFileName.lastIndexOf("."));
            String ext = originalFileName.substring(originalFileName.lastIndexOf("."));
            String romanized = KoreanRomanizer.romanize(fileNameWithoutExt);
            String sanitized = romanized.replaceAll("[^a-zA-Z0-9]", "_");
            String uuid = UUID.randomUUID().toString();
            String saveFileName = uuid + "_" + sanitized + ext;

            Bucket bucket = StorageClient.getInstance().bucket();
            Blob blob = bucket.create("uploads/" + saveFileName, file.getInputStream(), file.getContentType());

            // URL 생성
            String downloadUrl = "https://firebasestorage.googleapis.com/v0/b/"
                    + bucket.getName() + "/o/"
                    + URLEncoder.encode(blob.getName(), StandardCharsets.UTF_8.toString())
                    + "?alt=media";

            return downloadUrl;

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
  • 파일을 Firevase Storage의 uploads/ 경로로 업로드 합니다.

 

Service (파일 다운로드)
    @Override
    public Resource getFile(String fileName) {
        try {
            Bucket bucket = StorageClient.getInstance().bucket();
            Blob blob = bucket.get("uploads/" + fileName);

            if (blob == null || !blob.exists()) {
                return null;
            }

            byte[] content = blob.getContent(); // 전체 파일을 바이트 배열로 가져옴
            return new ByteArrayResource(content);

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

 

Controller
    @PostMapping("/upload")
    public String upload(
        @RequestParam("file") MultipartFile file
    ) {
        String url = fileService.upload(file);
        return url;
    }

    @GetMapping(value = "{fileName:.+}", produces = { 
        MediaType.IMAGE_JPEG_VALUE,
        MediaType.IMAGE_PNG_VALUE,
        MediaType.APPLICATION_PDF_VALUE,
        MediaType.APPLICATION_JSON_VALUE,
        MediaType.APPLICATION_OCTET_STREAM_VALUE
    })
    public ResponseEntity<Resource> getFile (
        @PathVariable("fileName") String fileName
    ) {
        Resource resource = fileService.getFile(fileName);
        if (resource == null) {
            return ResponseEntity.notFound().build();
        }

        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
                .body(resource);
    }

 

 


 

이제 배포 환경에서도 서버에 별도로 디렉터리를 만들 필요 없이, 안정적이고 확장 가능한 파일 업로드/다운로드 기능을 사용할 수 있습니다.

Firebase Storage를 통해 다음과 같은 이점을 얻을 수 있습니다.

  • 서버 업데이트 시 기존 파일 유지 가능
  • 확장성 높은 파일 저장 구조 확보
  • CDN을 통한 빠른 파일 전송 가능

이상으로 이번 포스팅을 마치겠습니다.

오늘도 즐거운 개발 되시길 바랍니다!