[스프링 핵심 원리 - 기본편] 5. 싱글톤 컨테이너

HJ·2022년 7월 30일
0

김영한 님의 스프링 핵심 원리 - 기본편 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard


1. 웹 어플리케이션과 싱글톤

  • 웹 어플리케이션은 보통 여러 고객이 동시에 요청 --> 스프링 없는 순수한 DI 컨테이너는 고객이 요청한만큼 객체가 생성됨

1-1. 테스트 코드로 확인

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

    // 1. 조회 : 호출할 때마다 객체를 생성하는지
    MemberService memberService1 = appConfig.memberService();

    // 2. 조회 : 호출할 때마다 객체를 생성하는지
    MemberService memberService2 = appConfig.memberService();

    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);

    assertThat(memberService1).isNotSameAs(memberService2);
}

  • 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다

  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다 --> 메모리 낭비가 심하다

  • 해결방안은 해당 객체가 딱 1개만 생성되도록 하고, 이 객체를 공유하도록 설계하면 된다 --> 싱글톤 패턴




2. 싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴

    • 하나의 JVM 안에서는 객체 인스턴스가 딱 1개만 생성되도록
  • 스프링에서는 스프링 컨테이너가 객체를 싱글톤으로 만들어서 관리해준다


2-1. 싱글톤 패턴 구현 예시

public class SingletonService {

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

    // 조회 시 사용하는 메소드
    public static SingletonService getInstance() {
        return instance;
    }

    // private 생성자
    private SingletonService() { }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
  • private static final SingletonService instance = new SingletonService() : static 영역에 객체 instance를 미리 하나 생성해서 올려둔다

  • 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다

  • 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 선언해서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다 ( 2개 이상 생성하지 못하도록 막는다 )


  • 싱글톤 패턴을 구현하는 방식은 매우 다양함

  • 위의 방식은 객체를 미리 생성해두는 방식을 사용 ( 단순하고 안전 )


2-2. 테스트 코드로 확인

@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonService() {
    SingletonService singletonService1 = SingletonService.getInstance();
    SingletonService singletonService2 = SingletonService.getInstance();

    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);

    assertThat(singletonService1).isSameAs(singletonService2);
}

  • 두 객체가 같은 객체임을 확인

  • 싱글톤 패턴을 적용하면 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 사용

  • 그러나 싱글톤 패턴에도 여러 문제점이 존재




3. 싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다

  • 의존관계상 클라이언트가 구체 클래스에 의존한다 --> DIP를 위반한다

    • 객체를 꺼낼 때 구체클래스.getInstance()를 사용해야함
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다

  • 테스트하기 어렵다

  • 내부 속성을 변경하거나 초기화 하기 어렵다

  • private 생성자를 사용하기 때문에 자식 클래스를 만들기 어렵다

  • 유연성이 떨어진다

    • DI 적용이 어렵다
  • 안티패턴으로 불리기도 한다




4. 싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다

    • 컨테이너는 객체를 하나만 생성해서 관리한다
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다

  • 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다

  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다

    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다

    • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다

  • 스프링 빈이 바로 싱글톤으로 관리되는 빈이다


4-1. 테스트 코드로 확인

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
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);
}




5. 싱글톤 방식 사용 시, 주의점

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

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

    • 특정 클라이언트에 의존적인 필드가 있으면 안된다

    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다

    • 가급적 읽기만 가능해야 한다 ( 수정하면 안된다 )

    • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다

  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다


public class StatefulService {
    private int price;  // 상태를 유지하는 필드

    public void order(String name, int price) {
        System.out.println("name = " + name + "price = " + price);
        this.price = price;     // 여기가 문제
    }
}
  • price가 공유되는 필드이기 때문에 아래처럼 price 필드를 없애고 공유되지 않는 parameter를 사용하도록 order() 수정

public class StatefulService {

    public int order(String name, int price) {
        return price;
    }
}



6. @Configuration과 싱글톤

public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}
  • memberService() 호출 ➜ memberRepository() 호출 ➜ return new MemoryMemberRepository()

  • orderService() 호출 ➜ memberRepository() 호출 ➜ return new MemoryMemberRepository()

  • 각각 다른 2개의 MemoryMemberRepository 가 생성되면서 싱글톤이 깨지는 것 처럼 보인다

  • 하지만 실제로 확인해보면 그렇지 않다 (아래 사진 참고)


6-1. memberRepository 객체 확인

  • 첫 번째는 memoryRepository

  • 두 번째는 memberService에 의한 memoryRepository

  • 세 번째는 orderService에 의한 memoryRepository

  • 결론 : memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용됨


6-2. 호출 확인

  • 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출

  • memberService() 로직에서 memberRepository() 호출

  • orderService() 로직에서 memberRepository() 호출

  • memoryRepository()가 총 세 번 호출된다고 생각할 수 있는데 실제로는 1번만 호출됨 ( 이유는 @Configuration 때문인데 자세한 설명은 아래 참고 )


6-3. AppConfig 확인

  • AnnotationConfigApplicationContext 에 파라미터로 넘긴 값은 스프링 빈으로 등록된다

  • AppConfig 스프링 빈을 조회해서 클래스 정보를 출력하면 위와 같이 출력된다

  • 순수한 클래스라면 class com.ghkwhd.core.AppConfig로 출력되어야 함

  • 순수한 클래스와는 다르게 클래스 이름에 xxxCGLIB가 붙어있다


  • 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다

  • 바로 이 임의의 다른 클래스가 싱글톤이 보장되도록 해준다


6-4. 결론

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다
    • memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 호출할 때 객체를 계속 생성하여 싱글톤을 보장하지 않는다
  • @Configuration과 함께 사용해야 바이트코드를 조작하는 CGLIB 기술을 사용해 싱글톤을 보장

    • 싱글톤을 보장하기 위해 스프링 설정 정보에는 반드시 @Configuration를 사용
  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어지기 때문에 싱글톤이 보장되는 것이다
  • @Configuration이 없는 경우

    • memberRepository() 가 여러 번 호출된다 ➜ 객체 여러 개 생성됨 ➜ 싱글톤 보장 X

    • 이 때 생성된 객체들은 스프링 빈이 아니다

    • 스프링 컨테이너에 등록된 객체가 아닌 다른 객체들임

      • new 키워드를 통해 객체를 생성한 것과 마찬가지

      • MemoryMemberRepository 인스턴스가 동일하지 않다

0개의 댓글