Ruby에서 관련된 데이터를 묶어서 다루고 싶을 때, 꼭 클래스를 새로 정의해야 할까요? 이럴 때 유용하게 쓸 수 있는 것이 바로 Struct와 OpenStruct입니다. 이 글에서는 두 클래스의 사용법, 장단점, 그리고 실무에서 알아야 할 주의사항까지 자세히 살펴보겠습니다.
Ruby의 Struct란 무엇인가?
Struct는 Ruby에 내장된 클래스로, 값 객체(value object)를 생성하는 새로운 클래스를 만들 때 사용합니다. 값 객체란 서로 연관된 속성들을 하나로 묶어 저장하는 객체를 말합니다.
예를 들어, 좌표 x와 y 두 개의 값을 가지는 Point 객체를 생각해 봅시다.
이런 데이터는 여러 가지 방식으로 표현할 수 있습니다.
- 배열:
[10, 20] - 해시:
{ x: 10, y: 20 } - 객체:
Point.new(10, 20)
Point 객체를 여러 개 다뤄야 한다면 객체 방식을 사용하는 것이 좋은 습관입니다.
하지만 문제가 있습니다.
두 값을 함께 저장하기 위해 일일이 클래스 전체를 정의하고 싶지는 않겠죠? 바로 이럴 때 Struct가 해결책이 됩니다.
Ruby에서 Struct 생성하기
Struct는 new 메서드를 호출하면서 심볼 목록을 인자로 전달하여 생성할 수 있습니다. 전달한 심볼들은 해당 클래스의 인스턴스 변수가 되며, 읽기와 쓰기를 위한 접근자(accessor) 메서드가 기본적으로 정의됩니다.
예시:
Person = Struct.new(:name, :age, :gender)
이제 다음과 같이 객체를 생성할 수 있습니다:
john = Person.new "john", 30, "M" david = Person.new "david", 25, "M"
다른 생성 방식
실제 코드에서는 아래와 같은 방식으로 Struct를 생성하는 경우도 볼 수 있습니다.
class Person < Struct.new(:name, :age, :gender) end
개인적으로는 이 방식을 추천하지 않습니다. 하지만 어딘가에서 마주쳤을 때 어떤 의미인지는 알아두면 도움이 됩니다.
Struct의 큰 장점: 값 비교
Struct의 좋은 점 중 하나는 속성 값을 기준으로 객체를 직접 비교할 수 있다는 것입니다.
예시:
john == john # true
만약 일반적인 Ruby 객체였다면 == 메서드를 직접 정의해야 했을 겁니다. 이것이 바로 우리가 말하는 '값 객체(value object)'입니다.
Ruby Struct 활용 방법
배열이나 해시 대신 Struct를 사용하면 얻을 수 있는 큰 이점 중 하나는 멤버에 메서드 형태로 접근할 수 있다는 점입니다.
예를 들어:
puts john.age # 30 puts david.gender # "M"
덕분에 객체 배열을 다룰 때 max, select, sum 같은 메서드를 편리하게 활용할 수 있습니다.
예시:
[john, david].max_by(&:age)
정말 깔끔하죠!
주의: Struct는 예상치 못한 동작을 할 수 있습니다
Struct로 생성한 클래스에는 한 가지 함정이 있는데요, 바로 생성자에 전달되는 인자의 개수를 검증하지 않는다는 점입니다.
설명해 드리겠습니다. 일반적인 클래스라면 다음과 같은 에러가 발생합니다:
ArgumentError: wrong number of arguments (0 for 3)
하지만 Struct를 사용하면 누락된 인자는 nil로 채워집니다:
Person.new("peter")
# struct Person name="peter", age=nil, gender=nil
Struct 객체를 다룰 때는 이 점을 반드시 기억하세요!
또 다른 특이 사항
다음 코드를 살펴보세요.
Struct.new(:a).ancestors [#<Class:0x29b1040>, Struct, Enumerable, Object, Kernel, BasicObject]
#<Class:0x29b1040>은 Struct에 추가되는 익명 클래스입니다. 또한 Enumerable 모듈이 포함되어 있어 each, map 같은 메서드를 호출할 수 있다는 점도 눈여겨볼 만합니다.
Ruby 2.5의 키워드 인자 지원
파일에서 로그 라인을 읽고 있고, 각 줄이 하나의 항목을 나타낸다고 가정해 봅시다.
예시:
200 /login 18:00 404 /bacon 18:03 200 /books 18:04
커스텀 클래스를 만들면 이 데이터를 더 쉽게 다룰 수 있습니다.
다음과 같이 말이죠:
LogEntry = Struct.new(:status, :url, :time) LogEntry.new(200, '/books', '18:04')
그런데 각 인자가 무엇을 의미하는지 더 명확하게 표현하고 싶다면 키워드 인자(keyword arguments)를 사용하는 게 좋습니다.
좋은 소식이 있습니다!
Ruby 2.5부터 Struct 객체에서 키워드 인자를 지원합니다.
사용 방법:
LogEntry = Struct.new(:status, :url, :time, keyword_init: true) LogEntry.new(status: 200, url: '/books', time: '18:04')
이제 파일을 파싱해서 LogEntry 객체로 변환할 수 있습니다!
OpenStruct 사용 방법
일회성 객체만 필요하다면 OpenStruct를 고려해 보세요.
코드 예시:
require 'ostruct' cat = OpenStruct.new(color: 'black') puts cat.class puts cat.color
이 클래스를 사용하려면 ostruct를 먼저 require해야 한다는 점에 유의하세요.
경고: OpenStruct는 성능이 느려서 프로덕션 애플리케이션에서는 사용하지 않는 것이 좋습니다. schneems의 Reddit 댓글에서도 이 점을 언급했으며, 이를 뒷받침하는 벤치마크 결과를 담은 블로그 글도 있습니다.
Struct vs OpenStruct: 무엇이 다른가?
Struct와 OpenStruct의 차이점은 다음과 같습니다.
- Struct: 미리 정의된 속성, 동등 비교 메서드(
==), Enumerable 기능을 갖춘 새로운 클래스를 생성합니다. - OpenStruct: 주어진 속성을 가진 새로운 객체를 생성합니다.
즉, OpenStruct는 기능이 강화된 해시 객체라고 볼 수 있고, Struct는 템플릿으로부터 새로운 클래스를 만드는 것에 가깝습니다.
마무리
지금까지 Ruby의 Struct와 OpenStruct에 대해 알아보았습니다! 각 클래스의 특수한 성질만 잘 이해하고 있다면 실무에서 충분히 유용하게 활용할 수 있습니다.
이제 직접 연습해 볼 차례입니다 🙂