본문 바로가기

강의/스프링 핵심 원리 - 기본편

Bean (싱글톤의 개념과 사용시 주의점)

웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.

요청을 할 때마다 Controller, Service, Repository를 생성하면 메모리 낭비가 심하기 때문에, 스프링에서는 싱글톤으로 객체를 가지고 있다.

싱글톤이란 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.

 

@Configuration과 싱글톤

@Configuration은 @Bean에 추가 설정을 줘서 싱글톤으로 만들지 않는 이상 무조건 빈에 대해 싱글톤을 보장한다.

아래 코드를 살펴 보자.

 

@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를 빈으로 등록할 때 모두 memberRepository() 메소드를 호출하는 것을 알 수 있다.

결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되어 싱글톤이 깨진다고 생각할 수 있다.

하지만 @Configuration은 클래스의 바이트 코드를 조작하는 라이브러리인 CGLIB를 사용하여 싱글톤을 보장한다.

CGLIB는 프록시 객체의 일종으로 AppConfig가 빈으로 등록될 때, AppConfig 대신 AppConfig를 상속 받은 AppConfig$CGLIB 형태로 프록시 객체가 등록된다.

 

 

위와 같이 이름은 appConfig가 되고, 실제 등록되는 스프링 빈은 CGLIB 클래스의 인스턴스가 등록된다. CGLIB는 대강 아래와 같이 구현이 되어 있다고 생각하면 편하다.

@Bean
public MemberRepository memberRepository() {
    if(memorymemberRepository가 이미 스프링 컨테이너에 등록되어있으면?) {
        return 스프링 컨테이너에서 찾아서 반환;
    } else { // 스프링 컨테이너에 없으면
        기존로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        return 반환;
    }
}

 

@Bean이 등록된 메소드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

이 덕분에 싱글톤이 보장되는 것이다.

참고로 AppConfig$CGLIB는 AppConfig의 자식 타입이므로 AppConfig 타입으로 조회가 가능하다.

 

싱글톤 방식의 주의점


여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.

무상태(stateless)로 설계해야 한다!

 

아래와 같이 인스턴스 변수로 price를 두고 있는 상태라고 가정해보자.

 

public class StatefulSingleton {

    private int price;

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


    public int getPrice() {
        return price;
    }
}

 

위와 같이 인스턴스 변수에 특정 값을 저장할 경우 아래와 같은 문제가 발생할 수 있다.

 

@DisplayName("stateful singleton test")
@Test
void stateful_singleton() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(
        TestConfig.class);

    StatefulSingleton statefulSingleton1 = ac.getBean("statefulService", StatefulSingleton.class);
    StatefulSingleton statefulSingleton2 = ac.getBean("statefulService", StatefulSingleton.class);

    statefulSingleton1.order(10000);
    statefulSingleton2.order(20000);

    Assertions.assertThat(statefulSingleton1.getPrice()).isEqualTo(20000);

}

 

고객 1명은 10000원을 결재 요청했음에도 불구하고, 실질적으로는 20000원이 결재 된 것을 확인할 수 있다.

위와 같이 인스턴스 변수를 사용하기 보다는 지역변수를 사용하는 습관을 가져서 이러한 위험에서 벗어나도록 하자.