웹사이트 검색

예제가 포함된 Java 싱글톤 디자인 패턴 모범 사례


소개

자바 싱글톤 패턴Gangs of Four Design 패턴 중 하나이며 창조적 디자인 패턴 범주에 속합니다. 정의상으로는 직관적인 디자인 패턴인 것 같지만 막상 구현해보면 고민이 많습니다.

이 기사에서는 싱글톤 디자인 패턴 원칙에 대해 배우고, 싱글톤 디자인 패턴을 구현하는 다양한 방법과 사용에 대한 몇 가지 모범 사례를 살펴봅니다.

싱글톤 패턴 원리

  • 싱글톤 패턴은 클래스의 인스턴스화를 제한하고 Java Virtual Machine에 클래스 인스턴스가 하나만 존재하도록 합니다.
  • 싱글톤 클래스는 클래스의 인스턴스를 가져오기 위해 전역 액세스 지점을 제공해야 합니다.
  • 스레드 풀에 싱글톤 패턴이 사용됩니다.
  • 싱글톤 디자인 패턴은 Facade 등과 같은 다른 디자인 패턴에서도 사용됩니다.
  • 싱글톤 디자인 패턴은 핵심 Java 클래스에서도 사용됩니다(예: java.lang.Runtime, java.awt.Desktop).

Java 싱글톤 패턴 구현

싱글톤 패턴을 구현하기 위해 다양한 접근 방식이 있지만 모두 다음과 같은 공통 개념을 가지고 있습니다.

  • 다른 클래스에서 클래스의 인스턴스화를 제한하는 개인 생성자.
  • 클래스의 유일한 인스턴스인 동일한 클래스의 개인 정적 변수
  • 클래스의 인스턴스를 반환하는 공용 정적 메서드입니다. 이것은 싱글톤 클래스의 인스턴스를 가져오기 위한 외부 세계의 전역 액세스 지점입니다.

추가 섹션에서는 싱글톤 패턴 구현에 대한 다양한 접근 방식과 구현과 관련된 설계 문제에 대해 알아봅니다.

1. 즉시 초기화

즉시 초기화에서 싱글톤 클래스의 인스턴스는 클래스 로딩 시 생성됩니다. 즉시 초기화의 단점은 클라이언트 응용 프로그램이 메서드를 사용하지 않는 경우에도 메서드가 생성된다는 것입니다. 다음은 정적 초기화 싱글톤 클래스의 구현입니다.

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // private constructor to avoid client applications using the constructor
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
}

싱글톤 클래스가 많은 리소스를 사용하지 않는 경우 사용할 수 있는 접근 방식입니다. 그러나 대부분의 시나리오에서 파일 시스템, 데이터베이스 연결 등과 같은 리소스에 대해 싱글톤 클래스가 생성됩니다. 클라이언트가 getInstance 메서드를 호출하지 않는 한 인스턴스화를 피해야 합니다. 또한 이 메서드는 예외 처리 옵션을 제공하지 않습니다.

2. 정적 블록 초기화

예외 처리.

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // static block initialization for exception handling
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

즉시 초기화와 정적 블록 초기화 모두 인스턴스가 사용되기 전에 인스턴스를 생성하므로 사용하기에 가장 좋은 방법은 아닙니다.

3. 지연 초기화

싱글톤 패턴을 구현하기 위한 지연 초기화 방법은 전역 액세스 방법에서 인스턴스를 생성합니다. 다음은 이 접근 방식으로 싱글톤 클래스를 만들기 위한 샘플 코드입니다.

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

이전 구현은 단일 스레드 환경의 경우 잘 작동하지만 다중 스레드 시스템의 경우 여러 스레드가 동시에 if 조건 내에 있으면 문제가 발생할 수 있습니다. 싱글톤 패턴을 파괴하고 두 스레드 모두 싱글톤 클래스의 다른 인스턴스를 얻습니다. 다음 섹션에서는 스레드로부터 안전한 싱글톤 클래스를 만드는 다양한 방법을 살펴보겠습니다.

4. 스레드 안전 싱글톤

스레드로부터 안전한 싱글톤 클래스를 만드는 간단한 방법은 한 번에 하나의 스레드만 이 메서드를 실행할 수 있도록 전역 액세스 메서드를 동기화하는 것입니다. 다음은 이 접근 방식의 일반적인 구현입니다.

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

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

}

이전 구현은 잘 작동하고 스레드 안전성을 제공하지만 동기화된 메서드와 관련된 비용으로 인해 성능이 저하됩니다. 비록 별도의 인스턴스를 생성할 수 있는 처음 몇 개의 스레드에만 필요하지만 동기화된 메서드와 관련된 비용으로 인해 성능이 저하됩니다. 매번 이러한 추가 오버헤드를 방지하기 위해 이중 확인 잠금 원칙이 사용됩니다. 이 접근 방식에서는 if 조건 내에서 동기화된 블록이 싱글톤 클래스의 인스턴스 하나만 생성되도록 추가 검사와 함께 사용됩니다. 다음 코드 스니펫은 이중 확인 잠금 구현을 제공합니다.

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

Thread Safe Singleton Class로 학습을 계속하십시오.

5. Bill Pugh 싱글톤 구현

Java 5 이전에는 Java 메모리 모델에 많은 문제가 있었고 이전 접근 방식은 너무 많은 스레드가 싱글톤 클래스의 인스턴스를 동시에 가져오려고 시도하는 특정 시나리오에서 실패했습니다. 따라서 내부 정적 도우미 클래스입니다. 다음은 Bill Pugh Singleton 구현의 예입니다.

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

싱글톤 클래스의 인스턴스를 포함하는 비공개 내부 정적 클래스에 주목하십시오. 싱글톤 클래스가 로드될 때 SingletonHelper 클래스는 메모리에 로드되지 않고 누군가가 getInstance() 메서드를 호출할 때만 이 클래스가 로드되고 싱글톤 클래스 인스턴스를 생성합니다. 이것은 동기화가 필요하지 않기 때문에 싱글톤 클래스에 가장 널리 사용되는 접근 방식입니다.

6. 리플렉션을 사용하여 싱글톤 패턴 파괴

리플렉션은 이전의 모든 싱글톤 구현 방식을 파괴하는 데 사용할 수 있습니다. 다음은 예제 클래스입니다.

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // This code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

이전 테스트 클래스를 실행하면 두 인스턴스의 hashCode가 싱글톤 패턴을 파괴하는 동일하지 않음을 알 수 있습니다. 리플렉션은 매우 강력하며 Spring 및 Hibernate와 같은 많은 프레임워크에서 사용됩니다. Java Reflection Tutorial로 학습을 계속하십시오.

7. 열거형 싱글톤

Reflection으로 이 상황을 극복하기 위해 Java Enum 값은 전역적으로 액세스 가능하므로 싱글톤도 마찬가지입니다. 단점은 enum 유형이 다소 융통성이 없다는 것입니다(예: 지연 초기화를 허용하지 않음).

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // do something
    }
}

8. 직렬화와 싱글톤

때때로 분산 시스템에서는 파일 시스템에 상태를 저장하고 나중에 검색할 수 있도록 싱글톤 클래스에 Serializable 인터페이스를 구현해야 합니다. 다음은 Serializable 인터페이스도 구현하는 작은 싱글톤 클래스입니다.

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}

직렬화된 싱글톤 클래스의 문제점은 역직렬화할 때마다 클래스의 새 인스턴스가 생성된다는 것입니다. 다음은 예입니다.

package com.journaldev.singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        // deserialize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}

해당 코드는 다음 출력을 생성합니다.

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

따라서 싱글톤 패턴을 파괴합니다. 이 시나리오를 극복하기 위해 우리가 해야 할 일은 readResolve() 메서드의 구현을 제공하는 것뿐입니다.

protected Object readResolve() {
    return getInstance();
}

그런 다음 테스트 프로그램에서 두 인스턴스의 hashCode가 동일하다는 것을 알 수 있습니다.

Java 역직렬화에 대해 읽어보십시오.

결론

이 기사에서는 싱글톤 디자인 패턴에 대해 설명했습니다.

더 많은 Java 자습서로 학습을 계속하십시오.