Singleton Pattern

김영우 (AvocadoSmasher)·2022년 11월 21일
0

Design Pattern

목록 보기
5/8

🔎 What is Singleton Pattern??

싱글톤 패턴(Singleton Pattern)은 객체의 생성에 관련된 패턴 중 하나로 한 Class의 객체를 하나만 존재하도록 하는 방법입니다.

일반적으로 getInstance 라는 메소드를 구현하여 해당 메소드를 통해서만 instance를 생성하고 받을 수 있도록 하는 형태로 구현됩니다.

⚓️ Digging into how it works

싱글톤 패턴은 객체의 생성과 관련된 패턴으로 생각보다 그 구조는 단순합니다. 간단한 예제를 통해서 보도록 하죠.

현재 사운드 드라이버에 대한 객체를 디자인 한다고 생각해 보도록 하죠. 이때 여러 프로그램에서 해당 사운드 드라이버를 호출하여 소리를 출력하려고 하는 상황입니다. 이럴때 사운드 드라이버 객체는 하나의 같은 객체에 접근해야 합니다. 어떻게 해야할까요? 밑의 Code 부분을 통해서 확인합시다.

💻 Code (feat. Java)

SoundDriver class

public class SoundDriver{
		SoundDriver(){
				System.out.println("SoundDriver has been activated");
		}
		public void makeSound(){
				System.out.println("Dong Dong");
		}
}

그런데 이렇게 디자인을 하고 객체를 생성할때 new 키워드를 통해서 생성하면 여러곳에서 해당 객체를 호출하면 다 각기 다른 Instance를 호출하게 되겠죠? 이때 Singleton Pattern을 적용시켜 봅시다.

SoundDirver class ( Singleton implemented )

public class SoundDriver{
		private static SoundDriver instance = null;
		private SoundDriver(){
				System.out.println("SoundDriver has been activated");
		}
		public SoundDriver getInstance(){
				if(instance == null) instance = new SoundDriver();
				return instance;
		}
		public void makeSound(){
				System.out.println("Dong Dong");
		}
}

이렇게 Singleton Pattern을 적용했습니다. 하나하나 설명은 아래와 같습니다.

  • private static SoundDriver instance instance를 저장할 속성으로 private으로 접근제어를 설정하여 외부에서 접근하지 못하도록 하고 static으로 선언하여 객체의 인스턴스 생성 전에도 값을 담을 수 있도록 합니다.
  • 생성자에 private 기본적으로 Java 에서 생성자에 접근제어자를 설정해주지 않으면 public을 default로 설정해줍니다.
  • getInstance() 메소드. if문을 통해서 인스턴스가 null이면 객체의 생성이 되지 않았음으로 객체를 생성해서 instance 속성에 할당해주고 instance를 반환해줍니다. if문을 사용함으로써 객체가 필요한 시점에 생성되어 할당될 수 있도록 디자인 되어있습니다.

이와 같이 생성하면 Singleton Pattern이 적용되어 class당 하나의 인스턴스만 생성하여 사용하도록 구현되었습니다. 이렇게 구현하면 일반적인 경우에는 문제가 되지 않습니다. 하지만 세상은 그렇게 녹록치 않더라구요… 이렇게 구현한 싱글톤 패턴도 멀티 쓰레드 환경에서는 문제가 됩니다. 이유는 다음과 같습니다.

getInstance 메소드의 if문의 조건 판별 부분을 통과한 후에 context switch가 이루어 진다면 오류가 생길 수 있습니다.

이러한 문제를 해결하기 위한 방법을 2가지 정도 제시하고자 합니다.

방법 1.) instance 속성에 바로 객체생성 및 할당.

위의 코드에서 instance 속성과 getInstance를 수정하여 적용가능한 방법입니다.

private static final SoundDriver instance = new SoundDriver();

public SoundDriver getInstance(){
		return instance;
}

이렇게 하면 우선 멀티 스레드 환경에서의 instance 생성시의 문제를 해결가능한데 이러한 방법은 객체를 사용하지 않더라도 일단 객체를 생성하기에 이러한 방법으로 다수의 객체를 사용시 메모리가 불필요하게 많이 사용됩니다.

방법 2.) synchronized & volatile 사용. Double-Checked Locking 방법.

public class SoundDriver{
		private volatile static SoundDriver instance;
		private SoundDriver(){
				System.out.println("SoundDriver has been activated");
		}
		public static SoundDriver getInstance(){
				if(instance == null) {
						synchronized(this){
								if(instance == null){
										instance = new SoundDriver();
								}
						}	
				}
				return instance;
		}
		public void makeSound(){
				System.out.println("Dong Dong");
		}
}

우선 이를 이해하기 위해 새로운 2가지 키워드에 대해 소개하고자 합니다.(사실 저도 생소한 문법이긴합니다.)

  • volatile 변수를 Main Memory에 저장하겠다는 의미입니다. 일반적으로 멀티 스레드 환경에서 변수의 값을 읽고 저장할때 성능향상을 위해 Main Memory가 아닌 각 스레드의 CPU cache 단에서 이루어집니다. 하지만 이로 인해 스레드간의 공유자산에 대해서 값의 불일치가 일어나게 되고 이를 해결하기 위해 Main Memory에 값을 쓰고 읽도록 하는 키워드가 volatile입니다. 이해를 돕기위해 아래 그림을 참고하시면 될것같네요. 출처 : https://jenkov.com/tutorials/java-concurrency/volatile.html
  • synchronized synchronized 키워드는 멀티스레드 환경에서 스레드간 동기화를 위한 문법으로 하나의 스레드만 해당 키워드를 붙인 데이터에 접근하게 허락하는 방법으로, 한마디로 lock을 거는 문법입니다. 위의 예제에서 보면 if문 안에서 synchronized(this){} 라는 부분이 쓰임을 볼 수 있다. 해당 괄호 안에는 object가 오는 자리인데 해당 코드에서는 해당 static 메소드를 가진 클래스를 가르키는 객체가 온것이다.

전체적으로 보면 해당 코드는 “멀티 스레드 환경에서 Thread-safe한 코드를 작성하기 위해 synchronized라는 키워드를 사용하여 lock을 한다” 라는 부분까지는 얼추 이해가 쉬울 것이라 보는데 왜 하필이면 if(instance == null) 부분 후에 synchronized를 사용하고 다시 if(instance == null) 을 사용하는가? 하는 부분이 의문이 들수 있다. 사실 해당 static 메소드 전체에 sychronized 키워드를 걸수도 있지만 그렇게 한다면 성능의 저하가 심하다. 성능의 비약적인 상승을 위해서 lock을 거는 부분은 문제가 생길수 있는 가장 작은 부분에 걸어주는 것이 좋은데 해당 코드에서는 if문 부분이다. 그렇다면 synchrozied를 if문 전에 걸어주고 if문을 한번만 사용하면 되는것이 아닌가? 라는 의문이 들 수 있는데 한가지 경우를 생각해보자. 예를들어 instance는 생성되어있고 해당 if문의 조건을 확인하다가 context switch가 일어났다고 생각해보자. 그렇다면 다음 스레드가 getInstance를 호출한다면 if문 앞에서 lock이 걸리는 것이 맞을까? 그렇지 않다. 그렇다면 if문 안에 synchronized를 사용하고 synchroznied 안에 if문을 제거한다면? 이부분은 생각해 볼 필요도 없다고 생각한다.

너무 이야기가 길어진것 같은데 아직 와닿지 않는다면 곰곰히 생각해보길 바란다.

(스스로 생각해서 해답을 찾는것이 중요하다고 생각한다.)

📌 Summary

오늘은 한 클래스당 하나의 instance만을 가지도록 하는 Singleton pattern에 대해서 알아보았습니다. 또한 추가적으로 Multi-Thread환경에서 thread-safe한 방법으로 singleton 패턴을 구현하는 것 또한 알아보았는데 이를 위해 synchronized 키워드와 volatile에 대해 추가적으로 알아보았습니다. 사실 가장 간단한 방법은 instance를 저장하는 속성 자체를 static final 로 선언한 후 바로 instance를 생성해서 할당해 주는 방법이 있는데 이는 해당 클래스를 아예 사용하지 않더라도 메모리 공간을 차지한다는 메모리 누수적인 단점이 있기에 언급하지 않았습니다. 아무튼! 해당 싱글톤 패턴을 잘 사용한다면 하나의 인스턴스만이 존재해야 하는 클래스를 만들어 사용하고 관리할 수 있다는 점 기억하고 넘어가도록 하면 좋을 것 같습니다.

profile
Android Studio 공부 중

0개의 댓글