본문 바로가기
부트캠프

자바 기초 스터디 3주차 - Enum 사용하기

by 데브겸 2023. 11. 9.

 

매직 넘버, 매직 리터럴

매직 넘버 혹은 매직 리터럴이란 프로그래밍에서 비즈니스적 의미를 가진 숫자나 문자를 그대로 표현하는 것을 말한다. 아래와 같은 코드가 있다고 치면 "3"이나 "3개의 숫자를 모두 맞히셨습니다! 게임 종료"와 같은 문자들이 매직 넘버 혹은 매직 리터럴이라고 할 수 있을 것이다.

if (attempt.get("strike") == 3) {
	System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
	pingPong = false;
}

 

이것들을 사용함으로써 어쩔때는 코드를 좀 더 명확하게 보일 수 있지만, 많은 경우 코드를 읽는 사람을 더 헷갈리게 만들고 코드의 유지보수를 힘들게 한다. 이유는 아래와 같다.

 

1. 코드 안에서 선언된 숫자나 문자열이 어떤 맥락에서 사용된 것인지 알 수 없음(특히 나 혼자 개발하는 것이 아닌, 여러 명이서 같이 하는 대규모 작업일 수록 그 문맥을 파악하기 힘듦

 

2. 변경에 취약함. 하나의 매직 넘버 혹은 리터럴은 특히 여러 군데에서 사용되는 경우, 로직이 변경되어야 할 때 해당 부분을 하나하나 찾아 다 바꿔줘야 함. 

 

 

이런 단점들이 있기 때문에 보통 이런 상수값들은 static final 혹은 enum을 통해 관리한다

 

 

static final로 관리하기

final을 통해 멤버 변수를 만들 경우 그 멤버 변수는 오직 한 번만 초기화 된다. 하지만 final만 사용할 경우 해당 멤버 변수를 지닌 클래스에 의해 생성된 인스턴스별로 각기 다른 멤버 변수가 만들어지게 된다. 따라서 static을 붙여 모든 인스턴스에서 동일한 데이터를 사용할 수 있도록 한다. 

 

static final을 통해서 상수값을 관리할 경우 아래와 같이 코드를 변경할 수 있다. (상수값을 제외한 부분의) 코드를 보면 위 코드보다 훨씬 더 그 의미와 문맥이 잘 보이는 것을 확인할 수 있다.

private static final Integer THREE_STRIKE = 3;
private static final String THREE_STRIKE_MSG = "3개의 숫자를 모두 맞히셨습니다! 게임 종료"

if (attempt.get("strike") == THREE_STRIKE) {
	System.out.println(THREE_STRIKE_MSG);
	pingPong = false;
}

 

 

다만 이렇게 할 경우 몇 가지 문제점이 발생할 수 있다.

 

1) 메모리에 부담이 갈 수도 있다. (probably... 확실치 않음)

static을 붙인다는 것은 데이터의 메모리 할당을 컴파일 시간에 한다는 뜻이고, 프로그램의 실행부터 끝날 때까지 메모리에서 남아있게 된다. 즉, (많지 않은 경우겠지만) 너무 많은 final static 값이 있다면 메모리가 낭비되고 JVM에 부담이 가게 된다.

 

2) 값을 값으로만 쓰게 된다. 

이게 뭔 말인가 싶을 것인데, 뒤에 나올 enum을 본다면 얼추 무슨 느낌인지 알 수 있을 것이다. static final로 선언하고 관리하는 것은 결국 값 하나이다. 그렇다면 동일한 의미를 지닌 다른 값이 나오거나, 또 다른 상수를 만들거나 이럴 경우 static final을 무한정으로 생산해야 한다. 또한 그냥 값 하나이기 때문에 동일하거나 유사한 의미와 쓰임을 가진 상수값끼리 묶거나 그것들끼리의 연산을 그 자체로는 할 수 없다. 

 

 

Enum으로 관리하기

드디어 글의 본 주제인 enum이다. enum의 가장 독특한 점은 상수로 구성된 '클래스'라는 점이다. 상속이나 인스턴스 생성은 불가하지만 상수를 변수와 메서드를 통해 관리할 수 있다. 상수값을 일종의 상태(혹은 데이터)로 가지는 클래스를 만들어 사용하는 것이라고 하면 그 의미가 조금 와닿을까.

 

아래 복권 당첨 예시를 통해 조금 더 자세하게 보자.

public enum MatchTypes {
    THREE_MATCH(3, 0, 5000),
    FOUR_MATCH(4, 0, 50000),
    FIVE_MATCH(5, 0, 1500000),
    FIVE_MATCH_WITH_BONUS(5, 1, 30000000),
    SIX_MATCH(6, 0, 2000000000);

    private Integer matchNum;
    private Integer bonusMatch;
    private Integer prize;

    MatchTypes(Integer matchNum, Integer bonusMatch, Integer prize) {
        this.matchNum = matchNum;
        this.bonusMatch = bonusMatch;
        this.prize = prize;
    }
}

 

각 매치 타입 상수 안에는 matchNum(당첨 번호와 몇 개 일치하는지), bonusMatch(보너스 번호와 일치하는지 여부), prize(상금)에 관한 상수가 함께 들어있다. 즉, 상수끼리 관계를 지을 수 있다. 원래라면 하나하나 final static 값으로 선언하거나, 클래스를 다르게 하여 관계 있는 것끼리만 묶거나, interface를 통해서 귀찮게 구현해야 하는 것들을 enum 안에서는 한 큐에 해낼 수 있다.

 

나아가 상수값에 대해서 메서드를 만들어 사용할 수 있는데, enum에 있는 다양한 데이터에 대해서 enum 안에서 연산을 할 수 있도록 돕는다. 아래는 메서드는 각각 matchCount와 bonusMatch 값을 바탕으로 MatchType을 필터링하고, 전체 상금을 구하는 메서드이다.

    public static MatchTypes findMatchType(Integer matchCount, Integer hasBonusMatch) {
        return Arrays.stream(MatchTypes.values())
                .filter(matchType -> matchType.matchNum.equals(matchCount))
                .filter(matchType -> (!matchCount.equals(FIVE_MATCH_WITH_BONUS.matchNum))
                        || hasBonusMatch.equals(matchType.bonusMatch))
                .findFirst()
                .orElse(null);
    }

    public static Integer calculateTotalPrize(Map<MatchTypes, Integer> drawResult) {
        return Arrays.stream(MatchTypes.values())
                .filter(drawResult::containsKey)
                .mapToInt(type -> type.prize * drawResult.get(type))
                .sum();
    }

 

만약 static final을 사용했다면 다른 클래스에서 getter 등을 통해 데이터를 가져와 계산을 해야 했겠지만, enum을 사용함으로써 상수값의 상태와 행위를 하나의 클래스 안에서 관리하게 된다.

 

아래 예시처럼 두 개 이상의 데이터 타입이 들어갈 수도 있다. BiFunction을 넣어 연산을 하게 만드는 것도 물론 가능하다. 상수값 조건에 따라 다른 연산을 enum을 통해 간편하게 구현할 수 있다.

public enum TurnOuts {
    TURN_OUT_RATE("\n총 수익률은 %.1f%%입니다.", (Integer totalPrize, Integer lotteryBudget) -> {
        return (totalPrize / (double) lotteryBudget) * 100;
    });

    private String message;
    private BiFunction<Integer, Integer, Double> calculator;

    TurnOuts(String message, BiFunction<Integer, Integer, Double> calculator) {
        this.message = message;
        this.calculator = calculator;
    }

    public String getMessage() {
        return message;
    }

    public Double calculate(Integer totalPrize, Integer lotteryBudget) {
        return calculator.apply(totalPrize, lotteryBudget);
    }
}

 

 

해당 예시도 이전에 값을 어디에선가 getter로 가져와서 연산을 해야 했던 것을 enum 안에서 한 번에 진행함으로써 객체 고유의 책임과 역할을 명확하게 하는 설계로 풀어낼 수 있었다.