nathan_H

[Java] Enum을 잡아보자 본문

Programming Laguage/Java

[Java] Enum을 잡아보자

nathan_H 2020. 4. 25. 21:52

Enum이란?


  • Enum은 일정 개수의 상수 값을 정의한 다음, 그 외의 값을 허용하지 않는 타입
  • C, C++, C# 와 같은 언어에도 Enum 이 존재함

정수 열거형


  • 자바는 과거 Enum 타입을 지원하지 않았는데, 그래서 아래와 같이 정수 상수 값을 한 묶으로 사용해 선언하곤 했다.
public static final int GALAXY_NOTE = 0;
public static final int GALAXY_TAP  = 1;
public static final int GALAXY_S    = 2;

public static final int APPLE_IPONE = 0;
public static final int APPLE_IPAD  = 1;
public static final int APPLE_SE    = 2;
  • 그리고 위와 같은 정수 열거형 사용시 다음과 같은 문제들이 발생할 수 있다.

  • 타입 안전성을 보장할 방법이 없음

    • 가령 아래와 같은 코드를 작성 시 GALAXY_NOTEAPPLE_IPONE 동등 연산자를 통해 비교시 컴파일러는 아무런 경고를 출력하지 않고 프로그램이 유지됨
  • 표현력이 좋지 않다

    • 자바에서는 정수 열거형에 대한 별도의 namespace를 제공하지 않기 때문에 접두어를 통해 구별을 해 표현력이 좋지 못함
  • 프로그램이 깨지기 쉬움

    • 해당 문제가 정수 열거형 사용시 가장 큰 문제점으로 평범한 상수를 나열한 것이기 때문에 컴파일 하면 그 값이 클라이언트 파일에 그대로 새겨져 디버깅이 어렵고 프로그램도 깨지기 쉬움
    • 또한 상수값이 바뀌면 클라이언트는 반드시 컴파일 해야한다. 만약 컴파일을 하지 않을 시 엉뚱하게 동작할 수 있음
  • 상수가 몇 개인지 파악하기 어려움

    • 해당 상수의 개수를 파악하기 위해서 별도의 메소드를 구현해 파악은 가능하나 위와 같이 접두어를 통해 구별하기 때문에 명확하게 상수의 개수를 파악하기 어려움

그리고 위와 같은 그래서 자바에서는 정수 열거형의 문제점을 해결 해주는 Enum이 등장을 했고, 자바에서의 Enum 은 다른 언어에서의 Enum과 달리 더욱 강력하다.

자바에서의 Enum


  • 위에 정수 열거형의 코드를 Enum을 활용해 아래와 같이 작성할 수 있고, 몇가지 예시를 통해 자바 Enum에 대해 알아보고 Enum을 사용하면 어떤 이점이 있는지 알아보자.
enum GALAXY {
    NOTE, TAP, S
}

enum Apple {
   IPONE, IPAD, SE
}

자바 Enum 알아보기


  • 자바에서의 Enum은 클래스이며, 상수 하나당 자신의 인스턴스를 하니씩 만들어 public static final 필드로 공개
    • 열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final
    • 따라서 클라이언트가 인스턴스를 직접 생성하거나 확장 할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함이 보장
  • 열거 타입은 인스턴스 통제
    • 싱글턴은 원소가 하나뿐인 열거 타입이라 할 수 있고, 열거 타입은 싱글턴을 일반화한 형태라고 볼 수 있다.

자바 Enum의 장점


  • 자바의 Enum은 위에서 언급한 자바 정수 열거형이 가장 문제점을 완벽히 커버하고 다른 언어의 enum과 달리 클래스로 취급이 되기 때문에 많은 장점을 가지고 있음.

  • 컴파일 타입 안정성 제공

    • 위 코드에서 Apple Enum 타입을 매개변수를 받는 메서드를 선언했다면, 세 가지 값 중 하나 임이 확실
      • 만약 다른 타입의 값을 넘기려 하면 컴파일 오류가 남
    • 타입이 다른 열거 타입 변수에 할당하려 하거나 다른 열거 타입의 값끼리 == 연산자로 비교하려는 꼴이기 때문에 타입의 안전성이 제공이 됨
  • 각자의 이름 공간이 있어서 이름이 같은 상수도 평화롭게 공존

    • 자바 정수 열거형과 달리 별도의 Enum 클래스를 통해 각자의 이름공간이 존재해 같은 이름이 존재해도 문제없이 작동이 됨
  • 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 됨

    • 공개되는 것이 오직 필드의 이름뿐이라, 정수 열거 패턴과 달리 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문에 새로운 상수나 순서를 바꾸더라도 클라이언트는 다시 컴파일 필요가 없음
  • 열거 타입의 toString 메서드는 출력하기에 적합한 문자열을 내어줌

  • 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수도 있음

    • 열거 타입은 가장 단순하게는 그저 상수 모음일 뿐인 열거 타입이지만(실제로는 클래스이므로) 고차원의 추상 개념 하나를 완벽히 표현해낼 수도 있음
    • 해당 장점은 아래 예시를 통해 살펴보고자 함

Enum메소드


  • Enum은 위에서도 언급 했듯이 원래는 클래스이기 때문에 다음과 같은 메소드를 가지고 있고, 메소드를 잘 활용한다면 Enum을 다양하게 이용할 수 있다.

Enum Method

  • 가령 예를 들어 한번에 Apple에 있는 제품의 이름과 가격을 출력한다고 했을 때, 아래와 같이 간단한 코드로 끝이 난다.
public enum Apple {
    IPHONE("백만원"),
    IPAD("백만원"),
    SE("오십만원"),
    MAC("삼백만원");

    private final String price;

    Apple(String price) {
        this.price = price;
    }
}
        public static void main(String[] args) {
        for (Apple product : Apple.values()) {
            System.out.printf("%s의 가격은 %s입니다.\n",product, product.price);
        }
    }
IPHONE의 가격은 백만원입니다.
IPAD의 가격은 백만원입니다.
SE의 가격은 오십만원입니다.
MAC의 가격은 삼백만원입니다.

필드와 메서드를 갖는 열거 타입


  • 자바의 Enum은 필드와 메서드를 가질 수 있기 때문에, 상수의 상태와 행위를 한 곳에 모아서 관리하고 실행할 수 있음
  • 또한 상수마다 다른 동작을 수행할 수 있고, 상수에게 책임을 부여할 수 있게 됨
  • 가령 "+-*/" 연산을 하는 수행 한다고 해보자. 그리고 가장 간단한 해결 방법은 아래와 같이 static method와 같이 연산을 코드를 짜는 것이다
public class Operator {

    public static double operate(double x, double y, String operator) {
        if (operator.equals("+")) {
            return x + y;
        } else if (operator.equals("-")) {
            return  x - y;
        } else if (operator.equals("*")) {
            return x * y;
        } else {
            return x / y;
        }
    }
}

하지만 열거 타입을 활용한다면 아래와 같이 짤 수도 있다.

public enum  OperationCaseOne {
    PLUS, MINUS, TIMES, DIVIDE;

    // 상수가 뜻하는 연산을 수행한다.
    public double apply(double x, double y) {
        switch (this) {
            case PLUS: return x + y;
            case MINUS: return  x - y;
            case TIMES: return  x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("알 수 없는 연산: " + this)
    }
}
  • 그러나 위 예시는 동작은 하지만 몇 가지 문제가 발생할 수 있다.

  • throw 문은 실제로는 도달할 일이 없지만 기술적으로는 도달할 수 있기 때문에 생략하면 컴파일조차 되지 않음

  • 깨지기 쉬운 코드임

    • 새로운 상수를 추가하면 해당 case 문도 추가해야 함.
    • 만약 깜박한다면 컴파일은 되지만 새로 추가한 연산을 수행 시 "알 수 없는 연산"이라는 런타임 오류를 냄

Enum의 추상 메서드


  • 그래서 추상 메서드를 선언해서 각 상수에서 자신에 맞게 재 정의함으로써 위와 같은 문제를 해결할 수 있다.
public enum OperationType {
    PLUS("+") {
        public double operate(double x , double y) { return x + y; }
    },
    MINUS("-") {
        public double operate(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double operate(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double operate(double x, double y) {
            if (y == 0) {
                throw new ArithmeticException("0으로 나눌 수 없습니다.");
            }
            return x / y;
        }
    };
    private String symbol;

    OperationType(String symbol) {
        this.symbol = symbol;
    }
}
  • 또한 나누기 연산의 경우 0으로 나누는 연산이 수행될 시, "나누기 연산"에게만 예외를 발생해 책임을 분리할 수 있다.

출력

   public static void main(String[] args) {
        double x = 10;
        double y = 2;
        for (OperationType op : OperationType.values()) {
            System.out.printf("%f %s %f = %f\n", x, op.symbol, y, op.operate(x, y));
        }
    }
10.000000 + 2.000000 = 12.000000
10.000000 - 2.000000 = 8.000000
10.000000 * 2.000000 = 20.000000
10.000000 / 2.000000 = 5.000000

Enum 추상 메소드 활용


  • OperationType 사용 시 아래와 같이 매개변수 혹은 주어진 연산자 값에 대해 OperationType에서 해당하는 연산자를 찾아 연산을 수행할 수 있을까?

  • Enum이 가진 메서드나 이해도가 높지 않다면 아래와 같은 코드를 작성할 수 있다. (내가 바로 그랬다)

if (operatorSymbol.equals("+")) { result = Operation.PLUS.apply(x, y); }
            if (operatorSymbol.equals("-")) { result = Operation.MINUS.apply(x, y); }
            if (operatorSymbol.equals("*")) { result = Operation.TIMES.apply(x, y); }
            if (operatorSymbol.equals("/")) { result = Operation.DIVIDE.apply(x, y); }

values & stream 활용

  • 그러나 Enumvalue메소드를 활용한다면 아래와 같이 간결하게 연산자를 구별해 연산을 수행할 수 있다.
public static OperationType fromString(String symbol) {
        return Arrays.stream(Operation.values())
                .filter(value -> value.symbol.equals(symbol))
                .findFirst()
                .orElseThrow(() -> new NoSuchElementException("해당 연산자가 없습니다."));
    }
  • 위 코드는 자바 8부터 지원하는 stream을 활용해 매개변수로 들어오는 Symbol에 해당하는 연산자를 찾아 OprationType해주는 메소드이다.
  • 그리고 실제 수행을 해보면 아래와 같이 잘 출력됨을 확인할 수 있다.
public static void main(String[] args) {
        String operator = "+";
        double x = 1.0;
        double y = 20.0;
        double result = Operation.fromString(operator).operate(x, y);

        System.out.printf("%f %s %f = %f\n", x, Operation.fromString(operator), y, result);
    }
1.000000 + 20.000000 = 21.000000

// toString()을 오버라이딩하면 위와 같이 바로 OperationType.symbol로 변환해 출력할 수 있다.

HashMap 활용

  • 그리고 추가로 아래와 같이 정적 변수로 <OperationType symbol, OperationType>형태의 HaspMap을 활용해 아래와 같이 작성할 수도 있다
  • 정답은 없기에 목적이나 개인의 성향에 따라 작성하면 된다
import java.util.HashMap;
import java.util.Map;

public enum OperationType {
    PLUS("+") {
        public double operate(double x , double y) { return x + y; }
    },
    MINUS("-") {
        public double operate(double x, double y) { return x - y; }
    },
    TIMES("*") {
        public double operate(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        public double operate(double x, double y) {
            if (y == 0) {
                throw new ArithmeticException("0으로 나눌 수 없습니다.");
            }
            return x / y;
        }
    };

    private String symbol;
    private static final Map<String, OperationType> BY_SYMBOL = new HashMap<>();

    OperationType(String symbol) {
        this.symbol = symbol;
    }

    static {
        for (OperationType operationType : values()) {
            BY_SYMBOL.put(operationType.symbol, operationType);
        }
    }

    public static boolean isOperator(String symbol) {
        return BY_SYMBOL.containsKey(symbol);
    }

    public static OperationType getType(String symbol) {
        return BY_SYMBOL.get(symbol);
    }

    public abstract double operate(double x, double y);
}
  • 개인적으로 HashMap()을 활용한다면 위에서 stream과 같이 매번 반복문을 돌리지 않고, 캐싱을 통해 바로 연산자를 찾아 연산을 수행하는 장점이 있음

마무리


아직까지 Enum에 대한 이해도가 많이 떨어져서 지금 당장 많은 부분에 활용은 하지 못할 것 같지만 Enum이 가진 장점 확실하기 때문에 추후에 더 공부를 해서 의미를 제대로 파악해 나간다면 보다 효율적으로 코드를 작성하고 유지보수를 할 수 있을 것 같다. 그리고 추가로 Enum을 적용한 다양한 사례가 궁금하다면 아래 참고한 책과 링크를 참고하길 바란다.

참고

Enum을 활용한 계산기 깃헙 링크

이펙티브 자바 Effective Java 3/E

Java Enum 활용기 - 우아한형제들 기술 블로그

Comments