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_missing과 define_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 테이블이 같으면 OpenStruct는 other와 같다고 판단해요.
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_json과 OpenStruct.json_create 메서드로 OpenStruct 객체를 직렬화·역직렬화할 수 있어요. (Marshal 참고)
OpenStruct#as_json은 self를 직렬화해 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
name과 identifiers로 지정된 중첩 객체에서 객체를 찾아 반환해요. 중첩 객체는 다양한 클래스의 인스턴스일 수 있어요. (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?이면 OpenStruct는 other에 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!
속성 값을 설정하는 내부 헬퍼예요.