[Spring - Inflearn] 웹 애플리케이션과 싱글톤

728x90
반응형
SMALL

스프링이 없는 순수한 DI 컨테이너

 
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainter() {
    AppConfig appConfig = new AppConfig();

    // 호출할 때마다 객체를 생성
    MemberService memberService1 = appConfig.memberService();
    MemberService memberService2 = appConfig.memberService();

    // 참조값이 다르다. JVM 메모리에 새로운 객체가 계속 생성되어 저장된다.    
    assertThat(memberService1).isNotSameAs(memberService2);
}    

호출이 올 때마다 객체를 새로 생성하고, 이후 소멸하기에 메모리 낭비가 심하다.

 

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
    • private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.
 
public class SingletonService {

    // static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();

    // public으로 열어서 객체 인스턴스가 필요하면 getInstance()를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance() {
        return instance;
    }

    // private으로 생성자를 선언하여 new 키워드를 통한 객체 생성을 막는다.
    private SingletonService() {

    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
 
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
    // SingletonService() has private access ~
    // new SingletonService();

    // getInstance()를 통해 static 영역에 선언된 객체를 사용한다.
    SingletonService singletonService1 = SingletonService.getInstance();
    SingletonService singletonService2 = SingletonService.getInstance();

    // 참조값이 같다.
    assertThat(singletonService1).isSameAs(singletonService2);
}

호출이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용한다.

 

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너 자체가 싱글톤 컨테이너 역할을 한다. (= 싱글톤 레지스트)
  • 싱글톤 패턴을 위한 지저분한 코드가 필요하지 않다.
  • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
 
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);

    assertThat(memberService1).isSameAs(memberService2);
}

 

스프링의 기본 빈 등록 방식은 싱글톤이지만, 다른 방식도 지원한다. 
singleton, protorype, request, session, global session.... => bean scope 참고

 

싱글톤 방식의 주의점

  • 객체 인스턴스를 하나만 생성해서 공유하는 방식이기에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안 된다.
  • 무상태(stateless)로 설계해야 한다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는 지역 변수, 파라미터, ThreadLocal 등을 사용해야 한다.
 
public class StatefulService {

    private int price;  // 상태를 유지하는 필드
    
    public void order(String name, int price) {
        this.price = price; // 여기가 문제!
    }

    public int getPrice() {
        return price;
    }
}

@Test
void statefulServiceSingleton() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    StatefulService statefulService1 = ac.getBean(StatefulService.class);
    StatefulService statefulService2 = ac.getBean(StatefulService.class);

    // Thread A : A사용자 10000원 주문
    statefulService1.order("userA", 10000);

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

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

	// A사용자의 주문 금액 10000이 아닌 B사용자의 주문 금액인 20000이 나온다.
    assertThat(statefulService1.getPrice()).isEqualTo(20000);
}

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

공유 필드는 항상 주의, 스프링 빈은 항상 무상태(stateless)로 설계해야 한다.

 

@Configuration

  • 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
  • 스프링은 CGLIB이라는 바이트코드 조작 라이브러리를 통해서 임의의 클래스를 만들고, 그 클래스를 빈으로 등록한다.
  • @Bean 어노테이션만 있어도 스프링 빈으로 등록되지만, 싱글톤을 보장하지는 않는다.
  • 그냥 @Configuration 쓰자.

 

728x90
반응형
LIST