[P2P 딥다이브] P2P는 서버가 없다는 뜻이 아니다
P2P를 처음 공부할 때 제일 먼저 버린 문장은 “서버가 없다”였다. 그 말은 설명을 쉽게 시작하게 해 주지만 실제 설계에는 거의 도움이 되지 않았다. 로그인은 누가 확인하는지, 방은 누가 만들고 닫는지, 링크 권한은 어디에 남는지, 결제나 감사 로그는 누가 책임지는지 묻기 시작하면 서버는 여전히 한가운데에 있다. 달라지는 것은 서버의 존재가 아니라 서버가 모든 바이트를 떠안느냐의 문제였다.

서버 없음이라는 말이 닳은 이유
*P2P 판단은 서버 비용 하나가 아니라 권한, 실패, 관측 가능성을 같이 봐야 한다. 마지막으로 남는 기준은 관찰 가능성이다. 직접 전송이든 서버 경유든 사용자가 겪은 시간을 나중에 설명할 수 있어야 한다. 어느 단계에서 기다렸고, 어떤 경로가 선택됐고, 왜 우회했는지 모르면 같은 문제가 반복된다. P2P의 실용성은 빠른 성공보다 느린 실패를 설명하는 능력에서 더 자주 드러난다.
특히 권한과 데이터 경로를 섞지 않는 것이 중요하다. 파일 바이트가 직접 이동한다고 해서 권한 판단까지 피어에게 맡겨도 된다는 뜻은 아니다. 방에 들어올 수 있는지, 링크가 만료됐는지, 누가 어떤 요청을 승인했는지는 중앙에서 일관성 있게 봐야 한다. 직접 경로는 이 판단이 끝난 뒤 바이트를 옮기는 방법일 뿐이다. 이 선을 지키면 보안 설명도 단순해진다.
개발 중에는 성공 로그보다 취소 로그를 더 자주 봐야 한다. 사용자가 어느 상태에서 창을 닫았는지, 몇 초를 기다린 뒤 나갔는지, relay 전환 안내를 본 뒤 계속 진행했는지 확인하면 문구와 fallback의 품질이 보인다. 기술적으로 connected가 되었더라도 사용자가 이미 떠났다면 제품은 실패한 것이다. P2P 공부가 제품 노트로 이어지려면 이런 지표까지 같이 적어야 한다.

처음에는 서버가 사라진 줄 알았다
그래서 P2P를 볼 때는 서버 유무보다 책임의 위치를 먼저 그린다. 제어면은 약속을 정하고 데이터면은 실제 payload를 옮긴다. 두 길이 같은 서버를 지나갈 수도 있고, signaling만 서버를 통과한 뒤 파일 조각은 피어 사이로 바로 흐를 수도 있다. 이 차이를 놓치면 “P2P인데 왜 서버가 필요하냐”는 질문에 계속 붙잡힌다. 반대로 이 선을 나누면 인증, 권한, 주소 교환, 전송, 실패 보고를 각각 어디에 둘지 차분히 따질 수 있다.
팀에서 용어를 맞출 때도 같은 기준을 쓴다. “서버리스 P2P” 같은 말은 피하고 “서버는 권한과 signaling을 맡고 파일 바이트는 가능한 경우 직접 전송한다”처럼 길어도 정확한 문장을 쓴다. 길어 보이지만 나중에 오해를 줄인다. 특히 고객 지원이나 운영 문서로 내려갈 말이라면 더 그렇다. 기술적으로 애매한 짧은 문장은 결국 장애 때 비용으로 돌아온다.
나중에 P2P 설계를 다시 펼칠 때 확인할 것은 많지 않다. 제어면과 데이터면을 따로 그렸는지, 직접 경로 실패 후 우회 경로가 있는지, 서버가 반드시 알아야 하는 정보와 몰라도 되는 정보를 구분했는지, 사용자가 실패 상태를 이해할 문구를 받는지 보면 된다. 이 네 가지가 정리되면 P2P는 “서버 없는 구조”라는 빈말에서 벗어난다. 서버와 브라우저가 각자 맡을 일을 나눠 갖는 설계 언어로 남는다.
이런 노트를 쓸 때 일부러 영어 약어를 남기는 곳과 풀어 쓰는 곳을 나눴다. API, WebRTC, ICE, NAT, TURN처럼 표준 용어는 그대로 둬야 검색과 디버깅이 쉽다. 대신 “이를 통해”, “본질적으로”, “강력한 구조” 같은 말은 최대한 걷어냈다. 그런 표현은 읽을 때는 그럴듯하지만 설계 판단에는 정보를 거의 주지 않는다. 기술 글에서는 멋진 결론보다 어떤 값을 보고 어떤 결정을 내릴지가 더 중요하다.
나중에 코드를 짤 때 이 노트가 쓸모 있으려면 각 문단이 테스트 질문으로 바뀔 수 있어야 한다. 예를 들어 “직접 경로 실패 후 우회한다”는 문장은 테스트에서는 UDP 차단 환경에서 relay나 cloud 경로로 넘어가는지 확인하는 일이 된다. “사용자가 기다리는 이유를 안다”는 문장은 화면 상태가 candidate 수집, relay 전환, 수신 지연을 구분하는지 보는 일이 된다. 공부 노트가 구현과 연결되는 지점은 바로 여기다.

바뀌지 않는 기준: 책임은 어디에 남는가
이렇게 적어 두면 다음 구현에서 같은 질문을 다시 꺼내기 쉽다. 용어보다 책임, 성공보다 실패, 구조보다 사용자가 보는 상태를 먼저 확인하는 습관이 남는다. PonsLink와 PonsWarp를 돌아볼 때도 같은 기준이 남는다. 방, 권한, 요청 상태는 서버가 알아야 했다. 반면 PonsWarp의 파일 바이트는 직접 경로가 열렸을 때 서버가 소유하지 않아도 되는 영역이었다. 다만 직접 연결 실패, backpressure, 중단 후 복구를 설명할 준비가 없으면 “서버를 거치지 않는다”는 장점은 곧 사용자 불안으로 바뀐다.
직접 연결, relay, cloud drop은 서로의 승패가 아니라 상황별 선택지로 봐야 한다. 실제 구현 순서로 옮기면 더 단순하다. 먼저 서버가 꼭 알아야 하는 상태를 정한다. 사용자, 방, 권한, 초대 만료, 전송 시작과 종료 같은 값이다. 그다음 서버가 몰라도 되는 payload를 고른다. 파일 본문이나 미디어 프레임처럼 무겁고 일시적인 데이터가 여기에 들어간다. 마지막으로 둘 사이를 잇는 이벤트를 정의한다.
누가 전송을 요청했고, 누가 수락했고, 직접 경로가 열렸고, 우회 경로로 바뀌었고, 전송이 끝났다는 사건이다. 이 순서로 보면 P2P가 들어갈 자리가 과장되지 않는다. 제품 문구는 이 측정값에서 나온다. “연결 중”만 계속 보여 주면 사용자는 기다려야 할지 나가야 할지 모른다. “직접 연결을 확인하고 있어요”, “우회 경로로 바꾸는 중이에요”, “상대 기기가 받는 속도에 맞추고 있어요”처럼 상태를 나누면 같은 지연도 덜 불안하다.
기술적으로 정확하면서도 사용자가 행동을 정할 수 있는 문장이 필요하다. P2P를 검토할 때는 서버 비용이 줄어드는 장면보다 실패했을 때 누가 설명을 맡는지를 먼저 본다. 직접 경로가 붙지 않으면 relay로 넘어갈지, 사용자가 기다려도 되는지, 권한과 감사 로그는 계속 중앙에서 보장되는지까지 이어서 적는다. 서버를 지웠다는 말보다 책임을 어디에 남겼는지가 제품 신뢰를 더 많이 좌우한다.
측정할 값도 미리 정해 둔다. 연결 시작 시각, 첫 candidate 도착 시각, selected candidate pair, relay 사용 여부, 첫 바이트 도착 시각, 전송 중 bufferedAmount 변화, 중단 후 재개 성공 여부를 따로 본다. 평균 연결 시간만 보면 꼬리 구간이 숨는다. p95 연결 시간과 relay 비율, 재시도 후 성공률을 같이 봐야 제품 약속을 정할 수 있다. P2P를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
권한과 데이터 경로를 나누기
P2P 제품에서 특히 조심할 부분은 성공 화면이 너무 빨리 나온다는 점이다. 같은 와이파이에 놓인 노트북 두 대로 테스트하면 직접 연결은 쉽게 성공한다. 이 성공은 쓸모 있지만 충분하지 않다. 회사망 한쪽, 모바일 핫스팟 한쪽, 오래 켜 둔 탭, 느린 디스크, 절전 모드에 들어간 브라우저를 떠올리면 실패 모양이 달라진다. 사용자는 그냥 “안 돼요”라고 말하지만 내부에서는 후보 수집 지연, relay 전환, 수신 버퍼 증가, 권한 만료가 각각 다른 사건으로 일어난다.
그래서 로그 이름도 작게 잡아야 한다. 연결 실패 하나로 덮으면 운영자가 볼 수 있는 것이 없다. 직접 후보 없음, relay 전환, 수신 backpressure, 상대 peer 이탈, 권한 만료 후 전송 시도처럼 나누면 화면 문구도 같이 좋아진다. 사용자는 ICE candidate라는 말을 몰라도 된다. 대신 지금 직접 연결을 시도하는 중인지, 우회 경로로 바뀌었는지, 상대 기기의 수신 속도를 기다리는지 정도는 알 수 있어야 한다.
마지막에는 기술 선택이 아니라 사용자가 겪는 기다림과 제한을 다시 확인한다. 덧붙여, P2P를 설계할 때 “서버가 모르면 더 안전하다”는 식의 말도 조심해야 한다. 서버가 파일 내용을 모르는 것은 개인정보와 비용 측면에서 장점이 될 수 있다. 그러나 서버가 아무 상태도 모르면 사용자가 문제를 겪었을 때 도와줄 방법도 줄어든다. 어느 사용자가 어떤 방에 있었고, 어떤 단계에서 연결이 멈췄고, 직접 경로와 relay 중 무엇을 택했는지 정도는 내용과 분리해서 남겨야 한다.
내용 비공개와 운영 가능성은 같이 설계해야 한다.
관측 가능성이 남긴 조건
사용자 경험에서는 “빠르면 직접, 안 되면 우회”라는 단순한 문장이 꽤 중요하다. 내부적으로는 STUN, TURN, ICE restart, DataChannel 상태가 복잡해도 사용자는 작업을 끝내고 싶다. 직접 연결이 열리면 빠르게 보내고, 막히면 우회 경로를 안내하고, 전송이 길어지면 중단 후 이어받을 수 있게 해 주는 것이 더 현실적인 약속이다. P2P를 순수성의 문제로 보면 이런 제품 판단이 늦어진다.
마지막으로 나는 P2P를 ‘탈중앙’이라는 큰 말로 시작하지 않으려 한다. 그 말은 철학적으로는 멋지지만 구현에서는 너무 넓다. 실제로 필요한 질문은 더 작다. 서버가 어떤 사실을 알아야 하는가. 브라우저가 어떤 바이트를 직접 옮겨도 되는가. 실패했을 때 어느 경로로 바꿀 것인가. 사용자는 그 변화를 어떤 문장으로 이해할 것인가. 이 질문에 답할 때 P2P는 비로소 쓸 만한 설계 도구가 된다.
문서에는 제한을 숨기지 않는 편이 좋다. “직접 연결이 가능할 때 더 빠르게 보냅니다”와 “일부 네트워크에서는 우회 경로를 사용할 수 있습니다”는 서로 충돌하지 않는다. 오히려 사용자가 나중에 속도 차이를 이해하게 해 준다. 기술 글도 마찬가지다. P2P의 장점만 쓰면 광고가 되고, 실패만 쓰면 겁주는 글이 된다. 어느 책임을 중앙에 남기고 어느 부담을 피어에게 옮기는지 말할 때 균형이 생긴다.
P2P를 적용할지 판단할 때 나는 네 가지 질문을 먼저 둔다. 데이터가 무겁고 일시적인가. 중앙 서버가 반드시 내용을 보관해야 하는가. 피어가 거짓 상태를 보내도 검증할 방법이 있는가. 직접 경로가 실패했을 때 사용자가 계속 진행할 다른 길이 있는가. 네 질문에 모두 좋은 답이 있으면 P2P는 꽤 매력적이다. 하나라도 흐리면 서버 경유나 하이브리드가 더 나을 수 있다.
이 구분은 비용 계산에도 영향을 준다. 두 사람이 5GB 파일 하나를 주고받을 때 서버 경유 방식은 서버가 업로드와 다운로드 양쪽 부담을 진다. 직접 전송이 성공하면 서버는 세션과 약속만 다루고 파일 바이트는 피어 사이로 흐른다. 하지만 여기서 “비용이 사라진다”고 쓰면 또 틀린 말이 된다. 비용은 피어의 업로드, 배터리, 브라우저 메모리, 실패 처리, relay 사용료로 옮겨 간다.
P2P는 비용 삭제가 아니라 비용 이동에 가깝다. '[P2P 딥다이브 P2P는 서버가 없다는 뜻이 아니다'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.