Skip to content

[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

Merged
merged 74 commits into from
Sep 13, 2024

Conversation

unifolio0
Copy link

안녕하세요 이상! 4단계 코드는 되돌리고 다시 리뷰 요청 보냅니다!

바뀐 점

  1. Controller 세분화
  2. HttpCookie 클래스 생성
  3. HttpResponse에 Builder 패턴 적용

리뷰 잘 부탁드립니다!

Copy link
Contributor

@kang-hyungu kang-hyungu left a 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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 synchronized를 붙이라고 했을까요?

Copy link
Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executors.newFixedThreadPool(...)와 Executors.newCachedThreadPool()는 어떤 차이가 있나요?

Copy link
Author

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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좀 더 간결하게 작성할 방법 없을까요

Copy link
Author

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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localtion 문자열 대신 HttpHeaderName을 활용해보시기 바랍니다.

Copy link
Author

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeException 예외를 사용하지 마시고 적절한 예외를 사용하세요.

Copy link
Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드 이렇게 길게 쓰실건가요?

Copy link
Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 메서드 10줄이 넘지 않게 정리하시고
  2. 문자열 제거 하시고
  3. http에서 처리할 일을 컨트롤러에게 맡기지 마세요.

Copy link
Author

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSESSIONID 계속 중복해서 보이네요

Copy link
Author

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파일 입출력 처리를 왜 컨트롤러에서 하는거에요...

Copy link
Author

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getClass().getClassLoader().getResource(...)

게속 반복해서 등장하네요. 중복을 제거하세요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분을 staticResource메소드에서만 사용하도록 하여 중복을 제거했습니다!

Copy link
Contributor

@kang-hyungu kang-hyungu left a 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 이게 왜 인터셉터죠?
  2. 인터셉터가 필요한 상황인가요?
  3. 인터셉터가 was와 무슨 관련이 있죠?

Copy link
Author

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;

Copy link
Contributor

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;
}

Copy link
Author

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";
Copy link
Contributor

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도 마찬가지입니다.

Copy link
Author

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드를 10줄로 줄여보세요.

Copy link
Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3이 뭔가요

Copy link
Author

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

세션 매니저 객체를 다른데서 생성할 수도 있겠군요?

Copy link
Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10줄 넘기지 말 것.

Copy link
Author

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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getString이라 붙이면 무슨 동작을 하는지 다른 개발자가 메서드명만 보고 판단하기 어렵습니다.
그냥 문자열 반환이 아니라 HTTP 헤더를 스펙에 맞춰 생성하고 있으니 적절한 이름을 붙이세요.
안 그러면 어떤 의도인지 다른 개발자가 매번 코드를 파악해야 해요.

그리고 10줄 넘기지 마세요.

Copy link
Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2뎁스

Copy link
Author

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<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동시성 문제는 없습니까?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 4단계라고 판단해서 HashMap으로 했었습니다. Concurrent Collections로 수정했습니다!

Copy link
Contributor

@kang-hyungu kang-hyungu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드가 훨씬 나아졌네요. pr 머지할테니 다음 단계 진행하시죠

@kang-hyungu kang-hyungu merged commit a7cde5e into woowacourse:unifolio0 Sep 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants