싱글톤 패턴, 진짜 필요한 순간만 써야 하는 이유

석현·2025년 5월 22일
0

Insight

목록 보기
38/43
post-thumbnail

저번에도 싱글톤 관련해서 글을 올렸는데 아쉽고 조금 이해하기 힘든 부분들이 있어서 새로운 글을 작성해봅니다!
디자인 패턴은 코드의 품질과 유지보수성을 높이는 데 큰 도움을 주는 도구입니다. 이번 글에서는 그중에서도 가장 자주 등장하고, 동시에 잘못 사용되기도 쉬운 싱글톤(Singleton) 패턴에 대해 깊이 있게 알아보겠습니다.


싱글톤 패턴이란?

정의는 간단합니다. "인스턴스를 하나만 만들고, 그 인스턴스를 어디서든 공유하는 패턴"입니다.

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
        // 외부에서 생성자를 호출하지 못하게 private 처리
    }

    public static Singleton getInstance() {
        return instance;
    }

    public void say() {
        System.out.println("hi, there");
    }
}
  • 생성자는 private으로 감추고,
  • static 필드로 미리 만들어둔 인스턴스
  • static 메서드를 통해 반환하는 구조입니다.

싱글톤 패턴의 장점

1. 메모리 효율

  • 객체가 한 번만 생성되므로, 중복된 메모리 낭비가 없습니다.

2. 속도 향상

  • 이미 만들어진 객체를 재사용하니, 매번 객체를 new 하지 않아도 됩니다.

3. 전역 접근 가능

  • 여러 클래스 간 공유가 편리하며, 설정 정보나 공통 유틸리티로 자주 사용됩니다.

하지만 문제는… 싱글톤의 단점들

단순한 구조 같지만, 싱글톤은 문제의 덩어리가 될 수 있습니다.

1. 테스트 어려움

  • 싱글톤 인스턴스는 애플리케이션 전체에서 하나의 객체로 유지되기 때문에, 테스트 케이스 간에 상태가 공유되는 문제가 발생합니다.
  • 예를 들어, Singleton.getInstance().setValue("A") 라는 상태 설정이 한 테스트에서 이루어졌다면, 다음 테스트에서도 같은 인스턴스를 사용하므로 이전 상태("A")가 그대로 유지됩니다.
  • 이로 인해 테스트 간 의존성이 생기고, 테스트의 독립성이 깨집니다.
어떻게 초기화할 수 있을까?
  • 가장 좋은 방법은 싱글톤을 완전히 재설정할 수 있는 테스트 전용 훅을 만드는 것입니다.
public class Singleton {
    private static Singleton instance = new Singleton();
    private String value;

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    // 테스트용 초기화 메서드 (주의: 실서비스에서는 사용 금지)
    static void resetInstanceForTest() {
        instance = new Singleton();
    }
}
  • 테스트 전후로 Singleton.resetInstanceForTest()를 호출하면 초기화가 가능하지만, 이 방식은 설계적으로 좋지 않고 위험할 수 있습니다.
  • 가장 좋은 해결책은 프레임워크(DI 컨테이너)를 사용하거나, 싱글톤을 직접 만들지 않는 것입니다.

2. 동시성 문제

  • 멀티스레드 환경에서 객체가 여러 번 생성되지 않도록 주의해야 합니다.
  • 이를 방지하기 위해 다음과 같은 방식으로 synchronized 키워드를 사용할 수 있습니다:
public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}
  • 하지만 synchronized는 성능 저하를 초래할 수 있습니다. 그 이유는 다음과 같습니다:

    • synchronized 메서드는 모든 스레드가 순차적으로 접근해야 하므로, 동시성(멀티스레드)의 장점을 살리지 못하게 됩니다.
    • 한 번 인스턴스가 생성되고 나면 더는 동기화가 필요 없지만, 위 코드처럼 항상 synchronized를 사용하면 불필요한 락 비용이 계속 발생합니다.
    • 결국, 병목 현상이 생기고 전체 애플리케이션의 처리 속도에 영향을 줄 수 있습니다.
  • 이를 보완하기 위해 보통 double-checked locking 패턴을 사용합니다.

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 이 패턴은 최초 생성 시에만 synchronized를 사용하므로 성능 저하를 최소화할 수 있습니다.

3. 클래스에 강한 결합

  • 싱글톤 객체를 직접 가져다 쓰면, 클라이언트 코드가 구체 클래스에 의존하게 됩니다.

  • 이로 인해 테스트가 어려워지고, 확장이나 변경이 어려운 구조가 됩니다.

  • 대표적으로 SOLID 원칙 중 아래 두 가지를 위반할 수 있습니다:

    • DIP (Dependency Inversion Principle, 의존 역전 원칙):

      • 고수준 모듈(비즈니스 로직)이 저수준 모듈(구현체)에 의존하게 됩니다. 싱글톤 객체를 직접 호출하면 인터페이스가 아닌 구체 클래스에 의존하게 되므로 DIP를 위반합니다.
    • OCP (Open/Closed Principle, 개방-폐쇄 원칙):

      • 기존 코드를 수정하지 않고 확장할 수 있어야 하지만, 싱글톤 객체를 고정적으로 사용하면 확장이 어렵고 새로운 구현체로 대체하기가 어렵습니다.
  • 이 문제를 피하려면 인터페이스 기반 설계의존성 주입(DI)을 통해 유연성을 확보하는 것이 좋습니다.

4. 확장성 부족

  • 싱글톤 클래스는 상속이 어렵고, 내부 상태 변경이 복잡해지므로 유연성이 떨어집니다.

그래서 싱글톤은 안티패턴일까?

절대 그렇진 않습니다. 단, 직접 구현하기보다는 프레임워크의 도움을 받는 것이 훨씬 낫다는 게 핵심입니다.

예: Spring에서는 기본적으로 Bean Scope가 Singleton이며,
컨테이너가 생명주기, 동시성, 의존성까지 안전하게 관리해줍니다.

즉, "싱글톤처럼 동작하지만, 단점은 없는 구조"를 프레임워크가 제공해주는 거죠.


마무리: 언제, 어떻게 써야 할까?

싱글톤 패턴은 다음과 같은 상황에서 유용하게 사용할 수 있습니다:

  • 설정이나 환경 정보를 전역으로 공유해야 할 때

    • 예: 애플리케이션 설정 객체, DB 연결 설정 등
  • 공통 유틸리티 객체를 재사용할 때

    • 예: 로깅 객체, 캐시 관리자, 암호화 유틸리티 등
  • 외부 자원과의 연결을 하나로 제한해야 할 때

    • 예: 데이터베이스 커넥션 풀 관리자, 네트워크 소켓 관리자 등
  • 동일한 상태를 여러 곳에서 참조해야 할 때

    • 예: 사용자 세션 매니저, 글로벌 이벤트 디스패처 등

다만, 아래 조건 중 하나라도 해당된다면 싱글톤 사용은 다시 한번 고려해보는 것이 좋습니다:

  • 해당 객체가 상태(state)를 갖고 있으며, 그 상태가 동적으로 변할 수 있다면
  • 해당 객체가 테스트 격리를 어렵게 만든다면
  • 여러 환경(멀티테넌시, 다중 인스턴스 운영 등)에서 인스턴스 분리가 필요하다면

직접 구현한다면 동시성 처리, 상태 관리, 의존성 문제를 신중히 고려해야 하며,
가능하다면 Spring과 같은 DI 프레임워크의 지원을 받는 것이 바람직하다고 생각합니다


싱글톤 패턴은 매우 강력하지만, 신중하게 사용해야 할 도구입니다. 객체를 하나만 만든다는 단순한 규칙 뒤에는 다양한 트레이드오프가 숨어 있기 때문입니다. 잘 사용하면 효율적인 구조가 되고, 잘못 사용하면 유지보수 악몽이 될 수도 있다는 것!

profile
Learner

0개의 댓글