enum 객체는 compile time에 정의된다
우선 구현방식을 알아보기에 앞서 결론부터 말하자면, enum 객체는 compile time에 정의된다.
왜 그래야만 할까?
사실 enum은 Java에서 객체의 성질을 띠고는 있지만, 본질은 상수이다.
상수는 특성상 인스턴스마다 생성될 필요가 전혀 없다.
오히려 컴파일 타임에 한번만 바인딩되는 것이 바람직하다.
리터럴이나 매직 넘버를 상수화할 때도, static final 키워드를 사용하지 않는가?
enum도 마찬가지로 상수의 특성이 강하기 때문에 compile time에 정의되는 것이라 예측할 수 있다.
어떻게 compile time에 정의되는 걸까?
내부적으로 어떤 구조이길래, 자꾸 compile time에 정의되는 것이라 하는걸까?
운송수단을 나타내는 Vehicle이라는 enum 클래스가 있다고 해보자.
public enum Vehicle {
CAR(100),
AIRPLANE(1000),
SHIP(500);
private final int fee;
Vehicle(final int fee) {
this.fee = fee;
}
}
이 Vehicle 클래스는 다음과 동일한 기능을 한다.
(사실 완전히 동일하지는 않다. Enum 클래스로부터 자동으로 상속받는 메소드 및 프로퍼티가 있기 때문이다.)
final class Vehicle {
public static final Vehicle CAR = new Vehicle(100);
public static final Vehicle AIRPLANE = new Vehicle(1000);
public static final Vehicle SHIP = new Vehicle(500);
private final int fee;
private Vehicle(int fee) {
this.fee = fee;
}
}
보다시피 각각의 enum 객체는 static final로 선언되고 있다.
이런 구조를 갖고 있기 때문에 런타임이 아닌 컴파일 시점에서 enum 객체가 결정되는 것이다.
그리고 중요한 것은, 생성자가 private이라는 점이다.
즉, Vehicle 클래스 안에서만 인스턴스를 만들 수 있고, 그렇기 때문에 enum 객체는 유일성을 보장받는다.
(이것이 enum으로 싱글톤을 만들 수 있는 이유이기도 하다. Effetive Java Item 3 참고)
또한 final을 통해 재할당을 금지하고 있기 때문에 런타임에 enum 객체가 수정되는 것도 불가능하다.
실제로 jdk 1.5 이전 버전에서는 enum 클래스가 없어 위와 같이 사용했다고 한다.
클래스가 추상 메소드를 가지는 경우
enum 클래스가 추상 메소드를 가지는 경우를 생각해보자.
어떻게 이런 것이 가능한걸까?
아래 코드를 한번 보자.
public enum Vehicle {
CAR(100) {
@Override
int calculateTotalFee() {
return fee * 2;
}
},
AIRPLANE(1000) {
@Override
int calculateTotalFee() {
return fee * 4;
}
},
SHIP(500) {
@Override
int calculateTotalFee() {
return fee * 3;
}
};
protected final int fee;
Vehicle(int fee) {
this.fee = fee;
}
abstract int calculateTotalFee();
}
운송수단 enum 객체마다 요금을 계산하는 메소드를 생성했다.
각 운송수단은 어떤 운송수단이냐에 따라 요금을 부여하는 정책이 다르다.
이번에는 enum 클래스가 아닌 일반 클래스로 생성한 Vehicle을 보자.
마찬가지로 위 코드와 아래 코드는 동일한 일을 수행한다.
abstract class Vehicle {
public static final Vehicle CAR = new Vehicle(100) {
@Override
int calculateTotalFee() {
return fee * 2;
}
};
public static final Vehicle AIRPLANE = new Vehicle(1000) {
@Override
int calculateTotalFee() {
return fee * 4;
}
};
public static final Vehicle SHIP = new Vehicle(500) {
@Override
int calculateTotalFee() {
return fee * 3;
}
};
protected final int fee;
private Vehicle(int fee) {
this.fee = fee;
}
abstract int calculateTotalFee();
}
구조가 복잡해보이지만, 간단하다.
Vehicle은 추상 메소드를 가지기에 추상 클래스로 선언되고,
이를 인스턴스화 하는 객체들은 익명 클래스를 통해 구현체를 생성한다.
그렇기에 객체마다 calculateTotalFee라는 행위의 구현 로직이 달라질 수 있는 것이다.
여기서 추가적으로 눈여겨봐야 할 점은, 프로퍼티인 fee가 protected로 선언되어 있다는 점이다.
이유는 벌써 눈치를 챘을지도 모르지만, 구현체에서 super 타입의 프로퍼티인 fee를 사용하기 때문이다.
따라서 private로 fee를 닫아두는 경우, 상속한 클래스들도 사용이 불가능하므로 컴파일 에러가 발생한다.
마치며
enum의 동작 방식 및 구현을 알아보았다.
enum이라고 다른 것이 아닌, 같은 클래스이며 내부적으로 동작하는 방식만 다를 뿐이다.
다음에는 enum 객체에서 행위를 정의하는 경우, 어떻게 유용하게 사용될 수 있는지를 포스팅해야겠다!
참고 자료
'Java' 카테고리의 다른 글
Java: 커스텀 예외 사용에 대한 생각 (0) | 2023.03.16 |
---|---|
Java: Throwable 소개 & API (0) | 2023.03.07 |
Java: enum 소개 및 API 파헤쳐 보기 (3) | 2023.03.05 |
Java: equals & hashCode (0) | 2023.03.04 |
Java: 동일성과 동등성 (2) | 2023.02.25 |