Netty(Netty의 레스토랑)

문정현·2024년 12월 2일
post-thumbnail

🗨️ 유지관리 가능한 고성능 프로토콜 서버 및 클라이언트의 신속한 개발을 위한 비동기 이벤트 기반 네트워크 어플리케이션 프레임워크

개요

네티를 이해하기 쉽게 비유를 한번 해보자면 레스토랑에 비유할 수 있다

레스토랑? 갑자기 레스토랑이 무슨말인가? 나의 경우 동기/비동기의 개념이 등장하면 항상 레스토랑에 빗대어 이해한다.

만약 식당에 요리사가 한명이라면 손님이 몰리는 점심시간에 그 식당은 어떻게 되겠는가.. 아마 웨이팅(병목현상)을 못버티고 손님들이 떠나갈 것 이다(Timeout)

이런 맥락에서 Netty의 비동기 작동 방식은 마치 여러명의 요리사(EventLoopGroup)를 고용해서 급증하는 주문(트래픽)에 대응할 수 있을 것이다

  1. 고객(Client)이 테이블(Channel)에 앉아서 요청

  2. 매니저(BossGroup Thread)는 요청을 수락(Accept)하는 작업을 하고 입출력작업(I/O)을 요리사(WorkerGroup Thread)에 할당

  3. 요리사(Worker)는 요리시작(ServerInitializer)

    3.1 파이프 라인은 데이터가 서버에 도달한 후 처리하는 체인

    • 데이터 수신: 클라이언트가 보낸 데이터가 서버에 도달합니다.
    • 데이터 디코딩: 수신된 데이터를 애플리케이션이 이해할 수 있는 형태로 변환합니다.
    • 비즈니스 로직 처리: 변환된 데이터를 바탕으로 필요한 작업을 수행합니다.
    • 데이터 인코딩: 응답 데이터를 클라이언트가 이해할 수 있는 형태로 변환합니다.
    • 데이터 전송: 처리된 응답 데이터를 클라이언트로 전송합니다.

    3.2 EchoServerHandler는 비즈니스 로직

    • 채널 비/활성화와 같이 연결이 되었을 때 메시지나 종료되었을 때의 리소스 정리
    • 예외를 만났을 때 로그 기록이나 Client에게 오류 메시지 전송

주의할점은 매니저와 요리사라는 예시로 인해 상하관계, 종속적인 관계라고 오해할 수 있지만
Process - Thread가 아니라 둘다 Thread로 Boss 스레드는 새로운 연결을 수락하고, Worker 스레드는 데이터를 처리하는 역할을 맡아 수행하는 수평적 관계이다.

특장점

Why Netty?

  • 대규모 트래픽을 좀 더 효율적으로 처리할 수 있는 서버를 개발하기 위해, 많은 사람들이 기존 Tomcat + Spring MVC 환경에서 Netty + Spring Webflux 전환을 고려하고 있다.
  • 각각의 환경의 장단점이 있고 상호보완적으로 쓰일 수 있다.
  • 자바 소켓 프로그래밍에 비해 네티의 추상화로 인해 편하고 간결하게 네트워크 프로그래밍이다
    • 추상화 네티는 소켓의 모드와 상관없이 개발할 수 있도록 추상화된 전송 API를 제공. 따라서 소켓 모드를 바꾸기 위해서 데이터 송수신 부분의 로직을 고치지 않아도 된다.
  • 특징
    • 비동기
    • 이벤트 기반
    • 고성능
    • 추상화
    • 안정적
    • 빠른 속도

이벤트 기반 프로그래밍

🗨️ 이벤트를 정의하고 이벤트가 발생했을 때 실행될 코드를 준비 논 블로킹 소켓의 Selector를 사용한 I/O 이벤트 감지 및 처리도 이 종류 중 하나이다.

네트워크 프로그램에서 이벤트 발생의 주체는 소켓이며 이벤트 종류는 소켓 연결, 데이터 송수신이다.

  • 추상화 수준
    • 이벤트 추상화가 너무 고수준이면 세부적 제어가 힘들고, 저수준이면 한 동작에 대해 너무 많은 이벤트가 발생하여 성능에 악영향
    • 서버에 연결될 클라이언트의 수는 매우 가변적이기 때문에 예측이 불가능하다. 이런 관점에서 서버에서 사용하는 이벤트 기반 프레임워크의 적절한 추상화 단위는 매우 중요

소켓 통신을 이용해서 클라이언트와 서버의 연결을 유지한다

netty에서는 소켓통신을 하기 때문에 IP, port 번호가 필요하다.

개념적으로 이해한다 해도 잘 이해가 가지 않을 것이다. 코드를 보며 동작원리를 따라가 보자.

구현해보기

1. Config

해당 config에는 기본 흐름중 첫번째인 Bootstrap부분이 포함되어 있고 네티 서버의 설정을 도와준다

/**
 *  ApplicationStartupTask
 *  서버가 실행이 되고 Netty server를 시작하는 역할
 */

@Component
@RequiredArgsConstructor
public class ApplicationStartupTask implements ApplicationListener<ApplicationReadyEvent> {
    // NettyServerSocket 의존성 주입
    private final NettyServerSocket nettyServerSocket;

    // (1)
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        nettyServerSocket.start();
    }
}
🗨️ **주석 해석 및 정리**
  • (1): 어플리케이션이 완전히 준비된 후 실행되는 이벤트 핸들러,시작될 때 별도의 초기화 작업이 필요한 경우 이런 패턴을 사용하여 가능
/**
 *  NettyConfiguration
 *  Netty의 전반적인 설정이 들어있는 클래스입니다.
 *  호스트라던지 포트등 기본 값으로 들어가는 값들의 설정과 연결에 관한 설정입니다.
 * 
 */
 
@Configuration
@RequiredArgsConstructor
public class NettyConfiguration {

    @Value("${server.host}")
    private String host;
    @Value("${server.port}")
    private int port;
    @Value("${server.netty.boss-count}")
    private int bossCount;
    @Value("${server.netty.worker-count}")
    private int workerCount;
    @Value("${server.netty.keep-alive}")
    private boolean keepAlive;
    @Value("${server.netty.backlog}")
    private int backlog;

    @Bean
    public ServerBootstrap serverBootstrap(NettyChannelInitializer nettyChannelInitializer) {
        // ServerBootstrap : 서버 설정을 도와주는 class
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup(), workerGroup()) // (1) 
                .channel(NioServerSocketChannel.class)    // (2)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(nettyChannelInitializer);   // (3)
        serverBootstrap.option(ChannelOption.SO_BACKLOG, backlog);

        return serverBootstrap;
    }

    @Bean(destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup bossGroup() {
        return new NioEventLoopGroup(bossCount); // (4)
    }

    @Bean(destroyMethod = "shutdownGracefully") // (5)
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(workerCount); // (6)
    }

    @Bean
    public InetSocketAddress tcpPort() {
        // (7)
        return new InetSocketAddress(host, port); // 적절한 포트 번호와 호스트 정보로 변경해주세요.
    }
}
🗨️ **주석 해석 및 정리**
  • (1): 루프 그룹 설정
  • (2): 비동기 네트워킹을 위한 채널을 설정
  • (3): 사용자 정의 채널 초기화 클래스 설정
  • (4): 들어오는 연결을 수락하는 역할을 담당함. 거기서 사용 될 스레드 수를 정의
  • (5): 스프링 컨테이너가 종료될 때 해당 메서드가 호출되도록 한다.네티의 이벤트 루프가 안전히 종료되게 해줍니다.
  • (6): 연결된 클라이언트와의 데이터 통신을 처리하는 역할을 담당 . 거기서 사용될 스레드수를 정의
  • (7): 여기서 포트 번호와 호스트 정보를 생성하여 반환합니다.

2. NettyServerSocket

  • Netty 서버의 시작 및 종료를 관리하는 역할
  • 해당 클래스는 채널 이니셜라이져가 연결이 될 때 항상 초기화를 해준다.
@Slf4j
@RequiredArgsConstructor
@Component
public class NettyServerSocket {
    private final ServerBootstrap serverBootstrap;
    private final InetSocketAddress tcpPort;
    private Channel serverChannel;

    // (1)
    public void start() {
        try {
            // (2)
            ChannelFuture serverChannelFuture = serverBootstrap.bind(tcpPort).sync();

            // (3)
            serverChannel = serverChannelFuture.channel().closeFuture().sync().channel();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @PreDestroy // (4)
    public void stop() {
        if (serverChannel != null) {
            serverChannel.close();
            serverChannel.parent().closeFuture();
        }
    }
}
🗨️ **주석 해석 및 정리**
  • (1): ServerBootStrap 을 사용하여 서버를 시작 주입된tcpPort를 사용하여 지정된 주소의 포트에서 서버를 바인딩
  • (2): bind 호출후 sync를 호출하여 작업이 완료 될 때까지 대기 한다.
  • (3): 필드에 바인딩 작업의 결과로 반환된 channel을 저장
  • (4): 스프링 컨테이너에서 빈이 파괴되기 직전에 호출되는 메서드를 표시

3. NettyChannelInitializer

  • 새로운 연결이 수립될 때 마다 채널 파이프라인을 초기화해주는 역할
  • 덕분에 클라이언트와의 1:1 관계의 연결이 여러개 이루어 질 수 있다.
/**
 *  NettyChannelInitializez
 * 
 */ 
@Component
@RequiredArgsConstructor
public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {
    private final TestHandler testHandler;

    // (1)
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        // (2)
        ChannelPipeline pipeline = ch.pipeline();

        TestDecoder testDecoder = new TestDecoder();
        // (3)
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8), testDecoder, testHandler);

    }
}
🗨️ **주석 해석 및 정리**
  • (1): 새로운 연결이 들어올 때마다 호출된다.
  • (2): 채널 파이프라인을 들고옵니다.
  • (3): StringDecoder 는 바이트 데이터를 UTF-8로 변경합니다.testDecoder는 특정 길이만큼 읽고 문자열로 변환한다. testHandler는 연결관리 및 메시지 처리

3.1 . Decoder

  • 해당 부분은 채널 이니셜라이져에 들어가는 부분
  • 어떠한 형식으로 데이터들을 주고 받을지 정한다
/**
 *  TestDecoder
 * 
 */

@Slf4j
@Component
@RequiredArgsConstructor
public class TestDecoder extends ByteToMessageDecoder {
    private final int DATA_LENGTH = 2048;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() < DATA_LENGTH) {
            return;
        }

        // (1)
        ByteBuf buffer = in.readBytes(DATA_LENGTH);
        String message = buffer.toString(StandardCharsets.UTF_8);
        out.add(message); // String 객체를 out에 추가
        buffer.release(); // (2)
    }
}
🗨️ **주석 해석 및 정리**
  • (1): 버퍼의 내용을 String 객체로 변환
  • (2): 버퍼의 참조 카운트를 감소시켜 메모리 누수를 방지

4. Handler

  • Netty 서버의 가장 핵심적인 부분
  • 클라이언트와 연결을 수립하고 데이터를 쓰고 읽는 등의 중요한 로직들을 구현하는 클래
/**
 *  TestHandler
 *  Netty server 의 핵심부분 클라이언트와의 연결 수립, 데이터 읽기 및 쓰기 , 예외처리등의 로직이 담겨있다.
 */
@ChannelHandler.Sharable // (1)
public class TestServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf)msg;
        System.out.println("Server received : " + in.toString(CharsetUtil.UTF_8));
        ctx.write(in); // (2)
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) // (3)
                .addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();	// 채널 닫기
    }
}
🗨️ **주석 해석 및 정리**
  • (1): 여러 채널에서 핸들러 인스턴스를 공유할 수 있음을 나타낸다.
  • (2): 받은 메시지를 발신자에게로 Echo 시킨다.
  • (3): 대기중인 메시지를 플러시하고 채널을 닫는다.

참고
https://velog.io/@akfls221/Netty
https://github.com/zbum/netty-spring-example

profile
기록 == 성장

0개의 댓글