반응형

싱글톤 컨테이너



 

스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.

대부분 스프링앱? => 웹 애플리케이션

웹 애플리케이션? => 대부분 여러 고객이 동시에 요청함

 

스프링이 없는 순수 DI 코드는 memberService를 요청할때 마다 객체를 각각 생성해서 새로 만들어주게된다.

코드로 보자.

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();
        //두 객체 비교
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }
}

매번 새로운 객체를 만드니까 메모리 낭비도 된다.

초당 만건의 요청이 들어오면 멤버 서비스 객체가 초당 만개가 생성된다....

따라서 딱 하나의 객체만 만들고 서로 공유하도록 설계하면 된다. => 이것이 싱글톤 패턴

 

 

- 싱글톤 패턴


클래스의 인스턴스가 딱 하나만 생성되도록 보장하는 디자인 패턴!

따라서 객체 인스턴스가 2개 이상 생성되지 못하게 막아야함!!!

How?? => private 생성자로 외부에서 객체를 생성하지 못하게 막아버림, 외부에서 new로 생성 불가!

 

싱글톤 패턴 예제

package hello.core.singleton;

public class SingletonService {
    //How? => 생성자를 private로 막아버림
    
    
    //먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
    private static final SingletonService instance = new SingletonService();
    
    //객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
    public static SingletonService getInstance(){
        return instance;
    }
    
    //이제 생성자를 막아보자 private로
    private SingletonService(){
        
    }
    
    //로직 테스트 위해서
    public void logic(){
        System.out.println("싱글톤 객체 호출됨!!");
    }
    
}

 

이제 테스트!!

package hello.core.singleton;

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 SingletonService {
    //How? => 생성자를 private로 막아버림


    //먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
    private static final SingletonService instance = new SingletonService();

    //객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
    public static SingletonService getInstance(){
        return instance;
    }

    //이제 생성자를 막아보자 private로
    private SingletonService(){

    }

    //로직 테스트 위해서
    public void logic(){
        System.out.println("싱글톤 객체 호출됨!!");
    }



    @Test
    @DisplayName("싱글톤 패턴이 적용된 객체 사용")
    public void singletonServiceTest(){
        //new SingletonService() => private로 막혀버렷쥬?

        //두번 호출 해본다.
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        //이제 테스트 무엇을? 참조값이 같은지
        Assertions.assertThat(singletonService1).isSameAs(singletonService2);


    }


}

 

 

싱글톤 패턴을 구현하는 방법은 여러방법들이 있다. 여기선 객체를 미리 생성해두는 단순하고 안전한 방법을 썻다.

 

하지만 싱글톤은 여러 단점들이 있다.

  1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. => 접근 못하게 막고, 미리 올려두고 하는 과정이 생겨버림
  2. 의존 관계상 클라이언트가 구체 클래스에 의존 -> DIP위반 ==>SingletonService.getInstance();로 꺼내버림 구체클래스 꺼냄
  3. DIP위반햇으니 OCP 위반할 가능성 많아짐
  4. 테스트하기 어려움 => 유연하지가 않게됨
  5. 내부 속성을 변경하거나 초기화 하기 어렵움
  6. private생성자이므로 자식 클래스를 만들기 어려움

위 단점들때문에 안티패턴이라고도 불림

 

하지만.... 스프링을 쓰면 스프링이 알아서 해결해줌... how?

밑에서 보자.

 

싱글톤 컨테이너


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

(이때 까지 햇던 스프링 빈이 싱글톤으로 관리 되었다.)

 

싱글톤 컨테이너

  • 싱글톤 패턴을 적용안해도 스프링 컨테이너가 다 알아서 해줌 => 객체 인스턴스를 싱글톤으로 관리함
  • 즉, 스프링 컨테이너는 싱글톤 컨테이너 역할을 함
  • 싱글톤 객체를 생성하고 관리하는 기능 => 싱글톤 레지스트리
  • 따라서 스프링을 사용하면 싱글톤 패턴의 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
  • 싱글톤 패턴을 위한 지저분한 코드가 없어짐
  • DIP, OCP, 테스트, private 생성자로부터 자유로워짐!! 호우!

스프링 컨테이너를 사용하는 테스트 코드를 보자

package hello.core.singleton;

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

public class SingletonTest {

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

        //1. 조회
        MemberService memberService1 = appConfig.memberService();
        //2. 조회
        MemberService memberService2 = appConfig.memberService();
        //두 객체 비교
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }


//=> 스프링 컨테이너를 사용!!!
    @Test
    @DisplayName("스프링컨테이너와 싱글톤")
    void springContainer(){

        //스프링 컨테이너
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        //1. 조회
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        //2. 조회
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);
        //두 객체 비교
        Assertions.assertThat(memberService1).isSameAs(memberService2);
        //Same => 참조값 비교
        //Equal => 값 비교
    }
}

 

멤버 서비스 객체를 공유하게 됨!!

 

하지만 이러한 싱글톤 방식도 문제가 있음....ㅠ

 

싱글톤 방식의 주의점


객체 인스턴스를 하나만 생성해서 공유하므로, 객체는 상태를 유지하면 안됨!!!

(운영체제 시간의 동시성 문제를 생각해보자..)

 

즉, 무상태로 설계해야한다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다!!
  • 즉, 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.!!!
  • 따라서 가급적 읽기만 해야됨
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreaLocal등을 사용해야한다.

 

상태를 유지해서 발생하는 문제점 - 예시

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

        //각각 price 입력해버림 = TheadA
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        statefulService1.order("userA", 10000);

        //각각 price 입력해버림 = ThreadB
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        statefulService1.order("userB", 20000);

        int price = statefulService1.getPrice();

        //뒤에서 값이 바뀌니까 같지 않을 것이다.
        Assertions.assertThat(price).isEqualTo(20000);



    }




    static class TestConfig{
        //빈 등록
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }

}

 

 

각각 Thead 2개라 가정(Order 하는 경우)

price필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경해버리게 된다.

 

싱글톤은 이런 문제가있다....

해결법으론 private int price를 사용하지 않고, int order로 price를 반환하게 해버린다.

즉 price를 지역 변수로 만들어버린다.

 

package hello.core.singleton;

public class StatefulService {
    //private int price;// 상태를 유지함, 즉, 값을 가지고 있음

    //값을 반환해서 가지고 있지 않게됨
    public int order(String name, int price){
        System.out.println("name = " + name + "price = " + price);
        return price; // 

    }
/*
    public int getPrice(){
        return price;
    }
    */
}

package hello.core.singleton;

import hello.core.AppConfig;
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);

        //각각 price 입력해버림 = TheadA
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        int userAPrice = statefulService1.order("userA", 10000);

        //각각 price 입력해버림 = ThreadB
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        int userBPrice =statefulService1.order("userB", 20000);

        //int price = statefulService1.getPrice();

        //뒤에서 값이 바뀌니까 같지 않을 것이다.
        Assertions.assertThat(userAPrice).isEqualTo(10000);



    }




    static class TestConfig{
        //빈 등록
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }

}

 

 

728x90
반응형
블로그 이미지

아상관없어

,