elevne's Study Note
Java Enum 본문
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);
}
}
}
그 다음으로는 valueOf() 메서드가 있다. 이는 전달된 문자열과 일치하는 해당 열거체의 상수를 반환한다.
import test_classes.TestEnum;
public class Main {
public static void main(String[] args) {
System.out.println(TestEnum.valueOf("RED"));
}
}
그 외에도 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);
}
}
위와 같이 더욱 간결하고 보기 쉽게 코드가 표현된다. 또한, 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);
}
}
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());
}
}
마지막으로 관리 주체를 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);
}
}
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:
'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 |