데이터 저장

이 문서에서는 Firebase Realtime Database에 데이터를 쓰는 4가지 메서드인 set, update, push, transaction 지원에 대해 설명합니다.

데이터 저장 방법

set 정의된 경로(예: messages/users/<username>)에 데이터를 쓰거나 대체합니다.
update 정의된 경로에서 모든 데이터를 대체하지 않고 일부 키를 업데이트합니다.
push 데이터베이스의 데이터 목록에 추가합니다. 목록에 새 노드를 푸시할 때마다 데이터베이스에서 고유 키(예: messages/users/<unique-user-id>/<username>)를 생성합니다.
트랜잭션 동시 업데이트에 의해 손상될 수 있는 복잡한 데이터를 다루는 경우 트랜잭션을 사용합니다.

데이터 저장

기본 데이터베이스 쓰기 작업은 지정된 데이터베이스 참조에 새 데이터를 저장하고 해당 경로의 기존 데이터를 모두 대체하는 set입니다. set를 이해할 수 있도록 간단한 블로깅 앱을 만들어 보겠습니다. 앱의 데이터는 아래와 같은 데이터베이스 참조에 저장됩니다.

자바
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog");
Node.js
// Import Admin SDK
const { getDatabase } = require('firebase-admin/database');

// Get a database reference to our blog
const db = getDatabase();
const ref = db.ref('server/saving-data/fireblog');
Python
# Import database module.
from firebase_admin import db

# Get a database reference to our blog.
ref = db.reference('server/saving-data/fireblog')
Go
// Create a database client from App.
client, err := app.Database(ctx)
if err != nil {
	log.Fatalln("Error initializing database client:", err)
}

// Get a database reference to our blog.
ref := client.NewRef("server/saving-data/fireblog")

우선 사용자 데이터를 저장해 보겠습니다. 고유한 사용자 이름을 기준으로 각 사용자를 저장하고 성명과 생일을 함께 저장하려고 합니다. 각 사용자는 고유한 사용자 이름을 갖게 되므로 별도의 키를 만들 필요가 없기 때문에 push 메서드 대신 set 메서드를 사용하는 것이 좋습니다.

우선 사용자 데이터를 가리키는 데이터베이스 참조를 만듭니다. 그런 다음 set()/setValue()로 데이터베이스에 사용자 이름, 성명, 생일과 함께 사용자 객체를 저장합니다. 문자열, 숫자, 부울, null, 배열 또는 임의의 JSON 객체를 전달할 수 있습니다. null을 전달하면 지정된 위치에서 데이터가 삭제됩니다. 여기에서는 객체를 전달합니다.

자바
public static class User {

  public String date_of_birth;
  public String full_name;
  public String nickname;

  public User(String dateOfBirth, String fullName) {
    // ...
  }

  public User(String dateOfBirth, String fullName, String nickname) {
    // ...
  }

}

DatabaseReference usersRef = ref.child("users");

Map<String, User> users = new HashMap<>();
users.put("alanisawesome", new User("June 23, 1912", "Alan Turing"));
users.put("gracehop", new User("December 9, 1906", "Grace Hopper"));

usersRef.setValueAsync(users);
Node.js
const usersRef = ref.child('users');
usersRef.set({
  alanisawesome: {
    date_of_birth: 'June 23, 1912',
    full_name: 'Alan Turing'
  },
  gracehop: {
    date_of_birth: 'December 9, 1906',
    full_name: 'Grace Hopper'
  }
});
Python
users_ref = ref.child('users')
users_ref.set({
    'alanisawesome': {
        'date_of_birth': 'June 23, 1912',
        'full_name': 'Alan Turing'
    },
    'gracehop': {
        'date_of_birth': 'December 9, 1906',
        'full_name': 'Grace Hopper'
    }
})
Go
// User is a json-serializable type.
type User struct {
	DateOfBirth string `json:"date_of_birth,omitempty"`
	FullName    string `json:"full_name,omitempty"`
	Nickname    string `json:"nickname,omitempty"`
}

usersRef := ref.Child("users")
err := usersRef.Set(ctx, map[string]*User{
	"alanisawesome": {
		DateOfBirth: "June 23, 1912",
		FullName:    "Alan Turing",
	},
	"gracehop": {
		DateOfBirth: "December 9, 1906",
		FullName:    "Grace Hopper",
	},
})
if err != nil {
	log.Fatalln("Error setting value:", err)
}

JSON 객체가 데이터베이스에 저장될 때 객체 속성은 중첩된 형태로 데이터베이스 하위 위치에 자동으로 매핑됩니다. 이제 https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name URL로 이동하면 'Alan Turing'이라는 값이 표시됩니다. 하위 위치에 데이터를 직접 저장할 수도 있습니다.

자바
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing"));
usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
Node.js
const usersRef = ref.child('users');
usersRef.child('alanisawesome').set({
  date_of_birth: 'June 23, 1912',
  full_name: 'Alan Turing'
});
usersRef.child('gracehop').set({
  date_of_birth: 'December 9, 1906',
  full_name: 'Grace Hopper'
});
Python
users_ref.child('alanisawesome').set({
    'date_of_birth': 'June 23, 1912',
    'full_name': 'Alan Turing'
})
users_ref.child('gracehop').set({
    'date_of_birth': 'December 9, 1906',
    'full_name': 'Grace Hopper'
})
Go
if err := usersRef.Child("alanisawesome").Set(ctx, &User{
	DateOfBirth: "June 23, 1912",
	FullName:    "Alan Turing",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

if err := usersRef.Child("gracehop").Set(ctx, &User{
	DateOfBirth: "December 9, 1906",
	FullName:    "Grace Hopper",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

위의 두 예시, 즉 객체 하나로 두 값을 한 번에 쓰는 예시와 하위 위치에 별도로 값을 쓰는 예시가 데이터베이스에 저장하는 데이터는 결과적으로 동일합니다.

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper"
    }
  }
}

첫 번째 예시에서는 데이터를 감시하는 클라이언트에서 이벤트가 한 번 발생하는 반면 두 번째 예시에서는 이벤트가 두 번 발생합니다. usersRef에 이미 데이터가 있는 경우 첫 번째 방식에서는 데이터를 덮어쓰지만 두 번째 방식에서는 하위 노드 각각의 값만 수정되며 usersRef의 다른 하위 노드는 바뀌지 않는다는 점을 유의해야 합니다.

저장된 데이터 업데이트

데이터베이스 위치에서 다른 하위 노드를 덮어쓰지 않고 여러 하위 노드에 동시에 쓰려면 아래와 같이 update 메서드를 사용합니다.

자바
DatabaseReference hopperRef = usersRef.child("gracehop");
Map<String, Object> hopperUpdates = new HashMap<>();
hopperUpdates.put("nickname", "Amazing Grace");

hopperRef.updateChildrenAsync(hopperUpdates);
Node.js
const usersRef = ref.child('users');
const hopperRef = usersRef.child('gracehop');
hopperRef.update({
  'nickname': 'Amazing Grace'
});
Python
hopper_ref = users_ref.child('gracehop')
hopper_ref.update({
    'nickname': 'Amazing Grace'
})
Go
hopperRef := usersRef.Child("gracehop")
if err := hopperRef.Update(ctx, map[string]interface{}{
	"nickname": "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating child:", err)
}

이렇게 하면 Grace의 데이터가 업데이트되어 별명이 포함됩니다. update 대신 set를 사용하면 hopperRef에서 full_namedate_of_birth 모두가 삭제됩니다.

Firebase Realtime Database는 다중 경로 업데이트도 지원합니다. 다시 말해 update로 데이터베이스의 여러 위치에서 동시에 값을 업데이트할 수 있으며, 이 강력한 기능을 통해 데이터를 비정규화할 수 있습니다. 다중 경로 업데이트를 사용하여 Alan과 Grace의 닉네임을 동시에 추가할 수 있습니다.

자바
Map<String, Object> userUpdates = new HashMap<>();
userUpdates.put("alanisawesome/nickname", "Alan The Machine");
userUpdates.put("gracehop/nickname", "Amazing Grace");

usersRef.updateChildrenAsync(userUpdates);
Node.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome/nickname': 'Alan The Machine',
  'gracehop/nickname': 'Amazing Grace'
});
Python
users_ref.update({
    'alanisawesome/nickname': 'Alan The Machine',
    'gracehop/nickname': 'Amazing Grace'
})
Go
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome/nickname": "Alan The Machine",
	"gracehop/nickname":      "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

이 업데이트에 따라 Alan과 Grace의 닉네임이 추가되었습니다.

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing",
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper",
      "nickname": "Amazing Grace"
    }
  }
}

경로가 포함된 객체를 기록하여 객체를 업데이트하려고 하면 동작이 달라집니다. Grace와 Alan을 다음과 같은 방법으로 업데이트하면 어떻게 되는지 살펴보겠습니다.

자바
Map<String, Object> userNicknameUpdates = new HashMap<>();
userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine"));
userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace"));

usersRef.updateChildrenAsync(userNicknameUpdates);
Node.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome': {
    'nickname': 'Alan The Machine'
  },
  'gracehop': {
    'nickname': 'Amazing Grace'
  }
});
Python
users_ref.update({
    'alanisawesome': {
        'nickname': 'Alan The Machine'
    },
    'gracehop': {
        'nickname': 'Amazing Grace'
    }
})
Go
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome": &User{Nickname: "Alan The Machine"},
	"gracehop":      &User{Nickname: "Amazing Grace"},
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

이렇게 하면 동작이 달라져서 전체 /users 노드를 덮어쓰게 됩니다.

{
  "users": {
    "alanisawesome": {
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "nickname": "Amazing Grace"
    }
  }
}

완료 콜백 추가

Node.js 및 Java Admin SDK에서 데이터가 커밋되는 시점을 파악하려면 완료 콜백을 추가합니다. 이 SDK의 set 및 update 메서드는 모두 쓰기가 데이터베이스에 커밋될 때 호출되는 선택적 완료 콜백을 사용합니다. 특정한 이유로 호출이 실패하면 콜백에 실패 이유를 나타내는 오류 객체가 전달됩니다. Python 및 Go Admin SDK에서 모든 쓰기 메서드는 블로킹 방식입니다. 즉, 쓰기 메서드는 데이터베이스에 쓰기가 커밋될 때까지 반환되지 않습니다.

자바
DatabaseReference dataRef = ref.child("data");
dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() {
  @Override
  public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
    if (databaseError != null) {
      System.out.println("Data could not be saved " + databaseError.getMessage());
    } else {
      System.out.println("Data saved successfully.");
    }
  }
});
Node.js
dataRef.set('I\'m writing data', (error) => {
  if (error) {
    console.log('Data could not be saved.' + error);
  } else {
    console.log('Data saved successfully.');
  }
});

데이터 목록 저장

데이터 목록을 만들 때 대부분의 애플리케이션은 여러 사용자를 관리해야 한다는 점을 염두에 두고 목록 구조를 적절히 조정해야 합니다. 이제 위 예시를 확장하여 앱에 블로그 게시물을 추가해 보겠습니다. 다음과 같이 자동으로 증가하는 정수 색인과 함께 set를 사용하여 하위 요소를 저장하는 방법이 가장 먼저 떠오를 것입니다.

// NOT RECOMMENDED - use push() instead!
{
  "posts": {
    "0": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "1": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

사용자가 새 게시물을 추가하면 /posts/2로 저장됩니다. 게시자가 1명이면 문제가 없지만, 수많은 사용자가 접속하는 블로깅 애플리케이션에서는 여러 사용자가 동시에 게시물을 추가할 수 있습니다. 2명의 사용자가 동시에 /posts/2에 기록하면 한 쪽 게시물이 다른 한 쪽을 삭제합니다.

이 문제를 해결하기 위해 Firebase 클라이언트는 새 하위 요소마다 고유 키를 생성하는 push() 함수를 제공합니다. 고유 하위 키를 사용하면 여러 클라이언트에서 쓰기 충돌에 대한 걱정 없이 동시에 같은 위치에 하위 요소를 추가할 수 있습니다.

자바
public static class Post {

  public String author;
  public String title;

  public Post(String author, String title) {
    // ...
  }

}

DatabaseReference postsRef = ref.child("posts");

DatabaseReference newPostRef = postsRef.push();
newPostRef.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language"));

// We can also chain the two calls together
postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
Node.js
const newPostRef = postsRef.push();
newPostRef.set({
  author: 'gracehop',
  title: 'Announcing COBOL, a New Programming Language'
});

// we can also chain the two calls together
postsRef.push().set({
  author: 'alanisawesome',
  title: 'The Turing Machine'
});
Python
posts_ref = ref.child('posts')

new_post_ref = posts_ref.push()
new_post_ref.set({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})

# We can also chain the two calls together
posts_ref.push().set({
    'author': 'alanisawesome',
    'title': 'The Turing Machine'
})
Go
// Post is a json-serializable type.
type Post struct {
	Author string `json:"author,omitempty"`
	Title  string `json:"title,omitempty"`
}

postsRef := ref.Child("posts")

newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

if err := newPostRef.Set(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

// We can also chain the two calls together
if _, err := postsRef.Push(ctx, &Post{
	Author: "alanisawesome",
	Title:  "The Turing Machine",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

고유 키는 타임스탬프 기준이므로 목록 항목이 시간순으로 자동 정렬됩니다. Firebase에서는 각 블로그 게시물에 대한 고유 키가 생성되므로 여러 사용자가 동시에 게시물을 추가해도 쓰기 충돌이 발생하지 않습니다. 이제 데이터베이스의 데이터는 다음과 같습니다.

{
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "-JRHTHaKuITFIhnj02kE": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

자바스크립트, Python, Go에서는 push()set()을 연달아 호출하는 패턴이 흔히 나타나므로 Firebase SDK에서 다음과 같이 설정할 데이터를 push()에 직접 전달하여 두 메서드를 결합할 수 있습니다.

자바
// No Java equivalent
Node.js
// This is equivalent to the calls to push().set(...) above
postsRef.push({
  author: 'gracehop',
  title: 'Announcing COBOL, a New Programming Language'
});;
Python
# This is equivalent to the calls to push().set(...) above
posts_ref.push({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})
Go
if _, err := postsRef.Push(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

push()가 생성한 고유 키 가져오기

push()를 호출하면 새 데이터 경로에 대한 참조가 반환되고, 이 참조로 키를 가져오거나 데이터를 설정할 수 있습니다. 다음 코드는 위 예시와 동일한 데이터를 반환하지만 이제 생성된 고유 키에 액세스할 수 있습니다.

자바
// Generate a reference to a new location and add some data using push()
DatabaseReference pushedPostRef = postsRef.push();

// Get the unique ID generated by a push()
String postId = pushedPostRef.getKey();
Node.js
// Generate a reference to a new location and add some data using push()
const newPostRef = postsRef.push();

// Get the unique key generated by push()
const postId = newPostRef.key;
Python
# Generate a reference to a new location and add some data using push()
new_post_ref = posts_ref.push()

# Get the unique key generated by push()
post_id = new_post_ref.key
Go
// Generate a reference to a new location and add some data using Push()
newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

// Get the unique key generated by Push()
postID := newPostRef.Key

보다시피 push() 참조에서 고유 키 값을 가져올 수 있습니다.

데이터 검색에 대한 다음 섹션에서는 Firebase 데이터베이스에서 이 데이터를 읽는 방법을 알아봅니다.

트랜잭션 데이터 저장

증분 카운터와 같이 동시 수정으로 인해 손상될 수 있는 복잡한 데이터를 다루는 경우를 위해 SDK에서 트랜잭션 작업이 제공됩니다.

자바 및 Node.js에서는 트랜잭션 작업에 2가지 콜백, 즉 업데이트 함수 및 선택적 완료 콜백을 지정할 수 있습니다. Python 및 Go에서는 트랜잭션 작업이 블로킹 방식이므로 업데이트 함수만 취합니다.

업데이트 함수는 데이터의 현재 상태를 인수로 취하고 이 데이터에 새로 기록하려는 상태를 반환합니다. 예를 들어 특정 블로그 게시물의 추천 수를 증가시키려는 경우 다음과 같은 트랜잭션을 작성할 수 있습니다.

자바
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes");
upvotesRef.runTransaction(new Transaction.Handler() {
  @Override
  public Transaction.Result doTransaction(MutableData mutableData) {
    Integer currentValue = mutableData.getValue(Integer.class);
    if (currentValue == null) {
      mutableData.setValue(1);
    } else {
      mutableData.setValue(currentValue + 1);
    }

    return Transaction.success(mutableData);
  }

  @Override
  public void onComplete(
      DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) {
    System.out.println("Transaction completed");
  }
});
Node.js
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes');
upvotesRef.transaction((current_value) => {
  return (current_value || 0) + 1;
});
Python
def increment_votes(current_value):
    return current_value + 1 if current_value else 1

upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes')
try:
    new_vote_count = upvotes_ref.transaction(increment_votes)
    print('Transaction completed')
except db.TransactionAbortedError:
    print('Transaction failed to commit')
Go
fn := func(t db.TransactionNode) (interface{}, error) {
	var currentValue int
	if err := t.Unmarshal(&currentValue); err != nil {
		return nil, err
	}
	return currentValue + 1, nil
}

ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes")
if err := ref.Transaction(ctx, fn); err != nil {
	log.Fatalln("Transaction failed to commit:", err)
}

위의 예시에서는 기본값이 기록되지 않은 null 상태에서 트랜잭션이 호출될 수도 있으므로 카운터가 null이거나 아직 증분되지 않았는지를 확인합니다.

트랜잭션 함수 없이 위 코드를 실행하면 두 클라이언트에서 동시에 값을 증분시키려고 할 때 두 경우 모두 새 값으로 1이 기록되어 결과적으로 2가 아닌 1만 증가합니다.

네트워크 연결 및 오프라인 쓰기

Firebase Node.js 및 자바 클라이언트는 활성 데이터의 내부 버전을 유지합니다. 데이터를 쓰면 우선 로컬 버전에 기록됩니다. 그런 다음 클라이언트가 해당 데이터를 데이터베이스 및 다른 클라이언트에 '최선을 다해' 동기화합니다.

이와 같이 데이터베이스에 대한 모든 쓰기 작업은 로컬 이벤트를 즉시 발생시키며, 그 이후에 데이터베이스에 데이터가 기록됩니다. 따라서 Firebase를 사용하여 애플리케이션을 개발하면 네트워크 지연 시간 또는 인터넷 연결 여부에 관계없이 앱이 원활하게 작동합니다.

네트워크에 다시 연결되면 해당 이벤트 조합이 수신되어 클라이언트가 현재 서버 상태를 따라잡으므로 맞춤 코드를 별도로 작성할 필요가 없습니다.

데이터 보안

Firebase Realtime Database는 데이터의 서로 다른 노드에 읽기 및 쓰기 권한을 갖는 사용자를 정의하는 보안 언어를 제공합니다. 자세한 내용은 데이터 보안을 참조하세요.