CodeGym /행동 /C# SELF /record, class, struct의 차이점

record, class, struct의 차이점

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

1. 왜 record가 필요했을까?

C# (그리고 .NET 전체)에서는 오랫동안 class(class)와 struct(struct)가 기본 빌딩 블록이었어. 근데 이 둘 다 완벽하진 않아. 클래스는 참조 타입이고, 변경 가능하고, 비교는 거의 항상 참조로 해 (아주 드물게 예외 있음). 구조체는 값 타입(전달할 때 복사됨)이고, 기본적으로 바이트 단위로 비교되고, 대부분 변경 가능해 (나중에 readonly struct가 나오기 전까지는).

근데 만약 네가 간단하고 자연스러운 데이터 기록이 필요하고, 값으로 비교하기 편하고, 빠르게 복제할 수 있고, 누가 어디서 객체를 바꿨는지 신경 쓰고 싶지 않다면 — 직접 구현하거나 ValueTuple이나 System.Tuple 같은 라이브러리를 써야 했어. 근데 이런 것들은 그렇게 예쁘거나 직관적이지 않아.

그래서 C# 9에서 record가 나왔어 — 선언이 짧고, 불변성 보장도 쉽고, 값 비교에 최적화된 데이터 타입이지.

2. 네 가지 타입 한눈에 보기

class struct record record struct
카테고리 참조 타입 값 타입 참조 타입 값 타입
변경 가능성 기본적으로 가능 기본적으로 가능 기본적으로 불가 (init) 기본적으로 불가 (init)
비교 참조로 (==) 값으로 (필드) 값으로 (필드/프로퍼티) 값으로 (필드)
복제 직접 구현해야 함 직접 구현해야 함 내장 지원 (with) 내장 지원 (with)
상속 가능 불가 가능 불가
불변성 직접 구현해야 함 직접 구현해야 함 아주 쉽게 구현 가능 아주 쉽게 구현 가능
문법 가장 길다 짧다 가장 짧다 (포지셔널) 꽤 짧다
컬렉션에서 사용 참조 복사 참조 복사

비주얼 다이어그램

+----------------+     +----------------+     +--------------------+
|    class       |     |    struct      |     |      record        |
+----------------+     +----------------+     +--------------------+
| Reference Type |     | Value Type     |     | Reference Type     |
| Mutable        |     | Mutable        |     | Immutable (init)   |
| == : Reference |     | == : By Fields |     | == : By Value      |
+----------------+     +----------------+     +--------------------+

3. record, class, struct의 본질적인 차이

메모리에서의 동작: 참조냐 값이냐?

  • classrecord는 참조 타입이야. 함수에 넘기면 객체의 참조가 복사돼.
  • structrecord struct는 값 타입이야. 항상 바이트 단위로 복사돼 (명시적으로 참조로 넘기지 않는 한).

class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);

void Demo()
{
    var pc = new PointClass { X = 1, Y = 2 };
    var ps = new PointStruct { X = 1, Y = 2 };
    var pr = new PointRecord(1, 2);
    var prs = new PointRecordStruct(1, 2);

    ChangeY(pc);    // pc.Y 바뀐다!
    ChangeY(ps);    // ps.Y 안 바뀜 — 복사!
    ChangeY(pr);    // pr.Y 바뀐다!
    ChangeY(prs);   // prs.Y 안 바뀜 — 복사!
}

void ChangeY(dynamic p) { p.Y = 99; }

여기서 dynamic이 마법처럼 보일 수도 있는데 — 그냥 예시를 위해 쓴 거야. struct로 작업할 때 값이 안 바뀌고, class나 record(참조 타입)로 작업할 때는 값이 바뀌는 걸 보여주려고.

비교: 두 객체가 같은지 어떻게 알지?

  • class: 참조로 비교함 (== — 메모리에서 같은 객체일 때만 true), Equals를 오버라이드하지 않으면.
  • struct: 모든 필드 값으로 비교함 (기본값).
  • record: 주 생성자에 지정된 모든 필드/프로퍼티 값으로 비교함.
class Foo { public int A; public int B; }
record Bar(int A, int B);

var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);

Console.WriteLine(foo1 == foo2); // False! 다른 객체
Console.WriteLine(bar1 == bar2); // True! 값이 같다

재밌는 사실:
record struct는 더 멋져: struct처럼 "값으로" 비교하면서도 record의 문법과 기능을 쓸 수 있어.

4. 불변성: 누가 얼마나 보장해줄까?

객체의 안전성 비교:

  • class: 기본적으로 쉽게 변경 가능, 모든 필드를 readonly로 안 하면.
  • struct: 마찬가지지만 readonly struct로 선언하면 필드/프로퍼티 변경 불가.
  • record: 보통 필드는 init으로 선언해서 생성자나 객체 이니셜라이저(with)에서만 값 설정 가능. 데이터 안전하게 전달할 때 편함.
  • record struct: 마찬가지로 readonly record struct로 만들면 값 타입의 불변성과 record의 장점을 동시에 얻을 수 있음.

record Person(string Name, int Age);

var p1 = new Person("알렉세이", 23);
// p1.Age = 24; // 에러! init만 가능

var p2 = p1 with { Age = 24 }; // 가능! 새로운 값으로 복사본 생성

프로젝트가 크고 엔티티 객체가 수십 개 있을 때, record를 쓰면 "누가 어디서 필드 바꿔서 다 망가졌다" 같은 버그를 피할 수 있어.

5. 문법: 선언은 어떻게 생겼고, 헷갈리지 않으려면?


// class
public class Product
{
    public int Id { get; init; }
    public string Name { get; init; }
}

// struct
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// record
public record Product(int Id, string Name);

// record struct
public record struct Point(int X, int Y);
다양한 타입 선언 문법 비교

보다시피 record 타입이 제일 짧아. 생성자, 분해자, 동등성, ToString() 등등을 "바로" 쓸 수 있어.

문법 자동으로 제공되는 기능 상속 가능 여부
class 거의 없음 가능
struct 거의 없음 불가
record ToString, Equals, Deconstruct, with 가능
record struct ToString, Equals, Deconstruct, with 불가

6. 복제와 with 연산자

recordrecord structwith 연산자를 쓸 수 있어서, 일부 프로퍼티만 바꿔서 객체를 쉽게 복제할 수 있어.


record User(string Name, int Age);

var user1 = new User("이리나", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "이리나", user2.Age == 29

class(class)의 경우 복사 메서드를 직접 구현해야 하고, 필드 하나라도 빼먹으면 버그가 생길 수 있어.

7. 상속: 누가 누구를 상속할 수 있나

  • class: 표준 상속 지원 (클래스 계층, 가상 메서드, 추상화 등).
  • struct: 상속 불가 (인터페이스만 구현 가능).
  • record: 상속 지원하지만 제한 있음 (record끼리만 상속 가능, class와는 불가).
  • record struct: struct처럼 상속 불가.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // 가능!

class Vehicle { }
class Car : Vehicle { } // 가능!

// struct는 struct를 상속할 수 없음

상속에 대한 자세한 내용은 다음 레벨에서 :P

8. 실제로 어디에 어떤 타입을 쓸까?

  • class: 동작이 많고, 수명 주기가 길고, 상태가 바뀌고, 계층 구조가 필요한 큰 객체 (예: 비즈니스 로직, UI 컴포넌트 등).
  • struct: 작고 값만 중요한 객체, 빠른 복사, GC 없음, 오버헤드 최소화가 중요한 경우 (예: 좌표, 색상, 금액 등 빠르게 복제 가능한 것).
  • record: DTO(Data Transfer Object), 파라미터 객체, 설정 파라미터, 불변 상태, 계산 결과 등 내용으로 비교하기 편한 객체.
  • record struct: 값 타입이면서 불변성과 record의 동작이 필요한데, 힙 할당을 피하고 싶을 때.

9. 흔한 실수와 주의할 점

가끔 개발자들이 record가 그냥 "클래스 대체품"이라고 생각하는데, 그건 아니야! 객체가 살아있는 동안 상태가 바뀌어야 한다면 class를 써.
객체를 참조로 비교해야 한다면(예: 싱글턴 패턴이나 객체의 수명 주기가 중요할 때) — class를 써.
숫자나 좌표처럼 값처럼 동작해야 하는 객체라면 — struct 또는 record struct.
불변이고, 비교가 쉽고, 여러 계층에서 자주 전달되고, 컬렉션에 저장되고, 로깅/직렬화가 필요한 객체라면 — record가 딱이야.

또 한 가지: record 클래스를 읽기 전용 프로퍼티로 선언했더라도, 내부에 있는 객체가 변경 가능하면 그 필드는 여전히 바뀔 수 있어.


record Student(string Name, List<int> Grades);

var s1 = new Student("안톤", new List<int>() {5,5,5});
var s2 = s1 with { };

s2.Grades.Add(2); // 두 객체 모두 "2점"이 추가됨! s1.Grades == s2.Grades
2
과제
C# SELF, 레벨 19, 레슨 4
잠금
record 생성 및 객체 비교
record 생성 및 객체 비교
2
과제
C# SELF, 레벨 19, 레슨 4
잠금
with 연산자를 사용한 데이터 기록
with 연산자를 사용한 데이터 기록
1
설문조사/퀴즈
DTO 문제, 레벨 19, 레슨 4
사용 불가능
DTO 문제
DTO, record 그리고 with
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION