[P2P 프로토콜] libp2p는 라이브러리보다 경계 묶음에 가깝다
처음 libp2p를 봤을 때는 P2P를 한 번에 해결해 주는 큰 라이브러리처럼 읽었다. import하고 설정 몇 줄을 넣으면 peer가 생기고, 연결되고, 메시지가 흐를 것 같은 느낌이었다.

libp2p를 다시 본 이유
libp2p가 답을 하나로 고정하지 않는다는 점은 장점이다. TCP, QUIC, WebSocket, WebTransport 같은 transport가 다르고, Noise 같은 보안 계층과 mplex/yamux 같은 stream 경계가 따로 있다. 대신 어떤 모듈을 왜 쓰는지 적지 않으면 선택 비용이 제품 뒤쪽에서 터진다. PonsLink/PonsWarp에 libp2p 감각을 가져온다면 통째 도입보다 필요한 계층을 빌리는 쪽이 먼저다. 작은 제품은 모든 예외를 인프라로 덮을 수 없다. 대신 peer id나 pubsub처럼 한 경계를 좁게 관측하고 빨리 고칠 수 있다.
- peer id는 네트워크 주체를 식별하지만 결제 계정이나 앱 사용자와 같은 말이 아니다. - transport는 연결 가능성과 방화벽 통과 가능성을 바꾼다. - 보안 채널은 인증과 암호화를 담당하지만 제품 권한 정책을 대신하지 않는다. - multiplexing은 하나의 연결 위에 여러 stream을 얹지만 각 stream의 backpressure 정책은 따로 봐야 한다. - pubsub는 메시지를 퍼뜨리는 규칙이지 정확히 한 번 전달되는 업무 큐가 아니다.
transport도 제품 정책과 붙어 있다. 특정 환경에서 WebSocket만 안정적이라면 그것은 기술적으로 덜 멋진 선택이 아니라 그 환경에서 고칠 수 있는 경계를 고른 것이다. 반대로 QUIC이나 WebTransport를 보려면 브라우저 지원과 서버 endpoint, fallback 문구까지 같이 준비해야 한다.

겉으로 비슷한 말들
제품 문구도 스택 이름을 그대로 번역하지 않는 편이 낫다. “libp2p 연결 중”보다 “보안 채널을 여는 중”, “메시지 경로를 다시 잡는 중”, “상대 기기를 새 피어로 확인하는 중”처럼 책임이 드러나는 문장이 낫다. 쉬운 말로 바꾸는 것이 목적이 아니라, 사용자가 다음 행동을 고를 수 있게 하는 것이 목적이다. *전체 도입 전에 peer id, transport, security, muxing, pubsub 중 실제 제품이 빌릴 경계를 고르는 점검표. security와 pubsub는 특히 “라이브러리가 해 줄 것”이라는 기대를 경계해야 한다.
보안 채널이 열려도 사용자가 어느 방에 들어갈 수 있는지는 제품이 판단한다. pubsub가 메시지를 퍼뜨려도 중복 처리, 순서 차이, 늦은 도착을 화면에서 어떻게 흡수할지는 애플리케이션 책임이다. 이 표가 채워지면 기능 플래그도 다루기 쉬워진다. peer id 경로를 탄 사용자를 알고, transport별 실패율을 알고, pubsub 때문에 모드를 바꾼 이유를 알면 점진 배포와 롤백이 가능하다.
반대로 전체 스택만 켜면 실패 하나를 고치기 위해 스택 전체를 다시 읽어야 한다. 실패 문구도 계층별로 달라진다. peer id가 바뀐 경우에는 “새 기기로 확인합니다”가 맞고, transport가 막힌 경우에는 “현재 네트워크에서 다른 경로를 시도합니다”가 맞다. security handshake가 실패한 경우에는 권한 문제인지 암호화 협상 문제인지 내부 로그에서 먼저 갈라져야 한다.

오해가 만든 잘못된 구현
libp2p를 하나의 라이브러리 이름이 아니라 peer id, transport, security, muxing, pubsub 경계로 나눠 보기 위한 흐름 노트. transport는 단순한 교체 부품이 아니다. 연결 가능성, 방화벽 통과 가능성, 브라우저 지원, 장애 문구가 같이 바뀐다. WebSocket으로 붙을 때와 QUIC이나 WebTransport 경로를 볼 때 사용자가 겪는 대기와 실패는 같은 모양이 아니다. 경계를 빌린다는 말은 추상적으로 들리지만 구현에서는 꽤 구체적이다.
peer id를 브라우저 저장소에 둘지, 세션마다 새로 만들지, 계정 변경 때 폐기할지부터 결정해야 한다. 이 선택 하나가 재접속 경험, 권한 회수, 장애 문의의 언어를 바꾼다. 이런 결정을 먼저 적으면 libp2p 도입 여부를 나중으로 미뤄도 손해가 적다. 실제 라이브러리를 쓰지 않더라도 peer id, transport, stream, pubsub라는 경계 언어는 남는다. 나중에 스택을 바꿀 때도 제품이 지키려던 약속과 기술이 맡은 역할을 분리해서 비교할 수 있다.
transport 선택은 사용자 환경을 만난다. 사내망, 학교망, 모바일 테더링, 브라우저 버전에 따라 WebSocket은 되지만 다른 경로는 막힐 수 있다. 그래서 transport 실패는 라이브러리 로그가 아니라 사용자에게 보이는 네트워크 안내와 같이 설계해야 한다. 스택을 고르는 회의에서도 같은 표를 쓴다. “libp2p를 쓸까?”보다 “peer id를 어떻게 보관할까?”, “pubsub 중복을 어디서 제거할까?”, “transport fallback을 사용자에게 어떻게 말할까?”가 먼저다.
이 질문에 답이 없으면 라이브러리 선택은 너무 이른 결정이다. P2P를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
경계를 묶음으로 보기
peer id와 사용자 id를 분리해 두면 권한 모델도 덜 흔들린다. 제품 계정은 결제, 팀, 문서 접근권을 말하고, peer id는 네트워크에서 보이는 한 주체를 말한다. 사용자가 같은 계정으로 새 브라우저를 열면 제품 입장에서는 같은 사람이어도 네트워크 입장에서는 새 피어일 수 있다. stream multiplexing도 제품 판단으로 내려오면 달라진다. multiplexing을 쓰면 여러 stream을 한 연결에 올릴 수 있지만, 각 stream이 밀릴 때의 압력은 제품이 따로 읽어야 한다.
파일 조각 전송이 밀릴 때 제어 메시지까지 막히면, 사용자는 연결이 살아 있어도 멈춘 것처럼 느낀다. security 계층도 같은 이유로 따로 둔다. Noise 같은 보안 채널이 열렸다는 사실은 통신이 보호된다는 뜻이지, 그 사용자가 특정 문서를 볼 권한이 있다는 뜻은 아니다. 암호화와 제품 권한을 한 문장으로 묶으면 장애가 났을 때 “안전한 연결”이라는 말만 남고 실제 거절 이유가 사라진다.
| 계층 | libp2p에서 보는 책임 | 제품에서 따로 정할 책임 | 흔한 실패 모습 | |---|---|---|---| | peer id | 네트워크 주체 식별 | 앱 계정, 권한, 기기 변경 정책 | 로그인은 됐는데 기존 피어로 인식되지 않음 | | transport | 연결 경로 선택 | 브라우저 지원, 방화벽, 네트워크 안내 | 특정 환경에서만 연결 실패 | | security | 보안 채널 생성 | 제품 권한, 세션, 접근 정책 | 암호화는 됐지만 권한 설명이 없음 | | muxing | 한 연결 위의 여러 stream | backpressure, 작업별 우선순위 | 작은 메시지가 큰 전송에 밀림 | | pubsub | 메시지 전파 규칙 | 정확히 한 번 처리, 중복 제거, UI 상태 | 채팅방처럼 기대했는데 중복/지연 발생 |
스택이 동작하는 순서
이 차이를 미리 적어 두면 사용자가 보는 문장이 짧아진다. 모든 기술 용어를 화면에 올릴 필요는 없다. 다만 짧은 문장 뒤에 실제 원인을 찾아갈 수 있는 trace가 있어야 한다. 그렇지 않으면 라이브러리 도입은 성공했는데 운영자는 더 많은 로그를 읽고도 덜 알게 된다. pubsub는 더 조심스럽다. 이름만 보면 채팅방이나 이벤트 버스처럼 편하게 보이지만, 제품은 중복 수신과 늦은 도착을 처리해야 한다.
읽음 상태, 협업 커서, 진행률 알림처럼 사용자에게 보이는 상태라면 “퍼졌다”와 “정확히 반영됐다”를 구분해야 한다. 그래서 스택을 읽을 때 API 이름보다 상태 이름을 먼저 본다. pending-peer-id, offered-transport, verified-security, opened-stream, pubsub-delayed처럼 계층을 갈라 두면 어디에서 멈췄는지 알 수 있다. 상태가 connecting 하나뿐이면 실패는 모두 같은 색으로 보인다. pubsub 역시 채팅방이 아니다. pubsub를 쓰면 메시지를 넓게 전파할 수 있지만, 정확히 한 번 처리되는 작업 큐처럼 기대하면 곧 어긋난다.
중복 수신, 늦은 도착, 순서 차이, 구독 범위를 제품이 어떻게 다룰지 정해야 한다. libp2p가 전파를 맡아도 제품의 상태 일관성까지 자동으로 해결되지는 않는다. 운영 로그는 개발자용 이벤트 이름만으로는 부족하다. peer_id.rotated, transport.blocked, security.handshake_failed, stream.backpressure, pubsub.duplicate 같은 내부 사건과 사용자가 본 문장이 같은 trace 안에 붙어야 한다. 그래야 문의가 들어왔을 때 네트워크 탓으로만 넘기지 않고 우리가 고른 경계를 설명할 수 있다.
'[P2P 프로토콜 libp2p는 라이브러리보다 경계 묶음에 가깝다'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
libp2p를 더 순수한 P2P의 이름으로만 보면 선택이 흐려진다. 내가 가져갈 기준은 서버 유무가 아니라 책임의 위치다. 어떤 계층을 피어에게 맡기고, 어떤 단서는 edge나 서버에 남기고, 어떤 실패를 사용자 경험으로 번역할지 정할 때 libp2p는 라이브러리보다 경계 묶음에 가까워진다. | 제품 장면 | 필요한 libp2p 감각 | 먼저 기록할 값 | 아직 피해야 할 약속 | |---|---|---|---| | 기기 간 파일 전달 | transport, stream | transport별 연결 시간, stream backpressure | 모든 네트워크에서 같은 속도 보장 | | 초대 기반 협업 | peer id, security | peer id 재생성, handshake 실패율 | peer id와 앱 계정 동일시 | | 실시간 상태 전파 | pubsub | 중복 수신, 늦은 도착, 구독 범위 | 정확히 한 번 전달되는 큐처럼 설명 | | 장애 문의 대응 | 계층별 trace | 멈춘 계층, 마지막 사용자 문구 | “P2P 문제” 한 문장으로 묶기 | 문서에도 같은 순서를 남긴다.
“libp2p 사용”이라고 쓰지 않고 “peer id는 브라우저에 보관한다”, “transport 실패는 사용자 문구로 분리한다”, “pubsub는 중복 수신 가능성을 가진다”처럼 제품 문장으로 바꾼다. 이 문장이 있어야 나중에 라이브러리를 바꾸거나 계층을 빼도 판단이 남는다. 가장 먼저 헷갈린 부분은 peer id였다. peer id는 네트워크 주체를 식별하지만 결제 계정이나 앱 사용자와 같은 말이 아니다. 같은 사람이 브라우저를 바꾸거나 키를 다시 만들면 peer id는 달라질 수 있고, 같은 peer id가 있다고 해서 제품 권한이 자동으로 보장되는 것도 아니다.
이 경계를 놓치면 로그가 이상해진다. 제품 계정은 정상인데 peer id가 바뀌어 기존 권한처럼 보이지 않거나, 반대로 네트워크 주체는 살아 있는데 앱 세션은 만료될 수 있다. 보안 채널이 열렸다고 해서 앱의 권한 판단까지 끝난 것은 아니다. 측정값은 transport별 연결 시간, 보안 handshake 실패율, stream별 backpressure 지연, pubsub 중복 수신, peer id 재생성 빈도로 나눠 본다. 이 숫자들이 있어야 어느 계층을 얇게 가져갈지 판단할 수 있다.
우리가 필요한 것은 libp2p 전체인가, 아니면 특정 경계 하나인가? 이 질문을 먼저 두지 않으면 스택 이름이 제품 책임을 덮어 버린다. 사용자는 libp2p를 쓰는지보다 왜 연결이 늦고, 왜 다시 눌러야 하고, 어떤 메시지가 안전하게 도착했는지를 본다. 이 질문은 라이브러리를 덜 쓰자는 뜻이 아니다. 오히려 libp2p가 제공하는 선택지를 제품 언어로 번역해서 쓰자는 뜻에 가깝다.
경계를 고르면 나중에 전체 스택을 쓰더라도 어떤 층을 믿고 어떤 층을 직접 책임질지 알 수 있고, 운영 로그의 첫 줄도 덜 흔들린다. 문서를 따라가면 기대가 조금 달라진다. libp2p는 하나의 마법 함수라기보다 transport, security, muxing, peer id, discovery, pubsub를 묶어 조립하는 스택에 가깝다. 이 자유도는 매력적이지만, 제품에 넣는 순간 질문이 바뀐다. libp2p 실험은 기능을 많이 켜는 쪽보다 하나씩 빼 보는 쪽에서 더 잘 드러난다.
peer id만 새로 만들었을 때 기존 권한과 어떻게 갈라지는지, transport만 바꿨을 때 실패 로그가 어떻게 달라지는지, pubsub가 늦게 도착할 때 화면은 어떤 상태를 보여야 하는지 따로 확인한다. stream을 여러 개 열 수 있다는 사실도 곧바로 좋은 UX가 되지는 않는다. 파일 조각, 제어 메시지, presence 이벤트가 같은 연결을 타면 어떤 stream을 먼저 살릴지 정해야 한다. 큰 파일을 보내는 동안 작은 취소 메시지가 뒤에 밀리면 사용자는 “취소가 안 된다”고 느낀다.
같은 기능을 더하는 요청이 들어와도 먼저 확인할 것은 책임 경계와 재시도 한도였다. 이 문단은 2026-07-04-p2p-protocol-02-libp2p-stack-boundaries 전용 기록이며, 다른 글의 문장을 복사하지 않는다. 배포 노트에는 기능 목록 대신 깨진 가정과 고정한 규칙을 남기는 편이 나았다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.