본문 바로가기
정리

싱글톤패턴

by dyddyd0 2025. 4. 29.

싱글톤 패턴이란

애플리케이션 전역에서 단 하나의 인스턴스만 생성하고 공유(전역 접근) 하기 위한 디자인 패턴임.

싱글톤 패턴은 “전역에서 하나로 관리해야 하는 객체”를 깔끔하게 구현하고, 자원 낭비를 막으며 일관된 상태를 보장할 수 있다.

 

- 유일성 보장과 공유자원에 적합한 전역 접근

private 생성자로 외부 인스턴스 생성을 막고, static 필드에 자신을 저장한 후,

public 메서드 getInstance()와 같은 메서드로 언제 어디서든 동일한 인스턴스를 반환.

 

- 스레드 안정성 고려

멀티스레드 환경에선 여러 쓰레드가 동시에 getInstance() 메서드를 호출해 인스턴스 중복생성을 막도록,

synchronized, 이른 초기화(Eager Init) 또는 Static Inner Class(Holder) 기법을 사용해 안전하게 구현함.

 

주로

- 애플리케이션 설정(Config) 객체

- 로깅(Log) 객체

- DB 커넥션 풀, 쓰레드풀 등 한곳에 집중 관리해야 하는 공유 자원

 

에 사용함

 

 

 

예를 들어 다음 코드처럼

package single;

public class SingletonNotThreadSafe {
    private static SingletonNotThreadSafe instance;  // 초기값은 null
    private int count = 0;

    private SingletonNotThreadSafe() {
        // private 생성자
    }

    public static SingletonNotThreadSafe getInstance() {
        if (instance == null) {
            // 여러 쓰레드가 이 부분을 동시에 진입하면 instance 가 두 번 이상 생성될 수 있음
            instance = new SingletonNotThreadSafe();
        }
        return instance;
    }
}

인스턴스 생성을 담당하는 클래스

 

실제 싱글톤 인스턴스를 호출하는 클래스

package single;

public class SingletonEx {
    public static void main(String[] args) {

        SingletonNotThreadSafe notSafeSingleton1 = SingletonNotThreadSafe.getInstance();
        SingletonNotThreadSafe notSafeSingleton2 = SingletonNotThreadSafe.getInstance();

        System.out.println(notSafeSingleton1);
        System.out.println(notSafeSingleton2);

    }

}

 

결과를 보면 생성된 인스턴스는 같은 객체를 참조한다.

single.SingletonNotThreadSafe@119d7047
single.SingletonNotThreadSafe@119d7047

 

 

싱글톤 패턴을 사용하면 다음의 장점을 얻을 수 있다.

 

인스턴스 제어

  • 애플리케이션에서 단 하나의 객체만 생성되므로 메모리 낭비를 방지할 수 있다.

전역 접근 지점 제공

  • getInstance()를 통해 어디서나 동일한 객체에 접근할 수 있어, 설정(config), 로거, 커넥션 풀 같은 공용 자원을 관리하기에 편리하다.

초기화 시점 제어

  • 이른 초기화(Eager)나 지연 초기화(Lazy) 방식을 선택해, 애플리케이션 구동 성능이나 자원 사용 타이밍을 조절할 수 있다.

상태 일관성 보장

  • 단일 인스턴스이므로 객체 내부 상태가 분산되지 않고 한곳에 집중되어, 관리·디버깅이 쉬워진다.

 

 

반면 단점으로는

 

전역 상태(Global State)

  • 전역 객체를 쓰면 숨은 의존성이 생기고, 모듈 간 결합도가 높아져 테스트나 리팩토링이 어려워진다.

테스트·모킹 어려움

  • JVM 내에 인스턴스가 하나만 존재하므로, 단위 테스트 시마다 상태를 초기화하기 불편하거나 별도 후킹이 필요하다.

확장성·유연성 저하

  • 상속이나 다형성 활용이 어렵고, DIP(의존성 역전 원칙) 위반 소지가 있어 구조 변경 시 제약이 생긴다.

직렬화·리플렉션 우회 이슈

  • Serializable 직렬화 과정에서 새로운 인스턴스가 만들어지거나, 리플렉션을 통해 private 생성자를 우회해 복수 생성될 수 있다.

멀티스레드 안전성 관리 부담

  • 멀티스레드 환경에서 인스턴스 생성·초기화 시 동기화(synchronized)나 Holder 기법 등을 반드시 적용해야 한다.

 

 

 

 

쓰레드 안정성을 고려한 싱글톤 패턴

위에서 예시로 본 코드는 쓰레드안정성을 고려하지 않은 단순한 싱글톤 예제였다.

 

쓰레드 안정성을 고려하지 않는다면 다음 코드에서

package single;

public class SingletonNotThreadSafe {
    private static SingletonNotThreadSafe instance;  // 초기값은 null
    private int count = 0;

    private SingletonNotThreadSafe() {
        // private 생성자
    }

    public static SingletonNotThreadSafe getInstance() {
        if (instance == null) {
            // 여러 쓰레드가 이 부분을 동시에 진입하면 instance 가 두 번 이상 생성될 수 있음
            instance = new SingletonNotThreadSafe();
        }
        return instance;
    }

}

 

여러 개의 쓰레드에서 동시에 getInstance()를 호출하면,

package single;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.IntStream;

public class SingletonEx {
    public static void main(String[] args) {

        final int THREADS = 500_000;

        // 병렬로 getInstance()만 호출 → 최초 생성이 동시다발적으로 일어나게 함
        Set<SingletonNotThreadSafe> instances =
                Collections.newSetFromMap(new ConcurrentHashMap<>());

        IntStream.range(0, THREADS)
                .parallel()
                .forEach(i ->
                        instances.add(SingletonNotThreadSafe.getInstance())
                );

        System.out.println("Expected unique instances: 1");
        System.out.println("Actual   unique instances: " + instances.size());
    }

}

 

다음과 같이 하나 이상의 인스턴스가 생성된 결과가 나온다.

 

 

 

이처럼 싱글톤 패턴이 깨지게 되는데 이를 방지하기 위해

대표적으로 다음 네 가지 방식으로 싱글톤을 구현해 단일 인스턴스를 보장할 수 있다.

더보기

기존싱글톤

package single;

public class SingletonNotThreadSafe {
    private static SingletonNotThreadSafe instance;  // 초기값은 null

    private SingletonNotThreadSafe() {
    }

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

}

 

synchronized 키워드(Lazy + Synchronized Method) 방식

getInstance() 전역에 synchronized를 걸어두었기 때문에, 여러 스레드 동시 진입 시에도 오직 한 번만 생성됨.

package single;

public class SingletonSync {

    private static SingletonSync instance;

    private SingletonSync() {}

    public static synchronized SingletonSync getInstance() {
        if (instance == null) {
            instance = new SingletonSync();
        }
        return instance;
    }
}

 

 

Eager Initialization 방식

클래스 로딩 시점에 인스턴스를 딱 한 번 생성하므로, 병렬 호출로도 인스턴스 추가 생성이 절대 일어나지 않음.

package single;

public class SingletonEager {

    private static final SingletonEager INSTANCE = new SingletonEager();

    private SingletonEager() {
    }

    public static SingletonEager getInstance() {
        return INSTANCE;
    }
}

 

 

Static Inner Class Holder (Lazy + No Sync Overhead) 방식

Holder 클래스의 초기화 시점(최초 getInstance() 호출)도 JVM 스펙으로 “한 번만” 보장되므로, 병렬 호출로도 인스턴스가 하나임.

package single;

public class SingletonHolder {

    private SingletonHolder() {}

    private static class Holder {
        private static final SingletonHolder INSTANCE = new SingletonHolder();
    }

    public static SingletonHolder getInstance() {
        return Holder.INSTANCE;
    }
}

 

 

enum 싱글톤 방식

자바 언어 차원에서 “한 개의 enum 상수”만 생성되도록 보장하기 때문에, 어떠한 병렬 호출에서도 인스턴스는 1개임.

package single;

public enum SingletonEnum {
    INSTANCE;
    // 메서드, 필드 추가 가능
}

 

 

 

결론 :

싱글톤 패턴은 애플리케이션 전역에서 단 하나의 인스턴스만 생성하고 공유(전역 접근) 하기 위한 디자인 패턴이다.

싱글톤 패턴을 사용하면 메모리, 데이터, 속도 측면에서 이점을 얻지만, 전역 객체로 관리가 어려울 수 있다.

멀티 쓰레드 환경에서 쓰레드 안정성을 고려해서 설계하지 않으면 여러 개의 인스턴스를 생성하게 될 수 있으니 잘 설계하자.

반응형

'정리' 카테고리의 다른 글

스프링의 @Transactional  (0) 2025.05.02
스프링 Bean의 scope  (0) 2025.04.30
자바 동시성이슈  (0) 2025.04.28
동기(Synchronous)와 비동기(Asynchronous), blocking과 nonBlocking  (0) 2025.04.27
RDBMS vs NoSQL  (0) 2025.04.26