10. MQ, Pub/Sub 기본 개념 이해
"스프링부트 백엔드 프로그래밍"이라는 주제로 약 8주간 글을 작성할 예정입니다. 스터디가 잘못된 방향으로 가지 않도록, 의견 및 조언을 아낌없이 해주시길 부탁드립니다.
1주 차 - 스프링부트란 무엇인가?, 간단한 API 서버 만들어보기
2주 차 - 스프링 프레임워크 기본 개념 이해하기
3주 차 - Rest API, 테스트 코드 작성하기, 예외 처리하기
[미정] 6. Rest API (HTTP 기본 개념)
4주 차 - AOP, 스프링부트 캐시 추상화, Redis 연동하기
5주 차 - JPA, Spring Data JPA
6주 차 - MQ, Pub/Sub
10. [이번글] MQ, Pub/Sub 기본 개념 이해
11. 스프링부트 환경에서 RabbiMQ, Kafka, Redis Pub/Sub 연동
[미정] 7주차 - 보안(인증)
12. Spring Session, JWT
13. Spring Security
[미정] 8주 차 - 병렬, 비동기 프로그래밍
14. Spring Async
15, Thread Pool 개념 이해
[미정] Spring Cloud, Spring Session 등
이번 주에는 MQ에 대해서 공부합니다. 취준생에게, MQ 를 공부하는 것이 취업에 얼마나 도움이 될까? 하는 의문이 있었습니다. 코딩테스트 및 알고리즘 공부에 집중하는것이 좋겠다는 생각이라서, 처음 목차에서 제외할까도 생각했습니다. 하지만, 최근 N모사 신입 채용 공고에 MQ 사용 경험이 우대사항에 있다는 사실에 놀랐습니다.
요즘 신입들은 정말 대단하다는 생각을 하면서... 스터디 목차에 넣어봤습니다.
지난 주 JPA가 너무 지루했다는 의견이 있어서, 이번 주는 조금은 가벼운 마음으로 진행합니다. 눈에 잘 들어오지 않는 소스코드 보다는, 그림 위주로 설명을 해보겠습니다.
잘못된 내용이 있다면 꼭 제보해주세요.
시스템 통합은, 다양한 방법으로 구축할 수 있습니다.
- 파일 전송
- 공유 데이터베이스
- HTTP Request/Response
- 메시징
등등 다양한 방법으로 시스템을 통합할 수 있습니다.
그동안 스터디를 진행하면서 우리는 HTTP Request/Response 방법을 주로 시스템 통합을 위해 사용했습니다. Rest API, GraphQL 등이 HTTP Request/Response 방법의 대표적인 방법입니다.
Request/Response 방식은 HTTP 프로토콜을 사용하기 때문에 Stateless 한 특징이 있으며, 심플하고 단순하기 때문에 구현하기 아주 쉽습니다. 하지만, 클라이언트-서버 사이에 강한 의존성이 생기며, 서버가 반드시 실행 중일 때만 데이터를 전달받을 수 있습니다. 그리고, 클라이언트는 서버가 다음 메시지를 보내기 전까지 응답을 기다리고 있습니다. 즉, 동기식 통신 방식으로 동작합니다.
장점
- 심플하고 구현하기 쉽다.
- Stateless (HTTP)
단점
- 클라이언트-서버 시스템 사이 높은 의존성
- 서버가 반드시 실행 중일 때 동작
- 동기식 통신 방식
"HTTP Request/Response 통신은 단단하게 결합된 시스템 아키텍처이다. "
참고로, 스프링 프레임워크 서버에서 WebFlux, 리액티브 서버(Netty..), HTTP 2.0 등 기술 도입으로, Non-Blocking(논블록킹) & Async(비동기) 통신을 HTTP 프로토콜에서 구현할 수 있습니다. 저도 자세히는 몰라서 이 부분은 따로 공부하세요~~
HTTP 통신과는 반대로, 느슨하게 결한 된 비동기 시스템 통합 방식 중 대표 기술은 바로 "메시징"입니다. "메시징"은 중간 시스템을 통해 발신자에서 수신자로 데이터를 전송하는 포괄적인 용어입니다.
중간 시스템을 통해서 전송하기 때문에, 발신자(서버)는 데이터를 전송받는 수신자(클라이언트)에 대해서 몰라도 됩니다. (서로 알아도 됩니다만, 굳이 알아야할 필요가 없습니다.)
장점
- 수신자(클라이언트)를 확장하기 쉽다.
- 마이크로서비스 아키텍처에 적합하다.
- 느슨한 연결
단점
- 복잡도 증가
- 기술 스택 추가
"메시징"에 대해서 아주 간단하게 설명하였습니다만, 상세한 내용이 궁금한 개발자는 아래 책을 반드시 읽어보시길 바랍니다.
http://www.yes24.com/Product/Goods/14631181?scode=032&OzSrank=2
책 내용이 너무 방대해서 가끔 필요할때마다 레퍼런스로 꺼내서 보시면 됩니다..
글 초반에 설명했듯이, "메시징"이라는 단어는 매우 포괄적인 용어이며, "메시징"을 구현하는 방법은 매우 다양합니다. 다양한 메시징 방법 중에서, Pub/Sub, 메시지 큐잉 에 대해서 설명하겠스빈다.
(지극히 개인적인 생각일 뿐입니다. 혹시, 다른 의견은 댓글로 알려주길 바랍니다. 주니어 개발자가 오해하지 않도록.. 꼭 제보해주세요.)
- 메시지 큐잉(Point-to-Point Channel)
- Publish-Subscribe(Pub/Sub)
메시지 큐잉은 Point-to-Point Channel 방식으로, 오직 한 수신자만 메시지를 수신(소비)하게 됩니다. 주문에 대한 처리를 수신하는 클라이언트가 있다고 가정하면, 아래와 같이 3건의 주문을 순차적으로 받아서 처리하게 됩니다.
순차적으로 메시지를 소비할 때, 한대의 서버에서 처리를 하다보면, 처리 속도가 지연되어서 메시지들이 채널에 쌓이게 되는 경우가 발생할 수 있습니다. 즉, 병목현상이 발생할 수 있습니다. 주문을 처리할 수 있는 서버를 늘려주면, 동시에 여러 메시지를 처리할 수 있게 구축할 수 있습니다. 아래 그림은, 주문 3건을 3개의 주문 처리 시스템이 하나씩 처리하는 구조입니다.
주문 처리를 하는 서버를 증설하는 경우, 주문 요청하는 서버에는 전혀 영향을 받지 않습니다. 주문 요청은 메시지 시스템에 전달하기 때문에, 주문 처리 시스템에 대한 의존성이 없기 때문입니다.
주문을 요청하는 시스템은 주문에 대한 이벤트 메시지를 전달하는데, 주문 처리 시스템은 각각 독립된 환경에서 실행되기 때문에, 각각 "경쟁 소비자"가 됩니다. 이런 경우, 각 메시지에 대한 처리 순서가 보장되지는 않습니다. 예를 들어서, 네트워크 장애가 발생해서 주문처리 서버1 에 전송된 "주문1" 이 제대로 처리되지 않았다면, 메시지 시스템에서는 주문1 을 재전송하게 됩니다. 즉, 주문1은 주문2, 주문3 이후에 처리가 될 수도 있습니다. 이벤트에 대한 순서를 보장하기 위해서는 별도의 작업이 필요할 것입니다.
(메시지 브로커마다 다양한 방법이 존재합니다. 예를 들어서, "Kafka" 브로커의 경우에는 파티션을 지정해서 메시지를 전송하는 방법 등이 있겠지요.. 자세한 설명은 생략합니다.)
메시지 큐잉과는 반대로 Pub/Sub 은 수신자(클라이언트) 모두에게 동일한 메시지를 일괄적으로 전송합니다. 날씨와 주식 데이터를 다수의 서버에 전송한다고 가정해봅시다.
위와 같은 개념이 Pub/Sub 입니다.
MQ, Pub/Sub 용도로 사용 가능한 메시지 브로커에는 어떤 솔루션이 있을까요? 대표적으로는 아래와 같습니다.
- RabbitMQ
- Kafka
- Apache SQS
- Active MQ
- Redis Pub/Sub
등등..
RabbitMQ, Apache ActiveMQ, Amazon SQS 등은 메시지 큐잉 용도로 초기 설계가 되었지만, Pub/Sub 으로 확장해서 사용이 가능합니다. Apach Kafka 는 대량의 메시지를 처리하기 위해서, Pub/Sub 와 메시지 큐잉 개념을 혼합한 형태라 생각됩니다.(뇌피셜...전문가는 의견을 댓글로 알려주길 바랍니다.)
어떤 메시지 브로커를 사용할지에 대해서,
비즈니스 요구사항, 개발팀 기술력, 유지보수 비용 등 전반적인 상황을 고려해서 현명하게 잘 선택해야 합니다.
RabbitMQ, Kafka 에 비해서는 상대적으로 실무에서 자주 사용되지는 않지만, Redis 에서 Pub/Sub 기능을 심플하게 사용할 수 있습니다.
다른 메시지 브로커와는 다르게, Redis Pub/Sub 메시지 지속성이 없습니다. 즉, 메시지를 전송한 후 해당 메시지는 삭제되는데, Redis 어디에도 저장되지 않습니다. 실시간 메시지를 처리하는 시스템에는 나름 적합하지만, 메시지가 저장되지 않는다는 단점은 개발자가 반드시 인지하고 있어야 합니다. 즉, 수신자(클라이언트)가 메시지를 전달 받는 것을 완벽하게 보장하지 않습니다. 그래서, (개인적인 생각으로는) Redis의 Pub/Sub 기능이 매우 심플하고 괜찮지만, 메시지 전송 신뢰성을 보장하지 않는 단점으로 인해서, 별도의 추가 구현을 해야할 수도 있습니다. 3번 서버가 잠시 재부팅 중이라고 가정해봅시다. 어떻게 될까요?
살아있는 클라이언트 서버에만 메시지를 전송하며, 재부팅 중인 서버 3번에는 메시지가 전송되지 않습니다. 참고로, Redis 5.0 에서 도입된 Stream 기능이 대안이 될 수 있지만, 이 글에서는 Redis Streams 에 대해서는 다루지 않을 예정이며, Redis 의 다른 자료구조(List 등)을 사용하는 것도 대안이 될 수도 있습니다. 자세한 설명은 생략하며...
자, 그럼 Redis 의 단점을 보완할 수 있는 조금더 성숙한 메시지 브로커인 RabbitMQ 를 사용해서 Pub/Sub 을 구현해보겠습니다. RabbitMQ 는 AMQP 프로토콜을 사용하는 메시지 브로커인데, 특이한점은 메시지를 발행하는 곳에서 Queue 에 직접 전달하지 않고, 중간에 Exchange 라는 곳에 메시지를 전달합니다. 그리고, Exchange 에서는 다양한 방식으로 메시지를 큐에 라우팅 할 수 있습니다. Exchange 에는 3가지 타입이 존재하는데, Topic, Direct, Fanout 등입니다. Fanout 타입은, 바인딩 키 상관없이 모든 Queue 에 메시지를 전송하는 타입입니다.
샘플 사례는, 설명을 위해서, 임시로 만든 가정 상황인데... 적합한 사례인지 모르겠습니다. 참고만 해주세요. 어쨋든, 다수의 메시지를 구독하는 서버에 모두 전달하고 싶은 경우 아래와 같이 각 구독자들이 각각의 Queue 를 리스닝하도록 구축합니다. 그리고, Fanout Type 으로 설정한 Exchange 에서 바인딩 된 모든 큐에 메시지를 전달합니다.
만약, 클라이언트 3번 서버가 재부팅 중이면 어떻게 될까요? 메시지는 queue 에서 Ready 상태로 대기합니다.
3번 서버가 올라오면, RabbitMQ 의 queue 를 리스팅하게 되며, 그동안 전달하지 못하고 쌓여있던 메시지를 전달 받을 수 있습니다.
단, 위 사례는 RabbitMQ 의 Queue Feature(속성) 중에서, durable 를 true 로 한 경우에만 가능합니다. 만약, 큐의 durable 속성이 false 이라면, 3번 서버의 애플리케이션이 재시작하게 되면, 큐는 삭제되었다가 새로 생성이 됩니다. 제가 실무에서 RabbitMQ 를 Pub/Sub 용도로 사용한 경우에는 durable 속성이 false 인 큐만 사용했었는데요, 애플리케이션이 잠시 다운되었을 때 전달되는 메시지는 굳이 저장해놨다가 다시 보내지 않아도 되는 기능이었습니다.. 이유는, 애플리케이션이 처음 실행될 때, 필요한 데이터를 퍼시스턴스 저장소에서 전부 가져오는 로직이었기 때문입니다. Redis 를 Pub/Sub 으로 사용하는 경우에도 마찬가지로, 애플리케이션이 재시작할 때 필요한 데이터를 전부 로딩하는 방식으로 사용은 했기 때문에, Pub/Sub 메시지가 유실되어도 크게 문제가 되지는 않았었습니다...
(어쨋든, 위 사례가 적합한지 의문이네요...ㅠㅠ RabbitMQ 를 Pub/Sub 용도로 사용해보신 분은 제보를 해주세요.)
이번에는, RabbitMQ 를 Pub/Sub 의 용도가 아닌, 메시지 큐잉 용도로 사용하는 경우에 대한 사례를 알아봅시다. 주문 처리 시스템을 RabbitMQ 를 사용하면 아래와 같이 구축할 수 있습니다. Exchange 타입을 fanout 이 아니라, Topic 또는 Direct 로 설정하였습니다. 바인딩에 대한 상세한 내용은 생략합니다. 너무 중요한 내용이지만, 시간 관계상 관심있는 분은 따로 공부하시면 됩니다. 암튼, 아래 그림과 같이 주문 요청 세 건을 주문처리 서버에서 사이좋게 한개씩 처리합니다.
만약, 주문 처리 3번 서버가 장애가 발생하면 어떻게 될까요? 아래 그림과 같이, 메시지는 누락되지 않고 다른 서버에서 처리를 해줄 것입니다. 메시지는 전부 수신했기 때문에, Ready 카운트는 0 인 상황입니다.
Exchange, 바인딩, 라우팅 등 RabbitMQ에 대해서 자세하게 설명하면 좋겠지만... 이번 글에서는 아주 기본적인 내용만 설명을 하였습니다. 자세한 내용은 따로 공부하길 바랍니다.
마지막으로 카프카에 대해서 설명합니다. 역시 이 글에서 모든 내용을 설명할 수는 없습니다. 위에서 소개했지만, 카프카는 조금 특이한 구조인데요, 카프카를 만들 개발자들이 초기 설계할 때 MQ 와 Pub/Sub 을 혼합해서 설계한 것 같습니다. 이미 아시겠지만, 카프카는 링크드인 이라는 회사에서 당시 적합한 메시지브로커가 없어서 직접 만든 오픈소스 메시지 브로커입니다. 주문 요청에 대한 처리를 컨슈머 그룹A 에서 처리합니다. 컨슈머 그룹에 포함된 서버들은 주문 처리를 나눠서 처리합니다. 즉, 메시지 큐잉 역할 역할을 합니다. 단, 메시지 전송 방식에서 RabbitMQ 와 큰 차이가 있습니다. 바로, 메시지를 가져오는 방식입니다. 카프카는 메시지를 수신하면 어디까지 가져왔는지 "현재 오프셋"이라는 기록을 저장하며, 컨슈머가 메시지를 처리 완료하면 "커밋 오프셋"이라는 기록을 저장합니다. 아래 그림을 보면, 컨슈머 그룹은 주문 4까지 메시지를 가져왔습니다. 그래서, 현재 오프셋은 주문4를 기록하였습니다. 하지만, Consumer 에서 주문 4에 대한 처리가 아직 진행중이기 때문에 커밋 오프셋은 아직 주문3에 머물러 있습니다.
만약, 주문4에 대한 처리가 완료가 되면, 아래와 같이 커밋 오프셋과 현재 오프셋이 동일한 위치까지 진행됩니다. 만약, 주문4에 대한 커밋이 되기 전에 Consumer 에서 장애가 발생한다면, 커밋 오프셋부터 메시지 전송을 재개할 것입니다. 아무튼, Consumer 에서 메시지 처리가 완료되면 아래와 같습니다.
RabbitMQ의 경우에는 메시지 브로커에서 컨슈머에 메시지를 Push 하는 방식입니다. 하지만, Kafka 에서는, 컨슈머 그룹에서 메시지를 Kafka에서 땡겨옵니다. 그리고, 메시지를 어디까지 가져왔는지 오프셋이라는 좌표(?) 를 저장하며, 처리가 완료되면 커밋 오프셋이라는 좌표(?)를 저장합니다. 위 그림만 봤을 때는 메시지 큐잉의 역할입니다.
그런데, 기존 기능을 그대로 유지하면서, 동시에 주문 데이터에 대한 머신러닝 기술을 도입하고 싶습니다. 어떻게 처리하면 될까요? 이 때 적용되어야 하는 개념이 바로, Pub/Sub 입니다. 각각의 컨슈머 그룹은, 각각의 오프셋을 저장합니다. 아래 그림을 보시면 이해하기 조금 편할겁니다.
컨슈머 그룹에서 메시지를 컨슈머들이 나눠서 처리하는 것은 메시지 큐잉 개념입니다. 하지만, 또 다른 컨슈머 그룹에서, 같은 메시지를, 또 다른 용도로 사용하기 위해서는, 동일한 메시지를 전달 받아야 합니다. 그래서, 이런 경우를 위해서 Pub/Sub 개념이 적용되었습니다. 메시지는 카프카에 영속성을 갖습니다. 각각의 컨슈머 그룹에서는, 데이터를 가져올 때 어디까지 가져왔는지 기록하기 위해서 각각의 오프셋을 저장합니다. 위 그림에서는, 컨슈머 그룹A 에서는 주문3까지 가져왔고, 컨슈머 그룹B에서는 주문4까지 가져간 것을 확인할 수 있습니다. 사실, 파티션 개념도 있고, 위와 같이 간단하지는 않습니다.
아무튼, 단순히 메시지를 전달하는 역할의 메시지 브로커의 용도 외에, 카프카는 매우 다양한 용도로 사용하는 상황입니다. 사실 필자는 자세히는 잘 모릅니다. 아래와 같은 용도로 사용할 수 있습니다.
- 데이터 허브
- 로그 수집
- 웹 활동 분석
- 사물인터넷
- 이벤트 소싱
아래 책에서 좀 더 자세히 참고할 수 있습니다.
http://www.yes24.com/Product/Goods/89233078
개인적인 생각이지만, 책에서 글로 이해하는 것보다는, 직접 서비스에서 경험해보는게 제일 좋을 것 같기는 합니다. 메시지 패턴은 글로 배울 수 있는 간단한 이론이 절대 아닙니다...
이번 글에서는, MQ, Pub/Sub 에 대해서 아주 간단하게 정리해봤습니다. 다음 글에서는, 해당 기술을 스프링부트에서 어떻게 사용하는지에 대해서 공부합니다.