Home Spring Framework - 싱글턴 컨테이너
Post
Cancel

Spring Framework - 싱글턴 컨테이너

싱글턴 컨테이너

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도 객체인스턴스를 싱글턴으로 관리함
    • 컨테이너는 객체를 하나만 생성해서 관리
  • 스프링 컨테이너는 싱글턴 컨테이너 영할을 함
    • 이러한 싱글턴 객체를 생성하고 관리하는 기능을 싱글턴 레지스트리라고 함
  • 스프링 컨테이너로 인하여 요청이 올때마다 객체를 생성하는것이 아닌 이미 만들어진 객체를 효율적으로 재사용함
    • 기본적으로 스프링의 기본 빈 등록방식은 싱글턴 이지만 요청할때 마다 새로운 객체를 생성해서 반환하는것도 가능

Alt text

싱글턴 방식의 문제점

  • 객체 인스턴스를 하나만 갱성해서 공유하는 싱글턴 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글턴 객체는 무상태(stateless)로 설계해야만 함
    • 특정 클라이언트에 의존적인 필드가 있으면 안됨
    • 특정 클라이언트가 값을 변경할수 있는 필드가 있으면 안됨
    • 가급적이면 read only이여야 함
    • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야만 함
  • 스프인 빈의 필드에 공유 자원을 설정하면 동시성 이슈가 발생할 수 있음

싱글턴 방식의 문제점 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class StatefullService { 
  // 공유 변수 
  private int price;

  public void order(String name, int price) {
    this.price = price;
  }

  public int getPrice() {
    return price;
  }
}

public 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("userA", 10000);

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

        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();

        //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
  • thead 로 인하여 동시적으로 price 변수에 접근하여 서로 값을 변경하려함
  • 위와 같은 이슈를 피하기 위해 반드시 스프링 빈은 항상 무상태(stateless)로 설계해야 함

@Configuration과 싱글턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    ...
}
  • 위 코드를 보면 memberService 빈과 orderService 빈을 만드는 과정에서 new MemoryMemberRepository() 가 두번 호출됨
  • new MemoryMemberRepository() 가 두번 호출되면서 인스턴스가 두개가 생성되며 싱글턴을 만족시키지 못할 것으로 보이나, 동작상 하나의 인스턴스만 존재함
  • 해당 동작에 대해선 아래에 정리

@Configuration과 바이트코드 조작

싱글턴 컨텐이너는 싱글턴 레지스트리임. 따라서 스프링 빈이 싱글턴이 되도록 보장을 해 주어야함. 이를 보장하기 위해 스프링 빈이 두개의 인스턴스가 생성되지 않도록 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용함

1
2
3
4
5
6
7
8
@Test
void configurationDeep() {
    ApplicationContext ac = new  AnnotationConfigApplicationContext(AppConfig.class);
    //AppConfig도 스프링 빈으로 등록된다.
    AppConfig bean = ac.getBean(AppConfig.class);
    
    System.out.println("bean = " + bean.getClass());
}
1
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70

순수한 클래스라면 아래와 같이 출력되어야 함

1
bean = class hello.core.AppConfig

이는 개발자가 만든 클래스가 아닌 스프링의 CGLIB이라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 해당 다른 클래스를 스피링 빈으로 등로하고 있기 때문임

Alt text

이러한 임의로 생성된 다른 클래스가 싱글턴이 보장되도록 해줌 즉, @Bean 이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고 없다면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 생성됨

AppConfig@CGLIB 은 AppConfig의 자식 타입이므로 AppConfig 타입으로 조회할 수 있음

정리

  • @Bean만 사용하더라도 스프링 빈으로 등록되지만 싱글턴을 보장하지 않음
  • 스프링 설정 정보는 항상 @Configuration을 사용하도록 함

참고

  • 스프링 핵심 원리 - 기본편(김영한)
This post is licensed under CC BY 4.0 by the author.