HTTP 웹 서버를 직접 구현하여 프로토콜 스펙을 이해해보자!
학습 배경
HTTP 웹 서버 직접 구현하기 미션 을 진행하면서 많은 시간을 할애했다. 고민했던 과정들이 정말 많았기에 재밌는 실습을 진행할 수 있었다 🙂 이번에 구현에 대한 내용과 과정들을 자세히 다루어보고자 한다.
요구사항에 대한 상세한 설명은 생략한다. 어떻게 요구사항을 준수하면서 HTTP 웹 서버를 구현했는지에 대해 중점으로 다루어 보고자 한다. 또 요구사항을 해결해가는 과정에서 HTTP 프로토콜 스펙 및 쿠키등 여러 지식에 대한 이론을 다루겠다.
웹서버를 구현할 메인 클래스 역할
HTTP 웹서버를 구현하기 위해, 2가지 클래스를 중점으로 구현해 나가도록 하겠다. 각 요구사항을 해결해나가는 과정속에서 HTTP 프로토콜에 대해 다루도록 한다.
WebServer
WebServer
클래스는 웹 서버를 시작하고, 클라이언트의 요청이 있을 때 까지 대기 상태에 있다가, 요청이 들어올 경우 해당 요청을 RequestHandler 클래스에게 위임하는 역할을 한다.
사용자 요청이 발생할 때 까지 대기 상태에 있도록 지원하는 역할은 자바의 ServerSocket
클래스가 담당한다. ServerSocket 에 클라이언트 요청이 들어오는 순간, 클라이언트와 연결을 담당하는 Socket 을 RequestHandler 에 전달하면서 새로운 쓰레드를 실행하는 방식으로 멀티쓰레드 프로그래밍을 지원하고 있다.
public class WebServer {
private static final Logger log = LoggerFactory.getLogger(WebServer.class);
private static final int DEFAULT_PORT = 8080;
public static void main(String args[]) throws Exception {
int port = 0;
if (args == null || args.length == 0) {
port = DEFAULT_PORT;
} else {
port = Integer.parseInt(args[0]);
}
// 서버소켓을 생성한다. 웹서버는 기본적으로 8080번 포트를 사용한다.
try (ServerSocket listenSocket = new ServerSocket(port)) {
log.info("Web Application Server started {} port.", port);
Socket connection; // Socket: 클라이언트와의 연결을 담당. 클라이언트가 연결될 때 까지 (요청이 들어올 때 까지) 대기한다.
while ((connection = listenSocket.accept()) != null) {
// 요청이 들어오면 해당 요청을, 즉 Socket을 RequestHandler 에 전달하면서 요청에 대한 처리를 위임한다.
RequestHandler requestHandler = new RequestHandler(connection);
requestHandler.start(); // 요청 처리 시작
}
}
}
}
RequestHandler
RequestHandler 클래스는 앞서 말했듯이 WebServer
클래스로 부터 전달받은 Socket 을 전달받고 클라이언트의 요청을 처리한다. 정확히는 이 클래스 내부의 run()
메소드에서 클라이언트 요청 처리 코드를 구현할 것이다.
InputStream
은 클라이언트(웹 브라우저) 에서 서버로 요청을 보낼 때 전달되는 데이터를 담당하는 스트림이다. 반면 OutputStream
은 반대로 서버에서 클라이언트에 응답을 보낼 때 전달하는 데이터를 담당하는 스트림이다.
public class RequestHandler extends Thread {
private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
DataBase dataBase = new DataBase();
private Socket connection;
public RequestHandler(Socket connectionSocket) {
this.connection = connectionSocket;
}
public void run() {
log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
connection.getPort());
try (InputStream in = connection.getInputStream();
OutputStream out = connection.getOutputStream()) {
// TODO: 사용자 요청에 대한 처리는 여기서 구현된다.
} catch(IOException e) {
log.error(e.getMessage());
}
}
private void response200Header(DataOutputStream dos, int lengthOfBodyContent) {
try {
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n");
dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
정리하자면, WebServer 는 클라이언트(웹 브라우저) 의 요청을 언제든지 받을 수 있도록 Socket 을 생성해두고 대기 상태에 있는다. 그러다 요청이 들어오면, 해당 요청을 RequestHandler 에게 처리하도록 떠넘기는 방식이다. 처리가 완료 되었다면, RequestHandler 는 응답을 WebServer 에게 보낸다.
HTTP 프로토콜 스펙
요구사항을 해결하기 위해 HTTP 의 표준 스펙에 대해 이해해야한다. 웹 클라이언트는 웹 서버와 데이터를 주고받기 위해 HTTP 라는 서로간의 약속된 규약을 따른다. 웹 클라이언트가 웹 서버에 요청을 전송하기 위한 규약을 이해하기 위해, 아래 스펙을 예를 들어보곘다.
요청(Request) 메시지 규약
POST /user/create HTTP/1.1 // (1) 요청 라인(Request Line)
HOST: localhost:8080 // (2) 요청 헤더(Request Header)
Connection-Length: 59
Content-Type: application/x-wwww-form-unlencoded
Accept: */*
userId=msung99&password=password // (3) 요청 본문(Request Body)
(1) 요청 라인(Request Line)
(1)
에 해당하는 내용이다. 요청 데이터의 첫번째 라인은 요청 라인(Request Line) 이라고 부른다. 요청 라인은 HTTP-메소드 URI HTTP-버전
의 형태로 구성된다. HTTP 메소드는 요청의 종류를 나타내며, HTTP 버전의 경우 현재 HTTP/1.1 을 기준으로 한다.
(2) 요청 헤더(Request Header)
(2)
에 해당하는 내용이다. 요청 헤더는 <필드 이름> : <필드 값>
형태의 key-value 쌍으로 구성된다.
(3) 요청 본문(Request Body)
(3)
에 해당하는 내용이다. 헤더와 본문 사이에 빈 공백 라인 1줄을 두고 본문이 주어진다.
응답(Response) 메시지 규약
HTTP/1.1 200 OK // (1) 상태 라인(Status Line)
Content-Type: text/html;charset=utf-8 // (2) 응답 헤더(Response Header)
Content-Length: 20
<h1>Hello World!</h1> // (3) 응답 본문(Response Body)
응답 메시지는 요청 메시지의 규약과 유사한 구조를 지닌다. 다만 다른점이라면 첫번째 라인 (1)
의 상태라인의 형식이 다르다는 것이다.
상태라인(Status Line)
응답 헤더의 첫번째 라인은 상태 라인이라고 부른다. HTTP-버전 상태코드 응답구문
의 구조를 취하고 있다.
이렇게까지 HTTP 요청과 응답의 기본 구조(규약) 에 대해 살펴봤다.
요구사항1. index.html 을 응답한다.
요구사항 : http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다.
BasicCode
이를 만족시키기 위한 코드는 아래와 같이 구현했다.
public void run() {
log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
DataOutputStream dos = new DataOutputStream(out);
BufferedReader br = new BufferedReader(
new InputStreamReader(in, "UTF-8"));//(1)
String line = br.readLine(); // (2)
log.debug("request line : {}", line);
if(line == null) {
return;
}
String[] tokens = line.split(" "); // (3)
while(!line.equals("")) { // (4)
line = br.readLine();
log.debug("header : {}", line);
}
byte[] body = Files.readAllBytes(
new File("./webapp" + tokens[1]).toPath()); // (5)
response200Header(dos, body.length);
responseBody(dos, body);
} catch (IOException e) {
log.error(e.getMessage());
}
}
BufferedReader 로 헤더 값 읽어오기
조금씩 뜯어보자. 우선 (1)
에서 BufferdReader
를 이용해 헤더 값을 읽는다.
요청 라인(Request Line) 읽어오기
곧바로 바로 아래 라인 (2)
에서 BufferedReader 를 활용해 요청 라인(Request Line)
을 읽어오게 되는데, 이렇게 읽어온 헤더의 첫번째 줄에는 HTTP 메소드, 요청 URL, HTTP 버전이 공백을 사이에 두고 들어온다. (ex. GET /index.html HTTP/1.1)
요청 라인을 split 하여 문자열 배열에 담기
(3)
에서는 요청 라인을 공백을 기준으로 자르는 모습을 볼 수 있다. 즉, tokens 라는 문자열 배열에 "GET", "/index.html", "HTTP/1.1" 에 담기게 될 것이다.
헤더 정보 출력하기
(4)
에서는 반복문을 계속 순환하면서 헤더의 나머지 데이터를 모두 출력한다. "/index.html" 로 요청을 보냈을 때 헤더를 출력한 결과는 아래와 같다.
그런데 이때 분명 index.html 로 요청을 단 1번을 보냈을 뿐인데 1번의 요청이 아니라 여러번의 추가 요청이 발생하는 것을 확인할 수 있다. 이렇게 많은 요청이 발생한 이유는 서버가 웹 페이지를 구성하는 모든 리소스(html, css, js, 이미지 등) 을 한번에 응답으로 보내지 않기 때문이다.
웹 서버는 첫번쨰로 /index.html 요청에 대한 응답에 HTML 만을 보낸다. 응답을 받은 브라우저는 HTML 내용을 분석하여 css, js , 이미지등의 자원이 포함되어 있다면 서버에 해당 자원을 다시 요청하게 된다. 따라서 하나의 웹 페이지를 사용자에게 정상적으로 서비스하려면 클라이언트와 서버간의 1번의 요청이 아닌 여러번의 요청과 응답을 주고받게 된다.
Response Body 구성하기
마지막으로 (5)
에서는 URL 해당하는 파일을 가지고와서 byte array 로 변환 후 body에 넣어준다. 앞서 살펴봤듯이 tokens 배열에는 헤더의 요청 라인(Request Line)
이 공백을 기준으로 담기는데, 2번째 요소인 tokens[1] 에는 URL 이 담기게 된다.
요구사항2. GET 방식으로 회원가입한다.
이 요구사항을 요약하면 아래와 같다.
- "회원가입" 메뉴를 클릭하면 /user/form.html 로 이동하면서 회원가입을 진행한다.
- 회원가입을 진행하면 다음과 같은 형태로 사용자가 입력한 값이 서버로 전달된다. "/user/create?userId=msung99&password=msung1234&name=minsung&email=msung99@gmail.com"
- HTML 과 URL 을 비교해보고 사용자가 입력한 값을 파싱하여 User 클래스에 저장한다.
run( ) 메소드를 아래와 같이 개선해줘야하는데, 이를 위해 /user/create 에서 회원가입 버튼을 클릭시 전송되는 URL 는 아래와 같은 형식으로 전송된다.
http://localhost:8080/user/create?userId=msung99&password=msung1234&name=minsung&email=msung99@gmail.com
쿼리 스트링(QueryStrig) 으로, 즉 URL 에 파라미터로 회원가입시 form 에 입력한 유저의 정보가 실리는 것을 확인할 수 있다. 우리는 이 URL 을 통해 전달된 유저 데이터를 기반으로 서버의 DB 에다 유저 데이터를 저장하는 로직을 구현해야한다.
다시 말하자면, /user/create로 URL이 시작될때 URL의 뒷부분인 QueryString 값을 가져와서 parseQueryString 메소드를 이용해 각각의 값을 User 객체 생성자에 param값으로 넣어주는 로직을 구현하면 된다.
GET - SignUp
물음표 "?" 이후에 유저 정보가 실리는 것을 볼 수 있는데, 이 URL 문자열을 파싱하여 입력된 유저 데이터를 추출하는 코드는 아래와 같이 구현했다.
public void run() {
log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
DataOutputStream dos = new DataOutputStream(out);
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String line = br.readLine();
log.debug("request line : {}", line);
if(line == null) {
return;
}
String[] tokens = line.split(" ");
while(!line.equals("")) {
line = br.readLine();
log.debug("header : {}", line);
}
String url = tokens[1];
if(url.startsWith("/user/create")) {
int index = url.indexOf("?");
String queryString = url.substring(index + 1);
Map<String, String> params = HttpRequestUtils.parseQueryString(queryString);
String userId = params.get("userId");
String password = params.get("password");
String name = params.get("name");
String email = params.get("email");
User user = new User(userId, password, name, email);
// TODO: 추후 DB 구현 필요
} else {
byte[] body = Files.readAllBytes(new File("./webapp" + tokens[1]).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
"/user/create" 로 요청이 들어올 경우 사용자가 입력한 값을 파싱하여 User 클래스에 저장하는 모습을 볼 수 있다. 추후 DB 가 구축됨에 따라 생성된 User 객체를 저장하는 로직을 구현할 예정이다.
요구사항3. POST 방식으로 회원가입한다.
요구사항 : /user/form.html 파일의 form 태그 메소드를 get 에서 post 로 수정한 후 회원가입 기능이 정상적으로 동작하도록 구현한다.
GET 방식으로 요청했던 회원가입 요청을 POST 로 변경해야한다. 때문에 /user/form.html 의 question form 태그의 method 를 get 에서 post 로 변경해주었다.
<form name="question" method="post" action="/user/create"></form>
변경해줬다면, GET 방식으로 요청할 때 URL 에 포함되어 있던 쿼리 스트링이 없어지고 method 가 GET 에서 POST 로 변경되었다. 요청 URL 에 포함되어 있던 쿼리 스트링의 내용물은 HTTP 요청의 Request Body(요청 본문)
을 통해 대신 전달된다. 또한 POST 방식으로 데이터를 전달하면서 헤더에 본문 데이터에 대한 길이가 Content-Length
라는 필드 이름으로 전달된다.
Post - SignUp
이를 고려한 구현 코드는 아래와 같이 개선해줬다.
public void run() {
log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
DataOutputStream dos = new DataOutputStream(out);
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String line = br.readLine();
log.debug("request line : {}", line);
if(line == null) {
return;
}
String[] tokens = line.split(" ");
int contentLength = 0; // (1)
while(!line.equals("")) {
line = br.readLine();
log.debug("header : {}", line);
if(line.contains("Content-Length")) { // (2)
contentLength = getContentLength(line);
}
}
String url = tokens[1];
if(("/user/create".equals(url)) {
String body = IOUtils.readData(br, contentLength); // (3)
// (4)
Map<String, String> params = HttpRequestUtils.parseQueryString(body);
String userId = params.get("userId");
String password = params.get("password");
String name = params.get("name");
String email = params.get("email");
User user = new User(userId, password, name, email);
// TODO: 추후 DB 구현 필요
} else {
byte[] body = Files.readAllBytes(new File("./webapp" + tokens[1]).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
우선 요청 본문에 담을 데이터의 길이(크기) 를 저장할 변수를 (1)
에서 선언해줬다. 이후 (2)
에서 헤더의 여러 key 값중 Content-Length
에 대한 value 값, 즉 요청 본문의 길이값을 추출한다.
이렇게 구한 길이만큼 (3)
처럼 본문을 읽는다. 본문을 읽는 기능은 IOUTils.readData()
로 구현했다. 이후 본문 데이터를 (4)
처럼 Map<String, String> 형태로 변환하면 된다. 요구사항2 를 구현했을땐 유저 데이터를 URL 의 쿼리 스트링으로 전달 받았다면, 이젠 요청의 바디(Request Body) 로 부터 유저 데이터를 전달받는 방식이다.
추가적으로 (2)
에서 본문의 길이값을 추출할 때 getContentLength 라는 메소드를 활용한 것을 볼 수 있다. 이에대한 구현은 아래와 같이 진행해줬다. 헤더의 한 줄을 읽어왔을 때 "Content-Length: 80" 과 같은 형식으로 읽어올텐데, 이를 ":" 로 split 한 후 80에 해당하는 값을 Integer.parseInt() 로 형식을 변환하여 리턴하는 기능이다.
private int getContentLength(String line) {
String[] headerTokens = line.split(":");
return Integer.parseInt(headerTokens[1].trim());
}
요구사항4. 302 Status Code 를 적용한다.
요구사항 : 회원가입을 완료하면 /index.html 페이지로 이동해야한다. 현재는 URL 이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다. 따라서 회원가입을 완료한 후 /index.html 페이지로 이동한다. 브라우저의 URL 도 /user/create 가 아니라 /index.html 로 변경해야 한다.
이를 구현하기 위해선, 선수지식으로 HTTP 의 상태코드 값을 이해하면 좋다. 이미 잘 알고있지만, 복습 겸 한번 되짚어보겠다.
- 200번대 : 성공. 클라이언트가 요청한 동작을 수신하여 이해하고 승인했으며 성공적으로 처리했음을 의미한다.
- 300번대 : 리다이렉션. 클라이너트는 요청을 마치기 위해 추가 동작이 필요하다.
- 400번대 : 요청 오류. 클라이언트에 오류가 있다.
- 500번대 : 서버 오류. 서버가 유효한 요청을 명백하게 수행하지 못했다.
회원가입을 처리하는 "/user/create" 요청과 첫 화면 "/index.html" 을 보여주는 요청을 분리한 후 HTTP 의 302 상태 코드를 활용해야한다. 즉, 웹 서버는 "/user/create" 요청을 받아 회원가입을 완료한 후 응답을 보낼 때 클라이언트(웹 브라우저) 에게 "/index.html" 로 이동하도록 할 수 있다. 이때 사용하는 상태 코드가 302
이다. "/index.html" 로 이동하도록 응답을 보낼 때 사용하는 응답 헤더는 Location
으로 다음과 같이 응답을 보내면 된다.
HTTP/1.1 302 Found
Location: /index.html
위 처럼 응답을 보내면 클라이언트는 첫 라인의 상태 코드를 확인 후 302라면 Location 값을 읽어서 서버에 재요청을 보내게 된다. 재 요청을 보내면 클라이언트의 요청은 회원가입 처리를 위한 "/user/create" 으로의 요청이 아니라 "/index.html" 으로의 요청으로 변경된다. 이 상태에서 URL 을 확인해보면 "/user/create" 가 아닌 "/index.html" 로 변경된 것을 확인할 수 있다.
302 를 적용한다.
구현 코드는 아래와 같다. 가장 눈여겨 볼 점은 response302Header()
라는 메소드를 생성해줬으며, 이를 호출한다는 점이다.
String url = tokens[1];
if("/user/create".equals(url)) {
String body = IOUtils.readData(br, contentLength);
// ... (이전 구현 코드와 동일)
response302Header(dos, "/index.html");
}
// ...
response302Header
response302Header()
의 구현 코드는 아래와 같다. 앞서 말한 302 상태 코드를 적절히 리턴하고 있으며, 302 상태 코드값이 확인 되었을때 리다이렉션 될 URL 값(즉, "/index.html") 을 Location
에 할당해줬다.
private void response302Header(DataOutputStream dos, String url) {
try {
dos.writeBytes("HTTP/1.1 302 Found \r\n");
dos.writeBytes("Location: " + url + "\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
요구사항5. 로그인한다.
- "로그인" 메뉴를 클릭하면 "/user/login.html" 로 이동해 로그인할 수 있다. 로그인이 성공하면 "/index.html" 로 이동하고, 로그인에 실패하면 "/user/login_failed.html" 로 이동해야 한다.
- 앞에서 회원가입한 사용자로 로그인될 수 이썽야한다. 로그인이 성공하면 로구인 상태를 유지할 수 있어야한다. 로그인이 성공할 경우 요청 헤더의 Cookie 값이 logined=true, 실패하면 logined=false 로 전달되어야 한다.
쿠키(Cookie) 의 등장배경
HTTP 는 무상태 프로토콜이다. 요청을 보내고 응답을 받으면 클라이언트와 서버간의 연결을 바로 끊는다. 연결을 끊기 때문에 각 요청 사이에 상태를 공유할 수가 없다.
무상태 프로토콜이므로, 서버는 클라이언트가 누구인지 식별할 수가 없다. 서버가 클라이언트를 식별할 수 없기 떄문에 앞에서 클라이언트가 한 행위를 기억할 수 없다. 가령 로그인을 완료한다고 한들, 매 요청마다 다시 로그인을 시도하지 않을 것이다.
HTTP 의 쿠기 지원 플로우
이를 위해 등장한 것이 쿠키(Cookie)
이다. HTTP 가 쿠키를 지원하는 방법은 다음과 같다.
-
(1)
: 먼저 서버에서 로그인 요청을 받으면 로그인 성공/실패 여부에 따라 응답 헤더에Set-Cookie
로 결과값을 저장할 수 있다. -
(2)
: 클라이언트는 응답 헤더에Set-Cookie
가 존재할 경우Set-Cookie
의 값을 읽어 서버에 보내는 요청 헤더의 Cookie 헤더 값으로 다시 전송한다. 즉, 각 HTTP 요청간에 데이터를 공유할 방법이 없기 떄문에 헤더를 통해 공유할 데이터를 매번 다시 전송하는 방식으로 데이터를 공유한다. 이때 공유할 데이터로 쿠키를 헤더에 넣을 수 있는 것이다.
쿠키(Cookie) 를 적용한다.
구현 코드는 아래와 같다.
if("/user/create".equals(url)) {
// ... (기존 로직과 동일)
DataBase.addUser(user); // (1)
response302Header(dos, "/index.html");
} else if("/user/login".equals(url)) {
String body = IOUtils.readData(br, contentLength);
Map<String, String> params = HttpRequestUtils.parseQueryString(body);
User user = DataBase.findUserById(params.get("userId"));
if(user == null) {
responseResource(out, "/user/login_failed.html");
return;
}
if(user.getPassword().equals(params.get("password"))) {
response302LoginSuccess(dos);
} else {
responseResource(out, "/user/login_failed.html");
}
}
로그인 기능을 구현하기 위해선, 우선 회원가입 기능 및 데이터베이스에 유저 정보를 저장하는 기능이 원활히 구현되어야 할 것이다. 이를 위해 위의 (1)
처럼 DB 에 유저 데이터를 저장하는 플로우를 구현했다.
response302LoginSuccess
private void response302LoginSuccess(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 302 Found \r\n");
dos.writeBytes("Set-Cookie: logined-true \r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
로그인이 성공하면 응답 헤더에 Set-Cookie
헤더의 값으로 logined=true
를 전달했다. 위와 같이 구현을 완료한 후 서버를 재시작하고 회원가입, 로그인 순으로 테스트를 진행한다.
responseResource
private void responseResource(OutputStream out, String url) throws IOException {
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
로그인이 실패할 경우 호출되는 메소드다.
이와 같이 모든 요청에 로그인 성공 유무에 대한 정보가 전달된다. response302LoginSuccess()
를 호출하여 서버는 응답 헤더에 Set-Cookie
에 대해 로그인 성공 여부를 전달한다. 이러한 Set-Cookie
필드가 포함된 HTTP 응답을 전달받은 클라이언트는, 이후 서버에게 요청을 보낼때마다 Set-Cookie
가 포함된 헤더를 매번 전송하게 된다. 그러면 서버는 클라이언트의 Cookie 요청 헤더를 확인하여 logined 값이 true 인지 여부를 판단하여 현재 로그인 상태 유무를 확인할 수 있다.
마치며
이렇게 기본적인 HTTP 웹 서버의 구축을 마쳤다. 하지만 아직 기능 추가, 리팩토링, 테스트를 거쳐할 부분이 많이 남아있다. 다음에는 현재 구현된 HTTP 웹서버 코드를 리팩토링 해보겠다.
더 학습해볼 키워드
- Legacy Code 를 개선하기 위한 고민
- LoggerFactory, ServerSocket, BufferedReader
- Session