[Dart] Record란?
record는 Dart 3.0에서 등장했고, 익명이고 불변인 집계(aggregate) 타입이다. 이걸 쓰면 여러 객체를 하나의 객체로 묶을 수 있다.
특징만 보면 컬렉션이 떠오르지만 Dart 공식문서를 보면 레코드와 컬렉션은 별도의 문서로 구분되어 있어서 서로 다른 개념이다.
어떤 것인지 공식문서를 확인해 본다.
https://dart.dev/language/records
레코드는 익명의 불변 집계 유형이다. 다른 컬렉션 타입과 마찬가지로 여러 객체를 하나의 객체로 묶을 수 있다. 다른 컬렉션 타입과 달리 레코드는 크기가 고정돼 있고 이질적이며 타입이 지정돼 있다. 레코드는 실제 값이므로 변수에 저장, 중첩하고 함수와 주고받을 수 있으며 List, Map, Set 같은 자료구조에 저장할 수 있다
- 레코드 구문
레코드 표현식은 괄호로 묶인 이름 또는 위치 필드의 쉼표로 구분된 목록이다
var record = ('first', a: 2, b: true, 'last');
레코드 타입 어노테이션은 괄호로 묶인 쉼표로 구분된 타입 목록이다. 이걸 써서 리턴 타입, 매개변수 타입을 정의할 수 있다. 예를 들어 아래의 (int, int)문은 레코드 타입 어노테이션이다...(중략)
(int, int) swap((int, int) record) {
var (a, b) = record;
return (b, a);
}
이 밑의 내용은 레코드 사용법에 대한 내용이기 때문에 생략한다.
일단 레코드는 크기가 고정돼 있는 객체로, 여러 객체를 하나로 묶을 수 있는 객체인 것 같다. 그런데 불변을 뺀 익명, 집계란 단어들이 왜 쓰였고 무슨 뜻인지 모르겠다.
https://www.darttutorial.org/dart-tutorial/dart-record/
익명 : 레코드에 특정 이름이 연결돼 있지 않음을 의미한다. 정의된 이름이 있는 클래스와 달리 레코드는 전용 이름 없이 인라인으로 정의된다. 실제론 1회성 데이터 구조에 레코드를 쓰거나 클래스의 가벼운 대안으로 레코드를 쓰는 경우가 많다
불변 : 레코드가 생성된 후에는 변경될 수 없음을 의미한다. 레코드를 생성하고 필드를 설정하면 레코드의 수명 동안 해당 값이 일정하게 유지되므로 변경할 수 없다. 불변성은 레코드의 일관된 상태를 보장하고 더 안전하고 실용적인 코드를 장려한다
집계 타입 : 레코드는 여러 값을 하나의 값으로 그룹화하므로 집계 타입이다. 따라서 레코드를 응집력 있는 단위로 취급해서 데이터 조작을 간소화하고 묶음 데이터를 전체적으로 전달할 수 있다. 예를 들어 함수에 레코드를 전달하거나 함수에서 레코드를 리턴할 수 있다
그럼 레코드는 왜 만들어졌을까? 설명만 놓고 보면 이게 왜 만들어졌는지 잘 모르겠다.
https://stackoverflow.com/a/76415171
(레코드는) 기본적으로 필드만 갖고 다른 건 없는 클래스를 선언하는 매우 짧고 동적인 방법이다
예를 들어 "({int r, int g, int b}) color"는 r, g, b 3개의 int 타입 필드가 있는 타입의 변수다. 클래스를 만들어 같은 목적을 달성할 수 있지만 더 많은 작업과 클래스 정의가 필요하다
레코드는 필드 구조에 따라 hashCode, == 메서드를 자동으로 정의하므로 필드 집계를 사용하는 매우 실용적이고 시간을 절약할 수 있는 방법이다. 또는 데이터 홀더 클래스라고도 한다. 컬렉션을 대체하진 않는다. 컬렉션은 같은 타입의 다양한 가짓수의 요소고 레코드는 미리 정의된 다양한 가짓수의 다양한 타입이다
https://github.com/dart-lang/language/blob/main/accepted/3.0/records/feature-specification.md
여러 객체를 하나의 값으로 묶을 때 Dart는 몇 가지 옵션을 제공한다. 값에 대한 필드가 있는 클래스를 정의할 수 있다. 이 방법은 데이터에 첨부할 의미 있는 동작이 있을 때 효과적이지만 상당히 장황하며 이 데이터 번들을 쓰는 다른 코드도 이제 특정 클래스 정의에 연결된다는 걸 의미한다
List, Map, Set 같은 컬렉션으로 래핑할 수 있다. 이 방법은 가볍고 Dart 코어 라이브러리 이외의 다른 커플링을 가져오는 걸 막는다. 하지만 정적 타입 시스템에선 잘 작동하지 않는다. 숫자, 문자열을 함께 묶으려면 List<Object>가 최선이지만 타입 시스템에선 요소의 수와 개별 타입이 뭔지 추적하지 못한다
Future.await()를 써서 서로 다른 타입의 선물 몇 개를 기다린 적이 있다면 이 문제를 경험했을 것이다. 타입 시스템이 리턴된 리스트에서 어떤 요소가 어떤 타입인지 알지 못하기 때문에 결과를 다시 캐스팅해야 한다
"튜플, 레코드 및 패턴 일치"라는 더 큰 기능군의 일부인 이 제안은 Dart에 레코드를 추가한다. 레코드는 익명의 불변 집계 유형이다. List, Map과 마찬가지로 여러 값을 하나의 새로운 객체로 결합할 수 있다. 다른 컬렉션 타입과 달리 고정된 크기, 이질적이며 타입이 지정된다. 레코드의 각 요소는 서로 다른 타입을 가질 수 있으며 정적 타입 시스템은 이를 개별적으로 추적한다
클래스와 달리 레코드는 구조적으로 타입이 지정된다. 레코드 타입을 선언하고 이름을 지정할 필요가 없다. 서로 무관한 2개의 라이브러리가 같은 필드 집합으로 레코드를 생성하는 경우, 타입 시스템은 라이브러리가 서로 결합돼 있지 않아도 해당 레코드가 동일한 타입으로 인식한다
정리하면 3가지 이유 때문에 레코드가 만들어진 것으로 이해했다.
- 클래스는 장황하고 다른 코드와 클래스의 커플링이 발생
- 정적 타입 시스템에서 숫자, 문자열 등 서로 다른 타입의 값을 함께 묶으면 개별 타입이 뭔지 알 수 없음
- 어떤 요소가 어떤 타입인지 알지 못해서 결과를 다른 타입으로 캐스팅해야 함
Dart 깃허브에 있는 md 파일의 내용이라 신뢰도는 높아 보이지만, 일부 설명은 잘 이해가 되지 않아서 좀 더 찾아봐야 할 것 같다.
그럼 레코드는 어떻게 사용할까? 아래는 Dart 공식문서에 있는 예시다.
var record = ('first', a: 2, b: true, 'last');
문자열, 숫자, boolean 값을 소괄호로 감싸서 record 변수에 담는다. 이렇게 작성하면 하나의 레코드를 선언한 것이다.
다른 예시는 아래와 같다. 정수 2개를 갖는 레코드를 받아서 레코드 안의 정수 순서를 바꿔 리턴하는 swap이라는 함수다.
(int, int) swap((int, int) record) {
var (a, b) = record;
return (b, a);
}
이걸 실제로 사용한다면 아래와 같이 사용할 수 있다.
void main() {
var record = (1, 3);
var swappedRecord = swap(record);
print("원본 레코드 : $record");
print("스왑된 레코드 : $swappedRecord");
}
(int, int) swap((int, int) record) {
var (a, b) = record;
return (b, a);
}
// 원본 레코드 : (1, 3)
// 스왑된 레코드 : (3, 1)
레코드 표현식과 타입 어노테이션의 필드는 함수에서 매개변수, 인수가 작동하는 방식을 반영한다.
positional fields는 괄호 안에 바로 들어간다.
아래와 같이 레코드가 어떤 타입을 받을지만 정의해두고, 밑에서 원하는 값을 대입하는 것도 가능하다.
이 방식은 명명된 필드를 사용하는 레코드라고도 한다.
void main() {
// 변수 선언의 레코드 타입 어노테이션
(String, int) record;
// 레코드 표현식으로 초기화
record = ("문자열", 123);
print(record);
}
// (문자열, 123)
또는 아래와도 같이 쓸 수 있다. 이 방식은 위치 기반 필드를 사용하는 레코드라고도 한다.
void main() {
// 변수 선언의 레코드 타입 어노테이션
({int a, bool b}) record;
// 레코드 표현식으로 초기화
record = (a: 123, b: true);
print(record);
}
// (a: 123, b: true)
그냥 보면 중괄호를 썼는지 여부와 record 변수를 초기화하는 방식이 다른 것밖엔 안 보이지만 두 방식의 차이는 아래와 같다.
- 명명된 필드 방식은 JSON 같은 자료구조를 다룰 때 유용할 수 있음. 내장된 getter를 통해 접근할 수 있음
- 위치 기반 방식은 함수가 여러 값을 리턴할 때 사용할 수 있음. $<position>이라는 getter를 노출함
아래는 Dart의 레코드 공식문서에서 볼 수 있는 예시다.
var record = ('first', a: 2, b: true, 'last');
print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'
레코드 필드에 접근하는 코드를 더 간소화하려면 패턴을 사용할 수 있다.
https://dart.dev/language/patterns#destructuring-multiple-returns