본문 바로가기
JAVA

Netty 기반 경량 Http Server 구성 ( with Gradle )

by 직장인B 2022. 9. 25.

 Netty는 이벤트 기반 네트워크 어플리케이션 구성을 위한 프레임워크다. 통상 NIO 기반 비동기 프로토콜 서버 구축용 프레임워크로 더 잘알려져있다. NIO는 New input output의 약자로 Channel Buffer의 방식으로 입출력을 다루는 자바의 기본 IO 라이브러리를 뜻한다. Netty가 비동기 프로토콜 서버를 구출할 수 있는 이유는 이러한 NIO를 사용하기 때문이며 달리 말하면 NIO가 Non-Blocking IO를 지원하기 때문이다. Blocking IO란 먼저 입력된 데이터가 처리되어 출력되기 전까지 새로운 데이터를 입력받지 않는 방식을 말하며 Non-Blocking은 이와 반대로 먼저 입력된 데이터의 처리 여부와 상관없이 다른 데이터들의 입력을 받고 입력 순서과 상관없이 처리 순서에 따라 처리값을 출력해주는 IO 방식을 말한다. 이와 별개로 Netty는 이벤트 기반의 처리 매커니즘을 가진 프레임워크다. 이벤트 기반의 처리 매커니즘이라하면 특정한 처리작업을 한 개의 이벤트로 개념화하고, 각기 만들어진 이벤트를 적절한 순서로 조합하여 전체 작업을 구성하는 방식을 말한다. 이렇게 이벤트들을 조합하는 것을 Chain이라고 한다. 

 정리해 말하자면, Netty란 데이터를 Non-Blocking 방식으로 입력받아 이벤트 기반의 처리 작업을 진행한 후 출력하는 어플리케이션 구성을 위한 프레임워크이다. Netty가 흔히 사용되는 경우엔 비동기 Http 통신 웹서버, WebSocket 통신 웹서버가 있다. 이외 다른 프로토콜 통신용 어플리케이션을 만들 수도 있다고 한다. 

 

 이번 포스팅에선 Netty 를 이용해 경량의 http 통신 웹서버를 만들어보겠다. 


######

환경구성

  • Java : 11
  • Gradle : 7.5.1
  • Netty : 4.1.78.final

######

Gradle Project 

 폴더 하나를 만들고 그 안에서 gradle init 명령어를 이용해 프로젝트를 구성한다. 프로젝트를 만들면 폴더 안에 app 폴더가 생겼을 것이다. 그 아래의 src가 있고 여기 밑에 어플리케이션 로직을 쓰면 된다. 

 

gradle init

#### 
Select type of project to generate:
  2: application
####
Select implementation language:
  3: Java
###
Split functionality across multiple subprojects?:
  1: no - only one application project
###
Select build script DSL:
  1: Groovy
###  
Generate build using new APIs and behavior (some features may change in the next minor release)? 
  no
###
Select test framework:
  1: JUnit 4

 

 개발을 진행하기 전 빌드 옵션을 조금 수정해주어야 한다. 추후에 어플리케이션을 jar로 말아서 실행할 것이기 때문에 jar 관련 옵션을 설정해주고 Dependency도 추가해줘야한다. 

 

# app/build.gradle

plugins { ... }

sourceCompatibility = 11 /** 추가 */

repositories { ... }

configurations {
    implementation {
        canBeResolved(true)
    }
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
    implementation 'com.google.guava:guava:31.0.1-jre'
    implementation 'io.netty:netty-all:4.1.78.Final'
}

jar { /** 추가 */
    manifest {
        attributes (
            'Main-Class': 'gradle.netty.http.App',
            "Class-Path": configurations.implementation.collect { it.absolutePath }.join(" ")
        )
    }
} 

tasks.register('list') { /** 옵션 */
    dependsOn configurations.implementation
    doLast {
        println "classpath = ${configurations.implementation.collect { it.absolutePath }.join(" ")}"
    }
}

application { ... }
tasks.named('test') { ... }

 

 수정이 끝났으면 프로젝트가 잘 돌아가는지 테스트를 해보자.

 

gradle list			# list task 추가한 경우 이걸로 classpath를 확인할 수 있음
gradle jar			# jar 설정 이상 여부 확인
gradle build -x test		# 프로젝트 빌드 이상 여부 확인
java -jar ./app/build/libs/app.jar # 어플리케이션 구동

 

Netty Server

 그럼 다음으로 경량의 Http Server를 구성해보자! 아래는 SimpleHttpServer 라는 이름의 서버 구동 클래스다. 

 

public class SimpleHttpServer {

    private int port;

    public SimpleHttpServer(int port) {
        this.port = port;
    }

    public void run() {
        EventLoopGroup eventGroup = new NioEventLoopGroup();    /** 이벤트 그룹 */
        try {
            ServerBootstrap b = new ServerBootstrap();          /** 웹서버 객체 */
            b.group(eventGroup)                                 /** 웹서버에 이벤트 매핑 */
                    .channel(NioServerSocketChannel.class)      /** IO 채널 선택 */
                    .childHandler(new ChannelInitializer() {    /** IO 처리 Handler 구성 */
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(
                              new HttpRequestDecoder(),         /** HttpRequest 처리 */
                              new HttpResponseDecoder()         /** HttpResponse 처리 */
                            );
                        }
                    });
            ChannelFuture f = b.bind(port).sync();              /** 웹서버 동기 구동 (비동기는 await) */
            f.channel().closeFuture().sync();                   /** 웹서버 종료 지연 */
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            eventGroup.shutdownGracefully();                    /** 이벤트그룹 삭제 */
        }
    }
}

 

 다음으로 구성한 서버 클래스를 이용해서 웹서버를 띄워보자!

 

public class App {

    private static int port = 8080;

    public static void main(String[] args) {
        SimpleHttpServer server = new SimpleHttpServer(port);
        server.run();
    }
}

 

 서버 통신 테스트를 해보자. 성공적으로 연결된 것이 확인되었다. 

 

$$ ➤ nc -z localhost 8080
Connection to localhost port 8080 [tcp/http-alt] succeeded!

 

 웹서버는 띄워졌지만 이건 통신 외에 아무런 기능도 하지 못하는 깡통 웹서버다. 앞서의 SimpleHttpServer엔 Http 패킷을 받는 로직만 있지 이걸 처리해서 특정한 응답을 내보내는 로직이 없다. 이 로직을 추가해보기로 하자. 

 

Netty Server Handler

 Request 패킷을 도로 텍스트로 반환해주는 웹서버를 만들어보자. Handler 클래스를 만들어주고 ChannelInboundHandlerAdaptor를 상속받아주자. Channel은 단순히 말하자면 하나의 입출력 프로세스이다. ChannelInboundHandlerAdaptor 엔 이러한 입출력 프로세스를 처리하는 여러 메소드들이 있다. 이중 channelRead 는 프로토콜 패킷을 읽어들이는 메소드다. 이 밖에 channelRegistered, channelActive, channerlReadComplete, exceptionCaught 등의 여러 메소드들이 있다. 필요에 따라서 적절히 Override 받아서 사용하면 되겠다. 

 앞서 체인에 연결했던 http 패킷 decoder 클래스인 HttpRequestDecoder는 패킷을 읽어 Header를 HttpMessage 객체로, body를 HttpContent 객체로 반환해서 channelRead 메소드에 넘겨준다. 이때 읽을 body가 더 없으면 HttpContent 객체를  LastHttpContent 객체로 감싸서 반환한다. 이를 이용해서 HttpMessage 객체를 받아 헤더를 처리하고, HttpContent를 받아  body를 처리하고 그 후 Response를 구성한다. 

 Reponse를 만들 때의 주의사항은 앞서 체인에 연결한 http 패킷 encoder 클래스가 읽을 수 있는 형태로 만들어서 넘겨야한다는 점이다. 여기에선 HttpResponseEncoder를 사용했다. 여기에 맞추려면 DefaultFullHttpResponse클래스로 구현한 FullHttpResponse 인터페스를 사용하면 된다. 

 응답의 형식은 원하는대로 맞출 수 있다. 

 

public class MyHttpServerHandler extends ChannelInboundHandlerAdapter {
    
    StringBuilder responseData = new StringBuilder();
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("READ");

        if (msg instanceof HttpMessage) { /** Request Header Parsing */
            responseData
                    .append("====== HEADER^ ======\r\n")
                    .append(msg)
                    .append("\r\n")
                    .append("====== HEADER$ =====\r\n");
        }

        if (msg instanceof HttpContent) { /** Request Header Parsing */
            HttpContent httpContent = (HttpContent) msg;
            ByteBuf content = httpContent.content();
            if (content.isReadable()) {
                responseData
                        .append("====== BODY^ ======\r\n")
                        .append(content.toString(CharsetUtil.UTF_8).toUpperCase())
                        .append("\r\n")
                        .append("====== BODY& ======\r\n");
            }

            if (msg instanceof LastHttpContent) {
                responseData.append("\r\n\nGood Bye!\r\n");
                LastHttpContent trailer = (LastHttpContent) msg;

                FullHttpResponse response = new DefaultFullHttpResponse(
                        HttpVersion.HTTP_1_1,
                        ((HttpObject) trailer).decoderResult().isSuccess() ? HttpResponseStatus.OK : HttpResponseStatus.BAD_REQUEST,
                        Unpooled.copiedBuffer(responseData.toString(), CharsetUtil.UTF_8)
                );
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");

                ctx.write(response);
                ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
            }
        }
    }
}

 

 이렇게 만들어준 Handler를 이벤트 체인에 등록해준다. ! 

 

.childHandler(new ChannelInitializer<>() {  
        @Override
        protected void initChannel(Channel ch) throws Exception {
            ChannelPipeline p = ch.pipeline();
            p.addLast(
              new HttpRequestDecoder(),  
              new HttpResponseEncoder(), 
              new MyHttpServerHandler()	  /** Handler 추가 */
            );
        }
    });

 

Netty Server Http Communication Test

 

 코드 구성은 끝났으니 전송 테스트를 해보자! 간단하게 GET, POST 를 날려보기로 한다.

 

/GET

### GET TEST

~ ➤ curl -X GET http://localhost:8080\?param\=A
====== HEADER^ ======
DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
GET /?param=A HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
====== HEADER$ =====


Good Bye!

/POST

### POST TEST

~ ➤ curl -X POST -d "BODY CONTENT" http://localhost:8080
====== HEADER^ ======
DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
Content-Type: application/x-www-form-urlencoded
content-length: 12
====== HEADER$ =====
====== BODY^ ======
BODY CONTENT
====== BODY& ======


Good Bye!

 

성공!

 

참조 :

https://www.baeldung.com/netty#core-concepts

https://www.baeldung.com/netty#6-server-bootstrap

https://www.baeldung.com/java-netty-http-server

https://brunch.co.kr/@myner/47