상세 컨텐츠

본문 제목

[JAVA] Singleton 패턴에 대해 알아보자

Java

by AyaanDev 2023. 4. 11. 19:54

본문

반응형

서론

Singleton패턴이 무엇이고 언제 쓰이는지 어떻게 구현하는지에 대해 작성하였으며 구현 방식은 Java언어를 통해 작성하였다. HeadFirst DesignPattern 책을 참고하여 작성하였다.

 

목차

1. Singleton 패턴이 무엇인가?

2. 고전적인 Singleton 패턴 구현방법

3. 개선된 Singleton 패턴 구현방법

 

1. Singleton 패턴이 무엇이고 언제 사용되는가?

우리가 생성하는 객체중에서는 프로그램에서 유일하게 만들어져야하는 객체들이 있다. 예를 들어, 스레드 풀, 사용자 설정을 처리하는 객체, 로그 기록용 객체, 프린터나 그래픽 카드 같은 디바이스를 위한 디바이스 드라이버 객체가 이런것이다. 이러한 객체들은 프로그램 내에서 유일해야 하며, 만약 여러개의 객체가 생성된다면 프로그램이 이상하게 돌아간다던가, 자원을 불 필요하게 잡아먹는다던가 하는 문제가 발생할 수 있다.

 

이러한 객체들을 프로그램 내에서 유일하게 하나만 생성할 수 있도록 제한을 두는 패턴을 Singleton패턴이라 부른다.

 

2. 고전적인 Singleton 패턴 구현방법

자바 코드를 읽다보면 종종 생성자에 private 접근제어자가 붙어있는 것을 본적이 있을 것이다. private은 인스턴스 내에서만 호출 할 수 있는 접근 제어자인데, 생성자는 인스턴스를 생성하기 위한 것이니 인스턴스를 생성하지 못하는 것이라 생각할 것이다. 하지만 이는 Singleton패턴을 구현하기위해 자주 사용되는 테크닉이다. 아래 코드를 읽어보자.

public class Singleton{
    private static Singleton uniqueInstance;	// 유일한 인스턴스를 저장하기위한 클래스 변수
    
    private int var1;
    private int var2;
    
    private Singleton(){ // 외부에서 생성자를 호출하지 못하게 하기위해 접근 제어자를 private으로 설정하였다.
    	var1=0;
        var2=0;
    }	
    
    public static Singleton getInstance(){
    	if(uniqueInstance ==null){	// 아직 생성된 인스턴스가 없는지를 확인하고 없다면 인스턴스를 생성한다.
        	uniqueInstance = new Singleton();
        }
        
        return uniqueInstance; // 이미 생성된 인스턴스가 있다면 이미 생성되어있는 인스턴스를 반환한다.
    }

}

instance를 클래스 변수로 선언하였다. 이 참조변수를 반환하는 메서드를 static으로 선언하였고, 만약 인스턴스가 없다면 생성해서 반환하는 것을 확인할 수 있다. 위 코드를 잘 생각해보면, 어떻게 인스턴스가 프로그램 내에서 유일하게 생성되게 하는지를 이해할 수 있을것이다.

 

3. 고전적인 Singleton 패턴의 문제점.

위 코드를 읽고나면 아마 요구사항을 완벽히 충족하였다는 생각이 들것이다. 그리고 고전적이라는 표현을 썼다는 것은 어떠한 문제점이 있어서 고전적이라는 표현을 썼을텐데 어떤 문제점이 있지? 라는 생각을 할것이다. single thread환경에서는 실제로 문제가 일어나지 않는다. 하지만 여러 쓰레드가 동시에 getInstance()메서드에 접근하는 경우를 생각하면 문제가 발생한다.

 

< Thread 1> < Thread 2>
1. getInstance()호출
2. uniqueInstance == null 이 true임을 확인하고 if문으로 들어감.
3. new Singleton() 생성자 호출
4. var1 까지 초기화하고 Context Switch 발생.
 
  5. getInstance()호출
6. uniqueInstance == null 이 true임을 확인하고 if문으로 들어감.
7. new Singleton() 생성자 호출 
8. var1, var2을 초기화하고 생성자가 끝난다.
9. 생성된 인스턴스의 주소값이 uniqueInstance 참조변수에 할당된다.
10. 실행을 계속하다 Context Switch가 발생한다.
11. var2를 마져 초기화 하고 생성자 수행을 마친다.
12. uniqueInstance 참조변수에 새로 생성된 인스턴스의 주소값을 덮어쓴다.
13. 최종적으로 uniqueInstance는 Thread1이 생성한 인스턴스가 되며 Thread2가 생성한 Instance를 가리키는 참조변수는 없다. 
 

 

위의 케이스처럼 동작할 경우 Singleton의 사용이유인 Instance는 하나만 생성된다는 규칙이 깨진다. 멀티 쓰레드 환경에서 Singleton을 고전적인 방식처럼 구현할 경우 예상치 못한 에러가 발생할 수 있다.

 

4. 개선된 Singleton 패턴 구현방법

개선된 Singleton패턴 구현방법에는 몇가지 방법이 있다. 3가지 방법을 알아보고 장단점을 비교해 보자.

 

4 - 1. getInstance메서드에 synchronized지정.

public class Singleton {
	private static Singleton uniqueInstance;
    
    private Singleton(){}
    
    public static synchronized Singleton getInstance(){ //synchronized 설정
    	if( uiqueInstance == null){
        	uniqueInstance=new Singleton();
        }
        
        return uniqueInstance;
    }
    
    ...
}

synchronized키워드를 getInstance에 지정하였으므로 여러 쓰레드가 해당 쓰레드를 동시에 호출하지 못하게 되었다.

따라서 앞서 말했던 멀티 쓰레드 환경에서 발생할 수 있는 문제는 해결되었다.

 

 

단점:

getInstance가 동기화 되어야 하는 시점은 처음 getInstance가 생성되어서 uniqueInstance에 할당되기 전까지이다. 그 이후에는 여러 쓰레드가 getInstance에 동시에 접근하여도 동기화 처리가 될 필요가 없으므로 낭비라고 할 수 있다.

( HeadFirst DesignPattern에 따르면 메서드에 동기화 처리가 들어가면 일반적으로 성능이 100배정도 저하된다고 한다.)

 

 

꼭 안좋은 방식인가?

그렇지는 않다. getInstance() 메서드가 자주 호출되지는 않아서, 혹은 자주 호출되더라도 구현부가 단순해 딱히 병목으로 작용하지 않는다면 getInstance()를 저런형태로 구현하는 것은 좋은 방식이다. 이후에 나오는 DCL보다 훨씬 간단하게 싱글톤을 구현할 수 있고 Class Variable을 사용하는 방식에 비해 메모리 낭비가 일어나지 않는다는 장점이 있다.

 

4 - 2. Class Variable 활용

public class Singleton{
	private static Singleton uniqueInstance = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
    	return uniqueInstance;
    }
}

 

단점 :

Lazy Initialization이 불가능하다. 인스턴스가 꼭 필요한 시점에 instance를 초기화 하는 것이 아닌, 클래스가 로딩되는 시점에 바로 instance가 초기화 되므로 실제 사용하지 않은 시점에도 항상 uniqueInstance가 메모리상에 존재해야하며 이는 메모리 낭비가 될 수 있다. 심지어는 프로그램이 돌아가는 동안 위 객체를 한번도 사용하지 않더라도 객체는 생성된다.또한 테스팅에서 mock객체를 생성하는데 어려움이 생길수 있다고 하는데, 필자는 아직 테스트를 공부해본적이 없어 나중에 더 자세히 알아보고 위 포스트를 수정하겠다.

 

4 - 3. DCL(Double Checking Locking)을 사용하는 방법.

1번 방법의 경우 getInstance 메서드 전체가 동기화 처리가 되어있다. 사실 getInstance 메서드의 동기화 처리는 인스턴스가 아직 생성되기 전까지만 필요한데 전체에 동기화가 걸려있어 불필요한 성능저하를 야기한다는 것이 단점이였다.

이를 해결하기 위해 uniqueInstance == null 일때 만 동기화를 거는 방식이 위 방식이다.

 

public class Singleton {
    private volatile static Singleton uniqueInstance;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
    	if ( uniqueInstnace == null ){
        	synchronized(Singleton.class){
            	if ( uniqueInstance == null ){
                	uniqueInstance = new Singleton();
                }
            }
        }
        
        return uniqueInstance;
    }

}

 

단점: 

위 방식은 1번의 성능 문제를 해결했으며, 2번 처럼 memory leak 문제가 발생하지도 않는다. 하지만, 비교적 구현의 복잡도가 높다는 단점이 있다.

또한 자바 버전이 1.4 이하라면 volatile 키워드를 사용할 수 없어 이 방법을 사용할 수 없다. 하지만 최근에는 자바 1.4버전 이하를 사용하는 경우는 거의 없을 것이므로 크게 문제가 되진 않을 거라 생각한다. 

 

 

반응형

'Java' 카테고리의 다른 글

객체 지향 설계의 5원칙(SOLID)  (0) 2023.05.30
String vs StringBuffer vs StringBuilder  (0) 2023.04.03
동등성(Equality) VS 동일성(Identity)  (0) 2023.02.15

관련글 더보기

댓글 영역