간단 정리/테코톡

[테코톡] Tecotalk - 싱글톤 패턴과 정적 클래스 / Singletone Pattern / DCL (Double Checked Locking) / volatile / Bill Pugh Solution

always-dev 2022. 7. 4.
반응형

프로그램 전역에서 사용 되는 유일한 클래스를 만드는 방법 두 가지가 있습니다.

  1. 싱글톤 패턴
  2. 정적 클래스

싱글톤 패턴이 무엇인지 알아보겠습니다!

 

 

 

싱글톤 패턴 이란 ?

먼저 디자인 패턴이라는 말을 보겠습니다.

디자인 패턴 ?

  • “바퀴를 재발명 하지마라”
  • 소프트웨어 개발 과정에서 자주 쓰이는 설계의 노하우들을 정리한 방법들

그렇다면 디자인 패턴에 해당하는 싱글톤 패턴은 왜 생겨난 걸까요 ?

 

 

싱글톤 패턴의 목적 & 의의

  • 객체의 인스턴스가 오로지 한개만 생성되도록 설계하는 것
  • 언제 ?
    • 로그 기록
    • 캐싱
    • 사용자 설정
  • 왜 ?
    • 유일성
    • 글로벌

다음과 같은 상황에서 싱글톤 패턴을 적용해보겠습니다.

 

 

싱글톤 패턴 구현해보기

환경 설정을 바꿀 수 있는 인스턴스 settings1과 settings2로 배경색을 동시에 바꾸게 되면 오류가 발생하게 됩니다.

한 애플리케이션에 환경 설정이 2개가 있다는 얘기입니다.

문제 상황

  • 애플리케이션 배경 설정
    • Settings : 배경색을 담당하는 클래스
  • Settings가 애플리케이션 내부에서 유일하지 않다면 ?? ⇒ 에러 발생

아래와 같이 인스턴스가 다르다는 것을 알 수 있습니다.

public class App {
    public static void main(String[] args) {
        Settings settings1 = new Settings();
        Settings settings2 = new Settings();

        System.out.println(settings1);	
        System.out.println(settings2);		
        System.out.println(settings1 == settings2);			
    }
}

/**
 * 출력 내용
 * game.Settings@23jgjfsda
 * game.Settings@95mbjfiv9
 * false
 */

이러한 상황에서 Settings 클래스의 인스턴스를 생성할 때 하나의 공동의 객체를 가져오게 된다면

환경 설정이 2개가 생기는 오류는 발생하지 않을 것입니다 !

 🙋‍♂️ 이제부터 싱글톤 패턴으로 하나의 인스턴스만 생성되도록 함으로써 오류를 없애겠습니다 !

 

 

1. 순수한 구현

  1. 인스턴스를 private static 변수
  2. getInstance()에서 인스턴스 생성
  3. 외부 생성자를 막는다.

아래와 같이 Settings 클래스를 작성하면 됩니다.

public class Settings {
    private static Settings instance;

    private Settings() {
    }

    public static Settings getInstance() {
        if (instance == null) {
            instance = new Settings();
        }
        return instance;
    }
}

이렇게 Settings가 싱글톤 패톤으로 구현 되었을 때

 

처음에 위 상황과 똑같이 Settings 클래스의 인스턴스를 2개 만들어서 작동시켜보겠습니다.

public class App {
    public static void main(String[] args) {
        Settings settings1 = Settings.getInstance();
        Settings settings2 = Settings.getInstance();

        System.out.println(settings1);	
        System.out.println(settings2);		
        System.out.println(settings1 == settings2);			
    }
}

/**
 * 출력 내용
 * game.Settings@23jgjfsda
 * game.Settings@23jgjfsda
 * true
 */

 

 

🙋‍♂️ 과연 이게 원하는 데로 작동이 될 거 같나요 ?? 아닙니다 !!!
위에서 나온 구현은 ‘쓰레드 세이프 하지 않습니다.”
즉, 각각 다른 인스턴스가 생길 가능성이 존재합니다.

 

 

 

 

이와 같이 A가 생성되지 직전에 B가 온다면 A, B 두 개의 인스턴스가 생성되고 맙니다.

저희는 이러한 상황도 막아주어야 합니다.

 

 

 

 

 

 

 

 

 

 

 

2. 동기화 (synchronized) - 쓰레드 세이프 하지 않은 싱글톤 패턴

1. synchronized 키워드 사용하기

  1. 인스턴스를 private static 변수
  2. synchronized getInstance()에서 인스턴스 생성
  3. 외부 생성자를 막는다.

 

 

synchronized 키워드를 사용하여 동시성 문제를 해결할 수 있습니다.

바로 다음 코드와 같이 사용하면 됩니다.

public class Settings {
    private static Settings instance;

    private Settings() {
    }

    public synchronized static Settings getInstance() {
        if (instance == null) {
            instance = new Settings();
        }
        return instance;
    }
}

 

 

 

1-1. 문제점 - synchronized 남발로 인한 리소스 낭비

이미 인스턴스가 만들어진 상태에서는 synchronized가 필요 없습니다.

하지만 현재 코드에서는 계속해서 getInstance() 메서드를 실행 할 때마다 Lock이 걸리게 되어 리소스 낭비가 발생하게 됩니다 !

 

 

3. 해결 - DCL (Double Checked Locking)

  • synchronized 시점 지연
  • private static volatile 인스턴스
  • 외부 생성자를 막는다.
  •  

DCL (Double Checked Locking) 이라는 방법이 있습니다 !!

 

이제 getInstance() 메서드를 호출할 때 마다 인스턴스가 있을 때는 synchronized 블록이 스킵됩니다.

이렇게 즉시 인스턴스만 반환하게 되며 리소스 낭비를 없앨 수 있습니다.

public class Settings {
    private static volatile Settings instance;

    private Settings() {
    }

    public static Settings getInstance() {
        if (instance == null) {
            synchronized (Settings.class) {
                if (instance == null) {
                    instance = new Settings();
                }
            }
        }
        return instance;
    }
}

이 때 클래스 변수에 아까 정의 해놨던 인스턴스를 volatile이라는 키워드를 이용해야합니다.

 

 

3-1. volatile 이 뭐고 왜 사용하는데 ??

스레드를 이용하게 되면 각각의 스레드는 성능을 위해서 캐시 메모리 (cache memory)를 사용하게 됩니다.

 

스레드 동작

  1. 첫 번째 스레드가 캐시 메모리에서 메인 메모리의 순서로 값을 대입해주면
  2. 다음 스레드는 메인 메모리에 담긴 값을 캐시 메모리에 담아 들어오는 방식으로 동작

 

문제

  1. 첫 번째 스레드가 메인 메모리에 값을 대입하기 전에
  2. 다음 스레드가 메인 메모리에서 값을 읽으려 할 때 발생합니다.

 

해결

  1. volatile이라는 키워드를 이용하게 되면
  2. 대입과 읽는 것 모두 메인 메모리에서 하도록 만듭니다.
  3. 때문에 시간차를 극복할 수 있습니다.

 

volatile 사용 시 주의점

  1. volatile (JDK 1.5 이상)
  2. JVM에 대한 이해
    1. JVM에 따라서 thread-safe 하지 않는 경우가 발생할 수 있습니다.
    2. 또한 자바가 어떻게 메모리를 관리하는지 이해하고 사용해야 합니다.

 

4. Bill Pugh Solution (Initialization on demand holder idiom)

🙋‍♂️ 싱글톤을 구현할 때 권장되어지는 방법 중의 하나입니다.

 

  • static inner class 인스턴스
  • 생성자를 private

 

이 싱글톤은 Initialization on demand holder idiom 개념을 이용한 방법입니다.

  • 구현 방법은 Holder역할을 하는 private static 클래스를 이용하는 것입니다
    • 해당 클래스(SettingHolder)는 static 이므로 메서드가 실행될 때 JVM의 static initializer에 의해 초기화 되고 메모리로 올라가게 됩니다.
    • 따라서 thread-safe와 lazy-loading을 둘 다 만족하는 싱글턴이 구현 가능합니다.
    public class Settings {
    	private Settings() {
    	}
    
    	private static class SettingsHolder {
    		private static final Settings SETTINGS = new Settings();
    	}
    
    	public static Settings getInstance() {
    		return SettingsHolder.SETTINGS;
    	}
    }
    
  • 최초로 Class Loader에 의해서 로드될 때 내부로 synchronized가 실행됩니다.
  • 때문에 명시적으로 synchronized를 이용하지 않고 동일한 효과를 낼 수 있습니다.
protected Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
{
	synchronized (getClassLoadingLock(name)) {
		// First, check if the class has already been loaded
		Class<?> c = findLoadedClass(name);
		if (c == null) {

// ...

 

 

하지만 이 Bill Pugh Solution 방법에도 문제점이 있습니다.

4-1. 문제점

🙋‍♂️ 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 것입니다. (리플렉션, 직렬화)
  • 리플렉션
  • 직렬화

그렇다면 대체 가능한 방법이 어떤 것이 있을까요 ??

바로 Enum이 있습니다.

 

 

5. Enum

  • Enum 자체가 싱글톤입니다.
  • 애초에 생성자를 private으로 갖게 만듭니다.
  • 상수만 갖는 클래스이기 때문에 싱글톤의 성질을 갖게 됩니다.
public enum Settings {
	INSTANCE;
}

이렇게 Enum으로 구현하게 되면 아까 Bill Pugh의 방법과는 다르게 리플렉션과 직렬화로 깨뜨릴 수 없게 됩니다.

하지만 그렇다고 해서 Enum이 장점만 있는 것은 아닙니다.

 

 

5-1. 문제점

Enum으로만 구현하게 됐을 때는 싱글톤에서 다시 멀티톤으로 변경하려고 할 때 코드를 다시 짜야합니다.

  • 싱글톤을 해제할 때 번거로움
  • Enum외의 클래스 상속 불가

 

 

6. 최종 권장하는 싱글톤 패턴 구현 방법 정리

1. Bill Pugh

1-1. 권장하는 이유

  • Lazy Loading
  • thread-safe

이 방법으로 즉시 로딩해서 리소스 낭비될 일도 없고 thread-safe 하게 구현될 수 있습니다.

 

 

2. Enum

2-1. 권장하는 이유

  • thread-safe
  • 간편하다.

싱글톤이 enum 외의 클래스를 상속하지 않는 환경이라면 enum으로 싱글톤 패턴을 쉽게 만들 수 있습니다.

 

 


7. 정적 클래스와의 차이

🙋‍♂️ 사실 자바에는 정적클래스가 따로 존재하지 않습니다.
편의상 정적메서드만을 갖고 있는 클래스를 정적클래스라 부르겠습니다.

 

static 메서드들은 클래스 초기화시에 메서드 영역(method area)에 등록되어 프로그램이 끝날 때 해제됩니다.

 

따라서 애플리케이션 내에서 싱글톤 패턴과 마찬가지로 전역적으로 사용할 수 있고

인스턴스를 따로 생성하지 않기 때문에 유일성을 보장받을 수 있습니다.

 

하지만 인스턴스를 생성할 수 없기 때문에 클래스 메서드를 이용합니다.

 

 

8. 싱글톤과 정적 클래스는 언제 사용할까 ?

8-1. 싱글톤 패턴

  • 상속받아서 사용가능하다.
  • 메서드 파라미터로 사용가능하다.

따라서

  • 완벽한 객체지향을 필요로 할 때
    • 애플리케이션 내에서 객체처럼 사용하고 싶을 때
  • 레이지 로딩이 필요할 때
    • 혹은 인스턴스 생성할 때 리소스가 많이 드는 경우 권장 되어집니다.

 

8-2. 정적 클래스

  • 객체 생성을 하지 않는다.
    • 정적 클래스는 객체처럼 사용할 수는 없지만 컴파일 시 정적 바인딩이 되기 때문에 보통 싱글톤보다 효율이 좋습니다.

따라서

  • 유틸 메서드를 보관하는 용도로 사용할 때
    • 객체 성질이 필요없을 때 사용하는 것 권장
  • 다형성이나 상속이 필요없는 클래스

 

 

 

 

 

 

 

 

[10분 테코톡] 아서의 싱글턴 패턴과 정적 클래스

 

 

 

 
반응형

댓글