설계 패턴 : 싱글톤 패턴이란 무엇인가? 2부

박철민·2023년 4월 5일
0

CS 지식

목록 보기
5/8

싱글톤 패턴을 구현하는 방식 7가지

서론

1부에서는 싱글톤 패턴이 무엇인지에 대해서 적었습니다. 2부에서는 직접 싱글톤 패턴을 코드로 적으면서 어떤 것이 싱글톤 패턴인지, 그리고 어떻게 하면 싱글톤 패턴들을 구현할 수 있는지를 서술 하겠습니다.

싱글톤 패턴 코드

싱글톤 구현하기

1. 단순한 메서드 호출

private static으로 인스턴스를 선언을 하고 이것을 필요로 하는 것이 getInstance를 불러와 인스턴스를 반환합니다.

코드와 함께 설명을 드리겠습니다.

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

특이한 점은 여러개가 있습니다.
1. private static 선언된 instance
2. private 생성자

이 2개의 private 접근자를 통해 외부에서는 함부로 접근을 할 수 없는 상황입니다. 사람들이 원하는 대로 버스를 막 만들 수 없게 되는 거죠.

이제 모든 사람들을 instance를 가지고 사용을 해야 합니다.

또한 instance도 마음대로 부를 수 없습니다. getInstance라는 특정 함수를 통해서만 접근이 가능합니다.

좋아보이는 이 코드는 문제가 있습니다.

그것은 바로 원자성이 결여되어 있습니다.

단점

보통의 경우에는 문제가 되지 않는데요. 하지만 멀티 스레드 환경에서는 문제가 발생할 위험이 있습니다.

멀티 스레드 환경에서 다수의 스레드가 인스턴스를 획득하기 위해 getInstance() 메서드를 호출하게 될 경우 어떻게 될까요?

예를 들어 생각해봅시다. 회사를 설립하고 보니 버스가 아직 안 존재하네요. 그래서 getInstance()를 통해 새로운 버스를 할당하려고 합니다. 근데 A사장과 B 부사장이 동시에 getInstance()를 호출한다면 어떻게 될까요? => 결과적으로는 2개의 버스가 만들어 질 것입니다. 이런식으로 우리는 원하는 1개의 인스턴스를 공유하는 싱글톤 패턴을 벗어나게 됩니다.

2. Synchronized를 사용해서 동기화한다.

자바에서는 Synchronized 명령어를 통해 스레드 동기화를 할 수 있습니다. 이를 통해 하나의 공유자원에 동시에 접근하지 못하도록 락(lock)을 걸어 막는것을 말합니다. 공유데이터가 사용되어 동기화가 필요한 부분을 임계영역이라고 합니다. 이러한 영역에 Synchronized로 선언하면 위의 문제가 해결이 됩니다.

다시 설명을 하겠습니다. 버스에 Synchronized하게 선언을 해줍니다. 그렇게 되면 다시 사장 A가 getInstance()를 호출하고 처리하는 중에 부사장 B가 getInstance()로 호출하면 이미 A가 Bus를 가지고 있다는 것을 알려주고 A가 자원을 놓을때까지 대기한 후에 B가 호출을 하게 됩니다.

Synchronized에 대해서 자세한 포스트 - https://kadosholy.tistory.com/123

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

단점

이제 우리는 동기화를 통해서 안전한 싱글톤 패턴을 구현했습니다.
와우 그러면 문제가 해결이 된걸까요?
정답은 아닙니다. 위에 적혀있는 예제를 봐주시면 대기한 후에 이 부분이 중요합니다.
Synchronized를 사용한다면 자원들을 불러오는데에 있어서 대기시간이 발생하게 됩니다. 일반적으로 성능이 저하되는 것을 관찰 할 수 있습니다.

Synchronized 성능 저하에 대해서 자세히 기록한 포스트 -
https://velog.io/@jsj3282/8.-synchronized%EB%8A%94-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%8D%A8%EC%95%BC-%ED%95%9C%EB%8B%A4

3. 정적 멤버

이러한 문제를 해결하기 위해서 아얘 맨 처음부터 최초 1회만 선언을 하면 되지 않을까요?
그렇게 된다면 이제 여러번 만들어지는 불상사가 없어질 거라 생각이 듭니다.

그럼 언제 선언을 하는게 좋을까요? 처음부터 클래스가 로딩되는 순간부터 만들어지게 하면 문제가 없을 것 같은데요? 그래서 static으로 선언을 하게 됩니다.

public class Bus {
	private static Bus instacne = new Bus();
    
    private Bus() {}
    
    public static Bus getInstance() {
        return instance;
    }
}

이거 이외에도 블록을 통해 만들 수 있습니다.

4. 정적 블록

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

이렇게 2가지 방법으로 static하게 선언을 하였으니 문제가 없어보이는 것 같습니다. 하지만 만약 아무도 버스를 이용하지 않는다면 어떨까요? 아무도 사용하지 않는 버스를 우리는 계속 보관해야하는 문제가 생겨버렸네요. 이렇듯 미리 만들어버리면 불필요한 시스템 리소스를 낭비할 가능성이 있습니다.

5. 정적 멤버와 Lazy Holder(중첩 클래스)

static의 문제는 부르지 않았을 경우 불필요한 리소스의 낭비였습니다. 이를 해결하기 위하여 Lazy Holder를 사용하였습니다.
다음 코드의 Bus 클래스의 getInstance() 메서드에서BusHolder를 참조하는 순간! class가 로딩이 되며 초기화가 진행이 됩니다.

즉 처음 불러오는 순간에 만들어지게 됩니다.

Class를 로딩하고 초기화하는 시점은 thread-safe를 보장하기 때문에 동기화(synchronized)키워드가 없어도 됩니다.

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

6. 이중 확인 잠금(DCL)

이중 확인 잠금은 이전에 잠금 기준을 테스트하여 잠금 획득의 오버헤드를 줄이는 데 사용되는 소프트웨어 설계 패턴입니다. - https://en.wikipedia.org/wiki/Double-checked_locking

즉 테스트를 하여 해당 경우에만 잠금을 거는 것입니다.

인스턴스 생성 여부를 싱글톤 패턴 잠금 전에 한 번, 객체를 생성하기 전에 한번으로 2번 체크하면 인스턴스가 존재하지 않을 때만 잠금을 걸 수 있기 때문에 앞서 생긴 문제들을 해결할 수 있습니다.

코드를 통해서 설명을 드리겠습니다.

class Bus {
	private static Bus instance = null;
    
   	private Bus() {}
    
    private static Bus getInstance() {
		if(instance == null) {
        	synchronized (Bus.class) {
            	if(instance == null) {
            		instance = new Bus();
                }
        	}
        }
	}
}

이 방법을 통해서 메서드 전체에서 synchronized를 쓰지 않고, 임계 구역에만 동기화를 걸어서 동기화 오버헤드를 줄였습니다.

volatile이란?

volatile을 사용하여 이중 확인 잠금을 만들 수 있습니다.

public class Bus {
	private volatile Bus instance;
    
    private Bus() {}
    
    public Bus getInstance() {
		if(instance == null) {
			synchronized (Bus.class) {
            	if(instance == null) {
                	instance = new Bus();
                }
            }
        }
    }
}

volatile은 Java5부터 유효하며, 이 키워드를 붙이지 않으면 제대로 동작하지 않습니다.

이에 대한 설명을 밑에 포스트에서 발췌해서 보여드리겠습니다.

main memory가 있고 각 스레드 마다 working memory가 있습니다. main memory <-> working memory 이렇게 두 메모리간 동기화가 진행되는 동안 빈틈이 생기게 되는데 이를 해결하기 위하여 volatile을 사용합니다.

램에는 L1Cache, L2Cache, L3Cashe가 존재합니다
JAVA에서는 스레드 2개가 열리면 변수를 메인 메모리로부터 가져오는 것이 아니라 각각의 캐시 메모리를 기반으로 가져오게 됩니다.

즉 공유가 되지 않고 각각의 캐시에 있는 변수들을 바라보게 됩니다.
이러한 문제를 없애기 위해 volatile을 선언하면 각각의 캐시가 아닌 메인 메모리의 값을 보게 됩니다.

DCL의 문제점에 대한 포트스 -
https://yeahhappyday.tistory.com/entry/singleton-%ED%8C%A8%ED%84%B4%EA%B3%BC-volatileDCLDouble-Checking-Locking

7. enum

enum의 인스턴스는 기본적으로 스레드세이프한 점이 보장되기 때문에 이를 통해 생성할 수 있습니다.

public enum Bus {
	INSTANCE;
    public void oortCloud() {
    
	}
}

결론

그래서 최고의 방법은?
5번과 7번을 추천! 5번은 가장 많이 쓰인다고 알려져있고 7번은 이펙티브 자바를 쓴 조슈아 블로크가 추천한 방법이라고 합니다!

싱글톤 패턴을 공부하면서 JAVA에 대해서 좀 더 배워야 하는 것이 많다는 것이 느껴졌다. JAVA만을 따로 공부하는 시간을 가지도록 해야할지도 모르겠다.

추가 공부

이 포스트에 싱글톤에 대해서 정말 자세히 적어놓아서 더 자세한 공부를 위해 꼭 보기를 권장합니다.
https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4-%EA%BC%BC%EA%BC%BC%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

출처 :
인프런 개발자 면접 cs 특강 -
https://www.inflearn.com/course/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-cs-%ED%8A%B9%EA%B0%95/dashboard
멀티스레드 환경에서의 싱글톤 패턴 -
https://jungwoon.github.io/java/2019/08/11/Singleton-Pattern-with-Multi-Thread.html
자바 - Synchronized 스레드 동기화 개념 및 사용예제 -
https://kadosholy.tistory.com/123
synchronized는 제대로 알고 써야 한다 - https://velog.io/@jsj3282/8.-synchronized%EB%8A%94-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%8D%A8%EC%95%BC-%ED%95%9C%EB%8B%A4

profile
멘땅에 헤딩하는 사람

0개의 댓글