본문 바로가기
Java관련/Java

Java enum

by devstep 2022. 11. 4.

Java enum

enum을 잘 사용해보고 싶어서 여러 자료를 많이 보았습니다. 남궁성님의 자바의 정석, 백기선님의 유튜브, 이동욱님의 enum 활용법 정리를 통해 그동안 가진 enum에 대한 모호함이 많이 해소되는 느낌이였습니다.
아직도 명확하지 않은 것들은 차차 해결해보려고 합니다.

학습주제

  • Java enum 이란
    • 객체의 주소가 저장된 상수
    • 상태와 행위를 한 곳에서 관리
  • type safety
  • 변경 시 재컴파일 불필요
  • enum 정의하는 방법
  • java.lang.Enum
  • enum이 제공하는 메소드 (values()와 valueOf())
  • EnumSet

Java enum 이란

자바 Enum은 상수를 클래스로 정의해서 서로 관련된 상수를 편리하게 선언하기 위한 것으로 열거형이 갖는 값과 타입을 관리하기 때문에 논리적인 오류를 줄일 수 있습니다.

자바 enum이 클래스라는 점이 다른 언어의 enum과 구별되는 부분입니다. Java enum 내부 구현 모습과 상태와 행위를 한 곳에서 관리하는 것을 통해 enum이 클래스라는 점을 확인해보겠습니다.

1. 객체의 주소가 저장된 상수

Java enum의 내부 구현 모습을 확인해보겠습니다.

enum Direction { EAST, SOUTH, WEST, NORTH }

열거형 Direction의 EAST, SOUTH, WEST, NORTH 상수 각각은 Direction의 객체입니다.
클래스로 구현하면 아래와 같은 모습입니다.

public class Direction {
    static final Direction EAST = new Direction("EAST");
    static final Direction SOUTH = new Direction("SOUTH");
    static final Direction WEST = new Direction("WEST");
    static final Direction NORTH = new Direction("NORTH");

    private String name;

    private Direction(String name) {
        this.name = name;
    }
}

Direction 클래스의 static 상수 EAST, SOUTH, WEST, NORTH의 값은 객체 주소이고, 이 값은 바뀌지 않으므로 '=='으로 비교가 가능한 것입니다.

참고 : 열거형 상수 비교

  • 열거형 상수는 객체주소를 저장하였기 때문에 ==를 사용할 수 있습니다. equals()가 아닌 ==로 비교가 가능하다는 것은 빠른 성능을 제공한다는 것입니다.
  • 객체 주소를 저장한 상수이므로 < 또는 > 같은 비교 연산자는 사용할 수 없습니다
  • compareTo()는 사용할 수 있습니다. ordinal값 비교
  • switch문 조건식에 열거형을 사용할 수 있습니다. 대신 case문에 상수명만 적어야 합니다.
  • case CLOVER:

2. 상태와 행위를 한 곳에서 관리

추상메서드 활용하거나 인스턴스 변수에 계산식을 가져 상수별로 행위를 달리할 수 있습니다. 즉 값이라는 상태와 함께 행위를 enum 클래스에 갖게 하여 클래스의 역할과 책임을 담당할 수 있습니다.

2-1. Java8버젼부터 : 상수가 계산식을 갖도록 지정

상수가 계산식 갖도록 enum 정의

public enum CalculatorType {
    CALC_A(value -> value), 
    CALC_B(value -> value * 10), 
    CALC_C(value -> value * 3), 
    CALC_D(value -> 0L);

    private Function<Long, Long> expression;

    CalculatorType(Function<Long, Long> expression) {
      this.expression = expression;
    }

    public Long calculate(Long value) {
      return expression.apply(value);
    }
}
  • 실행 코드
public class EnumExam3 {
    public static void main(String[] args) {
        Long originValue = 10_000L;

        CalculatorType codeA = CalculatorType.CALC_A;
        Long resultA = codeA.calculate(originValue);

        CalculatorType codeC = CalculatorType.CALC_C;
        Long resultC = codeC.calculate(originValue);

        System.out.println("resultA:" + resultA + ", resultC:" + resultC);
    }
}
  • output

    resultA:10000, resultC:30000

2-2. Java7버젼까지 : 추상메서드 선언

열거형에 추상메서드를 선언하면 각 열거형 상수가 추상 메서드를 반드시 구현해야 합니다.
그럼으로써 클래스의 행위를 표현할 수 있습니다.

public enum CalculatorType {

  CALC_A{Long calculate(Long value){return value;}},
  CALC_B{Long calculate(Long value){return value * 10;}},
  CALC_C{Long calculate(Long value){return value * 3;}},
  CALC_D{Long calculate(Long value){return 0L;}};

  abstract Long calculate(Long value);
}

type safety

자바의 열거형은 타입까지 체크하기 때문에 타입에 안전합니다. 실제 값이 같아도 타입이 다르면 컴파일 에러가 발생하는데 아래 코드에서 확인 할 수 있습니다.

class Card {
    enum Kind {CLOVER, HEART, DIAMOND, SPADE}
    enum Value {TWO, THREE, FOUR}

    final Kind kind;  
    final Value value;
}

//컴파일 에러. 값은 같지만 타입이 다르다.
if(Card.Kind.CLOVER == Card.Value.TWO) 

type safe가 보장되지 않는 케이스

여기서의 "hello" 메세지는 Typesafe 하지 못합니다. 오타가 나더라도 컴파일러는 알 수 없기 때문입니다.

public static void main(String[] args){
    System.out.println("hello");
}

Type safe 보장

Typesafe 하지 못한 단순한 문자열 "hello" 를 Greet 라는 열거형 하나의 타입으로 정의함으로 Typesafe한 방법으로 문자열을 다룰 수 있습니다.

public enum Greet {
    HELLO("hello");

    String message;

    Greet(String message){
            this.message = message;
    }

    public String getMessage(){
      return message;
    }
}

public static void main(String[] args){
    System.out.println(Greet.Hello.getMessage());
}

변경 시 재컴파일 불필요

상수의 값이 바뀌면 해당 상수를 참조하는 모든 소스를 다시 컴파일해야 합니다. 하지만 enum을 사용하면 기존 소스를 다시 컴파일 하지 않아도 됩니다.

  • public static final 을 사용해 상수를 선언했을 때, 상수가 변경되면 상수를 사용하는 쪽은 모두 재컴파일 되어야 한다. 왜일까?

enum 정의하는 방법

enum 정의하는 방법은 {} 안에 상수 이름을 나열하면 됩니다.

enum 열거형이름 {상수명1, 상수명2, ...}

enum Kind {CLOVER, HEART, DIAMOND, SPADE}
enum Value {TWO, THREE, FOUR}

사용하는 방법열거형이름.상수명으로 static 변수 참조하는 것과 동일합니다.

열거형 상수는 필드를 가질 수 있습니다.

1) 추가할 필드를 정의하고, 2) 생성자 인자로 추가한 후, 3) 각각의 상수에 값을 입력하면 됩니다.
예시는 int 타입을 필드로 갖는 열거형입니다.

enum Direction {
  EAST(1), SOUTH(5), WEST(-1), NORTH(10);

  private int value;
  Direction (int value) {
    this.value = value;
  }

  public int getValue() { return value; }
}

java.lang.Enum

모든 열거형의 조상

java.lang.Enum이 제공하는 메소드

public final String name()
public final int ordinal()
public String toString()
public final boolean equals(Object obj)
public final int hashCode()
public int compareTo(E obj)
public static <T extends Enum> T valueOf(Class enumType,String name)
public final Class <E> getDeclaringClass() // enum 객체의 타입 반환
public final Object clone() throws CloneNotSupportedException
protected final void finalize()
메서드 설명
Class getDeclaringClass() 열거형의 Class객체 반환
String name() 열거형 상수의 이름 반환
int ordinal() 열거형 상수가 정의된 순서 반환 (0부터 시작)
T valueOf(Class enumType, String name) 지정된 열거형에서 name과 일치하는 열거형 상수 반환
compareTo(Class enumType) 비교가능

enum이 제공하는 메소드 (values()와 valueOf())

모든 열거형이 가지고 있는 메서드로 컴파일러가 자동으로 추가해주는 메서드입니다.

  • 열거형 상수명으로 문자열 상수에 대한 참조를 얻을 수 있게 해준다.
values()
  • 코드를 확인해보면 java.lang.Enum 클래스에 정의되어 있지 않다.
  • 컴파일타임에 바이트코드가 추가된다. (신기하네. 왜그런거지?)
  • Enum 클래스를 컴파일해서 바이트코드를 확인해보자.
valueOf(String name)
enum Direction {EAST, SOUTH, WEST, NORTH}

Direction direction = Direction.valueOf("WEST")
System.out.printnln(direction); //WEST
System.out.println(Direction.WEST == Direction.valueOf("WEST")); //true

EnumSet

EnumSet은 Set 인터페이스를 기반으로 Enum Type을 사용하는 방법입니다.

제공 메서드

메서드 설명
allOf(Class elementType 인자로 들어온 enum을 enum set으로 생성
complementOf(EnumSet s) 인자로 들어온 EnumSet에 없는 요소들로 enum set 생성
of(E e1,...) 지정된 값으로 enum set 생성
noneOf() 빈 enum set 생성
range(E from, E to) 범위 안에 있는 값들로 enum set 생성

장점

  • 사용자는 어떤 구현 객체가 적합한지 몰라도 된다.
    • EnumSet 구현 객체가 여러 개 있는데 EnumSet사용자 입장은 어떤 구현체가 적합한지 고려하지 않아도 된다. (이펙티브 자바에도 나온다)
    • 아래 예시코드처럼 new로 만들면 구현체를 명시하기 때문에 바꾸기 어려운데 allOf()를 사용하면 Set 크기에 알맞은 구현체를 사용한다.
public class EnumExam2 {
    enum Fruit {KIWI, BANANA, MELON, APPLE}

    public static void main(String[] args) {
        /* 아래 코드는 EnumSet을 사용하여 대체할 수 있다.
        Set<Fruit> fruits = new HashSet<>();
        fruits.add(Fruit.APPLE);
        fruits.add(Fruit.BANANA);*/

        //EnumSet의 allOf()메서드를 사용하여 Set에 enum 객체를 넣음.
        EnumSet<Fruit> enumSet = EnumSet.allOf(Fruit.class);
        System.out.println(enumSet.size());
    }
}

싱글톤을 만드는 방법 Enum

대부분의 상황에서 원소가 하나인 enum타입이 싱글톤을 만드는 가장 좋은 방법입니다.
그러나 열거 타입은 인터페이스를 구현할 수는 있으나, 만드려는 싱글턴이 다른 클래스를 상속해야 한다면 이 방법은 사용할 수 없습니다.

그리고 스프링을 이용하여 개발하면 싱글톤을 위한 클래스를 따로 정의하지 않기 때문에 잘 사용하지 않는다고 합니다.

public enum Elvis {
    INSTANCE;

}

장점

이펙티브 자바의 싱글톤관련 아이템을 보면 아래와 같은 장점들이 나옵니다. 직렬화나 리플렉션에 대한 이해가 있어야하지만 간단히 적어봅니다.

  • public 필드 방식과 비슷하지만 더 간결하다
  • 추가 조건없이 직렬화할 수 있다.
  • 복잡한 직렬화 상황이나 리플렉션 공격에도 다른 인스턴스가 생기는 일을 완벽히 막아준다.

참고자료

'Java관련 > Java' 카테고리의 다른 글

Java 제네릭  (0) 2022.11.28
Java 애너테이션  (0) 2022.11.23
Java 클래스  (0) 2022.09.30
Java 예외 처리  (0) 2022.09.29
Java static, static final, final 차이  (0) 2022.09.15

댓글