춤추는 개발자

[Spring] 하나의 오브젝트로! 스프링에서의 싱글톤(Singleton) 본문

Developer's_til/스프링 프레임워크

[Spring] 하나의 오브젝트로! 스프링에서의 싱글톤(Singleton)

Heon_9u 2021. 8. 18. 20:45
728x90
반응형

 

 

 지금까지 스프링의 IoC를 적용한 DaoFactory와 UserDao를 구현했습니다. 이번에는 IoC를 적용하기 전과 후의 차이점을 확인하고, 싱글톤에 대해 알아보는 시간을 갖겠습니다.

 

 먼저, 앞에서 작성했던 DaoFactory를 다시 한번 사용하겠습니다. 만약, 어플리케이션 컨텍스트를 적용하기 전의 DaoFactory에 있는 UserDao() 메서드를 두번 호출하고 주소값을 출력하면 어떻게 될까요?

DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();

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


// spring.dao.UserDao@231d323
// spring.dao.UserDao@114f591

 

 위처럼 dao1과 dao2는 다른 값을 가진 동일하지 않은 오브젝트임을 알 수 있습니다. 즉, userDao() 메서드를 호출할 때마다 새로운 오브젝트가 만들어질 것입니다.

 

 이번에는 어플리케이션 컨텍스트를 적용한 DaoFactory에 있는 userDao라는 빈을 가져와서 오브젝트를 만들어보겠습니다. 위와 마찬가지로 2개의 오브젝트를 만들어서 주소값을 출력해봅니다.

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao3 = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);

System.out.println(dao3);
System.out.println(dao4);
System.out.pritln(dao3 == dao4);

// spring.dao.1134f547
// spring.dao.1134f547
// true

 

 두 오브젝트의 값이 같으므로, getBean() 메서드로 가져온 오브젝트가 동일하다는 것을 확인할 수 있습니다. 게다가 == 연산자로 비교했을 때, true를 반환합니다.

 

 이렇게 어플리케이션 컨텍스트를 적용하기 전과 후의 상황에서 UserDao 오브젝트를 구현할 때, 동작 방식의 차이점이 있습니다. 스프링은 여러 번에 걸쳐 빈을 요청하더라고 매번 동일한 오브젝트를 돌려준다는 것입니다. 단순히 getBean()으로 userDao() 메서드를 호출할 때마다 매번 new 연산자로 새로운 오브젝트를 생성하지 않는다는 뜻입니다.

 

📢오브젝트의 동일성과 동등성
흔히, 자바에서는 변수를 비교하는 방법이 2가지가 있습니다. 바로 '==' 연산자'equals()' 메서드입니다. 둘의 차이는 무엇을 비교하느냐에 있습니다. '==' 연산자는 대상의 주소값을 비교하며, 'equals()' 메서드는 객체의 내용을 비교합니다. 그래서 동일성은 '==' 연산자로, 동등성은 'equals()' 메서드를 이용하여 비교합니다.

 만약, 2개의 오브젝트가 동일하다면, 사실은 하나의 오브젝트만 존재하는 것이고, 2개의 오브젝트 레퍼런스 변수를 갖고 있을 뿐입니다.

 반면에 2개의 오브젝트가 동일하지 않지만, 동등한 경우 각기 다른 2개의 오브젝트가 메모리 상에 존재하는 것입니다. 단지 오브젝트에 저장된 정보가 동등하다고 판단할 뿐입니다.

 

✅ 서버 어플리케이션과 싱글톤

 어플리케이션 컨텍스트는 빈을 등록, 생성하고 스프링의 추가적인 서비스까지 제공하는 IoC 컨테이너입니다. 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리의 역할까지 하고 있습니다.

 스프링은 기본적으로 빈 오브젝트를 모두 싱글톤으로 만듭니다. 대신, 디자인 패턴에서 나오는 싱글톤 패턴과 비슷한 개념이지만, 구현 방법은 확연히 다릅니다.

 

1. 서버 어플리케이션의 관점에서

 스프링은 주로 자바 엔터프라이즈 기술을 사용하는 서버 환경이기 때문에 싱글톤으로 빈을 생성합니다. 엔터프라이즈의 서버 환경은 서버 하나당 1초에 수십 ~ 수백번씩 요청을 받아 처리할 수 있는 높은 성능이 요구됩니다.

 

 만약, 클라이언트로부터 요청을 받을 때마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용한다면? 1초에 수백개의 오브젝트가 생성되거나 그 이상이되면 부하가 걸리며 서버가 감당하기 힘들어집니다.

 

 이러한 상황에 대응하기 위해 어플리케이션 안에 제한된 수, 보통 한 개의 오브젝트만 만들어서 사용하는 것이 싱글톤 패턴의 원리입니다. 참고로 서비스 오브젝트인 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작합니다. 서블릿 클래스당 하나의 오브젝트만 만들어주고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용합니다.

 

 따라서 서버 환경에서는 서비스 싱글톤의 사용이 권장됩니다. 하지만 이러한 싱글톤 패턴에도 한계가 있습니다.

 

  • 클래스 외부에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.
  • 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 static 필드를 정의한다.
  • static 팩토리 메서드인 getInstance()를 만들고 이 메서드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 합니다. 생성된 오브젝트는 static 필드에 저장되거나 static 필드의 초기값으로 오브젝트를 미리 만들어둘 수 있습니다.
  • 한번 오브젝트(싱글톤)가 만들어진 이후에는 getInstance() 메서드를 통해 이미 만들어져 static 필드에 저장해둔 오브젝트를 넘겨줍니다.

 

 UserDao를 전형적인 싱글톤 패턴으로 만든다면 아래 코드와 같습니다.

public class UserDao {
    private static UserDao INSTANCE;
    private ConnectionMaker connectionMaker;

    private UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    public static synchronized UserDao getInstance() {
        if(INSTANCE == null)
            INSTANCE = new UserDao(...);

        return INSTANCE;
    }
}

 

 위 코드를 보면 private으로 바뀐 생성자로 인해 외부에서 호출할 수 없어졌습니다. 결국, DaoFactory에서 UserDao를 생성하며 ConnectionMaker 오브젝트를 넣어주는게 불가능해졌습니다.

 일반적으로 싱글톤 패턴 구현 방식에는 아래와 같은 문제가 있습니다.

 

  • 싱글톤 패턴은 생성자를 private으로 제한합니다. 오직 싱글톤 클래스 자신만이 자기 오브젝트를 만들도록 제한하는 것으로 private 생성자만 가진 클래스는 상속이 불가능합니다. 게다가 static 필드와 메서드를 사용하기 때문에 객체지향적인 설계의 장점인 다형성을 적용하기 어렵습니다.
  • 싱글톤은 테스트하기 어렵거나 아예 불가능합니다. 생성자가 만들어지는 방식이 제한적이기 때문에 테스트에서 사용될 목 오브젝트 등으로 대체하기가 어렵습니다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못합니다. 서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있습니다. 여러 개의 JVM에 분산돼서 서버가 설치되는 경우에도 각각 독립적으로 오브젝트가 생성되기 때문에 싱글톤으로서의 가치가 떨어집니다.
  • 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 어플리케이션 어디서든지 사용될 수 있습니다. 아무 객체나 자유롭게 접근하고 수정하는 전역 상태는 객체지향 프로그래밍에서 권장되지 않는 모델입니다.

 

2. 싱글톤 레지스트리

 스프링은 서버 환경에서 싱글톤이 만들어져서 서비스 오브젝트 방식으로 사용되는 것을 적극 지지합니다. 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공합니다. 이를 바로 싱글톤 레지스트리라고 합니다.

 스프링 컨테이너는 싱글톤을 생성, 관리, 공급하는 싱글톤 관리 컨테이너의 역할을 수행합니다. static 필드나 private 없이 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 특징이 있기 때문에 위와 같은 문제들이 발생하지 않습니다.

 

 즉, 싱글톤 레지스트리를 통해 객체지향적인 설계 방식과 원칙, 디자인 패턴 등을 적용하는데 아무런 제약이 없습니다. 

 

3. 싱글톤과 오브젝트의 상태

 멀티스레드 환경에서 여러 스레드가 동시에 같은 데이터를 참조한다면 어떻게 될까요? 저장할 공간이 하나뿐이니 서로 값을 덮어쓰거나 타인의 정보를 읽어올 수 있게 됩니다.

 싱글톤도 마찬가지입니다. 만약 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용된다면, 상태 정보를 내부에 갖고 있지 않은 무상태 방식으로 만들어져야 합니다.

 

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

 

 만약, 아래처럼 인스턴스 변수를 사용하는 UserDao가 있다면 어떤 문제가 있을까요?

public class UserDao {
    private ConnectionMaker connectionMaker;
    private Connection c;
    private User user;

    public User get(String id) throws ClassNotFoundException, SQLException {
        this.c = connectionMaker.makeNewConnection();
        ...
        this.user = new User();
        ...
        
        return this.user;
    }
}

 

 각각 개별적인 값을 가져야하는 변수들이 인스턴스 필드로 선언되어 있습니다. 이는 싱글톤으로 만들어져서 멀티스레드 환경에서 사용되면 위에 언급한 문제들이 발생하게 됩니다. 따라서 기존의 UserDao처럼 개별적으로 바뀌는 정보들은 메서드의 로컬 변수로 정의하거나, 파라미터로 주고 받으면서 사용해야 합니다.

 

 하지만, 별개로 ConnectionMaker 타입의 인스턴스 변수는 사용해도 괜찮습니다. 바로 읽기 전용의 정보이기 때문입니다. 이미 정해진 connectionMaker라는 변수를 수정할 일도 없고, DaoFactory에서 @Bean을 통해 스프링이 관리하는 빈이 될 것이기 때문입니다.

 

 이렇게 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수로 사용해도 좋습니다. 물론 단순한 읽기 전용의 값이라면 static final이나 final로 선언하는 편이 값이 변경되는 에러를 사전에 방지할 수 있을 것입니다.

 

4. 스프링의 빈 스코프

 스프링의 빈은 적용되는 범위인 스코프를 갖고 있습니다. 스프링 빈의 기본 스코프는 싱글톤입니다. 싱글톤 스코프는 컨테이너 내에서 한 개의 오브젝트만 만들어져서, 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지됩니다.

 경우에 따라서 프로토타입, 요청, 세션 스코프 등이 있습니다. 프로토타입은 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 새로운 오브젝트를 생성합니다. 요청 스코프란 웹을 통해 새로운 HTTP 요청이 생길 때마다 오브젝트가 생성됩니다. 세션 스코프는 웹의 세션과 스코프가 유사하다는 특징이 있습니다.

 

[빈 스코프의 종류]

  • 싱글톤
    • 스프링 빈의 기본 스코프
    • 스프링 컨테이너 내에서 단 한 개의 오브젝트만 생성

 

  • 프로토타입
    • 요청에 따라 매번 새로운 오브젝트를 생성
    • 프로토타입을 받은 클라이언트가 객체를 관리해야 함

 

    • request: 각 요청이 들어오고 응답할 때까지 유지되는 스코프
    • session: 세션이 생성되고 종료될 때까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스트와 같은 범위를 유지하는 스코프

🙋‍♂️ 느낀점

 싱글톤의 장단점을 살펴보며, 상황에 따라 철저하게 관리하고 사용해야하는 패턴임을 느꼈다. 어떤 개발 도구든 나의 프로젝트 상황에 맞게 적용하고 활용하듯이 싱글톤도 마찬가지다.

 

 예를 들어, 클래스 내부에서 다른 싱글톤 빈을 저장하려는 용도로 읽기 전용으로 사용하는 변수의 경우, 인스턴스 필드로 선언하며 싱글톤으로 사용할 수 있다. 또는 final로 선언되어 값이 변경될 위험이 없는 변수도 싱글톤으로 사용한다.

 이외에 경우 싱글톤으로 활용할 때, Theard-safe를 지키기 위한 동기화 작업이 필요할 것으로 예상된다.

728x90
반응형