싱글톤 컨테이너

고동현·2024년 3월 28일
0

Spring 기본

목록 보기
5/10

웹 애플리케이션과 싱글톤

웹을 생각해보면 다수의 사용자가 요청을 보낼것이다.
그림을 한번 봐보자.

클라이언트 마다 memberService를 요청하면 계속 새로운 memberService를 요청해서 반환한다.
진짜 그럴까?
AppConfig를 봐보자

전부다 new를 통해서 인스턴스를 생성하는것을 볼 수 있다.

Test검증

appconfig를 만들고
memberService를 요청을 한다.
그리고 실제 sout을 해보면 주솟값이 다르고,
isNotEqualTo가 통과를 한다.=>서로 다른 memberService

우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한
다.
고로 문제가 생기는데, 우선 요청마다 인스턴스 생성후 반환은 엄청난 비용을 초래한다.
또한, 여기 memberService에서 repository도 만드는데 이러면, 각 memberService가 다르므로, 당연히 저장소도 다르다.
그러므로, 철수가 가입을 하고, 영수도 가입을 했지만, 철수 repository에는 영수가 없고, 영수 repoistory에는 철수가 없는,
서로 다른 service와 repository를 이용하는 오류가 발생한다.

그러므로, 인스턴스를 딱 1개만 생성해서 공유하는 방식인 SingleTon 방식을 사용해야한다.

싱글톤 패턴

싱글톤 패턴이란, 클래스의 인스턴스가 딱 1개만 생성하는것을 보장하는 디자인 패턴이다.
고로, 객체 인스턴스가 2개이상 생성되지 못하도록 막아야한다.

그렇다면, 어떻게 막을까?
처음하나는 만들었다 치고, 어떻게 설정해야, 다른곳에서 new로 생성하지 못하게할까? =>private 생성자 사용하기

우선 처음에 private static final로 singletonService를 생성해준다.
자바를 잘 모르시는 분들을 위해서 private static final의 의미를 말씀드리자면,
메모리 공간은 static, heap ,stack으로 이루어져 있으며 간단히 소개하자면,
static은 static 키워드와, 전역변수가 올라가고,
heap은 참조형 객체, new로 생성된 객체
stack은 지역변수

static과 전역변수가 code 영역에 들어간다. 이건 제가 글을 쓴 운영체제 series를 봐주시면 감사하겠습니다.

쨋든 static을 사용하면 해당영역은 code영역에 들어가고, 프로그램이 시작하고 종료될때 까지 살아있는다. 고로 초기화 과정이 필요없어 static이 선언된 변수,메서드에 곧장 접근이 가능하다.

이말은, static을 사용한다는말은 해당 객체를 공유하겠다는 말이다.
만약 해당객체에서 무언가 바꾼다면 다른곳에서 해당 변수의 참조값이 변해버린다느것이다.

final은 불변인데 재할당이 불가능하게 한다. 상속 또는 최초 초기화 이후 다시 초기화가 불가능하다.

고로 private static final을 선언한 변수는=> 재할당 불가, 메모리에 한번 올라가면, static이므로 메모리에 올라가면, 같은 값을 클래스 내부의 전체 필드,메서드에서 공유하게 된다.

그래서 한번 메모리에 올려놓고, 같은값을 계속 가져다 쓰려면 private static final을 사용하면 된다.

그리고 이 SingletonService 객체를 사용하기 위해서 getInstance로 return instance를 해주고(이게 Java딱 뜰때 1개 new로 생성해 놓은 객체를 가져다 쓴다는 말이다.)

마지막으로, private생성자를 이용해서 외부에서 new를 불가능하게 막아놓는다.

정리 해보자면

  • static영역에 필요한 객체 singletonService 인스턴스를 미리 생성해 놓는다.
    private static final이므로, 재할당 불가능, static이므로, 공유하는 전역변수

  • 이 객체 인스턴스가 필요하면 오직 getInstance로만 조회가능 왜냐? private static final이므로 get메서드를 제외하고는 접근 불가

  • 딱 1개의 instance만 존재해야하므로 생성자로 private으로 막아 외부에서 new를 불가능하게 하였다.

    Test Code

getInstance로 가져오고 isSameAs로 확인하였다.

참고)
isSameAs vs isEqualTo
이전에 isEqualTo를 썻던것을 기억할 것이다.
그렇다면 sameAs와 EqualTo의 차이가 무엇일까?

Member member1 = new Member(1L, "memberA", Grade.VIP);
Member member2 = new Member(1L, "memberA", Grade.VIP);
//↑ member 1 과 member 2 는 같은 속성의 값을 갖지만, 다른 참조값을 갖고 있는 서로 다른 객체이다.
        
String a = "HI";
String b = new String("HI");
        
//Case 1
assertThat(member1).isSameAs(member2);
        
//Case 2
assertThat(member1).isEqualTo(member2);

이렇게 되면 case1과 2에서 오류가 나온다.
왜냐하면 equalTO는 같은 인지를 확인한다. 객체가 비교대상이면 Java의 equals()와 같은 기능을한다.

isSameAs는 객체의 참조값이 같은지 비교한다.

고로 특정값을 비교하거나, string비교시에는 isEqualTo를 사용하고, 객체의 참조값(주소)를 비교할때는 isSameAs를 사용한다.

https://inkyu-yoon.github.io/docs/Language/Spring/IsEqualIsSame

싱글톤 컨테이너

이제 스프링 컨테이너를 사용해볼 것이다. 이전에도 계속 사용해봤지만,,, 스프링 컨테이너를 사용하면, 앞에서 처럼 getInstance메서드만들고 private 생성자막고 이런 코드들을 내가 직접 안쳐도 된다.

스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
그래서 우리는 스프링 컨테이너에 있는 공유하는 1개의 인스턴스를 가져다가 쓰면 되는것이다.

TestCode

혹시 이번 글만 봐서 ApplicationContext가 뭔지 왜 여기서 AnnotationConfigApplicationContext를 썻는지는 저의 이전글에서 자세히 정리했으니 그 글을 봐주시면 됩니다.

간단히 말씀드리면, ApplicationContext가 스프링 컨테이너이고, 여기서 AppConfig를 어노테이션 기반으로 Bean등록을해서 new Annotation...으로 인스턴스를 생성했습니다.

그리고 ac.getBean으로 인스턴스를 가져오고 동일한지 확인하였습니다.


그림과 같이 이제는 요청이 들어올때 동일한 memberService를 반환합니다.

싱글톤 방식의 주의점

싱글톤 패턴, 싱글톤 컨테이너는 결국 객체 인스턴스 하나만 생성해서 공유하는 방식을 사용한다.

그래서 stateful하게 설계하면 안된다.

무상태 설계가 필요하다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
  • 가급적 읽기만 가능해야한다.
  • 필드 대신에 지역변수,파라미터,TreadLocal등을 사용해야한다.

일단 그래서 이게 뭔데? 싶을 수 있다.
처음부터 생각을 해보자 userA가 주문 1000원을 했고 userB가 주문 2000원을 했다.

아 그럼,orderSerivce에 price 필드를 넣고 여기서 주문한사람의 금액을 할당하자

public class orderServ ice{
   private int price; //각 user마다 금액이 할당됨
   ...
}

충분히 이렇게 생각하고 할 수 있다.
근데 이렇게 되면 문제가 뭘까?

이런 배경을 생각하고 test code로 가보자


우선 service를 만들었다. order가 들어오면 그 가격을 이 price에 저장하고, order메서드에서 이 price를 할당해주었다.
getPrice는 이 price를 return해준다.


그 다음 TestConfig에서 bean으로 이 statefulService를 스프링 컨테이너에 등록시켜준다.=>이렇게 되면 공유되는 StatefulService인스턴스를 사용하게된다.


그 다음 applicationCOntext 스프링 컨테이너에서 꺼내쓸것이고, 맨첫번째줄 내용

그다음 userA,userB가 주문을 할거니까 getBean으로 StatefulService를 가져와준다.

그리고 order메서드로 주문을 하는데 문제가 뭐냐면
앞에서 getBean으로 가져올때 이거는 같은 인스턴스이다. 공유를 하니까 그런데 여기서 price라는 상태를 유지하는 필드가 있으니까
userA의 order에서 1000원이 되고 그다음 userB의 order에서 2000원이 된다.

고로 공유되는 필드로 인해 특정 클라이언트의 값이 반환되는 결과가 나온것이다.

그럼 이런 문제는 어떻게 해결해야할까?

지역변수를 이용해보자,
이렇게 공유되는 필드가 아니라

이런식으로 그냥 order를 반환해주는것이다.
예를 들면 뭐 vip면 여기서 price들어오면 10%할인된 가격을 return해주는것이다.

그러면 testcode에서는
int price = statefulService1.order(vip,1000);이런식으로
지역변수로 해결 할 수가 있다.

또 다른 방식으로는 ThreadLocal을 이용하는 것이다.
강의에서는 ThreadLocal을 이용하라고 했지 자세한 내용은 다루지 않았는데
다른 글에서 정리를 해보겠다.
ThreadLocal이란?

어쨋든, 이번 시차의 결론은 공유필드는 없어야한다. 스프링 빈은 항상 무상태로 설계해야한다.

Configuration

저번 의존관계주입 글
저번시간의 글을 보면 컨테이너 등록과정과, 의존관계 주입이 따로 있다는 사실을 잠깐 언급하고 넘어갔다.
이번 시차는 그 내용을 설명하는 것이다.

보자,

이제 스프링 컨테이너가 올라갈때, bean 애노테이션 적혀있는걸 전부 등록할 것이다.

  1. memberService에서 memberRespository를 부른다.
  2. memberRepository에서 메모리멤버리포지토리를 맞든다.
  3. orderService에서 memberRespotiroy와 discountPolicy를 부른다.
  4. memberRepository를 부른다.
  5. discountPolicy를 각각 부른다.

고로, 순서는 보장하지않지만, 결국 5번의 함수 호출이 일어나야한다.

실제로 하지만 딱 memberService, orderService, dicountPolicy 3번의 호출만 일어난다.

어? java code가 무슨 용뺴는 재주가 있는것도 아니고 이건 어떻게 하는걸까? 싶다.

그이유는 @Configuration에 있다.

test code

실행을 시키면,

AppConfig뒤에 뭐가 더 딸려서 나온다.
우선 AppCofig도 스프링 빈으로 등록이 된다.
왜냐하면 AnnotationConfigApplicationContext에 파라미터로 넘긴 값은 스프링 빈으로 등록되기 때문이다.

만약에 순수한 JAVA class라면 class hello.core.AppConfig 여기까지 출력되어야한다.

근데 CGLIB가 붙었다.
이것은 내가 만든 클래스가 아니고 스프링이 CGLIB라는 바이트 코드 조작 라이브러리로 AppConfig를 상속받는 임의의 appConfig를 만들고 그것을 스프링 빈으로 등록해 놓은것이다.


그래서 이 내부의 AppConfig CGLIB 인스턴스는

그냥 Appconfig와 달리, 이런식으로 되어있다.

만약 memoryMemberRepository가 이미 컨테이너에 등록되어있으면 컨테이너에서 찾고, 아니면 기존로직으로 memorymemberRepository를 생성하고 반환한다.

이덕분에, 싱글톤이 보장되는것이다.

참고로, ac.getBean(appConfig@CBLIB어쩌구)가 아니라 AppConfig.class인데 다 딸려나오는것은
Appconfig를 appConfig@CBLIB가 상속받아서 자식은 전부다 튀어나오므로, 조회가 가능하다.

만약에 그럼, @Configuration을 적용하지 않고 @Bean만 적용하면 어떻게 될까?

스프링컨테이너에 등록이 될까? 된다..
근데 문제가 싱글톤을 보장하지 않는다.

여기서 AppConfig는 스프링 컨테이너가 관리하는 CBLIB가 아니라, bean = class hello.core.Appconfig로 순수한 AppConfig가 등록되게된다.

이렇게하면
이런식으로, 맨처음 생각했던것처럼 5번이 호출되게 된다.

인스턴스도 당연히 다르다.


결국 Configuration이 없으면 Bean으로 등록된다?O=>그런데 싱글톤을 보장한다?X이다.

memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할때 싱글톤을 보장하지 않는다는 말이다.

결국 Configuration,Bean을 둘다 써야한다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글