[Java] 열거형(Enum) 이해하고 사용하기

2025. 2. 22. 22:50·Java

enum 이란?

enum은 JDK 1.5 부터 도입된 연관된 상수(Constant)들의 집합을 정의하는 특수한 클래스다. enum 에 미리 정의해둔 정해진 값만 사용하도록 강제함으로써 데이터 일관성이 확보되고 가독성,유지보수가 편리해진다. enum 은 싱글톤(Singleton)과 유사하게JVM에서 클래스 로딩 시 단 한번만 인스턴스되고 이후 동일한 객체를 재사용하므로 불필요한 인스턴스 생성이 일어나지 않는다. 

enum 을 사용하지 않는 방식의 문제점

[문자열을 이용한 방식의 문제점]

enum 을 사용하지 않고, 단순 문자열 비교를 이용해 회원등급에 따라 할인을 적용하는 메서드를 정의했다. 

public class MemberService {
    public int discount(String grade, int price) {
        if (grade.equals("BASIC")) {
            return price - 1000;
        } else if (grade.equals("GOLD")) {
            return price - 3000;
        } else if (grade.equals("DIAMOND")) {
            return price - 5000;
        }
        return price;
    }
}

 해당 방식의 문제점은 "BASC", "GOD"처럼 오타가 있어도 컴파일 시점에서 검출할 수 없으며, 파라미터에 미리 정해둔 값이 아닌 임의의 문자열이 들어올 수 있다는 것이다.

 

[상수를 이용한 방식의 문제점]

다음으로 class 내부에 상수를 정의하고 이용하는 방식이다.

public class MemberGrade {
    public static final String BASIC = "BASIC";
    public static final String GOLD = "GOLD";
    public static final String DIAMOND = "DIAMOND";
}
public class MemberService {
    public int discount(String grade, int price) {
        if (grade.equals(MemberGrade.BASIC)) {
            return price - 1000;
        } else if (grade.equals(MemberGrade.GOLD)) {
            return price - 3000;
        } else if (grade.equals(MemberGrade.DIAMOND)) {
            return price - 5000;
        }
        return price;
    }
}

 단순 문자열을 사용하는 문자열 오타를 방지 (MemberGrade.BASIC 사용) 할 수 있다는 점에서 개선되었다. 하지만 문자열 상수를 사용해도 String 타입이기 때문에 파라미터로 어떤 문자열이든 입력할 수 있으며, 개발자가 실수로 문자열 상수를 사용하지 않고 직접 문자열을 사용해도 막을 수 있는 방법은 없다.

 

[Type-Safe Enum Pattern 을 이용하는 방식의 문제점]

일반적으로 잘 이용되지는 않지만, Enum을 사용하지 않고 Java 클래스를 통해 열거형을 구현하는 방식으로 구현할 수도 있다.

public class MemberGrade {
    public static final MemberGrade BASIC = new MemberGrade("BASIC");
    public static final MemberGrade GOLD = new MemberGrade("GOLD");
    public static final MemberGrade DIAMOND = new MemberGrade("DIAMOND");

    private final String grade;

    private MemberGrade(String grade) {
        this.grade = grade;
    }

    public String getGrade() {
        return grade;
    }
}

먼저 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별로 상수를 선언한다. 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다. 이때 각각을 상수로 선언하기 위해 static, final을 사용한다.

  • static을 사용해서 상수를 메서드 영역에 선언한다.
  • final을 사용해서 인스턴스(참조값)를 변경할 수 없게 한다.
public class MemberService {
    public int discount(MemberGrade grade, int price) {
        if (grade == MemberGrade.BASIC) {
            return price - 1000;
        } else if (grade == MemberGrade.GOLD) {
            return price - 3000;
        } else if (grade == MemberGrade.DIAMOND) {
            return price - 5000;
        }
        return price;
    }
}

 

MemberGrade.BASIC, MemberGrade.GOLD, MemberGrade.DIAMOND는 클래스 로딩 시점에 한번만 생성되어 JVM의 메서드 영역에 저장된다. 따라서 해당 클래스를 사용하면 동일한 메모리주소를 참조하므로 비교할 때는 grade == MemberGrade.BASIC와 같이 == 참조값(Ref) 비교를 사용한다. 

 

외부에서 MemberGrade 객체를 생성할 수 없으므로 BASIC, GOLD, DIAMOND 외에는 사용할 수 없도록 강제할 수 있게 되었다. 파라미터로 들어오는 타입도 MemberGrade 로 강제되었다.

 

하지만 이 방식도 아래와 같은 단점이 있다.

  • switch 문을 사용할 수 없다. (자바의 switch는 enum을 지원하지만 일반 클래스 타입은 지원하지 않음)
  • toString(), values() 같은 기본적인 유틸리티 메서드를 제공하지 않아, 필요시 직접 구현해야 하며, 코드가 길어진다.
  • Java 직렬화시 역직렬화로 인한 중복 인스턴스 생성문제가 있다.
  • 리플렉션을 막지 못한다. enum은 Reflection을 통한 객체 생성을 방지하지만, Type-Safe Enum 방식은 private 생성자를 setAccessible(true)로 변경하여 새로운 인스턴스를 만들 수 있다.

직렬화와 리플렉션 예시를 만들어 하나씩 살펴보면

public class MemberGrade implements Serializable {
    public static final MemberGrade BASIC = new MemberGrade("BASIC");
    ...
}

직렬화를 위해 Serializable 을 구현하고

public class JavaSerializationTest {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("membergrade.ser"));
        out.writeObject(MemberGrade.BASIC);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("membergrade.ser"));
        MemberGrade restored = (MemberGrade) in.readObject();
        in.close();

        System.out.println(MemberGrade.BASIC == restored); 
        System.out.println(MemberGrade.BASIC);
        System.out.println(restored);
    }
}

역직렬화해서 비교하면 서로다른 인스턴스다. 결론적으로, Java 직렬화시 Serializable 만으로는 인스턴스가 하나만 생성되는것을 보장할 수 없다.

Constructor<MemberGrade> constructor = MemberGrade.class.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
MemberGrade newInstance = constructor.newInstance("VIP");

System.out.println(MemberGrade.BASIC == newInstance);

리플렉션으로 새로운 인스턴스가 생성되는 것 또한 막을 수 없다.

 

이처럼 enum 을 사용하지 않았을 때 나타나는 문제점들을 enum 을 사용하면 해결할 수 있다.

 

enum 주요 메서드

아래와 같은 메서드를 기본 제공하며 활용할 수 있다.

values() 모든 enum 상수를 배열로 반환
valueOf(String name) 문자열을 enum으로 변환
ordinal() enum 선언 순서를 반환
name() enum 이름을 반환
toString() 기본적으로 name()을 반환하지만, 오버라이드 가능

 

ordinal()은 사용하지 않는 것이 좋다. ordinal() 을 활용해 순서로 코드를 작성하게되면, 중간에 상수를 선언하는 위치가 변경되거나, 중간에 새로운 상수를 추가하게 될 경우 경우 버그가 발생할 가능성이 있다.

enum 사용법과 활용

[switch 문에서 enum 사용 방법]

public int discount(MemberGrade grade, int price) {
    return switch (grade) {
        case BASIC -> price - 1000;
        case GOLD -> price - 3000;
        case DIAMOND -> price - 5000;
    };
}

일반적인 switch 문 뿐 아니라 자바 12부터 지원되는 switch expression 을 이용해 간단히 값을 반환하도록 만들 수 있다. enum 이 가지고있는 필드를 switch 문에서 모두 정의했다면 default 가 필요없다.

public int discount(MemberGrade grade, int price) {
    return switch (grade) {
        case BASIC -> price - 1000;
        case GOLD -> price - 3000;
        default -> throw new IllegalStateException("Unexpected value: " + grade);
    };
}

특정 필드를 제외하고, 누락시 처리를 추가하려면 default 를 이용할 수 있다.

 

[enum과 인터페이스 활용 방법]

인터페이스를 활용해 아래처럼 공통으로 정의될 메서드를 넣어줄 수 있다.

public interface DiscountPolicy {
    int discount(int price);
}

public enum Grade implements DiscountPolicy {
    BASIC {
        @Override
        public int discount(int price) {
            return price - 1000;
        }
    },
    GOLD {
        @Override
        public int discount(int price) {
            return price - 3000;
        }
    },
    DIAMOND {
        @Override
        public int discount(int price) {
            return price - 5000;
        }
    };
}
public class MemberService {
    public int discount(Grade grade, int price) {
        return grade.discount(price);
    }
}

추가적인 if-else나 switch 없이 각 열거형에서 직접 로직을 구현해 역할을 명확하게 정의할 수 있다. enum 값에 따라 각기 다른 동작을 할 수 있게 만들어 상태와 행위를 같이 관리할 수 있다.

 

[values() 와 valueOf() 사용법] 

for (MemberGrade grade : MemberGrade.values()) {
    System.out.println(grade.name());  // BASIC, GOLD, DIAMOND
}

MemberGrade grade = MemberGrade.valueOf("GOLD");
System.out.println(grade);  // GOLD

values() 를 이용해 모든 enum 값을 순회하거나, valueOf 로 문자열을 enum 으로 변환할 수 있다.

 

[EnumMap과 EnumSet 활용법]

enum을 활용하게 되면 Set, Map 과 같은 자료구조에 넣어서 사용해야 할 수도 있다.  

 

java는 일반적인 HashSet 과 HashMap 보다 빠르게 동작할 수 있도록 Enum 에 최적화된 EnumSet, EnumMap 을 제공한다.

 

둘다 Thread-safe 하지 않게 구현되어 있다.

Map<MemberGrade, Integer> discountMap = new EnumMap<>(MemberGrade.class);
discountMap.put(MemberGrade.BASIC, 1000);
discountMap.put(MemberGrade.GOLD, 3000);
discountMap.put(MemberGrade.DIAMOND, 5000);

enum 을 키로 사용해 EnumMap 을 사용할 수 있다. enumMap 은 내부적으로 배열기반으로 동작하며, null 키를 허용하지 않으며, null 값을 저장할 수는 있다. 

 EnumSet<MemberGrade> highGrades = EnumSet.of(MemberGrade.GOLD, MemberGrade.DIAMOND);
 System.out.println(highGrades.contains(MemberGrade.BASIC)); // false

EnumSet 은  내부적으로 비트 벡터(bit-vector)를 사용해 빠르고 메모리 효율적으로 동작한다. null 값을 허용하지 않으며 enum 만 저장할 수 있다.

 

[Function 을 이용해 비즈니스로직을 간결히 처리하는 방법]

import java.util.function.Function;

public enum DiscountPolicy {
    NONE(price -> price),              // 할인 없음
    BLACK_FRIDAY(price -> price * 0.7), // 30% 할인
    NEW_USER(price -> price - 3000),   // 신규 가입자 3,000원 할인
    VIP(price -> price * 0.8);         // VIP 고객 20% 할인

    private final Function<Double, Double> discountFunction;

    DiscountPolicy(Function<Double, Double> discountFunction) {
        this.discountFunction = discountFunction;
    }

    public double applyDiscount(double price) {
        return discountFunction.apply(price);
    }
}

각각 할인 정책에 맞는 가격을 반환하는 enum 을 만들어 활용할 수 있다.

public class DiscountService {
    public static void main(String[] args) {
        double originalPrice = 10000;

        System.out.println("일반 가격: " + DiscountPolicy.NONE.applyDiscount(originalPrice));  // 10000
        System.out.println("블랙 프라이데이 가격: " + DiscountPolicy.BLACK_FRIDAY.applyDiscount(originalPrice));  // 7000
        System.out.println("신규 유저 할인 가격: " + DiscountPolicy.NEW_USER.applyDiscount(originalPrice));  // 7000
        System.out.println("VIP 고객 가격: " + DiscountPolicy.VIP.applyDiscount(originalPrice));  // 8000
    }
}

enum 의 특징

[enum 은 컴파일 시 class로 변환된다]

enum 은 단순한 상수 그룹이 아니라, 컴파일되면 내부적으로 java.lang.Enum<T> 를 상속하는 final class 로 변환된다. 

public enum MemberGrade {
    BASIC(1000), 
    GOLD(3000), 
    DIAMOND(5000);

    private final int discountAmount;

    MemberGrade(int discountAmount) {
        this.discountAmount = discountAmount;
    }

    public int getDiscountAmount() {
        return discountAmount;
    }
}

 

javac 명령어를통해 컴파일 후 얻은 바이트 코드를 읽기 좋게 javap -p 명령어로 디어셈블해보면 

 

java.lang.Enum 클래스를 확장하는 클래스인 것을 확인할 수 있다.

 

[enum 인스턴스는 단 한번만 생성을 보장한다]

enum의 생성자는 반드시 private 이다. (protected 또는 public 으로 지정할 수 없다)

따라서 일반적으로 접근제어자(Access Modifier)를 생략하고 위처럼 생성자를 만든다.

 

컴파일한 enum 생성자 부분을 보면 자동으로 private 생성자로 매핑된다. 따라서 new 키워드를 사용하여 인스턴스를 만들 수 없다.

 

[enum 인스턴스는 클래스 로딩시점에 생성된다.] 

그럼 enum 인스턴스는 언제 생성되는걸까?

javap -v 로 바이트코드를 더 자세히 살펴보면 위와 같은 결과를 확인할 수 있다.

 

static {};  은 static initializer block(정적 초기화 블록)으로 클래스 로딩 시 단 한 번만 실행되며, 해당 클래스의 모든 정적 필드를 초기화하는 역할을 담당한다. Java에서 enum 타입을 컴파일하게 되면, 컴파일러가 자동으로 static initializer block을 생성해  클래스 내부의 정적 상수 필드를 초기화하는 코드를 추가하게 된다. 이후 static {} 블록을 실행해 enum의 모든 인스턴스를 초기화한다.

 

따라서 JVM이 enum 클래스를 처음 로드할 때, enum 인스턴스가 자동으로 생성된다.

 

[enum 은 불변객체다]

컴파일 결과를 확인했을 때 내부필드가 private static final 이므로 필드변경이 아예 불가능하다.  

 

[상속을 지원하지 않는다]

 

oracle java tutorial

 

Enum 은 내부적으로 java.lang.enum 클래스에 의해 상속된다. 자바는 다중상속을 지원하지 않기 때문에 enum 역시 다른 클래스를 사용받을 수 없다. (인터페이스는 구현 가능하다) 

 

[비교시 equals 또는 == 모두 가능]

enum 의 equals 는 == 비교로 구현되어 있다. 

 

[enum 직렬화 및 역직렬화]

https://docs.oracle.com/javase/1.5.0/docs/guide/serialization/spec/serial-arch.html#enum

 

Enum 상수는 일반적인 Serializable 또는 Externalizable 객체와는 다른 방식으로 직렬화된다.

  • 직렬화(Serialization)
    ObjectOutputStream이 enum 상수의 이름(name())을 직렬화한다. 즉, enum 상수 자체가 아닌 name() 값(String)만 저장된다.
  • 역직렬화(Deserialization)
    ObjectInputStream이 직렬화된 이름(String) 을 읽은 후, java.lang.Enum.valueOf(enumType, name) 메서드를 호출하여
    해당 enum 상수를 찾아 반환한다.

따라서 새로운 인스턴스가 생성되지 않고, 기존 인스턴스가 재활용된다.

'Java' 카테고리의 다른 글

[Java] 자바 객체의 Lock 과 Monitor 이해하기  (0) 2025.03.09
[Java] Blocking Queue 이해하고 사용해보기  (1) 2025.03.07
[Java] Callable, Feature 이해 및 사용예시  (0) 2025.02.26
[Java] Thread, Runnable 이해하고 사용하기  (1) 2025.02.21
자바의 정석 OOP(1)  (0) 2024.09.23
'Java' 카테고리의 다른 글
  • [Java] Blocking Queue 이해하고 사용해보기
  • [Java] Callable, Feature 이해 및 사용예시
  • [Java] Thread, Runnable 이해하고 사용하기
  • 자바의 정석 OOP(1)
기억은 RAM, 기록은 HDD
기억은 RAM, 기록은 HDD
  • 기억은 RAM, 기록은 HDD
    적립식 개발
    기억은 RAM, 기록은 HDD
  • 전체
    오늘
    어제
    • 분류 전체보기 (37)
      • Gradle (1)
      • 알고리즘 (14)
        • 강한 연결 요소 (1)
        • BFS (1)
        • 다이나믹 프로그래밍 (2)
        • 그리디 (1)
        • 투 포인터 (2)
        • 비트마스크 (1)
        • 스택 (1)
        • 백트래킹 (1)
        • 유니온-파인드 (1)
        • 기초 기하학 (1)
        • 분할정복을 이용한 거듭제곱 (1)
        • 볼록 껍질 (1)
      • JPA (3)
      • Java (8)
      • Spring (7)
      • Git&GitHub (1)
      • Infra (1)
  • 최근 글

  • 인기 글

  • 태그

    자바future
    @embedded
    데몬쓰레드
    api 문서 작성
    완전탐색
    enum정리
    기하학
    자바 callable
    투 포인터
    enum활용법
    정렬
    자바 runnable
    자바synchronized키워드
    java 라이브러리 추가
    swagger커스텀
    java synchronized
    통합테스트설정
    thread 와 runnable
    비트마스킹
    java
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.1
기억은 RAM, 기록은 HDD
[Java] 열거형(Enum) 이해하고 사용하기
테마상단으로

티스토리툴바