<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DevYGwan</title>
    <link>https://swmobenz.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 28 Jul 2026 13:11:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>YGwan</managingEditor>
    <image>
      <title>DevYGwan</title>
      <url>https://tistory1.daumcdn.net/tistory/5367548/attach/a3a6b3e63b954ff591f489dec05ee0e8</url>
      <link>https://swmobenz.tistory.com</link>
    </image>
    <item>
      <title>Terraform vs Pulumi &amp;mdash; 멀티 클라우드(AWS + NCP) 소규모 팀의 IaC 도구 선정기</title>
      <link>https://swmobenz.tistory.com/64</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 관리형 K8s(EKS/NKS) 없이 인스턴스 기반 K8s 노드 3개를 직접 운영하고 있습니다. 관리형 k8s을 사용하지 않는 이유는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;관리형 서비스의 업데이트 주기에 맞춰 저희도 관리해야한다.&lt;/li&gt;
&lt;li&gt;AWS 뿐만아니라 NCP, Azure 등 여러 cloud 서비스에서 운영해야 되는데 그때마다 관리형 서비스를 각각 개별적으로 관리해야한다. (현재는 AWS, NCP만 사용)&lt;/li&gt;
&lt;li&gt;Onprem 서비스로 직접 k8s 노드 3개로 직접 서비스를 운영해야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이 있습니다. 물론 비용적인 측면에서도 관리형 서비스를 사용하는 것보다 직접 운영하는게 더 저렴하기 때문에도 있습니다. 따라서 이렇게 여러 환경에서 직접 K8s 노드로 운영하다보니 동일한 구성을 AWS와 NCP(네이버 클라우드) 등 여러 클라우드 서비스에서| 반복해야 한다는 것. 콘솔 클릭으로 두 번씩 만들다 보면 환경 간 드리프트는 시간 문제라, IaC 도입을 결정하기로 결정했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Iac에는 여러 종류가 있지만 크게 Terraform과 Pulumi를 비교했습니다. 결론부터 말하자면, &lt;b&gt;Terraform&lt;/b&gt;(OpenTofu)&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 선택했습니다. 흥미로운 건 &quot;Pulumi가 기술적으로 더 못해서&quot;가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;우리 제약 조건 세 가지가 전부 Terraform 쪽을 가리켰기 때문이라는 점입니다. 같은 비교라도 제약이 다르면 결론은 뒤집힐 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;IaC(Infrastructure as Code) &amp;nbsp;&lt;br /&gt;- 서버&amp;middot;네트워크&amp;middot;로드밸런서 같은 인프라를 코드로 선언하고, 도구가 그 선언대로 실제 인프라를 만들어 주는 방식&lt;br /&gt;- 인프라 구성이 코드가 되는 순간 버전 관리&amp;middot;코드 리뷰&amp;middot;재현(같은 코드 &amp;rarr; 같은 환경)이 가능해짐&lt;/blockquote&gt;
&lt;h2 data-heading=&quot;1. 비교 전에 제약 조건부터 &amp;mdash; 판단 기준을 고정하기&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-heading=&quot;1. 비교 전에 제약 조건부터 &amp;mdash; 판단 기준을 고정하기&quot; data-ke-size=&quot;size26&quot;&gt;1. 비교 전에 제약 조건부터 &amp;mdash; 판단 기준을 고정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;두개의 기술에 대한 비교 글은 많았지만, 대부분 일반론이라 제 상황에 대입하기는 어려웠습니다.. 그래서 비교 전에 우리 환경의 제약을 먼저 고정했습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 69px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.8605%;&quot;&gt;제약&lt;/td&gt;
&lt;td style=&quot;width: 78.1395%;&quot;&gt;내용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 21.8605%; height: 18px;&quot;&gt;클라우드&lt;/td&gt;
&lt;td style=&quot;width: 78.1395%; height: 18px;&quot;&gt;AWS + NCP 등 멀티 클라우드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 21.8605%; height: 17px;&quot;&gt;팀&lt;/td&gt;
&lt;td style=&quot;width: 78.1395%; height: 17px;&quot;&gt;소규모&amp;nbsp;(인프라&amp;nbsp;전담&amp;nbsp;인력&amp;nbsp;없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 21.8605%; height: 17px;&quot;&gt;환경&lt;/td&gt;
&lt;td style=&quot;width: 78.1395%; height: 17px;&quot;&gt;Private&amp;nbsp;&amp;mdash;&amp;nbsp;외부&amp;nbsp;SaaS&amp;nbsp;의존&amp;nbsp;최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 21.8605%; height: 17px;&quot;&gt;대상&lt;/td&gt;
&lt;td style=&quot;width: 78.1395%; height: 17px;&quot;&gt;인스턴스&amp;nbsp;기반&amp;nbsp;K8s&amp;nbsp;노드&amp;nbsp;3개&amp;nbsp;+&amp;nbsp;NLB&amp;nbsp;(관리형&amp;nbsp;K8s&amp;nbsp;미사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;핵심 워크플로우는 단순합니다. 인스턴스 3개 생성 &amp;rarr; NLB 생성 &amp;rarr; Target Group 연결 &amp;rarr; (필요 시) Security Group/Subnet 구성. 이걸 두 클라우드에서 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Terraform vs Pulmi&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Terraform과 Pulumi 는 대표적인 Iac 툴 중 하나입니다. 간단하게 이것에 대해서 설명드리자면 이 2개 모두 선언적 모델과 State를 기반으로 동작합니다. Terraform과&amp;nbsp;Pulumi는&amp;nbsp;둘&amp;nbsp;다&amp;nbsp;&quot;인프라가&amp;nbsp;어떤&amp;nbsp;상태여야&amp;nbsp;하는가(desired&amp;nbsp;state)&quot;를&amp;nbsp;정의하면,&amp;nbsp;엔진이&amp;nbsp;현재&amp;nbsp;상태와의&amp;nbsp;차이(diff)를&amp;nbsp;계산해&amp;nbsp;필요한&amp;nbsp;변경만&amp;nbsp;실행하는&amp;nbsp;선언형&amp;nbsp;모델입니다..&amp;nbsp;이를&amp;nbsp;위해&amp;nbsp;두&amp;nbsp;도구&amp;nbsp;모두:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;State 파일: 도구가 관리 중인 실제 인프라의 스냅샷. &quot;코드 &amp;harr; 실제 인프라&quot;를 대조하는 기준점이라 유실되면 안 되고, 팀이 공유하려면 원격 저장소(예: S3)와 동시 실행 잠금(locking)이 필요하다.&lt;/li&gt;
&lt;li&gt;미리보기 &amp;rarr; 적용 워크플로우: Terraform은 terraform plan &amp;rarr; apply, Pulumi는 pulumi preview &amp;rarr; up. 무엇이 생성/변경/삭제될지 먼저 보여주고 실행한다.&lt;/li&gt;
&lt;li&gt;Provider 플러그인: 각 클라우드 API를 감싸는 어댑터. 도구 본체가 아니라 Provider가 &quot;AWS를 아는가, NCP를 아는가&quot;를 결정한다 &amp;mdash; 그래서 이 글의 비교에서 Provider 지원 품질이 핵심 축이 된다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Terraform
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HashiCorp가 2014년 공개한 IaC의 사실상 표준.&lt;/li&gt;
&lt;li&gt;인프라를 HCL(HashiCorp Configuration Language) 이라는 전용 선언형 DSL로 기술&lt;/li&gt;
&lt;li&gt;&quot;무엇을 원하는지&quot;를 설정 파일처럼 적는 방식이라 배우기 쉽고 읽기 쉬운 대신, 프로그래밍 언어가 아니라서 복잡한 로직 표현엔 한계가 있다.&lt;/li&gt;
&lt;li&gt;2023년 라이선스가 BSL로 전환되면서 커뮤니티가 오픈소스 포크인 OpenTofu를 만들었고, 기존 Terraform 코드와 호환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pulumi
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2018년 등장한 후발 주자로, 핵심 아이디어는&amp;nbsp;&lt;b&gt;&quot;인프라를 전용 DSL이 아니라 TypeScript/Python/Go/C# 같은 범용 프로그래밍 언어로 정의하자&quot;&lt;/b&gt;&amp;nbsp;는 것.&lt;/li&gt;
&lt;li&gt;반복문&amp;middot;조건문&amp;middot;함수&amp;middot;타입&amp;middot;테스트 프레임워크 등 언어 생태계를 그대로 인프라 코드에 쓸 수 있다. (코드가 명령형처럼 보여도 실행 모델은 Terraform과 같은 선언형이다.)&lt;/li&gt;
&lt;li&gt;프로그램을 실행하면 리소스 그래프(desired state)가 만들어지고, 엔진이 diff를 계산해 적용한다. forEach로 인스턴스를 만들었다고 매번 새로 생성되는 게 아니라, &quot;인스턴스 3개가 있어야 한다&quot;는 선언이 되는 것이다.&lt;/li&gt;
&lt;li&gt;Provider 생태계는 자체 네이티브 Provider와 함께,&amp;nbsp;&lt;b&gt;Terraform Provider를 감싸서 쓰는 브릿지 방식&lt;/b&gt;으로 커버리지를 확보한다 &amp;mdash; 뒤에서 보겠지만 이 구조가 NCP 같은 마이너 클라우드에서 양날의 검이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 기술 모두 실행 모델(선언형 + State)은 같고, 본질적 차이는 &quot;인프라를 무슨 언어로 정의하는가&quot;(전용 DSL vs 범용 언어)와 그에 따른 생태계 구조입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면, 간단히 제가 이 2개의 모델 중 Terraform을 선택한 이유에 대해서 설명드리도록 하겠습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;2. Terraform &amp;mdash; &amp;quot;공식 Provider&amp;quot;와 &amp;quot;State 자체 통제&amp;quot;의 힘&quot; data-ke-size=&quot;size26&quot;&gt;3. Terraform &amp;mdash; &quot;공식 Provider&quot;와 &quot;State 자체 통제&quot;의 힘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ NCP 공식 Provider가 존재합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;AWS Provider야 가장 성숙한 Provider니 논외고, 결정적인 건 NCP입니다. Terraform 공식 &lt;a href=&quot;https://registry.terraform.io/browse/providers&quot; data-tooltip-position=&quot;top&quot;&gt;Provider Registry&lt;/a&gt;에는 각 클라우드/서비스 벤더의 공식 Provider가 등록돼 관리되는데, NCP의 navercloudplatform Provider도 여기에 공식 제공합니다. 문서&amp;middot;예제가 Terraform 기준으로 작성돼 있으며, NCP + Terraform 조합의 한국어 트러블슈팅 자료까지 존재합니다. 멀티 클라우드 IaC에서는 &quot;마이너한 쪽 클라우드의 지원 품질&quot;이 실질적 병목이 되기 때문에 공식적으로 제공하고 자료도 많은 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ State를 완전히 자체 통제할 수 있다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Private 환경이라 State를 외부 SaaS에 둘 수 없는데, S3 Backend로 내부에서 처리가 가능합니다. 전통적으로는 동시 실행 잠금(locking)을 위해 S3 + DynamoDB 조합이 필수였지만 현재는 deprecated 예정이고, Terraform 1.10부터는 S3의 Conditional Writes를 활용한 네이티브 locking이 도입돼 DynamoDB 없이 S3만으로 잠금까지 해결됩니다. use_lockfile = true 한 줄이면 처리가 완료돼 쉽게 환경 설정이 가능하다는 장점이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;terraform {
  backend &quot;s3&quot; {
    bucket       = &quot;my-tfstate&quot;
    key          = &quot;prod/terraform.tfstate&quot;
    region       = &quot;ap-northeast-2&quot;
    encrypt      = true
    use_lockfile = true   # Terraform 1.10+ &amp;mdash; DynamoDB 불필요
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ 소규모 팀에는 HCL의 단순함이 오히려 장점이다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;선언적 DSL이라 학습 곡선이 낮고, 인프라 전담이 아닌 팀원도 코드 리뷰가 가능한 수준의 가독성이 나옵니다. 별도 런타임(Node.js/Python) 설치도 불필요하다. 공통 패턴(K8s 노드 3개 + NLB)은 클라우드별 모듈로 분리해 재사용 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;infra/
├── aws/          # aws-k8s-cluster 모듈 호출
├── ncp/          # ncp-k8s-cluster 모듈 호출
└── modules/
    ├── aws-k8s-cluster/
    └── ncp-k8s-cluster/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 약점도 명확합니다. 복잡한 조건 로직에서 HCL은 금방 한계가 오고(count/for_each/dynamic 중첩 시 가독성 급락), State에 민감 정보가 평문으로 저장되며, BSL 라이선스 변경 우려가 있습니다. 하지만 저희의 인프라 구조 상 기본적인 HCL로도 충분히 가능하고 암호화 처리로 인한 평문 저장 문제를 해결할 수 있고 OpenTofu 대체를 통해 라이센스 변경 우려도 해결할 수 있기 때문에 큰 문제가 되진 않았습니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;3. Pulumi &amp;mdash; 좋은 도구지만, 우리 제약과 어긋난 지점&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-heading=&quot;3. Pulumi &amp;mdash; 좋은 도구지만, 우리 제약과 어긋난 지점&quot; data-ke-size=&quot;size26&quot;&gt;3. Pulumi &amp;mdash; 좋은 도구지만, 우리 제약과 어긋난 지점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;범용 언어의 표현력&lt;/b&gt;: 노드별 역할(master/worker)&amp;middot;환경별 설정 분기를 TypeScript의 조건문/반복문으로 깔끔하게 처리할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const nodeConfigs = [
  { role: &quot;master&quot;, type: &quot;t3.medium&quot; },
  { role: &quot;worker&quot;, type: &quot;t3.small&quot; },
  { role: &quot;worker&quot;, type: &quot;t3.small&quot; },
];

nodeConfigs.forEach((cfg, i) =&amp;gt; {
  new aws.ec2.Instance(`node-${i}`, {
    instanceType: cfg.type,
    tags: { Role: cfg.role },
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테스트 용이성&lt;/b&gt;: jest/pytest 같은 기존 프레임워크로 인프라 코드를 Mock 기반 유닛 테스트할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;민감 정보 자동 암호화&lt;/b&gt;: pulumi.secret() 선언만으로 State에 암호화 저장 &amp;mdash; 보안 기본값은 Terraform보다 낫다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입 시스템&lt;/b&gt;: 잘못된 속성명&amp;middot;타입 불일치를 plan 전에 IDE에서 잡는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 우리 제약과 충돌하는 지점이 셋 있었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;NCP 지원이 브릿지 Provider&lt;/b&gt; &amp;mdash; Terraform NCP Provider를 브릿지해 쓰는 구조라 문서가 부족하고 업데이트가 늦을 수 있다. NCP 이슈 발생 시 레퍼런스를 찾기 어렵다. &lt;b&gt;이게 현재 상황에서 가장 큰 리스크 였습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;State 관리의 트레이드오프&lt;/b&gt; &amp;mdash; Pulumi Cloud(SaaS)는 Private 환경 정책과 충돌하고, 셀프 호스팅(S3)은 동시 접근 잠금(locking)을 별도로 풀어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;런타임 의존성과 커뮤니티&lt;/b&gt; &amp;mdash; CI/CD까지 Node.js 런타임이 필요하고, NCP + Pulumi 조합 사례는 거의 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;4. 비교 요약&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-heading=&quot;4. 비교 요약&quot; data-ke-size=&quot;size26&quot;&gt;4. 비교 요약&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;평가 항목&lt;/td&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;td&gt;Pulumi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NCP 지원 안정성&lt;/td&gt;
&lt;td&gt;✅ 공식 Provider&lt;/td&gt;
&lt;td&gt;⚠️ 브릿지 (리스크)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS 지원&lt;/td&gt;
&lt;td&gt;✅ 최고 수준&lt;/td&gt;
&lt;td&gt;✅ 우수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 곡선&lt;/td&gt;
&lt;td&gt;낮음 (HCL)&lt;/td&gt;
&lt;td&gt;중간 (언어는 쉬우나 IaC 패턴 적응 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복잡한 로직&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;자유로움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State 자체 관리&lt;/td&gt;
&lt;td&gt;✅ S3 + DynamoDB&lt;/td&gt;
&lt;td&gt;⚠️ SaaS or 셀프 호스팅 (locking 주의)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private 환경 적합성&lt;/td&gt;
&lt;td&gt;✅ 외부 의존 없음&lt;/td&gt;
&lt;td&gt;⚠️ Pulumi Cloud 사용 시 외부 통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테스트&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;우수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;민감 정보 보안&lt;/td&gt;
&lt;td&gt;⚠️ 평문 (암호화 Backend 필요)&lt;/td&gt;
&lt;td&gt;✅ 자동 암호화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커뮤니티/레퍼런스&lt;/td&gt;
&lt;td&gt;✅ 압도적 (NCP 포함)&lt;/td&gt;
&lt;td&gt;❌ 부족 (특히 NCP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소규모 팀 운영 부담&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-heading=&quot;5. 결론 &amp;mdash; 그리고 결론이 뒤집힐 조건&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-heading=&quot;5. 결론 &amp;mdash; 그리고 결론이 뒤집힐 조건&quot; data-ke-size=&quot;size26&quot;&gt;5. 결론 &amp;mdash; 그리고 결론이 뒤집힐 조건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform(OpenTofu)을 했습니다. 근거는 크게 네 가지로,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;NCP 공식 Provider(멀티 클라우드 양쪽 안정 지원은 Terraform뿐)&lt;/li&gt;
&lt;li&gt;Private 환경에서 State 완전 자체 관리&lt;/li&gt;
&lt;li&gt;소규모 팀엔 HCL의 단순함이 장점&lt;/li&gt;
&lt;li&gt;NCP 조합의 한국어 레퍼런스 존재.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;중요한 건 이 결론이 조건부라는 점이다. 다음 조건이 바뀌면 Pulumi를 재검토 할 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀에 개발자가 늘고 인프라 로직이 복잡해질 때 (범용 언어의 표현력&amp;middot;테스트가 이득으로 전환)&lt;/li&gt;
&lt;li&gt;NCP 비중이 줄고 AWS 중심으로 재편될 때 (브릿지 Provider 리스크 소멸)&lt;/li&gt;
&lt;li&gt;Pulumi의 NCP Provider가 성숙해졌을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/64</guid>
      <comments>https://swmobenz.tistory.com/64#entry64comment</comments>
      <pubDate>Sun, 19 Jul 2026 23:55:08 +0900</pubDate>
    </item>
    <item>
      <title>Keycloak과 DB를 한 트랜잭션처럼 &amp;mdash; 외부 인증 서버 연동에 보상 트랜잭션(SAGA) 적용기</title>
      <link>https://swmobenz.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;사내 B2B 플랫폼에 &quot;여러 사용자를 한 번에 추가하는 API&quot;를 만들면서 마주친 분산 데이터 정합성 문제와, 이를 경량 보상 트랜잭션으로 푼 의사결정 과정을 정리하려고 합니다. 저희는 사용자를 관리할 때 크게 2곳에서 관리합니다. 인증만을 관리하는 keycloak 서버 &amp;amp; 유저의 개인 정보를 갖고 있는 DB(Postgres)에서 관리합니다. 사용자 생성은 Keycloak(인증 서버) 과 우리 서비스 DB(Postgres) 두 곳에 일어납니다. 두개의 서버는 하나의 트랜잭션을 공유할 수 없어 특정 과정에서 문제가 생기면, 데이터 정합성에 문제가 생기는 경우가 발생합니다. 예를 들어, Keycloak 생성은 성공했는데 DB에 생성이 안되게 되면, 인증 서버에 고아(orphan) 계정이 남게 되는 경우가 생길 수 있습니다. 따라서 이러한 정합성 문제를 해결하기 위해 SAGA의 보상 트랜잭션 개념으로 풀되, DB가 1개뿐인 우리 상황에 맞춰 Spring 트랜잭션 동기화(afterCompletion) 로 &quot;DB 커밋 실패 시 Keycloak 생성을 되돌리는&quot; 경량 구조를 택했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;1. 문제 상황&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 문제 상황&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp;관리자가 사용자 목록을 한 번에 등록하는 기능. 사용자 1명은 두 시스템에 만들어집니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1. Keycloak      : 인증 계정 생성 (인증에 필요한 유저 정보)
2. Service DB    : UserEntity 저장 (우리 서비스에 필요한 유저 정보)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;비즈니스적으로는 이 둘이 하나처럼 처리돼야 데이터 정합성이 맞춰지기 때문에, 한쪽만 만들어지거나 한쪽만 삭제되면 안됩니다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;왜 &amp;#96;@Transactional&amp;#96; 하나로 안 되는가&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;왜 @Transactional 하나로 안 되는가&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;@Transactional은 우리 DB(Postgres) 트랜잭션만 관장합니다. Keycloak은 REST API로 호출하는 완전히 별개의 시스템이라 같은 트랜잭션에 참여하지 못합니다. 그래서 다음이 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1. Keycloak에 계정 생성 성공   (외부 호출, 트랜잭션 밖에서 이미 확정됨)
2. DB에 UserEntity 저장 시도
3. DB commit 단계에서 실패      (예: email unique 제약 위반)
4. DB 트랜잭션 롤백 &amp;rarr; 우리 DB엔 사용자 없음
5. 그러나 Keycloak엔 계정이 그대로 남음&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과는 Keycloak에 orphan(고아) 계정이 생기게 됩니다. 우리 DB엔 매칭되는 row가 없는데 인증 서버엔 로그인 가능한 계정이 떠 있는, 두 저장소의 불일치 상태가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;까다로운 지점: DB의 unique 제약 위반은 flush/commit 시점, 즉 서비스 메서드 본문이 리턴된 뒤 터집니다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;그래서 단순히 메서드 안의 단순 try/catch로는 이 케이스를 잡을 수 없다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-heading=&quot;2. 개념 정리 &amp;mdash; SAGA 패턴과 보상 트랜잭션&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot; data-heading=&quot;2. 개념 정리 &amp;mdash; SAGA 패턴과 보상 트랜잭션&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 이러한 문제를 해결하기 위해 분산 환경에서의 트랜잭션 처리를 통해 이러한 데이터 정합성을 해결하려고 합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;2. 개념 정리 &amp;mdash; SAGA 패턴과 보상 트랜잭션&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 개념 정리 &amp;mdash; SAGA 패턴과 보상 트랜잭션&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;분산 트랜잭션 문제&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 분산 트랜잭션 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모놀리식에서는 단일 DB가 ACID로 원자성을 보장합니다. 하지만 작업이 여러 시스템(DB, 외부 API&amp;hellip;)에 걸치면 하나의 ACID 트랜잭션으로 묶을 수 없습니다. 트랜잭션이란 하나의 데이터베이스의 상태를 변화시키기 위해 수행하는 &amp;lsquo;작업의 논리적 단위&amp;rsquo;이기 때문입니다. 따라서 여러 DB를 사용할 경우 하나가 실패해도 이미 처리된 작업이 남아 정합성이 깨집니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이건 단순히 MSA만의 얘기가 아닙니다. 단일 서비스라도 외부 API(여기선 Keycloak)를 끼는 순간, 여러 서버로 관리되는 서비스에서 겪은 것과 본질적으로 같은 분산 트랜잭션 문제가 생깁니다. 규모만 다를 뿐 구조는 동일하기 때문에 분산 트랜잭션 문제가 발생할 수 있습니다. (실제로 외부 API 자체가 별도의 서버를 호출하는거기 때문에)&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;크게 분산 트랜잭션 문제를 해결하는 방식은 2PC(Two-Phase Commit)과 SAGA 패턴이 있습니다. 저희는 이중 SAGA 패턴을 사용해서 이러한 문제를 해결했습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;SAGA = 로컬 트랜잭션 + 보상 트랜잭션&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;SAGA = 로컬 트랜잭션 + 보상 트랜잭션&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2PC(Two-Phase Commit)란&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #000000;&quot;&gt;2PC는 코디네이터(트랜잭션 매니저) 한 명과 참여자(리소스 매니저, 보통 DB들) 여럿으로 구성됩니다. 이름처럼 커밋을 두 단계로 쪼갭니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2PC는 이름 그대로 두 단계로 커밋합니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;투표 단계(Prepare) : 코디네이터가 모든 참여자에게 &quot;커밋 가능?&quot;을 묻고, 각 참여자는 트랜잭션을 열어둔 채 가부를 답한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커밋 단계(Commit) : 전원이 &quot;가능&quot;이면 코디네이터가 커밋 명령을, 단 하나라도 &quot;불가&quot;면 롤백 명령을 보낸다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문제는 이 구조의 비용이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;- 구현&amp;middot;운영 복잡
- 성능 부담 (가장 느린 참여자의 투표까지 락을 유지)
- 장애 시 블로킹 (coordinator 다운 &amp;rarr; 전원 대기)
- 모든 시스템이 XA를 지원하지 않음 &amp;larr; Keycloak이 정확히 이 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참여자가 늘어날수록 이 비용은 가파르게 커집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가장 느린 참여자의 투표까지 락을 유지하기 때문에 낮은 가용성과 낮은 확장성을 가집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 코디네이터가 모든 참여자의 트랜잭션을 관리하기 때문에 강한 일관성을 가집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 높은 트래픽과 확장을 전제하는 시스템일수록 2PC를 피하고 SAGA로 간다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;SAGA = 로컬 트랜잭션 + 보상 트랜잭션&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SAGA = 로컬 트랜잭션 + 보상 트랜잭션&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SAGA는 여러 작업을 하나의 큰 트랜잭션처럼 묶는 대신, 각각의 작업을 작은 로컬 트랜잭션들의 흐름으로 나눕니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 중간에 실패하면 이미 성공한 작업에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #ee2323;&quot;&gt;&lt;b&gt;보상 트랜잭션&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;을 실행합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;따라서 강한 일관성을 포기하고 최종 일관성(eventual consistency) 을 택해 높은 가용성과 확장성을 가지게 합니다.&lt;/li&gt;
&lt;li&gt;작동 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 단계는 자기 시스템의 로컬 트랜잭션으로 수행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중간에 실패하면, 성공한 단계들을 보상 트랜잭션(compensating transaction) 으로 되돌린다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심은 보상이 DB rollback이 아니라 비즈니스적 역작업이라는 점이다 (이미 commit된 건 rollback 불가).&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정방향 action &amp;amp; 보상 action 예시&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정방향 Action&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보상 Action&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주문 생성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주문 취소&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결제 승인&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결제 취소&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재고 차감&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재고 복구&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Keycloak 계정 생성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Keycloak 계정 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;두 가지 조율 방식&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;두 가지 조율 방식&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 가지 조율 방식&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 134px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 11.6279%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;구분&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.6278%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Choreography&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.628%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Orchestration&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 11.6279%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작동 방식&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.6278%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 서비스가 이벤트를 주고받으며 자율 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.628%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #000000; text-align: start;&quot;&gt;중앙 Orchestrator가 흐름&amp;middot;보상을 지휘&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 11.6279%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결합도&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.6278%; height: 20px;&quot;&gt;&lt;span&gt;중앙 제어자 없음 &amp;rarr; 단일 장애점 없음, 결합도 낮음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.628%; height: 20px;&quot;&gt;&lt;span&gt;흐름이 한곳에 모여 상태 추적 쉬움 -&amp;gt; 오케스트러에 의존&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 11.6279%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;디버깅&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.6278%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어려움&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.628%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쉬움&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 11.6279%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;복잡도&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.6278%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스가 적을 땐 단순하지만, 많아질수록 어려움&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.628%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;흐름을 한곳에서 관리하기 때문에 서비스가 많을 때 유리함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;저희는 Orchestration을 택했습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;따라서 UserService가 &quot;Keycloak 생성 &amp;rarr; User DB 저장 &amp;rarr; 실패 시 Keycloak 삭제&quot;를 직접 지휘합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;3. 의사결정 &amp;mdash; 풀 SAGA를 가지 않은 이유&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;3. 의사결정 &amp;mdash; 풀 SAGA를 가지 않은 이유&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의사결정 &amp;mdash; 풀 SAGA를 가지 않은 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;실무에서는 멱등성 + 상태 테이블 + 재처리 스케줄러를 한 세트로 가져가는 SAGA를 권장합니다. 강력하지만 그만큼 복잡하고 무겁죠. 저희 상황을 냉정히 보면 이 처리가 다 필요하진 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 테이블이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;필수적이지 않은 이유&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 테이블은 &quot;이 SAGA가 지금 어느 단계까지 갔나&quot;를 외부에 영속화해, 중간에 죽어도 다시 주워서 이어가기 위한 겁니다. 그런데 우리 단계는 사실상 외부 1 + DB 1뿐이고, 흐름이 단일 트랜잭션 안에서 동기로 끝납니다. 즉 DB 커밋 성공/실패 그 자체가 이미 완벽한 상태 표현이에요. 커밋됐으면 끝난 것, 안 됐으면 보상한 것이기 때문에 별도 상태 머신을 둘 만큼 단계가 많지도, 비동기로 흩어지지도 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재처리 스케줄러가 필수적이지 않은 이유&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재처리 스케줄러는 상태 테이블에 &quot;멈춰있는 SAGA&quot;가 쌓인다는 전제에서 그걸 주기적으로 긁어 재시도하는 장치입니다. 위에서 상태 테이블을 안 두기로 한 순간, 재처리할 대상 자체가 없습니다. 우리는 실패하면 그 자리에서 afterCompletion이 동기로 보상하거나, 보상마저 실패하면 알람으로 사람에게 넘깁니다. &quot;나중에 배치가 주워서 처리&quot;할 큐가 존재하지 않기 때문에 이러한 재처리 스케줄러도 필수적이지 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;멱등성만은 남긴 이유 (대조점)&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반면 멱등성은 풀세트 중 유일하게 우리도 가져갔습니다. 보상(삭제)이 재시도되거나, 이미 없는 계정을 또 지우려 할 수 있고, 같은 이메일이 중복 생성되면 안되기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결론적으로, &quot;DB는 진짜 트랜잭션 1개로 묶고, 트랜잭션 밖의 Keycloak만 보상한다.&quot; 의 정책으로 데이터를 관리했습니다. 즉 DB 커밋 여부를 단일 진실 기준(source of truth)으로 삼는 경량 보상 &amp;amp; 신뢰성 높은 리소스 하나를 커밋 기준으로 두고 나머지를 맞추는 식으로 데이터 정합성을 맞췄습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;가장 중요하게 생각한 것은, 과설계(over-engineering)를 피하면서 정합성 문제를 해결하는 것이였습니다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;※ 처리 방식&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J0R2G/dJMcacQ2jfc/JEtGk3WnIYKTiprjn4HTA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J0R2G/dJMcacQ2jfc/JEtGk3WnIYKTiprjn4HTA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J0R2G/dJMcacQ2jfc/JEtGk3WnIYKTiprjn4HTA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ0R2G%2FdJMcacQ2jfc%2FJEtGk3WnIYKTiprjn4HTA0%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;1914&quot; height=&quot;1478&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style3&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;4. 구현&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 구현&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;4-1. Orchestration &amp;mdash; 보상 등록 후 정방향 진행&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Orchestration &amp;mdash; 보상 등록 후 정방향 진행&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UserService가 흐름을 지휘합니다.&lt;/li&gt;
&lt;li&gt;트랜잭션 시작 직후 보상을 먼저 등록하고, 만든 Keycloak id를 저장해 이후에 보상 트랜잭션을 위해 관리합니다.&lt;/li&gt;
&lt;li&gt;이때 keycloak 유저를 먼저 추가하고 이후에 유저를 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Transactional(isolation = Isolation.REPEATABLE_READ, timeout = 30)
fun createUsers(customerId: String, users: List&amp;lt;CreateUserInput&amp;gt;): List&amp;lt;UserEntity&amp;gt; {
    val createdKeycloakUserIds = mutableListOf&amp;lt;String&amp;gt;()
    keycloakService.registerKeycloakCompensation(createdKeycloakUserIds) // afterCompletion 콜백 등록

    val entities = users.map { input -&amp;gt;
        id &amp;larr; keycloak에 유저 추가
        createdIds.추가(id)
        entity &amp;larr; DB에 유저 추가
    }
    return userRepository.saveAll(entities) // 단일 배치 insert
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 유저는 Keycloak에 한 명씩 생성 (Keycloak에 배치 API가 없음)&lt;/li&gt;
&lt;li&gt;DB는 일괄 저장(batch insert)으로 한 번에 처리&lt;/li&gt;
&lt;li&gt;정책은 All-or-nothing : 한 entry라도 실패하면 트랜잭션 전체 롤백&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;4-2. 보상 등록 &amp;mdash; &amp;#96;afterCompletion&amp;#96; 트랜잭션 동기화&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보상 등록 &amp;mdash; afterCompletion 트랜잭션 동기화&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기가 SAGA 패턴에서 가장 중요한 보상 처리 관련 코드입니다.&lt;/li&gt;
&lt;li&gt;보상을 트랜잭션 커밋 결과에 묶어서 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// Keycloak과 DB는 트랜잭션을 공유할 수 없는 별개 시스템이라, 보상 트랜잭션으로 되돌린다.
fun registerKeycloakCompensation(keycloakUserIds: List&amp;lt;String&amp;gt;) {
    TransactionSynchronizationManager.registerSynchronization(
        object : TransactionSynchronization {
            override fun afterCompletion(status: Int) {
                if (status != TransactionSynchronization.STATUS_COMMITTED) {
                    keycloakUserIds.forEach { deleteUser(keycloakUserId = it) }
                }
            }
        },
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;afterCompletion은 트랜잭션이 커밋이든 롤백이든 끝난 뒤 호출.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;STATUS_COMMITTED가 아닐 때만 보상(삭제)을 실행 &amp;rarr; 정상 커밋 시엔 아무작업도 X.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;keycloakUserIds를 콜백 안에서 lazy하게 읽으므로, 등록 이후 추가된 id까지 전부 보상 가능.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-heading=&quot;왜 inline &amp;#96;try/catch&amp;#96;가 아니라 &amp;#96;afterCompletion&amp;#96;인가&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-heading=&quot;왜 inline &amp;#96;try/catch&amp;#96;가 아니라 &amp;#96;afterCompletion&amp;#96;인가&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 왜 inline try/catch가 아니라 afterCompletion인가&lt;/span&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 53px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;실패 케이스&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;inline try/catch&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;afterCompletion&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;루프 중간 Keycloak 호출 실패&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잡힘&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잡힘 (롤백으로 처리됨)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;saveAll commit 시 DB 제약 위반&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;못 잡음 (메서드 리턴 후 발생)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잡힘&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 제약 위반이 메서드 본문 밖(커밋 시점)에서 터지기 때문에, 트랜잭션 생명주기 훅인 afterCompletion만이 두 케이스를 모두 일관되게 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;4-3. 보상 실패는 전용 예외로 가시화&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-heading=&quot;4-3. 보상 실패는 전용 예외로 가시화&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보상 실패는 전용 예외로 가시화&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보상(삭제)마저 실패하면 keycloak에 orphan이 확정되게 됩니다.&lt;/li&gt;
&lt;li&gt;이건 반드시 사람이 알아야 하는 상황이라 에러를 발생시켜 알림을 발생시킵니다.&lt;/li&gt;
&lt;li&gt;해당 알림이 발생하면, 개발자가 수동으로 이를 처리해 문제를 해결합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;fun deleteUser(keycloakUserId: String) {
    try {
        keycloakClient.realm(realm).users().delete(keycloakUserId).use { }
    } catch (e: Exception) {
        throw KeycloakUserDeleteException(keycloakUserId = keycloakUserId, cause = e)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;alert = true + debugInfo로 어떤 계정이 orphan인지 즉시 식별 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보상 자체가 실패해도 시스템이 조용히 깨지는 게 아니라, 알람이 뜨고 정리 정보가 남는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;5. 멱등성 &amp;mdash; SAGA에서 빠지면 안 되는 것&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 멱등성 &amp;mdash; SAGA에서 빠지면 안 되는 것&lt;/span&gt;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;5. 멱등성 &amp;mdash; SAGA에서 빠지면 안 되는 것&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&amp;nbsp; 보상과 각 단계는 재시도&amp;middot;중복 호출에도 안전해야 합니다.&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보상(삭제) 단계의 멱등성: 이미 없는 Keycloak 유저를 삭제 시도해도 치명적이지 않아야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&amp;nbsp;생성 단계의 멱등성/충돌 방지: 이메일은 우리 DB의 unique 제약 + Keycloak username 유니크로 이중 보호됩니다. 따라서 같은 이메일 중복 생성이 조용히 통과하지 못한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SAGA의 &quot;각 단계 멱등 + unique key&quot; 원칙을, DB 제약과 Keycloak의 유니크 username으로 충족시켰습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;6. 남은 한계와 다음 단계&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;이렇게 SAGA의 Orchestration 과 afterCompletion을 통한 보상 처리로 데이터 정합성 문제를 해결했습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;6. 남은 한계와 다음 단계&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 남은 한계와 다음 단계&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금 구현은 의도적으로 best-effort를 맞추기 위한 보상입니다. 따라서 다음과 같은 한계가 있습니다.&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;보상 삭제가 (네트워크 등으로) 실패하면 keycloak에 orphan이 남고, 예외로 알람만 뜨고 자동 재시도는 없습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한번이라도 실패하면, 개발자가 직접 알림을 확인해 별도의 처리를 해야합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;상태 테이블 + 실패건 재 처리 스케쥴러 + 실패시 DLQ &amp;amp; Outbox 패턴 등은 구현되어 있지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 한 이유는 이런걸 처리하기 위한 추가적인 서비스 구축까지 할 정도로 해당 기능이 정밀하게 관리될 필요가 없었기 때문입니다. 엄청나게 큰 장애도 아니고, 문제 상황 또한 빠르게 처리될 필요 없고 직접 처리도 그렇게 어려운 것이 아니기 때문에 추가적인 인프라 구축 + 코드 처리 보단 간단하게 처리하고 이후에 다른 서비스에서 비슷한 처리가 계속해서 반복되어 모니터링이 필요하다면 이러한 기능들을 단계적으로 확장하기도 용이하기 때문입니다. 이후에,&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;완전 보장이 필요해지면(사용자 생성량이 폭증하거나 SLA가 빡세지면) 풀 SAGA 쪽으로 단계적으로 확장할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1단계 (현재) : @Transactional + afterCompletion 보상 + 알람
2단계        : Saga 상태 테이블 + 실패 건 재처리 스케줄러 (retry_count 상한)
3단계        : 서비스 분리 시 이벤트 기반 + Outbox 패턴&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금 구조는 이 확장 경로를 막지 않습니다. &amp;mdash;&amp;gt; 보상 로직이 이미 한 곳에 모여 있어, 상태 저장만 끼워 넣으면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;7. 회고 &amp;mdash; 이 작업에서 배운 것&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-heading=&quot;7. 회고 &amp;mdash; 이 작업에서 배운 것&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 회고 &amp;mdash; 이 작업에서 배운 것&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;분산 트랜잭션은 MSA만의 문제라고 생각했는데, 단일 서비스라도 외부 API 같은 것을 끼는 순간 같은 문제가 생길 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;패턴은 통째로 가져오는 게 아니라 필요한 만큼 쓴다. 풀 SAGA의 화려한 구성요소(상태 테이블, Outbox, 이벤트)를 다 넣었으면 over-engineering이었다. 우리 제약(DB 1개, 외부 1개, 동기 흐름)에선 보상 + 멱등성 + 알람이면 충분했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;트랜잭션 경계와 예외 발생 시점을 정확히 알아야 한다. DB 제약 위반은 commit 시점에 터진다를 몰랐다면 inline catch로 짜고 버그를 만들었을 것이다. afterCompletion을 선택한 근거가 여기에 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패를 숨기지 말고 드러내라. 보상 실패를 전용 예외 + 알람으로 만든 덕분에, orphan이 생겨도 언젠가 들키는 게 아니라 즉시 알게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/63</guid>
      <comments>https://swmobenz.tistory.com/63#entry63comment</comments>
      <pubDate>Sat, 20 Jun 2026 14:03:14 +0900</pubDate>
    </item>
    <item>
      <title>AWS EBS CSI Driver을 통한 디스크 동적 프로비져닝 적용</title>
      <link>https://swmobenz.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;온프렘 서버 환경에서 k8s을 운영할 때는 pod 별로 디스크를 동적으로 관리하기 위해 OpenEBS LVM을 사용했습니다. 그래서 pod을 만들 때 매번 pv &amp;amp; pvc를 만들고 관리해야 되는 불편함을 해소했습니다. 따라서 이번에 AWS에서 k8s을 운영할 때도 이런식으로 운영하면 좋을 것 같아 찾아보던 중, AWS는 EBS(Elastic Block Store)를 CSI Driver를 통해 직접 제어하는 방식도 존재한다는 것을 알게 되었습니다. 따라서 이 2개의 방식을 비교해 최종적으로 AWS CSI Driver 방식을 사용하기로 결정했습니다. 그래서 이번기회에 2가지 방식을 비교하고 왜 이 방식을 사용했는지 정리하려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;온프렘 관리 방식 vs AWS 관리 방식&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;온프렘&lt;/b&gt;&lt;b&gt;&amp;nbsp;(OpenEBS LVM)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AWS (EBS CSI Driver)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로비져닝&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LVM VG/LV&amp;nbsp;직접&amp;nbsp;관리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EBS&amp;nbsp;볼륨&amp;nbsp;자동&amp;nbsp;생성/삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;드라이버&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OpenEBS&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;aws-ebs-csi-driver&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM&amp;nbsp;권한&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;불필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM User&amp;nbsp;또는&amp;nbsp;인스턴스&amp;nbsp;프로파일&amp;nbsp;필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;멀티&amp;nbsp;AZ&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;불가&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AZ&amp;nbsp;종속&amp;nbsp;(EFS로&amp;nbsp;대체&amp;nbsp;가능)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;볼륨&amp;nbsp;확장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수동&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;allowVolumeExpansion: true로&amp;nbsp;자동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EBS CSI Driver 사용 이유&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기술적으로는 AWS 에서도 OpenEBS를 사용할 수 있지만, 아래와 같은 이유로 AWS EBS CSI Driver를 사용하기로 결정했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #000000;&quot;&gt;&lt;b&gt;OpenEBS LVM은 로컬 디스크를 소프트웨어로 추상화 하는 도구입니다.&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;온프렘에서 도입한 이유는, 실제 물리 디스크가 노드에 붙어있고 이를 k8s가 바로 알 수 없는 상황이기 때문에 OpenEBS라는 중간의 연결고리로 자동 관리할 수 있도록 진행한 것입니다. 하지만, AWS의 경우 그 역할 자체를 EBS 자체가 해버립니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;온프렘: 물리디스크 &amp;rarr; OpenEBS(추상화) &amp;rarr; k8s PVC &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS: EBS &amp;rarr; CSI Driver(연결) &amp;rarr; k8s PVC&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비교&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OpenEBS on AWS&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EBS CSI Driver&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;디스크 관리 주체&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OpenEBS Operator&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 인프라&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;볼륨 생성 위치&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스 로컬&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 독립 EBS 서비스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;노드 죽으면&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 위험&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EBS는 노드와 분리되어 안전&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스냅샷/백업&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;별도 구성 필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 스냅샷 네이티브 지원&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모니터링&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;별도 구성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch 자동 연동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;운영 오버헤드&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OpenEBS 버전관리, 장애대응&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS가 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비용&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 스토리지 + OpenEBS 오버헤드&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EBS 비용만&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;따라서 AWS에서 OpenEBS를 사용하는 것은 이미 제공되는 기능을 다시 구현함과 동시에, 더욱 더 관리 포인트만 늘어나는 것이기 때문에 굳이 AWS에서는 OpenEBS를 사용하는 것보다 기본적으로 제공되는 EBS CSI Driver를 사용하는 것이 좋습니다. (Naver Cloud에서도 EBS CSI Driver를 사용하려고 했었는데, 그때는 관리형 k8s(nks)를 사용하지 않으면, csi driver를 기본적으로 사용할 수 없어서 그때는 OpenEBS를 사용했습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면, 지금부터 어떻게 AWS EBS CSI Driver을 사용할 수 있는지 정리해보도록 하겠습니다.&lt;br /&gt;정리하기에 앞서, 이게 어떻게 동작하는지를 간략하게 설명드리도록 하겠습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS EBS CSI Driver 동작 방식&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ew2c5/dJMcadH5yZb/VgzFkiP4KrtIqFxLdIIsw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ew2c5/dJMcadH5yZb/VgzFkiP4KrtIqFxLdIIsw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ew2c5/dJMcadH5yZb/VgzFkiP4KrtIqFxLdIIsw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEw2c5%2FdJMcadH5yZb%2FVgzFkiP4KrtIqFxLdIIsw0%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;1410&quot; height=&quot;830&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 EBS CSI Driver의 동작은 2개의 컴포넌트를 통해 동작합니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;ebs-csi-controller : 볼륨 생성/삭제를 담당&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;ebs-csi-node : 각 노드에서 실제 마운트를 담당&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9UkjJ/dJMcabQ4y7B/CeBrvHmkqXdDlKyxkFtSeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9UkjJ/dJMcabQ4y7B/CeBrvHmkqXdDlKyxkFtSeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9UkjJ/dJMcabQ4y7B/CeBrvHmkqXdDlKyxkFtSeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9UkjJ%2FdJMcabQ4y7B%2FCeBrvHmkqXdDlKyxkFtSeK%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;1410&quot; height=&quot;1036&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동작 순서&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1~2. PVC 생성 &amp;rarr; Pending &amp;mdash; kubectl apply로 PVC를 만들면 k8s API Server가 받아서 Pending 상태로 등록&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. controller가 감지 &amp;mdash; ebs-csi-controller 안의 external-provisioner 사이드카가 Pending PVC를 watch하다가 ebs.csi.aws.com provisioner인 걸 확인하고 처리 시작&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4~5. AWS API 호출 &amp;rarr; EBS 볼륨 생성 &amp;mdash; IAM 권한으로 ec2:CreateVolume 호출. WaitForFirstConsumer 덕분에 Pod가 스케줄된 AZ에 맞춰 볼륨이 생성됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. PV 자동 생성 &amp;rarr; Bound &amp;mdash; controller가 생성된 EBS 볼륨으로 PV를 만들고 PVC와 바인딩. 상태가 Bound로 변경&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. ebs-csi-node가 attach &amp;mdash; 해당 노드의 ebs-csi-node DaemonSet이 ec2:AttachVolume으로 볼륨을 노드에 붙임&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. Pod 마운트 &amp;mdash; 노드에 attach된 볼륨을 Pod의 지정 경로(/data 등)에 마운트. Pod가 Running 상태가 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PVC 생성 요청이 들어오면 EBS CSI Driver가 AWS API를 호출하여 EBS 볼륨을 자동 생성하고 해당 Pod에 마운트한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PVC 생성 요청&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;rarr; EBS CSI Driver (kube-system)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; AWS API 호출 (ec2:CreateVolume, ec2:AttachVolume ...)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; EBS 볼륨 생성 및 Pod 마운트&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Talos는 Self-managed 클러스터이므로 EKS 애드온 방식을 사용할 수 없기 때문에 Helm으로 직접 설치한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM 생성&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 EBS CSI Driver가 AWS API를 호출하려면 IAM 권한이 필요합니다. 따라서 필요한 권한을 가진 IAM User를 생성하고 Access Key를 k8s Secret으로 CSI Driver에 주입하는 방식을 통해 이를 관리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000; text-align: left;&quot;&gt;AmazonEBSCSIDriverPolicy 권한을 가진 유저를 생성하고 AccessKey와 SecretKey을 생성해 Secret으로 등록합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AmazonEBSCSIDriverPolicy 권한&lt;/b&gt;&lt;br /&gt;- &quot;EBS 볼륨의 생명주기 전체를 관리하는 권한&quot;&amp;nbsp;&lt;br /&gt;- EBS의 생성 &amp;rarr; attach &amp;rarr; detach &amp;rarr; 삭제까지. 단, 삭제는 CSI가 직접 만든 볼륨만 가능하도록 태그 조건으로 범위가 제한됨&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Secret 생성&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 생성한 aws key를 k8s에서 안전하게 사용하기 위해 k8s에 secret으로 등록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774194032258&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# aws ebs csi driver secret
apiVersion: v1
kind: Secret
metadata:
  name: aws-secret
  namespace: kube-system
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: &amp;lt;AWS ACCESS KEY 값&amp;gt;
  AWS_SECRET_ACCESS_KEY: &amp;lt;AWS SECRET KEY 값&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;EBS CSI Driver&lt;span&gt;&amp;nbsp;&lt;/span&gt;설치&lt;span&gt;&amp;nbsp;&lt;/span&gt;(Helm)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EBS CSI Driver을 설치를 helm을 통해 진행합니다.&lt;/li&gt;
&lt;li&gt;helm으로 사용할 설정 파일 생성 후, 이를 적용하여 설치를 진행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;helm 설정 values.yaml&amp;nbsp; 파일 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1774326885994&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;controller:
  volumeModificationFeature:
    enabled: true
  extraVolumeTags:
    managed-by: &amp;lt;태그 값&amp;gt;
  replicaCount: 1
  envFrom:
    - secretRef:
        name: &amp;lt;secret 명&amp;gt;

node:
  envFrom:
    - secretRef:
        name: &amp;lt;secret 명&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;table&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-initial-todom-render=&quot;true&quot;&gt;
&lt;div data-testid=&quot;table-alignment-container&quot;&gt;
&lt;div data-testid=&quot;table-container&quot; data-layout=&quot;default&quot; data-number-column=&quot;false&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 36.9768%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span data-text-custom-color=&quot;#ffffff&quot; data-prosemirror-content-type=&quot;mark&quot; data-prosemirror-mark-name=&quot;textColor&quot;&gt;옵션&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 63.0232%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span data-text-custom-color=&quot;#ffffff&quot; data-prosemirror-content-type=&quot;mark&quot; data-prosemirror-mark-name=&quot;textColor&quot;&gt;설명&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 36.9768%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;volumeModificationFeature.enabled&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 63.0232%; height: 18px;&quot;&gt;iops, throughput 등 볼륨 스펙을 나중에 변경 가능하게 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 36.9768%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #f1f2f5; color: #333333; text-align: start;&quot;&gt;extraVolumeTags&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 63.0232%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #f1f2f5; color: #333333; text-align: start;&quot;&gt;생성되는 EBS 볼륨에 AWS 태그 추가 (관리/비용 추적용)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 36.9768%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #f1f2f5; color: #333333; text-align: start;&quot;&gt;envFrom.secretRef&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 63.0232%; height: 18px;&quot;&gt;위에서 생성한 &lt;span style=&quot;background-color: #f1f2f5; color: #333333; text-align: start;&quot;&gt;aws-secret을 환경변수로 통째 주입 (AWS_ACCESS_KEY_ID 등)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;EBS CSI Driver&lt;span&gt;&amp;nbsp;&lt;/span&gt;설치&lt;span&gt;&amp;nbsp;&lt;/span&gt;(Helm)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1774327111457&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# helm repo 생성
helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver
helm repo update

# aws ebs csi driver 설치
helm install aws-ebs-csi-driver aws-ebs-csi-driver/aws-ebs-csi-driver \
  --namespace kube-system \
  --values tmp-pod-ebs-csi-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면, 기본적으로 위에서 말씀드린&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;-&amp;gt; ebs-csi-controller&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;-&amp;gt; ebs-csi-node&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 두개가 노드마다 각각 생성됩니다.&lt;br /&gt;그렇다면, 기본적인 준비는 끝났고 이를 통해 어떻게 파드에 적용해 사용할 수 있는지를 설명드리도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;StorageClass 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1774327465891&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: &amp;lt;storageClass 명&amp;gt;
  annotations:
    storageclass.kubernetes.io/is-default-class: &quot;true&quot;
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: &quot;3000&quot;
  throughput: &quot;125&quot;
  encrypted: &quot;true&quot;
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 92px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 29.8837%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span data-text-custom-color=&quot;#ffffff&quot; data-prosemirror-content-type=&quot;mark&quot; data-prosemirror-mark-name=&quot;textColor&quot;&gt;옵션&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%; height: 20px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 29.8837%; height: 18px;&quot;&gt;is-default-class&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%; height: 18px;&quot;&gt;PVC에 storageClassName을 명시하지 않으면 이 StorageClass가 자동으로 사용되게끔 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 29.8837%; height: 18px;&quot;&gt;provisioner&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%; height: 18px;&quot;&gt;이 StorageClass로 PVC가 생성되면 어떤 드라이버가 처리할지 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 29.8837%; height: 18px;&quot;&gt;type&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%; height: 18px;&quot;&gt;EBS 볼륨 타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 29.8837%; height: 18px;&quot;&gt;iops&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%; height: 18px;&quot;&gt;초당 I/O 처리 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.8837%;&quot;&gt;throughput&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%;&quot;&gt;초당 데이터 전송량(MiB/s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.8837%;&quot;&gt;encrypted&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%;&quot;&gt;EBS 볼륨 암호화 활성화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.8837%;&quot;&gt;WaitForFirstConsumer&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%;&quot;&gt;PVC가 생성되는 시점이 아니라 Pod가 스케줄된 시점에 볼륨을 생성.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.8837%;&quot;&gt;reclaimPolicy&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%;&quot;&gt;PVC가 삭제됐을 때 EBS 볼륨 처리 방식 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 29.8837%;&quot;&gt;allowVolumeExpansion&lt;/td&gt;
&lt;td style=&quot;width: 70.1163%;&quot;&gt;PVC의 storage 용량을 늘리는 게 가능. 운영 중 용량 부족 시 다운타임 없이 확장 가능. (축소 불가)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이렇게 만든 storageClass를 pod 생성시 storageClass 선언으로 해서 사용&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;table&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-initial-todom-render=&quot;true&quot;&gt;
&lt;div data-testid=&quot;table-alignment-container&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-testid=&quot;table-container&quot; data-layout=&quot;default&quot; data-number-column=&quot;false&quot;&gt;
&lt;div&gt;&amp;nbsp;&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&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6vhRy/dJMcahDLzrt/6Fx0Nnb6FAYHmWzDETk0W0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6vhRy/dJMcahDLzrt/6Fx0Nnb6FAYHmWzDETk0W0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6vhRy/dJMcahDLzrt/6Fx0Nnb6FAYHmWzDETk0W0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6vhRy%2FdJMcahDLzrt%2F6Fx0Nnb6FAYHmWzDETk0W0%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;1306&quot; height=&quot;615&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-media-vc-wrapper=&quot;true&quot;&gt;
&lt;div data-media-vc-wrapper=&quot;true&quot; data-width-type=&quot;pixel&quot; data-width=&quot;760&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pod를 띄울 때 storageClass 적용 후 Pod 생성 시 pv &amp;amp; pvc 생성 후 그거와 연결된 AWS EBS가 자동으로 생성되는 것을 확인&lt;/li&gt;
&lt;/ul&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;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주의 사항&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AZ &lt;/b&gt;&lt;b&gt;종속성&lt;/b&gt; &amp;mdash; EBS는 AZ 종속이다. 노드가 다른 AZ로 옮겨가면 볼륨 재마운트가 안 된다. 멀티 AZ 공유가 필요하면 EFS를 사용해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;WaitForFirstConsumer&lt;/b&gt; &amp;mdash; WaitForFirstConsumer를 반드시 설정해야 한다. 미설정 시 PVC 생성 시점에 AZ가 랜덤으로 선택되어 Pod와 볼륨이 다른 AZ에 생길 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;비용&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;관리&lt;/b&gt; &amp;mdash; reclaimPolicy: Retain 설정 시 PVC 삭제해도 EBS 볼륨은 보존된다. 불필요한 비용 발생에 주의하고 주기적으로 미사용 볼륨을 정리해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;접근&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;모드&lt;/b&gt; &amp;mdash; EBS는 ReadWriteOnce만 지원한다. 여러 Pod에서 동시에 마운트해야 한다면 EFS(ReadWriteMany)를 사용해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게해서 AWS에서 k8s을 운영할 때, 효율적인 디스크 관리에 대해서 정리 및 적용해봤습니다. 여러 고객사에 니즈를 맞추기 위해 Onprem &amp;amp; AWS &amp;amp; NCP에서 서버를 운영하다보니, 최대한 하나로 맞추기 위해 노력하지만 플랫폼 특성상 다른 부분이 존재해 매번 새롭게 서버 세팅을 하게 되는 것 같습니다...&lt;/p&gt;</description>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/62</guid>
      <comments>https://swmobenz.tistory.com/62#entry62comment</comments>
      <pubDate>Mon, 23 Mar 2026 00:41:00 +0900</pubDate>
    </item>
    <item>
      <title>여러 고객 온프레미스 K8s를 동일 IP로 운영할 때: Tailscale 4via6 적용기</title>
      <link>https://swmobenz.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;MSP(Managed Service Provider)나 SaaS 플랫폼을 운영할 때, 여러 고객의 온프레미스 또는 클라우드 인프라를 관리해야 하는 경우가 많습니다. 특히 각 고객이 독립적으로 구축한 Kubernetes 클러스터를 중앙에서 관리하려면 원격 접근이 필수적입니다. (물론 원격 접근이 불가능 한 고객들도 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 &lt;span&gt;3대의 서버와 이를 연결하는 라우터를 1세트&lt;/span&gt;로 구성해 온프레미스 환경을 구축합니다. 이후 Kubernetes 클러스터에 원격으로 안정적으로 접근하기 위해 &lt;span&gt;라우터에 Tailscale을 설치&lt;/span&gt;하고, 라우터가 연결된 &lt;span&gt;내부 IP 대역을 광고(advertise)하는 Tailscale subnet router&lt;/span&gt;로 설정합니다. 이를 통해 운영자는 Tailscale을 통해 &lt;span&gt;각 노드와 Kubernetes 리소스에 원격으로 접근&lt;/span&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그러다보니 문제가 발생했습니다. 여러 고객들에게 온프레미스 환경을 제공하다보니, 각 고객 별로 다른 IP 대역대를 사용하면 기본적인 k8s 설정이 바뀌게 되고 접근 방식도 바뀌게 되니 이를 개별적으로 관리하는 것이 너무 번거로웠습니다. 그렇다고 동일한 사설 IP 대역(예: 192.168.0.0/24, 10.0.0.0/24)을 사용하게 되면 일반적인 VPN이나 Subnet Router만으로는 IP 충돌이 발생하여 여러 고객에게 동시에 접근할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이러한 문제를 해결하기 위해, tailscale의 4via6 기술을 도입했습니다. 적용 결과 아쉬운 점도 있었지만, 생각보다 효율적으로 유저들을 관리할 수 있겠다 라는 생각이 들었습니다. 따라서 이번 기회에 제가 적용한 기술이 뭐고 어떻게 저희의 문제를 해결했는지 정리하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 다음과 같은 환경을 운영하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;각 고객&lt;/span&gt;: talos OS 위에 동작하는 Kubernetes &lt;span&gt;클러스터 &lt;/span&gt;(3&lt;span&gt;개 노드&lt;/span&gt;)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;노드 &lt;/span&gt;IP: 192.168.0.49, 192.168.0.59, 192.168.0.69&lt;/li&gt;
&lt;li&gt;&lt;span&gt;네트워크 대역&lt;/span&gt;&lt;b&gt;: &lt;/b&gt;&lt;span&gt;모든 고객이 &lt;/span&gt;&lt;b&gt;192.168.0.0/24 &lt;/b&gt;&lt;span&gt;사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;게이트웨이&lt;span&gt;: &lt;/span&gt;각 고객 사이트마다 &lt;span&gt;OpenWrt &lt;/span&gt;라우터 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;1202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zYDiT/dJMcagxzI77/0lgu6eldL7ZJ6SYxAcx28k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zYDiT/dJMcagxzI77/0lgu6eldL7ZJ6SYxAcx28k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zYDiT/dJMcagxzI77/0lgu6eldL7ZJ6SYxAcx28k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzYDiT%2FdJMcagxzI77%2F0lgu6eldL7ZJ6SYxAcx28k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1742&quot; height=&quot;1202&quot; data-origin-width=&quot;1742&quot; data-origin-height=&quot;1202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위와 같은 형태로 고객들이 Onpremise 환경을 구축합니다. 고객이 1명일 때는 OpenWrt 라우터에 Tailscale을 설치하여, 고객사 내부망인 192.168.0.0/24 대역을 호스팅하는 서브넷 라우터(Subnet Router) 역할을 하도록 구성했습니다. 그렇게해서, 저희의 로컬 PC에서 Tailscale을 통해 고객사 내부 k8s 서버로 원격 접속할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만 고객사가 늘어나면서 문제가 발생했습니다. 다수의 고객사에 동일하게 서버 IP 구성을 하려고 하니, 단일 Tailnet에서 서브넷 라우트를 그대로 광고하면 대역 충돌로 인해 라우팅이 모호해지거나 특정 고객사로 트래픽이 잘못 전달되는 상황이 생길 수 있는 문제가 생겼습니다. 따라서 이를 해결하기 위해 tailscale 4via6을 도입하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Tailscale이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;일단 tailscale의 4via6을 설명하기에 앞서, tailscale에 대해서 설명드리려고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tailscale은 WireGuard 기반의 제로 트러스트 네트워크(Zero Trust Network) 솔루션입니다.&lt;/li&gt;
&lt;li&gt;여러 디바이스들을 안전하게 연결해주는 VPN 서비스 (모든 디바이스가 하나의 거대한 사설 네트워크에 있는 것처럼 동작하게 함)&lt;/li&gt;
&lt;li&gt;Tailscale은 복잡한 네트워크 설정 없이 클릭 몇 번으로 디바이스 간 프라이빗 네트워크를 구성할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 디바이스에 고유한 IP 주소(100.x.y.z 형태)를 할당하고, P2P(peer-to-peer) 방식으로 직접 연결을 시도하기 때문에 속도가 빠릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보안 측면에서도 모든 트래픽이 암호화되어 전송되고 ACL로 유저 권한을 관리해 각 디바이스에 세밀한 접근 제어가 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Tailscale 4via6란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Tailscale의 4via6는 서로 겹치는 IPv4 사설망 대역(예: 여러 장소가 모두 10.0.0.0/24 또는 192.168.0.0/24를 쓰는 경우)을 주소 변경 없이 동시에 붙일 수 있게 해주는 서브넷 라우팅 기능입니다. 일반적인 서브넷 라우팅에서는 tailnet 안에서 10.0.0.5로 접속하면, 어느 장소의 10.0.0.5인지 구분이 안 돼서 잘못된 곳(현재 primary 라우터 뒤)으로 갈 수 있습니다. 4via6는 이 충돌을 해결합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;할당된 주소&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주소 생성 시 사용하는 명령어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tailscale&amp;nbsp;debug&amp;nbsp;via&amp;nbsp;&amp;lt;site-id&amp;gt;&amp;nbsp;&amp;lt;ipv4-cidr&amp;gt;&lt;/li&gt;
&lt;li&gt;이걸로 생성된 IPv6 라우트를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;--advertise-routes=&lt;/span&gt;로 광고하는 흐름입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;4via6는 fd7a:115c:a1e0:b1a:0:XXXX:YYYY:YYYY 형태의 Tailscale 전용 IPv6 프리픽스를 씀&lt;/li&gt;
&lt;li&gt;각 부분의 의미
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fd7a:115c:a1e0:b1a: Tailscale 4via6의 고정 prefix&lt;/li&gt;
&lt;li&gt;0:XXXX: Site ID (0-65535까지 사용 가능)&lt;/li&gt;
&lt;li&gt;YYYY:YYYY:&amp;nbsp;IPv4&amp;nbsp;주소를&amp;nbsp;16비트&amp;nbsp;hex로&amp;nbsp;표현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;동작 원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tailscale 4via6(Four via Six)는 중복되는 IPv4 서브넷을 고유한 IPv6 주소 공간으로 매핑하여 구분&lt;/li&gt;
&lt;li&gt;각 서브넷에 고유한 Site ID를 부여하면, Tailscale이 내부적으로 IPv6 주소를 사용하여 올바른 목적지로 트래픽을 라우팅합니다.&lt;/li&gt;
&lt;li&gt;중요한 점은 실제 고객 환경에서는 IPv4를 그대로 사용하며, IPv6 설정이 전혀 필요하지 않다는 것입니다. IPv6&amp;nbsp;주소&amp;nbsp;매핑은&amp;nbsp;Tailscale&amp;nbsp;라우팅&amp;nbsp;레벨에서만&amp;nbsp;사용되는&amp;nbsp;내부&amp;nbsp;메커니즘입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;서브넷 라우터가 패킷을 IPv6 &amp;harr; IPv4로 재작성(translation/rewrite) 해서 실제 내부 IPv4 장비로 전달합니다. 즉, &amp;ldquo;IPv4를 IPv6로 태그 붙여서(사이트 ID로) 구분한 뒤 전달&amp;rdquo;이라고 보시면 됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작 방식&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 고객 사이트의 Subnet Router에 고유 Site ID 할당 (예: 고객A=1, 고객B=2)&lt;/li&gt;
&lt;li&gt;Subnet Router&lt;span&gt;가 &lt;/span&gt;IPv6 &lt;span&gt;형식의 &lt;/span&gt;route&lt;span&gt;를 &lt;/span&gt;Tailscale&lt;span&gt;에 광고&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;관리자가 &lt;/span&gt;192.168.0.49&lt;span&gt;에 접근하면 &lt;/span&gt;Tailscale&lt;span&gt;이 &lt;/span&gt;Site ID&lt;span&gt;로 올바른 고객 구분&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;트래픽이 해당 &lt;span&gt;Subnet Router&lt;/span&gt;를 통해 목적지로 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.&amp;nbsp; 각 고객별 site ID 계획&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 각 고객에게 고유한 &lt;span&gt;&lt;b&gt;Site ID&lt;/b&gt;&lt;/span&gt;를 할당합니다&lt;span&gt;&lt;b&gt;. Site ID&lt;/b&gt;&lt;/span&gt;는 &lt;span&gt;&lt;b&gt;0&lt;/b&gt;&lt;/span&gt;부터 &lt;span&gt;&lt;b&gt;65535&lt;/b&gt;&lt;/span&gt;까지 사용할 수 있습니다&lt;span&gt;&lt;b&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Site ID란?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4via6에서 &lt;span&gt;&lt;b&gt;site ID&lt;/b&gt;&lt;/span&gt;는 이 트래픽이 어느 고객/현장(사이트)로 가야 하는지&amp;rdquo;를 구분하기 위해 4via6 IPv6 주소에 넣는 식별 값&lt;/li&gt;
&lt;li&gt;겹치는 IPv4 대역(예: 여러 고객 모두 192.168.0.0/24)을 동시에 쓰려면, &lt;span&gt;사이트마다 서로 다른 site ID를 부여&lt;/span&gt;해서 충돌을 피함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1769918543590&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Site ID 계획 예시
고객 A: Site ID = 1
고객 B: Site ID = 2
고객 C: Site ID = 3
고객 D: Site ID = 4
...


고객이 많은 경우 조직 단위나 지역별로 범위 할당 가능
예: 서울 고객 = 1000-1999, 부산 고객 = 2000-2999&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2.&amp;nbsp; OpenWrt 라우터에 Tailscale 설치&lt;/h4&gt;
&lt;pre id=&quot;code_1769918628856&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# OpenWrt 라우터에 SSH 접속 후

opkg update

opkg install iptables iptables-nft
opkg install tailscale

# Tailscale 시작 및 인증&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3.&amp;nbsp; 1번에서 정한 Site ID 기반으로 4via6 IPv6 주소 생성 및 tailscale Subnet Router 광고&lt;/h4&gt;
&lt;pre id=&quot;code_1769918750482&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 고객 A (Site ID = 1, IPv4 = 192.168.0.0/24)

tailscale debug via 1 192.168.0.0/24

# 출력:
fd7a:115c:a1e0:b1a:0:1:c0a8:0/120

tailscale up \
  --advertise-routes=fd7a:115c:a1e0:b1a:0:1:c0a8:0/120 \
  --snat-subnet-routes=false \
  --accept-dns=false \
  --accept-routes=false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;4.&amp;nbsp; Tailscale Admin Console에서 승인&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tailscale Admin Console (&lt;a href=&quot;https://login.tailscale.com/admin/machines&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://login.tailscale.com/admin/machines&lt;/a&gt;) 접속&lt;/li&gt;
&lt;li&gt;3번에 tailscale up 결과 생긴 기기 클릭&lt;/li&gt;
&lt;li&gt;광고된 4via6 router 승인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;5.&amp;nbsp; MagicDNS를 통한 서버 IPv6 확인&lt;/h4&gt;
&lt;pre id=&quot;code_1769920603635&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dig AAAA [서버 MagicDNS]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;MagicDNS는&amp;nbsp;Tailscale에서&amp;nbsp;제공하는&amp;nbsp;사설&amp;nbsp;DNS&amp;nbsp;기능입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Tailnet(=Tailscale 네트워크) 안의 각 장비에 대해 사람이 읽을 수 있는 이름(호스트네임)을 자동으로 만들어 주고, 그 이름으로 접속하면 해당 장비의 Tailscale IP로 자동 해석(resolution) 해줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;형식&lt;/span&gt;&lt;b&gt;: {IP&lt;/b&gt;&lt;span&gt;를 대시로 연결&lt;/span&gt;&lt;b&gt;}-via-{Site ID}&lt;/b&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;ex) 192-168-0-99-via-1&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;6.&amp;nbsp; /etc/hosts 파일 수정 및 kubeconfig 반영&lt;/h4&gt;
&lt;pre id=&quot;code_1769920910921&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/etc/host
[확인된 IPv6 주소] [적용할 DNS명]


/kube/config 파일 수정

server: https://[DNS 명]:6443
# /etc/hosts 에서 등록한 dns 명으로 server명 수정&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가로 저는 talos OS을 사용하기 때문에 talos machineconfig 파일에 적용할 DNS 명을 certSAN에 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이렇게하면, 192.168.0.0/24 대역대를 subnet router로 동작하는 tailscale 내부 기기들을 Site ID기반 IPv6로 접근하기 때문에 여러 기기들을 관리할 수 있습니다. 아래는 4via6을 적용해 여러 고객들을 관리하는 구조도 입니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;※&amp;nbsp; 구조도&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;1568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMoFWD/dJMcahcbqrR/HiWxFyRAUakB3WUm6b4nj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMoFWD/dJMcahcbqrR/HiWxFyRAUakB3WUm6b4nj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMoFWD/dJMcahcbqrR/HiWxFyRAUakB3WUm6b4nj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMoFWD%2FdJMcahcbqrR%2FHiWxFyRAUakB3WUm6b4nj0%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;1886&quot; height=&quot;1568&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;1568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게해서 여러 고객들을 동일한 IP 대역대로 관리할 수 있었습니다. tailscale 4via6 초기에는&amp;nbsp;IPv6&amp;nbsp;주소&amp;nbsp;매핑&amp;nbsp;개념이&amp;nbsp;복잡해&amp;nbsp;보일&amp;nbsp;수&amp;nbsp;있지만,&amp;nbsp;실제로는&amp;nbsp;고객&amp;nbsp;환경에서&amp;nbsp;IPv4를&amp;nbsp;그대로&amp;nbsp;사용하며&amp;nbsp;Tailscale이&amp;nbsp;백그라운드에서&amp;nbsp;모든&amp;nbsp;라우팅을&amp;nbsp;처리합니다.&amp;nbsp;관리자는&amp;nbsp;MagicDNS&amp;nbsp;이름이나&amp;nbsp;간단한&amp;nbsp;IPv6&amp;nbsp;주소만&amp;nbsp;사용하면&amp;nbsp;되므로&amp;nbsp;실제&amp;nbsp;운영은&amp;nbsp;매우&amp;nbsp;직관적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;물론 저희의 OpenWrt 라우터를 사용하기 때문에 다른 대역대로 node들을 관리하면, 이렇게 할 필요가 없긴 합니다. 다른 대역대의 IP를 사용한다면 다른 subnet router를 호스팅하면 되기 때문에 대역대 충돌이 생기지 않기 때문입니다. 하지만 이렇게되면 다음과 같은 문제가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고객 별로 어떤 IP 대역대를 사용하는지 알아야한다.&lt;/li&gt;
&lt;li&gt;k8s node IP &amp;amp; talosOS node IP를 고객 별로 관리해야 하기 때문에 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 알아보던 중 tailscale의 4via6을 알게 되었습니다. 이 기술 자체가 지금 저같은 문제를 해결하기 위한 기술이였어서 이를 적용해서 더 간편하게 고객들을 관리할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;물론, 고객들의 Site ID 및 IPv6는 별도로 관리해야하는 단점이 있지만 이 또한 DNS 서버를 따로 둔다면 해결될 문제라고 생각했습니다. 다음번에는 DNS 서버까지 적용해 다른 로컬 PC에서도 별도의 파일 수정 없이 접근할 수 있는 환경까지 추가로 설명드리도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study/DEVELOP</category>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/61</guid>
      <comments>https://swmobenz.tistory.com/61#entry61comment</comments>
      <pubDate>Tue, 27 Jan 2026 17:57:19 +0900</pubDate>
    </item>
    <item>
      <title>AWS MSK 도입을 통한 실시간 데이터 파이프라인 안정성 확보</title>
      <link>https://swmobenz.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 실시간으로 공장에 있는 기기들의 데이터를 수집하고 있습니다. 그러다보니 실시간으로 들어오는 데이터들을 유실 없이 저장해야 합니다. 그래서 저희는 실시간 데이터를 효율적으로 처리하기 위해 아래와 같은 아키텍처를 구현하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9Chpj/dJMcacIqbIJ/jkKclJ6TPsDCOG7ItbqlCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9Chpj/dJMcacIqbIJ/jkKclJ6TPsDCOG7ItbqlCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9Chpj/dJMcacIqbIJ/jkKclJ6TPsDCOG7ItbqlCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9Chpj%2FdJMcacIqbIJ%2FjkKclJ6TPsDCOG7ItbqlCk%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;1270&quot; height=&quot;388&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 순서를 설명드리자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 공장 기기 &amp;rarr; MQTT Broker&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공장의 각 기기에서 MQTT 프로토콜로 메시지를 발행하며, 이 메시지들은 모두 MQTT Broker로 전달됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. MQTT Broker &amp;rarr; Kafka Connect &amp;rarr; Kafka&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MQTT Broker에 들어온 메시지 중 Kafka에서 처리해야 하는 특정 토픽들은 Kafka Connect의 MQTT Source Connector를 통해 Kafka로 전달됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Kafka &amp;rarr; Spark Streaming &amp;rarr; ScyllaDB / ClickHouse&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka로 들어온 메시지는 Spark Streaming이 실시간으로 소비하여 처리하고, 처리된 데이터는 각각의 목적에 따라 ScyllaDB와 ClickHouse에 저장됩니다.&lt;/li&gt;
&lt;/ul&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;아키텍처는 디바이스에서 발생한 실시간 데이터를 MQTT Broker로 전달하는 것에서 시작됩니다. MQTT Broker에 퍼블리시된 메시지는 Kafka Connect의 MQTT Source Connector가 구독하여 가져오고, 이를 Kafka의 특정 Topic으로 안정적으로 적재합니다. Kafka는 데이터 스트림을 버퍼링하며 Spark Structured Streaming이 이를 실시간으로 소비할 수 있도록 전달하는 중간 허브 역할을 합니다. Spark Structured Streaming은 Kafka에서 데이터를 스트리밍 방식으로 읽어와 필요한 전처리와 변환을 수행한 뒤, 목적에 따라 두 가지 저장소로 데이터를 분기하여 저장합니다. 실시간 조회나 빠른 응답이 필요한 서비스는 ScyllaDB로 저장하고, 대량 분석이나 OLAP 쿼리를 위한 데이터는 ClickHouse로 저장합니다. 이렇게 구성된 전체 파이프라인은 실시간 데이터 수집, 처리, 조회, 분석까지 전 과정이 자동화되고 확장 가능한 형태로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯, 데이터 유실을 최소화하기 위해선 데이터 처리에 필요한 서비스들이 죽지 않고 리소스들이 효율적으로 관리되어야 합니다. 저희는 여기서 사용하는 서비스들을 각각,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MQTT Broker : AWS IoT Core&lt;/li&gt;
&lt;li&gt;Kafka : EC2 Instance&lt;/li&gt;
&lt;li&gt;Spark : EC2 Instance&lt;/li&gt;
&lt;li&gt;Scylla &amp;amp; Clickhouse : EC2 Instance&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;만약 서비스가 죽었을 때 데이터 유실이 발생하는 서비스가 무엇일까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였습니다. 데이터 유실이 발생하면 저희같은 경우에는 초당 데이터들이 들어오기 때문에 조금만 지나도 많은 데이터가 유실되기 때문입니다. 그래서 이런 서비스의 경우 관리형으로 바꾸는게 장기적인 관점에서도 좋을 것 같다는 생각이 들었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;MQTT Broker의 경우 이미 AWS IoT Core을 쓰고 있기 때문에 제외하고 나머지 후보군,&lt;br /&gt;1. kafka &amp;amp; kafka connect&lt;br /&gt;2. Spark&lt;br /&gt;3. DB&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;후보 1 : kafka Connect &amp;amp; Kafka&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka&amp;nbsp;Connect와&amp;nbsp;Kafka는&amp;nbsp;관리형&amp;nbsp;서비스로&amp;nbsp;전환했습니다.&amp;nbsp;그&amp;nbsp;이유는&amp;nbsp;MQTT가&amp;nbsp;메시지를&amp;nbsp;장기&amp;nbsp;보관하지&amp;nbsp;않는&amp;nbsp;Pub/Sub&amp;nbsp;전달&amp;nbsp;시스템이기&amp;nbsp;때문입니다.&lt;br /&gt;&amp;bull; QoS0은&amp;nbsp;전달&amp;nbsp;즉시&amp;nbsp;메시지가&amp;nbsp;사라지고&lt;br /&gt;&amp;bull; QoS1/2도&amp;nbsp;정상&amp;nbsp;전달&amp;nbsp;후&amp;nbsp;삭제되며&lt;br /&gt;&amp;bull; Retain&amp;nbsp;메시지도&amp;nbsp;Topic당&amp;nbsp;1개만&amp;nbsp;유지됩니다.&lt;br /&gt;&lt;br /&gt;따라서&amp;nbsp;MQTT에서&amp;nbsp;Kafka로&amp;nbsp;메시지가&amp;nbsp;전달되지&amp;nbsp;않으면&amp;nbsp;데이터는&amp;nbsp;금방&amp;nbsp;유실됩니다.&amp;nbsp;반면&amp;nbsp;Kafka는&amp;nbsp;메시지를&amp;nbsp;디스크에&amp;nbsp;로그&amp;nbsp;형태로&amp;nbsp;저장하고,&amp;nbsp;retention&amp;nbsp;정책에&amp;nbsp;따라서만&amp;nbsp;삭제되므로&amp;nbsp;장기&amp;nbsp;보관이&amp;nbsp;가능합니다.&lt;br /&gt;이러한 이유로 메시지 저장 안정성을 확보하기 위해 Kafka와 Kafka Connect를 관리형 서비스로 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;후보 2 :&amp;nbsp; Spark&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark는&amp;nbsp;관리형&amp;nbsp;서비스로&amp;nbsp;변경하지&amp;nbsp;않았습니다.&amp;nbsp;AWS&amp;nbsp;EMR이&amp;nbsp;존재하긴&amp;nbsp;하지만&amp;nbsp;현재&amp;nbsp;운영&amp;nbsp;환경에서는&amp;nbsp;직접&amp;nbsp;EC2&amp;nbsp;위에&amp;nbsp;Spark를&amp;nbsp;구성하는&amp;nbsp;것이&amp;nbsp;더&amp;nbsp;적합하다고&amp;nbsp;판단했습니다.&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;데이터 유실 관점에서도 Spark는 checkpoint를 활용하면 장애 후 재시작 시 처리 지점을 복구할 수 있기 때문에, Kafka만 안정적으로 메시지를 보관한다면 Spark를 관리형으로 전환할 필요성은 낮다고 판단했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spark Checkpoint란?&lt;br /&gt;&lt;br /&gt;Spark에서 Checkpoint는 스트리밍 애플리케이션이 중단되거나 재시작되더라도 정확한 상태와 진행 위치를 복원할 수 있게 해주는 매우 중요한 안정성 메커니즘입니다. Spark는 체크포인트를 통해 스트리밍 애플리케이션이 어디까지 처리했는지, 어떤 상태를 유지하고 있었는지를 저장해 둬 장애나 재시작이 발생해도 이전 상태에서 이어서 정확하게 처리할 수 있게 해줍니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;후보 3 :&amp;nbsp; DB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scylla와&amp;nbsp;ClickHouse도&amp;nbsp;관리형&amp;nbsp;서비스로&amp;nbsp;전환하지&amp;nbsp;않았습니다.&amp;nbsp;AWS가&amp;nbsp;직접&amp;nbsp;제공하는&amp;nbsp;관리형&amp;nbsp;버전은&amp;nbsp;없지만&amp;nbsp;유사한&amp;nbsp;서비스들이&amp;nbsp;존재하긴&amp;nbsp;합니다.&amp;nbsp;그러나&amp;nbsp;현재까지&amp;nbsp;운영&amp;nbsp;과정에서&amp;nbsp;장애가&amp;nbsp;발생한&amp;nbsp;적이&amp;nbsp;없었고,&amp;nbsp;관리형&amp;nbsp;데이터베이스는&amp;nbsp;비용이&amp;nbsp;높기&amp;nbsp;때문에&amp;nbsp;비용&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론적으로, 필수적으로 이번에 바꿔야 할 관리형 서비스는&lt;br /&gt;&lt;/span&gt;Kafka Connect &amp;amp; Kafka로 결정했습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka &amp;amp; Kafka Connect 관리형 서비스 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka는 AWS MSK을 사용했습니다. AWS IoT Core와 연결하기 위한 방법을 찾아보던 중, AWS의 공식문서에 해당 방법을 잘 정리해논 링크가 있어 그 링크대로 처리했습니다. 간단하게 제가 어떻게 했는지 순서를 설명드리고 관리형 서비스 전환하면서 중요하게 생각했던 점들을 정리하도록 하겠습니다. 제가 참고한 링크는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/iot/how-to-integrate-aws-iot-core-with-amazon-msk/&quot;&gt;https://aws.amazon.com/ko/blogs/iot/how-to-integrate-aws-iot-core-with-amazon-msk/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765619264991&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;How to integrate AWS IoT Core with Amazon MSK | Amazon Web Services&quot; data-og-description=&quot;Post by Milo Oostergo, Principal Solutions Architect and Doron&amp;nbsp;Bleiberg, Senior Solution Architect, AWS Startups Introduction Monitoring IoT devices in real time can provide valuable insights that can help you maintain the reliability, availability, and p&quot; data-og-host=&quot;aws.amazon.com&quot; data-og-source-url=&quot;https://aws.amazon.com/ko/blogs/iot/how-to-integrate-aws-iot-core-with-amazon-msk/&quot; data-og-url=&quot;https://aws.amazon.com/blogs/iot/how-to-integrate-aws-iot-core-with-amazon-msk/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b27L8x/hyZPuMNSDi/sUeZSdSNa1LBortC689NuK/img.png?width=768&amp;amp;height=385&amp;amp;face=0_0_768_385,https://scrap.kakaocdn.net/dn/xqK5l/hyZPl41v8t/AfSniBvnGmHEAmnI01UUEk/img.png?width=768&amp;amp;height=385&amp;amp;face=0_0_768_385,https://scrap.kakaocdn.net/dn/4X7qP/hyZPc74pyM/5CfYnIujotlHWpMAKt4Qw0/img.png?width=1024&amp;amp;height=621&amp;amp;face=0_0_1024_621&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/iot/how-to-integrate-aws-iot-core-with-amazon-msk/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aws.amazon.com/ko/blogs/iot/how-to-integrate-aws-iot-core-with-amazon-msk/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b27L8x/hyZPuMNSDi/sUeZSdSNa1LBortC689NuK/img.png?width=768&amp;amp;height=385&amp;amp;face=0_0_768_385,https://scrap.kakaocdn.net/dn/xqK5l/hyZPl41v8t/AfSniBvnGmHEAmnI01UUEk/img.png?width=768&amp;amp;height=385&amp;amp;face=0_0_768_385,https://scrap.kakaocdn.net/dn/4X7qP/hyZPc74pyM/5CfYnIujotlHWpMAKt4Qw0/img.png?width=1024&amp;amp;height=621&amp;amp;face=0_0_1024_621');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How to integrate AWS IoT Core with Amazon MSK | Amazon Web Services&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Post by Milo Oostergo, Principal Solutions Architect and Doron&amp;nbsp;Bleiberg, Senior Solution Architect, AWS Startups Introduction Monitoring IoT devices in real time can provide valuable insights that can help you maintain the reliability, availability, and p&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;※ MSK 클러스터 생성 &amp;amp; AWS IoT Core 연결 설정 순서&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Amazon MSK 클러스터 생성&lt;br /&gt;&amp;bull; VPC&amp;nbsp;안에&amp;nbsp;MSK(Kafka)&amp;nbsp;클러스터를&amp;nbsp;생성합니다.&lt;br /&gt;&amp;bull; 인증&amp;nbsp;방식(SASL/SCRAM&amp;nbsp;등)을&amp;nbsp;설정합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Secrets Manager에 Kafka 인증 정보 저장&lt;br /&gt;&amp;bull; Kafka에&amp;nbsp;접속할&amp;nbsp;사용자&amp;nbsp;계정/비밀번호를&amp;nbsp;Secret으로&amp;nbsp;저장합니다.&lt;br /&gt;&amp;bull; IoT&amp;nbsp;Core에서&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;KMS로&amp;nbsp;암호화된&amp;nbsp;Secret을&amp;nbsp;준비합니다.&lt;br /&gt;&amp;bull; Secret&amp;nbsp;이름은&amp;nbsp;AmazonMSK_&amp;nbsp;접두어&amp;nbsp;필요.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Secret을 MSK 클러스터에 연동&lt;br /&gt;&amp;bull; 생성한&amp;nbsp;Secret을&amp;nbsp;MSK의&amp;nbsp;인증&amp;nbsp;정보로&amp;nbsp;연결(associate)합니다.&lt;br /&gt;&amp;bull; IoT&amp;nbsp;Core가&amp;nbsp;Kafka로&amp;nbsp;접근할&amp;nbsp;때&amp;nbsp;이&amp;nbsp;Secret을&amp;nbsp;사용합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;IoT Core가 사용할 IAM Role 생성&lt;br /&gt;&amp;bull; IoT Rule 엔진이 VPC와 MSK에 접근할 수 있도록 IAM Role과 정책을 설정합니다.&lt;br /&gt;&amp;bull; IoT Rule 엔진이 VPC와 MSK에 접근할 수 있도록 IAM Role과 정책을 설정합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;IoT Core에서 VPC Destination 생성&lt;br /&gt;&amp;bull; MSK가&amp;nbsp;존재하는&amp;nbsp;VPC/Subnet/Security&amp;nbsp;Group을&amp;nbsp;선택해&amp;nbsp;IoT&amp;nbsp;&amp;rarr;&amp;nbsp;MSK&amp;nbsp;연결&amp;nbsp;지점을&amp;nbsp;생성합니다.&lt;br /&gt;&amp;bull; Kafka bootstrap 서버 주소, 인증 방식(SASL/SCRAM) 설정&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;IoT Rule 생성 (MQTT &amp;rarr; Kafka 라우팅)&lt;br /&gt;&amp;bull; MQTT Topic에서 받은 메시지를 IoT Rule(SQL)로 필터링한 뒤 &amp;rarr;&amp;nbsp;MSK의&amp;nbsp;특정&amp;nbsp;Kafka&amp;nbsp;Topic으로&amp;nbsp;전달하도록&amp;nbsp;설정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;※ MSK에서 사용한 Security Settings&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저는 MSK를 사용할 때 사용한 Security Setting은&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #0f141a; text-align: start;&quot;&gt;Unauthenticated access : disabled&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #0f141a; text-align: start;&quot;&gt;SASL / SCRAM : enabled&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #0f141a; text-align: start;&quot;&gt;IAM : enabled&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0f141a; text-align: start;&quot;&gt;로 설정했습니다. 이렇게 한 이유는 AWS IoT Core는 SASL / SCRAM 방식으로 접근을 허용할 예정이고, 백엔드 서버나, Spark는 IAM 방식으로 접근을 허용할 예정이기 때문입니다. 이렇게 설정하고 나니, 아래와 같이 각각 접근 방식에 따른 url이 별도로 2개씩 생성되었습니다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0f141a; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtZdHt/dJMcaf6cP39/w25ULyKpbze3xnaBIQFlz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtZdHt/dJMcaf6cP39/w25ULyKpbze3xnaBIQFlz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtZdHt/dJMcaf6cP39/w25ULyKpbze3xnaBIQFlz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtZdHt%2FdJMcaf6cP39%2Fw25ULyKpbze3xnaBIQFlz0%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;1814&quot; height=&quot;848&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;※ AWS IoT Rule&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;여기서 IoT Rule이 Kafka Connect 역할을 대신 해줄 수 있습니다. AWS IoT Rule은 특정 MQTT Topic을 조건에 맞게 필터링한 뒤,&lt;br /&gt;Lambda / Kinesis / S3 / MSK 등 다양한 대상으로 메시지를 라우팅하는 역할을 하기 때문입니다. 물론 Kafka Connect가 더 성능은 좋지만, 저희는 MQTT Message 크기도 최적화하여 그렇게 크지 않고 초당 메세지 수가 수십만 건도 아니기 때문에 운영 편의성을 고려할 때 AWS IoT Rule로도 충분히 처리가 가능하다고 판단했습니다. 따라서 AWS IoT Rule을 통해 Kafka Connect 역할을 대체했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBQH41/dJMcafZq4PA/bnhPlZnweKR7MTbkD5kkEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBQH41/dJMcafZq4PA/bnhPlZnweKR7MTbkD5kkEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBQH41/dJMcafZq4PA/bnhPlZnweKR7MTbkD5kkEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBQH41%2FdJMcafZq4PA%2FbnhPlZnweKR7MTbkD5kkEk%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;1996&quot; height=&quot;856&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이러한 방식으로 Kafka와 Kafka Connect을 관리형 서비스로 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 실시간 IoT 데이터 파이프라인을 운영하면서 어떤 서비스를 관리형으로 전환해야 안정성을 확보할 수 있는지 면밀히 검토했습니다. 장애 발생 시 어떤 구성 요소에서 실제 데이터 유실이 발생할 가능성이 있는지를 기준으로 관리형 전환 여부를 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MQTT는 메시지를 장기 보관하지 않는 Pub/Sub 특성상 Kafka로 메시지가 전달되지 않으면 즉시 데이터가 사라집니다. 반면 Kafka는 디스크 기반 로그 저장 방식과 retention 정책을 통해 데이터 유실 없이 장기 보관이 가능하기 때문에, 실시간 파이프라인에서 가장 중요한 안정성 지점을 담당합니다. 이러한 특성을 고려하여 Kafka와 Kafka Connect는 관리형 서비스(MSK)로 전환하여 장애 시에도 안정적으로 메시지를 보관할 수 있도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark는 체크포인트 기능을 통해 장애가 발생해도 처리 상태를 복원할 수 있어, 현재 규모에서는 EC2 기반 자체 운영이 더 유연하고 효율적이라고 판단했습니다. ScyllaDB와 ClickHouse 또한 큰 장애 사례가 없고, 관리형 서비스 비용 대비 이점이 아직 크지 않아 계속 자체 운영하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로, 실시간 데이터의 유실 가능성과 운영 안정성을 기준으로 판단했을 때 이번에 반드시 관리형으로 전환해야 하는 구성 요소는 Kafka와 Kafka Connect였으며, AWS IoT Core와 Amazon MSK를 연동하는 방식으로 안정적인 구조를 구축하게 되었습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 최종적인 구조&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crNmgN/dJMcai9Ev0Z/qJyC0cnwBO6tWUFj2tPMsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crNmgN/dJMcai9Ev0Z/qJyC0cnwBO6tWUFj2tPMsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crNmgN/dJMcai9Ev0Z/qJyC0cnwBO6tWUFj2tPMsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrNmgN%2FdJMcai9Ev0Z%2FqJyC0cnwBO6tWUFj2tPMsk%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;1300&quot; height=&quot;394&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study/AWS_Service</category>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/59</guid>
      <comments>https://swmobenz.tistory.com/59#entry59comment</comments>
      <pubDate>Sat, 13 Dec 2025 18:21:47 +0900</pubDate>
    </item>
    <item>
      <title>Talos + Kubernetes 환경에서 LVM과 OpenEBS로 동적 스토리지 관리 구축</title>
      <link>https://swmobenz.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 talos OS 위에 K8s을 이용하여 온프렘 서버를 개발하고 있습니다. talos OS가 생소하신 분들도 있을 것 같은데, 간단하게 말해서&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;컨테이너 오케스트레이션(Kubernetes)에 최적화된 초경량 리눅스 운영체제&lt;span&gt;입니다. 나중에 기회가 된다면 talos OS에 대해서 좀 더 자세히 설명하는 글을 만들도록 하겠습니다. 메인은 k8s 환경에서 서비스들을 관리하고 있다는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;본론으로 돌아가서, 저는 이 k8s 환경에서 pod들을 관리하는 도중 디스크 관리 &amp;amp; PV(Persistent Volume)관리에 불편함을 느꼈습니다. 제가 불편함을 느낀 사항은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;talos의 Immutable 특성 때문에 디스크 추가 &amp;amp; 변경 시 talos machineconfig 파일을 매번 수정해줘야됨&lt;/li&gt;
&lt;li&gt;업데이트 시, 디스크 탐지 순서 변화로 PV 경로가 깨지면 모든 서비스에 장애가 발생함&lt;/li&gt;
&lt;li&gt;정적 PV 생성 시 매번 PV or SC(Storage class)를 만들어줘야함 (원하는 경로에 원하는 pv을 연결하기 위해서)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;실제로,저희는 Talos MachineConfig로 사전에 디스크 파티션을 구성하고, 각 디스크에 대응하는 StorageClass를 생성해 Pod에 연결하는 방식으로 클러스터를 운영했습니다. 그러나 새로운 Pod를 추가할 때마다 MachineConfig를 다시 수정해야 했고, 그 과정에서 디스크 인식 순서가 바뀌어 기존 PV 경로가 깨지는 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이처럼 디스크 관리가 어렵고, 설정 변경 후마다 재부팅이 필요한 점 때문에 Kubernetes에서 사용하는 PV를 어떻게 관리해야 할지 고민이 생겼습니다. 또한 온프렘 환경에서 4TB SSD 두 개를 사용하고 있어 RAID로 하나의 볼륨으로 묶어 관리하고 싶었고, 초기부터 큰 용량을 모두 할당하는 방식이 아니라 필요에 따라 점진적으로 확장하는 디스크 관리 방식이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이러한 요구사항을 효율적으로 처리하기 위해 저희는&lt;br /&gt;&lt;br /&gt;- LVM&lt;br /&gt;- Open EBS&lt;br /&gt;&lt;br /&gt;도입하여 디스크를 동적으로 관리하기로 결정했습니다. 또한 디스크 레벨에서 암호화를 적용해 데이터 보안성을 강화하고, 노드 단위 장애나 디스크 교체 상황에서도 안정적으로 볼륨을 재구성할 수 있는 구조를 마련하는 것을 목표로 했습니다.&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LVM &amp;amp; Open EBS 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LVM(Logical Volume Manager)이란 리눅스에서 물리 디스크를 묶고 추상화하여, 디스크 공간을 논리적으로 유연하게 관리할 수 있도록 해주는 기술입니다. 기본적으로 이해해야 할 핵심 구성요소는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PV(Physical Volume): 실제 물리 디스크 또는 파티션을 LVM에서 사용할 수 있도록 초기화한 단위&lt;/li&gt;
&lt;li&gt;VG(Volume Group): 여러 개의 PV를 하나의 큰 스토리지 풀처럼 묶어 관리하는 단위&lt;/li&gt;
&lt;li&gt;LV(Logical Volume): VG에서 필요한 만큼 공간을 잘라 만들어 사용하는 논리 디스크로, 실제 디스크처럼 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 물리 디스크가 여러 개라도 사용자가 필요에 따라 LV 크기를 확장하거나 축소할 수 있고, 새로운 디스크를 추가해 VG 용량을 늘릴 수도 있습니다. 또한 스냅샷 기능을 통해 특정 시점의 데이터를 보관할 수 있는 것도 LVM의 중요한 특징입니다.&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;실제로 디스크를 논리적인 volume으로 관리하기 때문에 더 유연하고 쉽게 관리할 수 있습니다.&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;1. PV 설정&lt;/p&gt;
&lt;pre id=&quot;code_1763351464330&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lsblk

apk add lvm2 util-linux

pvcreate /dev/dm-2 /dev/dm-3

# pvs
PV         VG    Fmt  Attr PSize  PFree 
/dev/dm-2  lvmvg lvm2 a--  &amp;lt;3.64t &amp;lt;2.80t
/dev/dm-3  lvmvg lvm2 a--  &amp;lt;3.64t &amp;lt;3.64t&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lsblk로 lvm으로 관리할 디스크를 확인합니다.&lt;/li&gt;
&lt;li&gt;lvm 처리를 위한 apk를 추가한 뒤, pvcreate 명령어로 원하는 디스크를 pv로 생성합니다.&lt;/li&gt;
&lt;li&gt;그 후 pvs 명령어를 치면, 내가 원하는 디스크가 제대로 lvm의 pv로 설정된 것을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. vg 설정&lt;/p&gt;
&lt;pre id=&quot;code_1763351629686&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vgcreate lvmvg /dev/dm-2 /dev/dm-3

# vgs
VG    #PV #LV #SN Attr   VSize  VFree 
lvmvg   2   0   0 wz--n- &amp;lt;7.28t &amp;lt;7.28t&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 생성한 pv를 내가 원하는 vg명(lvmvg)과 함께 같은 vg로 묶습니다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&amp;ldquo;같은 VG로 묶는다&amp;rdquo;는 표현은 &lt;/span&gt;&lt;b&gt;여러 개의 PV를 하나의 VG안에 포함시켜 하나의 큰 스토리지 풀로 만든다&lt;/b&gt;&lt;span&gt;는 의미입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이후에 추가적으로 vgextend 명령어로 언제든 유연하게 vg에 pv를 추가할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;VG(Volume Group)은 볼륨을 물리적으로 묶는 RAID처럼 여러 디스크를 하나로 합치는 개념과 비슷한 느낌이지만, RAID가 하드웨어&amp;middot;블록 단에서 실제 데이터를 병합하고 중복 저장까지 처리하는 방식이라면, LVM의 VG는 그 위 레벨에서 여러 물리 디스크를 하나의 논리적 스토리지 풀처럼 취급해 유연하게 용량을 분할&amp;middot;확장할 수 있게 해주는 논리적 집합이라는 점에서 차이가 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위의 작업까지는 k8s에서 작업하는 것이 아닌, os 레벨에서 디스크에 설정하는 옵션입니다.&lt;br /&gt;아래부터는 k8s에서 사용하기 위한 설정입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. OpenEBS의 LVM LocalPV 추가&lt;/p&gt;
&lt;pre id=&quot;code_1764388348143&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm repo add openebs-lvm https://openebs.github.io/lvm-localpv
helm repo update

helm install openebs-lvm openebs-lvm/lvm-localpv \
  --namespace openebs \
  --create-namespace&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenEBS의 LVM LocalPV(로컬 스토리지 플러그인) 설치&lt;/li&gt;
&lt;li&gt;&lt;span&gt;k8s가 각 노드의 &lt;/span&gt;로컬 디스크를 자동으로 LVM 볼륨으로 만들어 쓰게 해주는 스토리지 플러그인&lt;/li&gt;
&lt;li&gt;아래에서 더 자세히 설명드리도록 하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. lv 설정&lt;/p&gt;
&lt;pre id=&quot;code_1763360575061&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Kubernetes &amp;rarr; OpenEBS &amp;rarr; LVM(VG/LV) &amp;rarr; 디스크(PV)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위에서 생성한 VG를 StorageClass와 연결해두면, Kubernetes에서 PVC가 생성될 때마다 OpenEBS가 해당 VG를 스토리지 풀로 사용하여 필요한 용량만큼 LV를 자동으로 생성하고 이를 PV로 제공하게 됩니다. 즉, 여러 디스크(PV)를 하나의 VG로 묶어 두기만 하면, 이후에는 Kubernetes의 PVC 요청에 따라 OpenEBS가 그 VG 내부에서 LV를 동적으로 생성해주는 구조가 완성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenEBS CSI란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Kubernetes에서 &lt;/span&gt;노드 로컬 디스크를 기반으로 동적 스토리지를 제공하는 CSI 스토리지 솔루션&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenEBS의 구성 방식은 여러 가지가 있으며 대표적으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LocalPV HostPath&lt;span&gt;&amp;nbsp;: 특정 디렉터리를 PV로 매핑 (사용자가 직접 경로를 제공)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;LocalPV Device : 디바이스 목록을 OpenEBS에 등록 (PVC 생성 시 자동으로 디바이스를 할당)&lt;/li&gt;
&lt;li&gt;LocalPV LVM : OpenEBS가 LVM을 관리(PVC 요청 시 자동으로 LV 생성)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Mayastor : 완전한 분산 스토리지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기본적으로 OpenEBS는 &lt;/span&gt;로컬 디스크를 Kubernetes 방식으로 관리 가능하게 만드는 CSI&lt;span&gt;입니다. 원래는 hostPath를 사용해서 디스크를 관리했습니다. 그러다보니 경로를 만들고, PV or SC를 사전에 미리 만들어서 작업해야 한다는 단점이 생겼습니다. (내가 원하는 경로를 파드에 연결하기 위해선)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;그래서 저는 lvm을 사용해서 디스크를 관리하기로 결정했기 때문에 CSI 스토리지 솔루션으로 LocalPV LVM를 사용하기로 결정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;LocalPV LVM&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenEBS LocalPV LVM(Local Persistent Volume &amp;ndash; LVM)은 &lt;span&gt;각 Kubernetes 노드의 로컬 디스크를 기반으로 &lt;/span&gt;&lt;b&gt;LVM 볼륨을 동적으로 생성해 PV로 제공하는 CSI 드라이버&lt;/b&gt;&lt;span&gt;입니다. 이의 특징은,&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OS 레벨에서 LVM을 직접 구성할 필요 없음&lt;/li&gt;
&lt;li&gt;CSI가 PV 필요할 때 LV를 자동으로 만들고 제거&lt;/li&gt;
&lt;li&gt;Talos 같은 Immutable OS에서도 LVM을 &lt;i&gt;Kubernetes 레벨에서&lt;/i&gt; 운영할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 동작방식을 간단히 설명드리자면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PVC &amp;rarr; StorageClass &amp;rarr; LocalPV LVM Provisioner &amp;rarr; LVMNode Driver &amp;rarr; LV 생성&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PVC : 필요 용량 요청&lt;/li&gt;
&lt;li&gt;StorageClass : &amp;ldquo;LocalPV-LVM을 사용하세요&amp;rdquo;라고 지정 (+ 사용할 vg 지정)&lt;/li&gt;
&lt;li&gt;LVM Provisioner : 적절한 노드를 선택&lt;/li&gt;
&lt;li&gt;LVMNode Daemon : 해당 노드에서 lvcreate를 실행해 LV 생성&lt;/li&gt;
&lt;li&gt;PV(LV) : Pod가 실제로 쓰는 파일시스템 볼륨 생성&lt;b&gt;&lt;br /&gt;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 구조&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;[ 물리 디스크 1 ] -- pvcreate --┐&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ├── VG(lvmvg) &amp;larr; OpenEBS가 사용하는 풀&lt;br /&gt;&lt;br /&gt;[ 물리 디스크 2 ] -- pvcreate --┘&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;PVC 요청 &amp;rarr; OpenEBS &amp;rarr; lvcreate &amp;rarr; LV 생성 &amp;rarr; Kubernetes PV 생성 &amp;rarr; Pod에 마운트&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 하는일&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 119px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 28.4884%;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;width: 71.3953%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;height: 37px; width: 28.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;LVM 기반 로컬 볼륨 관리&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 37px; width: 71.3953%;&quot;&gt;&lt;span&gt;각 노드의 디스크를 LVM으로 묶고, 필요한 크기만큼 LV(Logical Volume)를 동적으로 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;height: 36px; width: 28.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;Kubernetes와 연동 (CSI)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px; width: 71.3953%;&quot;&gt;&lt;span&gt;PVC를 생성하면, LVM LocalPV가 자동으로 LV를 만들어서 Pod에 마운트&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px; width: 28.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;퍼포먼스 최적화&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 71.3953%;&quot;&gt;&lt;span&gt;외부 NAS나 NFS 없이, 로컬 디스크 I/O 성능 그대로 활용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px; width: 28.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;노드 로컬 스토리지 모델&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px; width: 71.3953%;&quot;&gt;&lt;span&gt;데이터는 해당 노드의 디스크에 저장 &amp;rarr; Pod가 그 노드에 스케줄되어야 접근 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이를 사용했을 때의 장점을 저희 상황에서 설명드리자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;Talos&amp;nbsp;MachineConfig&amp;nbsp;수정&amp;nbsp;불필요&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스크를&amp;nbsp;LVM용&amp;nbsp;PV로만&amp;nbsp;등록해두면,&amp;nbsp;LV&amp;nbsp;생성&amp;middot;삭제&amp;middot;확장은&amp;nbsp;OpenEBS가&amp;nbsp;자동&amp;nbsp;처리합니다.&lt;/li&gt;
&lt;li&gt;PVC&amp;nbsp;확장&amp;nbsp;요청도&amp;nbsp;LVM&amp;nbsp;CSI가&amp;nbsp;자동&amp;nbsp;반영하므로&amp;nbsp;Talos&amp;nbsp;설정을&amp;nbsp;다시&amp;nbsp;수정할&amp;nbsp;필요가&amp;nbsp;없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디스크 확장 용이&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새&amp;nbsp;디스크&amp;nbsp;추가&amp;nbsp;후&amp;nbsp;pvcreate&amp;nbsp;&amp;rarr;&amp;nbsp;vgextend만&amp;nbsp;수행하면&amp;nbsp;기존&amp;nbsp;VG에&amp;nbsp;쉽게&amp;nbsp;합칠&amp;nbsp;수&amp;nbsp;있습니다.&lt;/li&gt;
&lt;li&gt;VG&amp;nbsp;용량이&amp;nbsp;늘어나면&amp;nbsp;PVC에서도&amp;nbsp;즉시&amp;nbsp;추가&amp;nbsp;용량&amp;nbsp;사용이&amp;nbsp;가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디스크 순서 변경 문제 해결&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Talos에서는 디바이스 이름(/dev/nvme0n1 &amp;rarr; /dev/nvme1n1)이 재부팅&amp;middot;업그레이드&amp;middot;장치 추가 시 바뀌는 경우가 있습니다.&lt;/li&gt;
&lt;li&gt;Talos에서&amp;nbsp;디스크&amp;nbsp;이름이&amp;nbsp;바뀌어도,&amp;nbsp;LVM은&amp;nbsp;PV를&amp;nbsp;UUID로&amp;nbsp;추적하므로&amp;nbsp;VG/LV&amp;nbsp;동작에&amp;nbsp;영향이&amp;nbsp;없습니다.&lt;/li&gt;
&lt;li&gt;이로 인해 PV&amp;nbsp;경로&amp;nbsp;깨짐이나&amp;nbsp;MachineConfig&amp;nbsp;수정&amp;nbsp;같은&amp;nbsp;문제를&amp;nbsp;근본적으로&amp;nbsp;예방합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디스크 관리 간편화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러&amp;nbsp;디스크를&amp;nbsp;하나의&amp;nbsp;VG로&amp;nbsp;묶어&amp;nbsp;풀처럼&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있어&amp;nbsp;StorageClass/PV를&amp;nbsp;디스크별로&amp;nbsp;만들&amp;nbsp;필요가&amp;nbsp;없습니다.&lt;/li&gt;
&lt;li&gt;실질적인 볼륨 생성&amp;middot;확장&amp;middot;정책 관리는 k8s(LVM + OpenEBS)에서 이루어지므로 Talos의 불변성 제약과 충돌하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;# lvs&lt;br /&gt;&lt;br /&gt;&amp;nbsp; LV &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; VG&amp;nbsp; &amp;nbsp; Attr &amp;nbsp; &amp;nbsp; &amp;nbsp; LSize &amp;nbsp; Pool Origin Data%&amp;nbsp; Meta%&amp;nbsp; Move Log Cpy%Sync Convert&lt;br /&gt;&lt;br /&gt;&amp;nbsp; pvc-22948b11-7b57-41d5-9caa-36e7cf3f53b0 lvmvg -wi-ao----&amp;nbsp; 10.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-5eccd3cd-d307-48b1-a347-f01f218f8728 lvmvg -wi-ao----&amp;nbsp; 50.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-870dc156-6538-4fac-8b7c-3d6aba4325e6 lvmvg -wi-ao---- 200.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-8ee774c6-bc05-40f1-afa0-e0764d216ce8 lvmvg -wi-ao----&amp;nbsp; 20.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-b084ffa1-2a29-46e1-a2bf-ea29cf933f44 lvmvg -wi-ao----&amp;nbsp; 10.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-bc8d6a6f-a65b-4d24-9618-54b08877ed79 lvmvg -wi-ao---- 300.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-c38b0c34-376c-409f-b9bf-27b67cfcb6f2 lvmvg -wi-ao---- 200.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-ca579027-378f-4b12-9e49-936bcb96121f lvmvg -wi-ao----&amp;nbsp; 50.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-d7aa9ecf-e94b-43fa-8e99-d2a3063bf7ec lvmvg -wi-a-----&amp;nbsp; 20.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&amp;nbsp; pvc-f6000a73-5087-4f08-ab52-d9ab4c51ba0c lvmvg -wi-ao---- 100.00g &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;# pvs&lt;br /&gt;&lt;br /&gt;&amp;nbsp; PV &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; VG&amp;nbsp; &amp;nbsp; Fmt&amp;nbsp; Attr PSize&amp;nbsp; PFree&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&amp;nbsp; /dev/dm-2&amp;nbsp; lvmvg lvm2 a--&amp;nbsp; &amp;lt;3.64t&amp;nbsp; 2.70t&lt;br /&gt;&lt;br /&gt;&amp;nbsp; /dev/dm-3&amp;nbsp; lvmvg lvm2 a--&amp;nbsp; &amp;lt;3.64t &amp;lt;3.64t&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;# vgs&lt;br /&gt;&lt;br /&gt;&amp;nbsp; VG&amp;nbsp; &amp;nbsp; #PV #LV #SN Attr &amp;nbsp; VSize&amp;nbsp; VFree&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&amp;nbsp; lvmvg &amp;nbsp; 2&amp;nbsp; 10 &amp;nbsp; 0 wz--n- &amp;lt;7.28t &amp;lt;6.34t&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;보시는 바와 같이, 동적으로 lv가 생성되어 pod의 UUID로 매핑되어 관리되고 있는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-pm-slice=&quot;1 2 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e723c086-8c91-48b8-9601-40e5f6186280&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번에는 onprem 환경에서 디스크를 어떻게하면 효율적으로 관리할 수 있는지를 고민해봤습니다. 간단하게 흐름을 도식화하자면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biROvp/dJMcagKJghN/IKjmtmjwT0TOdhIpoBvIKK/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biROvp/dJMcagKJghN/IKjmtmjwT0TOdhIpoBvIKK/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biROvp/dJMcagKJghN/IKjmtmjwT0TOdhIpoBvIKK/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiROvp%2FdJMcagKJghN%2FIKjmtmjwT0TOdhIpoBvIKK%2Ftfile.svg&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;933&quot; height=&quot;700&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게 표현할 수 있습니다. OpenEBS LVM LocalPV는 LVM의 논리 볼륨 관리 기능을 그대로 활용하면서, Kubernetes의 PVC 요청에 따라 자동으로 LV(Logical Volume)를 생성하고, 이를 PV(PersistentVolume)로 바인딩하여 Pod에 마운트합니다. 이 방식은 외부 스토리지(NFS, Ceph 등)를 구성하지 않아도 로컬 디스크의 고속 I/O 성능을 그대로 유지할 수 있다는 점이 큰 장점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;특히, 여러 개의 디스크를 하나의 Volume Group으로 통합해두면, 노드 내에서 스토리지 용량을 유연하게 분배할 수 있으며, 개별 Pod가 필요한 만큼의 용량만큼 LVM LV를 자동으로 할당받을 수 있습니다. 이로써&amp;nbsp;Kubernetes&amp;nbsp;환경에서도&amp;nbsp;LVM의&amp;nbsp;유연한&amp;nbsp;용량&amp;nbsp;확장성과&amp;nbsp;OpenEBS의&amp;nbsp;동적&amp;nbsp;볼륨&amp;nbsp;관리&amp;nbsp;기능을&amp;nbsp;함께&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;이밖에도, 이를 사용했을 때의 비즈니스 임팩트를 정리해보자면,&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;650ce002-22a9-41fd-942a-6c6e58f93b31&quot; data-ke-size=&quot;size23&quot;&gt;개발자 관점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;81ae1865-2948-4257-9995-a9ccc612d1b1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7bbe495d-805b-4d51-9f03-210bdf77339f&quot;&gt;운영 효율성 향상
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;801e87dd-1494-4c88-ac11-351941f4ea10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;68514014-e294-4ac8-889a-3fac16085f4b&quot;&gt;디스크 확장, PV 생성, 스토리지 정책 변경 등 대부분의 작업이 Kubernetes 내부에서 자동화되기 때문에 운영 인력이 매번 Talos MachineConfig를 수정하거나 재부팅할 필요가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b8c46a34-24ee-4c85-bb66-18ca3a8b5eed&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;94f80cdc-7390-446b-a08a-033d5a4237e6&quot;&gt;서비스 가용성 증가
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7129d52e-3e54-414d-8ad1-58739308b310&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;0c285fea-2bfd-4cb9-b544-375fa9b5493e&quot;&gt;디스크 이름 변경, PV 경로 깨짐 등 Talos 기반 시스템의 대표적인 장애 원인이 LVM+OpenEBS 구조에서 사라짐.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;254be0bf-d5fa-435a-9850-f9600b6d4f92&quot;&gt;스토리지 확장 및 변경 시 노드 재부팅이 필요 없기 때문에 서비스 중단이 발생하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;79d56a2c-3a4f-457b-ba53-1a4c5fe9c176&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;140108ba-5d6f-41fe-829a-1f4fbb4228bf&quot;&gt;스토리지 확장성 확보
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ac5caa1c-8996-408d-9bae-f667965dac04&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;0b0c9e9d-2c07-4261-b8ba-4383402ac82f&quot;&gt;디스크 추가 시 OS 재구성이 필요 없고, vgextend로 즉시 스토리지 용량을 확장할 수 있어 비즈니스 성장에 맞춰 빠르게 대응 가능합니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;64d45682-26d5-4fa4-b7eb-fc6fea4d0f86&quot;&gt;데이터 증가나 신규 서비스 출시 때 스토리지 제약이 줄어들어 확장 속도와 기민성이 향상됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;372743b1-6a2a-4cee-8e0e-61e9da58a2e4&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;406e78f6-a82a-4b2e-b48d-db4eb4176c47&quot;&gt;인프라 변경 리스크 감소
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;82252ea4-c82c-414f-8661-9942351d296d&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b991c8af-2a3e-49c3-aaad-a591f504b605&quot;&gt;Talos의 불변성(Immutable OS) 특성으로 인해 자주 발생하던 &amp;ldquo;MachineConfig 수정 &amp;rarr; 재부팅 &amp;rarr; 예기치 않은 장애&amp;rdquo; 위험이 사라집니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;01492c0c-81b2-474d-97c7-197ce4b554ec&quot;&gt;스토리지 운영이 Kubernetes/LVM에서 일관되게 관리되어 예측 가능한 인프라 환경을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ea8c96fe-4830-4d8f-a879-3ed9dc174415&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;c2ca6002-e5fe-477e-bb91-c4dea1a8542b&quot;&gt;하드웨어 비용 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;89250eb5-39b5-49c7-9c48-84c1b503f07f&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6efb40a1-dcb0-4600-88d0-b78808f24517&quot;&gt;여러 디스크를 하나의 VG로 묶어 스토리지 효율성을 극대화할 수 있습니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a710d100-fa44-43d0-8f6d-89e2d2433f6f&quot;&gt;노드 단위로 디스크를 유연하게 사용할 수 있어 기존 인프라의 활용도가 올라가고, 불필요한 신규 하드웨어 구매를 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;21b214aa-ab47-4f7f-bb18-048891c481c7&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;21b214aa-ab47-4f7f-bb18-048891c481c7&quot; data-ke-size=&quot;size23&quot;&gt;일반 사용자(고객) 관점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6c5a3f2e-4541-4380-91b6-daad060ecf1f&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;735d9302-0c07-448f-b3ef-e3d9544b3e1d&quot;&gt;서비스가 멈추지 않는다
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;4d1bdaed-2761-4803-9f66-35621ab003c0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6f59baf5-b359-4004-ad12-0fc3bd002a98&quot;&gt;저장공간을 늘리거나 디스크 변경 시 서버 재부팅 없이 저장공간을 확장할 수 있기 때문에 서비스가 멈추지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;71640cc4-f39c-4c96-abaa-a4bb404e50c0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;622f17ae-4670-4dd6-b309-3dba31fe9410&quot;&gt;저장 공간을 마음대로 늘릴 수 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;f9e7eab5-9186-4084-9d3e-3a89d2eb9921&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;213d2b07-9ce7-43ab-b86f-c173fb05bc92&quot;&gt;필요한 시점에 원하는 만큼 확장이 간편하게 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;78788896-3937-4238-bbd5-98d7337735b2&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;1d754c64-41ea-4493-8205-e3129516e5ec&quot;&gt;여러 개의 디스크를 하나처럼 쓸 수 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;38ee88c2-04e1-42b3-8b74-96b6b6a25ccd&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ab2cf34d-9b26-4e21-abf0-3235ad10fe7a&quot;&gt;하나의 대용량 디스크를 사서 사용하는 것이 아닌, 여러 작은 디스크를 하나의 논리적 단위로 묶어 사용하기 때문에 디스크의 가격적인 이점을 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;df9039e5-fc64-47cf-afbc-3cb17ee478ed&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e1180b00-62fb-42fa-832b-c0e719c249c6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 기술을 도입하면 서버 저장 공간이 자동으로 관리되고, 장애가 줄어들며, 서비스를 끄지 않고도 바로 확장할 수 있어서 전체적으로 회사가 더 안정적이고 효율적으로 운영됩니다.&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;</description>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/58</guid>
      <comments>https://swmobenz.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 17 Nov 2025 12:58:21 +0900</pubDate>
    </item>
    <item>
      <title>Air-Gapped 온프렘 환경에서 Kubernetes Private Registry 구축</title>
      <link>https://swmobenz.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;공장 같은 곳에 Onprem 서버를 구축할 때 보면 상당히 고려해야 할 점이 많습니다. 대부분 고객들이 Onprem 환경을 원하는 이유는 기기 데이터를 외부에 노출시키고 싶지 않기 때문입니다. 따라서 대부분 환경이 인터넷 연결이 안되어있는 내부망을 사용합니다. 그러다보니 인터넷이 안돼 인터넷이 필요한 작업들은 할 수가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또한, 공장 기기들 때문인지 LTE 라우터를 통해 인터넷 연결을 잠시 하려고 해도 인터넷이 생각보다 잘 되지 않았습니다. 실제로 k8s내의 clickhouse 이미지를 pull 받아 파드 하나를 띄우려했는데...&amp;nbsp; 이미지 pull만 30분이 넘게 걸렸습니다. 그 뒤로는 limit이 걸렸는지 그 이후에 인터넷 연결이 매우 느려진 현상을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※&amp;nbsp; 그때 당시 문제 상황&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2932&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRXzjE/dJMb9NaKTd4/F1LLj2Awut8zuQSypRfkT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRXzjE/dJMb9NaKTd4/F1LLj2Awut8zuQSypRfkT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRXzjE/dJMb9NaKTd4/F1LLj2Awut8zuQSypRfkT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRXzjE%2FdJMb9NaKTd4%2FF1LLj2Awut8zuQSypRfkT0%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;2932&quot; height=&quot;160&quot; data-origin-width=&quot;2932&quot; data-origin-height=&quot;160&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;2898&quot; data-origin-height=&quot;1170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pMWqo/dJMb9QefRXe/oSKjqlsKXKBqoFsphcqcg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pMWqo/dJMb9QefRXe/oSKjqlsKXKBqoFsphcqcg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pMWqo/dJMb9QefRXe/oSKjqlsKXKBqoFsphcqcg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpMWqo%2FdJMb9QefRXe%2FoSKjqlsKXKBqoFsphcqcg0%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;2898&quot; height=&quot;1170&quot; data-origin-width=&quot;2898&quot; data-origin-height=&quot;1170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;물론 미리 이미지를 서버 내 캐시에 저장해놓고 사용하면 외부에서 이미지를 받아오지 않기 때문에 문제가 안되긴 합니다. 저희는 기본적으로 imagePullPolicy 전략을 IfNotPresent로 설정해놨기 때문입니다. 하지만 준비를 철저히 해 가도 요구사항이나 문제가 발생하는 경우가 종종 있어 그때마다 문제가 되었습니다... (항상 완벽한 건 없는 것 같습니다.) 그리고 나중에 설치해 동작 중인 서버 이미지 업데이트가 필요할 경우에도 이런 상황이면 큰 문제가 될 것입니다. 따라서 궁극적으로 이를 효율적으로 할 수 있는 방법이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 방식&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rP2ii/dJMb9L44ULH/rjUD0ad4ggpHD8eb2ON8Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rP2ii/dJMb9L44ULH/rjUD0ad4ggpHD8eb2ON8Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rP2ii/dJMb9L44ULH/rjUD0ad4ggpHD8eb2ON8Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrP2ii%2FdJMb9L44ULH%2FrjUD0ad4ggpHD8eb2ON8Mk%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;547&quot; height=&quot;368&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 저희의 서버 이미지는 현재 AWS ECR에 올라가 있습니다. 실제로 BE 서비스, FE 서비스 등의 이미지는 AWS ECR에서 관리하고 있고 k8s에 띄워진 해당 서비스 별 파드들의 이미지 경로 또한 AWS ECR 경로를 따르고 있습니다. 그러다보니 이미지 업데이트가 발생하고 Onprem Server에 해당 내용을 반영하려면, 인터넷을 탈 수 밖에 없습니다. 그래서 이러한 구조를 변경해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;해결 방법 ( 방향성 )&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 저희가 생각한 방법은, 현재 사용 중인 Docker Registry인 AWS ECR을 로컬 Registry로 전환하는 것입니다. 이렇게 하면 인터넷 연결 없이도 로컬에 갱신된 이미지를 보관하고, onprem 서버에 해당 이미지를 배포할 수 있습니다. 물론 새로운 이미지를 빌드하기 위해서는 여전히 외부 패키지나 의존성 설치를 위해 인터넷 연결이 필요하긴 하지만, 이는 굳이 공장에서 하지 않아도 되기 때문에 이러한 방법으로 하는 것이 저희의 문제를 해결해 줄 수 있을 것이라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그래서 이 문제에 대한 방향성은 local docker registry를 사용해서 인터넷 연결 없이 onprem 서버 버전 업데이트를 하는 방향으로 정했습니다. 그렇다면, 어떻게 저희가 어떻게 했는지 설명드리도록 하겠습니다.&lt;br /&gt;설명드리기에 앞서 저희의 서버 환경은&lt;br /&gt;&lt;br /&gt;- Talos OS (1.9.6 version)&lt;br /&gt;- k8s ( 1.3.4 version)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법 1 : Local 컴퓨터 안에서 Docker Registry 생성 후 k8s 서버에서 사용&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4tirh/dJMb87UwRLv/kzR7LPaLFmK7vAVpb3KWQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4tirh/dJMb87UwRLv/kzR7LPaLFmK7vAVpb3KWQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4tirh/dJMb87UwRLv/kzR7LPaLFmK7vAVpb3KWQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4tirh%2FdJMb87UwRLv%2FkzR7LPaLFmK7vAVpb3KWQk%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;1410&quot; height=&quot;598&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Docker Registry을 Local에서 생성 후,&amp;nbsp; Local Docker Registry에 이미지를 푸시합니다. 그리고 연결이 유지된 상태에서, 해당 도커이미지에 푸시된 이미지 경로를 다른 pod에서 사용하게끔 설정하면, 저희의 로컬 컴퓨터에 있는 이미지를 사용해서, pod들이 뜨게 되기 때문에 인터넷이 약한 제한된 상황에서도 사용할 수 있었습니다. 실제로 위의 저 문제를 임시방편으로 해결하기 위해 현장에서 이러한 방법으로 로컬에 이미지를 빌드하고, 별도로 띄운 레지스트리를 통해 운영중인 파드 업데이트를 할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법을 사용할때 발생한 이슈 사항들은,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Local Docker Registry에 이미지 푸시를 위한 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 로컬 Registry에 이미지를 푸시하려면 반드시 레지스트리 주소를 포함한 이름으로 태그해야 합니다.&lt;/li&gt;
&lt;li&gt;따라서 이미지를 빌드하고 해당 이미지에 맞는 태그를 붙인 후에, push를 해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1762064638211&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker pull nginx:latest

docker tag nginx:latest localhost:5000/nginx:latest

docker push localhost:5000/nginx:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;HTTP 이미지 사용을 위한 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Talos는&amp;nbsp;&lt;/span&gt;k8s의 컨테이너 런타임으로 containerd를 사용 (&lt;b&gt; K8s에서 &lt;span&gt;Pod를 실제로 실행시키는 역할 -&amp;gt;&lt;/span&gt; 컨테이너 런타임)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;따라서, containerd가 기본적으로 허용하지 않는 HTTP 이미지를 허용하기 위한 설정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1762065121939&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;machine:
  registries:
    config:
      https://&amp;lt;로컬 ip&amp;gt;:&amp;lt;docker registry 포트&amp;gt;
        tls:
          insecureSkipVerify: true
    mirrors:
      https://&amp;lt;로컬 ip&amp;gt;:&amp;lt;docker registry 포트&amp;gt;:
        endpoints:
          - https://&amp;lt;로컬 ip&amp;gt;:&amp;lt;docker registry 포트&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;talos Node IP 고정 필요&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원래는 라우터를 통해 talos 서버의 ip을 고정했는데, 서버와 직접 이더넷으로 연결하기 때문에 서버가 해당 talos 서버의 ip을 제대로 인식하지 못하는 문제 발생&lt;/li&gt;
&lt;li&gt;local 서버가 라우터 역할을 할 수 있게끔 설정 (dnsmasq 사용)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dnsmasq는 경량 DNS 포워더(forwarder)이자 DHCP 서버로, 소규모 네트워크에서 널리 사용되는 오픈소스 소프트웨어&lt;/li&gt;
&lt;li&gt;dnsmasq로 talos 서버의 ip을 할당할 수 있는 라우터 대역대를 맞추고, 해당 서버에 static ip 부여&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;하지만 이 방법은 임시방편이였습니다. 이미지를 계속해서 talos 서버에 저장할 수도 없고, 업데이트 동안 라우터를 잠시 빼놔야하기 때문에 번거로웠습니다. 그동안은 데이터가 유실되는 문제도 있었습니다. (물론 라우터를 통해서 해도 되지만 그건 그것대로 번거로움...)&lt;br /&gt;&lt;br /&gt;그래서 저희가 생각한 2번째 방법은 local docker registry가 아닌, k8s 내부에 private registry을 띄우고 그걸 사용하게끔 하는 것이였습니다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;해결 방법 2 : Local 컴퓨터 안에서 Docker Registry 생성 후 k8s 서버에서 사용&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmdKxW/dJMcaeMKGqS/dIhPaAQvH6yXakC0kxb0nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmdKxW/dJMcaeMKGqS/dIhPaAQvH6yXakC0kxb0nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmdKxW/dJMcaeMKGqS/dIhPaAQvH6yXakC0kxb0nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmdKxW%2FdJMcaeMKGqS%2FdIhPaAQvH6yXakC0kxb0nk%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;1746&quot; height=&quot;842&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;k8s 내에서 docker registry을 별도의 pod로 띄웁니다. docker registry가 사용하는 저장소는 minio을 사용했습니다. 백엔드 스토리지로 MinIO를 사용한 이유는, 3노드 분산 환경에서 이미지 데이터의 고가용성과 자동 동기화를 보장하기 위함입니다. MinIO의&amp;nbsp;오브젝트&amp;nbsp;스토리지&amp;nbsp;특성을&amp;nbsp;활용하면,&amp;nbsp;한&amp;nbsp;노드에서&amp;nbsp;푸시된&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;이를 통해 레지스트리 관리 복잡도를 줄이고, 장애 시에도 데이터 일관성을 유지할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법을 사용할때 발생한 이슈 사항들은,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;k8s Docker Registry에 이미지 푸시를 위한 설정 ( + local docker engine 설정 추가)&lt;/h4&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;추가적으로, local docker engine에 insecure-registries 설정을 해줘야됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2996&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nzFrg/dJMcaiVU79Q/OzuL39BfhKMkNftBgvUJHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nzFrg/dJMcaiVU79Q/OzuL39BfhKMkNftBgvUJHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nzFrg/dJMcaiVU79Q/OzuL39BfhKMkNftBgvUJHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnzFrg%2FdJMcaiVU79Q%2FOzuL39BfhKMkNftBgvUJHK%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;2996&quot; height=&quot;1382&quot; data-origin-width=&quot;2996&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;※ local docker engine에서 해당 처리를 해야하는 이유&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;local docker registry&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;k8s docker registry&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Docker Engine 위치&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;macOS 안에서 직접 실행&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;macOS 안의 리눅스 VM 안에서 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Registry 위치&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;같은 Docker 네트워크(로컬 컨테이너)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Kubernetes 안에 존재 (port-forward로 연결)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;통신 경로&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Docker 내부 브리지 &amp;rarr; 바로 연결&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Docker VM &amp;rarr; Mac 호스트 &amp;rarr; port-forward &amp;rarr; K8s Pod&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;docker push localhost:5001&lt;span&gt; 바로 성공&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;docker push localhost:5000&lt;span&gt; 하면 연결 안 됨 (timeout)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예전엔 Docker와 registry가 같은 내부 네트워크라 HTTP로도 안전하게 통신할 수 있었지만, 지금은 Docker Engine이 VM 안에서 외부(맥) 로 나가기 때문에 HTTP(비암호화)&amp;nbsp;통신을&amp;nbsp;허용하려면&amp;nbsp;insecure&amp;nbsp;설정이&amp;nbsp;필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Docker Registry 설정&lt;/h4&gt;
&lt;pre id=&quot;code_1762071117146&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: registry-config
  namespace: docker-registry
data:
  config.yml: |
	//...
    storage:
      s3:
 	// s3 설정..
      redirect:
        disable: true
    http:
      addr: :5000

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry
  namespace: docker-registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
	// docker registry 설정
      volumes:
        - name: config
          configMap:
            name: registry-config

---

apiVersion: v1
kind: Service
metadata:
  name: registry
  namespace: docker-registry
spec:
  type: NodePort
  selector:
    app: registry
  ports:
    - port: 5000
      targetPort: 5000
      nodePort: 32000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Registry 설정 때문에 엄청 오랜시간이 걸렸습니다. 생각보다 고려해야 할 것이 많았기 때문입니다. 위의 k8s yaml 파일의 주요 내용은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;node ip로&amp;nbsp; registry을 외부 노출
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원래는, Service dns로 접근하게 하고 싶었습니다. 하지만 기본적으로 Talos의 containerd는 k8s 밖에 있어 service dns 명으로 접근이 불가능합니다. 물론 talos에서 해당 dns을 인식할 수 있게 하는 방법도 있겠지만, 권장하는 방식은 nodePort로 service를 외부에 노출시키는 방법이였습니다. 그래서 nodePort을 노출했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;minio 연결을 위한 설정 추가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mino 연결을 위한 storage.s3 설정을 추가했습니다. 기본적인 설정은 간단하게 쉽게 처리했지만 문제가 된 점은 pull 일 때입니다. push는 문제 없게 됐는데, push 후 다른 파드에서 해당 이미지를 pull 받을 때 이미지를 찾지 못하는 에러가 발생했습니다.&lt;/li&gt;
&lt;li&gt;이 문제를 해결하기 위해 docker registry에서 redirect.disable : true 로 설정해 해결했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp;redirect.disable: true&lt;/b&gt; 옵션은 Docker Registry의 S3 backend 동작 방식과 관련된 설정입니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 옵션은 레지스트리가 클라이언트에게 S3의 오브젝트 URL로 리다이렉트할지 여부를 제어합니다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;기본적으로 Docker Registry가 S3를 백엔드로 사용할 때는 클라이언트가 docker pul1 등을 통해 이미지 blob을 요청하면, Registry는 S3의 presigned URL(임시 접근 URL)을 발급하고, 클라이언트를 S3로 직접 리다이렉트 시켜서 blob을 다운로드하게 합니다. 따라서 MinIO나 내부 S3 endpoint를 쓰는 경우, docker registry가 endpoint(minio-tenant-hl.minio.svc.cluster.local:9000)에 직접 접근할 수 없습니다. 따라서 해당 옵션을 true로 설정해 이미지를 찾지 못하는 에러를 해결했습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;HTTP 이미지 사용을 위한 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;위와 동일하게 talos level에서 containerd가 HTTP 이미지를 허용하기 위한 설정이 필요합니다. 이때 설정은 로컬 ip가 아닌, k8s의 private registry의 node ip입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1762069868067&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;machine:
  registries:
    config:
      https://&amp;lt;docker-registry-node-ip&amp;gt;:&amp;lt;docker-registry-포트&amp;gt;
        tls:
          insecureSkipVerify: true
    mirrors:
      https://&amp;lt;docker-registry-node-ip&amp;gt;:&amp;lt;docker-registry-포트&amp;gt;:
        endpoints:
          - https://&amp;lt;docker-registry-node-ip&amp;gt;:&amp;lt;docker-registry-포트&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;이 방법으로 이미지 자체의 registry을 k8s 내부에서 관리하여 장기적으로 효율적인 이미지 관리 시스템을 구축 할 수 있었습니다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번에 온프렘의 air gapped 상태에서 컨테이너 이미지를 관리하기 위한 시스템을 구축했습니다. 원래도 talos OS에서 기본적으로 이미지를 캐싱해놓긴 하지만, 온프렘 특성상 현장에서 문제가 많이 발생할 수 있고 캐싱이 언제까지 되는지도 확실하게 알 수 없었기 때문에 장기적인 관점에서 운영 상 이미지를 구축하는 자체 private registry 설정은 필수적이였습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study/AWS_Service</category>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/57</guid>
      <comments>https://swmobenz.tistory.com/57#entry57comment</comments>
      <pubDate>Mon, 27 Oct 2025 00:40:11 +0900</pubDate>
    </item>
    <item>
      <title>안전하고 효율적인 대용량 CSV 추출 아키텍처</title>
      <link>https://swmobenz.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 실시간 공장 기기들의 데이터를 취급하고 있습니다. 데이터는 1초, 10초 등 특정 주기로 한번씩 올라옵니다. 기기 한개를 기준으로 1초에 한번씩 데이터들이 올라온다고 가정하면, 하루동안 &lt;span style=&quot;background-color: #ffffff; color: #001d35; text-align: start;&quot;&gt;24시간 &amp;times; 60분 &amp;times; 60초 = 86,400 개의 데이터가 올라옵니다. 물론 사용자는 기기 한개가 아니라 기기 여러개를 관리하고 있을 것이고, 1초, 5초, 10초 등등 특정 주기로 올라오는 데이터가 많아지면 이 데이터는 기하급수적으로 늘어날 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #001d35; text-align: start;&quot;&gt;&amp;nbsp;저희는 이러한 대용량 데이터를 관리하고 있는데, 이번에 구현해야 할 기능은 특정 범위에 해당하는 데이터를 추출하는 기능입니다. 추출은 csv로 추출합니다. 원래는 csv을 추출할 때, csv을 생성하는 라이브러리(OpenCSV 등)을 써서 단순히 데이터를 조회하고 csv로 변환해서 반환하는 것으로 로직을 구현합니다. 근데, 이번에는 대용량 데이터를 csv로 추출하는 경우도 존재하기 때문에 이를 어떻게 처리할지에 대해 고민했고, 여러 방법을 정리해 최종적으로 가장 안전하고 효율적인 방법을 찾아 처리했습니다. 그 방법은,&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #001d35; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;임시 파일에 데이터를 스트리밍 방식으로 csv 파일로 변환해 저장하고,&lt;br /&gt;저장한 파일을 S3에 업로드해 반환 값을 저장하는 방식&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다. 제가 고려한 여러 방법에 대한 장단점과 최종적으로 제가 왜 이러한 방법을 사용해 csv 변환 및 저장을 처리했는지 설명드리도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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.&amp;nbsp; In-Memory&amp;nbsp;버퍼&amp;nbsp;방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 메모리 버퍼(ByteArrayOutputStream)에 전부 작성한 뒤 그 바이트 배열(byte[])을 반환하는 방식&lt;/li&gt;
&lt;li&gt;파일을 디스크에 저장하지 않고 메모리에 담았다가 한번에 응답하는 방식으로 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757864761442&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ResponseEntity&amp;lt;byte[]&amp;gt; exportCsv() {
	// 1. ByteArrayOutputStream 생성 (메모리 버퍼 준비)
	// 2. OutputStreamWriter + CSVWriter를 연결
	// 3. 데이터 작성
	// 4. flush() 후 toByteArray()로 메모리에서 byte[] 배열 추출
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게 in memory 버퍼 방식으로 구현하면 디스크에 데이터를 쓰지 않기 때문에 디스크 정리를 할 필요가 없고 메모리를 사용하기 때문에 구현이 단순하고 빠릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만, 문서에 쓸 데이터의 크기 만큼 힙 메모리를 차지합니다. 즉, 데이터의 양과 비례하게 메모리 사용량이 증가합니다. 이 말은 결국 데이터가 많아지면, OOM이 발생할 가능성이 높다는 말이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;실제로 이러한 방식을 사용하여&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;동시에 여러 대용량 데이터 처리 관련된 요청을 보낸 후&lt;/span&gt; 모니터링 한 결과, 실제로 대용량 데이터 csv 추출 로직 전에는 heap memory가 전체 heap 메모리 중 약 1.7% 사용 중이였던 것에 반해, csv 추출이 끝난 시점에는 23.8%을 사용한 것을 확인할 수 있었습니다. 물론 csv 추출된 후 GC가 동작해 자동으로 heap 메모리를 정리해 다시 안정적인 상태로 돌아갔지만 이러한 csv 추출을 동시에 여러번, 더 큰 용량의 데이터를 추출한다면, OOM 에러가 발생할 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GuhQr/btsQzhwkCsz/uk2KXcP2DiVWd2FKYikSG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GuhQr/btsQzhwkCsz/uk2KXcP2DiVWd2FKYikSG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GuhQr/btsQzhwkCsz/uk2KXcP2DiVWd2FKYikSG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGuhQr%2FbtsQzhwkCsz%2Fuk2KXcP2DiVWd2FKYikSG1%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;1900&quot; height=&quot;607&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;간단하게 장단점을 설명드리자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;디스크를 사용하지 않기 때문에 임시 파일 &amp;amp; 디스크를 정리할 필요가 없다.&lt;/li&gt;
&lt;li&gt;byte[] 크기를 알 수 있어서 Content-Length 헤더 설정을 통해 클라이언트 진행률 표시나 캐싱 처리에 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서 크기만큼 힙 메로리를 사용하기 때문에 OOM 발생 위험이 있다.&lt;/li&gt;
&lt;li&gt;동시 요청이 많으면 요청이 끝나면 GC를 통해 힙 메모리를 정리하는 과정을 거치기 때문에 GC 압박이 크다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;따라서 이렇게 이 방식은 저희 같이 대용량 데이터를 처리할 때는 전체 데이터를 한번에 조회해 메모리에 저장 후 csv로 변환 및 처리하기 때문에 메모리 위험이 커 사용하기 힘들다고 판단했습니다. 그래서 이렇게 메모리를 사용하는 방식이 아닌 다른 방식을 고려해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.&amp;nbsp; Streaming&amp;nbsp;Direct&amp;nbsp;Download&amp;nbsp;방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대량 데이터를 CSV로 내보내되, &lt;span&gt;메모리에 다 올리지 않고&lt;/span&gt; 스트리밍으로 바로 응답하는 방식&lt;/li&gt;
&lt;li&gt;데이터를 flush()를 호출하기 전까진 버퍼(메모리)에서 관리하다가 flush가 호출될 때 HTTP 응답의 chunk가 생성돼 응답하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757948172122&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ResponseEntity&amp;lt;StreamingResponseBody&amp;gt; exportCsvStream() {
        // 1. StreamingResponseBody 생성 (OutputStream 직접 전달)
        // 2. 헤더 작성
        // 3. 데이터 페이지 단위/루프 단위로 바로바로 작성
        // 4. 네트워크 효율을 위해 일정 간격마다 flush
        // 5. ResponseEntity&amp;lt;StreamingResponseBody&amp;gt; 로 반환
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게 구현하면, 데이터 양의 따라 Memory 사용량이 증가하는 것이 아닌 flush 간격에 따른 데이터 chunk 단위만큼만 메모리를 사용하기 때문에(버퍼에 데이터를 저장하므로) OOM 발생 위험을 대폭 줄일 수 있습니다. 따라서 메모리 사용량이 거의 없습니다. 하지만 여러 단위(청크) 만큼 데이터를 보내기 때문에 동일한 데이터를 보내더라도 네트워크에서 더 많은 작은 패킷으로 쪼개져서 전송됩니다. 그래서 네트워크 오버헤드가 발생할 수 있습니다. 따라서 chunk를 관리하는 flush의 주기를 효율적으로 관리해야 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;실제로 이러한 방식을 사용하여 동시에 여러 대용량 데이터 처리 관련된 요청을 보낸 후 모니터링 한 결과, 실제로 대용량 데이터 csv 추출 로직 전과 후에 메모리 사용량의 차이가 거의 나타나지 않았습니다. chunk 크기 만큼 데이터를 메모리(버퍼)에 저장하기 때문에 heap 메모리 사용은 증가했지만 이전과 다르게 확연히 작은 양의 메모리 사용을 볼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k1P8g/btsQAXdJX9O/ybK5Qj7dPucjzuTtbi7xkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k1P8g/btsQAXdJX9O/ybK5Qj7dPucjzuTtbi7xkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k1P8g/btsQAXdJX9O/ybK5Qj7dPucjzuTtbi7xkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk1P8g%2FbtsQAXdJX9O%2FybK5Qj7dPucjzuTtbi7xkk%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;2864&quot; height=&quot;1036&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&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;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 사용 최소화 &amp;rarr; 초대형 CSV/PDF에도 안전하다.&lt;/li&gt;
&lt;li&gt;서버가 쓰는 즉시 클라이언트가 다운로드 &amp;rarr; 총 소요 시간 단축이 단축된다.&lt;/li&gt;
&lt;li&gt;백프레셔(backpressure): 네트워크가 느리면 생성도 자연스럽게 늦어져 안정적으로 메모리 관리가 가능하고 꾸준히 데이터를 받기 때문에 진행률 확인에 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chunk 단위로 데이터를 전송하기 때문에 파일 전체 크기(Content-length)을 알 수 없다.&lt;/li&gt;
&lt;li&gt;데이터 전송 중간에 에러가 나면 온전하지 않은 데이터가 남는다.&lt;/li&gt;
&lt;li&gt;네트워크 이동이 잦다. (chunk 단위로 데이터를 스트리밍하여 전송하기 때문에)&lt;/li&gt;
&lt;li&gt;streaming 처리를 위한 코드가 약간 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;따라서 이 방식은 메모리 사용을 안정적으로 처리할 수 있어 효율적으로 데이터 csv 추출 기능을 활용할 수 있다고 생각했습니다. 그래서 이 방법을 써도 됐습니다. 하지만 chunk 단위로 네트워크를 통해 데이터를 전송하기 때문에 flush 주기를 효율적으로 관리해야 된다. 라는 한계를 좀 더 잘 풀면 더 간편하게 안정적으로 서비스를 운영할 수 있겠다 라는 생각이 들었습니다. 또한 저희는 onprem 환경의 제한적인 네트워크 상황에서 되도록이면 네트워크의 &lt;span style=&quot;background-color: #ffffff; color: #001d35; text-align: start;&quot;&gt;Bandwidth을 생각하지 않았으면 좋겠다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Streaming&amp;nbsp;+&amp;nbsp;S3&amp;nbsp;Offload&amp;nbsp;방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 방법은 2번과 동일한 방식을 통해 스트리밍으로 csv 파일을 생성합니다. 하지만 2번과 다른점은 2번은 이렇게 스트리밍 방식으로 데이터를 처리하돼, 네트워크를 타는게 아닌 서버 내의 tmp파일에 csv 파일을 생성하고 최종적으로 파일이 다 만들어지면, 이 파일을 사용자에게 전송하는 형식으로 로직을 처리하는 방식입니다. 따라서 메모리 사용량은 위의 Data Streaming 방식과 크게 차이가 나진 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757986564396&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ResponseEntity&amp;lt;String&amp;gt; exportCsvS3() {
	// 1. 서버 로컬 tmp 파일 생성 (임시 CSV 저장소 준비)
	// 2. DB에서 데이터 페이지 단위로 조회
	// 3. tmp 파일에 CSVWriter로 데이터 작성 (페이지/배치 단위)
	// 4. 파일 작성이 끝나면 flush &amp;amp; close
	// 5. 완료된 tmp 파일을 S3에 업로드
	// 6. tmp 파일 정리
	// 7. S3 객체에 대한 Presigned URL 생성
	// 8. ResponseEntity&amp;lt;String&amp;gt; 으로 Presigned URL 반환
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게되면, 사용자에게 실시간으로 데이터를 주거나 진행률 확인을 하게 할 순 없지만, 네트워크 이동은 한번만 이루어지고 이를 만약 S3에 업로드 해(Onprem 환경에서는 Minio 사용) presigned url로 사용자에게 전달한다면, 더 가벼운 데이터로 사용자에게 전달할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;물론, 임시 파일을 생성하기 때문에 이후에 이 파일을 삭제하거나, S3나 Minio에 업로드 시에 이만큼의 오버헤드가 발생한다는 단점은 존재합니다. 하지만 사용자가 재다운로드 하거나 할 때 효율적으로 관리할 수 있을 것이라고 생각해 이 방법을 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 장단점을 설명드리자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 바로 S3에서 다운로드 &amp;rarr; 서버 부하가 감소한다. (네트워크 offload)&lt;/li&gt;
&lt;li&gt;S3로 생성된 파일이 관리되기 때문에 파일 재사용 가능하다. (동일 요청 시 바로 기존 URL 제공)&lt;/li&gt;
&lt;li&gt;최종적으로 만들어진 하나의 파일 url을 주기 때문에 네트워크 패킷이 가볍고, 네트워크 오버헤드가 발생하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;csv 생성 시 사용한 tmp 파일 처리가 필요하다.&lt;/li&gt;
&lt;li&gt;s3 &amp;amp; minio에 업로드하는 오버헤드가 발생한다.&lt;/li&gt;
&lt;li&gt;구현이 3가지 방법 중 가장 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;따라서 저희는 csv 생성 시 그렇게 엄청 오래 걸리지도 않기 때문에 진행률이나 Conent-Length가 필수적이지 않으며, 한번의 네트워크 간의 통신을 통해 완성된 데이터 csv파일을 받을 수 있는 3번째 방법(데이터 스트리밍 &amp;amp; tmp 파일 생성 후 S3 업로드) 을 사용하기로 결정했습니다. 또한, 이미 S3 &amp;amp; Minio 환경이 갖춰져 있는 상태에서 사용자 재 다운로드 같은 use case도 커버할 수 있어 더 효율적이라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼, 지금까지 소개한 방식들의 플로우 및 장단점을 다시한번 정리하고 어떤 상황에서 이러한 방식이 적합한지 제가 생각한 기준을 표를 통해 공유드리도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 장단점 비교&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6278%; text-align: center;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 21.7444%; text-align: center;&quot;&gt;플로우&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; text-align: center;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%; text-align: center;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6278%; text-align: left;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;In-Memory 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.7444%; text-align: left;&quot;&gt;1. DB 조회&lt;br /&gt;2. 전체 CSV 메모리에 생성&lt;br /&gt;3. 완료 후 한번에 응답&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; text-align: left;&quot;&gt;- 구현 단순&lt;br /&gt;- Content-Length&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;제공&lt;br /&gt;- 다운로드 진행률 등 제공&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%; text-align: left;&quot;&gt;- 대용량 시 OOM 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6278%; text-align: left;&quot;&gt;&lt;b&gt;Data Streaming 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.7444%; text-align: left;&quot;&gt;1. DB 조회&lt;br /&gt;2. CSV 행/페이지 단위로 작성 &lt;br /&gt;3. flush 시마다 네트워크로 전송 (chunked)&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; text-align: left;&quot;&gt;- 메모리 사용 최소화&lt;br /&gt;- 대규모 데이터에 안전&lt;br /&gt;- 클라이언트가 즉시 다운로드 시작&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%; text-align: left;&quot;&gt;- Content-Length 모름&lt;br /&gt;- 네트워크 중단 시 불완성한 파일 전송&lt;br /&gt;- flush 전략에 따라 패킷/효율 차이 발생&lt;br /&gt;- 구현 복잡성 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6278%; text-align: left;&quot;&gt;&lt;b&gt;Streaming + S3 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.7444%; text-align: left;&quot;&gt;1. DB 조회&lt;br /&gt;2. 서버 tmp 파일에 스트리밍 방식으로 CSV 생성&lt;br /&gt;3. 완료 후 S3 업로드&lt;br /&gt;4. Presigned URL 반환&lt;/td&gt;
&lt;td style=&quot;width: 24.8837%; text-align: left;&quot;&gt;- 서버 부하 최소화&lt;br /&gt;- 재시작 및 파일 재사용 가능&lt;br /&gt;- 클라이언트가 안정적으로 데이터 다운&lt;br /&gt;&lt;br /&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%; text-align: left;&quot;&gt;- tmp 파일 관리 필요&lt;br /&gt;- s3 &amp;amp; minio 등 업로드 비용 발생&lt;br /&gt;- 구현 복잡성 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 이러한 장단점을 통해 간단하게 어떤 상황에서 사용할지 좋은지 정리하자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;In-Memory&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소규모 데이터(수천 행, 수 MB 이하) 추출 시 용이&lt;/li&gt;
&lt;li&gt;빠른 프로토타입/테스트 시 용이&lt;/li&gt;
&lt;li&gt;작고 단순할 때는 베스트이다. (메모리 = 파일 크기, 구현 단순)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Data Streaming&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식&lt;/span&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;/li&gt;
&lt;li&gt;실시간 다운로드 UX 필요할 때 용이 (진행률 제공, 바로 다운로드 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Streaming + tmp &amp;amp; S3 방식&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초대용량 데이터(GB급)&lt;/li&gt;
&lt;li&gt;네트워크 불안정 환경에 용이 (한번만 네트워크 이동으로 전송 가능)&lt;/li&gt;
&lt;li&gt;여러 사용자 반복 다운로드 필요할 때 용이 (S3와 같은 별도의 저장소에서 관리)&lt;/li&gt;
&lt;li&gt;안정성과 재사용성 최강이지만 대신 즉시성&amp;darr;, 구현 복잡도&amp;uarr;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;확실히 구현 방법은 여러가지고 내 상황을 맞는 최적의 구현 방법을 선택하는 것이 중요한 것 같습니다. 모든 방법마다 장단점이 존재하는데 내 상황에서 가장 고려해야 할 점이 무엇인지 찾고, 그 점을 잘 커버할 수 있는 방법을 찾는게 능력인 것 같습니다... 그러기 위해선 더 많은 케이스를 경험하고 문제가 될 수 있는 상황을 가정하여 성능 테스트를 해보는 것이 중요한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 저의 케이스에서 가장 중요한 것은 대용량 데이터를 커버할 수 있는 방법 찾기였고 그걸 커버할 수 있는 방법은 위에서 소개한 방법 중 Streaming&amp;nbsp;Direct&amp;nbsp;Download&amp;nbsp;방식과 Streaming + S3 Offload 방식이였습니다. 그 중 제한된 네트워크 대역폭에서도 효율적으로 처리가 가능하고 재사용 및 csv 관리가 가능한 방식인 후자의 방식을 선택했습니다.&lt;/p&gt;</description>
      <category>Study/DEVELOP</category>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/56</guid>
      <comments>https://swmobenz.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 10 Sep 2025 00:56:54 +0900</pubDate>
    </item>
    <item>
      <title>분산 환경에서 안전한 스케줄링: Spring ShedLock 적용</title>
      <link>https://swmobenz.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 공장의 여러 기기에서 실시간으로 사용자 데이터를 가져와 관리하는 B2B 서비스를 개발하고 있습니다. 그래서 특별히 장애가 발생하지 않는 한 특정 사용자(공장)의 기기 데이터가 한번에 다 올라오지 않는 경우는 거의 없습니다. 물론, 그 공장이 전체 소등을 한다던가 하지 않는 이상은 말이죠... 그러다보니 특별한 일 없이, 특정 공장의 모든 기기에서 데이터가 만약 올라오지 않는다면 이는 큰 장애로 이어졌을 가능성이 높아 빠르게 처리해야되죠. (일단, 고객보다 먼저 알아서 빠르게 처리할 수 있어야 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이를 처리하기 위해 저희는 15분 주기마다 사용자의 기기 데이터들을 조회해서 올라온 데이터가 하나도 없는지 조회하여 없다면 알림을 보내는 로직을 구현해 사용하고 있습니다. 이를 구현하기 위해서 Spring Scheduler를 통해 15분을 간격으로 특정 로직을 돌리게끔 로직을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;처음에는 이게 문제가 되지 않았습니다. 하지만, 서버를 1개가 아닌 여러개로 Scale Out 해야 할 상황이 오니... 문제가 되었습니다. 저는 15분마다 한번씩 실행하게 하고 싶은데, 서버의 갯수만큼 실행하는 문제가 발생했습니다. 따라서 이를 처리하기 위해 ShedLock을 도입하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ShedLock이란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;shedLock은 분산 환경에서 스케줄러(Job/Scheduler) 작업이 여러 서버에서 동시에 실행되는 것을 막기 위한 라이브러리입니다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 주요한 키워드는 &quot;분산 환경&quot; 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;ShedLock의 기본 원리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;DB에 락 정보를 남기고 처리 시 이 락 정보를 확인해 선점한 인스턴스만 실행하도록 보장하는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 락 기반 스케줄러 제어 : 여러 서버 인스턴스가 동시에 예약되어 있는 Job을 실행하지 않도록 보장 (1번만 실행되도록 보장)&lt;/li&gt;
&lt;li&gt;스토리지 기반 처리 : Lock 정보를 공용 스토리지(DB)에 저장 및 조회&lt;/li&gt;
&lt;li&gt;Spring 환경에 친화적 : @SchedulerLock 어노테이션을 통해 기존 @Scheduled 메서드에 간단히 적용 가능&lt;/li&gt;
&lt;li&gt;시간 기반 락 해제 지원 : lockAtMostFor, lockAtLeastFor 같은 파라미터로 Lock 유지 시간 제어 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스케줄러가 실행될 때, DB의 shedlock 테이블에서 해당 Job 이름(name)으로 락을 획득하려고 시도합니다.&lt;/li&gt;
&lt;li&gt;락을 획득하면 획득과 동시에 row을 Insert &amp;amp; update 해 현재 실행 중임을 기록합니다.&lt;/li&gt;
&lt;li&gt;다른 인스턴스가 접근 시 Lock과 관련된 row을 확인하고 현재 실행중임을 확인하면, Job을 실행하지 않습니다.&lt;/li&gt;
&lt;li&gt;락을 선점한 인스턴스가 작업을 진행하고 작업이 끝나면 Lock을 해제합니다.&lt;/li&gt;
&lt;/ol&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;옵션&lt;/td&gt;
&lt;td style=&quot;width: 20.2326%;&quot;&gt;의미&lt;/td&gt;
&lt;td style=&quot;width: 32.3255%;&quot;&gt;사용 목적&lt;/td&gt;
&lt;td style=&quot;width: 29.3024%;&quot;&gt;방식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;&lt;span&gt;&lt;b&gt;lockAtMostFor&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.2326%;&quot;&gt;&lt;span&gt;락의 최대 유지 시간 &lt;br /&gt;(자동 해제 시점)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3255%;&quot;&gt;&lt;span&gt;서버 장애로 락이 풀리지 않는 상황 방지&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.3024%;&quot;&gt;&lt;span&gt;Job 최대 실행 시간 + 여유로 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;&lt;span&gt;&lt;b&gt;lockAtLeastFor&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.2326%;&quot;&gt;&lt;span&gt;락의 최소 유지 시간&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3255%;&quot;&gt;&lt;span&gt;Job이 너무 빨리 끝나 중복 실행되는 것 방지&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.3024%;&quot;&gt;&lt;span&gt;크론 주기 또는 실행 최소 주기로 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면, 지금부터 원래 코드를 간단하게 소개하고 이를 scale out 했을 때 어떤 문제가 발생하는지 설명드리고 shedLock을 통해 어떻게 해결했는지 설명드리도록 하겠습니다.&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원래 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1758693627821&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(
	initialDelay = 15 * 60 * 1000,
	fixedRate = 15 * 60 * 1000,
)
fun scheduledJobEvery15Min() {
	// 로직...			
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 시작 후 15분 후부터 시작해 매번 15분마다 아래의 로직을 실행하는 JOB 생성 및 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span&gt;&amp;lt; 이 코드의 문제점 &amp;gt;&lt;br /&gt;서버를 scale out 할 경우 여러 서버가 각각 15분마다 아래 로직을 실행한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RsF4d/btsQLgFHOlh/aVfKeyZaoHbMvK7mtOKiIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RsF4d/btsQLgFHOlh/aVfKeyZaoHbMvK7mtOKiIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RsF4d/btsQLgFHOlh/aVfKeyZaoHbMvK7mtOKiIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRsF4d%2FbtsQLgFHOlh%2FaVfKeyZaoHbMvK7mtOKiIk%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;1466&quot; height=&quot;780&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림과 같이 예를 들어 서버가 3개라고 가정했을 때 위의 코드는 서버마다 각각 15분마다 한번씩 특정 Job을 실행합니다. 따라서 서버의 개수만큼 동일한 작업을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;따라서 이 문제를 해결하기 위해, shedLock을 적용해 Lock을 관리했습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ShedLock 적용 코드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;shedLock 설정을 위한 부분도 존재하지만 여기 설명에서는 사용 관점의 설명만 드리도록 하겠습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;하나 이슈 사항이 있었다면, 기본적으로 DB 스키마가 고정되어 있었습니다. (DB 명, 컬럼 명 등 )-&amp;gt; 물론 설정을 통해 변경 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lock 관리는 기존 PostgresDB의 shedlock table을 사용했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758694571768&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@SchedulerLock(name = &quot;Job-Name&quot;, lockAtMostFor = &quot;PT1M&quot;, lockAtLeastFor = &quot;PT10S&quot;)
@Scheduled(
	initialDelay = 15 * 60 * 1000,
	fixedRate = 15 * 60 * 1000,
)
fun scheduledJobEvery15Min() {
	// 로직...			
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 시작 후 15분 후부터 시작해 매번 15분마다 아래의 로직을 실행하는 JOB 생성 및 처리&lt;/li&gt;
&lt;li&gt;이 때, Shedlock table에 Job-Name 이라는 이름으로 락이 생성돼 이를 통해 하나의 서버만 실행되도록 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2244&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnOD93/btsQNu3FL3a/F0VyRAwKSfkxg4nWmwk8N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnOD93/btsQNu3FL3a/F0VyRAwKSfkxg4nWmwk8N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnOD93/btsQNu3FL3a/F0VyRAwKSfkxg4nWmwk8N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnOD93%2FbtsQNu3FL3a%2FF0VyRAwKSfkxg4nWmwk8N1%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;2244&quot; height=&quot;1080&quot; data-origin-width=&quot;2244&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, 이 경우 이슈가 하나 있었습니다.&lt;br /&gt;지금 저의 @scheduled 관련된 코드를 보시면, 서버 실행 후 15분마다 저 Job을 실행하게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그런데, 서버 시작은 서버마다 다를 수도 있기 때문에 만약 서버 간 실행 시간 차이가 10분 이상이 난다면, 둘다 실행되는 문제가 있었습니다. 예시를 통해 쉽게 설명드리도록 하겠습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 A가 11:00분에 시작, 서버 B가 11:08분에 시작, 서버 C가 11:12분에 시작했다고 가정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnyfGv/btsQNBhhorp/LJ5ZsSMdLMLF5lJKDFX4ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnyfGv/btsQNBhhorp/LJ5ZsSMdLMLF5lJKDFX4ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnyfGv/btsQNBhhorp/LJ5ZsSMdLMLF5lJKDFX4ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnyfGv%2FbtsQNBhhorp%2FLJ5ZsSMdLMLF5lJKDFX4ak%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;433&quot; height=&quot;140&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버 A 기준 11:15분에 락 획득 시도 -&amp;gt; 아무도 락을 안걸고 있기 때문에 락 생성 후 JOB 처리 (위의 사진처럼 Lock 생성)&lt;/li&gt;
&lt;li&gt;서버 B가 11:23분에 락 획득 시도 -&amp;gt; 기존 락의 locked_until 보다 더 빠르기 때문에 Lock 획득 실패&lt;/li&gt;
&lt;li&gt;서버 C가 11:27분에 락 획득 시도 -&amp;gt; 기존 락의 Locked_until 보다 나중이기 때문에 Lock 획득 가능 -&amp;gt; JOB 처리&lt;/li&gt;
&lt;/ol&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;cron&lt;/b&gt;&lt;/span&gt;을 통해 절대 시간 기준으로 실행 주기를 맞추는 것이였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Cron 적용 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1758698395077&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SchedulerLock(name = &quot;Job-Name&quot;, lockAtMostFor = &quot;PT1M&quot;, lockAtLeastFor = &quot;PT10S&quot;)
@Scheduled(cron = &quot;0 0/15 * * * *&quot;)
fun scheduledJobEvery15Min() {
	// 로직...			
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cron은 스케줄을 표현하는 규칙(표현식)입니다.&lt;/li&gt;
&lt;li&gt;Spring 기준으로는 6자리(초 ~ 요일) 혹은 7자리(초 ~ 연도)까지 쓸 수 있습니다.&lt;/li&gt;
&lt;li&gt;형식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초 &lt;span&gt;&amp;nbsp; &lt;/span&gt;분 &lt;span&gt;&amp;nbsp; &lt;/span&gt;시 &lt;span&gt;&amp;nbsp; &lt;/span&gt;일(날짜) &lt;span&gt;&amp;nbsp; &lt;/span&gt;월 &lt;span&gt;&amp;nbsp; &lt;/span&gt;요일 &lt;span&gt;&amp;nbsp; &lt;/span&gt;[연도(옵션)]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;span&gt;*&lt;/span&gt; &amp;rarr; 모든 값&lt;/li&gt;
&lt;li&gt;&lt;span&gt;/&lt;/span&gt; &amp;rarr; 주기 (예: &lt;span&gt;*/5&lt;/span&gt; = 5 단위로)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;,&lt;/span&gt; &amp;rarr; 여러 값 지정 (예: &lt;span&gt;1,15&lt;/span&gt; = 1일과 15일)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;-&lt;/span&gt; &amp;rarr; 범위 (예: &lt;span&gt;9-17&lt;/span&gt; = 9시~17시)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;저같은 경우에는0 0/15 * * * * 이렇게 설정했고, 매 15분 마다 실행한다는 의미입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 설정을 사용하면 서버의 시작 시점과 관계없이 서버 시간 기준으로 매 15분마다 Job이 실행되어 실행 시점이 일정하게 맞춰집니다. 즉,&amp;nbsp;서버&amp;nbsp;시작&amp;nbsp;시간에&amp;nbsp;따라&amp;nbsp;실행&amp;nbsp;주기가&amp;nbsp;어긋나는&amp;nbsp;문제가&amp;nbsp;발생하지&amp;nbsp;않습니다.&lt;br /&gt;이처럼&amp;nbsp;cron&amp;nbsp;job과&amp;nbsp;ShedLock을&amp;nbsp;함께&amp;nbsp;사용하면&amp;nbsp;여러&amp;nbsp;서버&amp;nbsp;인스턴스가&amp;nbsp;동시에&amp;nbsp;같은&amp;nbsp;Job을&amp;nbsp;실행하지&amp;nbsp;않도록&amp;nbsp;안전하게&amp;nbsp;제어할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게해서 기존의 단일 인스턴스에서 발생하지 않은 문제를 찾고, 여러 테스트를 통해 문제를 해결했습니다. 확실히 단일 인스턴스 말고 scale-out을 통한 여러 인스턴스로 확장할 때는 다양한 고려사항이 필요한 것 같습니다. 단순히 성능을 위해 인스턴스를 확장하고 끝나면... 거기서 오는 추가적인 trade off 들로 인해 여러 문제가 발생할 것 같다는 생각이 들었습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/55</guid>
      <comments>https://swmobenz.tistory.com/55#entry55comment</comments>
      <pubDate>Tue, 2 Sep 2025 00:20:56 +0900</pubDate>
    </item>
    <item>
      <title>NATS JetStream으로 안정적인 보고서 생성 파이프라인 구축하기</title>
      <link>https://swmobenz.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저희는 이번에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;보고서 생성 로직&lt;/span&gt;을 구현했습니다. 저희의 보고서 생성 과정은 대용량 데이터를 조회하고, 이를 LLM 기반으로 분석하여 결과를 도출하는 일련의 절차로 구성됩니다. 이 과정은 DB, FE 서버, BE 서버 등 여러 인프라 자원을 동시에 많이 사용하는 작업입니다. 따라서 다수의 요청이 동시에 발생할 경우, 서버에 과부하가 걸리지 않도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;동시 처리량을 제어할 필요&lt;/span&gt;가 있었습니다.&amp;nbsp;또한, 보고서 생성은 처리 시간이 길고 도중에 실패할 가능성도 존재하기 때문에, 작업이 실패했을 경우 자동 재시작이 가능해야 했으며, 각 요청의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;성공 및 실패 여부를 명확히 보장&lt;/span&gt;할 수 있는 메커니즘이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 정리하자면, 이러한 요구사항을 충족하기 위해 저희가 고려한 사항은 다음과 같습니다.&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;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;안정적으로 완료될 수 있도록 보장해야한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;보고서 상태를 명확하게 관리해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이러한 요구사항을 충족하기 위해, 요청을 큐에 적재하고 순차적&amp;middot;제한적으로 처리할 수 있도록 메세지 큐를 도입하였습니다. 이를 통해 안정적으로 리소스를 관리하면서도 장시간 작업의 신뢰성을 확보할 수 있었습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;애초에는 별도의 메세지 큐 서버를 두지 않고 처리하려고 했습니다. 인프라 구성이 복잡해지면 그만큼 관리 리소스가 늘어나기 때문입니다. 그러나 프론트엔드나 백엔드 내부에서 단순히 인메모리 큐를 사용하는 방식은 영속성이 보장되지 않아 문제가 있었습니다. 결국 안정성과 확장성을 위해 별도의 메세지 큐 서버를 운영하기로 결정하였고, 여러 후보군을 조사한 끝에 최종적으로 RabbitMQ와 NATS 두 가지로 선택지를 좁혔습니다.&lt;br /&gt;&lt;br /&gt;&amp;nbsp;이제, 저희가 최종적으로 어떤 메시지 큐를 선택했는지, 그리고 그 이유에 대해 정리해보겠습니다. 또한, 메세지 큐를 도입해서 어떤식으로 보고서 관련된 처리를 진행했는지 정리해보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;RabbitMQ란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;RabbitMQ는 오랜 기간 사용되어 온 메시지 브로커로, AMQP 프로토콜(Advanced Message Queuing Protocol)을 구현한 시스템입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;RabbitMQ 아키텍처의 핵심은 프로듀서가 메시지를 익스체인지(exchange)에 발행하면, 익스체인지가 미리 정의된 라우팅 규칙에 따라 메시지를 하나 이상의 큐(queue)로 전달하고, 최종적으로 소비자는 해당 큐를 구독하여 메시지를 받아 처리하는 구조입니다. 기본적으로 Erlang 언어로 되어 있고 전통적인 브로커 아키텍처로 신뢰성 높은 메시지 전달과 다양한 패턴 구현에 적합하며 설정과 관리 면에서 더 많은 기능을 제공하지만, NATS에 비해 운영 오버헤드가 큰 편입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;NATS란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NATS란 대표적인 메세징 시스템 중 하나로, 경량화 &amp;amp; 고성능을 목표로 설계된 오픈소스 메세지 브로커로, Pub/Sub(발행/구독) 패턴을 기본으로 합니다. 별도의 큐 브로커 없이 Subject을 통해 메세지를 발행 &amp;amp; 구독하는 방식이며, 기본 버전인 Core 버전은 메세지를 저장하지 않고 실시간으로 전달합니다. 근본적으로 중앙 브로커의 복잡한 라우팅 없이 동작하는 간결한 구조를 갖고 있어 NATS 서버 자체는 10MB 미만의 매우 가벼운 바이너리 파일로 구현되었고, 설정도 최소화되어 있습니다. 기본 Core 버전이 아닌 jetstream 버전을 쓰면, 메세지 자체를 스트림으로 영속화해 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;RabbitMQ vs NATS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 사용 관점의 차이&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 15.814%;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.5348%;&quot;&gt;&lt;span&gt;&lt;b&gt;NATS JetStream&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 44.5349%;&quot;&gt;RabbitMQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 15.814%;&quot;&gt;아키텍처&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.5348%;&quot;&gt;&lt;span&gt;Go 언어 기반의 메세지 브로커&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 44.5349%;&quot;&gt;&lt;span&gt;Erlang 언어 기반의 메세지 브로커&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 15.814%;&quot;&gt;&lt;span&gt;CPU 사용량&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.5348%;&quot;&gt;Low&amp;ndash;Moderate&amp;nbsp;수준으로&amp;nbsp;매우&amp;nbsp;효율적&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 44.5349%;&quot;&gt;Erlang VM 위에서 동작하며, 미러 큐&amp;middot;디스크 지속성 활성화 시 CPU 부하가 크게 늘어남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 15.814%;&quot;&gt;Memory 사용량&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.5348%;&quot;&gt;Core&amp;nbsp;NATS는&amp;nbsp;10&amp;ndash;50&amp;nbsp;MB&amp;nbsp;정도로&amp;nbsp;가볍지만,&amp;nbsp;JetStream&amp;nbsp;활성화&amp;nbsp;시&amp;nbsp;100&amp;nbsp;MB~수&amp;nbsp;GB까지&amp;nbsp;소요될&amp;nbsp;수&amp;nbsp;있음&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 44.5349%;&quot;&gt;기본 설정에서 100&amp;ndash;500 MB 수준이지만, 큐 백로그나 HA 큐(미러 큐) 사용 시 디스크 + 메모리가 수 GB 단위로 급증 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 운영 관점의 차이&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 16.0465%; height: 19px;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;width: 37.907%; height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;NATS JetStream&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.9302%; height: 19px;&quot;&gt;RabbitMQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 16.0465%; height: 19px;&quot;&gt;&lt;span&gt;설치/설정 단순성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.907%; height: 19px;&quot;&gt;단일 바이너리 + 최소 config로 설정 가능&lt;/td&gt;
&lt;td style=&quot;width: 45.9302%; height: 19px;&quot;&gt;Erlang&amp;nbsp;cookie,&amp;nbsp;PV,&amp;nbsp;plugin&amp;nbsp;등&amp;nbsp;다수&amp;nbsp;설정&amp;nbsp;필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 16.0465%; height: 19px;&quot;&gt;&lt;span&gt;클러스터 관리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.907%; height: 19px;&quot;&gt;Raft&amp;nbsp;기반&amp;nbsp;자동&amp;nbsp;failover&amp;nbsp;(self-healing)&lt;/td&gt;
&lt;td style=&quot;width: 45.9302%; height: 19px;&quot;&gt;미러&amp;nbsp;큐/Quorum&amp;nbsp;큐&amp;nbsp;등&amp;nbsp;HA&amp;nbsp;설정&amp;nbsp;복잡,&amp;nbsp;상태&amp;nbsp;동기화&amp;nbsp;지연&amp;nbsp;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 16.0465%; height: 19px;&quot;&gt;&lt;span&gt;운영 복잡도&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.907%; height: 19px;&quot;&gt;낮음 (minimal ops overhead)&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 45.9302%; height: 19px;&quot;&gt;중간~높음&amp;nbsp;(Erlang&amp;nbsp;지식&amp;nbsp;+&amp;nbsp;다양한&amp;nbsp;플러그인&amp;nbsp;관리&amp;nbsp;필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0465%;&quot;&gt;&lt;span&gt;서버 실행 시간&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.907%;&quot;&gt;빠름&lt;/td&gt;
&lt;td style=&quot;width: 45.9302%;&quot;&gt;상대적으로 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저지연&amp;middot;고성능, 간단한 운영 구조가 중요하면 &amp;rarr; NATS JetStream&lt;/li&gt;
&lt;li&gt;복잡한 라우팅, 다양한 프로토콜 지원, 기업용 안정성을 중시하면 &amp;rarr; RabbitMQ&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 차이가 있습니다. 저희는 메시지 유실 시 재실행으로 처리할 수 있기 때문에 메시지 보존 자체보다는 운영의 효율성과 단순성을 더 중시했습니다. 따라서, 클라우드와 온프레미스 환경 모두에서 빠르고 안정적으로 동작하면서도 관리 부담이 적은 메시지 큐가 필요했기에, 설정이 쉽고 CPU &amp;amp; 메모리 사용량이 효율적인 NATS를 선택했습니다. 또한, 메시지 영속화가 가능하도록 Core가 아닌 JetStream 버전을 도입하기로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Nats 서버 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;신뢰성 있는 작업 큐 설정을 위해 아래와 같은 설정으로 Nats 큐 설정을 진행했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&amp;nbsp;아래의 설정을 보기 전 간단하게 알아야 할 개념은, stream은 Nats jetstream에서 메세지를 저장/관리하는 단위로 kafka의 &quot;토픽&quot;과 비슷한 개념이며 subject로 발행되는 모든 메세지를 수집합니다.&lt;/span&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table id=&quot;2466d03f-ff7e-8080-8efc-fd699930be49&quot; style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;설정 그룹&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;속성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설정값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-806b-a051-f53a846071ee&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&lt;b&gt;Connection&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8026-a719-eea2d105f8e4&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;serverUrl&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;NATS 서버 접속 URL&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;nats://localhost:4222&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8067-bae0-e10e48f516e4&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;connectionTimeout&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;연결 타임아웃(초)&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8072-94e3-f08d19abc0ef&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;reconnectWait&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;재접속 대기시간(초)&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8088-b94c-d88c26d9717d&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;maxReconnects&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;최대 재접속 시도 횟수&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8098-adb2-df2b79a3d612&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8099-aa73-dffcadd5652b&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&lt;b&gt;Stream&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-80f3-b60a-e3062571d93e&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;stream&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;생성할 Stream 이름&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8011-8990-f5c388e2617f&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;subject&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;Stream에 바인딩할 subject&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;message.process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-80d3-87db-ca94c0597ee0&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;storageType&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;스토리지 타입&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;StorageType.File&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-804f-9026-fdfadf9e5b05&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;retentionPolicy&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;보관 정책&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;RetentionPolicy.WorkQueue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8031-9959-d45266fc12ac&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;maxAge&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;메시지 최대 보관 기간&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;7일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8037-954d-eb88c2960346&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-802f-859e-ef521ad30ab5&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&lt;b&gt;Consumer&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-80ae-ae8a-c885b87652ae&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;consumer&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;Durable Consumer 이름&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;job_cons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-80df-8cac-f3be76f7bb23&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;filterSubject&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;구독할 subject 필터&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;message.process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-80ab-be94-ff74fa61902e&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;deliverPolicy&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;전달 정책 (All: 처음부터 전체 전달)&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;DeliverPolicy.All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-804c-a5fa-dc3adfccc24e&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;ackWait&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;ACK 대기 시간(초)&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-80a8-85e2-d225a706ab05&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;maxAckPending&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;최대 미응답 ACK 보류 수 (동시에 처리할 수 있는 메세지 수)&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-804f-89df-f79e045745cc&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;maxDeliver&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;최대 재전달(재시도) 횟수&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2466d03f-ff7e-8059-929d-da3bf96ff02a&quot;&gt;
&lt;td id=&quot;d|nB&quot; style=&quot;width: 14.4186%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td id=&quot;]na[&quot; style=&quot;width: 19.4187%;&quot;&gt;ackPolicy&lt;/td&gt;
&lt;td id=&quot;COmm&quot; style=&quot;width: 30.6976%;&quot;&gt;ACK 정책 (Explicit 명시적 ACK 필요)&lt;/td&gt;
&lt;td id=&quot;LqiR&quot; style=&quot;width: 35.4652%;&quot;&gt;AckPolicy.Explicit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 설정 중 주요한 설정은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안전한 연결을 위해 서버와의 연결 시도 시 최대 5초동안 연결을 기다린다.&lt;/li&gt;
&lt;li&gt;만약 연결이 끊기면 2초 후 다시 시도하며, 최대 10번까지 재연결을 시도한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stream 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메세지를 메모리가 아닌 디스크 파일(StorageType.File)에 영속적으로 저장한다.&lt;/li&gt;
&lt;li&gt;메세지가 한 Consumer에게만 전달되면 삭제되는 정책으로 메세지를 관리한다.(RetentionPolicy.WorkQueue)&lt;/li&gt;
&lt;li&gt;메세지는 최대 7일동안까지만 유효하다. 7일동안 처리되지 않은 메세지는 자동으로 버려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;job_cons 이름을 기준으로 만약 Consumer가 끊겼다가 다시 연결돼도 이어서 메세지 이어서 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;특정 subject를 가진 메세지(message.process)만 가져와 처리한다.&lt;/li&gt;
&lt;li&gt;stream에 저장된 모든 메세지를 처음부터 전달 받아 처리한다( DeliverPolicy.All)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NATS 서버가 &amp;ldquo;WorkQueue 보관 정책&amp;rdquo;을 사용하는 스트림에서는 DeliverPolicy가 All 외에는 허용되지 않음&lt;/li&gt;
&lt;li&gt;WorkQueue + All 조합은 여러 인스턴스가 한 번만 메시지를 처리해야 할 때(Competing Consumers) 가장 적합함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Consumer가 메세지를 받고 30초 안에 ACK(완료 응답)을 보내지 않으면 처리 실패로 간주하고 같은 메세지를 다시 보낸다.&lt;/li&gt;
&lt;li&gt;ACK 실패 시 최대 5번까지 재전송한다.&lt;/li&gt;
&lt;li&gt;메세지를 받으면, 반드시 컨슈머가 ack()을 명시적으로 호출해야 JetStream이 처리 완료로 인정한다.(AckPolicy.Explicit)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;이렇게 Nats 서버 설정을 했습니다. 이를 간단하게 요약하자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection: 안정적인 연결/재연결 관리&lt;/li&gt;
&lt;li&gt;Stream(job): message.process 메시지를 파일에 저장 (1분 유지, WorkQueue 모드)&lt;/li&gt;
&lt;li&gt;Consumer(job_cons): 처음부터 모든 메시지를 받고, 하나씩 처리하며, ack 기반 재전송 (최대 5번)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이를 통해 메시지 손실을 최대한 방지하고, 중복 처리를 최소화하며, 처리 실패 시 자동 재시도 및 오래된 메시지는 자동 폐기할 수 있게 되었습니다. 결국,&amp;nbsp;저희는&amp;nbsp;보고서&amp;nbsp;생성이라는&amp;nbsp;리소스&amp;nbsp;집약적이고&amp;nbsp;장시간&amp;nbsp;소요되는&amp;nbsp;작업을&amp;nbsp;안정적으로&amp;nbsp;제어할&amp;nbsp;수&amp;nbsp;있었으며,&amp;nbsp;동시에&amp;nbsp;작업&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;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;보고서 생성을 위한 아키텍처 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이제는 최종적으로 저희가 선택한 보고서 생성을 위한 아키텍처를 설명드리도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BgSgK/btsQR4EtUhE/yWwOOyPkrzT77cjntyzgDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BgSgK/btsQR4EtUhE/yWwOOyPkrzT77cjntyzgDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BgSgK/btsQR4EtUhE/yWwOOyPkrzT77cjntyzgDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBgSgK%2FbtsQR4EtUhE%2FyWwOOyPkrzT77cjntyzgDk%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;946&quot; height=&quot;499&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;보고서 생성을 위한 아키텍처는 위와 같습니다. 주요한 점은,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nats&amp;nbsp;서버는&amp;nbsp;퍼블릭&amp;nbsp;네트워크에&amp;nbsp;노출되지&amp;nbsp;않으며,&amp;nbsp;동일한&amp;nbsp;VPC/내부&amp;nbsp;네트워크&amp;nbsp;상에서&amp;nbsp;백엔드&amp;nbsp;서비스만&amp;nbsp;접근&amp;nbsp;가능하다.&lt;/li&gt;
&lt;li&gt;보고서 생성을 위한 별도의 FE 서버가 AWS Lambda을 통해 배포되어있다.&lt;/li&gt;
&lt;li&gt;보고서 생성 시 Nats 메세지 큐에 작업이 적재되고, 서버는 Nats을 주기적으로 확인해 순차적으로 작업을 처리한다.&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;&amp;nbsp;Nats 서버를 외부로 노출시키지 않는 점은 Nats 메세지 큐에 작업을 퍼블리시 하는 주체는 BE 서버이기 때문입니다. FE에서도 직접 Nats 서버에 메세지를 퍼블리시할 수도 있겠지만 이렇게 한 이유는, 백엔드 서버를 통해 1차적으로 메세지 검증이 이루어질 수 있고 FE에서 굳이 Nats 서버의 정보를 알 필요가 없을 것이라고 생각했기 때문입니다. 그리고 Nats를 외부에 노출시키지 않음으로서, 보안적으로도 더 유리하다고 생각했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또한, 위에서 말한 바와 같이 레포트 작성은 많은 리소스가 필요합니다. FE에 요청도 나름 FE서버 내부 Queue통해 한번에 보내는 요청량을 조절하는데, 간단하게만 봐도 Queue에 API 요청이 약 200개가 넘었습니다. (물론 분석하는 데이터 종류가 많으면 300개 넘게도 자주 발생함) 그러다보니 레포트 요청 처리시 FE 서버의 CPU &amp;amp; 메모리 사용량이 급등하는 현상을 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNmiwE/btsQSGwBDnS/Tn1r0Aikhu657wKZp9spB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNmiwE/btsQSGwBDnS/Tn1r0Aikhu657wKZp9spB0/img.png&quot; data-alt=&quot;FE 서버 내부 Queue에 대기 중인 API 요청 리스트들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNmiwE/btsQSGwBDnS/Tn1r0Aikhu657wKZp9spB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNmiwE%2FbtsQSGwBDnS%2FTn1r0Aikhu657wKZp9spB0%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;1760&quot; height=&quot;276&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FE 서버 내부 Queue에 대기 중인 API 요청 리스트들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 같은 FE 서버를 통해 레포트 생성이 이루어진다면, 레포트 생성 요청 처리 중에 다른 요청 처리가 원활하게 처리되지 않을 수 있는 위험이 발생할 수 있었습니다. 따라서, FE 서버를 분리하기로 결정했습니다. 하지만, 별도의 서버를 운영하기에는 보고서 생성 요청이 그렇게 많지 않기 때문에 계속해서 서버를 운영하는 것은 불필요하다고 생각했습니다. 따라서 저희가 선택한 방법은 FE서버를 Lambda로 띄우는 것이였습니다.&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;FE 서버를 Lambda로 띄울 때의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비용 효율성(Cost Efficiency)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lambda는 요청이 있을 때만 실행되고 사용한 만큼만 과금된다.&lt;/li&gt;
&lt;li&gt;저희 같이 레포트 요청 빈도가 높지 않은 상황에서는, 항상 서버를 띄워놓는 방식보다 훨씬 비용을 절감할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;격리된 실행 환경(Isolated Execution)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레포트 작업이 FE 서버와 분리되어 실행되므로, 레포트 처리 중에도 기존 FE 서버가 다른 API 요청을 원활히 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;즉, 레포트 생성이 서비스의 다른 기능에 영향을 주지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;운영 부담 감소(Operation Simplicity)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 서버를 유지&amp;middot;관리할 필요가 없고, 인프라 관리 부담이 줄어듭니다.&lt;/li&gt;
&lt;li&gt;장애 대응이나 서버 패치 등 운영 관리 작업을 최소화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 장점으로 AWS Lambda를 통해 별도의 레포트 생성 처리를 위한 FE 서버를 띄웠습니다. 물론 이 과정에서 여러 이슈들이 존재했지만, 이는 다음에 기회가 되면 자세히 설명드리도록 하겠습니다. 간단하게 설명드리자면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS Lambda에서 쓰는 AWS Key에 관한 권한 처리를 AWS Lambda에서는 별도로 처리해야 한다.&lt;/li&gt;
&lt;li&gt;AWS Lambda에 단순히 FE 서버를 연결하면 빌드 결과물을 S3에 올리고 CloudFront을 얹어서 배포함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CloudFront의 API 응답 대기 시간은 60초이고 최대 120초이다. -&amp;gt; 보고서 생성 작업으로 쓰긴 부족함...&lt;/li&gt;
&lt;li&gt;따라서 Lambda Function URL(LFU) 방식을 통해 Lambda 서버가 직접 배포함 -&amp;gt; 최대시간 15분으로 설정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;NEXT_PUBLIC 변수가 아닌 서버 전용 환경 변수는 별도로 AWS Lambda에서 사용하기 위해 넣어줘야됨&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이렇게 해서 Nats 설정 및 보고서 생성을 위한 전체적인 아키텍처를 설명드렸습니다. 그렇다면 마지막으로 이를 코드로 어떻게 구현했는지 간단하게 설명드리도록 하겠습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Nats 메세지 Worker 코드&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 설정을 통해 Nats 설정 및 백엔드 서버(Consumer) 설정은 완료했습니다. 그렇다면 간단하게 코드로 서버가 어떤식으로 메세지를 처리했는지 설명드리도록 하겠습니다. 저희는 Spring Kotlin을 사용하기 때문에 Spring Kotlin 환경을 기준으로 설명드리도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;※ Nats 환경 준비&lt;/h4&gt;
&lt;pre id=&quot;code_1759080953778&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// nats 의존성 build.gradle.kts에 추가
// implementation(&quot;io.nats:jnats:2.19.0&quot;)

@Configuration
class NatsConfig(
    private val properties: NatsConfigProperties,
) {
    private val logger = KotlinLogging.logger {}

    @Bean
    fun natsConnection(): Connection {
        //connection 생성 코드
    }

    @Bean
    fun jetStream(connection: Connection): JetStream {
        val jsm = connection.jetStreamManagement()
        createStream(jsm, properties.stream)
        createPullConsumer(jsm, properties.stream, properties.consumer)
        return connection.jetStream()
    }

    private fun createStream(
        jsm: JetStreamManagement,
        stream: String,
    ) {
        // stream 생성 코드
    }

    private fun createPullConsumer(
        jsm: JetStreamManagement,
        stream: String,
        consumer: String,
    ) {
        // consumer 생성 코드
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;서버 실행 시 Nats Jetstream 환경을 준비하는 Configuration 코드입니다. Stream &amp;amp; Consumer가 없으면 생성하고 이후 사용할 JetStream 객체를 초기화해 Spring 컨테이너에 등록하는 코드입니다. Stream &amp;amp; Consumer 생성은 위의 Nats 설정을 기반으로 설정을 했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;즉, JetStream 메세징 환경을 구성한 뒤, JetStream API 핸들러를 앱 전역에서 쓰도록 제공하는 역할을 합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;※ 메세지 Publish&lt;/h4&gt;
&lt;pre id=&quot;code_1759080551407&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class NatsService(
    private val jetStream: JetStream,
) {
    private val logger = KotlinLogging.logger {}

    fun publishMessage(
        subject: String? = &quot;message.process&quot;,
        message: ObjectNode,
    ): PublishAck = try {
        val msg = NatsMessage.builder()
            .subject(subject)
            .data(message.toJsonString())
            .build()

        jetStream.publish(msg)
    } catch (e: Exception) {
    	// 에러 처리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Nats의 메세지 퍼블리시는 위와 같이 간단히 처리했습니다. FE 서버에서 특정 요청이 오면 NatServic의 publishMessage 메서드를 실행해 특정 subject에 특정 메세지를 json 형태로 퍼블리시를 하도록 하는 코드입니다. 여기서 사용하는 JetStream의 경우 위에서 서버 실행 시 Spring 컨테이너에 등록한 JetStream 객체를 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;※ 메세지 Subscribe &amp;amp; 처리&lt;/h4&gt;
&lt;pre id=&quot;code_1759081288362&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class NatsMessageHandler(
    private val jetStream: JetStream,
    private val properties: NatsConfigProperties,
) {
    private val logger = KotlinLogging.logger {}

    @PostConstruct
    fun startConsumer() {
        logger.info { &quot;Starting NATS pull consumer...&quot; }

        val processingExecutor = Executors.newFixedThreadPool(1) { runnable -&amp;gt;
            Thread(runnable, &quot;nats-pull-consumer-${Thread.currentThread().name}&quot;).apply {
                isDaemon = true
            }
        }

        processingExecutor.submit {
            startPullingMessages()
        }
    }

    private fun startPullingMessages() {
        val pullOpts = PullSubscribeOptions.bind(properties.stream, properties.consumer)
        val subscription = jetStream.subscribe(null, pullOpts)

        logger.info { &quot;NATS pull consumer started successfully&quot; }

        while (!Thread.currentThread().isInterrupted) {
            try {
                val messages = subscription.fetch(1, Duration.ofSeconds(5))

                messages.forEach { message -&amp;gt;
                    processMessage(message)
                }
            } catch (e: Exception) {
                // 에러 처리
            }
        }
    }

    private fun processMessage(message: Message) {
        try {
        	// 로직 처리
            message.ack() // 성공 시 명시적으로 message ack 처리
        } catch (e: Exception) {
            val retryCount = message.metaData().deliveredCount() // 로직 실패 시 retryCount 확인

            if (retryCount &amp;lt; 5) {
                message.nak() // 5번 미만 시 재시도 처리를 위한 nak 발생
                return
            }

            // 5번 이상 재시도 처리 후에도 실패한 메세지인 경우 ack 발생 후 별도의 알림 발생
            message.ack()
        }
    }

    private fun processBusinessLogic(..) {
        // 레포트 생성 처리 로직 구현
        // AWS Lambda 호출 후 결과 리턴
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위의 코드는 Nats JetStream Pull Consumer 설정 코드로 주요 설정은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@PostConstruct로 서버 실행 직후 실행했습니다.&lt;/li&gt;
&lt;li&gt;NATS 메시지 구독 루프(fetch &amp;rarr; 처리)는 무한 루프라서 메인 스레드를 블로킹하면 안 되기 때문에 별도의 데몬 스레드에서 메세지를 처리하도록 구현했습니다.&lt;/li&gt;
&lt;li&gt;PullSubscribeOptions.bind(...) 를 통해 특정 stream &amp;amp; counsumer로 바인딩해 메세지를 받을 수 있게끔 설정했습니다.&lt;/li&gt;
&lt;li&gt;subscription.fetch(1, Duration.ofSeconds(5))을 통해 서버가 한번에 최대 1개의 메세지를 가져올 수 있게하고 메세지가 없으면 최대 5초동안 대기 후 다시 while 반복문을 통해 다시 메세지를 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 설정으로 서버는 Nats의 메세지를 주기적으로 확인합니다. 그렇다면, 이제는 만약 메세지가 있을 경우 서버가 받아와 어떻게 처리하는지 설명드리도록 하겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공 케이스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 로직 수행 성공 -&amp;gt; 명시적으로 ack() 호출&lt;/li&gt;
&lt;li&gt;JetStream은 ack 메세지를 통해 작업이 완료되었음을 인지하고 해당 메세지를 삭제함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패 케이스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;message.metaData().deliveredCount()&amp;nbsp;&amp;rarr;&amp;nbsp;지금까지&amp;nbsp;몇&amp;nbsp;번째&amp;nbsp;시도인지&amp;nbsp;확인&lt;/li&gt;
&lt;li&gt;서버에서 설정한 최대 재시도 횟수(5) 미만이면 nak() 호출 -&amp;gt; JetStream이 같은 메세지를 다시 큐에 넣고 재전송&lt;/li&gt;
&lt;li&gt;최대 재시도 횟수(5) 이상이면 ack() 호출 후 알림 발생 -&amp;gt; ack() 호출로 인해 JetStream은 더이상 해당 메세지를 추가로 처리하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;3840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXQ8Cl/btsQSIOLlnh/txzJC4Kpu55ZxkLGPmbhc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXQ8Cl/btsQSIOLlnh/txzJC4Kpu55ZxkLGPmbhc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXQ8Cl/btsQSIOLlnh/txzJC4Kpu55ZxkLGPmbhc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXQ8Cl%2FbtsQSIOLlnh%2FtxzJC4Kpu55ZxkLGPmbhc0%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;2928&quot; height=&quot;3840&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;3840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게해서 Nats 메세지 큐를 통한 안정적인 대용량 데이터 보고서 생성 파이프라인 구축을 완료했습니다. 어떻게하면 더 안정적으로 작업을 관리할지 고민하면서 메세지 큐 도입 뿐만 아니라 다양한 인프라 적 요소 &amp;amp; 고려사항이 함께 고려됐던 것 같습니다. 하면서 여러 이슈 사항이 있었지만 다행히... 해결하면서 최종적인 파이프라인을 구축하고 테스트해 어느정도의 안정성을 입증하게 되면서 나름의 뿌듯함도 있었던 작업이였던 것 같습니다.&lt;/p&gt;</description>
      <author>YGwan</author>
      <guid isPermaLink="true">https://swmobenz.tistory.com/54</guid>
      <comments>https://swmobenz.tistory.com/54#entry54comment</comments>
      <pubDate>Thu, 3 Jul 2025 16:55:21 +0900</pubDate>
    </item>
  </channel>
</rss>