[Design Pattern] 1-1. 싱글톤 패턴 (Singleton pattern)


Study/Design Pattern  2021. 6. 9. 18:23

안녕하세요. 명월입니다.


이 글은 디자인 패턴의 싱글톤 패턴(Singleton pattern)에 대한 글입니다.


싱글톤 패턴은 디자인 패턴 중에서 가장 유명한 패턴입니다. 디자인 패턴을 못 들어봐도 싱글톤 패턴을 들어봤을 정도로 유명한 패턴입니다.

싱글톤 패턴은 클래스의 인스턴스를 프로그램 실행 중에 딱 한번만 생성하고 계속 재사용하는 패턴입니다. 이점으로는 클래스의 데이터를 변환하지 않고 계속적으로 사용하거나 모든 객체에서 데이터를 공유해야 하는 경우에 사용합니다.


먼저 C/C++예제로 살펴보겠습니다.

// 중복 검사 전처리문
#pragma once
// 해더 선언
#include <stdio.h>
#include <iostream>
using namespace std;
// 클래스 선언
class Node
{
private:
  // 싱글톤 변수 설정
  static Node* singleton;
  // 생성자를 private에 설정해서 외부에서 생성 금지
  Node() {}
protected:
public:
  // 인스턴스를 취득하는 함수
  static Node* getInstance() {
    // 싱글톤 변수가 null이면
    if (singleton == nullptr) {
      // 메모리 할당 (클래스 내부이기 때문에 인스턴스 생성이 가능)
      singleton = new Node();
    }
    // 포인터를 반환
    return singleton;
  }
  // 출력 함수
  void print() {
    // 화면 출력
    cout << "hello world" << endl;
  }
};
// 싱글톤 변수 설정 초기화
Node* Node::singleton = nullptr;
// 실행 함수
int main()
{
  // 인스턴스 취득
  Node* node1 = Node::getInstance();
  // 인스턴스 취득
  Node* node2 = Node::getInstance();
  // 함수 실행
  node1->print();
  
  // 메모리 주소를 출력
  printf("%d\n", node1);
  // 메모리 주소를 출력
  printf("%d\n", node2);
  
  // Node 클래스 메모리 해지
  delete Node::getInstance();
  return 0;
}

싱글톤의 특성은 생성자를 private에 설정하는 게 중요합니다. 생성자를 private에 넣으면 클래스 외부에서는 인스턴스 생성을 할 수 없습니다.

그리고 클래스의 인스턴스는 프로그램이 종료될 때까지 유지되어야 하기 때문에 static으로 선언하여 클래스 내부에서 관리합니다.

다시 static 함수(getInstance())를 이용해서 클래스 인스턴스를 생성하고 singleton 변수에서 관리하게 되면 인스턴스를 한번 생성하면 계속해서 재사용할 수 있는 싱글톤 패턴이 만들어 집니다.


위 결과를 보시면 node1와 node2의 메모리 주소가 같습니다.

즉, Node::getInstance()로 계속 인스턴스를 취득해도 같은 클래스가 반환됩니다.

// 실행 함수가 있는 클래스
public class Program {
  // 실행 함수
  public static void main(String[] args) {
    // Node 인스턴스 취득
    Node node1 = Node.getInstance();
    // Node 인스턴스 취득
    Node node2 = Node.getInstance();
    // 함수 실행
    node1.print();
    
    // node1 인스턴스 메모리 주소 출력
    System.out.println(node1.hashCode());
    // node2 인스턴스 메모리 주소 출력
    System.out.println(node2.hashCode());
  }
}
// Node 클래스
class Node {
  // 싱글톤 변수 설정
  private static Node singleton;
  // 생성자를 private로 설정, 즉 외부 클래스에서는 인스턴스 생성이 안됨
  private Node() {}
  // 인스턴스 취득 함수
  public static Node getInstance() {
    // 싱글톤 변수가 null이면 인스턴스 생성
    if (singleton == null) {
      // 인스턴스 생성
      singleton = new Node();
    }
    // 인스턴스를 반환
    return singleton;
  }
  // 출력 함수
  public void print() {
    // 콘솔 출력
    System.out.println("Hello world");
  }
}

Java에서의 싱글톤도 생성자를 private로 설정하고 클래스 인스턴스를 가지고 있을 변수를 private static으로 선언합니다.

즉, 프로그램이 종료될 때까지 변수 내에서 인스턴스가 유지됩니다.


여기서도 hashcode를 출력해서 보니 같은 값이 출력되는 것을 확인할 수 있습니다.

즉, 같은 클래스인 것을 확인할 수 있습니다.

using System;

namespace TestExample1
{
  // Node 클래스
  class Node
  {
    // 싱글톤 변수 설정
    private static Node singleton;
    // 생성자를 private로 설정, 즉 외부 클래스에서는 인스턴스 생성이 안됨
    private Node() { }
    // 인스턴스 취득 함수
    public static Node GetInstance()
    {
      // 싱글톤 변수가 null이면 인스턴스 생성
      if (singleton == null)
      {
        // 인스턴스 생성
        singleton = new Node();
      }
      // 인스턴스를 반환
      return singleton;
    }
    // 출력 함수
    public void Print()
    {
      // 콘솔 출력
      Console.WriteLine("Hello world");
    }
  }
  // 실행 함수가 있는 클래스
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // Node 인스턴스 취득
      Node node1 = Node.GetInstance();
      // Node 인스턴스 취득
      Node node2 = Node.GetInstance();
      // 함수 실행
      node1.Print();
      // node1 인스턴스 메모리 주소 출력
      Console.WriteLine(node1.GetHashCode());
      // node2 인스턴스 메모리 주소 출력
      Console.WriteLine(node2.GetHashCode());

      Console.WriteLine("Press Any Key...");
      Console.ReadKey();
    }
  }
}

C#에서도 역시 생성자를 private로 설정하고 GetInstance() 함수를 이용해서 인스턴스를 취득합니다.

결과도 같은 HashCode가 출력되니 같은 클래스 인스턴스임을 확인할 수 있습니다.


싱글톤 패턴은 보통 리소스를 다루는 클래스에서 자주 사용합니다.

예를 들면 File IO라던가 통신 Socket 클래스에서 같이 사용합니다.

왜냐하면 하나의 File을 읽고 쓰는 클래스를 여러 인스턴스를 선언해서 접속하게 되면 connection error가 발생할 것입니다.

예를 들면 로그 시스템이 그 예가 되겠네요.. 로그 클래스를 여기저기서 인스턴스를 생성해서 파일 쓰기를 하게 되면 에러가 나는 것입니다.

using System;
using System.IO;
using System.Text;
using System.Threading;

namespace TestExample1
{
  // 로그 클래스 예제
  class LogSystem
  {
    // 파일에 메시지를 작성하는 함수
    public void Write(String msg)
    {
      // Stream을 생성
      using (var stream = new FileStream("d:\\work\\log.log", FileMode.Append, FileAccess.Write))
      {
        // String을 UTF8형식으로 변환
        var b = Encoding.UTF8.GetBytes(msg);
        // 파일에 메시지 작성
        stream.Write(b, 0, b.Length);
      }
    }
  }
  // 실행 함수가 있는 클래스
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 스레드
      ThreadPool.QueueUserWorkItem((_) =>
      {
        // 로그 클래스 예제 인스턴스 생성
        var log = new LogSystem();
        // 0부터 99까지
        for (int i = 0; i < 100; i++)
        {
          // 메시지 작성
          log.Write(i.ToString() + "\r\n");
        }
      });
      // 스레드
      ThreadPool.QueueUserWorkItem((_) =>
      {
        // 로그 클래스 예제 인스턴스 생성
        var log = new LogSystem();
        // 0부터 99까지 작성
        for (int i = 0; i < 100; i++)
        {
          // 메시지 작성
          log.Write(i.ToString() + "\r\n");
        }
      });

      Console.WriteLine("Press Any Key...");
      Console.ReadKey();
    }
  }
}

하나의 클래스이지만 각기의 인스턴스를 생성해서 실행하게 되면 IOException이 발생합니다.

(일본어로 표시되지만 뜻은 다른 프로세스에서 파일이 사용중이여서 액세스할 수 없습니다라는 뜻입니다.)

소켓도 하나의 인스턴스로 port를 열고 대기 중인데 다른 인스턴스로 접속하게 되면 port 사용 중이라는 에러가 발생할 것입니다.

그런 상황을 피하기 위해서 하나의 인스턴스를 유지해야 하는데 그럴 때에 싱글톤 패턴을 사용하게 됩니다.


여기까지 디자인 패턴의 싱글톤 패턴(Singleton pattern)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.