프로그래밍 언어/Java

[Java] 싱글톤(Singleton) 패턴의 사용 이유와 문제점

ioh'sDeveloper 2025. 1. 27. 15:41

싱글톤 패턴이란?

싱글톤 패턴(Singleton Pattern)은 소프트웨어 디자인 패턴 중 하나로, 클래스의 인스턴스를 단 하나만 생성하고, 해당 인스턴스에 전역적으로 접근할 수 있도록 보장하는 방법입니다.
이 패턴은 공통된 자원을 관리하거나 전역 상태를 유지해야 할 때 자주 사용됩니다.
주요 특징은 아래와 같습니다.

  • 인스턴스가 한 번만 생성됨.
  • 전역적으로 접근 가능.
  • 동일한 자원을 반복 생성하지 않아 효율적.

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

  1. 자원의 효율적 관리
    • 인스턴스를 하나만 생성하고 이를 공유하기 때문에 메모리 낭비를 줄일 수 있습니다.
    • 데이터베이스 연결, 로그 관리 등에서 유용합니다.
  2. 글로벌 접근성 제공
    • 애플리케이션 어디서나 동일한 객체에 접근 가능.
    • 중복 코드 작성 없이 공통 데이터와 로직을 공유.
  3. 상태 관리의 일관성
    • 단일 인스턴스를 통해 모든 클래스가 동일한 상태를 유지.
    • 설정값, 설정 관리와 같은 작업에 적합.

싱글톤 패턴 구현 방법

1. 기본 싱글톤 패턴

가장 기본적인 싱글톤 구현 방식입니다.
단, 멀티스레드 환경에서는 인스턴스가 여러 번 생성될 위험이 있습니다.

public class BasicSingleton {
    private static BasicSingleton instance;

    private BasicSingleton() {} // 생성자 private

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

    public void showMessage() {
        System.out.println("Hello, Singleton!");
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        BasicSingleton singleton = BasicSingleton.getInstance();
        singleton.showMessage();
    }
}

2. 멀티스레드 환경에서 안전한 싱글톤

멀티스레드 환경에서는 인스턴스가 중복 생성되지 않도록 동기화가 필요합니다.
아래 코드는 synchronized 키워드를 사용해 안전성을 보장합니다.

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton() {}

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

    public void showMessage() {
        System.out.println("Thread-Safe Singleton instance created!");
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        ThreadSafeSingleton singleton = ThreadSafeSingleton.getInstance();
        singleton.showMessage();
    }
}

3. 더블 체크 락(Double-Checked Locking)으로 최적화

동기화 성능 문제를 해결하기 위해 Double-Checked Locking 방식을 사용할 수 있습니다.
이 방법은 인스턴스가 이미 생성된 경우 동기화를 건너뛰어 성능을 향상시킵니다.

public class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;

    private DoubleCheckedLockingSingleton() {}

    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) { // 첫 번째 체크
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) { // 두 번째 체크
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }

    public void showMessage() {
        System.out.println("Double-Checked Locking Singleton instance created!");
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        DoubleCheckedLockingSingleton singleton = DoubleCheckedLockingSingleton.getInstance();
        singleton.showMessage();
    }
}

4. Enum 기반 싱글톤 (권장)

Enum을 사용하면 싱글톤 패턴의 구현이 간단하며, 직렬화와 멀티스레드 문제를 자동으로 해결할 수 있습니다.
이는 가장 안전하고 간단한 구현 방법으로, 권장됩니다.

public enum EnumSingleton {
    INSTANCE;

    public void showMessage() {
        System.out.println("Enum Singleton instance created!");
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        EnumSingleton singleton = EnumSingleton.INSTANCE;
        singleton.showMessage();
    }
}

싱글톤 패턴의 단점

1. 테스트 어려움

 TDD(Test Driven Developmennt)를 할 때 걸림돌이 됩니다. TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 합니다. 

하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 '독립적인' 인스턴스를 만들기가 어렵습니다.

  • 싱글톤은 전역 상태를 가지므로 독립적인 테스트 환경을 구성하기 어렵습니다.
  • 상태를 초기화하지 않으면 테스트 간 간섭이 발생할 수 있습니다.

2. 싱글톤 패턴의 유연성 부족

  • 싱글톤 패턴은 사용이 간단하고 실용적이지만, 클래스 간 결합도가 높아질 가능성이 있습니다.
  • 결합도 증가는 모듈 간의 변경이 어렵게 만들어 코드의 유연성을 낮추고, 확장성과 테스트를 저하시킵니다.
  • 싱글톤은 클래스 간 결합도를 높이며, 객체 지향 설계 원칙(SOLID)을 위반할 가능성이 있습니다.
  • 특히 DIP(의존성 역전 원칙)를 위반하여 코드 확장이 어려워질 수 있습니다.

2-1. 의존성 주입(DI)란?

  • 의존성 주입은 모듈 간의 결합을 줄이기 위해 의존성을 외부에서 주입하는 방식입니다.
  • 직접 의존성을 제거하고, 간접적으로 의존성을 주입하는 구조를 만듭니다.

2-2. 의존성 주입의 동작

  • 직접 의존성: 상위 모듈이 하위 모듈의 구체적인 구현을 직접 참조.
    • 결과적으로 상위 모듈이 하위 모듈의 변경에 민감하게 반응.
  • 간접 의존성(DI): 의존성 주입자가 중간에서 하위 모듈을 주입해 상위 모듈이 하위 모듈의 구체적인 구현을 알 필요가 없음.
    • 디커플링: 상위 모듈과 하위 모듈의 결합도가 낮아짐.

2-3. 의존성 주입의 동작

  • 직접 의존성: 상위 모듈이 하위 모듈의 구체적인 구현을 직접 참조.
    • 결과적으로 상위 모듈이 하위 모듈의 변경에 민감하게 반응.
  • 간접 의존성(DI): 의존성 주입자가 중간에서 하위 모듈을 주입해 상위 모듈이 하위 모듈의 구체적인 구현을 알 필요가 없음.
    • 디커플링: 상위 모듈과 하위 모듈의 결합도가 낮아짐.

2-4. 의존성 주입의 장점

  • 모듈 교체 용이성: 상위 모듈은 하위 모듈의 구체적인 구현을 알 필요가 없으므로, 하위 모듈을 쉽게 교체 가능.
  • 테스트 용이성: 하위 모듈 대신 목(Mock)을 사용해 독립적인 테스트 수행 가능.
  • 확장성 향상: 새로운 기능을 추가하거나 모듈을 교체해도 상위 모듈에는 영향을 주지 않음.
  • 코드 유연성 증가: 변경 가능성이 낮은 코드 구조를 통해 유지보수가 용이.

2-5. 의존성 주입의 단점

  1. 복잡성 증가: 모듈이 분리되면서 클래스와 구성 요소의 수가 늘어나 코드가 복잡해질 수 있음.
  2. 런타임 페널티: 주입 과정에서 약간의 성능 저하가 발생할 수 있음.

2-6. 의존성 역전 원칙(DIP, Dependency Inversion Principle)

  • 의존성 주입은 DIP를 지키기 위해 활용됩니다.
  • DIP 원칙:
    1. 상위 모듈은 하위 모듈에 의존하지 않아야 한다.
    2. 상위 모듈과 하위 모듈은 추상화(인터페이스 또는 추상 클래스)에 의존해야 한다.
    3. 추상화는 구체적인 구현에 의존하지 않아야 한다.

2-7. 싱글톤 패턴과 의존성 주입의 관계

  • 싱글톤 패턴은 클래스 간 결합도를 높이며, DIP 원칙을 위반할 가능성이 있음.
  • 의존성 주입을 활용하면 싱글톤 패턴으로 인해 발생하는 결합도를 줄이고 유연한 구조를 만들 수 있음.

2-8. 의존성 정리

  • 싱글톤 패턴의 문제점: 결합도 증가로 인한 유연성 부족, 테스트 어려움, DIP 위반 가능성.
  • 의존성 주입의 해결 방법:
    • 상위 모듈이 하위 모듈의 구체적인 구현에 의존하지 않도록 의존성 주입을 활용.
    • DI를 통해 모듈 간 결합을 줄이고, 유연하고 테스트 가능한 코드 구조를 설계.
  • 의존성 주입은 모듈 교체, 확장성, 테스트 용이성을 높여 코드 품질을 개선하는 핵심 설계 방식입니다.

3. 멀티스레드 환경 동기화 비용

  • synchronized 키워드는 성능 저하를 초래할 수 있습니다.
  • 더블 체크 락이나 Enum을 사용하여 이를 개선할 수 있습니다.

4. 전역 상태 의존성 증가

  • 싱글톤은 전역 상태를 관리하기 때문에 코드 복잡성을 증가시키고, 디버깅을 어렵게 만들 수 있습니다.

결론

싱글톤 패턴은 공통 자원을 관리하거나 전역 상태를 유지할 때 매우 유용합니다.
그러나 잘못 사용하면 유지보수성 저하, 테스트 어려움과 같은 문제를 초래할 수 있습니다.

  • 단순한 구현이 필요하면 기본 싱글톤 또는 Thread-Safe Singleton을 사용할 수 있습니다.
  • 멀티스레드 환경에서 효율성과 안전성을 모두 원한다면 더블 체크 락(Double-Checked Locking) 또는 Enum 기반 싱글톤을 사용하는 것이 좋습니다.
  • 싱글톤 패턴은 만능 해결책이 아니므로, 꼭 필요한 경우에만 사용해야 합니다.

프레임워크(SPRING)와 같은 환경에서는 싱글톤 스코프를 통해 효율적으로 관리할 수 있으니, 이를 활용하는 것도 고려해볼 만합니다.