들어가며

  • 이전 포스트에서는 동일한 요청에 대해 새로운 객체를 계속해서 만들어 반환해야 하는 경우의 문제점에 대해 알아보고, 이러한 문제를 해결할 수 있는 디자인 패턴인 싱글톤 패턴에 대해 알아보았다.
  • 또한 싱글톤 패턴이 가져오는 장점과 단점 역시 알아보았다.
  • 이번 포스트에서는 스프링 프레임워크에서 이러한 문제에 대해 어떻게 대처하는지에 대해 알아본다.
 

Spring Framework - 스프링과 Singleton - 1. Singleton

들어가며 이전 글에서는 스프링 프레임워크에서 생성하고 관리하는 스프링 DI 컨테이너(이하 스프링 컨테이너)가 어떻게 Bean을 조회하고, 어떠한 문제점이 발생할 수 있으며 그것을 어떻게 해결

sehun5515.tistory.com

 

 

 

싱글톤 컨테이너

  • 우리가 이전 포스트에서 확인했던 것은 스프링 프레임워크가 없는 상황에서 싱글톤 패턴을 이용하여 객체를 관리해야할 경우 어떤 문제가 발생할 수 있는지에 대해 싱글톤 패턴의 단점으로 알아보았다.
  • 스프링 프레임워크도 동일하게 싱글톤 패턴과 동일한 방식을 따르는데 조금 다른 부분들이 존재한다.
  • 스프링 컨테이너는 싱글톤 패턴에서 발생하는 문제점을 해결하면서 객체 인스턴트(스프링 빈)을 싱글톤으로 관리한다. 그런데 내부적으로 싱글톤 패턴 자체를 적용하는게 아닌 "단 한번만 실행시켜 반환받는" 방식으로 싱글톤 패턴을 구현하였다.
    • 이는 이전 스프링 컨테이너에서 어떻게 스프링 빈(Bean)을 등록하는가에 대해 기억하고 있다면 자연스럽게 이해되는 부분이다.
    • 스프링 컨테이너가 실행되면 AppConfig 등의 @Configuration이 들어간 설정 파일을 읽어들인다. 이후 @Bean 어노테이션이 붙은 메서드들을 "한 번씩" 실행시켜 메서드의 이름을 key로 메서드를 실행하고 반환받은 객체를 value로 하여 관리한다.
  • 한 번씩 실행시키기 때문에 인스턴스는 하나씩만 생성된다. 이후 시점에서 스프링 컨테이너가 새롭게 빈을 등록하거나 하는 일들은 일어나지 않기 때문에, 인스턴스들이 컨테이너 내부에서 하나만 존재하는게 보장된다.
  • 이렇게 싱글톤 객체를 생성하고 관리하는 기능(function)을 싱글톤 레지스트리(Singleton Registry)라고 한다.
 

Spring Framework - Spring DI 컨테이너

들어가며 이전 글에서 IoC의 개념과 IoC 컨테이너(또는 DI 컨테이너)에 대해서 알아보았다. 스프링 프레임워크는 자체적인 DI 컨테이너를 가지고 있으며 런타임 시점에서 스프링 빈(Bean)으로 등록

sehun5515.tistory.com

 

  • 싱글톤 패턴의 디자인을 적용시키지 않고 싱글톤 패턴을 구현함으로서 스프링 컨테이너는 다음과 같은 이점을 챙길 수 있다.
    • 첫 번째로 싱글톤 패턴의 단점 중 하나인 싱글톤 패턴을 구현하기 위해 들어가는 많은 코드들이 생략될 수 있다.
    • 또한 private 생성자로 객체의 인스턴스 생성을 막는 것이 아닌 스프링 컨테이너에서 스스로 객체를 생성, 관리함으로서(IoC) private 생성자를 강제시키지 않을 수 있고 이는 해당 객체를 확장시킨 자식 객체들을 만들기가 더 유용하다
    • 또한 싱글톤의 단점인 DIP, OCP에 대해서도 안전하다. 즉 싱글톤 패턴을 사용함으로서 변경에 대해 유연하지 않다는 단점이 같이 해결된다.

  • 위 사진은 service 객체가 @Bean으로 스프링 컨테이너에 싱글톤으로 관리되고 있을 때 3개의 클라이언트 객체에서 서비스 객체를 요청하는 것에 대해 그림으로 그린 것이다.
  • 보면 스프링 컨테이너에서 하나의 객체만을 생성해 해당 객체를 여러 클라이언트에게 반환하고 있는 것을 확인할 수 있다. 이렇게 함으로서 싱글톤의 단점은 제거하고, 하나의 인스턴스를 효율적으로 사용하는 장점만을 취할 수 있게 되었다.
  • 스프링 컨테이너의 디폴트 관리 방법은 싱글톤 방식이지만 다른 옵션들도 사용이 가능하다. 이것에 대해서는 추후 다른 포스트에서 설명한다.

 

 

 

싱글톤 방식의 주의점

  • 싱글톤 방식은 객체 인스턴스를 단 하나만 사용하여 이를 여러 클라이언트 객체에 반환한다는 특성 상 싱글톤 객체로 관리되는 객체는 내부에 상태가 유지되는 속성이 존재하면 안된다.
  • 즉 무상태(stateless)로 관리해야 한다는 것이며 이는 다음과 같은 주의점이 존재한다는 것을 의미한다.
    • 특정 클라이언트 객체에 의존적인 필드가 존재하면 안된다.
    • 특정 클라이언트가 값을 수정할수 있는 필드가 존재하면 안된다. 즉 특정 클라이언트에서 싱글톤 객체의 속성 또는 상태가 변경되도록 설계하면 절대 안된다.
    • 싱글톤 객체의 상태 또는 필드들은 가급적 읽기만 가능하게 설계해야 한다.
    • 객체 필드 대신에 공유되지 않고 메서드의 수행이 끝나면 제거되는 로컬 변수 또는 파라미터나 ThreadLocal을 사용해야 한다.
  • 사실 글로만 보기엔 조금 이해하기 힘들 수 있다. 그림을 그려보면서 생각해보도록 하자.

  • Service의 코드는 다음과 같다고 가정한다.
public class Service{
    private int itemPrice;	// 상태 유지
    private String itemName;	// 상태 유지
    
    public void order(String itemName, int itemPrice){
        this.itemPrice = itemPrice;
        this.itemName = itemName;
    }
    
    public void getPrice(){
        return itemPrice;
    }
    
    public void getName(){
        return itemName;
    }
}
  • 위의 상황을 시뮬레이션 하는 코드는 다음과 같다.
public class statefulTest{
    public static void main(String[] args){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        
        Service statefulService = ac.getBean(Service.class);
        
        statefulService.order("item1", 1000);
        System.out.println("itemName : " + statefulService.getName() +\n 
            " price : " + statefulService.getPrice());
    }
}
  • 위 코드의 결과는 매우 당연히 "itemName : item1 price : 1000" 이 될 것이다.
  • 그럼 다음의 상황을 생각해보자.

  • 이를 시뮬레이션한 코드는 다음과 같다.
public class statefulTest{
    public static void main(String[] args){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        
        Service statefulService1 = ac.getBean(Service.class);
        Service statefulService2 = ac.getBean(Service.class);
        
        statefulService1.order("item1", 1000);
        System.out.println("itemName : " + statefulService1.getName() +\n 
            " price : " + statefulService1.getPrice());
            
        statefulService2.order("item2", 2000);
        System.out.println("itemName : " + statefulService2.getName() +\n 
            " price : " + statefulService2.getPrice());
        
        
        // print statefulService1
        System.out.println("itemName : " + statefulService1.getName() +\n 
            " price : " + statefulService1.getPrice());
    }
}
  • 위의 코드에서 맨 마지막에 있는 출력문은 어떻게 될까.
  • 싱글톤의 특성에 의해서 동일한 객체가 들어온다. 그런데 statefulService2에서 itemName과 itemPrice를 바꿔버렸다. 이렇게 되면 statefulService2가 설정해놓은 상태가 그대로 유지되어 statefulService1의 정보까지 덮어씌워버릴 것이다.
  • 즉 statefulService2의 상품이름과 가격인 "item2"와 2000이 출력될 것이다.
  • 우리가 원하던건 statefulService1의 이름과 가격인데 statefulService2의 이름과 가격이 나와있어 문제가 발생한다. 특히나 거대한 프로그램에서 이러한 오류가 발생한다면 찾기가 매우 힘들 것이다.
  • 따라서 싱글톤으로 관리되는 객체는 저렇게 특정 클라이언트가 특정 메서드를 통해 싱글톤 인스턴스의 값을 변경할 수 없도록 해야 하며 로컬 변수로 관리하도록 만들어야 한다.

 

 

 

정리

  • 스프링 컨테이너는 싱글톤 방식으로 객체(스프링 빈)을 관리한다. 하지만 일반적인 싱글톤 패턴을 적용하는게 아닌 다른 방식으로(객체의 생성을 단 한번만 수행) 적용함으로서 기존 싱글톤 패턴의 단점들을 해결하고 장점만을 취하도록 하였다.
  • 싱글톤 방식으로 관리되는 객체는 기본적으로 내부에 공유 필드가 있다면 이를 특정 클라이언트에 의존적이거나 변경되지 않도록 주의해야 한다. 이를 지키지 못할 경우 찾기 힘든 에러가 발생할 수 있다.
  • 다음 포스트에서는 @Configuration과 싱글톤에 대해서 살펴보고 객체 생성이 여러번 되고 있는 것 처럼 보이나 실제로는 그렇지 않은 상황에 대해 논의해볼것이다.
복사했습니다!