[Java] Enum을 도대체 왜 쓰는 것일까?

필자는 처음에 Enum의 필요성을 잘 못느꼈다. 그냥 상수들을 모아놓은 것 뿐인데 왜 사용하는 건지 그 이유를 모르겠었더랬다. 하지만 이 포스팅을 써서 Enum을 사용하는 이유를 한번 정리해보겠다.


🧐 Enum이란 무엇인가?

오라클 공식 문서에 따르면, enum은 “미리 정의된 상수들의 특별한 집합”이다. 영어 단어 ‘enumeration’의 줄임말로, 말 그대로 상수들을 열거한 것이다.

Enum의 특징:

  1. 외부에서 추가 객체 생성 없이 사용 가능
  2. 불변(immutable)이며, 암시적으로 public static final
  3. 대문자로 작성하는 것이 관례

기본적인 Enum 작성 예시:

1
2
3
4
public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY;
}

열거 시에는 쉼표(,)로 구분하고, 마지막에는 세미콜론(;)으로 끝낸다.

Enum의 고급 기능: 생성자와 메서드

Enum은 단순히 상수의 나열을 넘어 클래스처럼 생성자와 메서드를 가질 수 있다. 이를 통해 더 복잡한 로직을 구현할 수 있다.

행성의 질량과 반지름을 이용한 Enum 예시:

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
public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS  (4.869e+24, 6.0518e6),
    EARTH  (5.976e+24, 6.37814e6),
    MARS   (6.421e+23, 3.3972e6),
    JUPITER(1.9e+27,   7.1492e7),
    SATURN (5.688e+26, 6.0268e7),
    URANUS (8.686e+25, 2.5559e7),
    NEPTUNE(1.024e+26, 2.4746e7);

    private final double mass;   // 킬로그램 단위
    private final double radius; // 미터 단위
    
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    
    private double mass() { return mass; }
    private double radius() { return radius; }

    // 만유인력 상수 (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
    
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           System.out.printf("Your weight on %s is %f%n",
                             p, p.surfaceWeight(mass));
    }
}

이 예제에서 각 행성은 질량과 반지름 값을 가지며, 이를 이용해 표면 중력과 무게를 계산할 수 있다. values() 메서드를 사용하면 모든 enum 상수를 배열로 얻을 수 있어 순회가 가능하다.


🌟 Enum을 사용해야 하는 이유

우아한형제들 기술 블로그에서 언급된 enum 사용의 주요 이점은 다음과 같다:

  1. IDE의 강력한 지원
    • 자동 완성, 오타 검증, 리팩토링 등
  2. 허용 가능한 값들의 제한
  3. 리팩토링 시 변경 범위 최소화
  4. 내용 추가 시 Enum 코드 외 수정 불필요

이러한 이점들을 실제 사례를 통해 살펴보겠다.


🔗 데이터 간 연관관계 표현

정산 시스템에서 다양한 테이블 간 데이터 변환이 필요한 경우를 생각해보자.

기존 방식:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LegacyCase {
    public String toTable1Value(String originValue) {
        if("Y".equals(originValue)) {
            return "1";
        } else {
            return "0";
        }
    }
    
    public String toTable2Value(String originValue) {
        if("Y".equals(originValue)) {
            return "true";
        } else {
            return "false";
        }
    }
}

이 방식의 문제점:

  • 메서드를 일일이 만들어야 하는 번거로움
  • 동일한 의미의 값들(“Y”, “1”, true)을 일관되게 관리하기 어려움

Enum을 사용한 개선:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum TableStatus {
    Y("1", true),
    N("0", false);
    
    private String table1Value;
    private boolean table2Value;
    
    TableStatus(String table1Value, boolean table2Value) {
        this.table1Value = table1Value;
        this.table2Value = table2Value;
    }
    
    public String getTable1Value() {
        return table1Value;
    }
    
    public boolean isTable2Value() {
        return table2Value;
    }
}

사용 예:

1
2
String table1Value = TableStatus.Y.getTable1Value();
boolean table2Value = TableStatus.Y.isTable2Value();

이렇게 하면 관련된 값들을 한 곳에서 관리할 수 있어 일관성 유지와 유지보수가 쉬워진다.


🔄 상태와 행위의 통합 관리

간단한 사칙연산 계산기를 예로 들어보겠다.

기존 방식:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LegacyOperation {
    private String operator;
    
    public LegacyOperation(String operator) {
        this.operator = operator;
    }
    
    public double apply(double a, double b) {
        switch (operator) {
            case "+":
                return a + b;
            case "-":
                return a - b;
            case "*":
                return a * b;
            case "/":
                return a / b;
        }
        throw new AssertionError("Not handled operation : " + operator);
    }
}

Enum을 사용한 개선 (Java 8 이상):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum Operation {
    PLUS((a, b) -> a + b),
    MINUS((a, b) -> a - b),
    TIMES((a, b) -> a * b),
    DIVIDE((a, b) -> a / b);

    private final BiFunction<Double, Double, Double> operation;

    Operation(BiFunction<Double, Double, Double> operation) {
        this.operation = operation;
    }

    public Double compute(Double x, Double y) {
        return operation.apply(x, y);
    }
}

이 방식의 장점:

  • 각 연산자와 그에 대한 동작이 하나의 enum 상수로 캡슐화됨
  • 람다 표현식을 사용해 간결하고 명확한 코드 작성 가능
  • 새로운 연산 추가 시 enum에만 추가하면 됨

사용 예:

1
double result = Operation.PLUS.compute(5.0, 3.0);

📊 데이터 그룹 관리

결제 시스템에서 다양한 결제 수단을 관리해야 하는 경우를 생각해보자.

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
public enum PayGroup {
    CASH("현금", Arrays.asList("ACCOUNT_TRANSFER", "REMITTANCE", "ON_SITE_PAYMENT")),
    CARD("카드", Arrays.asList("CREDIT_CARD", "KAKAO_PAY", "PAYCO")),
    ETC("기타", Arrays.asList("POINT", "COUPON")),
    EMPTY("없음", Collections.emptyList());

    private String title;
    private List<String> payList;

    PayGroup(String title, List<String> payList) {
        this.title = title;
        this.payList = payList;
    }

    public static PayGroup findByPayCode(String code){
        return Arrays.stream(PayGroup.values())
            .filter(payGroup -> payGroup.hasPayCode(code))
            .findAny()
            .orElse(EMPTY);
    }

    public boolean hasPayCode(String code){
        return payList.stream()
            .anyMatch(pay -> pay.equals(code));
    }
}

이 방식의 장점:

  • 결제 수단을 그룹화하여 관리 용이
  • 새로운 결제 수단 추가 시 해당 그룹의 enum 상수만 수정하면 됨
  • findByPayCode 메서드로 코드에 해당하는 그룹을 쉽게 찾을 수 있음

🎯 결론

Enum은 단순한 상수 그룹화 이상의 기능을 제공한다. 타입 안정성(type-safety)을 보장하여 컴파일 시점에서 오류를 잡을 수 있고, 코드의 가독성과 유지보수성을 크게 향상시킨다.

Enum을 적절히 활용하면:

  1. 관련된 상수들을 논리적으로 그룹화할 수 있다.
  2. 특정 데이터 타입에 대한 가능한 모든 값을 명시적으로 정의할 수 있다.
  3. 상태와 행위를 하나의 객체에 캡슐화할 수 있다.
  4. 코드의 자체 문서화 효과를 높일 수 있다.

참고

Tags: ,

Categories:

Updated:

Leave a comment