김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.
스프링은 웹 애플리케이션을 위해 탄생했고, 웹 애플리케이션은 보통 여러 요청이 동시에 들어옵니다.
스프링을 이용하지 않은 순수 DI 컨테이너는 호출이 들어올 때마다 새로운 객체를 생성했습니다.
즉, 100개의 주문이 들어오면 100개의 객체가 생성되고 소멸된다는 이야기에요. 이는 결국 메모리의 낭비로 이어집니다.
AppConfig appConfig = new AppConfig();
// 1. 조회 : 호출할 때마다 객체 생성
MemberService memberService1 = appConfig.memberService();
// 2. 조회 : 호출할 때마다 객체 생성
MemberService memberService2 = appConfig.memberService();
해결책 -> 하나의 객체를 공유! (싱글톤)
이는 클래스의 인스턴스가 단 1개만 생성되도록 보장하는 디자인 패턴으로서, 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 합니다.
외부에서 new 연산자를 통해 객체 생성을 방지하기 위해 생성자에 private 키워드 사용
public class SingletonService {
// 1. static 영역에 올리는 하나의 객체 생성
private static final SingletonService instance = new SingletonService();
// 2. 객체를 참조하기 위해 함수 호출
public static SingletonService getInstance() {
return instance;
}
// 3. 생성자를 private로 설정해서 외부에서 new 연산자 호출 방지
private SingletonService() {
}
}
싱글톤 패턴의 문제점
스프링 컨테이너는 위의 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리합니다. (스프링 빈이 바로 싱글톤으로 관리되는 빈..!!)
즉, 싱글톤 패턴을 적용하지 않아도 스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리합니다.
그냥 개꿀이라는 점..
이렇듯 하나의 객체를 공유해서 사용하기 때문에, 싱글톤 객체는 상태를 유지하면 절대 안됩니다. (stateless)
무상태로 설계해야 합니다.
문제가 되는 상황
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;
}
}
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
statefulService1.order("userA", 1000);
statefulService2.order("userB", 2000);
int price = statefulService1.getPrice();
System.out.println("price = " + price);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
위처럼 고객 A와 B가 주문을 하고 A가 price를 조회하면 값이 변경되는 문제가 발생합니다.
AppConfig 파일을 보면 memberRepository 호출을 memberService와 orderService에서 각각 진행하고 있음을 확인할 수 있습니다.
그렇다는 얘기는 두 번의 객체 생성을 하기 때문에 싱글톤 패턴이 깨지는 게 아닐까요?
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
테스트를 통해 확인해보면 모두 같은 객체이고, 애초에 호출이 한 번되는 것을 확인할 수 있습니다.
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
// 모두 같은 값 출력
System.out.println("memberService = " + memberService.getMemberRepository());
System.out.println("orderService = " + orderService.getMemberRepository());
System.out.println("memberRepository = " + memberRepository);
우리가 작성한 자바 파일까지 스프링이 싱글톤으로 관리하는 것은 여간 쉽지 않은 이야기입니다.
그래서 실제로 설정파일로 자바 클래스를 등록해줄 때, 순수한 클래스가 아닌 CGLIB
라는 바이트코드 조작 라이브러리를 사용해 설정 파일을 상속 받은 임의의 다른 클래스를 만들고, 그 클래스를 스프링 빈으로 등록하게 된답니다.
이 임의의 다른 클래스가 싱글톤을 보장해준답니다.
@Configuration
이 이를 가능하게 해줍니다. 즉, 어노테이션이 빠지게 되면 순수 클래스 자체를 등록하게 되므로 싱글톤 패턴이 깨지게 됩니다.