[자바 웹 프로그래밍 Next-Step] 2주차 CH05: 웹 서버 리팩토링, 서블릿 컨테이너와 서블릿의 관계

이호석·2023년 2월 11일
0
post-thumbnail

자바 웹 프로그래밍 Next-Step - 박재성 저자 책으로 스터디를 하며 진행했던 내용들을 기록하고 있습니다.

2주차에 진행했던 Chapter 05의 목표는 다음과 같습니다.

  • Chapter 05: Ch04에서 진행한 웹 서버 리팩토링 및 서블릿 컨테이너와 서블릿의 관계 이해하기

모든 코드들은 다음 저장소에서 확인할 수 있습니다.
https://github.com/Java-web-programming-Next-Step/next-step-web-programming/tree/HiiWee/5
프로젝트명: web-application-server-gradle

📌 2주차 5장


✅ 5.1.2.1 요청 데이터를 처리하는 로직을 별도의 클래스로 분리한다. (HttpRequest) (이슈❗️)

이슈❗️ BufferedReader를 close하게 되면 Socket이 같이 닫히게 된다.

BufferedReader를 close하게 되면 Socket이 닫히게 된다.

Stack Overflow: Does closing the BufferedReader/PrintWriter close the socket connection?

HttpRequest의 변경

기존의 HttpRequest 구조는 한가지 아쉬운점이 존재한다.

  1. GET과 POST의 쿼리스트링이 서로 다른 객체에 데이터가 저장됨

    GET은 url을 통해 쿼리스트링이 전달되고, POST는 Request Body를 통해 전달된다.
    따라서 구현할 당시에는 RequestLine이라는 클래스에 GET에서 들어온 쿼리스트링을 POST는 HttpRequest에 form 데이터용 쿼리스트링을 두었다. 이러한 부분을 합쳐보자

사실 위의 코드를 합치지 않고 RequestLine은 분리하는 선택을 했습니다. 단, GET 혹은 POST로 전달되는 서로 다른 쿼리스트링을 HttpRequest 객체에 일괄적으로 관리하도록 변경했습니다.

최종 코드

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

    private final RequestLine requestLine;
    private final HttpHeaders httpHeaders;
    private final RequestParameters requestParameters = new RequestParameters();

    public HttpRequest(final InputStream in) throws IOException {
        BufferedReader httpRequestReader = new BufferedReader(new InputStreamReader(in));
        requestLine = RequestLine.from(httpRequestReader.readLine());
        httpHeaders = new HttpHeaders(parseHttpRequestHeader(httpRequestReader));
        requestParameters.addParams(requestLine.getParams());
        addFormParams(httpRequestReader);
    }

    private void addFormParams(final BufferedReader httpRequestReader) throws IOException {
        if (requestLine.containMethod(HttpMethod.POST)) {
            requestParameters.addParams(parseQueryStringByForm(httpRequestReader));
        }
    }

    public boolean containMethod(final HttpMethod method) {
        return requestLine.containMethod(method);
    }

    public String getHeader(final String headerName) {
        return httpHeaders.getHeader(headerName);
    }

    public String getPath() {
        return requestLine.getUri();
    }

    public String getParameter(final String parameterName) {
        return requestParameters.getParameter(parameterName);
    }

    public HttpCookies getCookies() {
        return new HttpCookies(HttpRequestUtils.parseCookies(httpHeaders.getHeader("Cookie")));
    }

    public HttpSession getSession() {
        return HttpSessions.getSession(getCookies().getCookie(HttpSessions.SESSION_ID_NAME));
    }

    private Map<String, String> parseQueryStringByForm(final BufferedReader httpRequestReader) throws IOException {
        String formData = IOUtils.readData(httpRequestReader, httpHeaders.getContentLength());
        log.debug("POST form Data={}", formData);
        return HttpRequestUtils.parseQueryString(formData);
    }

    private Map<String, String> parseHttpRequestHeader(final BufferedReader httpRequestReader) throws IOException {
        String header = httpRequestReader.readLine();
        List<Pair> headerPairs = new ArrayList<>();
        while (!header.equals("")) {
            headerPairs.add(HttpRequestUtils.parseHeader(header));
            header = httpRequestReader.readLine();
        }
        return headerPairs.stream()
                .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
    }
}

✅ 5.1.2.2 응답 데이터를 처리하는 로직을 별도의 클래스로 분리한다(HttpResponse)

응답을 생성하는 부분에서 가장 크게 달라진건

// 변경전 응답 흐름
FrontControllerControllerHttpResponse에 응답내용 작성 → FrontControllerHttpResponse에게 응답 요청

// 변경후 응답 흐름
FrontControllerControllerHttpResponse → 응답 완료

위와 같이 조금 비효율적인 흐름을 다음과 같이 HttpResponse에서 즉시 끝나도록 변경했다.
(그 외에 응답 헤더를 생성하는 부분이나 중복이 많았던 코드들을 리팩토링했다.)

실제로 리팩토링 이전과 이후의 코드는 다음과 같다.

리팩토링 이전 FrontContoller의 응답 흐름

리팩토링 이후 FrontController의 응답 흐름

응답해야 하는 파일이 css인지 js인지 html인지와 같이 같은 정적파일이라도 응답 헤더가 달라지는 부분들은 모두 HttpResponse에서 처리하도록 변경했고, forward라는 정적 파일 응답 메소드를 두며 FrontController는 단순히 컨트롤러 호출인지, 정적파일 호출인지만 고려하여 알맞은 메소드를 실행시켜주기만 하면 된다.


✅ 5.1.2.3 다형성을 활용해 클라이언트 요청 URL에 대한 분기 처리를 제거한다.

해당 부분은 기존 4장에서 구현할때도 공통적인 Controller 인터페이스를 두어 다형성을 구현하고자 했다.

책에서는 다형성을 이용하는 흐름은 동일하지만, 메소드에 따른 실행을 모두 컨트롤러에게 위임했다.

Controller 인터페이스의 service 메소드

Controller 인터페이스는 오직 service 메소드만 가지고 있으며 AbstractController는 해당 인터페이스를 구현한다.

AbstractController를 통한 다형성

service를 구현한 추상 컨트롤러는 Http Method에 따른 실행을 위해 doGet(), doPost() 메소드를 가지고 있다. service 메소드 내부에서는 FrontController로 부터 전달받은 HttpRequest 객체를 이용해 HttpMethod를 검사해 doGet or doPost를 실행한다.

위의 리팩토링 이후 FrontControllerrequestDispatch 메소드를 살펴보면 단순히 매핑된 컨트롤러의 service만 호출해도 각 컨트롤러의 상위 클래스인 AbstractController가 구현한 service에서 자동으로 Http Method를 검사하고 알맞은 메소드를 실행시킨다.

호출된 doGet, doPost를 실행한 컨트롤러는 AbstractController를 구현한 Controller에서 반드시 구현하고 있으므로 해당 메소드를 실행하게 된다.


✅ 5.2 웹 서버 리팩토링 구현 및 설명

테스트 코드 기반 개발의 장점

  1. 클래스에 버그가 있는지 빨리 찾아 구현할 수 있음

    현재 프로젝트의 경우 웹서버를 실행하고 수동으로 확인하는 과정을 테스트 코드를 이용하면 훨씬 편리하게 결과를 확인할 수 있음

  2. 디버깅하기 쉽다.

    클래스에 대한 단위 테스트를 하는 것은 결과적으로 디버깅을 쉽고 빠르게 하므로 개발 생산성을 높여줌

  3. 테스트 코드를 기반으로 리팩토링하기 쉽다.

    리팩토링의 어려움 중 하나는 리팩토링하게 되면 지금까지 했던 모든 테스트를 재 실행 해봐야 하는데 테스트 코드가 이미 존재한다면 리팩토링 완료 후 테스트를 실행하기만 하면 된다.


✅ 5.2.1 요청 데이터를 처리하는 로직을 별도의 클래스로 분리한다.

  • private 메소드의 복잡도가 높아 별도의 테스트가 필요한데 테스트하기 힘들다면 어딘가 리팩토링할 부분이 있겠다는 힌트를 얻을 수 있다.
  • 상수 값이 서로 연관되어 있는 경우 자바의 enum을 쓰기 적합한 곳이다.
  • 객체지향 설계에서 중요한 연습은 요구사항을 분석해 객체로 추상화 하는 부분, 눈으로 보이지 않는 비즈니스 로직의 요구사항을 추상화 하는 작업은 쉽지 않다.

✅ 5.2.2 응답 데이터를 처리하는 로직을 별도의 클래스로 분리한다.

HttpResponse를 구현해 응답에 대한 처리를 구현하는것이 목적

  1. forward: 정적인 파일(html, css, js)를 처리하는 목적
  2. redirect: Location 헤더의 위치로 302 Redirect를 처리
  3. response body: 사용자의 목록을 출력할때 응답 바디에 직접 전송

✅ 5.2.3 다형성을 활용해 클라이언트 요청 URL에 대한 분기 처리를 제거

메소드의 원형이 같다면 자바의 인터페이스로 추출 할 고민을 해보자


✅ 5.2.4 HTTP 웹 서버의 문제점

  • HTTP 요청과 응답 헤더, 본문 처리와 같은 데 시간을 투자함으로써 정작 중요한 로직을 구현하는데 투자할 시간이 상대적으로 적다.
  • 동적인 HTML을 지원하는 데 한계가 존재, 동적으로 HTML을 생성할 수 있지만 많은 코딩량을 필요로 한다.
  • 사용자가 입력한 데이터가 서버를 재시작하면 사라진다. 사용자가 입력한 데이터를 유지하고 싶다.

✅ 5.3.1 서블릿, JSP 개발 환경 세팅 및 Hello World 출력

임베디드 톰캣의 사용

임베디드 톰캣을 사용하면 톰캣의 설정을 lib 파일로 추가를 해주어야 하고, 해당 시작 코드도 직접 작성해야 한다.

위에서 web 디렉토리의 위치를 webapp/ 으로 설정했다.

Default Directory Structure

톰캣은 class파일을 인텔리제이의 out 폴더가 아닌 우리가 설정한 루트경로(webapp) 아래
WEB-INF/classses/ 아래의 경로로 설정해 주어야 한다. 이후 톰캣을 재시작하면 된다.

profile
꾸준함이 주는 변화를 믿습니다.

0개의 댓글