Skip to content

Commit

Permalink
[카프카] 1단계 - HTTP 웹 서버 구현 미션 제출합니다. (#132)
Browse files Browse the repository at this point in the history
* feat: RequestUtils 구현

- Request에 대한 Util 정적 메서드를 저장하는 RequestUtils 클래스 구현
- RequestUtils 클래스에 대한 테스트 클래스 구현

* feat: index.html로 연결되도록 구현

- 요구사항 1에 맞춰 구현
- RequestHeader의 내용을 RequestUtils를 통해 분석하여, 적절한 파일을 반환함
- js, css 파일에 관련된 에러 이슈가 있어, 일단 html은 template로, 나머지는 static으로 경로를 잡도록 지정함

* feat: 잘못된 파일 경로 전달 시 예외 발생 구현

- RequestUtils에 전달되는 requestHeader의 값을 String으로 변경
- RequestUtils에 잘못된 파일 경로가 GET으로 요청될 시, InvalidFilePathException 발생
- 커스텀 예외 클래스인 InvalidFilePathException 구현
- RequestUtilsTest에 예외 테스트 추가

* feat: RequestUtils의 로직 수정 및 css 로딩 이슈 해결

- 요구사항 5에 해당하는 css 로딩 이슈 해결. RequestHeader의 Accept 값을 response시 전달하도록 구현함
- RequestUtils에 transferRequestHeaderToMap 메서드 추가. 아직 RequestHeader 클래스가 없어서, 그 대신 HashMap을 사용한다. RequestHeader의 메서드, 경로, 버전, 그리고 각각의 파라미터들을 매핑하여 반환한다.

* feat: HttpRequest 클래스 구현, 기존 로직 일부 대체

- HttpRequest 클래스를 구현함. 여기에는 Http Request의 메서드 타입, 경로, 버전 정보 및 기타 인자 정보가 저장됨
- HttpRequest에 잘못된 값이 전달된 채로 생성되면, InvalidHttpRequestException 가 발생하도록 구현
- HttpRequest의 생성과 예외처리에 대한 테스트 코드 구현
- RequestUtils의 Map 생성 로직을 HttpRequest 클래스로 대체함. 추후 경로 계산 로직 역시 점진적으로 대체할 예정

* test: HttpRequest에 대한 테스트 코드 보강

- HttpRequestTest의 테스트 코드 리팩토링
- HttpRequest의 getContentType에 대한 테스트 코드 추가

* docs: README.md 작성

- README.md에 진행내역 체크리스트 작성

* feat: HttpRequest의 요청 타겟을 별도 클래스로 분리

- HttpRequest에 포함되는 요청 타겟을 RequestPath로 분리하였음
- RequestPath는 타겟(파일명이나 요청명)과 파라미터(존재하지 않을 경우 빈 맵으로 저장)를 가짐

* feat: 회원가입 로직 구현

- RequestPath를 이용해 parameters를 mapping함
- User 클래스에 맵을 입력받아 생성하는 로직을 구현함
- RequestHandler에 로직 구현. user/create가 요청될 경우, 기존 로직 대신 user를 생성해 DB에 넣는 로직 수행함
- 2단계 요구사항 해결

* feat: RequestUtils 삭제, RequestPath에 대한 테스트 구현

- RequestPath의 생성, 예외, pathParams에 대한 테스트 구현
- RequestUtils는 RequestPath와 HttpRequest로 대체되어 삭제
- RequestPath의 parameters라는 필드변수를 pathParameters로 이름 변경하여 의미를 명확히 함

* feat: Parameters에 대한 연산을 ParameterMapper에 담고, 이를 상속하여 RequestPath를 구현하도록 리팩토링

- 추상 클래스 ParameterMapper를 만들어, Parameter HashMap, Getter, validator 등을 담도록 구현하였음. 또한 Parameter를 쪼개는 메인 로직을 옮겨, 추후 RequestBody 구현 시 재활용하도록 함
- RequestPath가 ParameterMapper를 상속하도록 구조 변경

* feat: ParameterMapper에 대한 테스트 코드 구현, RequestBody 구현

- RequestBody 구현. ParameterMapper의 로직을 그대로 쓰고, 별도의 validator만 사용함
- ParameterMapper에 대한 테스트 코드 구현. ParameterMapper가 추상 클래스이므로 테스트 클래스 내에 inner class를 만들어 테스트에 활용하였음. 다양한 예외 경우에 대해 테스트 수행함

* feat: URI 처리를 위한 URIUtils 구현, TEST 작성

* feat: 회원가입을 POST로 변경, body가 없는 경우 대응

- 회원가입을 수행하는 form.html의 form method를 POST로 변경하였음
- HttpRequest에 RequestBody를 넣어, POST로 온 요청이 있을 경우 body에 있는 값을 받도록 함
- 만약 body가 빈 request가 올 경우, RequestBody는 빈 생성자를 호출함. 이는 오류로 인해 빈 값을 호출하는 것과 분리됨

* feat: 회원가입 완료시 302를 response로 반환, index로 리다이렉트 구현

- 기존 RequestBody에 관련된 오류 수정, IOUtils를 사용하여 기존 로직 대체
- 회원가입 완료 시, 로그를 출력한 뒤 response의 메서드를 302로 반환, Location 값을 index로 부여

* refactor: HttpRequest의 헤더 생성 로직 수정

- HttpRequest에서 헤더의 값이 올바르지 않아도 수행되는 오류 발견, 테스트와 함께 수정

* docs: README.md 작성, 현재 진행해야 할 사항 todo로 정리

* feat: 파일의 경로를 정리해서 구하기 위해 FileType Enum 클래스 구현

- URIUtils에서 파일의 경로를 구할 때, 그 파일이 static인지 template인지 구별하는 로직 필요. 이를 enum 클래스로 구현

* style: 코드 전체의 인덴트와 import 정리, 불필요한 log 코드 삭제
  • Loading branch information
include42 authored Sep 19, 2020
1 parent aad12e4 commit b4af2b5
Show file tree
Hide file tree
Showing 21 changed files with 714 additions and 22 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# 웹 애플리케이션 서버
## 진행 방법
* 웹 애플리케이션 서버 요구사항을 파악한다.
* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다.
* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다.
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.
## 요구사항
- [x] http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다.
- [x] “회원가입” 메뉴를 클릭하면 http://localhost:8080/user/form.html 으로 이동하면서 회원가입할 수 있다.
- [x] http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한 후 회원가입 기능이 정상적으로 동작하도록 구현한다.
- [x] “회원가입”을 완료하면 /index.html 페이지로 이동하고 싶다. 현재는 URL이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다. 따라서 redirect 방식처럼 회원가입을 완료한 후 “index.html”로 이동해야 한다.
- [x] 지금까지 구현한 소스 코드는 stylesheet 파일을 지원하지 못하고 있다. Stylesheet 파일을 지원하도록 구현하도록 한다.

## 우아한테크코스 코드리뷰
* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)
13 changes: 13 additions & 0 deletions src/main/java/exception/InvalidHttpRequestException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package exception;

public class InvalidHttpRequestException extends RuntimeException {
private static final String INVALID_FILE_PATH_MASSAGE = "Http Request의 값이 올바르지 않습니다";

public InvalidHttpRequestException() {
this(INVALID_FILE_PATH_MASSAGE);
}

public InvalidHttpRequestException(String message) {
super(message);
}
}
13 changes: 13 additions & 0 deletions src/main/java/exception/InvalidRequestBodyException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package exception;

public class InvalidRequestBodyException extends RuntimeException {
private static final String INVALID_PATH_MESSAGE = "Request Body의 값이 올바르지 않습니다";

public InvalidRequestBodyException() {
this(INVALID_PATH_MESSAGE);
}

public InvalidRequestBodyException(String message) {
super(message);
}
}
12 changes: 12 additions & 0 deletions src/main/java/exception/InvalidRequestParamsException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package exception;

public class InvalidRequestParamsException extends RuntimeException {
private static final String INVALID_PARAMS_MESSAGE = "Request에 포함된 인자의 값이 올바르지 않습니다";
public InvalidRequestParamsException() {
this(INVALID_PARAMS_MESSAGE);
}

public InvalidRequestParamsException(String message) {
super(message);
}
}
13 changes: 13 additions & 0 deletions src/main/java/exception/InvalidRequestPathException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package exception;

public class InvalidRequestPathException extends RuntimeException {
private static final String INVALID_PATH_MESSAGE = "Request Path의 값이 올바르지 않습니다";

public InvalidRequestPathException() {
this(INVALID_PATH_MESSAGE);
}

public InvalidRequestPathException(String message) {
super(message);
}
}
13 changes: 13 additions & 0 deletions src/main/java/exception/RequestParameterNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package exception;

public class RequestParameterNotFoundException extends RuntimeException {
private static final String NOT_FOUND_PARAMS_MESSAGE = "해당 값을 인자 목록에서 찾을 수 없습니다";

public RequestParameterNotFoundException() {
super(NOT_FOUND_PARAMS_MESSAGE);
}

public RequestParameterNotFoundException(String key) {
super(key + " : " + NOT_FOUND_PARAMS_MESSAGE);
}
}
9 changes: 9 additions & 0 deletions src/main/java/model/User.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package model;

import java.util.Map;

public class User {
private String userId;
private String password;
Expand All @@ -13,6 +15,13 @@ public User(String userId, String password, String name, String email) {
this.email = email;
}

public User(Map<String, String> parameters) {
this.userId = parameters.get("userId");
this.password = parameters.get("password");
this.name = parameters.get("name");
this.email = parameters.get("email");
}

public String getUserId() {
return userId;
}
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/utils/URIUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package utils;

import exception.InvalidRequestPathException;
import web.FileType;

import java.util.Objects;

public class URIUtils {

/**
* getFilePathInRequest는 전달받은 파일 경로가 유효한지 확인하고, 파일 경로를 생성하여 반환한다.
*
* @param path Request에 포함되어 전달되는 파일 경로 값으로, String 객체가 전달된다.
* @return 파일의 경로가 String 객체로 반환된다.
* @throws InvalidRequestPathException 유효하지 않은 경로가 전달될 시 발생한다.
*/
public static String getFilePath(String path) {
try {
Objects.requireNonNull(path);
String resourcePath = FileType.findFileType(path).getResourcePath();
Objects.requireNonNull(resourcePath);

if (path.isEmpty() || resourcePath.isEmpty()) {
throw new ArrayIndexOutOfBoundsException();
}

return resourcePath + path;
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
throw new InvalidRequestPathException();
}
}
}
45 changes: 45 additions & 0 deletions src/main/java/web/FileType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package web;

import exception.InvalidRequestPathException;

import java.util.Arrays;

public enum FileType {
HTML(".html", "./templates", "text/html"),
ICO(".ico", "./templates", "image/vnd.microsoft.icon"),
CSS(".css", "./static", "text/css"),
JS(".js", "./static", "text/javascript"),
WOFF(".woff", "./static", "text/woff"),
PNG(".png", "./static", "image/png"),
JPEG(".jpeg", "./static", "image/jpeg"),
SVG(".svg", "./static", "image/svg_xml");

private final String fileType;
private final String resourcePath;
private final String contentType;

FileType(String fileType, String resourcePath, String contentType) {
this.fileType = fileType;
this.resourcePath = resourcePath;
this.contentType = contentType;
}

public static FileType findFileType(String path) {
return Arrays.stream(FileType.values())
.filter(fileType -> path.endsWith(fileType.getFileType()))
.findAny()
.orElseThrow(InvalidRequestPathException::new);
}

public String getFileType() {
return fileType;
}

public String getResourcePath() {
return resourcePath;
}

public String getContentType() {
return contentType;
}
}
88 changes: 88 additions & 0 deletions src/main/java/web/request/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package web.request;

import exception.InvalidHttpRequestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import utils.IOUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class HttpRequest {
private static final Logger logger = LoggerFactory.getLogger(HttpRequest.class);

private final String method;
private final RequestPath requestPath;
private final String version;
private final Map<String, String> requestHeader;
private final RequestBody requestBody;

public HttpRequest(BufferedReader request) {
try {
String requestHeaderFirstLine = request.readLine();
logger.debug(requestHeaderFirstLine);
String[] tokens = requestHeaderFirstLine.split(" ");
method = tokens[0].trim();
requestPath = new RequestPath(tokens[1].trim());
version = tokens[2].trim();

requestHeader = mappingHeaders(request);
requestBody = mappingBodies(request);
} catch (IndexOutOfBoundsException | NullPointerException | IOException e) {
throw new InvalidHttpRequestException();
}
}

private Map<String, String> mappingHeaders(BufferedReader request) throws IOException {
Map<String, String> headers = new HashMap<>();

String line = request.readLine();
while (!Objects.isNull(line) && !line.isEmpty()) {
logger.debug(line);
String[] splitLine = line.split(":");
String key = splitLine[0].trim();
String value = splitLine[1].trim();

headers.put(key, value);
line = request.readLine();
}
return headers;
}

private RequestBody mappingBodies(BufferedReader request) throws IOException {
if (!method.equals("POST")) {
return new RequestBody();
}
int contentLength = Integer.parseInt(requestHeader.get("Content-Length"));
String requestBodyData = IOUtils.readData(request, contentLength);
if (Objects.isNull(requestBodyData) || requestBodyData.isEmpty()) {
return new RequestBody();
}
return new RequestBody(requestBodyData);
}

public String getMethod() {
return method;
}

public RequestPath getRequestPath() {
return requestPath;
}

public String getVersion() {
return version;
}

public RequestBody getRequestBody() {
return requestBody;
}

public String getContentType() {
String acceptInfo = requestHeader.get("Accept");

return acceptInfo.split(",")[0];
}
}
65 changes: 65 additions & 0 deletions src/main/java/web/request/ParameterMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package web.request;

import exception.InvalidRequestParamsException;
import exception.RequestParameterNotFoundException;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public abstract class ParameterMapper {
private final Map<String, String> parameters;

public ParameterMapper() {
this.parameters = new HashMap<>();
}

protected void mappingParameters(String params) {
validateParameters(params);
for (String parameter : params.split("&")) {
validateParameter(parameter);
String[] tokens = parameter.split("=");
String key = tokens[0];
String value = tokens[1];

parameters.put(key, value);
}
}

private void validateParameters(String params) {
if (Objects.isNull(params) || params.isEmpty() || !params.contains("=")) {
throw new InvalidRequestParamsException();
}
}

private void validateParameter(String parameter) {
if (parameter.isEmpty() || !parameter.contains("=") || parameter.equals("=")) {
throw new InvalidRequestParamsException();
}
try {
String key = parameter.split("=")[0];
String value = parameter.split("=")[1];
Objects.requireNonNull(key);
Objects.requireNonNull(value);
if (key.isEmpty() || value.isEmpty()) {
throw new InvalidRequestParamsException();
}
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
throw new InvalidRequestParamsException();
}
}

public Map<String, String> getParameters() {
return parameters;
}

public String getParameterByKey(String key) {
if (Objects.isNull(key) || key.isEmpty()) {
throw new RequestParameterNotFoundException();
}
if (!parameters.containsKey(key)) {
throw new RequestParameterNotFoundException(key);
}
return parameters.get(key);
}
}
22 changes: 22 additions & 0 deletions src/main/java/web/request/RequestBody.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package web.request;

import exception.InvalidRequestBodyException;

import java.util.Objects;

public class RequestBody extends ParameterMapper {
public RequestBody() {
super();
}

public RequestBody(String requestBody) {
validateBody(requestBody);
mappingParameters(requestBody);
}

private void validateBody(String requestBody) {
if (Objects.isNull(requestBody) || requestBody.isEmpty()) {
throw new InvalidRequestBodyException();
}
}
}
27 changes: 27 additions & 0 deletions src/main/java/web/request/RequestPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package web.request;

import exception.InvalidRequestPathException;

import java.util.Objects;

public class RequestPath extends ParameterMapper {
private final String target;

public RequestPath(String path) {
validatePath(path);
target = path.split("\\?")[0];
if (path.contains("?")) {
mappingParameters(path.split("\\?")[1]);
}
}

private void validatePath(String path) {
if (Objects.isNull(path) || path.isEmpty()) {
throw new InvalidRequestPathException();
}
}

public String getTarget() {
return target;
}
}
Loading

0 comments on commit b4af2b5

Please sign in to comment.