[프로그래밍] 정적 클래스와 싱글톤

정적 클래스 및 멤버

정적(static) 클래스는 인스턴스화될 수 없는 클래스이다. 따라서 new 키워드를 사용하여 생성자(인스턴스 생성자)를 호출할 수 없다. 정적 클래스는 인스턴스화될 수 없으므로 public 인스턴스 생성자를 가질 수 없으며, 정적 멤버(필드 및 메서드)만 가지고 있다. 정적 멤버는 모든 클래스의 모든 인스턴스가 공유할 수 있다.

정적 클래스는 인스턴스 생성 없이 클래스의 이름으로 클래스의 정적 멤버를 사용할 수 있다. 클래스 필드(변수), 클래스 메서드가 정적 클래스의 멤버에 해당된다.

정적 클래스는 인스턴스화 될 수 없으므로 인스턴스 생성자를 포함할 수 없지만 정적 필드 초기화를 위해 정적 생성자를 포함할 수는 있다. 비정적 클래스도 초기화가 필요한 정적 멤버를 포함하는 경우 초기화를 위해 정적 생성자가 필요하다. 하나의 인스턴스만 생성할 수 있는 비정적 클래스를 정의하려면 싱글톤(singleton)을 사용하면 된다.

정적 클래스는 코드 상에서 처음으로 참조되기 전에 로드된다(비정적 클래스의 경우 인스턴스화될 때 로드된다). 이러한 로드를 지연(lazy) 로드라고 한다. 그리고 정적 필드가 초기화된 후 정적 생성자가 호출되도록 보장된다. 정적 생성자는 한 번만 호출되며, 정적 클래스는 애플리케이션 라이프사이클 동안 메모리에 존재한다.

정적 클래스를 사용하면 컴파일러가 클래스에 인스턴스 멤버가 실수로 추가되지 않았는지 확인할 수 있다는 장점이 있다. 컴파일러는 인스턴스 멤버가 정의된 정적 클래스의 인스턴스를 생성할 수 없도록 한다.

클래스에 정의된 정적 멤버는 동일한 클래스 내에 정의된 비정적 멤버에 접근할 수 없으며, 메서드 파라미터로 명시적으로 전달되지 않는 한 직접적으로 어떤 객체의 인스턴스 변수에 접근할 수 없다.

정적 클래스는 상속할 수 없으며 클래스의 어떤 인스턴스도 아닌 클래스에 속하기 때문에 오버로드될 수는 있지만 오버라이드(재정의)될 수는 없다. 또한 정적 클래스는 인터페이스를 구현할 수 없고 목 객체를 생성할 수 없기 때문에 테스트하기 어렵다.

정적 클래스만이 정적 멤버를 가질 수 있는 것은 아니다. 비정적 클래스도 정적 멤버를 포함할 수 있다. 비정적 클래스의 정적 멤버는 클래스의 인스턴스가 생성되지 않은 경우에도 사용할 수 있다. 정적 멤버는 인스턴스 이름이 아닌 클래스 이름으로 접근할 수 있다. 클래스의 인스턴스 수에 관계 없이 정적 멤버의 복사본은 하나만 존재한다.

멀티 스레드 애플리케이션에서 정적 멤버는 여러 스레드 간에 공유되므로 정적 클래스의 멤버는 스레드 안전(thread-safe)하도록 구현되어야 한다. 여러 스레드가 동시에 하나의 정적 멤버에 접근할 수 있지만 동시에 사용은 불가능하며 한 번에 하나의 스레드만 사용할 수 있다는 점을 유의해야 한다. 따라서 정적 멤버는 스레드 안전성을 갖도록 해야 한다.


싱글톤

싱글톤(singleton)이란 애플리케이션 내에서 클래스의 인스턴스를 하나만 생성하는 디자인 패턴을 의미한다. 싱글톤 인스턴스는 싱글톤 패턴으로 생성된 인스턴스를 말한다.

싱글톤 인스턴스의 참조는 생성자나 메서드의 인자로 전달할 수 있다. 또한 싱글톤은 정적 클래스와 달리 상속 및 인터페이스 구현이 가능하므로 런타임 수준에서 객체지향 프로그래밍의 다형성을 사용할 수 있다.

싱글톤 인스턴스는 애플리케이션 내에서 전역적으로 공유되어 사용된다. 멀티 스레드로 동작하는 애플리케이션에서는 스레드 간 경쟁 조건으로 인해 여러 개의 싱글턴 인스턴스가 생성되는 문제가 발생할 수 있다. 이를 해결하기 위해 스레드 안전한 싱글톤 인스턴스 생성 코드 구현이 반드시 필요하다.

싱글톤은 초기화 방법에 따라 두 가지로 나눌 수 있다. 즉시(eager) 초기화는 클래스 로딩 시점에 인스턴스가 생성된다. 반면 지연(lazy) 초기화는 싱글톤 인스턴스를 반환하는 정적 메서드가 처음 호출될 때 인스턴스가 생성된다.

스레드 안전한 싱글톤 구현을 가장 쉽게 하는 방법은 인스턴스 생성을 정적 블록 또는 필드 선언부에서 즉시 초기화하는 것이다. 이는 클래스가 로드될 때 객체가 생성되는 즉시 초기화 방법이다. 자바의 경우 클래스의 초기화는 정적 초기화 코드(정적 블록)와 클래스에 선언된 정적 필드(클래스 변수)에 대한 초기화 코드를 실행하는 것으로 구성되며, 멀티 스레드 환경에서 클래스나 인터페이스의 초기화는 동기화를 필요로 한다. 이를 위해 각 클래스 또는 인터페이스에서는 고유한 초기화 잠금이 일어난다. 이러한 JVM의 클래스 초기화 동작 방식을 이용하여 싱글톤 인스턴스를 스레드 안전하게 하나만 생성할 수 있다.

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

즉시 초기화는 클래스가 로드될 때 항상 객체가 생성되므로 사용되지 않는 객체를 위해 메모리 공간이 할당되는 단점이 있다. 지연 초기화는 이러한 리소스 부담을 개선한다. 하지만 지연 초기화는 멀티 스레드 애플리케이션에서 인스턴스를 여러 개 만드는 경쟁 조건을 일으킬 수 있다. 지연 초기화를 사용하면서 스레드 안전한 싱글톤 인스턴스 생성을 위해 이중 확인 잠금(double-checked locking) 또는 주문형 초기화 홀더(initialization on-demand holder) 디자인 패턴을 사용하면 지연 초기화를 사용하는 스레드 안전한 싱글톤 패턴 구현이 가능하다. 주문형 초기화 홀더 패턴은 정적 필드의 스레드 안전한 지연 초기화를 가능하게 하며 성능이 좋다.


자바의 정적 멤버와 싱글톤

자바에서는 static 키워드를 사용하여 최상위 클래스를 정적 클래스로 정의할 수는 없다. 하지만 다음과 같은 방법으로 클래스를 정적 클래스의 특성을 갖도록 할 수 있다.

  • 클래스의 인스턴스 생성을 막기 위해 생성자의 접근 제어자를 private로 설정한다.
  • 클래스가 정적 멤버만 갖도록 하기 위해 모든 멤버를 static으로 선언한다.
  • 클래스를 상속할 수 없도록 만들기 위해 final 접근 제어자를 사용하여 클래스를 선언한다.

클래스에 정적 멤버만 포함시킬 수도 있다. 이 경우 비정적 클래스가 정적 멤버를 갖는 형태이다. 비정적 클래스의 멤버가 서로 다른 인스턴스 간 서로 공유되어야 할 필요가 있다면 해당 멤버를 정적 멤버로 선언하면 된다. 정적 멤버는 힙 메모리에 존재한다.

일반적으로 클래스를 싱글톤으로 만들기 위해 다음과 같이 클래스를 정의한다.

  • final 클래스를 선언한다.
  • 해당 클래스의 유일한 인스턴스를 참조하는 정적 필드를 정의한다.
  • private 생성자를 정의한다.
  • 유일한 인스턴스 참조를 반환하는 정적 팩토리 메서드를 정의한다.
    public final class Singleton {
      private static Singleton INSTANCE;
        
      private Singleton() {        
      }
        
      public static Singleton getInstance() {
          if(INSTANCE == null) {
              INSTANCE = new Singleton();
          }
            
          return INSTANCE;
      }
    }
    

하지만 위 구현은 멀티 스레드 환경에서 경쟁 조건으로 인해 인스턴스가 여러 개 생성되는 문제가 존재한다. 동일 시점에 getInstance() 메서드가 두 번 호출되어 객체가 두 개 이상 생성될 가능성이 있다. 스레드 안전한 싱글톤 인스턴스 생성 코드 구현을 위해서는 연산(인스턴스 생성 및 반환)의 원자성을 위해 동기화(synchronization) 기법을 사용해야 한다. 이를 위해 정적 팩토리 메서드 정의 시 synchronized 키워드를 사용한다.

public synchronized static Singleton getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new Singleton();
    }
    return INSTANCE;
}

인스턴스를 반환하는 정적 팩토리 메서드인 getInstance() 메서드에 synchronized 키워드를 선언하여 여러 스레드가 동시에 접근하지 못하도록 함으로써 경쟁 조건으로 인한 문제를 해결할 수 있다. 그러나 이러한 스레드 간 동기화는 오버헤드로 인한 성능 저하를 유발할 수 있다.

싱글톤을 생성하는 또다른 방법은 열거(enumeration) 타입을 사용하는 것이다. 이 방법을 사용하면 스레드 안전한 싱글톤 인스턴스 생성 코드 구현이 가능하다.

public enum Singleton {
    INSTANCE; 

    private Singleton() {
    }
 
    public Singleton getInstance() {
        return INSTANCE;
    }
}

열거 타입을 사용하면 열거 타입 구현 자체에 의해 직렬화 및 스레드 안전성이 보장되어 내부적으로 단일 인스턴스만 사용할 수 있으며 스레드 안전성 문제가 해결된다.

자바에서 최상위 클래스 대신 클래스의 내부(inner) 클래스를 정적 클래스로 정의할 수는 있다. 이러한 내부 정적 클래스를 정적 중첩 클래스(static nested class)라고 한다. 정적 중첩 클래스는 인스턴스 메소드와 정적 메소드를 모두 가질 수 있으며 외부 클래스의 인스턴스에 대한 참조를 갖지 않으므로 외부 클래스의 정적 멤버에만 접근할 수 있다. 정적 중첩 클래스는 참조를 위해 외부 클래스의 인스턴스 생성이 필요하지 않으며 정적 중첩 클래스의 인스턴스만 생성하면 된다. 자바에서 정적 중첩 클래스는 대표적으로 싱글톤을 지연 초기화 방식으로 스레드 안전하게 구현하기 위해 사용된다.

지연 초기화를 사용하면서 스레드 안전한 싱글톤 인스턴스 생성을 위해 주문형 초기화 홀더 디자인 패턴을 사용하려면 다음과 같이 코드를 작성한다. 정적 중첩 클래스를 사용하여 정적 필드 초기화 시 싱글톤 인스턴스를 생성한다.

public class Singleton {
    private Singleton() {
    }

    private static class LazyHolder {
        static final Singleton INSTANCE = new Something();
    }

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

외부 클래스는 클래스 로드 시점에 초기화되지만 정적 클래스로 정의된 내부 클래스는 외부 클래스의 로드 시점에 초기화되지 않는다. 이 방법 역시 JVM의 클래스 초기화 과정을 이용한 것이며 스레드 간 동기화 문제가 해결된 스레드 안전한 구현이다.


코틀린의 정적 멤버와 싱글톤

코틀린에서 인스턴스가 아닌 클래스에 속한 정적 멤버(상수, 프로퍼티, 함수)를 선언하기 위해서는 object 키워드를 사용한다. object 선언은 싱글톤(singleton)을 정의한다. 싱글톤은 인스턴스가 단 하나뿐이고 이 인스턴스에 접근할 수 있는 전역 영역(global scope)에 단 하나만 존재한다. 코틀린은 싱글톤 객체가 다른 클래스를 확장하거나 인터페이스를 구현하도록 허용한다. object 선언 시 클래스 영역의 멤버를 선언하여 싱글톤을 정의할 수 있다. object의 모든 멤버는 해당 객체의 이름과 동일한 이름의 클래스의 멤버가 된다. 이때 멤버에 @JvmStatic을 적용하면 정적 멤버로 컴파일된다.

정적 멤버와 비정적 멤버를 하나의 클래스 내에 정의하거나 팩토리 메서드를 정의해야 하는 경우 클래스 내에서 정적인 멤버 선언만 companion object 선언 안에 위치시키면 된다. 이를 동반 객체(companion object)라고 한다.

동반 객체 내 멤버 선언은 해당 선언이 속한 클래스 파일 안에서 한 그룹으로 묶인다. 동반 객체의 멤버는 동반 객체가 속한 클래스의 인스턴스의 비공개 멤버에도 접근할 수 있다. 동반 객체도 object 선언을 통해 생성되는 싱글턴 객체와 마찬가지로 다른 클래스를 확장하거나 인터페이스를 구현할 수 있다.

서로 다른 인스턴스 간 공유할 정적 메서드가 필요하다면 동반 객체를 사용하면 된다. 동반 객체는 비정적 클래스에 정적 멤버를 포함시키기 위해 사용한다. 동반 객체와 @JvmStatic 애너테이션을 사용하면 기존 자바의 클래스를 코틀린으로 바꿔도 정적 메서드 호출 코드를 변경할 필요가 없다. 이를 코틀린 지원 기능을 사용하여 리팩토링하려면 최상위 함수를 사용하면 된다. 코틀린에서는 자바와 달리 정적 함수를 클래스에 정의하는 대신 최상위에 정의할 수 있다.

코틀린에서는 멤버(상수, 프로퍼티, 함수)를 클래스 밖에 선언할 수 있다. 하지만 JVM에서는 최상위 데이터나 함수를 표현할 방법이 없다. JVM은 최상위 함수가 아닌 정적 메서드만 지원한다. 따라서 코틀린 컴파일러는 파일 내 최상위 멤버 선언이 존재하는 경우 정적 멤버가 정의되어 있는 클래스를 생성해 준다.


싱글톤 객체 생성과 스프링 프레임워크의 싱글톤 빈


참고

Comments