C#에서 sealed 키워드는 해당 클래스를 더 이상 상속할 수 없다는 의미입니다. 반면 생성자를 private으로 선언하면 클래스 외부에서 인스턴스를 생성할 수 없게 됩니다.
그런데 여기서 주의할 점이 있습니다. private 생성자를 가진 기본 클래스라 하더라도 상속 자체는 여전히 가능합니다. 파생 클래스에서 public 생성자를 새로 정의하면, 그 과정에서 기본 클래스의 생성자가 호출되어 결국 기본 클래스의 인스턴스까지 만들어지게 됩니다.
핵심은 두 가지입니다. 첫째, 생성자는 상속되지 않습니다. 즉, 기본 클래스의 생성자가 모두 private이라 해서 파생 클래스가 자동으로 private 생성자를 갖는 것은 아닙니다. 둘째, 파생 클래스는 객체 생성 시 항상 기본 클래스의 생성자를 먼저 호출합니다.
따라서 클래스를 sealed로 선언하면 다른 개발자가 상속을 통해 신중하게 설계된 싱글톤 구조를 쉽게 우회하는 것을 차단할 수 있습니다. 이것이 싱글톤 클래스를 항상 봉인(sealed) 처리해야 하는 핵심 이유입니다.
예제 코드
static class Program {
static void Main(string[] args){
Singleton fromStudent = Singleton.GetInstance;
fromStudent.PrintDetails("From Student");
Singleton fromEmployee = Singleton.GetInstance;
fromEmployee.PrintDetails("From Employee");
Console.WriteLine("-------------------------------------");
Singleton.DerivedSingleton derivedObj = new Singleton.DerivedSingleton();
derivedObj.PrintDetails("From Derived");
Console.ReadLine();
}
}
public class Singleton {
private static int counter = 0;
private static object obj = new object();
private Singleton() {
counter++;
Console.WriteLine("Counter Value " + counter.ToString());
}
private static Singleton instance = null;
public static Singleton GetInstance{
get {
if (instance == null)
instance = new Singleton();
return instance;
}
}
public void PrintDetails(string message){
Console.WriteLine(message);
}
public class DerivedSingleton : Singleton {
}
}
위 예제의 Singleton 클래스는 sealed로 선언되지 않았습니다. 그 결과 중첩 클래스인 DerivedSingleton이 Singleton을 상속할 수 있으며, 중첩 클래스는 외부 클래스의 private 멤버에 접근할 수 있으므로 private 생성자 호출까지 가능해집니다.
프로그램을 실행하면 GetInstance를 통해 최초 인스턴스가 생성될 때 "Counter Value 1"이 출력되고, fromEmployee는 동일한 인스턴스를 재사용하므로 카운터가 늘어나지 않습니다. 그러나 DerivedSingleton 객체를 생성하는 순간 기본 클래스의 생성자가 다시 호출되어 "Counter Value 2"가 출력됩니다. 즉, 단 하나의 인스턴스만 존재해야 한다는 싱글톤 패턴의 핵심 규칙이 상속 하나로 무너지는 것입니다.
클래스 선언부를 public sealed class Singleton으로 변경하면 이러한 상속 시도가 컴파일 오류로 차단되며, 싱글톤 패턴의 무결성을 확실하게 보장할 수 있습니다.