스프링 컨테이너가 보장해주는 싱글톤 (2)

심현민·2024년 9월 9일
0

Spring

목록 보기
8/18

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

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
    • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
  • 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.
  • 또한 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.

springContainer()

@Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){
        //AppConfig appConfig = new AppConfig();
        AnnotationConfigApplicationContext 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);
    }

싱글톤 방식의 주의점

  • 무상태로 최대한 설계해야한다. 즉, 특정 클라이언트에 의존적인 필드가
    있으면 안된다.
  • 특정 클라이언트가 값을 바꿀 수 없어야 한다.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용해야 한다.

ThreadLocal이란?
자바의 클래스이고 오직 한 쓰레드에 의해서 읽고 쓰여질 수 있는 변수이다. ThreadLocal 변수를 선언하면 멀티쓰레드 환경에서 각 쓰레드마다 독립적인 변수를 가지고 접근할 수 있다.
예를 들어, 일반 변수의 수명은 특정 코드 블록 범위 내에서만 유효하지만, 쓰레드는 쓰레드 영역에 변수를 설정할 수 있기 때문에, 특정 쓰레드가 실행하는 모든 코드에서 그 쓰레드에 설정된 변수 값을 사용할 수 있게 된다.



잘못 설계한 경우

StatefulService.java

package com.example.demo.singleton;

public class StatefulService {

    private int price; //상태를 유지하는 필드
    public int order(String name,int price){
        System.out.println("name = " + name + " price = " + price);
        this.price = price;
        return price;
    }

    public int getPrice(){
        return price;
    }
}

StatefulServiceTest.java

package com.example.demo.singleton;

import org.junit.jupiter.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;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class StatefulServiceTest {

    @Test
    void statfulServiceSingleton(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);


        //ThreadA: A 사용자 10000원 주문
         int userAPrice =statefulService1.order("userA", 10000);

        int userBPrice = statefulService2.order("userB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + userAPrice);

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

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

}
  • 만약에 StatefulService 코드에 order 함수의 반환 타입이 void형이었다면
    나중에 get.Price()를 할 때 치명적인 오류가 발생할 수 있다.
  • statefulService1, statefulService2order를 같이 사용하기 때문에 price의 값이 A가 요청했 을 때 10000원이었다가 B가 요청할 때 20000원으로 바뀌면 나중에 A의 금액을 조회할 때 20000원이
    나오는 오류가 발생할 수 있다.
  • 따라서 order 함수의 반환 타입을 int로 하여 price값을 저장할 수 있게 해준다. 나중에 조회할 때는 A의 값 B의 값 따로 조회할 수 있게 설정

이를 토대로 싱글톤 방식의 문제점을 정리해보면,

싱글톤 방식 문제점

  • 인스턴스를 같은 사람이 쓰기 때문에 사용자 A 이후에 사용자 B가 왔기 때문에 getPrice()를 하게 되면 20000이 나오게 된다.
  • 특정 클라이언트가 공유되는 값을 변경해버리는 것이 문제가 된다.
  • 따라서 공유 필드는 각별하게 신경써야 한다.
  • 따라서 무상태로 설계하는 것이 중요하다.

@Configuration과 싱글톤

  • 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
  • 분명 memberService, memberRepository, orderService가 여러번 호출되어야 하지만, 한 번씩밖에 호출이 안되었다.

ConfigurationSingletonTest.class

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 memberRepository = ac.getBean("memberRepository", MemberRepository.class);
        MemberRepository memberRepositoy1 = memberService.getMemberRepositoy();
        MemberRepository memberRepositoy2 = memberService.getMemberRepositoy();

        System.out.println("memberService -> memberRepository = " + memberRepositoy1);
        System.out.println("memberService -> memberRepository = " + memberRepositoy2);
        System.out.println("memberRepository = " + memberRepository);

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

    }
  • ac.getBean(AppConfig.class); 를 직접 호출해주면 이상한 게 나온다
    bean = class com.example.demo.AppConfig$$SpringCGLIB$$0
  • 순수한 클래스라면 다음과 같이 출력되어야 한다.
    class.com.example.demo.AppConfig

configurationDeep()

@Test
    void configurationDeep(){

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean.getClass());
    }
  • AppConfig를 가져다가 CGLIB라는 라이브러리를 상속받아서 다른 클래스를 만든 것
  • 그 다음 조작한 클래스를 스프링 컨테이너에 등록한다.
  • 이 임의의 다른 클래스가 싱글톤이 되도록 보장을 해주는 것이다.
  • 우리는 AppConfig가 실행된다고 알고 있지만, 사실은
    AppConfig@CGLIB가 실행되고 있는 것이다.

AppConfig@CGLIB가 내부적으로 실행되는 모습

AppConfig는 아마 내부적으로 이렇게 실행될 것이다.

@Bean
public MemberRepository memberRepository(){
    if(MemoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?){
        return 스프링 컨테이너에서 찾아서 반환;

    } else {
        기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        return 반환
    }
}
  • 같은 것이 오면 기존에 있던 것을 반환하고
  • 만약 기존꺼가 없으면 기존 로직을 호출해서 생성한다.
  • 따라서 싱글톤이 보장되는 것이다.

@Configuration을 적용하지 않고 @Bean만 적용하면?

  • 당연히 싱글톤이 깨진다.
  • 또한 기존에 memberService가 참조하는 memberRepository의 주소가 서로 달라진다.
  • 이를 해결하기 위해 @AutoWired를 통해 직접 의존성 주입을 해주는 방법이 있지만 잘 사용할 일이 없으므로 설명은 생략하겠다.
profile
혼자 성장하는 것보다 함께 성장하는 것을 선호합니다.

0개의 댓글