Thread-safe한 Singleton Design Pattern들

like_ej_·2022년 10월 17일
0

Java

목록 보기
1/1

최근 F-Lab을 진행하면서 멘토님께서 내게 싱글톤 패턴을 구현해보라고 하셨다.
그런데 여기서 조건을 걸으셨다.

  1. thread-safe하고,
  2. 안정적이고 효율적인 싱글턴 패턴 구현하기

그래서 위의 조건을 만족하는 Singleton 패턴을 구현하기 위해서 자료 조사를 하였다.

싱글턴 패턴은 thread-safe한가?

싱글턴 패턴으로 작성된 클래스 자체는 thread-safe하지 않다. 멀티 쓰레드는 그 싱글턴 패턴으로 작성된 클래스를 동시에 접속하거나 여러 객체를 생성할 수 있기 떄문에, 싱글턴 패턴을 쓰는 이유를 무의미하게할 위험이 있다. 또한 그렇게 생성된 싱글톤 객체는 부분적으로 초기화가 된 것을 참조할 수도 있다.

싱글톤 클래스의 구현

싱글톤 클래스를 구현할 때, 싱글톤 클래스로 구현된 객체를 인스턴스화하는데에 특정 제약을 줘야한다. 그것은 이 싱글톤 객체를 호출하는 쪽에서 new 키워드를 통해 인스턴스를 생성하는 일이 없도록 해야한다는 것이다. 직접적인 인스턴스화를 못하게 하기 위해 싱글톤 클래스 내부에 선언된 생성자 앞의 접근제어자를 private로 해주고, 다른 외부에서의 클래스가 싱글톤 클래스의 new 키워드를 통한 인스턴스화를 막게 해주어야 한다.

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

(위에 구현한 코드는 thread-safe하지 않다. 그러나 일반적으로 싱글톤 패턴을 설명할 때 많이 구현하는 형태로 쓰인다.)

싱글톤 클래스의 인스턴스를 가져오기 위해 static method인 getInstance()를 사용하여 받아온다. 이 메서드는 static으로 선언되어 있는데, 이것은 이 싱글톤 클래스의 객체의 새로운 생성을 하지 않고 호출 할 수 있도록 사용된다. 이 static 메서드를 처음 호출하게 되면, 이 싱글턴 클래스의 인스턴스가 생성이 되고, 싱글턴 클래스 내부에 생성된 그 인스턴스를 저장하게 된다. 그 이후에 getInstance()메서드를 호출하게 될 시 그 내부에 저장된 인스턴스를 반환 받도록 되어 있다. 싱글턴 클래스의 instance 변수도 또한 static이므로 getInstance()에서 instance 변수를 사용할 수 있다.

그렇다면 왜 Singleton은 thread-safe하지 않을까?

위에서 작성된 코드를 유심히 살펴보면, 멀티 쓰레드로 처리해서 해당 Singleton 클래스에 접근할 때 Thread-Safety 이슈가 발생되는 것을 알 수가 있다. 동시성의 멀티 쓰레딩으로의 싱글톤 클래스에 접근을 하게 되면, 싱글톤 클래스의 자원이 일관되지 않은 데이터로 저장되거나 변경되어 원하는 방향으로 흘러가지 않을 수 있기 때문에, 그러한 현상을 막기 위해 한 쓰레드가 그 싱글턴 클래스를 점유하고 있을 때 또다른 쓰레드가 그 싱글톤 클래스의 getInstance()를 통한 접근을 막게끔 해줘야 한다. 그렇게 안하면 여러 쓰레드가 싱글턴 클래스의 getInstance()를 호출하는데, getInstance()를 여러 쓰레드에서 호출하게 되면, 하나의 쓰레드에서 getInstance()를 호출하여 Singleton 인스턴스를 생성하고 있는 도중, 다른 쓰레드에서 Singleton 인스턴스를 또 생성할 수 있는 경우의 수가 발생할 수 있다.

그럼 어떻게 Singleton을 thread-safe하게 만들까?

1. Eager Initialization(이른 초기화) -> static inner class를 사용(Bill Pugh Solution)

public class Singleton {
	private static class Load {
    	static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return Load.INSTANCE;
    }
}

이렇게 구현하는 것을 'thread-safe lazy-initialization of the object without explicit synchronization'로 표현하였다. 그러니까 동기화를 명시하는 것 대신 객체를 thread-safe하고 lazy-initialization을 하도록 구현하는 것이다. 여기서 알아야 할 것은 INSTANCE 변수가 inner 클래스 Load안에 싸여져 있고, 이는 클래스 로더가 동기화를 하기 위한 작업을 돕는다. Load클래스는 Singleton 클래스 내부에 있어 Singleton 클래스를 로딩함과 동시에 그 클래스의 인스턴스를 생성한다(lazy-initialization).

이렇게 구현하면 좋은점
1) synchronized 키워드를 사용하여 두번 null 체크를 하는 것보다는 구현이 간단하다.
2) lazy-initialization이 가능하다.
3) 멀티 쓰레드에서 safe하다.

2. static initializer block 사용

public class Singleton {
	private final static Singleton instance;
    static { // static initializer 블록
    	instance = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
    	return instance;
    }
}

Thread-safe & Lazy Initialization

3. synchronized 키워드 사용

방법 1
public class Singleton {
	private static Singleton instance;
    
    private Singleton() {}
    
    public synchronized static Singleton getInstance() {
        if(instance == null) {
        	instance = new Singleton();
        }
        return result;
    }
}

단점 : synchronized 키워드를 사용하면 JVM 내부에서 해당 클래스의 자원 사용에 대한 제어권을 lock이나 unlock이라는 메서드를 통해 처리하기 떄문에, 내부적으로 많은 cost가 발생하게 된다. 그래서 여러 Thread가 해당 클래스의 getInstance()를 호출하게 되면 프로그램 전반의 성능이 떨어지게 된다. getInstance() 메서드 전체를 synchronized로 감쌀 경우 비효율적이다. synchronized된 메서드를 사용하게 되면 하나의 쓰레드가 Singleton객체의 getInstance()를 사용하고 있는 중에 또 다른 쓰레드가 Singleton객체의 getInstance()를 사용하지 못하는 상황이 발생된다. Singleton 패턴을 thread-safe하게 만드는 목적은 싱글톤 클래스의 인스턴스가 하나만 생성되어 하나로만 사용할 수 있도록 만드는데에 목적이 있다. 우선 하나의 싱글턴 클래스의 인스턴스가 만들어지면 그 다음에는 synchronized 키워드가 필요 없어진다. 그래서 이를 해결한 방법을 다음에 소개한다.

방법 2 : Double Checked Locking
public class Singleton {
	private static volatile Singleton instance; // volatile 키워드가 쓰인다!
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	Singleton result = instance;
        if(result == null) { // 1번 check!
        	synchronized(Singleton.class) {
            	result = instance;
                if(result == null) { // 2번 check!
                	instance = result = new Singleton();
                }
            }
        }
        return result;
    }
}

여기서 핵심은 생성된 싱글톤 클래스의 인스턴스의 null 체크를 synchronized 블록 밖에서 한번, 안에서 한번 더 해줘야 한다는 것이다. 밖에서 체크하는 이유는 이미 인스턴스가 생성된 경우가 있어서 그럴 땐 바로 동기화 할 필요 없이 바로 빠르게 그 인스턴스를 반환하기 위함이고, synchronized 블록 내에서 다시 체크하는 이유는 싱글톤 클래스의 인스턴스가 생성되지 않았을 때 여러 쓰레드에서 여러 개의 인스턴스를 중복 생성하는 일이 없도록 하나의 인스턴스만 생성하도록 하기 위함이다.
이때 volatile 키워드가 쓰인다.

4. Enum을 사용한 Singleton구현

public enum Singleton {
	INSTANCE;
    
    public static Singleton getInstance() {
    	return INSTANCE;
    }
    
    // method 추가 시
    public void setTask() {
    	// TODO : 내용 작성
    }
}
Enum을 사용하여 Singleton을 구현시 장점

1) Enum자체가 Thread-safe하게 구현되어 있음.
2) 직렬화나 역직렬화에 대한 처리가 불필요함.
3) 싱글턴 패턴을 구현하는데 쉬움.

[참고자료]

profile
안녕하세요~!

0개의 댓글

관련 채용 정보