생성 패턴 - Singleton

이유석·2022년 5월 30일
0

Design Pattern

목록 보기
2/10
post-thumbnail

Singleton 패턴

정의

  • 애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴

  • 즉, 하나의 인스턴스만 생성하여 사용하는 디자인 패턴이다.

  • 클래스의 생성자가 여러번 호출되어도, 실제로 생성되는 객체는 하나이며
    최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 만드는 것 이다.

사용하는 이유

  • 객체를 생성할 때마다 메모리 영역을 할당받아야 한다. 하지만 한번의 new를 통해 객체를 생성한다면 메모리 낭비를 방지할 수 있다.

  • 싱글톤으로 구현한 인스턴스는 전역이므로, 다른 클래스의 인스턴스들이 데이터를 공유하는 것이가능한 장점이 있다.

사용되는 경우

  • 주로 공통된 객체를 여러개 생성해서 사용해야 하는 상황에서 많이 사용됩니다.

    ex) 데이터베이스에서 커넥션풀, 스레드풀, 캐시, 로그 기록 객체 등

  • 안드로이드 앱 : 각 액티비티 들이나, 클래스마다 주요 클래스들을 하나하나 전달하는게 번거롭기 때문에 싱글톤 클래스를 만들어 어디서든 접근하도록 설계

  • 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 때 사용됩니다.

단점

  • 만약 싱글톤 인스턴스가 혼자 너무 많을 일을 하거나, 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 된다. 이때 개방-폐쇄 원칙이 위배된다.
    결합도가 높아지게 되면, 유지보수가 힘들고 테스트도 원할하게 진행할 수 없는 문제점이 발생한다.

  • 또한, 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제가 발생할 수 있다.

구현 방법

싱글톤을 구현하는 방법은 굉장히 다양합니다.
그러나 각 방법들이 갖는 공통적인 특징이 있습니다.

  • private 생성자만을 정의해 외부 클래스로부터 인스턴스 생성을 차단합니다.
  • 싱글톤을 구현하고자 하는 클래스 내부에 멤버 변수로써 private static 객체 변수를 만듭니다.
  • public static 메소드를 통해 외부에서 싱글톤 인스턴스에 접근할 수 있도록 접점을 제공합니다.

지금부터 싱글톤을 구현하는 6가지 방법에 대해 살펴보겠습니다.

1. Eager Initialization

  • 가장 간단한 형태의 구현 방법입니다.
  • 싱글톤 클래스의 인스턴스를 클래스 로딩 단계에서 생성하는 방법입니다.
  • 그러나, 애플리케이션에서 해당 인스턴스를 사용하지 않더라도 인스터스를 생성하기 때문에 메모리 낭비가 발생할 수 있습니다.
public class Singleton {
	
    private static final Singleton instance = new Singleton();
    
    // private 생성자
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return instance;
    }
}
  • 즉 File System, Database Connection 등 큰 리소스들을 다루는 싱글톤을 구현할 때는 위와 같은 방식보다는 getInstance()메소드가 호출될 때까지 인스턴스를 생성하지 않는 것이 더 좋습니다.
  • 또한 예외처리 기능을 제공하지 않습니다.

2. Static Block Initialization

  • Eager Initialization과 유사하지만, static block 을 통해서 예외 처리에 대한 옵션을 제공합니다.
public class Singleton {
 
    private static Singleton instance;
    
    private Singleton(){}
    
    //static block initialization - 예외 처리를 위한 block
    static{
        try{
            instance = new Singleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static Singleton getInstance(){
        return instance;
    }
}
  • 위와 같이 구현할 경우 싱글톤 클래스의 인스턴스를 생성할 때 발생할 수 있는 예외에 대한 처리를 할 수 있지만,
    Eager Initialization과 마찬가지로 클래스 로딩 단계에서 인스턴스를 생성하기 때문에 여전히 큰 리소스를 다루는 경우에는 적합하지 않게 됩니다.

3. Lazy Initialization

  • 앞선 두 방식과는 다르게 나중에 초기화하는 방법입니다.
  • getInstance() 메소드를 호출할 때에 인스턴스가 없으면 생성합니다.
public class Singleton {
 
    private static Singleton instance;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}
  • 이 방식으로 구현할 경우 사용하지 않는 인스턴스의 생성으로 인한 메모리 낭비 문제에 대한 해결책이 될 수 있습니다.

  • 하지만, multi-thread 환경에서 동기화 문제가 발생할 수 있습니다.
    만약 인스턴스가 생성되지 않은 시점에서 여러 쓰레드가 동시에 getInstance()를 호출한다면 예상치 못한 결과를 얻을 수 있을뿐더러, 단 하나의 인스턴스를 생성한다는 싱글톤 패턴에 위반하는 문제점이 야기될 수 있습니다.

4. Thread Safe Singleton

  • 3번의 multi-thread 환경에서 동기화 문제를 해결하기 위해 getIntance() 메소드에 synchronized를 걸어두는 방식입니다.

    synchronized 키워드는 임계 영역(Critical Section)을 형성해 해당 영역에 오직 하나의 스레드만 접근 가능하도록 하게 해주니다.

public class Singleton {
 
    private static Singleton instance;
    
    private Singleton(){}
    
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
    
}
  • 위와 같은 방식으로 구현할 경우, getIntance() 메소드 내에 진입하는 스레드가 오직 하나밖에 없기 때문에 멀티 스레드 환경에서도 정상 동작합니다.

  • 하지만, synchronized 키워드 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 많은 애플리케이션에서는 성능이 떨어지게 됩니다.

  • 해당 문제를 해결하기 위해 제안된 방식이 double checked locking입니다.
    이 방식은 getInstance() 메소드 수준에 lock을 걸지 않고, instance가 null일 경우에만 synchronized가 동작하도록 합니다.

public static Singleton getInstance(){
    if(instance == null){
        synchronized (Singleton.class) {
            if(instance == null){
                instance = new Singleton();
            }
        }
    }
    return instance;
}

5. Bill Pugh Singleton Implementation

  • inner static helper class를 사용하는 방식입니다.

    inner class (내부 클래스, 중첩 클래스)
    클래스나 인터페이스 내부에서 선언되는 클래스 입니다.

  • 앞의 방식이 갖고있는 문제점들을 대부분 해결한 방식으로, 현재 가장 많이 쓰이는 싱글톤 구현 방법입니다.

public class Singleton {
 
    private Singleton(){}
    
    private static class SingletonHelper{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}
  • getInstance() 메소드가 호출되었을 때 inner class인 SingletonHelper 클래스가 JVM 메모리에 로드됩니다.

6. Enum Singleton

  • 앞서 1~5번에서 살펴본 싱글톤 방식은 사실 완전히 안전할 수 없습니다.
    왜냐하면 Java의 Reflection을 통해서 싱글톤을 파괴할 수 있기 때문입니다.

    Reflection : 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

  • Enum으로 싱글톤을 구현하는 방법

public enum EnumSingleton {
 
    INSTANCE;
    
    public static void doSomething(){
        //do something
    }
}
  • 그러나 이 방법 또한 1, 2번과 같이 사용하지 않았을 경우의 메모리 문제를 해결하지 못한 것과 유연성이 떨어진다는 면에서의 한계를 지니고 있습니다.
profile
https://github.com/yuseogi0218

0개의 댓글