Design Pattern

싱글톤 할 바에 Enum

Luti 2026. 4. 6. 00:53

 

헤드퍼스트 디자인패턴 책을 보는데 5장 싱글톤 부분에 이런 내용이 있더라고요.

"Enum을 사용하면 싱글톤의 동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화와 역직렬화 문제를 해결할 수 있다."

아무래도 책이 Java 교재가 아니라 디자인패턴 책이다 보니, 저정도의 언급만 있고 구체적인 이유까지는 기술이 안되어있는데요.

그래서 궁금함에 제가 알아본 내용을 정리해보겠습니다.

 

 

 

 

# 00. Singleton

 

## 00-1. Singleton이란

 

Singleton 패턴은 클래스 인스턴스를 하나만 만들고 , 그 인스턴스로 전역 접근을 제공하는 패턴입니다.

 

인스턴스가 하나만 있어도 충분히 잘 돌아가는 기능들은 많습니다.

스레드 풀, 캐시, 대화상자, 사용자 설정, 로깅 등이 그 얘입니다.

또한 이런 기능들은, 인스턴스가 하나만 있어도 잘 돌아가는 것을 넘어, 두 개 이상일 경우 프로그램이 꼬인다던가, 자원을 불필요하게 잡아먹던가, 결과에 일관성이 없어지는 등의 심각한 문제로 이어질 수도 있습니다.

이러한 상황에서 적절하게 인스턴스를 하나만 사용하도록 디자인된 패턴이 바로 Singleton입니다.

 

싱글톤의 생김새부터 들여다보겠습니다.

public class Singleton {
	private static Singleton uniqueInstance;
    
    // 기타 인스턴스 변수
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (uniqueInstance == null) {
        	uniqueInstance = new Singleton();
           }
        return uniqueInstance;
    }
    
    // 기타 메소드
}

 

가장 고전적인 방식의 싱글톤 패턴 형태입니다.

 

private static Singleton uniqueInstance;

 

클래스의 자기 자신의 타입을 정적 변수를 두고

 

private Singleton() {}

 

생성자를 private으로 두어 생성자로 인스턴스를 생성하는 것을 방지해, 오직 싱글톤 방식으로만 인스턴스를 생성하도록 합니다.

 

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

 

`uniqueInstance`가 null이면 아직 인스턴스가 생성되지 않았다는 의미입니다.

아직 인스턴스가 만들어지지 않았다면 `private`으로 선언된 생성자를 이용하여 싱글톤 객체를 만든 다음 `uniqueInstance`에 대입합니다.

`uniqueInstance`가 null이 아니라면 이미 객체가 생성된 것이기 때문에 이때는 바로 `uniqueInstance`를 리턴하도록 합니다.

 

이와 같은 방식의 싱글톤 형태는 멀티스레드 환경에서 안전하게 동작하지 않습니다.

가령, 여러 스레드가 동시에 `getInstance()`를 호출할 때, 각각의 스레드가 동시에 `uniqueInstance` 값을 null로 읽어 결과적으로 두 개 이상의 인스턴스를 생성할 수 있는데요.

이러한 문제를 해결하기 위해 인스턴스를 JVM 클래스 로딩 시 미리 할당하는 방식으로 구현할 수 있습니다.

 

public class Singleton {

    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return uniqueInstance;
    }
}

 

 

 

# 01. 안티패턴 Singleton

 

## 01-1. 인스턴스를 언제 만들 것인가

 

앞선 싱글톤을 구현하는 두 방식은 각각 장단점이 있습니다.

 

첫 번째 방식은 필요할 때 객체를 생성하므로, 실제로 사용되지 않는다면 아예 만들지 않아도 되지만, 멀티스레드 환경에서는 안전하지 않습니다.

 

두 번째 방식은 멀티스레드 환경에서도 비교적 단순하게 안전성을 확보할 수 있습니다.

다만 객체가 실제로 사용되든 말든, 클래스가 초기화되는 시점에 미리 생성됩니다.

 

즉 싱글톤은 곧바로 이런 고민으로 이어집니다.

  • 필요할 때 늦게 만들 것인가
  • 미리 만들어서 단순하게 갈 것인가
  • 늦게 만들되 스레드 안정성까지 보장할 것인가

늦게 만들면서 스레드 안전성을 확보하는 것이 좋아 보입니다.

이러한 선택을 위해서는 `synchronized` 키워드를 활용할 수 있습니다.

 

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}

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

 

다만 이방식은 하나의 스레드만 `getIntance()` 접근을 허용하기 때문에, `getInstance()` 메서드가 호출될 때마다 동기화 비용을 지불하게 됩니다.

객체를 처음 생성할 때만 보호하면 되는데, 이미 인스턴스가 만들어진 이후까지 계속 락을 걸게 되는 것입니다.

 

결국 싱글톤이 단순해 보이지만, 구현을 신경 쓰기 시작하면 위와 같은 문제들이 따라옵니다.

 

 

## 01-2. 리플렉션 

 

앞서 보여드렸던 싱글톤 구현 방식들은 모두 생성자를 `private` 키워드로 막아두어 외부에서는 생성자로 객체를 생성할 수 없도록 했습니다.

일반적으로는 맞는 말이지만, 자바에서는 리플렉션이라는 기능이 있습니다.

리플렉션은 실행 중인 프로그램이 클래스의 생성자, 필드, 메서드 같은 정보를 조사하고, 심지어 접근까지 할 수 있게 해주는 기능입니다.

 

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton anotherInstance = constructor.newInstance();

 

싱글톤임에도 다른 인스턴스가 생겨버릴 수 있는 가능성이 존재한다는 것이죠...

 

 

## 01-3. 직렬화 & 역직렬화

 

리플렉션 외에도 또 별개의 객체가 생겨버리는 케이스가 있는데요.

직렬화 및 역직렬화 시 또 새로운 인스턴스 문제가 발생합니다.

 

  • 직렬화(Serialization): 메모리에 있는 자바 객체를 파일 저장이나 네트워크 전송이 가능하도록 연속적인 바이트 데이터로 변환하는 과정
  • 역직렬화(Deserialization): 저장되거나 전송된 바이트 데이터를 다시 조립해서 원래의 자바 객체 상태로 복원하는 과정

이때, 역직렬화 과정에서 `private` 생성자를 무시하고 완전히 새로운 객체를 강제로 생성해 버립니다.

즉, 싱글톤 인스턴스를 저장해 두었다가 다시 읽어오면,

원래 메모리에 있던 객체와 별개의 객체가 다시 생길 수 있습니다.

 

위와 같은 이유들로 싱글톤은 단순히 생성자를 막고 정적 필드를 두는 정도만으로는 부족하고, 추가적인 대응 코드가 필요합니다.

 

 

# 02. Enum 싱글톤

 

## 02-1. Enum은 멀티스레드 인스턴스 생성 문제에서 자유롭다.

 

가장 고전적인 방식으로 소개드렸던 싱글톤 구현 방식에서 멀티스레드 경쟁 문제가 발생한다고 말씀드렸습니다.

 

이 문제를 해결하려면 개발자는

`synchronized`를 사용하거나,

클래스 로딩 시 미리 객체를 생성하거나,

그 외의 별도 락 패턴을 적용하는 등의 동기화 전략을 고민해야 합니다.

 

반면, Enum은 `if (instance == null)`과 같은 인스턴스 체크 후 생성 로직을 개발자가 직접 명시하지 않습니다.

 

public enum Singleton {
    INSTANCE;
}

 

여기서 INSTANCE는 개발자가 "없으면 만들자"는 흐름으로 생성하는 객체가 아니라

JVM이 Enum 클래스를 초기화하는 과정에서 정해진 Enum 상수로 생성하고 관리하는 대상입니다.

프로그램이 실행되다가 누군가 처음으로 `Singleton.INSTANCE`를 호출하는 그 순간 JVM이 클래스 로딩 및 초기화를 시작하고 이 타이밍에 내부적으로 `new Singleton()`이 실행되면서 객체가 만들어집니다.

 

위에서 예로 보여드린 두 번째 싱글톤 구현 방식과 같은 흐름이라고 볼 수 있습니다.

 

덕분에 싱글톤을 직접 구현할 때처럼,

멀티스레드 문제가 발생하지도,

동기화를 적용하여 추가적인 리소스를 사용할 필요도 없이

Enum은 인스턴스 생성 시점에 대한 고민을 없애줍니다.

 

물론, 이는 멀티스레드 환경에서 객체를 생성하는 시점에 대한 이야기이고, 일반적인 싱글톤의 문제인 '가변 상태에서 동시성 문제가 발생한다'는 점까지는 해결해주지 않습니다.

때문에 이 문제에 대해서는, 싱글톤 사용 시 필드를 무상태에 가깝게 두는 편이 좋다는 주의를 지키며 개발하는 것으로 극복해야 합니다.

 

 

## 02-2. enum은 리플렉션에도 강하다. 

 

앞서 설명드렸듯 일반적인 싱글톤은 리플렉션의 `setAccessible(true)`를 통해 `private` 생성자를 강제로 열어 새로운 객체를 만들 수 있습니다.

하지만 Enum의 경우, 리플렉션 `Constructor` 내부에 다음과 같은 강력한 방어 로직이 하드코딩되어 있습니다.

 

리플렉션으로 객체를 강제 생성하려고 시도할 때, 대상이 Enum이면 `IllegalArgumentException`을 던져버리는 것을 확인할 수 있는데요.

 

An enum type has no instances other than those defined by its enum constants. It is a compile-time error to attempt to explicitly instantiate an enum type

 

즉, 논리적인 원리가 아닌, 자바 언어 차원에서 enum을 특별취급하여 정해진 상수들만 인스턴스가 될 수 있다는 규칙을 강하게 보장해 주는 것입니다.

 

 

## 02-3. enum은 직렬화와 역직렬화에도 안전하다.

 

일반적인 클래스를 역직렬화할 때는 바이트 데이터를 읽어 완전히 새로운 객체를 메모리에 찍어냅니다.

하지만 Enum을 직렬화/역직렬화할 때는 이번에도 자바가 Enum만을 위한 완전히 다른 특별한 규칙을 사용합니다.

 

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form.
“To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method...”
  • 직렬화할 때: 객체의 복잡한 상태를 바이트로 저장하는 게 아니라, 단순히 enum 상수의 문자열 이름만 저장한다.
  • 역직렬화할 때: 바이트를 읽어 새로운 객체를 생성하는 것이 아니라 , 저장해 둔 이름을 읽은 뒤, 자바 내부에 이미 존재하는 `Enum.valueOf(Class, String name)` 메서드를 호출한다.

즉, 새로 만들지 않고, 메모리에 이미 올라가 있는(클래스 로딩 때 만들어진) 그 객체를 이름을 찾아서 그대로 가져오도록 동작하게 됩니다.

 

 


 

사실 근데, 스프링을 사용하면서 직접 싱글톤 클래스를 작성할 일도 거의 없거니와,

애초에 스프링의 싱글톤 빈이 GoF의 Singleton 패턴과 완전히 일치하는 개념도 아니며,

객체 생성 문제도 기존 싱글톤 구현 방식의 흐름과 크게 다를 것 없으며,

리플렉션과 직렬화&역직렬화 문제도 너무 특수한 상황에서 발생하는 것인지라,

굳이 굳이 Enum을 사용해서 싱글톤 클래스를 만들 일이.... 있을지 모르겠습니다.

 

싱글톤 패턴의 단점과 자바 Enum의 특수성에 대해 알아본 것에 더 의미를 남기겠습니다. 

 

 

'Design Pattern' 카테고리의 다른 글

Facade 잘 설계하기  (0) 2026.05.06
전략 패턴 적용기 + 스프링에서 List로 빈 주입하기  (0) 2025.08.07