Summary
- 스프링 컨테이너를 사용하면 객체 인스턴스를 싱글톤으로 관리하고 싱글톤에서 생기는 단점도 모두 해결해준다.
- 스프링 컨테이너를 사용하면 기본적으로 싱글톤 방식을 지원한다. (물론 필요하다면 요청할 때마다 다른 참조를 하는 방식도 지원을 한다.)
- 그래서 스프링 컨테이너를 사용하면 된다.
- 그래서 '스프링 컨테이너'를 '싱글톤 컨테이너' 라고 부르기도 한다.
- @Configuration 어노테이션 덕분에 스프링 컨테이너에서 싱글톤을 유지할 수 있다.
싱글톤?
- GOF 디자인 패턴의 싱글톤 패턴
- 클래스의 인스턴스가 1개임을 보장한다. (이미 만들어진 객체를 공유한다.)
- 스프링 컨테이너는 기본적으로 싱글톤 방식을 지원한다.
싱글톤이 필요한 이유
- 요청을 할 때마다 new를 통해 객체를 만든다면, 몇 천만명의 사용자가 요청한다면? -> 메모리 낭비
순수 자바로 싱글톤 만들기
- 인스턴스를 하나만 사용하므로 메모리 낭비가 되지 않는다.
- 싱글톤 구현 코드가 길다.
- 클라이언트가 구현체에 의존한다. -> DIP 위반
- 클라이언트가 구현체에 의존하기 때문에 OCP 위반 가능성 높음
- 생성자를 private로 사용하기 때문에 자식 클래스를 만들기 어려움
- 이러한 단점은 스프링 컨테이너가 해결해준다.
// JAVA로 싱글톤을 구현한 코드
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("싱글톤 객체 로직 호출");
}
}
스프링 컨테이너가 싱글톤을 적용해주는지 확인
- memberService1 와 memberService2의 참조값은 둘 다 같다.
@Test
@DisplayName("스프링 컨테이너는 싱글톤이 자동으로 적용된다. 그리고 순수 싱글톤의 단점도 해결한다.")
public void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 1. 조회 : 호출할 때 마다 객체를 생성하는지
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
// 2. 조회 : 호출할 때 마다 객체를 생성하는지
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 3. 주소(참조)가 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1 객체와 memberService2 객체가 같은 것을 증명
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
🌟 싱글톤 방식의 주의점 - 공유 필드를 조심하자!
- 객체의 인스턴스를 하나만 공유해서 사용한다. = 여러 클라이언트가 하나의 객체를 공유해서 사용한다.
- 그래서 싱글톤 객체는 상태를 유지(stateful) 하게 설계하면 안된다. 무상태(stateless) 로 설계해야 한다.
- stateful : 특정 클라이언트가 공유 변수의 값을 바꿀 수 있는 경우
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
@DisplayName("stateful 테스트 - 객체를 공유하기 때문에 price 변수가 공유되어 문제 발생")
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 price = statefulService1.getPrice();
System.out.println("price = " + price);
// 사용자A가 조회했을 때 10000원을 의도했는데 20000원이 나온다.
Assertions.assertThat(price).isEqualTo(10000);
}
- stateless : 특정 클라이언트가 공유 변수의 값을 바꾸는 것이 아니라 get 메서드를 통해 조회하도록 한다.
public class StatelessService {
// 전역(공유) 부분에 변수 두는 것은 항상 조심한다.
public int order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
return price;
}
}
@Test
@DisplayName("stateless 테스트")
public void statelessServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatelessService statelessService1 = ac.getBean(StatelessService.class);
StatelessService statelessService2 = ac.getBean(StatelessService.class);
int userAPrice = statelessService1.order("userA", 10000);
int userBPrice = statelessService2.order("userB", 20000);
Assertions.assertThat(userAPrice).isEqualTo(10000);
Assertions.assertThat(userAPrice).isNotEqualTo(userBPrice);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
@Bean
public StatelessService statelessService() {
return new StatelessService();
}
}
@Configuration 어노테이션 의미
- @Configuration 어노테이션 덕분에 스프링 컨테이너에서 싱글톤을 유지할 수 있다.
- 참고 : GCLIB 이라는 아이 덕분에 싱글톤을 유지할 수 있다.
생각을 해보자. 아래 코드에서
memberRepository()를 호출하면 new MemoryMemberRepository() 가 할당된다.
memberService()를 호출하면 memberRepository()를 호출하고 new MemoryMemberRepository ()가 할당된다.
orderService()를 호출하면 memberRepository()를 호출하고 new MemoryMemberRepository ()가 할당된다.
셋 다 new 을 통해 새롭게 할당하니까, 참조 값이 다를 것인데 어떻게 싱글톤이 된다는 이야기인가?
결론적으로 @Configuration 덕분에 싱글톤이 유지된다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
@Configuration 내부에서는 만약 스프링 컨테이너에 이미 등록되어 있는 객체라면 스프링 컨테이너에서 찾아서 반환하고,
스프링 컨테이너에 없던 새로운 객체라면 기존 로직을 호출해서 스프링 컨테이너를 등록한다.
@Configuration 어노테이션을 빼고 실행하면 아래 결과처럼 싱글톤이 깨져버린다.
참고 자료
이해를 위해 인프런_김영한님의 '스프링 핵심 원리 - 기본편'의 '섹션 6. 싱글톤 컨테이너'을 참고하여 쉬운 예시로 작성하였습니다.