Spring

[Spring] 스프링 싱글톤 컨테이너(Singleton Container)

cloud-grace 2024. 6. 6. 15:22

싱글톤이 등장한 배경

스프링은 엔터프라이즈 웹 애플리케이션 기술을 지원하는 프레임워크이다. 주로 여러 클라이언트가 동시에 요청을 한다. 요청이 들어올 때마다 새로운 객체를 생성하는 것은 비효율적이다.

 

싱글톤 패턴

싱글톤 패턴 포스팅 링크

 

[디자인 패턴] 싱글톤 패턴(Singleton Pattern)

싱글톤 패턴(Singleton Pattern)이란?싱글톤 패턴은 디자인 패턴(Design Pattern) 중 생성 패턴(Creational Pattern)이다.생성 패턴 : 객체의 생성과 관련된 패턴이며, 객체의 생성 절차를 추상화하는 패턴객체

cloud-grace.tistory.com

싱글톤 패턴에 대한 자세한 내용은 위 포스팅을 참고하자.

 

싱글톤 예제 코드

  • 구현 방법은 여러 가지이지만, 객체를 미리 생성해두는 가장 단순하면서도 안전한 방법을 이용하자.
  • Singleton 클래스 안에서 private static final으로 미리 객체를 생성하고 이를 참조하는 instance 변수를 만든다.
  • static 영역에 딱 1개의 인스턴스만 생성된다.
  • getInstance()로 외부에서 인스턴스를 조회한다.
  • 외부에서 new로 객체 생성을 막도록 private 생성자를 만든다.
public class Singleton {
    private static final Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {
        System.out.println("Singleton 객체 생성");
    }

    public void logic() {
        System.out.println("Singleton 객체 로직 호출");
    }
}
  • Singleton 인스턴스 호출 시 두 객체가 같은지 테스트 한다.
  • 이렇게 싱글톤 패턴은 여러 요청이 들어왔을 때 객체 생성을 하는 것이 아닌, 이미 만들어진 객체를 공유하여 효율적으로 사용한다.
import static org.junit.jupiter.api.Assertions.assertSame;
import org.junit.jupiter.api.Test;

public class SingletonTest {

    @Test
    public void testSingletonInstance() {
        // 싱글톤 객체
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();

        // 같은 객체인지 확인
        assertSame(instance1, instance2, "두 객체는 같습니다.");
    }
}

 

싱글톤 패턴의 문제점

  • 싱글톤 코드 구현 자체가 많고, 구체 클래스에 의존하여 DIP, OCP에 위반된다.
  • 테스트를 유연하게 하기 어렵고, 내부 속성을 바꾸고 초기화하기도 어렵다.
  • private 생성자로는 자식 클래스를 만들기에 어렵다.

 

스프링 싱글톤 컨테이너

출처 : 인프런 김영한님 강의 [스프링 핵심 원리 - 기본편]

  • 스프링 컨테이너(Spring Container)는 싱글톤 패턴의 문제점을 해결하면서도 객체 인스턴스를 1개만 생성하여 관리할 수 있다.
  • 스프링 빈(Spring Bean)이 싱글톤으로 관리된다.
  • 즉, 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
  • 싱글톤 컨테이너 역할을 하며, 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
  • 고객의 요청이 올 때마다 객체를 생성하는 것이 아닌, 이미 만들어진 객체를 공유하여 재사용할 수 있어 효율적이다.
스프링의 기본 빈 등록은 싱글톤 방식을 활용하지만, 요청할 때마다 새로운 객체를 생성하여 반환할 수 있는 Prototype Scope, Web 관련 Scope 등의 방식도 있다.

 

스프링 싱글톤 컨테이너 Test

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import singleton.AppConfig;
import singleton.ExampleService;

public class SingletonTest {

    @Test
    @DisplayName("스프링 싱글톤 컨테이너 Test")
    void springContainer() {
        ApplicationContext appConfig= new AnnotationConfigApplicationContext(AppConfig.class);
        ExampleService exampleService1 = appConfig.getBean(ExampleService.class);
        ExampleService exampleService2 = appConfig.getBean(ExampleService.class);

        System.out.println("exampleService1 : " + exampleService1);
        System.out.println("exampleService2 : " + exampleService2);

        Assertions.assertThat(exampleService1).isSameAs(exampleService2);
    }
}

 

Test 결과

  • 싱글톤 관련 코드를 따로 작성하지 않았지만 스프링 컨테이너가 빈을 싱글톤으로 관리하는 것을 알 수 있다.
<Test 결과>
exampleService1 : singleton.ExampleService@5e21e98f
exampleService2 : singleton.ExampleService@5e21e98f

 

싱글톤 방식 주의할 점

  • 싱글톤 객체는 전역에서 공유되므로 멀티쓰레드 환경의 동시성 문제가 있다.
  • 메모리 Heap 영역에서 프로세스 전체에 공유된다.
  • 동시성 문제로 인해 발생하는 문제가 많기 때문에 객체 상태를 Stateful하게 설계하면 안된다.

 

무상태(Stateless)

  • stateful로 상태를 유지하지 않는다.
  • 읽기만 가능하도록 하는 것이 좋다.
  • 특정 Client에 의존적이거나 값을 바꿀 수 있는 field가 없어야 한다.
  • field 말고 지역 변수, 파라미터, ThreadLocal 등을 사용해야 한다.

 

무상태 설계를 실패한 예제 코드

  • 여러 클라이언트가 주문을 요청하고, price 필드가 공유되는데 변경될 때 A의 price가 20000으로 출력되는 예제 코드이다.
import org.springframework.stereotype.Component;

@Component
public class ExampleService {
    private int price; // Stateful price

    public int getPrice() {
        return price;
    }

    public void order(String name, int price) {
        System.out.println("name: " + name + " price: " + price);
        this.price = price; // 문제 발생(특정 클라이언트로 인해 가격 값이 변경된다.)
    }
}
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import singleton.AppConfig;
import singleton.ExampleService;

public class StatefulTest {

    @Test
    @DisplayName("Stateful Test")
    void springContainer() {
        ApplicationContext appConfig= new AnnotationConfigApplicationContext(AppConfig.class);
        ExampleService exampleService1 = appConfig.getBean(ExampleService.class);
        ExampleService exampleService2 = appConfig.getBean(ExampleService.class);

        exampleService1.order("A", 10000);
        exampleService2.order("B", 20000);

        int price = exampleService1.getPrice();
        System.out.println("A의 price: " + price);

        Assertions.assertThat(exampleService1.getPrice()).isEqualTo(20000);
    }
}
  • A의 price가 20000으로 출력된다.

 

  • 다같이 공유되는 변수를 제고하고, 해당 메서드 내에서만 지역 변수를 사용할 수 있도록 바꿔야 한다.

 

@Configuration과 바이트 코드 조작

출처 : 인프런 김영한님 강의 [스프링 핵심 원리 - 기본편]

  • @Configuration : 설정 파일을 만드는 어노테이션이며 Bean을 등록한다.
  • @Configuration을 적용한 AppConfig는 바이트 코드를 조작한 CGLIB라는 라이브러리를 활용하여 AppConfig를 상속받은 임의의 클래스를 만들며, 그것을 스프링 빈으로 등록한다.
hello.core.AppConfig$$EnhancedBySpringCGLIB$$pc917d12
  • @Configuration을 사용하지 않고 @Bean만 사용하면 빈 등록은 되지만 싱글톤 보장은 되지 않는다.
  • 즉, @Configuration을 설정할 때 사용해야 싱글톤을 보장받을 수 있다.

 

 

 

참고 자료

이미지 출처 및 내용 참고 : 인프런 김영한 님의 강의 "스프링 핵심 원리 - 기본편"

https://scoring.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-Configuration

https://velog.io/@syleemk/Spring-Core-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88

https://dream-and-develop.tistory.com/419