Skip to content

Latest commit

 

History

History
204 lines (152 loc) · 8.24 KB

싱글톤 컨테이너.md

File metadata and controls

204 lines (152 loc) · 8.24 KB

웹 애플리케이션과 싱글톤

  • 스프링은 기업용 온라인 서비스 기술을 지원하기 위해 탄생
  • 대부분의 스프링 애플리케이션은 웹
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청한다

사용자가 요청할 때마다 필요한 객체를 생성한다면

  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다 → 메모리 낭비가 심하다
  • 해당 객체를 1개만 생성하고 공유하도록 설계하면 해결된다 - > 싱글톤 패턴


싱글톤 패턴

  • 클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴
  • 객체 인스턴스가 2개 이상 생성되지 않도록 막아야함
    • private 생성자를 사용하여 외부에서 new 키워드를 사용 못하게 막음
    • static 영역에 객체 instance를 미리 하나 생성
    • 이 객체 인스턴스에 접근하기 위해서 오로지 getInstance() 메서드를 통해서만 조회할 수 있도록 설계
public class SingletonService{
	private static final SingletonService instance = new SingletonService();
	

	public static SingletonService getInstance(){
		return instance;
	}

	private SingletonService(){};
}

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP 위반
  • 클라이언트가 구체 클래스에 의존해서 OCP원칙을 위반한다.
  • 테스트하기 어렵다
  • 내부 속성을 변경하거나 초기화 하기 어렵다
  • prvate 생성자로 자식 클래스를 만들기 어렵다


싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리

  • 스프링 빈이 싱글톤으로 관리된다.

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

  • 스프링 컨테이너의 이러한 기능 덕분에 개발자는 싱글톤을 구현하지 않아도, DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

  • 스프링의 기본 빈 등록 방식은 싱글톤이지만, 다른 방법도 지원한다(요청할 때 마다 새로운 객체 생성해서 반환하는 것도 지원)


싱글톤 방식의 주의점

  • 싱글톤 객체는 상태를 유지하게 설계하면 안된다
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용한다.
public class StatefulService {
    private int price;

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

    public int getPrice(){
        return price;
    }
}
public class StatefulServiceTest {

    @Test
    void statefulServiceSingleton(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);

        //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig{

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


@Configuration과 싱글톤

@Configuration
public class AppConfig {

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

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

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

    @Bean
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}
  • 위의 코드에서 memberService()와 memberRepository(), orderService()를 등록하는 과정에서 new MemoryMemberRepository()를 총 세 번 실행할 것 같은데 이는 싱글톤이 깨지는 것 아닐까?
@Test
    void configurationTest(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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

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

        System.out.println("memberService -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
  • 결과는 셋 다 같은 인스턴스를 반환한다.


@Configuration과 바이트코드 조작의 마법

  • 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해줘야 한다.
  • 위의 자바코드를 보면 getMemberRepository()가 3번 호출되어야 한다.
  • AppConfig에 적용시킨 @Configuration 어노테이션이 무언가를 했다.
		@Test
    void configurationDeep(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
				//출력 : bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$13481f13
    }
  • AppConfig 빈을 출력한 결과 순수한 클래스가 아닌 다른 클래스가 출력되었다.
  • 이렇게 어노테이션을 적용한 클래스를 CGLIB라는 바이트코드 조작 라이브러리를 활용하여 싱글톤을 보장해준다.
  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 존재하지 않으면 생성해서 등록한다.
  • 만약 AppConfig에 @Configuration이 아닌 그냥 @Bean을 붙여 사용한다면?
    • memberRepository가 3번 호출되어 싱글톤을 보장하지 못한다.