-
Notifications
You must be signed in to change notification settings - Fork 310
[3단계 - Tomcat 구현하기] 비토(오상훈) 미션 제출합니다. #594
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
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.
비토 3단계 다시 구현하시기 바랍니다. 코드 퀄리티에 신경 쓰세요.
테스트 코드도 제가 제시한 것 말고는 하나도 안 보이네요.
테스트 코드가 없는데 다른 개발자는 어떻게 정상 동작한다고 확신할 수 있죠?
꼼꼼하게 테스트도 추가하시기 바랍니다.
@@ -41,7 +40,7 @@ private static final class SynchronizedMethods { | |||
|
|||
private int sum = 0; | |||
|
|||
public void calculate() { | |||
public synchronized void calculate() { |
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.
왜 synchronized를 붙이라고 했을까요?
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.
만약 synchronized가 없으면 테스트 코드에서 실행되고 있는 1000개의 스레드 중 일부가 sum
이 정확히 변경되어 저장하기 전에 접근하게 되면서 정확하게 1000번 증가한 값이 나오지 않게 됩니다.
그래서 synchronized를 사용해 한번에 하나의 스레드만 접근 가능하게 하고 다른 스레드는 그동안 대기하게 만들어 스레드 안전성을 보장해줍니다.
@@ -31,8 +30,8 @@ void testNewFixedThreadPool() { | |||
executor.submit(logWithSleep("hello fixed thread pools")); | |||
|
|||
// 올바른 값으로 바꿔서 테스트를 통과시키자. | |||
final int expectedPoolSize = 0; | |||
final int expectedQueueSize = 0; | |||
final int expectedPoolSize = 2; |
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.
Executors.newFixedThreadPool(...)와 Executors.newCachedThreadPool()는 어떤 차이가 있나요?
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 ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
newFixedThreadPool
은 매개변수로 주어진 수만큼 스레드를 생성하고 해당 개수만큼의 스레드만 사용합니다. 그래서 해당 테스트에서 3개의 스레드를 실행시켰지만 2개의 스레드만 실행이 되고 남은 1개는 큐에서 대기하게 됩니다.
newCachedThreadPool
은 처음에는 스레드를 생성하지 않고 요청이 들어올때마다(코드상으론 Integer.MAX_VALUE만큼) 스레드를 생성하게 됩니다. 따라서 테스트에서 3번의 요청이 들어와서 3개의 스레드가 실행되고 있고 큐에서 대기하고 있는 스레드는 없습니다.
private final Set<String> unauthorizedPaths = new HashSet<>(); | ||
|
||
public UnauthorizedInterceptor() { | ||
unauthorizedPaths.add("/401"); |
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.
Set.of
를 통해 생성하는 방식으로 추가했는데 이게 더 좋은 건지는 모르겠습니다...
} | ||
|
||
public boolean checkPath(HttpRequest httpRequest) { | ||
return !httpRequest.containsKey("Location") && unauthorizedPaths.contains(httpRequest.getPath()); |
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.
Localtion 문자열 대신 HttpHeaderName을 활용해보시기 바랍니다.
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.
HttpHeaderName을 쓰도록 수정했고 관련된 다른 부분의 코드들도 전부 수정했습니다!
|
||
@Override | ||
protected HttpResponse doPost(HttpRequest httpRequest) { | ||
return null; | ||
throw new RuntimeException(); |
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.
RuntimeException 예외를 사용하지 마시고 적절한 예외를 사용하세요.
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.
예외 처리한 것 전부 적절한 예외로 수정했습니다!
import org.apache.coyote.http11.httpresponse.HttpStatusLine; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class LoginController extends AbstractController { | ||
|
||
private static final Logger log = LoggerFactory.getLogger(LoginController.class); | ||
private static final String LOGIN_PATH = "static/login.html"; | ||
|
||
private final Session session = Session.getInstance(); | ||
|
||
@Override | ||
protected HttpResponse doPost(HttpRequest httpRequest) { |
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.
메서드 분리와 역할 분담으로 간소화했습니다!
|
||
return new HttpResponse(httpStatusLine, httpResponseHeader); | ||
private boolean checkToken(String[] token) { | ||
return Arrays.stream(token).anyMatch(t -> t.split("=").length < 2); | ||
} | ||
|
||
@Override | ||
protected HttpResponse doGet(HttpRequest httpRequest) { |
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.
- 메서드 10줄이 넘지 않게 정리하시고
- 문자열 제거 하시고
- http에서 처리할 일을 컨트롤러에게 맡기지 마세요.
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.
문자열 전부 상수화 했고 역할을 이동하여 코드를 간소화했습니다!
log.info(user.toString()); | ||
HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.FOUND); | ||
return new HttpResponse.Builder(httpStatusLine) | ||
.setCookie("JSESSIONID=" + jsessionid) |
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.
JSESSIONID 계속 중복해서 보이네요
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 HttpResponse redirectLoginPage(HttpRequest httpRequest) throws URISyntaxException, IOException { | ||
HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); | ||
|
||
var resourceUrl = getClass().getClassLoader().getResource(LOGIN_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.
파일 입출력 처리를 전부 Response로 이동시켰습니다!
protected HttpResponse doGet(HttpRequest httpRequest) { | ||
try { | ||
String fileName = "static/404.html"; | ||
var resourceUrl = getClass().getClassLoader().getResource(fileName); |
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.
getClass().getClassLoader().getResource(...)
게속 반복해서 등장하네요. 중복을 제거하세요
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.
해당 부분을 staticResource
메소드에서만 사용하도록 하여 중복을 제거했습니다!
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 request 관련 로직도 복잡해보이는데 테스트 코드를 더 보충해보세요.
import org.apache.coyote.http11.HttpHeaderName; | ||
import org.apache.coyote.http11.httprequest.HttpRequest; | ||
|
||
public class UnauthorizedInterceptor { |
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.
- 이게 왜 인터셉터죠?
- 인터셉터가 필요한 상황인가요?
- 인터셉터가 was와 무슨 관련이 있죠?
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.
처음에 에러 페이지들인 401.html, 404.html, 500.html과 같은 페이지를 직접 호출해서 요청하는 건 막아야 된다고 생각해서 관련된 로직을 만드려고 하다가 스프링에서 사용했던 Interceptor와 비슷하다고 생각해서 만들었습니다.
지금보니 너무 과하게 생각했던 것 같습니다.
해당 클래스는 삭제했습니다!
@@ -1,4 +1,4 @@ | |||
package org.apache.coyote.http11.controller; | |||
package com.techcourse.controller; | |||
|
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 interface Controller {
void service(HttpRequest request, HttpResponse response) throws Exception;
}
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 final String ACCOUNT = "account"; | ||
private static final String PASSWORD = "password"; | ||
private static final String INDEX_PATH = "/index.html"; | ||
private static final String JSESSIONID = "JSESSIONID"; |
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.
세션 아이디를 컨트롤러에서 알 필요가 있을까요?
누구의 관심사일까요?
COOKIE_DELIMITER, SESSION_USER_NAME도 마찬가지입니다.
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.
cookie와 session과 관련된 정보는 각각 HttpResponse와 Session으로 옮겼습니다!
log.error(e.getMessage(), e); | ||
} | ||
} | ||
|
||
private HttpResponse getHttpResponse(HttpRequest httpRequest) throws IOException, URISyntaxException { |
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.
메서드를 10줄로 줄여보세요.
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.
StringBuilder
를 활용하여 간소화했습니다!
if (requestLine == null) { | ||
throw new IllegalArgumentException("요청이 비어 있습니다"); | ||
} | ||
if (requestLine.split(REQUEST_LINE_DELIMITER).length < 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.
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.
다시 한번 전체적으로 검사하여 상수화 처리 했습니다!
private static final int TUPLE_KEY_INDEX = 0; | ||
private static final int TUPLE_VALUE_INDEX = 1; | ||
private static final int HEADER_KEY_INDEX = 0; | ||
private static final SessionManager SESSION_MANAGER = new SessionManager(); |
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.
SessionManager
를 싱글톤으로 만들어 동일한 세션 매니저 객체를 사용하도록 수정했습니다!
return this; | ||
} | ||
|
||
public HttpResponseBuilder staticResource(String 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.
10줄 넘기지 말 것.
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 addHeaders(String key, String value) { | ||
headers.put(key, value); | ||
public String getString() { |
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.
getString이라 붙이면 무슨 동작을 하는지 다른 개발자가 메서드명만 보고 판단하기 어렵습니다.
그냥 문자열 반환이 아니라 HTTP 헤더를 스펙에 맞춰 생성하고 있으니 적절한 이름을 붙이세요.
안 그러면 어떤 의도인지 다른 개발자가 매번 코드를 파악해야 해요.
그리고 10줄 넘기지 마세요.
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.
createHeadersResponse
로 변경하고 StringBuilder
로 간소화했습니다!
int size = headers.keySet().size(); | ||
int i = 1; | ||
for (HttpHeaderName key : headers.keySet()) { | ||
if (i < size) { |
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.
2뎁스
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.
depth를 1로 줄였습니다!
|
||
public class SessionManager implements Manager { | ||
|
||
private static final Map<String, Session> SESSIONS = 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.
동시성 문제는 없습니까?
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.
해당 부분은 4단계라고 판단해서 HashMap
으로 했었습니다. Concurrent Collections
로 수정했습니다!
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.
코드가 훨씬 나아졌네요. pr 머지할테니 다음 단계 진행하시죠
안녕하세요 이상! 4단계 코드는 되돌리고 다시 리뷰 요청 보냅니다!
바뀐 점
리뷰 잘 부탁드립니다!