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


    반응형

    댓글 9

    • ㅇㅇ 2020.03.30 20:01

      우선 멀티 스레드 기반의 HTTP 요청/응답 모델을 보자. (Spring + JSP) 이 부분 쪽에서
      "클라이언트는 웹 서버에서 요청을 보낸다." 라는 말 오타아닌거죠? "저는 클라이언트는 웹 서버에게 요청을 보낸다."인걸로 알고있어서요...

    • jaehyeonkim 2020.04.03 18:42

      좋은 글 감사히 잘 읽었습니다. 궁금한 점이 하나 있는데, 아쉬운 점이 스프링의 단점, 노드의 장점만 이야기 되어있는것 같아서요 혹시 반대로 스프링의 강점 노드의 단점도 있을까요?

      • Favicon of https://mygumi.tistory.com 마이구미 mygumi 2020.04.07 22:51 신고

        그 내용을 수정했다고 생각했는데... 아니네요.
        맞습니다. Node.js 를 기준으로 작성되어 충분히 그렇게 느껴질 수 있습니다.
        JAVA의 장점은 충분히 다른 글들을 통해 이해하실 수 있을거라 생각합니다.
        전 개인적으로는 JAVA의 장점이 더 많다고 생각합니다.

    • Favicon of https://tosuccess.tistory.com 냠냠:) 2020.07.14 17:28 신고

      좋은 글 정말 감사합니다.
      궁금한 점이 있는데 Nodejs 설명하시는 부분에서 단순한 계산과 Non-blocking인 요청에 대해 스레드가 필요하지 않는다라고 하셨는데, 스레드는 어떤 요청이든 적어도 하나가 있어야 cpu가 처리할 수 있는 것 아닌가요? 제가 스레드에 대한 지식이 부족해서ㅠㅜ 알려주실 수 있나용

    • 지나가던dog발 2020.07.21 10:36

      '블라킹IO 요청이 들어오면 노드의 경우에 싱글스레드이기 때문에 하나 이상의 블라킹IO 작업을 할 수 없다' 가 맞나요? 반대로 '자바는 스레드 및 자원만 충분하다면 블라킹IO 작업을 동시 수행 할 수 있다' 로 도 해석이 되는거 같은데 제대로 이해한게 맞을까요? 또 노드의 경우에 블라킹IO 요청을 스레드에 할당 한다고 돼있는데 이벤트드리븐 방식이라면 할당 후 처리는 스레드에서 안하는게 아닌가용? 아니라면 블라킹이 들어온 순간부터 논블라킹 이벤트 역시 큐에서 대기하는게 아닌지... 틀린 부분이 있다면 지적 부탁드릴게요 ㅎㅎ

    • ㅁㅁ 2020.10.30 12:19

      요약해보자면 자바는 모든 요청에 대해 스레드를 할당하기 때문에, 요청의 개수가 사용 가능한 스레드의 개수를 넘어서게 되면 대기열에서 이용가능한 스레드가 할당될때까지 기다려야 하는데, Node.js는 이벤트 루프가 쉬운 요청(I/O사용하지 않는)들은 혼자 싱글스레드로 처리하고, I/O같은 작업만 따로 스레드를 할당해서 처리하기 때문에 싱글스레드 기반이라고 불리고, 가용할 수 있는 스레드의 개수가 늘어난다는 얘기로 이해하면 되겠네요.

    • 괴발 2020.11.17 14:24

      잘 읽었습니다. 정리가 잘 되어있네요

Designed by Tistory.