자바의 Enum

2020. 4. 18. 22:46Java

처음 자바의 Enum을 알고 난 뒤, 여기저기 유용하게 사용했다. Enum은 기본적으로 상수의 그룹을 나타낼 때 사용하는데, 자바의 Enum은 다른 언어의 Enum보다 강력한 기능을 여럿 제공한다. 이번 포스팅에서는 Enum을 활용하는 여러 방법에 대해 알아보려고 한다.

선언과 사용

public enum Rank {
    FIRST_PLACE(6),
    SECOND_PLACE(5),
    NONE(-1);

    private int matchingCount;

    Rank(int matchingCount) {
        this.matchingCount = matchingCount;
    }
}

이 때, RankEnum 타입이 되고 FIRST_PLACE, NONE 등은 Enum 상수가 된다. 각 상수들은 일반 클래스와 동일하게 필드를 가질 수 있다. 위 코드에서는 matchingCount가 필드이고, 이는 상수 옆 괄호 안에 NONE(-1)처럼 값을 넣어줄 수 있다. 이렇게 NONEmatchingCount가 -1이라고 명시할 수 있다. 위 코드는 내부적으로 아래와 같이 class로 바뀐다.

public class Rank {
    public static final Rank FIRST_PLACE = new Rank(6);
    public static final Rank SECOND_PLACE = new Rank(5);
    public static final Rank NONE = new Rank(-1);
}

Rank rank = Rank.FIRST_PLACE;

따라서, 각 Enum 상수의 사용은 위와 같이 할 수 있다.

values(), ordinal()

Rank.values()는 Rank에 포함되는 모든 상수들의 배열을 리턴한다. 즉, Rank[] ranks = {Rank.FIRST_PLACE, Rank.SECOND_PLACE, Rank.NONE}과 같다.
ordinal()은 해당 상수의 index를 리턴한다. Rank.FIRST_PLACE.ordinal()은 0이고, Rank.NONE.ordinal()은 2가 된다.

Comparable

자바의 Enum은 Comparable<T> 인터페이스를 구현하고 있다. 따라서 compareTo() 메서드를 사용할 수 있다. 또한 이 compareTo() 메서드는 final로 선언되어 있어서 오버라이드가 불가능하다. 따라서 Enum 상수의 순서는 상수들이 선언된 순서가 된다.

메서드

public enum Rank {
    FIRST_PLACE(6),
    SECOND_PLACE(5),
    NONE(-1);

    private int matchingCount;

    Rank(int matchingCount) {
        this.matchingCount = matchingCount;
    }

    public void printRankInfo() {
        System.out.println(this.toString());
    }
}

public static void main(String[] args) {
    FIRST_PLACE.printRankInfo();
}

Enum도 클래스와 동일하게 메서드를 가질 수 있다. 위 코드에서 printRankInfo()는 자기 자신의 문자열을 출력하는 메서드이다. 위 main 함수의 실행 결과는 아래와 같다.

output: FIRST_PLACE

Lambda

사칙연산은 아래와 같이 인터페이스로 구현할 수 있다.

public interface Operator {
    double calculate(double preOperand, double postOperand);
}

class Plus implements Operator {
    @Override
    public double calculate(double preOperand, double postOperand) {
        return preOperand + postOperand;
    }
}

class Minus implements Operator {
    @Override
    public double calculate(double preOperand, double postOperand) {
        return preOperand + postOperand;
    }
}

class Multiply implements Operator {
    @Override
    public double calculate(double preOperand, double postOperand) {
        return preOperand + postOperand;
    }
}

class Divide implements Operator {
    @Override
    public double calculate(double preOperand, double postOperand) {
        return preOperand + postOperand;
    }
}

class operationChoicer {
    public Operator findOperator(String operatorName) {
        if (operatorName.equals("+")) {
            return new Plus();
        }
        if (operatorName.equals("-")) {
            return new Minus();
        }
        if (operatorName.equals("*")) {
            return new Multiply();
        }
        if (operatorName.equals("/")) {
            return new Divide();
        }
        return null;
    }
}

하지만 위와 같이 구현하면 상태(+,- 등의 사칙연산의 기호)와 행위(실제로 덧셈, 뺄셈을 하는 것)가 따로 관리된다. 이를 Enum과 람다식을 사용해서 구현하면 아래와 같이 구현할 수 있다.

public enum Operator {
    PLUS("+", (preOperand, postOperand) -> preOperand + postOperand),
    MINUS("-", (preOperand, postOperand) -> preOperand - postOperand),
    MULTIPLY("*", (preOperand, postOperand) -> preOperand * postOperand),
    DIVIDE("/", (preOperand, postOperand) -> preOperand / postOperand);

    private String name;
    private BiFunction<Double, Double, Double> expression;

    Operator(String name, BiFunction<Double, Double, Double> expression) {
        this.name = name;
        this.expression = expression;
    }

    public static Operator findOperator(String operatorName) {
        return Stream.of(values())
                .filter(operator -> operator.name.equals(operatorName))
                .findFirst()
                .orElse(null);
    }

    public double calculate(double preOperand, double postOperand) {
        return expression.apply(preOperand, postOperand);
    }
}

코드가 훨신 깔끔해지고 양도 줄어든 기분이다. 이 경우는 확실히 그렇지만, Enum이 가져야할 필드가 늘어나고 람다식이 복잡해지면 인터페이스로 분리하는 편이 훨신 나을 것이다.

활용기

처음 나온 코드의 Rank는 복권 등수를 나타낸 Enum이다. 복권 등수는 6자리 번호 중 일치하는 번호의 갯수와 보너스 번호의 일치 여부로 결정된다. 이를 조금 더 구체적으로 적어보면 아래와 같다.

public enum Rank {
    FIRST_PLACE(6, 2_000_000_000),
    SECOND_PLACE(5, 300_000_000),
    THIRD_PLACE(5, 15_000_000),
    FOURTH_PLACE(4, 50_000),
    FIFTH_PLACE(3, 5_000),
    NONE(-1, 0);

    private int matchingCount;
    private int reward;

    Rank(int matchingCount, int reward) {
        this.matchingCount = matchingCount;
        this.reward = reward;
    }

    public static Rank findRankBy(int matchingCount, boolean isBonusMatching) {
        if (matchingCount == THIRD_PLACE.matchingCount && !isBonusMatching) {
            return THIRD_PLACE;
        }

        return Stream.of(values())
                .filter(rank -> rank.matchingCount == matchingCount)
                .findFirst()
                .orElse(NONE);
    }
}

findRankBy() 메서드는 일치하는 숫자의 갯수보너스 번호 일치 여부를 받아서 해당하는 Rank를 리턴하는 함수이다. 이 때, 2등과 3등은 일치하는 숫자의 갯수는 동일하고, 보너스 번호 일치 여부로 결정된다. 나와 페어는 이 코드가 마음에 들지 않았다. Enum의 각 상수들은 자신의 상태를 온전히 갖고 있어야 한다고 생각하는데, 위 코드의 상수들은 보너스 번호 일치 여부를 갖고 있지 않았기 때문이다. 따라서 아래와 같이 코드를 수정했다.

// Rank.java
public enum Rank {
    FIRST_PLACE(6, BonusType.NO_MATTER, 2_000_000_000),
    SECOND_PLACE(5, BonusType.SHOULD_MATCHING, 300_000_000),
    THIRD_PLACE(5, BonusType.SHOULD_NOT_MATCHING, 15_000_000),
    FOURTH_PLACE(4, BonusType.NO_MATTER, 50_000),
    FIFTH_PLACE(3, BonusType.NO_MATTER, 5_000),
    NONE(-1, BonusType.NO_MATTER, 0);

    private int matchingCount;
    private BonusType bonusType;
    private int reward;

    Rank(int matchingCount, BonusType bonusType, int reward) {
        this.matchingCount = matchingCount;
        this.bonusType = bonusType;
        this.reward = reward;
    }

    public static Rank findRankBy(int matchingCount, boolean isBonusMatching) {
        return Stream.of(values())
                .filter(rank -> (rank.matchingCount == matchingCount) 
                        && (rank.bonusType.filter(isBonusMatching)))
                .findFirst()
                .orElse(NONE);
    }
}

// BonusType.java
public enum BonusType {
    NO_MATTER(noMatterBonusMatchingOrNot -> true),
    SHOULD_MATCHING(isBonusMatching -> isBonusMatching.equals(true)),
    SHOULD_NOT_MATCHING(isBonusMatching -> isBonusMatching.equals(false));

    private Function<Boolean, Boolean> expression;

    BonusType(Function<Boolean, Boolean> expression) {
        this.expression = expression;
    }

    public boolean filter(boolean isBonusMatching) {
        return expression.apply(isBonusMatching);
    }
}

2, 3등을 제외한 나머지 등수들은 보너스 일치 여부와 상관이 없다(BonusType.NO_MATTER). 2등은 보너스 번호가 일치해야하고(BonusType.SHOULD_MATCHING), 3등은 일치하지 않아야한다(BonusType.SHOULD_NOT_MATCHING). 새로운 BonusType이란 Enum을 만듦으로써, Rank는 자신의 상태를 온전하게 표현할 수 있게 되었다.

사실, 처음 페어와 프로그래밍을 할 때는 보너스 일치 여부를 boolean으로 표현했다. 하지만 코드가 마음에 들지 않았고, 오랜 고민 끝에 페어가 보너스 일치 여부가 boolean이 아니지 않을까? 하는 질문을 던졌다. 그 질문에 대한 답으로 BonusType이 만들어졌다. 이번 주제와 별개지만 기존 사고 방식을 깨는게 얼마나 중요한지 깨닫게 되는 계기였다.

참고 사이트