[Design Pattern] Singleton Pattern

CoHa·2020년 9월 6일
0

Singleton Pattern

싱글톤 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고,
어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.

싱글톤 패턴의 장점은 다음과 같다.

고정된 메모리 영역을 얻을 수 있고 인스턴스를 하나만 생성하기 때문에 
메모리 낭비를 방지할 수 있다.

또한 싱글톤으로 만들어진 인스턴스는 전역 인스턴스이므로 
클래스의 다른 인스턴스들이 데이터를 공유하기 쉽다.

코드를 통해서 싱글톤 패턴을 살펴보자.

public class Singleton {
	private static Singleton singleton;
	private String name;
	
	public Singleton(String name) {
    	this.name = name;
    }
	
	public static Singleton getInstance(String name) {
		if(singleton == null) {
			singleton = new Singleton(name);
		}
		return singleton;
	}
	
	public String getName() {
		return name;
	}
}

위 코드는 기본적인 싱글턴 패턴이다. 사실 위 코드는 약점(?)이 존재한다.

위 코드는 생성자가 존재하기 때문에 인스턴스 생성을 방지할 수 없다.

멀티스레드 환경에서 인스턴스가 하나임을 보장하지 못한다.

그렇기 때문에 위의 코드는 보통 다음과 같이 수정한다.

Eager Initialization

public class Database {	
	private static Database singleton = new Database("unique");
	private String name;
	
	private Database(String name) {
    	this.name = name;
    }
	
	public static Database getInstance(String name) {
		return singleton;
	}
    
    public String getName() {
		return name;
	}
}

static이 실행되는 시점은 클래스가 메모리상에 올라갈 때이다.
따라서 static은 인스턴스의 생성과는 관계없이 클래스가 로딩되는 시점에 단 한 번만 생성하기 위해서 사용한다.

static의 단점도 존재한다.
static은 Method Area(비 객체 영역)에 할당된다.
Heap영역에 생성되는 인스턴스와 달리, GC의 영향을 받지 않기 때문에 메모리 회수가 되지 않는다.

이러한 이유로 static을 사용하게 되면 인스턴스의 생성되는 시점과 관계없이 인스턴스가 이미 생성되기 때문에 멀티스레드 환경에서 만족시킬 수 있다.

Lazy Initialization (DCL)

다음 코드는 컴파일 시점에 미리 인스턴스를 생성하는 것이 아니라 실제 인스턴스를 필요한 시점 (런타임 시점)에 로드하는 Lazy 방식이다.

synchronized를 사용해서 동기화를 진행하는 방법이 있지만, 멀티 스레드 환경에서 synchronized를 쓴다는 것은 결국 병목현상이 생겨 성능에 이슈가 생길 수 있다. 그래서 DCL과 같이 메서드에 synchronized 키워드를 제거하고, 인스턴스가 생성된 뒤에는 동기화 블록을 거치지 않게 함으로써 성능을 개선한 방법이다.

public class Singleton {
	private volatile static Singleton singleton;
	private String name;
	
	private Singleton(String name) {
    	this.name = name;
    }
	
	public static Singleton getInstance(String name) {
		if(singleton == null) {
			synchronized (Singleton.class) {
				if(singleton == null) {
					singleton = new Singleton(name);
				}
			}
		}
		return singleton;
	}
	
	public String getName() {
		return name;
	}
}

DCL을 자세히보면 인스턴스를 미리 생성하지 않고 있는 것을 확인할 수 있다. volatile이 가시성을 보장해주고 있기 때문이다.

간단히 volatile에 대해서 알아보자.
volatile에 대해서 이해하려면 메모리에 대한 이해가 중요하다. 우리가 어플리케이션을 실행할 때 인스턴스에 대한 정보는 Main memory에 저장되어 있는 값을 읽어올 수도 있고, 성능 향상을 위해 Cpu cache에 저장된 값을 읽어올 수 있다.
그렇기 때문에 멀티 스레드 환경에서는 해당 스레드가 어떤 값을 읽어올지 모르고 이렇게 되면 값에 대한 불일치가 일어날 수 밖에 없다. 하지만 volatile을 사용하면 항상 Main memory에서 읽어오기 때문에 가시성을 보장해줄 수 있다.

Enum Singleton

Enum의 인스턴스 생성은 Thread-safe하다는 것을 보장해준다. 또한 복잡한 직렬화 상황이나 리플렉션에도 또 다른 인스턴스가 생성되는 것을 막아준다.

public enum Singleton {
	INSTANCE("unique");
	
	private String name;
	
	private Singleton(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
}

한가지 문제점이 있다면 위의 이디엄은 Android와 같은 Context의존성이 있는 환경일 경우 싱글턴 초기화 과정에서 의존성이 끼어들 수 있다고 한다. (나는 안드로이드를 해본 적이 없어서 잘 모르지만 그렇다고 한다.)

Lazy Holder

Lazy Holder는 성능도 뛰어나고 가장 많이 사용하는 싱글톤의 이디엄이라고 한다.

public class Singleton {
	private String name;
	
	private Singleton(String name) {
		this.name = name;
	}
	
	public static Singleton getInstance() {
		return LazyHolder.INSTANCE;
	}
	
	private static class LazyHolder {
		private static final Singleton INSTANCE = new Singleton("unique");  
	}
	
	public String getName() {
		return name;
	}
}	

위 코드를 살펴보면, 현재 Singleton 클래스는 LazyHolder 클래스의 변수가 존재하지 않기 때문에 클래스 로더가 Singleton 클래스를 로드하더라도 LazyHolder 클래스를 초기화하지 않는다. 이후 getInstance 메서드를 호출하는 순간 LazyHolder 클래스를 참조하여 해당 클래스를 로딩하게 되고 초기화를 진행한다. Class를 로딩하고 초기화하는 시점은 Thread-safe를 보장하기 때문에 synchronized나 volatile이 없어도 되고 성능에서도 우수하다.

정리

싱글톤 패턴은 정말 기본적인 패턴이지만, 고려해야할 것이 많고 여러 환경에서 안정성을 보장해야하기 때문에 까다로운 패턴이다.

사실 싱글톤 패턴은 다음과 같은 이유로 안티패턴이라는 말이 많다.

전역에서 사용가능하기 때문에 어디서든 접근 가능하다.
코드의 결합도가 높다.

이러한 문제로 테스트를 진행할 때 어려움을 겪기도 하고, 디버깅도 어렵다.
보통 싱글톤은 정말 필요할 때 사용하고 그 외에는 사용을 지양하는 것이 맞는 것 같다.

profile
백엔드 개발을 좋아하는 주니어 개발자

0개의 댓글