[디자인 패턴] 싱글톤 패턴

이정규·2022년 6월 20일
0

정의

클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

사용하는 예시

  1. 레지스트리 설정 객체
  2. 연결 풀
  3. 스레드 풀

왜 사용할까?

정적 클래스로 선언하면 되지 않나? 하는 생각도 들었다. 정적 클래스로 사용되면 나타나는 문제는 다음과 같다.

  1. 자바에서 정적 초기화를 처리하는 방법때문에 일이 복잡해질 수 있다.
  2. 여러 클래스가 얽혀 있다면 지저분해진다.
  3. 초기화 순서 문제로 찾아내기 어려운 버그가 발생될 수 있다.

그리고 정적 클래스로 선언하게 된다면 무조건 메모리에 올라와 공간을 차지하게 된다.

만약 사용빈도가 적지 않다면? 불필요한 공간을 계속해서 차지하고 있는 골칫덩어리가 된다.

싱글톤 패턴을 사용하면 Lazy Loading으로 필요할 때 생성하여 사용할 수 있다. 자원을 효율적으로 사용할 수 있게 만들 수 있다는 뜻이다.

구현

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

이렇게 구현할 수 있다. 간단하니 설명은 생략하겠다.

그런데 만약 멀티스레드 환경에서도 객체가 한 개가 될것이 보장이 될까?

답은 아니다. 스레드 2개가 동시에 getInstance()를 호출한다면 객체가 2개가 될 수 있다.

이러한 방법은 어떻게 막을 수 있을까?

1. Synchronized 사용

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

이렇게 하면 하나의 스레드가 getInstance() 에 접근하면 다른 스레드는 기다리게 된다.

이로써 객체가 하나임을 보장할 수 있다.

하지만 synchronized는 처음 객체가 생성될때만 필요한 부분이지 하나가 생성되고 나면 필요없어지는 부분이다.

synchronized 를 붙이면 성능이 100배 저하되기 때문에 속도 문제가 발생될 수 있다.

2. Instance를 처음부터 생성

public class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton() {}
    
    public Singleton getInstance() {
        return singleton;
    }
}

관리하기 귀찮다면 다음과 같은 방법도 된다.

하지만 메모리에 처음부터 올라와있기 때문에 Lazy Loading이 불가능하다.

3. DCL 사용(Java 5이상)

Double-Checked Locking을 사용할 수 있다.

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {}
    
    public Singleton getInstance() {
        if (singleton == null) {
			synchronized (Singleton.class) {
				if (singleTon == null) {
				     singleton = new Singleton();
				}
			}
        }
    	return singleton;
    }
}
  1. 인스턴스가 있는지 확인한다. 없다면 동기화된 블록으로 들어간다.
  2. 동기화로 블록을 생성한다.
  3. 만약 인스턴스가 있다면 동기화된 블록을 들어가지 않고도 바로 싱글톤 객체를 얻어올 수 있다.

volatile란?

공통된 전역 변수를 사용하기 위함이다.

각각의 스레드는 캐시 메모리를 지니고 있다. 그래서 스레드가 캐시메모리에 값을 갖고와 변경해도 공통된 영역인 메인 메모리에는 영향을 끼치지 않는다.

언제 메인 메모리에 write되는지도 모른다.

그래서 volatile을 사용해 해당 값은 캐시 메모리에서 찾는게 아닌 main memory에서 찾게 되고 변경도 여기서 진행하게 된다.

cache보다는 읽기/쓰기 비용이 높기 때문에 느려 동기화를 위해서만 사용하는 것이 좋다.

문제점

  1. 클래스 로더가 여러 개라면?

    클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문에 인스턴스가 여러 개 만들어질 수 있다.

    클래스 로더를 직접 지정해서 문제를 해결할 수 있다.

  2. 리플렉션, 직렬화, 역직렬화 문제

    여기서는 싱글톤에서 문제가 발생될 수 있어 항상 염두해야한다.

  3. Loose Coupling이 아니다.

    Singleton객체에 의존하는 친구들은 전부 하나의 객체에 결합되는 문제로 강한 결합성을 띄게 된다.

    이는 싱글톤의 문제점으로 야기되는 내용으로 주의해야 한다.

  4. 싱글톤의 서브클래스는 만들 수 있나요?

    싱글톤은 일단 private로 생성자가 만들어져 있다. 그렇기 때문에 서브 클래스를 만들 수 없다.

    이 문제를 해결한다고 해도 서브 클래스가 똑같은 인스턴스 변수를 공유하게 되기 때문에 따로 레지스트리를 구현해야 한다.

    즉, 싱글톤을 확장해서 무엇을 하는지 정확하게 생각해야한다. 싱글톤은 특수한 상황에서 제한된 용도로 사용하도록 만들어진 것이다.

enum으로도 싱글톤을 만드는 건 어때요?

가능하다. 간단하게 싱글톤처럼 만들 수 있다.

public enum Singleton {
	UNIQUE_INSTANCE;
}

public class SingletonClient {
	public static void main(String[] args) {
			Singleton singleton = Singleton.UNIQUE_INSTANCE;
	}
}

참고
https://devcheon.tistory.com/82
헤드퍼스트 디자인 패턴

profile
강한 백엔드 개발자가 되기 위한 여정

0개의 댓글