package Hello.core.singleton;
import Hello.core.AppConfig;
import Hello.core.member.MemberService;
import org.junit.jupiter.api.DisplayName;
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 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.java.Hello.core.singleton
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("싱글톤 객체 로직 호출");
}
}
getInstance()
메소드를 통해서만 조회할 수 있고, 이 메소드를 호출하면 항상 같은 인스턴스를 반환한다package Hello.core.singleton;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonServiceTest {
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singlethonServiceTest(){
//1. 조회 : 메소드 호출시 같은 객체 반환
SingletonService singletonService1 = SingletonService.getInstance();
//2. 조회 : 메소드 호출시 같은 객체 반환
SingletonService singletonService2 = SingletonService.getInstance();
// 참조값이 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
assertThat(singletonService1).isSameAs(singletonService2);
//same 주소값 비교(==)
//equal 대상의 내용(content) 비교
singletonService1.logic();
}
}
getInstance()
메소드를 호출할 때 마다 같은 객체 인스턴스를 반환함참고
싱글톤 패턴을 구현하는 방법은 여러가지가 있음(호출 타이밍에 객체가 생성이 안돼있으면 그때서야 생성해 사용, 지연하는 방법)
그러나 이러한 부분들은 스프링이 다 관리해주기 때문에 궁금하면 찾아볼 것!
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
// AppConfig appConfig = new AppConfig();
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);
}
package Hello.core.singleton;
public class StatefulService {
private int price; // 상태를 유지하는 필드 (이것을 사용하지 말아야함)
public int order(String name, int price){
System.out.println("name = " + name + " price = " + price);
this.price = price; // 여기가 문제!
return price;
}
public int getPrice(){
return price;
}
}
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;
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원 주문
int userAPrice = statefulService1.order("userA", 10000);
// ThreadB : B사용자 20000원 주문
int userBPrice = statefulService2.order("userB", 20000);
// ThreadA : 사용자A 주문 금액 조회
// int price = statefulService1.getPrice();
System.out.println("price = " + userAPrice); // A 사용자가 10000원 주문하고 A사용자의 주문 내역을 호출하려했는데
// 호출하는 사이에 B 사용자가 20000원 주문하면 statefulService1과 statefulService2는 같은 인스턴스이기 때문에
// price값이 20000원으로 돼버림..
// Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
...
}
@bean
이 달린 메소드들을 호출하면 스프링 컨테이너에 빈을 등록한다memberRepository()
를 호출한다new MemoryMemberRepository()
가 2번 호출된다
public MemberRepository getMemberRepository() {
return memberRepository;
}
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.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);
}
}
package Hello.core;
import Hello.core.discount.DiscountPolicy;
import Hello.core.discount.FixDiscountPolicy;
import Hello.core.discount.RateDiscountPolicy;
import Hello.core.member.MemberService;
import Hello.core.member.MemberServiceImpl;
import Hello.core.member.MemberRepository;
import Hello.core.member.MemoryMemberRepository;
import Hello.core.order.OrderService;
import Hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
// 애플리케이션의 설정정보
public class AppConfig {
// @Bean memberService -> new MemoryMemberRepository()
// @Bean orderService -> new MemoryMemberRepository()
// 예상
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
// call AppConfig.memberRepository
// call AppConfig.memberRepository
// 실제
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository(); // 구현 : 메모리 회원 저장소
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy(); // 구현 : 정액 할인 정책
}
}
memberRepository()
가 3번 호출될 것 처럼 보이지만, @Configuration
이 관리해주기 때문에 1번만 호출된다 @Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
AppConfig
스프링 빈을 조회해서 클래스 정보를 출력해보자class hello.core.AppConfig
가 출력되지 않고 CGLIB뭐시기가 출력된다
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Bean
이 붙은 메소드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록 후 반환하는 코드가 동적으로 만들어진다참고
AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회할 수 있다(부모 타입을 조회하면 자식도 함께 조회된다)
@Bean
만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다memberRepository()
처럼 의존관계 주입이 필요해서 메소드를 직접 호출할 때 싱글톤을 보장하지 않는다@Configuration
을 사용하자