
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService(){
}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
private static final SingletonService instance = new SingletonService();
테스트 코드
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest(){
SingletonService singletonService1= SingletonService.getInstance();
SingletonService singletonService2= SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
결과

- 객체가 여러개가 생성되는게 아닌, 한 가지만 생성됨을 확인할 수 있다.
- 참고: 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다.
- 싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율
적으로 사용할 수 있다
싱글톤 패턴 문제점
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService(){
}
}
이렇게 많은 문제점들을 가지고 있다.
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다
싱글톤 컨테이너
테스트 코드
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//member1은 member2랑 같다.
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
}

결과가 같게 나오는 것을 확인할 수 있다.
싱글톤 컨테이너 적용 후

문제점 예시
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;
}
}
예시로 먼저 StatefulService를 만들어준다.
그리고 StatefulServiceTest를 통해 테스트를 진행했다.
class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//a사용자 만원 주문
statefulService1.order("userA",10000);
//b사용자 2만원 주문
statefulService2.order("userB",20000);
//ThreadA: 사용자a 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price); //만원 출력되야 하는데 2만원 출력.
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}

유저A는 10000원을 주문한 것을 확인했으나, 출력에선 20000원이 출력된다. -> 상태 유지 문제점.
이런 이유로 하여금 공유필드는 조심해야하고, 스프링 빈은 항상 무상태로 설계해야 한다.
전에 만들었던 AppConfig.java 파일을 살펴보자.
- memberService 빈을 만드는 코드를 보면 memberRepository() 를 호출한다.
- 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.
- orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다.
- 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.
결과적으로 각각 다른 2개의 MemoryMemberRepository 가 생성되면서 싱글톤이 깨지는 것 처럼 보인다.
이를 테스트해보고자 한다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
테스트를 위해 각각 MemberServiceImpl와 MemberServiceImpl에 get 메소드를 추가한다.
그리고 테스트 코드를 추가한다.
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
System.out.println("memberService->memberRepository1 = " + memberRepository1);
System.out.println("memberService->memberRepository2 = " + memberRepository2);
Assertions.assertThat(memberRepository1).isSameAs(memberRepository2);
}
}
결과는

실행이 잘 되는 것을 알 수 있다.
참고
- 메소드에 static을 붙이면 싱글톤이 깨지게 된다.
만약 AppConfig의 코드를
@Bean
private static MemoryMemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
이런 식으로 static을 붙이게 된다면

이렇게 에러가 나오게 된다.
스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자
바 코드까지 어떻게 하기는 어렵다.
그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
모든 비밀은 @Configuration 을 적용한 AppConfig 에 있다.
테스트 코드를 하나 만들었다.
@Test
void configurationDeep() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
//출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
결과는 이렇게 나온다. (강좌랑 조금 다르게 출력됐다.)

원래라면 class hello.core.AppConfig 으로 출력되어야 하지만, 본 출력에선 CGLIB가 붙었다.
이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.
그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.(실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡하다.)
AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
- @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
- memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
- 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자