S E P H ' S

[Spring] Spring 에서의 싱글톤 패턴 본문

Programing & Coding/Spring

[Spring] Spring 에서의 싱글톤 패턴

yoseph0310 2023. 3. 12. 18:07

싱글톤 컨테이너(Singleton Container)

싱글톤 컨테이너란 클래스의 인스턴스가 Java JVM 내에 단 하나만 존재하는 것을 의미한다.

웹 애플리케이션은 수많은 클라이언트에서 서비스를 요청받게 되는데 만약 서버에서 클라이언트의 요청을 받을때마다 클래스 인스턴스를 생성하게 되면 JVM 메모리 사용량이 증가하게 되고 서버 부하가 발생할 것이다.

 

예제로 함께 확인해보자. 예제는 Spring 프로젝트가 아닌 일반 Java 프로젝트로 만들어져있다.

Java 프로젝트로 만들어져 있다고 해서 잘못된 것이 아닌가? 하는 것이 아니라 Spring에서 어떻게 싱글톤을 쓰는지에 초점을 두고 감안하여 읽으면 좋을 듯 하다.

 

AppConfig.java
MemberService.java

먼저 AppConfig.java 에서 MemberService를 빈으로 등록했고 MemberService는 인터페이스이다. main 메소드에서 빈으로 등록된 MemberService를 호출해보면 어떤 일이 발생할까?

main 메소드 실행부

 

NoneSingleton main 메소드 실행결과

실행 결과는 호출이 될때 마다 MemberService의 클래스 인스턴스가 새로 생성되는 것을 확인할 수 있다.

즉, 호출을 반복할때마다 JVM 메모리 사용량은 증가하게 되는 것이다.

이를 해결하는 방법은 AppConfig에서 생성한 MemberService 객체를 공유하는 것이다.

 

MemberServiceImpl.java

MemberServiceImpl의 Instance를 Eager Initialization(Singleton 적용 방법 중 하나. 참고 링크) 해주었다.

우선 이렇게 하면 AppConfig.java에서 접근이 불가능하다는 메시지를 보낼 것이다.

그래서 다음과 같이 수정해주고 Test 코드를 작성해보겠다.

새로운 객체를 생성하는 것이 아닌 getInstance()를 이용해 동일한 객체를 가져오고 있다.
동일한 객체를 가져오고 있다.

여기까지가 순수 Java 코드로 싱글톤 패턴을 적용하는 방법이었다. 하지만 실제로는 사용하기 불편하고 실질적 문제점들이 존재한다.

  • 싱글톤 패턴을 구현하기 위한 코드가 늘어남
  • 인스턴스를 반환해주는 구현 클래스를 직접 참조해야 하므로 DIP 원칙을 위반한다. 또한 경우에 따라 OCP 원칙을 위배할 수도 있다.
  • 내부 속성을 변경, 초기화하기가 어렵다.
  • private 생성자로 자식 클래스를 생성하기 어렵다.
  • 유연성이 떨어진다.

 

스프링에서의 싱글톤 컨테이너 (Singleton Container)

스프링에서는 사용자자가 이렇게 일일이 구현하지 않고도 싱글톤 패턴의 문제점들도 보완해주면서 싱글톤 패턴으로 클래스의 인스턴스를 사용하게 해주는데 이것을 싱글톤 컨테이너라고 한다.

 

스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다. 그래서 스프링 컨테이너는 싱글톤 패턴의 문제를 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다. @Bean이라던지 여지껏 사용했던 스프링 빈이 바로 싱글톤 패턴으로 관리되는 빈이다.

 

Spring DI Container

 

실제 사용 예시

Spring에서는 필요한 Bean 객체들, 즉 프로젝트 전반적으로 동일하게 적용되어야 하는 설정 같은 것들을 Config라는 패키지로 두어 관리한다. 예를 들면 SpringSecurityConfig, WebConfig, SwaggerConfig와 같은 것들이다. 그렇다면 당연히 많은 곳에 적용되는 코드들이니 클래스 인스턴스가 하나이면 굉장히 합리적일 것이다. 실제 사용 예시를 통해 어떻게 Spring이 싱글톤을 가능하게 하는지 설명하도록 하겠다.

@Configuration
public class SecurityConfig {

	...
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.addAllowedOrigin("http://localhost:3000");
        configuration.addAllowedOrigin("https://promise-precure.site");
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
    
	...
    
}

 

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestSecurityConfig.class);

        CorsConfigurationSource corsConfigurationSource1 = ac.getBean("corsConfigurationSource", CorsConfigurationSource.class);
        CorsConfigurationSource corsConfigurationSource2 = ac.getBean("corsConfigurationSource", CorsConfigurationSource.class);

        System.out.println("corsConfigurationSource 1 : " + corsConfigurationSource1);
        System.out.println("corsConfigurationSource 2 : " + corsConfigurationSource2);

        assertThat(corsConfigurationSource1).isSameAs(corsConfigurationSource2);
    }
}

 

 

SecurityConfig에 등록된 CorsConfigurationSource의 Bean 객체를 호출을 여러번 해도 같은 객체를 참조하고 있음을 알 수 있다. 이렇게 별다른 Singleton 구현이 없어도 @Configuration 어노테이션은 싱글톤으로 관리한다. @Configuration은 바이트 코드 조작을 통해 싱글톤 패턴을 유지하는데 CGLIB이라는 라이브러리를 사용한다. 이는 Config(예제에선 SecurityConfig)를 상속받은 임의의 다른 클래스를 만들고 해당 클래스를 스프링 빈으로 등록한다.