주니어 개발자 성장기

2. 싱글톤 패턴 - 구현(Java) 본문

CS스터디/디자인 패턴

2. 싱글톤 패턴 - 구현(Java)

Junpyo Lee 2023. 3. 12. 15:49

싱글톤 패턴 구현


 

스프링의 도움 없이 순수 자바로 싱글톤 패턴을 구현해보자.

영문 위키피디아에 따르면 구현 절차는 간단하게 다음과 같다.

 

 

  • 다른 객체에 의해서 초기화되는 것을 방지하기 위해 모든 생성자를 private 으로 선언한다.
  • 인스턴스에 대한 참조(reference)를 반환하는 정적(static) 메서드를 제공한다.

 

 

 

위의 절차에 따라 구현하는 방법이 크게 5가지가 있다.

 

결론부터 말하자면, 가장 바람직한 모델은 5번 Bill Pugh 구현과 Enum 구현이다.

 

그럼 차례대로 직접 구현해보자.

 

 

 

1. Eager Initialization


Eager Initialization은 클래스 로딩 단계에서 인스턴스를 생성하는 방법이다.

// 싱글톤 객체
public class EagerSingletonService {

	private String status;
	private static final EagerSingletonService instance = new EagerSingletonService();

	private EagerSingletonService(){
		System.out.println("EagerSingletonService.EagerSingletonService 인스턴스 생성");
	}

	public static EagerSingletonService getInstance() {
		System.out.println("getInstance 호출");
		return instance;
	}

	public static void call() {
		System.out.println("EagerSingletonService.call");
	}

	public String getStatus() {
		return status;
	}

	public void setStatus(String status) {
		this.status = status;
	}
}

public class EagerSingletonApp {
	public static void main(String[] args) {
		System.out.println("앱 시작");
		EagerSingletonService.call();
		EagerSingletonService instance1 = EagerSingletonService.getInstance();
		EagerSingletonService instance2 = EagerSingletonService.getInstance();

		instance1.setStatus("변경 전");

		System.out.println();
		System.out.println("instance1 주소 = " + instance1);
		System.out.println("instance2 주소 = " + instance2);
		System.out.println("instance1.equals(instance2) = " + instance1.equals(instance2));
		System.out.println();


		System.out.println("instance1 상태 = " + instance1.getStatus());
		System.out.println("instance2 상태 = " + instance2.getStatus());

		System.out.println("instance 2의 상태 변경");
		instance2.setStatus("변경 후");
		System.out.println();

		System.out.println("instance1 상태 = " + instance1.getStatus());
		System.out.println("instance2 상태 = " + instance2.getStatus());
	}
}

EagerSingletonApp 실행 결과:

앱 시작
EagerSingletonService.EagerSingletonService 인스턴스 생성
EagerSingletonService.call
getInstance 호출
getInstance 호출

instance1 주소 = singleton.eager.EagerSingletonService@129a8472
instance2 주소 = singleton.eager.EagerSingletonService@129a8472
instance1.equals(instance2) = true

instance1 상태 = 변경 전
instance2 상태 = 변경 전
instance 2의 상태 변경

instance1 상태 = 변경 후
instance2 상태 = 변경 후

Process finished with exit code 0

결과를 보면 2가지를 알 수 있는데,

  1. 두 인스턴스의 주소값이 같아 싱글톤 객체이다.
  2. 인스턴스 호출 여부와 상관 없이 클래스가 static으로 호출되면(클래스 로딩) 인스턴스가 생성된다.

 

2번째 특징으로 인해

클래스에 접근하는 것만으로도 인스턴스가 생성되고, 불필요한 리소스 낭비가 발생한다.
따라서, 파일 시스템, DB 연결 등 큰 리소스를 다루는 경우 getInstnace() 호출 전 까지 인스턴스 생성을 미루는게 바람직하다.

라고 한다.

게다가 Exception Handling도 제공하지 않는다고 한다.

 

하지만 서버 프로그램의 경우 자바 프로그램이 계속 구동되기 때문에 굳이 의미가 있나 싶긴하다. (어차피 스프링 IoC 가 알아서 해주지만)

(개인 사견이므로 다른 의견 및 반박 환영합니다.)

 

 

 

2. Static Block Initialization


위의 방법과 차이는 Exception Hnadling이 가능하다는 것이다.

public class StaticBlockService {

	private String status;
	private static StaticBlockService instance;

	private StaticBlockService(){}

	static {
		try {
			instance = new StaticBlockService();
			System.out.println("StaticBlockService.static initializer 인스턴스 생성");
		} catch (Exception e) {
			throw new RuntimeException("Exception occured in creating singleton instance");
		}
	}

	public static StaticBlockService getInstance() {
		System.out.println("getInstance 호출");
		return instance;
	}

	//.... 중복 생략
}


public class StaticBlockApp {
	public static void main(String[] args) {
		System.out.println("앱 시작");
		StaticBlockService.call();
		StaticBlockService instance1 = StaticBlockService.getInstance();
		StaticBlockService instance2 = StaticBlockService.getInstance();
		//.... 중복 생략
	}
}

결과:

앱 시작
StaticBlockService.static initializer 인스턴스 생성
StaticBlockService.call
getInstance 호출
getInstance 호출

instance1 주소 = singleton.staticblock.StaticBlockService@129a8472
instance2 주소 = singleton.staticblock.StaticBlockService@129a8472
instance1.equals(instance2) = true

instance1 상태 = 변경 전
instance2 상태 = 변경 전
instance 2의 상태 변경

instance1 상태 = 변경 후
instance2 상태 = 변경 후

Process finished with exit code 0

Instance가 필요하지 않아도 클래스가 로딩될 때 즉시 인스턴스가 생성되는 것을 확인 할 수 있다. 하여, 여전히 큰 리소스를 다루기에는 적합하지 않다고 한다.

 

3. Lazy Initialization


클래스가 로딩될때가 아닌 최초의 인스턴스를 가져오는 메서드 호출에서 인스턴스를 생성하는 방식이다.

public class LazyService {

	private String status;
	private static LazyService instance;

	private LazyService(){
		System.out.println("LazyService.LazyService 인스턴스 생성");
	}

	public static LazyService getInstance() {
		System.out.println("getInstance 호출");
		// Thread-unsafe 하다.
		if(instance == null) {
			instance = new LazyService();
		}
		return instance;
	}

	//.... 중복 생략
}

public class LazyApp {
	public static void main(String[] args) {
		System.out.println("앱 시작");
		LazyService.call();
		LazyService instance1 = LazyService.getInstance();
		LazyService instance2 = LazyService.getInstance();
        //.... 중복 생략
        }
}

결과:

앱 시작
LazyService.call
getInstance 호출
LazyService.LazyService 인스턴스 생성
getInstance 호출

instance1 주소 = singleton.lazy.LazyService@129a8472
instance2 주소 = singleton.lazy.LazyService@129a8472
instance1.equals(instance2) = true

instance1 상태 = 변경 전
instance2 상태 = 변경 전
instance 2의 상태 변경

instance1 상태 = 변경 후
instance2 상태 = 변경 후

Process finished with exit code 0

최초 getInstance 호출 시에만 인스턴스가 생성되는 것을 확인 할 수 있다. 따라서, 리소스 낭비를 최소화 할 수 있다.

 

하지만 이 방식에도 문제점이 있다.

Thread-unsafe 하다는 것이다. 여러 쓰레드가 if문 안에 동시에 진입할 경우 예측 불가한 결과를 얻게 된다.

결국, 이 방식은 싱글 쓰레드 환경에서만 활용하는 것이 좋다.

 

 

4. Thread Safe Singleton


인스턴스를 호출하는 메서드에 synchronized 키워드를 걸어두어 Thread-safe를 보장할 수 있다.

public class ThreadSafeService {

	private String status;
	private static ThreadSafeService instance;

	private ThreadSafeService(){
		System.out.println("ThreadSafeService.ThreadSafeService 인스턴스 생성");
	}

	// synchronized 키워드로 Thread-safe 하다.
	// 하지만, 인스턴스 호출이 잦다면 성능저하로 이어진다.
	public static synchronized ThreadSafeService getInstance() {
		System.out.println("getInstance 호출");
		if(instance == null) {
			instance = new ThreadSafeService();
		}
		return instance;
	}

	//.... 중복 생략
}


public class ThreadSafeApp {
	public static void main(String[] args) {
		System.out.println("앱 시작");
		ThreadSafeService.call();
		ThreadSafeService instance1 = ThreadSafeService.getInstance();
		ThreadSafeService instance2 = ThreadSafeService.getInstance();
		//.... 중복 생략
	}
}

(앞으로 결과는 다 비슷하기 때문에 생략)

하지만, synchronized 키워드로 인해, 다른 쓰레드에서 getInstance()를 호출하는 것이 Block 되어 인스턴스 호출이 잦은 어플리케이션의 성능이 저하될 수 있다. 

 

이를 보완 하기 위해 Double Checked Locking을 이용할 수 있다.

public class DoubleCheckLockingService {

	private String status;
	private static DoubleCheckLockingService instance;

	private DoubleCheckLockingService(){
		System.out.println("DoubleCheckLockingService.DoubleCheckLockingService 인스턴스 생성");
	}

	// Double check locking 으로
	// instance가 null일 때만 synchronized가 작동한다.
	public static DoubleCheckLockingService getInstance() {
		System.out.println("getInstance 호출");
		if(instance == null) {
			synchronized (DoubleCheckLockingService.class) {
				if(instance == null) {
					instance = new DoubleCheckLockingService();
				}
			}
		}
		return instance;
	}
    //.... 중복 생략
}

instance가  null일 때만 synchronized가 동작하기 때문에, 성능 문제를 해결 할 수 있다.

 

 

5. Bill Pugh Singleton Implementation


inner static helper class를 사용하는 방식으로 앞선 방식의 문제점들을 대부분 해결해준다.

public class BillPughService {

	private String status;
	private BillPughService(){
		System.out.println("EagerSingletonService.EagerSingletonService 인스턴스 생성");
	}

	private static class SingletonHelper {
		private static final BillPughService INSTANCE = new BillPughService();
	}

	public static BillPughService getInstance() {
		System.out.println("getInstance 호출");
		return SingletonHelper.INSTANCE;
	}
	//.... 중복 생략
}

Helper Class를 통해

  • Lazy Initialization
  • Thread-safe
  • synchronized 로 인한 성능 저하 해결

를 모두 달성 할 수 있다.

 

하지만 1 ~ 5번 방식 모두 자바 리플렉션(Reflection)에 의해 싱글톤이 깨질 수 있다.

 

+) Enum Singleton


리플렉션으로 인한 싱글톤 파괴를 방지하는 Singleton이다.

public enum EnumSingleton {

	INSTANCE;

	private String status;

	public String getStatus() {
		return status;
	}

	public void setStatus(String status) {
		this.status = status;
	}
}

하지만 다음과 같은 한계를 가진다.

  • 1 - 2 번과 같이 사용되지 않을 경우 리소스 낭비가 발생할 수 있다.
  • Enum이기 때문에 상속이 불가능하다.

 

정리
싱글톤을 직접 구현할 때는 Bill Pugh 방식 혹은 Enum 싱글톤을 활용하자!

 

 

 

소스 코드: https://github.com/wnsvy607/Design-Pattern-Practice/tree/main/src/singleton

 

참고:

 

 

'CS스터디 > 디자인 패턴' 카테고리의 다른 글

1. 싱글톤 패턴 - 기본  (0) 2023.03.12