record
record
란 무엇인가?
C#에서 record
는 C# 9.0에서 도입된 새로운 참조 형식으로, 데이터 중심의 불변 객체를 간단하게 정의할 수 있도록 설계되었습니다. 주로 값 객체value object로 사용되며, 데이터의 평등성을 쉽게 처리할 수 있게 해줍니다. record
는 일반 클래스와 유사하지만, 몇 가지 중요한 차이점과 추가 기능이 있어 데이터를 더욱 간결하고 효율적으로 관리할 수 있습니다.
public record Person(string Name, int Age);
이 예시는 Person
이라는 record
를 정의하고 있습니다. 이 record
는 두 개의 속성(Name
, Age
)을 가지고 있으며, 생성자와 Equals, GetHashCode, ToString
등을 자동으로 생성해 줍니다.
값 기반 평등성
record
의 핵심 개념 중 하나는 참조 타입이지만 값처럼 사용된다는 점입니다. 일반적으로 참조 타입(예: 클래스)은 객체의 참조(메모리 주소)를 비교하고 다루지만, record
는 데이터를 값 기반으로 비교하고 다룹니다. 이런 방식은 record
가 참조 타입임에도 불구하고, 값 타입의 특징을 가지는 것처럼 행동하게 만들어줍니다.
Person person1 = new("Alice", 30);
Person person2 = new("Alice", 30);
Console.WriteLine(person1.Equals(person2)); // True (값 기반 비교)
Console.WriteLine(object.ReferenceEquals(person1, person2)); // False (참조 비교)
- Equals 메서드는 두 객체의 값이 동일한지 비교하고, ReferenceEquals 메서드는 두 객체의 참조가 동일한지, 즉 같은 메모리 주소를 가리키는지를 확인합니다.
person1
과person2
는 동일한 데이터를 가지고 있으므로Equals
메서드는True
를 반환합니다.person1
과person2
는 서로 다른 인스턴스이기 때문에ReferenceEquals
는False
를 반환합니다.- 이는
record
가 값 비교를 기본으로 하여 동일한 데이터를 가진 객체라면 같은 값으로 취급하는 것을 보여줍니다.
기본 비교 연산자 지원
record
는 값을 기반으로 객체 간의 비교로 하기 때문에 기본적으로 비교 연산자 (==
,!=
)를 지원합니다.
Person person1 = new("Alice", 30);
Person person2 = new("Alice", 30);
Console.WriteLine(person1 == person2); // True
불변성
record
는 불변 객체로 사용되는 것이 일반적입니다. 생성자를 통해 초기화된 속성들은 수정할 수 없으며, 이는 객체의 상태를 유지하고 예기치 않은 변경을 방지하는 데 도움을 줍니다. 불변성을 유지하려면 반드시 속성을 init
접근자로 정의해야 하며, set
접근자를 사용할 경우 불변성이 유지되지 않습니다.
public record Car
{
public string Model { get; init; }
public string Manufacturer { get; init; }
}
Car car = new() { Model = "Model S", Manufacturer = "Tesla" };
// car.Model = "Model X";
// 컴파일 오류: init 접근자 속성은 초기화 이후 수정 불가
init
접근자
init
접근자는 객체의 초기화 단계에서만 값을 설정할 수 있도록 해주는 접근자입니다. 이를 통해 생성 된 객체는 이후에는 속성 값을 변경할 수 없으므로 불변 객체를 쉽게 구현할 수 있습니다.
public record Book
{
public string Title { get; init; }
public string Author { get; set; } // 불변성이 유지되지 않음
}
Book book = new() { Title = "C# Programming", Author = "John Doe" };
book.Author = "Jane Doe"; // 변경 가능
위 예제에서 Author
속성은 set
접근자를 사용하여 변경 가능하므로, Book
객체는 불변성을 유지하지 않습니다. 반대로 Title
속성은 init
접근자를 사용하여 초기화 이후에는 변경할 수 없도록 보장합니다.
init 접근자를 사용하지 않는 record 는 class 와 매우 비슷하게 사용되며, 반대로 init 접근자를 사용한 class 는 불변성 측면에서는 record 와 매우 유사한 특징을 가지게 됩니다.
하지만 record는 추가로 값 기반 평등성, with 표현식, 그리고 데이터 중심 객체 모델링을 간편하게 해주는 여러 기능을 제공한다는 점에서 여전히 일반 클래스와 차별화됩니다.
with
표현식
record
는 with
표현식을 제공하여, 기존 객체의 일부 속성만 변경하면서 새로운 객체를 쉽게 생성할 수 있습니다. 이는 객체의 불변성을 유지하면서도 원하는 속성을 유연하게 변경하는 데 매우 유용합니다.
Person person1 = new("Alice", 30);
Person person2 = person1 with { Age = 31 }; // Name은 "Alice"로 유지하고 Age만 변경
Console.WriteLine(person2); // 출력: Person { Name = Alice, Age = 31 }
레코드의 깊은 복사와 얕은 복사
with
키워드를 사용한record
생성은 얕은 복사를 수행합니다. 만약 레코드에 참조 타입의 속성이 포함되어 있다면, 이 참조는 복사되지 않고 공유되며, 깊은 복사가 필요한 경우에는 수동으로 구현해야 합니다. 이는 레코드가 불변성을 유지하면서도 성능을 높이기 위한 기본적인 복사 방식을 제공한다는 점에서 이해할 수 있습니다.
public record Address(string City, string Street);
public record Person(string Name, int Age, Address Address);
var originalPerson = new Person("Alice", 30, new Address("New York", "5th Ave"));
var shallowCopy = originalPerson with { Age = 31 };
// 참조 타입인 Address는 공유됨
Console.WriteLine(object.ReferenceEquals(originalPerson.Address, shallowCopy.Address)); // True
생성자와 해제자
record
는 기본적으로 생성자Constructor와 해제자deconstructor를 자동으로 제공하며 이를 통해 데이터를 쉽게 초기화하고 추출할 수 있습니다. 또한 사용자 지정 생성자와 해제자를 정의하여 객체 초기화와 분해에 대한 유연성을 추가할 수 있습니다.
기본 생성자와 해제자
record
는 모든 속성을 초기화하는 기본 생성자와, 각 속성을 개별 변수로 추출할 수 있는 기본 해제자를 자동으로 제공합니다.
public record Person(string Name, int Age);
var person = new Person("Alice", 30);
var (name, age) = person;
Console.WriteLine($"Name: {name}, Age: {age}"); // Name: Alice, Age: 30
위 코드에서 Person
은 자동으로 생성된 기본 생성자를 통해 초기화되며, 기본 해제자를 사용해 name
과 age
를 개별 변수로 쉽게 추출하고 있습니다.
사용자 지정 생성자와 해제자
기본적으로 제공되는 생성자와 해제자 외에도, 사용자가 직접 생성자와 해제자를 정의할 수 있습니다. 이를 통해 객체 초기화 시 특정 로직을 추가하거나 해제할 때 특정 데이터를 추가로 추출하거나 추출 형식을 변경할 수 있습니다.
public record Person
{
public string Name { get; }
public int Age { get; }
// 사용자 지정 생성자
public Person(string name, int age, bool isUpperCase = false)
{
Name = isUpperCase ? name.ToUpper() : name;
Age = age;
}
// 사용자 지정 해제자
public void Deconstruct(out string name, out int age, string nameWithAge)
{
age = Age;
name = Name;
nameWithAge = $"{Name} ({Age})";
}
}
var person = new Person("Alice", 30);
var (Age, Name, NameWithAge) = person;
Console.WriteLine($"Name: {name}, Age: {age}, NameWithAge: {NameWithAge}");
// Name: ALICE, Age: 30, NameWithAge : ALICE (30)
위 예제에서는 사용자 지정 생성자를 통해 name
을 대문자로 변환하는 로직을 추가했습니다. 또한, 사용자 지정 해제자를 통해 AgeWithName 변수를 추가로 제공하였습니다.
상속
record
는 클래스처럼 상속이 가능합니다. 값을 기반으로 하면서도 참조 타입인 이유는 상속과 같은 객체 지향 특성을 활용하고 다양한 형태의 객체를 하나의 인터페이스 또는 베이스 타입으로 취급할 수 있는 다형성을 제공하기 위함입니다.
상속할 때는 부모 클래스의 생성자를 호출하여 속성을 초기화해야 합니다.
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
위의 예제에서 Dog
는 Animal
을 상속받고 있으며, 부모 클래스의 속성을 base
생성자를 통해 초기화하고 있습니다.
ToString
메서드
record
는 자동으로 ToString
메서드를 구현하여 객체의 모든 속성을 포함한 명확한 문자열 표현을 제공합니다. 예를 들어, 아래와 같이 사용할 수 있습니다.
Person person = new("Alice", 30);
Console.WriteLine(person); // 출력: Person { Name = Alice, Age = 30 }
이와 같이 record
의 ToString
메서드는 객체의 모든 속성을 손쉽게 확인할 수 있도록 해줍니다.
record struct
C# 10에서는 record struct가 도입되어, 이제 record는 클래스뿐만 아니라 구조체로도 사용할 수 있습니다. record class는 참조 형식으로 힙에 저장되지만, record struct는 값 형식으로 스택에 저장되어 더 경량화된 데이터 구조로 사용할 수 있습니다.
public record struct Point(int X, int Y);
record struct
는 값 기반 평등성을 제공하므로 동일한 값을 가진 두record struct
가 같다고 간주되도록Equals
및GetHashCode
메서드가 자동으로 생성합니다.- 일반
struct
는 기본적으로 참조 평등성을 지원하지 않으며, 동일 여부를 확인하기 위해서는 각 필드의 동일성을 모두 평가할 수 있는 Equals 와 GetHashCode 메서도를 직접 구현해야 합니다. record struct
는 데이터를 표현하기 위해 더욱 간결한 구문을 제공합니다. 생성자,Deconstruct
,ToString
,Equals
등이 자동으로 생성되므로 데이터 중심의 구조체를 더 쉽게 정의하고 사용할 수 있습니다.
record struct
예제
public record struct Point(int X, int Y);
var point1 = new Point(1, 2);
var point2 = point1 with { Y = 3 };
Console.WriteLine(point1.Equals(point2)); // False (값 비교)
Console.WriteLine(point2); // Point { X = 1, Y = 3 }
일반 struct
예제
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
// 사용자 지정 Equals 메서드
public override bool Equals(object obj)
{
if (obj is Point point)
{
return X == point.X && Y == point.Y;
}
return false;
}
// 사용자 지정 GetHashCode 메서드 (Equals를 재정의할 때는 GetHashCode도 재정의해야 함)
public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
// 사용자 지정 ToString 메서드
public override string ToString()
{
return $"Point(X: {X}, Y: {Y})";
}
}
var point1 = new Point(1, 2);
var point2 = new Point(1, 3);
Console.WriteLine(point1.Equals(point2)); // False (직접 구현한 경우에만 값 비교 가능)
Console.WriteLine(point2); // 기본 ToString 출력
불변 컬렉션
record
는 기본적으로 불변성을 갖도록 설계되어 있어 불변 컬렉션을 만들 때 매우 유용합니다. C#의 System.Collections.Immutable
네임스페이스와 함께 사용하면 데이터를 안전하게 유지할 수 있습니다. 불변 컬렉션은 데이터의 일관성을 보장하며, 데이터가 변경될 수 있는 위험을 방지하는 데 매우 효과적입니다. 예를 들어, 많은 쓰레드에서 동시에 접근하는 데이터를 사용할 때 불변 컬렉션은 특히 유용합니다.
다음 예제에서는 record
와 불변 컬렉션을 사용하는 방법을 보여줍니다:
using System.Collections.Immutable;
var person = new Person("Alice", 30);
var immutableList = ImmutableList.Create(person);
// 불변 컬렉션이므로, 리스트의 요소를 변경할 수 없습니다.
// immutableList.Add(new Person("Bob", 25)); // 이 작업은 불변 리스트이기 때문에 기존 리스트를 변경하지 않습니다.
Console.WriteLine(immutableList);
record
는 본질적으로 불변성을 가지며, 데이터의 안정성을 중시하는 구조입니다. 이런 특성은 불변 컬렉션ImmutableList, ImmutableArray 등과 잘 조화됩니다. 하지만 record
와 ImmutableList
는 직접적인 기능적 연관성이 있는 것은 아닙니다. 둘 다 불변성을 유지하려는 목적에 잘 맞기 때문에 같이 사용했을 때 더욱 유용하고 상호 보완적이라는 의미입니다.
복합 속성 및 중첩된 레코드
record
는 중첩된 데이터 구조를 다루기에 매우 유용합니다. 복합적인 데이터를 표현하기 위해 중첩된 record
를 사용하면, 구조적인 데이터 객체를 쉽게 정의하고 관리할 수 있습니다. 이를 통해 복잡한 데이터 모델을 표현할 수 있으며, 이러한 구조에서도 불변성 및 값 기반 평등성이 유지됩니다.
아래 예제에서는 Person
이 Address
라는 복합 속성을 가지는 형태로 중첩된 record
를 사용하는 모습을 보여줍니다:
public record Address(string City, string Street);
public record Person(string Name, int Age, Address Address);
var person = new Person("Alice", 30, new Address("New York", "5th Ave"));
Console.WriteLine(person); // 출력: Person { Name = Alice, Age = 30, Address = Address { City = New York, Street = 5th Ave } }
이 예제에서 Person
객체는 Address
라는 복합적인 속성을 가지며, record
의 특성을 활용하여 객체를 간단하고 명확하게 정의하고 있습니다.
중첩된 record
를 사용하면 다음과 같은 장점이 있습니다:
- 데이터의 명확한 표현: 복합 객체를 간단하게 정의할 수 있어, 데이터의 계층 구조를 명확히 표현할 수 있습니다.
- 불변성: 중첩된 속성 역시
record
이기 때문에, 상위 객체뿐만 아니라 하위 객체도 불변성을 가집니다. - 값 기반 비교: 중첩된
record
객체들도 값 기반 평등성을 지원하여 동일한 데이터를 가진 객체들이 동일하다고 판단됩니다. 이를 통해 복잡한 데이터 모델에서도 안전하고 일관된 데이터를 관리할 수 있으며, 데이터 중심의 애플리케이션에서 더욱 효율적입니다. 중첩된 데이터를 처리할 때 이런 구조는 객체의 안정성을 보장하고, 코드 작성 시 유지보수성을 높이는 데 기여합니다.
레코드 타입과 패턴 매칭
record
는 패턴 매칭과 함께 사용할 때 매우 유용합니다. 패턴 매칭을 통해 데이터의 속성을 손쉽게 확인하고, 특정 조건에 따라 다른 로직을 실행할 수 있습니다. 이는 데이터 중심 객체를 다룰 때 직관적이고 간결한 코드를 작성할 수 있게 해줍니다.
public record Person(string Name, int Age);
public void PrintPersonInfo(object obj)
{
if (obj is Person { Name: "Alice", Age: var age })
{
Console.WriteLine($"Alice의 나이는 {age}입니다.");
}
}
var person = new Person("Alice", 30);
PrintPersonInfo(person); // 출력: Alice의 나이는 30입니다.
위 예제에서는 패턴 매칭을 사용하여 Person
객체의 특정 속성 값에 따라 로직을 실행합니다.
재귀적인 레코드 구조
재귀적인 구조란?
재귀적인 레코드 구조란, 자기 자신을 참조하는 방식으로 정의된 데이터 구조를 말합니다. 즉, 객체의 속성 중 하나가 동일한 타입의 객체를 참조하는 형태이며, 트리Tree, 그래프Graph, 링크드 리스트Linked List와 같은 계층적 또는 순환적인 데이터 구조를 정의할 때 자주 사용됩니다.
record
와 재귀적 구조
record
는 재귀적인 데이터 구조를 표현할 때도 매우 유용합니다. 예를 들어, 트리 구조나 그래프 같은 데이터 모델에서 각 노드를 표현하기 위해 record
를 사용할 수 있으며, 복잡한 데이터 구조를 간단하고 명확하게 정의할 수 있습니다.
public record TreeNode(int Value, TreeNode? Left = null, TreeNode? Right = null);
var root = new TreeNode(10,
new TreeNode(5),
new TreeNode(15, new TreeNode(12), new TreeNode(18)));
Console.WriteLine(root);
// 출력: TreeNode { Value = 10, Left = TreeNode { Value = 5, Left = , Right = }, Right = TreeNode { Value = 15, Left = TreeNode { Value = 12, Left = , Right = }, Right = TreeNode { Value = 18, Left = , Right = } } }
위 코드에서 TreeNode
는 자기 자신을 참조하는 Left
와 Right
속성을 가지고 있습니다. 이러한 재귀적인 정의는 트리 구조를 간단히 표현할 수 있도록 해줍니다.
LinkedListNode
는 다음 노드를 가리키는 Next
속성을 가지며, Next
가 다시 LinkedListNode
타입입니다. 마찬가지로 쉽게 표현할 수 있습니다.
public record LinkedListNode<T>(T Value, LinkedListNode<T>? Next = null);
var node3 = new LinkedListNode<int>(3);
var node2 = new LinkedListNode<int>(2, node3);
var node1 = new LinkedListNode<int>(1, node2);
Console.WriteLine(node1);
// 출력: LinkedListNode { Value = 1, Next = LinkedListNode { Value = 2, Next = LinkedListNode { Value = 3, Next = } } }
레코드의 컨버전 연산
record
는 불변성과 값 비교 기능을 갖추고 있어, 데이터 전환 작업에서 매우 유용합니다. 예를 들어, 두 데이터 구조 간의 매핑 작업을 할 때 record
의 자동 생성 기능(생성자, with
표현식 등)을 활용하면 간단히 데이터를 변환할 수 있습니다.
record
와 Class
간의 변환
다음 예시는 일반 클래스와 record
간의 데이터 변환을 다룹니다. 데이터 전송 객체DTO와 도메인 객체 간의 변환 시 이러한 패턴을 사용할 수 있습니다.
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public Employee(string name, int age)
{
Name = name;
Age = age;
}
}
public record EmployeeRecord(string Name, int Age);
EmployeeRecord ConvertToRecord(Employee employee) =>
new EmployeeRecord(employee.Name, employee.Age);
Employee ConvertToClass(EmployeeRecord record) =>
new Employee(record.Name, record.Age);
// 사용 예시
var employee = new Employee("Bob", 40);
var employeeRecord = ConvertToRecord(employee);
Console.WriteLine(employeeRecord);
// 출력: EmployeeRecord { Name = Bob, Age = 40 }
var employeeFromRecord = ConvertToClass(employeeRecord);
Console.WriteLine($"Employee: {employeeFromRecord.Name}, Age: {employeeFromRecord.Age}");
// 출력: Employee: Bob, Age: 40
이 예제에서는 Employee
클래스를 EmployeeRecord
로 변환하고, 다시 이를 클래스로 변환합니다. 이런 방식으로 레코드를 기존의 클래스 구조와 쉽게 연동하여 사용할 수 있습니다.
API 응답 변환
실제 응용 시나리오에서는, API 응답을 DTO 형태로 받고, 이를 레코드로 변환해 데이터 일관성을 유지하는 경우가 많습니다. 예를 들어, 웹 API에서 JSON 형태의 데이터를 받아서, 이를 record
로 변환하는 코드입니다.
public record ApiResponse(string FullName, int Age);
public record Person(string Name, int Age);
Person ConvertFromApi(ApiResponse response) =>
new Person(response.FullName, response.Age);
// 사용 예시
var response = new ApiResponse("Alice Smith", 30);
var person = ConvertFromApi(response);
Console.WriteLine(person);
// 출력: Person { Name = Alice Smith, Age = 30 }
이 예제에서는 API 응답을 표현하는 ApiResponse
레코드를 받아, 도메인 객체인 Person
레코드로 변환합니다.
맺음말
C#의 record
는 데이터 중심의 객체를 표현하고 다루기 위해 매우 유용한 기능을 제공합니다. 참조 타입의 장점(상속 등)과 값 타입의 특징(값 기반 비교, 불변성)을 결합하여 참조 타입이지만 값처럼 사용될 수 있습니다. 값 기반 평등성, 불변성, 그리고 간편한 객체 복제 기능은 데이터 모델링에서 발생할 수 있는 오류를 줄이고, 코드의 간결함과 유지보수성을 높이는 데 큰 기여를 합니다. 이를 통해 불변 객체를 쉽게 다루고, 데이터 모델을 더욱 안정적으로 설계할 수 있습니다. record
는 특히 데이터 중심 애플리케이션에서 그 진가를 발휘하며, 간단하면서도 강력한 데이터 모델을 구축하는 데 큰 도움이 됩니다.