본문 바로가기
C#

C#의 readonly 키워드

by 공부봇 2025. 9. 19.
반응형

1) readonly가 뭔가요?

  • 필드(멤버 변수)에 붙이는 한정자로,
    선언 시 또는 생성자(ctor) 안에서만 한 번 값을 할당할 수 있고, 그 이후에는 수정이 불가능합니다.
  • 즉, 객체가 만들어진 뒤에는 바뀌지 않아야 할 값에 사용합니다.
    (정확히 말하면 참조형의 경우 **참조(주소)**가 고정될 뿐, 참조 대상 내부 상태까지 자동으로 불변이 되는 것은 아닙니다.)

2) 왜/언제 쓰나요? (써야 할 이유)

  • 불변성(immutability) 보장: 실수로 값이 바뀌는 버그를 차단합니다. 클래스 불변식을 지키기 쉬워요.
  • 코드 의도 표현: “이 값은 생성 시 결정되고 이후 변하지 않는다”를 컴파일러 수준에서 명시합니다.
  • 스레드 안정성에 기여: 공유되는 상태가 줄어들어 동시성 버그 위험을 줄입니다.
  • 설계 간결화: 의존성, 구성(config), ID, 상수에 준하는 값 등 “생성 시점에 확정되는 속성”을 표현하기 좋습니다.
  • const보다 유연: const는 컴파일 타임 상수만 가능하지만, readonly는 런타임에 계산한 값을 생성자에서 넣을 수 있습니다. 또한 readonly는 참조형/값형 모두 가능.

3) 자주 쓰는 상황

  • 설정 값, 경로, 연결 문자열 등 실행 시 한 번 결정되면 끝까지 안 바뀌는 값
  • DI로 주입한 서비스, 리포지토리 등 참조는 고정하고 싶을 때
  • 엔터티의 ID/생성시각 같이 객체 정체성을 규정하는 필드
  • 캐시된 계산 결과: 생성 시 계산하거나, 지연 초기화 후 더는 바꾸지 않을 때(초기화 패턴과 함께)

4) const vs readonly (헷갈리는 포인트)

항목 const readonly

값 결정 시점 컴파일 타임 런타임(생성자 또는 선언 시)
대상 타입 숫자/문자열/열거형 등 컴파일 상수 어떤 타입이든 가능(참조형 포함)
변경 가능 여부 절대 불가 생성자 이후 불가
바인딩 호출하는 어셈블리에 값이 박힘(binary baked-in) 필드 참조로 남음(버전 변경 시 안전)
static 여부 자동으로 static 필요하면 static readonly 명시

라이브러리 공개 상수는 const 남발보다 **public static readonly**가 버전 호환성 측면에서 안전한 경우가 많습니다.

5) 샘플 코드

(A) 기본 사용: 인스턴스 readonly 필드

public class User
{
    // 선언과 동시에 할당 가능
    private readonly DateTime _createdAt = DateTime.UtcNow;

    // 생성자에서 한 번만 할당 가능
    private readonly Guid _id;
    private readonly string _name;

    public User(string name)
    {
        _id = Guid.NewGuid();
        _name = name ?? throw new ArgumentNullException(nameof(name));
    }

    public Guid Id => _id;
    public string Name => _name;
    public DateTime CreatedAt => _createdAt;

    public void Rename(string newName)
    {
        // _name = newName; // 컴파일 오류: readonly 필드 수정 불가
    }
}

(B) const vs static readonly

public static class Paths
{
    // 컴파일 상수 (빌드 시 값이 박힘)
    public const string AppName = "MyApp";

    // 런타임에 조합 가능, 외부 환경 따라 달라질 수 있음
    public static readonly string DataDir =
        System.IO.Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            AppName);
}

(C) 참조형 readonly 주의점(참조는 고정, 내부는 가변)

public class Basket
{
    private readonly List<string> _items;

    public Basket(IEnumerable<string> initial)
    {
        _items = new List<string>(initial); // 생성자에서 한 번 할당 OK
    }

    public void Add(string item)
    {
        // _items 변수 자체는 다른 List로 바꿀 수 없음(참조 고정)
        // 하지만 List 내부에는 항목 추가/삭제가 가능 → 완전 불변 아님
        _items.Add(item);
    }
}

“정말 완전 불변”을 원하면 IReadOnlyList<T>를 노출하거나, 불변 컬렉션(예: ImmutableList<T>)을 사용하세요.

(D) readonly struct & readonly 멤버 (성능/의도)

public readonly struct Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    // 이 메서드는 상태를 절대 바꾸지 않음을 보장
    public readonly int ManhattanDistance() => Math.Abs(X) + Math.Abs(Y);
}
  • readonly struct는 모든 인스턴스 멤버가 상태를 변경하지 않음을 약속합니다.
  • 큰 값 형식을 다룰 때는 in 매개변수(읽기 전용 by-ref)로 복사 비용을 줄일 수 있습니다.

(E) in 매개변수 & ref readonly 반환(고급)

public static class Geometry
{
    // 큰 구조체를 복사 없이 읽기 전용으로 전달
    public static int Distance(in Point p) => p.ManhattanDistance();
}

public static class MinFinder
{
    // 배열에서 최소값 요소를 '읽기 전용 참조'로 돌려주기
    public static ref readonly int MinRef(ReadOnlySpan<int> span)
    {
        if (span.IsEmpty) throw new ArgumentException("empty");
        int minIdx = 0;
        for (int i = 1; i < span.Length; i++)
            if (span[i] < span[minIdx]) minIdx = i;

        return ref span[minIdx]; // 호출 측에서 복사 없이 읽기 전용 참조 사용
    }
}
  • 이런 패턴은 복사를 줄여 성능을 얻는 상황에서 쓰입니다. (특히 큰 struct)

6) 사용 시 주의사항 & 팁

  • 할당 시점: readonly 필드는 선언 시 또는 해당 타입의 생성자 안에서만 할당하세요.
    • 인스턴스 필드 → 인스턴스 생성자
    • static readonly → 정적 필드이므로 정적 생성자에서 할당 가능
  • 프로퍼티에는 readonly 키워드를 직접 붙이지 않습니다. 대신 getter만 제공하거나, C# 9의 init 접근자를 사용해 “초기화만 허용”하는 패턴을 씁니다.
  • public class Person { public string Name { get; init; } // 생성 시점에만 설정 가능 public int Age { get; } = 20; // 사실상 읽기 전용 }
  • 배열/컬렉션은 불변 아님: readonly는 참조 보호일 뿐, 요소 수정은 막지 않습니다. 외부로는 IReadOnlyList<T> 인터페이스로 노출하거나, 불변 컬렉션을 고려하세요.
  • 라이브러리 상수 공개: 공개 API에서 값이 변할 여지가 있거나 버전 영향이 우려되면 public static readonly가 const보다 안전합니다.

 

반응형