HttpRequest 를 단일 책임 원칙으로서 분리해내면서 배운점.

roach·2021년 4월 15일
0

Single Responsibility Principle

간략하게는 클래스는 하나의 책임을 져야 한다 라는 뜻이다. 위키 백과의 말을 따르면, 객체 지향 프로그래밍에서 단일 책임 원칙(single responsibility principle)이란 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함을 일컫는다.

내 클래스 에서는..?🧐

  • 현재 HttpRequest 라는 클래스가 애매하다. Class 자체의 Naming 으로는 HttpRequest 정보만 담아야 단일 책임 원칙을 수행한다고 할 수 있겠다. 하지만, 현재 HttpRequest 는 Client 에서 온 Request Data 를 HttpRequest 내의 속성을 Parsing 해주는 역할도 하고 있다.

기존의 코드를 가져와보자 😁

private InputStream inputStream;
private String url;
private RequestMethod method;
private Header header = new Header();
private Cookie cookie;
private Map<String, String> parameters;
private Map<String, String> requestBody;
  • HttpRequest 의 필드들이다. 기본적으로 저러한 필드들을 들고 있어도 되나. InputStream 의 경우에는 의심이 될 수 있는 부분이다. 보통 InputStream 의 바이트 들을 통해 Client 의 요청을 받아오므로, HttpRequestParser 와 같은 클래스로 분리해 내는 것이 좋다. 아래는 원래 HttpRequest 내에서 Parsing 해내던 코드이다.
public void run() throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));

        String line = br.readLine();

        if (line == null) {
            return;
        }

        final String[] fistLine = line.split(" ");

        this.method = RequestMethod.valueOf(fistLine[0]);
        this.url = fistLine[1];

        header.saveHeaders(br, line);

        int queryStringStartIndex = url.indexOf("?");

        if(queryStringStartIndex != -1) {
            parameters = HttpRequestUtils.parseQueryString(url.substring(queryStringStartIndex + 1, url.length()));
            url = url.substring(0, queryStringStartIndex);
        }

        if(!method.equals(RequestMethod.GET)) {
            requestBody = HttpRequestUtils.parseQueryString(IOUtils.readData(br, Integer.parseInt(header.getAttribute("Content-Length"))));
        }

        if (header.isExistAttribute(COOKIE)) {
            cookie.saveCookie(header.getAttribute(COOKIE));
        }

    }
  • 이 부분은 HttpRequest 에 있을 필요가 없다. 우리는 이 클래스를 HttpRequestParser 라는 클래스를 통해 분리해 보자. 해당 클래스에서 정보를 분석 해 낸뒤, HttpRequest 를 만들어서 전달하는 방식으로 진행해보자!

HttpRequestParser

  • HttpRequest 를 그전에 하나하나씩 넣을 수 있게 Builder 로 만들어볼까?
  • 필드가 많으므로 깔끔하게 BuilderPattern 을 적용할 수 있게하자.
package core;

import java.util.Map;

public class HttpRequestBuilder {

    private String url;
    private RequestMethod method;
    private Header header = new Header();
    private Cookie cookie;
    private Map<String, String> parameters;
    private Map<String, String> requestBody;

    public HttpRequestBuilder setUrl(String url) {
        this.url = url;
        return this;
    }

    public HttpRequestBuilder setMethod(RequestMethod method) {
        this.method = method;
        return this;
    }

    public HttpRequestBuilder setHeader(Header header) {
        this.header = header;
        return this;
    }

    public HttpRequestBuilder addHeader(String key, String value) {
        header.addHeader(key, value);
        return this;
    }

    public HttpRequestBuilder setCookie(Cookie cookie) {
        this.cookie = cookie;
        return this;
    }

    public HttpRequestBuilder addCookie(String key, String value) {
        cookie.addCookie(key, value);
        return this;
    }

    public HttpRequestBuilder setParameters(Map<String, String> parameters) {
        this.parameters = parameters;
        return this;
    }

    public HttpRequestBuilder setRequestBody(Map<String,String> requestBody) {
        this.requestBody = requestBody;
        return this;
    }

    public HttpRequest build() {
        return new HttpRequest(url, method, header, cookie, parameters, requestBody);
    }

}
  • 그럼 이제 Builder 를 이용할 Parser 클래스를 만들어보자!

HttpRequestParser

  • Parser 는 Client 의 요청을 받아들여서, 분석하고 HttpRequest 를 만들어주는 클래스이다.
public class HttpRequestParser {

    private final InputStream inputStream;
    private String url;
    private Header header = new Header();
    private RequestMethod method;
    private Cookie cookie;
    private Map<String, String> parameters;
    private Map<String, String> requestBody;

    public HttpRequestParser(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    public HttpRequest parser() throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));

        HttpRequestBuilder httpRequestBuilder = new HttpRequestBuilder();

        String line = br.readLine();

        final String[] fistLine = line.split(" ");

        url = fistLine[1];

        method = RequestMethod.valueOf(fistLine[0]);

        header.saveHeaders(br, line);

        int queryStringStartIndex = url.indexOf("?");

        if(queryStringStartIndex != -1) {
            parameters = HttpRequestUtils.parseQueryString(url.substring(queryStringStartIndex + 1, url.length()));
            url = url.substring(0, queryStringStartIndex);
        }

        if(!method.equals(RequestMethod.GET)) {
            requestBody = HttpRequestUtils.parseQueryString(IOUtils.readData(br, Integer.parseInt(header.getAttribute("Content-Length"))));
        }

        if (header.isExistAttribute(COOKIE)) {
            cookie.saveCookie(header.getAttribute(COOKIE));
        }

        return httpRequestBuilder
                .setUrl(url)
                .setMethod(method)
                .setHeader(header)
                .setCookie(cookie)
                .setParameters(parameters)
                .setRequestBody(requestBody)
                .build();
    }

}
  • 이로서 우리는 HttpRequest 에서 Client 의 요청을 분석하는 책임을 덜어낼 수 있게되었다. HttpRequest 는 말그래도 Request 의 정보만 가지게 되었다. 정상적으로 작동하게 된것이다.

리팩토링 후 HttpRequest

package core.request;

import core.Cookie;
import core.Header;
import core.RequestMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * HttpRequest 은 요청이 들어올 시 HttpRequest 정보를 저장하는 클래스 입니다.
 * url, RequestMethod Header Cookie 등등.. 여러가지 정보등을 보관합니다.
 * 현재는 HttpRequest 에서 InputStream 등의 작업을 수행하지만 Refactoring 후 다른곳에서 주입시켜줄 예정입니다.
 */
public class HttpRequest {

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

    private final String url;
    private final RequestMethod method;
    private final Header header;
    private final Cookie cookie;
    private Map<String, String> parameters;
    private Map<String, String> requestBody;

    public HttpRequest(String url, RequestMethod method, Header header, Cookie cookie, Map<String, String> parameters, Map<String, String> requestBody) {
        this.url = url;
        this.method = method;
        this.header = header;
        this.cookie = cookie;
        this.parameters = parameters;
        this.requestBody = requestBody;
    }

    public static Logger getLog() {
        return log;
    }

    public String getUrl() {
        return url;
    }

    public RequestMethod getMethod() {
        return method;
    }

    public Map<String, String> getParameters() {
        return parameters;
    }

    public Map<String, String> getRequestBody() {
        return requestBody;
    }

    @Override
    public String toString() {
        return "HttpRequest{" +
                ", url='" + url + '\'' +
                ", method='" + method + '\'' +
                ", parameters=" + parameters +
                ", requestBody=" + requestBody +
                '}';
    }

}

소감

  • 하나의 응집되어 있던 클래스들을 분리해내고, 이를 통해 공부해나가는 방식은 꽤 재미있다. 단순히 누군가 시켜서 아닌 공부를 하다가, 이런 부분들을 도입해보면 어떨지 생각해보고, 바꿔보는것은 공부에 큰 도움이 된다.
  • 아직 다른 코드들에도 역할을 분리해내야 할 부분들이 많다. 이번 달은 리팩토링에 신경을 쓰면서 기술적으로 정리하고 공부하는 시간을 가져야겠다.
profile
모든 기술에는 고민을

0개의 댓글