생성 패턴 - 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
소통을 중요하게 여기며, 정보의 공유를 통해 완전한 학습을 이루어 냅니다.

0개의 댓글

관련 채용 정보