-
Notifications
You must be signed in to change notification settings - Fork 309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[톰캣 구현하기 1, 2단계] 마코(이규성) 미션 제출합니다. #332
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
소소한 리뷰 남겼습니다!
오늘 밤이나 내일중으로 추가 리뷰 남길게요 ㅎㅎ
} | ||
|
||
public static HttpRequest read(InputStream inputStream) throws IOException { | ||
final var bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
InputStream은 try-with-resource 구문으로 닫아주고있지만, InputStreamReader와 BufferedReader는 그러지 않네요!
랩핑한 스트림을 닫을 때 내부 스트림은 함께 닫히지만, 그 반대는 아닌걸로 알고있어요!
이 친구들도 사용이 끝나면 닫아주는건 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
내부 코드를 확인해 보니 inputStream의 close()는 closed = true 처리를 해줄 뿐이고 다른 작업을 하지 않네요. BufferedReader의 close()는 생성할때 파라미터로 받은 Reader인 in을 in.close()를 호출하고 null처리를 해주는 식으로 처리해주고 있네요!😱 요 친구도 함께 닫아주도록 하겠습니다.
if (requestLine == null) { | ||
throw new InvalidHttpFormException(); | ||
} | ||
final var splitLine = requestLine.split(" "); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
" ", ": "와 같은 구분자를 상수로 정의해보는건 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아주 좋은 생각입니다💡
private static void parseUri(HttpRequest httpRequest, String uri) { | ||
if (uri.contains("?")) { | ||
final var queryStartIndex = uri.indexOf("?"); | ||
final var queryString = uri.substring(queryStartIndex + 1); | ||
final var parameters = parseParameters(queryString); | ||
httpRequest.addParameters(parameters); | ||
} | ||
|
||
final var path = getFilePath(uri); | ||
httpRequest.setUri(uri); | ||
httpRequest.setPath(path); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이친구가 사용되는 메서드 바로 밑으로 이동하면 더 보기 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static Map<String, String> parseParameters(String queryString) { | ||
Map<String, String> parameters = new HashMap<>(); | ||
String[] parametersArray = queryString.split("&"); | ||
for (String parameter : parametersArray) { | ||
String[] split = parameter.split("="); | ||
parameters.put(split[0], split[1]); | ||
} | ||
return parameters; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
http://localhost:8080/login?account 이렇게 요청이 들어오면 어떻게 될까요?! 그리고 어떻게 처리하는게 좋을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ArrayIndexOutOfBoundsException
이 발생하는군요!
private static Map<String, String> parseParameters(String queryString) {
return Arrays.stream(queryString.split(QUERY_STRING_DELIMITER))
.map(parameter -> parameter.split(KEY_VALUE_DELIMITER))
.filter(keyValuePair -> keyValuePair.length == 2)
.collect(Collectors.toMap(keyValuePair -> keyValuePair[0],
keyValuePair -> keyValuePair[1]));
}
위와 같이 keyValuePair의 length가 2인지 검증하도록 변경했습니다~
고민을 하다 보니 HttpRequest를 읽는 HttpRequestReader
에서 파싱을 하는 작업도 하는 것이 책임 분리가 필요하다고 생각하여 HttpParser
를 만들어서 파싱하는 책임을 이 친구한테 넘겼습니다.
if (session == null) { | ||
throw new SessionNotFoundException(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
올바르지 않은 세션정보가 넘어왔을 때 예외를 던지는 대신, 세션을 없는 친구처럼 여기는건 어떨까요?!
만료된 세션 정보로 요청을 보냈을 때, 접근자체가 불가능한 것 보다 로그아웃상태로 접근되는게 더 자연스러운 흐름일 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거 누가 짰죠???👀
|
||
if (account.isEmpty() || password.isEmpty()) { | ||
httpResponse.setHttpStatus(HttpStatus.OK); | ||
httpResponse.addHeader(HttpHeaders.LOCATION, "/500.html"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
400 페이지 하나 만드시죠 ㅎㅎ ㅋㅋㅋㅋ
상태코드는 왜 200인가요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오케이도 나쁘지 않죠
뷰도 추가하고 400으로 바꿨습니다
for (String parameter : parameters) { | ||
String[] keyValuePair = parameter.split("="); | ||
if (keyValuePair[0].equals("account")) { | ||
account = keyValuePair[1]; | ||
} | ||
if (keyValuePair[0].equals("password")) { | ||
password = keyValuePair[1]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
login 페이지에서 빈 칸으로 요청하면 어떻게될까요 ?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
account=&password=
이런 식으로 요청이 들어와서 ArrayIndexOutOfBoundsException이 발생하네요!!
자주 사용되는 body부분의 파싱이라 검증 로직을 추가하여 HttpParser
에서 다음과 같이 공통으로 처리하도록 변경했습니다~
public static Map<String, String> parseFormData(String formData) {
return Arrays.stream(formData.split(PARAMETER_SEPARATOR))
.map(parameter -> parameter.split(KEY_VALUE_DELIMITER))
.filter(keyValuePair -> keyValuePair.length == 2)
.collect(Collectors.toMap(keyValuePair -> keyValuePair[0],
keyValuePair -> keyValuePair[1]));
}
for (String parameter : parameters) { | ||
String[] keyValuePair = parameter.split("="); | ||
if (keyValuePair[0].equals("account")) { | ||
account = keyValuePair[1]; | ||
} | ||
if (keyValuePair[0].equals("password")) { | ||
password = keyValuePair[1]; | ||
} | ||
if (keyValuePair[0].equals("email")) { | ||
email = keyValuePair[1]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
얘도 마찬가지로 회원가입 페이지에서 빈 칸으로 요청하면 어떻게될까요 ?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위에서 정의한 HttpParser
재사용👍
httpResponse.addHeader(HttpHeaders.LOCATION, "/500.html"); | ||
return; | ||
} | ||
InMemoryUserRepository.save(new User(account, password, email)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사소한건데 이미 존재하는 account로 가입하려할 때 예외처리해주는건 어떤가요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다~
서비스 분리가 하고싶어지네요....ㅋㅋㅋㅋㅋ
private static void checkUser(HttpResponse httpResponse, String account, String password) { | ||
InMemoryUserRepository.findByAccount(account) | ||
.filter(user -> user.checkPassword(password)) | ||
.ifPresentOrElse(user -> loginSuccess(httpResponse, user), | ||
() -> loginFailed(httpResponse)); | ||
} | ||
|
||
private static void loginSuccess(HttpResponse httpResponse, User user) { | ||
httpResponse.setHttpStatus(HttpStatus.FOUND); | ||
httpResponse.addHeader(HttpHeaders.CONTENT_TYPE, TEXT_HTML); | ||
httpResponse.addHeader(HttpHeaders.LOCATION, INDEX_HTML); | ||
final var session = new Session(UUID.randomUUID().toString()); | ||
session.setAttribute("user", user); | ||
SessionManager.add(session); | ||
httpResponse.addCookie(new HttpCookie(HttpCookie.JSESSIONID, session.getId())); | ||
} | ||
|
||
private static void loginFailed(HttpResponse httpResponse) { | ||
httpResponse.setHttpStatus(HttpStatus.FOUND); | ||
httpResponse.addHeader(HttpHeaders.CONTENT_TYPE, TEXT_HTML); | ||
httpResponse.addHeader(HttpHeaders.LOCATION, "/401.html"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
얘도 사용하는 메서드 밑으로 옮겨주시면 읽을 때 더 편할 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오잉 여기 아닌가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
구조에 대한 리뷰를 드리고 싶었는데, 이미 구조를 깔끔하게 잘 잡아주셨네요 👍
아래 리뷰들은 변경을 요구하는 건 아니고 3단계때 반영해주셔도 좋을 것 같아요!
앞서 남긴 소소한 리뷰들 반영하시고 재요청 보내주세요!
하루살이 화이팅!
public static void handle(HttpRequest httpRequest, HttpResponse httpResponse) | ||
throws IOException { | ||
final var path = httpRequest.getPath(); | ||
HttpMethod method = httpRequest.getHttpMethod(); | ||
if (method == HttpMethod.GET && path.equals("/")) { | ||
httpResponse.setHttpStatus(HttpStatus.OK); | ||
httpResponse.setBody("Hello world!"); | ||
return; | ||
} | ||
if (method == HttpMethod.GET && path.isEmpty()) { | ||
httpResponse.setHttpStatus(HttpStatus.OK); | ||
httpResponse.addHeader(HttpHeaders.CONTENT_TYPE, TEXT_HTML); | ||
httpResponse.setBody(FileReader.readFile(INDEX_HTML)); | ||
return; | ||
} | ||
if (method == HttpMethod.GET && (path.equals("/login") || path.equals("/login.html"))) { | ||
Session session = httpRequest.getSession(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요청이 추가될 때 요청들이 모두 Handler의 handle 메서드에 추가되어 메서드가 너무 길어질 것 같은데요,
요청별로 하위 Handler을 만들어주는건 어떨까요?!
이건 이번 과정에서 수정을 요구하는 건 아니고, 3단계 리팩토링 때 고민해주세요!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
핸들러는 핸들링만 하도록 리팩토링 해보겠습니다 호호
HttpRequest httpRequest = HttpRequestReader.read(inputStream); | ||
final var httpResponse = new HttpResponse(); | ||
Handler.handle(httpRequest, httpResponse); | ||
HttpResponseWriter.write(outputStream, httpResponse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실제 서블릿 동작 과정과 유사하게 Handler에 httpRequest, httpResponse 객체를 넘기고 Handler가 httpResponse에 응답을 담아주는 것 정말 좋네용
따봉 드리겠슴니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
영광입니다👍
public void setPath(String path) { | ||
this.path = path; | ||
} | ||
|
||
public HttpCookie getCookie(String name) { | ||
return cookies.get(name); | ||
} | ||
|
||
public Session getSession() { | ||
return this.session; | ||
} | ||
|
||
public void setSession(Session session) { | ||
this.session = session; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Response에서는 응답을 담아주기 위해 setter가 필요했지만, Request는 클라이언트의 응답을 엔티티로 변환한것이니 도중에 변하는 것 보다 불변 객체로 만들어주는 게 좋을 것 같아요!
생성자도 빈 생성자를 열어두고 헤더/바디를 넣어주는 구조보단, 생성할 때 헤더/바디를 초기화시키는게 좋지 않을까?라는 생각이 드는데 어떻게 생각하시나요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오! Request는 불변이 충분히 가능하네요!!
다만, 파라미터가 너무 많아서 Builder를 통해 생성하도록 수정했습니다!
Response도 래핑을 한다거나 객체를 분리하면 가능할 것 같은데 요건 다음 단계에서 리팩토링하면서 고민해보곘습니다!
Kudos, SonarCloud Quality Gate passed! 0 Bugs 0.0% Coverage The version of Java (11.0.20.1) you have used to run this analysis is deprecated and we will stop accepting it soon. Please update to at least Java 17. |
축제처럼 뻥뻥 터져서 저도 버스터콜 이용해야겠네요. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하산하시죠!
깔끔한 코드 잘 봤습니다 🕶
3-4단계때 뵙겠습니다아
@@ -9,6 +9,7 @@ | |||
public class FileReader { | |||
|
|||
private static final String STATIC_RESOURCE_PATH = "static"; | |||
private static final String EXTENSION_HTML = ".html"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DEFAULT_FILE_EXTENSION 은 어떤가요?!
기본 확장자가 .html인걸 더 잘 알려주는 것 같아요!
public class ModelAndView { | ||
|
||
private final String viewName; | ||
private final Map<String, String> model = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은데요?
if (content == null) { | ||
StringBuilder sb = new StringBuilder(); | ||
for (final var value : model.values()) { | ||
sb.append(value); | ||
} | ||
httpResponse.addHeader(HttpHeaders.CONTENT_TYPE, DEFAULT_CHAR_SET); | ||
httpResponse.addHeader(HttpHeaders.CONTENT_LENGTH, | ||
String.valueOf(sb.toString().getBytes().length)); | ||
httpResponse.setBody(sb.toString()); | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
early return 👍
public static class Builder { | ||
|
||
private String protocol; | ||
private HttpHeaders headers; | ||
private HttpMethod httpMethod; | ||
private String uri; | ||
private String path; | ||
private Map<String, String> parameters; | ||
private String body; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Builder 👍
빌더가 여러 클래스에서 생길수도있으니 HttpRequestBuilder로 정확하게 명시해주는것도 좋을 것 같아요!
버스타고 사장님 반갑습니다~👋
결국 리뷰어로 만나게 됐네요. 잘 부탁드립니다~~
전체적인 흐름은 다음과 같습니다.
handler에서 핸들링하는 코드도 복잡할 뿐만 아니라 비즈니스 로직도 섞여있어서 리팩토링이 필요하나 고민이 더 필요할 것 같습니다.
부족한 점이나 궁금한 점, 피드백 얼마든지 환영합니다!😄