본문 바로가기
Java

Java enum 활용기(enum에서의 다형성)

by nak_honest 2023. 11. 10.

이 글은 enum의 기본적인 문법에 대해서는 다루지 않습니다! 해당 내용은 인터넷이나 책에 너무나도 잘 소개되어 있기 때문에, 만약 enum에 대해 잘 모르신다면 한번 보고 오셔도 좋을 것 같습니다 :)

 

 

우테코 프리코스 중 로또 미션을 진행하면서 다음과 같은 요구 사항을 만날 수 있었다.

 

먼저 해당 요구 사항을 보고, 로또 당첨 결과를 enum으로 만들면 좋을 것 같다고 생각하였다.

(실제로도 코드 리뷰를 하다보면 많은 분들이 이와 같이 구현하셨다.)

 

관련된 데이터들과 로직을 하나의 상수로 묶을 수 있기 때문에 많은 분들이 enum을 활용 하셨을 것이라 생각한다.

 

 

따라서 나도 다음과 같이 enum으로 로또 당첨 결과를 관리하였다.

public enum WinningResult {
    FIRST(6, false, 2_000_000_000),
    SECOND(5, true, 30_000_000),
    THIRD(5, false, 1_500_000),
    FOURTH(4, false, 50_000),
    FIFTH(3, false, 5_000),
    LOSING(0, false, 0);

    private final int matchCount;
    private final boolean needBonusMatch;
    private final Money winningMoney;

    WinningResult(int matchCount, boolean needBonusMatch, int winningMoney) {
        this.matchCount = matchCount;
        this.needBonusMatch = needBonusMatch;
        this.winningMoney = new Money(winningMoney);
    }
    // ...
}

위와 같이 enum을 활용하니 각 등수와 관련된 상수들(당첨금액, 일치하는 번호 개수 등)을 하나로 묶을 수 있게 되었다.

 

 

보너스 볼의 일치 여부를 boolean으로 표현해도 괜찮은걸까?

하지만 위처럼 구현하다 문득, 보너스 볼의 일치 여부를 단순히 boolean 값으로 표현해도 괜찮은 것인지 의문이 들었다.

 

왜냐하면 다음과 같은 질문이 떠올랐기 때문이다.

 

"3등의 false와 4, 5등의 false는 같은 의미를 가질까?"

 

일치하는 번호의 개수가 5개인 경우에는 보너스 볼의 일치 여부에 따라 2등과 3등이 명확하게 나뉘지만,

나머지 4, 5 등은 보너스 볼의 일치 여부와 "상관 없이" 번호의 개수만 일치하면 된다.

 

 

즉 3등의 false는 보너스 볼이 "일치하면 안된다" 라는 것을 의미하고, 4, 5등의 false는 보너스 볼이 "일치할 필요가 없다" 를 의미한다.

이게 무슨 말장난인가 싶겠지만, boolean 값에 대한 서로 다른 해석은 프로그램을 복잡하게 만드는 요소가 된다.

 

 

만약 위와 같은 상황에서 일치하는 번호의 개수, 보너스 볼 일치 여부를 받아서 해당되는 등수를 반환하는 메소드를 다음과 같이 정의한다고 해보자.

    public static WinningResult of(int matchCount, boolean bonusMatch) {
        return Arrays.stream(values())
                .filter(result -> result.matchCount == matchCount)
                .filter(result -> !result.needBonusMatch || result.needBonusMatch == bonusMatch)
                .findFirst()
                .orElse(LOSING);
    }

위의 코드를 봤을때 한번에 문제점을 찾을 수 있는가?

 

결론부터 말하자면, 위의 메소드는 enum 상수를 정의한 순서에 의존하고 있다.

 

 

만약 위의 상황에서 enum 상수들을 역순으로 정의한다면 어떻게 될까?

일치하는 번호의 개수가 5개인 경우, 보너스볼 일치 여부와 상관없이 항상 3등을 결과로 반환하게 된다.

 

역순으로 정의하게 되면 stream이 WinningResult.THIRD 먼저 가지고 올 것이다.

그러면 !result.needBounsMathch가 true를 반환하고, 뒤의 result.needBonusMatch == bonusMatch 를 검사하지 않는다.

 

 

needBonusMatch가 false인 경우에는 보너스 볼의 일치 여부를 확인하지 않는다는 것이다.

따라서 "5개 번호 + 보너스볼 일치" 라면 2등을 반환해야 하지만, 3등을 반환하는 버그가 발생한다.

 

이 버그는 false를 "보너스 볼이 일치할 필요가 없다" 라고 해석했기 때문에 발생한 것이다.

 

 

따라서 코드를 다음과 같이 수정해야 할것이다.

    public static WinningResult of(int matchCount, boolean bonusMatch) {
        return Arrays.stream(values())
                .filter(result -> result.matches(matchCount, bonusMatch))
                .findFirst()
                .orElse(LOSING);
    }

    public boolean matches(int matchCount, boolean bonusMatch) {
        if (matchCount == 5) {
            return this.needBonusMatch == bonusMatch;
        }

        return this.matchCount == matchCount;
    }

보너스 볼의 일치 여부를 확인해야 하는 케이스를 먼저 체크한 후에, 나머지 케이스는 matchCount가 같은지만 확인하면 된다.

 

그런데 만약 이후에 "4개 번호 + 보너스볼 일치" 라는 등수가 새로 추가된다면 어떻게 될까?

위의 if문에서 matchCount가 4인지도 확인해 주어야 할 것이다.

 

 

이는 코드의 복잡성을 증가시키고, 유지보수하기 어렵게 한다.

 

현재는 로또라는 도메인어서 그렇지, 만약 케이스가 수십개로 증가한다면 보너스 볼의 일치 여부를 확인해야 하는 케이스는 모두 if 문에서 걸러줘야 할 것이다.

 

 

그렇다면 보너스 볼의 일치 여부를 어떻게 관리할 수 있을까?

먼저 앞으로 돌아가 무엇이 문제였는지를 다시 살펴 보자.

 

현재 코드는 false라는 값에 대해 서로 다른 해석이 존재하기 때문에 발생한 문제였다.

즉 보너스 볼이 일치해야하는지 여부를 단순히 true/false로 관리하기 때문에 발생한 문제라는 것이다.

 

 

실제로는 보너스 볼의 일치 여부가 다음과 같은 3개의 상태 중 하나여야 한다.

1. "일치해야 한다."

2. "일치하면 안된다."

3. "상관없다"

 

마치 흑백 논리의 오류처럼, 3개로 나눠야 할 상황을 true/false 라는 2개의 상황으로 나누니 오류를 범하게 된 것이라 할 수 있다.

 

 

 

따라서 기초타입인 boolean으로 이를 표현할 수 없다면 다음과 같이 enum을 활용하면 되겠다는 생각을 하였다.

public enum BonusMatchStatus {
    MATCHED, NOT_MATCHED, IRRELEVANT;
}
public enum WinningResult {
    FIRST(6, BonusMatchStatus.IRRELEVANT, 2_000_000_000),
    SECOND(5, BonusMatchStatus.MATCHED, 30_000_000),
    THIRD(5, BonusMatchStatus.NOT_MATCHED, 1_500_000),
    FOURTH(4, BonusMatchStatus.IRRELEVANT, 50_000),
    FIFTH(3, BonusMatchStatus.IRRELEVANT, 5_000),
    LOSING(0, BonusMatchStatus.IRRELEVANT, 0);

    private final int matchCount;
    private final BonusMatchStatus needBonusMatch;
    private final Money winningMoney;

    WinningResult(int matchCount, BonusMatchStatus needBonusMatch, int winningMoney) {
        this.matchCount = matchCount;
        this.needBonusMatch = needBonusMatch;
        this.winningMoney = new Money(winningMoney);
    }

    public static WinningResult of(int matchCount, boolean bonusMatch) {
        // ...
}

하지만 of 메소드는 구매한 로또와 보너스 볼의 일치 여부를 boolean 값으로 받기 때문에, BonusMatchStatus가 boolean 값에 대응되어야 한다.

 

 

그래서 다음과 같이 IRRELEVANT를 null로 표현하여 해결하려 했다.

public enum BonusMatchStatus {
    MATCHED(true), NOT_MATCHED(false), IRRELEVANT(null);

    private final boolean matchStatus;
}
    public static WinningResult of(int matchCount, boolean bonusMatch) {
        return Arrays.stream(values())
                .filter(result -> result.matches(matchCount, bonusMatch))
                .findFirst()
                .orElse(LOSING);
    }

    public boolean matches(int matchCount, boolean bonusMatch) {
        if (needBonusMatch.equals(BonusMatchStatus.IRRELEVANT)) {
            return this.matchCount == matchCount;
        }

        return (this.matchCount == matchCount) && (needBonusMatch.getStatus == bonusMatch));
    }

하지만 위와같이 null을 사용하는 것은 좋지 못한 코드라고 생각되었다. null을 직접적으로 사용하는 것은 최대한 지양해야 한다고 생각했기 때문이다.

 

 

 

enum 상수에게 직접 물어 보자! (enum에서의 다형성)

그러다 문득 보너스 볼이 일치해야하는지 여부를 BonusMatchStatus 에게 직접 물어보면 되지 않을까라는 생각이 들었다.

즉 enum에서 추상 메소드를 정의한 후에 다형성을 적용하면, 해당 enum 상수에게 직접 물어볼 수 있지 않을까 라는 생각이 들었다.

 

 

따라서 BonusMatchStatus를 다음과 같이 정의하였다.

public enum BonusMatchStatus {
    MATCHED {
        @Override
        public boolean matches(boolean bonusMatch) {
            return bonusMatch == true;
        }
    },
    NOT_MATCHED {
        @Override
        public boolean matches(boolean bonusMatch) {
            return bonusMatch == false;
        }
    },
    IRRELEVANT {
        @Override
        public boolean matches(boolean bonusMatch) {
            return true;
        }
    };

    public abstract boolean matches(boolean bonusMatch);
}

즉 보너스 볼의 일치 여부를 받아서 보너스 볼이 "일치해야 하는지, 일치하지 말아야 하는지, 아니면 상관 없는지"를 스스로 결정하도록 구현하였다.

 

그렇게 하니 WinningResult에서는 이제 of 메소드를 다음과 같이 간단하게 정의할 수 있게 되었다.

public static WinningResult of(int matchCount, boolean bonusMatch) {
        return Arrays.stream(values())
                .filter(result -> result.matches(matchCount, bonusMatch))
                .findFirst()
                .orElse(LOSING);
    }

    private boolean matches(int matchCount, boolean bonusMatch) {
        return (matchCount == this.matchCount) && (needBonusMatch.matches(bonusMatch));
    }

 

enum 상수에게 직접 물어보니, 코드가 더 깔끔해진다는 것을 경험할 수 있었다.

객체에게 직접 물어보자!!

 

 

enum을 활용하는 더 다양한 방법은 다음 글에 잘 나와 있으니 해당 글도 읽어보길 추천한다!!

https://techblog.woowahan.com/2527/

 

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

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

 

그리고 이번 미션에서는 저렇게 마무리 지었지만, 이동욱 개발자님의 글처럼 함수형 인터페이스까지 활용해서 람다로 이를 전달해준다면 훨씬 깔끔한 코드가 되지 않을까 싶다.

 

 

마무리

이전에 이동욱 개발자님이 enum에서의 다형성을 멋있게 적용하신 것을 보고, 언젠가는 나도 이를 코드로 표현하고 싶다고 계속 생각해 왔다. 이번에 조금은? 비슷하게 적용한 것 같아 뿌듯하다!

 

 

그리고 enum의 강력함을 또 한번 맛 볼수 있는 시간이었다.

 

앞으로 여러 문제를 맞닥뜨렸을때 해당 상황을 기본 타입으로 표현할 수 없다면, enum으로 표현할 수는 없는지 고민해보자.

또한 enum 상수에게 "직접" 물어볼 수 없는지도 고민해보자.

 

 

앞으로도 enum을 적극적으로 활용하면서, 더 다양한 방식으로 사용해야겠다 다짐한다.

 

 


(실제로 작성한 코드는 다음 링크를 참고해 주시면 감사하겠습니다 ㅎㅎ)

https://github.com/woowacourse-precourse/java-lotto-6/pull/1085/files#diff-29cd064f3714977b544766bda77425645e0b4a182812b549a1940b9e6ef34464

 

[로또] 이낙헌 미션 제출합니다. by nak-honest · Pull Request #1085 · woowacourse-precourse/java-lotto-6

PR 타입 미션 제출 반영 브랜치 nak-honest/nak-honest -> woowacourse-precourse/main 상세 내용 enum에서 다형성을 적용하였습니다. 당첨 결과에서 2등은 보너스 볼이 일치해야만 하고, 3등은 보너스 볼이 일치

github.com

 

출처

- https://techblog.woowahan.com/2527/

'Java' 카테고리의 다른 글

JVM 구조  (1) 2023.08.08