상세 컨텐츠

본문 제목

[Design Pattern] 싱글톤 패턴(Singleton Pattern) -2

Development/Java

by thisisnew 2019. 10. 22. 23:09

본문

반응형

 

https://thisisnew-storage.tistory.com/8

 

[Design Pattern] 싱글톤 패턴(Singleton Pattern) -1

자바의 디자인 패턴에서 단골로 등장하는 '싱글톤 패턴'에 대해 알아보도록 하겠습니다. 이 패턴은 인스턴스가 오직 하나만 생성되고, 생성된 인스턴스를 어디서든 접근하여 이용할 수 있게 만드는 패턴입니다...

thisisnew-storage.tistory.com

1편에서 미리 동기화의 문제가 있다고 말씀드렸는데요.

 

구체적으로 어떤 문제인지 살펴보도록 하겠습니다.


'인스턴스를 하나만 생성하여, 이 동일한 객체를 어디서든 접근하여 이용하는 것'이 싱글톤 패턴이라고 말씀드렸었죠?

 

과연 '어떠한 경우에도' 객체는 하나만 생성되는지 코드를 통해 확인해보도록 하겠습니다.

 

먼저, 멀티스레드(Multi Thread)를 이용한 코드를 간단하게 구현해보죠.

//MultiThread.java

public class MultiThread extends Thread{
	
	private String name;

	public MultiThread(String name) {
	   this.name = name;
	}

	public void run() {

    	int count = 0;

    	for(int i=0; i<5; i++) {
    		count++;
    		Singleton singleton = Singleton.getInstance();
    		System.out.println(name+ "의"+ count+"번째 쓰레드의 singleton 객체  : " + singleton.toString());	

    		try {	
    			Thread.sleep(400);
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
}

반복문을 이용하여 Singleton 객체를 5회 생성할 것입니다.

 

그리고 상속받은 Thread 클래스를 이용하여 중간에 0.4초씩 멈출 겁니다.

 

이런 MultiThread 객체를 Main 클래스에서 2개(AAA, BBB) 생성하여 동작시켜보죠.

//Main.java

public class Main {
	public static void main(String[] args) {
	    
		MultiThread AAA = new MultiThread("AAA");
		MultiThread BBB = new MultiThread("BBB"); 
		
		AAA.start();
		BBB.start();
	    
	}
}

결과는 아래와 같습니다. 

 

어떤가요?

 

2개의 스레드가 번갈아가며 Singleton 객체에 접근하자 2개의 객체가 생성되는 것을 볼 수 있습니다.

 

빨간색 박스와 노란색 박스를 보면 더욱 명확하게 알 수 있죠.

 

어째서 이렇게 된 것일까요?

 

이론대로라면 분명 싱글톤 패턴은 하나의 객체만 생성해서 여럿이서 접근하여 이용하는 것인데 말이죠.

 

다음 사진은 2개의 MultiThread 객체가 어떻게 동작하는지 나타낸 것입니다.

 

순서대로 보도록 하죠.

 

  1. AAA 스레드 객체가 getInstance()에 접근합니다. single 객체는 null이므로 무난하게 조건문을 통과합니다. 다음은 BBB차례.
  2. BBB 스레드 객체가 getInstance()에 접근합니다. single 객체는 여전히 null이므로 역시 조건문을 통과합니다. 다시 AAA차례.
  3. AAA 스레드 객체가 접근한 single 객체는 새로운 객체를 생성하여 반환합니다.
  4. BBB 스레드 객체가 접근한 single 객체도 새로운 객체를 생성하여 반환합니다.

 

동기화 처리가 되지 않아서 발생한 문제임을 알 수 있죠? 그럼 이제 코드를 수정해보도록 하겠습니다.

//Singleton.java

public class Singleton {

  private Singleton() {}
  
  private static Singleton single = null;

  public static synchronized Singleton getInstance() {
  
    if(single == null) {
      System.out.println("싱글톤 생성...");
      single = new Singleton();
    }

   return single;
  }
  
}

getInstance() 메서드에 synchronized를 붙여 동기화 처리를 해줬습니다. 결과를 볼까요?

 

싱글톤 생성이 한 번만 일어나고, 그 뒤로는 동일한 객체를 사용하는 것을 알 수 있네요. 해결되었습니다.

 

하지만 메서드 자체를 동기화해서, 실행할 때마다 동기화가 되는 것이 성능적으로 과연 좋을까요?

 

원래의 목표는 처음 singleton 객체를 생성할 때만 동기화가 되는 것인데 말이죠.

 

코드를 다음과 같이 바꿔봅니다.

//Singleton.java

public class Singleton {

  private Singleton() {}
  
  private static volatile Singleton single = null;

  public static Singleton getInstance() {
  
    if(single == null) {
      synchronized(Singleton.class){
        if(single == null) {
          System.out.println("싱글톤 생성...");
          single = new Singleton();
        }
      }
    }

   return single;
  }
  
}

조건문을 이중으로 만들었죠? 이런 패턴을 'Double-Checked Locking Pattern(이하 DCLP)'라고 하는데요.

  1. 첫 번째 조건문으로 single 객체의 존재 여부를 판단하여 빠른 객체의 반환을 꾀할 수 있고,
  2. 두 번째 조건문에 진입하면서 동기화되어 하나의 객체만 생성하는 것이 가능해집니다.

 

private static volatile Singleton single = null;

그리고 기존의 single 객체에는 volatile을 붙여주었는데요.

 

이유를 알기 위해서는 스레드의 메모리 활용을 먼저 봐야 할 필요가 있습니다.

 

스레드를 이용하게 되면, 각각의 스레드는 성능을 끌어올리기 위해 'Cache Memory'를 사용하는데요.

 

첫 번째 스레드가 'Register'→ 'Cache Memory' → 'Main Memory'의 순서로 값을 대입해주면, 다음 스레드는 'Main Memory'에 담긴 값을 'Cache Memory'에 담아 읽어 들이는 식으로 동작합니다.

 

문제는 첫 번째 스레드가 'Main Memory'에 값을 대입하기 전에, 다음 스레드가 'Main Memory'에서 값을 읽어 들이려고 할 때 발생합니다.

 

volatile은 대입과 읽는 것을 전부 'Main Memory'에서 하도록 만드는데요. 그래서 이런 시간차를 극복하는데 도움이 될 수 있습니다. 

 

결과를 보도록 하죠.

 

 

문제없이 동기화가 되었네요.

 

하지만 'Main Memory'를 바로 사용하는 것은 곧, 성능을 떨어뜨릴 수 있고, 또 이렇게 구현하는 DCLP는 자바 1.5 버전 이상에서만 호환이 됩니다.

 

마지막으로, 코드를 하나만 더 작성해 보도록 하죠.


다음은 'Bill Pugh Solution'이라는 윌리엄 푸가 제안한 모델입니다.

 

클래스 내부에 클래스를 하나 더 만드는 방법인데요.

 

이게 무엇을 의미하는 것인지 코드를 통해 설명해 드리겠습니다.

//Singleton.java

public class Singleton {
  private Singleton() {}

  public static Singleton getInstance() {
  	return LazyHolder.singleton;
  }

  private static class LazyHolder{
  	private static final Singleton singleton = new Singleton();
  }

}

 

Singleton 클래스 안에 LazyHolder라는 이름의 클래스를 작성하였습니다.

 

LazyHolder 클래스는 static을 붙여 메모리에 미리 할당했는데요. 내부의 변수 singleton도 final을 붙여 차후에 값이 변하지 않도록 상수화 했습니다.

 

첫 번째 스레드가 getInstance() 메서드를 호출하면 JVM은 LazyHolder 클래스를 로드(load)하게 되는데요.

 

이미 메모리에는 올라가 있으니, JVM은 이것을 한 번만 로드합니다.

 

이때 중요한 것은 두 번째 스레드가 getInstance() 메서드를 호출하더라도, JVM은 두 번 로드하지 않고 첫 번째 로드가 끝나고 초기화가 완료될 때까지 기다리게 됩니다.

 

JVM이 제공하는 동기화 기법에 따른 것이죠.

 

이로서 이 방식은 동기화의 문제도 자연스럽게 해결할 수 있습니다.

 

그래서 여러 문서들에서 가장 추천하는 방식으로 꼽히고 있죠.

 

이 외에도 다양한 방법의 싱글톤 패턴이 있습니다만, 여기까지 소개해 드리려고 합니다.

 

도움이 되셨으면 좋겠네요. 감사합니다.

 

 

 

 

반응형

관련글 더보기

댓글 영역