스프링에서의 싱글톤 관리

허준현·2023년 7월 10일
0

JAVA

목록 보기
3/5
post-thumbnail

매번 클라이언트에서 요청이 올 때마다 각 로직을 처리하는 빈을 새로 만들어서 사용한다고 생각해보자.
요청 1번에 10개의 객체가 만들어진다고 하고, 1초에 500번 요청이 온다고 하면 초당 5000개의 새로운 객체가 생성된다.
해당 건은 GC에게 부하가 걸리면 감당이 힘들 것이다.

그래서 이러한 문제를 해결하고자 해당 객체를 싱글톤으로 선언하여 여러 쓰레드에서 공유할 수 있도록 하는 것이다.

이제 자바에서 일반적으로 사용하는 singleton 을 알아보고 스프링에서 어떻게 빈을 singleton 으로 생성하고 관리하는 지 알아보자.

자바에서 사용하는 singleton

자바에서 사용하는 singleton 에서는 많게는 7개 정도 이야기를 하고 있으나 여기서는 주로 많이 봐왔던 3가지의 경우에 대해서 알아보도록 하자.

1.Lazy Initialization

바로 singleton instance를 생성하지 않고 해당 객체가 불러 질 때 생성하는 방법이다.

public class Singleton {
 
    private static Singleton instance;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

하지만 해당 싱글톤 객체 생성은 Multi-thread 환경에서 여러개의 쓰레드가 동시 접근하게 되는 경우 여러개의 인스턴스가 생성될 수 있다는 문제점이 있다.

2.Thread Safe Singleton

1번 싱글톤 선언하는 부분에서 동기화 문제를 해결하기 위해 synchronized를 활용하여 동기화 문제를 해결해 보도록 하자.

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

synchronized 키워드를 사용함으로서 임계영역(Critical Section) 을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 해준다. 하지만 해당 키워드를 사용한 동기화 작업은 자바 입장에서 부하가 커서 어플리케이션의 성능을 저하할 수 있다.

3.Bill Pugh Singleton Implementaion

이런 성능 부하를 줄이기 위해서 Bill Pugh Singletion Solution이 있다.

public class Singleton {
 
    private Singleton(){}
    
    private static class SingletonHelper{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}

inner class로 인해 복잡해 보일 수 있지만 private inner static class를 두어 싱글톤 인스턴스를 갖게 한다.
SingletonHelper 클래스는 Singleton 클래스가 Load 될 때에도 Load 되지 않다가 getInstance()가 호출됐을 때 비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 된다.

왜 바로 JVM에 올라가지 않고 호출 이후에 로드 될까?

이곳 에서의 내용을 참고하였으며 간략하게 알아보면 JVM 에 클래스 정보가 저장되는 일의 순서는 로드, 링크 , 초기화 과정을 지니고 있으며 static으로 선언되어있다고 하더라도 모든 static 클래스를 초기 메모리에 올리는 것이 아닌 그때마다 필요한 클래스를 메모리에 올려 효율적으로 관리 한다.

추가적으로 내부 클래스를 static 을 붙이지 않고 선언하게 된다면 GC에 해당 클래스가 포함되지 않아 메모리 누수를 발생한다.

해당 방식이 Mutil-Thread에 안전한 방법인가?

stackoverflow 에서 알 수 있듯이
클래스의 정적 변수는 스레드로부터 안정한 방식으로 치고화 되며 해당 싱글톤 패턴에 접근한 쓰레드는 객체가 생성될 때 까지 대기하게 된다. 따라서 멀티 쓰레드에서 안전한 방식이다.
사진 추가하기

싱글톤을 사용했을 때 단점

싱글톤을 사용하면 발생하는 단점은 다음과 같다.

클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.

따라서 상속할 수 없으므로 객체지향의 장점인 상속과 이를 이용한 다형성(polymorphism)을 적용할 수 없다.

생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.

싱글톤은 사용하는 클라이언트가 정해져 있지 않으며, 아무 객체나 자유롭게 접근하고 공유할 수 있는 전역 상태를 갖는 것은 객체지향 프로그래밍에서는 권장되지 않는 프로그래밍 모델이다.

스태틱 팩토리 메서드인 getInstance()를 만들고 이 메서드가 최초로 호출되는 시점에서 한번만 오브젝트가 만들어지게 한다.

싱글톤은 테스트하기도 힘들다

만들어지는 방식이 제한적이라서 테스트에서 사용될 때 모의(Mock) 객체 등으로 대체하기가 힘들다. 초기화 과정에서 생성자 등을 통해 사용할 오브젝트를 다이나믹하게 주입하기도 힘들기 때문에 필요한 오브젝트는 직접 오브젝트를 만들어 사용할 수밖에 없다.

스프링에서 사용하는 singleton

먼저 스프링에서 사용하는 Bean 이 singleton으로 관리되는지 알아보자.

스프링에서는 빈 팩토리(BeanFactory) 를 통해서 Bean을 생성하고 의존관계를 설정하는 기능을 담당하는 가장 기본적인 IOC 컨테이너 이다.

Bean Factory를 상속받고 있는 ApplicationContext는 추가적으로 프로파일을 설정하고 소스 설정 및 프로퍼티값을 가져오는 것과 다국어를 지원하기 위한 MessageSource를 제공한다.

빈을 생성하는 과정을 알아보았고 이제 빈을 등록하는 과정을 알아보자.

@SpringBootApplication 에는 @SpringConfiguration 어노테이션이 포함되어 있는데 이는 같은 이름으로 Bean 등록이 되는 경우 우선순위에 맞는 Bean을 등록하거나 아니면 등록과정에서 오류가 나도록 한다.
이를 통해서 사용자로 하여금 객체지향적으로 프로그래밍을 할 수 있도록 도와주며 내부 복잡한 로직을 몰라도 쉽게 빈들을 생성, 관리 할 수 있게 도와준다.

스프링 빈 레지스트리

위에서 언급한 스프링에서 SingleTon을 관리하는 부분은 스프링 빈 레지스트리이다.

이는 스태틱 메서드와 private 생성자를 사용해야 하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해주어 public 생성자를 구현할 수 있어서 생성자 파라미터로 의존관계 주입이 가능하고, 테스트 시 모의 객체도 생성이 가능하다.

구현코드 부분

일반적으로 스프링에서 빈 레지스트리를 관리하는 경우에
package org.springframework.beans.factory.support 경로에 있는 DefaultSingletonBeanRegistry를 사용하게 된다. 해당 코드는 Spring Boot 2.6.2 버전을 기준으로 한다.

이곳 에서 확인 할 수 있듯이 ApplicationContext가 상속하고 있는 GenericApplicationContext 클래스 부분에서DefaultSingletonBeanRegistry를 통해서 빈을 관리하고 있다.

이제 해당 클래스에서 자세히 알아보도록 하자

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

	/** Maximum number of suppressed exceptions to preserve. */
	private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;


	/** Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

	/** Set of registered singletons, containing the bean names in registration order. */
	private final Set<String> registeredSingletons = new LinkedHashSet<>(256);

	/** Names of beans that are currently in creation. */
	private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));

	/** Names of beans currently excluded from in creation checks. */
	private final Set<String> inCreationCheckExclusions =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}

해당 클래스의 필드를 확인해보면 기존에 있었던 빈과 새롭게 추가되는 빈들에 대해서 동기화를 하기 위해 일반 HashMap이 아닌 ConcurrentHashMap을 사용하는 것을 알 수 있다.
초기에 빈을 등록하는 registerSingleton 메소드 부분을 확인해보자

@Override
	public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
		Assert.notNull(beanName, "Bean name must not be null");
		Assert.notNull(singletonObject, "Singleton object must not be null");
		synchronized (this.singletonObjects) {
			Object oldObject = this.singletonObjects.get(beanName);
			if (oldObject != null) {
				throw new IllegalStateException("Could not register object [" + singletonObject +
						"] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");
			}
			addSingleton(beanName, singletonObject);
		}
	}

앞 서 자바에서 싱글톤을 선언하는 방법 중에서 2.Thread Safe Singleton 동기화를 활용하여 Bean을 등록하고 관리 하는 것을 알 수 있다.

마무리

자바에서 heap 메모리를 관리하기 위해 사용하는 싱글톤 패턴에 대해서 알아보고
여러가지 싱글톤 기법에 대해서 알아보며 그 중에서 스프링에서는 어떤 방식으로 싱글톤을 구현하고 관리하는 것에 대해 알아보았다.

참고

https://mangkyu.tistory.com/210
스프링 컨테이너
싱글톤 레지스트리
클래스 로더
stackoverflow

profile
best of best

0개의 댓글