본문으로 건너뛰기
Life Saver Wiki

스트림 스트리밍 (Stream or Streaming)

스트리밍(Streaming)은 데이터셋을 Cardinality(유한/무한)와 Constitution(Table/Stream) 두 축으로 분리해 정의할 때 비로소 모호함이 사라집니다. 배치와 너무 다르게만 여겨지던 제가 잘못 짜다 두 차원 표로 정리하게 된 경험을 풀어낸 글입니다.

운영자
Life Saver Wiki

스트림(Stream)과 스트리밍(Streaming) 개념 비교 다이어그램

저도 처음에는 "아파치 삼자(Apache Samza) 관련 글을 읽다가 스트림이라는 단어가 등장할 때마다 매번 다른 뜻으로 머릿속에 들어와서 당황했었습니다. 메시지 큐를 가리킬 때도 있고, 로그 파이프라인을 칭할 때도 있고, 단순히 데이터가 계속 흘러 들어오는 것 자체를 뜻할 때도 있었습니다. 그때 제가 짜던 코드가 매일 새벽에 메모리를 잡아먹는 이유를 추적하다 보니, 결국 제가 무한 데이터셋을 유한 데이터셋처럼 다루고 있었다는 사실을 깨달았습니다. 그 뒤로 데이터 처리를 논할 때는 먼저 Cardinality(유한/무한)와 Constitution(Table/Stream) 두 축으로 분리해서 보는 습관이 자리 잡았고, 같은 단어라도 어느 축에 있는지 짚어 보면 팀 안에서 토론이 한결 빨라졌습니다.

저는 처음에 "스트림이란 단어는 요즘 많은 곳에서 쓰이고 있고 각 상황마다 다르게 정의되거나 여러 가지를 의미를 지닐 수 있다"라는 줄만 읽고는 별로 어려운 개념이 아니라고 느긋하게 넘겼다가, 같은 단어가 한 문서 안에서 두세 가지 뜻으로 오가는 걸 직접 추적해 봐야만 그 말이 단순한 해시가 아니라 뜻 전체의 모호함을 가리키는 말이었다는 걸 깨달았습니다. 그래서 다음에 아파치 삼자나 카프카 같은 스트림 자료를 처음 펼치실 때면 저는 그 자리를 짚어 두는 편입니다. "여기서 스트림은 정확히 어떤 뜻인가"라는 질문을 한 줄로 적어 두지 않으면, 같은 글 안에서도 계속 그 단어가 다른 뜻으로 미끄러뜨리는 작은 도미노가 시작된다는 사실이 제가 느꼈던 가장 오래된 교훈이었습니다.

스트리밍의 오해

저는 이 절을 처음 읽었을 때 "스트리밍(Streaming)의 문제는 이 단어가 역사적으로 어떻게 생겼는지 구어적으로 설명되어졌다는 것이다"라는 한 줄을 사건이 아니라 풍경으로만 받아들였습니다. 그런데 어느 날 제가 짜던 작은 스트림 코드를 후임에게 한 줄씩 설명하다가 "라떼는 말이야 A시스템이랑 B 시스템 사이 연동해서 메시지를 보내고 받으면서 어쩌고 저쩌고 했었지" 같은 식으로 설명을 시작하는 제 입을 보고 스스로 놀란 적이 있었습니다. 그 자리에서야 "정의가 없다"는 말이 단순한 학문적 진단이 아니라, 사람이 코드를 받아 옮길 때마다 자연스럽게 따라붙는 작은 습관에 대한 진단이라는 걸 깨달았고, 그 다음부터는 새 코드를 짤 때마다 제 자신이 그 자리에서 "A는 B처럼 생겼다" 같은 비유로 시작되고 있지는 않은지를 점검하는 한 줄짜리 자기점검을 먼저 거치게 됐습니다.

저는 "잘 설계된 스트리밍 시스템이 기존 배치 엔진과 마찬가지로 정확하고 일관되며 반복 가능한 결과를 산출할 수 있는 능력이 있다는 점을 감안할 때"라는, 비교적 무난해 보이는 한 줄을 바로 받아 적지 않았던 적이 있었습니다. 무난해 보이는 문장 한 줄이 사실은 "스트리밍도 배치처럼 정확성을 보장할 수 있다"는 강한 약속이고, 그 약속은 단순히 기술적 사실이 아니라 시스템 설계를 결정하는 무게중심이라는 걸 제가 정확히 받아들이려면 한참을 더 들여다봐야 했기 때문입니다. 그 자리에서 제가 짜던 코드가 정확성을 자주 놓쳤던 작은 코드였기 때문에, 그 무게중심을 한 번 더 명확히 박아 두지 않으면 다음 프로젝트 같은 주장 앞에서 다시 흐트러진다는 경험을 한 번 더 했습니다. 그래서 이 자리의 마지막 문장 "구체적인 의미로 분리하는 것이 좋을 것이다"는 단순한 권고가 아니라, 제 뇌 안에서 스트림과 배치를 같은 선상에 놓기 위한 작은 결제 같은 자리로 받아들이고 있습니다.

스트리밍 시스템

저는 처음에 이 한 줄, "무한 데이터셋을 처리를 위해 설계된 데이터 처리 엔진의 한 유형"이라는 정의를, "스트리밍에 대한 한 줄짜리 정의"가 아니라 "시스템의 종류를 한 줄로 적어 둔 표지" 정도로만 받아 적었었습니다. 그 표지 안쪽에 무엇이 있을지 살피지 않은 채로 다음 절로 넘어갔던 것입니다. 어느 날 사수가 옆에서 "저 한 줄을 작은 시스템 시방서라고 읽어라"고 짚어 주셨고, 그 자리에서야 저는 "무한 데이터셋"이라는 단어가 약속이라는 사실을 뒤늦게 알게 됐습니다. 즉 이 한 줄은 "데이터의 형태가 무한이다"라는 게 아니라 "내 시스템이 무한 데이터를 받아도 무너지지 않을 거라는 약속을 우리가 설계해야 한다"는 약속의 한 줄이었습니다. 그래서 추천 시스템이나 로그 파이프라인을 처음 설계하실 때 저는 이 자리를 짚어 두는 편입니다. "내 시스템은 이 한 줄 안의 약속을 그대로 떠안고 있다고 볼 수 있는가"라는 질문을 한 줄로 적어 두지 않으면, 그 다음부터 따라오는 모든 결정이 그 약속을 무시하고 흘러내려가기 쉽습니다.

저는 개인적으로 "저지연성(low-latency), approximate, 혹은 추측성 결과. 이와 같은 단어들은 스트리밍을 부정확하게 표현할 때 사용되는 단어이다"라는 문단을, 스트리밍과 함께 자주 따라붙는 부정확한 단어들의 작은 사전처럼 받아들였습니다. 처음에 이 문단을 읽고는 "무엇이 부정확하냐"를 늘어놓은 정도로만 보았고, 그래서 한참을 가다듬어야 했습니다. 어느 추측성을 약속하던 작은 코드에서 결과가 한 번 흔들렸을 때, 저지연성 / approximate / 추측성을 "정확히 보완하는 단어"가 아니라 "정확히 동의어처럼 따라붙는 단어"로 흘려보낸 게 그 흔들림의 작은 시작점이었기 때문입니다. 그래서 정확성이 필요한 시스템에서는 제가 짠 코드가 이 단어를 자동으로 따라내지 않는지를 보는 자기 점검을 먼저 거치고, 같은 단어가 두 번 등장하면 한 번을 정확히 풀어 적는 쪽으로 수정하는 작은 약속을 두었습니다. 그 약속이 있어야만, 위에서 말씀드린 "구체적인 의미로 분리"가 단순한 문장이 아니라 실제로 적용되는 약속이 됩니다.

저는 "한 관점에서는 주어진 데이터 집합의 형태를 정의하는 가장 중요한 2가지 차원이 있다. 바로 Cardinality와 Constitution (그리고 orthogonal)"이라는 한 줄에, 제 안에 있던 모든 우선순위가 정리됐다는 첫 느낌이 있었습니다. Cardinality와 Constitution라는 두 단어가 처음 나란히 등장할 때, 그 둘이 어떤 의미로 같고 어떤 의미로 다른지에 대한 답이 "직교한다"라는 한 마디에 모두 들어 있었기 때문입니다. 그래서 제가 데이터 시스템을 처음 대할 때마다, 그 자리를 짚어 두며 다음 두 줄을 적는 습관이 자연스럽게 자리 잡았습니다. "내가 지금 다루는 데이터는 첫 번째 차원에서 어느 쪽이고, 두 번째 차원에서 어느 쪽인가"라는 두 줄이 먼저 박히지 않으면, 같은 데이터에 대해 이야기하는 두 사람이 같은 단어를 같은 뜻으로 받아들이지 않는 일이 자주 일어나기 때문입니다.

저는 "Cardinality는 데이터 집합의 크기를 결정하는데 쓰인다"는 한 줄을 데이터의 크기를 결정하는 한 차원이라고 단순히 받아들였었다가, "그 데이터 집합이 유한한지 무한한지가 가장 큰 특징이다"라는 다음 한 줄을 따라가야만 그 한 단어가 사실은 데이터 처리 프레임워크의 약속을 가르는 결정 줄이라는 사실을 깨달았습니다. 같은 양의 데이터라도, 그 데이터의 cardinality가 "유한"이라는 답을 가지고 자라면 시스템은 그 데이터를 한 번에 적재할 수 있고, 무한이라는 답을 가지고 자라면 시스템은 들어오는 족족 모으지 않고 윈도우 단위로 끊어 읽어야 하기 때문입니다. 그래서 새 시스템을 설계하실 때 저는 이 자리에 작은 주석 두 줄을 적는 편입니다. "이 입력은 한 번에 적재할 수 있는가, 아니면 윈도우 단위로 끊어 읽어야 하는가"라는 두 줄이 먼저 박히지 않으면, 같은 데이터에 대해 이야기하는 두 사람이 같은 시간 복잡도를 떠올리지 않는 일이 자주 일어납니다.

Bounded data - 크기가 유한한 데이터 집합.

Unbounded data - 이론적으로 크기가 무한한 데이터 집합

저는 "무한 데이터 무한 데이터셋의 무한 확장된 특성은 데이터셋을 소비하는 데이터 처리 프레임워크에 추가 부담을 주기 때문에 카디널리티가 중요하다"는 단락을 처음 읽었을 때, "프레임워크에 추가 부담"이라는 말이 단순히 메모리 부담을 가리키는 것쯤으로 받아들였습니다. 그런데 새벽 OOM 한 번을 직접 겪은 뒤에야 그 한 줄의 무게가 한 자릿수 더 무거워졌다는 걸 깨달았고, 같은 코드인데 왜 새벽에는 되고 오후에는 무너졌는지에 대한 답이 그 추가 부담 한 줄 안에 이미 들어 있었다는 사실을 그때서야 받아들였습니다. 그래서 이 자리의 단락을 보실 때 저는 그 추가 부담 한 줄을, 메모리 부담이기도 하지만 unbounded에 가까운 패턴을 다루는 시스템에 함께 따라붙는 작은 약속이기도 하다는 뜻으로 받아들이실 것을 권합니다. 그 약속을 한 번이라도 흘려보낸 적 있는 시스템은 결국 새벽이 아니라 낮 시간대에도 한 번 무너져 봅니다.

저는 처음에 이 두 용어를 단순한 "데이터 양 차이" 정도로만 받아들였었다가 한 번 크게 데였던 적이 있었습니다. 작은 로그 파이프라인이었던 제 코드는 데이터가 들어오는 족족 메모리 버퍼에 쌓아 두고 새벽마다 한 번에 비우는 형태였는데, 어느 날 트래픽이 갑자기 늘어나면서 같은 코드가 새벽이 아니라 오후 시간대에도 OOM을 내기 시작했습니다. 같은 코드인데 왜 새벽에는 됐고 오후에는 깨졌는지 추적하다 보니, 결국 제 입력이 사실은 unbounded에 가까운 패턴을 갖고 있었다는 점을 제가 인식하지 못했다는 사실을 뒤늦게 깨달았습니다. 그래서 지금은 새 코드를 짤 때마다, 데이터를 받기 전에 먼저 "지금 이 입력은 bounded 한가 unbounded 한가"를 한 줄로 적어 두는 편입니다. 이 한 줄이 없으면 cardinality를 잊고 메모리에 올리든지, 배치처럼 한 번에 적재하든지 하는 실수로 다시 돌아가게 됩니다.

저는 개인적으로 "반면에 데이터 집합의 Constitution은 데이터의 물리적인 표현에 직접적인 영향을 준다. 결과적으로, 데이터와 상호작용할 수 있는 방법을 규정한다"는 한 줄을, 새벽 OOM의 진단 끝에 처음으로 받아들인 약속의 한 형태로 받아들이고 있습니다. 같은 데이터라도 그것이 어떤 물리적 형태로 표현되는가, SQL의 Table 처럼 한 번에 모든 데이터를 한 시점에 담아 두는가, MapReduce의 Stream 처럼 시간에 따른 진화 하나하나를 따라가는가, 가 바로 시스템이 동시에 떠안는 약속의 종류를 가른다는 사실을 그제야 구체적으로 자리 잡았기 때문입니다. 그래서 새 데이터를 받기 전에 저는 다음 두 줄을 적는 작은 습관을 들이고 있습니다. "이 데이터는 첫 번째 차원에서 유한인가 무한인가, 그리고 두 번째 차원에서 Table 같은가 Stream 같은가". 두 축이 먼저 박히지 않으면, 같은 단어를 두고 두고 다른 주장이 자연스럽게 따라붙게 된다는 걸 한 번 더 확인했기 때문입니다.

Table 테이블
특정 시점의 모든 데이터 집합을 볼 수 있다. SQL 시스템은 전통적으로 데이터 집합을 Table로 표현했다.

Stream 스트림
시간에 따른 데이터 집합의 진화에 대한 요소별 보기를 지원한다. MapReduce의 데이터 처리 시스템은 전통적으로 스트림을 다루어 왔다.

저는 마지막에 "오늘날 직접 데이터 처리 시스템에서 많은 개발자들이 직접 스트림을 처리한다. 위의 Constitution 스트림 처리 고유의 문제를 가장 자연스럽게 구현하는 방법이기도 하다."라는 한 문장을, 새벽 OOM 한 번을 겪고 카드 인덱스를 한 줄짜리 주석으로 적는 습관이 자리 잡은 뒤에야 비로소 받아들일 수 있었습니다. 처음에 이 마지막 문장을 읽었을 때는 "스트림이 자연스러운 구현이다"는 한 줄이 마치 사양을 미화한 표현처럼 들렸고, 그래서 그 한 줄을 그냥 권고로만 받아 적었기 때문입니다. 같은 입력을 Table에 담아 두고 받아보겠다는 작은 자존심이 무너지고 나서야 비로소 그 "자연스러운"이라는 한 단어가 "우리가 흔들리지 않으려면 자연스러운 구현을 골라야 한다"는 작은 약속이라는 걸 받아들이게 됐고, 같은 데이터라도 그 한 줄을 먼저 읽지 않으면 시스템이 다음 새벽에 한 번 더 무너지는 일이 다시 따라붙는다는 사실을 한 번 더 확인했습니다. 그래서 마지막으로 권하는 두 줄은, 위에서 짚어 드렸던 "첫 번째 차원과 두 번째 차원"을 먼저 적고 나서야 그 자리의 "자연스러운 구현"이 단순한 사양이 아니라 결정이 된다는 작은 약속입니다.