[DataChannel 딥다이브] reliable과 partially reliable을 어떻게 이해할까
reliable이라는 단어는 이름만 보면 늘 좋은 선택처럼 들린다. 손실된 메시지를 다시 보내 준다니 안전해 보이고, 실시간 제품에서도 일단 reliable로 두면 문제가 줄 것 같다. 나도 처음에는 그렇게 이해했다. 그런데 DataChannel을 파일, 커서, 진행률, 제어 명령에 나눠 대입해 보니 질문이 달라졌다. 모든 데이터가 늦게라도 반드시 도착해야 할까. 숫자로 포장하기보다, 재현 조건과 운영에서 고정한 규칙을 중심으로 적었다.

reliable과 partially reliable을 다시 본 이유
Partially reliable은 drop 수가 생긴다. 이 숫자를 실패로만 보면 안 된다. 오래된 progress 이벤트를 버린 것은 정상 동작일 수 있다. 대신 버린 이유가 분명해야 한다. 시간이 지나서 버렸는지, 재전송 횟수를 넘었는지, 더 최신 version이 이미 반영되어 버렸는지 나눠야 한다. 그래야 데이터 손실과 의도된 폐기를 구분한다. 사용자 문구도 실패 이름을 따라 달라진다. 파일 chunk가 누락되어 재확인 중이라면 일부 조각을 다시 확인하는 중이라고 말할 수 있다.
실시간 이벤트가 오래되어 버려진 경우라면 굳이 사용자에게 알리지 않아도 된다. 제어 명령 확인이 늦으면 요청을 반영하는 중 같은 문구가 필요하다. 내부 정책이 사용자 문장으로 번역되지 않으면 운영할 때 결국 같은 문의가 반복된다. 그다음 좋은 네트워크와 나쁜 네트워크를 나란히 둔다. 좋은 네트워크에서는 reliable도 partially reliable도 차이가 작다.
회사망, 모바일 핫스팟, VPN, 오래 켜 둔 탭이 들어오면 차이가 보인다. 손실이 생겼을 때 reliable 큐가 얼마나 길어지는지, partially reliable 이벤트가 얼마나 버려지는지, 사용자가 보는 문구가 얼마나 자주 바뀌는지 확인해야 한다. Partially reliable 전송은 재전송 횟수나 시간을 제한한다. 일정 시간이 지나거나 정해 둔 재시도 횟수를 넘으면 메시지를 포기할 수 있다. 이 선택은 실시간 이벤트에 어울린다.
마우스 위치를 초당 30번 보내는데 그중 몇 개가 빠져도 최신 위치가 오면 화면은 자연스럽게 이어진다. 오히려 오래된 위치를 끝까지 전달하려고 줄을 세우면 사용자는 끊기는 느낌을 받는다.

겉으로 비슷한 말들
이 주제를 공부할 때는 거대한 아키텍처보다 작은 재현이 더 도움이 됐다. 같은 DataChannel 위에서 file chunk, control, cursor, progress 네 종류의 메시지를 상상한다. 네트워크 손실이 생겼을 때 각각 어떻게 움직여야 하는지 적어 본다. 파일 chunk는 재전송하거나 누락 구간을 요청한다. control은 도착 여부를 확인한다. cursor는 오래된 값을 버린다. progress는 단조 증가를 깨는 값을 무시한다. 신뢰성 정책을 나누고 나면 bufferedAmount와 backpressure를 봐야 한다.
메시지를 반드시 보내기로 했더라도, 보내는 쪽과 받는 쪽이 감당할 속도를 넘으면 전송은 불안정해진다.에서 이어지고에서 계속 정리한다. 반대로 진행률 이벤트는 성격이 다르다. 41%가 도착한 뒤 40%가 늦게 오면 화면에 반영하지 않는 편이 맞다. 커서 위치도 마찬가지다. 오래된 위치를 정확히 재생하려는 순간 실시간성이 깨진다. 이런 데이터는 최신 값, version, timestamp를 기준으로 판단한다.
도착하지 않은 값을 끝까지 기다리는 것이 아니라 다음 값으로 갱신한다. 여기서 조심할 점은 partially reliable을 손실 허용으로만 이해하지 않는 것이다. 손실되어도 되는 메시지가 아니라, 늦게 도착하면 의미가 약해지는 메시지에 쓰는 선택이다. 어떤 데이터는 100ms 안에 오면 가치가 있고 2초 뒤에는 없다. 어떤 데이터는 10초 뒤에 와도 반드시 필요하다.
이 차이를 먼저 적어야 신뢰성 정책을 고를 수 있다. 평균 지연만 보면 reliable 정책이 괜찮아 보일 수 있다. 하지만 실시간 제품은 꼬리 구간에서 사용자 불만이 생긴다. p95, p99 지연, 재전송 후 완료 시간, drop된 이벤트 비율을 같이 봐야 한다. reliable 메시지가 많아질수록 손실 순간에 큐가 길어지고, 그 큐가 현재 화면을 밀어낼 수 있다.

오해가 만든 잘못된 구현
PonsWarp 파일 전송을 떠올리면 파일 chunk는 가볍게 잃어버릴 수 없다. 한 조각이 빠지면 전체 결과가 깨진다. 하지만 진행률 UI나 순간 상태 이벤트는 최신성이 더 중요하다. 오래된 진행률을 끝까지 보내느라 현재 상태가 늦어지면 사용자는 답답함을 느낀다. 같은 DataChannel 계층을 쓰더라도 데이터 종류를 나눠야 하는 이유다. 나는 실패 이름을 reliable backlog, stale event dropped, required chunk missing, control ack timeout처럼 작게 나누려 한다.
개발자가 보는 로그는 영어 약어가 섞여도 괜찮지만, 사건의 경계는 분명해야 한다. 운영자는 사용자가 말한 느림이 재전송 대기인지, 오래된 이벤트 폐기인지, 무결성 실패인지 구분해야 한다. 무결성이 필요한 데이터에는 checksum이나 ACK를 둔다. DataChannel이 reliable하다고 해서 제품 수준의 완료 확인이 자동으로 생기는 것은 아니다. 파일을 받았는지, 저장까지 끝났는지, 사용자가 다시 열 수 있는지 확인해야 한다.
반대로 오래된 이벤트에는 drop 기준을 둔다. createdAt, sequence, version이 없으면 무엇이 오래됐는지 알 수 없다. 이 노트에서는 reliable과 partially reliable을 좋은 것과 나쁜 것으로 나누지 않는다. 대신 어떤 데이터가 보존 가치를 갖고, 어떤 데이터가 시간 가치를 갖는지 따져 본다. 제품에서 중요한 것은 옵션 이름보다 사용자가 기다리는 이유와 실패했을 때 복구할 수 있는 범위다. 실시간 시스템에서는 메시지마다 시간이 지나면서 가치가 달라진다.
파일 chunk나 결제 결과처럼 한 조각이라도 빠지면 안 되는 데이터가 있다. 반대로 2초 전 커서 위치, 지나간 preview 프레임, 오래된 진행률 이벤트처럼 늦게 도착하면 버리는 편이 나은 데이터도 있다. partially reliable은 대충 보내는 방식이 아니라, 시간이 지나 가치가 낮아진 메시지를 포기할 수 있게 해 주는 정책이다. 모든 이벤트를 reliable로 보내면 오래된 메시지가 줄을 만들 수 있다.
네트워크가 잠깐 흔들린 뒤 예전 위치와 진행률이 한꺼번에 도착하면 UI는 현재를 따라가지 못한다. 반대로 중요한 chunk를 partially reliable로 보내면 사용자는 끝까지 기다렸는데 파일을 열 수 없는 상황을 만난다. 둘 다 전송 실패라고 부르면 원인을 놓친다. DataChannel를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
신뢰도를 경계로 두기
실험 값도 미리 정한다. 메시지 type별 전송 수, 재전송 수, drop 수, 평균 지연, p95 지연, 사용자가 취소한 시점, 우회 경로로 넘어간 시점을 따로 본다. 파일이라면 checksum 실패와 재요청 횟수를 붙인다. 커서나 진행률이라면 stale drop 수와 최신 값 반영 지연을 본다. 이렇게 적어야 reliable과 partially reliable이 감이 아니라 관찰 가능한 선택이 된다. 그다음 로그와 화면 상태를 연결한다. 파일 chunk 재요청은 개발자 로그에는 offset과 checksum으로 남기고, 화면에는 일부 데이터를 다시 확인하는 중처럼 보여 준다.
progress drop은 사용자에게 보이지 않아도 로그에는 남긴다. control ack timeout은 사용자가 다시 누를 수 있는 문구로 이어져야 한다. 이 연결이 없으면 신뢰성 정책은 코드 안에만 갇힌다. 내가 새로 구현한다면 메시지 type을 먼저 고정하겠다. file.chunk, file.ack, control.pause, control.cancel, progress.update, cursor.move처럼 이름을 나누고 type마다 정책을 붙인다. reliable인지 partially reliable인지, 순서가 필요한지, 늦은 메시지를 버릴 수 있는지, 누락을 복구해야 하는지 같이 적는다.
나는 이 지점에서 늦게 도착했을 때 여전히 쓸모가 있는가라는 질문을 먼저 쓴다. 답이 예라면 reliable 쪽으로 기운다. 답이 아니라면 partially reliable 후보가 된다. 다만 중간값도 있다. 예를 들어 preview 썸네일은 누락되어도 다음 preview가 오면 괜찮지만, 사용자가 특정 시점의 결과를 저장하는 기능이라면 보존 가치가 생긴다. 데이터 이름만 보지 말고 제품 장면까지 봐야 한다.
전송 모드를 고르는 순서
Partially reliable을 고르면 지연 꼬리를 줄일 수 있다. 대신 애플리케이션은 빠진 메시지를 감당해야 한다. 파일 chunk에 잘못 쓰면 결과물이 깨진다. 제어 명령에 잘못 쓰면 상태가 꼬인다. 즉 partially reliable은 코드를 덜 짜는 방법이 아니라, 어떤 누락을 허용하고 어떤 누락은 허용하지 않을지 더 명확히 적는 방법이다. PonsLink에서는 방, 권한, 요청, 세션 상태처럼 중앙에서 일관성을 가져야 하는 데이터가 있었다.
이런 데이터는 reliable 쪽에 가깝고 확인도 필요하다. PonsWarp에서는 실제 파일 바이트가 직접 경로로 흐르더라도 중단 후 복구, backpressure, checksum 같은 제품 책임이 남는다. 신뢰성 옵션 하나로 해결되는 문제가 아니다. 제어 명령은 또 따로 봐야 한다. 취소, 일시정지, 완료, 권한 변경은 순서와 신뢰성이 모두 중요할 때가 많다. 사용자가 취소를 눌렀는데 그 명령이 오래된 이벤트 뒤에 밀리거나 포기되면 제품은 믿기 어렵다.
그래서 DataChannel 하나에서 모든 메시지를 처리하더라도 envelope에 type을 넣고, type별 보존 가치와 시간 가치를 분리해야 한다. realtime-network-21-r 파일 chunk는 거의 항상 보존 가치가 높다. 한 조각이 빠지면 전체 파일의 checksum이 맞지 않고 사용자는 결과물을 열 수 없다. 네트워크 계층의 reliable만 믿을지, 애플리케이션 ACK와 재요청을 더할지는 구현에 따라 달라지지만, 최소한 손실을 가볍게 봐서는 안 된다.
특히 큰 파일에서는 중간 실패 후 복구와 무결성 확인이 제품 품질을 좌우한다. 파일 전송에서는 완료율과 복구율이 더 중요하다. 순간 최고 속도보다 중단 후 이어받기, checksum 실패 후 재요청, 장시간 전송 안정성을 본다. 커서나 preview에서는 최신 값 반영 지연과 화면 끊김을 본다. 같은 DataChannel이어도 숫자의 의미가 다르다. '[DataChannel 딥다이브 reliable과 partially reliable을 어떻게 이해할까'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
이 기준을 표로 만든 뒤에야 DataChannel 옵션을 고른다. 모든 메시지를 reliable 하나로 묶으면 구현은 단순하지만 지연 꼬리가 길어진다. 모든 메시지를 partially reliable로 느슨하게 보내면 빠른 것처럼 보이다가 중요한 데이터가 깨진다. type별 정책을 두고, 최소한 envelope에서 처리 방식을 갈라야 한다. Reliable 전송은 손실된 메시지를 복구하려고 재전송한다. 애플리케이션은 메시지가 도착하리라는 기대를 둘 수 있다.
이 성질은 파일, 문서, 거래 기록, 세션 명령처럼 빠지면 안 되는 데이터에 잘 맞는다. 시간이 조금 더 걸리더라도 정확히 도착해야 하기 때문이다. 특히 DataChannel에서는 재현 조건을 같이 적어야 의미가 있었다. 이 기준은 팀 안에서 합의할 때도 쓸모가 있다. 누군가는 손실을 싫어하고, 누군가는 지연을 더 싫어한다. 둘 다 맞는 말일 수 있다. 그래서 메시지마다 늦은 도착의 가치와 누락의 비용을 적어 두면 논쟁이 덜 감정적이 된다.
파일 chunk는 누락 비용이 크고, cursor 이벤트는 늦은 도착의 비용이 크다. 이렇게 적어야 reliable이라는 이름이 주는 막연한 안심에서 벗어난다. 서비스에 반영하기 전에 나는 각 메시지 옆에 두 값을 적는다. 보존 가치가 높은가. 시간 가치가 빨리 떨어지는가. 파일 chunk는 보존 가치가 높고 시간 가치도 길다. 반드시 도착해야 한다. 진행률 이벤트는 보존 가치는 낮고 시간 가치는 짧다.
최신 값만 남기면 된다. 제어 명령은 보존 가치가 높고 순서도 중요하다. preview나 cursor는 시간 가치가 짧아 partially reliable 후보가 된다. 관련해서는 [과 [을 같이 보면 좋다. reliable과 partially reliable은 안전과 위험의 구분이 아니라, 데이터의 시간 가치와 보존 가치를 제품 언어로 옮기는 기준에 가깝다. Reliable을 고르면 손실을 복구할 수 있지만, 그만큼 지연과 버퍼가 쌓일 수 있다. 손실이 잦은 네트워크에서 오래된 메시지까지 모두 되살리려 하면 현재 상태가 뒤로 밀린다.
사용자 눈에는 멈춤, 끊김, 뒤늦은 화면 갱신으로 보인다. 이름은 reliable이지만 경험은 덜 reliable하게 느껴질 수도 있다.
체크할 문장은 길지 않다. 데이터별 보존 가치를 정했는가. 늦은 메시지의 가치를 판단했는가. 재전송 제한을 문서화했는가. 무결성이 필요한 데이터에 checksum이나 ACK를 두었는가. 오래된 이벤트를 버리는 기준이 있는가. 이 질문이 비어 있으면 reliable이라는 이름에 너무 많은 기대를 걸고 있을 가능성이 크다. Partially reliable은 drop 수가 생긴다. 이 숫자를 실패로만 보면 안 된다.
오래된 progress 이벤트를 버린 것은 정상 동작일 수 있다. 대신 버린 이유가 분명해야 한다. 시간이 지나서 버렸는지, 재전송 횟수를 넘었는지, 더 최신 version이 이미 반영되어 버렸는지 나눠야 한다. 그래야 데이터 손실과 의도된 폐기를 구분한다. 사용자 문구도 실패 이름을 따라 달라진다. 파일 chunk가 누락되어 재확인 중이라면 일부 조각을 다시 확인하는 중이라고 말할 수 있다.
실시간 이벤트가 오래되어 버려진 경우라면 굳이 사용자에게 알리지 않아도 된다. 제어 명령 확인이 늦으면 요청을 반영하는 중 같은 문구가 필요하다. 내부 정책이 사용자 문장으로 번역되지 않으면 운영할 때 결국 같은 문의가 반복된다. 그다음 좋은 네트워크와 나쁜 네트워크를 나란히 둔다. 좋은 네트워크에서는 reliable도 partially reliable도 차이가 작다.
회사망, 모바일 핫스팟, VPN, 오래 켜 둔 탭이 들어오면 차이가 보인다. 손실이 생겼을 때 reliable 큐가 얼마나 길어지는지, partially reliable 이벤트가 얼마나 버려지는지, 사용자가 보는 문구가 얼마나 자주 바뀌는지 확인해야 한다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.