본문 바로가기
Java

Item 35. int 상수 대신 열거 타입을 사용하라

by Jordy-torvalds 2020. 3. 4.
반응형

열거 타입의 정의와 필요성

열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다. 사계절, 태양계의 행성 등이 좋은 예다.

열거 타입은 정적 변수를 나열해 놓은 정수 열거 패턴과 많이 비교 되곤 한다.

// 정수 열거 패턴 예시
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

정서 열거 패턴의 단점은 다음과 같다.

  • 타입 안전이 보장되지 않는다.
  • 표현력이 좋지 않다.
    (+) 위 예시에서는 이름공간(namespace)라 불리는 APPLE, ORAGE과 같은 단어를 반복하여 사용해 불필요하게 변수명이 길어지게 한다.
  • 컴파일러가 적절히 경고 메세지를 출력하지 않는다.
    ex) APPLE 이 들어갈 자리에 ORANGE 를 넣어도 컴파일 에러가 나지 않으며, 운영 중에 문제가 생겨야 알게 된다.

그 외에, 하드코딩을 하게 됨으로써 여러 문제를 야기하게 된다.

이러한 수 많은 단점을 보완할 수 있는 것이 Enum이다.

열거 타입의 세부 개념

  • 자바의 열거 타입은 완전한 형태의 클래스다.
  • 열거 타입으로 공개된 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
  • 열거 타입은 컴파일타임 타입 안전성을 제공한다. Apple 열거 타입을 매개변수로 받는 메서드를 선언했다면, 건네받은 참조는 Apple의 세가지 값중 하나 임이 확실하며, 다른 타입의 값(예, Orange)를 넘기려 하면 컴파일 오류가 난다.
  • 열거 타입에는 각자의 이름 공간이 있어서 이름이 같은 상수도 평화롭게 공존한다. 열거 타입에 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일 하지 않아도 된다. 공개되는 것이 오직 필드의 이름 뿐이라, 정수 열거 패턴과 달리 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문이다.
  • 열거 타입의 toString 메서드는 출력하기에 적합한 문자열을 네어준다.

열거 타입 예시

// 열거 타입의 행성
public enum Planet {
    MERCURY(3.3,2.4),
    VENUS(4.8, 6),
    EARTH(5.9, 6.3),
    MARS (6.4, 3.3);

    private final double mass; // 질량 (단위: kg)
    private final double radius; // 반지름 (단위: 미터)
    private final double surfaceGravity; // 표면 중력 ( 단위 : m / s^2 = a)

    // 중력 상수
    private static final double G = 6.6;


    Planet (double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() { return mass; }
    public double radius() { return radius; }
    public double surfaceGravity() { return surfaceGravity; }

    public double surfaceWeight (double mass) {
        return mass * surfaceGravity ; // F = ma;
    }
}

-

// 열거 타입 실행 클래스
public class WeightTable {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble("100");
        double mass = earthWeight / Planet.EARTH.surfaceGravity();

        for(Planet p : Planet.values()) {
            System.out.printf("%s 에서의 무게는 %f 이다. %n",
                    p, p.surfaceWeight(mass));
        }
    }
}

-

// 콘솔
MERCURY 에서의 무게는 385.407839 이다. 
VENUS 에서의 무게는 89.694915 이다. 
EARTH 에서의 무게는 100.000000 이다. 
MARS 에서의 무게는 395.349489 이다.

질량, 반지름, 표면 중력은 정적 상수로 개별 이름 공간 마다 다른 값을 가진다.

메인 메서드를 실행하면 콘솔 항목과 같이 결과가 출력된다.

열거 타입 개발간 주의사항은 다음과 같다.

  • 열거 타입을 선언한 클래스 혹은 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현한다.
  • 프로젝트 내에서 널리 쓰이는 열거 타입은 톱레벨 클래스로 만들고, 특정 톱레벨 클래스 에서만 쓰인다면 멤버 클래스로 만든다.
  • 기존 열거 타입에 상수별 동작을 혼합해 넣는 경우가 아니라면 switch 문을 쓰지 않는다.

위 주의사항과 관련된 또 다른 예제를 보자.

// 예제 1
public enum Operation {
    PLUS, MINUS, TIMES, DIVIDES;

    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 DIVIDES: return x / y;
        }
        throw new AssertionError("알 수 없는 연산" + this);
    }
}

예제 1의 경우 switch 문을 사용해 선택된 연산식에 따라 파라미터 연산 결과가 정해지게 된다. 위 코드의 문제 점은 추가적인 연산식이 생겨났을 때, 일일히 apply에 case를 추가해야할 뿐더러 컴파일 에러가 나지 않고 이후에 운영 간에 문제가 발견된다는 것이다.

// 예제 2
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toMap;

public enum Operation_v2 {
    PLUS("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDES("/") {
        public double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;
    Operation_v2(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }
    public abstract double apply (double x, double y);

    private static final Map<String, Operation_v2> stringToEnum =
            Stream.of(values()).collect(
                    toMap(Object :: toString, e -> e));

    public static Optional<Operation_v2> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }
}

예시 2는 예시 1에 비해 복잡해졌지만, 추가된 연산식에 대한 추상 메서드를 구현하지 않았을 때 컴파일 에러가 나지 않아 더 편리하다.

각 상수에 걸맞게 추상 메서드를 재정의하는 방법을 상수별 메서드 구현(constant-specific method implementation) 이라고 한다.

또한 toString으로 현재 사용 중인 연산 기호를 반환하고, fromString으로 특정 연산기호를 파라미터로 받았을 때 그에 맞는 Enum을 반환하는 기능이 있다.

이를 응용하면 비즈니스 로직 구현시 요긴하게 쓸 수 있을 것이다.

또한 해당 클래스 내에서만 쓰이는 stringToEnum, symbol을 private 으로 선언한 반면, 여러 클라이언트에게 사용될 수 있는 메서드인 fromString, toString은 public으로 선언했다.

배울 점이 많은 좋은 예시라고 볼 수 있겠다.

// 예제 3
import static item34.PayrollDay.PayType.WEEKDAY;
import static item34.PayrollDay.PayType.WEEKEND;

public enum PayrollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
    THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
    SATURDAY(WEEKEND),SUNDAY(WEEKEND)
    ;
    private final PayType payType;

    PayrollDay (PayType payType) {
        this.payType = payType;
    }

    int pay (int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    enum PayType {
        WEEKDAY {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 :
                        (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 :
                        (minsWorked) * payRate / 2;
            }
        };
        abstract int overtimePay(int mins, int payRate) ;
        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }
}

예시 3번은 여러 클라이언트에서 쓰일 Enum인 PayrollDay 을 톱레벨 클래스로, 특정 클래스 내에서만 쓰일 enum인 PayType 는 멤버 클래스로 선언했다.

또한 Switch문 대신 멤버 클래스를 적절히 사용하여 효율적인 비즈니스 로직을 구현했다.

위 예시를 따라 치며 코드를 이해해 간다면 기본적인 Enum 사용법을 익힐 수 있을 것이다.

반응형

'Java' 카테고리의 다른 글

쓰레드와 프로세스, 그리고 자바 비동기 API  (4) 2020.03.10
자바 링크  (0) 2020.03.06
빌더 메서드로 배열을 리스트로 만드는 법  (0) 2020.02.25
SOLID  (2) 2020.02.08
응집도와 결합도  (2) 2020.02.08