싱글톤 패턴

손성우·2021년 7월 13일
0

디자인 패턴

목록 보기
1/1

주 내용은 자바위주로 정리가 되어있습니다.

싱글톤 패턴

클래스의 인스턴스가 딱 1개만 생성하는 디자인 패턴.

사용하는 이유

  1. 메모리 낭비 방지
public class Test {

 void Test() {
 AppConfig appConfig = new AppConfig();
 
 MemberService memberService1 = appConfig.memberService(); 
 MemberService memberService2 = appConfig.memberService();
 
 }
}

위 코드처럼 서비스 요청이 있을 때마다 객체가 생성된다고 하자.
요청 100개가 있으면 객체를 100번 생성해야하는 메모리의 낭비가 발생하는데 이를 방지함.

  1. 다른 클래스간에 데이터 공유가 쉽다.
    싱글톤 인스턴스가 전역으로 사용되는 인스턴스이기 때문에 다른 클래스의 인스턴스들이 접근하여 사용할 수 있다.

구현 방법

여러 방법이 있으나 가장 대표적인 holder 방법으로 구현한다.

  1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
  3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
  4. holder안에 선언된 instance가 static이기 때문에 클래스 로딩시점에 한번만 호출될 것이며 final을 사용해 다시 값이 할당되지 않도록 만든 방법.
    public class ExampleClass {

    // 외부에서 생성자에 접근하지 못하게.
    private ExampleClass() {}

    // 
    private static class InnerClass() {
        private static final ExampleClass instance = new ExampleClass();
    }

    public static ExampleClass getInstance() {
        return InnerClass.instance;
    }
}



문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.

스프링에서는 이러한 싱글톤의 단점들을 해결하면서 장점을 취할 수 있는 스프링 컨테이너를 제공한다.

주의점

싱글톤 패턴이든, 스프링과 같은 싱글톤 컨테이너를 사용하든, 하나의 인스턴스를 공유하기 때문에 상태를 유지(stateful)하지 않고 무상태(stateless)로 설계해야 한다.

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

문제 코드(공유필드가 있을 경우)

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(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

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

        int price = statefulService1.getPrice();
        System.out.println("priceA = " + price);

    }

    static class TestConfig {

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

priceA = 20000이 출력되는 에러가 발생.

개선 코드

public class StatefulService {

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

    public int order(String name, int price){
        System.out.println("name = " + name + " price = " + price);      
        return price;
    }
    
}

class StatefulServiceTest {

    @Test
    void statefulServiceSingleton(){
    
        AnnotationConfigApplicationContext 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 = statefulService1.order("userB", 20000);
     
        Assertions.assertThat(userAPrice).isEqualTo(10000);
        Assertions.assertThat(userBPrice).isEqualTo(20000);

    }

    static class TestConfig {

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

스프링 컨테이너

스프링 패턴을 적용하지 않아도, 인스턴스를 싱글톤으로 관리해준다.

@Configuration
public class AppConfig {

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

    // new DiscountPolicy가 중복되지 않게
    @Bean
    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

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

}

AppConfig.class라는 빈 관련 설정파일에서 sout를 통해 알아보자. 위 코드 상으로는 memberRepository()가 3번 호출되므로 System.out.println("call AppConfig.memberRepository");도 3번 실행되지만 결과는?

call AppConfig.memberRepository
23:08:07.772 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
23:08:07.776 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
call AppConfig.memberService
23:08:07.779 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService

이렇게 1번만 호출되는 것을 확인할 수가 있는데 이것이 가능한 이유는 CGLIB라는 바이트코드 조작 라이브러리 덕분이다.

CGLIB, @Configuration

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다. 위 자바 코드를 보면 분명 3번 호출되어야 하는 것이 맞다. 그래서 스프링은 클래스의 바이트코드를 조작하는 CGLIB라는 라이브러리를 사용한다.

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

        System.out.println(bean.getClass());        
    }

위 코드를 실행하면 class com.example.basic.AppConfigEnhancerBySpringCGLIBEnhancerBySpringCGLIB335108b9라고 뜬다.
순수한 클래스라면 class hello.core.AppConfig라고 떠야하는데 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB를 이용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

AppConfig@CGLIB 예상 코드

중요한것은 @Configuration 어노테이션을 사용해야만 CGLIB가 작동해서 싱글톤을 보장한다는것! @Bean만 사용해도 스프링빈으로 등록은 된다 하지만, 싱글톤을 보장하지 않고 생성되는 인스턴스도 모두 다르다.

출처 : 김영한의 스프링 핵심 원리 -기본편

profile
백엔드 개발자를 꿈꾸며 공부한 내용을 기록하고 있습니다.

0개의 댓글