스프링

스프링 기초 - 싱글톤 컨테이너

ho코딩 2024. 2. 14. 17:21

Di 컨테이너까지 알아보았고, 스프링으로 전환하여 컨테이너를 스프링 빈 등록 및 컨테이너에 대해서 알아보았습니다.

 

애플리케이션이 실행되면 클라이언트가 요청할 때 마다 DI 컨테이너에서 객체가 생성됩니다. 

 

 

하지만 그림처럼, 웹 애플리케이션은 소수가 이용하기보다는 다수의 클라이언트가 이용하기 때문에 

 

만약 고객 트래픽이 초당 10000건이 나오면 초당 10000건의 객체가 생성되고 소멸된다. 

-> 메모리가 낭비가 너무 심하다. 

객체 생성 테스트

 

테스트 결과

 

테스트 결과의 노란 부분을 보면  memberService1과 memberService2의 주소가 다른 것을 알 수 있다.

때문에 만약, 한 번에 수만~수십만의 트래픽이 발생한다면 서버가 터져버리고 말 것이다. 

 

그렇기 때문에 해결방안으로는 , 객체가 딱 1개만 생성되고, 생성된 객체를 공유하도록 설계하면 된다. 

-> 이를 "싱글톤" 패턴이라고 한다. 

 

 

 

싱글톤 패턴

 

싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다 .

때문에 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야한다. 

-> private 생성자를 사용해서 외부에서 임의로 new 키워드를 통한 객체 생성을 막는다. 

 

 

위처럼 

1. static 영역에 객체를 딱 1개만 생성해서 미리 올려놓는다. 

->static이란 class 안에서 마치 공유되는 키워드 입니다. 

-> 위 사진에서는 SingletonService 클래스 안에 있는 메서드 끼리는 SingletonService()를 인스턴스 객체 생성 없이 

 직접 호출 가능합니다. 

 

2.public으로 열어서 객체 인스턴스가 필요하면 static 메서드를 통해서만 조회되도록 허용한다. 

-> getInstance() 메서드는 public이기 때문에 외부에서 접근이 가능합니다. 이때 getInstance() 메서드는 

미리 1번에서 생성해둔 SingletonService 객체를 반환해줍니다. 

 

3. 마지막으로 private 선언으로 외부에서 new 키워드를 통해 SingletonService()의 새로운 객체가 생성되지 않도록 막습니다. 

 

싱글톤 패턴 테스트
결과

 

이렇게 코드를 짠 후 테스트를 돌려보면, 이번에는 각각 불러낸 객체의 메모리 주소가 같은 것을 알 수 있습니다. 

다시말해서, 같은 객체가 불러졌습니다. 

 

- 참고로, 싱글톤 패턴을 구현하는 방법에는 다양한 방법이 있으나, 여기에서는 객체를 1개만 미리 생성해두는 

단순하고 안전한 방법을 선택했습니다. 

 

하지만 싱글톤 패턴은 여러 문제점을 가지고 있습니다. 

1.싱글톤 패턴 구현 코드 자체가 많다.

2.의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP 위반

3. 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.

4.테스트하기 어렵다.

5.private 생성자로 자식 클래스를 만들기 어렵다. 

6. 안티패턴이라고 불린다. 

 

 

싱글톤 패턴 해결책 -> 싱글톤 컨테이너

 

싱글톤 컨테이너 

-> 스프링의 편리함이 나타나는 부분이다. 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 

싱글톤으로 관리해줍니다 .

->덕분에 지저분한 코드가 들어가지도 않고, private 생성자, DIP,OCP,테스트 등의 단점도 극복해줍니다. 

 

스프링 싱글톤 컨테이너

 

어디서 많이 본 코드가 등장합니다. 

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); 

 

AnnotationConfigApplicationContext 위에서 사용한 스프링 컨테이너 코드입니다.

이 코드가 싱글톤을 유지해주고 있었습니다. 설정 정보인 AppConfig.class를 컨테이너에 담아서 

 

memberService를 각각 getBean()하면 

 

메모리 주소가 같은, 다시말해서 같은 객체를 반환해줍니다. -> 싱글톤이라는 뜻.

 

 

 

싱글톤 방식의 주의점

 

 

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든,

객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가

하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안됩니다. 

-> 무상태로 설계해야 합니다. 

*stateful/stateless (상태/무상태)에 관한 내용은 HTTP 카테고리에 있습니다. 

 

로그인을 예시로 설명하자면, 만약 stateful하게 설계를 한다면, 

사용자1이 로그인을 성공함 -> 객체는 '로그인' 상태로 변경후 반환됨

사용자2가 로그인을 시도했으나 로그인 실패함 -> '로그인' 상태가 유지된 객체를 받았으므로 실패해도 로그인 성공 상태. 

 

이러한 보안적인 문제가 발생할 수 있습니다. 

 

Stateful 설계 예시
stateful 설계 문제점 예시

 

stateful 결과

 

이 예시를 통해서도 알 수 있습니다. 

A사용자는 10000원을 주문하고 , B 사용자는 20000원을 주문했는데

싱글톤 객체 1개를 가지고 여러 사용자가 공유하고 상태까지 유지해버리니 

statefulService1의 가격을 getPrice()해도 20000원이 찍혀버립니다. 

 

이러한 이유 때문에 stateless를 유지해야합니다. 

 

 

Configuration과 싱글톤

 

그전에 이상한 점이 있습니다. 

AppConfig 코드를 보면

AppConfig의 이상한점

 

memberService 빈을 만드는 코드에서 memberRepository를 한 번 호출하고 

orderService 빈을 만드는 코드를 보면 memberRepository를 또 한 번 호출한다. 

-> 그러면 new MemoryMemberRepository가 2번 호출되면서 싱글톤이 깨지는것 아닌가? 

 

스프링 컨테이너가 이것도 해결해주는가? 

 

싱글톤 검증

싱글톤이 깨지는지 안깨지는지 확인하기 위해 

OrderServiceImpl과 MemberServiceImpl 코드에 위 사진의 코드를 추가해줍니다. 

 

싱글톤 검증

그리고 테스트 코드를 작성하여 memberServiceImpl , orderServiceImpl, memberRepository의 주소값이 

모두 같은지 확인을 해보았습니다.

 

위처럼 모두 같은것을 확인할 수 있었다 ! 

 

어떻게 스프링은 이를 싱글톤으로 유지할 수 있었을까?

 

그 비밀은 @Configuration을 적용한 AppConfig(설정파일)에 있었습니다. 

 

이 코드를 보면, AnnotationConfigApplicationContext의 파라미터로 넘긴 AppConfig는 스프링 빈으로 등록된다.

때문에 AppConfig도 스프링 빈이 된다. 

 

조회해보면 bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70 이와같이 

빈으로 등록된 것을 확인할 수 있다. 만약 빈으로 등록된것이 아닌 순수한 클래스라면 

`class hello.core.AppConfig` 이 값이 출력되었을 것이다. 

 

그리고 

bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70  에서 보면 

CGLIB라는 코드를 볼 수 있는데, 이가 바로 바이트코드 조작 라이브러리를 사용한 것이다. 

이 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 클래스를 빈으로 등록했다.

 

 

그니까 엄연히 말해서 AppConfig도 스프링 빈으로 등록되지만

AppConfig를 상속받은(자식타입) 확장된 버전의 AppConfig@CGLIB도 한 것 입니다.

AppConfig@CGLIB는 추가적으로 빈의 특별한 동작 또는 AOP(Aspect-Oriented Programming)를 가능하게 만듭니다 .

 

AppConfig@CGLIB의 코드를 예상해보자면 

이렇게 만약, 컨테이너에 존재하다면 있는거 반환, 없다면 만들어서 컨테이너에 등록일것입니다.

 

이 덕분에 싱글톤이 보장됩니다.  

 

주의할 점은 @Configuration과 @Bean 모두 존재해야 싱글톤이 보장되므로 

설정파일에는 항상 @Configuration 어노테이션을 사용해야합니다.