자바 네트워크 소녀 네티( 정경석 저 / 이희승 감수, 한빛미디어 ) 를 읽고 정리합니다.
📘2부 3장. 네티의 상세 - 부트스트랩
🔹부트스트랩
부트스트랩은 네티로 작성한 네트워크 애플리케이션이 시작할 때 가장 처음 수행되는 부분으로 애플리케이션이 수행할 동작과 각종 설정을 지정하며 네티 애플리케이션의 기본이 된다.
부트스트랩 설정은 크게 세가지로 나뉟다.
- 이벤트 루프
- 채널의 전송 모드
- 채널 파이프라인
이벤트 루프 / 전송모드
이벤트 루프는 소켓 채널에서 발생한 이벤트를 처리하는 스레드 모델을 구현한 부분이다.
부트스트랩에서 설정한 소켓 모드에 따라 블로킹, 논블로킹, 또는 EPOLL 모드를 사용할 수 있다.
+) epoll : 입출력 다중화 기법 중 하나로, 리눅스에서 가장 빠른 입출력 방식
채널 파이프라인
채널 파이프라인은 소켓 채널로 수신된 데이터를 처리할 데이터 핸들러를 지정한다.
🔸부트스트랩이란
네티로 작성한 네트워크 애플리케이션의 동작 방식과 환경을 설정하는 도우미 클래스
예를 들어 네트워크에서 수신한 데이터를 단일 스레드로 데이터베이스에 저장하는 네트워크 애플리케이션을 작성한다. 이 애플리케이션은 8080포트에서 데이터를 수신하고 소켓 모드는 NIO를 사용한다. |
⇒ 부트스트랩은 NIO 소켓모드를 지원하는 서버 소켓 채널, 데이터를 변환해 DB에 저장하는 데이터 처리 이벤트 핸들러, 8080 포트 바인딩, 단일 스레드를 지원하는 이벤트 루프에 대한 설정이 필요하다.
🔸부트스트랩 구조
통상적으로 네트워크 애플리케이션은 서비스를 제공할 네트워크 포트, 네트워크 전송에 사용할 소켓 모드와 소켓 옵션, 소켓 데이터를 처리하는 스레드, 그리고 프로토콜로 구성된다.
+) 프로토콜은 채널 파이프라인에 등록되는 인코더와 디코더에서 처리한다.
아파치 톰겟의 경우 XML을 사용해 톰캣 커넥터가 사용할 입출력 모드와 스레드 수 그리고 TCP 서비스 포트를 설정할 수 있다. 또한 웹 서버이므로 HTTP프로토콜을 사용하며 웹 서비스 관리자를 위한 계정을 설정한다. 웹 서버에 특화된 내용이 추가되었을 뿐, 네티의 부트스트랩 설정 내용과 대부분 동일하다.
네티 부트스트랩은 서버 애플리케이션을 위한 ServerBootstrap 클래스와 클라이언트 애플리케이션을 위한 Bootstrap 클래스로 나뉜다.
ServerBootstrap 클래스는 클라이언트 접속을 대기할 포트를 설정하는 메서드가 추가되어 있을 뿐 API구조는 Bootstrap 클래스와 같다.
각 부트스트랩 클래스는 AbstractBoostrap 클래스와 Channel 인터페이스를 상속 받고 있다.
앞서 2장에서는 블로킹 소켓을 지원하는 서버 애플리케이션을 논블로킹으로 전환하는데 아주 많은 코드를 변경해야했다. 하지만 네티는 부트스트랩의 설정을 통해서 BlockingServer, NonBlockingServer와 동일한 동작을 하는 애플리케이션을 작성할 수 있다.
⇒ 부트스트랩은 소켓 채널에 대한 입출력을 추상화함으로써, 입출력 모드에 해당하는 소켓 채널 클래스를 설정하기만 하면 변경이 완료된다!
🔸ServerBootstrap
이벤트 루프, 채널의 전송 모드, 채널 파이프라인에 대해 알아보자
ServerBootstrap 은 애플리케이션이 시작할 때 초기화된다.
ServerBootstrap 클래스로 논블로킹 소켓모드의 애플리케이션을 작성하는 코드를 보자
public class EchoServer {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
//1
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
//클라이언트로부터 연결된 채널이 초기화될 때의 기본 동작이 지정된 추상 클래스
protected void initChannel(SocketChannel ch) throws Exception {
// 채널 파이프라인 객체 생성
ChannelPipeline p = ch.pipeline();
//채널 파이프라인에 EchoServerHandler 등록,
// EchoServerHandler는 이후에 클라이언트 연결이 생성되었을때 데이터 처리를 담당한다.
p.addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
ServerBootstrap 객체에 서버 애플리케이션이 사용할 두 스레드 그룹을 설정했다.
- 부모 스레드 그룹(bossGroup): 클라이언트의 연결을 수락한다.
- 자식 스레드 그룹(workerGroup): 연결된 클라이언트의 소켓에서 데이터 입출력과 이벤트 처리를 담당한다.
NioEventLoopGroup 클래스 생성자의 인수로 사용된 숫자는 스레드 그룹 내에서 생성할 최대 스레드 수를 의미한다.
⇒ 여기서는 1로 설정했으니 단일 스레드로 동작한다.
인수가 없는 생성자는사용할 수레드 수를 서버 애플리케이션이 동작하는 하드웨어 코어 수를 기준으로 결정하는데 스레드 수는 하드웨어가 가지고 있는 CPU 코어 수의 2배를 사용한다. ⇒ 4코어 CPU라면 스레드 8개가 생성된다.
소켓 채널에 설정할 수 있는 값은 블로킹, 논블로킹, EPOLL 이다.
🔹블로킹 소켓 모드로 변경
위의 코드를 블로킹 모드로 바꾸려면 다음과 같이 하면 된다.
public class BlockingEchoServer {
public static void main(String[] args) {
**EventLoopGroup bossGroup = new OioEventLoopGroup(1);
EventLoopGroup workerGroup = new OioEventLoopGroup();**
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
**.channel(OioServerSocketChannel.class)**
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
bossGroup , workerGroup 을 OioEventLoopGroup 객체로 생성하고, 그룹의 채널을 OioServerSocketChannel클래스로 지정해준다.
참고로 OioEventLoopGroup 블로킹 I/O 모드는 deprecated 되었다.
🔹 EPOLL 소켓 모드로 변경
EPOLL도 비슷한 방식으로 설정 가능하다.
public class EpollEchoServer {
public static void main(String[] args) {
**EventLoopGroup bossGroup = new EpollEventLoopGroup(1);
EventLoopGroup workerGroup = new EpollEventLoopGroup();**
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(**EpollServerSocketChannel**.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
EpollEventLoopGroup 클래스의 객체가 스레드 그룹에 설정되고, 서버 채널에 EPOLL 입출력 모드를 위한
EpollServerSocketChannel 클래스를 설정해준다.
🔸ServerBootstrap API
이벤트 루프, 채널의 전송 모드, 채널 파이프라인의 세가지 설정은 각각 ServerBootStrap 의 API로 매칭된다.
🔹group - 이벤트 루프 설정
데이터 송수신처리를 위한 이벤트 루프를 설정하는 메서드이다.
/**
* The {@link EventLoopGroup} which is used to handle all the events for the to-be-created
* {@link Channel}
*/
public B group(EventLoopGroup group) {
ObjectUtil.checkNotNull(group, "group");
if (this.group != null) {
throw new IllegalStateException("group set already");
}
this.group = group;
return self();
}
[AbstractBootstrap 코드의 일부분]
클라이언트는 연결 요청이 완료된 이후의 데이터 송수신 처리를 위해 하나의 이벤트 루프로 모든 처리가 가능하다.
반대로 서버는 1)클라이언트의 연결 요청을 수락하기 위한 이벤트 루프와 2)데이터 송수신 처리를 위한 이벤트 루프가 필요하다.
⇒ AbstractBootstrap 클래스에서는 this.group = group; 과 같이 하나의 이벤트 루프만 설정하도록 되어있다. 즉 Bootstrap 클래스에서 설정한 이벤트 루프는 클라이언트 소켓의 데이터 입출력만 담당한다.
ServerBootstrap 클래스에서는 AbstractBootstrap 클래스의 group 메서드를 재정의하여 부모 스레드 그룹을 설정한다.
public final class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
...
@Override
public ServerBootstrap group(EventLoopGroup group) {
return group(group,group);
}
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if(childGroup==null){
throw new NullPointerException("childGroup");
}
if(this.childGroup!=null){
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
return group(group,group);
⇒ 부모 스레드그룹과 자식 스레드 그룹에 동일한 스레드 그룹을 설정했다. 즉 하나의 스레드 그룹에서 클라이언트의 연결 수락과 연결된 소켓의 데이터 입출력을 모두 처리한다는 의미이다.
super.group(parentGroup);
⇒ AbstractBootstrap 클래스의 멤버 변수로 부모 스레드 그룹을 설정했다. 이로 인해 클라이언트 연결을 수락하는 부모 스레드 그룹과 연결된 클라이언트 소켓에 대한 데이터 입출력을 처리하는 자식스레드 까지 두개의 스레드 그룹을 설정할 수 있다.
🔹 channel - 소켓 입출력 모드 설정
부트스트랩의 channel 메서드에 등록된 소켓 채널 생성 클래스가 소켓 채널을 생성한다.
LocalServerChannel | 가상 통신을 위한 서버 소켓 채널을 생성 |
OioServerSocketChannel | 블로킹 모드의 서버 소켓 채널을 생성 |
NioServerSocketChannel | 논블로킹 모드의 서버 소켓 채널을 생성 |
EpollServerSocketChannel | 리눅스 커널의 epoll 입출력 모드를 지원하는 서버 소켓 채널을 생성 |
OioSctpServerSocketChannel | SCTP 전송계층을 사용하는 블로킹 모드의 서버 소켓 채널을 생성 |
NioSctpServerSocketChannel | SCTP 전송계층을 사용하는 논블로킹 모드의 서버 소켓 채널을 생성 |
NioUdtByteAcceptorChannel | UDT 프로토콜을 지원하는 논블로킹 모드의 서버 소켓 채널을 생성 (스트림 데이터 처리) |
NioUdtMessageAcceptorChannel | UDT 프로토콜을 지원하는 블로킹 모드의 서버 소켓 채널을 생성 (데이터그램 패킷 처리) |
⇒ ServerBootstrap 의 channel 메서드에는 ServerChannel 인터페이스를 상속받은 클래스를 인수로 사용 가능하다.
+) SCTP(Stream Control Transmission Protocol):
- TCP의 연결지향 및 전송 보장 특성과 UDP 의 메시지 지향 특성을 모두 갖추고 있다.
- 네방향 핸드셰이크를 통해 연결을 수립하고, 이때 연결 정보에 쿠키를 넣어 DoS 와 같은 네트워크 공격으로부터 보호한다.
- TCP 와 다르게 TIME_WAIT 상태가 존재하지 않아 반닫힘 상태를 지원하지 않는다.
- 애플리케이션 계층에 해당한다. (특정 플랫폼에 종속적이지 않음)
🔹channelFactory- 소켓 입출력 모드 설정
channel 메서드와 동일하게 입출력 모드를 설정할 수 있다.
네티가 제공하는 channelFactory 의 구현체로는 NioUdtProvider 가 있다.
🔹handler - 서버 소켓 채널의 이벤트 핸들러 설정
이 메서드를 통해 등록되는 이벤트 핸들러는 서버 소켓 채널에서 발생하는 이벤트를 수신하여 처리한다.
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
handle 메서드로 LoggingHandler를 설정했다. LoggingHandler는 네티에서 기본적으로 제공하는 코덱이다.
LoggingHandler는 ChannelDuplexHandler 를 상속받고 있으며, ChannelDuplexHandler 는
ChannelInboundHandlerAdapter,ChannelOutboundHandler 인터페이스를 상속받아 구현하고 있다.
⇒ 채널에서 발생하는 양방향 이벤트를 모두 로그로 출력하도록 구현되어있다.
handler 메서드로 등록된 이벤트 핸들러는 서버 소켓 채널에서 발생한 이벤트만 처리한다.
🔹childHandler - 소켓 채널의 데이터 가공 핸들러 설정
클라이언트의 소켓 채널로 송수신 되는 데이터를 가공하는 데이터 핸들러 설정 api이다.
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
**p.addLast(new LoggingHandler(LogLevel.DEBUG));**
p.addLast(new EchoServerHandler());
}
});
ChannelPipeline 을 사용해 LoggingHandler를 설정했다. → 클라이언트 소켓 채널의 파이프라인에 LoggingHandler 를 등록한것이다.
childHandler 메서드는 서버 소켓 채널로 연결된 클라이언트 채널에 파이프라인을 설정하는 역할을 한다.
소켓 채널 활성화 이벤트(ACTIVE)는 연결된 채널이 네티의 이벤트 루프에 등록된 이후에 발생하며 활성화 이벤트가 발생한 이후부터 네티가 데이터의 송수신을 처리할 수 있다.
🔹option - 서버 소켓 채널의 소켓 옵션 설정
소켓 옵션은 소켓의 동작 방식을 지정한다.
socket.send() 메서드가 호출되면 해당 코드는 커널의 시스템 함수를 호출한다.
-> 시스템 함수는 애플리케이션에서 수신한 데이터를 데이터 송신용 커널 버퍼에 쌓아두었다가 인터넷에 전송한다.
네티의 부트스트랩을 통해서 설정할 수 있는 소켓 옵션은 다음과 같고, 네티는 자바의 가상머신을 기반으로 하기 때문에 자바에서 설정가능한 소켓 옵션은 모두 설정 가능하다.
- TCP_NODELAY: 데이터 송수신에 Nagle 알고리즘의 비활성화 여부(기본값: false 비활성화)
- SO_KEEPALIVE: 운영체제에서 지정된 시간에 한번씩 keepAlive 패킷을 상대방에게 전송(기본값: false 비활성화)
- SO_SNDBUF: 상대방으로 송신할 커널 송신 버퍼 크기(커널 설정에 따라 다름)
- SO_RCVBUF: 상대방으로 수신할 커널 송신 버퍼 크기(커널 설정에 따라 다름)
- SO_REUSEADDR: TIME_WAIT 상태의 포트를 서버 소켓에 바인드할 수 있게 한다. (기본값: false 비활성화)
마지막 ACK 패킷을 전송한 피어는 자신이 전송한 ACK패킷이 상대방으로 도달하기 까지 소켓 상태가 일정 시간 동안 TIME_WAIT 상태가 된다.
애플리케이션 서버가 강제 종료 또는 비정상 종료로 인해 재시작하는 상황에서 사용하던 포트가 TIME_WAIT에 있다면 애플리케이션 서버는 bind 함수가 실패해 정상 동작하지 못한다.
이때 이 옵션을 사용해 해당 포트가 TIME_WAIT 되더라도 bind 되도록 설정할 수 있다.
- SO_LINGER: 소켓을 닫을 때 커널의 소신 버퍼에 전송되지 않은 데이터의 전송 대기시간을 지정 (기본값: false 비활성화)
- SO_BACKLOG: 동시에 수용 가능한 소켓 연결 요청 (SYN_RECIEVED 상태로 변경된 소켓 연결을 가지고 있는 큐의 크기를 지정하는 옵션)
+) Nagle 알고리즘 : 가능하면 데이터를 나누어보내지말고 한번에 보내라는 원칙으로 기반으로 만들어진 알고리즘
- TCP/IP에서 데이터를 전송하려면 데이터에 헤더를 포함해야하는데 헤더의 크기는 약 50바이트 이다. 보낼때마다 불필요한 50바이트의 헤더 정보로 인한 오버헤드를 방지하자는 의도
- 작은 크기의 데이터를 전송하면 커널의 송신 버퍼에서 적당한 크기로 모아서 보냄
- 이전에 보낸 패킷의 ACK 를 받아야 다음 패킷을 전송함 ⇒ 빠른 응답시간이 필요한 네트워크 애플리케이션에서는 좋지 못한 결과
🔹childOption - 소켓 채널의 소켓 옵션 설정
접속한 클라이언트 소켓 채널의 소켓 옵션 설정할 수 있다.
🔸Bootstrap API
기본적으로 ServerBootstrap API 와 동일하다
단, 클라이언트에서 사용하는 단일 소켓 채널에 대한 설정이므로 부모, 자식이라는 관계에 해당하는 API 가 없다.
⇒ option과 childOption과 같이 두가지 설정을 제공하지 않고 option메서드만 존재한다.
🔹group - 이벤트 루프 설정
단 하나의 이벤트 루프만 설정 가능하다.
🔹channel - 소켓 입출력 모드 설정
ServerBootstrap 와는 달리 클라이언트 소켓 채널만 설정할 수 있는 것이 있다.
LocalChannel | 가상 통신을 위한 클라이언트 소켓 채널을 생성 |
OioSocketChannel | 블로킹 모드의 클라이언트 소켓 채널을 생성 |
NioSocketChannel | 논블로킹 모드의 클라이언트 소켓 채널을 생성 |
EpollSocketChannel | 리눅스 커널의 epoll 입출력 모드를 지원하는 서버 소켓 채널을 생성 |
OioSctpSocketChannel | SCTP 전송계층을 사용하는 블로킹 모드의 클라이언트 소켓 채널을 생성 |
NioSctpSocketChannel | SCTP 전송계층을 사용하는 논블로킹 모드의 클라이언트 소켓 채널을 생성 |
🔹channelFactory- 소켓 입출력 모드 설정
ServerBootstrap 와 동일하다.
🔹handler - 클라이언트 소켓 채널의 이벤트 핸들러 설정
ServerBootstrap 의 handler 메서드와 동일하게 소켓 채널에서 발생하는 이벤트를 처리한다. 서버 소켓 채널이 아니라 클라이언트 소켓 채널이라는 점만 다르다.
🔹option - 소켓 채널의 소켓 옵션 설정
서버와 연결된 클라이언트 소켓 채널의 옵션을 설정한다.
'Netty' 카테고리의 다른 글
[Netty] 코덱을 이용한 HTTP 서버 구현과 사용자 정의 코덱 작성 (0) | 2025.03.25 |
---|---|
[Netty] 네티의 이벤트 실행, 채널 파이프라인, 이벤트 핸들러 (0) | 2025.03.23 |
[Netty] 네티의 주요특징 - 이벤트 기반 프로그래밍 (1) | 2025.03.15 |
[Netty] 네티의 주요특징 - 동기와 비동기, 블로킹과 논블로킹 (0) | 2025.03.14 |
[Netty] Discard & Echo 서버, 클라이언트 구현 (1) | 2025.03.12 |