레코드 형식으로 만드는 불변객체
불변(Immutable)객체는 내부 상태를 변경할 수 없는, 즉 데이터 변경이 안 되는 객체를 말한다. 상태를 변경할 수 없다는 특성 때문에 불변 객체에서는 데이터 복사와 비교가 많이 이뤄진다,
새로운 상태 표현을 위해 기존 상태를 복사해 이중 일부를 수정해 새로운 객체를 만들고, 상태 확인을 위해 객체 내용을 자주 비교하는... 이 빈번하게 이뤄지는 두 가지 연산을 편리하게 할수있도록 C# 9.0에서 도입한것이 바로 'Record' 다.
참조형식은 클래스의 모든 필드를 readonly로 선언 하면 불변 객체가 되었고, 값 형식은 readonly struct로 구조체를 선언하면 됐었다.
값 형식 객체는 다른 객체에 할당할 때 *깊은 복사 를 수행한다.
깊은 복사 : 모든 필드를 새 객체가 가진 필드에 1:1로 복사하는 것을 의미한다.
배열 요소를 입력하거나 함수 인수로 사용할때도 늘 깊은 복사를 하고있다. 필드가 많으면 많을수록 복사 비용은 커지며, 객체를 여러곳에서 사용할 경우엔 더 커지게 된다. 참조 형식은 이러한 오버헤드가 없다. 객체를 참조하는 메모리 주소만 복사하면 되기 때문이다. 단점으로는 프로그래머가 직접 깊은 복사를 구현해야 합니다.
값 형식은 객체를 비교할 때 기본적으로 모든 필드의 내용을 1:1로 비교합니다. 불변객체에 필요한 비교 방법이죠.
참조형식은 어떨까요? 프로그래머가 직접 비교 코드를 작성해야합니다... 보통 object로부터 상속하는 Equals() 메소드를 오버라이딩해서요.
불변객체를 참조 형식으로 선언하면 함소 호출 인수나 컬렉션 요소를 사용할 때 복사 비용을 줄일수 있습니다.
한편 불변 객체는 새 상태 표현과 상태 확인을 위해 깊은 복사와 내용 비교가 필수적이므로 값 형식으로 선언하는 편이 프로그래머에게 편리한 부분이 많을 것입니다. 불변 참조형식의 비용 효율과 불변 값 형식의 편리함을 얻는 방법이 있을까요? 그것이 바로 레코드 형식입니다.
레코드 형식은 값 형식처럼 다룰 수 있는 불변 참조 형식으로, 참조 형식의 비용 효율과 값 형식의 편리함을 모두 제공합니다. 우와!
레코드 선언
레코드 선언 방법입니다. record 키워드와 지난 시간에 배운 초기화 전용 자동 구현 프로퍼티를 함께 이용해서 선언합니다. 아 물론 초기화 전용 자동 구현 프로퍼티 뿐 아니라 쓰기 가능 프로퍼티 필드도 가능하다네요. 대단하잖아 레코드 오마에~
using System;
namespace InitOnly
{
record RTransaction
{
public string From { get; init; }
public string To { get; init; }
public int Amount { get; init; }
public override string ToString()
{
return $"{From,-10} -> {To,-10} : ${Amount}";
}
}
class Program
{
static void Main(string[] args)
{
RTransaction tr1 = new RTransaction
{
From = "텐하흐",
To = "안토니",
Amount = 100
};
RTransaction tr2 = new RTransaction
{
From = "텐하흐",
To = "호일룬",
Amount = 70
};
Console.WriteLine(tr1);
Console.WriteLine(tr2);
}
}
}
텐하흐 -> 안토니 : $100
텐하흐 -> 호일룬 : $70
with를 이용한 레코드 복사
C# 컴파일러는 똑똑하게 레코드 형식을 위한 복사 생성자가 자동으로 작성해준다. 근데 이 복사 생성자는 protected로 선언되어 보호수준 때문에 명시적으로 호출할 순 없고, with 식을 이용해야만 한다.
[tr1의 모든 상태를 복사한 후 To 프로퍼티 값 "호일룬"만 수정]
================================================================
RTransaction tr1 = new RTransaction { From = "텐하흐", To = "안토니", Amount = 100};
RTransaction tr2 = tr1 with { To = "호일룬" }
어떤가요. 정말 간편해졌죠? with식은 이렇게 객체 상태(프로퍼티)가 많을 수록 유용합니다.
다음 예제입니다! 살짝 바꿔봤어요.
namespace WithExp
{
record Rtransaction
{
public string From { get; init; }
public string To { get; init; }
public int Amount { get; init; }
public override string ToString()
{
return $"{From,-10} -> {To,-10} : ${Amount}";
}
}
class Program
{
static void Main(string[] args)
{
Rtransaction tr1 = new Rtransaction { From = "텐하흐", To = "마운트", Amount = 65 };
Rtransaction tr2 = tr1 with { To = "호일룬" };
Rtransaction tr3 = tr2 with { To = "안토니", Amount = 100 };
Console.WriteLine(tr1);
Console.WriteLine(tr2);
Console.WriteLine(tr3);
}
}
}
텐하흐 -> 마운트 $65
텐하흐 -> 호일룬 $65
텐하흐 -> 안토니 $100
레코드 객체 비교하기
이쯤에서 record로 작성한 Transaction과 class로 작성한 Transaction의 차이를 명확하게 알아보고 가보는건 어떨까요.
참고로 작성자 본인은 글을 쓰고있음에도 class대신 record를 왜 쓰는건지도 모르고 작성하고있었습니다.ㅋㅋ
using System;
namespace RecordComp
{
class CTransaction
{
public string From { get; init; }
public string To { get; init; }
public int Amount { get; init; }
public override string ToString()
{
return $"{From,-10} -> {To,-10} : ${Amount}";
}
}
record Rtransaction
{
public string From { get; init; }
public string To { get; init; }
public int Amount { get; init; }
public override string ToString()
{
return $"{From,-10} -> {To,-10} : ${Amount}";
}
}
class Program
{
static void Main(string[] args)
{
CTransaction trA = new CTransaction { From = "텐하흐", To = "안토니", Amount = 100 };
CTransaction trB = new CTransaction { From = "텐하흐", To = "안토니", Amount = 100 };
Console.WriteLine(trA);
Console.WriteLine(trB);
Console.WriteLine($"trA equals to trB : {trA.Equals(trB)}");
Console.WriteLine();
Rtransaction tr1 = new Rtransaction { From = "텐하흐", To = "안토니", Amount = 100 };
Rtransaction tr2 = new Rtransaction { From = "텐하흐", To = "안토니", Amount = 100 };
Console.WriteLine(tr1);
Console.WriteLine(tr2);
Console.WriteLine($"tr1 equals to tr2 : {tr1.Equals(tr2)}");
}
}
}
텐하흐 -> 안토니 : $100
텐하흐 -> 안토니 : $100
trA equals to trB : False
텐하흐 -> 안토니 : $100
텐하흐 -> 안토니 : $100
trA equals to trB : True
? 아니 분명 똑같은데 왜 위는 False고 아래는 true인거죠? 저의 배움이 부족해서라고요? 예 맞습니다... 그래서 왜 그런거냐고요? 일단 기본적으로 책에는 이렇게 나와있습니다.
"Equals()의 기본 구현은 내용 비교가 아닌 참조를 비교하므로 두 CTransaction 객체 비교 결과는 False가 나옵니다" 라고 하네요.
여기서 C#은 "참조 비교"와 "내용 비교"는 객체 비교에 관련된 두 가지 다른 개념을 나타냅니다.
CTransaction은 참조 비교로 객체의 메모리 주소를 비교하고있습니다. C#에서는 모든 객체가 메모리에 저장된 고유한 주소를 가지고있으며, 이 주소를 통해 객체를 식별하기 때문이죠. 따라서 이 두 개의 객체가 동일한 메모리 주소를 참조하면 이 두 객체는 동일한 객체라 판단합니다. 한마디로 객체의 실제 데이터 내용이 같다해도 서로 다른 메모리 주소를 같고 있기 때문에 다른 객체로 간주되어 False가 된 것입니다.
반면에 RTransaction은 내용비교로 객체가 갖고있는 데이터의 내용을 비교하고있습니다. 두 객체의 데이터가 동일한지를 비교해 값이 같은 경우 두 객체가 동등하다 판단하고있거든요. 이는 객체의 내부 상태가 동일한지 확인하는 것이므로 객체가 같은 데이터를 가지고 있다면 동등하다고 간주되어 True인 것입니다.
즉, CTransaction는 class라 객체지향 형태이며 참조 타입이라 'trA' 'trB'는 값이 같아도 다른 객체를 참조하므로 'Equals' 메서드 비교 결과는 False이고 RTransaction는 Record라 내용 비교(값 비교)를 기본적으로 지원해 레코드가 가지고 있는 값의 모든 속성들의 값이 동일하다면 같다고 판단하며 'tr1'과 'tr2'는 값이 모두 같기 때문에 'Equals'메서드 비교결과 true입니다.
'C#.' 카테고리의 다른 글
| 이것이 C# 이다 10장) 배열 Array [1] (0) | 2023.08.31 |
|---|---|
| 이것이 C#이다 9장 ) 인터페이스와 추상클래스의 프로퍼티 (feat:무명형식) (0) | 2023.08.31 |
| 이것이 C#이다 9장) 자동구현 Property (0) | 2023.08.30 |
| 이것이 C#이다 8장) 인터페이스와 추상클래스 (0) | 2023.08.25 |
| 이것이 C#이다 7장) 클래스와 객체, 인스턴스의 차이 (0) | 2023.08.23 |