반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 슬로우 쿼리
  • 알고리즘
  • 참조 해제
  • 도커공식문서
  • 메이븐 골
  • 이펙티브자바
  • 추상 팩터리 패턴
  • Java
  • 싱글톤 패턴
  • bean Validation
  • 디자인패턴
  • prerendering
  • Next.js
  • NEXT JS
  • item9
  • 자바
  • 자바스크립트 면접
  • 서버리스 컴퓨팅
  • serverless computing
  • 이펙티브 자바
  • 싱글턴
  • 자바스크립트 인터뷰
  • 자료구조
  • item7
  • Pre-rendering
  • rust
  • 자바 검증
  • 토비의 스프링
  • 자바스크립트
  • MSSQL
  • 프로그래머의 뇌
  • 객체복사
  • try-with-resources
  • 빈 검증
  • item8
  • 러스트
  • 작업기억공간
  • 메이븐 페이즈
  • 스프링 검증
  • pnpm
  • 외래키 제약조건
  • 자바 디자인패턴
  • 싱글톤
  • Javadoc 자바독 자바주석 주석 Comment
  • 팩터리 메서드 패턴
  • 메이븐 라이프사이클
  • 플라이웨이트패턴
  • 느린 쿼리
  • next js app
  • 이펙티브 자바 item9

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

Java/이펙티브 자바

이펙티브 자바, 쉽게 정리하기 - item 34. int 상수 대신 열거 타입을 사용하라

2022. 2. 24. 09:14

이펙티브 자바, 쉽게 정리하기 - item 34. int 상수 대신 열거 타입을 사용하라

int 상수 패턴 (int enum pattern)

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;
  • 경우의 수가 한정될 때 각 경우를 상수 값으로 치환하여 표현하는 것이다.

int 상수 패턴의 단점

  • 타입 안전을 보장할 방법도 없고, 표현력도 좋지 않다.
    • 오렌지를 보낼 메서드에 사과를 보내고 == 연산자로 비교해도 정상적인 결과가 나올 것이다.
  • 앞에 전부 ORANGE, APPLE 등의 접두어를 붙인 이유는 네임스페이스가 없기 때문이다.
  • 만일 중간에 값 하나가 빠져서 상수 값이 바뀌면 모두 다시 작성해야 한다.
  • 문자열로 출력하기 까다롭다.

자바 열거 타입 (enum type)

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum ORANGE { NAVEL, TEMPLE, BLOOD }

열거 타입(enum type)의 기반 아이디어

  • 상수 하나당 인스턴스를 하나씩 만들어 public static final 필드로 공개하는 것이다.
    • 인스턴스가 통제된다.
    • 원소가 하나이면 싱글턴으로 볼 수 있다.
  • 컴파일 타입 안정성을 제공한다.
    • 이전의 int 상수 패턴처럼 ORANGE가 갈 곳에 APPLE이 간다면, 명확히 타입 에러가 발생한다.
  • 네임스페이스를 제공하여, 이름이 같은 상수도 평화롭게 공존할 수 있다.
    • APPLE.RED와 ORANGE.RED는 구분된다.
  • toString()이 출력하기에 적합한 문자열을 내어준다.
  • 열거 타입에는 다양한 메서드나 필드도 추가 가능하다.
    • 추가로 임의의 인터페이스도 구현하게 할 수 있다.

Planet 의 예

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.677300E-11;

    // 생성자
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        this.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 static final로 공개함을 다시한번 상기하자.
    • 열거 타입은 근본적으로 불변이라 모든 필드는 final이어야 한다.

enum.values()

@Test
public void useValueOfEnumTest() {
    double earthWeight = Double.parseDouble("185");
    double mass = earthWeight / Planet.EARTH.surfaceGravity;

    for (Planet value : Planet.values()) {
        System.out.printf("%s에서의 무게는 %f이다.%n", value, value.surfaceWeight(mass));
    }
}
  • enum.values() 메서드는 해당 enum 네임스페이스에 선언된 모든 값을 배열의 형태로 반환한다.
  • 위의 테스트코드를 통해 간단하게 모든 행성에서의 무게를 출력하는 코드를 만들었다.
MERCURY에서의 무게는 69.912739이다.
VENUS에서의 무게는 167.434436이다.
EARTH에서의 무게는 185.000000이다.
MARS에서의 무게는 70.226739이다.
JUPITER에서의 무게는 467.990696이다.
SATURN에서의 무게는 197.120111이다.
URANUS에서의 무게는 167.398264이다.
NEPTUNE에서의 무게는 210.208751이다.

만약에 Planet 열거 타입의 행성이 하나 줄더라도, 출력하는 줄 수가 하나 줄어들 뿐 그 이상의 큰 변화는 없다. int형 상수 패턴과 대비되는 장점이다.

열거타입 상수마다 동작이 달라지는 메서드 구성해보기

enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;

    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;
            }
            default -> throw new IllegalStateException();
        }
    }
}

@Test
public void operationApplyTest() {
    double x = 10;
    double y = 15;

    for (Operation value : Operation.values()) {
        System.out.printf("%f %s %f = %f%n", x, value, y, value.apply(x, y));
    }
}
  • 간단히 switch문을 활용하여 구성해보았다.
  • 이 방법엔 단점이 두개 있다.
    • 도달할 일 없는 throw 문을 작성해야 한다.
    • 새로운 상수가 생길 때마다 case를 추가하는 것을 잊으면 안된다.
      • 만일 잊게 되면, 런타임 에러를 만나게 될 것이다.

열거타입 상수마다 동작이 달라지는 메서드 구성해보기 2: 추상 메서드 이용하기 (switch 단점 개선)

enum Operation {
    PLUS {
        @Override
        public double apply(double x, double y) {
            return x+y;
        }
    },
    MINUS {
        @Override
        public double apply(double x, double y) {
            return x-y;
        }
    },
    TIMES {
        @Override
        public double apply(double x, double y) {
            return x*y;
        }
    },
    DIVIDE {
        @Override
        public double apply(double x, double y) {
            return x/y;
        }
    };

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

@Test
public void operationApplyTest() {
    double x = 10;
    double y = 15;

    for (Operation value : Operation.values()) {
        System.out.printf("%f %s %f = %f%n", x, value, y, value.apply(x, y));
    }
}
  • 상수별 클래스 몸체에 apply 메서드를 재정의하였다.
  • 위와 같이 추상 클래스를 이용하면, case를 이용할 때처럼 무언가 빼먹을 일이 없다.
    • 실수로 재정의하지 않았다면, 컴파일 오류로 알려준다.

열거타입 상수마다 동작이 달라지는 메서드 구성해보기 3: 생성자 이용 및 공통 메서드 재정의

enum Operation {
    PLUS ("+") {
        @Override
        public double apply(double x, double y) {
            return x+y;
        }
    },
    MINUS ("-") {
        @Override
        public double apply(double x, double y) {
            return x-y;
        }
    },
    TIMES ("*") {
        @Override
        public double apply(double x, double y) {
            return x*y;
        }
    },
    DIVIDE ("/") {
        @Override
        public double apply(double x, double y) {
            return x/y;
        }
    };

    private final String symbol;

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

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

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

@Test
public void operationApplyTest() {
    double x = 10;
    double y = 15;

    for (Operation value : Operation.values()) {
        System.out.printf("%f %s %f = %f%n", x, value, y, value.apply(x, y));
    }
}
  • 내부적으로 symbol이라는 필드를 두어, +, -, *, / 등 알맞은 기호를 저장했다.
  • toString()을 재정의하여 symbol 필드를 반환하도록 만들었다.
  • 출력 결과는 다음과 같다.
10.000000 + 15.000000 = 25.000000
10.000000 - 15.000000 = -5.000000
10.000000 * 15.000000 = 150.000000
10.000000 / 15.000000 = 0.666667

enum에서 toString() 구현 이후에 fromString() 고려하기

enum Operation {
    PLUS ("+") {
        @Override
        public double apply(double x, double y) {
            return x+y;
        }
    },
    MINUS ("-") {
        @Override
        public double apply(double x, double y) {
            return x-y;
        }
    },
    TIMES ("*") {
        @Override
        public double apply(double x, double y) {
            return x*y;
        }
    },
    DIVIDE ("/") {
        @Override
        public double apply(double x, double y) {
            return x/y;
        }
    };

    private final String symbol;

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

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

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

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

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

@Test
public void fromStringTest() {
    double operation = operation("3 * 5");
    System.out.println("operation = " + operation);
}

public double operation(String equation) {
    String[] s = equation.split(" ");

    if(s.length != 3) {
        throw new IllegalArgumentException();
    }

    Optional<Operation> optionalOperation = Operation.fromString(s[1]);

    if(optionalOperation.isPresent()) {
        return optionalOperation.get().apply(Double.parseDouble(s[0]), Double.parseDouble(s[2]));
    }

    throw new IllegalArgumentException();
}
  • fromString()을 만들어두면 위와 같이 편리하게 다시 문자열을 enum으로 변경할 수 있다.
  • 아래의 operation 메서드는 이를 응용해본 예제이다. a operation b 형태의 문자열을 받으면, 문자열을 해석하여 결과를 double 타입의 숫자로 반환한다.

열거타입 상수끼리 코드 공유해보기

  • Operation 열거 타입에서는 따로 겹치는 로직이 존재하지 않았다.
    • 그냥 x Operation type y 형식의 계산 결과만 반환했다.

열거타입 상수끼리 코드를 공유하는 예 1: switch case

enum PayrollDay {
    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 overtimePay;

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

        return basePay + overtimePay;
    }
}

@Test
public void payrollDayTest() {
    int pay1 = PayrollDay.FRIDAY.pay(480, 200);
    System.out.println("pay1 = " + pay1);

    int pay2 = PayrollDay.FRIDAY.pay(540, 200);
    System.out.println("pay2 = " + pay2);

    int pay3 = PayrollDay.SUNDAY.pay(480, 200);
    System.out.println("pay3 = " + pay3);
}
  • switch문을 이용한 형태는 이전과 같이 case를 반드시 같이 추가해주어야 한다.
    • 동작은 하지만, 관리 관점에서 위험한 포인트가 있는 코드이다.
  • 출력 결과
pay1 = 96000
pay2 = 114000
pay3 = 144000
  • 생각 가능한 해결책
    • 잔업수당을 계산하는 코드를 모든 상수에 중복해서 넣는다.
    • 계산 코드를 평일용과 주말용으로 나눈다.

사실 위 두 방법은 모두 가독성이 크게 떨어진다.

열거타입 상수끼리 코드를 공유하는 예 2: 전략 상수 패턴 사용하기

enum PayrollDay {
    MONDAY(PayType.WEEKDAY)
    , TUESDAY(PayType.WEEKDAY)
    , WEDNESDAY(PayType.WEEKDAY)
    , THURSDAY(PayType.WEEKDAY)
    , FRIDAY(PayType.WEEKDAY)
    , SATURDAY(PayType.WEEKEND)
    , SUNDAY(PayType.WEEKEND);

    private final PayType payType;

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

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

    // 전략 열거 타입
    enum PayType {
        WEEKDAY {
            @Override
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            @Override
            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);
        }
    }
}
  • 새로운 상수를 추가할 때 무조건 잔업수당 전략을 선택해야 한다.
  • 이 패턴은 switch문보다 조금 더 복잡하지만, 더 안전하고 유연하다.

단, 기존 열거 타입에 상수별 동작을 혼합해 넣는다면, switch문이 더 좋은 선택이 될 수 있다.

열거 타입을 쓰는 기준

  • 필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 열거 타입을 사용하자.
    • ex) 태양계 행성, 한 주의 요일, 체스 말
    • ex) 메뉴 아이템, 연산 코드, 명령줄 플래그

단, 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다. 열거 타입은 나중에 상수가 추가되더라도 바이너리 수준에서 호환되도록 설계되었기 때문에 걱정하지 않아도 된다.

핵심 정리

  • 열거 타입은 정수 상수보다 뛰어나다.
    • 가독성도 더 좋고 안전하고 강력하다.
  • 대다수 열거 타입은 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작할 때는 필요하다.
    • 이 경우, 보통은 추상 메서드를 선언한 뒤, switch문 대신 상수별 메서드 구현이 낫다.
    • 열거 타입 상수가 같은 코드를 공유한다면, 전략 열거 타입 패턴을 사용하자.
반응형
저작자표시 (새창열림)

'Java > 이펙티브 자바' 카테고리의 다른 글

이펙티브 자바, 쉽게 정리하기 - item 36. 비트 필드 대신 EnumSet을 사용하라  (0) 2022.02.24
이펙티브 자바, 쉽게 정리하기 - item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라  (0) 2022.02.24
이펙티브 자바, 쉽게 정리하기 - item 33. 타입 안전 이종 컨테이너를 고려하라  (0) 2022.01.24
이펙티브 자바, 쉽게 정리하기 - item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라  (0) 2022.01.24
이펙티브 자바, 쉽게 정리하기 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라  (0) 2022.01.24
    'Java/이펙티브 자바' 카테고리의 다른 글
    • 이펙티브 자바, 쉽게 정리하기 - item 36. 비트 필드 대신 EnumSet을 사용하라
    • 이펙티브 자바, 쉽게 정리하기 - item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라
    • 이펙티브 자바, 쉽게 정리하기 - item 33. 타입 안전 이종 컨테이너를 고려하라
    • 이펙티브 자바, 쉽게 정리하기 - item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바