[Spring-기본] 싱글톤 컨테이너

DANI·2023년 11월 24일

Spring[김영한T]

목록 보기
23/31
post-thumbnail

보통 웹 애플리케이션은 여러 고객이 동시에 요청한다. 각 요청마다 새로운 객체를 생성한다면 메모리 낭비가 심해지기 때문에 싱글톤 객체를 생성하고 공유하도록 설계한다.


💻 순수한 DI 컨테이너

💾 SingletonTest

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class SingletonTest {

  @Test
  @DisplayName("스프링 없는 순수한 DI 컨테이너")
  void pureContainer(){
      AppConfig appConfig = new AppConfig();


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

      // 참조 값이 다르다
      System.out.println(memberService1);
      System.out.println(memberService2);

      Assertions.assertThat(memberService1).isNotEqualTo(memberService2);
  }
}

🔵 실행결과

스프링 없는 순수 DI 컨테이너의 경우 요청마다 객체를 새로 생성했다.




💾 Singleton 클래스 생성

package Singleton;

public class Singleton {

  // 상수로 싱글톤 객체를 생성한다
  private static final Singleton instance = new Singleton();

  // 싱글톤 객체를 호출
  public static Singleton getInstance(){
      return instance;
  }
  
  // 생성자를 private으로 선언해서 외부에서 new로 생성할 수 없도록 한다.
  // 즉 외부에서는 getInstance()로 호출만 가능하다.
  private Singleton(){
  }

  public void logic(){
      System.out.println("싱글톤 객체 호출");
  }
}


💾 SingletonTest 테스트 파일

  @Test
  @DisplayName("싱글톤 패턴을 적용한 객체 사용")
  void singletontest(){
      Singleton singleton1 = Singleton.getInstance();
      Singleton singleton2 = Singleton.getInstance();

      System.out.println("singleton1 : " + singleton1);
      System.out.println("singleton2 : " + singleton2);

      // isSameAs -> " == "
      // isEqualsTo -> " equals "
      Assertions.assertThat(singleton1).isSameAs(singleton2);
  }

🔵 실행결과

같은 객체임이 확인되었다.



🚫 싱글톤 패턴의 문제점


  • 의존관계상 클라이언트가 구체 클래스에 의존한다(DIP 위반, OCP 원칙 위반 가능성 有)

  • 내부 속성을 변경하거나 초기화 하기 어렵다.

  • private 생성자로 유연성이 떨어진다.






📕 싱글톤 컨테이너

객체 인스턴스를 1개만 생성하여 관리하는 것. 즉, 스프링 빈은 싱글톤으로 관리되는 빈이다.


📝 스프링 빈

스프링 컨테이너는 객체를 하나만 생성해서 관리한다.

스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라한다.



💾 SingletonTest 테스트 파일

  @Test
  @DisplayName("스프링 빈 싱글톤인지 확인하기")
  void springBeanTest(){
      AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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

      System.out.println("memberService1 : " + memberService1);
      System.out.println("memberService2 : " + memberService2);

      Assertions.assertThat(memberService1).isSameAs(memberService2);
  }

🔵 실행결과



💡 스프링빈은 싱글톤으로 관리된다.




🚫 싱글톤 패턴의 주의점


  • 싱글톤 객체는 상태를 유지하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다.

  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.

  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용해야 한다.



💾 SingtonService 클래스 생성

package hello.core.singleton;

public class SingtonService {

    private int price;

    public void buy(String name, int price){
        System.out.println("name = " + name + " price = " + price);
        this.price = price;
    }

    public int getPrice(){
        return price;
    }

}


💾 SingtonServiceTest 테스트 파일

package hello.core.singleton;


import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

class SingtonServiceTest {

    @Test
    @DisplayName("싱글톤 패턴 문제점")
    void singletonService(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(configex.class);

        SingtonService singleton1 = ac.getBean("singtonServie", SingtonService.class);
        SingtonService singleton2 = ac.getBean("singtonServie", SingtonService.class);

        singleton1.buy("User1", 10000);
        singleton2.buy("User2", 20000);

        System.out.println("User1의 기대값(10000) : "+singleton1.getPrice());
        System.out.println("User2의 기대값(20000) : "+singleton2.getPrice());

        Assertions.assertEquals(singleton1.getPrice(), singleton2.getPrice());


    }


    static class configex{
        @Bean
        public SingtonService singtonServie(){
            return new SingtonService();
        }
    }


}

🔵 실행결과

클라이언트가 값을 변경할 수 있도록 코드를 짰기 때문에 값이 변경된 상태로 유지가 된다.

✨ 절대로 필드에 공유값을 넣어서는 안된다✨




💡 @Configuration과 싱글톤

다음 설정 파일을 보자

💾 AppConfig 파일

package hello.core;


import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.Order.OrderService;
import hello.core.Order.OrderServiceImpl;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 애너테이션 추가
@Configuration
public class AppConfig {

  @Bean
  public OrderService orderService(){

      return new OrderServiceImpl(memberRepository(), discountPolicy());
  }

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

  @Bean
  public DiscountPolicy discountPolicy(){

      return new RateDiscountPolicy();
  }

  @Bean
  public MemberService memberService(){

      return new MemberServiceImpl(memberRepository());
  }
}


🔍 만약 memberRepository() 와 memberService()를 호출했다고 가정해보자

위 그림처럼 memberRepository()가 호출되었을 때 new MemoryMemberRepository() 가 생성된다. 그리고 memberService()가 호출 되었을 때 new MemberServiceImpl가 생성되고, memberRepository()를 한 번 더 부르면서 new MemoryMemberRepository()가 한 번더 생성되는 것이 아닌가?


테스트를 통하여 확인해보자!

각 구현체에 리턴 메소드를 추가한다.

💾 OrderServiceImpl

package hello.core.Order;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixedDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

  ...(생략)...

  public MemberRepository getMemberRepository(){
      return memberRepository;
  }
}

💾 MemberServiceImpl

package hello.core.member;

public class MemberServiceImpl implements MemberService{

 ...(생략)...

  public MemberRepository getMemberRepository(){
      return memberRepository;
  }
}

💾 ConfigurationTest ("AppConfig 싱글톤 테스트")

package hello.core;

import hello.core.Order.OrderService;
import hello.core.Order.OrderServiceImpl;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;

public class ConfigurationTest {
  
  @Test
  @DisplayName("AppConfig 싱글톤 테스트")
  void ConfigurationTest(){
      ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
      OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
      MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
      MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);

      System.out.println(orderService.getMemberRepository());
      System.out.println(memberService.getMemberRepository());
      System.out.println(memberRepository);

      Assertions.assertEquals(orderService.getMemberRepository(), memberRepository);
      Assertions.assertEquals(orderService.getMemberRepository(), memberService.getMemberRepository());
      Assertions.assertEquals(memberRepository, memberService.getMemberRepository());
  }
}

🔵 실행결과

세 개가 모두 같은 인스턴스이다!




호출로그를 남겨 확인해보자!

💾 AppConfig 파일

package hello.core;


import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.Order.OrderService;
import hello.core.Order.OrderServiceImpl;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 애너테이션 추가
@Configuration
public class AppConfig {

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

  @Bean
  public MemberRepository memberRepository(){

      System.out.println("call Appconfig.memberRepository");
      return new MemoryMemberRepository();
  }

  @Bean
  public DiscountPolicy discountPolicy(){
      System.out.println("call Appconfig.discountPolicy");
      return new RateDiscountPolicy();
  }

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




💾 ConfigurationTest("AppConfig 싱글톤 테스트")

🔵 실행결과

위의 테스트 파일을 그대로 돌려본 결과 인스턴스가 1개씩만 생성되는 것이 확인된다.




❓ 스프링 컨테이너는 싱글톤 레지스트리다. 그런데 어떻게 싱글톤이 되도록 보장해주는 것일까?

👉 @Configuration 애너테이션에 있다.



💾 ConfigurationTest ("AppCofig 파일의 빈을 조회해보자")

@Test
@DisplayName("AppCofig 파일의 빈을 조회해보자")
void AppConfigBean(){
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
}

🔵 실행결과

순수한 자바 클래스 파일이 아닌 $$SpringCGLIB$$0 가 나오는데 이것은 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 다른 클래스를 만들고 그것을 스프링빈으로 등록한 것이다.



AppConfig@CGLIB의 내부 코드는 알 수 없지만, 스프링 컨테이너에 등록이 되어있으면 찾아서 반환하고 그렇지 않으면 새로 생성해서 컨테이너에 등록하는 로직일 것이다.





❓ 만약 @Configuration을 적용하지 않고 @Bean만 적용한다면?

💻 애너테이션 삭제 후 위 테스트 실행하기(ConfigurationTest ("AppCofig 파일의 빈을 조회해보자"))

// 애너테이션 삭제
// @Configuration
public class AppConfig {

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

🔵 실행결과

자바클래스로 AppConfig 파일이 등록되었으며, 싱글톤을 보장해주지 못한다.



💾 ConfigurationTest ("AppConfig 싱글톤 테스트")




✨ @Bean만 사용하면 싱글톤을 보장해주지 못한다. 반드시 @Configuration을 붙여주도록 하자 ✨

0개의 댓글