OpenStruct 클래스

OpenStruct 클래스

OpenStruct는 Hash와 비슷한 데이터 구조로, 원하는 속성과 그 값을 자유롭게 정의할 수 있게 해 줘요. Ruby의 메타프로그래밍으로 클래스 자체에 메서드를 정의하는 방식으로 동작해요.

출처: Ruby 3.3 API

본문

예시 (Examples)

require "ostruct"

person = OpenStruct.new
person.name = "John Smith"
person.age  = 70

person.name      # => "John Smith"
person.age       # => 70
person.address   # => nil

OpenStruct는 내부적으로 Hash를 써서 속성과 값을 저장하며, Hash로 초기화할 수도 있어요.

australia = OpenStruct.new(:country => "Australia", :capital => "Canberra")
  # => #<OpenStruct country="Australia", capital="Canberra">

공백이나 메서드 호출에 보통 쓸 수 없는 문자(예: ()[]*)가 있는 Hash 키는 OpenStruct 객체에서 메서드로 바로 조회/할당할 수 없어요. 하지만 Object#send[]로는 접근할 수 있어요.

measurements = OpenStruct.new("length (in inches)" => 24)
measurements[:"length (in inches)"]       # => 24
measurements.send("length (in inches)")   # => 24

message = OpenStruct.new(:queued? => true)
message.queued?                           # => true
message.send("queued?=", false)
message.queued?                           # => false

속성의 존재를 제거하려면 delete_field 메서드를 실행해야 해요. 프로퍼티 값을 nil로 설정한다고 속성이 제거되지는 않거든요.

first_pet  = OpenStruct.new(:name => "Rowdy", :owner => "John Smith")
second_pet = OpenStruct.new(:name => "Rowdy")

first_pet.owner = nil
first_pet                 # => #<OpenStruct name="Rowdy", owner=nil>
first_pet == second_pet   # => false

first_pet.delete_field(:owner)
first_pet                 # => #<OpenStruct name="Rowdy">
first_pet == second_pet   # => true

Ractor 호환성: shareable한 값을 가진 frozen OpenStruct는 그 자체로 shareable이에요.

주의사항 (Caveats)

OpenStruct는 프로퍼티를 위한 메서드를 찾고 정의할 때 Ruby의 메서드 탐색 구조를 사용해요. method_missingdefine_singleton_method를 통해 이뤄져요.

만들어지는 객체의 성능이 걱정된다면 이 점을 고려해야 해요. 이 속성들을 설정하는 데는 Hash나 Struct를 쓰는 것보다 훨씬 많은 오버헤드가 있어요. 작은 Hash에서 OpenStruct를 만들어 몇 개 항목에 접근하는 것이 Hash를 직접 접근하는 것보다 200배 느릴 수 있어요.

이것은 잠재적 보안 이슈이기도 해요. 신뢰할 수 없는 사용자 데이터(예: JSON 웹 요청)로부터 OpenStruct를 만들면, 키가 메서드를 만들고 메서드 이름은 가비지 컬렉션되지 않기 때문에 "symbol denial of service" 공격에 취약할 수 있어요.

또 Ruby 버전 사이에 비호환이 생길 수 있어요.

o = OpenStruct.new
o.then # => nil in Ruby < 2.6, enumerator for Ruby >= 2.6

내장 메서드가 이렇게 덮어써질 수 있고, 이는 버그나 보안 이슈의 원인이 될 수 있어요.

o = OpenStruct.new
o.methods # => [:to_h, :marshal_load, :marshal_dump, :each_pair, ...
o.methods = [:foo, :bar]
o.methods # => [:foo, :bar]

충돌을 줄이기 위해 OpenStruct!로 끝나는 protected/private 메서드만 사용하고, 내장 public 메서드에는 !를 붙인 별칭을 정의해요.

o = OpenStruct.new(make: 'Bentley', class: :luxury)
o.class # => :luxury
o.class! # => OpenStruct

!로 끝나는 필드를 쓰지 않는 것이 권장되지만(강제는 아니에요) 절대 덮어써지지 않아요. 하위클래스의 메서드는 덮어써질 수 없고, OpenStruct 자신의 !로 끝나는 메서드도 마찬가지란 점을 참고하세요.

이 모든 이유 때문에, OpenStruct를 아예 쓰지 않는 것도 고려해 볼 만해요.

상수 (Constants)

  • HAS_PERFORMANCE_WARNINGS

Public Class Methods

json_create (object)

as_json을 참고해요.

new (hash=nil)

새 OpenStruct 객체를 만들어요. 기본적으로 결과 OpenStruct 객체는 속성이 없어요.

선택 hash(Hash이거나 OpenStruct, Struct일 수 있어요)가 주어지면 속성과 값을 생성해요. 예:

require "ostruct"
hash = { "country" => "Australia", :capital => "Canberra" }
data = OpenStruct.new(hash)

data   # => #<OpenStruct country="Australia", capital="Canberra">

Public Instance Methods

== (other)

이 객체와 other를 비교해요. other가 OpenStruct이고 두 객체의 Hash 테이블이 같으면 OpenStructother와 같다고 판단해요.

require "ostruct"
first_pet  = OpenStruct.new("name" => "Rowdy")
second_pet = OpenStruct.new(:name  => "Rowdy")
third_pet  = OpenStruct.new("name" => "Rowdy", :age => nil)

first_pet == second_pet   # => true
first_pet == third_pet    # => false

ostruct[name] → object

속성의 값을 반환하고, 그런 속성이 없으면 nil을 반환해요.

require "ostruct"
person = OpenStruct.new("name" => "John Smith", "age" => 70)
person[:age]   # => 70, same as person.age

ostruct[name] = obj → obj

속성의 값을 설정해요.

require "ostruct"
person = OpenStruct.new("name" => "John Smith", "age" => 70)
person[:age] = 42   # equivalent to person.age = 42
person.age          # => 42

as_json (*)

OpenStruct#as_jsonOpenStruct.json_create 메서드로 OpenStruct 객체를 직렬화·역직렬화할 수 있어요. (Marshal 참고)

OpenStruct#as_jsonself를 직렬화해 self를 나타내는 2원소 Hash를 반환해요.

require 'json/add/ostruct'
x = OpenStruct.new('name' => 'Rowdy', :age => nil).as_json
# => {"json_class"=>"OpenStruct", "t"=>{:name=>'Rowdy', :age=>nil}}

JSON.create 메서드는 그런 Hash를 역직렬화해 OpenStruct 객체를 반환해요.

OpenStruct.json_create(x)
# => #<OpenStruct name='Rowdy', age=nil>

delete_field (name) { || ... }

객체에서 지정한 필드를 제거하고, 그 필드가 정의돼 있었으면 그 필드가 담고 있던 값을 반환해요. 블록을 선택적으로 제공할 수 있어요. 필드가 정의돼 있지 않으면 블록의 결과를 반환하고, 블록이 주어지지 않았다면 NameError를 던져요.

require "ostruct"

person = OpenStruct.new(name: "John", age: 70, pension: 300)

person.delete_field!("age")  # => 70
person                       # => #<OpenStruct name="John", pension=300>

값을 nil로 설정하는 것은 속성을 제거하지 않아요.

person.pension = nil
person                 # => #<OpenStruct name="John", pension=nil>

person.delete_field('number')  # => NameError

person.delete_field('number') { 8675_309 } # => 8675309

dig(name, *identifiers) → object

nameidentifiers로 지정된 중첩 객체에서 객체를 찾아 반환해요. 중첩 객체는 다양한 클래스의 인스턴스일 수 있어요. (Dig Methods 참고)

예시:

require "ostruct"
address = OpenStruct.new("city" => "Anytown NC", "zip" => 12345)
person  = OpenStruct.new("name" => "John Smith", "address" => address)
person.dig(:address, "zip") # => 12345
person.dig(:business_address, "zip") # => nil

each_pair {|name, value| block } → ostruct

모든 속성(symbol로서)과 그에 대응하는 값을 넘겨주고, 블록이 없으면 enumerator를 반환해요.

require "ostruct"
data = OpenStruct.new("country" => "Australia", :capital => "Canberra")
data.each_pair.to_a   # => [[:country, "Australia"], [:capital, "Canberra"]]

eql? (other)

이 객체와 other를 비교해요. other가 OpenStruct이고 두 객체의 Hash 테이블이 eql?이면 OpenStructother에 eql?이에요.

freeze ()

객체를 동결해요.

inspect ()

키와 값의 상세 요약을 담은 문자열을 반환해요.

to_h (&block)

속성을 Hash로 변환해요.

to_json (*args)

self를 나타내는 JSON 문자열을 반환해요.

require 'json/add/ostruct'
puts OpenStruct.new('name' => 'Rowdy', :age => nil).to_json

출력:

{"json_class":"OpenStruct","t":{'name':'Rowdy',"age":null}}

to_s ()

문자열 표현을 반환해요.

Private Instance Methods

set_ostruct_member_value!

속성 값을 설정하는 내부 헬퍼예요.