스프링 싱글톤 이유 - seupeuling sing-geulton iyu

이유는 상태가 유지된 상태 stateful2.studentAddress("hoe", "미국"); 을 하여 name 부분에 "hoestory"에서 "hoe"로 변경되었기 때문입니다. 이렇게 되는 이유는 스프링 컨테이너에 빈으로 등록된 stateful 빈이 객체의 인스턴스를 한 번만 생성하기 때문에 stateful1과 stateful2는 같은 인스턴스입니다.

직전 강의 ‘4강. 스프링 컨테이너와 스프링 빈’에서는 스프링 컨테이너에서 빈을 생성하고 가져오는 방법들에 대해서 공부했다. (관련글 : https://velog.io/@ace0390/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88)

이번 강의에서는

  • 싱글톤 패턴
  • 왜 스프링 컨테이너가 주로 싱글톤으로 빈을 관리하는지
  • 어떻게 싱글톤 빈을 관리하는지

에 대해 공부한다.

웹 애플리케이션에서 싱글톤이 좋은 이유

다양한 애플리케이션이 있지만 그 중 웹애플리케이션의 특징 중 하나는 여러 종류의 요청을 동시에 처리해야 한다는 것이다.

만약 싱글톤 객체를 쓰지 않는다면 요청 마다 요청에 필요한 객체들을 새로 생성해야 할 것이고 이 객체들은 요청이 끝난 후 gc 의 대상이 될 것이다. 요청이 많아지면 빈번한 gc 로 인해 시스템에 악영향을 끼칠 수 있다.

요청에 필요한 객체들을 미리 싱글톤 객체로 생성해 놓고 사용하면 위의 문제를 해결할 수 있다.

싱글톤 패턴

클래스의 인스턴스가 메모리상에 1개만 생성되는 것을 보장하는 디자인 패턴이다.

싱글톤 예제

static 키워드와 private 생성자를 이용해 애플리케이션이 올라갈때 SingletonService 객체가 method area 에 하나만 생성되게 함으로써, SingletonService 객체는 메모리에 단 하나만 존재하게 된다.

SingletonService 가 필요한 곳에서는 아래와 같은 코드로 SingletonService 객체를 가져와 사용할 수 있다.

SingletonService singletonService = SingletonService.getInstance();

싱글톤 패턴 주의점

싱글톤 패턴은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체가 상태를 유지하면 안된다.

  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 읽기만 가능해야 한다.
  • 클래스 변수가 아닌 지역변수, ThreadLocal 등을 사용해야 한다.

싱글톤 패턴 단점

싱글톤 패턴은 위와 같은 장점도 있지만 단점도 존재한다.

  • 클라이언트가 구체 클래스의 의존한다. => DIP 위반
  • 클라이언트가 구체 클래스에 의존하기 때문에 OCP 원칙을 위반할 가능성이 높다.
  • 여러 테스트 세트내에서 하나의 인스턴스가 그 상태를 유지하기 때문에 테스트가 어렵다.
  • private 생성자 때문에 자식 클래스를 만들기 어렵다.
싱글톤 컨테이너
  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너 덕분에 싱글턴 패턴의 단점들을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
@Configuration 어노테이션과 싱글톤

빈을 정의하고 있는 AppConfig.java 코드를 보자

AppConfig.class

AppConfig.memberService() 메서드는 new 연산자로 MemberServiceImpl 객체를 생성하고 있다.

memberService1 == memberService2 test

위 코드에서는 appConfig.memberService() 를 2번 호출 후 메서드 결과의 인스턴스가 같은 인스턴스 인지 비교하고 있다.

자바 코드만 놓고 본다면 AppConfig.memberService() 에서 MemberSerivceImpl 객체를 매번 생성하므로 해당 테스트는 실패해야할 것처럼 보이나 성공한다.

memberService1 == memberService2

위에서 설명했듯이 스프링에서 기본적으로 싱글톤으로 빈을 관리하기 때문이다. 하지만 자바 코드상 분명 실패해야 하는 테스트인데 스프링은 어떻게 memberSerivce 를 싱글톤으로 유지하는 것일까?

@Configuration @Bean

appConfig 객체는 우리가 만든 AppConfig 클래스의 객체가 아닌 스프링이 cglib 라이브러리로 만든 프록시 클래스다. (위의 스크린샷에서 appConfig 를 출력한 것을 보자)

프록시 클래스 내에서 @Bean 이 붙은 메서드 마다 이미 빈이 존재하면 존재하는 빈을 반환하고, 빈이 없다면 생성해서 빈으로 등록하고 반환하는 코드가 동적으로 만들어 지는 것이다.

상황에 따라서 Application에서 특정 클래스에 대해 하나의 객체만 존재해야 하는 경우가 있습니다. 예를 들어 특정 세션에 대해 하나의 데이터베이스 연결만 존재해야 하고, 여러 모듈에서 공유되고 있는 글로벌 속성 집합을 참조하는 개체의 경우가 그 대표적인 케이스입니다. 

이러한 클래스의 유일한 목적은 클래스의 객체가 전체 응용프로그램에서 하나만 생성되고, 여러 클라이언트에서 공유되어야 합니다. 

# Code 1

package tips.pattern.singleton;

public class MyConnection {
  private static MyConnection connection = new MyConnection();
  
  private MyConnection() {
  }
  
  public static MyConnection getConnection() {
    return connection;
  }
}

생성자를 private 선언하여 외부에서 이 클래스를 직접 인스턴스화할 수 없도록 했습니다. 

유일하게 호출할 수 있는 방법은 static method인 MyConnection.getConnection() 을 통한 방법입니다. 

# Code 2

package tips.pattern.singleton;

public class MyConnectionConsumer {
  public static void main(String[] args) {
    MyConnection connection1 = MyConnection.getConnection();
    MyConnection connection2 = MyConnection.getConnection();
    if (connection1 == connection2) {
      System.out.println("Both the references are pointing to the same object");
    }
    else {
      System.out.println("Objects are not equals");
    }
  }
}

위의 코드로 증명할 수 있습니다. 

Code 1 에는 한가지 문제점이 있습니다. Client가 MyConnection을 사용하지 않는다면? connection 객체가 static 개체로 관리되고 있기 때문에 MyConnection class가 로딩되는 시점에 객체가 로딩될 것입니다. 

# Code 3 - 필요할때 connection 이 생성되도록 변경

package tips.pattern.singleton;

public class MyConnection {
  private static MyConnection connection = null;
  
  private MyConnection() {
  }
  
  public static MyConnection getConnection() {
    initObject();
    return connection;
  }
  
  private static void initObject() {
    if (connection == null) {
      connection = new MyConnection();
    }
  }
}

Code 3는 멀티스레드 환경에서 잘 동작할까?라고 생각했을 때 잘 동작하지 않을 것입니다. 

동시에 MyConnection.getConnection()을 호출한다면? 문제가 될 것이라 이걸 수정해보면 다음과 같이 될 것입니다. 

# Code 4

private static void initObject() {
  synchronized (MyConnection.class) {
    if (connection == null) {
      connection = new MyConnection();
    }
  }
}

Thread Safe에 대한 Bill Pugh 싱글톤 패턴도 있습니다. 

# Code 5

public class MyConnection {
  private MyConnection() {
  }
  
  private static class MyConnectionHolder {
    static final MyConnection CONNECTION = new MyConnection();
  }
  
  public static MyConnection getConnection() {
    return MyConnectionHolder.CONNECTION;
  }
}

가장 보편적으로 사용되는 싱글톤 구현법이며, static 키워드로 선언된 객체는 메모리 할당이 단 한 번만 이루어지고, final 키워드로 선언되면 그 값을 덮어쓸 수 없음을 이용한 방법입니다. 


Java Singleton 클래스는 classloader 단위이며, Spring Singleton 은 Application context 단위입니다. 

Java 싱글톤

싱클턴 클래스를 구현하기 위해 사전 정의된 규칙 같은건 없지만 클래스는 클래스 로더마다 한 번만 인스턴스화 해야 합니다. 

  • 싱글턴 클래스라고 하면 해당 클래스의 인스턴스를 하나만 만들수 있습니다. 
  • 외부 클래스로부터 인스턴스화 되는 것을 막기 위해 생성자는 private으로 선언되어야 합니다. 
  • 인스턴스화 된 클래스를 static 변수로 선언합니다. 
  • 인스턴스화 된 클래스를 리턴하는 함수를 선언합니다. 

Spring 싱글톤

Spring에서는 컨테이너 내에서 특정 클래스에 대해서 @Bean이 정의되면 스프링 컨테이너는 그 클래스에 대해서 딱 한 개의 인스턴스를 만듭니다. 이 공유 인스턴스는 설정 정보에 의해서 관리되고, Bean이 호출될 때마다 스프링은 생성된 공유 인스턴스를 리턴 시킵니다.