우아한테크코스 레벨 1, 로또 자동 미션 진행중이에요.
각 등수별 당첨 횟수를 집계한 데이터를 어떻게 전달해야할까...
이 부분이 가장 고민이 됐었어요.
로또 당첨 결과의 형태
View에 전달될 DTO의 필드는 어떻게 구성할 것인가?
{
"winTimesByRank" : {
"first": 0,
"second": 0,
"third": 2,
"fourth": 3,
"fifth": 5
},
"rateOfReturn": 1.05
}
웹 환경에서 로또 당첨 결과를 반환하는 API를 만든다고 가정하고,
어떻게 구성하면 API 사용자 입장에서 사용하기 편할까 고민해봤습니다.
최종적으로 위 형태로 반환을 하는 게 좋다고 판단했습니다.
각 등수별 당첨 횟수에 직접 접근이 가능하다는 점,
현재 비즈니스 필요에만 집중한다는 점,
이렇게 두 가지에 주안점을 두고 설계했습니다.
public class LottoResultDto {
private final Map<Rank, Long> winningCountByRank;
private final double rateOfReturn;
// constructor...
public Long getWinningCountByRank(Rank rank) {
return winningCountByRank.getOrDefault(rank, ZERO_FOR_INVALID_KEY);
}
public double getRateOfReturn() {
return rateOfReturn;
}
}
그리고 위 JSON 형태의 데이터를 현재 로또 미션에 대입하여 DTO로 표현해봤습니다.
rateOfReturn 은 getter로 직접 꺼내서 사용할 수 있고,
각 등수별 당첨 횟수는 원하는 등수 Enum Rank를 전달하면 당첨 횟수를 반환받을 수 있게 구성했습니다.
그럼 View에게 어떻게 전달할지는 정해졌으니,
구현을 하러 가봅시다!
당첨 결과 집계의 역할은 누가?
당첨 결과를 집계하는 행위는 누구의 역할인가?!
로또 묶음? 당첨 번호? 제3의 누군가?
위 그림은 현재 저의 로또 결과 집계 및 출력 프로세스 흐름도입니다. (깃허브 링크입니다!)
당첨 결과 집계의 책임을 누구에게 맡길 것인지는 세 가지 정도 옵션이 있었던 것 같아요.
1. 로또 묶음 Lottos에게 당첨 번호 WinningLotto 를 줘서 집계하게 한다.
2. WinningLotto 에게 Lottos 를 줘서 집계하게 한다.
3. 별도의 핸들러를 만들어서 WinningLotto 와 Lottos 를 줘서 집계하게 한다.
그 중에서 저는 3번을 택했습니다.
만약 1번과 2번 옵션 중 하나를 선택하게 되면, 도메인과 DTO간의 결합이 발생하는 점,
WinningLotto와 Lottos는 각각의 정보를 보관할 뿐인데 어울리지 않는 책임을 담당하게 되는 점,
등의 이유로 3번을 선택했습니다.
LottoResultHandler는 결과를 집계해서 DTO를 반환하는 역할만 수행한다는 점,
DTO에 수정이 발생하더라도 WinningLotto, Lottos에 수정이 발생하지 않는다는 점,
코드 흐름이나 클래스명으로 봤을 때 역할이 직관으로 전달 가능하다는 점이 장점으로 느껴졌습니다.
더 좋은 설계가 없을 거다! 라는 확신이 들지는 않지만..
현재의 설계의 트레이드 오프가 무엇인지 이해하고 결정하려 노력해봤습니다.
당첨 결과 집계의 구현은 어떻게?
java.util.stream.Collectors.groupingBy !
private static Map<Rank, Long> getLottoResultGroupByRank(WinningLotto winningLotto, Lottos lottos) {
return lottos.getLottos()
.stream()
.map(winningLotto::getRankByLotto)
.collect(groupingBy(rank -> rank, () -> new EnumMap<>(Rank.class), counting()));
}
당첨 정보를 가진 WinningLotto에게
Lottos 내부의 로또 여러 게임 묶음인 List<Lotto>를 전달해서,
당첨 결과 Enum Rank를 Key로, 갯수 Long을 Value로 갖는 EnumMap으로 반환하도록 구성했습니다.
groupingBy 를 몰랐을 때에는
Map을 직접 만들고,
Enum들을 하나씩 만날 때마다 맵에서 값을 꺼내고
1을 더해서 다시 넣어주는 식으로 구현했었는데,
이렇게 구현하니 코드가 매우 지저분해졌었어요.
groupingBy를 통해 코드가 한결 간결해졌습니다.
추가적으로 groupingBy에 두번째 파라미터로 전달된 new EnumMap의 코드는
groupingBy에서 집계한 결과를 EnumMap으로 반환하게 만드는데요,
Enum을 Key로 갖는 Map을 사용할 경우, 성능상 이점이 있어서 EnumMap 사용을 권장합니다.
이펙티브 자바 아이템 37번을 참고해주세요.
EnumMap, 니가 그렇게 빨라?
정말 차이가 많이 날까?
성능상 이점이 있다, 몇 %의 성능 차이가 있다 등의 이야기는
개인적으로 바로 듣기보단 의심해보는 편이에요.
가끔은 유의미하지 않은 차이가 있을 뿐인데도,
성능이라는 이름으로 유지보수하기 좋은 코드, 가독성 좋은 코드를 후순위로 둘 때가 있거든요.
완전한 실험은 아니겠지만, 그래도 제가 할 수 있는 실험을 한 번 해봤습니다!
private static final int TWO_BILLION = 2_000_000_000;
private static final String TEST_STRING = "문자열";
@Test
void enumMapHashMapTest() {
long mapStarted = System.currentTimeMillis();
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < TWO_BILLION; i++) {
map.put(1, TEST_STRING);
}
for (int i = 0; i < TWO_BILLION; i++) {
map.get(1);
}
long mapFinished = System.currentTimeMillis();
long enumMapStarted = System.currentTimeMillis();
Map<Rank, String> enummap = new EnumMap<>(Rank.class);
for (int i = 0; i < TWO_BILLION; i++) {
enummap.put(Rank.FIRST, TEST_STRING);
}
for (int i = 0; i < TWO_BILLION; i++) {
enummap.get(Rank.FIRST);
}
long enumMapFinished = System.currentTimeMillis();
System.out.println("HashMap Operation took " + (mapFinished - mapStarted));
System.out.println("EnumMap Operation took " + (enumMapFinished - enumMapStarted));
}
20억번 같은 Key에 같은 Value를 put하고, 20억번 같은 Key로 get 하는 실험을 해봤습니다.
HashMap은 22초, EnumMap은 5초 정도 소요됐습니다.
HashMap은 값을 저장하거나 꺼낼 때 Hash Function을 수행해야 하고,
해시 충돌에 대한 처리까지 필요하기 때문에 성능에 저하가 불가피합니다.
반면 EnumMap은 Key에 올 수 있는 값들이 Enum으로 이미 정해져있기 때문에,
Hash Function이나 해시 충돌 방지 등의 절차 없이, 배열의 인덱스값으로 직접 접근 가능하며,
추가적으로 Enum의 정의된 순서를 보장합니다.
이 정도면 상당히 유의미한 수준의 성능 차이를 보인다고 판단되며,
결론적으로
Enum을 Key로 사용하는 Map 자료구조를 사용할 경우, EnumMap을 사용하라
라고 단정적으로 말할 수 있을 것 같습니다.
EnumMap.. 너.. 빠르구나 이넘...
'우아한테크코스 4기' 카테고리의 다른 글
컬렉션의 복사 방법을 정리해봅시다! (unmodifiable view / list) (6) | 2022.02.28 |
---|---|
for-loop vs IntStream, 그리고 멀티 스레딩 (4) | 2022.02.28 |
첫 오프라인 페어 프로그래밍! (6) | 2022.02.24 |
함께 자라기 - 우아한테크코스에 적용된 애자일 (0) | 2022.02.21 |
우아한테크코스 - 첫 팀 프로젝트 회고, 첫 화음 🎶 (4) | 2022.02.18 |