[생성패턴]싱글톤(Singleton)

seyeong oh·2022년 11월 29일

JAVA

목록 보기
3/6
post-thumbnail

싱글톤

싱글톤은 생성 패턴(Creational Pattern) 중 하나이다.

  • 생성패턴은 인스턴스를 만드는 절차를 추상화하는 패턴이다.
  • 생성패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현방식을 시스템과 분리해줍니다.
  • 생성패턴은 시스템이 상속보다 복합방법을 사용하는 방향으로 진화되어 가면서 더 중요해지고 있다.

생성패턴의 가장 큰 특징은 생성패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며, 이것이 어떻게 생성되는지, 언제 생성할것인지 결정하는데 유연성을 확보할 수 있게 된다.

싱글톤 패턴이란?

싱글톤 패턴은 어떤 클래스의 인스턴스가 오직 하나임을 보장하며, 이 인스턴스에 접근할 수 있는 전역적인 접촉점을 제공하는 패턴입니다. 다시 말해 프로그램 시작부터 종료 시까지 어떤 클래스의 인스턴스가 메모리상에 단 하나만 존재할 수 있게 하고 이 인스턴스에 대레 어디서나 접근할 수 있도록 하는 패턴입니다.

싱글톤을 사용하는 이유
개발을 하다보면 어떤 클래스에 대해 단 하나의 인스턴스만을 갖도록 하는것이 좋은 경우가 있습니다.
예를 들어, 로그를 찍는객체라던가 쓰레드 풀, 윈도우 관리자 등 여러 객체를 관리하는 역할의 객체는 프로그램 내에서 단 하나의 인스턴스를 갖는것이 바람직하다.


싱글톤 구현방법

싱글톤 패턴을 구현하는 방법은 굉장히 다양하다.
그러나 각각의 패턴이 공통적으로 갖는 특징이 있다.

  • private 생성자만을 정의해 외부 클래스로부터 인스턴스 생성을 차단한다.
  • 싱글톤을 구현하고자 하는 클래스 내부에 멤버 변수로써 private static 객체 변수를 만든다.
  • public static 메소드를 통해 외부에서 싱글톤 인스턴스에 접글 할 수 있도록 접점을 제공한다.

예제)_ 개발중에 시스템에서 스피커에 접근할 수 있는 클래스를 만들자!


1. Eager initialization (이른 초기화 방식)

  • Eager Initialization은 가장 간단한 형태의 구현 방법이다. 이는 싱글톤 클래스의 인스턴스를 클래스 로딩 단계에서 생성하는 방법이다.
    그러나 어플리케이션에서 해당 인스턴스를 사용하지 않더라도 인스턴스를 생성하기 때문에 자칫 낭비가 발생할 수 있다.
public class SystemSpeaker {
	
	static private SystemSpeaker instance = new SystemSpeaker();
	
	private int Volume;
	
	private SystemSpeaker() {
		Volume =  5;
	}
	
	public static SystemSpeaker getInstance() {
		return instance;
	}
	
	public int getVolume() {
		return Volume;
	}
	
	public void setVolume(int volume) {
		Volume = volume;
	}
	
    
}
  • 이 방법을 사용할 때는 싱글톤 클래스가 다소 적은 리소스를 다룰 때여야 한다. File System, Database Connection 등 큰 리소스들을 다루는 싱글톤을 구현할 때는 위와 같은 방식보다는 getInstance() 메소드가 호출될 때까지 싱글톤 인스턴스를 생성하지 않는것이 더 좋다. 게다가 Eager Initialization은 Exception에 대한 Handling도 제동하지 않는다.

2. Static Block Initialization

  • Static Block Initialization은 1번에서 살펴본 Eager Initialization과 유사하지만 static block을 통해서 Exception Handling에 대한 옵션을 제공한다.
public class SystemSpeaker {
		
		static private SystemSpeaker instance;
		
		private int Volume;
		
		private SystemSpeaker() {
			Volume =  5;
		}
		static {
			try {
				instance = new SystemSpeaker();	
			} catch (Exception e) {
				throw new RuntimeException("Exception occured in creating singleton instance");
			}
		}
		public static  SystemSpeaker getInstance() {
			
			return instance;
		}
		
		public int getVolume() {
			return Volume;
		}
		
		public void setVolume(int volume) {
			Volume = volume;
		}
		
		
	}
  • 위와 같이 구현할 경우 싱글톤 클래스의 인스턴스를 생성할 때 발생할 수 있는 예외에 대한 처리를 할 수 있지만, Eager Initialization과 마찬가지로 클래스 로딩 단계에서 인스턴스를 생성하기 때문에 여전히 큰 리소스를 다루는 경우에는 적합하지 않게 된다.

3. Lazy Initialization (늦은 초기화 방식)

  • Lazy Initialization은 이름에 걸맞게, 앞선 두 방식과는 달리 나중에 초기화하는 방법이다.

  • 이는 global access 한 getInstance() 메소드를 호출할 때에 인스턴스가 없다면 생성한다.

public class SystemSpeaker {		
	
	static private SystemSpeaker instance;
	
	private int Volume;
	
	private SystemSpeaker() {
		Volume =  5;
	}
	
	public static SystemSpeaker getInstance() {
		if (instance==null) {
			instance = new SystemSpeaker(); 
		}
		return instance;
	}
	
	public int getVolume() {
		return Volume;
	}
	
	public void setVolume(int volume) {
		Volume = volume;
	}
	
	
}
  • 이 방식으로 구현할 경우 1, 2번에서 안고 있던 문제(사용하지 않았을 경우에는 인스턴스가 낭비)에 대해 어느 정도 해결책이 된다. 그러나 이 경우에도 문제점이 있다. 그건 바로 multi-thread 환경에서 동기화 문제이다.

  • 만약 인스턴스가 생성되지 않은 시점에서 여러 쓰레드가 동시에 getInstance()를 호출한다면 예상치 못한 결과를 얻을 수 있을뿐더러, 단 하나의 인스턴스를 생성한다는 싱글톤 패턴에 위반하는 문제점이 야기될 수 있다.

  • 그렇기에 이 방법으로 구현을 해도 괜찮은 경우는 single-thread 환경이 보장됐을 때이다.


4. Thread Safe Singleton

  • Thread Safe Singleton은 3번의 문제를 해결하기 위한 방법으로, getInstance() 메소드에 synchronized를 걸어두는 방식이다. synchronized 키워드는 임계 영역(Critical Section)을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 해 준다. 코드는 아래와 같다.
public class SystemSpeaker {
		
		static private SystemSpeaker instance;
		
		private int Volume;
		
		private SystemSpeaker() {
			Volume =  5;
		}
		
		public static synchronized  SystemSpeaker getInstance() {
			if(instance == null){
				instance = new SystemSpeaker();
			}
			return instance;
		}
		
		public int getVolume() {
			return Volume;
		}
		
		public void setVolume(int volume) {
			Volume = volume;
		}
		
		
	}

위와 같은 방식으로 구현한다면 getInstance() 메소드 내에 진입하는 쓰레드가 하나로 보장받기 때문에 멀티 쓰레드 환경에서도 정상 동작하게 된다. 그러나 synchronized 키워드 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 잦은 어플리케이션에서는 성능이 떨어지게 된다.


5. Bill Pugh Singleton Implementaion

  • 이는 Bill Pugh가 고안한 방식으로, inner static helper class를 사용하는 방식이다.

  • 앞선 방식이 안고 있는 문제점들을 대부분 해결한 방식으로, 현재 가장 널리 쓰이는 싱글톤 구현 방법이다.

public class SystemSpeaker {
		
		
		private int Volume;
		
		private SystemSpeaker() {
			Volume =  5;
		}
		
		private static class SystemSpeakerHelper {
			private static final SystemSpeaker INSTANCE = new SystemSpeaker();
		};
		
		public static SystemSpeaker getInstance() {
			return SystemSpeakerHelper.INSTANCE;
		}
		
		public int getVolume() {
			return Volume;
		}
		
		public void setVolume(int volume) {
			Volume = volume;
		}
		
		
	}
  • inner class로 인해 복잡해 보일 수 있지만 생각보다 간단하다.

  • private inner static class를 두어 싱글톤 인스턴스를 갖게 한다.

  • 이때 1번이나 2번 방식과의 차이점이라면 SystemSpeakerHelper 클래스는 Singleton 클래스가 Load 될 때에도 Load 되지 않다가 getInstance()가 호출됐을 때 비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 된다.

  • 아울러 synchronized를 사용하지 않기 때문에 4번에서 문제가 되었던 성능 저하 또한 해결된다.


6. Enum Singleton

  • 앞서 1~5번에서 살펴본 싱글톤 방식은 사실 완전히 안전할 수 없습니다. 왜냐하면 Java의 Reflection을 통해서 싱글톤을 파괴할 수 있기 때문이다.

  • 이에 Java 계의 거장 Joshua Bloch는 Enum으로 싱글톤을 구현하는 방법을 제안했다.

6. Enum Singleton
	public enum SystemSpeaker {
	INSTANCE;
	int Volume =5;
	public static void getInstance() {
		
	}
	public int getVolume() {
		return Volume;
	}
	public void setVolume(int volume) {
		Volume = volume;
	}
	
	}
  • 그러나 이 방법 또한 1, 2번과 같이 사용하지 않았을 경우의 메모리 문제를 해결하지 못한 것과 유연성이 떨어진다는 면에서의 한계를 지니고 있다.

출력을 통해 스피커1과 2가 동일한 인스턴스라는 것을 확인할 수 있다!

구현 Main method

public class main {
	public static void main(String[] args) {
		SystemSpeaker speaker1 = SystemSpeaker.getInstance();
		SystemSpeaker speaker2 = SystemSpeaker.getInstance();
		System.out.println(speaker1.getVolume());
		System.out.println(speaker2.getVolume());
		speaker1.setVolume(11);
		System.out.println(speaker1.getVolume());
		System.out.println(speaker2.getVolume());
		speaker2.setVolume(22); 
		System.out.println(speaker1.getVolume());
		System.out.println(speaker2.getVolume());
	}	
}

디버그를 통해 같은 주소값을 확인하여 완벽히 동일한 인스턴스라는것을 확인 할 수있다.

참고 블로그

0개의 댓글