본문 바로가기

Development/Spring

[Spring] 싱글톤 패턴과 싱글톤 컨테이너

 

싱글톤 패턴이란?

하나의 서비스에 대해 여러 건의 요청이 들어오더라도 해당 인스턴스를 1개만 생성하여 해당 서비스 요청들을 생성된 1개의 인스턴스로 처리하는 것이다. 

 

웹 애플리케이션 상에서 클라이언트로부터 서비스 요청을 받게 되면 요청을 받은 인스턴스를 지속적으로 생성한다. 예를 들어 서비스 요청을 100번 받으면 100개의 인스턴스가 생성되고 요청을 1번 받으면 1개의 인스턴스가 생성된다. 이렇게 요청받은 수만큼 인스턴스를 생성하게 되면 메모리도 그만큼 많이 차지하게 된다. 이렇게 메모리를 하게 되면 자연스레 서버에도 부하가 생기게 된다.

 

이러한 문제점을 해결하기 위해서 싱글톤 패턴을 통해서 서비스 요청이 여러 번 들어오더라도 하나의 인스턴스로 처리하고자 한다.

 

아래 코드를 통해 싱글톤이 적용되지 않은 상황을 확인해 보자

public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
      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);
      }
}

결과

싱글톤을 적용하지 않을 경우 위에 코드의 결과처럼 각각의 다른 인스턴스가 클라이언트가 요청한 횟수만큼 생성되는 것을 알 수가 있다.(예시 코드에서는 2번 요청)

 

이번에는 싱글톤 패턴이 적용된 코드를 확인해 보자

public class SingletonService {

    //1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();

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

    //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다. 
    private SingletonService() {
    }
}
public void singletonServiceTest() {

    //1. 조회: 호출할 때 마다 같은 객체를 반환
    SingletonService singletonService1 = SingletonService.getInstance();

    //2. 조회: 호출할 때 마다 같은 객체를 반환
    SingletonService singletonService2 = SingletonService.getInstance();

    //참조값이 같은 것을 확인
    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);

    // singletonService1 == singletonService2
    assertThat(singletonService1).isSameAs(singletonService2);
}

결과

static을 선언하고 생성자를 private을 적용해서 외부에서 인스턴스 생성을 막은 싱글톤 방식의 예제이다. 해당 코드로 테스트를 진행해 본 결과 그 전과 다르게 2번의 클라이언트 호출 상황에서 동일한 1개의 인스턴스가 호출된 것을 확인할 수가 있다.

 

이처럼 싱글톤 패턴을 적용하면 클라이언트에서 요청이 올 때마다 이미 만들어진 인스턴스를 이용해 효율적으로 사용할 수가 있다. 하지만 이러한 싱글톤 패턴에도 문제점이 존재한다.

 

싱글톤 패턴의 문제점

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

이처럼 싱글톤 패턴에도 문제점이 존재함에 따라 스프링에서는 이러한 싱글톤 패턴을 어떻게 해결하는지 살펴보도록 하자

 

스프링에서 싱글톤 컨테이너 구현

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }

}
void springContainer() {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    //1. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);

     //2. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);

    //참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);

    //memberService1 == memberService2
    assertThat(memberService1).isSameAs(memberService2);
  }

결과

이번에는 스프링 컨테이너를 통해서 memberService 빈을 2번 호출한 경우인데, 결괏값에서도 볼 수 있듯이 동일한 인스턴스가 두 번 호출된 것을 확인할 수가 있다.

 

이처럼 스프링에서 싱글톤 컨테이너를 통해서 확인할 수 있는 특징은 아래와 같다.

1. 따로 코드 자체를 작성하지 않아도 선언된 빈을 통해서 싱글톤 구현

2. 클라이언트의 요청 횟수와 상관 없이 인스턴스 1개 생성

 

싱글톤 방식의 문제점

싱글톤 방식은 하나의 인스턴스에 대해서 여러 클라이언트가 공유하여 사용하기 때문에 싱글톤 인스턴스는 상태를 유지하게 설계하면 안 되며, 아래 내용을 참고하여 작성해야 된다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.
  • 읽기만 가능해야 된다.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, 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;
    } 
}
@Test
void statefulServiceSingletoe() {

    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    StatefulService statefulService1 = ac.getBean(StatefulService.class);
    StatefulService statefulService2 = ac.getBean(StatefulService.class);

    // ThreadA : A사용자 10,000원 주문
    int userAPrice = statefulService1.order("userA", 10000);
    // ThreadB : B사용자 20,000원 주문
    int userBPrice = statefulService2.order("userB", 20000);

    System.out.println("pirce = " + userAPrice);

}

static class TestConfig {

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

결과

해당 테스트에서 A사용자는 10,000원을 주문하고, B사용자는 20,000을 주문했다. 하지만 결과적으로 출력한 A사용자의 금액은 20,000원이 저장되었다. 이러한 문제가 발생한 이유는 A사용자와 B사용자가 사용하는 싱글톤으로 사용되는 인스턴스가 공유되는 상태에서 B사용자가 공유되는 금액을 20,000으로 바꾸었기 때문이다.

 

이러한 경우에는 위에서 공유되는 price를 제거하고 order 메소드에서 price를 반환하면 된다. 아래 코드를 참조하여 위에 StatefulService와 비교해 보자

public class StatefulService {

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

}

결과

StatefulService order메서드에서 price를 반환하도록 수정하고 동일한 테스트를 진행하니 기대하던 결괏값이 나오는 것을 확인할 수가 있다.

 

출처 : 인프런 우아한 형제들 최연소 기술이사 김영한의 스프링 완전 정복(스프링 핵심원리 - 기본 편)