본문 바로가기

Spring/Spring

Controller 1개가 어떻게 수 많은 Request를 처리하는가? (spring mvc, tomcat thread, singleton bean)

반응형

Controller 1개가 어떻게 수 많은 Request를 처리하는가

배경 의식의 흐름

Controller는 한 개인가?

지금와서 생각하면 조금 바보같았지만, 저런 질문이 떠오른 이유는 아래와 같습니다.

스프링에서 웹 애플리케이션을 개발할 때, (대부분의 경우에) 스프링 빈(Bean)으로 객체를 생성합니다.

Bean의 스코프를 prototype으로 변경하면 HTTP Request 마다 해당 빈을 새롭게 생성해서 처리하도록 할 수 있지만, 그렇게 하는 경우를 거의 본 적이 없습니다.

그러면 내가 만든 Controller(Bean) 하나가 1만 개의 요청이든 10만 개의 요청이든 처리를 한다는 얘긴데, 이게 감당가능한가? 이런 생각이 갑자기 스쳐지나가면서 의문이 생겼습니다.

혹시 여러 컨트롤러가 만들어지는 것은 아닐까 의문을 품고 조금 찾아보니 우선은 Controller 객체(Bean)는 한 개가 생성되는 맞고 그 하나의 컨트롤러를 사용하는게 맞았습니다. (일단 OK)

tomcat worker(?) thread가 200개라던데?

그리고 WAS마다 다르겠지만 학습할 때 많이 쓰이는 톰캣(tomcat)... 이게 옛 기억을 더듬었을 때 default로 HTTP Request를 받아주는 스레드(Thread)가 200개라 들었던게 생각났습니다.

찾아보니까 쓰레드 풀에 생성될 수 있는 쓰레드 의 수가 MAX가 있는 것이고, 실질적으로는 최소로 유지될(idle 상태) 쓰레드 수(10개?)가 있고, 요청이 많아지면 그에 따라 큐에 쌓였다가 쓰레드를 만드는 등 일반적인 쓰레드풀의 동작을 하고 있었습니다.

그래도 결국에는 동시에 10만건이 오면 쓰레드풀 동작에 따라 200개의 쓰레드가 작업을 할 수도 있는 것이라고 생각했습니다.

그러면 어쨋든 10만 개의 Request가 있다면 Controller 1개가 전부다 처리하는거잖아?

네. 저런 위험한(?) 생각을 해버렸습니다.

Request별로 쓰레드가 따로 생성되고 각각의 Context를 갖는다는 것을 인지하고 있었지만, 그 쓰레드들이 Controller 객체를 공유하니까 이거 문젠데? 라는 생각을 말이죠.

어떻게 돌아가는지 보자

놓치고 있는게 있는지 살펴본다

위와 같은 의식의 흐름 끝에 도달한 결론인 "Controller 객체 한개가 10만 개의 Request를 처리하는 건 문제다." 라는 것을 갖고 뭔가 빠진게 있나 고민하기 시작했습니다.

왜냐하면 스프링은 그 10만 개의 Request를 하나의 Controller가 이슈 없이 잘 처리하고 있으니까 제가 이상한게 분명한 것 같았기 때문입니다.

나와 같은 의문을 갖는 사람들이 예전에도 있었다

위의 내용들을 간략하게 요약하면, 결국에는 내가 생성한 Controller 클래스에 대한 정보가 JVM 메모리 영역 중에 '메소드 영역'에 올라간다는데에 있다는 것입니다.

무슨 얘기냐면, Controller 객체를 생성하면 객체는 힙에 생성되지만 해당 클래스의 정보(메소드라면 메소드 처리 로직(명령들))는 메소드영역에 생성된다는 것입니다.

결론적으로 JVM 구조를 언급했던 지난 포스트를 참고했을 때, 힙영역이든 메소드영역이든 결국에 모든 쓰레드가 객체의 메서드(바이너리 코드 자체이자 메모리)를 공유할 수 있다는 것일 뿐 공유를 위해 블록이 되는 것은 아닌 것입니다.

이게 내 결론이다

바보같은 질문

"공유"라는 말이 단순하게 같이 쓸 수 있다! 이렇게 생각하고 끝냈어야하는데 바보같이 공유하면 동기화를 해야지 이러면서 lock을 걸고... lock이면 병목... 이런 생각을 했던게 문제였습니다.

그냥 말 그대로 공유였습니다. 내부적으로 상태를 갖는게 없으니 그냥 메소드 호출만 하기 때문에 동기화할 필요가 없고 처리 로직만 쓰이기 때문에 1만 개의 요청이든 10만 개의 요청이든 상관없다는 얘기인 것이지요.

(단, 매번 Controller 객체를 새로 생성하는 방식(scope=prototype)은 언제 쓰이는지 여전히 의문입니다.)

앞으로 해야할 일

  • 스프링 Bean을 생성할 때, 절대로 상태를 갖는 Bean을 생성하지 말아야 한다.
    • 스프링 Bean이 상태를 갖게 되었을 때는 그 상태를 공유하는 모든 쓰레드들로 부터 안전할 수 있게 동기화를 해줘야하고 동기화를 하는 순간 싱글톤으로써의 혜택이 날아간다고 봐야하기 때문입니다.

의식의 흐름

  • Controller(Bean) 하나가 어떻게 수 많은 쓰레드로부터 처리를 할 수 있는지 알게되었다.
  • 그런데 이제 생각이 확장되어 '톰캣 MaxThreadPoolSize는 200개인데 DBCP인 HikariCP는 왜 maximumPoolSize가 default 10개인가에 대한 고민이 시작되었다. (ThreadPool이랑 ConnectionPool이 다른 것 같긴 한데...)
    • 실질적인 쓰레드의 수가 아니라 DB 연결에 쓰이는 Connection 객체를 공유하는 것이라 개수가 다르다는 결론!
  • 톰캣 쓰레드는 왜 200개까지나 늘릴까?, 그 이상 튜닝도 하는거로 아는데...? 어차피 서버 코어 쓰레드 수 이상으로는 작업 못하지 않나? 하는 생각도 했다. (톰캣이 blocking 방식이라 여러 요청을 처리하기 위해 그렇다는 것 같은데...) -> (수정합니다. tomcat 8부터는 nio 즉, non-blocking 방식을 쓴다고 합니다. 댓글 참조! 그러고 보니 스프링 부트 애플리케이션 실행하면 아래와 같이 나왔던 것도 같다. nio 라고 써있네!)
...Starting ProtocolHandler ["http-nio-8080"] ...

그런데 상기의 고민들은 여기서의 주제와 약간 벗어나기 때문에 일단은 마무리를 짓는다.

추후에 저 고민에 대한 포스팅이 있기를 바라며...

출처 : https://m.blog.naver.com/tmondev/220731906490

 

반응형
  • Favicon of https://brocess.tistory.com BlogIcon brocess 2020.02.18 18:32 신고

    tomcat 8 버전부터 connector의 방식은 bio가 아니라 nio입니다.

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.02.19 16:10 신고

      오! 감사합니다. 수정하겠습니다.
      nio를 쓰는데도 여전히 MaxThreadPoolSize가 200개네요...ㅠㅠ
      조금 더 공부를 해봐야겠습니다 알려주셔서 감사합니다.

  • Favicon of https://ssaemo.tistory.com BlogIcon Hojong 2020.05.01 03:05 신고

    안녕하세요 좋은 글 잘 읽었습니다
    저도 비슷한 내용을 공부 중이어서, 몇 가지 코멘트 남겨봅니다
    - tomcat max thread pool size가 connection max pool size보다 작은 이유는, 모든 요청이 DB를 사용하는 것은 아니기 때문이라는 내용을 본 적이 있습니다 (서버에 따라 다르겠지만)
    - tomcat thread pool size는 각자 성능 측정을 하고 필요에 따라 조절해야하므로 (가이드도 그렇게 되어있고) default value가 200인 것은 그저 기본값일 뿐이라고 생각하면 될 것 같아요
    좋은 글 감사합니다

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.05.03 11:33 신고

      도움 주셔서 감사합니다!
      모든 요청이 DB에 요청하진 않겠지만 대부분의 요청이 DB를 쓰지 않나 하는 의문도 있습니다. (현실적으로요...)
      제가 고민해볼 문제에 도움을 주셔서 다시 한 번 감사의 말씀을 드립니다!

    • Favicon of https://ssaemo.tistory.com BlogIcon Hojong 2020.05.03 11:56 신고

      저도 같은 의문이 들었었는데, 지금 생각해보니 요청을 받았을 때 DB가 아닌 다른 서버에 요청을 보내는 케이스는 자주 있을 것 같네요. 사내 또는 사외 API 서버로 나뉠 수 있겠습니다
      저도 많은 공부가 되었습니다 감사합니다~

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.05.05 23:25 신고

      또 한 번 감사의 말씀 전합니다!

  • Favicon of https://myage.tistory.com BlogIcon 바다21 2020.05.04 18:49 신고

    일단.. 쓰레드풀과 DB커넥션풀의 개수가 다른건.... 쓰레드는 쓰레드고.. 커넥션은 메모리상에 존재하는 자바객체이기 때문입니다. 톰캣의 쓰레드 입장에서는 커넥션 객체도 일종의 자원인거죠. 쓰레드라는건 어차피 머신의 스펙을 뛰어넘지 못합니다. 가진 시간과 자원을 쪼개서 쓰는 것 뿐이죠. 근데 그걸 크게 잡아놓으면.. 시간이 걸리더라도 요청을 다 처리하겠다는 의미이고... 그걸 작게 잡아놓으면 제한된 요청만 처리하고 나머지는 안하겠다.. 이런 뜻입니다.

    컴퓨터에서 프로그램이 어떻게 돌아가는가? 내지는 Java 객체가 메모리 상에서 어떻게 존재하고 실행되는가? CPU? OS가 프로세스나 쓰레드를 어떻게 처리하는가? 등의 내용을 추가로 탐구해보시면... 이해에 많은 도움이 되실거 같아요. 요새는 java를 배움과 동시에 spring 을 배우는 경우가 대부분이라 처음부터 너무나 추상화된 이미지를 머리에 넣고 개발하시는 분들이 많은 것 같아요. 알고보면 생각보다 어렵지 않을 수도 있습니다. 블로그에 파이팅이 넘쳐서.. 저도 모르게 파이팅 넘치는 댓글을 달게되네요.

    암튼.. 스프링부트 관련된 내용 찾다가 들어와서 우연히 본 글에 댓글 남겨봅니다. 포스팅해주신 스프링부트 관련자료는 많은 도움이 되었네요. 감사합니다! 그럼 열공하세요~!

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.05.05 23:29 신고

      선배님이 해주시는 말씀같아 이해도 잘 되고 와닿고 동기부여가 많이 됩니다.
      말씀 정말 감사하고요. 더 열심히 공부해보도록 하겠습니다!
      서로 좋은 의미로 파이팅할 수 있기를 바라봅니다 :-)

    • Favicon of https://myage.tistory.com BlogIcon 바다21 2020.05.06 13:08 신고

      파이팅이란... 저도 새로 알게 된것들.. 블로그로 포스팅하고 싶은데.. 게을러서 전혀 못하고 있거든요 ㅠㅠ 정아마추어님 블로그보고 반성 많이 하고 갑니다. 그런 파이팅이에요~! 파이팅!! ㅋㅋ

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.05.06 15:39 신고

      네~ 오해 없었습니다!
      좋은 말씀 다시 한 번 감사드립니다^^

  • 럭키아빠 2020.05.22 10:10

    지나가다가 저랑 완전 같은 고민을 하셨고, 잘 전개하셔서 댓글남깁니다! 화이팅

  • 바지년 2020.08.24 01:14

    자기 전에 궁금해서 찾고 있었는데, 좋은 글이 있군요ㅎ 감사합니다.

  • Favicon of https://sysgongbu.tistory.com BlogIcon 소연쏘 2020.09.27 12:58 신고

    좋은 글 감사합니다!! 많은 도움이 되었어요

  • galid1 2020.10.16 15:57

    잘 정리된것 같습니다!
    제가 생각하는 핵심 내용 한가지가 더있는데 어떻게 생각하시는지 궁금합니다.
    싱글코어에서 다중프로그래밍의 경우 어차피 한순간에는 하나의 프로세스(스레드)만 CPU를 할당받기 때문에, 수백 수천개의 요청이 와도 결국 한순간에는 하나의 스레드가 동작하기 때문에, 하나의 컨트롤러로 모든 요청처리가 가능하다고 생각합니다.
    이때 물론 정리해주신 것 처럼, 공유하는 데이터 즉, 클래스변수, 전역변수를 컨트롤러에서 사용하지 않기 때문에, "문제"가 없는 것이 아닌가 싶습니다.

    정리하자면, 한순간에 하나의 스레드만이 동작하므로, 하나의 컨트롤러로 모든 요청 처리가 "가능"하며,
    공유하는 데이터가 없기 때문에 "문제"가 발생하지 않는것 이라고 생각해봤습니다.

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.10.20 13:05 신고

      조금 엄밀하게 해서 분리하여 생각하면 맞는 말씀인 것 같습니다.
      깊은 이해는 저도 부족하여 공부를 하면서 생각을 해봐야겠습니다!

  • 익명 2020.12.15 16:51

    비밀댓글입니다

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.12.15 17:36 신고

      질문을 명확하게 이해하지는 못했습니다만... 답변을 달자면,
      스프링에서 싱글톤으로 동작하는 빈(ex. service, controller)이 상태를 갖으면 문제가 발생합니다.
      멀티 쓰레드가 각각의 스택 영역을 갖고있고, 메소드 영역에 있는 메소드(코드)를 실행하다가 메소드(코드) 중간에 힙에 있는 서비스 객체의 '상태 값에 접근하는 코드가 있으면 힙에 있는 '상태 값이 있는 공간(메모리)에 접근하는데 이 때 동시성 문제가 걸리면 동기화 문제가 생기는 것이지요.

  • 익명 2020.12.16 10:20

    비밀댓글입니다

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.12.17 11:51 신고

      네넵
      Map이라든지 boolean 값이라든지 그런 것들이요.
      이게 엄밀하게 얘기하면 Controller가 Service를 인터페이스로 주입받은 것도 컨트롤러라는 인스턴스의 필든데 이것을 상태값이라고 할 순 없거든요.
      그리고 불변 객체도 상태 값이라고 할 순 없기도 하고요.
      근데 또 불변이라도 final Map같이 자체로는 불변이어도 불변객체가 가지고 있는 값이 변할 수 있으니까 이럴 때는 또 동시성 문제가 생깁니다...

  • ㄹㅁ 2021.03.18 17:28

    제가 궁금했던 글이 딱 있네요 좋은글 감사드립니다!!

  • 궁금자 2021.06.05 15:00

    이거에 대해 궁금해 왔었는데 저는 하나의 요청은 하나의 쓰레드인데 메소드 안에 변수나 파라미터 리턴 값은 스택영역은 쓰레드 별로 생성되니 문제가 없겠구나라고 생각했는데 그렇게 생각하는게 혹시 틀렸를까요??

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2021.06.06 15:27 신고

      메소드 안의 변수나 파라미터, 리턴 값등이 어떤 것이냐에 따라 틀릴 수 있다고 봅니다.
      argument가 멀티 스레드에서 공유되는 객체면 스레드 별로 스택영역에 레퍼런스를 따로 두고 있겠지만 같은 객체 레퍼런스를 다루기 때문이지요.
      마찬가지로 메소드 안의 변수나 리턴 값이 새로 생성된 로컬 객체면 힙에 생성되어도 스레드간 레퍼런스를 공유하지 않으니 괜찮았겠지만 스레드간 공유되는 객체를 리턴값, 변수로 사용하면 문제가 되겠지요..!

  • 잘봤어요 2021.06.25 01:28

    윗 댓글 중에

    컴퓨터에서 프로그램이 어떻게 돌아가는가? 내지는 Java 객체가 메모리 상에서 어떻게 존재하고 실행되는가? CPU? OS가 프로세스나 쓰레드를 어떻게 처리하는가?

    위 3질문에 대해서는 저도 정말 알고 싶네요.
    이제 1년차인데 이 글과 같은 생각을 하고 있었습니다.
    면접을 위해서 외웠던 JVM 메모리 구조가 이제 좀 이해가 가네요.

    혹시 책 추천해주실 수 있을까요? 정말 알고싶습니다.

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2021.10.01 15:02 신고

      해당 부분만을 다루는 책은 아마 없을거에요!
      궁금하신 부분을 키워드로 웹에 흩어져있는 것 긁어모아서 봐야하실 것 같습니다.

  • ㅇㅇㅇ 2021.09.30 14:22

    감사합니다 의문이 풀렸네요~~

  • Favicon of https://hongdor.dev BlogIcon hongdor 2021.10.20 23:06 신고

    잘 봤습니다 ㅎㅎ

  • Favicon of https://runnz121.tistory.com BlogIcon 이봐요이상해씨 2021.11.26 00:50 신고

    올리신 게시글 하나하나가 모두 흥미를 끄는 주제들이네요 잘 보고갑니다 ㅎㅎ

  • 오소리 2022.01.25 18:14

    재밌게 잘 봐서 bean scope prototype 을 실서비스에서 본적이 있어서 공유드립니다 ㅎㅎ

    > 마지막에서 Spring 에서 '상태 / 변경될수있는 값' 를 갖는 bean 은 생성하지 말아야한다고 하셨죠
    prototype 은 multi thread 상황에서 상태가 변경될 수 있는 bean 을 만들 때 사용했었습니다.

    예시로 멀티쓰레드로 돌리기위해 Runnable 을 구현한 @Component 가 있고 이녀석은 몇개의 '상태 / 변경가능한 값을' 들고 있었습니다. 한 10개의 Runnabkle 을 executerService 이용해 invokeAll 하는 식으로 되어있었고,
    scope prototype 으로 하지않으면 하나의 객체를 변경하기 떄문에 문제가 발생했었습니다 ㅎㅎ

    하지만 별로 좋은 방법은 아니었다고 생각하는데요,
    @COmponent 로 선언해놓으면 dependency injection 이 되어 db access도 쉬우니 그랬던것같습니다.

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2022.01.26 11:44 신고

      오호 실사용 후기 남겨주셔서 감사합니다!
      말씀하신대로 그것이 좋은 방법이었는지 아니었는지보다는 구현 방법에 대해서 생각해볼 수 있었네요
      감사합니다~!

  • ㅇㅇ 2022.02.03 16:03

    좋은 글이에요 ㅎ

  • 타태 2022.02.09 23:34

    리뉴얼해서 새롭게 배포한 프로젝트 화면 로딩이 원본에 비해 너무 느린데 프론트가 무거워서인지 백이 느려서인지 궁금하더라구요.
    프론트 파트가 눈에 띄게 용량이 늘었고 Postman으로 API 요청시 서버의 요청처리 속도는 채 0.3초를 넘기지 않았기에 프론트 문제라고 생각이 되던 찰나...
    한 페이지에서 요청을 3개를 보내면 0.3초가 3개 모여서 1초가 되나....?하는 무지성으로 의식이 흘렀습니다.

    스프링의 싱글톤 + 따로 멀티 쓰레드 환경을 만든 적이 없기 때문에 요청이 단일 쓰레드로 밀려 들어오나 싶더라구요;;;
    동시 요청 처리를 그동안 어떻게 하고 있던거지..?하는 생각과 함께요.

    하지만 블로그 주소는 정프로, 닉네임은 정아마추어 ㅋㅋㅋ 님의 의식의 흐름 덕에 딱 제가 지금 궁금한 걸 해결했습니다.
    또 제가 기본기가 많이 부족하구나~~ 그리고 의식의 흐름이 공부까지는 가지 않고 있구나 느꼈습니다.

    공부 법을 하나 얻어간다 생각하고 감사 인사 남깁니다.
    감사합니다. : )

  • Favicon of https://kobumddaring.tistory.com BlogIcon 고딩왕 코범석 2022.03.12 20:05 신고

    도움 많이되었습니다!

  • 초보자 2022.04.11 16:53

    죄송한데, 읽어도 잘 이해가 안가서 여쭤봅니다 ㅠㅠ.

    요청이 발생 -> 할당된 스레드가 메소드 영역에 있는 메소드 정보를 본다. -> 해당 메소드 라인을 실행 -> 메소드 라인에서 힙에 공유되는 객체가 있으면 문제가 생긴다.

    controller 의 메소드 -> service 의 메소드 를 참조하는 일반적인 관계에서 getMember() { service.searchMember() } 라면
    controller 의 getMember() 메소드 -> service 의 searchMember() 순으로 스택에 쌓이는 건가요?..