GoF 디자인 패턴 4 - [생성] 싱글턴 패턴

김정환·2024년 9월 2일
0

GoF 디자인패턴

목록 보기
4/9

싱글턴 패턴 Singleton Pattern

  • 생성 패턴 중 가장 많이 주목받고 있다.
  • 자원 공유를 위해 객체 생성 개수를 1개로 제한한 패턴이다.

객체 생성


new 키워드

  • 객체는 클래스 선언 후 new 키워드로 인스턴스화해서 생성한다.
  • new 키워드 : 객체를 생성할 유일한 방법.

객체의 중복

  • 객체 생성 : 선언된 클래스에 따라 객체를 메모리에 할당하는 동작.
  • 객체 생성 과정은 new 키워드를 통해 반복 생성할 수 있다.
    • new 키워드로 여러 객체 생성 가능하다.
    • 한 번 선언된 클래스로 동일한 객체를 무제한으로 생성할 수 있다.
    • 시스템 자원이 허락하는 한, 무제한으로 객체 생성 가능.
  • 선언된 클래스 = 모형 틀 ⇒ 모형 틀로 동일한 객체를 수 없이 생성할 수 있다.
    • = 객체 지향의 원리, 특징

유일한 객체


객체지향에선 객체를 무제한으로 생성할 수 있다.
단, 이 장점이 단점이 될 수도 있다.

자원 공유

  • new 키워드로 생성한 객체는 각각 독립된 자원이다.
    • 서로 다른 메모리 영역을 차지하고 있다.
  • 하나의 클래스로 2개의 객체를 생성했다고 가정
    • Class A
      • 객체 A1
      • 객체 A2
    • Class B
      • 객체 B
    • 객체 B는 A1, A2가 동일한 객체라고 착각하는데
      → A1, A2는 메모리 영역이 다른 독립된 자원이다.
      = 객체의 상탯값이 공유되지 않는다.
      = 두 객체는 동일하지 않다.

스코프 (Scope)

  • 프로그래밍에서 변수 구분
    • 변수의 접근 영역 구분(=스코프)에 따라 분류된다.
    • 변수
      • 전역 변수 (Global) : 프로그램 전반에서 접근 가능한 공용 변수이다.
      • 지역 변수 (Local)
  • 함수는 변수의 접근 영역을 구분하는 프로그래밍 방식이다.
    • 함수들은 변수 영역이 서로 나뉘어져 있다 → 함수 간 데이터를 공유하기 힘들다.
    • ⇒ 전역 변수를 이용하여 함수 간 값을 공유 처리한다.
  • 전역 변수로 외부의 값을 공유하면 다양한 문제가 발생할 수 있다.
    • 전역 변수로 값 공유 시, 스코프의 변수 범위를 보다 면밀하게 관리해야 한다.

싱글턴 (Singleton)


  • Singleton = 하나, 단독, 단일체
  • 전역 변수처럼 생성한 객체를 공유하려면 하나의 객체만 존재해야 한다.
    • 객체가 중복된다면 공유할 수 없다.
    • 싱글턴 패턴 :
      • 다른 생성 패턴과 달리, 하나의 객체만 생성을 제한하는 패턴이다.
      • 생성된 객체는 공유되어 어디서든 접근할 수 있다.

유일한 객체

  • 응용 프로그램에서 전역 변수, 공용 장치 등 하나의 객체만 필요한 경우가 많다.
    • ex) 프로그램의 환경 설정 파일 config 등
  • 이런 것이 여럿 있다면 시스템과 프로그램은 자주 충돌할 것이다.
    • 충돌을 방지하려면 단일 객체를 사용해야한다.
    • ⇒ 싱글턴 : 하나의 객체만 유지하게 하는 생성 패턴이다.
  • 싱글턴이 유용한 상황
    • 공유 자원 접근
    • 복수의 시스템이 하나의 자원에 접근할 때
    • 유일한 객체가 필요할 때
    • 값의 캐시가 필요할 때

전역 객체

  • 변수 → 전역 변수
    객체 → ?
    - 객체는 중복 생성이 가능하기 때문에 생성된 객체를 공유하려면 약간의 속임수가 필요하다.
  • 객체를 공유하려면
    1. 객체를 복수로 생성하는 문제가 해결되어야 한다.
    2. 생성되는 객체를 1개의 단일 객체로 유지해야 한다.
    - 단일 객체는 여러 곳에서 접근을 시도해도 결국은 동일한 객체를 사용한다.
            
  • 한 클래스로 생성된 객체의 상탯값을 공유하려면 객체 생성을 제한해야 한다.
    • 제한 :
      new 키워드 사용 시, A1, A2 객체가 생성되는 것이 아니라 동일한 객체 1개만 유지하는 것.
      = 싱글턴
  • 싱글턴 패턴
    • 몇 번의 new 명령을 수행해도 동일한 객체를 반환한다.
    • 1개의 객체만 존재 → 공유해도 문제 발생의 소지가 적다.
      = 전역 변수를 객체 형태로 변형한 전역 객체
  • 객체지향에서 객체 1개만으로 설계하는 경우는 없다.
    • 프로그램 설계 시, 수많은 클래스 선언과 객체를 생성하고 각각의 객체는 고유한 책임을 갖는다. (SRP) ⇒ 객체들은 서로 관계를 맺고 복잡한 동작을 수행한다.
    • 모든 객체는 상호 관계 속에서 작용, 동작하며 객체의 상호 관계 속에는 공유 객체도 존재한다.
  • 싱글턴 패턴을 적용하는 경우
    • 서로 다른 객체가 값을 공유할 때
    • 중복되는 자원을 줄일 때

보증

  • 싱글턴의 중요한 핵심 : 어떻게 하나의 객체만 생성할 수 있는가?
    1. 싱글턴에선 new 키워드를 이용한 객체 생성을 원천적으로 금지한다.
      • 클래스의 생성자 접근 제한으로 new 키워드의 객체 생성 동작을 방해한다.
      • 대신 객체를 생성할 수 있는 메서드를 추가한다.
      • new 키워드 대신 생성 메서드 호출만으로 객체를 생성할 수 있다.
    2. 싱글턴은 내부 참조체가 있어 자신의 객체를 보관한다.
      • 내부적으로 중복 생성을 방지하는 로직(=플라이웨이트 패턴)이 있다.
        ⇒ 참조체를 통해 하나의 객체만 갖도록 보증한다.
  • 싱글턴 패턴을 적용하면
    • 클래스 상속과 복수 객체를 생성할 수 있는 객체지향의 장점을 포기해야 한다.

객체 생성 과정


  • 객체 생성의 프로그램 언어 원리
    • new 키워드는 객체 생성 시, 선언된 클래스의 생성자(constructor)를 호출한다.
    • 생성자(__constructor() : php ⇒ 생성자)
      • 클래스에 생성자를 추가하면 사용자가 정의한 생성자로 동작한다.
      • 필요없는 경우 생략할 수 있다.
      • 매직 키워드
        • ‘__’로 시작하는 특수한 목적을 위한 예약자이다.
        • php, 자바, 파이썬 등

접근 권한

  • 객체지향은 프로퍼티, 메서드 선언 시 접근 권한을 설정한다.
    • 객체의 접근 권한 속성
      • public : 모든 접근이 가능한 권한 (내, 외부 및 상속 포함)
      • private : 내부적인 접근만 허용
      • protected : 상속된 경우에만 허용
  • 일반 클래스를 싱글턴 패턴으로 변환하려면
    new키워드로 객체를 생성하지 못하게 방해해야 한다.
    ⇒ 생성자의 접근 권한을 변경한다.

생성자 제한

  • new 키워드 객체 생성 방해
    • 생성자의 접근 제한 속성은 기본적으로 public이다.
      ⇒ 접근 제한 속성을 private로 변경한다.
    • new 키워드로 생성 시, 에러가 발생한다.
      (외부에서 생성자를 호출하지 못하게 되었으므로 에러가 발생한다.)
  • 싱글턴 패턴은 생성자를 제어하는 것으로 시작
    • 생성자를 꼭 public으로 선언할 필요는 없다.

복제 방지

  • 객체 생성 방법은 new가 유일하다.
    하지만, 예외적으로 객체를 생성할 방법이 있다.
    기존의 생성된 객체를 복제하는 방법 = clone 키워드
  • clone 키워드 또한 실행 시, __clone() 매직 메서드를 호출한다.
  • 객체 생성 과정을 완벽하게 제어하기 위해 복제 메서드도 private로 수정한다.
    ⇒ 이제 어떤 방법으로도 클래스의 객체를 생성할 수 없다.

인스턴스 생성


싱글턴으로 변경한 클래스는 외부적으로 객체를 생성할 수 없다.

객체를 생성하려면 내부에 선언된 메서드를 호출해야 한다.

생성 메서드

  • 싱글턴 내부적으로 객체를 생성하는 특수한 메서드를 추가한다.
// Singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H

class Singleton
{
private:
    Singleton(){};
public:
    ~Singleton(){};
        /*		
            자기자신을 객체로 생성하여 반환.

            정적 메서드로 작성
            ㄴ싱글턴은 아직 생성되지 않아 객체를 통해 호출할 수 없음.
            =>정적 타입으로 설정하여 객체없이도 메서드를 호출. 
        */	
    static Singleton get_instance(){ return Singleton(); }; 
};

#endif

싱글턴에서는 객체를 생성하는 것이 아닌, 객체 생성을 요청한다는 것이 더 정확한 표현.

참조체

  • 위 코드에서 get_instance() 메서드를 여러 번 호출하면, 매번 다른 객체가 생성, 반환된다.
    • 싱글턴은 오직 1개의 객체만 생성해야 하므로
      플라이웨이트 패턴에서 응용되는 참조체를 도입한다.
  • 싱글턴은 내부적으로 하나의 객체만 보장하기 위해 자체 객체를 저장하는 참조체(reference)를 갖고 있으며, 이를 통해 생성 여부를 판단한다.
// Singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H

class Singleton
{
private:
    static Singleton* instance;
    Singleton(){};
public:
    ~Singleton(){};
    static Singleton get_instance();
};

#endif

// Singleton.cpp
#include <iostream>
#include "Singleton.h"

using namespace std;

Singleton* Singleton::instance = nullptr;

Singleton Singleton::get_instance(){
    if(instance == nullptr){
        Singleton inst = Singleton();
        instance = &inst;
    }
    
    return *instance;
}
#include <iostream>
#include "singleton/Singleton.h"
#include "singleton/Singleton.cpp"

using namespace std;

int main(){
		// 1. create new instance.
    Singleton::get_instance();

		// 2. call existing instance.
    Singleton::get_instance();

    return 0;
}

플라이웨이트 패턴

  • 생성된 객체를 공유하는 패턴
    • 싱글턴은 플라이웨이트 패턴의 처리 로직을 추가하여 유일한 객체 생성을 보장한다.
  • 단, 플라이웨이트 패턴의 참조체 적용 시, 코드 양을 증가시키는 단점이 발생한다.
    • 짧고 간단한 코드라도 매번 참조체를 확인하다 보면 성능에 영향을 줄 수 있다.

2가지 책임

  • 싱글턴은 엄밀히 따지면 2가지 책임을 갖는다.
    1. 클래스 설계는 본연의 목적을 해결하기 위해 고유한 처리 로직을 가진다.
      • 클래스는 목적을 해결하기 위한 본연의 책임을 자체적으로 갖는다.
    2. 중복된 객체 생성을 방지하기 위한 책임을 가진다.
      • 참조체를 통한 중복 객체 생성을 방지하기 위한 처리 로직도 포함한다.
  • 단일 책임의 원칙 (SRP)를 위배한다.
  • 객체지향의 원칙을 반드시 적용해야 하는 것은 아니고, 예외적인 상황과 목적을 위해 위배하는 경우도 많다.

정적 클래스


싱글턴 패턴 말고도 정적 클래스를 통해 전역 변수처럼 객체를 공유하는 경우도 있다.

하지만, 둘은 유사해 보일 수 있지만 근본적으로 다르다.

정적 (Static)

  • 정적이라는 용어는 변수 선언 시 자주 등장한다.
    • 변수 선언 ⇒ 메모리 자원 할당
    • 변수 사용 후 ⇒ 시스템에 메모리 자원을 반환
  • 개발 시, 한정된 자원(메모리)을 유용하게 사용하는 것이 중요하다.
    • 시스템 자원 관리를 못하면 시스템이 성능을 제대로 발휘하지 못하거나
      프로그램 동작에 문제가 발생한다.
  • 클래스 = 객체를 선언하기 위한 선언문 = 단순 설계도
    • 클래스 자체로는 실행할 수 없다.
    • 클래스로 객체를 생성한 후, 메서드 호출이 이루어져야 실제 동작이 수행한다.
  • 객체지향도 클래스로 객체를 생성할 경우 변수처럼 시스템 메모리 자원에 할당한다.
    하지만, 정적 클래스는 객체 생성 없이 클래스 선언을 통해 프로그램을 실행시킬 수 있다.
  • 일반적 클래스 : 할당된 메모리의 객체를 통해 호출한다.
    정적 클래스 : 객체를 생성하지 않고, 소스 코드의 클래스 선언 자체를 객체로 인식해 접근한다.
  • 일반적으로 클래스로 생성된 객체는 여러 개를 생성할 수 있다.
    정적 클래스는 소스 코드를 이용한다.→ 여러 개의 객체로 인식하지 않는다.
    - 클래스를 정적으로 사용할 경우 존재하는 객체는 1개다.

static 선언

  • 클래스에 정적 메서드를 정의할 때 static 키워드를 이용한다.
    • static 키워드를 같이 선언하면 해당 메서드를 정적 방식으로 호출할 수 있다.
  • 정적 클래스를 선언했으나 객체는 생성되지 않는다.
    대신 클래스의 메서드를 정적으로 접근하여 호출한다.
    - 공용 변수가 필요할 때는 정적 클래스의 특징을 이용한다.

차이점과 한계

  • 차이
    • 정적 클래스
      • 객체를 메모리에 생성하지 않는다.
      • ⇒ 메모리 관리 차원에서 보면 효과적인 관리 방법이다.
      • 코드가 실행되면서 고정으로 바인딩된다.
    • 싱글턴 패턴
      • 메모리 자원에 할당해서 동적 객체를 만든다.
    • 정적 클래스와 일반 클래스의 차이
      1. 메모리의 상주
      2. 비동적 차이 여부
  • 싱글턴
    • 내부적으로 자기 자신의 객체를 저장하기 위해 정적 프로퍼티와 정적 메서드를 이용한다.
      • 선언된 정적 메서드로 자체 객체를 생성 > 생성된 객체를 정적 프로퍼티에 저장한다.
    • 복잡한 싱글턴 구조보다 정적 클래스가 더 유용한 것처럼 보인다.
      하지만, 개발에선 정적 클래스와 일반 클래스를 엄밀히 구분해서 처리한다.
  • 일반 클래스와 정적 클래스 구분 처리의 이유
    • 객체의 다형성
      • 인터페이스를 통해 클래스의 다양한 동작을 수행할 수 있도록 구현을 대체한다.
      • 정적 클래스는 다형성을 위한 인터페이스를 사용할 수 없다.
  • 유일성 측면
    • 싱글턴 대신 정적 클래스를 사용할 수도 있다.
      단, 정적 클래스만 사용한다면, 필요한 모든 동작 기능이 정적 클래스 안에 존재한다.
  • 정적 클래스가 다른 클래스와 관계를 맺거나, 클래스의 초기화 동작이 복잡할 경우
    ⇒ 정적 클래스만으로는 처리하기 어려워진다.

싱글턴 확장


일반적으로 잘 사용하진 않으나 싱글턴 클래스를 상속받을 수 있다.

제한 범위

  • 싱글턴 = 클래스 ⇒ 객체 생성 및 상속이 가능하다.
  • 하지만, 싱글턴으로 변형된 클래스는 직접 상속받을 수 없다.
    • 생성자를 private로 제한해서 객체를 생성할 수 없다.

protected 속성

  • 싱글턴의 생성자 속성을 변경해 싱글턴 클래스를 상속 받을 수 있다.
    • protected 속성 : 상속만 접근이 허용된 속성
      • new를 통한 생성자 접근을 외부에서 제한할 수 있다. ⇒ private 대체 가능
      • protected 상태에서 상속 가능.

상속

  • protected로 변환된 싱글턴 클래스로 상속, 확장.
// SingletonParent.h
#ifndef SINGLETON_PARENT_H
#define SINGLETON_PARENT_H

class SingletonParent
{
protected:
    static SingletonParent* instance;
    SingletonParent(){};
public:
    ~SingletonParent(){};
};

#endif
  • 생성이 제한된 이 클래스는 어떤 경우에도 객체를 생성할 수 없다.
    ⇒ 하위 클래스에 상속하여 사용한다.
// SingletonChild.h
#include <iostream>

#include "SingletonParent.h"
#include "SingletonChild.h"

using namespace std;

SingletonParent* SingletonParent::instance = nullptr;

SingletonChild::SingletonChild(){}
SingletonChild::~SingletonChild(){}

SingletonChild* SingletonChild::get_instance(){
    if(instance == nullptr){
        SingletonChild inst = SingletonChild();
        instance = &inst;
    }

    return (SingletonChild*)instance;
}

// 행위 메서드 1
void SingletonChild::method_1(){
    cout << "call method_1" << endl;
}

// 행위 메서드 2
void SingletonChild::method_2(){
    cout << "call method_2" << endl;
}
  • 내부에 정적 메서드 get_instance()를 통해 싱글턴 객체 생성.
  • 이 클래스는 생성자가 protected 속성으로 제한된다.
    ⇒ 외부에서 객체 생성은 불가
    ⇒ 객체 생성을 위해선 싱글턴 메서드를 호출해야 한다.
  • 객체 생성은 상속 받은 클래스의 정적 메서드로 생성한다.

자원처리


싱글턴은 자주 사용되는 패턴이지만 다른 개발자는 안티 패턴으로 분류하기도 한다.

경합조건

💡 싱글턴은 하나의 객체만 생성하는 패턴이다.

  • 하지만 특수한 환경에서 단일 객체 생성을 보장하지 못 하는 경우도 있다.

    • 멀티 스레드 환경에서 싱글턴 사용에 주의해야 한다.
    • 자바 등 멀티 스레드 환경을 제공하는 조건에서
      싱글턴 객체 생성이 동시에 요청되는 경우 경합성이 발생한다.
  • 경합 조건 : 동일한 메모리나 자원에 동시에 접근하는 것.

    • 2개 이상의 스레드가 동일한 자원을 사용할 경우 충돌이 발생한다.
    • 경합성 : 메서드의 원자성(Atomic)의 결여로 2개의 객체가 만들어지는 오류이다.
  • 싱글턴은 멀티스레드 환경의 특성 + 경합성 문제를 해결하기 위해 늦은 바인딩을 사용한다.

    • 싱글턴은 정적 호출을 통해 생성 호출하기 전에 객체를 만들지 않는다.
      = 최초 생성 호출 발생 시에 객체 생성 및 내부 참조체에 저장한다.
  • 싱글턴은 객체 요청 시에 객체를 생성하고 자원(메모리)에 할당한다.

    • 늦은 바인딩을 통해 객체 생성을 동적으로 처리하는 것
      = 늦은 초기화 (Lazy Initialization)

💡 단, 늦은 바인딩도 경합성 충돌을 해결할 수는 없다.

  • 경합성과 늦은 초기화 문제 보완을 위해서 시스템 부팅 시 필요한 싱글턴 객체를 미리 생성한다.
    • 싱글턴 처리 부분을 부팅 시 미리 처리하면
      참조체를 구별하는 if문의 오동작을 방지할 수 있다.
    • = 미리 공유 자원을 만들어 실행 도중 발생할 경합성 충돌을 최소화할 수 있다.
  • 단, 실행 중 한번도 사용되지 않는 객체가 있을 수 있다.
    • 불필요한 객체를 모두 생성해 메모리에 상주시키는 것 = 메모리 낭비
    • 매번 공유 자원의 객체에 접근할 때마다 자원 중복 여부 체크도 번거롭다.

메모리

  • 시스템 자원에는 한계가 있다.
    • 메모리 자원도 유한하므로 효율적인 관리가 필요하다.
      • 메모리 필요 시, 동적으로 할당
      • 사용 후에는 반환
  • 싱글턴은 시스템에 유일한 객체를 생성한다.
    • 자원이 필요할 때 동적으로 할당 받을 수 있지만,
      정적으로 생성된 자원을 해제하여 반환하기는 어렵다.
  • 싱글턴 객체 = 공유자원
    • 언제 어디서 쓰일지 알 수 없다. ⇒ 프로그램에 종료될 때까지 생존한다.
  • 싱글턴 패턴은 프로그램 내에서 해제하기 어려운 공유 자원을 많이 사용한다.
    • 이런 점에서 안티 패턴으로 분류하기도 한다.

정리


싱글턴 패턴은 다양한 곳에서 폭 넓게 사용된다.
싱글턴을 올바르게 사용하기 위해선 숙달된 경험이 필요하다.

profile
만성피로 개발자

0개의 댓글