[디자인 패턴] 싱글톤 패턴

듀듀·2023년 6월 13일
3
post-thumbnail

[싱글톤 패턴]

  • 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴
  • 보통 데이터베이스 연결모듈에 많이 사용한다.

장점

하나의 인스턴스를 기반으로 해당 인스턴스를 다른 모듈들이 공유하여 사용하기 때문에 인스턴스 생성 비용이 줄어든다.
그렇기 때문에 인스턴스 생성에 많은 비용이 드는 I/O바운드 작업 (Network, DB, File System)에 많이 사용한다.

  • 메모리 낭비 방지
  • 싱글톤으로 만들어진 클래스와 다른 클래스의 인스턴스들의 데이터 공유가 쉽다.
  • 인스턴스가 절대적으로 한개만 존재하는 것을 보장하기에 개발 시 실수를 줄일 수 있다.
  • 싱글톤 객체를 사용하지 않는 경우 인스턴스를 생성하지 않는다.
  • 싱글톤을 상속시킬 수 있다.

단점

  • 전역변수보다 사용하기가 불편하다.
  • 싱글톤의 역할이 커질수록 결합도가 높아져 객체 지향 설계 원칙에 어긋날 수 있다.
  • 멀티스레드 환경에서 컨트롤이 어렵다.
  • 객체의 파괴 시점을 컨트롤하기 어려울 수 있다.
  • 의존성이 높아진다.
  • TDD를 할 때 걸림돌이 된다.
    - 단위 테스트를 할때 테스트가 서로 독립적이어야 하고 어떤 순서로든 실행할 수 있어야 하는데 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 독립적인 인스턴스를 만들기가 어렵다.

자바에서 싱글톤 클래스를 만들 때

1.eager init

  • 클래스 내에 전역변수로 instance 변수를 생성하고 private static 을 사용하여 인스턴스화에 상관없이 접근이 가능하면서 동시에 private 접근 제어 키워드를 사용해 instance로 바로 접근 할 수 없도록 한다.
  • 생성자에도 private 접근 제어자를 붙여 다른 클래스에서 새로운 인스턴스를 생성하는것을 방지한다.
  • 오로지 정적 메서드인 getInstance()메서드를 이용해 인스턴스에 접근하도록 하여 유일한 동일 인스턴스를 사용하는 기본 싱글톤 원칙을 지키게 한다.
public class EagerInitialization {

	//private static으로 선언
	private static EagerInitialization instance = new EagerInitialization();
	
	//생성자
	private EagerInitialization() {}
	
	//인스턴스 리턴
	public static EagerInitialization getInstance() {
		return instance;
	}

}

장점

  • static으로 생성된 변수에 싱글톤 객체를 선언했기 때문에 클래스가 로딩 될 때 싱글톤 객체가 생성된다.
  • 클래스 로더에 의해 클래스가 최초 로딩 될 때 객체가 생성됨으로 Thread-safe하다.

단점

  • 싱글톤객체 사용유무와 관계없이 클래스가 로딩되는 시점에 항상 싱글톤 객체가 생성되고, 메모리를 잡고있기 때문에 인스턴스 생성을 필요로 하지 않는 클래스라면 자원낭비다.

2.lazy init, synchronized lazy init

Eager 방식과 반대로 클래스가 로딩되는 시점이 아닌 클래스의 인스턴스가 사용되는 시점에서 싱글톤 인스턴스를 생성한다.
사용시점까지 싱글톤 객체 생성을 미루기 때문에 사용하기 전까지 메모리를 점유하지 않는다.

public class LazyInitialization {

	private static LazyInitialization instance;

	private LazyInitialization(){}
	
	public static LazyInitialization getInstance(){
		if(instance == null){
			instance = new LazyInitialization();
		}
		return instance;
	}
    
    //thread-safe하지 않은 단점을 보완하기 위해 멀티스레드에서 스레드들이 동시접근하는 동시성을 synchronized 키워드를 이용해 해결한다.
    public static synchronized LazyInitialization getInstance(){ 
		if(instance == null){
			instance = new LazyInitialization();
		}
		return instance;
	}

}

인스턴스가 null인 경우에만 새로 선언

장점

  • 싱글톤 객체가 필요할 때 인스턴스를 얻을 수 있다.
  • Eager 방식의 단점을 보완할 수 있다.(메모리 누수 방지)

lazy 단점

  • 만약 multi-thread 환경에서 여러 곳에서 동시에 getInstance() 를 호출할 경우 인스턴스가 두번 생성될 여지가 있다. (multi-thread 환경에서는 싱글톤 철학이 깨질 수 있다.)

synchronized lazy 단점

synchronized 키워드를 사용할 경우 자바 내부적으로 해당 영역이나 메서드를 lock, unlock 처리하기 때문에 내부적으로 많은 cost가 발생한다. 따라서 많은 thread들이 getInstance()를 호출하게 되면 프로그램 전반적인 성능저하가 발생한다.


3.double check locking

많은 스레드들이 동시에 synchronized 처리된 메서드를 접근하면 발생하는 성능저하를 완화하기 위해 사용

인스턴스 생성 여부를 패턴 잠금 전에 한번, 인스턴스 생성 전에 한번, 총 2번 확인하기 때문에 인스턴스가 존재하지 않을 때만 synchronized로 잠금을 건다.

public class ThreadSafeLazyInitialization {

	private volatile ThreadSafeLazyInitialization instance;

	private ThreadSafeLazyInitialization(){}
	
	public static ThreadSafeLazyInitialization getInstance(){
		//Double-checked locking
		if(instance == null){
			synchronized (ThreadSafeLazyInitialization.class) {
				if(instance == null)
					instance = new ThreadSafeLazyInitialization();
			}

		}
		return instance;
	}
}

volatile이란?

CPU 코어는 메모리에서 읽어온 값을 캐시에 저장하고, CPU cache 메모리(L1,L2,L3)에서 작업하다보니 메모리에 저장된 변수의 값이 변경돼도 캐시에 저장된 값이 갱신되지 않아 메모리에 저장된 값과 달라지는 경우가 발생한다. volatile키워드를 통해서 메인메모리영역을 참조하게 함으로서 다른 스레드끼리도 같은 메모리 주소를 참조하게 한다.


4.holder

클래스 안에 Holder클래스를 두어 JVM의 Class Loader 매커니즘과 Class가 로드되는 시점을 이용한 방법이다. Lazy init 방식을 가져가면서 Thread간 동기화 문제를 동시에 해결할 수 있다.

Holder 클래스는 getInstance() 가 호출되기 전에는 참조되지 않고, 최초로 호출 될 때 클래스 로더에 의해 싱글톤 객체를 생성하여 리턴한다.

Holder에 선언된 instancestatic이기 때문에 클래스 로딩 시점에 한번만 호출된다. final을 붙여 값이 다시 할당되지 않도록 한다.

인스턴스가 필요할 때만 정적 멤버로 선언하기에 Static으로 미리 선언해놓는 것의 단점인 메모리 낭비를 방지할 수 있다.

public class InitializationOnDemandHolderIdiom {

	private InitializationOnDemandHolderIdiom(){}
	
	private static class SingleTonHolder{
		private static final InitializationOnDemandHolderIdiom instance = new InitializationOnDemandHolderIdiom();
	}
	
	public static InitializationOnDemandHolderIdiom getInstance(){
		return SingleTonHolder.instance;
	}
}

5.Enum init

enum 타입은 프로그램 내에서 한번 초기화되는 점을 이용해 싱글톤을 구현한다.
enum 자체는 thread-safe 함을 보장한다.

public enum EnumSingleTon {

		INSTANCE;
		public void excute(String arg){
			//...code
		}
}

결론

가장 많이 쓰는 방법 : Holder init
추천 방법 : Holder init, Enum init

profile
나는 내 의지대로 된다.

1개의 댓글

comment-user-thumbnail
2023년 6월 29일

메인 사진이 너무 와닿아서 들어와봤습니다 :D

답글 달기