출처. inflearn, 김영한 스프링 핵심 원리 - 기본편
스프링 없는 순수한 DI 컨테이너 테스트`
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);
//참조값이 다른 것을 확인
assrertThat(memberService1).isNoSameAs(memberService2);
}
}
싱글톤 패턴을 적용한 예제 코드를 보자. main이 아닌 test 위치에 생성하자.
public class SingletonService{
//1. static 영역에 객체를 딱 1개만 생성해둔다.
private static final SingletonService instance = new SingletonService();
//2.public으로 열어서 객체 인스턴스가 필요하면 이 static메서드를 통해서만 조회하도록 허용한다.
public static SingletonService getInstance(){
return instance;
}
private SingletonService(){
}
public void logic(){
System.out.println("싱글통 객체 로직 호출");
}
}
getInstance()
메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.싱글톤 패턴을 사용하는 테스트 코드를 보자.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest(){
//private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
//new SingletonService();
//1.조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService1 = SingletonService.getInstance();
//2.조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService2 = SingletonService.getInstance();
//참조값이 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.ptintln("singletonService2 = " + singletonService2);
//singletonService1 = singeltonService2
assertThat(singletonService1).isSameAs(singletonService2);
singletonService.logic();
}
참고: 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다.
싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.
싱글톤 패턴 문제점
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.
싱글톤 컨테이너
스프링 컨테이너를 사용하는 테스트 코드
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
ApplicationContext 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);
//memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}
싱글톤 컨테이너 적용 후
참고: 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. 자세한 내용은 뒤에 빈 스코프에서 설명하겠다.
상태를 유지할 경우 발생하는 문제점 예시
public class StatefulService {
private int price; //상태를 유지하는 필드
pubilc void order(String name, int price) {
System.out.println("name = " + "price = " + price);
this.price = price;
}
public int getPrice(){
return price;
}
}
상태를 유지할 경우 발생하는 문제점 예시
public class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
Application ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//ThreadA: A사용자 1000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 2000원 주문
statefulService2.order("userB", 20000);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
//ThreadA : 사용자A 10000원 주문
int price = statefulService1.getPrice();
//THreadA : 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statfulService(){
return new StatefulService();
}
}
}
StatefulService
의 price
필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.그런데 이상한점이 있다. 다음 AppConfig 코드를 보자.
@Configuration
public class AppConfig{
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRespotiroy(){
return new MemoryMemberRepository();
}
}
memberRepository()
를 호출한다.new MemoryMemberRepository()
를 호출한다.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 memberRepositoy;
//테스트 용도
public MemberRepository getMemberRepository(){
return memberRepository;
}
}
테스트 코드
public class ConfigurationTest {
@Test
void configurationTest(){
AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
//모두 같은 인스턴스를 참고하고 있다.
System.out.println("memberService -> memberRepository = " +
memberService.getMemberRepository());
System.out.println("orderService -> memberRepository = " +
orderService.getMemberRepository());
System.out.println("memberRepository = " + memberRepository);
//모두 같은 인스턴스를 참고하고 있다.
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
new MemoryMemberRepository
호출해서 다른 인스턴스가 생성되어야 하는데?