안녕하세요 Side-Project를 이용해서 얻고싶은 지식들이 많습니다.
1.Springboot 뿐만아닌 WAS가 무엇인가?
2.WAS를 통해서 웹서비스가 어떻게 사용자(client)에 정보를 제공하는것인가?
3.프로젝트를 통해서 Web Server의 개발 흐름을 알아내고싶습니다.
지금은 현재 이정도만해도 괜찮을것 같습니다만 결국 궁극적으로 얻고싶은 지식은 무엇인가? 입니다.
어떤 부분이 문제인지 "분석"하고 어떻게 해결하려 했는지 "접근방법"을 잘 정리하여 어떤 "결과"가 도출되는 흐름을 배우고싶습니다.
부가적으로 Springboot 지식과 CS 지식을 통한 개발을 이루고싶습니다. 여태까지 전 "개발"에만 몰두했지 어떤 "개발"을 할것이며 왜 그렇게 생각했는지 ? 그렇게 생각한 근거가 무엇인지 깊게 생각해보지 않았던것 같습니다. 두서가 좀 길었네요 WAS를 이용한 실습 1 index.html을 진행해보도록 하겠습니다.
먼저 HTTP 웹서버를 구축하기위해서 핵심이되는 코드는 WebServer와 RequestHandler 클래스를 중점으로 다루겠습니다.
WebServer 클래스같은 경우에는 웹 서버를 시작하고, 사용자의 요청이 있을때까지 대기 상태에 있다가 사용자 요청이 있을경우 사용자의 요청을 RequestHandler 클래스에 위임하는 역할을합니다.
아! 그러면 일단 WebServer로 서버 구동을한다음 사용자의 지시에따라 RequestHandler가 처리해주는군요? 여기서 Handler! 이건 나중에 살펴보겠습니다.
WebServer 코드
package webserver;
import java.net.ServerSocket;
import java.net.Socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
while ((connection = listenSocket.accept()) != null) {
RequestHandler requestHandler = new RequestHandler(connection);
requestHandler.start();
}
}
}
}
코드를 보아하니 알고싶은 코드들이 많습니다. 서버소켓을 생성한다... 아마 PID를 가진 Process Server가 Create/Open을 통해서 socket을 여는거라고 생각하겠습니다 이 Web Server가 socket을 열면 Client와 통신이 될때 RequestHandler 객체를 생성하여 connection을 이어주어 시작하는군요? 일단 알겠습니다!
package webserver;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RequestHandler extends Thread {
private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
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 사용자 요청에 대한 처리는 이 곳에 구현하면 된다.
DataOutputStream dos = new DataOutputStream(out);
byte[] body = "Hello World".getBytes();
response200Header(dos, body.length);
responseBody(dos, body);
} 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());
}
}
private void responseBody(DataOutputStream dos, byte[] body) {
try {
dos.write(body, 0, body.length);
dos.flush();
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
좀길죠..? 어쩔수없습니다 ! 여러분들에게 다 설명하고 저도 복습하는의미로 넘어가죠 ! 우리가 보아야할건 일단 run() 메소드입니다. run이 실행된다는건 Socket간에 connection이 이루어졌고, 사용자의 요청이 있을때 실행되는것 같습니다.
connection.getInputStream(), connection.getOutputStream()을 통해서 무엇가를 받고있는거 같습니다. 이를 자세하게 설명하자면 getInputStream() 메서드와 getOutputStream() 메서드는 Socket 객체로부터 각각 입력스트림과 출력스트림을 얻을수있습니다. 예시를 보여드리자면

만일 getOutputStream()으로 객체를 보내기위해선 byte[] 배열로 생성하여 write() 메서드를 사용해 보내면됩니다.
역으로 getInputStream()으로 객체를 받기위해선 byte[]배열을 생성후 매개값으로 InputStream()의 read()메서드를 호출 하면됩니다.
하지만 ! 이미 InputStream으로 값을 받고있고 OutputStream으로 받고있으니 미리 작업을 해놓은 과정입니다. DataOutputStream도 존재하지만 이는 쉽게 생각하여 자바의 기본 데이터 타입별로 출력하는 별도의 메서드를 이용하기위한 객체입니다 Boolean으로 바꿀수도있고, Byte로 바꿀수도있습니다.
자 ! 이제 코드에 대한 긴설명이 끝났습니다...! 누군가가 저희에게 업무를 맡겼고, 우리는 이제 요구사항을 들어줘야합니다. 과연 요구사항이 뭔지 한번 볼까요?
1단계 : InputStream을 한줄 단위로 읽기위해 BufferedReader를 생성한다.
BufferedReader.readLine() 메소드를 활용해 라인별로 HTTP 요청 정보를 읽는다.
HTTP 요청 정보 전체를 출력한다.
2단계 : HTTP 요청 정보의 첫 번째 라인에서 요청 URL(ex. /index.html)을 추출한다.
3단계 : 요청 URL에 해당하는 파일을 webapp 디렉토리에서 읽어 전달하면 된다.
원하는게 정말 많네요 ! 한번 진행해봐요
1단계 처리
InputStream은 데이터가 전송되는 통로라고 이해하시면될것같습니다. 일단 데이터가 전송되니 BuffereReader를 생성하라고하는거겠죠? 일단 한번해보겠습니다.

이를 진행하니

Client Connect 되면서 GET /index.html HTTP/1.1 이 출력되었습니다. 이게 아마 1번째 줄일테고, 라인별로 HTTP 요청 정보를 읽으라했으니 한줄이 아니라는 뜻일겁니다. HTTP 요청 정보 전체를 출력해야겠죠?

자 while문이 나왔습니다. 일단 많은줄들이 출력돼고 우리는 그많은 줄들을 출력하기위해선 !"".equals(line) => 만약 line이 "" 상태가 아니라면 ? => 출력할 줄들이 남았다면 출력. 이런의미로 사용하였습니다.

정상적으로 출력되는거같았는데 이게 뭐죠? Thread가 2개 생기면서 보지못한 favicon.ico가 뭐야..? 하실수도있습니다. 이건 ...

이는 우리가 사용하진않지만 브라우저에서 자동으로 요청을합니다. 출력을 안하고싶지만... 일단 손님이니 반갑게 맞이해보죠? favicon.ico를 split로 꺼내 없앨순있지만 우리는 갈길이 멉니다 일단 무시하겠습니다!! (0byte니 너무 싫어하진 마세요!)
favicon.ico문제를 제치니 정상적으로 문제가 해결되면서 1단계를 완료하였습니다. 우리는 이를 통해 Client의 요청으로 인해서 HTTP Header가 InputStream 객체에 담겨져있고 사용자의 HTTP Header를 손쉽게 출력할수 있었습니다. HTTP Header를 어떻게 출력하고 다룰수있었던건지 아는 기회를 받았습니다.
2단계 처리
결국 GET /index.html HTTP/1.1을 에서 GET 이후로의 정보를 꺼내야하는군요...? 좋습니다 ! 꺼내면되죠 왜 꺼내야하는지는 일단 나중에 생각하고 꺼내봅시다 !
일단 TestCode를 만들어 실험을 한번 해봐야되지 않을까요? Test Code를 default로 하는 습관 !

뭔가 작게나마 Test 단위를 만들어 하니 뿌듯하네요 좋습니다 ! 이건 똑같이 사용해도되겠네요 !

이 코드를 통해서~

이 코드를 얻어 낼수 있었습니다.
HTTP Header의 html을 얻을수있었고 Test Code를 사용하여 미리 점검할수있던시간도 가질수 있었습니다. 이제 3단계로 가볼까요?
3단계 처리
3단계 : 요청 URL에 해당하는 파일을 webapp 디렉토리에서 읽어 전달하면 된다. 라는데 제일 어렵네요 역시 3단계 일까요...?
하고싶은 말은 이겁니다 요청 URL은 방금 우리가 찾아낸 /index.html이고, 현재 webapp 디렉토리에 /index.html 파일이 존재한다는것입니다. 요청 URL을 얻은 우리가 다시 webapp 디렉토리에서 해당 File을 찾아 전달해주면되는것이죠 잠깐 누구한테요? 당연히 Client이죠 !! 자 문제를 해결해볼까요? 이제는 궁금증을 가져와야합니다 우리가 얻은건 요청한 URL밖에없습니다. 단계별로 나눠보죠
1. /index.html이란 String 값을 webapp에 디렉토리의 index.html로 접근한다.
2. 접근했다면 File을 읽는다
3. 읽은 File을 Client로 보낸다.
역시 어렵네요 3단계에서 1단계를 다시 시작해보죠
3-1,2단계
먼저, 우리는 일단 File을 읽어 Client로 보낸다고 했죠 ? 그럼 Client로 보낼땐 OutputStream을 사용하겠네요? => byte로 만들어야겠네요? 라는 생각을 먼저해야합니다. 그럼 File을 읽으려면 InputStream에서 read를 사용한다고 하지않았나요? 그 즉시 우리는 File에 대한 byte를 read해야합니다. file read all byte...! Files 객체에선 readAllbytes라는 메서드를 지원합니다.

이 친구도 app.log라는 파일을 읽으려고하는 노력이 보입니다. 우리도 똑같이 따라하면됩니다 !!

자 여기서 Files 객체의 readAllBytes를 통해 new File이란 객체를 만든다음 현재 위치에서 "./webapp" + 우리가 얻은 URL" 을 하면됩니다.
저기선 Paths.get을 사용했고 우리는 File 객체를 만들어 toPath를 해줬습니다. Path.get은 문자열을 받아 경로 객체를 만들지만 이미 우린 File객체를 사용하여 바로 경로로 만들어주는 역할을해줬습니다 toPath는 시스템의 경로(Path) 객체로 변환하는 역할을 하는것이죠
1. Path.get => 경로를 문자열로 나타낼때
2. toPath => 이미 File 객체가 있을때 File객체의 Path객체로 변환
결국 둘다 똑같은 행위를 하는것이니 너무 염려마세요 !
결국 우리는 3-1,2 단계를 완벽하게 수행하였습니다 이제 보내기만하면되겠네요 !
3-3단계

response200Header와 responseBody는 임의로 만든 메서드이니 나중에 설명도록 하겠습니다.
결국 우리는 byte[] 배열로 Client에게 보내는 역할을 하였습니다

짜잔~ 이렇게 우리는 Web Server 의index.html 요청,응답 구조에대해 알아보았습니다. 다음엔 더 재밌게 알찬걸로 오겠습니다
회고
어떤 부분이 문제인지 "분석"
저는 InputStream, OutputStream이 어떤 역할을 하는지 알아야했습니다. 이 역할을 모른다면 Web Server에서 어떤 data를 주고 받는지 몰랐을겁니다. 또한 URL을 따내기위해서는 어떤 작업이 필요할까도 고민했습니다. 마지막으로 외부에서 data를 주기위해서 어떤 방식으로 data를 줘야하는지도 고민해야했습니다.
어떻게 해결하려 했는지 "접근방법"
BufferedReader 를 사용하여 InputStream의 모든 Line을 얻어낼수있었습니다. 만일 Scanner를 사용했다면 공백까지 얻기에 힘들었을겁니다. 또한 Data를 주고 받기위해서 InputStream와 OutputStream의 java docs를 확인하여 해결하였습니다.
URL을 따기위해선 subString과 split 사이에 고민을하였으며, split를 사용하였지만 substring이 split보다 성능이 좋았던것이 아쉬웠습니다. 마지막으로 data을 보내기위해선 OutputStream의 특성을 고려해야했고, byte단위를 보내줘야하는것을 인지하였습니다. java에서 File을 먼저 read로 읽고 보내기위해선 File객체의 readAllBytes을 고려하여 Path.get이 아닌 File객체를 만들어 toPath라는 객체로 해결하였습니다.
어떤 "결과"
HTTP Header를 읽을수있었고, InputStream, OutputStream의 특징을 알아내었습니다. 또한 split를 사용하여 URL을 얻을수있었지만 split보단 substring이 성능이 더 좋다고 생각합니다. 이 부분이 아쉬웠지만 나중을 통해서 해결해보겠습니다. 또한 byte배열로 index.html의 정보를 얻어내고 client 화면상에서 정보를 보여줄수있는 뿌듯함을 얻어낸것 같았습니다.