본문 바로가기
Java

[Java] Enum이 철벽치는 방법

by soro.k 2024. 11. 15.

 

 

들어가기 전에

Effective Java 스터디를 진행하다가 [Item 3]. private 생성자나 열거 타입으로 싱글턴임을 보증하라에서 아래의 내용이 나왔다.

싱글턴을 만드는 세 번째 방법은 원소가 하나인 열거 타입을 선언하는 것이다.
public 필드 방식과 비슷하지만, 더 간결하고, 추가 노력 없이 직렬화할 수 있고, 심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

출처 : Effective Java 3/E

 

해당 아이템을 담당한 발표자가 "왜 열거 타입이 리플렉션 공격으로부터 안전하다는 걸까요?"라는 질문을 던졌고 그에 대한 답변으로 자바는 열거 타입의 구조를 변경할 수 없게끔 리플렉션을 제한하기 때문이라는 설명을 들었다. 

 

리플렉션 런타임 시점에 클래스, 인터페이스, 생성자, 메소드, 필드들을 탐색하고 조작할 수 있게 해준다. 런타임 시점에 자바의 Class 클래스가 Objects와 Classes에 대한 모든 정보를 가지고 있기 때문이다. 자세한 과정은 다음과 같다. 

1. 김노력이 자바 코드를 작성한다. 
2. 자바의 JVM이 실행되면서 김노력이 작성한 자바 코드를 바이트코드로 컴파일한다.
3. 컴파일된 정보가 static 영역에 저장된다.
4. 김노력은 런타임 시점에 찾고 싶은 클래스 이름을 명시하고 클래스 정보를 가져온다.

 

 

그렇다면 Enum 클래스는 어떻게 리플렉션을 통한 인스턴스화를 막을 수 있다는 걸까?

 

인스턴스화 불가

Enum이 말합니다

 

Enum 클래스는 기본적으로 인스턴스화하려고 하면 컴파일 오류가 발생하게끔 구현되어있다. 리플렉션 공격을 통한 인스턴스화를 어떻게 Enum이 막을 수 있는지 예제 코드를 통해 알아보겠다.

출처 :  https://docs.oracle.com/javase/specs/jls/se17/html/jls-15.html#jls-15.9.1

 

 

[1] 일반 Class - Reflection을 통한 인스턴스 생성 예제

일반적인 상황에서 리플렉션을 활용할 수 있는 코드이다. 생성자를 반환하고 접근 제한을 허용하도록 변경해 새롭게 인스턴스를 생성할 수 있다는 것을 알 수 있다.

public class ReflectionDemo {

    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;

        // 리플렉션을 통해 생성자를 반환한다.
        Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
        // 생성자의 접근 제한이 설정되었다면 접근할 수 있도록 허용한다.
        constructor.setAccessible(true);

        // 새롭게 인스턴스를 생성한다.
        Singleton singleton2 = (Singleton) constructor.newInstance();
    }
}

 

 

[2] Enum Class - Reflection을 통한 인스턴스 생성 예제

이번에는 똑같은 코드를 Enum 클래스에 적용해보자.

@SpringBootApplication
public class EnumTestApplication {

    public static void main(String[] args) {
        try {
            Constructor<Color> constructor = Color.class.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            Color newColor = constructor.newInstance("YELLOW", 4);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

결과

 

바로 리플렉션을 활용해서 Enum 클래스를 인스턴스화할 수 없다는 메시지와 함께 IllegalArgumentException이 발생한다. 그러니까 내부적으로 이미 인스턴스화 자체에 철벽을 쳐놓은 셈이다.

 

 

직렬화

"추가 노력 없이 직렬화가 가능하다"는 이야기에 대해서도 알아보자. Enum 클래스는 내부적으로 직렬화를 구현하고 있다. Java 스펙에 따르면 Enum은 직렬화 메커니즘의 특별한 처리를 통해 역직렬화 결과로 중복된 인스턴스가 생성되는 것을 방지한다고 한다. 

Enumeration classes are all serializable and receive special handling by the serialization mechanism. The serialized representation used for enum constants cannot be customized.

출처 : https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Enum.html

 

 

중복된 인스턴스 생성을 방지하는 과정은 다음과 같이 이루어진다.

  • Enum 상수를 역직렬화하기 위해 ObjectInputStream에서 상수 이름을 읽는다. (직렬화 시 내보낸 바이트를 다시 읽는 과정)
  • Enum.valueOf() 메서드를 호출해 해당 Enum 타입과 받은 상수 이름을 인수로 전달하여 역직렬화된 상수를 얻는다.

Enum은 클래스와 상수의 이름만을 저장하는데 역직렬화 과정에서 이 정보를 사용해 JVM 내에 이미 존재하는 상수에 대한 참조를 반환하기 때문에 중복 생성을 막을 수 있는 것이다.

 

 

단일 생성자

Enum 클래스는 기본적으로 protected로 지정된 단일 생성자만 존재한다. 외부에서 직접 생성자를 호출할 수 없기 때문에 상수가 새로 생성되거나 임의로 변경되는 것을 방지할 수 있다. 더불어서 Enum의 상수는 컴파일러에 의해 static과 final을 부여 받으므로 한 번 초기화된 이후로 불변함을 유지하게 된다.

 

 

Clone 불가

clone() 메서드 내부에서 CloneNotSupportedException을 반환하게 해 싱글톤 상태를 유지할 수 있게 한다.

 

 

 

정리

Java 스펙에 이번 글과 관련하여 잘 정리된 글이 있어 아래의 내용으로 이번 글을 마친다.

열거형 클래스의 인스턴스는 열거형 상수로 정의된 것 외에는 존재하지 않습니다. 열거형 클래스를 명시적으로 인스턴스화하려고 시도하면 컴파일 시 오류가 발생합니다 (§15.9.1).컴파일 오류 외에도 열거형 클래스 인스턴스가 열거형 상수 외에 존재하지 않도록 보장하는 세 가지 추가 메커니즘이 있습니다.
- Enum의 final clone 메서드는 열거형 상수가 절대로 복제(clone)될 수 없도록 합니다.
- 리플렉션을 통한 열거형 클래스의 인스턴스 생성은 금지됩니다.
- 직렬화 메커니즘의 특별한 처리를 통해 역직렬화 결과로 중복된 인스턴스가 생성되는 것을 방지합니다.

출처 : https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.9

 

 

 

참고

 

'Java' 카테고리의 다른 글

[Java] MessageFormat  (0) 2023.05.27
[Java] Object.clone()  (0) 2023.03.04
[Java] Object.hashCode()와 Hashing, 그리고 Hash Collision  (3) 2023.02.20
[Java] Object.equals()  (0) 2023.02.14
[Java] Socket에 관하여  (0) 2023.02.08