<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>뭉게구름 엔지니어 되는 과정</title>
    <link>https://ksh-cloud.tistory.com/</link>
    <description>3년차 클라우드/DevOps 엔지니어 입니다.      

궁금하신 부분이나 따로 연락주실 일이 있으시다면 아래 이메일로 연락주세요

kksh.cloud@gmail.com </description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 20:22:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>황동리</managingEditor>
    <image>
      <title>뭉게구름 엔지니어 되는 과정</title>
      <url>https://tistory1.daumcdn.net/tistory/6767404/attach/fe964c38f7bd46bcaf549d79fc438321</url>
      <link>https://ksh-cloud.tistory.com</link>
    </image>
    <item>
      <title>Kubernetes MongoDB Community 버전 업그레이드 가이드 (6.0.5 &amp;rarr; 8.2.6)</title>
      <link>https://ksh-cloud.tistory.com/207</link>
      <description>&lt;h1&gt;개요&lt;/h1&gt;
&lt;p&gt;MongoDB Community Operator를 사용하여 Kubernetes에서 운영 중인 MongoDB를 6.0.5에서 8.2.6으로 업그레이드하는 과정을 정리합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Operator&lt;/strong&gt;: mongodb-kubernetes (Community) v1.7.0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;업그레이드 경로&lt;/strong&gt;: 6.0.5 → 7.0.31 → 8.0.20 → 8.2.6&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;MongoDB 설치 방법에 대한 글은 아래 링크를 타고 가시면 볼 수 있습니다.&lt;br&gt;&lt;a href=&quot;https://ksh-cloud.tistory.com/206&quot;&gt;Kubernetes에 MongoDB 설치하기&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h1&gt;사전 지식&lt;/h1&gt;
&lt;h2&gt;MongoDB는 Major 버전을 건너뛸 수 없다&lt;/h2&gt;
&lt;p&gt;MongoDB는 Major 버전을 순차적으로 업그레이드해야 합니다. 예를 들어 6.0에서 8.0으로 바로 올릴 수 없습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6.0 → 7.0 → 8.0 → 8.2&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;featureCompatibilityVersion (FCV)&lt;/h2&gt;
&lt;p&gt;FCV는 MongoDB 내부 기능 호환성을 제어하는 설정입니다. 새로운 Major 버전으로 업그레이드하기 전에 현재 버전의 FCV가 올바르게 설정되어 있어야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;7.0.x 실행 중 → FCV &lt;code&gt;7.0&lt;/code&gt; 필요&lt;/li&gt;
&lt;li&gt;8.0.x 실행 중 → FCV &lt;code&gt;8.0&lt;/code&gt; 필요&lt;/li&gt;
&lt;li&gt;8.2.x는 8.x 계열이므로 FCV &lt;code&gt;8.0&lt;/code&gt; 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MongoDB Community Operator와 FCV&lt;/h2&gt;
&lt;p&gt;Operator는 &lt;code&gt;MongoDBCommunity&lt;/code&gt; 리소스의 &lt;code&gt;featureCompatibilityVersion&lt;/code&gt; 필드를 통해 FCV를 관리합니다. &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;code&gt;version&lt;/code&gt;과 &lt;code&gt;featureCompatibilityVersion&lt;/code&gt;을 yaml에서 함께 변경 후 apply하면 Operator가 순서에 맞게 처리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;업그레이드 절차&lt;/h1&gt;
&lt;h2&gt;Step 1. 6.0.5 → 7.0.31&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mongodb.yaml&lt;/code&gt;의 &lt;code&gt;version&lt;/code&gt;과 &lt;code&gt;featureCompatibilityVersion&lt;/code&gt;을 함께 변경 후 apply합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: mongodb
spec:
  members: 1
  type: ReplicaSet
  version: &amp;quot;7.0.31&amp;quot; # MongoDB 버전 업그레이드 6.0.5 -&amp;gt; 7.0.31
  featureCompatibilityVersion: &amp;quot;7.0&amp;quot; # FCV 버전 정의하는 내용 추가
  ...&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이제 &lt;code&gt;mongodb.yaml&lt;/code&gt; 파일을 적용해줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f mongodb.yaml -n mongodb&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 적용을 해주면 &lt;code&gt;mongodb-operator&lt;/code&gt; 가 알아서&lt;code&gt;version&lt;/code&gt;,&lt;code&gt;featureCompatibilityVersion&lt;/code&gt;의 변경된 값을 감지하여 업그레이드 시켜줍니다.&lt;/p&gt;
&lt;h3&gt;업그레이드 확인&lt;/h3&gt;
&lt;p&gt;우선 MongoDB 버전이 업그레이드 되었나 확인해보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get mongodbcommunity.mongodbcommunity.mongodb.com -n mongodb
---
NAME                   PHASE     VERSION
mongodb-0   Running   7.0.31&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이제 FCV 버전도 업그레이드 되었는지 확인해보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 먼저 설치된 mongodb 파드 접속
kubectl exec -it mongodb-0  -c mongod -- bash -n mongodb

# mongosh 를 사용하여 FCV 버전 확인
mongosh -u admin -p powertask --authenticationDatabase admin --eval &amp;quot;db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })&amp;quot;
---
{
  featureCompatibilityVersion: { version: &amp;#39;7.0&amp;#39; }, # 여기 보면 7.0 으로 업그레이드 된 것을 확인 할 수 있습니다.
  ok: 1,
  ...
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 2. 7.0.31 → 8.0.20&lt;/h2&gt;
&lt;p&gt;여기서 부터는 앞서 &lt;code&gt;6.0.5 -&amp;gt; 7.0.31&lt;/code&gt; 버전으로 업그레이드 했던 방식과 동일하게 진행을 하시면 됩니다.&lt;/p&gt;
&lt;h2&gt;Step 3. 8.0.20 → 8.2.6&lt;/h2&gt;
&lt;p&gt;8.2.x는 8.x 계열이므로 FCV는 &lt;code&gt;8.0&lt;/code&gt;을 유지합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;따라서 이번엔 &lt;code&gt;version&lt;/code&gt; 만 변경을 해주면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spec:
  version: &amp;quot;8.2.6&amp;quot; # 8.0.20 -&amp;gt; 8.2.6 변경
  featureCompatibilityVersion: &amp;quot;8.0&amp;quot; # Step 2에서 8.0으로 변경한 그대로 두기&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;버전 업그레이드 확인&lt;/h3&gt;
&lt;p&gt;앞선 방식과 동일하게 업그레이드 된 버전을 확인해보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get mongodbcommunity.mongodbcommunity.mongodb.com -n mongodb
---
NAME                   PHASE     VERSION
mongodb-0   Running   8.2.6

# 먼저 설치된 mongodb 파드 접속
kubectl exec -it mongodb-0  -c mongod -- bash -n mongodb

# mongosh 를 사용하여 FCV 버전 확인
mongosh -u admin -p powertask --authenticationDatabase admin --eval &amp;quot;db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })&amp;quot;
---
{
  featureCompatibilityVersion: { version: &amp;#39;8.0&amp;#39; }, # 여기 보면 7.0 으로 업그레이드 된 것을 확인 할 수 있습니다.
  ok: 1,
  ...
}&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;이번 업그레이드를 진행하면서 처음에는 &lt;code&gt;version&lt;/code&gt; 필드만 변경했더니 Pod가 계속 오류로 재시작되는 문제를 겪었습니다. 원인을 찾아보니 &lt;code&gt;featureCompatibilityVersion&lt;/code&gt;도 함께 올려줘야 한다는 것을 알게 되었고, 두 필드를 같이 변경하니 정상적으로 동작했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;업그레이드를 진행하게 된 계기는 &lt;strong&gt;Vector Search&lt;/strong&gt; 기능 도입 때문이었습니다. MongoDB의 Vector Search는 버전 &lt;strong&gt;8.2 이상&lt;/strong&gt;에서 지원되기 때문에, 6.0.5에서 8.2.6까지 순차 업그레이드를 진행했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;같은 상황을 겪는 분들에게 도움이 되길 바라며 과정을 정리해 공유합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>DB</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/207</guid>
      <comments>https://ksh-cloud.tistory.com/207#entry207comment</comments>
      <pubDate>Fri, 27 Mar 2026 14:40:36 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes에 MongoDB 설치하기</title>
      <link>https://ksh-cloud.tistory.com/206</link>
      <description>&lt;h1&gt;들어가며&lt;/h1&gt;
&lt;p&gt;Kubernetes 환경에서 MongoDB를 운영하려면 직접 Pod를 관리하는 것보다 &lt;strong&gt;MongoDB Community Operator&lt;/strong&gt;를 활용하는 것이 훨씬 효율적입니다. &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;Operator를 사용하면 MongoDB의 설치, 업그레이드, 사용자 관리 등을 Kubernetes 리소스(CRD)로 선언적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;설치 환경&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes: EKS&lt;/li&gt;
&lt;li&gt;MongoDB Community Operator: v1.7.0&lt;/li&gt;
&lt;li&gt;MongoDB: 6.0.5&lt;/li&gt;
&lt;li&gt;Storage: AWS EBS gp3&lt;/li&gt;
&lt;li&gt;Node: 전용 노드 그룹 (Taint/Toleration 적용)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;저는 위 환경에서 진행을 하였습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;1. MongoDB Community Operator 설치&lt;/h1&gt;
&lt;p&gt;아래 주소는 MongoDB 공식문서 주소 입니다.&lt;br&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/kubernetes/current/tutorial/install-k8s-operator/&quot;&gt;참고링크&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Helm Repository 추가&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;helm repo add mongodb https://mongodb.github.io/helm-charts
helm repo update&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Operator 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;helm install mongodb-operator mongodb/mongodb-kubernetes \
  --namespace mongodb \
  --create-namespace \
  --version 1.7.0&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;설치 확인&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;kubectl get deployment mongodb-kubernetes-operator -n mongodb
---
NAME                                           READY   STATUS    RESTARTS   AGE
mongodb-kubernetes-operator-6574674d6b-44dw9   1/1     Running   0          20h&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;2. MongoDB 설치를 위한 비밀번호 Secret 생성&lt;/h1&gt;
&lt;p&gt;MongoDB 사용자 비밀번호를 Secret으로 먼저 생성합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create secret generic mongodb-admin-admin \
  --from-literal=password=&amp;#39;your-strong-password&amp;#39; \
  --namespace mongodb&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;3. MongoDBCommunity 리소스 작성 및 배포&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;mongodb.yaml&lt;/code&gt; 파일을 작성합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;해당 파일 내용은 &lt;a href=&quot;https://github.com/mongodb/mongodb-kubernetes/tree/1.7.0/public/samples/community&quot;&gt;mongodb-kubernetes Github&lt;/a&gt; 에서 참고하였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: mongodb
spec:
  members: 1
  type: ReplicaSet
  version: &amp;quot;6.0.5&amp;quot;
  security:
    authentication:
      modes: [&amp;quot;SCRAM&amp;quot;]
  users:
    - name: admin
      db: admin
      passwordSecretRef:
        name: mongodb-admin-admin
      roles:
        - name: clusterAdmin
          db: admin
        - name: userAdminAnyDatabase
          db: admin
        - name: root
          db: admin
      scramCredentialsSecretName: my-scram
  additionalMongodConfig:
    storage.wiredTiger.engineConfig.journalCompressor: zlib
  statefulSet:
    spec:
      volumeClaimTemplates:
        - metadata:
            name: data-volume
          spec:
            accessModes: [&amp;quot;ReadWriteOnce&amp;quot;]
            storageClassName: ebs-gp3-sc
            resources:
              requests:
                storage: 30Gi
        - metadata:
            name: logs-volume
          spec:
            accessModes: [&amp;quot;ReadWriteOnce&amp;quot;]
            storageClassName: ebs-gp3-sc
            resources:
              requests:
                storage: 2Gi
      template:
        spec:
          containers:
            - name: mongod
              resources:
                limits:
                  cpu: 800m
                  memory: 1.5Gi
                requests:
                  cpu: 100m
                  memory: 250Mi
            - name: mongodb-agent
              resources:
                limits:
                  cpu: 200m
                  memory: 512Mi
                requests:
                  cpu: 100m
                  memory: 256Mi
          nodeSelector:
            node-group-type: mongodb-nodes
          tolerations:
            - key: mongodb
              operator: Equal
              value: only
              effect: NoSchedule&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;주요 설정 설명&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;members: 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ReplicaSet 멤버 수. 운영 환경은 3 권장 (저는 테스트여서 1개만 생성을 하였습니다.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type: ReplicaSet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Community Operator는 ReplicaSet만 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실행할 MongoDB 버전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modes: [&amp;quot;SCRAM&amp;quot;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인증 방식. SCRAM-SHA-256 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scramCredentialsSecretName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SCRAM 인증 정보를 저장할 Secret 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storageClassName: ebs-gp3-sc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EBS gp3 StorageClass 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nodeSelector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MongoDB 전용 노드 그룹에 배치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tolerations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Taint가 걸린 전용 노드에 스케줄링 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br&gt;

&lt;p&gt;이제 이대로 배포를 해보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f mongodb.yaml -n mongodb&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;배포를 하고 상태 확인해보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get mongodbcommunity -n mongodb
---
NAME      PHASE     VERSION
mongodb   Running   6.0.5

kubectl get pods -n mongodb
---
NAME          READY   STATUS    RESTARTS   AGE
mongodb-0     2/2     Running   0          2m&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Pod가 &lt;code&gt;2/2&lt;/code&gt;인 이유는 &lt;code&gt;mongod&lt;/code&gt; 컨테이너와 &lt;code&gt;mongodb-agent&lt;/code&gt; 컨테이너가 함께 실행되기 때문입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h1&gt;4. 연결 확인&lt;/h1&gt;
&lt;h2&gt;kubectl로 직접 접속&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;kubectl exec -it mongodb-0 -n mongodb -c mongod -- \
  mongosh -u admin \
  -p $(kubectl get secret mongodb-admin-admin -n mongodb -o jsonpath=&amp;#39;{.data.password}&amp;#39; | base64 -d) \
  --authenticationDatabase admin&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Connection String&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mongodb://admin:&amp;lt;password&amp;gt;@mongodb-0.mongodb-svc.mongodb.svc.cluster.local:27017/?replicaSet=mongodb&amp;amp;authSource=admin&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;5. 생성되는 리소스 정리&lt;/h1&gt;
&lt;p&gt;Operator가 자동으로 생성하는 리소스들입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;리소스&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;StatefulSet&lt;/td&gt;
&lt;td&gt;mongodb&lt;/td&gt;
&lt;td&gt;MongoDB Pod 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service (Headless)&lt;/td&gt;
&lt;td&gt;mongodb-svc&lt;/td&gt;
&lt;td&gt;Pod별 DNS 엔드포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;mongodb-config&lt;/td&gt;
&lt;td&gt;Automation Agent 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;my-scram-scram-credentials&lt;/td&gt;
&lt;td&gt;SCRAM 인증 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;mongodb-keyfile&lt;/td&gt;
&lt;td&gt;ReplicaSet 내부 인증 키&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p&gt;MongoDB Community Operator를 사용하면 Kubernetes에서 MongoDB를 선언적으로 관리할 수 있어 운영이 훨씬 간편해집니다.&lt;br&gt;버전 업그레이드도 &lt;code&gt;version&lt;/code&gt; 필드만 변경하면 Operator가 자동으로 rolling upgrade를 처리합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;단, Major 버전 업그레이드 시에는 &lt;strong&gt;featureCompatibilityVersion(FCV)&lt;/strong&gt; 관리가 필요합니다. 이 내용은 별도 포스팅에서 다루겠습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>DB</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/206</guid>
      <comments>https://ksh-cloud.tistory.com/206#entry206comment</comments>
      <pubDate>Fri, 27 Mar 2026 11:00:18 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Schema Registry 구축 - EKS에 Apicurio Registry 설치하기</title>
      <link>https://ksh-cloud.tistory.com/205</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://ksh-cloud.tistory.com/204&quot;&gt;Kafka 스키마 레지스트리란 무엇인가?&lt;/a&gt;&lt;br&gt;앞선 글에서 &lt;strong&gt;스키마 레지스트리&lt;/strong&gt;가 무엇인지, 왜 필요한지 살펴보았습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이번 글에서는 실제로 &lt;strong&gt;Apicurio Registry&lt;/strong&gt;를 EKS 환경에 설치하고 내부적으로 어떻게 동작이 되는지 알아보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;사전 준비&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;EKS 클러스터&lt;/li&gt;
&lt;li&gt;ArgoCD 설치 및 Git 레포지터리 연결&lt;/li&gt;
&lt;li&gt;Kafka 클러스터 (Strimzi 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;1. Apicurio Operator 설치&lt;/h2&gt;
&lt;p&gt;Apicurio Registry는 Kubernetes Operator 방식으로 동작합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;Operator를 먼저 설치해야 이후 Registry 인스턴스를 CRD로 생성할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;1-1. Operator YAML 준비&lt;/h3&gt;
&lt;p&gt;저는 공식 Github에서 제공하는 &lt;code&gt;apicurio-registry-operator-3.1.7.yaml&lt;/code&gt; 다운로드하여 Operator 설치를 진행하였습니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Apicurio Registry v3.0부터 Operator 코드가 기존의 분리된 &lt;code&gt;apicurio-registry-operator&lt;/code&gt; 레포에서&lt;br&gt;메인 &lt;code&gt;apicurio-registry&lt;/code&gt; 레포로 통합되었습니다. v3.x 설치 시 아래 경로를 사용해야 합니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/Apicurio/apicurio-registry/blob/main/operator/install/apicurio-registry-operator-3.1.7.yaml&quot;&gt;Apicurio operator YAML 파일이 있는 Github 주소&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;파일을 열어보면 다음 리소스들이 포함되어 있습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;리소스&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;CustomResourceDefinition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apicurioregistries3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registry 인스턴스를 CRD로 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServiceAccount&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apicurio-registry-operator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operator가 사용하는 서비스 계정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClusterRole&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apicurio-registry-operator-clusterrole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;필요한 권한 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClusterRoleBinding&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apicurio-registry-operator-clusterrolebinding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ClusterRole을 ServiceAccount에 바인딩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apicurio-registry-operator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operator 파드&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;1-2. 문제 발견 — PLACEHOLDER_NAMESPACE&lt;/h3&gt;
&lt;p&gt;파일을 보면 네임스페이스가 하드코딩되지 않고 &lt;code&gt;PLACEHOLDER_NAMESPACE&lt;/code&gt;로 되어 있는 부분이 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ClusterRoleBinding
subjects:
- kind: ServiceAccount
  name: apicurio-registry-operator
  namespace: PLACEHOLDER_NAMESPACE   # ← 교체 필요

# Deployment
metadata:
  namespace: PLACEHOLDER_NAMESPACE   # ← 교체 필요&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이 값들은 &lt;strong&gt;실제 사용하려는 네임스페이스로 교체&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;저 같은 경우 ArgoCD + Kustomize 환경에서 배포를 하기 때문에 YAML에서 직접 수정하지 않고 &lt;code&gt;kustomization.yaml&lt;/code&gt;로 처리합니다.&lt;/p&gt;
&lt;h3&gt;1-3. kustomization.yaml 작성&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - apicurio-operator.yaml        # operator.yaml
  - kafka-schema-registry.yaml    # apicurio-registry.yaml
namespace: apicurio

patches:
  - patch: |-
      - op: replace
        path: /subjects/0/namespace
        value: apicurio
    target:
      kind: ClusterRoleBinding
      name: apicurio-registry-operator-clusterrolebinding
&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;kustomization.yaml 구성 설명&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;code&gt;namespace: apicurio&lt;/code&gt;는 Kustomize의 Namespace Transformer를 트리거하여 namespace 리소스의 &lt;code&gt;metadata.namespace&lt;/code&gt;를 일괄 교체합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;단, &lt;code&gt;ClusterRoleBinding&lt;/code&gt;은 cluster-scoped 리소스라 &lt;code&gt;metadata.namespace&lt;/code&gt;가 존재하지 않고,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;내부의 &lt;code&gt;subjects[].namespace&lt;/code&gt;는 Namespace Transformer가 자동으로 처리하지 않습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이 때문에 &lt;code&gt;patches&lt;/code&gt;를 사용해 JSON Patch 방식으로 해당 필드를 명시적으로 교체합니다.&lt;/p&gt;
&lt;br&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;리소스&lt;/th&gt;
&lt;th&gt;처리 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;namespace: apicurio&lt;/code&gt;로 자동 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ServiceAccount&lt;/td&gt;
&lt;td&gt;&lt;code&gt;namespace: apicurio&lt;/code&gt;로 자동 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClusterRoleBinding subjects&lt;/td&gt;
&lt;td&gt;명시적 patch 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br&gt;

&lt;p&gt;이제 Github Repository에 &lt;code&gt;operator.yaml&lt;/code&gt;, &lt;code&gt;kustomization.yaml&lt;/code&gt; 을 Push 하고&lt;br&gt;해당 레포지터리를 ArgoCD와 연결을 시켜주면 &lt;strong&gt;operator 배포가 완료 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2. Apicurio Registry 배포&lt;/h2&gt;
&lt;p&gt;Operator가 정상적으로 작동하면, &lt;code&gt;ApicurioRegistry3&lt;/code&gt; CRD로 실제 Registry 인스턴스를 생성합니다.&lt;/p&gt;
&lt;h3&gt;2-1. Apicurio Registry YAML 작성 (스토리지 설정)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: registry.apicur.io/v1
kind: ApicurioRegistry3
metadata:
  name: apicurio-registry-kafkasql
  namespace: apicurio
spec:
  app:
    storage:
      type: kafkasql
      kafkasql:
        bootstrapServers: &amp;lt;kafka boostrap/broker 주소&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;각 필드를 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;spec.app.storage.type: kafkasql&lt;/code&gt;&lt;/strong&gt;: Registry의 백엔드 스토리지를 Kafka로 사용하겠다는 설정입니다. 스키마 데이터가 Kafka 토픽에 저장됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;spec.app.storage.kafkasql.bootstrapServers&lt;/code&gt;&lt;/strong&gt;: Registry가 연결할 Kafka 브로커 주소입니다. 클러스터 내부 DNS 형식(&lt;code&gt;&amp;lt;service&amp;gt;.&amp;lt;namespace&amp;gt;.svc.cluster.local&lt;/code&gt;)으로 작성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2-2. Apicurio Registry YAML 작성 (Ingress 설정)&lt;/h3&gt;
&lt;p&gt;Apicurio Registry는 &lt;strong&gt;API 서버&lt;/strong&gt;(&lt;code&gt;app&lt;/code&gt;)와 &lt;strong&gt;UI&lt;/strong&gt;(&lt;code&gt;ui&lt;/code&gt;) 두 컴포넌트로 구성되며, 각각 별도의 Ingress를 설정할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;아래 예시는 AWS EKS 환경에서 &lt;strong&gt;AWS Load Balancer Controller(ALB)&lt;/strong&gt;를 사용하는 경우입니다.&lt;br&gt;nginx, traefik 등 다른 Ingress Controller를 사용하는 경우 &lt;code&gt;ingressClassName&lt;/code&gt;과 &lt;code&gt;annotations&lt;/code&gt;를 해당 환경에 맞게 변경하면 됩니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;spec:
  app:
    ingress:
      host: &amp;lt;api-도메인&amp;gt;
      ingressClassName: alb
      annotations:
        alb.ingress.kubernetes.io/scheme: internet-facing
        alb.ingress.kubernetes.io/target-type: ip
        alb.ingress.kubernetes.io/listen-ports: &amp;#39;[{&amp;quot;HTTP&amp;quot;: 80}, {&amp;quot;HTTPS&amp;quot;: 443}]&amp;#39;
        alb.ingress.kubernetes.io/certificate-arn: &amp;lt;ACM 인증서 ARN&amp;gt;
        alb.ingress.kubernetes.io/ssl-redirect: &amp;#39;443&amp;#39;
        alb.ingress.kubernetes.io/group.name: &amp;lt;ALB 그룹명&amp;gt;
  ui:
    env:
      - name: REGISTRY_API_URL
        value: &amp;quot;https://&amp;lt;api-도메인&amp;gt;/apis/registry/v3&amp;quot;
    ingress:
      host: &amp;lt;ui-도메인&amp;gt;
      ingressClassName: alb
      annotations:
        # app과 동일한 ALB annotations 사용&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;spec.app.ingress&lt;/code&gt;&lt;/strong&gt;: Registry REST API 엔드포인트에 대한 Ingress 설정입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;spec.ui.ingress&lt;/code&gt;&lt;/strong&gt;: Registry 웹 UI에 대한 Ingress 설정입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;spec.ui.env[REGISTRY_API_URL]&lt;/code&gt;&lt;/strong&gt;: UI가 API를 호출할 주소를 지정합니다. &lt;code&gt;app.ingress.host&lt;/code&gt;와 동일한 도메인으로 맞춰줘야 UI에서 스키마 목록이 정상적으로 로드됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2-3. kustomization.yaml에 추가&lt;/h3&gt;
&lt;p&gt;앞서 &lt;code&gt;1-3&lt;/code&gt; 과정에서 작성한 &lt;code&gt;kustomization.yaml&lt;/code&gt;의 resources에 추가합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resources:
  - apicurio-operator.yaml
  - kafka-schema-registry.yaml # 앞서 작성한 Apicurio Registry.yaml 파일&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;3. 동작 확인&lt;/h2&gt;
&lt;p&gt;정상적으로 설치가 완료가 되었다면, &lt;strong&gt;Ingress로 노출 시킨 UI 주소로 접속&lt;/strong&gt; 하면 아래와 같이 웹 UI가 나오게 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/a593657c-f798-4225-acd6-b84e58c4ed5d/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;hr&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p&gt;이렇게 Kafka 스키마 레지스트리로 사용할 Apicurio Registry 설치를 해보았습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;다음 글에서는 &lt;strong&gt;스키마 레지스트리를 사용하여 어떻게 동작시키는 지에 대해 알아보겠습니다.&lt;/strong&gt;&lt;/p&gt;</description>
      <category>Kafka</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/205</guid>
      <comments>https://ksh-cloud.tistory.com/205#entry205comment</comments>
      <pubDate>Fri, 27 Feb 2026 16:52:51 +0900</pubDate>
    </item>
    <item>
      <title>Kafka 스키마 레지스트리란 무엇인가?</title>
      <link>https://ksh-cloud.tistory.com/204</link>
      <description>&lt;h1&gt;스키마(Schema) 란?&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;데이터의 구조(형식)를 정의한 설계도&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;즉,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 필드가 있는지&lt;/li&gt;
&lt;li&gt;각 필드의 타입이 무엇인지&lt;/li&gt;
&lt;li&gt;필수인지 선택인지&lt;/li&gt;
&lt;li&gt;기본값은 무엇인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;를 정의한 문서&lt;/p&gt;
&lt;h1&gt;Kafka 스키마 레지스트리(Schema Registry) 란?&lt;/h1&gt;
&lt;p&gt;Kafka는 원래 메시지 바이트 배열 만 저장/전달 합니다.&lt;br&gt;즉, Kafka 자체는 “이 데이터가 어떤 구조인지(필드/타입)”을 모릅니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그래서 스키마 레지스트리는 Kafka 메시지의 데이터 구조(스키마)를 중앙에서:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;저장(버전 관리)&lt;/li&gt;
&lt;li&gt;호환성 검사(Compatibility)&lt;/li&gt;
&lt;li&gt;조회(역 직렬화에 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해주는 &lt;strong&gt;메타 데이터 서버&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;핵심: 데이터는 Kafka에 저장되고, 스키마는 Registry에 저장&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;사용 이유&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;스키마 변경으로 인한 장애를 막기 위해&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;예를 들어) Producer가 갑자기 필드 타입을 바꾸거나 삭제를 하는 경우&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;레지스트리를 사용하면 &lt;strong&gt;“허용되는 변경만”&lt;/strong&gt; 통과 시키고,&lt;br&gt;&lt;strong&gt;위험한 변경은 등록 단계에서 차단&lt;/strong&gt;할 수 있음&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;멀티팀/멀티서비스에서 스키마 구조를 강제하기 위해&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Topic을 여러 팀이 공유하면, 메시지 포맷이 문서만으로는 절대 안정적으로 유지되지 않기에&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;스키마 레지스트리를 두어 실제 &lt;strong&gt;실행 경로에서 스키마 구조를 강제&lt;/strong&gt;하기 합니다.&lt;/p&gt;
&lt;h2&gt;동작 흐름&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/6ab287d2-5663-4e1a-bd54-39e06033c367/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Producer 쪽&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;스키마를 레지스트리에 등록, 또는 이미 등록된 스키마 ID 조회 진행&lt;/li&gt;
&lt;li&gt;메시지를 직렬화 할 때, &lt;strong&gt;schema id를 헤더처럼 같이 붙여서&lt;/strong&gt; Kafka로 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Consumer 쪽&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Kafka에서 메시지를 받으면 schema id를 확인&lt;/li&gt;
&lt;li&gt;레지스트리에서 그 schema id에 해당하는 스키마를 받아서 역직렬화&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 호환성을 지키기 위해 아래와 같은 전략을 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BACKWARD: 새 Consumer가 옛날 데이터를 읽을 수 있게&lt;/li&gt;
&lt;li&gt;FORWARD: 옛 Consumer가 새 데이터를 읽을 수 있게&lt;/li&gt;
&lt;li&gt;FULL: 양방향 호환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실무에선 보통 &lt;strong&gt;BACKWARD&lt;/strong&gt;로 두고 스키마 변경룰을 팀 규칙으로 정합니다.&lt;/p&gt;
&lt;h2&gt;스키마 변경을 할 때 사용하는 안전 규칙&lt;/h2&gt;
&lt;p&gt;스키마는 한 번 배포되면 여러 Producer/Consumer가 함께 사용하므로, 변경할 때는 &lt;strong&gt;호환성(Compatibility)&lt;/strong&gt; 기준을 반드시 지켜야 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;여기서는 실무에서 가장 많이 사용하는 &lt;strong&gt;BACKWARD 호환성 기준&lt;/strong&gt;으로 정리합니다.&lt;/p&gt;
&lt;h3&gt;일반적으로 안전한 변경 (BACKWARD 호환 기준)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;필드 추가&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;기존 Consumer가 해당 필드를 몰라도 동작할 수 있도록 설계하면 안전합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;optional 필드 추가&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;새 필드가 없어도 기존 데이터 해석이 가능하므로 비교적 안전합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기본값(default)이 있는 필드 추가&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;이전 메시지에 해당 필드가 없어도 기본값으로 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;enum 값 추가 (주의 필요)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Consumer 구현이 새 enum 값을 처리할 수 있는지 확인해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;위험한 변경 (호환성 깨질 가능성이 큼)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;필드 삭제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;기존 Consumer가 기대하는 필드가 사라져 역직렬화/로직 처리에 문제가 생길 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;필드 타입 변경&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;예: &lt;code&gt;int -&amp;gt; string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;대부분의 경우 기존 Consumer와 호환되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;필드 이름 변경(리네이밍)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;단순 이름 변경처럼 보여도, 실제로는 기존 Consumer 입장에서 다른 필드로 인식될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;필드 의미 변경&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;타입은 같아도 의미가 바뀌면(예: &lt;code&gt;price&lt;/code&gt; 단위 변경) 운영 장애로 이어질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;실무 팁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;스키마 변경은 “기술적으로 등록 가능하냐”보다 &lt;strong&gt;기존 Consumer가 안전하게 읽을 수 있느냐&lt;/strong&gt;를 기준으로 판단해야 합니다.&lt;/li&gt;
&lt;li&gt;실무에서는 보통 호환성 모드를 &lt;strong&gt;BACKWARD&lt;/strong&gt;로 두고, 팀 규칙으로 다음을 함께 관리합니다.&lt;ul&gt;
&lt;li&gt;필드 추가 시 &lt;code&gt;optional/default&lt;/code&gt; 사용&lt;/li&gt;
&lt;li&gt;삭제/타입 변경은 새 버전 토픽 또는 충분한 마이그레이션 전략과 함께 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;hr&gt;
&lt;h2&gt;마무리&lt;/h2&gt;
&lt;p&gt;Kafka는 메시지를 잘 전달하는 데 강하지만, 메시지의 &lt;strong&gt;구조(스키마)&lt;/strong&gt; 자체를 이해하지는 못합니다.&lt;br&gt;그래서 실제 운영 환경에서는 스키마 레지스트리를 통해 &lt;strong&gt;스키마 버전 관리&lt;/strong&gt;, &lt;strong&gt;호환성 검사&lt;/strong&gt;, &lt;strong&gt;안전한 변경 통제&lt;/strong&gt;를 함께 가져가는 것이 중요합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;정리하면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;데이터(payload)는 Kafka에 저장&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스키마는 Schema Registry에 저장&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Producer/Consumer는 스키마 ID를 기준으로 직렬화/역직렬화를 수행&lt;/li&gt;
&lt;li&gt;스키마 변경은 호환성 규칙(BACKWARD/FORWARD/FULL) 아래에서 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;다음 글에서는 실제로 스키마 레지스트리(&lt;strong&gt;Apicurio Registry)를 설치하고&lt;/strong&gt;, 어떻게 동작이 되는지 알아보겠습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>Kafka</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/204</guid>
      <comments>https://ksh-cloud.tistory.com/204#entry204comment</comments>
      <pubDate>Thu, 26 Feb 2026 18:49:52 +0900</pubDate>
    </item>
    <item>
      <title>Strimzi Kafka + Debezium으로 MySQL CDC 구축하기</title>
      <link>https://ksh-cloud.tistory.com/203</link>
      <description>&lt;h1&gt;CDC 사용하는 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CDC는 Change Data Capture&lt;/b&gt; 의 약자 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 운영하다 보면 DB의 변경사항을 다른 시스템에 전파해야 하는 상황이 생깁니다. 흔히 애플리케이션 코드에서 직접 이벤트를 발행하는 방식을 쓰지만, 이 경우 DB 저장과 이벤트 발행이 동시에 성공해야 한다는 부담이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법 중 하나가 &lt;b&gt;CDC(Change Data Capture)&lt;/b&gt; 입니다. DB에 직접 쓰기만 하면 변경사항이 자동으로 이벤트로 발행되기 때문에, 애플리케이션 코드를 건드릴 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Kubernetes 환경에서 Strimzi Kafka와 Debezium을 사용해 MySQL의 변경사항을 실시간으로 Kafka 토픽으로 스트리밍하는 방법을 정리해보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;사전 준비&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;k8s&lt;/li&gt;
&lt;li&gt;Strimzi Kafka 설치&lt;/li&gt;
&lt;li&gt;MySQL 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;아키텍처&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/5179ec47-28f7-4114-9761-502523f950a0/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App DB: 애플리케이션이 실제로 읽고/쓰는 데이터베이스입니다.&lt;/li&gt;
&lt;li&gt;Binlog: MySQL에서 발생한 INSERT / UPDATE / DELETE 같은 변경 내역이 기록되는 로그입니다.&lt;/li&gt;
&lt;li&gt;Debezium은 테이블을 직접 계속 조회(polling)하는 게 아니라, 이 Binlog를 읽어서 변경 이벤트를 감지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka Connect&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Debezium MySQL Source Connector가 동작하는 런타임입니다.&lt;/li&gt;
&lt;li&gt;역할은 MySQL Binlog를 읽고 -&amp;gt; Kafka 메시지 이벤트로 변환 -&amp;gt; Kafka Cluster로 전송하는 것입니다.&lt;/li&gt;
&lt;li&gt;즉, MySQL과 Kafka 사이의 브리지(중간 연결 계층)입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka Cluster&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDC Topics: 실제 변경 이벤트가 쌓이는 Kafka 토픽들입니다.&lt;br /&gt;ex) 예: 주문 테이블 변경 이벤트, 고객 테이블 변경 이벤트&lt;/li&gt;
&lt;li&gt;Schema History Topic: Debezium이 스키마 변경 이력(컬럼 구조 변경 등)을 관리하기 위한 내부 토픽입니다.&lt;/li&gt;
&lt;li&gt;운영 시 디버깅/복구에 중요하지만, 일반 소비자 서비스가 직접 소비하는 토픽은 보통 아닙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 흐름&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App DB -&amp;gt; Binlog: 애플리케이션 트랜잭션 발생&lt;/li&gt;
&lt;li&gt;Binlog -&amp;gt; Kafka Connect: Debezium Connector가 변경 로그 읽음 (CDC read)&lt;/li&gt;
&lt;li&gt;Kafka Connect -&amp;gt; CDC Topics: 변경 이벤트를 Kafka 토픽으로 발행 (change events)&lt;/li&gt;
&lt;li&gt;Kafka Connect -&amp;gt; Schema History Topic: 스키마 이력 저장 (schema history)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 요약:&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 변경사항을 Binlog 기반으로 감지&lt;/li&gt;
&lt;li&gt;Kafka Connect(Debezium)가 이벤트로 변환&lt;/li&gt;
&lt;li&gt;Kafka Cluster 토픽으로 전달&lt;/li&gt;
&lt;li&gt;이후 소비자(Consumer)가 Kafka 토픽을 구독해서 후속 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Kafka &amp;lt;-&amp;gt; MySQL 연결을 해보도록 하겠습니다.&lt;/p&gt;
&lt;h1&gt;MySQL 설정 변경&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Binary Log 설정 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저 같은 경우는 MySQL 설치를 bitnamilegacy Helm 차트를 사용해서 설치를 했다보니, values.yaml 내용을 수정하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;primary.configuration&lt;/code&gt;의 &lt;code&gt;[mysqld]&lt;/code&gt; 섹션에 아래 내용 추가&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;primary:
  configuration: |-
    [mysqld]
    # ... 기존 설정 ...

    # Kafka Debezium CDC 설정
    server-id=1                        # Debezium이 MySQL에 Replica 처럼 등록될 때 서버를 식별하는 고유 ID. 해당 ID가 없으면 Debezium이 연결 자체를 못함
    log-bin=mysql-bin                # Binlog 활성화, 기본적으로 꺼져있을 수 있으며 이게 없으면 읽을 로그가 없음
    binlog_format=ROW                # Binlog를 행(Row) 단위로 기록 &quot;ROW&quot; 모드여야 행의 before/after 값을 정확히 알 수 있음
    binlog_row_image=FULL            # 변경된 행의 모든 컬럼 값을 로그에 기록
    binlog_expire_logs_seconds=604800  # binlog 저장 기간 7일 (MySQL 8.x 이상, expire_logs_days 대신)
    gtid_mode=ON                    # 각 트랜잭션에 전역 고유 ID 부여, Debezium 재시작 후 정확히 &quot;어디서부터&quot; 이어 읽을 지 위치 추적에 필요
    enforce_gtid_consistency=ON        # GTID 모드를 켯을 때, GTID와 호환되지 않는 트랜잭션을 거부, GTID 무결성 보장용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위 설정을 추가한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Debezium이 동작하는 방식을 먼저 이해해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Debezium은 MySQL의 &quot;복제 슬레이브(Replica)&quot; 처럼 동작&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL Master -&amp;gt; Slave 복제할 때 사용하는 &lt;b&gt;Binlog(바이너리 로그)&lt;/b&gt; 를 직접 읽어서 변경사항을 Kafka로 보냅니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;MySQL (binlog 생성) &amp;rarr; Debezium (binlog 읽기) &amp;rarr; Kafka Topic&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Debezium 전용 사용자 권한 부여 (MySQL 접속 후 실행)&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- Debezium 전용 사용자 생성
CREATE USER 'debezium'@'%' IDENTIFIED BY 'debezium_password';
GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'debezium'@'%';
FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정이 필요한 이유&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;권한&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REPLICATION SLAVE&lt;/td&gt;
&lt;td&gt;Debezium이 Binlog를 직접 스트리밍받는 핵심 권한. Replica 서버가 Master의 binlog를 받는 것과 동일한 권한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPLICATION CLIENT&lt;/td&gt;
&lt;td&gt;SHOW MASTER STATUS, SHOW SLAVE STATUS 등 복제 상태 조회 권한. Debezium이 현재 binlog 위치를 확인하는 데 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELECT&lt;/td&gt;
&lt;td&gt;커넥터 처음 시작 시 스냅샷(초기 전체 데이터 복사) 를 위해 테이블을 읽는 데 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RELOAD&lt;/td&gt;
&lt;td&gt;FLUSH TABLES WITH READ LOCK 실행 권한. 스냅샷 시 데이터 일관성을 위해 잠깐 테이블을 잠그는 용도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHOW DATABASES&lt;/td&gt;
&lt;td&gt;어떤 DB/테이블을 모니터링할지 목록을 확인하는 데 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. DB 테이블에 PK 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium은 기본적으로 &lt;b&gt;PK가 없는 테이블은 캡처 불가&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정이 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debezium은 Kafka 토픽에 메시지를 보낼 때 &lt;b&gt;Kafka Message Key = MySQL PK 값으로 매핑&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;MySQL Row (id=5, name=&quot;홍길동&quot;) 
    &amp;rarr; Kafka Message { key: &quot;5&quot;, value: { before: {...}, after: {...} } }&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;PK가 없으면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka에서 어떤 행이 변경됐는지 식별 불가&lt;/li&gt;
&lt;li&gt;같은 행에 대한 UPDATE/DELETE 이벤트가 다른 파티션에 분산되어 순서 보장 불가&lt;/li&gt;
&lt;li&gt;Debezium이 기본 설정에서 해당 테이블을 아예 캡처 거부&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Kafka Connect 설치&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strimzi의 KafkaConnect는 기본 이미지에 Debezium 플러그인이 포함되어 있지 않습니다.&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;KafkaConnect는 플러그인을 통해 다양한 외부 시스템과 연결할 수 있는데, 어떤 플러그인을 사용할지는 사용자가 직접 이미지에 포함시켜야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, &quot;Debezium MySQL 플러그인이 포함된 KafkaConnect 이미지&quot; 를 직접 빌드해서 사용해야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Debezium 플러그인 다운로드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 빌드하기 위해선, 아래 정보들을 먼저 확인해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;strimzi operator 버전&lt;/li&gt;
&lt;li&gt;Kafka 버전&lt;/li&gt;
&lt;li&gt;Kafka 버전과 호환되는 Debezium 플러그인 버전&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 링크에서 Release 버전 별로, 자신이 사용하고 있는 Kafka 버전에 맞춰 Plugin 다운로드를 하면 됩니다.&lt;br /&gt;&lt;a href=&quot;https://debezium.io/releases/3.2/release-notes&quot;&gt;Debezium Release Note 링크&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/6dd5814c-c061-4d48-842d-a9ab8420f220/image.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;다운로드는 아래 링크에서 해주면 됩니다.&lt;br /&gt;&lt;a href=&quot;https://debezium.io/releases/3.2/#installation&quot;&gt;Debezium 다운로드 링크&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/98e41d6b-90af-45fd-a0df-90a6ba9eeb7d/image.png&quot; alt=&quot;&quot; /&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;저 같은 경우 Kafka 4.0.0 버전을 사용하고 있어서 &lt;code&gt;3.2.6.Final&lt;/code&gt; 버전의 플러그인을 다운 받았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka Connect 이미지 빌드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 플러그인을 넣어서 Kafka Connect 이미지 빌드를 해보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 빌드하는 방법은 아래 공식문서를 참조하였습니다.&lt;br /&gt;&lt;a href=&quot;https://strimzi.io/docs/operators/latest/full/deploying#using-kafka-connect-with-plug-ins-str&quot;&gt;Strimzi Kafka 공식문서&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/8af67d55-f26f-40db-b425-17f3d7104bf5/image.png&quot; alt=&quot;&quot; /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 베이스 이미지는 현재 사용하고 계신 &lt;b&gt;Operator&lt;/b&gt;, &lt;b&gt;Kafka&lt;/b&gt; 버전을 넣어주시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;quay.io/strimzi.kafka:&amp;lt;자신이 사용하는 Operator Version&amp;gt;-kafka-&amp;lt;자신이 사용하는 Kafka Version&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;FROM quay.io/strimzi/kafka:0.47.0-kafka-4.0.0

USER root:root
COPY ./my-plugins/ /opt/kafka/plugins/
USER 1001&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저는 이대로 빌드하여 ECR에 Push 하여 사용하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 KafkaConnect 리소스 생성을 해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaConnect
metadata:
  name: kafka-connect
  namespace: &amp;lt;원하는 네임스페이스&amp;gt;
  annotations:
    strimzi.io/use-connector-resources: &quot;true&quot;
spec:
  version: &amp;lt;사용하고 있는 Kafka Cluster 버전&amp;gt;
  image: &amp;lt;앞서 빌드한 이미지&amp;gt;
  replicas: 1
  bootstrapServers: &amp;lt;설치한 Strimzi Kafka 서비스 주소&amp;gt;
  tls:
    trustedCertificates:     # Kafka Connect가 브로커의 인증서가 진짜인지 검증
      - secretName: kafka-cluster-cluster-ca-cert
        pattern: &quot;*.crt&quot;
  authentication:     # Kafka Connect가 브로커에게 자기 자신을 검증 할 때 사용하는 인증서
    type: tls
    certificateAndKey:
      secretName: &amp;lt;kafka cert가 있는 secret 이름&amp;gt;
      certificate: user.crt
      key: user.key
  config:    # Kafka Connect는 자신의 상태를 관리하기 위해 Kafka에 내부 토픽 생성
    group.id: connect-cluster
    offset.storage.topic: connect-cluster-offsets
    config.storage.topic: connect-cluster-configs
    status.storage.topic: connect-cluster-status
    config.storage.replication.factor: -1
    offset.storage.replication.factor: -1
    status.storage.replication.factor: -1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;spec.config&lt;/code&gt; 에서 정의한 내용에 대해 설명을 하자면, &lt;b&gt;KafkaConnect 내부 토픽 설정입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaConnect는 자신의 상태를 관리하기 위해 Kafka에 내부 토픽 3개를 자동으로 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;토픽명&lt;/th&gt;
&lt;th&gt;저장 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;offset.storage.topic&lt;/td&gt;
&lt;td&gt;connect-cluster-offsets&lt;/td&gt;
&lt;td&gt;각 Connector가 MySQL binlog 어디까지 읽었는지 위치 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;config.storage.topic&lt;/td&gt;
&lt;td&gt;connect-cluster-configs&lt;/td&gt;
&lt;td&gt;Connector 설정 정보 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;status.storage.topic&lt;/td&gt;
&lt;td&gt;connect-cluster-status&lt;/td&gt;
&lt;td&gt;Connector/Task 상태 저장 (RUNNING, FAILED 등)&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;code&gt;group.id&lt;/code&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KafkaConnect 워커가 있을 때 같은 클러스터로 묶어주는 식별자&lt;/li&gt;
&lt;li&gt;group.id를 가진 워커들이 하나의 Connect 클러스터를 구성합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;replication.factor: -1 의미&lt;/code&gt; :&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브로커의 default.replication.factor 설정을 따라간다는 뜻&lt;/li&gt;
&lt;li&gt;현재 kafka-persistent.yaml:143 에 default.replication.factor: 2 로 설정되어 있으므로 이 토픽들도 2로 복제됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Kafka Connector 리소스 생성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Kafka Connect 리소스가 정상적으로 생성이 되었다면,&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;MySQL과 연결하기 위해 &lt;b&gt;KafkaConnector&lt;/b&gt; 리소스를 생성해보도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaConnector
metadata:
  name: mysql-cdc-source
  namespace: &amp;lt;앞서 생성한 KafkaConnect 리소스와 같은 네임스페이스&amp;gt;
  labels:
    strimzi.io/cluster: &amp;lt;앞서 생성한 KafkaConnect 리소스 이름&amp;gt;
spec:
  class: io.debezium.connector.mysql.MySqlConnector
  tasksMax: 1
  config:
    database.hostname: &amp;lt;mysql 주소&amp;gt;
    database.port: &amp;lt;mysql 포트&amp;gt;
    database.user: &amp;lt;mysql 사용자 이름&amp;gt;
    database.password: &amp;lt;mysql 사용자 비밀번호&amp;gt;
    database.server.id: &quot;&amp;lt;MySQL에 연결된 다른 슬레이브/Debezium 인스턴스 ID와 겹치지 않는 숫자&amp;gt;&quot;

    topic.prefix: mysql1

    database.include.list: appdb
    table.include.list: appdb.orders,appdb.customers

    schema.history.internal.kafka.bootstrap.servers: &amp;lt;설치한 Kafka Service 주소&amp;gt;
    schema.history.internal.kafka.topic: schema-history.mysql1

    snapshot.mode: initial

    provide.transaction.metadata: &quot;true&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka Connector가 하는 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MySQL binlog를 읽음&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL에 직접 쿼리하는 게 아니라, binlog(DB 변경 로그)를 실시간으로 구독&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT&lt;/code&gt; / &lt;code&gt;UPDATE&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt; 이벤트 감지&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;읽고싶은 DB와 테이블 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;database.include.list: appdb&lt;/code&gt; 기준으로 변경 사항을 Kafka에 주고 싶은 데이터베이스 지정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table.include.list: appdb.orders,appdb.customers&lt;/code&gt; 기준으로 변경 사항을 kafka에 주고 싶은 table 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;변경 이벤트를 Kafka Topic으로 발행&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;topic.prefix: mysql1&lt;/code&gt; 기준으로 자동으로 토픽 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mysql1.appdb.orders&lt;/code&gt;, &lt;code&gt;mysql1.appdb.customers&lt;/code&gt; 이런 형태로 만들어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스냅샷 (첫 실행 시)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;snapshot.mode: initial&lt;/code&gt; 이므로 처음 실행할 때, 현재 테이블 데이터를 전부 읽어서 Kafka에 밀어넣고, 그 이후부터 binlog 변경분만 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한줄 요약&lt;br /&gt;MySQL 데이터 변경 사항을 실시간으로 Kafka 메시지로 변환해주는 역할&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;결과 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 &lt;code&gt;appdb&lt;/code&gt; 데이터베이스 생성하고, &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;customers&lt;/code&gt; 테이블을 생성을 해준 후 데이터 &lt;code&gt;INSERT&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 데이터베이스 생성
CREATE DATABASE IF NOT EXISTS appdb
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

USE appdb;

-- 2. 테이블 생성
CREATE TABLE customers (
  id BIGINT NOT NULL AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  email VARCHAR(255),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);

CREATE TABLE orders (
  id BIGINT NOT NULL AUTO_INCREMENT,
  customer_id BIGINT NOT NULL,
  status VARCHAR(50),
  total_amount DECIMAL(10, 2),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);

-- 3. 트랜잭션으로 데이터 INSERT
START TRANSACTION;

INSERT INTO customers (name, email) VALUES
  ('홍길동', 'hong@example.com'),
  ('김철수', 'kim@example.com'),
  ('이영희', 'lee@example.com');

INSERT INTO orders (customer_id, status, total_amount) VALUES
  (1, 'pending', 15000.00),
  (1, 'completed', 32000.00),
  (2, 'pending', 8500.00);

-- 트랜잭션 ID 확인
SELECT TRX_ID FROM information_schema.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();

COMMIT;&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;그리고 kafka 토픽에서 메시지를 확인하면 아래와 같이 결과가 나오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/4a89d963-bf2b-44a7-a268-b3b542f899a3/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/a879bfcf-1577-4eb1-9ff3-1ac4dc000b80/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;</description>
      <category>Kafka</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/203</guid>
      <comments>https://ksh-cloud.tistory.com/203#entry203comment</comments>
      <pubDate>Tue, 24 Feb 2026 17:45:22 +0900</pubDate>
    </item>
    <item>
      <title>EKS 내부 서비스들을 하나의 Internal NLB로 통합하여 비용 개선 (ECS 연동 개선기)</title>
      <link>https://ksh-cloud.tistory.com/202</link>
      <description>&lt;p&gt;이번 글에서는 NLB 하나에 여러 서비스를 사용할 수 있도록 해보겠습니다.&lt;/p&gt;
&lt;h1&gt;해당 작업을 하게된 계기&lt;/h1&gt;
&lt;p&gt;현재 대부분의 코어 서비스들은 EKS 클러스터 내부에서 동작하고 있습니다.&lt;br&gt;Redis, Kafka, OpenObserve, AI-Gateway 등 주요 인프라 구성 요소들이 모두 Kubernetes 환경 위에서 운영되고 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;하지만 AI Agent 중 일부 애플리케이션은 안정성을 최우선으로 고려하여 ECS Fargate Task 기반으로 실행되고 있었고,&lt;br&gt;이 애플리케이션이 EKS 내부 서비스들과 통신해야 하는 요구사항이 발생했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;처음 고려했던 구조: Internal ALB 통합&lt;/h2&gt;
&lt;p&gt;여러 서비스를 외부로 노출해야 하는 상황에서, 처음에는 하나의 Internal ALB에 여러 서비스를 묶는 구조를 고려했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;ALB는 Host 기반, Path 기반 라우팅을 지원하기 때문에&lt;br&gt;HTTP 기반 서비스라면 하나의 로드밸런서로 충분히 통합이 가능하다고 판단했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러나 여기서 문제가 발생했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;ALB를 사용할 수 없었던 이유&lt;/h2&gt;
&lt;p&gt;ALB는 L7(Application Layer) 로드밸런서로, HTTP/HTTPS 프로토콜만을 지원합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;반면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;이 두 서비스는 TCP 기반의 바이너리 프로토콜을 사용합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;즉, ALB는 HTTP 헤더를 기반으로 트래픽을 처리하는 구조이기 때문에,&lt;br&gt;TCP 레벨에서 동작하는 Redis와 Kafka 트래픽을 처리할 수 없습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;결과적으로 ALB는 이 요구사항을 충족할 수 없는 구조였습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;L4 기반 설계로 방향 전환&lt;/h2&gt;
&lt;p&gt;이에 따라 L7이 아닌, &lt;strong&gt;L4 레벨에서 동작하는 NLB(Network Load Balancer)&lt;/strong&gt;를 사용하도록 방향을 전환했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;NLB는 TCP/UDP 레벨에서 동작하기 때문에&lt;br&gt;Redis, Kafka와 같은 비-HTTP 프로토콜을 문제없이 처리할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;또 다른 문제: 서비스 수만큼 생성되는 NLB&lt;/h2&gt;
&lt;p&gt;하지만 새로운 문제가 생겼습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;각 서비스마다 Internal NLB를 하나씩 생성하는 구조가 되었고,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis → NLB 1개&lt;/li&gt;
&lt;li&gt;Kafka → NLB 1개&lt;/li&gt;
&lt;li&gt;OpenObserve → NLB 1개&lt;/li&gt;
&lt;li&gt;기타 서비스 → 각각 NLB&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;이렇게 되면서 서비스 수만큼 NLB가 증가하게 되었고,&lt;br&gt;시간당 비용 + LCU 비용이 누적되면서 유지 비용이 비효율적인 구조가 되었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;해결 방법: 단일 NLB + 포트 기반 Listener 분리&lt;/h2&gt;
&lt;p&gt;이 문제를 해결하기 위해 고민하던 중,&lt;br&gt;NLB의 Listener를 서비스별로 포트를 다르게 구성하면 하나의 NLB로 여러 서비스를 처리할 수 있다는 점에 주목했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;즉,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NLB:6379 → Redis&lt;/li&gt;
&lt;li&gt;NLB:31021 → Kafka&lt;/li&gt;
&lt;li&gt;NLB:5080 → OpenObserve&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;와 같이 Listener 포트 기반으로 Target Group을 분리하는 구조로 설계했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이렇게 구성함으로써,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NLB 리소스를 하나로 통합&lt;/li&gt;
&lt;li&gt;비용 절감&lt;/li&gt;
&lt;li&gt;관리 포인트 단순화&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;라는 세 가지 목표를 동시에 달성할 수 있었습니다.&lt;/p&gt;
&lt;h1&gt;구현 방법&lt;/h1&gt;
&lt;p&gt;실제로 제가 했던 방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2&gt;1. NLB 생성&lt;/h2&gt;
&lt;p&gt;콘솔에서 다음과 같이 생성 하였습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/26e68a3d-2aad-41dd-948c-1c00b22372be/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;저 같은 경우 같은 ECS와 EKS가 같은 VPC에 있어서 내부 로드밸런서로 생성하였습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/1011cc3c-57c6-43db-bb75-4bf98c2248ca/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;ECS, EKS가 존재하는 VPC와 서브넷을 선택 해줍니다.&lt;/p&gt;
&lt;h2&gt;2. 대상 그룹 생성&lt;/h2&gt;
&lt;p&gt;이제 EKS 내부에 존재하는 파드들과 연결할 대상 그룹을 생성 해줍니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/70c70dfb-76a2-44ed-a128-77d949e34fe9/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;대상 유형은 IP 주소로 선택 하였습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그리고 포트는 EKS Service에서 사용하고 있는 포트를 넣어주면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ex) 만약, redis 서비스가 6380 포트를 사용하고 있다면, 대상 그룹의 포트도 6380으로 지정
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)
redis             ClusterIP   10.100.206.42   &amp;lt;none&amp;gt;        6380/TCP&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;프로토콜 또한, 각 대상이 사용하는 프로토콜에 맞춰서 사용하면 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;즉, redis는 TCP 프로토콜 사용하므로 대상 그룹의 프로토콜도 TCP로 지정&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;3. TargetGroupBinding 리소스 생성&lt;/h2&gt;
&lt;p&gt;저 같은 경우는 &lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt;가 설치되어있어서&lt;br&gt;해당 controller가 제공하는 커스텀 리소스인 &lt;strong&gt;TargetGroupBinding&lt;/strong&gt;을 사용하여&lt;br&gt;자동으로 &lt;strong&gt;대상 그룹의 대상을 매핑&lt;/strong&gt;하도록 하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TargetGroupBinding 리소스 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: elbv2.k8s.aws/v1beta1
kind: TargetGroupBinding
metadata:
  name: redis-tgb
spec:
  serviceRef:
    name: redis          # 실제 redis 파드와 연결된 서비스 이름
    port: 6380        # 서비스가 노출하는 포트
  targetGroupARN: &amp;lt;2번 과정에서 생성한 대상 그룹의 ARN을 넣어주면 됩니다.&amp;gt;
  targetType: &amp;lt;2번 과정에서 생성한 대상 그룹의 대상 유형을 넣어주면 됩니다.&amp;gt;
  networking: 
    ingress:
    - from:
      - securityGroup:
          groupID: &amp;lt;1번 과정에서 생성한 NLB의 보안그룹을 넣어주면 됩니다.&amp;gt;
      ports: 
      - port: &amp;lt;2번 과정에서 생성한 대상 그룹의 포트를 넣어주면 됩니다.&amp;gt;
        protocol: &amp;lt;2번 과정에서 생성한 대상 그룹의 프로토콜을 넣어주면 됩니다.&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이제 해당 리소스를 EKS에서 apply 하면 &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/c77fbcc2-cd61-422b-97a2-b2adb7cde4bb/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;자동으로 대상그룹의 대상에 Redis 파드와 연결이 됩니다.&lt;/p&gt;
&lt;h2&gt;4. NLB 리스너 추가&lt;/h2&gt;
&lt;p&gt;1번 과정에서 생성한 NLB에서 아래 이미지 처럼 리스너를 추가합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/7db7a7e7-40be-4142-997f-812698fcaf99/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;리스너를 추가할 때는, &lt;strong&gt;대상 그룹이 사용하는 프로토콜과 포트&lt;/strong&gt;를 넣어서 생성해주면 됩니다.&lt;/p&gt;
&lt;h2&gt;5. 동작 확인&lt;/h2&gt;
&lt;p&gt;저는 Redis insight 사용하여 실제로 앞선 과정으로 생성된 NLB 주소와 포트를 넣으면 연결이 되는지 확인해보았습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/bdb534fb-f8a2-4961-9dc5-04b4ecb664c1/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이미지를 보면 정상적으로 접속이 되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;hr&gt;
&lt;br&gt;

&lt;p&gt;이상 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>AWS</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/202</guid>
      <comments>https://ksh-cloud.tistory.com/202#entry202comment</comments>
      <pubDate>Thu, 12 Feb 2026 18:55:55 +0900</pubDate>
    </item>
    <item>
      <title>Prometheus에서 수집 가능한 커스텀 메트릭 만들기 (Python)</title>
      <link>https://ksh-cloud.tistory.com/201</link>
      <description>&lt;h1&gt;커스텀 메트릭 생성하게 된 계기&lt;/h1&gt;
&lt;p&gt;기존에는 KEDA를 사용하여 SQS 큐 안에있는 메시지 개수를 보고 오토스케일링을 했었는데, &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;문제는 파드가 늘어나면 SQS 메시지 수를 &lt;strong&gt;파드 수로 나누어 평균 분산&lt;/strong&gt; 해버리는 구조여서 원하는 방식대로 오토스케일링 기준을 잡기가 어려웠습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이에 따라 개발자분이 만들어둔 &lt;strong&gt;SlotManager의 슬롯 상태를 기준&lt;/strong&gt;으로 오토스케일링을 하려고 메트릭을 만들게 되었습니다.&lt;/p&gt;
&lt;h1&gt;메트릭 생성&lt;/h1&gt;
&lt;p&gt;이제 실제로 제가 어떻게 했는지 과정을 말씀드리겠습니다.&lt;/p&gt;
&lt;h2&gt;Prometheus Client 의존성 추가&lt;/h2&gt;
&lt;p&gt;먼저 Python 애플리케이션에서 Prometheus 메트릭을 만들기 위해 &lt;strong&gt;prometheus-client&lt;/strong&gt; 라이브러리를 추가하였습니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;prometheus-client&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;이 라이브러리는, Counter, Gauge, Summary 같은 메트릭 타입을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pyproject.toml
---
prometheus-client = &amp;quot;^0.21.0&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;메트릭 모듈 생성&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from prometheus_client import Gauge # 앞서 정의한 prometheus_client 라이브러리에서 Gauge 타입을 사용할 수 있도록 선언해줍니다.

# 활성 슬롯 수 (처리 중 + 대기)
SLOTMANAGER_ACTIVE_TOTAL = Gauge(
    &amp;quot;slotmanager_active_total&amp;quot;, # 메트릭을 slotmanager_active_total 라는 이름으로 선언
    &amp;quot;Number of active slot entries (processing + available).&amp;quot;, # slotmanager_active_total 메트릭에 대한 설명
)
# 처리 중인 슬롯 수
SLOTMANAGER_ACTIVE_PROCESSING = Gauge(
    &amp;quot;slotmanager_active_processing&amp;quot;,
    &amp;quot;Number of slots currently processing.&amp;quot;,
)
# 남은 슬롯 수
SLOTMANAGER_AVAILABLE_SLOTS = Gauge(
    &amp;quot;slotmanager_available_slots&amp;quot;,
    &amp;quot;Number of available slots.&amp;quot;,
)
# 슬롯 활용률 (%)
SLOTMANAGER_UTILIZATION_PERCENT = Gauge(
    &amp;quot;slotmanager_utilization_percent&amp;quot;,
    &amp;quot;Slot utilization percentage.&amp;quot;,
)
# 설정된 최대 슬롯 수
SLOTMANAGER_MAX_SLOTS = Gauge(
    &amp;quot;slotmanager_max_slots&amp;quot;,
    &amp;quot;Configured maximum slots.&amp;quot;,
)

# SlotManager의 현재 상태(dict)를 받아서 Prometheus 메트릭 값으로 그대로 반영하는 함수
def update_from_status(status: dict) -&amp;gt; None:
    if not status:
        return

    SLOTMANAGER_MAX_SLOTS.set(status.get(&amp;quot;max_slots&amp;quot;, 0))
    SLOTMANAGER_ACTIVE_TOTAL.set(status.get(&amp;quot;active_total&amp;quot;, 0))
    SLOTMANAGER_ACTIVE_PROCESSING.set(status.get(&amp;quot;active_processing&amp;quot;, 0))
    SLOTMANAGER_AVAILABLE_SLOTS.set(status.get(&amp;quot;available_slots&amp;quot;, 0))
    SLOTMANAGER_UTILIZATION_PERCENT.set(status.get(&amp;quot;utilization_percent&amp;quot;, 0))&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;update_from_status&lt;/strong&gt; 함수는 &lt;strong&gt;SlotManager&lt;/strong&gt;의 현재 상태(dict)를 받아서 각 값들을 Prometheus Gauage에 그대로 반영(set)하는데, &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;status&lt;/strong&gt;에 빈 값이 들어오면, 아무것도 안하고 함수룰 종료 시키려고 아래와 같이 방어 코드를 넣어두었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if not status:
    return&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;그리고 개발자 분이 개발한 &lt;strong&gt;SlotManager&lt;/strong&gt; 클래스에서,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;앞서 생성한 코드가 있는 디렉터리를 &lt;strong&gt;파이썬 패키지로 인식&lt;/strong&gt;시키기 위해 &lt;strong&gt;&lt;strong&gt;init&lt;/strong&gt;.py&lt;/strong&gt;를 추가해줍니다.&lt;/p&gt;
&lt;h2&gt;SlotManager에서 메트릭 갱신 연결&lt;/h2&gt;
&lt;p&gt;이제 개발자 분이 만들어둔 SlotManager의 현재 상태를 찍는 함수에서 앞서 생성한 &lt;strong&gt;update_from_status&lt;/strong&gt; 함수에 &lt;strong&gt;status&lt;/strong&gt; 값을 전달 해줍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slot_manager.py
---
from app.service.metrics.slotmanager_metrics import update_from_status

class SlotManager:
    def get_status(self) -&amp;gt; Dict[str, Any]:
        ...
        status ={
            &amp;quot;max_slots&amp;quot;: self.max_slots,
            &amp;quot;active_processing&amp;quot;: processing_count,
            &amp;quot;active_total&amp;quot;: active_count,
            &amp;quot;utilization_percent&amp;quot;: utilization,
            &amp;quot;available_slots&amp;quot;: self.max_slots - active_count,
            ...
        }

        update_from_status(status)
        return status&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;/metrics 경로에서 Prometheus 메트릭 수집 할 수 있도록 정의&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;main.py
---
@app.get(&amp;quot;/metrics&amp;quot;)
def metrics():
    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;FastAPI 애플리케이션에 &lt;strong&gt;HTTP GET /metrics&lt;/strong&gt; 엔드포인트를 등록 해주고,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;해당 PATH로 접근하면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;현재 Prometheus Registry에 등록된 모든 메트릭을 Prometheus 표준 텍스트 포맷으로 직렬화해서,&lt;br&gt;&lt;br&gt;&lt;br&gt;올바른 Content-Type 헤더와 함께 HTTP 응답으로 반환 해줍니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;이렇게 생성을 해서 Prometheus에 메트릭을 수집 시켜주기 위해, &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;Kubernetes에서 &lt;strong&gt;ServiceMonitor&lt;/strong&gt; 리소스를 생성해주고 결과를 확인하면&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/9ba2220c-0f80-4802-979b-a5e6332db159/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;정상적으로 메트릭이 나오는 것을 볼 수 있습니다.&lt;/p&gt;</description>
      <category>개발/Python</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/201</guid>
      <comments>https://ksh-cloud.tistory.com/201#entry201comment</comments>
      <pubDate>Thu, 8 Jan 2026 15:56:16 +0900</pubDate>
    </item>
    <item>
      <title>[Go 언어 학습기 #9] Go 언어(Golang) 의 포인터</title>
      <link>https://ksh-cloud.tistory.com/200</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/ff663ac2-195b-48be-b351-39ce7e30e346/image.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;이 글은 &lt;strong&gt;Must Have Tucker의 Go 언어 프로그래밍&lt;/strong&gt; 책을 참고하여 작성하였으며, 개인적인 학습 내용을 정리한 글입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h1&gt;포인터&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;포인터는 메모리 주소를 값으로 갖는 타입입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;포인터를 이용하면 동일한 메모리 공간을 여러 변수가 가리킬 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;예를 들어 int 타입 변수 a를 선언하면, &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;변수 a&lt;/strong&gt;는 &lt;strong&gt;메모리에 저장&lt;/strong&gt;되어 있고 &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;속성으로 메모리 주소&lt;/strong&gt;를 가지고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var a int = 3
---
- 변수 a는 값 10으로 선언
- 메모리 어딘가에 저장됨
- 그 메모리 주소를 가리키는 것이 포인터&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이 때 &lt;strong&gt;변수 a의 주소가 0x0100 번지라고 가정&lt;/strong&gt;을 했을 때, &lt;strong&gt;메모리 주솟값(0x0100)&lt;/strong&gt; 또한 &lt;strong&gt;숫자값이기 때문에 다른 변수의 값으로 사용&lt;/strong&gt;될 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이렇게 메모리 주솟값을 변숫값으로 가질 수 있는 변수를 &lt;strong&gt;포인터 변수&lt;/strong&gt;라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/d6ff80b0-37ce-427d-8563-51395b934b28/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;위 그림에서 정의한대로 나타내면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p = &amp;amp;a
---
- p는 포인터 변수 p 라는 의미
- 그리고 포인터 변수 p는 변수 a의 값이 저장된 위치를 가리킵니다.
- 즉, p = 0x0100 번지&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이렇게 &lt;strong&gt;메모리 주소를 값으로 가져 메모리 공간을 가리키는 타입을 포인터&lt;/strong&gt; 라고 합니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;포인터 변수 선언&lt;/h2&gt;
&lt;p&gt;포인터 변수는 가리키는 데이터 타입 앞에 * 를 붙여서 선언 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. var p *int
2. var a int
3. p = &amp;amp;a
---
1. p는 int 타입 데이터의 메모리 주소를 가리키는 포인터 변수라는 의미
2. int 타입의 변수 a 선언
3. 변수 a의 메모리 주소를 포인터 변수 p에 대입&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;여기서 보면 &lt;strong&gt;&amp;amp;&lt;/strong&gt;과 * 은 무엇인가 궁금하실 텐데요&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&amp;amp;&lt;/strong&gt;은 주소 연산자 입니다. 변수의 메모리 주소를 가져옵니다.&lt;/li&gt;
&lt;li&gt;*는 역참조 연산자 입니다. 포인터가 가리키는 실제 값을 가져옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var a int = 10
var p *int

p = &amp;amp;a
fmt.Println(p) // 변수 a가 가진 10 값에 대한 메모리 주솟값 출력
fmt.Println(*p) // 10 출력&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;예시를 통해 포인터 변수 사용법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    var a int = 500
    var p *int // ❶ int 포인터 변수 p 선언

    p = &amp;amp;a // ❷ a의 메모리 주소를 변수 p의 값으로 대입(복사)

    fmt.Printf(&amp;quot;p의 값: %p\n&amp;quot;, p)            // ❸ 메모리 주솟값 출력
    fmt.Printf(&amp;quot;p가 가리키는 메모리의 값: %d\n&amp;quot;, *p) // ❹ p가 가리키는 메모리의 값 출력
    *p = 100                               // ➎ p가 가리키는 메모리 공간의 값을 변경합니다.
    fmt.Printf(&amp;quot;a의 값: %d\n&amp;quot;, a)            // ➏ a값 변화 확인
}
---
p의 값: 0x1400000e088
p가 가리키는 메모리의 값: 500
a의 값: 100&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;❷ 포인터 변수 p에 a의 메모리 주소 값을 넣어줍니다.&lt;br&gt;❸ 그리고 p 값을 찍어보면, 변수 a의 메모리 주솟값을 출력합니다.&lt;br&gt;❹ 그리고 p가 가리키는 메모리 주소에 담긴 값을 출력하면 변수 a에 담겨있던 500이 출력됩니다.&lt;br&gt;➎ 포인터 변수 p가 가리키는 메모리 공간 값을 100으로 변경해주면,&lt;br&gt;➏ a의 값이 500에서 100으로 변경되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;포인터의 기본값 nil&lt;/h2&gt;
&lt;p&gt;포인터 변숫값은 초기화해주지 않으면 기본값 &lt;strong&gt;nil&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이 값은 0이지만 정확한 의미는 유효하지 않는 메모리 주솟값&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;즉, 어떤 메모리 공간도 가리키고 있지 않다는 걸 나타냅니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이 상태에서 &lt;strong&gt;역참조(*)&lt;/strong&gt;를 하여 메모리 공간 값을 꺼내려고 하면 프로그램이 즉시 panic이 됩니다.&lt;br&gt;따라서 Go 포인터에서 가장 중요한 런타임 에러 원인 중 하나입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이제 포인터에 대해 얼추 알아보았는데, 그러면 이 포인터 언제써야하는지 알아보겠습니다.&lt;/p&gt;
&lt;h2&gt;포인터 사용 이유&lt;/h2&gt;
&lt;p&gt;변수 대입이나 함수 인수 전달은 항상 값을 복사하기 때문에 많은 메모리 공간을 사용하는 문제와&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;큰 메모리 공간을 복사할 때 발생하는 성능 문제를 안고 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;또한 다른 공간으로 복사가 되기 때문에 변경 사항이 적용되지도 않습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이제 예시를 통해 포인터를 사용하는 이유를 알아보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포인터를 사용하지 않을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

type Data struct { // ❶ Data형 구조체
    value int
    data  [200]int
}

func ChangeData(arg Data) { // ❷ 파라미터로 Data를 받습니다.
    arg.value = 999
    arg.data[100] = 999
}

func main() {
    var data Data

    ChangeData(data) // ❸ 인수로 data를 넣습니다.
    fmt.Printf(&amp;quot;value = %d\n&amp;quot;, data.value)
    fmt.Printf(&amp;quot;data[100] = %d\n&amp;quot;, data.data[100]) // ❹ data 필드 출력
}
---
value = 0
data[100] = 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;❸ ChangeData(data)를 호출할 때 data 전체가 복사되어 함수로 전달이 됩니다.&lt;br&gt;❷ 그래서 함수 안에서 바꾼 건 복사본이고,&lt;br&gt;❹ main에 있는 원본 data를 출력해보면 값이 전혀 바뀌지 않은 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;해당 예제의 문제점은 ChangeData() 함수 호출 시 data 변숫값이 모두 복사되기 때문에 구조체 크기만큼 복사됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;즉, ChangeData() 함수를 짧은 시간에 많이 호출하면 성능 문제가 발생 할 수 있습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;이러한 문제를 해결해주는 것이 포인터 입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포인터를 사용할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//ch12/ex12.4/ex12.4.go
package main

import &amp;quot;fmt&amp;quot;

type Data struct {
    value int
    data  [200]int
}

func ChangeData(arg *Data) { // ❶ 파라미터로 Data 포인터를 받습니다.
    arg.value = 999 // ❸ arg 데이터 변경
    arg.data[100] = 999
}

func main() {
    var data Data

    ChangeData(&amp;amp;data)                      // ❷ 인수로 data의 주소를 넘깁니다.
    fmt.Printf(&amp;quot;value = %d\n&amp;quot;, data.value) // ❹ data 필드값 출력
    fmt.Printf(&amp;quot;data[100] = %d\n&amp;quot;, data.data[100])
}
---
value = 999
data[100] = 999&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;앞선 내용과 비슷하지만, ❶ 여기서 파라미터로 Data 구조체의 포인터를 받은 걸로 변경합니다.&lt;br&gt;그리고 ❷ 에서 Data 구조체의 data 변수의 포인터 주소값을 넘겨주면,&lt;br&gt;data 변수의 주소의 공간에 ChangeData() 함수의 데이터들이 들어가게되어&lt;br&gt;❹ 에서 보면 data 필드의 값들이 변경되어 출력되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이게 포인터를 안썻을 때와 어떤 차이점이 있냐면, &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;❷ 여기서 data 변숫값이 아니라 메모리 주소를 인수로 전달을 한거여서, 구조체 전부가 복사되는 것이 아닌 메모리 주소 값만 복사가 되어, 효율적으로 데이터를 조작할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;구조체를 생성해 포인터 변수 초기화하기&lt;/h3&gt;
&lt;p&gt;앞선 예제에서는 구조체 변수를 별도로 생성을 하고, 포인터 변수에 구조체 변수의 메모리 주솟값을 넣어주었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ex)
var data Data // 구조체 변수 선언
var p *Data = &amp;amp;data // 포인터 변수 p에 구조체 변수(data)의 메모리 주솟값 복사&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 하면 번거로우니 아래의 방식으로 하면 코드 한줄로 정의할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ex)
var p *Data = &amp;amp;Data{} // 포인터 변수 p에 구조체의 메모리 주솟값을 복사&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 하면 따로 구조체 변수를 선언하지 않고 바로 포인터 변수 p에 구조체의 메모리 주솟값을 복사해줄 수 있습니다.&lt;/p&gt;
&lt;h1&gt;인스턴스&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;인스턴스란 메모리에 할당된 데이터의 실체를 말합니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;예시를 들자면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var data Data&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;위와 방법은 Data 구조체 타입의 data 변수를 만드는 방법인데, &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이렇게 하면 data 변수가 사용할 수 있는 메모리 공간이 생기는데, 해당 공간의 실체를 &lt;strong&gt;인스턴스&lt;/strong&gt; 라고 부릅니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러면 인스턴스를 만들어보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var data Data // data 변수가 사용할 수 있는 인스턴스 생성
var p1 *Data = &amp;amp;data // 포인터 변수 p가 인스턴스를 가리킴&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 인스턴스가 생성이 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그리고 포인터 변수로 해당 인스턴스를 가리켜도 인스턴스는 추가로 생성되지 않습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var p2 *Data = p1
var p3 *Data = p1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/851382ae-59f2-48c0-99d5-d1d2a2a26b62/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러나 포인터와 달리 하나의 구조체 변수를 복사한 여러 개의 구조체 변수들의 인스턴스들은 다릅니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var data1 Data
var data2 Data = data1
var data3 Data = data1&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 되면 &lt;strong&gt;data1, data2, data3&lt;/strong&gt; 모두 인스턴스 입니다.&lt;br&gt;포인터와 달리 서로 같은 값을 가지고 있을 뿐 이지 각각 별개의 인스턴스가 생성이 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;인스턴스에 대해 좀 더 설명을 하자면,&lt;/p&gt;
&lt;h3&gt;인스턴스는 데이터의 실체 입니다.&lt;/h3&gt;
&lt;p&gt;앞서 설명을 했던 것 처럼 인스턴스는 데이터의 실체인데,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;포인터를 이용해서 인스턴스에 접근할 수 있습니다.&lt;br&gt;구조체 포인터를 함수 매개변수로 받는다는 말은 구조체 인스턴스로 입력을 받겠다는 이야기와 같습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;앞서 포인터값을 별도의 변수를 선언하지 않고 초기화하는 방법을 보았습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그런데, &lt;strong&gt;new()&lt;/strong&gt; 내장 함수를 이용하면 더 간단히 표현할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p := new(int)&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;위와 같이 사용을 하는데, &lt;strong&gt;new(Type)&lt;/strong&gt; new 함수에는 꼭 타입이 들어가야합니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;즉, 타입의 &amp;quot;제로값으로 초기화된 인스턴스&amp;quot;를 하나 만들고 그 인스턴스의 주소를 반환하는 내장 함수 입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;인스턴스는 언제 사라지나&lt;/h3&gt;
&lt;p&gt;인스턴스는 메모리에 할당된 데이터의 실체인데, 메모리는 무한한 자원이 아닙니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;만약 메모리에 데이터가 할당만 되고 사라지지 않는다면 프로그램은 금세 메모리가 고갈되어 비정상 종료가 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그래서 쓸모없는 데이터를 메모리에서 해제하는 기능이 필요합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;Go 언어는 &lt;strong&gt;가비지 컬렉터&lt;/strong&gt; 라는 메모리 청소부 기능을 제공합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그래서 우린 따로 메모리를 정리하지 않아도 가비지 컬렉터가 알아서 메모리를 청소를 해줍니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;다만,&lt;/strong&gt; 가비지 컬렉터가 쓸모 없는 데이터를 지워주는 데 성능이 많이 씁니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;따라서 메모리 관리 면에서는 이득이 있지만 성능면에서는 손해가 발생합니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;정리하자면,&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;인스턴스는 메모리에 생성된 데이터의 실체&lt;/li&gt;
&lt;li&gt;포인터를 이용해서 인스턴스를 가리키게 할 수 있음&lt;/li&gt;
&lt;li&gt;함수 호출 시 포인터 인수를 통해서 인스턴스를 입력받고 그 값을 변경 가능&lt;/li&gt;
&lt;li&gt;쓸모 없어진 인스턴스는 가비지 컬렉터가 자동으로 지워줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;스택 메모리와 힙 메모리&lt;/h1&gt;
&lt;p&gt;대부분 프로그래밍 언어는 메모리를 할당할 때 스택 메모리 영역 또는 힙 메모리 영역을 사용합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이론상 스택 메모리가 힙 메모리보다 효율적이지만, &lt;strong&gt;스택 메모리는 함수 내부에서만 사용가능한 영역&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그래서 &lt;strong&gt;함수 외부로 공개되는 메모리 공간은 힙 메모리 영역에서 할당&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;Go 언어는 탈출 검사(escape analysis)를 통해 어느 메모리에 할당할지 결정 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;예제 코드를 보겟습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

type User struct {
    Name string
    Age  int
}

func NewUser(name string, age int) *User {
    var u = User{name, age}
    return &amp;amp;u // 탈출 분석으로 u 메모리가 사라지지 않음
}

func main() {
    userPointer := NewUser(&amp;quot;AAA&amp;quot;, 23)

    fmt.Println(userPointer)
}
---
&amp;amp;{AAA 23}&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;원래라면  NewUser() 함수에서 선언한 변수 u는 함수 내부에서 선언된 변수여서 함수가 종료되면 사라져야 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그런데 userPointer 변수를 출력 해보면 정상적으로 값이 나옵니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이 말은, Go 언어에서는 탈출 검사를 통해서 u 변수의 인스턴스가 함수 외부로 공개되는 것을&lt;br&gt;분석해내서 u를 스택메모리가 아닌 힙 메모리에 할당을 하게된 것 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;즉 Go 언어는 메모리 공간이 함수 외부로 공개되는지 여부를 자동으로 검사해서 스택 or 힙 메모리에 할당할지 결정 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;hr&gt;
&lt;br&gt;

&lt;p&gt;이상 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>개발/Go</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/200</guid>
      <comments>https://ksh-cloud.tistory.com/200#entry200comment</comments>
      <pubDate>Wed, 24 Dec 2025 16:04:18 +0900</pubDate>
    </item>
    <item>
      <title>[Go 언어 학습기 #8] Go 언어(Golang) 의 슬라이스</title>
      <link>https://ksh-cloud.tistory.com/199</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/ff663ac2-195b-48be-b351-39ce7e30e346/image.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;이 글은 &lt;strong&gt;Must Have Tucker의 Go 언어 프로그래밍&lt;/strong&gt; 책을 참고하여 작성하였으며, 개인적인 학습 내용을 정리한 글입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h1&gt;슬라이스&lt;/h1&gt;
&lt;p&gt;슬라이스는 Go 언어에서 제공하는 동적 배열 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;동적 배열&lt;/strong&gt; 이란? 자동으로 배열 크기를 증가시키는 자료 구조 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;일반적인 배열은 처음 배열을 선언할 때 정한 길이에서 더 늘어나지 않습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;하지만 슬라이스를 사용하면 이런 불편함에서 벗어날 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;바로 선언 방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2&gt;슬라이스 선언&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;var slice []int&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;기존 배열 선언에서 배열의 길이만 정의하지 않으면 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러다보니 슬라이스 길이를 초기화 해주지 않으면 길이가 0인 슬라이스가 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;예시 코드를 보면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    var slice []int

    if len(slice) == 0 { // ❶ slice 길이가 0인지 확인
        fmt.Println(&amp;quot;slice is empty&amp;quot;, slice)
    }

    slice[1] = 10 // ❷ 에러 발생
    fmt.Println(slice)
}
---
slice is empty []
panic: runtime error: index out of range [1] with length 0

goroutine 1 [running]:&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;❶ slice 길이가 0 인지 검사를 합니다. 저희는 slice를 초기화 시켜주지 않았기 때문에 초깃값 0 그대로여서 조건이 true 나오고 &amp;quot;slice is empty&amp;quot; 문구가 출력이 됩니다.&lt;br&gt;❷ 길이가 0인 slice에서 두번째 요솟값에 접근을 하다보니 패닉이 발생을 합니다. 여기서 할당되지 않은 메모리 공간에 접근해서 프로그램이 비정상적으로 종료된 거라고 생각하시면 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러면 이제 슬라이스 초기화 방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h3&gt;슬라이스 초기화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;첫번째 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var slice1 = []int{1, 2, 3}
var slice2 = []int{1, 5:2, 10:3} // [1 0 0 0 0 2 0 0 0 0 3] 이렇게 배열됨, 5:2는 인덱스 5인 요소는 2 라는 의미 입니다.&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;두번째 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var slice = make([]int, 3)&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이 방식은 &lt;strong&gt;make() 내장 함수&lt;/strong&gt;를 사용하는 방법 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;위와 같이 slice 선언을 하면, 길이 3개짜리 int 슬라이스 값을 갖습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;ex) [0 0 0] &amp;lt;= 이렇게 길이는 3개고, 요소 값을 지정해주지 않았으니 초깃값 0으로 생성됨&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;슬라이스 요소 접근&lt;/h3&gt;
&lt;p&gt;배열과 동일한 방법으로 접근 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var slice = make([]int, 3)
slice[1] = 5&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 하면 길이 3짜리 int 슬라이스 만들고, 인덱스 1에 5 값으로 변경하게 됩니다.&lt;/p&gt;
&lt;h3&gt;슬라이스 순회&lt;/h3&gt;
&lt;p&gt;이것 역시 배열과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var slice = []int{1, 2, 3}

for i := 0; i &amp;lt; len(slice); i++ { // 1번
    slice[i] += 10
}

for i, v := range slice { // 2번
    slice[i] = v * 2
}&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;1번, len() 내장 함수를 사용해 slice 길이를 알아내어 순회하면서 각 요소의 값에 10씩 더해줌&lt;br&gt;2번, range 키워드를 사용해 각 요소들을 순회, for 문에 있는 v는 요솟값을 뜻하므로 각 요솟값에 2씩 곱해줍니다.&lt;/p&gt;
&lt;h3&gt;슬라이스 요소 추가 - append()&lt;/h3&gt;
&lt;p&gt;기존 배열은 한번 길이가 정해지면 늘릴 수 없지만, 슬라이스는 요소를 추가해 길이를 늘릴 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;요소 추가를 할 때, &lt;strong&gt;append&lt;/strong&gt; 내장 함수를 사용 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {

    var slice = []int{1, 2, 3} // ❶ 요소가 3개인 슬라이스

    slice2 := append(slice, 4) // ❷ 요소 추가

    fmt.Println(slice)
    fmt.Println(slice2)
}
&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;❷ 에서 보면, append 함수를 사용해서, ❶ 에서 정의한 slice에 새로운 요솟값 4를 추가해주면&lt;br&gt;&lt;strong&gt;[1 2 3 4]&lt;/strong&gt; 로 4개의 요소를 같은 슬라이스가 생기는 걸 알 수 있습니다.&lt;/p&gt;
&lt;h2&gt;슬라이스 동작 원리&lt;/h2&gt;
&lt;p&gt;슬라이스는 내장 타입으로 내부 구현이 감춰져 있지만, &lt;strong&gt;SliceHeader&lt;/strong&gt; 구조체를 사용해 내부 구현을 살펴볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type SliceHeader struct {
    Data uintptr    // 실제 배열을 가리키는 포인터
    Len int            // 요소 개수
    Cap int            // 실제 배열의 길이
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;슬라이스 구현은 배열을 가리키는 포인터와 요소 개수를 나타내는 &lt;strong&gt;len&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;전체 배열 길이를 나타내는 &lt;strong&gt;cap&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;즉, 슬라이스는 &lt;strong&gt;배열 자체가 아니라 배열을 가리키는 뷰&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;앞서 make() 함수를 사용해 slice를 생성을 했었는데, 좀 더 자세히 알아보면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var slice = make([]int, 3)&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 정의를 하면 &lt;strong&gt;SliceHeader&lt;/strong&gt; 구조체에서는 아래 그림 처럼 들어 갑니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/22c5b6ee-06f3-4401-8e84-f3669acf7541/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;slice&lt;/strong&gt; 는 &lt;strong&gt;len(요소 개수)&lt;/strong&gt;이 3이고, &lt;strong&gt;cap(배열 길이)&lt;/strong&gt;이 3인 슬라이스가 생깁니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;또 다른 예시를 보면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var slice2 = make([]int, 3, 5)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/1c31b433-2266-4af4-bb8f-5de10c984c1a/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;len(요소 개수)은 3개, cap(배열 길이)은 5개 말하자면 총 5개 중 3개만 사용하고 나머지 2개는 나중에 추가될 요소를 위해 비워뒀다고 생각하면 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그리고 요솟값을 초기화 해주지 않았으니 초기값인 0으로 3개의 요소에 들어가는 것을 알 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;지금까지의 설명을 들어보면 &lt;strong&gt;슬라이스는 SliceHeader 구조체를 사용하는 배열이라고 생각되어 배열을 사용하는 방법과 비슷하게 사용할 수도 있을 것 같습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러나 배열과 사용법이 비슷하다고 해서 똑같이 사용하면 버그를 만날 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;예시를 통해 알아보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

func changeArray(array2 [5]int) { // ❶ 배열을 받아서 세 번째 값 변경
    array2[2] = 200
}

func changeSlice(slice2 []int) { // ❷ 슬라이스를 받아서 세 번째 값 변경
    slice2[2] = 200
}

func main() {
    array := [5]int{1, 2, 3, 4, 5}
    slice := []int{1, 2, 3, 4, 5}

    changeArray(array)
    changeSlice(slice)

    fmt.Println(&amp;quot;array:&amp;quot;, array)
    fmt.Println(&amp;quot;slice:&amp;quot;, slice)
}
---
array: [1 2 3 4 5]
slice: [1 2 200 4 5]&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;결과 값을 보면, 배열은 인덱스 2번의 값이 200으로 변경 되지 않았지만, 슬라이스는 변경 된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;왜 이런지 알아보겠습니다.&lt;/p&gt;
&lt;h3&gt;동작 차이의 원인&lt;/h3&gt;
&lt;p&gt;Go 언어에서는 모든 값의 대입은 복사로 일어납니다. 배열도 마찬가지 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그렇다 보니 &lt;strong&gt;changeArray()&lt;/strong&gt; 함수가 &lt;strong&gt;main()&lt;/strong&gt; 함수에서 호출이 될 때, changeArray() 함수의 인수로 array를 입력해서 호출을 하면 array 값이 array2로 복사가 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그런데, array 배열과 array2 배열은 메모릭 공간이 다른 &lt;strong&gt;완전히 다른 배열&lt;/strong&gt; 이기 때문에, array2의 인덱스 2번 요소 값을 200으로 변경해도 array 배열은 변경되지 않습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그래서 마지막에 array 배열의 값을 찍어보면 [1 2 3 4 5] 그대로 나옵니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러나 changeSlice() 함수가 호출될 때를 보면, &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/784c67bb-ed9e-4edb-9cce-e2c860581e88/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;앞서 설명한 것 처럼 slice는 &amp;quot;배열 자체가 아니라 배열을 가리키는 뷰&amp;quot; 라고 했었습니다.&lt;br&gt;그래서 changeSlice() 함수의 인수로 slice가 입력되어 호출되면 slice, slice2 모두 같은 배열을 보게 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러다 보니, slice2에서 인덱스 2번 요솟값을 200으로 변경하면 slice의 인덱스 2번 요솟값도 200으로 변경되는 것과 동일해서 결과값에서 slice는 [1 2 200 4 5]로 바뀐 것을 알 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러나 만약 &lt;strong&gt;append() 함수&lt;/strong&gt;를 사용해서 slice2의 배열 길이를 늘려주면 slice와 다른 배열로 취급 되기도 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;append() 함수&lt;/strong&gt;가 호출되면 먼저 슬라이스에서 값을 추가할 수 있는 빈 공간이 있는지 확인 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;남은 빈 공간 = cap - len&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 계산을 해서 남은 빈 공간의 개수가 추가하는 값의 개수보다 크거나 같은 경우 배열의 뒷부분에 값을 추가한 뒤 len 값을 증가 시킵니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러나, 빈공간이 없으면 &lt;strong&gt;새로운 더 큰 배열을 마련합니다.&lt;/strong&gt;&lt;br&gt;일반적으로 &lt;strong&gt;기존 배열의 2배 크기로 마련&lt;/strong&gt;합니다. 그런 뒤 기존 배열의 요소를 모두 새로운 배열에 복사를 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그러다 보니, &lt;strong&gt;기존 배열을 사용을 하더라도 앞선 내용 처럼 같은 배열을 바라보는 것이 아닌 새로운 배열이 생기게 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;예시를 들어보면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    slice1 := []int{1, 2, 3} // ❶ len:3 cap:3 슬라이스 생성

    slice2 := append(slice1, 4, 5) // ❷ append() 함수로 요소 추가

    fmt.Println(&amp;quot;slice1:&amp;quot;, slice1, len(slice1), cap(slice1))
    fmt.Println(&amp;quot;slice2:&amp;quot;, slice2, len(slice2), cap(slice2))

    slice1[1] = 100 // ❸ slice1 요솟값 변경

    fmt.Println(&amp;quot;After change second element&amp;quot;)
    fmt.Println(&amp;quot;slice:&amp;quot;, slice1, len(slice1), cap(slice1))
    fmt.Println(&amp;quot;slice2:&amp;quot;, slice2, len(slice2), cap(slice2))

    slice1 = append(slice1, 500) // ➍ slice1 요솟값 변경

    fmt.Println(&amp;quot;After append 500&amp;quot;)
    fmt.Println(&amp;quot;slice1:&amp;quot;, slice1, len(slice1), cap(slice1))
    fmt.Println(&amp;quot;slice2:&amp;quot;, slice2, len(slice2), cap(slice2))
}
---
slice1: [1 2 3] 3 3
slice2: [1 2 3 4 5] 5 6
After change second element
slice: [1 100 3] 3 3
slice2: [1 2 3 4 5] 5 6
After append 500
slice1: [1 100 3 500] 4 6
slice2: [1 2 3 4 5] 5 6&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;slice1은 &lt;strong&gt;cap - len&lt;/strong&gt;을 했을 때, 남은 빈 공간이 0 이여서 append() 함수를 사용해서&lt;br&gt;slice2에 slice1 배열에 4, 5 값을 추가를 해주면 &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;slice1과 slice2 모두 같은 배열을 바라보는 것이 아닌, 각각 서로 다른 배열을 바라보게 됩니다.&lt;/p&gt;
&lt;h1&gt;슬라이싱&lt;/h1&gt;
&lt;p&gt;슬라이싱은 &lt;strong&gt;배열의 일부를 집어내는 기능&lt;/strong&gt;을 말합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;슬라이싱 기능을 이용하면 그 결과로 &lt;strong&gt;슬라이스를 반환&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;즉, 배열의 일부를 떼어내 슬라이스로 만드는 기능 입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;바로 사용법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;array[startIdx:endindex]&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 배열에서 &lt;strong&gt;시작인덱스:끝인덱스&lt;/strong&gt;를 적어주면,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;배열의 &lt;strong&gt;시작인덱스&lt;/strong&gt; 부터 &lt;strong&gt;끝인덱스 - 1&lt;/strong&gt;까지 잘라내어 슬라이스로 반환 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/9de751ca-6f3a-4e60-9ccd-3a8e3c1caaa6/image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;위 그림에서 &lt;code&gt;array[1:3]&lt;/code&gt; 슬라이싱 하면&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;code&gt;[2 3]&lt;/code&gt; 슬라이스가 생기게 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;다만 여기서 중요한 점이 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;원본 배열에서 슬라이싱 하여 슬라이스를 생성을 하면 새로운 슬라이스가 생기는 것이 아닌, 원본 배열을 바라보는 슬라이스가 생성이 되는 것 입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;예시로 보면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &amp;quot;fmt&amp;quot;

func main() {
    array := [5]int{1, 2, 3, 4, 5}

    slice := array[1:2] // ❶ 슬라이싱

    fmt.Println(&amp;quot;array:&amp;quot;, array)
    fmt.Println(&amp;quot;slice:&amp;quot;, slice, len(slice), cap(slice))

    array[1] = 100 // ❷ array의 두 번째 값 변경

    fmt.Println(&amp;quot;After change second element&amp;quot;)
    fmt.Println(&amp;quot;array:&amp;quot;, array)
    fmt.Println(&amp;quot;slice:&amp;quot;, slice, len(slice), cap(slice))

    slice = append(slice, 500) // ❸ slice에 값 추가

    fmt.Println(&amp;quot;After append 500&amp;quot;)
    fmt.Println(&amp;quot;array:&amp;quot;, array)
    fmt.Println(&amp;quot;slice:&amp;quot;, slice, len(slice), cap(slice))
}
---
array: [1 2 3 4 5]
slice: [2] 1 4
After change second element
array: [1 100 3 4 5]
slice: [100] 1 4
After append 500
array: [1 100 500 4 5]
slice: [100 500] 2 4&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 원본 배열에서 잘라낸 슬라이스의 요솟값을 변경해도 원본 배열의 요솟값도 같이 변경 되는 것을 알 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;또한, 슬라이스의 cap의 길이가 4인 이유도 array에서 슬라이싱할 때, &lt;strong&gt;원본 배열의 길이 - 슬라이싱의 startIdx&lt;/strong&gt; 길이를 가지게 되어 cap(slice)의 값이 4가 나오게 됩니다.&lt;/p&gt;
&lt;h2&gt;슬라이스를 슬라이싱&lt;/h2&gt;
&lt;p&gt;슬라이싱은 배열 뿐 아니라 슬라이스의 일부를 집어낼 때도 사용 가능 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;pre&gt;&lt;code&gt;slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[1:2] // slice2는 [2]

fmt.Println(len(slice1), cap(slice1)) // len:5, cap: 5
fmt.Println(len(slice2), cap(slice2)) // len:1, cap: 4&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;처음부터 슬라이싱&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;slice1 := []int{1, 2, 3, 4, 5}

slice2 := slice1[0:3] // slice2는 [1, 2, 3] 
slice2 := slice1[:3] // slice2는 [1, 2, 3] 위 방식과 동일한 결과&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;끝까지 슬라이싱 &lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;slice1 := []int{1, 2, 3, 4, 5}

slice2 := slice1[2:len(slice1)] // slice2는 [3, 4, 5] 
slice2 := slice1[2:] // slice2는 [3, 4, 5] 위 방식과 동일한 결과&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;전체 슬라이싱&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;array := []int{1, 2, 3, 4, 5}

slice := array[:] // 전체 슬라이스&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;hr&gt;
&lt;br&gt;

&lt;p&gt;이렇게 슬라이스에 대해 알아보았습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>개발/Go</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/199</guid>
      <comments>https://ksh-cloud.tistory.com/199#entry199comment</comments>
      <pubDate>Tue, 23 Dec 2025 18:12:49 +0900</pubDate>
    </item>
    <item>
      <title>[Go 언어 학습기 #7] Go 언어(Golang) 의 패키지</title>
      <link>https://ksh-cloud.tistory.com/198</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dongli/post/ff663ac2-195b-48be-b351-39ce7e30e346/image.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;이 글은 &lt;strong&gt;Must Have Tucker의 Go 언어 프로그래밍&lt;/strong&gt; 책을 참고하여 작성하였으며, 개인적인 학습 내용을 정리한 글입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h1&gt;패키지 란?&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Go에서 패키지는 코드를 묶는 가장 큰 단위 입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;패키지 = 관련된 Go 파일들의 집합&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;하나의 패키지는 &lt;strong&gt;하나의 디렉터리&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;같은 디렉터리에 있는 .go 파일들은 &lt;strong&gt;모두 같은 패키지명&lt;/strong&gt;을 가져야 합니다.&lt;/li&gt;
&lt;li&gt;코드 재사용, 의존성 관리, 네임스페이스 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myapp/
 ├─ main.go        ← package main
 ├─ utils/
 │   ├─ math.go    ← package utils
 │   └─ string.go  ← package utils&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;utils 디렉터리에 있는 .go 파일들은 모두 package utils 로 같은 패키지를 사용하는데,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;main.go 는 package myapp이 아니라 package main 으로 시작을 해야 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;왜 이런 것인지 알아보겠습니다.&lt;/p&gt;
&lt;h2&gt;main 패키지&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;main 패키지는 특별한 패키지로 프로그램 시작점을 포함한 패키지&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;프로그램 시작점이란 main() 함수를 의미&lt;/strong&gt; 합니다.&lt;br&gt;프로그램이 실행되면 운영체제는 프로그램을 메모리로 올립니다. 그리고 이것을 &lt;code&gt;로드(Load)&lt;/code&gt; 라고 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그런 다음 프로그램 시작점 부터 한 줄씩 코드를 실행합니다. 이 때 프로그램 시작점이 main() 함수이고,&lt;br&gt;main() 함수를 포함한 패키지가 main 패키지 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이제 패키지 사용법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h1&gt;패키지 사용하기&lt;/h1&gt;
&lt;p&gt;패키지를 사용하려면 &lt;strong&gt;import&lt;/strong&gt; 예약어로 임포트를 하고 원하는 패키지 경로를 따옴표로 묶어서 사용해야합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &amp;quot;fmt&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;여러 패키지를 임포트 하려면 아래와 같이 하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;os&amp;quot;
)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;패키지 멤버에 접근하기&lt;/h2&gt;
&lt;p&gt;패키지를 사용하려고 import로 가져오면 해당 패키지에 접근을 하려면 &lt;strong&gt;점(.)&lt;/strong&gt; 연산자를 사용해 패키지에서 제공하는 함수, 구조체 등에 접근할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;예를 들어,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fmt.Println(&amp;quot;Hello world&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이렇게 fmt 패키지에 . 연산자를 사용해서 Println() 함수를 사용하는 것 처럼 다른 패키지들도 이와 동일한 방식으로 사용 하면 됩니다.&lt;/p&gt;
&lt;h2&gt;경로(Path)가 있는 패키지 사용하기&lt;/h2&gt;
&lt;p&gt;예시 코드로 바로 알아보자면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//ch14/ex14.1/ex14.1.go
package main

import ( // ❶ 둘 이상의 패키지는 소괄호로 묶어줍니다.
    &amp;quot;fmt&amp;quot;
    &amp;quot;math/rand&amp;quot; // ❷ 패키지명은 rand입니다.
)

func main() {
    fmt.Println(rand.Int()) // ❸ 랜덤한 숫자값을 출력합니다.
}&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;❷ fmt 패키지와 달리 math/rand 라는 패키지를 사용하려고 import에 정의를 해줍니다.&lt;br&gt;❸ 실제로 패키지를 사용하려면, 패키지의 가장 끝 경로에 있는 폴더명을 정의해주면 됩니다. 여기서는 rand 가 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;따라서 마지막에 &lt;strong&gt;fmt.Println(rand.Int())&lt;/strong&gt; 이렇게 사용한 것을 알 수 있습니다.&lt;/p&gt;
&lt;h2&gt;겹치는 패키지 문제, 별칭으로 풀기&lt;/h2&gt;
&lt;p&gt;만약 패키지명이 겹칠 땐 어떻게 할까요?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
    &amp;quot;text/template&amp;quot;
    &amp;quot;html/template&amp;quot;
)&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;위 예시 처럼 패키지의 끝 폴더명이 template 일 때는 어떻게 사용을 하냐?&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;별칭을 붙여줘서 해결하면 됩니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;import (
    &amp;quot;text/template&amp;quot;
    htemplate &amp;quot;html/template&amp;quot; // 별칭 htemplate
)

func main() {
    template.New(&amp;quot;foo&amp;quot;).Parse(`{{define &amp;quot;T&amp;quot;}}Hello`)
    htemplate.New(&amp;quot;foo&amp;quot;).Parse(`{{define &amp;quot;T&amp;quot;}}Hello`)
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;사용하지 않는 패키지 포함하기&lt;/h2&gt;
&lt;p&gt;Go 언어에서는 사용하지 않는 패키지를 import에 정의하고 실행시키면 에러가 발생합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;패키지를 직접 사용하지 않지만 부가효과를 얻고자 import에 정의하는 경우 &lt;strong&gt;밑줄_&lt;/strong&gt; 을 패키지명 앞에 붙여주면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
    &amp;quot;database/sql&amp;quot;
    _ &amp;quot;github.com/mattn/go-sqlite3&amp;quot; // 밑줄 _을 이용해서 오류 방지
)&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;Go 모듈&lt;/h1&gt;
&lt;p&gt;Go 모듈은 Go 패키지들을 모아놓은 Go 프로젝트 단위 입니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;즉, &lt;strong&gt;Go 모듈 = 이 프로젝트의 이름표 + 의존성 잠금 장치&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;Go 1.16 버전부터는 Go 모듈 사용이 기본이 되어 모든 Go 코드는 Go 모듈 아래 있어야 합니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;실제로 Go 모듈은 &lt;code&gt;go.mod&lt;/code&gt; 파일로 정의 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myapp/
 ├─ go.mod   ← 이게 모듈
 ├─ main.go
 └─ utils/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그래서 go.mod 파일은 모듈 이름과 Go 버전, 필요한 외부 패키지 등이 명시되어 있습니다.&lt;/p&gt;
&lt;p&gt;Go 모듈은 &lt;code&gt;go mod init&lt;/code&gt; 명령을 통해 만들 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go mod init [패키지명]&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;패키지명과 패키지 외부 공개&lt;/h1&gt;
&lt;p&gt;Go 언어에서 패키지명을 지을 때 아래와 같이 권장 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;쉽고 간단하게 지을 것&lt;/li&gt;
&lt;li&gt;모든 문자를 소문자로 할 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 이전에도 언급했듯이, 패키지 전역으로 선언된 첫 글자가 대문자로 시작되는 모든 변수, 상수, 타입, 함수, 메서드는 패키지 외부로 공개 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이제 패키지 외부로 공개되는 것과 공개되지 않는 것을 예제로 알아보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상수, 변수, 구조체, 함수 등을 정의 해놓은 publicpkg 패키지 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package publicpkg

import &amp;quot;fmt&amp;quot;

const (
    PI = 3.1415   // 외부로 공개되는 상수
    pi = 3.141516 // 외부로 공개되지 않는 상수
)

var ScreenSize int = 1080 // 외부로 공개되는 변수
var screenHeight int      // 외부로 공개되지 않는 변수

func PublicFunc() { // 외부로 공개되는 함수
    const MyConst = 100 // 외부로 공개되지 않습니다.
    fmt.Println(&amp;quot;This is a public function&amp;quot;, MyConst)
}

func privateFunc() { // 외부로 공개되지 않는 함수
    fmt.Println(&amp;quot;This is a private function&amp;quot;)
}

type MyInt int       // 외부로 공개되는 별칭 타입
type myString string // 외부로 공개되지 않는 별칭 타입

type MyStruct struct { // 외부로 공개되는 구조체
    Age  int    // 외부로 공개되는 구조체 필드
    name string // 외부로 공개되지 않는 구조체 필드
}

func (m MyStruct) PublicMethod() { // 외부로 공개되는 메서드
    fmt.Println(&amp;quot;This is a public method&amp;quot;)
}

func (m MyStruct) privateMethod() { // 외부로 공개되지 않는 메서드
    fmt.Println(&amp;quot;This is a private method&amp;quot;)
}

type myPrivateStruct struct { // 외부로 공개되지 않는 구조체
    Age  int    // 외부로 공개되지 않는 구조체 필드
    name string // 외부로 공개되지 않는 구조체 필드
}

func (m myPrivateStruct) PrivateMethod() { // 외부로 공개되지 않는 메서드
    fmt.Println(&amp;quot;This is a private method&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;위 코드로 publicpkg 패키지를 정의했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;이제 아래 코드를 생성 후 실행시켜보고 결과 값을 확인해보면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실제로 외부 노출이 되는 지 확인하기 위한 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &amp;quot;fmt&amp;quot;

    &amp;quot;ch14/ex14.2/publicpkg&amp;quot;
)

func main() {
    fmt.Println(&amp;quot;PI:&amp;quot;, publicpkg.PI)
    publicpkg.PublicFunc()

    var myint publicpkg.MyInt = 10
    fmt.Println(&amp;quot;myint:&amp;quot;, myint)

    var mystruct = publicpkg.MyStruct{Age: 18}
    fmt.Println(&amp;quot;mystruct:&amp;quot;, mystruct)
}
---
PI: 3.1415
This is a public function 100
myint: 10
mystruct: {18 }&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;publicpkg 패키지에서 외부로 노출 가능하도록 설정해둔 상수, 함수, 구조체 등을 확인할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그런데 헷갈리는 게, 어떤 건 외부 노출이 되고 어떤 건 외부 노출이 안돼고 이러한 내용이 있어서 표로 간단하게 정리해보았습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;식별자&lt;/th&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;선언 위치/소속&lt;/th&gt;
&lt;th&gt;패키지 외부 접근&lt;/th&gt;
&lt;th&gt;왜?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;PI&lt;/td&gt;
&lt;td&gt;상수&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;대문자로 시작하는 패키지 레벨 식별자(exported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pi&lt;/td&gt;
&lt;td&gt;상수&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;소문자로 시작하는 패키지 레벨 식별자(unexported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScreenSize&lt;/td&gt;
&lt;td&gt;변수&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;대문자로 시작하는 패키지 레벨 식별자(exported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;screenHeight&lt;/td&gt;
&lt;td&gt;변수&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;소문자로 시작하는 패키지 레벨 식별자(unexported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PublicFunc&lt;/td&gt;
&lt;td&gt;함수&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;대문자로 시작하는 패키지 레벨 함수(exported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MyConst&lt;/td&gt;
&lt;td&gt;상수&lt;/td&gt;
&lt;td&gt;function scope (PublicFunc 내부)&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;함수(블록) 스코프라 외부 접근 경로가 없음(=export 개념 적용 대상 아님)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;privateFunc&lt;/td&gt;
&lt;td&gt;함수&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;소문자로 시작하는 패키지 레벨 함수(unexported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MyInt&lt;/td&gt;
&lt;td&gt;타입(alias/defined type)&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;대문자로 시작하는 패키지 레벨 타입(exported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;myString&lt;/td&gt;
&lt;td&gt;타입(alias/defined type)&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;소문자로 시작하는 패키지 레벨 타입(unexported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MyStruct&lt;/td&gt;
&lt;td&gt;구조체 타입&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;대문자로 시작하는 패키지 레벨 타입(exported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MyStruct.Age&lt;/td&gt;
&lt;td&gt;구조체 필드&lt;/td&gt;
&lt;td&gt;타입(MyStruct) 멤버&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;필드명이 대문자이고, 소속 타입(MyStruct)도 exported라 접근 경로가 열림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MyStruct.name&lt;/td&gt;
&lt;td&gt;구조체 필드&lt;/td&gt;
&lt;td&gt;타입(MyStruct) 멤버&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;필드명이 소문자라 unexported(외부에서 필드 선택 불가)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(MyStruct) PublicMethod&lt;/td&gt;
&lt;td&gt;메서드&lt;/td&gt;
&lt;td&gt;수신자: MyStruct(exported)&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;메서드명 대문자 + 수신자 타입(MyStruct)가 exported라 외부에서 호출 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(MyStruct) privateMethod&lt;/td&gt;
&lt;td&gt;메서드&lt;/td&gt;
&lt;td&gt;수신자: MyStruct(exported)&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;메서드명이 소문자라 unexported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;myPrivateStruct&lt;/td&gt;
&lt;td&gt;구조체 타입&lt;/td&gt;
&lt;td&gt;package scope&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;타입명이 소문자라 unexported(외부에서 타입 자체를 참조 불가)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;myPrivateStruct.Age&lt;/td&gt;
&lt;td&gt;구조체 필드&lt;/td&gt;
&lt;td&gt;타입(myPrivateStruct) 멤버&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;필드명이 대문자여도, 소속 타입가 unexported라 외부에서 그 타입 경로로 접근 자체가 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;myPrivateStruct.name&lt;/td&gt;
&lt;td&gt;구조체 필드&lt;/td&gt;
&lt;td&gt;타입(myPrivateStruct) 멤버&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;필드명도 소문자 + 타입도 unexported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(myPrivateStruct) PrivateMethod&lt;/td&gt;
&lt;td&gt;메서드&lt;/td&gt;
&lt;td&gt;수신자: myPrivateStruct(unexported)&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;메서드명은 대문자여도 수신자 타입이 unexported라 외부에서 값/변수 생성·참조가 불가 → 호출 경로가 막힘&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h1&gt;패키지 초기화&lt;/h1&gt;
&lt;p&gt;패키지를 임포트하고 나면, 아래와 같은 순서로 동작을 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;패키지를 임포트하면 컴파일러는 패키지 내 전역 변수를 초기화 합니다.&lt;/li&gt;
&lt;li&gt;그런 다음 패키지에 init() 함수가 있다면 호출해 패키지를 초기화 합니다. 여기서 init() 함수는 반드시 입력 매개변수가 없고 반환값도 없는 함수여야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;

&lt;p&gt;그런데 만약 어떤 패키지의 초기화 함수인 init() 함수 기능만 사용하길 원할 경우 &lt;strong&gt;밑줄 _&lt;/strong&gt;을 이용해서 임포트 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ex)
import
    &amp;quot;database/sql&amp;quot;
    _ &amp;quot;github.com/mattn/go-splite3&amp;quot; // 이렇게 밑줄 _ 을 이용해서 init() 함수 호출&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;이제 예시를 통해 자세히 알아보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;exinit 패키지 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package exinit

import &amp;quot;fmt&amp;quot;

var (
    a = c + b // 
    b = f()   // 
    c = f()   // 
    d = 3     // 
)

func init() { 
    d++                             
    fmt.Println(&amp;quot;init function&amp;quot;, d) 
}

func f() int { // 
    d++                      // 
    fmt.Println(&amp;quot;f() d:&amp;quot;, d) // 
    return d                 // 
}

func PrintD() {
    fmt.Println(&amp;quot;d:&amp;quot;, d)
}&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;만약 exinit 패키지를 다른 코드에서 import 해서 사용을 하면 앞서 이야기 했던 내용 대로 &lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;가장 먼저&lt;/strong&gt;, 패키지 내 전역 변수들을 초기화 시켜줍니다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a값은 c, b가 초기화 된 다음 초기화 되고&lt;/li&gt;
&lt;li&gt;b 값은 &lt;strong&gt;f()&lt;/strong&gt; 함수에서 초깃값이 d=3으로 정의 되어있는 걸로 초기화 되어 4가 되고&lt;/li&gt;
&lt;li&gt;c 값은 b 값을 초기화할 때, d=4 로 초기화되어 c 값은 5가 되고&lt;/li&gt;
&lt;li&gt;d 값은 c 값을 초기화할 때, d=5 로 초기화 되어 마지막으로 d 값은 6이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;이렇게 전역 변수들의 값 초기화가 먼저 되고 그 이후 init() 함수가 실행된 다음에 &lt;strong&gt;exinit&lt;/strong&gt; 패키지를 사용할 수 있게 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그래서 아래 코드에서 결과를 보면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;exinit 패키지를 사용하는 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &amp;quot;ch14/ex14.3/exinit&amp;quot; // exinit 패키지 임포트
    &amp;quot;fmt&amp;quot;
)

func main() { 
    fmt.Println(&amp;quot;main function&amp;quot;)
    exinit.PrintD()
}
---
f() d: 4
f() d: 5
init function 6
main function
d: 6&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;결과 값의 내용을 보면,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;방금 전에 말한 것 처럼 &lt;strong&gt;exinit 패키지를 import 한 시점에 전역 변수들의 값이 제일 먼저 초기화가 이루어지고&lt;/strong&gt;,&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;main() 함수가 실행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;br&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;즉, 간단하게 요약을 하면 &lt;strong&gt;패키지를 import 하면 패키지 초기화가 시작되는데 이때 패키지 내 모든 전역 변수들이 초기화 되고 그 다음에 init() 함수가 호출 되고 난 이후에 main() 함수가 시작 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;hr&gt;
&lt;br&gt;

&lt;p&gt;이상으로 패키지에 대해 알아보았습니다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>개발/Go</category>
      <author>황동리</author>
      <guid isPermaLink="true">https://ksh-cloud.tistory.com/198</guid>
      <comments>https://ksh-cloud.tistory.com/198#entry198comment</comments>
      <pubDate>Mon, 22 Dec 2025 14:35:44 +0900</pubDate>
    </item>
  </channel>
</rss>