본문 바로가기
Engineering/SW Design

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

by 쿨쥰 2024. 3. 11.

생성패턴 - 싱글턴


이전 글 : 생성패턴 - 프로토타입

2024.03.03 - [Engineering/SW Design] - [디자인 패턴] 생성패턴 - 프로토타입

 

[디자인 패턴] 생성패턴 - 프로토타입

생성패턴 - 프로토타입 이전 글 : 생성패턴 - 빌더 2023.05.25 - [Engineering/SW Design] - [디자인 패턴] 생성패턴 - 빌더 [디자인 패턴] 생성패턴 - 빌더 생성패턴 - 빌더 이전 글 : 생성패턴 - 추상 팩토리 20

skidrow6122.tistory.com

 

요약

클래스에 대한 인스턴스를 하나만 생성해야 하거나, 여러 클래스에서 한 클래스의 내용을 공유해야 할 때 사용하는 패턴이다.

다시 말해, 매번 새로운 객체를 생성하지 않고 오직 하나의 객체만 생성하여 사용하려고 할때 사용하며,

클래스에 인스턴스가 단 하나만 있도록 하면서 이 인스턴스에 대한 전역 액세스 지점을 제공해 준다.

단 하나의 인스턴스를 생성하므로 메모리 낭비를 자연히 막아주며, 객체에 대한 로드를 초기에만 시도하므로 객체 로딩 타임이 줄어든다.

또한 전역 인스턴스이므로 다른 클래스의 인스턴스들이 데이터를 서로 공유하기 쉬워진다.

반면에 아래의 한계점 들도 분명히 존재 한다.

  • private 생성자를 가지고 있으므로 상속이 불가
  • 테스트하기 힘듦
  • 멀티쓰레드 환경이나 다수 WAS 나 POD 기반 서비스 서버 환경에서는 싱글턴이 하나만 생성되는 것을 보장 못함

이러한 한계점들 때문에 spring 에서는 싱글턴패턴을 대신하여 싱글턴 자체를 생성관리 해주는 싱글턴레지스트리를 지원한다.

 

키워드 : 오직 하나의 객체 / 전역 인스턴스 / 싱글턴 레지스트리 / getInstance()

 

 

싱글턴의 모습

  • 클래스에 인스턴스가 단 하나만 있도록 함

클래스에 대한 인스턴스를 제한하려는 가장 일반적인 이유는 일부 공유 리소스 (ex, 데이터베이스, 파일)에 대한 접근 제어를 위해서이다.

DB 접근 인스턴스를 매번 생성자 호출을 한다면 반드시 새 객체가 로딩되어 반환되므로, 엄청난 자원의 소모를 불러일으킬 것이다.

따라서, 이런 경우는 새로운 객체를 생성하는 대신 이미 만들어진 객체를 리턴받게 된다.

  • 인스턴스에 대한 전역 접근 지점을 제공함

사실 전역 변수는 쓰기 편하지만, 모든 코드가 잠재적으로 전역 변수의 내용을 덮어써버릴 수 있으므로 그리 안전한 방법은 아니다.

싱글턴 패턴 역시 전역 변수와 마찬가지로 프로그램의 구석구석 모든 곳에서 부터 접근 할 수 있는데, 싱글턴 패턴이 바로 이 인스턴스를 덮어쓰지 못하도록 보호한다.

 

 

싱글턴의 적용

  • 다른 객체들이 싱글턴 클래스를 new 연산자를 사용해서 호출하지 못하도록 디폴트 생성자를 private 로 제한한다
  • 대신 생성자 역할을 하는 정적 생성 메서드를 만든다.

[고전적 방식의 싱글턴 패턴]

public class Singleton {
    private static Singleton uniqueInstance;
    
    private Singleton() {
        //init block 
   }
   
   public static Singleton getInstance() {
       if (uniqueInstance == null) {
           uniqueInstance = new Singleton
      } 
      return uniqueInstance
   }
}
  • Singleton 타입의 정적 변수, private singleton 생성자, Singleton 을 반환하는 정적메소드
  • 세가지 필수사항을 모두 갖추고 있으므로, 외부에서는 생성자를 호출할 수 는 없고 본 클래스의 정적 메서드만 호출이 가능하다.
  • 단, 이같은 형태에서 멀티스레드 환경이라 한다면 내부적으로 정적 메서드 내 new Singleton 이 두번 호출되면서 싱글턴 패턴이 무너질 수 있다.

[synchronized를 통한 멀티스레드 환경 문제 해결]

public class Singleton {
    private static Singleton uniqueInstance;
    
    private Singleton() {
        //init block 
   }
   
   public static synchronized Singleton getInstance() {
       if (uniqueInstance == null) {
           uniqueInstance = new Singleton
      } 
      return uniqueInstance
   }
}
  • synchronized 키워드가 붙었을 때 하나의 스레드가 이 메서드를 사용하고 있다면, 다른 스레드는 이 메서드를 사용 못하고 대기한다.
  • 이는 속도가 느려질 수 있다는 단점이 있으나 큰 문제가 없다면 그냥 둔다.

[성능 저하 문제에 대한 해결책]

public class Singleton {
    private volatile static Singleton uniqueInstance;
    
    private Singleton() {
        //init block 
    }
   
    public static synchronized Singleton getInstance() {
    if (uniqueInstance == null) {
        synchronized (Singleton.class) {
            if (uniqueInstance == null)
                uniqueInstance = new Singleton
        } 
        return uniqueInstance
    }
}
  • volatile 키워드를 사용하여 메모리 사용성을 높였고
  • if문 제어를 통해 singleton 객체가 생성된 이후부터는 여러 스레드에서 동시에 사용할 수 있게 잠금을 풀어주었다. 
    • 기존 코드에서는 singleton 객체가 생성 된 이후에 getInstance()를 여러 스레드에서 동시에 사용해도 문제가 발생하지 않는데도 불구하고 하나의 스레드에서만 gentInstance()를 사용할 수 있도록 잠궈버렸었다.

 

구조

 

 

Hands on

[간단한 선언]

object Singleton {
    var language = "kotlin"
}
  • 그냥 object 키워드로서만 싱글턴이 제공되며, thread-safe 하고 lazy 하게 초기화 가능한다.

[파라미터가 필요한 케이스]

class Singleton private constructor(private val name: String) {
    companion object {
        @Volatile
        private var instance: Singleton? = null

        @Synchronized
        fun getInstance(param: String) = instance
                ?: Singleton(param).also { instance = it }
    }
}
  • 클라이언트 코드에서 불러 쓸때는 그냥 main() 같은 데 안에서
    • Singleton foo = Singleton.getInstance() 와 같이 불러 쓰면 된다.

 

 

핵심

  • 프로그램의 클래스에 모든 클라이언트가 사용할 수 있는 단일 인스턴스만 있어야 할때 사용 (ex, DB 객체)
  • 전역 변수들을 더 엄격하게 제어해야 할때 사용
  • 클래스가 하나의 인스턴스만 갖는 다는것을 확신할 수 있으며 인스턴스에 대한 전역 접근지점을 얻음
  • 처음 요청 될 때만 초기화 됨

 


간단히 hands on 을 통해 싱글턴 패턴을 구현 해 보았다.

추상 팩토리, 빌더, 프로토타입 등 생성 패턴들은 모두 싱글턴으로 구현할 수 도 있다.

일반적으로 사용할때는 그냥 DB 인스턴스 붙일때나 redis 클라이언트 정도에서 쓰고 괜히 여기저기서 남발하지 않아도 될 것 같다.

싱글턴은 하나의 인스턴스에 전역접 접근을 허용하므로 인스턴스 내 내용 변경이 가능하게 구현한다면 추적도 어려워지고, 유닛 테스트에 어려움을 겪을 수도 있기 때문이다.

댓글