

보통 웹 애플리케이션은 여러 고객이 동시에 요청한다. 각 요청마다 새로운 객체를 생성한다면 메모리 낭비가 심해지기 때문에 싱글톤 객체를 생성하고 공유하도록 설계한다.
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer(){
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
// 참조 값이 다르다
System.out.println(memberService1);
System.out.println(memberService2);
Assertions.assertThat(memberService1).isNotEqualTo(memberService2);
}
}

스프링 없는 순수 DI 컨테이너의 경우 요청마다 객체를 새로 생성했다.
package Singleton;
public class Singleton {
// 상수로 싱글톤 객체를 생성한다
private static final Singleton instance = new Singleton();
// 싱글톤 객체를 호출
public static Singleton getInstance(){
return instance;
}
// 생성자를 private으로 선언해서 외부에서 new로 생성할 수 없도록 한다.
// 즉 외부에서는 getInstance()로 호출만 가능하다.
private Singleton(){
}
public void logic(){
System.out.println("싱글톤 객체 호출");
}
}
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletontest(){
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("singleton1 : " + singleton1);
System.out.println("singleton2 : " + singleton2);
// isSameAs -> " == "
// isEqualsTo -> " equals "
Assertions.assertThat(singleton1).isSameAs(singleton2);
}

같은 객체임이 확인되었다.
의존관계상 클라이언트가 구체 클래스에 의존한다(DIP 위반, OCP 원칙 위반 가능성 有)
내부 속성을 변경하거나 초기화 하기 어렵다.
private 생성자로 유연성이 떨어진다.
객체 인스턴스를 1개만 생성하여 관리하는 것. 즉, 스프링 빈은 싱글톤으로 관리되는 빈이다.

스프링 컨테이너는 객체를 하나만 생성해서 관리한다.
스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라한다.
@Test
@DisplayName("스프링 빈 싱글톤인지 확인하기")
void springBeanTest(){
AnnotationConfigApplicationContext 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);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}

💡 스프링빈은 싱글톤으로 관리된다.
싱글톤 객체는 상태를 유지하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용해야 한다.
package hello.core.singleton;
public class SingtonService {
private int price;
public void buy(String name, int price){
System.out.println("name = " + name + " price = " + price);
this.price = price;
}
public int getPrice(){
return price;
}
}
package hello.core.singleton;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
class SingtonServiceTest {
@Test
@DisplayName("싱글톤 패턴 문제점")
void singletonService(){
ApplicationContext ac = new AnnotationConfigApplicationContext(configex.class);
SingtonService singleton1 = ac.getBean("singtonServie", SingtonService.class);
SingtonService singleton2 = ac.getBean("singtonServie", SingtonService.class);
singleton1.buy("User1", 10000);
singleton2.buy("User2", 20000);
System.out.println("User1의 기대값(10000) : "+singleton1.getPrice());
System.out.println("User2의 기대값(20000) : "+singleton2.getPrice());
Assertions.assertEquals(singleton1.getPrice(), singleton2.getPrice());
}
static class configex{
@Bean
public SingtonService singtonServie(){
return new SingtonService();
}
}
}

클라이언트가 값을 변경할 수 있도록 코드를 짰기 때문에 값이 변경된 상태로 유지가 된다.
✨ 절대로 필드에 공유값을 넣어서는 안된다✨
다음 설정 파일을 보자
package hello.core;
import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.Order.OrderService;
import hello.core.Order.OrderServiceImpl;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 애너테이션 추가
@Configuration
public class AppConfig {
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
}

위 그림처럼
memberRepository()가 호출되었을 때new MemoryMemberRepository()가 생성된다. 그리고memberService()가 호출 되었을 때new MemberServiceImpl가 생성되고,memberRepository()를 한 번 더 부르면서new MemoryMemberRepository()가 한 번더 생성되는 것이 아닌가?
각 구현체에 리턴 메소드를 추가한다.
package hello.core.Order;
import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixedDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
...(생략)...
public MemberRepository getMemberRepository(){
return memberRepository;
}
}
package hello.core.member;
public class MemberServiceImpl implements MemberService{
...(생략)...
public MemberRepository getMemberRepository(){
return memberRepository;
}
}
package hello.core;
import hello.core.Order.OrderService;
import hello.core.Order.OrderServiceImpl;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
public class ConfigurationTest {
@Test
@DisplayName("AppConfig 싱글톤 테스트")
void ConfigurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
System.out.println(orderService.getMemberRepository());
System.out.println(memberService.getMemberRepository());
System.out.println(memberRepository);
Assertions.assertEquals(orderService.getMemberRepository(), memberRepository);
Assertions.assertEquals(orderService.getMemberRepository(), memberService.getMemberRepository());
Assertions.assertEquals(memberRepository, memberService.getMemberRepository());
}
}

세 개가 모두 같은 인스턴스이다!
package hello.core;
import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.Order.OrderService;
import hello.core.Order.OrderServiceImpl;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 애너테이션 추가
@Configuration
public class AppConfig {
@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(){
System.out.println("call Appconfig.discountPolicy");
return new RateDiscountPolicy();
}
@Bean
public MemberService memberService(){
System.out.println("call Appconfig.memberService");
return new MemberServiceImpl(memberRepository());
}
}

위의 테스트 파일을 그대로 돌려본 결과 인스턴스가 1개씩만 생성되는 것이 확인된다.
👉 @Configuration 애너테이션에 있다.
@Test
@DisplayName("AppCofig 파일의 빈을 조회해보자")
void AppConfigBean(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}

순수한 자바 클래스 파일이 아닌
$$SpringCGLIB$$0가 나오는데 이것은CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 다른 클래스를 만들고 그것을 스프링빈으로 등록한 것이다.
AppConfig@CGLIB의 내부 코드는 알 수 없지만, 스프링 컨테이너에 등록이 되어있으면 찾아서 반환하고 그렇지 않으면 새로 생성해서 컨테이너에 등록하는 로직일 것이다.
// 애너테이션 삭제
// @Configuration
public class AppConfig {
@Bean
public OrderService orderService(){
System.out.println("call Appconfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
...(생략)...
}

자바클래스로 AppConfig 파일이 등록되었으며, 싱글톤을 보장해주지 못한다.
