서론
파이어베이스(Firebase)는 구글이 소유한 모바일 어플리케이션 개발 플랫폼이다. 어플리케이션 개발을 위한 다양한 기능을 제공한다.
토이프로젝트나 과제 등에 파이어베이스의 Firestore Database와 Storage를 사용하면, 돈 한 푼 내지 않고 원격 저장공간을 사용할 수 있다. 사용 용량, 요청 횟수 등의 지표가 일정 기준을 넘지만 않으면 무료로 이용할 수 있다. 예상치 못한 원인으로 인해 갑자기 지출이 발생하는 일이 AWS에 비해 적어서 좋다고 생각한다. 팀원과 데이터를 실시간으로 공유하며 작업해야 할 때 굉장히 편리하다.
그러나 Java 및 스프링부트로 파이어베이스를 사용하는 방법에 대한 자료는 찾기가 굉장히 어렵다. JavaScript 자료만 많고, Java 자료는 잘 보이지 않는다. 프론트엔드만 만들어주면 백엔드의 역할 대부분을 파이어베이스에 맡길 수 있기 때문에 프론트엔드 개발자를 위한 관련 자료는 많다. 그에 비해, 백엔드 개발자를 위한 한글 자료는 매우 적다.
그래서 나처럼 Java와 스프링부트의 원격 저장소로서만 파이어베이스를 사용하고자 하는 사람들을 위해 이 글을 남긴다.
Java 및 스프링부트에는 익숙하되, 파이어베이스 연동 방법만 모르는 사람들을 대상으로 하는 글이다.
기본적인 JSON 데이터를 저장할 수 있는 Firestore Database와, 버킷에 파일을 저장할 수 있는 Storage를 다룬다.
파이어베이스 프로젝트 생성은 사용 언어/프레임워크에 관계 없이 공통되는 과정이다. 이 과정은 공식문서 및 다양한 자료를 통해 쉽게 완료할 수 있다. 그러므로 프로젝트 생성과 Firebase Database 생성까지는 이미 되어있다고 가정하고 설명하겠다.
본 글에서 사용하는 스프링부트 프로젝트는 Maven이 아닌 Gradle을 사용한다.
Dependency 내역은 다음과 같다.
IDE로는 IntelliJ를 사용한다.
테스트에는 Postman을 사용한다.
목차
1. 스프링부트-파이어베이스 연동
- 비공개 키 다운로드
- Dependency 설정
- FirebaseInitialization 클래스 작성
2. Firestore Database 사용
- 개요
- 코드 작성
- 테스트
3. 파이어베이스 Storage에 파일 저장하기
- Storage 버킷 생성
- 개요
- 코드 작성
- 테스트
스프링부트-파이어베이스 연동
비공개 키 다운로드
파이어베이스를 사용하기 위해서는, 각 프로젝트마다 고유하게 주어지는 비공개 키가 필요하다.
비공개 키가 적힌 파일은 다음과 같은 과정을 따라 파이어베이스 콘솔에서 다운로드받을 수 있다.
만들어둔 프로젝트 콘솔로 들어가서, 좌측 상단의 톱니바퀴를 누른 후 '프로젝트 설정' 또는 '사용자 및 권한'을 클릭한다.
이동한 화면에서, '서비스 계정' 탭으로 들어간다.
그럼 아래와 같은 화면이 보인다.
여기서 '새 비공개 키 생성'을 클릭한다.
그럼 json 파일이 하나 다운로드받아진다.
이 파일의 이름을 serviceAccountKey.json으로 바꾼 후, 로컬에 있는 스프링 부트 프로젝트 폴더 안에 넣어준다. 프로젝트의 최상위 폴더(build.gradle 파일과 .gitignore 파일 등이 있는 곳)에 넣어주면 된다.
이 파일은 절대로 외부에 유출되어서는 안 된다.
그러므로 .gitignore에 반드시 다음의 코드를 추가해주도록 하자.
serviceAccountKey.json
Dependency 설정
이제 build.gradle에 dependency를 추가해주어야 한다.
추가해야 하는 내용은 다음의 웹사이트에서 찾을 수 있다.
https://mvnrepository.com/artifact/com.google.firebase/firebase-admin
들어가보면 여러 버전이 나열되어있다.
현재 가장 최신 버전인 9.1.1의 dependency 코드를 찾아보자.
아래 사진에서 노란색으로 강조된 9.1.1 항목을 클릭한다.
그럼 아래와 같은 화면으로 이동한다. 하단에 dependency 코드가 여러 언어로 표시된다.
Gradle 탭을 선택하여 나온 코드를 복사한다.
복사한 코드를 build.gradle에 붙여넣어준다.
dependencies {
...
// https://mvnrepository.com/artifact/com.google.firebase/firebase-admin
implementation group: 'com.google.firebase', name: 'firebase-admin', version: '9.1.1'
}
FirebaseInitialization 클래스 작성
파이어베이스를 사용하기 위해서는, 프로젝트 코드를 실행하자마자 파이어베이스 초기화 코드가 실행되어야 한다.
이를 위한 FirebaseInitialization 클래스를 작성해주자.
프로젝트를 실행할 때 사용하는 Application 실행 클래스와 같은 패키지 안에 firebase라는 패키지를 하나 만들어주고, 그 안에 FirebaseInitialization 클래스를 생성한다.
이 클래스 안에는, 비공개 키를 받을 때 보았던 구성 스니펫 코드를 넣어주면 된다.
예외 처리를 포함한 전체 클래스의 코드는 다음과 같다.
import com.google.auth.oauth2.GoogleCredentials;
import org.springframework.stereotype.Service;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.FirebaseApp;
import javax.annotation.PostConstruct;
@Service
public class FirebaseInitialization {
@PostConstruct
public void initialize() {
FileInputStream serviceAccount =
null;
try {
serviceAccount = new FileInputStream("./serviceAccountKey.json");
FirebaseOptions options = null;
try {
options = new FirebaseOptions.Builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
} catch (IOException e) {
e.printStackTrace();
}
FirebaseApp.initializeApp(options);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
위 코드는 serviceAccountKey.json 파일을 최상위 폴더에 넣었을 경우를 위한 코드이다.
만약 serviceAccountKey.json 파일을 최상위 폴더가 아닌 다른 위치에 넣어두었다면,
serviceAccount = new FileInputStream("./serviceAccountKey.json"); 의 괄호 안에 그 위치로 가는 path를 반영하여 적어야 한다.
Firestore Database 사용
이제 데이터를 실제로 저장해보자.
개요
본 게시글에서는 회원 정보를 저장해볼 것이다.
회원 정보의 구성:
- 고유 번호 (각 회원을 구분하기 위해 사용됨. 가장 처음 가입한 회원부터 순서대로 1, 2, 3, ... 의 번호가 순서대로 부여됨.)
- 아이디 (회원이 로그인할 때 입력하는 아이디 문자열)
- 별명
- 비밀번호
회원가입 로직:
프론트엔드(또는 모바일 앱)에서 POST 요청을 보낸다.
이때 요청의 Body는 다음과 같은 정보를 포함한다.
{
"id" : "아이디 값",
"nickname" : "별명 값",
"password" : "비밀번호 값"
}
요청이 들어오면, 서버는 Firebase의 Firestore Database를 탐색하여 가장 최근에 가입한 회원의 고유 번호가 몇인지 알아낸다.
이번에 새로 회원가입 요청을 보낸 유저는 가장 최근에 가입한 회원의 고유 번호에 1을 더한 수를 고유 번호로 배정받는다.
고유 번호 배정이 완료되면 새 회원 정보를 Firestore Database에 저장한다.
코드 작성
프로젝트 구조는 다음과 같다.
먼저, 회원 정보 객체의 구조를 정의하는 Member.java부터 작성해보자.
Member.java
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Data
@Getter
@NoArgsConstructor
public class Member {
private String sequence; // 고유 번호
private String id; // 아이디
private String nickname; // 별명
private String password; // 비밀번호
public Member (String id, String nickname, String password){
this.id = id;
this.nickname = nickname;
this.password = password;
}
public void setSequence(String sequence) {
this.sequence = sequence;
}
public void setId(String id) {
this.id = id;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void setPassword(String password) {
this.password = password;
}
}
그 다음은 컨트롤러이다.
URL은 편한 대로 설정해주면 된다. 본 예시에서는 "/api/member/"를 사용하였다.
POST 요청을 보내는 회원가입 메소드를 작성해준다.
FirebaseMemberController.java
import 프로젝트패키지.member.domain.Member;
import 프로젝트패키지.member.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/api/members")
public class FirebaseMemberController {
@Autowired
private MemberService memberService;
// 회원가입 (POST)
@PostMapping
@ResponseStatus(value = HttpStatus.CREATED)
public String saveMember(@RequestBody Member member) throws ExecutionException {
return memberService.saveMember(member);
}
}
마지막으로, 파이어베이스에 직접적으로 가 닿는 부분인 서비스 코드이다.
members라는 컬렉션에, sequence 값을 이름으로 하는 문서를 만들고, 그 문서 안의 필드로 id, nickname, password, sequence를 저장하는 방식이다.
저장에 성공하면 저장 시점의 시각을 문자열로 리턴하고, 실패하면 memberService failed라는 문자열을 리턴한다.
MemberSerivce.java
import 프로젝트패키지.member.domain.Member;
import com.google.api.core.ApiFuture;
import com.google.cloud.firestore.*;
import com.google.firebase.cloud.FirestoreClient;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.ExecutionException;
@Service
public class MemberService {
// 사용할 데이터베이스
private static Firestore dbFirestore;
private static final String COLLECTION_NAME = "members";
private long sequence = 1L;
public String saveMember (Member member) {
dbFirestore = FirestoreClient.getFirestore();
// 가장 최근에 가입된 회원 한 명의 데이터 가져오기
Query query = dbFirestore.collection(COLLECTION_NAME).orderBy("sequence", Query.Direction.DESCENDING).limit(1);
ApiFuture<QuerySnapshot> future = query.get();
List<QueryDocumentSnapshot> documents = null;
try {
documents = future.get().getDocuments();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
for (QueryDocumentSnapshot document : documents) {
// 새 회원에 부여할 sequence 값을, 가장 최근에 가입된 회원보다 1 큰 수로 변경
sequence = Long.parseLong(document.getId()) + 1;
System.out.println("sequence of the last member : " + document.getId());
}
// 만약 데이터베이스가 비어있었다면, sequence의 값은 처음에 초기화한 값인 1L임
// Member 객체에 sequence 값 반영하기
member.setSequence(Long.toString(sequence));
// 데이터베이스에 Member 객체를 저장
ApiFuture<WriteResult> collectionApiFuture = dbFirestore.collection(COLLECTION_NAME).document(member.getSequence()).set(member);
try {
return collectionApiFuture.get().getUpdateTime().toString();
} catch (InterruptedException e) {
e.printStackTrace();
return "memberService failed";
} catch (ExecutionException e) {
e.printStackTrace();
return "memberService failed";
}
}
}
테스트
작성한 코드가 잘 동작하는지, Postman을 사용하여 테스트해보자.
프로젝트를 실행시키고,
아래와 같이 Body에 id, nickname, password 값을 넣어보내주면, 하단에 저장 성공 시각이 문자열로 리턴되는 것을 확인할 수 있다.
파이어베이스 콘솔에 들어가서, 저장된 데이터를 직접 눈으로 확인해보자.
Firebase Database 페이지에 들어가 데이터 탭을 확인해보자.
member 컬렉션이 생성되었고,
1번 회원 문서가 새로 생성되어 그 안에 Postman의 Body 내용과 동일한 필드 값이 잘 저장되었다.
한 번 더 요청을 보내보자.
1번 회원이 존재하기 때문에, 고유 번호 2번을 부여받아 저장되었다.
Firebase Storage에 파일 업로드하기
이제 파이어베이스에 파일을 업로드해보자.
Storage 버킷 생성
Firestore Database와 같은 파이어베이스 프로젝트에 Storage 버킷을 생성한다.
파이어베이스 콘솔에 들어가서, 좌측의 '빌드' 메뉴 안에 있는 Storage를 선택한다.
'시작하기'를 누르면 아래와 같은 화면이 표시된다.
프로덕션 모드에서 시작해도 되지만, 그럼 보안 규칙을 설정해주어야 한다. 그러므로 테스트 모드를 선택하고 다음으로 넘어가자.
단, 테스트 모드를 사용한 지 30일이 경과하면 프로덕션 모드로 전환한 뒤 보안 규칙을 설정해주어야 계속 사용이 가능하다.
'다음'을 누르면 아래와 같이 위치를 설정하는 창이 표시된다.
아무 곳이나 해도 큰 문제는 없지만, 현재 자신이 위치한 곳을 선택하는 것이 권장된다. 한국이 위치한 동북아시아를 선택해주었다.
만약 해외 고객이 주로 사용하는 서비스라면 해당하는 해외 지역을 선택하는 것이 좋을 수 있다.
위치를 고르고 '완료'를 누르면 버킷이 생성된다.
아래는 생성된 저장공간(버킷)의 모습이다.
개요
사용자가 파일 하나를 넘겨주면서 POST 요청을 보내면 그 파일을 Storage에 올리는 코드를 작성할 것이다.
이때, Storage에 이미 있는 파일 중 하나와 파일명이 동일한 파일을 사용자가 업로드하려 할 수 있다.
따라서 파일을 Storage로 전송하기 전에 이름을 바꾸어줄 것이다.
절대 파일명이 중복되는 일이 없도록, 사용자가 요청을 보낸 시각을 YYYYMMddHHmmss 형태의 문자열로 포맷팅하여 파일명으로 사용하자. (1초 이하의 시간차를 두고 두 파일이 업로드되었을 때는 파일명이 중복될 수 있다. 이 점이 걱정된다면 밀리초 단위까지 반영하여 파일 이름을 짓자.)
코드 작성
코드를 작성해주자.
member 패키지와 같은 위치에 photo 패키지를 만들고 그 안에 controller, dto, service 패키지를 만든다.
각각의 패키지 안에 아래의 사진과 같이 클래스들을 생성한다.
일반적으로는 아래와 같이 domain>Photo.java도 만드는 것이 맞다.
그러나 오늘 우리는 보통의 프로젝트를 만들어 작동시키는 게 아니라 파이어베이스 Storage에 이미지를 올려보기만 할 것이다. 사용자가 POST 요청으로 보낸 파일을 그대로 받아 업로드할 것이므로 Domain이 필요없다. 다음에 실제 프로젝트를 만들 때에는 꼭 Domain까지 작성해서 사용하도록 하자.
먼저, 업로드 결과를 한눈에 확인할 수 있게 만들어줄 DTO를 만들어준다.
PhotoUploadResponseDto.java
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PhotoUploadResponseDto {
String fileName; // 파이어베이스 스토리지에 저장된 파일명
String fileLocation; // 파이어베이스 스토리지 상의 파일 위치
public PhotoUploadResponseDto (String fileName, String fileLocation) {
this.fileName = fileName;
this.fileLocation = fileLocation;
}
}
이 DTO는 업로드가 성공적으로 완료되었을 경우 리턴될 값들을 정해주는 역할을 한다.
잘 보면 파일명(fileName), 파일 위치(fileLocation)가 필드로 들어가있다.
즉, 파일 업로드가 성공적으로 완료되면 우리는 리턴 값으로 이 파일이 어떤 이름으로 파이어베이스에 저장되었는지와 어느 위치에 저장되었는지를 바로 읽어볼 수 있게 된다.
이렇게 DTO를 사용하여 개발 및 테스트의 효율을 올릴 수 있다.
다음으로 컨트롤러를 작성한다.
간단하게, 파일을 업로드하는 POST 요청 메소드만 만들어준다.
PhotoController.java
import 프로젝트패키지.photo.dto.PhotoUploadResponseDto;
import 프로젝트패키지.photo.service.PhotoService;
import com.google.firebase.auth.FirebaseAuthException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/api/photos")
public class PhotoController {
@Autowired
private PhotoService photoService;
// 파일 업로드
@PostMapping
@ResponseStatus(value = HttpStatus.OK)
public PhotoUploadResponseDto changeProfile (@RequestParam("file")MultipartFile file) throws IOException, FirebaseAuthException, ExecutionException, InterruptedException {
return photoService.upload(file);
}
}
마지막으로 서비스 코드를 작성한다.
현재 시각을 LocalDateTIme으로 받아와서, YYYYMMddHHmmss 포맷의 문자열로 변환한 뒤,
이 문자열을 파일의 이름으로 지정하여 파이어베이스 Storage에 업로드하는 내용이다.
PhotoService.java
import 프로젝트패키지.photo.dto.PhotoUploadResponseDto;
import com.google.cloud.firestore.*;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Bucket;
import com.google.firebase.auth.FirebaseAuthException;
import com.google.firebase.cloud.StorageClient;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutionException;
@Service
public class PhotoService {
// 사용할 데이터베이스
private static Firestore dbFirestore;
// 파이어베이스 버킷명
private String firebaseBucket = "본인의 버킷 이름";
// 사진 업로드 메소드
public PhotoUploadResponseDto changeProfile(String memberId, MultipartFile file) throws IOException {
// 파일명이 겹치면 안 되므로, 현재시각을 파일명으로 설정
LocalDateTime now = LocalDateTime.now();
String fileName = now.format(DateTimeFormatter.ofPattern("YYYYMMddHHmmss"));
// 버킷 정보 설정
Bucket bucket = StorageClient.getInstance().bucket(firebaseBucket);
InputStream content = new ByteArrayInputStream(file.getBytes());
// 파일 저장
Blob blob = bucket.create(fileName, content, file.getContentType());
// 업로드된 프로필 사진의 정보를 담은 DTO를 리턴
return new PhotoUploadResponseDto(sequence, fileName, blob.getMediaLink());
}
}
여기서 주의할 것이 있다!
private String firebaseBucket 문자열의 값은 본인의 파이어베이스 콘솔에 들어가서 복사해와야 한다.
다음과 같이 가져오면 된다.
파이어베이스 콘솔에 들어가서, Storage 창으로 간다.
들어가면 아래와 같은 화면이 표시되는데, 파일 목록 좌측 상단에 gs:// 로 시작하는 문자열이 하나 있다.
이 문자열에서 gs:// 를 뺀 뒷부분을 그대로 복사해서, private String firebaseBucket의 값으로 지정해주면 된다.
테스트
이제 파일을 실제로 올려봄으로써 테스트를 하자.
Postman을 사용하여 파일 업로드 테스트를 할 수 있다.
Postman을 열어, 지정한 URL을 아래와 같이 적어준다.
그리고 Body 탭을 열어 form-data를 선택한다.
form-data의 표 1행의 Key 값에 file이라고 적는다. PhotoController.java에서 MultipartFile을 file이라는 이름으로 받아오기로 정의하였기 때문에, 꼭 파일의 키값을 file이라고 적어줘야 한다!
여기서 file이 적힌 칸에 마우스를 올려보면, 아래 사진과 같이 Text라고 적힌 드롭다운 버튼이 나타난다.
이 드롭다운을 눌러서, File로 바꾸어주자.
그럼 아래와 같이 Value 칸에 Select Files 버튼이 나타난다.
Select Files 버튼을 눌러서, 본인의 컴퓨터에 있는 파일 중 하나를 선택해준다.
나는 아래와 같이 생긴 사진을 업로드하겠다.
이렇게 해주고, Send를 눌러 POST 요청을 보낸다.
요청을 보낸 후 하단의 결과창을 보면, 우리가 DTO에 정의해준 필드인 fileName과 fileLocation이 표시된다.
파일이 잘 업로드되었다는 뜻이다.
현재 시각인 2023년 5월 16일 21시 47분 9초가 반영된 파일명도 잘 적용되었다.
이제 파이어베이스 콘솔을 열어 Storage에 들어가서, 20230516214709라는 이름의 파일이 정말 업로드되어있는지 확인해보자.
잘 올라가있다!
파일명을 클릭하면 우측에 상세정보가 표시된다. 한 번 클릭해보자.
우리가 올린 사진이 맞다!
사진 출처:
https://unsplash.com/ko/사진/yCl03axiMA4