<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>후니 님의 블로그</title>
    <link>https://0321ljh.tistory.com/</link>
    <description>후니 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sun, 17 May 2026 01:06:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>0321ljh</managingEditor>
    <image>
      <title>후니 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8615544/attach/8b9b42e50b7d4e2089f32189d0452e3f</url>
      <link>https://0321ljh.tistory.com</link>
    </image>
    <item>
      <title>Terraform 입문클릭으로 만들던 인프라를 코드로 관리하기</title>
      <link>https://0321ljh.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- ===== HERO ===== --&gt;
&lt;div class=&quot;hero&quot;&gt;
&lt;div class=&quot;hero-badge&quot;&gt;☁️ Infrastructure as Code&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 콘솔 없이도 동일한 인프라를 반복 재현하는 법&amp;nbsp;&lt;br /&gt;개념부터 실전 패턴, 실수 방지까지&lt;/p&gt;
&lt;div class=&quot;meta-row&quot;&gt;&lt;span&gt;  2025년 5월&lt;/span&gt; &lt;span&gt; ️ Terraform &amp;middot; IaC &amp;middot; DevOps&lt;/span&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;wrapper&quot;&gt;&lt;!-- ===== TOC ===== --&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div class=&quot;toc-title&quot;&gt;  목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#why&quot;&gt;왜 Terraform을 써야 하는가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#core&quot;&gt;핵심 개념 4가지 &amp;mdash; Provider / Resource / State / Plan+Apply&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#commands&quot;&gt;기본 명령어와 동작 흐름&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#state&quot;&gt;State와 Remote Backend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#vars&quot;&gt;변수 &amp;middot; 출력 &amp;middot; 모듈&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#security&quot;&gt;절대로 코드에 넣으면 안 되는 것들&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#workflow&quot;&gt;변경 요청 6단계 워크플로우&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#analysis&quot;&gt;Plan 결과 직접 분석해보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#extra&quot;&gt;추가로 알아야 할 개념들&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tools&quot;&gt;실무 도구 생태계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#summary&quot;&gt;정리 &amp;amp; 체크리스트&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;!-- ===== 1. WHY ===== --&gt;
&lt;section id=&quot;why&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 1&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Terraform을 써야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 스타트업의 월요일 아침, 팀장님이 이렇게 메시지를 보냈다고 상상해 보세요.&lt;/p&gt;
&lt;div class=&quot;callout info&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  실제 상황&lt;/div&gt;
&quot;스테이징 환경 그대로 프로덕션에 한 벌 더 만들어주세요. 이번 주 안에 가능하시죠?&quot;&lt;br /&gt;&amp;mdash; 스테이징은 6개월 전 누군가가 AWS 콘솔에서 클릭클릭 만든 환경. 문서는 없음.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC, Subnet, Security Group, RDS, EC2, ALB, IAM Role &amp;hellip; 하나씩 콘솔 열어서 비교하고 다시 클릭으로 만드는 것, 가능하긴 합니다. 그런데 그 과정에서 &lt;b&gt;누락이나 실수가 생기면?&lt;/b&gt; 다음에 또 같은 요청이 들어오면?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘솔 관리 vs IaC &amp;mdash; 무엇이 다른가&lt;/h3&gt;
&lt;div class=&quot;tbl-wrap&quot;&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;IaC (Terraform)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;핵심 질문&lt;/td&gt;
&lt;td&gt;지금 무엇을 만들 것인가?&lt;/td&gt;
&lt;td&gt;어떤 &lt;i&gt;상태&lt;/i&gt;를 유지할 것인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변경 이력&lt;/td&gt;
&lt;td&gt;❌ 남지 않음&lt;/td&gt;
&lt;td&gt;✅ Git에 기록됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;환경 복제&lt;/td&gt;
&lt;td&gt;수작업 반복&lt;/td&gt;
&lt;td&gt;동일 코드로 반복 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 리뷰&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;✅ PR 리뷰 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실수 회복&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;td&gt;코드 되돌리기로 복구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;환경 동기화&lt;/td&gt;
&lt;td&gt;dev/prod 설정이 서서히 어긋남&lt;/td&gt;
&lt;td&gt;같은 코드로 동일 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;callout success&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;✅ 한 줄 정의&lt;/div&gt;
&lt;b&gt;Terraform&lt;/b&gt;은 HashiCorp가 만든 IaC 도구로, 인프라를 HCL(HashiCorp Configuration Language) 코드로 선언하고, 현재 상태와 비교한 뒤, 변경을 안전하게 반영하는 도구입니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 IaC의 5가지 조건&lt;/h3&gt;
&lt;div class=&quot;tbl-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;왜 필요한가&lt;/th&gt;
&lt;th&gt;어떻게&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;선언적&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;어떤 상태가 되어야 하는지&quot;만 표현&lt;/td&gt;
&lt;td&gt;&lt;code&gt;resource&lt;/code&gt; 블록 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;재현 가능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;같은 코드 = 같은 결과&lt;/td&gt;
&lt;td&gt;Terraform 자체 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;검토 가능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;변경 전 영향 파악&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;협업 가능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;팀원과 동시 작업&lt;/td&gt;
&lt;td&gt;Remote Backend + Locking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;안전&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사고 시 복구 가능&lt;/td&gt;
&lt;td&gt;Git 이력 + 백업&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;!-- ===== 2. CORE ===== --&gt;
&lt;section id=&quot;core&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 2&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 개념 4가지&lt;/h2&gt;
&lt;div class=&quot;kw-grid&quot;&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Provider&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;Terraform과 외부 서비스(AWS, GCP 등)를 연결하는 플러그인&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Resource&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;만들거나 관리할 실제 대상 (EC2, S3, RDS 등)&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;State&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;코드와 실제 인프라를 매핑한 기록 파일 (.tfstate)&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Plan / Apply&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;변경 계획 미리보기(plan) &amp;rarr; 실제 반영(apply)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Provider &amp;mdash; Terraform과 AWS를 연결하는 다리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 자체는 AWS EC2가 뭔지 모릅니다. Provider가 각 클라우드 서비스의 리소스 타입과 API 호출 방법을 알고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# Provider 설정 &amp;mdash; 항상 버전을 고정하세요
terraform {
  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;~&amp;gt; 5.0&quot;  # 5.x 범위 내 최신 버전 사용
    }
  }
}

provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;  # 서울 리전
}main.tf&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout warn&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;⚠️ 주의&lt;/div&gt;
&lt;code&gt;version = &quot;~&amp;gt; 5.0&quot;&lt;/code&gt;은 &lt;b&gt;5.x 버전만 허용&lt;/b&gt;한다는 뜻입니다. 버전을 고정하지 않으면 팀원마다 다른 버전으로 실행해서 예기치 않은 동작이 발생할 수 있습니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Resource &amp;mdash; 관리할 대상 선언&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 형식: resource &quot;리소스_타입&quot; &quot;로컬_이름&quot; { ... }
resource &quot;aws_s3_bucket&quot; &quot;logs&quot; {
  bucket = &quot;my-app-logs-2026&quot;

  tags = {
    Environment = &quot;prod&quot;
    ManagedBy   = &quot;terraform&quot;
  }
}main.tf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;aws_s3_bucket&lt;/code&gt;은 리소스 타입, &lt;code&gt;logs&lt;/code&gt;는 이 코드 내에서 참조할 때 쓰는 이름입니다. 실제 AWS에는 &lt;code&gt;bucket&lt;/code&gt; 값인 &lt;code&gt;my-app-logs-2026&lt;/code&gt;로 만들어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;State &amp;mdash; Terraform의 기억&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State는 Terraform이 &lt;b&gt;&quot;내가 어떤 리소스를 만들었는가&quot;를 기록한 파일&lt;/b&gt;입니다. &lt;code&gt;terraform.tfstate&lt;/code&gt;라는 JSON 파일로 저장됩니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;// terraform.tfstate (자동 생성 &amp;mdash; 직접 수정 금지!)
{
  &quot;version&quot;: 4,
  &quot;resources&quot;: [
    {
      &quot;type&quot;: &quot;aws_s3_bucket&quot;,
      &quot;name&quot;: &quot;logs&quot;,
      &quot;instances&quot;: [
        {
          &quot;attributes&quot;: {
            &quot;bucket&quot;: &quot;my-app-logs-2026&quot;,
            &quot;arn&quot;:    &quot;arn:aws:s3:::my-app-logs-2026&quot;
          }
        }
      ]
    }
  ]
}terraform.tfstate&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout danger&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  절대 금지&lt;/div&gt;
&lt;code&gt;terraform.tfstate&lt;/code&gt;를 &lt;b&gt;절대 Git에 커밋하지 마세요&lt;/b&gt;. DB 비밀번호, IAM Secret Key 등 민감 정보가 &lt;b&gt;평문(plain text)&lt;/b&gt;으로 담겨 있습니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Plan / Apply &amp;mdash; 변경의 양면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;plan&lt;/code&gt;은 변경 예고편, &lt;code&gt;apply&lt;/code&gt;는 실제 반영입니다. 실무에서 &lt;b&gt;&lt;code&gt;plan&lt;/code&gt; 결과를 정확히 읽는 것이 apply보다 더 중요&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;+ aws_instance.web       # ✅ 신규 생성 &amp;rarr; 비용 발생
~ aws_s3_bucket.logs    #   속성 수정 (in-place)
- aws_eip.legacy        # ❌ 삭제 &amp;rarr; 데이터 손실 위험!
-/+ aws_db_instance.main #   삭제 후 재생성 (다운타임!)plan 기호 해석&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout danger&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  Force Replacement (-/+)&lt;/div&gt;
&lt;code&gt;-/+&lt;/code&gt;는 기존 리소스를 &lt;b&gt;삭제 후 새로 만든다&lt;/b&gt;는 뜻입니다. RDS의 &lt;code&gt;db_name&lt;/code&gt; 변경이 대표적인 예시 &amp;mdash; DB가 삭제되면 데이터도 함께 사라집니다. 반드시 스냅샷 백업 후 진행하세요.&lt;/div&gt;
&lt;/section&gt;
&lt;!-- ===== 3. COMMANDS ===== --&gt;
&lt;section id=&quot;commands&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 3&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 명령어와 동작 흐름&lt;/h2&gt;
&lt;div class=&quot;tbl-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;언제 실행?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업 디렉터리 초기화, Provider 다운로드&lt;/td&gt;
&lt;td&gt;프로젝트 시작 시, Provider 변경 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform fmt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;코드 포맷 자동 정리&lt;/td&gt;
&lt;td&gt;커밋 전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform validate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;문법 검증&lt;/td&gt;
&lt;td&gt;커밋 전, CI에서 자동 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;변경 계획 미리 보기&lt;/td&gt;
&lt;td&gt;apply 전 &lt;b&gt;항상&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 인프라 반영&lt;/td&gt;
&lt;td&gt;plan 검토 후&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform destroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;관리 중인 리소스 전체 삭제&lt;/td&gt;
&lt;td&gt;테스트 환경 정리 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;state에 등록된 리소스 목록&lt;/td&gt;
&lt;td&gt;state 확인 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state mv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리소스 이름 변경 시 state 동기화&lt;/td&gt;
&lt;td&gt;리소스 식별자 변경 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기존 리소스를 state로 가져옴&lt;/td&gt;
&lt;td&gt;콘솔 리소스를 IaC로 전환 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;output 값 조회&lt;/td&gt;
&lt;td&gt;apply 후 값 확인 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 동작 흐름&lt;/h3&gt;
&lt;div class=&quot;flow&quot;&gt;
&lt;div class=&quot;flow-item&quot;&gt;
&lt;div class=&quot;flow-box&quot;&gt;.tf 파일 작성&lt;/div&gt;
&lt;div class=&quot;flow-desc&quot;&gt;HCL로 원하는 인프라 상태를 선언&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;flow-arrow&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div class=&quot;flow-item&quot;&gt;
&lt;div class=&quot;flow-box hl&quot;&gt;terraform init&lt;/div&gt;
&lt;div class=&quot;flow-desc&quot;&gt;Provider 다운로드, .terraform 폴더 생성&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;flow-arrow&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div class=&quot;flow-item&quot;&gt;
&lt;div class=&quot;flow-box&quot;&gt;terraform fmt &amp;amp; validate&lt;/div&gt;
&lt;div class=&quot;flow-desc&quot;&gt;코드 스타일 정리 + 문법 검증&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;flow-arrow&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div class=&quot;flow-item&quot;&gt;
&lt;div class=&quot;flow-box hl&quot;&gt;terraform plan&lt;/div&gt;
&lt;div class=&quot;flow-desc&quot;&gt;현재 state와 코드를 비교 &amp;rarr; 변경 예고&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;flow-arrow&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div class=&quot;flow-item&quot;&gt;
&lt;div class=&quot;flow-box&quot;&gt;  plan 결과 검토&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;flow-desc&quot;&gt;삭제 항목, Force Replacement 여부 확인&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;flow-arrow&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div class=&quot;flow-item&quot;&gt;
&lt;div class=&quot;flow-box hl&quot;&gt;terraform apply&lt;/div&gt;
&lt;div class=&quot;flow-desc&quot;&gt;실제 클라우드 API 호출 &amp;rarr; 인프라 변경 + state 업데이트&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;flow-arrow&quot;&gt;&amp;darr;&lt;/div&gt;
&lt;div class=&quot;flow-item&quot;&gt;
&lt;div class=&quot;flow-box&quot;&gt;다시 plan &amp;rarr; &quot;No changes&quot;&lt;/div&gt;
&lt;div class=&quot;flow-desc&quot;&gt;코드와 실제가 일치함을 확인&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습: Random Provider로 흐름 체험하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 계정 없이도 Terraform 명령어 흐름을 연습할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;terraform {
  required_providers {
    random = {
      source  = &quot;hashicorp/random&quot;
      version = &quot;~&amp;gt; 3.0&quot;
    }
  }
}

resource &quot;random_id&quot; &quot;server&quot; {
  byte_length = 8
}

output &quot;server_id&quot; {
  value = random_id.server.hex
}main.tf&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shell&quot;&gt;&lt;code&gt;# 순서대로 실행
$ mkdir terraform-practice &amp;amp;&amp;amp; cd terraform-practice
$ terraform init
$ terraform plan
$ terraform apply
$ cat terraform.tfstate   # state 파일 확인
$ terraform destroyterminal&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;!-- ===== 4. STATE ===== --&gt;
&lt;section id=&quot;state&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 4&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;State와 Remote Backend&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;State가 꼬이면 벌어지는 일&lt;/h3&gt;
&lt;div class=&quot;tbl-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;state 파일 분실&lt;/td&gt;
&lt;td&gt;Terraform이 기존 리소스를 &quot;없는 것&quot;으로 인식 &amp;rarr; 중복 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;state와 실제 인프라 불일치&lt;/td&gt;
&lt;td&gt;plan 결과가 예상과 전혀 다르게 나옴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;두 명이 동시에 apply&lt;/td&gt;
&lt;td&gt;state 파일 손상 (race condition)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;state를 Git에 커밋&lt;/td&gt;
&lt;td&gt;DB 비밀번호 등이 GitHub에 평문 노출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Remote Backend &amp;mdash; 팀 협업의 필수 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 쓸 때는 로컬 state 파일로도 되지만, 팀이 함께 작업한다면 &lt;b&gt;Remote Backend&lt;/b&gt;는 필수입니다. 가장 흔한 조합은 &lt;b&gt;AWS S3 + DynamoDB&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;terraform {
  backend &quot;s3&quot; {
    bucket         = &quot;my-terraform-state&quot;
    key            = &quot;prod/terraform.tfstate&quot;
    region         = &quot;ap-northeast-2&quot;
    encrypt        = true                       # S3 저장 시 암호화
    dynamodb_table = &quot;terraform-state-lock&quot;     # 동시 실행 방지 (Locking)
  }
}backend.tf&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;State Locking &amp;mdash; 동시 실행 방지&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[10:00] 개발자 A: terraform apply 시작
        &amp;rarr; DynamoDB에 lock 획득

[10:01] 개발자 B: terraform apply 시도
        &amp;rarr; Error: Error acquiring the state lock
        &amp;rarr; 대기 또는 중단

[10:05] 개발자 A: apply 완료 &amp;rarr; lock 해제
[10:06] 개발자 B: 재시도 가능State Locking 흐름&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Drift &amp;mdash; 코드와 실제 인프라가 어긋날 때&lt;/h3&gt;
&lt;div class=&quot;callout warn&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;⚠️ Drift란?&lt;/div&gt;
Terraform으로 만든 리소스를 &lt;b&gt;콘솔에서 직접 수정&lt;/b&gt;하면 코드와 실제가 달라집니다. 다음 &lt;code&gt;plan&lt;/code&gt; 때 의도치 않은 변경이 나타납니다.&lt;/div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 시나리오
1. Terraform으로 EC2 생성 (코드: t3.medium)
2. 콘솔에서 t3.large로 수동 변경  &amp;larr; Drift 발생!
3. 다음 terraform plan 결과:
   ~ aws_instance.web
     ~ instance_type: &quot;t3.large&quot; &amp;rarr; &quot;t3.medium&quot;
4. 모르고 apply 하면 t3.medium으로 되돌려짐Drift 예시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원칙: Terraform으로 만든 리소스는 콘솔에서 수정하지 않는다.&lt;/b&gt; &lt;br /&gt;긴급 상황으로 콘솔 수정이 불가피했다면, 반드시 코드에도 동일하게 반영&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가: terraform import&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 콘솔로 만든 리소스를 Terraform 관리로 가져오고 싶을 때 씁니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 콘솔에서 만든 S3 버킷을 state로 가져오기
$ terraform import aws_s3_bucket.logs my-app-logs-2026

# 가져온 뒤에는 코드를 state에 맞게 작성해야 합니다
$ terraform plan  # &quot;No changes&quot;가 나올 때까지 코드를 맞춰가세요terminal&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout info&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;ℹ️ Terraform 1.5+&lt;/div&gt;
Terraform 1.5부터 &lt;code&gt;import&lt;/code&gt; 블록을 코드 안에 선언할 수 있습니다. 1.6부터는 &lt;code&gt;terraform plan&lt;/code&gt;이 자동으로 import 코드를 생성해 주는 기능이 추가됐습니다.&lt;/div&gt;
&lt;/section&gt;
&lt;!-- ===== 5. VARS ===== --&gt;
&lt;section id=&quot;vars&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변수 &amp;middot; 출력 &amp;middot; 모듈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;variable &amp;mdash; 환경별 값 분리&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# variables.tf
variable &quot;environment&quot; {
  type        = string
  description = &quot;배포 환경 (dev, staging, prod)&quot;
  validation {
    condition     = contains([&quot;dev&quot;, &quot;staging&quot;, &quot;prod&quot;], var.environment)
    error_message = &quot;environment는 dev, staging, prod 중 하나여야 합니다.&quot;
  }
}

variable &quot;db_instance_class&quot; {
  type    = string
  default = &quot;db.t3.micro&quot;
}

variable &quot;db_password&quot; {
  type      = string
  sensitive = true   # plan/apply 출력에서 마스킹
}variables.tf&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout info&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  validation 블록&lt;/div&gt;
Terraform 0.13부터 &lt;code&gt;validation&lt;/code&gt; 블록으로 변수 값을 검증할 수 있습니다. 잘못된 환경 이름이 들어오면 apply 전에 에러를 발생시켜 사고를 예방합니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tfvars &amp;mdash; 환경별 값 파일&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# dev.tfvars
environment       = &quot;dev&quot;
db_instance_class = &quot;db.t3.micro&quot;

# prod.tfvars
environment       = &quot;prod&quot;
db_instance_class = &quot;db.r6g.large&quot;*.tfvars&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;$ terraform apply -var-file=&quot;dev.tfvars&quot;
$ terraform apply -var-file=&quot;prod.tfvars&quot;terminal&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout danger&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  .gitignore 필수 등록&lt;/div&gt;
&lt;code&gt;*.tfvars&lt;/code&gt; 파일에 비밀번호 같은 값이 들어갈 수 있으므로, 반드시 &lt;code&gt;.gitignore&lt;/code&gt;에 추가하세요.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;output &amp;mdash; 생성된 리소스 정보 노출&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;output &quot;db_endpoint&quot; {
  value       = aws_db_instance.main.endpoint
  description = &quot;애플리케이션에서 사용할 DB 엔드포인트&quot;
}

output &quot;db_password&quot; {
  value     = aws_db_instance.main.password
  sensitive = true   # 화면 출력 시 &quot;sensitive&quot; 표시
}outputs.tf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sensitive = true&lt;/code&gt;로 설정해도 &lt;b&gt;state 파일에는 평문으로 저장&lt;/b&gt;됩니다. state 파일 자체를 암호화(S3 encrypt)하고 접근 권한을 제한하는 것이 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;module &amp;mdash; 재사용 단위&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;project/
├── main.tf
├── variables.tf
└── modules/
    └── web-server/
        ├── main.tf        # 실제 리소스 정의
        ├── variables.tf   # 입력 변수
        └── outputs.tf     # 출력 값디렉터리 구조&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# project/main.tf
module &quot;web_dev&quot; {
  source        = &quot;./modules/web-server&quot;
  environment   = &quot;dev&quot;
  instance_type = &quot;t3.micro&quot;
}

module &quot;web_prod&quot; {
  source        = &quot;./modules/web-server&quot;
  environment   = &quot;prod&quot;
  instance_type = &quot;t3.large&quot;
}main.tf&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout warn&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;⚠️ 모듈화 함정&lt;/div&gt;
처음부터 모든 것을 모듈로 쪼개면 안 됩니다. &lt;b&gt;같은 패턴이 2~3번 반복될 때&lt;/b&gt; 모듈로 분리하는 것이 안전합니다. 너무 이른 모듈화는 변수가 수십 개로 늘어나고 디버깅이 어려워집니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;locals &amp;mdash; 중간 계산 값 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 사용되는 표현식을 &lt;code&gt;locals&lt;/code&gt;로 정의하면 코드가 훨씬 깔끔해집니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = &quot;terraform&quot;
    Project     = &quot;my-app&quot;
  }
  name_prefix = &quot;${var.environment}-myapp&quot;
}

resource &quot;aws_instance&quot; &quot;web&quot; {
  ami           = &quot;ami-0abc12345&quot;
  instance_type = &quot;t3.medium&quot;
  tags          = local.common_tags  # 재사용
}locals 활용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;for_each / count &amp;mdash; 여러 리소스를 한 번에&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# count: 숫자로 반복
resource &quot;aws_instance&quot; &quot;web&quot; {
  count         = 3
  ami           = &quot;ami-0abc12345&quot;
  instance_type = &quot;t3.micro&quot;
  tags = { Name = &quot;web-${count.index}&quot; }
}

# for_each: 맵/셋으로 반복 (더 권장됨)
resource &quot;aws_s3_bucket&quot; &quot;buckets&quot; {
  for_each = toset([&quot;logs&quot;, &quot;assets&quot;, &quot;backups&quot;])
  bucket   = &quot;my-app-${each.key}&quot;
}for_each vs count&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout info&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  for_each를 권장하는 이유&lt;/div&gt;
&lt;code&gt;count&lt;/code&gt;는 중간 항목을 삭제하면 인덱스가 바뀌어 이후 리소스가 줄줄이 재생성됩니다. &lt;code&gt;for_each&lt;/code&gt;는 키 기반이라 다른 항목에 영향을 주지 않습니다.&lt;/div&gt;
&lt;/section&gt;
&lt;!-- ===== 6. SECURITY ===== --&gt;
&lt;section id=&quot;security&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 6&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;절대로 코드에 넣으면 안 되는 것들&lt;/h2&gt;
&lt;div class=&quot;callout danger&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  코드에 절대 넣지 말 것&lt;/div&gt;
AWS Access Key / Secret Key &amp;middot; RDS 비밀번호 &amp;middot; API Token &amp;middot; Private Key (.pem, .key) &amp;middot; JWT Secret &amp;middot; OAuth Client Secret &amp;middot; DB 접속 문자열 (계정 포함) &amp;middot; 개인정보&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 흔한 실수 &amp;mdash; Provider에 키를 직접 입력&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# ❌ 절대 이렇게 하면 안 됨
provider &quot;aws&quot; {
  region     = &quot;ap-northeast-2&quot;
  access_key = &quot;AKIAIOSFODNN7EXAMPLE&quot;   #  
  secret_key = &quot;wJalrXUtnFEMI/K7MDENGbPxRfiCYEXAMPLEKEY&quot;  #  
}절대 금지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub public 저장소에 push하면 &lt;b&gt;자동 스캐너&lt;/b&gt;가 수초 내에 키를 감지합니다. AWS 계정 탈취로 수백만 원의 요금 청구가 발생한 실제 사례가 많습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전한 자격 증명 방법&lt;/h3&gt;
&lt;div class=&quot;tbl-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;추천 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;환경 변수&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로컬 개발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자격 증명 파일&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.aws/credentials&lt;/code&gt; (aws configure)&lt;/td&gt;
&lt;td&gt;로컬 개발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM Role&lt;/td&gt;
&lt;td&gt;EC2/EKS에 Role 부여&lt;/td&gt;
&lt;td&gt;서버에서 실행 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS SSO&lt;/td&gt;
&lt;td&gt;임시 자격 증명 사용&lt;/td&gt;
&lt;td&gt;기업 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OIDC (GitHub Actions)&lt;/td&gt;
&lt;td&gt;키 없이 임시 토큰 발급&lt;/td&gt;
&lt;td&gt;CI/CD 파이프라인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# ✅ 올바른 방법 &amp;mdash; Provider에 키를 적지 않음
provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
  # 자격 증명은 환경 변수나 ~/.aws/credentials에서 자동으로 읽힘
}main.tf&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;민감 값 주입 방법&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 방법 1: 환경 변수로 주입 (로컬 작업 시)
$ export TF_VAR_db_password=&quot;MySecurePassword!&quot;
$ terraform apply

# 방법 2: AWS Secrets Manager에서 동적으로 읽기 (운영 환경 권장)
data &quot;aws_secretsmanager_secret_version&quot; &quot;db_pass&quot; {
  secret_id = &quot;prod/myapp/db_password&quot;
}

resource &quot;aws_db_instance&quot; &quot;main&quot; {
  username = &quot;admin&quot;
  password = data.aws_secretsmanager_secret_version.db_pass.secret_string
}민감 값 주입&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.gitignore 필수 항목&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# .gitignore
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
*.tfvars.json
crash.log
override.tf
override.tf.json
.terraform.lock.hcl    # 팀에 따라 커밋 여부 결정.gitignore&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;!-- ===== 7. WORKFLOW ===== --&gt;
&lt;section id=&quot;workflow&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 7&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변경 요청 6단계 워크플로우&lt;/h2&gt;
&lt;div class=&quot;steps&quot;&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;요구사항을 코드로 표현 가능한 형태인지 확인&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이번에만 임시로 콘솔에서 열어주세요&quot;도 Terraform 코드로 처리해야 합니다. 임시라도 콘솔 변경은 Drift를 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;적용 환경 확정 (dev &amp;rarr; staging &amp;rarr; prod 순서)&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prod에 바로 적용하지 않습니다. dev에서 먼저 검증하세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;terraform plan 결과 검토&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Add / Change / Destroy 수치가 의도와 일치하는지, Force Replacement가 있는지 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;삭제 항목이 의도된 것인지 재확인&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resource 이름 변경, 블록 삭제 등으로 의도치 않은 destroy가 발생할 수 있습니다. 이름 변경이 필요하면 &lt;code&gt;terraform state mv&lt;/code&gt;를 먼저 실행하세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;5&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;동료 리뷰 (코드 + plan 결과 함께)&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plan 결과를 PR에 붙여넣고, 보안 / 가독성 / 영향 범위를 함께 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;6&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;apply 후 실제 리소스 확인&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apply 직후 콘솔 또는 CLI로 리소스가 정상 생성됐는지, 서비스가 정상 응답하는지, 다시 plan했을 때 &quot;No changes&quot;가 뜨는지 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# plan 결과를 파일로 저장해서 동일한 내용을 apply
$ terraform plan -out=tfplan
$ terraform apply tfplan   # 검토한 그대로만 반영됨terminal&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;!-- ===== 8. ANALYSIS ===== --&gt;
&lt;section id=&quot;analysis&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 8&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Plan 결과 직접 분석해보기&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ terraform plan

Terraform will perform the following actions:

  # aws_security_group.web will be updated in-place
  ~ resource &quot;aws_security_group&quot; &quot;web&quot; {
      ~ ingress = [
          - {
              - cidr_blocks = [&quot;10.0.0.0/16&quot;]   # 내부 IP만 허용
              - from_port   = 22
              - protocol    = &quot;tcp&quot;
              - to_port     = 22
            },
          + {
              + cidr_blocks = [&quot;0.0.0.0/0&quot;]     # 전체 인터넷 허용!
              + from_port   = 22
              + protocol    = &quot;tcp&quot;
              + to_port     = 22
            },
        ]
    }

  # aws_db_instance.main must be replaced
  -/+ resource &quot;aws_db_instance&quot; &quot;main&quot; {
      ~ db_name = &quot;appdb&quot; &amp;rarr; &quot;app_db&quot;  # forces replacement
      ~ id      = &quot;db-abc123&quot; &amp;rarr; (known after apply)
    }

Plan: 1 to add, 1 to change, 1 to destroy.plan 결과&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;analysis&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  분석 결과&lt;/h4&gt;
&lt;div class=&quot;tbl-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질문&lt;/th&gt;
&lt;th&gt;답변&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;변경되는 리소스는?&lt;/td&gt;
&lt;td&gt;security group(수정) + db_instance(삭제 후 재생성) = 총 2개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SG 변경 내용&lt;/td&gt;
&lt;td&gt;SSH 허용 IP가 내부 VPC(&lt;code&gt;10.0.0.0/16&lt;/code&gt;)에서 &lt;span class=&quot;badge badge-red&quot;&gt;전체 인터넷(0.0.0.0/0)&lt;/span&gt;으로 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SG 변경이 안전한가?&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge badge-red&quot;&gt;위험!&lt;/span&gt; SSH 포트를 전 세계에 여는 건 심각한 보안 취약점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RDS 처리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;db_name&lt;/code&gt; 변경은 &lt;b&gt;삭제 후 재생성&lt;/b&gt;을 유발 (Force Replacement)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 영향&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge badge-red&quot;&gt;데이터 손실!&lt;/span&gt; 기존 DB 데이터가 함께 삭제됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;안전한 진행 방법&lt;/td&gt;
&lt;td&gt;① RDS 스냅샷 백업 먼저 ② SG 변경 의도 재확인 ③ db_name 변경 방법 재검토 (마이그레이션 방안 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;callout danger&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  이 plan은 apply하면 안 됩니다&lt;/div&gt;
SSH를 전 세계에 열고 + DB 데이터가 삭제됩니다. 즉시 코드 작성자에게 의도를 확인하고, RDS는 스냅샷 + 데이터 마이그레이션 계획을 먼저 수립해야 합니다.&lt;/div&gt;
&lt;/section&gt;
&lt;!-- ===== 9. EXTRA ===== --&gt;
&lt;section id=&quot;extra&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 9 &amp;mdash; 추가 학습&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가로 알아야 할 개념들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;data source &amp;mdash; 기존 리소스 참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform이 만들지 않은 기존 리소스(다른 팀이 만든 VPC 등)를 코드에서 참조할 때 씁니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 기존에 존재하는 VPC를 참조 (만드는 게 아님)
data &quot;aws_vpc&quot; &quot;existing&quot; {
  filter {
    name   = &quot;tag:Name&quot;
    values = [&quot;prod-vpc&quot;]
  }
}

resource &quot;aws_subnet&quot; &quot;new&quot; {
  vpc_id     = data.aws_vpc.existing.id  # 참조!
  cidr_block = &quot;10.0.100.0/24&quot;
}data source&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;depends_on &amp;mdash; 명시적 의존성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 리소스 간 참조를 분석해서 자동으로 순서를 결정합니다. 하지만 참조 관계가 없는데도 순서가 필요할 때는 &lt;code&gt;depends_on&lt;/code&gt;을 명시합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role_policy_attachment&quot; &quot;attach&quot; {
  role       = aws_iam_role.example.name
  policy_arn = aws_iam_policy.example.arn

  depends_on = [aws_iam_role.example]  # 명시적 의존성
}depends_on&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;lifecycle &amp;mdash; 리소스 생명주기 제어&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;web&quot; {
  ami           = &quot;ami-0abc12345&quot;
  instance_type = &quot;t3.medium&quot;

  lifecycle {
    create_before_destroy = true   # 삭제 전 새 것을 먼저 만듦 (무중단)
    prevent_destroy       = true   # 실수로 destroy 막기 (prod DB에 유용)
    ignore_changes        = [ami]  # AMI 변경은 Terraform이 무시
  }
}lifecycle 블록&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout success&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;✅ 실무 팁&lt;/div&gt;
프로덕션 RDS나 중요 S3 버킷에는 &lt;code&gt;prevent_destroy = true&lt;/code&gt;를 반드시 붙이세요. 실수로 &lt;code&gt;terraform destroy&lt;/code&gt;를 실행해도 에러가 나면서 막아줍니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dynamic 블록 &amp;mdash; 반복 속성&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;ingress_rules&quot; {
  default = [
    { port = 80,  cidr = &quot;0.0.0.0/0&quot; },
    { port = 443, cidr = &quot;0.0.0.0/0&quot; },
  ]
}

resource &quot;aws_security_group&quot; &quot;web&quot; {
  name = &quot;web-sg&quot;

  dynamic &quot;ingress&quot; {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = &quot;tcp&quot;
      cidr_blocks = [ingress.value.cidr]
    }
  }
}dynamic 블록&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;terraform.lock.hcl &amp;mdash; Provider 버전 고정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;terraform init&lt;/code&gt; 후 생성되는 파일입니다. Provider의 정확한 버전과 체크섬을 기록해서 팀원 모두가 동일한 버전을 사용하도록 보장합니다. &lt;b&gt;반드시 Git에 커밋&lt;/b&gt;하세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;moved 블록 &amp;mdash; 리소스 이름 변경 시&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# Terraform 1.1+ &amp;mdash; state mv 없이 코드로 이름 변경
moved {
  from = aws_instance.old_name
  to   = aws_instance.new_name
}moved 블록&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 &lt;code&gt;terraform state mv&lt;/code&gt; CLI를 써야 했지만, &lt;code&gt;moved&lt;/code&gt; 블록으로 코드에서 선언적으로 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;workspaces &amp;mdash; 하나의 코드로 여러 환경&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;$ terraform workspace new dev
$ terraform workspace new prod
$ terraform workspace select prod
$ terraform apply    # prod workspace에 반영workspaces&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout warn&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;⚠️ workspace 주의사항&lt;/div&gt;
workspace는 state만 분리합니다. 코드는 공유되므로 환경별로 큰 차이가 있는 경우에는 &lt;b&gt;디렉터리를 분리하는 방식(envs/dev, envs/prod)을 더 권장&lt;/b&gt;합니다.&lt;/div&gt;
&lt;/section&gt;
&lt;!-- ===== 10. TOOLS ===== --&gt;
&lt;section id=&quot;tools&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 10&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 도구 생태계&lt;/h2&gt;
&lt;div class=&quot;tbl-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;코드 작성&lt;/td&gt;
&lt;td&gt;Terraform / OpenTofu&lt;/td&gt;
&lt;td&gt;OpenTofu는 완전 오픈소스 포크 (Linux Foundation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 검사&lt;/td&gt;
&lt;td&gt;tflint&lt;/td&gt;
&lt;td&gt;문법 오류, 잘못된 인스턴스 타입 탐지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안 검사&lt;/td&gt;
&lt;td&gt;tfsec / Checkov&lt;/td&gt;
&lt;td&gt;공개 S3, 평문 시크릿 등 보안 취약점 탐지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용 예측&lt;/td&gt;
&lt;td&gt;Infracost&lt;/td&gt;
&lt;td&gt;plan 결과로 월 예상 비용 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 자동화&lt;/td&gt;
&lt;td&gt;Atlantis&lt;/td&gt;
&lt;td&gt;오픈소스, GitHub/GitLab PR에서 plan/apply 자동화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 자동화&lt;/td&gt;
&lt;td&gt;Terraform Cloud (HCP)&lt;/td&gt;
&lt;td&gt;HashiCorp SaaS, 팀 플랜 이상에서 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모듈 반복 제거&lt;/td&gt;
&lt;td&gt;Terragrunt&lt;/td&gt;
&lt;td&gt;backend 설정, variable 주입 반복 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State 저장&lt;/td&gt;
&lt;td&gt;S3 + DynamoDB&lt;/td&gt;
&lt;td&gt;AWS 환경에서 가장 흔한 조합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 자동화 파이프라인&lt;/h3&gt;
&lt;div class=&quot;steps&quot;&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;.tf 코드 수정 후 PR 생성&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 로컬에서 작업 후 GitHub/GitLab에 PR을 올립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;CI에서 자동으로 terraform plan 실행&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tflint, tfsec 검사도 함께 실행합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;plan 결과를 PR 댓글로 게시&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Atlantis 또는 GitHub Actions이 자동으로 붙입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;동료가 plan 결과 리뷰 후 승인&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;5&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;PR 머지 시 자동으로 terraform apply&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 로컬에서 직접 apply를 실행하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;6&lt;/div&gt;
&lt;div class=&quot;step-body&quot;&gt;&lt;b&gt;결과를 Slack 등 알림 채널에 게시&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;!-- ===== 11. SUMMARY ===== --&gt;
&lt;section id=&quot;summary&quot;&gt;
&lt;div class=&quot;section-label&quot;&gt;Part 11&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리 &amp;amp; 체크리스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 키워드 한 눈에&lt;/h3&gt;
&lt;div class=&quot;kw-grid&quot;&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Provider&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;외부 서비스와 연결하는 플러그인&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Resource&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;만들고 관리할 대상 (EC2, RDS, S3 &amp;hellip;)&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;State&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;코드&amp;harr;실제 인프라 매핑 기록&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Plan&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;변경 계획 미리 보기&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Apply&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;실제 반영&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Backend&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;state 저장 방식 (S3 등)&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Module&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;재사용 단위&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Variable&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;환경별 값을 주입하는 입력&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Output&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;생성된 리소스 정보 노출&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;Drift&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;코드&amp;harr;실제 인프라 불일치 상태&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;locals&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;중간 계산 값 재사용&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;kw-card&quot;&gt;
&lt;div class=&quot;kw-card-name&quot;&gt;data&lt;/div&gt;
&lt;div class=&quot;kw-card-desc&quot;&gt;기존 리소스 참조 (읽기 전용)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 적용 체크리스트&lt;/h3&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;&lt;code&gt;terraform.tfstate&lt;/code&gt;를 Git에 커밋하지 않는다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;Secret을 코드에 직접 적지 않는다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;변경 전 항상 &lt;code&gt;terraform plan&lt;/code&gt;을 확인한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;Destroy 항목이 의도된 것인지 점검한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;Remote Backend를 사용한다 (협업 시 필수)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;콘솔에서 수동 변경을 하지 않는다 (Drift 방지)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;동료 리뷰 후 apply한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;Provider 버전을 고정하고 &lt;code&gt;.terraform.lock.hcl&lt;/code&gt;을 커밋한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;중요 리소스에 &lt;code&gt;prevent_destroy = true&lt;/code&gt;를 붙인다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;ck&quot;&gt;☐&lt;/span&gt; &lt;span&gt;모든 리소스에 &lt;code&gt;ManagedBy = &quot;terraform&quot;&lt;/code&gt; 태그를 붙인다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다음 학습 방향&lt;/h3&gt;
&lt;div class=&quot;tbl-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;학습 주제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terraform 심화&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dynamic&lt;/code&gt; 블록, &lt;code&gt;for_each&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;locals&lt;/code&gt;, &lt;code&gt;moved&lt;/code&gt; 블록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State 운영&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform import&lt;/code&gt;, &lt;code&gt;state mv&lt;/code&gt;, &lt;code&gt;state rm&lt;/code&gt;, &lt;code&gt;state pull/push&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모듈 설계&lt;/td&gt;
&lt;td&gt;Terraform Registry 공개 모듈 분석, 모듈 버전 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안 강화&lt;/td&gt;
&lt;td&gt;AWS Secrets Manager 연동, IRSA, OIDC (키 없는 CI/CD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자동화&lt;/td&gt;
&lt;td&gt;Atlantis, GitHub Actions, Terragrunt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분석 도구&lt;/td&gt;
&lt;td&gt;tflint, tfsec, Checkov, Infracost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티 클라우드&lt;/td&gt;
&lt;td&gt;GCP, Azure Provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes 연동&lt;/td&gt;
&lt;td&gt;Helm Provider, Kubernetes Provider&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;callout success&quot;&gt;
&lt;div class=&quot;callout-icon&quot;&gt;  마무리 한 줄 요약&lt;/div&gt;
&lt;b&gt;코드는 Git에, 실행은 자동화에, State는 안전한 Backend에.&lt;/b&gt;&lt;br /&gt;이 세 가지를 지키면 인프라 관리의 90%는 해결됩니다.&lt;/div&gt;
&lt;/section&gt;
&lt;/div&gt;</description>
      <category>MSA</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/54</guid>
      <comments>https://0321ljh.tistory.com/54#entry54comment</comments>
      <pubDate>Fri, 15 May 2026 14:56:59 +0900</pubDate>
    </item>
    <item>
      <title>로깅(Observability) 완전 정리장애가 나면 어디부터 봐야 할까?</title>
      <link>https://0321ljh.tistory.com/53</link>
      <description>&lt;div&gt;
&lt;style&gt;
:root {
  --bg: #f0f4ff;
  --surface: #ffffff;
  --surface-2: #f8faff;
  --text: #0f172a;
  --text-2: #334155;
  --muted: #64748b;
  --line: #e2e8f0;
  --blue: #2563eb; --blue-l: #eff6ff; --blue-m: #bfdbfe;
  --teal: #0d9488; --teal-l: #f0fdfa; --teal-m: #99f6e4;
  --violet: #7c3aed; --violet-l: #f5f3ff; --violet-m: #ddd6fe;
  --amber: #d97706; --amber-l: #fffbeb; --amber-m: #fde68a;
  --rose: #e11d48; --rose-l: #fff1f2; --rose-m: #fecdd3;
  --lime: #65a30d; --lime-l: #f7fee7; --lime-m: #d9f99d;
  --sky: #0284c7; --sky-l: #f0f9ff; --sky-m: #bae6fd;
  --shadow-sm: 0 1px 3px rgba(15,23,42,.06), 0 4px 12px rgba(15,23,42,.05);
  --shadow-md: 0 4px 20px rgba(15,23,42,.08), 0 1px 4px rgba(15,23,42,.04);
  --shadow-lg: 0 12px 40px rgba(15,23,42,.10), 0 2px 8px rgba(15,23,42,.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
  line-height: 1.8;
  word-break: keep-all;
}
.wrap { max-width: 1080px; margin: 0 auto; padding: 36px 20px 100px; }
.hero {
  position: relative;
  background: linear-gradient(135deg, #e0f2fe 0%, #e9d5ff 50%, #d1fae5 100%);
  border: 1px solid #c7d7f5;
  border-radius: 28px;
  padding: 52px 40px 44px;
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 80% 20%, rgba(124,58,237,.10) 0%, transparent 60%),
              radial-gradient(circle at 10% 80%, rgba(13,148,136,.10) 0%, transparent 50%);
  pointer-events: none;
}
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 16px;
  border-radius: 999px;
  background: rgba(37,99,235,.12);
  border: 1.5px solid rgba(37,99,235,.22);
  font-size: 12px;
  font-weight: 800;
  color: var(--blue);
  letter-spacing: .04em;
  margin-bottom: 20px;
}
.hero h1 {
  font-size: clamp(28px, 5vw, 44px);
  font-weight: 900;
  line-height: 1.25;
  letter-spacing: -0.03em;
  color: #0f172a;
  margin-bottom: 16px;
}
.hero h1 span { color: var(--blue); }
.hero-desc { font-size: 16px; color: #334155; max-width: 680px; line-height: 1.85; }
.hero-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; }
.hero-tags span {
  padding: 6px 14px;
  border-radius: 999px;
  background: #fff;
  border: 1.5px solid #dbeafe;
  font-size: 12px;
  font-weight: 700;
  color: #2563eb;
}
.toc {
  margin-top: 24px;
  background: #fff;
  border: 1.5px solid var(--line);
  border-radius: 22px;
  padding: 26px 28px;
  box-shadow: var(--shadow-sm);
}
.toc-title {
  font-size: 15px;
  font-weight: 800;
  color: var(--muted);
  letter-spacing: .06em;
  text-transform: uppercase;
  margin-bottom: 14px;
}
.toc ul {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 8px;
}
.toc a {
  display: block;
  text-decoration: none;
  color: var(--text-2);
  background: var(--surface-2);
  border: 1.5px solid #e8f0fe;
  border-radius: 12px;
  padding: 11px 14px;
  font-size: 14px;
  font-weight: 600;
  transition: all .18s ease;
}
.toc a:hover {
  border-color: #93c5fd;
  background: #eff6ff;
  color: var(--blue);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(37,99,235,.10);
}
section {
  margin-top: 22px;
  background: var(--surface);
  border: 1.5px solid var(--line);
  border-radius: 26px;
  padding: 38px 34px;
  box-shadow: var(--shadow-md);
}
.section-label {
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 5px 12px;
  border-radius: 999px;
  margin-bottom: 12px;
}
.lbl-blue   { background: var(--blue-l);   color: var(--blue);   border: 1.5px solid var(--blue-m); }
.lbl-teal   { background: var(--teal-l);   color: var(--teal);   border: 1.5px solid var(--teal-m); }
.lbl-violet { background: var(--violet-l); color: var(--violet); border: 1.5px solid var(--violet-m); }
.lbl-amber  { background: var(--amber-l);  color: var(--amber);  border: 1.5px solid var(--amber-m); }
.lbl-rose   { background: var(--rose-l);   color: var(--rose);   border: 1.5px solid var(--rose-m); }
.lbl-lime   { background: var(--lime-l);   color: var(--lime);   border: 1.5px solid var(--lime-m); }
.lbl-sky    { background: var(--sky-l);    color: var(--sky);    border: 1.5px solid var(--sky-m); }
section h2 {
  font-size: 26px;
  font-weight: 900;
  letter-spacing: -0.025em;
  line-height: 1.3;
  color: var(--text);
  margin-bottom: 16px;
}
section h3 {
  font-size: 19px;
  font-weight: 800;
  color: var(--text);
  margin: 30px 0 12px;
}
section p { color: var(--text-2); font-size: 15.5px; line-height: 1.85; margin-bottom: 14px; }
.quote {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  margin: 20px 0;
  padding: 18px 22px;
  background: linear-gradient(135deg, #eff6ff, #f5f3ff);
  border: 1.5px solid #bfdbfe;
  border-radius: 16px;
  font-weight: 700;
  font-size: 15.5px;
  color: #1e3a8a;
}
.quote::before { content: '&quot;'; font-size: 28px; line-height: 1; color: #93c5fd; flex-shrink: 0; }
.callout {
  margin: 18px 0;
  padding: 16px 20px;
  border-radius: 14px;
  background: #fffbeb;
  border: 1.5px solid #fde68a;
  color: #78350f;
  font-size: 14.5px;
}
.callout strong { color: #92400e; }
.grid-2, .grid-3 { display: grid; gap: 14px; margin-top: 18px; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.card { border-radius: 18px; padding: 22px; border: 1.5px solid var(--line); }
.card-blue   { background: var(--blue-l);   border-color: var(--blue-m); }
.card-teal   { background: var(--teal-l);   border-color: var(--teal-m); }
.card-violet { background: var(--violet-l); border-color: var(--violet-m); }
.card-amber  { background: var(--amber-l);  border-color: var(--amber-m); }
.card-rose   { background: var(--rose-l);   border-color: var(--rose-m); }
.card-lime   { background: var(--lime-l);   border-color: var(--lime-m); }
.card-sky    { background: var(--sky-l);    border-color: var(--sky-m); }
.card h4 { font-size: 17px; font-weight: 800; color: var(--text); margin: 10px 0 8px; }
.card p { font-size: 14.5px; margin-bottom: 0; }
.badge { display: inline-block; padding: 5px 12px; border-radius: 999px; font-size: 11.5px; font-weight: 800; letter-spacing: .04em; }
.bg-blue   { background: var(--blue);   color: #fff; }
.bg-teal   { background: var(--teal);   color: #fff; }
.bg-violet { background: var(--violet); color: #fff; }
.bg-amber  { background: var(--amber);  color: #fff; }
.bg-rose   { background: var(--rose);   color: #fff; }
.bg-lime   { background: var(--lime);   color: #fff; }
.bg-sky    { background: var(--sky);    color: #fff; }
.checklist, .mini-list { list-style: none; padding: 0; margin: 10px 0 0; }
.checklist li {
  position: relative;
  padding: 6px 0 6px 28px;
  font-size: 14.5px;
  color: var(--text-2);
  border-bottom: 1px solid rgba(0,0,0,.05);
}
.checklist li:last-child { border-bottom: none; }
.checklist li::before { content: '✓'; position: absolute; left: 2px; color: var(--teal); font-weight: 900; font-size: 15px; }
.mini-list li { position: relative; padding: 5px 0 5px 22px; font-size: 14px; color: var(--text-2); }
.mini-list li::before { content: '—'; position: absolute; left: 0; color: var(--muted); font-weight: 700; }
.table-wrap { border-radius: 18px; border: 1.5px solid var(--line); overflow: hidden; margin-top: 18px; box-shadow: var(--shadow-sm); }
table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
thead tr { background: linear-gradient(90deg, #f0f4ff, #f5f3ff); }
th { padding: 14px 16px; text-align: left; font-weight: 800; font-size: 13px; color: var(--text); border-bottom: 2px solid var(--line); }
td { padding: 13px 16px; border-bottom: 1px solid #f1f5f9; color: var(--text-2); vertical-align: top; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: #fafbff; }
td strong { color: var(--text); font-weight: 700; }
.compare { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 18px; }
.cmp-box { border-radius: 18px; padding: 22px; border: 1.5px solid; }
.cmp-bad  { background: var(--rose-l);  border-color: var(--rose-m); }
.cmp-good { background: var(--teal-l);  border-color: var(--teal-m); }
.cmp-title { font-size: 16px; font-weight: 800; margin-bottom: 12px; }
.cmp-bad  .cmp-title { color: var(--rose); }
.cmp-good .cmp-title { color: var(--teal); }
pre {
  margin: 16px 0;
  padding: 20px 22px;
  border-radius: 16px;
  background: #0f172a;
  color: #e2e8f0;
  font-size: 13.5px;
  line-height: 1.7;
  overflow-x: auto;
}
code { font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; }
.steps { display: flex; flex-direction: column; gap: 10px; margin-top: 18px; }
.step {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  background: #f8faff;
  border: 1.5px solid #e0e7ff;
  border-radius: 18px;
  padding: 18px 20px;
}
.step-num {
  flex: 0 0 40px;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: linear-gradient(135deg, #3b82f6, #6366f1);
  color: #fff;
  font-weight: 900;
  font-size: 17px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 14px rgba(99,102,241,.30);
  flex-shrink: 0;
}
.step h4 { font-size: 16px; font-weight: 800; color: var(--text); margin-bottom: 5px; }
.step p { font-size: 14.5px; margin: 0; }
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 16px; }
.summary-item { background: var(--surface-2); border: 1.5px solid var(--line); border-radius: 16px; padding: 18px; }
.summary-item strong { display: block; font-size: 13px; font-weight: 800; color: var(--text); margin-bottom: 6px; }
.summary-item span { font-size: 14px; color: var(--text-2); }
.footer-box {
  margin-top: 20px;
  padding: 26px;
  border-radius: 20px;
  background: linear-gradient(135deg, #eff6ff 0%, #f5f3ff 100%);
  border: 1.5px solid #c7d2fe;
}
.footer-box h3 { margin-top: 0; color: var(--blue); }
.footer-box p { color: #1e3a8a; }
.pill-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.pill { padding: 8px 14px; border-radius: 999px; background: #f0f4ff; border: 1.5px solid #c7d7f5; color: #2563eb; font-size: 13px; font-weight: 700; }
.divider { height: 1.5px; background: linear-gradient(90deg, transparent, var(--line), transparent); margin: 24px 0; }
@media (max-width: 720px) {
  .wrap { padding: 16px 14px 60px; }
  .hero { padding: 32px 22px; border-radius: 22px; }
  section { padding: 26px 18px; border-radius: 20px; }
  .compare { grid-template-columns: 1fr; }
  pre { font-size: 12.5px; padding: 16px; }
}
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;wrap&quot;&gt;&lt;header class=&quot;hero&quot;&gt;
&lt;div class=&quot;eyebrow&quot;&gt;Backend &amp;middot; Observability &amp;middot; Logging&lt;/div&gt;
&lt;h1&gt;로깅(Observability) 완전 정리&lt;br /&gt;&lt;span&gt;장애가 나면 어디부터 봐야 할까?&lt;/span&gt;&lt;/h1&gt;
&lt;p class=&quot;hero-desc&quot; data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 디버거를 붙일 수 없습니다. 결국 우리에게 남는 건 &lt;b&gt;로그, 메트릭, 트레이스&lt;/b&gt;입니다. 왜 로깅이 필요한지, 언제 무엇을 봐야 하는지, 실무에서 어떻게 써야 하는지를 백엔드 개발자 관점에서 한 번에 정리합니다.&lt;/p&gt;
&lt;div class=&quot;hero-tags&quot;&gt;&lt;span&gt;#Observability&lt;/span&gt;&lt;span&gt;#Logging&lt;/span&gt;&lt;span&gt;#Metrics&lt;/span&gt;&lt;span&gt;#TraceId&lt;/span&gt;&lt;span&gt;#SpringBoot&lt;/span&gt;&lt;span&gt;#MSA&lt;/span&gt;&lt;/div&gt;
&lt;/header&gt;&lt;nav class=&quot;toc&quot;&gt;
&lt;div class=&quot;toc-title&quot;&gt;  목차&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#intro&quot;&gt;1. 왜 Observability가 필요한가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#monitoring&quot;&gt;2. Monitoring vs Observability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mlt&quot;&gt;3. Metrics / Logs / Traces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#logging&quot;&gt;4. 좋은 로그는 어떻게 남기나&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#incident&quot;&gt;5. 장애가 났을 때 보는 순서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#traceid&quot;&gt;6. MSA와 Trace ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mdc&quot;&gt;7. Spring Boot MDC 적용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#security&quot;&gt;8. 절대 로그에 남기면 안 되는 정보&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tools&quot;&gt;9. 실무 도구와 조합&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#checklist&quot;&gt;10. 실무 체크리스트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;section id=&quot;intro&quot;&gt;
&lt;div class=&quot;section-label lbl-blue&quot;&gt;Section 01&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Observability가 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금요일 오후 6시, 퇴근 직전 슬랙에 메시지가 옵니다.&lt;/p&gt;
&lt;div class=&quot;quote&quot;&gt;고객센터에서 결제가 안 된다고 문의가 들어왔어요. 확인 부탁드려요.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 무작정 코드를 뒤지는 것부터 시작하면 시간이 오래 걸립니다. 운영 환경에서는 브레이크포인트를 걸고 한 줄씩 따라갈 수 없기 때문입니다. 대신 우리는 &lt;b&gt;시스템이 남긴 흔적&lt;/b&gt;을 따라가야 합니다.&lt;/p&gt;
&lt;div class=&quot;grid-3&quot;&gt;
&lt;div class=&quot;card card-blue&quot;&gt;&lt;span class=&quot;badge bg-blue&quot;&gt;LOGS&lt;/span&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;무슨 일이 있었는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지, 주문 번호, 사용자 ID, 처리 시간처럼 사건의 맥락을 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-teal&quot;&gt;&lt;span class=&quot;badge bg-teal&quot;&gt;METRICS&lt;/span&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지금 이상한가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러율, 응답시간, 트래픽, CPU처럼 숫자로 장애 징후를 빠르게 감지합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-violet&quot;&gt;&lt;span class=&quot;badge bg-violet&quot;&gt;TRACES&lt;/span&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어디서 느렸는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 요청이 어떤 서비스와 구간을 지나며 어디서 병목이 났는지 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin-top: 18px;&quot; data-ke-size=&quot;size16&quot;&gt;결국 Observability의 핵심은 단순합니다. &lt;b&gt;어디까지 성공했고, 어디서 실패했는지 빠르게 좁히는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;monitoring&quot;&gt;
&lt;div class=&quot;section-label lbl-teal&quot;&gt;Section 02&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Monitoring vs Observability&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서도 자주 나오는 질문이죠. 많은 분들이 둘을 비슷하게 생각하지만, 실무에서는 분명한 차이가 있습니다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&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;Monitoring&lt;/th&gt;
&lt;th&gt;Observability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;핵심 질문&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;지금 문제가 있는가?&lt;/td&gt;
&lt;td&gt;왜 문제가 생겼는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주요 데이터&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메트릭, 알람&lt;/td&gt;
&lt;td&gt;메트릭, 로그, 트레이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;대표 예시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;에러율 5% 초과 알람&lt;/td&gt;
&lt;td&gt;PG timeout이 어디서 났는지 추적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;감지&lt;/td&gt;
&lt;td&gt;원인 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;compare&quot; style=&quot;margin-top: 18px;&quot;&gt;
&lt;div class=&quot;cmp-box cmp-bad&quot;&gt;
&lt;p class=&quot;cmp-title&quot; data-ke-size=&quot;size16&quot;&gt;  Monitoring&lt;/p&gt;
&lt;p style=&quot;font-size: 14.5px; color: #4c0519;&quot; data-ke-size=&quot;size16&quot;&gt;자동차 계기판의 경고등과 비슷합니다. &quot;문제가 있다&quot;는 사실은 알려주지만, &lt;b&gt;왜 생겼는지&lt;/b&gt;는 알려주지 못합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;cmp-box cmp-good&quot;&gt;
&lt;p class=&quot;cmp-title&quot; data-ke-size=&quot;size16&quot;&gt;  Observability&lt;/p&gt;
&lt;p style=&quot;font-size: 14.5px; color: #134e4a;&quot; data-ke-size=&quot;size16&quot;&gt;정비사가 엔진, 팬, 오일 상태를 보며 원인을 좁혀가는 과정과 비슷합니다. &lt;b&gt;문제의 내부 상태를 이해할 수 있게 해주는 능력&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;mlt&quot;&gt;
&lt;div class=&quot;section-label lbl-violet&quot;&gt;Section 03&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Metrics / Logs / Traces는 언제 써야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 세 가지를 경쟁 관계로 보지 않습니다. 각자 답하는 질문이 다르기 때문에 &lt;b&gt;함께 써야&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&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;th&gt;언제 가장 유용한가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Metrics&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;숫자&lt;/td&gt;
&lt;td&gt;얼마나 느린가? 얼마나 실패하는가?&lt;/td&gt;
&lt;td&gt;장애 징후를 가장 먼저 볼 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Logs&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사건 기록&lt;/td&gt;
&lt;td&gt;그 순간 무슨 일이 있었나?&lt;/td&gt;
&lt;td&gt;실패 요청의 맥락과 에러 원인을 확인할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Traces&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;요청 경로&lt;/td&gt;
&lt;td&gt;어느 구간에서 오래 걸렸나?&lt;/td&gt;
&lt;td&gt;MSA, 외부 API 호출, 병목 구간 분석 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;grid-3&quot;&gt;
&lt;div class=&quot;card card-teal&quot;&gt;&lt;span class=&quot;badge bg-teal&quot;&gt;Metrics를 볼 때&lt;/span&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러율이 갑자기 튀었는지 보고 싶을 때&lt;/li&gt;
&lt;li&gt;응답시간이 평소보다 느려졌는지 확인할 때&lt;/li&gt;
&lt;li&gt;CPU, 메모리, DB 커넥션이 포화 상태인지 볼 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-blue&quot;&gt;&lt;span class=&quot;badge bg-blue&quot;&gt;Logs를 볼 때&lt;/span&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 주문, 어떤 사용자에서 실패했는지 확인할 때&lt;/li&gt;
&lt;li&gt;실제 에러 메시지와 에러 코드를 보고 싶을 때&lt;/li&gt;
&lt;li&gt;스택 트레이스로 코드 레벨 원인을 좁힐 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-violet&quot;&gt;&lt;span class=&quot;badge bg-violet&quot;&gt;Traces를 볼 때&lt;/span&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Order &amp;rarr; Payment &amp;rarr; PG 중 어디가 느린지 알고 싶을 때&lt;/li&gt;
&lt;li&gt;MSA에서 서비스 간 호출 흐름을 한눈에 보고 싶을 때&lt;/li&gt;
&lt;li&gt;외부 API, Kafka, 비동기 구간 병목을 분석할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애 분석은 위에서 아래로 내려갑니다&lt;/h3&gt;
&lt;div class=&quot;quote&quot;&gt;Metrics로 이상 징후 감지 &amp;rarr; Trace로 느린 구간 확인 &amp;rarr; Logs로 정확한 원인 분석&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;평균보다 p95, p99를 보는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 응답시간은 종종 거짓말을 합니다. 95명은 200ms에 응답받고, 5명은 10초를 기다려도 평균은 &quot;그럭저럭&quot;처럼 보일 수 있습니다. 그래서 실무에서는 &lt;b&gt;p95, p99 같은 백분위수&lt;/b&gt;를 더 중요하게 봅니다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;실무 해석&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;p50&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;50% 사용자가 이 안에 응답받음&lt;/td&gt;
&lt;td&gt;보통 사용자 경험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;p95&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;95% 사용자가 이 안에 응답받음&lt;/td&gt;
&lt;td&gt;느린 사용자 5%의 경계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;p99&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;99% 사용자가 이 안에 응답받음&lt;/td&gt;
&lt;td&gt;최악에 가까운 사용자 경험&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;logging&quot;&gt;
&lt;div class=&quot;section-label lbl-sky&quot;&gt;Section 04&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 로그는 어떻게 남겨야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 많이 찍는 게 핵심이 아닙니다. &lt;b&gt;검색 가능하게&lt;/b&gt;, &lt;b&gt;맥락 있게&lt;/b&gt;, &lt;b&gt;원인을 좁힐 수 있게&lt;/b&gt; 남겨야 합니다.&lt;/p&gt;
&lt;div class=&quot;compare&quot;&gt;
&lt;div class=&quot;cmp-box cmp-bad&quot;&gt;
&lt;p class=&quot;cmp-title&quot; data-ke-size=&quot;size16&quot;&gt;❌ 나쁜 로그&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;try {
  payment.process(order);
} catch (Exception e) {
  log.error(&quot;결제 실패&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul class=&quot;mini-list&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 주문인지 모릅니다&lt;/li&gt;
&lt;li&gt;어떤 사용자인지 모릅니다&lt;/li&gt;
&lt;li&gt;왜 실패했는지 모릅니다&lt;/li&gt;
&lt;li&gt;얼마나 걸렸는지 모릅니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;cmp-box cmp-good&quot;&gt;
&lt;p class=&quot;cmp-title&quot; data-ke-size=&quot;size16&quot;&gt;✅ 좋은 로그&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;try {
  payment.process(order);
} catch (PaymentException e) {
  log.error(
    &quot;Payment failed. orderId={}, userId={}, errorCode={}, elapsedMs={}&quot;,
    order.getId(), order.getUserId(),
    e.getErrorCode(), elapsed, e
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul class=&quot;mini-list&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업무 맥락(orderId, userId)이 있습니다&lt;/li&gt;
&lt;li&gt;원인 분류(errorCode)가 가능합니다&lt;/li&gt;
&lt;li&gt;성능 분석(elapsedMs)이 가능합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조화 로그를 쓰면 더 좋아집니다&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;level&quot;: &quot;ERROR&quot;, &quot;service&quot;: &quot;payment-service&quot;,
  &quot;traceId&quot;: &quot;trc-20260515-001&quot;, &quot;orderId&quot;: &quot;ORD-1004&quot;,
  &quot;userId&quot;: &quot;U-77&quot;, &quot;message&quot;: &quot;External payment API timeout&quot;,
  &quot;elapsedMs&quot;: 3200, &quot;errorCode&quot;: &quot;PG_TIMEOUT&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 로그의 5가지 조건&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;왜 필요한가&lt;/th&gt;
&lt;th&gt;예시 필드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;식별 가능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;검색해서 원하는 요청을 찾아야 함&lt;/td&gt;
&lt;td&gt;traceId, requestId&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;맥락 보유&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어떤 업무에서 발생했는지 알아야 함&lt;/td&gt;
&lt;td&gt;userId, orderId&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;출처 명확&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어느 서비스의 문제인지 알아야 함&lt;/td&gt;
&lt;td&gt;serviceName&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;원인 분류&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;알람/통계/패턴 분석에 필요&lt;/td&gt;
&lt;td&gt;errorCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시간 측정&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;느린 요청과 성능 저하를 잡아야 함&lt;/td&gt;
&lt;td&gt;elapsedMs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그 레벨 구분&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;레벨&lt;/th&gt;
&lt;th&gt;언제 쓰는가&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class=&quot;badge bg-rose&quot;&gt;ERROR&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;사용자 요청이 실제로 실패했을 때&lt;/td&gt;
&lt;td&gt;결제 실패, DB 연결 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class=&quot;badge bg-amber&quot;&gt;WARN&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;실패는 아니지만 비정상 패턴일 때&lt;/td&gt;
&lt;td&gt;재시도 후 성공, 임계치 근접&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class=&quot;badge bg-sky&quot;&gt;INFO&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;중요한 비즈니스 이벤트를 남길 때&lt;/td&gt;
&lt;td&gt;주문 생성, 결제 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class=&quot;badge bg-lime&quot;&gt;DEBUG&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;개발/테스트 환경에서 내부 흐름을 볼 때&lt;/td&gt;
&lt;td&gt;변수 값, 상세 분기 로직&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;incident&quot;&gt;
&lt;div class=&quot;section-label lbl-rose&quot;&gt;Section 05&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장애가 났을 때 어디부터 봐야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 가장 중요한 건 &lt;b&gt;순서&lt;/b&gt;입니다. 요청 흐름을 따라 &lt;b&gt;어디까지 성공했는지&lt;/b&gt; 확인해야 합니다.&lt;/p&gt;
&lt;div class=&quot;steps&quot;&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;1&lt;/div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요청이 들어왔는지 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;access log, API Gateway, Load Balancer 로그를 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;2&lt;/div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;규모 파악&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metrics에서 에러율, p95 응답시간, 특정 인스턴스/API 문제인지 먼저 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;3&lt;/div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 요청 식별&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traceId, requestId, orderId, userId 중 하나를 확보합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;4&lt;/div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;흐름 따라 좁히기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller &amp;rarr; Service &amp;rarr; DB &amp;rarr; 외부 API &amp;rarr; 응답 순서대로 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;5&lt;/div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MSA라면 서비스 간 로그 연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traceId로 order-service, payment-service 로그를 한 화면에서 이어 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div class=&quot;step-num&quot;&gt;6&lt;/div&gt;
&lt;div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원인 후보 정리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드, DB, 외부 API, 네트워크, 메시징, 리소스 포화 중 원인을 확정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결제 실패 시나리오 예시&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[14:23:01] POST /orders 수신
[14:23:01] order-service:   주문 생성 시작 (orderId=ORD-1004)
[14:23:01] order-service:   주문 저장 성공 (120ms)
[14:23:04] payment-service: 외부 PG API 호출 시작
[14:23:07] payment-service: PG_TIMEOUT (3,200ms)
[14:23:07] order-service:   결제 실패 응답&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;quote&quot;&gt;주문 저장은 성공했고, 결제 서비스 호출도 성공했지만, 외부 PG API에서 3.2초 지연 후 timeout이 발생했습니다. 이 장애의 핵심 원인은 외부 결제사 응답 지연입니다.&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;traceid&quot;&gt;
&lt;div class=&quot;section-label lbl-violet&quot;&gt;Section 06&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MSA에서 Trace ID가 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식에서는 한 서버의 로그만 보면 됐지만, MSA에서는 같은 요청의 로그가 여러 서비스에 흩어집니다.&lt;/p&gt;
&lt;div class=&quot;grid-2&quot;&gt;
&lt;div class=&quot;card card-amber&quot;&gt;&lt;span class=&quot;badge bg-amber&quot;&gt;모놀리식&lt;/span&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Client &amp;rarr; Backend &amp;rarr; DB&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그가 한곳에 모여 있어 비교적 추적이 쉽습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-violet&quot;&gt;&lt;span class=&quot;badge bg-violet&quot;&gt;MSA&lt;/span&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Client &amp;rarr; Order &amp;rarr; Payment &amp;rarr; Coupon &amp;rarr; Notification&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 요청의 로그가 서비스별로 분산되어 공통 식별자가 필수입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin-top: 18px;&quot; data-ke-size=&quot;size16&quot;&gt;이때 필요한 것이 &lt;b&gt;Trace ID&lt;/b&gt;입니다. 사용자 요청 하나가 시작될 때 고유 ID를 만들고, 그 요청이 거치는 모든 서비스 로그에 같은 값을 남깁니다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;order-service        | traceId=trc-001 | 주문 생성
payment-service      | traceId=trc-001 | 결제 실패
coupon-service       | traceId=trc-001 | 쿠폰 사용
notification-service | traceId=trc-001 | 알림 발송 실패&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;quote&quot;&gt;같은 요청이라면, 다음 서비스 호출 시에도 같은 traceId를 헤더에 실어 보내야 합니다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 관점에서는 W3C Trace Context의 &lt;code&gt;traceparent&lt;/code&gt; 헤더를 많이 사용합니다. 표준을 따르면 도구 간 연동이 쉬워집니다.&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;mdc&quot;&gt;
&lt;div class=&quot;section-label lbl-teal&quot;&gt;Section 07&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Boot에서 MDC로 traceId 자동 부착하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 로그를 찍을 때마다 traceId를 직접 넘기면 코드가 지저분해집니다. 이때 사용하는 것이 &lt;b&gt;MDC (Mapped Diagnostic Context)&lt;/b&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;grid-2&quot;&gt;
&lt;div class=&quot;card card-blue&quot;&gt;&lt;span class=&quot;badge bg-blue&quot;&gt;왜 쓰나&lt;/span&gt;
&lt;h4 style=&quot;font-size: 15px;&quot; data-ke-size=&quot;size20&quot;&gt;요청 단위 공통 값 자동 출력&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traceId, requestId 등을 스레드 컨텍스트에 넣어두고 로그 패턴에서 자동으로 출력합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-teal&quot;&gt;&lt;span class=&quot;badge bg-teal&quot;&gt;언제 쓰나&lt;/span&gt;
&lt;h4 style=&quot;font-size: 15px;&quot; data-ke-size=&quot;size20&quot;&gt;MSA 로그 연결이 필요할 때&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot API 서버에서 요청 단위 추적이 필요할 때 매우 유용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 1. 요청 입구에서 traceId 생성 또는 이어받기
MDC.put(&quot;traceId&quot;, UUID.randomUUID().toString());

// 2. 이후 로그에는 자동으로 traceId 부착
log.info(&quot;Order created. orderId={}&quot;, order.getId());

// 3. 요청 종료 시 반드시 정리
MDC.clear();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter에서 처리하는 예시&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Component
public class TraceIdFilter extends OncePerRequestFilter {
    private static final String TRACE_HEADER = &quot;X-Trace-Id&quot;;

    @Override
    protected void doFilterInternal(
        HttpServletRequest req, HttpServletResponse res, FilterChain chain
    ) throws ServletException, IOException {
        try {
            String traceId = req.getHeader(TRACE_HEADER);
            if (traceId == null || traceId.isBlank()) {
                traceId = UUID.randomUUID().toString();
            }
            MDC.put(&quot;traceId&quot;, traceId);
            res.setHeader(TRACE_HEADER, traceId);
            chain.doFilter(req, res);
        } finally {
            MDC.clear();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;callout&quot;&gt;&lt;b&gt;⚠️ 주의:&lt;/b&gt; 비동기 작업에서는 MDC가 자동 전파되지 않을 수 있습니다. &lt;code&gt;@Async&lt;/code&gt;, &lt;code&gt;CompletableFuture&lt;/code&gt;, Kafka Consumer 같은 구간에서는 traceId 전파 전략을 별도로 고려해야 합니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;traceId만으로 부족할 때: Span&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Trace ID: trc-001
├─ 주문 조회          20ms
├─ 쿠폰 검증          80ms
├─ 결제 요청        3200ms  &amp;larr; 병목
│  ├─ 카드사 통신   3150ms
│  └─ 결과 저장       30ms
└─ 알림 발송          90ms&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka도 관찰 대상입니다&lt;/h3&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 발행 시 traceId를 헤더에 담아 전파&lt;/li&gt;
&lt;li&gt;Consumer 로그에도 같은 traceId 기록&lt;/li&gt;
&lt;li&gt;lag 증가, 재처리 횟수, DLQ 적재량도 함께 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id=&quot;security&quot;&gt;
&lt;div class=&quot;section-label lbl-rose&quot;&gt;Section 08&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;절대 로그에 남기면 안 되는 정보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 개인정보와 인증 정보는 절대 평문으로 남기면 안 됩니다.&lt;/p&gt;
&lt;div class=&quot;grid-2&quot;&gt;
&lt;div class=&quot;card card-rose&quot;&gt;&lt;span class=&quot;badge bg-rose&quot;&gt;절대 금지&lt;/span&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비밀번호&lt;/li&gt;
&lt;li&gt;주민등록번호&lt;/li&gt;
&lt;li&gt;카드번호 전체 / CVC / CVV&lt;/li&gt;
&lt;li&gt;JWT, Access Token, Refresh Token&lt;/li&gt;
&lt;li&gt;세션 ID&lt;/li&gt;
&lt;li&gt;계좌번호, 의료 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-amber&quot;&gt;&lt;span class=&quot;badge bg-amber&quot;&gt;흔한 실수&lt;/span&gt;
&lt;pre class=&quot;1c&quot; style=&quot;margin-top: 12px;&quot;&gt;&lt;code&gt;log.info(&quot;Request received: {}&quot;, requestDto); // &amp;larr; 절대 금지&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin-top: 10px; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;요청 객체를 통째로 찍으면 password, token, cardNumber 같은 민감 정보가 함께 남을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전한 방식: 필요한 값만 선택적으로 기록&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;log.info(&quot;Payment request received. traceId={}, orderId={}, userId={}, amount={}&quot;,
    traceId, orderId, maskedUserId, order.getAmount());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마스킹 예시&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 원본: 1234-5678-9012-3456  &amp;rarr;  결과: 1234-****-****-3456
String masked = cardNumber.substring(0, 4) + &quot;-****-****-&quot; + cardNumber.substring(15);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;quote&quot;&gt;로그는 디버깅 도구이기도 하지만, 보안 관점에서는 민감한 데이터가 가장 쉽게 새는 통로가 되기도 합니다.&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;tools&quot;&gt;
&lt;div class=&quot;section-label lbl-lime&quot;&gt;Section 09&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서는 어떤 도구를 조합할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 도구 이름을 외우는 게 아니라, &lt;b&gt;각 도구가 어떤 역할을 맡는지&lt;/b&gt; 이해하는 것입니다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;대표 도구&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;계측 표준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;OpenTelemetry&lt;/td&gt;
&lt;td&gt;로그, 메트릭, 트레이스를 표준 방식으로 생성/전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메트릭 저장소&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Prometheus&lt;/td&gt;
&lt;td&gt;시계열 지표 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;로그 저장소&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Loki, ELK&lt;/td&gt;
&lt;td&gt;로그 검색/집계/분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;트레이스 저장소&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Jaeger, Zipkin, Tempo&lt;/td&gt;
&lt;td&gt;분산 추적 및 병목 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시각화&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Grafana, Kibana&lt;/td&gt;
&lt;td&gt;대시보드, 검색, 알람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;에러 트래킹&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;에러 그룹화, 스택 추적, 영향 범위 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;클라우드 기본 도구&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CloudWatch, Azure Monitor&lt;/td&gt;
&lt;td&gt;기본 로그/메트릭/알람 운영&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회사 규모별로 자주 보이는 조합&lt;/h3&gt;
&lt;div class=&quot;summary-grid&quot;&gt;
&lt;div class=&quot;summary-item&quot;&gt;&lt;b&gt;초기 스타트업&lt;/b&gt;&lt;span&gt;Sentry + CloudWatch 또는 Datadog&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;summary-item&quot;&gt;&lt;b&gt;중간 규모 회사&lt;/b&gt;&lt;span&gt;Sentry + 통합 SaaS (Datadog, New Relic)&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;summary-item&quot;&gt;&lt;b&gt;Kubernetes 기반&lt;/b&gt;&lt;span&gt;Prometheus + Grafana + Loki + Tempo&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;summary-item&quot;&gt;&lt;b&gt;한국 대기업/금융권&lt;/b&gt;&lt;span&gt;Pinpoint + 자체 로그 시스템&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;footer-box&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심은 도구보다 연결입니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 도구를 쓰든 상관없이, &lt;b&gt;같은 요청을 묶는 ID(traceId)&lt;/b&gt;가 있어야 하고, 여러 도구가 그 ID 기준으로 이어져야 진짜 Observability가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;checklist&quot;&gt;
&lt;div class=&quot;section-label lbl-blue&quot;&gt;Section 10&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서 바로 써먹을 수 있는 기준만 짧게 정리합니다.&lt;/p&gt;
&lt;div class=&quot;grid-2&quot;&gt;
&lt;div class=&quot;card card-blue&quot;&gt;&lt;span class=&quot;badge bg-blue&quot;&gt;로그 체크리스트&lt;/span&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;traceId / requestId가 있는가&lt;/li&gt;
&lt;li&gt;userId / orderId 같은 업무 맥락이 있는가&lt;/li&gt;
&lt;li&gt;serviceName, errorCode가 있는가&lt;/li&gt;
&lt;li&gt;elapsedMs를 남기고 있는가&lt;/li&gt;
&lt;li&gt;Exception 객체를 마지막 인자로 넘기고 있는가&lt;/li&gt;
&lt;li&gt;민감 정보를 마스킹하거나 제외했는가&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;card card-teal&quot;&gt;&lt;span class=&quot;badge bg-teal&quot;&gt;장애 대응 체크리스트&lt;/span&gt;
&lt;ul class=&quot;checklist&quot; style=&quot;margin-top: 12px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청이 실제로 들어왔는지 먼저 확인&lt;/li&gt;
&lt;li&gt;Metrics로 범위와 규모를 파악&lt;/li&gt;
&lt;li&gt;문제 요청 하나를 선정&lt;/li&gt;
&lt;li&gt;traceId로 관련 로그 연결&lt;/li&gt;
&lt;li&gt;어디까지 성공했는지 순서대로 확인&lt;/li&gt;
&lt;li&gt;DB / 외부 API / 네트워크 / 코드 중 원인 확정&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;quote&quot; style=&quot;margin-top: 22px;&quot;&gt;좋은 Observability의 목표는 로그를 예쁘게 쌓는 것이 아니라, &lt;b&gt;새벽 2시에도 원인을 빠르게 좁힐 수 있게 만드는 것&lt;/b&gt;입니다.&lt;/div&gt;
&lt;div class=&quot;divider&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서 로그는 단순한 출력문이 아닙니다. 메트릭은 &lt;b&gt;문제가 있다&lt;/b&gt;고 알려주고, 트레이스는 &lt;b&gt;어디가 느린지&lt;/b&gt; 보여주며, 로그는 &lt;b&gt;정확히 무슨 일이 있었는지&lt;/b&gt; 말해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자가 성장할수록 중요한 건 기능 구현 속도만이 아닙니다. &lt;b&gt;문제가 생겼을 때 빠르게 복구할 수 있는 시스템을 만드는 능력&lt;/b&gt;, 그게 바로 Observability입니다.&lt;/p&gt;
&lt;div class=&quot;pill-row&quot;&gt;&lt;span class=&quot;pill&quot;&gt;#로그는많이가아니라찾기쉽게&lt;/span&gt; &lt;span class=&quot;pill&quot;&gt;#Metrics먼저Trace다음Logs마지막&lt;/span&gt; &lt;span class=&quot;pill&quot;&gt;#MSA에서는TraceId필수&lt;/span&gt; &lt;span class=&quot;pill&quot;&gt;#민감정보는절대로그금지&lt;/span&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;/div&gt;</description>
      <category>MSA</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/53</guid>
      <comments>https://0321ljh.tistory.com/53#entry53comment</comments>
      <pubDate>Fri, 15 May 2026 11:31:53 +0900</pubDate>
    </item>
    <item>
      <title>OpenFeign 공식 문서 ,선언적 HTTP 클라이언트의 모든 것</title>
      <link>https://0321ljh.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;/p&gt;
&lt;!-- 포스팅 헤더 --&gt;
&lt;h1&gt;[Spring Cloud] OpenFeign 공식 문서 파헤치기: 선언적 HTTP 클라이언트의 모든 것&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA(Microservices Architecture) 구조에서 서비스 간 통신(Inter-service Communication)은 피할 수 없는 과제입니다. 오늘은 이를 가장 자바답고 우아하게 해결하는 &lt;b&gt;Spring Cloud OpenFeign&lt;/b&gt;의 핵심 개념과 실무 적용 팁을 정리해 보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 1. 개요 --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. OpenFeign이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign은 &lt;b&gt;선언적 HTTP 클라이언트(Declarative HTTP Client)&lt;/b&gt;입니다. 인터페이스를 작성하고 어노테이션을 붙이는 것만으로도 실제 HTTP 요청을 수행하는 구현체를 스프링이 자동으로 만들어줍니다. 개발자는 로직에만 집중할 수 있게 됩니다.&lt;/p&gt;
&lt;!-- 2. 시작하기 및 설정 해상도 --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 시작하기 및 속성 해상도(Attribute Resolution)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 추가 후 &lt;code&gt;@EnableFeignClients&lt;/code&gt;를 선언하면 준비완료&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background: #f4f4f4; padding: 15px; border-radius: 5px;&quot;&gt;&lt;code&gt;@SpringBootApplication
@EnableFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  알아두어야 할 개념: 즉시 해석(Eager Resolution)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 버전부터는 &lt;code&gt;@FeignClient&lt;/code&gt;의 속성값을 &lt;b&gt;빌드 시점(AOT)&lt;/b&gt;에 미리 해석하는 것이 기본값이 되었습니다. &lt;br /&gt;이는 &lt;b&gt;애플리케이션 시작 속도를 높이고 메모리를 절약&lt;/b&gt;하기 위함입니다. 만약 과거처럼 런타임에 지연 해석(Lazy)이 필요하다면 별도의 설정이 필요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 3. Named Client --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Named Client: 독립된 설정의 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign의 가장 중요한 설계 철학 중 하나는 &lt;b&gt;Named Client&lt;/b&gt;입니다. 각 Feign 클라이언트는 자신만의 별도 &lt;code&gt;ApplicationContext&lt;/code&gt;를 가집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개별 설정:&lt;/b&gt; 'A 서비스'는 타임아웃 2초, 'B 서비스'는 타임아웃 5초와 같은 독립적 설정이 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴포넌트 구성:&lt;/b&gt; Encoder, Decoder, Logger, Contract 등을 클라이언트마다 다르게 갈아 끼울 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 4. 실무에서 가장 중요한 3요소 --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 시 필수 체크 포인트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;① 타임아웃(Timeout) 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 지연이나 외부 서비스 장애가 내 서비스로 전파되는 것을 막는 첫 번째 방어선입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ConnectTimeout:&lt;/b&gt; 서버와 연결을 맺는 시간 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ReadTimeout:&lt;/b&gt; 연결 후 응답 데이터가 오기까지의 시간 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② ErrorDecoder: 예외 처리의 정석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fallback이 단순히 '에러 발생 시 기본값'을 주는 것이라면, &lt;code&gt;ErrorDecoder&lt;/code&gt;는 &lt;b&gt;HTTP 상태 코드(4xx, 5xx)에 따라 비즈니스 예외를 던질 수 있게&lt;/b&gt; 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;404가 오면 EntityNotFoundException을 던지고, 401이 오면 로그아웃 처리를 하겠다&quot;&lt;/i&gt;는 식의 정교한 제어가 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ 로깅(Logging) 레벨&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign 로깅은 오직 &lt;b&gt;DEBUG&lt;/b&gt; 레벨에서만 작동합니다. 성능과 보안(헤더의 민감 정보 노출 방지) 때문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;NONE&lt;/code&gt;, &lt;code&gt;BASIC&lt;/code&gt;, &lt;code&gt;HEADERS&lt;/code&gt;, &lt;code&gt;FULL&lt;/code&gt; 네 가지 단계로 제어할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 5. 서킷 브레이커와 폴백 --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장애 탄력성: Circuit Breaker &amp;amp; Fallback&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서는 특정 서비스가 죽었을 때 시스템 전체가 마비되지 않도록 하는 것이 중요합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background: #f4f4f4; padding: 15px; border-radius: 5px;&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;product-service&quot;, fallback = ProductFallback.class)
public interface ProductClient { ... }

@Component
class ProductFallback implements ProductClient {
    @Override
    public String getProductInfo() {
        return &quot;정보를 일시적으로 불러올 수 없습니다.&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FallbackFactory&lt;/b&gt;를 사용하면 어떤 에러(Throwable) 때문에 폴백이 발생했는지 원인 로그를 남길 수 있어 유지보수에 유리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 6. 성능 최적화 --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 한 단계 더 나아가는 성능 최적화 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  HTTP 커넥션 풀(Connection Pool) 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feign은 기본적으로 JDK의 &lt;code&gt;HttpURLConnection&lt;/code&gt;을 사용합니다. 이는 매 요청마다 새로운 연결을 맺기 때문에 성능이 떨어집니다. 실무에서는 반드시 &lt;b&gt;Apache HttpClient&lt;/b&gt;나 &lt;b&gt;OkHttp&lt;/b&gt;를 사용하여 커넥션을 재사용하도록 설정하세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Gzip 압축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청/응답 데이터 양이 많다면 &lt;code&gt;spring.cloud.openfeign.compression&lt;/code&gt; 설정을 통해 네트워크 비용을 줄일 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;&lt;!-- 7. 마무리 --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 요약: 언제 무엇을 써야 할까?&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin-top: 10px; height: 89px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead style=&quot;background-color: #eee;&quot;&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;padding: 10px; height: 23px;&quot;&gt;상황&lt;/th&gt;
&lt;th style=&quot;padding: 10px; height: 23px;&quot;&gt;추천 도구&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 22px;&quot;&gt;일반적인 Spring MVC 기반의 동기 통신&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 22px;&quot;&gt;&lt;b&gt;OpenFeign&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 22px;&quot;&gt;대규모 트래픽 처리를 위한 비동기 통신&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 22px;&quot;&gt;&lt;b&gt;WebClient&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;padding: 10px; height: 22px;&quot;&gt;단순하고 짧은 일회성 HTTP 호출&lt;/td&gt;
&lt;td style=&quot;padding: 10px; height: 22px;&quot;&gt;&lt;b&gt;RestTemplate&lt;/b&gt; (유지 관리 모드)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin-top: 20px;&quot; data-ke-size=&quot;size16&quot;&gt;OpenFeign은 설정이 간편하지만, 그 내부 동작 원리(Named Context, ErrorDecoder 등)를 이해하고 써야 예상치 못한 장애에 대응할 수 있습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/52</guid>
      <comments>https://0321ljh.tistory.com/52#entry52comment</comments>
      <pubDate>Fri, 15 May 2026 11:20:43 +0900</pubDate>
    </item>
    <item>
      <title>MSA 분산 환경의 동시성 제어와 비동기 통신 정리</title>
      <link>https://0321ljh.tistory.com/51</link>
      <description>&lt;div style=&quot;max-width: 860px; margin: 0 auto; font-family: 'Pretendard','Noto Sans KR',sans-serif; line-height: 1.8; color: #222;&quot;&gt;
&lt;div style=&quot;padding: 28px 24px; border: 1px solid #e5e7eb; border-radius: 18px; background: linear-gradient(135deg, #f8fbff 0%, #eef6ff 100%);&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; font-size: 13px; color: #2563eb; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;TIL &amp;middot; Redis &amp;middot; MSA &amp;middot; Concurrency Control&amp;nbsp; &amp;nbsp;&lt;br /&gt;Redis 분산 락, Redisson Watchdog, Pub/Sub vs MQ&lt;/p&gt;
&lt;p style=&quot;margin: 16px 0 0; font-size: 16px; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 MSA 환경에서 반드시 마주치게 되는 동시성 문제를 Redis 중심으로 정리했다. 단순히 &quot;락을 건다&quot; 수준이 아니라, 왜 초과 판매가 발생하는지, SETNX가 왜 위험할 수 있는지, Redisson Watchdog은 언제 믿어도 되는지, Pub/Sub과 MQ는 어떻게 구분해야 하는지까지 한 번에 연결해서 정리해봤다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;height: 24px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote style=&quot;margin: 0; padding: 18px 20px; border-left: 6px solid #3b82f6; background: #f8fafc; border-radius: 10px; color: #1f2937;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;한 줄 요약&lt;/b&gt;&lt;br /&gt;분산 환경에서는 여러 서버가 동시에 같은 자원을 건드리기 때문에 단일 서버 시절의 방식만으로는 정합성을 지키기 어렵다. 그래서 Redis 분산 락, 원자적 명령어, Lua 스크립트, 메시지 큐 같은 도구를 문제 성격에 맞게 선택해야 한다.&lt;/blockquote&gt;
&lt;div style=&quot;height: 28px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;1. 왜 MSA에서는 동시성 문제가 더 무서워질까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 서버 환경에서는 하나의 프로세스 안에서 synchronized나 JVM lock으로 어느 정도 제어가 가능했다. 하지만 MSA 환경에서는 여러 서버 인스턴스가 동시에 같은 재고, 쿠폰 수량, 계좌 잔액 같은 공유 자원에 접근한다. 이때 각 서버가 &quot;지금 재고가 있네?&quot;라고 동시에 판단하면 초과 판매(Over-sell) 같은 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 핵심은 &lt;b&gt;읽기와 쓰기 사이의 틈&lt;/b&gt;이다. 여러 요청이 같은 값을 읽고, 그 사이에 서로의 변경을 모른 채 업데이트를 수행하면 데이터 정합성이 깨진다. 이 문제를 막기 위해 등장하는 대표적인 해법이 바로 &lt;b&gt;분산 락(Distributed Lock)&lt;/b&gt;이다.&lt;/p&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;2. 분산 락은 무엇이고, 왜 Redis를 많이 쓸까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은 쉽게 말해 &quot;공용 화장실 열쇠&quot; 같은 개념이다. 여러 서버가 공용 자원에 접근하려고 할 때, 먼저 열쇠를 가져간 서버만 임계 구역에 들어가고 나머지는 기다리거나 실패 처리하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 싱글 스레드 기반으로 명령이 원자적으로 처리되고, 메모리 기반이라 매우 빠르다. 그래서 짧은 시간 동안 &quot;오직 하나만 들어가게&quot; 만드는 락 저장소로 자주 사용된다.&lt;/p&gt;
&lt;div style=&quot;padding: 18px 20px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 14px; margin: 18px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-weight: bold; color: #111827;&quot; data-ke-size=&quot;size16&quot;&gt;핵심 명령&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 0; white-space: pre-wrap; font-size: 14px; line-height: 1.7; color: #111827;&quot;&gt;&lt;code&gt;SET lock:product:1 request-uuid NX EX 5&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 &lt;code&gt;NX&lt;/code&gt;와 &lt;code&gt;EX&lt;/code&gt;를 함께 쓰는 것이다. 예전처럼 &lt;code&gt;SETNX&lt;/code&gt;로 먼저 락을 잡고, 다음 줄에서 &lt;code&gt;EXPIRE&lt;/code&gt;를 따로 호출하면 그 사이 서버가 죽었을 때 락이 영원히 안 풀릴 수 있다. 즉, 락 획득과 TTL 설정은 반드시 &lt;b&gt;한 번의 원자적 명령&lt;/b&gt;으로 처리해야 한다.&lt;/p&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;3. 문서 속 예시와 도표에서 배운 포인트&lt;/h2&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr; gap: 14px;&quot;&gt;
&lt;div style=&quot;padding: 18px; border: 1px solid #e5e7eb; border-radius: 14px; background: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin: 0 0 8px; font-size: 20px; color: #1f2937;&quot; data-ke-size=&quot;size23&quot;&gt;재고 차감 / 초과 판매 시나리오&lt;/h3&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;여러 서버가 동시에 재고를 읽고 각각 &quot;재고 있음&quot;으로 판단한 뒤 차감을 수행하면 실제 수량보다 더 많이 판매되는 상황이 생긴다. 이 예시는 MSA에서 분산 락이 왜 필요한지를 가장 직관적으로 보여준다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;padding: 18px; border: 1px solid #e5e7eb; border-radius: 14px; background: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin: 0 0 8px; font-size: 20px; color: #1f2937;&quot; data-ke-size=&quot;size23&quot;&gt;데드락 시나리오&lt;/h3&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;락은 잡았지만 만료 시간 설정 전에 서버가 죽으면, 락 키가 영구히 남아 다른 서버가 영원히 진입하지 못한다. 그래서 SETNX와 EXPIRE를 분리 호출하는 구현은 실무에서 매우 위험하다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;padding: 18px; border: 1px solid #e5e7eb; border-radius: 14px; background: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin: 0 0 8px; font-size: 20px; color: #1f2937;&quot; data-ke-size=&quot;size23&quot;&gt;동시성 제어 기술 비교표&lt;/h3&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;DB 비관적 락, DB 낙관적 락, Redis 기본 락, Redisson은 각각 장단점이 다르다. 정합성이 최우선이면 DB 락이 더 맞을 수 있고, 고성능 선착순 처리에는 Redis 계열이 유리하다. 결국 기술 선택은 트래픽과 정합성 요구사항의 균형 문제다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;padding: 18px; border: 1px solid #e5e7eb; border-radius: 14px; background: #ffffff;&quot;&gt;
&lt;h3 style=&quot;margin: 0 0 8px; font-size: 20px; color: #1f2937;&quot; data-ke-size=&quot;size23&quot;&gt;Pub/Sub vs MQ 비교표&lt;/h3&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub은 빠르지만 휘발성이고, Kafka/RabbitMQ는 보관과 Ack를 통해 더 높은 처리 보장성을 제공한다. 채팅 알림처럼 실시간성이 중요한 경우와, 결제 이벤트처럼 절대 유실되면 안 되는 경우를 반드시 구분해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;4. Redisson을 쓰면 왜 편해질까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 Redis 락을 구현하면 생각보다 신경 쓸 게 많다. 락 획득 재시도, TTL 관리, 락 해제 시 본인 검증, 예외 상황 처리, 스핀락 부하 문제까지 모두 개발자가 챙겨야 한다. 반면 Redisson은 이런 부분을 더 안전하고 편하게 추상화해준다.&lt;/p&gt;
&lt;div style=&quot;padding: 18px 20px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 14px; margin: 18px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; font-weight: bold; color: #1d4ed8;&quot; data-ke-size=&quot;size16&quot;&gt;Watchdog이 중요한 이유&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 로직이 예상보다 오래 걸리면 TTL이 먼저 끝나서 락이 풀릴 수 있다. 그러면 아직 작업 중인데도 다른 서버가 같은 자원에 들어와 버린다. Redisson의 Watchdog은 이런 상황을 막기 위해 락 만료 시간을 자동 연장해준다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 함정도 있다. &lt;b&gt;leaseTime을 직접 지정하면 Watchdog이 동작하지 않는다.&lt;/b&gt; 그래서 &quot;무조건 leaseTime 넣는 게 더 안전하겠지?&quot;라고 생각하면 오히려 긴 작업에서 락이 중간에 풀릴 수 있다. 이건 실무에서 정말 자주 헷갈리는 포인트다.&lt;/p&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;5. 락 해제는 왜 UUID 검증이 필요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 값(value)에 단순히 1 같은 값을 넣으면 안 된다. 내 요청이 락을 잡은 뒤 시간이 지나 TTL이 만료되고, 다른 요청이 같은 키로 새 락을 잡았다고 가정해보자. 그런데 이전 요청이 뒤늦게 &lt;code&gt;DEL lock:key&lt;/code&gt;를 실행하면, 내가 잡은 락이 아니라 다른 요청이 새로 잡은 락을 지워버릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 락 value에는 반드시 UUID 같은 고유 식별자를 넣고, 해제할 때도 &quot;현재 value가 내 UUID와 같을 때만 삭제&quot;해야 한다. 이 검증과 삭제는 보통 Lua 스크립트로 원자적으로 처리한다.&lt;/p&gt;
&lt;div style=&quot;padding: 18px 20px; background: #111827; border-radius: 14px; color: #f9fafb; margin: 18px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;예시 개념&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;margin: 0; white-space: pre-wrap; font-size: 14px; line-height: 1.7; color: #e5e7eb;&quot;&gt;&lt;code&gt;if redis.call(&quot;get&quot;, KEYS[1]) == ARGV[1] then
  return redis.call(&quot;del&quot;, KEYS[1])
else
  return 0
end&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;6. Redis Pub/Sub과 MQ는 어떻게 구분해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서에서 가장 인상적이었던 비교는 Redis Pub/Sub을 &quot;라디오 방송&quot;, Kafka를 &quot;우체국&quot;에 비유한 부분이다. Redis Pub/Sub은 발행 시점에 연결된 구독자에게만 메시지가 전달되고, 중간에 구독자가 내려가 있으면 메시지는 그대로 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Kafka나 RabbitMQ 같은 메시지 큐는 메시지를 저장하고, 소비자의 처리 여부를 추적할 수 있다. 그래서 이벤트 유실이 치명적인 주문, 결제, 정산, 포인트 적립 같은 기능에는 MQ가 더 적합하다. 반대로 실시간 알림, 일시적인 브로드캐스트, 워커 깨우기 같은 용도라면 Pub/Sub이 훨씬 가볍고 빠르다.&lt;/p&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;7. 락 없이 해결하는 방법도 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문서가 좋은 이유는 락만 강조하지 않는다는 점이다. 단순 수량 증가/감소라면 락보다 &lt;code&gt;INCR&lt;/code&gt;, &lt;code&gt;DECR&lt;/code&gt; 같은 원자적 명령이 훨씬 빠르고 단순하다. 또 여러 조건이 걸린 복합 로직도 Lua 스크립트로 묶어 서버 내부에서 원자 실행하면 네트워크 왕복을 줄이면서 안전하게 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실무에서는 먼저 &quot;이걸 진짜 락으로 풀어야 하나?&quot;를 질문해야 한다. 카운터 문제인지, 상태 전이 문제인지, 메시지 전달 문제인지에 따라 정답이 달라진다.&lt;/p&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;8. 실무에서 추가로 꼭 알아야 할 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 분산 락은 매우 유용하지만, 완벽한 분산 합의 시스템은 아니다. 단일 마스터 복제 지연이나 장애 전환 상황에서는 상호 배제가 깨질 가능성이 있다. 그래서 금융처럼 정합성 요구 수준이 매우 높은 시스템에서는 Redis 락만 믿지 않고 DB 락이나 추가 검증 로직을 함께 사용하는 다층 방어 전략이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 락이 있어도 중복 요청은 언제든 발생할 수 있으므로 멱등성(idempotency)을 반드시 고려해야 한다. 결제 승인, 주문 생성, 쿠폰 발급 같은 기능은 같은 요청이 여러 번 들어와도 결과가 한 번만 반영되도록 설계해야 실제 운영에서 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &quot;Exactly-once&quot;라는 표현은 매우 조심해서 써야 한다. 대부분의 시스템은 결국 중복 가능성을 전제로 설계하고, idempotency key, 상태 전이 검증, outbox/inbox 패턴 등으로 안정성을 확보한다.&lt;/p&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;9. 실무 사례로 이해한 선택 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물류나 재고 시스템에서는 DB 락만으로 버티기 어렵기 때문에 Redis 기반 분산 락이 성능상 유리할 수 있다. 하지만 금융 서비스처럼 오차가 허용되지 않는 영역에서는 Redis 락만으로 끝내지 않고 DB 레벨 검증까지 겹쳐 쓰는 방식이 더 현실적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 선착순 이벤트 시스템에서는 Pub/Sub을 &quot;데이터 전달&quot;이 아니라 &quot;워커를 깨우는 트리거&quot; 정도로만 쓰고, 실제 데이터는 Redis List나 MQ에 안전하게 적재하는 방식이 훨씬 안정적이다. 이 지점이 Pub/Sub과 MQ를 구분해서 써야 하는 이유를 가장 잘 보여준다.&lt;/p&gt;
&lt;div style=&quot;height: 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;10. 오늘의 TIL&lt;/h2&gt;
&lt;div style=&quot;padding: 20px; border-radius: 16px; background: linear-gradient(135deg,#fefce8,#fff7ed); border: 1px solid #fde68a;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의 핵심 배움 1&lt;/b&gt;&lt;br /&gt;분산 환경의 동시성 문제는 &quot;여러 서버가 동시에 읽고 수정한다&quot;는 사실에서 시작된다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의 핵심 배움 2&lt;/b&gt;&lt;br /&gt;Redis 락은 빠르지만, 락 획득/TTL/해제 검증을 모두 제대로 설계해야 안전하다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의 핵심 배움 3&lt;/b&gt;&lt;br /&gt;Redisson Watchdog은 편리하지만, leaseTime을 직접 주면 동작하지 않는다는 점을 반드시 기억해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의 핵심 배움 4&lt;/b&gt;&lt;br /&gt;Redis Pub/Sub은 실시간 브로드캐스트에 적합하고, 유실되면 안 되는 비즈니스 이벤트는 MQ로 다뤄야 한다.&lt;/p&gt;
&lt;p style=&quot;margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오늘의 핵심 배움 5&lt;/b&gt;&lt;br /&gt;모든 동시성 문제를 락으로 풀 필요는 없고, 원자적 명령어와 Lua 스크립트가 더 좋은 해법일 때도 많다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;height: 24px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 style=&quot;font-size: 26px; color: #111827; border-bottom: 2px solid #dbeafe; padding-bottom: 10px;&quot; data-ke-size=&quot;size26&quot;&gt;11. 면접 대비 한 줄 답변&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. SETNX와 EXPIRE를 왜 분리하면 안 되나요?&lt;/b&gt;&lt;br /&gt;락을 잡은 뒤 EXPIRE 전에 서버가 죽으면 락이 영원히 안 풀려 데드락이 발생할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. Redisson Watchdog은 언제 동작하나요?&lt;/b&gt;&lt;br /&gt;일반적으로 leaseTime을 직접 주지 않았을 때 동작하며, 락의 TTL을 자동 연장해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. Pub/Sub과 MQ의 가장 큰 차이는 뭔가요?&lt;/b&gt;&lt;br /&gt;Pub/Sub은 휘발성 브로드캐스트이고, MQ는 메시지 저장과 처리 보장을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. 락 없이 동시성을 제어할 수도 있나요?&lt;/b&gt;&lt;br /&gt;가능하다. 단순 카운팅은 INCR/DECR, 복합 조건은 Lua 스크립트로 원자 처리할 수 있다.&lt;/p&gt;
&lt;div style=&quot;padding: 18px 20px; border: 1px solid #e5e7eb; border-radius: 14px; background: #fafafa;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/51</guid>
      <comments>https://0321ljh.tistory.com/51#entry51comment</comments>
      <pubDate>Tue, 12 May 2026 15:37:59 +0900</pubDate>
    </item>
    <item>
      <title>[Backend] Redis 코어 아키텍처와 기초 특강</title>
      <link>https://0321ljh.tistory.com/50</link>
      <description>&lt;div&gt;
&lt;style&gt;
    /* 기술 블로그 전용 프리미엄 스타일 */
    .redis-deep-dive {
        line-height: 1.8;
        color: #334155;
        max-width: 900px;
        margin: 0 auto;
        font-family: 'Pretendard', -apple-system, sans-serif;
        word-break: keep-all;
    }
    .redis-deep-dive h1 { color: #0f172a; font-size: 2.5em; border-bottom: 4px solid #3b82f6; padding-bottom: 20px; margin-bottom: 50px; font-weight: 900; }
    .redis-deep-dive h2 { color: #1e40af; font-size: 1.8em; margin-top: 70px; margin-bottom: 25px; border-left: 8px solid #3b82f6; padding-left: 20px; font-weight: 800; }
    .redis-deep-dive h3 { color: #2563eb; font-size: 1.4em; margin-top: 40px; margin-bottom: 15px; font-weight: 700; }
    .redis-deep-dive p, .redis-deep-dive li { font-size: 1.1em; margin-bottom: 18px; }
    
    .redis-deep-dive code { background-color: #f1f5f9; padding: 3px 6px; border-radius: 6px; color: #e11d48; font-family: 'Fira Code', monospace; font-size: 0.95em; }
    .redis-deep-dive pre { background-color: #1e293b; color: #f8fafc; padding: 24px; border-radius: 12px; overflow-x: auto; margin: 24px 0; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); }
    
    .redis-deep-dive .pro-tip { background-color: #f0fdf4; border: 1px solid #bbf7d0; padding: 25px; margin: 40px 0; border-radius: 16px; }
    .redis-deep-dive .pro-tip strong { color: #15803d; display: block; margin-bottom: 10px; font-size: 1.2em; }
    
    .redis-deep-dive .warning-box { background-color: #fff1f2; border: 1px solid #fecdd3; padding: 25px; margin: 40px 0; border-radius: 16px; }
    .redis-deep-dive .warning-box strong { color: #be123c; display: block; margin-bottom: 10px; font-size: 1.2em; }

    .redis-deep-dive .comparison-table { width: 100%; border-collapse: collapse; margin: 32px 0; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
    .redis-deep-dive .comparison-table th, .redis-deep-dive .comparison-table td { border: 1px solid #e2e8f0; padding: 18px; text-align: left; }
    .redis-deep-dive .comparison-table th { background-color: #f8fafc; font-weight: 700; color: #475569; }
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;redis-deep-dive&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초에 수만 건의 트래픽이 몰리는 대형 서비스에서 RDBMS만으로 버티는 것은 불가능에 가깝습니다. &lt;br /&gt;물리적인 디스크 I/O의 한계를 극복하고 쾌적한 사용자 경험을 제공하기 위해 &lt;b&gt;Redis&lt;/b&gt;는 이제 선택이 아닌 '생존'을 위한 필수 인프라가 되었습니다.&lt;br /&gt;오늘은 실무에서 Redis를 다룰 때 반드시 챙겨야 할 핵심 아키텍처와 전략을 정리.&lt;/p&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Redis가 압도적으로 빠른 물리적 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 DB가 지하 서고에 있는 책을 가지러 가는 과정이라면, Redis는 책상 바로 위 '손 닿는 곳에 둔 책꽂이'와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;In-Memory의 위력:&lt;/b&gt; RAM은 전자의 이동만으로 데이터를 처리합니다.&lt;br /&gt;메모리 접근 속도(약 120ns)는 아무리 빠른 SSD(약 50~150us)보다 최소 1,000배가량 빠릅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;싱글 스레드와 이벤트 루프:&lt;/b&gt; 여러 일꾼이 복잡하게 락(Lock)을 걸고 싸우는 대신, 한 명의 초인적인 웨이터(이벤트 루프 다중화)가 모든 주문을 순차적으로, 하지만 미친 듯이 빠르게 처리하여 컨텍스트 스위칭 오버헤드를 없앴습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 실무에서 가장 많이 쓰는 캐싱 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;① Look-Aside (Cache-Aside)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대중적인 방식으로 조회가 많은 서비스에 적합합니다. &lt;br /&gt;앱이 캐시를 먼저 보고, 없으면 DB에서 가져와 캐시에 채워넣습니다. &lt;br /&gt;&lt;b&gt;장점은 Redis 장애 시에도 DB를 통해 서비스 유지가 가능하다는 안정성&lt;/b&gt;에 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② Write-Back&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 쓰기를 Redis에 먼저 하고 주기적으로 DB에 배치(Batch) 반영합니다.&lt;br /&gt;&lt;br /&gt;좋아요 폭주나 실시간 이벤트 등 쓰기 트래픽이 몰릴 때 DB의 부하를 막아주는 강력한 무기이지만, &lt;b&gt;DB 반영 전 Redis 장애 시 데이터 유실 위험&lt;/b&gt;이 있음을 인지해야 한다.&lt;/p&gt;
&lt;/section&gt;
&lt;div class=&quot;warning-box&quot;&gt;&lt;b&gt;⚠️ 운영 함정: 캐시 스탬피드 (Cache Stampede)&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 상품 100개의 만료 시간을 똑같이 '정오 12시'로 설정하면 안 됩니다. &lt;br /&gt;12시 정각에 캐시가 동시에 삭제되는 순간, 수만 명의 유저가 DB로 한꺼번에 몰려 서버가 다운된다&lt;br /&gt;.&lt;b&gt;해결책은 TTL 설정 시 1~5분 정도의 랜덤 난수(Jitter)를 더해 만료 시점을 분산해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 현업 개발자가 반드시 추가로 알아야 할 운영 포인트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 고가용성(HA) 구성 : Replication &amp;amp; Sentinel&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 단일 Redis 인스턴스(Standalone)를 쓰는 것은 매우 위험합니다. &lt;br /&gt;&lt;b&gt;Master-Slave 복제&lt;/b&gt;를 통해 데이터를 실시간 백업하고, Master 서버가 죽었을 때 &lt;b&gt;Sentinel&lt;/b&gt;이 이를 감지해 자동으로 Slave를 Master로 승격(Failover)시키는 구성이 필수이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 메모리 정책 (Eviction Policy)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리는 한정된 자원입니다. 메모리가 가득 찼을 때 어떤 데이터를 지울지 결정해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;allkeys-lru:&lt;/b&gt; 가장 최근에 사용되지 않은 데이터를 삭제 (가장 추천되는 정책)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;volatile-ttl:&lt;/b&gt; 만료 시간이 설정된 키 중 수명이 가장 짧은 것부터 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 Big Keys &amp;amp; Hot Keys 피하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글 스레드 특성상, 하나의 키에 너무 큰 데이터(Big Key)를 넣거나 특정 키 하나에만 트래픽이 몰리면(Hot Key) 전체 시스템이 멈춥니다. &lt;br /&gt;대량의 데이터는 Hash나 여러 키로 쪼개어 저장하는 설계가 필요합니다.&lt;/p&gt;
&lt;/section&gt;
&lt;div class=&quot;pro-tip&quot;&gt;&lt;b&gt;  Senior's Advice: 데이터 정합성 관리&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 업데이트 시 캐시를 지울지, 아니면 새 값으로 바꿀지 고민되시나요?&lt;br /&gt;실무에서는 &lt;b&gt;'Active Invalidation'&lt;/b&gt;, 즉 DB 수정 직후 캐시를 명시적으로 삭제(DEL)하는 방식을 기본으로 한다. &lt;br /&gt;여기에 시스템 오류를 대비해 적절한 &lt;b&gt;TTL&lt;/b&gt;을 이중 안전장치로 거는 것이 정석이라고 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 캐싱을 넘어 &lt;b&gt;배달의민족 B마트 사례처럼 분산 락(Distributed Lock)&lt;/b&gt;을 통해 동시성을 제어하거나, Sorted Set을 활용한 실시간 랭킹 시스템을 구축하는 등 Redis는 활용도가 무궁무진합니다.&lt;br /&gt;하지만 비싼 메모리 자원을 쓰는 만큼, 데이터의 수명(TTL)을 엄격히 관리하고 모니터링하는 습관을 가져야 한다.&amp;nbsp;&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 사이트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redis 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redis/redis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redis GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redislabs.com/blog/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redis Labs Blog (실무 사례)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;/div&gt;</description>
      <category>인메모리 저장소 ,Redis 기초</category>
      <category>Redis</category>
      <category>개발자</category>
      <category>인메모리</category>
      <category>특강정리</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/50</guid>
      <comments>https://0321ljh.tistory.com/50#entry50comment</comments>
      <pubDate>Tue, 12 May 2026 11:27:11 +0900</pubDate>
    </item>
    <item>
      <title>[Backend] 데이터 성능의 핵심, Redis 기초]</title>
      <link>https://0321ljh.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0f172a; font-size: 1.8em; font-weight: 800; letter-spacing: -1px; font-family: Pretendard, -apple-system, sans-serif;&quot;&gt;1. 인메모리 저장소는 왜 필요할까&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;redis-post&quot;&gt;
&lt;section&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트에서 주로 사용하는 RDBMS(MySQL, PostgreSQL 등)는 &lt;b&gt;영속성(Persistence)&lt;/b&gt;을 위해 데이터를 파일 시스템(HDD, SSD)에 저장합니다.&lt;br /&gt;서비스 종료 시에도 데이터가 유지되어야 하는 핵심 정보에는 필수적이지만, 물리적인 디스크 I/O가 발생하기 때문에 속도가 상대적으로 느릴 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;로그인 정보, 장바구니, 게시글 조회수&lt;/b&gt;와 같이 변경이 잦고 일시적인 데이터는 메모리(RAM)를 사용하는 데이터베이스가 훨씬 유리합니다. 메모리는 디스크보다 압도적으로 빠른 읽기/쓰기 성능을 제공하기 때문입니다.&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Redis의 정의와 핵심 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis(Remote Dictionary Server)&lt;/b&gt;는 Java의 &lt;code&gt;Map&amp;lt;Key, Value&amp;gt;&lt;/code&gt; 구조와 유사하게 데이터를 저장하는 대표적인 인메모리 NoSQL 데이터베이스입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초고속 성능:&lt;/b&gt; RAM 기반 저장 방식으로 복잡한 입출력 과정이 생략되어 지연 시간이 극도로 낮습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다양한 자료구조:&lt;/b&gt; 단순 String 외에도 List, Set, Hash, Sorted Set 등을 지원하여 복잡한 로직을 DB 레벨에서 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NoSQL 접근법:&lt;/b&gt; 스키마가 엄격하지 않아 확장성과 유연성이 뛰어나며 비정형 데이터를 처리하기에 최적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;warning-box&quot;&gt;&lt;b&gt;⚠️ 중요: 라이선스 변경&amp;nbsp;&lt;/b&gt;&lt;br /&gt;2024년 3월 20일 이후, Redis는 라이선스를 기존 BSD에서 &lt;b&gt;SSPLv1 및 RSALv2&lt;/b&gt;로 변경했다.&amp;nbsp;&lt;br /&gt;일반적인 상용 서비스 사용에는 여전히 무료이지만, Redis를 직접 재판매하는 클라우드 서비스 제공 업체 등은 제약이 생겼으니 프로젝트 도입 시 참고가 필요하다.&lt;/div&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실무자가 알아야 할 Redis 최신 트렌드 (GitHub Insight)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 Redis(7.x 버전 이상)와 실무 개발 환경에서 반드시 챙겨야 할 핵심 내용입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 데이터 지속성 (Persistence: RDB vs AOF)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 기본적으로 휘발성이지만, 데이터를 디스크에 백업하는 두 가지 메커니즘을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RDB (Redis Database):&lt;/b&gt; 특정 시점의 메모리 스냅샷을 생성합니다. 파일 크기가 작고 복구가 빠르지만, 스냅샷 사이의 데이터 손실 위험이 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AOF (Append Only File):&lt;/b&gt; 모든 쓰기 명령을 로그로 기록합니다. 데이터 유실이 거의 없지만 파일이 커지고 복구 속도가 느릴 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Java 클라이언트 선택: Lettuce vs Jedis&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 환경에서 Redis를 사용할 때 클라이언트 선택이 중요하다.&amp;nbsp;&lt;br /&gt;최근 GitHub 동향과 Spring Data Redis의 기본값은 &lt;b&gt;Lettuce&lt;/b&gt;입니다. &lt;br /&gt;&lt;b&gt;Jedis&lt;/b&gt;는 동기 방식인 반면, &lt;b&gt;Lettuce&lt;/b&gt;는 Netty 기반의 비동기/논블로킹(Non-blocking) 구조를 가져 성능과 확장성 면에서 훨씬 우수합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 Redis Streams &amp;amp; Probabilistic Data Structures&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 캐싱을 넘어 최근에는 로그 수집이나 메시징을 위한 &lt;b&gt;Redis Streams&lt;/b&gt;가 많이 쓰입니다. 또한 대규모 데이터에서 중복 체크를 위한 &lt;b&gt;Bloom Filter&lt;/b&gt;와 같은 확률적 자료구조 기능을 통해 메모리 사용량을 획기적으로 줄이는 설계가 유행하고 있습니다.&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실전 활용 사례&lt;/h2&gt;
&lt;table class=&quot;comparison-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;활용 분야&lt;/th&gt;
&lt;th&gt;상세 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Session Clustering&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;다중 서버 환경에서 동일한 사용자 세션 정보를 공유할 수 있도록 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Caching&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자주 조회되는 DB 쿼리 결과를 저장하여 전반적인 응답 속도 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Leaderboard&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Sorted Set 자료구조를 활용하여 실시간 순위 시스템 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Rate Limiting&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;특정 API에 대한 요청 횟수를 제한하는 보안 기능 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;div class=&quot;insight-box&quot;&gt;&lt;b&gt;  알아야 될 것 &lt;/b&gt;&amp;nbsp;&quot;Redis는&amp;nbsp; 메모리는 디스크보다 훨씬 비싼 자원이기 때문에, 모든 데이터를 Redis에 넣으려는 시도는 지양해야 한다.&amp;nbsp;&lt;br /&gt;&lt;b&gt;'데이터의 수명(TTL)'&lt;/b&gt;을 명확히 설정하고, &lt;b&gt;'Eviction Policy(메모리 부족 시 삭제 정책)'&lt;/b&gt;를 서비스 성격에 맞게 튜닝하는 것이 시역량인거 같다 .&amp;nbsp;&lt;/div&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 참고 사이트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redis/redis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redis 공식 GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redis 공식 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/projects/spring-data-redis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Data Redis Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;
    /* 기술 블로그 전용 스타일 최적화 */
    .redis-post {
        line-height: 1.8;
        color: #334155;
        max-width: 900px;
        margin: 0 auto;
        font-family: 'Pretendard', -apple-system, sans-serif;
        word-break: keep-all;
    }
    .redis-post h2 {
        color: #0f172a;
        border-bottom: 2px solid #e2e8f0;
        padding-bottom: 12px;
        margin-top: 60px;
        margin-bottom: 24px;
        font-size: 1.8em;
        font-weight: 800;
    }
    .redis-post h3 {
        color: #2563eb;
        margin-top: 35px;
        margin-bottom: 16px;
        font-size: 1.4em;
        font-weight: 700;
    }
    .redis-post p, .redis-post li { font-size: 1.1em; margin-bottom: 16px; }
    .redis-post b, .redis-post strong { color: #1d4ed8; }
    
    .redis-post code {
        background-color: #f1f5f9;
        padding: 3px 6px;
        border-radius: 6px;
        font-family: 'Fira Code', monospace;
        font-size: 0.95em;
        color: #e11d48;
    }
    .redis-post pre {
        background-color: #1e293b;
        color: #f8fafc;
        padding: 24px;
        border-radius: 12px;
        overflow-x: auto;
        margin: 24px 0;
        font-size: 0.95em;
        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
    }

    .redis-post .comparison-table {
        width: 100%;
        border-collapse: collapse;
        margin: 32px 0;
        background: #ffffff;
        border-radius: 8px;
        overflow: hidden;
    }
    .redis-post .comparison-table th, .redis-post .comparison-table td {
        border: 1px solid #e2e8f0;
        padding: 16px;
        text-align: left;
    }
    .redis-post .comparison-table th { background-color: #f8fafc; font-weight: 700; color: #475569; }

    .redis-post .insight-box {
        background-color: #eff6ff;
        border-left: 6px solid #3b82f6;
        padding: 24px;
        margin: 40px 0;
        border-radius: 0 12px 12px 0;
    }
    .redis-post .insight-box strong { color: #1e40af; display: block; margin-bottom: 8px; font-size: 1.2em; }
    
    .redis-post .warning-box {
        background-color: #fff1f2;
        border-left: 6px solid #f43f5e;
        padding: 20px;
        margin: 30px 0;
        border-radius: 4px;
    }
&lt;/style&gt;
&lt;/div&gt;</description>
      <category>인메모리 저장소 ,Redis 기초</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/49</guid>
      <comments>https://0321ljh.tistory.com/49#entry49comment</comments>
      <pubDate>Mon, 11 May 2026 15:20:17 +0900</pubDate>
    </item>
    <item>
      <title>JIRA MCP와 Harness Engineering</title>
      <link>https://0321ljh.tistory.com/48</link>
      <description>&lt;div style=&quot;max-width: 880px; margin: 0 auto; font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; color: #334155; line-height: 1.8; font-size: 16px; word-break: keep-all; background-color: #ffffff;&quot;&gt;
&lt;div style=&quot;background-color: #f8fafc; border: 1px solid #e2e8f0; padding: 50px 40px; border-radius: 24px; margin-bottom: 40px; text-align: left; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&quot;&gt;
&lt;h1 style=&quot;margin: 0; font-size: 34px; font-weight: 800; color: #0f172a; line-height: 1.3;&quot;&gt;JIRA MCP와 Harness Engineering&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 알아야될 필수적인 입문 지식들 특강 정리&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;border: 1px solid #e5e7eb; border-radius: 16px; padding: 24px; background: #f8fafc; margin-bottom: 34px;&quot;&gt;
&lt;h2 style=&quot;margin-top: 0; font-size: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;이 글에서 다룰 것&lt;/h2&gt;
&lt;ul style=&quot;padding-left: 20px; margin-bottom: 0;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#jira&quot;&gt;Jira란 무엇인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#issue-tracking&quot;&gt;이슈 트래킹이 왜 중요한가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#mcp&quot;&gt;MCP란 무엇인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#jira-mcp&quot;&gt;JIRA MCP가 실제로 바꾸는 것&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#harness&quot;&gt;Harness Engineering이란 무엇인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#gitops&quot;&gt;GitOps와 SSOT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#visibility&quot;&gt;Visibility가 왜 생산성인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #2563eb; text-decoration: none;&quot; href=&quot;#checklist&quot;&gt;처음 시작하는 개발자를 위한 실전 체크리스트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id=&quot;intro&quot; style=&quot;font-size: 28px; margin-top: 0;&quot; data-ke-size=&quot;size26&quot;&gt;왜 이 주제들이 한꺼번에 묶여서 나올까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보면 Jira는 프로젝트 관리 도구이고, MCP는 AI 연동 표준이며, Harness Engineering은 개발 문화 이야기처럼 보입니다. 하지만 이 셋은 사실 같은 질문을 다른 각도에서 다룹니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 24px 0; padding: 18px 20px; border-left: 5px solid #2563eb; background: #eff6ff; border-radius: 10px;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&amp;ldquo;AI 에이전트가 우리 팀에서 사람처럼 일하려면, 무엇이 준비되어 있어야 할까?&amp;rdquo;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 답하려면 세 가지가 필요합니다. 첫째, AI가 팀의 업무 도구와 연결되어야 합니다. 둘째, AI가 읽고 판단할 수 있도록 프로젝트의 규칙과 맥락이 정리되어 있어야 합니다. 셋째, 코드와 배포 상태가 모두에게 보이는 구조여야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;jira&quot; style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;1. &lt;a style=&quot;color: #111827; text-decoration: none;&quot; href=&quot;https://www.atlassian.com/software/jira&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Jira&lt;/a&gt;란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jira는 Atlassian이 만든 대표적인 &lt;b&gt;이슈 트래킹 및 프로젝트 관리 도구&lt;/b&gt;입니다. &lt;br /&gt;쉽게 말하면, 팀이 해야 할 일을 쪼개서 기록하고, 누가 맡고 있고, 지금 어느 단계에 있으며, 언제 끝나야 하는지를 한곳에서 관리하는 시스템입니다.&lt;br /&gt;Atlassian은 Jira를 팀이 계획하고, 추적하고, 전달하는 일을 하나의 흐름으로 연결하는 도구로 설명한다.&lt;/p&gt;
&lt;div style=&quot;background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 16px; padding: 22px; margin: 24px 0;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;개발자가 처음 알아야 할 Jira 핵심 용어&lt;/h3&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 15px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background: #eef2ff;&quot;&gt;
&lt;th style=&quot;padding: 12px; border: 1px solid #e5e7eb; text-align: left;&quot;&gt;용어&lt;/th&gt;
&lt;th style=&quot;padding: 12px; border: 1px solid #e5e7eb; text-align: left;&quot;&gt;쉽게 설명하면&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;Issue&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;해야 할 일 하나를 담는 기본 단위. 버그, 기능 개발, 작업 요청 등이 모두 이슈가 될 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;Project&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;관련 이슈들을 묶어 관리하는 공간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;Board&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;이슈를 칸반처럼 시각화해서 보여주는 화면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;Backlog&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;아직 진행 전이지만 앞으로 해야 할 일 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;Sprint&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;정해진 기간 동안 완료하기로 한 작업 묶음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;Workflow&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;To Do &amp;rarr; In Progress &amp;rarr; Done 같은 상태 흐름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jira의 강점은 단순히 할 일 목록을 적는 데 있지 않습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;팀이 지금 어떤 일을 하고 있는지, 무엇이 막혀 있는지, 어떤 일이 먼저 처리되어야 하는지&lt;/b&gt;를 모두가 같은 화면에서 볼 수 있다는 점이 핵심입니다. &lt;br /&gt;&lt;br /&gt;특히 소프트웨어 팀에서는 버그, 기능, 배포 준비, 리뷰 요청 같은 서로 다른 종류의 작업을 한 체계로 묶을 수 있다는 점이 중요합니다.&lt;/p&gt;
&lt;h3 id=&quot;issue-tracking&quot; style=&quot;font-size: 22px;&quot; data-ke-size=&quot;size23&quot;&gt;이슈 트래킹은 왜 중요한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 팀은 늘 여러 일이 동시에 돌아갑니다. 버그도 처리해야 하고, 신규 기능도 만들어야 하며, 운영 이슈도 확인해야 합니다.&lt;br /&gt;&lt;br /&gt;이때 이슈 트래킹이 없으면 팀은 &amp;ldquo;무엇이 가장 중요한가&amp;rdquo;, &amp;ldquo;누가 무엇을 맡고 있는가&amp;rdquo;, &amp;ldquo;어디서 멈췄는가&amp;rdquo;를 계속 사람 머리로 기억해야 합니다. Jira 같은 도구는 이 모든 작업을 기록하고 연결해 &lt;b&gt;팀의 공용 작업 기억장치&lt;/b&gt; 역할을 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jira 보드는 초보자에게 특히 유용합니다. Scrum 보드는 스프린트처럼 일정한 주기로 일하는 팀에 잘 맞고, Kanban 보드는 작업 흐름을 끊기지 않게 관리하는 팀에 잘 맞습니다. &lt;br /&gt;&lt;br /&gt;즉, Jira는 단순한 할 일 앱이 아니라 &lt;b&gt;개발 방식 자체를 시각화하는 도구&lt;/b&gt;라고 보는 편이 더 정확합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background: #fff7ed; border: 1px solid #fdba74; border-radius: 14px; padding: 18px 20px; margin: 26px 0;&quot;&gt;&lt;b&gt;초보자 포인트&lt;/b&gt;&lt;br /&gt;Jira를 처음 볼 때 &amp;ldquo;왜 이렇게 복잡하지?&amp;rdquo;라고 느끼기 쉽습니다. &lt;br /&gt;그런데 실제로는 반대이다 &lt;b&gt;팀의 현실이 원래 복잡한데, Jira가 그 복잡함을 눈에 보이게 만든 것&lt;/b&gt;이다&lt;/div&gt;
&lt;h2 style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;mcp&quot; style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;2. &lt;a style=&quot;color: #111827; text-decoration: none;&quot; href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MCP&lt;/a&gt;란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP는 &lt;b&gt;Model Context Protocol&lt;/b&gt;의 약자입니다. 쉽게 말하면, AI가 외부 도구와 데이터를 표준 방식으로 연결하기 위한 공통 인터페이스입니다. 공식 문서는 MCP를 &amp;ldquo;AI 애플리케이션을 외부 시스템에 연결하는 오픈 표준&amp;rdquo;으로 설명하며, USB‑C처럼 하나의 규격으로 다양한 도구를 연결하는 비유를 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 표준이 필요할까요? 아무리 똑똑한 AI라도, 외부 시스템과 연결되지 않으면 실제 업무를 할 수 없습니다. 예를 들어 AI가 Jira 티켓을 읽거나, GitHub PR을 열거나, 사내 문서를 검색하려면 각 시스템과 연결되는 통로가 필요합니다. Anthropic은 MCP가 이런 파편화된 통합을 줄이고, AI 시스템이 필요한 데이터에 더 단순하고 안정적으로 접근하게 만든다고 설명합니다.&lt;/p&gt;
&lt;div style=&quot;background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 16px; padding: 22px; margin: 24px 0;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;MCP를 아주 쉽게 비유하면&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;예전에는 기기마다 충전 케이블이 달랐다면, 이제는 USB‑C 하나로 통일되는 방향으로 가고 있습니다. MCP도 비슷합니다. 도구마다 AI 연결 방식을 따로 만드는 대신, &lt;b&gt;AI와 외부 시스템이 공통 규격으로 대화하게 만드는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 소개에 따르면 MCP를 쓰면 AI는 데이터 소스, 각종 도구, 워크플로우와 연결될 수 있습니다. 그래서 단순히 대답만 하는 챗봇이 아니라, 실제로 &lt;b&gt;검색하고, 읽고, 실행하고, 업데이트하는 에이전트&lt;/b&gt;로 발전할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic은 MCP를 통해 개발자가 각 데이터 소스마다 개별 커넥터를 계속 만드는 대신, 하나의 표준 프로토콜 위에서 AI와 시스템을 연결할 수 있다고 설명합니다. &lt;br /&gt;즉, MCP의 핵심은 &amp;ldquo;AI가 더 똑똑해지는 것&amp;rdquo;이 아니라, &lt;b&gt;AI가 실제 업무 환경과 연결되는 것&lt;/b&gt;에 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;jira-mcp&quot; style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;3. &lt;a style=&quot;color: #111827; text-decoration: none;&quot; href=&quot;https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JIRA MCP&lt;/a&gt;는 무엇을 바꾸는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jira를 AI와 연결하는 대표적인 방식이 바로 Atlassian의 &lt;b&gt;Rovo MCP Server&lt;/b&gt;입니다. Atlassian은 이를 Atlassian Cloud와 외부 AI 도구를 연결하는 &lt;b&gt;클라우드 기반 브리지&lt;/b&gt;로 설명합니다. 이 구성을 사용하면 AI가 Jira, Confluence, Compass 데이터를 실시간으로 읽고 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자 입장에서 중요한 변화는 하나입니다. 이제 AI가 Jira를 &amp;ldquo;설명받는 대상&amp;rdquo;이 아니라, &lt;b&gt;직접 상호작용하는 도구&lt;/b&gt;로 다룰 수 있다는 점입니다. 예전에는 사람이 Jira 화면에 들어가 필터를 만들고, 티켓을 열고, 내용을 읽고, 정리 결과를 다른 문서로 옮겨야 했습니다. 하지만 JIRA MCP가 연결되면, AI가 그 과정을 자연어 명령을 받아 대신 수행할 수 있는 기반이 생깁니다.&lt;/p&gt;
&lt;div style=&quot;background: #f8fafc; border: 1px solid #cbd5e1; border-radius: 16px; padding: 22px; margin: 24px 0;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;예를 들면 이런 흐름입니다&lt;/h3&gt;
&lt;div style=&quot;background: #111827; color: #f9fafb; padding: 16px; border-radius: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 14px;&quot;&gt;&amp;ldquo;내가 이번 스프린트에 맡은 티켓 정리해줘.&amp;rdquo;&lt;br /&gt;&amp;ldquo;결제 관련 진행 중 버그만 모아서 요약해줘.&amp;rdquo;&lt;br /&gt;&amp;ldquo;회의록 보고 액션 아이템을 Jira 이슈로 정리해줘.&amp;rdquo;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Atlassian 문서에 따르면 Rovo MCP Server는 Jira, Confluence, Compass와 연결되며, ChatGPT, Claude, GitHub Copilot CLI, Gemini 등 여러 AI 클라이언트와 연동할 수 있습니다. 중요한 점은 이 연결이 별도의 슈퍼 권한을 주는 방식이 아니라, &lt;b&gt;사용자가 이미 가진 권한 범위 안에서만 동작&lt;/b&gt;한다는 것입니다. 즉, 내가 볼 수 없는 이슈는 AI도 볼 수 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 권한 모델은 실무에서 매우 중요합니다. AI가 강력해질수록 사람들이 가장 먼저 걱정하는 것은 보안과 접근 범위인데, Rovo MCP Server는 OAuth 2.1 기반 인증과 기존 Atlassian 권한 체계를 그대로 따르는 방식으로 설계되어 있습니다. 즉, &amp;ldquo;AI가 내 권한으로 대신 일한다&amp;rdquo;는 &lt;b&gt;위임 모델&lt;/b&gt;에 가깝습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-size: 22px;&quot; data-ke-size=&quot;size23&quot;&gt;개발자 입장에서 JIRA MCP가 유용한 장면&lt;/h3&gt;
&lt;ul style=&quot;padding-left: 22px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스탠드업 전에 내 티켓 상태를 자동 정리할 때&lt;/li&gt;
&lt;li&gt;버그 이슈와 코멘트를 빠르게 훑고 핵심만 요약할 때&lt;/li&gt;
&lt;li&gt;회의록이나 명세 문서에서 액션 아이템을 티켓으로 바꿀 때&lt;/li&gt;
&lt;li&gt;이슈 상태와 배포/PR 맥락을 함께 확인하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background: #ecfeff; border: 1px solid #67e8f9; border-radius: 14px; padding: 18px 20px; margin: 26px 0;&quot;&gt;&lt;b&gt;한 줄 정리&lt;/b&gt;&lt;br /&gt;Jira가 팀의 일감을 정리하는 시스템이라면, JIRA MCP는 &lt;b&gt;AI가 그 시스템 안에서 실제로 일하게 해주는 연결 장치&lt;/b&gt;입니다.&lt;/div&gt;
&lt;h2 style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;harness&quot; style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;4. &lt;a style=&quot;color: #111827; text-decoration: none;&quot; href=&quot;https://openai.com/index/harness-engineering/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Harness Engineering&lt;/a&gt;이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 개발자가 AI 코딩 도구를 처음 써보고 비슷한 경험을 합니다. 프롬프트를 꽤 잘 썼는데도 엉뚱한 파일을 고치거나, 팀 규칙을 무시하거나, 이미 정한 구조를 깨는 코드를 만들곤 합니다. &lt;br /&gt;&lt;br /&gt;이때 흔히 &amp;ldquo;모델이 아직 부족해서 그래&amp;rdquo;라고 생각하기 쉽지만, OpenAI는 다른 관점을 제시합니다. 문제의 상당 부분은 모델 자체보다도 &lt;b&gt;에이전트가 일하는 환경이 설계되지 않았기 때문&lt;/b&gt;이라는 것입니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Harness Engineering은 바로 그 환경을 설계하는 접근입니다. OpenAI는 이를 통해, 사람의 주된 역할이 직접 코드를 쓰는 것이 아니라 &lt;b&gt;의도를 명확히 전달하고, 시스템을 설계하고, 피드백 루프를 만들어 AI가 안정적으로 일하게 하는 것&lt;/b&gt;으로 이동한다고 설명합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 16px; padding: 22px; margin: 24px 0;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;쉽게 말해 하네스는 무엇인가&lt;/h3&gt;
&lt;p style=&quot;margin-bottom: 0;&quot; data-ke-size=&quot;size16&quot;&gt;AI에게 &amp;ldquo;잘해봐&amp;rdquo;라고 던지는 것이 아니라, &lt;b&gt;어디까지 할 수 있는지, 무엇을 참고해야 하는지, 무엇이 틀렸는지 어떻게 알 수 있는지&lt;/b&gt;까지 포함해 작업 환경 전체를 설계하는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;font-size: 22px;&quot; data-ke-size=&quot;size23&quot;&gt;Harness Engineering의 핵심 요소&lt;/h3&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 15px; margin: 18px 0 24px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background: #eef2ff;&quot;&gt;
&lt;th style=&quot;padding: 12px; border: 1px solid #e5e7eb; text-align: left;&quot;&gt;영역&lt;/th&gt;
&lt;th style=&quot;padding: 12px; border: 1px solid #e5e7eb; text-align: left;&quot;&gt;왜 중요한가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;문서화된 컨텍스트&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;AI는 레포 안에서 읽을 수 있는 정보만 제대로 활용할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;아키텍처 제약&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;AI가 아무 방향으로나 퍼지지 않도록 구조적 경계를 강제함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;피드백 루프&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;린터, 테스트, 타입체커가 틀린 결과를 즉시 알려줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;관측성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;로그, 메트릭, 트레이스를 볼 수 있어야 버그를 추적할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;&lt;b&gt;SSOT&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 12px; border: 1px solid #e5e7eb;&quot;&gt;흩어진 정보 대신 저장소 안의 버전 관리된 지식을 기준으로 삼음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 특히 &lt;b&gt;레포지토리 안의 지식을 시스템 오브 레코드(system of record)&lt;/b&gt;로 만드는 것을 강조합니다. 이유는 명확합니다. AI 에이전트 입장에서, 실행 시점에 접근할 수 없는 정보는 사실상 존재하지 않는 것과 같기 때문입니다. &lt;br /&gt;&lt;br /&gt;회의록, 채팅방, 사람 머릿속에 있는 규칙은 에이전트가 안정적으로 참조할 수 없습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AGENTS.md, CLAUDE.md, docs/architecture.md, ADR 같은 문서가 중요해집니다.&lt;br /&gt;&lt;br /&gt;이 문서들은 사람이 보기 위한 참고자료이기도 하지만, 동시에 AI가 프로젝트를 이해하기 위한 &lt;b&gt;맥락이 된다&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background: #fefce8; border: 1px solid #fde68a; border-radius: 14px; padding: 18px 20px; margin: 26px 0;&quot;&gt;&lt;b&gt;중요한 포인트&lt;/b&gt;&lt;br /&gt;AI 코딩의 품질은 프롬프트만으로 결정되지 않습니다. &lt;b&gt;더 좋은 결과는 더 긴 프롬프트보다 더 좋은 작업 환경에서 나온다&lt;/b&gt;는 것이 Harness Engineering의 핵심 메시지입니다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;gitops&quot; style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;5. &lt;a style=&quot;color: #111827; text-decoration: none;&quot; href=&quot;https://opengitops.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitOps&lt;/a&gt;는 왜 함께 이해해야 할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps는 Git 저장소를 시스템 상태의 &lt;b&gt;Single Source of Truth&lt;/b&gt;로 삼고, 자동화된 에이전트가 실제 환경을 Git에 정의된 원하는 상태와 지속적으로 맞추는 운영 방식입니다. &lt;br /&gt;&lt;br /&gt;OpenGitOps는 GitOps의 핵심 원칙을 선언적 구성, 버전화와 불변성, 자동 pull, 지속적 reconciliation으로 정리합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념이 왜 Harness Engineering과 연결될까요? 둘 다 같은 철학을 공유하기 때문입니다. &lt;b&gt;정답은 흩어진 설명이 아니라 저장소 안에 있어야 한다&lt;/b&gt;입니다. GitOps는 이를 인프라와 배포에 적용하고, Harness Engineering은 이를 AI 에이전트의 작업 환경 전반으로 확장합니다.&lt;/p&gt;
&lt;div style=&quot;background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 16px; padding: 22px; margin: 24px 0;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;GitOps의 4가지 핵심 원칙&lt;/h3&gt;
&lt;ol style=&quot;padding-left: 20px; margin-bottom: 0;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Declarative&lt;/b&gt; &amp;mdash; 시스템이 어떤 상태여야 하는지 선언적으로 적는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Versioned and Immutable&lt;/b&gt; &amp;mdash; 원하는 상태는 Git에 버전 이력과 함께 남는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pulled Automatically&lt;/b&gt; &amp;mdash; 소프트웨어 에이전트가 Git의 상태를 자동으로 가져온다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Continuously Reconciled&lt;/b&gt; &amp;mdash; 실제 상태가 Git과 다르면 다시 맞춘다&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Kubernetes 환경에서는 &lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Argo CD&lt;/a&gt; 같은 도구가 대표적입니다. Argo CD는 자신을 &amp;ldquo;Kubernetes를 위한 선언적 GitOps Continuous Delivery 도구&amp;rdquo;로 설명하며, Git 저장소를 원하는 상태의 기준점으로 삼고 현재 클러스터 상태와 비교하여 어긋난 부분을 동기화합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자 관점에서는 이렇게 이해하면 쉽습니다. &lt;br /&gt;예전에는 운영자가 kubectl 명령으로 직접 바꿨다면, GitOps에서는 &amp;ldquo;어떤 상태가 되어야 하는지&amp;rdquo;를 Git에 커밋하고, 컨트롤러가 그 상태를 실제 환경에 맞춰 반영합니다. &lt;br /&gt;그래서 변경 이력이 명확하고, 리뷰가 가능하며, 롤백도 쉬워집니다.&lt;/p&gt;
&lt;h2 id=&quot;visibility&quot; style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;6. Visibility가 왜 사람과 AI 모두에게 중요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 팀에서 자주 나오는 질문이 있습니다. &amp;ldquo;이 버그는 어떤 티켓이었지?&amp;rdquo;, &amp;ldquo;이 변경은 어떤 PR로 들어갔지?&amp;rdquo;, &amp;ldquo;이게 지금 운영에 반영됐나?&amp;rdquo; 이런 질문에 답하려면 이슈, 코드, 빌드, 배포, 운영 지표가 서로 연결되어 있어야 합니다. 이 연결 상태를 잘 보여주는 능력이 바로 &lt;b&gt;Visibility&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가시성이 높은 팀에서는 하나의 이슈가 어떤 브랜치와 PR로 이어졌는지, 어떤 빌드가 돌았는지, 언제 배포됐는지, 그 뒤 운영 환경에서 어떤 상태를 보였는지까지 연결해서 볼 수 있습니다. Jira가 작업을 정리하고, MCP가 AI를 도구에 연결하고, GitOps가 배포 상태를 Git과 맞추면, 사람과 AI가 &lt;b&gt;같은 사실을 기준으로 판단할 수 있는 환경&lt;/b&gt;이 만들어집니다.&lt;/p&gt;
&lt;div style=&quot;background: #ecfccb; border: 1px solid #86efac; border-radius: 14px; padding: 18px 20px; margin: 26px 0;&quot;&gt;&lt;b&gt;결국 보이는 것이 관리된다&lt;/b&gt;&lt;br /&gt;사람도, AI도, 보이지 않는 것은 제대로 다루기 어렵습니다. &lt;br /&gt;그래서 좋은 개발 문화는 친절한 설명이 아니라, &lt;b&gt;잘 보이는 시스템&lt;/b&gt;을 만드는 쪽으로 발전합니다.&lt;/div&gt;
&lt;h2 id=&quot;checklist&quot; style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;7. 처음 시작하는 개발자를 위한 실전 체크리스트&lt;/h2&gt;
&lt;div style=&quot;display: grid; gap: 18px; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;border: 1px solid #dbeafe; background: #eff6ff; border-radius: 16px; padding: 20px;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;Level 1 &amp;mdash; 개인 프로젝트에서 해볼 것&lt;/h3&gt;
&lt;ul style=&quot;padding-left: 20px; margin-bottom: 0;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jira에서 이슈, 보드, 스프린트 개념 익히기&lt;/li&gt;
&lt;li&gt;프로젝트 루트에 AGENTS.md 또는 CLAUDE.md 만들어보기&lt;/li&gt;
&lt;li&gt;README에 실행 방법, 테스트 방법, 폴더 구조 정리하기&lt;/li&gt;
&lt;li&gt;린터와 포매터를 자동화해 AI와 사람이 같은 규칙을 쓰게 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;border: 1px solid #fde68a; background: #fefce8; border-radius: 16px; padding: 20px;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;Level 2 &amp;mdash; 팀 단위로 해볼 것&lt;/h3&gt;
&lt;ul style=&quot;padding-left: 20px; margin-bottom: 0;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀 규칙을 채팅방이 아니라 docs/ 디렉토리로 옮기기&lt;/li&gt;
&lt;li&gt;아키텍처 규칙을 문서뿐 아니라 린터/테스트로 검증하기&lt;/li&gt;
&lt;li&gt;Jira 이슈와 PR, 빌드, 배포를 연결해 흐름 보이게 만들기&lt;/li&gt;
&lt;li&gt;MCP 기반 도구 연동을 검토해 AI가 실제 업무 도구를 읽게 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;border: 1px solid #fecaca; background: #fef2f2; border-radius: 16px; padding: 20px;&quot;&gt;
&lt;h3 style=&quot;margin-top: 0; font-size: 21px;&quot; data-ke-size=&quot;size23&quot;&gt;Level 3 &amp;mdash; 운영 환경까지 확장할 것&lt;/h3&gt;
&lt;ul style=&quot;padding-left: 20px; margin-bottom: 0;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitOps 도입으로 배포 상태를 Git 기준으로 정리하기&lt;/li&gt;
&lt;li&gt;Argo CD나 유사 도구로 drift 감지와 동기화 자동화하기&lt;/li&gt;
&lt;li&gt;로그, 메트릭, 트레이스를 AI가 읽을 수 있는 통로 설계하기&lt;/li&gt;
&lt;li&gt;엔트로피 관리용 정리 루틴이나 에이전트 운영하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;font-size: 28px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;8. 마무리: 이제 중요한 것은 &amp;lsquo;더 좋은 프롬프트&amp;rsquo;만이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람이 AI 활용을 이야기할 때 프롬프트 작성법부터 떠올립니다. 물론 그것도 중요합니다. 하지만 실제 팀 개발에서는 프롬프트보다 더 중요한 것이 있습니다. 바로 &lt;b&gt;AI가 일할 수 있는 환경&lt;/b&gt;입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jira는 일을 구조화하고, MCP는 AI를 도구와 연결하고, JIRA MCP는 AI가 이슈 트래커 안에서 실제로 일할 수 있게 만들고, Harness Engineering은 그 AI가 흔들리지 않도록 맥락과 제약과 피드백 루프를 설계합니다.&lt;br /&gt;&lt;br /&gt;그리고 GitOps는 코드와 인프라의 진실을 저장소 안에 고정해, 사람과 AI가 같은 기준을 보게 만듭니다.&lt;/p&gt;
&lt;blockquote style=&quot;margin: 24px 0; padding: 18px 20px; border-left: 5px solid #111827; background: #f3f4f6; border-radius: 10px;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;좋은 AI 협업은 더 좋은 모델만으로 오지 않는다. 더 잘 설계된 개발 환경에서 나온다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로의 개발자는 단순히 코드를 많이 쓰는 사람보다, &lt;b&gt;사람과 AI가 함께 일할 수 있는 구조를 설계하는 사람&lt;/b&gt;이 더 강해질 가능성이 큽니다.&lt;br /&gt;그래서 지금 이 주제들을 이해해두는 것은 단순한 신기술 공부가 아니라, 앞으로의 개발 방식 자체를 이해하는 일에 가까울거 같다.&lt;/p&gt;
&lt;h2 style=&quot;font-size: 24px;&quot; data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.atlassian.com/software/jira/guides/getting-started/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Atlassian Jira Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.atlassian.com/software/jira/guides/boards/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Atlassian Jira Boards Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.atlassian.com/software/jira/features/bug-tracking&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Atlassian Jira Bug Tracking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/news/model-context-protocol&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Anthropic &amp;mdash; Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Model Context Protocol Official Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.atlassian.com/atlassian-rovo-mcp-server/docs/getting-started-with-the-atlassian-remote-mcp-server/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Atlassian Rovo MCP Server Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/index/harness-engineering/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenAI &amp;mdash; Harness Engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opengitops.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenGitOps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Argo CD Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI</category>
      <category>AI</category>
      <category>개발</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/48</guid>
      <comments>https://0321ljh.tistory.com/48#entry48comment</comments>
      <pubDate>Fri, 8 May 2026 12:40:05 +0900</pubDate>
    </item>
    <item>
      <title>[Git] &amp;quot;왜 이렇게 하나요?&amp;quot; 맥락으로 이해하는 팀 협업 브랜치 전략</title>
      <link>https://0321ljh.tistory.com/46</link>
      <description>&lt;div&gt;
&lt;style&gt;
    /* 기술 블로그 전용 스타일시트 */
    .git-only-post {
        line-height: 1.8;
        color: #333;
        max-width: 850px;
        margin: 0 auto;
        font-family: 'Pretendard', -apple-system, sans-serif;
    }
    .git-only-post h2 {
        color: #2c3e50;
        border-bottom: 2px solid #3498db;
        padding-bottom: 10px;
        margin-top: 50px;
        margin-bottom: 20px;
        font-weight: bold;
    }
    .git-only-post h3 {
        color: #2980b9;
        margin-top: 35px;
        font-weight: 600;
    }
    .git-only-post p, .git-only-post li { font-size: 1.05em; margin-bottom: 12px; }
    .git-only-post b { color: #e67e22; }
    .git-only-post code {
        background-color: #f3f4f6;
        padding: 2px 5px;
        border-radius: 4px;
        font-family: 'Fira Code', monospace;
        color: #c0392b;
    }
    .git-only-post pre {
        background-color: #282c34;
        color: #abb2bf;
        padding: 20px;
        border-radius: 8px;
        overflow-x: auto;
        margin: 20px 0;
    }
    .git-only-post .comparison-table {
        width: 100%;
        border-collapse: collapse;
        margin: 25px 0;
    }
    .git-only-post .comparison-table th, .git-only-post .comparison-table td {
        border: 1px solid #eee;
        padding: 15px;
        text-align: left;
    }
    .git-only-post .comparison-table th { background-color: #f8f9fa; }
    
    .git-only-post .insight-box {
        background-color: #f0f7ff;
        border-left: 5px solid #007bff;
        padding: 20px;
        margin: 35px 0;
        border-radius: 0 8px 8px 0;
    }
    .git-only-post .warning-box {
        background-color: #fff5f5;
        border-left: 5px solid #ff7675;
        padding: 20px;
        margin: 25px 0;
        border-radius: 4px;
    }
    .git-only-post img {
        max-width: 100%;
        height: auto;
        display: block;
        margin: 30px auto;
        border-radius: 8px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    }
&lt;/style&gt;
&lt;/div&gt;
&lt;article class=&quot;git-only-post&quot;&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 배경 및 문제 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙 없이 &lt;code&gt;main&lt;/code&gt; 브랜치에만 푸시하는 팀은 반드시 사고를 마주합니다. 내 작업이 동료의 코드를 덮어쓰거나, 미완성 기능이 운영 서버에 섞여 들어가는 등의 문제는 '명령어'를 몰라서가 아니라 '전략'이 없어서 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치 전략은 단순히 멋을 위한 것이 아니라, &lt;b&gt;사고 방지 장치&lt;/b&gt;이자 팀의 &lt;b&gt;소통 규약&lt;/b&gt;입니다.&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 해결 방안: 우리 팀에 맞는 브랜치 전략 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 크게 두 가지 전략이 통용됩니다. 우리 서비스의 성격에 맞는 판단 기준을 세워야 합니다.&lt;/p&gt;
&lt;table class=&quot;comparison-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전략&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;권장 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;GitHub Flow&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;main + feature 구성 (단순)&lt;/td&gt;
&lt;td&gt;빠른 배포가 필요한 웹 서비스, SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Git Flow&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5가지 브랜치로 엄격하게 관리&lt;/td&gt;
&lt;td&gt;정기 릴리즈가 필요한 앱, 패키지 프로젝트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;insight-box&quot;&gt;&quot;작은 팀이나 스타트업은 &lt;b&gt;GitHub Flow&lt;/b&gt;로 시작하고, 규모가 커지며 버전 관리가 복잡해질 때 &lt;b&gt;Git Flow&lt;/b&gt;로 확장하는 것이 가장 실용적이다..&quot;&lt;/div&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. PR(Pull Request) 워크플로우와 코드 리뷰&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR을 단순히 '머지 버튼'으로만 활용한다면 협업의 가치를 절반 이상 놓치는 것입니다. PR은 &lt;b&gt;코드 리뷰를 시작하는 트리거&lt;/b&gt;이자, 6개월 뒤의 내가 읽을 &lt;b&gt;변경 일기장&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리뷰어가 좋아하는 PR 작성법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;작게 쪼개라:&lt;/b&gt; 1,000줄짜리 PR은 집중력을 흐트러뜨립니다. 200~400줄 내외가 적당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제목은 한 줄로 명확히:&lt;/b&gt; &lt;code&gt;[FIX] 로그인 토큰 만료 시 자동 리프레시 처리&lt;/code&gt;와 같이 타입과 목적을 명시하세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨텍스트를 담아라:&lt;/b&gt; 무엇을(What), 왜(Why), 어떻게(How) 바꿨는지 상세히 기록해야 리뷰어의 시간을 아낄 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id=&quot;merge-rebase&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Merge vs Rebase: 히스토리 통합 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토리를 있는 그대로 보존할 것인가, 아니면 한 줄로 깔끔하게 정리할 것인가의 차이입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Merge:&lt;/b&gt; 두 브랜치의 히스토리를 보존합니다. 누가 어느 시점에 작업했는지 명확하지만 로그가 복잡해질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rebase:&lt;/b&gt; 내 커밋을 대상 브랜치 끝에 옮겨 붙입니다. 로그가 직선형으로 깔끔해지지만, 커밋 해시가 변하므로 주의가 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;warning-box&quot;&gt;&lt;b&gt;  Golden Rule:&lt;/b&gt; 이미 푸시되어 동료들과 공유 중인 브랜치는 절대 &lt;b&gt;Rebase&lt;/b&gt;하지 마세요. 협업 환경을 파괴하는 행위가 될 수 있습니다.&lt;/div&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 충돌(Conflict) 해결 프로세스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌은 에러가 아니라, Git이 &lt;b&gt;사람의 판단을 요청&lt;/b&gt;하는 신호입니다. 빨간 문구에 당황하지 말고 아래 5단계를 따르세요.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상태 확인:&lt;/b&gt; &lt;code&gt;git status&lt;/code&gt;로 충돌 파일 식별&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마커 찾기:&lt;/b&gt; &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD&lt;/code&gt;와 &lt;code&gt;=======&lt;/code&gt; 사이의 코드 대조&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정:&lt;/b&gt; 원하는 코드로 병합 및 마커 제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스테이징:&lt;/b&gt; &lt;code&gt;git add .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커밋:&lt;/b&gt; &lt;code&gt;git commit&lt;/code&gt;으로 머지 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 암기하는 것보다 &quot;왜 이렇게 하는가&quot;의 맥락을 잡는 것이 중요한거 같다.&amp;nbsp;&lt;/p&gt;
&lt;/section&gt;
&lt;/article&gt;</description>
      <category>GIT</category>
      <category>GIT</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/46</guid>
      <comments>https://0321ljh.tistory.com/46#entry46comment</comments>
      <pubDate>Wed, 6 May 2026 11:38:02 +0900</pubDate>
    </item>
    <item>
      <title>MSA프로젝트 시작</title>
      <link>https://0321ljh.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;git 저장소 기본 필수적인 뼈대 6개 생성.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;284&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OyhxA/dJMcagMb2tk/mozNNIwlKG8krZPA4dzKl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OyhxA/dJMcagMb2tk/mozNNIwlKG8krZPA4dzKl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OyhxA/dJMcagMb2tk/mozNNIwlKG8krZPA4dzKl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyhxA%2FdJMcagMb2tk%2FmozNNIwlKG8krZPA4dzKl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;284&quot; height=&quot;485&quot; data-origin-width=&quot;284&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소를 클론 받기 위해 큰 폴더를 만들고 cmd창에 들어가 git clone&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1529&quot; data-origin-height=&quot;1037&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MVcJj/dJMcacXhUv1/Lkt9e7jIkXPm466kAeEnIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MVcJj/dJMcacXhUv1/Lkt9e7jIkXPm466kAeEnIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MVcJj/dJMcacXhUv1/Lkt9e7jIkXPm466kAeEnIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMVcJj%2FdJMcacXhUv1%2FLkt9e7jIkXPm466kAeEnIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1529&quot; height=&quot;1037&quot; data-origin-width=&quot;1529&quot; data-origin-height=&quot;1037&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://start.spring.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://start.spring.io/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 접속을 해 공통 설정을 해준다&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,4,0&quot;&gt;Java:&lt;/b&gt; 17&lt;br /&gt;&lt;br /&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;1. eureka-server&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Eureka Server&lt;/li&gt;
&lt;li&gt;Spring Boot Actuator&lt;/li&gt;
&lt;/ul&gt;
&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10&quot;&gt;2. config-server&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Config Server&lt;/li&gt;
&lt;li&gt;Spring Boot Actuator&lt;/li&gt;
&lt;/ul&gt;
&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12&quot;&gt;3. api-gateway&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gateway&lt;/li&gt;
&lt;li&gt;Eureka Discovery Client&lt;/li&gt;
&lt;li&gt;Config Client&lt;/li&gt;
&lt;li&gt;Spring Boot Actuator&lt;/li&gt;
&lt;/ul&gt;
&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14&quot;&gt;4. user-service&amp;nbsp;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Web&lt;/li&gt;
&lt;li&gt;Spring Data JPA&lt;/li&gt;
&lt;li&gt;PostgreSQL Driver&lt;/li&gt;
&lt;li&gt;Eureka Discovery Client&lt;/li&gt;
&lt;li&gt;Config Client&lt;/li&gt;
&lt;li&gt;OpenFeign&lt;/li&gt;
&lt;li&gt;Spring Boot Actuator&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Lombok&lt;/li&gt;
&lt;/ul&gt;
&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16&quot;&gt;5. common-module (공통 라이브러리)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Web&lt;/li&gt;
&lt;li&gt;Lombok&lt;/li&gt;
&lt;li&gt;&lt;i data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,2,0&quot;&gt;(주의: DB나 클라이언트 관련 라이브러리는 하위 서버들과의 스캔 충돌 방지를 위해 절대 넣지 마세요!)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,0&quot;&gt;  주의사항:&lt;/b&gt; config-hub는 스프링 프로젝트가 아니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;1213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kHpEm/dJMb997ls6f/lKNgob3QLkpJbGwfUO4Ypk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kHpEm/dJMb997ls6f/lKNgob3QLkpJbGwfUO4Ypk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kHpEm/dJMb997ls6f/lKNgob3QLkpJbGwfUO4Ypk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkHpEm%2FdJMb997ls6f%2FlKNgob3QLkpJbGwfUO4Ypk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1193&quot; height=&quot;1213&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;1213&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1092&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjnBDk/dJMcacQwI1A/Be1G6u1dOCbiKrHiinjCqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjnBDk/dJMcacQwI1A/Be1G6u1dOCbiKrHiinjCqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjnBDk/dJMcacQwI1A/Be1G6u1dOCbiKrHiinjCqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjnBDk%2FdJMcacQwI1A%2FBe1G6u1dOCbiKrHiinjCqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1510&quot; height=&quot;1092&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1092&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;Spring Web&lt;/b&gt; (REST API 구현용)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;Spring Data JPA&lt;/b&gt; (DB ORM)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,2,0&quot;&gt;PostgreSQL Driver&lt;/b&gt; (발제문 권장 DB)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,3,0&quot;&gt;Eureka Discovery Client&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,4,0&quot;&gt;Config Client&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,5,0&quot;&gt;OpenFeign&lt;/b&gt; (발제문 요구사항: 서비스 간 통신 기본 구성)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,6,0&quot;&gt;Spring Boot Actuator&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,7,0&quot;&gt;Validation&lt;/b&gt; (발제문 요구사항: 데이터 유효성 검사)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,8,0&quot;&gt;Lombok&lt;/b&gt; (코드 간소화)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,9,0&quot;&gt;Zipkin&lt;/b&gt; (분산 추적)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b data-index-in-node=&quot;3&quot; data-path-to-node=&quot;16,2,0,0&quot;&gt;주의사항:&lt;/b&gt; common-module에는 &lt;b data-index-in-node=&quot;29&quot; data-path-to-node=&quot;16,2,0,0&quot;&gt;절대로 DB(JPA, PostgreSQL)나 클라우드(Eureka, Feign) 관련 의존성을 넣으면 안된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 서비스들이 이 모듈을 가져다 쓸 때 원치 않는 자동 설정(Auto-configuration) 충돌이 발생할 수 있음&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA프로젝트(whereIsMyParcel)</category>
      <category>MSA</category>
      <category>개발 시작</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/45</guid>
      <comments>https://0321ljh.tistory.com/45#entry45comment</comments>
      <pubDate>Wed, 6 May 2026 10:39:57 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 시작</title>
      <link>https://0321ljh.tistory.com/44</link>
      <description>&lt;!-- 티스토리 글쓰기 HTML 모드에 붙여넣으세요 --&gt;
&lt;div&gt;
&lt;style&gt;
    /* 블로그 스타일 최적화 */
    .msa-post { line-height: 1.8; color: #333; max-width: 900px; margin: 0 auto; font-family: 'Pretendard', sans-serif; }
    .msa-post h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; margin-bottom: 30px; }
    .msa-post h2 { color: #2980b9; margin-top: 50px; border-left: 5px solid #2980b9; padding-left: 15px; font-size: 1.6em; }
    .msa-post h3 { color: #16a085; margin-top: 35px; font-size: 1.3em; }
    
    .toc { background: #f8f9fa; border: 1px solid #e9ecef; padding: 20px; border-radius: 10px; margin-bottom: 40px; }
    .toc h3 { margin-top: 0; color: #495057; font-size: 1.1em; }
    .toc ul { list-style: none; padding-left: 5px; margin-bottom: 0; }
    .toc li { margin-bottom: 8px; }
    .toc a { text-decoration: none; color: #495057; transition: 0.2s; }
    .toc a:hover { color: #3498db; font-weight: bold; }
    
    .code-block { background: #2d3436; color: #dfe6e9; padding: 20px; border-radius: 10px; font-family: 'Fira Code', 'Consolas', monospace; overflow-x: auto; margin: 20px 0; position: relative; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
    .code-header { font-size: 0.85em; color: #b2bec3; margin-bottom: 12px; border-bottom: 1px solid #636e72; padding-bottom: 8px; display: block; }
    
    .warning-box { background-color: #fff5f5; border-left: 5px solid #ff7675; padding: 20px; margin: 25px 0; border-radius: 4px; }
    .warning-box strong { color: #d63031; display: block; margin-bottom: 5px; }
    
    .info-box { background-color: #e7f3fe; border-left: 5px solid #3498db; padding: 20px; margin: 25px 0; border-radius: 4px; }
    .info-box strong { color: #0984e3; display: block; margin-bottom: 5px; }
    
    .step-badge { display: inline-block; padding: 4px 12px; background: #3498db; color: #fff; border-radius: 20px; font-size: 0.9em; font-weight: bold; margin-bottom: 10px; }
    .msa-table { width: 100%; border-collapse: collapse; margin: 25px 0; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
    .msa-table th, .msa-table td { border: 1px solid #eee; padding: 15px; text-align: left; }
    .msa-table th { background-color: #f1f3f5; color: #495057; font-weight: bold; }
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;msa-post&quot;&gt;
&lt;h1&gt;[MSA] wherelsMyParcel 프로젝트 시작 Actuator 검증&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA(Microservice Architecture) 프로젝트 &lt;b&gt;'DelivHub'&lt;/b&gt;의 첫 삽을 떴습니다. 비즈니스 로직을 구현하기 전, 가장 먼저 진행한 작업은 팀원들이 안정적으로 개발할 수 있는 인프라 기반을 닦고, 서비스의 생존 여부를 확인할 수 있는 지표를 확보하는 것이었습니다.&lt;/p&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Contents&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#setup&quot;&gt;1. 인프라 초기 설정: 왜 Java 21인가?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#structure&quot;&gt;2. 서비스별 의존성(Dependency) 설계 전략&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#actuator&quot;&gt;3. 실전 구축: Actuator를 활용한 Health Check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#troubleshooting&quot;&gt;4. 트러블슈팅: 라이브러리 제거와 코드 정합성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;5. 마치며: 기초가 튼튼한 MSA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;section id=&quot;setup&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인프라 초기 설정: 왜 Java 21인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 핵심 스택을 결정하는 것은 성능과 유지보수성을 좌우합니다. 이번 DelivHub 프로젝트는 &lt;b&gt;Java 21&lt;/b&gt;과 &lt;b&gt;Spring Boot 3.3.x&lt;/b&gt;를 선택했습니다.&lt;/p&gt;
&lt;table class=&quot;msa-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;선택한 설정값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Project / Language&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Gradle - Groovy / Java 21 (LTS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Spring Boot&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3.3.x (Stable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Packaging / DB&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Jar / PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;b&gt;  Senior's Insight: Java 21을 선택한 전략적 이유&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Java 17:&lt;/b&gt; 안정적이지만 최신 기능(Virtual Threads 등)을 활용할 수 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Java 25(최신):&lt;/b&gt; 성능은 기대되나, 아직 서드파티 라이브러리(Spring AI, Zipkin 등)와의 호환성 버그 가능성이 있어 부트캠프나 단기 프로젝트에선 리스크가 큽니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Java 21:&lt;/b&gt; 현재 실무에서 가장 권장되는 최신 LTS 버전으로, 레퍼런스가 풍부하고 호환성이 완벽히 검증된 가장 합리적인 선택입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;structure&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 서비스별 의존성(Dependency) 설계 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5개의 마이크로서비스가 유기적으로 움직이기 위해 각 서비스의 책임에 맞는 의존성만 깔끔하게 할당했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 서비스 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Eureka Server:&lt;/b&gt; 서비스들의 위치를 관리하는 전화번호부 역할&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Config Server:&lt;/b&gt; 모든 서비스의 설정값(.yml)을 중앙에서 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API Gateway:&lt;/b&gt; 클라이언트의 요청을 적절한 서비스로 라우팅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;User-Service:&lt;/b&gt; 사용자 비즈니스 로직 담당 (템플릿 서버)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Common-Module:&lt;/b&gt; 전 서비스에서 공통으로 사용할 유틸리티 및 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;warning-box&quot;&gt;&lt;b&gt;  주의사항: Common-Module 설계의 핵심&lt;/b&gt; common-module에는 &lt;b&gt;절대로 DB(JPA, PostgreSQL)나 클라우드(Eureka, Feign) 관련 의존성을 넣어서는 안 됩니다.&lt;/b&gt; &lt;br /&gt;공통 모듈에 무거운 의존성이 포함되면, 이를 참조하는 다른 서비스들에서 '원치 않는 자동 설정(Auto-configuration)' 충돌이 발생해 빌드 오류가 남.&lt;/div&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;b&gt;❓ Config-Hub는 무엇인가요?&lt;/b&gt; 스프링 프로젝트가 아닌 &lt;b&gt;'설정 데이터베이스(보관소)'이다.&lt;br /&gt;&lt;/b&gt;실제 코드 없이 &lt;code&gt;user-service.yml&lt;/code&gt; 같은 설정 파일만 담고 있으며, Config Server가 이곳에서 데이터를 읽어와 각 서비스에 전달&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;actuator&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실전 구축: Actuator를 활용한 Health Check&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 환경에선 서비스 하나가 죽으면 도미노처럼 장애가 번집니다. 이를 방지하기 위해 &lt;b&gt;Spring Boot Actuator&lt;/b&gt;를 도입해 가동 지표를 확보했습니다.&lt;/p&gt;
&lt;span class=&quot;step-badge&quot;&gt;Step 1: 포트 규칙 정의&lt;/span&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌 방지를 위해 팀원들과 포트 규칙을 미리 정했습니다. &lt;b&gt;User-Service는 8081&lt;/b&gt;을 사용합니다.&lt;/p&gt;
&lt;span class=&quot;step-badge&quot;&gt;Step 2: 의존성 및 설정&lt;/span&gt;&lt;/section&gt;
&lt;section&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;&lt;span class=&quot;code-header&quot;&gt;application.yaml&lt;/span&gt; server:&lt;br /&gt;&amp;nbsp;&amp;nbsp;port: 8081&lt;br /&gt;&lt;br /&gt;management:&lt;br /&gt;&amp;nbsp;&amp;nbsp;endpoints:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;web:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exposure:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;include: health&lt;br /&gt;&amp;nbsp;&amp;nbsp;endpoint:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;health:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;show-details: always&lt;/div&gt;
&lt;/section&gt;
&lt;section id=&quot;troubleshooting&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 트러블슈팅: 라이브러리 제거와 코드 정합성&lt;/h2&gt;
&lt;div class=&quot;warning-box&quot;&gt;&lt;b&gt;❌ 이슈 발생: 컴파일 오류 (Symbol not found)&lt;/b&gt; 의존성을 경량화하기 위해 &lt;code&gt;build.gradle&lt;/code&gt;에서 Spring Cloud 관련 라이브러리를 제거했는데, 코드에는 여전히 &lt;code&gt;@EnableDiscoveryClient&lt;/code&gt; 어노테이션이 남아있어 발생한 문제입니다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lesson Learned:&lt;/b&gt; 라이브러리를 걷어낼 때는 설정 파일(Gradle/Maven)뿐만 아니라 소스 코드 내의 &lt;b&gt;Annotation과 Import 문&lt;/b&gt;까지 세트로 정리해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;/section&gt;
&lt;section id=&quot;conclusion&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마치며: 기초가 튼튼한 MSA&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 기동 후 &lt;code&gt;GET http://localhost:8081/actuator/health&lt;/code&gt;를 통해 &lt;b&gt;&quot;status&quot;: &quot;UP&quot;&lt;/b&gt;이라는 응답을 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;&lt;span class=&quot;code-header&quot;&gt;Success Response&lt;/span&gt; {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;status&quot;: &quot;UP&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;components&quot;: {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;diskSpace&quot;: { &quot;status&quot;: &quot;UP&quot;, ... },&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;ping&quot;: { &quot;status&quot;: &quot;UP&quot; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/section&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1939&quot; data-origin-height=&quot;1354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7htbU/dJMcacQwNAj/0SwdFbpzFgyKK3WKRdFscK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7htbU/dJMcacQwNAj/0SwdFbpzFgyKK3WKRdFscK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7htbU/dJMcacQwNAj/0SwdFbpzFgyKK3WKRdFscK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7htbU%2FdJMcacQwNAj%2F0SwdFbpzFgyKK3WKRdFscK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1939&quot; height=&quot;1354&quot; data-origin-width=&quot;1939&quot; data-origin-height=&quot;1354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;</description>
      <category>MSA프로젝트(whereIsMyParcel)</category>
      <category>MSA</category>
      <category>프로젝트</category>
      <author>0321ljh</author>
      <guid isPermaLink="true">https://0321ljh.tistory.com/44</guid>
      <comments>https://0321ljh.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 6 May 2026 10:37:28 +0900</pubDate>
    </item>
  </channel>
</rss>