<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기억의 파편들</title>
    <link>https://pulpul8282.tistory.com/</link>
    <description>초보 개발자의 기억의 저장소입니다!</description>
    <language>ko</language>
    <pubDate>Sat, 13 Jun 2026 13:54:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>추억을 백앤드하자</managingEditor>
    <image>
      <title>기억의 파편들</title>
      <url>https://tistory1.daumcdn.net/tistory/4207847/attach/032b48d7c6104ef28c6b4342c03e532f</url>
      <link>https://pulpul8282.tistory.com</link>
    </image>
    <item>
      <title>로컬 LLM 기반 AI Code Reviewer 구축기 - 단순 코드 리뷰를 넘어 아키텍처 리뷰까지</title>
      <link>https://pulpul8282.tistory.com/441</link>
      <description>&lt;h1&gt;로컬 LLM 기반 AI Code Reviewer 구축기 - 단순 코드 리뷰를 넘어 아키텍처 리뷰까지&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AI 기반 코드 리뷰 도구들이 빠르게 발전하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Copilot, Cursor, Claude Code 같은 도구들은 코드 작성뿐만 아니라 코드 리뷰까지 지원하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 프로젝트에서 사용해보니 한 가지 아쉬운 점이 있었다.&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;p data-ke-size=&quot;size16&quot;&gt;특히 프로젝트의 설계 원칙이나 레이어 규칙을 모르는 상태에서 PR Diff만 보고 리뷰하기 때문에 종종 일반론적인 피드백이 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 직접 GitHub Pull Request 이벤트를 기반으로 동작하는 AI Code Reviewer를 만들어보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 단순했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR 생성 시 자동 리뷰&lt;/li&gt;
&lt;li&gt;코드 품질 리뷰&lt;/li&gt;
&lt;li&gt;프로젝트 아키텍처 리뷰&lt;/li&gt;
&lt;li&gt;로컬 LLM 기반 운영&lt;/li&gt;
&lt;li&gt;GitHub Comment 자동 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;시스템 아키텍처&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;GitHub PR 생성

      │

      ▼

Webhook 수신

      │

      ▼

PR Diff 수집

      │

      ▼

Review Context 생성

      │
      ├── Diff
      ├── 변경 파일 전체 코드
      ├── AGENTS.md
      └── Package Tree

      ▼

Local LLM

      │
      ├── Code Review
      └── Architecture Review

      ▼

Review Merge

      ▼

GitHub Comment 작성
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 스택은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 3&lt;/li&gt;
&lt;li&gt;Spring AI&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;Qwen3 30B&lt;/li&gt;
&lt;li&gt;GitHub Webhook&lt;/li&gt;
&lt;li&gt;GitHub REST API&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;왜 Diff만으로는 부족했을까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 구현은 매우 단순했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Webhook으로 PR 생성 이벤트를 수신한 후 Diff를 그대로 LLM에게 전달했다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;PR Diff
   &amp;darr;
LLM
   &amp;darr;
Review
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 리뷰 품질은 꽤 괜찮았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Null 처리 누락&lt;/li&gt;
&lt;li&gt;예외 처리 부족&lt;/li&gt;
&lt;li&gt;테스트 코드 필요&lt;/li&gt;
&lt;li&gt;성능 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등은 잘 찾아냈다.&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;ebnf&quot;&gt;&lt;code&gt;PullRequestReviewService
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에 새로운 책임이 추가되었는데도&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;전반적으로 구조가 양호합니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 답변이 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곰곰이 생각해보니 이유는 명확했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 현재 변경된 몇 줄만 보고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 클래스가 원래 어떤 역할인지&lt;/li&gt;
&lt;li&gt;같은 패키지에 어떤 클래스가 있는지&lt;/li&gt;
&lt;li&gt;프로젝트 설계 원칙이 무엇인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전혀 모르는 상태였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;코드 리뷰와 아키텍처 리뷰를 분리하다&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 리뷰를 두 종류로 나누었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Code Review&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;PR Diff
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검토&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버그 가능성&lt;/li&gt;
&lt;li&gt;예외 처리&lt;/li&gt;
&lt;li&gt;성능&lt;/li&gt;
&lt;li&gt;가독성&lt;/li&gt;
&lt;li&gt;테스트 필요성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Architecture Review&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;PR Diff
+ 변경 파일 전체 코드
+ Package Tree
+ AGENTS.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검토&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계층 분리&lt;/li&gt;
&lt;li&gt;책임 분산&lt;/li&gt;
&lt;li&gt;Service 비대화&lt;/li&gt;
&lt;li&gt;외부 API 호출 위치&lt;/li&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;h1&gt;AGENTS.md 도입&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 효과가 좋았던 개선은 AGENTS.md였다.&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;# Architecture Rules

- Controller는 HTTP 처리만 담당한다.
- Service는 유스케이스 조율만 담당한다.
- 외부 API 호출은 Client 계층에 둔다.
- Prompt 생성은 PromptBuilder에 둔다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 시 이 파일을 읽어서 프롬프트에 포함한다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[PROJECT_RULES]
AGENTS.md

[PR_DIFF]
...

[CHANGED_FILE_SOURCE]
...

[PACKAGE_TREE]
...
&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;&quot;현재 프로젝트 규칙을 위반하는지&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 판단하기 시작했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;변경 파일 전체 코드 수집&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 리뷰에서 가장 중요한 컨텍스트였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Diff만 보면&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;+ private final GithubClient githubClient;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도만 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 전체 클래스를 보면&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;PullRequestReviewService
 ├─ Diff 조회
 ├─ LLM 호출
 ├─ Comment 작성
 └─ Prompt 생성
&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;GitHub API를 이용해 변경 파일의 최신 코드를 수집하도록 개선했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;프로젝트 규칙을 모르는 AI는 좋은 리뷰어가 아니다&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 얻은 가장 큰 교훈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 똑똑하지만 프로젝트를 모른다.&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;Controller
  &amp;darr;
Service
  &amp;darr;
Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 따르는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD를 사용하는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hexagonal Architecture를 사용하는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 좋은 AI 리뷰를 만들기 위해서는&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;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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;앞으로 개선할 내용&lt;/h1&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;GitHub Webhook&lt;/li&gt;
&lt;li&gt;PR Diff 수집&lt;/li&gt;
&lt;li&gt;Local LLM 연동&lt;/li&gt;
&lt;li&gt;Code Review&lt;/li&gt;
&lt;li&gt;Architecture Review&lt;/li&gt;
&lt;li&gt;GitHub Comment 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 다음 기능을 추가할 예정이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAG 기반 프로젝트 컨텍스트 검색&lt;/h2&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;PR Diff
   &amp;darr;
Vector Search
   &amp;darr;
관련 코드 검색
   &amp;darr;
LLM
&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;pre class=&quot;cmake&quot;&gt;&lt;code&gt;Gradle Test
    &amp;darr;
실패 테스트
    &amp;darr;
LLM
&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;pre class=&quot;stata&quot;&gt;&lt;code&gt;과거 PR
과거 버그
과거 설계 결정
&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;h1&gt;마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 코드 리뷰어를 만들면서 느낀 점은 하나였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 리뷰 품질은 모델 크기로 결정되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마나 많은 프로젝트 컨텍스트를 제공하느냐가 더 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Diff만 보는 리뷰어와&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 규칙, 전체 코드, 패키지 구조를 이해하는 리뷰어는 전혀 다른 결과를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 단순히 LLM을 붙이는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;프로젝트를 이해하는 AI 리뷰어&quot;를 만드는 과정이었다.&lt;/p&gt;</description>
      <category>나의 주니어 개발 일기/트러블슈팅</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/441</guid>
      <comments>https://pulpul8282.tistory.com/441#entry441comment</comments>
      <pubDate>Thu, 11 Jun 2026 09:37:05 +0900</pubDate>
    </item>
    <item>
      <title>PolyCARP를 활용한 드론 Geofence/Area 공간 판단 구현기 - 좌표계, 경계선 그리고 수많은 삽질들</title>
      <link>https://pulpul8282.tistory.com/440</link>
      <description>&lt;h1&gt;&lt;span&gt;PolyCARP를 활용한 드론 Geofence/Area 공간 판단 구현기 - 좌표계, 경계선 그리고 수많은 삽질들&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;드론 안전 제어 시스템을 개발하면서 가장 먼저 구현해야 했던 기능 중 하나는 &quot;공간 판단(Spatial Evaluation)&quot; 이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;겉으로 보기에는 단순해 보인다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;드론이 비행 가능 구역 안에 있는가?
드론이 비행 금지 구역에 진입했는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 실제 구현을 시작해보니 생각보다 훨씬 어려운 문제였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 실제 운영 환경에서는 단순히 내부/외부 여부만 알면 되는 것이 아니라&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;현재 비행 가능 구역(Area) 안에 있는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;비행 금지 구역(Geofence)에 진입했는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;경계선까지 얼마나 남았는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;위험 상태라면 어디로 복귀시켜야 하는가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;까지 판단해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 글에서는 NASA PolyCARP 기반의 공간 판단 기능을 구현하면서 겪었던 경험과 트러블슈팅 내용을 정리해보려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;왜 PolyCARP를 선택했는가&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 단순히 위도/경도 범위를 비교하는 방법도 고려했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;if(lat &amp;gt;= minLat &amp;amp;&amp;amp;
   lat &amp;lt;= maxLat &amp;amp;&amp;amp;
   lon &amp;gt;= minLon &amp;amp;&amp;amp;
   lon &amp;lt;= maxLon){
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 실제 비행 구역은 대부분 사각형이 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 아래와 같은 다각형 형태일 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;          *
        *   *
      *       *
    *           *
      *       *
        *   *
          *&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 경우 min/max 방식으로는 정확한 판단이 불가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 다각형 기반 공간 판단이 필요했고 PolyCARP의 Polygon Geometry 개념을 활용하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;첫 번째 문제 - 위경도는 생각보다 계산하기 어렵다&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 위도/경도 좌표만으로 모든 계산을 시도했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 곧 문제가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;위도 0.0001 차이
경도 0.0001 차이&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 항상 같은 거리가 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 경도는 위도에 따라 실제 길이가 달라진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 모든 계산을 Local 좌표계로 변환하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;x = (lon - refLon)
      * 111320
      * cos(refLat)

y = (lat - refLat)
      * 111320&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;변환 후에는&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;x = 동서 방향 거리(m)
y = 남북 방향 거리(m)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 표현할 수 있게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후 Polygon 포함 여부, 경계 거리 계산, 복귀 지점 계산이 훨씬 단순해졌다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;두 번째 문제 - GeoJSON 좌표 순서 함정&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;구현 후 이상한 현상이 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;분명 Geofence를 적재했는데 조회 결과가 항상 0건이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;디버깅해보니 Spatial Index가 비어 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;원인을 찾아보니 GeoJSON 좌표 순서를 잘못 해석하고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GeoJSON은&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  127.123,
  37.123
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;형태로 저장된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[lon, lat]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;순서이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 나는 무의식적으로&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;double refLat = coordinate[0];
double refLon = coordinate[1];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 작성해버렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결과적으로&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;위도 = 127
경도 = 37&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라는 말도 안 되는 좌표가 생성되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spatial Index의 Bounding Box도 전부 깨졌고 Geofence 검색 결과는 항상 0건이 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수정 후 정상적으로 동작했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;double refLon = coordinate[0];
double refLat = coordinate[1];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;단순한 실수였지만 실제로 하루 가까이 원인을 찾지 못했던 버그였다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;세 번째 문제 - Polygon 내부 판정 버그&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;어느 날 테스트 중 이상한 결과가 나왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Geofence 밖에 있는 드론이 내부로 판정되고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;원인을 추적해보니 Polygon 마지막 점을 시작점과 동일하게 넣고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;A
B
C
D
A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Polygon을 닫기 위해 마지막 점을 중복 추가했던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문제는 마지막 선분이&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;A -&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 되면서 길이가 0인 선분이 생성된다는 점이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 선분 때문에 Boundary Projection 계산이 꼬였고 contains() 결과까지 잘못 나오고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후에는&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Polygon은 중복 없이 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하거나&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if(segmentLength == 0){
    continue;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;방어 코드를 추가해서 해결했다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;네 번째 문제 - 고도 단위(ft / m)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 버그는 찾는 데 정말 오래 걸렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수평 위치는 분명 Area 내부였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 수직 판정이 계속 실패했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Position.makeLatLonAlt(
    lat,
    lon,
    100
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;고도를 100m로 넣었다고 생각했는데 실제로는 아니었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;NASA 라이브러리의 기본 단위는 feet였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100ft
&amp;asymp; 30m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 해석되고 있었던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 때문에&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;현재 고도 = 30m
Area 최소고도 = 100m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 판단되어 계속 위험 상태가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 모든 Position 생성부를 아래처럼 수정했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Position.makeLatLonAlt(
    lat,
    &quot;deg&quot;,
    lon,
    &quot;deg&quot;,
    altitude,
    &quot;m&quot;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후 수직 판단이 정상적으로 동작했다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;다섯 번째 문제 - Geofence와 Area는 복귀 방향이 반대다&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;초기 구현에서는 Geofence와 Area 모두 같은 복귀 알고리즘을 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 테스트를 해보니 이상했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Area 밖에 있는 드론이 더 멀리 도망가고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;곰곰이 생각해보니 두 개념은 완전히 달랐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Geofence는&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;위험구역&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 내부에 있으면 밖으로 나가야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 Area는&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;허용구역&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 외부에 있으면 안으로 들어와야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 복귀 알고리즘을 분리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Geofence&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재위치
&amp;rarr; 경계점
&amp;rarr; 경계 밖&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Area&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재위치
&amp;rarr; 경계점
&amp;rarr; 중심 방향 내부&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 방향 자체가 반대였다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;최종 구조&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 공간 판단 구조는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;TrackPosition
      &amp;darr;
Local Projection
      &amp;darr;
Polygon Contains
      &amp;darr;
Boundary Distance
      &amp;darr;
Area / Geofence Evaluation
      &amp;darr;
Recovery Point Calculation
      &amp;darr;
Alert &amp;amp; Avoidance Command&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;평가 로직과 회피 로직을 분리하면서 테스트도 훨씬 쉬워졌다.&lt;/span&gt;&lt;/p&gt;
&lt;div contenteditable=&quot;false&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 &quot;다각형 안에 있는지만 확인하면 되겠지&quot;라고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 실제 구현 과정에서는&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;좌표계 변환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;GeoJSON 표준&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;단위(ft/m)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Polygon Geometry&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;경계 거리 계산&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;복귀 지점 계산&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;등 예상보다 훨씬 많은 요소를 고려해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 이번 프로젝트를 통해 배운 점은 공간 판단은 단순한 좌표 계산이 아니라는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 중요한 것은&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&quot;현재 어디에 있는가?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;보다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&quot;얼마나 위험한가?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&quot;어디로 이동해야 안전한가?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 판단하는 것이라는 사실을 다시 한번 느낄 수 있었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>나의 주니어 개발 일기/트러블슈팅</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/440</guid>
      <comments>https://pulpul8282.tistory.com/440#entry440comment</comments>
      <pubDate>Thu, 4 Jun 2026 20:57:48 +0900</pubDate>
    </item>
    <item>
      <title>드론 안전 제어 시스템을 설계하며 고민했던 것들</title>
      <link>https://pulpul8282.tistory.com/439</link>
      <description>&lt;p data-end=&quot;421&quot; data-start=&quot;359&quot; data-ke-size=&quot;size16&quot;&gt;드론 관제 시스템을 개발하면서 가장 먼저 들었던 생각은 생각보다 &quot;위치 판단&quot;이 쉬운 문제가 아니라는 것이었다.&lt;/p&gt;
&lt;p data-end=&quot;467&quot; data-start=&quot;423&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 드론 위치를 받아서 위험 여부를 판단하면 될 것이라고 생각했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 위치 수신
&amp;rarr; 위험 판단
&amp;rarr; 알림 발행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;548&quot; data-start=&quot;507&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로 설계를 시작해보니 고려해야 할 요소가 예상보다 훨씬 많았다.&lt;/p&gt;
&lt;p data-end=&quot;597&quot; data-start=&quot;550&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 하나의 드론 위치 정보가 들어오더라도 동시에 다음과 같은 평가가 필요했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;689&quot; data-start=&quot;599&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;618&quot; data-start=&quot;599&quot; data-section-id=&quot;rrd1q7&quot;&gt;다른 드론과 충돌 위험은 없는가&lt;/li&gt;
&lt;li data-end=&quot;644&quot; data-start=&quot;619&quot; data-section-id=&quot;1blonan&quot;&gt;비행금지구역(Geofence)을 침범했는가&lt;/li&gt;
&lt;li data-end=&quot;667&quot; data-start=&quot;645&quot; data-section-id=&quot;1dpz0mv&quot;&gt;허용 비행구역(Area)을 이탈했는가&lt;/li&gt;
&lt;li data-end=&quot;689&quot; data-start=&quot;668&quot; data-section-id=&quot;pnewf7&quot;&gt;지정 경로(Route)를 벗어났는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;735&quot; data-start=&quot;691&quot; data-ke-size=&quot;size16&quot;&gt;더 큰 문제는 이 모든 판단 결과가 서로 다른 회피 전략을 요구한다는 점이었다.&lt;/p&gt;
&lt;p data-end=&quot;785&quot; data-start=&quot;737&quot; data-ke-size=&quot;size16&quot;&gt;충돌 위험은 즉시 회피가 필요하지만, 경로 이탈은 상대적으로 우선순위가 낮을 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;842&quot; data-start=&quot;787&quot; data-ke-size=&quot;size16&quot;&gt;단순히 모든 위험 상황에 대해 명령을 생성하면 오히려 서로 충돌하는 회피 명령이 발생할 수 있었다.&lt;/p&gt;
&lt;hr data-end=&quot;847&quot; data-start=&quot;844&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;885&quot; data-start=&quot;849&quot; data-section-id=&quot;11mm1vv&quot; data-ke-size=&quot;size26&quot;&gt;첫 번째 고민: 모든 판단을 하나의 서비스에서 처리할 것인가&lt;/h2&gt;
&lt;p data-end=&quot;920&quot; data-start=&quot;887&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 초기에 가장 단순하게 생각한 구조는 다음과 같았다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public void evaluateSafety(TrackPosition position) {
    evaluateCollision(position);
    evaluateGeofence(position);
    evaluateArea(position);
    evaluateRoute(position);

    publishAlert();
    registerCommand();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1165&quot; data-start=&quot;1156&quot; data-ke-size=&quot;size16&quot;&gt;작동은 가능하다.&lt;/p&gt;
&lt;p data-end=&quot;1195&quot; data-start=&quot;1167&quot; data-ke-size=&quot;size16&quot;&gt;하지만 기능이 늘어날수록 문제가 발생하기 시작했다.&lt;/p&gt;
&lt;p data-end=&quot;1220&quot; data-start=&quot;1197&quot; data-ke-size=&quot;size16&quot;&gt;충돌 로직을 수정하려고 열어본 클래스 안에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1258&quot; data-start=&quot;1222&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1235&quot; data-start=&quot;1222&quot; data-section-id=&quot;5fempf&quot;&gt;Geofence 로직&lt;/li&gt;
&lt;li data-end=&quot;1246&quot; data-start=&quot;1236&quot; data-section-id=&quot;550fkc&quot;&gt;Route 로직&lt;/li&gt;
&lt;li data-end=&quot;1258&quot; data-start=&quot;1247&quot; data-section-id=&quot;pxw3sk&quot;&gt;JMS 발행 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1273&quot; data-start=&quot;1260&quot; data-ke-size=&quot;size16&quot;&gt;까지 모두 섞여 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1310&quot; data-start=&quot;1275&quot; data-ke-size=&quot;size16&quot;&gt;결국 특정 도메인의 변경이 전체 시스템에 영향을 주기 시작했다.&lt;/p&gt;
&lt;hr data-end=&quot;1315&quot; data-start=&quot;1312&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1334&quot; data-start=&quot;1317&quot; data-section-id=&quot;ri9ypc&quot; data-ke-size=&quot;size26&quot;&gt;도메인별로 책임을 분리하기&lt;/h2&gt;
&lt;p data-end=&quot;1372&quot; data-start=&quot;1336&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 각 영역을 독립적인 도메인으로 분리했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;collision
geofence
area
route&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1442&quot; data-start=&quot;1417&quot; data-ke-size=&quot;size16&quot;&gt;그리고 모든 도메인에 동일한 패턴을 적용했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Service
Evaluator
ResultHandler&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1512&quot; data-start=&quot;1489&quot; data-ke-size=&quot;size16&quot;&gt;Evaluator는 오직 판단만 수행한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;RouteCheckResult result =
        routeEvaluator.evaluate(position, route);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1610&quot; data-start=&quot;1603&quot; data-ke-size=&quot;size16&quot;&gt;이 단계에서는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1639&quot; data-start=&quot;1612&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1619&quot; data-start=&quot;1612&quot; data-section-id=&quot;94gict&quot;&gt;알림 발행&lt;/li&gt;
&lt;li data-end=&quot;1630&quot; data-start=&quot;1620&quot; data-section-id=&quot;1re0zq9&quot;&gt;회피 명령 저장&lt;/li&gt;
&lt;li data-end=&quot;1639&quot; data-start=&quot;1631&quot; data-section-id=&quot;xyoztg&quot;&gt;이벤트 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1650&quot; data-start=&quot;1641&quot; data-ke-size=&quot;size16&quot;&gt;를 하지 않는다.&lt;/p&gt;
&lt;p data-end=&quot;1669&quot; data-start=&quot;1652&quot; data-ke-size=&quot;size16&quot;&gt;오직 &quot;위험한가?&quot;만 판단한다.&lt;/p&gt;
&lt;hr data-end=&quot;1674&quot; data-start=&quot;1671&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1693&quot; data-start=&quot;1676&quot; data-section-id=&quot;1ganuj5&quot; data-ke-size=&quot;size26&quot;&gt;판단과 부수효과를 분리하기&lt;/h2&gt;
&lt;p data-end=&quot;1747&quot; data-start=&quot;1695&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 가장 중요하다고 느꼈던 부분은 판단 로직과 부수효과를 분리하는 것이었다.&lt;/p&gt;
&lt;p data-end=&quot;1782&quot; data-start=&quot;1749&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Route 평가 결과가 DANGER라고 가정하자.&lt;/p&gt;
&lt;p data-end=&quot;1805&quot; data-start=&quot;1784&quot; data-ke-size=&quot;size16&quot;&gt;Evaluator의 역할은 여기까지다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;RouteCheckResult result =
        RouteCheckResult.builder()
                .routeDeviationLevel(DANGER)
                .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1996&quot; data-start=&quot;1952&quot; data-ke-size=&quot;size16&quot;&gt;그 이후 알림 발행이나 회피 명령 생성은 ResultHandler에게 위임한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public void handle(
        RouteCheckResult result,
        TrackPosition position,
        RouteModel route
) {
    routeAlertPublisher.publishIfNeeded(result);
    routeAvoidanceCommandService.register(
            position,
            route,
            result
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2331&quot; data-start=&quot;2286&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 위험 판단 로직을 테스트할 때는 MQ나 저장소를 신경 쓸 필요가 없어졌다.&lt;/p&gt;
&lt;hr data-end=&quot;2336&quot; data-start=&quot;2333&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2364&quot; data-start=&quot;2338&quot; data-section-id=&quot;57z00p&quot; data-ke-size=&quot;size26&quot;&gt;MQ 장애가 안전 판단을 막아서는 안 된다&lt;/h2&gt;
&lt;p data-end=&quot;2410&quot; data-start=&quot;2366&quot; data-ke-size=&quot;size16&quot;&gt;실시간 안전 시스템을 설계하면서 또 하나 중요하게 생각한 부분은 장애 격리였다.&lt;/p&gt;
&lt;p data-end=&quot;2459&quot; data-start=&quot;2412&quot; data-ke-size=&quot;size16&quot;&gt;만약 MQ 발행 실패 때문에 안전 판단 자체가 실패한다면 심각한 문제가 될 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;2489&quot; data-start=&quot;2461&quot; data-ke-size=&quot;size16&quot;&gt;그래서 알림 발행은 별도의 이벤트 구조로 분리했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Evaluator
 &amp;darr;
ResultHandler
 &amp;darr;
ApplicationEventPublisher
 &amp;darr;
@Async EventHandler
 &amp;darr;
MQ Publish&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2647&quot; data-start=&quot;2597&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 적용한 이후에는 MQ 장애가 발생하더라도 안전 판단 자체는 정상적으로 수행된다.&lt;/p&gt;
&lt;p data-end=&quot;2711&quot; data-start=&quot;2649&quot; data-ke-size=&quot;size16&quot;&gt;실시간 시스템에서는 &quot;기능 수행&quot;과 &quot;외부 연동&quot;을 분리하는 것이 생각보다 중요하다는 것을 다시 한 번 느꼈다.&lt;/p&gt;
&lt;hr data-end=&quot;2716&quot; data-start=&quot;2713&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2739&quot; data-start=&quot;2718&quot; data-section-id=&quot;ixqtpc&quot; data-ke-size=&quot;size26&quot;&gt;가장 어려웠던 부분은 우선순위였다&lt;/h2&gt;
&lt;p data-end=&quot;2792&quot; data-start=&quot;2741&quot; data-ke-size=&quot;size16&quot;&gt;충돌 위험, Geofence 침범, 경로 이탈이 동시에 발생하면 어떤 명령을 수행해야 할까?&lt;/p&gt;
&lt;p data-end=&quot;2830&quot; data-start=&quot;2794&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 각 도메인이 독립적으로 회피 명령을 생성하도록 구현했다.&lt;/p&gt;
&lt;p data-end=&quot;2876&quot; data-start=&quot;2832&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 테스트를 해보니 서로 다른 명령이 동시에 생성되는 문제가 발생했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;충돌 회피
&amp;rarr; 상승

Geofence 회피
&amp;rarr; 하강&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2967&quot; data-start=&quot;2920&quot; data-ke-size=&quot;size16&quot;&gt;결국 최종 단계에서 하나의 명령만 선택할 수 있는 Safety Layer를 추가했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Collision
Geofence
Area
Route
 &amp;darr;
Safety Layer
 &amp;darr;
Final Command&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3065&quot; data-start=&quot;3045&quot; data-ke-size=&quot;size16&quot;&gt;그리고 우선순위를 명확하게 정의했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 충돌 위험
2. Geofence 침범
3. Area 이탈
4. Route 이탈&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3175&quot; data-start=&quot;3127&quot; data-ke-size=&quot;size16&quot;&gt;이후부터는 각 도메인이 독립적으로 판단하더라도 최종 결과는 일관성을 유지할 수 있었다.&lt;/p&gt;
&lt;hr data-end=&quot;3180&quot; data-start=&quot;3177&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3188&quot; data-start=&quot;3182&quot; data-section-id=&quot;1h9nj85&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-end=&quot;3251&quot; data-start=&quot;3190&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 진행하면서 느낀 점은 드론 안전 제어 시스템은 단순히 좌표를 계산하는 문제가 아니라는 것이다.&lt;/p&gt;
&lt;p data-end=&quot;3257&quot; data-start=&quot;3253&quot; data-ke-size=&quot;size16&quot;&gt;실제로는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3313&quot; data-start=&quot;3259&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3266&quot; data-start=&quot;3259&quot; data-section-id=&quot;1r37d6p&quot;&gt;공간 판단&lt;/li&gt;
&lt;li data-end=&quot;3275&quot; data-start=&quot;3267&quot; data-section-id=&quot;1ee4qk9&quot;&gt;위험도 평가&lt;/li&gt;
&lt;li data-end=&quot;3286&quot; data-start=&quot;3276&quot; data-section-id=&quot;l14l39&quot;&gt;회피 전략 생성&lt;/li&gt;
&lt;li data-end=&quot;3295&quot; data-start=&quot;3287&quot; data-section-id=&quot;xyoztg&quot;&gt;이벤트 처리&lt;/li&gt;
&lt;li data-end=&quot;3303&quot; data-start=&quot;3296&quot; data-section-id=&quot;m7osq0&quot;&gt;장애 격리&lt;/li&gt;
&lt;li data-end=&quot;3313&quot; data-start=&quot;3304&quot; data-section-id=&quot;tez6sl&quot;&gt;우선순위 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3336&quot; data-start=&quot;3315&quot; data-ke-size=&quot;size16&quot;&gt;등 다양한 문제를 함께 해결해야 했다.&lt;/p&gt;
&lt;p data-end=&quot;3382&quot; data-start=&quot;3338&quot; data-ke-size=&quot;size16&quot;&gt;특히 구현보다 어려웠던 것은 &quot;어떤 책임을 어디에 둘 것인가&quot;에 대한 설계였다.&lt;/p&gt;
&lt;p data-end=&quot;3453&quot; data-start=&quot;3384&quot; data-ke-size=&quot;size16&quot;&gt;결국 좋은 안전 시스템은 복잡한 알고리즘보다도 각 구성 요소가 자신의 역할만 수행하도록 만드는 구조에서 시작된다고 생각한다.&lt;/p&gt;</description>
      <category>나의 주니어 개발 일기/트러블슈팅</category>
      <category>드론</category>
      <category>트러블슈핑</category>
      <category>프로젝트</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/439</guid>
      <comments>https://pulpul8282.tistory.com/439#entry439comment</comments>
      <pubDate>Thu, 4 Jun 2026 20:51:17 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 정리2</title>
      <link>https://pulpul8282.tistory.com/438</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Controller&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Healing, Software Update, Auto Scaling, Job 의 기능들을 제공한다.(- 표시는 추후 중급과정에서)&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;581&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIOAU0/dJMcacoVDmr/hIor1PLCK8n7KWVGAvMk6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIOAU0/dJMcacoVDmr/hIor1PLCK8n7KWVGAvMk6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIOAU0/dJMcacoVDmr/hIor1PLCK8n7KWVGAvMk6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIOAU0%2FdJMcacoVDmr%2FhIor1PLCK8n7KWVGAvMk6k%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;1128&quot; height=&quot;581&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;581&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Controller 옵션&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;685&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lGkPT/dJMcajuM26H/dRAl4qkfI2Ke9E0if4t7iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lGkPT/dJMcajuM26H/dRAl4qkfI2Ke9E0if4t7iK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lGkPT/dJMcajuM26H/dRAl4qkfI2Ke9E0if4t7iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlGkPT%2FdJMcajuM26H%2FdRAl4qkfI2Ke9E0if4t7iK%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;1241&quot; height=&quot;685&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;685&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Template&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller와 Pod는 Service와 Pod 처럼 label과 selector로 연결된다.&lt;/li&gt;
&lt;li&gt;selector를 선언하고 template에 label을 선언한다. 버전 변경시에는 기존 pod만 죽이면 controller는 다시 되살리려는 특성을 갖고있기 때문에 그 특성을 이용해서 새로운 버전을 올리기 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Replicas&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;replicas 숫자만큼 pod의 개수가 관리된다. replicas가 3개이면 pod도 3개로 scale out하여 만들어준다&lt;/li&gt;
&lt;li&gt;pod에 대한 내용이 없으면 replicas가 동작하지 않게된다. pod와 controller를 따로 만들지 않고 한번에 만들 수 있는데 templete에 선언된 pod의 내용을 기반으로 연결되어 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Selector&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;replication의 selector는 같은 정보를 갖고 있는 pod의 label을 보고 pod와 연결해주고 정보가 하나라도 일치하지 않으면 연결되지 않는다.&lt;/li&gt;
&lt;li&gt;반면 replicaSet에는 selector에 matchLabels 속성을 사용하여 같은 정보(key와 value값이 같아야한다)를 갖고 있는 pod들과 연결되며, matchExpressions 속성을 사용하여 value는 다르지만 key값이 같은 정보들을 갖고 있는 pod들을 연결할 수도 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;matchExpressions 의 value를 담당하는 operator 값에는 4가지가 있다.&lt;/li&gt;
&lt;li&gt;Exists: 키값이 같은 pod만 연결&lt;/li&gt;
&lt;li&gt;DoesNotExist:: 키값이 다른 pod만 연결&lt;/li&gt;
&lt;li&gt;In: 키값이 같고 value가 하나라도 포함된 pod와 연결&lt;/li&gt;
&lt;li&gt;NotIn: 키값이 같고 value가 하나라도 포함되지 않은 pod와 연결&lt;/li&gt;
&lt;/ul&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;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: replica2
spec:
  replicas: 1
  selector:
    matchLabels:
      type: web
      ver: v1
    matchExpressions:
    - {key: type, operator: In, values: [web]}
    - {key: ver, operator: Exists}
  template:
    metadata:
      labels:
        type: web
        ver: v1
        location: dev
    spec:
      containers:
      - name: container
        image: kubetm/app:v1
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Deployment&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHB1dQ/dJMcaaLpOTp/zC4y4jujK9u8bHAMeH2Ha1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHB1dQ/dJMcaaLpOTp/zC4y4jujK9u8bHAMeH2Ha1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHB1dQ/dJMcaaLpOTp/zC4y4jujK9u8bHAMeH2Ha1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHB1dQ%2FdJMcaaLpOTp%2FzC4y4jujK9u8bHAMeH2Ha1%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;1120&quot; height=&quot;533&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ReCreate&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Recreate로 Deployment 배포시 먼저 기존 버전들이 다운되고 새로운 버전이 생성된다.&lt;/li&gt;
&lt;li&gt;다운 되는 순간 Downtime이 발생하기 때문에 &lt;b&gt;일시적인 서비스 정지가 가능한 서비스&lt;/b&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;deployment&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-1
spec:
  selector:
    matchLabels:
      type: app
  replicas: 2
  strategy:
    type: Recreate
  revisionHistoryLimit: 1
  template:
    metadata:
      labels:
        type: app
    spec:
      containers:
      - name: container
        image: kubetm/app:v1
      terminationGracePeriodSeconds: 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    type: app
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규버전(v2)으로 업그레이드시&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;# kubectl set image deployment &amp;lt;deployment-name&amp;gt; &amp;lt;container-name&amp;gt;=&amp;lt;image&amp;gt;
$ kubectl set image deployment deployment-1 container=kubetm/app:v2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전으로 롤백시&lt;/p&gt;
&lt;pre class=&quot;shell&quot;&gt;&lt;code&gt;# 특정 버전으로 롤백하기
$ kubectl rollout undo deployment deployment-1 --to-revision=1
# 롤백 버전 리스트 확인
$ kubectl rollout history deployment deployment-1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 업그레이드 확인해보기&lt;/p&gt;
&lt;pre class=&quot;shell&quot;&gt;&lt;code&gt;$ while true; do curl &amp;lt;service-ip&amp;gt;:8080/version; sleep 1; done&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rolling Update&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rolling update로 배포시 먼저 신규 버전이 1개씩 생성되며 그에 따른 추가적인 자원사용량이 발생한다.&lt;/li&gt;
&lt;li&gt;신규 버전이 다 생성되었으면 자동으로 기존 버전들을 삭제한다.&lt;/li&gt;
&lt;li&gt;배포 과정에서 기존버전과 신규버전이 공존하는 시간이 있는데 그사이에 사용자들이 서로 다르게 버전을 사용하게 될 수도 있다.&lt;/li&gt;
&lt;li&gt;추가적인 자원 사용량이 발생하지만 Downtime이 없다는게 장점이다&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;&lt;b&gt;deployment&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-2
spec:
  selector:
    matchLabels:
      type: app
  replicas: 2
  strategy:
    type: RollingUpdate
  minReadySeconds: 10
  template:
    metadata:
      labels:
        type: app
    spec:
      containers:
      - name: container
        image: kubetm/app:v1
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;service&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: svc-2
spec:
  selector:
    type: app
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;업데이트시 기존 deployment의 내부 이미지 버전 수정(kubetm/app:v1 -&amp;gt; kubetm/app:v2)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-2
spec:
  selector:
    matchLabels:
      type: app
  replicas: 2
  strategy:
    type: RollingUpdate
  minReadySeconds: 10
  template:
    metadata:
      labels:
        type: app
    spec:
      containers:
      - name: container
        image: kubetm/app:v2
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Blue/Green&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Deployment 자체기능으로도 있지만 Controller 를 사용하여 이번편을 설명한다.&lt;/li&gt;
&lt;li&gt;배포하는 Pod의 라벨을 분리하여 배포한다. Service에는 타겟 라벨만 변경해주면 된다.&lt;/li&gt;
&lt;li&gt;순간적으로 변경이 되기 때문에 Downtime이 없다.&lt;/li&gt;
&lt;li&gt;신규버전 배포시에 문제가 있다면 Service의 라벨만 이전버전으로 변경해주면 되기 때문에 문제시 롤백이 쉽다는 장점이 있다.&lt;/li&gt;
&lt;li&gt;최종 배포후에 문제가 없을때만 이전 버전의 Pod를 삭제해주면 된다. 다만 그전까지 이전버전,신규버전의 자원 사용량이 발생하기 때문에 이 부분만 단점이다.&lt;/li&gt;
&lt;li&gt;실무에서 자주 사용하는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ReplicaSet&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: replica1
spec:
  replicas: 2
  selector:
    matchLabels:
      ver: v1
  template:
    metadata:
      labels:
        ver: v1
    spec:
      containers:
      - name: container
        image: kubetm/app:v1
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: svc-3
spec:
  selector:
    ver: v1
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Update할 새 ReplicaSet&lt;/b&gt;(replica2) &lt;b&gt;생성 후 Service&lt;/b&gt;(svc-3)&lt;b&gt;의 Selector를 변경하여 트래픽 변경&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: replica2
spec:
  replicas: 2
  selector:
    matchLabels:
      ver: v2
  template:
    metadata:
      labels:
        ver: v2
    spec:
      containers:
      - name: container
        image: kubetm/app:v2
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service의 Selector 수정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: svc-3
spec:
  selector:
    ver: v2
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Canary&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카나리아는 1초에 17번 숨을 쉬는 새다. 공기중 유해한 물질이 있으면 빨리죽는다. 그래서 광산안에 일산화탄소를 감지하는 역할로 많이 사용했다고 한다. 위험을 감지하는 느낌쓰&lt;/li&gt;
&lt;li&gt;방법1. Pod의 type,version으로 배포를 관리한다. 신규 버전 배포 문제가 될시 v2(신규버전) controller에 replicas만 0으로 만들면 된다. 배포를 해두면 누군가는 v1로 누군가는 v2로 서비스를 이용하게된다. 해당 방식은 불특정 다수를 테스트할때 사용하는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RxMUd/dJMcaiQao5X/srv70NPXc69rbMSvrBWXYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RxMUd/dJMcaiQao5X/srv70NPXc69rbMSvrBWXYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RxMUd/dJMcaiQao5X/srv70NPXc69rbMSvrBWXYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRxMUd%2FdJMcaiQao5X%2Fsrv70NPXc69rbMSvrBWXYK%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;422&quot; height=&quot;722&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방법2. 기존버전과 신규버전 각각 Service를 만들고, Ingress Controller에서 url로 패킷을 분리하여 전달해준다&lt;/li&gt;
&lt;li&gt;이렇게 특정 타겟을 위하여 테스트및 배포가 가능하게 된다.(ex) 해외 사용자/국내 사용자 접속시 다른 서비스로 대응)&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg3tiZ/dJMcafeVazm/pWgfLWBKQw0IPUTEOs4U5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg3tiZ/dJMcafeVazm/pWgfLWBKQw0IPUTEOs4U5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg3tiZ/dJMcafeVazm/pWgfLWBKQw0IPUTEOs4U5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg3tiZ%2FdJMcafeVazm%2FpWgfLWBKQw0IPUTEOs4U5k%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;321&quot; height=&quot;578&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0lsp8/dJMcaiWWb1Q/ukXlElyDvX3Pk7iS1K9me1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0lsp8/dJMcaiWWb1Q/ukXlElyDvX3Pk7iS1K9me1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0lsp8/dJMcaiWWb1Q/ukXlElyDvX3Pk7iS1K9me1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0lsp8%2FdJMcaiWWb1Q%2FukXlElyDvX3Pk7iS1K9me1%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;342&quot; height=&quot;119&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Blue/Green 방식과 마찬가지로 Downtime은 없지만 자원 사용량이 2배가 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Controller - DaemonSet, Job ,CronJob&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQKMgu/dJMcagLD7dk/qn7C6ziVBll9lQk5W6nUgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQKMgu/dJMcagLD7dk/qn7C6ziVBll9lQk5W6nUgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQKMgu/dJMcagLD7dk/qn7C6ziVBll9lQk5W6nUgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQKMgu%2FdJMcagLD7dk%2Fqn7C6ziVBll9lQk5W6nUgk%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;827&quot; height=&quot;406&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목적&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DaemonSet&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드의 자원 여부와 상관 없이 모든 노드에 pod가 1개씩 생성되는 특징이 있다. 만약 노드가 10개면 각 노드에 pod1개씩 총 pod 10개가 생성되는 것이다.&lt;/li&gt;
&lt;li&gt;성능 수집 에이전트(prometheus), 로그 수집(fluentd), 스토리지(GlusterFS) 에 주로 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job, CronJob&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod들이 Node1에서 돌아갈때 Node1이 down되었을때 controller에 의해서 만들어진 pod들은 장애를 감지하여 Node2에 pod를 재생성하는 등으로 장애 대응이 가능하게 된다.(ReplicaSet, Job)&lt;/li&gt;
&lt;li&gt;ReplicaSet 에서 Recreate: pod를 다시 만들어주기때문에 pod의 ip나 이름이 변경된다, Restart: pod는 그대로 있고 pod 안에 컨테이너만 재기동 시켜준다&lt;/li&gt;
&lt;li&gt;반면 Job으로 만들어진 pod는 프로세스가 일을 하지 않으면 pod가 종료(자원을 사용하지 않는 상태로 멈춤, 없어지는것은 아님)&lt;/li&gt;
&lt;li&gt;CronJob들은 이런 Job들을 주기적으로 생성하는 역할을 한다.(특정 시간에 반복적으로 사용할 목적으로 사용, db백업, 주기적 업데이트, 메일/sms 메시지 발송)&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HsMym/dJMcagESYwT/3zxOmtnQjgVkt0QtsoQ2c1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HsMym/dJMcagESYwT/3zxOmtnQjgVkt0QtsoQ2c1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HsMym/dJMcagESYwT/3zxOmtnQjgVkt0QtsoQ2c1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHsMym%2FdJMcagESYwT%2F3zxOmtnQjgVkt0QtsoQ2c1%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;844&quot; height=&quot;405&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DaemonSet&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;selector와 template으로 모든 node에 Pod를 만든다.&lt;/li&gt;
&lt;li&gt;nodeSelector를 통해 os를 지정하여 특정 os에서 돌아가는 pod만 연결할 수도 있다. 지정하지 않으면 모든 node에 생긴다&lt;/li&gt;
&lt;li&gt;아래와 같이 NodePort 타입의 Service를 만들때 &lt;code&gt;extarnalTrafficPolicy: Local&lt;/code&gt; 옵션을 설정하면 특정 노드의 nodePort를 외부에서 접근했을때 Service로 연결이 된다. 이와 같이 DaemonSet에서도 &lt;code&gt;hostPort&lt;/code&gt; 옵션을 사용하면 외부에서 hostPort로 접근하면 containerPort 를 통해서 각 노드의 pod로 연결해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2T1dl/dJMcaadAAIN/q0912Y4DyVF7Sk34qU6LXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2T1dl/dJMcaadAAIN/q0912Y4DyVF7Sk34qU6LXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2T1dl/dJMcaadAAIN/q0912Y4DyVF7Sk34qU6LXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2T1dl%2FdJMcaadAAIN%2Fq0912Y4DyVF7Sk34qU6LXK%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;254&quot; height=&quot;387&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제1(hostPort)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: daemonset-1
spec:
  selector:
    matchLabels:
      type: app
  template:
    metadata:
      labels:
        type: app
    spec:
      containers:
      - name: container
        image: kubetm/app
        ports:
        - containerPort: 8080
          hostPort: 18080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제2(NodeSelector)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: daemonset-2
spec:
  selector:
    matchLabels:
      type: app
  template:
    metadata:
      labels:
        type: app
    spec:
      nodeSelector:
        os: centos
      containers:
      - name: container
        image: kubetm/app:v1
        ports:
        - containerPort: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Job&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;template에는 특정 작업만 하고 종료하는 pod들만 담아둔다. selector는 직접 만들지 않아도 Job이 만들어준다.&lt;/li&gt;
&lt;li&gt;completions 옵션을 사용하면 pod를 completions 개수 만들고 모두 작업을 완료해야 Job이 종료된다.&lt;/li&gt;
&lt;li&gt;parallelism 옵션을 사용하면 pod가 생성시 동시에 parallelism 개수만큼 생성된다.&lt;/li&gt;
&lt;li&gt;activeDeadlineSeconds 옵션을 사용하면 설정 시간 후에 모든 Job은 종료된다. 특정 작업에 행이 걸릴꺼 같을때 자원을 릴리즈 하기위해 사용한다.&lt;/li&gt;
&lt;li&gt;template에서 pod를 만들때 restartPolicy 옵션을 필수값이다.(Never / onFailure) 해당 옵션은 추후 Pod 파트에서 자세히 설명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제1&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: batch/v1
kind: Job
metadata:
  name: job-1
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: container
        image: kubetm/init
        command: [&quot;sh&quot;, &quot;-c&quot;, &quot;echo 'job start';sleep 20; echo 'job end'&quot;]
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제2&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: batch/v1
kind: Job
metadata:
  name: job-2
spec:
  completions: 6
  parallelism: 2
  activeDeadlineSeconds: 40
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: container
        image: kubetm/init
        command: [&quot;sh&quot;, &quot;-c&quot;, &quot;echo 'job start';sleep 20; echo 'job end'&quot;]
      terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CronJob&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jobTemplate을 통해 job을 만들어준다. schedule 통해 주기적으로 job을 생성한다.&lt;/li&gt;
&lt;li&gt;concurrencyPolicy 라는 옵션이 있는데 설정하지 않으면 Allow값이 default로 적용된다.&lt;/li&gt;
&lt;li&gt;Allow: schedule 주기마다 pod가 생성된다 , Forbid: schedule 주기마다 pod가 생성되지만 기존 Job이 안끝났으면 생성을 skip하고, 기존 Job이 종료되었으면 생성한다, Replace: 기존 Pod를 삭제하고 새로운 Job과 Pod가 주기적으로 생성된다.&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;less&quot;&gt;&lt;code&gt;apiVersion: batch/v1
kind: CronJob
metadata:
  name: cron-job
spec:
  schedule: &quot;*/1 * * * *&quot;
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: container
            image: kubetm/init
            command: [&quot;sh&quot;, &quot;-c&quot;, &quot;echo 'job start';sleep 20; echo 'job end'&quot;]
          terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제2&lt;/b&gt;(concurrencyPolicy 옵션)&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: batch/v1
kind: CronJob
metadata:
  name: cron-job-2
spec:
  schedule: &quot;20,21,22 * * * *&quot;
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: container
            image: kubetm/init
            command: [&quot;sh&quot;, &quot;-c&quot;, &quot;echo 'job start';sleep 140; echo 'job end'&quot;]
          terminationGracePeriodSeconds: 0&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;demonset, job, cronjob, node 삭제 명령어들&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ kubectl delete ds daemonset-1 daemonset-2
$ kubectl delete job job-1 job-2
$ kubectl delete cronjob cron-job cron-job-2 cron-job-3

// Node에 라벨 삭제
$ kubectl label nodes k8s-worker1 os-
$ kubectl label nodes k8s-worker2 os-&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>나의 주니어 개발 일기/쿠버네티스</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/438</guid>
      <comments>https://pulpul8282.tistory.com/438#entry438comment</comments>
      <pubDate>Mon, 23 Mar 2026 09:58:31 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 정리1</title>
      <link>https://pulpul8282.tistory.com/437</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;쿠버네티스&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 격리를 Docker로 했지만, 엄청 많은 서비스들을 일일이 배포하고 운영하는 역할이 필요하게 되면서 쿠버네티스가 등장하였다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8um6/dJMcagSpd84/yJXyAOs8UjfhYYN1PpFmoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8um6/dJMcagSpd84/yJXyAOs8UjfhYYN1PpFmoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8um6/dJMcagSpd84/yJXyAOs8UjfhYYN1PpFmoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8um6%2FdJMcagSpd84%2FyJXyAOs8UjfhYYN1PpFmoK%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;847&quot; height=&quot;407&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIgA7M/dJMcahw04gk/yYIKhG38DDBNSxngXNtq0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIgA7M/dJMcahw04gk/yYIKhG38DDBNSxngXNtq0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIgA7M/dJMcahw04gk/yYIKhG38DDBNSxngXNtq0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIgA7M%2FdJMcahw04gk%2FyYIKhG38DDBNSxngXNtq0K%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;835&quot; height=&quot;426&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠버네티스 클러스터를 운영하는 &lt;b&gt;운영자(Admin)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;자신의 서비스를 배포하려는 &lt;b&gt;사용자(User)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Overview&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kH34G/dJMcaaq7nmS/hqN2WvX22YjCxU1TeD9p5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kH34G/dJMcaaq7nmS/hqN2WvX22YjCxU1TeD9p5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kH34G/dJMcaaq7nmS/hqN2WvX22YjCxU1TeD9p5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkH34G%2FdJMcaaq7nmS%2FhqN2WvX22YjCxU1TeD9p5k%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;843&quot; height=&quot;407&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;namespace&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pod&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service&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;Pod 들을 외부와 연결시켜줌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ResourceQuota/LimitRange&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;namespace에서 사용할 수 있는 자원의 양을 한정(pod의 개수, cpu, memory 제한등..)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ConfigMap / Secret&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;Pod의 환경변수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Replication Controller, ReplicaSet&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;Pod 헬스체크 및 되살리기, Pod scale in out&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pod&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3FSSv/dJMcahDKkzJ/MGTZbz7bkCLkXH1HHZJcM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3FSSv/dJMcahDKkzJ/MGTZbz7bkCLkXH1HHZJcM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3FSSv/dJMcahDKkzJ/MGTZbz7bkCLkXH1HHZJcM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3FSSv%2FdJMcahDKkzJ%2FMGTZbz7bkCLkXH1HHZJcM1%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;835&quot; height=&quot;383&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 읽기를 예로 들어보면 cpu공유시에 여러 파일을 동시에 읽는다고 파일 읽는 속도가 느려질뿐 시스템이 종료되지는 않는다(cpu)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 메모리 공유시에는 잘못된 메모리가 참조되었습니다 라고 하면서 시스템이 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 특성으로 cpu 초과시에는 Pod가 종료되지 않지만 메모리 초과시 Pod가 종료된다.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Service&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMwY1L/dJMcadnNuKN/Ee2S1Y0dOyE4hfkHMHlrFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMwY1L/dJMcadnNuKN/Ee2S1Y0dOyE4hfkHMHlrFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMwY1L/dJMcadnNuKN/Ee2S1Y0dOyE4hfkHMHlrFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMwY1L%2FdJMcadnNuKN%2FEe2S1Y0dOyE4hfkHMHlrFk%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;857&quot; height=&quot;418&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ClusterIP&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ClusterIp는 클러스터 내에서만 유효한 IP 이다&lt;/li&gt;
&lt;li&gt;Pod의 아이피는 Pod가 on/off 할때마다 변하지만 Service는 변하지 않는다.&lt;/li&gt;
&lt;li&gt;그래서 Pod는 항상 Service로부터 접근해야한다.&lt;/li&gt;
&lt;li&gt;주 사용처: 인가된 사용자, 내부 대시보드, Pod의 서비스 상태 디버깅&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NodePort&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodePort가 열리는 위치&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[Client]
   &amp;darr;
&amp;lt;Node IP&amp;gt;:30080   &amp;larr; 여기!
   &amp;darr;
kube-proxy (iptables/ipvs)
   &amp;darr;
Service (ClusterIP)
   &amp;darr;
Pod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ip도 호스트의 ip에 포트를 연다.&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;1번 노드에서 온 데이터라도 2번 노드로 전송 가능&lt;/li&gt;
&lt;li&gt;externalTrafficPolicy: Local 옵션을 적용하면 특정 노드 포트의 ip로 접근하는 트래픽은 service가 해당 노드위에 올려져있는 Pod에게만 데이터를 전달한다.&lt;/li&gt;
&lt;li&gt;주 사용처: 내부망 연결, 데모나 임시 연결용, 내부 환경에서 시스템 개발을 하다가 외부의 간단한 데모를 보여줄때 종종 네트워크 중계기에 포트포워딩을 해서 외부에서 내부시스템에 연결할때, 이럴때 NodePort를 잠시 뚫어두고 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Load Balancer&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주 사용처: 외부 시스템 노출용도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Load Balancer 타입으로 만들면 따로 별도의 외부접속 ip 플러그인이 설치되어 있어야만 Ip가 자동으로 할당된다.(ex. GCP, AWS..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;LoadBalancer가 필요하군요. 누군가 외부 LB 만들어 주세요  &amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 LB 생성 &amp;amp; IP 할당:&lt;br /&gt;  &lt;b&gt;클라우드 제공자(GCP, AWS, Azure 등) 또는 LB 컨트롤러&lt;/b&gt;가 담당&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폐쇠망에서는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ LoadBalancer 타입 생성은 &lt;b&gt;가능&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ kubectl expose deployment my-app \
  --type=LoadBalancer \
  --port=80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 상태를 보면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl get svc
NAME       TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)
my-app     LoadBalancer   10.96.12.34    &amp;lt;pending&amp;gt;     80:31234/TCP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;EXTERNAL-IP가 &lt;code&gt;&amp;lt;pending&amp;gt;&lt;/code&gt;에서 절대 안 바뀜&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면: 외부 IP를 할당해 줄 &lt;b&gt;LB 컨트롤러가 없기 때문&lt;/b&gt;&lt;/p&gt;
&lt;br /&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;Ingress + Ingress Controller&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NGINX Ingress Controller&lt;/li&gt;
&lt;li&gt;Traefik&lt;/li&gt;
&lt;li&gt;HAProxy&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;less&quot;&gt;&lt;code&gt;[Client]
   &amp;darr; 80/443
[Ingress Controller Service]  (NodePort or LoadBalancer)
   &amp;darr;
[Ingress Controller Pod]      &amp;larr; NGINX 실행 중
   &amp;darr; (Ingress 규칙 적용)
[Service (ClusterIP)]
   &amp;darr;
[Pod]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Ingress Controller = 실제 HTTP 서버 (NGINX)&lt;/b&gt;&lt;br /&gt;  &lt;b&gt;Ingress = 그 서버에 먹이는 라우팅 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Controller Pod (Deployment)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ingress-nginx
  template:
    metadata:
      labels:
        app: ingress-nginx
    spec:
      containers:
      - name: controller
        image: registry.k8s.io/ingress-nginx/controller:v1.9.5
        args:
          - /nginx-ingress-controller
        ports:
          - name: http
            containerPort: 80
          - name: https
            containerPort: 443&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔️ 이 Pod 안에서 &lt;b&gt;NGINX가 실행 중&lt;/b&gt;&lt;br /&gt;✔️ Ingress 리소스를 감시(watch)함&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller Service (외부 노출)&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  type: NodePort   # 로컬/폐쇄망에서는 보통 NodePort
  selector:
    app: ingress-nginx
  ports:
    - name: http
      port: 80
      nodePort: 30080
    - name: https
      port: 443
      nodePort: 30443&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(클라우드면 &lt;code&gt;type: LoadBalancer&lt;/code&gt;로 바뀜)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ingress 리소스 (라우팅 규칙)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  rules:
  - host: app.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend
            port:
              number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;code&gt;app.local&lt;/code&gt;로 오면 &lt;code&gt;frontend&lt;/code&gt; Service로 전달&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금증: nginx 설정 파일이 어디에도 마운트 되지 않았는데 어떻게 로드밸런싱을 해주지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 여러 개면, Ingress Controller(NGINX)가 Service 뒤에 있는 Pod들로 자동 로드밸런싱을 한다.&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;Ingress Controller (NGINX)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;L7 로드밸런싱 (HTTP)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service (kube-proxy)&lt;/td&gt;
&lt;td&gt;L4 로드밸런싱 (TCP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod&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;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Volume&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9Ew2z/dJMcadg394t/kWKUvuiRWyP2ngAOL6SYa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9Ew2z/dJMcadg394t/kWKUvuiRWyP2ngAOL6SYa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9Ew2z/dJMcadg394t/kWKUvuiRWyP2ngAOL6SYa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9Ew2z%2FdJMcadg394t%2FkWKUvuiRWyP2ngAOL6SYa0%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;859&quot; height=&quot;429&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;429&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;emptyDir&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;Pod 안에서 생성되기 때문에 일시적인 사용목적에 의한 데이터 용도로만 사용되어야 한다&lt;/li&gt;
&lt;li&gt;Pod가 죽으면 emptyDIr 볼륨의 데이터로 사라진다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-1
spec:
  containers:
  - name: container1
    image: kubetm/init
    volumeMounts:
    - name: empty-dir
      mountPath: /mount1
  - name: container2
    image: kubetm/init
    volumeMounts:
    - name: empty-dir
      mountPath: /mount2
  volumes:
  - name : empty-dir
    emptyDir: {}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;hostPath&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;Pod들이 개별적으로 path를 공유하기 때문에 Pod가 죽어도 데이터가 사라지지 않는다.&lt;/li&gt;
&lt;li&gt;만약 Pod2가 죽고 되살아났을때 기존 Node1에서 Node2로 재생성되었다면 Pod2는 기존 Node1의 path를 자동으로 읽지 못한다.&lt;/li&gt;
&lt;li&gt;때문에 노드가 추가될때마다 리눅스 시스템 별도의 마운트 기술을 이용하여 Node2에도 자동으로 구성해주어야 한다. 이것은 쿠버네티스가 자동으로 해주는것이 아닌 직접 사람이 개입해줘야 함으로 권장되지는 않는 방법이다.&lt;/li&gt;
&lt;li&gt;참고로 hostPath 정보는 Pod가 만들어지기 전에 미리 만들어져있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 생성해두고, 다음 새로운 Pod를 만들때 &lt;b&gt;노드선언&lt;/b&gt;부를 제외하고 만들게되면 새로운 노드가 만들어지게 되면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 노드에 생성되어 있던 hostPath의 설정값은 새로운 Pod에서는 존재하지 않게 된다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-3
###### 노드선언 ######  
spec:
  nodeSelector:
    kubernetes.io/hostname: k8s-worker1
#####################  
  containers:
  - name: container
    image: kubetm/init
    volumeMounts:
    - name: host-path
      mountPath: /mount1
  volumes:
  - name : host-path
    hostPath:
      path: /node-v
      type: DirectoryOrCreate&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PVC/PV&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-01
spec:
  capacity:
    storage: 1G
  accessModes:
  - ReadWriteOnce
  local:
    path: /node-v
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - {key: kubernetes.io/hostname, operator: In, values: [k8s-worker1]} &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;values: [k8s-worker1]}&lt;/b&gt; 만 주의깊게 확인하면 된다. worker1에 저장된다는 의미다&lt;/p&gt;
&lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처:&lt;a href=&quot;https://biz.inflearn.com/course/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EA%B8%B0%EC%B4%88/dashboard?cid=324190&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://biz.inflearn.com/course/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EA%B8%B0%EC%B4%88/dashboard?cid=324190&lt;/a&gt;&lt;/p&gt;</description>
      <category>나의 주니어 개발 일기/쿠버네티스</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/437</guid>
      <comments>https://pulpul8282.tistory.com/437#entry437comment</comments>
      <pubDate>Mon, 23 Mar 2026 09:54:01 +0900</pubDate>
    </item>
    <item>
      <title>[K8s] 쿠버네티스 사설 레지스트리 구축 및 x509: certificate signed by unknown authority 에러 해결 가이드</title>
      <link>https://pulpul8282.tistory.com/436</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;[K8s] 쿠버네티스 사설 레지스트리 구축 및 x509: certificate signed by unknown authority 에러 해결 가이드&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스(Kubernetes) 환경에서 보안상의 이유로 &lt;b&gt;사설 레지스트리(Private Registry)&lt;/b&gt;를 운영하다 보면 반드시 마주치는 문제가 있습니다. 바로 &lt;b&gt;HTTPS/TLS 인증서&lt;/b&gt; 관련 에러입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;ctr&lt;/code&gt; 명령어로는 이미지가 잘 당겨지는데, 정작 &lt;code&gt;kubectl&lt;/code&gt;로 배포하면 &lt;code&gt;ImagePullBackOff&lt;/code&gt;와 함께 &lt;b&gt;&quot;x509: certificate signed by unknown authority&quot;&lt;/b&gt; 에러가 발생하는 상황에서의 완벽한 해결법을 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  사설 TLS 인증서 생성 (OpenSSL)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설 레지스트리를 HTTPS로 운영하기 위해서는 자체 서명 인증서(Self-Signed Certificate)가 필요합니다. 이때 주의할 점은 &lt;b&gt;IP 주소를 인증서에 명시(SAN 설정)&lt;/b&gt;해야 한다는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증서 생성 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터 노드에서 아래 명령어를 실행하여 10년(3650일) 유효한 인증서를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bash&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;openssl req -newkey rsa:4096 -nodes -sha256 -keyout domain.key \
-x509 -days 3650 -out domain.crt \
-subj &quot;/CN=10.202.2.129&quot; \
-addext &quot;subjectAltName = IP:10.202.2.129&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  왜 &lt;code&gt;subjectAltName&lt;/code&gt;이 중요한가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근의 컨테이너 런타임(containerd, Docker 등)은 보안상의 이유로 일반적인 &lt;code&gt;Common Name(CN)&lt;/code&gt;만으로는 인증을 통과시키지 않습니다. &lt;b&gt;&lt;code&gt;subjectAltName&lt;/code&gt; (SAN)&lt;/b&gt; 필드에 레지스트리의 IP 주소가 정확히 명시되어야만 &lt;code&gt;x509: certificate signed by unknown authority&lt;/code&gt; 에러를 방지할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  쿠버네티스 Secret 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 &lt;code&gt;domain.crt&lt;/code&gt;와 &lt;code&gt;domain.key&lt;/code&gt;를 쿠버네티스 클러스터에서 사용할 수 있도록 &lt;code&gt;Secret&lt;/code&gt; 객체로 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bash&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 기존 시크릿이 있다면 삭제 후 재생성
kubectl delete secret registry-tls --ignore-not-found

# TLS 타입의 시크릿 생성
kubectl create secret tls registry-tls \
  --cert=domain.crt \
  --key=domain.key&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 사설 레지스트리(Private Registry) 배포 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설 레지스트리는 내부망에서 이미지를 안전하게 관리하기 위해 필수적입니다. 아래는 TLS 인증서를 적용한 &lt;code&gt;registry.yaml&lt;/code&gt; 예시입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Registry Deployment &amp;amp; Service YAML&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YAML&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: private-registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      dnsPolicy: ClusterFirstWithHostNet
      containers:
      - name: registry
        image: registry:2
        env:
        - name: REGISTRY_HTTP_ADDR
          value: &quot;0.0.0.0:30500&quot;
        - name: REGISTRY_HTTP_TLS_CERTIFICATE
          value: /certs/tls.crt
        - name: REGISTRY_HTTP_TLS_KEY
          value: /certs/tls.key
        ports:
        - containerPort: 5000
        volumeMounts:
        - name: storage
          mountPath: /var/lib/registry
        - name: cert-vol
          mountPath: /certs
          readOnly: true
      volumes:
      - name: storage
        persistentVolumeClaim:
          claimName: watercluster-pvc
      - name: cert-vol
        secret:
          secretName: registry-tls
---
apiVersion: v1
kind: Service
metadata:
  name: registry-service
spec:
  type: NodePort
  selector:
    app: registry
  ports:
    - port: 30500
      targetPort: 30500
      nodePort: 30500&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 트러블슈팅: x509 인증서 에러 해결 (CentOS/RHEL)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설 인증서(Self-Signed Certificate)를 사용하면 쿠버네티스 노드들은 해당 레지스트리를 신뢰하지 않습니다. 이를 해결하기 위해 &lt;b&gt;모든 마스터 및 워커 노드&lt;/b&gt;에서 다음 작업을 수행해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계 1: 시스템 인증서 저장소에 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레지스트리 서버에서 인증서를 추출하여 OS 신뢰 목록에 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bash&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 1. 인증서 추출 (129번은 레지스트리 IP)
openssl s_client -showcerts -connect 10.202.2.129:30500 &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 -outform PEM &amp;gt; /tmp/registry.crt

# 2. 신뢰 저장소 경로로 복사
sudo cp /tmp/registry.crt /etc/pki/ca-trust/source/anchors/watercluster-registry.crt

# 3. 인증서 갱신
sudo update-ca-trust extract&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계 2: containerd 설정 최적화 (핵심 포인트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 분이 &lt;code&gt;/etc/containerd/config.toml&lt;/code&gt;에 &lt;code&gt;config_path&lt;/code&gt;를 설정하지만, 사설 인증서를 OS에 등록했다면 이 설정이 오히려 충돌을 일으킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bash&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;sudo vi /etc/containerd/config.toml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정 내용:&lt;/b&gt; 아래 설정을 찾아 &lt;b&gt;주석 처리&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ini, TOML&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;[plugins.&quot;io.containerd.grpc.v1.cri&quot;.registry]
  # config_path = &quot;/etc/containerd/certs.d&quot;  &amp;lt;-- 주석 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;code&gt;containerd&lt;/code&gt;가 개별 설정 대신 &lt;b&gt;시스템 기본 인증서 저장소&lt;/b&gt;를 참조하게 되어 훨씬 안정적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계 3: 서비스 완전 재시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 적용을 위해 엔진을 새로고침합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bash&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl restart containerd
sudo systemctl restart kubelet&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 대규모 환경에서의 자동화 (Ansible)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리해야 할 서버가 100대 이상이라면 수동 작업은 불가능합니다. &lt;b&gt;Ansible&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;에이전트 미설치(Agentless):&lt;/b&gt; SSH만 연결되면 즉시 실행 가능.&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 서비스 노출 시 포트 매핑 이해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 접속을 위해 &lt;code&gt;Service&lt;/code&gt;의 &lt;code&gt;ports&lt;/code&gt; 설정을 할 때 다음의 구분을 명확히 해야 접속 오류를 방지할 수 있습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;포트 항목&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;port&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클러스터 내부 서비스 IP에서 사용하는 포트&lt;/td&gt;
&lt;td&gt;8080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;targetPort&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;컨테이너 내부 앱(Tomcat 등)이 실제 리스닝하는 포트&lt;/td&gt;
&lt;td&gt;8080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;nodePort&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;외부에서 노드 IP를 통해 접속할 때 사용하는 포트&lt;/td&gt;
&lt;td&gt;30080&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;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;OS 레벨(&lt;code&gt;update-ca-trust&lt;/code&gt;)&lt;/b&gt;에 인증서를 등록하는 것이 가장 확실한 방법입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;containerd&lt;/code&gt;의 &lt;b&gt;&lt;code&gt;config_path&lt;/code&gt; 설정 충돌&lt;/b&gt;을 주의해야 합니다.&lt;/li&gt;
&lt;li&gt;서버 대수가 많다면 &lt;b&gt;Ansible&lt;/b&gt;과 같은 자동화 도구 도입을 적극 고려하세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도움이 되셨다면 공감과 댓글 부탁드립니다! 추가적인 질문은 언제든 환영합니다.&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>나의 주니어 개발 일기/쿠버네티스</category>
      <category>certificate_signed_by_unknown_authority</category>
      <category>containerd</category>
      <category>docker</category>
      <category>ErrImagePull</category>
      <category>ImagePullBackOff</category>
      <category>k8s</category>
      <category>kubernetes</category>
      <category>PrivateRegistry</category>
      <category>x509</category>
      <category>사설레지스트리</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/436</guid>
      <comments>https://pulpul8282.tistory.com/436#entry436comment</comments>
      <pubDate>Fri, 6 Mar 2026 12:21:56 +0900</pubDate>
    </item>
    <item>
      <title>  Next.js에서 통합 테스트 환경 구축하기 (Vitest)</title>
      <link>https://pulpul8282.tistory.com/435</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;  Next.js에서 Supabase 실무 통합 테스트 환경 구축하기 (Vitest)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 프로젝트에서 Supabase 실제 DB를 대상으로 &lt;b&gt;통합 테스트(Integration Test)&lt;/b&gt;를 작성할 때, 가장 큰 걸림돌은 &lt;b&gt;환경 변수(&lt;code&gt;process.env&lt;/code&gt;) 로딩 순서&lt;/b&gt;입니다. 본 가이드에서는 &lt;code&gt;Vitest&lt;/code&gt;와 &lt;code&gt;dotenv&lt;/code&gt;를 활용해 이 문제를 해결하고 실제 DB 연동까지 확인하는 완벽한 워크플로우를 소개합니다.&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;테스트 환경을 구축하기 위해 필요한 핵심 도구들을 설치합니다. Next.js의 경로 별칭(&lt;code&gt;@/&lt;/code&gt;)과 브라우저 환경(&lt;code&gt;jsdom&lt;/code&gt;)을 지원하는 패키지들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install -D vitest @vitejs/plugin-react vite-tsconfig-paths jsdom dotenv @testing-library/jest-dom&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙️ 2. Vitest 설정 (환경 변수 문제 해결)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 환경에서 &lt;code&gt;import&lt;/code&gt; 문은 코드 실행보다 먼저 처리되어 환경 변수가 &lt;code&gt;undefined&lt;/code&gt;로 뜨는 경우가 많습니다. 이를 방지하기 위해 &lt;b&gt;&lt;code&gt;setupFiles&lt;/code&gt;&lt;/b&gt;를 활용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;vitest.config.ts&lt;/code&gt; 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import path from 'path';

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: 'jsdom',
    globals: true,
    // [핵심] 테스트 실행 직전 환경 변수를 먼저 로드합니다.
    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;vitest.setup.ts&lt;/code&gt; 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env.test&lt;/code&gt; 파일을 읽어 &lt;code&gt;process.env&lt;/code&gt;에 강제로 주입하는 셋업 파일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import { config } from 'dotenv';
import path from 'path';

// .env.test 파일을 로드하여 테스트 프로세스에 할당합니다.
config({ path: path.resolve(__dirname, '.env.test') });&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  3. 환경 변수 및 스크립트 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 전용 DB 정보를 관리하기 위해 &lt;code&gt;.env.test&lt;/code&gt; 파일을 별도로 생성하는 것을 권장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;.env.test&lt;/code&gt; 파일 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 스니펫&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=your-secret-key&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;package.json&lt;/code&gt; 스크립트 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;scripts&quot;: {
  &quot;test&quot;: &quot;vitest --config ./vitest.config.ts --mode test&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;  4. 실제 DB 통합 테스트 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 대상 파일(&lt;code&gt;supabaseService.ts&lt;/code&gt;)과 같은 위치에 &lt;code&gt;.test.ts&lt;/code&gt; 파일을 생성하여 관리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;supabaseService.ts&lt;/code&gt; (원본 코드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;import { supabaseAdmin } from '@/lib/supabaseServer';

export const supabaseService = {
  async save(content: string) {
    const { data, error } = await supabaseAdmin
      .from('ko_grammer') // 테이블명 확인 필수
      .insert([{ content }])
      .select()
      .single();

    if (error) {
      console.error('DB 저장 실패:', error.message);
      return { success: false, error };
    }
    return { success: true, data };
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;supabaseService.test.ts&lt;/code&gt; (테스트 코드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 네트워크 통신이 발생하므로 타임아웃을 넉넉히 설정하고, 테스트 후 데이터를 삭제하는 &lt;b&gt;Cleanup&lt;/b&gt; 로직을 포함합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import { afterAll, describe, expect, it } from 'vitest';
import { supabaseAdmin } from '@/lib/supabaseServer';
import { supabaseService } from './supabaseService';

describe('supabaseService.save 실제 DB 통합 테스트', () =&amp;gt; {
  const TEST_CONTENT = '테스트 코드에서 보낸 실제 데이터입니다.';

  // 테스트 종료 후 생성된 데이터를 삭제하여 DB 정결 유지
  afterAll(async () =&amp;gt; {
    await supabaseAdmin
      .from('ko_grammer')
      .delete()
      .eq('content', TEST_CONTENT);
  });

  it('실제 Supabase DB에 데이터를 저장하고 결과를 반환해야 한다', async () =&amp;gt; {
    // 1. 함수 실행
    const result = await supabaseService.save(TEST_CONTENT);

    // 2. 응답 값 검증
    expect(result.success).toBe(true);
    expect(result.data?.content).toBe(TEST_CONTENT);

    // 3. DB 직접 조회 교차 검증
    const { data } = await supabaseAdmin
      .from('ko_grammar')
      .select()
      .eq('id', result.data?.id)
      .single();

    expect(data).not.toBeNull();
    expect(data?.content).toBe(TEST_CONTENT);
  }, 10000); // 네트워크 통신 고려 타임아웃 10초 설정
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>나의 주니어 개발 일기/REACT</category>
      <category>dotenv</category>
      <category>EnvironmentVariables</category>
      <category>IntegrationTest</category>
      <category>nextjs</category>
      <category>supabase</category>
      <category>testing</category>
      <category>typescript</category>
      <category>vitest</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/435</guid>
      <comments>https://pulpul8282.tistory.com/435#entry435comment</comments>
      <pubDate>Fri, 27 Feb 2026 14:04:26 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshoot] Next.js TTS 모바일 발음 이슈 및 Gemini API 서버 사이드 캐싱 해결기</title>
      <link>https://pulpul8282.tistory.com/434</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;[Troubleshoot] Next.js TTS 모바일 발음 이슈 및 Gemini API 서버 사이드 캐싱 해결기&lt;/h1&gt;
&lt;p&gt;Next.js 환경에서 학습 서비스를 개발하며 겪은 두 가지 핵심 이슈인 &lt;strong&gt;&amp;#39;모바일 TTS 발음 파편화&amp;#39;&lt;/strong&gt;와 &lt;strong&gt;&amp;#39;API 호출 비용 최적화를 위한 서버 캐싱&amp;#39;&lt;/strong&gt;에 대한 트러블슈팅 과정을 공유합니다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;&lt;strong&gt;1. TTS 발음 이슈: 왜 내 폰에서는 영어가 한국어처럼 들릴까?&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;  문제 상황&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;PC 웹 브라우저에서는 유창한 영어로 들리던 TTS(Text-to-Speech)가 모바일(Android/iOS) 기기에서 접속하면 한국어 억지로 영어를 읽는 듯한 &amp;#39;한국어식 발음&amp;#39;으로 출력되는 현상이 발생했습니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;  원인 분석&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;브라우저 내장 &lt;code&gt;window.speechSynthesis&lt;/code&gt;는 OS 및 브라우저 엔진에 따라 제공하는 음성 데이터(Voices)가 다릅니다. 단순히 &lt;code&gt;lang = &amp;#39;en-US&amp;#39;&lt;/code&gt;만 설정할 경우, 모바일 기기에서는 기본 설정된 한국어 엔진이 영어를 읽어버리는 경우가 발생하기 때문입니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;✅ 해결 방법: 음성 엔진 우선순위 할당 로직 도입&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;모바일 환경을 감지하여, 품질이 검증된 엔진(Google, Apple 등)을 강제로 매칭하고 모바일 특유의 먹먹함을 해결하기 위해 &lt;code&gt;pitch&lt;/code&gt;와 &lt;code&gt;rate&lt;/code&gt;를 보정했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이전로직&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt; async playTTS(text: string, options?: AudioServiceOptions): Promise&amp;lt;void&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; {
      if (!this.isSupported()) {
        reject(new Error(&amp;#39;Browser does not support speech synthesis&amp;#39;));
        return;
      }

      const opts = { ...this.defaultOptions, ...options };

      // 이전 재생 중지
      window.speechSynthesis.cancel();

      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang = opts.lang || &amp;#39;en-US&amp;#39;;
      utterance.rate = opts.rate || 0.9;
      utterance.pitch = opts.pitch || 1;

      // 최고 품질 음성 선택
      const voice = this.selectBestVoice(utterance.lang);
      if (voice) {
        utterance.voice = voice;
      }

      utterance.onend = () =&amp;gt; resolve();
      utterance.onerror = (error) =&amp;gt; {
        console.error(&amp;#39;Speech synthesis error:&amp;#39;, error);
        reject(error);
      };

      window.speechSynthesis.speak(utterance);
    });
  }&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-tsx&quot;&gt;async playTTS(text: string, options?: AudioServiceOptions): Promise&amp;lt;void&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; {
      if (typeof window === &amp;#39;undefined&amp;#39; || !window.speechSynthesis) {
        return reject(new Error(&amp;#39;지원하지 않는 브라우저입니다.&amp;#39;));
      }
      window.speechSynthesis.cancel();

      const opts = { ...this.defaultOptions, ...options };
      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang = opts.lang || &amp;#39;en-US&amp;#39;;

      const isMobile = this.checkIsMobile();
      if(isMobile){
        utterance.rate = options?.rate || 0.85; 
        utterance.pitch = 1.2;  보
      } else {

        utterance.rate = options?.rate || 0.9;
        utterance.pitch = options?.pitch || 1.0;
      }

// 핵심 로직: 영어 발음 강제를 위한 Voice Selection
      const voices = window.speechSynthesis.getVoices();
      const enVoices = voices.filter(v =&amp;gt; v.lang.startsWith(&amp;#39;en&amp;#39;));

     // 모바일 품질이 좋은 엔진 순서대로 매칭
      const selectedVoice = 
        enVoices.find(v =&amp;gt; v.name.includes(&amp;#39;Google&amp;#39;) &amp;amp;&amp;amp; v.lang === &amp;#39;en-US&amp;#39;) ||
        enVoices.find(v =&amp;gt; v.name.includes(&amp;#39;Apple&amp;#39;) &amp;amp;&amp;amp; v.lang === &amp;#39;en-US&amp;#39;) ||
        enVoices.find(v =&amp;gt; v.lang === &amp;#39;en-US&amp;#39;) ||
        enVoices[0];

      if (selectedVoice) {
        utterance.voice = selectedVoice;
      }

      utterance.onend = () =&amp;gt; resolve();
      utterance.onerror = (e) =&amp;gt; reject(e);

      window.speechSynthesis.speak(utterance);
    });
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;BR&gt;&lt;BR&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;strong&gt;2. Gemini API 재요청 이슈: LocalStorage의 한계와 서버 캐싱&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;  문제 상황&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Gemini API는 호출당 비용이나 할당량이 제한되어 있습니다. 처음에는 사용자의 새로고침을 막기 위해 &lt;strong&gt;LocalStorage&lt;/strong&gt;와 &lt;strong&gt;Client-side 메모리 캐시&lt;/strong&gt;를 사용했으나, 이는 &lt;strong&gt;&amp;#39;개별 사용자&amp;#39;&lt;/strong&gt;에게만 유효했습니다. 서비스에 1,000명의 새로운 사용자가 들어오면 여전히 1,000번의 API 호출이 발생하는 구조였습니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;  원인 분석&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LocalStorage/SWR:&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;strong&gt;✅ 해결 방법: Next.js Route Segment Config를 통한 정적 캐싱&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;모든 사용자가 동일한 학습 데이터를 본다면, 매번 생성할 필요 없이 서버 레벨에서 정적 결과물로 빌드하거나 긴 주기(24시간 등)로 캐싱하는 것이 가장 효율적입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// ✅ Next.js App Router 서버 캐시 전략 적용

export const revalidate = 86400; // 24시간 동안 캐시 유지
export const dynamic = &amp;#39;force-static&amp;#39;; // 해당 라우트를 정적 페이지로 강제

export async function GET(request: NextRequest) {
  try {
    // API 호출 및 데이터 처리 로직...
    return NextResponse.json(grammarData);
  } catch (error) {
    return NextResponse.json({ error: &amp;#39;Failed&amp;#39; }, { status: 500 });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;strong&gt;  마치며: 무엇을 배웠는가?&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;TTS는 엔진 선택이 핵심이다:&lt;/strong&gt; 단순 &lt;code&gt;lang&lt;/code&gt; 설정만으로는 부족하며, 디바이스별 보이스 리스트를 직접 필터링해야 일관된 UX를 제공할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐싱의 주체를 명확히 하자:&lt;/strong&gt; * 개인화된 데이터   &lt;strong&gt;LocalStorage / SWR&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;공통된 리소스 (콘텐츠)   &lt;strong&gt;Server-side Caching (Next.js Cache)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;불필요한 API 호출을 줄임으로써 비용 절감은 물론, 서버 응답 속도를 획기적으로 개선할 수 있었습니다.&lt;/p&gt;
&lt;/div&gt;  </description>
      <category>나의 주니어 개발 일기/트러블슈팅</category>
      <category>frontend</category>
      <category>geminiapi</category>
      <category>nextjs</category>
      <category>PerformanceOptimization</category>
      <category>Troubleshooting</category>
      <category>TTS</category>
      <category>webdevelopment</category>
      <category>WebSpeech</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/434</guid>
      <comments>https://pulpul8282.tistory.com/434#entry434comment</comments>
      <pubDate>Tue, 3 Feb 2026 10:32:56 +0900</pubDate>
    </item>
    <item>
      <title>Supabase 도커(Docker)로 로컬 개발 환경 구축하기</title>
      <link>https://pulpul8282.tistory.com/433</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;Supabase 도커(Docker)로 로컬 개발 환경 구축하기&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Supabase란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Supabase&lt;/b&gt;는 오픈소스 기반의 &lt;b&gt;BaaS(Backend as a Service)&lt;/b&gt; 플랫폼입니다. 구글의 Firebase와 유사한 경험을 제공하지만, 가장 큰 차이점은 &lt;b&gt;PostgreSQL(SQL)&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; 인증(Auth), 실시간 데이터베이스, 스토리지, Edge Functions 등.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; SQL의 강력한 쿼리 기능을 그대로 사용하면서, 백엔드 구축 없이 API를 자동으로 생성해 줍니다. 개발 속도를 극대화하고 프론트엔드 로직에만 집중할 수 있게 돕습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 왜 Docker로 로컬 환경을 구축해야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 버전을 사용할 수도 있지만, 도커를 통한 &lt;b&gt;Self-hosting&lt;/b&gt;은 다음과 같은 이점이 있습니다.&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 통한 self hosting은 아래의 공식문서를 참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://supabase.com/docs/guides/self-hosting/docker&quot;&gt;https://supabase.com/docs/guides/self-hosting/docker&lt;/a&gt;&lt;/p&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Supabase 도커 설치 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 1. 저장소 클론 (Clone Repository)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에 docker-compose.yml 파일이 존재한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 코드 가져오기
$ git clone https://github.com/supabase/supabase 

# 설정 파일이 있는 위치로 이동
$ cd supabase/docker

# 기본 .env 파일 복사
cp .env.example .env&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 2. &lt;code&gt;.env&lt;/code&gt; 파일 핵심 설정 (필수 수정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 전 보안과 정상 작동을 위해 &lt;code&gt;.env&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;POSTGRES_PASSWORD:&lt;/b&gt; PostgreSQL 접속 비밀번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JWT_SECRET:&lt;/b&gt; Supabase Auth와 RLS에 사용하는 JWT 서명 키&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ANON_KEY:&lt;/b&gt; 클아이언트가 사용할 수 있는 공개 키&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SERVICE_ROLE_KEY:&lt;/b&gt; 서버 측에서만 사용할 수 있는 권한 높은 JWT&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DASHBOARD_USERNAME:&lt;/b&gt; Supabase Studio 로그인 ID&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DASHBOARD_PASSWORD:&lt;/b&gt; Supabase Studio 로그인 비밀번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SECRET_KEY_BASE:&lt;/b&gt; Supabase 내부에서 사용하는 Rails 비밀키&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VAULT_ENC_KEY:&lt;/b&gt; 환경 변수 암호화를 위한 Valut 암호화 키(32자 이상)&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;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 .env&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;name: supabase

services:

  studio:
    container_name: supabase-studio
    image: supabase/studio:2026.01.27-sha-6aa59ff
    restart: unless-stopped
    ports:
      - &quot;6661:3000&quot;
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;node&quot;,
          &quot;-e&quot;,
          &quot;fetch('http://studio:3000/api/platform/profile').then((r) =&amp;gt; {if (r.status !== 200) throw new Error(r.status)})&quot;
        ]
      timeout: 10s
      interval: 5s
      retries: 3
    depends_on:
      analytics:
        condition: service_healthy
    environment:
      # Binds nestjs listener to both IPv4 and IPv6 network interfaces
      HOSTNAME: &quot;::&quot;

      STUDIO_PG_META_URL: http://meta:8080
      POSTGRES_PORT: ${POSTGRES_PORT}
      POSTGRES_HOST: ${POSTGRES_HOST}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}

      DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
      DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
      OPENAI_API_KEY: ${OPENAI_API_KEY:-}

      SUPABASE_URL: http://kong:8000
      SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
      SUPABASE_ANON_KEY: ${ANON_KEY}
      SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
      AUTH_JWT_SECRET: ${JWT_SECRET}

      # LOGFLARE_API_KEY is deprecated
      LOGFLARE_API_KEY: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
      LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
      LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}

      LOGFLARE_URL: http://analytics:4000
      NEXT_PUBLIC_ENABLE_LOGS: true
      # Comment to use Big Query backend for analytics
      NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
      # Uncomment to use Big Query backend for analytics
      # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery
      SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
      EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
    volumes:
      - ./volumes/snippets:/app/snippets:Z
      - ./volumes/functions:/app/edge-functions:Z

  kong:
    container_name: supabase-kong
    image: kong:2.8.1
    restart: unless-stopped
    ports:
      - ${KONG_HTTP_PORT}:8000/tcp
      - ${KONG_HTTPS_PORT}:8443/tcp
    volumes:
      # https://github.com/supabase/supabase/issues/12661
      - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
    depends_on:
      analytics:
        condition: service_healthy
    environment:
      KONG_DATABASE: &quot;off&quot;
      KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
      # https://github.com/supabase/cli/issues/14
      KONG_DNS_ORDER: LAST,A,CNAME
      KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction
      KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
      KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
      SUPABASE_ANON_KEY: ${ANON_KEY}
      SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
      DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
      DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
    # https://unix.stackexchange.com/a/294837
    entrypoint: bash -c 'eval &quot;echo \&quot;$$(cat ~/temp.yml)\&quot;&quot; &amp;gt; ~/kong.yml &amp;amp;&amp;amp; /docker-entrypoint.sh kong docker-start'

  auth:
    container_name: supabase-auth
    image: supabase/gotrue:v2.185.0
    restart: unless-stopped
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;wget&quot;,
          &quot;--no-verbose&quot;,
          &quot;--tries=1&quot;,
          &quot;--spider&quot;,
          &quot;http://localhost:9999/health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      GOTRUE_API_HOST: 0.0.0.0
      GOTRUE_API_PORT: 9999
      API_EXTERNAL_URL: ${API_EXTERNAL_URL}

      GOTRUE_DB_DRIVER: postgres
      GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}

      GOTRUE_SITE_URL: ${SITE_URL}
      GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
      GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}

      GOTRUE_JWT_ADMIN_ROLES: service_role
      GOTRUE_JWT_AUD: authenticated
      GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
      GOTRUE_JWT_EXP: ${JWT_EXPIRY}
      GOTRUE_JWT_SECRET: ${JWT_SECRET}

      GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
      GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
      GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}

      # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile.
      # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true

      # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true
      # GOTRUE_SMTP_MAX_FREQUENCY: 1s
      GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
      GOTRUE_SMTP_HOST: ${SMTP_HOST}
      GOTRUE_SMTP_PORT: ${SMTP_PORT}
      GOTRUE_SMTP_USER: ${SMTP_USER}
      GOTRUE_SMTP_PASS: ${SMTP_PASS}
      GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
      GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
      GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
      GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
      GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}

      GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
      GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
      # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook

      # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: &quot;true&quot;
      # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: &quot;pg-functions://postgres/public/custom_access_token_hook&quot;
      # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: &quot;&amp;lt;standard-base64-secret&amp;gt;&quot;

      # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: &quot;true&quot;
      # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI: &quot;pg-functions://postgres/public/mfa_verification_attempt&quot;

      # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: &quot;true&quot;
      # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: &quot;pg-functions://postgres/public/password_verification_attempt&quot;

      # GOTRUE_HOOK_SEND_SMS_ENABLED: &quot;false&quot;
      # GOTRUE_HOOK_SEND_SMS_URI: &quot;pg-functions://postgres/public/custom_access_token_hook&quot;
      # GOTRUE_HOOK_SEND_SMS_SECRETS: &quot;v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n&quot;

      # GOTRUE_HOOK_SEND_EMAIL_ENABLED: &quot;false&quot;
      # GOTRUE_HOOK_SEND_EMAIL_URI: &quot;http://host.docker.internal:54321/functions/v1/email_sender&quot;
      # GOTRUE_HOOK_SEND_EMAIL_SECRETS: &quot;v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n&quot;

  rest:
    container_name: supabase-rest
    image: postgrest/postgrest:v14.3
    restart: unless-stopped
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
      PGRST_DB_ANON_ROLE: anon
      PGRST_JWT_SECRET: ${JWT_SECRET}
      PGRST_DB_USE_LEGACY_GUCS: &quot;false&quot;
      PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
      PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
    command:
      [
        &quot;postgrest&quot;
      ]

  realtime:
    # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
    container_name: realtime-dev.supabase-realtime
    image: supabase/realtime:v2.72.0
    restart: unless-stopped
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    healthcheck:
      test:
        [
          &quot;CMD-SHELL&quot;,
          &quot;curl -sSfL --head -o /dev/null -H \&quot;Authorization: Bearer ${ANON_KEY}\&quot; http://localhost:4000/api/tenants/realtime-dev/health&quot;
        ]
      timeout: 5s
      interval: 30s
      retries: 3
      start_period: 10s
    environment:
      PORT: 4000
      DB_HOST: ${POSTGRES_HOST}
      DB_PORT: ${POSTGRES_PORT}
      DB_USER: supabase_admin
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_NAME: ${POSTGRES_DB}
      DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
      DB_ENC_KEY: supabaserealtime
      API_JWT_SECRET: ${JWT_SECRET}
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      ERL_AFLAGS: -proto_dist inet_tcp
      DNS_NODES: &quot;''&quot;
      RLIMIT_NOFILE: &quot;10000&quot;
      APP_NAME: realtime
      SEED_SELF_HOST: &quot;true&quot;
      RUN_JANITOR: &quot;true&quot;
      DISABLE_HEALTHCHECK_LOGGING: &quot;true&quot;

  # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up
  storage:
    container_name: supabase-storage
    image: supabase/storage-api:v1.33.5
    restart: unless-stopped
    volumes:
      - ./volumes/storage:/var/lib/storage:z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;wget&quot;,
          &quot;--no-verbose&quot;,
          &quot;--tries=1&quot;,
          &quot;--spider&quot;,
          &quot;http://storage:5000/status&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      rest:
        condition: service_started
      imgproxy:
        condition: service_started
    environment:
      ANON_KEY: ${ANON_KEY}
      SERVICE_KEY: ${SERVICE_ROLE_KEY}
      POSTGREST_URL: http://rest:3000
      PGRST_JWT_SECRET: ${JWT_SECRET}
      DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      REQUEST_ALLOW_X_FORWARDED_PATH: &quot;true&quot;
      FILE_SIZE_LIMIT: 52428800
      STORAGE_BACKEND: file
      FILE_STORAGE_BACKEND_PATH: /var/lib/storage
      TENANT_ID: stub
      # TODO: https://github.com/supabase/storage-api/issues/55
      REGION: stub
      GLOBAL_S3_BUCKET: stub
      ENABLE_IMAGE_TRANSFORMATION: &quot;true&quot;
      IMGPROXY_URL: http://imgproxy:5001

  imgproxy:
    container_name: supabase-imgproxy
    image: darthsim/imgproxy:v3.30.1
    restart: unless-stopped
    volumes:
      - ./volumes/storage:/var/lib/storage:z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;imgproxy&quot;,
          &quot;health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    environment:
      IMGPROXY_BIND: &quot;:5001&quot;
      IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
      IMGPROXY_USE_ETAG: &quot;true&quot;
      IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION}
      IMGPROXY_MAX_SRC_RESOLUTION: 16.8

  meta:
    container_name: supabase-meta
    image: supabase/postgres-meta:v0.95.2
    restart: unless-stopped
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      PG_META_PORT: 8080
      PG_META_DB_HOST: ${POSTGRES_HOST}
      PG_META_DB_PORT: ${POSTGRES_PORT}
      PG_META_DB_NAME: ${POSTGRES_DB}
      PG_META_DB_USER: supabase_admin
      PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
      CRYPTO_KEY: ${PG_META_CRYPTO_KEY}

  functions:
    container_name: supabase-edge-functions
    image: supabase/edge-runtime:v1.70.0
    restart: unless-stopped
    volumes:
      - ./volumes/functions:/home/deno/functions:Z
    depends_on:
      analytics:
        condition: service_healthy
    environment:
      JWT_SECRET: ${JWT_SECRET}
      SUPABASE_URL: http://kong:8000
      SUPABASE_ANON_KEY: ${ANON_KEY}
      SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
      SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786
      VERIFY_JWT: &quot;${FUNCTIONS_VERIFY_JWT}&quot;
    command:
      [
        &quot;start&quot;,
        &quot;--main-service&quot;,
        &quot;/home/deno/functions/main&quot;
      ]

  analytics:
    container_name: supabase-analytics
    image: supabase/logflare:1.30.3
    restart: unless-stopped
    ports:
      - 4000:4000
    # Uncomment to use Big Query backend for analytics
    # volumes:
    #   - type: bind
    #     source: ${PWD}/gcloud.json
    #     target: /opt/app/rel/logflare/bin/gcloud.json
    #     read_only: true
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;curl&quot;,
          &quot;http://localhost:4000/health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 10
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
    environment:
      LOGFLARE_NODE_HOST: 127.0.0.1
      DB_USERNAME: supabase_admin
      DB_DATABASE: _supabase
      DB_HOSTNAME: ${POSTGRES_HOST}
      DB_PORT: ${POSTGRES_PORT}
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_SCHEMA: _analytics
      LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
      LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
      LOGFLARE_SINGLE_TENANT: true
      LOGFLARE_SUPABASE_MODE: true

      # Comment variables to use Big Query backend for analytics
      POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
      POSTGRES_BACKEND_SCHEMA: _analytics
      LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
      # Uncomment to use Big Query backend for analytics
      # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID}
      # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER}

  # Comment out everything below this point if you are using an external Postgres database
  db:
    container_name: supabase-db
    image: supabase/postgres:15.8.1.085
    restart: unless-stopped
    ports:
      - &quot;54322:5432&quot;
    volumes:
      - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
      # Must be superuser to create event trigger
      - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
      # Must be superuser to alter reserved role
      - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
      # Initialize the database settings with JWT_SECRET and JWT_EXP
      - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
      # PGDATA directory is persisted between restarts
      - ./volumes/db/data:/var/lib/postgresql/data:Z
      # Changes required for internal supabase data such as _analytics
      - ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z
      # Changes required for Analytics support
      - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
      # Changes required for Pooler support
      - ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z
      # Use named volume to persist pgsodium decryption key between restarts
      - db-config:/etc/postgresql-custom
    healthcheck:
      test:
        [
        &quot;CMD&quot;,
        &quot;pg_isready&quot;,
        &quot;-U&quot;,
        &quot;postgres&quot;,
        &quot;-h&quot;,
        &quot;localhost&quot;
        ]
      interval: 5s
      timeout: 5s
      retries: 10
    depends_on:
      vector:
        condition: service_healthy
    environment:
      POSTGRES_HOST: /var/run/postgresql
      PGPORT: ${POSTGRES_PORT}
      POSTGRES_PORT: ${POSTGRES_PORT}
      PGPASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATABASE: ${POSTGRES_DB}
      POSTGRES_DB: ${POSTGRES_DB}
      JWT_SECRET: ${JWT_SECRET}
      JWT_EXP: ${JWT_EXPIRY}
    command:
      [
        &quot;postgres&quot;,
        &quot;-c&quot;,
        &quot;config_file=/etc/postgresql/postgresql.conf&quot;,
        &quot;-c&quot;,
        &quot;log_min_messages=fatal&quot; # prevents Realtime polling queries from appearing in logs
      ]

  vector:
    container_name: supabase-vector
    image: timberio/vector:0.28.1-alpine
    restart: unless-stopped
    volumes:
      - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
      - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;wget&quot;,
          &quot;--no-verbose&quot;,
          &quot;--tries=1&quot;,
          &quot;--spider&quot;,
          &quot;http://vector:9001/health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    environment:
      LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
    command:
      [
        &quot;--config&quot;,
        &quot;/etc/vector/vector.yml&quot;
      ]
    security_opt:
      - &quot;label=disable&quot;

  # Update the DATABASE_URL if you are using an external Postgres database
  supavisor:
    container_name: supabase-pooler
    image: supabase/supavisor:2.7.4
    restart: unless-stopped
    ports:
      - ${POSTGRES_PORT}:5432
      - ${POOLER_PROXY_PORT_TRANSACTION}:6543
    volumes:
      - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;curl&quot;,
          &quot;-sSfL&quot;,
          &quot;--head&quot;,
          &quot;-o&quot;,
          &quot;/dev/null&quot;,
          &quot;http://127.0.0.1:4000/api/health&quot;
        ]
      interval: 10s
      timeout: 5s
      retries: 5
    depends_on:
      db:
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      PORT: 4000
      POSTGRES_PORT: ${POSTGRES_PORT}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
      CLUSTER_POSTGRES: true
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      VAULT_ENC_KEY: ${VAULT_ENC_KEY}
      API_JWT_SECRET: ${JWT_SECRET}
      METRICS_JWT_SECRET: ${JWT_SECRET}
      REGION: local
      ERL_AFLAGS: -proto_dist inet_tcp
      POOLER_TENANT_ID: ${POOLER_TENANT_ID}
      POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE}
      POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN}
      POOLER_POOL_MODE: transaction
      DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE}
    command:
      [
        &quot;/bin/sh&quot;,
        &quot;-c&quot;,
        &quot;/app/bin/migrate &amp;amp;&amp;amp; /app/bin/supavisor eval \&quot;$$(cat /etc/pooler/pooler.exs)\&quot; &amp;amp;&amp;amp; /app/bin/server&quot;
      ]

volumes:
  db-config:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;b&gt;docker-compose.yml&lt;/b&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1770180005065&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: supabase

services:

  studio:
    container_name: supabase-studio
    image: supabase/studio:2026.01.27-sha-6aa59ff
    restart: unless-stopped
    ports:
      - &quot;6661:3000&quot;
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;node&quot;,
          &quot;-e&quot;,
          &quot;fetch('http://studio:3000/api/platform/profile').then((r) =&amp;gt; {if (r.status !== 200) throw new Error(r.status)})&quot;
        ]
      timeout: 10s
      interval: 5s
      retries: 3
    depends_on:
      analytics:
        condition: service_healthy
    environment:
      # Binds nestjs listener to both IPv4 and IPv6 network interfaces
      HOSTNAME: &quot;::&quot;

      STUDIO_PG_META_URL: http://meta:8080
      POSTGRES_PORT: ${POSTGRES_PORT}
      POSTGRES_HOST: ${POSTGRES_HOST}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}

      DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
      DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
      OPENAI_API_KEY: ${OPENAI_API_KEY:-}

      SUPABASE_URL: http://kong:8000
      SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
      SUPABASE_ANON_KEY: ${ANON_KEY}
      SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
      AUTH_JWT_SECRET: ${JWT_SECRET}

      # LOGFLARE_API_KEY is deprecated
      LOGFLARE_API_KEY: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
      LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
      LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}

      LOGFLARE_URL: http://analytics:4000
      NEXT_PUBLIC_ENABLE_LOGS: true
      # Comment to use Big Query backend for analytics
      NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
      # Uncomment to use Big Query backend for analytics
      # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery
      SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
      EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
    volumes:
      - ./volumes/snippets:/app/snippets:Z
      - ./volumes/functions:/app/edge-functions:Z

  kong:
    container_name: supabase-kong
    image: kong:2.8.1
    restart: unless-stopped
    ports:
      - ${KONG_HTTP_PORT}:8000/tcp
      - ${KONG_HTTPS_PORT}:8443/tcp
    volumes:
      # https://github.com/supabase/supabase/issues/12661
      - ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
    depends_on:
      analytics:
        condition: service_healthy
    environment:
      KONG_DATABASE: &quot;off&quot;
      KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
      # https://github.com/supabase/cli/issues/14
      KONG_DNS_ORDER: LAST,A,CNAME
      KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction
      KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
      KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
      SUPABASE_ANON_KEY: ${ANON_KEY}
      SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
      DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
      DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
    # https://unix.stackexchange.com/a/294837
    entrypoint: bash -c 'eval &quot;echo \&quot;$$(cat ~/temp.yml)\&quot;&quot; &amp;gt; ~/kong.yml &amp;amp;&amp;amp; /docker-entrypoint.sh kong docker-start'

  auth:
    container_name: supabase-auth
    image: supabase/gotrue:v2.185.0
    restart: unless-stopped
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;wget&quot;,
          &quot;--no-verbose&quot;,
          &quot;--tries=1&quot;,
          &quot;--spider&quot;,
          &quot;http://localhost:9999/health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      GOTRUE_API_HOST: 0.0.0.0
      GOTRUE_API_PORT: 9999
      API_EXTERNAL_URL: ${API_EXTERNAL_URL}

      GOTRUE_DB_DRIVER: postgres
      GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}

      GOTRUE_SITE_URL: ${SITE_URL}
      GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
      GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}

      GOTRUE_JWT_ADMIN_ROLES: service_role
      GOTRUE_JWT_AUD: authenticated
      GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
      GOTRUE_JWT_EXP: ${JWT_EXPIRY}
      GOTRUE_JWT_SECRET: ${JWT_SECRET}

      GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
      GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
      GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}

      # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile.
      # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true

      # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true
      # GOTRUE_SMTP_MAX_FREQUENCY: 1s
      GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
      GOTRUE_SMTP_HOST: ${SMTP_HOST}
      GOTRUE_SMTP_PORT: ${SMTP_PORT}
      GOTRUE_SMTP_USER: ${SMTP_USER}
      GOTRUE_SMTP_PASS: ${SMTP_PASS}
      GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
      GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
      GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
      GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
      GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}

      GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
      GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
      # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook

      # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: &quot;true&quot;
      # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: &quot;pg-functions://postgres/public/custom_access_token_hook&quot;
      # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: &quot;&amp;lt;standard-base64-secret&amp;gt;&quot;

      # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: &quot;true&quot;
      # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI: &quot;pg-functions://postgres/public/mfa_verification_attempt&quot;

      # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: &quot;true&quot;
      # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: &quot;pg-functions://postgres/public/password_verification_attempt&quot;

      # GOTRUE_HOOK_SEND_SMS_ENABLED: &quot;false&quot;
      # GOTRUE_HOOK_SEND_SMS_URI: &quot;pg-functions://postgres/public/custom_access_token_hook&quot;
      # GOTRUE_HOOK_SEND_SMS_SECRETS: &quot;v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n&quot;

      # GOTRUE_HOOK_SEND_EMAIL_ENABLED: &quot;false&quot;
      # GOTRUE_HOOK_SEND_EMAIL_URI: &quot;http://host.docker.internal:54321/functions/v1/email_sender&quot;
      # GOTRUE_HOOK_SEND_EMAIL_SECRETS: &quot;v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n&quot;

  rest:
    container_name: supabase-rest
    image: postgrest/postgrest:v14.3
    restart: unless-stopped
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
      PGRST_DB_ANON_ROLE: anon
      PGRST_JWT_SECRET: ${JWT_SECRET}
      PGRST_DB_USE_LEGACY_GUCS: &quot;false&quot;
      PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
      PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
    command:
      [
        &quot;postgrest&quot;
      ]

  realtime:
    # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
    container_name: realtime-dev.supabase-realtime
    image: supabase/realtime:v2.72.0
    restart: unless-stopped
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    healthcheck:
      test:
        [
          &quot;CMD-SHELL&quot;,
          &quot;curl -sSfL --head -o /dev/null -H \&quot;Authorization: Bearer ${ANON_KEY}\&quot; http://localhost:4000/api/tenants/realtime-dev/health&quot;
        ]
      timeout: 5s
      interval: 30s
      retries: 3
      start_period: 10s
    environment:
      PORT: 4000
      DB_HOST: ${POSTGRES_HOST}
      DB_PORT: ${POSTGRES_PORT}
      DB_USER: supabase_admin
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_NAME: ${POSTGRES_DB}
      DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
      DB_ENC_KEY: supabaserealtime
      API_JWT_SECRET: ${JWT_SECRET}
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      ERL_AFLAGS: -proto_dist inet_tcp
      DNS_NODES: &quot;''&quot;
      RLIMIT_NOFILE: &quot;10000&quot;
      APP_NAME: realtime
      SEED_SELF_HOST: &quot;true&quot;
      RUN_JANITOR: &quot;true&quot;
      DISABLE_HEALTHCHECK_LOGGING: &quot;true&quot;

  # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up
  storage:
    container_name: supabase-storage
    image: supabase/storage-api:v1.33.5
    restart: unless-stopped
    volumes:
      - ./volumes/storage:/var/lib/storage:z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;wget&quot;,
          &quot;--no-verbose&quot;,
          &quot;--tries=1&quot;,
          &quot;--spider&quot;,
          &quot;http://storage:5000/status&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      rest:
        condition: service_started
      imgproxy:
        condition: service_started
    environment:
      ANON_KEY: ${ANON_KEY}
      SERVICE_KEY: ${SERVICE_ROLE_KEY}
      POSTGREST_URL: http://rest:3000
      PGRST_JWT_SECRET: ${JWT_SECRET}
      DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      REQUEST_ALLOW_X_FORWARDED_PATH: &quot;true&quot;
      FILE_SIZE_LIMIT: 52428800
      STORAGE_BACKEND: file
      FILE_STORAGE_BACKEND_PATH: /var/lib/storage
      TENANT_ID: stub
      # TODO: https://github.com/supabase/storage-api/issues/55
      REGION: stub
      GLOBAL_S3_BUCKET: stub
      ENABLE_IMAGE_TRANSFORMATION: &quot;true&quot;
      IMGPROXY_URL: http://imgproxy:5001

  imgproxy:
    container_name: supabase-imgproxy
    image: darthsim/imgproxy:v3.30.1
    restart: unless-stopped
    volumes:
      - ./volumes/storage:/var/lib/storage:z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;imgproxy&quot;,
          &quot;health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    environment:
      IMGPROXY_BIND: &quot;:5001&quot;
      IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
      IMGPROXY_USE_ETAG: &quot;true&quot;
      IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION}
      IMGPROXY_MAX_SRC_RESOLUTION: 16.8

  meta:
    container_name: supabase-meta
    image: supabase/postgres-meta:v0.95.2
    restart: unless-stopped
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      PG_META_PORT: 8080
      PG_META_DB_HOST: ${POSTGRES_HOST}
      PG_META_DB_PORT: ${POSTGRES_PORT}
      PG_META_DB_NAME: ${POSTGRES_DB}
      PG_META_DB_USER: supabase_admin
      PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
      CRYPTO_KEY: ${PG_META_CRYPTO_KEY}

  functions:
    container_name: supabase-edge-functions
    image: supabase/edge-runtime:v1.70.0
    restart: unless-stopped
    volumes:
      - ./volumes/functions:/home/deno/functions:Z
    depends_on:
      analytics:
        condition: service_healthy
    environment:
      JWT_SECRET: ${JWT_SECRET}
      SUPABASE_URL: http://kong:8000
      SUPABASE_ANON_KEY: ${ANON_KEY}
      SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
      SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786
      VERIFY_JWT: &quot;${FUNCTIONS_VERIFY_JWT}&quot;
    command:
      [
        &quot;start&quot;,
        &quot;--main-service&quot;,
        &quot;/home/deno/functions/main&quot;
      ]

  analytics:
    container_name: supabase-analytics
    image: supabase/logflare:1.30.3
    restart: unless-stopped
    ports:
      - 4000:4000
    # Uncomment to use Big Query backend for analytics
    # volumes:
    #   - type: bind
    #     source: ${PWD}/gcloud.json
    #     target: /opt/app/rel/logflare/bin/gcloud.json
    #     read_only: true
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;curl&quot;,
          &quot;http://localhost:4000/health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 10
    depends_on:
      db:
        # Disable this if you are using an external Postgres database
        condition: service_healthy
    environment:
      LOGFLARE_NODE_HOST: 127.0.0.1
      DB_USERNAME: supabase_admin
      DB_DATABASE: _supabase
      DB_HOSTNAME: ${POSTGRES_HOST}
      DB_PORT: ${POSTGRES_PORT}
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_SCHEMA: _analytics
      LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
      LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
      LOGFLARE_SINGLE_TENANT: true
      LOGFLARE_SUPABASE_MODE: true

      # Comment variables to use Big Query backend for analytics
      POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
      POSTGRES_BACKEND_SCHEMA: _analytics
      LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
      # Uncomment to use Big Query backend for analytics
      # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID}
      # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER}

  # Comment out everything below this point if you are using an external Postgres database
  db:
    container_name: supabase-db
    image: supabase/postgres:15.8.1.085
    restart: unless-stopped
    ports:
      - &quot;54322:5432&quot;
    volumes:
      - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
      # Must be superuser to create event trigger
      - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
      # Must be superuser to alter reserved role
      - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
      # Initialize the database settings with JWT_SECRET and JWT_EXP
      - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
      # PGDATA directory is persisted between restarts
      - ./volumes/db/data:/var/lib/postgresql/data:Z
      # Changes required for internal supabase data such as _analytics
      - ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z
      # Changes required for Analytics support
      - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
      # Changes required for Pooler support
      - ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z
      # Use named volume to persist pgsodium decryption key between restarts
      - db-config:/etc/postgresql-custom
    healthcheck:
      test:
        [
        &quot;CMD&quot;,
        &quot;pg_isready&quot;,
        &quot;-U&quot;,
        &quot;postgres&quot;,
        &quot;-h&quot;,
        &quot;localhost&quot;
        ]
      interval: 5s
      timeout: 5s
      retries: 10
    depends_on:
      vector:
        condition: service_healthy
    environment:
      POSTGRES_HOST: /var/run/postgresql
      PGPORT: ${POSTGRES_PORT}
      POSTGRES_PORT: ${POSTGRES_PORT}
      PGPASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATABASE: ${POSTGRES_DB}
      POSTGRES_DB: ${POSTGRES_DB}
      JWT_SECRET: ${JWT_SECRET}
      JWT_EXP: ${JWT_EXPIRY}
    command:
      [
        &quot;postgres&quot;,
        &quot;-c&quot;,
        &quot;config_file=/etc/postgresql/postgresql.conf&quot;,
        &quot;-c&quot;,
        &quot;log_min_messages=fatal&quot; # prevents Realtime polling queries from appearing in logs
      ]

  vector:
    container_name: supabase-vector
    image: timberio/vector:0.28.1-alpine
    restart: unless-stopped
    volumes:
      - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
      - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;wget&quot;,
          &quot;--no-verbose&quot;,
          &quot;--tries=1&quot;,
          &quot;--spider&quot;,
          &quot;http://vector:9001/health&quot;
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    environment:
      LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
    command:
      [
        &quot;--config&quot;,
        &quot;/etc/vector/vector.yml&quot;
      ]
    security_opt:
      - &quot;label=disable&quot;

  # Update the DATABASE_URL if you are using an external Postgres database
  supavisor:
    container_name: supabase-pooler
    image: supabase/supavisor:2.7.4
    restart: unless-stopped
    ports:
      - ${POSTGRES_PORT}:5432
      - ${POOLER_PROXY_PORT_TRANSACTION}:6543
    volumes:
      - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z
    healthcheck:
      test:
        [
          &quot;CMD&quot;,
          &quot;curl&quot;,
          &quot;-sSfL&quot;,
          &quot;--head&quot;,
          &quot;-o&quot;,
          &quot;/dev/null&quot;,
          &quot;http://127.0.0.1:4000/api/health&quot;
        ]
      interval: 10s
      timeout: 5s
      retries: 5
    depends_on:
      db:
        condition: service_healthy
      analytics:
        condition: service_healthy
    environment:
      PORT: 4000
      POSTGRES_PORT: ${POSTGRES_PORT}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
      CLUSTER_POSTGRES: true
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      VAULT_ENC_KEY: ${VAULT_ENC_KEY}
      API_JWT_SECRET: ${JWT_SECRET}
      METRICS_JWT_SECRET: ${JWT_SECRET}
      REGION: local
      ERL_AFLAGS: -proto_dist inet_tcp
      POOLER_TENANT_ID: ${POOLER_TENANT_ID}
      POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE}
      POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN}
      POOLER_POOL_MODE: transaction
      DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE}
    command:
      [
        &quot;/bin/sh&quot;,
        &quot;-c&quot;,
        &quot;/app/bin/migrate &amp;amp;&amp;amp; /app/bin/supavisor eval \&quot;$$(cat /etc/pooler/pooler.exs)\&quot; &amp;amp;&amp;amp; /app/bin/server&quot;
      ]

volumes:
  db-config:&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 3. 컨테이너 실행&lt;/h3&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ docker-compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 4. 접속 테스트 및 환경 확인&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:3000  (Supabase Studio 접속)
http://localhost:5432  (PostgreSQL 접속, ID: postgres, PW: postgres)
http://localhost:8000/rest/v1  (자동 생성된 API)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>나의 주니어 개발 일기/DB</category>
      <category>Baas</category>
      <category>docker</category>
      <category>PostgreSQL</category>
      <category>Self-hosting</category>
      <category>supabase</category>
      <category>도커설치</category>
      <category>백엔드</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/433</guid>
      <comments>https://pulpul8282.tistory.com/433#entry433comment</comments>
      <pubDate>Mon, 2 Feb 2026 18:06:07 +0900</pubDate>
    </item>
    <item>
      <title>언제 Go 언어가 괜찮을까?</title>
      <link>https://pulpul8282.tistory.com/432</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;
&lt;h1&gt;Go 언어&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go&amp;nbsp;언어는&amp;nbsp;goroutine&amp;nbsp;기반&amp;nbsp;병행성&amp;nbsp;모델과&amp;nbsp;빠른&amp;nbsp;컴파일&amp;nbsp;속도를&amp;nbsp;바탕으로&amp;nbsp;I/O&amp;nbsp;중심&amp;nbsp;서버에&amp;nbsp;강점을&amp;nbsp;가진다.&amp;nbsp; &lt;br /&gt;Spring&amp;nbsp;Boot&amp;nbsp;환경에서&amp;nbsp;운영&amp;nbsp;중인&amp;nbsp;서비스&amp;nbsp;중&amp;nbsp;일부는&amp;nbsp;Go로&amp;nbsp;전환했을&amp;nbsp;때&amp;nbsp;성능과&amp;nbsp;비용&amp;nbsp;측면에서&amp;nbsp;더&amp;nbsp;큰&amp;nbsp;이점을&amp;nbsp;얻을&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp; &lt;br /&gt;필자는 Spring Boot 를 주로 사용하는데 그러면 만약 Go로 전환하면 가장 효과가 좋은 영역은 어디인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1. 트래픽 많고 단순한 API 서버&lt;/h3&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;조회(Read) 위주의 API&lt;/li&gt;
&lt;li&gt;단순 CRUD&lt;/li&gt;
&lt;li&gt;인증 토큰 검증&lt;/li&gt;
&lt;li&gt;외부 시스템 중계 API (Proxy)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 Go가 좋은가&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;JVM 힙/GC 부담 없음&lt;/li&gt;
&lt;li&gt;메모리 사용량 작음&lt;/li&gt;
&lt;li&gt;빠른 cold start &amp;rarr; 컨테이너/서버리스에 유리&lt;/li&gt;
&lt;li&gt;TPS 대비 비용 효율 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Spring Boot API Gateway / Edge API &amp;rarr; Go&lt;/b&gt;&lt;br /&gt;(특히 BFF, API Gateway 성격이면 매우 좋음)&lt;/p&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 메시지 기반 Consumer / Worker&lt;/h3&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;Kafka / RabbitMQ / IBM MQ Consumer&lt;/li&gt;
&lt;li&gt;이벤트 처리기&lt;/li&gt;
&lt;li&gt;비동기 작업 Worker&lt;/li&gt;
&lt;li&gt;로그 처리, 변환기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 Go가 좋은가&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;goroutine으로 동시성 처리 매우 쉬움&lt;/li&gt;
&lt;li&gt;long-running process에 안정적&lt;/li&gt;
&lt;li&gt;CPU/메모리 효율 뛰어남&lt;/li&gt;
&lt;li&gt;컨테이너로 여러 개 띄우기 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Spring @KafkaListener / @JmsListener &amp;rarr; Go Consumer&lt;/b&gt;&lt;/p&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. 배치성 작업 / 스케줄러&lt;/h3&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;/li&gt;
&lt;li&gt;파일 변환&lt;/li&gt;
&lt;li&gt;외부 API polling&lt;/li&gt;
&lt;li&gt;ETL 전처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 Go가 좋은가&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;크론 + Go 조합 단순&lt;/li&gt;
&lt;li&gt;Spring Batch 대비 구조 단순&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Spring Batch &amp;rarr; Go CLI / Worker&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ Go로 전환해도 되지만 신중해야 할 영역&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 4. 비즈니스 로직이 복잡한 핵심 도메인&lt;/h3&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;/li&gt;
&lt;li&gt;정산&lt;/li&gt;
&lt;li&gt;복잡한 트랜잭션&lt;/li&gt;
&lt;li&gt;DDD 기반 도메인 로직&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring의 강점:
&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;AOP&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;JPA/Hibernate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Go는 직접 다 만들어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;이 영역은 Spring 유지 + 주변부만 Go 전환 추천&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 5. 관리자 페이지 / 백오피스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring + JPA + Security 조합이 생산성 좋음&lt;/li&gt;
&lt;li&gt;Go는 ORM, Admin UI 모두 직접 구성 필요&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;❌ 6. 복잡한 ORM 기반 대규모 CRUD&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA Entity 많음&lt;/li&gt;
&lt;li&gt;Lazy loading 의존&lt;/li&gt;
&lt;li&gt;복잡한 연관관계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이걸 Go로 옮기면 &lt;b&gt;생산성 급감&lt;/b&gt;&lt;/p&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;go 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.Go 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://go.dev/dl/&quot;&gt;https://go.dev/dl/&lt;/a&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tzRSC/dJMcahb6pfn/nZvA4M7kB8z5mHIXVm7Eq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tzRSC/dJMcahb6pfn/nZvA4M7kB8z5mHIXVm7Eq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tzRSC/dJMcahb6pfn/nZvA4M7kB8z5mHIXVm7Eq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtzRSC%2FdJMcahb6pfn%2FnZvA4M7kB8z5mHIXVm7Eq1%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;1180&quot; height=&quot;772&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.환경변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go를 설치할때 기본적으로 c드라이브에 설치하면 환경변수 작업을 안해줘도 되지만 내가 원하는 위치에 설치하고 싶을 경우 환경변수 설정을 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 d드라이브에 설치했기 때문에 &lt;b&gt;GOROOT 변수&lt;/b&gt;에 대한 경로를 잡아줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고)원하는 위치에 go 설치시 go 폴더를 먼저 만들어주고 그 안에 go를 설치해야 한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1kZ4M/dJMcaihNi2w/K6BOG3dHtwNxoJnyI45Nrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1kZ4M/dJMcaihNi2w/K6BOG3dHtwNxoJnyI45Nrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1kZ4M/dJMcaihNi2w/K6BOG3dHtwNxoJnyI45Nrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1kZ4M%2FdJMcaihNi2w%2FK6BOG3dHtwNxoJnyI45Nrk%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;615&quot; height=&quot;523&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.vs code에서 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.go 파일을 만들어주고 터미널에서 go 버전을 확인하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상확인이 되었으면 잘 세팅 되었다는것이다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;$ go version&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;406&quot; data-origin-height=&quot;43&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GLocX/dJMcafFm8JX/irVWJpk7Tx7hwCh8uDwsO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GLocX/dJMcafFm8JX/irVWJpk7Tx7hwCh8uDwsO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GLocX/dJMcafFm8JX/irVWJpk7Tx7hwCh8uDwsO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGLocX%2FdJMcafFm8JX%2FirVWJpk7Tx7hwCh8uDwsO0%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;406&quot; height=&quot;43&quot; data-origin-width=&quot;406&quot; data-origin-height=&quot;43&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.go의 폴더구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;go 1.10 이전버전(GOPATH 방식)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go에서 프로젝트 파일을 만드려고 한다면 일단&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src 하위에 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 주로 도메인/유저이름 구조로 생성되도록 권장된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;GOPATH
 └─ src
     └─ github.com
         └─ username
             └─ myapp
                 └─ main.go&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.17+(GO Modules 방식)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;D:\projects\myapp
 ├─ go.mod
 ├─ main.go
 └─ internal/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔ GOPATH 필요 없음&lt;br /&gt;✔ src 필요 없음&lt;br /&gt;✔ 디렉토리 자유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go 모듈으로 실행시 초기설정&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;go mod init learngo //루트 폴더 설정&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;go 문법&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;접근자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대문자로 시작하는 메서드는 public 메서드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;something.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package something

import &quot;fmt&quot;

/*
*  private
*/
func sayBye() {
    fmt.Println(&quot;Bye&quot;)
}

/*
*  public
*/
func SayHello() {
    fmt.Println(&quot;Hello&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;

    &quot;github.com/learngo/something&quot;
)

func main() {
    fmt.Println(&quot;Hello World!&quot;)
    something.SayHello()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eP2gOb/dJMcaa48cP7/jii0ASVgunyFXNdCRXJOq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eP2gOb/dJMcaa48cP7/jii0ASVgunyFXNdCRXJOq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eP2gOb/dJMcaa48cP7/jii0ASVgunyFXNdCRXJOq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeP2gOb%2FdJMcaa48cP7%2Fjii0ASVgunyFXNdCRXJOq1%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;457&quot; height=&quot;68&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변수와 상수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go는 타입언어이다.&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;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    const name string = &quot;jyj&quot;
    //const name = &quot;jyj&quot;  이런식으로 타입 추론도 가능
    fmt.Println(name)
}

//결과
jyj
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상수로 선언했기 때문에 변경할 수 없고 변경시 에러가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func main() {
    const name string = &quot;jyj&quot;
    name = &quot;test&quot; //컴파일 단계 에러 발생
    fmt.Println(name)
}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법1&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func main() {
    var name string = &quot;jyj&quot;
    name = &quot;test&quot;
    fmt.Println(name)
}

//결과
test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법2&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 타입 추론을 이용하여 선언도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    name := &quot;jyj&quot;
    name = &quot;test&quot;
    fmt.Println(name)
}&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;name 값을 string으로 선언했가 때문에 다음에 name에 bool 값인 false를 넣으면 에러가 난다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    name := &quot;jyj&quot;
    name = false //컴파일 단계 에러 발생
    fmt.Println(name)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&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://go101.org/article/type-system-overview.html&quot;&gt;https://go101.org/article/type-system-overview.html&lt;/a&gt;&lt;/p&gt;
&lt;br /&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;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func multiply(a int, b int) int {
    return a * b
}

func main() {
    fmt.Println(multiply(2, 2))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터 a,b의 타입이 같다면 아래와 같이 작성 가능하다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func multiply(a,b int) int {
    return a * b
}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;멀티 value&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go는 다른 언어와 달리 다양한 타입을 동시에 return 해줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;strings&quot;
)

func lenAndUpper(name string) (int, string) {
    return len(name), strings.ToUpper(name)
}

func main() {
    totalLength, upperName := lenAndUpper(&quot;yjy&quot;)
    fmt.Println(totalLength, upperName)
}

//결과 
3 YJY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리턴 개수가 여러개일때 1개의 값만 받고 싶다면, 언더바(_)를 이용하자&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func main() {
    totalLength, _ := lenAndUpper(&quot;yjy&quot;)
    fmt.Println(totalLength)
}

//결과 
3&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러개의 파라미터 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터에 &lt;b&gt;...&lt;/b&gt; 선언만 해주면 여러 파라미터를 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func repeatMe(words ...string) {
    fmt.Println(words)
}

func main() {
    repeatMe(&quot;yjy&quot;, &quot;test&quot;, &quot;hi&quot;, &quot;hello&quot;)
}

//결과
[yjy test hi hello]&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;naked return&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return 만 적어주고 대상을 선언 할 필요가 없다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 V1을 naked return을 적용해 V2로 만들었다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;import (
    &quot;fmt&quot;
    &quot;strings&quot;
)

func lenAndUpperV1(name string) (int, string) {
    return len(name), strings.ToUpper(name)
}

func lenAndUpperV2(name string) (lenght int, uppercase string) {
    lenght = len(name)
    uppercase = strings.ToUpper(name)
    return
}

func main() {
    totalLength, upperName := lenAndUpperV2(&quot;yjy&quot;)
    fmt.Println(totalLength, upperName)
}

//결과
3 YJY
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;defer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;function이 끝났을때 다른 작업을 실행시키고 싶을때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lenAndUpperV2의 작업이 모두 끝나고 나서 &lt;code&gt;defer fmt.Println(&quot;I'm done&quot;)&lt;/code&gt;이 실행된다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;strings&quot;
)

func lenAndUpperV2(name string) (lenght int, uppercase string) {
    defer fmt.Println(&quot;I'm done&quot;)
    lenght = len(name)
    uppercase = strings.ToUpper(name)
    return
}

func main() {
    totalLength, upperName := lenAndUpperV2(&quot;yjy&quot;)
    fmt.Println(totalLength, upperName)
}

//결과
I'm done
3 YJY&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;for, range, ...args&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go의 기본 loop는 오로지 for만 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;for 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
)

func superAdd(numbers ...int) int {
    for i := 0; i &amp;lt; len(numbers); i++ {
        fmt.Println(i)
    }
    return 1
}

func main() {
    superAdd(1, 2, 3, 4, 5, 6)
}
//결과
0
1
2
3
4
5&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;range 키워드 사용&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;angelscript&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
)

func superAdd(numbers ...int) int {
    for number := range numbers {
        fmt.Println(number)
    }
    return 1
}

func main() {
    superAdd(1, 2, 3, 4, 5, 6)
}

//결과
0
1
2
3
4
5&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;angelscript&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
)

func superAdd(numbers ...int) int {
    for index, number := range numbers {
        fmt.Println(index, number)
    }
    return 1
}

func main() {
    superAdd(1, 2, 3, 4, 5, 6)
}

//결과
0 1
1 2
2 3
3 4
4 5
5 6&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;fortran&quot;&gt;&lt;code&gt;func superAdd(numbers ...int) int {
    for _, number := range numbers {
        fmt.Println(index, number)
    }
    return 1
}&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;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func superAdd(numbers ...int) int {
    total := 0
    for _, number := range numbers {
        total += number
    }
    return total
}

func main() {
    result := superAdd(1, 2, 3, 4, 5, 6)
    fmt.Println(result)
}

//결과
21&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;if, else&lt;/h3&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func canIDrink(age int) bool {
    if age &amp;lt; 18 {
        return false
    } else {
        return true
    }
}

func main() {
    fmt.Println(canIDrink(16))
}

//결과
false&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;if에 변수 적용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func canIDrink(age int) bool {
    if koreanAge := age + 2; koreanAge &amp;lt; 18 {
        return false
    } else {
        return true
    }
}

func main() {
    fmt.Println(canIDrink(16))
}
//결과
true&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Switch&lt;/h3&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func canIDrinkV1(age int) bool {
    switch age {
    case 10:
        return false
    case 18:
        return true
    }
    return false
}

func canIDrinkV2(age int) bool {
    switch {
    case age &amp;lt; 18:
        return false
    case age == 18:
        return true
    case age &amp;gt; 50:
        return false
    }
    return false
}

func canIDrinkV3(age int) bool {
    switch koreanAge := age + 2; koreanAge {
    case 10:
        return false
    case 18:
        return true
    }
    return false
}

func main() {
    fmt.Println(canIDrinkV1(18))
    fmt.Println(canIDrinkV2(18))
    fmt.Println(canIDrinkV3(18))
}

//결과
true
true
false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pointer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;amp;는 메모리 주소&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;func main() {
    a := 2
    b := &amp;amp;a
    a = 10

    fmt.Println(a, b)
    fmt.Println(&amp;amp;a, b)
}

//결과
10 0xc00000a098
0xc00000a098 0xc00000a098&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*은 메모리 주소가 가르키는 실제값을 가르킨다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    a := 2
    b := &amp;amp;a
    a = 5

    fmt.Println(*b)
}

//결과
5&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;func change(x *int) {
    *x = 20
}

a := 10
change(&amp;amp;a)
fmt.Println(a) // 20&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    a := 2
    b := &amp;amp;a
    *b = 20

    fmt.Println(a) //20
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Arrays and Slices&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Arrays&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    //사이즈 선언
    namesV1 := [3]string{&quot;yjy&quot;, &quot;hi&quot;, &quot;hello&quot;}
    namesV1[3] = &quot;lala&quot; //에러

    //사이즈 미선언시 slice 라고도 부른다
    namesV2 := []string{&quot;yjy&quot;, &quot;hi&quot;, &quot;hello&quot;}
    namesV2[3] = &quot;lala&quot;

    fmt.Println(namesV1)
    fmt.Println(namesV2)
}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Slices&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    names := []string{&quot;yjy&quot;, &quot;hi&quot;, &quot;hello&quot;}
    fmt.Println(names)

    names = append(names, &quot;test&quot;)
    fmt.Println(names)
}

//결과
[yjy hi hello]
[yjy hi hello test]&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Maps&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map[키의타입 선언]value의타입선언&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    yjy := map[string]string{&quot;name&quot;: &quot;yjy&quot;, &quot;age&quot;: &quot;30&quot;}
    fmt.Println(yjy)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요소확인 여부&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;struct&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

type person struct {
    name    string
    age     int
    favFood []string
}

func main() {
    favFood := []string{&quot;chicken, pizza&quot;}
    // 가독성 좋음
    yjy := person{name: &quot;yjy&quot;, age: 30, favFood: favFood}
    // 가독성 않좋음
    // yjy := person{&quot;yjy&quot;, 30, favFood}
    fmt.Println(yjy)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;struct에서도 소문자면 private 대문자면 public 이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;struct의 속성도 마찬가지이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;//private
type person struct {
    name    string
    age     int
    favFood []string
}

//public
type Person struct {
    Name    string
    Age     int
    FavFood []string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;function을 이용한 생성자 개념만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go에는 생성자가 따로 없다. 그래서 만들어 줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;accounts.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package accounts

// Account struct
type Account struct {
    owner   string
    balance int
}

// NewAccount creates Account
// &amp;amp;account을 리턴함으로서 새로 만들어진 account의 주소값을 반환한다.
// 늘 복사하면 프로그램 속도는 느려진다.
func NewAccount(owner string) *Account {
    account := Account{owner: owner, balance: 0}
    return &amp;amp;account
}

/*
    ✔️ 값 타입으로 리턴하면
    func NewAccount(owner string) Account {
        return Account{owner: owner, balance: 0}
    }


    호출 시점에 Account 전체가 복사
    필드가 많아질수록 복사 비용 증가
    이후 수정 시 또 복사 발생 가능

    ✔️ 포인터로 리턴하면
    주소값(8바이트)만 복사
    구조체 크기와 무관
    동일 객체를 여러 곳에서 공유 가능
      그래서 구조체가 클수록 포인터 반환이 유리
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;

    &quot;github.com/learngo/accounts&quot;
)

func main() {
    account := accounts.NewAccount(&quot;yjy&quot;)
    fmt.Println(account)
}

//결과
&amp;amp;{yjy 0}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;receiver&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deposit 이라는 함수는 a라는 receiver를 갖고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;receiver는 struct의 첫 글자를 따서 소문자로 지어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;struct 가 Account 라면은 a 로 지어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 개념이라고 생각하면 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;accounts.go&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deposit 이라는 메서드를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func NewAccount(owner string) *Account {
    account := Account{owner: owner, balance: 0}
    return &amp;amp;account
}

func (a Account) Deposit(amount int) {
    a.balance += amount
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;

    &quot;github.com/learngo/accounts&quot;
)

func main() {
    account := accounts.NewAccount(&quot;yjy&quot;)
    account.Deposit(10)

    fmt.Println(account.Balance())
}

//결과
0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 0이 나왔다 왜그럴까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NewAccount를 통해서 account 값을 먼저 만들었다. 그리고 Deposit을 통해 balance를 증가시키려고 한다. 아래 Deposit의 (a Account)에서 go는 복사값을 만든다. 그래서 원본값이 변하지 않는것이다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func NewAccount(owner string) *Account {
    account := Account{owner: owner, balance: 0}
    return &amp;amp;account
}

func (a Account) Deposit(amount int) {
    a.balance += amount
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 메서드안에서 안전하게 복사값을 만들어서 핸들링하고 싶다면 이처럼 써도 되지만 원본값이 변경되야 하는 경우는 변경되야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변경후&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func (a *Account) Deposit(amount int) {
    a.balance += amount
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;...
func main() {
    account := accounts.NewAccount(&quot;yjy&quot;)
    account.Deposit(10)

    fmt.Println(account.Balance())
}

//결과
10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 10으로써 의도대로 동작한다!.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;error 핸들링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;go는 exception이 따로 없기 때문에 error를 직접 핸들링 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;accounts.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;...
// Withdraw x amount from your account
func (a *Account) Withdraw(amount int) error {
    if a.balance &amp;lt; amount {
        return errors.New(&quot;돈이 부족합니다&quot;)
    }
    a.balance -= amount
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;github.com/learngo/accounts&quot;
)

func main() {
    account := accounts.NewAccount(&quot;yjy&quot;)
    account.Deposit(10)

    fmt.Println(account.Balance())

    err := account.Withdraw(20)
    //프로그램 종료
    //log.Fatalln(err)

    //주로 error 핸들링 방법
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(account.Balance())
}

//결과
10
돈이 부족합니다
10&lt;/code&gt;&lt;/pre&gt;
&lt;br /&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;error 들을 변수로 선언하고 사용하자&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;...
var errNoMoney = errors.New(&quot;돈이 없습니다.&quot;)

// Withdraw x amount from your account
func (a *Account) Withdraw(amount int) error {
    if a.balance &amp;lt; amount {
        return errNoMoney
    }
    a.balance -= amount
    return nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고사항) nil에 대해&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;nil&lt;/code&gt;이 될 수 있는 타입들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go에서 &lt;code&gt;nil&lt;/code&gt;은 &lt;b&gt;&amp;ldquo;아무것도 가리키지 않음(없음)&amp;rdquo;&lt;/b&gt; 을 의미하는 &lt;b&gt;미리 정의된 식별자(predeclared identifier)&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ nil 가능&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;*T&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;슬라이스 (&lt;code&gt;[]T&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;맵 (&lt;code&gt;map[K]V&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;채널 (&lt;code&gt;chan T&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;함수 (&lt;code&gt;func&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;인터페이스 (&lt;code&gt;interface&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ nil 불가&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;int&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt;, &lt;code&gt;struct&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;String 재정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obj 호출시 go는 내부적으로 String 함수를 호출하게 되는데 해당 함수를 재정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;accounts.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;...
func (a Account) String() string {
    return fmt.Sprint(a.Owner(), &quot;'s account. \nHas: &quot;, a.Balance())
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func main() {
    account := accounts.NewAccount(&quot;yjy&quot;)

    fmt.Println(account)
}

//결과
yjy's account. 
Has: 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dictionary&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;struct 말고도 Dictionary를 통해 타입을 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mydict.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package mydict

// Dictionary  type
type Dictionary map[string]string&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;

    &quot;github.com/learngo/mydict&quot;
)

func main() {
    dictionary := mydict.Dictionary{&quot;first&quot;: &quot;First word&quot;}
    fmt.Println(dictionary)
}

//결과
map[first:First word]&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Dictionary만의 메서드 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Search라는 메서드를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mydict.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package mydict

import &quot;errors&quot;

// Dictionary  type
type Dictionary map[string]string

var errNotFound = errors.New(&quot;Not Found&quot;)

// Search for a word
func (d Dictionary) Search(word string) (string, error) {
    value, exists := d[word]
    if exists {
        return value, nil
    }
    return &quot;&quot;, errNotFound
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;

    &quot;github.com/learngo/mydict&quot;
)

func main() {
    dictionary := mydict.Dictionary{&quot;first&quot;: &quot;First word&quot;}
    definition, err := dictionary.Search(&quot;second&quot;)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(definition)
    }
}


//결과
Not Found&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Add 메서드 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mydict.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;...
var errNotFound = errors.New(&quot;Not Found&quot;)
var errWordExists = errors.New(&quot;Word already Exists&quot;)

func (d Dictionary) Add(word, def string) error {
    _, err := d.Search(word)
    switch err {
    case errNotFound:
        d[word] = def
    case nil:
        return errWordExists
    }
    return nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;github.com/learngo/mydict&quot;
)

func main() {
    dictionary := mydict.Dictionary{&quot;first&quot;: &quot;First word&quot;}
    word := &quot;hello&quot;
    definition := &quot;Greeting&quot;

    err := dictionary.Add(word, definition)
    if err != nil {
        fmt.Println(err)
    }

    hello, _ := dictionary.Search(word)
    fmt.Println(&quot;found&quot;, word, &quot;, definition:&quot;, hello)

    err2 := dictionary.Add(word, definition)
    if err2 != nil {
        fmt.Println(err2)
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Update 메서드 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mydict.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;...
var errNotFound = errors.New(&quot;Not Found&quot;)
var errWordExists = errors.New(&quot;Word already Exists&quot;)
var errCantUpdate = errors.New(&quot;Cant update non-existing word&quot;)

// Update a word
func (d Dictionary) Update(word, def string) error {
    _, err := d.Search(word)
    switch err {
    case nil:
        d[word] = def
    case errNotFound:
        return errCantUpdate
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.do&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;

    &quot;github.com/learngo/mydict&quot;
)

func main() {
    dictionary := mydict.Dictionary{&quot;first&quot;: &quot;First word&quot;}
    word := &quot;hello&quot;

    dictionary.Add(word, &quot;First&quot;)
    dictionary.Update(word, &quot;Second&quot;)

    result, _ := dictionary.Search(word)
    fmt.Println(result)
}

//결과
Second
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Delete 메서드 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mydict.go&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;...
func (d Dictionary) Delete(word string) {
    delete(d, word)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;

    &quot;github.com/learngo/mydict&quot;
)

func main() {
    dictionary := mydict.Dictionary{&quot;first&quot;: &quot;First word&quot;}
    word := &quot;hello&quot;

    dictionary.Add(word, &quot;First&quot;)
    dictionary.Update(word, &quot;Second&quot;)
    result, _ := dictionary.Search(word)
    fmt.Println(result)

    dictionary.Delete(word)
    result, err := dictionary.Search(word)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}

//결과
Second
Not Found&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;URL Checker 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 순서대로 URL을 처리하는 기능을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;errors&quot;
    &quot;fmt&quot;
    &quot;net/http&quot;
)

func main() {
    var results = map[string]string{}

    urls := []string{
        &quot;https://www.google.com/&quot;,
        &quot;https://www.naver.com/&quot;,
        &quot;https://www.reddit.com/&quot;,
        &quot;https://www.airbnb.com/&quot;,
    }

    for _, url := range urls {
        result := &quot;OK&quot;
        err := hitUrl(url)
        if err != nil {
            result = &quot;FAILED&quot;
        }
        results[url] = result
    }

    for url, result := range results {
        fmt.Println(url, result)
    }
}

var errRequestFailed = errors.New(&quot;Request Failed&quot;)

func hitUrl(url string) error {
    fmt.Println(&quot;Checing: &quot;, url)
    resp, err := http.Get(url)
    if err != nil || resp.StatusCode &amp;gt;= 400 {
        fmt.Println(err, resp.StatusCode)
        return errRequestFailed
    }
    return nil

}

//결과
Checing:  https://www.google.com/
Checing:  https://www.naver.com/
Checing:  https://www.reddit.com/
Checing:  https://www.airbnb.com/
https://www.google.com/ OK
https://www.naver.com/ OK
https://www.reddit.com/ OK
https://www.airbnb.com/ OK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 알면 좋은 부분은 아래부분을&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;var results = map[string]string{}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 써도 된다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;var results = make(map[string]string)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;capacity 값이 고정이라면 이렇게도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;results := make(map[string]string, 1000)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 케이스도 있다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;var results map[string]string

fmt.Println(results == nil) // true
results[&quot;a&quot;] = &quot;1&quot;          // panic!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화를 하지 않고 선언을 할 수 있다. 선언만 하면 아무의미 없는 값으로 nil 이라는 값을 갖고 있지만 값을 추가하는 행동따윈 할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Goroutines&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고루틴이란 기본적으로 다른 함수와 동시에 실행시키는 함수&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 순차적으로 동작하는 Top-down 방식이 일반적인 프로그래밍 방식이다. 총 20초가 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    sexyCount(&quot;yjy&quot;)
    sexyCount(&quot;kkk&quot;)
}

func sexyCount(person string) {
    for i := 0; i &amp;lt; 10; i++ {
        fmt.Println(person, &quot;is sexy&quot;, i)
        time.Sleep(time.Second)
    }
}

//결과
yjy is sexy 0
yjy is sexy 1
yjy is sexy 2
yjy is sexy 3
yjy is sexy 4
yjy is sexy 5
yjy is sexy 6
yjy is sexy 7
yjy is sexy 8
yjy is sexy 9
kkk is sexy 0
kkk is sexy 1
kkk is sexy 2
kkk is sexy 3
kkk is sexy 4
kkk is sexy 5
kkk is sexy 6
kkk is sexy 7
kkk is sexy 8
kkk is sexy 9&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고루틴을 적용하면 병행처리하여 10초에 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에 go 키워드만 추가해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;func main() {
    go sexyCount(&quot;yjy&quot;)
    sexyCount(&quot;kkk&quot;)
}
...

//결과
kkk is sexy 0
yjy is sexy 0
yjy is sexy 1
kkk is sexy 1
kkk is sexy 2
yjy is sexy 2
yjy is sexy 3
kkk is sexy 3
kkk is sexy 4
yjy is sexy 4
yjy is sexy 5
kkk is sexy 5
kkk is sexy 6
yjy is sexy 6
yjy is sexy 7
kkk is sexy 7
kkk is sexy 8
yjy is sexy 8
yjy is sexy 9
kkk is sexy 9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 주의할 점이 있다. 이렇게 go 키워드를 모두 추가하여 더이상 main 함수가 할일이 없어지게되면 main 함수 자체가 바로 종료되어서 아무 작업도 할 수 없게 된다. &lt;b&gt;이것의 고루틴의 약속이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func main() {
    go sexyCount(&quot;yjy&quot;)
    go sexyCount(&quot;kkk&quot;)
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 방식은 고루틴이 yjy를 담당하고 kkk를 main이 작업을 담당했기 때문에 가능햇던것이다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;func main() {
    go sexyCount(&quot;yjy&quot;)
    sexyCount(&quot;kkk&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘다 go 키워드를 적용해보고 main에게 따로 할일을 주면 동작할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main에게 5초동안 기다리라는 작업을 주었고 정말 5초동안 고루틴에의해 병렬동시 실행이 된것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;func main() {
    go sexyCount(&quot;yjy&quot;)
    go sexyCount(&quot;kkk&quot;)
    time.Sleep(time.Second * 5)
}

//결과
kkk is sexy 0
yjy is sexy 0
yjy is sexy 1
kkk is sexy 1
kkk is sexy 2
yjy is sexy 2
yjy is sexy 3
kkk is sexy 3
kkk is sexy 4
yjy is sexy 4&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func main(){
    go playYadong()
    go sex()
    time.Sleep(time.Second * 5)
}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Channels&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;channel은 고루틴과 메인함수 사이 혹은 고루틴끼리의 정보를 전달하기 위한 방법이다.&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고루틴이 동작하기 위해서는 결국 main 함수가 어떤일을 하고 있어야만 실행이 가능한데 항상 sleep만 하고 있을수는 없는일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 channel을 이용하여 고루틴으로 부터 완료된 내용을 수신하는 역할을 main 함수가 하면 될것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 핵심은 main 함수에게 할일을 주어 고루틴의 작업이 멈추지 않도록 하는것.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    c := make(chan bool) // bool 타입의 결과를 받을 채널 선언

    people := [4]string{&quot;yjy&quot;, &quot;kkk&quot;, &quot;jjj&quot;, &quot;mmm&quot;}
    for _, person := range people {
        go isSexy(person, c)
    }

    for i := 0; i &amp;lt; len(people); i++ {
        fmt.Println(&amp;lt;-c) // 채널로부터 값을 가져오는 오퍼레이션은 blocking 오퍼레이션이다,
    }
}

func isSexy(person string, c chan bool) {
    time.Sleep(time.Second * 5)
    c &amp;lt;- true // 채널에게 true값을 전달
}

//결과
waiting for  0
mmm is sexy
waiting for  1
jjj is sexy
waiting for  2
kkk is sexy
waiting for  3
yjy is sexy
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고루틴의 Concurrency 때문에 people의 배열 순서와 결과값의 순서가 같지 않게됨을 확인 할 수 있다.&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;code&gt;c&amp;lt;-&quot;data&quot;&lt;/code&gt; 데이터를 채널에 넣는다. &lt;code&gt;&amp;lt;-c&lt;/code&gt; 데이터를 채널에서 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고루틴,채널을 적용하여 URL Checker 다시 만들어보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;errors&quot;
    &quot;fmt&quot;
    &quot;net/http&quot;
)

func main() {
    var results = map[string]string{}

    urls := []string{
        &quot;https://www.google.com/&quot;,
        &quot;https://www.naver.com/&quot;,
        &quot;https://www.reddit.com/&quot;,
        &quot;https://www.airbnb.com/&quot;,
    }

    for _, url := range urls {
        result := &quot;OK&quot;
        err := hitUrl(url)
        if err != nil {
            result = &quot;FAILED&quot;
        }
        results[url] = result
    }

    for url, result := range results {
        fmt.Println(url, result)
    }
}

var errRequestFailed = errors.New(&quot;Request Failed&quot;)

func hitUrl(url string) error {
    fmt.Println(&quot;Checing: &quot;, url)
    resp, err := http.Get(url)
    if err != nil || resp.StatusCode &amp;gt;= 400 {
        fmt.Println(err, resp.StatusCode)
        return errRequestFailed
    }
    return nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고루틴 적용후 main.go&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;package main

import (
    &quot;errors&quot;
    &quot;fmt&quot;
    &quot;net/http&quot;
)

type result struct {
    url    string
    status string
}

var errRequestFailed = errors.New(&quot;Request Failed&quot;)

func main() {
    results := map[string]string{}
    c := make(chan result)

    urls := []string{
        &quot;https://www.google.com/&quot;,
        &quot;https://www.naver.com/&quot;,
        &quot;https://www.reddit.com/&quot;,
        &quot;https://www.airbnb.com/&quot;,
    }

    for _, url := range urls {
        go hitUrl(url, c)
    }

    for i := 0; i &amp;lt; len(urls); i++ {
        result := &amp;lt;-c
        results[result.url] = result.status
    }

    for url, status := range results {
        fmt.Println(url, status)
    }
}

// 해당 채널은 받을 수는 없고 보내기만 가능(c chan&amp;lt;- result)
func hitUrl(url string, c chan&amp;lt;- result) {
    fmt.Println(&quot;Checing: &quot;, url)
    // fmt.Println(&amp;lt;-c), 에러발생!, 채널을 c chan&amp;lt;- result 이런식으로 받을수만 있게 선언했기 때문에 채널에서 꺼내올수는 없다.

    resp, err := http.Get(url)
    status := &quot;OK&quot;
    if err != nil || resp.StatusCode &amp;gt;= 400 {
        status = &quot;FAILED&quot;
    }

    c &amp;lt;- result{url: url, status: status}
}
&lt;/code&gt;&lt;/pre&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;BY 윤주영.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&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;a href=&quot;https://nomadcoders.co/go-for-beginners/&quot;&gt;https://nomadcoders.co/go-for-beginners/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Language/Go</category>
      <category>golang</category>
      <category>Go언어</category>
      <category>springboot</category>
      <category>백엔드</category>
      <category>서버개발</category>
      <author>추억을 백앤드하자</author>
      <guid isPermaLink="true">https://pulpul8282.tistory.com/432</guid>
      <comments>https://pulpul8282.tistory.com/432#entry432comment</comments>
      <pubDate>Tue, 20 Jan 2026 10:18:10 +0900</pubDate>
    </item>
  </channel>
</rss>