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

흑수·2022년 2월 1일
0

김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.

웹 애플리케이션과 싱글톤

스프링은 웹 애플리케이션을 위해 탄생했고, 웹 애플리케이션은 보통 여러 요청이 동시에 들어옵니다.

스프링을 이용하지 않은 순수 DI 컨테이너는 호출이 들어올 때마다 새로운 객체를 생성했습니다.
즉, 100개의 주문이 들어오면 100개의 객체가 생성되고 소멸된다는 이야기에요. 이는 결국 메모리의 낭비로 이어집니다.

AppConfig appConfig = new AppConfig();
// 1. 조회 : 호출할 때마다 객체 생성
MemberService memberService1 = appConfig.memberService();
// 2. 조회 : 호출할 때마다 객체 생성
MemberService memberService2 = appConfig.memberService();

해결책 -> 하나의 객체를 공유! (싱글톤)

싱글톤 패턴

이는 클래스의 인스턴스가 단 1개만 생성되도록 보장하는 디자인 패턴으로서, 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 합니다.

외부에서 new 연산자를 통해 객체 생성을 방지하기 위해 생성자에 private 키워드 사용

public class SingletonService {
    // 1. static 영역에 올리는 하나의 객체 생성
    private static final SingletonService instance = new SingletonService();

    // 2. 객체를 참조하기 위해 함수 호출
    public static SingletonService getInstance() {
        return instance;
    }

    // 3. 생성자를 private로 설정해서 외부에서 new 연산자 호출 방지
    private SingletonService() {
    }
}

싱글톤 패턴의 문제점

  • 구현 코드가 많음
  • 클라이언트가 구체 클래스에 의존 (DIP 위반)
  • 클라이언트가 구체 클래스에 의존하기 때문에 OCP 위반할 가능성 높음
  • 유연성이 떨어짐
  • 등등..

싱글톤 컨테이너

스프링 컨테이너는 위의 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리합니다. (스프링 빈이 바로 싱글톤으로 관리되는 빈..!!)

즉, 싱글톤 패턴을 적용하지 않아도 스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리합니다.
그냥 개꿀이라는 점..

싱글톤 방식의 주의점

이렇듯 하나의 객체를 공유해서 사용하기 때문에, 싱글톤 객체는 상태를 유지하면 절대 안됩니다. (stateless)

무상태로 설계해야 합니다.

  • 특정 클라이언트에 의존적인 필드, 값을 변경할 수 있는 필드 존재 x
  • 가급적 읽기만 허용

문제가 되는 상황

public class StatefulService {
    private int price;

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

    public int getPrice () {
        return price;
    }
}
@Test
void statefulServiceSingleton() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
    StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

    statefulService1.order("userA", 1000);
    statefulService2.order("userB", 2000);
    int price = statefulService1.getPrice();
    System.out.println("price = " + price);
}

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

위처럼 고객 A와 B가 주문을 하고 A가 price를 조회하면 값이 변경되는 문제가 발생합니다.

@Configuration과 싱글톤

AppConfig 파일을 보면 memberRepository 호출을 memberService와 orderService에서 각각 진행하고 있음을 확인할 수 있습니다.

그렇다는 얘기는 두 번의 객체 생성을 하기 때문에 싱글톤 패턴이 깨지는 게 아닐까요?

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

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

@Bean
public OrderService orderService() {
    return new OrderServiceImpl(memberRepository(), discountPolicy());
}

테스트를 통해 확인해보면 모두 같은 객체이고, 애초에 호출이 한 번되는 것을 확인할 수 있습니다.

MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

// 모두 같은 값 출력
System.out.println("memberService = " + memberService.getMemberRepository());
System.out.println("orderService = " + orderService.getMemberRepository());
System.out.println("memberRepository = " + memberRepository);

@Configuraion과 바이트코드 조작의 마법

우리가 작성한 자바 파일까지 스프링이 싱글톤으로 관리하는 것은 여간 쉽지 않은 이야기입니다.
그래서 실제로 설정파일로 자바 클래스를 등록해줄 때, 순수한 클래스가 아닌 CGLIB라는 바이트코드 조작 라이브러리를 사용해 설정 파일을 상속 받은 임의의 다른 클래스를 만들고, 그 클래스를 스프링 빈으로 등록하게 된답니다.

이 임의의 다른 클래스가 싱글톤을 보장해준답니다.

@Configuration이 이를 가능하게 해줍니다. 즉, 어노테이션이 빠지게 되면 순수 클래스 자체를 등록하게 되므로 싱글톤 패턴이 깨지게 됩니다.

profile
기록용

0개의 댓글