sealed 키워드는 클래스의 상속을 금지하거나, 오버라이드한 멤버의 추가 오버라이드를 막을 때 사용합니다. 불필요한 확장을 제한해 설계 의도를 명확히 하고, JIT 최적화에 도움을 줘 성능상 이점도 얻을 수 있습니다.
1. sealed 클래스: 더 이상 상속 불가
클래스에 sealed를 붙이면 해당 클래스를 상속할 수 없습니다. 대신 인터페이스 구현은 가능합니다.
public sealed class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// 아래 코드는 컴파일 오류입니다. sealed 클래스는 상속할 수 없습니다.
// public class Bonus : Money {}
public interface ICurrency { string Code { get; } }
public sealed class Won : ICurrency
{
public string Code => "KRW";
}
참고로 struct는 본질적으로 상속이 불가능하며, static class는 컴파일러가 sealed abstract로 처리하여 상속과 인스턴스 생성을 모두 막습니다.
2. sealed override: 더 이상 오버라이드 불가
오버라이드한 멤버에 sealed를 붙이면 그 이후 파생 클래스에서 해당 멤버를 다시 오버라이드할 수 없습니다.
public class Shape
{
public virtual double Area() => 0;
}
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double w, double h)
{
Width = w; Height = h;
}
public sealed override double Area() => Width * Height;
}
public class Square : Rectangle
{
public Square(double s) : base(s, s) { }
// 아래 코드는 컴파일 오류입니다. Area는 sealed override입니다.
// public override double Area() => base.Area();
}
속성, 인덱서, 이벤트도 동일하게 sealed override를 적용할 수 있습니다.
public class Config
{
public virtual string Mode { get; set; } = "dev";
}
public class AppConfig : Config
{
public sealed override string Mode { get; set; } = "prod";
}
3. new 숨김과 sealed의 차이
new는 멤버를 숨길 뿐, 다형적 디스패치를 막지 않습니다. sealed override는 아예 오버라이드 자체를 차단합니다.
public class Base
{
public virtual void Run() => Console.WriteLine("Base");
}
public class Mid : Base
{
public sealed override void Run() => Console.WriteLine("Mid");
}
public class Leaf : Mid
{
// 오버라이드는 금지되므로 new로 숨길 수는 있지만 다형성은 유지되지 않습니다.
public new void Run() => Console.WriteLine("Leaf");
}
Base b = new Leaf();
b.Run(); // Mid (기반 형식 관점의 가상 호출)
((Leaf)b).Run(); // Leaf (정적 바인딩)
4. 언제 sealed를 사용할까
명시적으로 확장 지점을 제공할 의도가 없다면 클래스는 기본적으로 sealed로 두는 것이 안전합니다. 라이브러리/공용 API는 상속 계약을 지원하려면 가상 멤버, 보호 생성자, 확장 지침이 필요합니다. 그렇지 않다면 sealed로 닫아 버전 호환성 문제와 예기치 않은 사용을 줄입니다.
도메인 모델에서 개념적으로 더 이상 세분화할 필요가 없는 타입(예: Money, Email, Id 등)에 sealed를 적용하면 모델이 단순해지고 불변 설계와도 잘 맞습니다.
5. 성능 이점: 디버추얼라이제이션과 인라이닝
JIT는 호출 대상이 확정되는 sealed 타입/멤버에서 가상 호출을 정적 호출로 바꾸고, 인라이닝을 쉽게 적용할 수 있습니다. 빈번한 호출 경로에서 미세하지만 누적 성능 이점을 기대할 수 있습니다.
public class Counter
{
public virtual int Next(int x) => x + 1;
}
public sealed class FastCounter : Counter
{
public sealed override int Next(int x) => x + 1; // JIT가 쉽게 인라인/디버추얼라이즈
}
물론 최신 JIT는 분석을 통해 비sealed에서도 최적화를 시도하지만, sealed는 의도를 명확히 하고 최적화 가능성을 높입니다.
6. 주의할 점과 팁
테스트 용이성: 일부 목킹 프레임워크는 sealed 클래스/멤버를 대리(mock)하기 어렵습니다. 외부 확장이 필요한 지점은 인터페이스나 가상 멤버로 설계합니다.
호환성: 나중에 클래스를 sealed로 바꾸면 파생 클래스를 사용하던 코드가 깨지는 중대한 변경입니다. 반대로 sealed를 제거해 상속을 허용하는 것은 대개 호환되지만, 상속 계약을 충분히 문서화해야 합니다.
조합 규칙: abstract와 sealed를 동시에 클래스에 사용할 수 없습니다. 부분 클래스(partial)는 sealed를 허용하지만, 선언 전체가 일관되게 동일한 한정자를 가져야 합니다.
record: record class는 필요 시 sealed로 선언할 수 있습니다. record struct는 struct 특성상 상속이 불가합니다.
7. 요약
sealed는 “여기서 계층을 닫는다”는 의도를 컴파일러와 사용자에게 전달하는 강력한 키워드입니다. 상속이 설계 요구사항이 아니라면 기본적으로 sealed를 적용하고, 꼭 필요한 지점에만 명시적으로 확장 포인트를 제공하는 것이 유지보수성과 성능 모두에 유리합니다.
'C#' 카테고리의 다른 글
| C# Reflection으로 Private 멤버 접근 및 테스트 활용 (0) | 2026.06.30 |
|---|---|
| C# 이벤트 vs 콜백(CallBack) 비교와 선택 기준 (0) | 2026.06.26 |
| C# 인스턴스 멤버와 정적 멤버의 메모리 차이 (0) | 2026.06.25 |
| C# 객체 초기화(Object Initializer)와 컬렉션 초기화 비교 (0) | 2026.06.25 |
| C# 로컬 변수 초기화와 스코프 관리 (0) | 2026.06.25 |