스프링은 기업용 온라인 서비스 기술을 지원하기위해 만들어졌다. 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. (약 90%)
웹 애플리케이션은 보통 여러 고객이 "동시에" 요청을 한다 ‼️
✔️ 애플리케이션 : 특정 목적을 위해 만들어진 소프트웨어 덩어리
스프링 없는 순수 DI 컨테이너 (AppConfig)에서는 고객이 요청을 할 때마다 memberService 스프링 빈이 생성된다. 3명이 요청하면 서비스 객체가 3개가 컨테이너에 생겨난다.
즉 100명이면 100개, 10만명이면 10만개의 같은 객체가 만들어지고 GC되면서 엄청난 메모리 낭비가 생긴다.
이 문제를 위해서 하나의 클래스 당 하나의 객체만 가지도록 설정했다.
싱글톤 패턴 이란 클래스의 인스턴스가 단 하나만 생성되는 것을 보장하는 패턴이다.
이걸 보장하기 위해서 2번 생성되는 일을 막아야한다. ➡️ private 으로 외부에서 new 못하게 막음.
package hello.core.singletonTest;
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
public SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
➡️ static 영역에 private 으로 객체를 생성해둔다.
그런데 안에서만 new 할 수 있게 만들면 DIP 원칙을 위배하게 된다. 클라이언트가 인터페이스 뿐만 아니라 구현체에도 의존하게되기 때문이다.
그래서 이런 싱글톤 패턴은 메모리 낭비를 방지해주지만 유연성이 떨어지고 테스트하기도 어렵다. (안티패턴)
스프링 컨테이너가 위의 두 장점을 가지면서 단점들을 보완해준다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
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);
}
두번 호출을 해도 이미 만들어진 같은 객체가 반환된다는 걸 확인할 수 있었다.
싱글톤 패턴이든 스프링 싱글톤 컨테이너든 객체 인스턴스 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체를 사용하기 때문에 특정한 상태를 가져서는 안된다.
여러 곳에서 공유되서 사용되는 대상은 여러 곳에서 사용할 수 있어야하기 때문에 어느 한 곳으로 기울어져 있으면 안된다. 그러면 공유를 해서 사용할 수가 없기 때문이다. 어떤 상태를 가지게되면 클라이언트마다 각자의 상태를 가지게되고 동시에 들어오는 상태를 처리할 수 없다. 모두를 만족시킬 수 없으면 아무 것도 하지 않아야 한다.
이렇게 스프링 빈에 공유필드를 만들어둬서 생기는 문제를 동시성 문제 또는 멀티스레드 문제라고 한다.
(실무에서 몇년에 한번은 꼭 만나는 문제라고 하니 항상 조심하자 ‼️)
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.singletonTest;
import hello.core.singleton.StatefulService;
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;
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);
// ThreadA: A 사용자 10000 원 주문
statefulService1.order("userA", 10000);
// ThreadB: B 사용자 20000 원 주문
statefulService1.order("userB", 20000);
// ThreadA : A 사용자 주문 금액 조회
int price = statefulService1.getPrice();
// ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
스프링 빈은 싱글톤으로 관리되어 여러 스레드가 공유하므로, 특정 클라이언트에 의존적인 상태 필드를 가져서는 안된다. 상태를 가지게 되면 동시성 문제가 발생하여 서비스의 신뢰도가 깨지기 때문이다. 따라서 가급적 무상태(Stateless) 로 설계하고, 필요한 데이터는 파라미터나 지역 변수를 통해 처리해야 한다.
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
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
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 FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
보면 memberService 와 orderService 에서 두번 memberRepository() 가 호출되서 자바코드 상으로 new 객체가 2개가 생기게 된다.
그런데 실제로 두 곳에서 사용하는 리포지토리를 비교해보면 같은 객체라는 결과가 나온다.
스프링은 이런 문제를 해결하기 위해서 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. AppConfig 를 출력해보면 원래 나와야할 정보 뒤에 뭔가가 더 붙어서 나오는 걸 확인할 수 있다.
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
이건 스프링이 CGLIB 라는 라이브러리를 통해서 AppConfig 를 상속받은 프록시(임의의 객체) 를 만들어서 이걸 스프링 빈으로 등록하기 때문이다.
상속받아 등록된 프록시 스프링 빈에는 같은 타입의 객체가 있으면 기존 것을 반환해주고 없으면 생성해서 반환하는 로직이 들어있다. 이 덕분에 자바코드로 여러번 생성되게 되는 스프링 빈도 하나만 생성되도록 싱글톤을 보장할 수 있다.
@Configuration 없이 @Bean 만 붙이면 스프링이 일반 자바 코드라고 생각해서 스프링 빈으로 등록만 될 뿐 DI 컨테이너라고 인식하지 못해서 싱글톤이 유지되지 않는다.