자바 웹 프로그래밍 Next Step - 4. HTTP 웹 서버 구현을 통해 HTTP 이해하기

이전 장에서 요구사항 6번은 동영상이 빠져있는데 그 부분은 박재성님께서 구현한 코드를 통해서 확인 가능할거 같습니다. 여기에서 확인할 수 있습니다.

백재성님께서 구현한 코드를 보기전에 저는 책을 보고 직접 구현해보았기 때문에 위 링크에서 확인 가능한 코드와 제가 구현한 코드는 조금 다른 부분이 있습니다.

우선 박재성님께서 구현한 코드는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
else if ("/user/list".equals(url)) {
if (!logined) {
responseResource(out, "/user/login.html");
return;
}

Collection<User> users = DataBase.findAll();
StringBuilder sb = new StringBuilder();
sb.append("<table border='1'>");
for (User user : users) {
sb.append("<tr>");
sb.append("<td>" + user.getUserId() + "</td>");
sb.append("<td>" + user.getName() + "</td>");
sb.append("<td>" + user.getEmail() + "</td>");
sb.append("</tr>");
}
sb.append("</table>");
byte[] body = sb.toString().getBytes();
DataOutputStream dos = new DataOutputStream(out);
response200Header(dos, body.length);
responseBody(dos, body);
}

이번엔 제가 구현한 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
else if (url.startsWith("/user/list")) {
Map<String, String> cookies = HttpRequestUtils.parseCookies(headerMap.get("Cookie"));
if(cookies.get("logined") == null || !Boolean.parseBoolean(cookies.get("logined"))) {
DataOutputStream dos = new DataOutputStream(out);
response302Header(dos);
} else {
int idx = 3;

Collection<User> userList = DataBase.findAll();
StringBuilder sb = new StringBuilder();
sb.append("<tr>");
for(User user : userList) {
sb.append("<th scope=\"row\">"+idx+"</th><td>"+user.getUserId()+"</td> <td>"+user.getName()+"</td> <td>"+user.getEmail()+"</td><td><a href=\"#\" class=\"btn btn-success\" role=\"button\">수정</a></td></tr>");
idx++;
}

String fileData = new String(Files.readAllBytes(new File("./webapp" + url).toPath()) );
fileData = fileData.replace("%user_list%", URLDecoder.decode(sb.toString(), "UTF-8"));

DataOutputStream dos = new DataOutputStream(out);
byte[] body = fileData.getBytes();
response200Header(dos, body.length);
responseBody(dos, body);
}
}

박재성님께서는 공통적으로 사용될만한 로그인 확인 부분을 따로 빼서 체크 하셨는데, 저는 따로 제외처리 하지 않았네요.
구현할때 공통적으로 들어갈만한 부분을 미리 따로 빼서 구현하면 훨씬더 좋지 않을까 라고 생각을 했구요
StringBuilder를 이용해서 테이블을 생성할때 아무래도 제가 구현한 부분이 가독성이 더 떨어지는거 같습니다.

그 외에 박재성님께서는 테이블만 구현해서 간단하게 뷰를 구현하셨지만 저는 list.html 파일에 한줄 추가 하는 형태로 구현을 했습니다. (박재성님께서 작성하신 다른 브랜치 또는 다른 곳에서는 list.html에 추가하는 형태로 하셨을 수 도 있습니다)

4.2 이전 장에서 구현한 웹서버의 이론적인 부분을 다뤄봅니다.

요구사항1 - index.html 응답하기

HTTP를 이용해 데이터를 주고받기 위해서는 아래와 같은 형테로 데이터를 주고 받습니다.

이미지1
출처 :https://developer.mozilla.org/ko/docs/Web/HTTP/Messages

요청 데이터는 Request-Line, Header, Body로 구성되어 있고
응답 데이터는 Status-Line, Header, Body 로 구성되어 있습니다.

Request-Line에는 HTTP 메소드 - URI - HTTP 버전 으로 구성되어 있습니다.
Header에는 Key : Value형태로 값이 들어가있고, 여러개의 값을 쉼표(,)를 이용해 전달 할 수 있습니다.
ex) Accept-Encoding: gzip, deflate, sdch

Status-Line은 HTTP 버전 - 상태코드2 - 응답 구문 으로 구성되어 있습니다.

요구사항2 - GET 방식으로 회원가입하기

회원가입 요청시에 URI는 다음과 같습니다. /user/create?userId=test&password=password&name=tester …
여기서 /user/create는 path라 부르고, ?이후 key=value&key=value&… 형태로 전달되는 값은 QueryString 이라고 부릅니다.

GET방식은 사용자가 입력한 데이터가 브라우저 URL에 노출되고, 요청 데이터의 길이 제한이 있으므로 사용자가 입력한 데이터를 서버에 전송할때는 적합하지 않습니다.

요구사항 3 - POST 방식으로 회원가입하기

GET방식에서 POST방식으로 요청을 전송할경우 사용자가 입력한 데이터가 쿼리스트링대신 body에 담겨 전달됩니다.
Header에 Content-Length라는 필드 이름으로 body에 담긴 데이터의 길이가 전송됩니다.

요구사항 3번에서는 헤더에 포함되어있는 Content-Length값을 구해서 본문의 길이를 구하고, 그 길이만큼 본문을 Map<String, String>형태로 변환하면 됩니다.
본문을 읽는 기능은 IOUtils.readData()를 사용하면 됩니다.

HTTP 스펙은 GET, POST 이외에 PUT, DELETE, OPTIONS 등의 메소드를 지원하지만 HTML은 GET과 POST만 사용가능하도록 지원하고 있습니다.
GET, POST 메소드 외의 것들은 AJAX를 이용해 사용 가능합니다.

GET은 서버에서 데이터를 조회할때, POST는 서버의 상태를 변경하는 작업을 할때 사용한다.

요구사항 4 - 302 status code 적용

특정 동작을 수행후 페이지 이동을 처리할때 URL을 변경해서 처리하면 브라우저에 이전 페이지에 대한 정보를 가지고 있어서 뒤로가기를 하게되면 동작이 다시 수행될 가능성이 있습니다.
따라서 response 헤더에 상태값을 302로 설정해서 보내면 브라우저에서 자동으로 페이지를 이동시킨다.
https://developer.mozilla.org/ko/docs/Web/HTTP/Status/302

헤더는 아래와 같이 작성하면 되는데 책에는 302 Redirect로 되어있다.
HTTP프로토콜 문서에는 302 Found로 되어있는데, Redirect동작을 수행하므로 어떤걸 사용해도 동작하는데 문제는 없을거 같습니다.

1
2
HTTP/1.1 302 Found
Location: https://www.naver.com

참고로 Redirect를 수행하는 코드가 301 Moved Permanently도 있다고합니다.
301, 302 상태코드의 차이점에 대해서 알아보고 포스팅 하도록 하겠습니다.

그리고 Redirect의 경우 브라우저에서 새로운 위치로 이동을 요청하므로 요청이 2개가 발생하게 된다.
302 처리 이미지
출처 : https://blogs.oracle.com/ebstech/secure-oracle-e-business-suite-122-with-allowed-redirects

요구사항 5 - 로그인하기

HTTP는 클라이언트와 서버간 요청을 주고받고 나서 연결을 끊는 무상태 프로토콜(stateless protocol) 이라고도 부릅니다.
따라서 서버가 클라이언트를 식별할 수 없다는 문제가 있는데요
예를 들어서 쇼핑몰에서 상품을 장바구니에 담는 요청을 서버로 날리고 요청이 끊긴 후 장바구니 리스트로 이동하면 서버는 어떤 클라이언트가 장바구니에 어떤 상품을 담았는지 식별이 불가능 하기때문에 비어있는것처럼 보이겠죠. 이런 문제가 발생할 가능성이 있기때문에 서버가 클라이언트를 식별할 수 있는 방법이 필요한데요 이번 요구사항에서는 쿠키를 이용해 클라이언트를 식별하는 정보를 저장하도록 했습니다.

클라이언트가 서버로 로그인 요청을 보내면 응답값의 헤더에 Set-Cookie라는 이름으로 어떤 정보를 저장할지 정해서 클라이언트에 넘겨주면
클라이언트에서 해당 값을 저장하고 있다가 다음번 요청때 Cookie를 불러와서 요청을 보내게 됩니다.

요구사항 6 - 사용자 목록 출력

요구사항 5에서 구현된 내용은 로그인시 쿠키를 설정하는 내용이었는데, 요구사항 6에서는 저장된 쿠키값을 서버에서 식별해 값에 따라 동작을 하게끔 구현하는 내용입니다. 클라이언트로부터 요청이 들어올때 요청 헤더의 Cookie값을 읽고 그 값에 맞는 동작을 수행하면 됩니다.

요구사항 7 - CSS 지원하기

모든 요청에 대한 응답에는 응답의 데이터가 어떤 형태의 데이터인지 정의 할수 있는 Content-Type라는 헤더가 있습니다.
이전까지 구현했던 내용에서는 모든 Content-Type의 데이터를 text/html로 설정해서 보냈습니다.
이렇게 되면 클라이언트에서 CSS파일을 요청했을때에도 Content-Type이 text/html이므로 CSS가 정상적으로 적용되지 않는 문제가 있습니다.
이와 같은 문제를 해결하기 위해서 CSS파일 요청시에 Content-Type을 text/css로 설정해서 보내면 정상적으로 동작합니다.

CSS뿐만아니고 JS, 이미지등 다양한 타입의 데이터도 Content-Type을 맞춰서 설정해야 정상적으로 동작 할거 같습니다.

다음단계의 실습을 진행하기위해서는 기존 코드에서 브랜치를 따서 작업해야합니다.

4.3 추가 학습 자료


  1. 1.https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
  2. 2.https://tools.ietf.org/html/rfc7231#section-6.1
공유하기