개발자

“쉽게 쓰는 정적 HTTP 서버” 자바 심플 웹 서버의 이해

Matthew Tyson | InfoWorld 2023.12.07
자바 18 릴리스(2022년 3월)에서 가장 유용한 새로운 기능 중 하나는 HTTP 파일 서버를 손쉽게 구동하고 구성할 수 있는 심플 웹 서버(Simple Web Server)다. 새로운 심플 웹 서버의 기능을 알아보자.
 

명령줄의 자바 심플 웹 서버 

자바의 새로운 jwebserver 명령을 이용하면 기본적인 웹 서버를 간단히 실행할 수 있다. 파이썬에서 인기 있는 SimpleHTTPServer 툴과 비슷하다. 가장 먼저 할 일은 자바 18 이상 릴리스를 실행 중인지 확인하는 것이다. Java – version을 입력해 현재 실행 중인 릴리스를 확인한다. SDKMan을 사용해 JDK 설치를 관리하는 것이 좋다. 여러 버전을 번갈아 사용할 때 특히 유용하다. 

자바 심플 웹 서버로 할 수 있는 가장 기본적인 일은 포트 8000에서 현재 디렉터리를 제공하는 것이다. <예시 1> 명령을 입력하면 된다. 

<예시 1> 인수 없는(no-arg) 웹 서버 
$ jwebserver

Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /home/matthewcarltyson and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/

여기서 브라우저를 사용해 localhost:8000으로 이동하면 <화면 1>과 같은 파일 시스템 목록을 볼 수 있다. 
 
<화면 1> 8000 포트에서 실행 중인 심플 웹 서버 인스턴스

명령줄에서 심플 웹 서버를 세부적으로 조정하기 위해 해야 하는 몇 가지 작업이 있다. 예를 들어 포트, 바인딩할 주소(수신 대기할 네트워크 인터페이스), 제공할 디렉터리를 변경하는 것 등이다. <예시 2>는 포트 8080, 모든 인터페이스, 그리고 /foo/bar 디렉터리에서 수신 대기하는 방법을 보여준다. 

<예시 2> 포트 8080, 모든 인터페이스, /foo/bar에서 수신 대기 
$ jwebserver -b 0.0.0.0 -p 8081 -d /foo/bar

You can get a list of all the options with $ jwebserver -h.

<예시 2>에서 볼 수 있듯, jwebserver 명령줄 툴은 가능한 가장 간단한 구문을 사용해 정적 파일을 제공한다. 다음으로, 심플 웹 서버 API를 살펴보자. 
 

자바 심플 웹 서버 API 사용하기 

심플 웹 서버 자바독(Javadoc)은 API에 대해 알아볼 때 가장 먼저 봐야 할 자료다. SimpleFileServer 클래스는 com.sun.net.httpserver 패키지에 있다. 이 패키지에는 웹 서버를 구축하는 더 오래된 하위 수준의 API도 포함돼 있다. httpserver 서버 패키지는 요구사항을 간소화하기 위해 이 기능을 확장한다. jwebserver CLI 툴은 SimpleFileServer를 사용해서 작동한다. 이 툴은 프로그램을 통해서도 사용할 수 있다. 

SimpleFileServer 클래스는 GET과 HTTP/1.1만 처리하지만, 이 클래스를 사용해 몇 가지 흥미로운 일을 할 수 있다. 예를 들어 이 심플 웹 서버 사용 입문서에는 구글 자바 인메모리 파일 시스템 프로젝트를 사용해서 이 핸들러의 파일 시스템을 가짜로 만드는 방법이 나와 있다. 여기서는 인메모리 파일 시스템 개념을 사용해 SimpleFileServerFileHandler를 수정해서 메모리에서 가상 파일 시스템을 제공한다. 그런 다음 httpserver 패키지를 사용해 POST를 처리해서 가짜 파일 시스템에 가짜 파일을 추가한다. 
 

메모리에서 가상 파일 시스템 제공 

우선 다음 명령을 사용해 간단한 메이븐(Maven) 프로젝트를 만든다. 
 
$ mvn archetype:generate -DgroupId=.com.infoworld -DartifactId=jsws -DarchetypeArtifactId=maven-archetype-quickstart

/jsws 디렉터리로 이동한다. pom.xml에서 컴파일러와 소스 버전을 18로 설정한다(이 설명 참조). 그 다음 <예시 3>과 같이 종속성 항목에 구글 jimfs를 추가한다. 

<예시 3> 구글 자바 인메모리 파일 시스템 종속성 
<dependency>
  <groupId>com.google.jimfs</groupId>
  <artifactId>jimfs</artifactId>
  <version>1.3.0</version>
</dependency>

이제 src/main/java/App.java 파일을 수정해서 가짜 파일 시스템을 제공할 수 있다. 이 작업을 위한 코드는 <예시 4>에서 볼 수 있다. 

<예시 4> SimpleFileServer로 인메모리 파일 시스템 제공 
package com.infoworld;

import java.util.List;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.sun.net.httpserver.SimpleFileServer;

import static com.sun.net.httpserver.SimpleFileServer.OutputLevel;

public class Mem {
  private static final InetSocketAddress ADDR =
    new InetSocketAddress("0.0.0.0", 8080);

  public static void main( String[] args ) throws Exception {
    Path root = createDirectoryHierarchy();
    var server = SimpleFileServer.createFileServer(ADDR, root, OutputLevel.VERBOSE);
    server.start();
  }

  private static Path createDirectoryHierarchy() throws IOException {
    FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
    Path root = fs.getPath("/");

    Files.createDirectories(root.resolve("test/test2"));

    Path foo = fs.getPath("/foo");
    Files.createDirectory(foo);

    Path hello = foo.resolve("hello.txt"); 
    Files.write(hello,  List.of("hello", "world"), StandardCharsets.UTF_8);

    return root;
  }
}

<예시 4>는 구글의 오픈소스 jimfs 라이브러리를 사용해 표준 로컬 파일 시스템 API를 시뮬레이션하는 것이다. 이는 java.nio.file API를 구현하지만 가상 파일 시스템과 같이 모든 작업을 메모리 내에서 수행한다. 이 라이브러리를 사용해 자체 디렉터리 경로와 파일을 프로그램을 통해 정의할 수 있다. 따라서 자체 가상 디렉터리 구조를 만들어 SimpleFileServer에 파일 핸들러로 제공할 수 있다. 이제 다음과 같이 프로그램을 통해 SimpleFileServer를 구성한다. 
 
var server = SimpleFileServer.createFileServer(ADDR, root, OutputLevel.VERBOSE);

이는 명령줄에서 본 것과 같이 바인딩할 인터넷 주소를 받는다. 이 경우 지정되지 않은 인터페이스와 포트 8080을 전달한다. 그 다음은 파일 시스템 루트다. 이 예제에서는 createDirectoryHierarchy()로 생성한 Path 객체를 전달한다. 

createDirectoryHierarchy() 메서드는 jimfs를 사용해 다음과 같이 Path 객체를 빌드한다. FileSystem fs = Jimfs.newFileSystem(Configuration.unix());. 그런 다음 Path에 파일과 디렉터리를 채운다. 콘텐츠와 함께 경로와 파일을 생성하기 위한 jimfs API는 이해하기가 어렵지 않다. 예를 들어 Path hello = foo.resolve("hello.txt");를 사용해 경로를 만들 수 있다. 대부분의 객체는 일반적인 자바 NIO 경로처럼 사용할 수 있다. 이제 이 코드를 실행하고 localhost:8080을 열면 일반적인 파일 서버와 같은 방식으로 디렉터리 목록을 탐색하면서 파일 콘텐츠를 볼 수 있다. 

이 개념을 한 단계 더 발전시켜 새 파일을 업로드하는 기능을 추가해 보자. com.sun.net.httpserver 패키지를 사용해 인메모리 파일 시스템에 새 파일을 업로드할 POST 요청을 받을 수 있다. <예시 5>에서 이 과정을 볼 수 있다. 

<예시 5> 인메모리 파일 시스템에 새 파일 업로드하기 
package com.infoworld;

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;

import java.net.InetSocketAddress;
import java.util.List;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.sun.net.httpserver.SimpleFileServer;

import static com.sun.net.httpserver.SimpleFileServer.OutputLevel;

public class App {
  public static void main( String[] args ) throws Exception {
    // same config...
    server.start();

    // Create the HTTP server with the UploadHandler using the same 'root' path
    HttpServer httpServer = HttpServer.create(new InetSocketAddress("0.0.0.0", 8081), 0);
    httpServer.createContext("/upload", new UploadHandler(root));
    httpServer.start();
  }

  private static Path createDirectoryHierarchy() throws IOException {
    // same ...
  }

  // Handler to process POST requests and upload files
  static class UploadHandler implements HttpHandler {
    private final Path root;

    public UploadHandler(Path root) {
      this.root = root;
    }

    @Override
       public void handle(HttpExchange exchange) throws IOException {
       if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) {
         String filename = exchange.getRequestHeaders().getFirst("filename");

         if (filename != null) {
           Path newFilePath = root.resolve(filename);
          try (OutputStream os = Files.newOutputStream(newFilePath)) {
            exchange.getRequestBody().transferTo(os);
          }
          String response = "File uploaded successfully: " + filename;
          exchange.sendResponseHeaders(200, response.getBytes(StandardCharsets.UTF_8).length);
          try (OutputStream os = exchange.getResponseBody()) {              
            os.write(response.getBytes(StandardCharsets.UTF_8));
          }
        } else {
          exchange.sendResponseHeaders(400, -1); // Bad Request
        }
      } else {
        exchange.sendResponseHeaders(405, -1); // Method Not Allowed
      }
    }
  }
}

<예시 5>를 보면 대부분 클래스는 같하지만 HttpServer 인스턴스, 포트 8081에서 수신 대기한다. 이는 업로드된 데이터를 받아 사용해 createDirectoryHierarchy에서 만든 루트 경로에 새 파일을 작성하는 맞춤형 uploadHandler로 구성된다. 테스트하려면 다음과 같이 전체 서버 클러스터를 실행한다. 
 
$ mvn clean install exec:java -Dexec.mainClass="com.infoworld.Mem"

<예시 6>과 같이 CURL 요청으로 서버에 새 파일을 푸시할 수 있다. 

<예시 6> CURL POST로 파일 푸시 
$ touch file.txt
$ curl -X POST -H "filename: file.txt" -d "@file.txt" http://localhost:8081/upload

File uploaded successfully: file.txt

localhost:8080/ 파일 목록을 다시 로드하면 새 file.txt를 볼 수 있고, 이 파일을 클릭해 내용을 볼 수 있다. 
 

빠르고 간편하고 유연한 웹 서버 

심플 웹 서버는 자바 툴셋에 추가된 반가운 기능이다. CLI를 통해 파일을 매우 간단히 호스팅할 수 있고, 특히 하위 수준의 HttpServer API와 함께 사용하면 API로 여러 가지 흥미로운 작업을 할 수 있다.
editor@itworld.co.kr 
Sponsored

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.