재우니의 블로그

싱글톤

 

싱글톤은 전역 변수와 거의 동일한 장단점을 가지고 있습니다. 매우 편리하지만 코드의 모듈성을 깨뜨립니다. 싱글톤에 의존하는 클래스를 다른 컨텍스트에서 싱글톤을 다른 컨텍스트로 옮기지 않고 그냥 사용할 수 없습니다. 대부분의 경우 이러한 제한은 단위 테스트를 생성하는 동안 발생합니다.

 

- 사용 예시: 많은 개발자가 싱글톤 패턴을 안티패턴으로 간주합니다. 그렇기 때문에 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;
        }
    }

    // 나머지 코드는 동일
}

 

참고 사이트

 

https://refactoring.guru/design-patterns/singleton/csharp/example#example-1

 

Singleton in C# / Design Patterns

Usage examples: A lot of developers consider the Singleton pattern an antipattern. That’s why its usage is on the decline in C# code. Identification: Singleton can be recognized by a static creation method, which returns the same cached object. Naïve Si

refactoring.guru

 

 


 

실무에 활용해 보기

 

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);
    }
}