대부분의 스프링 어플리케이션은 웹 어플리케이션이다. 그 밖에것도 가능하다.
웹 어플리케이션에서는 보통 여러 고객이 동시에 요청을 한다.
예를 들어, A B C 고객이 동시에 memberService를 요청한다고 하자. 그러면 DI컨테이너 (AppConfig)는 new memberService를 생성해서 계속 반환해준다.
즉, 3회 신청시 3회 반납이 오는것이다.
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 SingletoneTest {
@Test
@DisplayName("Pure DI Container")
void pureContainer() {
AppConfig appConfig = new AppConfig();
//1. call
MemberService memberService1 = appConfig.memberService();
//2. call2
MemberService memberService2 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
}
이렇게 콜 할때 마다 객체가 여러개 생기는걸 볼 수 있다. 만약 리퀘가 오만번오면 과연 감당할 수 있을까?
안된다.
따라서 하나만 만들고 공유하는 방식을 사용하는것이 좋다.
3. private 생성자를 만들어서 혹시라도 외부에서
public static void main(String[] args) {
SingletonService ss = new SingletonService();
}
를 써서 호출하는것을 막아야한다.
package hello.core.singleton;
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {
}
@Test
@DisplayName("Singleton Test")
void SingletonService() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
Assertions.assertThat(singletonService1).isInstanceOf(SingletonService.class);
이렇게 테스트를 해보면 싱글톤은 하나만 된다는것을 알 수 있다.
이 밖에 싱글톤 구현방법은 자세한 내용은 이 글을 참고하자.
스프링은 알아서 싱글톤을 적용해준다!
하지만 스프링에서는 단점은 전부 제거하고 객체관리만 싱글톤으로 해준다
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
위에 퓨어 컨테이너 코드에서 스프링 컨테이너 코드로 바꾸면, 이렇게 알아서 잘 바꿔준다.
테스트 돌려보면, 알아서 똑같은 객체를 잡아주는것을 알 수 있다.
싱골톤은 무상태로 설계되어야한다.
빈의 필드에 공유값이 들어가면 장애가 발생할 수 있다!
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;
}
}
이런 코드가 있을 때, private int price는 필드에 값을 저장한다. 그리고 특정 클라이언트에 의해 값이 변경되는 문제가 생긴다.
이러면 멀티 쓰레드에서 각 필드에 접근하다보면 값이 덮어씌어지는것처럼, 문제가 생기게 된다.
package hello.core.singleton;
public class StatefulService {
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
return price;
}
public int getPrice() {
return price;
}
}
이를 방지하려면, 필드에 값을 저장하지 말고, 받은 price를 바로 돌려주는 방식으로 처리해야한다.
Appconfig에는 여러코드가 있는데 아래 예시를 보자
@Bean memberService -> new MemoryMemberRepository
@Bean orderService -> memberRepository() --> new MemoryMemberRepository --> ???
memberSerivce를 호출하면 memberRepository가 호출되고 이는 MemoryMemberRepository를 호출한다.
OrderService를 호출하면 이는 memberRepository()를 호출하는데 이렇게 하면 new MemoryMemberRepository가 또 생성된다. 이게 싱글톤이 될까?
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("memberRepository = " + memberRepository);
System.out.println("memberRepository1 = " + memberRepository1);
System.out.println("memberRepository2 = " + memberRepository2);
이렇게 각각 호출하고 비교하는 코드를 작성해보면, 이게 다 같은 인스턴스를 공유하고 있는것을 알 수 있다.
하지만 AppConfig에서는 분명 new가 두번 부르니깐 다른 인스턴스를 써야만 할것같다.
실제로 검증을 해보면, memberRepository 메소드가 1회만 호출된것을 알 수 있다.
이는 스프링의 싱글톤 보장기능이다.
실제로 클래스명을 출력해보면 xxxCGLIB가 붙는 이상한 클래스가 나온다.
이는 스프링이 내가 만든 Appconfig를 상속받는 다른 클래스를 하나 만든 것이다. 그리고 이 다른 클래스를 컨테이너에 등록하는것이다.
이 방식을 통해 싱글톤을 보장한다.
만약 이미 등록이 되어있는거면 거기서 찾아서 꺼내온다는 의미이다.
CGLIB은 자식이라 부모조회시 자식이 끌려나오는 원리에 의해 조회된다.
이게 빠지면, 위에서 말한 CGLIB이 적용되지 않는다. 내가 만든 클래스 그 자체가 Bean으로 등록이 되는것이다.
대신, 이 방법은 싱글톤을 보장하지 않는다.
즉, 스프링 컨테이너에 등록이 되지 않는것이다.
물론 이 방법 말고도 DI주입기법중 하나인 AutoWired를 쓰면 해결될 수 도 있다.
@Configuration을 빼먹지말자...
스프링이 해주는데 굳이 안쓸 이유가?