월루를 꿈꾸는 대학생

[핵심원리-기본] 싱글톤 컨테이너 본문

Programing/Spring Boot

[핵심원리-기본] 싱글톤 컨테이너

하즈시 2023. 7. 18. 21:06
728x90

웹 애플리케이션과 싱글톤

  • 스프링은 기업용 서비스 기술 목적이며 이런 경우 보통 여러 고객이 동시에 요청을한다

  • AppConfig는 new해서 새로 만들어서 반환하지 그래서 클라이언트가 요청할 때마다 생성해서 반환해서 주는 거임.. 무수한 인스턴스가 생성이 된다!

  • 메모리 낭비가 심하다

    호출마다 서로 다른 객체가 생성되므로 효율적이지 않음
    그래서 딱 1개의 객체만 생성되고 공유하도록 설계를 해야한다 =>싱글톤 패턴


싱글톤 패턴

인스턴스 1개만 생성하고 쓰는 것
2개 이상 못 만들도록 막아야함
-> private 생성자를 통해서 외부에서 맘대로 new 키워드 못 사용하도록 막아야함!!!

public class SingletonService {  
    // 자기 자신을 private로 선언한 후에 new로 인스턴스 생성  
    // static : 클래스 레벨로 올라가니까 1개만 올라감 -> 자바 실행시에 static 친구들은 내부적으로 메모리 올라감 (즉 미리 만들어짐)  
    private  static final SingletonService instance = new SingletonService();  

    //만들어진 인스턴스 반환해서 사용할 수 있도록  
    public static SingletonService getInstance(){  
        return instance;  
    }  

    // private로 선언해서 다른 코드에서 new 못쓰도록 막음  
    private SingletonService(){  
    }  

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

}
  • static으로 변수 선언해서 자바 실행시 싱글톤 인스턴스 1개가 만들어져서 내부 메모리에 올라가도록
  • getInstance로만 참조할 수 있도록하고 생성자에 private붙여서 외부에서 생성 막음
  • 같은 인스턴스가 나옴을 확인

이렇게 싱글톤 적용을 위해선 생성자부터 다시 싹 만들어야 하는데 스프링을 사용하면 그냥 스프링 임마가 알아서 싱글톤으로 만들어준다고 함 ;;

싱글톤 패턴 문제점

  1. 구현 코드에 공수가 더 들어간다
  2. 의존관계상 클라이언트가 구체 클래스 의존 -> DIP위반 getInstance이렇게 써야함;;
  3. 클라이언트가 구체 클래스에 의존 OCP의존
  4. 테스트 어렵
  5. private 생성자 쓰니까 자식 클래스 만들기 어렵다

싱글톤 컨테이너

스프링 컨테이너

  • 자동으로 객체 인스턴스를 싱글톤으로 관리
  • 스프링 컨테이너 = 싱글톤 컨테이너
  • 싱글톤 객체 생성 관리 기능 = 싱글톤 레지스트리
  • 스프링은 자동으로 이런 관리를 해주니까 싱글톤 패턴의 단점을 해결하면서 객체를 싱글톤 유지 가능
  • 싱글톤 패턴을 위해서 코드 공수 추가 x
  • DIP,OCP,private 등등 자유롭게 싱글톤 사용

@Test  
@DisplayName("스프링 컨테이너와 테스트")  
void springContainer(){  
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);  
    // 스프링 컨테이너가 알아서 싱글톤으로 만들어서 반환해줌 즉 memberService1,memberService2는 같은 객체 참조  
    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);  
}

고객이 요청할 때마다 객체 생성이 아니라 이미 만들어준 객체를 공유해서 효율적인 사용 가능

보통은 싱글톤으로 사용한다


싱글톤 방식의 주의점

  • 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하니까 절대로 상태를 유지하게 설계하면 안 된다 -> stateless 설계가 필요
  • 무상태 설계가 필요
    • 특정 클라이언트에 의존적 필드 x -> 수정 x , 읽기만
    • 필드 대신에 자바에서 공유되지 않는 지역변수 , 파라미터 , ThreadLocal 등 사용해야함
  • 스프링 빈에 공유 값 설정하면 망함;
public class StatefullService {  
    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;  
    }  
}
  • 여기서 보면 this.price로 변수를 가지고 있음 이 변수는 싱글톤 사용할 때 망하는 지름길 여러사람이 값을 바꿀 수 있으니까

테스트

@Test  
void statefulServiceSingleton(){  
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);  
    StatefullService statefullService1 = ac.getBean(StatefullService.class);  
    StatefullService statefullService2 = ac.getBean(StatefullService.class);  

    //ThreadA: 사용자A 10000원 주문  
    statefullService1.order("userA",10000);  
    //ThreadB: 사용자B 20000주문  
    statefullService2.order("userB",20000);  

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

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

이런 경우 출력으로 20000이 나옴
왜냐하면 이미 statefullService2가 같은 인스턴스를 참조해서 값을 바꿨기 때문에
이런 문제가 생길 수 있으니까 무상태 stateless 설계를 해야한다


이런 식으로 무상태로 상태를 저장하지 않도록 설계 하는 것이 중요


@Configuration 과 싱글톤

참고
https://www.inflearn.com/questions/609357/comment/202743

appconfig에 static으로 되어 있으면 싱글톤 적용 안됨!!

@Configuration  // 어노테이션 추가  구성정보를 담당한다는 뜻  
public class AppConfig {  
    @Bean  
    public MemberService memberService(){  
        return new MemberServiceImpl(memberRepository());  
    }  
    @Bean  
    private static MemoryMemberRepository memberRepository() {  
        return new MemoryMemberRepository();  
    }  
    @Bean  
    public OrderService orderService(){  
        return new OrderServiceImpl(memberRepository(), discountPolicy());  
    }  
    @Bean  
    public DiscountPolicy discountPolicy(){  
        //return new FixDiscountPolicy();  
        return new RateDiscountPolicy();  
    }  

}

// @Bean memberService -> new MemoryMemberRepository()
// @Bean orderService -> new MemoryMemberRepository()
보면 첨 구성파일을 스프링 컨테이너에 올릴 때 MemoryMemberRepository를 두 번 생성하게 될텐데 이러면 싱글톤이 깨지는 것이 아닌가??

@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 : " + memberRepository1);  
    System.out.println("orderService : " + memberRepository2);  
    System.out.println("memberRepository : " + memberRepository);  

    Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);  
    Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);  
}
  • 확인해보면 모두 같은 인스턴스를 공유해서 쓰는 싱글톤으로 되어 있음을 확인 가능
  • AppConfig에서는 분명 2번씩 호출이 될텐데 어떻게 하나로 공유를 하는 것인가 .... 밑에 또 테스트 해봐야함

실제 테스트 결과
call memberRepository
call orderService
call discountPolicy

보면 딱 한 번만 실행해서 싱글톤을 보장해줌 !!!


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

스프링 컨테이너 = 싱글톤 레지스터 = 싱글톤을 보장
원래라면 분명 memberRepository가 3번 호출이 되어야하는데 1번만 호출 됨 그 비밀은 @Configuration임

@Test  
void configurationDeep(){  
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);  
    // AppConfig도 스프링 빈으로 등록이 됨  
    AppConfig bean = ac.getBean(AppConfig.class);  
    System.out.println("bean : "+bean.getClass());  
    // 출력결과 : bena : class hello.core.AppConfig$$EnhancerBySpringCGLIB$$4a5fc277}

보면 출력결과가 AppConfig에서 끝나는게 아니라 머 이상한거 더 붙어 있음
hello.core.AppConfig==$ $ EnhancerBySpringCGLIB$ $4a5fc277==

즉 내가 만든 클래스가 올라간게 아님.. 내가 만든 클래스 이름은 AppConfig니까
무슨 CGLIB 가 붙어 있음 저건 스프링이 CGLIB 바이트 코드 조작 라이브러리를 사용해서 내가 만들어둔 AppConfig를 상속한 새로운 클래스를 만들어서 그 클래스를 빈에 등록 시킨거다!!

즉 내가 만든게 아니라 한 번 수정을 거친 클래스가 올라갔고 이 수정을 한 클래스에서 내부적으로 싱글톤을 유지하는 코드를 추가해서 싱글톤을 유지 시킨 것

  • @Bean 메서드마다 스프링 빈에 이미 존재하는지 유무 체크해서 없으면 생성후 빈 등록 있으면 등록된 인스턴스 생성해서 반환 이런식으로 싱글톤 보장

@Configuration 적용 안 하고 @Bean만 적용한다면?
-> @Configuration 을 써야 바이트 코드 조작 라이브러리를 써서 싱글톤을 보장해줬는데 이거 없으면 싱글톤 깨짐

@Bean만 써도 스프링 빈 등록해서 쓸 수 있지만 싱글톤은 보장하지 않는다

웬만하면 그냥 설정 정보에는 @Configuration 사용하자

출처
https://inf.run/wFfL

728x90