<aside> ❗

웹 애플리케이션과 싱글톤

image.png

// 순수한 DI 컨테이너
    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();
        // 1. 조회: 호출할 때 마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();

        // 2. 조회: 호출할 때 마다 객체를 생성
        MemberService memberService2 = appConfig.memberService();

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

        // 두 객체가 다른 경우 테스트 통과
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }
 /*
  결과 - 서로 다른 객체 생성됨
	memberService1 = hello.core.member.MemberServiceImpl@709ba3fb
	memberService2 = hello.core.member.MemberServiceImpl@3d36e4cd
 */

<aside> ❗

싱글톤 패턴

package hello.core.singleton;

public class SingletonService {

    // SingletonService 클래스가 로드될 때 자기 자신의 인스턴스를 딱 한 번 생성하여, 이후 변경 불가능한 클래스 변수로 참조하게 함
    private static final SingletonService instance = new SingletonService();

    // instance 참조 변수의 Getter
    // 해당 인스턴스를 꺼낼 수 있는 방법은 이 Getter 밖에 없다.
    public static SingletonService getInstance() {
        return instance;
    }

    // private 생성자
    // 접근 제어자를 private으로 줌으로써 클래스 밖에서 생성하지 못하도록 함
    private SingletonService() {

    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
  1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
  3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);
        
        // isSameAs - ==와 동일 
        // isEqualTo - equals() 메서드를 어떻게 정의하느냐에 따라 다름
        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
    }
/*
결과 - 동일한 객체 출력 됨
singletonService1 = hello.core.singleton.SingletonService@70b0b186
singletonService2 = hello.core.singleton.SingletonService@70b0b186
*/

[참고]

<aside> ❗

싱글톤 패턴 문제점


<aside> ❗

싱글톤 컨테이너

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

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

[싱글톤 컨테이너]

@Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
        ApplicationContext 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);
    }
/*
결과
memberService1 = hello.core.member.MemberServiceImpl@26ceffa8
memberService2 = hello.core.member.MemberServiceImpl@26ceffa8
*/

image.png

[참고]

스프링 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.

<aside> ❗

매우 중요 !!!!!!

싱글톤 방식 주의점

// StatefulService
package hello.core.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;
    }

}

package hello.core.singleton;

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

class StatefulServiceTest {

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

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

        // ThreadBL B 사용자 20000원 주문
        statefulService1.order("userB", 20000);

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

        // 싱글톤 패턴을 사용해 동일한 객체의 price 변수에 값을 저장하므로 마지막으로 주문한 사용자의 값이 저장됨
        Assertions.assertThat(statefulService1.getPrice()).isNotEqualTo(10000);

        // ThreadB : 사용자B가 주문 금액을 조회
        System.out.println("statefulService2.getPrice() = " + statefulService2.getPrice());
        Assertions.assertThat(statefulService2.getPrice()).isEqualTo(20000);
    }

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

    }
}
// 무상태로 설계
package hello.core.singleton;

public class StatefulService {
    public int order(String name, int price){
        System.out.println("name = " + name + " price = " + price);
        return price;
    }
}
package hello.core.singleton;

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

class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        AnnotationConfigApplicationContext 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);

        // ThreadBL B 사용자 20000원 주문
        int userBPrice = statefulService1.order("userB", 20000);

        // ThreadA : 사용자A가 주문 금액을 조회
        System.out.println("statefulService1.getPrice() = " + userAPrice);
        // 싱글톤 패턴을 사용해 동일한 객체의 price 변수에 값을 저장하므로 마지막으로 주문한 사용자의 값이 저장됨
        Assertions.assertThat(userAPrice).isEqualTo(10000);

        // ThreadB : 사용자B가 주문 금액을 조회
        System.out.println("statefulService2.getPrice() = " + userBPrice);
        Assertions.assertThat(userBPrice).isEqualTo(20000);
    }

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

    }
}

</aside>

<aside> ❗

@Configuration과 싱글톤

@Configuration
public class AppConfig {

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

    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }

    @Bean DiscountPolicy discountPolicy2() {
        return new RateDiscontPolicy();
    }

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

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy2());
    }
}
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.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

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

        // 구체 타입으로 꺼내면 안 좋지만 getMemberRepository() 메서드를 사용하기 위해 구체 타입으로 꺼낸다.
        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("memberRepository = " + memberRepository);
        System.out.println("memberRepository1 = " + memberRepository1);
        System.out.println("memberRepository2 = " + memberRepository2);  
        
        Assertions.assertThat(memberRepository1).isSameAs(memberRepository);
        Assertions.assertThat(memberRepository2).isSameAs(memberRepository);
    }
}
/*
결과 - 동일한 MemoryMemberRepository 객체를 사용 
memberRepository = hello.core.member.MemoryMemberRepository@63f259c3
memberRepository1 = hello.core.member.MemoryMemberRepository@63f259c3
memberRepository2 = hello.core.member.MemoryMemberRepository@63f259c3
*/

<aside> ❗

Configuration과 바이트 코드 조작

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

        System.out.println("been.getClass() = " + been.getClass());
    }
/*

been.getClass() = class hello.core.AppConfig$$SpringCGLIB$$0
*/

image.png

[참고]

자세한 처리 과정

** 프록시 클래스는 바이트 코드 수준에서 생성되고 런타임에만 존재하는 코드

</aside>

<aside> ❗

@Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 되는가 ??

// 테스트 결과 
// memberRepository()를 세 번 실행해 서로 다른 객체가 3개 생성됨
call AppConfig.memberRepository
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
memberRepository = hello.core.member.MemoryMemberRepository@2dc9b0f5
memberRepository1 = hello.core.member.MemoryMemberRepository@6531a794
memberRepository2 = hello.core.member.MemoryMemberRepository@3b5fad2d

</aside>

<aside> ❗

정리