티스토리 뷰

디자인 패턴 관련한 엄청 좋은 사이트를 발견했다. 이제 여기 게시글을 한개씩 정리하면서 미뤄왔던 디자인 패턴 공부를 해봐야겠다. 💪🤔

 

👾singleton

 

Singleton

Real-World Analogy The government is an excellent example of the Singleton pattern. A country can have only one official government. Regardless of the personal identities of the individuals who form governments, the title, “The Government of X”, is a g

refactoring.guru

 

📝한 줄 정리

어느 코드 영역에서든지 이 클래스에 대해 유일하고 동일한 인스턴스를 참조하고 있음을 보장하도록 만든다.

 

 

 

🤔해결할 문제 상황

1. 클래스가 오직 하나의 인스턴스만 가짐을 보장할 수 있어야 한다.

=> 왜 이런 니즈가 있을까? 공유 자원 (db, file 같은)에 대한 접근을 컨트롤할 필요가 있기 때문에. 

객체를 생성한 다음 다시 객체를 생성하려고 할때는 새로운 객체 대신 이전에 생성한 그 객체를 리턴 받는 식으로 동작 해야 한다. 그러려먼 무조건 새로운 객체를 리턴하는 일반적인 constructor 로는 해결이 안된다.

 

2. 해당 인스턴스에 대해 global access point를 제공해야 한다.

=> 프로그램 어디서나 이 클래스에 대해 접근 가능해야 하지만 함부로 overwrite하지 못하게 막고, 1의 기능을 하는 코드들을 한 클래스에서 모아서 관리해야 할 필요성이 있다.

 

 

 

🧐해결 방법

모든 싱글턴 패턴의 구현체들은 이 2스텝을 따른다.

1. 디폴트 constructor를 private으로 만든다. ( 다른 객체에서 싱글턴 클래스에 new 연산자를 사용하는 것을 방지)

2. constructor 처럼 동작하는 static creation 메소드 만들기 (이 메소드는 위의 private constructor를 호출해서 메모리의 static 필드에 객체를 생성해 둔 뒤에 다음 호출부터는 이 객체를 리턴해 준다)

 

 

 

🍎현실 세계 비유

그 구성원이 달라질 순 있더라도 X 나라의 공식 정부라는 객체는 하나 뿐인 것. 그리고 "The Government of X"가 그 정부에 대한 global access point이다.

 

 

 

🏗구조

getInstance 라는 정적메소드를 작성한다. ( 초기 1번만 새 객체를 생성하고, 항상 같은 객체를 리턴해야 함 )

이 Singleton 클래스의 constructor는 클라이언트 코드로 부터 숨겨져 있어야 하고 오직 이 getInstance 만이 이 객체의 인스턴스에 접근할 수 있는 유일한 방법이어야 한다.

 

 

 

👩‍💻pseudo 코드

class Database is
    // 싱글턴 인스턴스를 저장할 필드는 static으로 선언되어야 한다.
    private static field instance: Database

    // new 연산자를 통한 직접 호출을 막기 위해 싱글턴 클래스 생성자는 항상 private이어야 한다.
    private constructor Database() is
        // db 서버 연결 같은 실제 객체 초기화에 필요한 코드 여기 적기

    // 싱글턴 인스턴스 접근을 컨트롤 하는 static 메소드 (항상 같은 db 연결 인스턴스를 리턴)
    public static method getInstance() is
        if (Database.instance == null) then
            acquireThreadLock() and then
                // 이 스레드가 lock release를 기다리는 동안 
                // 다른 스레드가 먼저 인스턴스 생성을 하지 않았는지 한번 더 확인
                if (Database.instance == null) then
                    Database.instance = new Database()
        return Database.instance

    // 인스턴스에서 실행 될 비지니스 로직
    public method query(sql) is
        // 예를 들어, 어떤 앱의 모든 db 쿼리는 이 메소드를 통해 실행 될 것이다.
        // 그러므로 여기서 스로틀링, 캐싱 같은 로직 수행하면 된다.

class Application is
    method main() is
        Database foo = Database.getInstance()
        foo.query("SELECT ...")
        // ...
        Database bar = Database.getInstance()
        bar.query("SELECT ...")
        // 변수 foo와 bar는 같은 객체를 가리킬 것이다.

 

 

 

🍟적용 분야

* 당신의 프로그램에서 "유일한 db 객체" 같이 모든 코드 부분에서 오직 한개의 인스턴스를 가리켜야 할 필요성이 있다면 싱글턴 패턴을 사용하라.

=> 싱글턴 패턴은 special creation 메소드 ( getInstance 같은 ) 외의 모든 객체 생성 수단을 불가능하게 만들고 이 메서드가 항상 같은 인스턴스를 리턴함을 보장해 준다. 

 

* 글로벌 변수에 대한 더 엄격한 제어가 필요할때 사용하라

=> 싱글턴 클래스 자신만이 이 유일한 인스턴스에 대한 교체 권한을 가져갈 수 있다. ( getInstance 메소드 코드만 수정하면 생성 가능한 인스턴스 개수에 대한 limit은 얼마든지 수정할 수 있다 )

 

 

 

🚀구현 방법

1. private static 필드 추가 => 싱글턴 인스턴스 참조 용

2. public static creation 메소드 추가 => getInstance 같이

3. 이 static method에 "lazy initialization" 구현해 두기 => 최초 호출 때는 새 객체 생성해서 1의 정적 필드에 담아둔 뒤 그 이후의 호출에서는 항상 이 객체를 리턴하도록

4. 생성자를 private으로 만들기 => 다른 객체에서는 이 싱글턴 객체 생성자 호출 못하도록

5. client 코드로 가서 이 클래스 생성자 직접 호출 방식에서 2 메소드 호출로 코드 수정하기

 

 

 

🔮장점

- 이 클래스가 오직 하나의 인스턴스만 가짐을 보장할 수 있다

- 이 인스턴스에 대한 global access point를 가질 수 있다.

- 이 객체는 최초 요청시 단 한번만 초기화 된다

 

 

 

🧸단점

- Single Responsibility Principle을 위배한다

- 프로그램 요소가 서로에 대해 너무 많이 알 때는 나쁜 디자인일 수 있다. (어떤 경우가 이렇단 거지..?!)

- 멀티 스레드 환경에서는 각 스레드가 각자 객체 생성하지 않도록 스페셜 케어를 해줘야 한다 ( 락 require 했을 때 진짜 인스턴스가 null 인지 확인하고 나서야 객체 생성 )

- 클라이언트 코드를 unit 테스트 하기 어려워 진다. 이 싱글턴 객체를 mocking 할 창의적인 아이디어를 생각해 내야 한다. 아니면 테스트를 안하든, 싱글턴 패턴을 사용하지 않든 둘 중 하나를 선택하든가.

 

 

 

🎎다른 디자인 패턴과의 관계

- Facade 객체는 대부분의 경우 1개면 충분하기 때문에 싱글턴으로 변경할 수 있다

- 모든 공유 상태를 Flyweight 객체 하나로 모아서 관리한다는 점에서 싱글턴 패턴과 비슷하다. 하지만 근본적으로 다른 특징 2가지가 있다 ( 1. flyweight 패턴은 다른 상태를 가지는 여러개의 인스턴스르 가질 수 있지만 싱글턴 패턴은 인스턴스가 오직 1개여야 함. 2. 싱글턴 객체는 mutable flyweight 객체는 immutable )

- Abstract factories, Builders, Prototypes 패턴은 싱글턴으로 구현될 수 있다.

 

 

 

🏀루비 코드

class Singleton
  attr_reader :value

  @instance_mutex = Mutex.new

  private_class_method :new

  def initialize(value)
    @value = value
  end

  # static 메소드
  def self.instance(value)
    return @instance if @instance

    @instance_mutex.synchronize do
      @instance ||= new(value)
    end

    @instance
  end

  def some_business_logic
    # ...
  end
end

# @param [String] value
def test_singleton(value)
  singleton = Singleton.instance(value)
  puts singleton.value
end

# The client code.

puts "If you see the same value, then singleton was reused (yay!)\n"\
     "If you see different values, then 2 singletons were created (booo!!)\n\n"\
     "RESULT:\n\n"

t1 = Thread.new { test_singleton('FOO') }
t2 = Thread.new { test_singleton('BAR') }
t1.join
t2.join

# Thread#join => 이 메서드를 호출하면 프로세스는 해당 스레드가 종료할 때까지 블럭된다

(*참고) 루비 문법

attr_reader => 여기 적은 인스턴스 변수는 . notation으로 참조 가능

attr_writer => 여기 적은 인스턴스 변수는 . notationa으로 재할당 가능

 

 

🦄다른 참고하면 좋을 자료

자바스크립트 싱글턴 패턴
=> 자바스크립트에서 싱글턴은 안티 패턴이다.

다른 oop 언어랑 다르게  클래스 없이 객체를 바로 생성할 수 있기 때문에 굳이 싱글턴 클래스 대신 regular 객체를 사용하면 되고,

싱글턴은 매 테스트 때마다 객체를 새로 생성할 수 없어 지난 테스트의 글로벌 변수 modification에 다음 테스트가 영향을 받기 때문에 테스트 순서에 따라 결과가 달라지는 등 테스트할 때도 tricky 하기 때문.

 

* 모두 shared 싱글 인스턴스를 참조하기 때문에 멀티스레드 환경에서는 Race condition이 발생할 수 있는 것도 고려하기!

* 또 참고로 ES6에 도입된 (1) let, const (=> 블록 스코프, 그리고 let, const 키워드로 선언한 전역변수는 전역 객체의 프로퍼티가 아니다. 참고 => 2021.04.01 - [언어와 프레임워크/javascript] - 실행 컨텍스트를 알면 보이는 것들) 그리고 (2) new 모듈 시스템 (다른 파일로 value를 export, import 할 수 있음) 덕분에 개발자들의 "accidently polluting global state"를 방지할 수 있다고 함.

* react에서는 싱글턴 패턴대신 reducx, react context를 활용해서 global 상태 관리를 한다. (=> 이런 툴은 mutable인 싱글턴 클래스와 다르게 read-only 상태 제공. Redux에서는 오직 reducer라는 함수만 상태 변화시킬 수 있다)

 

얄팍한 코딩사전 - 객체지향 디자인패턴(1) 타임라인 1:01

코드없는 프로그래밍 - 디자인패턴, singleton pattern, 싱글톤 패턴

web dev simplified - singleton pattern

댓글
공지사항
최근에 올라온 글