Web3 Finance 조상욱

다중화 시스템하에서의 웹소켓 퍼블리싱 처리

우리가 일상적으로 사용하는 서비스들을 가만히 보면 거의 중단이 없이 지속적으로 사용할 수 있는 것을 보게 됩니다. 은행 서비스나 배달 서비스, 게임, 메신저 등 사전에 점검 공지가 있지 않는 한 24시간 365일 서비스가 이루어지고 있죠. 이를 위해 각 서비스는 우리에게 보이지 않는 그 뒤에 “다중화 시스템” 이라는 구성을 가지고 있으며, 서비스의 개발 역시 이에 대응되어 있습니다.

본문에서는 Xangle Datafeed에서 사용된 다중화 시스템에 대응하는 개발 기술에 대해 간략하게 소개하도록 하겠습니다.

다중화 시스템

개요

그 전에 다중화 시스템이 무엇인지에 대해 간단하게 알아봐야죠. 우선 본문에서 이야기 하는 다중화는 데이터 통신에서 이야기하는 Multiplexing 이 아닌 시스템적인 Redundancy를 이야기 합니다. 우선 이 redundancy의 사전적 의미를 보자면 “필요하거나 정상적인것보다 넘치는 어떤 것” 으로 정의되어 있습니다. 흔히 이야기 하는 이중화 시스템은 똑같은 일을 하는 시스템을 두 개를 설정하여 운영하는 것으로 비용이 (단순 계산으로) 2배가 들게 되는 “필요 이상의” 서비스 운영 방식입니다.

그럼 이게 왜 필요할까요?

개발이 완료된 서비스를 서버에 설치하여 작동을 시켰을 때 일반적으로는 큰 문제가 없을 것입니다. 하지만 서버라는 것은 외부 요인(네트워크 장애, 정전, 하드웨어 노후화 등)에 의해 쉽게 손상될 수 있습니다. 이를 하나의 서버로만 처리하게 될 경우 서비스가 지속적으로 공급되기 어렵죠. 이를 방지하기 위해 “여분의” 시스템을 두어 하나의 서버에 장애가 발생해도 여분 시스템을 통해 서비스를 지속적으로 이어나가기 위한 것입니다. 우리는 이러한 것을 외부에서 느끼진 못하지만 은행, 메신저, 게임 등 매우 친숙한 서비스들도 다중화 시스템을 가지고 있기 때문에 서비스를 계속 사용할 수 있는 것입니다.

종류

우선 다중화 시스템은 크게 세가지 형태로 나눌 수 있습니다.

고가용성 (High Availability)

서비스의 제공을 최소한의 다운타임을 가져가며 지속적으로 유지할 수 있도록 하는 것을 말합니다.

내결함성 (Fault Tolerance)

서비스에 오류가 발생했을 때 오류가 발생한 서비스만을 중지하고 다른 서비스는 유지될 수 있도록 하는 것을 말합니다. 이는 MSA에 부합되는 것이죠.

재해복구 (Disaster Recovery)

동일한 구성 또는 서비스의 작동을 위한 최소한의 구성을 물리적/지리적으로 분리된 다른 장소에 구성하여 서비스의 지속성을 유지하는 것을 말합니다. 하지만 매우 높은 비용을 요구하게 됩니다.

각각의 차이를 알아보자 (feat. 강남역사거리에 오픈한 24/365 피자집)

위의 내용을 간단한 예시를 통해 조금 더 와 닿을 수 있도록 해드릴게요.

강남역사거리에 오픈한 단 하루도 쉬지 않는 24/365 피자집을 예로 들어보겠습니다. 이 피자집에서 요리사를 한명만 고용하여 운영하면… 네. 식당주인 잡혀갑니다. 한 사람이 24시간 365일 쉬지 않고 요리를 하는것이 불가능하니 피자집 서비스가 불가능하게 되는거죠.

[그림 1] 요리사가 한 명만 있는 24/365 피자집 (출처 : freeCodeCamp) [그림 1] 요리사가 한 명만 있는 24/365 피자집 (출처 : freeCodeCamp)

그래서 이를 위해 요리사를 7명 더 고용합니다. 이로써 24/365 피자집은 법정 노동 근로시간을 준수하며 요리사에게 휴식시간도 보장하며 서비스를 지속할 수 있게 됩니다.

[그림 2] 요리사를 추가 고용한 24/365 피자집 (출처 : freeCodeCamp) [그림 2] 요리사를 추가 고용한 24/365 피자집 (출처 : freeCodeCamp)

만약 요리사 중 한 명이 와병, 경조사, 휴가 등으로 출근할 수 없는 경우 다른 대기중인 요리사를 불러 근무를 시킬 수 있죠. 이게 고가용성(HA) 입니다.

어느날 강남역사거리 일대에 정전이 나서 이 피자집에서 사용하던 전기오븐이 작동을 하지 않습니다. 요리사는 충분한데 서비스를 할 수 없어요. 정전이 해소될때까지는 서비스를 할 수 없습니다. 하지만 우리 24/365 피자집은 미리 소형 발전기와 UPS를 구비하여 정전이 되었지만 전기오븐의 작동이 중단되는 사고를 미연에 방지하였습니다. 전기라는 기능에 문제가 발생하였으나 전기오븐의 기능은 문제 없이 작동하고 있으므로 서비스에 큰 차질을 주지 않는 것, 이게 내결함성(FT) 이에요.

피자집과 잠시 멀어지지만 이러한 내결함성을 가장 확실하게 알 수 있는 것이 우리가 여행을 갈 때마다 자주 신세를 지는 비행기에서 찾아볼 수 있습니다. 비행기에는 양 쪽 날개에 엔진을 하나씩 총 두개 또는 4개를 장착하고 있습니다. 이 중 한 개의 엔진이 작동을 하지 않더라도 다른 엔진으로 비행기를 계속 날게할 수 있습니다.

[그림 3] 쌍발 엔진이 장착된 민항여객기 (출처 : freeCodeCamp) [그림 3] 쌍발 엔진이 장착된 민항여객기 (출처 : freeCodeCamp)

다시 피자집으로 돌아옵니다. 여름마다 강남역사거리는 워터파크가 열리죠. 올해도 어김없이 이 워터파크는 24/365 피자집을 덮칩니다. 매장도 주방도 전부 물바다가 되어 영업이 불가능하게 되었습니다. 하지만 이 피자집 사장님은 돈이 남아 도는지 역삼역 언덕 꼭대기에도 24/365 피자집 2호점을 만들어 운영하고 있습니다. 1호점이 자연재해로 서비스가 불가능 하지만 2호점 덕분에 서비스를 계속 이어갈 수 있습니다. 이 워터파크가 폐장을 하게 되면 1호점도 복구에 들어가겠죠. 이게 재해복구(DR) 에요. (최근에 모 회사가 이걸 제대로 안해서…)

웹소켓으로 퍼블리싱을 해보자.

이제 어느정도 다중화 시스템에 대한 기본적인 개념은 이해가 되셨으리라 믿습니다. 그럼 이를 바탕으로 웹소켓으로 시세정보를 퍼블리싱하는 서비스 프로그램의 주요 기능을 보면서 이야기를 진행해 보겠습니다.

기본 코드

자바 웹소켓 서버 코드는 워낙 많은 레퍼런스들이 있으니 상세한 내용은 생략하겠습니다. 간단하게 거래소의 시세정보를 받아와 레디스를 통해 프론트엔드의 웹소켓 서버로 실시간으로 전달해주는 모듈을 개발해 보죠.

우선은 거래소의 시세정보를 받아오는 코드입니다. 거래소에 지정된 자산에 대한 실시간 시세를 subscribe 하면 수신된 메시지를 doData 에서 처리해 퍼블리싱 합니다.

[코드 1] 시세정보 수신

public void receiveTicker() {
		WebSocketClient c = new ReactorNettyWebSocketClient();
		String url = "https://www.exchange.com/websocket";
		String reqData = "{'type':'ticker', 'symbol':'BTCKRW'}";
		c.execute(url, session -> {
				Mono<Void> init = session.send(Flux.just(session.textMessage(reqData)));
				Flux<WebSocketMessage> outStream = ...;
				Flux<String> data = session.receive()
						.map(WebSocketMessage::getPayloadAsText)
						.doOnNext(this::doData).then();
				Mono<Void> send = session.send(outStream);
				return Flux.merge(send, data, init).then();
			}
		);
	}

[코드 2] 시세정보 처리 후 데이터 퍼블리싱

public void doData(String message) {
	String returnMessage;
	// message 정제 및 처리 로직 시작
	...
	// message 정제 및 처리 로직 끝
	RedisTemplate.convertAndSend("웹소켓서비스", returnMessage);
}

다중화 시스템에 올려서 실행해보자.

우선 서버의 구성은 프론트엔드의 웹소켓 서버 1대, 데이터 Pub/Sub을 위한 레디스 서버 1대, 백엔드 서비스 서버 2대의 구성입니다. 데이터를 처리하는 서비스 서버 1대가 장애가 발생해도 다른 1대가 계속 작동하여 서비스가 문제 없이 작동할 수 있도록 하는 구성이죠.

[그림 4] 실시간 시세 웹소켓 서비스 구성도 [그림 4] 실시간 시세 웹소켓 서비스 구성도

서비스 서버에 개발된 프로그램을 올려 기동합니다. 이제 사용자는 실시간 시세를 제대로 받을 수 있을까요?

[그림 5] 기대를 저버리고 메세지가 두개 씩 날아온다. (확대해서 보세요.) [그림 5] 기대를 저버리고 메세지가 두개 씩 날아온다. (확대해서 보세요.)

네.. 여러분의 기대와는 달리 동일한 메세지가 두개씩 날아옵니다. 왜일까요?

다중 서버에서 처리를 하지만 메세지는 하나만 나가게 하고 싶어…

백엔드 각 서버에 올라간 서비스 프로그램은 독립적으로 실행되고 있습니다. 즉 각 서버에 설치된 서비스 프로그램은 서로가 무엇을 하는지 알 수가 없죠. 두 서버 모두 거래소로부터 A라는 시세 메세지를 받았고 각 서버가 똑같이 이 메세지를 처리해서 퍼블리싱을 했으니 같은 메세지를 두 번 수신할 수 밖에 없게되죠. 그럼 사용자가 중복된 메세지를 받지 않게 하려면 어떻게 해야할까요?

1. 서비스 서버 간 데이터 검증을 위한 통신을 한다.

두 서버가 서로 어떤 데이터를 처리하고 있는지 알 수 있다면 중복된 데이터가 퍼블리싱 되는 것을 막을 수 있습니다. 그럼 두 서버가 서로 데이터 검증을 위한 통신을 할 수 있도록 추가적인 개발을 진행하면 되겠습니다만… 여기서 서버가 하나 이상 더 추가되면 3대 이상의 데이터 검증 통신을 해야 합니다. 다자간 통신의 개발이 어려울 뿐 더러 통신 및 데이터 검증 과정에서 많은 시간을 소모해야 합니다. 사용자는 매우 늦게 실시간 데이터를 받게 되겠죠.

2. Redis 서버의 기능을 활용한다.

Redis 에는 SETNX 라는 명령어가 존재 합니다. 이 명령어가 하는 일은 입력하고자 하는 key-value 데이터의 key 가 Redis 내에 존재하지 않는 경우 데이터를 저장하며 결과로 true 를 반환합니다. 이미 key가 존재하는 경우에는 데이터를 저장하지 않고 false 를 반환합니다.

이를 활용하면 두 서버 중 먼저 데이터가 처리된 서버의 데이터는 Redis 에 저장이 되고 true를 수신하는 반면 처리가 늦어진 서버의 데이터는 저장이 되지 않고 false를 수신하기 때문에 true를 수신받은 서버만 Redis에 데이터를 퍼블리싱하면 데이터가 중복으로 나가지 않게 됩니다.

서버가 3대 이상 이어도 추가적인 개발이나 통신 설정 없이 중복된 데이터가 나가지 않게 할 수 있죠.

수정 코드

그러면 Redis의 SETNX를 사용하여 중복된 데이터가 퍼블리싱 되지 않게 처리하는 로직을 추가해 봅시다. 동일한 내용의 메세지는 hash함수의 결과가 동일하기 때문에 비교적 가벼운 hash 함수를 사용하여 생성된 hash값을 키로 하는 key-value 쌍을 데이터 검증으로 사용합니다. 검증에 사용된 데이터는 검증이 완료되면 필요가 없어지므로 일정 시간 이후에 삭제가 되도록 합니다.

[그림 6] 수정된 서비스 서버 작동 흐름 [그림 6] 수정된 서비스 서버 작동 흐름

hash 함수는 레퍼런스가 많이 있으므로 본문에서는 생략합니다.

[코드 3] 해쉬값을 키로하여 데이터 중복을 체크한 후 데이터를 퍼블리싱하도록 수정한 코드

public void doData(String message) {
	String returnMessage;
	// message 정제 및 처리 로직 시작
	...
	// message 정제 및 처리 로직 끝
	String key = getHashData(returnMessage);  // 해쉬 함수를 구현하면 됩니다.
	boolean isNotPublished = RedisTemplate.opsForValue().setIfAbsent(key, returnMessage, 300L, TimeUnit.SECONDS);
	if (isNotPublished) {
		RedisTemplate.convertAndSend("웹소켓서비스", returnMessage);
	}
}

다시 서버에 올려서 실행해보자

이제 수정된 프로그램을 각 서버에 올려 실행해 봅시다. 이제 사용자는 정상적으로 실시간 시세를 수신받을 수 있을까요?

[그림 7] 이제 중복 없이 제대로 데이터를 수신할 수 있다. (확대해서 보세요) [그림 7] 이제 중복 없이 제대로 데이터를 수신할 수 있다. (확대해서 보세요)

이제 다중화 시스템에서도 데이터 중복 없이 정상적으로 데이터를 수신할 수 있게 되었습니다.

마치며

Xangle Datafeed는 모든 거래소의 실시간 시세/거래 데이터 및 히스토리컬 OHLCV (시가, 고가, 저가, 종가, 볼륨) 을 제공하고 있습니다. 히스토리컬 데이터는 대용량 데이터를 핸들링 하면서도 고 TPS를 감당해 낼 수 있도록 서비스하고 있으며, 실시간 시세/거래 데이터는 각 거래소의 파편화된 정보를 정제하여 일정한 포맷으로 수신하여 데이터를 볼 수 있도록 하면서 지연시간을 최소화하여 각 거래소에서 수신하는 속도와 동일하게 실시간 정보를 받을 수 있도록 하고 있습니다.

이와 더불어 무중단 서비스를 제공하기 위해 다중화 시스템을 구축하여 장애가 발생하더라도 서비스의 중단 없이 안정적인 서비스가 이루어 질 수 있도록 하고 있습니다.

대형 금융사, 게임 개발사 등에서나 볼 수 있는 안정화된 다중화 시스템 아키텍쳐 설계, 대용랑 데이터 처리와 고TPS라는 두마리 토끼를 잡은 개발 능력은 타 회사에서 쉽게 경험할 수 없으리라 봅니다.

Reference

https://www.freecodecamp.org/news/high-availability-fault-tolerance-and-disaster-recovery-explained/