생성자와 소멸자는 객체의 시작과 끝을 책임지는 핵심 요소입니다. 생성자는 초기화 로직을, 소멸자(파이널라이저)는 비관리 자원 정리를 다룹니다. 실무에서는 Dispose 패턴과 함께 올바르게 사용하는 것이 중요합니다.
1. 기본/매개변수 생성자
클래스에 생성자를 하나도 정의하지 않으면 컴파일러가 매개변수 없는 기본 생성자를 제공합니다. 하나라도 정의하면 기본 생성자는 자동으로 생성되지 않습니다.
public class Person
{
public string Name { get; }
public int Age { get; }
// 기본 생성자
public Person()
{
Name = "Unknown";
Age = 0;
}
// 매개변수 생성자
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
var p1 = new Person();
var p2 = new Person("Alice", 30);2. 생성자 체이닝과 base 호출
중복 초기화를 줄이려면 this(...)로 체이닝합니다. 상속에서는 base(...)로 기본 클래스 생성자를 명시적으로 호출합니다.
public class ConnectionOptions
{
public string Host { get; }
public int Port { get; }
public ConnectionOptions() : this("localhost", 5432) { }
public ConnectionOptions(string host, int port)
{
Host = host;
Port = port;
}
}
public class BaseService
{
public BaseService(int retryCount) { /* ... */ }
}
public class OrderService : BaseService
{
public OrderService() : base(3)
{
// BaseService 생성자 실행 후 이 본문이 실행됩니다.
}
}3. 정적 생성자
정적 생성자는 타입이 처음 사용되기 전에 한 번만 실행됩니다. 매개변수/접근 제한자를 가질 수 없고, 명시적으로 호출할 수 없습니다.
public class AppConfig
{
public static string RootPath { get; }
static AppConfig()
{
// 한 번만 실행
RootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MyApp");
}
}
// 최초 접근 시 정적 생성자가 실행됩니다.
Console.WriteLine(AppConfig.RootPath);4. 불변 설계와 readonly 초기화
생성자에서만 설정되는 불변 상태를 만들 때 readonly 필드를 사용합니다. 복잡한 상태는 생성자에서 유효성 검사를 통해 일관성을 보장합니다.
public class Money
{
public readonly decimal Amount;
public readonly string Currency;
public Money(decimal amount, string currency)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount));
if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("currency");
Amount = amount;
Currency = currency.ToUpperInvariant();
}
}5. 소멸자(파이널라이저)의 사실
C#의 소멸자 ~ClassName()는 CLR의 파이널라이저입니다. 실행 시점이 비결정적이며, GC에 의해 별도의 파이널라이저 스레드에서 호출됩니다. 일반적으로 비관리 자원 해제가 필요할 때만 구현합니다. 관리 객체에 의존하는 로직은 피해야 합니다.
public class NativeHolder
{
// 예시용 비관리 핸들
private IntPtr _nativeHandle;
public NativeHolder()
{
_nativeHandle = AcquireNative();
}
~NativeHolder()
{
// 최후의 보루: 비관리 자원만 해제
if (_nativeHandle != IntPtr.Zero)
{
ReleaseNative(_nativeHandle);
_nativeHandle = IntPtr.Zero;
}
}
[DllImport("native.dll")] private static extern IntPtr AcquireNative();
[DllImport("native.dll")] private static extern void ReleaseNative(IntPtr h);
}- 소멸자는 오버로드할 수 없고, 직접 호출할 수 없습니다.
- 성능에 영향을 줄 수 있으므로 꼭 필요한 경우에만 사용합니다.
6. IDisposable과 Dispose 패턴 권장
대부분의 경우 소멸자 대신 IDisposable을 구현하고 using을 사용합니다. 비관리 자원이 필요하면 SafeHandle을 쓰고, 소멸자는 마지막 안전장치로 둡니다.
public sealed class FileWriter : IDisposable
{
private bool _disposed;
private readonly FileStream _stream; // 관리 자원 (내부적으로 비관리를 가짐)
public FileWriter(string path)
{
_stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
}
public void WriteLine(string text)
{
ThrowIfDisposed();
var bytes = Encoding.UTF8.GetBytes(text + Environment.NewLine);
_stream.Write(bytes, 0, bytes.Length);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 파이널라이저가 있다면 큐에서 제거
}
protected void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 관리 자원 해제
_stream.Dispose();
}
// 비관리 자원 해제 위치(있다면)
_disposed = true;
}
private void ThrowIfDisposed()
{
if (_disposed) throw new ObjectDisposedException(nameof(FileWriter));
}
}
// 사용 예: using이 Dispose를 보장합니다.
using (var writer = new FileWriter("out.txt"))
{
writer.WriteLine("Hello");
}비관리 자원을 직접 다룰 경우 SafeHandle을 권장합니다. 이때는 별도 소멸자 없이 Dispose에서 SafeHandle.Dispose()만 호출하면 됩니다.
public sealed class NativeWrapper : IDisposable
{
private bool _disposed;
private readonly SafeFileHandle _handle; // SafeHandle 파생형 예시
public NativeWrapper(SafeFileHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
public void Dispose()
{
if (_disposed) return;
_handle.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}7. 베스트 프랙티스 요약
- 생성자에서는 필수 의존성 주입과 유효성 검사만 수행합니다. 무거운 I/O는 지연 초기화로 분리합니다.
- 생성자에서 가상 메서드를 호출하지 않습니다. 파생 클래스 상태가 완전히 초기화되지 않았을 수 있습니다.
- 정적 생성자에서 던진 예외는 타입 초기화를 실패하게 하므로 반드시 처리 또는 안정적으로 작성합니다.
- 소멸자는 최소화하고, IDisposable과 using을 기본으로 사용합니다.
- Dispose에서 GC.SuppressFinalize(this)를 호출해 파이널라이저 오버헤드를 줄입니다.
- 파이널라이저에서는 비관리 자원만 다루고, 다른 관리 객체에 의존하지 않습니다.
생성자와 소멸자를 올바르게 이해하면 객체 수명 주기를 명확하게 관리할 수 있습니다. 특히 자원 관리가 중요한 코드에서는 Dispose 패턴을 표준으로 삼으시길 권장합니다.
'C#' 카테고리의 다른 글
| C# 애트리뷰트 (Attribute) 정의와 활용 (1) | 2026.04.16 |
|---|---|
| C# 접근 제한자 (Access Modifiers) (0) | 2026.04.15 |
| C# ref, out, in 매개변수 한 번에 정리 (0) | 2026.04.14 |
| C# static 키워드 완벽 이해 (1) | 2026.04.13 |
| C# 컬렉션 초기화와 Index/Range (1) | 2026.04.13 |