Enum을 사용하면 value 를 조회 할 일이 굉장히 많습니다.
보통 이때 stream 으로 순회 조회를하거나 Enum 내부에 map 을 만들어 놓고 조회하는 방식을 사용합니다.
저는 원래 후자로 사용하곤 했는데, 코드의 가독성과 응집력을 이유로 전자로 바꿔서 사용하게 되었습니다.
코드의 응집력?
map 방식 조회를 하게 되면 "1. map 을 만드는 로직 2. 조회하는 로직" 두개로 분리 되게 된다.
그렇다면 코드 응집력이 떨어지고, 중간에 다른 코드가 끼어들어 코드의 목적성이 옅어질 여지가 있다.
하지만 이번에 코드 리뷰를 받으며 성능 문제가 언급 되어 정말 어느정도의 성능 차이가 있나 궁금해서 실험해보게 되었습니다.
public enum MainMenu {
EXIT("exit", ControllerAdapter::handleExit),
CREATE_VOUCHER("create voucher", ControllerAdapter::createVoucher),
FIND_ALL_VOUCHER("list voucher", ControllerAdapter::getAllVouchers),
FIND_VOUCHER("find voucher", ControllerAdapter::getVoucherById),
DELETE_VOUCHER("delete voucher", ControllerAdapter::deleteVoucherById),
UPDATE_VOUCHER("update voucher", ControllerAdapter::updateVoucher),
CREATE_USER("register user", ControllerAdapter::createUser),
LIST_BLACK_USER("list black user", ControllerAdapter::getBlackList),
CREATE_USER_VOUCHER("register my voucher", ControllerAdapter::createUserVoucher),
FIND_USER_BY_VOUCHER("find users by voucher Id", ControllerAdapter::findUserByVoucherId),
FIND_VOUCHER_MINE("find my voucher", ControllerAdapter::findVoucherByUserNickname),
DELETE_VOUCHER_MINE("delete my voucher", ControllerAdapter::deleteUserVoucherById),
;
private final String command;
private final BiFunction<ControllerAdapter, Object[], ConsoleResponse> function;
}
메인 메뉴 선택지를 가지고 있는 Enum 입니다. 상수를 총 12개 가지고 있습니다.
각 상수는 command 와 BiFunction 을 필드로 가지고 있습니다.
테스트 할 함수는 command 를 인자로 받아 같은 command를 가진 상수를 찾아 BiFunction으로 연결해주는 역할을 합니다.
기존 함수 (stream 버전)
public static ConsoleResponse routeToController_stream(
ConsoleRequest req,
ControllerAdapter controllerAdapter
) {
return Stream.of(values())
.filter(menuCommand -> menuCommand.getCommand().equals(req.getCommand()))
.findFirst()
.map(menuCommand -> {
if (req.getBody().isPresent()) {
return menuCommand.execute(controllerAdapter, req.getBody().get());
} else {
return menuCommand.execute(controllerAdapter);
}
})
.orElseThrow(() -> new CustomException(INVALID_MENU));
}
map 부분 때문에 복잡해 보이는데, .findFirst()
전 stream으로 매칭되는 command 를 찾고 있습니다.
map을 활용한 버전
private static final Map<String, MainMenu> map =
Collections.unmodifiableMap(Stream.of(values())
.collect(Collectors.toMap(MainMenu::getCommand, Function.identity())));
...
public static ConsoleResponse routeToController_map(
ConsoleRequest req,
ControllerAdapter controllerAdapter
) {
var result = map.get(req.getCommand());
if (result != null) {
if (req.getBody().isPresent()) {
return result.execute(controllerAdapter, req.getBody().get());
} else {
return result.execute(controllerAdapter);
}
}
throw new CustomException(INVALID_MENU);
}
위에 Enum 정적 필드로 command 를 key 로 하고 상수를 value 로 하는 map 을 만들어 주었습니다.
함수 내부는 if 문으로 매칭 되는 값이 있나 처리를 하였습니다.
// command 를 파라미터에서 받을 Request 형태로 변경해서 List에 저장
private List<ConsoleRequest> requests =
Stream.of(MainMenu.values())
.map(menu -> new ConsoleRequest(menu.getCommand())).toList();
private final List<Long> streamPerformances = new ArrayList<>();
private final List<Long> mapPerformances = new ArrayList<>();
@Test
void routeToController_map() {
for (int i = 0; i < 10000; i++) {
var index = i % MainMenu.values().length;
long startNanoTime = System.nanoTime();
MainMenu.routeToController_map(requests.get(index), controllerAdapter);
long endNanoTime = System.nanoTime();
long durationNano = endNanoTime - startNanoTime;
mapPerformances.add(durationNano);
}
}
@Test
void routeToController_stream() {
for (int i = 0; i < 10000; i++) {
var index = i % MainMenu.values().length;
long startNanoTime = System.nanoTime();
MainMenu.routeToController_stream(requests.get(index), controllerAdapter);
long endNanoTime = System.nanoTime();
long durationNano = endNanoTime - startNanoTime;
streamPerformances.add(durationNano);
}
}
@AfterAll
void showResult() {
System.out.println("스트림 최고 시간 : " + streamPerformances.stream().max(Long::compareTo).get());
System.out.println("스트림 평균 시간 : " + streamPerformances.stream().mapToLong(Long::longValue).average()
.getAsDouble());
System.out.println();
System.out.println("맵 최고 시간 : " + mapPerformances.stream().max(Long::compareTo).get());
System.out.println("맵 평균 시간 : " + mapPerformances.stream().mapToLong(Long::longValue).average()
.getAsDouble());
}
최고 시간과 평균 시간 2가지를 측정
평균적으로 실행 하였을때 2배정도 속도가 차이난다고 보면 될 것 같습니다.
하지만 0.01ms 차이라 거의 차이가 없다고 봐도 무방 합니다.
더 쉬운 예시로 함수를 적을 수도 있지만 가독성을 보시라고 실제 사용하는 예시로 테스트 진행했습니다.
저는 stream 을 많이 써서 사실 stream 쪽이 더 가독성이 좋은데, (map은 map을 보기 위해 한번 더 위로 올라가서 볼 것 같음) map 도 변수명을 가독성 있게 잘 작성하면 충분히 가독성을 챙겨 갈 수 있을 것 같습니다.
저는 매 요청마다 호출 되는 함수라 map 방식으로 변경을 진행하였습니다.
자신의 case에 맞게 선택해서 사용하시면 될 것 같습니다!
평균시간과 최대시간 모두 측정해서보니 스트림쪽이 최대시간이 꽤나 오래걸리네요.. 왜 그런거려나요?? 저라면 평균시간도 map 압승일뿐더러 최대시간이 느리게 나오는것때문에 map 쓸거같네요 ㅎㅎ