싱글톤 패턴: 클래스의 인스턴스가 단 1개만 생성되는 것을 보장하는 디자인 패턴
아래는 싱글톤 패턴을 적용한 코드다.
public class SingletonService {
// 1. static 영역에 객체를 딱 1개만 생성해둔다.
private static final SingletonService instance = new SingletonService();
// 2. public으로 열어서 객체 인스터스가 필요하면 이 static 메서드를 통해서만 조회하도록허용한다.
public static SingletonService getInstance() {
return instance;
}
// 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
위의 코드를 테스트하는 코드는 아래와 같다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
// private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
// new SingletonService();
// 1. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService1 = SingletonService.getInstance();
// 2. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService2 = SingletonService.getInstance();
// 참조값이 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// singletonService1 == singletonService2
assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}
하지만 위의 코드는 문제가 있다.
스프링 컨테이너는 이런 문제들을 해결해준다. 스프링 컨테이너에서 관리하는 bean들은 모두 싱글톤으로 관리된다.
즉 스프링 컨테이너는 싱글톤 컨테이너이기도 하다.
스프링 컨테이너는 bean을 컨테이너에 저장해 놓았다가 사용 요청이 오면 이미 만들어진 객체를 공유하면서 사용하게 된다.
아래의 코드를 보자
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;
}
}
// Bean 등록
class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
위의 코드는 order함수를 쓰면 price의 값이 변할 수 있다. 이렇게 되면 어떤 문제가 있는지 확인해보자.
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService",
StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService",
StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
// StatuefulService Bean 등록
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
만약 userA가 order함수를 사용해서 1000원으로 값을 변경했다고 해보자.
그후 userB가 order함수를 사용해서 2000원으로 값을 변경한다.
그리고 userA는 getPrice함수로 값을 확인한다. 우리는 1000원이 나오기를 기대했지만 2000원으로 출력이 된다. 왜냐하면 StatefulService는 싱글톤으로 관리되는 bean이기 때문이다.
그러므로 스프링 빈은 항상 무상태(stateless)로 설계해야 한다.
하지만 @Configuration을 클래스에 쓴다면 이야기가 달라질 것이다.
class hello.core.AppConfig에서 바이트코드를 조작하여
class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70란 임의의 클래스를 만든다.
이런식으로 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!
덕분에 싱글톤이 보장되는 것이다.
참고 AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다.