1. 불변성이란?
비유부터 시작해보자: 네가 매일매일 판매 보고서를 만드는 회계사라고 상상해봐. 진짜 프로라면 지난주 보고서를 바꾸지 않고, 그걸 기반으로 새로운 보고서를 만들지. 프로그래밍에서 불변 객체도 똑같아: 한 번 만들면 더 이상 안 바뀌고, "변경"이란 건 사실 새로운 복사본을 만드는 거야.
불변성 (immutability)은 객체가 초기화된 후엔 바뀌지 않는 성질이야. 모든 속성이 "얼어붙은" 상태가 돼: 다른 값이 필요하면 그냥 새 객체를 만들어.
왜 필요할까?
- 안전함: 네 객체를 아무도 실수로 못 바꾸니까, 갑자기 데이터가 망가질 일이 없어. 멀티스레드 프로그램에서 여러 스레드가 동시에 뭔가 바꿀 때 이런 일이 자주 생기거든.
- 디버깅이 쉬움: 객체가 안 바뀌니까, 생성 이후에 무슨 일이 있었는지 확실히 알 수 있어.
- 데이터 전달에 편함, 특히 분산 시스템에서 복사본이 따로 놀 때 좋아.
- "상태 스냅샷" (snapshots)을 만들 수 있어서, 변경 이력이 명확해져.
2. record 타입의 불변성
일반 class를 선언하면, 속성은 기본적으로 변경 가능해 (mutable). 예시:
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}
var user = new UserProfile { Name = "이반", Age = 25 };
user.Age = 26; // 문제없음 — 일반 클래스니까 나이를 바로 바꿀 수 있음
record는 기본적으로 불변 데이터 저장용이야.
public record UserProfile(string Name, int Age);
// 객체 생성:
var user = new UserProfile("이반", 25);
// 나이 바꿔보자:
user.Age = 26; // 컴파일 에러: 속성은 읽기 전용이야!
포지셔널 record의 속성은 읽기 전용(init-only)으로 선언돼. 생성 후엔 못 바꾸고, with-표현식으로만 바뀐 복사본을 만들 수 있어.
변경 가능한 클래스 vs 불변 record
| 클래스 | 포지셔널 Record |
|---|---|
기본 속성 |
불변 |
| 어떻게 바꾸나 속성에 바로 접근 |
새 복사본만 생성 가능 |
| 객체 비교 참조로 (ReferenceEquals) |
값으로 (Equals) |
| 데이터 전달에 편함? 항상은 아님 |
응 |
3. with-표현식
"record 진짜 좋은데, 이제 못 바꾸면 어떡하지?" 이런 생각 들지? 여기서 with-표현식의 마법이 등장!
with은 새 복사본을 만들면서 필요한 속성만 바꿔주는 특별한 문법이야.
즉, "이 객체 복사해서, 여기 속성 몇 개만 바꿔줘" 이런 느낌.
간단 예시
var user1 = new UserProfile("안나", 30);
// ... 근데 시간이 흘러서 안나가 나이를 먹었어
var user2 = user1 with { Age = 31 };
// user1은 그대로고, user2는 복사본인데 한 살 더 많아짐
Console.WriteLine(user1); // UserProfile { Name = 안나, Age = 30 }
Console.WriteLine(user2); // UserProfile { Name = 안나, Age = 31 }
속을 들여다보면
이건 그냥 복사본이 아니라, 새 객체야. 자동 생성되는 Clone() 메서드로 복사하고, 새 값만 넣어주는 거지.
만약 with-표현식이 현실에 있었다면, 아침마다 "지친 몸"이 아니라 기분 세팅된 복사본 몸으로 일어날 수 있었을걸? (record라면 말이지!)
4. 중첩과 복사에 대해
record 안에 또 다른 record가 있으면? 문제없어:
public record Address(string City, string Street);
public record Student(string Name, int Age, string Email, Address Home);
var a1 = new Address("모스크바", "트베르스카야");
var s1 = new Student("레나", 21, "lena@mail.ru", a1);
var s2 = s1 with { Home = a1 with { Street = "아르바트" } };
여기선 진짜 불변처럼 동작해, 왜냐면 안에 들어간 Address도 record라서.
5. 마지막 팁들
포지셔널 record = 간결함
record를 "짧은" 포지셔널 문법으로 선언할 수 있어. 그러면 모든 속성이 자동으로 init-only가 돼.
public record Course(string Name, int Credits);
var c1 = new Course("C#", 5);
var c2 = c1 with { Credits = 6 };
읽기 전용(init-only) 속성과 비슷한 점
record에서 속성을 이렇게 명시적으로 선언할 수도 있어:
public record Student
{
public string Name { get; init; }
public int Age { get; init; }
}
이런 속성도 초기화할 때만 바꿀 수 있고(with으로도 가능), 그 이후엔 못 바꿔.
6. 실습: 데모 앱
우리만의 "온라인 학교"를 만들어보자. 이미 학생용 record가 있다고 치자:
public record Student(string Name, int Age, string Email);
클래식: 누가 이메일 주소를 잘못 썼는데, 이미 학생 계정이 만들어졌어. "이메일 업데이트"는 어떻게 할까? 당연히 with으로!
var student = new Student("예카테리나", 19, "kate@school.com");
var updatedStudent = student with { Email = "ekaterina@school.com" };
// 객체 확인:
Console.WriteLine(student); // Student { Name = 예카테리나, Age = 19, Email = kate@school.com }
Console.WriteLine(updatedStudent); // Student { Name = 예카테리나, Age = 19, Email = ekaterina@school.com }
7. 자주 하는 실수와 함정
이제 약간의 고통(?) — 학생들이 불변 record로 자주 실수하는 부분을 보자.
- 첫째, 많은 사람들이 with이 원본 객체를 바꾼다고 착각해. 실제로는 원본은 그대로고, 바뀐 필드로 새 객체가 만들어져. 그래서 새 값을 놓치는 함정에 빠질 수 있어.
- 둘째, record 안에 변경 가능한 객체(예: 배열이나 List)가 있으면, with-표현식이 깊은 복사(deep copy)를 안 해! 컬렉션은 두 복사본에서 똑같이 공유돼.
public record Student(string Name, int Age, List<string> Subjects);
var s1 = new Student("올렉", 22, new List<string> { "Math", "Physics" });
var s2 = s1 with { };
s1.Subjects.Add("C#"); // 오잉, 이제 s2.Subjects에도 "C#"이 들어감
그래서 진짜로 불변 상태를 원하면, 단순 타입이나 자체적으로 불변인 컬렉션(ImmutableList<T> 등 System.Collections.Immutable 참고)을 써야 해.
진짜 불변성을 보장하려면, 이런 컬렉션을 쓰거나 직접 깊은 복사를 해줘야 해.
GO TO FULL VERSION