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

무한성장개발자·2025년 9월 25일

package me.whiteship.chapter06.item34.integer_values;

public class Fruits {

    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 = 3;
    public static final int ORANGE_TEMPLE = 4;
    public static final int ORANGE_BLOOD = 5;

}
package me.whiteship.chapter06.item34.integer_values;

public class FruitStore {

    public int getPrice(int fruit) {
        return switch (fruit) {
            case Fruits.APPLE_FUJI -> 100;
            case Fruits.APPLE_PIPPIN -> 200;
            case Fruits.APPLE_GRANNY_SMITH -> 300;
            case Fruits.ORANGE_NAVEL -> 150;
            case Fruits.ORANGE_TEMPLE -> 250;
            case Fruits.ORANGE_BLOOD -> 350;
            default -> throw new IllegalArgumentException("No such fruit");
        };
    }

    public String getNameOfFruit(int fruit) {
        return switch (fruit) {
            case Fruits.APPLE_FUJI -> "Fuji Apple";
            case Fruits.APPLE_PIPPIN -> "Pippin Apple";
            case Fruits.APPLE_GRANNY_SMITH -> "Granny Smith Apple";
            case Fruits.ORANGE_NAVEL -> "Navel Orange";
            case Fruits.ORANGE_TEMPLE -> "Temple Orange";
            case Fruits.ORANGE_BLOOD -> "Blood Orange";
            default -> throw new IllegalArgumentException("No such fruit");
        };
    }

    public static void main(String[] args) {
        FruitStore fruitStore = new FruitStore();
        System.out.println(fruitStore.getPrice(Fruits.APPLE_FUJI));
    }
}

int가 들어오면 아무거나 받기 때문에 너무 범용적이다.
값을 제한할수가 없다.
과일의 종류가 변경될 때 문제를 찾는 시점이 프로그램을 찾는 도중이다.
enumeration을 사용하면 컴파일 타임에 알 수 있다.

아이템 34. 핵심 정리 2: 열거 타입의 장점 1

package me.whiteship.chapter06.item34.integer_values;

public class AppleClient {

    public static void main(String[] args) {
        FruitStore fruitStore = new FruitStore();
        System.out.println(fruitStore.getPrice(Fruits.APPLE_PIPPIN));
    }
}

클라이언트 코드

바이트코드

APPLE_PIPPIN이 1이라서 ICONST_1로 나온다.
그래서 값이 변경될 때마다 클라이언트도 같이 빌드를 해줘야 한다.

package me.whiteship.chapter06.item34.enumeration;

public enum Apple {

    FUJI, PIPPIN, GRANNY_SMITH;

}

각각의 인스턴스타입이 싱글턴 타입이다.

package me.whiteship.chapter06.item34.enumeration;

public class FruitStore {

    public int getPrice(Apple apple) {
        return switch (apple) {
            case FUJI -> 100;
            case PIPPIN -> 200;
            case GRANNY_SMITH -> 300;
        };
    }

    public int getPrice(Orange orange) {
        return switch (orange) {
            case NAVEL -> 150;
            case TEMPLE -> 250;
            case BLOOD -> 350;
        };
    }
}
package me.whiteship.chapter06.item34.enumeration;

public class AppleClient {

    public static void main(String[] args) {
        FruitStore fruitStore = new FruitStore();
        System.out.println(fruitStore.getPrice(Apple.PIPPIN));
        System.out.println(Apple.PIPPIN);
    }
}

enum에서 정하지 않은 다른 값을 사용 불가하다. 타입안정성이 보장된다. 같은 값을 쓸 수 없다.

package me.whiteship.chapter06.item34.enumeration;

public enum Orange {

    NAVEL, TEMPLE, BLOOD;
}

NAVER라는게 없어지는 경우 컴파일 일때 발견 가능하다.

아이템 34. 핵심 정리 3: 열거 타입의 장점 2

package me.whiteship.chapter06.item34.enumeration_method;

public interface Fruit {

    int getPrice();

}

인터페이스로 상속 가능

package me.whiteship.chapter06.item34.enumeration_method;

public enum Apple implements Fruit {

    FUJI(100), PIPPIN(120), GRANNY_SMITH(140);

    private int price;

    Apple(int price) {
        this.price = price;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}
package me.whiteship.chapter06.item34.enumeration_method;

public enum Orange implements Fruit {

    NAVEL(40), TEMPLE(35), BLOOD(20);

    private final int price;

    Orange(int price) {
        this.price = price;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}
package me.whiteship.chapter06.item34.enumeration_method;

public class FruitStore {

    public int getPrice(Fruit fruit) {
        return fruit.getPrice();
    }
}
package me.whiteship.chapter06.item34.enumeration_method;

public class AppleClient {

    public static void main(String[] args) {
        FruitStore fruitStore = new FruitStore();
        System.out.println(fruitStore.getPrice(Apple.PIPPIN));
        System.out.println(Apple.PIPPIN);
    }
}

아이템 34. 핵심 정리 4: 예제 살펴보기

package me.whiteship.chapter06.item34.planet;

// 코드 34-3 데이터와 메서드를 갖는 열거 타입 (211쪽)
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);

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

    // 중력상수(단위: m^3 / kg s^2)
    private static final double G = 6.67300E-11;

    // 생성자
    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
    }
}
package me.whiteship.chapter06.item34.planet;

// 어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력한다. (212쪽)
public class WeightTable {
   public static void main(String[] args) {
      double earthWeight = 10;
      double mass = earthWeight / Planet.EARTH.surfaceGravity();
      Planet[] values = Planet.values();
      for (Planet p : values)
         System.out.printf("%s에서의 무게는 %f이다.%n", p, p.surfaceWeight(mass));
   }
}

values가 적어준 순서대로 값을 준다.

package me.whiteship.chapter06.item34.enumeration_method;

public class AppleClient {

    public static void main(String[] args) {
        FruitStore fruitStore = new FruitStore();
        System.out.println(fruitStore.getPrice(Apple.PIPPIN));
//        System.out.println(Apple.PIPPIN);

        for (Apple apple : Apple.values()) {
            System.out.println(apple);
        }
    }
}

순서에 의존해서 나오기는 하지만 순서가 바뀔 수도 있기 때문에 그것에 의존해서 작성하지 말라는 말이다.
순서가 계속해서 보장된다는 것을 제약할 수 가 없다.

package me.whiteship.chapter06.item34.operation;

import me.whiteship.chapter06.item34.enumeration.Orange;

import java.util.HashMap;
import java.util.Map;

public enum OperationOrigin {

    PLUS, MINUS, TIMES, DIVIDE, NEW_OPERATION;

    // enum에서는 생성자가 호출되는 시점에는 정적인 필드들이 초기화 되지 않은 상태이다.
    // 따라서 enum 상수에도 접근할 수 없다.
//    private static final Map<String, OperationOrigin> stringToEnum = new HashMap<>();
//
//    OperationOrigin() {
//        stringToEnum.put(toString(), this);
//    }

    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 DIVIDE: return x / y;
        }

        throw new IllegalArgumentException("not supported operation");
    }
}

메소드 하나에서 계산 방식이 다르다. 새로운 오퍼레이션이 있는데 여기에 로직을 추가하는 걸 깜빡할 수도 있지 않겠느냐?

switch case문으로 작성하는 경우 NEW_OPERATION이라는게 새로 있어도 컴파일 오류가 발생하지 않는다.

package me.whiteship.chapter06.item34.operation;

import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

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

// 코드 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (215-216쪽)
public enum Operation {
    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; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override public String toString() { return symbol; }

    public abstract double apply(double x, double y);

    // 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
    private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(
                    toMap(Object::toString, e -> e));

    // 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }

    public static void main(String[] args) {
        double x = 10;
        double y = 5;
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
    }
}

추상화 메서드를 활용하면 apply를 구현해야만 한다.

NEW_OPERATION("new") {
	public double apply(double x, double y) { return x / y; }
};

를 강제할 수 있다.

package me.whiteship.chapter06.item34.enumeration_method;

public class AppleClient {

    public static void main(String[] args) {
        FruitStore fruitStore = new FruitStore();
        System.out.println(fruitStore.getPrice(Apple.PIPPIN));
//        System.out.println(Apple.PIPPIN);

        for (Apple apple : Apple.values()) {
            System.out.println(apple);
        }

//        System.out.println(Apple.PIPPIN);
        Apple pippin = Apple.valueOf("PIPPIN");
        System.out.println(pippin);

    }
}

Apple.PIPPIN.name()은 String으로 PIPPIN이 나온다.
valueOf는 STRING -> ENUM으로 나온다.

package me.whiteship.chapter06.item34.operation;

import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

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

// 코드 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (215-216쪽)
public enum Operation {
    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; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override public String toString() { return symbol; }

    public abstract double apply(double x, double y);

    // 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
    private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(
                    toMap(Object::toString, e -> e));

    // 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }

    public static void main(String[] args) {
        double x = 10;
        double y = 5;
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
    }
}

toString을 재정의를 한 경우 "+" -> OPERATION.PLUS, "-" -> OPERATION.MINUS 를 fromString이라는 걸로 바꿔준다는 것이다.

stringToEnum을 String을 ENUM map으로 만들어준다. 그래서 거기서 꺼내온다.

아이템 34. 핵심 정리 5: 열거 타입의 제약

package me.whiteship.chapter06.item34.operation;

import me.whiteship.chapter06.item34.enumeration.Orange;

import java.util.HashMap;
import java.util.Map;

public enum OperationOrigin {

    PLUS, MINUS, TIMES, DIVIDE, NEW_OPERATION;

    // enum에서는 생성자가 호출되는 시점에는 정적인 필드들이 초기화 되지 않은 상태이다.
    // 따라서 enum 상수에도 접근할 수 없다.
//    private static final Map<String, OperationOrigin> stringToEnum = new HashMap<>();
//
//    OperationOrigin() {
//        stringToEnum.put(toString(), this);
//		  System.out.println(MINUS.toString()); 도 안된다. 
//    }

    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 DIVIDE: return x / y;
        }

        throw new IllegalArgumentException("not supported operation");
    }
}

enum static필드가 생성자가 호출된 다음에 초기화가 되기 때문에 생성자 시에는 사용이 안된다.
"+","-"도 static한 필드들이다.

package me.whiteship.chapter06.item34.operation;

import java.util.HashMap;
import java.util.Map;

public class OperationClass {

    private final static OperationClass PLUS = new OperationClass();

    private final static Map<String, OperationClass> stringToEnum = new HashMap<>();

    private OperationClass() {
        System.out.println(PLUS.toString());
        stringToEnum.put("+", PLUS);
    }
}

일반 클래스는 참조가 된다.
enum에서만 안된다.

package me.whiteship.chapter06.item34.payroll;

// 코드 34-8 값에 따라 분기하며 코드를 공유하는 열거 타입. (217쪽)
enum PayrollDayOriginal {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    private static final int MINS_PER_SHIFT = 8 * 60;

    int pay(int minutesWorked, int payRate) {
        int basePay = minutesWorked * payRate;

        int overrimePay;
        switch (this) {
            case SATURDAY: case SUNDAY:
                overrimePay = basePay / 2;
                break;
            default: // 주중
                overrimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
        }

        return basePay + overrimePay;
    }

}
package me.whiteship.chapter06.item34.payroll;

import static me.whiteship.chapter06.item34.payroll.PayrollDay.PayType.WEEKDAY;
import static me.whiteship.chapter06.item34.payroll.PayrollDay.PayType.WEEKEND;

// 코드 34-9 전략 열거 타입 패턴 (218-219쪽)
enum PayrollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
    THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
    SATURDAY(WEEKEND), SUNDAY(WEEKEND);
    // (역자 노트) 원서 1~3쇄와 한국어판 1쇄에는 위의 3줄이 아래처럼 인쇄돼 있습니다.
    // 
    // MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    // SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
    //
    // 저자가 코드를 간결하게 하기 위해 매개변수 없는 기본 생성자를 추가했기 때문인데,
    // 열거 타입에 새로운 값을 추가할 때마다 적절한 전략 열거 타입을 선택하도록 프로그래머에게 강제하겠다는
    // 이 패턴의 의도를 잘못 전달할 수 있어서 원서 4쇄부터 코드를 수정할 계획입니다.

    private final PayType payType;

    PayrollDay(PayType payType) { this.payType = payType; }
    // PayrollDay() { this(PayType.WEEKDAY); } // (역자 노트) 원서 4쇄부터 삭제
    
    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 * 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);
        }
    }

    public static void main(String[] args) {
        for (PayrollDay day : values())
            System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
    }
}

enum을 전략으로 만들고, 전략을 선택하는 경우를 선택해서 enum을 2개로 사용한다.

package me.whiteship.chapter06.item34.operation;

// 코드 34-10 switch 문을 이용해 원래 열거 타입에 없는 기능을 수행한다. (219쪽)
public class Inverse {
    public static Operation inverse(Operation op) {
        switch(op) {
            case PLUS:   return Operation.MINUS;
            case MINUS:  return Operation.PLUS;
            case TIMES:  return Operation.DIVIDE;
            case DIVIDE: return Operation.TIMES;

            default:  throw new AssertionError("Unknown op: " + op);
        }
    }

    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values()) {
            Operation invOp = inverse(op);
            System.out.printf("%f %s %f %s %f = %f%n",
                    x, op, y, invOp, y, invOp.apply(op.apply(x, y), y));
        }
    }
}

이렇게 사용하는 방법이 많다.

0개의 댓글