출처 : 인프런 > 스프링 핵심 원리 - 기본편 강의를 듣고 작성한 글입니다.
강의 링크 : 스프링 핵심 원리 - 기본편

웹 애플리케이션은 기본적으로 많은 고객이 동시에 요청을 한다. 그러나 위에 있는 그림처럼 요청이 들어올때마다 새로운 객체를 만들게 되면 메모리 낭비가 너무 심하므로, 1개를 생성하고 공유하도록 설계 하도록 만들어진게 싱글톤 패턴이다.
test안에 singleton 패키지를 하나 만들어준다.
src\test\java\hello\core\singleton\SingletonTest.java
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
// 1. 조회: 호출할 때 마다 객체를 생성
MemberService memberService1 = appConfig.memberService();
// 2. 조회: 호출할 때 마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
// 참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1 != memberService2
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
}

memberService1, memberService2가 jvm 메모리에 계속 다른 객체가 생성돼서 올라간다. 이렇게 되면 고객이 많은 웹 애플리케이션에 효율적이지 않다.


src\test\java\hello\core\singleton\SingletonService.java
package hello.core.singleton;
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("싱글톤 객체 로직 호출");
}
}

src\test\java\hello\core\singleton\SingletonTest.java
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
// 1. 조회: 호출할 때 마다 객체를 생성
MemberService memberService1 = appConfig.memberService();
// 2. 조회: 호출할 때 마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
// 참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1 != memberService2
assertThat(memberService1).isNotSameAs(memberService2);
}
@Test
@DisplayName("싱글폰 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}
}




@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext 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);
// memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}

📢 싱글톤 객체는 무상태(stateless)로 설계해야 한다!!

src\test\java\hello\core\singleton\StatefulServiceTest.java
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class StatefulServiceTest {
@Test
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);
// ThreadA: 사용자B 20000원 주문
statefulService2.order("userB", 20000);
// ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
// ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}

statefulService1과 statefulService2 모두 같은 instance를 사용하기 때문에 기대했던 결과가 나오지 않고 statefulService2에서 적용된 20000원이 나온다.

그런데 이상한점이 있다. 다음 AppConfig 코드를 보자.
src\main\java\hello\core\AppConfig.java
package hello.core;
...
@Configuration
public class AppConfig {
// @Bean memberService -> new MemoryMemberRepository()
// @Bean orderService -> new MemoryMemberRepository()
// new MemoryMemberRepository() 두 번 호출!! -> 싱글톤 깨지는 거 아니야 ???
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository()); // 생성자 주입
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
...
}

src\main\java\hello\core\member\MemberServiceImpl.java
package hello.core.member;
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
...
// 테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
src\main\java\hello\core\order\OrderServiceImpl.java
package hello.core.order;
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
...
// 테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다. 기능 검증을 위해 잠깐 사용하는 것이니 인터페이스에 조회기능까지 추가하지는 말자.
src\test\java\hello\core\singleton\SingletonService.java
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
//모두 같은 인스턴스를 참고하고 있다.
System.out.println("memberService -> memberRepository = " + memberRepository1);
System.out.println("orderService -> memberRepository = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
//모두 같은 인스턴스를 참고하고 있다.
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}


src\main\java\hello\core\AppConfig.java
package hello.core;
...
@Configuration
public class AppConfig {
...
// method 실행 순서는 보장하지 않음
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.memberRepository
// call AppConfig.orderService
// call AppConfig.memberRepository
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository()); // 생성자 주입
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
...
}



src\test\java\hello\core\singleton\ConfigurationSingletonTest.java
package hello.core.singleton;
...
public class ConfigurationSingletonTest {
...
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
// 출력 : bean = class hello.core.AppConfig$$SpringCGLIB$$0
}
}





