싱글톤 패턴

  • 게임 개발했던 것처럼, 하나의 객체를 전역적으로 생성하고 공유해서 사용하는 것.
  • 구체 클래스.getInstance() 해서 가져와야 하므로, OCP와 DIP를 위반함.
  • 유연성이 떨어져서 안티패턴으로 불림. 게임 개발땐 많이 썼는데, 백엔드 개발은 좀 다른 것 같다.
public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    // 외부 생성 막음
    private SingletonService() {}

    public static SingletonService getInstance() {
        return instance;
    }
    
    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}



// ---------------------------------------------------------------------



public class SingletonTest {

    // 호출할 때 마다 객체를 생성하는 문제 발생 -> 메모리 비효율성
    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        MemberService memberService1 = appConfig.memberService();

        MemberService memberService2 = appConfig.memberService();

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

        assertThat(memberService1).isNotSameAs(memberService2);
    }

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        assertThat(singletonService1).isSameAs(singletonService2);
    }
}

 

 

 

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리함.
  • 덕분에, 싱글톤 패턴의 문제점을 해결하면서 장점을 이용할 수 있게 됨.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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

    assertThat(memberService1).isSameAs(memberService2);
}

 

싱글톤 컨테이너

 

 

 

싱글톤 방식의 문제점

  • 값은 외부에서 변경되어선 안되며, 가급적 읽기만 가능해야 한다. 즉, 동시성 문제가 발생하지 않도록 해야 함.
  • 즉, 클라이언트에 의존적인 필드와 값을 변경할 수 있는 필드가 있으면 안된다. -> 스프링 빈은 무상태(stateless)로 설계.
  • 이와 관련해서 실무에서 해결하기 어려운 큰 문제들이 실제로 발생함.
package hello.core.singleton;

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



class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        AnnotationConfigApplicationContext 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 주문 금액 조회 -> 만원이 아니라 2만원으로 의도치 않게 나옴
        // 동시성 문제
        int price = statefulService1.getPrice();

        assertThat(price).isEqualTo(20000);
    }

    static class TestConfig {

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



// ---------------------------해결 후------------------------------



public class StatefulService {

    // 상태를 유지하지 않도록 변경
    // private int price;

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

        return price;
    }
}



class StatefulServiceTest {

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

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

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

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

        // ThreadA: 사용자A 주문 금액 조회 -> 만원이 아니라 2만원으로 의도치 않게 나옴
        // int price = statefulService1.getPrice();

        assertThat(userAPrice).isNotSameAs(userBPrice);
    }

    static class TestConfig {

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

 

 

 

@Configuration

  • 해당 어노테이션을 통해, 하나의 스프링 빈을 생성하고 이후에 새로 생성하지 않고 동일 객체 참조 값을 반환하게 함.
  • 즉, 생성자는 한 번만 호출되는 것임. 또한 AppConfig도 스프링 빈이 됨.
  • 이 어노테이션을 사용하지 않으면, 동일한 객체 참조를 반환하지 않고 새로 객체를 생성해서 반환 -> 싱글톤 깨짐
@Configuration
public class AppConfig {

	// call AppConfig.memberService
    // call AppConfig.memberRepository
    // call AppConfig.memberRepository
    // call AppConfig.orderService
    // call AppConfig.memberRepository
    
    // 위 처럼 되지 않고 세 번만 호출 됨.
    
    // call AppConfig.memberService
    // call AppConfig.memberRepository
    // call AppConfig.orderService 

    @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();
        return new RateDiscountPolicy();
    }
}



// ---------------------------------------------------------------------------



public class ConfigurationSingeltonTest {

    @Test
    void configuration() {
        AnnotationConfigApplicationContext 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 = orderService.getMemberRepository();

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