//0. 순수한 DI 컨테이너 생성
AppConfig appConfig = new AppConfig();
//1. 조회: 호출할 때마다 객체 생성
MemberService memberService1 = appConfig.memberService();
//2. 조회: 호출할 때마다 객체 생성
MemberService memberService2 = appConfig.memberService();
// 참조값이 다름을 확인
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
memberService
메소드를 호출하여 memberService
객체를 생성한다.memberService
메소드를 호출할 경우, 내부적으로 memberRepository
객체 또한 생성되어 트래픽 1회 발생마다 2개의 객체가 생성된다.public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance(){
return instance;
}
private SingletonService(){}
public void logic(){
// 싱글톤 객체 로직 호출
...
}
}
getInstance()
메소드를 통해서만 조회할 수 있으며, 이 메소드를 호출하면 항상 동일한 인스턴스를 반환한다.private
으로 선언하여 외부에서 객체 인스턴스가 생성되는 것을 방지한다.void singletonServiceTest(){
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
assertThat(singletonService1).isSameAs(singletonService2);
}
getInstance()
메소드를 통해 조회한 후, 동일한 인스턴스인지 확인한다.isSameAs()
: ==
연산과 동일하며, 인스턴스의 참조값을 비교한다.isEqualTo()
: equal
과 동이라면, 인스턴스의 값 자체를 비교한다.싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.
getInstance()
메소드를 통해 참조해야 하므로, 구체클래스에 의존하게 된다.void SpringContainer(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
assertThat(memberService1).isSameAs(memberService2);
}
AnnotationConfigApplicationContext
클래스를 통해 스프링 컨테이너를 생성하며, AppConfig
를 설정 정보로 전달하여, 빈을 등록한다.public class StatefulService {
private int price; // stateful한 필드
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; // 문제!
}
public int getPrice() { return price;}
}
order()
메소드를 호출할 수 있다.price
라는 필드의 값을 변경하게 된다.getPrice()
메소드를 통해 자신의 주문 가격을 조회할 수 있다.void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// TreadA: A사용자가 10000원 주문
statefulService1.order("userA", 10000);
// TreadB: B사용자가 20000원 주문
statefulService2.order("userB", 20000);
// TreadA: 사용자A가 주문금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
TestConfig
라는 static 클래스를 선언한다.AnnotationConfigApplicationContext
를 사용하여 선언하며, TestConfig
클래스를 설정정보로 전달한다.statefulService
라는 이름의 스프링 빈 1개가 싱글톤으로 관리되게 된다.StatefulService
의 price
필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.price
의 값이 20,000원으로 변경되게 된다.public class StatefulService {
public int order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
return price;
}
}
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// TreadA: A사용자가 10000원 주문
int userAPrice = statefulService1.order("userA", 10000);
// TreadB: B사용자가 20000원 주문
int userBPrice = statefulService2.order("userB", 20000);
Assertions.assertThat(userAPrice).isEqualTo(10000);
}
price
를 삭제하였다.userAPrice
, userBPrice
)를 사용한다.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());
}
...
}
MemoryMemberRepository()
가 2번 호출 되며 싱글톤이 꺠지는 것 처럼 보인다.public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
...
// test 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
...
// test 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
memberRepository
를 조회하기 위한 메소드를 선언한다.MemberService
, orderService
메소드 모두 memberRepository
메소드를 호출하는데 과연 모두 동일한 인스턴스를 참조하는 지 확인해보자.@Test
void configurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService1 = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService1 = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
MemberRepository memberRepository1 = memberService1.getMemberRepository();
MemberRepository memberRepository2 = orderService1.getMemberRepository();
System.out.println("Real => memberRepository = " + memberRepository);
System.out.println("MemberService => memberRepository1 = " + memberRepository1);
System.out.println("OrderService => memberRepository2 = " + memberRepository2);
assertThat(memberService1.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService1.getMemberRepository()).isSameAs(memberRepository);
}
출력
Real => memberRepository = hello.core.member.MemoryMemberRepository@37052337
MemberService => memberRepository1 = hello.core.member.MemoryMemberRepository@37052337
OrderService => memberRepository2 = hello.core.member.MemoryMemberRepository@37052337
memberRepository
인스턴스와 MemberService
, orderService
메소드가 파라미터로 전달하는 memberRepository
인스턴스가 정확히 일치하는 것을 확인할 수 있다.memberRepository
인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.// 예상
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
// 실제
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
memberRepository
등록이 이루어져야할것으로 보여진다.ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
AppConfig
또한 스프링 빈으로 등록이 된다.AppConfig
스프링 빈을 조회하여 클래스 정보를 조회해본다.출력
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$68989ba6
class hello.core.AppConfig
라고 출략되어야한다.AppConfig
클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Configuration
을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장한다출력 1.
bean = class hello.core.AppConfig
AppConfig
가 CGLIB 기술 없이 순수한 AppConfig
로 스프링 빈에 등록된 것을 확인할 수 있다.출력 2.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
MemberRepository
가 총 3번 호출되었다.출력 3.
Real => memberRepository = hello.core.member.MemoryMemberRepository@1c481ff2
MemberService => memberRepository1 = hello.core.member.MemoryMemberRepository@72437d8d
OrderService => memberRepository2 = hello.core.member.MemoryMemberRepository@1b955cac
MemoryMemberRepository
인스턴스를 가지고 있다.memberRepository()
처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.출처: 인프런 스프링 핵심 원리 - 기본편 (김영한)
인프런 스프링 핵심 원리