(java) Enum field에 Enum List를 사용하며 발생한 코드 리팩토링
enum field에서 enum list를 사용하면서 코드가 리팩토링 되는 과정을 정리하였습니다.
리팩토링(Refactoring)이라는 다소 고급진 단어를 사용하였지만, 상당히 간단한 코드 개선 내용임을 미리 말씀드립니다.
(마지막에는 리팩토링에 따른 속도 차이가 비교되어 있으니 참고 부탁드리겠습니다.)
Enum Class
public enum Status {
READY,
PROCESSING,
CANCEL,
TERMINATE,
DONE;
}
(Status Enum Class)
public enum ViewStatus {
PROCESSING,
DONE;
}
(ViewStatus Enum Class)
예시로 사용될 두 개의 enum class입니다.
어떤 Entity에서 내부적으로 사용되는 실제 상태 값은 Status지만 클라이언트 측에서 서버에 요청을 하거나, 응답을 받을 때는 ViewStatus만을 사용한다고 가정했을 때, 이와 같은 두 개의 enum class를 사용할 수 있는데요.
(Status를 그대로 내려주고 프론트 단에서 묶어서 처리할 수도 있지만, 여기서는 백엔드에서 처리를 해야 하는 상황이라고 가정하겠습니다.)
Refactoring
ViewStatus viewStatus;
switch (status) {
case READY:
case PROCESSING:
viewStatus = ViewStatus.PROCESSING;
break;
case CANCEL:
case TERMINATE:
case DONE:
viewStatus = ViewStatus.DONE;
break;
default:
viewStatus = null;
}
해당 객체를 조회하는 API가 있고, 최종적으로 사용자에게 내려가는 ResponseDto에서 반환되는 Enum 값이 ViewStatus라고 할 때, Status 타입의 데이터를 ViewStatus로 바꿔주는 위와 같은 과정이 필요한데요.
지금 상황에서는 비교하는 enum 값이 많지 않기 때문에 변환하는 코드가 상당히 짧지만, 만약 비교하는 enum 값이 계속해서 늘어나는 경우 해당 코드는 그때마다 수정이 필요해지며 코드의 길이도 길어지게 됩니다.
그래서 이러한 문제를 개선하기 위해 코드를 아래와 같이 변경하였는데요.
@Getter
public enum ViewStatus {
PROCESSING(Arrays.asList(Status.READY, Status.PROCESSING)),
DONE(Arrays.asList(Status.CANCEL, Status.TERMINATE, Status.DONE));
private List<Status> statusList;
ViewStatus(List<Status> statusList) {
this.statusList = statusList;
}
public static ViewStatus findByStatus(Status status) {
return Arrays.stream(ViewStatus.values())
.filter(viewStatus -> viewStatus.hasStatus(status))
.findAny()
.orElse(null); //또는 orElseThrow
}
public boolean hasStatus(Status inputStatus) {
return statusList.stream()
.anyMatch(status -> status.equals(inputStatus));
}
}
다음 코드와 같이 ViewStatus enum 값의 필드가 해당되는 Status enum 값을 리스트로 가지고 있도록 수정했으며, findByStatus() 메서드와 hasStatus() 메서드를 통해 Status 값을 넣었을 때, 해당되는 ViewStatus 값을 반환하도록 코드를 수정하였습니다.
하지만 조회 API를 통해 해당 데이터가 Status -> ViewStatus 변환 과정을 거쳐 리스트로 나와야 한다면, 매 요청에서 각각의 데이터를 가져올 때마다 values() 메서드가 돌며 각각의 enum 값을 비교하게 되는데요.
때문에 요청이 많아질수록 + 요청에 대한 데이터가 많을수록 내부적인 동작으로 인해 속도가 느려질 수 있다고 판단했습니다.
private static final Map<Status, ViewStatus> map = new HashMap<>();
static {
for (ViewStatus viewStatus : values()) {
for (Status status : viewStatus.getStatusList()) {
map.put(status, viewStatus);
}
}
}
public static ViewStatus findByStatus(Status status) {
return map.get(status);
}
때문에 위 코드를 한번 더 수정했는데요.
ViewStatus 내부적으로 다음과 같이 Status 타입을 key로 가지고 ViewStatus 타입을 value로 가지는 static final map 객체를 하나 만들어, 컴파일 시점에서 해당 enum 값들을 모두 등록시킨 뒤 사용되는 시점에서는 Status 값을 통해 map에서 value를 가져오는 방식으로 변경하였습니다.
Test
기본적인 속도가 빠르기 때문에 저 정도의 작업은 크게 속도 차이가 없을 것 같기는 하지만, 확실히 map을 사용하여 컴파일 시점에 값들을 등록하고 key 값을 통해 map에서 value 값을 빼오는 방식이 더 빠를 것 같은데요.
실제로 데이터베이스에 10만 건의 데이터를 넣어 호출하며 enum 값이 변환하는 과정의 작업 시간을 측정해 보았습니다.
10만 건의 데이터를 조회했을 때 결과는 map을 사용한 방식이 values() 메서드가 사용된 방식에 비해 10배 정도 빨랐는데요.
수치로 치면 큰 차이가 나지만 values() 메서드가 사용된 방식의 동작 시간이 0.2초 이고, map을 사용한 방식이 0.02초 이기 때문에 웬만큼 요청이 많은 서비스가 아니면 해당 작업이 서비스에 큰 영향을 주지는 않을 것 같다는 생각입니다.
< 함께 보기 좋은 enum 관련 포스팅들 >
2022.10.01 - [Programming/Java] - Java Enum 활용하기1 - AttributeConverter
2022.10.08 - [Programming/Java] - Java Enum 활용하기2 - ConverterFactory