반응형

# 싱글톤 컨테이너

## 싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너가 싱글톤 역할을 한다.
  • 싱글톤 레지스트리 : 싱글톤 객체를 생성, 관리하는 기능.

SingletonTest

public class SingletonTest {
    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 1. 조회 : 호출할 때마다 객체 생성하는지 확인.
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        // 참조값이 다른 것 확인.
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }
}
  • 클라이언트의 요청(memberService 요청) 시 동일한 memberService를 반환(요청시마다 새로 생성하는게 아닌 이미 만들어진 객체(동일한 객체)를 반환).

## 싱글톤 방식의 주의점 (공유필드 조심! 항상 무상태 (stateless) 로 설계하기!)

  • 객체 인스턴스를 하나만 생성해서 공유하는 경우, 여러 클라이언트가 하나의 객체를 공유해서 사용하기 때문에 싱글톤 객체는 상태를 유지 (stateful) 하게 설계하면 안된다. -> 무상태 (stateless) 로 설계해야 한다.
  1. 특정 클라이언트에 의존적인 필드가 없어야 한다.
  2. 특정 클라이언트가 값을 변경할수 있는 필드가 없어야 한다.
  3. 읽기만 가능해야 하고 값을 수정하면 안된다.
  4. 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

상태 유지로 설계된 경우 예시.

StatefulService (상태 유지)

package singleton;

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

statefulServiceTest

package 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(TestConfig.class);

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA : A 사용자 20,000 주문.
        statefulService1.order("userA", 20000);
        // ThreadB : B 사용자 150,000 주문.
        statefulService2.order("userB", 150000);

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

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(150000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
  • 위 테스트 코드 실행 시  A사용자가 주문한 금액을 조회하면 아래와 같은 결과가 나온다.

  • 특정 클라이언트가 공유되는 필드의 값을 변경하여 문제 발생. (공유 필드는 조심해야 함)

무상태로 설계된 경우 예시. (위 상태 유지 코드를 아래와 같이 변경 가능)

package singleton;

public class StatefulService {
    public int order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        return price;
    }
}
package 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(TestConfig.class);

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA : A 사용자 20,000 주문.
        int userAPrice = statefulService1.order("userA", 20000);
        // ThreadB : B 사용자 150,000 주문.
        int userBPrice = statefulService2.order("userB", 150000);

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

    }

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

## @Configuration 과 싱글톤

AppConfig

@Configuration
public class AppConfig {

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

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

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

    // discountPolicy 역할
    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • AppConfig 에서 로직을 살펴보면 아래와 같은 문제점이 존재한다는걸 확인할 수 있다.
@Bean memberService() 시 new MemoryMemberRepository() 호출

@Bean orderService() 시 new MemoryMemberRepository() 중복 호출

 

위 내용 관련하여 테스트해서 확인 진행

MemberServiceImpl 에 테스트용 메서드 생성.

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }

    // 테스트용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

OrderServiceImpl 에 테스트용 메서드 생성.

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    // 테스트용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

ConfigurationSingletonTest

package singleton;

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

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

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        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);
    }
}
  • 테스트 코드를 실행해 보면 아래와 같은 결과를 확인할 수 있다. (동일함)

  • 동일한 인스턴스를 공유해서 사용중임을 확인할 수 있다. 
  • AppConfig 동작 시 아래와 같이 특정 메서드가 중복 호출될 것 같은데 실제로는 중복 없이 호출된다. (@Configuration)
AppConfig.memberService
AppConfig.memberRepository
AppConfig.memberRepository
AppConfig.orderService
AppConfig.memberRepository

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

  • 스프링 컨테이너는 싱글톤 레지스트리임. (스프링 빈이 싱글톤이 되도록 보장)
  • AppConfig 에서 특정 메서드가 중복으로 호출될 것 같은데 실제로는 중복 없이 호출되는 비밀은 @Configuration 에 있다.
  • 아래와 같이 AnnotationConfigApplicationContext 에 파라미터로 넘긴 값(AppConfig) 은 스프링 빈으로 등록된다. 그렇기 때문에 AppConfig 도 스프링 빈으로 등록됨.
new AnnotationConfigApplicationContext(AppConfig.class);

ConfigurationSingletonTest 에서 등록된 AppConfig 조회 관련 테스트 코드 추가. (빈을 조회하여 클래스 정보 출력)

@Test
void configurationDeep() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
}

 

  • 위 테스트 코드 실행 시 아래와 같은 결과를 확인 할 수 있다. (순수한 클래스라면 정상적인 path가 출력)

  • 위와 같은 결과가 나온 이유는 스프링 빈이 CGLIB라는 바이트코드 조작 라이브러리를 사용, AppConfig 클래스를 상속받은 임의의 다른 클래스를 생성하고 그걸 스프링 빈으로 등록한 것.
  • 그래서 이름은 등록한 AppConfig 이지만, 바이트코드를 조작하여 작성된 다른 클래스 정보가 표시되는 것.
  • AppConfig@CGLIB 내부 로직을 예상해 보면 아마도, 등록된 빈이 없으면 신규로 빈을 등록하고, 존재하면 등록된 빈을 반환하도록 되어있을 것임 -> 그래서 AppConfig에서 중복없이 메서드 호출.

@Configuration 부착여부에 따라

  • @Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 를 사용해서 싱글톤을 보장해준다.
  • @Configuration  붙이지 않고 @Bean 만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장하지 않게됨. > AppConfig 실행 시, 중복된 메서드를 호출하게 된다. (중복 호출된 빈은 모두 동일하지 않음)
반응형

+ Recent posts