FP(함수형 프로그래밍)의 이점 - 예제

Keunjae Song·2020년 3월 11일
1

fp

목록 보기
2/6

스칼라로 배우는 함수형 프로그래밍을 읽고 정리한 글입니다.

이 글에서는 FP의 이점을 예제를 통해 소개합니다.

부수 효과가 있는 예제

커피숍에서 커피를 구매하는 과정을 처리하는 프로그램을 작성한다고 해보자.

class Cafe {
  def buyCoffee(cc: CreditCard): Coffee = {
    val cup = new Coffee()
    // 신용카드로 실제 대금을 청구한다.
    // side-effect 발생
    cc.charge(cup.price)	
    cup
  }
}

위 예제에서는 cc.charge(cup.price)가 부수 효과(side effect)이다.
신용카드 청구는 결국 어떤 웹 서비스를 이용해 신용 카드 회사와 정보를 주고 받으며 거래 승인 - 대금 청구 - 거래 기록 저장 등 여러 작업이 수행된다.
그렇기에 buyCoffee 함수는 결국 Coffee 객체를 돌려주는 것 이외에 신용카드 청구라는 부수 효과가 있는 non-pure function이 되어 버린 것이다.

해당 부수 효과가 있기 때문에 buyCoffee에 대한 테스트는 매우 어려워진다.
왜냐하면 실제로 함수를 테스트해보기 위해 신용카드 회사와 접촉하여 여러 정보를 주고 받아봐야 하기 때문이다.

문제점은 이것 뿐만이 아니다.
만약 한 사람이 커피 열두 잔을 주문한다고 하면, 이에 대한 구현을 어떻게 할 것인가?
결국 루프를 돌면서 buyCoffee를 열두 번 호출해야 할 것이다.
그럼 결국 신용카드 회사에 열두 번 청구해야 하고, 카드 수수료가 추가되므로 커피숍에도 손해가 갈 것이다.
즉, 현재 buyCoffee는 부수 효과가 있으며 재사용이 어려운 함수이다.

부수 효과 제거

buyCoffee의 기존 문제(부수 효과 존재, 재사용 어려움)를 FP를 통해 해결할 수 있다.
buyCoffee가 Coffee 뿐만이 아니라 신용카드 청구 건을 하나의 값으로 같이 return하게 구현하는 것이다.
신용카드 청구 문제는 buyCoffee 말고 바깥 어딘가에서 해결하도록 한다.
그럼 일단 buyCoffee 내부에서는 신용카드 청구를 수행하지 않기에 부수 효과가 없어지게 된다.

class Cafe {
  // 이제 buyCoffee는 Coffee 말고도 
  // 신용카드 청구 건을 나타내는 Charge도 같이 pair로 return한다.
  def buyCoffee(cc: CreditCard): (Coffee, Charge) = {
    val cup = new Coffee()
    (cup, Charge(cc, cup.price))
  }
}

이렇게 구현을 바꾸면 이제 함수는 청구 건을 생성만 할 뿐, 신용카드 회사에 청구를 요청하는 처리 작업을 수행하지 않는다.

여기서 Charge는 user-defined type으로서 신용 카드에 해당하는 cc(type: CreditCard)와 청구 금액에 해당하는 amount(type: Double)을 멤버로 가지고 있다.
또한, 동일한 CreditCard에 대한 청구건들을 취합하는 combine 함수도 가지고 있다.

case class Charge(cc: CreditCard, amount: Double) {
  def combine(other: Charge): Charge =
    if (cc == other.cc)
      Charge(cc, amount + other.amount)
    else
      throw new Exception("Can't combine charges to different cards")
}

결국, buyCoffee를 부수 효과를 없애고 Coffee와 Charge를 pair로 return하게 만들었다.
또한, 이에 따라 Charge라는 user-defined type을 만들었고 멤버 함수인 combine을 구현하였다.

이 두가지 사항을 통해 buyCoffee의 재사용성이 높아졌으므로 이제 n잔의 커피에 대한 구매 처리를 좀 더 간단하게 할 수 있다.

n 잔의 커피를 처리하는 buyCoffees라는 함수를 정의해보자.

def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = {
  //  n개짜리 List에 buyCoffee로 생성된 Coffee, Charge를 담는다.
  // Coffee, Charge pair들의 리스트가 생긴다.
  val purchases: List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc))
  // 여러 개의 청구 건(Charge)을 하나의 청구 건으로 만든다.
  // unzip을 이용해 List[(Coffee, Charge)]를 (List[Coffee], List[Charge])로 만든다.
  val (coffees, charges) = purchases.unzip
    (coffees, charges.reduce((c1, c2) => c1.combine(c2)))
}

한줄 한줄 뜯어보자.
val purchases: List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc))
n개의 (Coffee, Charge)를 담는 List를 만드는 데, 각각의 pair(튜플)를 buyCoffee(cc)로 생성한다.

val (coffees, charges) = purchases.unzip(coffees, charges.reduce((c1, c2) => c1.combine(c2)))
먼저 unzip을 통해 List[(Coffee, Charge)] 형태의 데이터를 (List[Coffee], List[Charge]) 형태로 만든다.
이는 Coffee는 Coffee끼리, Charge는 Charge끼리 가지고 있고 싶기 때문이다.
그 다음 우리가 원하는 건 같은 신용카드 청구 건들끼리는 하나의 청구건으로 만드는 것이었으므로 reduce와 combine을 이용해 List[Charge]를 Charge로 변환한다.

예제를 통해 알아낸 FP의 이점

이로써, buyCoffee의 부수 효과를 걷어내고 재사용성이 증가한 함수로 변하니, 이로 인해 같은 신용카드 청구 건들을 하나의 청구건으로 만드는 기능이 생각보다 쉽게 구현될 수 있었다.

결국 부수 효과들을 걷어내어 함수의 재사용성이 크게 증가해 추가되는 기능들을 쉽게 구현할 수 있을 것으로 예측이 가능해진다.

또한 테스트도 쉬워진다.
부수 효과가 있는 기존 buyCoffee 함수를 테스트한다고 하면, 실제 신용카드 회사와 컨택까지 하여 청구를 요청하고 정확한 가격이 결제되었는지 확인해야 했을 것이다.

그러나 부수 효과를 제거하여 청구 요청에 대한 생각을 다른 어딘 곳에다 맡겨버린 새로운 버전의 buyCoffee는 테스트하기 위해 신용카드 회사와 컨택을 할 필요가 전혀 없어졌다.
단지, 청구할 금액(return되는 pair 중 Charge의 amount)과 커피 한 잔(return되는 pair 중 Coffee의 price 멤버)의 가격이 같은지만 테스트해보면 된다.

0개의 댓글