우리는 지난번까지 @Bean과 스프링 컨테이너가 외부에서 DI를 주입하는 방식을 알아보았다. 스프링 컨테이너가 어떻게 DIP원칙과 OCP원칙을 지켜 더 나은 객체 지향 프로그래밍을 할 수 있게 하는지 알 수 있었다. 허나 여기에는 문제가 있다.
@Configuration //애플리케이션 구성정보
public class AppConfig {
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemorymemberRepository();
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
위 코드는 지난번 스프링 컨테이너가 외부에서 DI를 주입시켜 객체를 생성하는 코드이다. 만약에 이 웹 애플리케이션을 사용하는 유저가 100명, 1000명, 10000명이라면 어떻게 될까? 객체가 100개, 1000개, 10000개가 생성이 될거다.
딱 봐도 느끼기에 좋은 현상은 아니다, 객체가 많아지니 메모리 부담과 GC가 계속해서 동작해야하니 성능면에서 매우 좋지 않다. 아래는 객체생성 메서드를 호출할 때마다 다른 객체가 생성됨을 보여주는 코드이다.
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
//1. 조회1 : 호출 할 때마다 객체 생성
MemberService memberService1 = appConfig.memberService();
//2. 조회2 : 호출 할 때마다 객체 생성
MemberService memberService2 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertNotSame(memberService1, memberService2);
}

memberService1과 memberService2 객체의 뒤의 해시코드를 보면 다른 객체임을 알 수 있다.
그렇다면 어떻게 이 문제를 해결해야 할까?
싱글톤 패턴은 자바의 대표적인 디자인 패턴 중 하나이다. 싱글톤 패턴의 정의는 객체의 인스턴스를 오직 단 하나만 생성하는 패턴을 의미한다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance(){
return instance;
} //조회시 사용
private SingletonService(){}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
싱글톤을 사용하는 이유는 위의 메모리 해결책을 포함하여 데이터 공유가 쉽다는 장점이 있다.
물론 문제점도 있다
이러한 문제점이 있는 싱글톤 패턴이지만 스프링에서는 이 문제점들을 모두 배제하여 장점들만 모아놓은 싱글톤 컨테이너로 위의 문제를 해결했다!
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(only one)으로 관리한다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
assertSame(singletonService1, singletonService2);
// same ==
// equal
}

출력 결과를 보면 해시코드가 같아, 하나의 인스턴스만 생성된걸 볼 수 있다.
stateful
상태 유지라고 하며 클라이언트와 서버 관계에서 서버가 클라이언트의 상태를 보전하는 것을 의미한다.
즉 클라이언트와 서버 간에 송수신을 하며 단계별 과정을 진행하는데 있어, 서버에서 클라이언트가 단계에서 제공한 값을 저장하고 다음 단게에서도 저장한 상태이다.
대표적으로 홈페이지에서 한번 로그인을 하면 페이지를 이동해도 로그인이 풀리지 않고 계속 유지되는 것이 바로 서버가 클라이언트의 상태를 유지하고 있으니까 가능한 것이다.
stateless
아무튼 계속해서 싱글톤으로 이어가서 아래 코드로 문제점 예시를 들어보면
package hello.core.singleton;
public class StatefulService {
private int price; // 상태를 유지하는 필드
public void order(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.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
statefulService1.order("userA", 10000);
statefulService2.order("userB", 20000);
System.out.println("statefulService1.getPrice() = " + statefulService1.getPrice());
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig{
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
여기서 테스트가 통과된다면 잘못되고 있는거다. 왜냐하면 statefulService1의 price값은 10000이 되야하는데 20000이랑 같냐고 묻는 테스트에서 통과됐으니,, 20000원이 된 상태라 볼 수 있다.
코드에서 볼 수 있는데 statefulService1과 statefulService2는 같은 객체 인스턴스(싱글톤) 객체를 바라보기 때문에 이러한 문제가 발생한 것이다.
여기서 price는 싱글톤으로 하나의 객체 인스턴스를 통해 공유되기 때문에 userA는 자신이 주문한 뒤 주문한 userB의 금액이 입력되는 것이다.
즉, 싱글톤을 사용할 때는 Stateless 상태가 되어야한다.
스프링 컨테이너(싱글톤 컨테이너)는 빈들을 하나만 생성해 관리한다.
@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 RateDiscountPolicy();
}
}
위 코드를 보면 memberService , orderService 모두 각각 자신의 구현체를 반환하는데 그 내부 파라미터를 보면 둘 다 memberRepository를 호출하고 있다.
memberRepository 는 MemoryMemberRepository 인스턴스를 반환한다. 그렇다면 총 세번 내부 코드가 실행되어 MemoryMemberRepository 인스턴스가 3번 생성되는데 어떻게 싱글톤을 유지할 수 있을까?
답은 바이트 코드 조작 에 있다.
이 의문점에 대한 정답은 @Configuration 어논테이션에 있다!

앞선 과정에서 보다시피 Sping에서 Bean을 수동으로 등록하기 위해서는, 설정 class위에 @Confoguration 을 추가하고 @Bean을 사용해 수동으로 빈을 등록할 수 있었다.
하지만 @Confoguration은 단순히 Bean을 등록하기 위한 애논테이션이 아니다
아래 테스트 코드에서 AppConfig.class가 어떻게 생성되는지 보자
@Test
void configurationDeep(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}

AppConfig면 AppConfig였지 뒤에 이상한 단어가 엄청 붙었다. 우리는 CGLIB에 집중할 필요가 있다.
스프링에서 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고 그 다른 클래스를 스프링 빈으로 등록한 것이다.

이렇게 바이트코드 조작으로 만들어진 AppConfig@CGLIB는 싱글톤이 보장된다. 그러면서 AppConfig를 상속했으니 당연히 AppConfig 타입으로도 조회가 가능하다.