본문 바로가기
개발 관련 공부/스프링 김영한 로드맵

[스프링 기본] 5. 싱글톤 컨테이너

by 슴새 2022. 10. 12.
반응형

웹 어플리케이션과 싱글톤

대부분의 스프링 애플리케이션은 웹 애플리케이션이다.

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

요청할때마다 객체를 만들면 낭비가 심하다. 

그러므로 싱글톤 패턴으로 설계하는게 효율적이다.

 

싱글톤 패턴

private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
    return instance;
}

자바에서 배운것과 같이, private static으로 생성하고 getInstance를 통해서만 조회할 수 있도록 하면 된다.

이러면 단 하나의 인스턴스를 여럿이 돌려쓰게 된다. 

 

하지만 이런 싱글톤 패턴은 문제점이 많다.

  • 코드 자체가 많이 들어감.
  • 클라이언트가 구체 클로스에 의존(DIP 위반)
  • 초기화 힘듦
  • private이라 자식 클래스 만들기 힘듬

 

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.

이것이 바로 스프링 기본 3에서 언급했던 스프링 컨테이너의 장점 중 하나이다. 

지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

 

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
 //1. 조회: 호출할 때 마다 같은 객체를 반환
 MemberService memberService1 = ac.getBean("memberService",MemberService.class);
 //2. 조회: 호출할 때 마다 같은 객체를 반환
 MemberService memberService2 = ac.getBean("memberService",MemberService.class);

이렇게 getBean으로 조회하면 싱글톤처럼 같은 객체를 반환한다.

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

 

싱글톤 방식의 주의점

싱글톤 객체는 여럿이 쓰는 것이므로 stateless 하게 설계해야 한다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능해야 한다.  
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;
    

예를 들어 이런 코드가 있다고 치자.

 

StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);

이렇게 테스트하면, A 사용자한테 B 사용자의 주문 내역이 출력된다.

공유필드 price를 특정 클라가 값을 변경해서 발생한 문제이다.

이렇게 공유필드를 두지 말고 파라미터로 받은 값을 바로 리턴해서 넘기는게 더 현명하다.

 

스프링 빈은 항상 무상태(stateless)로 설계하자.

@Configuration과 싱글톤

이제 다시 AppConfig를 보자.

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

맴버서비스와 오더서비스가 둘 다 memberRepository를 호출한다.

그 말은 new MemoryMemberRepository가 두 번 호출되었다는 소리이다.

이러면 싱글톤이 깨지는 것처럼 보이는데?

하지만 결론적으로 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다. 

 

@Bean
public MemberService memberService() {
    System.out.println("call AppConfig.memberService");
    return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
    System.out.println("call AppConfig.orderService");
    return new OrderServiceImpl(
            memberRepository(),
            discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
    //스프링 컨테이너가 한 번, memberService()에서 한 번, orderService()에서 한 번 호출
    System.out.println("call AppConfig.memberRepository");
    return new MemoryMemberRepository();
}

출력문을 찍어보면...

memberService, orderService 모두 호출됐음에도 memberRepository는 한 번만 출력된 것을 볼 수 있다.

 

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

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해야 한다.

자바코드가 위와 같은 상황에서 싱글톤을 보장하기 위해, 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 

@Configuration
public class AppConfig {
....
}

비밀은 @Configuration이 붙은 AppConfig에 있다.

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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

그래서 AppConfig 도 스프링 빈이 된다.

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());

AppConfig 빈을 조회해서 출력해보자.

AppConfig뒤에 이상한게 붙어서 출력된다.

순수한 클래스를 출력할 땐 저런게 붙지 않았다.

저것의 정체는 내가 만든 클래스가 아니다. 스프링이 바이트코드 조작 라이브러리인 CGLIB를 사용해 AppConfig 상속받은 다른 클래스를 만들고, 걔를 스프링 빈으로 등록한 것이다. AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다.

 

그리고 이렇게 만들어진 AppConfig@CGLIB가 싱글톤을 보장하는 로직을 가지고 있다.

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

 

이 덕분에 자바 코드에서 new 빈객체()가 여러번 호출되어도 스프링은 싱글톤을 보장할 수 있다.

 

@Configuration이 빠진다면?

스프링빈에 순수한 AppConfig가 등록되고, 각각 다 다른 MemoryMemberRepository가 만들어져 싱글톤이 깨지게 된다.

 

그러므로 스프링 설정 정보는 항상 @Configuration을 사용하자.

 

 

이 포스팅은 인프런 김영한님의 '스프링 핵심 원리 기본편'을 듣고 정리한 것입니다.
강의 링크: https://url.kr/udopsk  
반응형

댓글