Spring/Framework 탐방 zone

[Netty] Netty가 뭐에요? - 4탄 : Spring MVC 와 Spring Web Flux 그리고 Netty 예시 코드 탐방

공대키메라 2025. 10. 7. 17:07

 

지난 시간에는 Netty의 공식문서에 있는 특징에 대해 알아보고 NIO 에 대한 예시 코드를 작성해보았다.

(지난글은 여기 클릭!)

 

그리고 Architecture 초입부를 읽었는데... 그 전에!

 

 

이번 시간에는 자바공화국 한국에서 가장 많이 사용하는 Spring framework 와 Netty Framework을 간단하게 비교해보고

 

Netty 공식문서에서 제공하고 있는 예시 코드를 탐방해볼 것이다.

 

 

개인적으로 특정 기술에 대해 잘 모르겟으면, 필자 키메라는 비교군을 찾는 것이 좋다고 생각한다.

 

기존의 Spring은 뭐가 부족했길레 Netty 가 필요했던 것이고 두 개의 성격을 비교해보고자 한다.

 

물론 이전의 글 중 하나인 [Netty] Netty가 뭐에요? - 2탄 : Netty가 필요하기 까지 를 보면 짐작할 수 있겠지만,

 

이에 대한 근거를 좀 더 확인해보고 가려고 한다.

 

 

 

바로 고! 올ㅋ


0. Reactor Pattern

리액터 패턴? 이것이 무엇인가?

 

잘 몰랐는데, 비동기 프로그래밍을 할 때 자주 사용하는 패턴이라고 한다. :)

(참고글 :Reactor Pattern in Java: Mastering Non-blocking Event-Driven Architectures)

 

 

해당 패턴의 목적은 적은 수의 스레드(직원)로 동시에 들어오는 수많은 요청(주문)을 효율적으로 처리하기 위함이다.

 

 

 

글의 Reactor는 Netty의 EventLoop, Handler는 Netty의 ChannelHandler랑 매치되니 미리 참고하자!

 

 

1. Spring MVC

왜 뜬금없이 Spring MVC에 대해 다시 보냐고?

 

우리는 2탄 글에서 Servlet에 대해서 알아보았다. (이전글 - [Netty] Netty가 뭐에요? - 2탄 : Netty가 필요하기 까지 )

 

Spring 에도 MVC 패턴에서 확인할 수 있는 Servlet 구현체가 있는데, Dispatcher Servlet이다.

 

 

사실 Spring MVC에 대한 소개를 공식문서에서 할 때 Servlet 기반으로 만들어졌다는 글이 똭! 그대로 적혀있다.

 

Spring 공식 문서에 따르면, Spring Web MVC는 Servlet API 위에 구축된 원조 웹 프레임워크로,
Spring Framework 초창기부터 포함되어 왔다.

공식 명칭은 Spring Web MVC지만, 일반적으로는 Spring MVC로 더 잘 알려져 있다.
(출처 : https://docs.spring.io/spring-framework/reference/web/webmvc.html)


Servlet이 뭐였는지 지난 글을 다시 보자면

 

서블릿은 요청-응답 프로그래밍 모델(request-response programming model) 을 통해 접근되는 애플리케이션을 호스팅하는 서버의 기능을 확장하기 위해 사용되는 Java 프로그래밍 언어 클래스이다.

 

그리고 Serlvet는 그냥 규격이고 직접 구현은 안되었다는 단점이 존재했다.

 

이를 Spring 에서는 Spring MVC, 혹은 Spring Web MVC로 하나의 프레임워크로 제공하는 것이다.

 

작동 순서를 정리하면 다음과 같다.

 

Spring MVC 전체 동작 순서

 

필자는 그림을 찾아보고, 강의도 들어봤지만, 내가 직접 이해한 그림을 다시 그렸다.

 

특히 화살표에 주의하라! 양방향 화살표의 경우 데이터를 주고 받는다면, 화살표가 한 방향으로만 향한다면 데이터의 맞교환이 없는것으로 이해하라!

 

Dispatcher Servlet의 역할
HandlerAdapter에게 받은 Model을 View(화면 객체)에게 넘겨주면서 '이거 가지고 섞어서 완성해!'라고 시키는 역할

1. 요청 수신 (Client → DispatcherServlet)
사용자(클라이언트)가 웹 브라우저를 통해 URL(예: /users/profile)로 요청을 보냄.

2. 핸들러 조회 (DispatcherServlet → HandlerMapping)
HandlerMapping: 요청 URL과 매칭되는 Controller(정확히는 핸들러)를 찾아서 반환.'

3. 핸들러 어댑터 조회 및 실행
DispatcherServlet은 찾은 Controller를 바로 실행하지 않고, HandlerAdapter를 통해 실행.

4. 비즈니스 로직 실행 (Controller ↔ Service ↔ Repository)

5. 결과 반환 (Controller → DispatcherServlet)

5.1) [Controller]가 View Name과 Model을 만듦. 이 둘은 [HandlerAdapter]를 타고 [DispatcherServlet]으로 들어감.    
5.2) [DispatcherServlet]은 View Name만 떼어서 [ViewResolver]에게 던짐.

6. [ViewResolver]는 [View] 객체를 찾아 Servlet에게 줌.

7. 마지막으로 [DispatcherServlet]은 Model을 들고 [View]에게 가서 "완성해!"라고 지시함.

 

 

뭐... 그렇다. 자꾸 반환을 해야 하는데 화살표를 하나로 그른 부분들이 있고, 내가 이해가 안돼서 새롭게 정리했다.

 

2. Spring Web Flux

Spring Framework 5.0(2017년)에서 reactive-stack 웹 프레임워크인 Spring WebFlux가 추가되었다.

 

주요 특징

- 완전한 Non-blocking 지원

- Reactive Streams 백프레셔 지원

- Netty, Undertow, Servlet 컨테이너 등 다양한 서버에서 실행 가능 기존

 

Spring MVC가 Servlet API에 특화되어 있었다면, WebFlux는 비동기 Non-blocking 처리에 최적화된 프레임워크다.

(출처: https://docs.spring.io/spring-framework/reference/web/webflux.html)

 

이전에는 비동기 프로그래밍은 직접 해야 하거나 Netty를 사용해야 한 것 같다.

 

여기서 reactive-stack, reactive streams, back pressure 그리고 Undertow 가 뭐고, Netty도 지원한다는게 무슨 말인지

 

또한 Servlet container같은 서버에서도 작동한다는게 무슨 말인지 이해가 안됐다.

 

Reactive streams?

Reactive Streams는 백프레셔를 지원하는 비동기 컴포넌트 간의 상호작용을 정의하는 스펙이다(Java 9에서도 채택됨).

 

동작 방식 

- Publisher(데이터 생성자): 예를 들어 데이터 저장소

- Subscriber(데이터 소비자): 예를 들어 HTTP 서버

- Subscriber가 Publisher의 데이터 생성 속도를 제어할 수 있음

 

이를 통해 데이터 생산자와 소비자 간의 속도 차이로 인한 문제를 해결한다.

 

Reactive Streams에 대한 자세한 내용은 별도로 찾아보는 것을 추천한다.

 

Back Pressure

Back Pressure는 유체역학(Fluid Dynamics)에서는 유동유체의 흐름이 방해를 받아 유체의 흐름방향과 반대방향으로 유체에 가해지는 압력으로,

 

기본적으로 일정한 유량이 흐르지만 파이프의 크기가 작아지면 속도가 빨라진다. (베르누이 방정식)

 

에너지 보존 법칙에 따르면 속도는 빨라지지만 압력은 낮아질 것이다. 유량의 에너지는 마찰력이 0이라는 가정 하게 동일하게 유지되기 때문이다. (갑분 유체역학 시간...)

 

그런데 여기서 파이프가 막혀버린 것이다. 그러면 압력이 증가한다. 뒤로 나오는 역압력(back pressure)이 증가한다.

 

 

이를 IT분야에서 차용한다면 백프레셔는 이미 받은 요청 내에서 데이터 흐름이 막혔을 때 발생한다.

 

데이터 흐름을 눌러서 천천히 들어오게 하는 것이다.

 

즉, Spring Web Flux 에서 말하는 백프레셔 지원은 우리가 서버 → 데이터 소스 간의 흐름 제어를 할 수 있도록 도와준다는 말이다.

 

Spring Web Flux - Server 섹션

 

공식문서에서 해당 섹션의 글을 잘 보면 Netty를 기본값으로 사용한다고 한다.

 

그러니까 netty를 기본적으로 application 서버로 사용한다는 말이다.

 

결국 Spring Web Flux는 Reactive Streams를 기존에 잘 사용하도록 도와주는 framework로 따로 기존에 있던 서버를 호환해서 사용할 수 있도록 지원해주는 비동기 프레임워크이다.

 

기본적으로 Netty서버를 기반으로 하기에 Netty를 잘 이해하면 결국 추후에 Spring Web Flux같은 프레임워크도 이해하는게 어렵지 않을 거라는 판단이다.

 

 

그런데 이해가 안되는 부분이 있는데,

 

Netty는 공식문서에서 framework라고 소개했는데, Spring Web Flux에서는 서버로 사용한다니 이게 무슨 말인가?

 

Netty는 빠른 유지가능한 고성능 프로토콜 서버 클라이언트 개발을 위한 비동기 이벤트기반 네트워크 어플리케이션 프레임워크다. 

 

 

서버 클라이언트 개발을 위한 비동기 이벤트기반 네트워크 어플리케이션 프레임워크

 

라고 대놓고 공식문서 맨 앞에 대문짝만하게 명시가 되어 있다.

 

 

이전에 NIO 예시 코드를 물어봐서 작성을 해봤을 때 NIO에서 client 역할도 하고, server역할도 하긴 했다.

 

아하! 그래서 이전에 NIO 예시 코드로 파일도 여러개를 작성하였다.

 

client과 server역할을 하는 코드를 따로 분리해서 client에서 server에 호출하는 구조였다.

(참고할 이전 코드는 여기 클릭 - [Netty] Netty가 뭐에요? - 3탄 : Netty의 특징과 구조 및 NIO 테스트)

 

 

결론적으로 Netty가 서버 클라이언트 개발을 위한 비동기 이벤트기반 네트워크 어플리케이션 프레임워크라면

 

Spring WebFlux는 Reactive Streams 기반으로 웹 애플리케이션 개발을 편하게 해주는 비동기 웹 프레임워크로 다양한 서버를 지원한다.

 

Spring Webflux위에 포함된 것이 아닌 지원해주는 것으로 Netty를 실행할 수 있는 구조이다.

 

Spring Webflux 에 대한 공식문서를 보면 더 많은 내용이 있지만, 이번에는 이정도로만 보겠다.

 

3. Netty의 구조 그림 파악

사실 이전에 2025년에 작성한 글에서는 해당 섹션이 없었는데, 2026 신년에 Netty의 큰 흐름을 대강 파악하고, 이를 보여주는것이 추후 글을 읽고 이해하는데 더 도움이 된다 판단했다.

 

 이전 Netty 3탄에서 보았던 NIO에서 제공하는 Non-Blokcing I/O 의 구조를 다시 보고 가자.

 

 

Thread / Event Loop
하나의 스레드가 이벤트 루프처럼 동작해서, Selector를 호출해 준비된 채널이 있는지 감시함.

 

Selector
여러 채널을 등록해놓고, 어떤 채널이 읽기/쓰기 가능 상태인지 감시. select() 등의 메소드를 통해 “준비된” 채널 집합을 리턴.

 

Channel (SelectableChannel 계열)
네트워크 소켓, 파일 등의 I/O 대상. 논블로킹(non-blocking) 모드로 설정 가능. 데이터를 읽거나 쓰려면 버퍼와 상호작용함.

 

Buffer
채널로부터 읽은 데이터를 저장하거나, 채널에 보낼 데이터를 담는 메모리 버퍼. position, limit, capacity 같은 속성을 통해 현재 읽기/쓰기 위치 관리.

 

그러면 Netty는 어떤 그림일까? 

 

직접 그림. 참고는 https://www.alibabacloud.com/blog/essential-technologies-for-java-developers-io-and-netty_597367

 

해당 부분에 대한 자세한 설명은 다음 글로 옮겼다. (여기 클릭!)

 

4. Netty 예시 찾아보기 - DiscardServer와 내부 class 파악

그려면 드디어... 실제 예시 코드를 찾아 보려고 한다.

 

user guide를 공식 사이트에서 예시들을 다양하게 제공하고 있다.

 

필자는 4.x 버전의 가이드를 보려고 한다. (회사에서 그걸 봄)

 

첫 번째 예시로 writing a Discard Server를 소개한다.

 

DiscardServerHandler.java

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/*
 * Copyright 2012 The Netty Project
 * Licensed under the Apache License, Version 2.0
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // Discard the received data silently.
        ((ByteBuf) msg).release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

 

5. 다시 예시 확인하기 - DiscardServer

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/*
 * Copyright 2012 The Netty Project
 * Licensed under the Apache License, Version 2.0
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // Discard the received data silently.
        ((ByteBuf) msg).release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

 

 

코드를 읽어보면, Channel 내부의 ChannelPipeline에서 관리하는 DiscardServerHandler를 구현한 것이다.

 

release()하는 코드는 channel로 넘어오는 msg를 그대로 버린다. 

 

exceptionCaught 는 I/O 작업시 혹은 이벤트 처리시에 에러가 날 시 에러를 처리한다. 

주석에 적힌대로 에러가 발생하면 연결을 끊는다. 

 

일반적으로 데이터를 처리하려면 연결을 열고, 처리하고 닫는게 순리니까... 

 

ByteBuf는 기존에 Java 에서 제공하는 ByteBuffer을 좀 더 쉽게 사용할 수 있도록 Netty에서 제공하는 class이다.

 

우리는 Handler, 즉 channl내로 들어오는 응답을 처리하는 handler를 만든 것이다. 

 

 

그러면 이것을 실제로 쓸 수 있도록 해야 하지 않나?

 

server를 만들어서 Handler를 배정해야한다.

 

DiscardServer.java

import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
    
/*
 * Copyright 2012 The Netty Project
 * Licensed under the Apache License, Version 2.0
 */
public class DiscardServer {
    
    private int port;
    
    public DiscardServer(int port) {
        this.port = port;
    }
    
    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // (3)
             .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(new DiscardServerHandler());
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
    
            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)
    
            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
    
    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new DiscardServer(port).run();
    }
}

 

이제 그럼 테스트를 해봐야지?

 

 

 

아 이러면... 넘어오는지 아닌지 확인해야하니 코드를 잠시 변경해보겠다.

 

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/*
 * Copyright 2012 The Netty Project
 * Licensed under the Apache License, Version 2.0
 * Modifyed By : 공대키메라 (tistory)
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // Discard the received data silently.
        ByteBuf msg1 = (ByteBuf) msg;
        String string = msg1.toString();
        System.out.println("string = " + string);
        msg1.release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

 

그리고 요청을 보내면 다음과 같이 나올것이다.

 

 


 

이전 글과 다르게 현재 2026년 1월 25일 기준으로 Channel관련 설명을 해당 글에서 제외했다.

 

이유는 간단한다. 뭔가 큰 맥락에서 세부적인 구조에 접근을 해야 하는데, 그게 아니라 엉망징찬이 되어버려서 그랬다.

 

DiscardServer 예제는 그냥 아 그런가 보다~ 하고 감으로 잡아주길 바란다. 

 

이 뒤에 글은 [Netty] Netty가 뭐에요? - 5탄 : Netty Architecture - ByteBuf Summary 읽기 인데

 

4탄과 5탄 사이에 새롭게 Netty의 구조에 대해 더 심도있게 이해할 수 있는 섹션을 끼워넣으려고 한다.

 

 

내가 고민을 해봤는데, 이제 30이 넘은 나이지만 나름 공부를 열심히 한거같은데 들인 시간에 비해서 좀 더딘 느낌이 있었다.

 

아무래도... 큰 그림을 무조건 잡고 작업을 해야 하는데 그렇게 하지 못함이 크고, 

 

이는 글을 정리하는 부분에서도 마찬가지라고 생각한다. 

 

 

고로! 다시 새롭게 정비중이니 양해 바란다.(어짜피 나만 보지만 올ㅋ)


 

 

사실 불과 1년 전만해도 아무 강의나 막 보고 그랫었는데,

 

이러한 이해를 바탕으로 뭔가 하지 않으면 정말 금방 까먹는다는것을 깨닫고 (몇년이 걸린거냐 에휴 바보 ㅠㅠ)

 

현재 이렇게 찾다보니 정말 많은 것을 배울 수 있었다.

 

 

앞으로도 공식문서를 보고, AI와 함께 찾아보면서 학습하려고 한다.

 

또한, 공부를 진행하면서 부족한 부분이나 그러한 부분은 여태 좀 부족할 수 있으나 생각이 나면 다시 덫붙여서 사용하려고 한다. 

 

 

아, 그리고 Effective Java도 2년전에 보려다가 에잇 하고 왜 필요한가 하고 치웠는데... 다시 이 책을 꺼낼때가 된 것 같다.

 

볼 당시에는 아... 이게 뭔 말인가 정말 필요한건가 했는데, 이 책에서 각 챕터가 주는 의미를 잘 이해하지 못했었다. 

 

역시 고기도 먹어본 놈이 먹는다고 좀 뭘 알아야 아는것 같다. 

 

 

다음 글에서는 더 많은 예시와 여러 클래스들에 대해 알아볼 예정이다.

 

물론! 그 이전에 먼저... Netty의 Architecture의 ByteBuf 에 대해 알아보려고 한다. 

 

 

참고

https://docs.spring.io/spring-framework/reference/web/webflux.html

https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html

https://netty.io/wiki/user-guide-for-4.x.html

https://netty.io/4.0/api/io/netty/channel/ChannelHandler.html

https://medium.com/@akhaku/netty-data-model-threading-and-gotchas-cab820e4815a

https://www.alibabacloud.com/blog/essential-technologies-for-java-developers-io-and-netty_597367

https://java-design-patterns.com/patterns/reactor/