[Java] Enum 내부 동작과 컴파일 구조

하비·2025년 8월 30일
3

Java

목록 보기
13/13
post-thumbnail

이번엔 Enum의 내부 동작을 알아보겠다.

1. 기본 문법

기본 enum 문법이 컴파일 이후에 어떻게 바뀌는지 알아보자

1) 코드

enum Week { MONDAY, TUESDAY; }

2) 컴파일/디컴파일

javac Main.java
javap -p Main$Week.class

javac Main.java 가 컴파일을 하는 명령어이고,
javap -p가 컴파일된 바이트 코드를 디컴파일해서, private 멤버까지 포함해서 클래스의 구조(필드, 메서드, 생성자)를 보여준다.

3) 결과

Compiled from "Main.java"
final class find.Main$Week extends java.lang.Enum<find.Main$Week> {
  public static final find.Main$Week MONDAY;
  public static final find.Main$Week TUESDAY;
  private static final find.Main$Week[] $VALUES;
  public static find.Main$Week[] values();
  public static find.Main$Week valueOf(java.lang.String);
  private find.Main$Week();
  private static find.Main$Week[] $values();
  static {};
}

4) 설명

final class find.Main$Week extends java.lang.Enum<find.Main$Week> {

Main$Week는 Main 클래스 내부에 Week num이 있어서 $로 표현되었다.
일단 첫 클래스 정의를 보자.
Week enum이 class 형태로 변환되어 있고, Enum을 상속받고 있는 걸 볼 수 있다. class 앞에 final이 붙어있다. 이건 이 Week 클래스를 상속을 못하게 한다는 뜻이다. 즉, 상속 금지를 통해서 enum의 핵심 가치를 보장해준다.

  • 상속 금지를 통해 보장되는 것들
  1. 열거 상수 집합의 불변성: 새로운 상수가 추가되지 않는다는 보장이 생긴다. enum Week { MONDAY, TUESDAY }로 되어 있으면, Week라는 타입은 오직 MONDAY, TUESDAY만 가진다.
  2. 타입 안정성: enum을 쓰면 Week 타입에는 MONDY, TUESDAY만 올 수 있다. 만약 상속이 가능하면 Week 타입 변수에 Sunday 같은 외부 정의 상수가 들어올 수 있다. 이렇게 되면 enum 의미가 깨진다.
  3. switch문 안정성: 상속 허용 시 새로운 상수가 등장할 수 있어 switch문이 불완전해진다. 상속 금지로 인해 컴파일러가 모든 경우를 다 처리했는지 검증이 가능하다.
  4. jvm 내부 최적화: jvm은 enum을 특별 취급한다. 각 상수를 싱글턴 인스턴스로 생성하고 $VALUES 배열에 모두 저장한다. values(), valueOf() 등을 자동으로 생성한다. 상속 금지 덕분에 JVM은 열거 상수 집합이 절대 변하지 않는다는 가정하에 안전한 최적화가 가능하다.
  5. 코드 가독성과 의도 전달: enum을 보면 개발자는 이건 고정된 집합이다 라고 즉시 이해한다. 상속이 열려 있다면, 확장 가능성이라는 불안 요소가 생기게 된다.
public static final find.Main$Week MONDAY;
public static final find.Main$Week TUESDAY;

이게 이제 enum의 상수 값이다. 컴파일에서 public static final 인스턴스로 enum의 상수를 만든다. 즉, MONDAY, TUESDAY는 Week 타입의 싱글톤 객체이다. 이런 구조로 되어 있기 때문에 우리가 Week.MONDAY 이렇게 쓸 수 있는 것이다.

private static final find.Main$Week[] $VALUES;

enum의 모든 상수를 담고 있는 배열 캐시다. values() 호출 시 이 배열을 복사해서 돌려준다. private으로 숨겨진 필드이기 때문에 우리가 직접 접근은 못하고, 컴파일러/JVM이 관리한다.

public static find.Main$Week[] values();

모든 상수를 배열로 리턴하는 메서드다. Week.values()를 쓸 때 호출된다. 내부적으로는 $VALUES.clone() 같은 걸 리턴한다.

public static find.Main$Week valueOf(java.lang.String);

문자열 이름으로 상수를 찾는 메서드다. Week.valueOf(”MONDAY”)라고 하면 Week.MONDAY를 반환해준다. 잘못된 이름이면, IllegalArgumentException이 발생한다.

private find.Main$Week();

enum 생성자는 항상 private이다. 외부에서 새로운 인스턴스 생성 못한다. 오직 JVM이 내부에서만 호출해서 상수(MONDAY, TUESDAY)를 초기화를 한다.
사실상, enum 생성자는 상수 정의 시 자동으로 이름과 순서를 매개변수로 받는다. 생성자에 아무것도 없는게 아니라 상수 이름(”MONDAY”)랑 ordinal이 들어가게 된다. javap가 디컴파일 시 소스 코드 형태를 단순화해서 보여주었기 때문에 아무것도 없는 것처럼 보인다.

private static find.Main$Week[] $values();

컴파일러가 자동으로 넣어주는 헬퍼 메서드다. $VALUES 배열을 초기화하는 역할을 한다. (static block에서 호출)

static {};

static 초기화 블록이다.
여기서 실제로

static {
    MONDAY = new Week("MONDAY", 0);
    TUESDAY = new Week("TUESDAY", 1);
    $VALUES = new Week[]{ MONDAY, TUESDAY };
}

같은 초기화 코드가 들어간다.

2. 필드와 생성자

이번엔 필드와 생성자가 추가된 enum을 보자

1) 코드

enum Week{
	Monday("m");
	
	Week(String string) {
		this.s=string;
	}

	String s;
}

2) 컴파일/디컴파일

javac Main.class
javap -p Main$Week.class

3) 결과

Compiled from "Main.java"
final class find.Main$Week extends java.lang.Enum<find.Main$Week> {
  public static final find.Main$Week Monday;
  java.lang.String s;
  private static final find.Main$Week[] $VALUES;
  public static find.Main$Week[] values();
  public static find.Main$Week valueOf(java.lang.String);
  private find.Main$Week(java.lang.String);
  private static find.Main$Week[] $values();
  static {};
}

4) 설명

아까와 다른 바뀐 부분은 이렇다.

java.lang.String s;
private find.Main$Week(java.lang.String);

필드가 추가되었기 때문에, static에서는

static {
    MONDAY = new Week("MONDAY", 0, "m");
    $VALUES = new Week[]{ MONDAY };
}

이런 식으로 초기화가 수행되게 된다.

3. 메서드 정의 & 상수별 동작

1) 코드

public enum Operation {
    PLUS { public int apply(int x, int y) { return x + y; } },
    MINUS { public int apply(int x, int y) { return x - y; } };

    public abstract int apply(int x, int y);
}

2) 컴파일/디컴파일

javac Main.java

Main.java를 컴파일하면, 다음과 같이 Operation 관련된 class 파일이 3개가 나오게 된다.

이 각각의 class 파일들을 디컴파일 해보겠다.

3) 결과

C:\Users\hobby\eclipse-workspace\Algorithm\src\find>javap -p Main$Operation.class
Compiled from "Main.java"
public abstract class find.Main$Operation extends java.lang.Enum<find.Main$Operation> {
  public static final find.Main$Operation PLUS;
  public static final find.Main$Operation MINUS;
  private static final find.Main$Operation[] $VALUES;
  public static find.Main$Operation[] values();
  public static find.Main$Operation valueOf(java.lang.String);
  private find.Main$Operation();
  public abstract int apply(int, int);
  private static find.Main$Operation[] $values();
  static {};
}

C:\Users\hobby\eclipse-workspace\Algorithm\src\find>javap -p Main$Operation$1.class
Compiled from "Main.java"
final class find.Main$Operation$1 extends find.Main$Operation {
  private find.Main$Operation$1(java.lang.String, int);
  public int apply(int, int);
}

C:\Users\hobby\eclipse-workspace\Algorithm\src\find>javap -p Main$Operation$2.class
Compiled from "Main.java"
final class find.Main$Operation$2 extends find.Main$Operation {
  private find.Main$Operation$2(java.lang.String, int);
  public int apply(int, int);
}

4) 설명

public abstract class find.Main$Operation extends java.lang.Enum<find.Main$Operation> {
  public static final find.Main$Operation PLUS;
  public static final find.Main$Operation MINUS;
  private static final find.Main$Operation[] $VALUES;
  public static find.Main$Operation[] values();
  public static find.Main$Operation valueOf(java.lang.String);
  private find.Main$Operation();
  public abstract int apply(int, int);
  private static find.Main$Operation[] $values();
  static {};
}

일단 Operation부터 보자.

public abstract class find.Main$Operation extends java.lang.Enum<find.Main$Operation> {

이전에는 class 앞에 final이 붙었던게 abstract가 붙어, 추상 클래스가 되었다. 왜냐하면 apply() 추상 메서드가 있으니까 본체에서 구현할 수 없기 때문이다. 따라서 각 상수별로 다른 동작을 구현하기 위해 컴파일러가 상수별 클래스를 자동 생성한다.

static은 이렇게 저장이 된다.

static {
    PLUS = new Operation$1("PLUS", 0);
    MINUS = new Operation$2("MINUS", 1);

    $VALUES = new Operation[] { PLUS, MINUS };
}

Operation$1을 보자

final class find.Main$Operation$1 extends find.Main$Operation {
  private find.Main$Operation$1(java.lang.String, int);
  public int apply(int, int);
}

Operation이 추상 클래스이기 때문에 apply(int,int) 같은 추상 메소드를 구현해줘야 객체를 생성할 수 있다. enum 상수별로 다르게 구현해야 하므로, 각 상수 전용 클래스가 필요하다. 이것이 Operation$1이다.
이 클래스는 컴파일러가 자동으로 만들어준다. 그리고 이것은 final class로 만들어진다.

profile
멋진 개발자가 될테야

0개의 댓글