[spring 핵심 기본] 싱글톤 컨테이너

채원·2024년 7월 11일

스프링

목록 보기
13/18
post-thumbnail

출처) 인프런 스프링 핵심 원리 기본편 강의

웹애플리케이션과 싱글톤

스프링이 없는 순수 DI 컨테이너는 아래 그림과 같이 클라이언트가 요청할 때마다 객체를 새로 만들어서 보내줌

고객 트래픽이 많이 나올 때, 초당 객체가 생성되고 소멸되면서 메모리 낭비가 심함
딱 한 개의 객체만 생성하고, 공유하는 싱글톤 패턴으로 해결

싱글톤 패턴

싱글톤 = 클래스의 인스턴스를 오직 하나만 생성하도록 보장하는 패턴
private 생성자를 사용해서 외부에서 임의로 new 키워드 사용하지 못하게 막아야함.
싱글톤 패턴 구현 중 가장 단순, 안전한 방법 (미리 객체 생성)

package hello.core.singleton;

public class SingletonService {
    private static final SingletonService instance = new SingletonService();;

    public static SingletonService getInstance(){
        return instance;
    }

    private SingletonService(){

    }
    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}
  1. static 영역에 객체 instance를 생성해서 올려둠
  2. 객체 인스턴스가 필요하면 오직 getInstance()를 통해서만 조회 가능 (항상 같은 인스턴스)
    ⭐️ 3. 생성자를 private으로 하여서, 외부에서 new로 객체 인스턴스가 생성되는 것을 막음

테스트를 통해서 실제로 같은 인스턴스인지 확인
+) same vs equal

  • isSameAs: == 비교하기
  • isEqual: 자바의 equals 메서드
    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest(){
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        // 두 객체가 다른 것을 확인
        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
    }

싱글톤 패턴의 문제점

  • 구현 코드가 많이 들어감
  • 의존 관계상 클라이언트가 구체 클래스에 의존 DIP 위반
  • private 생성자로 자식 클래스를 만들기 어려움
  • 유연성이 떨어짐

-> 스프링 컨테이너는 기존 싱글톤이 가진 단점은 해결하고, 객체를 싱글톤으로 관리해줌

싱글톤 컨테이너

스프링에서는 싱글톤 패턴을 적용하지 않아도, 객체를 싱글톤으로 관리함
싱글톤 레지스트리
스프링 컨테이너에서, 빈 객체를 미리 생성해서 등록하고, 조회 요청이 들어오면 같은 걸 반환하는 기능

요청이 올 때마다 객체 생성을 하는 것이 아니라, 이미 만들어진 객체를 공유해서 재사용함.
아래 테스트코드를 돌리면 같은 것을 확인할 수 있음

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

        // 두 객체가 다른 것을 확인
        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }

⭐️ 싱글톤 방식의 주의점

여러 클라이언트가 같은 객체 인스턴스를 공유하므로, 싱글톤 객체는 상태를 stateful하게 설계하면 안 됨. ‼️ stateless ‼️

  • 특정 클라이언트에 의존적인 필드 X
  • 특정 클라이언트가 값을 변경할 수 있는 필드 X
  • 가급적 읽기만 가능
  • 필드 대신 공유되지 않는 지역 변수, 파라미터, ThreadLocal 사용

StatefulService에서 price 필드를 각 클라이언트가 변경할 수 있도록 한 예시

public class StatefulService {
  private int price; // 상태 유지

  public void order(String name, int price) {
    System.out.println("name = " + name + "price = " + price);
    this.price = price; // 클라이언트가 필드를 변경할 수 있음 (문제가 되는 부분)
  }
  ....

유저 A이 주문하고, 이후 유저B 가 주문한 상황
필드값이 유저2에 의해 변경되어서, 유저1의 주문 금액을 조회했을 때 변경된 값이 나옴
유저 A나 B나 같은 객체이기 때문에 공유 필드를 두면 안 됨

기존 코드에서 price를 지역 변수로, 값을 받아 리턴하는 방식으로 고침

public int order(String name, int price) {
    System.out.println("name = " + name + "price = " + price);
    return price;
  }

@Configuration과 싱글톤

AppConfig 코드를 보면
memberService 호출 시 -> memberRepository 호출 -> new MemoryMemberRepository()

orderService 호출 시 -> memberRepository 호출 -> new MemoryMemberRepository()

이러면 MemoryMemberRepository 객체가 두개 생성되어서 싱글톤이 깨지는 것 아닌가?

test로 확인해보면, 두 객체가 같은 것을 확인할 수 있음

메서드 호출 로그를 확인해보면, memberRepository()는 한 번만 호출됨

@Configuration과 바이트코드 조작

AppConfig 코드상으로는 3번 호출되어야하는 memberRepository()는 1번만 호출됨
@Configuration로 바이트코드를 조작하기 때문
CGLIB이라는 바이트코드 조작 라이브러리를 가지고, AppConfig를 상속받아 조작한 클래스를 컨테이너에 등록함

@Bean 메서드가 붙은 메서드마다 이미 빈이 존재하면, 반환하고 없으면 생성해서 등록함

@Bean만 적용하면, 순수한 AppConfig가 등록되어서 싱글톤이 깨짐
스프링 설정 정보는 항상 @Configuration 을 사용

0개의 댓글