• Home
  • About
    • 2daistheday's Tech Blog photo

      2dayistheday

      Today is the day.

    • Learn More
    • Email
    • Facebook
    • LinkedIn
    • Instagram
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

싱글턴 패턴

03 Sep 2017

Reading time ~4 minutes

싱글턴 패턴

: 세상에서 단 하나뿐인 특별한 객체

  • 들어가기 전 질문

    • 하나만 쓰는 경우가 많은가요?
      스레드 풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정을 처리하는 객체, 로그 기록용 객체, 프린터나 그래픽 카드 디바이스 드라이버 등등
    • 이 클래스는 객체를 하나만 가짐으로 생각하면 되잖아요?
      오랫동안 검증받는 방법으로 쓰는 패턴이란다
    • 스테틱(static)으로 선언하면 되지 않을까요?
      정적변수로 만들 경우의 여러 단점을 감수하지 않고 객체 인스턴스를 어디서나 엑세스 할 수 있다.
    • 전역 변수의 단점은?
      애플리케이션이 시작될 때 객체가 생성될 것이야. 객체가 자원을 많이 차지한다면 자원의 낭비이겠지. 싱글턴은 패턴을 사용하면 필요할 때만 객체를 만들 수 있단다.
    • 간단해 보이는데?
      싱글턴 코드 자체는 쉬워보이지만 제대로 만들고 이해하는 것은 만만치 않은 일이란다. 만약 어떻게 하면 한 클래스의 인스턴스가 두 개 이상 만들어지지 않도록 한다고 생각해보렴. 쉽지 않을걸?!
    public MyClass{
      private MyClass() {}
      public static MyClass getInstance(){
        return new MyClass();
      }
    }
    
  • 이제 MyClass가 인스턴스가 하나만 만들어지도록 구현해볼까요?

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

: 싱글턴 패턴의 고전적인 구현법을 배웠습니다.

  • 싱글턴 패턴
    싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴입니다.
  • 초콜릿 공장
    보일러가 비어 있을 경우에만 재료를 넣고 끓이기 시작합니다. 초콜릿이 다 끓여지면 다시 보일러를 비웁니다. 보일러가 가득 차 있는 상태에서 새로운 원료를 붓는다거나 빈 보일러에 불을 지피지 않도록 합니다.
    public class ChocolateBoiler {
    	private boolean empty;
    	private boolean boiled;
      	
    	public ChocolateBoiler() {
    		empty = true;
    		boiled = false;
    	}//보일러가 비어있을 때만 돌아갑니다.
    	public void fill() {
    		if(isEmpty()) {
    			empty = false;
    			boiled = false;
    		}
    	}//보일러에 우유/초콜렛을 혼합한 재료를 집어넣
    	public void drain() {
    		if(!isEmpty() && isBoiled()) {
    			empty = true;
    		}
    	}//끓인 재료를 다음 단계로 넘
    	public void boil() {
    		if(!isEmpty() && !isBoiled()) {
    			boiled = true;
    		}
    	}
    	public boolean isEmpty() {
    		return empty;
    	}
    	public boolean isBoiled() {
    		return boiled;
    	}
    }
    
  • 초콜릿보일러 클래스를 싱글턴 패턴으로 바꿔보세요.

  • 문제가 있습니다. 보일러에 있는 fill()메소드에서 아직 초콜릿이 끓고 있는데 재료를 집어넣었습니다.
    uniqueInstance가 있는 초콜릿 보일러에 두 개의 스레드가 접근한다고 했을 때 생기는 문제에 대해 생각해봅시다. 1번 스레드 - 초콜릿 보일러에서 인스턴스를 가져옴 2번 스레드 - 초콜릿 보일러에서 인스턴스를 가져옴 1번 스레드 - if문 통과 유니크 인스턴스가 널 2번 스레드 - if문 통과 유니크 인스턴스가 널 1번 스레드 - 새로운 보일러 생성 2번 스레드 - 새로운 보일러 생성 … 두 스레드가 서로다른 인스턴스를 두 개 생성했습니다.
  • 멀티스레딩 문제 해결 방법
    sycnonized 로 동기화를 해줍니다.
    public class ChocolateBoiler {
    	private boolean empty;
    	private boolean boiled;
      	
    	private static ChocolateBoiler uniqueInstance;
      	
    	private ChocolateBoiler() {
    		empty = true;
    		boiled = false;
    	}//보일러가 비어있을 때만 돌아갑니다.
      	
    	public static synchronized ChocolateBoiler getInstance() {
    		if(uniqueInstance == null) {
    			uniqueInstance = new ChocolateBoiler();
    		}
    		return uniqueInstance;
    	}
      	
    	public void fill() {
    		if(isEmpty()) {
    			empty = false;
    			boiled = false;
    		}
    	}//보일러에 우유/초콜렛을 혼합한 재료를 집어넣
    	public void drain() {
    		if(!isEmpty() && isBoiled()) {
    			empty = true;
    		}
    	}//끓인 재료를 다음 단계로 넘
    	public void boil() {
    		if(!isEmpty() && !isBoiled()) {
    			boiled = true;
    		}
    	}
    	public boolean isEmpty() {
    		return empty;
    	}
    	public boolean isBoiled() {
    		return boiled;
    	}
    }
    
  • 인스턴스를 매번 부를 때마다 동기화를 하네요? 조금 오버가 아닐까요? 인스턴스를 생성할 때만 동기화 하고 싶은데,..(참고로 메소드를 동기화 하면 성능이 100배 정도 저하된다고 합니다;)

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

    : 이 접근법을 사용하면 클래스가 로딩될 때 JVM에서 Singleton의 유일한 인스턴스를 생성해줍니다. JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없습니다.

  • “DCL(Double-Checking Locking)”을 써서 getInstance()에서 동기화되는 부분을 줄입시다.
    DCL을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있습니다. 이렇게하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 됩니다.
    public class Singleton {
    	private volatile static Singleton uniqueInstance;
    	//volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance변수가 Singleton인스턴스로 초기화되는 과전이 올바르게 진해되도록 합니다.
    	private Singleton() {}
    	public static Singleton getInstance() {
    		if(uniqueInstance == null) {//인스턴스가 있는지 확인하고 없으면 동기화된 블럭으로 들어갑니다.
    			synchronized (Singleton.class) {
    				if(uniqueInstance == null) {
    					uniqueInstance = new Singleton();
    				}
    			}
    		}
    		return uniqueInstance;
    	}
    }
    

    : volatile 선언은 외부요인에 의해 변수의 값이 바뀔 수 있음을 나타냅니다. volatile변수를 참조할 때는 레지스터에 로드되어 있는 값을 부르지 않고 매번 메모리를 참조한다.

  • 싱글턴 클래스는 서브클래스를 못만듭니다.
    서브클래스에서는 기본적으로 상위클래스의 생성자를 부릅니다. 생성자가 private이기 때문에 안되겠죠? 그리고 접근지정자를 바꾸면서 서브클래스를 바꾼다면? 같은 하나의 인스턴스를 여러 서브클래스가 공유하게 됩니다. 그리 좋지 않습니다.


JAVA디자인패턴HeadFirst Share Tweet +1