• Node.js vs Java 구조적 차이 :: 마이구미
    Nodejs 2017. 4. 30. 23:10
    반응형

    Node.js Architecture - Single Threaded Event Loop 링크를 참고하여 작성된 글이다.


    이번 글은 Node.js의 구조를 다뤄본다.

    단순히 Node.js에 대한 글이 아니기에, 도움이 될만한 글이라 생각하기에 읽고 판단하길 바란다.

    * Node.js 를 기준으로 작성되었기에, 다소 편중된 느낌을 받을 수 있다.


    기본적으로 많은 웹 어플리케이션은 멀티 스레드 기반의 구조를 따른다.

    간단하게 예를 들자면, Java에서 동시 요청을 처리 하기 위해 멀티 스레드를 이용한다고 생각하면 된다.


    하지만 Node.js를 알고 있다면 싱글 스레드를 기반으로 한다고 들어봤을 것이다. 

    싱글 스레드라면 동시 요청에 대해 비효율적이지 않을까? 의문이 들 수 있다.

    (완전히 싱글 스레드로 동작 되는건 아니다. 천천히 읽어보자)


    Node.js는 Java와 함께 거론이 많이 되었고, 되고 있다.

    둘 사이의 차이점에 대한 글이라고 봐도 무방하다.

    정확히는 멀티 스레드(Multithreaded) 구조 싱글 스레드 이벤트 루프(Single Threaded Event Loop) 구조의 차이점이 이번 글의 주제가 된다.


    HTTP 요청/응답에 대한 내부를 알아볼 것이다.

    이해를 돕기 위해 구조로써, Spring + JSP 와 Node.js를 비교한다.


    우선 멀티 스레드 기반의 HTTP 요청/응답 모델을 보자. (Spring + JSP)

    1. 클라이언트는 웹 서버에게 요청을 보낸다.
    2. 웹 서버는 내부적으로 제한된 스레드 풀을 관리하여 클라이언트의 요청에 서비스를 제공한다.
    3. 웹 서버는 무한 루프에 있으며, 클라이언트의 요청을 기다리는 중이다.
    4. 웹 서버는 요청들을 받아들인다.
      1. 웹 서버는 하나의 요청을 가져온다.
      2. 스레드 풀로부터의 하나의 스레드에 가져온다.
      3. 이 스레드에 요청이 할당된다.
      4. 이 스레드가 요청을 읽고 처리한다. (요청 처리, Blocking I/O가 있다면 수행, 응답 준비)
      5. 이 스레드는 준비된 응답을 다시 웹 서버에게 보낸다.
      6. 웹 서버는 이 응답을 클라이언트에게 보낸다.

    서버는 무한 루프에서 기다리고, 모든 클라이언트 요청을 위의 단계로 수행한다.

    여기서는 하나의 요청이 하나의 스레드를 생성하는 것을 의미한다.


    만약 많은 요청들이 Blocking I/O를 요구한다면, 거의 모든 스레드는 응답 준비중이다.

    그 결과, 나머지 클라이언트 요청은 더 오랜 시간을 기다리게 된다. (아래에서 자세하게 다룬다.)



    멀티 스레드


    위 다이어그램을 통해 다시 한번 자세히 알아보자.

    1. Client-1, Client-2, ...Client-n의 요청이 동시에 웹 서버로 보내진다.
    2. 웹 서버는 내부적으로 제한된 스레드 풀을 관리하는데 스레드 풀의 스레드 갯수를 m이라고 가정하자.
    3. 웹 서버는 요청들을 받아들인다.
      • 웹 서버는 요청(Client-1 Request-1)을 스레드 풀의 스레드(T-1)에 올린다. 그리고 이 요청을 스레드(T-1)에 할당한다.
        1. 스레드(T-1)는 요청을 읽고 처리한다.
        2. 요청(Client-1 Request-1)은 Blocking I/O 작업 요구하지 않기에, Blocking I/O 작업을 하지 않는다.
        3. 스레드(T-1)는 필요한 단계를 거쳐 응답을 준비한 후, 다시 서버에게 보낸다.
        4. 서버는 Respone-1을 Client-1에게 보낸다.
      • 웹 서버는 요청(Client-2 Request-2)을 스레드 풀의 스레드(T-2)에 올린다. 그리고 이 요청을 스레드(T-2)에 할당한다.
        1. 스레드(T-2)는 요청을 읽고 처리한다.
        2. 요청(Client-2 Request-2)은 Blocking I/O 작업 요구하지 않기에, Blocking I/O 작업을 하지 않는다.
        3. 스레드(T-2)는 필요한 단계를 거쳐 응답을 준비한 후, 다시 서버에게 보낸다.
        4. 서버는 Respone-2을 Client-2에게 보낸다.
      • 웹 서버는 요청(Client-n Request-n)을 스레드 풀의 스레드(T-n)에 올린다. 그리고 이 요청을 스레드(T-n)에 할당한다.
        1. 스레드(T-1)는 요청을 읽고 처리한다.
        2. 요청(Client-n Request-n)은 Blocking I/O 작업을 요구한다.
        3. 스레드(T-n)는 외부 시스템과의 상호작용을 위해 더 많은 시간을  필요로하고, 필요한 단계를 거쳐 응답을 준비한 후, 다시 서버에게 보낸다.
        4. 서버는 Respone-n을 Client-n에게 보낸다.


    만약 n이 m보다 크다면, 서버는 사용할 수 있는 스레드까지 클라이언트 요청에 스레드를 할당한다.

    모든 m개가 사용된 후, 나머지 클라이언트 요청은 사용중인 스레드가 다음 요청을 받을 수 있게 될 때까지 대기열에서 기다리게 된다.

    즉, 스레드가 스레드 풀에서 사용 가능해지고, 다음 작업을 수행할 수 있게 되면, 서버는 스레드에 가져온 후 다음 요청을 할당하게 된다.

    * 각 스레드는  메모리 같은 많은 자원을 사용하기 때문에 사용 중인 스레드는 대기 상태에 가기 전에 모든 자원을 해제해야한다.


    그 결과, 위와 같은 멀티 스레드 구조의 단점은 아래와 같다.

    • 점점 더 많은 동시에 발생하는 클라이언트의 요청을 처리하는 것이 어렵다.
    • 동시에 발생하는 클라이언트의 요청이 증가할 때, 많은 스레드를 이용해야하기 때문에 많은 메모리를 사용하게 된다.
    • 때때로, 클라이언트의 요청은 사용 가능한 스레드가 요청을 처리할 때까지 기다려야한다.
    • Blocking I/O 작업으로 인해 시간이 낭비된다.


    이번에는 Node.js의 싱글 스레드 기반의 구조를 살펴보자.

    1. 클라이언트는 웹 서버에게 요청을 보낸다.
    2. Node.js의 웹 서버는 내부적으로 제한된 스레드 풀을 관리하여 클라이언트의 요청에 서비스를 제공한다.
    3. Node.js의 웹 서버는 클라이언트의 요청을 받아들이고, 큐 안에 배치한다. 이 큐를 이벤트 큐(Event Queue)라 부른다.
    4. Node.js의 웹 서버는 내부적으로 이벤트 루프(Event Loop)라 불리는 컴포넌트를 가진다. 이름의 유래는 요청을 받아들이고 처리하는 것이 무한 루프를 사용하기 때문이다. (아래에 있는 Java 슈더 코드를 보면 이해하기 쉬울 것이다)
    5. 이벤트 루프는 싱글 스레드를 사용한다. 이것이 Node.js의 핵심이다.
    6. 이벤트 루프는 이벤트 큐에 배치된 클라이언트의 요청을 확인한다. 만약 없다면, 요청을 무한히 기다리게 된다.
    7. 만약 있다면, 이벤트 큐에 배치된 클라이언트의 요청을 가져온다.
      1. 클라이언트 요청을 처리한다.
      2. 만약 클라이언트 요청이 Blocking I/O 작업이 없다면, 모든 처리는 응답을 준비하고 클라이언트에 보낸다.
      3. 만약 클라이언트 요청이 Blocking I/O 작업이 있다면,
        1. 스레드 풀에서의 사용 가능한 스레드를 확인한다.
        2. 하나의 스레드를 가져와 요청을 할당한다.
        3. 이 스레드는 요청을 처리하고, 응답을 준비하고, 이벤트 루프에 보낸다.
        4. 이벤트 루프는 클라이언트에게 응답을 보낸다.


    싱글 스레드 이벤트 루프


    위 다이어그램을 통해 다시 한번 자세히 알아보자.

    1. Client-1, Client-2, ...Client-n의 요청이 동시에 웹 서버로 보내진다.
    2. 웹 서버는 내부적으로 제한된 스레드 풀을 관리하는데 스레드 풀의 스레드 갯수를 m이라고 가정하자.
    3. 웹 서버는 요청들을 받아들이고, 이벤트 큐(Event Queue)에 배치한다.
    4. 이벤트 루프는 요청들을 단계별로 가져온다.
      • 이벤트 루프는 요청(Client-1 Request-1)을 가져온다.
        1. 요청(Client-1 Request-1)이 Blocking I/O 또는 많은 시간이 걸리는 복잡한 작업을 요구하는지 확인한다.
        2. 요청(Client-1 Request-1)이 단순한 계산과 Non-Blocking I/O 작업이기에, 별도의 스레드가 필요하지 않는다.
        3. 이벤트 루프는 요청(Client-1 Request-1)을 처리하고 Response-1을 준비한다.
        4. 이벤트 루프는 Response-1을 Client-1에게 보낸다.
      • 이벤트 루프는 요청(Client-2 Request-2)을 가져온다.
        1. 요청(Client-2 Request-2)이 Blocking I/O 또는 많은 시간이 걸리는 복잡한 작업을 요구하는지 확인한다.요청(Client-2 Request-2)이 단순한 계산과 Non-Blocking I/O 작업이기에, 별도의 스레드가 필요하지 않는다.
        2. 요청(Client-2 Request-2)이 단순한 계산과 Non-Blocking I/O 작업이기에, 별도의 스레드가 필요하지 않는다.
        3. 이벤트 루프는 요청(Client-2 Request-2)을 처리하고 Response-2을 준비한다.
        4. 이벤트 루프는 Response-2을 Client-2에게 보낸다.
      • 이벤트 루프는 요청(Client-n Request-n)을 가져온다.
        1. 요청(Client-n Request-n)이 Blocking I/O 또는 많은 시간이 걸리는 복잡한 작업을 요구하는지 확인한다.
        2. 요청(Client-n Request-n)이 복잡 계산 또는 Blocking I/O 작업이기에, 이벤트 루프는 이 요청을 처리하지 않는다.
        3. 이벤트 루프는 스레드 풀의 스레드(T-1)를 선택하고, 요청(Client-n Request-n)을 스레드(T-1)에 할당한다.
        4. 스레드(T-1)은 요청을 처리하고, 필요한 Blocking I/O 작업 또는 복잡한 작업을 수행한 후, 마지막으로 Response-n을 준비한다.
        5. 스레드(T-1)은 Response-n을 이벤트 루프에게 보낸다.
        6. 이벤트 루프는 Response-n을 Client-n에게 보낸다.


    그 결과, 싱글 스레드 이벤트 루프 방식의 장점은 아래와 같다.

    • 점점 더 많은 동시에 발생하는 클라이언트의 요청을 처리하는 것이 쉽다.
    • 동시에 발생하는 클라이언트의 요청이 증가할 때, 이벤트 루프를 이용하기 때문에 많은 스레드를 이용하지 않는다.
    • 멀티 스레드 방식보다 스레드를 덜 이용하기 때문에 메모리 또는 자원 소모가 작다.


    이벤트 루프를 코드로 나타내면 아래와 같다.


    public class EventLoop { while(true){ if(Event Queue receives a JavaScript Function Call){ ClientRequest request = EventQueue.getClientRequest(); If(request requires BlokingIO or takes more computation time) Assign request to Thread T1 Else Process and Prepare response } } }


    반응형

    댓글

Designed by Tistory.