[Lecture-Core] Singleton
순수 DI컨테이너(AppConfig)에서는 요청마다 객체를 생성한다.
→ 100건의 요청마다 100개의 객체를 생성 → 낭비 발생
★해결책: 객체가 1개만 생성되고 공유하도록 설계한다 → 싱글톤(Singleton)패턴
// 싱글톤 컨테이너 사용
ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
//1. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService1 = ac.getBean("memberService",
MemberService.class);
//2. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService2 = ac.getBean("memberService",
MemberService.class);
assertThat(memberService1).isSameAs(memberService2); // True
싱글톤 컨테이너를 사용하면 요청마다 객체를 생성하지 않고 만들어진 객체를 공유하여 효율적으로 재사용한다.
※ 싱글톤 패턴 사용시 주의점(Stateful ↔ Stateless)
싱글톤은 stateful(유지)가 아닌 Stateless(무상태)로 설계 하여야 한다.
public class Stateful {
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;
}
}
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();
//Price값을 가장 이후에 주문한 price로 덮어버린다.
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
Stateful 클래스에서 필드에 price를 두어 값이 공유가 되어버린다.
★해결책 → 공유 되지 않는 지역변수, 파라미터를 사용해야 한다.
◆ @Configuration
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
//memberService 1회 호출, memberRepository 1회 호출
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
// orderService 1회 호출, memberRepository 2회 호출
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
// memberRepository 3회호출 ???
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
System.out.println("memberService -> memberRepository = " +
memberService.getMemberRepository());
System.out.println("orderService -> memberRepository = " +
orderService.getMemberRepository());
System.out.println("memberRepository = " + memberRepository);
Test를 실행해보자!
출력결과 모두 1번씩만 호출되었고, memberService와 orderService에서 호출했던 memberRepository도 같았다.
왜 3번의 호출이 1번씩만 호출되는 걸까?
@Test
void configurationDeep() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class); //AppConfig도 스프링 빈으로 등록
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
AnnotationConfigApplicationContext(AppConfig.class)로 값을 넘길시에 AppConfig도 스프링 빈에 등록된다.
클래스 정보에서 CGLIB가 붙어있는데 스프링이 라이브러리를 사용하여 AppConfig를 상속받은 어떠한 클래스를 만들고
스프링 빈에 등록한다. 그리고 그 클래스가 싱글톤이 보장되도록 한다.
→ 스프링 컨테이너에 존재시 찾아서 반환, 없을시 컨테이너에 등록하고 반환하는 동작을 한다.
스프링은 애초에 온라인 서비스 기술을 지원하기위해 생겨났다. 웹 애플리케이션은 여러개의 Client들이 동시에 그리고 수없이 요청을 한다. 이를 효율적으로 처리하기 위한 싱글톤(Singleton)패턴에 대해 하나씩 살펴보았다.
1. 싱글톤의 동작을 get.bean을 통해 호출하여 직접 확인해보았으며
2. 필드변수 사용을 하지않는 주의점에 유의(Stateful → Stateless)
3. @Configuration의 역할까지 알아 보았다.
