SpringBoot/Spring 김영한님

Spring 김영한 - 스프링 입문 (4)

함형우 2023. 1. 16. 09:06
728x90

싱글톤 컨테이너

스프링은 시작부터 기업용 온라인 서비스 기술을 지원하기 위해서 탄생했다. 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 즉 여러 고객의 동시요청을 커버해야한다.

 

고객이 요청할 때마다 새로운 객체를 생성할 경우, 요청마다 트래픽이 생기고 트래픽마다 객체가 생기게되어 메모리의 낭비가 심하게된다. 그렇기 때문에 객체를 딱 1개만 생성하고, 나머지는 공유하도록 설계해야하는데 이런 설계를 싱글톤 패턴을 적용했다고 한다.

 

private를 사용하여 외부에서 임의로 객체를 생성할 수 없게 만들고, 클래스의 인스턴스를 딱 1개만 사용하도록 보장해야한다.

private static final SingletonService instance = new SingletonService()우

싱글톤 패턴의 문제점

  • 싱글톤 패턴이던, 스프링에서 싱글톤 컨테이너를 사용하던, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
  • 무상태로 설계해야한다,.
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 읽기만 가능해야 한다.
    • 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, 스레드로컬 등을 사용해야한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
		@Test
    @DisplayName("스프링 없음")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();

        Assertions.assertThat(memberService1).isNotSameAs(memberService2);

    }

스프링 컨테이너의 기본 빈 등록 방식은 싱글톤 패턴이지만, 싱글톤 방식만 지원하는 것은 아니다 새로운 객체를 생성하여 반환하는 기능도 제공한다. 빈 스코프 참조.

 

아래의 코드는 StatefulService의 price필드가 공유되는 필드인데, 특정 클라이언트 값을 변경하는 코드이다. 주문금액같은 경우 userA의 값이 10000이 되어야하는데 20000이라는 결과가 나온다.

package hello.core.notico.singleton;

public class StatefulService {

    private int price;

    public void order(String name,int price) {
        System.out.printf("name = " + name + "price = "+ price);
        this.price = price; // 문제점
    }

    public int getPrice() {
        return price;
    }

}

package hello.core.notico.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext();
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

        statefulService1.order("userA", 10000);
        statefulService2.order("userB", 20000);

        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);

    }

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

@Configuration과 싱글톤

싱글톤 컨테이너의 대해 이해한 후, AppConfig의 코드를 보면 아래와 같은 문제점이 보인다.

  • memberService 빈을 만드는 코드의 memberRepository() 가 호출된다.
    • 이 메서드르 호출하면 new MemoryMemberRepository() 를 호출한다.
  • orderService 빈을 만드는 코드도 동일하게 memberRepository() 가 호출된다.
    • 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.

각각 다른 2개의 객체가 생성되어 싱글톤이 깨지는 것 처럼 보인다.

package hello.core.notico.singleton;

import hello.core.notico.AppConfig;
import hello.core.notico.member.MemberRepository;
import hello.core.notico.member.MemberServiceImpl;
import hello.core.notico.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        System.out.println("memberService -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);

    }

}

테스트코드를 작성하여 확인해보면, memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다. AppConfig의 자바 코드를 봤을때의 결과창과 조금 다룬 결과가 도출된다.

 

@Configuration과 바이트코드

 

스프링 컨테이너는 싱글톤 레지스트리다, 스프링 빈이 싱글톤이 되도록 보장해주어야 하는데 자바코드 자체를 수정할 수는 없으니 스프링은 클래스 바이트코드를 조작하는 라이브러리를 사용한다.

스프링 자체가 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들어, 그 다른 클래스를 스프링 빈으로 등록한 것 이다. 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다.

@Bean
public MemberRepository memberRepository() {
 
		 if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
		 return 스프링 컨테이너에서 찾아서 반환;
		 } else { //스프링 컨테이너에 없으면
		 기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
		 return 반환
	 }
}

CGLIB라는 기술의 예상도이다. 훨씬 복잡한 코드를 가지고있지만, 대력적으로 이런 기능을 한다고 생각하면 편하다. 이미 등록되어 있는 컨테이너가 있다면, 스프링 컨테이너 내에 이미 등록된 빈을 반환한다. 최초 등록하는 빈이라면 그대로 등록된다. 이러한 기능이 내부적으로 이루어지기때문에 싱글톤이 유지된다.

 

@Bean만을 사용해도 스프링 빈은 등록되지만 싱글톤은 보장되지 않는다. 그러나 @Configuration을 사용하여야 스프링 설정 정보가 사용된다.

728x90