-
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
[톰캣 구현하기 - 3, 4단계] 땡칠(박성철) 미션 제출합니다. #471
Changes from 6 commits
a4b2bd7
1f6ab90
fa959af
04ee075
7dff12a
9d3d82c
a550e22
16cc50b
ef2ac0d
93015f9
5fdd9e8
5092d78
1c46ac7
5372bd0
4e85edc
d8f979d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,75 @@ | ||
package nextstep.jwp.handler; | ||
|
||
import java.util.Objects; | ||
import java.util.Optional; | ||
import nextstep.jwp.db.InMemoryUserRepository; | ||
import nextstep.jwp.model.User; | ||
import org.apache.coyote.http11.HttpRequest; | ||
import org.apache.coyote.http11.HttpResponse; | ||
import org.apache.coyote.http11.body.FormData; | ||
import org.apache.coyote.http11.handler.FileHandler; | ||
import org.apache.coyote.http11.handler.Handler; | ||
import org.apache.coyote.http11.handler.GetAndPostHandler; | ||
import org.apache.coyote.http11.header.Cookies; | ||
import org.apache.coyote.http11.session.Session; | ||
import org.apache.coyote.http11.session.SessionManager; | ||
|
||
public class LoginHandler implements Handler { | ||
public class LoginHandler extends GetAndPostHandler { | ||
|
||
private static final String SESSION_KEY = "JSESSIONID"; | ||
private static final String UNAUTHORIZED_LOCATION = "/401"; | ||
private static final String MAIN_LOCATION = "/index"; | ||
private static final String SESSION_USER_KEY = "user"; | ||
|
||
private final FileHandler fileHandler = new FileHandler(); | ||
|
||
@Override | ||
public HttpResponse handle(final HttpRequest httpRequest) { | ||
if (httpRequest.getMethod().isPost()) { | ||
FormData formData = FormData.of(httpRequest.getBody()); | ||
String account = formData.get("account"); | ||
String password = formData.get("password"); | ||
protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) { | ||
Session session = getSessionFrom(httpRequest.getCookies()); | ||
if (session != null && hasUserOn(session)) { | ||
httpResponse.redirectTo(MAIN_LOCATION); | ||
} | ||
fileHandler.handle(httpRequest, httpResponse); | ||
} | ||
|
||
Optional<User> found = InMemoryUserRepository.findByAccount(account); | ||
if (found.isEmpty()) { | ||
return HttpResponse.redirectTo(UNAUTHORIZED_LOCATION); | ||
} | ||
User user = found.get(); | ||
return signIn(httpRequest, user, password); | ||
private Session getSessionFrom(final Cookies cookies) { | ||
String sessionId = cookies.get(SESSION_KEY); | ||
if (sessionId == null) { | ||
return null; | ||
} | ||
if (httpRequest.getMethod().isGet()) { | ||
String jsessionId = httpRequest.getCookies().get(SESSION_KEY); | ||
if (jsessionId == null) { | ||
return fileHandler.handle(httpRequest); | ||
} | ||
Session session = SessionManager.findSession(jsessionId); | ||
if (session != null) { | ||
return HttpResponse.redirectTo(MAIN_LOCATION); | ||
} | ||
return SessionManager.findSession(sessionId); | ||
} | ||
|
||
private boolean hasUserOn(Session session) { | ||
return Objects.nonNull(session.getAttribute(SESSION_USER_KEY)); | ||
} | ||
|
||
@Override | ||
protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) { | ||
FormData formData = FormData.of(httpRequest.getBody()); | ||
String account = formData.get("account"); | ||
String password = formData.get("password"); | ||
|
||
Optional<User> found = InMemoryUserRepository.findByAccount(account); | ||
if (found.isPresent()) { | ||
User user = found.get(); | ||
signIn(httpResponse, user, password); | ||
return; | ||
} | ||
return fileHandler.handle(httpRequest); | ||
httpResponse.redirectTo(UNAUTHORIZED_LOCATION); | ||
} | ||
|
||
private HttpResponse signIn(final HttpRequest httpRequest, final User user, final String password) { | ||
private void signIn(final HttpResponse httpResponse, final User user, final String password) { | ||
if (user.checkPassword(password)) { | ||
Session session = new Session(); | ||
session.addAttribute("user", user); | ||
session.addAttribute(SESSION_USER_KEY, user); | ||
SessionManager.add(session); | ||
|
||
Cookies cookies = httpRequest.getCookies(); | ||
Cookies cookies = new Cookies(); | ||
cookies.add(SESSION_KEY, session.getId()); | ||
|
||
return redirectToMainWith(cookies); | ||
httpResponse.with(cookies).redirectTo(MAIN_LOCATION); | ||
return; | ||
} | ||
return HttpResponse.redirectTo(UNAUTHORIZED_LOCATION); | ||
} | ||
|
||
private HttpResponse redirectToMainWith(Cookies cookies) { | ||
HttpResponse httpResponse = HttpResponse.redirectTo(MAIN_LOCATION); | ||
httpResponse.putHeader(cookies.toHeader("Set-Cookie")); | ||
return httpResponse; | ||
httpResponse.redirectTo(UNAUTHORIZED_LOCATION); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,19 +20,19 @@ public class Http11Processor implements Runnable, Processor { | |
|
||
private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); | ||
|
||
private static final String HTTP_VERSION = "HTTP/1.1 "; | ||
private static final String CRLF = "\r\n"; | ||
|
||
private static final Handler DEFAULT_HANDLER = new FileHandler(); | ||
private static final LoginHandler LOGIN_HANDLER = new LoginHandler(); | ||
private static final RegisterHandler REGISTER_HANDLER = new RegisterHandler(); | ||
private static final Map<String, Handler> PREDEFINED_HANDLERS = Map.of( | ||
"/", httpRequest -> new HttpResponse("Hello world!", "text/html"), | ||
"/", (request, response) -> response.setBody("Hello world!"), | ||
"/login", LOGIN_HANDLER, | ||
"/login.html", LOGIN_HANDLER, | ||
"/register", REGISTER_HANDLER, | ||
"/register.html", REGISTER_HANDLER | ||
); | ||
private static final String WHITE_SPACE = " "; | ||
|
||
private final Socket connection; | ||
|
||
|
@@ -52,13 +52,14 @@ public void process(final Socket connection) { | |
final var outputStream = connection.getOutputStream()) { | ||
|
||
HttpRequest httpRequest = HttpRequest.from(bufferedReader); | ||
HttpResponse httpResponse = handle(httpRequest); | ||
HttpResponse httpResponse = new HttpResponse(); | ||
handle(httpRequest, httpResponse); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 handle 로직을 hanlerMapper 객체로 분리해내주셨으면 더 책임분리가 깔끔하게 되지 않았을까 싶어요! 땡칠도 잘 아실거라 생각하지만 이 handlerMapper 객체가 서블릿 컨테이너와 책임이 동일한 객체인지라 coyote 패키지의 Http11Processor와는 분리가 되는 것이 맞다는 생각입니다. 이 생각의 흐름으로 보면 땡칠께서 남겨주신 의문점이 해결되지 않을까 싶습니다. "response가 어디서든 수정 가능하다는 장점"이 왜 필요한 것일까요? 저는 Http11Processor에서 처음 생성된 request, response가 handlerMapper(=servletContainer)에서 mapping된 handler(servlet)을 통해 비즈니스 로직을 수행하고 해당 결과가 response에 담겨 client에게 다시 전달되어야 하기 때문이라고 생각합니다. 또한 response에 담기는 헤더 값들이 어떤 한 지점에서 바로 완성되는 것이 아니라 계속 여러 단계를 거치면서 추가되는 것이기 때문에 response 객체가 전달되어야 한다고도 생각할 수 있을 것 같습니다. 이 생각들은 저도 개인적으로 학습하고 갖고있는 생각이라 충분히 틀릴 수 있으니 땡칠께서 보시고 이상하다 싶은 부분은 편하게 말씀해주시면 감사하겠습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 응답이 동적으로 생성되기까지 수많은 과정 속에서 언제든 응답코드, 헤더, 바디를 수정하기 위함이라는 말씀이시네요! 이 말을 듣고보니까 예를 들어 서비스 객체가 있다면, 호출 스택의 끝인 서비스 객체에서 response를 만들어야할 것 같아요. 결론은 '어디서든 응답 수정을 쉽게 하려는 의도가 담긴 디자인이다'. 네요 |
||
|
||
String headerLines = httpResponse.getHeaders().stream() | ||
.map(HttpHeader::toLine) | ||
.collect(Collectors.joining(CRLF)); | ||
final var response = String.join(CRLF, | ||
HTTP_VERSION + httpResponse.getStatus().toLine(), | ||
HttpResponse.HTTP_VERSION + WHITE_SPACE + httpResponse.getStatus().toLine(), | ||
headerLines, | ||
"", | ||
httpResponse.getBody() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. client에게 내려줄 최종 response 형태를 만드는 책임도 HttpResponse 객체에 부여해보시는건 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 좋은 생각입니다 역시 👍 |
||
|
@@ -71,10 +72,12 @@ public void process(final Socket connection) { | |
} | ||
} | ||
|
||
private HttpResponse handle(final HttpRequest httpRequest) { | ||
private void handle(final HttpRequest httpRequest, final HttpResponse httpResponse) { | ||
if (PREDEFINED_HANDLERS.containsKey(httpRequest.getTarget().getPath())) { | ||
return PREDEFINED_HANDLERS.get(httpRequest.getTarget().getPath()).handle(httpRequest); | ||
PREDEFINED_HANDLERS.get(httpRequest.getTarget().getPath()) | ||
.handle(httpRequest, httpResponse); | ||
return; | ||
} | ||
return DEFAULT_HANDLER.handle(httpRequest); | ||
DEFAULT_HANDLER.handle(httpRequest, httpResponse); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,28 +9,19 @@ | |||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public class HttpRequest { | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static final String WHITE_SPACE = " "; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private final HttpMethod method; | ||||||||||||||||||||||||||
private final HttpTarget target; | ||||||||||||||||||||||||||
private final String version; | ||||||||||||||||||||||||||
private final RequestLine requestLine; | ||||||||||||||||||||||||||
private final Headers headers; | ||||||||||||||||||||||||||
private final String body; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private HttpRequest(final String method, final String target, final String version, | ||||||||||||||||||||||||||
final Headers headers, | ||||||||||||||||||||||||||
final String body) { | ||||||||||||||||||||||||||
this.method = new HttpMethod(method); | ||||||||||||||||||||||||||
this.target = new HttpTarget(target); | ||||||||||||||||||||||||||
this.version = version; | ||||||||||||||||||||||||||
public HttpRequest(final RequestLine requestLine, final Headers headers, final String body) { | ||||||||||||||||||||||||||
this.requestLine = requestLine; | ||||||||||||||||||||||||||
this.headers = headers; | ||||||||||||||||||||||||||
this.body = body; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public static HttpRequest from(BufferedReader reader) { | ||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||
String[] requestLineElements = reader.readLine().split(WHITE_SPACE); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
RequestLine requestLine = new RequestLine(reader.readLine()); | ||||||||||||||||||||||||||
Headers headers = new Headers(); | ||||||||||||||||||||||||||
String next; | ||||||||||||||||||||||||||
while (reader.ready() && !(next = reader.readLine()).isEmpty()) { | ||||||||||||||||||||||||||
|
@@ -45,9 +36,7 @@ public static HttpRequest from(BufferedReader reader) { | |||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. headers에서 contentLength만 따로 get 로직을 작성해주셨네요! headers 내부에 EMPTY 객체를 잘 만들어두셔서 해당 객체를 활용한 일관적인 처리방식으로 수행되지 못하는 것 같아 약간 아쉬운 부분 같습니다. 여기 headers에서 contentLength 값을 가져와서 로직 처리하는 부분은 저번과 같은데 제가 그때는 미처 생각하지 못했나봅니다...😭 반드시 수정해야 할 부분은 아니지만 여유가 되신다면 수정해보시는 것도 좋을 것 같아서 리뷰드립니다! |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
요런 느낌은 어떠신지 조심히 제안드려봅니당 😁 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위 커멘트와 이어지는 내용이네요. EMPTY 객체를 만들어두고 잘 활용하지 못하는 부분이 아쉬우셨군요. 일관적이지 못하다는 관점 충분히 이해가 갑니다! 이 메서드를 별도로 만든 의도는 그래서 '자주 사용하는 일반적인 헤더니까 걱정없이 정수 형태의 컨텐츠 길이를 얻자' 라는 의도로 일종의 헬퍼 메서드를 만들었습니다. 사실 로직의 위치만 다를 뿐, 히이로가 제안해주신 내용과 같은 코드인 것 같아요. 하지만 지금 구현한 헬퍼 메서드를 통해 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. null 처리 로직의 캡슐화가 주 목적이셨군요! 이해했습니다 ㅎㅎ 👍 |
||||||||||||||||||||||||||
return new HttpRequest( | ||||||||||||||||||||||||||
requestLineElements[0], | ||||||||||||||||||||||||||
requestLineElements[1], | ||||||||||||||||||||||||||
requestLineElements[2], | ||||||||||||||||||||||||||
requestLine, | ||||||||||||||||||||||||||
headers, | ||||||||||||||||||||||||||
new String(bodyBuffer) | ||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||
|
@@ -58,15 +47,15 @@ public static HttpRequest from(BufferedReader reader) { | |||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public HttpMethod getMethod() { | ||||||||||||||||||||||||||
return method; | ||||||||||||||||||||||||||
return requestLine.getMethod(); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public HttpTarget getTarget() { | ||||||||||||||||||||||||||
return target; | ||||||||||||||||||||||||||
return requestLine.getTarget(); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public String getVersion() { | ||||||||||||||||||||||||||
return version; | ||||||||||||||||||||||||||
return requestLine.getHttpVersion(); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public List<HttpHeader> getHeaders() { | ||||||||||||||||||||||||||
|
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.
정확한지는 잘 모르겠는데 제가 이해한대로 우선 적어보았습니다.
히이로는
최대 ThradPool의 크기는 250, 모든 Thread가 사용 중인(Busy) 상태이면 100명까지 대기 상태로 만들려면 어떻게 할까?
라는 요구사항을 어떻게 반영해야한다고 생각하시나요?
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.
네 저도 정확하게 땡칠과 같은 방식으로 이해하고 있습니다. 모든 쓰레드가 사용중일 떄 100개의 connection 요청들을 queueing하기 위해서 acceptCount로 ServerSocket을 생성할 때 OS에 저장되는 connection들에 대한 queue size를 100으로 설정해줍니다.
ThreadPool 생성 시 corePoolSize와 maximumPoolSize는 250으로 동일하게 설정했으니 시작부터 250개의 쓰레드가 생성되어 동작하면서 요청들을 처리하게 될 것 같습니다.
혹시 어떤 부분이 궁금하셨던건지 더 자세하게 따로 말씀해주시면 같이 고민해봐도 좋을 것 같습니다!
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.
답변해주신 내용이 바로 궁금한 부분이었습니다.
Accept Count가 OS가 queueing 할 크기(백로그 크기)를 결정하는 부분이라고 이해했어요!