Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby Struct와 OpenStruct 완벽 가이드: 사용법부터 주의사항까지

Ruby에서 관련된 데이터를 묶어서 다루고 싶을 때, 꼭 클래스를 새로 정의해야 할까요? 이럴 때 유용하게 쓸 수 있는 것이 바로 StructOpenStruct입니다. 이 글에서는 두 클래스의 사용법, 장단점, 그리고 실무에서 알아야 할 주의사항까지 자세히 살펴보겠습니다.

Ruby의 Struct란 무엇인가?

Struct는 Ruby에 내장된 클래스로, 값 객체(value object)를 생성하는 새로운 클래스를 만들 때 사용합니다. 값 객체란 서로 연관된 속성들을 하나로 묶어 저장하는 객체를 말합니다.

예를 들어, 좌표 xy 두 개의 값을 가지는 Point 객체를 생각해 봅시다.

이런 데이터는 여러 가지 방식으로 표현할 수 있습니다.

  • 배열: [10, 20]
  • 해시: { x: 10, y: 20 }
  • 객체: Point.new(10, 20)

Point 객체를 여러 개 다뤄야 한다면 객체 방식을 사용하는 것이 좋은 습관입니다.

하지만 문제가 있습니다.

두 값을 함께 저장하기 위해 일일이 클래스 전체를 정의하고 싶지는 않겠죠? 바로 이럴 때 Struct가 해결책이 됩니다.

Ruby에서 Struct 생성하기

Structnew 메서드를 호출하면서 심볼 목록을 인자로 전달하여 생성할 수 있습니다. 전달한 심볼들은 해당 클래스의 인스턴스 변수가 되며, 읽기와 쓰기를 위한 접근자(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: 무엇이 다른가?

StructOpenStruct의 차이점은 다음과 같습니다.

  • Struct: 미리 정의된 속성, 동등 비교 메서드(==), Enumerable 기능을 갖춘 새로운 클래스를 생성합니다.
  • OpenStruct: 주어진 속성을 가진 새로운 객체를 생성합니다.

즉, OpenStruct는 기능이 강화된 해시 객체라고 볼 수 있고, Struct는 템플릿으로부터 새로운 클래스를 만드는 것에 가깝습니다.

마무리

지금까지 Ruby의 Struct와 OpenStruct에 대해 알아보았습니다! 각 클래스의 특수한 성질만 잘 이해하고 있다면 실무에서 충분히 유용하게 활용할 수 있습니다.

이제 직접 연습해 볼 차례입니다 🙂