ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [헤드 퍼스트 디자인 패턴] 5장 하나뿐인 특별한 객체 만들기: 싱글턴 패턴
    Book/헤드 퍼스트 디자인 패턴 2023. 5. 13. 00:14

    5장. 싱글턴 패턴

    본격적으로 들어가기 전에, 객체 인스턴스를 1개만 만드는 이유와 싱글턴 패턴의 사용 이유에 대해서 알아보자.

     

    인스턴스를 하나만 만드는 경우는 무엇이 있을까?

    하나만 있어도 잘 돌아가는 객체는 많다. 스레드 풀, 캐시, 대화 상자, 사용자 설정, 레지스트리 설정을 처리하는 객체, 로그 기록용 객체, 디바이스 드라이버 등이 그 예가 될 수 있다. 만일 이런 객체를 사용할 때 인스턴스가 2개 이상이면 프로그램이 이상하게 돌아간다든가, 자원을 불필요하게 잡아먹는다든가, 결과에 일관성이 없어진다든가 하는 심각한 문제가 발생할 수 있다. 

     

    그런데 우리는 여기서 '이러한 객체의 인스턴스를 만들 때 꼭 싱글턴 패턴을 사용해야 하는가?' 라는 질문이 생길 수 있다. 전역변수를 사용하거나 관행을 정해두는 방법도 있기는 하지만, 싱글턴 패턴을 사용하면 전역변수를 사용할 때의 단점을 감수할 필요가 없다.

     

    그러면 전역변수에 객체를 대입하면 생기는 단점이 무엇일까? 전역변수에 객체를 대입해 인스턴스를 생성하면, 그 객체는 애플리케이션이 시작할 때 생성이 된다. 그런데 그 객체가 자원을 많이 차지한다고 생각해보자. 만일 그 객체를 사용하는 태스크가 한번도 발생하지 않으면 괜히 자원만 잡아먹는 쓸데없는 객체가 될 것이다.

     

    그래서 싱글턴 패턴을 사용하면 필요할 때에만 객체를 만들 수 있다.

     

    리틀 싱글턴 패턴 알아보기

    • 1개의 개체를 만들려면 어떻게 해야할까?
    new MyObject();

    다른 객체에서 MyObject 만들려면 어떻게 해야할까? new MyObject();를 다시 사용하면 된다. 그런데 만약 생성자가 다음과 같이 된다면 어떻게 해아할까?

    public MyClass{
    	private MyClass(){}
    }

    생성자가 private으로 선언되어 있기 때문에 인스턴스를 만들 수는 없는 클래스이다. 

    만일 코드가 더 확장되어서 다음과 같아진다면? 

    public MyClass{
    	private MyClass() {}
        public static MyClass getInstance(){
        	return new MyClass();
        }
    }

    이렇게 된다면 다음과 같은 코드로 MyClass의 인스턴스를 만들 수 있다.

    MyClass.getInstance();

     

    여기서 인스턴스를 하나만 만들도록 하는 코드를 고전적인 싱글턴 패턴을 통해 마무리 해보자.

    고전적인 싱글턴 패턴 구현법

    public class SingleTon{
    	private static Singleton uniqueInstance; //Singleton 클래스의 하나뿐인 인스턴스를 저장하는 정적변수
        
        //기타 인스턴스 변수
       	private Singleton(){//생성자를 private으로 선언했으므로 Singleton에서만 클래스의 인스턴스를 만들 수 있다.)
        
        public static Singleton getInstance(){
        	if(uniqueInstance == null){
            	uniqueInstance = new Singleton();
            }
            return uniqueInstance;
            //getInstance() 메소드는 클래스의 인스턴스를 만들어서 리턴한다.
       }
       //기타 메소드
    }

    싱글턴 패턴 특징 정리하기

    앞서 살펴본 것을 통해 우리는 싱글턴 패턴에는 public으로 지정된 생성자가 없다. getInstance()라는 정적 메소드가 잇다. 그 메소드를 호출하면 인스턴스를 달라고 호출이 되는 것이다. 

    초콜릿 보일러 코드 살펴보기 

    아래에는 최신형 초콜릿 보일러를 제어하는 클래스이다. 코드를 잘 보면 실수를 하지 않도록 세심한 주의를 기울였음을 알 수 있다.

    public class ChocolateBoiler {
    	private boolean empty;
        private boolean boiled;
        
        private ChocolateBoiler(){
        	empty = true;
            boiled = false;
            //이 코드는 보일러가 비어있을 때만 돌아간다.
        }
        
        public void fill(){
        	if(isEmpty()){
            //보일러가 비어 있을 때만 재료를 넣는다. 원료를 가득 채우고 나면 empty와 boiled flag를 false로 설정한다.
            	empty = false;
                boiled = false;
            }
        }
        
        public void drain(){
        	if(!isEmpty() && isBoiled()){
            	//보일러가 가득 차있고, 다 끌여진 상태에서만 보일러에 들어있는 재료를 다음 단계로 넘긴다. 보일러를 다 비우고 나면 empty플래그를 true로 다시 설정한다.
                //끓인 재료를 다음 단계로 넘김
                empty = true;
            }
        }
        public void boil(){
        	if(!isEmpty && !isBoiled()){
            //보일러가 가득 차 있고, 아직 끓지 않은 상태에서만 초콜릿과 우유가 혼합된 재료를 끓인다. 재료를 다 끓이면 boiled 플래그를 true로 설정한다
            	//재료를 끓임
            	boild = true;
            }
        }
        public boolean isEmpty(){
        	return empty;
        }
        public boolean isBoiled(){
        	return boiled();
        }
    }

    싱글턴 패턴의 정의

    지금까지 싱글턴의 고전적인 구현법을 배웠다.

     

    싱글턴 패턴의 정의는 다음과 같다.

    싱글턴 패턴(Singleton Pattern)은 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

    자세히 집고 넘어가보자.

    • 싱글턴 패턴을 실제로 적용할 때에는 클래스에서 하나뿐인 인스턴스를 관리하도록 만들면 된다. 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하게 해야한다. 인스턴스가 필요하다면 반드시 클래스 자신을 거치도록 해야한다.
    • 어디서든 그 인스턴스에 접근할 수 있도록 전역 접근 지점을 제공한다. 언제든 이 인스턴스가 필요하면 클래스에 요청할 수 있게 만들어 놓고, 그 요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 하는 지점이 필요하다. 

    싱글턴 패턴을 이런 특징으로 자원을 많이 잡아먹는 인스턴스가 있다면 꽤 유용하게 사용될 수 있다.

     

    싱글턴 패턴의 클래스 다이어그램

    uniqueInstance 클래스 변수에 싱글턴의 하나뿐인 인스턴스가 저장된다.

    Singleton
    static uniqueInstance
    //기타 데이터
    static getInstance()
    //기타 메소드

    getInstance() 메소드는 정적 메소드, 즉 클래스 메소드이다. Singleton.getInstance()라는 코드만 사용하면 언제 어디서든 이 메소드를 호출할 수 있다. 전역 변수에 접근하는 것 만큼이나 쉬우면서도 게으른 인스턴스를 생성할 수 있다는 장점이 있다.

     

    싱글턴 패턴을 사용할 떄 여기 있는 Singleton 클래스처럼 간단하게 만들어야 하는 것은 아니다. 그냥 일반적인 클래스를 만들 때와 마찬가지로 다양한 데이터와 메소드를 사용할 수 있다.

    멀티스레딩 문제 살펴보기

    2개의 스레드에서 아래의 코드를 실행한다고 가정해보자.

    Chocolate boiler = ChocolateBoiler.getInstance();
    boiler.fill();
    boiler.boil();
    boiler.drain();

     

    JVM 입장에서 두 스레드가 다른 보일러 객체를 사용하게 될 가능성은 없는 지 살펴보자. 

    스레드가 다음과 같이 문제가 생길 수 있다.

    멀티스레딩 문제 해결하기

    getInstance()를 동기화 하면 멀티스레딩과 관련된 무넺가 간단하게 해결된다.

    public class SingleTon{
    	private static Singleton uniqueInstance; //Singleton 클래스의 하나뿐인 인스턴스를 저장하는 정적변수
        
        //기타 인스턴스 변수
       	private Singleton(){//생성자를 private으로 선언했으므로 Singleton에서만 클래스의 인스턴스를 만들 수 있다.)
        
        public static sychronized Singleton getInstance(){
        //synchronized 키워드르리 사용하면 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려야한다. 즉 2개의 스레드가 이 메소드를 동시에 실행하는 일은 일어나지 않는다.
        	if(uniqueInstance == null){
            	uniqueInstance = new Singleton();
            }
            return uniqueInstance;
            //getInstance() 메소드는 클래스의 인스턴스를 만들어서 리턴한다.
       }
       //기타 메소드
    }

    이렇게 하면 해결해 줄 수 있지만 또 우리는 속도 문제를 걱정할 수 있다. 동기화를 하게 되면 속도 문제가 생길 수 있기 때문이다.

     

    그런데 사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때뿐이다. 다시 말하자면,  uniqueInstance 변수에 Singleton 인스턴스를 대입하면 굳이 이 메소드를 동기화된 상태로 유지할 필요가 없다. 그래서 처음을 제외하면 동기화는 불필요한 오버헤드만 증가시킬 뿐이다.

    더 효율적으로 멀티스레딩 문제 해결하기

    방법 1. getInstance()의 속도가 그리 중요하지 않다면 그냥 둔다.

    방법 2. 인스턴스가 필요할 때는 생성하지 말고 처음부터 만든다.

    애플리케이션에서 Singletono의 인스턴스를 생성하고 계속 사용하거나 인스턴스를 실행 중에 수시로 만들고 관리하기가 성가시다면 다음과 같은 방식으로 Singleton 인스턴스를 만들면 좋다.

    public class Singleton {
    	private static Singleton uniqueInstance = new Singleton();
    		//정적 초기화 부분(static initializer) Singleton의 인스턴스를 생성한다. 이러면 스레드를 써도 별문제가 없다
    	private Singleton() {}
    	public static Singleton getInstance) {
    		return uniqueInstance; //인스턴스는 이미 있으니까 리턴만 하면 된다.
        }
    }

    이런 방법을 사용하면 클래스가 로딩될 때 JVM에서 Singleton의 하나뿐인 인스턴스를 생성해 준다. JVM에서 하나뿐인 인스턴스를 생성하기 전까지 그 어떤 스레드도  uniqueInstance 정적 변수에 접근할 수 없다.

    방법 3. 'DCL'을 써서 getInstance()에서 동기화 되는 부분을 줄인다.

    DCL은 자바 1.4 이전 버전에서는 쓸 수 없다.

    DCL(Double Checked Locking)을 사용하면 인스턴스가 생성되어 있는지 확인한 다음에 생성되어 있지 않았을 때만 동기화할 수 있다.

    이러면 처음에만 동기화하고 나중에는 동기화 하지 않아도 된다. 

    public class Singleton {
    	private volatile static Singleton uniqueInstance;
    	private Singleton() {}
    	public static Singleton getInstance) {
    		if (uniqueInstance == null) { //인스턴스가 있는지 확인하고, 없으면 동기화된 블록으로 들어간다.
    			synchronized (Singleton.class) {
    				if (uniqueInstance = null) { // 이러면 처음에만 동기화된다.
    					uniqueInstance = new Singleton(); //블록에서도 다시 한 번 변수가 null인지 확인한 다음 인스턴스를 생성한다.
    			}
    		return uniqueInstance;
        }
    }      
    /* volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance 변수가 Singleton 인스턴스로 초기화되는 과정이 올바르게 진행된다. */

    싱글턴이 만날 수 있는 문제들

    여러가지 문제점이 만들어질 수 있지만, 책에서 내가 이해되는 문제 내용만 담아보겠다.

     

    첫번째로, 클래스 로더가 여러 개라면 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있다. 

    두번째, 싱글턴은 느슨한 결합 원칙에 위배되지 않는다. Singleton에 의존하는 객체는 전부 하나의 객체에 단단하게 결합된다. 

     

    위의 문제들을 해결하는 방법

    이 외에도 앞서 살펴본 동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화와 역직렬화 문제 등은 enum으로 싱글ㅇ턴을 생성해서 해결 할 수 있다.

    public enum Singleton {
    	UNIQUE_INSTANCE;
    	//기타 필요한 필드
    }
    
    public class SingletonClient {
    	public static void main(String[] args) (
    		Singleton singleton = Singleton.UNIQUE_INSTANCE;
    		//여기서 싱글턴 사용
        }
    }

    이렇게 해결하면 된다. 앞으로 싱글턴이 필요할 때에면 바로 enum을 사용하면 된다. 

     

    이상으로 싱글턴 패턴에 대한 글을 마친다.

Designed by Tistory.