싱글턴 패턴과 정적 클래스

Jeong Gyejin·2023년 4월 5일
0

JAVA

목록 보기
18/18

싱글턴 패턴

객체의 인스턴스가 오로지 한개만 생성되도록 설계하는 것을 의미합니다. 주로 로그 기록, 캐싱, 사용자 설정 등에 사용이됩니다.
그렇다면 왜 이 싱글턴 패턴을 이용하는 것일까요? 그 이유는 유일성을 보장하고 전역적으로 사용할 수 있기 때문입니다.

싱글턴 패턴을 만들어보자

만약 애플리케이션의 배경을 설정하는 상황에서 Settings라는 배경색을 담당하는 클래스를 만들어보도록 하겠습니다.
만약 Settings가 애플리케이션 내부에서 유일하지 않다고 하면 에러가 발생할 수 있기 때문에 싱글턴 패턴을 이용해서 구현해보도록 하겠습니다.

1. 순수한 구현

  • 인스턴스를 private static 변수로 정적필드에 저장하였습니다.
  • getInstance()에서 인스턴스를 생성했습니다.
  • private을 이용해서 외부 생성자가 접근할 수 없도록 했습니다.
public class Settings{
	private static Settings instance;
        
    private Settings(){
    }

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

하지만 해당 구현에는 멀티 스레드환경에서 안정성이 떨어지게 됩니다. 스레드 2개가 동시에 진행될때 첫번째 스레드가 조건문을 통과하는 동안에 두번째 스레드가 먼저 통과하게 된다면, 다른 인스턴스가 발생할 위험이 있습니다.

2. 동기화(Synchronized)

  • synchronized getInstance()에서 인스턴스를 생성했습니다.
public class Settings{
	private static Settings instance;
        
    private Settings(){
    }

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

하지만 이렇게 작성을 하게된다면 멀티 스레드환경에서 인스턴스 하나만 만들기 위해서 synchrnized 키워드를 이용하게 된다면 인스턴스가 있는 경우에는 리소스의 낭비가 발생하게 됩니다.

3. Double Checked Locking(DCL)

  • synchronized 시점 지연
  • private static volatile 인스턴스를 이용합니다.
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;
	}
}

위와 같이 코드를 작성할 경우에는 인스턴스가 존재할 경우에는 아래의 영역이 skip이 되어 즉시 반환할 수 있어 리소스의 낭비가 줄어들게 됩니다.

	synchronized(Settings.class){
		if(instance == null){
    		instance = new Settings();
    		}

스레드를 이용하게 되면 각각의 스레드는 성능을 위해서 캐시메모리를 이용하게 됩니다. 첫번째 스레드가 캐시메모리에서 메인 메소드로 값을 대입해주게 된다면 다음 스레드는 메인메모리에 담긴값을 캐시메모리에 담는 방식으로 이용하게 됩니다.

그러나 첫번째 스레드가 메인메모리에 값을 대입하기 전에 다음 스레드가 메인메모리의 값을 읽으려고 할때 문제가 발생합니다.

  • volatile:
    volatile이라는 키워드를 이용하게 된다면 대입과 읽는 행위 모두 메인메모리에서 하게 만들어서 시간 차이를 줄일 수 있습니다.

그러나 이 패턴의 경우 volatile은 JDK 1.5이상일때만 이용이 가능하며, JVM에따라서 스레드를 스윕하지 않는 경우가 발생할 수 있습니다.

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

가장 권장되어지는 사용법입니다.

  • Holder역할을하는 static inner class 인스턴스를 이용하여 구현하는 방식입니다.
public class Settings{        
    private Settings(){
    }
    private static class SettingsHolder{
    	private static final Settings SETTINGS = new Settings();
    }
	
    public class Settings getInstance(){
    	return SettingsHolder.SETTINGS;
    }
}

최초로 class loader에 의해서 로딩될 때, synchronized 실행되며 명시적으로 synchronized를 이용하지 않고 동일한 효과를 낼 수 있습니다.

    private static class SettingsHolder{
    	private static final Settings SETTINGS = new Settings();
    }

위 영역은 static이기 때문에 메소드가 실행이 될때, JVM에 StaticInitialize에 의해 초기화되고, 메모리에 올라가게 됩니다. 따라서 스레드의 안정성과 LAZY로딩을 모두 만족하게 됩니다.
하지만 클라이언트가 리플렉션과 직렬화를 이용해 임의로 싱글턴을 파괴할 수 있습니다.

5. Enum

public enum Settings{
	INSTANCE;
}

Enum자체가 싱글톤패턴이기 때문에 생성자를 private으로 가지게 만들며,상수만 갖는 클래스이기 때문에 싱글턴의 성질을 가지게 됩니다.
하지만 enum으로만 구현하게 될 경우에는 싱글턴을 해제할 때 번거로움의 문제가 있으며, Enum외의 클래스를 상속할 경우 사용할 수가 없습니다.

4번과 5번의 경우가 사용을 권장합니다.

정적 클래스

정적 메서드만 가지고 있는 클래스를 의미합니다.
static 메소드들은 클래스 초기화시에 메소드 영역에 등록되어 프로그램이 끝날때 해제가 됩니다. 애플리케이션 내에서 싱글턴패턴과 마찬가지로 전역적으로 사용할 수 있으며, 유일성을 보장받을 수 있습니다.

그러나 차이점으로는 인스턴스를 생성할 수 없기 때문에, 클래스 메소드를 이용한다는 점입니다.

싱글턴 패턴은 상속받아서 사용이 가능하며, 메서드 파라미터로 이용이 가능합니다. 그렇기 때문에 객체처럼 사용하고 싶거나 인스턴스 생성시 리소스가 많이 드는 경우 권장이 됩니다.
정적 클래스는 객체처럼 사용할 수는 없지만 컴파일시 정적 바인딩이 되기 때문에 보통 싱글턴보다 효율이 좋으며, 유틸 클래스처럼 객체의 성질이 필요없을 때 이용하는 것이 좋습니다.

profile
항상 더 나은 개발자가 되기 위해서 끊임없이 공부하고 학습하면서 성장하는 사람이 되겠습니다.

0개의 댓글