이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › 제네릭, 열거형, 애너테이션
열거형(enum)
enum을 숫자로 저장해 두면 왜 다른 상수가 되어 돌아오나
enum은 주문 상태처럼 정해진 몇 가지 값만 가질 수 있는 타입입니다. 상수 하나하나가 프로그램에 하나뿐인 객체여서 ==로 비교할 수 있고, 필드와 메서드도 가질 수 있습니다. 그런데 이 상수를 DB에 저장할 때 선언 순서 번호(ordinal)를 적어 두면, 나중에 enum 선언을 고쳤을 때 같은 숫자가 다른 상수를 가리키게 됩니다.
실행으로 대조 · JDK 21에서 마흔다섯 가지 조합을 실제로 컴파일하고 실행해 대조
이 실습에 나오는 개념
- 열거형(enum)
- 정해진 몇 개의 값만 가질 수 있는 타입을 만드는 문법이다. enum Status { ORDERED, PAID }처럼 선언하면 ORDERED와 PAID가 각각 Status 타입의 객체가 되고, 이 객체들은 한 번만 만들어지며 new로 더 만들 수 없다. 상수마다 객체가 하나뿐이라 ==로 비교할 수 있고 switch에도 쓸 수 있다. enum도 클래스이므로 필드, 생성자, 메서드를 가질 수 있어서 상수마다 표시 이름이나 코드 값을 붙여 둘 수 있다. 문자열이나 숫자로 상태를 나타낼 때와 달리, 정해지지 않은 값을 쓰면 컴파일 오류가 나므로 상태나 종류를 나타내는 데 쓴다.
- ordinal()과 name()
- 모든 enum 상수가 가지고 있는 두 메서드다. ordinal()은 그 상수가 선언에서 몇 번째인지를 0부터 센 번호를 돌려주고, name()은 선언에 적은 상수의 이름을 문자열로 돌려준다. ordinal()은 선언 순서로 정해지므로 상수를 중간에 끼워 넣거나 순서를 바꾸면 값이 달라진다. 그래서 ordinal() 값을 DB나 파일에 저장해 두면 나중에 같은 숫자가 다른 상수로 읽힐 수 있다. 저장할 때는 name()이나, enum에 따로 만들어 둔 필드의 값을 쓴다.
- enum의 valueOf
- 이름 문자열을 받아 그 이름을 가진 enum 상수를 돌려주는 static 메서드로, 모든 enum에 자동으로 만들어진다. Status.valueOf("PAID")는 Status.PAID를 돌려준다. 이름은 대소문자까지 정확히 같아야 하고, 그런 상수가 없으면 null을 돌려주는 것이 아니라 IllegalArgumentException을 던진다. 그래서 사용자가 입력한 문자열처럼 믿을 수 없는 값을 넘길 때는 예외에 대비해야 한다. 모든 상수를 선언 순서대로 담은 배열을 돌려주는 values()도 함께 만들어진다.
- 동일성과 동등성
- 동일성은 두 참조가 메모리상 같은 객체를 가리키는지를 뜻하며 == 연산자로 비교한다. 동등성은 두 객체가 논리적으로 같은 값을 가지는지를 뜻하며 equals() 메서드로 비교한다. 문자열이나 값 객체를 비교할 때 ==를 쓰면 내용이 같아도 false가 나올 수 있다.
이렇게 바꿔 보세요
- 처음 설정으로 — 번호로 저장한 뒤 상수를 중간에 끼워 넣었습니다. 결제 완료가 주문 취소로 읽힙니다.
- 순서만 바꿔도 — 상수를 추가하지 않고 순서만 바꿔도 번호가 가리키는 상수가 달라집니다.
- 맨 끝에 추가하면 — 기존 상수의 번호가 그대로라서 무사합니다.
- 운 좋게 무사한 상수 — 같은 변경인데 맨 앞의 상수는 번호가 바뀌지 않습니다.
- 이름으로 저장 — 중간에 끼워 넣어도 같은 상수가 나옵니다.
- 이름으로 저장했는데 이름을 바꾸면 — valueOf가 없는 이름을 받아 IllegalArgumentException을 던집니다.
- 직접 만든 code로 저장 — 순서를 바꿔도 이름을 바꿔도 같은 상수가 나옵니다.
처음 설정으로 실행하면
- 예전의 enum 선언은 ORDERED, PAID, SHIPPED 순서였습니다: enum의 상수는 저마다 객체 하나입니다. PAID("결제 완료")는 생성자에 "결제 완료"를 넘겨 만든 Status 객체이고, 프로그램 전체에 PAID 객체는 그것 하나뿐입니다. new로 더 만들 수도 없습니다.
- PAID 상태인 주문을 DB에 저장했습니다: enum 객체를 그대로 넣을 수는 없으므로 그 상수를 나타낼 값을 대신 적어 둡니다. 여기서는 PAID.ordinal()의 값(1)을 적었습니다. ordinal()은 그 상수가 선언에서 몇 번째인지를 0부터 센 번호입니다.
- 그 뒤 enum 선언을 고쳤습니다: 새 상수 CANCELED를 ORDERED와 PAID 사이에 끼워 넣었습니다. 지금의 순서 번호는 ORDERED(0), CANCELED(1), PAID(2), SHIPPED(3)입니다. DB에 적어 둔 값(1)은 고쳐지지 않고 그대로 남아 있습니다.
- Status.values()[1] 실행: values()는 지금 선언의 순서대로 상수를 담은 배열을 돌려줍니다. 1번 칸에 있는 것은 CANCELED입니다. 저장한 것은 PAID인데 다른 상수가 나왔습니다. 예외도 경고도 없습니다.
- System.out.println(s + " (" + s.label() + ")") 실행: 출력은 "CANCELED (주문 취소)"입니다. label()은 enum 안에 직접 만든 메서드로, 그 상수가 만들어질 때 받은 label 값을 돌려줍니다. enum도 클래스라서 이렇게 필드와 메서드를 가질 수 있습니다.
- s == Status.PAID 실행: false입니다. enum 상수는 상수마다 객체가 하나뿐이어서, 어떤 방법으로 얻었든 같은 상수면 같은 객체입니다. 그래서 equals 없이 ==로 비교해도 됩니다. false가 나온 것은 비교 방법 때문이 아니라, s가 정말로 다른 상수(CANCELED)이기 때문입니다. 프로그램은 정상으로 끝납니다.
실행 결과
- ordinal()은 선언 순서일 뿐이어서, 상수를 중간에 끼워 넣거나 순서를 바꾸면 이미 저장해 둔 숫자가 다른 상수를 가리키게 됩니다. 예외가 나지 않으므로 "결제 완료" 주문이 "주문 취소" 주문으로 보이는 잘못된 데이터가 조용히 쓰입니다. ordinal() 값은 저장하지 않습니다.
- enum 상수의 비교에는 ==를 씁니다. 상수마다 객체가 하나뿐이라 결과가 equals와 같고, 한쪽이 null이어도 예외가 나지 않으며, 서로 다른 enum 타입을 비교하면 컴파일 오류로 알려 줍니다.
코드
enum Status {
ORDERED("주문 접수"),
CANCELED("주문 취소"),
PAID("결제 완료"),
SHIPPED("배송 시작");
private final String label;
Status(String label) {
this.label = label;
}
String label() {
return label;
}
}
// 예전 선언(ORDERED, PAID, SHIPPED 순서)일 때 PAID.ordinal() 값을 저장해 둔 것
int saved = 1;
Status s = Status.values()[saved];
System.out.println(s + " (" + s.label() + ")");
System.out.println(s == Status.PAID);
이 모형의 전제
마흔다섯 가지 조합 모두를 이 PC의 JDK 21에서 화면의 코드(고친 뒤의 enum 선언과 그 아래 코드)를 실제로 컴파일하고 실행해서, 저장해 둔 값, 출력 두 줄, 종료 코드, 예외 메시지를 대조했습니다. 저장해 둔 값은 예전 선언의 enum에서 ordinal(), name(), code를 실제로 불러 얻은 것입니다. 화면의 코드에서 enum 선언 아래의 다섯 줄은 main 메서드 안에 들어 있는 것으로, main의 선언은 생략했습니다. 같은 상수는 어떤 방법으로 얻어도 같은 객체라는 것, valueOf가 대소문자가 다른 이름에도 예외를 던진다는 것, ==는 한쪽이 null이어도 예외가 나지 않는다는 것, 서로 다른 enum 타입의 == 비교가 컴파일 오류라는 것도 같은 방법으로 확인했습니다. 실제 DB를 쓰지는 않았고, 저장해 둔 값을 코드에 직접 적어 흉내 냈습니다. 상수를 지우는 변경은 설정에 넣지 않았습니다. 지우면 번호로 읽을 때는 뒤의 상수가 당겨져 나오거나 배열 범위를 벗어난 예외(ArrayIndexOutOfBoundsException)가 나고, 이름으로 읽을 때는 valueOf가 예외를 던집니다(이것도 따로 실행해 확인했습니다).
앞 실습: 제네릭 · 다음 실습: 애너테이션