[Spring] 5-2 싱글톤 컨테이너

송광호·2023년 12월 27일

[Spring]

목록 보기
22/41
post-thumbnail

Spring 시리즈는 혼자 공부하며 기록으로 남기고, 만약 잘못 학습 한 지식이 있다면 공유하며 피드백을 받고자 작성합니다.
스프링에 대해 깊게 공부해보고자 인프런의 김영한 강사님께서 강의를 진행하시는 (스프링 핵심 원리 - 기본편) 강의를 수강하며 정리하는 글입니다.
혹여나 글을 읽으시며 잘못 설명된 부분이 있다면 지적 부탁드리겠습니다.


싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서 객체 인스턴스를 싱글톤으로 관리한다.
지금까지 계속 학습한 스프링 빈이 싱글톤 방식으로 동작한다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 따로 적용시키지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다.
  • 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
  • 싱글톤 패턴의 단점 해결
    • 지저분한 코드가 들어가지 않아도 된다.
    • DIP와 OCP 도 만족하며, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

테스트 코드

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

        //memberService != memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }
}

getBean()메서드를 사용하여 컨테이너에 있는 객체 인스턴스를 꺼내온다.

결과

  • 참조값이 동일하게 출력되는걸 확인해볼 수 있다.

싱글톤 컨테이너 적용 후

  • 클라이언트로부터 요청이 오면 객체를 새로 생성하는 것이 아니라 이미 스프링 컨테이너에 등록되어있는 객체를 공유하여 효율적으로 재사용 할 수 있다.

    참고 : 기본 빈 등록은 싱글톤 방식이지만, 요청할때마다 새로운 객체를 생성해서 반환하는 방법도 존재한다.

싱글톤 방식의 주의점

  • 싱글톤 패턴이나, 싱글톤 컨테이너나, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(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;
    }
}
  • 상태를 유지하는 price 필드가 존재한다.
  • 해당 코드만 봐서는 왜인지 의문을 가질 수 있다. 문제가 발생하는 테스트 코드를 작성해보자.

테스트 코드

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();
        }
    }
}
  • 스프링 컨테이너에 등록된 StatefulService.class를 각각 statefulService1, statefulService2에 할당한다.
  • 스프링 컨테이너는 싱글톤 방식으로 등록된다고 설명했다. 두 인스턴스가 참조하고있는 것은 똑같은 객체 인스턴스이다.
  • 그럼 A와 B사용자가 각각 주문을 하고 A사용자의 주문 금액을 조회한다면?
    • price 라는 공유 필드가 존재하기때문에 B사용자가 주문했을때 금액이 덮어씌워진다.
  • 실무에서 이런 오류가 나오면 해결하기 정말 힘들다.
  • 공유필드는 반드시 조심해야한다. 스프링 빈은 항상 무상태(stateless)로 설계하여야한다.
    • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
      • 멀티쓰레드는 나중에 한번 공부해봐야겠다.

0개의 댓글