Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

소개

얼마전에 스타벅스는 2단계 커밋을 사용하지 않는다.라는 문서를 읽었다. 어떤 식으로 메시지를 교환하고, 할당하고, 폐기해야 하는지에 대한 내용을 스타벅스 매장에서의 주문관리를 예로 들어서 아주 쉽고 깔끔하게 설명하고 있다.

이 문서의 내용을 소프트웨어에 어떻게 적용할 수 있을지 고민해 보면 재미있겠다는 생각에 이 글을 만들었다. 가능한 원문의 흐름을 비슷하게 따라가도록 구성했다.

소프트웨어 시나리오

실제 소프트웨어를 예로 들어서 문제를 풀어나갈까 한다. 나는 IoT 서비스를 선택했다. IoT 서비스를 선택한 이유는 아래와 같다.
  1. 요즘( 현재) IoT 관련일을 하고 있어서.
  2. 기기와 기기, 기기와 사람은 메시지를 주고 받는 것으로 관계를 맺는다.
  3. 사람이 기계에 명령을 내리는 것, 그리고 명령의 결과를 전송해 주는 과정이, 커피매장에서의 주문관리와 비슷하기 때문에.
  4. (스타벅스 처럼)다양한 기기, 다양한 명령을 처리해야 한다.

작업의 처리

스타벅스에 들어선 고객이 주문을 넣는 과정은 그림과 같이 묘사할 수 있다.

여러 명의 고객이 다양한 주문(order)을 한다. 고객과의 접점에는 점원이 있는데, 이 점원은 단지 주문만 받는 일을 한다. 주문을 받은 다음에는 주문 순서대로 에스프레소 기계의 상단에 빈컵을 올려 놓는다. 물론 빈컵에는 주문내역이 적혀 있고, 바리스타는 주문내역을 읽어서 커피를 만든다. 에스프레소 기계가 메시지 큐로서의 역할을 하는 셈이다.

이 과정의 핵심은 점원과 바리스타가 물리적으로 분리돼 있다는데 있다. 분리된 두 지점을 연결하는 건 메시지큐로, 요청과 처리가 분리돼 있으므로, 각 부분은 독립적으로 관리할 수 있다.
  1. 일반적으로 점원 보다는 바리스타의 작업 처리가 느리다. 한명의 점원에 3명의 바리스타 이런 식으로 작업 처리 과정을 설계할 수 있다.
  2. 점원은 잡다한 상황을 신경 쓸 필요가 없다. 바리스타가 할당됐는지 아닌지 등을 신경 쓸 필요 없이, 주무만 받아서 큐에 적재하면 된다.
  3. 바리스타도 다른 상황에 신경 쓸 필요가 없다. 에스프레소 기계의 맨 앞에 있는 컵을 읽어서 커피를 제조하기만 하면 된다.
  4. 스케일링이 쉽다. 작업의 지연 정도는 큐를 모니터링 하는 걸로 확인 할 수 있다. 빈잔이 늘어나면, "바리스타"만 더 투입하면 된다.(혹은 바리스타가 바빠지거나)
  5. 큐는 버퍼의 역할을 한다. 지금당장 일할 수 있는 바리스타가 없더라도 요청을 받을 수 있다. 물론 이 경우 커피가 배달되는 시간이 약간 더 길어지긴 하겠지만, 서비스가 중단되지는 않는다. 바리스타들은 동료의 상황과 큐의 상황을 보면서 업무속도를 조절 할 수도 있다.
  6. 주문과 배달이 분리돼 있기 때문에, 고객은 점원의 응답을 기다릴 필요 없이, 자리에 가서 배달신호를 기다리면 된다. 그동안 고객은 게임을 하거나 수다를 떨 수 있다.
IoT 서비스에서는 아래와 같은 구성이 가능 할 것이다.

  • 구간 - 1에 있는 API Server는 유저의 요청을 받아서 큐에 전송 하기 만 한다.
  • 구간 - 2에 있는 Message Worker는 메시지만을 처리한다. 이들 구간 사이에는 Message Queue가 있으며, 큐를 기준으로 두 개 구간이 나뉜다.
API 서버와 Message worker이 서로 분리되 있기 때문에, 요청에 대한 응답이 동기화 되지 않는다. 따라서 큐를 기준으로 비동기 구간이 만들어진다. 점원이 바리스타의 상태를 알 필요가 없고, 응답을 기다릴 필요가 없는 것처럼, API 서버 역시 Message Worker의 상태와 응답을 알 필요가 없다.

요청을 한 통신 채널로 응답 메시지를 받을 필요가 없기 때문에, HTTP 프로토콜을 사용 할 수 있다.

HTTP는 20년이 넘는 시간 동안 인터넷 표준 프로토콜로 사용해왔다. 언어와 플랫폼을 막론하고 훌륭한 라이브러리와 툴들이 준비돼 있으며, 다룰 수 있는 엔지니어들도 많다. 특히 웹 브라우저와의 상호 운용성은 엄청난 장점이다. 네이티브 애플리케이션과의 상호운용성 역시 매우 뛰어나다. 인터넷 애플리케이션 혹은 서비스 연동을 위한 막판보스 같은 녀석이다.

메시지큐로는 AMQP 기반의 애플리케이션 혹은 REDIS와 같은 특화된 NoSQL 애플리케이션으로 구현할 수 있다. 메시지큐는 하나 이상의 메시지 워커들이 메시지가 도착하기를 바라고 있기 때문에, 작업이 몰릴 때 경쟁적인 처리자(Competing Consumer)시나리오로 움직일 수 있다.

상관 관계

스타벅스의 비동기식 처리 방식은 주문한 순서대로 처리를 할 필요가 없다. IoT 서비스에서도 마찬가지다. 각 요청의 처리 방식이 다르고 요청이 서로 독립적이기 때문에 순서를 지킬 필요가 없다.

대신 요청과 응답의 상관관계를 처리 하기 위한 방안을 찾아야 한다. 동기 처리 방식의 가장 대표적인 예는 은행 업무다. 은행업무의 경우 고객의 요청이 들어오면, 요청에 대한 응답이 끝날 때까지 다른 요청을 받지 않는다. 요청에서 응답까지 연속된 하나의 통신 채널이 만든다. 따라서, 상관 관계를 고민할 필요가 없다. 응답은 무조건 이 고객의 요청에 대한 것이란느 걸 보장 할 수 있다.

하지만 스타벅스식 비동기 처리 방식에서는 요청과 응답이 분리되므로, 두 개의 상관관계를 식별하기 위한 장치가 필요하다. 컵에 고객의 이름을 적는게 가장 쉬운 방법일 테다.

이렇게 하면, 주문순서와 상관없이 고객에게 정확히 배달할 수 있다.

IoT도 이 문제를 해결해야 한다. IoT에서는 메시지에 고유한 ID(MsgID)를 부여하는 것으로 이 문제를 해결 한다. 클라이언트는 MsgID를 추적하는 것으로 어떤 요청에 대한 응답인지 확인 할 수 있다. MsgID는 각 클라이언트 안에서 유일하면 된다.

메시지 통신 프로토콜로 널리 사용하는 JSON-RPC를 보자.
--> {"method": "echo", "params": ["Hello JSON-RPC"], "id": 1}
<-- {"result": "Hello JSON-RCP", "error": null, "id":1}
id를 이용해서 상관관계를 기술하고 있다. JSON-RPC 뿐만 아니라 거의 모든 통신 프로토콜들이 MsgID를 이용해서 비동기하에서의 상관관계를 기술한다.

MsgID를 어느 영역에서 만드는 게 좋을까 ? 서버에서 만드는 방법이 있긴한데, 간단하지만 CPU 자원을 소모한다는 문제가 있다. JSON-RPC를 사용한다고 가정하면, 단지 MsgID를 넣기 위해서, 메시지를 역질렬화 해서 값을 채워넣고 다시 직렬화 해야 한다. 여기에는 상당히 많은 비용이 들어간다.

클라이언트에서 메시지를 만들때 MsgID를 함께 넣어서 보내는게 좋을 것 같다.

예외 처리

IoT에서의 예외는 여러 단계의 소프트웨어 혹은 네트워크 문제로 발생한다. 메시지 형식이 잘못됐거나, 값이 잘못되거나, 소프트웨어가 뻗어버리거나 등등 다양한 문제가 발생할 수 있다.

이런 문제에 대응하는 나의 기본 노선은 "최대한 단순하게"이다. 자동으로 문제를 해결하거나 자동으로 복구하는 등의 작업은 하지 않는다. MsgID를 근거로 에러 메시지를 전송하고 클라이언트로 하여금 처리하도록 한다. 클라이언트가 할 수 있는 일은, 에러 메시지를 출력하거나 리트라이 하거나 일 것이다. 그외에 다른 건 하지 않는다.

이걸 스타벅스 서비스에 응용하자면 이런 거다.

머신이 고장나거나, 바리스타가 자리를 비웠다면, 그걸 복구하기 위한 작업을 하지 않는다. 주문에 응할 수 없습니다. (혹은 실패 했으니)
  1. 주문을 새로 하세요.
  2. 주문을 새로 하기 싫다고요 ? 그럼 그냥 집에가세요.
현실에서야 이런식의 대응을 하는게 쉽진 않겠지만, 소프트웨어에서는 부담없이 구현 할 수 있다.

물론 수행 중인 요청의 취소, 실패시 원래 상태로의 복구와 같은 복잡한 작업을 해야 하는 시스템들도 있다. 금융 시스템의 경우에는 반드시 이러한 작업이 필요할 거다. 그러나 IoT에서는 그런 복잡한 작업이 필요 없다. 오류가 발생하기 전의 상태로 되돌리기 위해서 수행한 작업을 취소하는 것은 지나치게 복잡하다. 아예 처음부터 이런 복잡한 처리가 필요 없도록 설계를 할 필요가 있다.

대신, 오류는 반드시 모니터링 할 수 있어야 하며, 클라이언트에 반드시 전달해야 한다. 비동기 시스템에서는 오류(주로 메시지 실패)를 탐지하는 것도 어려운 일이 될 수 있다. 앞단에 있는 서버는 뒷단에 전달한 요청이 제대로 처리됐는지 알 수 없기 때문이다. 예를 들어 Message Worker에서 메시지를 읽어서 처리하는 도중에 실패하면, 이 메시지는 결코 처리할 수 없을 것이다. Message Worker가 메시지를 처리하고 Send까지 한 후 큐의 내용을 삭제하는 등의 방법을 고민해야 한다.

"클라이언트가 offline 인 상태"에 대한 처리 역시 필요하다. 메시지가 도착하지 않았을 경우 메시지 박스에 저장해서 online 상태가 되면 읽도록 한다.

대화 Conversations

지금까지의 구성은 "응답"이 빠져있다. 스타벅스의 경우를 먼저 살펴보자. 바리스타가 커피제조를 끝내면, 이것은 테이블로 올리고 즉시 큐에서 다음 주문을 가져와서 처리한다. 점원은 컵에 있는 이름을 부르는 걸로 고객에게 커피를 배달한다. 요즘에는 일일이 호명하는 대신에 진동패드를 이용한다. 원리는 동일하다.

요청<->처리<->응답이 모두 독립적으로 그리고 비동기적으로 이루진다. 각 구간을 전문화 할 수 있으며, 구간별로 스케일링을 할 수 있다는 장점이 있다.

IoT에서의 응답 처리에 응용해보자.

세부적인 구현은 달라질 수 있겠다. 요는 응답과 요청의 분리다. 응답은 Server Push로 persistent 한 연결이 필요하다. 요청은 HTTP, 응답은 MQTT(혹은 다른 프로토콜)등으로 완전히 분리한다.

세부구현 단계에서는 진동패드 같은 걸 어떻게 구현 할 것인지가 중요한 문제다. 클라이언트의 연결 테이블(클라이언트가 어느 Push server에 연결해 있는지)을 유지하는 방법, Consistent hash 구성을 해서 클라이언트가 연결된 push server를 해시연산으로 찾는 방법에 대한 고민이 필요하다.

비동기 메시지 통신이라고는 하지만 동기 구간과 비동기 통신 구간이 함께하는 구성됨을 알 수 있다. 이 경우 비동기 구간이 전체 전체 통신의 성격을 결정하기 때문에 비동기 통신으로 분류한다. 이것으로 요청에서 응답까지의 사이클을 완성했다.

정리

스타벅스는 2단계 커밋, 즉 점원이 커피가 완성 될 때까지 하나의 고객만을 바라보고 기다리는 방법을 사용하지 않고 있다. 이런 과정은 "트랜잭션"이 완전히 끝날 때 까지 자리를 떠날 수 없게 만든다. 이는 서비스 가능한 고객의 수를 극적으로 감소시킬 것이다. 이 방법도 물론 장점은 있다. 커밋에서 요청까지를 하나의 원자적 트랜잭션으로 구성 함으로써, 요청의 성공과 실패를 명확히 관리할 수 있다 점은 장점이다. 그리고 매우 직관적이라서 프로세스 설계를 단순화 할 수 있다.

2단계 커밋을 사용하는 대신, 스타벅스는 비동기적으로 업무를 처리 하게 함으로써, 더 많은 고객을 효율적으로 처리 할 수 있게 됐다. 비동기 처리의 경우에는 독립된 여러 흐름들의 트랜잭션을 관리하기 위해서 메시지 흐름에 대한 좀 더 세밀한 설계가 필요하다. 하지만 설계만 확실하다면, 보다 효율적으로 메시지를 처리 할 수 있다.