1. index.html에 대한 응답
가. 준비단계
1) 요구사항 정리
•
•
응답
→ 로컬 저장소의 index.html 파일 읽기
→ index.html 파일을 응답으로 전송
나. 클라이언트의 Request URI에서 파일 정보 추출
1) Request Header 읽기
•
HTTP Request Header 전체 읽기
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
// InputStream을 line by line으로 변환
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String line = br.readLine();
// HEADER 부분에 내용이 더이상 없는 경우
if (line == null) {
return;
}
// HEADER의 내용을 한줄씩 읽어서 출력
while (!"".equals(line)) {
log.debug("header: {}", line);
line = br.readLine();
}
} catch (IOException e) {
log.error(e.getMessage());
}
Java
복사
2) HEADER에서 원하는 정보 추출하기
•
HEADER의 Request Line 중 URI에서 index.html 부분 추출하기
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String line = br.readLine();
if (line == null) {
return;
}
String[] splited = line.split(" ");
String path = splited[1];
Java
복사
다. 서버 로컬 저장소의 특정 파일 전송
1) 로컬 저장소의 파일을 byte[]로 읽기
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(new File("./webapp" + path).toPath());
Java
복사
2) 파일을 응답으로 전송
response200Header(dos, body.length);
responseBody(dos, body);
Java
복사
라. 리팩토링
1) URL 추출 부분에 대해 메소드 분할
// HttpRequestUtils.java
public static String getUrl(String line) {
String[] splited = line.split(" ");
log.debug("request path: {}", splited[1]);
return splited[1];
}
Java
복사
2. GET 요청 처리
가. GET 방식의 요청에서 queryString 추출
1) Header의 Request Line에서 URI 추출
if (uri.startsWith("/user/create")) {
int index = uri.indexOf("?");
String queryString = uri.substring(index + 1);
log.debug("queryString {}", queryString);
}
Java
복사
2) 회원가입 필드 정보로 User 객체 생성
Map<String, String> params = HttpRequestUtils.parseQueryString(queryString);
User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email"));
log.debug("User: {}", user);
Java
복사
3) GET 요청 처리 완료 후 index.html로 이동
if (uri.startsWith("/user/create")) {
// 생략
uri = "/index.html";
}
Java
복사
3. POST 요청 처리
가. POST 요청의 Body에서 데이터 추출
1) Request Header 읽고 Content-Length 추출
•
헤더를 line by line으로 읽기
•
헤더를 ": " 기준으로 분할해서 String[]에 저장
•
String[]에서 Content-Length에 해당하는 부분을 Map 자료구조에 추가
String uri = HttpRequestUtils.getUri(line);
Map<String, String> headers = new HashMap<>();
while (!line.equals("")) {
log.debug("header: {}", line);
line = br.readLine();
String[] headerTokens = line.split(": ");
if (headerTokens.length == 2) { // contents length 부분
headers.put(headerTokens[0], headerTokens[1]);
}
}
Java
복사
2) Request Body 읽기 & 파싱
•
Request Body에 해당하는 부분(Content-Length만큼만) BufferedReader에서 읽어오기
•
Request Body에서 User의 속성에 해당하는 부분을 파싱
•
파싱한 값으로 User 객체 생성
if (uri.startsWith("/user/create")) {
String requestBody = IOUtils.readData(br, Integer.parseInt(headers.get("Content-Length")));
log.debug("requestBody {}", requestBody);
Map<String, String> params = HttpRequestUtils.parseQueryString(requestBody);
User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email"));
log.debug("User: {}", user);
uri = "/index.html";
}
Java
복사
4. 302 status code 적용
가. 개념 정리
1) Redirect 원리
•
클라이언트의 Request URI에 매칭되어 있는 새로운 URI를 서버에서 response로 전달함. 클라이언트는 reponse에 포함된 새로운 URI로 재요청을 실시함
→ Spring MVC의 'redirect:'도 위와 같은 원리로 동작함
2) Redirect 사용하는 경우
•
시스템 변화에 대한 Request의 경우 Redirect를 사용
•
Redirect 적용 X, 회원가입 요청
→ 클라이언트에서 폼 태그에 회원가입 정보를 담아서 회원 가입 요청(POST, user/create) 시도
→ 서버에서 클라이언트 요청 처리 후, 200 ok Response 전달
→ 클라이언트에서 새로고침 클릭
→ 똑같은 회원가입 요청 발생, 클라이언트의 회원가입 요청 Request 객체 그대로 존재
•
Redirect 적용 O, 회원가입 요청
→ 클라이언트에서 폼 태그에 회원가입 정보를 담아서 회원 가입 요청(POST, user/create) 시도
→ 서버에서 클라이언트 요청 처리 후, 303 SEE OTHER와 함께 새로운 URI를 포함하여 Response 전달
→ 클라이언트에서 Response에 포함된 새로운 URI로 요청 실행
→ 새로운 URI 요청에 대해 서버에서 200 ok 응답을 보냄
→ 클라이언트에서 새로고침을 눌러도, 회원가입 요청에 대한 객체가 사라졌기 때문에 회원가입 요청 발생 X
3) 3xx Status Code 정리
나. Redirect 구현
1) 303 상태를 포함한 Header 작성
•
303 status code는 Redirect로 GET 방식을 요구하는 것이기 때문에 response에 body 부분이 필요 없음
private void response303Header(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 303 SEE OTHER \r\n");
dos.writeBytes("Location: /index.html\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
Java
복사
2) Header 추가해서 response 생성
if (uri.startsWith("/user/create")) {
// 생략
DataOutputStream dos = new DataOutputStream(out);
response303Header(dos);
} else {
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(new File("./webapp" + uri).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
Java
복사
5. 로그인 요청 처리
가. 개념 정리
1) Set-Cookie 헤더 필드 활용
•
최초 Request에 대한 Response에 Set-Cookie 헤더에 logined=true 값을 할당하여 전송
•
두번째 Request의 Cookie 헤더에 logined=true 값 추출하여 확인
나. 구현
1) POST, '/user/login' 요청의 Message Body 확인
•
path.startsWith을 path.equals로 변경
→ path.startsWith의 경우 부분적으로 일치하면 true가 되기 때문에 /user/login.html 요청도 true로 인식하게 됨
•
user/login.html에서 요구하는 필드에 대한 로그를 찍어서 정상적으로 request에 포함되는지 확인
else if (path.equals("/user/login")) {
// 생략
Map<String, String> params = HttpRequestUtils.parseQueryString(body);
log.debug("userId: {}, password: {}", params.get("userId"), params.get("password"));
// 생략
}
Java
복사
2) DB에서 저장된 User 객체의 존재여부 확인
else if (path.equals("/user/login")) {
// 생략
User loginUser = DataBase.findUserById(params.get("userId"));
log.debug("loginUser: {}", loginUser);
if (loginUser == null) {
log.debug("User Not Found");
} else if (!loginUser.getPassword().equals(params.get("password"))) {
log.debug("Password Mismatch!");
} else {
log.debug("Login Success");
}
// 생략
}
Java
복사
3) Response의 Cookie 필드에 값 할당
if (loginUser == null) {
log.debug("User Not Found");
DataOutputStream dos = new DataOutputStream(out);
response302Header(dos, "/index.html");
} else if (!loginUser.getPassword().equals(params.get("password"))) {
log.debug("Password Mismatch!");
DataOutputStream dos = new DataOutputStream(out);
response302Header(dos, "/index.html");
dos.flush();
} else {
log.debug("Login Success");
DataOutputStream dos = new DataOutputStream(out);
response302HeaderWithCookie(dos, "/index.html", "logined=true");
dos.flush();
}
private void response302HeaderWithCookie(DataOutputStream dos, String location, String cookie) {
try {
dos.writeBytes("HTTP/1.1 302 FOUND \r\n");
dos.writeBytes("Location: " + location + "\r\n");
dos.writeBytes("Set-Cookie: " + cookie + "\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
Java
복사
4) browser의 network에서 쿠키 상태 확인
•
login request에 대한 response의 Headers 탭에서 Set-Cookie Header 안에서 logined=true 확인
•
index.html 외 나머지 파일의 경우, Cookies 탭 안에서 'logined' Cookie 확인 가능
→ 안보일 경우, 'show filtered out request cookies' 클릭
6. CSS 지원
가. 구현
1) Header 내 content-type에 css 반영
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());
}
}
Java
복사
2) request 경로 상의 파일이 css인 경우 처리
else if (path.endsWith(".css")) {
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(new File("./webapp" + path).toPath());
response200HeaderWithCss(dos, body.length);
responseBody(dos, body);
}
Java
복사
7. 기타
가. 프로젝트 세팅
•
IntelliJ의 Welcome 화면에서 'Open' X but 'Get from VCS' 사용
→ local에 remote repository를 clone하고 IntelliJ에서 open하는 방식 X.
→ 프로젝트 본 파일(java 코드 및 build 파일) 불러오는데 실패