Java Enum Class 정리

Enum 사용법에 대하여 정리 및 사내 공유를 위해 작성

  • 해당 글의 용어 및 내용은 Java Enum 활용기 - 우아한형제들 기술 블로그를 많이 참고 하였습니다.

  • Enum이란 ?

    Enumeration은 프로그래밍언어에서 상수의 그룹을 나타낼 때 사용한다. Enum은 컴파일 당시 우리가 모든 가능한 값을 알고있는 경우 사용된다. 항상 enum안의 상수는 타입이 정해져 있어야 되는 것은 아니다.
    Java 1.5버전 부터, enum은 enum 데이터 타입이라고 표시되었다. 자바 enum은 C/C++ enum보다 더 강력한 기능을 제공한다. 자바에서 변수, 메소드 그리고 생성자를 추가할 수 있다. enum의 주된 목적은 우리만의 데이터 타입을 가지기 위해서이다. (Enumberated Data Type)
    참고 자바의 enum

  • 위 정의를 보더라도 Enum은 상수의 그룹을 나타내기 위하여 사용한다. 라고 선언 되어 있습니다.
    과연 상수만을 위하여 Enum을 쓰는것 보다 Enum을 더욱 강력히 사용할 수 없을까? 라는 생각을 가지고 이 글을 보시면 더욱 좋을것 같습니다.

  • 들어가기 앞서 객체는 상태와 행위를 가진다. 라는 개념에 대해 알고 계시는것이 좋을 것 같습니다.

Enum - 상수의 집합 ?

  • 사칙연산을 위한 프로그램을 만든다 할때 연산자를 아래와 같은 Enum으로 구분하여 사용할 수 있을것 같습니다.
  • OperatorType은 사칙연산을 위한 연산자를 가지고 있고 여러 클래스에 선언되어 사용하지 않고 연산자를 Enum으로 관리하여 응집도를 높였다고 볼수 있겠네요.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public enum OperatorType {
      PLUS_OPERATOR("+"),
      MINUS_OPERATOR("-"),
      MULTIPLY_OPERATOR("*"),
      DIVIDE_OPERATOR("/");
    
      private String operator;
    
      OperatorType(String operator) {
          this.operator = operator;
      }
    
      public boolean isEqual(String operator) {
          return this.operator.equals(operator);
      }
    }
    
  • OperatorType은 과연 상태와 행위를 가지는 객체 인가요 ?
    • 코드를 보시고 상태만 가지고 있다. 라고 생각이 든다면 정답입니다.
  • 과연 행위는 어디에 있을까 ?
    • 어디선가 아래와 같은 코드를 가지고 있는 클래스를 만들어서 사용해야 할것 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public double calculate() {
    validateOperator();
    String operator = findOperator(); // +, -, %, *

    if (PLUS_OPERATOR.isEqual(operator)) {
        return plus();
    }

    if (MINUS_OPERATOR.isEqual(operator)) {
        return minus();
    }

    if (MULTIPLY_OPERATOR.isEqual(operator)) {
        return multiply();
    }

    if (DIVIDE_OPERATOR.isEqual(operator)) {
        return divide();
    }

    throw new IllegalArgumentException("사칙연산 기호가 아닙니다.");
}
  • 다시 처음 이야기한 객체의 정의에 대해서 생각해봅시다.
    • 객체는 상태와, 행위를 가진다.
    • OperatorType은 행위를 가지고 있지 않으니 위 코드에 포함된 행위를 OperatorType 으로 옮겨 보겠습니다.
  • 위 코드에서의 문제점은 무엇일까요 ?
    • 곰곰히 생각을 해봅시다 : )

Enum - 상태와 행위를 한곳에

  • 상태와 행위를 한곳에 모아둔 OperatorType을 먼저 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public enum OperatorType {
    PLUS_OPERATOR("+") {
        @Override
        public double calculate(double sum, double nextNumber) {
            return sum + nextNumber;
        }
    },
    MINUS_OPERATOR("-") {
        @Override
        public double calculate(double sum, double nextNumber) {
            return sum - nextNumber;
        }
    },
    MULTIPLY_OPERATOR("*") {
        @Override
        public double calculate(double sum, double nextNumber) {
            return sum * nextNumber;
        }
    },
    DIVIDE_OPERATOR("/") {
        @Override
        public double calculate(double sum, double nextNumber) {
            if (nextNumber == 0) {
                throw new IllegalArgumentException("0 으로 나눌수 없습니다.");
            }
            return sum / nextNumber;
        }
    };

    private String operator;

    OperatorType(String operator) {
        this.operator = operator;
    }

    public static OperatorType createOperator(String operator) {
        if(operator.equals(PLUS_OPERATOR.operator)) {
            return PLUS_OPERATOR;
        }

        if(operator.equals(MINUS_OPERATOR.operator)) {
            return MINUS_OPERATOR;
        }

        if(operator.equals(MULTIPLY_OPERATOR.operator)) {
            return MULTIPLY_OPERATOR;
        }

        if(operator.equals(DIVIDE_OPERATOR.operator)) {
            return DIVIDE_OPERATOR;
        }

        throw new IllegalArgumentException("사칙연산 기호가 아닙니다.");
    }

    public abstract double calculate(double sum, double nextNumber);
}
  • 이제 OperatorType 내부에 선언되어있는 객체들은 calculate라는 행위를 가지게 되었습니다.
    이로 인해서 처음에 이야기 했던 객체의 정의를 만족 하게 되었네요 !
  • 이전에 행위를 구현했던 코드는 어떻게 변했을까요 ?

    참고 Java7 부터 Enum 상수에 추상 메소드를 구현할 수 있습니다.

1
2
3
4
5
6
7
public double calculate() {
    validateOperator();
    String operator = findOperator(); // +, -, %, *
    OperatorType operatorType = createOperator(operator);

    return operatorType.calculate(sum, nextNumber());
}
  • 이전엔 Operator 별로 분기를 타며 계산을 하던 메소드가 실제 계산 하라 라는 메시지를 OperatorType에 던지는 단순하고 깔끔한 메소드로 변경 되었습니다.
  • 여기까지만 보더라도 연산자를 가지고 있는 OperatorType은 자신이 가지고 있는 책임인 계산한다 라는것을 완벽하게 이행 함으로서 외부에선 모든 사칙연산에 관한 계산을 OperatorType을 사용할 수 있게 되었습니다.
  • 이전에 상태와 행위를 한곳에서 관리 하지 않은 코드를 생각해보시면, 계산 하라라는 메시지를 누구에게도 던지지 않고 전혀 의미 없는 클래스에서 행위를 구현하여, 다른 클래스에서의 중복이 발생할 가능성이 컸습니다.
  • 아직 남아 있는 문제점.
  • 보시다 시피 Enum 내부 정적 메소드에 파라미터로 전달 받은 Operator에 맞는 상수를 찾아 전달 하고 있습니다.
    위와 같은 분기문이 나열되어 있는 것이 좋은 패턴일까요 ?

Enum 리팩토링 - 분기문을 없애보자.

  • 분기문을 없애면서 저희는 Java 8 이상을 사용하는 개발자들로 추상메소드를 사용한 방식보다 Lambda를 사용한 OperatorType으로 리팩토링 해보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public enum OperatorType {
    PLUS_OPERATOR("+", (sum, nextNumber) -> sum + nextNumber),
    MINUS_OPERATOR("-", (sum, nextNumber) -> sum - nextNumber),
    MULTIPLY_OPERATOR("*", (sum, nextNumber) -> sum * nextNumber),
    DIVIDE_OPERATOR("/", (sum, nextNumber) -> {
        if (nextNumber == 0) {
            throw new IllegalArgumentException("0 으로 나눌수 없습니다.");
        }
        return sum / nextNumber;
    });

    private String operator;
    private Calculation calculation;

    OperatorType(String operator, Calculation calculation) {
        this.operator = operator;
        this.calculation = calculation;
    }

    public static OperatorType findOperator(String operator) {
        if (Objects.isNull(operator)) {
            throw new IllegalArgumentException("연산자는 null 이 될 수 없습니다.");
        }

        return Arrays.asList(values()).stream()
                .filter(operatorType -> operatorType.operator.equals(operator))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("사칙연산 기호가 아닙니다."));
    }

    public double calculate(double sum, double nextNumber) {
        return calculation.calculate(sum, nextNumber);
    }
}
1
2
3
4
@FunctionalInterface
public interface Calculation {
    double calculate(double sum, double nextNumber);
}
  • 뭔가 더욱 깔끔하게 리팩토링된것이 느껴진다면 성공입니다 : )
  • 추상메소드를 사용한 방식 대신 Lambda로 calculate를 구현 하였습니다.
    • Lambda를 사용하기 위하여 @FunctionalInterface를 만들었습니다.
      • @FunctionalInterface는 메소드 1개를 가진 인터페이스 입니다.
    • Java가 제공하는 함수형 인터페이스도 사용할 수 있습니다. Function<T, R> 은 매개변수 1개를 지원하고 2개는 BiFunction을 사용하셔도 됩니다.
  • 분기문이 없어진 것을 확인 할 수 있습니다.
    • Stream 을 사용하여 분기문을 제거하고 코드의 가독성을 높혀 다른 개발자들이 보더라도 어떤 기능을 하는지 쉽게 알수 있게 되었습니다. : )
  • 이처럼 리팩토링을 하게 되어 얻을수 있는 가장 큰 이점은 재사용성과 확장성을 얻을 수 있게 되었다는 것 입니다.
  • 다른 클래스에서 사칙연산을 하기 위해선 OperatorType만 사용하면 될 것이고 새로운 연산자가 추가 되더라도
    복잡한 분기문들을 제거 하였기 때문에 상수 정의만 해준다면 어디서든 정상적으로 동작하겠죠 :)

  • 예제를 사칙연산으로 들었지만 Enum을 사용하는 어느 곳이라도 비슷하게 적용할 수 있을것이라 생각하여 공유드렸습니다.