CodeGym /행동 /C# SELF /record랑 포지셔널 문법 처음 만나보...

record랑 포지셔널 문법 처음 만나보기

C# SELF
레벨 19 , 레슨 1
사용 가능

1. 데이터 전달의 문제

예를 들어, 우리 학교 앱이 있다고 치자. 학생 정보를 여러 모듈 사이에서 전달해야 해: 이름, 출생 연도, 반. 보통 어떻게 하지?

이렇게 class를 만들어:


public class Student
{
    public string Name { get; set; }
    public int YearOfBirth { get; set; }
    public string Class { get; set; }
}
학생 데이터 저장용 class (옛날 방식)

익숙하지? 근데 이 방식엔 몇 가지 문제가 있어:

  • 학생 두 명을 비교할 때, 기본적으로 객체 참조만 비교해. 즉, 필드 값이 똑같아도 객체가 다르면 달라!
  • class는 만들고 나서도 값이 바뀔 수 있어서(특히 이미 어딘가에서 쓰고 있으면) 버그가 생기기 쉬워;
  • 생성자, 비교 메서드, 복사(클론) 메서드, ToString 등등 템플릿 코드가 너무 많아.

이미 눈치챘겠지만: C#이 이 걱정들을 싹 해결해줄 수 있어! 바로 record 등장.

2. record가 뭐야?

record는 C#에서 데이터 저장을 위해 특별히 만들어진 타입이야. record에는 두 가지 핵심 기능이 있어:

  1. 불변성(immutability): record 타입 객체는 기본적으로 값이 한 번 정해지면 못 바꿔(정확히는 set 세터가 private). 물론 변경 가능한 record도 만들 수 있지만, 기본은 불변성이야.
  2. 값 비교:record 객체가 모든 필드 값이 같으면, 그 둘은 같은 거로 쳐(==이랑 .Equals()가 다르게 동작해!).

사실 record는 앱의 여러 레이어(예: DB에서 컨트롤러, 컨트롤러에서 뷰 등) 사이에서 데이터 전달할 때 완전 딱이야.

3. record 문법

가장 쉬운 방법 — 포지셔널 문법

값 몇 개만 전달하면 될 때, 한 줄로 타입 선언 가능:


public record Student(string Name, int YearOfBirth, string Class);
record 포지셔널 문법

이게 내부적으로 어떻게 동작하냐면? 컴파일러가 알아서 아래를 만들어줘:

  • 읽기 전용 자동 프로퍼티(프라이빗 세터);
  • 모든 파라미터 받는 생성자;
  • 비교, 복사 메서드;
  • 깔끔하게 출력해주는 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!
record 객체 값 비교

즉, 필드가 똑같으면 메모리상 다른 객체여도 서로 같다고 쳐.

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);
record — 똑같은 기능, 한 줄로!

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 구조 분해

컴파일러가 각 포지셔널 record마다 Deconstruct 메서드를 만들어줘서, LINQ, switch 패턴 등 쓸 때도 편해.

8. record는 class야, struct야?

기본적으로 record참조 타입이야, 즉 class랑 같아. 그래서 참조 타입 특징(힙에 저장, 참조 복사 등)이 그대로 적용돼.

값 타입(value type)이 필요하면, C#에선 이런 것도 가능해:


public record struct Point(int X, int Y);
record struct — 값 타입 record

근데 데이터 전달엔 거의 항상 클래식 record(참조 타입)를 써. record struct는 다음 강의에서 더 자세히 다룰게 :P

class, struct, record 비교

타입 기본 불변성 값 비교 쉬운 구조 분해 자동 ToString
class 아니 아니 (참조로 비교) 아니 아니
struct 아니 아니 아니
record

9. 특징 & 흔한 실수

초보자들이 record 쓸 때 헷갈리는 포인트가 많아. 예를 들어, record 객체 하나의 필드를 바꾸면 다른 객체도 바뀔 거라 생각하는데(참조 복사된 class처럼), 절대 아님! 그리고 with 쓸 때마다 항상 복사본이 만들어지고 원본은 안 바뀌는 것도 기억해. record는 데이터의 순수성과 예측 가능성이 중요한 로직에 완전 잘 맞아.

그리고 init으로 필드 선언하면 set 대신, 생성 시나 with로만 값 줄 수 있고, 그 이후엔 못 바꿔.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION