티스토리 뷰

내용 구성

  1. 싱글톤 패턴이란?
  2. 코드로 보는 싱글톤 패턴
  3. 멀티 스레드 환경에서의 문제점
  4. 안전하게 사용하는 싱글톤 패턴

 

참고

 


1. 싱글톤 패턴이란?

길동이는 로그인 기능이 있는 작은 홈페이지를 만들고 테스트를 하고 있다. 인덱스 페이지에서 다크 모드를 설정해본다. 정상적으로 검은색 배경으로 전환되었다. 그러나 로그인 페이지로 이동하니 홈페이지 배경이 밝은 하얀색이다! 다시 접속해본 인덱스 페이지는 여전히 검은색 배경인데... 왜 이런 문제가 발생했을까?

 

길동이는 IndexPage 클래스, LoginPage 클래스, Settings 클래스를 작성했는데, IndexPage와 LoginPage 클래스에서 각각 Settings 인스턴스를 생성해서 사용하고 있기 때문이다.

서로 다른 Settings 인스턴스

즉, 인덱스 페이지에서 다크 모드를 설정한 IndexPage의 Settings 인스턴스와 LoginPage의 Settings 인스턴스는 서로 다른 인스턴스라는 것이다.

 

그렇다면 이것을 해결하는 방법은 무엇일까?

 

바로 싱글톤 패턴이다.

싱글톤 패턴은 하나의 클래스가 딱 한 개의 인스턴스를 가지는 패턴이다.

따라서 인스턴스를 생성하는 데 드는 비용이 줄어든다는 장점이 있다.

 

Settings에서 생성한 하나의 인스턴스

현재는 인덱스 페이지와 로그인 페이지 각각에 서로 다른 Settings 클래스의 인스턴스가 총 2개 존재한다. 싱글톤 패턴을 적용하면, 1개의 Settings 인스턴스를 인덱스 페이지와 로그인 페이지에서 공유하게 된다. 따라서 인덱스 페이지에서 설정한 다크 모드가 로그인 페이지로 이동하더라도, 그대로 설정 상태가 유지될 수 있다.

 

이를 코드로 살펴보자.

 

 

 

2. 코드로 보는 싱글톤 패턴

싱글톤 패턴을 적용한길동이의 홈페이지 코드이다.

 

Settings 클래스에서 자신의 인스턴스 1개를 생성하고, 인덱스 페이지와 로그인 페이지는 getInstance()를 통해 Settings 인스턴스를 가져와서 사용한다. 이때 다른 클래스에서 Settings 클래스의 인스턴스를 생성하지 못하도록 Settings 클래스의 생성자를 private으로 선언한다.

 

static 키워드를 사용하면, 컴파일할 때부터 이미 메모리의 어느 한 공간에 딱 한 개가 올라간다.

 

** private 생성자부터 getInstance()까지가 중요하다. 그 외 코드는 테스트를 위한 부가적인 코드이다.

public class Settings {
    private Settings() {}; // private 생성자 → 외부에서 Settings 객체 생성 불가

    private static Settings instance = null;

    public static Settings getInstance() {
        if (instance == null) {
            // 아직 다른 곳에서 getInstance() 메서드를 실행하지 않았다면
            // Settings 객체를 선언하여 변수에 넣음
            instance = new Settings();
        }

        return instance; // 이미 생성되어 있는 인스턴스를 반환
    }

    private boolean isDarkMode = false;
    private int fontSize = 10;

    public boolean isDarkMode() {
        return isDarkMode;
    }

    public void setDarkMode(boolean darkMode) {
        isDarkMode = darkMode;
    }

    public int getFontSize() {
        return fontSize;
    }

    public void setFontSize(int fontSize) {
        this.fontSize = fontSize;
    }
}

 

IndexPage 클래스에서는 Settings의 getInstance() 메서드를 통해 Settings 인스턴스를 사용한다.

한편, 인덱스 페이지에서는 다크 모드를 설정하고 폰트 크기를 15로 변경한다.

public class IndexPage {

    private Settings settings = Settings.getInstance();

    public void setSettings() {
        settings.setDarkMode(true);
        settings.setFontSize(15);
    }

    public Settings getSettingsInstance() {
        return settings;
    }

    public String settingsToString() {
        return "IndexPage{" +
                "darkMode: " + settings.isDarkMode() + ", " +
                "fontSize: " + settings.getFontSize() +
                "}";
    }

}

 

LoginPage 클래스 역시 Settings의 getInstance() 메서드를 통해 Settings 인스턴스를 사용한다.

LoginPage에서는 다시 다크 모드를 해제하고, 폰트 크기는 8로 변경한다.

public class LoginPage {

    private Settings settings = Settings.getInstance();

    public void setSettings() {
        settings.setDarkMode(false);
        settings.setFontSize(8);
    }

    public Settings getSettingsInstance() {
        return settings;
    }

    public String settingsToString() {
        return "LoginPage{" +
                "darkMode: " + settings.isDarkMode() + ", " +
                "fontSize: " + settings.getFontSize() +
                "}";
    }

}

 

이제 Main 클래스에서 인덱스 페이지에서 설정한 내용이 로그인 페이지에도 반영이 되는지, 그리고 그 반대도 가능한지 확인해 보자!

public class Main {

    public static void main(String[] args) {
        IndexPage indexPage = new IndexPage();
        LoginPage loginPage = new LoginPage();

        System.out.println(indexPage.settingsToString());
        indexPage.setSettings();
        System.out.println(indexPage.settingsToString());

        System.out.println(loginPage.settingsToString());
        loginPage.setSettings();
        System.out.println(loginPage.settingsToString());

        System.out.println(indexPage.settingsToString());

        boolean result = indexPage.getSettingsInstance() == loginPage.getSettingsInstance();
        System.out.println("** indexPage.settings == loginPage.settings => " + result);
    }

}

 

위 코드를 실행한 결과이다. 인덱스에서 다크 모드를 설정하고 폰트 크기를 15로 변경한 내용이 로그인 페이지에도 그대로 적용되고 있다. 반대로 로그인 페이지에서 다크 모드를 해제하고 폰트 크기를 8로 변경한 내용 역시 인덱스 페이지에 반영되고 있음을 확인할 수 있다.

마지막은 인덱스 페이지와 로그인 페이지의 Settings 인스턴스가 서로 동일한 것인지 확인하는 코드이다.

 

 

 

3. 멀티 스레드 환경에서의 문제점

private static Settings instance = null;

public static Settings getInstance() {
    if (instance == null) {
        instance = new Settings();
    }

    return instance;
}

Settings 클래스에서 위 부분이 싱글톤 패턴을 구현하는 기본적인 코드이다.

 

그러나 위 코드를 멀티 스레드 환경에서 사용한다면 원하는 대로 동작하지 않을 수 있다.

 

예를 들어 스레드A와 스레드B가 동시에 getInstance() 메서드를 수행하고 있다고 가정하자.

스레드A는 현재 getInstance()에서 if문의 조건문을 통과해서 instance = new Settings()를 실행하기 직전이고,

스레드B는 현재 if문의 조건문 if (instance == null)을 실행하고 있다.

아직 스레드A가 Settings 객체를 생성하지 않았기 때문에 instance는 여전히 null이므로, 스레드B는 if문을 무사히 통과한다. 이러면 스레드A와 스레드B 모두 instance = new Settings() 코드를 실행하게 되어 2개의 Settings 인스턴스가 생성되고 만다.

 

그러나 다행히도, 이런 멀티 스레드 환경에서 안전하게 싱글톤 패턴을 구현하는 방법이 존재한다.

 

 

 

4. 안전하게 사용하는 싱글톤 패턴

멀티 스레드 환경에서 안전한 싱글톤을 구현하는 방법은 다양하지만, 가장 대표적이고 권장되는 방법 중 하나를 소개한다.

 

바로 중첩 클래스(Lazy Holder)를 사용하는 방식이다.

public class Settings {
    private Settings() {};

    private static class InstanceHolder {
        private static final Settings INSTANCE = new Settings();
    }

    public static Settings getInstance() {
        return InstanceHolder.INSTANCE;
    }
}

getInstance() 메서드가 호출되면, static 멤버 INSTANCE에 Settings 객체가 할당된다. 여기서 핵심은 INSTANCE가 getInstance() 메서드가 호출될 때까지 초기화되지 않는다는 점이다.

 

static 키워드가 사용된 변수나 메서드, 클래스는 컴파일될 때 메모리의 한 공간을 차지하게 된다. 따라서 기존에 작성한 private static Settings instance = null; 코드는 실제로 해당 instance를 사용하든 말든 상관없이 무조건 메모리에 올라갔다.

그러나 Lazy Holder 방식은 Settings 인스턴스를 사용하기 위해 getInstance() 메서드를 실행해야만 INSTANCE에 Settings 객체가 할당된다. 즉, Lazy Holder 방식은 인스턴스가 실제로 필요한 시점에 메모리에 할당되기 때문에 불필요한 자원 할당을 방지할 수 있다는 장점이 있다.

728x90