kimyu0218
  • [node] http 모듈 커스텀하기 2편
    2023년 12월 24일 01시 47분 22초에 업로드 된 글입니다.
    작성자: @kimyu0218
    아직 부족한 게 많은 패키지에요. 핵심만 구현했으니 귀엽게(?) 봐주세요.

     

    http 모듈에는 어떤 게 들어있을까?

    http 모듈을 위한 코드를 작성하기에 앞서 http 모듈에 들어있는 객체들을 살펴보자.

    http 모듈은 HTTP 프로토콜을 다루기 위한 핵심 모듈이다.

     
    nodejs - http documentation
    (사실 캡처해서 가져오려고 했으나 너무 많아서 포기했다. 위 사이트 들어가서 확인해주세요)
     
    내가 구현할 객체는 일부 상수와 HTTP 메시지 부분이다. 메시지 객체를 구현하기 위해서는 메서드와 상태코드 정보를 넘겨줘야 하기 때문에 `METHODS`, `STATUS_CODES`를 구현하기로 결정했다.

    • http.METHODS
    • http.STATUS_CODES
    • http.IncomingMessage
    • http.OutgoingMessage

    http를 위한 상수 정의하기

    메시지를 구현하기 전에 메시지에 필요한 상수부터 정의해보도록 하자.
     

    http.METHODS

    `http.METHODS`는 HTTP 요청 메서드에 대한 상수를 담고 있는 배열이다. http 모듈에서는 `string[]` 자료형을 이용하고 있으나 좀 더 상수처럼 관리하기 위해 다음과 같이 작성해줬다.

    export const METHODS: Record<HttpMethods, HttpMethods> = {
      GET: 'GET',
      HEAD: 'HEAD',
      PUT: 'PUT',
      POST: 'POST',
      PATCH: 'PATCH',
      DELETE: 'DELETE',
      TRACE: 'TRACE',
      OPTIONS: 'OPTIONS',
    };

     

    http.STATUS_CODES

    `http.STATUS_CODES`는 HTTP 응답에 사용되는 상태코드의 집합이다. 상태코드가 너무 많은 관계로 자주 사용되는 상태코드에 대해서만 선언하기로 결정했다.

    export const STATUS_CODES: Record<number, string> = {
      100: 'Continue',
      101: 'Switching Protocols',
      200: 'OK',
      201: 'Created',
      204: 'No Content',
      301: 'Moved Permanently',
      302: 'Found',
      304: 'Not Modified',
      400: 'Bad Request',
      401: 'Unauthorized',
      402: 'Payment Required',
      403: 'Forbidden',
      404: 'Not Found',
      405: 'Method Not Allowed',
      409: 'Conflict',
      429: 'Too Many Requests',
      500: 'Internal Error',
      501: 'Not Implemented',
      502: 'Bad Gateway',
      503: 'Service Unavailable',
    };

    http 메시지 정의하기

    1편에서 살펴봤듯이 http request와 response는 start-line에 들어있는 내용만 다를 뿐 뼈대는 동일하다. 즉, start-line, header, message-body로 구성되어 있다.
     

    HttpMessage 클래스

    request와 response를 작성하기 전에 뼈대를 작성해보자. 뼈대 클래스를 상속하여 request, response를 구현할 것이다.

    export default class HttpMessage {
      private startLine?: string;
      private version?: string;
      private header: Map<string, string> = new Map();
      private messageBody: any;
    
      getStartLine(): string {
        return this.startLine ?? null;
      }
    
      getVersion(): string {
        return this.version ?? null;
      }
    
      getHeader(key: string): string {
        return this.header.get(key) ?? null;
      }
    
      getHeaders(): Map<string, string> {
        return this.header;
      }
    
      getMessageBody(): any {
        return this.messageBody;
      }
    
      setStartLine(startLine: string): this {
        this.startLine = startLine;
        return this;
      }
    
      setVersion(version: string): this {
        this.version = version;
        return this;
      }
    
      setHeader(key: string, value: string): this {
        this.header.set(key, value);
        return this;
      }
    
      setMessageBody(body: any): this {
        this.messageBody = body;
        return this;
      }
    }

    공통 부분인 start-line, header, message-body를 프로퍼티로 선언하고 getter와 setter를 작성해줬다. (`return this`로 메서드 체이닝을 구현하기 위해 `get`, `set` 키워드를 사용하지 않았다.)
     

    HttpResponse 클래스

    이제 앞에서 작성한 `HttpMessage`를 상속하여 `HttpRequest`를 정의할 것이다. `HttpResponse`는 서버가 클라이언트에게 전달하는 메시지이므로 상태코드를 수정하고 메시지를 전달할 수 있어야 한다.

    export class HttpResponse extends HttpMessage {
      private readonly socket: Socket;
      private statusCode: number = 200;
      private statusMessage: string = STATUS_CODES[200];
    
      constructor(socket: Socket, version: string) {
        super();
        this.setVersion(version);
        this.socket = socket;
      }
    
      send(): void {
        this.socket.write(this.getMessage());
        this.socket.end();
      }
    
      throwError(statusCode: number, message?: string): HttpResponse {
        this.statusCode = statusCode;
        this.statusMessage = STATUS_CODES[statusCode];
        this.setMessageBody(message ? message : this.statusMessage);
        this.setHeader('Content-Type', `${CONTENT_TYPE.HTML}; charset=utf-8`);
        return this;
      }
    
      getMessage(): string {
        this.setStartLine(this.makeStatusLine());
        const contentType: string = this.getHeader('Content-Type');
    
        const messageBody: any =
          contentType === CONTENT_TYPE.JSON
            ? JSON.stringify(this.getMessageBody())
            : this.getMessageBody();
    
        return new StringBuilder(this.getStartLine()) // status-line
          .add(CRLF)
          .add(this.makeHeader()) // header
          .add(CRLF)
          .add(messageBody) // message-body
          .build();
      }
    
      setStatusCode(statusCode: number): HttpResponse {
        this.statusCode = statusCode;
        this.statusMessage = STATUS_CODES[statusCode];
        return this;
      }
    
      setStatusMessage(message: string): HttpMessage {
        this.statusMessage = message;
        return this;
      }
    
      private makeStatusLine(): string {
        return new StringBuilder(this.getVersion()) // HTTP-Version
          .add(SP)
          .add(this.statusCode.toString()) // Status-Code
          .add(SP)
          .add(this.statusMessage) // Reason-Phrase
          .build();
      }
    
      private makeHeader(): string {
        const header: Map<string, string> = this.getHeaders();
        return Object.entries(header).reduce(
          (acc: string, [key, value]: string[]) => acc + `${key}:${value}${CRLF}`,
          '',
        );
      }
    }

    `HttpResponse` 객체를 생성할 때 생성자의 파라미터로 소켓을 넘겨준다. 이를 통해 send 메서드 호출 시 클라이언트에게 응답을 전달할 수 있다. (socket.write(), socket.end()에 관한 내용은 net 모듈을 살펴보시는 걸 추천합니다.)

    메시지를 전달할 때, HTTP 프로토콜의 양식을 지켜야하므로 1편을 참고하여 `getMessage()`, `makeStatusLine()`을 작성한다.


    아직 `HttpRequest`가 나오지 않았지만 이번 포스트는 여기서 마무리하겠다. 요청은 클라이언트가 보내는 메시지이기 때문에 파싱이 필요한데, 파서 코드가 꽤 길어 별도의 글로 작성하는 게 좋다고 판단했다.
     
    모든 코드를 포스트에 담을 수 없다는 점 양해부탁드립니다. 제가 작성한 코드는 깃허브에서 확인하실 수 있습니다.
     
    참고자료

    댓글