언어(Language)/Java

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

잇트루 2023. 12. 3. 06:20
반응형

싱글톤 패턴(Singleton Pattern)

싱글톤 패턴은 객체 지향 프로그래밍에서 특정 클래스가 단 하나만의 인스턴스를 생성하여 사용하기 위한 패턴이다. 생성자를 여러 번 호출하더라도 인스턴스가 하나만 존재하도록 보장하여 애플리케이션에서 동일한 객체 인스턴스에 접근할 수 있도록 한다.

 

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

커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같은 경우 인스턴스를 여러 개 만들게 되면 불필요한 자원을 사용하게 되고, 프로그램이 예상치 못한 결과를 낳을 수 있다. 따라서 객체를 필요할 때마다 생성하는 것이 아닌 단 한 번만 생성하여 전역에서 이를 공유하고 사용할 수 있게 하기 위해 싱글톤 패턴을 사용한다.

 

 

싱글톤 패턴의 장단점

장점

  • 유일한 인스턴스 : 싱글톤 패턴이 적용된 클래스의 인스턴스는 애플리케이션 전역에서 단 하나만 존재하도록 보장한다. 이는 객체의 일관된 상태를 유지하고 전역에서 접근 가능하도록 한다.
  • 메모리 절약 : 인스턴스가 단 하나뿐이므로 메모리를 절약할 수 있다. 생성자를 여러 번 호출하더라도 새로운 인스턴스를 생성하지 않아 메모리 점유 및 해제에 대한 오버헤드를 줄인다.
  • 지연 초기화 : 인스턴스가 실제로 사용되는 시점에 생성하여 초기 비용을 줄일 수 있다.

 

단점

  • 결합도 증가 : 싱글톤 패턴은 전역에서 접근을 허용하기 때문에 해당 인스턴스에 의존하는 경우 결합도가 증가할 수 있다.
  • 테스트 복잡성 : 싱글톤 패턴은 단 하나의 인스턴스만을 생성하고 자원을 공유하기 때문에 싱글톤 클래스를 의존하는 클래스는 결합도 증가로 인해 테스트가 어려울 수 있다.
  • 상태 관리의 어려움 : 만약, 싱글톤 클래스가 상태를 가지고 있는 경우 전역에서 사용되어 변경될 수 있다. 이로 인해 예상치 못한 동작이 발생할 수 있다.
  • 전역에서 접근 가능 : 애플리케이션 내 어디서든 접근이 가능한 경우, 무분별한 사용을 막기 힘들다. 이로 인해 변경에 대한 복잡성이 증가할 수 있다.

 

싱글톤 패턴을 사용할 때에는 장단점을 고려하여 상황에 맞게 적절히 사용해야 한다. 단 하나의 인스턴스를 생성하여 메모리 효율성을 높일 수 있지만, 그로 인해 따를 수 있는 문제점들이 있기 때문이다.

 

 

싱글톤 패턴의 기본 구현

싱글톤 패턴을 적용할 경우 두 개 이상의 객체는 존재할 수 없다. 이를 구현하기 위해서는 객체 생성을 위한 new 생성자에 제약을 걸어야 하고, 만들어진 단일 객체를 반환할 수 있는 메서드가 필요하다. 따라서 다음 세 가지 조건이 반드시 충족되어야 한다.

  • new 키워드를 사용할 수 없도록 생성자에 private 접근 제어자를 지정해야 한다.
  • 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요하다.
  • 유일한 단일 객체를 참조할 정적 참조 변수가 필요하다.

 

아래 이미지는 싱글톤 패턴의 구조다.

  • 클라이언트는 getInstance() 메서드를 통해 싱글톤 인스턴스를 얻을 수 있다.
  • getInstance() 메서드 내부에는 instance가 null이면 생성하고, null이 아니면 instance를 반환한다.
  • 이로써 단 하나만의 객체를 생성하여 사용할 수 있도록 한다.

 

구현 코드

public class Singleton {
    // 정적 참조 변수(싱글톤 객체를 담을 변수)
    private static Singleton singletonObject;

    // private 생성자
    private Singleton() {
    }
    
    // getInstance()
    public static Singleton getInstance() {
        if (singletonObject == null) {
            singletonObject = new Singleton();
        }
        
        return singletonObject;
    }
}
  • singletonObject는 단일 객체를 저장하기 위한 정적 참조 변수다.
  • 외부에서는 해당 객체를 생성할 수 없도록 생성자를 private로 지정한다.
  • getInstance() 메서드를 통해 인스턴스를 얻을 수 있다.
  • getInstance()를 최초로 실행할 때에만 초기화한다. (지연 초기화)

 

싱글톤 패턴 사용

public class Client {
    public static void main(String[] args) {
        // private 생성자(에러 발생)
        // Singleton singleton = new Singleton();

        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        Singleton instance3 = Singleton.getInstance();

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance3);

        System.out.println(instance1 == instance2);
        System.out.println(instance1 == instance3);
    }
}
// 출력
com.book.objects.designpattern.singleton.Singleton@24d46ca6
com.book.objects.designpattern.singleton.Singleton@24d46ca6
com.book.objects.designpattern.singleton.Singleton@24d46ca6
true
true
  • 싱글톤 객체를 getInstance()를 통해 여러 변수에서 호출하더라도 같은 인스턴스를 참조한다.

 

 

싱글톤 패턴의 주의사항

싱글톤 패턴은 단일 객체이기 때문에 공유 객체로 사용된다. 따라서 상태 값을 가지지 않는 것이 좋다. 단일 객체가 상태 값을 가지는 경우 특정 참조 변수가 상태를 변경했을 때 다른 참조 변수에도 영향을 미치기 때문이다. 상태 값이 아닌 읽기 전용 속성을 가지거나 또 다른 단일 객체를 참조하는 속성을 가지는 경우에는 문제가 되지 않는다.

 

위 코드의 경우 멀티 스레드 환경에서 Thread Safe하지 않는 문제점이 있다. Thread Safe는 여러 스레드가 동시에 접근하는 경우 해당 애플리케이션에 어떠한 문제도 발생하지 않는 것을 의미한다. 즉, 싱글톤 패턴은 여러 스레드가 동시에 접근하는 경우 문제가 발생한다는 것을 의미한다.

두 스레드 A와 B가 존재한다고 가정할 때, 스레드 A가 getInstance() 메서드의 if 검사에 통과하여 싱글톤 객체를 생성하려고 할 때, 스레드 B도 if 검사를 통과할 수 있는 문제가 있다.

public static Singleton getInstance() {
    if (singletonObject == null) {  // (2) 스레드 B 진입 가능
        singletonObject = new Singleton();  // (1) 스레드 A 진입 (객체 생성 X)
    }
    
    return singletonObject;
}

 

실행 테스트

public class Client {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        });

        Thread thread2 = new Thread(() -> {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        });

        Thread thread3 = new Thread(() -> {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }
}
com.book.objects.designpattern.singleton.Singleton@2e58d931
com.book.objects.designpattern.singleton.Singleton@12985f41
com.book.objects.designpattern.singleton.Singleton@12985f41
  • thread1과 thread2가 동시에 실행되면서 싱글톤 객체가 2번 초기화되어 서로 다른 주소를 참조하고 있다.

 

 

Thread Safe한 싱글톤 패턴 구현

Thread Safe한 싱글톤 패턴을 구현하는 방법에는 여러 가지가 있다. 가장 쉬운 방법부터 시작해서 싱글톤 패턴 구현의 권장하는 방법까지 차례대로 알아보자.

 

sychronized 키워드 사용

가장 단순한 방법으로 sychronized 키워드를 통해 getInstance() 메서드에 하나의 스레드만 접근 가능하도록 하는 방법이다.

public class Singleton {
    private static Singleton singletonObject;

    private Singleton() {
    }
    
    public static synchronized Singleton getInstance() {
        if (singletonObject == null) {
            singletonObject = new Singleton();
        }
        
        return singletonObject;
    }
}
  • 단, 여러 스레드가 getInstance() 메서드를 동시에 접근하려 할 때 동기화 작업(Lock) 때문에 성능 저하가 발생할 수 있다.

 

이른 초기화(eager initialization) 사용

만약 싱글톤 객체를 생성하는 비용이 크지 않은 경우 이른 초기화 방법을 적용할 수 있다.

public class Singleton {
    private static final Singleton SINGLETON_OBJECT = new Singleton();

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        return SINGLETON_OBJECT;
    }
}
  • 변수 선언과 동시에 초기화하여 Thread Safe한 싱글톤 패턴을 구현할 수 있다.
  • 부가적으로 final 키워드를 붙여 상수로써 사용하면 전역에서 공유하면서 변하지 않는 속성으로 사용한다.
  • 다만, 미리 생성하는 것 자체가 단점으로 작용할 수 있다.
  • 애플리케이션 실행과 동시에 Singleton 객체가 인스턴스화하여 메모리를 점유하게 된다.
  • 만약, 해당 리소스를 사용하지 않는다면 자원 낭비일 뿐이다.

 

Double checked locking

getInstance() 메서드를 사용할 때마다 동기화 작업을 하는 것이 아닌 초기화 할 때만 동기화 작업을 수행하는 방법으로 volatile 키워드와 더블 체크를 통한 synchronized 키워드를 활용하는 방법이다.

public class Singleton {
    // volatile 키워드 사용
    private static volatile Singleton singletonObject;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singletonObject == null) {
            // if 문 진입 시에만 Singleton 클래스에 대한 동기화 작업 수행
            synchronized (Singleton.class) {
                if (singletonObject == null) {
                    singletonObject = new Singleton();
                }
            }
        }

        return singletonObject;
    }
}
  • 이 방법은 최초 getInstance() 메서드를 호출할 때 두 스레드가 if 문에 진입하더라도, 이후에 진행되는 synchronized 키워드로 인해 두 번째 if 문에서는 lock이 걸려 Thread Safe한 싱글톤 패턴의 구현 방법이다.
  • 매번 getInstance() 메서드를 호출할 때마다 동기화 작업이 걸리는 것이 아닌 최초에 초기화할 때에만 동기화 작업을 수행한다.
  • 다만, 이 방법은 volatile 키워드를 사용하기 위해 JVM 버전이 1.5 이상이어야만 한다.
  • 따라서 JVM에 따라 Thread Safe하지 않을 수 있는 문제점이 있다.

 

Bill Pugh Solution 사용 (권장)

private static inner class를 사용하여 Thread Safe한 싱글톤 패턴을 구현하는 방법이다. JVM의 ClassLoader에 의해서 로드될 때 내부적으로 실행되는 synchronized 키워드를 이용하는 방법이다.

public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton SINGLETON_OBJECT = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.SINGLETON_OBJECT;
    }
}
  • Double checked locking보다 단순하면서 JVM 버전에 제약 없이 지연 초기화와 Thread Safe한 싱글톤 패턴을 구현할 수 있다.
  • 싱글톤 패턴 구현의 권장되는 방법이지만, 이 방법 또한 문제점이 없지 않다.
  • 자바 리플렉션과 직렬화를 통해 싱글톤이 파괴될 수 있다.

 

Enum 사용 (권장)

Enum 클래스는 생성자를 private으로 갖게 만들고, 상수만 갖는 클래스이기 때문에 싱글톤의 성질을 가진다. 이를 이용하여 다음과 같이 싱글톤 객체를 구현할 수 있다.

public enum Singleton {
    SINGLETON_OBJECT
}
  • 지금까지의 노력이 헛수고처럼 느껴질 수 있게 만드는 코드다.
  • 단순한 코드로 싱글톤 패턴을 구현할 수 있다.
  • 다만, Enum 외의 클래스는 상속 불가능한 문제점이 있다.

 

 

정리

  • 싱글톤 패턴은 객체의 인스턴스가 단 하나만을 생성하도록 만드는 디자인 패턴이다.
  • private 생성자를 가져야 하는 특징이 있다.
  • 단일 객체 참조 변수는 static 이어야 하고 getInstance() 메서드를 참조한다.
  • 변경 여지가 있는 상태 값을 가지지 않는 것이 좋다.
  • 멀티 스레드 환경의 경우 Thread Safe한 구현 방법을 고려해야 한다.
  • Thread Safe한 싱글톤 패턴은 Bill Pugh Solution 방법과 Enum 사용을 권장하고 있다.
반응형