본문 바로가기
Today I Learned/Spring

[Spring] 싱글톤을 관리하는 스프링 컨테이너

by 프로그래 밍구 2023. 11. 2.

싱글톤 컨테이너 역할을 하는 스프링 컨테이너

 스프링 컨테이너는 기존 싱글톤 패턴의 문제점을 해결하고 객체 인스턴스를 한 개만 생성하여 관리한다. 잘 알고 있는 스프링 빈이 스프링 컨테이너에 의해 싱글톤으로 관리된다. 스프링 컨테이너 싱글톤 객체를 관리하므로 여러 장점이 있는데, 먼저 싱글톤 패턴을 위해서 코드를 작성하지 않아도 된다는 점이다. 그리고 앞서 싱글톤 패턴의 문제점으로 말했던 DIP, OCP, 테스트, private 생성자로부터 제한없이 싱글톤을 사용할 수 있다.

 

@Test
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 = hello.core.member.MemberServiceImpl@35e5d0e5
// memberService2 = hello.core.member.MemberServiceImpl@35e5d0e5

 

 AnnotationConfigApplicationContext를 통해 스프링 컨테이너를 생성하고 getBean(  ) 메서드를 통해서 빈을 호출하는 방식으로 memberService 2개를 생성하였다. memberService1과 memberService2가 같은 객체이므로 테스트를 통과하였고, 출력한 결과로도 같은 객체임을 알 수 있다. 

 

 이처럼 스프링 컨테이너를 통해 빈 객체를 생성하면 손쉽게 싱글톤 패턴을 적용하고 관리할 수 있다. 웹 어플리케이션에서는 고객의 요청이 올 때마다 이미 생성된 객체 인스턴스를 활용할 수 있어 더 효율적이다. 참고로 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.


싱글톤 패턴 적용 시 주의점

 객체 인스턴스를 하나만 생성하여 공유하는 싱글톤 방식은 주의할 점이 있는데 다음과 같다. 

 

  • 객체가 stateful(상태유지)하도록 설계하면 안되고, stateless(무상태)로 설계해야 한다.
  • 특정 클라이언트에 의존적인 필드가 존재하면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 존재하면 안된다.
  • 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 가급적 읽기만 가능해야 한다.

다음은 stateful하게 설계되어 문제를 일으키는 예시 코드이다.

 

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.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        statefulService1.order("userA", 10000);
        statefulService2.order("userB", 20000);

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

// 테스트 실행 결과
// name = userA price = 10000
// name = userB price = 20000
// price = 20000

 

 A는 10,000원을 주문하였고, A의 주문 금액을 출력하기 전에 B가 20,000원을 주문한 상황이다. A의 주문을 출력하는 것이므로 원하는 출력 결과는 10,000원일 것이다. 하지만 출력 결과는 20000이 나왔고 그로 인해 테스트가 통과되었다. 즉, StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경함을 알 수 있다. 실제 서비스에서 이런 일이 생긴다면 금액적으로도 큰 문제가 될 수 있다. 다음은 수정된 코드이다.

 

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() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

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

        int userAPrice = statefulService1.order("userA", 10000);
        int userBPrice = statefulService2.order("userB", 20000);

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

    }

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

// 실행 결과
// name = userA price = 10000
// name = userB price = 20000
// price = 10000

 

 A의 주문을 조회한 결과, 10000으로 올바르게 출력되었다. B의 주문 요청이 중간에 들어와도 A값은 변경되지 않음을 알 수 있다. 만약 실무에서 stateful 코드처럼 값이 중간에 변경되는 실수가 나온다면 큰 어려움을 겪을 것이다. 따라서 싱글톤 패턴을 적용할 때는 항상 공유필드를 주의하고 스프링 빈은 항상 stateless(무상태)로 설계해야 한다. 

댓글