스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다!
웹 어플리케이션은 보통 여러 고객이 동시에 요청을 한다.
⬆️ 이처럼 동시에 요청
요청이 올 때 마다 계속 객체를 만들어야 한다는 것이 문제점
AppConfig
는 요청을 할 때 마다 객체를 새로 생성한다.test/../singleton/SingletonTest.java
파일을 만들고, 실행해 본 결과 메모리 낭비가 심하다는 것을 알 수 있음)그럼 어떻게 해야 할까🤔?
➡️ 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다!
➡️ 이것이 바로 싱글톤 패턴❗️
싱글톤 패턴 이란 ❓
✔️ test/../singleton/SingletonService.java
생성
package hello.core.singleton;
public class SingletonService {
// 1. static 영역에 객체를 딱 1개만 생성해둔다.
// 자기 자신을 내부에 private로 가지는데, static으로 가짐
private static final SingletonService instance = new SingletonService();
// public으로 열어서 객체 인스턴스가 필요하면 이 static 메소드를 통해 조회하도록 허용
public static SingletonService getInstance() {
return instance;
}
// 생성자를 private으로 선언해 외부에서 new 키워드를 사용한 객체 생성을 못하도록 막음
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
getInstance()
메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.📌 참고 : 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다.
싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유에서 효율적인 사용이 가능해진다!!
그러나 싱글톤 패턴도 몇 가지 문제점을 가진다 🤔
스프링 컨테이너를 쓰면, 스프링 컨테이너가 기본적으로 객체를 다 싱글톤으로 만들어서 관리해준다! 기가 막히지요 👍🏻
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.
싱글톤 컨테이너란❓
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 1. 조회: 호출할 때마다 같은 객체 반환
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
// 2. 조회: 호출할 때마다 같은 객체 반환
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 참조값이 같은 것 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}
⬆️ 싱글톤 컨테이너 테스트 코드
⬆️ 싱글톤 컨테이너 적용 후 모습
📌 참고 : 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다! 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. 자세한 내용은 뒤에 빈 스코프에서 설명하겠다.
싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다 !
무상태(stateless)로 설계해야 한다❗️❗️❗️❗️❗️
✔️ test/../singleton/StatefulService.java
생성
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;
}
}
✔️ test/../singleton/StatefulServiceTest.java
생성
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;
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);
// ThreadA: 사용자 A가 10000원 주문
statefulService2.order("userB", 20000);
// ThreadA: 사용자 A 주문 금액 조회
int price = statefulService1.getPrice();
// 기댓값은 10000 이지만 20000 출력
// 사용자A 의 주문 후 사용자B가 주문을 바꿔버렸기 때문
System.out.println("price = " + price);
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
StatefulService
의 price 필드는 공유되는 필드(싱글톤 코드)인데, 특정 클라이언트가 값을 변경한다.@Configuration
의 비밀에 대해 알아보자!!
우리가 이전에 작성한 AppConfig.java
코드에 약간 이상한 부분이 있다🤔
@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();
}
.
.
.
memberService
빈을 만드는 코드를 보면 memberRepository()
를 호출한다.MemoryMemberRepository()
를 호출한다. orderService
빈을 만드는 코드도 동일하게 memberRepository()
를 호출한다.MemoryMemberRepository()
를 호출한다.➡️ 이렇게 되면, 각각 다른 2개의 MemoryMemberRepository
가 생성되면서 싱글톤이 깨지는 것 처럼 보이는데?? 스프링 컨테이너는 싱글톤을 보장해 준다고 했는데,,,, 이게 무슨 일 일까🙁??
이것을 테스트해보기 위해 !
1) MemberServiceImpl
OrderServiceImpl
파일에 싱글톤 테스트를 위한 코드를 잠시 추가하고,
2) test/../singleton/ConfigurationSingletonTest.java
파일을 만들어,
테스트 해보았다 !
⬆️ 결과는❓
모두 같은 인스턴스를 가리킨다 !!!
AppConfig
의 자바 코드를 보면 분명히 각각 2번 new MemoryMemberRepository
호출해서 다른 인스턴스가 생성되어야 하는데 왜 이런 걸까??
또 다른 실험을 통해 알아보자 !
✔️ AppConfig.java
에 호출 로그 남기기
@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();
}
우리가 예상한 출력❗️(순서는 보장하지 않음)
call Appconfig.memberService
call Appconfig.memberRepository
call Appconfig.memberRepository
call Appconfig.orderService
call Appconfig.memberRepository
이처럼 memberRepository
가 3번 호출되어야 한다!
그런데❗️❗️
⬆️ 우리의 예상과는 다르게 call은 3번이 찍혀있고, memberRepository
는 단 1번 만 호출되었다 !
💡 스프링은..정말..어떠한 방법을 통해서든 싱글톤을 보존해주는구나..!
💡 모든 비밀은 @Configuration 을 적용한
AppConfig
에 있다!
✔️ test/../singleton/ConfigurationSingletonTest.java
마지막에 테스트 코드 추가
@Test
void configurationDeep() {
// 이 과정에서 Appconfig 도 스프링 빈으로 등록된다
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$$f1bf9c1
⬆️ 이와 같이 조금은,, 이상한결과가 나온다,,
만약 이게 순수한 클래스라면 class hello.core.AppConfig
와 같은 결과가 나와야 한다.
그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다.
이것은 내가 만든 클래스가 아니라, ❗️스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것❗️이다.
⬆️ 이처럼 AppConfig 가 있는데 CGLIB 라는 바이트코드 조작 라이브러리를 가지고 상속받아서 아예 다른 클래스를 만들어버린것!
그리고 스프링이 조작한 클래스를 스프링 빈으로 등록한것!
그래서 스프링 컨테이너를 보면, 이름은 AppConfig 인데 인스턴스 객체로AppConfi@CGLIB
가 들어가 있음🤭
(실제로 CGLIB의 내부 기술을 사용하는데 매우 복잡하다)
✔️ AppConfi@CGLIB
예상 코드
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Bean
이 붙은 메소드마다 이미 스프링 빈이 존재한다면, 존재하는 빈을 반환!
만약 없다면 생성해서 스프링 빈으로 등록하고, 반환하는 코드가 동적으로 생성!
➡️ 이로 인해 싱글톤이 보장되는 것!
💡 정리
@Bean
만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지는 않음!
(싱글톤을 보장하기 위해서는@Configuration
이 필요)- 그냥 우리는 크게 고민하지 말고, 스프링 설정 정보는 항상
@Configuration
을 사용하자! 설정 정보가 필요한 곳에는 무조건 넣자!
스프링.. 아무나 하는 게 아닌 거 같아요^^