즉 클라이언트A,B,C 가 memberServic 요청을 하면 DI컨테이너(AppConfig)에서 new memberService x01,x02,x03 ... 과 같이 요청마다 객체를 생성하는 문제가 발생하는게 아닌가?
직접 테스트해보자
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
//1. 조회 : 호출할 때 마다 객체를 생성.
MemberService memberService1 = appConfig.memberService();
//2. 조회 : 호출할 때 마다 객체를 생성.
MemberService memberService2 = appConfig.memberService();
//3. 참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberServic1 != memberService2
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
결과는 다음과 같이 나왔다!
memberService1 = hello.core.member.MemberServiceImpl@5dda768f
memberService2 = hello.core.member.MemberServiceImpl@7a8c8dcf
즉 요청마다 새로운 객체를 만들고 있다. -> 즉 메모리를 계속해서 잡아먹는다 -> 매우 비효율적.
이런 문제를 해결하기 위한 방법이 바로 싱글톤 패턴이다.
즉 해당 객체가 딱1개만 생성되고 공유하도록 하는 방법이다.
public class SingletonService {
//static으로 생성(단1개만 생성)
private static final SingletonService instance = new SingletonService();
//instace의 참조를 꺼낼 수 있는 방법은 이제 이 메서드 밖에 없다.
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
이제 TestCode를 통해서 확인해보자 같은 Instance를 가리키는지 확인할 수 있다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
}
결과는 다음과 같이 같은 Instance가 나온것을 알 수 있다.
singletonService1 = hello.core.singleton.SingletonService@ba2f4ec
singletonService2 = hello.core.singleton.SingletonService@ba2f4ec
그렇다면 AppConfig도 싱글톤으로 변경해주면 될까?
맞다 그렇지만 모두 수작업으로 변경할 필요가 없다. 스프링 컨테이너를 사용하면 기본적으로 객체를 싱글톤으로 관리해준다!
그렇다면 어떻게 싱글톤으로 관리하면서 단점들을 상쇄할 수 있을까?
일일이 싱글톤 패턴코드를 적용하는게 아니라 스프링 컨테이너를 거치게 되면 자동으로 싱글톤으로 관리하게 된다.
그러면 추가적인 코드를 작성할 필요도 없으며 구체 클래스에 의존할 필요성이 없어진다.
TestCode로 확인해보자
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
//3. 참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberServic1 != memberService2
assertThat(memberService1).isSameAs(memberService2);
}
결과는 다음과 같다.
memberService1 = hello.core.member.MemberServiceImpl@5dd91bca
memberService2 = hello.core.member.MemberServiceImpl@5dd91bca
스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라. 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.
싱글톤 패턴을 사용하던 싱글톤 컨테이너를 사용하던! 인스턴스를 하나만 생성해서 공유하는 방식은 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
다음과 같은 경우로 확인할 수 있다.
price라는 상태를 가지는 필드를 가진 class가 있고 컨테이너를 통해서 싱글톤방식으로 공유되고 있다.
public class StatefulService {
private int price; //상태를 유지하는 필드
public void order(String name , int price) {
System.out.println("name = " + name + " pirce = " + price);
this.price = price; // 여기가 문제!
}
public int getPrice(){
return price;
}
}
그런데! 유저A 유저B가 order()메서드를 통해서 주문을 넣을때 바로 price의 값이 변경되버린다는 점이다.
문제는 유저A의 값이 유저B가 주문한 값으로 변경되되는 점아다.
TestCode는 다음과 같다.
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//ThreadA : A사용자 10,000원 주문
statefulService1.order("userA",10000);
//ThreadA : B사용자 20,000원 주문
statefulService2.order("userB",20000);
// ThreadA : 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
결과는 다음과 같다.
name = userA pirce = 10000
name = userB pirce = 20000
price = 20000
이 문제는 아주 간단한 예시일뿐 이 상황에서도 유저A의 결제가 20000원으로 이루어질 위험이 존재한다는 것이다.
더욱더 복잡한 문재가 발생한다면 피해가 기하급수적으로 커질 수 있다.
그렇기 때문에 this.price를 통해서 변경하는 부분을 제거하고 return을 바로해버리면 된다.
그리고 price를 출력하는게 아니라 userAprice ,userBprice와 같은 지역변수로 받을 수 있다.
의외로 공유필드와 관련된 문제가 실무에서도 꼭 한번씩은 나온다고 한다!
스프링 빈은 항상 무상태(stateless)로 관리하는것이 좋다.
인프런 김영한님의 강의를 공부하면서 정리한 내용이다.
단순이 강의 내용을 따라하면서 정리한것이지만 그냥 라이브코딩으로 따라하는것 보다는 생각이 정리되는점이 많아서 훨씬 이해도가 높을 수 있었던것 같다.