<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>오늘도 운영중</title>
    <link>https://dev-lee-6.tistory.com/</link>
    <description>오늘도 운영하며 배우고 기록합니다.
Laravel, AI, MySQL, Git, AWS 중심의 실무 경험과 운영 노하우를 기록하는 백엔드 개발 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Sun, 6 Sep 2026 15:03:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>열심운영자</managingEditor>
    <image>
      <title>오늘도 운영중</title>
      <url>https://tistory1.daumcdn.net/tistory/6493589/attach/b7e7819f7c4847df80e1ab2657f4a7d4</url>
      <link>https://dev-lee-6.tistory.com</link>
    </image>
    <item>
      <title>[개발면접] 로드 밸런서(Load Balancer)는 왜 필요할까? L4와 L7 차이까지</title>
      <link>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EB%A9%B4%EC%A0%91-%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%84%9CLoad-Balancer%EB%8A%94-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C-L4%EC%99%80-L7-%EC%B0%A8%EC%9D%B4%EA%B9%8C%EC%A7%80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;처음 서비스를 만들 때는 한 대의 서버로도 충분한 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Server
  &amp;darr;
Database&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용자가 늘어나면 한 대의 서버가 모든 요청을 처리하기 어려워질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 단순히 더 좋은 서버 한 대로 교체하는 방법도 있지만, 여러 대의 서버로 요청을 나누는 방법도 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;             ┌&amp;rarr; Server A
Client &amp;rarr; ? ──┼&amp;rarr; Server B
             └&amp;rarr; Server C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 필요한 것이 &lt;b&gt;로드 밸런서(Load Balancer)&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 여러 서버 앞에서 요청을 받아 적절한 서버로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 면접에서는 여기서 한 단계 더 들어가 다음 질문이 자주 이어진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;L4와 L7 Load Balancer는 무엇이 다른가?&lt;/li&gt;
&lt;li&gt;서버 한 대에 장애가 발생하면 어떻게 처리하는가?&lt;/li&gt;
&lt;li&gt;Round Robin은 무엇인가?&lt;/li&gt;
&lt;li&gt;Load Balancer를 사용하면 무조건 고가용성이 보장되는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서를 단순히 &quot;트래픽을 분산하는 장치&quot;라고 외우기보다 &lt;b&gt;왜 필요한지와 어떤 기준으로 요청을 분산하는지&lt;/b&gt;를 이해해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버를 여러 대로 늘리면 무엇이 달라질까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 트래픽이 증가했을 때 서버 처리 능력을 높이는 방법은 크게 Scale Up과 Scale Out으로 나눠볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Scale Up&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 서버의 성능을 높이는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;4 Core / 8GB

&amp;darr;

16 Core / 32GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 크게 바꾸지 않고 성능을 높일 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버 한 대의 성능을 계속 높이는 데는 비용과 물리적인 한계가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서버 자체가 장애를 일으키면 서비스가 영향을 받을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Scale Out&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 자체를 여러 대로 늘리는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A

Server B

Server C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 서버를 세 대 만들었다고 해서 클라이언트 요청이 자동으로 적절하게 분산되는 것은 아니라는 점이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client
  &amp;darr;
어느 서버로 보내야 할까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역할을 담당할 수 있는 것이 로드 밸런서다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;                 ┌&amp;rarr; Server A
Client &amp;rarr; Load Balancer &amp;rarr; Server B
                 └&amp;rarr; Server C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 일반적으로 개별 애플리케이션 서버를 직접 선택할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서가 요청을 받아 Backend Server 중 하나로 전달한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로드 밸런서는 요청을 어떻게 분산할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요청을 단순히 순서대로 서버에 보내는 것만이 방법은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 특성에 따라 여러 분산 알고리즘을 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Round Robin&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 이해하기 쉬운 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 서버에 순서대로 전달한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Request 1 &amp;rarr; Server A
Request 2 &amp;rarr; Server B
Request 3 &amp;rarr; Server C
Request 4 &amp;rarr; Server A
Request 5 &amp;rarr; Server B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 처리 능력이 비슷하고 요청별 처리 비용도 크게 다르지 않다면 단순하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모든 요청의 처리 시간이 같다는 보장은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Server A가 오래 걸리는 요청을 여러 개 처리하고 있다고 해도 단순 Round Robin은 다음 요청을 다시 Server A로 전달할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Weighted Round Robin&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버마다 처리 능력이 다르다면 가중치를 줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A &amp;rarr; Weight 3
Server B &amp;rarr; Weight 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server A가 더 높은 사양이라면 더 많은 요청을 전달하도록 설정할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Least Connections&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 연결 수가 상대적으로 적은 서버로 요청을 전달한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A &amp;rarr; 100 connections
Server B &amp;rarr; 30 connections
Server C &amp;rarr; 70 connections

            &amp;darr;

새 연결 &amp;rarr; Server B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 유지 시간이 서로 다른 서비스에서 고려할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;b&gt;연결 수가 적다고 실제 CPU나 메모리 부하까지 반드시 낮은 것은 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 알고리즘 이름만 보고 선택하기보다 요청과 서버의 특성을 함께 봐야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;L4와 L7 Load Balancer는 무엇이 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서를 공부할 때 가장 많이 비교하는 것이 &lt;b&gt;L4와 L7&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 핵심 차이를 보면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;L4 Load Balancer&lt;/th&gt;
&lt;th&gt;L7 Load Balancer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기준 계층&lt;/td&gt;
&lt;td&gt;Transport Layer&lt;/td&gt;
&lt;td&gt;Application Layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 판단 정보&lt;/td&gt;
&lt;td&gt;IP, Port, TCP/UDP 등&lt;/td&gt;
&lt;td&gt;HTTP Method, Host, Path, Header 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청 내용 기반 라우팅&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 이해&lt;/td&gt;
&lt;td&gt;애플리케이션 수준까지 해석하지 않음&lt;/td&gt;
&lt;td&gt;HTTP 정보를 활용할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 활용&lt;/td&gt;
&lt;td&gt;TCP/UDP 연결 분산&lt;/td&gt;
&lt;td&gt;HTTP/HTTPS 요청 라우팅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;어느 계층의 정보를 보고 요청을 분산하는가&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L4 Load Balancer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4는 OSI 7 Layer의 Transport Layer를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 프로토콜은 TCP와 UDP다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 L4 Load Balancer는 주로 다음 정보를 기반으로 트래픽을 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Source IP
Destination IP
Source Port
Destination Port
Protocol&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 TCP 443 Port로 들어온 연결을 여러 서버로 분산하는 식이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Client
   &amp;darr;
TCP :443
   &amp;darr;
L4 Load Balancer
   &amp;darr;
┌────────┬────────┐
&amp;darr;        &amp;darr;        &amp;darr;
A        B        C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP의 &lt;code&gt;/users&lt;/code&gt;, &lt;code&gt;/orders&lt;/code&gt; 같은 URL Path를 애플리케이션 수준에서 분석해서 라우팅하는 것이 L4의 핵심 역할은 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L7 Load Balancer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L7은 Application Layer에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청의 내용을 이해하고 이를 라우팅에 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 요청들이 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /api/users&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /api/orders&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L7에서는 Path에 따라 서로 다른 Backend로 전달하는 구조를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;/api/users/*
      &amp;darr;
User Server

/api/orders/*
      &amp;darr;
Order Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host를 기준으로도 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;api.example.com
      &amp;darr;
API Server

admin.example.com
      &amp;darr;
Admin Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 HTTP의 Host, Path, Header 등의 정보를 이용한 세밀한 라우팅이 필요하다면 L7이 유용하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;L4가 L7보다 무조건 빠를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서 L4와 L7을 비교하다 보면 흔히 다음처럼 외우기도 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;L4 = 빠르다
L7 = 느리다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향 자체는 이해에 도움이 되지만 너무 단정적으로 답하는 것은 좋지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L7은 HTTP 요청 내용을 해석하고 추가적인 기능을 수행할 수 있기 때문에 일반적으로 L4보다 처리 과정이 복잡할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 성능은 제품, 구현 방식, TLS 처리, 네트워크 구조, 설정, 트래픽 특성 등에 따라 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 면접에서는 다음 정도로 설명하는 편이 좋다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4는 전송 계층의 정보를 기반으로 처리하기 때문에 구조적으로 단순한 반면, L7은 HTTP 같은 애플리케이션 계층 정보를 분석해 더 세밀한 라우팅을 할 수 있습니다. 대신 그만큼 처리해야 할 정보와 기능이 많을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 단순히 어느 쪽이 빠른지를 외우는 것이 아니라 &lt;b&gt;필요한 라우팅 기능이 무엇인지에 따라 선택하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버 한 대가 죽으면 어떻게 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서의 중요한 역할 중 하나가 &lt;b&gt;Health Check&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 세 대 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Load Balancer
   &amp;darr;
┌─────────┬─────────┐
&amp;darr;         &amp;darr;         &amp;darr;
Server A  Server B  Server C

Healthy   Healthy   Healthy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Server B에 장애가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A &amp;rarr; Healthy
Server B &amp;rarr; Unhealthy
Server C &amp;rarr; Healthy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서가 이를 확인할 수 있다면 Server B를 요청 대상에서 제외할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;             ┌&amp;rarr; Server A
Client &amp;rarr; LB ─┤
             └&amp;rarr; Server C

Server B &amp;rarr; 제외&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Health Check는 무엇을 확인할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 특정 Port에 연결 가능한지 확인할 수도 있고, HTTP Endpoint를 호출할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 Endpoint를 제공할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 상태라면 다음처럼 응답한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서도 고민할 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 프로세스는 살아 있는데 DB 연결이 끊어진 상태라면 이 서버를 정상이라고 볼 수 있을까?&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Load Balancer
    &amp;darr;
Application &amp;rarr; 정상
    &amp;darr;
Database &amp;rarr; 연결 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에 따라 Health Check가 무엇을 의미해야 하는지 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 단순한 Health Check는 실제 요청을 처리하지 못하는 서버를 정상으로 판단할 수 있고, 반대로 너무 많은 외부 의존성을 검사하면 일시적인 의존성 장애 때문에 정상 서버까지 트래픽에서 제외될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 단순히 &lt;code&gt;/health&lt;/code&gt; Endpoint를 만드는 것보다 &lt;b&gt;어떤 상태를 Healthy로 판단할 것인지&lt;/b&gt;가 중요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Load Balancer가 있으면 장애에 안전할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서를 두고 애플리케이션 서버를 여러 대로 구성하면 한 서버 장애에 대응하기 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이것만으로 전체 서비스의 고가용성이 자동으로 보장되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 구조를 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
   &amp;darr;
Load Balancer
   &amp;darr;
┌─────────┬─────────┐
&amp;darr;         &amp;darr;         &amp;darr;
Server A  Server B  Server C
   \         |         /
            DB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버는 세 대지만 Database가 하나이고 DB 장애에 대한 대비가 없다면 DB가 중단됐을 때 서비스 전체가 영향을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 직접 구축한 Load Balancer 자체가 한 대뿐이라면 그 장비가 새로운 &lt;b&gt;Single Point of Failure(SPOF)&lt;/b&gt;가 될 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client
   &amp;darr;
Load Balancer &amp;larr; 장애
   &amp;darr;
Servers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 고가용성을 생각할 때는 특정 계층만 여러 대로 만드는 것이 아니라 전체 요청 경로를 봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DNS

&amp;darr;

Load Balancer

&amp;darr;

Application

&amp;darr;

Cache

&amp;darr;

Database

&amp;darr;

외부 서비스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 지점에 단일 장애점이 있는지 확인하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리형 클라우드 Load Balancer를 사용하는 경우에는 서비스 제공자가 로드 밸런서 계층의 가용성과 확장을 관리해주지만, 애플리케이션과 데이터 계층까지 자동으로 고가용성이 되는 것은 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인 Session과 Load Balancing은 어떤 관계가 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 Cookie와 Session을 공부했다면 로드 밸런서와 연결해서 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 Server A에서 로그인했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
   &amp;darr;
Load Balancer
   &amp;darr;
Server A

Session 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session이 Server A의 로컬 메모리에만 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 요청은 Server B로 전달됐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
   &amp;darr;
Load Balancer
   &amp;darr;
Server B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server B에는 해당 Session이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조에서는 로그인 상태가 제대로 유지되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법 중 하나가 Sticky Session이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User A

&amp;darr;

항상 Server A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 방법은 Session Store를 서버 외부로 분리하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;             ┌&amp;rarr; Server A ─┐
Client &amp;rarr; LB ─┼&amp;rarr; Server B ─┼&amp;rarr; Redis
             └&amp;rarr; Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 서버가 요청을 받아도 같은 Redis에서 Session을 조회할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사례를 보면 로드 밸런서는 단순히 트래픽만 나누는 문제가 아니라 &lt;b&gt;애플리케이션이 상태를 어디에 저장하는지&lt;/b&gt;와도 연결된다는 것을 알 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포할 때도 Load Balancer를 활용할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 배포하는 상황도 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 한 대뿐이라면 애플리케이션 재시작 과정에서 요청 처리가 어려운 시간이 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서버와 Load Balancer를 사용한다면 서버를 순차적으로 교체하는 구조를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A &amp;rarr; 트래픽 제외 &amp;rarr; 배포 &amp;rarr; Health Check &amp;rarr; 다시 투입

Server B &amp;rarr; 트래픽 제외 &amp;rarr; 배포 &amp;rarr; Health Check &amp;rarr; 다시 투입&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식은 Rolling Deployment 같은 배포 전략과도 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실제 무중단 배포는 Load Balancer만 있다고 완성되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 종료 시 기존 요청을 어떻게 처리할지, DB Migration은 호환되는지, 새 버전과 이전 버전이 동시에 실행되어도 문제가 없는지 등도 함께 고려해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 면접에서는 어떻게 답하면 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서 다음 질문이 나왔다고 해보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 왜 사용하나요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 답하면 다음 정도로 정리할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 들어오는 트래픽을 여러 서버로 분산해서 특정 서버에 요청이 집중되는 것을 줄이고, 서버 확장과 장애 대응을 쉽게 하기 위해 사용합니다. Health Check를 통해 장애가 발생한 서버를 요청 대상에서 제외하는 방식도 사용할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4와 L7 차이를 묻는다면 다음처럼 답할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4 Load Balancer는 Transport Layer의 IP, Port, TCP, UDP 같은 정보를 기반으로 트래픽을 분산합니다. L7 Load Balancer는 HTTP 같은 Application Layer의 정보를 이해하기 때문에 Host, URL Path, Header 등을 기준으로 더 세밀하게 라우팅할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 나오는 꼬리 질문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Round Robin은 무엇인가요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 여러 서버에 순서대로 전달하는 방식이다. 구현은 단순하지만 각 요청의 처리 비용이나 서버의 현재 부하까지 반영하는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L4와 L7 중 어떤 것을 사용해야 하나요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Path나 Host 기반 라우팅처럼 애플리케이션 계층의 정보가 필요하다면 L7을 검토할 수 있다. 단순 TCP/UDP 수준의 분산이 필요하다면 L4가 적합할 수 있다. 서비스 요구사항을 기준으로 선택해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Load Balancer를 사용하면 서버 장애가 발생해도 괜찮나요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Health Check를 통해 장애 서버를 트래픽 대상에서 제외할 수 있지만, 전체 시스템의 장애가 사라지는 것은 아니다. Load Balancer 자체와 Database, Cache 등 다른 계층의 SPOF도 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Health Check는 무엇인가요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend Server가 요청을 정상적으로 처리할 수 있는 상태인지 주기적으로 확인하는 과정이다. TCP 연결 여부를 확인하거나 HTTP Health Endpoint를 호출하는 방식 등을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Sticky Session은 무엇인가요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 사용자의 요청을 가능한 한 같은 Backend Server로 전달하는 방식이다. 서버 로컬에 Session을 저장하는 구조에서 활용할 수 있지만 특정 서버에 상태가 묶일 수 있다는 점도 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로드 밸런서와 Reverse Proxy는 같은 건가요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 같은 개념은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reverse Proxy는 클라이언트 대신 Backend Server에 요청을 전달하는 역할을 의미하고, Load Balancing은 여러 Backend에 트래픽을 분산하는 기능을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx처럼 Reverse Proxy 역할을 하면서 동시에 Load Balancing 기능을 제공하는 소프트웨어도 있기 때문에 실제 구성에서는 두 개념을 함께 접하는 경우가 많다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서의 가장 기본적인 역할은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client

&amp;darr;

Load Balancer

&amp;darr;

┌─────────┬─────────┐
&amp;darr;         &amp;darr;         &amp;darr;
Server A  Server B  Server C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단순히 요청을 세 서버에 나눠주는 것으로만 이해하면 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스에서는 다음을 같이 생각해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;서버를 어떻게 선택할까?

&amp;rarr; Round Robin / Least Connections / Weight 등

HTTP Path에 따라 서버를 나누고 싶은가?

&amp;rarr; L7 Load Balancing 검토

서버에 장애가 발생하면?

&amp;rarr; Health Check

Session이 서버 로컬에 있다면?

&amp;rarr; Sticky Session 또는 공용 Session Store 검토

Load Balancer만 두면 고가용성이 완성될까?

&amp;rarr; 전체 시스템의 SPOF 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 면접에서는 &lt;b&gt;로드 밸런서는 트래픽을 분산한다&lt;/b&gt;는 정의에서 끝내지 않고, L4와 L7이 어떤 정보를 기준으로 동작하는지와 Health Check가 왜 필요한지까지 설명할 수 있으면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발에서도 서버가 여러 대가 되는 순간 인증 Session, 배포, 장애 대응처럼 이전에는 보이지 않던 문제들이 함께 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Load Balancing은 단순한 네트워크 설정이 아니라 &lt;b&gt;서비스를 여러 서버로 확장했을 때 요청과 상태, 장애를 어떻게 관리할 것인지에 대한 인프라 설계의 기본 요소&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;</description>
      <category>개발/개발면접</category>
      <category>healthcheck</category>
      <category>L4</category>
      <category>L7</category>
      <category>LoadBalancer</category>
      <category>ScaleOut</category>
      <category>개발면접</category>
      <category>로드밸런서</category>
      <category>백엔드개발</category>
      <category>서버</category>
      <category>인프라</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/187</guid>
      <comments>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EB%A9%B4%EC%A0%91-%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%84%9CLoad-Balancer%EB%8A%94-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C-L4%EC%99%80-L7-%EC%B0%A8%EC%9D%B4%EA%B9%8C%EC%A7%80#entry187comment</comments>
      <pubDate>Wed, 2 Sep 2026 13:35:03 +0900</pubDate>
    </item>
    <item>
      <title>Laravel Redis Queue와 Database Queue 차이는 무엇일까?</title>
      <link>https://dev-lee-6.tistory.com/entry/Laravel-Redis-Queue%EC%99%80-Database-Queue-%EC%B0%A8%EC%9D%B4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel에서 Queue를 처음 적용할 때는 Job 코드보다 어떤 Queue Connection을 사용할지 먼저 고민하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 프로젝트에서는 별도 인프라 없이 시작할 수 있는 Database Queue가 접근하기 쉽다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Application

&amp;darr;

jobs 테이블

&amp;darr;

Queue Worker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모가 커지거나 Queue 처리량이 많아지면 Redis Queue도 자연스럽게 후보에 들어온다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Application

&amp;darr;

Redis

&amp;darr;

Queue Worker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이런 질문이 생긴다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Database Queue도 잘 동작하는데 Redis로 바꿔야 할까?

Redis Queue는 정확히 무엇이 다른가?

Job이 얼마나 많아지면 Redis를 사용해야 할까?

Redis가 무조건 더 좋은 선택일까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &lt;code&gt;Redis가 메모리 기반이라 빠르다&lt;/code&gt; 정도로만 비교하면 실제 Queue Driver를 선택하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue는 애플리케이션과 Worker 사이에서 계속 데이터를 넣고 가져가는 인프라이기 때문에 처리량뿐 아니라 운영 복잡도, 장애 대응, 모니터링까지 함께 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Laravel Database Queue와 Redis Queue가 어떻게 다른지 살펴보고, 실제 서비스에서는 어떤 기준으로 선택하면 좋은지 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Database Queue와 Redis Queue의 핵심 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 핵심적인 차이를 간단히 정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Database Queue&lt;/th&gt;
&lt;th&gt;Redis Queue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;저장 위치&lt;/td&gt;
&lt;td&gt;RDB 테이블&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추가 인프라&lt;/td&gt;
&lt;td&gt;거의 없음&lt;/td&gt;
&lt;td&gt;Redis 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;도입 난이도&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;상대적으로 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue 처리량&lt;/td&gt;
&lt;td&gt;규모가 커질수록 DB 영향 고려&lt;/td&gt;
&lt;td&gt;높은 처리량에 상대적으로 유리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;애플리케이션 DB 영향&lt;/td&gt;
&lt;td&gt;있음&lt;/td&gt;
&lt;td&gt;Queue 부하를 Redis로 분리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laravel Horizon&lt;/td&gt;
&lt;td&gt;사용 불가&lt;/td&gt;
&lt;td&gt;사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 복잡도&lt;/td&gt;
&lt;td&gt;비교적 낮음&lt;/td&gt;
&lt;td&gt;Redis 운영 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합한 상황&lt;/td&gt;
&lt;td&gt;소규모&amp;middot;중간 규모 Queue, 빠른 도입&lt;/td&gt;
&lt;td&gt;Queue 비중과 처리량이 큰 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Database Queue = 나쁨

Redis Queue = 좋음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아니라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모가 작고 Queue 처리량이 많지 않다면 Database Queue가 오히려 단순하고 실용적인 선택일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Queue가 서비스의 중요한 처리 경로가 되고 Job 처리량이 계속 증가한다면 Redis로 분리하는 장점이 커질 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Database Queue는 어떻게 동작할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel에서 Database Queue를 사용하면 Job이 일반적으로 &lt;code&gt;jobs&lt;/code&gt; 테이블에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 설정은 Laravel 버전과 프로젝트 설정에 따라 다를 수 있지만 개념적으로 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;QUEUE_CONNECTION=database&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job을 Dispatch한다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;SendNotificationJob::dispatch($userId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Application

&amp;darr;

jobs 테이블 INSERT

&amp;darr;

Worker가 처리할 Job 조회 및 예약

&amp;darr;

Job 실행

&amp;darr;

정상 완료 후 Queue에서 제거&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 애플리케이션에서 사용하는 관계형 데이터베이스가 Queue Backend 역할도 담당하게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Database Queue의 가장 큰 장점은 단순함이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 MySQL을 사용하고 있다면 Redis 같은 별도 인프라 없이 Queue를 도입할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기존

Application + MySQL


Queue 도입

Application + MySQL
              └ Queue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 때문에 새로운 서버나 관리 대상을 추가하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 서비스나 Queue 사용량이 많지 않은 프로젝트에서는 꽤 큰 장점이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Database Queue는 언제 문제가 될 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database Queue라고 해서 처리량이 조금만 증가해도 바로 문제가 생기는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Queue 사용량이 커지면 애플리케이션 데이터베이스가 Queue 처리까지 담당한다는 점을 생각해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서비스에서 다음 작업이 동시에 발생한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;사용자 API

&amp;darr;

SELECT / INSERT / UPDATE


관리자 기능

&amp;darr;

대량 조회


Scheduler

&amp;darr;

Batch 처리


Queue

&amp;darr;

Job INSERT / 조회 / 예약 / 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 같은 MySQL을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 처리량이 적을 때는 큰 문제가 없을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Job 생성과 처리가 매우 빈번해지면 Queue를 위한 DB 작업도 무시하기 어려워진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker를 늘릴수록 DB 접근도 증가할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 밀린다고 Worker를 계속 늘려보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 1 ─┐
Worker 2 ─┤
Worker 3 ─┤
...       ├&amp;rarr; MySQL jobs
Worker 20 ┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 늘어나면 동시에 Queue를 확인하고 Job을 처리하는 프로세스도 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Job 내부에서도 MySQL을 사용한다면 DB 입장에서는&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Queue 자체를 위한 DB 접근

+

Job 비즈니스 로직의 DB 접근

+

일반 API의 DB 접근&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 모두 처리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점부터는 Queue 처리 성능만 볼 것이 아니라 애플리케이션 DB에 미치는 영향도 함께 봐야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Queue는 무엇이 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Queue를 사용하면 Queue 데이터를 관계형 데이터베이스가 아니라 Redis에서 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적인 설정은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;QUEUE_CONNECTION=redis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같이 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Application

&amp;darr;

Redis Queue

&amp;darr;

Worker

&amp;darr;

Job 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 내부에서 MySQL을 사용할 수 있다는 점은 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;Queue 자체를 관리하기 위한 작업은 Redis로 분리된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;MySQL

&amp;rarr; 비즈니스 데이터


Redis

&amp;rarr; Queue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분리가 Redis Queue를 사용하는 중요한 이유 중 하나다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis가 메모리 기반이라는 것만 보면 안 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Queue를 설명할 때 흔히 다음처럼 이야기한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Redis는 메모리를 사용한다.

&amp;darr;

DB보다 빠르다.

&amp;darr;

Queue도 빠르다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향 자체가 완전히 틀린 설명은 아니지만 실제 선택 기준으로는 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue에서는 단순 조회 속도 하나보다 다음을 같이 봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Job 유입량

Worker 수

Queue Polling

Queue 대기 시간

애플리케이션 DB 부하

Redis 운영 비용

장애 대응

모니터링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Queue의 장점은 단순히 &lt;code&gt;SELECT보다 Redis 명령이 빠르다&lt;/code&gt;는 것에만 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Queue의 저장과 소비에 필요한 부하를 애플리케이션 RDB에서 분리할 수 있다는 점&lt;/b&gt;도 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처리량이 많아지면 차이가 커질 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 하루에 몇십 개 정도의 메일 Job만 처리한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;하루 Job

50건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도 규모라면 Queue Backend의 성능 차이가 서비스에서 중요한 병목이 아닐 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 Queue 때문에 Redis 인프라를 추가하는 것이 더 복잡할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음과 같은 서비스라면 이야기가 달라질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;대량 알림

상품 데이터 동기화

외부 API 데이터 수집

대량 Batch

이벤트 처리

수많은 비동기 Job&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시간에 수만 개 이상의 Job이 생성되고 여러 Worker가 동시에 처리한다면 Queue Backend 자체의 처리량과 DB 부하를 검토할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Job 10,000개부터 Redis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 특정 숫자로 기준을 정하기는 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 하나의 처리 시간과 Payload, Worker 수, Job 생성 패턴, DB 성능, 서버 구조가 모두 다르기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue가 느리다고 Redis부터 도입하면 안 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 대기 시간이 길어지면 Database Queue가 원인이라고 생각하기 쉽다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue 느림

&amp;darr;

Database Queue 때문?

&amp;darr;

Redis 전환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 병목이 다른 곳에 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job 하나가 다음 Query 때문에 30초 걸린다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;SELECT ...
FROM orders
JOIN customers ...
WHERE ...;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Backend를 Redis로 변경해도 Job 내부 Query는 그대로 30초가 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 외부 API가 느릴 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Redis

&amp;darr;

Worker

&amp;darr;

외부 API 요청

&amp;darr;

30초 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis로 바꿔도 해결되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Queue가 밀린다면 먼저 다음을 구분하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Queue Backend가 병목인가?

Job 자체가 느린가?

Worker가 부족한가?

DB가 느린가?

외부 API가 느린가?

Retry가 반복되고 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Queue 성능 문제의 만능 해결책이 아니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Database Queue가 충분한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 있다고 해서 모든 프로젝트의 Queue를 Redis로 운영할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database Queue가 잘 맞는 상황도 많다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue 사용량이 많지 않다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 정도의 작업만 비동기로 처리한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;간헐적인 메일 발송

관리자 Excel 생성

소규모 Batch

일부 외부 API 연동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 유입량이 적고 Queue 대기 시간에도 문제가 없다면 Database Queue로 충분할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 구조를 단순하게 유지하고 싶다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 추가하면 관리 대상도 하나 늘어난다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Redis Connection

Memory

장애

Persistence

Monitoring

Network&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 규모가 작은데 이런 운영 비용까지 추가하는 것이 항상 이득인 것은 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL에 충분한 여유가 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database Queue를 사용하더라도 MySQL CPU와 Connection에 충분한 여유가 있고 Queue 처리 때문에 일반 API가 영향을 받지 않는다면 당장 변경할 이유가 크지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 선택에서는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;더 좋은 기술이 무엇인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 문제를 해결하기 위해 필요한가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 보는 것이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Queue를 검토할 만한 시점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음 상황이 반복된다면 Redis Queue를 검토할 이유가 생긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue 처리량이 계속 증가한다&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Job 유입량 증가

&amp;darr;

Worker 증가

&amp;darr;

Queue Backend 접근 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 서비스의 주요 처리 경로가 되기 시작했다면 별도의 Queue Backend를 사용하는 장점이 커진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue 때문에 애플리케이션 DB 부하가 커진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database Queue를 사용하는 상황에서 Queue 작업 자체가 DB 부하의 의미 있는 부분을 차지하기 시작한다면 Redis로 분리하는 것을 검토할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker를 많이 운영해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue별 Worker 수가 증가하고 동시 처리량이 커진다면 Queue Backend의 확장성도 중요해진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Horizon을 사용하고 싶다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Horizon은 Redis Queue를 대상으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 처리량과 Runtime, 실패 Job 등을 관리하고 Worker 운영을 더 체계적으로 가져가고 싶다면 Redis + Horizon 구성을 검토할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Queue에도 운영 비용이 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Queue가 성능과 확장성 측면에서 유리한 경우가 있지만 무료로 얻는 장점은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis도 하나의 인프라다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 다음을 생각해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Redis 장애

Memory 부족

Connection 문제

Eviction 정책

Persistence

Network 장애

Failover

Monitoring&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 메모리는 무한하지 않다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 비정상적으로 쌓이면 Redis 메모리도 증가한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;외부 API 장애

&amp;darr;

Job 처리 실패

&amp;darr;

Queue 처리 지연

&amp;darr;

새로운 Job 계속 유입

&amp;darr;

Queue 증가

&amp;darr;

Redis Memory 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue를 Redis로 옮겼다고 Queue 적체 문제가 사라지는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 Redis 메모리 사용량이라는 새로운 운영 지표가 생긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시와 Queue를 같은 Redis에서 사용할 때도 주의한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 하나의 Redis를 다음 용도로 모두 사용한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Cache

Session

Queue

Lock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모가 작다면 충분히 가능한 구성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용량이 커지면 하나의 Redis 장애가 여러 기능에 동시에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 메모리 정책과 장애 영향 범위를 생각해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 데이터와 일반 캐시 데이터는 중요도가 다를 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 커진다면 Redis를 용도별로 어떻게 분리할지도 별도의 설계 문제가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Persistence도 생각해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 단순 캐시로만 사용한다면 일부 데이터 손실을 허용할 수 있는 경우가 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Cache 삭제

&amp;darr;

DB에서 다시 조회

&amp;darr;

Cache 재생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue는 성격이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리되지 않은 Job이 사라지면 비즈니스 작업 자체가 누락될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;결제 후처리

알림

외부 시스템 동기화

포인트 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Job이라면 데이터 유실이 서비스 문제로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Redis Queue를 운영할 때는&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Persistence

Replication

Backup

Failover

장애 시 Job 처리 정책&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 부분도 서비스 요구 수준에 맞게 검토해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 Redis를 Cache처럼 생각하고 Queue를 올리는 것은 위험할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Queue로 변경하면 Job 코드는 달라질까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue 추상화를 사용하고 있다면 기본적인 Job 코드는 크게 달라지지 않는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;class SendNotificationJob implements ShouldQueue
{
    public function handle(): void
    {
        // 비동기 처리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispatch도 동일하다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;SendNotificationJob::dispatch($userId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection 설정을 통해 Queue Backend를 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 추상화 덕분에 애플리케이션 코드 전체를 다시 작성하지 않고 Queue Backend를 변경할 수 있는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 전환에서는 설정만 바꾸고 끝내면 안 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Database Queue에서 Redis로 전환할 때 확인할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중인 서비스의 Queue Backend를 변경한다면 기존 Job 처리 상태까지 생각해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Database Queue에 아직 Job이 남아 있는 상태에서&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;database

&amp;darr;

redis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 Connection만 변경했다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;jobs&lt;/code&gt; 테이블의 Job은 Redis로 자동 이동되는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 전환 시에는&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기존 Database Queue에 남은 Job

현재 Worker 상태

새로운 Job이 들어갈 Connection

Supervisor 설정

배포 순서

실패 Job

모니터링&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 함께 확인해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker 설정도 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supervisor에서 Connection을 명시했다면 Worker 명령도 변경해야 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 기존에&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;php artisan queue:work database \
    --queue=default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용했다면 Redis 전환 후에는 해당 운영 설정도 맞춰야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 변경 후 Worker 재시작까지 배포 과정에서 관리하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue 이름과 Connection은 다른 개념이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue를 처음 다룰 때 헷갈리기 쉬운 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 두 개는 서로 다른 개념이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Connection

database
redis


Queue

default
notification
export
batch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Redis Connection 안에서도 여러 Queue를 운영할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Redis

├ default
├ notification
├ export
└ batch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database Connection에서도 마찬가지다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Database

├ default
├ notification
├ export
└ batch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;SendSmsJob::dispatch()
    -&amp;gt;onQueue('notification');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 &lt;code&gt;notification&lt;/code&gt;은 Redis나 Database를 의미하는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 Connection을 사용할지는 별도로 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하다면 Connection 자체를 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;SendSmsJob::dispatch()
    -&amp;gt;onConnection('redis')
    -&amp;gt;onQueue('notification');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 설정을 볼 때 이 두 개념을 구분하면 Queue 구조를 이해하기 쉬워진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQS 같은 다른 선택지도 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue 선택지가 Database와 Redis만 있는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모와 인프라 환경에 따라 Amazon SQS 같은 관리형 메시지 Queue를 선택할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로 보면 선택지가 다음처럼 넓어진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Database Queue

&amp;rarr; 도입과 운영이 단순


Redis Queue

&amp;rarr; 빠른 Queue 처리
&amp;rarr; Horizon 활용
&amp;rarr; Redis 직접 운영 필요


Managed Queue

&amp;rarr; Queue 인프라 운영 부담 일부 감소
&amp;rarr; 클라우드 서비스 특성과 비용 고려&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Database Queue에서 바로 Redis로 가는 것이 항상 정답은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 중심의 인프라라면 SQS가 더 자연스러운 선택일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Redis와 SQS는 동작 특성과 전달 모델, 운영 방식이 다르기 때문에 별도의 비교가 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 Queue Driver를 선택하면 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 특정 Job 개수보다 현재 서비스 상황을 기준으로 판단하는 편이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Database Queue를 먼저 고려할 수 있는 경우&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue 규모가 작다.

&amp;darr;

Database Queue


Queue 사용량이 많지 않다.

&amp;darr;

Database Queue


별도 Redis 운영이 부담스럽다.

&amp;darr;

Database Queue


현재 DB 부하에 문제가 없다.

&amp;darr;

Database Queue 유지&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Queue를 검토할 수 있는 경우&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue 처리량이 계속 증가한다.

&amp;darr;

Redis 검토


Queue Worker가 많아지고 있다.

&amp;darr;

Redis 검토


Queue 작업의 DB 부하가 커지고 있다.

&amp;darr;

Redis 검토


Horizon을 활용하고 싶다.

&amp;darr;

Redis 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;서비스가 커졌으니까 Redis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니라 현재 Queue Backend가 실제 병목이나 운영상의 제약이 되고 있는지를 확인하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue Driver를 바꾸기 전에 먼저 확인할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database Queue가 느리다고 느껴진다면 Redis로 변경하기 전에 다음 정도는 확인해보는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;□ Queue 대기 시간이 실제로 증가하고 있는가?

□ Job 자체의 실행 시간이 긴 것은 아닌가?

□ Worker 수가 부족한 것은 아닌가?

□ Worker가 정상적으로 실행되고 있는가?

□ Job 내부에 Slow Query가 있지 않은가?

□ 외부 API가 느린 것은 아닌가?

□ Retry가 반복되고 있지 않은가?

□ jobs 테이블 접근이 실제 DB 부하의 원인인가?

□ Redis를 추가했을 때 운영 복잡도를 감당할 수 있는가?

□ Queue Job의 유실 허용 수준을 정했는가?

□ Redis 장애 시 대응 방법을 생각했는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문을 확인했을 때 Database Queue 자체가 병목이라면 Redis 전환의 근거도 훨씬 명확해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Database Queue와 Redis Queue의 차이를 단순하게 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Database Queue

&amp;rarr; 기존 RDB 활용
&amp;rarr; 도입이 단순
&amp;rarr; Queue 규모가 크지 않을 때 실용적


Redis Queue

&amp;rarr; Queue 부하를 RDB에서 분리
&amp;rarr; 높은 처리량에 유리
&amp;rarr; Horizon 활용 가능
&amp;rarr; Redis 운영 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음부터 Redis Queue를 선택해야 하는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 사용량이 많지 않고 Database Queue가 안정적으로 처리되고 있다면 굳이 구조를 복잡하게 만들 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Job과 Worker가 계속 증가하면서 Queue 처리 자체가 애플리케이션 DB에 부담을 주기 시작한다면 Redis를 검토할 이유가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Queue가 느리다는 이유만으로 Redis로 변경해서도 안 된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Queue Wait Time

Job Runtime

Worker 상태

DB 부하

외부 API

Retry&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 먼저 확인해 실제 병목을 찾는 것이 우선이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Queue Driver를 선택할 때 가장 중요한 질문은&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Redis가 Database보다 빠른가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니라&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;현재 서비스에서 Queue를 어디에 저장하고 처리하는 것이
성능과 운영 복잡도 사이에서 가장 적절한가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Database Queue가 문제없이 동작한다면 그대로 사용하는 것도 충분히 좋은 선택이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 생기기 전에 모든 인프라를 미리 복잡하게 만드는 것보다 &lt;b&gt;Queue 처리량과 DB 부하를 관찰하면서 전환이 필요한 시점을 판단하는 것&lt;/b&gt;이 운영 환경에서는 더 현실적인 접근이다.&lt;/p&gt;</description>
      <category>개발/Laravel</category>
      <category>backend</category>
      <category>DatabaseQueue</category>
      <category>Laravel</category>
      <category>LaravelHorizon</category>
      <category>LaravelQueue</category>
      <category>php</category>
      <category>Queue</category>
      <category>QueueWorker</category>
      <category>redis</category>
      <category>비동기처리</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/186</guid>
      <comments>https://dev-lee-6.tistory.com/entry/Laravel-Redis-Queue%EC%99%80-Database-Queue-%EC%B0%A8%EC%9D%B4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C#entry186comment</comments>
      <pubDate>Mon, 31 Aug 2026 16:37:03 +0900</pubDate>
    </item>
    <item>
      <title>[개발면접] 쿠키(Cookie)와 세션(Session)의 차이, 로그인은 어떻게 유지될까?</title>
      <link>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EB%A9%B4%EC%A0%91-%EC%BF%A0%ED%82%A4Cookie%EC%99%80-%EC%84%B8%EC%85%98Session%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9C%A0%EC%A7%80%EB%90%A0%EA%B9%8C</link>
      <description>&lt;p&gt;웹 개발을 하다 보면 로그인 이후 페이지를 이동하거나 API를 여러 번 호출해도 로그인 상태가 계속 유지된다.&lt;/p&gt;
&lt;p&gt;그런데 HTTP는 기본적으로 &lt;strong&gt;Stateless&lt;/strong&gt;한 프로토콜이다. 각각의 요청은 독립적으로 처리되기 때문에 별도의 장치가 없다면 서버는 이전 요청에서 누가 로그인했는지 기억하지 못한다.&lt;/p&gt;
&lt;p&gt;그렇다면 서버는 어떻게 같은 사용자의 요청이라는 것을 알 수 있을까?&lt;/p&gt;
&lt;p&gt;이 문제를 이해할 때 가장 먼저 알아야 하는 개념이 &lt;strong&gt;쿠키(Cookie)&lt;/strong&gt;와 &lt;strong&gt;세션(Session)&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;핵심 차이부터 보면 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Cookie&lt;/th&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;주요 저장 위치&lt;/td&gt;
&lt;td&gt;클라이언트&lt;/td&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 역할&lt;/td&gt;
&lt;td&gt;작은 데이터 저장 및 전달&lt;/td&gt;
&lt;td&gt;사용자 상태 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증에서의 역할&lt;/td&gt;
&lt;td&gt;Session ID 등을 전달&lt;/td&gt;
&lt;td&gt;로그인 사용자 정보 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 저장 공간&lt;/td&gt;
&lt;td&gt;직접 필요하지 않음&lt;/td&gt;
&lt;td&gt;필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 고려사항&lt;/td&gt;
&lt;td&gt;탈취, 위·변조, 보안 옵션&lt;/td&gt;
&lt;td&gt;저장소, 만료, 서버 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 활용&lt;/td&gt;
&lt;td&gt;사용자 설정, Session ID&lt;/td&gt;
&lt;td&gt;로그인 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;여기서 중요한 점은 &lt;strong&gt;Cookie와 Session이 서로 반대되는 인증 방식은 아니라는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;일반적인 세션 기반 로그인에서는 서버가 Session을 관리하고, 그 Session을 찾기 위한 &lt;strong&gt;Session ID를 Cookie에 저장해서 전달하는 방식&lt;/strong&gt;을 많이 사용한다.&lt;/p&gt;
&lt;h2&gt;HTTP가 Stateless한데 로그인은 어떻게 유지할까?&lt;/h2&gt;
&lt;p&gt;사용자가 로그인을 요청한다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
  |
  | POST /login
  v
Server&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버는 아이디와 비밀번호를 확인하고 로그인에 성공한다.&lt;/p&gt;
&lt;p&gt;그다음 사용자가 마이페이지를 요청한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
  |
  | GET /mypage
  v
Server&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTP 관점에서는 &lt;code&gt;/login&lt;/code&gt;과 &lt;code&gt;/mypage&lt;/code&gt;가 서로 독립적인 요청이다.&lt;/p&gt;
&lt;p&gt;아무런 인증 정보도 전달하지 않는다면 서버는 &lt;code&gt;/mypage&lt;/code&gt; 요청을 보낸 사용자가 조금 전에 로그인한 사람인지 알 수 없다.&lt;/p&gt;
&lt;p&gt;결국 보호된 API를 처리할 때 서버는 다음 질문에 답할 수 있어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이 요청을 보낸 사용자는 누구인가?

↓

로그인된 사용자인가?

↓

어떤 권한을 가지고 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 해결하는 대표적인 방법 중 하나가 세션 기반 인증이다.&lt;/p&gt;
&lt;h3&gt;Cookie는 무엇일까?&lt;/h3&gt;
&lt;p&gt;Cookie는 브라우저가 관리하는 작은 데이터다.&lt;/p&gt;
&lt;p&gt;서버는 HTTP 응답의 &lt;code&gt;Set-Cookie&lt;/code&gt; 헤더를 이용해 브라우저에 Cookie를 저장하도록 요청할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Set-Cookie: theme=dark&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;브라우저는 조건이 맞는 이후 요청에서 해당 Cookie를 자동으로 포함할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Cookie: theme=dark&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server
  |
  | Set-Cookie
  v
Browser

Browser
  |
  | Cookie
  v
Server&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;언어 설정이나 UI 설정처럼 간단한 데이터를 저장할 수도 있고, 인증에서는 Session ID를 전달하는 용도로도 많이 사용한다.&lt;/p&gt;
&lt;h3&gt;Session은 무엇일까?&lt;/h3&gt;
&lt;p&gt;Session은 서버가 특정 사용자의 상태를 관리하는 방식이다.&lt;/p&gt;
&lt;p&gt;예를 들어 로그인한 사용자가 다음과 같다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;user_id = 123&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버가 Session ID를 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;session_id = abc123xyz&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 서버의 Session Store에는 두 값을 연결해서 저장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;abc123xyz
    ↓
user_id = 123&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;브라우저에는 사용자 정보 전체를 보내는 대신 Session ID를 전달할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Set-Cookie: session_id=abc123xyz&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 요청에서는 브라우저가 Session ID를 다시 전달한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
   |
   | session_id=abc123xyz
   v
Server
   |
   | Session Store 조회
   v
user_id = 123&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTP 자체는 Stateless하지만 이런 식으로 요청마다 사용자 식별 정보를 전달하면 애플리케이션에서는 로그인 상태를 유지할 수 있다.&lt;/p&gt;
&lt;h2&gt;세션 기반 로그인은 실제로 어떻게 동작할까?&lt;/h2&gt;
&lt;p&gt;전체 흐름을 한 번 연결해보자.&lt;/p&gt;
&lt;h3&gt;1. 사용자가 로그인한다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST /login
Content-Type: application/json

{
    &amp;quot;email&amp;quot;: &amp;quot;user@example.com&amp;quot;,
    &amp;quot;password&amp;quot;: &amp;quot;password&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버는 전달받은 인증 정보를 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Email / Password 확인

↓

로그인 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 서비스에서는 비밀번호 원문을 DB 값과 직접 비교하는 것이 아니라 안전한 비밀번호 해시 검증 방식을 사용해야 한다.&lt;/p&gt;
&lt;h3&gt;2. 서버가 Session을 생성한다&lt;/h3&gt;
&lt;p&gt;로그인에 성공하면 서버는 사용자를 식별할 수 있는 Session을 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Session ID

abc123xyz&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Session Store에는 예를 들어 다음과 같은 정보가 저장될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;abc123xyz

↓

user_id = 123&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Session ID를 Cookie로 전달한다&lt;/h3&gt;
&lt;p&gt;서버는 응답을 통해 Session ID를 전달한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Set-Cookie: session_id=abc123xyz; HttpOnly; Secure&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;브라우저는 Cookie를 저장한다.&lt;/p&gt;
&lt;h3&gt;4. 다음 요청에서 Cookie를 보낸다&lt;/h3&gt;
&lt;p&gt;사용자가 마이페이지를 요청한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /mypage
Cookie: session_id=abc123xyz&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 서버가 Session을 조회한다&lt;/h3&gt;
&lt;p&gt;서버는 전달받은 Session ID를 이용해 Session Store를 조회한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;session_id = abc123xyz

↓

Session Store 조회

↓

user_id = 123

↓

로그인 사용자 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이것이 세션 기반 로그인 상태 유지의 기본 구조다.&lt;/p&gt;
&lt;h2&gt;서버가 여러 대라면 Session은 어디에 저장할까?&lt;/h2&gt;
&lt;p&gt;여기까지는 서버가 한 대라면 비교적 단순하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client

↓

Server A&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server A의 메모리나 로컬 파일에 Session을 저장해도 요청이 계속 Server A로 들어온다면 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 서비스가 커지면서 애플리케이션 서버를 여러 대로 확장하면 이야기가 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;             ┌── Server A
Client → LB ─┼── Server B
             └── Server C&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용자가 Server A에서 로그인했다고 해보자.&lt;/p&gt;
&lt;p&gt;Server A의 메모리에 Session이 생성됐다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server A

session_id = abc123
user_id = 123&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 다음 요청은 Load Balancer에 의해 Server B로 전달될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client

↓

Load Balancer

↓

Server B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server B의 로컬 메모리에는 해당 Session이 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;session_id = abc123

↓

Session 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인증이 정상적으로 유지되지 않을 수 있다.&lt;/p&gt;
&lt;h3&gt;Sticky Session&lt;/h3&gt;
&lt;p&gt;한 가지 방법은 특정 사용자의 요청을 계속 같은 서버로 보내는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User A → Server A
User A → Server A
User A → Server A&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 Sticky Session이라고 한다.&lt;/p&gt;
&lt;p&gt;구조가 단순할 수 있지만 특정 서버에 대한 의존성이 생기고 서버 장애나 확장 상황도 고려해야 한다.&lt;/p&gt;
&lt;h3&gt;공용 Session Store&lt;/h3&gt;
&lt;p&gt;다른 방법은 Session을 애플리케이션 서버 밖의 공용 저장소에 두는 것이다.&lt;/p&gt;
&lt;p&gt;예를 들어 Redis를 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;             ┌── Server A ─┐
Client → LB ─┼── Server B ─┼── Redis
             └── Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어느 서버에서 요청을 처리하더라도 같은 Redis에서 Session을 조회할 수 있다.&lt;/p&gt;
&lt;p&gt;이 때문에 분산 환경에서 Redis를 Session Store로 활용하는 구조를 볼 수 있다.&lt;/p&gt;
&lt;p&gt;다만 Redis를 사용한다고 문제가 모두 끝나는 것은 아니다.&lt;/p&gt;
&lt;p&gt;Redis 장애 시 인증에 어떤 영향을 주는지, Session TTL은 어떻게 관리할지, 메모리 사용량은 어느 정도인지도 함께 고려해야 한다.&lt;/p&gt;
&lt;h2&gt;Cookie를 사용할 때 무엇을 조심해야 할까?&lt;/h2&gt;
&lt;p&gt;Session 데이터를 서버에 저장한다고 해서 인증이 자동으로 안전해지는 것은 아니다.&lt;/p&gt;
&lt;p&gt;Session ID 자체가 탈취되면 공격자가 해당 사용자의 Session을 이용할 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 인증 Cookie에는 몇 가지 중요한 보안 옵션이 있다.&lt;/p&gt;
&lt;h3&gt;HttpOnly&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Set-Cookie: session_id=abc123; HttpOnly&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JavaScript에서 해당 Cookie에 접근하는 것을 제한한다.&lt;/p&gt;
&lt;p&gt;XSS를 이용한 Cookie 탈취 위험을 줄이는 데 도움이 된다.&lt;/p&gt;
&lt;p&gt;다만 &lt;strong&gt;HttpOnly가 XSS 공격 자체를 막아주는 것은 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Secure&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Set-Cookie: session_id=abc123; Secure&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTPS 연결에서만 Cookie가 전송되도록 한다.&lt;/p&gt;
&lt;p&gt;인증 Cookie를 운영 환경에서 사용할 때 중요하게 확인할 설정이다.&lt;/p&gt;
&lt;h3&gt;SameSite&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Set-Cookie: session_id=abc123; SameSite=Lax&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cross-Site 요청에서 Cookie가 전송되는 범위를 제어한다.&lt;/p&gt;
&lt;p&gt;대표적인 값으로 &lt;code&gt;Strict&lt;/code&gt;, &lt;code&gt;Lax&lt;/code&gt;, &lt;code&gt;None&lt;/code&gt;이 있다.&lt;/p&gt;
&lt;p&gt;서비스 구조에 따라 적절한 값을 선택해야 하며, &lt;code&gt;SameSite=None&lt;/code&gt;을 사용할 때는 일반적으로 &lt;code&gt;Secure&lt;/code&gt;도 함께 필요하다.&lt;/p&gt;
&lt;p&gt;SameSite는 CSRF 위험을 줄이는 데 도움이 되지만 이것만으로 모든 CSRF 문제를 해결한다고 생각하면 안 된다.&lt;/p&gt;
&lt;h3&gt;사용자 ID 자체를 Cookie에 저장하면 안 될까?&lt;/h3&gt;
&lt;p&gt;다음처럼 Cookie를 만든다고 생각해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;user_id=123&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버가 이 값을 그대로 믿어서 로그인 사용자를 결정한다면 문제가 된다.&lt;/p&gt;
&lt;p&gt;클라이언트가 값을 바꿀 수 있기 때문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;user_id=123

↓

user_id=1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버가 이를 그대로 신뢰하면 다른 사용자의 권한으로 접근하는 심각한 취약점이 생길 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 인증에서는 &lt;strong&gt;클라이언트가 보낸 식별 값을 그대로 신뢰하지 않고 서버에서 검증할 수 있어야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Session과 JWT는 무엇이 다를까?&lt;/h2&gt;
&lt;p&gt;Cookie와 Session을 공부하면 자연스럽게 JWT가 이어진다.&lt;/p&gt;
&lt;p&gt;여기서도 흔한 오해가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Session = 오래된 방식
JWT = 최신 방식&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 단순하게 나누면 안 된다.&lt;/p&gt;
&lt;p&gt;두 방식은 인증 상태를 관리하는 구조가 다르다.&lt;/p&gt;
&lt;h3&gt;Session 기반 인증&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
  |
  | Session ID
  v
Server
  |
  | Session Store 조회
  v
User 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버가 Session 상태를 관리한다.&lt;/p&gt;
&lt;p&gt;Session을 제거하면 해당 인증 상태를 서버에서 종료시키는 것도 비교적 직관적이다.&lt;/p&gt;
&lt;p&gt;대신 사용자가 많아지면 Session Store를 운영해야 하고 서버 확장 구조도 고려해야 한다.&lt;/p&gt;
&lt;h3&gt;JWT 기반 인증&lt;/h3&gt;
&lt;p&gt;일반적인 JWT 인증에서는 Token에 Claim이 포함되고 서버가 Token의 서명을 검증한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
  |
  | JWT
  v
Server
  |
  | Signature 검증
  v
Claims 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;매 요청마다 중앙 Session Store를 조회하지 않는 구조를 만들 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p&gt;하지만 JWT에도 다른 문제가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;발급한 Token을 즉시 무효화하려면?

Access Token이 탈취되면?

Refresh Token은 어디에 저장할까?

Token 만료 시간은 얼마나 설정할까?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 Session과 JWT 중 무엇이 더 좋다고 단정하기보다 서비스 요구사항에 맞춰 선택해야 한다.&lt;/p&gt;
&lt;h2&gt;실무에서는 어떤 기준으로 선택하면 좋을까?&lt;/h2&gt;
&lt;p&gt;인증 방식을 선택할 때는 기술의 유행보다 서비스 구조를 먼저 보는 것이 좋다.&lt;/p&gt;
&lt;h3&gt;서버 중심의 웹 애플리케이션&lt;/h3&gt;
&lt;p&gt;브라우저와 하나의 백엔드 애플리케이션이 중심이고 서버가 사용자 상태를 관리하는 것이 자연스럽다면 Session 방식이 단순할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser

↓

Backend

↓

Session Store&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;여러 서버에서 Session을 공유해야 하는 경우&lt;/h3&gt;
&lt;p&gt;애플리케이션 서버를 여러 대 운영하면서 Session 기반 인증을 사용한다면 공용 Session Store를 검토할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server A ─┐
Server B ─┼── Redis
Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;다양한 API Client를 고려해야 하는 경우&lt;/h3&gt;
&lt;p&gt;웹, 모바일 앱, 외부 Client 등 여러 환경에서 API를 사용한다면 Token 기반 인증을 검토할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Web ──────┐
Mobile ───┼── API
Client ───┘&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그렇다고 이런 구조에서는 반드시 JWT를 사용해야 한다는 의미는 아니다.&lt;/p&gt;
&lt;p&gt;인증 구조를 결정할 때는 다음을 같이 봐야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;인증 상태를 어디에서 관리할 것인가?

로그아웃을 어떻게 처리할 것인가?

권한 변경을 얼마나 빠르게 반영해야 하는가?

서버가 여러 대일 때 어떻게 확장할 것인가?

인증 정보가 탈취되면 어떻게 대응할 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 Cookie, Session, JWT는 이름만 비교하기보다 &lt;strong&gt;전체 인증 흐름 안에서 어떤 역할을 하는지&lt;/strong&gt;를 이해하는 것이 중요하다.&lt;/p&gt;
&lt;h2&gt;개발 면접에서는 어떻게 답하면 될까?&lt;/h2&gt;
&lt;p&gt;면접에서 다음 질문이 나왔다고 해보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Cookie와 Session의 차이를 설명해주세요.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;짧게 답한다면 다음 정도로 정리할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Cookie는 클라이언트에 저장되는 작은 데이터이고, Session은 서버에서 사용자 상태를 관리하는 방식입니다. 세션 기반 로그인에서는 서버가 로그인 정보를 Session Store에 저장하고 클라이언트에는 Session ID를 Cookie로 전달하는 방식이 일반적입니다. 이후 요청에서 브라우저가 Session ID를 보내면 서버가 해당 Session을 조회해서 로그인 사용자를 확인합니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;여기서 서버 확장까지 질문한다면 이렇게 이어갈 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;애플리케이션 서버가 여러 대인데 각 서버의 메모리에 Session을 저장하면 다른 서버로 요청이 전달됐을 때 Session을 찾지 못할 수 있습니다. 이런 경우 Sticky Session을 사용하거나 Redis 같은 공용 Session Store를 사용하는 방법을 고려할 수 있습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;자주 나오는 꼬리 질문&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;HTTP는 Stateless한데 로그인 상태는 어떻게 유지하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Cookie나 Token처럼 사용자를 식별할 수 있는 정보를 요청마다 전달해서 애플리케이션 차원에서 인증 상태를 확인한다. Session 방식에서는 일반적으로 Session ID를 Cookie에 담아 전달한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session은 반드시 Cookie를 사용하나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;반드시 그런 것은 아니다. Session ID를 다른 방법으로 전달할 수도 있다. 다만 브라우저 기반 웹에서는 Cookie를 사용하는 방식이 일반적이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session을 Redis에 저장하는 이유는 무엇인가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여러 애플리케이션 서버가 동일한 Session을 공유할 수 있고 빠르게 조회할 수 있기 때문이다. 대신 Redis 장애와 메모리, Session 만료 정책도 같이 고려해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cookie와 Session 중 무엇이 더 안전한가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저장 위치만으로 어느 하나가 무조건 안전하다고 볼 수 없다. Session 방식에서도 Session ID가 탈취되면 문제가 발생한다. 인증 데이터의 설계와 HTTPS, HttpOnly, Secure, SameSite 같은 보안 설정을 함께 봐야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JWT가 Session보다 무조건 좋은가요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아니다. JWT는 Session Store 의존성을 줄일 수 있지만 Token 무효화, 만료, Refresh Token, 탈취 대응 같은 다른 복잡성이 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HttpOnly를 사용하면 XSS를 막을 수 있나요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아니다. HttpOnly는 JavaScript가 해당 Cookie에 접근하는 것을 제한해서 Cookie 탈취 위험을 줄이는 데 도움을 줄 뿐 XSS 자체를 막지는 않는다.&lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;Cookie와 Session의 차이는 단순히 저장 위치만 외우기보다 로그인 흐름 전체로 이해하는 게 좋다.&lt;/p&gt;
&lt;p&gt;세션 기반 인증은 다음과 같이 동작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;로그인 성공

↓

서버에서 Session 생성

↓

Session ID를 Cookie로 전달

↓

브라우저가 다음 요청에 Cookie 전송

↓

서버가 Session Store 조회

↓

로그인 사용자 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 서비스가 커지면 다음 단계의 문제가 생긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;서버가 여러 대라면?

→ 공용 Session Store 검토

Session ID를 어떻게 보호할까?

→ HttpOnly / Secure / SameSite 검토

서버에 인증 상태를 저장하고 싶지 않다면?

→ Token 기반 인증 검토&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;개발 면접에서는 &lt;code&gt;Cookie는 클라이언트, Session은 서버&lt;/code&gt;라는 한 문장만 답하는 것보다 &lt;strong&gt;HTTP의 Stateless 특성을 어떻게 보완하는지, Session ID가 어떤 역할을 하는지, 서버가 여러 대일 때 어떤 문제가 생기는지&lt;/strong&gt;까지 연결해서 설명할 수 있으면 좋다.&lt;/p&gt;
&lt;p&gt;백엔드 개발에서도 마찬가지다.&lt;/p&gt;
&lt;p&gt;인증은 로그인 API 하나를 만드는 것으로 끝나지 않는다. 서버 확장, Session Store, 만료 정책, 로그아웃, Cookie 보안까지 함께 고려해야 실제 서비스에서 안정적으로 사용할 수 있다.&lt;/p&gt;</description>
      <category>개발/개발면접</category>
      <category>Cookie</category>
      <category>HTTP</category>
      <category>redis</category>
      <category>Session</category>
      <category>SessionStore</category>
      <category>개발면접</category>
      <category>로그인</category>
      <category>백엔드개발</category>
      <category>웹백엔드</category>
      <category>인증</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/185</guid>
      <comments>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EB%A9%B4%EC%A0%91-%EC%BF%A0%ED%82%A4Cookie%EC%99%80-%EC%84%B8%EC%85%98Session%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9C%A0%EC%A7%80%EB%90%A0%EA%B9%8C#entry185comment</comments>
      <pubDate>Fri, 28 Aug 2026 14:51:13 +0900</pubDate>
    </item>
    <item>
      <title>Laravel Queue 모니터링에서 어떤 지표를 확인해야 할까?</title>
      <link>https://dev-lee-6.tistory.com/entry/Laravel-Queue-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%A4-%EC%A7%80%ED%91%9C%EB%A5%BC-%ED%99%95%EC%9D%B8%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue를 운영할 때 Job이 정상적으로 실행되고 있다는 것만 확인하면 충분해 보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Queue 장애는 꼭 Worker가 완전히 멈추는 형태로만 나타나지 않는다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Job은 처리되고 있음

&amp;darr;

그런데 대기 시간이 점점 길어짐

&amp;darr;

사용자는 결과를 늦게 받음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 실패한 Job은 많지 않은데 특정 Job의 실행 시간이 계속 증가할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제는 &lt;code&gt;failed_jobs&lt;/code&gt;만 확인해서는 발견하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 상태를 제대로 보려면 최소한 다음 질문에 답할 수 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;현재 Job이 얼마나 쌓여 있는가?

Job은 얼마나 오래 기다리고 있는가?

얼마나 빠르게 들어오고 처리되는가?

Job 하나를 처리하는 데 얼마나 걸리는가?

실패와 Retry가 증가하고 있지는 않은가?

Worker는 정상적으로 동작하고 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Laravel Queue를 운영할 때 어떤 지표를 확인하면 좋은지, 그리고 각 지표를 실제 장애 분석에 어떻게 활용할 수 있는지 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue Depth만 보면 충분할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue를 모니터링할 때 가장 먼저 떠올리기 쉬운 값은 대기 중인 Job 개수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 상태가 다음과 같다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;default

20건


notification

150건


export

5건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값을 보통 Queue Depth라고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Depth가 계속 증가한다면 Worker의 처리량보다 Job 유입량이 많을 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;Queue Depth 하나만으로 Queue가 정상인지 판단하기는 어렵다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job 1,000개가 있어도 정상일 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job 하나가 0.01초 정도 걸리고 Worker가 빠르게 처리하고 있다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순간적으로 1,000개의 Job이 들어와도&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1,000

&amp;darr;

700

&amp;darr;

300

&amp;darr;

0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 빠르게 감소한다면 큰 문제가 아닐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Queue에 30개밖에 없어도 각 Job이 5분씩 기다리고 있다면 사용자에게는 더 큰 문제가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Queue Depth를 볼 때는 다음 값도 같이 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue Depth

+

Queue Wait Time&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue Wait Time을 같이 확인하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Wait Time은 Job이 Queue에 등록된 후 실제 Worker에서 실행되기까지 기다린 시간이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Dispatch

&amp;darr;

[ Queue Wait Time ]

&amp;darr;

Worker 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10:00:00

Job 등록


10:00:02

Job 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 대기 시간은 약 2초다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10:00:00

Job 등록


10:05:00

Job 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 5분을 기다린 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 자체가 1초 만에 처리되더라도 사용자 입장에서는 5분 이상 걸린 작업이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue별 허용 대기 시간은 다를 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Queue에 같은 기준을 적용할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서비스 요구사항에 따라 다음처럼 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;notification

&amp;rarr; 빠르게 처리되어야 함


export

&amp;rarr; 어느 정도 대기 가능


batch

&amp;rarr; 사용자 요청과 직접 관계없다면 더 긴 대기 허용 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단순히&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Queue가 100건 이상이면 장애&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 기준보다 &lt;b&gt;업무 특성에 맞는 대기 시간 기준을 정하는 편이 더 의미 있다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유입량과 처리량을 함께 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Depth가 왜 증가하는지 알려면 Job이 들어오는 속도와 처리되는 속도를 비교해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Enqueue Rate

100 Job / 분


Processing Rate

70 Job / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매분 약 30개의 Job이 남는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10분

&amp;darr;

약 300개 누적


1시간

&amp;darr;

약 1,800개 누적&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 자체에는 아무 문제가 없어도 Queue는 계속 밀리게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반대 상황도 있다&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Enqueue Rate

100 Job / 분


Processing Rate

150 Job / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에는 처리 능력이 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이벤트나 Batch 때문에 순간적으로 3,000개의 Job이 들어왔다면 잠시 Queue Depth가 증가할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;3,000

&amp;darr;

2,000

&amp;darr;

800

&amp;darr;

0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Queue가 쌓였다는 사실보다 &lt;b&gt;정상적으로 해소되고 있는지&lt;/b&gt;가 더 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네 값을 같이 보면 상황이 잘 보인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 상태를 볼 때 다음 네 값을 같이 생각하면 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue Depth

&amp;rarr; 현재 얼마나 쌓였는가?


Enqueue Rate

&amp;rarr; 얼마나 빠르게 들어오는가?


Processing Rate

&amp;rarr; 얼마나 빠르게 처리하는가?


Queue Wait Time

&amp;rarr; 실제로 얼마나 기다리는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지를 같이 보면 단순히 &quot;Queue가 많다&quot;보다 훨씬 구체적으로 상태를 판단할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Job Runtime도 중요한 지표다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 밀리는 원인이 Worker 수가 아니라 Job 실행 시간 증가일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Worker가 5개이고 Job 하나가 평균 2초 걸렸다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 데이터가 증가하면서 평균 실행 시간이 10초로 늘었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수는 그대로인데 처리량은 크게 감소한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;기존

Job Runtime 2초


변경 후

Job Runtime 10초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서 Queue Depth만 보면&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Worker가 부족하다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고 판단하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 문제는 Job이 느려진 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;평균만 보면 이상 징후를 놓칠 수도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job 실행 시간이 다음과 같다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;대부분

1초


일부

3초


가끔

40초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균은 크게 이상하지 않아 보일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 40초짜리 Job이 Worker를 오래 점유하면서 뒤의 Job을 지연시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링 환경이 지원한다면 평균뿐 아니라 다음 값도 함께 보면 좋다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Average

P95

P99

Max&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 P95나 P99가 계속 증가한다면 일부 느린 Job이 Queue 전체에 영향을 주고 있는지 확인할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Job이 느려졌다면 Queue 밖의 병목도 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job Runtime 증가가 확인됐다면 Queue 설정만 볼 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 내부에서 어떤 작업이 시간을 사용하고 있는지 확인해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Database&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Job Runtime

30초


SQL 실행

27초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 Worker를 늘리는 것이 핵심 해결책이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 부분을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Slow Query

Index

N+1

DB Lock

조회 데이터 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 늘리면 느린 Query가 동시에 더 많이 실행되어 DB 부하가 커질 수도 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부 API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 사용하는 Job이라면 상대 서비스의 응답 시간도 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;평소

500ms


현재

8초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API 지연 때문에 Worker가 오래 점유될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 Queue Runtime과 함께 외부 API Response Time을 확인하면 원인을 찾기 쉬워진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실패율과 Retry도 모니터링해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 처리되고 있다는 것만으로 정상이라고 볼 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job이 계속 실패하고 Retry되고 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 외부 API 장애가 발생했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Job 실행

&amp;darr;

실패

&amp;darr;

Retry

&amp;darr;

다시 실패

&amp;darr;

Retry&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 하나가 여러 번 처리되면서 Queue 처리량을 소비한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 새로운 Job도 계속 들어온다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;새로운 Job

+

Retry Job

&amp;darr;

Queue 증가&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;failed_jobs만 보는 것으로 부족할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel의 &lt;code&gt;failed_jobs&lt;/code&gt;는 최종적으로 실패한 Job을 확인할 때 유용하다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;php artisan queue:failed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Retry 중인 Job은 아직 최종 실패 상태가 아닐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 가능하다면 다음 값을 같이 확인하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Job Attempts

Retry 발생량

최종 실패량

실패 원인&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실패율이 갑자기 증가했다면 원인을 분류한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 특정 시점부터 Notification Job 실패가 증가했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;Application Exception?

&amp;darr;

External API?

&amp;darr;

Timeout?

&amp;darr;

Rate Limit?

&amp;darr;

잘못된 Payload?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 실패 건수보다 &lt;b&gt;어떤 이유로 실패하는지 분류할 수 있어야 운영에서 더 유용하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker 상태와 리소스도 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 지표가 이상하다면 Worker 프로세스 자체도 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supervisor를 사용한다면 다음처럼 상태를 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo supervisorctl status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Worker 4개를 운영한다고 생각했는데 실제로는 다음과 같을 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;worker_00    RUNNING
worker_01    RUNNING
worker_02    FATAL
worker_03    EXITED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 Worker 두 개만 정상 동작하고 있는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker 수만 확인하면 끝은 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 &lt;code&gt;RUNNING&lt;/code&gt;이어도 서버 리소스가 부족할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 확인할 수 있는 값은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CPU

Memory

Swap

DB Connection

Redis Connection&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Worker를 늘린 이후 Queue 처리량은 증가했지만 DB CPU가 100%에 가까워졌다면 좋은 확장이라고 보기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 처리 성능 때문에 웹 API까지 느려질 수 있기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker 메모리 증가도 관찰할 필요가 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Worker는 장시간 실행되는 프로세스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 처리 과정에서 사용한 메모리가 예상대로 정리되지 않거나 특정 라이브러리와 처리 방식 때문에 Worker 메모리가 점점 증가하는 상황도 발생할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 시작

100MB


몇 시간 후

300MB


며칠 후

800MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황이 반복되면 서버 메모리 부족으로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Worker 운영에서는 필요에 따라 &lt;code&gt;--memory&lt;/code&gt;, &lt;code&gt;--max-jobs&lt;/code&gt;, &lt;code&gt;--max-time&lt;/code&gt; 같은 옵션을 활용해 Worker를 주기적으로 교체하는 방법도 검토할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;php artisan queue:work \
    --memory=256 \
    --max-jobs=1000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 운영 정책을 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 값을 무작정 작게 설정하기보다 실제 Worker의 메모리 사용 패턴과 Job 특성을 먼저 확인하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue별로 지표를 분리해서 보는 것이 좋다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue를 다음처럼 나눠 운영한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;default

notification

export

batch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 Queue Job 수만 보면 문제가 잘 드러나지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;전체 대기

1,000건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 하면 어떤 Queue가 문제인지 알기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue별로 보면 상황이 명확해진다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;default

5건


notification

3건


export

12건


batch

980건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch에만 Job이 몰린 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음 상황이라면 우선순위가 달라진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;notification

500건


가장 오래된 Job

7분 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 직접 영향을 줄 가능성이 있기 때문에 더 빠르게 확인해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue마다 기준도 다르게 잡는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 운영 정책을 개념적으로 다음처럼 가져갈 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;notification

대기 시간 중심


export

대기 시간 + Runtime 중심


batch

처리량 + DB 부하 중심&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Queue를 같은 숫자로 평가하기보다 업무 특성을 반영하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Database Queue라면 무엇을 확인할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Database Queue를 사용한다면 &lt;code&gt;jobs&lt;/code&gt; 테이블에서도 기본적인 상태를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 다음과 같은 정보가 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;queue

attempts

reserved_at

available_at

created_at&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Queue별 대기 건수를 확인하려면 환경과 스키마에 맞게 다음과 비슷한 Query를 활용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT queue, COUNT(*) AS job_count
FROM jobs
GROUP BY queue;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 특정 이름에 집중되어 있는지 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오래된 Job도 찾아볼 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;created_at&lt;/code&gt;이 Unix Timestamp 형태로 저장된 기본 Queue 테이블이라면 환경에 맞게 오래 대기 중인 Job을 확인하는 Query를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    id,
    queue,
    attempts,
    FROM_UNIXTIME(created_at) AS created_at
FROM jobs
ORDER BY created_at ASC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;code&gt;created_at&lt;/code&gt;만으로 항상 정확한 &quot;실제 실행 가능 상태에서의 대기 시간&quot;을 표현할 수 있는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 Job이나 Retry처럼 &lt;code&gt;available_at&lt;/code&gt;의 영향을 받는 경우가 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Queue 테이블을 직접 모니터링한다면 각 컬럼의 의미를 구분해서 보는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Queue라면 Laravel Horizon을 검토할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 Queue Driver로 사용한다면 Laravel Horizon을 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Horizon은 Redis Queue를 운영할 때 다음과 같은 정보를 관찰하고 Worker를 관리하는 데 도움을 준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Job 처리량

Runtime

실패 Job

Queue 상태

Worker / Supervisor 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 종류가 많아지고 Worker 운영이 복잡해진다면 직접 로그와 서버 상태만 확인하는 것보다 관리가 편해질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Horizon을 도입한다고 Queue 문제가 자동으로 해결되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 어떤 지표를 보고 어떤 상태를 이상으로 판단할지 정하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Horizon 도입

&amp;ne;

Queue 운영 문제 해결&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Horizon은 Queue를 관찰하고 관리하기 위한 도구이고, Slow Query나 외부 API 장애 같은 실제 병목은 별도로 해결해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 상황에 알림을 걸면 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링은 Dashboard를 만들어두는 것만으로 끝나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애가 발생했는데 누군가 Dashboard를 열어볼 때까지 모른다면 대응이 늦어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 중요한 지표에는 알림을 검토할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue 대기 시간 증가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Notification Queue가 일정 시간 이상 지연되는 상황이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Notification Queue

&amp;darr;

가장 오래된 Job의 대기 시간이 기준 초과

&amp;darr;

알림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 직접 영향을 주는 Queue라면 단순 Job 개수보다 대기 시간이 좋은 신호가 될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue Depth 지속 증가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순간적인 Spike에는 알림을 보내지 않고 일정 시간 이상 계속 증가할 때 알림을 보내는 방식도 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;일시적인 1,000건

&amp;darr;

빠르게 감소

&amp;darr;

정상


30분 동안 계속 증가

&amp;darr;

확인 필요&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실패율 증가&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;평소 실패 거의 없음

&amp;darr;

최근 5분 실패 급증

&amp;darr;

알림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API 장애나 배포 오류를 빠르게 발견하는 데 도움이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker 비정상 종료&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;기대 Worker

5개


현재 정상 Worker

2개

&amp;darr;

알림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Depth가 크게 증가하기 전에 Worker 장애를 먼저 발견할 수도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모니터링 지표를 장애 원인과 연결해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지표는 많이 수집하는 것보다 서로 연결해서 해석할 수 있는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 상황을 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue Depth 증가

+

Queue Wait Time 증가

+

Job Runtime 정상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 자체는 정상인데 처리 능력이 부족할 가능성이 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Worker 상태

Job 유입량

Worker 수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 먼저 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음과 같다면 이야기가 달라진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue Depth 증가

+

Queue Wait Time 증가

+

Job Runtime 증가

+

DB CPU 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 부족보다 DB가 병목일 가능성을 먼저 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 상황이다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Queue Depth 증가

+

Retry 증가

+

외부 API 응답 시간 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 서비스 장애가 Queue 적체로 이어지고 있을 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;하나의 지표보다 여러 지표의 변화를 함께 보는 것이 원인 분석에 훨씬 유용하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음부터 모든 지표를 수집할 필요는 없다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 모니터링을 시작한다고 해서 처음부터 복잡한 Dashboard를 만들 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 규모가 크지 않다면 우선 다음 정도부터 확인해도 충분하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue Depth

Queue Wait Time

Job Runtime

Failed / Retry

Worker 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제 운영에서 문제가 생기면서 필요한 지표를 추가할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 DB가 자주 병목이라면&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DB CPU

DB Connection

Slow Query&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API Job이 많다면&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;API Response Time

Timeout

Rate Limit

API Error Rate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 같이 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 모니터링의 목적은 지표를 많이 만드는 것이 아니라 &lt;b&gt;문제가 발생했을 때 어디부터 확인해야 하는지 빠르게 판단할 수 있게 만드는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;운영에서는 이 정도를 먼저 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue 모니터링을 처음 구성한다면 다음 정도를 핵심 지표로 볼 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;확인하려는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queue Depth&lt;/td&gt;
&lt;td&gt;현재 대기 중인 Job이 얼마나 많은가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue Wait Time&lt;/td&gt;
&lt;td&gt;Job이 실행되기까지 얼마나 기다리는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enqueue Rate&lt;/td&gt;
&lt;td&gt;Job이 얼마나 빠르게 들어오는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing Rate&lt;/td&gt;
&lt;td&gt;Worker가 얼마나 빠르게 처리하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job Runtime&lt;/td&gt;
&lt;td&gt;Job 하나의 실행 시간이 얼마나 걸리는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry&lt;/td&gt;
&lt;td&gt;반복적으로 다시 실행되는 Job이 있는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed Job&lt;/td&gt;
&lt;td&gt;최종 실패가 증가하고 있는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker 상태&lt;/td&gt;
&lt;td&gt;필요한 Worker가 정상 동작하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU / Memory&lt;/td&gt;
&lt;td&gt;Worker를 실행할 서버에 여유가 있는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB / 외부 API&lt;/td&gt;
&lt;td&gt;실제 병목이 Queue 밖에 있지는 않은가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 값을 반드시 하나의 도구에서 확인할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 장애가 발생했을 때 이 질문들에 답할 수 있는 상태를 만드는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue 모니터링에서 가장 흔하게 보기 쉬운 값은 Queue에 쌓인 Job 개수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Queue Depth만으로는 Queue가 정상인지 판단하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소한 다음 흐름을 함께 보는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;얼마나 들어오는가?

&amp;rarr; Enqueue Rate


얼마나 쌓였는가?

&amp;rarr; Queue Depth


얼마나 기다리는가?

&amp;rarr; Queue Wait Time


얼마나 빠르게 처리하는가?

&amp;rarr; Processing Rate


Job 하나는 얼마나 걸리는가?

&amp;rarr; Job Runtime


실패가 반복되고 있는가?

&amp;rarr; Retry / Failed Job&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Worker 상태와 CPU, Memory, DB, 외부 API 상태를 연결하면 Queue가 밀렸을 때 원인을 훨씬 구체적으로 좁힐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 운영에서는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Queue가 1,000건 쌓였다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 사실보다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;왜 1,000건이 쌓였는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 설명할 수 있는 것이 더 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 유입이 갑자기 증가한 것인지, Worker가 줄어든 것인지, Job 실행 시간이 길어진 것인지, 외부 API 장애 때문에 Retry가 증가한 것인지에 따라 해결 방법은 모두 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Queue 모니터링의 목표는 단순히 Dashboard에 숫자를 많이 보여주는 것이 아니라 &lt;b&gt;Queue의 대기부터 실제 처리, 실패까지 하나의 흐름으로 추적할 수 있게 만드는 것&lt;/b&gt;이라고 생각하면 좋다.&lt;/p&gt;</description>
      <category>개발/Laravel</category>
      <category>backend</category>
      <category>Laravel</category>
      <category>LaravelHorizon</category>
      <category>LaravelQueue</category>
      <category>php</category>
      <category>Queue</category>
      <category>QueueWorker</category>
      <category>Queue모니터링</category>
      <category>redis</category>
      <category>supervisor</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/184</guid>
      <comments>https://dev-lee-6.tistory.com/entry/Laravel-Queue-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%A4-%EC%A7%80%ED%91%9C%EB%A5%BC-%ED%99%95%EC%9D%B8%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C#entry184comment</comments>
      <pubDate>Thu, 27 Aug 2026 14:21:11 +0900</pubDate>
    </item>
    <item>
      <title>Laravel Queue 대기 시간이 길어질 때 무엇부터 확인해야 할까?</title>
      <link>https://dev-lee-6.tistory.com/entry/Laravel-Queue-%EB%8C%80%EA%B8%B0-%EC%8B%9C%EA%B0%84%EC%9D%B4-%EA%B8%B8%EC%96%B4%EC%A7%88-%EB%95%8C-%EB%AC%B4%EC%97%87%EB%B6%80%ED%84%B0-%ED%99%95%EC%9D%B8%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue를 운영하다 보면 Job 자체는 몇 초 만에 끝나는데 사용자가 결과를 받기까지는 몇 분씩 걸리는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 처음에는 Worker 수가 부족하다고 생각하기 쉽다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Queue가 느림

&amp;darr;

Worker 부족?

&amp;darr;

Worker 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Worker가 부족한 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Queue가 느려지는 원인은 생각보다 다양하다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Job 유입량 증가

Worker 부족

특정 Job 실행 시간 증가

Slow Query

외부 API 지연

Retry 반복

무거운 Job과 가벼운 Job이 같은 Queue를 사용

Worker 프로세스 이상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 확인하지 않고 Worker만 늘리면 DB 부하가 더 커지거나 외부 API 요청이 동시에 증가하면서 오히려 상황이 나빠질 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Laravel Queue가 밀릴 때는 단순히 대기 Job 개수만 보는 것이 아니라 &lt;b&gt;Job이 들어오는 속도, 실제 처리 속도, 대기 시간, Worker 상태를 함께 확인해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Laravel Queue 대기 시간이 길어졌을 때 어떤 순서로 원인을 확인하면 좋은지 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue가 느리다는 것을 먼저 구분해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 느리다고 할 때 실제로는 두 가지 시간이 섞여 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Job Dispatch

&amp;darr;

[ Queue 대기 시간 ]

&amp;darr;

Worker 실행 시작

&amp;darr;

[ Job 실행 시간 ]

&amp;darr;

처리 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job을 12시 정각에 등록했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;12:00:00

Job 등록


12:02:00

Worker 실행 시작


12:02:03

Job 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적으로는 결과를 받기까지 2분 3초가 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Job 자체의 실행 시간은 3초다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 2분 동안 Queue에서 기다렸다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음 상황도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;12:00:00

Job 등록


12:00:01

Worker 실행


12:02:01

Job 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Queue 대기 시간은 1초밖에 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 Job 자체가 2분 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 사용자 입장에서는 &quot;Queue가 느리다&quot;고 느낄 수 있지만 해결 방법은 완전히 다르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue 대기 시간이 긴 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 항목을 우선 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Worker 수

Job 유입량

Queue 분리

Retry

Worker 장애&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job 실행 시간이 긴 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 항목을 먼저 확인하는 편이 좋다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Slow Query

외부 API

대량 데이터 처리

N+1

DB Lock

메모리

Job 구조&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 가장 먼저 해야 할 일은 &lt;b&gt;대기 시간이 긴 것인지 실행 시간이 긴 것인지 구분하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Job 유입량과 처리량을 비교해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 지속적으로 쌓이는 가장 기본적인 이유는 들어오는 Job보다 처리하는 Job이 적기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1분 동안 다음과 같이 Job이 들어온다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Job 유입

100건 / 분


Worker 처리

60건 / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매분 40개의 Job이 남는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1분 후

40건


10분 후

400건


1시간 후

2,400건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 장애가 없어도 Queue는 계속 밀리게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음과 같다면 상황이 다르다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Job 유입

60건 / 분


Worker 처리

100건 / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순간적으로 500건이 들어오더라도 시간이 지나면서 Queue가 다시 줄어들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순간적인 증가와 지속적인 적체는 다르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 새벽 Batch가 실행되면서 한 번에 5,000개의 Job이 등록됐다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;02:00

5,000건


02:05

3,200건


02:10

1,500건


02:15

0건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 많아 보이지만 정상적으로 줄어들고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;02:00

5,000건


02:05

5,500건


02:10

6,300건


02:15

7,100건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 현재 처리 능력이 유입량을 따라가지 못하고 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 현재 Queue에 몇 건이 있는지만 보는 것보다 &lt;b&gt;시간에 따라 증가하는지 감소하는지를 보는 것이 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker가 실제로 정상 동작하는지 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 갑자기 밀리기 시작했다면 Worker 수를 조정하기 전에 Worker 프로세스부터 확인하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supervisor를 사용한다면 다음처럼 상태를 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo supervisorctl status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 정상이라면 Worker가 &lt;code&gt;RUNNING&lt;/code&gt; 상태여야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;laravel-worker_00    RUNNING
laravel-worker_01    RUNNING&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 있다면 다음과 같은 상태를 볼 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;STOPPED

FATAL

BACKOFF

EXITED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue에 Job이 아무리 정상적으로 등록되어도 Worker가 실행되고 있지 않으면 Job은 처리되지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포 이후 Worker도 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel의 &lt;code&gt;queue:work&lt;/code&gt;는 장시간 실행되는 프로세스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포했다고 해서 실행 중인 Worker가 자동으로 새로운 애플리케이션 코드를 다시 읽는다고 생각하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 과정에서는 환경에 맞게 Worker 재시작도 관리해야 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;php artisan queue:restart&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supervisor가 Worker를 관리하고 있다면 종료된 Worker를 다시 실행하는 구조로 운영할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 장애를 볼 때 애플리케이션 로그만 확인하기보다 &lt;b&gt;실제로 어떤 Worker가 몇 개 실행 중인지&lt;/b&gt;도 같이 확인하는 편이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특정 Job이 Worker를 오래 점유하고 있지 않은지 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker는 한 번에 하나의 Job을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 2개 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker A

&amp;rarr; GenerateExcelJob
&amp;rarr; 3분


Worker B

&amp;rarr; GenerateExcelJob
&amp;rarr; 3분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 Worker가 모두 대용량 엑셀 생성에 사용되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤에 다음 Job이 들어온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SendSmsJob

실행 시간 1초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 자체는 1초면 끝나지만 실행할 Worker가 없다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GenerateExcelJob ─ Worker A

GenerateExcelJob ─ Worker B


SendSmsJob

&amp;darr;

Queue 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 가벼운 Job도 앞의 무거운 Job 때문에 늦어진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue를 목적별로 분리할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제가 반복된다면 Queue를 분리하는 방법을 검토할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;notification

&amp;rarr; SMS
&amp;rarr; Push
&amp;rarr; Email


export

&amp;rarr; Excel
&amp;rarr; CSV


batch

&amp;rarr; 통계
&amp;rarr; 대량 동기화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Worker도 별도로 운영한다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;Notification Worker

&amp;darr;

notification


Export Worker

&amp;darr;

export&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 대용량 엑셀 작업이 쌓여도 알림 Worker까지 점유하는 문제를 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue를 어떻게 분리할지는 이전 글에서 별도로 다뤘으므로, 현재 글에서는 &lt;b&gt;실행 시간이 크게 다른 Job이 같은 Worker를 경쟁하고 있는지 확인한다&lt;/b&gt;는 점만 기억하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Job 실행 시간이 갑자기 늘어난 것은 아닌지 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수가 그대로인데 Queue가 갑자기 밀리기 시작했다면 Job 하나의 실행 시간이 증가했을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 평소에는 다음과 같았다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;평균 Job 실행 시간

2초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 하나는 단순 계산으로 분당 약 30개의 Job을 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 데이터 증가로 실행 시간이 10초가 됐다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;평균 Job 실행 시간

10초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Worker 하나가 분당 약 6개밖에 처리하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 개수는 그대로지만 전체 처리량이 크게 줄어든 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Job이 느려졌는지 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Job 실행 시간이 길어지는 원인은 여러 가지가 있다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;데이터 증가

Slow Query

N+1

외부 API 응답 지연

DB Lock 대기

처리 로직 증가

파일 생성

네트워크 I/O&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job 전체가 30초 걸리는데 SQL 하나가 25초를 차지한다면 Worker를 늘리는 것보다 Query를 개선하는 것이 먼저일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Queue 처리량 부족

&amp;darr;

Worker 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 생각하지 말고&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Queue 처리량 부족

&amp;darr;

Job 실행 시간 확인

&amp;darr;

병목 확인

&amp;darr;

Query / 로직 개선&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 같이 생각해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Retry가 Queue를 더 밀리게 만들 수도 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패한 Job이 계속 Retry되고 있다면 새로운 Job과 Retry Job이 함께 Queue에 들어오면서 적체가 심해질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 외부 API에 장애가 발생했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Job 100건

&amp;darr;

외부 API 실패

&amp;darr;

Retry


새로운 Job 100건

+

Retry Job&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애가 계속되면 Queue에는 새로운 요청뿐 아니라 이전에 실패한 작업까지 다시 들어온다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tries와 backoff를 같이 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job에서 다음처럼 Retry 정책을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;class SyncProductJob implements ShouldQueue
{
    public $tries = 3;

    public function backoff(): array
    {
        return [10, 30, 60];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일시적인 장애라면 Retry가 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 요청할 때마다 항상 실패하는 구조적인 문제라면 Retry는 Queue 부하만 늘릴 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;잘못된 데이터

&amp;darr;

실패

&amp;darr;

Retry

&amp;darr;

동일한 이유로 실패

&amp;darr;

Retry&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 Retry 횟수를 늘리는 것보다 실패 원인을 수정해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;failed_jobs도 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 이상하게 동작한다면 실패 Job도 같이 확인하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;php artisan queue:failed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 Job이 반복적으로 실패하는지 확인하면 Queue 적체 원인을 찾는 데 도움이 될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;timeout과 retry_after도 같이 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 실행 시간이 길다면 &lt;code&gt;timeout&lt;/code&gt;과 &lt;code&gt;retry_after&lt;/code&gt; 관계도 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 설정되어 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;timeout

120초


retry_after

60초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 Worker는 최대 120초 동안 Job을 실행할 수 있는데 Queue에서는 더 일찍 해당 Job을 다시 처리 가능한 상태로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경과 Queue Driver 설정에 따라 이런 잘못된 관계는 동일한 Job이 예상치 않게 다시 처리되는 문제로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는 다음 관계를 유지하는 것이 중요하다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;timeout &amp;lt; retry_after&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;timeout&lt;/code&gt;과 &lt;code&gt;retry_after&lt;/code&gt;의 구체적인 역할은 이전 글에서 자세히 다뤘으므로 여기서는 &lt;b&gt;긴 Job이 있다면 실행 시간과 재시도 관련 설정도 같이 확인해야 한다&lt;/b&gt;는 정도로 정리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker를 늘리기 전에 병목이 어디인지 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 밀릴 때 가장 간단한 해결 방법은 Worker를 늘리는 것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 2

&amp;darr;

Worker 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Worker 부족이 원인이라면 효과가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Job이 MySQL을 많이 사용하는 경우에는 이야기가 달라진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 1 ─┐
Worker 2 ─┤
Worker 3 ─┤
Worker 4 ─┼&amp;rarr; MySQL
Worker 5 ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 늘린 만큼 동시에 실행되는 Query도 늘어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DB CPU 증가

Connection 증가

Lock 경쟁 증가

Query 실행 시간 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 처리 속도를 높이려다가 웹 API까지 느려질 수도 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부 API도 같은 문제가 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자나 외부 데이터 동기화 Job이라면 외부 API의 Rate Limit도 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Worker 증가

&amp;darr;

동시 API 요청 증가

&amp;darr;

Rate Limit 초과

&amp;darr;

실패

&amp;darr;

Retry

&amp;darr;

Queue 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 증가가 오히려 악순환을 만들 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수를 어떻게 정할지는 이전 글에서 다뤘듯이 Job 처리 시간과 서버 리소스, DB, 외부 시스템의 제한을 함께 보는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue를 모니터링할 때 어떤 값을 보면 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 장애가 발생한 뒤 로그를 확인하는 것도 중요하지만 평소에 몇 가지 값을 관찰할 수 있다면 문제를 더 빨리 찾을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue Depth&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Queue에서 대기 중인 Job 수다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;default

120건


notification

3건


export

15건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 어디에서 밀리고 있는지 파악하는 데 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Queue Depth만으로 정상 여부를 판단하기는 어렵다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue 대기 시간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 대기 Job 개수와 함께 중요하게 볼 값이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Queue Depth

100건


가장 오래된 Job

2초 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Queue Depth

100건


가장 오래된 Job

10분 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 완전히 다른 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 사용자에게 직접 영향을 주는 Queue라면 허용 가능한 대기 시간을 정해두는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job 실행 시간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 실행 시간이 계속 증가하고 있는지도 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;지난달

평균 2초


이번 달

평균 4초


현재

평균 8초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 증가나 Query 성능 저하가 진행되고 있을 가능성이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실패율&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 Queue의 실패율이 갑자기 증가한다면 외부 API나 데이터 문제를 의심할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;notification

평소 실패율 낮음

&amp;darr;

갑자기 실패 증가

&amp;darr;

외부 발송 API 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 Queue가 얼마나 쌓였는지보다 &lt;b&gt;왜 처리되지 않는지 알 수 있는 지표&lt;/b&gt;를 같이 보는 것이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그에는 대기 시간과 실행 시간을 구분해서 남기는 것이 좋다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 문제를 분석하려면 언제 Dispatch됐고 언제 실행됐는지 알 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무 특성에 따라 Job 생성 시점을 Payload에 포함하거나 별도의 작업 테이블을 두는 방법 등을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Export 요청을 별도 테이블로 관리한다면 다음 값을 저장할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;requested_at

started_at

completed_at&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다음 값을 계산할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Queue Wait Time

=

started_at - requested_at


Job Runtime

=

completed_at - started_at&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;엑셀 생성에 3분 걸렸다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니라&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Queue 대기

2분 50초


실제 생성

10초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 사실을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 측정할 수 있어야 해결 방향도 정확해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Laravel Queue가 밀릴 때는 이 순서로 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 적체가 발생하면 한 번에 모든 것을 확인하기보다 범위를 좁혀가는 편이 좋다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. Queue가 실제로 계속 증가하고 있는가?

&amp;darr;

2. 대기 시간이 긴가, Job 실행 시간이 긴가?

&amp;darr;

3. Worker가 정상적으로 실행 중인가?

&amp;darr;

4. Job 유입량이 갑자기 증가했는가?

&amp;darr;

5. 특정 Job이 Worker를 오래 점유하고 있는가?

&amp;darr;

6. Job 실행 시간이 이전보다 증가했는가?

&amp;darr;

7. Slow Query나 외부 API 지연이 있는가?

&amp;darr;

8. 실패와 Retry가 반복되고 있는가?

&amp;darr;

9. Queue를 분리해야 하는가?

&amp;darr;

10. 그래도 처리량이 부족하다면 Worker 증가 검토&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서로 보면 처음부터 Worker 수만 늘리는 것보다 원인을 찾기가 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;운영 환경에서 확인할 핵심 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue 대기 시간이 갑자기 길어졌다면 다음 정도는 확인해볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;□ Queue 대기 Job이 지속적으로 증가하고 있는가?

□ Queue 대기 시간과 Job 실행 시간을 구분했는가?

□ Worker가 모두 정상적으로 실행 중인가?

□ 배포 이후 Worker가 정상적으로 재시작됐는가?

□ Job 유입량이 평소보다 증가했는가?

□ 특정 Job이 Worker를 오래 점유하고 있지 않은가?

□ Job 실행 시간이 이전보다 증가하지 않았는가?

□ Slow Query나 DB Lock이 발생하고 있지 않은가?

□ 외부 API 응답이 느려지지 않았는가?

□ 실패한 Job이 계속 Retry되고 있지 않은가?

□ 무거운 Job과 빠른 Job을 Queue로 분리할 필요가 있는가?

□ Worker를 늘렸을 때 DB나 외부 시스템이 감당할 수 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Queue 문제를 이 체크리스트만으로 해결할 수 있는 것은 아니지만, 원인을 찾기 위한 출발점으로 활용하기 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue가 밀린다고 해서 바로 Worker부터 늘릴 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 구분해야 하는 것은 다음 두 가지다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Queue에서 오래 기다리는가?

vs

Job 자체가 오래 실행되는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 대기가 문제라면&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Job 유입량

Worker 상태

Worker 수

Queue 분리

Retry&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 실행 시간이 문제라면&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Slow Query

N+1

외부 API

DB Lock

대량 데이터 처리

Job 구조&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 먼저 살펴보는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음에도 처리량이 부족하다면 Worker 증가를 검토한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 중요한 것은&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Queue가 몇 개 쌓였는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 보는 것이 아니라&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;얼마나 빠르게 들어오고 있는가?

얼마나 빠르게 처리하고 있는가?

얼마나 오래 기다리고 있는가?

Job 하나는 실제로 얼마나 오래 실행되는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 함께 보는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지를 구분해서 볼 수 있으면 Laravel Queue가 느려졌을 때 단순히 Worker를 늘리는 방식에서 벗어나 실제 병목이 어디에 있는지 훨씬 빠르게 판단할 수 있다.&lt;/p&gt;</description>
      <category>개발/Laravel</category>
      <category>backend</category>
      <category>Laravel</category>
      <category>LaravelQueue</category>
      <category>php</category>
      <category>Queue</category>
      <category>QueueWorker</category>
      <category>Queue모니터링</category>
      <category>supervisor</category>
      <category>비동기처리</category>
      <category>성능최적화</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/183</guid>
      <comments>https://dev-lee-6.tistory.com/entry/Laravel-Queue-%EB%8C%80%EA%B8%B0-%EC%8B%9C%EA%B0%84%EC%9D%B4-%EA%B8%B8%EC%96%B4%EC%A7%88-%EB%95%8C-%EB%AC%B4%EC%97%87%EB%B6%80%ED%84%B0-%ED%99%95%EC%9D%B8%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C#entry183comment</comments>
      <pubDate>Wed, 26 Aug 2026 15:54:38 +0900</pubDate>
    </item>
    <item>
      <title>메모리 계층 구조란? Register부터 SSD까지 속도가 다른 이유</title>
      <link>https://dev-lee-6.tistory.com/entry/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0%EB%9E%80-Register%EB%B6%80%ED%84%B0-SSD%EA%B9%8C%EC%A7%80-%EC%86%8D%EB%8F%84%EA%B0%80-%EB%8B%A4%EB%A5%B8-%EC%9D%B4%EC%9C%A0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 구조를 공부하다 보면 메모리가 하나의 공간처럼 느껴지기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Register&lt;/li&gt;
&lt;li&gt;L1 Cache&lt;/li&gt;
&lt;li&gt;L2 Cache&lt;/li&gt;
&lt;li&gt;L3 Cache&lt;/li&gt;
&lt;li&gt;RAM&lt;/li&gt;
&lt;li&gt;SSD&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 여러 단계의 저장 공간을 함께 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 저장 공간들은 속도, 용량, 가격이 모두 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 빠른 저장 공간만 사용하면 좋을 것 같지만 현실적으로는 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 저장 공간은 비싸고 용량이 작기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컴퓨터는 &lt;b&gt;빠른 공간은 작게, 느린 공간은 크게&lt;/b&gt; 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 &lt;b&gt;Memory Hierarchy(메모리 계층 구조)&lt;/b&gt; 라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 메모리 계층 구조가 왜 필요한지, 각 계층은 어떤 역할을 하는지, 그리고 백엔드 개발자 입장에서 왜 알아두면 좋은지 정리해보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 계층 구조란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 계층 구조는 데이터를 저장하는 공간을 속도와 용량에 따라 여러 단계로 나눈 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;빠름
&amp;uarr;

Register

L1 Cache

L2 Cache

L3 Cache

RAM

SSD

&amp;darr;

느림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 용량은 보통 아래로 갈수록 커진다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;작음
&amp;uarr;

Register

Cache

RAM

SSD

&amp;darr;

큼&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;빠른 대신 작음

느린 대신 큼&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 Trade-off가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 하나의 메모리만 사용하지 않을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 빠른 메모리만 사용하면 성능은 좋을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 모든 데이터를 Register에 저장한다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Register가 매우 비싸고 용량이 극도로 작다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 SSD는 매우 큰 용량을 저렴하게 사용할 수 있지만 CPU가 직접 연산하기에는 너무 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 계층을 나눈다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;CPU가 바로 필요한 데이터
&amp;rarr; Register

곧 다시 사용할 데이터
&amp;rarr; Cache

실행 중인 프로그램 데이터
&amp;rarr; RAM

장기 저장 데이터
&amp;rarr; SSD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 데이터를 상황에 맞는 계층에 두는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Register란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Register는 CPU 내부에 있는 가장 빠른 저장 공간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 실제 연산을 수행할 때 직접 사용하는 값들이 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;10 + 20&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 계산한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10 &amp;rarr; Register

20 &amp;rarr; Register

&amp;darr;

CPU 연산

&amp;darr;

30&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Register는 CPU 내부에 있기 때문에 접근 속도가 매우 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 용량은 매우 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 모든 데이터를 Register에 저장할 수는 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU Cache란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Register 다음에는 CPU Cache가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;L1

L2

L3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 RAM에서 매번 데이터를 가져오면 느리기 때문에 자주 사용하는 데이터를 가까운 Cache에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;CPU

&amp;darr;

L1

&amp;darr;

L2

&amp;darr;

L3

&amp;darr;

RAM&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 데이터를 요청하면 가장 가까운 Cache부터 확인한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;L1 Hit
&amp;rarr; 바로 사용

L1 Miss
&amp;rarr; L2 확인

L2 Miss
&amp;rarr; L3 확인

L3 Miss
&amp;rarr; RAM 접근&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 단계에서 찾을수록 빠르다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L1 Cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L1 Cache는 CPU Core와 가장 가까운 Cache다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;CPU Core

&amp;darr;

L1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 빠르지만 용량도 가장 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 반복해서 사용하는 데이터나 명령어를 빠르게 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 Instruction Cache와 Data Cache를 분리해서 사용하는 구조도 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;L1 Instruction Cache

L1 Data Cache&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 구조마다 세부 구현은 다르지만 중요한 점은 &lt;b&gt;가장 빠르고 가장 작은 Cache&lt;/b&gt;라는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L2 Cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L2 Cache는 L1보다 조금 느리지만 더 큰 용량을 가진다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;L1 Miss

&amp;darr;

L2 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L1에서 찾지 못한 데이터를 L2에서 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 입장에서는 RAM까지 가지 않고 L2에서 데이터를 찾는 것만으로도 큰 이득이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L3 Cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L3 Cache는 일반적으로 L1, L2보다 더 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 CPU Core가 공유하는 형태로 구성되는 경우도 많다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Core 1 ─┐
Core 2 ─┼&amp;rarr; L3
Core 3 ─┤
Core 4 ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core 각각의 Cache에서 데이터를 찾지 못했을 때 더 큰 L3 Cache를 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 CPU 아키텍처마다 구조는 다를 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAM은 어떤 역할을 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM은 현재 실행 중인 프로그램과 데이터를 저장하는 주 메모리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 브라우저&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Laravel Worker&lt;/li&gt;
&lt;li&gt;Python Application&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이 실행되면 관련 데이터가 RAM에 올라온다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;SSD

&amp;darr;

프로그램 실행

&amp;darr;

RAM 적재

&amp;darr;

CPU 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 SSD에서 프로그램을 직접 실행하는 것이 아니라 필요한 데이터를 RAM에 올려 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RAM도 CPU 입장에서는 느리다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM은 SSD보다 훨씬 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 CPU는 더 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 CPU 관점에서는 RAM조차 느린 저장 공간이 될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;CPU 연산 속도

&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;

RAM 접근 속도&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 RAM의 데이터를 기다리면 연산을 하지 못하고 대기할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 CPU Cache가 필요한 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSD는 왜 필요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM은 빠르지만 전원이 꺼지면 데이터가 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;휘발성 메모리&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 SSD는 전원이 꺼져도 데이터를 유지한다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;RAM

전원 OFF
&amp;rarr; 데이터 사라짐


SSD

전원 OFF
&amp;rarr; 데이터 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Database 파일&lt;/li&gt;
&lt;li&gt;이미지&lt;/li&gt;
&lt;li&gt;로그&lt;/li&gt;
&lt;li&gt;프로그램&lt;/li&gt;
&lt;li&gt;운영체제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 데이터를 SSD에 저장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HDD는 어디에 들어갈까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSD보다 더 느린 저장 장치로 HDD도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적인 계층을 조금 더 확장하면&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;Register

&amp;darr;

L1

&amp;darr;

L2

&amp;darr;

L3

&amp;darr;

RAM

&amp;darr;

SSD

&amp;darr;

HDD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 볼 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HDD는 SSD보다 느리지만 대용량 저장 비용이 낮다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백업이나 대용량 저장 용도로 여전히 사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;속도 차이는 왜 이렇게 클까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 저장 장치는 구조 자체가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Register와 Cache는 CPU에 매우 가까이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM은 CPU 밖의 메모리 모듈을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSD는 저장 장치 Controller와 I/O 경로를 거쳐 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 데이터가 CPU에서 멀어질수록 접근 과정도 많아진다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CPU

&amp;darr;

Cache

&amp;darr;

Memory Controller

&amp;darr;

RAM

&amp;darr;

Storage Controller

&amp;darr;

SSD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 계층이 아래로 내려갈수록 지연 시간이 커진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU는 어떻게 느린 메모리를 숨길까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 단순히 기다리지만은 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 최적화 기법을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cache&lt;/li&gt;
&lt;li&gt;Prefetching&lt;/li&gt;
&lt;li&gt;Out-of-Order Execution&lt;/li&gt;
&lt;li&gt;Branch Prediction&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 메모리 관점에서는 Cache와 Prefetching이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Prefetching이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 앞으로 사용할 가능성이 높은 데이터를 미리 가져오기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;Prefetching&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 배열을 순서대로 읽는다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 4 &amp;rarr; 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;다음에도 주변 데이터를 읽겠구나&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 예상하고 미리 데이터를 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재 1 사용

&amp;darr;

2, 3, 4 미리 준비&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 실제로 데이터를 사용할 때 기다리는 시간을 줄일 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이전 글의 Locality와 연결된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 &lt;b&gt;Locality(지역성)&lt;/b&gt; 를 알아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램은 일반적으로&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;최근 사용한 데이터

+

주변 데이터&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 다시 사용할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성이 있기 때문에 Cache와 Prefetching이 효과적으로 동작할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Temporal Locality&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 사용한 데이터를 다시 사용할 가능성이 높다.&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;A 사용

&amp;darr;

조금 뒤

&amp;darr;

A 다시 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spatial Locality&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용한 데이터 주변을 사용할 가능성이 높다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Array

1 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 계층 구조의 성능은 이런 지역성을 적극적으로 활용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 계층 구조를 백엔드에서도 볼 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 구조는 Application에서도 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 데이터 조회 흐름을 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Application

&amp;darr;

Redis

&amp;darr;

MySQL

&amp;darr;

Buffer Pool

&amp;darr;

SSD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 있으면 MySQL까지 가지 않는다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Redis Hit

&amp;darr;

Response&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 없다면 MySQL을 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 Buffer Pool을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Buffer Pool Hit

&amp;darr;

Memory에서 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Buffer Pool에도 없다면 SSD까지 접근한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Buffer Pool Miss

&amp;darr;

Disk Read&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 &lt;b&gt;빠른 계층부터 확인한다&lt;/b&gt;는 메모리 계층 구조와 비슷한 아이디어다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cache가 여러 단계인 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU도 하나의 Cache만 사용하는 것이 아니라 L1, L2, L3로 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application도 마찬가지로 여러 Cache 계층을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Browser Cache

&amp;darr;

CDN

&amp;darr;

Redis

&amp;darr;

MySQL Buffer Pool

&amp;darr;

Disk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 구조가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계마다&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;위에서 처리 가능하면
아래 단계 접근을 피한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 원칙이 적용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CDN도 메모리 계층과 비슷하게 이해할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 한국에 있고 이미지 서버가 미국에 있다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 미국 서버에서 이미지를 가져오면 네트워크 시간이 오래 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 가까운 CDN Edge에 데이터를 저장한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User

&amp;darr;

CDN Edge

&amp;darr;

Origin Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN에 데이터가 있다면 Origin Server까지 가지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache와 구현은 완전히 다르지만&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;느린 곳까지 가지 않도록
가까운 곳에 복사해둔다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 Cache의 핵심 아이디어는 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 계층에서 중요한 것은 Latency다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능을 이야기할 때 단순히 처리량만 보는 것이 아니라 &lt;b&gt;Latency&lt;/b&gt;도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Latency는 요청한 데이터가 돌아오기까지 걸리는 시간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 Cache에서 데이터를 찾으면 Latency가 매우 낮다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM으로 내려가면 더 오래 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSD는 훨씬 더 오래 걸린다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;L1
&amp;rarr; 매우 낮은 Latency

RAM
&amp;rarr; 더 높은 Latency

SSD
&amp;rarr; 훨씬 높은 Latency&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Cache Miss 하나가 생각보다 큰 성능 차이를 만들 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 계층에서 중요한 또 하나는 Bandwidth다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Latency뿐만 아니라 Bandwidth도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bandwidth는 일정 시간 동안 얼마나 많은 데이터를 전송할 수 있는지를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 대용량 데이터를 읽는 작업에서는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;한 번 접근하는 시간&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐만 아니라&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;초당 얼마나 많은 데이터를 읽을 수 있는가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 시스템 성능을 볼 때 Latency와 Bandwidth를 구분해서 보는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대용량 데이터 처리에서 메모리 접근이 중요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 수백만 개의 데이터를 반복해서 처리한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 자체는 단순하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;값 읽기

&amp;darr;

계산

&amp;darr;

다음 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터가 Cache에 잘 올라오지 않는 접근 패턴이라면 CPU가 계속 메모리를 기다릴 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;CPU

계산
&amp;darr;
Memory 기다림
&amp;darr;
계산
&amp;darr;
Memory 기다림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 사용률이 높아 보여도 실제 효율은 좋지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 고성능 컴퓨팅에서는 알고리즘뿐만 아니라 Memory Access Pattern도 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array가 실무에서 유리한 이유도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Array는 메모리에 연속적으로 배치되는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[A][B][C][D][E]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순차적으로 읽으면 CPU Cache와 Prefetching 효과를 잘 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Pointer를 따라가야 하는 구조에서는&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;A
&amp;darr;
메모리 다른 위치 B
&amp;darr;
또 다른 위치 C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Random Memory Access가 많아질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이론적인 시간 복잡도가 같더라도 실제 실행 속도가 달라질 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB에서도 Sequential Access가 유리한 경우가 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 개념은 Storage에서도 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Disk나 SSD에서 데이터를 읽을 때도 작은 Random I/O를 반복하는 것보다 연속적인 데이터를 읽는 것이 유리한 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database가 Page 단위로 데이터를 관리하는 것도 이런 저장 장치 특성과 연결해서 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Page A
Page B
Page C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 연속적으로 읽는 것과&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Page A
Page Z
Page M
Page X&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 무작위로 읽는 것은 I/O 특성이 다를 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가상 메모리도 계층 구조와 연결된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM이 부족해지면 운영체제는 일부 데이터를 Disk로 이동시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Swap과 연결해서 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RAM 부족

&amp;darr;

사용하지 않는 Page

&amp;darr;

Disk Swap&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 해당 데이터가 필요하면 다시 RAM으로 가져와야 한다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Swap

&amp;darr;

RAM

&amp;darr;

CPU&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Disk 접근이 발생하기 때문에 성능이 크게 떨어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 서버에서 Swap 사용량이 급격히 증가하면 성능 문제를 의심하는 이유다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Page Fault도 같은 흐름이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스가 필요한 Page를 RAM에서 찾지 못하면 Page Fault가 발생할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Process

&amp;darr;

Page 필요

&amp;darr;

RAM에 없음

&amp;darr;

Disk에서 가져오기

&amp;darr;

RAM 적재

&amp;darr;

실행 계속&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 느린 계층으로 내려간 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memory Hierarchy를 이해하면 Page Fault가 왜 성능에 큰 영향을 주는지도 이해하기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;백엔드 서버에서 메모리가 부족하면 어떻게 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Laravel 서버에 Worker를 계속 늘렸다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 1
Worker 2
Worker 3
...
Worker 50&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Worker가 메모리를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM이 부족해진다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;RAM 부족

&amp;darr;

Swap 사용

&amp;darr;

Disk I/O 증가

&amp;darr;

Response 느려짐&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 충분해도 Memory Hierarchy가 아래 단계로 내려가면서 성능이 떨어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Worker 수를 결정할 때 CPU만 보는 것이 아니라 Memory도 함께 봐야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL에서도 메모리가 중요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 Buffer Pool을 적극적으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리가 충분하면 많은 Data Page와 Index Page를 Buffer Pool에 유지할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Query

&amp;darr;

Buffer Pool Hit

&amp;darr;

빠른 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Buffer Pool이 부족하면 Disk Read가 증가할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Query

&amp;darr;

Buffer Pool Miss

&amp;darr;

SSD

&amp;darr;

응답 지연&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DB에서도 Memory Hierarchy가 성능에 직접 영향을 준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis가 빠른 이유도 메모리를 사용하기 때문이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 데이터를 주로 Memory에 유지한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Application

&amp;darr;

Redis Memory&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Disk 기반 DB에서 Query하고 Page를 찾는 것보다 단순 Key Lookup이 매우 빠를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Session&lt;/li&gt;
&lt;li&gt;Cache&lt;/li&gt;
&lt;li&gt;Ranking&lt;/li&gt;
&lt;li&gt;임시 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등에서 자주 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 메모리는 SSD보다 비싸고 용량이 제한되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis도 결국&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;속도
vs
용량과 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trade-off 위에 있는 시스템이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빠른 저장 공간일수록 비싼 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 계층 구조를 보면 항상 같은 패턴이 나온다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;빠름
&amp;rarr; 비쌈
&amp;rarr; 용량 작음

느림
&amp;rarr; 저렴함
&amp;rarr; 용량 큼&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 모든 데이터를 가장 빠른 계층에 저장하는 것은 경제적으로 비효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 시스템은 필요한 데이터만 빠른 계층에 유지하고 나머지는 더 큰 저장 공간에 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 Cache가 계속 등장하는 근본적인 이유다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서는 어떻게 활용할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자가 CPU Cache를 직접 제어할 일은 많지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 메모리 계층 구조를 이해하면 성능 문제를 볼 때 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 API가 느리다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Application 문제인가?

&amp;darr;

DB Query 문제인가?

&amp;darr;

Buffer Pool Miss인가?

&amp;darr;

Storage I/O가 느린가?

&amp;darr;

Swap이 발생하는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 여러 계층을 살펴볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;CPU가 낮으니까 서버는 정상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 판단하면 실제 병목을 놓칠 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 문제는 느린 계층 접근 횟수를 확인해보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 성능 문제를 볼 때 도움이 되는 관점이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;지금 요청이 느린 계층까지 얼마나 자주 내려가고 있는가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Redis Hit
&amp;rarr; DB 접근 없음

Redis Miss
&amp;rarr; MySQL

Buffer Pool Hit
&amp;rarr; Disk 접근 없음

Buffer Pool Miss
&amp;rarr; Disk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache Miss가 계속 겹치면 가장 느린 저장 공간까지 접근하게 된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Cache Miss

&amp;darr;

Cache Miss

&amp;darr;

Disk I/O

&amp;darr;

Latency 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관점은 CPU Cache부터 Redis까지 공통적으로 적용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무조건 Cache를 많이 두는 것도 정답은 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느린 계층으로 가지 않기 위해 Cache를 계속 추가하면 성능은 좋아질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시스템은 복잡해진다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Browser Cache

CDN Cache

Application Cache

Redis Cache

DB Buffer Pool&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층에서 데이터가 오래될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;어떤 Cache를 삭제해야 하는가?

어떤 TTL을 사용할 것인가?

데이터가 변경되면 어디까지 갱신해야 하는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 새로운 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Cache도 필요한 곳에 적절히 사용하는 것이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서 느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 서버 성능을 볼 때는 CPU 사용률과 Query 실행 시간 정도만 확인하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시스템을 조금 더 공부하면 성능이 결국 &lt;b&gt;데이터를 어디에서 가져오는가&lt;/b&gt;와 밀접하게 연결되어 있다는 것을 알게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 데이터라도&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;CPU Cache에서 가져오는지

RAM에서 가져오는지

SSD에서 가져오는지

네트워크 너머 DB에서 가져오는지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 따라 비용은 크게 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 백엔드에서도 같은 원리가 반복된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Redis

&amp;darr;

MySQL Buffer Pool

&amp;darr;

Disk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 빠른 저장 공간을 앞에 두고 느린 공간 접근을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Cache를 이해하려면 Redis 사용법만 보는 것보다 Memory Hierarchy라는 CS 개념부터 이해하는 것이 훨씬 도움이 된다고 느꼈다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 계층 구조는 &lt;b&gt;속도, 용량, 비용이 서로 다른 저장 공간을 여러 단계로 구성해 전체 시스템 성능을 높이는 구조&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 계층은&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Register

&amp;darr;

L1 Cache

&amp;darr;

L2 Cache

&amp;darr;

L3 Cache

&amp;darr;

RAM

&amp;darr;

SSD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위로 갈수록&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;빠름
+
작음
+
비쌈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래로 갈수록&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;느림
+
큼
+
저렴함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 특징이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터는 지역성과 Cache를 이용해서 자주 사용하는 데이터는 빠른 계층에 두고, 모든 데이터를 느린 계층에서 매번 가져오는 일을 피한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 원리는 CPU 내부에서만 사용되는 것이 아니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Browser Cache

CDN

Redis

MySQL Buffer Pool

Disk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 백엔드 시스템에서도 비슷한 구조를 반복해서 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 성능 최적화의 핵심 중 하나는 &lt;b&gt;자주 사용하는 데이터를 얼마나 빠른 계층에서 처리할 수 있도록 만드는가&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 계층 구조까지 이해하면 다음으로 가상 메모리, Page Table, TLB 같은 운영체제와 컴퓨터 구조의 메모리 관리 방식도 훨씬 자연스럽게 이해할 수 있다.&lt;/p&gt;</description>
      <category>개발/CS</category>
      <category>BufferPool</category>
      <category>Cache</category>
      <category>CPUCache</category>
      <category>CS</category>
      <category>MemoryHierarchy</category>
      <category>RAM</category>
      <category>redis</category>
      <category>SSD</category>
      <category>메모리</category>
      <category>백엔드</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/182</guid>
      <comments>https://dev-lee-6.tistory.com/entry/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0%EB%9E%80-Register%EB%B6%80%ED%84%B0-SSD%EA%B9%8C%EC%A7%80-%EC%86%8D%EB%8F%84%EA%B0%80-%EB%8B%A4%EB%A5%B8-%EC%9D%B4%EC%9C%A0#entry182comment</comments>
      <pubDate>Thu, 20 Aug 2026 15:52:54 +0900</pubDate>
    </item>
    <item>
      <title>캐시(Cache)는 왜 빠를까? 지역성(Locality)과 CPU Cache 쉽게 이해하기</title>
      <link>https://dev-lee-6.tistory.com/entry/%EC%BA%90%EC%8B%9CCache%EB%8A%94-%EC%99%9C-%EB%B9%A0%EB%A5%BC%EA%B9%8C-%EC%A7%80%EC%97%AD%EC%84%B1Locality%EA%B3%BC-CPU-Cache-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다 보면 Cache라는 말을 정말 자주 듣게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis Cache&lt;/li&gt;
&lt;li&gt;Browser Cache&lt;/li&gt;
&lt;li&gt;CDN Cache&lt;/li&gt;
&lt;li&gt;MySQL Buffer Pool&lt;/li&gt;
&lt;li&gt;CPU Cache&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종류는 다르지만 기본 아이디어는 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자주 사용하는 데이터를 더 빠른 곳에 보관해두고, 느린 곳까지 접근하는 횟수를 줄이는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발에서는 Cache라고 하면 Redis를 먼저 떠올리기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Cache라는 개념은 훨씬 아래 계층인 CPU와 메모리 구조에서도 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 RAM보다 훨씬 빠르게 동작하기 때문에 CPU 입장에서는 메모리에서 데이터를 가져오는 시간조차 긴 대기 시간이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 CPU 가까이에 매우 빠른 Cache Memory를 두고 자주 사용할 가능성이 높은 데이터를 미리 보관한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 개념이 &lt;b&gt;지역성(Locality)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램은 데이터를 완전히 무작위로 사용하는 것이 아니라 최근에 사용했던 데이터나 그 주변 데이터를 다시 사용할 가능성이 높은데, Cache는 이런 특성을 적극적으로 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 CPU Cache가 왜 필요한지부터 시작해서 Temporal Locality, Spatial Locality, Cache Line을 살펴보고, 마지막에는 Redis와 MySQL Buffer Pool까지 연결해서 Cache라는 개념을 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU Cache는 왜 필요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 실행되면 CPU는 계속 데이터를 읽고 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드가 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;a = 10
b = 20

c = a + b&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 &lt;code&gt;c&lt;/code&gt;를 계산하려면 먼저 &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt; 값을 가져와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 생각하면 다음 과정이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;CPU

&amp;darr;

RAM에서 데이터 읽기

&amp;darr;

계산&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 CPU가 매우 빠르다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 연산을 처리하는 속도에 비해 RAM에서 데이터를 가져오는 것은 상대적으로 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 RAM까지 데이터를 읽으러 간다면 CPU는 계산보다 데이터를 기다리는 데 시간을 쓰게 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래서 CPU와 RAM 사이에 Cache가 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;CPU

&amp;darr;

L1 Cache

&amp;darr;

L2 Cache

&amp;darr;

L3 Cache

&amp;darr;

RAM

&amp;darr;

SSD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU에 가까울수록 일반적으로&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;빠름

+

용량 작음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 특징을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 아래 계층으로 내려갈수록&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;느림

+

용량 큼&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 특징을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 원하는 데이터를 L1에서 찾을 수 있다면 RAM까지 내려갈 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache Hit와 Cache Miss&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 데이터 A를 필요로 한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache에 이미 데이터가 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;CPU

&amp;darr;

Cache 조회

&amp;darr;

데이터 존재

&amp;darr;

바로 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 &lt;b&gt;Cache Hit&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Cache에 데이터가 없다면 다음 과정이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;CPU

&amp;darr;

Cache 조회

&amp;darr;

데이터 없음

&amp;darr;

더 느린 메모리에서 조회

&amp;darr;

Cache에 저장

&amp;darr;

CPU에서 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;Cache Miss&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Cache를 효과적으로 사용하려면 가능한 한 Cache Hit가 많이 발생해야 한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Cache Hit 증가

&amp;darr;

느린 메모리 접근 감소

&amp;darr;

CPU 대기 감소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cache 성능의 핵심은 지역성(Locality)이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache의 용량은 RAM보다 훨씬 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM에 있는 모든 데이터를 Cache에 넣을 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떤 데이터를 Cache에 보관해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 개념이 &lt;b&gt;지역성(Locality)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램은 일반적으로 다음 두 가지 특성을 가진다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;최근 사용한 데이터를 다시 사용할 가능성이 높음

사용한 데이터 주변을 곧 사용할 가능성이 높음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 각각&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Temporal Locality

Spatial Locality&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Temporal Locality란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporal Locality는 &lt;b&gt;최근에 사용한 데이터를 가까운 미래에 다시 사용할 가능성이 높다&lt;/b&gt;는 특성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리말로는 시간적 지역성이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 반복문을 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;$count = 0;

for ($i = 0; $i &amp;lt; 1000; $i++) {
    $count++;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;$count&lt;/code&gt;와 &lt;code&gt;$i&lt;/code&gt;는 계속 반복해서 사용된다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;count 사용

&amp;darr;

조금 뒤

&amp;darr;

count 다시 사용

&amp;darr;

또 다시 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 사용한 데이터를 Cache에 유지해두면 다시 필요한 순간에 더 빠르게 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;백엔드에서도 비슷한 패턴을 볼 수 있다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 상품 하나가 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Product #100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 시간에 여러 사용자가 같은 상품을 조회한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User A &amp;rarr; Product #100

User B &amp;rarr; Product #100

User C &amp;rarr; Product #100

User D &amp;rarr; Product #100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 매번 같은 상품을 조회하기보다 Redis에 저장해두면 반복 조회 비용을 줄일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;첫 번째 요청

&amp;darr;

DB 조회

&amp;darr;

Redis 저장


이후 요청

&amp;darr;

Redis 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache와 Redis Cache의 구현 방식은 전혀 다르지만 &lt;b&gt;최근 사용한 데이터가 다시 필요할 가능성이 높다&lt;/b&gt;는 아이디어에서는 연결해서 생각해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spatial Locality란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spatial Locality는 &lt;b&gt;특정 데이터를 사용했다면 그 주변 데이터도 곧 사용할 가능성이 높다&lt;/b&gt;는 특성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리말로는 공간적 지역성이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 배열을 순서대로 읽는다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[1][2][3][4][5][6][7][8]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 데이터를 사용했다면 곧 다음 데이터도 사용할 가능성이 높다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1

&amp;darr;

2

&amp;darr;

3

&amp;darr;

4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache는 이런 특성을 활용하기 위해 필요한 값 하나만 가져오는 것이 아니라 일정 범위의 데이터를 함께 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cache Line과 메모리 접근 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache는 일반적으로 데이터를 &lt;b&gt;Cache Line&lt;/b&gt;이라는 단위로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 배열의 특정 값을 읽는다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Array

[1][2][3][4][5][6][7][8]
    &amp;uarr;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로 &lt;code&gt;2&lt;/code&gt; 하나만 가져오는 것이 아니라 인접한 메모리 영역을 같이 Cache에 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;RAM

[1][2][3][4]

&amp;darr;

Cache Line

[1][2][3][4]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 프로그램에서 &lt;code&gt;3&lt;/code&gt;, &lt;code&gt;4&lt;/code&gt;를 읽는다면 이미 Cache에 들어 있을 가능성이 높아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배열을 순서대로 읽으면 왜 빠를까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 배열을 순차적으로 탐색한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 4 &amp;rarr; 5 &amp;rarr; 6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 가져온 Cache Line의 데이터를 계속 활용할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 메모리의 완전히 다른 위치를 반복해서 접근한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100

&amp;darr;

50000

&amp;darr;

120

&amp;darr;

900000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 기존 Cache Line을 활용하지 못하고 Cache Miss가 늘어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 개수의 데이터를 읽더라도 &lt;b&gt;메모리에 어떻게 접근하느냐에 따라 실제 성능이 달라질 수 있는 이유&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array와 Linked List의 실제 성능 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료구조를 공부할 때 Array와 Linked List를 비교하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Array는 일반적으로 데이터가 연속적인 메모리 공간에 배치되는 형태를 갖는다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[A][B][C][D][E]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 순차 탐색할 때 Cache Line을 효과적으로 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linked List는 각 Node가 서로 떨어진 메모리에 위치할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Node A

&amp;darr;

메모리 다른 위치

&amp;darr;

Node B

&amp;darr;

또 다른 위치

&amp;darr;

Node C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 Node로 이동할 때마다 새로운 메모리 위치에 접근하면서 Cache Miss가 발생할 가능성이 높아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 차이 때문에 실제 프로그램 성능은 단순한 Big-O 시간 복잡도만으로 완전히 설명되지 않는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;L1, L2, L3와 메모리 계층 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache는 보통 여러 단계로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L1 Cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Core에 매우 가깝다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;CPU Core

&amp;darr;

L1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 가장 작지만 가장 빠른 Cache다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용하는 데이터와 명령어를 CPU에 빠르게 공급하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L2 Cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L1보다 상대적으로 크지만 조금 느리다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;CPU

&amp;darr;

L1 Miss

&amp;darr;

L2 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L1에서 데이터를 찾지 못했을 때 다음 계층을 확인하는 식으로 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L3 Cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 L1, L2보다 더 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 구조에 따라 다르지만 여러 Core가 공유하는 형태도 흔하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Core 1 ─┐
Core 2 ─┼&amp;rarr; L3 Cache
Core 3 ─┤
Core 4 ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부 구조는 CPU마다 다르기 때문에&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;L1은 무조건 이런 구조다.

L3는 무조건 모든 Core가 공유한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 외울 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 CPU와 가까운 계층일수록 빠른 대신 용량이 작다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Register까지 포함하면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 내부에는 Register도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적인 메모리 계층을 더 확장하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Register

&amp;darr;

L1 Cache

&amp;darr;

L2 Cache

&amp;darr;

L3 Cache

&amp;darr;

RAM

&amp;darr;

SSD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위쪽으로 갈수록&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;빠름

비쌈

용량 작음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래쪽으로 갈수록&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;느림

상대적으로 저렴함

용량 큼&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라는 형태를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조를 &lt;b&gt;Memory Hierarchy&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티코어 환경에서는 Cache도 동시성 문제가 생긴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache가 빠르다고 해서 관리해야 할 문제가 없는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 멀티코어 CPU에서는 각 Core가 별도의 Cache를 사용하면서 데이터 일관성 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache Coherence&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 두 Core가 같은 값을 Cache하고 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Core 1 Cache

value = 10


Core 2 Cache

value = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core 1이 값을 변경한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Core 1

value = 20&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Core 2가 계속 이전 값인&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;value = 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 사용한다면 문제가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 멀티코어 CPU에서는 각 Cache가 바라보는 데이터 상태를 조정하기 위한 &lt;b&gt;Cache Coherence&lt;/b&gt; 메커니즘이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 MESI 같은 Protocol이 알려져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부 구현을 모두 외울 필요는 없더라도 다음 흐름 정도는 이해해두면 좋다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;멀티코어

&amp;darr;

각 Core의 Cache

&amp;darr;

같은 데이터의 여러 복사본 존재 가능

&amp;darr;

데이터 일관성 필요

&amp;darr;

Cache Coherence&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;False Sharing&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티스레딩 환경에서 성능에 영향을 줄 수 있는 개념 중 하나가 False Sharing이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread A와 Thread B가 서로 다른 변수를 수정한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;Thread A

&amp;rarr; variable A


Thread B

&amp;rarr; variable B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보면 서로 다른 데이터라 충돌할 일이 없어 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 두 변수가 같은 Cache Line에 들어 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Cache Line

[ variable A ][ variable B ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread A가 A를 변경하면 해당 Cache Line에 대한 일관성 처리가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread B가 B를 변경해도 다시 같은 Cache Line이 영향을 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;서로 다른 변수

&amp;darr;

같은 Cache Line

&amp;darr;

불필요한 Cache 경쟁&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 &lt;b&gt;False Sharing&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Laravel API를 만들면서 직접 신경 쓸 일은 많지 않지만, 멀티스레드 프로그램이나 고성능 시스템을 이해할 때 중요한 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU Cache와 Redis Cache는 어떻게 연결될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 개발자가 Cache라고 하면 보통 Redis를 먼저 떠올린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache와 Redis는 구현 방식도 목적도 완전히 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 큰 관점에서는 공통된 아이디어가 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;느린 계층까지 매번 접근하지 말고

자주 사용하는 데이터를

더 빠른 계층에 두자.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU Cache&lt;/h3&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;CPU

&amp;darr;

CPU Cache

&amp;darr;

RAM&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM에 매번 접근하지 않도록 Cache를 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Cache&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Application

&amp;darr;

Redis

&amp;darr;

MySQL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 요청마다 DB Query를 실행하지 않도록 Redis에 데이터를 둘 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL Buffer Pool&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL InnoDB도 Buffer Pool을 이용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Application

&amp;darr;

MySQL

&amp;darr;

Buffer Pool

&amp;darr;

Disk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 데이터 페이지가 Buffer Pool에 이미 있다면 Disk까지 읽으러 갈 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 전체를 보면 여러 종류의 Cache가 계층적으로 존재하는 셈이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CPU

&amp;darr;

CPU Cache

&amp;darr;

RAM


Application

&amp;darr;

Redis


MySQL

&amp;darr;

Buffer Pool

&amp;darr;

Disk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Cache의 구현은 다르지만 공통적으로 &lt;b&gt;느린 영역에 접근하는 횟수를 줄이는 것&lt;/b&gt;이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애플리케이션 Cache에서는 무엇을 고민해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache는 하드웨어가 대부분 관리해주지만 Redis 같은 애플리케이션 Cache는 개발자가 직접 정책을 결정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다른 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 데이터를 Cache해야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 다음 특성을 가진 데이터는 Cache 효과를 보기 좋다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;조회가 자주 발생함

+

변경은 상대적으로 적음

+

DB 조회나 계산 비용이 큼&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 데이터다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;인기 상품

카테고리

공통 설정

복잡한 통계 결과

외부 API 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 데이터가 요청마다 달라지고 항상 최신 값이 중요한 경우에는 Cache 관리 비용이 더 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache Hit Ratio&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache를 사용하고 있다는 사실만으로 충분하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마나 많은 요청을 Cache에서 해결하고 있는지도 봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100건 요청

&amp;darr;

90 Cache Hit

10 Cache Miss&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 Cache 효과가 좋을 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100건 요청

&amp;darr;

10 Cache Hit

90 Cache Miss&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 Cache 저장과 무효화 비용에 비해 얻는 효과가 작을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 다음을 같이 보는 편이 좋다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;조회 빈도

변경 빈도

Cache Hit Ratio

Cache Miss 시 DB 비용

Cache 메모리 사용량&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cache에서 가장 어려운 것은 Invalidation이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache에 데이터를 넣는 것 자체는 어렵지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel에서는 다음처럼 저장할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Cache::put(
    'product:' . $id,
    $product,
    3600
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 데이터가 바뀌었을 때다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 상태라고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;DB

price = 10,000


Redis

price = 10,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자가 가격을 수정한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;DB

price = 8,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Redis 값이 그대로 남아 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Redis

price = 10,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 오래된 가격을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache를 사용하면 삭제 전략도 같이 설계해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 데이터 변경 시 Cache를 제거할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Cache::forget(
    'product:' . $id
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 조회에서는 DB에서 최신 데이터를 가져온 뒤 다시 Cache에 넣는다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;상품 수정

&amp;darr;

DB Update

&amp;darr;

Cache 삭제

&amp;darr;

다음 요청

&amp;darr;

Cache Miss

&amp;darr;

DB 조회

&amp;darr;

새 Cache 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Cache를 설계할 때는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;어떻게 저장할까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;언제 삭제하거나 갱신할까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 더 어려운 문제일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TTL과 최신성 사이의 균형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL을 길게 설정한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;TTL = 24시간&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache Hit는 늘어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 오래된 데이터를 볼 가능성도 높아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;TTL = 1초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 최신성은 높지만 Cache 효과가 거의 없을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 다음 사이의 균형을 잡아야 한다.&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;성능

vs

데이터 최신성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cache-Aside와 Cache Stampede&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 Redis를 사용할 때 자주 볼 수 있는 방식 중 하나가 &lt;b&gt;Cache-Aside Pattern&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache-Aside Pattern&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Request

&amp;darr;

Cache 조회

├─ Hit
│
│  Cache 반환
│
└─ Miss
   │
   DB 조회
   │
   Cache 저장
   │
   Response&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel에서는 &lt;code&gt;Cache::remember()&lt;/code&gt;를 이용해 단순하게 구현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;$product = Cache::remember(
    'product:' . $id,
    3600,
    function () use ($id) {
        return Product::findOrFail($id);
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache에 데이터가 있으면 Cache 값을 반환하고, 없다면 DB를 조회한 뒤 Cache에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인기 Cache가 동시에 만료되면 문제가 생길 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 상품 Cache가 만료되는 순간을 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;product:100

TTL 만료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 순간 요청 1,000개가 들어온다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Request 1 &amp;rarr; Cache Miss

Request 2 &amp;rarr; Cache Miss

Request 3 &amp;rarr; Cache Miss

...

Request 1000 &amp;rarr; Cache Miss&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요청이 동시에 DB를 조회한다면 순간적인 부하가 크게 증가할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Redis Cache Miss

&amp;darr;

MySQL 요청 폭증&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 현상을 &lt;b&gt;Cache Stampede&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache Stampede를 줄이는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 다음 방법을 검토할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Lock

TTL Randomization

Cache 미리 갱신

Stale Data 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 최초 한 요청만 Lock을 획득해 Cache를 생성하도록 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Cache Miss

&amp;darr;

Request A

Lock 획득

&amp;darr;

DB 조회

&amp;darr;

Cache 저장


다른 Request

&amp;darr;

잠시 대기

&amp;darr;

생성된 Cache 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cache도 결국 Lock, Race Condition 같은 동시성 문제와 연결될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cache를 성능 문제를 숨이는 용도로 사용하면 안 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API가 느려졌다고 바로 Redis부터 적용하는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 실제 원인은 Index가 없는 Slow Query라고 해보자.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;API 느림

&amp;darr;

Slow Query&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Redis를 적용한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;API

&amp;darr;

Redis Hit

&amp;darr;

빠름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로는 문제가 해결된 것처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Cache Miss가 발생하면 여전히 느린 Query가 실행된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Cache Miss

&amp;darr;

Slow Query

&amp;darr;

DB 부하&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근본적인 문제는 그대로다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무에서는 병목부터 확인하는 편이 좋다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 보통 다음 순서로 보는 편이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;API가 느림

&amp;darr;

Slow Query 확인

&amp;darr;

Index / Query 개선

&amp;darr;

애플리케이션 로직 확인

&amp;darr;

반복 조회 비용이 여전히 크다면

&amp;darr;

Cache 검토&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache는 잘못된 Query를 숨이는 용도라기보다 &lt;b&gt;이미 충분히 최적화된 비싼 작업의 반복 비용을 줄이는 도구&lt;/b&gt;로 사용하는 편이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 MySQL 인덱스를 다룰 때도 같은 이유로 실제 Slow Query와 &lt;code&gt;EXPLAIN&lt;/code&gt;부터 확인하는 것이 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서 Cache를 적용할 때 보는 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cache를 도입할 때는 단순히 DB보다 Redis가 빠른지만 보지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 실제로 반복 조회되는 데이터인가?&lt;/h3&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;같은 Key를 반복 조회

&amp;rarr; Cache 효과 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 다른 데이터를 조회한다면 Hit Ratio가 낮을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 원본 조회 비용이 충분히 큰가?&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;복잡한 Query

통계 계산

외부 API

다수 JOIN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 원본 조회가 비쌀수록 Cache의 가치가 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 데이터 변경 빈도는 어느 정도인가?&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;조회 매우 많음

변경 적음

&amp;rarr; Cache에 유리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 매초 변경되는 데이터를 긴 TTL로 Cache하면 정합성 문제가 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 최신 데이터가 얼마나 중요한가?&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1분 정도 늦어도 괜찮음

vs

항상 최신이어야 함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 따라 TTL과 Invalidation 전략이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Cache Miss가 동시에 발생하면 안전한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 Key라면 Stampede를 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Cache가 없어도 서비스가 동작하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하다면 Cache는 성능을 개선하는 계층이고 원본 데이터는 DB가 가지는 구조가 운영하기 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Hit Ratio를 측정할 수 있는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 후 실제 효과를 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Hit Ratio 낮음

&amp;darr;

왜 Cache를 유지하고 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 다시 검토할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cache 개념을 한 번에 정리해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU부터 애플리케이션까지 보면 Cache가 반복해서 등장한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CPU Cache

&amp;rarr; RAM 접근 감소


MySQL Buffer Pool

&amp;rarr; Disk 접근 감소


Redis Cache

&amp;rarr; DB 접근 감소


CDN Cache

&amp;rarr; Origin Server 접근 감소


Browser Cache

&amp;rarr; Network 요청 감소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 계층이든 핵심 아이디어는 비슷하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;느린 계층에 접근하는 비용이 큼

&amp;darr;

자주 사용할 데이터를 빠른 계층에 저장

&amp;darr;

반복 접근 비용 감소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Cache가 효과적인 이유 중 하나는 프로그램과 사용자 요청에 지역성이 존재하기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Temporal Locality

&amp;rarr; 최근 사용한 것을 다시 사용할 가능성이 높음


Spatial Locality

&amp;rarr; 주변 데이터를 사용할 가능성이 높음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Cache가 생기면 새로운 문제도 같이 생긴다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Cache Miss

Cache Invalidation

Cache Stampede

데이터 정합성

메모리 사용량

Lock 경쟁&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache는 단순히&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;넣으면 빨라지는 기능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아니라 &lt;b&gt;무엇을 저장하고 언제 버릴지를 결정하는 정책&lt;/b&gt;까지 포함하는 기술이라고 보는 편이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache는 &lt;b&gt;자주 사용하는 데이터를 더 빠른 저장 공간에 보관해 느린 계층에 접근하는 횟수를 줄이는 기술&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU에서도 다음과 같은 메모리 계층을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;Register

&amp;darr;

L1

&amp;darr;

L2

&amp;darr;

L3

&amp;darr;

RAM

&amp;darr;

SSD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Cache가 효과적인 중요한 이유 중 하나는 프로그램이 지역성을 가지기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Temporal Locality

&amp;rarr; 최근 사용한 데이터를 다시 사용할 가능성이 높음


Spatial Locality

&amp;rarr; 사용한 데이터 주변을 사용할 가능성이 높음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 원리를 이해하면 배열의 순차 접근이 왜 빠른지, Linked List와 실제 성능 차이가 왜 생길 수 있는지, Cache Line과 False Sharing이 왜 중요한지도 연결해서 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 한 단계 위로 올라오면 같은 Cache 개념이 Redis나 MySQL Buffer Pool에서도 반복해서 등장한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CPU Cache

&amp;rarr; RAM 접근 감소


MySQL Buffer Pool

&amp;rarr; Disk 접근 감소


Redis

&amp;rarr; DB 접근 감소&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 모두 다르지만 결국 공통된 목표는 &lt;b&gt;더 느리고 비싼 작업의 반복을 줄이는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Cache를 사용하면&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Cache Invalidation

TTL

데이터 정합성

Cache Stampede

Hit Ratio&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 새로운 문제도 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제 서비스에서는 단순히&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Redis가 DB보다 빠르다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 보고 Cache를 적용하기보다&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;이 데이터가 정말 반복해서 조회되는가?

원본 조회 비용은 얼마나 큰가?

얼마나 오래된 데이터까지 허용할 수 있는가?

언제 Cache를 제거해야 하는가?

Cache Miss가 몰려도 안전한가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 같이 확인하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Cache를 제대로 이해하려면 Redis 사용법만 보는 것보다 &lt;b&gt;CPU Cache와 Locality에서 시작해 시스템 전체의 메모리 계층 구조로 연결해서 보는 것&lt;/b&gt;이 도움이 된다고 생각한다.&lt;/p&gt;</description>
      <category>개발/CS</category>
      <category>BufferPool</category>
      <category>Cache</category>
      <category>CPUCache</category>
      <category>CS</category>
      <category>locality</category>
      <category>redis</category>
      <category>SpatialLocality</category>
      <category>TemporalLocality</category>
      <category>메모리</category>
      <category>백엔드</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/181</guid>
      <comments>https://dev-lee-6.tistory.com/entry/%EC%BA%90%EC%8B%9CCache%EB%8A%94-%EC%99%9C-%EB%B9%A0%EB%A5%BC%EA%B9%8C-%EC%A7%80%EC%97%AD%EC%84%B1Locality%EA%B3%BC-CPU-Cache-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#entry181comment</comments>
      <pubDate>Thu, 20 Aug 2026 10:29:13 +0900</pubDate>
    </item>
    <item>
      <title>[개발면접] TCP 3-Way Handshake와 4-Way Handshake 쉽게 이해하기</title>
      <link>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EB%A9%B4%EC%A0%91-TCP-3-Way-Handshake%EC%99%80-4-Way-Handshake-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 관련 개발 면접에서 정말 자주 등장하는 질문이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;TCP 3-Way Handshake를 설명해주세요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 끝나지 않고 보통 이런 질문도 이어진다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;연결할 때는 왜 3번이고, 종료할 때는 왜 4번인가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;TIME_WAIT은 왜 필요한가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 공부할 때는&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;SYN
SYN + ACK
ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서만 외우기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 면접에서는 순서 자체보다 &lt;b&gt;왜 이런 과정을 거치는지&lt;/b&gt;를 이해하고 설명하는 게 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심부터 정리하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCP 3-Way Handshake는 클라이언트와 서버가 데이터를 주고받기 전에 서로 통신 가능한 상태인지 확인하고 연결을 설정하는 과정이다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 연결을 종료할 때는 &lt;b&gt;4-Way Handshake&lt;/b&gt;를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 살펴보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TCP부터 간단하게 정리해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 Transmission Control Protocol의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 특징은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Connection-Oriented
Reliable
Ordered
Error Detection
Flow Control
Congestion Control&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 &lt;b&gt;신뢰성 있는 데이터 전송을 제공하는 프로토콜&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클라이언트가 다음 데이터를 보냈다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;A
B
C
D&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 데이터가 중간에 유실되거나 순서가 바뀌었을 때 이를 확인하고 처리할 수 있는 메커니즘을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터를 보내기 전에 먼저 해야 할 일이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;b&gt;연결 설정&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 바로 데이터를 보내지 않을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 서버에 바로 데이터를 보낸다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client
   │
   │ Data
   ▼
Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 서버가 현재 요청을 받을 수 없는 상태일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 클라이언트가 보낸 패킷이 서버에 도착할 수 있는지, 서버의 응답이 다시 클라이언트까지 돌아올 수 있는지도 확인되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 데이터를 보내기 전에 양쪽이 통신 가능한 상태인지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client &amp;larr;────&amp;rarr; Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 &lt;b&gt;3-Way Handshake&lt;/b&gt;다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-Way Handshake 전체 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 전체 구조부터 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │                              │
   │ -------- SYN -------------&amp;gt;  │
   │                              │
   │ &amp;lt;------ SYN + ACK ---------  │
   │                              │
   │ -------- ACK -------------&amp;gt;  │
   │                              │
   │       Connection Established │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 세 번의 통신이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;3-Way Handshake&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: SYN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Client가 Server에게 연결을 요청한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │ -------- SYN -------------&amp;gt;  │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SYN은 &lt;b&gt;Synchronize Sequence Numbers&lt;/b&gt;의 의미를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP에서는 데이터 순서를 관리하기 위해 Sequence Number를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client가 다음과 같은 Sequence Number를 보냈다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;SYN

Seq = 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미를 단순화하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나 TCP 연결하고 싶어. 내 시작 Sequence Number는 100이야.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client는 SYN을 보낸 후 Server의 응답을 기다린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Client 상태는 보통 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;SYN_SENT&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: SYN + ACK&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server가 SYN을 정상적으로 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Server는 두 가지 정보를 함께 보낸다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;SYN
+
ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │ -------- SYN -------------&amp;gt;  │
   │                              │
   │ &amp;lt;------ SYN + ACK ---------  │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Server가 다음과 같이 응답했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Seq = 500
Ack = 101&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &lt;code&gt;101&lt;/code&gt;일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client가 보낸 Sequence Number가&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이었기 때문에 다음 번호를 요청하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;100 + 1 = 101&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;네 SYN 잘 받았어. 다음에는 101부터 보내줘. 그리고 내 시작 Sequence Number는 500이야.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 의미로 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server는 이 시점에서 보통 다음 상태가 된다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;SYN_RECEIVED&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: ACK&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client가 Server의 SYN + ACK를 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Client도 Server의 Sequence Number를 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server가&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Seq = 500&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 보냈으므로 Client는 다음과 같이 응답한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Ack = 501&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 과정은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

Seq=100

   │ -------- SYN -------------&amp;gt;  │
   │                              │
   │ &amp;lt;------ SYN + ACK ---------  │
   │         Seq=500              │
   │         Ack=101              │
   │                              │
   │ -------- ACK -------------&amp;gt;  │
   │         Ack=501              │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 양쪽 모두 연결이 정상적으로 설정됐다는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태는 다음과 같이 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client : ESTABLISHED
Server : ESTABLISHED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 실제 데이터를 주고받을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 무엇을 확인한 걸까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-Way Handshake를 단순히 SYN과 ACK를 주고받는 과정으로 외우기보다 &lt;b&gt;양방향 통신이 가능한지 확인하는 과정&lt;/b&gt;으로 이해하면 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 SYN으로&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client &amp;rarr; Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신 가능 여부를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 SYN + ACK로&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Server &amp;rarr; Client&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신 가능 여부도 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 ACK를 통해 Server는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;내 응답도 Client에게 정상적으로 도착했구나.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 사실을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client &amp;rarr; Server 가능

Server &amp;rarr; Client 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 통신이 가능하다는 것을 확인하고 연결을 설정한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 2-Way Handshake가 아니라 3-Way일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서 꽤 자주 나오는 꼬리 질문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다음처럼 두 번만 통신한다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │ -------- SYN -------------&amp;gt;  │
   │                              │
   │ &amp;lt;------ SYN + ACK ---------  │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client는 Server의 응답을 받았으므로 Server가 정상적으로 통신 가능한 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Server 입장에서는 자신의 SYN + ACK가 Client에게 정상적으로 전달됐는지 확실하게 알 수 없다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server &amp;rarr; Client

도착했을까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Client가 마지막 ACK를 보내준다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client -------- ACK --------&amp;gt; Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Server도 자신의 응답이 Client에게 도착했다는 사실을 확인할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sequence Number는 왜 사용할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 신뢰성 있는 데이터 전송을 제공해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크에서는 패킷이 순서대로 도착한다는 보장이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 순서로 데이터를 보냈다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1
2
3
4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 다음처럼 도착할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1
3
2
4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 Sequence Number를 이용해서 원래 순서를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 특정 데이터가 유실됐는지도 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 3-Way Handshake 과정에서 서로의 초기 Sequence Number를 동기화한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결 종료는 왜 4-Way Handshake일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 연결을 종료할 때는 보통 &lt;b&gt;4-Way Handshake&lt;/b&gt;가 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │ -------- FIN -------------&amp;gt;  │
   │                              │
   │ &amp;lt;-------- ACK ------------  │
   │                              │
   │ &amp;lt;-------- FIN ------------  │
   │                              │
   │ -------- ACK -------------&amp;gt;  │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 설정은 3번인데 종료는 4번이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그럴까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 살펴보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: FIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client가 더 이상 보낼 데이터가 없다고 판단해서 연결 종료를 요청한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client -------- FIN --------&amp;gt; Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FIN은 Finish의 의미로 이해하면 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나는 이제 보낼 데이터가 없어. 연결을 종료하고 싶어.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client는 일반적으로 다음 상태로 변경된다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;FIN_WAIT_1&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: ACK&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server는 Client의 FIN을 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Server에게 아직 보낼 데이터가 남아 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우선&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 보낸다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │ -------- FIN -------------&amp;gt;  │
   │                              │
   │ &amp;lt;-------- ACK ------------  │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;네가 종료하고 싶다는 것은 확인했어.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직 Server의 연결 종료 준비가 끝났다는 의미는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server는 남은 데이터를 처리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: Server FIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server도 남은 데이터 처리가 끝났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Server도 연결을 종료할 준비가 됐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │ &amp;lt;-------- FIN ------------  │&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server가 Client에게 FIN을 보낸다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나도 이제 보낼 데이터가 없어.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 의미다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계: 마지막 ACK&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client는 Server의 FIN을 확인하고 ACK를 보낸다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client -------- ACK --------&amp;gt; Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 연결 종료 과정이 완료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름을 다시 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                         Server

   │ -------- FIN -------------&amp;gt;  │
   │                              │
   │ &amp;lt;-------- ACK ------------  │
   │                              │
   │      남은 데이터 처리        │
   │                              │
   │ &amp;lt;-------- FIN ------------  │
   │                              │
   │ -------- ACK -------------&amp;gt;  │&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 종료는 4번일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 설정에서는 Server가 SYN과 ACK를 동시에 보낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;SYN + ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 종료할 때는 상황이 조금 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client가 FIN을 보냈다고 해서 Server도 바로 연결을 종료할 수 있는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server에게 아직 보낼 데이터가 남아 있을 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우선&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 보내 FIN을 받았다는 사실만 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 남은 데이터를 모두 처리한 다음&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;FIN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 별도로 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일반적으로&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;FIN

ACK

FIN

ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 네 단계가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Half-Close란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 Full-Duplex 통신을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client &amp;rarr; Server

Server &amp;rarr; Client&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방향의 데이터 전송이 독립적으로 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Client가 FIN을 보냈다는 것은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Client &amp;rarr; Server 방향으로는 더 이상 데이터를 보내지 않겠다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 반대 방향은 아직 가능하다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client &amp;larr; Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태를 &lt;b&gt;Half-Close&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성 때문에 연결 종료 과정에서 FIN과 ACK가 분리될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIME_WAIT은 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 면접에서 정말 자주 따라오는 질문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4-Way Handshake의 마지막 ACK를 보낸 Client가 바로 연결 정보를 삭제하지 않고 일정 시간 기다리는 상태가 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;TIME_WAIT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server FIN
    &amp;darr;
Client ACK
    &amp;darr;
TIME_WAIT
    &amp;darr;
Connection Closed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 바로 연결을 닫지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 두 가지 이유가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마지막 ACK가 유실될 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client가 마지막 ACK를 보냈다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;Client -------- ACK ----X----&amp;gt; Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 문제로 ACK가 사라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server 입장에서는 ACK를 받지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 FIN을 다시 보낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Server -------- FIN --------&amp;gt; Client&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Client가 이미 연결 정보를 완전히 삭제했다면 정상적으로 대응하기 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일정 시간 연결 정보를 유지한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;TIME_WAIT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server가 FIN을 다시 보내면 Client는 ACK를 다시 보낼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 연결의 패킷이 남아 있을 수도 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크에서는 이전 연결에서 전송된 패킷이 늦게 도착할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 연결을 종료하고 같은 IP와 Port 조합으로 바로 새로운 연결을 만들었다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Connection 1 종료

&amp;darr;

Connection 2 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Connection 1에서 늦게 도착한 패킷이 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Old Packet
     &amp;darr;
Connection 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 연결의 패킷으로 잘못 처리될 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TIME_WAIT을 두면 이전 연결의 패킷이 네트워크에서 사라질 시간을 확보할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIME_WAIT은 얼마나 유지될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 TCP에서는 &lt;b&gt;2MSL&lt;/b&gt; 개념과 연결해서 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSL은&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Maximum Segment Lifetime&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, TCP Segment가 네트워크에서 존재할 수 있다고 보는 최대 시간을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TIME_WAIT 상태를 일정 기간 유지해서 이전 연결의 패킷이 사라질 시간을 확보한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 TIME_WAIT 유지 시간은 운영체제 설정에 따라 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서는 특정 초 단위 숫자를 외우기보다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 연결의 지연 패킷이 사라지고 마지막 ACK 유실 상황에 대응하기 위해 일정 시간 유지한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 이해하는 게 더 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIME_WAIT이 많으면 장애일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 서버에서 다음 명령 등을 확인하다 보면 TIME_WAIT 연결이 굉장히 많이 보일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;netstat -an&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ss -ant&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과에&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;TIME_WAIT
TIME_WAIT
TIME_WAIT
TIME_WAIT
TIME_WAIT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 잔뜩 보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 무조건 장애는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 연결을 정상적으로 종료하면 자연스럽게 발생할 수 있는 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 매우 많은 Short-Lived Connection이 반복된다면 TIME_WAIT 연결이 많아질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Connection 생성

&amp;darr;

요청

&amp;darr;

Connection 종료

&amp;darr;

다시 Connection 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 반복하고 있는 것은 아닌지 확인해볼 필요가 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP Keep-Alive와도 연결된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청마다 TCP 연결을 새로 만든다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;HTTP Request 1
&amp;darr;
TCP 연결
&amp;darr;
요청
&amp;darr;
TCP 종료

HTTP Request 2
&amp;darr;
TCP 연결
&amp;darr;
요청
&amp;darr;
TCP 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 3-Way Handshake와 연결 종료 과정이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용이 계속 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 TCP 연결을 일정 시간 유지하면서 여러 HTTP 요청에 재사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;TCP Connection
      │
      ├── HTTP Request 1
      ├── HTTP Request 2
      ├── HTTP Request 3
      └── HTTP Request 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 HTTP Keep-Alive를 이해하는 기본 배경이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결을 재사용하면 불필요한 TCP 연결 생성과 종료 비용을 줄일 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SYN Flood 공격은 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-Way Handshake를 이용한 대표적인 공격으로 &lt;b&gt;SYN Flood&lt;/b&gt;가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 Server에 대량의 SYN을 보낸다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Attacker

SYN
SYN
SYN
SYN
SYN

&amp;darr;

Server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server는 SYN을 받고 SYN + ACK를 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막 ACK를 기다린다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;SYN_RECEIVED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 공격자가 마지막 ACK를 보내지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 Half-Open Connection이 대량으로 쌓이면 Server의 자원을 소모시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 SYN Flood 공격이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방어 방법 중 하나로 &lt;b&gt;SYN Cookie&lt;/b&gt; 같은 기술을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP Handshake가 보안과도 연결되는 이유다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서는 어디에서 볼 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자가 직접 TCP Handshake를 구현할 일은 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버 운영 중 네트워크 문제를 분석하다 보면 관련 개념을 자주 만나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 API 호출이 느리다면 단순히 애플리케이션 코드만 볼 게 아니라&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DNS Lookup

&amp;darr;

TCP Connection

&amp;darr;

TLS Handshake

&amp;darr;

HTTP Request

&amp;darr;

Server Processing

&amp;darr;

HTTP Response&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름을 생각해봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 외부 API를 많이 호출하는 서비스라면 연결 재사용 여부도 성능에 영향을 줄 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;면접에서는 어떻게 대답하면 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;TCP 3-Way Handshake를 설명해주세요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 질문한다면 다음처럼 답할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 3-Way Handshake는 TCP 연결을 설정하기 위해 Client와 Server가 SYN, SYN+ACK, ACK를 주고받는 과정입니다. Client가 SYN을 보내 연결을 요청하고, Server가 SYN+ACK로 요청을 확인하면서 자신의 연결 요청도 전달합니다. 마지막으로 Client가 ACK를 보내면서 양쪽이 서로 통신 가능한 상태임을 확인하고 연결이 설정됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;왜 3번 필요한가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 질문한다면 다음처럼 이어갈 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽이 서로 데이터를 송수신할 수 있다는 것을 확인하고 초기 Sequence Number를 동기화해야 하기 때문입니다. 마지막 ACK가 있어야 Server도 자신의 응답이 Client에게 정상적으로 전달됐다는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4-Way Handshake 면접 답변&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;TCP 연결 종료는 왜 4-Way Handshake인가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 질문한다면 다음처럼 답할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 양방향 통신이 독립적으로 동작하기 때문에 한쪽이 FIN을 보냈다고 해서 반대쪽도 바로 연결을 종료할 수 있는 것은 아닙니다. 상대방은 우선 ACK로 FIN 수신을 확인하고, 남은 데이터를 모두 처리한 뒤 별도로 FIN을 보냅니다. 마지막으로 상대가 ACK를 보내면서 연결이 종료되기 때문에 일반적으로 네 단계가 필요합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;꼬리 질문으로 나올 만한 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;SYN은 무엇인가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 연결을 시작하면서 Sequence Number를 동기화하기 위해 사용하는 Flag다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;ACK는 무엇인가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대방이 보낸 데이터나 요청을 정상적으로 받았다는 것을 확인하는 데 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;TCP 연결은 왜 2-Way가 아니라 3-Way인가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽 모두 자신의 데이터가 상대방에게 전달될 수 있다는 것을 확인하고 Sequence Number를 동기화해야 하기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;종료는 왜 3-Way가 아니라 4-Way인가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FIN을 받은 쪽에서 아직 보낼 데이터가 남아 있을 수 있기 때문에 ACK와 FIN이 별도로 전송될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;TIME_WAIT은 왜 필요한가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 ACK가 유실됐을 때 FIN 재전송에 대응하고, 이전 연결의 지연된 패킷이 새로운 연결에 영향을 주는 것을 방지하기 위해 일정 시간 연결 정보를 유지한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;TIME_WAIT이 많으면 문제가 있는 건가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 TCP 연결 종료에서도 발생하기 때문에 무조건 문제는 아니다. 다만 Short-Lived Connection이 지나치게 많다면 연결 재사용이나 서버 설정 등을 확인할 필요가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;SYN Flood가 무엇인가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 SYN을 보내고 마지막 ACK를 보내지 않아 Server에 Half-Open Connection을 쌓아 자원을 소모시키는 공격 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 연결 과정은 다음 세 단계다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                       Server

SYN
  --------------------------&amp;gt;

             SYN + ACK
  &amp;lt;--------------------------

ACK
  --------------------------&amp;gt;

       ESTABLISHED&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 종료 과정은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                       Server

FIN
  --------------------------&amp;gt;

                 ACK
  &amp;lt;--------------------------

                 FIN
  &amp;lt;--------------------------

ACK
  --------------------------&amp;gt;

TIME_WAIT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 면접을 준비할 때는 단순히&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;SYN &amp;rarr; SYN+ACK &amp;rarr; ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;FIN &amp;rarr; ACK &amp;rarr; FIN &amp;rarr; ACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 외우기보다 다음 질문까지 연결해서 이해해두는 게 좋다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;왜 3번인가?

왜 종료는 4번인가?

Sequence Number는 왜 필요한가?

TIME_WAIT은 왜 필요한가?

Keep-Alive는 왜 사용하는가?

SYN Flood는 무엇인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 이해하고 있다면 TCP Handshake 관련 질문에서 단순 암기가 아니라 실제 네트워크 동작 원리를 기반으로 답변할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 백엔드 개발자라면 TCP를 단순한 CS 지식으로 끝내지 말고 &lt;b&gt;HTTP 요청이 실제 서버까지 도착하기 전에 어떤 과정이 발생하는지&lt;/b&gt;와 연결해서 이해해두는 게 좋다.&lt;/p&gt;</description>
      <category>개발/개발면접</category>
      <category>3wayhandshake</category>
      <category>4wayhandshake</category>
      <category>CS</category>
      <category>HTTP</category>
      <category>tcp</category>
      <category>timewait</category>
      <category>개발면접</category>
      <category>네트워크</category>
      <category>네트워크면접</category>
      <category>백엔드개발</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/180</guid>
      <comments>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EB%A9%B4%EC%A0%91-TCP-3-Way-Handshake%EC%99%80-4-Way-Handshake-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#entry180comment</comments>
      <pubDate>Thu, 20 Aug 2026 09:39:19 +0900</pubDate>
    </item>
    <item>
      <title>[개발 면접] Context Switching 쉽게 이해하기</title>
      <link>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C-%EB%A9%B4%EC%A0%91-Context-Switching-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context Switching이란? 프로세스와 스레드 전환 비용 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제 관련 개발 면접에서 &lt;b&gt;프로세스와 스레드의 차이&lt;/b&gt;를 설명하다 보면 자주 이어지는 질문이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Context Switching이 뭔가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 조금 더 깊게 들어가면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;프로세스와 스레드 중 Context Switching 비용은 어느 쪽이 더 큰가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 질문을 받을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching은 한마디로 정리하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CPU가 현재 실행 중인 작업의 상태를 저장하고 다른 작업의 상태를 불러와 실행 대상을 전환하는 과정이다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티태스킹을 이해하려면 반드시 알아야 하는 개념이고, 서버에서 스레드나 프로세스를 무작정 많이 만들면 왜 성능이 떨어질 수 있는지 설명할 때도 중요한 개념이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU는 여러 프로그램을 동시에 실행하는 걸까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터를 사용하다 보면 여러 프로그램이 동시에 실행되는 것처럼 보인다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Chrome
Slack
IDE
Docker
Music
Terminal&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 하나의 CPU Core는 기본적으로 한 순간에 하나의 실행 흐름을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 운영체제가 매우 빠르게 실행 대상을 변경한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Process A 실행
     &amp;darr;
Process B 실행
     &amp;darr;
Process C 실행
     &amp;darr;
Process A 실행
     &amp;darr;
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전환이 매우 빠르게 발생하기 때문에 사용자 입장에서는 여러 프로그램이 동시에 실행되는 것처럼 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Multi-Core CPU에서는 실제로 여러 작업을 병렬로 실행할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 4개의 Core가 있다면 여러 작업을 각각의 Core에서 동시에 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실행 가능한 작업이 Core 수보다 많다면 결국 CPU 시간을 나눠 사용해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context란 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching의 &lt;code&gt;Context&lt;/code&gt;는 현재 작업을 나중에 다시 이어서 실행하기 위해 필요한 상태 정보를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 CPU가 Process A를 실행하고 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Process A

1번 명령 실행
2번 명령 실행
3번 명령 실행
4번 명령 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번까지 실행한 상태에서 Process B로 전환됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 Process A를 다시 실행하려면 어디까지 실행했는지 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 처음부터 다시 실행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 운영체제는 현재 실행 상태를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 다음과 같은 정보가 포함될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Program Counter
CPU Register
Stack Pointer
Process State&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 실행 상태를 Context라고 이해하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context Switching 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Process A가 실행되고 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;CPU

&amp;darr;
Process A 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제가 Process B를 실행하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 대략 다음 과정이 발생한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Process A 실행

&amp;darr;

Process A 상태 저장

&amp;darr;

Process B 상태 불러오기

&amp;darr;

Process B 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 자세히 표현하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;CPU
 │
 │ Process A 실행
 ▼

[Process A Context 저장]

Program Counter
Register
Stack Pointer
...

        &amp;darr;

[Process B Context 복원]

Program Counter
Register
Stack Pointer
...

        &amp;darr;

Process B 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전체 과정이 Context Switching이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context는 어디에 저장할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스의 실행 상태는 운영체제가 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제는 각 프로세스에 대한 정보를 관리하기 위해 &lt;b&gt;PCB(Process Control Block)&lt;/b&gt;라는 자료구조를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PCB에는 보통 다음과 같은 정보가 들어간다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Process ID
Process State
Program Counter
CPU Registers
Scheduling Information
Memory Information&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching이 발생하면 현재 프로세스의 실행 상태를 PCB 등에 저장하고, 다음에 실행할 프로세스의 상태를 복원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 PCB는 프로세스 스케줄링과 Context Switching에서 중요한 역할을 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context Switching은 언제 발생할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 상황은 여러 가지가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU 사용 시간이 끝났을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제 스케줄러가 Process A에 일정 시간 동안 CPU를 할당했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Process A

CPU 사용

&amp;darr;

Time Slice 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다음 프로세스로 전환할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Process A
   &amp;darr;
Process B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Context Switching이 발생한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;I/O 작업을 기다릴 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Process A가 파일을 읽거나 네트워크 응답을 기다린다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Process A

&amp;darr;

Network 요청

&amp;darr;

응답 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 입장에서 응답을 기다리면서 아무것도 하지 않는 것은 낭비다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 운영체제는 다른 작업을 실행한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Process A
I/O 대기

&amp;darr;

Process B 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때도 실행 대상이 변경된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더 높은 우선순위 작업이 들어왔을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 낮은 우선순위 작업이 실행되고 있는데 더 높은 우선순위 작업이 실행 가능한 상태가 될 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Low Priority Process 실행

&amp;darr;

High Priority Process 등장

&amp;darr;

실행 대상 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에도 Context Switching이 발생할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context Switching은 공짜가 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 면접에서 중요한 부분이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching 자체는 실제 애플리케이션의 비즈니스 로직을 처리하는 작업이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버가 해야 하는 실제 작업이&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;주문 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching 동안에는 주문을 처리하는 것이 아니라&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 상태 저장

&amp;darr;

다음 상태 복원&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Context Switching이 너무 자주 발생하면 CPU가 실제 작업보다 작업 전환에 많은 시간을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 비용을 &lt;b&gt;Context Switching Overhead&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스와 스레드 중 어느 쪽이 더 비쌀까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서 자주 나오는 질문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로세스 간 Context Switching이 스레드 간 Context Switching보다 비용이 크다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 설명할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그런지 프로세스와 스레드 구조를 생각해보면 이해하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스는 각각 독립적인 메모리 공간을 가진다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Process A

Code
Data
Heap
Stack&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Process B

Code
Data
Heap
Stack&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Process A에서 Process B로 전환하면 서로 다른 주소 공간을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 메모리 관리와 관련된 추가 작업이 발생할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드는 메모리를 공유한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 프로세스 내부의 Thread들은 Code, Data, Heap 영역 등을 공유한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Process

Code
Data
Heap
────────────────────

Thread A Stack
Thread B Stack
Thread C Stack&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread마다 별도의 Stack과 Register 상태 등을 가지지만 많은 프로세스 자원을 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 같은 프로세스 내부의 Thread A에서 Thread B로 전환하는 경우 프로세스 자체를 전환하는 것보다 일반적으로 처리해야 할 작업이 적다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Process Switching

A Process
   &amp;darr;
B Process

비용 상대적으로 큼&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Thread Switching

Thread A
   &amp;darr;
Thread B

비용 상대적으로 작음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 스레드를 &lt;b&gt;Lightweight Process&lt;/b&gt;라고 표현하기도 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 스레드는 많을수록 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 실무에서도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 요청 하나당 하나의 Thread를 사용한다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 많아지면서 Thread를 계속 늘린다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100 Threads

&amp;darr;

1,000 Threads

&amp;darr;

10,000 Threads&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread가 많아지면 동시에 많은 작업을 처리할 수 있을 것처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 CPU Core 수는 제한되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 4 Core CPU에서 실행 가능한 Thread가 1,000개라면 CPU가 계속 실행 Thread를 변경해야 한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Thread 1
   &amp;darr;
Thread 200
   &amp;darr;
Thread 17
   &amp;darr;
Thread 843
   &amp;darr;
Thread 32
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 Context Switching이 매우 많이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 사용률은 높은데 실제 처리량은 오히려 떨어지는 상황도 생길 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Thread가 많으면 메모리도 사용한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread에는 각각 Stack 영역이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Thread A
└── Stack

Thread B
└── Stack

Thread C
└── Stack&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Thread를 무작정 늘리면 Context Switching뿐 아니라 메모리 사용량도 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 서버를 운영할 때&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker나 Thread를 많이 설정하면 무조건 성능이 좋아진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Core, 작업 특성, I/O 비율, 메모리 등을 같이 고려해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU Bound와 I/O Bound&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching과 Thread 수를 이야기할 때 &lt;b&gt;CPU Bound와 I/O Bound&lt;/b&gt;도 같이 알아두면 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU Bound&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 계산을 많이 사용하는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이미지 처리
영상 인코딩
암호화
대규모 계산
데이터 압축&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU가 계속 일을 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 작업은 CPU Core 수보다 지나치게 많은 실행 단위를 만들어도 성능 향상이 크지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 Context Switching 비용이 증가할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;I/O Bound&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 계산보다 외부 작업을 기다리는 시간이 많은 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;DB 조회
외부 API 호출
파일 읽기
네트워크 통신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 DB 조회에 100ms가 걸린다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;CPU 작업

&amp;darr;

DB 요청

&amp;darr;

100ms 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 100ms 동안 CPU는 다른 작업을 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 I/O Bound 작업에서는 여러 Thread나 비동기 처리를 활용해 CPU 대기 시간을 효율적으로 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기 I/O가 등장하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread가 너무 많아지면 Context Switching과 메모리 비용이 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Node.js나 Python의 asyncio 같은 환경에서는 &lt;b&gt;Event Loop 기반의 비동기 I/O&lt;/b&gt;를 활용하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 Thread 방식이 다음과 같다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Request A &amp;rarr; Thread A
Request B &amp;rarr; Thread B
Request C &amp;rarr; Thread C
Request D &amp;rarr; Thread D&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 I/O에서는 I/O 작업을 기다리는 동안 다른 작업을 처리한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Request A &amp;rarr; DB 대기
               &amp;darr;
Request B 처리
               &amp;darr;
Request C 처리
               &amp;darr;
A DB 응답 도착
               &amp;darr;
Request A 계속 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O 작업이 많은 서버에서는 많은 Thread를 만들지 않고도 높은 동시성을 처리할 수 있는 이유다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 비동기 방식이라고 Context Switching이 완전히 사라지는 것은 아니고, 운영체제와 런타임 수준에서 다양한 스케줄링과 작업 전환이 존재한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context Switching과 Cache도 관계가 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching 비용은 단순히 Register를 저장하고 복원하는 시간만 있는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 성능을 높이기 위해 Cache를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 실행 중인 작업의 데이터를 Cache에 올려두고 빠르게 접근한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CPU

&amp;darr;

L1 Cache
L2 Cache
L3 Cache

&amp;darr;

Memory&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실행 중인 작업이 변경되면 새로운 작업에서 필요한 데이터가 기존 Cache에 없을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Process A 데이터
&amp;rarr; Cache

Context Switch

Process B 실행
&amp;rarr; 필요한 데이터 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Memory에서 다시 데이터를 가져와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 &lt;b&gt;Cache Miss&lt;/b&gt;도 실제 Context Switching 비용에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Context Switching 비용을 단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Register 저장하고 가져오는 비용&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로만 이해하면 부족하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 Context Switching이 더 무거운 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 프로세스 간 전환이 일반적으로 더 무겁다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 이유를 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;프로세스 A
    &amp;darr;
프로세스 B

CPU 상태 변경
+
주소 공간 변경
+
메모리 관련 상태 변경
+
Cache / TLB 영향&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 같은 프로세스 내부 Thread는 주소 공간을 공유한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Thread A
    &amp;darr;
Thread B

CPU 실행 상태 변경

주소 공간 공유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일반적으로 Thread Context Switching 비용이 상대적으로 작다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TLB는 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 조금 더 깊게 들어가면 &lt;b&gt;TLB(Translation Lookaside Buffer)&lt;/b&gt;가 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU는 Virtual Address를 Physical Address로 변환해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 Page Table을 조회하면 느리기 때문에 최근 주소 변환 결과를 TLB라는 Cache에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Virtual Address

&amp;darr;

TLB

&amp;darr;

Physical Address&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스가 변경되면 주소 공간도 달라질 수 있기 때문에 TLB 효율에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서 Context Switching을 깊게 물어본다면 Cache와 TLB까지 연결해서 설명할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 일반적인 백엔드 개발 면접에서는 여기까지 먼저 이야기할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문이 깊어졌을 때 추가로 설명하면 충분하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서는 어디에서 체감할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자가 Context Switching을 직접 구현할 일은 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버 설정을 하다 보면 간접적으로 자주 만나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 설정이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;PHP-FPM Worker
Nginx Worker
Gunicorn Worker
Uvicorn Worker
Java Thread Pool
Laravel Queue Worker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 늘리면 처리량이 증가할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 무작정 늘리면&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Worker 증가
    &amp;darr;
실행 작업 증가
    &amp;darr;
Context Switching 증가
    &amp;darr;
Memory 증가
    &amp;darr;
성능 저하 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Worker 수는 서버 CPU와 Memory, 작업 특성을 보면서 조정해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;면접에서는 어떻게 대답하면 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Context Switching이 무엇인가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 질문한다면 다음처럼 답할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching은 CPU가 현재 실행 중인 프로세스나 스레드의 상태를 저장하고, 다음에 실행할 작업의 상태를 복원해서 실행 대상을 전환하는 과정입니다. 운영체제의 멀티태스킹을 위해 필요한 과정이지만 상태 저장과 복원, 캐시나 메모리 관련 비용이 발생하기 때문에 너무 빈번하면 성능 저하가 발생할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;프로세스와 스레드 중 어느 쪽의 Context Switching 비용이 더 큰가요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 물어본다면 다음처럼 이어갈 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 프로세스 간 Context Switching 비용이 더 큽니다. 프로세스는 독립적인 주소 공간을 가지기 때문에 실행 상태뿐 아니라 메모리 주소 공간 전환 등의 추가 비용이 발생할 수 있습니다. 반면 같은 프로세스의 스레드는 Code, Data, Heap 등의 주소 공간을 공유하기 때문에 상대적으로 전환 비용이 작습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도가 기본 답변으로 적당하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;꼬리 질문으로 나올 만한 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;Context Switching은 왜 필요한가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한정된 CPU 자원을 여러 프로세스와 스레드가 나눠 사용하면서 멀티태스킹을 제공하기 위해 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;Context Switching이 많으면 왜 성능이 떨어지나요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 저장과 복원 자체에 비용이 발생하고 CPU Cache나 TLB 효율에도 영향을 줄 수 있기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;스레드 Context Switching이 항상 프로세스보다 빠른가요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 같은 프로세스 내부 스레드 전환이 더 가볍지만 실제 비용은 운영체제, CPU, 스케줄링 상황 등에 따라 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서는 절대적인 표현보다는 &lt;b&gt;일반적으로 비용이 더 작다&lt;/b&gt;고 설명하는 게 안전하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;Thread를 많이 만들면 성능이 좋아지나요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 그렇지는 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread가 너무 많아지면 Context Switching과 메모리 사용량이 증가해서 오히려 처리량이 감소할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;CPU Bound 작업에서 Thread를 많이 만들면 왜 효과가 적나요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 자체가 계속 사용되고 있기 때문에 실행할 Thread만 늘린다고 CPU 자원이 늘어나는 것은 아니다. 오히려 스케줄링과 Context Switching 비용이 증가할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;I/O Bound에서는 왜 여러 Thread가 효과적일 수 있나요?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 Thread가 DB나 네트워크 I/O를 기다리는 동안 CPU가 다른 Thread를 실행할 수 있기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context Switching의 핵심 흐름은 간단하다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Process A 실행
     &amp;darr;
상태 저장
     &amp;darr;
Process B 상태 복원
     &amp;darr;
Process B 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 단순한 실행 대상 변경 이상의 비용이 발생할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Context Switching

CPU Register 저장/복원
+
Scheduling
+
Cache 영향
+
TLB 영향
+
메모리 관련 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 일반적으로&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Process Context Switching
        &amp;darr;
비용 상대적으로 큼

Thread Context Switching
        &amp;darr;
비용 상대적으로 작음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 면접에서는 정의만 외우기보다 &lt;b&gt;왜 Context Switching에 비용이 발생하는지, 프로세스와 스레드는 왜 비용 차이가 있는지, Thread를 무작정 늘리면 왜 문제가 되는지&lt;/b&gt;까지 연결해서 설명할 수 있으면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 백엔드 개발자라면 Worker나 Thread 수를 설정할 때도 단순히 숫자를 늘리는 것보다 CPU Bound인지 I/O Bound인지, 서버의 Core와 Memory는 얼마나 되는지까지 함께 생각해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Context Switching은 운영체제 이론으로 끝나는 개념이 아니라 &lt;b&gt;실제 서버의 동시성과 성능을 이해하기 위한 기본 개념&lt;/b&gt;이라고 볼 수 있다.&lt;/p&gt;</description>
      <category>개발/개발면접</category>
      <category>ContextSwitching</category>
      <category>CPU</category>
      <category>CS</category>
      <category>CS면접</category>
      <category>개발면접</category>
      <category>멀티태스킹</category>
      <category>백엔드개발</category>
      <category>스레드</category>
      <category>운영체제</category>
      <category>프로세스</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/179</guid>
      <comments>https://dev-lee-6.tistory.com/entry/%EA%B0%9C%EB%B0%9C-%EB%A9%B4%EC%A0%91-Context-Switching-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#entry179comment</comments>
      <pubDate>Thu, 20 Aug 2026 08:56:45 +0900</pubDate>
    </item>
    <item>
      <title>Laravel Queue Worker 수는 어떻게 결정해야 할까?</title>
      <link>https://dev-lee-6.tistory.com/entry/Laravel-Queue-Worker-%EC%88%98%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B2%B0%EC%A0%95%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue를 운영하다 보면 결국 한 번쯤 이런 고민을 하게 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker를 몇 개 띄워야 하지?

1개면 부족한가?

5개면 충분한가?

10개로 늘리면 더 빨라질까?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Queue가 밀리기 시작하면 Worker 수부터 늘리고 싶어진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Queue 대기

100건

&amp;darr;

Worker 2개

&amp;darr;

처리 느림

&amp;darr;

Worker 10개로 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보면 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 많아지면 동시에 처리하는 Job 수도 늘어나기 때문에 Queue가 더 빨리 줄어들 것처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 운영에서는 Worker를 많이 늘린다고 무조건 좋은 것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CPU 부족

메모리 증가

DB Connection 증가

Slow Query 동시 실행

외부 API Rate Limit 초과

Redis 부하 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Queue Job이 DB나 외부 API에 많이 의존한다면 Worker 수를 무작정 늘리는 것이 오히려 전체 서비스 성능을 떨어뜨릴 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Worker 수를 정할 때는 단순히&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Queue가 많이 쌓였다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 볼 것이 아니라 &lt;b&gt;Job 처리 시간, 서버 리소스, DB 부하, Queue 유입량&lt;/b&gt;을 같이 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Laravel Queue Worker 수를 어떤 기준으로 정하면 좋은지, Supervisor의 &lt;code&gt;numprocs&lt;/code&gt;를 어떻게 조정하면 되는지 실무 관점에서 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker란 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue Worker는 Queue에 쌓인 Job을 가져와 실행하는 프로세스다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;php artisan queue:work&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 하나가 실행되고 있다면 기본적으로 하나의 Job을 처리한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Worker 1

&amp;darr;

Job A

&amp;darr;

Job B

&amp;darr;

Job C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 여러 개 실행하면 여러 Job을 동시에 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Worker 1

&amp;rarr; Job A


Worker 2

&amp;rarr; Job B


Worker 3

&amp;rarr; Job C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Worker 수를 늘리면 병렬 처리량을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker를 늘리면 얼마나 빨라질까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 경우를 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 하나가 평균 10초 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 1개라면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1분

&amp;darr;

약 6개 Job 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 5개라면 이상적으로는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1분

&amp;darr;

약 30개 Job 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Queue가 많이 쌓이는 서비스에서는 Worker를 늘리는 것이 효과가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 계산은 모든 Job이 서로 영향을 주지 않는다는 가정이 있을 때만 맞다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제로는 병목이 따로 있을 수 있다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 모든 Job이 MySQL에서 무거운 Query를 실행한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 1개 사용할 때는 괜찮다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Worker 1

&amp;darr;

Slow Query

&amp;darr;

MySQL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Worker를 10개로 늘린다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 1 ─┐
Worker 2 ─┤
Worker 3 ─┤
Worker 4 ─┤
...       ├&amp;rarr; MySQL
Worker 10 ┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 무거운 Query가 동시에 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DB CPU 증가

Lock 증가

Connection 증가

Query 실행 시간 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 10배 늘렸는데 오히려 각 Job의 실행 시간이 더 길어질 수도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU Bound Job과 I/O Bound Job을 구분하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수를 정할 때 Job이 어떤 종류인지 보는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 두 가지로 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;CPU Bound

I/O Bound&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU Bound Job이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 계산을 많이 사용하는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이미지 처리

영상 처리

압축

암호화

복잡한 데이터 계산&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 사용량이 핵심 병목이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 2 Core인데 CPU를 많이 사용하는 Worker를 30개 실행하면 오히려 성능이 떨어질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Worker 30개

&amp;darr;

CPU 100%

&amp;darr;

Context Switching 증가

&amp;darr;

전체 처리 속도 저하&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU Bound 작업에서는 Worker 수를 무작정 늘리기 어렵다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;I/O Bound Job이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 시간 대부분을 외부 응답을 기다리는 데 사용하는 Job이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;외부 API 호출

DB Query

S3 업로드

메일 발송

문자 발송&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 문자 발송 Job이 2초 걸리는데 실제 CPU를 사용하는 시간은 0.1초일 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;API 요청

&amp;darr;

1.8초 대기

&amp;darr;

응답 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 작업은 CPU Bound Job보다 Worker를 조금 더 늘릴 여지가 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 외부 API의 Rate Limit이나 DB Connection은 별도로 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 CPU부터 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수를 늘릴 때 가장 기본적으로 볼 것은 CPU다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Worker가 2개인데 평소 CPU가&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;20%&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도라면 여유가 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Worker 2개만으로도&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CPU

80 ~ 90%&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용하고 있다면 Worker를 더 늘리는 것은 위험할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영에서는 평균 CPU뿐 아니라 Peak 시간대도 확인하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리도 중요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Worker는 PHP 프로세스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 하나가 메모리를 200MB 사용한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 1개

200MB


Worker 5개

1GB


Worker 10개

2GB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버에 사용 가능한 메모리가 2GB밖에 없다면 Worker 10개는 현실적으로 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 대용량 엑셀이나 PDF 생성 Job은 Worker 하나가 수백 MB 이상의 메모리를 사용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Queue Worker 수를 정할 때는 &lt;b&gt;Job 실행 중 최대 메모리 사용량&lt;/b&gt;을 같이 보는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Supervisor numprocs가 바로 Worker 수다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supervisor를 사용한다면 보통 다음과 같은 설정이 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[program:laravel-worker]

command=php /var/www/html/artisan queue:work \
    --queue=default \
    --sleep=3 \
    --tries=3 \
    --timeout=60

numprocs=3

autostart=true
autorestart=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;numprocs=3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 Worker 프로세스를 3개 실행하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;laravel-worker_00

laravel-worker_01

laravel-worker_02&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 여러 Worker가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue마다 Worker 수를 다르게 설정할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 Queue를 목적별로 분리하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;notification

export

batch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 있다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supervisor 설정도 따로 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;notification

Worker 4개


export

Worker 2개


batch

Worker 1개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Queue의 특성에 따라 Worker 수를 다르게 가져가는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 운영에서는 Queue를 분리해서 Worker를 관리했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 서비스를 운영하면서 모든 Job을 하나의 Queue에서 처리하기보다 작업 성격에 따라 Queue를 분리해서 운영한 적이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 일반적인 비동기 작업과 알림성 작업을 같은 Queue에서 처리하면 한쪽에 무거운 Job이 쌓였을 때 다른 작업까지 영향을 받을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 처리 시간이 긴 Job이 먼저 Queue에 많이 들어오면 뒤에 들어온 알림성 Job도 같이 기다리게 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;default Queue

무거운 Job
무거운 Job
무거운 Job
알림 Job
알림 Job&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 Job 자체는 몇 초 안에 끝날 수 있어도 앞에 있는 Job 때문에 실제 처리가 늦어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 작업 성격이 다른 경우에는 Queue를 분리하고 Supervisor Worker도 각각 관리하는 방식을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;default Queue
&amp;rarr; 일반적인 비동기 작업

notification Queue
&amp;rarr; 알림성 작업&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Supervisor에서도 각각 별도의 Worker를 실행하도록 구성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[program:worker-default]
command=php /var/www/html/artisan queue:work --queue=default
numprocs=2

[program:worker-notification]
command=php /var/www/html/artisan queue:work --queue=notification
numprocs=2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 Worker 수를 조정할 때도 전체 Worker를 한꺼번에 늘릴 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 Queue가 자주 밀린다면 알림 Worker만 조정하고, 대량 처리가 필요한 Queue라면 해당 Queue의 동시성을 낮게 유지하는 식으로 대응할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영하면서 느낀 점은 &lt;b&gt;Worker 수를 몇 개로 할 것인가보다 먼저 어떤 종류의 Job을 같은 Worker가 처리하게 할 것인가를 정하는 것이 중요하다는 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue가 분리되어 있어야 이후에 Worker 수도 작업 특성에 맞게 조정하기 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Notification Worker는 많아도 괜찮을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notification Job은 대부분 실행 시간이 짧다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;SMS

Email

Push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 외부 서비스 요청을 사용하기 때문에 Rate Limit을 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 SMS 업체가&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;초당 최대 10건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 허용한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 50개 실행해 동시에 요청하면&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;429 Too Many Requests&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수를 결정할 때는 우리 서버 성능뿐 아니라 상대 시스템의 처리 한도도 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Export Worker는 적게 운영하는 편이 나을 수도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 Excel Job을 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 Job에서&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;DB 조회

&amp;darr;

데이터 가공

&amp;darr;

Excel 생성

&amp;darr;

파일 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리와 CPU를 많이 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 Queue는 Worker 수를 적게 유지하는 편이 안정적일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;notification

Worker 5


export

Worker 1~2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 작업별 특성을 반영할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Batch Worker도 무조건 많이 두면 안 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량 Batch는 DB를 많이 사용하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Worker 10개가 동시에 각각 10만 건씩 처리한다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;총 100만 건

&amp;darr;

동시에 대량 Query

&amp;darr;

MySQL 부하 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch 자체는 빠르게 처리하고 싶지만 DB가 감당하지 못하면 웹 API까지 느려질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 서비스라면 Batch 처리 속도보다 사용자 요청 성능을 우선해야 하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB Connection 수도 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 하나가 실행될 때 DB Connection을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Web PHP Process

50개


Queue Worker

20개


Scheduler

일부&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 모두 같은 MySQL에 연결된다면 Connection 수가 빠르게 늘어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 &lt;code&gt;max_connections&lt;/code&gt;만 늘리는 것이 항상 해결책은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection이 많아질수록 DB 리소스 사용량도 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Worker를 늘릴 때는 애플리케이션 전체 Connection 사용량을 같이 보는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis Connection도 확인할 필요가 있다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Queue를 사용한다면 Worker가 Redis와 통신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수가 늘면&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue Pop

Queue Delete

Retry

Cache Lock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Redis 작업도 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 규모에서는 큰 문제가 없을 수 있지만 대량 Worker 환경에서는 Redis CPU와 Connection도 관찰하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처리량을 계산해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수를 감으로 정하기보다 간단하게 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job 하나의 평균 실행 시간이&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;5초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 하나의 처리량은 대략&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;60 / 5

=

12 Job / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 5개라면 이론적으로&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;12 &amp;times; 5

=

60 Job / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도를 처리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue 유입량과 비교한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job이 들어오는 속도가&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;30 Job / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인데 처리량이&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;60 Job / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 Worker가 충분할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;유입

100 Job / 분


처리

60 Job / 분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 Queue가 계속 쌓인다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1분

40개 증가


10분

400개 증가


1시간

2,400개 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 Worker 수나 Job 처리 성능을 개선해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순 공식으로 생각해볼 수도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적인 출발점으로는 다음 관계를 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;필요 Worker 수

&amp;asymp;

초당 Job 유입량

&amp;times;

평균 Job 실행 시간&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;초당 2 Job 유입

평균 실행 시간 3초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;2 &amp;times; 3

=

6 Worker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도가 이론적인 기준이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 운영에서는 Peak 트래픽과 장애 상황을 고려해 여유를 둬야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;평균만 보면 안 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 실행 시간이 3초라고 해도 실제 분포가 다음과 같을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;대부분

1초


일부

5초


가끔

40초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;40초짜리 Job이 Worker를 장시간 점유하면 Queue 지연이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 가능하다면&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Average

P95

P99

Max&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 실행 시간 분포를 같이 보는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue Depth만 보고 Worker를 늘리지 않는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue에 5,000개의 Job이 쌓여 있다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀라서 Worker를&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2개

&amp;darr;

20개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바로 늘리고 싶을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 먼저 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;갑자기 Batch Job이 등록된 것인가?

평소에도 계속 밀리는가?

Job 실행 시간이 길어진 것인가?

외부 API 장애인가?

DB가 느려진 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일시적으로 Job이 많이 들어온 것이라면 현재 Worker로도 곧 해소될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Queue 대기 시간이 더 중요할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue Depth가 10,000이어도 Job이 0.01초짜리라면 빠르게 처리될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Queue에 20개밖에 없어도 Job 하나가 5분씩 걸린다면 대기 시간이 매우 길어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 운영에서 확인하고 싶은 것은&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Queue에 몇 개가 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐 아니라&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;가장 오래된 Job은 몇 초 기다리고 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중요한 Queue는 대기 시간 기준을 다르게 잡는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;notification

최대 대기 허용

10초


export

최대 대기 허용

5분


batch

최대 대기 허용

30분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 서비스 요구사항이 다를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수 역시 이 목표를 만족하도록 정하는 편이 더 합리적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker Auto Scaling은 필요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽 변화가 크다면 Worker를 자동으로 늘리고 줄이는 구조도 고려할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;평상시

Worker 2개


Queue 증가

&amp;darr;

Worker 10개


Queue 감소

&amp;darr;

Worker 2개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 운영할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단일 EC2 + Supervisor 구조에서는 자동 확장까지 구현하면 관리 복잡도가 크게 올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 일정하다면 고정 Worker 수로 운영해도 충분한 경우가 많다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 Auto Scaling을 고려할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 상황이 반복된다면 검토해볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;트래픽 편차가 매우 큼

특정 시간에 Queue가 급격하게 쌓임

Worker를 항상 많이 띄워두는 비용이 큼

수평 확장이 가능한 구조

Redis / SQS 기반 Queue 운영&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 규모가 커진 뒤에 고려해도 늦지 않다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Horizon을 사용하면 Worker 관리가 편해질 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Queue를 사용한다면 Laravel Horizon을 이용해 Worker 프로세스를 관리하는 방법도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Queue별 Worker를 운영할 때&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Queue 처리량

Runtime

Failed Job

Worker 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 확인하고 Balance 전략을 사용할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 현재 Queue 규모가 작다면 Supervisor만으로도 충분할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker 수보다 Job 자체를 먼저 최적화해야 할 수도 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job 하나가 60초 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2개

&amp;darr;

10개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 늘리는 것보다 Query를 개선해서&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;60초

&amp;darr;

5초&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 줄이는 것이 훨씬 큰 개선일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 N+1이나 Slow Query가 있다면 Worker 수를 늘리기 전에 먼저 해결하는 편이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue Worker를 늘리는 것은 Scale Out이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 늘리는 것은 처리 리소스를 추가하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;성능이 느림

&amp;darr;

리소스 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 먼저 코드와 Query에 비효율이 없는지도 봐야 한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;N+1

불필요한 API 호출

전체 데이터 get()

한 건씩 INSERT

느린 Index&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 문제를 그대로 두고 Worker만 늘리면 비용만 증가할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하나의 Job을 여러 Job으로 나누는 것도 고려한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 부족한 것이 아니라 Job 하나가 너무 오래 걸리는 문제일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100만 건

&amp;darr;

Job 1개

&amp;darr;

20분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 Worker가 20개 있어도 해당 Job은 Worker 하나만 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10만 건 &amp;times; 10 Job&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 분할해야 병렬 처리가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Worker 수 증가

+

Job 분할&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함께 필요한 경우도 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker를 늘리기 전에 Job이 병렬 실행 가능한지도 확인한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Job을 병렬로 실행할 수 있는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 고객의 잔액을 수정하는 Job을 여러 Worker가 동시에 처리하면 Race Condition이 발생할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Worker A

Customer 100 수정


Worker B

Customer 100 수정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 작업에서는&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WithoutOverlapping

Redis Lock

DB Row Lock

Idempotency&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 동시성 제어가 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 늘리면 기존에 보이지 않던 동시성 문제가 드러나기도 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 Worker를 늘릴 때는 조금씩 늘린다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 Worker가 2개라면&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2

&amp;darr;

4

&amp;darr;

6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 단계적으로 늘리면서 관찰하는 편이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계에서&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue 대기 시간

CPU

Memory

DB CPU

DB Connection

Job 실행 시간

실패율&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 2개에서 30개로 늘리면 어느 지점부터 문제가 생겼는지 확인하기 어렵다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Worker 수를 줄여야 하는 신호도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker를 늘렸는데 다음 현상이 나타난다면 오히려 줄여야 할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;DB CPU 급증

Job 실행 시간 증가

Deadlock 증가

외부 API 429 증가

서버 Swap 발생

OOM 발생

웹 API 응답 속도 저하&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 처리 속도 하나를 높이기 위해 서비스 전체를 느리게 만드는 것은 좋은 선택이 아니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹 서버와 Queue 서버를 분리할 수도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 커지면 웹 요청과 Queue Worker를 같은 서버에서 운영하지 않을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Web Server

&amp;darr;

API 처리


Worker Server

&amp;darr;

Queue 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 분리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량 Batch가 CPU와 메모리를 많이 사용해도 웹 API에 미치는 영향을 줄일 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처음부터 서버를 분리할 필요는 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 많지 않다면 하나의 서버에서도 충분히 운영할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;API

Nginx

PHP-FPM

Queue Worker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 함께 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 실제 리소스 사용량을 보고 확장하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 미리 복잡하게 만드는 것보다 병목이 확인되었을 때 분리하는 편이 현실적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Worker 수를 결정하는 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 보통 다음 순서로 확인하는 편이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. Queue가 실제로 밀리고 있는가?

&amp;darr;

2. 평균 / P95 Job 실행 시간은?

&amp;darr;

3. Job 유입량은?

&amp;darr;

4. 현재 Worker 처리량은?

&amp;darr;

5. CPU와 Memory 여유는?

&amp;darr;

6. DB 부하는?

&amp;darr;

7. 외부 API Rate Limit은?

&amp;darr;

8. Worker를 늘려도 동시성 문제가 없는가?

&amp;darr;

9. Worker를 조금 늘려서 다시 측정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자를 먼저 정하기보다 실제 측정값으로 결정하는 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue가 밀렸다고 바로 Worker부터 늘리지는 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영에서는 Queue가 쌓이는 것을 발견했다고 바로 &lt;code&gt;numprocs&lt;/code&gt;부터 올리지는 않는 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Queue가 밀리기 시작한 시점과 원인을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 평소에는 문제가 없었는데 갑자기 Queue가 쌓였다면 Worker 자체가 부족한 것이 아니라 특정 Job의 처리 시간이 길어진 것일 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;평소

Job 1건 &amp;rarr; 2초

&amp;darr;

특정 시점

Job 1건 &amp;rarr; 15초

&amp;darr;

Queue 대기 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황에서 Worker만 늘리면 원인을 해결하는 것이 아니라 느려진 Job을 동시에 더 많이 실행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 원인이 DB Query라면 상황이 더 나빠질 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Slow Query 발생

&amp;darr;

Job 처리시간 증가

&amp;darr;

Queue 적체

&amp;darr;

Worker 증가

&amp;darr;

Slow Query 동시 실행 증가

&amp;darr;

DB 부하 증가

&amp;darr;

Job이 더 느려짐&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 먼저 로그를 통해 특정 Job의 처리 시간이 이전보다 증가했는지 확인하고, DB를 많이 사용하는 작업이라면 Slow Query와 DB CPU도 같이 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 사용하는 Job이라면 상대 API의 응답 시간이 느려졌거나 오류가 증가하지 않았는지도 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 거쳤는데도 개별 Job의 처리 시간은 정상이고 단순히 Job 유입량 자체가 증가한 것이라면 그때 Worker 증가를 검토한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Queue 적체 확인
        &amp;darr;
Job 처리시간 확인
        &amp;darr;
DB / 외부 API 상태 확인
        &amp;darr;
특별한 병목 없음
        &amp;darr;
Job 유입량 증가 확인
        &amp;darr;
Worker 소폭 증가
        &amp;darr;
Queue 대기시간 다시 측정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 접근하면 단순히 &lt;code&gt;Queue가 쌓였으니 Worker를 늘린다&lt;/code&gt;가 아니라 &lt;b&gt;Queue가 왜 쌓였는지 확인한 뒤 필요한 경우에만 동시성을 높일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영에서는 Worker 개수 자체보다 이 판단 과정이 더 중요하다고 느꼈다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예를 들어 계산해보자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 Notification Queue가 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;평균 Job 실행 시간

2초


Peak Job 유입량

초당 3개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로 필요한 Worker 수는&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;3 &amp;times; 2

=

6개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Peak 상황에서 여유를 두어 7~8개를 테스트해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 외부 문자 업체의 Rate Limit이&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;초당 5회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 Worker를 8개로 늘리는 것이 적절하지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 외부 제한까지 같이 봐야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Batch Queue는 반대로 계산이 달라질 수 있다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch Job 하나가 평균 30초 걸리고 한 시간에 100개가 등록된다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 유입은 낮다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 Worker를 20개 띄워둘 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 2~3개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만으로도 충분할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 DB를 많이 사용하는 Job이라면 낮은 동시성을 유지하는 것이 오히려 안정적일 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Queue마다 Worker 수를 따로 생각한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 전체 Queue에&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 10개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각하기보다&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;default

2개


notification

5개


export

2개


batch

1개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 각각의 업무 특성에 따라 결정하는 편이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue를 목적별로 분리하는 이유가 여기에서도 드러난다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그에 처리 시간을 남겨두면 도움이 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker 수를 조정하려면 데이터가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Job마다 다음을 남길 수 있다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;$startedAt = microtime(true);

try {
    $this-&amp;gt;handleProcess();
} finally {
    Log::info(
        'Queue job completed',
        [
            'job' =&amp;gt; static::class,

            'duration_ms' =&amp;gt; round(
                (
                    microtime(true)
                    - $startedAt
                ) * 1000
            ),

            'attempt' =&amp;gt; $this-&amp;gt;attempts(),
        ]
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 로그가 쌓이면 Job별 평균 처리 시간을 계산할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순한 체크리스트로 정리하면&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Worker를 늘리기 전

&amp;darr;

Queue 대기 확인

Job 처리 시간 확인

CPU 확인

Memory 확인

DB 확인

외부 API 제한 확인


Worker 증가

&amp;darr;

조금씩 증가


증가 후

&amp;darr;

Queue 대기 감소?

CPU 괜찮음?

DB 괜찮음?

실패율 증가 없음?

Job 실행 시간 증가 없음?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 반복해서 적절한 지점을 찾는 것이 가장 현실적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 체크리스트&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;□ Queue가 실제로 지속적으로 밀리고 있는가?

□ Queue별 Job 유입량을 알고 있는가?

□ 평균 Job 실행 시간을 알고 있는가?

□ P95 또는 오래 걸리는 Job을 확인했는가?

□ 현재 Worker 처리량을 알고 있는가?

□ 서버 CPU에 여유가 있는가?

□ Worker당 메모리 사용량을 알고 있는가?

□ DB CPU와 Connection이 충분한가?

□ 외부 API Rate Limit이 있는가?

□ Worker를 늘리면 같은 데이터를 동시에 수정하지 않는가?

□ Queue별 Worker를 따로 운영해야 하는가?

□ 긴 Job을 더 작은 Job으로 나눌 수 있는가?

□ Worker를 늘리기 전에 Slow Query를 확인했는가?

□ 한 번에 Worker를 너무 많이 늘리고 있지 않은가?

□ Worker 증가 후 Queue 대기 시간이 실제로 줄었는가?

□ 웹 API 성능에 영향을 주고 있지 않은가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel Queue Worker 수에는 정답이 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CPU 4 Core니까 Worker 4개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 단순하게 결정할 수도 없고,&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;많으면 빠르겠지.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 무조건 늘리는 것도 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job마다 특성이 다르기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;CPU를 많이 사용하는 Job

DB를 많이 사용하는 Job

외부 API를 기다리는 Job

메모리를 많이 사용하는 Job&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 적절한 동시성이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 Worker 수를 정할 때 먼저 다음 세 가지를 보는 편이다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Job이 얼마나 자주 들어오는가?

Job 하나가 얼마나 오래 걸리는가?

동시에 많이 실행했을 때 어디가 병목이 되는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 실제 서버와 DB 상태를 확인하면서 Worker를 조금씩 늘린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 실제 서비스에서는 Queue만 단독으로 존재하는 것이 아니라 웹 요청과 같은 DB, Redis, 서버 리소스를 공유하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Queue 처리 속도만 보면 Worker를 더 늘리는 것이 좋아 보여도, API 응답 속도나 DB 부하까지 같이 보면 다른 판단을 하게 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 내가 중요하게 보는 기준은 &lt;b&gt;Queue를 가장 빨리 비우는 설정이 아니라 서비스 전체가 안정적인 상태에서 필요한 시간 안에 Queue를 처리할 수 있는 설정인가&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue 운영에서 중요한 것은 &lt;b&gt;가장 많은 Worker를 실행하는 것&lt;/b&gt;이 아니라, 사용자 요청과 DB에 영향을 주지 않으면서 Queue가 원하는 시간 안에 처리되도록 적절한 동시성을 유지하는 것이라고 생각한다.&lt;/p&gt;</description>
      <category>개발/Laravel</category>
      <category>backend</category>
      <category>Laravel</category>
      <category>LaravelQueue</category>
      <category>php</category>
      <category>Queue</category>
      <category>QueueWorker</category>
      <category>redis</category>
      <category>supervisor</category>
      <category>비동기처리</category>
      <category>성능최적화</category>
      <author>열심운영자</author>
      <guid isPermaLink="true">https://dev-lee-6.tistory.com/177</guid>
      <comments>https://dev-lee-6.tistory.com/entry/Laravel-Queue-Worker-%EC%88%98%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B2%B0%EC%A0%95%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C#entry177comment</comments>
      <pubDate>Wed, 19 Aug 2026 17:19:47 +0900</pubDate>
    </item>
  </channel>
</rss>