// 에드센스


1. 느슨한 결합을 위한 노력

 

예시 정책.

1. 회원 도메인과 가족계정 도메인이 있다.

2. 회원의 인증정보가 초기화되면 가족계정에서 탈퇴된다.

 

 

 

1단계. 모놀리틱한 구조(그저 동기적인 코드로 구현)

회원의 인증이 초기화되면 -> 가족계정 서비스에서 탈퇴된다.

 

행위의 주체는 회원이다.

즉, 회원 본인인증 해제 로직과 가족계정 탈퇴 로직이 강한 결합을 이루고 있다.

 

 

 

 

 

2단계. 시스템의 물리적인 분리

유튜브 영상을 캡쳐해 온 것이라 발표자님 얼굴이 나온다ㅠ 출처는 밑에 정리해 두었다.

물리적으로는 분리되었지만 코드레벨의 호출이 HTTP 요청에 의한 호출로 변경되었을 뿐 여전히 강한 결합이다.

 

 

 

 

 

3. 비동기 HTTP 요청

비동기로 구현된 HTTP 요청은 쓰레드레벨의 의존을 제거할 뿐 여전히 강한 결합이다.

 

 

 

 

 

4. 메시지 큐를 사용한 요청

 

 

회원 인증 초기화시 가족계정 탈퇴를 요구하는 메시지를 발행함으로 물리적인 결합은 제거되었지만 논리적으로는 강하게 결합된 상태다.

회원인증을 담당하는 도메인에서 가족계정을 담당하는 도메인에서 해야할 작업을 명시하고 있으니까 말이다.

 

주의해야할 것.

요청을 위해 발행한 메시지가 대상 도메인에게 바라는 행위를 담은 메시지라면 그저 비동기 구조일 뿐,

우리가 다루고자 하는 이벤트가 아니다.

 

.

.

.

.

.

.

.

 

5. 드디어 느슨한 결합

메시지큐에 똑같이 메시지를 발행하였지만 4단계와 다른점이 있다면

회원 도메인에서 발행한 메시지는 가족계정의 탈퇴를 지시하는 메시지가 아니라 그저 "회원인증 초기화"를 알리는 메시지라는 점이다.

 

 

 

출처:

https://youtu.be/b65zIH7sDug

캡쳐본에서는 얼굴 가렸는데 썸네일에 대문짝만하게 나오네ㅋㅋ

 

 

 

 

 


2. 이벤트란?

여기서 말하는 이벤트는 다음과 같다.

밥을 먹고 설거지를 하는 서비스라면

밥을 먹고나서 "설거지를 하라"는 이벤트를 발행하는 것이 아닌

"밥을 먹었다"라는 이벤트를 발행하는 것이다.

 

 

다른 예시로,

 

 

경찰관이 자동차에게 "직진하라"는 이벤트를 발행한다면,

 

 

 

 

 

이렇게 차들이 많아지면 이벤트를 발행하는 경찰관에게는 부하가 걸리게 된다. 차 하나하나마다 발행을 해줘야하기 때문

 

 

 

 

하지만, 신호등은 우리에게 이벤트를 발행하지 않는다. 정확히 말하면 경찰관처럼 우리에게 직진하라고 지시하지 않는다.

 

그저 지금이 빨간불인지 파란불인지만 알려줄 뿐 자동차가 신호등을 관찰하다가 판단한다.

 

 

 

 

따라서 신호등은 자신의 상태만 메시지로 발행하고 후속 행동은 이를 관찰하는 자동차들이 판단하여 처리한다.

한마디로 줄여서

 

"주목할 만한 상태를 이벤트로 발행한다"

 

고 보면 될 것같다.

 

 

 

AWS에서 발생하는 거의 모든 동작은 이벤트를 발생시킨다. 다음은 EC2의 생명주기 변경 이벤트이다.

 

 

 

출처:

https://youtu.be/4aoKv545ViA

 

 

 

 

 


3. 이벤트 드리븐 아키텍쳐의 이모저모

 

이벤트 드리븐 아키텍쳐의 제약사항

  • 이벤트는 발생한 사건에 대해 약속된 형식 사용
  • 불변 : 과거의 메시지를 변경할 수 없음
  • 발생한 사건에 대한 결과 상태 전달
  • 생성자는 이벤트를 누가 처리했는지 이벤트 처리 상태에 관여하지 않음
  • 소비자는 이벤트를 누가 생성했는지, 생성자에 관심 갖지 않음

 

 

이벤트 드리븐 아키텍쳐 설계시 고려사항

Loosely coupling
  • 한 모듈이 따른 모듈에 너무 종속적이면 안된다.
  • 고려사항이라고는 하지만 비동기적인 프로그램을 하기 위해선 사실상 필수 사항

 

Removing dependencies 
  • 의존성을 제거
  • 의존성을 제거 해서 Loosely한 coupling 상태로 만들어야 함

 

Nonblocking transaction
  • 트랜잭션 단계에서 서로 영향을 주지 않도록 해야함.
  • 회원가입을 할때, 핸드폰 인증을 하고 이메일 인증을 순차적으로 진행을 하는데 이메일 인증에서 오류가 났다고 해서 핸드폰 인증까지 같이 취소가 되면 안됨

 

Asynchronized callback
  • 비동기 콜백은 필수는 아님
  • 그러나 사용의 편리성으로 가장 많이 사용

 

Fallback /Retry
  • 실패 했을 경우 어떻게 하고 어디까지 돌아가서 재시도 할것인가를 구분 지을 필요 있음
  • 딱히 정해진 방식이 없어서 설계자의 능력이 중요

 

Event logging
  • 비동기는 로그가 중구난방으로 찍히기 때문에, 이벤트 로그가 잘 찍히게 설계해야함.

 

 

 

 

이벤트 드리븐 아키텍쳐의 장단점

특징과 장점
  • Sevice간 호출이 많은 Microservice Architecture에 적합.
  • Infra Structure의 유연한 사용
  • Polyglot 구성에 용이 -> 모듈별로 서로 다른 언어와 DB 사용 가능
  • 가용성, 응답성 ↑ -> 효율성, 성능 ↑ -> 요청이 많아질 수록 성능 향상 기대
  • Fault Isolation : 에러가 생겨도 각각의 모듈에 독립적으로 생기기 때문에 다른 모듈은 정상 가동이 가능

 

 

단점
  • 설계 복잡도, 운영 복잡도 증가
  • Message Broker 의존성 증가 ->Fault Isolation의 장점이 있어도 모든 Message가 모이는 Message Broker 자체가 고장나면 시스템이 멈춰버림
  • 코드 가독성 하락 -> 눈에 익은 코드가 아니므로 코드 읽기가 어려움
  • 디버깅 난이도 상승 -> 스텝투스텝으로 동기적으로 따라갈 수가 없고, 중구난방으로 로그가 찍히기 때문에 어디서 고장이 났는지 파악하기 힘듬
  • log를 통한 system flow 가독성 하락

 

 

출처: https://be-developer.tistory.com/37

 

[빗썸테크아카데미] 첫번째 - OT, 동기/비동기와 Event-driven Architecture

[빗썸테크아카데미] 첫번째 - OT, 동기/비동기와 Event-driven Architecture https://be-developer.tistory.com/34 [빗썸 테크 아카데미] BE 심화 과정 최종 합격 후기 >< (+기술과제) [빗썸 테크 아카데미] BE 심화 과정

be-developer.tistory.com

 

혹시 제가 사용한 방법보다 더 간단하고 좋은 방법이 있다면 훈수 부탁드립니다.


1. 마주한 문제

클레이튼 기반의 dapp을 개발하며 블록체인의 트랜잭션과 서비스의 데이터베이스간의 싱크를 보장해야하는 상황을 마주했다.

 

 

성공한 트랜잭션이면 관련 정보를 데이터베이스에 반영해주고,

실패한 트랜잭션이면 관련 정보를 데이터베이스에 반영해주지 않는, 그런 요구사항이었다.

 

 

클레이튼 블록체인에서 자체적으로 실패한 트랜잭션에 대한 롤백은 시켜주기에 클레이튼의 성공여부를 먼저 확인하고 데이터베이스 반영 여부를 정해야했다. [클레이튼 트랜잭션] -> [데이터베이스 트랜잭션]의 플로우였기에 이 사이에 트랜잭션의 성공여부만 확인하고 데이터베이스의 트랜잭션을 발생시킬지 말지 결정하면 됐다. 

 

[클레이튼 트랜잭션] -> [트랜잭션이 성공했는지 확인] -> [데이터베이스 트랜잭션]

 

하지만 블록체인은 트랜잭션이 확정되기까지 걸리는 시간인 Finality라는 개념이 있기에 트랜잭션의 성공 여부를 조회하려면 이  Finality 시간 이후에 해야했다. 클레이튼의 경우 이 시간이 평균 1초라고 알려져있다. 평균이기에 실제 상황에서 약간의 오차는 있을 것 이다. 따라서 Finality만큼의 딜레이를 하고 트랜잭션 조회 -> 데이터베이스 트랜잭션 실행의 절차를 밟아야 했다. 

 

 

즉, 필요한 조건은

 

1. 트랜잭션 조회에 delay를 줘야한다. -> Finality 때문에

2. 적어도 2회 이상 retry를 하며 조회해야한다. -> Finality 시간이 일정하지 않기 때문에

3. 최대한 빠르게 트랜잭션의 성공 여부를 판단해야한다. -> 원활한 서비스 제공을 위해

 

 

 

 


2. 고려했던 선택지

세가지를 고려했었다. 참고로 NodeJS로 개발했다.

 

1. 스케줄러를 통해 짧은 간격으로 확정되지 않은 트랜잭션들의 성공 여부를 조회하기

  1. 트랜잭션을 발생시킬 때 transaction_history 테이블에 트랜잭션의 발생 여부를 기록해두고 is_confirmed는 false로 둔다.
  2. NodeJS의 스케줄러를 통해서 is_confirmed가 false인 기록들을 짧은 간격으로 주기적으로 조회하며 트랜잭션의 성공 여부를 확인한다.(트랜잭션 성공여부는 caverExtKAS 라이브러리의 getTransferHistoryByTxHash 함수를 사용했다)

 

문제점.

트랜잭션이 발생하지 않을때도 몇 초마다 항상 실행되기에 리소스 낭비라고 생각됐다.

 

 

 

2. 클레이튼 이벤트 리스너를 통해 트랜잭션이 수행될 때 발생되는 이벤트를 추적하여 판별하기

  1. 트랜잭션을 발생시킬 때 transaction_history 테이블에 트랜잭션의 발생 여부를 기록해두고 is_confirmed는 false로 둔다.
  2. 트랜잭션이 성공하면 열어둔 소켓을 통해 클레이튼 이벤트를 Listen할 수 있는데 트랜잭션이 성공하면 이벤트가 수신되므로(아마..?) 수신했다는 것은 해당 트랜잭션이 성공했다는 의미.

 

문제점.

가끔 성공한 트랜잭션의 이벤트가 리슨되지 않을때가 있었다. (왜일까..)

 

 

 

3. RabbitMQ를 통해서 딜레이와 재시도 기능을 구현하여 조회하기 (이 방법을 사용했다)

  1. 트랜잭션을 발생시킬 때 transaction_history 테이블에 트랜잭션의 발생 여부를 기록해두고 is_confirmed는 false로 둔다. 또 동시에 RabbitMQ로 발생한 트랜잭션의 정보를 publish한다.
  2. 컨슈머에서 이를 구독하고 이 시점에 트랜잭션 성공 여부를 조회한다.
  3. 트랜잭션이 성공이면 데이터베이스에도 관련 정보를 반영 후 ack으로 마무리하고, 실패면 retry_count를 1추가해서 DLX로 보낸다. 이곳에서 5000ms동안 대기하고 다시 작업큐로 메시지를 보내서 반복한다.(넉넉히 5초로 딜레이를 주었다)
  4. 3번에서 반복 횟수동안 모두 트랜잭션이 확정이 안됐거나 실패 판정이 나면 데이터베이스에 관련 정보를 반영하지 않고 ack을 반환한다.

 

이 방법을 선택한 이유

  • RabbitMQ를 통해서 사용자가 몰릴때 비동기적으로 안정적인 로직 수행이 가능하기에
  • RabbitMQ의 Durability를 통해 중요한 정보를 잃어버리지 않을 수 있기에
  • RabbitMQ의 DLX 기능을 통해 딜레이와 재시도를 구현할 수 있기에

 

 

 

 


3. RabbitMQ 구성

RabbitMQ는 위와같이 구성해보았다.

 

[1], [2]

클레이튼 트랜잭션을 발생시킴과 동시에 트랜잭션의 정보와 retry_count를 담은 메시지를 publish한다.

 

[3], [4]

지정한 익스체인지에서 지정한 큐로 메시지를 전달한다.

 

[5]

트랜잭션의 성공 여부를 확인하는 컨슈머에서 트랜잭션의 성공 여부를 확인한다.

성공이라면 서비스 로직을 실행하고 ack을 반환한다.

 

[6]

트랜잭션이 아직 확정되지 않았거나 실패했다면 WAITING 익스체인지로 같은 메시지를 그대로 전달한다.

이때마다 retry_count에 1 씩 더해서 전달한다.

 

[7]

전달된 메시지는 TX.Q.WAIT_CONFIRM_CHECK 큐로 보내지는데 이 큐는 DLX와 TTL 설정이 돼있다.

 

[8]

지정한 TTL 시간이 지나면 메시지가 x-dead-letter-exchange로 지정된 익스체인지로 x-dead-letter-routing-key 바인딩 키를 사용해서 전달된다.

즉, 이 큐에서 5000ms 동안 대기하고 TX.E.DIRECT 익스체인지로 가게된다.

이때 바인딩키는 TX를 가지고간다.

TX는 TX.E.DIRECT와 TX.Q.CONFIRM_CHECK의 바인딩키다.

따라서 TX.E.DIRECT 익스체인지에 도착한 메시지는 TX.Q.CONFIRM_CHECK 큐로 다시 보내진다.

 

 

 

[4], [5], [6], [7], [8]의 과정을 최대 3번까지 반복하고도 트랜잭션이 실패라고 판별되면 최종적으로 실패처리(데이터베이스에 반영하지 않기)를 한 후 ack를 반환한다.

 

 

 

DLX의 개념과 동작방식은 아래 아저씨가 쉽게 설명해준다!

https://www.youtube.com/watch?v=ovE8NKAwqTI&ab_channel=MikeM%C3%B8llerNielsen 

 

 

 

 


4. 세부 구성

메시지는 

[1] -> [2]에서 5000ms 대기 -> [3] -> [4]의 순서로 움직인다.

 

 

 

 

 

후기.

뭔가 긴 고민끝에 찾은 방법이지만 결국은 기본적인 RabbitMQ의 활용법이었다!

RabbitMQ가 참 유용한 도구임을 새삼스래 느꼈던 경험이었다.

 

 

참고:

더보기


1. 왜 RabbitMQ를 사용하는가?

RabbitMQ는 *AMQP를 따르는 오픈소스 메세지 브로커이다.

RabbitMQ는 데이터를 잠시 보관하고 나중에 비동기적으로 처리하고 싶을 경우 사용하는 일종의 데이터 저장소이다.

 

*AMQP: Advanced Message Queing Protocol의 약자로, 흔히 알고 있는 MQ의 오픈소스에 기반한 표준 프로토콜

 

실생활에서 예를 들어보자

스타벅스에서 손님들이 줄을 서서 커피를 주문한다.

 

1. 메시지 큐X

첫번째 손님이 커피를 주문하면 첫번째 손님의 커피가 완성될때까지 두번째 손님은 계속 줄을 서서 대기해야한다.

 

2. 메시지 큐O

첫번째 손님은 커피를 주문하고 자리로 간다. 두번째, 세번째 손님들도 주문서만 던져놓고 자리로 간다. 바리스타는 쌓여가는 주문서들을 보며 순서대로 커피를 만든다. 커피가 만들어지면 손님들이 받아간다.

 

이때 손님(프로듀서)들이 바리스타(컨슈머)에게 던져놓는 주문서가 메시지가 되고 주문서가 쌓여가는 곳이 메시지 큐가 된다.

 

이런 구조는 커피를 비동기적으로 만들기에 효율적이며,

주문서는 바리스타에게 전달될때 까지 잠시 저장되기에 바리스타가 까먹거나 하는 주문 누락이 발생하지 않는다.

 

 

 

정리하자면

  • 메시지를 많은 사용자에게 전달해야할 때
  • 요청에 대한 처리시간이 길어 해당 요청을 다른 API에 위임하고 빠른 응답처리가 필요할 때
  • 애플리케이션 간 결합도를 낮춰야 할 때

RabbitMQ를 사용한다

 

 

 

 


2. RabbitMQ는 어떻게 이루어져있는가?

[프로듀서 → 브로커(익스체인지+큐) → 컨슈머]

의 구조로 메시지를 전달해주는 메시징 서버

RabbitMQ는 다음과 같이 구성된다.

  1. Producer: 메시지를 보내는 놈
  2. Exchange: 메시지를 알맞은 큐에 전달해주는 놈
  3. Queue: 메시지를 차곡차곡 쌓아두는 놈
  4. Consumer: 메시지를 받는 놈
 

 

위 그럼처럼 Producer는 Queue에 직접 메시지를 전달하는 것이 아니다.

[프로듀서 → 익스체인지 → 큐 → 컨슈머]의 절차를 밟는다.

Exchange에서 알맞은 Queue로 메시지를 분배한다.(Exchange들과 Queue들은 바인딩되어있다)

무슨 기준으로 분배하느냐?

Exchange Type에 따라 다르다.

 

Exchange Type 4가지

Direct

메시지에 포함된 Routing Key를 기반으로 특정 Queue에 메시지를 하나씩 전달한다.

 

 

Fanout

Routing Key에 상관 없이 연결돼있는 모든 Queue에 동일한 메시지를 전달한다.

라우팅키를 평가할 필요가 없기때문에 성능적인 이점이 있다.

 

Topic

라우팅키 전체가 일치하거나 일부 패턴과 일치하는 모든 Queue로 메시지가 전달된다.

Topic Exchange 에서 사용하는 binding key 는 점(.)으로 구분된 단어를 조합해서 정의한다.

* 와 #을 이용해 와일드 카드를 표현할 수 있으며, * 는 단어 하나 일치 # 는 0 또는 1개 이상의 단어 일치를 의미한다.

 

다음과 같이 binding key 를 정의한 경우에 메시지의 routing key 가 quick.orange.rabbit 또는 lazy.orange.elephant 이면, Q1, Q2 둘 다 전달된다. lazy.pink.rabbit 는 binding key 2개와 일치 하더라도 1번만 전달된다.

quick.brown.fox, quick.orange.male.rabbit 는 일치하는 binding key 가 없기 때문에 무시된다.

 

Header

메시지 속성 중 headers 테이블을 사용해 특정한 규칙의 라우팅을 처리한다.

  • x-match = any 일 경우 헤더 테이블 값 중 하나가 연결된 값 중 하나와 일치하면 메시지 전달
  • x-match = all 일 경우 모든 값이 일치해야 메시지를 전달한다.

 

 

추가적인 사용 패턴들은 다음 블로그에 잘 설명이 되어있더라.

https://hamait.tistory.com/402

 

RabbitMQ 사용패턴들

역주: 메세지큐에 대한 글을 적기 전에 왜 메세지큐냐? 를 먼저 생각해봐야한다. 메세지큐는 그냥 메세지를 전달해주는 서버인건데, 기술자체에 집중할 필요는 나중에 생각해보고 , 처음 생각해

hamait.tistory.com

 

 

 

 


3. Connection과 Channel

Connection

  • RabbitMQ에서 지원하는 모든 프로토콜은 TCP 기반이다.
  • 효율성을 위해 긴 연결을 가정한다. (프로토콜 작업당 새 연결이 열리지 않음.)
  • 하나의 클라이언트 연결은 단일 TCP 연결을 사용한다.
  • 클라이언트가 연결을 성공하려면, RabbitMQ 대상 노드는 특정 프로토콜에 대한 연결을 허용해야 한다.
  • 연결은 오래 지속되어야 하기 때문에 일반적으로 구독을 등록하고, 폴링 대신에 메시지를 전달하여 소비한다.
  • 연결이 더 이상 필요하지 않은 경우, 리소스 절약을 위해 연결을 닫아야 한다. 이를 수행하지 못하는 클라이언트는 리소스의 대상 노드를 고갈시킬 위험이 있다.
  • 운영 체제는 단일 프로세스가 동시에 열 수 있는 TCP 연결(소켓) 수에 대한 제한이 있다. QA 환경에서는 충분한 경우가 있지만, Production 환경에서는 더 높은 제한을 사용하도록 구성해야 할 수도 있다.

 

Channel

  • 단일 TCP 연결을 공유하는 논리적인 개념의 경량 연결로 다중화된다.
  • 클라이언트가 수행하는 모든 프로토콜 작업은 채널에서 발생한다.
  • 채널 안에 연결할 Queue를 선언할 수 있으며, 채널 하나당 하나의 Queue만 선언이 가능하다.
  • 채널은 Connection Context에만 존재하기 때문에 Connection이 닫히면, 연결된 모든 채널도 닫힌다.
  • 클라이언트에서 처리를 위해 멀티 프로세스/스레드를 사용한다면, 프로세스/스레드 별로 새 채널을 열고 공유하지 않는 것이 일반적이다.

 

아무튼.

Connection: Application과 RabbitMQ Broker사이의 물리적인 TCP 연결

Channel: connection내부에 정의된 가상의 연결. queue에서 데이터를 손볼 때 생기는 일종의 통로같은 개념

 

한개의 Connection에 여러개의 Channel,
한개의 Channel에 한개의 Queue

https://hyos-dev-log.tistory.com/8

 

RabbitMQ의 Connection과 Channel

사내에서 진행되는 서비스를 개편하게 되면서, RabbitMQ를 사용하게 되었습니다. RabbitMQ을 사용하고, 운영하면서 Connection과 Channel에 대한 개념이 잡히지 않아서 정리하게 되었습니다. Connection 일반

hyos-dev-log.tistory.com

 

 

 

 


4. 적용하기

NodeJS에 적용하기

1. 초기 설정

amqp.connect함수를 통해서 Rabbitmq에 연결한다.

export async function makeConnection() {
  const connection = await amqp.connect({
    protocol: config.rabbitmq.protocol,
    username: config.rabbitmq.username,
    password: config.rabbitmq.password,
    hostname: config.rabbitmq.hostname,
    port: config.rabbitmq.port,
    vhost: '/',
    heartbeat: 0,
  });
}

 

2.  NodeJS에서 메시지 Subscribe하기

const messageQueueConnectionString = {
  protocol: config.rabbitmq.protocol,
  username: config.rabbitmq.username,
  password: config.rabbitmq.password,
  hostname: config.rabbitmq.hostname,
  port: config.rabbitmq.port,
  vhost: '/',
  heartbeat: 0,
};


export async function listenForResults() {
  try {
    // connect to Rabbit MQ (1)
    const connection = await amqp.connect(messageQueueConnectionString);

    // create a channel and prefetch 1 message at a time (2)
    const myChannel1 = await connection.createChannel();
    await myChannel1.prefetch(1);

    const myChannel2 = await connection.createChannel();
    await myChannel2.prefetch(1);

    // start consuming messages (3)
    console.log('start consuming messages from web service');
    await myController.myConsume1({ connection, myChannel1 });
    await myController.myConsume2({ connection, myChannel2 });
  } catch (e) {
    Raven.captureException(e);
  }
}

(1) connection 획득

(2) connection으로부터 channel을 생성

(3) consume 실행

 

위 1, 2, 3번의 과정은 서버가 최초 실행될때 실행된다.

*Prefetch란, 큐의 메시지를 컨슈머의 메모리에 쌓아놓을 수 있는 최대 메시지의 양

 

https://minholee93.tistory.com/entry/RabbitMQ-Prefetch

 

[RabbitMQ] Prefetch

이번 글에서는 RabbitMQ의 Prefetch에 대해 알아보겠습니다. 1. Prefetch란? Queue의 메세지를 Consumer의 메모리에 쌓아놓을 수 있는 최대 메세지의 양 입니다. 예를 들어 Prefetch가 250일 경우, RabbitMQ는 250..

minholee93.tistory.com

 

 

export function myConsume1({ connection, myChannel1 }) {
  return new Promise((resolve, reject) => {
    myChannel1.consume('q1', async function (msg) { // queue 이름
      console.log('myConsume1');
      
      // (4)
      const msgBody = msg.content.toString();
      const data = JSON.parse(msgBody);

      // (5)
      // 비즈니스로직

      // acknowledge message as received
      await myChannel1.ack(msg);
    });

    // handle connection closed
    connection.on('close', err => {
      return reject(err);
    });

    // handle errors
    connection.on('error', err => {
      return reject(err);
    });
  });
}

 

(4) 메시지 파싱

(5) 원래 수행하고자 했던 로직 실행

ack를 전송하지 않고 소비자가 죽거나 (채널이 닫히거나 연결이 끊어 지거나 TCP 연결이 끊어지는 경우),

RabbitMQ는 메시지가 완전히 처리되지 않았 음을 인식하고 다시 대기한다.

즉 컨슈머(소비자)가 사망했을 경우를 대비

 

3.  NodeJS에서 메시지 Publish하기

export let channel1;

// (1)
export async function makeConnection() {
  const connection = await amqp.connect({
    protocol: config.rabbitmq.protocol,
    username: config.rabbitmq.username,
    password: config.rabbitmq.password,
    hostname: config.rabbitmq.hostname,
    port: config.rabbitmq.port,
    vhost: '/',
    heartbeat: 0,
  });
  
  // (2)
  channel1 = await connection.createConfirmChannel(); // createConfirmChannel는 ack과 nack을 확인한다
}

// (3)
export async function publishToChannel(channel, { exchangeName, routingKey, data }) {
  await channel.publish(exchangeName, routingKey, Buffer.from(JSON.stringify(data), 'utf-8'), { persistent: true });
}

(1) 서버 최초 실행시 makeConnection 메소드를 사용하여 연결한다

(2) channel당 하나의 queue만 생성이 가능하기에 필요한 큐만큼의 채널을 생성하고 전역변수로 export해준다

(3) channel의 publish메소드를 사용하는 함수를 만든다

(1), (2)의 과정은 서버가 최초 실행될때 실행된다.

 

RabbitMQ가 종료되거나 충돌하면 사용자가 알리지 않는 한 대기열과 메시지를 잃게된다. 메시지가 손실되지 않도록하려면 큐와 메시지에 durable과 persistent 설정을 주어야한다.

즉, ack과 달리 RabbitMQ가 사망할 경우를 대비

 

https://skarlsla.tistory.com/13

 

[RABBITMQ] persistent, durable, ack

1. 메시지 수신 자동 확인(ack , noAck) 작업을 수행하는 데 몇 초가 걸릴 수 있습니다. 소비자 중 한 명이 긴 작업을 시작하고 부분적으로 만 수행되어 사망하는 경우 어떻게되는지 궁금 할 수 있습

skarlsla.tistory.com

 

 

 

// (4)
const result = await rabbitmq.publishToChannel(rabbitmq.channel1, {
  exchangeName: 'ex1', // exchange name
  routingKey: 'p1', // bind pattern
  data: { uid: uid },
});

 

(4) 이후 메시지를 publish할 곳에서 export한 channel과 설정값을 publish함수의 인자로 넣어주며 사용한다.

 

 

 

Spring에서 사용하기

1. 초기설정 - application.yml

spring:
  rabbitmq:
    host: somethingsomethingaddress.mq.region.amazonaws.com
    port: 5671
    username: myname
    password: mypassword
    ssl:
      enabled: true
  listener:
    simple:
      acknowledge-mode: manual

 

2. Spring에서 메시지 Subscribe하기

@RabbitListener(queues = ["sample.queue"])
public fun receiveMessage (message : Message){
    System.out.println("수신받은 메시지는 $message");
}

 

 

조금 더 자세한 설정을 하려면 어노테이션에 옵션을 추가해주면 된다.

@RabbitListener(bindings = [QueueBinding(
    value = Queue(value = "q1"),
    exchange = Exchange(value = "ex1"),
)])
fun receiveMessage (message : Message){
    System.out.println("수신받은 메시지는 $message");
}

 

https://cheese10yun.github.io/spring-rabbitmq/

 

Rabbit MQ 기초 사용법 - Yun Blog | 기술 블로그

Rabbit MQ 기초 사용법 - Yun Blog | 기술 블로그

cheese10yun.github.io

https://syaku.tistory.com/?page=7 

 

개발자 샤쿠 @syaku

Full Stack Web Developer.

syaku.tistory.com

 

 

3. Spring에서 메시지 Publish하기

// (1)
val rabbitTemplate: RabbitTemplate            

// (2)
val dto = mqDto(uid, req.amount)
rabbitTemplate.convertAndSend("ex1", "p2", objectMapper.writeValueAsString(dto)); // (exchangeName, routingKey, value)

(1) 생성자 주입

(2) 보낼 메시지를 dto로 포장해서 objectMapper.writeValueAsString 로 직렬화해서 전송한다.

 

 

 

 


5. 그 외 읽어보면 좋을 RabbitMQ 관련 이모저모

1. RabbitMQ에서 최적을 성능을 뽑는 팁?

https://kamang-it.tistory.com/627

 

[AMQP][RabbitMQ]RabbitMQ그리고 Work Queue의 사용법 - (3)

참고 [AMQP][RabbitMQ]RabbitMQ를 사용하는 이유와 설치방법 - (1) [AMQP][RabbitMQ]RabbitMQ아주 기초적이게 사용하기 - Java(feat.Hello World!) - (2) https://www.rabbitmq.com/tutorials/tutorial-two-java...

kamang-it.tistory.com

 

 

2. Spring boot Exchange와 Queue 자동 생성 이해

스프링 부트가 구동될때 exchange와 queue 설정에 따라 자동으로 생성되지 않는 다.

자동 생성 시점은 메시지가 발행이 될때와 메시지를 구독하기 위해 RabbitMQ 서버에 연결될때 생성된다.

아래 설정은 자동 생성 여부를 설정하기 위한 옵션이고 기본 값은 true 이다.

spring.rabbitmq.dynamic=true

https://syaku.tistory.com/?page=7 

 

개발자 샤쿠 @syaku

Full Stack Web Developer.

syaku.tistory.com

 

 

3. kafka vs RabbitMQ

앞선 내용들을 통해 각각의 정의 및 프로세스에 대해 면밀히(?) 살펴봤다.
위 내용을 통해서도 kafkaRabbitMQ의 차이에 대해 어느정도 이해할 수 있겠지만 본 글의 주제가 주제인만큼 다시 한번 간단히 정리해보도록 하겠다.

  1. kafka는 pub/sub 방식 / RabbitMQ는 메시지 브로커 방식
    kafka의 pub/sub방식은 생산자 중심적인 설계로 구성. 생성자가 원하는 각 메시지를 게시할 수 있도록 하는 메시지 배포 패턴으로 진행
    RabbitMQ의메시지브로커방식은 브로커 중심적인 설계로 구성. 지정된 수신인에게 메시지를 확인, 라우팅, 저장 및 배달하는 역할을 수행하며 보장되는 메시지 전달에 초점
  2. 전달된 메시지에 대한 휘발성
    RabbitMQ는 queue에 저장되어 있던 메시지에 대해 Event Consumer가져가게 되면 queue에서 해당 메시지를 삭제한다.
    하지만, kafka는 생성자로부터 메시지가 들어오면 해당 메시지를 topic으로 분류하고 이를 event streamer에 저장한다. 그 후, 수신인이 특정 topic에 대한 메시지를 가져가더라도 event streamer는 해당 topic을 계속 유지하기 때문에 특정 상황이 발생하더라도 재생이 가능하다.
  3. 용도의 차이
    kafka는 클러스터를 통해 병렬처리가 주요 차별점인 만큼 방대한 양의 데이터를 처리할 때, 장점이 부각된다.
    RabbitMQ는 데이터 처리보단 Manage UI를 제공하는 만큼 관리적인 측면이나, 다양한 기능 구현을 위한 서비스를 구축할 때, 장점이 부각된다.

 

 

https://velog.io/@cho876/%EC%B9%B4%ED%94%84%EC%B9%B4kafka-vs-RabbitMQ

 

카프카(kafka) vs RabbitMQ

오늘은 kafka와 RabbitMQ의 차이에 대해 알아보도록 하겠다. 1. 이해 1-1. 메시지 큐(MessageQuque : MQ) kafka와 rabbitMQ를 이해하기 위해선 우선 메시지 큐에 대한 이해가 선제적으로 필요하다. >메시지 큐(Mes

velog.io

  • 둘다 쩌는 솔루션이다.  -  RabbitMQ 가 좀더 성숙하다. (Written 12 Sep, 2012)
  • 철학은 좀 다른데, 기본적으로 RabbitMQ 는 브로커 중심적이며, 생산자와 소비자간의 보장되는 메세지 전달에 촛점을 맞추었다.  
  • 반면 Kafka 는 생산자 중심적이며, 엄청난 이벤트 데이터을 파티셔닝하는데 기반을 둔다. 배치 소비자를 지원하며, 온라인, 오프라인에 저 지연율(Low latency)을 보장하며 메세지를 전달해준다. 
  • RabbitMQ 는 브로커상에서 전달 상태를 확인하기위한 메세지 표식을 사용한다. 카프카는 그런 메세지 표식이 없으며 컨슈머가 전달(배달) 상태를 기억하는것을 기대한다.
  • 둘다 클러스터간의 상태를 관리하기위해 Zookeeper 를 사용한다. 
  •  RabbitMQ 는 커다란 크기의 데이터를 위해 디자인되지 않았으며 만약 컨슈머가 매우 느리다면 실패할것이다.그러나 post 2.0 에  RabbitMQ  는 느린 배치 컨슈머를 핸들링 되는게 요청되어졌다. 
  • Kafka 은 오직 토픽같은 exchanges 를 사용한다. RabbitMQ 는 다양한 exchanges 를 사용한다.
  • Kafka 는 파티션들 안에서 메세지 순서를 제공하며, 파티션들간에 엄격한 순서를 가진다. 카프카 컨슈머들은 충분히 스마트해야하며 , 그들 스스로 파티션간의 순서를 해결(resolve) 해야한다.
  • Kafka 는 디스크상에서 메세지를 저장하고 데이타 손실을 막기위해 클러스터로 그들을 복제한다. 각각의 성능에 큰 문제없이 브로커는 테라바이트를 핸들링할수있다. Kafka 는 쓰기에 초당  200k 메세지를 , 읽는데는 3M 메세지를  제공되도록 테스트되었다.

https://hamait.tistory.com/403?category=138704 

 

카프카(Kafka) vs RabbitMQ

생산자 (Sender) 테스트 결과 이유 카프카 생산자는 브로커로 부터의 ack 를  기다리지 않고 메세지를 보낸다.브로커가 핸들링 할수있는 만큼 빠르게 메세지를 마구 보낸다. 카프카는 좀더 효율

hamait.tistory.com

 

 

 

 

 

참고:

더보기

https://sg-choi.tistory.com/406

 

[RabbitMQ] RabbitMQ란?

RabbitMQ란? RabbitMQ는 AMQP를 따르는 오픈소스 메세지 브로커 AMQP는 클라이언트가 메시지 미들웨어 브로커와 통신할 수 있게 해주는 메세징 프로토콜 주요 개념으로 Producer, Exchange, Binding, Queue, Consu..

sg-choi.tistory.com

https://jin2rang.tistory.com/entry/RabbitMQ%EB%9E%80

 

RabbitMQ란?

RabbitMQ란? 서버간 메세지를 전달해주는 오픈소스 메세지 브로커이다. A → B에게 또는 A → B,C,D,E,F ... 등 메세지를 보내려고 할 때 RabbitMQ가 이 메세지를 받아서 전달 해주는 것으로 이해하면 된다

jin2rang.tistory.com

https://velog.io/@cckn/%EB%B2%88%EC%97%AD-RabbitMQ-%EB%B0%8F-Node.js%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4

 

(번역) RabbitMQ 및 Node.js를 사용한 비동기 마이크로 서비스

메세지와 이벤트를 통한 확장성과 내결함성.이 글은 Asynchronous Microservices with RabbitMQ and Node.js를 번역한 글입니다

velog.io

https://daily-coding-diary.tistory.com/13

 

[Node.js] RabbitMQ

목표 : RabbitMQ를 이용하여, 데이터를 임시로 저장하고 확인해봅시다. RabbitMQ는 디비 또는 데이터 처리에 문제가 생겼을때 데이터를 임시로 처리해줄때 많이 사용합니다. 예를 들면, 채팅방에 한

daily-coding-diary.tistory.com

https://www.joinc.co.kr/w/man/12/%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90/Message#sid_1

 

스타벅스로 살펴보는 Message 아키텍처

소프트웨어 시나리오

www.joinc.co.kr

 

무중단 배포에 대해 알아보자.


1. 무중단 배포란?

말 그대로 애플리케이션의 중단 없이 배포를 하는 것을 말한다. 

애플리케이션은 언제 중단될까?

 

 

v1 서비스가 실행 중일 때 v2 버전을 다운로드 받는다.
v1 서비스를 종료 시키는 시점부터 v2를 시작하기 전까지 애플리케이션은 중단된다.
이렇게 서비스가 중단되는 시간을 다운타임(downtime)이라고 한다.

 

이런 문제를 해결하기위해 무중단 배포라는 개념이 나왔고 크게 3가지 방법이 있다.

 

 

 

 

 


2. 무중단 배포 기법

1. Rolling 배포

롤링 배포는 사용 중인 인스턴스 내에서 새 버전을 점진적으로 교체하는 것으로 무중단 배포의 가장 기본적인 방식이다.

서비스 중인 인스턴스 하나를 로드밸런서에서 라우팅하지 않도록 한 뒤, 새 버전을 적용하여 다시 라우팅하도록 한다.

이를 반복하여 모든 인스턴스에 새 버전의 애플리케이션을 배포한다.

 

위 움짤을 보면

1. 4개의 기존 버전(v0.0.1)이 있다.

2. 그 중에 2개의 라우팅을 끊고 업데이트 버전(v0.0.2)로 업그레이드 한다.

3. 업데이트 버전이 배포가 되면 다시 라우팅을 시작한다.

4. 나머지 2개 인스턴스의 라우팅을 끊고 업데이트 버전을 배포한다.

5. 나머지 2개의 인스턴스의 업데이트가 끝나면 다시 라우팅을 연결한다.

 

장점

  • 인스턴스마다 차례로 배포를 진행하기에 상황에 따라 손쉽게 롤백이 가능하다.
  • 추가적인 인스턴스를 늘리지 않아도 된다.
  • 간편한 관리

단점

  • 새 버전을 배포할때 인스턴스의 수가 감소하기 때문에 사용중인 인스턴스에 트래픽이 몰릴 수 있다. 즉, 서비스 처리 용량을 고려해야 한다. 위 움짤에서 2번과정, 4번과정에서 4개의 인스턴스에서 감당하던 트래픽이 2개의 인스턴스로 몰리는 경우이다.
  • 배포가 진행될때 구버전과 신버전이 공존하기에 호환성 문제가 발생할 수 있다. 위 움짤에서 3번과정에서 업데이트 된 버전2개, 안된버전 2개가 공존하므로 사용자들은 균일한 서비스를 받지 못한다.

 

 

 

 

2. Blue Green 배포

블루는 구버전, 그린은 신버전을 의미한다.

운영중인 구버전과 동일하게 신버전의 인스턴스를 구성한 후 로드밸런서를 통해 모든 트래픽을 한번에 신버전 쪽으로 전환하는 방식이다.

 

장점.

  • 구버전의 인스턴스가 그대로 남아있어서 손쉬운 롤백이 가능하다.
  • 구버전의 환경을 다음 배포에 재사용할 수 있다.
  • 운영환경에 영향을 주지 않고 새 버전 테스트 가능.

 

단점.

  • 시스템 자원이 두배로 필요하다.
  • 새로운 환경에 대한 테스트가 전제되어야 한다.

 

 

 

 

3. 카나리 배포

옛날 광부들이 유독 가스에 민감한 카나리아 새를 이용해 가스 누출 위험을 감지했던 것에서 유래한 것으로 잠재적 문제 상황을 미리 발견하기 위한 방식이다.

신버전을 소수의 유저들에게만 배포를 해보고 문제가 없는것을 확인해가며 점차 많은 유저들에게 배포하는 기법이다.

블루그린 배포와 유사하지만 블루그린처럼 트래픽을 한번에 확 바꾸는 것이 아니라 단계적으로 전환하기에 부정적 영향을 최소화하고 상황에 따라 트래픽 양을 조절하며 롤백할 수 있다.

 

장점.

  • 문제 상황을 빠르게 감지
  • A/B 테스트로 활용 가능

 

단점.

  • 네트워크 트래픽 제어 부담.

 

*A/B 테스트란? 

대조군과 실험군으로 나누어서 특정한 UI나 알고리즘의 효과를 비교하는 방법론

 

 

 

 


3. 정리

1. Rolling 배포

  • 인스턴스를 늘리지 않고,
  • 하나씩 새로운 버전으로 늘리는 기법.
  • 사용중인 인스턴스에 트래픽이 몰리는 문제가 있다.
  • 버전간 호환성 문제가 생기는 순간이 있다.

 

 

 

2. Blue Green 배포

배포 전
배포 후

 

  • 구버전과 같은 환경으로 신버전을 미리 준비.
  • 로드밸런서의 라우팅을 한번에 확! 전환시킴.
  • 구버전의 환경을 재활용하거나 롤백하기 쉬움.
  • 단, 시스템 자원이 두배로 든다.

 

 

 

3. 카나리 배포

  • 소수만 사용하는 환경에 신버전을 배포하고 문제를 관찰한다.
  • 문제가 없으면 신버전으로의 트래픽을 단계적으로 늘린다.
  • 블루그린 배포와 유사. 하지만 블루그린은 한번에 전환, 이건 단계적으로 전환.
  • 문제를 빠르게 발견할 수 있다.

 

 

 

 

 

 

참고:

출처: https://kr.freepik.com/free-vector/web-hosting-server-rack-isometric-icon-of-database-and-data-center-blockchain-digital-technology_4102271.htm

인터넷을 돌아다니다 사용자가 증가하면 서버를 어떻게 확장시킬지에 관한 글이 올라와있는것을 확인했다.

그냥 읽고만 넘어가기엔 너무 정리가 잘 돼있기에 내용을 다시 훑어보며 포스팅으로 녹여보고자 한다.

 

출처는 맨 밑에 접은글에 표기하였다.


1. 대규모 트래픽이 됐다.

사용하던 서비스가 대박이 나서 늘어난 트래픽을 서버가 견딜 수 없어한다. 이때 어떻게 해야할까?

간단하게 두 가지 방법을 통해 우리의 서버를 견고히 할 수 있을것 이다.

  • 더 성능 좋은 서버로 업그레이드
  • 서버를 여러대 추가로 설치

성능 좋은 서버로 업그레이드하는 방법이 Scale Up이고,

서버의 개수를 늘리는 방법이 Scale Out이다.

 

 

Scale Up

서버의 CPU나 메모리 등을 추가하여 서버 자체의 성능을 향상시키는 방법.

 

장점

  • 서버의 장비만 추가 or 교체하면 되기에 구축과 설계가 간단.
  • 별도의 컨트롤러나 네트워크 인프라 등 비용이 발생하지 않는다.
  • 추가적인 별도의 서버가 생기는 것이 아니기에 데이터 정합성 문제가 없다.

 

단점

  • 부품 업그레이드로는 성능 향상의 한계가 있다.
  • 하나의 서버에서 모든 트래픽을 감당하기에 장애극복 기능이 떨어진다.
  • 매번 장비 업그레이드를 할 때 마다 교체 비용이 든다.

 

 

 

 

Scale Out

서버 자체의 성능 향상이 아닌 서버의 대수를 늘리는 방법.

 

장점

  • 확장에 유연하다.
  • 특정 서버에 트래픽이 몰리는 것을 분산하여 부하를 막을 수 있다.
  • 하나의 서버에 문제가 생겨도 다른 서버가 있으므로 가용성을 높일 수 있다.

 

단점

  • 설계와 구현이 복잡하고 이에 따른 관리 비용도 증가한다.
  • 데이터 불일치 문제가 발생한다.

 

이 데이터 불일치 문제가 오늘 해결할 핵심 문제다.

서버1에서 로그인 처리를 했다고 이걸 서버2에서 알고있을까?

Scale Out 방식에서는 데이터 정합성에 대한 문제를 고민해야한다.

 

 

 

 

 


2. 다중 서버 환경에서 Session 관리

Scale Out을 통해 서버를 늘리면 다음 그림과 같은 상태가 된다.

 

각 서버마다 세션 저장소를 각자 보유하고 있고 이를 서로 공유하지 않기에 데이터 정합성 문제가 발생한다.

 

 

개선1. Sticky Session

 

Sticky Session 방식은 사용자가 한번 사용했던 서버를 이후 요청시 계속 사용하는 것이다.

즉 로드밸런서에서 User1이 WAS1에서 세션을 생성했다는 사실을 알게되면 User1의 요청은 이후부터 WAS1로만 보내지는 것이다. 

User1은 동일한 서버만 사용하게 되기에 정합성 문제가 해결되긴 한다. 

 

하지만,

 

고정된 세션을 사용한다는 것은 특정 서버에 트래픽이 몰릴 수 있다는 것을 의미한다. Scale Out을 한 의미가 없어진다.

 

 

 

 

 

개선2. Session Clustering

한마디로 표현하면, 여러 서버들을 동기화 하는 것이다.

Tomcat 9.0 Document를 보면 톰캣이 세션 클러스터링을 구현하는 방법으로 DeltaManager를 사용하여 all-to-all 세션 복제 방식을 제안한다.

 

이렇게 한 서버의 상태를 다른 세션 저장소에서 복제하여 반영하기에 어떤 서버로 로그인 요청을 보내도 데이터 정합성 문제는 발생하지 않게된다.

 

하지만 이 방법도 문제가 있어보인다. 

 

세션 저장소에 데이터가 입력될 때 마다 모든 서버에 똑같이 반영해 줘야하기에 서버 수에 비례하여 네트워크 트래픽이 증가한다. 따라서 너무 많은 서버를 가진 상태에서는 오히려 비효율적이다.

 

 

Tomcat에서는 이를 또 개선하기 위해 BackupManager를 활용한 Primary Secondary 세션 복제 방식을 제시한다.

 

얘도 간단하게 말하자면, Primary 서버에서 다른 서버로 모든 정보를 복제하는 것이 아닌 Key만 복제하게 하고 이를 통해 세션 전체를 복제하는 것보다 메모리 사용면에서 개선을 한 것이다.

 

 

 

 


3. 출제자의 의도

그래서 어떤 방법을 쓰라는 것일까? 정답은 물론 없지만 사람들이 "흔히" 쓰는 방법은,

기존 서버들이 가지고 있던 로컬 세션 저장소를 사용하는 것이 아닌 제 3의 저장소를 사용하는 것이다.

 

이렇게 하면

  • Sticky Session처럼 트래픽이 몰리는 현상이 일어나지 않고,
  • 서버 하나에 장애가 발생해도 다른 서버로 대체 가능하여 가용성을 확보할 수 있고,
  • 제 3의 통일된 스토리지에 세션을 저장하기에 데이터 정합성을 해결할 수 있다.

 

 

이때 어떤 세션 스토리지가 적당할지 요구사항을 정리해보자면,

  • Read/Write가 빈번하게 이루어지기에 Disk 기반 데이터베이스는 느린 IO속도로 인해 부적합.
  • In-Memory 데이터베이스를 사용한다면 메모리에서 Read/Write를 하기에 속도면에서 적합.
  • 데이터베이스 전원이 끊기면 데이터가 사라지는 In-Memory 데이터베이스이지만 세션들이 사라지는 것은 다시 로그인만 하면 되기에 치명적이지 않다.

즉, In-Memory 데이터베이스를 제 3의 세션 스토리지로 활용하는 것이 좋다.

이에는 Redis와 Memcached가 있는데 Redis는 이후에 따로 포스팅 할 예정이므로 이정도만 알고 넘어가자.

 

 

 

4. JWT 연전연승?

JWT를 처음 공부할 때 이런 상황(서버가 다수 존재할 때)에서 유용하게 쓰인다고 들었는데 정말 그럴 것 같다.

 

 

 

 

 

 

참고:

더보기

+ Recent posts