해당 블로그 글은 김영한님의 스프링 핵심원리 강의를 수강한 이유 강의 내용에서 배운 것을 정리한 내용입니다.
스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해서 태어났다.
즉 기업용 온라인 서비스 다양한 이용 고객들이 사용하는 서비스를 지원하기 위해서 만들어졌다는 의미이다.
웹 애플리케이션은 일반적으로 다양한 고객이 동시 다발적으로 요청이 발생한다.
그림 처럼 여러 고객이 동시에 멤버 서비스에 요청을 했을때 AppConfig가 요청마다 멤버 서비스 인스턴스를 생성하고 응답 시 제거하는 방식으로 동작한다고 생각해보자
이는 메모리나 자원 낭비로 이어진다. 멤버 서비스라는 것은 즉 단일 객체를 통해서 1개만 유지한 상태로 계속 들어오는 요청만 처리하면 되는 것이다.
이렇게 하나의 인스턴스만 생성한 채로 유지하는 디자인 패턴을 싱글톤 패턴이라고 한다.
과연 Spring 에서는 어떻게 싱글톤 패턴을 사용하는지 왜 사용하는지 한번 알아보자
클래스 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
즉 현재 구동중인 JVM에서는 A라는 클래스 인스턴스는 오직 한개만 존재하는 것이다.
package hello.core.singleton;
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance(){
return instance;
}
private SingletonService(){
}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
즉 이렇게 싱글톤 패턴으로 구현을 했을때 static 변수를 통해서 인스턴스 자체를 new 로 생성하기 이전에 이미 생성되게 만들고 이를 private으로 했기 때문에 외부에서는 접근을 할수 없게 만든다.
이렇게 만들면 해당 인스턴스 오직 1개만 만들어지는 구조이다.
인스턴스가 필요하면 getInstance 메서드를 통해서 호출하는 경우이다.
자사 솔루션에서도 싱글톤을 자주 썻는데 싱글톤 클래스를 구현하고 이를 상속받아서 쓰는 식으로 했음
그럼 AppConfig에 빈으로 등록하는 모든 클래스 인스턴스를 싱글톤으로 해야한다고 하면 이 싱글톤 패턴을 만들고 상속하든지, 싱글톤으로 구현하든지 해서 싱글톤으로 내가 만들어줘야하나❓
정답은 ❌
Spring 컨테이너를 통해서 해당 객체들을 모두 싱글톤 패턴으로 생성해준다.
어떻게 하는지 한번 알아보자
싱글톤 패턴을 사용하지만 싱글톤 패턴은 만능이 아니다. 싱글톤 패턴은 문제점은 어떤게 있는지 한번 알아보자
싱글톤 패턴의 문제점⛔
1. 싱글톤 패턴을 구현하는 코드를 추가해야함
2. 의존 관계상 클라이언트가 구체 클래스에 의존하게 된다. ➡ DIP 위배
3. 클라이언트가 구체 클래스에 의존하기에 OCP 위배 가능성 높아짐
4. 테스트 하기 어려움
5. 내부 속성 변경이나 초기화가 어려움'
6. private 생성자를 쓰기 때문에 자식 클래스 만들기 어려움
7. 1-6 특성으로 클래스의 유연성이 감소
스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤 관리해준다.
스프링 컨테이너는 싱글톤 컨테이너 역할을 수행한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리 라고 한다.
스프링 컨테이너의 이런 기능으로 싱글톤 패턴의 단점을 상쇄하여 싱글톤으로 유지하게 되는 것
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
AnnotationConfigApplicationContext ax = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ax.getBean("memberService",MemberService.class);
MemberService memberService2 = ax.getBean("memberService",MemberService.class);
System.out.println("memberservice1 = " + memberService1);
System.out.println("memberservice2 = " + memberService2);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
테스트 코드를 이렇게 쓰고 작성해보면 스프링 컨테이너 AnnotationConfigApplicationContext에 등록된 멤버 서비스 빈을 가져와서 서로가 같은 인스턴스인지 비교해보자
결과는 동일한 인스턴스라는 것을 알 수 있다.
DI 컨테이너(스프링 컨테이너)를 통해서 싱글톤 패턴을 유지하고 OCP,DIP 위배를 방지하는 것이다.
싱글톤 패턴이든 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식에서 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계 ❌
특정 클라이언트에 의존적인 필드가 존재 X
가급적 읽기만 가능하게 구성하고 값을 수정하는 메소드나 소스가 포함되면 안된다.
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;
}
}
이런 코드가 있다고 생각해보자
주문을 하는 order메서드를 수행하면 price를 바꿔준다.
그럼 멀티 쓰레딩으로 하는 상황을 생각해보자
만약 A 쓰레드가 A고객의 주문을 수행했다.
sc1.order("UserA",10000);
sc2.order("UserB",20000);
이렇게 수행했을때 UserA의 가격을 getPrice 메서드로 받았을 때 어떻게 될까
당연하게도 수정하는 부분이 있어 20000이 반환된다.
A 고객은 10000원을 주문했지만 20000원이 주문 금액이라는 크리티컬한 문제가 발생한다.
그렇기 때문에 공유 필드는 정말 조심해서 싱글톤에서 다뤄야한다. 특히 값을 바꾸는 부분에서는 더욱이 조심해야함
영한님의 말로는 꼭 몇년에 한번씩 일어나는 일이기도 하며 장애를 수습하는데 길게는 몇개월이 걸릴 수 도 있다.
AppConfig 클래스는 자바 코드로 결국 이루어지고 코드 상에서 확인해보면 MemoryMemberRepository는 new로 두번 생성하는 구조이다.
@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());
}
자바 코드 상에서만 보면 이것은 싱글톤 패턴을 절대 유지할수가 없다.
그럼 테스트를 통해서 싱글톤 유지가 되는지 알아보자
@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);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
System.out.println("memberService --> memberRepository : " + memberRepository1);
System.out.println("orderService --> memberRepository : " + memberRepository2);
System.out.println("memberRepository : " + memberRepository);
}
테스트 코드를 확인해보면 new 로 생성한 2개의 멤버 리포지토리 ,그리고 등록된 리포지토리 빈을 불러와서 비교해보면 모두 같은 인스턴스를 가진다.
자바 코드로만 이해하면 절대 이해 불가❌
👉 해답은 @Configuration 이다.
실제로 확인했을때 한번 생성 이후 새로운 인스턴스로 생성하지 않는다.
AppConfig도 결국 스프링 빈으로 등록되는데 이 AppConfig를 빈에서 꺼내서 확인해보면 단순한 클래스가 아니다.
정상적으로 스프링 빈에 등록했을때 인스턴스 이름은 AppConfig 이어야 하지만 그렇지 않다.
Bean = class hello.core.AppConfig$$SpringCGLIB$$0
이런 이름으로 나오게 된다. 뒤에 CGLIB 라는 것이 추가로 생기는데 이것이 핵심이다.
CGLIB라는 바이트 코드를 조작하는 라이브러리를 사용해서 임의의 AppConfig 자식 클래스를 만들어서 빈에 등록한다.
if(memoryMemberRepository 스프링빈 등록 ⭕)
return 스프링컨테이너에 찾은 리포지토리 인스턴스 반환
else
로직 호출 후 인스턴스 생성 -> 컨테이너 빈 등록 반환
CGLIB는 아주 복잡한 라이브러리 지만 이런 로직을 가지고 운영된다.
즉 빈이 등록 되었는지 확인하고, 없으면 생성 있으면 반환을 수행한다.
@Configuration이 없다면❔
실행하는데 자체는 문제가 없다. 하지만 싱글톤 패턴을 보장하지 않기 때문에 앞서 언급됬던 새로운 인스턴스를 계속 생성하는 문제가 생기는 것이다.