본문 바로가기
Back-End/Spring

5.싱글톤 컨테이너

by 두두리안 2021. 1. 5.
728x90

5.싱글톤 컨테이너

  • 목차
    • 1.웹 애플리케이션 싱글톤
    • 2.싱글톤 패턴
    • 3.싱글톤 컨테이너
    • 4.싱글톤 방식의 주의점
    • 5.@Configuration 과 싱글톤
    • 6.@Configuration 과 바이트코드 조작의 마법

1.웹 애플리케이션 싱클톤

  • 웹 애플리케이션은 보통 고객이 동시에 요청한다

SingletonTest

public class SingletonTest {

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

        //1.조회
        MemberService memberService1 = appConfig.memberService();

        //2.조회
        MemberService memberService2 = appConfig.memberService();

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

        assertThat(memberService1).isNotSameAs(memberService2);


    }

}
  • 스프링 없는 순수한 DI 컨테이너는 요청할때마다 객체를 생성
  • 메모리 낭비가 심하다
  • 해당객체를 1개만 생성되고, 공유하도록 설계 한다 - 싱글톤 패턴

2.싱글톤 패턴

  • 클래스의 인스턴스가 1개만 생성되는것을 보장한다
  • private 생성자를 사용해서 외부에서 임의 new 키워드를 못사용하도록 사용

SingletonService

public class SingletonService {

    // 클래스안에 static 가 있으면 하나만 만들어짐
    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }

    // private 생성자를 만들어서 new 사용을 방지
    private SingletonService(){

    }

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

}
  • 객체가 필요하면 getInstance 에서 조회할수 있다
  • private 생성자를 만들어서 외부에서 new 키워드로 객체 인스턴스 생성을 막는다

SingletonService

public class SingletonTest {

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체")
    void singletonServiceTest() {

        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        assertThat(singletonService1).isSameAs(singletonService2);

    }
}
  • isSameAs 을 이용해서 같은 객체를 쓰는지 알수 있다
  • 호출할때 마다 같은 객체를 반환한다

싱글톤 패턴의 문제점

  • 코드자체가 많이 들어간다
  • 구체클래스에 의존한다 - DIP 위반
  • 구체클래스에 의존한다 - OCP 위반
  • 테스트가 어렵다
  • 내부속성 변경하거나 초기화하기 어렵다
  • private 생성자 때문에 자식클래스 만들기 어렵다
  • 유연성이 많이 떨어진다

3.싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤의 문제점을 해결하면서 인스턴스를 싱글톤으로 관리한다
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다
  • 지저분한 코드가 안들어간다 , DIP OCP 테스트 private 생성자 등 자유롭게 사용

SingletonTest

public class SingletonTest {

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);


        //1.조회
        MemberService memberService1 = ac.getBean("memberService",MemberService.class);

        //2.조회
        MemberService memberService2 = ac.getBean("memberService",MemberService.class);


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

        assertThat(memberService1).isSameAs(memberService2);
    }
}
  • 같은 객체를 반환한다

4.싱글톤 방식의 주의점

  • 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤방식은 상태를 유지하게 설계하면 안된다(무상태)
  • 특정 클라이언트에 의존적인 필드가 있으면 안된다
  • 값을 변경할수 있는 필드가 있으면 안된다
  • 가급적 읽기만 사용
  • 필드대신에 자바에서 공유되지않는 지역변수,파라미터,스레드로컬에 사용

StatefulService

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;
    }

}
  • 테스트 용도로 StatefulService 를 만든다
  • 간단한 이름과 가격

StatefulServiceTest

class StatefulServiceTest {

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

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        ///스레드 A
        statefulService1.order("userA",10000);

        //스레드 B
        statefulService2.order("userB",20000);

        //사용자 A 금액
        int price =statefulService1.getPrice();

        System.out.println("price = " + price);

    }

    static class TestConfig{

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

    }

}
  • 같은객체를 사용하는 중에서 this.price=price 변경되는 값이 있을때
  • statefulService1 의 가격은 20000원이 찍힌다
  • statefulService 필드는 공유되는 필드이다
  • 공유필드는 조심해야된다. 스프링 빈은 항상 무상태로 설계하자

StatefulService 변경

public class StatefulService {

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

}

StatefulServiceTest 변경

class StatefulServiceTest {

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

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        ///스레드 A
        int userA = statefulService1.order("userA", 10000);

        //스레드 B
        int userB = statefulService2.order("userB", 20000);

        //사용자 A 금액


        System.out.println("price = " + userA);

    }

    static class TestConfig{

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

    }

}
  • 지역변수로 문제를 해결하자
  • 항상 무상태로 설계하자

5.@Configuration 과 싱글톤

ConfigurationSingletonTest

public class ConfigurationSingletonTest {

    @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();

        System.out.println("memberRepository1 = " + memberRepository1);
        System.out.println("memberRepository2 = " + memberRepository2);

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository1);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository2);

    }

}
  • AppConfig 에 @Configuration 을 통해서 같은 객체를 공유한다

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

  • 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다

ConfigurationSingletonTest

public class ConfigurationSingletonTest {

    @Test
    void configurationDeep(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean);

    }

}

image

  • AppConfig 빈을 출력하면 스프링이 CGLB 라는 바이트코드 조작 라이브러리 사용
  • AppConfig 클래스를 상속받은 임의의 다른클래스 생성
  • 임의의 다른클래스를 스프링빈으로 등록한다
  • @Configuration 이 없어도 동작은되나 CGLB 적용이 안된다 - 싱글톤 x

정리

  • @Bean 만 사용하면 싱글톤은 보장을 안한다
  • 스프링 설정정보는 항상 @Configuration 을 쓰자
728x90