싱글턴 패턴(Singleton Pattern)

Dope
Webdev TechBlog
Published in
13 min readApr 13, 2020

--

서론

디자인 패턴 중 하나인 싱글턴 패턴(Singleton Pattern) 에 대해서 배워보겠습니다. 자바의 싱글턴과 스프링의 싱글턴이 어떻게 다른지, Thread-safe 한 싱글턴 구현방법 등에 대해서 배워보겠습니다.

싱글턴(Singleton) 이란 ?

싱글턴 패턴은 인스턴스가 오직 1개만 생성되야 하는 경우에 사용되는 패턴입니다. 예를들어 레지스트리 같은 설정 파일의 경우 객체가 여러개 생성되면 설정 값이 변경될 위험이 생길 수 있습니다.

인스턴스가 1개만 생성되는 특징을 가진 싱글턴 패턴을 이용하면, 하나의 인스턴스를 메모리에 등록해서 여러 스레드가 동시에 해당 인스턴스를 공유하여 사용하게끔 할 수 있으므로, 요청이 많은 곳에서 사용하면 효율을 높일 수 있습니다.

주의 해야할 점은 싱글턴을 만들때 동시성(Concurrency) 문제를 고려해서 싱글턴을 설계해야합니다.

아래에서 자바의 싱글턴 패턴 구현방법과, 스프링에서 사용되는 싱글턴 패턴에 대해서 배워보겠습니다.

자바의 싱글턴 패턴(Sigleton Pattern in Java)

싱글턴 패턴의 공통적인 특징은 private constructor 를 가진다는 것과, static method 를 사용한다는 점입니다.

싱글턴 패턴 구현에 사용되는 몇가지 이디엄 방식을 소개하겠습니다.

Eager Initialization(이른 초기화, Thread-safe)

이른 초기화 방식은, static 키워드의 특징을 이용해서 클래스 로더가 초기화 하는 시점에서 정적 바인딩(static binding)(컴파일 시점에서 성격이 결정됨)을 통해 해당 인스턴스를 메모리에 등록해서 사용하는 것입니다.

이른 초기화 방식은 클래스 로더에 의해 클래스가 최초로 로딩 될 때 객체가 생성되기때문에 Thread-safe 합니다.

싱글턴 구현 시 중요한 점이, 멀티 스레딩 환경에서도 동작 가능하게끔 구현해야 한다는 것입니다. 즉, Thread-safe 가 보장되어야 합니다.

Lazy Initialization with synchronized (동기화 블럭, Thread-safe)

두 번째 방식은, synchronized 키워드를 이용한 게으른 초기화 방식인데, 메서드에 동기화 블럭을 지정해서 Thread-safe 를 보장합니다.

게으른 초기화 방식이란 ? 컴파일 시점에 인스턴스를 생성하는 것이아니라, 인스턴스가 필요한 시점에 요청 하여 동적 바인딩(dynamic binding)(런타임시에 성격이 결정됨)을 통해 인스턴스를 생성하는 방식을 말합니다.

동기화 블럭을 지정한 게으른 초기화 방식은 스레드 세이프하지만, 단점이 있습니다. 인스턴스가 생성되었든, 안되었든 무조건 동기화 블록을 거치게 되어있다는 것입니다.

synchronized 키워드를 사용하면 성능이 약 100배 가량 떨어집니다. 만약, getInstance 메서드의 속도가 중요하지 않은 경우라면 그냥 둬도 상관은 없습니다. (성능은 떨어지지만… 어쨋든 구현했으므로)

하지만 아래에서 나오는 방식들을 배우고 나서는 위 방식을 사용하면 안됩니다. (굳이 좋은 방식이 있는데 안좋은 방식을 쓸 필요는 없느니…)

Lazy Initialization. Double Checking Locking(DCL, Thread-safe)

위 동기화 블럭 방식을 개선한 방식이 DCL(Double Checking Locking) 방식 입니다. 이 방식은, 인스턴스가 생성되지 않은 경우에만 동기화 블럭이 실행되게끔 구현하는 방식입니다.

위 코드에서 volatile 키워드가 등장하는데, volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance 변수가 Sigleton 인스턴스로 초기화 되는 과정이 올바르게 진행되도록 할 수 있습니다.

volatile 키워드가 필요한 이유 ?

volatile 변수를 사용하고 있지 않는 멀티 스레드 어플리케이션에서는 작업(Task)을 수행하는 동안 성능 향상을 위해 Main Memory 에서 읽은 변수 값을 CPU Cache 에 저장하게 됩니다. 만약에 멀티 스레드 환경에서 스레드가 변수 값을 읽어올 때 각각의 CPU Cache 에 저장된 값이 다르기 때문에 변수 값 불일치 문제가 발생하게 되는데, volatile 키워드가 이런 문제를 해결해 줍니다.

즉, volatile 변수는 Main Memory 에 값을 저장하고 읽어오기 때문에(read and write) 변수 값 불일치 문제가 생기지 않습니다.

  • 멀티 스레드 환경에서 하나의 스레드는 read and write 하며, 나머지 스레드는 read 만 하는 경우 변수의 최신 값을 보장
  • 멀티 스레드 환경에서 여러개의 스레드가 write 하는 상황이라면 동기화 블럭(synchronized) 을 지정해서 원자성(atomic) 을 보장해야 한다.

Lazy Initailization. Enum(열거 상수 클래스, Thread-safe)

Enum 인스턴스의 생성은 기본적으로 Thread-safe 합니다. 따라서 스레드 관련된 코드가 없어져서 코드가 간단해집니다. 하지만 Enum 내의 다른 메서드가 있는 경우에 해당 메서드가 Thread-safe 한지는 개발자가 책임져야합니다.

Enum 방식을 사용한 장점은 아주 복잡한 직렬화 상황이나, 리플렉션 공격에도 제 2의 인스턴스가 생성되는 것을 막아줍니다. 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 하는 경우에는 사용할 수 없습니다. 또한, Android 같이 Context 의존성이 있는 환경일 경우, 싱글턴의 초기화 과정에 Context 라는 의존성이 끼어들 가능성이 있습니다.

Lazy Initialization. LazyHolder(게으른 홀더, Thread-safe)

LazyHolder 방식은 가장 많이 사용되는 싱글턴 구현 방식입니다.

volatile 이나 synchronized 키워드 없이도 동시성 문제를 해결하기 때문에 성능이 뛰어납니다.

싱글턴 클래스에는 InnerInstanceClazz 클래스의 변수가 없기 때문에, static 멤버 클래스더라도, 클래스 로더가 초기화 과정을 진행할때 InnerInstanceClazz 메서드를 초기화 하지 않고, getInstance() 메서드를 호출할때 초기화 됩니다. 즉, 동적바인딩(Dynamic Binding) (런타임시에 성격이 결정) 의 특징을 이용하여 Thread-safe 하면서 성능이 뛰어납니다.

InnerInstanceClazz 내부 인스턴스는 static 이기 때문에 클래스 로딩 시점에 한번만 호출된다는 점을 이용한것이며, final을 써서 다시 값이 할당되지 않도록 합니다.

싱글턴 사용시 주의사항

클래스 로더를 2개 이상 사용하는 경우, 인스턴스가 2개 이상 생성될 수 있습니다. 이런 경우에는 클래스 로더를 지정해야 합니다.

자바와 스프링의 싱글톤 차이점은, 싱글톤 객체의 생명주기가 다릅니다. 또한 자바에서 범위는 클래스 로더가 기준이지만, 스프링 에서는 어플리케이션 컨텍스트(ApplicationContext) 가 기준이 됩니다.

클래스 로더 기준이라는 것은 톰캣이 WAR 파일을 만들게 되면, WAR 파일 하나 당 클래스 로더 하나 1:1 식으로 배치가 되기 때문에 다른 WAR 파일은 참조가 불가능합니다.

반면, ApplicationContext 기준이라는 것은 web.xml 에서 root context 하나와 servlet cotnext 여러개를 등록할 수 있는데, 이 각각의 context 들이 싱글턴 범위가 됩니다.

스프링의 싱글턴 패턴(Singleton Pattern in Spring)

스프링은 빈을 등록할 때 범위(scope)를 지정할 수 있는데 디폴트가 싱글턴(Singleton) 입니다. 그 외에도 prototype, request, session 이 있습니다.

  • prototype : 컨테이너에 빈을 요청할 때마다 매번 새로운 객체를 만든다.
  • request : HTTP 요청 하나당 하나의 객체를 만든다.

스프링에서 싱글턴을 저장하고 관리해주는 녀석이 applicationContext 입니다. 이 녀석의 명칭은 Singleton Registry, IOC 컨테이너, 스프링 컨테이너, 빈 팩토리 등으로 불립니다.

스프링의 핵심 컨테이너의 빈 관리를 담당하는 BeanFactory 의 핵심 구현 클래스는 DefaultListableBeanFactory입니다. 대부분의 애플리케이션 컨텍스트는 바로 이 클래스를 BeanFactory 로 사용하는데, 핵심 구현 클래스인DefaultListableBeanFactory 가 구현하고 있는 인터페이스의 한가지가 바로 SingletonRegistry 입니다.

스프링은 왜 Bean 을 Singleton 으로 생성할까?

스프링에서 하나의 요청을 처리하기 위해서는 Presentation Layer, Business Layer, Data Access Layer 등 다양한 기능을 담당하는 객체들이 계층형을 이루고 있는데, 클라이언트 요청 마다 각 로직을 담당하는 객체를 만들어 사용한다면, GC 가 있더라도 메모리 부하가 올 수 있습니다.

이 때문에 엔터프라이즈 분야에서는 서비스 오브젝트(Service Object) 라는 개념을 사용해 왔는데 서블릿은 Java 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할수 있습니다.

서블릿은 대부분의 멀티 스레딩 환경에서 싱글턴을 동작하며, 서블릿 클래스 하나당 하나의 객체를 생성하여, 클라이언트 요청 처리를 담당하는 스레드 들이 해당 객체를 공유해서 사용합니다.

스프링은 어떻게 빈을 싱글턴으로 생성할까?

방법은 간단합니다. 우리가 Bean 을 어떻게 만드는지 생각하시면 됩니다.

스프링은 어노테이션 설정만으로 IoC 컨테이너(applicationContext) 에 제어권을 넘겨줌으로써 손쉽게 빈(Bean) 을 싱글턴으로 생성하여 사용할 수 있습니다.

Component-scan 대상이 되는 어노테이션들 @Repository, @Service, @Controller, @Component 등 을 사용하면됩니다.

private 생성자를 가진 클래스도 빈으로 등록이 가능할까?

private 생성자를 가진 클래스도 스프링의 빈으로 등록해서 사용이 가능합니다. 스프링은 리플렉션을 통해서 인스턴스를 만들고, 리플렉션을 통해서라면 private 생성자를 호출해서 인스턴스를 만드는 것이 가능합니다.

하지만 추천하는 방법은 아니며, 가능하면 클래스 설계자의 의도를 존중하고, 접근방법을 지키도록 하는 것이 바람직합니다.

싱글턴 설계시 주의사항

싱글턴의 중요한 특징 중 하나가 멀티 스레딩 환경에서도 동작 가능하게 구현해야 한다고 했습니다. 즉, Thread-safe 를 보장해야 한다고 했습니다.

따라서 Thread-safe 를 보장하려면, 무상태성(stateless) 을 지켜야 합니다. 즉, 상태 정보를 클래스 내부에 가지고 있으면 안됩니다.

무상태성을 지키기 위해서는 클래스 내부에 상태 정보를 가지고 있으면 안된다고 했는데, 예외가 있습니다.

(1) 번 같은 경우는 자신의 클래스 내부에서 다른 싱글턴 빈(Singleton Bean) 을 저장하려는 용도이면 사용 가능합니다.

(2) 번과 같은 전역 변수는 메모리의 메서드 영역(Static Area) 에 저장됩니다. 메서드 영역은 스레드가 공유 가능한 영역 이므로, 여러개의 스레드가 접근하는 경우 값이 변경될 위험이 있기 때문에 Thread-safe 하지 않습니다.

따라서 (2) 번같은 경우는 지역변수나, 메서드의 매개변수로 이용하여 스레드가 공유 불가능한 스택 영역(Stack Area) 에 저장되도록하여 Thread-safe 를 보장하게끔 만들어 줘야 합니다.

즉, 싱글턴이라해도 메서드 파라미터나, 메서드 안에서 생성되는 로컬변수는 메서드가 호출될 때마다 매번 새로 할당되므로, 여러 스레드가 변수의 값을 덮어쓸 일은 없습니다.

스프링에서 synchronized 사용하는 경우도 있던데?

저도 최근에 회사에서 프로젝트를 진행하면서 synchronized 를 사용하여 동시성 문제를 해결한 경험이 있습니다.

보통 MVC 패턴을 이용하여 프로젝트를 진행할 때, serviceImpl 클래스에 @Service 어노테이션을 붙여 싱글톤으로 등록해서 사용하는데, 비지니스 로직을 처리하다 보면 여러 스레드가 접근할 때 DB에 값이 여러번 쓰여지지 않게 해야하는 경우가 있습니다.

PK 는 다르더라도 검증이 필요한 필수값(unique key 로 지정 될만한 값) 에 대해서 DB에서 테이블 락을 건다던지, 오라클의 select for update 같은 쿼리로 동시성 문제를 해결하지 않는 경우에는 synchronized 로 동시성 문제를 해결해야 합니다.

만약, 애플리케이션이 다중 서버 환경으로 구성될 가능성이 있다고 하면, synchronized 보단 DBMS 에서 동시성을 해결하는 방법이 더 안전할 것입니다.

저도 최근에 디자인 패턴을 공부하면서 싱글턴에 대해 깊게 공부한 것이라, 자바 단에서 무조건 synchronized 를 붙여야 하는 줄 알았습니다.

싱글턴을 배우고 드는 생각이, serviceImpl 은 빈이고 싱글턴이니까 Thread-safe 하므로, 메서드에 synchronized 를 빼고, DB 쪽에서 동시성문제를 해결한다면, 자바 성능을 더 높일 수 있지 않나라고 생각합니다.

결론

싱글턴 패턴에 대해서 알아봤는데, 개인적으로 디자인 패턴을 공부해야하는 이유를 한가지 더 안 것같습니다. 디자인 패턴을 공부하다 보면, 우리가 사용하고 있는 라이브러리나 프레임 워크 등에 상당히 많이 녹아있다는것을 느낄 수 있습니다.자바에서 싱글턴 패턴을 구현하려면 Thread-safe 하게 LazyHolder 방식을 사용하는 것을 추천합니다. 잘못된 내용이나, 오타, 스프링에서 synchronized 를 사용해본 경험이 있으시면 댓글로 알려주시면 감사하겠습니다.

More.

동적바인딩과 정적바인딩

Java Virtual Machine Architecture

References.

--

--

Dope
Webdev TechBlog

Developer who is trying to become a clean coder.