반응형
좋은 질문이에요! C#의 readonly는 “한 번 정해진 값(또는 상태)을 바꾸지 않겠다”는 불변(immutability) 의도를 코드에 담아 주는 키워드예요. 쓰임새가 몇 가지(필드, 구조체, 멤버 등)로 나뉘어 헷갈리기 쉬우니 교재식으로 차근히 정리해 드릴게요. (가정: C# 12 / .NET 8)
1) 왜 생겼는가 — “변하면 안 되는 건, 애초에 못 바꾸게”
- 버그 예방: 실수로 값을 바꾸는 것을 컴파일 단계에서 차단합니다.
- 가독성/의도 전달: “생성 후 불변”이라는 설계를 명확히 보여줍니다.
- 최적화/성능: JIT가 읽기 전용임을 가정해 불필요한 방어적 복사(defensive copy)를 줄이거나 인라이닝에 유리합니다(특히 readonly struct, readonly members).
2) 무엇인가 — readonly의 네 가지 큰 얼굴
구분 적용 대상 효과 요약 변경 가능 시점
필드 readonly | 인스턴스/정적 필드 | 생성자(또는 선언부) 이후 재할당 금지 | 생성자 실행 전까지 |
readonly struct | 구조체 선언 | 모든 인스턴스 필드가 사실상 읽기 전용, 가상 아닌 멤버가 “읽기 전용 컨텍스트”로 동작 → 복사 최소화 | 생성 시점 |
readonly members | 구조체의 특정 멤버(메서드/프로퍼티/getter) | 해당 멤버 내에서 this가 읽기 전용 → 멤버 호출 시 구조체 복사 방지 | N/A |
ref readonly | 반환 값/지역/매개변수 수식 | 참조로 전달/반환하되 수정 금지 | N/A |
혼동 주의: const는 컴파일 타임 상수이고 반드시 값 형식/문자열만 가능. readonly는 런타임에 한 번 결정되는 불변(예: DateTime.Now로 초기화, 컬렉션 인스턴스 참조 고정 등)이 가능합니다.
3) 언제/어떻게 쓰는가 — 상황별 패턴과 MWE
3-1. 필드에 붙이는 readonly
public sealed class Config
{
public readonly string RootPath; // 인스턴스 필드
public static readonly TimeSpan Timeout; // 정적 필드
static Config()
{
Timeout = TimeSpan.FromSeconds(30); // 정적 생성자에서 1회 설정
}
public Config(string rootPath)
{
RootPath = rootPath ?? throw new ArgumentNullException(nameof(rootPath));
// 여기 이후로 RootPath 재할당 불가
}
}
- 언제? 생성 시 한 번 정하고 절대 안 바꿀 값(경로, 연결 문자열, 아이디 등).
- 장점: 생성자 이후 재할당 시 컴파일 에러로 잡아줌.
3-2. readonly struct — 값 타입의 “진짜 불변” 의도
public readonly struct Point
{
public int X { get; } // set 없음 → 값 변경 불가
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
// 멤버 내에서 this 수정 불가, 호출 시 복사 최소화에 도움
public double Length() => Math.Sqrt(X * X + Y * Y);
}
- 언제? 작은 값 타입(좌표, 범위, 단위 값 등)을 완전 불변으로 다루고 싶을 때.
- 성능: 읽기 전용 멤버 호출 시 불필요한 복사 감소 → 캐시 친화.
3-3. 구조체의 readonly members
public struct BigVector
{
private readonly double[] _data; // 참조는 고정(읽기 전용)
public BigVector(double[] data) => _data = data;
public readonly int Length => _data.Length; // 이 멤버 안에선 this가 readonly
public readonly double Sum()
{
double s = 0;
foreach (var v in _data) s += v;
return s;
}
}
- 효과: 해당 멤버를 호출해도 구조체 인스턴스의 불필요한 복사를 막음.
- 언제? 구조체가 크고, 주로 읽기 전용 연산을 제공할 때.
3-4. ref readonly (참조로 넘기되 수정 금지)
public readonly struct Matrix3
{
private readonly double[] _m; // 길이 9
public Matrix3(double[] m) => _m = m;
public ref readonly double At(int r, int c)
=> ref _m[3 * r + c]; // 요소를 복사 없이 “읽기 전용 참조”로 반환
}
var m = new Matrix3(new double[9]);
ref readonly double cell = ref m.At(0, 0);
// cell = 1.0; // ❌ 컴파일 에러(읽기 전용 참조)
- 언제? 큰 값/배열 요소를 복사 없이 읽기만 하려 할 때.
3-5. const vs readonly vs init 비교 요약
키워드 시점 대상 변경 가능 대표 예
const | 컴파일 타임 | 값 형식/문자열 | 전혀 불가 | const double Pi = 3.14159; |
readonly(필드) | 런타임(생성자까지) | 필드(참조/값) | 생성자 이후 불가 | readonly string Root; |
init(setter) | 객체 초기화 시 | 프로퍼티 | 초기화 식에서만 가능 | public string Name { get; init; } |
실무 팁: “불변 참조 + 가변 내부” 상황에선 readonly 필드에 불변 컬렉션(Immutable Collections) 사용을 고려하세요. readonly List<T>는 “참조”만 고정이지, 리스트 내용은 변할 수 있습니다.
4) 자주 하는 실수와 베스트 프랙티스
흔한 실수들
- readonly를 “내용까지 불변”으로 오해
- readonly SomeClass obj;는 obj 참조만 고정할 뿐, obj 내부 상태는 바뀔 수 있어요. 불변 타입 또는 불변 컬렉션을 병행하세요.
- 구조체에서 복사 비용 간과
- 큰 구조체에 readonly 멤버가 없으면 호출 때마다 방어적 복사가 발생할 수 있습니다. readonly struct 또는 readonly members를 적용해 보세요.
- 필드 초기화 타이밍 혼동
- readonly 필드는 선언부 또는 (정적/인스턴스) 생성자에서만 설정 가능합니다. 일반 메서드에서 재할당하면 컴파일 에러.
- const로 해야 할 것을 readonly로
- 진짜 상수 값(예: 수학 상수, 버전 문자열 등)은 const가 적합. 호출 사이트에 인라인되어 JIT 최적화에 유리.
- 스레드-세이프와의 혼동
- readonly는 동기화를 보장하지 않습니다. 다만 공유 불변 데이터 패턴은 본질적으로 스레드-세이프에 가깝습니다.
베스트 프랙티스
- 도메인 모델은 기본 불변: 생성자/팩토리에서 값을 모두 설정하고, 이후 수정 불가가 기본. 변경이 필요하면 새 인스턴스를 생성.
- 성능 민감 구조체: 가능하면 readonly struct + 필요한 멤버에 readonly 지정, 또는 ref readonly로 반환해 복사 최소화.
- 외부 노출 시 컬렉션 주의: IReadOnlyList<T>/ImmutableArray<T> 등을 쓰거나 AsReadOnly()로 감싸서 변경을 차단.
- init과 조합: DTO/레코드에선 init 접근자를 활용해 초기화만 허용하고 이후 불변 유지.
- API 계약 명확화: “읽기 전용” 의도를 타입/멤버에 꾸준히 부여하면 팀 합의와 코드 품질이 좋아집니다.
미니 종합 예제
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
=> (Amount, Currency) = (amount, currency ?? throw new ArgumentNullException(nameof(currency)));
// 읽기 전용 멤버: 구조체 복사 방지
public readonly Money Add(Money other)
{
if (Currency != other.Currency) throw new InvalidOperationException("통화 불일치");
return new Money(Amount + other.Amount, Currency);
}
}
public sealed class Order
{
public readonly Guid Id; // 생성자 이후 변경 불가
public Money Total { get; private set; } // 값 타입 자체가 불변
public IReadOnlyList<string> Items { get; } // 읽기 전용 뷰
public Order(Guid id, IEnumerable<string> items, Money total)
{
Id = id;
Items = items.ToList().AsReadOnly(); // 내부 변경 차단
Total = total;
}
public void AddFee(Money fee) => Total = Total.Add(fee); // 새 Money로 교체
}
반응형
'C#' 카테고리의 다른 글
“IDisposable: 가비지 컬렉터가 다 못하는 ‘자원 반납’을 결정적으로 보장하는 계약” (0) | 2025.10.10 |
---|---|
C# “??는 ‘null이면 우변’ — 안전한 기본값 지정의 한 줄 공식” (0) | 2025.10.10 |
“C# 값 형식에도 없음(null)을 — T? = Nullable<T> 제대로 쓰는 법” (0) | 2025.10.10 |
error. 이벤트핸들러 이름 충돌 (0) | 2025.09.22 |
volatile는 멀티스레드에서 “가벼운 동기화”를 위한 읽기/쓰기 가시성 보장 장치 (0) | 2025.09.19 |
왜 IDisposable을 구현하나요? (0) | 2025.09.19 |
“직접 델리게이트 콜백” → “.NET 표준 이벤트 패턴(Event + EventArgs)” (0) | 2025.09.19 |
sealed를 왜 써야 할까? (0) | 2025.09.19 |