본문 바로가기
디자인 패턴

[Design Pattern] 싱글톤 패턴

by 감자b 2025. 5. 30.

싱글톤 패턴이란?

싱글톤(Singleton) 패턴은 객체를 단 하나만 생성하고, 어디서든 그 객체를 공유해서 쓸 수 있도록 해주는 디자인 패턴이다.

쉽게 말해, 매번 새로 객체를 만들지 않고 이미 만들어진 객체를 재사용하는 방식이라고 생각하면 된다.

 

이 패턴은 특정 클래스의 인스턴스가 반드시 하나만 존재해야 하는 상황에서 사용된다.

애플리케이션을 개발하다 보면, 어떤 객체는 여러 개가 필요 없고 딱 하나만 있으면 되는 경우가 있는데 예로 다음과 같은 상황이 있다.

  • 로거 : 로그를 찍기 위해 매번 새로운 로거를 만들 필요 없이, 하나의 로거를 계속 사용해도 충분하다.
  • 환경 설정 : 시스템 전반에서 사용하는 설정 정보는 하나만 존재해도 된다.
  • 데이터베이스 연결 : 데이터베이스 연결은 무거운 작업이다. 이미 만들어진 연결 객체를 공유해서 쓰면, 성능도 좋고, 상태도 일관되게 유지할 수 있다.

위 상황에서 싱글톤 패턴을 사용하면 다음과 같은 이점을 얻을 수 있다.

  • 메모리 낭비 방지: 매번 새로 객체를 만들지 않아, 불필요한 메모리 사용을 줄일 수 있다.
  • 상태 일관성 유지: 같은 객체를 공유하므로, 여러 인스턴스에서 상태가 달라지는 문제를 방지한다.
  • 성능 향상: 무거운 객체를 반복적으로 만드는 데 드는 성능 오버헤드를 줄인다.

싱글톤 패턴 구현 방법

싱글톤 패턴의 핵심은 단 하나의 인스턴스를 만드는 것이며, 구체적으로는 아래와 같은 방식으로 동작한다.

1.  클래스 내부에 어디서든 접근할 수 있는 유일한 인스턴스를 전역변수로 생성

2. 외부에서 new Logger()를 사용하여 무분별한 객체 생성을 막기 위해 private 생성자를 정의
3. 유일한 인스턴스를 반환하는 정적 메서드(getInstance())를  구현

public class Logger {
    private static Logger logger; // (1)

    private Logger() { // (2)
        System.out.println("생성자 호출");
    }

    // (3)
    public static Logger getInstance() {
        if (logger == null) {
            logger = new Logger();
        }
        return logger;
    }

    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}
public class LoggerMain {
    public static void main(String[] args) {
        Logger logger1 = Logger.getInstance();
        Logger logger2 = Logger.getInstance();
        Logger logger3 = Logger.getInstance();

        System.out.println(logger1);
        System.out.println(logger2);
        System.out.println(logger3);
    }
}

출력

생성자 호출

study.Logger@33c7353a

study.Logger@33c7353a

study.Logger@33c7353a

 

모두 같은 인스턴스가 반환되는 것을 확인할 수 있다.


주의사항

위 코드를 보면 별 문제가 없는 것처럼 보이지만 치명적인 문제점이 존재한다.

바로 멀티스레드 상황에서는 싱글톤이 보장되지 않는다는 것이다.

public class LoggerMain {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            Logger logger = Logger.getInstance();
            logger.log("Logger 인스턴스: " + logger);
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        Thread t3 = new Thread(task);
        Thread t4 = new Thread(task);

        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(10);
        t4.start();
    }
}

출력

생성자 호출

생성자 호출

생성자 호출

[LOG] Logger 인스턴스: study.Logger@6c76dc2f
[LOG] Logger 인스턴스: study.Logger@6c76dc2f
[LOG] Logger 인스턴스: study.Logger@1ca081e4
[LOG] Logger 인스턴스: study.Logger@3f3b085a

 

위 코드에선 4개의 스레드가 동시에 getInstance() 메서드를 호출하였을 때의 상황이다. 출력을 보면 싱글톤으로 구현하였지만 몇몇 스레드에서 다른 인스턴스가 반환된 것을 확인할 수 있다.

이는 처음에 instance가 존재하지 않는 상황에서 여러 스레드가 동시에 logger = new Logger() 초기화 코드를 실행하였기 때문에 발생한다.

따라서 멀티스레드 환경에서 싱글톤을 보장하기 위해선 동기화가 필요하다.

 

[자바] 동기화 락

공유 자원 → 여러 스레드가 접근할 수 있는 자원대표적으로 인스턴스의 필드(멤버 변수)에 여러 스레드가 접근할 수 있는데 이를 공유 자원이라 하고 이 때 공유 자원에 대한 접근을 적절하게

hbb-devlog.tistory.com

public class Logger {
    private static Logger logger; // (1)

    private Logger() { // (2)
        System.out.println("생성자 호출");
    }

    // (3) synchronized 동기화 적용
    public synchronized static Logger getInstance() {
        if (logger == null) {
            logger = new Logger();
        }
        return logger;
    }

    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

 

이렇게 하면 멀티스레드 환경에서 안전하지만 getInstance() 메서드 전체를 동기화하므로 성능이 저하된다.


LazyHolder 방식

public class Logger {
    private Logger() {
        System.out.println("생성자 호출");
    }

    private static class LoggerHolder {
        private static final Logger LOGGER = new Logger();
    }

    public static Logger getInstance() {
        return LoggerHolder.LOGGER;
    }

    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

위 코드는 싱글톤을 구현하는 방법 중 하나인 LazyHolder 기법으로 동기화를 이용하지 않았음에도 멀티스레드 환경에서 안전하게 동작하는데, 이유가 무엇일까?

 

이는 JVM의 클래스 로딩 특성과 관련이 있다.

LoggerHolder는 정적 내부 클래스이고 정적 내부 클래스는 사용될 때까지 JVM에 로드되지 않는다. (Lazy Loading)

이는 getInstance()가 호출될 때  정적 내부 클래스(LoggerHolder)의 static final 변수를 호출하고 이 때 Logger 내부의 Holder 클래스가 JVM에 로드가 된다는 이야기다.

클래스가 JVM 메모리에 로드되었다는 것은 초기화가 되었다는 것이고 클래스 초기화는 JVM이 한 번만 수행한다.

따라서 동기화 코드 없이도 멀티스레드 환경에서 안전하게 싱글톤 보장이 된다.

 

정리하면 싱글톤 패턴은 주로 데이터베이스 연결, 설정 객체, 로그 처리처럼 리소스를 많이 소모하는 무거운 객체를 하나만 생성해 프로그램 전체에서 공유해야 할 때 유용하다.

하지만 전역 상태를 공유하므로, 프로그램이 커질수록, 서로 다른 컴포넌트가 하나의 싱글톤 객체를 동시에 사용하게 되면서 코드 간 결합도가 높아져 유지보수가 어려워질 수 있고, 테스트 환경에서 다른 인스턴스로 대체하거나 Mocking 하는 것이 힘들어진다.

또한, 멀티스레드 환경에서는 동시성 문제가 발생할 수 있으므로 주의해서 사용해야 한다.