Struct는 물러나라: Ruby의 새로운 Data.define를 만나보세요
Source: Dev.to
Introduction
Ruby 3.2가 도입되면서 Data.define은 현대 Ruby 애플리케이션에서 DTO(데이터 전송 객체)를 다루는 선호되는 방법이 되었습니다. 오랫동안 Struct가 기본 선택이었지만, 순수 데이터 전송에 있어서는 몇 가지 특이점 때문에 이상적이지 않았습니다. Data는 값 객체 팩토리를 위해 특별히 설계되었습니다.
Data.define ?
Data는 ( Struct와 유사한) 클래스 팩토리로, 간단하고 불변 객체를 정의하는 데 사용됩니다.
# Define a Data class
Address = Data.define(:city, :zip)
# Initialize it
home = Address.new(city: "Vilnius", zip: "01100")
# Access data
home.city # => "Vilnius"
Immutable by Design
Data와 Struct의 가장 큰 차이점은 Data 객체가 불변이라는 점입니다—설정자 메서드가 없습니다.
Point = Data.define(:x, :y)
p1 = Point.new(x: 1, y: 2)
p1.x = 10
# NoMethodError (undefined method `x=' for #)
DTO 상황에서는 불변성이 큰 장점이 됩니다. 데이터가 애플리케이션의 여러 계층을 통과하면서 일관성을 유지하도록 보장해 줍니다.
Flexible initialization
Data는 기본적으로 위치 인자와 키워드 인자 모두를 받아들입니다.
Point = Data.define(:x, :y)
# Both work:
p1 = Point.new(1, 2)
p2 = Point.new(x: 1, y: 2)
Note: 키워드를 사용할 때는 검증이 이루어집니다. 오타(
z: 3등)를 전달하면ArgumentError가 발생합니다.
Equality and pattern matching
Data 객체의 두 인스턴스는 값이 동일하면 같은 것으로 간주됩니다.
p1 = Point.new(1, 2)
p2 = Point.new(1, 2)
p1 == p2 # => true
p1.eql?(p2) # => true
Data 객체는 Ruby의 패턴 매칭(case/in)과도 완벽히 호환됩니다.
case p1
in Point(x, y) if x > 0
puts "X is positive: #{x}"
end
Adding custom logic
Struct와 마찬가지로 Data.define에 블록을 전달해 메서드를 추가할 수 있습니다.
Money = Data.define(:amount, :currency) do
def to_s
"#{amount} #{currency}"
end
end
puts Money.new(100, "USD").to_s # => "100 USD"
Data vs. Struct
| Feature | Data.define (Ruby 3.2+) | Struct |
|---|---|---|
| Mutability | Immutable (read‑only) | Mutable (read/write) |
| Initialization | Positional or keywords | Positional (keywords must be enabled) |
| Enumerable methods | No | Yes (.each, .map, etc.) |
| Intended use | Value objects / DTOs | Lightweight “property” classes |
| Interface size | Minimal (clean) | Large (inherits many methods) |
When to still use Struct
Data가 일반적으로 DTO에 더 좋지만, Struct가 여전히 유용한 상황도 있습니다:
- Mutability needed – 객체가 자주 업데이트되는 임시 “스크래치패드” 역할을 할 때.
- Enumerable needed – 객체의 속성에 대해
.each,.select등을 직접 호출하고 싶을 때. - Legacy Ruby – Ruby 3.1 이하를 사용하고 있어
Data.define을 사용할 수 없을 때.
Data.define for DTOs
일반적인 Rails 또는 Dry‑Ruby 스택에서 DTO는 서비스 객체에서 뷰로, 혹은 API 응답에서 모델로 데이터를 전달합니다. Data.define을 사용하면 다음과 같은 장점이 있습니다:
- 우발적인 부작용을 방지합니다(요청 중간에 값이 변경될 수 없습니다).
- 코드가 더 “정직”해집니다—객체는 단순히 데이터일 뿐입니다.
- 키워드 인자를 기본 지원하므로 위치 기반
Struct인자보다 코드 가독성이 크게 향상됩니다.
Example of a modern DTO
UserDTO = Data.define(:id, :email, :role)
# In a controller or service
user_data = UserDTO.new(id: user.id, email: user.email, role: user.role)