본문 바로가기
Framework/Spring

[토비의 스프링] Singleton Registry에 대한 이해

by 코딩하는 랄로 2024. 1. 2.
728x90

이전 글에서 @Configuration  annotation을 사용하여 application context를 사용해보았는데, 이번 글에서는 그 이전에 userDaoFactory를 직접 사용했던 방법과 application context를 사용했던 방식의 차이점을 알아보면서 이를 통해 무엇을 알 수 있는지에 대해 공부하여 보겠다.

 

 

 

UserDaoFactory VS Application Context

두 가지 방식은 테스트 결과만 보면 동일하지만, 파고들어보면 중요한 차이점이 있다. 이를 알아보기 위해서, 먼저 UserDaoFactory를 직접 사용했던 방식으로 userDAO 객체를 두 개를 생성해보자.

public static void main(String[] args) {
    UserDaoFactory factory = new UserDaoFactory();
    UserDAO dao1 =  factory.userDAO();
    UserDAO dao2 =  factory.userDAO();

    System.out.println(dao1);
    System.out.println(dao2);
}

 

해당 코드를 동작시키면 아래와 같은 결과가 나온다.

com.chapter1.spring.dao.UserDAO@5910e440
com.chapter1.spring.dao.UserDAO@6267c3bb

 

두 객체의 주소가 다르기 때문에, UserDaoFactory를 통해 생성된 두 객체는 서로 다른 객체임을 알 수 있다. 왜냐하면 UserDaoFactory 클래스의 userDAO 메소드는 호출 될 때마다 new 생성자를 통해 새로운 객체를 생성하여 반환하기 때문이다.

 

이번에는 application context 방식을 이용하여 두개의 userDAO 객체를 생성한 뒤 비교하여 보자.

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(UserDaoFactory.class);
    UserDAO dao1 = context.getBean("userDAO", UserDAO.class);
    UserDAO dao2 = context.getBean("userDAO", UserDAO.class);

    System.out.println(dao1);
    System.out.println(dao2);
}

 

해당 코드를 동작시키면 아래와 같은 결과가 나온다.

com.chapter1.spring.dao.UserDAO@a2431d0
com.chapter1.spring.dao.UserDAO@a2431d0

 

두 객체가 동일한 것을 확인할 수 있다!! 해당 두 객체가 정말 동일한지를 확실히 확인하기 위해서 == 를 이용하여 출력해보자.

System.out.println(dao1==dao2);  //true

 

== 이용하여 확실하게 비교해보아도 application context를 이용하여 getBean 메소드를 통해 userDAO 객체를 생성할 때에는 두 객체가 동일한 객체임을 보여주고 있다. 이를 통해 알 수 있는 것은, Spring은 빈을 싱글톤으로 관리를 한다는 것이다!!

 

 

 

Singleton Registry

application context는 IoC container인 동시에, Singleton을 저장하고 관리하는 Singleton Registry이기도 하다.( 이 때에 Singleton은 디자인 패턴의 Singleton과 유사하지만 구현 방법은 다르다. ) Spring은 별 다른 설정을 하지 않으면 기본적으로 Bean을 Singleton으로 만들어 관리한다.

 

그렇다면 왜 Spring은 Singleton으로 Bean을 생성하고 관리하는 것일까? 그 이유는 Spring이 동작하는 환경이 주로 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다. 서버 내에서 수많은 요청이 오갈 때마다 각 로직을 담당하는 오브젝트를 새로 생성하여 사용하게 된다면 상당한 부하가 발생할 것이다.

 

그렇기 때문에 엔터프라이즈에서는 일찍이 서비스 오브젝트라는 개념을 사용하여 서블릿 클래스 당 하나의 오브젝만 생성하고 해당 오브젝트를 공유하여 동시에 사용해온 것이고 서비스 싱글톤의 사용을 권장하는 것이다.

 

하지만, 자바에서 싱글톤 패턴을 구현하여 사용하는 것에는 여러 단점들이 있기 때문에 Spring은 자바의 Singleton 패턴을 보완한 Singleton Registry를 통해 Singleton object를 만들고 관리하는 기능을 제공하여 준 것이다. 

 

 

 

Statefull VS Stateless

Singleton은 하나의 자원을 공유하기 때문에, 멀티 쓰레드 환경에서는 상태 관리에 신경을 써주어야 한다. Singleton을 만드는 방식에는 인스턴스 필드의 값을 변경하고 유지하는 상태 유지 방식(statefull)과 상태 정보를 내부에 저장하지 않는 무상태 방식(stateless) 방식으로 나뉜다.

 

읽기 전용 값이라면 크게 문제가 없지만, 이외의 경우에 멀티 쓰레드 환경에서는 여러 쓰레드가 동시에 값에 접근하여 수정하는 위험한 상황이 발생할 수 있기 때문에 무상태 방식을 이용하여 Singleton을 생성해야 한다. 무상태 방식을 사용할 경우, 여러 정보는 메소드 파라미터나 로컬 변수를 통해 다루어 주면 된다.

 

다음 UserDAO 클래스를 통해 어떤 경우에 무상태 방식을 사용하는지 살펴보자.

public class UserDAO {

    private DBConnection dbConn;

    public UserDAO(DBConnection dbConn) {
        this.dbConn = dbConn;
    }

    public User get(int id) throws ClassNotFoundException, SQLException {
        Connection conn = dbConn.dbConnection();

        PreparedStatement ps = conn.prepareStatement(
                "select * from user where id = ?"
        );

        ps.setInt(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setName(rs.getString("name"));
        user.setNickName(rs.getString("nick_name"));

        rs.close();
        ps.close();
        conn.close();

        return user;
    }
}

 

위의 UserDAO에서 DBConnection 타입의 dbConn은 statefull 방식으로, add 메소드 안의 Connect 타입의 conn, User 타입의 user는 로컬 변수로서 stateless 방식으로 상태가 관리되는 것을 볼 수 있다. 각 변수의 상태 관리 방식이 다른 이유는 다음과 같다.

  • DBConnection dbConn : 읽기 전용으로 스프링이 관리하는 Bean을 할당 받음. => 런타임에 값이 변동될 일이 없음
  • Connect conn/User user : 개별적으로 변하는 정보 => 인스턴스 필드로 선언하면 위험 => 로컬 변수로 사용 

각 상황에 맞춰 적절한 방식을 사용하여야, 여러 요청이 오가는 멀티 쓰레드 환경에서 Singleton object를 안전하게 사용할 수 있다.

 

 

 

 

 

참조 :  이일민, 토비의 스프링 3.1 - 스프링의 이해와 원리, 에이콘, 2012

728x90