1. 데이터 전달의 문제
예를 들어, 우리 학교 앱이 있다고 치자. 학생 정보를 여러 모듈 사이에서 전달해야 해: 이름, 출생 연도, 반. 보통 어떻게 하지?
이렇게 class를 만들어:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
익숙하지? 근데 이 방식엔 몇 가지 문제가 있어:
- 학생 두 명을 비교할 때, 기본적으로 객체 참조만 비교해. 즉, 필드 값이 똑같아도 객체가 다르면 달라!
- class는 만들고 나서도 값이 바뀔 수 있어서(특히 이미 어딘가에서 쓰고 있으면) 버그가 생기기 쉬워;
- 생성자, 비교 메서드, 복사(클론) 메서드, ToString 등등 템플릿 코드가 너무 많아.
이미 눈치챘겠지만: C#이 이 걱정들을 싹 해결해줄 수 있어! 바로 record 등장.
2. record가 뭐야?
record는 C#에서 데이터 저장을 위해 특별히 만들어진 타입이야. record에는 두 가지 핵심 기능이 있어:
- 불변성(immutability): record 타입 객체는 기본적으로 값이 한 번 정해지면 못 바꿔(정확히는 set 세터가 private). 물론 변경 가능한 record도 만들 수 있지만, 기본은 불변성이야.
- 값 비교: 두 record 객체가 모든 필드 값이 같으면, 그 둘은 같은 거로 쳐(==이랑 .Equals()가 다르게 동작해!).
사실 record는 앱의 여러 레이어(예: DB에서 컨트롤러, 컨트롤러에서 뷰 등) 사이에서 데이터 전달할 때 완전 딱이야.
3. record 문법
가장 쉬운 방법 — 포지셔널 문법
값 몇 개만 전달하면 될 때, 한 줄로 타입 선언 가능:
public record Student(string Name, int YearOfBirth, string Class);
이게 내부적으로 어떻게 동작하냐면? 컴파일러가 알아서 아래를 만들어줘:
- 읽기 전용 자동 프로퍼티(프라이빗 세터);
- 모든 파라미터 받는 생성자;
- 비교, 복사 메서드;
- 깔끔하게 출력해주는 ToString!
포지셔널 record 사용하기
이제 우리 학교 앱에서 이 타입을 써보자:
var student1 = new Student("이반", 2008, "8A");
var student2 = new Student("마리야", 2008, "8B");
프로퍼티 접근은 평소처럼(근데 값은 못 바꿔):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
생성 후 프로퍼티 변경 시도
student1.Name = "표트르"; // 에러! 읽기 전용 프로퍼티야.
이 줄을 주석 해제하면 컴파일러가 바로 화내: 읽기 전용 값은 바꿀 수 없다고.
자동 생성된 ToString은 이렇게 생겼어
Console.WriteLine(student1); // 출력: Student { Name = 이반, YearOfBirth = 2008, Class = 8A }
직접 포맷 안 해도 예쁘고 알아보기 쉬워!
4. record 객체 비교
다시 말하지만: 일반 class로 같은 데이터 가진 객체 두 개 만들어도, 서로 다르다고 나와:
var a = new Student("이반", 2008, "8A");
var b = new Student("이반", 2008, "8A");
Console.WriteLine(a == b); // class일 때: false
근데 Student가 record면, 네가 기대한 대로 동작해:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("이반", 2008, "8A");
var b = new Student("이반", 2008, "8A");
Console.WriteLine(a == b); // record일 때: true!
즉, 필드가 똑같으면 메모리상 다른 객체여도 서로 같다고 쳐.
5. 내부적으로 어떻게 생겼나
학생들이 record가 얼마나 많은 걸 자동으로 해주는지 보고 깜짝 놀라더라. class로 직접 짜면 얼마나 귀찮은지 비교해보자.
옛날식 class 직접 작성
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //자기 자신의 class 참조
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
이래서 개발자들이 맨날 똑같은 코드 10번씩 쓰다가 예민해지는 거지!
record — 한 줄이면 끝
public record Student(string Name, int YearOfBirth, string Class);
6. record와 불변성: 뭐가 되고 뭐가 안 됨?
record는 기본적으로 읽기 전용 프로퍼티라서 버그를 많이 막아줘. 근데 진짜 필요하면(예: 엄청 옛날 API랑 연동할 때) 변경 가능한 record도 만들 수 있어:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
이제 이 필드들은 바꿀 수 있지만, 장점 일부(예: 안전성)는 잃게 돼.
7. record 구조 분해
포지셔널 문법이 튜플이랑 비슷해서, record도 쉽게 구조 분해 가능해:
var student = new Student("이반", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // 이반 - 2008, 8A
컴파일러가 각 포지셔널 record마다 Deconstruct 메서드를 만들어줘서, LINQ, switch 패턴 등 쓸 때도 편해.
8. record는 class야, struct야?
기본적으로 record는 참조 타입이야, 즉 class랑 같아. 그래서 참조 타입 특징(힙에 저장, 참조 복사 등)이 그대로 적용돼.
값 타입(value type)이 필요하면, C#에선 이런 것도 가능해:
public record struct Point(int X, int Y);
근데 데이터 전달엔 거의 항상 클래식 record(참조 타입)를 써. record struct는 다음 강의에서 더 자세히 다룰게 :P
class, struct, record 비교
| 타입 | 기본 불변성 | 값 비교 | 쉬운 구조 분해 | 자동 ToString |
|---|---|---|---|---|
| class | 아니 | 아니 (참조로 비교) | 아니 | 아니 |
| struct | 아니 | 응 | 아니 | 아니 |
| record | 응 | 응 | 응 | 응 |
9. 특징 & 흔한 실수
초보자들이 record 쓸 때 헷갈리는 포인트가 많아. 예를 들어, record 객체 하나의 필드를 바꾸면 다른 객체도 바뀔 거라 생각하는데(참조 복사된 class처럼), 절대 아님! 그리고 with 쓸 때마다 항상 복사본이 만들어지고 원본은 안 바뀌는 것도 기억해. record는 데이터의 순수성과 예측 가능성이 중요한 로직에 완전 잘 맞아.
그리고 init으로 필드 선언하면 set 대신, 생성 시나 with로만 값 줄 수 있고, 그 이후엔 못 바꿔.
GO TO FULL VERSION