PStore 클래스

PStore 클래스

PStore는 Hash 기반의 파일 영속 메커니즘을 구현해요. 사용자 코드는 데이터 스토어에 Ruby 객체(값)의 계층을 이름(키)으로 저장할 수 있어요. 객체 계층이 단일 객체일 수도 있죠. 나중에 스토어에서 값을 다시 읽거나 필요에 따라 데이터를 갱신할 수도 있어요.

출처: Ruby 3.3 API

본문

트랜잭션 동작은 어떤 변경이든 모두 함께 성공하거나 모두 함께 실패하게 보장해요. 이를 통해 일부 값은 갱신됐는데 다른 값은 안 된 중간 상태로 데이터 스토어가 남는 것을 막을 수 있어요.

내부적으로 Ruby 객체는 Marshal로 데이터 스토어 파일에 저장돼요. 그에 따른 일반적인 제한이 따르죠. 예를 들어 Proc 객체는 marshal될 수 없어요.

여기 중요한 개념이 세 가지 있어요(자세한 내용은 링크 참고):

  • Store: store는 PStore의 인스턴스예요.
  • Entries: store는 hash처럼 동작해요. 각 엔트리는 저장된 객체의 키예요.
  • Transactions: 각 트랜잭션은 store에 대한 예정 변경들의 모음이에요. 트랜잭션은 PStore#transaction 호출과 함께 주어진 블록 안에서 정의돼요.

예시에 대하여 (About the Examples)

이 페이지의 예시는 알려진 속성을 가진 store가 필요해요. 다음과 같이 호출해 새(그리고 채워진) store를 얻을 수 있어요.

example_store do |store|
  # Example code using store goes here.
end

example_store에 대해 우리가 알 필요가 있는 건 알려진 엔트리 집합을 가진 새 store를 넘겨준다는 것뿐이에요. 그 구현은:

require 'pstore'
require 'tempfile'
# Yield a pristine store for use in examples.
def example_store
  # Create the store in a temporary file.
  Tempfile.create do |file|
    store = PStore.new(file)
    # Populate the store.
    store.transaction do
      store[:foo] = 0
      store[:bar] = 1
      store[:baz] = 2
    end
    yield store
  end
end

Store

store의 내용물은 store가 생성될 때 지정된 경로의 파일에 유지돼요(PStore.new 참고). 객체는 Marshal 모듈로 저장·검색되는데, 이는 특정 객체는 store에 추가될 수 없다는 뜻이에요(Marshal::dump 참고).

Entries

store는 몇 개든 엔트리를 가질 수 있어요. 각 엔트리는 hash에서처럼 키와 값을 가져요.

  • Key: hash에서처럼 키는 (거의) 어떤 객체든 될 수 있어요(Hash Keys 참고). 키를 symbol이나 string만 쓰는 식으로 단순하게 유지하면 편할 거예요.
  • Value: 값은 Marshal(의 Marshal::dump)이 marshal할 수 있는 어떤 객체든 될 수 있고, 실제로 컬렉션(예: array, hash, set, range 등)일 수 있어요. 그 컬렉션은 어떤 깊이든 중첩 객체(컬렉션 포함)를 담을 수 있고, 그 객체들도 Marshal 가능해야 해요(Hierarchical Values 참고).

Transactions

트랜잭션 블록 (The Transaction Block)

transaction 메서드 호출과 함께 주어진 블록은 store에서 읽거나 쓰는 PStore 메서드 호출들(transaction 자신, path, Pstore.new를 제외한 모든 PStore 메서드)로 구성된 트랜잭션을 담아요.

example_store do |store|
  store.transaction do
    store.keys # => [:foo, :bar, :baz]
    store[:bat] = 3
    store.keys # => [:foo, :bar, :baz, :bat]
  end
end

트랜잭션의 실행은 블록이 끝날 때까지 지연되고, 원자적(all-or-nothing)으로 실행돼요. 트랜잭션 호출이 모두 실행되거나 아무것도 실행되지 않아요. 이게 store의 무결성을 유지해요.

블록 안의 다른 코드(pathPStore.new 호출까지 포함)는 지연되지 않고 즉시 실행돼요.

트랜잭션 블록은:

  • 중첩된 transaction 호출을 포함할 수 없어요.
  • store에서 읽거나 쓰는 메서드가 허용되는 유일한 장소예요.

위에서 봤듯이 트랜잭션의 변경은 블록이 끝날 때 자동으로 적용돼요. 블록은 commit이나 abort 메서드를 호출해 일찍 끝낼 수 있어요.

  • commit 메서드는 store에 대한 갱신을 적용하고 블록을 끝내요.
example_store do |store|
  store.transaction do
    store.keys # => [:foo, :bar, :baz]
    store[:bat] = 3
    store.commit
    fail 'Cannot get here'
  end
  store.transaction do
    # Update was completed.
    store.keys # => [:foo, :bar, :baz, :bat]
  end
end
  • abort 메서드는 store에 대한 갱신을 버리고 블록을 끝내요.
example_store do |store|
  store.transaction do
    store.keys # => [:foo, :bar, :baz]
    store[:bat] = 3
    store.abort
    fail 'Cannot get here'
  end
  store.transaction do
    # Update was not completed.
    store.keys # => [:foo, :bar, :baz]
  end
end

읽기 전용 트랜잭션 (Read-Only Transactions)

기본적으로 트랜잭션은 store에서 읽기와 쓰기를 모두 허용해요.

store.transaction do
  # Read-write transaction.
  # Any code except a call to #transaction is allowed here.
end

인자 read_onlytrue로 주어지면 읽기만 허용돼요.

store.transaction(true) do
  # Read-only transaction:
  # Calls to #transaction, #[]=, and #delete are not allowed here.
end

계층적 값 (Hierarchical Values)

엔트리의 값은 단순 객체(위에서 봤듯이)일 수도 있고, 어떤 깊이든 중첩된 객체 계층일 수도 있어요.

deep_store = PStore.new('deep.store')
deep_store.transaction do
  array_of_hashes = [{}, {}, {}]
  deep_store[:array_of_hashes] = array_of_hashes
  deep_store[:array_of_hashes] # => [{}, {}, {}]
  hash_of_arrays = {foo: [], bar: [], baz: []}
  deep_store[:hash_of_arrays] = hash_of_arrays
  deep_store[:hash_of_arrays]  # => {:foo=>[], :bar=>[], :baz=>[]}
  deep_store[:hash_of_arrays][:foo].push(:bat)
  deep_store[:hash_of_arrays]  # => {:foo=>[:bat], :bar=>[], :baz=>[]}
end

반환된 객체 계층에서는 dig 메서드를 쓸 수 있다는 점도 기억해 두세요.

Store 다루기 (Working with the Store)

Store 만들기 (Creating a Store)

PStore.new 메서드로 store를 만들어요. 새 store는 자신이 담길 파일을 만들거나 열어요.

store = PStore.new('t.store')

Store 수정하기 (Modifying the Store)

[]= 메서드로 엔트리를 갱신하거나 만들어요.

example_store do |store|
  store.transaction do
    store[:foo] = 1 # Update.
    store[:bam] = 1 # Create.
  end
end

delete 메서드로 엔트리를 제거해요.

example_store do |store|
  store.transaction do
    store.delete(:foo)
    store[:foo] # => nil
  end
end

값 가져오기 (Retrieving Values)

fetch 메서드(기본값 허용)나 [](기본값 nil)로 엔트리를 가져와요.

example_store do |store|
  store.transaction do
    store[:foo]             # => 0
    store[:nope]            # => nil
    store.fetch(:baz)       # => 2
    store.fetch(:nope, nil) # => nil
    store.fetch(:nope)      # Raises exception.
  end
end

Store 질의하기 (Querying the Store)

key? 메서드로 주어진 키가 존재하는지 판단해요.

example_store do |store|
  store.transaction do
    store.key?(:foo) # => true
  end
end

keys 메서드로 키를 가져와요.

example_store do |store|
  store.transaction do
    store.keys # => [:foo, :bar, :baz]
  end
end

path 메서드로 store의 밑 파일 경로를 가져와요. 이 메서드는 트랜잭션 블록 밖에서 호출할 수 있어요.

store = PStore.new('t.store')
store.path # => "t.store"

트랜잭션 안전 (Transaction Safety)

트랜잭션 안전에 대해선 다음을 보세요.

  • PStore.new 메서드의 선택 인자 thread_safe.
  • 속성 ultra_safe.

말할 것도 없이 PStore로 귀중한 데이터를 저장한다면, PStore 파일을 가끔 백업해야 해요.

예시 store (An Example Store)

require "pstore"

# A mock wiki object.
class WikiPage

  attr_reader :page_name

  def initialize(page_name, author, contents)
    @page_name = page_name
    @revisions = Array.new
    add_revision(author, contents)
  end

  def add_revision(author, contents)
    @revisions << {created: Time.now,
                   author: author,
                   contents: contents}
  end

  def wiki_page_references
    [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/)
  end

end

# Create a new wiki page.
home_page = WikiPage.new("HomePage", "James Edward Gray II",
                         "A page about the JoysOfDocumentation..." )

wiki = PStore.new("wiki_pages.pstore")
# Update page data and the index together, or not at all.
wiki.transaction do
  # Store page.
  wiki[home_page.page_name] = home_page
  # Create page index.
  wiki[:wiki_index] ||= Array.new
  # Update wiki index.
  wiki[:wiki_index].push(*home_page.wiki_page_references)
end

# Read wiki data, setting argument read_only to true.
wiki.transaction(true) do
  wiki.keys.each do |key|
    puts key
    puts wiki[key]
  end
end

Attributes

ultra_safe

PStore가 드물게 일어나는 에러(메모리 에러나 파일시스템 에러 같은)가 발생해도 파일 손상을 최대한 막아야 하는지 여부예요.

  • true: 임시 파일을 만들고 갱신된 데이터를 그 파일에 쓴 뒤, 그 파일을 주어진 경로로 이름 바꾸는 방식으로 변경을 반영해요. 파일 무결성이 유지돼요. 주의: 파일시스템이 원자적 파일 이름 바꾸기를 지원할 때만 효과가 있어요(POSIX 플랫폼인 Linux, MacOS, FreeBSD 등).
  • false(기본값): 열린 파일을 되감고 갱신된 데이터를 쓰는 방식으로 변경을 반영해요. 파일시스템이 예상치 못한 I/O 에러를 내지 않으면 파일 무결성이 유지돼요. 그런 에러가 store 쓰기 중 발생하면 파일이 손상될 수 있어요.

Public Class Methods

new (file, thread_safe = false)

새 PStore 객체를 반환해요.

인자 file은 객체가 저장될 파일의 경로예요. 파일이 존재한다면 PStore가 쓴 파일이어야 해요.

path = 't.store'
store = PStore.new(path)

PStore 객체는 재진입 가능해요. 인자 thread_safetrue로 주어지면 객체는 스레드 안전해져요(작은 성능 대가가 있지만).

store = PStore.new(path, true)

Public Instance Methods

[] (key)

주어진 key가 존재하면 그 값을 반환해요. 그렇지 않으면 nil을 반환해요. nil이 아니라면 반환값은 객체 또는 객체 계층이에요.

example_store do |store|
  store.transaction do
    store[:foo]  # => 0
    store[:nope] # => nil
  end
end

그런 키가 없으면 nil을 반환해요. Hierarchical Values도 참고해요.

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

[]= (key, value)

주어진 key의 값을 만들거나 교체해요.

example_store do |store|
  store.transaction do
    store[:bat] = 3
  end
end

Hierarchical Values도 참고해요.

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

abort ()

현재 트랜잭션 블록을 끝내고, 트랜잭션 블록에서 지정된 변경을 버려요.

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

commit ()

현재 트랜잭션 블록을 끝내고, 트랜잭션 블록에서 지정된 변경을 커밋해요.

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

delete (key)

key의 값이 존재하면 제거하고 반환해요.

example_store do |store|
  store.transaction do
    store[:bat] = 3
    store.delete(:bat)
  end
end

그런 키가 없으면 nil을 반환해요.

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

fetch (key, default=PStore::Error)

[]와 비슷하지만 store의 기본값을 받아들여요. key가 존재하지 않으면:

  • defaultPStore::Error이면 예외를 던져요.
  • 그 외에는 default의 값을 반환해요.
example_store do |store|
  store.transaction do
    store.fetch(:nope, nil) # => nil
    store.fetch(:nope)      # Raises an exception.
  end
end

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

key? (key)

key가 존재하면 true, 존재하지 않으면 false를 반환해요.

example_store do |store|
  store.transaction do
    store.key?(:foo) # => true
  end
end

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

keys ()

존재하는 키의 배열을 반환해요.

example_store do |store|
  store.transaction do
    store.keys # => [:foo, :bar, :baz]
  end
end

트랜잭션 블록 밖에서 호출하면 예외를 던져요.

path ()

store를 만드는 데 사용된 문자열 파일 경로를 반환해요.

store.path # => "flat.store"

root? (key)

주어진 key가 루트로 존재하는지 여부를 반환해요.

roots ()

모든 루트 키의 배열을 반환해요.

transaction (read_only = false) { |pstore| ... }

store의 트랜잭션 블록을 열어요. Transactions 참고.

read_onlyfalse이면 블록은 store에서 읽기와 쓰기를 모두 할 수 있어요.

read_onlytrue이면 블록은 transaction, []=, delete 호출을 포함할 수 없어요.

트랜잭션 블록 안에서 호출하면 예외를 던져요.

Private Instance Methods

in_transaction ()

호출 코드가 PStore#transaction 안에 없으면 PStore::Error를 던져요.

in_transaction_wr ()

호출 코드가 PStore#transaction 안에 없거나, 읽기 전용 PStore#transaction 안에 있으면 PStore::Error를 던져요.