Ian's Archive 🏃🏻

Profile

Ian

Ian's Archive

Developer / React, SpringBoot ...

📍 Korea
Github Profile →
Categories
All PostsAlgorithm19C1CI/CD2Cloud3DB1DesignPattern9ELK4Engineering1Front3Gatsby2Git2IDE1JAVA7JPA5Java1Linux8Nginx1PHP2Python1React9Security4SpatialData1Spring26
thumbnail

싱글톤 패턴 정리

DesignPattern
2025.10.16.

1. 싱글톤 패턴

단 하나의 유일한 객체를 만들기 위한 코드 패턴

싱글톤 패턴의 목표는 크게 2가지이다.

  1. 인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요
  2. 만들어진 한개의 인스턴스에 글로벌하게 접근할 수 있는 방법이 필요하다.

2. 싱글톤 패턴을 사용하는 이유

ex) DB커넥션과 Pool을 담당하는 인스턴스, 시스템 전역의 로깅을 담당하는 로거

  • 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개 일 때 문제가 생길 수 있는 객체들을 한 곳에서 제어하기 위해서이다.

-> 따라서 싱글톤 패턴은 2가지 목적이 존재

  • 인스턴스를 오직 1개만 만들어야한다. (한 곳에서만 제어하게)
  • 만든 인스턴스에 글로벌하게 접근하는 방식을 제공해야한다.

1

3. 싱글톤 패턴 구현하는 방법

3.1. private 생성자에 static 메소드

  • 가장 간단하고 보편적인 방법이지만, 수많은 스레드가 동시에 생성 체크(if) 문에 접근 시 객체 생성 체크를 통과하기 때문에, 멀티스레드 환경에서는 안전하지 않은 방법
  • 객체를 호출할 떄 인스턴스를 생성하는 Lazy initialization 방식
  • instance 호출 시 만들어진 instance 가 있다면 반환해주고 없다면 생성
복사
public class SettingsPrivateStatic {
  private static SettingsPrivateStatic instance;

  private SettingsPrivateStatic() {}

  public static SettingsPrivateStatic getInstance() {
    if (instance == null) {
      instance = new SettingsPrivateStatic();
    }
    return instance;
  }
}
다음을 직접 설명해라

1. 생성자를 private으로 만든 이유?  
-> 외부에서의 **객체 생성을 막고, 한 곳에서 instance를 관리**하기 위해서

2. getInstance() 메소드를 static으로 선언한 이유?  
-> 외부에서 글로벌하게 만들어진 **객체에 접근하는 방법**이 필요하기 때문

3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?  
-> 객체 생성 전, 동시에 생성함수에 스레드가 접근하게 되면, 올바른 객체 생성 체크가 이루어지지 않을 수 있기 때문

3.2. Thread safe initialization

  • synchronized 를 사용하여 객체대한 접근에 lock 을 걸어 Thread Safe 하게 만드는 방법
  • lock 을 사용해서 순차접근을 하는 것이기 때문에 성능저하가 일어날 수 있다.
복사
public class SettingsSynchronized {

    private static SettingsSynchronized instance;

    private SettingsSynchronized() {}

    public static synchronized SettingsSynchronized getInstance() {
        if (instance == null) {
            instance = new SettingsSynchronized();
        }
        return instance;
    }
}
다음을 직접 설명해라

1. 자바의 동기화 블럭 처리 방법은?  
-> 여러 스레드가 동시에 하나의 자원에 접글할 때 데이터 일관성과 정합성을 보장하기 위해 사용  
-> synchronized 키워드 사용 (동기화 블럭 또는 메소드 지정), 내부적으로 모니터 락 또는 뮤텍스 개념을 이용

2. getInstance() 메소드 동기화시 사용하는 락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?  
-> 클래스 락이다.
  - static 메소드에 붙으면 (클래스 락)
    - 그 클래스 자체(Class 객체)에 락이 걸린다.
    - 클래스 단위로 단 하나의 락만 존재
    - 같은 객체로 메소드를 여러 스레드가 동시에 호출(어떤 인스턴스로 접근하든)하면 한 번에 한 스레드만 실행 가능하고 다른 스레드는 락이 풀릴 때까지 대기, 다른 객체로 접근하면 동시에 실행 가능 (락이 다르기 때문에)
  - 인스턴스 메소드에 붙으면 (인스턴스 락)
    - 그 메소드를 호출한 객체의 락(this)을 건다.
    - 각 인스턴스마다 락이 따로 존재
    - 모든 인스턴스가 공유하는 클래스 단위 락이기 때문에 한 스레드가 실행 중이면 다른 스레드는 어떤 인스턴스로도 그 static 메소드를 동시에 실행 불가

3.3. 이른 초기화 (eager initialization)

  • 멀티쓰레드환경에서 안전한 싱글톤의 2번째 방법
  • 클래스가 로드되는 시점에 객체가 미리 생성되므로, 별도의 동기화 없이 안전하게 사용할 수 있음
  • 단점: 앱 구동 시점에 객체가 미리 생성되므로, 객체 생성 비용이 큰 경우 불필요하게 리소스를 점유할 수 있음
복사
public class SettingsEagerInitialization {
    private static final SettingsEagerInitialization instance = new SettingsEagerInitialization();

    private SettingsEagerInitialization() {}

    public static SettingsEagerInitialization getInstance() {
        return instance;
    }
}
다음을 직접 설명해 보세요.
1. 이른 초기화가 단점이 될 수도 있는 이유? 
-> 앱 구동 시 미리 만드는데 객체 생성 비용이 큰 경우 너무 많은 리소스 사용

2. 만약에 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?
-> Static block initialization 방법 사용
  - static block : 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행되는 블록
복사
class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static Singleton instance;

    // 생성자를 private로 선언 (외부에서 new 사용 X)
    private Singleton() {}
    
    // static 블록을 이용해 예외 처리
    static {
        try {
            instance = new Singleton();
        } catch (Exception e) {
            throw new RuntimeException("싱글톤 객체 생성 오류");
        }
    }

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

3.4. double checked locking

  • 매번 synchronized 동기화가 문제이니, 최초 초기화 할때만 적용하고 이미 만들어진 인스턴스를 반환할떄는 사용하지 않도록 하는 기법
  • I/O 불일치를 해결하기 위해 volatile 이라는 키워드를 사용해야 하는데, 이 키워드는 JVM에 대한 심층적인 이해가 필요하고, JVM에 따라서 여전히 쓰레드 세이프 하지 않는 경우가 발생하기 때문에 사용 지양
    • volatile 키워드
      • 메모리 가시성과 명령 재정렬 방지를 보장
      • 메모리 가시성
        • 멀티스레드 환경에서 여러 CPU 코어는 성능을 위해서 각각의 쓰레드들은 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 캐시(Cache) 메모리에서 가져오게 된다.
        • 이로 인해 한 스레드가 변경한 변수
        • 의 값이 다른 스레드의 캐시에는 반영되지 않아 서로 다른 값을 볼 수도 있다.
        • 그래서 volatile 키워드를 통해 이 변수는 캐시에서 읽지 말고 메인 메모리에서 읽어오도록 지정해주는 것이다.
      • 명령 재정렬 문제
        • 자바에서 객체 생성 과정은 1. 메모리 공간 할당, 2. 생성자 호출, 3. 변수에 인스턴스 참조 대입
        • 컴파일러나 CPU의 최적화 과정에서 2 -> 3 순서가 바뀌는 경우가 발생
          • 한 스레드가 아직 생성자를 다 수행하지 않은 객체를 다른 스레드에게 참조로 넘기는 경우, 다른 스레드가 불완전한 객체에 접근
        • volatile을 사용하면 이러한 명령어 재배치(reordering)을 금지해 객체가 완전히 초기화된 후에만 다른 스레드가 볼 수 있도록 보장
복사
public class SettingsDoubleCheckSynchronized {
    private static volatile SettingsDoubleCheckSynchronized instance;

    private SettingsDoubleCheckSynchronized() {}

    public static synchronized SettingsDoubleCheckSynchronized getInstance() {
        if (instance == null) {
            synchronized (SettingsDoubleCheckSynchronized.class) {
                if (instance == null) {
                    instance = new SettingsDoubleCheckSynchronized();
                }
            }
        }
        return instance;
    }
}
다음을 직접 설명해 보세요.

1. double check locking이라고 부르는 이유? 
-> 두번의 조건 검사가 있기 때문에
  - 첫번째 if에서 인스턴스가 이미 생성되어 있는지 1차로 검사, 이미 생성된 경우 synchronized 블록에 들어가지 않아도 되므로 성능 향상의 효과
  - 여러 스레드가 동시에 첫번째 if문을 통과할 가능성이 있으므로 동기화 블록 안에서 한번 더 검사하여 실제 인스턴스를 한번만 생성되게 한다.

2. instacne 변수는 어떻게 정의해야 하는가? 그 이유는?
-> volatile 키워드를 사용, 메모리 가시성과 명령 재정렬 방지를 보장하기 위해

3.5. Bill Pugh Solution (LazyHolder)

  • 권장하는 2가지 방법 중 1개
  • 멀티스레드 환경에서도 안전하고, 호출될 때 instance 가 만들어지는 Lazy initialization방식
  • 클래스 안에 내부 클래스를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법
  • static 메소드에서는 static 멤버만을 호출할 수 있기 때문에 내부 클래스를 static으로 설정, 이밖에도 내부 클래스의 치명적인 문제점인 메모리 누수 문제를 해결하기 위하여 내부 클래스를 static으로 설정
복사
public class SettingsStaticInner implements Serializable {
    // 외부에서 new로 생성하지 못하게 생성자를 private로 막는다.
    private SettingsStaticInner() {}

    // 인스턴스를 보관하는 역할을 하는 private static 내부 클래스
    private static class SettingsHolder {
        private static final SettingsStaticInner INSTANCE = new SettingsStaticInner();
    }

    public static SettingsStaticInner getInstance() {
        return SettingsHolder.INSTANCE;
    }
}
  • 외부에서 new로 생성하지 못하게 생성자를 private로 막는다.
  • 인스턴스를 보관하는 역할을 하는 private static 내부 클래스

요 두개가 중요

  1. 우선 내부 클래스를 static으로 선언하기 때문에, 싱글톤 클래스가 초기화 되어도 SettingsHolder 내부 클래스는 메모리에 ㄴ로드되지 않음
  2. 어떤 모듈에서 getInstance() 메서드를 호출할 때, SettingsHolder 내부 클래스의 static 멤버를 가져와 리턴하게 되는데, 이 때 내부 클래스가 한번만 초기화 되면서 싱글톤 객체를 최초로 생성 및 리턴하게 된다
  3. 마지막으로 final로 지정함으로서 다시 값이 할당되지 않도록 방지한다.
다음을 직접 설명해 보세요.

1. 이 방법은 static final를 썼는데도 왜 지연 초기화 (lazy initialization)라고 볼 수 있는가?  
-> static 필드는 클래스가 처음 로딩될 때 정적인 메모리 공간에 만들어지는데, holder가 가지고 있는 클래스가 로딩되는 시점은 getInstance()를 호출할 때 로딩되기 때문에 lazy-initialization이라고 한다.

이 방법은 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점을 지닌다.

  1. 리플렉션

구체적인 클래스 타입을 알지 못해도, 클래스에 대한 메서드, 타입, 변수들을 접근할 수 있도록 해주는 Java API

  • 자바에서 제공하는 리플렉션 API를 사용해서 내부 설정을 변경할 수 있다
  • 리플렉션으로 생성자를 가져와, 접근 제어자에 대한 설정을 변경해 Singleton으로 구현한 객체를 새로운 인스턴스로 생성할 수 있다.

-> 해당 객체의 생성자를 불러오고 → 접근제어를 풀고 → 새로운 인스턴스를 생성하여 싱글톤 객체의 제어에서 벗어날 수 있다.

복사
@Test
@DisplayName("리플렉션을 사용해서 Singleton 깨드리기")
void reflection() throws Exception{

    SettingsStaticInner setting = SettingsStaticInner.getInstance();

    Constructor<SettingsStaticInner> declaredConstructor = SettingsStaticInner.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);

    SettingsStaticInner setting1 = declaredConstructor.newInstance();

    assertThat(setting).isEqualTo(setting1);
}
다음을 직접 설명해 보세요.

1. 리플렉션에 대해 설명하세요.
-> 구체적인 클래스 타입을 알지 못해도, 클래스에 대한 메서드, 타입, 변수들을 접근할 수 있도록 해주는 Java API

2. setAccessible(true)를 사용하는 이유는?→ 필드나 메서드의 접근제어 지시자에 대한 제어를 변경하기 위한 메소드
-> 즉 private으로 제어한 접근 설정에 대해 접근 권한을 주는 행위
  1. 직렬화 및 역직렬화의 사용
  • 객체를 직렬화시킨 후 다시 역직렬화 시켜서 새로운 객체를 만드는 방법
    • 자바에서 외부 파일을 역직렬화 할 시에는, 반드시 생성자를 사용하여 새로운 인스턴스를 만들어 준다.
    • 이러한 설정을 이용해서, Singleton 설정을 깨뜨릴 수 있다.
다음을 직접 설명해 보세요.

1. 자바의 직렬화 & 역직렬화에 대해 설명하세요. 
-> 직렬화(Serialization): 객체를 바이트 스트림으로 변환하여 파일, 네트워크, 데이터베이스 등에 저장하거나 전송할 수 있도록 만드는 과정
-> 역직렬화(Deserialization): 바이트 스트림을 다시 원래 객체로 복원하는 과정

2. SerializableId란 무엇이며 왜 쓰는가?
-> serialVersionUID는 직렬화된 객체의 버전을 관리하는 고유한 ID 값.
-> 객체의 클래스가 변경되었을 때 역직렬화 오류를 방지하기 위해 사용됨.

3. try-resource 블럭에 대해 설명하세요
-> try-with-resources는 AutoCloseable을 구현한 리소스를 자동으로 닫아주는 try문.
-> 자원(File, Socket, Stream 등)을 명시적으로 close() 호출하지 않아도 자동 해제됨.

3.6. Enum 이용

복사
enum SingletonEnum {
    // INSTANCE가 자기 자신(SingletonEnum 타입)의 인스턴스이면서, 동시에 public static final 특성을 갖는 필드
    INSTANCE;

    private final Client dbClient;
	
    // enum 생성자는 JVM에 의해 단 한번만 호출
    SingletonEnum() {
        dbClient = Database.getClient();
    }

    public static SingletonEnum getInstance() {
        return INSTANCE;
    }

    public Client getClient() {
        return dbClient;
    }
}

public class Main {
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.getInstance();
        singleton.getClient();

        Client client2 = SingletonEnum.INSTANCE.getClient();
    }
}
  • 자바에서 가장 간결하고 안전하게 싱글톤을 구현하는 방법은 **Enum(열거 타입)**을 사용하는 것
    • 코드가 매우 간결
    • 리플렉션 공격에 안전 (Enum은 내부적으로 리플렉션을 통한 인스턴스 생성을 막는다)
    • JVM이 Enum 인스턴스의 생성을 단 한 번만 수행하도록 보장 -> 직렬화에 자동으로 대비
    • 스레드 안전성도 완벽하게 보장
  • 단점
    • 싱글톤 클래스를 멀티톤(일반 클래스)로 마이그레이션 할 때 처음부터 코드를 다시짜야 되는 단점이 존재한다. (개발 스펙은 언제어디서 변경 될 수 있기 때문에)
    • 클래스 상속이 필요할 때, enum외의 클래스 상속은 불가능하다.

4. 싱글톤 패턴 복습

최종 정리하자면, 싱글톤 패턴 클래스를 만들기 위해서는 Bill Pugh Solution기법을 사용하거나 Enum으로 만들어 사용

이 둘의 사용 선택은 자신의 싱글톤 클래스의 목적에 맞게 사용한다

  • LasyHolder : 성능이 중요시 되는 환경
  • Enum : 직렬화, 안정성이 중요시 되는 환경

4.1 싱글톤 패턴의 단점

  • 모듈 간 의존성 증가
    • 싱글톤 객체를 여러 모듈이 공유하기 때문에, 인스턴스 변경 시 참조 모듈도 수정 필요
    • 남용 시 클래스 간 결합도가 높아져 패턴 사용이 오히려 문제를 일으킬 수 있음
  • S.O.L.I.D 원칙 위배 가능
    • 단일 책임 원칙(SRP): 하나의 싱글톤이 여러 책임을 가질 수 있음
    • 개방-폐쇄 원칙(OCP): 다른 클래스와 강하게 결합되면 변경 시 영향 범위가 커짐
    • 의존 역전 원칙(DIP): 클라이언트가 추상화가 아닌 구체 클래스에 의존하게 됨
    -> 잘못 쓰면 안티패턴이 될 수 있음
  • 단위 테스트(TDD) 어려움
    • 인스턴스를 전역에서 공유하기 때문에 테스트 간 독립성이 깨질 수 있음
    • 상태 초기화가 필요하며, Mock 객체 생성과 테스트가 어려움

5. 싱글톤 패턴 실무에서는 어떻게 쓰이나

  • 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프
  • 자바 java.lang.Runtime
  • 다른 디자인 패턴 (빌더, 퍼사드, 추상 팩토리 등) 구현체의 일부로 쓰인다

Reference

코딩으로 학습하는 GoF의 디자인 패턴
싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자 - Inpa Dev

Previous Post
Elastic Search Analyzer 정리
Next Post
팩토리 메서드 패턴 정리
Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.