brunch

You can make anything
by writing

C.S.Lewis

by 권순목 Jul 28. 2021

2.3 데이터 엔지니어링

데이터 과학 미니북 - 2. 데이터 가공

2.3 데이터 엔지니어링


2.3.1 데이터 엔지니어링 업무


분석 팀을 구성하는 여러 역할 중 하나로 데이터 엔지니어가 있습니다. 데이터 엔지니어가 하는 일이 바로 데이터 엔지니어링입니다. 이 단원에서는 데이터 엔지니어링 업무에 대해서 자세하게 살펴보겠습니다.


데이터 엔지니어링을 작게 보면 "데이터를 가져와서 분석하기 좋은 형태로 가공하는 것"입니다. 여기서 "분석하기 좋은 형태"란 정돈된 데이터(Tidy Data) 형식의 테이블 데이터를 말하는 것으로 보면 적절합니다. 이러한 의미의 데이터 가공 작업을 ETL 작업이라고 부르기도 합니다. 이것의 의미는 Extracting, Transforming, Loading으로, 데이터를 여러 소스에서 추출하여, 변형한 다음, 통계학자나 머신 러닝 엔지니어가 접근할 수 있는 저장소에 저장한다는 뜻입니다. 데이터 분석 팀의 규모나 성격에 따라 이러한 작은 의미의 데이터 엔지니어링 업무 정의로 충분할 수도 있습니다.


데이터 엔지니어링을 크게 보면 "컴퓨터/소프트웨어 공학과 빅데이터가 만나는 영역 전체"라고 할 수 있습니다. 소프트웨어 공학의 목적 중 하나는 "대규모의, 복잡도 높은, 장기간 운영될 소프트웨어 시스템을 여러 사람의 협력으로 개발하고 유지 보수해 가는 것"이라 할 수 있습니다. 이러한 특징을 가지는 시스템이 빅데이터의 가공을 목적으로 하는 경우 데이터 엔지니어링 시스템 또는 데이터 파이프라인이라고 부를 수 있으며, 그런 시스템을 만드는 업무를 데이터 엔지니어링이라 할 수 있습니다.


[그림 1] 데이터 엔지니어링


데이터 파이프라인의 기능을 몇 가지 나열해 보면 다음과 같습니다.


ETL 작업: 데이터를 추출하고, 변환하고, 저장하는 작업

대규모 피쳐 엔지니어링: 데이터 내 변수들에 피쳐 엔지니어링을 가하는 작업

머신 러닝 및 통계 모델 서빙(Serving): 머신 러닝 엔지니어나 통계학자가 개발한 머신 모델들을 빅 데이터에 적용하여 대규모로 예측 값을 생성하는 작업

작업 스케줄링/모니터링: 위에 언급된 작업들을 주기적으로 실행시키고 작업 상태를 모니터링하여 실패 시 자동 복구 등을 수행하는 것

데이터 모니터링 UI: 파이프라인 상에서 처리 중이거나 처리가 완료된 데이터 내용을 일부 문서화해주고, 필요한 경우 처리 중인 값에 실시간으로 접근할 수 있게 해주는 유저 인터페이스 (User Interface)


기능적 측면은 더 세부적으로 파고 들어갈 수 있습니다. 특히 위에 ETL과 피쳐 엔지니어링 부분은 데이터를 어떤 방식으로 처리하고 저장하는가에 따라 수많은 분류로 더 나뉠 수 있습니다. 예를 들어 데이터는 뭉텅이로 한꺼번에 처리될 수도 있고(Batch), 입력되는 대로 조금씩 실시간에 가깝게 처리할 수 도 있습니다(Streaming). 데이터는 임의의 항목에 직접 접근(Random Access)이 가능한 형태로 저장할 수도 있고, 순차적 접근(Sequential Access)만 가능한 파일 형태로 저장할 수도 있습니다. [1] 이런 모든 기능은 분석 요구 사항이나 구조 효율성 등을 고려하여 선택적으로 구현되게 됩니다.


[그림 2] Matt Turck의 “Data & AI Landscape 2020” 중 “오픈 소스” 부분


[그림 2]는 맷 터크(Matt Turck)가 그린 “Data & AI Landscape 2020”[2]에서 오픈소스 부분을 발췌한 것입니다. 여기 나오는 오픈 소스들 중 60 % 이상이 데이터 엔지니어링 영역에 해당하며, 앞서 언급한 다양한 기능들을 구현할 때 사용될 수 있습니다. 데이터 엔지니어링 관련 오픈 소스에 대해서는 깃 허브의 “Awesome Big Data”[3] 문서를 통해서도 그 다양성과 규모를 확인할 수 있습니다. 데이터 엔지니어링은 여차하면 컴퓨터공학 전체를 끌어들이는 상당히 넓고 복잡한 세계입니다.


데이터 파이프라인 시스템을 대략적으로 [그림 3]과 같이 표현할 수 있습니다. 여기서 data라로 표시된 것은 저장된 데이터를, app이라 표시된 것은 데이터를 가공하는 작업을 의미합니다. 데이터 파이프라인에서는 앞 단계 데이터 가공 작업으로 생성된 데이터가 다음 단계 가공 작업의 입력으로 들어가는 연쇄적 과정이 일어나며, 시스템 전체는 이러한 연쇄가 그물망처럼 엮인 형태인 경우가 많습니다.


[그림 3] 데이터 파이프라인


한편 [그림 3]의 각 데이터와 앱 은 앞서 설명한 여러 가지 오픈소스들로 구성될 수 있습니다.[4] 예를 들어 데이터 자리에는 하둡 파일 시스템(HDFS), 카산드라(Cassandra) 데이터베이스, 카프카(Kafka) 등이 들어갈 수 있고, 앱 자리에는 하둡 맵 리듀스(MapReduce)나 스파크(Spark)로 작성한 여러 가지 응용이 들어갈 수 있습니다. 머신 러닝 모델을 학습하거나 모델을 이용하여 예측값을 생성하는 작업도 파이프라인에 앱으로 추가될 수 있으며, 이런 경우 스파크(Spark), 텐서플로우(Tensorflow) 등이 앱 자리에 들어가게 됩니다. 웹 검색으로 데이터 파이프라인 사례(data pipeline example)을 검색해서 이미지를 뽑아보면 여러 기업체의 파이프라인 사례를 살펴볼 수 있습니다.


데이터 엔지니어링 업무와 관련하여, 마지막으로 언급할 것은, 이 업무가 통계학이나 머신 러닝과 무관하지 않으며, 데이터 엔지니어로서 통계나 머신 러닝을 아는 것이 점점 더 중요해지고 있다는 것입니다. 예를 들어 천억 건의 숫자 데이터가 있고, 이 데이터의 평균, 분산, 중앙값 등 기초 통계량을 뽑아내야 한다고 합시다. 데이터 엔지니어 입장에서 맵리듀스나 스파크를 이용하여 천억 건의 데이터를 전부 계산하는 것은 어려운 일이 아닙니다. 그러나 통계를 아는 사람이라면 애초 천억 건을 전부 계산하는 것이나 그 중 십만 개 정도만 샘플로 뽑아서 그 데이터만 계산하는 것이나 결과 신뢰도는 크게 다르지 않으며, 때로는 오히려 그런 방식이 샘플 크기를 고정함으로써 통계량의 분포를 안정화시키고 장기적으로 분석 모델 성능에 더 도움이 될 수 있다는 것을 고려할 것입니다. 사실 후자의 방식이 컴퓨팅 자원 사용량 측면과 통계적 엄밀성에서 측면에서 모두 우수한 경우가 많습니다.


2.3.2 테이블 데이터 SQL, JOIN


데이터 엔지니어링 분야에 알아 두어서 좋은 것은 많습니다만, 테이블 데이터를 다루는 SQL과 JOIN 개념은 데이터 과학자들도 꼭 알아두어야 할 개념입니다.


[표 1] ‘runner’ 테이블


SQL(Structured Query Language)은 빅데이터가 등장하기 이전 대세를 이루었던 관계형 DB(Relational Database)에서 데이터를 조작하기 위한 명령어였습니다. 예를 들어, 앞서 살펴봤던 달리기 기록 데이터가 RDB에 [표 1]와 같은 테이블로 들어가 있다고 하면, 나이 30세 이상의 성별 기록 데이터를 선택해서 뽑아내기 위한 SQL 명령어는 다음과 같이 작성할 수 있습니다.[5]


SELECT gender, record FROM runner WHERE age >= 30


보시다시피 SQL은 인간의 언어(영어)에 가까운 구조를 가지고 있습니다. 물론 작업이 복잡해지면 SQL 명령 한 개가 몇 페이지가 되는 경우도 있긴 합니다만, 대부분의 경우 SQL은 데이터를 선택하거나, 입력, 변경, 삭제하는 등 간단한 내용이며, 코드도 직관적으로 이해할 수 있습니다. 예를 들어 다음과 같은 명령어가 무엇을 의미할지는 그냥 보기만 해도 알 수 있습니다.


UPDATE runner SET record = 93.0 WHERE id = 1
DELETE FROM runner WHERE id = 2


SQL 명령 중에 다소 복잡하지만, 알아두어야 할 것이 JOIN 명령입니다. 이 명령은 기본적으로 SELECT 명령과 같이 데이터를 선택하는 명령이지만 두 개 이상의 테이블을 조합하여 데이터를 선택한다는 점이 다릅니다.


[표 2] ‘state’ 테이블


[표 3] ‘runner’ JOIN ‘state’


[표 2]와 같이 국가별 GDP와 GINI 계수가 담긴 테이블 데이터가 있다고 해 봅시다.[6] [표 1][표 2] 데이터를 조합하여 [표 3]과 같은 데이터를 만들 수 있는데 이때 쓰이는 SQL 명령은 다음과 같습니다.


SELECT age, gender, record, country, gdp, gini
FROM runner
INNER JOIN state ON runner.country = state.country;


이 명령은 두 테이블에 공통으로 들어있는 ‘country’ 변수를 이용해 runner 데이터와 state 데이터의 측정값 변수들을 결합한 다음, age, gender 등 5개의 변수를 뽑아내는 내용입니다. ‘country’와 같이 결합의 기준이 되는 변수를 JOIN 키(Key)라고 합니다. JOIN 앞에 나오는 INNER라는 키워드는 양쪽 테이블의 country 변수에 모두 존재하는 값만 뽑아내라는 의미로, runner 테이블에만 있는 ‘독일’, state 테이블에만 있는 ‘프랑스’, ‘러시아’는 JOIN 결과에서 사라지게 됩니다.


INNER는 두 테이블의 JOIN 키 변수 집합의 교집합만 취한다는 의미로 볼 수 있는데, JOIN 키 집합의 어떤 부분을 취할 것인가를 정하는 방식은 INNER 말고도 다양한 방식이 있습니다. [그림 4]는 대표적인 것들 몇 가지를 보여주고 있습니다.


[그림 4] 여러 가지 JOIN 방식


예를 들어 runner 테이블은 하나도 누락시키지 않고, state 테이블 정보만 가져다 붙이려고 할 경우 LEFT OUTER JOIN 방식을 쓰면 되고, 다음과 같은 명령으로 작성할 수 있습니다.


SELECT age, gender, record, country, gdp, gini
FROM runner
LEFT OUTER JOIN state ON runner.country = state.country;


위 명령을 실행시킨 결과는 [표 4]와 같이 됩니다. [표 3]과 달리 runner에 있는 모든 데이터가 나타나고, state 데이터가 없는 경우는 결측치로 처리됩니다.


[표 4] ‘runner’ LEFT OUTER JOIN ‘state’


SQL 언어를 사용하는 것과 그것을 실제로 구현하는 것은 다른 문제입니다. 빅데이터를 대상으로 JOIN 등의 SQL 연산을 사용할 수 있는 데이터베이스를 만드는 것은 어려운 일이었기 때문에, 빅데이터 시대 초기에 SQL은 하둡 생태계와 잘 어울리지 못했습니다. 그래서 하둡 기반으로 빅데이터 용 데이터베이스로 개발된 솔루션들 일부는 No-SQL 데이터 베이스라고 불리며, SQL 대신에 다른 명령어 셋을 가지고 있기도 합니다. 그러나 SQL 언어는 변수의 선택, 조합 등 데이터 처리 작업을 정의하는 데 있어 매우 뛰어났고, 그 편의성은 무시할 수 있는 것이 아니었습니다. 시간이 지나 데이터 엔지니어링 도구들이 발달하면서 RDBMS에서 사용되던 SQL 명령어 일부를 빅 데이터에서 사용할 수 있게 해주는 도구들이 등장했습니다. 대표적인 것이 하둡 맵리듀스 기반으로 동작하는 하이브(Hive), 그리고 스파크를 기반으로 동작하는 스파크 SQL입니다. 하이브에서 정의한 SQL 언어를 HiveQL이라고 하는데, 이것이 빅데이터 세계에서 SQL 언어의 표준이라 할 만한 지위를 누려 왔고, Spark SQL에서도 거의 동일한 내용을 지원합니다.


하이브와 스파크에서 SQL을 지원한다고 하여 모든 빅데이터 처리를 SQL을 이용해서 할 수 있다고 생각해서는 안됩니다. 흔치는 않습니다만, 데이터의 크기가 아주 크거나, 그 내용이 독특한 경우 하이브나 스파크 SQL을 이용한 JOIN이 매우 비효율적일 수 있습니다. 때로는 아예 동작이 불가능한 경우도 있습니다. 이런 경우는 맵리듀스, 스파크(non-SQL) 등을 활용하여 JOIN 과정을 구현하면서 최적화 작업을 수행해야 합니다. JOIN을 최적화하는 방식은 다양하며, 예를 들자면, 브로드캐스트 조인(Broadcast Join), 솔팅(Salting) 등이 있습니다.


2.3.3 하둡, 스파크


수많은 데이터 엔지니어링 도구들이 오픈 소스로 개발되어 사용되고 있습니다만, 그 중심에는 하둡과 스파크가 있다고 해도 과언이 아닙니다. 이번 단원에서는 이 두 오픈 소스의 동작 원리 일부를 소개하겠습니다.


하둡 파일 시스템 (Hadoop File System, HDFS)


HDFS는 분산 파일 시스템입니다. 디스크 사이즈가 100 TB (Tera-Bytes)인 컴퓨팅 장비 12대를 구매하여, HDFS를 설치한다고 가정해 봅시다. 각 장비에는 데이터노드(Datanode)라고 불리는 HDFS 소프트웨어가 설치되어 디스크 공간을 관리하게 되며, 12개의 장비에 데이터노드를 설치하면 총 1200 TB의 데이터를 담을 수 있는 분산 파일 시스템이 준비됩니다.


이제 1 TB 용량의 데이터가 있다고 합시다. 이 데이터는 HDFS에 여러 개로 잘게 쪼개져서 저장됩니다. 이때 쪼개는 단위를 블록(Block)이라고 합니다. 블록 한 개의 크기가 1 GB (Giga-Bytes)로 정해져 있다면, 1 TB 용량의 데이터는 1024개 블록으로 쪼개지게 됩니다. 이 1024개 블록이 각 데이터노드로 분산되어 저장되는데, 블록 하나를 여러 개 복제하여 장비 한 두 개에 장애가 발생하더라도 버틸 수 있도록 합니다. 블록을 3개로 복제하여 저장할 경우 총 3072개의 데이터 블록이 분산 저장되며 3 TB의 디스크 용량을 차지하게 됩니다. [그림 5]는 방금 설명한 방식으로 1 TB 데이터가 장비 12대에 흩뿌려지는 것을 보여주고 있습니다. 편의상 1, 2, 3번 블록이 저장된 모습만 그림으로 표시하였습니다.


[그림 5] 1 TB(복제본 포함 3 TB) 데이터가 HDFS에 저장되는 모습


앞서 설명한 예의 경우 HDFS 시스템은 다음과 같이 설정되어 있습니다.


dfs.blocksize = 1073741824
dfs.replication = 3


각각 블록 사이즈 1 GB, 블록 복제본 3개를 의미합니다. HDFS에는 이외에도 수많은 설정 값이 있습니다. 전부 다 알 필요는 전혀 없습니다만, 그런 다양한 설정 값들 중 일부가 가끔씩 데이터 처리와 관련된 문제를 해결할 때 큰 도움이 된다는 점을 기억해 두십시오.


HDFS는 분산 파일 시스템의 일종입니다. 지금까지 블록에 대해서만 설명했는데, 이 블록들을 모아 이름을 붙인 것을 HDFS에서 파일이라고 합니다. 앞서 1 TB의 데이터는 1024개의 블록으로 구성되어 있는데, 한 개 파일에 블록 한 개씩 엮어서 1024개 파일명이 등록되어 있을 수도 있고, 2개씩 엮어서 512개 파일명이 등록되어 있을 수도 있습니다. 파일 이름들을 저장하고 관리하는 역할을 하는 하둡 프로그램이 네임노드(Namenode)입니다. 일반적으로 HDFS 전체 시스템은 수많은 데이터노드와 한 두 개의 네임노드로 구성됩니다.


하둡 맵리듀스 (Hadoop MapReduce)


앞서 구매한 12대 장비의 디스크는 HDFS 시스템이 차지했습니다. 그렇다면 그 장비들의 CPU와 메모리는 어떻게 활용 가능할까요? 한 개 장비에 CPU가 10개, 메모리가 100 GB씩 달려있었다고 가정하면 총 120개의 CPU와 1200 GB의 메모리가 활용 가능합니다. 이런 컴퓨팅 자원을 이용해 데이터를 분산 처리할 수 있게 해주는 도구가 하둡 맵리듀스입니다.


앞서 살펴본 예에서 1 TB의 데이터를 HDFS에 저장하였습니다. 그 데이터에 0과 1 사이의 숫자 1000억 건이 들어있었고, 이것을 모두 오름차순으로 정렬해야 한다고 해 봅시다. 이 데이터는 한 대의 장비에서 한꺼번에 처리할 수 없는 분량이므로 분산 처리가 필요하며, [그림 6]과 같은 방법을 생각해 볼 수 있습니다.


[그림 6] 숫자 천억 개 정렬하기


[그림 6]이 설명하는 방법은 이렇습니다. 우선 숫자들을 각각의 크기가 0.001인 1000 개의 작은 구간으로 나누어 모읍니다. 각 구간 데이터는 장비 한 대에서 처리 가능한 크기가 되므로, 정렬하여 파일 한 개로 저장합니다. (블록은 여러 개가 될 수도 있습니다.) 이렇게 처리한 결과는 1000개의 파일로 분산 저장된 완전히 정렬된 데이터입니다.


[그림 6]의 정렬 기법은 하둡 맵리듀스에서 쉽게 구현이 되는데, 이를 위해서 컨테이너(Container)라는 개념을 알아야 합니다. 컨테이너는 소량의 메모리와 CPU를 묶은 일종의 가상 머신입니다. 하둡은 자원 관리자(Resource Manager)라는 프로그램을 두어, 사용자가 필요에 따라 컨테이너를 생성하여 사용할 수 있게 해 줍니다. 예를 들어, 사용자는 앞서 설명한 12대의 장비 상에 CPU 1개 메모리 4 GB인 컨테이너를 한번에 120개까지 구동할 수 있습니다. (메모리는 남는데 CPU 수에 의해 제한을 받았습니다.)


[그림 7] 구간 분류 단계


[그림 7]은 여러 대의 컨테이너가 한꺼번에 구동되어 데이터를 구간 분류하는 작업을 보여주고 있습니다. 컨테이너에 입력으로 들어가는 데이터 블록(Block)들은 [그림 5]에서 분산 저장한 바로 그 블록들입니다. 구간 분류된 결과는 신규 블록들로 출력됩니다.


[그림 8] 각 구간 내 정렬 단계


[그림 8]은 1차 분류된 각 구간 내의 데이터를 최종 정렬하는 모습을 보여주고 있습니다. 여기서 컨테이너에 입력으로 들어가는 데이터는 앞 단계에서 구간 분류한 데이터이고, 최종적으로 정렬된 데이터가 출력됩니다.


1000억 개 숫자 분산 처리는 이렇게 두 단계로 나뉘어 처리될 수 있습니다. 이러한 두 단계 처리 방식은 정렬 문제 외에도 여러 가지 데이터 처리 문제에 응용할 수 있습니다. 예를 들어, 앞 단원에서 공부했던 두 테이블 데이터의 JOIN 문제에 적용하면 앞 단계에서는 runner 데이터와 state 데이터를 각 국가별로 모아주고, 뒤 단계에서 모인 데이터를 결합해주면 됩니다. 일반적으로, 두 단계를 다음과 같이 설명할 수 있습니다:


1단계: 입력 데이터 블록들을 가공하고 분류하여 저장

2단계: 분류된 데이터를 최종 가공하여 출력


이와 같이 일반화된 두 단계 처리 방식을 쉽게 구현할 수 있도록 해주는 도구가 바로 맵리듀스 프레임워크입니다. 맵리듀스에서는 앞 단계 과정을 수행하는 컨테이너를 매퍼(Mapper) 뒤 단계 과정을 수행하는 컨테이너를 리듀서(Reducer)라고 부르는데, 맵리듀스(MapReduce)라는 이름 자체가 거기서 왔습니다. 참고로 소프트웨어 도구 중에서 사용자가 능동적으로 기능을 호출해 가면서 사용하는 것들을 라이브러리(Library)라고 하고, 사용자 코드가 해당 도구에 의해 호출되는 방식으로 동작하는 것들을 프레임워크(Framework)라고 하는데, 맵리듀스는 프레임워크에 해당됩니다.


숫자 정렬 과정에서 분류 구간을 1000개로 나누면서 각 구간의 크기를 0.001로 고정한다고 설명했는데, 사실 이 부분이 문제를 일으킬 수도 있습니다. 데이터가 균일하게 분포(uniform distribution)하지 않을 경우, 한 구간에 많은 데이터가 몰릴 수 있고, 데이터가 많이 몰린 구간을 처리해야 하는 리듀서는 메모리 부족으로 죽을 수 있습니다.[7] 이러한 문제를 데이터 쏠림(skew) 이라 하는데, 데이터 엔지니어링 업무 중 가장 빈번하게 부딪치는 문제 중 하나입니다.


본 단원에서 설명한 1000억 개 정렬 방식은 테라소트(Terasort)라고 불리는 알고리즘의 단순한 형태입니다. 테라소트는 하둡 맵리듀스를 이용한 기본적인 응용 중 하나로 하둡 오픈소스에는 예제 프로그램으로 기본 탑재되어 있기도 합니다. 원래의 테라소트는 데이터 쏠림에 대비하여 맵리듀스 작업에 본격적으로 들어가기 전에 데이터를 일부 샘플하여 분포를 추정하고 그에 맞춰 1차 분류 구간을 설정하는 과정을 포함하고 있습니다.


스파크 (Spark)


빅데이터 시대 초기 맵리듀스 앱(Application)들이 데이터 센터들을 가득 채우고 돌아가던 시절이 있었습니다. 그러나 맵리듀스에는 몇 가지 아쉬운 부분이 있었고, 이 부분을 거의 완벽하게 채워준 도구가 스파크(Spark)라 할 수 있습니다. 이 단원에서는 현재 오히려 맵리듀스보다 널리 쓰이고 있는 스파크의 특징을 몇 가지 살펴봅니다.


하나의 맵리듀스 앱은 맵과 리듀스 두 단계로 구성됩니다. 그런데, 그 두 단계만으로 원하는 데이터를 얻기 어려운 복잡한 처리 과정의 경우에는 어떻게 할까요? 여러 개의 맵리듀스 앱을 연달아 사용하면 됩니다. 예를 들어 [그림 9]와 같이 여러 개의 맵리듀스 앱이 연달아 작동할 수 있습니다.


[그림 9] 여러 개의 맵 리듀스 앱으로 데이터 처리하기


이러한 방식으로 못 처리할 데이터는 없습니다. 그러나 해야 할 일은 하나인데 여러 개의 앱을 작성해야 하다 보니, 핵심 알고리즘과는 관계없는 반복되는 설정 코드의 양이 많아졌습니다. 이런 반복 코드를 소프트웨어 세계에서는 보일러플레이트 코드(Boilerplate Code, 상용구 코드)라고 부르는데, 맵리듀스 앱을 많이 작성해 본 엔지니어들은 하나같이 이런 부분에 불만을 가지고 있었습니다.


데이터를 처리하기 위해 맵리듀스 앱 여러 개를 연달아 사용하는 것의 또 다른 문제점은 불필요한 디스크 입출력이 있다는 것입니다. [그림 9]에서 볼 수 있듯이 각 맵리듀스 앱 사이는 HDFS 파일들로 이어집니다. 하나의 맵리듀스 작업 안에서도 맵 작업들과 리듀스 작업들 사이는 HDFS 파일들로 이어집니다. 디스크 입출력은 결코 그 자체로 느린 작업은 아닙니다만, 불필요하게 수행된다면 낭비가 아닐 수 없습니다.


초기 스파크의 혁신은 앞서 말한 두 문제를 해결함에 있었습니다. 스파크는 다단계 맵리듀스 앱을 작성함에 수반되던 보일러플레이트 코드를 없애고, 불필요한 디스크 입출력도 없앴습니다.


한 개의 스파크 앱은 여러 개의 스테이지(Stage)로 구성되는데, 이 스테이지는 각각이 맵리듀스의 매퍼 또는 리듀서와 동급이라 할 수 있습니다. 그런데, 한 앱에서 실행할 수 있는 스테이지 수에는 제한이 없습니다. 맵리듀스 앱 여러 개가 필요한 복잡한 작업이라도 하나의 스파크 앱으로 작성 가능하다는 뜻입니다. 스파크는 또한 맵리듀스와 달리 프레임워크 형태가 아니라 라이브러리 형태의 도구로써, 코드 작성이 비교적 자유롭고, 작성된 코드는 가독성이 높습니다. 한마디로 스파크는 훌륭한 사용자 경험(user experience)을 제공하는데, 많은 사람들이 스파크에 대해서 “그 사용자 경험 만으로도 스파크를 사용할 충분한 이유가 된다”라고 할 정도입니다.[8]


맵리듀스 앱에서 매퍼와 리듀서 컨테이너가 동시에 떠있는 것이 가능하다면, 매퍼에서 리듀서로 곧장 데이터를 전송하고 중간 과정의 디스크 입출력은 생략할 수도 있을 것입니다. 스파크에서는 앞 스테이지에서 처리한 데이터를 메모리 상에 캐시(cache)해 놓는 것이 가능하기 때문에 이런 일이 실제로 일어납니다. 앞쪽 스테이지에서 한번 처리해 둔 데이터를 뒤쪽 스테이지 여러 개가 활용할 수 있다, 그것도 디스크를 통하지 않고 메모리에서 직접 가져다 활용할 수 있다는 것은 작업 종류에 따라 때로는 아주 큰 성능 향상을 가져올 수 있습니다. 캐시는 스파크가 항상 내세우는 하둡에 대한 성능 우위의 원천이자, 스파크가 인메모리(in-memory) 데이터 처리 도구라 불리는 이유이기도 합니다.


스파크는 사용자 편의성과 동작 효율성을 동시에 만족시키면서 큰 인기를 끌게 되었습니다. 스파크 오픈 소스에는 수많은 개발자들이 모여들어 온갖 기능들을 추가하게 되는데, 가장 중요한 것 두 가지를 꼽자면 SQL과 머신 러닝 라이브러리입니다.


스파크가 제공하는 사용자 경험 중에서 데이터 과학 측면에서 가장 맵리듀스와 구별되는 지점은 코드상에서 “전체 데이터가 보인다”는 것입니다. 예를 들어 다음 스파크 코드는 HDFS /data/one 디렉토리 안의 텍스트 파일들을 읽어 들여서, 소문자화 한 다음 정렬하는 코드인데, 전체 데이터가 dataset이라는 변수에 담겨서 가시화되었습니다. 당연히 이래야 할 것 같지만, 맵리듀스는 이렇지가 않습니다.


val dataset = spark.read.text(“/data/one”).toDF(“text”)
dataset.select(lower($”text”).as(“lower_text”)).sort(“lower_text”)


이런 식으로 전체 데이터에 대한 명령을 내릴 수 있게 되다 보니, 자연스럽게 SQL 형태의 명령도 내리고 싶은 욕구가 사용자에게 생겼고, 스파크가 그것을 지원하기에 이르렀습니다. 위에서 실행한 텍스트 처리와 완전히 동일한 내용을 SQL 형태로 다음과 작성할 수 있습니다.


val dataset = spark.read.text(“/data/one”).toDF(“text”)
dataset.createOrReplaceTempView("dataset")
spark.sql(“SELECT lower(text) AS lower_text FROM dataset ORDER BY lower_text”)


전체 데이터를 변수에 담아두고 할 수 있는 또 다른 일은, 그 데이터를 통계 데이터로 보고, 평균이나 분산 같은 기술 통계량을 계산하거나, 그 데이터를 기반으로 한 통계 모델 또는 머신 러닝 모델을 학습하는 것입니다. 스파크 머신 러닝 라이브러리는 후자의 목적을 위해 개발되어 널리 쓰이고 있습니다.


지금까지 스파크의 탄생 배경과 여러 가지 장점에 대해서 언급하였습니다. 마지막으로 스파크의 한계점도 몇 가지 언급하고 본 단원을 마치겠습니다. 스파크의 문제점으로 첫 번째 언급할 수 있는 것은 맵리듀스에 비해 메모리 요구량이 높고, 에러 상황에 대한 저항력이 낮다는 것입니다. 이것은 사용자 경험을 끌어올리려다 보니 내부 동작 과정이 복잡해진 것이 원인이며, 데이터의 크기가 아주 커질 경우 JOIN이나 정렬 등의 단순한 동작도 제대로 동작하지 않을 수 있습니다. 보통 10 TB보다 데이터가 커질 경우 이런 일이 자주 발생하는데[9], 이 경우 스파크 응용을 구동하기 위해서는 상당한 수준의 튜닝(tuning)이 필요하며, 맵리듀스로 전환하는 것이 여러모로 나을 수 있습니다. 스파크의 문제점 두 번째로 언급할 수 있는 것은 사용할 수 있는 머신 러닝 모델이 많지 않고, 신경망 분야의 모델은 사실상 없는 것과 마찬가지라는 점입니다. 다만 이 부분은 머신 러닝, 특히 신경망 분야에 특화된 텐서플로우(TensorFlow), 파이토치(Pytorch)와 같은 오픈 소스들이 등장하여 자연스럽게 역할 분담이 된 것으로 볼 수 있고, 스파크가 제공하는 샐로우 러닝(Shallow Learning) 모델들은 상당 부분 쓸 만하여, 스파크의 독자적 영역도 아직 남아 있다고 볼 수 있습니다.




[1] 데이터 저장 포맷은 이것 하나만으로 책이 나올 수 있을 만큼 큰 분야입니다. 특히 컬럼 기반 저장 포맷 (Columnar Storage Format)의 개념 같은 것은 알아두면 좋습니다.

[2] Data & AI Landscape https://mattturck.com/data2020/

[3] Awesome Big Data https://github.com/0xnr/awesome-bigdata

[4] [그림 3] https://d2.naver.com/helloworld/1179024

[5] SQL 코드는 Spark SQL 또는 HiveQL을 기반으로 작성하였고, 다른 SQL 엔진에서 동작하려면 일부 수정이 필요할 수 있습니다.

[6] GDP, GINI 자료 https://en.wikipedia.org/wiki

[7] 죽지 않고, 다른 리듀서가 모두 작업을 완료한 뒤에도 한참 동안 혼자서 작업을 계속하면서, 전체 작업 시간을 지연시킬 수도 있습니다. 사실 이렇게 작업 시간이 심하게 불균일해지는 경우가 더 빈번하게 일어납니다.

[8] “하둡 완벽 가이드 (Hadoop Definite Guide)”라는 하둡을 배우기에 가장 좋은 책이 있는데, 거기서도 스파크에 대해 이렇게 말합니다. “Spark is very attractive for a couple of other reasons… user experience is also second to none.”

[9] 하둡 버전 3.x, 스파크버전 2.x 기준으로 여러 앱을 작성해 보았을 때 나온 결과입니다.


이전 07화 2.2 피쳐 엔지니어링
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari