Skip to content

Commit

Permalink
feat(함수형인터페이스.md): 우테코 프리코스에서 함수형 인터페이스 적용기
Browse files Browse the repository at this point in the history
  • Loading branch information
02ggang9 committed Nov 5, 2023
1 parent e38ba14 commit 99b4e2b
Showing 1 changed file with 158 additions and 0 deletions.
158 changes: 158 additions & 0 deletions _posts/wooteco/2023-11-05-함수형인터페이스.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
published: true
title: "우아한테크코스 - 함수형 인터페이스"
categories:
- wooteco
---

## 서론
이번 우아한 테크코스의 프리코스 과정에서 Enum을 적극적으로 사용하라는 요구사항이 있었습니다. 우아한 기술블로그, nextree 블로그에서 함수형 인터페이스와 Enum을 같이 활용하는 모습을 보고 이번 우테코 로또 미션에 적용해 봤습니다. 아래에서는 많이 사용하는 함수형 인터페이스의 종류에 대해서 살펴보고 이번 우테코에서 어떻게 적용했는지 간략하게 알아보도록 하겠습니다.


## 함수형 인터페이스 종류
자바 8에 추가된 함수형 인터페이스의 종류는 크게 5가지 있습니다. 이 외에도 정말 여러가지가 있지만 평소 스트림을 자주 사용하면서 써먹었던 종류들을 가져왔습니다.


| 함수형 인터페이스 | 함수 디스크립터 |
|--------------|--------------|
| Supplier<T> | () -> T | Collect
| Consumer<T> | T -> void | forEach
| Predicate<T> | T -> boolean | filter
| Function<T, R> | T -> R | map
| BiFunction<T, U, R> | (T, U) -> R |


Supplier는 아무런 인자를 받지 않고 하나의 반환 값을 리턴해 줍니다. 아래에서 랜덤한 6개의 숫자를 뽑고 정렬한 후 collect()를 사용해 아무런 인자를 받지 않고 리스트를 생성해 줍니다.

~~~java
private Lotto generateLotto() {
List<Integer> randomNumbers = Randoms.pickUniqueNumbersInRange(MIN_RANGE_VALUE, MAX_RANGE_VALUE, NUMBER_OF_PICK)
.stream()
.sorted(Integer::compareTo)
.collect(Collector.toList());

return new Lotto(randomNumbers);
}
~~~

Consumer는 하나의 타입을 받고 반환 값을 리턴하지 않습니다. 예를 들어 아래에서 list의 원소 값들을 하나씩 받으면서 출력하는 모습을 확인할 수 있습니다. 여기서 T의 타입은 Lotto 입니다.

~~~java
public void presentLottoList(final List<Lotto> lottos) {
System.out.println();
System.out.println(lottos.size() + PURCHASED_LOTTO_COUNT.getMessage());
lottos.forEach(System.out::println);
}
~~~

Predicate는 하나의 타입을 받고 boolean 타입을 리턴해 줍니다. 아래에서 winningNumber의 원소를 하나씩 가져온 다음에 numbers 리스트에 포함되는지 확인하고 맞으면 true, 틀리면 false 반환하고 true인 개수를 카운트 합니다.

~~~java
private int countWinningNumbers(final List<Integer> winningNumbers) {
return (int) winningNumbers.stream()
.filter(this.numbers::contains)
.count();
}

public WinningLotto compareWinningNumbers(final List<Integer> winningNumbers, final Integer bonus) {
int matchWinningNumbersCount = this.countWinningNumbers(winningNumbers);
int matchBonusCount = this.countBonusMatch(bonus);

return Arrays.stream(WinningLotto.values())
.filter(winningLotto -> winningLotto.getLottoRank(matchWinningNumbersCount, matchBonusCount))
.findFirst()
.orElse(WinningLotto.LAST_PLACE);
}
~~~

Function은 하나의 타입을 받고 하나의 타입을 리턴해 줍니다. 아래에서 map을 활용해 lotto 타입을 받고 lotto 타입을 반환하는 것을 확인할 수 있습니다.

~~~java
public LottoResult showLottoResult(final List<Integer> winningNumbers, final Integer bonus, final List<Lotto> lottos) {
this.winningLottos = lottos.stream()
.map(lotto -> lotto.compareWinningNumbers(winningNumbers, bonus))
.toList();

float averageOfPrice = (float) this.winningLottos.stream()
.mapToLong(WinningLotto::getPrice)
.average()
.getAsDouble();

this.rateOfReturn = averageOfPrice / 10;

return this;
}
~~~


## 프리코스 실습 (BiFunction)
BiFunction은 Function과 거의 비슷하고 입력 받는 타입이 하나 더 늘어난 것 뿐입니다. 하지만 이런 BiFunction과 Enum을 함께 사용하는 것이 신박했습니다.

BiFunction은 apply 메서드를 이용해 인자를 넘깁니다. 따라서 function.apply(6,0)을 넘긴다면 matchCount은 6이 되므로 참을 반환합니다.

~~~java
public enum WinningLotto {

FIRST_PLACE(6, 0, 2000000000, (matchCount, bonusCount) -> matchCount == 6),
SECOND_PLACE(5, 1, 30000000, (matchCount, bonusCount) -> matchCount == 5 && bonusCount == 1),
THIRD_PLACE(5, 0, 1500000, (matchCount, bonusCount) -> matchCount == 5 && bonusCount == 0),
FOURTH_PLACE(4, 0, 50000, (matchCount, bonusCount) -> matchCount == 4),
FIFTH_PLACE(3, 0, 5000, (matchCount, bonusCount) -> matchCount == 3),
LAST_PLACE(2, 0, 0, (matchCount, bonusCount) -> matchCount <= 2),
;

WinningLotto(int matchCount, int bonusCount, long price, BiFunction<Integer, Integer, Boolean> function) {
this.matchCount = matchCount;
this.bonusCount = bonusCount;
this.price = price;
this.function = function;
}

public boolean getLottoRank(final int matchCount, final int bonusCount) {
return function.apply(matchCount, bonusCount);
}
}
~~~

이렇게 BiFunction 함수형 인터페이스를 사용한다면 if-else문을 사용하는 코드를 깔끔하게 제거할 수 있습니다.

~~~java
// Before
private void saveResult(List<Integer> result) {
Integer winningNumberMatchCount = result.get(0);
Integer bonusMatchCount = result.get(1);

if (winningNumberMatchCount == 3) {
matchesThree++;
} else if (winningNumberMatchCount == 4) {
matchesFour++;
} else if (winningNumberMatchCount == 5 && bonusMatchCount == 1) {
matchFiveAndBonus++;
} else if (winningNumberMatchCount == 5 && bonusMatchCount == 0) {
matchFive++;
} else if (winningNumberMatchCount == 6) {
matchSix++;
}
}

// After
public enum WinningLotto {

FIRST_PLACE(6, 0, 2000000000, (matchCount, bonusCount) -> matchCount == 6),
SECOND_PLACE(5, 1, 30000000, (matchCount, bonusCount) -> matchCount == 5 && bonusCount == 1),
THIRD_PLACE(5, 0, 1500000, (matchCount, bonusCount) -> matchCount == 5 && bonusCount == 0),
FOURTH_PLACE(4, 0, 50000, (matchCount, bonusCount) -> matchCount == 4),
FIFTH_PLACE(3, 0, 5000, (matchCount, bonusCount) -> matchCount == 3),
LAST_PLACE(2, 0, 0, (matchCount, bonusCount) -> matchCount <= 2),
;

public boolean getLottoRank(final int matchCount, final int bonusCount) {
return function.apply(matchCount, bonusCount);
}
}
~~~

## 결론
이번에는 함수 인터페이스들의 종류에 대해서 알아봤습니다. 평소 스트림을 사용할 때 Command + P를 누르면 인자 타입의 힌트를 얻을 수 있는데 Supplier? Predicate? 가 나올 때 마다 이해할 수 없었는데 이번 기회에 정리해 봤습니다.

또 이번 프리코스 요구사항 중 if-else문 사용 금지가 있었는데 어떻게 WinningLotto의 종류를 확인할까.. 깊은 고민을 했는데 BiFunction + Enum을 사용하면 깔끔한 코드를 작성할 수 있었습니다. 하지만 오브젝트 책을 읽으면서 Enum은 객체와의 의존성을 높이고 응집도를 떨어트린다고 합니다. 따라서 상속과 합성을 사용하라고 제시했는데 아직 머릿속에 잘 떠오르지 않습니다. 아마 다음 미션은 상속에 관한 미션이 나오지 않을까 조심스럽게 예상해봅니다.

0 comments on commit 99b4e2b

Please sign in to comment.