기타/개발일기

[디자인 패턴] Spring Boot로 싱글톤 패턴 알아보기

hu6r1s 2024. 12. 29. 20:41
public class SigletonApplication {

  public static void main(String[] args) {
    Setting setting1 = Setting.getInstance();
    Setting setting2 = Setting.getInstance();
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
  }

}

싱글톤 패턴이 무엇인가?

싱글톤 패턴은 싱글이라는 말대로 인스턴스를 오직 한 개만 제공하는 클래스이다.

시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개일 때 문제가 생길 수 있는 경우가 있다.

그래서 인스턴스를 오직 한 개만 만들어 제공하는 클래스가 필요하다.

예를 들면, 게임의 설정에서 A라는 설정에서는 공격을 스페이스바로 지정하고 B라는 설정에서는 마우스로 공격할 수 있도록 설정하면 헷갈릴 것이다. 그래서 이와 같은 경우는 설정을 하나로 하여 제공할 수 있도록 해야한다.

이와 같은 코드를 실행하면 어떤 결과가 출력될까?

new를 통해 새로운 인스턴스를 생성했기 때문에 이는 각각의 다른 인스턴스로 `true`로 출력이 될 것이다.

그렇다면 싱글톤 패턴을 사용하려면 어떻게 해야 할까?

일단은 `new` 키워드를 사용하면 안된다. 그럼 어떤 방법이 있는지 보자.

싱글톤 패턴 구현

private 생성자 만들기

Setting 클래스에 private한 생성자를 만들어주게 되면 main 메서드에서 생성한 Setting 인스턴스에 컴파일 에러가 발생할 것이다.

public class Setting {

  private Setting() {}
}

 

private로 인해 Setting 클래스 안에서만 설정을 할 수 있고 외부에서는 건드릴 수 없게 된다.

그러면 Setting 클래스 안에서 인스턴스를 만들어 외부로 보내줄 방법을 알아야 한다.

public static 메서드 만들기

public class Setting {

  private Setting() {}

  public static Setting getInstance() {
    return new Setting();
  }
}
public class SigletonApplication {

  public static void main(String[] args) {
    Setting setting1 = Setting.getInstance();
    Setting setting2 = Setting.getInstance();
    System.out.println(setting1 == setting2);
  }

}

 

public static 메서드를 사용하면 Setting 클래스 내부에서 생성자를 만들어 외부에서 사용할 수 있게 된다.

하지만 이렇게 해도 우리가 원하는 값은 인스턴스들이 같아야 한다는 것인데 해결할 수는 없다.

그렇다면 어떻게 해야 할까?

private static 필드 만들기

public class Setting {

  private static Setting INSTANCE;

  private Setting() {}

  public static Setting getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Setting();
    }

    return INSTANCE;
  }
}
public class SigletonApplication {

  public static void main(String[] args) {
    Setting setting1 = Setting.getInstance();
    Setting setting2 = Setting.getInstance();
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
    System.out.println(setting1 == setting2);
  }

}

이렇게 private static으로 필드를 만들고 인스턴스가 null이면 새로 만들어주고 있다면 그냥 반환해주면 된다.

그러면 몇번을 출력해도 같은 인스턴스로 나오게 된다.

이렇게 하면 싱글톤 패턴을 가장 쉽게 구현하는 방법이다.

이렇게 구현한 방법의 문제점

하지만 이렇게 구현하면 심각한 문제점이 있다.

우리가 웹 개발 즉, 애플리케이션을 만들게 되면 멀티스레드 환경일 것이다.

멀티스레드 환경에서는 여러 스레드들이 동시에 접근할 수 있기 때문에 제대로 동작하지 않을 것이다.

멀티스레드 환경에서 위의 코드를 테스트해봤다.

public class SigletonApplication {

  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(10);

    Runnable task = () -> {
      Setting setting = Setting.getInstance();
      System.out.println(setting);
    };

    for (int i = 0; i < 100; i++) {
      executor.submit(task);
    }

    executor.shutdown();
  }

}

 

스레드풀을 10개로 맞추고 인스턴스를 가저와 출력하는 task를 만들었다. 이를 100번으로 출력하고 클래스의 주소값이 모두 동일해야 위의 코드는 제대로 싱글톤 패턴을 구현했다고 볼 수 있다.

이미지를 보면 다른 주소값이 있는 것을 확인할 수 있고 이는 위의 코드는 제대로 된 싱글톤 패턴이 아니라는 뜻이다.

public static Setting getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Setting();
    }

    return INSTANCE;
}

 

왜 그런지 코드적으로 알아보면 멀티 스레드 환경에서 A 스레드와 B 스레드가 있다고 가정해보자.

A 스레드가 if문을 타고 INSTANCE가 null인 것을 확인하고 `INSTANCE = new Setting();`으로 넘어갈려 할 때,

B 스레드가 if문을 사고INSTANCE가 똑같이 null이라고 할 수 있다. 그렇기 때문에 인스턴스는 여러개가 생기게 된다.

그렇다면 멀티스레드 환경에서 싱글톤 패턴을 구현할 수 있는 방법을 알아보자.

멀티스레드 환경에서의 싱글톤 구현

synchronized 키워드

synchronized 키워드를 사용하면 동기화가 되면서 getInstance 메서드에 하나의 스레드만 접근할 수 있도록 할 수 있다.

그렇게 되면 여러 스레드들이 들어오지 못하고 하나의 스레드만 들어와 처리를 하고 끝나면 다른 스레드가 접근할 수 있도록 된다.

public static synchronized Setting getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Setting();
    }

    return INSTANCE;
}

 

하지만 이 synchronized 키워드는 동기화하여 하나의 스레드만 접근하기 때문에 약간의 성능이 저하될 수 있다는 단점을 가지고 있다.

이른 초기화 (eager initialization)

객체 생성의 비용이 그렇게 크지 않다라고 한다면 미리 INSTANCE를 초기화하는 방법도 있다.

public class Setting {

  private static final Setting INSTANCE = new Setting();

  private Setting() {}

  public static synchronized Setting getInstance() {
    return INSTANCE;
  }
}

 

필드를 미리 생성자를 통해 초기화를 해주면 된다.

단점으로는 이 객체가 매우 많은 리소스를 사용한다고 가정하고 이 객체를 사용하지 않는다면?

애플리케이션이 구동되면서 미리 초기화를 했기 때문에 리소스가 차지하게 되고 사용하지 않는다면 이렇게 비효율적인 시스템은 없을 것이다.

double checked locking

이 방법은 이른 초기화를 하기 싫고, synchronized 키워드를 사용하여 성능 저하를 걱정하다면 사용하면 된다.

public class Setting {

  private static volatile Setting INSTANCE;

  private Setting() {}

  public static Setting getInstance() {
    if (INSTANCE == null) {
      synchronized(Setting.class) {
        if (INSTANCE == null) {
          INSTANCE = new Setting();
        }
      }
    }

    return INSTANCE;
  }
}

 

double checked locking이란 말 그대로 더블체크 즉, 두번 확인을 한다는 것이다.

맨 처음 if문에서 한 번 확인하고, synchronized 안에서 다시 한 번 더 확인을 하는 것이다.

예를 들면 A 스레드에서 if문을 확인하고 다음 코드로 넘어가고, 동시에 B 스레드도 if문을 확인하고 다음 코드로 넘어간다.

그 다음부터는 동기화가 되어있기 때문에 A 스레드가 먼저 synchronized에서 if을 확인하고 인스턴스를 생성해준다. 그리고 바로 B 스레드도 확인을 하는데 if에서 걸려서 if문을 나가게 된다.

 

그런데 getInstance에 synchronized를 사용하는 것과 똑같이 synchronized를 사용하는 건데 왜 이게 더 성능이 좋다는 걸까?

위에서 사용한 getInstance에 사용한 것은 해당 메서드를 자체를 동기화하는 것이고, 위의 코드에서는 인스턴스가 없을 때 동기화를 하는 것이다. 평상시에는 그냥 인스턴스를 반환하게 될 것이고, 만약 스레드가 동시에 접근을 하게 된다면 그 때 동기화를 하는 것이기 때문에 synchronized가 메서드에 사용된 것보다 효율이 좋다.

static inner class

이 방법이 가장 권한하는 방법 중 하나이다.

public class Setting {

  private Setting() {}

  private static class SettingHolder {
    private static final Setting INSTANCE = new Setting();
  }

  public static Setting getInstance() {
    return SettingHolder.INSTANCE;
  }
}

 

이렇게 사용하면 eager initialization를 하여 미리 초기화하지 않아도 되고, synchronized를 메서드에 사용하여 성능 저하를 이르키지도 않으며 코드블럭에 사용하여 코드가 길어지고 복잡해지지도 않는다.

Spring Boot에서의 싱글톤 패턴

스프링부트에서는 싱글톤 패턴을 어떻게 사용하고 있을까?

java.lang.Runtime

Runtime 클래스는 실행되고 있는 환경의 정보 등을 알 수 있는 클래스이다.

Runtime runtime = Runtime.getRuntime();

 

Runtime 클래스를 직접 들어가 확인해보면

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    private static Version version;

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class {@code Runtime} are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the {@code Runtime} object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

 

위의 eager initialization 방법을 사용하여 getRuntime 메서드로만 반환할 수 있는 것을 알 수 있다.

스프링 ApplicationContext와 Bean

@Configuration
public class SpringConfig {

  @Bean
  public String hello() {
    return "hello";
  }
}
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
String hello1 = applicationContext.getBean("hello", String.class);
String hello2 = applicationContext.getBean("hello", String.class);

System.out.println(hello1 == hello2);

 

이건 싱글톤 패턴은 아니지만 싱글톤 패턴처럼 유일한 객체를 필요로 할 때, 실무에서 많이 사용되는 방법이다.

이는 싱글톤 스코프로 빈의 기본 스코프이다. ApplicationContext가 생성되면서 종료될 때까지 빈이 존재하게 된다.

Reference

해당 포스팅은 백기선님의 디자인 패턴 강의를 통해 학습하였습니다.

https://www.youtube.com/watch?v=OwOEGhAo3pI&list=PLfI752FpVCS_v_sc8Q6V9QQN7GhoyktKD&index=1