본문 바로가기
IT/Java

[Java] Java Singleton Pattern

by kyu-nahc 2024. 9. 14.
반응형

 

Singleton Pattern

싱글톤 패턴(Singleton Pattern)은 객체 지향 프로그래밍에서,

특정 클래스가 단 하나만의 인스턴스를 생성하여 사용하기 위한 패턴이다.

생성자를 여러 번 호출하더라도 인스턴스가 하나만 존재하도록 보장하여

애플리케이션에서 동일한 객체 인스턴스에 접근할 수 있도록 한다.

싱글톤 패턴을 사용하는 이유에 대해 살펴보면

커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같은 경우 인스턴스를 여러 개 만들게 되면

불필요한 자원을 사용하게 되고, 프로그램이 예상치 못한 결과를 낳을 수 있다.

따라서 객체를 필요할 때마다 생성하는 것이 아닌 단 한 번만 생성하여 전역에서

이를 공유하고 사용할 수 있게 하기 위해 싱글톤 패턴을 사용한다.

 

Singleton Pattern 구조

싱글톤 패턴을 적용할 경우 두 개 이상의 객체는 존재할 수 없다.

이를 구현하기 위해서는 객체 생성을 위한 new 생성자에 제약을 걸어야 하고,

만들어진 단일 객체를 반환할 수 있는 메서드가 필요하다.

따라서 다음 세 가지 조건이 반드시 충족되어야 한다.

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

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

 

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

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {
        System.out.println("Singleton Object Created");
    }

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

public class Client {

    public static void main(String[] args) {

        Singleton singleton = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        Singleton singleton3 = Singleton.getInstance();

        System.out.println(singleton.hashCode());
        System.out.println(singleton2.hashCode());
        System.out.println(singleton3.hashCode());
    }
}

 

위의 객체의 hashCode를 이용하여 비교해 보면 객체들이 모두 동등하다는 것을 볼 수 있다.

싱글톤 패턴은 단일 객체이기 때문에 공유 객체로 사용된다. 따라서 상태 값을 가지지 않는 것이 좋다.

단일 객체가 상태 값을 가지는 경우 특정 참조 변수가 상태를 변경했을 때,

다른 참조 변수에도 영향을 미치기 때문이다. 상태 값이 아닌 읽기 전용 속성을 가지거나

또 다른 단일 객체를 참조하는 속성을 가지는 경우에는 문제가 되지 않는다.

 

하지만 위 코드의 경우 멀티 스레드 환경에서 Thread Safe 하지 않는 문제점이 있다.

Thread Safe는 여러 스레드가 동시에 접근하는 경우에도

해당 애플리케이션에 어떠한 문제도 발생하지 않는 것을 의미한다.

즉, 싱글톤 패턴은 여러 스레드가 동시에 접근하는 경우 문제가 발생한다는 것을 의미한다.

public class Client {

    public static void main(String[] args) {

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

 

위의 결과와 다르게 Thread를 여러 개 생성하여 getIntance()를 호출하면,

Singleton 객체가 여러 개 생성되는 것을 볼 수 있다.

만약 DB 커넥션을 다음과 같이 2개를 생성하게 된다면, 트랜잭션에서 문제가 생길 수 있다.

이를 Thread safe 환경으로 변경하는 방법을 살펴보면 다음과 같다.

 

Synchronized 동기화 사용

// Synchronized를 통해 동기화 진행
public static synchronized Singleton getInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

 

Synchronized 동기화를 사용한다.

맨 처음 Thread가 접근하여 해당 로직을 실행하는 도중에

다른 Thread가 접속할 경우 메서드 접근을 Block 하고,

접근을 시도하는 Thread는 로직을 실행하는 Thread가 메서드의 return 구문이 완료될 때까지 기다린다.

대기하고 있는 Thread는 로직을 실행하는 Thread가 완료되어야만 해당 메서드를 접근할 수 있다.

이처럼 Sychronized 키워드를 통해 getInstance() 메서드에

하나의 스레드만 접근 가능하도록 하는 방법이 있다.

하지만 이는 Block 한다는 점에서 동시에 접근하는 Thread가 많을 때,

동기화 작업 (Block)에 의해 성능이 떨어질 수 있다는 단점이 존재한다.

 

Eager Initialization 방법

따라서 Eager Initialization 방법을 이용할 수 있다.

만약 싱글톤 객체를 생성하는 비용이 크지 않은 경우 이른 초기화 방법을 적용할 수 있다.
변수 선언과 동시에 초기화하여 Thread Safe 한 싱글톤 패턴을 구현할 수 있다.

public class Singleton {

    private static final Singleton uniqueInstance = new Singleton();

    private Singleton() {
        System.out.println("Singleton Object Created");
    }

    public static synchronized Singleton getInstance() {
        return uniqueInstance;
    }
}

 

부가적으로 final 키워드를 붙여 상수로써 사용하여 불변 객체로 사용할 수 있으며,

static으로 선언하므로 Memory Data Section에 바로 들어가게 된다.

따라서 전역에서 공유하면서 변하지 않는 속성으로 사용할 수 있다.
다만, 미리 생성하는 것 자체가 단점으로 작용할 수 있다.
애플리케이션 실행과 동시에 Singleton 객체가 인스턴스화하여 메모리를 점유하게 된다.
만약, 해당 리소스를 사용하지 않는다면 자원 낭비로 이어진다.

 

 

Volatile / Synchronized 키워드

다음으로 getInstance() 메서드를 사용할 때마다 동기화 작업을 하는 것이 아닌 

초기화할 때만 동기화 작업을 수행하는 방법으로 volatile 키워드와 

더블 체크를 통한 synchronized 키워드를 활용하는 방법이다.

public class Singleton {

    private static volatile Singleton uniqueInstance;

    private Singleton() {
        System.out.println("Singleton Object Created");
    }

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

 

이 방법은 최초 getInstance() 메서드를 호출할 때 두 스레드가 if 문에 진입하더라도, 

이후에 진행되는 synchronized 키워드로 인해 두 번째 if 문에서는 lock이 걸려 

Thread Safe 한 싱글톤 패턴의 구현 방법이다.
즉, 매번 getInstance() 메서드를 호출할 때마다 동기화 작업이 걸리는 것이 아닌 

최초에 초기화할 때에만 동기화 작업을 수행한다.

즉 메서드 전체에 동기화시키는 것이 아닌 Object가 null일 경우에만 동기화하도록 수정하는 것이다.

부분적 동기화를 통해 시간 및 성능 면에서 개선된 방법이라고 할 수 있다.
다만, 이 방법은 volatile 키워드를 사용하기 위해 JVM 버전이 1.5 이상이어야만 한다.
따라서 JVM에 따라 Thread Safe 하지 않을 수 있는 문제점이 있다.

 

 

Static inner class

private static inner class를 사용하여 Thread Safe 한 싱글톤 패턴을 구현하는 방법이다. 

JVM의 ClassLoader에 의해서 로드될 때,

내부적으로 실행되는 synchronized 키워드를 이용하는 방법이다.
volatile를 사용하는 동기화 방법보다 단순하면서 JVM 버전에 제약 없이

지연 초기화와 Thread Safe 한 싱글톤 패턴을 구현할 수 있다.

public class Singleton {

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

    public static Singleton getInstance(){
        return SingletonHolder.uniqueInstance;
    }
}

 

객체 생성 시간이 Eager Initialization과 조금 다르다.

Eager Initialization애플리케이션 실행과 동시에 생성되어 메모리를 점유하고 있다는 단점이 있다.

하지만 inner static class에서는 맨 초기에 getInstance()를 호출할 때 Singleton 객체가 생성되어

메모리를 점유하게 된다. 마찬가지로 static과 final 키워드를 통해

Data Section에 들어가며, 불변하지 않는 전역 객체로 사용할 수 있다.

즉 Eager Initialization의 단점을 보완하면서 장점은 가지고 있는 방법이다.

Class를 로딩하고 초기화하는 시점은 thread-safe를 보장하기 때문에,

volatile이나 synchronized 같은 키워드가 없어도 thread-safe 하면서 성능도 보장하는 방법이다.

 

Singleton Pattern 특징

패턴 장점

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

패턴 단점

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

Singleton Pattern 적용 예시

Singleton Pattern은 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등의

상황에 사용할 수 있으며 이미 많이 실무에서 사용된다.

또한 Spring framework에서도 스프링 컨테이너의 Bean 객체 또한 싱글톤 스코프(Scope)로 생성된다.

싱글톤 스코프를 통해 프로그램에서 해당 Bean의 인스턴스를 오직 하나만 생성해서 재사용하는 것이다.

이를 통해 대규모 트래픽을 처리할 수 있도록 하기 위함이다.

스프링은 최초에 설계될 때부터 대규모의 엔터프라이즈 환경에서 요청을 처리할 수 있도록 고안되었다.

그리고 그에 따라 계층적으로 처리 구조가 나뉘게 되었다.

매번 클라이언트에서 요청이 올 때마다 각 로직을 처리하는 빈을 새로 만든다면 

무수히 많은 빈 객체가 생성되게 된다. 이를 방지하기 위해 싱글톤 스코프로 생성하는 것이다.

이처럼 Singleton Pattern은 이미 Spring framework에서 사용되고 있으며,

DB 커넥션 풀을 위해서도 많이 사용되고 있다.

Singleton Pattern를 정리해 보면 다음과 같다.

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

 

    •  

참고자료

https://ittrue.tistory.com/563

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Iterator Pattern  (0) 2024.09.15
[Java] Java Adapter Pattern  (1) 2024.09.15
[Java] Java Command Pattern  (0) 2024.09.13
[Java] Java Builder Pattern  (2) 2024.09.12
[Java] Java Factory Pattern  (1) 2024.09.11

loading