스프링이 없는 순수 DI 컨테이너는 아래 그림과 같이 클라이언트가 요청할 때마다 객체를 새로 만들어서 보내줌
고객 트래픽이 많이 나올 때, 초당 객체가 생성되고 소멸되면서 메모리 낭비가 심함
→ 딱 한 개의 객체만 생성하고, 공유하는 싱글톤 패턴으로 해결

싱글톤 = 클래스의 인스턴스를 오직 하나만 생성하도록 보장하는 패턴
private 생성자를 사용해서 외부에서 임의로 new 키워드 사용하지 못하게 막아야함.
싱글톤 패턴 구현 중 가장 단순, 안전한 방법 (미리 객체 생성)
package hello.core.singleton;
public class SingletonService {
private static final SingletonService instance = new SingletonService();;
public static SingletonService getInstance(){
return instance;
}
private SingletonService(){
}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
테스트를 통해서 실제로 같은 인스턴스인지 확인
+) same vs equal
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest(){
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
// 두 객체가 다른 것을 확인
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
-> 스프링 컨테이너는 기존 싱글톤이 가진 단점은 해결하고, 객체를 싱글톤으로 관리해줌
스프링에서는 싱글톤 패턴을 적용하지 않아도, 객체를 싱글톤으로 관리함
싱글톤 레지스트리
스프링 컨테이너에서, 빈 객체를 미리 생성해서 등록하고, 조회 요청이 들어오면 같은 걸 반환하는 기능
요청이 올 때마다 객체 생성을 하는 것이 아니라, 이미 만들어진 객체를 공유해서 재사용함.
아래 테스트코드를 돌리면 같은 것을 확인할 수 있음
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 1. 조회: 호출할 때마다 객체를 생성
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
// 2. 조회: 호출할 때마다 객체를 생성
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// 두 객체가 다른 것을 확인
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
여러 클라이언트가 같은 객체 인스턴스를 공유하므로, 싱글톤 객체는 상태를 stateful하게 설계하면 안 됨. ‼️ stateless ‼️
StatefulService에서 price 필드를 각 클라이언트가 변경할 수 있도록 한 예시
public class StatefulService {
private int price; // 상태 유지
public void order(String name, int price) {
System.out.println("name = " + name + "price = " + price);
this.price = price; // 클라이언트가 필드를 변경할 수 있음 (문제가 되는 부분)
}
....
유저 A이 주문하고, 이후 유저B 가 주문한 상황
필드값이 유저2에 의해 변경되어서, 유저1의 주문 금액을 조회했을 때 변경된 값이 나옴
유저 A나 B나 같은 객체이기 때문에 공유 필드를 두면 안 됨

기존 코드에서 price를 지역 변수로, 값을 받아 리턴하는 방식으로 고침
public int order(String name, int price) {
System.out.println("name = " + name + "price = " + price);
return price;
}
AppConfig 코드를 보면
memberService 호출 시 -> memberRepository 호출 -> new MemoryMemberRepository()
orderService 호출 시 -> memberRepository 호출 -> new MemoryMemberRepository()
이러면 MemoryMemberRepository 객체가 두개 생성되어서 싱글톤이 깨지는 것 아닌가?
test로 확인해보면, 두 객체가 같은 것을 확인할 수 있음

메서드 호출 로그를 확인해보면, memberRepository()는 한 번만 호출됨
AppConfig 코드상으로는 3번 호출되어야하는 memberRepository()는 1번만 호출됨
@Configuration로 바이트코드를 조작하기 때문
CGLIB이라는 바이트코드 조작 라이브러리를 가지고, AppConfig를 상속받아 조작한 클래스를 컨테이너에 등록함
@Bean 메서드가 붙은 메서드마다 이미 빈이 존재하면, 반환하고 없으면 생성해서 등록함

@Bean만 적용하면, 순수한 AppConfig가 등록되어서 싱글톤이 깨짐
스프링 설정 정보는 항상 @Configuration 을 사용