hoon's bLog

Spring gradle project 스프링 핵심 원리 기본편 | 싱글톤 컨테이너 Singleton Container @Configuration 본문

IT/Spring

Spring gradle project 스프링 핵심 원리 기본편 | 싱글톤 컨테이너 Singleton Container @Configuration

개발한기발자 2024. 5. 20. 10:19
반응형


본 포스팅은 인프런에 있는 인터넷 강좌인,

김영한 강사님의 스프링 핵심 원리 기본편을 공부하며,

개인적으로 공부하고, 정리하는 용도로 포스팅을 해보겠다.

 

Spring gradle project 환경설정 및 회원 가입 서비스 예제 만들기
Spring gradle project 주문/할인 도메인 설계

Spring gradle project 객체 지향 원리 적용

Spring gradle project AppConfig 리팩토링 OCP 위반 해결 및 중복 제거

Spring gradle project 좋은 객체 지향 설계 5가지 원칙 적용 및 스프링 전환

Spring gradle project 스프링 컨테이너와 스프링 빈

Spring gradle project BeanFactory, ApplicationContext 이해 및 차이

Spring gradle project XML 설정 사용, BeanDefinition, 스프링 빈 설정 메타 정보

Spring gradle project 웹 애플리케이션과 싱글톤, WebApplication and Singleton

싱글톤 컨테이너, @Configuration과 싱글톤

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안 된다.
  • 무상태(stateless)로 설계, 즉, 특정 클라이언트에 의존적인 필드,  값을 변경할 수 있는 필드가 있으면 안 된다.
    가급적 읽기만 가능하고, 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 의도치 않은 큰 장애가 발생할 수 있다!

상태를 유지할 경우 발생하는 문제점 예시

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;
    }
}

 

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(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

        // Thread A: A 사용자 10000원 주문
        statefulService1.order("userA", 10000);

        // Thread B: B 사용자 20000원 주문
        statefulService2.order("userB", 20000);

        // Thread A: 사용자 A 주문 금액 조회
        int price = statefulService1.getPrice();

        // Thread A: 사용자 A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);

        // Assertion
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
  • ApplicationContext를 사용하여 TestConfig 설정 클래스에서 정의된 빈을 로드하고, AnnotationConfigApplicationContext는 Java 기반의 설정 클래스에서 빈을 로드하는 데 사용된다.
  • statefulService1과 statefulService2는 같은 싱글턴 인스턴스를 참조하고 있다.
    여기서 두 개의 다른 스레드가 같은 서비스 인스턴스에 접근해서 order 메서드를 호출해서, statefulService1은 사용자 A의 주문을 10000원에 처리하고, 동시에 statefulService2는 사용자 B의 주문을 20000원에 처리한다.
  • 주문 금액을 저장하는 변수가 서비스 내부에 상태로 관리되기 때문에, 사용자 A가 주문 금액을 조회할 때 사용자 B의 주문 금액이 출력되는데, 이것이 바로 싱글턴 서비스가 상태 정보를 갖게 되면 생길 수 있는 문제점이다.
  • 따라서, 싱글턴 빈은 상태를 가지지 않는 게 좋고, 상태가 필요하면 각 요청이나 사용자별로 독립적인 빈 인스턴스를 생성하는 방식을 고려해야 한다.

@Configuration과 싱글톤

@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();
    }
    ...
}
  • memberService 빈을 만드는 코드를 보면 memberRepository()를 호출하면서, new MemoryMemberRepository()를 호출
  • orderService 빈을 만드는 코드도 동일하게 memberRepository()를 호출하면서 new MemoryMemberRepository()를 호출
  • 결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보인다.
  • 스프링 컨테이너를 이용해 이 문제를 해결해 보자!

검증을 위한 코드 추가

public class MemberServiceImpl implements MemberService {
    
    private final MemberRepository memberRepository;
    
    //테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

public class OrderServiceImpl implements OrderService {
    
    private final MemberRepository memberRepository;
    
    //테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
package hello.core.singleton;

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

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext 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);

        // 출력하여 인스턴스 비교 확인
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);

        // 모두 같은 인스턴스인지 검증
        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}
  • getMemberRepository() 메서드를 호출하여 각 서비스 내의 MemberRepository 인스턴스를 출력해 확인하고, 직접 memberRepository 빈도 가져와서 출력
  • MemberRepository가 실제로 컨테이너로부터 직접 가져온 memberRepository 빈과 동일한 인스턴스인지 확인해서, 모두 true면, 스프링이 성공적으로 싱글톤을 보장하고 있는 것이고, 설정 클래스가 제대로 작동하고 있는 것이다.

AppConfig에 호출 로그 남김

import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
    //1번
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public OrderService orderService() {
        //1번
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    @Bean
    public MemberRepository memberRepository() {
    
        System.out.println("call AppConfig.memberRepository"); return new MemoryMemberRepository();
    }
    
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
    
    // 출력
    // call AppConfig.memberService
    // call AppConfig.memberRepository
    // call AppConfig.orderService
}
  • 스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성한다
  • 그래서 memberRepository()는 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository(), memberService() 로직에서 호출, orderService() 로직에서 호출 총 3번을 하는 것 같지만, 결과는 모두 1번만 호출한다.

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

  • 스프링 컨테이너는 싱글톤 레지스트리다.
  • 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
  • 위 코드를 보면 분명 3번 호출되어야 하는 것이 맞다.
  • 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
@Test
void configurationDeep() {
    ApplicationContext ac = new
    AnnotationConfigApplicationContext(AppConfig.class);
    
    //AppConfig도 스프링 빈으로 등록된다.
    AppConfig bean = ac.getBean(AppConfig.class);
    
    System.out.println("bean = " + bean.getClass());
    //출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
  • AnnotationConfigApplicationContext` 에 파라미터로 넘긴 값은 스프링 빈으로 등록된다.
  • 따라서 AppConfig도 스프링 빈이 된다.
  • 그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다.
  • 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!
  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어지기 때문에, 싱글톤이 보장되는 것이다.

@Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까?

  • AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 확인할 수 있다.
// 출력
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
// call AppConfig.memberRepository
// call AppConfig.memberRepository
  • 출력 결과를 통해서 MemberRepository가 총 3번 호출된 것을 알 수 있다.
  • 1번은 @Bean에 의해 스프링 컨테이너에 등록하기 위해서이고, 2번은 각각 memberRepository()를 호출하면서 발생한 코드다.

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
  • 스프링 설정 정보는 항상 @Configuration을 사용하자.

끝.

728x90
반응형