요구사항 | 지금까지 구현한 소스코드는 CSS 파일을 지원하지 못하고 있다. CSS 파일을 지원하도록 구현한다. |
지금까지 서비스한 HTML 소스코드를 보면 <link> 태그에 CSS 파일도 정상적으로 설정되어 있으며, 물리적인 CSS 파일도 해당 위치에 존재한다. 이클립스 콘솔을 통해 확인해보면 CSS 파일에 대한 요청도 정상이다. 문제는 응답을 보낼 때 모든 컨텐츠의 타입을 text/html로 보내는 것이다. 브라우저는 응답을 받은 후 Content-Type 헤더 값을 통해 응답 본문(body)에 포함되어 있는 컨텐츠가 어떤 컨텐츠인지를 판단한다. 그런데 지금까지 구현한 모든 응답은 text/html로 고정되어 있어 브라우저는 CSS 파일도 HTML로 인식했기 때문에 정상적으로 동작하지 않았다.
이 문제를 해결하려면 CSS 요청에 대해 Content-Type 헤더 값을 text/html이 아니라 text/css로 응답을 보내면 문제를 해결할 수 있다. 간단히 해결하기 위해 요청 URL의 확장자가 css인 경우 text/css로 응답을 보내도록 구현해 문제를 해결한다.
- RequestHandler.java
package webserver;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.File;
import java.nio.file.Files;
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 사용자 요청에 대한 처리는 이 곳에 구현하면 된다.
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(" ");
boolean logined = false;
while (!line.equals("")) {
log.debug("header : {}", line);
line = br.readLine();
if (line.contains("Cookie")) {
logined = isLogin(line);
}
}
String url = tokens[1];
// POST
if ("/user/create".equals(url)) {
String body = IOUtils.readData(br, contentLength);
Map<String, Object> params = HttpRequestUtils.parseQueryString(body);
User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email"));
DataBase.addUser(user);
} 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"))) {
DataOutputStream dos = new DataOutputStream(out);
response302LoginSuccessHeader(dos);
} else {
responseResource(out, "/user/login_failed.html");
}
} 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);
} else if (url.endsWith(".css")) {
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200CssHeader(dos, body.length);
responseBody(dos, body);
} else {
responseResource(out, url);
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
private void response200CssHeader(DataOutputStream dos, int lengthOfBodyContent) {
try {
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes("Content-Type: text/css\r\n");
dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
private boolean isLogin(String line) {
String[] headerTokens = line.split(":");
Map<String, String> cookies = HttpRequestUtils.parseCookies(headerTokens[1].trim());
String value = cookies.get("logined");
if (value == null) {
return false;
}
return Boolean.parseBoolean(value);
}
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);
}
private void response302LoginSuccessHeader(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 302 Redirect \r\n");
dos.writeBytes("Set-Cookie: logined=true \r\n");
dos.writeBytes("Location: /index.html \r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
private int getContentLength(String line) {
String[] headerTokens = line.split(":");
return Integer.parseInt(headerTokens[1].trim());
}
private void response302Header(DataOutputStream dos, String url) {
try {
dos.writeBytes("HTTP/1.1 302 Redirect \r\n");
dos.writeBytes("Location: " + url + " \r\n");
dos.writeBytes("\r\n");
} catch {
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());
}
}
}
이와 같이 각 요청과 응답 헤더는 각 요청과 응답이 포함하고 있는 본문 컨텐츠에 대한 정보를 제공하고 있다. 이와 같이 데이터에 대한 정보를 포함하고 있는 헤더 정보들을 메타데이터라고 부른다. 예를 들어 위 소스코드의 Content-Type, Content-Length 헤더 정보는 본문 컨텐츠에 대한 타입과 길이 정보를 포함하고 있는데, 실제 데이터가 아닌 본문 데이터에 대한 정보를 포함하고 있다. 메타데이터는 요청과 응답 헤더 이외에도 애플리케이션 개발의 많은 곳에서 사용되는 용어이니 반드시 알아야 하는 용어이다.
지금까지 7개의 요구사항을 해결하면서 간단 웹 서버를 구현했다. 지금까지 구현한 전체 소스코드는 http://github.com/slipp/web-application-server 저장소의 was-step1-bad-version 브랜치의 RequestHandler 클래스에서 확인할 수 있다.
이 책은 하나의 소스코드에 새로운 기능을 계속 추가하면서 실습을 진행한다. 각 과정은 Git의 브랜치를 통해 다음 단계의 실습을 진행하는 방식으로 구성되어 있다. 따라서 실습을 진행하려면 계속해서 브랜치를 변경해 가면서 진행해야 한다. 이클립스 또는 터미널에서 브랜치를 변경하는 방법은 다음 동영상을 참고한다.
참고 동영상 : https://youtu.be/VeTjDYl7UVs
브랜치를 변경할 때 에러가 발생하는 대부분의 경우는 현재 브랜치에서 소스코드를 변경한 후 변경된 내용을 커밋(commit)하지 않아 발생한다. 브랜치를 변경하기 전에 먼저 커밋을 진행한 후 브랜치를 변경할 것을 추천한다.
HTTP를 학습하기 위해 이와 같이 웹 서버를 직접 구현하면서 학습하지 않고 책을 통해 학습을 시작해도 된다. 책을 통해 학습하는 것이 훨씬 더 적은 시간을 투자해 효과적으로 학습할 수도 있다. 하지만 HTTP를 이해하기 위해 고민하고 생각하는데 투자하는 시간은 그만큼 적어진다. 또한 책을 통해 수많은 정보를 수동적으로 학습하기 때문에 몸에 체득되는 부분도 적다. 앞의 7개 문제는 HTTP의 극히 일부분 만을 학습할 수 있다. 이 실습을 통해 HTTP의 모든 것을 학습할 수는 없지만 HTTP의 기본에 대해서는 확실하게 학습할 수 있다. 이 장에서 학습한 내용을 바탕으로 다음 절에서 추천하는 책을 통해 추가 학습한다면 HTTP에 대해 더 깊이 있게 이해할 수 있을 것이다.
요구사항을 구현하는 과정에서 메서드를 분리하는 간단한 리팩토링을 진행했을 뿐 리팩토링을 거의 하지 않다보니 소스코드의 복잡도가 급격하게 증가하고 있다. 실제로 웹 애플리케이션을 개발하면 수십, 수백 개의 서로 다른 요청을 처리할 필요가 있는데, 현재 소스코드로는 확장 가능하지도 않고 기능이 추가될 때마다 복잡도는 빠르게 증가할 가능성이 높다. 다음 장은 새로운 지식을 학습하기보다 이 장에서 구현한 웹 서버를 리팩토링하는 과정을 다루면서 2장에서 학습한 테스트와 리팩토링을 복습하도록 한다. 리팩토링을 하면서 객체지향 설계와 인터페이스를 활용한 다형성에 대해 살펴보도록 한다.
참고도서 : https://roadbook.co.kr/169
[신간안내] 자바 웹 프로그래밍 Next Step
● 저자: 박재성 ● 페이지: 480 ● 판형: 사륙배변형(172*225) ● 도수: 1도 ● 정가: 30,000원 ● 발행일: 2016년 9월 19일 ● ISBN: 978-89-97924-24-0 93000 [강컴] [교보] [반디] [알라딘] [예스24] [인터파크] [샘
roadbook.co.kr
'교재 실습 > 자바 웹 프로그래밍 Next Step' 카테고리의 다른 글
4.3.1 HTTP (0) | 2025.03.13 |
---|---|
4.3 추가 학습 자료 (0) | 2025.03.13 |
4.2.1.6 요구사항 6 - 사용자 목록 출력 (3) | 2025.03.11 |
4.2.1.5 요구사항 5 - 로그인하기 (0) | 2025.02.25 |
4.2.1.4 요구사항 4 - 302 status code 적용 (2) | 2025.02.21 |
댓글