BACK/SPRING

[Spring] 스프링 핵심 원리 - 싱글톤 컨테이너

연듀 2024. 1. 30. 17:57

 

스프링 없는 순수한 DI 컨테이너 AppConfig는 고객이 요청하는 만큼 객체를 생성해 반환 - 메모리 낭비

 

 

싱글톤: 클래스의 인스턴스가 딱 1개만 생성되는 디자인 패턴

private 생성자를 사용해 외부에서 임의로 new 키워드를 사용하지 못하도록 함

 

 

public class SingletonService {
    private static final SingletonService instance = new SingletonService();
    // JVM이 맨처음 실행할 때 static 영역을 초기화 하면서 딱 한번 객체를 하나 생성

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

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

    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}
public class SingletonTest {
		// new SingletonService() -> private 생성자 호출해 컴파일 오류 

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        MemberService memberService1 = appConfig.memberService();
       	MemberService memberService2 = appConfig.memberService();

        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + 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);
        // same : == 비교 (참조값비교)
        // equal : equal 비교
    }
}

 

 

문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어감
  • 클라이언트가 구체 클래스에 의존함(구체클래스 .getInstance()로 접근) → DIP, OCP 위반
  • 테스트, 내부 속성 변경, private 생성자로 자식 클래스를 만들기 어렵다
  • 유연성이 떨어짐

 


싱글톤 컨테이너

 

스프링 컨테이너는 위의 문제점을 해결하며 객체를 싱글톤으로 관리한다.

클라이언트의 코드를 고칠 필요가 전혀 없다.

 

  • 컨테이너는 객체를 하나만 생성해서 관리
  • 스프링 컨테이너 = 싱글톤 컨테이너 역할

 

고객의 요청이 올때마다 이미 만들어진 객체를 공유해서 재사용

 

 


싱글톤 방식의 주의점

 

하나의 객체를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.

특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.

같은 객체이므로 값이 덮어 씌워질 수 있기 때문이다. 

 

무상태(stateless)로 설계해야한다.

 

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

 

class StatefulServiceTest {

    @Test
    void statefulServiceSingleton(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);
        // 둘은 같은 객체
        statefulService1.order("userA", 10000);
        statefulService2.order("userB", 20000);

        int price = statefulService1.getPrice();

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000); // test 통과

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

 

 

해당 코드에서 스레드A가 userA, 스레드B가 userB의 코드를 호출한다 가정

같은 객체이므로 값이 덮어 씌워짐

공유 필드 price를 공유하기 때문에 값이 덮어씌워져 버린다.

 

 

💡 공유필드 조심

 

 

 


@Configuration

 

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

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

 

@Configuration을 붙이면

스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서

AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한다. 

 

 

 

이 임의의 클래스에서는

@Bean 이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고

없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 만들어진다.

⇒ 싱글톤 보장

 

@Configuration을 제거하면 순수한 AppConfig로 스프링 빈에 등록되어 싱글톤이 적용되지 않는다.

 

 

 

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