Study/Spring

[Spring] 싱글톤 컨테이너

dev_kong 2023. 1. 31. 00:04
728x90
728x90

김영한님의 스프링 핵심 원리 - 기본편을 바탕으로 작성하였습니다.

[Spring] 스프링 컨테이너와 이어지는 포스팅입니다.
전체 소스코드 보기

웹애플리케이션과 싱글톤

웹 애플리케이션은 보통 여러고객이 동시에 요청을 한다.

 

만약 3명의 고객이 회원가입을 한다면,
각각의 요청마다 새로운 memberService 가 생겨날 것이다.

 

테스트 코드를 통해 확인해보자.

public class SingletonTest {

    @Test
    @DisplayName("여러개의 멤버 서비스 생성 확인")
    void memberServiceTest() {
        AppConfig appConfig = new AppConfig();

        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();

        assertThat(memberService1).isNotSameAs(memberService2);
    }
}

테스틀 돌려보면 통과하는 것을 확인해볼 수 있다.

 

즉, 이전에 작성한 스프링 없은 순수 DI 컨테이너는 요청을 할때마다 새로운 객체를 생성한다.
만약 초당 트래픽이 100이라면, 초당 100개의 객체가 생성되고 소멸된다.

 

이를 해결 하기 위해 해당 객체가 한번만 생성되고, 이후의 호출부터는 이전의 생성된 객체를 공통으로 사용하면된다.
이를 위해, 사용되는 것이 싱글톤 패턴이다.

싱글톤 패턴

싱글톤 패턴에 대해 포스팅한 내용이 있으니,
참고하면 좋을듯.

https://kong-dev.tistory.com/201

 

[CS] 디자인 패턴 - 싱글톤

디자인 패턴이란? 프로그램을 설계할 때 발생했던 문제점들을 해결 할 수 있도록 하나의 규약형태로 만들어 놓은 것 싱글톤 패턴(Singletone pattern) 싱글톤 패턴이란? 하나의 클래스에 오직 하나의

kong-dev.tistory.com

 

싱글톤패턴을 만들어보고 이게 잘 동작하는지 테스트코드를 통해 확인해보자.

public class SingletonService {
    private static SingletonService INSTANCE = new SingletonService();

    private SingletonService() {
    }

    public static SingletonService getInstance() {
        return INSTANCE;
    }
}


// 테스트코드
@Test
@DisplayName("싱글톤 테스트")
void singletonTest() {
  SingletonService instance1 = SingletonService.getInstance();
  SingletonService instance2 = SingletonService.getInstance();

  assertThat(instance1).isSameAs(instance2);
}

테스트가 통과 했으니 싱글톤을 통해 하나의 인스턴스를 공유한다는 것을 확인했다.

 

그럼 이제 AppConfig에 있던 내용을 전부다 싱글톤 패턴으로 변경을 해서 확인을 해보면 된다..
매우 귀찮은데... 사실 할 필요 없음.

 

스프링 컨테이너는 기본적으로 모든 빈을 싱글톤으로 만들어서 관리해준다.(갓프링...)

싱글톤을 생성하는 방법

싱글톤 관련 포스팅의 예제코드와 현재 작성한 코드가 다르다.
싱글톤을 생성하는 방법은 여러가지가 있다.

현재 사용한 방법은 사용을 하든 말든 일단 생성해놓고, 필요하면 가져다 쓰는 방식이고

이전의 포스팅에서 사용한 방식은
호출할때 까지 생성 안하고 있다가, 호출하면 생성하고, 생성한 이후에는 참조해서 사용하는 방식이다.

싱글톤 컨테이너

사실 싱글톤 패턴에는 여러가지 문제점들이 있다.

이 때문에, 안티패턴으로 여겨지는 경우도 더러 있다.

 

그런데 스프링 컨테이너는 기본적으로 모든 빈을 싱글톤으로 만들어서 관리한다고 했는데..(싱글톤 레지스트리)
이거 괜찮은걸까..?

 

결론은 괜찮다.


스프링은 싱글턴 패턴의 모든 단점을 해결하며 객체를 싱글톤으로 유지한다.

테스트코드를 통해 스프링이 정말 빈들을 싱글톤으로 만들어주는지 확인해보자.

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

  MemberService memberService1 = ac.getBean("memberService", MemberService.class);
  MemberService memberService2 = ac.getBean("memberService", MemberService.class);

  assertThat(memberService1).isSameAs(memberService2);
}

테스트가 통과하는 것을 통해 스프링이 빈들을 싱글톤으로 관리한다는 것을 확인할 수 있다.

주의사항

싱글톤의 특성상 stateful 하게 설계를 하면 치명적인 오류를 야기할 수 있다.

 

항상 스프링빈의 설계는 stateless(무상태성)하게 설계해야한다.

이렇게 말하면 뭔소린지 까먹을거 같으니 예제를 작성해보자.

public class StatefulService {
    private String name;

    public void join(String name) {
        System.out.println("name = " + name);
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

static class TestConfig {
  @Bean
  public StatefulService statefulService() {
    return new StatefulService();
  }
}

이런식으로 필드를 가지는 빈을 생성한다고 해보자.
위의 코드로 아래와 같이 테스트를 진행해보면,

public class StatefulTest {
    @Test
    void statefulServiceTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

        statefulService1.join("kong");
        statefulService2.join("pea");

        String name = statefulService1.getName(); // 기대값 : kong, 실제결과: pea
        Assertions.assertThat(name).isEqualTo("kong");
    }
}

당연히 테스트를 통과하지 못한다.

 

statefulService1에서 name을 조회하기 때문에 당연히 kong 라는 값을 기대하지만,
싱글톤으로 관리되는 스프링빈의 특성상 필드역시 공유되기 때문에
필드의 값이 pea로 변경되어 테스트를 통과하지 못한다.

 

예제를 이해를 위해 단순하게 작성하였지만,


실무에서는 훨씬더 복잡하고 에러를 발견하기도 힘들것이다.

위의 코드를 stateless하게 변경하면 아래와 같다.

public class StatefulService {
//      삭제
//    private String name;

    public String join(String name) {
        System.out.println("name = " + name);
        return name;
    }
//        쓸모없음
//    public String getName() {
//        return name;
//    }
}

테스트코드 이에 맞게 역시 변경해주자.

public class StatefulTest {
    @Test
    void statefulServiceTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

        String userAName = statefulService1.join("kong");
        String userBName = statefulService2.join("pea");

        Assertions.assertThat(userAName).isEqualTo("kong");
    }
}

테스트가 정상적으로 통과하는 것을 확인할 수 있다.

 

스프링 빈의 공유 변수는 항상 조심하게 사용해야 하며,
앵간하면 stateless하게 설계하는 것이 좋다.

@Configuration과 싱글톤

AppConfig 클래스를 살펴보자.

@Configuration
public class AppConfig {

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

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

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

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

memberServiceorderService 두개의 메서드에서 각각 memberRepository를 호출한다.

 

memberRepository는 new 키워드를 사용하여 새로운 인스턴스를 생성하려 반환해주는데,
이렇게 되면 싱글톤으로 관리가 안되는 거 아닐까 싶다.

 

코드를 약간 수정하고 테스트코드를 통해 확인해보자.

public class ConfigSingletonTest {
    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        Assertions.assertThat(memberRepository1).isSameAs(memberRepository);
        Assertions.assertThat(memberRepository2).isSameAs(memberRepository);
    }
}

MemberServiceImplOrderServiceImpl에 각각 memberRepository getter를 만들어준 뒤,

위와같이 테스트 코드를 작성하였다.

 

위의 테스트코드를 돌리면 테스트코드가 통과되는 것을 확인할 수 있는데,
위에서 예상했던 것과는 다른 결과를 보여준다.

 

혹시 호출이 안되는 것은 아닐까 싶으니,
AppConfig 파일에 출력을 통해 로그를 찍어 보자.

@Configuration
public class AppConfig {

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

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("🎉 AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

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

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

discountPolicy는 굳이 출력 안해도 되서 생략했다.

출력순서가 정확하진 않겠지만, 테스트코드의 내용을 토대로 대충 출력내용을 추측해보자면

🎉 AppConfig.memberService
🎉 AppConfig.memberRepository
🎉 AppConfig.orderService
🎉 AppConfig.memberRepository
🎉 AppConfig.memberRepository

위와 같을 것이라 예상한다.

하지만 테스트를 돌려보면,

🎉 AppConfig.memberService
🎉 AppConfig.memberRepository
🎉 AppConfig.orderService

위와 같이 출력되는 것을 확인할 수 있다.

 

memberRepository가 세번 호출 되지 않고 한번 호출된 뒤에는 호출되지 않는다.

 

왜인지는 모르겠지만, 무튼 스프링은 어떤방식을 이용해서 스프링빈의 싱글톤을 보장한다는 것을 알 수 있다.

이러한 역할을 해주는 것이 @Configuration 어노테이션의 역할이다.


그럼 AppConfig 파일에서 Configuration 어노테이션을 삭제한뒤 다시 테스트를 돌려보자.

🎉 AppConfig.memberService
🎉 AppConfig.memberRepository
🎉 AppConfig.orderService
🎉 AppConfig.memberRepository
🎉 AppConfig.memberRepository

위와 같이 처음 예상했던 대로 출력이 되는 것을 확인할 수 있다.

스프링이 Configuration 어노테이션을 통해 어떤 방식을 취하여 스프링 빈의 싱글톤을 보장한다는 사실을 알 수 있다.

바이트조작

그 어떠한 방식이 뭔지 알아보자.

아무리 갓프링이라지만, java 코드 자체를 어찌하지는 못할 텐데,
왜 예상과는 다른 출력결과가 나오는 걸까.

 

스프링이 컨테이너를 생성할 때 자바 바이트코드를 조작하여 우리가 만든 것과는 다른 무언가를 생성한다.

테스트코드를 통해 확인해보자.

@Test
@DisplayName("다른 무언가를 생성한다.")
void configurationDeep() {
  ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

  AppConfig bean = ac.getBean(AppConfig.class);
  System.out.println(bean.getClass()); 
  //class hello.springbasic.AppConfig$$EnhancerBySpringCGLIB$$6126727e
}

다시 Configuration 어노테이션을 살려준 뒤,
위의 테스트코드를 돌려보면, 주석문의 내용이 출력되는 것을 확인 할 수 있다.

 

누구세요..?
원래라면 class hello.springbasic.AppConfig 까지만 출력되어야 하는데,
뒤에 이상한 애들이 더 붙어서 출력된다.

 

이 클래스는 스프링이 CGLIB 라는 라이브러리를 통해 바이트코드를 조작하여 만들어 낸
AppConfig를 상속하는 새로운 클래스이다.


이 새로운 클래스가 스프링빈이 싱글톤이 되도록 보장해준다.

정확히는 모르겠지만, Bean 어노테이션이 붙은 메서드마다 이미 스프링 빈이 존재하면 기존의 것을 반환하고,
존재하지 않는다면, 생성하고 스프링빈으로 등록한 뒤, 반환하는 코드로 이뤄져있을 것이라 예상한다.

 

갓프링..!

728x90
728x90