싱글톤 패턴이란 간단하게 말하자면 클래스의 인스턴스가 하나만 존재해야한다는 패턴이다. 아래에서 좀 더 자세히 알아보자.
웹 애플리케이션과 싱글톤
스프링은 주로 웹 애플리케이션에 많이 사용된다.
일반적인 웹 애플리케이션은 여러명의 사용자가 요청을 날린다.
그런데 우리가 이전에 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성한다.
사용자가 많아지면 그 방식은 메모리 낭비가 심하다.
따라서 '싱글톤 패턴'을 이용해 객체 딱 1개만 생성해서 공유하도록 한다.
싱글톤 패턴
그럼 이제 싱글톤 패턴이 무엇인지 자세히 알아보자.
한마디로 하자면 싱글톤 패턴은 '클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴'이다.
아래와 같이 클래스를 정의하면 싱글톤 패턴이 적용된 모습이다.
public class SingletonService {
// JVM이 뜰때 처음에 SingletonService 인스턴스를 요기서 생성해서 아래의 instance 변수에 참조를 넣어둠
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
// 다른 곳에서 생성자로 SingletonService의 instance를 생성할 수 있으므로
// private으로 생성자를 정의 -> 외부에서 instance 생성을 할 수 없음
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
포인트는 3가지다.
1. 첫 줄에 static으로 instance를 생성했으니, 처음 JVM이 뜰 때 instance가 하나 생성된다.
2. 두번째로 있는 getInstance 메서드를 통해 1에서 생성된 instance를 가져올 수 있다. 이 때 이 메서드는 호출될 때마다 항상 같은 instance를 반환한다.
3. 딱 1개의 instance만 존재해야 하므로 생성자를 private으로 설정해서 외부에서 new로 생성할 수 없도록 한다.
이렇게 싱글톤 패턴을 적용하는 것은 스프링 컨테이너의 역할 중 하나다.
스프링은 사용자의 요청이 올때마다 객체를 생성하지 않고 이미 만들어진 객체를 공유해서 효율적으로 사용한다.
물론 이런 싱글톤에도 문제점들이 있다.
- 싱글톤 패턴 구현하는 코드 자체가 많음
- 의존관계상 클라이언트가 구체 클래스에 의존 -> DIP 위반
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙 위반할 가능성 높음
- 테스트하기 어려움 -> 인스턴스 미리 다 받아와서 이미 설정 끝난거임. 유연하게 테스트 어려움
- 내부 속성 변경하거나 초기화 하기 어려움
- private 생성자를 쓰기 때문에 자식 클래스를 만들이 어려움
- 결론적으로 유연성이 떨어짐
- 그래서 안티패턴으로 불리기도 함
근데 스프링 컨테이너는 이런 문제점을 해결해주면서 싱글톤 패턴을 활용한다.
싱글톤 컨테이너라고도 불리는 스프링 컨테이너는 싱글톤 패턴을 어떻게 활용할까?
싱글톤 컨테이너
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
이전에 배웠던 스프링 빈이 싱글톤으로 관리되는 빈이다. 이렇게 스프링 컨테이너가 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다. 이 기능 덕분에 스프링 컨테이너는 '코드가 많아지는', 'DIP, OCP, 테스트, private 생성자' 문제점으로부터 자유롭게 싱글톤을 사용할 수 있다.
싱글톤 방식의 주의점
싱글톤 방식은 여러개의 클라이언트가 하나의 객체를 공유하기 때문에 상태를 유지하게 설계하면 안됨!
즉, 무상태(stateless)하게 설계해야한다. 되도록이면 필드를 쓰지 않고 지역변수, 파라미터, ThreadLocal 등 자바에서 공유되지 않는 값들을 사용해야 한다.
예를들어 아래와 같이 설계한 싱글톤은 상태를 유지하는 필드가 있으므로 좋지 않은 설계다.
public class StatefulService {
private int price; // 상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; // 여기가 문제!
}
public int getPrice() {
return price;
}
}
테스트를 해보면 사용자A와 사용자B가 같은 price라는 필드를 사용해서 문제가 생기는 것을 볼 수 있다.
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA: A 사용자가 10000원 주문
statefulService1.order("userA", 10000);
// ThreadB: B 사용자가 20000원 주문
statefulService2.order("userB", 20000);
// ThreadA: 사용자 A 주문 금액 조회
int price1 = statefulService1.getPrice();
System.out.println("price = " + price1); // 20000원이 나옴. B 사용자가 바꿔버리기 때문
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
@Configuration과 싱글톤
@Configuration과 바이트코드 조작의 마법
스프링 컨테이너는 싱글톤 레지스트리(스프링 컨테이너가 싱글톤 객체를 생성하고 관리하는 기능)다. 그래서 스프링 빈이 싱글톤이 되도록 보장해줘야 한다. 근데 스프링이 자바 코드까지 어떻게 하긴 어렵다. 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
아래와 같이 테스트를 적고 출력해보자.
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
이렇게 나온다.
hello.core.AppConfig.. 뒤에 $$EnhancerBySpringCGLIB$$ 라고 나온다.
이건 AppConfig 클래스가 내가 만든 클래스가 아니라는 의미다.
우리가 @Configuration을 붙이면 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고 그걸 스프링 빈으로 등록한 것이다. 이 새로운 클래스가 싱글톤이 보장되도록 해주는 것이다. (만약 우리가 @Bean만 작성하고, AppConfig 클래스에 @Configuration을 떼고 실행한다면 싱글톤이 보장되지 않을 것이다) 따라서 스프링 설정 정보를 사용할 때는 @Configuration을 사용하도록 하자!
정리
스프링 컨테이너가 스프링 빈이 싱글톤으로 설계되도록 지원해준다는 것을 배웠다.
싱글톤 패턴도 자세히 알진 못했지만 이번 강의로 찾아보고, 공부할 수 있는 기회가 되었다.
'Java & Spring' 카테고리의 다른 글
[Spring] 핵심원리 기본편 - 의존관계 자동 주입 (0) | 2022.08.29 |
---|---|
[Spring] 핵심원리 기본편 - 컴포넌트 스캔 (0) | 2022.08.22 |
[Spring] 핵심원리 기본편 - 스프링 컨테이너와 스프링 빈 (0) | 2022.07.27 |
[Spring] 핵심원리 기본편 - 스프링 핵심 원리 이해1 (객체 지향 원리 적용) (0) | 2022.06.25 |
[Spring] 핵심원리 기본편 - 스프링 핵심 원리 이해1 (예제만들기) (0) | 2022.06.06 |