스프링 컨테이너를 사용하지 않았을 때 클라이언트가 서비스를 호출하면 어떤 결과가 발생하는지 알아보는 테스트 코드를 작성
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
MemberService memberService3 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
System.out.println("memberService3 = " + memberService3);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService3);
Assertions.assertThat(memberService2).isNotSameAs(memberService3);
}
}
실행 결과
memberService1 = hello.core.member.MemberServiceImpl@327b636c
memberService2 = hello.core.member.MemberServiceImpl@45dd4eda
memberService3 = hello.core.member.MemberServiceImpl@60611244
스프링이 없는 순수한 DI 컨테이너 AppConfig는 요청을 할 때마다 새롭게 생성을 한다.
이렇게 되면 메모리 낭비가 심하므로 해당 객체가 딱 1개만 생성되고 공유하도록 설계하는 싱글톤 패턴을 적용하면 해결할 수 있다.
private
접근 제어자를 생성자에 사용해서 외부에서 new
연산자를 통한 객체 생성을 방지하도록 한다.public class SingletonService {
// ✔️싱글톤 서비스 클래스에서 static 영역에 하나만 생성
private static final SingletonService instance = new SingletonService();
// ✔️getxxx를 통해 싱글톤 인스턴스 반환
public static SingletonService getInstance() {
return instance;
}
// ✔️기본 생성자를 private 접근 제어자를 사용하여 외부에서 사용할 수 없도록
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
static 영역에 객체 instance를 하나 생성해서 올려둔다.
객체 인스턴스가 필요하면 getInstance()
를 통해서 반환된 인스턴스를 사용하면 된다. static 영역에 있으며 final
키워드를 통해 재할당을 금지했기때문에 getInstance()
를 호출하면 항상 같은 인스턴스가 반환이 된다.
또한 딱 1개의 객체 인스턴스만 존재해야 하므로 생성자에 private
접근 제어자를 사용해서 외부에서 new
키워드로 새롭게 객체를 생성하는 것을 방지한다.
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
MemberService memberService3 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
System.out.println("memberService3 = " + memberService3);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService3);
Assertions.assertThat(memberService2).isNotSameAs(memberService3);
}
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService service1 = SingletonService.getInstance();
SingletonService service2 = SingletonService.getInstance();
// 구체 클래스에 의존
service1.logic();
Assertions.assertThat(service1).isSameAs(service2);
}
}
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
MemberService memberService3 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
System.out.println("memberService3 = " + memberService3);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService3);
Assertions.assertThat(memberService2).isNotSameAs(memberService3);
}
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService service1 = SingletonService.getInstance();
SingletonService service2 = SingletonService.getInstance();
service1.logic();
Assertions.assertThat(service1).isSameAs(service2);
}
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
}
스프링 컨테이너가 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있도록 해준다.
싱글톤 패턴은 객체 인스턴스를 하나만 생성해서 공유하는 방식이기 때문에 클라이언트가 하나의 같은 객체 인스턴스를 공유한다. 따라서 이 싱글톤 객체는 상태를 유지(stateful)하도록 설계하면 안 된다. 무상태(stateless)로 설계를 해야한다.
상태 유지 코드 및 테스트 코드
package hello.core.singleton;
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;
}
}
public class StatefulServiceTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
void statefulServiceTest() {
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//A사용자 주문
statefulService1.order("userA", 10000);
//B사용자 주문
statefulService2.order("userB", 20000);
//A사용자 주문 금액 확인
int price1 = statefulService1.getPrice();
//B사용자 주문 금액 확인
int price2 = statefulService1.getPrice();
//❗A사용자 주문 금액인 10000원이 나와야 하나 20000원이 나오는 문제가 발생
System.out.println("price1 = " + price1);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
무상태 코드 및 테스트 코드
package hello.core.singleton;
public class StatelessService {
public int order(String name, int price) {
System.out.println("name = " + name + ", price = " + price);
return price;
}
}
public class StatelessServiceTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
void statelessServiceTest() {
StatelessService statefulService1 = ac.getBean("statelessService", StatelessService.class);
StatelessService statefulService2 = ac.getBean("statelessService", StatelessService.class);
//A사용자 주문
int priceA = statefulService1.order("userA", 10000);
//B사용자 주문
int priceB = statefulService2.order("userB", 20000);
//A사용자 주문 금액 10000원 확인
System.out.println("priceA = " + priceA);
//B사용자 주문 금액 20000원 확인
System.out.println("priceB = " + priceB);
}
static class TestConfig {
@Bean
public StatelessService statelessService() {
return new StatelessService();
}
}
}
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
//return new FixDiscountPolicy();
}
}
✔️memberService()
를 호출하면 memberRepository()
를 호출하게 되고 orderService()
를 호출하면 여기서도 memberRepository()
를 호출하게 된다.
이 두 메서드를 각각 실행하게 되면 MemberRepository 타입의 객체가 두 개가 생성되는 것처럼 보이나 실제론 한 개가 생성된다.
public class ConfigurationSingletonTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
void configurationTest() {
// ✔️구현체에 의존하는 코드가 좋지 못하나 memberRepository가 같은 객체인지를 확인하기 위한 과정이라 작성
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
Assertions.assertThat(memberService.getMemberRepository()).isSameAs(orderService.getMemberRepository());
}
}
@Bean
이 붙은 메서드마다 스프링 빈이 존재하면 존재하는 빈을 반환하는 것이고 없다면 스프링 빈을 생성해서 스프링 빈을 등록하도록 코드가 동적으로 만들어져 싱글톤이 보장되는 것이다.
@Bean
만 사용해도 스프링 빈으로 등록은 되나 싱글톤을 보장하지 않는다.
스프링 설정 정보는 항상 @Configuration
어노테이션을 붙여서 사용하도록 하자.