스프링은 어떤 방식의 싱글턴 패턴을 적용하고 있을까?

Hyunta·2022년 11월 14일
0
post-custom-banner

학습동기

1차 전환 면접 때 테코톡으로 발표했었던 싱글턴 패턴과 정적클래스와 관련된 질문을 받았다. 마지막 질문으로 그럼 스프링에서는 어떤 방식의 싱글턴 패턴을 사용하고 있냐고 질문이 와서 답변을 못했다. 대충 싱글턴 스코프를 통해서 싱글턴 패턴을 사용하지 않고 유사하게 구현하고 있다고 알고 있었는데 답변하지 못했다. 이번 기회에 한번 스프링은 어떻게 싱글턴 스코프로 빈을 관리하는지 알아보려고 한다.

싱글턴 패턴

먼저 싱글턴 패턴을 복습해보자. 구현 방법은 여러가지가 있지만 권장하는 방식은 static inner 클래스를 사용하던가, enum을 사용하는 방식이다.
자세한 설명

//static inner 클래스를 사용한 구현
public class Settings {
    private Settings() {
    }

    private static class SettingsHolder {
        private static final Settings SETTINGS = new Settings();
    }

    public static Settings getInstance() {
        return SettingsHolder.SETTINGS;
    }
}

//enum으로 구현
public enum Settings {
    INSTANCE;
}

싱글턴 패턴은 안티패턴?

싱글턴 패턴은 전역 상태로 이용할 수 있지만 문제가 있다.
1. private 생성자를 갖고 있어 상속이 불가능하다.
2. 테스트하기 어렵다
3. 서버 환경에서는 싱글턴이 1개만 생성됨을 보장하지 못한다.
4. 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

스프링에서는 싱글턴을 어떻게 보장할까?

스프링에서 싱글톤 스코프의 경우 컨테이너 하나당 빈 하나를 만든다. 따라서 스프링 컨테이너에 원하는 클래스를 요청하면 항상 같은 인스턴스를 반환해준다.

실험1. 같은 클래스를 빈으로 등록할 수 있을까?

결론부터 말하면 가능하다.

class SingletonTest {

    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    Setting setting;

    @Autowired
    DefaultListableBeanFactory beanFactory;

    @Test
    void same_bean() {
        Setting setting1 = applicationContext.getBean("setting1", Setting.class);
        System.out.println("setting1 = " + setting1);

        Setting setting2 = applicationContext.getBean("setting2", Setting.class);
        System.out.println("setting2 = " + setting2);
        
        System.out.println("setting = " + setting);

        String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();

        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
    }

    @TestConfiguration
    static class SameBeanConfig {

        @Bean
        public Setting setting1() {
            return new Setting();
        }

        @Bean
        public Setting setting2() {
            return new Setting();
        }
    }
}

-------------------------------------------------
//출력값
setting = com.example.springjpaground.singleton.Setting@49809275
setting1 = com.example.springjpaground.singleton.Setting@25e8a111
setting2 = com.example.springjpaground.singleton.Setting@21022cbb


beanDefinitionName = setting
beanDefinitionName = setting1
beanDefinitionName = setting2

여기서 추리한게 스프링은 클래스 기반의 싱글턴을 구현하지 않는다. setting 인스턴스는 총 3개가 생성됐다. 하나는 prod 에서 구현한 @Component, 나머지 2개는 @TestConfiguration에서 주입한 두개의 빈이다.

지금은 스프링 컨테이너에 클래스 타입을 요구하면서 빈을 받아오지 않는다. 만약 스프링 컨테이너에 클래스 타입을 요구하면 아래와 같은 예외를 던진다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type 'com.example.springjpaground.singleton.Setting' available: 
expected single matching bean but found 3: setting,setting1,setting2

스프링 빈으로 등록하는데는 문제가 없었지만 조회를 할 때 같은 종류의 빈이 3개나 있으니 문제가 생긴 것이다.

이름을 통해서 조회를 하다가 하나가 아닌 3개나 찾게되어서 오류가 생겼다. 따라서 스프링 컨테이너는 클래스 타입을 기준으로 유일한 인스턴스를 생성하는 것이 아니라 빈 이름을 기준으로 유일한 인스턴스를 생성한다고 알 수 있었다.

혹시 @TestConfiguration으로 해서 싱글턴이 보장되지 않은게 아닐까? 해서 두번째 실험을 해봤다.

실험2. @Configuration을 할 때 Singleton 확인?

    @Configuration
    static class SameBeanConfig {

        @Bean
        public Setting setting1() {
            return new Setting();
        }

        @Bean
        public Setting setting2() {
            return new Setting();
        }
    }
    
org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'com.example.springjpaground.singleton.SingletonTest': 
Unsatisfied dependency expressed through field 'setting'; 
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.example.springjpaground.singleton.Setting' available: 
expected single matching bean but found 2: setting1,setting2

@TestConfiguration@Configuration 으로 바꾸자 빈을 가져올 때 문제가 발생하는 것이 아니라 빈을 생성할 때 문제가 생겼다. setting2를 주석처리하고 테스트를 돌렸더니 정상적으로 실행됐다. 그리고 조금 신기한 결과가 나왔는데

setting1 = com.example.springjpaground.singleton.Setting@31e72cbc
setting = com.example.springjpaground.singleton.Setting@31e72cbc

beanDefinitionName = setting1

setting1과 setting의 인스턴스값이 같아졌다. Setting에는 @Component가 붙어있고 @Configuration을 통해서 Setting을 @Bean으로 주입시켜줬는데, 스프링은 거의 수동작업이 우선순위를 가지도록 구현되어있어서 setting1만 빈으로 등록이 됐다.

다시 돌아가서 그럼 @Configuration을 통해서 빈을 등록할 때 싱글턴임을 확인한다고 추리하고 예외를 한번 조사해봤다.

실험3. 테스트코드에서 @SpringBootTest를 통해 필요한 빈을 주입하는 과정

먼저 SpringBootDependencyInjectionTestExecutionListner를 통해서 필요한 의존성을 주입한다.

의존성을 주입하다 AutowiredAnnotationBeanPostProcessor 에서 해당 catch문에서 잡혀서 예외를 반환한다.

요청하는 클래스 타입과 일치하는 빈을 autowired를 체크하는 시점에 가져온다.

Setting으로 등록한 빈이 2개 이상이므로 아래 if문을 통해 resolveNotUnique를 통해 예외를 발생하게된다.

따라서 @Configuration을 통해 빈을 등록할 때 같은 클래스타입의 빈들을 조회하고 unique하지 않다면 예외를 던지는 방식으로 구현되어있다.

정리

스프링은 엄밀히 말하면 싱글턴 패턴을 구현하고 있지 않다.
@Configuration을 통해 빈을 등록할 때 같은 타입의 빈은 등록되지 않도록 구현되어있음을 알 수 있었다.

profile
세상을 아름답게!
post-custom-banner

0개의 댓글