로컬 개발 환경 구축
https://dev-ljw1126.tistory.com/294
실습 프로젝트 저장소
실습의 경우 처음에 fork 받았는데, 깃 허브 잔디가 심어지지 않아 기술 블로그 참고(링크)하여 저장소 설정을 변경하도록 함
web-application-server (3 ~ 6장)
https://github.com/slipp/web-application-server
웹 서버 실습 요구사항
1. index.html 응답하기(p96)
Http Request Header 예시
GET /index.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: */*
요청 헤더를 Parsing 하여 Map 자료 구조에 담은 후 지정된 위치의 index.html 파일을 읽어와 응답할 수 있도록 함
RequestHandler 클래스
public void run() {
try(InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
// 1. 요청 헤더 Parsing
BufferedReader br = new BufferedReader(new IntputStreamReader(in, StandardCharsets.UTF_8));
String line = br.readLine();
if(line == null) {
return;
}
String[] tokens = line.split(" ");
String url = tokens[1]; // 요청 주소
Map<String, String> headerMap = new HashMap();
while(!"".equals(line = br.readLine())) {
log.debug("{}", line);
String[] headerTokens = line.split(": ");
headerMap.put(headerTokens[0], headerTokens[1]);
}
// 2. index.html 응답 처리
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
} catch(IOException e) {
log.error(e.getMessage());
}
}
response200Header() 와 responseBody() 메서드
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"); // response body와 구분하기 위해 한 줄 띄우기*
} 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());
}
}
2. GET 방식으로 회원가입하기
① "회원가입" 메뉴를 클릭하면 http://localhost:8080/usrt/form.html 이동하여 회원가입 가능
② queryString 형태로 사용자가 입력한 값을 파싱해 model.User 클래스에 저장
GET /user/creeate?userId=javajigi&password=password&name=unknown
HTTP/1.1
*HttpRequestUtils.parseQuerString(String) 을 사용하면 Map<String 키, String 값> 형태로 반환해준다. 이를 활용하도록 하자
db.DataBase 클래스
Map 자료구조 활용
public class DataBase {
private static Map<String, User> users;
static {
users = Maps.newHashMap();
users.put("test1", new User("test1", "1234", "테스터1", ""));
users.put("test2", new User("test2", "1234", "테스터2", ""));
users.put("test3", new User("test3", "1234", "테스터3", ""));
}
public static void addUser(User user) {
users.put(user.getUserId(), user);
}
public static User findUserById(String userId) {
return users.get(userId);
}
public static Collection<User> findAll() {
return users.values();
}
}
model.User 클래스
public class User {
private String userId;
private String password;
private String name;
private String email;
public User(String userId, String password, String name, String email) {
this.userId = userId;
this.password = password;
this.name = name;
this.email = email;
}
// getter 생략
}
RequestHandler 클래스
*.html 요청사항과 GET /user/create 를 구분하기 위해 조건문 분기 작성
public void run() {
try(..) {
// parsing 생략
DataOutputStream dos = new DataOutputStream(out);
if(url.startsWith("/user/create") {
int idx = url.indexOf("?");
String queryString = url.subString(idx + 1);
Map<String, String> userMap = HttpRequestUtils.parseQueryString(queryString);
User user = new User(userMap.get("userId"), userMap.get("password"),
userMap.get("name"), userMap.get("email"));
DataBase.addUser(user);
log.debug("User : {}", user);
} else {
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch(..) {
}
}
*우선 유효성 검사나 성공시 리다이렉트 처리는 하지 않음
3. POST 방식으로 회원가입하기
① http://localhost:8080/usrt/form.html 파일의 form 태그의 method를 post로 수정한 후 회원가입이 정상동작하도록 구현
② request body 에 데이터 담긴다
POST /user/create HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 59
Content-Type: application/x-www-form-urlencoded
Accept: */*
userId=javajigi&password=password&name=unknown
*Header와 Body 사이에 한 줄을 띄어서 구분한다
RequestHandler 클래스
-요구사항2의 if 조건문 안에서 0 ~ Content-Length만큼 BufferReader에 있는 Request Body 데이터를 읽어와 처리하도록 변경
-이때 IOUtils 클래스는 실습에서 제공되는 클래스이다
public void run() {
try(..) {
// parsing 생략
DataOutputStream dos = new DataOutputStream(out);
if(url.startsWith("/user/create") {
// 여기만 변경
String requestBody = IOUtils.readData(br, Integer.parseInt(headerMap.get("Content-Length"));
Map<String, String> userMap = HttpRequestUtils.parseQueryString(requestBody);
User user = new User(userMap.get("userId"), userMap.get("password"),
userMap.get("name"), userMap.get("email"));
DataBase.addUser(user);
log.debug("User : {}", user);
} else {
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch(..) {
}
}
4. 302 status code 적용
- "회원가입" 완료 후 /index.html 페이지로 이동하도록 하도록 한다 (redirect)
- 이때 브라우저의 URL도 /user/create가 아니라 /index.html로 변경해야 한다
참고. https://en.wikipedia.org/wiki/HTTP_302
① 302 상태 코드 사용 전
회원 가입 후 200 상태 코드로 forwarding(index.html)하게 될 경우, 화면은 index.html로 변하지만 브라우저 주소창 URI는 "/user/create"가 유지 되고 있음 → 이때 새로고침할 경우 중복 회원가입 발생
② 302 상태 코드 사용 후
302 상태 코드 응답시 웹 브라우저는 Location 정보로 redirect 수행 → 브라우저 주소가 /index.html로 변경되고 엔터 눌러진 상태와 같아짐
RequestHandler 클래스
회원가입 완료 후 응답 코드 302로 하여 /index.html로 redirect 시킨다
private void response302Hedaer(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 302 Found\r\n");
dos.writeBytes("Location: /index.html\r\n");
dos.writeBytes("\r\n");
} catch(IOException e) {
log.error(e.getMessage());
}
}
public void run() {
try(..) {
// parsing 생략
DataOutputStream dos = new DataOutputStream(out);
if(url.startsWith("/user/create") {
String requestBody = IOUtils.readData(br, Integer.parseInt(headerMap.get("Content-Length"));
Map<String, String> userMap = HttpRequestUtils.parseQueryString(requestBody);
User user = new User(userMap.get("userId"), userMap.get("password"),
userMap.get("name"), userMap.get("email"));
DataBase.addUser(user);
log.debug("User : {}", user);
response302Header(dos); // 여기 추가 *
} else {
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch(..) {
}
}
5. 로그인 하기
"로그인" 메뉴를 클릭하면 http://localhost:8080/user/login.html 로 이동해 로그인 할 수 있다
① 로그인 성공시 응답 헤더에 Set-Cookie: logined=true 추가 후 /index.html로 이동해야 한다(이때 응답 코드 302 Found)
② 로그인 실패시 응답 헤더에 Set-Cookie: logined=false 추가, 이때 응답 코드는 401 Unauthorized이고, /user/login_failed.html로 이동해야 한다
RequestHandler 클래스
마찬가지로 조건문 분기를 통해 처리하도록 한다. 로그인 페이지로 가는 URL과 로그인 POST 요청을 잘 구분하도록하자
public void run() {
try(..) {
// parsing 생략
DataOutputStream dos = new DataOutputStream(out);
if(url.startsWith("/user/create") {
// 중략..
} else if(url.startsWith("/user/login" && "POST".equals(tokens[0])) {
String requestBody = IOUtils.readData(br, Integer.paraseInt(headerMap.get("Content-Length"));
Map<String, String> userMap = HttpRequestUtils.parseQueryString(requestBody);
User user = DataBase.findUserById(userMap.get("userId"));
if(user == null) {
responseLoginFail(dos);
}
// User model에 method를 만드는게 좀 더 나을 거 같다
if(user.getPassword().equals(userMap.get("password")) {
response302HeaderWithCookie(dos);
} else {
responseLoginFail(dos);
}
} else {
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch(..) {
}
}
response302HeaderWithCookie() 와 responseLoginFail() 메서드
private void response302HeaderWithCookie(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 302 Found\r\n");
dos.writeBytes("Location: /inedx.html\r\n");
dos.writeBytes("Set-Cookie: logined=true\r\n");
dos.writeBytes("\r\n");
} catch(IOException e) {
log.error(e.getMessage());
}
}
private void responseLoginFail(DataOutputStream dos) {
byte[] body = Files.readAllBytes(new File("./webapp/user/login_failed.html").toPath());
response401HeaderWithCookie(dos);
responseBody(dos, body);
}
private void response401HeaderWithCookie(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 401 Unauthorized\r\n");
dos.writeBytes("Content-Type: text/html;charset=utf-8;\r\n");
dos.writeBytes("Set-Cookie: logined=false\r\n");
dos.writeBytes("\r\n");
} catch(IOException e) {
log.error(e.getMessage());
}
}
6. 사용자 목록 출력
http://localhost:8080/user/list 로 접근했을 때
① 접근하고 있는 사용자가 "로그인 상태"인 경우(Cookie: logined=true) 사용자 목록을 출력한다
② 만약 로그인 하지 않은 경우 /login.html로 302 Found Redirect 이동한다
RequestHandler 클래스
Cookie 값을 Parsing 하기 위해 HttpRequestUtils.parseCookies(..) 메서드를 활용한다
public void run() {
try(..) {
// parsing 생략
DataOutputStream dos = new DataOutputStream(out);
if(url.startsWith("/user/create") {
// 중략..
} else if(url.startsWith("/user/login" && "POST".equals(tokens[0])) {
// 중략..
} else if(url.startsWith("/user/list")) {
Map<String, String> cookieMap = HttpRequestUtils.parseCookies(headerMap.get("Cookie"));
boolean logined = Boolean.parseBoolean(cookieMap.get("logined"));
if(logined) { // 로그인 한 경우
StringBuilder sb = new StringBuilder();
List<User> users = new ArrayList<>(DataBase.findAll());
sb.append("<table>");
users.forEach(user -> sb.append("<tr><td>" + user.getName() + "<td></tr>"));
sb.append("</table>");
String body = sb.toString();
response200Header(dos, body.length());
responseBody(dos, body.getBytes());
} else { // 로그인 하지 않은 경우
response302Header(dos);
}
} else {
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
} catch(..) {
}
}
7. CSS 지원하기
Http Request Header 예시
GET /css/style.css HTTP/1.1
Host: localhost:8080
Accept: text/css, */*;q=0.1
Connection: keep-alive
RequestHandler 클래스에서 .css로 끝나는 url의 경우 css 파일을 읽어 byte 코드로 응답해주면 되었다. 이때 응답 헤더의 Content-type: text/css으로 전송한다.
public class RequestHandler extends Thread {
// 중략
public void run() {
log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
// header parsing ..
// 조건문 분기..
} else if(url.endsWith(".css")) {
log.debug("request css : {}", url);
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200HeaderWithCss(dos, body.length);
responseBody(dos, body);
} else {
//..
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
private void response200HeaderWithCss(DataOutputStream dos, int lengthOfBodyContent) {
try {
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes("Content-Type: text/css;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());
}
}
}
결과 화면
솔직히 말해서 쉽지 않았다..
개발 경력은 있지만, 직접 요청/응답 처리를 어떻게 처리해야 할 지 알지 못했기 때문에 시간 소비를 오래 하였다.
" HttpServletRequest, HttpServletResponse 어딨어?! "
결국에는 byte 데이터를 Response Body 에 넣고, Response Header에 메타를 적절하게 작성하면 브라우저에서 처리된다는 사실을 알 수 있었다
'독서 > 📚' 카테고리의 다른 글
[Next Step] 6장 서블릿/JSP를 활용해 동적인 웹 애플리케이션 개발하기 (0) | 2023.11.16 |
---|---|
[Next Step] 5장 웹 서버 리팩토링, 서블릿 컨테이너와 서블릿의 관계 (0) | 2023.11.16 |
[Next Step] 12.8 웹서버 도입을 통한 서비스 운영(p458) 정리 (0) | 2023.11.10 |
[Next Step] 10.4 배포 자동화를 위한 쉘 스크립트 개선 (p362) 정리 (0) | 2023.11.10 |
[Next Step] 6.6 쉘 스크립트를 활용한 배포 자동화(p218) 정리 (0) | 2023.11.03 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!