웹 애플리케이션과 싱글톤
웹 애플리케이션은 보통 여러 고객이 동시 요청

Spring X DI Container
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI Container")
void singleton_X(){
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
//스피링 없는 순순한 DI Container는 AppConfig는 요청 할 때 마다, 객체를 새로 생성
}
SpringX 순수한 DI Container인 AppConfig는 요청이 올때마다 새로운 객체를 생성한다.
고객 traffic이 초당 100이 나오면 객체가 생성되고 소멸 -> Memory 낭비 크다
SingletonPattern
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 Pattern
그래서 객체 인스턴스를 2개 이상 생성X 막아야 한다.
public class SingletonService {
//1. static 영역 객체를 딱 1개만 생성
private static final SingletonService instance = new SingletonService();
//2. public으로 열어서 객체 인스턴스가 필요하면, 이 statci 메서드를 통해서만 조회
public static SingletonService getInstance() {
return instance; //instance에 참조를 꺼낼 수 O
}
//3. 생성자를 private으로 설정해서 외부에서 new라는 키워드 사용 X
//new라는 keyWord instance 생성X -> 다른 곳에서 저장
private SingletonService(){
}
public void logic(){
System.out.println("싱글톤 객체 로직 호출 ");
}
}
private 생성자를 사용해서 외부에서 임의로 new 키워드 사용X
//1. static 영역 객체를 딱 1개만 생성
private static final SingletonService instance = new SingletonService();
싱글톤 Pattern 사용하는 TestCode
class SingletonServiceTest {
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
assertThat(singletonService1).isEqualTo(singletonService2);
}
싱글톤 패턴을 적용하면 고객의 요청이 올때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적 사용, 하지만 싱글톤 패턴은 문제점 소유
싱글톤 패턴 문제점
1. 구현 코드양이 증가
2. 의존관계상 클라이언트가 구체 클래스에 의존 -> DIP 의존
3. 클라이언트가 구체 클래스에 의존해서 OCP 원칙 위반 가능성
4. Test 어려움
5. 내부 속성 변경, 초기화 어려움
6. private 생성자로 자식 클래스 만들기 어려움
7. 결론적으로 유연성 떨어점
싱글톤 Container
스프링 컨테이너는 싱글톤 패턴의 문제점 해걸, 객체 인스턴스를 싱글톤 1개만 생성 관리
@Test
@DisplayName("스프링 컨테이너 & 싱글톤")
void singletonServiceTest2(){
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);
assertThat(memberService1).isSameAs(memberService2);
}
}

이미 만들어진 객체를 공유해서 재사용 할 수 있다.
싱글톤 방식 주의점
싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나와 같은 객체 인스턴스를 공유, 싱글톤 객체 상태는 statful하게 설계 X -> statfuless 하게 설계
특정 클라이언트에 의존적 필드 X
틀정 클라이언트가 값을 변경할 수 있는 필드 X
가급적 읽기만
필드 대신에 자바에서 공유되지 X, 지역변수, 파라미터, ThreadLocal등을 사용
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;
}
}
상태를 유지할 경우 문제점
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//ThreadA
statefulService1.order("userA", 10000);
//ThreadB
statefulService2.order("userB", 20000);
//userA는 10000원을 기대했지만 20000원 찍힘
//service1 이던 service2던 관계 x 같은 instance 사용
int price = statefulService1.getPrice();
System.out.println("price = " + price);
// assertThat(statefulService1.getPrice()).isEqualTo(10000);
// assertThat(statefulService2.getPrice()).isEqualTo(20000);
}
ThreadA가 사용자A 코드를 호출, ThreadB가 사용자B 코드를 호출
StatefulService의 Price 필드는 공유되지 않은 필드, 특정 클라이언트가 값을 변경
사용자 A의 주문금액은 10000원, 20000원이라는 결과
@Configuration & Singleton
@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
public DiscountPolicy discountPolicy(){
//return new FixDiscountPrice();
return new RateDiscountPrice();
}
}
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(
memberRepository(),
discountPolicy()
);
}
--> memberRepository 2번 호출??
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
확인을 하기 위해서 임의로 code 추가
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);
//모두 같은 인스턴스를 참고하고 있다.
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);
}
}
SpringContainer가 각각 @Bean을 호출해서 SpringBean을 생성 -> memberRepository() 총 3번 호출???
-> 결과는 한번만 호출된다.
@Configuration과 바이트코드 조작
스프링 컨테이너는 싱글톤 레지스트리. 스프링 빈이 싱글톤 되도록 보장, 그런데 스프링이 자바 코드까지 어떻게 하기는 어려움,
@Test
void configurationDeep() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
//출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
AnnotationCofigApplicationContext에 파라미터로 넘긴 값은 스프링 빈으로 동록,
AppConfig도 스프링 빈이 된다.
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
클래스 명이 상당히 복잡, 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 클래스를 스프링 빈으로 등록

임의의 다른 클래스가 바로 싱글통이 보장되도록 보장되도록 해준다.
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Bean이 붙은 메서드마다 이미 스프링 빈이 존재, 존재하는 빈을 반환, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 형성
@Configuraltion을 적용X -> AppConfig에 등록된 것만 호출