elevne's Study Note

Java Enum 본문

Backend/Java

Java Enum

elevne 2023. 4. 19. 22:51

Enum (열거체, Enumeration Type) 에 대해서 알아보았다. C 언어와 C++ 에서는 열거체를 사용할 수 있었지만, JDK 1.5 이전의 Java 에서는 Enum 을 사용할 수 없었다고 한다. 하지만 JDK 1.5 이후로는 C 언어의 열거체보다 더욱 향상된 성능의 Enum 클래스를 사용할 수 있게되었다고 한다. 자바의 Enum 은 열거체를 비교할 때 실제값 뿐만 아니라 타입까지도 체크하며, 열거체의 상수값이 재정의되더라도 다시 컴파일할 필요가 없다는 장점이 있다. 우선 아래와 같이 매우 간단한 Enum 클래스를 생성해보았다.

 

 

 

package test_classes;

public enum TestEnum {
    RED, BLUE, GREEN
}

 

 

 

위 TestEnum 내에 RED, BLUE, GREEN 이라는 상수들을 넣었다. 이들에 접근할 때는 TestEnum.RED 와 같은 형태로 접근하면 된다. 위와 같이 정의된 Enum 의 첫 번째 상수값은 0 부터 설정되며, 그 다음은 바로 앞의 상숫값보다 1만큼 증가되며 설정된다고 한다. 사용자 정의 값을 설정하고자 한다면, 상수의 이름 옆에 괄호를 추가하고, 그 안에 원하는 상수값을 명시해줄 수 있다. 하지만, 이 때는 불규칙한 특정 값을 저장할 수 있는 인스턴스 변수와 생성자를 아래와 같이 작성해줄 필요가 있다.

 

 

 

package test_classes;

public enum TestEnum {
    RED(10), BLUE(50), GREEN(99);

    private final int value;
    TestEnum(int value) {this.value = value;}
    public int getValue() {return value;}
}

 

 

 

Enum 클래스는 모든 자바 열거체의 공통된 조상 클래스이다. 몇 가지 메서드를 기본 제공한다. 우선 values() 메서드가 있다. 이는 해당 열거체의 모든 상수를 저장한 배열을 생성하여 반환한다.

 

 

 

import test_classes.TestEnum;

public class Main {
    public static void main(String[] args) {
        TestEnum[] testEnum = TestEnum.values();
        for (TestEnum testEnums : testEnum){
            System.out.println(testEnums);
        }
    }
}

 

 

result

 

 

 

그 다음으로는 valueOf() 메서드가 있다. 이는 전달된 문자열과 일치하는 해당 열거체의 상수를 반환한다.

 

 

 

import test_classes.TestEnum;

public class Main {
    public static void main(String[] args) {
        System.out.println(TestEnum.valueOf("RED"));
    }
}

 

 

result

 

 

 

그 외에도 ordinal() (해당 열거체 상수가 정의된 순서를 반환), finalize() (해당 Enum 클래스가 final 메서드를 가지 수 없게됨), name() (해당 열거체 상수의 이름 반환)  메서드가 대표적으로 있다.

 

 

 

Enum 을 사용하게 되면 문자열과 비교했을 때, IDE 의 지원을 받아 자동완성, 오타검증, 텍스트 리팩토링 등이 가능해진다. 또 허용 가능한 값들을 제한 가능하며, 리팩토링 시 변경 범위가 최소화된다는 장점이 있다. 아래는 우아한형제들 기술블로그에서 가져온 예시이다.

 

 

 

- 기존 데이터들 간의 연관관계 표현 코드

 

package test_classes;

public class LegacyCase {

    public String table1(String originValue) {
        if ("Y".equals(originValue)){
            return "1";
        } else {
            return "0";
        }
    }

    public boolean table2(String originValue){
        if ("Y".equals(originValue)){
            return true;
        } else {
            return false;
        }
    }
}

 

 

 

위 코드를 보면 "Y", "1", true 는 모두 같은 의미라는 것을 알 수 있다. 하지만 두 개의 다른 메서드로 분리되어 있으며, 여기서 Y, N 외에 다른 값이 추가되는 경우에는 if 문을 포함한 메서드 단위로 코드가 더욱 증가하게 된다. 반복성 코드가 생기는 것을 Enum 을 활용하여 아래와 같이 바꿔볼 수 있다.

 

 

 

- Enum

 

package test_classes;

public enum Status {
    Y("1", true), N("0", false);
    private String table1Status;
    private boolean table2Status;

    Status(String table1Status, boolean table2Status){
        this.table1Status = table1Status;
        this.table2Status = table2Status;
    }

    public String getTable1Status() {
        return table1Status;
    }
    public boolean getTable2Status() {
        return table2Status;
    }

}

 

 

- Test Code

 

import test_classes.Status;
import test_classes.TestEnum;

public class Main {
    public static void main(String[] args) {
        Status status = Status.N;

        String st1 = status.getTable1Status();
        boolean st2 = status.getTable2Status();

        System.out.println(st1);
        System.out.println(st2);
    }
}

 

 

result

 

 

 

위와 같이 더욱 간결하고 보기 쉽게 코드가 표현된다. 또한, Enum 을 통해 상태와 행위를 한 곳에서 관리할 수 있다. 아래와 같이 if 문으로 값에 따라 다른 행위로 이어지는 코드를 Enum 으로 대체해볼 수 있다.

 

 

 

- 기존 계산 코드

 

package test_classes;

public class LegacyCase {
    public static long calculate(String code, long originValue){
        if ("A".equals(code)){
            return originValue;
        } else if ("B".equals(code)){
            return originValue * 10;
        } else if ("C".equals(code)){
            return originValue * 100;
        } else {
            return 0;
        }
    }
}

 

 

 

- Enum

 

package test_classes;

import java.util.function.Function;

public enum Calc {

    A(value -> value),
    B(value -> value * 10),
    C(value -> value * 100),
    ETC(value -> 0L);

    private Function<Long, Long> expression;

    Calc(Function<Long, Long> expression) {this.expression = expression;}

    public long calculate(long value) {return expression.apply(value);}

}

 

 

- Test Code

 

import test_classes.Calc;
import test_classes.Status;
import test_classes.TestEnum;

public class Main {
    public static void main(String[] args) {
        Calc calc = Calc.C;
        long originVal = 99L;
        long result = calc.calculate(originVal);

        System.out.println(result);
    }
}

 

 

result

 

 

 

Enum 을 사용하면 데이터 그룹 관리가 용이해진다. 예를 들어 결제라는 데이터 (기술블로그의 예) 는 결제종류와 결제수단이라는 두 가지의 형태로 표현된다. 현금에는 계좌이체, 무통장입금, 현장결제, 토스 등이 포함, 카드에는 신용카드, 카카오페이, 배민페이 등이 포함, 그 외에는 포인트, 쿠폰 등이 포함될 수 있다. 결제 건이 어떤 수단으로 진행되었는지 받아서 어느 결제 종류에 속하는지를 확인할 때 보통 if 문을 써서 진행하게 될 것이다. 이 경우, 가지 수가 많아지면 코드가 매우 보기 힘들어진다. 결제종류와 결제수단의 관계를 파악하기도 어려우며, 그룹별 기능을 추가하기도 어려워진다. 아래와 같은 Enum 클래스를 활용해볼 수 있다고 한다.

 

 

 

- Enum

 

package test_classes;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public enum PayGroup {
    CASH("현금", Arrays.asList("TOSS", "ACCOUNT_TRANSFER", "REMITTANCE", "ON_SITE_PAYMENT")),
    CARD("카드", Arrays.asList("PAYCO", "KAKAO_PAY", "BAEMIN_PAY")),
    ETC("기타", Arrays.asList("POINT", "COUPON")),
    EMPTY("없음", Collections.EMPTY_LIST);

    private String name;
    private List<String> payList;

    PayGroup(String name, List payList) {
        this.name = name;
        this.payList = payList;
    }

    public static PayGroup findByPayCode(String payCode) {
        return Arrays.stream(PayGroup.values())
                .filter(payGroup -> payGroup.hasPayCode(payCode))
                .findAny()
                .orElse(EMPTY);
    }

    public boolean hasPayCode(String payCode) {
        return payList.stream().anyMatch(pay -> pay.equals(payCode));
    }

    public String getName() {return name;}
}

 

 

 

위의 리스트 내에 등록된 결제수단들 또한 또다른 Enum 클래스로 분리하여 PayType.ACCOUNT_TRANSFER 과 같은 형태로 가져다 사용하는 것이 더욱 바람직하긴 하다. 

 

 

 

- Test Code

 

import test_classes.Calc;
import test_classes.PayGroup;
import test_classes.Status;
import test_classes.TestEnum;

public class Main {
    public static void main(String[] args) {
        PayGroup payGroup = PayGroup.findByPayCode("TOSS");
        System.out.println(payGroup.getName());
    }
}

 

 

result

 

 

 

마지막으로 관리 주체를 DB 에서 객체로 옮길 수 있다고 한다. 실무에서는 코드 테이블을 별도로 두고 이를 조회하며 사용하는 일이 많은데, 이러한 방향으로 일을 진행하니 코드명만 봐서는 무엇인지 알 수 없는 문제, 항상 코드 테이블 조회 쿼리가 실행되어야 하는 문제, 그리고 카테고리 코드를 기반으로한 서비스 로직을 추가할 때 위치가 애매해지는 문제 등이 발생하게 된다고 한다. 그래서 카테고리성 데이터를 DB 에서 관리하는 것이 아니라 Enum 으로 전환해볼 수 있는 것이다. 팩토리와 인터페이스 타입을 선언하여 일관된 방식으로 관리되고 사용할 수 있도록 진행될 수 있다고 한다.

 

 

 

- EnumMapperType interface

 

package test_classes;

public interface EnumMapperType {
    String getCode();
    String getTitle();
}

 

 

- EnumMapperValue class

 

package test_classes;

public class EnumMapperValue {

    public String code;
    public String title;

    public EnumMapperValue(EnumMapperType enumMapperType){
        code = enumMapperType.getCode();
        title = enumMapperType.getTitle();
    }

    public String getCode() {return code;}
    public String getTitle() {return title;}

    @Override
    public String toString() {
        return "{" +
                "code='" + code + '\'' +
                ", title='" + title + '\'' +
                "}";
    }
}

 

 

- Enum

 

package test_classes;

public enum TestEnum implements EnumMapperType {
   PERCENT("정율"), MONET("정액");

   private String title;
   TestEnum(String title) {this.title = title;}

    @Override
    public String getCode() {return name();}

    @Override
    public String getTitle() {return title;}
}

 

 

 

위와 같이 Enum 은 미리 선언한 interface 를 implement 하면 된다. 그 후 필요한 곳에서 아래와 같이 Enum 을 Value 클래스로 변환한 후 전달하면 된다고 한다.

 

 

 

- Test Code

 

import test_classes.*;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<EnumMapperValue> enumTest = Arrays.stream(TestEnum.values()).map(EnumMapperValue::new).collect(Collectors.toList());
        System.out.println(enumTest);
    }
}

 

 

result

 

 

 

JSON 결과가 나오는 것을 확인할 수 있다. 하지만 위 코드에는 Enum.values 를 통해 Value 인스턴스를, 필요할 때마다 생성하는 과정이 반복된다는 단점이 있다. 런타임에 의해 Enum 의 상수들이 변경될 일이 없기 때문에 관리 대상인 Enum 들을 Bean 에 등록하여 사용할 수 있다고 한다. 

 

 

 

package test_classes;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class EnumMapper {
    
    private Map<String, List<EnumMapperValue>> factory = new LinkedHashMap<>();
    
    public EnumMapper() {}
    
    public void put(String key, Class<? extends EnumMapperType> e) {
        factory.put(key, toEnumValues(e));
    }
    
    private List<EnumMapperValue> toEnumValues(Class<? extends  EnumMapperType> e){
        return Arrays.stream(e.getEnumConstants())
                .map(EnumMapperValue::new)
                .collect(Collectors.toList());
    }
    
    public List<EnumMapperValue> get(String key){
        return factory.get(key);
    }
    
    public Map<String, List<EnumMapperValue>> get(List<String> keys){
        if (keys == null || keys.isEmpty()){
            return new LinkedHashMap<>();
        } 
        return keys.stream()
                .collect(Collectors.toMap(Function.identity(), key -> factory.get(key)));
    }
    
    public Map<String, List<EnumMapperValue>> getAll() {return factory;}
}

 

 

 

위 클래스를 Bean 에 등록하는 것이다. 등록시 enumMapper.put("TestEnum", TestEnum.class)Map 에 값들을 넣어주고 이후에 get 메서드를 통해 value 들을 가져올 수 있는 것이다.

 

 

 

 

 

 

Reference:

http://www.tcpschool.com/java/java_api_enum

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

'Backend > Java' 카테고리의 다른 글

Java (Primitive Type, Reference Type ~)  (0) 2023.04.25
Java Gson  (0) 2023.04.21
Java Functional Interface  (0) 2023.04.18
Thread, Future, ScheduledFuture, Runnable  (0) 2023.04.13
Java Keywords ...  (0) 2023.04.12