누군가 “데이터 스트림이 뭐에요?”라고 물으면 어떻게 대답하실건가요?
어…. 이벤트가 계속 끊임없이 들어오는거…? like 비디오 스트리밍…?
이 글을 적기 전의 저는 저렇게 대답했을 것 같습니다.(😇…..) 저와 비슷한 분이라면, 이 글을 읽고 나서 더 나은 답변을 할 수 있게 되기를 바라는 마음으로 적어봅니다. 이 글에 나오는 인용문은 모두 ‘카프카 핵심 가이드 14장. 스트림 처리’ 에서 가져왔습니다.
무언가의 특징을 이해하기 위한 가장 쉬운 방법은 이미 알고 있는 것과 비교하는 것이다. 이번 글에서는 비교군을 통해 스트리밍 데이터의 두가지 측면을 알아보자.
소프트웨어는 현실 세계에서 일어나는 일 중, 자신에게 중요한 부분에만 집중한다. 그리고 그걸 데이터 모델로 만들어 처리하고 기록한다. 그 방식에도 여러가지가 있다. 🎴시나리오를 통해 이게 대체 무슨 소리인지 알아보자.
적절한 예시가 뭐가 있을지 고민하며 공책을 펴는 순간, 어제(엄밀히 말하자면 오늘 자정부터) 구경했던 고스톱 경기의 흔적을 발견했다. (나는 룰을 잘 몰라서 돈 계산만 담당했는데, 화투를 칠 때에도 공책과 필기구가 필요하다는걸 처음 알게 되었다.^^) 아무튼 테이블과 스트림을 비교하기에 꽤 괜찮은 예시다. 한 번 살펴보자. 생생함을 위해 따로 편집은 하지 않고 이름만 살짝 가렸다.
민*, 남*, 윤* 세 명이 점당 100인 고스톱 경기를 했다. 왼쪽 그림은 각 경기별 기록으로, 한 사람이 나머지 두 명한테 받아야 할 돈이 적혀있다. 예를 들어서 첫 번째 열은 민*의 열이고 그 중 첫 번째 기록인 “남 300 / 윤 300” 은 남, 윤에게 각각 300원씩 받아야 함을 의미한다. 오른쪽 그림은 중간 정산과 마지막 정산까지 끝낸 모습이다. 남 → 민 으로 3,900원, 윤 → 민 으로 1,900원, 윤 → 남 으로 300 원씩 보내줘야 한다. (기록을 보면 알겠지만 결국 막판에 큰 돈을 딴 사람이 커피를 얻어먹게 되었다.)
데이터베이스와 메세지큐는 이걸 각각 어떻게 기록할까?
데이터베이스는 결과에 집중한다. 2개의 사진 중 DB에 저장되는 테이블은 오른쪽 사진에 가깝다. 민, 남, 윤 세 명이 각각 누구에게 얼마나 돈을 받아야 하는지에 대한 최종적인 상태가 저장되는 것이다. 경기가 진행되는동안 각 화살표에 해당하는 금액이 바뀌었을 것이고 DB는 기존 값을 수정, 즉 update
연산을 하면서 이 사실을 기록하게 된다.
메세지큐는 과정에 집중한다. 2개의 사진 중 메세지큐가 처리하는 데이터는 왼쪽에 가깝다. 최종적인 상태가 아닌 각 경기가 끝날 때마다 경기 결과가 스트림으로 들어온다. 메세지큐는 그 이벤트 하나하나를 저장하고 전달하는 역할을 하게 되는 것이다.
데이터베이스 테이블과 스트림에 쌓이는 데이터의 차이점은 변경 가능성에 있다. 데이터베이스 테이블에 저장한 데이터는 update
연산을 통해 수정할 수 있다. 하지만 스트림에 쌓이는 데이터는 수정할 수 없다. 스트림 데이터는 변경 과정인데 이걸 바꾼다는건 현실에서 일어난 사실 자체를 바꾸는 셈이기 때문이다. 스트림 데이터는 불변(immutable)이다.
한편, ‘데이터베이스를 쓰면서도 변경을 저장할 수 있는데?’ 라고 생각할 수도 있다. 맞는 말이다. ‘게임 과정’이라는 테이블을 두고 왼쪽 사진에 해당하는 데이터를 계속 누적할 수 있다. 하지만 이것은 애플리케이션 레벨에서 변경 이력을 데이터베이스에 저장하는 것이지, 데이터베이스가 메세지큐처럼 스트리밍 기능을 할 수 있다는 걸 의미하지는 않는다.
이것은 전적으로 프로그램 요구사항에 달려있다. 화투를 치면서 ‘이번 판에서 돈을 잃음’ 이벤트가 발생할 때마다 ‘술을 마심’ 행위를 해야한다면 스트리밍 모델로 가는게 좋다. 변경 이벤트를 받아서 곧바로 처리할 수 있기 때문이다. 그게 아니라 승리의 순간을 두고두고 기억하기 위해 마지막 결과만 사진으로 찍어두는게 목적이라면 굳이 메세지큐를 사용할 필요는 없다. 데이터베이스 테이블에 남아있는 기록으로도 충분하기 때문이다.
물론 이 둘을 섞을 수도 있다. 데이터베이스에 update
연산이 일어날 때마다 데이터 스트림 이벤트를 발생시켜서 원하는 동작을 하는 것이다. 이를 CDC(change date capture, 변경 이벤트 감지)라고 부른다.
앞에서 ‘스트리밍 모델’이라는 단어를 사용했다. 이번에는 데이터를 주고 받기 위한 패러다임을 스트리밍 모델과 비교해보자. 이벤트 기반 처리, pull/push 기반 처리, 멀티캐스팅/브로드캐스팅 등등… 다양한 모델이 많지만 여기서는 스트리밍 모델이 어떤 특징을 갖고 있는지 이해하기 위해 쉬운 비교군 2가지를 가져올 것이다. 바로 요청-응답 모델과 배치 모델이다.
애플리케이션 개발자가 가장 흔하게 접하는 모델이 바로 이 모델이 아닐까싶다. 먼저 데이터에 대한 요청을 보내고, 그 요청을 처리하는 곳이 따로 있다. 요청을 보내는 쪽은 클라이언트, 처리하는 쪽은 서버라고 한다. 서버가 먼저 클라이언트에게 뭔가를 보내는 일은 잘 없다. 클라이언트가 요청을 보내고 잠깐! 기다렸다가 데이터를 받아간다. 끝이다.
데이터베이스 세계에서 이 패러다임은 OLTP(OnLine Transaction Processing)으로 알려져 있다. POS(Point-of-sale) 시스템, 신용카드 결제 시스템 그리고 시간 추적 시스템(time tracking system)이 보통 이 패러다임으로 작동한다. - 425p.
배치 모델은 한 번에 많은 양의 데이터를 처리하기 위한 모델이다. 배치 프로세스는 요청-응답 모델의 서버보다 훨씬 많은 양의 데이터를 처리한다.
처리 속도도 다르다. 요청-응답 모델에서 서버가 하나의 요청을 처리하는 데 몇 분, 몇 시간이 걸린다면 클라이언트로부터 무수히 많은 관심과 덕담 세례를 받게 될 것이다. 하지만 배치 모델에서는 그렇지 않다. 분, 시간, 일 단위로 실행되는 배치에는 정해진(그러니까 누군가가 희망하는) 처리 시간 같은게 없다. 처리해야하는 데이터양만큼 지연 시간도 커질 수 있고 개발자는 이 사실을 이미 알고 있으며 자연스러운 것으로 받아들인다. 2-3ms 만에 몇 억건의 데이터를 처리하는 배치 프로세스 같은건 아무도 바라지도 않고 만들려 하지도 않는다.
요청-응답 모델에서 서버가 클라이언트의 요청에 대한 응답을 보내면 그대로 ‘처리 완료’가 되듯이 배치 모델에서도 처리 완료라는 단어를 쓸 수 있다. 원래 계획했던만큼의 데이터를 처리하고나면 종료되는게 배치 프로세스다.
데이터베이스 세계에서 데이터 웨어하우스(data warehouse)나 비즈니스 인텔리전스 시스템(business intelligence system)이 이러한 부류에 속한다. 하루에 한 번 대량의 배치 단위로 적재되고, 리포트가 생성되고, 사용자들은 다음 번 데이터 적재가 일어날 때까지 똑같은 리포트를 보게 된다. - 425p.
스트리밍 모델에서의 ‘처리 완료’는 배치 프로세스의 처리 완료와 좀 다르다. 스트림을 처리하는 서버의 ‘처리 완료’는 스트림 안에 들어있는 데이터 하나하나를 처리했다는 의미이지 결코 그 서버가 종료되어도 된다는걸 의미하지 않는다. 전체 스트림에 대한 ‘처리 완료’는 아예 그 스트림의 시작점이 사라지거나 서비스가 종료되지 않는 이상 이야기 할 수 없다. 배치 프로세스가 다루는 데이터와 다르게, 스트림 데이터는 무한하기 때문이다.
무한한 크기의 데이터세트에서 연속적으로 데이터를 읽어와서, 뭔가를 하고, 결과를 내
보내는 한 우리는 스트림 처리를 수행하고 있는 것이다. 단, 이것이 지속적으로 계속되어야 한다. 매
일 오전 2시에 시작되어서 스트림에서 500개의 레코드를 읽어서 처리하고, 결과를 내놓은 뒤 끝나는
프로세스는 엄밀히 말해서 스트림 처리 프로세스라고 할 수 없다. - 426p.
요청-응답 모델과 비교했을 때 스트리밍 모델은 논블로킹하게 작동한다고 말할 수 있다. 요청-응답 모델에서는 “클라이언트가 요청을 시작하고 잠깐! 기다렸다가
데이터를 받아간다”고 했다. 통신의 흐름이 서버가 응답을 하는 데에 걸리는 시간만큼 블로킹 되는 것이다.
하지만 스트리밍 모델에서는 다르다. 우선 스트리밍 모델에서는 데이터를 쓰는 쪽과 읽는 쪽이 있는데, 데이터를 쓰는 쪽에서는 읽는 쪽의 속도에 맞추지 않고 끊임없이 쓸 수 있다. 데이터를 읽는 쪽에서도 마찬가지로 쓰는 쪽의 속도와 관계없이 자신이 처리할 수 있는만큼 이벤트 가져가서 처리할 수 있다. 그 중간 조율은 둘 사이에 위치하는 메세지큐가 할 일이다. 따라서 스트리밍 모델에서는 데이터를 읽는 쪽이 충분히 빠르지 않아서 이슈가 될 수 있을지언정 데이터 흐름을 블로킹하는 구간은 없다고 보면 된다.
논블로킹이라고 해서 요청-응답 모델보다 스트리밍 모델의 처리 속도가 빠르다는 것을 의미하지는 않는다. 오히려 클라이언트에 대한 서버의 응답 속도는 나머지 두 모델보다 훨씬 빠른 편이다. 스트리밍 모델은 요청-응답 모델과 배치 모델 사이의 어딘가에 위치하는 정도의 지연 시간을 보장한다. (그도 그럴것이 요청-응답 모델에서는 데이터 흐름이 서버 → 클라이언트
로 한 hop뿐이지만 스트리밍 모델에서는 쓰는 쪽 → 메세지 큐 → 읽는 쪽
으로 hop이 하나 늘어날 뿐더러, 각 프로세스의 처리 성능도 제각각이기 때문에 시간이 좀 더 걸리게 된다.)
스트림 처리는 이벤트 처리에 2밀리초 정도 기다리는 응답-요청 방식과 하루 한 번 작업이 실행되고 완료하는 데 8시간이 걸리는 배치 처리 사이의 격차를 메워준다. 대부분의 비즈니스 프로세스는 굳이 수 밀리초 이내의 응답을 즉시 요구하지도 않지만, 그렇다고 해서 다음 날까지 기다릴 수도 없다. - 426p.
예를 들어서 저녁 9시 이후에 식당에서 카드 결제를 하면 몇 분 뒤에 페이백 해주는 기능을 떠올려볼 수 있다.
(그렇다고 토스에서 이렇게 구현했다!는 뜻은 아니다. 내가 만들었다면 이렇게 만들었을 것 같다.)
물론 모든 애플리케이션이 위 분류 중 정확히 한 가지에만 해당하는건 아니다. 다들 한 번씩 들어보았을 토렌트를 생각해보자. 토렌트에서 만들어지는 데이터 흐름은 보통 파일 하나가 단위가 된다. 사용자는 그 파일 하나 중 몇 프로를 전달받았는 지 실시간으로 확인할 수 있다. 그렇다면 이건 배치일까, 스트림일까?
연속적인 데이터 흐름이 만들어진다는 점에서 스트리밍이라고 볼 수 있다. 하지만 무한한 데이터가 아닌 ‘파일 하나’를 전송하는 것이기 때문에 그 데이터 흐름의 시작과 끝이 있다는 점에서 배치 처리의 특징도 갖게 된다.
이쯤에서 우리는 위에서 만든 모델의 분류가 모든 소프트웨어 애플리케이션을 커버할 수 없다고 혼란스러워하는 대신, 소프트웨어 개발과 설계를 할 때 각 모델을 어떻게 써먹을지를 생각해봐야 한다.
코드 짤 때 써먹지. 예를 들어서 어떤 내용의 로그를 남기고, 오류가 발생했을 때 어떻게 처리할 것인지 결정하는 데에 쓰일 수 있다. 배치 프로세싱은 한 번에 많은 양의 데이터를 처리하고, 한 번 실행되는데 걸리는 시간이 길기 때문에 각 아이템 하나에 대한 추적을 기록하는 것이 중요하다.
배치 실행에서 수천건의 데이터를 처리하다가 하나가 막혔다고 해서 전체 프로그램이 멈춰버리면 안 된다. 어디까지 했는지 남겨놓고, 다시 다른 아이템에 대해 작업을 이어나가야 한다. 요청-응답 모델을 생각해보면 좀 다르다. 어차피 여기서 처리하는 아이템은 하나뿐이기 때문이다. 여기서는 클라이언트에게 처리에 실패했음을 명확히 전달하는게 중요하다. 적절한 HTTP status code 나 에러 메세지를 던져주는 것이다.
아이디 X 인 사용자를 처리하다가 원하는 데이터가 없어서 여기까지만 처리했음. 넘어감.
이라는 로그를 남긴 뒤 다음 작업을 계속 수행하고404 Not Found
를 응답하는게 바람직하다.스트림 처리에서는 어떨까? 스트림의 종류에 따라 다르다. (사실 나머지 모델도 요구사항에 따라 다르지만 일반적인 얘기를 해봤다.) 비디오 스트리밍 같은 경우엔 수많은 화소와 끊임없는 소리에 대한 정보 중, 단 1 bit 도 놓치지 말아야 하는 상황은 잘 없다. 그 대신 멈추지 않고 영상을 계속 보여주는 것에 집중하기 위해 무언가 유실되더라도 넘어가는 방법이 있다.(그래서 UDP를 쓰거나 TCP 통신을 쓰면서도 처리량과 속도에 집중할 수 있는 기술을 택한다.)
혹은 변경 이벤트 하나하나가 중요한 상황에서 이벤트 처리에 실패한다면 그 이벤트를 어디엔가 기록한 뒤 원래 스트림과 따로 처리하거나, 배치 프로그램을 일정 주기마다 돌려서 데이터를 보정하는 방법도 있다. 스트리밍 데이터 하나가 잘못되었다고 해서 그 다음 데이터의 흐름을 멈출 수는 없는 노릇이기 때문이다. 앞서 말했듯이, 스트리밍 데이터는 논블로킹이다.
좀 더 넓은 관점에서 보면 기능 설계에도 써먹을 수 있다. (사실 그러려고 이런걸 배우는게 아니겠는가!) 각 패러다임의 특징을 이해하고, 내게 주어진 요구사항을 구현하기 위해서는 어떤 방식을 써야 장점을 최대한 활용하고 단점을 최소화 할 수 있을지 생각해보자.
위에서는 스트림이 한 번 지나가고 나면 마치 돌이킬 수 없는 것처럼 생각했지만 실제 세계에서 개발자에게 그런 제약조건은 너무 가혹하다. 초당 수천, 수만건씩 몰려오는 데이터를 단 한 번의 실수도 누락도 중복도 없이 처리해야 한다니! 다행히 이 가혹한 조건을 조금 완화해주는 이벤트 스트리밍 플랫폼이 있다. 바로 아파치 카프카다.
카프카 스트림즈
라이브러리로 데이터 ETL 처리를 할 수 있다. (이건 나도 아직 안 해봤다. 언젠가 해보면 재밌을 것 같다.)아파치 카프카는 이미 널리 쓰이고 있다. 많은 기업에서 채택해서 실제 서비스에 적용하고 있으며 활발히 컨트리뷰트 되는 오픈소스 프로젝트다. 공식 문서는 https://kafka.apache.org/intro 에서 확인할 수 있다.
스트림은 변경을 유발하는 이벤트의 연속이다. 테이블은 여러 상태 변경의 결과물인 현재 상태를 저장한다. 이러한 점에서 볼 때 스트림과 테이블은 같은 동전의 양면임이 명백하다. - 430p.
저장하게 된다.
분류 | 요청-응답 | 배치 | 스트리밍 |
---|---|---|---|
데이터의 양 | 유한 (적음) | 유한 (많음) | 무한 |
지연 시간 | 짧음 | 긺 | 중간 |
블로킹? | O | ▵ (전통적인 배치는 블로킹이지만 아닌 것도 있다.) | X |