[스프링/Spring] 싱글톤(Singleton) 패턴과 스프링 컨테이너

kindof

·

2021. 8. 23. 14:18

📖 싱글톤 패턴이란 무엇일까?

스프링은 기업용 온라인 서비스 기술을 지원하기 위해 탄생했고, 웹 어플리케이션은 보통 여러 고객이 동시에 요청을 보냅니다.

 

 

 

위와 같은 그림의 예시에서 세 명의 클라이언트가 동시에 어떤 요청을 보내게 되면, DI 컨테이너는 memberService를 생성해서 반환해줍니다. 하지만 이 상황에서는 고객의 요청만큼 객체가 계속 생성되고, JVM 메모리에 객체가 계속 쌓이는 문제가 발생합니다.

 

아래 테스트 코드와 결과를 통해 확인해보죠.

public class SingletonTest {

    @Test
    @DisplayName("스프링이 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        // 1. 조회: 호출할 때마다 객체를 생성한다.
        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();

        // 2. 호출 할 때마다 다른 객체가 생성된다.
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);

    }
}

테스트코드 - 클라이언트 호출마다 객체 생성

따라서 이런 문제를 해결하기 위해서는 해당 객체를 딱 한 번만 생성하고 이 객체를 공유해서 사용하게 해야 합니다. 그리고 이러한 디자인 패턴을  싱글톤 패턴이라고 합니다.

 

 

💻 싱글톤 패턴의 적용

싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되도록 보장하는 디자인 패턴입니다. 이를 위해서는 객체 인스턴스를 2개 이상 생성하지 못하도록 해야 하는데, 우리는 private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 하면 됩니다.

 

무슨 말인지 아래 코드를 통해 확인해보겠습니다.

public class SingletonService {

    // static 선언: 컴파일 시점에 static 영역에 instance 객체 생성
    private static final SingletonService instance = new SingletonService();

    // 조회할 때마다 이미 생성된 객체를 리턴
    public static SingletonService getInstance(){
        return instance;
    }

    // 생성자를 private으로 선언하여 다른 곳에서 생성 불가능
    private SingletonService(){
    }

    public void doSomething(){
        System.out.println("싱글톤 객체의 작업을 수행합니다.");
    }
}

주석에서 설명한 것처럼 우리가 사용할 컨테이너 객체를 SingletonService라고 명명하고, instance라는 객체를 static 영역에 생성하겠습니다.

 

그러면 해당 객체는 컴파일 시점에 메모리의 static 영역에 올라가게 될 것이고, 다른 곳에서 해당 인스턴스를 조회하거나 사용하고 싶을 때는 getInstace() 메서드를 이용할 수 있게 됩니다.

 

다만, 위와 같은 방식으로 컴파일 시점에 인스턴스를 생성하게 되면 만약 해당 인스턴스를 사용하지 않더라도 메모리를 잡아먹게 되는 단점은 존재합니다.

 

한편, 아래 그림처럼 생성자를 이미 private로 막아두었기 때문에 다른 곳에서 SingletonService 인스턴스를 또 생성하려고 하면 에러가 발생하게 됩니다.

 

private 생성자를 통해 외부에서 객체 생성 금지

 

또한 실제로 getInstace()를 통해 싱글톤 객체를 가져오게 되면, 여러 번 호출을 해도 같은 객체를 두고 사용할 수 있게 되죠.

 

아래 테스트 코드를 통해 결과를 확인해보겠습니다.

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest(){
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        // 같은 객체 인스턴스가 반환된다.
        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
    }

우리가 의도한 것처럼 하나의 SingletonService 인스턴스가 클라이언트의 호출(getInstace())마다 리턴되어 사용되는 것을 알 수 있습니다.

 

이렇게 순수한 자바 코드만으로 싱글톤 패턴을 구현하여 객체를 사용할 수 있음을 살펴보았는데요. 그렇다면 스프링에서는 이러한 싱글톤 패턴을 어떻게 구현할까요?

 

 

💡 싱글톤 패턴과 스프링 컨테이너

다시 맨 처음으로 돌아가서, 지금까지 흐름은 우리의 스프링 어플리케이션은 동시에 여러 클라이언트의 요청을 처리해야 하기 때문에 싱글톤 패턴이 필요하다는 것이었습니다.

 

그러면 위에서 설명한 방식처럼 각 Bean을 정적으로 생성해두고, private 키워드로 다시 생성하여 사용하는 것을 막는 코드를 작성해줘야 할까요? 

 

답은 '그렇지 않다'입니다.

 

만약 각각의 Bean들에 대해 위와 같은 코드를 일일이 다 작성해주어야 한다면 너무 번거로울 것입니다. 다만 엄밀히 말하자면, 위와 같은 패턴의 코드를 작성해줘야 하는 것은 맞지만 우리가 직접 작성하는 것이 아니라 스프링 컨테이너가 제공하는 기능으로 자동화된다는 것인데요.

 

스프링 컨테이너에서는 자체적으로 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하며, 이를 싱글톤 레지스트리라고 합니다. 그리고 우리가 만든 스프링 빈들이 바로 싱글톤으로 관리되고 있는 빈이 되는 것이죠.

 

 

자동이라는 뜻입니다. 자동!

 

 

우선 위 설명이 무엇을 뜻하는지 스프링 컨테이너를 통해 싱글톤 객체를 사용하는 테스트 코드를 작성해보겠습니다.

 

지난 시간에 살펴본 ApplicationContext은 스프링 컨테이너로써 Bean Factory를 상속하여 Bean 객체를 생성하고 관리는 기능을 가지고 있습니다. 다만, Bean Factory는 컨테이너가 구동될 때 Bean 객체를 생성하는 것이 아니라 클라이언트의 요청에 의해서 Bean 객체가 사용되는 시점에 객체를 생성하는 방식(Lazy Loading)을 사용하는 데 반해, ApplicationContext는 컨테이너가 구동되는 시점에 객체들을 생성하는 Pre-Loading 방식을 사용하게 됩니다.

 

우리는 ApplicationContext 스프링 컨테이너를 이용하여 스프링 빈을 등록하고 두 번의 memberService를 호출해보겠습니다. 그러면 위에서 말한 싱글톤 패턴 방식으로 호출할 때마다 같은 memberService 객체가 리턴됩니다.

    @Test
    @DisplayName("스프링 컨테이너를 사용한 싱글톤 패턴")
    void springContainer(){
        //AppConfig appConfig = new AppConfig();
        // 스프링 컨테이너를 생성
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        // 2. 호출 할 때마다 같은 객체가 반환된다.
        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }

.

.

.

 

그런데 아래에서 스프링 빈으로 등록하는 코드를 살펴보면 memberSerivce와 orderSerivce가 각각 MemoryMemberRepository()를  new로 생성해서 주입받는데, MemberRepository는 그냥 new MemoryMemberRepositry()를 return하고 있습니다.

 

그러면 결국 MemoryMemberRepository()가 두 번 호출되어서 싱글톤 패턴이 깨지는 것이 아닐까요?

 

 

하지만, 실제로 테스트 코드를 작성해서 실행해보면 두 번 new로 호출된 memberRespoitory()는 같은 객체인 것을 알 수 있습니다.

 

스프링 컨테이너는 어떻게 이러한 기능을 제공할 수 있을까요? 이는 바로 @Configuration 어노테이션을 통해 Bean을 조회해서 스프링 컨테이너에 집어넣을 때 일련의 과정을 거치기 때문입니다.

 

 

🤭 @Configuration과 바이트코드 조작

우리가 사용하고 있는 스프링 컨테이너 AppConfig를 스프링 빈으로 뽑아서 출력해보면 아래와 같습니다.

public class ConfigurateionSingletonTest {
    @Test
    void configureationDeep(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
    }
}

출력 결과를 보면 "$$EnhancerBySpringCGLIB$$369b2ce0" 이라는 내용이 있죠.

 

이는 내가 직접 만든 AppConfig가 스프링 빈으로 등록된 것이 아니라, 스프링이 CGLIB라는 바이트 코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것입니다.

 

그리고 이렇게 등록된 AppConfig@CGLIB가 싱글톤 패턴을 보장하도록 도와주는데, 그 안에서 스프링 컨테이너에 등록된 객체를 호출하면 기존의 빈을 리턴해주는 로직을 담고 있는 것입니다.

 

따라서 우리는 스프링 컨테이너를 그냥 사용해도, 그 안에서 자동적으로 싱글톤 패턴을 지켜주는 로직이 실행되는 것이죠.

 

👀 나가면서

지금까지 설명한 내용을 요약하면 아래와 같은 그림이 됩니다.

스프링 컨테이너로 인해 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용이 가능해집니다. 이상으로 싱글톤 디자인 패턴과 자바에서 실습, 스프링 컨테이너를 적용한 실습을 진행해보았습니다.

 

감사합니다.