값 타입을 불변으로 설계하면 코드가 단순해지고, 방어적 복사(defensive copy)를 줄여 성능을 개선할 수 있습니다. C#의 readonly struct는 이런 목적에 딱 맞는 기능입니다. 핵심만 빠르게 정리합니다.
1. 왜 읽기 전용 구조체인가
- 불변 특성으로 상태 추적이 쉬워집니다.
- 읽기 전용 참조(in, ref readonly)와 결합하면 복사 비용을 줄일 수 있습니다.
- 멀티스레드 시나리오에서 동기화 부담이 줄어듭니다.
2. readonly struct란?
구조체 앞에 readonly를 붙이면 모든 인스턴스 필드가 읽기 전용이 되며, 인스턴스 멤버에서 상태 변경이 금지됩니다. 생성자 내부에서만 필드 할당이 가능합니다.
3. 기본 선언 패턴
using System;
using System.Diagnostics.CodeAnalysis;
public readonly struct Point2D
{
public double X { get; }
public double Y { get; }
public Point2D(double x, double y)
{
X = x;
Y = y;
}
public double Length => Math.Sqrt(X * X + Y * Y);
public Point2D Normalize()
{
var len = Length;
return len == 0 ? this : new Point2D(X / len, Y / len);
}
public static Point2D operator +(Point2D a, Point2D b)
=> new(a.X + b.X, a.Y + b.Y);
public override string ToString() => $"({X}, {Y})";
}
포인트: 생성자로만 값 설정, get 전용 자동 구현 속성, 메서드는 상태를 변경하지 않습니다.
4. 멤버 readonly와 방어적 복사 줄이기
읽기 전용 참조(in)로 전달된 가변 구조체의 멤버를 호출하면 컴파일러가 방어적 복사를 만들 수 있습니다. 멤버를 readonly로 표시하거나 구조체 자체를 readonly로 만들어 복사를 없애야 합니다.
// 나쁜 예: 읽기 전용 참조로 접근 시 Length 호출이 복사 유발 가능성
struct Vector2
{
public float X;
public float Y;
public float Length => MathF.Sqrt(X * X + Y * Y);
}
static float GetLength(in Vector2 v) => v.Length; // v가 복사될 수 있음
// 개선 1: 멤버에 readonly 표시
struct Vector2Fixed
{
public float X;
public float Y;
public readonly float Length => MathF.Sqrt(X * X + Y * Y);
}
static float GetLength2(in Vector2Fixed v) => v.Length; // 복사 없음
// 개선 2: 구조체 자체를 readonly로
readonly struct Vector2Ro
{
public float X { get; }
public float Y { get; }
public Vector2Ro(float x, float y)
{
X = x;
Y = y;
}
public float Length => MathF.Sqrt(X * X + Y * Y);
}
static float GetLength3(in Vector2Ro v) => v.Length; // 복사 없음
팁: 전체를 readonly struct로 설계할 수 있으면 가장 간단합니다. 불가하다면 변하지 않는 멤버에 readonly를 붙입니다.
5. in 매개변수와 ref readonly 반환
- in T: 읽기 전용 참조로 전달해 큰 구조체의 복사 비용을 줄입니다.
- ref readonly 반환: 내부 필드의 읽기 전용 참조를 안전하게 노출합니다(복사 방지).
public readonly struct Bounds
{
private readonly Point2D _min;
private readonly Point2D _max;
public Bounds(Point2D min, Point2D max)
{
_min = min;
_max = max;
}
public ref readonly Point2D Min => ref _min;
public ref readonly Point2D Max => ref _max;
}
static double Perimeter(in Bounds b)
{
// 필드 복사 없이 참조로 읽기
var width = b.Max.X - b.Min.X;
var height = b.Max.Y - b.Min.Y;
return 2 * (width + height);
}
6. 동등성, 해시, 연산자 구현
값 타입은 의미 기반 동등성을 명확히 해야 합니다. IEquatable<T>와 연산자를 구현합니다.
using System;
public readonly struct Angle : IEquatable<Angle>
{
public double Radians { get; }
public Angle(double radians) => Radians = radians;
public double Degrees => Radians * 180.0 / Math.PI;
public bool Equals(Angle other) => Radians.Equals(other.Radians);
public override bool Equals(object? obj) => obj is Angle a && Equals(a);
public override int GetHashCode() => HashCode.Combine(Radians);
public static bool operator ==(Angle a, Angle b) => a.Equals(b);
public static bool operator !=(Angle a, Angle b) => !a.Equals(b);
}
7. readonly record struct 한 줄 요약
불변 값 객체 + 자동 생성 멤버(Equals, GetHashCode, with-표현)는 readonly record struct로 간단히 표현할 수 있습니다.
public readonly record struct Temperature(double Celsius)
{
public double Fahrenheit => Celsius * 9 / 5 + 32;
}
참고: record struct의 with은 복사 기반이므로 큰 구조체에는 주의합니다.
8. 피해야 할 함정과 체크리스트
- 크기: 구조체는 작게 유지합니다(일반적으로 16~32바이트 이하 권장). 너무 크면 참조형이 유리할 수 있습니다.
- set/init 접근자: readonly struct에서는 상태 변경이 불가하므로 get 전용 속성과 생성자를 사용합니다.
- 얕은 불변성: readonly struct라도 참조형 필드를 담으면 내부 객체는 여전히 변경될 수 있습니다. 가능한 값 타입 또는 불변 참조형을 사용합니다.
- 기본값(default) 상태: 의미 있는 불변 기본값을 제공하거나, 생성자 사용을 강제하는 설계를 고려합니다.
- 멤버 readonly 누락: non-readonly struct에서 in 매개변수로 멤버 접근 시 복사 경고가 뜨면 readonly 멤버로 표시합니다.
9. 성능 팁과 마무리
- 빈번히 전달되는 구조체는 in 매개변수로 전달하고, 멤버를 readonly로 만들어 방어적 복사를 없앱니다.
- 항상 측정하세요. BenchmarkDotNet으로 복사/호출 비용을 검증하면 의외의 병목을 찾을 수 있습니다.
- 가능하면 전체를 readonly struct로 설계해 불변과 성능을 동시에 얻는 것이 좋습니다.
'C#' 카테고리의 다른 글
| C# BigInteger를 활용한 대규모 숫자 연산 (0) | 2026.05.21 |
|---|---|
| C# Weak Event Pattern 적용하기 (0) | 2026.05.20 |
| C# 동시 컬렉션(Concurrent Collections) 활용하기 (0) | 2026.05.19 |
| C# Attribute 기반 유효성 검증 시스템 만들기 (0) | 2026.05.19 |
| C# 전처리 지시문(Preprocessor Directives) 심층 탐구 (0) | 2026.05.18 |