[GoF 디자인 패턴] 싱글톤 (Singleton) 패턴

JMM·2025년 1월 3일
0

GoF 디자인 패턴

목록 보기
1/11
post-thumbnail

싱글톤 패턴 : 인스턴스를 오직 한 개만 제공하는 클래스

  • 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개일 때 문제가 생길 수 있는 경우가 있다. 인스턴스를 오직 한 개만 만들어 제공하는 클래스가 필요하다.

싱글톤 패턴 구현 방법 1

private 생성자에 static 메서드

public class Settings1 {

        private static Settings1 instance;

        private Settings1() { }
        public static Settings1 getInstance() {

            if (instance == null) {
                instance = new Settings1();
            }
            return instance;
        }
}

1. 생성자를 private으로 만든 이유

  • 싱글톤 패턴 구현을 위해서

    • 클래스의 인스턴스 생성을 외부에서 하지 못하도록 제한한다.
    • 이는 클래스 내부에서 인스턴스를 관리하고 단일 인스턴스만 생성되도록 보장한다.
    • 외부에서 new Settings() 호출을 방지함으로써 인스턴스가 여러 개 생성되는 문제를 막는다.

2. getInstance() 메소드를 static으로 선언한 이유?

  • 클래스 레벨에서 접근 가능하도록 하기 위해서

    • 인스턴스를 얻기 위해 객체 생성 없이도 호출할 수 있도록 하기 위해 static으로 선언한다.
    • getInstance() 메소드는 인스턴스를 리턴하는 역할을 하며, 객체가 생성되지 않은 상태에서도 호출 가능해야 한다.
    • ex) Settings1.getInstance()와 같이 사용할 수 있도록 한다.

3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?

인스턴스 생성시 동시성 문제

 if (instance == null) {
    instance = new Settings1();
 }
  • 멀티쓰레드 환경에서 여러 쓰레드가 동시에 이 메소드를 호출하면, 조건문 if (instance == null)를 동시에 통과할 수 있다.
  • 결과적으로, 여러 쓰레드가 동시에 새로운 Settings1 객체를 생성하게 되어 싱글톤 패턴이 깨질 위험이 존재한다.

싱글톤 패턴 구현 방법 2

동기화(synchronized)를 사용해 멀티쓰레드 환경에 안전하게 만드는 방법

/**
 * synchronized 사용해서 동기화 처리
 */
public class Settings2 {

    private static Settings2 instance;

    private Settings2() { }

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

        return instance;
    }

}

1. 자바의 동기화 블럭 처리 방법은?

자바에서 동기화를 위해 synchronized 키워드를 사용한다. 동기화는 멀티쓰레드 환경에서 공유 자원의 상태를 보호하기 위해 사용된다.

여기서는 메소드 동기화 방법을 사용하였다.

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

메서드 동기화

  • synchronized 키워드를 메소드 선언에 붙인다.
  • 해당 메소드에 동시에 접근할 수 있는 쓰레드는 하나로 제한된다.

2. getInstance() 메소드 동기화 시 사용하는 락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?

1) 사용하는 락은 "클래스의 락"이다.

  • getInstance() 메소드는 static 메소드이다.
  • synchronized 키워드가 static 메소드에 사용되면, 락은 해당 클래스의 Class 객체에 사용된다.
  • 즉, Settings2.class를 기준으로 동기화가 이루어진다.

2) 이유

  • 인스턴스의 락은 객체(instance) 단위로 동기화된다. 하지만, getInstance는 클래스 단위에서 호출되며, 락 생성 이전에도 접근 가능하다.
  • 따라서 클래스 수준의 동기화가 필요하며, 이를 위해 클래스 자체의 락(Settings2.class)이 사용된다.

+) 단점: 동기화로 인해 성능 저하 가능.

싱글톤 패턴 구현 방법 3

이른 초기화 (eager initialization)을 사용하는 방법

private static final Settings INSTANCE = new Settings();

private Settings() {}

public static Settings getInstance() {

return INSTANCE;

}

1) 이른 초기화(Eager Initialization)가 단점이 될 수 있는 이유

이른 초기화 방식은 클래스가 로드되는 시점에서 싱글톤 인스턴스를 생성한다. 이러한 방식에는 몇 가지 단점이 존재한다.

  • 리소스 낭비
    싱글톤 인스턴스가 사용되지 않더라도 클래스가 로드되기만 하면 객체가 생성된다.
    예를 들어, 인스턴스가 무겁거나 초기화 과정에서 많은 리소스를 사용하는 경우, 사용되지 않을 가능성이 있는 객체를 미리 생성하는 것은 비효율적이다.

  • 유연성 부족
    이른 초기화 방식은 싱글톤 인스턴스를 초기화할 때 외부적인 조건(환경 변수, 설정 파일 등)에 따라 동적으로 초기화 로직을 변경하기 어렵다. 예를 들어, 인스턴스 생성 시점에서 설정값에 따라 초기화가 달라져야 하는 경우 문제가 될 수 있다.

  • 초기화 시점의 제어 부족
    클래스가 로드될 때 즉시 초기화되므로, 초기화 타이밍을 더 세밀히 제어해야 하는 요구사항에 대응하기 어렵다.
    특히, 특정 조건이 충족되기 전에는 객체를 생성하지 않아야 하는 경우 적합하지 않다.

2) 생성자에서 Checked 예외를 던질 경우 이 코드를 변경하는 방법

싱글톤 패턴에서 private 생성자가 Checked 예외를 던진다면, 이 코드를 변경하려면 Lazy Initialization 방식으로 전환해야 한다. 이 방식에서는 인스턴스가 필요할 때만 초기화되며, 초기화 과정에서 예외 처리가 가능하다.

변경된 코드: Lazy Initialization with Exception Handling

public class Settings {
    private static Settings instance;

    private Settings() throws Exception {
        // Checked 예외를 던질 수 있는 초기화 코드
        if (/* 특정 조건 */) {
            throw new Exception("Initialization failed");
        }
    }

    public static Settings getInstance() throws Exception {
        if (instance == null) {
            synchronized (Settings.class) { // Thread-safe 처리
                if (instance == null) {
                    instance = new Settings();
                }
            }
        }
        return instance;
    }
}

주요 변경 사항

1) Lazy Initialization:

getInstance() 메서드가 호출될 때 인스턴스가 생성된다. 이를 통해 필요하지 않은 경우 인스턴스를 생성하지 않는다.

2) 예외 처리:

생성자에서 Checked 예외를 던질 수 있으므로, getInstance() 메서드가 예외를 던지도록 선언(throws Exception)합니다.

3) Thread-Safe 처리:

멀티스레드 환경에서 싱글톤이 안전하게 초기화될 수 있도록 synchronized 블록과 이중 체크(Double-Checked Locking)를 사용한다.

싱글톤 패턴 구현 방법 4

double checked locking으로 효율적인 동기화 블럭 만들기

/**
 * double checked locking
 */
public class Settings3 {

    private static volatile Settings3 instance;

    private Settings3() { }

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

        return instance;
    }

1. double check locking이라고 부르는 이유?

  • 첫 번째 체크: if (instance == null) 조건으로 이미 초기화된 경우 동기화 없이 빠르게 반환한다.
  • 두 번째 체크 : 동기화 블록 내부에서 다시 if (instance == null) 확인한다. 이는 멀티스레드 환경에서 다른 스레드가 동시에 초기화하려는 문제를 방지한다.

2. instacne 변수는 어떻게 정의해야 하는가? 그 이유는?

  • volatile은 재정렬 방지: 객체 초기화 과정에서 재정렬 문제로 초기화가 완전히 끝나지 않은 상태의 객체를 참조하는 것을 막음.
  • 메모리 가시성 보장: 한 스레드에서 변경한 값을 다른 스레드가 즉시 확인할 수 있도록 보장.

싱글톤 패턴 구현 방법 5

static inner 클래스를 사용하는 방법

/**
 * static inner 클래스 홀더
 */
public class Settings4 {

    private Settings4() { }

    private static class Settings4Holder {
        private static final Settings4 INSTANCE = new Settings4();
    }

    public static Settings4 getInstance() {
        return Settings4Holder.INSTANCE;
    }

}

1. 이 방법은 static final를 썼는데도 왜 지연 초기화 (lazy initialization)라고 볼 수 있는가?

  • 내부 클래스(Settings4Holder)Settings4.getInstance()가 처음 호출될 때 로드되므로, INSTANCE도 그 시점에 초기화된다.
  • Java 클래스 로더 매커니즘 덕분에 내부 클래스는 필요할 때까지 로드하지 않아 객체 생성이 지연된다.
  • 따라서 static final이어도 Lazy Initialization이 가능하다.

싱글톤 (Singleton) 패턴 구현 깨트리는 방법 1

리플렉션을 사용한다면?

Settings settings = Settings.getInstance();

Constructor<Settings> declaredConstructor = Settings.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Settings settings1 = declaredConstructor.newInstance();

System.out.println(settings == settings1);

1. 리플렉션에 대해 설명하세요.

  • 런타임클래스와 멤버 정보를 조회하거나 수정하는 기능이다.
  • private 멤버 접근, 객체 생성, 메서드 호출 등이 가능하다.

2. setAccessible(true)를 사용하는 이유는?

  • private 멤버에 접근을 허용하여 리플렉션의 제약을 해제한다.
  • 싱글톤 패턴을 깨뜨리는 데 사용될 수 있으나, 보안 및 캡슐화 위반 문제가 있다.

싱글톤 (Singleton) 패턴 구현 깨트리는 방법 2

직렬화 & 역직렬화를 사용한다면?

public class Settings implements Serializable
...
Settings settings = Settings.getInstance();
Settings settings1 = null;

try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
out.writeObject(settings);
}

try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
settings1 = (Settings) in.readObject();
}

System.out.println(settings == settings1);

1. 자바의 직렬화 & 역직렬화에 대해 설명하세요.

직렬화&역직렬화 : 객체를 바이트 스트림으로 변환(직렬화)하여 저장하거나 전송하고, 다시 객체로 복원(역직렬화)하는 과정이다.

2. Serializable란 무엇이며 왜 쓰는가?

Serializable은 객체를 직렬화하여 바이트 스트림으로 변환하거나, 이를 다시 역직렬화하여 객체로 복원할 수 있도록 하는 인터페이스이다.

객체를 저장, 전송, 복원하기 위해 사용되며, 네트워크 통신과 데이터 유지 등에 유용하다.

3. try-resource 블럭에 대해 설명하세요.

자원을 사용하는 코드를 간결하고 안전하게 작성하기 위한 기능으로, 자원 자동 해제를 지원한다.

싱글톤 (Singleton) 패턴 구현 방법 6

enum을 사용하는 방법

/**
 * Enum을 사용해서 싱글톤 만들기
 */
public enum Settings5 {

    INSTANCE;

}

1. enum 타입의 인스턴스를 리팩토링을 만들 수 있는가?

리팩토링 가능 여부

enum 타입은 본질적으로 상수 집합이며, 자바에서 싱글톤 패턴을 구현할 때 매우 적합한 방식으로 간주된다.
그러나 리팩토링하여 새로운 인스턴스를 생성하거나 기존의 INSTANCE를 변경할 수 없다.

이유

  • Java의 enum은 스레드 안전하고, 단일 인스턴스를 보장하며, 리플렉션이나 직렬화로도 추가 객체를 생성할 수 없다.
  • enum 인스턴스는 JVM에 의해 클래스 로드 시점에 단 한 번만 생성되며, 이를 재정의하거나 새로운 인스턴스를 만들 수 없다.

2. enum으로 싱글톤 타입을 구현할 때의 단점은?

1) 유연성 부족

enum은 Java에서 기본적으로 상수 집합의 용도로 설계되었기 때문에, 일반 클래스와 달리 상속이 불가능하다.
싱글톤 확장이나 복잡한 초기화 로직 구현에는 제약이 존재한다.

2) 프레임워크 호환성 문제

일부 프레임워크(특히 구버전)에서는 enum 타입의 싱글톤 사용을 지원하지 않을 수 있다.

3) 초기화 제약:

복잡한 초기화 로직이 필요한 경우, enum 생성자에서는 제한적인 초기화만 가능하다.
ex) 초기화 과정에서 Checked 예외를 처리할 수 없다.

3. 직렬화 & 역직렬화 시에 별도로 구현해야 하는 메소드가 있는가?

필요없다!

enum은 직렬화 시 자동으로 단일 인스턴스를 보장하므로, 별도로 readResolve() 메서드를 구현할 필요가 없다.
이는 ObjectOutputStreamObjectInputStreamenum의 직렬화와 역직렬화를 자동으로 처리하기 때문이다.

싱글톤 (Singleton) 패턴 실무에서는?

싱글톤 패턴은 애플리케이션 전체에서 하나의 인스턴스를 공유해야 할 때 유용하다. 이는 자원 절약, 상태 공유, 관리 간소화를 위해 실무에서 여러 방식으로 활용된다.


1. 스프링 프레임워크에서의 싱글톤 스코프

스프링의 싱글톤 스코프

  • 스프링에서 빈(Bean)의 기본 스코프는 singleton이다.
  • 애플리케이션 컨텍스트(Application Context)가 초기화될 때 빈을 한 번 생성하며, 이후에는 동일한 인스턴스를 전역적으로 재사용한다.
  • 스프링의 싱글톤 빈은 스레드 안전성을 보장하지 않으므로 상태를 가지지 않는 Stateless Bean으로 설계하는 것이 중요하다.

장점

  1. 자원 절약: 객체를 한 번만 생성하여 메모리를 효율적으로 사용.
  2. 관리 간소화: 중앙에서 빈의 라이프사이클을 관리.

예제

@Component
public class MyService {
    public void performAction() {
        System.out.println("Action performed");
    }
}
  • @Component로 등록된 빈은 기본적으로 싱글톤 스코프로 관리된다.
  • 모든 컨트롤러, 서비스, 리포지토리 등은 싱글톤 스코프를 기본으로 사용한다.

싱글톤 빈 사용 시 주의점

  1. 상태를 가지는 필드를 사용하지 말아야 함. (Stateless Bean 권장)
  2. 동시성 문제를 피하기 위해 동기화나 쓰레드로컬(ThreadLocal)을 고려.

2. java.lang.Runtime 클래스

Runtime 클래스란?

  • Java에서 java.lang.Runtime 클래스는 JVM의 런타임 환경을 제어할 수 있는 유일한 싱글톤 클래스다.
  • 이를 통해 JVM의 메모리 관리, 프로세스 실행, 종료 등의 작업을 수행할 수 있다.

특징

  1. 싱글톤 패턴으로 설계됨: Runtime.getRuntime() 메서드를 통해 인스턴스를 가져옴.
  2. JVM 환경에 대한 중요한 작업을 수행.

예제

public class Main {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();

        System.out.println("Available Processors: " + runtime.availableProcessors());
        System.out.println("Free Memory: " + runtime.freeMemory());

        try {
            runtime.exec("notepad");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 활용:
    • 시스템 메모리 및 프로세스 정보 조회.
    • 외부 프로세스 실행 (예: OS 명령어, 애플리케이션 실행).
    • JVM 종료 (runtime.exit()).

장점

  • JVM의 런타임 환경을 효율적으로 관리.
  • 애플리케이션 전역에서 동일한 Runtime 객체를 공유.

3. 다른 디자인 패턴에서 싱글톤의 활용

빌더 패턴 (Builder Pattern)

  • 싱글톤 빌더를 사용하여 객체 생성 과정을 단순화.
  • 복잡한 객체 생성 로직을 한 번 정의하여 여러 곳에서 재사용 가능.

예제:

public class ConfigurationBuilder {
    private static final ConfigurationBuilder INSTANCE = new ConfigurationBuilder();
    
    private ConfigurationBuilder() { }
    
    public static ConfigurationBuilder getInstance() {
        return INSTANCE;
    }
    
    public Configuration build() {
        // 복잡한 설정 로직
        return new Configuration();
    }
}

퍼사드 패턴 (Facade Pattern)

  • 퍼사드 객체를 싱글톤으로 구현하여 서브시스템 접근을 단일 지점으로 제한.
  • 예를 들어, 데이터베이스 연결 관리, API 요청 등을 싱글톤 퍼사드로 처리.

예제:

public class DatabaseFacade {
    private static final DatabaseFacade INSTANCE = new DatabaseFacade();

    private DatabaseFacade() { }

    public static DatabaseFacade getInstance() {
        return INSTANCE;
    }

    public void executeQuery(String query) {
        // Query 실행 로직
    }
}

추상 팩토리 패턴 (Abstract Factory Pattern)

  • 팩토리 클래스 자체를 싱글톤으로 구현하여 객체 생성 전략을 애플리케이션 전역에서 공유.

예제:

public class ShapeFactory {
    private static final ShapeFactory INSTANCE = new ShapeFactory();

    private ShapeFactory() { }

    public static ShapeFactory getInstance() {
        return INSTANCE;
    }

    public Shape createCircle() {
        return new Circle();
    }
}

실무에서 싱글톤 사용 시 주의할 점

  1. 상태 관리 문제
    • 싱글톤이 상태를 가지면(예: 가변 필드), 동시성 문제가 발생할 수 있음.
    • 항상 Stateless 설계로 유지하는 것이 안전.

관련 이유)
만약 싱글톤이 "상태를 가지는 필드"(예: 사용자 입력값, 계산 결과 등)를 가지고 있다면, 여러 스레드가 동시에 같은 객체를 사용할 때 문제가 생길 수 있다.

  1. 테스트 어려움

    • 싱글톤은 객체를 전역적으로 공유하기 때문에, 유닛 테스트에서 독립성을 유지하기 어려울 수 있음.
    • 이를 해결하려면 의존성 주입(DI) 패턴을 활용하거나, 인터페이스 기반으로 설계.
  2. 메모리 누수

    • 싱글톤 객체가 종료되지 않고 메모리에 계속 남아 있을 수 있으므로, 적절한 리소스 관리를 해야 함.

출처 : 코딩으로 학습하는 GoF의 디자인 패턴

0개의 댓글