2026. 8. 25. 21:51ㆍBlockchain/Ethereum
EVM 계열 체인의 노드들은 블록체인에 쓰기/읽기를 위한 JSON-RPC API를 제공한다. 일반적인 tx 전송 및 데이터 조회는 HTTP를 통한 JSON-RPC를 사용한다. EVM 체인에서는 HTTP 외에도 Websocket을 이용해서 노드와 직접 통신할 수 있는데, 블록체인 백엔드 내 Websocket을 사용시 고려해야 할 점에 대해서 정리하려 한다.
HTTP Vs. Websocket
EVM 체인 노드의 JSON-RPC는 세가지 전송 계층으로 나뉜다. HTTP, Websocket, IPC이다.
IPC(Inter Process Communication)의 경우 프로세스 간 데이터를 주고 받는 동일 로컬 환경 내에서 데이터를 주고 받는 기능이므로 일반적인 블록체인 백엔드 서버는 EVM 노드와 별개인 상황이 많기 때문에 제외한다.
별도의 서버에 있는 백엔드는 JSON-RPC가 제공하고 있는 HTTP를 통한 통신을 수행하거나 Websocket을 통한 통신을 수행해야 한다. 두 방식의 가장 큰 차이는 단방향 통신/양방향 통신으로 나뉜다. 아래의 표는 차이점을 나열한 것이다.
| HTTP | WebSocket | |
|---|---|---|
| Port(general) | 8545 | 8546 |
| 방향성 | 요청-응답 단방향 | 양방향, 서버 push 가능 |
| 이벤트 수신 | eth_newFilter + eth_getFilterChanges polling |
eth_subscribe push |
| 연결 비용 | 요청당 핸드셰이크 | 1회 핸드셰이크 후 유지 |
EVM Websocket 연결의 특징
Websocket은 HTTP와 다르게 1번의 handshake 이후 연결이 유지가 되며 노드에서 연결되어 있는 백엔드 서버에 직접 push를 통해 데이터를 전달할 수 있는 특징이 있다.
예를 들어, HTTP로 연결하고 있는 상태에서는 노드 내에 특정 컨트랙트에 대한 이벤트가 발생해도 백엔드 서버에서는 알 방법이 없다. 주기적으로 컨트랙트 이벤트 발생 여부를 polling 하여야 이벤트를 확인할 수 있다.
Websocket을 사용한다면 이벤트 발생 시 연결되어 있는 통신 구간 내에서 노드가 직접 백엔드 서버에 데이터를 전달할 수 있으므로 polling을 지속적으로 할 필요 없이 즉각 이벤트를 확인할 수 있게 된다.
또한 Websocket도 HTTP처럼 eth_call이나 eth_getLogs 같은 호출 기능을 그대로 사용할 수 있다. 반면에 HTTP는 Websocket처럼 노드에 연결하여 데이터를 확인하는 구독(subscription) 방식을 사용할 수 없다.
장단점
Websocket을 씀으로써 백엔드 서버에서 얻을 수 있는 장점들이 있다.
- 거의 실시간으로 EVM의 데이터를 확인할 수 있다. 블록이 생성되고 전파됨과 거의 동시에 데이터를 수신 가능하다.(~수ms) 컨트랙트의 이벤트 데이터나 트랜잭션의 receipt를 즉각적으로 수령할 수 있어서 대기 시간이 짧아지는 효과가 있다.
- Polling을 제거한다. HTTP 방식만 사용했을 시에는 폴링을 함으로써 트래픽을 계속 사용하게 되는데 Websocket 사용 시 특정 이벤트가 발생할 때만 바이트가 전송된다.
- 핸드셰이크 리소스를 제거할 수 있다. HTTP가 요청당 핸드셰이크를 수행해야 하는 반면에 Websocket은 한번 연결로 지속적 유지가 가능하다. 고빈도로 호출 시 RTT(Round Trip Time)가 조금 줄어든다.
단점도 존재한다.
- Websocket 연결이 끊어 졌을 시, 끊긴 동안의 이벤트는 다시 연결해도 수령할 수 없다.
- 장애 반경도 넓다. Websocket 연결이 끊어졌을 시 진행 중이던 구독의 모든 기능이 동작하지 않게 된다.
- 에러 리턴 없이 연결이 끊기는 경우가 있다. 특히나 Alchemy 같은 노드 서비스들을 쓸떄도 지속적인 Websocket 연결을 보장하지 않으며 조용히 끊기는 경우가 있다.
Websocket 사용의 결론
HTTP 방식과 Websocket 방식은 상이한 특성을 보유하고 있기 때문에 백엔드 서버를 설계할 때는 용도에 따라 기능을 구분하여 사용하는 것이 좋다.
HTTP 같은 경우에는 eth_call을 통해 특정 계정의 잔고나 상태 조회 같은 단발성의 조회에 사용하는 것이 좋다. 재시도 하는 것이 단순하기 때문이다. 또한 상태 변화를 위한 트랜잭션 전송도 HTTP를 사용하는 것이 좋다. 재시도 및 실패 원인을 분석하기에는 Websocket 보다 추적이 용이하기 때문이다.
Websocket은 즉시성이 필요한 기능에 사용하는 것이 좋다. Receipt 대기 시에는 polling 대비 지연이 대폭 감소하고, 신규 블록 생성 및 이벤트 감지도 HTTP보다 빠르게 확인할 수 있다. 또한 노드의 tx mempool의 모니터링 시에도 Websocket을 사용해야한다. pending 트랜잭션에 대한 스트림은 polling으로 재현이 불가능하기 때문이다.
Websocket 사용 설계 예시
위 Websocket 장단점 단락에서 확인할 수 있듯이 Websocket을 사용하면서 발생할 수 있는 설계 고려 사항의 큰 부분은 Websocket의 연결 지속성이다. 그렇기 때문에 연결의 끊김을 감지했을 시 즉시 재연결하는 로직을 구현하면 보다 안전한 Websocket 사용이 가능하다.
Geth의 ethclient를 사용한 예를 확인한다. 기능 동작보다는 컨셉으로만 확인한다.
용도에 따라 사용할 프로토콜을 구분하기 위하여 백엔드 서버 내의 노드 호출 클라이언트는 HTTP client와 Websocket client를 둘 다 선언한다. Websocket client는 재연결 로직은 클래스 내에 구현하기 위하여 Websocket client manager로 구현한다.
type EvmClient struct {
client *ethclient.Client
wsClientManager *wsClientManager
}
func NewEvmClient(ctx context.Context, rpcURL, wsURL string) (*EvmClient, error) {
httpClient := &http.Client{
Transport: defaultTransport,
Timeout: defaultHTTPTimeout * time.Second,
}
rpcClient, err := erpc.DialOptions(ctx, rpcURL, erpc.WithHTTPClient(httpClient))
if err != nil {
return nil, err
}
client := ethclient.NewClient(rpcClient)
var wsClientManager *wsClientManager
wsClientManager, err = newWsClientManager(ctx, wsURL)
if err != nil {
return nil, err
}
return &EvmClient{client, wsClientManager}, nil
}
func (e *EvmClient) Client() *ethclient.Client { return e.client }
func (e *EvmClient) WsClientManager() *wsClientManager { return e.wsClientManager }
위에서의 wsClientManager는 Websocket 호출 클라이언트의 생성 뿐만 아니라 websocket 헬스체크, 재연결로직을 함께 구현한다.
type wsClientManager struct {
url string
dialOpts []erpc.ClientOption
mu sync.RWMutex
client *ethclient.Client
raw *erpc.Client
healthInterval time.Duration
maxHealthFails int
backoff time.Duration
resubs map[string]ResubFn
}
func newWsClientManager(ctx context.Context, wsURL string, opts ...erpc.ClientOption) (*wsClientManager, error) {
m := &wsClientManager{
url: wsURL,
dialOpts: opts,
healthInterval: 20 * time.Second,
maxHealthFails: 2,
backoff: 1 * time.Second,
resubs: make(map[string]ResubFn),
}
if err := m.connectOnce(ctx); err != nil {
return nil, err
}
m.startLoops()
return m, nil
}
Manager 내에는 노드의 Websocket URL, ethclient, 헬스체크 파라미터, re-subscription 인스턴스를 정의한다. Manager 생성과 동시에 Websocket client를 통한 노드 연결(connectOnce()) 실행 및 내부 헬스체크 및 재연결 루프(startLoops())를 goroutine으로 실행한다.
func (m *wsClientManager) connectOnce(ctx context.Context) error {
raw, err := erpc.DialOptions(ctx, m.url, m.dialOpts...)
if err != nil {
return err
}
cli := ethclient.NewClient(raw)
...
return nil
}
func (m *wsClientManager) startLoops() {
...
t := time.NewTicker(m.healthInterval)
defer t.Stop()
failCount := 0
for {
select {
case <-ctx.Done():
return
case <-t.C:
cli := m.Get()
if cli == nil {
if m.reconnect(ctx) == nil {
failCount = 0
}
continue
}
_, err := cli.BlockNumber(ctx)
if err != nil {
failCount++
if failCount >= m.maxHealthFails {
if m.reconnect(ctx) == nil {
failCount = 0
}
}
continue
}
// OK
failCount = 0
}
}
}
ethclient 다이얼을 통해 Websocket으로 노드에 최초 연결 후 manager loop 내에서 헬스체크 주기 동안 Websocket 호출을 하며 연결된 Websocket이 종료되어 있는지 확인한다. Websocket의 응답이 없을 시 재연결 로직을 통해 Websocket을 연결한다.
내부 호출 경로의 시각화는 아래와 같이 정리할 수 있다.
newWsClientManager
├─► connectOnce ──► erpc.DialOptions ──► ethclient.NewClient ──► [swap client]
└─► startLoops ──go──► healthLoop
│
▼
┌──────────── ticker(20s) ────────────┐
│ │
<-ctx.Done() ──► return │
<-t.C ──► Get() │
├─ nil ─────────────┐ │
└─ BlockNumber(ctx) │ │
├─ ok ──► fail=0 ─────────┤
└─ err ─► fail++ │
└─ fail>=2 ─┐ │
▼ │
reconnect│
├ ok ► fail=0
└ err ► 다음 tick ─┘
│
└─► connectOnce + resubs 재등록
// 상태 전이 요약
[CONNECTED] ──BlockNumber err──► [DEGRADED failCount=1]
▲ │
│ err (>= max)
│ ▼
└────── reconnect ok ────────── [RECONNECTING] ──err──┐
▲ │
└───backoff─────┘
결론
EVM 블록체인에서 제공하는 Websocket은 실시간성, 즉시성이 필요한 서비스에 우선적으로 사용하면 좋다. 하지만 위에서 언급했듯이 장점과 더불어 단점도 명확히 존재하니 Websocket을 다루면서 백엔드 서버를 개발할 때는 고려해야 할 점을 기억해야한다.
Websocket을 통한 백엔드 서버 설계 시 가장 고려해야 할 점은 통신 연결의 지속성 관리이다. 위에서 작성한 수도코드는 Websocket 연결을 최대한 유지하기 위한 하나의 방안에 불과하며 다른 방식도 존재할 수 있다. 노드 서비스의 플랜을 업그레이드 했을 시 Websocket 연결 순단 빈도를 낮출 수는 있어도 완벽하게 방어를 할 수는 없을 것이다. HTTP와의 trade-off로 Websocket을 사용해 실시간성을 가져간다 했을 시 안정성 있게 구현하는 것을 고민하여 서버 개발을 하면 좋을것 같다.
'Blockchain > Ethereum' 카테고리의 다른 글
| 블록체인 백엔드 설계: Reorg (0) | 2026.08.07 |
|---|---|
| Gas price != zero (0) | 2025.03.18 |
| ENS(Ethereum Name Service) 개념 및 구조 (0) | 2023.06.15 |
| Ethereum의 PoS 전환을 반기며 (0) | 2023.06.15 |
| Ethereum Gas (0) | 2023.06.15 |