1. 참고자료

 

2. Singleton Pattern 개요

더보기

✔️ 핵심 개념

  • 클래스의 인스턴스를 오직 1개만 생성하도록 제한
  • 정적 메서드(Instance, GetInstance()) 를 통해 접근
  • 일반적으로 생성자를 private으로 제한하여 외부에서 직접 생성하지 못하게 함

 

 

🟢 장점:

  • 자원 낭비 방지 (ex: DB 연결, 설정 로딩 등)
  • 전역 상태 유지 (한번 생성된 인스턴스를 재사용)
  • 스레드 동기화로 안정성 향상 가능

 

 

🔴 단점:

  • 전역 상태 → 테스트 어려움
  • 의존성 주입(DI)과 충돌할 수 있음
  • 멀티스레드 환경에서는 구현 시 주의가 필요

 

 

 

 


3. Singleton 예제

더보기

📁 프로젝트 구조

SingletonPatternDemo/
├── Singleton/
│   └── Logger.cs
├── Program.cs

 

 

 

✔️  Logger 클래스 만들기

public class Logger
{  
    // 로그 메시지를 저장할 StringBuilder입니다.
    // StringBuilder는 문자열을 효율적으로 누적할 수 있는 클래스입니다.
    private readonly StringBuilder _logs = new StringBuilder();

    // 로그 기록 메서드입니다. 현재 시간과 함께 메시지를 문자열로 저장합니다.
    // "DateTime.Now"는 현재 시간을 가져오며, "AppendLine"은 줄바꿈 포함 추가입니다.
    public void Log(string message)
    {
        _logs.AppendLine($"{DateTime.Now}: {message}");
    }

    // 지금까지 기록된 로그들을 하나의 문자열로 반환합니다.
    // "ToString()"은 StringBuilder에 저장된 전체 문자열을 반환합니다.
    public string GetLogs()
    {

        return _logs.ToString();
    }

}

 



✔️  Logger 클래스 >> Singleton 패턴으로 만들기

// Logger 클래스는 애플리케이션 전역에서 하나만 존재해야 하는 로깅 클래스입니다.
// Singleton 패턴을 적용하여 인스턴스가 하나만 생성되고 어디서든 접근 가능하도록 만듭니다.
public sealed class Logger
{
    // [1] 정적(static) 필드: 클래스의 인스턴스를 저장하는 필드입니다.
    // "static" 키워드는 클래스 단위로 존재하며 프로그램 실행 동안 하나만 유지됩니다.
    // "Logger?"는 널 가능 타입으로, 아직 인스턴스가 생성되지 않았음을 나타냅니다.
    private static Logger? _instance = null;

    // [2] 동기화용 lock 객체입니다.
    // 여러 스레드가 동시에 Instance 프로퍼티에 접근할 경우,
    // 한 번에 하나의 스레드만 인스턴스를 생성하도록 보장합니다.
    // "readonly"는 생성자 또는 선언 시 한 번만 할당할 수 있게 합니다.
    private static readonly object _lock = new();

    // [3] 로그 메시지를 저장할 StringBuilder입니다.
    // StringBuilder는 문자열을 효율적으로 누적할 수 있는 클래스입니다.
    private readonly StringBuilder _logs = new StringBuilder();

    // [4] 생성자: 외부에서 new Logger()를 호출하지 못하도록 private으로 지정합니다.
    // Singleton 패턴의 핵심으로, 외부 생성 차단을 통해 오직 Instance를 통해서만 생성됩니다.
    private Logger()
    {
        Console.WriteLine("Logger 인스턴스가 생성되었습니다.");
    }

    // [5] 외부에서 Logger 인스턴스를 얻을 수 있는 유일한 접근 지점입니다.
    // get 액세서에서 인스턴스를 생성하거나 이미 있는 것을 반환합니다.
    public static Logger Instance
    {
        get
        {
            // [6] lock 블록: 멀티스레드 환경에서 스레드 간 충돌을 방지합니다.
            // "lock" 키워드는 특정 코드 영역을 한 번에 하나의 스레드만 실행하도록 제한합니다.
            lock (_lock)
            {
                // [7] Lazy Initialization: 인스턴스가 아직 생성되지 않았다면 생성합니다.
                // "??="는 널 병합 할당 연산자로, 왼쪽이 null일 경우 오른쪽 값을 대입합니다.
                return _instance ??= new Logger();
            }
        }
    }

    // [8] 로그 기록 메서드입니다. 현재 시간과 함께 메시지를 문자열로 저장합니다.
    // "DateTime.Now"는 현재 시간을 가져오며, "AppendLine"은 줄바꿈 포함 추가입니다.
    public void Log(string message)
    {
        _logs.AppendLine($"{DateTime.Now}: {message}");
    }

    // [9] 지금까지 기록된 로그들을 하나의 문자열로 반환합니다.
    // "ToString()"은 StringBuilder에 저장된 전체 문자열을 반환합니다.
    public string GetLogs()
    {
        return _logs.ToString();
    }
}



✔️  Logger 테스트

public MainWindow()
{
    InitializeComponent();

    Logger logger1 = Logger.Instance;
    Logger logger2 = Logger.Instance;

    logger1.Log("첫 번째 메시지");
    logger2.Log("두 번째 메시지");

    string v = $"logger1과 logger2는 같은 객체? {object.ReferenceEquals(logger1, logger2)}";
    MessageBox.Show(Logger.Instance.GetLogs() + v);
}

 

 

 

 

✔️ 소스코드

 

WPF_Singleton.zip
0.14MB

 

 

 

 

 

4. static field 예제

더보기

✔️ 요약

  • static이 인스턴스가 아닌 클래스 전체에서 공유되는 변수/메서드라는 개념을 이해한다.
  • static과 non-static의 차이를 눈으로 확인한다.

 

구분 의미 결과
static 필드 클래스 전체에서 공유됨 전 고객이 같은 번호를 공유함
인스턴스 필드 객체마다 별도로 존재함 고객마다 따로 주문 번호 카운트
// 주문횟수를 발급해주는 클래스
public class OrderSystem
{
    // static 필드: 매장에서 필요한 전체 주문 수 카운트
    private static int _cntTotalOrder = 0;

    // 인스턴스 필드: 고객마다 따로 관리되는 개인 주문 횟수
    private int _cntMyOrder = 0;

    // 전체 공유 주문횟수 발급 메서드
    public static int TotalCount()
    {
        _cntTotalOrder++;
        return _cntTotalOrder;
    }

    // 내 개인 주문횟수 발급 메서드
    public int MyCount()
    {
        _cntMyOrder++;
        return _cntMyOrder;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var customer1 = new OrderSystem();
        var customer2 = new OrderSystem();

        Console.WriteLine("고객1의 주문");
        Console.WriteLine($"전체 주문횟수: {OrderSystem.TotalCount()}");  // 1
        Console.WriteLine($"나의 주문횟수: {customer1.MyCount()}");     // 1

        Console.WriteLine("\n고객2의 주문");
        Console.WriteLine($"전체 주문횟수: {OrderSystem.TotalCount()}");  // 2
        Console.WriteLine($"나의 주문횟수: {customer2.MyCount()}");     // 1 (개인 기준)

        Console.WriteLine("\n다시 고객1의 주문");
        Console.WriteLine($"전체 주문횟수: {OrderSystem.TotalCount()}");  // 3
        Console.WriteLine($"나의 주문횟수: {customer1.MyCount()}");     // 2 (개인 기준)
    }
}

 

 

 

 


5. static class 예제

더보기

✔️ 요약

 

항목 설명
생성자 ✅ 생성자는 private static으로 선언 가능하지만, 호출할 수 없습니다.
멤버 ✅ 모든 필드, 메서드, 속성 등 전부 static이어야 함
상속 ❌ 다른 클래스에서 상속받거나 상속할 수 없습니다 (sealed 암묵적 포함됨)
인스턴스 생성 ❌ 인스턴스를 생성할 수 없습니다. (new StaticClass() 불가능)
//올바른 static 클래스 정의
public static class MathHelper
{
    public static int Add(int a, int b)
    {
        return a + b;
    }

    public static double Pi = 3.1415;
}

//사용방법
int result = MathHelper.Add(3, 4);  // 인스턴스 생성 없이 직접 접근
//컴파일 오류 예: static 클래스에 non-static 멤버 선언
public static class WrongExample
{
    public int Count = 0;  // ❌ 오류: static 클래스에는 인스턴스 멤버가 있을 수 없음
}

 

 

 

 


6. static readonly 예제

더보기
public class CafeInfo
{
    // static readonly: 프로그램 시작 시 한 번 초기화되고 이후 변경 불가
    public static readonly string CafeBranchCode = "GWANGJU-001";
    public static readonly DateTime OpeningTime = DateTime.Now;

    public void ShowCafeInfo()
    {
        Console.WriteLine($"지점 코드: {CafeBranchCode}");
        Console.WriteLine($"개점 시간: {OpeningTime}");
    }
}

class Program
{
    static void Main()
    {
        var cafe = new CafeInfo();
        cafe.ShowCafeInfo();

        // ❌ 아래는 불가능: 컴파일 에러 발생
        // CafeInfo.CafeBranchCode = \"SEOUL-999\";
    }
}

 

 


 


7. static readonly vs const 예제

더보기

✔️요약

 

const static readonly
선언 시점 컴파일 시 값이 고정됨 런타임 시 값이 설정됨
변경 여부 완전 불변 (컴파일 타임 상수) 생성자 또는 선언 시 한 번만 설정 가능
타입 숫자, 문자열 등 기본형만 가능 모든 타입 가능 (객체, DateTime 등 포함)
초기화 위치 선언 시에만 가능 선언 시 또는 생성자에서 가능
바인딩 방식 컴파일 타임 상수로 치환됨 런타임에 참조되는 값
사용 용도 변하지 않는 상수 (예: 앱 이름, 빌드 버전) 실행 시 결정되는 상수 (예: 시작 시간)
//const 예제
public class AppConfig
{
    public const string AppName = "MyApp";  // 컴파일 시 값이 고정됨
    public const int MaxUserCount = 100;
}
//static readonly 예제
public class AppConfig
{
    public static readonly string StartupTime = DateTime.Now.ToString();
    public static readonly int ConfigValue;

    static AppConfig()
    {
        ConfigValue = LoadFromConfig();  // 파일이나 DB 등에서 불러올 수 있음
    }

    private static int LoadFromConfig() => 42;
}

 

 

 

 

✔️ 예제 5. Logger 예제에서 Static 사용법 다시 확인하기

// 전역에서 하나만 존재하는 로거 클래스
// Singleton 패턴을 사용하여 인스턴스가 하나만 생성되도록 제한합니다.
public sealed class Logger
{
    // static 키워드는 해당 필드가 클래스 단위로 관리되며,
    // 프로그램 실행 중 하나만 존재함을 의미합니다.
    // null 가능 타입(Logger?)로 초기화되어, 아직 생성되지 않은 상태를 나타냅니다.
    private static Logger? _instance = null;

    // 동기화 처리를 위한 객체입니다.
    // lock 키워드는 여러 스레드가 동시에 접근하는 것을 방지하는데 사용됩니다.
    // readonly는 생성 시에만 초기화 가능하며 이후 변경할 수 없습니다.
    private static readonly object _lock = new();

    // 로그를 누적 저장할 StringBuilder 객체입니다.
    // 문자열을 반복해서 더하는 작업에 적합한 자료형입니다.
    private readonly StringBuilder _logs = new StringBuilder();

    // private 생성자: 외부에서 new Logger()로 생성하지 못하도록 막습니다.
    // Singleton 패턴의 핵심으로, 인스턴스를 외부에서 직접 만들 수 없게 합니다.
    private Logger()
    {
        Console.WriteLine("Logger 인스턴스가 생성되었습니다!");
    }

    // 외부에서 Logger 인스턴스를 사용할 수 있는 유일한 접근 지점
    // Instance 프로퍼티를 통해 클래스의 인스턴스를 가져옵니다.
    public static Logger Instance
    {
        get
        {
            // lock 블록: 한 번에 하나의 스레드만 이 블록을 실행할 수 있게 합니다.
            // 이를 통해 멀티스레드 환경에서도 인스턴스가 한 번만 생성되도록 보장합니다.
            lock (_lock)
            {
                // ??= 연산자는 왼쪽 변수가 null일 경우에만 오른쪽 값을 대입합니다.
                // 즉, 아직 Logger 인스턴스가 없다면 새로 생성하고, 있으면 기존 값을 반환합니다.
                return _instance ??= new Logger();
            }
        }
    }

    // Log 메서드는 현재 시간과 함께 로그 메시지를 저장합니다.
    // AppendLine은 한 줄씩 로그를 추가하며, 자동으로 줄바꿈을 포함합니다.
    public void Log(string message)
    {
        _logs.AppendLine($"{DateTime.Now:HH:mm:ss} - {message}");
    }

    // GetLogs 메서드는 지금까지 누적된 로그 전체를 문자열로 반환합니다.
    // ToString()은 StringBuilder 내부의 모든 문자열을 하나의 문자열로 반환합니다.
    public string GetLogs()
    {
        return _logs.ToString();
    }
}

// 실습용 콘솔 애플리케이션
class Program
{
    static void Main(string[] args)
    {
        // FirstClass에서 로그 남기기
        Console.WriteLine("첫 번째 클래스에서 로그를 남깁니다.");
        FirstClass.DoSomething();

        // SecondClass에서 로그 남기기
        Console.WriteLine("\n두 번째 클래스에서도 로그를 남깁니다.");
        SecondClass.DoSomethingElse();

        // Logger 인스턴스를 통해 전체 로그 출력
        Console.WriteLine("\n모든 로그를 출력합니다.");
        Console.WriteLine(Logger.Instance.GetLogs());

        // logger1 == logger2 검사: 동일 인스턴스인지 확인
        Console.WriteLine("\nogger1 == logger2 ? => " +
            ReferenceEquals(Logger.Instance, Logger.Instance));
    }
}

// 첫 번째 클래스
public class FirstClass
{
    // 로그 남기는 작업 메서드
    public static void DoSomething()
    {
        // Logger 인스턴스를 통해 로그 기록
        Logger.Instance.Log("FirstClass에서 작업 수행");
    }
}

// 두 번째 클래스
public class SecondClass
{
    // 로그 남기는 작업 메서드
    public static void DoSomethingElse()
    {
        // Logger 인스턴스를 통해 로그 기록
        Logger.Instance.Log("SecondClass에서 다른 작업 수행");
    }
}

 

 

 

 


8. lock 사용 예제

더보기

✔️ 학습 목표

  • lock 키워드는 멀티스레드 환경에서 데이터 충돌을 방지한다는 것을 이해한다.
  • lock을 사용하지 않으면 예상하지 못한 결과나 오류가 발생할 수 있음을 확인한다.

 

 

✔️ 시나리오: “은행 잔고에서 출금하기”

  • 여러 개의 스레드가 동시에 하나의 은행 계좌에서 돈을 출금합니다.
  • lock을 사용하지 않으면 잔고가 음수가 되는 이상한 결과가 나올 수 있습니다.

 

 

✔️ 예제 1: lock을 사용하지 않은 버전 (문제 발생)

class BankAccount
{
    public int Balance = 1000;

    public void Withdraw(int amount)
    {
        if (Balance >= amount)
        {
            Console.WriteLine($"출금 요청: {amount}원");
            Thread.Sleep(10); // 일부러 딜레이 줘서 충돌 유도
            Balance -= amount;
            Console.WriteLine($"출금 완료! 현재 잔고: {Balance}원");
        }
        else
        {
            Console.WriteLine("잔고 부족!");
        }
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount();

        Thread t1 = new Thread(() => account.Withdraw(700));
        Thread t2 = new Thread(() => account.Withdraw(500));

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"최종 잔고: {account.Balance}원");
    }
}

//예상 출력(동기화 문제 발생)
//출금 요청: 700원
//출금 요청: 500원
//출금 완료! 현재 잔고: 300원
//출금 완료! 현재 잔고: -200원 ← ❗️잔고가 마이너스X
//최종 잔고: -200원
//여러 스레드가 동시에 Balance를 읽고 수정해서 오류 발생

  

 

✔️ 예제2: lock을 사용한 버전 (안전한 결과)

class BankAccount
{
    public int Balance = 1000;
    private readonly object _lock = new();

    public void Withdraw(int amount)
    {
        lock (_lock)
        {
            if (Balance >= amount)
            {
                Console.WriteLine($"출금 요청: {amount}원");
                Thread.Sleep(10); // 일부러 딜레이 줘서 테스트
                Balance -= amount;
                Console.WriteLine($"출금 완료! 현재 잔고: {Balance}원");
            }
            else
            {
                Console.WriteLine("잔고 부족!");
            }
        }
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount();

        Thread t1 = new Thread(() => account.Withdraw(700));
        Thread t2 = new Thread(() => account.Withdraw(500));

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"최종 잔고: {account.Balance}원");
    }
}