본문 바로가기

Computer Science/Design Pattern

[Design Pattern] 싱글톤 패턴(Singleton Pattern)에 대해서 - 컴도리돌이

728x90

싱글톤 패턴은 하나의 클래스에 대해 단 하나의 인스턴스만 생성하고, 이를 전역적으로 접근할 수 있도록 하는 디자인 패턴이에요. 싱글톤 패턴을 구현하는 방법은 여러 가지가 있지만, 가장 쉬운 방법부터 확인해 볼까요? 


싱글톤 패턴(Singleton Pattern)

다음은 자바에서 기본적인 싱글톤 패턴 구현 방법입니다. 

public class Singleton {
    // static 변수로 인스턴스 선언
    private static Singleton instance;

    // private 생성자, 외부에서 직접 인스턴스를 생성할 수 없음
    private Singleton() {}

    // public static 메서드로 인스턴스에 접근
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    // 싱글톤 클래스의 메서드
    public void doSomething() {
        System.out.println("Singleton instance is doing something.");
    }
}

 

 

getInstance() 메서드는 인스턴스를 생성하거나 반환하는 역할을 하며, 인스턴스가 이미 존재하는 경우에는 새로운 인스턴스를 생성하지 않고 기존 인스턴스를 반환합니다.

 

위와 같이 인스턴스를 오직 한 개로만 가져가면 어떤 이점이 있을까요? 🤔 먼저 떠올 리 수 있는 이점은 메모리 측면이 아닐까요? 😆

애플리케이션의 실행되면 한 번의 생성을 통해서 고정된 메모리 영역을 사용하기 때문에 추후 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있겠죠.

또 다른 이점으로는 클래스 간에 데이터 공유도 쉽게 할 수 있겠네요. 싱글톤 인스턴스는 전역으로 사용되는 인스턴스이기 때문에 다른 클래스의 인스턴스들이 접근하여 사용할 수 있습니다.

 

그렇다면 전역으로 사용되고, 오직 한 개만 존재하는 인스턴스를 여러 곳에서 동시에 접근하게 되면 어떤 문제가 발생할까요? 


싱글톤 패턴의 문제점

싱글톤 패턴은 단일 객체이기 때문에 공유 객체로 사용됩니다. 그러므로 이 또한 싱글톤 패턴의 문제로 이루어져요. 😳

멀티스레드 환경에서 getInstance() 메서드를 동시에 두 개 이상의 스레드가 호출하면, 두 스레드가 모두 'if(instance == null)' 조건을 통과할 수 있겠죠? 그러면 두 개 이상의 인스턴스가 생성될 수 있으며, 이는 당연히 싱글톤 패턴의 의도에 위반하게 됩니다. 

 

동시 접근 문제를 해결하려면 getInstance() 메서드를 동기화를 해야 합니다. 이를 통해 여러 스레드가 동시에 이 메서드에 접근할 수 없도록 하고, 인스턴스가 중복으로 생성되는 것을 방지할 수 있죠. 동시 접근을 해결하기 위한 다음 코드를 한번 보겠습니다 😤

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

    public void doSomething() {
        System.out.println("Singleton instance is doing something.");
    }
}

 

위 코드에서 getInstance() 메서드에 synchronized 키워드를 추가하여, 여러 스레드가 동시에 이 메서드를 호출할 수 없도록 합니다. 이렇게 하면 멀티스레드 환경에서도 싱글톤 클래스의 인스턴스가 하나만 생성되도록 보장할 수 있습니다. 

 

하지만 synchronized를 통해서 getInstance() 메서드를 전체를 동기화하는 것은 성능에 영향을 줄 수 있다고 합니다. 어떠한 성능 이슈가 있을까요?🤔

 

https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/java-intrinsic-locks.html

 

synchronized 블록은 자바에서 성능 오버헤드를 추가합니다. 모든 스레드가 getInstance() 메서드를 호출할 때 동기화 메커니즘에 의해 락이 걸리기 때문에, 여러 스레드가 동시에 이 메서드를 호출하는 경우 성능이 저하될 수 있습니다.  또한 다수의 스레드가 동시에 getInstance()를 호출할 때, 스레드들은 동기화된 블록에 접근하기 위해 대기해야 합니다. 이로 인해 성능 병목이 발생되겠죠

 

실제로 다음 코드를 사용해서 테스트를 해보았습니다. 

 

public class Main {
    private static final int THREAD_COUNT = 1000;
    private static final int ITERATIONS = 100000;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < ITERATIONS; i++) {
                Singleton.getInstance().doSomething();
            }
        };

        Thread[] threads = new Thread[THREAD_COUNT];
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }

        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i].join();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime) + " ms");
        System.out.println("All threads have finished execution.");
    }
}

 

 

스레드의 수를 1,000개로 정의하고, 각 스레드가 getInstance() 메서드를 100,000번 호출하게끔 설정하였습니다. 값을 크게 설정하여 성능 오버헤드를 극대화를 시켜줬죠. 오버헤드란 자바에서 해당 메서드나 블록에 진입할 때와 빠져나올 때 각각의 스레드에 대해 락을 걸고 해제해야 합니다. 이 과정에서 JVM은 락을 관리하는 추가적인 작업을 수행해야 하며, 이로 인해 메서드 자체 호출 속도가 느려지는 거죠. 동기화된 블록에 들어가려는 스레드가 많을수록 이 오버헤드는 커지며, 결국 메서드 실행이 병목 현상을 겪게 됩니다. 

 

또한 대기 중인 스레드들이 계속해서 CPU 사이클을 소모하며 락을 얻으려고 시도하기 때문에, 시스템 자원이 낭비되며 응답 시간이 길어집니다. 아래는 위에 코드가 실행 시간을 나타내며, 41초가 걸렸네요 🥱

 

Time taken: 41698 ms
All threads have finished execution.

 

 


Double-Chekced Locking 

getInstance() 메서드 전체를 동기화하는 것은 성능에 영향을 줄 수 있다고 하니, 이를 개선하기 위해 Double-Checked Locking 방식을 사용하여 성능 개선을 해보겠습니다. 이 방법은 두 번의 검사를 통해 필요할 때만 동기화를 적용하므로 성능을 최적화할 수 있습니다. 

 

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public void doSomething() {
        System.out.println("Singleton instance is doing something.");
    }
}

 

위에 코드를 보면 volatile를 사용했는데, 인스턴스 변수의 가시성을 보장하며 synchronized 블록 안에서 다시 한번 인스턴스가 null인지 확인하여 필요한 경우에만 동기화를 수행합니다. 이를 통해 성능을 최적화하면서도 멀티스레드 환경에서의 안전성을 보장할 수 있죠. 

 

그렇다면 이제 진짜로 성능이 개선되었는지 한번 볼까요?

 

Time taken: 32934 ms
All threads have finished execution.

 

41초에서 32초로 줄어들었네요 😆 멀리스레드 환경에서 싱글톤 패턴을 사용할 때는 동시성 문제를 피하기 위해 getInstance() 메서드를 동기화하거나, Double-Checked Locking을 사용하는 것이 중요합니다. 이를 통해 올바른 싱글톤 인스턴스를 유지하면서도 성능 정하를 최소화할 수 있습니다. 


 

저는 스프링 부트를 사용하니깐 기본적으로 빈이 싱글톤으로 관리되므로, 특별하게 싱글톤 패턴을 직접 구현할 필요는 없었는데, 이번 스터디를 하면서 찍먹정도 해본 거 같아요 😆 


 

싱글톤(Singleton) 패턴이란?

이번 글에서는 디자인 패턴의 종류 중 하나인 싱글톤 패턴에 대해 알아보자. 싱글톤 패턴이 무엇인지, 패턴 구현 시 주의할 점은 무엇인지에 대해 알아보는 것만으로도 많은 도움이 될 것이라

tecoble.techcourse.co.kr

 

 

[자바 성능 튜닝이야기] synchronized는 제대로 알고 써야 한다.

우리가 개발하는 WAS는 여러 개의 스레드가 동작하도록 되어 있다. 그래서 synchronized를 자주 사용한다. 하지만 synchonized를 쓴다고 무조건 안정적인 것은 아니며, 성능에 영향을 미치는 부분도 있

reference-m1.tistory.com

 

[Java] 싱글톤 패턴(Singleton Pattern) - 개념 및 예제

싱글톤 패턴(Singleton Pattern) 싱글톤 패턴은 객체 지향 프로그래밍에서 특정 클래스가 단 하나만의 인스턴스를 생성하여 사용하기 위한 패턴이다. 생성자를 여러 번 호출하더라도 인스턴스가 하

ittrue.tistory.com

 

Java - Intrinsic Locks and Synchronization

Java - Intrinsic Locks and Synchronization [Last Updated: Feb 7, 2017]

www.logicbig.com