Spark: Collection의 flatMap을 이용한 비정상 패턴 처리

Spark: Collection의 flatMap을 이용한 비정상 패턴 처리

스칼라에서 map과 flatMap의 차이점을 파악하고 이해하는 것은 쉽지 않은것 같습니다. 이상 데이터 처리는 두 함수를 차아점을 구분하는 예제로 적합하다고 생가합니다. 이상 데이터 처리 예젤로 두 함수의 의미를 정리하겠습니다.

스칼라에서 컬렉션을 map함수로 변환시킬 때 어떤 것을 걸러 내야 할 때가 있습니다.

val x = List("taewan 45", "minsu 6", "sunny 40")
x.map{ v =>
    val Array(name, age) = v.split(" ")
    (name, age.toInt)
}

위 코드는 다음과 같이 출력됩니다.

출력:

List((taewan,45), (minsu,6), (sunny,40))

이름과 나이가 문자열과 숫자로 구분되는 튜플 리스트를 반환합니다. 입력 데이터가 모두 예상된 패턴이기에 매끄럽게 처리됩니다.

예상치 못한 입력 패턴에 대한 고민

위와 같은 깨끗한 데이터가 아닌 다음과 같은 데이터라면 어떻게 될까요?

val x = List("taewan 45", "minsu 6", "sunny secret")

나이가 비밀이라고 적혀 있네요. 이런 데이터를 위와 같은 로직으로 처리하면 어떻게 될까요?

val x = List("taewan 45", "minsu 6", "sunny secret")

출력은 다음과 같은 예외가 발생됩니다. 예상치 못한 타입이 출현했으니 당연할 결과입니다.

출력:

java.lang.NumberFormatException: For input string: "secret"
  at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
  at java.lang.Integer.parseInt(Integer.java:580)
  at java.lang.Integer.parseInt(Integer.java:615)
  at scala.collection.immutable.StringLike$class.toInt(StringLike.scala:272)
  at scala.collection.immutable.StringOps.toInt(StringOps.scala:29)
  at $anonfun$1.apply(<console>:81)
  at $anonfun$1.apply(<console>:79)
  at scala.collection.immutable.List.map(List.scala:277)
  ... 58 elided

예상치 못한 입력 패턴에 대한 대응

예상치 못한 패턴에 대하여 다음과 같이 예외 처리를 할 수 있습니다.

val x = List("taewan 45", "minsu 6", "sunny secret")
x.map{ v =>
    val Array(name, age) = v.split(" ")
    try{
        Some(name, age.toInt)
    }catch{
        case _:NumberFormatException => None
    }
}

결과는 다음과 같습니다. 왠지 결과가 마음에 들지 않습니다. None객체가 포함되어 있고 제가 원하는 튜플이 아닌 Some객체입니다.

출력:

List(Some((taewan,45)), Some((minsu,6)), None)

저는 튜플 객체 리스트를 원합니다. 그리고 불필요한 None도 제거하고 싶습니다.

예상치 못한 입력 패턴에 대한 해법: flatMap과 Some/None

위와 같은 불필요한 요소를 제거하기 위해서는 filter 함수를 추가하면 됩니다. 그러나 filter함수를 추가하면 map 다음에 추가적인 연산이 적용되고 불필요한 오버헤드를 유발할 수 있습니다. 컬렉션 처리에 한 단계가 더 추가되는 상황이 됩니다. 좀 지져분한 느낌이 듭니다.

이렇게 오류가 발생한 데이터를 제거하고 결과를 컬렉션 데이터를 변형하여 다른 컬렉션을 만들어야 하는 상황에서, flatMat과 Some/None을 사용하면 이런 기능을 간단하게 구현할 수 있습니다.

val x = List("taewan 45", "minsu 6", "sunny secret")
x.flatMap{ v =>
    val Array(name, age) = v.split(" ")
    try{
        Some(name, age.toInt)
    }catch{
        case _:NumberFormatException => None
    }
}

출력은 다음과 같습니다.

출력:

List[(String, Int)] = List((taewan,45), (minsu,6))

비정상 입력 데이터를 제거하고 정상적인 데이터만로 새로운 컬렉션을 만들었습니다.

flatMap 함수는 출력 결과로 부터 컨테이너를 제거하는 과정을 한번더 수행합니다. 이 과정에서 랩핑된 객체를 제거합니다. 이때 None 요소는 자연스럽게 제거 됩니다. 위 코드를 보면 flatMap을 사용하고 내부적으로 정상적이면 Some 객체로, 비정상 포멧은 None을 반환합니다.

그리고 flatMap은 최상위 컨테이너를 제거하고 내부 객체를 반환합니다. 이 과정에서 None 객체는 완전히 제거되고, Some 객체가 벗겨진 내부 객체가 등장하게 됩니다.

결과적으로 비 정상적인 데이터 패턴을 걸러내는 기능을 제공하게 됩니다.

flatMap에 대한 이해

flatMap은 차원을 낮춘다고 표현하시는 분들도 있습니다. 처음에는 flatMap은 차원을 낮추는 기능이다라고 표현하는 것을 이해하기 어려웠습니다. 사실 이 표현은 List, Array 같은 컬렉션 혹은 Some과 같이 어떤 객체를 감싸는 최상위 객체를 제거하고 그 안의 요소 혹은 객체를 반환하여 컬렉션을 만드는 기능을 제공한다와 같은 의미입니다.

flatMap과 Some/None 객체를 이용하면 불안전한 입력 데이터를 효과적으로 처리할 수 있습니다.

김태완 avatar
작성자: 김태완
1999년 부터 Java, Framework, Middleware, SOA, DB Replication, Cache, CEP, NoSQL, Big Data, Cloud를 키워드로 살아왔습니다. 현재는 빅데이터와 Machine Learning을 중점에 두고 있습니다.
E-mail: taewanme@gmail.com