Spring

[Spring] Spring 핵심 원리 : 웹 애플리케이션과 싱글톤 패턴

yjk490 2023. 7. 6. 14:45

웹 애플리케이션이 받는 요청

웹 애플리케이션은 보통 다수의 클라이언트가 동시에 요청을 한다. 이전에 개발했던 순수한 DI 컨테이너인 AppConfig를 예를 들면, 주문 요청이 들어올 때마다 서버는 orderService 객체를 생성한다. 그리고 orderService가 의존하는 객체는 orderRepository, memberRepository, discountPolicy이다. 만약 초당 100건의 요청이 들어오면 초당 400개의 객체가 생성, 소멸된다. 이는 메모리 관점에서 비효율적이다.

이 문제를 해결하기 위해서는 요청을 처리하는 객체는 단 1개만 생성되고, 요청이 들어올 때마다 공유하도록 설계하면 된다. 이러한 설계 방법이 '싱글톤 패턴'이다. 스프링 프레임워크는 주로 웹 애플리케이션을 개발하는 데 사용된다. 그래서 스프링에서 비즈니스 로직을 처리하는 클래스는 싱글톤 패턴을 따르도록 지원한다.

싱글톤 패턴이란?

클래스의 인스턴스가 단 한 개만 생성되는 것을 보장하는 디자인 패턴이다. 생성자가 여러 차례 호출되더라도 실제로 생성되는 인스턴스는 한 개이고, 최초 생성 이후 호출된 생성자는 최초의 생성자가 생성한 인스턴스를 리턴한다.

Java에서는 아래와 같이 생성자를 private으로 선언하여 다른 곳에서 생성자를 호출하지 못하도록 막고, 인스턴스는 getInstance() 메서드를 통해 사용한다.

public class SingletonService {
    private static final SingletonService instance = new SingletonService();

    private SingletonService() {}
    
    public static SingletonService getInstance() {
        return instance;
    }

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

 

그러나 싱글톤 패턴이 장점만 있는 것은 아니다. 싱글톤 패턴의 단점은 우선 싱글톤 패턴을 구현하는 코드가 많이 들어간다는 점이다. 또한 위 예시 코드에서 볼 수 있듯 클라이언트 코드가 싱글톤 객체의 getInstance()를 직접 호출하는 경우, 클라이언트는 구체 클래스를 의존하게 되는 격이다. 이는 DIP 위반이고, 나아가 OCP를 위반할 가능성이 높다. 결과적으로 싱글톤 패턴은 애플리케이션의 유연성을 떨어뜨리고 객체지향 설계에서 멀어지게끔 한다.

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서 인스턴스를 싱글톤으로 관리한다. 비즈니스 로직을 처리하는 클래스를 싱글톤 패턴으로 개발하지 않아도 스프링 빈으로 등록될 때 싱글톤 패턴이 적용된다. 이전에 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다. 스프링 컨테이너는 싱글톤 컨테이너 역할을 하고 이처럼 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.

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);
    }
    /* 실행 결과
    memberService1 = study.conversiontospring.member.MemberServiceImpl@305b7c14
    memberService2 = study.conversiontospring.member.MemberServiceImpl@6913c1fb
    */

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤 객체")
    void springContainer() {
        ApplicationContext 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);

        assertThat(memberService1).isSameAs(memberService2);
    }
    /* 실행 결과
    memberService1 = study.conversiontospring.member.MemberServiceImpl@4febb875
    memberService2 = study.conversiontospring.member.MemberServiceImpl@4febb875
    */
}

위 테스트 결과와 같이 스프링을 사용하지 않았을 때에는 생성된 인스턴스의 주소값이 다르지만 스프링 컨테이너를 통해 호출한 인스턴스는 주소값이 같다.

주의점

싱글톤 패턴이나 스프링 같은 싱글톤 컨테이너는 객체가 상태를 유지하도록(stateful) 설계하면 안 된다. 여러 클라이언트가 한 개의 인스턴스를 공유하기 때문이다. 무상태(stateless)로 설계해야 한다. 즉, 특정 클라이언트에 의존하는 필드나 값을 변경할 수 있는 필드가 있어서는 안 된다는 의미이다.

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() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

        // ThreadA : A사용자가 10000원 주문
        statefulService1.order("A", 10000);
        // ThreadB : B사용자가 20000원 주문
        statefulService2.order("B", 20000);

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

        Assertions.assertThat(price).isEqualTo(20000);
    }

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

위와 같이 StatefulService는 price라는 상태를 유지하는 필드가 있다. order() 메서드를 호출할 때마다 매개변수로 전달된 price를 StatefulService의 price 필드에 저장한다. 그렇다면 A사용자가 10000원을 주문하고 B사용자가 20000원을 주문한 후, A용자가 주문금액을 조회한다면 B사용자의 주문금액이 조회될 것이다. 이러한 이유로 싱글톤 객체는 무상태로 설계되어야 한다.