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

Kyu0·2023년 4월 24일
0

디자인 패턴

목록 보기
1/1

0. 도입📚

이번 작성 카테고리는 디자인 패턴입니다. 그 중에서도 싱글톤 패턴에 대해서 소개하도록 하겠습니다.


1. 싱글톤 패턴이란?🤔

쉽게 말해 런타임 중에 객체의 인스턴스가 오직 하나만 생성되도록 설계하는 디자인 패턴입니다.

출처 : https://refactoring.guru/design-patterns/singleton

객체의 인스턴스가 오직 하나만 있기 때문에 메모리 공간을 절약할 수 있으며 보통 전역 범위에서 싱글톤 객체의 인스턴스를 공유하므로 여러 클래스들이 접근하기 쉽다는 장점이 있습니다.

반대로, 객체 지향의 관점에서 봤을 때 인스턴스가 전역 범위에서 공유되는 싱글톤 패턴에는 여러 단점이 있습니다.

  1. 단일 책임 원칙(Single Responsilibity Principle)
  2. 개방-폐쇄 원칙(Open-Closed Principle)
  3. 리스코프 치환 원칙(Liskov Substitution Principle)
  4. 인터페이스 분리 원칙(Interface Segregation Princple)
  5. 의존성 역전 원칙(Dependency Inversion Principle)

우선, 전역 범위에서 싱글톤 객체가 제공하는 메소드를 사용할 수 있기 때문에 싱글톤 객체를 수정하면 싱글톤 객체를 참조하는 다른 클래스에 대해 영향을 끼칠 수 있습니다. 이는 확장에 대해서는 열려있어야 하고 수정에 대해서는 폐쇄되어야 한다는 개방-폐쇄 원칙을 위배하는 것입니다.

그리고 여러 클래스가 싱글톤 객체의 인스턴스를 직접 참조하기 때문에 인터페이스, 추상 클래스를 의존해야 한다는 의존성 역전 원칙에도 위배됩니다.

객체 지향의 원칙들을 어긴다고 해서 큰 문제가 일어나는 것은 아니지만 유지보수, 확장성이 높은 코드를 작성할 수 있습니다.

추가적인 문제로는, 여러 클래스가 싱글톤 인스턴스에 동시에 접근하게 되면 동기화 문제가 일어날 수 있습니다. 해당 부분은 다음 목차에서 자세하게 설명드리도록 하겠습니다.


2. 싱글톤 객체 작성

본 목차에서는 싱글톤 객체를 작성하는 가장 직관적인 방법부터 단점을 보완하는 방법까지 소개하겠습니다. 해당 부분은 Head First: Design Pattern 교재를 참고했습니다.(교재)

2-1. 고전적인 싱글톤 패턴

public class Singleton {
	private static Singleton instance;
    
	private Singleton() {
    	// do nothing.
	}
    
    public static Singleton getInstance() {
    	if (this.instance == null) {
        	this.instance = new Singleton();
        }
    	return instance;
    }
}

instance 라는 필드에 static 키워드를 붙여 런타임 시에 메모리 공간이 유일함을 보장하고, getInstance() 메소드를 통해 instance 필드가 한 번만 초기화 될 수 있도록 유도했습니다.

하지만 다음 그림을 살펴볼까요?

2개의 쓰레드가 싱글톤 클래스의 getInstance() 메소드에 접근하는 상황입니다. 절묘하게 호출 시간이 맞물려서 Thread A, B 모두 instance 필드가 아직 null이라고 확인하고 새로운 인스턴스를 초기화하는 결과를 볼 수 있습니다.

여기서 Java의 경우 static 영역의 메모리는 가비지 콜렉션의 대상이 아니기 때문에 앞서 생성된 인스턴스가 저장된 메모리 주소인 0x01은 프로그램 종료 시까지 사용하지 못할 수 있습니다.

2-2. Synchronized 키워드 추가

public class Singleton {
	private static Singleton instance;
    
	private Singleton() {
    	// do nothing.
	}
    
    public static synchronized Singleton getInstance() {
    	if (this.instance == null) {
        	this.instance = new Singleton();
        }
    	return instance;
    }
}

위에서 언급한 동시성 문제를 해결하기 위해 getInstance() 메소드에 synchronized 키워드를 추가했습니다. 이로 인해 모든 쓰레드는 getInstance() 메소드에 접근하기 위해 락을 획득해야 합니다.

하지만 동시성 문제가 발생할 때는 처음 getInstance() 메소를 호출할 때 밖에 없습니다. 인스턴스가 초기화 된 이후에는 초기화된 인스턴스를 반환하는 기능만 하기 때문에 여러 쓰레드가 동시에 접근해도 문제가 일어날 여지가 없습니다.

2-3. Eager initialization

public class Singleton {
	private static Singleton instance = new Singleton();
    
    private Singleton() {
    	// do nothing.
    }
    
    public static Singleton getInstance() {
    	return this.instance;
    }
}

싱글톤 객체의 인스턴스 생성 시점을 런타임 중이 아니라 클래스 로드 시로 변경되었습니다. 이로써 단 하나의 인스턴스만을 가지고 있음을 보장할 수 있습니다.

2-4. DCL(Double Checked Locking)

Eager initialization이 아닌 다른 해법을 살펴보도록 하겠습니다. 기존의 synchronized 키워드를 추가한 싱글톤 패턴의 경우 불필요한 경우에도 동기화를 한다는 점 때문에 성능 하락을 야기할 수 있었습니다.

이러한 단점을 임계 영역(Critical Section, 위키백과)을 줄이는 방법(DCL)으로 해결해보도록 하겠습니다.

public class Singleton {
	private static Singleton instance;
    
    private Singleton() {
		// do nothing.
	}
    
    public static Singleton getInstance() {
    	if (this.instance == null) {
        	synchronized (Singleton.class) {
            	if (this.instance == null) {
                	this.instance = new Singleton();
                }
            }
        }
        return this.instance;
    }
}

instance == null 이라면 임계 영역에 대한 Lock을 획득한 상태에서 다시 한 번 instance == null 인지 확인합니다. 처음 확인할 때에는 Lock이 획득되지 않은 상태이기 때문에 다른 쓰레드가 인스턴스를 생성했을 수도 있는 상황이지만 두 번 째 확인을 할 때에는 Lock을 얻었기 때문에 접근하는 쓰레드가 Lock을 얻은 쓰레드 단 하나라는 상태에서 확인을 하게 됩니다.

위와 같은 코드를 작성하게 되면 첫 getInstance() 메소드 호출 이후에는 동기화 과정이 일어나지 않기 때문에 동시성 문제와 성능 하락 문제를 어느 정도 완화할 수 있습니다.

다만, 실제 실행 환경에서는 데이터가 CPU에 캐싱된다는 사실을 고려해야 합니다. 인스턴스를 초기화하더라도 CPU에 instance == null 이라는 정보가 캐싱되어 있다면 여러 쓰레드 간 정보의 동기화가 일어나지 않아 또 새로운 인스턴스를 생성할 수 있습니다.

public class Singleton {
	// volatile 키워드 추가
	private static volatile Singleton instance;
    
    private Singleton() {
    	// do nothing.
    }
    
    public static Singleton getInstance() {
    	if (this.instance == null) {
        	synchronized (Singleton.class) {
            	if (this.instance == null) {
                	this.instance = new Singleton();
                }
            }
        }
        
        return this.instance;
    }
}

volatile 키워드는 쓰기 작업을 완료했을 때는 주 메모리에 바로 갱신하도록 하고, 읽기 작업을 할 때는 캐시 메모리가 아닌 주 메모리에서만 데이터를 가져오도록 하는 키워드입니다. volatile을 적용한 필드에 접근할 때는 해당 데이터의 원천에서 가져오기 때문에 데이터의 동기화 문제를 방지할 수 있습니다.


3. 스프링 빈(Spring Bean)🫘

싱글톤 패턴을 사용하는 주된 예가 바로 스프링 프레임워크의 빈입니다.

스프링은 엔터프라이즈급 어플리케이션 개발을 위한 프레임워크이며 엔터프라이즈급 어플리케이션이란 간단하게 말하자면 대규모의 요청 및 로직을 처리하는 어플리케이션입니다.

다수의 요청을 받을 때마다 새로운 인스턴스를 생성하고 처리하도록 설계하면 그에 따른 메모리 사용량이 높아지기 때문에 싱글톤 패턴을 채택한 것으로 보입니다.

정리하자면, 메모리를 효율적으로 사용하기 위해 스프링 빈은 사용자에게 stateless의 싱글톤 객체를 제공합니다. 싱글톤 객체를 제공하지만 stateless이기 때문에 하나의 인스턴스로도 다수의 요청을 처리할 수 있습니다.

참고 자료


4. 마무리 👏🏻

이번 포스트에서는 디자인 패턴 중 가장 유명한 싱글톤 패턴과 해당 패턴에서 사용되는 개념들인 synchronized, volatile 등에 대한 개념에 대해서도 알아봤습니다. 도움이 되셨길 바랍니다..!

잘못된 내용이나 오타 지적 언제나 환영입니다.

profile
개발자

0개의 댓글

관련 채용 정보