HTTP 에 대해서 간단히 알아보고 직접 구현해보자.
웹의 핵심 요소인 HTTP는 웹 개발자에게 있어서 필수적으로 알아야 하는 지식이다. 그러나 상당수의 개발자들이 HTTP가 어떠한 원리로 동작하는지 제대로 알지 못하고 있다. 이번 과정을 통해 HTTP 통신을 처리하는 웹 서버가 의외로 간단한 원리로 구현되어 있다는 걸 알게 될 것이다. 직접 간단한 웹 서버를 구현해보면서, HTTP를 제대로 이해하고 동시에 Spring Web MVC가 어떤 편의를 제공하는지 절실하게 느껴보자.
Java HTTP Server 를 이용해서 훨씬 더 쉽게 HTTP Server 를 만들어보자.
지금까지 우리는 저수준으로 했다. 자바에는 고수준의 API 가 준비되어 있다. 이걸 사용해보자.
Java HTTP Server 는 내부적으로 NIO 를 쓴다. (Non-Blocking IO)
우리가 그냥 하는 것보다 훨씬 더 효율적으로 되는 걸 기대할 수 있다.
package com.ahastudio.http.server;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
public class App {
public static void main(String[] args) throws IOException {
App app = new App();
app.run();
}
private void run() throws IOException {
// 서버 객체 준비
InetSocketAddress address = new InetSocketAddress(8080);
HttpServer httpServer = HttpServer.create(address, 0);
httpServer.start();
}
}
curl localhost:8080
을 해보자. 404 에러는 나지만 성공적으로 페이지가 나온다.
이번에는 주소에 따라 처리하도록 해보자.
HTTPHandler 인터페이스가 가지고 있는 메소드가 하나라서 다음과 같이 람다를 사용할 수 있다.
httpServer.createContext("/", (exchange) -> {
// TODO
});
package com.ahastudio.http.server;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
public class App {
public static void main(String[] args) throws IOException {
App app = new App();
app.run();
}
private void run() throws IOException {
InetSocketAddress address = new InetSocketAddress(8080);
HttpServer httpServer = HttpServer.create(address, 0);
// URL (정확히는 Path) 에 핸들러 지정
httpServer.createContext("/", (exchange) -> {
});
httpServer.start();
}
}
앞 코드에서도 계속 써주긴 했다.
httpServer.start();
위에서 Listen 까지 코드를 작성하고 실행해보면 물려있는 걸 볼 수 있다. 아무것도 처리를 안해줘서 묶여있는 것이다.
우리는 무엇을 알아야 할까? HTTP 요청 메시지을 알고 싶다.
얘네 전부 exchange 에서 얻을 수 있다.
http localhost:8080 name=tester
을 해보자. 성공적으로 데이터가 나온다.
package com.ahastudio.http.server;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;
public class App {
public static void main(String[] args) throws IOException {
App app = new App();
app.run();
}
private void run() throws IOException {
InetSocketAddress address = new InetSocketAddress(8080);
HttpServer httpServer = HttpServer.create(address, 0);
httpServer.createContext("/", (exchange) -> {
// 1. Request
String method = exchange.getRequestMethod();
System.out.println("Method: " + method); // `HTTP MEthod` 출력 결과: GET
URI uri = exchange.getRequestURI();
String path = uri.getPath();
System.out.println("Path: " + path); // `path` 출력 결과: /
Headers headers = exchange.getRequestHeaders();
for (String key : headers.keySet()) {
List<String> values = headers.get(key);
System.out.println(key + ": " + values);
// `Headers` 출력 결과 ⬇️
// Accept: [*/*]
// Host: [localhost:8080]
// User-agent: [curl/8.1.2]
}
InputStream inputStream = exchange.getRequestBody();
byte[] bytes = inputStream.readAllBytes();
String body = new String(bytes);
System.out.println(body);
// 요청하면서 넣은 데이터가 없기 때문에 아무것도 안 나온다
// httpie 를 이용하면 쉽게 Body에 JSON 데이터를 넣을 수 있다.
// http localhost:8080 name=tester
// `Body` 출력 결과 ⬇️
// {"name": "tester"}
});
httpServer.start();
}
}
curl localhost:8080
을 해보자. 성공적으로 결과가 나온다.
Response 를 안 주니까 여전히 물려있다.
Response 를 할 때 HTTP Status Code
와 Content-length
는 항상 줘야한다. 이때 Content-length 바이트로 잡아야 한다. 그래서 중간에 바이트로 한 번 변환 후에 길이로 넘겨준다.
`byte[] bytes = content.getBytes();`
그리고 재밌는건 브라우저에서 /
아래에 어떤 경로를 추가해도 같은 곳으로 요청을 한다.
그래서 만약 따로 해주고 싶다면 다음 코드로 주소 계속 추가해주면 다른 것도 처리할 수 있다.
httpServer.createContext("path_name", (exchange) -> { // TODO }
중복되는 코드가 많아서 Refactor -> Extract Method
로 함수 추출을 해서 작성했다.
package com.ahastudio.http.server;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;
public class App {
public static void main(String[] args) throws IOException {
App app = new App();
app.run();
}
private void run() throws IOException {
InetSocketAddress address = new InetSocketAddress(8080);
HttpServer httpServer = HttpServer.create(address, 0);
// Path : "/"
httpServer.createContext("/", (exchange) -> {
// 1. Request
displayRequest(exchange);
// 2. Response
String content = "Hello, world\n";
sendContent(exchange, content);
});
// Path : "/hi"
httpServer.createContext("/hi", (exchange) -> {
// 1. Request
displayRequest(exchange);
// 2. Response
String content = "Hi, world\n";
sendContent(exchange, content);
});
httpServer.start();
}
// Request 에서 쓰는 함수 (이름 짓기가 애매해서 displayRequest로 했다. request 를 화면에서 보는 거니까?)
private void displayRequest(HttpExchange exchange) throws IOException {
String method = exchange.getRequestMethod();
System.out.println("Method: " + method);
URI uri = exchange.getRequestURI();
String path = uri.getPath();
System.out.println("Path: " + path);
Headers headers = exchange.getRequestHeaders();
for (String key : headers.keySet()) {
List<String> values = headers.get(key);
System.out.println(key + ": " + values);
}
InputStream inputStream = exchange.getRequestBody();
String body = new String(inputStream.readAllBytes());
System.out.println(body);
}
// Response 에서 쓰는 함수
private void sendContent(HttpExchange exchange, String content) throws IOException {
byte[] bytes = content.getBytes();
exchange.sendResponseHeaders(200, bytes.length);
OutputStream outputStream = exchange.getResponseBody();
outputStream.write(bytes);
outputStream.flush();
}
}
자바에 기본으로 있는 HTTP Server 를 써봤다.
Socket 으로 했을 때보다 훨씬 쉽게 경로 여러 개에 대해 처리할 수 있었다.
exchange 를 이용해서 getRequestMethod 함수를 이용하는 부분도 이전과 달리
파싱으로 하지 않고 바로 얻어서 편리했다. 또한 Header 에 대한 부분도 key, value 로 얻을 수 있었다.
다음에는 Spring 을 이용해서 이것보다 훨씬 쉽게 처리해보자.