싱글톤은 전역 변수와 거의 동일한 장단점을 가지고 있습니다. 매우 편리하지만 코드의 모듈성을 깨뜨립니다. 싱글톤에 의존하는 클래스를 다른 컨텍스트에서 싱글톤을 다른 컨텍스트로 옮기지 않고 그냥 사용할 수 없습니다. 대부분의 경우 이러한 제한은 단위 테스트를 생성하는 동안 발생합니다.
- 사용 예시: 많은 개발자가 싱글톤 패턴을 안티패턴으로 간주합니다. 그렇기 때문에 C# 코드에서 싱글톤 패턴의 사용이 감소하고 있습니다.
- 식별: 싱글톤은 동일한 캐시된 객체를 반환하는 정적 생성 메서드로 인식할 수 있습니다.
싱글톤을 구현하는 것은 매우 쉽습니다. 생성자를 숨기고 정적 생성 메서드를 구현하기만 하면 됩니다. 멀티스레드 환경에서는 동일한 클래스가 잘못 동작할 수 있습니다. 여러 스레드가 생성 메서드를 동시에 호출하여 싱글톤 클래스의 인스턴스를 여러 개 가져올 수 있기 때문입니다.
따라서, 멀티 스레드를 고려한 전체 코드입니다. 이 예제에서는 Lazy 클래스를 사용한 방법을 사용하였습니다. .NET Framework 4 이상의 Lazy 클래스 사용: .NET 4 이상에서 제공하는 Lazy 클래스를 사용하여 동시성 문제를 해결하는 방법입니다. 이 방법은 간결하고 성능도 우수합니다.
// IUnit.cs
public interface IUnit
{
string Name { get; }
int HP { get; set; }
int AttackPower { get; }
void Attack(IUnit enemy);
}
// Terran.cs
public class Terran : IUnit
{
public string Name { get; }
public int HP { get; set; }
public int AttackPower { get; }
public Terran(string name, int hp, int attackPower)
{
Name = name;
HP = hp;
AttackPower = attackPower;
}
public void Attack(IUnit enemy)
{
enemy.HP -= AttackPower;
}
}
// Protoss.cs
public class Protoss : IUnit
{
public string Name { get; }
public int HP { get; set; }
public int AttackPower { get; }
public Protoss(string name, int hp, int attackPower)
{
Name = name;
HP = hp;
AttackPower = attackPower;
}
public void Attack(IUnit enemy)
{
enemy.HP -= AttackPower;
}
}
// GameManager.cs
public class GameManager
{
// Lazy 클래스를 사용하여 인스턴스 초기화
private static readonly Lazy<GameManager> _lazy = new Lazy<GameManager>(() => new GameManager());
public static GameManager Instance
{
get { return _lazy.Value; }
}
private List<IUnit> _units;
private GameManager()
{
_units = new List<IUnit>();
}
public void CreateUnit(IUnit unit)
{
_units.Add(unit);
}
public void Battle(IUnit unit1, IUnit unit2)
{
while (unit1.HP > 0 && unit2.HP > 0)
{
unit1.Attack(unit2);
if (unit2.HP <= 0)
{
Console.WriteLine($"{unit2.Name} is defeated!");
_units.Remove(unit2);
break;
}
unit2.Attack(unit1);
if (unit1.HP <= 0)
{
Console.WriteLine($"{unit1.Name} is defeated!");
_units.Remove(unit1);
break;
}
}
}
}
// Game.cs
public class Game
{
public static void Main(string[] args)
{
GameManager gameManager = GameManager.Instance;
IUnit marine = new Terran("Marine", 40, 5);
IUnit zealot = new Protoss("Zealot", 60, 8);
gameManager.CreateUnit(marine);
gameManager.CreateUnit(zealot);
gameManager.Battle(marine, zealot);
}
}
4.0 미만 버전에는 아래와 같이 이 문제를 해결하기 위해, 싱글톤 객체를 처음 생성하는 동안 스레드를 동기화해야 합니다.
public class GameManager
{
private static volatile GameManager _instance;
private static readonly object _lock = new object();
public static GameManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new GameManager();
}
}
}
return _instance;
}
}
// 나머지 코드는 동일
}
Singleton 패턴은 오직 하나의 객체만을 생성하도록 하는 디자인 패턴으로, 하나의 객체 인스턴스를 여러 곳에서 공유해서 사용할 때 많이 사용됩니다. 예를 들어, 로그 출력을 위한 유틸리티 클래스나 DB 캐시 등에서 여러 스레드에서 동시에 사용될 때 하나의 인스턴스를 공유해서 사용하면 메모리 낭비를 줄이고 성능을 향상시킬 수 있습니다. 또한, 객체를 생성하는데 들어가는 시간이 많이 걸리거나, 생성된 객체를 재사용해야 하는 경우에도 Singleton 패턴을 많이 사용합니다.
아래 예제는 파일로그를 기재하는 샘플 소스 입니다. (멀티스레드 환경에 맞추어 구현된 소스)
using System;
using System.IO;
public class Logger
{
private static readonly Lazy<Logger> instance = new Lazy<Logger>(() => new Logger());
private static readonly object logLock = new object();
private Logger() { }
public static Logger Instance
{
get
{
return instance.Value;
}
}
public void Log(string message)
{
lock (logLock)
{
string logFilePath = "logs.txt";
string logMessage = $"[{DateTime.Now}] {message}{Environment.NewLine}";
File.AppendAllText(logFilePath, logMessage);
}
}
}
class Program
{
static void Main(string[] args)
{
Task task1 = Task.Run(() => Logger.Instance.Log("Log message from task1"));
Task task2 = Task.Run(() => Logger.Instance.Log("Log message from task2"));
Task.WaitAll(task1, task2);
}
}