-
C# Singleton Pattern(구현 방법, 각각의 장단점, 적합한 환경)공부/C# 2023. 10. 7. 12:27
최근 Unity로 간단한 RPG게임을 만들어보면 게임의 메인 흐름을 관리하는
Managers Class
를 구현했습니다. 이 Class는 게임의 상태를 관리하고, UI, 네트워크, 사운드, Scene 관리 등의 기능을 수행합니다. Unity에서Managers Class
를 구현할 때 메모리 관리, 속도 향상, 데이터 공유, 객체 생성 제한 등의 이유로 Singleton 패턴을 사용했습니다.Singleton 패턴을 구현하면서 적절한 방법을 채택하고 주의해야할 점을 알기위해 해당 글을 작성하게 되었습니다.
1. Singleton 패턴의 다양한 구현 방법
- Lazy Initialization (게으른 초기화): 이 방법은 Singleton 인스턴스가 필요할 때까지 초기화를 연기합니다. 이 방법은 런타임 오버헤드가 적지만, 첫 번째 호출에서 약간의 지연이 발생할 수 있습니다. 메모리 오버헤드는 일반적으로 낮습니다.
- Eager Initialization (즉시 초기화): 이 방법은 애플리케이션 시작 시 Singleton 인스턴스를 초기화합니다. 이 방법은 런타임 오버헤드가 없지만, 인스턴스가 실제로 필요하지 않은 경우에도 메모리를 차지하게 됩니다.
- Double-Checked Locking (이중 검사 잠금): 이 방법은 Singleton 인스턴스에 대한 동시 액세스를 제어하기 위해 잠금을 사용합니다. 이 방법은 런타임 오버헤드가 높을 수 있지만, 멀티스레드 환경에서 안전성을 보장합니다. 메모리 오버헤드는 일반적으로 낮습니다.
- ThreadLocal Singleton (스레드 로컬 싱글톤): 이 방법은 각 스레드에 대해 별도의 Singleton 인스턴스를 유지합니다. 이 방법은 런타임 오버헤드가 적지만, 메모리 오버헤드가 높을 수 있습니다.
각 방법의 선택은 애플리케이션의 요구 사항과 성능 목표에 따라 달라집니다. 예를 들어, 멀티스레드 안전성이 중요한 경우 Double-Checked Locking을 고려할 수 있습니다. 반면, 메모리 사용량이 중요한 경우 Lazy Initialization을 고려할 수 있습니다.
2. C#으로 Singleton 패턴 구현하기
Lazy Initialization
Lazy Initialization은 Singleton 패턴에서 인스턴스가 필요할 때까지 초기화를 연기하는 방법입니다. 이 방법은 메모리를 효율적으로 사용하며, 인스턴스가 실제로 필요하지 않은 경우에는 초기화를 완전히 피할 수 있습니다.
다음은 C#을 사용한 Lazy Initialization의 예입니다:
public sealed class Singleton { private static Singleton _instance = null; private static readonly object _lock = new object(); Singleton() { } public static Singleton Instance { get { if (_instance == null) { lock (_lock) { if (_instance == null) { _instance = new Singleton(); } } } return _instance; } } }
위의 코드에서는 _instance가 null인지 확인하고, null인 경우에만 새 Singleton 인스턴스를 생성합니다. 이렇게 하면 인스턴스가 필요할 때까지 초기화를 연기할 수 있습니다. 또한, lock 문을 사용하여 동시에 여러 스레드가 인스턴스를 생성하지 못하도록 합니다.
이 방법의 장점은 런타임 오버헤드가 적다는 것입니다. 즉, 애플리케이션이 시작될 때 모든 Singleton 인스턴스를 초기화할 필요가 없으므로 애플리케이션 시작 시간을 단축할 수 있습니다. 또한, 인스턴스가 실제로 필요하지 않은 경우 메모리를 절약할 수 있습니다.
단점은 첫 번째 호출에서 약간의 지연이 발생할 수 있다는 것입니다. 이는 인스턴스를 처음 생성할 때 추가적인 처리가 필요하기 때문입니다. 그러나 이 지연은 일반적으로 미미하며, 대부분의 경우에서는 무시할 수 있는 수준입니다.
Eager Initialization
Eager Initialization은 Singleton 패턴에서 애플리케이션 시작 시 Singleton 인스턴스를 즉시 초기화하는 방법입니다. 이 방법은 런타임 오버헤드를 최소화하며, 인스턴스가 필요한 첫 번째 호출에서 지연을 방지할 수 있습니다.
다음은 C#을 사용한 Eager Initialization의 예입니다:
public sealed class Singleton { private static readonly Singleton _instance = new Singleton(); // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Singleton() { } private Singleton() { } public static Singleton Instance { get { return _instance; } } }
위의 코드에서는 애플리케이션 시작 시 _instance를 즉시 초기화합니다. 이렇게 하면 인스턴스가 필요한 첫 번째 호출에서 추가적인 처리 없이 인스턴스에 즉시 액세스할 수 있습니다.
이 방법의 장점은 런타임 오버헤드가 없다는 것입니다. 즉, 애플리케이션 실행 중에 추가적인 처리 없이 인스턴스에 액세스할 수 있습니다.
단점은 메모리 사용량이 높을 수 있다는 것입니다. 즉, 인스턴스가 실제로 필요하지 않은 경우에도 메모리를 차지하게 됩니다. 그러나 이는 일반적으로 미미한 단점으로 간주되며, 대부분의 경우에서는 무시할 수 있는 수준입니다.
Double-Checked Locking
Double-Checked Locking은 Singleton 패턴에서 동시 액세스를 제어하기 위해 잠금을 사용하는 방법입니다. 이 방법은 런타임 오버헤드가 높을 수 있지만, 멀티스레드 환경에서 안전성을 보장합니다.
다음은 C#을 사용한 Double-Checked Locking의 예입니다:
public sealed class Singleton { private static Singleton _instance = null; private static readonly object _lock = new object(); Singleton() { } public static Singleton Instance { get { if (_instance == null) { lock (_lock) { if (_instance == null) { _instance = new Singleton(); } } } return _instance; } } }
위의 코드에서는 _instance가 null인지 확인하고, null인 경우에만 새 Singleton 인스턴스를 생성합니다. 이렇게 하면 인스턴스가 필요할 때까지 초기화를 연기할 수 있습니다. 또한, lock 문을 사용하여 동시에 여러 스레드가 인스턴스를 생성하지 못하도록 합니다.
이 방법의 장점은 멀티스레드 환경에서 안전성을 보장한다는 것입니다. 즉, 동시에 여러 스레드가 인스턴스를 생성하는 것을 방지하여 데이터 무결성을 유지할 수 있습니다.
단점은 런타임 오버헤드가 높을 수 있다는 것입니다. 즉, 잠금 메커니즘은 추가적인 처리를 필요로 하므로, 인스턴스에 액세스하는 데 시간이 더 걸릴 수 있습니다. 그러나 이는 일반적으로 미미한 단점으로 간주되며, 대부분의 경우에서는 무시할 수 있는 수준입니다.
ThreadLocal Singleton
ThreadLocal Singleton은 각 스레드에 대해 별도의 Singleton 인스턴스를 유지하는 방법입니다. 이 방법은 런타임 오버헤드가 적지만, 메모리 오버헤드가 높을 수 있습니다.
다음은 C#을 사용한 ThreadLocal Singleton의 예입니다:
public sealed class Singleton { private static readonly ThreadLocal<Singleton> _instance = new ThreadLocal<Singleton>(() => new Singleton()); private Singleton() { } public static Singleton Instance { get { return _instance.Value; } } }
위의 코드에서는 ThreadLocal를 사용하여 각 스레드에 대해 별도의 Singleton 인스턴스를 유지합니다. 이렇게 하면 각 스레드는 자체 Singleton 인스턴스에 액세스할 수 있습니다.
이 방법의 장점은 런타임 오버헤드가 적다는 것입니다. 즉, 각 스레드는 자체 인스턴스에 직접 액세스할 수 있으므로 추가적인 처리 없이 인스턴스에 액세스할 수 있습니다.
단점은 메모리 오버헤드가 높을 수 있다는 것입니다. 즉, 각 스레드는 자체 Singleton 인스턴스를 유지하므로 메모리 사용량이 늘어날 수 있습니다. 그러나 이는 일반적으로 미미한 단점으로 간주되며, 대부분의 경우에서는 무시할 수 있는 수준입니다.
각 Singleton 패턴 방법이 더 적합한 환경
- Lazy Initialization (게으른 초기화): 이 방법은 메모리 사용량이 중요한 경우에 적합합니다. 인스턴스가 실제로 필요하지 않은 경우 초기화를 완전히 피할 수 있으므로 메모리를 절약할 수 있습니다. 또한, 애플리케이션 시작 시간을 단축하려는 경우에도 유용합니다.
- Eager Initialization (즉시 초기화): 이 방법은 인스턴스에 대한 첫 번째 호출에서 지연을 방지하려는 경우에 적합합니다. 애플리케이션 실행 중에 추가적인 처리 없이 인스턴스에 액세스할 수 있으므로, 빠른 응답 시간이 필요한 경우에 유용합니다.
- Double-Checked Locking (이중 검사 잠금): 이 방법은 멀티스레드 환경에서 안전성이 중요한 경우에 적합합니다. 동시에 여러 스레드가 인스턴스를 생성하는 것을 방지하여 데이터 무결성을 유지할 수 있습니다.
- ThreadLocal Singleton (스레드 로컬 싱글톤): 이 방법은 각 스레드가 독립적으로 작동해야 하는 경우에 적합합니다. 각 스레드는 자체 Singleton 인스턴스에 액세스할 수 있으므로, 스레드 간에 상태를 공유하지 않아도 됩니다.