<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Aleph Kim의 IT 블로그</title>
    <link>https://dev-kimchi.tistory.com/</link>
    <description>에러 수집가 Aleph Kim의 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 1 Jul 2026 13:52:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Aleph Kim</managingEditor>
    <image>
      <title>Aleph Kim의 IT 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/5268597/attach/f084f280d60c41da8f7c3f021192ee17</url>
      <link>https://dev-kimchi.tistory.com</link>
    </image>
    <item>
      <title>Laravel - 큐</title>
      <link>https://dev-kimchi.tistory.com/entry/Laravel-%ED%81%90</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v6kED/dJMcadCprzl/4DJaEkEMJ0A6DxQeMYczD0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v6kED/dJMcadCprzl/4DJaEkEMJ0A6DxQeMYczD0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v6kED/dJMcadCprzl/4DJaEkEMJ0A6DxQeMYczD0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv6kED%2FdJMcadCprzl%2F4DJaEkEMJ0A6DxQeMYczD0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제는 &quot;느린 코드&quot;가 아니라 &quot;워커 점유 시간&quot;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP 웹 애플리케이션에서 요청은 보통 nginx를 거쳐 PHP-FPM worker로 전달된다. FPM worker 하나는 한 번에 요청 하나를 처리한다. 응답을 돌려주기 전까지 그 worker는 다른 요청을 받을 수 없다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;동시 처리 가능한 요청 수
= 사용 가능한 FPM 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;예를 들어 평소 50ms로 끝나던 엔드포인트가 특정 입력에서 2분 동안 실행된다고 해보자. 대량 메일 발송, 외부 API 폴링, 엑셀 리포트 생성, 대규모 데이터 집계 같은 작업이 여기에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 요청이 몇 개만 겹쳐도 FPM worker가 오래 묶인다. 그러면 원래 빠르게 끝날 수 있었던 로그인, 목록 조회, 결제 확인 같은 요청까지 뒤에서 기다리게 된다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;빠른 요청들
   &amp;darr;
[FPM worker pool]
   &amp;uarr;
느린 요청 몇 개가 worker를 오래 점유&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx와 FPM 사이에는 &lt;code&gt;fastcgi_read_timeout&lt;/code&gt; 같은 제한도 있다. nginx의 기본 &lt;code&gt;fastcgi_read_timeout&lt;/code&gt;은 60초이며, 이 시간 동안 FastCGI 서버가 응답 데이터를 보내지 않으면 연결이 닫힌다. 사용자는 504 Gateway Timeout을 보게 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 핵심은 &quot;이 작업을 더 빠르게 만들자&quot;가 아니다.&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;이 작업이 사용자의 HTTP 응답 안에서 반드시 끝나야 하는가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답이 아니면 요청 경로 밖으로 빼야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 큐는 작업을 나중에 실행하는 장치가 아니라, 요청 수명을 끊어내는 장치다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 큐의 가장 중요한 역할은 무거운 작업을 HTTP 요청의 수명에서 분리하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;HTTP 요청
&amp;rarr; Job dispatch
&amp;rarr; 큐 백엔드에 저장
&amp;rarr; 즉시 응답

별도 worker
&amp;rarr; 큐에서 Job pop
&amp;rarr; 실제 작업 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 안에서 메일을 직접 보내면 HTTP 요청이 메일 발송 시간만큼 길어진다. 반대로 Job을 dispatch하면 요청에서는 &quot;작업을 큐에 넣는 일&quot;만 하고 끝난다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;SendWelcomeMail::dispatch($user-&amp;gt;id);

return response()-&amp;gt;json([
    'message' =&amp;gt; 'queued',
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 실제 메일 발송은 FPM worker가 아니라 별도의 &lt;code&gt;queue:work&lt;/code&gt; 프로세스가 처리한다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;[HTTP 요청] ─ dispatch ─▶ [queue backend] ◀─ pop ─ [queue worker]
   빠르게 응답              jobs table / Redis       무거운 작업 수행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 큐를 도입한다는 것은 &quot;코드를 비동기로 바꾼다&quot;보다 더 정확히 말해 &quot;사용자 응답을 위한 worker와 백그라운드 작업을 위한 worker를 분리한다&quot;는 뜻이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. connection과 queue는 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 큐에서 자주 헷갈리는 용어가 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;connection = 큐 백엔드
queue      = 그 백엔드 안의 논리적 채널&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;redis&lt;/code&gt;는 connection이고, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;emails&lt;/code&gt;는 queue 이름이다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;SendMail::dispatch($user-&amp;gt;id)
    -&amp;gt;onConnection('redis')
    -&amp;gt;onQueue('emails');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worker는 특정 connection에서 특정 queue들을 소비한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;php artisan queue:work redis --queue=high,default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;high,default&lt;/code&gt;의 순서가 중요하다. Laravel worker는 앞에 있는 queue를 우선 처리한다. 그래서 인증 메일, 결제 후처리처럼 지연이 민감한 Job은 &lt;code&gt;high&lt;/code&gt;에 넣고, 대량 통계 집계나 마케팅 발송은 &lt;code&gt;default&lt;/code&gt; 또는 &lt;code&gt;bulk&lt;/code&gt;로 분리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;high queue     &amp;rarr; 즉시성 중요
default queue  &amp;rarr; 일반 작업
bulk queue     &amp;rarr; 느려도 되는 대량 작업&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분리가 없으면 대량 작업이 쌓였을 때 중요한 작업까지 같이 밀린다. 큐를 쓴다고 자동으로 우선순위가 생기는 것이 아니다. queue 이름을 어떻게 나누고 worker를 어떻게 띄우느냐가 운영 품질을 결정한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. database 드라이버는 단순하지만, DB를 큐로 쓰는 선택이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel은 database, Redis, SQS 같은 여러 큐 백엔드를 같은 API 뒤로 숨긴다. 그중 database 드라이버는 별도 인프라 없이 RDB 테이블을 큐로 쓰는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;code&gt;jobs&lt;/code&gt; 테이블에 Job payload가 저장된다.&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;&lt;code&gt;queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;논리적 queue 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;직렬화된 Job 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attempts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시도 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reserved_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;worker가 점유한 시각&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;available_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이 시각 이후 실행 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;생성 시각&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job을 dispatch하면 Laravel은 Job 객체를 직렬화해서 payload로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Job 객체
&amp;rarr; serialize
&amp;rarr; payload JSON
&amp;rarr; jobs table 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 생성자에 넘긴 값이 큐 payload에 들어간다는 것이다. 큰 바이너리, 거대한 배열, 필요 이상으로 많은 관계가 로드된 Eloquent 모델을 넣으면 payload가 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분은 데이터 자체보다 식별자만 넘기는 편이 낫다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// 권장하기 쉬운 형태
ProcessReport::dispatch($report-&amp;gt;id);

// 위험해질 수 있는 형태
ProcessReport::dispatch($largeCollection);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐 payload는 &quot;작업에 필요한 최소 정보&quot;만 담아야 한다. 실제 데이터는 worker가 처리 시점에 다시 조회하는 편이 안전하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. database 큐에서 worker들은 같은 Job을 어떻게 피하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;database 큐에서 가장 중요한 문제는 여러 worker가 같은 Job을 동시에 집지 않게 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel의 &lt;code&gt;DatabaseQueue&lt;/code&gt;는 트랜잭션 안에서 다음 실행 가능한 Job을 찾고, 그 행을 잠근 뒤, &lt;code&gt;reserved_at&lt;/code&gt;과 &lt;code&gt;attempts&lt;/code&gt;를 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 이런 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM jobs
WHERE queue = ?
  AND (
    (reserved_at IS NULL AND available_at &amp;lt;= ?)
    OR reserved_at &amp;lt;= ?
  )
ORDER BY id ASC
LIMIT 1
FOR UPDATE SKIP LOCKED;&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
또는
점유된 지 retry_after가 지나 죽은 worker의 Job&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FOR UPDATE&lt;/code&gt;는 선택한 행을 잠근다. &lt;code&gt;SKIP LOCKED&lt;/code&gt;는 다른 worker가 이미 잠근 행을 기다리지 않고 건너뛴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 10.x 기준 source를 보면 MySQL 8.0.1 이상, MariaDB 10.6 이상, PostgreSQL 9.5 이상, Vitess 19 이상에서는 &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;를 사용한다. SQL Server에서는 별도 lock 힌트를 쓰고, 그 외에는 일반적인 lock으로 폴백한다.&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;SKIP LOCKED 있음
&amp;rarr; 잠긴 행을 건너뛰므로 worker들이 병렬로 다른 Job을 잡기 쉬움

SKIP LOCKED 없음
&amp;rarr; 같은 앞쪽 행의 lock을 기다릴 수 있어 처리량이 떨어짐&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 구버전 DB에서 약해지는 것은 보통 정확성보다 처리량이다. 중복 실행을 무조건 허용한다는 뜻이 아니라, worker들이 더 자주 기다리게 된다는 뜻에 가깝다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. retry_after와 timeout을 헷갈리면 중복 실행이 나온다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐 운영에서 가장 자주 터지는 설정 실수는 &lt;code&gt;retry_after&lt;/code&gt;와 &lt;code&gt;timeout&lt;/code&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;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retry_after&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;worker가 죽었다고 보고 점유 Job을 다시 풀어주는 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$timeout&lt;/code&gt; / &lt;code&gt;--timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한 Job 실행을 worker가 강제로 종료하는 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 Job이 정상적으로 100초 걸리는데 &lt;code&gt;retry_after&lt;/code&gt;가 90초라면 어떻게 될까.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;0초    worker A가 Job 점유
90초   Laravel이 &quot;A가 죽었나?&quot;라고 보고 Job을 다시 사용 가능하게 봄
100초  worker A가 아직 처리 중
동시에 worker B가 같은 Job을 다시 처리할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 중복 실행이다. 메일이 두 번 가거나, 외부 API가 두 번 호출되거나, 결제가 두 번 시도되는 식의 문제가 생길 수 있다.&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;Job timeout &amp;lt; retry_after&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 공식 문서도 worker timeout 값을 &lt;code&gt;retry_after&lt;/code&gt;보다 몇 초 짧게 잡으라고 설명한다. 그래야 오래 걸리는 worker가 먼저 종료되고, 그 뒤 &lt;code&gt;retry_after&lt;/code&gt;가 지난 Job이 다시 시도된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 10.x 문서에서는 Job별 timeout을 Job 클래스의 &lt;code&gt;$timeout&lt;/code&gt; property로 설명한다. 이 값은 &lt;code&gt;queue:work --timeout&lt;/code&gt;으로 지정한 값보다 우선한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐는 기본적으로 at-least-once 전달 모델에 가깝다. 즉 &quot;최소 한 번은 실행&quot;을 목표로 하며, 장애 상황에서는 중복 실행이 가능하다. 그래서 메일, 결제, 쿠폰 지급, 포인트 적립처럼 부수효과가 있는 Job은 멱등성을 직접 설계해야 한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;7. queue:work는 웹 요청과 다른 생명주기를 가진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;php artisan queue:work&lt;/code&gt;는 PHP-FPM 요청이 아니다. CLI에서 실행되는 별도 PHP 프로세스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 중요한 점은 &lt;code&gt;queue:work&lt;/code&gt;가 상주 프로세스라는 것이다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;프레임워크 boot
&amp;rarr; Job 처리
&amp;rarr; 다음 Job 처리
&amp;rarr; 다음 Job 처리
&amp;rarr; 계속 반복&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 빠르다. 매 Job마다 Laravel을 새로 부팅하지 않아도 되기 때문이다.&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;FPM 요청
&amp;rarr; 요청 끝나면 상태 정리

queue:work
&amp;rarr; 프로세스가 살아 있는 동안 상태가 남을 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 객체, static property, 전역 상태, 누적 배열, 닫히지 않은 리소스가 Job 사이에 남을 수 있다. 전통적인 PHP 요청 모델에 익숙하면 이 차이를 놓치기 쉽다.&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;php artisan queue:work redis \
  --queue=high,default \
  --tries=3 \
  --timeout=60 \
  --max-time=3600 \
  --max-jobs=1000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--max-time&lt;/code&gt;과 &lt;code&gt;--max-jobs&lt;/code&gt;는 worker가 영원히 늙어가지 않도록 하는 안전장치다. 메모리 누수나 상태 오염이 누적되기 전에 프로세스를 새로 띄우는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포할 때도 주의해야 한다. 상주 worker는 이미 로드한 코드를 메모리에 들고 있다. 새 코드를 배포했다고 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;이 명령은 worker에게 현재 Job을 끝낸 뒤 재시작하라고 알려준다. Supervisor나 systemd 같은 프로세스 관리자는 종료된 worker를 다시 띄운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;code&gt;fastcgi_finish_request()&lt;/code&gt;로 응답을 먼저 flush한 뒤 뒤처리를 계속하는 방법도 있다. 하지만 이 코드는 여전히 같은 PHP 요청 흐름 안에서 실행된다. 큐처럼 별도 worker, 재시도, 실패 추적, 처리량 제어가 생기는 것은 아니다. 그래서 짧은 후처리에는 쓸 수 있어도, 무거운 작업을 분리하는 기본 해법으로 보기는 어렵다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Supervisor 상주 worker와 cron 기동 worker는 목적이 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐 worker를 운영하는 대표적인 방식은 두 가지다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. Supervisor/systemd로 항상 띄워둔다
2. cron 또는 scheduler가 주기적으로 worker를 띄운다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리량과 지연 시간이 중요하면 상주 worker가 정석이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[program:app-worker]
command=php /path/artisan queue:work redis --queue=high,default --sleep=3 --tries=3 --timeout=60 --max-time=3600 --max-jobs=1000
autostart=true
autorestart=true
numprocs=4
stopwaitsecs=3600
stopsignal=TERM&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 worker가 항상 떠 있다. Job이 들어오면 바로 처리할 수 있고, &lt;code&gt;numprocs&lt;/code&gt;로 병렬성을 늘릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 작은 서비스나 인프라 제약이 있는 환경에서는 scheduler로 worker를 짧게 띄우는 절충도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Schedule::command('queue:work --stop-when-empty --max-time=50')
    -&amp;gt;everyMinute()
    -&amp;gt;withoutOverlapping(10)
    -&amp;gt;runInBackground();&lt;/code&gt;&lt;/pre&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;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--stop-when-empty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;큐가 비면 worker 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-time=50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1분 주기보다 짧게 실행해 겹침을 줄임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;withoutOverlapping(10)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이전 실행이 남아 있으면 다음 실행 건너뜀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runInBackground()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scheduler의 다른 작업을 막지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 상주 worker보다 처리량이 낮다. 하지만 유휴 상태에서 worker가 계속 메모리를 점유하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 선택 기준은 이렇다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;지연 시간이 중요하고 Job이 자주 들어온다
&amp;rarr; Supervisor/systemd + Redis/Horizon

작은 규모이고 작업이 가끔 들어온다
&amp;rarr; scheduler로 짧게 queue:work 기동&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. ShouldQueue는 실행 경로를 바꾸는 마커다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel에서는 Job을 직접 dispatch할 수도 있지만, Mailable, Notification, Listener에 &lt;code&gt;ShouldQueue&lt;/code&gt;를 붙여 비동기화할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class WelcomeMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출부는 크게 달라지지 않는다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Mail::to($user)-&amp;gt;send(new WelcomeMail($user));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;WelcomeMail&lt;/code&gt;이 &lt;code&gt;ShouldQueue&lt;/code&gt;를 구현하고 있으면 Laravel은 즉시 발송 대신 queue로 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ShouldQueue&lt;/code&gt; 자체는 메서드가 없는 마커 인터페이스다. 프레임워크가 &quot;이 객체는 큐로 보내야 한다&quot;는 신호로 읽는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notification과 Event Listener도 같은 방식으로 동작한다. 그래서 호출부를 크게 바꾸지 않고도 느린 부수효과를 요청 경로 밖으로 밀어낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 자체의 시도 횟수와 timeout은 Laravel 10.x 기준으로 Job 클래스의 property를 이용해 표현할 수 있다. 재시도 대기 시간은 단순 값이면 &lt;code&gt;$backoff&lt;/code&gt; property로, 점증 backoff처럼 여러 값을 쓰려면 &lt;code&gt;backoff()&lt;/code&gt; 메서드로 표현한다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendWelcomeMail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;

    public $timeout = 30;

    public function backoff(): array
    {
        return [10, 60, 300];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. SerializesModels는 모델을 통째로 저장하지 않게 돕는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job에 Eloquent 모델을 넘길 때 Laravel은 &lt;code&gt;Queueable&lt;/code&gt; / &lt;code&gt;SerializesModels&lt;/code&gt; 계열의 직렬화 규칙을 통해 모델 전체를 그대로 payload에 넣지 않는다.&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 모델 객체
&amp;rarr; User 클래스명 + primary key + connection
&amp;rarr; worker에서 다시 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 payload가 작아지고, worker가 처리 시점의 최신 DB 상태를 다시 읽는다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단점도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dispatch 시점에는 존재하던 모델이 worker 처리 시점에는 삭제되어 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;dispatch
&amp;rarr; User id=10 저장
&amp;rarr; 그 사이 User 삭제
&amp;rarr; worker가 id=10 조회 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Job을 실패시키지 않고 조용히 버리고 싶다면 Laravel 버전에 맞는 missing model 처리 방식을 써야 한다. Laravel 10.x 문서 기준으로는 &lt;code&gt;$deleteWhenMissingModels = true&lt;/code&gt; property를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class SendWelcomeMail implements ShouldQueue
{
    public $deleteWhenMissingModels = true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 트랜잭션 안에서 dispatch하면 afterCommit을 확인해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐와 DB 트랜잭션을 함께 쓸 때 흔한 레이스가 있다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;DB::transaction(function () {
    $order = Order::create([...]);

    ProcessOrder::dispatch($order);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 보기에는 자연스럽다. 하지만 worker가 빠르면 트랜잭션이 commit되기 전에 Job을 집을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 worker 입장에서는 아직 DB에 보이지 않는 데이터를 읽으려고 하는 셈이다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;요청 프로세스
&amp;rarr; 트랜잭션 시작
&amp;rarr; 주문 생성
&amp;rarr; Job dispatch
&amp;rarr; 아직 commit 전

worker
&amp;rarr; Job pop
&amp;rarr; 주문 조회
&amp;rarr; 조회 실패 또는 불완전한 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 &lt;code&gt;after_commit&lt;/code&gt; 설정 또는 &lt;code&gt;afterCommit()&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;ProcessOrder::dispatch($order)-&amp;gt;afterCommit();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 열린 DB 트랜잭션이 모두 commit된 뒤 Job이 실제로 dispatch된다. 트랜잭션이 rollback되면 그 안에서 예약된 Job도 버려진다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;트랜잭션과 큐를 함께 쓴다
&amp;rarr; afterCommit 여부를 반드시 확인한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 규칙 하나만 지켜도 프로덕션에서 애매하게 재현되는 큐 버그를 꽤 줄일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. Queue, Command, Scheduler, Event, Observer는 책임이 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel에는 &quot;요청 밖에서 무언가를 실행하는 도구&quot;가 여러 개 있다. 이름만 보면 비슷하지만 책임이 다르다.&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;th&gt;책임&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queue Job&lt;/td&gt;
&lt;td&gt;코드에서 dispatch&lt;/td&gt;
&lt;td&gt;무거운 작업을 비동기로 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artisan Command&lt;/td&gt;
&lt;td&gt;수동 실행 또는 scheduler&lt;/td&gt;
&lt;td&gt;배치, 운영 작업, 데이터 보정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler&lt;/td&gt;
&lt;td&gt;시간&lt;/td&gt;
&lt;td&gt;정해진 시점에 Command나 Job을 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event + Listener&lt;/td&gt;
&lt;td&gt;사건 발생&lt;/td&gt;
&lt;td&gt;하나의 사건에 여러 부수효과 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observer&lt;/td&gt;
&lt;td&gt;모델 생명주기&lt;/td&gt;
&lt;td&gt;모델 저장/삭제에 항상 붙는 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 매일 새벽 리포트를 생성해야 한다면 Scheduler가 시간을 담당하고, Command가 실행 단위를 담당하고, 내부에서 Queue Job을 dispatch해 병렬 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Scheduler
&amp;rarr; Artisan Command
&amp;rarr; Queue Job 여러 개 dispatch
&amp;rarr; worker가 실제 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 가입 후 메일, 슬랙 알림, CRM 동기화를 붙이고 싶다면 Event와 Listener가 잘 맞는다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;UserRegistered event
&amp;rarr; SendWelcomeMail listener
&amp;rarr; NotifySlack listener
&amp;rarr; SyncCrm listener&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Listener에 &lt;code&gt;ShouldQueue&lt;/code&gt;를 붙이면 느린 부수효과만 비동기로 분리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observer는 모델 저장/삭제에 항상 따라야 하는 규칙에 적합하다. 다만 Observer 자체는 동기 실행되므로, 무거운 작업은 Observer 안에서 직접 처리하지 말고 Job으로 넘기는 편이 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 운영 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐는 코드를 깔끔하게 만드는 도구이기도 하지만, 실제로는 운영 시스템이다. 그래서 다음 항목을 계속 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Job이 쌓이기만 한다
&amp;rarr; worker가 떠 있는지 확인
&amp;rarr; scheduler/cron이 도는지 확인
&amp;rarr; withoutOverlapping lock이 남아 있는지 확인

Job이 중복 실행된다
&amp;rarr; timeout &amp;lt; retry_after 확인
&amp;rarr; 멱등성 키 확인
&amp;rarr; afterCommit 누락 확인

배포 후 코드가 안 바뀐다
&amp;rarr; 상주 worker가 구버전 코드를 들고 있는지 확인
&amp;rarr; php artisan queue:restart 실행

worker가 조용히 죽는다
&amp;rarr; 메모리 제한, OOM, 로그 확인
&amp;rarr; --max-jobs / --max-time / --memory 설정 확인

DB 부하가 커진다
&amp;rarr; database 큐 polling 비용 확인
&amp;rarr; Redis 전환 또는 Horizon 도입 검토&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 큐를 쓰는 경우 Laravel Horizon도 좋은 선택이다. Horizon은 Redis 기반 queue의 처리량, 실행 시간, 실패 Job 같은 지표를 볼 수 있는 대시보드와 worker 설정 체계를 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 큐의 핵심은 비동기 문법이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 HTTP 요청의 책임과 백그라운드 작업의 책임을 분리하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HTTP 요청
&amp;rarr; 사용자에게 필요한 응답을 빠르게 반환

Queue worker
&amp;rarr; 오래 걸리고 실패할 수 있는 작업을 따로 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FPM worker는 사용자 응답을 위해 아껴야 한다. 메일 발송, 외부 API 호출, 대량 집계, 파일 변환 같은 작업이 FPM worker를 오래 붙잡고 있으면 전체 애플리케이션 응답성이 무너진다.&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;cos&quot;&gt;&lt;code&gt;worker 운영
재시도 정책
실패 Job 관측
중복 실행 방어
트랜잭션 경계
배포 시 worker 재시작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 큐는 &quot;나중에 실행&quot;이 아니라 &quot;다른 실행 주체에게 책임을 넘기는 것&quot;으로 이해하는 편이 좋다.&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;요청에 꼭 필요한 일은 HTTP 경로에서 처리하고, 실패해도 재시도할 수 있는 무거운 일은 큐로 넘긴다. Laravel 큐 설계의 품질은 이 경계를 얼마나 정확히 긋느냐에서 결정된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Laravel Queues 10.x: &lt;a href=&quot;https://laravel.com/docs/10.x/queues&quot;&gt;https://laravel.com/docs/10.x/queues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Laravel Mail 10.x: &lt;a href=&quot;https://laravel.com/docs/10.x/mail&quot;&gt;https://laravel.com/docs/10.x/mail&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Laravel Task Scheduling 10.x: &lt;a href=&quot;https://laravel.com/docs/10.x/scheduling&quot;&gt;https://laravel.com/docs/10.x/scheduling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Laravel Horizon 10.x: &lt;a href=&quot;https://laravel.com/docs/10.x/horizon&quot;&gt;https://laravel.com/docs/10.x/horizon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Laravel Framework &lt;code&gt;DatabaseQueue&lt;/code&gt; source: &lt;a href=&quot;https://github.com/laravel/framework/blob/10.x/src/Illuminate/Queue/DatabaseQueue.php&quot;&gt;https://github.com/laravel/framework/blob/10.x/src/Illuminate/Queue/DatabaseQueue.php&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;nginx &lt;code&gt;fastcgi_read_timeout&lt;/code&gt;: &lt;a href=&quot;https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_read_timeout&quot;&gt;https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_read_timeout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PHP &lt;code&gt;fastcgi_finish_request&lt;/code&gt;: &lt;a href=&quot;https://www.php.net/manual/en/function.fastcgi-finish-request.php&quot;&gt;https://www.php.net/manual/en/function.fastcgi-finish-request.php&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>PHP/laravel</category>
      <category>cli</category>
      <category>laravel</category>
      <category>php</category>
      <category>Queue</category>
      <category>transaction</category>
      <category>WORKER</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/304</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Laravel-%ED%81%90#entry304comment</comments>
      <pubDate>Fri, 26 Jun 2026 18:49:31 +0900</pubDate>
    </item>
    <item>
      <title>PHP - 인터프리터 언어와 컴파일 언어, 그리고 PHP가 Java처럼 동작하지 않는 이유</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EC%96%B8%EC%96%B4%EC%99%80-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EC%96%B8%EC%96%B4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-PHP%EA%B0%80-Java%EC%B2%98%EB%9F%BC-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSIADG/dJMcajbApxA/H1QxyVdxlYjLl8vpHnoRR1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSIADG/dJMcajbApxA/H1QxyVdxlYjLl8vpHnoRR1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSIADG/dJMcajbApxA/H1QxyVdxlYjLl8vpHnoRR1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSIADG%2FdJMcajbApxA%2FH1QxyVdxlYjLl8vpHnoRR1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;437&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;인터프리터와 컴파일러, 그리고 PHP가 Java처럼 동작하지 않는 이유&lt;/h1&gt;
&lt;p&gt;프로그래밍 언어는 결국 컴퓨터가 실행할 수 있는 형태로 바뀌어야 한다. 사람이 작성한 소스 코드를 CPU가 바로 이해하는 것은 아니기 때문이다.&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;전통적으로 이 변환을 실행 전에 미리 하면 컴파일 방식, 실행하면서 처리하면 인터프리터 방식이라고 부른다. 하지만 현대 언어에서는 이 구분이 아주 깔끔하게 나뉘지 않는다.&lt;/p&gt;
&lt;h2&gt;1. 컴파일 언어와 인터프리터 언어는 절대 분류가 아니다&lt;/h2&gt;
&lt;p&gt;C, C++, Rust, Go 같은 언어는 보통 실행 전에 기계어 실행 파일로 컴파일한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C/Rust/Go source
→ compiler
→ machine code binary
→ 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반대로 Python, Ruby, PHP, JavaScript 같은 언어는 사용자가 별도의 빌드 산출물을 직접 만들지 않고 바로 실행하는 경험을 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;source
→ runtime/interpreter
→ 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 관습적으로 “컴파일 언어”, “인터프리터 언어”라고 부르지만, 더 정확히는 &lt;strong&gt;언어 자체보다 실행 구현 방식의 차이&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;Java는 소스를 &lt;code&gt;.class&lt;/code&gt; bytecode로 컴파일한 뒤 JVM에서 실행하고, JavaScript V8은 bytecode interpreter와 JIT compiler를 함께 사용한다. Python도 CPython 기준으로 소스를 bytecode로 컴파일한 뒤 VM에서 실행한다.&lt;/p&gt;
&lt;p&gt;즉 현대적인 관점에서는 이렇게 보는 게 맞다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;컴파일 vs 인터프리터
= 실행 전에 얼마나 번역하느냐
= 실행 중에 얼마나 최적화하느냐
= 실행 상태를 얼마나 오래 유지하느냐&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Python은 순수 인터프리터인가?&lt;/h2&gt;
&lt;p&gt;아니다. 적어도 표준 구현체인 CPython은 순수 인터프리터라고 보기 어렵다.&lt;/p&gt;
&lt;p&gt;CPython은 &lt;code&gt;.py&lt;/code&gt; 파일을 바로 한 줄씩 읽어 실행하는 것이 아니라, 먼저 bytecode로 컴파일한다. 그 bytecode는 Python VM이 실행한다. 또한 bytecode는 &lt;code&gt;.pyc&lt;/code&gt; 파일로 &lt;code&gt;__pycache__&lt;/code&gt;에 저장될 수 있어 다음 실행에서 재컴파일 비용을 줄인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Python source
→ CPython bytecode
→ Python VM
→ 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다만 사용자가 &lt;code&gt;javac&lt;/code&gt;처럼 명시적인 빌드 단계를 거치지 않기 때문에, 개발 경험상 “인터프리터 언어”라고 부르는 것이다.&lt;/p&gt;
&lt;p&gt;그리고 CPython도 3.13부터 실험적 JIT를 포함할 수 있다. 다만 기본 실행 모델을 설명할 때는 여전히 “bytecode를 VM이 해석한다”는 표현이 더 안전하다.&lt;/p&gt;
&lt;h2&gt;3. PHP도 순수 인터프리터가 아니다&lt;/h2&gt;
&lt;p&gt;PHP도 Python과 비슷하게 동작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PHP source
→ Zend opcode
→ Zend VM
→ 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PHP는 소스를 Zend Engine이 opcode로 컴파일하고, Zend VM이 그 opcode를 실행한다. 여기에 OPcache를 켜면 컴파일된 opcode를 공유 메모리에 저장해 매 요청마다 소스를 다시 파싱하고 컴파일하는 비용을 줄인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;첫 요청
PHP source → opcode compile → OPcache 저장 → 실행

다음 요청
OPcache의 opcode 재사용 → 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 “PHP는 매 요청마다 무조건 처음부터 인터프리팅한다”는 표현은 부정확하다. OPcache가 활성화된 운영 환경에서는 컴파일 결과를 재사용한다.&lt;/p&gt;
&lt;p&gt;PHP 8부터는 JIT도 추가됐다. 다만 JIT가 있다고 해서 일반적인 PHP 웹 애플리케이션이 Java HotSpot처럼 항상 강한 JIT 효과를 얻는다고 보면 안 된다. PHP JIT는 설정에 따라 활성화되고, 웹 요청 중심 애플리케이션에서는 병목이 DB, 네트워크, I/O인 경우가 많기 때문이다.&lt;/p&gt;
&lt;h2&gt;4. 그럼 PHP는 왜 Java처럼 계속 떠 있는 VM으로 쓰지 않을까?&lt;/h2&gt;
&lt;p&gt;질문의 핵심은 “PHP도 bytecode와 VM이 있는데 왜 Java처럼 애플리케이션을 메모리에 올려두고 계속 재사용하지 않느냐”다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 차이는 &lt;strong&gt;컴파일 여부가 아니라 요청 상태를 유지하느냐&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;Java 웹 애플리케이션은 보통 JVM 프로세스가 오래 살아 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;JVM 시작
→ 애플리케이션 로드
→ 객체/상태/캐시 유지
→ 여러 요청 처리
→ JIT 최적화 누적&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반면 전통적인 PHP-FPM 모델은 worker 프로세스는 살아 있지만, 각 요청의 애플리케이션 상태는 요청이 끝나면 정리되는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;php-fpm worker 유지
→ 요청 수신
→ Laravel/PHP 요청 처리
→ 응답
→ 요청 중 만든 상태 정리
→ 다음 요청 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 PHP가 매 요청마다 프로세스를 새로 띄우는 것은 아니다. FPM worker는 살아 있다. OPcache도 살아 있다. 하지만 요청 중 생성한 객체, 전역 상태, 서비스 컨테이너 상태는 기본적으로 다음 요청에 그대로 이어지지 않는다.&lt;/p&gt;
&lt;p&gt;이것이 전통적인 PHP의 중요한 특징이다.&lt;/p&gt;
&lt;h2&gt;5. PHP가 이 모델을 택한 이유&lt;/h2&gt;
&lt;p&gt;PHP의 기본 모델은 흔히 shared-nothing에 가깝다고 설명한다. 요청 하나가 끝나면 그 요청에서 만든 대부분의 상태가 사라진다.&lt;/p&gt;
&lt;p&gt;이 방식은 성능 면에서 Java식 상주 애플리케이션보다 불리한 부분이 있다. 매 요청마다 프레임워크 부트스트랩 비용이 들 수 있기 때문이다.&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;Java나 Node.js처럼 애플리케이션이 오래 살아 있으면 전역 변수, 싱글톤, static 상태, 메모리 누수가 계속 누적될 수 있다. 반면 전통적인 PHP는 요청이 끝나면 대부분 정리되므로 이런 문제가 구조적으로 덜하다.&lt;/p&gt;
&lt;p&gt;대신 PHP는 OPcache로 컴파일 비용을 줄인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;컴파일 결과는 유지한다
애플리케이션 요청 상태는 버린다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이게 PHP의 절충점이다.&lt;/p&gt;
&lt;h2&gt;6. PHP도 Java처럼 상주 모델로 쓸 수 있다&lt;/h2&gt;
&lt;p&gt;PHP가 반드시 전통적인 FPM 모델로만 동작해야 하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;Laravel Octane, Swoole, RoadRunner, FrankenPHP worker mode 같은 도구를 쓰면 애플리케이션을 한 번 부트스트랩한 뒤 메모리에 유지하고 여러 요청을 처리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PHP application boot
→ 메모리에 유지
→ 여러 요청 처리
→ worker 재시작 전까지 상태 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 빠르다. Laravel 부트스트랩 비용을 줄일 수 있고, 처리량도 좋아질 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 대가가 있다. 요청 사이에 상태가 남을 수 있으므로 전통적인 PHP에서 신경 쓰지 않아도 되던 문제를 직접 관리해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;static 배열에 데이터를 계속 쌓으면 메모리 누수가 된다.&lt;/li&gt;
&lt;li&gt;request 객체를 싱글톤에 보관하면 다음 요청에서 오래된 request를 볼 수 있다.&lt;/li&gt;
&lt;li&gt;배포 후 worker reload를 하지 않으면 새 코드가 바로 반영되지 않을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 PHP도 Java식 모델을 사용할 수 있지만, 그 순간부터 Java식 장기 실행 프로세스의 책임도 함께 따라온다.&lt;/p&gt;
&lt;h2&gt;7. 정리&lt;/h2&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;하지만 현대 언어는 대부분 섞여 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Python  → bytecode + VM
Java    → bytecode + JVM + JIT
PHP     → opcode + Zend VM + OPcache + 선택적 JIT
JS V8   → bytecode interpreter + JIT&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PHP와 Java의 진짜 차이는 “VM이 있느냐 없느냐”가 아니다.&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;Java는 애플리케이션 상태를 오래 유지하는 쪽에 가깝고, 전통적인 PHP는 컴파일 결과는 캐시하되 요청 상태는 버리는 쪽에 가깝다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그래서 PHP는 단순한 운영, 요청 격리, 상태 초기화의 안전성을 얻었다. 대신 장기 실행 애플리케이션이 얻는 강한 warm-up 효과와 상태 재사용은 기본값으로 포기했다. 필요하다면 Octane, Swoole, RoadRunner, FrankenPHP 같은 상주 런타임으로 그 반대 선택을 할 수 있다.&lt;/p&gt;
&lt;h2&gt;8. 참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;컴파일의 기본 개념&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MDN: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Compile&quot;&gt;Compile&lt;/a&gt;&lt;br&gt;컴파일은 한 언어/형식의 프로그램을 다른 형식의 명령어로 변환하는 과정이며, AOT와 JIT 컴파일을 함께 설명한다.&lt;/li&gt;
&lt;li&gt;GCC 공식 문서: &lt;a href=&quot;https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html&quot;&gt;Options Controlling the Kind of Output&lt;/a&gt;&lt;br&gt;GCC 컴파일 과정이 preprocessing, compilation, assembly, linking 단계로 이어지고 최종적으로 실행 파일을 만들 수 있음을 설명한다.&lt;/li&gt;
&lt;li&gt;GCC 공식 문서: &lt;a href=&quot;https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html&quot;&gt;Optimize Options&lt;/a&gt;&lt;br&gt;최적화 옵션이 실행 성능이나 코드 크기 개선을 목표로 하며, 그 대신 컴파일 시간/메모리 비용이 늘 수 있음을 설명한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;컴파일 언어 예시&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rust 공식 문서: &lt;a href=&quot;https://doc.rust-lang.org/rustc/what-is-rustc.html&quot;&gt;What is rustc?&lt;/a&gt;&lt;br&gt;&lt;code&gt;rustc&lt;/code&gt;가 Rust 컴파일러이며, 소스 코드를 라이브러리나 실행 가능한 binary code로 만든다고 설명한다.&lt;/li&gt;
&lt;li&gt;Oracle 공식 문서: &lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html&quot;&gt;The javac Command&lt;/a&gt;&lt;br&gt;&lt;code&gt;javac&lt;/code&gt;가 Java 소스 파일을 JVM에서 실행되는 &lt;code&gt;.class&lt;/code&gt; 파일로 컴파일한다고 설명한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;인터프리터 방식 / Python&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 공식 튜토리얼: &lt;a href=&quot;https://docs.python.org/3/tutorial/appetite.html&quot;&gt;Whetting Your Appetite&lt;/a&gt;&lt;br&gt;Python을 interpreted language로 설명하며, 별도 compile/link 과정 없이 개발 중 빠르게 실험할 수 있다는 장점을 언급한다.&lt;/li&gt;
&lt;li&gt;Python 공식 문서: &lt;a href=&quot;https://docs.python.org/3/glossary.html#term-bytecode&quot;&gt;Glossary - bytecode&lt;/a&gt;&lt;br&gt;Python 소스 코드가 CPython 인터프리터 내부 표현인 bytecode로 컴파일되고, virtual machine에서 실행된다고 설명한다.&lt;/li&gt;
&lt;li&gt;Python 공식 문서: &lt;a href=&quot;https://docs.python.org/3/library/py_compile.html&quot;&gt;py_compile&lt;/a&gt;&lt;br&gt;Python 소스 파일을 bytecode cache 파일로 컴파일하는 기능을 설명한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;현대 언어가 섞여 있다는 근거&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MDN: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Just_In_Time_Compilation&quot;&gt;Just-In-Time Compilation&lt;/a&gt;&lt;br&gt;JIT는 실행 중에 중간 표현이나 고수준 언어 코드를 machine code로 컴파일하는 방식이며, interpreter와 AOT의 장점을 섞는 접근이라고 설명한다.&lt;/li&gt;
&lt;li&gt;V8 공식 블로그: &lt;a href=&quot;https://v8.dev/blog/ignition-interpreter&quot;&gt;Firing up the Ignition interpreter&lt;/a&gt;&lt;br&gt;V8이 JavaScript 함수를 bytecode로 컴파일하고 Ignition interpreter가 이를 실행한다고 설명한다.&lt;/li&gt;
&lt;li&gt;PyPy 공식 문서: &lt;a href=&quot;https://doc.pypy.org/interpreter.html&quot;&gt;Bytecode Interpreter&lt;/a&gt;&lt;br&gt;PyPy도 Python 소스에서 파싱/컴파일된 code object와 bytecode interpreter 구조를 가진다고 설명한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>PHP/PHP</category>
      <category>Java</category>
      <category>php</category>
      <category>Python</category>
      <category>인터프리터</category>
      <category>컴파일</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/303</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EC%96%B8%EC%96%B4%EC%99%80-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EC%96%B8%EC%96%B4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-PHP%EA%B0%80-Java%EC%B2%98%EB%9F%BC-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%9C%A0#entry303comment</comments>
      <pubDate>Fri, 26 Jun 2026 18:12:46 +0900</pubDate>
    </item>
    <item>
      <title>PHP - SAPI</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-SAPI</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;50%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deNMho/dJMcajbAnle/UOxFdtiWBjMlvr6FbouHGk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deNMho/dJMcajbAnle/UOxFdtiWBjMlvr6FbouHGk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deNMho/dJMcajbAnle/UOxFdtiWBjMlvr6FbouHGk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeNMho%2FdJMcajbAnle%2FUOxFdtiWBjMlvr6FbouHGk%2Fimg.webp&quot; width=&quot;50%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;개요&lt;/h1&gt;
&lt;p&gt;PHP 애플리케이션은 같은 코드베이스라도 항상 같은 방식으로 실행되지 않는다. 웹 요청은 보통 PHP-FPM worker가 처리하고, &lt;code&gt;php artisan&lt;/code&gt; 같은 명령은 CLI 프로세스가 처리한다. 로컬 개발에서 사용하는 &lt;code&gt;php artisan serve&lt;/code&gt;는 또 다른 실행 방식인 PHP built-in server를 사용한다.&lt;/p&gt;
&lt;p&gt;이 차이를 이해하려면 먼저 &lt;strong&gt;SAPI&lt;/strong&gt;를 분리해서 봐야 한다. SAPI는 PHP 엔진이 외부 세계와 만나는 실행 인터페이스다. 즉 PHP 코드 자체가 달라지는 것이 아니라, PHP 엔진에 요청을 어떻게 전달하고 결과를 어디로 내보낼지가 달라진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. SAPI란?&lt;/h2&gt;
&lt;p&gt;SAPI는 PHP 문맥에서 &lt;strong&gt;Server API&lt;/strong&gt;, 또는 &lt;strong&gt;Server Application Programming Interface&lt;/strong&gt;로 설명된다. 이름에 server가 들어가지만 웹 서버 전용이라는 뜻은 아니다. CLI, FPM, CGI, built-in server, phpdbg 모두 PHP의 SAPI다.&lt;/p&gt;
&lt;p&gt;PHP 실행은 대략 이렇게 나뉜다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;입력 환경
→ SAPI
→ PHP 엔진 / Zend Engine
→ PHP 코드 실행
→ SAPI를 통해 결과 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SAPI는 다음을 결정한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;입력을 어디서 받을지: HTTP 요청, 터미널 인자, FastCGI payload 등&lt;/li&gt;
&lt;li&gt;출력을 어디로 보낼지: HTTP response, stdout, debugger 등&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$_SERVER&lt;/code&gt;, &lt;code&gt;php://input&lt;/code&gt;, header 처리 방식을 어떻게 구성할지&lt;/li&gt;
&lt;li&gt;프로세스 수명이 요청 단위인지, 명령 단위인지, 장기 상주인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 Laravel 코드가 같아도 실행 컨텍스트는 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;웹 요청             → FPM/FastCGI SAPI
php artisan 명령     → CLI SAPI
php artisan serve    → built-in web server / cli-server SAPI
테스트 커버리지      → phpdbg SAPI&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;2. CGI&lt;/h2&gt;
&lt;p&gt;CGI(Common Gateway Interface)는 웹 서버가 외부 프로그램을 실행해 동적 응답을 만드는 오래된 인터페이스다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
→ Web Server
→ 요청마다 php-cgi 프로세스 실행
→ PHP script 실행
→ 응답 반환
→ 프로세스 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CGI의 핵심 특징은 요청마다 외부 프로그램 프로세스를 새로 만든다는 점이다. 웹 서버는 요청 정보를 환경변수와 표준 입력으로 넘기고, 외부 프로그램은 표준 출력으로 HTTP 헤더와 본문을 돌려준다.&lt;/p&gt;
&lt;p&gt;구조는 단순하지만 비용이 크다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;요청마다 프로세스 생성 비용이 든다.&lt;/li&gt;
&lt;li&gt;Laravel 같은 프레임워크는 매 요청마다 부트스트랩 비용이 크다.&lt;/li&gt;
&lt;li&gt;트래픽이 늘면 프로세스 생성/종료 자체가 병목이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 현대 PHP 운영 환경에서 CGI를 직접 쓰는 경우는 드물다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. FastCGI&lt;/h2&gt;
&lt;p&gt;FastCGI는 CGI의 가장 큰 문제였던 &lt;strong&gt;요청마다 프로세스를 새로 만드는 비용&lt;/strong&gt;을 줄이기 위해 나온 별도 인터페이스/프로토콜이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
→ Nginx / Apache
→ FastCGI protocol
→ 이미 떠 있는 PHP 프로세스
→ PHP script 실행
→ 응답 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CGI와 달리 PHP 프로세스를 미리 띄워두고 재사용한다. 웹 서버는 매 요청마다 새 프로세스를 실행하지 않고, 이미 살아 있는 FastCGI 프로세스에 요청을 전달한다.&lt;/p&gt;
&lt;p&gt;다만 FastCGI 자체는 “요청 전달 방식”에 가깝다. 운영에서는 프로세스 수 조절, 죽은 프로세스 재시작, graceful reload, slow log, max requests 같은 관리 기능이 필요하다. PHP에서 이 역할을 맡는 대표 구현이 PHP-FPM이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. FPM&lt;/h2&gt;
&lt;p&gt;FPM(FastCGI Process Manager)은 PHP용 FastCGI 프로세스 매니저다. 웹 서버가 직접 PHP 코드를 실행하는 것이 아니라, FPM worker에게 FastCGI 요청을 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
→ Nginx
→ FastCGI
→ php-fpm master
→ php-fpm worker
→ Laravel public/index.php
→ 응답 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FPM의 역할은 PHP worker pool을 운영하는 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;worker 프로세스를 미리 띄워둔다.&lt;/li&gt;
&lt;li&gt;요청이 들어오면 놀고 있는 worker에게 배정한다.&lt;/li&gt;
&lt;li&gt;worker 수를 &lt;code&gt;pm.max_children&lt;/code&gt; 등으로 제한한다.&lt;/li&gt;
&lt;li&gt;느린 요청, timeout, 재시작, 로그 등을 관리한다.&lt;/li&gt;
&lt;li&gt;요청이 끝나면 worker는 다음 요청을 받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;중요한 점은 FPM worker 하나가 한 번에 요청 하나를 처리한다는 것이다. 응답이 끝나기 전까지 그 worker는 다른 요청을 받을 수 없다. 그래서 오래 걸리는 작업을 웹 요청 안에서 처리하면 worker가 묶이고, worker pool이 고갈될 수 있다.&lt;/p&gt;
&lt;p&gt;큐를 쓰는 이유도 여기에 있다. 요청에 꼭 필요한 작업만 FPM worker가 처리하고, 메일 발송·파일 변환·외부 API 처리 같은 무거운 작업은 CLI worker로 넘긴다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. CLI&lt;/h2&gt;
&lt;p&gt;CLI(Command Line Interface)는 터미널, cron, Supervisor, systemd 등에서 &lt;code&gt;php&lt;/code&gt; 실행 파일을 직접 실행하는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan queue:work
php artisan migrate
php script.php&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구조는 FPM과 다르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Shell / cron / Supervisor
→ php CLI 바이너리
→ artisan 또는 PHP script 실행
→ 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 &lt;code&gt;queue:work&lt;/code&gt;처럼 장기 실행될 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Supervisor
→ php artisan queue:work
→ Laravel 부트스트랩
→ Job 처리 루프
→ 여러 Job 처리 후 재시작&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CLI 프로세스는 FPM worker를 사용하지 않는다. &lt;code&gt;php artisan queue:work&lt;/code&gt;, &lt;code&gt;php artisan schedule:run&lt;/code&gt;, &lt;code&gt;php artisan migrate&lt;/code&gt;는 모두 FPM pool과 별개의 PHP 프로세스다.&lt;/p&gt;
&lt;p&gt;따라서 FPM의 웹 timeout에는 묶이지 않지만, 대신 장기 프로세스 특유의 문제가 생긴다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메모리 누수가 누적될 수 있다.&lt;/li&gt;
&lt;li&gt;싱글톤, static 상태가 Job 사이에 남을 수 있다.&lt;/li&gt;
&lt;li&gt;코드 배포 후 재시작이 필요하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--max-time&lt;/code&gt;, &lt;code&gt;--max-jobs&lt;/code&gt;, &lt;code&gt;--memory&lt;/code&gt; 같은 제한으로 주기적 재활용이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. CLI server&lt;/h2&gt;
&lt;p&gt;CLI server는 PHP가 제공하는 개발용 built-in web server다. Laravel의 &lt;code&gt;php artisan serve&lt;/code&gt;는 내부적으로 이 서버를 실행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan serve&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제로는 대략 이런 형태다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php -S 127.0.0.1:8000 server.php&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;구조는 CGI도 FPM도 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
→ php -S 프로세스
→ PHP built-in web server
→ Laravel public/index.php
→ 응답 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CGI처럼 요청마다 &lt;code&gt;php-cgi&lt;/code&gt; 프로세스를 새로 띄우지 않는다. FPM처럼 Nginx가 FastCGI로 worker pool에 요청을 넘기는 구조도 아니다. PHP 프로세스 자체가 간단한 HTTP 서버 역할을 한다.&lt;/p&gt;
&lt;p&gt;운영용으로 적합하지 않은 이유는 운영 서버로써 필요한 기능이 부족하기 때문이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기본적으로 개발 편의성에 초점을 둔 서버다.&lt;/li&gt;
&lt;li&gt;정교한 worker/process 관리가 없다.&lt;/li&gt;
&lt;li&gt;Nginx/Apache 수준의 정적 파일 처리, buffering, reverse proxy, TLS 운영 기능이 없다.&lt;/li&gt;
&lt;li&gt;장애 복구, 튜닝, 관측성이 제한적이다.&lt;/li&gt;
&lt;li&gt;일반적인 운영 트래픽을 전제로 설계되지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OPcache 같은 opcode cache는 설정에 따라 CLI server에서도 사용할 수 있다. 다만 개발 서버에서는 코드 변경이 즉시 반영되는 편이 중요하고, 운영 수준의 캐시·프로세스 관리까지 붙일 바에는 Nginx + FPM 구조를 쓰는 것이 맞다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;php artisan serve&lt;/code&gt;는 로컬 개발용으로 보는 것이 정확하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 정리&lt;/h2&gt;
&lt;p&gt;같은 Laravel 애플리케이션도 실행 주체에 따라 성격이 완전히 달라진다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;실행 방식&lt;/th&gt;
&lt;th&gt;대표 명령/구조&lt;/th&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;CGI&lt;/td&gt;
&lt;td&gt;Web Server → &lt;code&gt;php-cgi&lt;/code&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;FastCGI&lt;/td&gt;
&lt;td&gt;Web Server → FastCGI process&lt;/td&gt;
&lt;td&gt;요청 전달 방식&lt;/td&gt;
&lt;td&gt;프로세스 재사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FPM&lt;/td&gt;
&lt;td&gt;Nginx → PHP-FPM worker&lt;/td&gt;
&lt;td&gt;운영 웹 요청&lt;/td&gt;
&lt;td&gt;PHP FastCGI worker pool 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;php artisan ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;명령어, 큐, 스케줄&lt;/td&gt;
&lt;td&gt;FPM과 독립된 PHP 프로세스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;php -S&lt;/code&gt;, &lt;code&gt;php artisan serve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로컬 개발 서버&lt;/td&gt;
&lt;td&gt;CGI/FPM이 아닌 PHP 내장 HTTP 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;운영 Laravel 기준으로 가장 중요한 구분은 이것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;사용자 웹 요청       → PHP-FPM worker
큐/스케줄/명령어     → PHP CLI process
로컬 개발 서버       → PHP built-in server&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;php artisan serve&lt;/code&gt;는 CGI가 아니라 built-in server를 사용한다.&lt;br&gt;&lt;code&gt;php artisan queue:work&lt;/code&gt; 같은 CLI 명령은 FPM worker를 사용하지 않는다.&lt;br&gt;&lt;code&gt;FPM&lt;/code&gt;은 웹 요청을 처리하기 위한 PHP FastCGI worker pool이고, &lt;code&gt;CLI&lt;/code&gt;는 터미널이나 프로세스 매니저가 PHP 바이너리를 직접 실행하는 별도 실행 경로다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SAPI / PHP_SAPI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/function.php-sapi-name.php&quot;&gt;&lt;code&gt;php_sapi_name()&lt;/code&gt;&lt;/a&gt;&lt;br&gt;현재 PHP가 사용하는 Server API, 즉 SAPI 이름을 반환한다. 예시 값으로 &lt;code&gt;cli&lt;/code&gt;, &lt;code&gt;cli-server&lt;/code&gt;, &lt;code&gt;fpm-fcgi&lt;/code&gt;, &lt;code&gt;cgi-fcgi&lt;/code&gt;, &lt;code&gt;apache2handler&lt;/code&gt;, &lt;code&gt;phpdbg&lt;/code&gt;, &lt;code&gt;embed&lt;/code&gt; 등이 제시되어 있다.&lt;/li&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/reserved.constants.php&quot;&gt;&lt;code&gt;PHP_SAPI&lt;/code&gt; 상수&lt;/a&gt;&lt;br&gt;현재 PHP 빌드의 Server API를 나타내는 상수.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/features.commandline.php&quot;&gt;Using PHP from the command line&lt;/a&gt;&lt;br&gt;CLI SAPI의 목적은 PHP로 shell application을 개발/실행하는 것이며, CLI와 CGI는 서로 다른 SAPI라고 설명한다.&lt;/li&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/features.commandline.options.php&quot;&gt;Command line options&lt;/a&gt;&lt;br&gt;&lt;code&gt;php -f&lt;/code&gt;, &lt;code&gt;php -r&lt;/code&gt;, &lt;code&gt;php -S&lt;/code&gt; 등 CLI 옵션 설명. &lt;code&gt;-S &amp;lt;addr&amp;gt;:&amp;lt;port&amp;gt;&lt;/code&gt;가 built-in web server 실행 옵션이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CGI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RFC 3875: &lt;a href=&quot;https://www.rfc-editor.org/info/rfc3875/&quot;&gt;The Common Gateway Interface CGI Version 1.1&lt;/a&gt;&lt;br&gt;CGI를 HTTP 서버 아래에서 외부 프로그램을 실행하기 위한 인터페이스로 정의한다.&lt;/li&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/install.unix.commandline.php&quot;&gt;CGI and command line setups&lt;/a&gt;&lt;br&gt;PHP가 기본적으로 CLI와 CGI 프로그램으로 빌드될 수 있고, CGI processing에 사용될 수 있음을 설명한다.&lt;/li&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/security.cgi-bin.php&quot;&gt;Installed as CGI binary&lt;/a&gt;&lt;br&gt;CGI 방식으로 PHP를 설치했을 때의 보안 고려사항.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FastCGI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FastCGI 원문 스펙: &lt;a href=&quot;https://fastcgi-archives.github.io/FastCGI_Specification.html&quot;&gt;FastCGI Specification&lt;/a&gt;&lt;br&gt;FastCGI를 CGI의 open extension으로 설명하고, CGI와 달리 long-lived application process를 지원한다고 설명한다.&lt;/li&gt;
&lt;li&gt;Nginx 공식 문서: &lt;a href=&quot;https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html&quot;&gt;&lt;code&gt;ngx_http_fastcgi_module&lt;/code&gt;&lt;/a&gt;&lt;br&gt;Nginx가 FastCGI server로 요청을 전달하는 &lt;code&gt;fastcgi_pass&lt;/code&gt;, &lt;code&gt;fastcgi_param&lt;/code&gt;, &lt;code&gt;fastcgi_read_timeout&lt;/code&gt; 등을 설명한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PHP-FPM&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/install.fpm.php&quot;&gt;FastCGI Process Manager FPM&lt;/a&gt;&lt;br&gt;FPM은 PHP의 주요 FastCGI 구현이며, heavy-loaded sites에 유용한 advanced process management, worker pool, slowlog, dynamic/ondemand/static child spawning 등을 제공한다고 설명한다.&lt;/li&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/function.fastcgi-finish-request.php&quot;&gt;&lt;code&gt;fastcgi_finish_request()&lt;/code&gt;&lt;/a&gt;&lt;br&gt;응답을 flush한 뒤에도 시간이 오래 걸리는 작업을 계속할 수 있는 FPM 관련 함수. 단, 워커 점유 관점에서는 주의가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apache module / mod_php&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/install.unix.apache2.php&quot;&gt;Apache 2.x on Unix systems&lt;/a&gt;&lt;br&gt;&lt;code&gt;--with-apxs2&lt;/code&gt;, &lt;code&gt;LoadModule php_module modules/libphp.so&lt;/code&gt;, &lt;code&gt;SetHandler application/x-httpd-php&lt;/code&gt; 등을 통해 PHP를 Apache SAPI module로 붙이는 방식을 설명한다.&lt;/li&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/security.apache.php&quot;&gt;Installed as an Apache module&lt;/a&gt;&lt;br&gt;PHP가 Apache 모듈로 실행될 때 Apache 사용자 권한을 상속한다는 점을 설명한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Built-in web server / CLI server&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/features.commandline.webserver.php&quot;&gt;Built-in web server&lt;/a&gt;&lt;br&gt;CLI SAPI가 built-in web server를 제공하며, &lt;code&gt;php -S localhost:8000&lt;/code&gt;으로 실행한다. 개발/테스트용이며 public network에서 쓰지 말라고 명시한다. 기본적으로 single-threaded process라는 설명도 있다.&lt;/li&gt;
&lt;li&gt;Laravel 공식 소스: &lt;a href=&quot;https://github.com/laravel/framework/blob/12.x/src/Illuminate/Foundation/Console/ServeCommand.php&quot;&gt;&lt;code&gt;ServeCommand.php&lt;/code&gt;&lt;/a&gt;&lt;br&gt;&lt;code&gt;serve&lt;/code&gt; 명령의 설명은 “Serve the application on the PHP development server”이고, 실제 &lt;code&gt;serverCommand()&lt;/code&gt;가 &lt;code&gt;php_binary()&lt;/code&gt;, &lt;code&gt;-S&lt;/code&gt;, &lt;code&gt;host:port&lt;/code&gt;, router script를 반환한다. 즉 &lt;code&gt;php artisan serve&lt;/code&gt;는 PHP built-in server 래퍼다.&lt;/li&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/opcache.configuration.php&quot;&gt;OPcache runtime configuration&lt;/a&gt;&lt;br&gt;&lt;code&gt;opcache.enable_cli&lt;/code&gt;가 CLI 버전의 PHP에서 opcode cache를 켤 수 있는 설정임을 설명한다. 따라서 “CLI server는 캐시가 절대 없다”보다는 “기본/일반 개발 설정에서는 운영용 캐시·프로세스 관리 구조가 아니다”가 정확하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;phpdbg&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PHP 공식 문서: &lt;a href=&quot;https://www.php.net/manual/en/book.phpdbg.php&quot;&gt;Interactive PHP Debugger&lt;/a&gt;&lt;br&gt;&lt;code&gt;phpdbg&lt;/code&gt;는 SAPI module로 구현된 PHP 디버깅 플랫폼이며 step debugging, breakpoint, opcode 출력 등을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>PHP/PHP</category>
      <category>CGI</category>
      <category>cli</category>
      <category>FastCgi</category>
      <category>FPM</category>
      <category>laravel</category>
      <category>php</category>
      <category>SAPI</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/302</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-SAPI#entry302comment</comments>
      <pubDate>Fri, 26 Jun 2026 17:52:33 +0900</pubDate>
    </item>
    <item>
      <title>기타 - 아키텍처의 종류</title>
      <link>https://dev-kimchi.tistory.com/entry/%EA%B8%B0%ED%83%80-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%9D%98-%EC%A2%85%EB%A5%98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1769503360139.jpeg&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhNByF/dJMcaipx6op/kEqw8QKip9gQQ9cKBMSep1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhNByF/dJMcaipx6op/kEqw8QKip9gQQ9cKBMSep1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhNByF/dJMcaipx6op/kEqw8QKip9gQQ9cKBMSep1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhNByF%2FdJMcaipx6op%2FkEqw8QKip9gQQ9cKBMSep1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;390&quot; height=&quot;426&quot; data-filename=&quot;1769503360139.jpeg&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;1&quot; data-ke-size=&quot;size23&quot;&gt;1. 엔터프라이즈 아키텍처 (Enterprise Architecture, EA)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;2&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,0,0&quot;&gt;범위:&lt;/b&gt; 조직 전체 (기업 수준)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,1,0&quot;&gt;목적:&lt;/b&gt; 비즈니스 목표와 IT 전략의 일치&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,2,0&quot;&gt;설명:&lt;/b&gt; 회사의 비즈니스 프로세스, 데이터, 애플리케이션, 기술 인프라가 전체적으로 어떻게 맞물려 돌아가는지 정의하는 가장 거시적인 설계&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;6,2,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;참고 자료:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://www.gartner.com/reviews/market/enterprise-architecture-tools&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.gartner.com/reviews/market/enterprise-architecture-tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size23&quot;&gt;2. 솔루션 아키텍처 (Solution Architecture, SA)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;4&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0,0&quot;&gt;범위:&lt;/b&gt; 특정 프로젝트 또는 서비스&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0&quot;&gt;목적:&lt;/b&gt; 비즈니스 요구사항을 기술적 해결책으로 변환&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,2,0&quot;&gt;설명:&lt;/b&gt; 특정 문제를 해결하기 위해 필요한 소프트웨어, 인프라, 보안, 데이터 관리 등을 어떻게 통합하여 하나의 완성된 시스템(솔루션)을 만들지 설계&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;6,2,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;참고 자료:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/dynamics365/guidance/implementation-guide/solution-architecture-design-pillars&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://learn.microsoft.com/en-us/dynamics365/guidance/implementation-guide/solution-architecture-design-pillars&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;3. 소프트웨어 아키텍처 (Software Architecture)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;범위:&lt;/b&gt; 애플리케이션 내부 (코드 및 모듈)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;목적:&lt;/b&gt; 코드의 유지보수성, 확장성, 재사용성 확보&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,0&quot;&gt;설명:&lt;/b&gt; 소프트웨어 구성 요소 간의 관계와 규칙을 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,1,0,0&quot;&gt;주요 패턴:&lt;/b&gt; 레이어드(Layered), 마이크로서비스(MSA), 클린 아키텍처, 이벤트 기반 아키텍처 등&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;6,2,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;참고 자료:&lt;/b&gt;&amp;nbsp;&lt;a href=&quot;https://martinfowler.com/architecture/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://martinfowler.com/architecture/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;4. 인프라 아키텍처 (Infrastructure Architecture)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;범위:&lt;/b&gt; 물리적/논리적 하드웨어 환경&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;목적:&lt;/b&gt; 시스템의 안정성, 성능, 가용성 보장&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;설명:&lt;/b&gt; 서버, 네트워크(VPC, Subnet), 스토리지, 로드밸런서, CDN(CloudFront) 등 코드가 구동되는 환경의 물리적/가상적 배치를 설계&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,1,0,0&quot;&gt;참고 자료:&lt;/b&gt; &lt;a href=&quot;https://aws.amazon.com/what-is/iac/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/what-is/iac/&lt;/a&gt; - 코드형 인프라는 어떻게 작동하나요? 섹션&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;5. 데이터 아키텍처 (Data Architecture, DA)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;범위:&lt;/b&gt; 데이터 자산 전반&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;목적:&lt;/b&gt; 데이터의 무결성, 보안, 효율적 활용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,2,0&quot;&gt;설명:&lt;/b&gt; 데이터베이스(MariaDB 등)의 스키마 설계부터 데이터의 생성, 저장, 처리, 통합, 분배에 이르는 생애주기 규칙을 수립&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;6,2,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;참고 자료:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://www.ibm.com/kr-ko/think/topics/data-architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ibm.com/kr-ko/think/topics/data-architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;6. 보안 아키텍처 (Security Architecture)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;범위:&lt;/b&gt; 시스템 전체의 보호 계층&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;목적:&lt;/b&gt; 자산 보호 및 위험 관리&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,2,0&quot;&gt;설명:&lt;/b&gt; 인증, 인가, 데이터 암호화, 네트워크 방화벽 등 시스템 전반에 걸쳐 위협으로부터 보호하기 위한 보안 통제 구조를 설계&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;6,2,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;참고 자료:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://www.ibm.com/docs/en/apptio-gov/tbm-studio/saas?topic=model-security-architecture-overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ibm.com/docs/en/apptio-gov/tbm-studio/saas?topic=model-security-architecture-overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;15&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16&quot;&gt;요약 비교&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 140px;&quot; border=&quot;1&quot; data-path-to-node=&quot;17&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;핵심 질문&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;대상&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,1,0,0&quot;&gt;엔터프라이즈 아키텍처&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,1,1,0&quot;&gt;회사가 어떻게 IT를 활용하는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,1,2,0&quot;&gt;비즈니스 + IT 전체&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,2,0,0&quot;&gt;솔루션&lt;b data-path-to-node=&quot;17,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아키텍처&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,2,1,0&quot;&gt;이 프로젝트를 어떻게 완성하는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,2,2,0&quot;&gt;서비스 전체 기술 스택&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,3,0,0&quot;&gt;소프트웨어&lt;b data-path-to-node=&quot;17,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아키텍처&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,3,1,0&quot;&gt;코드를 어떻게 구조화하는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,3,2,0&quot;&gt;애플리케이션 로직&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,4,0,0&quot;&gt;인프라&lt;b data-path-to-node=&quot;17,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아키텍처&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,4,1,0&quot;&gt;서버와 네트워크를 어떻게 배치하는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,4,2,0&quot;&gt;컴퓨팅 자원 환경&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,5,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,5,0,0&quot;&gt;데이터&lt;b data-path-to-node=&quot;17,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아키텍처&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,5,1,0&quot;&gt;데이터를 어떻게 관리하는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,5,2,0&quot;&gt;DB 및 데이터 흐름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,6,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,6,0,0&quot;&gt;보안&lt;b data-path-to-node=&quot;17,1,0,0&quot; data-index-in-node=&quot;0&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아키텍처&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,6,1,0&quot;&gt;어떻게 시스템을 방어하는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;17,6,2,0&quot;&gt;전 영역 보안 계층&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타</category>
      <category>데이터</category>
      <category>보안</category>
      <category>설계</category>
      <category>소프트웨어</category>
      <category>솔루션</category>
      <category>아키텍처</category>
      <category>엔터프라이즈</category>
      <category>인프라</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/301</guid>
      <comments>https://dev-kimchi.tistory.com/entry/%EA%B8%B0%ED%83%80-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%9D%98-%EC%A2%85%EB%A5%98#entry301comment</comments>
      <pubDate>Thu, 23 Apr 2026 14:11:22 +0900</pubDate>
    </item>
    <item>
      <title>Laravel - Unable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 1. OpenSSL Error messages: error:0A000086:SSL routines::certificate verify failed 에러 해결</title>
      <link>https://dev-kimchi.tistory.com/entry/Laravel-Unable-to-connect-with-STARTTLS-streamsocketenablecrypto-SSL-operation-failed-with-code-1-OpenSSL-Error-messages-error0A000086SSL-routinescertificate-verify-failed-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;50%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZoPMX/dJMcafS1hIo/aIgRt1Hk3hQN1OKRpXryk1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZoPMX/dJMcafS1hIo/aIgRt1Hk3hQN1OKRpXryk1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZoPMX/dJMcafS1hIo/aIgRt1Hk3hQN1OKRpXryk1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZoPMX%2FdJMcafS1hIo%2FaIgRt1Hk3hQN1OKRpXryk1%2Fimg.jpg&quot; width=&quot;50%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;Laravel에서 SMTP로 메일 발송 시 다음 오류가 발생했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Unable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 1. 
OpenSSL Error messages: error:0A000086:SSL routines::certificate verify failed&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;원인 분석&lt;/h2&gt;
&lt;p&gt;먼저 메일 서버 인증서 상태를 확인했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;openssl s_client -starttls smtp -connect office.example.co.kr:25 -servername office.example.co.kr 2&amp;gt;&amp;amp;1 | grep -E &amp;quot;(Verify return code|verify error)&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;verify error:num=66:EE certificate key too weak
Verify return code: 66 (EE certificate key too weak)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;원인&lt;/strong&gt;: 메일 서버 SSL 인증서가 1024-bit 이하 RSA 키로 발급되어 있었다.&lt;/p&gt;
&lt;p&gt;OpenSSL 3.0부터 보안 정책이 강화되어 2048-bit 미만 키를 가진 인증서는 기본적으로 거부한다. PHP 8.1+가 OpenSSL 3.0을 사용하면서 이 정책이 적용된 것이다.&lt;/p&gt;
&lt;h2&gt;해결&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;config/mail.php&lt;/code&gt;에서 인증서 검증 비활성화:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#39;smtp&amp;#39; =&amp;gt; [
    &amp;#39;transport&amp;#39; =&amp;gt; &amp;#39;smtp&amp;#39;,
    &amp;#39;url&amp;#39; =&amp;gt; env(&amp;#39;MAIL_URL&amp;#39;),
    &amp;#39;host&amp;#39; =&amp;gt; env(&amp;#39;MAIL_HOST&amp;#39;, &amp;#39;smtp.mailgun.org&amp;#39;),
    &amp;#39;port&amp;#39; =&amp;gt; env(&amp;#39;MAIL_PORT&amp;#39;, 587),
    &amp;#39;encryption&amp;#39; =&amp;gt; env(&amp;#39;MAIL_ENCRYPTION&amp;#39;, &amp;#39;tls&amp;#39;),
    &amp;#39;username&amp;#39; =&amp;gt; env(&amp;#39;MAIL_USERNAME&amp;#39;),
    &amp;#39;password&amp;#39; =&amp;gt; env(&amp;#39;MAIL_PASSWORD&amp;#39;),
    &amp;#39;verify_peer&amp;#39; =&amp;gt; false # 추가
],&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan config:clear&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;verify_peer 옵션이란?&lt;/h2&gt;
&lt;p&gt;SSL/TLS 연결 시 상대방 인증서의 유효성을 검증할지 여부다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;true&lt;/code&gt; (기본값): 인증서 체인, 만료일, 키 강도 등 검증&lt;/li&gt;
&lt;li&gt;&lt;code&gt;false&lt;/code&gt;: 검증 없이 암호화 연결만 수립&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;false&lt;/code&gt;로 설정해도 &lt;strong&gt;통신 암호화는 유지&lt;/strong&gt;된다. 단지 상대방 서버가 진짜인지 확인을 안 할 뿐이다.&lt;/p&gt;
&lt;h2&gt;보안 고려사항&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;verify_peer =&amp;gt; false&lt;/code&gt;의 위험은 MITM(중간자 공격)인데, 이 공격이 성립하려면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;공격자가 DNS를 조작하거나&lt;/li&gt;
&lt;li&gt;네트워크 경로를 장악해야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DNS 권한이 본인에게 있고, 서버 인프라를 신뢰할 수 있는 환경이라면 실질적 위험은 낮다.&lt;/p&gt;
&lt;h2&gt;근본적 해결책&lt;/h2&gt;
&lt;p&gt;메일 서버 관리자에게 2048-bit 이상 RSA 키로 인증서 재발급을 요청하는 것이 올바른 해결책이다.&lt;/p&gt;</description>
      <category>PHP/laravel</category>
      <category>laravel</category>
      <category>php</category>
      <category>php8.3</category>
      <category>RSA</category>
      <category>SMTP</category>
      <category>SSL</category>
      <category>tls</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/300</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Laravel-Unable-to-connect-with-STARTTLS-streamsocketenablecrypto-SSL-operation-failed-with-code-1-OpenSSL-Error-messages-error0A000086SSL-routinescertificate-verify-failed-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0#entry300comment</comments>
      <pubDate>Thu, 5 Feb 2026 13:56:10 +0900</pubDate>
    </item>
    <item>
      <title>Linux - nmap 포트 스캔</title>
      <link>https://dev-kimchi.tistory.com/entry/Linux-nmap-%ED%8F%AC%ED%8A%B8-%EC%8A%A4%EC%BA%94</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;50%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tc5Pr/dJMcafyB7aa/EK2HZRSz8iyjKHZa8K2b1k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tc5Pr/dJMcafyB7aa/EK2HZRSz8iyjKHZa8K2b1k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tc5Pr/dJMcafyB7aa/EK2HZRSz8iyjKHZa8K2b1k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftc5Pr%2FdJMcafyB7aa%2FEK2HZRSz8iyjKHZa8K2b1k%2Fimg.jpg&quot; width=&quot;50%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;기본 명령어&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 특정 호스트의 주요 포트 스캔 (1000개 포트)
nmap 192.168.1.1

# 특정 포트만 스캔
nmap -p 80,443,3306 192.168.1.1

# 포트 범위 스캔
nmap -p 1-65535 192.168.1.1

# 서비스 버전 확인
nmap -sV -p 80,443 192.168.1.1&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;옵션 설명&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# === 포트 지정 ===
-p 80              
# 80번 포트만 검사 (nginx/apache 확인할 때)

-p 80,443,3306     
# 여러 포트 동시 검사 (웹+DB 서버 확인)

-p 1-1000          
# 1번부터 1000번까지 순서대로 검사

-p-                
# 1~65535번 전부 검사 (시간 오래 걸림)

-p U:53,T:80       
# UDP 53번(DNS), TCP 80번(HTTP) 검사


# === 스캔 속도 ===
-F                 
# 자주 쓰는 100개 포트만 검사 (30초 안에 끝남)

--top-ports 20     
# 가장 많이 쓰는 상위 20개 포트만 검사


# === 스캔 방식 ===
-sS                
# SYN 스캔 - 연결 안하고 포트만 확인 (빠르고 로그 안남음, sudo 필요)

-sT                
# TCP 연결 스캔 - 실제로 연결해서 확인 (권한 없을 때, 느림)

-sU                
# UDP 포트 검사 (DNS 53번 같은거, 매우 느림)

-sV                
# 포트가 열려있으면 어떤 프로그램인지까지 확인
# 예: 80번 포트 → nginx 1.18.0


# === 호스트 확인 ===
-Pn                
# &amp;quot;서버 살아있나?&amp;quot; 체크 건너뛰기
# 방화벽이 ping 막아놨을 때 필수

-n                 
# IP를 도메인으로 바꾸는 작업 안함 (2~3배 빠름)


# === 결과 보기 ===
-v                 
# 검사하면서 중간 결과 계속 보여줌

-oN result.txt     
# 결과를 텍스트 파일로 저장


# === 속도 조절 ===
-T0               # 매우 느림 (탐지 회피용)
-T3               # 보통 속도 (기본값)
-T4               # 빠름 (실무에서 가장 많이 씀)
-T5               # 매우 빠름 (결과 부정확할 수 있음)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Linux</category>
      <category>Linux</category>
      <category>namp</category>
      <category>port</category>
      <category>SCAN</category>
      <category>스캔</category>
      <category>포트</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/299</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Linux-nmap-%ED%8F%AC%ED%8A%B8-%EC%8A%A4%EC%BA%94#entry299comment</comments>
      <pubDate>Thu, 22 Jan 2026 12:00:01 +0900</pubDate>
    </item>
    <item>
      <title>Database - mariadb, mysql dump(로컬, 원격, docker)</title>
      <link>https://dev-kimchi.tistory.com/entry/Database-mariadb-mysql-dump%EB%A1%9C%EC%BB%AC-%EC%9B%90%EA%B2%A9-docker</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGmR6U/dJMcahJWKdo/bdBZnKt4k1Zv6ipCPryoH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGmR6U/dJMcahJWKdo/bdBZnKt4k1Zv6ipCPryoH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGmR6U/dJMcahJWKdo/bdBZnKt4k1Zv6ipCPryoH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGmR6U%2FdJMcahJWKdo%2FbdBZnKt4k1Zv6ipCPryoH0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;서버 작업을 하다 보면 DB 덤프를 떠야 하는 상황이 자주 생긴다.&lt;br&gt;로컬 서버, 원격 서버, 도커 컨테이너 등 환경에 따라 명령어가 달라서 헷갈릴 때가 많아 환경별 덤프 명령어를 정리해보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 로컬 MariaDB 서버에서 DB 덤프&lt;/h2&gt;
&lt;h3&gt;기본 명령어&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mariadb-dump -u root -p database_name &amp;gt; backup.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;사용 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 특정 데이터베이스 덤프
mariadb-dump -u root -p my_laravel_db &amp;gt; ~/Desktop/laravel_backup_20250117.sql

# 모든 데이터베이스 덤프
mariadb-dump -u root -p --all-databases &amp;gt; ~/Desktop/all_databases.sql

# 특정 테이블만 덤프
mariadb-dump -u root -p my_laravel_db users posts &amp;gt; ~/Desktop/users_posts.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;주요 옵션&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mariadb-dump -u root -p \
  --single-transaction \     # InnoDB 일관성 보장
  --routines \               # 저장 프로시저 포함
  --triggers \               # 트리거 포함
  --events \                 # 이벤트 포함
  database_name &amp;gt; backup.sql&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;2. 원격 MariaDB 서버에서 로컬로 DB 덤프&lt;/h2&gt;
&lt;h3&gt;기본 명령어&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh user@remote_host &amp;quot;mariadb-dump -u username -p database_name&amp;quot; &amp;gt; local_backup.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;사용 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# IP 주소로 접속
ssh ubuntu@192.168.1.100 &amp;quot;mariadb-dump -u root -p my_laravel_db&amp;quot; &amp;gt; ~/Desktop/remote_backup.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;주의사항&lt;/h3&gt;
&lt;p&gt;사용 시 ssh 명령어 부분은 각자 원격 서버 환경에 따라 수정을 해야한다(-p, -i 옵션 추가 등)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 원격 MariaDB 도커 컨테이너에서 로컬로 DB 덤프&lt;/h2&gt;
&lt;h3&gt;기본 명령어&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh user@server &amp;quot;docker exec -i container_name mariadb-dump -u root -p database_name&amp;quot; &amp;gt; local_dump.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;사용 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 비밀번호 입력 방식
ssh ubuntu@123.45.67.89 &amp;quot;docker exec -i mariadb mariadb-dump -u root -p production_db&amp;quot; &amp;gt; ~/Desktop/prod_dump.sql
# 실행 시 비밀번호 입력 프롬프트가 나타남&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;4. mariadb-dump vs mysqldump&lt;/h2&gt;
&lt;h3&gt;MariaDB 10.5부터 변경됨&lt;/h3&gt;
&lt;p&gt;MariaDB 10.5 이상에서는 &lt;code&gt;mariadb-dump&lt;/code&gt;가 공식 명령어다. &lt;code&gt;mysqldump&lt;/code&gt;는 심볼릭 링크로 남아있지만 쥐도 새도 모르게 제거될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 기존 방식 (비권장)
mysqldump -u root -p database_name &amp;gt; backup.sql

# 권장 방식 (MariaDB 10.5+)
mariadb-dump -u root -p database_name &amp;gt; backup.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;명령어 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# MariaDB 버전 확인
mariadb --version
# mariadb from 11.8.2-MariaDB

# mariadb-dump 존재 확인
which mariadb-dump
# /usr/bin/mariadb-dump&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 포인트&lt;/h2&gt;
&lt;h3&gt;반드시 체크할 사항&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;도커 컨테이너&lt;/strong&gt;: &lt;code&gt;docker exec -i&lt;/code&gt; 사용 (&lt;code&gt;-it&lt;/code&gt; 아님)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파일 크기&lt;/strong&gt;: 대용량 DB는 &lt;code&gt;gzip&lt;/code&gt;으로 압축 권장&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;압축과 함께 사용&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 덤프와 동시에 압축
mariadb-dump -u root -p database_name | gzip &amp;gt; backup.sql.gz

# 원격 도커에서 압축
ssh ubuntu@server &amp;quot;docker exec -i mariadb mariadb-dump -u root -p db_name | gzip&amp;quot; &amp;gt; dump.sql.gz&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;MySQL 사용 시&lt;/h2&gt;
&lt;p&gt;MySQL을 사용하는 경우 &lt;code&gt;mysqldump&lt;/code&gt; 명령어를 사용한다. 사용법은 동일하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 로컬 MySQL 덤프
mysqldump -u root -p database_name &amp;gt; backup.sql

# 원격 MySQL 서버에서 덤프
ssh user@server &amp;quot;mysqldump -u root -p database_name&amp;quot; &amp;gt; local_dump.sql

# 원격 MySQL 도커 컨테이너에서 덤프
ssh ubuntu@server &amp;quot;docker exec -i mysql_container mysqldump -u root -p database_name&amp;quot; &amp;gt; local_dump.sql&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DataBase</category>
      <category>Database</category>
      <category>docker</category>
      <category>dump</category>
      <category>MariaDB</category>
      <category>MySQL</category>
      <category>ssh</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/298</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Database-mariadb-mysql-dump%EB%A1%9C%EC%BB%AC-%EC%9B%90%EA%B2%A9-docker#entry298comment</comments>
      <pubDate>Sat, 17 Jan 2026 09:18:27 +0900</pubDate>
    </item>
    <item>
      <title>Docker - Ubuntu 서버에 docker, docker compose 설치하기</title>
      <link>https://dev-kimchi.tistory.com/entry/Docker-Ubuntu-%EC%84%9C%EB%B2%84%EC%97%90-docker-docker-compose-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ywdMG/dJMcadtV20C/6fzSMfbQpKk6s1eeG2rN8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ywdMG/dJMcadtV20C/6fzSMfbQpKk6s1eeG2rN8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ywdMG/dJMcadtV20C/6fzSMfbQpKk6s1eeG2rN8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FywdMG%2FdJMcadtV20C%2F6fzSMfbQpKk6s1eeG2rN8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;498&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Docker 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 필수 패키지 설치
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# Docker GPG key 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# Docker 저장소 추가
echo \
  &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# Docker 설치
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# Docker 서비스 시작 및 자동 시작 설정
sudo systemctl start docker
sudo systemctl enable docker&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# 설치 확인
docker -v&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;Docker Compose 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Docker Compose 다운로드 및 설치
sudo curl -L &amp;quot;https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep -Po &amp;#39;&amp;quot;tag_name&amp;quot;: &amp;quot;\K.*?(?=&amp;quot;)&amp;#39;)/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# 실행 권한 추가
sudo chmod +x /usr/local/bin/docker-compose

# 설치 확인
docker-compose --version&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;unable to get image &amp;#39;이미지&amp;#39;: permission denied while trying to ~~ 에러 발생 시&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 1. 현재 사용자를 docker 그룹에 추가
sudo usermod -aG docker $USER

# 2. 재로그인
exit

# 3. SSH 다시 접속

# 4. 확인
docker ps&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Docker</category>
      <category>compose</category>
      <category>docker</category>
      <category>ubuntu</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/297</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Docker-Ubuntu-%EC%84%9C%EB%B2%84%EC%97%90-docker-docker-compose-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0#entry297comment</comments>
      <pubDate>Thu, 15 Jan 2026 11:39:17 +0900</pubDate>
    </item>
    <item>
      <title>Js - 국내 전화번호 정규식</title>
      <link>https://dev-kimchi.tistory.com/entry/Js-%EA%B5%AD%EB%82%B4-%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8-%EC%A0%95%EA%B7%9C%EC%8B%9D</link>
      <description>&lt;h1&gt;2021. 12. 07 기준 전기통신번호관리세칙 기반 국내 전화번호 형식 종류&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;구분&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;번호&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;형식&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;세칙 근거&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;설명&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;시내전화&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;02-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;서울&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;031&lt;/td&gt;
&lt;td&gt;031-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;경기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;032&lt;/td&gt;
&lt;td&gt;032-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;인천&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;033&lt;/td&gt;
&lt;td&gt;033-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;강원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;041&lt;/td&gt;
&lt;td&gt;041-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;충남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;042&lt;/td&gt;
&lt;td&gt;042-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;대전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;043&lt;/td&gt;
&lt;td&gt;043-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;충북&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;044&lt;/td&gt;
&lt;td&gt;044-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;세종&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;051&lt;/td&gt;
&lt;td&gt;051-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;부산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;052&lt;/td&gt;
&lt;td&gt;052-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;울산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;053&lt;/td&gt;
&lt;td&gt;053-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;대구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;054&lt;/td&gt;
&lt;td&gt;054-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;경북&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;055&lt;/td&gt;
&lt;td&gt;055-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;경남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;061&lt;/td&gt;
&lt;td&gt;061-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;전남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;062&lt;/td&gt;
&lt;td&gt;062-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;광주&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;063&lt;/td&gt;
&lt;td&gt;063-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;전북&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;064&lt;/td&gt;
&lt;td&gt;064-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제7조③, 별표1&lt;/td&gt;
&lt;td&gt;제주&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;이동전화&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;010&lt;/td&gt;
&lt;td&gt;010-XXXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 3호 나,다&lt;/td&gt;
&lt;td&gt;2007.9.1 이후 통합번호 (셀룰러/PCS/IMT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;011&lt;/td&gt;
&lt;td&gt;011-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 3가호&lt;/td&gt;
&lt;td&gt;2007.9.1 이전 셀룰러/PCS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;016&lt;/td&gt;
&lt;td&gt;016-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 3가호&lt;/td&gt;
&lt;td&gt;2007.9.1 이전 셀룰러/PCS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;017&lt;/td&gt;
&lt;td&gt;017-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 3가호&lt;/td&gt;
&lt;td&gt;2007.9.1 이전 셀룰러/PCS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;018&lt;/td&gt;
&lt;td&gt;018-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 3가호&lt;/td&gt;
&lt;td&gt;2007.9.1 이전 셀룰러/PCS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;019&lt;/td&gt;
&lt;td&gt;019-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 3가호&lt;/td&gt;
&lt;td&gt;2007.9.1 이전 셀룰러/PCS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;인터넷전화&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;070&lt;/td&gt;
&lt;td&gt;070-XXXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 8호&lt;/td&gt;
&lt;td&gt;인터넷전화서비스 (VoIP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;특수목적&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;012&lt;/td&gt;
&lt;td&gt;012-XXXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 9호&lt;/td&gt;
&lt;td&gt;사물지능통신(IoT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;013X&lt;/td&gt;
&lt;td&gt;013X-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 6가호&lt;/td&gt;
&lt;td&gt;선박무선/주파수공용/무선데이터/5G특화망&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0135&lt;/td&gt;
&lt;td&gt;0135-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 6나호&lt;/td&gt;
&lt;td&gt;통합공공망 (재난안전/철도/해상무선)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;014XY&lt;/td&gt;
&lt;td&gt;014XY-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 7호&lt;/td&gt;
&lt;td&gt;부가통신역무 제공사업자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;015&lt;/td&gt;
&lt;td&gt;015-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 5호&lt;/td&gt;
&lt;td&gt;무선호출(페이저)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0100&lt;/td&gt;
&lt;td&gt;0100-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조③ 4호&lt;/td&gt;
&lt;td&gt;위성휴대통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;공통서비스&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;030&lt;/td&gt;
&lt;td&gt;030-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제8조④ 1호&lt;/td&gt;
&lt;td&gt;통합메시징 (음성/팩스/이메일 통합)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;(0N0계열)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;050&lt;/td&gt;
&lt;td&gt;050-XXX(X)-XXXX&lt;/td&gt;
&lt;td&gt;제8조④ 2호&lt;/td&gt;
&lt;td&gt;개인번호서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;060&lt;/td&gt;
&lt;td&gt;060-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조④ 3호&lt;/td&gt;
&lt;td&gt;전화정보서비스 (유료정보)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;080&lt;/td&gt;
&lt;td&gt;080-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제8조④ 4호&lt;/td&gt;
&lt;td&gt;착신과금서비스 (수신자부담 프리콜)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;특수번호&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10Y&lt;/td&gt;
&lt;td&gt;100~109&lt;/td&gt;
&lt;td&gt;제11조①&lt;/td&gt;
&lt;td&gt;전화번호안내 등 사업자 공통서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;(1YY계열)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11Y&lt;/td&gt;
&lt;td&gt;110~119&lt;/td&gt;
&lt;td&gt;제11조①&lt;/td&gt;
&lt;td&gt;공공기관 긴급신고 (112경찰, 119소방 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;12Y&lt;/td&gt;
&lt;td&gt;120~129&lt;/td&gt;
&lt;td&gt;제11조①&lt;/td&gt;
&lt;td&gt;공공기관 민원상담 (120다산콜 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;13YY&lt;/td&gt;
&lt;td&gt;1300~1399&lt;/td&gt;
&lt;td&gt;제11조①&lt;/td&gt;
&lt;td&gt;공공기관 재난안전/행정서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;14YY&lt;/td&gt;
&lt;td&gt;14YY-XXXX-XXXX&lt;/td&gt;
&lt;td&gt;제11조①, 제20조⑥&lt;/td&gt;
&lt;td&gt;대표번호 (수신자부담)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;15YY&lt;/td&gt;
&lt;td&gt;15YY-XXXX-XXXX&lt;/td&gt;
&lt;td&gt;제11조①, 제20조⑥&lt;/td&gt;
&lt;td&gt;대표번호 자동응답/결제호처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;16YY&lt;/td&gt;
&lt;td&gt;16YY-XXXX-XXXX&lt;/td&gt;
&lt;td&gt;제11조①, 제20조⑥&lt;/td&gt;
&lt;td&gt;대표번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;18YY&lt;/td&gt;
&lt;td&gt;18YY-XXXX-XXXX&lt;/td&gt;
&lt;td&gt;제11조①, 제20조⑥&lt;/td&gt;
&lt;td&gt;대표번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;17YY&lt;/td&gt;
&lt;td&gt;1700~1799&lt;/td&gt;
&lt;td&gt;제11조①&lt;/td&gt;
&lt;td&gt;예비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;19YY&lt;/td&gt;
&lt;td&gt;1900~1999&lt;/td&gt;
&lt;td&gt;제11조①&lt;/td&gt;
&lt;td&gt;예비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;사업자식별&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;00X&lt;/td&gt;
&lt;td&gt;00X-국제번호&lt;/td&gt;
&lt;td&gt;제8조③ 1가호&lt;/td&gt;
&lt;td&gt;국제전화 (회선설비보유, X≠3,7,9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;(국제/시외)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;003YY&lt;/td&gt;
&lt;td&gt;003YY-국제번호&lt;/td&gt;
&lt;td&gt;제8조③ 1나호&lt;/td&gt;
&lt;td&gt;국제전화 (설비보유재판매)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;007YY&lt;/td&gt;
&lt;td&gt;007YY-국제번호&lt;/td&gt;
&lt;td&gt;제8조③ 1나호&lt;/td&gt;
&lt;td&gt;국제전화 (설비보유재판매)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;08X&lt;/td&gt;
&lt;td&gt;08X-지역번호-번호&lt;/td&gt;
&lt;td&gt;제8조③ 2가호&lt;/td&gt;
&lt;td&gt;시외전화 (회선설비보유, X≠5,9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;085YY&lt;/td&gt;
&lt;td&gt;085YY-지역번호-번호&lt;/td&gt;
&lt;td&gt;제8조③ 2나호&lt;/td&gt;
&lt;td&gt;시외전화 (설비보유재판매)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;데이터망&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4500~4509&lt;/td&gt;
&lt;td&gt;4XXX-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제13조 1호&lt;/td&gt;
&lt;td&gt;데이터망 (기간통신사업자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;4800~4819&lt;/td&gt;
&lt;td&gt;48XX-XXX-XXXX&lt;/td&gt;
&lt;td&gt;제13조 2호&lt;/td&gt;
&lt;td&gt;데이터망 (부가통신사업자)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h1&gt;전화번호 검증 정규식&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;// 정규식: /^(02\d{7,8}|0(31|32|33|41|42|43|44|51|52|53|54|55|61|62|63|64)\d{7,8}|01[0-1|6-9]\d{7,8}|070\d{8}|012\d{8}|0100\d{7}|013[0-46-9]\d{7}|0135\d{7}|014\d{9}|015\d{7}|030\d{7}(?!\d)|050\d{7}(?!\d)|0(60|80)\d{7}|1[0-2]\d(?!\d)|1[3-9]\d{2}(?!\d)|1[4-68]\d{6}(?!\d)|1[4-68]\d{10}|(450[0-9]|48[01]\d)\d{7})$/

// 국내 전화번호 검증
const isPhoneNumber = (phone) =&amp;gt; /^(02\d{7,8}|0(31|32|33|41|42|43|44|51|52|53|54|55|61|62|63|64)\d{7,8}|01[0-1|6-9]\d{7,8}|070\d{8}|012\d{8}|0100\d{7}|013[0-46-9]\d{7}|0135\d{7}|014\d{9}|015\d{7}|030\d{7}(?!\d)|050\d{7}(?!\d)|0(60|80)\d{7}|1[0-2]\d(?!\d)|1[3-9]\d{2}(?!\d)|1[4-68]\d{6}(?!\d)|1[4-68]\d{10}|(450[0-9]|48[01]\d)\d{7})$/.test(String(phone).replace(/[\s\-().]/g, &amp;#39;&amp;#39;));

// 사용 예시
console.log(isPhoneNumber(&amp;#39;01012345678&amp;#39;));  // true
console.log(isPhoneNumber(&amp;#39;02-123-4567&amp;#39;));    // true
console.log(isPhoneNumber(&amp;#39;070-1234-5678&amp;#39;));  // true
console.log(isPhoneNumber(&amp;#39;112&amp;#39;));            // true
console.log(isPhoneNumber(&amp;#39;099-1234-5678&amp;#39;));  // false
console.log(isPhoneNumber(&amp;#39;1234&amp;#39;));           // false&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h1&gt;전기통신번호관리세칙 출처&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=35486&amp;amp;efYd=0&quot;&gt;국가법령정보센터 - https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=35486&amp;amp;efYd=0&lt;/a&gt;&lt;/p&gt;</description>
      <category>Js/JavaScript</category>
      <category>JS</category>
      <category>국가법령정보센터</category>
      <category>전기통신번호</category>
      <category>전기통신번호관리세칙</category>
      <category>전화번호</category>
      <category>정규식</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/296</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Js-%EA%B5%AD%EB%82%B4-%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8-%EC%A0%95%EA%B7%9C%EC%8B%9D#entry296comment</comments>
      <pubDate>Tue, 6 Jan 2026 10:52:45 +0900</pubDate>
    </item>
    <item>
      <title>DB - ACID</title>
      <link>https://dev-kimchi.tistory.com/entry/DB-ACID</link>
      <description>&lt;h1&gt;ACID란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACID는 데이터베이스 트랜잭션이 안전하게 처리되기 위해 반드시 지켜야 하는 4가지 속성이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Atomicity (원자성)&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;예시:&lt;/b&gt; 계좌이체 시 A계좌 출금과 B계좌 입금이 모두 성공하거나 모두 취소된다. 출금만 되고 입금 실패는 불가능하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consistency (일관성)&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;예시:&lt;/b&gt; 쇼핑몰에서 주문 시 &quot;재고수량 &amp;ge; 주문수량&quot; 규칙이 있다면, 재고 10개인 상품을 15개 주문하려고 하면 트랜잭션 자체가 거부된다. 트랜잭션이 완료되면 반드시 재고가 충분히 남아있는 상태가 보장된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Isolation (격리성)&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;예시:&lt;/b&gt; 콘서트 티켓 마지막 1장이 남았을 때 A와 B가 정확히 동시에 예매 버튼을 클릭했다. 격리성이 없으면 둘 다 &quot;1장 남음&quot;을 보고 둘 다 구매에 성공하는 문제가 발생한다. 격리성이 있으면 A의 트랜잭션이 끝날 때까지 B는 대기하고, A 구매 후 B는 &quot;품절&quot;을 확인한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Durability (지속성)&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;설명:&lt;/b&gt; 데이터베이스는 트랜잭션 커밋 시 디스크에 즉시 기록한다(메모리가 아닌). WAL(Write-Ahead Logging)을 사용해 변경사항을 먼저 로그 파일에 저장하고, 서버가 다운되어도 로그 파일이 디스크에 남아있어서 재시작 시 복구할 수 있다. Mysql은 InnoDB 엔진이 이 기능을 기본 제공한다.&lt;/p&gt;</description>
      <category>DataBase</category>
      <category>acid</category>
      <category>Atomicity</category>
      <category>consistency</category>
      <category>db</category>
      <category>durability</category>
      <category>Isolation</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/295</guid>
      <comments>https://dev-kimchi.tistory.com/entry/DB-ACID#entry295comment</comments>
      <pubDate>Wed, 10 Dec 2025 09:54:11 +0900</pubDate>
    </item>
    <item>
      <title>Mysql - 비관적 락(Pessimistic Lock)</title>
      <link>https://dev-kimchi.tistory.com/entry/Mysql-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BDPessimistic-Lock</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;70%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bplOsY/dJMcacayiku/hOdlQMSAzkRkEDwqg9vVEk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bplOsY/dJMcacayiku/hOdlQMSAzkRkEDwqg9vVEk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bplOsY/dJMcacayiku/hOdlQMSAzkRkEDwqg9vVEk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbplOsY%2FdJMcacayiku%2FhOdlQMSAzkRkEDwqg9vVEk%2Fimg.jpg&quot; width=&quot;70%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;락(Lock)에 대해 찾아보다 Mysql이 &amp;quot;자동으로 비관적 락을 건다&amp;quot;는 말을 발견했는데 &amp;#39;그럼 내가 따로 코드를 짤 필요가 없는 건가?&amp;#39; 싶은 마음에 헷갈려서 정리해보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Mysql 자동 락의 역할&lt;/h2&gt;
&lt;p&gt;Mysql(InnoDB)은 UPDATE나 DELETE 실행 시 자동으로 해당 행에 락을 건다.&lt;/p&gt;
&lt;h3&gt;예시: 동시 수정 방지&lt;/h3&gt;
&lt;p&gt;재고를 10으로 변경하는 A 요청과 20으로 변경하는 B 요청이 동시에 실행되는 상황이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// A 요청
UPDATE products SET stock = 10 WHERE id = 1;

// B 요청 (거의 동시)
UPDATE products SET stock = 20 WHERE id = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;자동 락 덕분에:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A의 UPDATE 실행 중 → B는 대기&lt;/li&gt;
&lt;li&gt;A 완료 → B 실행&lt;/li&gt;
&lt;li&gt;결과: stock = 20&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자동 락이 없었다면 두 UPDATE가 동시에 섞여서 데이터가 깨질 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;그럼 어떤 상황에서 코드상으로 비관적 락을 걸어야 하는가?&lt;/h2&gt;
&lt;p&gt;자동 락은 &lt;strong&gt;쿼리 실행 순간&lt;/strong&gt;만 보호한다. 실제 애플리케이션에서는 조회 -&amp;gt; 수정 순서로 진행되는데, 이 사이에 동시성 문제가 발생한다.&lt;/p&gt;
&lt;h3&gt;재고 차감 시나리오&lt;/h3&gt;
&lt;p&gt;재고 10개 있을 때, 두 명이 동시에 1개씩 주문한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// A 요청
$product = Product::find(1); // stock = 10 조회 (락 없음)

// B 요청 (동시에)
$product = Product::find(1); // stock = 10 조회 (락 없음)

// A 요청
$product-&amp;gt;update([&amp;#39;stock&amp;#39; =&amp;gt; $product-&amp;gt;stock - 1]); // 10 - 1 = 9

// B 요청
$product-&amp;gt;update([&amp;#39;stock&amp;#39; =&amp;gt; $product-&amp;gt;stock - 1]); // 10 - 1 = 9&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt;: 2번 차감했지만 stock = 9, 재고 1개가 증발했다.&lt;/p&gt;
&lt;p&gt;조회할 때는 락이 없어서 둘 다 10을 읽고, 각자 9로 업데이트해버린 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;코드상으로 비관적 락 추가&lt;/h2&gt;
&lt;p&gt;조회할 때부터 락을 걸어서 다른 트랜잭션이 접근하지 못하게 막는 방법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;DB::transaction(function () {
    // SELECT ... FOR UPDATE로 락 걸기
    $product = Product::lockForUpdate()-&amp;gt;find(1);

    $product-&amp;gt;update([&amp;#39;stock&amp;#39; =&amp;gt; $product-&amp;gt;stock - 1]);
}); // 트랜잭션 종료 시 락 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;동작 과정:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// A 요청
DB::transaction(function () {
    $product = Product::lockForUpdate()-&amp;gt;find(1); // stock = 10, 락 걸림
    $product-&amp;gt;update([&amp;#39;stock&amp;#39; =&amp;gt; 9]);
}); // 락 해제

// B 요청 (A가 끝날 때까지 대기)
DB::transaction(function () {
    $product = Product::lockForUpdate()-&amp;gt;find(1); // stock = 9 조회
    $product-&amp;gt;update([&amp;#39;stock&amp;#39; =&amp;gt; 8]);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt;: stock = 8&lt;/p&gt;
&lt;h3&gt;비관적 락 종류&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// 배타적 락 (Exclusive Lock) - 읽기/쓰기 모두 차단
Product::lockForUpdate()-&amp;gt;find(1);

// 공유 락 (Shared Lock) - 읽기 허용, 쓰기만 차단
Product::sharedLock()-&amp;gt;find(1);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;주의사항&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;데드락 위험&lt;/strong&gt;: 여러 테이블 락 시 순서 주의&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;성능 저하&lt;/strong&gt;: 대기 시간 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;일상적으로 접하는 DB에 알면 알수록 다양한 기능이 포함되어있다는 사실이 놀라웠고, 코드로 작성하는 비관적 락과의 차이점도 분명하게 정리된 것 같아 유익한 시간이 된 것 같다.&lt;/p&gt;</description>
      <category>DataBase/MySQL</category>
      <category>db</category>
      <category>Exclusive</category>
      <category>Lock</category>
      <category>Pessimistic</category>
      <category>shared</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/294</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Mysql-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BDPessimistic-Lock#entry294comment</comments>
      <pubDate>Wed, 10 Dec 2025 09:47:44 +0900</pubDate>
    </item>
    <item>
      <title>DB - 파티셔닝과 샤딩</title>
      <link>https://dev-kimchi.tistory.com/entry/DB-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%EA%B3%BC-%EC%83%A4%EB%94%A9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YnwOH/dJMcafLSiAL/JvkjQ7DxwOWnSkHt1x1FDk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YnwOH/dJMcafLSiAL/JvkjQ7DxwOWnSkHt1x1FDk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YnwOH/dJMcafLSiAL/JvkjQ7DxwOWnSkHt1x1FDk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYnwOH%2FdJMcafLSiAL%2FJvkjQ7DxwOWnSkHt1x1FDk%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;개발을 하다보면 파티셔닝과 샤딩이라는 용어를 종종 듣게 된다. 찾아보니 둘 다 데이터를 분할한다는 공통점이 있지만, 작동 방식과 목적이 완전히 다르다는 것을 알게 되었다. 이에 대한 내용과 더불어 샤딩을 구현할 때 Laravel ORM에서 어떻게 특정 샤드를 찾아가는지에 대한 내용까지 정리해보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;파티셔닝 (Partitioning)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;하나의 데이터베이스 서버 내에서&lt;/strong&gt; 대용량 테이블을 물리적으로 여러 조각으로 분할하는 기술&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;주요 특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 서버, 단일 DB 인스턴스에서 동작&lt;/li&gt;
&lt;li&gt;쿼리 성능 향상 (필요한 파티션만 스캔)&lt;/li&gt;
&lt;li&gt;유지보수 용이 (파티션 단위 삭제/아카이빙)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MariaDB는 RANGE, LIST, HASH, KEY 네 가지 파티셔닝을 지원한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- RANGE 파티셔닝 예시
CREATE TABLE orders (
    id BIGINT,
    created_at DATETIME,
    ...
) PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026)
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;샤딩 (Sharding)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;여러 데이터베이스 서버&lt;/strong&gt;로 데이터를 수평 분할하는 기술&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;주요 특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다중 서버에 데이터 분산&lt;/li&gt;
&lt;li&gt;수평 확장 가능 (Scale-out)&lt;/li&gt;
&lt;li&gt;애플리케이션 레벨에서 라우팅 로직 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Laravel에서는 다중 DB 연결 설정으로 구현한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// config/database.php
&amp;#39;connections&amp;#39; =&amp;gt; [
    &amp;#39;shard_0&amp;#39; =&amp;gt; [
        &amp;#39;driver&amp;#39; =&amp;gt; &amp;#39;mysql&amp;#39;,
        &amp;#39;host&amp;#39; =&amp;gt; &amp;#39;10.0.1.1&amp;#39;,
        &amp;#39;database&amp;#39; =&amp;gt; &amp;#39;app_shard_0&amp;#39;,
        ...
    ],
    &amp;#39;shard_1&amp;#39; =&amp;gt; [
        &amp;#39;driver&amp;#39; =&amp;gt; &amp;#39;mysql&amp;#39;,
        &amp;#39;host&amp;#39; =&amp;gt; &amp;#39;10.0.1.2&amp;#39;,
        &amp;#39;database&amp;#39; =&amp;gt; &amp;#39;app_shard_1&amp;#39;,
        ...
    ],
],&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심 차이:&lt;/strong&gt; 파티셔닝은 단일 서버 내 최적화, 샤딩은 다중 서버로 확장성 확보다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ORM에서 샤딩 처리 방법&lt;/h2&gt;
&lt;h3&gt;Laravel Eloquent의 on() 메서드&lt;/h3&gt;
&lt;p&gt;Laravel은 &lt;code&gt;on()&lt;/code&gt; 메서드로 동적 연결 선택을 지원한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// config/database.php 설정만 되어 있으면
&amp;#39;connections&amp;#39; =&amp;gt; [
    &amp;#39;shard_0&amp;#39; =&amp;gt; [&amp;#39;driver&amp;#39; =&amp;gt; &amp;#39;mysql&amp;#39;, &amp;#39;host&amp;#39; =&amp;gt; &amp;#39;10.0.1.1&amp;#39;, ...],
    &amp;#39;shard_1&amp;#39; =&amp;gt; [&amp;#39;driver&amp;#39; =&amp;gt; &amp;#39;mysql&amp;#39;, &amp;#39;host&amp;#39; =&amp;gt; &amp;#39;10.0.1.2&amp;#39;, ...],
],

// 바로 사용 가능
User::on(&amp;#39;shard_0&amp;#39;)-&amp;gt;find(1);
User::on(&amp;#39;shard_1&amp;#39;)-&amp;gt;where(&amp;#39;status&amp;#39;, &amp;#39;active&amp;#39;)-&amp;gt;get();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심:&lt;/strong&gt; ORM은 단일 연결만 처리하므로, 개발자가 연결 선택(&lt;code&gt;on()&lt;/code&gt;)을 명시해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;파티셔닝과 샤딩을 막연하게만 알고 있었는데, 이번에 정리해보며 훨씬 명확히 기억할 수 있게 되었다. 특히 ORM에서 &lt;code&gt;User::on(&amp;#39;shard_0&amp;#39;)&lt;/code&gt;처럼 간단하게 연결을 지정할 수 있다는 점이 놀라웠다.&lt;/p&gt;</description>
      <category>DataBase</category>
      <category>ORM</category>
      <category>Part</category>
      <category>Partitioning</category>
      <category>shard</category>
      <category>sharding</category>
      <category>SQL</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/293</guid>
      <comments>https://dev-kimchi.tistory.com/entry/DB-%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%EA%B3%BC-%EC%83%A4%EB%94%A9#entry293comment</comments>
      <pubDate>Mon, 8 Dec 2025 17:52:57 +0900</pubDate>
    </item>
    <item>
      <title>Laravel - encrypt, bcrypt의 차이와 안전성</title>
      <link>https://dev-kimchi.tistory.com/entry/Laravel-encrypt-bcrypt%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%99%80-%EC%95%88%EC%A0%84%EC%84%B1</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;50%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0rfjF/dJMcachezNR/ty9w3PwV061Vk6GI5hcYwK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0rfjF/dJMcachezNR/ty9w3PwV061Vk6GI5hcYwK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0rfjF/dJMcachezNR/ty9w3PwV061Vk6GI5hcYwK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0rfjF%2FdJMcachezNR%2Fty9w3PwV061Vk6GI5hcYwK%2Fimg.jpg&quot; width=&quot;50%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;Laravel에서 데이터 암호화에 사용되는 encrypt와 비밀번호 해싱에 사용되는 bcrypt의 차이점과, 각각의 salt 방식, 그리고 bcrypt가 APP_KEY 없이도 충분히 안전한 이유를 정리해보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;encrypt와 bcrypt의 근본적 차이&lt;/h2&gt;
&lt;h3&gt;암호화 방향성&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;encrypt (양방향 암호화)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$encrypted = encrypt(&amp;#39;신용카드번호&amp;#39;);
$original = decrypt($encrypted); // 복호화 가능&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;: 나중에 원본 데이터가 필요한 경우&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예시&lt;/strong&gt;: 신용카드 번호, API 토큰, 개인정보&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: APP_KEY를 사용하여 암호화/복호화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;bcrypt (단방향 해싱)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$hashed = bcrypt(&amp;#39;password123&amp;#39;);
// 복호화 불가능, 검증만 가능
Hash::check(&amp;#39;password123&amp;#39;, $hashed); // true/false&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;: 원본을 절대 알 필요 없는 데이터&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예시&lt;/strong&gt;: 사용자 비밀번호&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;: 같은 입력값도 매번 다른 해시 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;사용 기준&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;원본 데이터 필요&lt;/strong&gt; → encrypt() 사용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;원본 데이터 불필요, 검증만 필요&lt;/strong&gt; → bcrypt() 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;encrypt의 salt 방식&lt;/h2&gt;
&lt;h3&gt;APP_KEY 의존성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// .env 파일
APP_KEY=base64:xvE7QxT8E9yP...

// 암호화
$encrypted = encrypt(&amp;#39;민감한 데이터&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;동작 원리:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Laravel의 APP_KEY를 암호화 키로 사용&lt;/li&gt;
&lt;li&gt;같은 데이터도 매번 다른 암호문 생성 (내부적으로 랜덤 IV 사용)&lt;/li&gt;
&lt;li&gt;복호화 시 동일한 APP_KEY 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;프로젝트 간 이동 불가&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// A 프로젝트 (APP_KEY: xxx)
$encrypted = encrypt(&amp;#39;data&amp;#39;);

// B 프로젝트 (APP_KEY: yyy)
decrypt($encrypted); // DecryptException 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APP_KEY가 다르면 복호화 불가능&lt;/li&gt;
&lt;li&gt;APP_KEY 변경 시 기존 암호화 데이터 모두 무효화&lt;/li&gt;
&lt;li&gt;단, 개발자가 구 키로 복호화 후 신 키로 재암호화 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;bcrypt의 salt 방식&lt;/h2&gt;
&lt;h3&gt;자체 salt 포함&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$hash = bcrypt(&amp;#39;password123&amp;#39;);
// 결과: $2y$10$N9qo8uLOickgx2ZMRZoMye1234567890abcdefghijklmnopqr
// 구조: $알고리즘$cost$salt(22자)해시값(31자)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심 특징:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;매번 랜덤 salt 자동 생성&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;salt가 해시 안에 포함되어 저장&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APP_KEY 사용 안 함&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;동일한 비밀번호, 다른 해시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$hash1 = bcrypt(&amp;#39;password123&amp;#39;);
$hash2 = bcrypt(&amp;#39;password123&amp;#39;);

// $hash1 !== $hash2 (salt가 다름)
// 하지만 둘 다 검증 가능
Hash::check(&amp;#39;password123&amp;#39;, $hash1); // true
Hash::check(&amp;#39;password123&amp;#39;, $hash2); // true&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;프로젝트 간 이동 가능&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// A 프로젝트
$hash = bcrypt(&amp;#39;password123&amp;#39;);

// B 프로젝트로 해시 복사
Hash::check(&amp;#39;password123&amp;#39;, $hash); // true
// salt가 해시에 포함되어 있어 어디서든 검증 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;이동 가능한 이유:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;salt가 해시 안에 포함됨&lt;/li&gt;
&lt;li&gt;APP_KEY를 사용하지 않음&lt;/li&gt;
&lt;li&gt;bcrypt 알고리즘이 표준이라 어디서든 동일하게 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;bcrypt가 APP_KEY 없이도 안전한 이유&lt;/h2&gt;
&lt;h3&gt;1. 작업 증명(Proof of Work) 방식&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// bcrypt는 의도적으로 느리게 설계됨
$hash = bcrypt(&amp;#39;password&amp;#39;); // 약 0.3초 소요

// 무차별 대입 공격 시
// 1억 개 시도 = 0.3초 × 100,000,000 = 347일
// 병렬 처리(100코어) = 3.47일&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 시도마다 계산 비용 강제&lt;/li&gt;
&lt;li&gt;하드웨어로 극복하기 어려움&lt;/li&gt;
&lt;li&gt;GPU 가속 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. APP_KEY 추가의 실질적 이득 없음&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;DB만 유출된 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt; bcrypt 단독
 - 무차별 대입 필요 (347일)

 bcrypt + APP_KEY
 - APP_KEY 모르므로 정확한 입력 조합 불가능
 - 약간 더 안전할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;DB + .env 모두 유출된 경우:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;bcrypt 단독
 - 무차별 대입 필요 (347일)

bcrypt + APP_KEY  
 - 무차별 대입 필요 (347일)
 - 보안 수준 동일&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;결론:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;보안 향상: 매우 미미함&lt;/li&gt;
&lt;li&gt;실무에서 .env 유출 = 서버 전체 장악 상황&lt;/li&gt;
&lt;li&gt;이미 심각한 보안 사고이기에 bcrypt + APP_KEY로 막을 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 운영 리스크 vs 보안 이득&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;APP_KEY 변경 시 영향:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;encrypt 데이터&lt;/th&gt;
&lt;th&gt;bcrypt 단독&lt;/th&gt;
&lt;th&gt;bcrypt + APP_KEY&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;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;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;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;td&gt;전체 리셋 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;시나리오: APP_KEY 유출로 긴급 변경&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt; bcrypt 단독
 1. APP_KEY 즉시 변경
 2. 암호화 데이터만 재암호화
 3. 사용자는 계속 로그인 가능 ✓

 bcrypt + APP_KEY
 1. APP_KEY 변경 불가 (사용자 영향 고려)
 2. 또는 변경 후 전체 사용자 비밀번호 리셋
 3. 수천/수만 명 동시 로그인 불가
 4. CS 문의 폭증, 사용자 이탈&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 보안 철학과 표준&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kerckhoffs의 원칙:&lt;/strong&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;&amp;quot;암호 시스템의 안전성은 키의 비밀성에만 의존해야 하며, 알고리즘 자체는 공개되어도 안전해야 한다&amp;quot;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt; bcrypt 단독
 ✓ 해시 알고리즘 공개
 ✓ 해시값 유출되어도 안전 (무차별 대입 필요)
 ✓ 단일 실패 지점 없음

 bcrypt + APP_KEY
 ✗ APP_KEY = 단일 실패 지점 생성
 ✗ 추가 의존성 = 복잡도 증가
 ✗ 실질적 보안 이득 미미&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;OWASP, NIST 권장사항:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;비밀번호 해싱에 애플리케이션 시크릿 혼합 금지&lt;/li&gt;
&lt;li&gt;bcrypt/argon2 자체 salt로 충분&lt;/li&gt;
&lt;li&gt;&amp;quot;간단한 것이 안전하다&amp;quot; - 보안의 기본 원칙&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 복구 가능성의 차이&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt; encrypt 데이터 (개인정보, 결제정보)
 - 양방향 암호화
 - 백업에서 복구 후 재암호화 가능
 - 개발자가 처리 가능
 - 사용자 액션 불필요

 bcrypt 비밀번호
 - 단방향 해싱
 - 백업에도 해시만 있음
 - 개발자가 복구 불가능
 - 사용자만 리셋 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;이것이 bcrypt를 APP_KEY와 분리하는 핵심 이유:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개인정보: 개발자가 복구 가능 (시간은 걸려도)&lt;/li&gt;
&lt;li&gt;비밀번호: 복구 불가능 (사용자만 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;일단 첫째로 bcrypt/encrypt가 단/양방향 암호화라는걸 몰랐던 나 자신의 멍청함에 한 번 놀랐고 bcrypt로 암호화할때 app key로 추가 해싱해봤자 보안적으로 큰 이득이 없다는 사실에 또 한 번 놀랐다. 또한 app key가 유출 되어 변경할 경우 기존 암호화 데이터 복구 방안에 대한 내용은 전혀 생각치 못한 부분이라 많은 걸 배우게 된 것 같다.&lt;/p&gt;</description>
      <category>PHP/laravel</category>
      <category>app</category>
      <category>BCrypt</category>
      <category>encrypt</category>
      <category>hash</category>
      <category>Hashing</category>
      <category>key</category>
      <category>laravel</category>
      <category>php</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/292</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Laravel-encrypt-bcrypt%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%99%80-%EC%95%88%EC%A0%84%EC%84%B1#entry292comment</comments>
      <pubDate>Tue, 25 Nov 2025 18:04:17 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - Too many open files 에러 / 1024 worker_connections are not enough 에러</title>
      <link>https://dev-kimchi.tistory.com/entry/Nginx-Too-many-open-files-%EC%97%90%EB%9F%AC-1024-workerconnections-are-not-enough-%EC%97%90%EB%9F%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;nginx2.jpg&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CHnGL/dJMcaesrX3S/foPk94ZW2VTeSyX2g9fxz1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CHnGL/dJMcaesrX3S/foPk94ZW2VTeSyX2g9fxz1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CHnGL/dJMcaesrX3S/foPk94ZW2VTeSyX2g9fxz1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCHnGL%2FdJMcaesrX3S%2FfoPk94ZW2VTeSyX2g9fxz1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;334&quot; height=&quot;392&quot; data-filename=&quot;nginx2.jpg&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;/figure&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;대용량 트래픽 테스트 중 Nginx 캐싱을 적용한 후, 추가적인 시스템 레벨 병목 현상을 발견하게 되었다. 캐시 설정만으로는 해결되지 않는 파일 디스크립터 부족과 워커 커넥션 한계 문제를 해결한 과정을 기록해보았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Too many open files 에러 해결&lt;/h2&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;/etc/nginx/nginx.conf&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 내용 추가:&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;worker_rlimit_nofile 65535;
&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo systemctl edit nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 내용 추가:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[Service]
LimitNOFILE=65536
&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;sudo systemctl daemon-reload
sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;worker_rlimit_nofile를 최대값(65535)으로 설정해도 되는 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Nginx 워커 프로세스별 제한&lt;/b&gt;: 각 워커 프로세스가 독립적으로 파일 디스크립터를 사용하므로 시스템 전체 한계와 별개&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 사용량은 워커 커넥션에 의존&lt;/b&gt;: 실제 열리는 파일은 worker_connections 설정값과 동시 접속 수에 따라 결정됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여유 확보&lt;/b&gt;: 캐시 파일, 로그 파일, 업스트림 연결 등을 고려하면 넉넉한 설정이 안전&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. worker_connections 부족 에러 해결&lt;/h2&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;/etc/nginx/nginx.conf&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적정 worker_connections 설정 방법&lt;/h3&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;실제 필요한 커넥션 수는 트래픽 패턴, keep-alive 설정, 응답 속도 등 다양한 요인에 따라 달라지므로 모니터링과 부하 테스트를 통한 점진적 조정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일반적인 시작값:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소규모: 1024&lt;/li&gt;
&lt;li&gt;중규모: 4096&lt;/li&gt;
&lt;li&gt;대규모: 8192~16384&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모니터링 기반 조정:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 현재 활성 커넥션 수 확인
sudo netstat -an | grep :80 | wc -l&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;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Apache Bench 예시
ab -n 10000 -c 100 http://example.com&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;worker_connections are not enough&quot; 로그 발생 &amp;rarr; 값 증가&lt;/li&gt;
&lt;li&gt;CPU 사용률 비정상 급증 &amp;rarr; 커넥션 부족 가능성&lt;/li&gt;
&lt;li&gt;정상 처리 중 &amp;rarr; 현재 값 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종 설정 예시:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;worker_processes auto;  # CPU 코어 수 자동 설정
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;  # 중규모 시작값
    multi_accept on;          # 동시 다중 연결 수락(메모리 사용량 증가, 처리 속도 향상)
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;worker_connections는 worker_rlimit_nofile보다 작아야 함&lt;/li&gt;
&lt;li&gt;프록시 사용 시 커넥션 2배 소비 (클라이언트 &amp;harr; Nginx &amp;harr; 백엔드)&lt;/li&gt;
&lt;li&gt;실제 환경에서 테스트 후 점진적으로 조정&lt;/li&gt;
&lt;/ul&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;캐시 설정으로 PHP-FPM 부하를 줄였지만, Nginx 자체의 시스템 리소스 한계를 간과하면 또 다른 병목이 발생한다는 것을 배웠다. 특히 worker_connections는 이론적 공식이 없어 실제 트래픽 패턴에 맞춰 조정해야 하며, 모니터링과 부하 테스트를 통한 경험적 최적화가 필수적임을 알게 되었다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>Connections</category>
      <category>error</category>
      <category>nginx</category>
      <category>processes</category>
      <category>WORKER</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/291</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Nginx-Too-many-open-files-%EC%97%90%EB%9F%AC-1024-workerconnections-are-not-enough-%EC%97%90%EB%9F%AC#entry291comment</comments>
      <pubDate>Sun, 2 Nov 2025 18:40:53 +0900</pubDate>
    </item>
    <item>
      <title>PHP - Nginx 캐싱 설정</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-Nginx-%EC%BA%90%EC%8B%B1-%EC%84%A4%EC%A0%95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;50%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbUG3I/dJMcafSqCXH/qz9zNK3VxV1hQhY9ejAaJk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbUG3I/dJMcafSqCXH/qz9zNK3VxV1hQhY9ejAaJk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbUG3I/dJMcafSqCXH/qz9zNK3VxV1hQhY9ejAaJk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbUG3I%2FdJMcafSqCXH%2Fqz9zNK3VxV1hQhY9ejAaJk%2Fimg.jpg&quot; width=&quot;50%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;laravel 서버의 대용량 트래픽 처리에 대해 테스트하다 PHP FPM 프로세스의 부족으로 인해 병목 현상이 발생함을 발견하게 되었고 서버 스펙을 무리하게 올리기 보다는 다른 방식으로 해결을 하는 것이 바람직하다 판단되어 Nginx 캐싱에 대해 찾아보게 되었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;/etc/nginx/sites-available/default&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# server 블록 상단에 추가
fastcgi_cache_path /dev/shm/nginx_cache
levels=1:2
keys_zone=PHPCACHE:100m
max_size=500m
inactive=60m
use_temp_path=off

fastcgi_cache_use_stale error timeout updating invalid_header http_500;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;

server {

...

  location ~ \.php$ {
      set $skip_cache 1;  # 기본값: 캐시 안함

      # 예시) 메인페이지(빈 문자열 매칭), /project 페이지 캐시
      if ($request_uri ~* &amp;quot;^/(project|)$&amp;quot;) {
          set $skip_cache 0;
      }

      # POST는 캐시 안함
      if ($request_method = POST) {
          set $skip_cache 1;
      }

      fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
      fastcgi_cache_bypass $skip_cache;
      fastcgi_no_cache $skip_cache;

      fastcgi_cache PHPCACHE;
      fastcgi_cache_valid 200 10m;
      fastcgi_cache_key &amp;quot;$scheme$request_method$host$request_uri&amp;quot;;
      add_header X-Cache-Status $upstream_cache_status always;

      fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
      fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
      include fastcgi_params;
  }

...
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h3&gt;각 항목에 대한 설명 - server 블록 상단(전역 설정)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache_path /dev/shm&lt;/code&gt;: 캐시 파일 저장 경로(RAM을 사용하는 경로)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;levels&lt;/code&gt;: 해시값으로 &lt;code&gt;a/bc/캐시파일&lt;/code&gt; 형태 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keys_zone&lt;/code&gt;: 공유 메모리 영역 이름과 크기 (1MB당 약 8,000개 키)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_size&lt;/code&gt;: 최대 캐시 용량&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inactive&lt;/code&gt;: 미사용 캐시 삭제 시간 (60분간 접근 없으면 자동 삭제, 만료된 캐시라도 추후 서버 장애등의 상황을 대비하기 위해 넉넉하게 설정)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;use_temp_path&lt;/code&gt;: 임시 경로 사용 여부&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache_use_stale&lt;/code&gt; : 백엔드 오류 시 만료된 캐시 제공&lt;ul&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt;: PHP-FPM 연결 실패&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt;: 응답 지연&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updating&lt;/code&gt;: 캐시 갱신 중&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invalid_header&lt;/code&gt;: 잘못된 헤더&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http_500&lt;/code&gt;: 500 에러&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache_background_update&lt;/code&gt;: 활성화 시 캐시가 만료되면 백그라운드에서 갱신하고 기존 캐시 즉시 반환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache_lock&lt;/code&gt;: 활성화 시 동일 요청이 동시 발생하면 하나만 백엔드 호출, 나머지는 대기 (Thundering herd 방지)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;각 항목에 대한 설명 - server 블록 내부&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fastcgi_ignore_headers&lt;/code&gt;: PHP 응답 헤더 무시 (Cache-Control, Expires, Set-Cookie 무시하고 Nginx가 캐시 제어)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache_bypass&lt;/code&gt;: &lt;code&gt;$skip_cache&lt;/code&gt; 변수가 true면 캐시 읽기 건너뛰고 백엔드 직접 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_no_cache&lt;/code&gt;: &lt;code&gt;$skip_cache&lt;/code&gt; 변수가 true면 응답을 캐시에 저장하지 않음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache&lt;/code&gt;: 사용할 캐시 존 지정 (위에서 정의한 PHPCACHE 존 사용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache_valid&lt;/code&gt;: HTTP 200 응답을 10분간 캐시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_cache_key&lt;/code&gt;: 캐시 키 생성 규칙 (스킴+메소드+호스트+URI 조합으로 고유 키 생성)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add_header X-Cache-Status&lt;/code&gt;: 응답 헤더에 캐시 상태 추가 (HIT/MISS/BYPASS 등 확인 가능)&lt;ul&gt;
&lt;li&gt;&lt;code&gt;always&lt;/code&gt;: 모든 응답 코드에 헤더 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_pass&lt;/code&gt;: PHP-FPM 소켓 경로 (Unix 소켓 사용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fastcgi_param SCRIPT_FILENAME&lt;/code&gt;: PHP 스크립트 실제 경로 전달 ($realpath_root로 심볼릭 링크 해석)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;include fastcgi_params&lt;/code&gt;: 표준 FastCGI 파라미터 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;사실 이전에 다니던 회사에서는 너무 다양하게 걸려있는 캐싱이 불필요하고 변동사항이 바로 확인되지 않아 혼란만 초래하니 다 없애버려야한다고 부정적으로만 생각했었는데 이제와 돌이켜보니 순간적으로 치솟는 트래픽에도 잘 버티기 위한 조치였음을 깨닫게 되었다. 물론 관리가 되지 않는 캐싱 시스템은 혼란만 초래할 수도 있지만 잘만 사용한다면 서버 리소스를 아낄 수 있는 양날의 검임을 다시 한 번 깨닫게 되었다.&lt;/p&gt;</description>
      <category>PHP/PHP</category>
      <category>cache</category>
      <category>FastCgi</category>
      <category>FPM</category>
      <category>laravel</category>
      <category>nginx</category>
      <category>php</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/290</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-Nginx-%EC%BA%90%EC%8B%B1-%EC%84%A4%EC%A0%95#entry290comment</comments>
      <pubDate>Sun, 2 Nov 2025 17:46:33 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - www, non-www redirect 시 도메인 검색엔진의 색인 방식</title>
      <link>https://dev-kimchi.tistory.com/entry/Nginx-www-non-www-redirect-%EC%8B%9C-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B2%80%EC%83%89%EC%97%94%EC%A7%84%EC%9D%98-%EC%83%89%EC%9D%B8-%EB%B0%A9%EC%8B%9D</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bixJR9/dJMcafLD2RI/TPuBRm7TrCfduUkeXPrpyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bixJR9/dJMcafLD2RI/TPuBRm7TrCfduUkeXPrpyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bixJR9/dJMcafLD2RI/TPuBRm7TrCfduUkeXPrpyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbixJR9%2FdJMcafLD2RI%2FTPuBRm7TrCfduUkeXPrpyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Nginx에서 www 서브도메인을 메인 도메인으로 리다이렉트 설정 시, 검색엔진이 어떤 도메인을 색인하는지에 대한 내용을 정리해보았다.&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;질문의 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx에서 &lt;code&gt;www.example.com&lt;/code&gt;을 &lt;code&gt;example.com&lt;/code&gt;으로 301 리다이렉트 설정 시, 검색엔진(구글, 네이버)이 www 도메인을 노출할지에 대한 의문이 발생하였다.&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;b&gt;크롤러의 리다이렉트 추적&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;크롤러 접근: www.example.com
       &amp;darr;
Nginx 응답: 301 &amp;rarr; example.com
       &amp;darr;
크롤러 인식: &quot;example.com이 정규 URL&quot;
       &amp;darr;
검색 결과: example.com으로 색인&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색엔진은 301 리다이렉트를 따라가며 최종 도착 URL을 정규 버전으로 인식&lt;/li&gt;
&lt;li&gt;리다이렉트 대상(destination)이 검색 결과에 노출됨&lt;/li&gt;
&lt;li&gt;www &amp;rarr; non-www 리다이렉트 = non-www가 색인됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Nginx 리다이렉트 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권장 설정&lt;/h3&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# www를 non-www로 리다이렉트
server {
    listen 80;
    server_name www.example.com;
    return 301 $scheme://example.com$request_uri;
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 결과: &lt;code&gt;example.com&lt;/code&gt; (www 없이)&lt;/li&gt;
&lt;li&gt;사용자가 www로 접근해도 자동으로 non-www로 이동&lt;/li&gt;
&lt;li&gt;모든 SEO 점수가 하나의 도메인에 집중&lt;/li&gt;
&lt;/ul&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;왜 불가능하고 하면 안 되는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 검색엔진의 중복 콘텐츠 페널티&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 내용이 2개 URL에 존재 = 검색 순위 하락&lt;/li&gt;
&lt;li&gt;검색엔진이 임의로 하나만 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. SEO 점수 분산&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;www로 받은 백링크와 non-www로 받은 백링크가 따로 계산&lt;/li&gt;
&lt;li&gt;도메인 권위도(Domain Authority)가 분산되어 불리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 검색엔진의 강제 선택&lt;/b&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;일관성 없는 검색 결과&lt;/li&gt;
&lt;/ul&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;하나를 선택하고 나머지는 301 리다이렉트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 옵션 1: non-www 선호 (일반적)
www.example.com &amp;rarr; example.com

# 옵션 2: www 선호
example.com &amp;rarr; www.example.com&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가 SEO 최적화:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Search Console에 대표 도메인 지정&lt;/li&gt;
&lt;li&gt;sitemap.xml에는 선택한 버전만 포함&lt;/li&gt;
&lt;li&gt;canonical 태그로 정규 URL 명시&lt;/li&gt;
&lt;/ul&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;반드시 이해할 사항&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;301 리다이렉트 = 영구 이동&lt;/b&gt;: &quot;이 주소는 이제 저기로 완전히 옮겼어요&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검색엔진은 리다이렉트 대상을 색인&lt;/b&gt;: 출발지가 아닌 도착지 URL&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하나의 정규 URL만 유지&lt;/b&gt;: SEO를 위한 필수 조건&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리다이렉트 방향 선택&lt;/b&gt;: www/non-www 중 하나를 비즈니스 요구사항에 맞게 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 권장사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대부분의 경우&lt;/b&gt;: non-www 사용 (간결함)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브랜드 강조 필요&lt;/b&gt;: www 사용 (전통적, 공식적 느낌)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성 유지&lt;/b&gt;: 한 번 선택하면 변경하지 않기&lt;/li&gt;
&lt;/ul&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;도메인 정규화는 SEO의 기본이지만 간과하기 쉬운 부분이다. 처음부터 하나의 버전으로 통일하고 리다이렉트를 설정하는 것이 중요하다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>domain</category>
      <category>nginx</category>
      <category>non</category>
      <category>redirect</category>
      <category>SEO</category>
      <category>WWW</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/289</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Nginx-www-non-www-redirect-%EC%8B%9C-%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B2%80%EC%83%89%EC%97%94%EC%A7%84%EC%9D%98-%EC%83%89%EC%9D%B8-%EB%B0%A9%EC%8B%9D#entry289comment</comments>
      <pubDate>Thu, 30 Oct 2025 10:13:39 +0900</pubDate>
    </item>
    <item>
      <title>PHP -dom pdf로 html -&amp;gt; pdf 변환</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-dom-pdf%EB%A1%9C-html-pdf-%EB%B3%80%ED%99%98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DhBui/dJMcaacuIM3/559QzpOg7AhFUxHtzlPiY0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DhBui/dJMcaacuIM3/559QzpOg7AhFUxHtzlPiY0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DhBui/dJMcaacuIM3/559QzpOg7AhFUxHtzlPiY0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDhBui%2FdJMcaacuIM3%2F559QzpOg7AhFUxHtzlPiY0%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;DomPDF로 PDF를 생성할 때 한글이 깨지는 문제가 발생하여 폰트 등록부터 인코딩 문제까지, 여러 시행착오를 겪어가며 해결하였다.&lt;br&gt;하지만 DomPDF로 PDF를 생성하는 방법은 font 관련 css를 넣었을때 또 깨지는 문제가 발생하고 일부 css가 먹질 않아서 폐기하게 되었다.&lt;br&gt;그리하여 결국에는 이 방식을 사용하지 않게 되었지만 그냥 갖다버리기는 아까워서 글로라도 남기려한다...&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;DomPDF 한글 깨짐 문제&lt;/h2&gt;
&lt;h3&gt;문제의 원인&lt;/h3&gt;
&lt;p&gt;DomPDF는 기본 폰트(DejaVu Sans)에 &lt;strong&gt;한글 글리프가 없다&lt;/strong&gt;. 한글을 표시하려면 한글을 지원하는 폰트를 명시적으로 등록해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증상별 원인&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt; (물음표): 인코딩 문제 또는 폰트 미등록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;□&lt;/code&gt; (네모): 폰트에 해당 글리프 없음&lt;/li&gt;
&lt;li&gt;깨진 문자: UTF-8 인코딩 미지정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;해결 과정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1단계: 인코딩 확인&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$this-&amp;gt;dompdf-&amp;gt;loadHtml($html, &amp;#39;UTF-8&amp;#39;); // 명시적 인코딩 지정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2단계: 한글 지원 폰트 준비&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pretendard, Noto Sans KR, 나눔고딕 등&lt;/li&gt;
&lt;li&gt;반드시 &lt;strong&gt;TTF 형식&lt;/strong&gt; 사용 (OTF는 제한적)&lt;/li&gt;
&lt;li&gt;폰트 파일을 &lt;code&gt;public/fonts/&lt;/code&gt;에 배치&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3단계: 폰트 수동 등록&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$fontMetrics = $this-&amp;gt;dompdf-&amp;gt;getFontMetrics();
$fontMetrics-&amp;gt;registerFont([
    &amp;#39;family&amp;#39; =&amp;gt; &amp;#39;Pretendard&amp;#39;,
    &amp;#39;style&amp;#39; =&amp;gt; &amp;#39;normal&amp;#39;,
    &amp;#39;weight&amp;#39; =&amp;gt; &amp;#39;normal&amp;#39;
], public_path(&amp;#39;fonts/Pretendard.ttf&amp;#39;));&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;최종 구현 코드&lt;/h2&gt;
&lt;h3&gt;PdfHelper 클래스&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php

namespace App\Helpers;

use Dompdf\Dompdf;
use Dompdf\Options;
use Illuminate\Support\Facades\View;

class PdfHelper
{
    private Dompdf $dompdf;

    public function __construct()
    {
        $options = new Options();

        // HTML5 파서 활성화: 최신 HTML5 태그 및 속성 지원
        $options-&amp;gt;set(&amp;#39;isHtml5ParserEnabled&amp;#39;, true);
        // 원격 리소스 비활성화: 외부 URL(이미지, CSS 등) 로드 차단 (보안)
        $options-&amp;gt;set(&amp;#39;isRemoteEnabled&amp;#39;, false);
        // 파일 접근 루트 경로 설정: public/ 디렉토리로 제한하여 보안 강화
        $options-&amp;gt;set(&amp;#39;chroot&amp;#39;, public_path());

        $this-&amp;gt;dompdf = new Dompdf($options);

        $this-&amp;gt;dompdf-&amp;gt;setBasePath(public_path());

        // 폰트 수동 등록
        $fontMetrics = $this-&amp;gt;dompdf-&amp;gt;getFontMetrics();
        $fontMetrics-&amp;gt;registerFont([
            &amp;#39;family&amp;#39; =&amp;gt; &amp;#39;Pretendard&amp;#39;,
            &amp;#39;style&amp;#39; =&amp;gt; &amp;#39;normal&amp;#39;,
            &amp;#39;weight&amp;#39; =&amp;gt; &amp;#39;normal&amp;#39;
        ], public_path(&amp;#39;fonts/PretendardVariable.ttf&amp;#39;));
    }

    /**
     * pdf 파일을 생성하는 메서드
     * @param string $view pdf로 변환할 html 파일명
     * @param array $data html 내에 삽입할 데이터
     * @return $this pdf 객체를 render한 helper 객체
     */
    public function generate(string $view, array $data = []): self
    {
        $html = View::make($view, $data)-&amp;gt;render();
        $this-&amp;gt;dompdf-&amp;gt;loadHtml($html);
        $this-&amp;gt;dompdf-&amp;gt;setPaper(&amp;#39;A4&amp;#39;, &amp;#39;portrait&amp;#39;);
        $this-&amp;gt;dompdf-&amp;gt;render();

        return $this;
    }

    /**
     * 생성한 pdf 파일을 로컬에 다운로드하는 메서드
     * @param string $filename 설정할 pdf 파일명
     */
    public function download(string $filename)
    {
        return response()-&amp;gt;streamDownload(
            fn() =&amp;gt; print($this-&amp;gt;dompdf-&amp;gt;output()),
            $filename,
            [&amp;#39;Content-Type&amp;#39; =&amp;gt; &amp;#39;application/pdf&amp;#39;]
        );
    }

    /**
     * 생성한 pdf 파일을 브라우저에 뷰어로 노출하는 메서드
     * @param string $filename 설정할 pdf 파일명
     */
    public function stream(string $filename)
    {
        return response($this-&amp;gt;dompdf-&amp;gt;output(), 200, [
            &amp;#39;Content-Type&amp;#39; =&amp;gt; &amp;#39;application/pdf&amp;#39;,
            &amp;#39;Content-Disposition&amp;#39; =&amp;gt; &amp;#39;inline; filename=&amp;quot;&amp;#39; . $filename . &amp;#39;&amp;quot;&amp;#39;,
        ]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;사용 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// 컨트롤러에서
public function downloadPdf()
{
    $pdfHelper = new PdfHelper();

    return $pdfHelper
        -&amp;gt;generate(&amp;#39;pdf.template&amp;#39;, [&amp;#39;link&amp;#39; =&amp;gt; &amp;#39;https://example.com&amp;#39;])
        -&amp;gt;download(&amp;#39;document.pdf&amp;#39;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HTML 템플릿&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta http-equiv=&amp;quot;Content-Type&amp;quot; content=&amp;quot;text/html; charset=utf-8&amp;quot;/&amp;gt;
    &amp;lt;style&amp;gt;
        body {
            font-family: &amp;#39;Pretendard&amp;#39;, sans-serif;
            margin: 0;
            padding: 0;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;한글 제목&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;본문 내용입니다.&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;핵심 포인트&lt;/h2&gt;
&lt;h3&gt;반드시 체크할 사항&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;폰트 파일 형식&lt;/strong&gt;: TTF 권장 (OTF 지원 제한적)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;폰트 글리프 확인&lt;/strong&gt;: 폰트 파일에 한글 포함 여부&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인코딩 명시&lt;/strong&gt;: &lt;code&gt;loadHtml()&lt;/code&gt; 두 번째 인자에 &lt;code&gt;&amp;#39;UTF-8&amp;#39;&lt;/code&gt; 지정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;경로 설정&lt;/strong&gt;: &lt;code&gt;chroot&lt;/code&gt;와 &lt;code&gt;setBasePath&lt;/code&gt; 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;왜 CLI 명령어 대신 수동 등록인가?&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 이 방법은 사용하지 않음
php vendor/dompdf/dompdf/load_font.php&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;수동 등록의 장점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;배포 환경에서 추가 설정 불필요&lt;/li&gt;
&lt;li&gt;코드로 관리되어 버전 관리 용이&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인에서 별도 스크립트 실행 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;한글이 기본적으로 지원되지 않는 부분은 이해가 되지만 css의 일부 문법이 사용할 수 없는 건 많이 아쉽다.. 추후에 꼭 DomPDF로 pdf를 생성해야만 하는 일이 생기질 않길 빌어야겠다.&lt;/p&gt;</description>
      <category>PHP/PHP</category>
      <category>dompdf</category>
      <category>font</category>
      <category>Korean</category>
      <category>PDF</category>
      <category>php</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/288</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-dom-pdf%EB%A1%9C-html-pdf-%EB%B3%80%ED%99%98#entry288comment</comments>
      <pubDate>Wed, 29 Oct 2025 13:58:57 +0900</pubDate>
    </item>
    <item>
      <title>PHP - aws t3 서버 사용 시 관련 스펙 정리</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-aws-t3-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EA%B4%80%EB%A0%A8-%EC%8A%A4%ED%8E%99-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1299&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbISpq/dJMcaaKkE5q/d36JC3W6V9MuteTbIb4Ty0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbISpq/dJMcaaKkE5q/d36JC3W6V9MuteTbIb4Ty0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbISpq/dJMcaaKkE5q/d36JC3W6V9MuteTbIb4Ty0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbISpq%2FdJMcaaKkE5q%2Fd36JC3W6V9MuteTbIb4Ty0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;596&quot; data-origin-width=&quot;1299&quot; data-origin-height=&quot;1374&quot;/&gt;&lt;/span&gt;&lt;/figure&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;a href=&quot;https://aws.amazon.com/ko/ec2/instance-types/t3/&quot;&gt;aws t3 서버 스펙 공식문서&lt;/a&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;RAM&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&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;t3.nano&lt;/td&gt;
&lt;td&gt;0.5GB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2~5명&lt;/td&gt;
&lt;td&gt;개발/테스트 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.micro&lt;/td&gt;
&lt;td&gt;1GB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5~15명&lt;/td&gt;
&lt;td&gt;매우 가벼운 사이트, 사내 도구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.small&lt;/td&gt;
&lt;td&gt;2GB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;20~50명&lt;/td&gt;
&lt;td&gt;소규모 비즈니스 사이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.medium&lt;/td&gt;
&lt;td&gt;4GB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;50~150명&lt;/td&gt;
&lt;td&gt;중소 규모 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.large&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;150~300명&lt;/td&gt;
&lt;td&gt;중간 규모 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.xlarge&lt;/td&gt;
&lt;td&gt;16GB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;300~600명&lt;/td&gt;
&lt;td&gt;대규모 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.2xlarge&lt;/td&gt;
&lt;td&gt;32GB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;600~1,200명&lt;/td&gt;
&lt;td&gt;엔터프라이즈급&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 성능 - 최대 5Gbps&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;t3.micro (1GB RAM) 예시&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OS + 기본 서비스: ~300MB&lt;/li&gt;
&lt;li&gt;MariaDB: 200 ~ 300MB&lt;/li&gt;
&lt;li&gt;Nginx: ~50MB&lt;/li&gt;
&lt;li&gt;남은 여유: 350 ~ 450MB&lt;/li&gt;
&lt;li&gt;PHP-FPM 프로세스 수: 7 ~ 15개 (프로세스당 평균 메모리 : 30 ~ 50MB)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이론적 동시 접속 가능량: 7~15명&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;fpm 프로세스당 메모리 사용량 계산&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czePTb/dJMcaa4DriJ/V4BJ7Svjf1qUwla6iL0WAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czePTb/dJMcaa4DriJ/V4BJ7Svjf1qUwla6iL0WAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czePTb/dJMcaa4DriJ/V4BJ7Svjf1qUwla6iL0WAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczePTb%2FdJMcaa4DriJ%2FV4BJ7Svjf1qUwla6iL0WAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;144&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;VIRT&lt;/b&gt; (Virtual): 접근 가능한 메모리 전체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RES&lt;/b&gt; (Resident): 실제 물리 메모리 사용량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SHR&lt;/b&gt; (Shared): 다른 프로세스와 공유하는 메모리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;PID 135381: VIRT 245640KB, RES 79180KB, SHR 59564KB 
PID 97394: VIRT 245488KB, RES 79264KB, SHR 59720KB 
PID 135299: VIRT 245612KB, RES 79816KB, SHR 60204KB 

실제 독점 메모리 = RES - SHR 
- 135381: 79180 - 59564 = 19.6MB
- 97394: 79264 - 59720 = 19.5MB
- 135299: 79816 - 60204 = 19.6MB&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;서버 스펙을 어떻게 정해야할지 모르겠다면 t3.micro로 시작해서 CloudWatch로 모니터링을 하다 필요 시 스케일업하는 방식을 권장합니다.&lt;/p&gt;</description>
      <category>PHP/PHP</category>
      <category>AWS</category>
      <category>FPM</category>
      <category>php</category>
      <category>t3</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/287</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-aws-t3-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EA%B4%80%EB%A0%A8-%EC%8A%A4%ED%8E%99-%EC%A0%95%EB%A6%AC#entry287comment</comments>
      <pubDate>Wed, 29 Oct 2025 11:33:46 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - 리버스 프록시란?</title>
      <link>https://dev-kimchi.tistory.com/entry/Nginx-%EB%A6%AC%EB%B2%84%EC%8A%A4-%ED%94%84%EB%A1%9D%EC%8B%9C%EB%9E%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckTHk8/dJMcaksDSK5/LqQsgonAdbNSP36OAnzK4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckTHk8/dJMcaksDSK5/LqQsgonAdbNSP36OAnzK4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckTHk8/dJMcaksDSK5/LqQsgonAdbNSP36OAnzK4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckTHk8%2FdJMcaksDSK5%2FLqQsgonAdbNSP36OAnzK4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;372&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/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;식당 비유로 이해하기&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;b&gt;일반적인 상황 (프록시 없음)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;손님(사용자) &amp;rarr; 직접 주방장(서버)에게 주문&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;pre class=&quot;gcode&quot;&gt;&lt;code&gt;손님(사용자) &amp;rarr; 직원(Nginx) &amp;rarr; 주방장(서버)에게 전달&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리버스 프록시의 주요 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로드 밸런싱&lt;/b&gt;: 트래픽이 많을 때 여러 서버로 요청 분산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안&lt;/b&gt;: 실제 서버의 위치와 구조를 클라이언트로부터 숨김&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱&lt;/b&gt;: 자주 요청되는 콘텐츠를 미리 저장하여 빠른 응답 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 구조&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;사용자 브라우저 &amp;rarr; Nginx &amp;rarr; 서버(애플리케이션 레벨)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx가 클라이언트의 요청을 받아 서버에 전달하고, 서버의 응답을 다시 클라이언트에게 반환한다.&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;Forward Proxy (일반 프록시)&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사용자 &amp;rarr; 프록시 &amp;rarr; 인터넷의 여러 서버&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트를 대신해서&lt;/b&gt; 요청을 보냄&lt;/li&gt;
&lt;li&gt;클라이언트의 신원을 숨김&lt;/li&gt;
&lt;li&gt;예시: VPN, 학교/회사 방화벽 우회&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reverse Proxy (리버스 프록시)&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사용자 &amp;rarr; 리버스 프록시 &amp;rarr; 백엔드 서버&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버를 대신해서&lt;/b&gt; 요청을 받음&lt;/li&gt;
&lt;li&gt;서버의 위치와 구조를 숨김&lt;/li&gt;
&lt;li&gt;예시: Nginx, Apache, HAProxy&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 &quot;리버스(Reverse)&quot;인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 프록시는 &lt;b&gt;클라이언트 측&lt;/b&gt;에서 동작하지만, 리버스 프록시는 &lt;b&gt;서버 측&lt;/b&gt;에서 동작하기 때문에 반대(Reverse)라는 이름이 붙었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;간단 정리:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Forward Proxy = 나의 대리인 (내가 누군지 숨김)&lt;/li&gt;
&lt;li&gt;Reverse Proxy = 서버의 대리인 (서버가 어디 있는지 숨김)&lt;/li&gt;
&lt;/ul&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;이런 cs 지식은 까먹더라도 개발하는데에 지장이 생기지는 않지만 누군가가 물어봤을때 대답하지 못하면 쪽팔리니 꼭 기억하도록 하자.&lt;/p&gt;</description>
      <category>기타</category>
      <category>nginx</category>
      <category>Proxy</category>
      <category>reverse</category>
      <category>리버스</category>
      <category>엔진엑스</category>
      <category>프록시</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/286</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Nginx-%EB%A6%AC%EB%B2%84%EC%8A%A4-%ED%94%84%EB%A1%9D%EC%8B%9C%EB%9E%80#entry286comment</comments>
      <pubDate>Tue, 28 Oct 2025 14:25:33 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - www 서브도메인을 non-www로 리다이렉트하기</title>
      <link>https://dev-kimchi.tistory.com/entry/nginx-www-%EC%84%9C%EB%B8%8C%EB%8F%84%EB%A9%94%EC%9D%B8%EC%9D%84-non-www%EB%A1%9C-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%ED%8A%B8%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQN0V/dJMb9L44gRq/cBnXsr393BENRj3lIFcNL0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQN0V/dJMb9L44gRq/cBnXsr393BENRj3lIFcNL0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQN0V/dJMb9L44gRq/cBnXsr393BENRj3lIFcNL0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQN0V%2FdJMb9L44gRq%2FcBnXsr393BENRj3lIFcNL0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;516&quot; height=&quot;606&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;현재 단일 도메인으로 서비스를 운영하고 있는데, &lt;code&gt;www.도메인&lt;/code&gt;으로 접속했을 때 기존 도메인으로 리다이렉트 시키고 싶었다. SEO 측면에서도 단일 도메인으로 통일하는 것이 좋고, 사용자 경험 측면에서도 일관된 URL을 제공하는 것이 중요하다고 판단했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;설정 방법&lt;/h2&gt;
&lt;h3&gt;1. DNS 레코드 추가&lt;/h3&gt;
&lt;p&gt;먼저 호스팅 사이트의 DNS 설정에서 &lt;code&gt;www&lt;/code&gt; 서브도메인에 대한 CNAME 레코드를 추가했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;-   레코드 타입: CNAME
-   호스트: www
-   값: 도메인. (호스팅 사이트에 따라 끝에 .을 붙이는 경우도 있다.)
-   TTL: 600 (10분)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TTL을 낮게 설정한 이유는 새로운 레코드를 추가하는 것이기 때문에 문제 발생 시 빠르게 수정할 수 있도록 하기 위함이다. 안정화된 후에는 3600(1시간) 이상으로 올려도 무방하다.&lt;/p&gt;
&lt;h4&gt;TTL(Time To Live)에 대하여&lt;/h4&gt;
&lt;p&gt;DNS 설정 시 입력하는 TTL은 DNS 레코드가 캐시에 저장되는 시간(초 단위)이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;-   TTL 600 설정 시 → DNS 서버와 브라우저가 10분 동안 캐시 보관
-   TTL 동안은 DNS 서버에 재조회 없이 캐시된 IP 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;TTL과 서버 부하&lt;/h4&gt;
&lt;p&gt;TTL을 낮추면 DNS 조회가 자주 발생하여 &lt;strong&gt;DNS 서버&lt;/strong&gt;(도메인 등록업체의 네임서버)에 부하가 증가하지만, &lt;strong&gt;실제 웹서버&lt;/strong&gt;와는 무관하다. DNS 조회와 실제 HTTP 트래픽은 별개이기 때문이다.&lt;/p&gt;
&lt;h4&gt;DNS 전파 시간&lt;/h4&gt;
&lt;p&gt;TTL 600이라고 해서 10분 안에 무조건 전파되는 것은 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;-   이미 캐시된 경우: 기존 TTL이 만료될 때까지 대기 필요
-   새 레코드인 경우: 대부분 즉시~수분 내 반영
-   ISP DNS 서버가 TTL을 무시하고 더 오래 캐시하는 경우도 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TTL 600이어도 완전 전파까지 30분~1시간 정도 여유를 두는 것이 안전하다.&lt;/p&gt;
&lt;h4&gt;DNS 캐시 삭제&lt;/h4&gt;
&lt;p&gt;로컬 DNS 캐시는 삭제할 수 있지만, 중간 DNS 서버(ISP, 공용 DNS 등)의 캐시는 직접 삭제할 수 없으며 TTL 만료까지 대기해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. DNS 전파 확인&lt;/h3&gt;
&lt;p&gt;DNS 설정 후 다음 명령어로 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 일반 조회
dig www.도메인

# 권한 네임서버에 직접 조회 (캐시 무시)
dig @ns.gabia.co.kr www.도메인

# 다른 공용 DNS로 조회
dig @8.8.8.8 www.도메인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 조회했을 때 &lt;code&gt;status: NXDOMAIN&lt;/code&gt; 응답이 나왔는데, 이는 해당 도메인이 DNS 서버에 아직 등록되지 않았거나 전파 중임을 의미한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. Nginx 리다이렉트 설정&lt;/h3&gt;
&lt;p&gt;애플리케이션 레벨에서도 처리할 수 있지만, 웹서버 레벨에서 처리하는 것이 성능상 더 효율적이다. Nginx 설정 파일에 다음과 같이 추가했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;server {
    listen 80;
    listen 443 ssl http2;
    server_name www.도메인;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    return 301 $scheme://도메인$request_uri;
}

server {
    listen 80;
    listen 443 ssl http2;
    server_name 도메인;

    # 기존 설정...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설정 후 Nginx를 재시작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo nginx -t
sudo systemctl reload nginx&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. SSL 인증서 설정&lt;/h3&gt;
&lt;p&gt;리다이렉트만 시켜줄 것이기 때문에 SSL 인증서가 필요 없을 거라고 생각했지만, 사용자가 &lt;code&gt;https://www.도메인&lt;/code&gt;으로 접속하면 Nginx가 SSL 핸드셰이크를 먼저 처리해야 리다이렉트 응답을 보낼 수 있다. 인증서 없이는 브라우저에서 SSL 오류가 발생한다.&lt;/p&gt;
&lt;p&gt;다행히 대부분의 SSL 인증서는 &lt;code&gt;도메인&lt;/code&gt;과 &lt;code&gt;www.도메인&lt;/code&gt;을 모두 커버하므로 동일한 인증서를 사용하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;간단한 리다이렉트 설정이지만 그 안에는 까먹기 쉬운 개념들이 포함되어있었다. 조사를 하다보니 DNS 설정에 관해 조금 더 이해도가 높아지게 된 것 같은데 ISP DNS 서버가 TTL을 무시하고 더 오래 캐시하는 경우가 있다는 것을 알게 된 것이 큰 수확인 것 같다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>dns</category>
      <category>domain</category>
      <category>nginx</category>
      <category>non</category>
      <category>TTL</category>
      <category>WWW</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/285</guid>
      <comments>https://dev-kimchi.tistory.com/entry/nginx-www-%EC%84%9C%EB%B8%8C%EB%8F%84%EB%A9%94%EC%9D%B8%EC%9D%84-non-www%EB%A1%9C-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%ED%8A%B8%ED%95%98%EA%B8%B0#entry285comment</comments>
      <pubDate>Fri, 24 Oct 2025 16:17:21 +0900</pubDate>
    </item>
    <item>
      <title>laravel - sail 컨테이너 apt-get update 지연 해결 방법</title>
      <link>https://dev-kimchi.tistory.com/entry/laravel-sail-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-apt-get-update-%EC%A7%80%EC%97%B0-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;671&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blCaNT/btsQem6kamb/m1qLcP0IXmVHpklNoWkm6K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blCaNT/btsQem6kamb/m1qLcP0IXmVHpklNoWkm6K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blCaNT/btsQem6kamb/m1qLcP0IXmVHpklNoWkm6K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblCaNT%2FbtsQem6kamb%2Fm1qLcP0IXmVHpklNoWkm6K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;671&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;laravel 프로젝트를 도커에 얹기 위해 sail로 컨테이너를 빌드하는데 &lt;code&gt;apt-get update &amp;amp;&amp;amp; apt-get upgrade ...&lt;/code&gt; 명령어에서 억겁의 시간이 걸리는 상황이 발생했다.&lt;/p&gt;
&lt;h2&gt;원인&lt;/h2&gt;
&lt;p&gt;sail로 컨테이너를 빌드할때 패키지 파일을 &lt;code&gt;ports.ubuntu.com&lt;/code&gt;에서 받아오는데 이 서버가 영국에 있기 때문에 한국과 물리적으로 거리가 멀어 지연이 발생하는 것으로 확인되었다.&lt;/p&gt;
&lt;h2&gt;해결방법&lt;/h2&gt;
&lt;p&gt;권장되는 방법은 아닐 것 같지만&lt;del&gt;(애초에 vendor 내의 파일을 수정하려고 하면 PhpStorm에서 안내 메시지가 나온다.)&lt;/del&gt; &lt;code&gt;vendor&lt;/code&gt; 디렉토리 내에 있는 기본 Dockerfile에서 저장소 서버의 url을 한국에 위치한 비공식 미러서버 url로 바꿔주는 명령어를 추가해 해결하였다.&lt;br&gt;이 방법에서 사용한 미러서버는 KAIST 학부 총학생회 산하 특별기구에서 제공하는 &lt;a href=&quot;https://ftp.kaist.ac.kr&quot;&gt;미러서버&lt;/a&gt;이다. &lt;del&gt;(사랑해요 카이스트)&lt;/del&gt;&lt;/p&gt;
&lt;h3&gt;추가할 명령어&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RUN sed -i &amp;#39;s|ports.ubuntu.com|ftp.kaist.ac.kr|g&amp;#39; /etc/apt/sources.list.d/ubuntu.sources&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3164&quot; data-origin-height=&quot;2068&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z0THB/btsQfefCSH5/KISBQZF2x07GORSSx8ZYx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z0THB/btsQfefCSH5/KISBQZF2x07GORSSx8ZYx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z0THB/btsQfefCSH5/KISBQZF2x07GORSSx8ZYx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz0THB%2FbtsQfefCSH5%2FKISBQZF2x07GORSSx8ZYx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3164&quot; height=&quot;2068&quot; data-origin-width=&quot;3164&quot; data-origin-height=&quot;2068&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Dockerfile 경로 찾기&lt;/h2&gt;
&lt;p&gt;현재 프로젝트에서 사용하는 Dockerfile의 경로는 &lt;code&gt;php artisan sail:install&lt;/code&gt; 시 생성되는 compose.yaml파일에 명시되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-14 오전 9.36.49.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJG0a/dJMcacICsXQ/Kz2llbcfb5CTGe2B4n9umK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJG0a/dJMcacICsXQ/Kz2llbcfb5CTGe2B4n9umK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJG0a/dJMcacICsXQ/Kz2llbcfb5CTGe2B4n9umK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJG0a%2FdJMcacICsXQ%2FKz2llbcfb5CTGe2B4n9umK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;653&quot; data-filename=&quot;스크린샷 2026-01-14 오전 9.36.49.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi vendor/laravel/sail/runtimes/8.5/Dockerfile&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;여담&lt;/h2&gt;
&lt;p&gt;추측하건데 sail의 버전에 따라 ubuntu 버전이 낮아진다면 카이스트 미러서버와는 호환이 안 되는 경우가 발생할 것 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.419 E: The repository &amp;#39;http://archive/ubuntu-ports noble Release&amp;#39; does not have a Release file.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;대충 위와 비슷한 에러가 발생할텐데 그럴 경우 구버전 ubuntu 패키지 저장소 서버를 미러링한 &lt;code&gt;mirror.kakao.com&lt;/code&gt;나 &lt;code&gt;kr.archive.ubuntu.com&lt;/code&gt;로 주소를 바꿔주면 해결이 될 것 같다.&lt;/p&gt;</description>
      <category>PHP/laravel</category>
      <category>apt</category>
      <category>Archive</category>
      <category>docker</category>
      <category>dockerfile</category>
      <category>laravel</category>
      <category>Sail</category>
      <category>ubuntu</category>
      <category>Update</category>
      <category>Upgrade</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/284</guid>
      <comments>https://dev-kimchi.tistory.com/entry/laravel-sail-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-apt-get-update-%EC%A7%80%EC%97%B0-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95#entry284comment</comments>
      <pubDate>Sat, 30 Aug 2025 21:39:03 +0900</pubDate>
    </item>
    <item>
      <title>기타 - 빈약한 도메인 모델(Anemic Domain Model)이란?</title>
      <link>https://dev-kimchi.tistory.com/entry/%EA%B8%B0%ED%83%80-%EB%B9%88%EC%95%BD%ED%95%9C-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8Anemic-Domain-Model%EC%9D%B4%EB%9E%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Anemic.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csGFit/btsPw2WcAED/UP1G6ASDjY3PWYU8ziEx5K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csGFit/btsPw2WcAED/UP1G6ASDjY3PWYU8ziEx5K/img.jpg&quot; data-alt=&quot;빈약 빈약!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csGFit/btsPw2WcAED/UP1G6ASDjY3PWYU8ziEx5K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsGFit%2FbtsPw2WcAED%2FUP1G6ASDjY3PWYU8ziEx5K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;480&quot; data-filename=&quot;Anemic.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빈약 빈약!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈약한 도메인 모델(Anemic Domain Model)이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈약한 도메인 모델은 겉모습은 도메인 모델처럼 보이지만, 실제로는 &lt;b&gt;데이터(속성)만 가지고 있고, 행동(비즈니스 로직)은 거의 없는 객체들&lt;/b&gt;을 말한다.&lt;br /&gt;즉, 도메인 객체가 단순히 &amp;lsquo;데이터 보관용 그릇&amp;rsquo; 역할만 하고, 핵심 로직은 전부 별도의 서비스 객체에 분리되어 있는 구조&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;객체지향 설계의 기본 원칙 위반&lt;/b&gt;&lt;br /&gt;객체지향 설계에서는 데이터와 그 데이터를 처리하는 행위(동작)를 한 객체 안에 함께 담는 것이 원칙&lt;br /&gt;빈약한 도메인 모델은 이 원칙을 무시하고, 데이터를 가진 객체와 로직을 가진 서비스를 따로 분리해 놨기 때문에 사실상 절차지향 프로그래밍과 크게 다르지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도메인 모델의 장점을 살리지 못함&lt;/b&gt;&lt;br /&gt;도메인 모델을 잘 활용하면 비즈니스 로직을 객체 안에 편리하게 숨겨 복잡한 로직을 깔끔하게 관리할 수 있음&lt;br /&gt;반면, 빈약한 도메인 모델은 핵심 로직을 서비스에 몰아넣기 때문에 도메인 모델의 이점이 사라짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스(비즈니스 로직)와 데이터 객체가 따로 놀음&lt;/b&gt;&lt;br /&gt;도메인 객체에는 데이터만 있고, 서비스에서 모든 로직과 데이터 변경을 담당합니다. 이러면 도메인 객체와 서비스 간의 연관성이 떨어지고, 유지보수가 어려워질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;올바른 도메인 모델 설계는 어떠해야 할까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;도메인 객체에 비즈니스 로직을 담아야 한다&lt;/b&gt;&lt;br /&gt;객체 안에 해당 도메인과 관련된 비즈니스 규칙, 검증, 계산 등 핵심 로직이 있어야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 레이어는 얇고 단순하게 유지&lt;/b&gt;&lt;br /&gt;서비스 레이어는 도메인 객체(도메인 모델) 사이의 작업을 조율하고, 외부와의 인터페이스 역할만 해야 함. 비즈니스 규칙은 도메인 객체가 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계층간 역할 분리 지키기&lt;/b&gt;&lt;br /&gt;도메인 로직은 도메인 계층에, 화면 처리나 데이터 저장 같은 기술적 관심사는 인프라스트럭처 계층에 맡김. 이 규칙을 어기면 안 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈약한 도메인 모델이 흔한 이유?&lt;/h2&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;일부 프레임워크(예: 예전 J2EE의 Entity Beans)가 이런 패턴을 조장하기도 함&lt;/li&gt;
&lt;li&gt;도메인 객체의 로직을 어디에 둬야 할지 혼란스러워서 결국 서비스에 몰아넣는 실수가 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;빈약한 도메인 모델 (Anemic Domain Model)&lt;/th&gt;
&lt;th&gt;풍부한 도메인 모델 (Rich Domain Model)&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;/tr&gt;
&lt;tr&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유지보수와 확장성 어려움&lt;/td&gt;
&lt;td&gt;유지보수성과 확장성 뛰어남&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>기타</category>
      <category>anemic</category>
      <category>domain</category>
      <category>model</category>
      <category>Rich</category>
      <category>객체지향</category>
      <category>도메인</category>
      <category>모델</category>
      <category>빈약한</category>
      <category>풍부한</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/283</guid>
      <comments>https://dev-kimchi.tistory.com/entry/%EA%B8%B0%ED%83%80-%EB%B9%88%EC%95%BD%ED%95%9C-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8Anemic-Domain-Model%EC%9D%B4%EB%9E%80#entry283comment</comments>
      <pubDate>Fri, 25 Jul 2025 10:16:49 +0900</pubDate>
    </item>
    <item>
      <title>PHP - Docker Prometheus Grafana 모니터링</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-Docker-Prometheus-Grafana-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;최강록.jpg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/18IsU/btsNZgNCyOg/PWuWvxTXsVB9H0evwjy6a0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/18IsU/btsNZgNCyOg/PWuWvxTXsVB9H0evwjy6a0/img.jpg&quot; data-alt=&quot;제모옥은 그라파나로 하겠습니다. 근데 이제 Docker를 곁들인...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/18IsU/btsNZgNCyOg/PWuWvxTXsVB9H0evwjy6a0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F18IsU%2FbtsNZgNCyOg%2FPWuWvxTXsVB9H0evwjy6a0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-filename=&quot;최강록.jpg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제모옥은 그라파나로 하겠습니다. 근데 이제 Docker를 곁들인...&lt;/figcaption&gt;
&lt;/figure&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;php로 서비스를 운영하며 속도가 너무 느려 원인을 찾아보려 했으나 코드상에서 병목이 발생할만한 부분을 찾지 못했다.&lt;br /&gt;서버 성능의 문제가 아닌가 싶었지만 원인을 분명히 하고 싶어 모니터링 시스템을 추가하게 되었고 많은 모니터링 시스템 중 Prometheus Grafana 조합을 선택하게 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Prometheus + Grafana인가?&lt;/h2&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;Prometheus + Grafana&lt;/th&gt;
&lt;th&gt;Zabbix&lt;/th&gt;
&lt;th&gt;Nagios&lt;/th&gt;
&lt;th&gt;InfluxDB + Chronograf&lt;/th&gt;
&lt;th&gt;Datadog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;데이터 수집&lt;/td&gt;
&lt;td&gt;Pull 기반, PHP-FPM exporter로 PHP 메트릭 수집 가능, 다양한 exporter 지원&lt;/td&gt;
&lt;td&gt;Push 기반, 에이전트 설치 필요, PHP 스크립트로 커스텀 메트릭 지원&lt;/td&gt;
&lt;td&gt;스크립트 기반 체크, PHP 메트릭은 플러그인으로 제한적&lt;/td&gt;
&lt;td&gt;Push 기반, Telegraf 플러그인으로 PHP 메트릭 수집 가능&lt;/td&gt;
&lt;td&gt;에이전트 기반, PHP 통합 가능, 다양한 애플리케이션 메트릭 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시각화&lt;/td&gt;
&lt;td&gt;Grafana의 강력한 대시보드, 다양한 차트와 템플릿 지원, 커뮤니티 대시보드 풍부&lt;/td&gt;
&lt;td&gt;기본 대시보드 제공, Grafana 통합 가능하나 설정 복잡&lt;/td&gt;
&lt;td&gt;기본 UI 제한적, 외부 도구(Grafana 등) 필요&lt;/td&gt;
&lt;td&gt;Chronograf 제공, Grafana보다 시각화 옵션 제한적&lt;/td&gt;
&lt;td&gt;고급 대시보드, 사용자 친화적이나 비용 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쿼리 기능&lt;/td&gt;
&lt;td&gt;PromQL로 강력하고 유연한 쿼리, 복잡한 분석 가능&lt;/td&gt;
&lt;td&gt;SQL 기반 쿼리, 복잡한 쿼리는 제한적&lt;/td&gt;
&lt;td&gt;쿼리 기능 제한적, 스크립트에 의존&lt;/td&gt;
&lt;td&gt;InfluxQL, PromQL보다 직관적이나 덜 유연함&lt;/td&gt;
&lt;td&gt;자체 쿼리 언어, 유연하지만 학습 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장성&lt;/td&gt;
&lt;td&gt;Prometheus는 고가용성 설정 복잡(Thanos 등 필요), Grafana는 다중 데이터 소스 지원&lt;/td&gt;
&lt;td&gt;대규모 환경 지원, 클러스터링 가능&lt;/td&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;Docker로 배포 간단, PHP-FPM exporter 설정 쉬움, 초기 학습 필요&lt;/td&gt;
&lt;td&gt;에이전트 설치 및 설정 복잡, UI 기반 설정 가능&lt;/td&gt;
&lt;td&gt;플러그인 설치 및 구성 복잡, 수동 작업 많음&lt;/td&gt;
&lt;td&gt;Telegraf 설정 간단, UI 기반 관리 가능&lt;/td&gt;
&lt;td&gt;에이전트 설치 간단, 클라우드 기반으로 설정 최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP 통합&lt;/td&gt;
&lt;td&gt;PHP-FPM exporter로 PHP 메트릭 수집 용이, 커스텀 메트릭 지원&lt;/td&gt;
&lt;td&gt;PHP 스크립트로 커스텀 메트릭 가능, 설정 추가 필요&lt;/td&gt;
&lt;td&gt;PHP 모니터링은 플러그인 개발 필요, 제한적&lt;/td&gt;
&lt;td&gt;Telegraf로 PHP 메트릭 수집 가능, 추가 설정 필요&lt;/td&gt;
&lt;td&gt;PHP-FPM 및 애플리케이션 메트릭 지원, 통합 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커뮤니티 지원&lt;/td&gt;
&lt;td&gt;CNCF 졸업 프로젝트, 활발한 커뮤니티, 풍부한 문서 및 exporter&lt;/td&gt;
&lt;td&gt;오픈소스, 대규모 커뮤니티, 다양한 플러그인&lt;/td&gt;
&lt;td&gt;오래된 오픈소스, 플러그인 생태계 제한적&lt;/td&gt;
&lt;td&gt;오픈소스, Prometheus보다 커뮤니티 작음&lt;/td&gt;
&lt;td&gt;상용 솔루션, 커뮤니티 제한, 공식 지원 강력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;오픈소스, 무료, 관리형(Grafana Cloud 등)은 유료 옵션&lt;/td&gt;
&lt;td&gt;오픈소스, 무료, 엔터프라이즈 지원 유료&lt;/td&gt;
&lt;td&gt;오픈소스, 무료, 상용 버전(Nagios XI) 유료&lt;/td&gt;
&lt;td&gt;오픈소스, 무료, InfluxCloud는 유료&lt;/td&gt;
&lt;td&gt;유료, 구독 기반, 무료 티어 제한적&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Docker로 배포하기 쉽다.&lt;/li&gt;
&lt;li&gt;PHP 메트릭 수집이 간단하다.&lt;/li&gt;
&lt;li&gt;추후 진행할 mysql 모니터링 시스템도 추가하기 간단하다.&lt;/li&gt;
&lt;li&gt;기본적인 기능이 무료이고 오픈소스다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://community.grafana.com/&quot;&gt;커뮤니티&lt;/a&gt;가 활발하게 운영되고 있어 자료 수집이 원활하다.&lt;br /&gt;크게 위 5가지 이유로 Prometheus + Grafana를 선택하게 되었다.&lt;br /&gt;&lt;del&gt;그리고 Grafana 대시보드가 너무 예쁘다&lt;/del&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;~
├── grafana
│    ├── prometheus.yml
│    ├── promtail-config.yml
│    └── docker-compose.yml
└── php
     └── logs&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&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;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[Apache Logs] ------------&amp;gt; [Promtail] ----------&amp;gt; [Loki]
                                                      |
                                                      v
[Node Exporter] --------&amp;gt; [Prometheus] --------&amp;gt; [Grafana]
                                                      ^
                                                      |
[Apache server-status] --&amp;gt; [Apache Exporter] --&amp;gt; [Prometheus]

* 모든 서비스는 'local_net' 네트워크를 통해 연결
* 볼륨:
  - Prometheus: prometheus-data
  - Grafana: grafana-data
  - Loki: loki-data&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Logs: /var/log/apache2 디렉토리에서 생성된 로그를 Promtail이 읽음.&lt;br /&gt;Promtail: 로그를 수집하여 Loki로 전송.&lt;br /&gt;Loki: 로그를 저장하고 Grafana에서 시각화를 위해 사용&lt;br /&gt;Node Exporter: 시스템 메트릭(CPU, 메모리 등)을 Prometheus로 제공&lt;br /&gt;Apache Exporter: Apache 서버 상태(http://apache-server/server-status?auto)를 스크랩하여 Prometheus로 전송&lt;br /&gt;Prometheus: 메트릭을 저장하고 Grafana로 데이터 제공&lt;br /&gt;Grafana: Prometheus(메트릭)와 Loki(로그)를 소스로 사용하여 대시보드 제공&lt;br /&gt;네트워크: 모든 서비스는 local_net 네트워크를 통해 통신&lt;br /&gt;볼륨: 각 서비스의 데이터는 prometheus-data, grafana-data, loki-data에 저장&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;apache /server-status 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. mod_status 모듈 활성화 확인&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# status_module이 나타나면 활성화 되어있음
apachectl -M | grep status

# 나타나지 않으면 활성화
sudo a2enmod status&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Apache 설정 파일 수정&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 설정 파일 열기
sudo vi /etc/apache2/conf-available/server-status.conf&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;IfModule mod_status.c&amp;gt;
    # 자세한 상태 정보를 표시
    ExtendedStatus On
    &amp;lt;Location /server-status&amp;gt;
        # 핸들러 설정
        SetHandler server-status
    &amp;lt;/Location&amp;gt;
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Apache 재시작&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo service apache2 restart&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;http://apache-server/server-status&lt;/code&gt;(본인의 아파치 서버 url)로 접속하여 페이지가 정상적으로 표시되는지 확인&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모니터링 시스템 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. grafana/prometheus.yml&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Prometheus 전역 설정
global:
  # 메트릭 수집 주기 설정 (15초마다 수집)
  scrape_interval: 15s

# 수집할 대상 설정
scrape_configs:
  # Node Exporter 설정 - 시스템 메트릭 수집
  - job_name: 'node'
    static_configs:
      - targets: ['grafana-node-exporter-1:9100']

  # Apache Exporter 설정 - Apache 웹서버 메트릭 수집
  - job_name: 'apache'
    static_configs:
      - targets: ['grafana-apache-exporter-1:9117']&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. grafana/promtail-config.yml&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Promtail 서버 설정
server:
  # HTTP 서버 포트 설정
  http_listen_port: 9080
  # gRPC 서버 비활성화
  grpc_listen_port: 0

# 로그 파일 위치 추적을 위한 설정
positions:
  filename: /tmp/positions.yaml

# Loki 서버 연결 설정
clients:
  - url: http://grafana-loki-1:3100/loki/api/v1/push

# 로그 수집 설정
scrape_configs:
  - job_name: system
    static_configs:
      # PHP 에러 로그 수집 설정
      - targets:
          - localhost
        labels:
          job: php-error
          __path__: /var/log/apache2/php_errors.log
      # PHP 액세스 로그 수집 설정
      - targets:
          - localhost
        labels:
          job: php-access
          __path__: /var/log/apache2/access.log
      # PHP 일반 로그 수집 설정
      - targets:
          - localhost
        labels:
          job: php-info
          __path__: /var/log/apache2/error.log&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. grafana/docker-compose.yml&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Docker Compose 버전 정의
version: '3.8'
services:
  # Prometheus: 메트릭 수집 및 저장을 위한 모니터링 시스템
  prometheus:
    image: prom/prometheus:v2.54.1
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml  # Prometheus 설정 파일
      - prometheus-data:/prometheus                      # 메트릭 데이터 저장소
    networks:
      - local_net

  # Grafana: 시각화 대시보드 도구
  grafana:
    image: grafana/grafana:11.2.0
    volumes:
      - grafana-data:/var/lib/grafana                   # Grafana 데이터 저장소
    ports:
      - &quot;4000:3000&quot;                                     # 외부 접속 포트:4000(기본적으로는 3000포트를 많이 사용하나, 보통 다른 서비스가 3000포트를 사용하는 경우가 많아 4000으로 설정), 내부 포트:3000
    networks:
      - local_net

  # Loki: 로그 수집 및 저장 시스템
  loki:
    image: grafana/loki:3.1.1
    volumes:
      - loki-data:/loki                                 # 로그 데이터 저장소
    networks:
      - local_net

  # Promtail: 로그 수집기
  promtail:
    image: grafana/promtail:3.1.1
    volumes:
      - ./promtail-config.yml:/etc/promtail/config.yml  # Promtail 설정 파일
      - ../php/logs:/var/log/apache2                 # Apache 로그 디렉토리
    networks:
      - local_net

  # Node Exporter: 시스템 메트릭 수집기
  node-exporter:
    image: prom/node-exporter:v1.8.2
    volumes:
      - /proc:/host/proc:ro                            # 프로세스 정보
      - /sys:/host/sys:ro                              # 시스템 정보
      - /:/rootfs:ro                                   # 루트 파일시스템
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'
    networks:
      - local_net

  # Apache Exporter: Apache 웹서버 메트릭 수집기
  apache-exporter:
    image: lusotycoon/apache-exporter:v1.0.9
    command:
      - --scrape_uri=http://apache-server/server-status?auto  # Apache 상태 페이지 URL
    networks:
      - local_net

# 영구 저장소 정의
volumes:
  prometheus-data:    # Prometheus 데이터 저장소
  grafana-data:       # Grafana 데이터 저장소
  loki-data:          # Loki 로그 데이터 저장소

# 네트워크 설정
networks:
  local_net:
    name: local_net&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. docker compose 실행&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;cd ~/grafana
docker compose up --build&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 접속 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://localhost:4000&quot;&gt;http://localhost:4000&lt;/a&gt;에 접속하여 그라파나에 정상적으로 접속이 가능한지 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WBKRB/btsNX2bFcYH/NQLQtkSM333u61ubjEQi50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WBKRB/btsNX2bFcYH/NQLQtkSM333u61ubjEQi50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WBKRB/btsNX2bFcYH/NQLQtkSM333u61ubjEQi50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWBKRB%2FbtsNX2bFcYH%2FNQLQtkSM333u61ubjEQi50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;grafana 대시보드 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 거의 완료했다.&lt;br /&gt;대시보드는 사용하는 사람이 편한 대로 설정하면 된다.&lt;br /&gt;밑의 과정은 내 프로젝트에 적용한 예시이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 대시보드 로그인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WBKRB/btsNX2bFcYH/NQLQtkSM333u61ubjEQi50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WBKRB/btsNX2bFcYH/NQLQtkSM333u61ubjEQi50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WBKRB/btsNX2bFcYH/NQLQtkSM333u61ubjEQi50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWBKRB%2FbtsNX2bFcYH%2FNQLQtkSM333u61ubjEQi50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 유저명/비밀번호는 admin/admin이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 소스 추가&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0cZ2E/btsNZhTc8is/NbEcMtajbjpR0qeJWAU5Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0cZ2E/btsNZhTc8is/NbEcMtajbjpR0qeJWAU5Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0cZ2E/btsNZhTc8is/NbEcMtajbjpR0qeJWAU5Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0cZ2E%2FbtsNZhTc8is%2FNbEcMtajbjpR0qeJWAU5Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&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;왼쪽 사이드바의 Connections -&amp;gt; data sources 탭으로 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4MlKQ/btsNYgm9SaH/fErT71FX0eU9YuDkheXmqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4MlKQ/btsNYgm9SaH/fErT71FX0eU9YuDkheXmqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4MlKQ/btsNYgm9SaH/fErT71FX0eU9YuDkheXmqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4MlKQ%2FbtsNYgm9SaH%2FfErT71FX0eU9YuDkheXmqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Add data source 버튼을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qKFTn/btsNW0yGLMy/h74emZaARgcKSF04KSxFW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qKFTn/btsNW0yGLMy/h74emZaARgcKSF04KSxFW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qKFTn/btsNW0yGLMy/h74emZaARgcKSF04KSxFW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqKFTn%2FbtsNW0yGLMy%2Fh74emZaARgcKSF04KSxFW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus와 Loki data source를 각각 추가해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEnwCS/btsNYm1ZwWi/e7AwugZsk21WbeK2hdqr8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEnwCS/btsNYm1ZwWi/e7AwugZsk21WbeK2hdqr8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEnwCS/btsNYm1ZwWi/e7AwugZsk21WbeK2hdqr8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEnwCS%2FbtsNYm1ZwWi%2Fe7AwugZsk21WbeK2hdqr8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection의 server URL을 http://prometheus:9090으로 지정해 주고 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YXSxc/btsNW4OBl7w/KC89XvOVTBSYkZZbJLPUO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YXSxc/btsNW4OBl7w/KC89XvOVTBSYkZZbJLPUO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YXSxc/btsNW4OBl7w/KC89XvOVTBSYkZZbJLPUO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYXSxc%2FbtsNW4OBl7w%2FKC89XvOVTBSYkZZbJLPUO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection의 server URL을 http://Loki:3100으로 지정해주고 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdJIR6/btsNXqjRvJc/99SMcuZWeX7X1mk0Jox3X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdJIR6/btsNXqjRvJc/99SMcuZWeX7X1mk0Jox3X0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdJIR6/btsNXqjRvJc/99SMcuZWeX7X1mk0Jox3X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdJIR6%2FbtsNXqjRvJc%2F99SMcuZWeX7X1mk0Jox3X0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&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;각 데이터 소스 페이지에 접속하면 url 끝에 uid가 나오는데 이를 복사해두어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLJnXZ/btsNW2b9h3t/IiEfJR06fkrBTg1xC4pmaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLJnXZ/btsNW2b9h3t/IiEfJR06fkrBTg1xC4pmaK/img.png&quot; data-alt=&quot;prometheus의 uid&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLJnXZ/btsNW2b9h3t/IiEfJR06fkrBTg1xC4pmaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLJnXZ%2FbtsNW2b9h3t%2FIiEfJR06fkrBTg1xC4pmaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;prometheus의 uid&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oMfUG/btsNZazNYBE/O9st9ayg3Lqim9meIRol8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oMfUG/btsNZazNYBE/O9st9ayg3Lqim9meIRol8K/img.png&quot; data-alt=&quot;loki의 uid&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oMfUG/btsNZazNYBE/O9st9ayg3Lqim9meIRol8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoMfUG%2FbtsNZazNYBE%2FO9st9ayg3Lqim9meIRol8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;loki의 uid&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prometheus와 loki의 데이터 소스 추가가 완료되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 대시보드 추가&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHR6YS/btsNYvdCwN6/qjhoKkkmC8QKO4t9alKOU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHR6YS/btsNYvdCwN6/qjhoKkkmC8QKO4t9alKOU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHR6YS/btsNYvdCwN6/qjhoKkkmC8QKO4t9alKOU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHR6YS%2FbtsNYvdCwN6%2FqjhoKkkmC8QKO4t9alKOU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 사이드바의 Dashboards 탭으로 들어가 Create dashboard 버튼을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yjTG0/btsNXs9TYIq/zGJEeRxaJMOeNRrkZuJ8TK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yjTG0/btsNXs9TYIq/zGJEeRxaJMOeNRrkZuJ8TK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yjTG0/btsNXs9TYIq/zGJEeRxaJMOeNRrkZuJ8TK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyjTG0%2FbtsNXs9TYIq%2FzGJEeRxaJMOeNRrkZuJ8TK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;import dashboard 버튼을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGMnJW/btsNZux2C2V/shIngK2yvcCGdUYZZYLZL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGMnJW/btsNZux2C2V/shIngK2yvcCGdUYZZYLZL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGMnJW/btsNZux2C2V/shIngK2yvcCGdUYZZYLZL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGMnJW%2FbtsNZux2C2V%2FshIngK2yvcCGdUYZZYLZL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 grafana에서 지원하는 &lt;a title=&quot;dashboard json model&quot; href=&quot;https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/view-dashboard-json-model/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;dashboard json model&lt;/a&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 내용 중 datasource uid를 자신의 uid로 수정하여 Import via dashboard JSON model 입력창에 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747287753751&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;time&quot;: {
        &quot;from&quot;: &quot;now-1h&quot;,
        &quot;to&quot;: &quot;now&quot;
    },
    &quot;timezone&quot;: &quot;browser&quot;,
    &quot;title&quot;: &quot;PHP Server Monitoring&quot;,
    &quot;version&quot;: 1,
    &quot;refresh&quot;: &quot;5s&quot;,
    &quot;schemaVersion&quot;: 39,
    &quot;tags&quot;: [
        &quot;php&quot;,
        &quot;apache&quot;,
        &quot;monitoring&quot;
    ],
    &quot;templating&quot;: {
        &quot;list&quot;: [
            {
                &quot;current&quot;: {
                    &quot;text&quot;: &quot;grafana-node-exporter-1:9100&quot;,
                    &quot;value&quot;: &quot;grafana-node-exporter-1:9100&quot;
                },
                &quot;datasource&quot;: {
                    &quot;type&quot;: &quot;prometheus&quot;,
                    &quot;uid&quot;: &quot;change me&quot;
                },
                &quot;name&quot;: &quot;instance&quot;,
                &quot;query&quot;: &quot;label_values(node_cpu_seconds_total, instance)&quot;,
                &quot;refresh&quot;: 1,
                &quot;type&quot;: &quot;query&quot;
            }
        ]
    },
    &quot;panels&quot;: [
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;prometheus&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;fieldConfig&quot;: {
                &quot;defaults&quot;: {
                    &quot;decimals&quot;: 1,
                    &quot;mappings&quot;: [
                        {
                            &quot;options&quot;: {
                                &quot;match&quot;: &quot;null&quot;,
                                &quot;result&quot;: {
                                    &quot;text&quot;: &quot;N/A&quot;
                                }
                            },
                            &quot;type&quot;: &quot;special&quot;
                        }
                    ],
                    &quot;thresholds&quot;: {
                        &quot;mode&quot;: &quot;absolute&quot;,
                        &quot;steps&quot;: [
                            {
                                &quot;color&quot;: &quot;green&quot;,
                                &quot;value&quot;: null
                            },
                            {
                                &quot;color&quot;: &quot;red&quot;,
                                &quot;value&quot;: 80
                            }
                        ]
                    },
                    &quot;unit&quot;: &quot;s&quot;
                }
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 4,
                &quot;w&quot;: 4,
                &quot;x&quot;: 0,
                &quot;y&quot;: 0
            },
            &quot;options&quot;: {
                &quot;colorMode&quot;: &quot;none&quot;,
                &quot;graphMode&quot;: &quot;none&quot;,
                &quot;reduceOptions&quot;: {
                    &quot;calcs&quot;: [
                        &quot;mean&quot;
                    ]
                },
                &quot;textMode&quot;: &quot;auto&quot;
            },
            &quot;targets&quot;: [
                {
                    &quot;datasource&quot;: {
                        &quot;type&quot;: &quot;prometheus&quot;,
                        &quot;uid&quot;: &quot;change me&quot;
                    },
                    &quot;expr&quot;: &quot;apache_uptime_seconds_total&quot;,
                    &quot;format&quot;: &quot;time_series&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                }
            ],
            &quot;title&quot;: &quot;Uptime&quot;,
            &quot;type&quot;: &quot;stat&quot;
        },
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;prometheus&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;fieldConfig&quot;: {
                &quot;defaults&quot;: {
                    &quot;color&quot;: {
                        &quot;mode&quot;: &quot;palette-classic&quot;
                    },
                    &quot;custom&quot;: {
                        &quot;drawStyle&quot;: &quot;line&quot;,
                        &quot;fillOpacity&quot;: 100,
                        &quot;lineWidth&quot;: 3,
                        &quot;showPoints&quot;: &quot;never&quot;
                    },
                    &quot;thresholds&quot;: {
                        &quot;mode&quot;: &quot;absolute&quot;,
                        &quot;steps&quot;: [
                            {
                                &quot;color&quot;: &quot;green&quot;,
                                &quot;value&quot;: null
                            },
                            {
                                &quot;color&quot;: &quot;red&quot;,
                                &quot;value&quot;: 80
                            }
                        ]
                    },
                    &quot;unit&quot;: &quot;short&quot;
                },
                &quot;overrides&quot;: [
                    {
                        &quot;matcher&quot;: {
                            &quot;id&quot;: &quot;byName&quot;,
                            &quot;options&quot;: &quot;Apache Down&quot;
                        },
                        &quot;properties&quot;: [
                            {
                                &quot;id&quot;: &quot;color&quot;,
                                &quot;value&quot;: {
                                    &quot;fixedColor&quot;: &quot;#BF1B00&quot;,
                                    &quot;mode&quot;: &quot;fixed&quot;
                                }
                            },
                            {
                                &quot;id&quot;: &quot;custom.transform&quot;,
                                &quot;value&quot;: &quot;negative-Y&quot;
                            }
                        ]
                    }
                ]
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 4,
                &quot;w&quot;: 20,
                &quot;x&quot;: 4,
                &quot;y&quot;: 0
            },
            &quot;options&quot;: {
                &quot;legend&quot;: {
                    &quot;calcs&quot;: [
                        &quot;mean&quot;,
                        &quot;lastNotNull&quot;,
                        &quot;max&quot;,
                        &quot;min&quot;
                    ],
                    &quot;displayMode&quot;: &quot;table&quot;,
                    &quot;placement&quot;: &quot;right&quot;,
                    &quot;showLegend&quot;: true
                }
            },
            &quot;targets&quot;: [
                {
                    &quot;datasource&quot;: {
                        &quot;type&quot;: &quot;prometheus&quot;,
                        &quot;uid&quot;: &quot;change me&quot;
                    },
                    &quot;expr&quot;: &quot;count(apache_up == 1)&quot;,
                    &quot;legendFormat&quot;: &quot;Apache Up&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                },
                {
                    &quot;datasource&quot;: {
                        &quot;type&quot;: &quot;prometheus&quot;,
                        &quot;uid&quot;: &quot;change me&quot;
                    },
                    &quot;expr&quot;: &quot;scalar(count(apache_up == 0))&quot;,
                    &quot;legendFormat&quot;: &quot;Apache Down&quot;,
                    &quot;refId&quot;: &quot;B&quot;
                }
            ],
            &quot;title&quot;: &quot;Apache Up / Down&quot;,
            &quot;type&quot;: &quot;timeseries&quot;
        },
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;prometheus&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;fieldConfig&quot;: {
                &quot;defaults&quot;: {
                    &quot;color&quot;: {
                        &quot;mode&quot;: &quot;palette-classic&quot;
                    },
                    &quot;custom&quot;: {
                        &quot;drawStyle&quot;: &quot;line&quot;,
                        &quot;fillOpacity&quot;: 10,
                        &quot;lineWidth&quot;: 1,
                        &quot;showPoints&quot;: &quot;never&quot;
                    },
                    &quot;thresholds&quot;: {
                        &quot;mode&quot;: &quot;absolute&quot;,
                        &quot;steps&quot;: [
                            {
                                &quot;color&quot;: &quot;green&quot;,
                                &quot;value&quot;: null
                            },
                            {
                                &quot;color&quot;: &quot;red&quot;,
                                &quot;value&quot;: 80
                            }
                        ]
                    },
                    &quot;unit&quot;: &quot;short&quot;
                }
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 10,
                &quot;w&quot;: 12,
                &quot;x&quot;: 0,
                &quot;y&quot;: 4
            },
            &quot;interval&quot;: &quot;15s&quot;,
            &quot;options&quot;: {
                &quot;legend&quot;: {
                    &quot;calcs&quot;: [
                        &quot;mean&quot;,
                        &quot;lastNotNull&quot;,
                        &quot;max&quot;,
                        &quot;min&quot;
                    ],
                    &quot;displayMode&quot;: &quot;table&quot;,
                    &quot;placement&quot;: &quot;bottom&quot;,
                    &quot;showLegend&quot;: true
                },
                &quot;tooltip&quot;: {
                    &quot;mode&quot;: &quot;multi&quot;
                }
            },
            &quot;targets&quot;: [
                {
                    &quot;datasource&quot;: {
                        &quot;type&quot;: &quot;prometheus&quot;,
                        &quot;uid&quot;: &quot;change me&quot;
                    },
                    &quot;expr&quot;: &quot;rate(apache_accesses_total[1m])&quot;,
                    &quot;legendFormat&quot;: &quot;Accesses&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                }
            ],
            &quot;title&quot;: &quot;초당 평균 요청량&quot;,
            &quot;type&quot;: &quot;timeseries&quot;
        },
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;prometheus&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;fieldConfig&quot;: {
                &quot;defaults&quot;: {
                    &quot;color&quot;: {
                        &quot;mode&quot;: &quot;palette-classic&quot;
                    },
                    &quot;custom&quot;: {
                        &quot;drawStyle&quot;: &quot;line&quot;,
                        &quot;fillOpacity&quot;: 10,
                        &quot;lineWidth&quot;: 1,
                        &quot;showPoints&quot;: &quot;never&quot;
                    },
                    &quot;thresholds&quot;: {
                        &quot;mode&quot;: &quot;absolute&quot;,
                        &quot;steps&quot;: [
                            {
                                &quot;color&quot;: &quot;green&quot;,
                                &quot;value&quot;: null
                            },
                            {
                                &quot;color&quot;: &quot;red&quot;,
                                &quot;value&quot;: 80
                            }
                        ]
                    },
                    &quot;unit&quot;: &quot;deckbytes&quot;
                }
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 10,
                &quot;w&quot;: 12,
                &quot;x&quot;: 12,
                &quot;y&quot;: 4
            },
            &quot;options&quot;: {
                &quot;legend&quot;: {
                    &quot;calcs&quot;: [
                        &quot;mean&quot;,
                        &quot;lastNotNull&quot;,
                        &quot;max&quot;,
                        &quot;min&quot;
                    ],
                    &quot;displayMode&quot;: &quot;table&quot;,
                    &quot;placement&quot;: &quot;bottom&quot;,
                    &quot;showLegend&quot;: true
                },
                &quot;tooltip&quot;: {
                    &quot;mode&quot;: &quot;multi&quot;
                }
            },
            &quot;targets&quot;: [
                {
                    &quot;datasource&quot;: {
                        &quot;type&quot;: &quot;prometheus&quot;,
                        &quot;uid&quot;: &quot;change me&quot;
                    },
                    &quot;expr&quot;: &quot;rate(apache_sent_kilobytes_total[1m])&quot;,
                    &quot;legendFormat&quot;: &quot;Kilobytes Sent&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                }
            ],
            &quot;title&quot;: &quot;초당 평균 전송량&quot;,
            &quot;type&quot;: &quot;timeseries&quot;
        },
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;prometheus&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;fieldConfig&quot;: {
                &quot;defaults&quot;: {
                    &quot;color&quot;: {
                        &quot;mode&quot;: &quot;palette-classic&quot;
                    },
                    &quot;custom&quot;: {
                        &quot;drawStyle&quot;: &quot;line&quot;,
                        &quot;fillOpacity&quot;: 10,
                        &quot;lineWidth&quot;: 1,
                        &quot;showPoints&quot;: &quot;auto&quot;
                    },
                    &quot;max&quot;: 100,
                    &quot;min&quot;: 0,
                    &quot;thresholds&quot;: {
                        &quot;mode&quot;: &quot;absolute&quot;,
                        &quot;steps&quot;: [
                            {
                                &quot;color&quot;: &quot;green&quot;,
                                &quot;value&quot;: null
                            },
                            {
                                &quot;color&quot;: &quot;red&quot;,
                                &quot;value&quot;: 80
                            }
                        ]
                    },
                    &quot;unit&quot;: &quot;percent&quot;
                }
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 8,
                &quot;w&quot;: 12,
                &quot;x&quot;: 0,
                &quot;y&quot;: 14
            },
            &quot;options&quot;: {
                &quot;legend&quot;: {
                    &quot;displayMode&quot;: &quot;list&quot;,
                    &quot;placement&quot;: &quot;bottom&quot;,
                    &quot;showLegend&quot;: true
                },
                &quot;tooltip&quot;: {
                    &quot;mode&quot;: &quot;single&quot;
                }
            },
            &quot;targets&quot;: [
                {
                    &quot;datasource&quot;: {
                        &quot;type&quot;: &quot;prometheus&quot;,
                        &quot;uid&quot;: &quot;change me&quot;
                    },
                    &quot;expr&quot;: &quot;100 - (avg by(instance) (rate(node_cpu_seconds_total{mode=\&quot;idle\&quot;,instance=\&quot;$instance\&quot;}[1m])) * 100)&quot;,
                    &quot;legendFormat&quot;: &quot;{{instance}}&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                }
            ],
            &quot;title&quot;: &quot;CPU 사용량&quot;,
            &quot;type&quot;: &quot;timeseries&quot;
        },
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;prometheus&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;fieldConfig&quot;: {
                &quot;defaults&quot;: {
                    &quot;color&quot;: {
                        &quot;mode&quot;: &quot;thresholds&quot;
                    },
                    &quot;max&quot;: 100,
                    &quot;min&quot;: 0,
                    &quot;thresholds&quot;: {
                        &quot;mode&quot;: &quot;absolute&quot;,
                        &quot;steps&quot;: [
                            {
                                &quot;color&quot;: &quot;green&quot;,
                                &quot;value&quot;: null
                            },
                            {
                                &quot;color&quot;: &quot;yellow&quot;,
                                &quot;value&quot;: 70
                            },
                            {
                                &quot;color&quot;: &quot;red&quot;,
                                &quot;value&quot;: 90
                            }
                        ]
                    },
                    &quot;unit&quot;: &quot;percent&quot;
                }
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 8,
                &quot;w&quot;: 12,
                &quot;x&quot;: 12,
                &quot;y&quot;: 14
            },
            &quot;options&quot;: {
                &quot;reduceOptions&quot;: {
                    &quot;calcs&quot;: [
                        &quot;lastNotNull&quot;
                    ]
                },
                &quot;showThresholdMarkers&quot;: true
            },
            &quot;targets&quot;: [
                {
                    &quot;datasource&quot;: {
                        &quot;type&quot;: &quot;prometheus&quot;,
                        &quot;uid&quot;: &quot;change me&quot;
                    },
                    &quot;expr&quot;: &quot;(node_memory_MemTotal_bytes{instance=\&quot;$instance\&quot;} - node_memory_MemAvailable_bytes{instance=\&quot;$instance\&quot;}) / node_memory_MemTotal_bytes{instance=\&quot;$instance\&quot;} * 100&quot;,
                    &quot;legendFormat&quot;: &quot;Used Memory&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                }
            ],
            &quot;title&quot;: &quot;Memory 사용량&quot;,
            &quot;type&quot;: &quot;gauge&quot;
        },
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;loki&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 8,
                &quot;w&quot;: 24,
                &quot;x&quot;: 0,
                &quot;y&quot;: 30
            },
            &quot;options&quot;: {
                &quot;dedupStrategy&quot;: &quot;none&quot;,
                &quot;enableLogDetails&quot;: true,
                &quot;showTime&quot;: true,
                &quot;sortOrder&quot;: &quot;Descending&quot;,
                &quot;wrapLogMessage&quot;: true
            },
            &quot;targets&quot;: [
                {
                    &quot;expr&quot;: &quot;{job=\&quot;php-access\&quot;}&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                }
            ],
            &quot;title&quot;: &quot;Access Logs&quot;,
            &quot;type&quot;: &quot;logs&quot;
        },
        {
            &quot;datasource&quot;: {
                &quot;type&quot;: &quot;loki&quot;,
                &quot;uid&quot;: &quot;change me&quot;
            },
            &quot;gridPos&quot;: {
                &quot;h&quot;: 8,
                &quot;w&quot;: 24,
                &quot;x&quot;: 0,
                &quot;y&quot;: 38
            },
            &quot;options&quot;: {
                &quot;dedupStrategy&quot;: &quot;exact&quot;,
                &quot;enableLogDetails&quot;: true,
                &quot;showTime&quot;: true,
                &quot;sortOrder&quot;: &quot;Descending&quot;,
                &quot;wrapLogMessage&quot;: true
            },
            &quot;targets&quot;: [
                {
                    &quot;expr&quot;: &quot;{job=\&quot;php-info\&quot;}&quot;,
                    &quot;refId&quot;: &quot;A&quot;
                }
            ],
            &quot;title&quot;: &quot;Info Logs&quot;,
            &quot;type&quot;: &quot;logs&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;pre id=&quot;code_1747287964684&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;datasource&quot;: {
    &quot;type&quot;: &quot;prometheus&quot;,
    &quot;uid&quot;: &quot;change me&quot;
},

-&amp;gt;

&quot;datasource&quot;: {
    &quot;type&quot;: &quot;prometheus&quot;,
    &quot;uid&quot;: &quot;belx4b5wxrk74a&quot;
},&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8nfxF/btsNYfogwLY/bg92IzLzsZTzPRkkNq0lS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8nfxF/btsNYfogwLY/bg92IzLzsZTzPRkkNq0lS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8nfxF/btsNYfogwLY/bg92IzLzsZTzPRkkNq0lS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8nfxF%2FbtsNYfogwLY%2Fbg92IzLzsZTzPRkkNq0lS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드의 이름을 지정해주고 Import 버튼을 클릭하여 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZJVyl/btsNZsfXjBT/90yOTh9zTP8pS6zl40SVpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZJVyl/btsNZsfXjBT/90yOTh9zTP8pS6zl40SVpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZJVyl/btsNZsfXjBT/90yOTh9zTP8pS6zl40SVpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZJVyl%2FbtsNZsfXjBT%2F90yOTh9zTP8pS6zl40SVpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드 이름을 클릭하여 대시보드 페이지로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-15 오후 3.22.11.png&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lu5TC/btsNYFtB1j8/S3JoKCLz4ReJO3EEwOgNdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lu5TC/btsNYFtB1j8/S3JoKCLz4ReJO3EEwOgNdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lu5TC/btsNYFtB1j8/S3JoKCLz4ReJO3EEwOgNdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flu5TC%2FbtsNYFtB1j8%2FS3JoKCLz4ReJO3EEwOgNdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-filename=&quot;스크린샷 2025-05-15 오후 3.22.11.png&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-15 오후 3.22.13.png&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biFC8R/btsNYCwVJdz/p0HMgf9OkDowKpy44CgXtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biFC8R/btsNYCwVJdz/p0HMgf9OkDowKpy44CgXtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biFC8R/btsNYCwVJdz/p0HMgf9OkDowKpy44CgXtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiFC8R%2FbtsNYCwVJdz%2Fp0HMgf9OkDowKpy44CgXtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2032&quot; height=&quot;1167&quot; data-filename=&quot;스크린샷 2025-05-15 오후 3.22.13.png&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;p data-ke-size=&quot;size16&quot;&gt;Ex. 초당 평균 요청량 = 특정 시간의 15초(prometheus 설정에서 지정한 매트릭 수집 주기)동안 매 초 평균적으로 발생한 request 횟수&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;각 패널이 데이터를 가져오는 곳&lt;/h4&gt;
&lt;pre id=&quot;code_1747288993329&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- Uptime, Apache Up / Down, 초당 평균 요청량, 초당 평균 전송량 : Apache Exporter(prometheus)
- CPU 사용량, Memory 사용량 : Node Exporter(prometheus)
- Access Logs, Info Logs : ~/php/logs 경로에서 가져오는 apache log 파일 내용(Loki)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 테스트&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZznkf/btsNXROMpxR/Q94iE06FmSRr1qlaxvDhrK/img.png&quot; data-origin-width=&quot;2032&quot; data-origin-height=&quot;1167&quot; data-is-animation=&quot;false&quot; /&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apache 서버로 10,000번 정도 리퀘스트를 날려보면 위 이미지처럼 서버에 발생한 부하를 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1747288190496&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ab -n 10000 -c 100 http://apache-server/

# Ex. ab -n 10000 -c 100 http://localhost/&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람의 글을 여기저기 참고하고 ai의 도움도 많이 받았지만 결국&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;새로운 시스템에 대한 이해는 내 머릿속에서 혼자 해야 하고 이것저것 뒤져보려 하는 탐구심이 있어야 공부가 되는 것 같다. 이 모니터링 시스템에 대해 이해하기까지 너무 오랜 시간이 걸렸지만 불필요한 시간은 한순간도 없었던 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 모니터링 시스템 구축뿐만 아니라 이것저것 하다 보니 처음 모니터링 시스템을 추가하려 한 때부터 이 글을 작성하기까지 너무 많은 시간이 흘렀다. 결과적으로 지식은 다양하게 늘었지만 한 가지 작업을 시작하기로 마음먹었으면 꾸준히 밀고 나가 완료시키는 끈기를 더욱 길러야 할 것 같다.&lt;/p&gt;</description>
      <category>PHP/PHP</category>
      <category>Apache</category>
      <category>exporter</category>
      <category>grafana</category>
      <category>Loki</category>
      <category>Node</category>
      <category>php</category>
      <category>Prometheus</category>
      <category>Promtail</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/282</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-Docker-Prometheus-Grafana-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81#entry282comment</comments>
      <pubDate>Thu, 15 May 2025 15:35:42 +0900</pubDate>
    </item>
    <item>
      <title>Ubuntu - 서버간 파일 전송(scp 명령어)</title>
      <link>https://dev-kimchi.tistory.com/entry/Ubuntu-%EC%84%9C%EB%B2%84%EA%B0%84-%ED%8C%8C%EC%9D%BC-%EC%A0%84%EC%86%A1scp-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ubuntu.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;1459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsDViw/btsNURtqv38/1hBOabYnySSqbdVKWv1XG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsDViw/btsNURtqv38/1hBOabYnySSqbdVKWv1XG0/img.png&quot; data-alt=&quot;그의 이름은 linux mint입니다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsDViw/btsNURtqv38/1hBOabYnySSqbdVKWv1XG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsDViw%2FbtsNURtqv38%2F1hBOabYnySSqbdVKWv1XG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;341&quot; height=&quot;585&quot; data-filename=&quot;ubuntu.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;1459&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그의 이름은 linux mint입니다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;여러 서버를 운영하다보면 가끔 필요해지는 서버간 파일 전송 방법에 대해 정리해보았다.&lt;br&gt;scp보다는 rsync를 사용하는게 안정적이라고는 들었으나 일단은 scp로도 충분한 것 같아 rsync는 추후에 사용하게 되면 그때 정리할 예정이다.&lt;/p&gt;
&lt;h3&gt;1. 서버 A에 SSH 접속&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh 서버A유저@서버A의IP
# ex. ssh userA@123.123.123.123&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. SSH 키쌍 생성 (서버 A에서)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t rsa -b 4096&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 생성된 키쌍 파일 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -al&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. pub key 내용 확인 및 복사&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat ~/.ssh/id_rsa.pub&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 서버 B에 SSH 접속&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh 서버B유저@서버B의IP
# ex. ssh userB@456.456.456.456&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 서버 A의 pub key를 서버 B의 authorized_keys에 추가&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo [서버 A에서 복사한 공개키 내용] &amp;gt;&amp;gt; ~/.ssh/authorized_keys
# ex. echo ssh-rsa AAAAB3Nza...userA@123.123.123.123 &amp;gt;&amp;gt; ~/.ssh/authorized_keys&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. 서버 A에서 서버 B로 접속 테스트&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh 서버B유저@서버B의IP
# ex. ssh userB@456.456.456.456&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8. 서버 A에서 파일 전송&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp 전송할파일 서버B유저@서버B의IP:저장할경로
# ex. scp test.txt userB@456.456.456.456:/home/userB/test.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9. 서버 B에서 파일 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -al&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;폴더 전송&lt;/h3&gt;
&lt;p&gt;폴더 전체를 전송할 때는 &lt;code&gt;-r&lt;/code&gt; 옵션을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp -r 전송할폴더 서버B유저@서버B의IP:저장할경로
# ex. scp -r testFolder userB@456.456.456.456:/home/userB/testFolder&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;주의 사항&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;파일 저장 시 전송 대상 경로에 폴더가 없으면 에러 발생하므로 사전에 수동으로 생성해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p 저장할경로
# ex. mkdir -p /home/userB/testFolder&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;서버 B의 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;가 없을 경우&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Linux/Ubuntu</category>
      <category>chmod</category>
      <category>File</category>
      <category>rsync</category>
      <category>SCP</category>
      <category>ssh</category>
      <category>ubuntu</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/281</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Ubuntu-%EC%84%9C%EB%B2%84%EA%B0%84-%ED%8C%8C%EC%9D%BC-%EC%A0%84%EC%86%A1scp-%EB%AA%85%EB%A0%B9%EC%96%B4#entry281comment</comments>
      <pubDate>Mon, 12 May 2025 17:34:40 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - Nginx Proxy Manager 502 bad gateway 에러 해결</title>
      <link>https://dev-kimchi.tistory.com/entry/Nginx-Nginx-Proxy-Manager-502-bad-gateway-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;nginx2.jpg&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SlHEA/btsNSH61uSs/OKax7eXO4prkreFshFGS8k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SlHEA/btsNSH61uSs/OKax7eXO4prkreFshFGS8k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SlHEA/btsNSH61uSs/OKax7eXO4prkreFshFGS8k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSlHEA%2FbtsNSH61uSs%2FOKax7eXO4prkreFshFGS8k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;462&quot; data-filename=&quot;nginx2.jpg&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;/figure&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;GCP 인스턴스에서 OCI 인스턴스로 서버를 이전하던 중 기존 GCP에서는 문제없이 작동하던 Docker 기반 네트워크 구조가 OCI에서는 제대로 동작하지 않았고,&amp;nbsp;그로 인해 Nginx Proxy Manager(이하 npm) 컨테이너가 다른 서비스 컨테이너와 통신을 할 수 없어 도메인을 연결할 수 없게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 꽤나 많은 시간을 허비하게 되었고, 이는 그 과정을 압축한 글이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI로 서버를 옮긴 뒤 Docker 설정을 기존과 동일하게 구성했지만, npm이 다른 컨테이너와 통신하지 못했다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1. GCP에서는 npm이 `172.17.0.1:[service port]`로 통신하면 정상 작동했지만 OCI에서 같은 방식으로 설정할 경우 502 bad gateway 에러 발생
2. `npm` 컨테이너 쉘에 접속해서 직접 `172.17.0.1:[service port]`로 curl 요청을 날려보았지만 실패
3. oci 서버의 Docker 버전이 gcp 서버와 차이가 있어 동일하게 맞춰봤지만 실패
4. npm을 Docker 기본 `bridge` 네트워크에 수동으로 연결해봤지만 실패
5. gcp 서버의 설정과 비교하며 모든 설정을 동일함을 확인했으나 여전히 실패&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게는 컨테이너의 내부 ip로 연결하는 방법도 있겠지만 컨테이너가 재시작될때마다 새로 설정을 해주어야 해서 실용성이 없다. 물론 컨테이너를 생성할때 ip가 고정되도록 설정할 수 있겠지만 추후에 컨테이너를 생성할때마다 옵션 명령어를 덕지덕지 붙여야해서 마음에 들지 않는다. 그래서 나는 아래 방법을 선택하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;code&gt;npm&lt;/code&gt;과 연결할 서비스 컨테이너를 같은 네트워크로 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747029415947&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# network 연결 명령어(service 부분에 컨테이너명을 입력)
docker network connect nginx_default service

# network 연결 확인 명령어
docker inspect nginx_default&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9reUb/btsNR4Vlkfl/JOfewGraOU5Mx6rytoxvY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9reUb/btsNR4Vlkfl/JOfewGraOU5Mx6rytoxvY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9reUb/btsNR4Vlkfl/JOfewGraOU5Mx6rytoxvY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9reUb%2FbtsNR4Vlkfl%2FJOfewGraOU5Mx6rytoxvY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;592&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;code&gt;[컨테이너명]:[내부 포트]&lt;/code&gt;로 연결한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dczevg/btsNTGrVEpM/fldQc2l1qKzwnKLeQq5UCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dczevg/btsNTGrVEpM/fldQc2l1qKzwnKLeQq5UCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dczevg/btsNTGrVEpM/fldQc2l1qKzwnKLeQq5UCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdczevg%2FbtsNTGrVEpM%2FfldQc2l1qKzwnKLeQq5UCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;784&quot; height=&quot;386&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지긋지긋한 502 bad gateway 페이지가 사라졌다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도커의 네트워크 구성에 대해 뜯어보다 알게된 사실&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;컨테이너명으로 DNS 접근&lt;/b&gt;은 기본 &lt;code&gt;bridge&lt;/code&gt; 네트워크에서는 지원되지 않는다.&lt;/li&gt;
&lt;li&gt;동일 네트워크 내에서는 내부 포트로 접근해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직도 왜 컨테이너 내부에서 기본 bridge 네트워크로 요청을 날릴 수 없는건지 이해가 되질 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 최선의 방법을 찾아낸 것 같으니 여기까지만 하고 다른 작업을 시작해야겠다...&lt;/p&gt;</description>
      <category>기타</category>
      <category>docker</category>
      <category>Manager</category>
      <category>Network</category>
      <category>nginx</category>
      <category>Proxy</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/280</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Nginx-Nginx-Proxy-Manager-502-bad-gateway-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0#entry280comment</comments>
      <pubDate>Mon, 12 May 2025 14:59:37 +0900</pubDate>
    </item>
    <item>
      <title>MAC 주소란?</title>
      <link>https://dev-kimchi.tistory.com/entry/MAC-%EC%A3%BC%EC%86%8C%EB%9E%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;mac_address.jpg&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;815&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T17nV/btsNSbZAsrE/mk3UPoc7X7fqLgSvSakfZ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T17nV/btsNSbZAsrE/mk3UPoc7X7fqLgSvSakfZ1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T17nV/btsNSbZAsrE/mk3UPoc7X7fqLgSvSakfZ1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT17nV%2FbtsNSbZAsrE%2Fmk3UPoc7X7fqLgSvSakfZ1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;640&quot; data-filename=&quot;mac_address.jpg&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;815&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MAC 주소란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MAC 주소(Media Access Control Address)&lt;/b&gt;는 네트워크에 연결된 기기의 &lt;b&gt;네트워크 인터페이스(예: Wi-Fi 칩, 이더넷 포트)&lt;/b&gt;에 부여된 고유한 식별자이다.&lt;br /&gt;&lt;b&gt;데이터 링크 계층(Layer 2)&lt;/b&gt;에서 로컬 네트워크(LAN) 내에서 기기를 식별하고, 데이터를 정확히 전달하는 데 사용된다.&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;애플의 mac과는 전혀 상관 없다. 애플에서는 혼동되지 않도록 mac 주소를 프로토콜 주소&lt;/i&gt;(Ex. 와이파이 주소) 같은 식으로 표기한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MAC 주소의 특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;형식&lt;/b&gt;: 48비트(6바이트) 16진수. 예: &lt;code&gt;AA:BB:CC:DD:EE:FF&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞 24비트: &lt;b&gt;OUI(제조사 고유 ID)&lt;/b&gt; &amp;mdash; 예: 삼성, 애플 등&lt;/li&gt;
&lt;li&gt;뒤 24비트: 제조사가 임의로 지정하는 고유 번호&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고유성&lt;/b&gt;:&lt;br /&gt;총 가능한 조합: &lt;code&gt;2^48 &amp;asymp; 281조 개&lt;/code&gt; &amp;rarr; 전 세계 기기 수 충분히 커버&lt;br /&gt;제조사 실수로 중복될 수 있으나 매우 드묾&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고정성&lt;/b&gt;:&lt;br /&gt;기본적으로 하드웨어에 고정되지만, 소프트웨어로 &lt;b&gt;스푸핑&lt;/b&gt;하거나&lt;br /&gt;&lt;b&gt;랜덤 MAC 주소&lt;/b&gt;(브라우저 시크릿모드 등)로 변경 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 범위&lt;/b&gt;:&lt;br /&gt;&lt;b&gt;LAN 내 통신&lt;/b&gt;에서만 사용됨. 인터넷 같은 외부 네트워크에서는 사용되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MAC 주소의 역할&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  기기 식별&lt;/h3&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;예: 스마트폰(MAC: &lt;code&gt;AA:BB...&lt;/code&gt;)과 노트북(MAC: &lt;code&gt;11:22...&lt;/code&gt;)을 정확히 식별&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  데이터 프레임 전달&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프레임&lt;/b&gt; 단위로 전송되는 데이터에 송/수신 MAC 주소가 포함됨&lt;/li&gt;
&lt;li&gt;공유기나 스위치는 이를 기준으로 데이터를 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  ARP (Address Resolution Protocol)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 주소 &amp;rarr; MAC 주소로 변환&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;192.168.1.100&lt;/code&gt;에 데이터 전송 시 해당 IP의 MAC 주소 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  네트워크 보안 및 관리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAC 필터링: 특정 MAC 주소만 Wi-Fi 접속 허용&lt;/li&gt;
&lt;li&gt;관리자가 MAC 기반으로 사용자 추적 및 접속 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MAC 주소와 IP 주소&lt;/h2&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;MAC 주소&lt;/th&gt;
&lt;th&gt;IP 주소&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;LAN 내 기기 식별&lt;/td&gt;
&lt;td&gt;네트워크 간 통신 위치 식별&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;계층&lt;/td&gt;
&lt;td&gt;데이터 링크 계층 (Layer 2)&lt;/td&gt;
&lt;td&gt;네트워크 계층 (Layer 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부여 시점&lt;/td&gt;
&lt;td&gt;제조 시 하드웨어에 고정&lt;/td&gt;
&lt;td&gt;네트워크 연결 시 DHCP/수동 할당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변경 가능성&lt;/td&gt;
&lt;td&gt;스푸핑 등으로 가능&lt;/td&gt;
&lt;td&gt;자주 변경 가능 (DHCP 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 범위&lt;/td&gt;
&lt;td&gt;로컬 네트워크(LAN)&lt;/td&gt;
&lt;td&gt;인터넷 포함 전 범위&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MAC 주소와 Wi-Fi 공유기의 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Wi-Fi 공유기는 &lt;b&gt;Layer 2&lt;/b&gt;와 &lt;b&gt;Layer 3&lt;/b&gt;를 동시에 처리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Layer 2 (MAC 기반 내부 통신)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAC 주소 기반 프레임 포워딩&lt;/li&gt;
&lt;li&gt;ARP 테이블을 통해 IP &amp;harr; MAC 변환&lt;/li&gt;
&lt;li&gt;클라이언트 간 직접 통신도 MAC 주소로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Layer 3 (IP 기반 외부 통신)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DHCP로 IP 주소 할당&lt;/li&gt;
&lt;li&gt;NAT로 내부 IP &amp;harr; 외부 공인 IP 변환&lt;/li&gt;
&lt;li&gt;라우팅 기능을 통해 인터넷과 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 예시 &amp;ndash; 웹사이트 로딩 시 MAC 주소의 역할&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저 &amp;rarr; 웹 서버로 HTTP 요청&lt;/li&gt;
&lt;li&gt;공유기 &amp;rarr; 클라이언트의 MAC 주소로 요청 수신&lt;/li&gt;
&lt;li&gt;NAT 처리 후 인터넷으로 전달&lt;/li&gt;
&lt;li&gt;웹 서버 응답 &amp;rarr; 공유기 &amp;rarr; 클라이언트 MAC 주소로 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 질문 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MAC 주소는 왜 필요할까?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 로컬 네트워크에서 실제 기기를 식별해야 하므로 필수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;바뀔 수 있나?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 기본은 고정이지만 스푸핑, 프라이버시 보호 목적으로 변경 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;겹치면 어떻게 되나?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 이론적으로 가능, 실제로는 ARP 혼선 &amp;rarr; 데이터 오작동 가능성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;어떻게 확인하나?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;arp -a&lt;/code&gt; (로컬 ARP 테이블 확인)&lt;/li&gt;
&lt;li&gt;라우터 관리자 페이지&lt;/li&gt;
&lt;li&gt;Fing, Nmap, Wireshark 등 툴 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;MAC 주소는 로컬 네트워크에서 기기를 &lt;b&gt;물리적으로 식별&lt;/b&gt;하고&lt;br /&gt;데이터 전달의 정확성을 보장하는 &lt;b&gt;필수 요소&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IP 주소는 &quot;어디로&quot; 보낼지를 결정&lt;/li&gt;
&lt;li&gt;MAC 주소는 &quot;누구에게&quot; 보낼지를 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 비유를 하자면&lt;/p&gt;
&lt;pre class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;네트워크 = 회사
MAC 주소 = 직원의 사번
-   사번은 입사 시 부여되는 고유 ID, 바뀌지 않음
-   그 직원이 누구인지 회사 내에서 항상 이 번호로 식별
IP 주소 = 좌석 번호
-   직원이 매일 같은 자리에 앉지 않을 수도 있고, 부서 이동 시 자리를 바꿈
-   위치는 상황에 따라 유동적이며, 그날 그 순간만 유효
ARP = 좌석배치도  
-   누가 어느 자리에 앉아 있는지 알려주는 배치표
-   &amp;ldquo;사번 1234번 직원이 지금 5번 좌석에 앉아 있음&amp;rdquo;
공유기 = 부서 관리자
-   부서 간 문서를 전달하거나 전화 연결 등 소통 중개자 역할
-   사람 이름이나 부서가 아니라 사번으로 정확히 찾아 연결&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인 셈이다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>ARP</category>
      <category>IP</category>
      <category>Mac</category>
      <category>Network</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/279</guid>
      <comments>https://dev-kimchi.tistory.com/entry/MAC-%EC%A3%BC%EC%86%8C%EB%9E%80#entry279comment</comments>
      <pubDate>Fri, 9 May 2025 15:43:25 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - Nginx Proxy Manager Stream 설정</title>
      <link>https://dev-kimchi.tistory.com/entry/Nginx-Nginx-Proxy-Manager-Stream-%EC%84%A4%EC%A0%95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;생각_3D.gif&quot; data-origin-width=&quot;256&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qmCy3/btsNOh0kDvv/44E7Y2fudXoBraKbNkrtU1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qmCy3/btsNOh0kDvv/44E7Y2fudXoBraKbNkrtU1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qmCy3/btsNOh0kDvv/44E7Y2fudXoBraKbNkrtU1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/qmCy3/btsNOh0kDvv/44E7Y2fudXoBraKbNkrtU1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;256&quot; height=&quot;256&quot; data-filename=&quot;생각_3D.gif&quot; data-origin-width=&quot;256&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;개요&lt;/h3&gt;
&lt;p&gt;최근 GCP 프리티어 VM이 너무 느린 것 같아 Oracle Cloud Infrastructure(OCI)의 프리티어로 서버를 옮기게 되었다. OCI는 상상 이상으로 쾌적하게 잘 작동했지만, 인스턴스 간 네트워크 구성에서 예상치 못한 문제가 발생했다.&lt;br&gt;OCI에서는 프리티어 기준으로 &lt;strong&gt;VM 인스턴스를 2개까지 생성 가능&lt;/strong&gt;하지만, &lt;strong&gt;Public IP는 1개만 예약 가능&lt;/strong&gt;하다.&lt;br&gt;그리하여 아래와 같이 구성했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;main-instance&lt;/strong&gt;: 실제 서비스 운영 (Nginx, 백엔드 API 등), Public IP 보유&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;db-instance&lt;/strong&gt;: MySQL DB만 운영, 사설 IP만 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 위 구조로 진행 중 DB 툴(MySQL Workbench, DBeaver 등)에서 MySQL에 접속하려고 할 때 문제가 발생했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;원인&lt;/h3&gt;
&lt;p&gt;처음엔 단순히 main-instance의 Nginx Proxy Manager를 통해 프록시 설정을 하면 해결될 줄 알았다. 실제로 HTTP 프록시 기능을 이용해 여러 서비스들을 잘 연동하고 있었기 때문에 MySQL도 비슷할 거라고 생각했다.&lt;/p&gt;
&lt;p&gt;하지만 문제는 MySQL은 HTTP 기반이 아닌 &lt;strong&gt;TCP 기반의 독자적인 프로토콜&lt;/strong&gt;을 사용하는 서비스라는 점이었다. 기존 HTTP 프록시 설정으로는 전혀 동작하지 않았다.&lt;/p&gt;
&lt;p&gt;이건 결국 &lt;strong&gt;OSI 7계층&lt;/strong&gt;의 차이에서 비롯된 문제였다.&lt;/p&gt;
&lt;p&gt;구분HTTP Proxy (기본 프록시)Stream Proxy (TCP/UDP)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;계층&lt;/th&gt;
&lt;th&gt;7계층 (Application Layer)&lt;/th&gt;
&lt;th&gt;4계층 (Transport Layer)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;사용 프로토콜&lt;/td&gt;
&lt;td&gt;HTTP, HTTPS&lt;/td&gt;
&lt;td&gt;TCP, UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 용도&lt;/td&gt;
&lt;td&gt;웹 서비스, 리버스 프록시&lt;/td&gt;
&lt;td&gt;DB 연결, SSH, 커스텀 포트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 예시&lt;/td&gt;
&lt;td&gt;Web → Nginx Proxy&lt;/td&gt;
&lt;td&gt;MySQL, Redis, SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;해결&lt;/h3&gt;
&lt;p&gt;문제를 해결하기 위해 Nginx Proxy Manager의 고급 기능 중 하나인 &lt;strong&gt;stream 프록시&lt;/strong&gt;를 사용했다. 이 기능은 HTTP가 아닌 &lt;strong&gt;TCP 또는 UDP 기반의 트래픽을 프록시&lt;/strong&gt;해주는 기능이다.&lt;/p&gt;
&lt;h4&gt;✅ 설정 방법&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager 대시보드 접속&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;quot;Streams&amp;quot; 메뉴 &amp;gt; Add Stream&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;다음과 같이 설정:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Incoming Port&lt;/strong&gt;: 로컬 PC에서 접근할 포트(예: 3306)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forward Host&lt;/strong&gt;: db-instance의 Private IP (예: 10.0.0.5)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forward Port&lt;/strong&gt;: db-instance에서 접근할 포트(예: 3306)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP Forwarding&lt;/strong&gt;: true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-08 오후 1.09.31.png&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkwGRO/btsNMSN9qFl/UfwP8uleoyYg3R7ObwfPo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkwGRO/btsNMSN9qFl/UfwP8uleoyYg3R7ObwfPo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkwGRO/btsNMSN9qFl/UfwP8uleoyYg3R7ObwfPo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkwGRO%2FbtsNMSN9qFl%2FUfwP8uleoyYg3R7ObwfPo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;416&quot; data-filename=&quot;스크린샷 2025-05-08 오후 1.09.31.png&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;설정 저장 후, 로컬 PC에서 main-instance의 Public IP:3306으로 접속하면 MySQL에 정상적으로 연결된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;로컬 PC - MySQL 접속 구조&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[로컬 PC] --- TCP 3306 --&amp;gt; [main-instance (Public IP)]  
                                         |  
                                         | [Stream 프록시: 3306]  
                                         |  
                              [db-instance (사설 IP)]  
                                       MySQL&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h3&gt;후기&lt;/h3&gt;
&lt;p&gt;아주 먼 옛날 고등학교 정보 시간에 배웠던 계층에 대한 내용을 실제 개발을 하며 마주하니 감회가 새로웠다.&lt;/p&gt;
&lt;p&gt;단순히 구현만 잘하는 개발자보다는 폭 넓은 지식으로 다양한 시각으로 문제를 바라볼 수 있는 개발자가 되기 위해 꾸준히 노력해야겠다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>Manager</category>
      <category>nginx</category>
      <category>NPM</category>
      <category>OCI</category>
      <category>Proxy</category>
      <category>Stream</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/278</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Nginx-Nginx-Proxy-Manager-Stream-%EC%84%A4%EC%A0%95#entry278comment</comments>
      <pubDate>Thu, 8 May 2025 13:23:57 +0900</pubDate>
    </item>
    <item>
      <title>Docker - Dockerfile에서 crontab 설정하기</title>
      <link>https://dev-kimchi.tistory.com/entry/Docker-Dockerfile%EC%97%90%EC%84%9C-crontab-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n07Q6/btsNAURZ0ud/9PTGx7GNHkmz7rEUG8iyXk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n07Q6/btsNAURZ0ud/9PTGx7GNHkmz7rEUG8iyXk/img.jpg&quot; data-alt=&quot;도커 덕분에 행복해요&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n07Q6/btsNAURZ0ud/9PTGx7GNHkmz7rEUG8iyXk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn07Q6%2FbtsNAURZ0ud%2F9PTGx7GNHkmz7rEUG8iyXk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;376&quot; height=&quot;464&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;839&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도커 덕분에 행복해요&lt;/figcaption&gt;
&lt;/figure&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;Docker 컨테이너 안에서 cron을 실행해야 하는 작업이 있어서 Dockerfile에 cron 설정을 추가했는데, 컨테이너는 잘 올라갔지만 cron이 실행되지 않는 문제가 발생했다.&lt;br /&gt;crontab -l로 확인해보면 제대로 등록까지 되어 있었는데도 아무 반응이 없었고, 구글로 검색했더니 명확한 해결방법을 찾지 못해 수많은 삽질을 통해 찾게된 해결방법을 정리해본다(몹시 간단).&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dockerfile에 crontab 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 사용한 Dockerfile은 대략 아래와 같았다&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 베이스 이미지
FROM ubuntu:20.04

# 패키지 설치
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y cron

# cron 파일 복사
COPY cronfile /etc/cron.d/cronfile

# cron 파일 실행 권한 부여
RUN chmod 0644 /etc/cron.d/cronfile

# cron 작업 정의
RUN crontab /etc/cron.d/cronfile

# cron 로그 디렉토리 생성
RUN touch /cron.log

# cron log 확인
ENTRYPOINT [&quot;tail&quot;, &quot;-f&quot;, &quot;/cron.log&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 cronfile&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;* * * * * echo &quot;Hello, World!&quot; &amp;gt;&amp;gt; /var/log/cron.log&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;crontab -l로 확인해보면?&lt;/h2&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker exec -it &amp;lt;container_name&amp;gt; crontab -l&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어로 확인해보면 분명히 crontab은 잘 등록되어 있었다.&lt;br /&gt;그런데 로그는 전혀 찍히지 않았고, cron이 도는 흔적이 없었다.&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;crontab 등록만 해서는 cron 데몬이 자동으로 실행되지 않는다.&lt;/b&gt;&lt;br /&gt;도커 컨테이너는 기본적으로 단일 프로세스를 실행하기 때문에, cron 서비스를 명시적으로 시작해주지 않으면 crontab만 등록된 상태로 끝난다.&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;Dockerfile에 cron 서비스를 실행하도록 명시적으로 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;CMD&lt;/code&gt; 또는 &lt;code&gt;ENTRYPOINT&lt;/code&gt;로 컨테이너 실행 시점에 cron을 실행하도록 해야 한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 베이스 이미지
FROM ubuntu:20.04

# 패키지 설치
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y cron

# cron 파일 복사
COPY cronfile /etc/cron.d/cronfile

# cron 파일 실행 권한 부여
RUN chmod 0644 /etc/cron.d/cronfile

# cron 작업 정의
RUN crontab /etc/cron.d/cronfile

# cron 로그 디렉토리 생성
RUN touch /cron.log

# entrypoint.sh 파일 복사
COPY entrypoint.sh /entrypoint.sh

# entrypoint.sh 파일 실행 권한 부여
RUN chmod +x /entrypoint.sh

# 컨테이너 실행 시 cron 실행
ENTRYPOINT [&quot;/bin/bash&quot;, &quot;/entrypoint.sh&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 entrypoint.sh&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;#!/bin/bash

# cron 서비스 시작
service cron start

# cron 로그 확인
tail -f /cron.log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 정상적으로 매 분마다 Hello World 로그가 추가된다.&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;cron이 정상적으로 등록되었는데도 &lt;b&gt;지정한 시간에 실행되지 않는다면&lt;/b&gt;,&lt;br /&gt;컨테이너의 &lt;b&gt;시간대(timezone)&lt;/b&gt; 설정이 잘못되어 있을 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;date&lt;/code&gt; 명령어로 현재 시간을 확인해보자:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;docker exec -it &amp;lt;container_name&amp;gt; date&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 한국 시간이 아니라면, Dockerfile에 다음 설정을 추가해주면 된다:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime &amp;amp;&amp;amp; echo $TZ &amp;gt; /etc/timezone&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;docker에서 쌩으로 crontab을 돌려보는건 처음이라 좀 해메게 되었는데 덕분에 docker와 cron에 대해 조금 더 알아가는 시간이 된 것 같아 좋은 것 같다.&lt;br /&gt;docker는 뭔 놈의 기술이 이렇게도 이해가 됐다 싶으면 멀어지고 또 이해가 됐다 싶으면 또 멀어지는지 알다가도 모르겠다.&lt;/p&gt;</description>
      <category>Docker</category>
      <category>cron</category>
      <category>crontab</category>
      <category>date</category>
      <category>docker</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/277</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Docker-Dockerfile%EC%97%90%EC%84%9C-crontab-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0#entry277comment</comments>
      <pubDate>Fri, 25 Apr 2025 21:10:27 +0900</pubDate>
    </item>
    <item>
      <title>기타 - compinit:480: compdump: function definition file not found 에러 해결(feat. m4 에어 마이그레이션)</title>
      <link>https://dev-kimchi.tistory.com/entry/%EA%B8%B0%ED%83%80-compinit480-compdump-function-definition-file-not-found-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0feat-m4-%EC%97%90%EC%96%B4-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;112&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byX0k5/btsNxZLXs9N/O5v0tXm65g2esK5BZXGDPk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byX0k5/btsNxZLXs9N/O5v0tXm65g2esK5BZXGDPk/img.gif&quot; data-alt=&quot;사랑스러운 우리 맥부기맥북맥북어맥북스딱스맥부르크맥부가우가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byX0k5/btsNxZLXs9N/O5v0tXm65g2esK5BZXGDPk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/byX0k5/btsNxZLXs9N/O5v0tXm65g2esK5BZXGDPk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;267&quot; height=&quot;267&quot; data-origin-width=&quot;112&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사랑스러운 우리 맥부기맥북맥북어맥북스딱스맥부르크맥부가우가&lt;/figcaption&gt;
&lt;/figure&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;드디어 맥북을 업그레이드 했다. 오랜 기간 동고동락한 m1 에어를 보내주고 m4 에어를 질러버렸다.&lt;br /&gt;기분좋게 마이그레이션까지 마치고 VScode에서 터미널을 열었는데 갑자기 아래와 같은 에러가 출력되기 시작했다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;compinit:480: compdump: function definition file not found
.zshrc:188: add-zsh-hook: function definition file not found&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;compinit 관련 에러&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zsh에서 &lt;code&gt;compinit&lt;/code&gt;은 자동 완성 기능을 초기화할 때 사용하는 함수인데, 뭔가 꼬였는지 함수 파일 자체를 못 찾는다고 에러가 발생했다.&lt;br /&gt;그래서 &lt;code&gt;.zshrc&lt;/code&gt;에 다음 스크립트를 추가해보았다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;autoload -Uz compinit
compinit

autoload -U bashcompinit
bashcompinit&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;Homebrew zsh 제거 시도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 zsh가 이상하게 설치되어 있는 건가 싶어서 Homebrew로 설치된 zsh를 제거해보려 했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;brew uninstall zsh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 새로운 에러가 발생&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Error: Permission denied @ rb_sysopen - /opt/homebrew/var/homebrew/locks/go.formula.lock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 &lt;code&gt;brew&lt;/code&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;code&gt;/opt/homebrew&lt;/code&gt;&amp;nbsp;아래에 있는 파일들이 &lt;b&gt;admin 계정 소유&lt;/b&gt;로 되어 있어서 현재 사용자 권한으로는 접근이나 수정이 불가능한 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ls -al /opt/homebrew&lt;/code&gt;&amp;nbsp;해보면 소유자가 &lt;code&gt;admin&lt;/code&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;code&gt;brew&lt;/code&gt; 설치 경로 전체의 소유자를 현재 사용자로 변경했다&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo chown -R $(whoami) /opt/homebrew&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령을 실행한 후 &lt;code&gt;brew uninstall zsh&lt;/code&gt; 명령도 잘 작동했고, &lt;code&gt;.zshrc&lt;/code&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;code&gt;brew&lt;/code&gt; 권한부터 의심해보자.&lt;/p&gt;</description>
      <category>기타</category>
      <category>BREW</category>
      <category>chown</category>
      <category>homebrew</category>
      <category>Mac</category>
      <category>migrations</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/276</guid>
      <comments>https://dev-kimchi.tistory.com/entry/%EA%B8%B0%ED%83%80-compinit480-compdump-function-definition-file-not-found-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0feat-m4-%EC%97%90%EC%96%B4-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98#entry276comment</comments>
      <pubDate>Thu, 24 Apr 2025 11:34:14 +0900</pubDate>
    </item>
    <item>
      <title>PHP - Apache server status 설정 + 프록시 IP 처리</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-Apache-server-status-%EC%84%A4%EC%A0%95-%ED%94%84%EB%A1%9D%EC%8B%9C-IP-%EC%B2%98%EB%A6%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWVdGx/btsNr6yAQeV/m8WxRM7wlp9zQFuDDFKXuk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWVdGx/btsNr6yAQeV/m8WxRM7wlp9zQFuDDFKXuk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWVdGx/btsNr6yAQeV/m8WxRM7wlp9zQFuDDFKXuk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWVdGx%2FbtsNr6yAQeV%2Fm8WxRM7wlp9zQFuDDFKXuk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;558&quot; height=&quot;339&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Grafana로 PHP 서버 상태를 모니터링하려고 하다가 Apache 환경에서의 설정과 보안 문제를 겪게 되어, 그 과정을 정리해보았다.&lt;br /&gt;특히 &lt;code&gt;server-status&lt;/code&gt;를 외부에서 접근 가능하게 열었을 때 생기는 보안 이슈와, Nginx 프록시 환경에서의 IP 처리 문제를 어떻게 해결했는지를 중심으로 다룬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Apache server-status 활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache에서는 &lt;code&gt;mod_status&lt;/code&gt; 모듈을 활성화한 뒤 &lt;code&gt;/server-status&lt;/code&gt; 엔드포인트를 열어 서버 상태 정보를 확인할 수 있다.&lt;br /&gt;Grafana로 apache 서버를 모니터링 할때는 해당 데이터를 수집하기 위해 보통 Prometheus용 Apache Exporter와 연결하여 시각화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 다음과 같은 설정으로 &lt;code&gt;server-status&lt;/code&gt;를 활성화했었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;Location /server-status&amp;gt;
    SetHandler server-status
    Require all granted
&amp;lt;/Location&amp;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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IP 제한 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Exporter는 내부망에서 동작하도록 구성되어 있었기 때문에, 내부망에서만 접근 가능하도록 IP 제한을 걸어주었다.&lt;br /&gt;여기서 말하는 내부망은 &lt;code&gt;172.16.0.0/12&lt;/code&gt; 대역이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: 172.16.0.0/12는 사설 IP 주소 대역 중 하나로,&lt;br /&gt;172.16.0.0부터 172.31.255.255까지 포함하는 범위이다.&lt;br /&gt;보통 기업 내부망이나 VPC 등에서 많이 사용된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래처럼 IP 제한을 설정했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;lt;Location /server-status&amp;gt;
    SetHandler server-status
    Require ip 172.16.0.0/12
&amp;lt;/Location&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;Nginx Proxy 환경에서의 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 여기서 발생했다.&lt;br /&gt;서버는 Nginx Reverse Proxy 뒤에 있었고, Apache는 클라이언트의 IP가 아닌 &lt;b&gt;프록시 서버의 IP&lt;/b&gt;를 클라이언트 IP로 인식하고 있었다.&lt;br /&gt;즉, 실제 클라이언트가 내부망에 있지 않더라도, Apache 입장에서는 Nginx의 IP만 보이는 상황이었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client &amp;rarr; Nginx Proxy (내부망) &amp;rarr; Apache (내부망)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache는 클라이언트가 아닌 프록시 IP가 &lt;code&gt;172.16.0.0/12&lt;/code&gt;에 포함되는지만 보고 판단했는데, 프록시 서버도 내부망에 있었기 때문에 &lt;b&gt;사실상 모든 요청이 이미 IP 조건을 통과하고 있었던&lt;/b&gt; 상황이었고 결과적으로 &lt;code&gt;Require ip&lt;/code&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;클라이언트의 실제 IP를 인식하도록 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 제대로 해결하기 위해 Apache의 &lt;code&gt;mod_remoteip&lt;/code&gt; 모듈을 사용해서 프록시가 전달하는 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 헤더를 신뢰하도록 설정했다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;lt;IfModule remoteip_module&amp;gt;
    RemoteIPHeader X-Forwarded-For
    RemoteIPInternalProxy 172.16.0.0/12
&amp;lt;/IfModule&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;RemoteIPHeader&lt;/code&gt;: 프록시 서버가 전달하는 헤더 이름 (&lt;code&gt;X-Forwarded-For&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RemoteIPInternalProxy&lt;/code&gt;: 이 IP 대역에서 오는 요청만 프록시로 신뢰함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Apache는 &lt;code&gt;172.16.0.0/12&lt;/code&gt;에 해당하는 프록시 서버에서 온 요청에 대해서만 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 헤더를 신뢰하고 클라이언트 IP로 간주한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;X-Forwarded-For 보안 이슈?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위처럼 설정을 하고 나니 누군가가 직접 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 헤더를 조작하면 IP 제한을 우회할 수 있지 않을까 걱정되었다.&lt;br /&gt;하지만 테스트를 해보니 그렇지 않았다. 그것은 apache서버의 요청 처리 방식 때문이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apache는 먼저 &lt;b&gt;요청의 Remote IP가 &lt;code&gt;RemoteIPInternalProxy&lt;/code&gt; 조건을 만족하는지&lt;/b&gt; 검사한다.&lt;/li&gt;
&lt;li&gt;조건을 통과한 경우에만 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 값을 IP로 반영한다.&lt;/li&gt;
&lt;li&gt;즉, 프록시 서버로부터 온 요청이 아니면 헤더를 무시하고, IP 제한은 정상 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만약 사용자가 Remote IP 자체를 172.16.x.x로 조작하면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 Remote IP를 내부망처럼 보이게 해서 우회하는 것이 가능하지 않을까 생각할 수 있다.&lt;br /&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출발지 IP를 172.16.0.10으로 스푸핑하고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Forwarded-For&lt;/code&gt;도 같이 조작해서 내부망 요청처럼 보이게 한다면?&lt;/li&gt;
&lt;/ul&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;Apache가 인식하는 Remote IP는 단순히 HTTP 헤더로 전달된 게 아니라,&lt;br /&gt;&lt;b&gt;커널 네트워크 계층에서 수신된 TCP 연결의 출발지 IP 주소&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RemoteAddr은 서버가 수신한 진짜 패킷의 IP이고, 이건 다음 조건을 모두 만족해야 한다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TCP 3-way handshake가 완성되어야 함&lt;/li&gt;
&lt;li&gt;서버가 응답을 보낸 IP와 다시 통신이 가능해야 함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 출발지 IP를 위조하는 순간, 응답은 &lt;b&gt;진짜 공격자에게 가지 않고 조작된 IP&lt;/b&gt;(예: 172.16.0.10)로 가게 된다.&lt;br /&gt;그러면 TCP 연결이 성립되지 않고, Apache는 요청 자체를 수락하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 단순히 curl이나 Postman 같은 도구로는 절대 &lt;code&gt;Require ip&lt;/code&gt;와 &lt;code&gt;RemoteIPInternalProxy&lt;/code&gt; 조건을 우회할 수 없다.&lt;br /&gt;출발지 IP 자체를 속이는 건 커널 수준에서 막히며, 정말로 이걸 뚫으려면 네트워크 경로를 통제하거나 내부망 장비를 뚫어야 한다.&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;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 외부 프록시 헤더 사용
&amp;lt;IfModule remoteip_module&amp;gt;
    RemoteIPHeader X-Forwarded-For
    RemoteIPInternalProxy 172.16.0.0/12
&amp;lt;/IfModule&amp;gt;

# /server-status 설정
&amp;lt;Location /server-status&amp;gt;
    SetHandler server-status
    Require ip 172.16.0.0/12
&amp;lt;/Location&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;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 단순히 Grafana 연동만 잘하면 되겠지 싶었는데, 프록시 구조나 보안 설정까지 챙기게 될 줄은 몰랐다.&lt;br /&gt;특히 &lt;code&gt;X-Forwarded-For&lt;/code&gt; 같은 헤더는 자칫 잘못 쓰면 보안 구멍이 될 수 있어서 신중하게 접근해야 했는데 다행히 Apache가 이런 시나리오를 잘 고려해놔서 설정만 잘 하면 안전하게 운영할 수 있다는 걸 확인했다.&lt;br /&gt;앞으로도 이런 설정은 주기적으로 점검하면서 사용하는 게 좋겠다.&lt;/p&gt;</description>
      <category>PHP/PHP</category>
      <category>Apache</category>
      <category>config</category>
      <category>IP</category>
      <category>nginx</category>
      <category>php</category>
      <category>Proxy</category>
      <category>Server</category>
      <category>Status</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/275</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-Apache-server-status-%EC%84%A4%EC%A0%95-%ED%94%84%EB%A1%9D%EC%8B%9C-IP-%EC%B2%98%EB%A6%AC#entry275comment</comments>
      <pubDate>Mon, 21 Apr 2025 18:45:58 +0900</pubDate>
    </item>
    <item>
      <title>DB를 털리다.</title>
      <link>https://dev-kimchi.tistory.com/entry/DB%EB%A5%BC-%ED%84%B8%EB%A6%AC%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;피곤한 월요일, 여느 때와 같이 일을 하다 잠깐 쉬는 시간에 내 사이트는 잘 돌아가고 있나 들어가보았는데&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t8fYZ/btsNkJw7MJZ/mYqUVsS39tHjnhNiPIMBA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t8fYZ/btsNkJw7MJZ/mYqUVsS39tHjnhNiPIMBA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t8fYZ/btsNkJw7MJZ/mYqUVsS39tHjnhNiPIMBA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft8fYZ%2FbtsNkJw7MJZ%2FmYqUVsS39tHjnhNiPIMBA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;321&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&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;메인페이지에서 500 에러가 발생하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경험상 잘 돌아가던 사이트의 메인페이지에서 에러가 발생하는 경우는 보통 db 연결에 장애가 생겨 그런 경우가 대부분이었어서 '뭐지 db가 꺼졌나?', 'cloudflare 연결한게 원인인가? 아닌데.. db는 ip로 연결해서 별 관련 없는데..' 하는 생각을 하며 서버에 접속을 해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐으음... db는 안 내려갔고... 이상이 없는데? redis가 내려갔나? 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;그&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RqV53/btsNlyVtetl/VTipPLBYVQoGmaz7mSh2lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RqV53/btsNlyVtetl/VTipPLBYVQoGmaz7mSh2lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RqV53/btsNlyVtetl/VTipPLBYVQoGmaz7mSh2lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRqV53%2FbtsNlyVtetl%2FVTipPLBYVQoGmaz7mSh2lk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1742&quot; height=&quot;137&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.33.05.png&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MMiWn/btsNkJw8yed/ZqXlv50BlGmcSGUM43Yeu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MMiWn/btsNkJw8yed/ZqXlv50BlGmcSGUM43Yeu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MMiWn/btsNkJw8yed/ZqXlv50BlGmcSGUM43Yeu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMMiWn%2FbtsNkJw8yed%2FZqXlv50BlGmcSGUM43Yeu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;884&quot; height=&quot;92&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.33.05.png&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.34.04.png&quot; data-origin-width=&quot;127&quot; data-origin-height=&quot;59&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CU2cj/btsNkG1vW9x/JBHkAKcTPHUyEe60gkYs91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CU2cj/btsNkG1vW9x/JBHkAKcTPHUyEe60gkYs91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CU2cj/btsNkG1vW9x/JBHkAKcTPHUyEe60gkYs91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCU2cj%2FbtsNkG1vW9x%2FJBHkAKcTPHUyEe60gkYs91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;375&quot; height=&quot;174&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.34.04.png&quot; data-origin-width=&quot;127&quot; data-origin-height=&quot;59&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;All your data is backed up. You must pay 0.0100 BTC to bc1qwvthsvr7agkmn36qxxn4ju2qe6mur37s453w43nl In 48 hours, your data will be publicly disclosed and deleted. (more information: go to https://해킹범.사이트)&lt;br /&gt;After payment send mail to us: rambler+29eqk@xxx.org and we will provide a link for you to download your data. Your DBCODE is: 29EQK&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해킹범이 내 데이터를 인질로 잡고 0.01 비트코인을 요구하고 있는 걸 알게 되었다. 근데 그게 얼마지?...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhjg5e/btsNk3Ptak6/47uyBNpie3geacq69hQkz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhjg5e/btsNk3Ptak6/47uyBNpie3geacq69hQkz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhjg5e/btsNk3Ptak6/47uyBNpie3geacq69hQkz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhjg5e%2FbtsNk3Ptak6%2F47uyBNpie3geacq69hQkz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;469&quot; height=&quot;367&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwNq4w/btsNlbeRd6s/4qVVJgfTPYsXkFG5gTsHYK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwNq4w/btsNlbeRd6s/4qVVJgfTPYsXkFG5gTsHYK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwNq4w/btsNlbeRd6s/4qVVJgfTPYsXkFG5gTsHYK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwNq4w%2FbtsNlbeRd6s%2F4qVVJgfTPYsXkFG5gTsHYK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;194&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비트코인이 1억이면... 0.01 비트코인이면...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.38.09.png&quot; data-origin-width=&quot;243&quot; data-origin-height=&quot;121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UlUyd/btsNlfISXyx/a7aDrcRwulE3PNvYZzY9w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UlUyd/btsNlfISXyx/a7aDrcRwulE3PNvYZzY9w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UlUyd/btsNlfISXyx/a7aDrcRwulE3PNvYZzY9w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUlUyd%2FbtsNlfISXyx%2Fa7aDrcRwulE3PNvYZzY9w0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;243&quot; height=&quot;121&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.38.09.png&quot; data-origin-width=&quot;243&quot; data-origin-height=&quot;121&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 120만원이 어디있어&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTqQRv/btsNkla9lQN/EoBNg3zvsB5HNk1MdNGoF1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTqQRv/btsNkla9lQN/EoBNg3zvsB5HNk1MdNGoF1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTqQRv/btsNkla9lQN/EoBNg3zvsB5HNk1MdNGoF1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTqQRv%2FbtsNkla9lQN%2FEoBNg3zvsB5HNk1MdNGoF1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;236&quot; height=&quot;297&quot; data-origin-width=&quot;236&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 천 원 한 장도 줄 생각 없었지만 120만원이라니 택도 없는 가격이었다.&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;/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;grok한테 물어볼때 코드에 노출이 됐나? 머스크형이 ai로 수집한 데이터를 판매한다고?.... 그럴....수도 있겠는데?!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql 서버 로그는 해킹범이 싹 지워버려 확인도 못하고 여러가지 추측만 난무하던 차에 문득 든 생각!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgresql은 안 털렸나?!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 공부용으로 프로젝트를 진행하고 바꾸기 귀찮아서 올려둔 psql 서버가 생각 나 로그를 조회해봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-04-14-19-50-16.png&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRiUZS/btsNlzz6Mcg/C7aFhIFVl7gUN60uGRjib0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRiUZS/btsNlzz6Mcg/C7aFhIFVl7gUN60uGRjib0/img.png&quot; data-alt=&quot;아아... psql좌.... 무슨 싸움을 하고 계셨던 겁니까...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRiUZS/btsNlzz6Mcg/C7aFhIFVl7gUN60uGRjib0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRiUZS%2FbtsNlzz6Mcg%2FC7aFhIFVl7gUN60uGRjib0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;596&quot; data-filename=&quot;KakaoTalk_Photo_2025-04-14-19-50-16.png&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아아... psql좌.... 무슨 싸움을 하고 계셨던 겁니까...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;psql 서버도 공격이 있었다!!!! 하지만 피해는 발생하지 않았다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.54.23.png&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXzrbt/btsNkkQQpHZ/U3HXmUqCXCFl2Fl2ynH3Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXzrbt/btsNkkQQpHZ/U3HXmUqCXCFl2Fl2ynH3Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXzrbt/btsNkkQQpHZ/U3HXmUqCXCFl2Fl2ynH3Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXzrbt%2FbtsNkkQQpHZ%2FU3HXmUqCXCFl2Fl2ynH3Z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;266&quot; height=&quot;101&quot; data-filename=&quot;스크린샷 2025-04-14 오후 7.54.23.png&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;?&lt;/span&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;기본 3306 포트를 사용하면서 mysql 서버 root 계정의 비밀번호를 mysql로 하는 배짱 좋은 과거의 나를 마주하자 어이가 없어 헛웃음이 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 원인은 찾았으니 기존 mysql 서버는 보내주고 기존과 다른 포트, 비밀번호로 설정한 db를 새로 올려 서비스를 다시 정상적으로 운영할 수 있게 되었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 사태를 통해 느낀 점은 크게 3가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;백업은 필수로 해야겠다.&lt;/li&gt;
&lt;li&gt;아무리 작은 서비스라도 운영중인 서비스가 장애가 생겼을때 신속히 대처하려면 모니터링 툴을 사용해야겠구나&lt;/li&gt;
&lt;li&gt;2000개 가량의 데이터를 날리게 되어 아주 귀찮아졌지만 덕분에 보안에 더 신경을 쓰게 되었으니 럭키비키다.&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 다음 포스팅은 그라파나를 사용한 php 서버 모니터링에 관한 글이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝!&lt;/p&gt;</description>
      <category>기타</category>
      <category>db</category>
      <category>MySQL</category>
      <category>보안</category>
      <category>안일</category>
      <category>해킹</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/274</guid>
      <comments>https://dev-kimchi.tistory.com/entry/DB%EB%A5%BC-%ED%84%B8%EB%A6%AC%EB%8B%A4#entry274comment</comments>
      <pubDate>Mon, 14 Apr 2025 20:14:01 +0900</pubDate>
    </item>
    <item>
      <title>Nginx - Nginx Proxy Manager + Cloudflare ssl 적용</title>
      <link>https://dev-kimchi.tistory.com/entry/Nginx-Nginx-Proxy-Manager-Cloudflare-ssl-%EC%A0%81%EC%9A%A9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cloudflare1.jpeg&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5m1m7/btsNgEphHaj/P3Fkokuk4pzSTyJqvcq8nK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5m1m7/btsNgEphHaj/P3Fkokuk4pzSTyJqvcq8nK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5m1m7/btsNgEphHaj/P3Fkokuk4pzSTyJqvcq8nK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5m1m7%2FbtsNgEphHaj%2FP3Fkokuk4pzSTyJqvcq8nK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;436&quot; height=&quot;245&quot; data-filename=&quot;cloudflare1.jpeg&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&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;가난한 개발자는 무료 서버를 사용할 수 밖에 없고 8,000km나 떨어진 서버를 사용하는 사이트에 접속하면 속도는 끔찍한 경우가 대부분이다.&lt;br /&gt;특히나 메인페이지에 접속할때마다 자체 이미지 호스팅 서버에서 수많은 이미지를 불러오고, db 서버에서 복잡한 조회 쿼리를 실행하는 내 사이트의 속도는 최악 중의 최악에 치달았다.&lt;br /&gt;이를 해결하기 위해 방법을 찾아보다 CDN 서비스라는 것을 알게 되었고, 그 중에 전세계 1등 CDN 서비스 회사인 Cloudflare를 사용하게 되었다.&lt;br /&gt;DNS 설정을 하고, 처음보는 서비스가 신기해 이것저것 설정을 해본 후 속도가 얼마나 빨라졌나 기대를 하며 내 사이트에 접속을 하는데! 이게 뭐람? 정상적이던 내 사이트가 갑자기 접속이 안 되기 시작했다.&lt;br /&gt;&lt;code&gt;ERR_SSL_PROTOCOL_ERROR&lt;/code&gt;라는 유언을 남긴 내 서버를 살리기 위해 방법을 찾아본 결과 기존 NPM에서 설정한 ssl 인증서와 cloudflare의 ssl 설정에서 충돌이 발생한 것 같았다.&lt;br /&gt;이를 해결하기 위해 NPM에서 cloudflare의 인증서를 사용하게끔 수정해보았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;cloudflare api 토큰 세팅&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.43.49.png&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ndvov/btsNfrKh4iG/AK1So6gXbHBryAaZMK5rK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ndvov/btsNfrKh4iG/AK1So6gXbHBryAaZMK5rK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ndvov/btsNfrKh4iG/AK1So6gXbHBryAaZMK5rK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fndvov%2FbtsNfrKh4iG%2FAK1So6gXbHBryAaZMK5rK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;302&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.43.49.png&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 개요 탭에 접속하여 하단에 있는API 토큰 가져오기 페이지로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.04.png&quot; data-origin-width=&quot;1657&quot; data-origin-height=&quot;817&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxxvlh/btsNf8YB2ox/SsUPb12JPt7Y2ILbA3oafK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxxvlh/btsNf8YB2ox/SsUPb12JPt7Y2ILbA3oafK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxxvlh/btsNf8YB2ox/SsUPb12JPt7Y2ILbA3oafK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcxxvlh%2FbtsNf8YB2ox%2FSsUPb12JPt7Y2ILbA3oafK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;327&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.04.png&quot; data-origin-width=&quot;1657&quot; data-origin-height=&quot;817&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 토큰 생성 페이지로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.09.png&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;779&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYrQlk/btsNhqqbPy0/kB1LkWDEkLskfONaXuse40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYrQlk/btsNhqqbPy0/kB1LkWDEkLskfONaXuse40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYrQlk/btsNhqqbPy0/kB1LkWDEkLskfONaXuse40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYrQlk%2FbtsNhqqbPy0%2FkB1LkWDEkLskfONaXuse40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;343&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.09.png&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;779&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 영역 DNS 편집 템플릿을 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.23.png&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oEthq/btsNh6xLjSH/Cb6bYYTMsN4mFFGSYONX10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oEthq/btsNh6xLjSH/Cb6bYYTMsN4mFFGSYONX10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oEthq/btsNh6xLjSH/Cb6bYYTMsN4mFFGSYONX10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoEthq%2FbtsNh6xLjSH%2FCb6bYYTMsN4mFFGSYONX10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;335&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.23.png&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 영역 리소스를 모든 영역으로 설정한 후 요약 페이지로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.32.png&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi93uc/btsNh1JPSRf/AgYVM24SDWfMX5oMUZPKSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi93uc/btsNh1JPSRf/AgYVM24SDWfMX5oMUZPKSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi93uc/btsNh1JPSRf/AgYVM24SDWfMX5oMUZPKSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi93uc%2FbtsNh1JPSRf%2FAgYVM24SDWfMX5oMUZPKSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;588&quot; height=&quot;275&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.32.png&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 토큰 생성 버튼을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.38.png&quot; data-origin-width=&quot;1383&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sChkU/btsNhlW0Ww4/WiJQFMrXBSmsPOMEchKUWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sChkU/btsNhlW0Ww4/WiJQFMrXBSmsPOMEchKUWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sChkU/btsNhlW0Ww4/WiJQFMrXBSmsPOMEchKUWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsChkU%2FbtsNhlW0Ww4%2FWiJQFMrXBSmsPOMEchKUWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;325&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.44.38.png&quot; data-origin-width=&quot;1383&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 생성한 토큰을 복사해둔다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NPM 세팅&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.45.10.png&quot; data-origin-width=&quot;1589&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zs8Sr/btsNgEW0LnB/sW2T3KlXvWfEJSJIL0vKEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zs8Sr/btsNgEW0LnB/sW2T3KlXvWfEJSJIL0vKEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zs8Sr/btsNgEW0LnB/sW2T3KlXvWfEJSJIL0vKEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZs8Sr%2FbtsNgEW0LnB%2FsW2T3KlXvWfEJSJIL0vKEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;124&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.45.10.png&quot; data-origin-width=&quot;1589&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. NPM의 SSL Certificates 탭의 Add SSL Certificate 버튼을 클릭한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.45.35.png&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAqCRT/btsNh6drJ87/YHeslJOFWLB2DOKuO8GbL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAqCRT/btsNh6drJ87/YHeslJOFWLB2DOKuO8GbL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAqCRT/btsNh6drJ87/YHeslJOFWLB2DOKuO8GbL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAqCRT%2FbtsNh6drJ87%2FYHeslJOFWLB2DOKuO8GbL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;353&quot; height=&quot;624&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.45.35.png&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;873&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인증서 정보를 입력한 후 저장한다(저장 시 약간의 시간이 소요됨).
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;범용 도메인명 입력&lt;/li&gt;
&lt;li&gt;DNS Challenge 설정
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DNS Provider - Cloudflare로 설정&lt;/li&gt;
&lt;li&gt;Credentials File Content에 api_token 부분을 복사한 토큰으로 수정&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;약관 동의&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.45.58.png&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c34UXc/btsNhMGwPPA/ixrFJDSuQsyULe5kyf8OH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c34UXc/btsNhMGwPPA/ixrFJDSuQsyULe5kyf8OH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c34UXc/btsNhMGwPPA/ixrFJDSuQsyULe5kyf8OH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc34UXc%2FbtsNhMGwPPA%2FixrFJDSuQsyULe5kyf8OH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;376&quot; height=&quot;293&quot; data-filename=&quot;스크린샷 2025-04-11 오전 11.45.58.png&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 기존 proxy host를 수정하거나 새 proxy host를 추가할때 ssl 탭에서 SSL Certificate를 저장한 인증서로 저장해준다.&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;하지만 헤매는 시간동안 SSL과 DNS에 대해 조금 더 이해를 하게 되었으니 럭키비키다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>CDN</category>
      <category>cloudflare</category>
      <category>dns</category>
      <category>Manager</category>
      <category>nginx</category>
      <category>NPM</category>
      <category>Proxy</category>
      <category>SSL</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/273</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Nginx-Nginx-Proxy-Manager-Cloudflare-ssl-%EC%A0%81%EC%9A%A9#entry273comment</comments>
      <pubDate>Fri, 11 Apr 2025 12:49:18 +0900</pubDate>
    </item>
    <item>
      <title>Go - 이미지 리사이즈 기능(/h2non/bimg)</title>
      <link>https://dev-kimchi.tistory.com/entry/Go-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A6%88-%EA%B8%B0%EB%8A%A5h2nonbimg</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xDUmP/btsMZBFvxFV/NUHRsNgx2DXmdBlf2rTKNk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xDUmP/btsMZBFvxFV/NUHRsNgx2DXmdBlf2rTKNk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xDUmP/btsMZBFvxFV/NUHRsNgx2DXmdBlf2rTKNk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxDUmP%2FbtsMZBFvxFV%2FNUHRsNgx2DXmdBlf2rTKNk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;340&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;819&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;Go로 이미지 호스팅 서버를 개발하던 중, 이미지 리사이즈 기능을 구현하는 과정에서 고민했던 점들을 정리해보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;go 주요 이미지 리사이즈 라이브러리 비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;라이브러리&lt;/th&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;bimg&lt;/td&gt;
&lt;td&gt;- libvips 기반으로 매우 빠른 처리 속도   - 메모리 사용량이 적음   - 다양한 이미지 포맷 지원&lt;/td&gt;
&lt;td&gt;- C 바인딩으로 인한 설치 복잡도 증가   - libvips 의존성 필요   - Windows 환경에서 설정이 까다로움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imaging&lt;/td&gt;
&lt;td&gt;- 순수 Go로 작성되어 설치가 간단   - 크로스 플랫폼 지원이 용이   - API가 직관적이고 사용하기 쉬움&lt;/td&gt;
&lt;td&gt;- 처리 속도가 상대적으로 느림   - 메모리 사용량이 많음   - 일부 고급 이미지 처리 기능 부재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nfnt/resize&lt;/td&gt;
&lt;td&gt;- 가볍고 단순한 구현   - 순수 Go로 작성되어 의존성이 없음   - 기본적인 리사이즈 기능 제공&lt;/td&gt;
&lt;td&gt;- 성능이 상대적으로 낮음   - 고급 이미지 처리 기능 부재   - 메모리 사용량이 많음   - 지원이 종료됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imgproxy&lt;/td&gt;
&lt;td&gt;- 다양한 이미지 처리 옵션 제공   - 캐싱 기능 내장   - 고성능 처리&lt;/td&gt;
&lt;td&gt;- 별도의 서비스로 실행 필요   - 설정이 복잡함   - 리소스 사용량이 많음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;라이브러리 선택 과정&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;1. 초기 고려사항
   - 순수 Go로 작성된 라이브러리 선호 (설치 간편성)
   - 기본적인 리사이즈 기능만 필요

2. 시도한 라이브러리들
   - `github.com/nfnt/resize`: 순수 Go로 작성되어 설치가 간단했으나, WebP 미지원
   - `github.com/disintegration/imaging`: 순수 Go로 작성되어 설치가 간단했으나, WebP 지원이 제한적

3. bimg 선택 이유
   - libvips 기반으로 매우 빠른 처리 속도
   - WebP를 포함한 다양한 이미지 포맷 지원
   - 메모리 효율적인 처리
   - Docker 환경에서 쉽게 설정 가능
   - 활발한 커뮤니티와 안정적인 업데이트

4. 선택 후 고려사항
   - libvips 설치 필요성
   - Docker 환경에서의 설정&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;webp 확장자를 지원하는 라이브러리 중 비교적 간단하게 관리할 수 있는 bimg를 선택하게 되었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;코드를 작성하기 전에!&lt;/h2&gt;
&lt;p&gt;go 구동 환경에 libvips 설치가 필요하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS: Homebrew를 통한 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install vips&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu/Debian: apt를 통한 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get update
sudo apt-get install -y libvips-dev&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Windows: MSYS2를 통한 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pacman -S mingw-w64-x86_64-libvips&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;설치 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# libvips 버전 확인
vips --version&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;p&gt;Go의 &lt;code&gt;github.com/h2non/bimg&lt;/code&gt; 라이브러리를 사용하여 이미지 리사이즈 기능을 구현했다.&lt;br&gt;이 라이브러리는 libvips를 기반으로 하여 빠른 이미지 처리가 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;log&amp;quot;
    &amp;quot;strconv&amp;quot;
    &amp;quot;strings&amp;quot;

    &amp;quot;github.com/h2non/bimg&amp;quot;
)

/*
이미지를 주어진 크기로 리사이징

Parameters:
  - size: &amp;quot;width x height&amp;quot; 형식의 문자열 (예: &amp;quot;100x100&amp;quot;)
  - filePath: 리사이징할 이미지 파일 경로

Returns:
  - []byte: 리사이징된 이미지 바이트
  - error: 에러 발생 시 에러 객체 반환
*/
func ResizeImage(size string, filePath string) ([]byte, error) {
    // 이미지 파일 확장자 체크
    if !strings.HasSuffix(strings.ToLower(filePath), &amp;quot;.jpg&amp;quot;) &amp;amp;&amp;amp;
        !strings.HasSuffix(strings.ToLower(filePath), &amp;quot;.jpeg&amp;quot;) &amp;amp;&amp;amp;
        !strings.HasSuffix(strings.ToLower(filePath), &amp;quot;.png&amp;quot;) &amp;amp;&amp;amp;
        !strings.HasSuffix(strings.ToLower(filePath), &amp;quot;.webp&amp;quot;) {
        return nil, fmt.Errorf(&amp;quot;지원하지 않는 이미지 형식입니다&amp;quot;)
    }

    // size 파라미터 파싱
    sizes := strings.Split(size, &amp;quot;x&amp;quot;)
    if len(sizes) != 2 {
        return nil, fmt.Errorf(&amp;quot;잘못된 크기 형식입니다. (예: 100x100)&amp;quot;)
    }

    width, err := strconv.Atoi(sizes[0])
    if err != nil {
        return nil, fmt.Errorf(&amp;quot;잘못된 너비 값입니다&amp;quot;)
    }

    height, err := strconv.Atoi(sizes[1])
    if err != nil {
        return nil, fmt.Errorf(&amp;quot;잘못된 높이 값입니다&amp;quot;)
    }

    // 이미지 읽기
    buffer, err := bimg.Read(filePath)
    if err != nil {
        return nil, fmt.Errorf(&amp;quot;이미지 처리 중 오류가 발생했습니다&amp;quot;)
    }

    // 리사이즈 수행
    newImage, err := bimg.NewImage(buffer).Resize(width, height)
    if err != nil {
        return nil, fmt.Errorf(&amp;quot;이미지 처리 중 오류가 발생했습니다&amp;quot;)
    }

    return newImage, nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;라이브러리를 사용하여 이미지 리사이즈 기능 자체를 구현하는 것은 어렵지 않았으나 webp 확장자를 지원하는 라이브러리를 찾느라 조금 헤매게 되었다.&lt;br&gt;그래도 정리하고 나니 뿌듯하다.&lt;/p&gt;</description>
      <category>Go</category>
      <category>bimn</category>
      <category>go</category>
      <category>image</category>
      <category>Imaging</category>
      <category>img</category>
      <category>imgproxy</category>
      <category>libvips</category>
      <category>nfnt</category>
      <category>resize</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/272</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Go-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A6%88-%EA%B8%B0%EB%8A%A5h2nonbimg#entry272comment</comments>
      <pubDate>Fri, 28 Mar 2025 11:47:20 +0900</pubDate>
    </item>
    <item>
      <title>PHP - 클로저(Closure, 익명 함수)</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-%ED%81%B4%EB%A1%9C%EC%A0%80Closure%EC%9D%B5%EB%AA%85-%ED%95%A8%EC%88%98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRJ1MZ/btsMTZzbrSX/qkygcuCNLwRxYYhJtasg8k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRJ1MZ/btsMTZzbrSX/qkygcuCNLwRxYYhJtasg8k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRJ1MZ/btsMTZzbrSX/qkygcuCNLwRxYYhJtasg8k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRJ1MZ%2FbtsMTZzbrSX%2FqkygcuCNLwRxYYhJtasg8k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;406&quot; height=&quot;565&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&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;code&gt;.&lt;/code&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;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;// 문자열에 변수 추가
$exam_url = fn($id) =&amp;gt; &quot;https://example.com/user/$id&quot;;
echo $exam_url(123); // &quot;https://example.com/user/123&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;// 문자열에 변수 여러 개 추가
$profile = fn($name, $age) =&amp;gt; &quot;이름: $name, 나이: $age&quot;;
echo $profile(&quot;aleph&quot;, 26); // &quot;이름: aleph, 나이: 26&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;// 문자열에 삼항연산자로 변수 추가
$status = fn($point) =&amp;gt; &quot;상태: &quot; . ($point &amp;gt; 50 ? &quot;좋음&quot; : &quot;나쁨&quot;);
echo $status(70); // &quot;상태: 좋음&quot;
echo $status(30); // &quot;상태: 나쁨&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;// 배열에 사용
$numbers = [1, 2, 3, 4];
$double = array_map(fn($num) =&amp;gt; &quot;숫자: $num&quot;, $numbers);
print_r($double); // [&quot;숫자: 1&quot;, &quot;숫자: 2&quot;, &quot;숫자: 3&quot;, &quot;숫자: 4&quot;]&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;남발하면 가독성이 멸망하겠지만 간단히 url에 변수 넣는 용도쯤은 나쁘지 않은거 같다.&lt;/p&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: 378px; top: 1183.98px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>PHP</category>
      <category>closure</category>
      <category>php</category>
      <category>익명</category>
      <category>클로저</category>
      <category>함수</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/271</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-%ED%81%B4%EB%A1%9C%EC%A0%80Closure%EC%9D%B5%EB%AA%85-%ED%95%A8%EC%88%98#entry271comment</comments>
      <pubDate>Fri, 21 Mar 2025 20:55:08 +0900</pubDate>
    </item>
    <item>
      <title>PHP - html 태그 추출기</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-html-%ED%83%9C%EA%B7%B8-%EC%B6%94%EC%B6%9C%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A0euf/btsMNggLa3t/lZlkKrkSvfEM0GHxKf9yPK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A0euf/btsMNggLa3t/lZlkKrkSvfEM0GHxKf9yPK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A0euf/btsMNggLa3t/lZlkKrkSvfEM0GHxKf9yPK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA0euf%2FbtsMNggLa3t%2FlZlkKrkSvfEM0GHxKf9yPK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;553&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;1162&quot;/&gt;&lt;/span&gt;&lt;/figure&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;웹개발을 할때 svg 아이콘이 필요하면 유튜브에서 사용하는 svg 태그를 가져다 사용하고 있는데 유튜브에 존재하는 모든 svg를 미리 분류해두고 싶어 php를 사용해 유튜브 페이지의 전체 html에서 svg 태그만 추출하기 위해 php로 태그 추출기를 만들어보았다.&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;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;&amp;lt;?php

// 추출할 태그명을 변수로 정의
$tagName = 'div'; // 원하는 태그명으로 변경 (예: 'div', 'p' 등)

// HTML 파일 읽기
$html = file_get_contents('input.html');

// 동적 태그명으로 패턴 생성
$pattern = &quot;/&amp;lt;{$tagName}.*?&amp;gt;(.*?)&amp;lt;\/{$tagName}&amp;gt;/is&quot;;
preg_match_all($pattern, $html, $matches);

// 결과 처리
$output = implode(&quot;\n&quot;, $matches[0]);

// 파일로 저장
file_put_contents('output.html', $output);

echo &quot;태그 추출이 완료되었습니다.&quot;;&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;h3 data-ke-size=&quot;size23&quot;&gt;input.html&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;

&amp;lt;body&amp;gt;
  &amp;lt;div class=&quot;div1&quot;&amp;gt;
    &amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;div2&quot;&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;
  &amp;lt;div class=&quot;div3&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 html을 php 파일과 같은 경로에 input.html로 저장하고 php 파일의 $tagName 변수를 추출하고 싶은 태그(예시에서는 div)로 작성한 뒤 터미널에 &lt;code&gt;php index.php&lt;/code&gt; 명령어를 사용하면 &lt;code&gt;태그 추출이 완료되었습니다.&lt;/code&gt; 라는 메시지와 함께 아래의 내용이 담긴 output.html 파일이 저장된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;output.html&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;div1&quot;&amp;gt;
    &amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;div2&quot;&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;div3&quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>PHP/PHP</category>
      <category>HTML</category>
      <category>php</category>
      <category>tag</category>
      <category>추출</category>
      <category>태그</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/270</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-html-%ED%83%9C%EA%B7%B8-%EC%B6%94%EC%B6%9C%EA%B8%B0#entry270comment</comments>
      <pubDate>Mon, 17 Mar 2025 09:57:29 +0900</pubDate>
    </item>
    <item>
      <title>HTML, CSS - 별점 선택(Star Rating)</title>
      <link>https://dev-kimchi.tistory.com/entry/HTML-CSS-%EB%B3%84%EC%A0%90-%EC%84%A0%ED%83%9DStar-Rating</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VAKDg/btsMIF25bnr/1skd2ktwWybaLdq9ywfVUk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VAKDg/btsMIF25bnr/1skd2ktwWybaLdq9ywfVUk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VAKDg/btsMIF25bnr/1skd2ktwWybaLdq9ywfVUk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVAKDg%2FbtsMIF25bnr%2F1skd2ktwWybaLdq9ywfVUk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;356&quot; height=&quot;597&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

  
  &lt;iframe height=&quot;300&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Star Rating&quot; src=&quot;https://codepen.io/aleph-kim/embed/zxYPKVv?default-tab=html%2Cresult&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &lt;a href=&quot;https://codepen.io/aleph-kim/pen/zxYPKVv&quot;&gt;
  Star Rating&lt;/a&gt; by 김채민 (&lt;a href=&quot;https://codepen.io/aleph-kim&quot;&gt;@aleph-kim&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.
&lt;/iframe&gt;</description>
      <category>HTML, CSS</category>
      <category>CSS</category>
      <category>HTML</category>
      <category>rating</category>
      <category>star</category>
      <category>별점</category>
      <category>선택</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/269</guid>
      <comments>https://dev-kimchi.tistory.com/entry/HTML-CSS-%EB%B3%84%EC%A0%90-%EC%84%A0%ED%83%9DStar-Rating#entry269comment</comments>
      <pubDate>Thu, 13 Mar 2025 12:51:45 +0900</pubDate>
    </item>
    <item>
      <title>PHP - Docker-Compose 환경 PHP 프로젝트 빌드 시 Composer 설치</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-Docker-Compose-%ED%99%98%EA%B2%BD-PHP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B9%8C%EB%93%9C-%EC%8B%9C-Composer-%EC%84%A4%EC%B9%98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;php3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9folu/btsMGqqNg3I/40fkz78cnvPiky5FZm4tsK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9folu/btsMGqqNg3I/40fkz78cnvPiky5FZm4tsK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9folu/btsMGqqNg3I/40fkz78cnvPiky5FZm4tsK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9folu%2FbtsMGqqNg3I%2F40fkz78cnvPiky5FZm4tsK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;407&quot; height=&quot;496&quot; data-filename=&quot;php3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1756&quot;/&gt;&lt;/span&gt;&lt;/figure&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;php, docker compose로 프로젝트를 진행하면서 composer를 설치하려는데 뭐 컨테이너를 따로 만들고 어쩌구 볼륨을 설정하고 어쩌구 이상하리만치 복잡하길래 간단한 방법을 만들어보았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;docker-compose.yml&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 compose 파일로 composer를 쓰나 안 쓰나 차이는 없다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  web:
    build: .
    ports:
      - &quot;8080:80&quot;
    volumes:
      - .:/var/www/html&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dockerfile&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile에서 직접 composer를 설치하고, 라이브러리도 설치한다.&lt;/p&gt;
&lt;pre class=&quot;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;# PHP 7.4와 Apache 이미지 사용
FROM php:7.4-apache

# Composer 설치에 필요한 패키지 설치 (curl, unzip, git)
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    curl \
    unzip \
    git

# Composer 설치
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# 작업 디렉토리 설정
WORKDIR /var/www/html

# 애플리케이션 파일 복사
COPY . /var/www/html/

# composer 라이브러리 설치 &amp;amp;&amp;amp; Apache 서비스 시작
CMD bash -c &quot;composer install --no-interaction --prefer-dist &amp;amp;&amp;amp; apache2-foreground&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;index.php&lt;/h3&gt;
&lt;pre class=&quot;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;&amp;lt;?
require 'vendor/autoload.php'; // composer 라이브러리 autoload

use Carbon\Carbon; // 테스트용 라이브러리

$now = Carbon::now('Asia/Seoul'); // 현재 시간 출력
echo $now-&amp;gt;toDateTimeString();&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;composer.json&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
    &quot;require&quot;: {
        &quot;nesbot/carbon&quot;: &quot;^2.7.3&quot; // 테스트용으로 사용할 php datetime 라이브러리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile에서 북치고 장구치고 다 하게끔 만들어보았다. 이렇게 하면 컨테이너를 추가로 만들 필요도 없고 원래 하던대로 &lt;code&gt;docker compose up&lt;/code&gt; 명령어 딸깍으로 composer를 사용할 수 있다.&lt;/p&gt;</description>
      <category>PHP/PHP</category>
      <category>compose</category>
      <category>Composer</category>
      <category>docker</category>
      <category>lib</category>
      <category>php</category>
      <category>vendor</category>
      <category>컴포저</category>
      <category>컴포즈</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/268</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-Docker-Compose-%ED%99%98%EA%B2%BD-PHP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B9%8C%EB%93%9C-%EC%8B%9C-Composer-%EC%84%A4%EC%B9%98#entry268comment</comments>
      <pubDate>Tue, 11 Mar 2025 11:40:35 +0900</pubDate>
    </item>
    <item>
      <title>Go - 파일 호스팅 서버 프로젝트 기술 설명서</title>
      <link>https://dev-kimchi.tistory.com/entry/Go-%ED%8C%8C%EC%9D%BC-%ED%98%B8%EC%8A%A4%ED%8C%85-%EC%84%9C%EB%B2%84-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EC%88%A0-%EC%84%A4%EB%AA%85%EC%84%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img-host-server.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfPG2Q/btsMF3BFSJi/si5PHZz61lovSwMY8m1l60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfPG2Q/btsMF3BFSJi/si5PHZz61lovSwMY8m1l60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfPG2Q/btsMF3BFSJi/si5PHZz61lovSwMY8m1l60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfPG2Q%2FbtsMF3BFSJi%2Fsi5PHZz61lovSwMY8m1l60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;193&quot; data-filename=&quot;img-host-server.png&quot; data-origin-width=&quot;468&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이 프로젝트는 &lt;b&gt;파일 호스팅&lt;/b&gt;를 위한 &lt;b&gt;RESTful API 서버&lt;/b&gt;입니다. Go 언어로 작성되었으며, Docker를 이용해 배포할 수 있도록 구성되었습니다.   서버는 사용자가 인증된 상태에서 파일을 업로드하고 관리할 수 있도록 기능을 제공합니다.&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서버 url: &lt;a href=&quot;https://img.aleph.kr/files/admin/readme.md&quot;&gt;https://img.aleph.kr/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;api 명세서 url:&lt;span&gt; &lt;a href=&quot;https://github.com/Aleph-Kim/img-host-server/wiki&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Aleph-Kim/img-host-server/wiki&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;깃허브&lt;/b&gt;: &lt;a href=&quot;https://github.com/Aleph-Kim/img-host-server&quot;&gt;https://github.com/Aleph-Kim/img-host-server&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;zwj;  주요 기능  &amp;zwj; &lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 업로드&lt;/b&gt;: 인증된 사용자만 파일을 업로드할 수 있습니다. 파일은 사용자별로 구분된 폴더에 저장됩니다.  &lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 수정&lt;/b&gt;: 인증된 사용자는 자신이 업로드한 파일을 수정할 수 있습니다. (파일명 및 내용 수정 가능) ✏️&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 다운로드&lt;/b&gt;: 모든 사용자는 인증 없이 모든 파일을 다운로드할 수 있습니다. ⬇️&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 삭제&lt;/b&gt;: 인증된 사용자는 자신이 업로드한 파일을 삭제할 수 있습니다.  ️&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 관리&lt;/b&gt;: 관리자는 새로운 사용자를 등록할 수 있으며, 각 사용자에게 자동으로 고유한 비밀번호를 생성하여 해시값을 저장합니다.  &lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ 기술 스택  ️&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Go&lt;/b&gt;: 서버 사이드 로직 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker&lt;/b&gt;: 애플리케이션 컨테이너화 (개발 및 프로덕션 환경 지원)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Gorilla Mux&lt;/b&gt;: HTTP 라우팅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bcrypt&lt;/b&gt;: 안전한 비밀번호 해싱&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JSON&lt;/b&gt;: 데이터 저장 및 응답 형식으로 사용&lt;/li&gt;
&lt;/ul&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;ada&quot;&gt;&lt;code&gt;  img-host-server
│
├── /cmd/server/main.go          : 서버 초기화 및 라우팅 설정
│
├── /internal
│   ├── handles
│   │   ├── file_handler.go      : 파일 업로드, 수정, 삭제, 다운로드 처리
│   │   └── user_handler.go      : 사용자 등록 및 관리 처리
│   ├── utils
│   │   ├── auth.go              : 사용자 인증 관련 함수 (비밀번호 검증, 사용자 정보 로딩)
│   │   ├── response.go          : JSON 형식의 응답을 처리하는 유틸리티
│   │   ├── file.go              : 파일 업로드 및 저장 관련 함수
│   │   └── sanitize.go          : 파일명 유효성 검사
│   └── db
│       └── users.json           : 사용자 정보 저장
│
├── .air.toml                    : 개발 환경 hotswap 라이브러리 설정 파일(air 라이브러리)
├── .env                         : 환경변수 관리(관리자 비밀번호)
├── /docker-compose.yml          : Docker 설정 파일
├── /Dockerfile.prod             : 프로덕션 환경을 위한 Dockerfile
├── /Dockerfile.dev              : 개발 환경을 위한 Dockerfile
├── /go.mod                      : Go 모듈 설정 파일
└── /go.sum                      : Go 의존성 정보 파일&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  API 엔드포인트  &lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;POST &lt;code&gt;/files&lt;/code&gt;&lt;/b&gt;: 파일 업로드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PUT &lt;code&gt;/files/{filename}&lt;/code&gt;&lt;/b&gt;: 파일 수정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET &lt;code&gt;/files/{username}/{filename}&lt;/code&gt;&lt;/b&gt;: 파일 다운로드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DELETE &lt;code&gt;/files/{filename}&lt;/code&gt;&lt;/b&gt;: 파일 삭제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;POST &lt;code&gt;/users&lt;/code&gt;&lt;/b&gt;: 사용자 등록&lt;/li&gt;
&lt;/ul&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;GitHub에서 이 프로젝트를 &lt;b&gt;Clone&lt;/b&gt;한 후, 로컬에서 실행하는 방법은 다음과 같습니다.  &lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1️⃣ &lt;b&gt;GitHub에서 프로젝트 Clone&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone https://github.com/Aleph-Kim/img-host-server
cd img-host-server&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2️⃣ &lt;b&gt;관리자 비밀번호 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env&lt;/code&gt; 파일을 사용하여 관리자 비밀번호를 설정할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3️⃣ &lt;b&gt;Docker 환경 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 이용하여 애플리케이션을 실행할 수 있습니다. &lt;code&gt;docker-compose.yml&lt;/code&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;b&gt;개발 환경 실행&lt;/b&gt;:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose up dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로덕션 환경 실행&lt;/b&gt;:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose up prod&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4️⃣ &lt;b&gt;서버 실행&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 기본적으로 &lt;code&gt;3000&lt;/code&gt; 포트에서 실행됩니다. 서버가 성공적으로 실행되면, 브라우저나 Postman 등을 통해 API를 테스트할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Docker 구성 설명  &lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Dockerfile.prod&lt;/code&gt;: 프로덕션 환경용 Dockerfile로, 실제 배포용 설정이 포함되어 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dockerfile.dev&lt;/code&gt;: 개발 환경용 Dockerfile로, 소스 코드 변경 시 자동 반영되도록 설정되어 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;: 개발 및 프로덕션 환경을 동시에 지원하는 설정 파일로, 필요한 서비스를 자동으로 구성해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  주의 사항  &lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 파일에 관리자의 비밀번호가 설정되어야 하며, 관리자 인증을 통해 사용자를 등록할 수 있습니다.&lt;/li&gt;
&lt;li&gt;파일 업로드/수정/삭제 시 사용자 인증을 위해 &lt;b&gt;X-Username&lt;/b&gt;과 &lt;b&gt;X-Secret&lt;/b&gt; 헤더를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 파일 업로드와 관리, 사용자 인증을 간편하고 안전하게 처리할 수 있도록 설계되었습니다. 추가적인 기능이 필요하면 언제든지 요구 사항을 반영할 수 있습니다.  &lt;/p&gt;</description>
      <category>Go</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/267</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Go-%ED%8C%8C%EC%9D%BC-%ED%98%B8%EC%8A%A4%ED%8C%85-%EC%84%9C%EB%B2%84-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EC%88%A0-%EC%84%A4%EB%AA%85%EC%84%9C#entry267comment</comments>
      <pubDate>Sun, 9 Mar 2025 23:21:17 +0900</pubDate>
    </item>
    <item>
      <title>Docker - sudo 없이 docker 명령어 사용하기</title>
      <link>https://dev-kimchi.tistory.com/entry/Docker-sudo-%EC%97%86%EC%9D%B4-docker-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7Vi3Y/btsMGhl9gZl/xx71FNGjLhYQ6sJlBEFmu1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7Vi3Y/btsMGhl9gZl/xx71FNGjLhYQ6sJlBEFmu1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7Vi3Y/btsMGhl9gZl/xx71FNGjLhYQ6sJlBEFmu1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7Vi3Y%2FbtsMGhl9gZl%2Fxx71FNGjLhYQ6sJlBEFmu1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;446&quot; height=&quot;793&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;현재 인스턴스 서버에서 docker 명령어를 실행할 때마다 sudo를 붙여야 하는 불편함이 있어 이를 해결하기 위해 방법을 찾아보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 현재 사용자 Docker 그룹에 추가하기&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 현재 사용자를 docker 그룹에 추가하는 명령어
sudo usermod -aG docker $USER&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;2. 세션 재시작 또는 로그아웃 후 다시 로그인&lt;/h2&gt;
&lt;p&gt;그룹 변경 사항이 적용되기 위해 로그아웃 후 다시 로그인하거나 터미널을 재시작&lt;/p&gt;
&lt;h2&gt;3. 권한 확인&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 현재 사용자의 그룹 리스트 출력
groups&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;p&gt;출력 결과에 docker 그룹이 포함되어 있다면 성공적으로 추가된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iao2s/btsMEzodeKs/FkfL3uChRwzhPODxbK3Eck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iao2s/btsMEzodeKs/FkfL3uChRwzhPODxbK3Eck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iao2s/btsMEzodeKs/FkfL3uChRwzhPODxbK3Eck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIao2s%2FbtsMEzodeKs%2FFkfL3uChRwzhPODxbK3Eck%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Docker</category>
      <category>compose</category>
      <category>docker</category>
      <category>groups</category>
      <category>sudo</category>
      <category>usermod</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/266</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Docker-sudo-%EC%97%86%EC%9D%B4-docker-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0#entry266comment</comments>
      <pubDate>Sun, 9 Mar 2025 12:48:13 +0900</pubDate>
    </item>
    <item>
      <title>Docker - Debian 서버에 docker, docker compose 설치하기</title>
      <link>https://dev-kimchi.tistory.com/entry/Docker-Debian-%EC%84%9C%EB%B2%84%EC%97%90-docker-docker-compose-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1823&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgW8RY/btsMEKiRWjD/AGpQLCvOkJ9uL4MRCGB2k0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgW8RY/btsMEKiRWjD/AGpQLCvOkJ9uL4MRCGB2k0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgW8RY/btsMEKiRWjD/AGpQLCvOkJ9uL4MRCGB2k0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgW8RY%2FbtsMEKiRWjD%2FAGpQLCvOkJ9uL4MRCGB2k0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;411&quot; height=&quot;937&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1823&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Docker 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# 필수 패키지 설치
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Docker GPG key 추가
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Docker 저장소 추가
echo \
  &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \
  $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Docker 설치
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Docker 서비스 시작 및 자동 시작 설정
sudo systemctl start docker
sudo systemctl enable docker&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# 설치 확인
docker -v&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Docker Compose 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Docker Compose 다운로드 및 설치
sudo curl -L &amp;quot;https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep -Po &amp;#39;&amp;quot;tag_name&amp;quot;: &amp;quot;\K.*?(?=&amp;quot;)&amp;#39;)/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# 실행 권한 추가
sudo chmod +x /usr/local/bin/docker-compose

# 설치 확인
docker-compose --version&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Docker</category>
      <category>compose</category>
      <category>Debian</category>
      <category>docker</category>
      <category>install</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/265</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Docker-Debian-%EC%84%9C%EB%B2%84%EC%97%90-docker-docker-compose-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0#entry265comment</comments>
      <pubDate>Sun, 9 Mar 2025 12:36:23 +0900</pubDate>
    </item>
    <item>
      <title>Go - error strings should not end with punctuation or newlines (ST1005)</title>
      <link>https://dev-kimchi.tistory.com/entry/Go-error-strings-should-not-end-with-punctuation-or-newlines-ST1005</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;go1.jpg&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JZJwO/btsMEkSm3rx/Amg9q8QqVbMXQi92esahyK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JZJwO/btsMEkSm3rx/Amg9q8QqVbMXQi92esahyK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JZJwO/btsMEkSm3rx/Amg9q8QqVbMXQi92esahyK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJZJwO%2FbtsMEkSm3rx%2FAmg9q8QqVbMXQi92esahyK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;524&quot; data-filename=&quot;go1.jpg&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;Go로 새로운 프로젝트를 진행하던 중 vscode에서 &lt;code&gt;errors.New(&amp;quot;에러가 발생했습니다.&amp;quot;)&lt;/code&gt; 부분에 노란색 밑줄이 생겼다.&lt;br&gt;그냥 냅둬도 서버 굴리는 데에 아무런 영향 없이 잘 돌아가지만 저 노란색 밑줄을 용납할 수 없어 조금 찾아보았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;원인&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;오류 메시지의 문자열 첫 번째 글자가 대문자인 경우&lt;/li&gt;
&lt;li&gt;오류 메시지의 문자열 끝에 마침표, 느낌표, 물음표 등의 구두점이나 개행 문자가 포함될 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;해결방법&lt;/h2&gt;
&lt;p&gt;원인에 따라 첫 번째 글자를 소문자로 바꾸거나 끝에 특수문자가 들어가지 않도록 수정하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;하지만&lt;/h2&gt;
&lt;p&gt;하지만 한국인의 에러 메시지가 마침표로 끝나는 것은 일어날 수 있는 일이 아닌가? 나는 &lt;code&gt;에러가 발생했습니다&lt;/code&gt; 라며 마침표 없이 끝나는 에러 메시지를 보고 싶지 않다.&lt;br&gt;고로 노란색 밑줄이 뜨지 않도록 vscode의 설정을 수정하기로 했다.&lt;/p&gt;
&lt;h3&gt;settings.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;go.lintTool&amp;quot;: &amp;quot;staticcheck&amp;quot;,
&amp;quot;go.lintFlags&amp;quot;: [
    &amp;quot;-exclude=ST1005&amp;quot;
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;파일을 저장한 후 vscode를 재시작하면 에러가 사라지게 된다.&lt;/p&gt;</description>
      <category>Go</category>
      <category>error</category>
      <category>go</category>
      <category>golang</category>
      <category>st1005</category>
      <category>vscode</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/264</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Go-error-strings-should-not-end-with-punctuation-or-newlines-ST1005#entry264comment</comments>
      <pubDate>Sun, 9 Mar 2025 11:34:20 +0900</pubDate>
    </item>
    <item>
      <title>Git - 특정 파일의 변경 사항 추적 제어 (올라간 파일 업데이트 안 하는 방법)</title>
      <link>https://dev-kimchi.tistory.com/entry/Git-%ED%8A%B9%EC%A0%95-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EB%B3%80%EA%B2%BD-%EC%82%AC%ED%95%AD-%EC%B6%94%EC%A0%81-%EC%A0%9C%EC%96%B4-%EC%98%AC%EB%9D%BC%EA%B0%84-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%95%88-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;git5.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM7ikT/btsMEOMfG2n/EeDU9oph7yTsJmDmfaAwiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM7ikT/btsMEOMfG2n/EeDU9oph7yTsJmDmfaAwiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM7ikT/btsMEOMfG2n/EeDU9oph7yTsJmDmfaAwiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM7ikT%2FbtsMEOMfG2n%2FEeDU9oph7yTsJmDmfaAwiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;462&quot; height=&quot;384&quot; data-filename=&quot;git5.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&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;설명&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git update-index --skip-worktree [파일명]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;지정한 파일에 대해 skip-worktree 플래그를 설정합니다. 로컬 변경 사항을 Git이 무시하도록 합니다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git update-index --skip-worktree config.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git update-index --no-skip-worktree [파일명]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;설정된 skip-worktree 플래그를 해제하여, 해당 파일의 변경 사항을 다시 추적하도록 합니다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git update-index --no-skip-worktree config.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git ls-files -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 추적 중인 파일들의 상태를 보여줍니다. `H`는 일반 상태, `S`는 skip-worktree가 설정된 파일입니다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git ls-files -v&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>git</category>
      <category>git</category>
      <category>github</category>
      <category>변경사항</category>
      <category>업데이트</category>
      <category>제어</category>
      <category>추적</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/263</guid>
      <comments>https://dev-kimchi.tistory.com/entry/Git-%ED%8A%B9%EC%A0%95-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EB%B3%80%EA%B2%BD-%EC%82%AC%ED%95%AD-%EC%B6%94%EC%A0%81-%EC%A0%9C%EC%96%B4-%EC%98%AC%EB%9D%BC%EA%B0%84-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%95%88-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95#entry263comment</comments>
      <pubDate>Sun, 9 Mar 2025 10:41:06 +0900</pubDate>
    </item>
    <item>
      <title>PHP - 내장 함수만으로 .env 파일 사용하기</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-%EB%82%B4%EC%9E%A5-%ED%95%A8%EC%88%98%EB%A1%9C-env-%ED%8C%8C%EC%9D%BC-%EC%82%AC%EC%9A%A9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;747&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxb00H/btsMD41TIxd/JpUe3AvXtKflC33H8kKKR1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxb00H/btsMD41TIxd/JpUe3AvXtKflC33H8kKKR1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxb00H/btsMD41TIxd/JpUe3AvXtKflC33H8kKKR1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcxb00H%2FbtsMD41TIxd%2FJpUe3AvXtKflC33H8kKKR1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;411&quot; height=&quot;409&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;747&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;p&gt;다른 라이브러리를 사용하지 않고 php 내장 함수만으로 .env 파일을 사용하는 방법에 관한 포스팅이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;코드&lt;/h2&gt;
&lt;h3&gt;loadEnv 함수&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?

&amp;lt;?php

/**
 * 환경 변수(.env) 파일을 로드하여 PHP의 환경 변수로 설정하는 함수
 *
 * @throws Exception .env 파일이 존재하지 않거나 읽을 수 없을 경우 예외를 발생
 * @return bool 환경 변수 로드가 성공하면 true를 반환
 */
function loadEnv() {
    // 루트 디렉토리에 있는 .env 파일 사용(환경에 따라 수정)
    $filePath = &amp;#39;/.env&amp;#39;;

    // 파일이 존재하지 않으면 예외 처리
    if (!file_exists($filePath)) {
        throw new Exception(&amp;quot;환경 변수 파일을 찾을 수 없습니다: $filePath&amp;quot;);
    }

    // .env 파일을 한 번만 로드하도록 캐싱
    static $loaded = false;
    if ($loaded) return true;

    // 파일 읽기
    $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

    foreach ($lines as $line) {
        $line = trim($line);

        // 주석(#) 또는 빈 줄은 무시
        if ($line === &amp;#39;&amp;#39; || strpos($line, &amp;#39;#&amp;#39;) === 0) continue;

        // &amp;#39;=&amp;#39;가 없는 경우 예외 처리
        if (!strpos($line, &amp;#39;=&amp;#39;)) continue;

        list($key, $value) = explode(&amp;#39;=&amp;#39;, $line, 2);
        $key = trim($key);
        $value = trim($value);

        // 환경 변수 설정 (중복 방지)
        if (!array_key_exists($key, $_ENV) &amp;amp;&amp;amp; !array_key_exists($key, $_SERVER)) {
            putenv(&amp;quot;$key=$value&amp;quot;);
            $_ENV[$key] = $value;
            $_SERVER[$key] = $value;
        }
    }

    $loaded = true;
    return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;사용 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?

loadEnv();

// 환경 변수 가져오기
$db_host = getenv(&amp;#39;DB_HOST&amp;#39;);
$db_user = getenv(&amp;#39;DB_USER&amp;#39;);
$db_pass = getenv(&amp;#39;DB_PASS&amp;#39;);

echo &amp;quot;DB Host: $db_host, User: $db_user, Password: $db_pass&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;사랑해 php야 망하지만 말아줘...&lt;/p&gt;</description>
      <category>PHP/PHP</category>
      <category>ENV</category>
      <category>getenv</category>
      <category>php</category>
      <category>putenv</category>
      <category>내장함수</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/262</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-%EB%82%B4%EC%9E%A5-%ED%95%A8%EC%88%98%EB%A1%9C-env-%ED%8C%8C%EC%9D%BC-%EC%82%AC%EC%9A%A9#entry262comment</comments>
      <pubDate>Fri, 7 Mar 2025 10:25:10 +0900</pubDate>
    </item>
    <item>
      <title>HTML, CSS - song card slide</title>
      <link>https://dev-kimchi.tistory.com/entry/HTML-CSS-song-card-slide</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;html2.jpg&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br7S7H/btsMDx3GzTK/HhGX1SnGdt3UDysfeomkUK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br7S7H/btsMDx3GzTK/HhGX1SnGdt3UDysfeomkUK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br7S7H/btsMDx3GzTK/HhGX1SnGdt3UDysfeomkUK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr7S7H%2FbtsMDx3GzTK%2FHhGX1SnGdt3UDysfeomkUK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;394&quot; height=&quot;488&quot; data-filename=&quot;html2.jpg&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&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;프로젝트를 진행하며 flickity 라이브러리를 사용하여 만든 노래 슬라이드를 코드펜에 정리해보았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;codepen&lt;/h2&gt;
&lt;iframe height=&quot;600&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;song card slide&quot; src=&quot;https://codepen.io/aleph-kim/embed/RNwVqJZ?default-tab=result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &lt;a href=&quot;https://codepen.io/aleph-kim/pen/RNwVqJZ&quot;&gt;
  song card slide&lt;/a&gt; by 김채민 (&lt;a href=&quot;https://codepen.io/aleph-kim&quot;&gt;@aleph-kim&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.
&lt;/iframe&gt;</description>
      <category>HTML, CSS</category>
      <category>card</category>
      <category>CSS</category>
      <category>flickity</category>
      <category>HTML</category>
      <category>JS</category>
      <category>Slide</category>
      <category>song</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/261</guid>
      <comments>https://dev-kimchi.tistory.com/entry/HTML-CSS-song-card-slide#entry261comment</comments>
      <pubDate>Thu, 6 Mar 2025 11:02:26 +0900</pubDate>
    </item>
    <item>
      <title>CSS - 노멀라이즈 / 리셋</title>
      <link>https://dev-kimchi.tistory.com/entry/CSS-%EB%85%B8%EB%A9%80%EB%9D%BC%EC%9D%B4%EC%A6%88-%EB%A6%AC%EC%85%8B</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ohOV4/btsMCsu7WnO/g65SsxUSOp7Bb1vphCjuYK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ohOV4/btsMCsu7WnO/g65SsxUSOp7Bb1vphCjuYK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ohOV4/btsMCsu7WnO/g65SsxUSOp7Bb1vphCjuYK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FohOV4%2FbtsMCsu7WnO%2Fg65SsxUSOp7Bb1vphCjuYK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;441&quot; height=&quot;588&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;비교&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;특징&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;CSS 리셋 (Reset)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;CSS 노멀라이즈 (Normalize)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;기본 스타일 처리&lt;/strong&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;strong&gt;접근 방식&lt;/strong&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;strong&gt;대표 라이브러리&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://elad2412.github.io/the-new-css-reset/&quot;&gt;Elad Shechter’s CSS Reset&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/necolas/normalize.css/&quot;&gt;normalize.css&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;장점&lt;/strong&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;strong&gt;단점&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;- 기본 스타일을 모두 제거하므로 추가적인 스타일 설정이 필요합니다.&lt;/td&gt;
&lt;td&gt;- 모든 브라우저 차이를 완벽하게 없애지는 못할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;후기&lt;/h2&gt;
&lt;p&gt;복잡하게 생각하지 말고 그냥 reset.css 파일 하나 추가하는게 제일 간단할 것 같다.&lt;/p&gt;
&lt;h3&gt;reset.css&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/***
    The new CSS reset - version 1.11.3 (last updated 25.08.2024)
    GitHub page: https://github.com/elad2412/the-new-css-reset
***/

/*
    Remove all the styles of the &amp;quot;User-Agent-Stylesheet&amp;quot;, except for the &amp;#39;display&amp;#39; property
    - The &amp;quot;symbol *&amp;quot; part is to solve Firefox SVG sprite bug
    - The &amp;quot;html&amp;quot; element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
 */
 *:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
    all: unset;
    display: revert;
}

/* Preferred box-sizing value */
*,
*::before,
*::after {
    box-sizing: border-box;
}

/* Fix mobile Safari increase font-size on landscape mode */
html {
    -moz-text-size-adjust: none;
    -webkit-text-size-adjust: none;
    text-size-adjust: none;
}

/* Reapply the pointer cursor for anchor tags */
a, button {
    cursor: revert;
}

/* Remove list styles (bullets/numbers) */
ol, ul, menu, summary {
    list-style: none;
}

/* Firefox: solve issue where nested ordered lists continue numbering from parent (https://bugzilla.mozilla.org/show_bug.cgi?id=1881517) */
ol {
    counter-reset: revert;
}

/* For images to not be able to exceed their container */
img {
    max-inline-size: 100%;
    max-block-size: 100%;
}

/* removes spacing between cells in tables */
table {
    border-collapse: collapse;
}

/* Safari - solving issue when using user-select:none on the &amp;lt;body&amp;gt; text input doesn&amp;#39;t working */
input, textarea {
    -webkit-user-select: auto;
}

/* revert the &amp;#39;white-space&amp;#39; property for textarea elements on Safari */
textarea {
    white-space: revert;
}

/* minimum style to allow to style meter element */
meter {
    -webkit-appearance: revert;
    appearance: revert;
}

/* preformatted text - use only for this feature */
:where(pre) {
    all: revert;
    box-sizing: border-box;
}

/* reset default text opacity of input placeholder */
::placeholder {
    color: unset;
}

/* fix the feature of &amp;#39;hidden&amp;#39; attribute.
   display:revert; revert to element instead of attribute */
:where([hidden]) {
    display: none;
}

/* revert for bug in Chromium browsers
   - fix for the content editable attribute will work properly.
   - webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
:where([contenteditable]:not([contenteditable=&amp;quot;false&amp;quot;])) {
    -moz-user-modify: read-write;
    -webkit-user-modify: read-write;
    overflow-wrap: break-word;
    -webkit-line-break: after-white-space;
    -webkit-user-select: auto;
}

/* apply back the draggable feature - exist only in Chromium and Safari */
:where([draggable=&amp;quot;true&amp;quot;]) {
    -webkit-user-drag: element;
}

/* Revert Modal native behavior */
:where(dialog:modal) {
    all: revert;
    box-sizing: border-box;
}

/* Remove details summary webkit styles */
::-webkit-details-marker {
    display: none;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>HTML, CSS</category>
      <category>CSS</category>
      <category>노말라이즈</category>
      <category>노말라이징</category>
      <category>노멀라이즈</category>
      <category>노멀라이징</category>
      <category>리셋</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/260</guid>
      <comments>https://dev-kimchi.tistory.com/entry/CSS-%EB%85%B8%EB%A9%80%EB%9D%BC%EC%9D%B4%EC%A6%88-%EB%A6%AC%EC%85%8B#entry260comment</comments>
      <pubDate>Wed, 5 Mar 2025 15:57:45 +0900</pubDate>
    </item>
    <item>
      <title>PHP - include, include_once, require, require_once 차이</title>
      <link>https://dev-kimchi.tistory.com/entry/PHP-include-includeonce-require-requireonce-%EC%B0%A8%EC%9D%B4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;php1.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MfTaI/btsMBifCnKg/5N12jFklqj9sKAvCQOxLk1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MfTaI/btsMBifCnKg/5N12jFklqj9sKAvCQOxLk1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MfTaI/btsMBifCnKg/5N12jFklqj9sKAvCQOxLk1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMfTaI%2FbtsMBifCnKg%2F5N12jFklqj9sKAvCQOxLk1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;605&quot; data-filename=&quot;php1.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;에러 처리&lt;/th&gt;
&lt;th&gt;중복 포함&lt;/th&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;&lt;strong&gt;include()&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;파일이 없으면 경고(E_WARNING) 발생, 실행 계속&lt;/td&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;strong&gt;include_once()&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;파일이 없으면 경고(E_WARNING) 발생, 실행 계속 (단, 중복 방지)&lt;/td&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;strong&gt;require()&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;파일이 없으면 치명적 오류(E_COMPILE_ERROR) 발생, 실행 중단&lt;/td&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;strong&gt;require_once()&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;파일이 없으면 치명적 오류(E_COMPILE_ERROR) 발생, 실행 중단 (단, 중복 방지)&lt;/td&gt;
&lt;td&gt;중복 방지&lt;/td&gt;
&lt;td&gt;필수 파일 포함&lt;/td&gt;
&lt;td&gt;프레임워크 초기화 파일, 핵심 모듈, 공통 유틸리티 클래스 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;</description>
      <category>PHP/PHP</category>
      <category>include</category>
      <category>include_once</category>
      <category>php</category>
      <category>require</category>
      <category>require_once</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/259</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PHP-include-includeonce-require-requireonce-%EC%B0%A8%EC%9D%B4#entry259comment</comments>
      <pubDate>Wed, 5 Mar 2025 12:10:31 +0900</pubDate>
    </item>
    <item>
      <title>docker - docker compose networks 옵션과 드라이버 종류</title>
      <link>https://dev-kimchi.tistory.com/entry/docker-docker-compose-networks</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mNDuY/btsMBWpvryR/kcvLJ5b8wr8vVswNagUSA0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mNDuY/btsMBWpvryR/kcvLJ5b8wr8vVswNagUSA0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mNDuY/btsMBWpvryR/kcvLJ5b8wr8vVswNagUSA0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmNDuY%2FbtsMBWpvryR%2FkcvLJ5b8wr8vVswNagUSA0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;719&quot; height=&quot;500&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&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;프로젝트를 진행하던 중 나의 작고 소중한 php 서버가 mysql 서버를 접근하지 못하여 찾아보니 컨테이너간 네트워크가 연결되어있지 않아 발생한 해프닝이었다.&lt;br /&gt;딸깍만 하면 모든게 자동으로 되는 환경을 원하기 때문에 docker compose의 networks 옵션에 대해서 찾아보았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Compose 네트워크 옵션&lt;/h2&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;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;external&lt;/td&gt;
&lt;td&gt;이미 만들어진 네트워크를 사용한다.(새로 만들지 않음)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;external: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;네트워크의 이름을 정한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name: my_custom_network&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;driver&lt;/td&gt;
&lt;td&gt;컨테이너들이 서로 연결되는 방식을 정한다.(default: bridge)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;driver: overlay&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;driver_opts&lt;/td&gt;
&lt;td&gt;드라이버가 필요로 하는 추가 설정한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;driver_opts: com.docker.network.driver.mtu: &quot;1200&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ipam&lt;/td&gt;
&lt;td&gt;네트워크에서 사용할 IP 주소 범위, 서브넷, 게이트웨이를 정한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ipam: driver: default; config: - subnet: &quot;192.168.1.0/24&quot;, gateway: &quot;192.168.1.1&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;internal&lt;/td&gt;
&lt;td&gt;네트워크를 내부 전용으로 만들어 외부와 연결되지 않게 한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attachable&lt;/td&gt;
&lt;td&gt;Docker Swarm 모드에서 일반 컨테이너도 네트워크에 연결할 수 있게 한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;attachable: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;labels&lt;/td&gt;
&lt;td&gt;네트워크에 태그(이름표)를 붙여 쉽게 관리할 수 있게 한다.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;labels: project: my_project&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 네트워크 드라이버 종류&lt;/h3&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;bridge&lt;/td&gt;
&lt;td&gt;한 컴퓨터 안에서 컨테이너끼리 서로 연결할 때 가장 기본적으로 사용하는 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;overlay&lt;/td&gt;
&lt;td&gt;여러 컴퓨터(서버)에서 컨테이너가 서로 연결될 수 있도록 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;host&lt;/td&gt;
&lt;td&gt;컨테이너가 컴퓨터의 네트워크를 직접 사용한다. 속도가 빠르지만, 다른 컨테이너와 격리되지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macvlan&lt;/td&gt;
&lt;td&gt;컨테이너마다 고유한 네트워크 주소(MAC 주소)를 주어, 실제 다른 기기처럼 보이게 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;네트워크 연결을 하지 않는다. 컨테이너는 자기 자신만 사용할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  app:
    image: nginx:latest
    networks:
      - external_net
      - custom_net

networks:
  # 이미 만들어진 네트워크 사용
  external_net:
    external: true

  # 이 Compose가 만드는 네트워크
  custom_net:
    name: custom_bridge_network  # 네트워크 이름
    driver: bridge               # bridge 드라이버 사용
    driver_opts:
      com.docker.network.driver.mtu: &quot;1200&quot;  # 추가 옵션 설정(최대 전송 단위를 1200로 제한)
    ipam:
      driver: default          # 기본 IP 관리 드라이버 사용
      config:
        - subnet: &quot;192.168.10.0/24&quot;  # 서브넷 범위 설정
          gateway: &quot;192.168.10.1&quot;     # 게이트웨이 설정
    internal: true             # 내부 전용 네트워크로 만듦
    attachable: true           # 일반 컨테이너도 연결 가능하게
    labels:
      project: my_project      # 태그
&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;h3 data-ke-size=&quot;size23&quot;&gt;mysql/docker-compose.yml&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  db:
    image: mysql:8.0
    container_name: mysql_dev
    restart: always
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: MYSQL_ROOT_PASSWORD
      TZ: Asia/Seoul
    networks:
      - mysql_net  # 사용할 네트워크 지정

networks:
  mysql_net:
    name: mysql_net  # 생성할 네트워크 이름&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;php/docker-compose.yml&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  web:
    build: .
    ports:
      - &quot;8080:80&quot;
    volumes:
      - .:/var/www/html
    networks:
      - mysql_net # 사용할 네트워크 지정

networks:
  mysql_net:
    external: true # 네트워크가 외부에 이미 존재함을 지정&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;나는 단순히 컨테이너간 네트워크를 최대한 편하게 연결하고 싶었을 뿐인데 생각보다 깊어진 것 같다...&lt;/p&gt;</description>
      <category>Docker</category>
      <category>compose</category>
      <category>docker</category>
      <category>Network</category>
      <category>networks</category>
      <category>네트워크</category>
      <category>도커</category>
      <category>컨테이너</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/258</guid>
      <comments>https://dev-kimchi.tistory.com/entry/docker-docker-compose-networks#entry258comment</comments>
      <pubDate>Tue, 4 Mar 2025 23:02:42 +0900</pubDate>
    </item>
    <item>
      <title>한글명언 OPEN API 기술 설명서</title>
      <link>https://dev-kimchi.tistory.com/entry/%ED%95%9C%EA%B8%80%EB%AA%85%EC%96%B8-OPEN-API-%EA%B8%B0%EC%88%A0-%EC%84%A4%EB%AA%85%EC%84%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjUbZW/btsL7NTvZvu/CWeKkPNf9LKhbLJVOhpkdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjUbZW/btsL7NTvZvu/CWeKkPNf9LKhbLJVOhpkdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjUbZW/btsL7NTvZvu/CWeKkPNf9LKhbLJVOhpkdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjUbZW%2FbtsL7NTvZvu%2FCWeKkPNf9LKhbLJVOhpkdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;443&quot; height=&quot;176&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;  한글명언 OPEN API 기술설명서&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 문서는 한글명언 OPEN API 프로젝트의 전반적인 시스템 구성, 사용 기술(버전 포함), 모듈별 역할, 실행 방법, 배포 환경, 관련 링크 정보 및 CI/CD 파이프라인 설정에 관한 설명서입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api key나 복잡한 인증 필요없이 한글로 번역한 명언을 받아볼 수 있는 OPEN API 서비스 구현&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 관련 링크  &lt;/h2&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;b&gt;API 문서&lt;/b&gt;: &lt;a href=&quot;https://quote.aleph.kr/api-docs/&quot;&gt;https://quote.aleph.kr/api-docs/&lt;/a&gt;&lt;br /&gt;(API 문서 페이지)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관리자 페이지&lt;/b&gt;: &lt;a href=&quot;https://quote.aleph.kr/admin&quot;&gt;https://quote.aleph.kr/admin&lt;/a&gt;&lt;br /&gt;(관리자 전용 명언 관리 페이지)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;깃허브&lt;/b&gt;: &lt;a href=&quot;https://github.com/Aleph-Kim/korean-quote&quot;&gt;https://github.com/Aleph-Kim/korean-quote&lt;/a&gt;&lt;br /&gt;(프로젝트 소스 코드 페이지)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 시스템 아키텍처  ️&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 시스템 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[클라이언트 (프론트엔드)]&lt;/b&gt;&lt;br /&gt;&amp;bull; EJS 템플릿 엔진: 동적 HTML 페이지 렌더링&lt;br /&gt;&amp;bull; Tailwind CSS: 빠르고 효율적인 UI 스타일링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[서버 (백엔드)]&lt;/b&gt;&lt;br /&gt;&amp;bull; Express.js: RESTful API 엔드포인트 제공&lt;br /&gt;&amp;bull; 미들웨어: 요청 파싱, 쿠키 및 세션 관리, CORS 설정 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[데이터베이스]&lt;/b&gt;&lt;br /&gt;&amp;bull; PostgreSQL: 명언 데이터를 저장&lt;br /&gt;&amp;bull; Sequelize ORM: 데이터베이스와의 객체 지향적 인터랙션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[배포 및 인프라]&lt;/b&gt;&lt;br /&gt;&amp;bull; Docker: 컨테이너 기반 배포 관리&lt;br /&gt;&amp;bull; Nginx Proxy Manager: 프록시 관리 및 SSL/TLS 인증서 발급&lt;br /&gt;&amp;bull; GCP (Google Cloud Platform): 클라우드 인프라 배포&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 데이터 흐름  &lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;요청 처리&lt;/b&gt; &amp;ndash; 클라이언트가 &lt;code&gt;/quote/random&lt;/code&gt; 등 API 엔드포인트로 HTTP 요청을 전송합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직 처리&lt;/b&gt; &amp;ndash; Express 라우터에서 요청을 처리하며, 필요시 Sequelize를 이용해 데이터베이스와 상호작용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 생성&lt;/b&gt; &amp;ndash; 조회된 데이터(랜덤 명언 등)를 JSON 형식 또는 EJS 템플릿을 통해 HTML로 응답합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 처리&lt;/b&gt; &amp;ndash; 에러 발생 시 적절한 상태 코드와 메시지로 클라이언트에 응답합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 사용 기술 및 버전  ️&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. 백엔드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Express.js&lt;/b&gt; &amp;ndash; Node.js 기반 웹 프레임워크, 버전: &lt;code&gt;^4.19.2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EJS&lt;/b&gt; &amp;ndash; 서버 사이드 템플릿 엔진, 버전: &lt;code&gt;^3.1.10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;body-parser&lt;/b&gt; &amp;ndash; HTTP 요청 데이터 파싱, 버전: &lt;code&gt;1.20.2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cookie-parser&lt;/b&gt; &amp;ndash; 쿠키 파싱 미들웨어, 버전: &lt;code&gt;^1.4.7&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;express-session&lt;/b&gt; &amp;ndash; 세션 관리, 버전: &lt;code&gt;^1.18.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;express-validator&lt;/b&gt; &amp;ndash; 요청 데이터 유효성 검증, 버전: &lt;code&gt;^7.2.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;multer&lt;/b&gt; &amp;ndash; 파일 업로드 처리, 버전: &lt;code&gt;^1.4.5-lts.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cors&lt;/b&gt; &amp;ndash; CORS 설정, 버전: &lt;code&gt;^2.8.5&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. 데이터베이스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PostgreSQL&lt;/b&gt; &amp;ndash; 관계형 데이터베이스 시스템, 버전: &lt;code&gt;^16.4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sequelize&lt;/b&gt; &amp;ndash; ORM (Object Relational Mapping) 라이브러리, 버전: &lt;code&gt;^6.37.3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3. 기타 도구&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;dotenv&lt;/b&gt; &amp;ndash; 환경 변수 관리, 버전: &lt;code&gt;^16.4.5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;apidoc&lt;/b&gt; &amp;ndash; API 문서 자동 생성 도구, 버전: &lt;code&gt;^1.2.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tailwind CSS&lt;/b&gt; &amp;ndash; 유틸리티 클래스 기반 CSS 프레임워크, 버전: &lt;code&gt;^3.4.17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PostCSS&lt;/b&gt; &amp;ndash; CSS 전처리 도구, 버전: &lt;code&gt;^8.5.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Autoprefixer&lt;/b&gt; &amp;ndash; 자동 벤더 프리픽스 추가 도구, 버전: &lt;code&gt;^10.4.20&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;nodemon&lt;/b&gt; &amp;ndash; 파일 변경 시 자동 서버 재시작 도구, 버전: &lt;code&gt;^3.1.4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4. 배포 및 인프라 관련 도구&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker&lt;/b&gt; &amp;ndash; 컨테이너 기반 배포 관리 도구, 버전: &lt;code&gt;^26.1.4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nginx Proxy Manager&lt;/b&gt; &amp;ndash; 프록시 관리 및 SSL/TLS 인증서 발급 도구, 버전: &lt;code&gt;^2.11.3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GCP (Google Cloud Platform)&lt;/b&gt; &amp;ndash; 클라우드 인프라 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 모듈 구성 및 주요 기능  &lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1. 서버 엔트리 포인트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일: &lt;code&gt;src/app.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;역할: Express 애플리케이션 인스턴스 생성, 미들웨어 설정( body-parser, cookie-parser, cors, session 등 ), 라우터 및 API 엔드포인트 연결, 에러 핸들링 미들웨어 설정, 서버 포트 리스닝 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2. 라우터 모듈&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경로: &lt;code&gt;src/routes/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;주요 기능:&lt;br /&gt;&amp;bull; 랜덤 명언 조회 API: &lt;code&gt;GET /quote/random&lt;/code&gt;&lt;br /&gt;&amp;bull; 오늘의 명언 조회 API: &lt;code&gt;GET /quote/today&lt;/code&gt;&lt;br /&gt;&amp;bull; 관리자페이지: &lt;code&gt;GET /admin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3. 데이터베이스 모델&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sequelize 모델:&lt;br /&gt;&amp;bull; &lt;b&gt;Quote 모델&lt;/b&gt; &amp;ndash; 속성: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt; (명언 내용), &lt;code&gt;author&lt;/code&gt; (남긴이), &lt;code&gt;createdAt&lt;/code&gt;, &lt;code&gt;updatedAt&lt;/code&gt; 등&lt;br /&gt;&amp;bull; 모델 간 관계 및 데이터 유효성 검증 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4. 뷰 템플릿&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EJS 템플릿&lt;br /&gt;&amp;bull; 경로: &lt;code&gt;views/&lt;/code&gt;&lt;br /&gt;&amp;bull; 동적 HTML 렌더링을 통해 명언 데이터를 사용자에게 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 빌드 및 실행 환경  &lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1. 의존성 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 &lt;code&gt;npm install&lt;/code&gt; 명령을 실행하여 의존성 패키지를 설치합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2. 환경 변수 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 &lt;b&gt;.env&lt;/b&gt; 파일을 생성하고, 다음과 같이 설정합니다:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;PORT=3000
DATABASE_URL=postgres://username:password@localhost:5432/database_name
SESSION_SECRET=your_secret_key&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3. 실행 명령어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 모드 실행 (nodemon 사용): &lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;프로덕션 모드 실행: &lt;code&gt;npm start&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tailwind CSS 빌드 (실시간 감시): &lt;code&gt;npm run css&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tailwind CSS 정적 빌드: &lt;code&gt;npm run build:css&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;API 문서 생성 (apidoc 사용): &lt;code&gt;npm run api&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 배포 및 인프라 설정 ☁️&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1. Docker 기반 배포&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Docker Compose 파일 (docker-compose.yml)&lt;/h4&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  prod:
    build:
      context: .
      dockerfile: Dockerfile.prod
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development

  dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;버전&lt;/b&gt;: Compose 파일 버전 3.8 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;services&lt;/b&gt;: prod와 dev 두 가지 서비스로 구성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;prod 서비스&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 디렉토리를 빌드 컨텍스트로 사용하며, &lt;code&gt;Dockerfile.prod&lt;/code&gt;를 사용하여 이미지를 빌드&lt;/li&gt;
&lt;li&gt;포트 3000을 호스트와 컨테이너 간 매핑&lt;/li&gt;
&lt;li&gt;소스코드와 node_modules 디렉토리를 볼륨으로 연결하여 실시간 동기화&lt;/li&gt;
&lt;li&gt;NODE_ENV 환경 변수를 development로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dev 서비스&lt;/b&gt;: prod와 동일한 방식으로 설정하지만, 별도의 &lt;code&gt;Dockerfile.dev&lt;/code&gt;를 사용하여 개발 환경에 맞는 설정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Dockerfile.prod&lt;/h4&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 베이스 이미지 설정
FROM node:20

# 작업 디렉토리 설정
WORKDIR /app

# 패키지 설치를 위해 package.json 및 package-lock.json 복사
COPY package*.json ./

# 패키지 설치 (production 모드)
RUN npm install --production

# 어플리케이션 코드 복사
COPY . .

# Tailwind CSS 빌드
RUN npm run build:css

# 어플리케이션이 실행될 포트 설정
EXPOSE 3000

# 서버 실행
CMD [ &quot;npm&quot;, &quot;run&quot;, &quot;start&quot; ]&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;베이스 이미지&lt;/b&gt;: node:20 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작업 디렉토리&lt;/b&gt;: &lt;code&gt;/app&lt;/code&gt;으로 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패키지 설치&lt;/b&gt;: package.json 및 package-lock.json 파일을 복사 후, production 모드로 의존성 설치&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 복사&lt;/b&gt;: 전체 소스 코드를 컨테이너 내 &lt;code&gt;/app&lt;/code&gt; 디렉토리로 복사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tailwind CSS 빌드&lt;/b&gt;: &lt;code&gt;npm run build:css&lt;/code&gt;를 통해 CSS 빌드 수행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포트 노출&lt;/b&gt;: 3000번 포트를 외부에 노출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 명령어&lt;/b&gt;: &lt;code&gt;npm run start&lt;/code&gt;로 서버 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2. Nginx Proxy Manager 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 도메인 프록시 설정, SSL 인증서 관리, Docker 컨테이너로 배포된 API 서버 접근 제어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 예시&lt;/b&gt;: API 서버 내부 주소(예: &lt;code&gt;http://localhost:3000&lt;/code&gt;)를 프록시 대상으로 등록하고, HTTPS 및 필요한 포트 포워딩 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3. GCP 배포&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;활용 방법&lt;/b&gt;: Compute Engine, Kubernetes Engine 또는 Cloud Run을 통해 컨테이너 기반 애플리케이션 배포&lt;/li&gt;
&lt;li&gt;Docker 이미지를 빌드 후 GCP Container Registry 또는 Artifact Registry에 업로드, 배포 진행&lt;/li&gt;
&lt;li&gt;Nginx Proxy Manager와 연동하여 도메인 관리 및 SSL 인증서 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 에러 처리 및 로깅  &lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1. 에러 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;400 Bad Request&lt;/b&gt; &amp;ndash; 요청 데이터가 올바르지 않을 경우, 상태 코드 400과 에러 메시지 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;404 Not Found&lt;/b&gt; &amp;ndash; 존재하지 않는 엔드포인트 접근 시, 상태 코드 404와 에러 메시지 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;500 Internal Server Error&lt;/b&gt; &amp;ndash; 서버 내부 오류 발생 시, 상태 코드 500과 에러 메시지 반환 및 로깅을 통해 원인 분석&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2. 로깅&lt;/h3&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;에러 발생 시 스택 트레이스를 함께 기록하여 디버깅에 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 보안 고려 사항  &lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;환경 변수 관리&lt;/b&gt;: 민감한 정보는 &lt;b&gt;.env&lt;/b&gt; 파일로 관리하고, 버전 관리 시스템에서 제외(&lt;code&gt;.gitignore&lt;/code&gt; 설정)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 관리&lt;/b&gt;: &lt;code&gt;express-session&lt;/code&gt;을 사용하여 세션을 안전하게 관리하며, &lt;code&gt;SESSION_SECRET&lt;/code&gt;은 반드시 안전한 값으로 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CORS 정책&lt;/b&gt;: 허용된 도메인만 접근하도록 CORS 미들웨어 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 유효성 검증&lt;/b&gt;: &lt;code&gt;express-validator&lt;/code&gt;를 통해 입력 데이터 검증을 수행하여 SQL Injection 및 XSS 공격 대비&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. CI/CD Pipeline (GitHub Actions)  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 GitHub Actions를 활용하여 CI/CD 파이프라인을 구축하고 있습니다.&lt;br /&gt;파이프라인은 master 브랜치에 대한 push 또는 pull request 이벤트 발생 시 실행되며, Docker 이미지를 빌드하여 Docker Hub에 업로드 후, SSH를 통해 원격 서버에 배포합니다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: CI/CD Pipeline

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build Docker image
        run: |
          docker build . -f Dockerfile.prod -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }}
          docker tag ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:latest

      - name: Push Docker image to Docker Hub
        run: |
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:${{ github.sha }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:latest

  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:latest
            sudo docker stop ${{ secrets.PROJECT_NAME }} || true
            sudo docker rm ${{ secrets.PROJECT_NAME }} || true
            sudo docker run -d -p 3000:3000 --name ${{ secrets.PROJECT_NAME }} --env-file ${{ secrets.ENV_PATH }} ${{ secrets.DOCKER_USERNAME }}/${{ secrets.PROJECT_NAME }}:latest&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 내용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;트리거&lt;/b&gt;: push 및 pull_request 이벤트가 master 브랜치에서 발생하면 파이프라인 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빌드 단계&lt;/b&gt;: Repository 체크아웃, Docker Buildx 설정, Docker Hub 로그인, Dockerfile.prod를 사용한 이미지 빌드 및 태그 생성, Docker Hub로 이미지 업로드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 단계&lt;/b&gt;: 빌드 완료 후 SSH로 원격 서버 접속, 최신 이미지 pull, 기존 컨테이너 중지 및 삭제 후 새 컨테이너 실행 (포트 3000 매핑 및 환경 변수 파일 적용)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 마무리  &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;a href=&quot;mailto:dktjdej@naver.com&quot;&gt;dktjdej@naver.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다!  &lt;/p&gt;</description>
      <category>Js/express</category>
      <category>API</category>
      <category>docker</category>
      <category>EJS</category>
      <category>express</category>
      <category>JS</category>
      <category>nginx</category>
      <category>Open</category>
      <category>PostgreSQL</category>
      <category>명언</category>
      <category>한글</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/257</guid>
      <comments>https://dev-kimchi.tistory.com/entry/%ED%95%9C%EA%B8%80%EB%AA%85%EC%96%B8-OPEN-API-%EA%B8%B0%EC%88%A0-%EC%84%A4%EB%AA%85%EC%84%9C#entry257comment</comments>
      <pubDate>Tue, 4 Feb 2025 15:21:14 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL - 한글 정렬</title>
      <link>https://dev-kimchi.tistory.com/entry/PostgreSQL-%ED%95%9C%EA%B8%80-%EC%A0%95%EB%A0%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIO6bU/btsL6R9z6pY/tsdSq5oP7tHGWkxU6t1QYK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIO6bU/btsL6R9z6pY/tsdSq5oP7tHGWkxU6t1QYK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIO6bU/btsL6R9z6pY/tsdSq5oP7tHGWkxU6t1QYK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bIO6bU/btsL6R9z6pY/tsdSq5oP7tHGWkxU6t1QYK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;490&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&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;PostgreSQL db로 프로젝트를 진행하던 중 한글 컬럼을 기준으로 정렬을 했더니 정렬이 이상하게 되는걸 발견했다. 이를 해결하는 방법을 정리해본다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-04 오후 12.26.22.png&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9bb57/btsL4Jlrv4M/2IY3fFRmF8a4osV3lzOCE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9bb57/btsL4Jlrv4M/2IY3fFRmF8a4osV3lzOCE0/img.png&quot; data-alt=&quot;정렬이 이상하다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9bb57/btsL4Jlrv4M/2IY3fFRmF8a4osV3lzOCE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9bb57%2FbtsL4Jlrv4M%2F2IY3fFRmF8a4osV3lzOCE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;476&quot; height=&quot;290&quot; data-filename=&quot;스크린샷 2025-02-04 오후 12.26.22.png&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정렬이 이상하다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 1 - pg_database 업데이트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 방법은 데이터베이스의 collate 값을 업데이트하는 방식으로 간단하게 데이터베이스 전체에 로케일 변경이 가능하다.&lt;br /&gt;이 방법은 시스템에 ko_KR.utf8 로케일이 미설치된 경우 실패하므로, 해당 로케일이 설치되어 있는지 확인(명령어: &lt;code&gt;locale -a | grep ko_&lt;/code&gt;)해야한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;select datname, datdba, encoding, datcollate, datctype from pg_database; -- 현재 데이터베이스 로케일 확인

UPDATE pg_database SET datcollate = 'ko_KR.utf8' WHERE datname = '데이터베이스명'; -- 로케일 변경&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ko_KR.utf8을 새로 설치한 경우 데이터베이스에 바로 적용 가능한 것이 아닌 이후로 생성하는 데이터베이스에만 적용이 가능하다. 따라서 기존 데이터베이스에 적용을 하고 싶다면 기존 데이터를 백업하고 데이터베이스를 재생성하는 과정이 불가피하다.&lt;/li&gt;
&lt;li&gt;PostgreSQL에서 pg_database와 같은 시스템 카탈로그를 직접 수정하는 것은 공식적으로 권장하지 않는 방법이라고 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 2 - ko_KR.utf8로 데이터베이스 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방법은 데이터베이스 생성 시에 ko_KR.utf8로 데이터베이스를 생성하는 방법이다.&lt;br /&gt;이 방법은 시스템에 ko_KR.utf8 로케일이 미설치된 경우 실패하므로, 해당 로케일이 설치되어 있는지 확인(명령어: &lt;code&gt;locale -a | grep ko_&lt;/code&gt;)해야한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE DATABASE 데이터베이스명
  LC_COLLATE 'ko_KR.utf8' -- 문자열 정렬 규칙을 지정
  LC_CTYPE 'ko_KR.utf8' -- 문자 분류(대소문자 구분, 문자 유형 등)를 지정
  TEMPLATE template0; -- 기본 템플릿이 아닌 template0을 사용함으로써, 데이터베이스 생성 시 기존의 로케일 설정이 영향을 주지 않도록&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;방법 3 - 컬럼에 COLLATE &quot;ko-KR-x-icu&quot; 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 방법은 테이블의 컬럼에 COLLATE &quot;ko-KR-x-icu&quot; 옵션을 설정하는 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ko-KR-x-icu란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ICU의 커스텀 국제화 규칙을 반영하는 ICU 라이브러리 전용 확장 로케일로 ko_KR.utf8과 큰 차이 없음&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 생성 시&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE example (
    name VARCHAR(100) COLLATE &quot;ko-KR-x-icu&quot;
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 수정 시&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE example
ALTER COLUMN name TYPE VARCHAR(255) COLLATE &quot;ko-KR-x-icu&quot; USING name::VARCHAR(255);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정하면 복잡한 과정 없이 바로 한글 로케일 적용이 가능하다. 다만 개인적으로는 컬럼마다 일일히 설정하는 것이 임시방편에 불과하다고 생각해 2번 방법을 권장하고 싶다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 docker 컨테이너로 구성된 psql 서버라서 단순 한글 정렬로부터 파생된 추가 작업이 꼬리에 꼬리를 물고 이어지기 때문에 너무 귀찮아 임시방편으로 처리하게 되었지만 다음에 한글 정렬을 구현할 때는 데이터베이스 생성 시부터 ko_KR.utf8 로케일을 지정할 수 있도록 해야겠다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-04 오후 12.26.48.png&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rDov0/btsL4Imxxby/5CN5qYSKcFSEQ7kegk2gsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rDov0/btsL4Imxxby/5CN5qYSKcFSEQ7kegk2gsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rDov0/btsL4Imxxby/5CN5qYSKcFSEQ7kegk2gsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrDov0%2FbtsL4Imxxby%2F5CN5qYSKcFSEQ7kegk2gsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;554&quot; height=&quot;349&quot; data-filename=&quot;스크린샷 2025-02-04 오후 12.26.48.png&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DataBase/PostgreSQL</category>
      <category>Database</category>
      <category>ko</category>
      <category>kr</category>
      <category>locale</category>
      <category>PostgreSQL</category>
      <category>sort</category>
      <category>table</category>
      <category>정렬</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/256</guid>
      <comments>https://dev-kimchi.tistory.com/entry/PostgreSQL-%ED%95%9C%EA%B8%80-%EC%A0%95%EB%A0%AC#entry256comment</comments>
      <pubDate>Tue, 4 Feb 2025 12:20:12 +0900</pubDate>
    </item>
    <item>
      <title>git - 커밋 메시지 수정</title>
      <link>https://dev-kimchi.tistory.com/entry/git-%EC%BB%A4%EB%B0%8B-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%88%98%EC%A0%95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cip7s8/btsLVEKqFTI/zMG8zxoiPA4oUkU5Ajl0Rk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cip7s8/btsLVEKqFTI/zMG8zxoiPA4oUkU5Ajl0Rk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cip7s8/btsLVEKqFTI/zMG8zxoiPA4oUkU5Ajl0Rk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cip7s8/btsLVEKqFTI/zMG8zxoiPA4oUkU5Ajl0Rk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;373&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 브랜치의 마지막 커밋 메시지를 수정하기&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;git commit --amend&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령을 입력하면 마지막 커밋 메시지를 수정할 수 있다.&lt;br /&gt;메시지를 원하는 대로 수정한 후 저장하고 종료(&lt;code&gt;:wq!&lt;/code&gt;)하면 커밋 메시지가 변경된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이전 커밋 메시지 수정하기 (예: 마지막 3개의 커밋)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;git rebase -i HEAD~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;pick abcdef1 커밋 메시지 1
pick bcdefg2 커밋 메시지 2
pick cdefgh3 커밋 메시지 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 수정하고 싶은 커밋 앞의 pick을 reword(또는 줄여서 r)로 변경한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;pick abcdef1 커밋 메시지 1
reword bcdefg2 커밋 메시지 2  # 여기서 커밋 메시지를 수정할 예정
pick cdefgh3 커밋 메시지 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 저장하고 종료하면, Git이 선택한 커밋들의 메시지를 수정할 수 있는 상태로 들어간다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;커밋 메시지 2

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Wed Jan 22 14:30:38 2025 +0900
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 원하는 대로 수정한 후 저장하고 종료(:wq!)하면 커밋 메시지가 변경된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커밋 메시지를 수정하면 커밋의 해시 값이 변경된다.&lt;/li&gt;
&lt;li&gt;이미 공유된 커밋의 메시지를 변경할 경우, 다른 사용자와 충돌이 발생할 수 있으니 주의해야 한다.&lt;/li&gt;
&lt;li&gt;특히 원격 저장소에 푸시된 커밋을 수정할 경우 강제 푸시(git push --force)가 필요하므로 신중하게 처리해야 한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>git</category>
      <category>commit</category>
      <category>git</category>
      <category>github</category>
      <category>Rebase</category>
      <category>깃</category>
      <category>메시지</category>
      <category>수정</category>
      <category>커밋</category>
      <author>Aleph Kim</author>
      <guid isPermaLink="true">https://dev-kimchi.tistory.com/255</guid>
      <comments>https://dev-kimchi.tistory.com/entry/git-%EC%BB%A4%EB%B0%8B-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%88%98%EC%A0%95#entry255comment</comments>
      <pubDate>Wed, 22 Jan 2025 15:15:50 +0900</pubDate>
    </item>
  </channel>
</rss>