[Spring] 스프링 컨테이너와 싱글톤 패턴
싱글톤 패턴
가장 단순한 방식으로 싱글톤 패턴이 적용된 클래스는 다음과 같다.
1. Static 영역에 상수로 인스턴스가 생성되어야 한다.
2. 생성자는 private 접근자로 막아놓는다.
3. 인스턴스는 static 메소드로만 외부 접근을 허용한다.
public class Box {
// 1. statice 영역에 상수로 객체를 생성한다.
private static final Box instance = new Box();
// 2. 생성자는 private 접근자로 막아놓는다.
private Box(){
}
// 3. 인스턴스는 static 메소드로 조회한다.
public static Box getBox(){
return instance;
}
}
웹에서 싱글톤 객체가 중요한 이유는 수많은 Request가 들어올 때마다, 객체를 생성할 수 없기 때문이다. 하나의 객체만 생성하고 요청 스레드가 접근하는 방식이 호율적이다.
그러나 효율이 좋다는 이유로
Controller, Service, Reposiotry 등의 클래스를 모두 싱글톤 객체로 만들어 버리면 어떻게 될까?
1) 불필요한 코드가 많아진다.
static으로 선언하니, final로 선언하니, 생성자를 private로 막으니, static 메소드로 인스턴스에 접근하니 등등, 클래스 하나를 만드는데 들어가는 코드가 많아진다. 웹에서 싱글톤 객체로 사용하는 클래스는 3가지 조건을 반복적으로 적용해야 한다.
2) DIP, OCP를 위반한다.
클라이언트가 의존하는 싱글톤 객체를 변수에 주입하려면 static 메소드를 호출해야 한다. static 메소드는 특정한 싱글톤 객체 전용 메소드이다. 클라이언트가 추상적 대상이 아니라 세부 대상(구체클래스)에 직접 의존하는 것은 DIP 위반이다. 만약 싱글톤 객체가 다른 객체로 변경될 경우 클라이언트 객체의 코드도 변경해야 하므로, OCP 원칙도 위반하게 된다.
3) 유연성이 떨어진다.
static 영역에 상수로 인스턴스가 생성되다보니 변경되지 못한다. 생성자도 private으로 막아놓으니 새로운 속성값을 가진 인스턴스 생성도 불가능하다. 그렇다보니 새로운 값을 가진 객체를 테스트 하고 싶어도 테스트가 어려워진다.
이렇듯, 싱글톤 패턴은 메모리 영역에 인스턴스를 하나만 유지하기 위해 수많은 것들을 희생하고 있다.
스프링 컨테이너
스프링 컨테이너는 위에서 제기한 3가지 문제를 모두 해결한다.
이전 포스팅에서 스프링을 사용하는 이유를 다루어 보았다. 스프링의 제어의 역전(IOC)으로 객체(Bean)를 생성하고 의존관계를 대신 주입(DI)하는 방식으로 객체지향 프로그래밍으로 도와주는 프레임워크이다. 스프링은 싱글톤 객체인 Bean을 생성하고 Spring 컨테이너에 보관한다. 그리고 Bean을 필요로 하는 클라이언트에게 의존관계 주입을 해준다.
1) 의존관계 주입 ( DI )
싱글톤 패턴은 클라이언트가 객체를 직접 생성하지는 않지만 static 메소드를 호출하여 직접 의존관계를 주입한다. DIP, OCP 원칙을 위반하지 않으려면 누군가가 대신 의존관계를 주입해주어야 한다. 그 누군가가 바로, 스프링이다.
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderRepository orderRepository;
public OrderServiceImpl(){
}
}
스프링은 @Autowired로 선언된 변수에 타입에 맞는 Bean을 주입한다. OrderServiceImpl은 직접 OrderRepository의 구현체를 주입하지 않으니, 추상적인 OrderRepository 인터페이스만 선언하면 된다. 이로써 DIP 원칙을 지킬 수 있게 된다. OrderRepository 구현체의 Bean이 변경되어도, OrderServiceImpl에서 수정할 코드는 없으니, OCP 원칙도 지킬 수 있게 된다.
2) 불필요한 코드가 사라짐
스프링에서 알아서 싱글톤 객체로 생성하고 생명주기를 관리하다보니, 개발자가 싱글톤 관련 코드를 작성하지 않아도 된다.
3) 유연성이 좋아짐
생성자의 접근자도 public으로 선언할 수 있고 자유로운 객체 생성및 초기화가 가능해진다. 이로써 테스트를 비롯한 다양한 것들이 유연해진다.
싱글톤 객체(Bean) 생성하기 ( @Configuration )
Bean을 싱글톤 객체로 생성하려면 @Configuration으로 선언된 클래스에 생성해야 한다.
@Configuration
public class SpringConfig{
@Bean
public OrderController orderController(){
return new OrderController(orderService()); // orderService() 호출 1
}
@Bean
public ItemController itemController(){
return new ItemController(orderService()); // orderService() 호출 2
}
@Bean
public OrderService orderService(){ // orderService() 호출 3
return new OrderService();
}
}
orderSerivce() 는 총 3번 호출된다.
OrderController가 Bean으로 생성할 때 한번, ItemController가 Bean으로 생성할 때 한 번, 그리고 OrderService가 Bean으로 생성할 때 한번이다. new 연산자는 3번 호출되므로 메모리에는 OrderService 객체가 3개 생성되어야 한다.
그러나
실제로는 하나만 생성된다. ( 싱글톤 객체 )
이유는 스프링이 @Configuration으로 선언한 클래스는 프록시 객체를 스프링 Bean으로 등록하기 때문이다.
프록시는 가짜를 하나 두어 접근을 제어하는 기술이다.
orderService() 요청이 들어왔다고 가정해보자.
프록시는 스프링컨테이너에 OrderService Bean이 존재하는지 체크한다.
존재하지 않으면 실제 SpringConfig의 orderService()를 호출한다.
존재하면 프록시가 OrderService Bean을 반환하고 요청을 종료한다.
이렇게 하면 실제로 호출되는 new 연산은 한 번 뿐이니 싱글톤 객체를 유지할 수 있다.
주의사항
스프링 컨테이너를 사용하므로써, 많은 부분이 자유로워졌지만 주의할 점이 있다.
Bean으로 사용할 클래스의 코드는 Stateless(무상태)하게 설계되어야 한다. Bean은 싱글톤 객체로 여러 요청 스레드가 동시에 접근하게 된다. Bean이 상태를 유지하면 요청A의 처리가 요청B의 처리에 영향을 주는 불상사가 발생한다. 그러므로 Stateful이 아닌 Stateless하게 설계 되어야 한다.
Stateless하게 설계하려면 멤버변수를 정의하지 않으면 된다.
스레드는 JVM 프로세스의 Stack 영역 메모리를 독립적으로 분할하고 Heap영역은 공통으로 사용한다. 객체의 멤버변수는 Heap 영역에 저장되므로, 이곳에 데이터가 저장되면 스레드가 사라져도 데이터는 유지된다.(Stateful) 그러므로 객체의 멤버변수가 아닌 메소드 블록 안 지역변수에 저장해야 한다.(Statless)
만약 멤버변수를 정의해야만 한다면 반드시 상호배제(Mutex)를 적용해야 한다.
참고자료