싱글톤 패턴(Singleton Pattern)

가장 널리 사용되는 디자인 패턴 중 하나이며, Spring의 DI 개념에 근간이 되는 싱글톤 패턴

 

싱글톤 패턴이란?

어플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(static) 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴이다.
쉽게 말해 객체의 인스턴스가 오직 1개만 생성되는 패턴이라고 생각하면 된다.
인스턴스가 1개만 생성되는 특징을 가진 싱글톤 패턴을 이용하면, 하나의 인스턴스를 메모리에 등록해서 여러 쓰레드가 동시에 해당 인스턴스를 공유하여 사용할 수 있게끔 할 수 있기 때문에 요청이 많은 곳에서 사용하면 효율을 높일 수 있다.
다만, 싱글톤을 사용할 때 동시성(Concurrency) 문제를 고려해서 설계해야 한다.

 

싱글톤 패턴 사용 시 주의해야 할 점

싱글톤 패턴 사용 시 주의해야 할 점은, 상태를 가진 객체를 싱글톤으로 만들면 안된다는 것이다.
앱 내의 단 한개의 인스턴스가 존재하고, 이를 전역에서 접근할 수 있다면 각기 다른 스레드에서 객체의 상태를 마구잡이로 변경시킬 여지가 있다.
상태가 공유된다는 것은 매우 위험한 일이기 때문에 무상태 객체 혹은 설계상 유일해야 하는 시스템 컴포넌트를 싱글톤으로 만들어야 한다.

지금까지 구현했던 스프링 백엔드 프로젝트를 생각해보자. Service, Repository 등 spring Bean을 이용해서 싱글톤 패턴을 사용하는 객체 중에 상태를 가지는 객체가 있었던가?

 

싱글톤 패턴 구현 예제

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 생성자만을 정의해 외부 클래스로부터 인스턴스 생성을 차단한다는 점이다.

 

싱글톤 패턴의 장점

  • 고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있다.
  • 싱글톤으로 만들어진 클래스의 인스턴스는 전역이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다.
  • 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 경우 사용한다.

 

싱글톤 패턴의 단점

  • 위의 구현 예제와 같이 싱글톤 패턴을 구현하는 코드 자체가 많이 필요하다. 그 외에도 정적 팩토리 메서드에서 객체 생성을 확인하고 생성자를 호출하는 경우에 멀티스레딩 환경에서 발생할 수 있는 동시성 문제 해결을 위해 syncronized 키워드를 사용해야 한다.
  • 테스트하기 어렵다. 싱글톤 인스턴스는 자원을 공유하고 있기 때문에 테스트가 결정적으로 격리된 환경에서 수행되려면 매번 인스턴스의 상태를 초기화시켜주어야 한다.(JUnit의 BeforeEach 사용이유) 그렇지 않으면 어플리케이션 전역에서 상태를 공유하기 때문에 테스트가 온전하게 수행되지 못한다.
  • 의존 관계 상 클라이언트(싱글톤 패턴으로 이루어진 클래스를 가져다 사용하는 쪽)가 구체 클래스에 의존하게 된다. new 키워드를 직접 사용하여 클래스 안에서 객체를 생성하고 있으므로, 이는 SOLID 원칙 중 DIP를 위반하게 되고 OCP 원칙 또한 위반할 가능성이 높다.
DIP - 의존관계 역전 원칙
- 추상화(interface)에 의존해야 하고, 구체화(implements)에 의존하면 안된다.

OCP - 개방-폐쇄 원칙
- 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

싱글톤이 안티패턴이라고 불리는 이유

SOLID 설계원칙을 위반한다.

SOLID 원칙의 대부분은 인터페이스 설계와 관련이 있다. 의존성을 인터페이스에 두면 실제 구현 클래스의 구현이 변경되어도 이를 사용한 코드는 큰 영향을 받지 않는다.
그렇기 때문에 SOLID 원칙을 지키기 위해서는 인터페이스로 설계하는게 좋은 설계이다.


하지만 싱글톤을 사용하는 경우 대부분 인터페이스가 아닌 구현 클래스의 객체를 미리 생성해놓고 정적 메서드를 이용하여 구현하게 된다.
이는 SOLID 원칙을 위반할 수 있는 가능성이 있으며, 동시에 싱글톤을 사용하는 곳과 싱글톤 클래스 사이에 의존성이 생기게 된다.


이는 결합도를 높이는 행위로, 수정 및 단위테스트의 어려움이 생기게 된다.

 

객체지향의 의도와 맞지 않는다.

싱글톤의 사용은 Global state를 만들 수 있기 때문에 바림직한 방법은 아니다.
아무 객체나 자유롭게 접근하고 수정하고 공유할 수 있는 전역 상태를 갖는 것은 객체지향 프로그래밍에서는 지양되어야 한다.
또한 싱글톤은 자바의 경우 private 생성자를 가지고 있기 때문에 상속할 수 없다. 이는 다형성같은 객체지향의 특징을 적용할 수 없게 된다.

 

스프링은 싱글톤 패턴의 문제점들을 어떻게 해결했는가?

  • 구현하는 코드가 많이 필요하다는 단점은 스프링 컨테이너가 자체적으로 생성해주는 것으로 해결하였다.
  • 테스트가 어려워진다는 단점은 클래스 간의 의존성을 낮춤으로서 해결한다.
  • DIP와 OCP 원칙을 위반한다는 단점은 스프링 컨테이너에게 객체의 생성 및 관리 권한을 넘김으로써(IoC, 제어의 역전) 해결한다.

 

스프링 컨테이너는 어떻게 이러한 것들이 가능한가?

엄밀히 말하자면 스프링 컨테이너는 객체를 싱글톤으로 이용하게 하지만, '싱글톤 패턴'을 사용하진 않는다.
스프링은 싱글톤 레지스트리 라는 것을 이용하여 싱글톤을 구현한다.

 

싱글톤 레지스트리란?

스프링이 직접 싱글톤 객체를 만들고 관리하는 기능을 제공하는 것을 의미한다. 이 구현 방법은 static 메서드와 private 생성자를 이용하는 것을 강제하지 않는다.
단지, 평범한 java class를 싱글톤으로 활용하게 해준다. 쉽게 말해 평범한 public 생성자를 가진 자바 클래스를 싱글턴으로 활용할 수 있게 만들어준다.
그렇게 할 수 있는 이유는 클래스의 제어권을 컨테이너에게 넘기면 해당 컨테이너가 객체 생성에 대한 모든 권한을 가질 수 있기 때문이다.(IoC)
정리하자면, 스프링 컨테이너가 제어권을 가짐으로써 빈을 싱글톤으로 만들 수 있는 것이고 이러한 방식을 IoC(제어의 역전) 라고 부른다.

 

스프링에서 싱글톤 패턴, DI(의존성 주입), IoC(제어 역전) 간의 연관관계

스프링은 웹 애플리케이션을 작성하기 위한 프레임워크다. 싱글톤을 적용하지 않은 웹 서버가 있다고 가정하자.
이 서버는 클라이언트가 한 번 연결될 때마다 하나의 인스턴스를 생성할 것이다. 그리고 동시에 많은 수의 커넥션이 일어난다면?
이러한 이유 때문에 웹 서버는 싱글톤으로 인스턴스의 수를 제한할 필요가 있다.
하지만 싱글톤을 사용하게 되면, 위에서 언급했듯이 코드 복잡도 증가, SOLID 원칙을 어겨 클래스 간의 의존성 높아짐 등의 문제점이 있다.
이를 해결하기 위해 스프링은 DI(의존성 주입)을 이용해 싱글톤 객체를 사용하는 클래스 내부에서 생성하는 것이 아닌 외부에서 생성 후 주입받도록 해 클래스 간의 의존성을 줄였다.
또한 IoC를 통해 싱글톤 객체를 외부에서 생성하는 것조차 스프링에게 위임하여(객체의 생명주기 관리를 위임) 유연한 코드를 작성할 수 있게 하고, 코드 중복, 유지 보수를 개선했다.

'Design Pattern' 카테고리의 다른 글

디자인 패턴의 존재 이유  (2) 2024.07.16
potatoCompletion