자바 Enum을 더 잘쓰기 위한 방법

nawhew·2021년 10월 22일
2
post-thumbnail

0. 참고자료

해당 글은 아래의 자료들을 바탕으로 작성하였습니다.자세한 내용은 아래의 글을 확인해주세요.내용, 저작권등의 문제가 있는 경우 댓글이나 메일 부탁드립니다.


1. 단순 정수 열거 패턴보다 뛰어난 자바의 Enum class

자바의 열거 타입은 완전한 형태의 클래스라서 (단순한 정숫값일 뿐인) 다른 언어의 열거 타입보다 훨씬 강력하다. - effective java 3

enum을 이용하면 코드의 중복, if문의 사용등을 줄여 코드를 간결하고 보기 좋게 만들 수 있다.

기본적인 열거형으로의 사용도 기존의 정수열거패턴 보다 장점이 많다.

  • 정수 열거 패턴(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;
  • 타입 안전을 보장할 방법이 없고 표현력도 좋지 않다.

  • 동등 연산자(==)로 값을 비교 할 수 없다.

  • 값 자체가 숫자로만 보이기 때문에, 출력 및 디버거에서 크게 도움이 안된다.

  • namespace를 지원하지 않아, 위처럼 사과용 상수에는 'APPLE', 오렌지용 상수에는 'ORANGE'로 접두어를 사용하는 방식으로 값을 나누어야 한다.

    • 이 방식은 APPLE_FUJI와 ORANGE_NAVEL의 값이 같다 (APPLE_FUJI==ORANGE_NAVEL)
  • Enum class

    public enum Apple {FUJI, PIPPIN, GRANNY_SMITH, ORIGINAL}
    public enum Orange {NAVEL, TEMPLE, BLOOD, ORIGINAL}
  • 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개하는 클래스
    • 허용 가능한 값들을 제한
    • 컴파일 시 타입 안전성을 제공
      • 위에서 Enum인 Apple을 파라미터로 받는다면 Apple의 세가지 값 중 하나임이 확실하다
  • Enum 별로 namespace가 있어서 서로 다른 Enum의 같은 이름을 가지는 값을 가질 수 있다.
    • 위에서 ORIGINAL이라는 값이 Apple과 Orange에 있다.
  • 기존의 정수열거패턴과 달리 임의의 메서드나 필드를 추가하고 인터페이스를 구현 할 수 있다.
  • 문자열과 비교해 IDE의 지원을(자동완성, 오타 검증, 텍스트 리팩토링) 받을 수 있음.
  • 리팩토링 시 변경 범위가 최소화 된다. (내용 추가가 필요해도 Enum 코드 외에 수정할 필요가 없다.)

2. Ordinal이 아닌 name 데이터 사용하기

  • Enum의 구조는 아래와 같이 name과 ordinal 값으로 구성되어 있다.
    public abstract class Enum<E extends Enum<E>> ... {
    
        private final String name;
        public final String name() {
            return name;
        }
        private final int ordinal;
    		@Range(from = 0, to = java.lang.Integer.MAX_VALUE)
    		public final int ordinal() {
            return ordinal;
        }
  • 여기서 ordinal 값은 값들이 나열된 순서대로 0,1,2,3 ... ,N과 같이 정수 값을 가지게 된다.
  • 이때 값 사이에 다른 값이 추가 될 경우 뒤에 있는 값들의 ordinal 값이 변경되며, 이때 ordinal 값을 사용하던 곳에 모두 영향을 주게 된다.
  • 그러기때문에 ordinal 값을 안쓰는 것을 추천한다
  • 예시 : Juice에서 WATERMELON 값이 추가되며 LEMON의 ordinal 값이 3에서 4로 변경된다.
            // 출력값 : APPLE:0 CHERRY:1 LEMON:2
            public enum Juice {APPLE, CHERRY, LEMON}
            // 출력값 : APPLE:0 CHERRY:1 WATERMELON:2 LEMON:3
            public enum Juice {APPLE, CHERRY, WATERMELON, LEMON}
  • DB에 사용되는 값에도 name을 사용하면 좋다.
    • JPA에서 제공하는 @Enumerated(EnumType.String) 를 사용하여 간편하게 name값을 사용 할 수 있다.
    @Column
    @Enumerated(EnumType.String)
    private TestType testType;
  • @Enumerated의 기본 값은 EnumType.ORDINAL이므로 반드시 변경하여 사용하자

3. 데이터와 메소드를 가지는 Enum

자바의 Enum은 메서드나 필드를 추가하고 인터페이스를 구현 할 수 있다.

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

    private final String symbol;
    private final BiFunction<Integer, Integer, Integer> operation;
		abstract double apply(double value, double value);

    BasicOperation(String symbol, BiFunction<Integer, Integer, Integer> operation) {
        this.symbol = symbol;
        this.operation = operation;
    }

    public Integer operate(Integer x, Integer y) {
        return this.operation.apply(x, y);
    }

    @Override public String toString() {
        return symbol;
    }
}
  • Enum의 원소들은 위와 같이 데이터(위의 symbol)와 메소드(위의 BiFunction과 apply)를 가질 수 있다.
    • Enum은 함수형 인터페이스(위의 BiFunction)나 추상메소드(위의 apply)를 줄 수 있다.

이러한 특징으로 아래와 같은 장점을 갖도록 구현 할 수 있다.

  1. 데이터들 간의 연관관계 표현 할 수 있다.
    • Enum은 원소에 값을 가지게하여 아래의 경우 Y, "1", true가 같은 의미를 가지도록 해준다.
    • get 메소드를 만들어 각 의미를 가지고 온다면, 값이 추가된다고 해서 (if문 등을 통한) 반복되는 코드가 생성 되지 않는다.
    public enum TableStatus {
    
    		Y("1", true) // Y, "1", true가 같은 의미임을 쉽게 알 수 있다.
        , N("0", false)
        ;
    
        private String table1value;
        private boolean table2value;
    
        TableStatus(String table1value, boolean table2value) {
            this.table1value = table1value;
            this.table2value = table2value;
        }
    
    		// table1value의 getter는 enum에 값이 추가된다고 코드가 추가되지 않는다.
    		public String getTable1value() {
            TableStatus.Y.ordinal();
            return table1value;
        }
  1. 상태와 행위를 한곳에서 관리 할 수 있다
    • 위의 Enum(BasicOperation)과 달리 데이터 조회와 계산 메소드가 따로 따로 있는 기존의 코드(LegacyOperation)
      • 똑같은 기능을 하는 메소드를 중복 생성 할 수 있다.
      • 계산 메소드를 누락 할 수 있다.
    public class LegacyOperation {
    
        public static int operate(String symbol, int x, int y) {
            if("PLUS".equals(symbol)) {
                return x + y;
            } else if("MINUS".equals(symbol)) {
                return x - y;
            } else if("TIMES".equals(symbol)) {
                return x * y;
            } else if("DIVIDE".equals(symbol)) {
                return x / y;
            }
            throw new IllegalArgumentException("Do not operate. Not found symbol.");
        }
    
        public static void main(String[] args) {
            String symbol = findSymbol(); // 데이터는 데이터 대로 조회하고
            int x = 100;
            int y = 200;
            int result = LegacyOperation.operate(symbol, x, y); // 계산은 별도의 메소드를 통해서 진행
        }
  • 기존 방식과 달리 Enum으로 만든 BasicOperation은 아래와 같이 데이터를 가지고 있는 Enum에 직접 계산을 요청 할 수 있다.
  • 이경우 데이터에 따라 지정된 메소드만 사용 하도록 강제하는 효과도 있다.
  • DB의 테이블에서 뽑은 특정 값이 지정된 메소드와 관계가 있는 경우 유용하게 사용 할 수 있다.
    	public static void main(String[] args) {
            BasicOperation operation = findOperation();
            int x = 100;
            int y = 200;
            int result = operation.operate(x, y); // Enum에게 직접 계산을 요청
        }
  1. 데이터를 묶어서 그룹 관리 할 수 있다

[그림 : 결제 종류 및 결제 수단 | 출처 : 우아한형제들 기술 블로그 Java Enum 활용기]

위의 그림과 같이 결제 데이터는 결제 종류와 결제 수단으로 표현 할 수 있다.

  • 결제 종류 : 현금, 카드, 기타
  • 결제 수단 : 계좌이체, 신용카드, 포인트 등..
  • 아래는 이전에 많이 쓰인 문자열로 구분하는 방식
  • 그룹과 그룹의 데이터가 추가 될 수록 해당 값을 사용하는 곳들의 코드가 늘어난다
    		public String getPayGroup(String payCode) {
            if("ACCOUNT_TRANSFER".equals(payCode) || "REMITTANCE".equals(payCode) || "ON_SITE_PAYMENT".equals(payCode) || "TOSS".equals(payCode)) {
                return "CASH";
            } else if ("PAYCO".equals(payCode) || "CARD".equals(payCode) || "KAKAO_PAY".equals(payCode) || "BAEMIN_PAY".equals(payCode)) {
                return "CARD";
            } else if ("POINT".equals(payCode) || "COUPON".equals(payCode)) {
                return "ETC";
            } else {
                return "EMPTY";
            }
        }
  • PayGroup을 기준으로 수행되는 메소드는 아래와 같이 구현해야한다.
  • 이 경우 수행되는 메소드가 추가 될 때마다 아래와 같은 코드가 계속해서 늘어나게 된다.
        public void pushByPayGroup(String payGroupCode) {
            if("CASH".equals(payGroupCode)) {
                pushCashMethod();
            } else if("CARD".equals(payGroupCode)) {
                pushCashMethod();
            } else if("ETC".equals(payGroupCode)) {
                pushCashMethod();
            }
            throw new RuntimeException("payGroupCode가 없습니다.");
        }
  • 아래와 같이 Enum의 상수에 결제 타입을 가지는 리스트를 추가하여 PayGroup별로 PayType들을 그룹화 해서 가질 수 있다.
  • 아래와 같이 결제 타입을 Enum이 아닌 문자열 배열/리스트를 사용 할 경우 문제가 생길 수 있다. (파라미터로 전달 된 값이 잘못 되었을 경우 등)
    public enum PayGroupAdvanced {
    
        CASH("현금", Arrays.asList(PayType.ACCOUNT_TRANSFER, PayType.REMITTANCE, PayType.ON_SITE_PAYMENT, PayType.TOSS))
        , CARD("카드", Arrays.asList(PayType.PAYCO, PayType.CARD, PayType.KAKAO_PAY, PayType.BAEMIN_PAY))
        , ETC("기타", Arrays.asList(PayType.POINT, PayType.COUPON))
        , EMPTY("없음", Collections.EMPTY_LIST)
        ;
    
        private String title;
        private List<PayType> payTypeList;
    
        PayGroupAdvanced(String title, List<PayType> payTypeList) {
            this.title = title;
            this.payTypeList = payTypeList;
        }
    
        public static PayGroupAdvanced findByPayType(PayType payType) {
            return Arrays.stream(PayGroupAdvanced.values())
                    .filter(payGroup -> payGroup.hasPayCode(payType))
                    .findAny()
                    .orElse(EMPTY);
        }
    
        private boolean hasPayCode(PayType payType) {
            return payTypeList.stream()
                    .anyMatch(pay -> pay == payType);
        }
    }

4. 싱글톤을 구현하기 위한 가장 좋은 방법

  • Enum class는 private 생성자만 생성 가능하다. (public, protected 사용 불가능)
    • 고정된 상수들의 집합으로, 컴파일 시 모든 값을 알고 있어야 하기 때문에,다른 패키지나 클래스에서 enum 타입에 접근해 동적으로 값을 변경 할 수 없음.
    • 미리 정의된 enum 변수안의 상수외 다른 것을 할당할 수 없다.
    • new 키워드, clone() 등의 메소드도 사용이 불가능 하다.
    • 이로인해 컴파일시 타입 안정성이 보장된다.
  • 이러한 특징을 바탕으로 아래와 같은 Enum class를 만들 수 있다.
    public enum Elvis {
        INSTANCE;

        public void leaveTheBuilding() {
            System.out.println("기다려 자기야, 지금 나갈께!");
        }

        // 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
        public static void main(String[] args) {
            Elvis elvis = Elvis.INSTANCE;
            elvis.leaveTheBuilding();
        }
    }
  • 단일 원소를 가진 열거타입으로 리플렉션 공격도 완벽히 막아 준다.
  • 부자연스러운 부분이 있지만,대부분의 상황에서는 해당 방법이 싱글턴을 만드는 가장 좋은 방법
  • 단, enum class는 extends 할 수 없기 때문에, 다른 클래스를 상속해야한다면 사용이 불가능 하다.
  • Enum을 사용하여 싱글톤을 구현하는 경우는 드물겠지만, Enum의 특성을 이용한 좋은 예시이다.

도대체 이 코드가 어디에서 쓰이는 것인지,
이 필드에는 어떤 값들만 허용 가능한 것인지,
A값과 B값이 실제로는 동일한 것인지,
전혀 다른 의미인지,이 코드를 사용하기 위해 추가로 필요한 메소드들은 무엇이고,
변경되면 어디까지 변경해야하는 것인지 등등불확실한 것들이 너무 많았던 상황에서 Enum을 통해 확실한 부분과 불확실한 부분을 분리할 수 있었습니다.
특히 가장 실감했던 장점은 문맥(Context)을 담는다는 것이였습니다.

Enum에 새로운 관점을 보게 해준 우아한형제들 기술 블로그 Java Enum 활용기 에 나온 내용입니다.

요약하는 과정에서 생략된 부분이 많으니, 이글을 읽어보신 분은 해당 글도 꼭 한번 읽어보시기 바랍니다.

감사합니다 :)

profile
개인 기술 블로그 - 개념정리/프로젝트/테스트/오류/짧은지식

0개의 댓글