Spring
[Spring] 스프링 싱글톤 컨테이너(Singleton Container)
cloud-grace
2024. 6. 6. 15:22
싱글톤이 등장한 배경
스프링은 엔터프라이즈 웹 애플리케이션 기술을 지원하는 프레임워크이다. 주로 여러 클라이언트가 동시에 요청을 한다. 요청이 들어올 때마다 새로운 객체를 생성하는 것은 비효율적이다.
싱글톤 패턴
싱글톤 패턴에 대한 자세한 내용은 위 포스팅을 참고하자.
싱글톤 예제 코드
- 구현 방법은 여러 가지이지만, 객체를 미리 생성해두는 가장 단순하면서도 안전한 방법을 이용하자.
- 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을 설정할 때 사용해야 싱글톤을 보장받을 수 있다.
참고 자료
이미지 출처 및 내용 참고 : 인프런 김영한 님의 강의 "스프링 핵심 원리 - 기본편"