[Spring] 스프링 기본편_5. 싱글톤 컨테이너

유진·2024년 7월 6일
0

스프링 기본편

목록 보기
5/9
post-thumbnail

출처 : 인프런 > 스프링 핵심 원리 - 기본편 강의를 듣고 작성한 글입니다.
강의 링크 : 스프링 핵심 원리 - 기본편

섹션 5. 싱글톤 컨테이너

📘 웹 애플리케이션과 싱글톤

웹 애플리케이션은 기본적으로 많은 고객이 동시에 요청을 한다. 그러나 위에 있는 그림처럼 요청이 들어올때마다 새로운 객체를 만들게 되면 메모리 낭비가 너무 심하므로, 1개를 생성하고 공유하도록 설계 하도록 만들어진게 싱글톤 패턴이다.

  • 스프링 없는 순수한 DI 컨테이너 테스트

test안에 singleton 패키지를 하나 만들어준다.
src\test\java\hello\core\singleton\SingletonTest.java

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class SingletonTest {

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

        // memberService1 != memberService2
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);

    }
}


memberService1, memberService2가 jvm 메모리에 계속 다른 객체가 생성돼서 올라간다. 이렇게 되면 고객이 많은 웹 애플리케이션에 효율적이지 않다.


📘 싱글톤 패턴

src\test\java\hello\core\singleton\SingletonService.java

package hello.core.singleton;

public class SingletonService {

    //1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();

    //2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance() {
        return instance;
    }

    //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {
    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }

}

  • 싱글톤 패턴을 사용하는 테스트 코드

src\test\java\hello\core\singleton\SingletonTest.java

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

public class SingletonTest {

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

        // memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);

    }

    @Test
    @DisplayName("싱글폰 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        assertThat(singletonService1).isSameAs(singletonService2);

        singletonService1.logic();
    }
}


📘 싱글톤 컨테이너

  • 스프링 컨테이너를 사용하는 테스트 코드
  @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);

        // memberService1 == memberService2
        assertThat(memberService1).isSameAs(memberService2);

    }
  • 싱글톤 컨테이너 적용 후


📘 📢 싱글톤 방식의 주의점 📢

📢 싱글톤 객체는 무상태(stateless)로 설계해야 한다!!

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

src\test\java\hello\core\singleton\StatefulServiceTest.java

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

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

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가 10000원 주문
        statefulService1.order("userA", 10000);
        // ThreadA: 사용자B 20000원 주문
        statefulService2.order("userB", 20000);

        // ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        // ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);

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

    static class TestConfig {

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

}

statefulService1과 statefulService2 모두 같은 instance를 사용하기 때문에 기대했던 결과가 나오지 않고 statefulService2에서 적용된 20000원이 나온다.


📘 @Configuration과 싱글톤

그런데 이상한점이 있다. 다음 AppConfig 코드를 보자.

src\main\java\hello\core\AppConfig.java

package hello.core;

...

@Configuration
public class AppConfig {

    // @Bean memberService -> new MemoryMemberRepository()
    // @Bean orderService -> new MemoryMemberRepository()
    // new MemoryMemberRepository() 두 번 호출!! -> 싱글톤 깨지는 거 아니야 ???

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository()); // 생성자 주입
    }

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

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

  • 검증 용도의 코드 추가

src\main\java\hello\core\member\MemberServiceImpl.java

package hello.core.member;

public class MemberServiceImpl implements MemberService {

	private final MemberRepository memberRepository;
   ...
   
    // 테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

src\main\java\hello\core\order\OrderServiceImpl.java

package hello.core.order;

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    ...
	
    // 테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다. 기능 검증을 위해 잠깐 사용하는 것이니 인터페이스에 조회기능까지 추가하지는 말자.

  • 테스트 코드

src\test\java\hello\core\singleton\SingletonService.java

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.assertj.core.api.Assertions;
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);

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

        //모두 같은 인스턴스를 참고하고 있다.
        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}

  • AppConfig에 호출 로그 남김

src\main\java\hello\core\AppConfig.java

package hello.core;

...

@Configuration
public class AppConfig {

   ...

    // method 실행 순서는 보장하지 않음
    // call AppConfig.memberService
    // call AppConfig.memberRepository
    // call AppConfig.memberRepository
    // call AppConfig.orderService
    // call AppConfig.memberRepository

    @Bean
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository()); // 생성자 주입
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

   ...
}


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

src\test\java\hello\core\singleton\ConfigurationSingletonTest.java

package hello.core.singleton;

...

public class ConfigurationSingletonTest {

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

        System.out.println("bean = " + bean.getClass());
        // 출력 : bean = class hello.core.AppConfig$$SpringCGLIB$$0
    }
}

profile
유진진입니덩

0개의 댓글