지난 시간에는 Netty의 구조 중 Core와 Transport Services에 대해 알아보았다. (지난 내용은 여기!)
이번에는... 가장 잘게 쪼개져 있는 섹션인 Protocol Support를 보려고 한다.
이전과 동일하게 각각 항목에서 이것들이 도대체 무슨 말인지... 이해를 해보려고 한다.
그러면 시작 전에... 다시 구조를 보도록 하자.

0. Protocol Support
사실 너무 기본적이고... 지겹지만 그래도 다시 한 번 체크하자.
Protocol은 무엇인가? 규약이다.
사람들끼리 대화를 한다고 하면, 어느 언어로 할지 정해야 한다.
예를 들어 한국사람들은 보통 한국어로 말을 하지만 영국인은 영어를 할 것이다.
이와 마찬가지로 컴퓨터나 장치들은 대화를 하는게 아니라 데이터를 주고받는데, 서로 데이터를 주고받기 위해 미리 정해놓은 통신 규칙과 절차의 체계를 프로토콜이라고 한다.
위 문장을 한 용어로 정리하면 "통신규약" 이다.
Protocol Support 는 말 그대로 통신규약을 지원하는 기능을 말하는것으로 이해된다.
필자가 어떤것이 Procotol Support로 포함되어 있는지 보려고 했는데 아씨... 스크롤을 올리면 글을 적을 공간이 안보이고
그렇다고 쭉 내리면 그림이 안보이고 바로 밑에 놓고 적어보도록 하겠다 후... (나를 슬프게 하는 사람들 - 김경호)

- HTTP & Web Socket
- SSL & StartTLS
- Google Protobuf
- zlib/gzip Compression
- Large File Transfer
- FTSP
- Legacy Text - Binary Protocols with Unit Testability
생각보다 재미있는 내용을 많이 지원하는데, 차근차근 알아보도록 하겠다.
아! 관전 포인트(?)는 많은 사람들이 사용하는 Spring 기준으로 비교를 주로 하려고 한다. 그러면... 후비고...!!!! 하기전에...
다시 Netty와 Spring Framework 의 차이를 기억하는가? (이전 관련 글 여기!)
Netty 는 서버 클라이언트 개발을 위한 비동기 이벤트기반 네트워크 어플리케이션 프레임워크 이다.
Spring Framework는 스프링 프레임워크는 자바(Java) 플랫폼을 위한 오픈소스 애플리케이션 프레임워크이다.
애초에 Netty는 서버 클라이언트 개발을 위한 비동기 이벤트 기반 네트워크 어플리케이션을 개발하기위해서 나온 것이기에
Spring Framework에서 제공하는 기능보다 점 더 섬세하면서 고성능을 도모할 수 있을것이다.
이것을 좀 더 있어보이게(?) 말하자면? Netty는 Spring보다 훨씬 로우 레벨(Low-Level)의 제어가 가능하다.
Spring Framework는 뛰어난 추상화를 제공한다. 이를 다시 말하면, 개발자가 작업할 것들을 프레임워크 자체에서 많이 해준다.
그만큼 네트워크 단의 세밀한 제어권은 프레임워크에게 넘기게 되는데 이것은 범용적이지 각자 상황에 맞는 것은 아닐것이다.
특히나 고성능을 내야 한다면 Netty를 쓰는 것이 좋다는 것이다.
1. HTTP & Web Socket
HTTP와 Web Socket!
우선 Spring Framework에서 지원하는 Spring WebSocket에 대해서 글을 정리했다. (여기 클릭)
HTTP에 대해 다시 생각해보자.
HTTP의 약자는 HyperText Transfer Protocol로
웹 브라우저(클라이언트)와 웹 서버 간에 데이터를 주고받기 위한 통신 규약(프로토콜)이다.
맨 처음 HTML은 Hyper Text Markup Language로 논문을 보려고 HTTP를 통해서 전송했다.
문서의 형태인 HTML을 전송하려면 당연 전송 규약 즉, 통신 규약(Protocol) 이 필요하지 않나?
이러한 것을 Netty에서도 지원한다는 것으로 보인다.
그러면 Web Socket은?
Web Socket의 정의를 다시 복기하면 단일 TCP 연결로 동시양방향통신 채널을 제공하는 컴퓨터 통신 프로토콜이다.
사실 이것을 구현하는 것은 쉽다. 한국에서 가장 많은 개발자가 애용하는 Spring Framework의 Spring Web Socket을 사용하면 된다.
결국 개인적인 생각으로는 HTTP, WebSocket이 Netty에서도 지원하는거지만 뭔가 Spring Framework보다는 더 자세한 설정이 있을것으로 추측하고있다. (푸앵카레추측)
패키지들을 한 번 살펴보고 테스트용 실행 코드를 작성할 것이다.
살펴볼 패키지는 io.netty.handler.codec.http 와 io.netty.handler.codec.http.websocketx 이다.
그 전에! 뽀나스(앗싸 가오리~)로 io.netty.handler.codec packcage 도 볼 것이다.
io.netty.handler.codec.http 하위에 websockey 관련한 코드들이 위치해 있는데
WebSocket도 처음 연결을 할 때 HTTP 위에서 작동하기 때문에 http package의 하부에 위치하는것이 잘 맞다고 판단된다.
Package io.netty.handler.codec
TCP/IP와 같은 스트림 기반 전송에서 발생하는 패킷 조각화 및 재조립 문제를 처리하는 확장 가능한 디코더와 그 일반적인 구현
방식.
여기서 OSI 7 Layer에 대해 다시 상기해보자.
Application Layer(7계층), Presentation Layer,(6계층) Session Layer(5계층),
Transport Layer(4계층), Network Layer(3계층), Data Link Layer(2계층), Physical Layer(1계층)가 있다.
이것들 중에서 Transport Layer에서 TCP(Transmission Control Protocol) 프로토콜이 데이터를 '바이트 스트림(Byte Stream)' 형태로 전송한다.
아니, 4계층의 데이터 단위는 Segment 아니었어?
프로그래머의 입장에서는 사실 바이트 스트림이 맞고, 이것을 네트워크 선위(물리적 실체)로 표현하면 세그먼트라고 한다.
패킷 조각화 및 재조립 문제를 처리한다는 것은, 데이터를 외부로 보낼 때 Segment가 Packet형태로 Network Layer에서 분해되서 갈 것인데, 이것을 패킷 조각화라고 한다.
재조립은 당연하게 데이터를 받는 쪽에서는 Packet을 알맞은 Segment로 재조립을 해야하기 때문이다.
이 과정에서 필요한 Decoder를 제공한다는 것이다! (후... 쉽지않네)
io.netty.handler.codec.http
Package Summary를 보면 사실 별 거 없다.
HTTP를 위한 인코더, 디코더 그리고 관련 메시지 타입들을 제공한다.
io.netty.handler.codec.http.websocketx
웹소켓 데이터 프레임을 위한 인코더, 디코더, 핸드셰이커 그리고 관련있는 메시지 타입을 제공한다.
예시 코드 - HTML 과 Netty Server
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Netty WebSocket Test</title>
</head>
<body>
<h2>Netty WebSocket Tester</h2>
<div>
<input type="text" id="msgInput" placeholder="보낼 메시지 입력" value="Hello Netty!">
<button onclick="sendMsg()">전송</button>
</div>
<div id="log" style="border: 1px solid #ccc; margin-top: 10px; padding: 10px; height: 300px; overflow-y: scroll;"></div>
<script>
var socket = new WebSocket("ws://localhost:8080/ws");
socket.onopen = function() {
log("✅ 서버에 연결되었습니다!");
};
socket.onmessage = function(event) {
log("📥 서버로부터 받음: " + event.data);
};
socket.onclose = function() {
log("❌ 연결이 종료되었습니다.");
};
function sendMsg() {
var input = document.getElementById("msgInput");
var text = input.value;
socket.send(text);
log("📤 보냄: " + text);
}
function log(msg) {
var logDiv = document.getElementById("log");
logDiv.innerHTML += msg + "<br>";
logDiv.scrollTop = logDiv.scrollHeight;
}
</script>
</body>
</html>
WebSocketServer.java
package com.example.demo.testing;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
public class WebSocketServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// ============================================================
// 1. HTTP 패키지 (io.netty.handler.codec.http)
// ============================================================
// HTTP 요청/응답을 인코딩/디코딩합니다. (가장 기본)
pipeline.addLast(new HttpServerCodec());
// 조각난 HTTP 데이터(Segment)들을 하나의 완전한 FullHttpRequest로 합쳐줍니다.
// 앞서 질문하신 "패킷 조각화/재조립"을 해결해주는 핵심 핸들러입니다.
pipeline.addLast(new HttpObjectAggregator(65536));
// ============================================================
// 2. WebSocketx 패키지 (io.netty.handler.codec.http.websocketx)
// ============================================================
// "/ws" 경로로 들어오는 HTTP Upgrade 요청을 감지하여 웹소켓 핸드셰이크를 수행합니다.
// Ping, Pong, Close 프레임 처리도 자동으로 해줍니다.
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// ============================================================
// 3. 커스텀 핸들러 (비즈니스 로직)
// ============================================================
// 실제 주고받는 데이터를 처리합니다.
pipeline.addLast(new MyWebSocketHandler());
}
});
System.out.println("WebSocket Server started on port 8080...");
Channel ch = b.bind(8080).sync().channel();
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
// 실제 메시지를 처리하는 핸들러
static class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
// 클라이언트가 보낸 텍스트 메시지 읽기
String request = frame.text();
System.out.println("받은 메시지: " + request);
// 클라이언트에게 그대로 돌려주기 (Echo)
// 주의: 나갈 때도 TextWebSocketFrame으로 감싸서 보내야 인코더가 처리합니다.
ctx.channel().writeAndFlush(new TextWebSocketFrame("Server received: " + request));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("✅ 클라이언트가 접속했습니다! (IP: " + ctx.channel().remoteAddress() + ")");
super.channelActive(ctx);
}
}
}
실행 결과


흐름을 보고 싶으면 펼쳐서 보기를 바란다.
해당 글은 gemini에게 정리를 요청했다.하긴 위의 코드도 물론 작성해달라 하긴 했는데...
[1단계: 서버 구동 준비]
main() 메서드 실행
- BossGroup(문지기)과 WorkerGroup(일꾼) 스레드 그룹이 생성됩니다.
- ServerBootstrap이 설정을 마치고 **8080 포트 바인딩(bind)**을 시도합니다.
[2단계: 서버 대기]
Listening 상태
- 서버가 8080 포트를 점유하고, 들어오는 연결 요청을 기다리는 상태가 됩니다. (아직 아무 일도 안 일어남)
[3단계: 클라이언트 접속 (TCP 연결)]
브라우저 접속 시도
- 브라우저가 ws://localhost:8080/ws로 접속합니다.
- TCP 3-way Handshake가 성공하고, 운영체제 레벨에서 물리적 연결이 성립됩니다.
[4단계: 파이프라인 조립]
ChannelInitializer 작동
- 연결된 클라이언트를 위한 전용 채널(SocketChannel)이 만들어집니다.
- 우리가 작성한 initChannel()이 실행되어, 파이프라인에 4개의 핸들러(HttpServerCodec, Aggregator, ProtocolHandler, MyHandler)가 순서대로 장착됩니다.
- 할 일을 다 한 ChannelInitializer는 파이프라인에서 사라집니다.
[5단계: 연결 활성화 알림]
channelActive() 실행
- 파이프라인 세팅이 끝나자마자 MyWebSocketHandler.channelActive()가 호출됩니다.
- 콘솔에 "✅ 클라이언트가 접속했습니다!" 로그가 찍힙니다.
[6단계: 핸드셰이크 요청 수신]
HTTP Upgrade 요청 도착
- 브라우저가 "나 웹소켓 쓰고 싶어(Upgrade)"라는 내용이 담긴 HTTP 패킷을 보냅니다.
- HttpServerCodec과 HttpObjectAggregator가 이 바이트 덩어리를 FullHttpRequest 객체로 예쁘게 변환합니다.
[7단계: 핸드셰이크 처리 및 승인]
WebSocketServerProtocolHandler 작동
- 파이프라인에 있던 프로토콜 핸들러가 /ws 경로와 헤더를 검사합니다.
- 정상적인 요청이면 HTTP 101 Switching Protocols 응답을 브라우저에게 보냅니다.
- 이 시점부터 통신 규약이 HTTP에서 WebSocket으로 전환됩니다.
[8단계: 실제 메시지 수신]
데이터 프레임 도착
- 브라우저에서 사용자가 "Hello"를 입력하고 전송 버튼을 누릅니다.
- 데이터가 WebSocketFrame 형태로 들어옵니다.
- 앞단의 핸들러들은 통과하고, 마지막 MyWebSocketHandler의 **channelRead0()**에 도달합니다.
[9단계: 비즈니스 로직 실행]
channelRead0() 내부 로직
- frame.text()를 통해 "Hello" 문자열을 꺼냅니다.
- 콘솔에 **"받은 메시지: Hello"**를 출력합니다.
[10단계: 응답 전송 (Echo)]
writeAndFlush() 실행
- "Server received: Hello" 문자열을 다시 TextWebSocketFrame에 담아 ctx.writeAndFlush() 합니다.
- 이 데이터는 파이프라인을 거꾸로 타고 올라가며 바이트로 인코딩되어 브라우저로 날아갑니다.
- 브라우저 화면에 메시지가 뜹니다.
하여간 HTTP랑 WebSocket 예시 끝!
2. SSL & StartTLS
우선 SSL, TLS에 대해서 생각해보자.
SSL(Secure Sockets Layer)과 TLS(Transport Layer Security)는 웹사이트와 사용자 브라우저 간의 데이터를 암호화하여 보안 통신을 제공하는 프로토콜이다.
TLS는 SSL의 보안 취약점을 개선하여 발전된 후속 버전이며, 현재는 TLS 1.3이 최신 버전으로 사용된다.
그러면 SSL, TLS 관련해서 Package 요약을 보도록 하자.
Package io.netty.handler.ssl
SSLEngine에 기반을 둔 SSL, TLS 구현체를 제공한다.
이것을 따라가보면 SSLEngine 클래스로 안내한다.
Netty에서 생성한 SSL, TLS 관련 코드는 javax에서 제공하는 SSLEngine의 구현체다.
SSLEngine 클래스의 설명을 살펴보자.
"Secure Sockets Layer(SSL) 또는 IETF RFC 2246 '전송 계층 보안(TLS)' 프로토콜을 사용하여 보안 통신을 가능하게 해주지만, 전송 계층(Transport)에는 종속되지 않는(independent) 클래스입니다."
해당 글을 읽으려면 너무 길어서... 그렇게 까지는 못했고, 그런가보다 한다. 나중에 시간이 되면 좀 자세히 읽어야겠다. (과연 ㅎ)
그러면 하여간... 뭐.. .그렇다니까 예시 코드를 보여달라고 Gemini에게 요청했다.
이전에 HTTP & WebSocket 예시 코드에 간단한 테스트 인증서를 설정하는 코드를 WebSocketServer와 index.html 에 추가한다.
WebSocketServer.java
// EventLoopGroup bossGroup = new NioEventLoopGroup(1); 위에 해당 코드 추가
// 1. [SSL 추가] 테스트용 가짜 인증서 만들기
// (실제 운영 시에는 정식 인증서 파일을 로드해야 합니다.)
SelfSignedCertificate ssc = new SelfSignedCertificate();
SslContext sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
// 2. ChannelPipeline 바로 밑에 해당 코드 추가
// ============================================================
// 0. [SSL 핸들러 추가] (가장 중요!)
// ============================================================
// 반드시 파이프라인의 **가장 첫 번째**에 있어야 합니다.
// 들어오는 모든 데이터를 암호화/복호화합니다.
pipeline.addLast(sslContext.newHandler(ch.alloc()));
index.html
// WebSocket연결부에 Secure 추가
var socket = new WebSocket("wss://localhost:8080/ws");
그리고 build.gradle에는 bouncycastle을 추가해야 한다.
JDK 1.8 이상 버전과 JDK 1.8 미만 버전의 경우 살짝 다르니 눈 부릅뜨고 잘 보길!
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
그리고 서버를 킨 다음, https:localhost:8080에 가서 해당 주소를 허용한 다음에 index.html에 가서 메시지를 보내면 성공!



하여간! SSL, TLS관련해서 코드를 추가할 수 있다. 솔직히 잘 모르겠는데, 그렇다니 아 그런가 보다~ 하고 넘어갈 것이다.
3. Google Protobuf
Protocol buffer 혹은 Protobuf란 구조화된 데이터를 직렬화하기 위한 언어 및 플랫폼 중립적인 확장가능한 직렬화 메커니즘이다.
여기서 플랫폼 중립적(Platform-neutral) 이라는 말은 어느 프로그램에서 받아도 깨지지 않게끔 했다는 말이다.
예를 들자면 Java에서 Protobuf를 이용해 메시지를 Python에 보내고 Python에서 문제없이 열어볼 수 있다는 말이다.
해당 메커니즘은 구글에서 만들어서 Google Protobuf라고 하는 듯 보인다.
이를 좀 더 자세하게 설명하면 장점이 무엇인지 쉽게 파악할 수 있다.
장점
프로토콜 버퍼는 구조화된 데이터, 레코드 형태의 데이터, 혹은 타입(Type)이 지정된 데이터를 언어와 플랫폼에 중립적이고 확장 가능한 방식으로 직렬화(Serialize)해야 하는 모든 상황에 가장 이상적인 솔루션입니다.
주로 (gRPC와 함께) 통신 프로토콜을 정의하거나 데이터를 저장하는 용도로 가장 널리 사용됩니다.
프로토콜 버퍼 사용의 주요 장점은 다음과 같습니다:
- 적은 용량의 데이터 저장 (Compact): 데이터의 크기가 매우 작고 효율적입니다.
- 빠른 파싱 속도 (Fast parsing): 데이터를 읽고 해석하는 속도가 빠릅니다.
- 다양한 프로그래밍 언어 지원: 수많은 프로그래밍 언어에서 바로 사용할 수 있습니다.
- 기능 최적화: 자동으로 생성된 클래스(코드)를 통해 성능이 최적화된 기능을 제공합니다.
뭐... 사실 좋으니까 쓰겠지?
이걸 봐도 JSON 을 쓰면 되는걸 왜 굳이 Protobuf 같은걸 써야하는지 궁금하다.
기존 방식의 한계
- 메모리에 있는 데이터 구조(Struct)를 그대로 디스크에 저장하는 것은 시스템마다 포인터 레이아웃이나 정수 인코딩 방식이 달라 호환되지 않는다.
- JSON이나 XML은 사람이 읽기는 좋지만, 구글이 매일 처리하는 페타바이트급 데이터를 감당하기에는 비효율적이고 비용이 너무 많이 든다.
- 데이터 타입마다 커스텀 직렬화(Serializer)를 만드는 것은 오류가 발생하기 쉽고 최적화를 공유하기 어렵다.
JSON vs Protobuf
JSON : 텍스트 기반, 인간 친화적인 포맷. 직관적이지만 압축하지 않는다.
Protobuf: 이진 인코딩 포맷을 활용. 본질적으로 더 압축되어있고, 공간효율적이다.
JSON은 엄격한 schema제어가 불가능하다. 하지만 Protobuf는 .proto 파일들을 통해서 스키마 제어가 가능하다.
이는 타입안정성과 일관성을 제공한다.
또한, payload 가 점점 커질수록 JSON은 네트워크 지연(latency)을 증가시킬 수 있다.
하지만 Protobuf의 경우에는 메시지 크기가 작아 필요한 대역폭(Bandwidth)이 줄어들며, 결과적으로 더 빠른 네트워크 전송과 낮은 지연 시간을 제공한다.
그래서, Netty에서는 어떻게 쓰나?
해당 기능을 지원하는 package위치는 io.netty.handler.codec.protobuf 이다.
Google Protocol Buffer Message와 MessageNano를 ByteBuf로 인코딩 혹은 디코딩한다. 반대로도 한다.
일전에 ByteBuf에 대해서 알아보았는데, zero-copy 를 위해서 Netty에서 제공하는 고성능의 Buffer이다. (궁금하면 여기 클릭!)
여기서 또 처음 보는 것이 나오는데, MessageNano이다. 이건 또 뭔가?
찾아보니 모바일 버전의 Protobuf라고 하는데, 요즘은 잘 쓰지 않는다고 한다. 하여간 Netty에서 이것들을 ByteBuf로 변환해서 읽도록 기능을 제공한다는 것이다.
build.gradle 수정
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.4'
// 구글이 만든 변환기 플러그인 추가!
id 'com.google.protobuf' version '0.9.4'
}
// ... 중간 생략 ...
// dependency 추가!
dependencies {
implementation 'io.netty:netty-all:4.1.101.Final'
implementation 'com.google.protobuf:protobuf-java:3.25.1'
}
// 파일 맨 아래에 이 설정을 통째로 복사해서 붙여넣기
protobuf {
protoc {
// 프로토콜 버퍼 컴파일러를 다운로드 받아서 씁니다.
artifact = "com.google.protobuf:protoc:3.25.1"
}
}
ProtoServer.java
package com.example.demo.testing.util;
import com.example.demo.protobuf.UserProto;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.*;
public class ProtoServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// ============================================================
// [핵심] Protobuf 코덱 4총사 설정
// ============================================================
// 1. [IN] 길이 정보(Varint32)를 읽어서 패킷을 자릅니다. (TCP Stickiness 해결)
p.addLast(new ProtobufVarint32FrameDecoder());
// 2. [IN] 잘린 바이트를 'User' 객체로 변환합니다. (디코딩)
// 인자로는 변환하고 싶은 Protobuf 클래스의 'DefaultInstance'를 넣어줍니다.
p.addLast(new ProtobufDecoder(UserProto.User.getDefaultInstance()));
// 3. [OUT] 나가는 메시지 앞에 길이 정보(Varint32)를 붙입니다.
p.addLast(new ProtobufVarint32LengthFieldPrepender());
// 4. [OUT] 'User' 객체를 바이트로 변환합니다. (인코딩)
p.addLast(new ProtobufEncoder());
// 5. [Business] 실제 로직 처리
p.addLast(new ProtoServerHandler());
}
});
System.out.println("Protobuf Server started on port 8888...");
b.bind(8888).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
// 서버 비즈니스 로직
static class ProtoServerHandler extends SimpleChannelInboundHandler<UserProto.User> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, UserProto.User msg) {
// 들어온 데이터는 바이트가 아니라 이미 완벽한 'User' 객체입니다.
System.out.println("📥 [Server] 받은 데이터:");
System.out.println("ID: " + msg.getId());
System.out.println("Name: " + msg.getName());
// 응답 보내기 (Echo)
// Protobuf는 Setter가 없고 'Builder'를 써야 합니다.
UserProto.User response = UserProto.User.newBuilder()
.setId(msg.getId())
.setName("Server confirmed: " + msg.getName())
.setEmail("server@example.com")
.build();
ctx.writeAndFlush(response);
}
}
}
ProtoClient.java
package com.example.demo.testing;
import com.example.demo.protobuf.UserProto;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.*;
public class ProtoClient {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// 클라이언트도 서버와 똑같은 코덱 구조를 가집니다.
p.addLast(new ProtobufVarint32FrameDecoder());
p.addLast(new ProtobufDecoder(UserProto.User.getDefaultInstance()));
p.addLast(new ProtobufVarint32LengthFieldPrepender());
p.addLast(new ProtobufEncoder());
p.addLast(new ProtoClientHandler());
}
});
b.connect("localhost", 8888).sync().channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
static class ProtoClientHandler extends SimpleChannelInboundHandler<UserProto.User> {
// 연결되자마자 실행됨
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("📤 [Client] 서버로 메시지 전송 중...");
// Protobuf 객체 생성 (Builder 패턴)
UserProto.User user = UserProto.User.newBuilder()
.setId(1001)
.setName("Kim Netty")
.setEmail("kim@netty.io")
.build();
ctx.writeAndFlush(user);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, UserProto.User msg) {
System.out.println("📥 [Client] 서버 응답 수신:");
System.out.println("Message: " + msg.getName());
}
}
}
실행하면 다음과 같은 모습을 확인할 수 있다.


해당 예시 코드의 흐름을 Gemini에게 요청해서 정리했다. 궁금하면 열어보기!
상황: 클라이언트가 접속하자마자 id: 1001, name: "Kim Netty"라는 유저 정보를 서버에 보냅니다.
1단계: 클라이언트의 출발 (보내기)
- 객체 생성 (ClientHandler):
- 클라이언트가 서버에 접속(connect)하자마자 channelActive가 실행됩니다.
- 자바 코드(User.newBuilder()...)를 통해 User 자바 객체를 생성합니다.
- 파이프라인 진입 (writeAndFlush):
- ctx.writeAndFlush(user)를 호출하여 파이프라인의 뒤쪽(Tail)에서 앞쪽(Head)으로 데이터를 던집니다.
- 길이 붙이기 (LengthFieldPrepender):
- User 객체가 바이트로 변환되기 전(혹은 직후)에, "이 데이터는 총 15바이트야" 라는 **길이 정보(Header)**를 데이터 맨 앞에 붙입니다. (TCP에서 데이터가 뭉치는 걸 방지하기 위함)
- 직렬화/인코딩 (ProtobufEncoder):
- User 객체를 0과 1로 이루어진 **바이트 배열(Binary)**로 변환합니다.
- 결과물: [길이 헤더][데이터 본문] 형태의 바이트 뭉치가 됩니다.
- 네트워크 전송:
- 랜선을 타고 서버로 날아갑니다. 슝~ 🚀
2단계: 서버의 수신 (받기)
- 데이터 도착:
- 서버의 소켓에 바이트 덩어리가 도착합니다.
- 패킷 자르기 (Varint32FrameDecoder):
- 가장 먼저 길이 헤더를 읽습니다. "아, 15바이트짜리네?"
- 뒤따라오는 데이터가 15바이트가 될 때까지 기다렸다가, 딱 그만큼만 잘라서 다음 단계로 넘깁니다. (이게 없으면 데이터가 깨집니다.)
- 객체 변환/디코딩 (ProtobufDecoder):
- 잘라낸 순수 데이터 바이트를 읽어서, 다시 UserProto.User 자바 객체로 복원합니다.
- 이때 User.proto 설계도를 참고합니다.
- 비즈니스 로직 (ServerHandler):
- channelRead0 메서드에 완벽한 User 객체가 배달됩니다.
- System.out.println으로 "Kim Netty"를 출력합니다.
3단계: 서버의 응답 (답장하기)
- 응답 객체 생성:
- 서버가 "잘 받았음!"이라는 내용을 담아 새로운 User 객체(Response)를 만듭니다.
- 역순 과정 (인코딩 -> 전송):
- 서버도 똑같이 Prepender(길이 붙이기)와 Encoder(직렬화)를 거쳐 바이트로 변환한 뒤 클라이언트에게 쏩니다.
4단계: 클라이언트의 수신 (응답 받기)
- 응답 수신 및 디코딩:
- 클라이언트도 서버와 똑같은 Decoder 2개를 가지고 있습니다.
- [길이 확인 및 자르기] -> [객체로 변환] 과정을 거칩니다.
- 최종 확인 (ClientHandler):
- channelRead0가 실행되고, 서버가 보낸 메시지를 콘솔에 출력합니다.
일일이 찾아서 보다보니 생각보다 굉장히 많이 찾아보게 되었다. (아 귀찮아 죽겠네 ㅠㅠ...)
궁금한것에 대해서 바로바로 AI를 활용해서 찾고 예시 코드도 직접 작성할 필요없이 친절히 만들어주니 너무 학습하기 좋은 세상이다.
다만 이것에 대해서 왜 그런지를 이해하고 원하는 방향으로 AI가 일하도록 하는건 사람이니...
큰 그림을 그릴 수 있는 개발자가 되어야 겠다는생각이 든다.
다음에는 Transfer Protocol section 에서 다른 기능들에 대해서 다시 찾아보도록 하겠다.
출처
https://docs.spring.io/spring-framework/reference/web/websocket.html
https://www.ucert.co.kr/ssl/info
https://netty.io/4.1/api/io/netty/handler/ssl/package-summary.html
https://netty.io/4.1/api/io/netty/handler/codec/http/websocketx/package-summary
https://netty.io/4.1/api/io/netty/handler/codec/http/package-summary
https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/SSLEngine.html?is-external=true
https://www.gravitee.io/blog/protobuf-vs-json
https://www.youtube.com/watch?v=FR754e5xIwg
https://netty.io/4.1/api/io/netty/handler/codec/protobuf/package-summary