애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이다. 하지만 왜 싱글톤으로 관리하는걸까? 기존의 싱글톤 디자인 패턴과 무엇이 다를까? 그 부분에 대해 토비의 스프링 3.1을 읽고 다시 한 번 정리해보았다.

서버 애플리케이션과 싱글톤

왜 스프링은 싱글톤으로 빈을 만들까? 이는 스프링이 적용되는 주요 대상이 자바 엔터프라이즈 기술을 사용하는 서버 환경이기 때문이다. 서버 환경에서는 하나의 서버당 최대 초당 수십~수백 번의 요청을 받아 처리할 수 있어야 한다. 여기서 매번 클라이언트 요청이 올때마다 오브젝트를 새로 만들어서 사용한다고 생각해보자. 요청 한 번에 5개의 오브젝트가 생성되고, 초당 500개의 요청이 들어온다고 가정하면, 1초에 2500개의 인스턴스가 생성된다. 1분이면 15만개, 한시간이면 9백만개의 오브젝트가 생성되는데, 아무리 GC가 좋아졌다고 하여도 이정도의 부하는 서버가 감당하기 힘들다.
이러한 부하를 감당하기 위해서, 서블릿은 대부분 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드는 하나의 오브젝트를 공유해 동시에 사용한다. 이렇게 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어 사용하는 것이 싱글톤 패턴의 원리이다.

싱글톤 패턴의 한계

이전에 싱글톤 디자인 패턴을 통해서도 학습하였지만, 자바는 보통 아래의 방법을 토대로 싱글톤을 구현한다.

  • 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private로 설정한다.
  • 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
  • 스태틱 팩토리 메소드인 getInstance()를 만들고, 이 메소드가 최초로 호출될 때에만 오브젝트가 생성되도록 한다. (생성된 오브젝트는 스태틱 필드에 저장되며, 또는 스태틱 필드의 초기값으로 미리 만들 수도 있다.)
  • 한 번 오브젝트가 만들어지고 나면, getInstance()를 통해 기존에 생성되어 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.
public class UserDao {
	private statid UserDao INSTANCE;
	....
	private UserDao(ConnectionMaker  connectionMaker) {
		this.connectionMaker = connectionMaker;
	}
	public static synchronized UserDao getInstance() {
		if( INSTANCE == null) INSTANCE = new UserDAO(???????);
		return INSTANCE;
	}
}

싱글톤 패턴으로 구현한 UserDao클래스는 위와 같다. 여기서 우리는 아래와 같은 문제점을 발견할 수 있다.

  • private생성자를 가지고 있어 상속할 수 없다.
    • 싱글톤 패턴은 생성자를 private로 제한한다. 오직 싱글톤 클래스 자신만이 자기 오브젝트를 만들도록 제한하는 것인데, 이 private생성자만 가지고 있을 경우, 상속이 불가능하다. 객체지향의 장점인 상속과 이를 이용한 다형성을 적용할 수 없다.
  • 싱글톤은 테스트하기 어렵다.
    • 싱글톤은 생성방식이 제한적이기 때문에 테스트에서 사용할 때 목 오브젝트 등으로 대체하기 어렵다. 초기화 과정에서 생성자등을 통해 사용할 오브젝트를 다이내믹하게 주입하기도 어려워 필요한 오브젝트는 직접 오브젝트를 만들어 사용할 수 밖에 없다. 이런 경우 테스트용 오브젝트로 대체하기 힘들다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    • 서버환경이 클래스 로더를 어떻게 구성하고 있느냐에 따라 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어 질 수 있다. 여러개의 JVM에 분상되어 설치될 경우, 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.
  • 싱글톤의 사용은 전역 상태를 만들 수 있어 바람직하지 못하다.
    • 싱글톤은 사용하는 클라이언트가 정해져 있지 않다. 스태틱 메소드를 이용해 언제든지 싱글톤에 접근할 수 있기 때문에 애플리케이션 어디서든지 사용될 수 있고, 그러다 보면 자연스럽게 전역 상태로 사용되기 쉽다. 아무 객체나 자유롭게 접근하고 수정하고 공유할 수 있는 전역상태를 가지는 것은 객체지향 프로그래밍에서는 권장하지 않는 방법이다.

싱글톤 레지스트리

스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하는데, 이를 싱글톤 레지스트리라고 한다. 기존싱글톤 패턴과 다르게 아래의 장점을 가지고 있다 :

  • 스태틱 메소드와 private생성자를 사용하지 않아도 되며 평범한 자바 클래스를 싱글톤으로 활용 가능
  • 테스트 환경에서 자유롭게 오브젝트를 만들 수 있다.(public생성자를 가질 수 있다).
  • 싱글톤 객체를 생성하고 관리하는 기능을 가진다.

싱글톤과 오브젝트의 상태

싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 빈에 접근하여 사용할 수 있다. 기본적으로 싱글톤을 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용할 경우, 상태정보를 내부에 갖고있지 않은 무상태 방식으로 만들어야 한다.

상태가 없는 방식으로 클래스를 만드는 경우, 각 요청에 대한 정보, DB나 서버의 리소스로부터 생성한 정보는 어떻게 다뤄야 할까? 이때는 파라미터와 로컬 변수, 리턴 값등을 이용하면 된다. 메소드 파라미터나, 메소드 내부에서 생성되는 로컬 변수는 스택에 저장되어 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드가 변수의 값을 덮어 쓸 일은 없다.

public class UserDao {
	private ConnectionMaker connectionMaker;
	private Connection c;
	private User user;
	
	public User get(String id) throws ClassNotFoundException, SQLException {
		this.c = connectionMaker.makeConnection();
		....
		this.user = new User();
		this.user.setId(rs.getString("id"));
		this.user.setName(rs.getString("name"));
		this.user.setPassword(rs.getString("password"));
		.....
		return this.user;
	
	}
}

위에서 정의된 인스턴스 변수(=상태변수)는

  • ConnectionMaker connectionMaker
  • Connection c
  • User user

세가지가 있다. connectionMaker는 초기에 값을 담아오고 나서 변하지 않는 읽기전용 변수이기 때문에 문제가 되지 않는다.(물론 단순 읽기 전용 값이라면 static final 혹은 final로 선언하는 편이 낫다) 여기서 문제가 되는 부분은

  • Connection c
  • User user 이다. 어떠한 쓰레드가 요청에 의해 get()메소드를 처리하고 있는 과정 중에, 다른 요청이add()를 처리 하고 있다면 user는 갑자기 다른 값으로 바뀔 것이다. 이러한 현상을 미연에 방지하기 위해 개별적으로 바뀌는 정보는 로컬 변수로 정의하거나, 파라미터로 주고받으면서 사용하게 해야한다.

참고자료 :

  1. 토비의 스프링 3.1

oksusutea's blog

꾸준히 기록하려고 만든 블로그