AR삽질러

Spring Singleton, SingletionContainer (11) 본문

JAVA/Spring

Spring Singleton, SingletionContainer (11)

아랑팡팡 2023. 8. 5. 19:13
728x90

 

Spring Singleton

 - 특정 클래스의 인스턴스가 하나만 생성되도록 보장하는 디자인패턴이다. 예를 들어 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 매번 새로운 연결을 생성하면 리소스를 많이 사용하게 되지만 싱글톤 패턴을 사용하여 하나의 연결 객체를 공유하면 리소스를 효율적으로 사용할 수 있다.

 

Spring Singleton Container

 - Bean 애플리케이션의 객체를 관리하고 처음 요청될 때 생서되며 동일한 인스턴스가 재사용된다.

특징

1) 인스턴스관리 : Bean의 생명주기를 관리, 생성, 초기화, 소멸등

2) 의존성주입 : Singleton Container는 빈간의 의존성을 관리하며 필요시 빈을 자동으로 주입할 수 있다.

3) 설정과 커스터마이징 : XML, JavaConfig, Annotation등 다양한 방법으로 빈을 설정하고 커스터마이징 할 수 있게 한다.

*커스터마이징? : 재품/서비스 등 사용자의 특정 요구사항이나 선호도에 맞추어 변경 또는 조정하는 과정

 

순수DI컨테이너

 - DI(Dependency Injection) Container인 'AppConfig' 클래스를 사용해서 'MemberService;의 인스턴스를 두번생성한다.

public class SingletonTest {

    @Test
    @DisplayName("Spring x 순수DI Container")
    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);
    }
}

 

문제점

1. 메모리의 낭비 -> 2. 성능저하 -> 3. 공유상태관리의 어려움

'memberService1' 과 'memberService2' 는 서로 다른 인스턴스로 각 호출시 새로운 객체가 생성된다. 만약 객체가 더 많아 진다면 요청이 많이 발생하고 이로 인해 메모리 낭비가 발생한다.

 

Singleton 패턴을 통한 해결 방안

1. 메모리 효휼성 -> 2. 성능향상 -> 3. 상태공유용이

클래스의 인스턴스를 하나만 생성하고 이를 재사용하므로써 메모리의 낭비를 방지하고 성능이 향상된다.

동일한 인스턴스를 사용하므로 필요한 경우 상태를 쉽게 공유할 수 있다.

 

Singleton patten

 - 클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인패턴

 * 객체 인스턴스를 2개 이상 생성하지 못하도록 해야한다. (private 생성자를 이용해 외부에서 임의로 new 키워드를 사용하지 못하도록 한다.)

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }

    private SingletonService(){

    }

    public void logic(){
        System.out.println("Singleton 객체 로직 호출");
    }
}
public class SingletonTest {

    @Test
    @DisplayName("Spring x 순수DI Container")
    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("Singleton 패턴을 적용한 객체 사용")
    void singletonServiceTest(){
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

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

 

1) 인스턴스의 유일성 보장

'SingletonService' 클래스는 자기 자신의 'static'필드로 인스턴스를 하나만 가지게 되어 'getInstance()' 메서드 호출을 통해 항상 같은 인스턴스를 반환하게 된다.

2) 리소스 절약

싱글톤 패턴을 사용하므로 하나의 인스턴스만 생성하고 재사용하기 때문에 리소르를 절약한다.

3) 동일한 상태 유지

4) 접근제어

 

 싱글톤 패턴은 특정 클래스의 인스턴스가 하나만 필요한 경우 적합하지만 싱글톤 패턴을 구현하는 코드가 많거나 DIP위반, 테스트가 어려울 수 있기 때문에 싱글톤 컨데이너로 발전시켜보자.

 

Singleton Container

 Spring Container는 Singleton Bean들을 관리하여 애플리케이션 전반에 걸쳐 빈의 인스턴스를 하나만 생성하고 재사용한다. 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리해 컨테이너에서 객체를 하나만 생성해서 곤리한다.

 Spring Container는 Singleton Container역할을 하는데 이것을 싱글톤 레지스트리라고 한다.

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

        //AppConfig appConfig = new AppConfig();
        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);
    }

 

1. Spring Container 생성 : 'ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); 에서 스프링 컨테이너를 초기화하고 AppConfig.class를 통해 설정 정보를 로드한다.

2. 빈조회 : ac.getBean("memberService", MemberService.class)를 두번 조회하더라도 동일한 인스턴스가 반환된다.

3. 테스트 검증 : assertThat(memberService1). isSameAs(memberService2)를 통해 두 빈이 동일한 인스턴스인지 테스르 한다.

 

이 예제를 통해 이전 싱글톤 객체를 스프링 컨테이너가 대신 관리하여 개발자는 빈의 생명주기나 의존성 관리에 대해 복잡성을 줄이고 로직에만 집중할 수 있게된다.

 

Singleton 방식의 주의점

 - 싱글톤 패턴, 싱글톤 컨테이너 등 객체 인스턴스 하나만 생성해서 공유하는 싱글톤 방식은 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체를 상태유지하게 설계하면 안되고 

무상태로 설계해야한다.

 - 특정 클라이언트에 의존적인 플드가 있으면 안된다.

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

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

        // TreadA : A 주문금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

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

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

}

 

1. StatefulService는 price라는 상태를 가지고 있고 이 상태는 order메서드를 호출할 때마다 변경된다.

2. 테스트에서 동일한 StatefulService 빈을 두번 조회하고 주문을 진행하면 두번째 주문에서 가격이 업데이트 되면서 첫 번째 주문의 가격 정보가 덮어씌워지기 때문에 statefulService1.getPrice()를 호출하면 10000이 아닌 20000이 반환된다.

 

해결방법

 - 스프링 빈은 무상태(stateless)로 설계해야한다. 상태를 가진 필드는 피하고 로컬변수, 파라미터, 반환값등을 사용해 메서드 내에서 처리해야한다.

 

public class StatefulService {

    // private int price; // 가격 상태를 유지하는 필드

    public int order(String name, int price){
        System.out.println("name = " + name + " price = " + price);
    //    this.price = price;
        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);

        int userAPrice = statefulService1.order("userA", 10000);
        int userBPrice = statefulService2.order("userB", 20000);

        // TreadA : A 주문금액 조회
        // int price = statefulService1.getPrice();
        System.out.println("price = " + userAPrice);

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

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

}

 

수정된 스프링컨테이너

1. StatefulService 클래으싀 price필드를 제거하고 무상태(stateless)가 되었다.

2. order메서드는 주문 가격을 직접반환하게 되고 userAPrice, userBPrice에 저장하여 필요한 작업을 수행한다.

 

728x90
반응형
LIST