[Spring 기본] Spring Container의 Singleton 전략

2022. 10. 14. 08:05·Spring/Spring 기본
728x90
반응형

스프링은 대부분이 웹 앱을 사용됩니다. 저 또한 웹 앱을 만들기 위해서만 사용했고, 그 외의 사용은 본 적도 없네요.

웹 앱의 가장 큰 역할, 가장 중요한 역할은 바로 '서버로서의 대응'입니다. 웹 앱은 보통 다수의 고객들의 요청을 동

시에 처리하게 됩니다. 잘나가는 웹 서비스들은 한 고객만이 앱을 사용하는 동안 수백번씩의 요청이 이루어지기도

합니다. 

 

만약 Spring이 지금까지 우리가 설계한 설정대로 Bean을 생성하고 주입해준다면 어떻게 될까요? N명의 고객들이

요청을 하여 Service 대응이 필요할 때, Service 객체가 그대로 N번 생성이 되게 됩니다. 그리고 객체를 생성하기 위

해 메모리를 할당할 것이고, 그 주소 역시 힙에 저장하는 과정을 반복하게 됩니다. 이렇게 된다면, 심각한 메모리 낭

비가 발생하고, 진작 JVM이 없었다면 요청이 조금만 많이 들어와도 바로 메모리 과부하가 발생할 것입니다. 그 해

결책을 위해 Spring이 채택한 방식은 바로 Singleton 전략입니다.

 

(싱글톤 컨테이너 - 웹 앱과 싱글톤 강의 1분대 그림)

 

 

Singleton 전략

 

이번 포스트에서는 Spring과 연관이 아주 깊은 Singleton 전략과 그 연관성에 대해서 살펴보겠습니다. 우선 싱글톤

패턴이란 특정한 객체에 최초 한번만 메모리를 할당하고 해당 메모리에 static 인스턴스를 만들어서 활용하는 Java

디자인 패턴을 말합니다 (추후 가능하다면 Java Design Pattern 들에 대해서도 한번 정리해보겠습니다).

 

쉽게 말해서, 해당 프로그램에서 단 하나의 공유 인스턴스를 만들어놓고, 그것을 다같이 사용하는 패턴을 말합니다.

보통 한 프로그램에 한 객체만 존재해야 할 때, 동일한 객체를 지속적으로 반복해서 사용해야 할 때 Singleton 방식

을 사용합니다.

 

보통 싱글톤 패턴으로 객체를 디자인하면, 다음과 같다고 말할 수 있습니다. 

 

public class SingletonClass{
    public static SingletonClass INSTANCE = new SingletonClass();
    
    private SingletonClass(){ // 외부에서의 생성을 막음
    }
    
    public static getInstance(){
    	if(INSTANCE==null){ // 만약의 에러를 대비
        	INSTANCE = new SingletonClass(); 
        }
        return INSTANCE;
    }
}

 

Spring 역시 Client 의 요청에 효율적으로 대응하기 위해서 Bean을 Singleton 전략으로 관리를 합니다. 그렇다면

지금까지 만든 Bean 등록되는 모든 객체들을 다 Singleton 디자인 패턴으로 바꿔줘야 할까요? 그렇지 않습니다.

Spring Conatiner 에 저장되는 객체들은 기본적으로 모두 Singleton 객체로 관리될 수 있도록 지원하기 때문에,

객체 코드, Configuration 코드들을 수정할 필요가 없습니다. 

 

Singleton의 장점은 확실히 알겠습니다. 하지만 언제나 그렇듯이 Singleton 역시 단점이 있습니다. 개인적으로 S

W 분야에서는 기술이 계속 좋은 방향으로 발전하는 것처럼 보이기 쉽다고 생각합니다. 하지만 어떤 기술을 도입하

든, 그 기술의 단점에 대해서도 확실히 알고 있어야, 원리를 제대로 이해할 수 있고, 문제 발생시 제대로 대처할 수

있다고 생각하기 때문에, 항상 의심하는 습관을 갖는것이 개인적으로 좋은 것 같습니다.

 

1. 싱글톤 객체 하나를 위한 코드가 너무 복잡함
2. 클라이언트가 어떤 객체를 사용하는지 알게됨(SingletonClass.getInstance()를 사용해야 하기 때문)
3. 2번으로 인해 DIP, OCP 문제가 있음
4. 내부 속성 변경, 초기화가 어렵기 때문에 유연성이 떨어짐

 

 

 

Spring과 Singleton Conatiner

 

 

위에서 말했듯이, 스프링에서는 싱글톤 패턴을 적용시키지 않아도, 객체 인스턴스들을 알아서 싱글톤으로 관리해줍

니다. 

 

----

싱글톤 컨테이너 5분 20초 대

---

 

스프링 컨테이너는 기본적으로 싱그톤 컨테이너의 역할을 하게 되며, 이런 관리 기능을 Singleton Registry 라고 부

릅니다. 위의 1~4 문제들이 어떻게 해결되는지 살펴봅시다. 

 

1. 해당 객체를 Bean으로 관리한다 등록하면 자동으로 싱글톤 객체로 관리, 코드 패터닝 불필요
2,3. 스프링 컨테이너는 기본적으로 의존성 주입되어 완성된 객체를 사용, 추상화에 최적화 (DIP, OCP, Test, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있음)
4. 싱글톤의 특성에 국한되지 않고 자유롭게 사용할 수 있기에, 유연성이 좋음

 

Spring은 이와 같이 Singleton 패턴의 단점들도 다 해소해주면서, 완벽을 지향하는 객체지향 설계를 지향합니다.

지난번에 사용하던 AppSpringConfig.class 설정 클래스를 가지고 와서 이어가겠습니다. 

 

@Configuration
public class AppSpringConfig {

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }

}

 

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void test2(){
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppSpringConfig.class);

    MemberService memberService = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);
    
    Assertions.assertThat(memberService).isSameAs(memberService2);
}

 

AppSpringConfig.class 를 보면 알겠지만 싱글톤 관련된 코드는 하나도 없고, 오히려 memberService 안에서

new MemberService() 를 매번 return 하는 코드임을 확인할 수 있으나, 스프링 컨테이너가 한번 빈으로 등록된

MemberService 객체를 싱글톤으로 관리하고 있는 모습을 확인할 수 있습니다.

 

 

 

싱글톤 방식의 주의점 (매우 중요)

 

 

운영체제를 공부하시거나, 운영체제 포스트를 봐주신 분들은 바로 느낌이 오셨겠지만, 싱글톤 방식은 다르게 말하

면 "한 자원을 쓰레드들끼리 공유하는 상태" 입니다. 즉, Concurrency 문제가 발생할 가능성이 있습니다. 따라서

Singleton 객체들은 상태가 유지되는 Stateful 설계가 되면 안됩니다. 

 

Singleton 객체들은 다음과 같은 사항들을 고려하여 특정한 상태가 유지될 수 없는 Stateless 한 상태로 설계되어

야 합니다. 

 

1. 특정 Client에 의존하는 필드가 있으면 안됨
2. 특정 Client 가 값을 변경할 수 있는 setter 가 있으면 안됨
3. 읽기 및 Custom 활용만이 가능해야함
4. 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다

 

Singleton 객체들을 생성해보시면 위 4개를 지키는 것은 그렇게 어려운 사항이 아닙니다. 하지만 Spring이라면 얘

기가 조금 달라지죠. Spring Container 즉, Singleton Container에서 관리하는 객체들은 우리가 "싱글톤으로 만

들어야지!" 하고 만드는 객체들이 아닙니다. 일반적으로 만드는 객체들을 Spring에서 Singleton화 시켜주는 것이

기 때문에, Bean 관리 대상 객체들을 만들 때는 항상 주의해야 합니다. Bean 필드에 공유 값을 설정하면, 정말 큰

장애가 발생할 가능성이 높습니다. 다음과 같은 예제를 살펴봅시다.

 

public static class PriceCalculator{
    
    private int discount = 10000;
    
    public static PriceCalculator INSTANCE = new PriceCalculator();
    
    private PriceCalculator(){
	}
    
    public PriceCalculator getInstance(){
    	if(INSTANCE == null) INSTANCE = new PriceCalculator();
    	return INSTANCE;
    }
    
    public void setDiscount(int discount){
    	this.discount = discount;
    }
    
    public int getPrice(int prePrice){
    	return prePrice - discount;
    }
}

 

Thread A와 Thread B가 동시에 Calculator 를 사용하기 위해 접근한다고 가정하겠습니다. 각자의 가격을 들고

PriceCalculator 객체를 사용하나, Thread A는 처리하는 고객이 VIP라 discount 를 더 상승시켜야 한다고 가정해

봅시다. 

 

PriceCalculator pc = PriceCalculator.getInstance();
pc.setDiscount(20000);

 

만약 이와 같은 설계를 한다면, ThreadB는 고정 할인율이 변한것을 모르고, 20000원의 할인을 받게 되어 비즈니

스 에러가 발생하게 됩니다. 일단 위의 예시는 설계부터 잘못되어 있기 때문입니다. Thread A 클라이언트에 의존하

여 discount 를 바꿨고, setter 함수부터 열려있는게 문제입니다. 할인 계산기를 앱 전체에서 쓰고 싶어서 싱글톤으

로 설계할거라면, 각 쓰레드 별로 할인율을 들고오게 하는게 맞고, 계산기에서는 결과 로직만 처리해주는게 맞습니

다. 

 

위와 같은 문제는 운영체제에서 가장 중요하게 생각하는 문제 중 하나인 Concurrency 문제입니다 (한번 살펴 보시

려면 포스트를 확인해주세요 ㅎㅎ). 물론 한 행위를 돌려 놓는 로직을 적용한다든가, 세마포어를 적용하면 싱글톤

객체에서도 사용할 자원을 둬도 되지만, 굳이 그럴 필요가 없기 때문에 지양하는 것이 좋습니다.

 

싱글톤 설계하자고 마음을 먹어도 위와 같은 문제가 발생합니다. 그러므로 싱글톤으로 자동으로 관리되는 Bean 은

항상 무상태로 설계할 것을 명심해야 합니다. 근데 하다보면 또 안됩니다 당연히. 분명히 Bean 등록 객체에도 필요

한 것들이 있고, 아무 생각 없이 setter를 만들게 되고, 또 테스트에서 문제 안걸리고 나중에 운영 가야 문제가 생깁

니다.

 

Service, Repository 등은 보통 DB 관련된 일들을 주로 수행하지만, 특별하게 쓰이는 변수들이 있을 수도 있고, 필

요한 자원을 공유 객체로 등록할 수도 있습니다. 이럴 경우 위와 같이 지역변수 등을 활용해서 각자의 값을 가지게

가게 하는 등 Concurrency Control 을 꼭 해줘야 합니다. 당연히 설계는 마음대로지만, 그냥 "Bean 에 등록될 객

체다!"와 "Concurrency 문제는 항상 발생한다!"라는 점을 명심하고 Spring 설계를 해나가면 될 것 같습니다. 걍

안 만들고 안건드리는게 당연히 제일 낫습니다 ㅎㅎ

 

 

 

그래서 스프링이 그거 어떻게 하는건데

 

 

자동관리 뭐 다 좋은데, 이 쯤 와서 궁금할 수는 있습니다. 아무리 스프링이라지만 그래도 JAVA인데... 위에도 있는

AppSpringConfig 설정 클래스를 보면.... 어쨌든 계속 new 를 사용해서 객체를 호출하는 모습을 볼 수 있습니다.

이게 어떻게 싱글톤이라고 할 수 있을까요?

 

@Test
@DisplayName("싱글톤 관리가 정말될까?")
void test7(){

    // 1차 MemberRepository 호출 (MemoryMemberRepository 로 설정됨)
    MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);

    // 2차 MemberRepository 호출 + 1차 DiscountPolicy 호출
    OrderServiceImpl orderSerivce = ac.getBean("orderService", OrderServiceImpl.class);

    // 3차 MemberRepository 직접 호출
    MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);


    // 테스트를 위해 꺼낼 수 있게 만든 get method
    // 각 서비스에서 꺼내서 같은 애들인지 확인해보자.

    // 1차로 가져온 친구
    MemberRepository memRepoFromMemService = memberService.getMemberRepository();
    MemberRepository memRepoFromOrdService = orderSerivce.getMemberRepository();

    // 정말 셋 다 같을까?
    Assertions.assertThat(memRepoFromMemService).isEqualTo(memRepoFromOrdService);
    Assertions.assertThat(memRepoFromMemService).isEqualTo(memberRepository);

}

 

위 테스트 케이스는 무사히 통과하므로, 셋 다 같다고 할 수 있고, 싱글톤으로 관리되고 있는게 맞음을 알 수 있습니

다. 자바 코드 상 매번 다른 것이 호출되어야 하는데, 이게 어떻게 된 일인지 AppSpringConf.class에 로그를 찍으

면서 확인해보자. 

 

@Configuration
public class AppSpringConfig {
    @Bean
    public MemberRepository memberRepository(){
        System.out.println("AppSpringConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }

    @Bean
    public MemberService memberService(){
        System.out.println("AppSpringConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService(){
        System.out.println("AppSpringConfig.orderService");
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }
}

 

위 테스트 코드를 우리가 아는 자바대로 잘 실행이 된다면, 다음과 같은 예상 순서로 call 들이 찍힐 것입니다.

 

calling AppSpringConfig.memberService
calling AppSpringConfing.memberRepository // 멤버서비스에 의한 첫번째 호출
calling AppSpringConfig.orderService
calling AppSpringConfing.memberRepository  // 오더 서비스에 의한 두번째 호출
calling AppSpringConfig.memberRepository // 직접 호출에 의한 세번째 호출

 

그렇다면 이제 실행시켜서 실제 순서를 볼까요?

 

calling AppSpringConfig.memberRepository
calling AppSpringConfig.memberService
calling AppSpringConfig.orderService

 

이와 같이 MemberRepository 호출은 한번 이루어진다는것을 알 수 있습니다. 우리가 생각한 자바의 흐름과는 다

르게 스프링의 싱글톤 Container는 싱글톤을 확실히 보장해주는 모습을 확인할 수 있습니다. 그리고 그 비밀은

@Configuration에 있다고 합니다.

 

 

(@Configuration 과 바이;트코드 조작의 마법 강의 3분 40초 그림)

 

 

AppSpringConfig.class 는 분명히 우리가 만든 클래스가 맞습니다. 하지만 @Configuration 이 붙는 순간 실행시

점에 AppConfig@CGLIB 라는 나의 클래스를 상속받는 임의의 다른 클래스를 만들게 됩니다. 이 상속 클래스를

Bean으로 등록하라고 하는 반환 객체를 각 methodName 으로 Singleton Container에 등록을 시킵니다 (Bean

들을 모두 뺐을 때 모든 설정 클래스들과 AppSpringConfig 도 Bean 으로 등록되어 있는 이유). 즉, 자바대로 작동

하지만, 내부적으로 스프링이 자체 클래스를 만들어서 엮어주게 되는 것입니다.

 

이 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해줍니다. 이 임의의 클래스는, 아마 다음과 같이 형성이 될 것

이라고 예상을 하면 될 것 같습니다.

 

class XXCGLIB extends AppSprinfConfig{ // 내가 만든 Config의 자식
    ...
    @Override
    @Bean
    public Member membeRepository(){
    	if(ApplicationContext has MemoryMemberRepository){
        	return MemoryMemberRepository from Current Bean Container
        }else{
            build MemoryMemberRepository from My Class
            return MemoryMemberRepository
        }
    } 
   ...
   }

 

이와 같이 현재 스프링 컨테이너 내가 지금 호출하려는 구현 객체가 있다면, 그것을 반환하고, 없으면 설정해준 로

직으로 구현 객체를 생성해서 반환해줄 것입니다. 그리고 생성을 했다면 그것을 Spring Container에 넣어주도록

동작할 것으로 보입니다.

 

 

이와 같은 방식으로 @Configuration 을 달아준 Class 에 대하여 스프링 컨테이너는 Singleton 방식을 보장해줍니

다. 하지만 오해하시면 안되는게, @Configuration이 없어도, @Bean이 붙은 대상들은 Spring이 자동으로 Singleton화 시켜서 관리해줍니다 (@Bean도 위처럼 변환시켜서 컨테이너에 저장하는게 있을 것으로 보입니다).

(다음 질문을 참고하시죠 :: )

 

항상 누군가가 대신 짜준 코드가 있기에 내가 쓰는 기능들이 보장되는 모습으로 보이네요.. 대단한 사람들...

 

이번 포스트는 이렇게 마무리해 보겠습니다. Spring Container 가 Bean 들을 관리하는 방식, 관리해달라고 말하는

원리에 대해서 알아봤으니, 이제는 이 복잡한 과정을 어떻게 편하게 할 수 있도록 Spring이 지원하는지 알아보겠습

니다 : ) 감사합니다!

 

 

 

 

 

포스트 요약

 

 

  • 싱글톤이란 자바 디자인 패턴 중 하나로, 앱 내에서 한 객체의 생성 및 공용 사용을 보장해주는 패턴이다
  • 수많은 요청들을 처리할 웹 서버에서는 싱글톤 패턴이 효율적인 메모리 및 응답에 적합하다
  • 스프링에서 @Configuration은 등록하려는 @Bean들을 자동으로 싱글톤 패턴으로 관리하는데, 이는 CGLIB라는 스프링 자체적인 로직으로 가능하다
  • 싱글톤을 사용할 땐 객체를 Stateless로 설계해야 하며, Concurrency 문제에 유의해야 한다

 

 

 

 

출처

 

 

 

[스프링 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 김영한님의 [스프링 핵심 원리] 강의를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다. 

 

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

728x90
반응형

'Spring > Spring 기본' 카테고리의 다른 글

[Spring 기본] 의존 관계 주입 전략 (DI Strategy)  (0) 2022.10.17
[Spring 기본] Component Scan을 통한 Bean 자동화 관리  (0) 2022.10.17
[Spring 기본] Container와 Bean과 조금 더 친해져보자  (0) 2022.10.13
[Spring 기본] Spring의 객체 지향 IoC, DI, Bean, Container  (0) 2022.10.13
[Spring 기본] 앱 기본 설계, Config 클래스 및 주입의 시작  (0) 2022.10.13
'Spring/Spring 기본' 카테고리의 다른 글
  • [Spring 기본] 의존 관계 주입 전략 (DI Strategy)
  • [Spring 기본] Component Scan을 통한 Bean 자동화 관리
  • [Spring 기본] Container와 Bean과 조금 더 친해져보자
  • [Spring 기본] Spring의 객체 지향 IoC, DI, Bean, Container
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    analyzer
    mockito
    디자인 패턴
    spring container
    Spring
    k8s
    BEAN
    Design Pattern
    decorator
    단위테스트
    Java
    lazy loading
    junit
    n+1
    GoF
    김영한
    runtime exception
    객체지향
    Configuration
    OOAD
    SRP
    elasticsearch
    composition
    JPA
    Setter
    OOP
    lombok
    spring boot
    Composite
    di
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[Spring 기본] Spring Container의 Singleton 전략
상단으로

티스토리툴바