싱글턴 패턴이란?

"클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다."

 

말 그대로 하나의 클래스로 하나의 객체만 만들겠다는 이야기다.

 

이해를 돕기위해 코드를 살펴보자. 아래는 고전적인 싱글턴 패턴을 구현한 것이다.

 

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton(){}

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

    }
}

자 생성자(Singleton)에 주목해보자. 

private으로 선언함으로써 이 클래스 외에서는 접근이 불가능하다. 즉, new Singleton() 으로 객체 생성이 불가능해진다.

 

그래서 getInstance() 메소드를 통해 static 변수 uniqueInstance에 값이 없으면 객체를 생성해주고, 값이 있으면 그 값을 반환하게 끔 해준다. 이러면 단 Singleton 객체는 단 하나만 존재할 수 밖에 없게 되는 것이다.

 

그런데 위 코드는 멀티 스레딩 시에 문제점이 생긴다. if(uniqueInstance == null) 이 부분에서 각각의 스레드가 true 값을 가지기 때문에 두 개의 스레드에서 각각 객체 생성이 일어난다. 즉, 두 번 이상 객체가 생성되어 싱글턴 패턴에 위반하게 된다. 

 

그러면 어떻게 해결할까? 다양한 방법이 있다. 한 번 살펴보자.


프로그램 시작 시, 인스턴스 생성하기

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

    private Singleton(){
    }

    public static Singleton getInstance(){
        return uniqueInstance;
    }
}

- 처음부터 Singleton2 인스턴스를 만들어보자. 정적 초기화 부분에서 싱글턴 패턴을 생성하면서 스레드를 사용해도 아무 문제 없이 작동하는 것을 확인할 수 있을 것이다.

 


DCL (Double Checking Locking)

public class Singleton {

    private volatile static Singleton instance = null;
    
    private Singleton() {

    }
    
    public static Singleton getInstance() {
       if (instance == null) {
          synchronized (Singleton.class){
             if(instance == null) {
                instance = new Singleton();
             }
          }
        }
       return instance;
    }
    
}

- 인스턴스가 생성되어 있는지 확인 후, 생성되어 있지 않았을 때만 동기화를 시킴

- volatile

    -> 변수를 CPU cache에 저장하지 않고 메모리에서 읽고 저장한다.

    -> 스레드를 사용할 때 다른 프로세서에 있는 cache에 변수값이 저장되어 서로 다른 값을 사용하는 것을 방지한다.

- synchronized

     -> 여러 스레드에서 사용하려고 할 때 locking 매커니즘을 제공하여 한 번에 한 개 스레드만 사용할 수 있도록 한다.

 

 


inner static class 사용

 

public class Singleton {
    private static class InnerSingleton{
       static final Singleton instance = new Singleton();
    }
    
    private Singleton() {
       System.out.println("Singleton constructor");
    }
    
    public static Singleton getInstance() {
       return InnerSingleton.instance;
    }
    
    public void print() {
       System.out.println("Singleton instance hashCode = " + InnerSingleton.instance.hashCode());
    }
}

- 내부 정적 클래스를 사용하면 JVM에서는 해당 싱글턴 클래스를 메모리에 load할 때, 정적 멤버 변수가 없으므로 싱글턴 인스턴스를 생성하지 않는다.

 


enum

자 이제까지 싱글턴 패턴의 원리를 알아봤다. 근데 사실 enum 키워드로 모든 것이 해결된다.

public enum Singleton3{
    UNIQUE_INSTANCE;
}

... 

옵저버 패턴 (observer pattern)

정의를 살펴보기 전에 클래스 다이어그램부터 살펴보자!

당연히 이해가 되지 않을 것이다. 

 

옵저버 패턴이란?

 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

 

역시 무슨 말인지 모르겠다.

 

유튜브가 가장 좋은 예시인 것 같다.

우리가 유튜브의 쯔양의 먹방 유튜브 채널을 구독과 알림 설정을 했다고 치자. 그러면 우리는 쯔양이 실시간 방송을 하거나 컨텐츠를 올렸을 때 우리는 알림을 받게 된다. 이게 옵저버 패턴이다. 간단하지 않은가?

 

처음 봤던 클래스 다이어그램을 다시 보며 조금 더 들어가보자.

 쯔양 유튜브 채널은 Subject가 된다. 즉, 구독(registerObserver)을 하거나 구독 취소(unregisterObserver)을 하는 역할을 하고 영상이 올라왔다고 알려준다.(notifyObservers)

 구독자들(Observer)은 그저 구독을 하면 알림이 온다.(update)

ConcreteSubject, ConcreteObserver는 각각 Subject 인터페이스, Observer 인터페이스의 구현이라고 보면 된다.

그래서 저 클래스 다이어그램의 구조가 옵저버 패턴이라고 볼 수 있지.

 

---

(심화) 구현 예제

 이제는 다른 예제로 코드로 살펴보자. 이 예제는 헤드퍼스트 디자인 패턴의 예제를 가져왔다.

 

기상 데이터 중 습도, 온도, 기압의 정보를 가져와서 스마트폰 디스플레이에 띄워본다고 생각하자. 데이터 정보는 WeatherData라는 객체에 담길 것이다.

 

그럼 여기서 Subject는 WeatherData가 될 것이고, Observer는 각각의 디스플레이가 될 것이다. 클래스 다이어그램을 살펴보자.

 모든 클래스를 구현할 것은 아니다. 일단 Subject, Observer 인터페이스 부터 살펴보자.

public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObserver();
}

 

public interface Observer {
    public void update(float temperature, float humidity, float pressure);
}

간단하다. 

Subject의 역할은 Observer의 등록을 받아주거나 삭제하거나 Observer에게 알려주는 것이다.

Observer은 Subject에서 알려준 정보를 update하면 된다.

여기까지만 이해해도 옵저버 패턴에 대해서는 이해한 것이다. Subject, Observer를 구현한 것을 살펴보자.

 

public class WeatherData implements Subject{
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData(){
        observers = new ArrayList<Observer>();
    }
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObserver() {
        for(Observer observer:observers){
            observer.update(temperature,humidity,pressure);
        }
    }

    public void measurementsChanged(){
        notifyObserver();
    }

    public void setMeasurements(float temperature,float humidity, float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

위는 WeatherData는 Subject의 구현부 즉, ConcreteSubject이다. 쯔양 유튜브 채널이라고 생각하면 된다.

온도, 습도, 기압 데이터를 입력 받아 이를 Observer에게 알려준다.

 

 

public class CurrentConditionsDisplay implements Observer,DisplayElement{
    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }
    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    @Override
    public void display() {
        System.out.println("현재 상태: 온도 "+temperature +"F, 습도 "+humidity+"%");
    }
}

위 코드는 Observer의 구현부 즉, ConcreteObserver로 여러 구독자 중 한 명이라고 생각하면 된다.

그저 Subject에서 제공된 데이터를 update하면 된다.

클래스 다이어그램의 모든 것을 구현한 것은 아니다. 옵저버 패턴에 대해 이해할 정도로만 알아두자!

 

정리

 옵저버 패턴은   "한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다." 이제는 이 문장이 조금 이해되었을 것이라고 생각한다.

 

 

+ Recent posts