멀티 쓰레드 환경에서 싱글톤 패턴 구현하기

xeonu·2022년 8월 27일
0

객체지향

목록 보기
1/1

싱글톤 패턴

하나의 인스턴스만 생성하여 사용하는 디자인 패턴

구현

(1번 구현) 기초적인 구현

Setting.java

public class Settings{

  private static Settings instance;

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

    return instance;
  }
}

App.java(main 메소드)

public class App{
  public static void main(Stirng[] args){
    Settings settings = Settings.getInstance();
  }
}

Settings 클래스는 Settings 객체를 맴버변수로 가지고 있고 getInstance() 메소드를 호출하면 객체를 생성한다. 이때 getInstance() 메소드를 실행하기 전까지는 instance는 null이다. main에서 getInstance()를 실행하면 instance의 존재 여부(null 여부)에 따라 new를 통해 Settings를 생성할지 말지 결정한다.

하지만 이러한 구현은 멀티쓰레드 환경에서 싱글톤 패턴을 보장하지 않는다. 두개 이상의 쓰레드가 getInstance() 메소드에 동시에 접근할 때 Settings 객체를 생성하기 전에 두개 이상의 쓰레드가 if문에 진입하면 두개 이상의 객체를 생성하기 때문이다.

(2번 구현) Draconian synchronization

public class Settings{

  private static Settings instance;

// synchronized 키워드 추가
  public static synchronized Settings getInstance(){
    if(instance == null){
      instance == new Settings();
    }

    return instance;
  }
}

App.java(main 메소드)

public class App{
  public static void main(Stirng[] args){
    Settings settings = Settings.getInstance();
  }
}

1번 구현에서 근본적인 문제점은 동시에 여러 쓰레드에서 getInstance() 메소드를 호출해서다. 이런 근본적인 원인을 제거하기 위해 getInstance() 메소드를 synchronized 키워드로 동기화 시키면 동시에 여러 쓰레드가 getInstace() 메소드에 접근하지 못하고 오직 1개의 쓰레드만 작업할 수 있다.(동시간대 1개) 이렇게 구현하면 객체를 생성하기 전에 다른 쓰레드가 if문에 접근할 일도 없을 것이다.

하지만 이 방법은 synchronized 키워드로 동기화 하였기 때문에 멀티쓰레딩의 성능상의 이점을 활용할 수 없다는 것이다.

(3번 구현) 이른 초기화(eager initialization)

public class Settings{
  // 클래스 생성시 instance를 초기화해줌
  private static final Settings INSTANCE = new Settings();
  
  // if문 불필요 
  public static Settings getInstance(){
    return instance;
  }
}

App.java(main 메소드)

public class App{
  public static void main(Stirng[] args){
    Settings settings = Settings.getInstance();
  }
}

getInstance() 메소드에 여러 쓰레드가 동시에 접근하더라도 이미 instance 객체는 Java가 실행될 때 생성되어 있기 때문에 싱글톤을 보장한다.

하지만 instance를 생성하는 과정이 무겁고 오래걸리는 작업이라면 프로그램을 로딩할 때 부담이 될 수도 있고, 해당 인스턴스를 사용하지 않는다면 GC의 대상도 되지 않아 불필요하게 리소스를 잡아먹게 된다.

(4번 구현) double checked locking

public class Settings{

  // volatile 키워드 추가
  private static volatile Settings instance;

  // 생성자를 private로 선언
  private Settings(){

  }

  public static Settings getInstance(){
    // synchronized 키워드를 메소드 내에 블럭으로 추가
    synchronized(Settings.class){
      if(instance == null){
      instance == new Settings();
      }
    }
    
    return instance;
  }
}

App.java(main 메소드)

public class App{
  public static void main(Stirng[] args){
    Settings settings = Settings.getInstance();
  }
}

해당 방법은 synchronized 키워드를 메소드 안에 블럭 형태로 선언했으므로 getInstance() 메소드를 여러 쓰레드가 동시에 접근해도 무방하다. 다만 데이터의 무결성을 보장하기 위해 instance를 꼭 volatile 키워드로 선언해야한다.

위 방법은 volatile 키워드를 사용할 수 있는 Java 1.5 이상의 버전에서만 사용 가능하다. 하지만 이 글을 작성하는 시점에서 Java17 버전까지 출시했으므로 옛 이야기이다. 그 뿐만 아니라 코드가 복잡하므로 가독성이 떨어진다.

(5번 구현) statice inner class

public class Settings{

  private Settings() { }

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

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

객체 선언을 Settings 안에 존재하는 내부 클래스에 static final로 초기에 선언하였고 getInstance() 메소드를 통해 해당 내부클래스의 static 객체를 return한다.

이 방법은 lazy loading도 가능하고 Thread-safe하다. 또한 Double Checked Locking 방식보다 구현도 간단하다. 현재 싱글톤 패턴을 구현할 때 권장되는 방법이다.

총평

싱글톤 패턴은 매번 new 연산을 통해 객체를 생성하지 않아 메모리를 효율적으로 사용할 수 있다. 또한 인스턴스가 static으로 선언되어 있기 때문에 클래스 간에 데이터 공유가 쉽다.

하지만 싱글톤 패턴을 구현하기 위해서 부가적인 코드를 작성해야 하고 멀티쓰레드 환경에서 오류가 발생하지 않게 synchronized를 사용하는 등의 방법으로 개발해야한다.

사실 싱글톤 패턴은 객체지향의 원칙을 위반할 가능성이 매우 높은 디자인 패턴이다. 싱글톤 패턴을 사용할 때는 trade-off를 고려하여 적절하게 사용해야한다.

profile
백엔드 개발자가 되기위한 여정

0개의 댓글