[{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/build-error/","section":"Tags","summary":"","title":"Build-Error","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"로컬에서 정상 빌드되던 Hugo·Blowfish 블로그가 Cloudflare Pages에서는 다음 오류로 실패했다.\ncan\u0026#39;t evaluate field Locale in type *langs.Language 이번 원인은 글이나 테마 파일의 문법 오류가 아니라 로컬과 Cloudflare 빌드 환경의 Hugo 버전 불일치였다. Cloudflare에 HUGO_VERSION을 지정한 뒤 재배포해 해결했다.\n1. 실패 로그에서 오류와 버전 경고 확인 # Cloudflare Pages의 Deployments → 실패한 배포 → Build log를 열어 로그를 확인한다. 이번 실패 로그의 핵심 부분은 다음과 같다.\nhugo v0.147.7 ... +extended WARN Module \u0026#34;blowfish\u0026#34; is not compatible with this Hugo version: 0.163.0/0.166.0 extended can\u0026#39;t evaluate field Locale in type *langs.Language Failed: build command exited with code: 1 위 로그는 버전과 오류 메시지를 중심으로 발췌했다. 0.163.0/0.166.0은 이때 사용한 Blowfish가 선언한 Hugo 최소·최대 버전이며, 실행된 Hugo 버전은 앞줄의 0.147.7이다.\n홈·404·카테고리 등 여러 페이지에서 같은 오류가 반복됐다. 공통 템플릿을 렌더링하는 과정에서 실패한 것이므로, 오류에 나열된 글 파일을 각각 수정할 문제가 아니었다.\nFailed: build command exited with code: 1은 빌드 실패 결과다. 원인을 찾을 때는 그 앞의 실제 오류 메시지와 비호환 경고를 함께 읽어야 한다.\n2. 로컬과 Cloudflare의 Hugo 버전 비교 # 로컬 터미널에서 사용 중인 버전을 확인한다.\nhugo version 이 블로그의 로컬·기존 GitHub Actions 환경과 실패한 배포의 차이는 다음과 같았다.\n확인 대상 Hugo 버전 로컬·기존 GitHub Actions 0.165.0 Extended 실패한 Cloudflare 빌드 0.147.7 Extended 사용한 Blowfish의 최소 요구 버전 0.163.0 Extended 두 환경 모두 Extended였지만 Cloudflare의 버전이 요구사항보다 낮았다. Extended 사용 여부만 확인해서는 이 문제를 발견할 수 없다.\n템플릿이 참조하는 Locale을 해당 Hugo 버전에서 처리하지 못했고, 모듈 호환성 경고도 같은 방향을 가리켰다. 따라서 테마의 Locale 참조를 삭제하는 대신 배포 환경을 정상 동작하던 Hugo 버전에 맞췄다.\n위 0.147.7은 이번 실패 당시 실제 실행된 값이다. Cloudflare의 기본 Hugo 버전이 모든 시점·프로젝트에서 같다는 의미는 아니다. 다른 환경에서도 먼저 자신의 빌드 로그를 확인한다.\n3. HUGO_VERSION 지정 후 재배포 # Cloudflare의 해당 Pages 프로젝트 → Settings → Variables and secrets에서 실패한 배포가 사용하는 환경을 선택한다. 이번 배포는 운영 브랜치의 Production 환경이다.\n다음 변수를 등록한다.\n변수 값 역할 HUGO_VERSION 0.165.0 Cloudflare 빌드에서 사용할 Hugo 버전 지정 HUGO_ENVIRONMENT production Hugo의 실행 환경 구분 이번 버전 오류를 해결하는 설정은 HUGO_VERSION이다. HUGO_ENVIRONMENT=production만 추가해도 Hugo 버전이 바뀌는 것은 아니다.\n0.165.0은 이 블로그에서 정상 동작을 확인한 버전이다. 다른 테마를 사용한다면 무조건 이 값을 복사하기보다 로컬에서 검증한 버전과 테마 요구사항에 맞춰 지정한다. Cloudflare도 특정 Hugo 버전이 필요하면 HUGO_VERSION 환경변수를 사용하도록 안내한다. Hugo 버전 지정 공식 문서\n설정을 저장한 뒤 Deployments → 실패한 배포의 메뉴 → Retry deployment를 실행한다. 변수 저장만으로 기존 빌드 결과가 바뀌지는 않으므로 새 빌드가 필요하다.\nPreview 배포에서만 같은 오류가 발생한다면 Preview 환경에도 변수가 설정되어 있는지 확인한다. Production에 등록한 값이 Preview에도 적용됐다고 가정하지 않는다.\n4. 새 빌드 로그와 배포 결과 확인 # 새 로그에서 Hugo 실행 버전이 설정값과 일치하는지, 테마가 요구하는 Extended 빌드인지 확인한다. 이 프로젝트의 기준은 0.165.0과 +extended 표시다.\n이후 템플릿 렌더링과 정적 파일 업로드까지 완료됐는지 확인한다. 이번 재시도에서는 저장소 복제·빌드·배포가 모두 성공했다.\n환경변수를 바꿨는데도 실패한다면 다음을 구분해서 확인한다.\n로그가 계속 이전 버전: 변수 이름·값, Production·Preview 적용 환경, 새 배포를 실행했는지 확인한다. 버전은 맞지만 같은 오류: 빌드가 사용하는 테마·서브모듈 커밋과 로컬 구성이 같은지 확인한다. 이 글의 사례와 원인이 같다고 단정하지 않는다. 다른 오류로 변경됨: 새 로그의 오류를 기준으로 다시 점검한다. --minify 추가나 DNS 변경은 Hugo 버전 불일치의 해결 방법이 아니다. 이번에는 버전 설정으로 해결되어 테마 소스 수정이나 커스텀 도메인 변경이 필요하지 않았다.\n빌드가 성공했는데 특정 글만 보이지 않는다면 별도 문제다. 배포 대상 커밋에 글과 이미지가 포함됐는지, 글의 draft가 false인지, 작성일이 빌드 시점보다 미래인지 확인한다. 기본 hugo --minify 빌드는 초안과 미래 날짜의 글을 게시하지 않는다. 로컬의 hugo server -D와 게시 결과가 다를 수 있는 이유다. Hugo 빌드 옵션\nCloudflare Pages의 전체 배포 설정은 GitHub 연동과 Hugo 빌드 설정에서 확인할 수 있다.\n","date":"2026년 10월 21일","externalUrl":null,"permalink":"/posts/cloudflare-pages-hugo-locale-build-error/","section":"글 목록","summary":"","title":"Cloudflare Pages Hugo 빌드 오류 해결: can't evaluate field Locale","type":"posts"},{"content":"GitHub에 저장한 Hugo 소스를 Cloudflare Pages에서 빌드하고 pages.dev 주소로 배포했다. 기존 GitHub Pages는 유지한 상태에서 새 배포를 먼저 검증했다.\n이 글은 GitHub 저장소 연결, 첫 배포, 이후 push에 따른 자동 배포까지 다룬다. 신규 사이트에도 같은 순서를 적용할 수 있다. 기존 운영 도메인과 DNS는 여기서 변경하지 않는다.\n1. 배포 환경과 사전 확인 # 항목 사용한 값 소스 저장소 jinosoft/jinosoft-blog 운영 브랜치 main 정적 사이트 생성기 Hugo 0.165.0 Extended 테마 Blowfish, Git 서브모듈 빌드 출력 디렉터리 public 기본 배포 주소 https://jinosoft-blog.pages.dev/ 로컬에서 다음 명령으로 빌드가 가능한 저장소를 준비한다.\nhugo version hugo --minify 이번 저장소의 themes/blowfish는 .gitmodules에 등록된 공개 서브모듈이다. Cloudflare의 복제 로그에서도 해당 서브모듈을 체크아웃한 것을 확인했다. 외부 테마 파일을 로컬에만 두고 Git에 등록하지 않았다면 배포 환경에서 읽을 수 없으므로 먼저 확인한다.\n2. Workers 생성 화면에서 Pages로 이동 # Cloudflare에 로그인하고 Workers \u0026amp; Pages → Create application으로 이동한다.\n이번 화면에서는 GitHub 연결 버튼이 먼저 보였지만, 하단의 Continue to Pages를 선택해야 Pages 생성 절차로 이동했다.\nDeploy command에 npx wrangler deploy, Preview command에 npx wrangler preview가 표시된다면 Workers 빌드 설정 화면이다. Workers로도 정적 사이트를 배포할 수 있지만, 이 글은 Cloudflare Pages의 Git 연동을 사용한다. 명령만 바꾸지 말고 Pages 생성 화면으로 이동한다. Workers 빌드 설정\nPages 화면에서 Import an existing Git repository → Get started를 선택한다. 메뉴 이름이나 진입 경로는 UI 변경에 따라 달라질 수 있다. Pages Git 연동 안내\n이 과정에서 만드는 것은 **Pages 프로젝트(project)**다. 프로젝트에 저장소·빌드 명령·도메인을 설정하고, 빌드를 실행할 때마다 배포(deployment) 기록이 생성된다. 글을 올릴 때마다 새 프로젝트를 만드는 구조는 아니다.\n3. GitHub 저장소 접근 권한 설정 # GitHub 연결 과정에서 Cloudflare의 GitHub App 설치·권한 요청이 표시되면 Only select repositories를 선택하고 배포할 저장소만 지정한다.\n이번에는 jinosoft/jinosoft-blog만 선택한 뒤 Install \u0026amp; Authorize를 진행했다. GitHub App 설치 권한과 로컬 git push에 사용하는 PAT는 별개다. 이 작업을 위해 기존 PAT를 Cloudflare 입력란에 붙여 넣지 않는다.\nCloudflare로 돌아와 저장소를 선택하고 Begin setup을 클릭한다.\n저장소가 표시되지 않으면 Cloudflare Pages용 GitHub App에 해당 저장소 접근 권한을 부여했는지 확인한다. Public·Private 저장소 모두 연동할 수 있지만, Private 저장소는 앱의 접근 권한이 있어야 한다.\n4. Hugo 빌드 설정 # 프로젝트 이름과 운영 브랜치, 빌드 설정을 지정한다.\n항목 설정값 Project name jinosoft-blog Production branch main Framework preset Hugo Build command hugo --minify Build output directory public Root directory 저장소 루트, 기본값 유지 초기 캡처에는 프리셋의 기본 명령인 hugo가 표시되어 있다. 이후 Settings → Build configuration에서 hugo --minify로 변경했다. --minify는 생성 결과를 최소화하는 옵션이며, 다음 절의 버전 불일치 오류를 해결하는 옵션은 아니다.\npublic은 저장소의 소스 디렉터리가 아니라 Hugo 빌드 결과가 생성되는 경로다. 빌드 후 Cloudflare가 이 디렉터리를 정적 사이트로 배포한다.\n이 블로그는 최종 운영 주소를 유지할 예정이므로 baseURL을 변경하지 않았다.\nbaseURL = \u0026#34;https://blog.jinosoft.com/\u0026#34; 새 사이트라면 자신의 최종 공개 주소에 맞춰 설정한다. pages.dev를 최종 주소로 사용할지, 커스텀 도메인을 연결할지에 따라 결정한다. 이 블로그처럼 커스텀 도메인을 유지할 경우 빌드 명령에 --baseURL $CF_PAGES_URL을 무조건 추가하면 표준 URL이 임시 주소로 바뀔 수 있다.\n5. Hugo 버전 지정과 Locale 오류 해결 # 첫 배포는 빌드 단계에서 실패했다. 로그에는 다음 버전과 오류가 표시됐다.\nhugo v0.147.7 ... +extended WARN Module \u0026#34;blowfish\u0026#34; is not compatible with this Hugo version can\u0026#39;t evaluate field Locale in type *langs.Language 로컬과 기존 GitHub Actions는 Hugo 0.165.0 Extended를 사용했지만, 이번 Cloudflare 첫 빌드는 0.147.7 Extended로 실행됐다. 테마가 요구하는 Hugo 버전을 충족하지 못한 것이 원인이었다. 이 오류를 해결하려고 테마의 Locale 코드를 삭제할 필요는 없다.\n프로젝트의 Settings → Variables and secrets에서 Production 환경에 다음 변수를 추가한다.\n변수 값 목적 HUGO_VERSION 0.165.0 사용할 Hugo 버전 지정 HUGO_ENVIRONMENT production Hugo 실행 환경 명시 버전 오류 해결에 직접 필요한 값은 HUGO_VERSION이다. HUGO_ENVIRONMENT는 테마·템플릿이 참조하는 환경 구분이며 Hugo 버전을 바꾸지 않는다.\n저장한 뒤 Deployments에서 실패한 배포의 Retry deployment를 실행한다. 환경변수는 이미 완료된 배포에 소급 적용되지 않으므로 새 빌드가 필요하다. 빌드 로그에서 실제 실행 버전과 +extended 표시를 확인한다. Preview 브랜치에도 같은 구성이 필요하다면 Preview 환경의 변수도 별도로 설정한다. Hugo 버전 지정 공식 안내\n실패 로그 해석과 버전 확인 절차는 can\u0026rsquo;t evaluate field Locale 오류 해결에 별도로 정리했다.\n6. 배포 성공과 pages.dev 접속 확인 # 변수 지정 후 재시도한 배포는 저장소 복제·빌드·업로드가 모두 성공했다.\n로그의 다음 안내는 이번 정적 사이트 배포에서 실패 원인이 아니었다.\nNo Wrangler configuration file found. Continuing. No functions dir at /functions found. Skipping. 이번 구성은 Pages Functions나 Wrangler 설정 없이 Hugo의 정적 결과물만 배포했다.\n배포 주소를 브라우저에서 열어 홈 화면과 기존 글이 표시되는지 확인한다.\n최종 도메인이 아직 기존 호스팅을 가리킨다면 절대 URL 링크를 클릭할 때 기존 사이트로 이동할 수 있다. 새 호스팅의 검증은 주소창의 호스트도 확인하면서 pages.dev의 글 경로를 직접 열어 진행한다.\nhttps://jinosoft-blog.pages.dev/ https://jinosoft-blog.pages.dev/posts/docker-volume-data-persistence/ 홈 화면만으로 끝내지 말고 글 본문, 이미지·CSS, 검색 기능도 확인한다. 이번 단계에서는 새 호스팅 배포를 확인했으며, 기존 도메인의 DNS나 저장소 공개 범위는 아직 변경하지 않았다.\n7. 이후 글 수정은 push하면 자동 배포 # GitHub와 연동한 Pages 프로젝트는 운영 브랜치에 새 커밋이 push되면 자동으로 빌드·배포한다. 운영 브랜치를 main으로 지정하고 자동 배포를 활성화한 이 구성에서는 매번 Cloudflare에서 배포 버튼을 누를 필요가 없다. Cloudflare Git 연동 동작\n글을 작성한 뒤 반영되는 순서는 다음과 같다.\n로컬에서 글·이미지를 수정하고 변경 파일을 commit한다. git push origin main으로 GitHub의 main 브랜치에 올린다. Cloudflare가 변경을 감지해 저장소와 테마 서브모듈을 복제한다. 설정된 hugo --minify를 실행하고 public의 결과물을 배포한다. 배포가 성공하면 프로젝트의 운영 주소에 변경 내용이 반영된다. 로컬 파일 저장이나 commit만으로는 배포되지 않는다. GitHub로 push해야 하며, push 성공과 Cloudflare 배포 성공도 별개다. Pages의 Deployments에서 해당 커밋·브랜치와 빌드 결과를 확인한다. Hugo 빌드가 실패하면 수정 내용은 게시되지 않는다.\npublic을 로컬에서 생성해 업로드하거나 Git에 추가할 필요는 없다. 이 구성은 Cloudflare가 Hugo를 실행하므로 별도 GitHub Actions 배포 워크플로도 필요하지 않다. 커스텀 도메인을 연결한 뒤에는 같은 자동 배포 결과가 그 주소에도 반영된다.\nmain과 다른 브랜치는 어떻게 처리될까 # main은 Production 배포 대상이다. 다른 브랜치는 Preview 배포가 활성화되어 있으면 별도의 미리보기 URL로 배포되며, 운영 사이트를 바로 교체하지 않는다. 이후 변경 내용을 main에 merge하면 운영 브랜치가 갱신되어 Production 배포가 실행된다.\npush해도 배포 기록이 생기지 않으면 프로젝트 설정의 Branch control에서 운영 브랜치와 Enable automatic production branch deployments를 확인한다. Preview 배포 대상도 이곳에서 지정한다. GitHub App의 저장소 접근 권한이나 빌드 건너뛰기 설정 역시 확인할 항목이다. 브랜치별 자동 배포 설정\nRetry deployment는 실패 원인을 수정한 뒤 같은 커밋을 다시 빌드할 때 사용한다. 평소 글을 수정할 때는 새 커밋을 push하면 된다.\n마무리 # GitHub 저장소와 Blowfish 서브모듈을 Cloudflare에서 복제하고, Hugo 버전을 맞춰 pages.dev 배포를 완료했다. 이후에는 GitHub에 소스를 올리고 Cloudflare의 배포 결과를 확인하는 방식으로 운영한다.\n서비스 선택 기준은 GitHub Pages와 Cloudflare Pages 비교, 기존 도메인과 Private 저장소 전환은 GitHub Pages에서 Cloudflare Pages로 이전하기에서 확인할 수 있다.\n","date":"2026년 10월 21일","externalUrl":null,"permalink":"/posts/deploy-hugo-cloudflare-pages/","section":"글 목록","summary":"","title":"Cloudflare Pages에 Hugo 블로그 배포하기: GitHub 연동과 빌드 설정","type":"posts"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/cloudflare-pages/","section":"Tags","summary":"","title":"Cloudflare-Pages","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/deployment/","section":"Tags","summary":"","title":"Deployment","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/dns/","section":"Tags","summary":"","title":"Dns","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/github/","section":"Tags","summary":"","title":"Github","type":"tags"},{"content":"GitHub 안에서 공개 프로젝트 문서와 배포를 함께 관리하려면 GitHub Pages가 간편하고, 무료로 Private 저장소를 연결하거나 브랜치별 미리보기를 사용하려면 Cloudflare Pages가 유리하다. 두 서비스 모두 정적 사이트를 자동 배포할 수 있지만, 저장소 공개 범위와 배포 관리 방식에 따라 선택이 달라진다.\n이 블로그는 GitHub Pages를 사용하다가 소스 저장소를 Private으로 전환하면서 Cloudflare Pages를 선택했다. 글과 이미지는 공개하되 원본 Markdown·설정·커밋 기록은 비공개로 관리하려는 목적이었다. 이 사례와 함께, GitHub Pages를 유지하는 편이 나은 조건도 비교한다.\n1. 저장소와 웹 호스팅을 분리해서 보기 # 두 서비스 모두 Hugo가 생성한 HTML·CSS·JavaScript·이미지를 제공할 수 있다. 차이는 소스를 보관하는 위치보다 어디서 빌드하고 배포하는지에 있다.\n구성 소스 저장 빌드·배포 방문자가 접속하는 서비스 GitHub Pages + Hugo GitHub 저장소 GitHub Actions의 사용자 정의 워크플로 GitHub Pages Cloudflare Pages + Hugo GitHub 저장소 Cloudflare Pages의 Git 연동 빌드 Cloudflare Pages 이 표의 GitHub Pages 구성은 이 블로그에서 사용하던 방식이다. GitHub Pages의 모든 배포가 같은 워크플로를 요구하는 것은 아니다.\nCloudflare Pages로 이전하더라도 소스는 GitHub에 그대로 둘 수 있다. 저장소를 다른 Git 서비스로 옮기거나 로컬 Git 사용법을 바꿀 필요는 없다. Cloudflare Git 연동\n두 구성 모두 push를 계기로 자동 배포할 수 있다. Cloudflare Pages도 Git 연동과 운영 브랜치 자동 배포를 설정하면 GitHub push → Cloudflare 빌드 → 사이트 배포로 이어진다. 호스팅을 옮긴다고 매번 파일을 수동 업로드해야 하는 것은 아니다.\n2. 무료 플랜에서 비교할 항목 # 2026년 10월 21일 공식 문서 확인 기준이다. 정적 사이트 호스팅을 비교하며, 도메인 구매비·유료 플랜·외부 DB와 서버 API 비용은 포함하지 않는다.\n항목 GitHub Pages — GitHub Free Cloudflare Pages — Free Public 저장소 배포 지원 지원 Private 저장소 배포 미지원, GitHub Pro 등 지원 플랜 필요 Git 연동으로 지원 GitHub push 기반 배포 Actions 등으로 구성 연결한 브랜치의 push로 자동 빌드·배포 기본 도메인 github.io pages.dev 커스텀 도메인·HTTPS 지원 지원 빌드 제한 시간당 10회 소프트 제한. 사용자 정의 Actions 워크플로에는 이 제한 미적용 월 500회, 계정당 동시 빌드 1개 빌드·배포 시간 제한 Pages 배포 10분 빌드 20분 정적 사이트 용량 제한 게시 사이트 최대 1GB 사이트당 최대 20,000개 파일, 파일당 최대 25MiB 트래픽·요청 월 100GB 대역폭 소프트 제한 Functions를 호출하지 않는 정적 자산 요청은 무료·무제한 근거: GitHub Pages 이용 조건, GitHub Pages 제한, Cloudflare Pages 제한, Cloudflare 정적 자산과 Functions 요금 구분.\n한도의 기준은 서로 다르다. GitHub의 사이트 전체 용량과 Cloudflare의 파일 수·개별 파일 크기를 같은 제한으로 비교하면 안 된다. 이미지가 많다면 파일 수를, 큰 다운로드 파일을 제공한다면 개별 파일 크기를 확인한다.\nCloudflare Pages의 정적 자산 요청과 Pages Functions 실행도 구분해야 한다. 서버 측 처리를 추가하면 Functions 요청은 Workers 플랜의 사용량·요금 체계를 따른다. 정적 HTML 블로그의 조건을 서버 API에 그대로 적용할 수는 없다.\n3. GitHub Pages가 유리한 목적 # GitHub 안에서 소스와 배포를 함께 관리 # 이미 GitHub에서 코드·이슈·PR을 관리한다면 Pages까지 같은 서비스에서 운영할 수 있다. 별도 Cloudflare 계정과 GitHub App 접근 권한을 관리할 필요가 줄어든다. 관리할 서비스 수를 최소화하려는 공개 프로젝트 문서나 개인 사이트에 적합하다.\n저장소마다 프로젝트 사이트를 만들 수 있어, 코드와 사용 설명서를 같은 저장소에서 관리하기에도 편하다. 기본 주소는 \u0026lt;owner\u0026gt;.github.io/\u0026lt;repositoryname\u0026gt;/ 형태다. GitHub Pages 사이트 유형\n기존 GitHub Actions 절차 활용 # 이미 Actions에서 테스트와 문서 생성을 실행한다면, 같은 워크플로에 Pages 배포를 연결할 수 있다. 예를 들어 링크 검사 → Hugo 빌드 → 성공한 결과 배포를 작업 의존관계로 구성하고 YAML 파일로 버전 관리할 수 있다. Pages 사용자 정의 워크플로\nCloudflare에서도 빌드 스크립트나 외부 CI를 활용할 수 있다. 여기서 GitHub Pages의 이점은 기능의 독점이 아니라 이미 운영 중인 Actions 절차와 관리 화면을 그대로 활용한다는 점이다. 반대로 Hugo 빌드 명령 하나면 충분하고 워크플로를 직접 관리하고 싶지 않다면 Cloudflare의 Git 연동이 간편할 수 있다.\n단순한 정적 문서를 바로 게시 # HTML 파일이나 기본 Jekyll 구성이라면 게시할 브랜치와 루트 또는 /docs 폴더를 지정하는 방식으로 시작할 수 있다. 다만 이 글의 Hugo 구성은 빌드가 필요하므로 사용자 정의 Actions 워크플로를 사용했다. GitHub Pages 게시 소스 설정\n4. Cloudflare Pages가 유리한 목적 # 무료로 Private 저장소 기반 사이트 운영 # GitHub Free 계정에서 저장소는 Private으로 유지하고 사이트는 공개하려면 Cloudflare Pages의 Git 연동을 사용할 수 있다. GitHub Pages는 Private 저장소에 대해 지원되는 유료 플랜이 필요하다. 원본 소스의 공개 여부가 핵심 요구사항인 경우에 실질적인 차이가 생긴다.\n변경 내용을 브랜치별로 미리보기 # Cloudflare Pages는 Git 연동 시 운영 브랜치 외의 변경을 Preview URL로 배포할 수 있다. 본문·테마 수정을 운영 사이트에 반영하기 전에 브라우저에서 검토하려는 경우에 유용하다. Preview 배포 대상 브랜치는 설정으로 제한할 수 있다. Cloudflare Git 연동과 Preview\nGitHub Pages에서도 별도 저장소·경로와 Actions 등을 조합해 검토 환경을 만들 수 있지만, Cloudflare Pages처럼 Git 연동에 포함된 브랜치별 Preview 기능과는 구분해야 한다.\n정적 자산 요청이 무료·무제한이라는 점도 트래픽이 많은 사이트의 검토 요소다. 다만 파일 크기·개수와 빌드 한도는 따로 적용되며, 이 조건이 속도나 가용성의 우위를 보장하는 것은 아니다.\n5. 이 블로그가 이전한 이유: Private 저장소 # 이 블로그 저장소를 Private으로 전환한 뒤 GitHub Pages 설정에는 다음 안내가 표시됐다.\nGitHub Pro·Team 등 지원 플랜을 사용하면 Private 저장소에서도 GitHub Pages를 운영할 수 있다. 따라서 Private 저장소가 필요하다는 이유만으로 반드시 이전해야 하는 것은 아니다. GitHub 유료 플랜을 사용할지, 외부 호스팅을 연결할지를 선택하는 문제다.\n이번에는 GitHub 저장소는 유지하고 Cloudflare Pages에 배포하는 방식을 선택했다. 이전 후 저장소를 Private으로 바꾸고, 그 상태에서도 Cloudflare의 저장소 복제·빌드·배포가 성공하는지 확인했다.\n단, 저장소 비공개와 콘텐츠 비공개는 다르다. Private 전환은 원본 저장소에 대한 일반 접근을 제한한다. 웹에 공개된 글·이미지·HTML은 방문자가 계속 열람할 수 있고, 기존에 생성된 공개 fork나 clone도 회수되지 않는다. GitHub 저장소 공개 범위 변경\n6. 목적별 선택 기준 # 요구사항 먼저 검토할 선택 공개 프로젝트의 코드·문서·배포를 GitHub에서 함께 관리 GitHub Pages 테스트·문서 생성용 Actions 워크플로를 이미 운영 GitHub Pages를 기존 절차에 연결 GitHub 유료 플랜을 사용 중이고 현재 배포에 문제가 없음 Private 저장소 지원 여부를 확인한 뒤 GitHub Pages 유지 GitHub Free에서 소스는 Private, 웹사이트는 Public으로 운영 Cloudflare Pages 브랜치별 Preview URL로 수정 내용을 검토 Cloudflare Pages의 Git 연동 공개 저장소의 소규모 정적 블로그, 특별한 배포 요구 없음 둘 다 가능. 익숙한 관리 방식과 한도를 기준으로 선택 호스트 디렉터리·커널·상주 프로세스까지 직접 관리 두 서비스가 아니라 VM·서버 호스팅 등 검토 서비스 이전만으로 검색 순위나 응답 속도가 개선된다고 가정하지 않는다. 이 비교에서는 동일 조건의 성능 측정을 하지 않았다. 현재 구성이 요구사항을 만족하면 이전 작업을 추가할 필요도 없다.\n또한 정적 사이트를 제공할 수 있다는 사실이 모든 상업적 용도의 허용을 뜻하지는 않는다. GitHub Pages는 온라인 사업·전자상거래·상업용 SaaS 등의 사용에 별도 제한을 둔다. 수익화 목적이라면 실제 서비스 형태와 각 제공자의 이용약관을 함께 확인한다. GitHub Pages 제품 약관\n마무리 # GitHub에서 공개 프로젝트와 배포를 함께 관리하는 목적이라면 GitHub Pages를, 무료 Private 저장소 연동이나 브랜치별 Preview가 필요하다면 Cloudflare Pages를 먼저 검토한다. 이미 GitHub 유료 플랜과 Actions를 사용 중이라면 Private 저장소 때문에 호스팅을 바꿀 필요도 없다.\n이 블로그는 무료 Private 저장소 연동이 필요해 Cloudflare Pages로 이전했다. 다른 사이트에도 같은 선택을 적용하기보다 저장소 공개 범위, 기존 배포 절차, 검토용 미리보기의 필요성을 기준으로 결정하면 된다.\n실제 설정은 Cloudflare Pages에 Hugo 블로그 배포하기, 운영 도메인 전환과 저장소 비공개 설정은 GitHub Pages에서 Cloudflare Pages로 이전하기에서 이어서 정리했다.\n","date":"2026년 10월 21일","externalUrl":null,"permalink":"/posts/github-pages-vs-cloudflare-pages/","section":"글 목록","summary":"","title":"GitHub Pages vs Cloudflare Pages: 무료 호스팅 비교와 선택 가이드","type":"posts"},{"content":"지노소프트 블로그의 호스팅을 GitHub Pages에서 Cloudflare Pages로 변경했다. https://blog.jinosoft.com/과 /posts/\u0026lt;slug\u0026gt;/는 유지하고, GitHub 소스 저장소만 Private으로 전환했다.\nCloudflare의 pages.dev에서 빌드와 화면 표시를 먼저 확인한 뒤 운영 도메인을 전환했다. 첫 배포가 필요하다면 Cloudflare Pages의 GitHub 연동과 Hugo 빌드 설정을 먼저 진행한다.\n1. 이전 대상과 변경 순서 # 항목 이전 전 이전 후 소스 저장소 GitHub jinosoft/jinosoft-blog, Public 같은 저장소, Private 빌드·배포 GitHub Actions Cloudflare Pages Git 연동 웹 호스팅 GitHub Pages Cloudflare Pages 운영 주소 https://blog.jinosoft.com/ 변경 없음 글 URL /posts/\u0026lt;slug\u0026gt;/ 변경 없음 DNS 관리 카페24 카페24 유지 새 배포 확인 → 도메인 등록 → DNS 전환 → HTTPS·기존 경로 검증 → 기존 배포 중단 → 저장소 비공개 → 재배포 검증 순서로 진행한다.\nGitHub Free에서 먼저 저장소를 Private으로 바꾸면 기존 GitHub Pages가 게시 중단될 수 있다. 따라서 운영 도메인이 새 호스팅에서 정상 응답하는 것을 확인하기 전에는 저장소 공개 범위를 변경하지 않는다. GitHub 공개 범위 변경 시 영향\n2. baseURL과 글 경로 유지 # Hugo 설정은 기존 값을 유지했다.\nbaseURL = \u0026#34;https://blog.jinosoft.com/\u0026#34; [permalinks] posts = \u0026#34;/posts/:slug/\u0026#34; 호스팅을 바꾸더라도 도메인·slug·경로를 유지하면 독자가 사용하는 기존 URL을 그대로 제공할 수 있다. 이 사례에서는 GitHub 저장소 이름이나 글 디렉터리 이름도 변경하지 않았다.\npages.dev는 새 배포의 검증 주소로 사용했다. 운영 주소를 임시 주소로 교체하거나 모든 기존 URL을 새 도메인으로 바꾸는 작업이 아니다.\n3. Cloudflare Pages에 커스텀 도메인 등록 # Cloudflare의 jinosoft-blog 프로젝트에서 Custom domains → Set up a custom domain을 선택하고 blog.jinosoft.com을 입력한다.\n이 사례의 blog.jinosoft.com은 서브도메인이다. 외부 DNS를 유지하고 CNAME으로 Pages에 연결할 수 있으므로, jinosoft.com 전체 네임서버를 옮기지 않았다. 루트 도메인인 jinosoft.com 자체를 Pages에 연결하는 경우와는 조건이 다르다. Cloudflare 서브도메인 연결 안내\n도메인을 등록하면 필요한 CNAME 값이 표시된다.\nPages에 도메인을 등록한 뒤 DNS를 변경한다. DNS에 CNAME만 추가하고 Pages 프로젝트에 도메인을 연결하지 않으면 정상 서비스되지 않을 수 있다.\n4. 카페24에서 CNAME 대상 변경 # 카페24의 도메인관리 → DNS 관리 → 별칭(CNAME) 관리에서 기존 blog.jinosoft.com 레코드를 수정했다.\n레코드 이름 기존 대상 변경 대상 CNAME blog.jinosoft.com jinosoft.github.io jinosoft-blog.pages.dev 대상에는 https://나 / 경로를 넣지 않는다. 동일한 호스트에 CNAME을 중복 추가하지 말고 기존 대상을 수정한다. 홈페이지·메일 등 다른 DNS 레코드는 변경하지 않는다.\n캡처의 입력란은 가로 폭이 좁아 값의 앞부분이 보이지 않는다. 입력해야 할 전체 값은 **jinosoft-blog.pages.dev**다.\nDNS를 변경한 뒤 Cloudflare에서 Check DNS records를 실행한다. 처음에는 Inactive (Requires DNS setup)로 표시될 수 있다. DNS 캐시와 도메인 검증·인증서 준비 상태가 모두 같은 시점에 갱신되는 것은 아니므로, CNAME이 반영됐다는 이유만으로 HTTPS까지 완료됐다고 판단하지 않는다.\n5. Active·HTTPS·기존 URL 검증 # 도메인 상태가 Active, SSL enabled로 전환된 것을 확인했다.\n이후 운영 주소를 직접 열어 홈과 기존 글·이미지가 표시되는지 확인한다. 이번 전환 후에는 홈, Docker 글 두 편과 이미지, 사이트맵, robots.txt, 파비콘의 HTTPS 요청이 모두 200으로 응답했고, 응답 헤더의 서버는 Cloudflare로 확인됐다.\n확인할 경로는 다음과 같다.\n대상 확인 경로 또는 항목 홈 / 기존 글 /posts/docker-volume-data-persistence/ 이미지·CSS 글 이미지와 사이트 레이아웃 정상 표시 사이트맵 /sitemap.xml, 내부 URL이 운영 도메인인지 확인 크롤러 안내 /robots.txt, 사이트맵 주소와 크롤링 차단 여부 파비콘 /favicon.ico 표준 URL HTML의 canonical이 기존 운영 URL인지 확인 WSL에서 응답을 직접 확인하려면 다음 명령을 사용할 수 있다.\ncurl -I https://blog.jinosoft.com/ curl -I https://blog.jinosoft.com/posts/docker-volume-data-persistence/ curl -I https://blog.jinosoft.com/sitemap.xml 인증서 오류가 나면 -k로 무시하지 말고 도메인 활성화와 HTTPS 구성을 먼저 확인한다.\n도메인과 경로가 동일하므로 Search Console의 주소 변경 도구로 새 도메인 이전을 신청하는 상황은 아니다. 기존 속성과 사이트맵 URL은 유지하되, 전환 이후에도 크롤링·색인 오류가 발생하지 않는지 확인한다. 호스팅 변경 자체가 검색 순위 유지나 상승을 보장하지는 않는다. Google의 URL 변경 없는 호스팅 이전 안내\n6. GitHub Pages 자동 배포와 기존 게시 중단 # 운영 주소의 Cloudflare 응답을 확인한 뒤 GitHub의 기존 배포를 정리했다. 일반적인 운영 이전에서는 DNS 캐시 유효기간과 기존 호스팅의 접근 기록도 확인하고, 이전 호스팅으로 향하는 요청이 남아 있지 않은지 검토한 뒤 중단한다. 새 호스팅에서 한 번 접속됐다는 사실만으로 모든 이용자의 DNS 전환이 끝났다고 판단하지 않는다.\n먼저 저장소의 Actions → Deploy Hugo site to Pages → ⋯ → Disable workflow를 선택한다. GitHub Actions 전체가 아니라 기존 Pages 배포 워크플로만 비활성화한다.\n다음으로 Settings → Pages → Unpublish site를 실행한다.\n워크플로 비활성화는 이후 자동 배포를 막는 작업, Unpublish는 기존 배포를 내리는 작업이다. 둘은 다르다. Unpublish만 실행하고 워크플로를 계속 활성화해 두면 이후 배포로 다시 게시될 수 있다. 소스 저장소와 커밋을 삭제하는 작업은 아니다. GitHub 게시 중단 안내\n여기서 중단하는 것은 GitHub Pages로 보내는 배포다. Cloudflare Pages는 GitHub App 연동으로 push를 감지하므로, 기존 Actions 워크플로를 꺼도 Cloudflare의 자동 배포는 계속 사용할 수 있다.\n이 시점에는 DNS가 이미 Cloudflare를 가리켜야 한다. 기존 GitHub 주소를 가리키는 DNS를 남긴 채 게시부터 중단하지 않는다.\n7. 저장소를 Private으로 전환 # GitHub 저장소의 Settings → General → Danger Zone → Change repository visibility → Change to private로 이동한다. 대상이 jinosoft/jinosoft-blog인지 확인하고, 변경 영향 안내를 읽은 뒤 진행한다.\n완료 후 저장소에 Private 표시가 나타났다.\n이 전환은 GitHub 원본 저장소 접근을 제한한다. Cloudflare에서 공개하는 글·이미지는 계속 공개된다. 기존 공개 fork는 Private으로 바뀌지 않고, 이미 받은 clone도 회수되지 않는다. 비공개 전환을 공개 이력이나 유출 정보의 완전 삭제 방법으로 사용해서는 안 된다.\nCloudflare의 GitHub App에는 이 저장소 접근 권한이 유지되어야 한다. 권한을 해제하면 기존 사이트는 보이더라도 다음 빌드에서 저장소를 복제하지 못할 수 있다.\n8. Private 저장소에서 재배포 검증 # Private 전환 후 기존 사이트가 열린다는 것만으로는 검증이 끝나지 않는다. Cloudflare는 이미 생성된 정적 파일을 제공할 수 있으므로, 새 빌드가 비공개 저장소를 읽을 수 있는지도 확인해야 한다.\nCloudflare Pages의 Deployments → Retry deployment를 실행하고 저장소 복제·빌드·배포 결과를 확인했다.\nPrivate 전환 후 재배포도 success로 완료됐다. 소스는 같은 GitHub 저장소에서 관리하고, 이후 운영 브랜치 push에 따른 빌드·배포는 Cloudflare가 담당한다.\n이번 Retry deployment는 비공개 저장소 접근을 확인하기 위한 수동 재시도였다. 이후에는 자동 배포가 활성화된 main에 새 커밋을 push하면 Cloudflare가 빌드하고, 성공한 결과를 blog.jinosoft.com에 반영한다. 매번 Retry를 누르거나 DNS를 다시 설정할 필요는 없다. push 이후 자동 배포 흐름\n새 호스팅과 기본 pages.dev 주소는 모두 접근될 수 있다. 운영 URL을 canonical과 사이트맵에 유지하고, 기본 도메인 접근을 운영 도메인으로 리다이렉트할지는 별도로 검토한다. 이번 캡처에는 pages.dev 리다이렉트 설정 과정은 포함하지 않았다. 기본 도메인 접근 관리\n마무리 # 기존 도메인과 글 경로를 유지한 채 Cloudflare Pages로 호스팅을 전환하고, GitHub 저장소를 Private으로 변경했다. 마지막 재배포 성공으로 비공개 저장소에서도 다음 배포를 생성할 수 있는 상태까지 확인했다.\n이전의 핵심은 저장소부터 비공개로 바꾸는 것이 아니라, 새 호스팅 검증과 DNS 전환을 마친 뒤 기존 배포·저장소 설정을 정리하는 순서다.\n서비스 선택 이유는 GitHub Pages와 Cloudflare Pages 비교, 새 배포 구성은 Cloudflare Pages Hugo 배포 가이드에서 확인할 수 있다.\n","date":"2026년 10월 21일","externalUrl":null,"permalink":"/posts/migrate-hugo-github-pages-to-cloudflare-pages/","section":"글 목록","summary":"","title":"GitHub Pages에서 Cloudflare Pages로 이전하기: 도메인 유지와 저장소 비공개 전환","type":"posts"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/github-pages/","section":"Tags","summary":"","title":"Github-Pages","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/hosting/","section":"Tags","summary":"","title":"Hosting","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/categories/it%EA%B0%9C%EB%B0%9C/","section":"Categories","summary":"","title":"IT개발","type":"categories"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/migration/","section":"Tags","summary":"","title":"Migration","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/tags/troubleshooting/","section":"Tags","summary":"","title":"Troubleshooting","type":"tags"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/posts/","section":"글 목록","summary":"","title":"글 목록","type":"posts"},{"content":"","date":"2026년 10월 21일","externalUrl":null,"permalink":"/","section":"지노소프트 기술 블로그","summary":"","title":"지노소프트 기술 블로그","type":"page"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/container/","section":"Tags","summary":"","title":"Container","type":"tags"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"Docker 컨테이너의 데이터 유지 여부는 중지·재시작인지 삭제·재생성인지, 파일이 컨테이너 내부에 저장됐는지 볼륨에 저장됐는지에 따라 달라진다.\nWSL Ubuntu에서 텍스트 파일로 세 가지 동작을 확인했다. 컨테이너 내부 파일은 중지·재시작 후 유지됐지만 삭제·재생성 후에는 소실됐다. 같은 볼륨을 마운트한 새 컨테이너에서는 기존 파일을 조회할 수 있었다.\n1. Docker Desktop 실행 상태와 WSL 연동 확인 # Windows의 Docker Desktop과 WSL Ubuntu를 연동한 환경에서 진행했다. Docker Desktop은 4.94.0, Docker Engine은 29.8.2이며, 컨테이너 이미지는 alpine:3.22를 사용했다. 설치가 필요하다면 Docker Desktop 설치와 WSL 연동부터 진행한다.\n이후 명령은 모두 WSL Ubuntu 터미널에서 실행한다. Compose 파일이나 별도 스크립트는 필요 없다.\n처음 docker version을 실행했을 때는 다음 오류가 표시됐다.\nThe command \u0026#39;docker\u0026#39; could not be found in this WSL 2 distro. Windows에서 Docker Desktop이 실행 중인지 먼저 확인한다. Docker Desktop이 실행되지 않았거나 해당 Ubuntu 배포판의 WSL 연동이 비활성화되어 있으면, WSL에서 Docker 명령을 찾지 못하거나 엔진 연결 오류가 발생할 수 있다. 위 메시지만으로 어느 설정이 원인인지 확정할 수는 없다.\nDocker Desktop을 열고 하단에 Engine running이 표시되는지 확인한다. 계속 오류가 나면 Settings → Resources → WSL integration에서 사용할 Ubuntu를 활성화하고 설정을 적용한다. Docker의 WSL 연동 안내\n오류가 계속된다면 WSL 2에서 docker 명령을 찾지 못할 때의 점검 순서를 참고한다.\nWSL에서 다시 실행한다.\ndocker version Client와 Server 정보가 모두 출력되면 실습을 진행한다. Client만 출력되고 Server 연결이 실패한다면 아직 엔진에 연결되지 않은 상태다.\n기존 실습과 충돌하지 않도록 아래 컨테이너·볼륨 이름을 사용한다. 같은 이름의 리소스가 이미 있다면 용도를 확인하고 다른 이름으로 바꾼다.\ndocker ps -a --filter name=jinosoft-storage- docker volume ls --filter name=jinosoft-storage-data 2. 컨테이너 중지·재시작 시 데이터 유지 여부 # 먼저 볼륨을 마운트하지 않은 컨테이너를 실행하고 /data/note.txt를 생성한다.\ndocker run -d --name jinosoft-storage-layer alpine:3.22 sleep infinity docker exec jinosoft-storage-layer sh -c \u0026#39;mkdir -p /data; printf \u0026#34;container-only\\n\u0026#34; \u0026gt; /data/note.txt\u0026#39; docker exec jinosoft-storage-layer cat /data/note.txt -d는 백그라운드 실행, sleep infinity는 실습용 컨테이너가 바로 종료되지 않도록 대기하는 명령이다. docker exec는 실행 중인 컨테이너 안에서 명령을 수행한다. 처음 실행하면 Alpine 이미지 다운로드가 먼저 진행될 수 있다.\n파일 내용으로 container-only가 출력되는지 확인한 뒤, 같은 컨테이너를 중지했다가 다시 시작한다.\ndocker stop jinosoft-storage-layer docker start jinosoft-storage-layer docker exec jinosoft-storage-layer cat /data/note.txt 재시작한 뒤에도 container-only가 출력됐다. docker stop은 컨테이너를 삭제하지 않으므로, 이 예제의 컨테이너 내부 파일은 그대로 남는다.\n볼륨 미사용과 중지 시 데이터 소실은 같은 의미가 아니다. 이번 파일은 메모리 기반 tmpfs가 아니라 컨테이너의 디스크 기반 쓰기 계층에 저장됐다.\n3. 컨테이너 삭제·재생성 시 데이터 소실 확인 # 이번에는 컨테이너를 삭제하고, 동일한 이미지와 컨테이너 이름으로 재생성한다.\n아래 docker rm -f는 지정한 실습용 컨테이너를 강제 종료하고 삭제한다. 실제 서비스 컨테이너에 그대로 적용하지 않는다. 운영 환경에서는 정상 종료와 필요한 데이터의 백업을 먼저 확인한다.\ndocker rm -f jinosoft-storage-layer docker run -d --name jinosoft-storage-layer alpine:3.22 sleep infinity docker exec jinosoft-storage-layer cat /data/note.txt 이번에는 다음 오류가 출력됐다.\ncat: can\u0026#39;t open \u0026#39;/data/note.txt\u0026#39;: No such file or directory 컨테이너 안에 새로 만든 파일은 기본적으로 해당 컨테이너의 **쓰기 계층(writable layer)**에 저장된다. 컨테이너를 삭제하면 이 계층도 삭제된다. 같은 이름을 다시 사용해도 새 컨테이너가 이전 파일을 이어받지는 않는다. Docker 저장 구조\n실행 중 생성한 note.txt는 원본 alpine:3.22 이미지에 반영되지 않는다. 같은 이미지로 컨테이너를 재생성해도 이전 쓰기 계층은 복원되지 않는다.\n4. 네임드 볼륨 마운트로 데이터 유지 # 컨테이너 삭제·재생성 후에도 유지할 데이터는 쓰기 계층과 분리해 저장해야 한다. 여기서는 이름을 지정해 생성하는 **네임드 볼륨(named volume)**을 사용한다. 볼륨을 컨테이너의 특정 경로에서 사용할 수 있도록 연결하는 작업을 **마운트(mount)**라고 한다.\n볼륨을 생성하고 컨테이너의 /data에 마운트한다.\ndocker volume create jinosoft-storage-data docker run -d --name jinosoft-storage-volume \\ --mount type=volume,source=jinosoft-storage-data,target=/data \\ alpine:3.22 sleep infinity source는 볼륨 이름, target은 컨테이너 내부의 마운트 경로다. 이제 /data에 기록하는 파일은 jinosoft-storage-data 볼륨에 저장된다.\ndocker exec jinosoft-storage-volume sh -c \u0026#39;printf \u0026#34;saved-in-volume\\n\u0026#34; \u0026gt; /data/note.txt\u0026#39; docker exec jinosoft-storage-volume cat /data/note.txt saved-in-volume이 출력되면 컨테이너를 삭제한다. 이후 동일한 볼륨을 마운트해 컨테이너를 재생성한다.\ndocker rm -f jinosoft-storage-volume docker run -d --name jinosoft-storage-volume \\ --mount type=volume,source=jinosoft-storage-data,target=/data \\ alpine:3.22 sleep infinity docker exec jinosoft-storage-volume cat /data/note.txt 새 컨테이너에서도 saved-in-volume이 출력됐다. 파일 생성 명령을 다시 실행하지 않았는데도 기존 데이터를 조회할 수 있었다. 컨테이너와 별도로 관리되는 네임드 볼륨은 컨테이너 삭제 후에도 유지됐기 때문이다. Docker 볼륨의 수명\n컨테이너 이름은 달라도 된다. 데이터 재사용 조건은 동일한 Docker 엔진에서 동일한 볼륨을 마운트하는 것이다. 이 예제에서는 /data만 마운트했으므로, /tmp 등 마운트 경로 밖에 생성한 파일까지 유지되는 것은 아니다.\n5. 네임드 볼륨과 바인드 마운트 비교 # 호스트의 파일·디렉터리를 직접 지정해 마운트하려면 **바인드 마운트(bind mount)**를 사용한다.\n구분 네임드 볼륨 바인드 마운트 마운트 대상 Docker가 관리하는 볼륨 직접 지정한 호스트 파일·디렉터리 지정 예 source=jinosoft-storage-data source=/home/user/project 주로 쓰는 경우 DB 데이터 등 컨테이너와 분리해 보관할 데이터 호스트에서 편집하는 소스 코드·설정 파일 공유 컨테이너 삭제 후 볼륨을 별도로 삭제하지 않으면 유지 원본 호스트 파일을 삭제하지 않으면 유지 둘 다 컨테이너 밖의 저장소를 연결하지만, 볼륨은 Docker가 저장 위치를 관리하고 바인드 마운트는 사용자가 경로를 지정한다. 바인드 마운트를 기본 쓰기 가능 상태로 연결하면 컨테이너에서 호스트 파일을 수정하거나 삭제할 수도 있다. 설정 파일을 읽기만 해야 한다면 readonly 옵션을 검토한다. 바인드 마운트 공식 문서\n이번 실습의 검증 대상은 네임드 볼륨이다. 바인드 마운트는 저장 경로 관리 방식과 사용 목적을 비교하기 위해 함께 정리했다.\n6. 실습 리소스 삭제와 볼륨 관리 주의사항 # 캡처와 확인이 끝나면 이번 실습용 컨테이너만 삭제한다. 아래는 정리가 필요할 때 실행하는 명령이며, 앞의 캡처에서 실행한 결과는 아니다.\ndocker rm -f jinosoft-storage-layer jinosoft-storage-volume 컨테이너 삭제만으로는 jinosoft-storage-data 네임드 볼륨이 삭제되지 않는다. 테스트 파일도 더 이상 필요 없을 때만 아래 명령을 실행한다.\ndocker volume rm jinosoft-storage-data 볼륨을 삭제하면 그 안의 파일도 삭제된다. 다른 프로젝트의 데이터까지 정리하지 않도록 전체 볼륨 삭제·정리 명령 대신 이름을 명시했다.\n볼륨 삭제 옵션은 명령에 따라 적용 범위가 다르다. docker rm -v는 컨테이너에 연결된 익명 볼륨을 삭제하며 네임드 볼륨은 유지한다. 익명 볼륨은 사용자가 이름을 지정하지 않아 Docker가 임의의 이름을 부여한 볼륨이다. 반면 docker compose down -v는 Compose 파일에 선언된 네임드 볼륨도 삭제 대상으로 포함한다. Compose에서 external로 선언한 볼륨은 제외된다. 컨테이너 삭제 옵션, Compose down 옵션\n볼륨은 컨테이너 재생성에 대비한 저장 방식이지 백업은 아니다. 볼륨 안에서 파일을 지우면 삭제가 반영되고, 저장 장치 장애나 Docker Desktop 데이터 초기화에도 대비해야 한다. 중요한 데이터는 별도 백업과 복구 확인이 필요하다.\n마무리 # 이번 실습에서는 중지·재시작 시 쓰기 계층 유지, 삭제·재생성 시 쓰기 계층 소실, 동일한 볼륨 마운트 시 기존 데이터 조회를 확인했다.\n컨테이너를 교체할 때는 이름 재사용 여부가 아니라, 유지할 데이터 경로에 볼륨 또는 바인드 마운트가 설정되어 있는지 먼저 확인해야 한다.\n","date":"2026년 10월 19일","externalUrl":null,"permalink":"/posts/docker-volume-data-persistence/","section":"글 목록","summary":"","title":"Docker 컨테이너를 삭제하면 데이터도 사라질까? 볼륨으로 보관하기","type":"posts"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/docker-desktop/","section":"Tags","summary":"","title":"Docker-Desktop","type":"tags"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/storage/","section":"Tags","summary":"","title":"Storage","type":"tags"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/ubuntu/","section":"Tags","summary":"","title":"Ubuntu","type":"tags"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/volume/","section":"Tags","summary":"","title":"Volume","type":"tags"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/wsl/","section":"Tags","summary":"","title":"Wsl","type":"tags"},{"content":"Windows에 Docker Desktop을 설치했더라도 WSL Ubuntu에서 docker 명령이 바로 실행되지 않을 수 있다. 볼륨 실습을 시작하려고 docker version을 실행했을 때 다음 오류가 표시됐다.\nThe command \u0026#39;docker\u0026#39; could not be found in this WSL 2 distro. We recommend to activate the WSL integration in Docker Desktop settings. 이 글은 Windows의 Docker Desktop과 WSL 2를 연동하는 구성을 기준으로 한다. Ubuntu에 Docker Engine을 직접 설치해 운영하는 구성과는 점검 방법이 다르다. 설치부터 필요한 경우에는 Docker Desktop 설치와 WSL 연동을 참고한다.\n1. Docker Desktop 실행 상태 확인 # 먼저 Windows 시작 메뉴에서 Docker Desktop을 실행한다. 창을 열었다는 것만으로 엔진이 준비된 것은 아니므로, 하단에 Engine running이 표시될 때까지 기다린다.\nDocker Desktop이 실행되지 않았거나 WSL 배포판 연동이 비활성화되어 있으면, WSL에서 docker 명령을 찾지 못하거나 엔진 연결 오류가 발생할 수 있다. 단, 위 오류 메시지만으로 Desktop 미실행이 유일한 원인이라고 단정할 수는 없다. 실행 상태와 배포판 연동 설정을 함께 확인한다.\n엔진이 실행 중이면 WSL Ubuntu에서 다시 확인한다.\ndocker version Client와 Server 정보가 모두 표시되면 정상이다. 같은 오류가 계속되면 다음 단계에서 현재 사용하는 배포판의 연동 설정을 확인한다.\n2. Ubuntu의 WSL integration 활성화 # Docker Desktop에서 Settings → Resources → WSL integration으로 이동한다. 사용할 Ubuntu 배포판을 활성화하고 Apply \u0026amp; restart를 클릭한다. 버전에 따라 적용 버튼의 이름은 다를 수 있다.\n위 설정처럼 Enable integration with my default WSL distro를 선택하지 않아도, 아래 배포판 목록에서 Ubuntu를 개별적으로 활성화할 수 있다. Ubuntu를 여러 버전 설치했다면 현재 터미널에서 사용하는 배포판이 활성화되어 있는지 확인한다.\n설정 적용이 끝나면 Ubuntu 터미널을 닫았다가 다시 열고 docker version을 실행한다. Docker 공식 문서도 배포판의 WSL 연동을 활성화해야 해당 배포판 터미널에서 Docker 명령을 사용할 수 있다고 안내한다. Docker WSL 2 연동 문서\nWSL integration 메뉴가 보이지 않는 경우 # Docker Desktop이 Linux 컨테이너 모드인지 확인한다. Windows 컨테이너 모드를 사용 중이라면 Docker 메뉴에서 Switch to Linux containers로 전환한다.\nGeneral에 Use WSL 2 based engine 옵션이 표시되면 활성화 여부를 확인한다. WSL 2 사용 환경에서는 기본 적용되어 옵션이 보이지 않을 수도 있다.\n배포판의 WSL 버전은 Windows PowerShell에서 확인할 수 있다.\nwsl --list --verbose 사용할 Ubuntu 행의 VERSION이 2인지 확인한다. 배포판 이름과 WSL 버전은 구분해서 확인해야 한다.\n3. Client와 Server 정보로 정상 연결 확인 # WSL Ubuntu에서 실행한다.\ndocker version 이번 환경에서는 이후 실행에서 Client와 Server 정보가 모두 출력됐다. Client 버전은 29.8.2, Server는 Docker Desktop 4.94.0으로 표시됐다.\n이 결과는 WSL에서 Docker CLI를 실행할 수 있고, Docker 엔진에도 연결됐다는 뜻이다.\n4. 오류 메시지에 따라 점검 대상 구분 # docker 명령을 찾지 못하는 경우 # 이 글의 could not be found in this WSL 2 distro 메시지나 docker: command not found는 WSL에서 Docker CLI를 정상적으로 사용할 수 없는 상태를 의미한다. 먼저 Desktop 실행 상태, 해당 배포판의 WSL integration, 설정 적용 후 터미널 재실행을 확인한다.\n곧바로 Ubuntu에 docker.io를 추가 설치하는 것은 피한다. Desktop 연동을 사용할 계획인데 별도 Engine·CLI를 설치하면 구성과 데이터 위치가 혼재할 수 있다. 이미 직접 설치한 Docker가 있다면 기존 서비스와 데이터부터 확인한다. Docker 설치 방식 충돌 주의사항\nClient는 나오지만 Server 연결이 실패하는 경우 # 이 경우에는 CLI 자체는 실행되고 있다. 다음과 같은 메시지는 엔진 연결 문제로 구분한다.\nCannot connect to the Docker daemon 위 메시지는 구분을 위한 예시이며, 이번 오류 캡처에 나온 메시지는 아니다. Docker Desktop이 Engine running 상태인지 확인하고, 계속 실패하면 WSL에서 연결 대상을 확인한다.\ndocker context ls printenv DOCKER_HOST DOCKER_CONTEXT 이전에 다른 Docker 엔진을 사용했다면 컨텍스트나 환경변수가 그 엔진을 가리킬 수 있다. 원격 연결용 설정을 무조건 지우거나 기본값으로 변경하지 말고 기존 용도를 먼저 확인한다. 설치 글의 Server 연결 오류 점검도 참고할 수 있다.\n마무리 # Docker Desktop 연동 구성에서는 Windows의 엔진 실행 상태와 WSL 배포판 연동 상태를 모두 확인해야 한다. Windows에 설치되어 있다는 사실만으로 현재 Ubuntu 터미널에서도 사용할 수 있다고 판단하지 않는다.\ndocker version에서 Client와 Server가 모두 출력되면 연결 확인은 끝난다. 이번에는 정상 출력을 확인한 뒤 컨테이너와 볼륨의 데이터 유지 실습을 진행했다.\n","date":"2026년 10월 19일","externalUrl":null,"permalink":"/posts/docker-command-not-found-wsl2/","section":"글 목록","summary":"","title":"WSL 2 Docker 오류 해결: The command 'docker' could not be found","type":"posts"},{"content":"","date":"2026년 10월 19일","externalUrl":null,"permalink":"/tags/wsl2/","section":"Tags","summary":"","title":"Wsl2","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/availability-zone/","section":"Tags","summary":"","title":"Availability-Zone","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/aws/","section":"Tags","summary":"","title":"Aws","type":"tags"},{"content":"클라우드 서버를 한국·일본·미국에 각각 배포하려면 해당 CSP가 그 지역에 리전을 제공해야 한다. 전 세계 리전이 많더라도 필요한 국가에 없거나, 그 리전에서 사용하는 DB·VM 상품을 지원하지 않으면 배포 후보가 되지 못한다.\nAWS·Azure·GCP·네이버클라우드의 리전 수, 지역별 분포, 한국 리전을 비교했다. 확인 기준일은 2026년 10월 16일이며, 공식 문서의 현재 목록을 집계한 값이다. 개설 예정·프리뷰 리전은 포함하지 않았다.\n1. 전 세계 리전 수 비교 # CSP 공식 소개 수치 본문 집계 집계 대상 한국 리전 AWS 39개 34개 상용 AWS 리전 목록. 중국·GovCloud·European Sovereign Cloud 제외 서울 1개 Azure 70개 이상 57개 Azure public cloud 목록에서 프리뷰 제외. 접근 제한 리전 포함 서울·부산 2개 GCP 43개 43개 Compute Engine의 표준 존을 리전 코드로 묶어 집계 서울 1개 NCP 아래 사용 환경 표 기준 5개 일반 서비스의 공식 리전·존 표 한국 1개 공식 소개 숫자와 본문 집계는 범위가 다르다. 일반 서비스의 배포 위치를 비교하기 위해 별도 계정·운영 체계인 정부 전용·중국 별도 클라우드·주권 클라우드는 본문 집계에서 제외했다. CDN 엣지 거점, Local Zone, 데이터센터 건물 수도 리전 수에 더하지 않는다.\n집계에 사용한 목록은 다음과 같다.\nAWS: 상용 AWS 리전 목록의 34개다. 별도 계정·파티션으로 운영하는 범위는 제외했으므로 글로벌 인프라 소개의 39개와 구분한다. European Sovereign Cloud도 별도 파티션이다. 사용 전 별도 활성화(Opt-in)가 필요한 상용 리전은 포함했다. Azure: Azure regions list의 All 표 58개 중 프리뷰 리전인 Taiwan North를 제외한 57개다. 해당 리전 아이콘의 대체 텍스트는 \u0026lsquo;coming soon\u0026rsquo;이지만, 표의 범례에서는 프리뷰 상태로 정의한다. 권역별 탭의 반복 행은 중복 집계하지 않았다. 포함된 57개 중 8개에는 접근 제한 표시가 있다. 공식 개요의 70개 이상은 이 집계값과 구분한다. GCP: Compute Engine 리전·존 목록의 표준 존 130개를 리전 코드로 묶으면 43개다. 글로벌 위치 소개의 43개와 일치한다. 추가 AI 존이 생기더라도 이를 별도 리전으로 세지 않는다. 집계에는 이용 조건이 있는 리전도 포함한다. 예를 들어 사우디아라비아 외 청구 주소를 사용하는 고객이 담맘(me-central2)을 이용하려면 인보이스 결제(Invoiced Billing)가 필요하다. 담맘 리전 이용 조건 NCP: 사용 환경 개요의 리전·존 표에 있는 한국·미국 서부·싱가포르·일본·독일 5개다. 과거 자료나 전용회선 요금표에는 홍콩도 나오지만, 현재 리전·존 목록에는 없어 이번 집계에서 제외했다. 이를 근거로 종료 시점이나 사유까지 단정하지는 않는다. 이 표는 위 날짜에 확인한 문서 목록의 스냅샷이며, 신규 자원을 생성할 수 있는지는 해당 계정·구독의 콘솔에서 다시 확인해야 한다.\n2. 리전과 가용 영역은 무엇이 다를까 # 리전(Region)은 서울·도쿄처럼 서비스를 배포할 지리적 범위다. 가용 영역(Availability Zone, AZ)은 리전 안에서 장애 영향을 분리하도록 구성한 배포 단위다. GCP·NCP는 주로 존(Zone)이라는 명칭을 사용한다.\n도식의 리전당 존 3개는 설명용이다. 모든 CSP의 모든 리전이 3개 AZ를 제공하는 것은 아니다. AZ 하나를 데이터센터 건물 하나와 동일하게 보아서도 안 된다. AWS는 AZ가 하나 이상의 데이터센터로 구성된다고 설명한다. AWS 리전·AZ 개념\n서울 리전의 서로 다른 AZ에 서버를 배치하는 것은 멀티 AZ, 서울과 도쿄 리전에 나누어 배치하는 것은 멀티 리전이다. 전자는 존 장애, 후자는 리전 전체 장애까지 고려하는 구성이다. 서버 배치 외에 DB 복제와 트래픽 전환도 따로 구성해야 한다. GCP 리전·존과 장애 분리\n3. 어느 지역에 리전이 많을까 # 앞의 본문 집계 대상을 권역별로 나눈 결과다. 아시아와 오세아니아를 분리하고, 중동은 별도 열로 묶었다. 북미에는 미국·캐나다·멕시코를 포함한다.\nCSP 아시아 오세아니아 북미 남미 유럽 중동 아프리카 합계 AWS 11 3 7 1 8 3 1 34 Azure 12 5 12 3 19 4 2 57 GCP 10 2 12 2 13 3 1 43 NCP 3 0 1 0 1 0 0 5 출처는 앞의 4사 리전 목록이다. GCP의 europe-* 등 리전 코드와 실제 위치, AWS의 Geography, Azure의 Physical location을 기준으로 분류했다. Azure의 Asia Pacific 탭 중 호주·뉴질랜드는 오세아니아로, Americas 탭 중 브라질·칠레는 남미로 분리했다. 접근 제한 리전도 첫 표와 동일하게 포함했다.\n이 표에서 0은 해당 권역 이용자에게 서비스할 수 없다는 뜻이 아니라, 집계 대상 목록에 현지 리전이 없다는 뜻이다. 예를 들어 NCP로 오세아니아 이용자에게 서비스를 제공하려면 다른 권역의 리전에서 응답하게 된다. 이때 실제 이용자 네트워크에서 지연 시간을 측정해 배포 후보를 판단해야 한다.\n공식 지도에서 분포 확인하기 # 지도는 배포 위치를 살펴보는 자료다. 개설 예정 리전이나 별도 클라우드, 데이터센터 거점까지 표시될 수 있어, 지도상의 점 개수를 앞의 집계값으로 사용하지 않았다.\nAWS 아시아 태평양\n이 화면에는 중국 본토 리전도 포함된다. 지도에 표시된 아시아 태평양 13개와 본문 집계의 아시아 11개는 범위가 다르며, 본문에서는 중국 리전을 제외했다. 호주·뉴질랜드는 별도 권역으로 분류했다.\nAzure 글로벌 리전\n청록색 원은 현재 이용 가능한 리전, 흰색 원은 개설 예정 리전이다. Geography·네트워크 PoP·에너지 프로젝트 등의 표시를 해제한 화면이며, 본문 집계에서는 개설 예정 리전을 제외했다.\nGCP 글로벌 리전\n현재·미래 리전이 함께 선택되어 있다. 파란색은 현재 리전, 초록색은 미래 리전이며, 본문의 43개는 현재 리전만 집계한 값이다. 메뉴의 위치 (6)은 선택한 권역 수이지 리전 수가 아니다.\nNCP 글로벌 데이터센터\n글로벌 데이터센터 분포도에는 미국 동부도 표시된다. 다만 앞서 집계한 공식 리전·존 목록에는 미국 동부가 없어, 이 글의 리전 수에서는 제외했다. 이 지도만으로 미국 동부의 존 구성이나 현재 VM 생성 가능 여부를 판단하지 않는다.\n4. 한국 리전은 어떻게 구성되어 있을까 # CSP 한국 리전 코드·표기 공식 목록의 AZ·존 확인할 차이 AWS 서울 ap-northeast-2 4개 AZ 상용 AWS 리전 목록에서 한국은 서울 1개 Azure 서울 koreacentral 3개 AZ 사용하는 서비스의 AZ 지원도 별도 확인 Azure 부산 koreasouth N/A 리전 2개와 리전별 AZ 지원은 별개 GCP 서울 asia-northeast3 a·b·c 3개 존 VM은 존 단위로 배치 NCP 한국 존: KR-1·KR-2 Classic·VPC에 2개 존 두 존을 한국 리전 2개로 세지 않음 출처: AWS 리전 목록, Azure 리전 목록, GCP 서울 존 목록, NCP 리전·존 표.\n한국 내 서로 다른 리전에 배포할 요구가 있다면 Azure의 서울·부산을 후보로 검토할 수 있다. 다만 두 리전에서 필요한 상품을 지원하는지, 구독에서 배포할 수 있는지부터 확인해야 한다. 같은 리전의 다른 AZ에 배치하는 것으로 그 요구를 대신하지 않는다.\n5. 리전이 있어도 필요한 상품은 없을 수 있다 # 리전 목록 확인 다음에는 사용할 상품의 리전·존 지원표를 확인한다. VM을 만들 수 있다고 같은 지역에서 특정 GPU, 관리형 Kubernetes, DB 엔진까지 사용할 수 있는 것은 아니다.\n필요한 자원 확인할 항목 VM·GPU 인스턴스 계열, GPU 모델, 존별 공급, 계정 할당량 관리형 DB DB 엔진·버전, 고가용성 구성, 다른 리전으로의 복제 지원 Kubernetes 관리형 서비스 제공 여부, 노드용 VM, 멀티 AZ 지원 스토리지 저장 위치, 존·리전 간 복제 방식, 백업 저장 위치 네트워크 리전 간 연결, 인터넷 아웃바운드(이그레스)·리전 간 데이터 전송 요금 공식 확인 경로: AWS 서비스별 리전 지원, Azure 리전별 제품, GCP 리전별 제품, NCP 한국 환경별 서비스.\nNCP는 해외 리전의 플랫폼 차이도 확인해야 한다. 사용 환경 표에서 한국·싱가포르·일본은 VPC 존을 안내하지만, 미국 서부·독일은 Classic 존을 안내한다. 한국 VPC 구성을 해외 리전에도 그대로 만들 수 있다고 가정하면 안 된다.\n6. 실제 배포 리전은 이렇게 고른다 # 요구 조건 선택 기준 한국 이용자 대상 서비스 한국 리전부터 검토하고 실제 접속 지연·사용할 상품·비용 확인 한국·일본 각각에서 빠른 응답 필요 두 국가의 배포 위치와 데이터 동기화 방식 검토. 단일 리전으로도 목표 응답 시간을 충족하는지 먼저 측정 특정 존 장애에도 서비스 유지 같은 리전의 여러 AZ와 애플리케이션·DB의 장애 전환 구성 검토 리전 장애 시 다른 지역에서 복구 다른 리전에 백업·복제본 배치. RPO·RTO와 복구 절차 검증 데이터의 국내 저장 필요 서버뿐 아니라 DB·백업·로그·복제 대상의 저장 위치와 서비스 약관 확인 해외 다운로드 트래픽이 많음 배포 리전과 CDN을 함께 검토하고 아웃바운드(이그레스)·복제 트래픽 비용 계산 RPO는 장애 시 허용할 데이터 손실 시간, RTO는 복구에 허용할 시간이다. 백업만 다른 리전에 두는 구성과, 서버·DB 복제본을 준비해 전환하는 구성은 복구 시간과 운영비가 다르다. 고가용성과 재해 복구·RPO·RTO 설명\n리전 수는 배포 후보의 범위를 보여주는 숫자다. 최종 선택은 이용자 위치와 필요한 상품으로 후보를 줄인 뒤, 지연 시간·장애 대응·비용을 확인해서 결정한다. 국내 VM의 디스크·공인 IP·데이터 전송 요금은 4사 VM 비용 비교에서 이어서 확인할 수 있다.\n","date":"2026년 10월 16일","externalUrl":null,"permalink":"/posts/cloud-region-comparison/","section":"글 목록","summary":"","title":"AWS·Azure·GCP·네이버클라우드 리전 비교: 전 세계 어디에 배포할 수 있을까?","type":"posts"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/azure/","section":"Tags","summary":"","title":"Azure","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/compute-engine/","section":"Tags","summary":"","title":"Compute-Engine","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/cost/","section":"Tags","summary":"","title":"Cost","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/custom-machine-type/","section":"Tags","summary":"","title":"Custom-Machine-Type","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/gcp/","section":"Tags","summary":"","title":"Gcp","type":"tags"},{"content":"앱·웹 백엔드 서버 한 대를 GCP에서 운영하려는데, 2vCPU·메모리 8GiB는 필요 이상으로 커 보일 수 있다. 그렇다고 VM 요금만 낮추면 디스크·공인 IP·인터넷 데이터 전송 비용을 빠뜨리기 쉽다.\n소규모 백엔드의 시작 후보로 E2 커스텀 2vCPU·메모리 4GiB를 검토하고, 디스크와 네트워크까지 더해 월 예산을 계산했다. 요청이 드물고 CPU 사용량이 낮다면 더 저렴한 e2-medium·e2-small도 후보가 된다. 특정 사양으로 처리할 수 있는 동시 접속자 수나 요청 수를 보장하는 글은 아니다.\n1. 먼저 볼 추천 구성과 월 비용 # 가격 확인일은 2026년 10월 16일이다. 서울(asia-northeast3), 유료 OS 라이선스가 없는 Linux VM 1대, 월 730시간, 온디맨드, 단일 존 디스크, 사용 중인 공인 IPv4 1개를 기준으로 계산했다. 앞의 비교표는 약정·Spot·신규 가입 크레딧을 제외하며, 약정 견적은 7절에서 별도로 비교한다. 원화는 $1 = 1,400원 가정, VAT 제외이며 실제 청구 환율이나 원화 SKU 가격과 다를 수 있다.\n선택 방향 VM 사양 디스크 VM 요금 디스크 요금 IPv4 월 기본비 원화 환산 비용 우선, 낮은 CPU 사용량 0.5vCPU·메모리 2GiB\ne2-small, 공유 코어 HDD 50GiB $15.69 $2.60 $3.65 $21.94 30,716원 비용 우선, 메모리 4GiB 필요 1vCPU·메모리 4GiB\ne2-medium, 공유 코어 HDD 50GiB $31.38 $2.60 $3.65 $37.63 52,682원 커스텀, 저장 비용 최소화 2vCPU·메모리 4GiB HDD 50GiB $54.43 $2.60 $3.65 $60.68 84,952원 커스텀, 저장 공간 여유 2vCPU·메모리 4GiB HDD 100GiB $54.43 $5.20 $3.65 $63.28 88,592원 커스텀, 디스크 응답성 고려 2vCPU·메모리 4GiB Balanced SSD 50GiB $54.43 $6.50 $3.65 $64.58 90,412원 공유 코어 행의 vCPU는 두 vCPU를 합산한 지속 CPU 할당량이다. 게스트 OS에는 둘 다 2개 vCPU가 보이며, 자세한 차이는 3절에서 설명한다.\n기본비에는 인터넷 아웃바운드(이그레스) 요금, 백업·스냅샷, 관리형 DB, LB·NAT, 유료 DNS, 로그·모니터링의 별도 과금 항목을 포함하지 않았다. VM과 디스크는 제공된 계산기 견적을 사용했다. 공유 코어 두 행의 HDD 50GiB는 해당 VM 요금에 50GiB 디스크 요금을 조합한 계산 예시이며, 원본 견적의 HDD는 100GiB다.\n이 표는 GCP 전체 상품의 절대 최저가 순위가 아니다. e2-micro 같은 더 작은 상품도 있지만, 여기서는 제공된 견적과 메모리 2~4GiB급 후보를 비교한다. Compute Engine 무료 사용량의 e2-micro는 미국의 지정 리전 조건이 있으므로 서울 VM을 무료로 운영하는 근거로 사용하지 않는다. 무료 사용량 조건\n2. 커스텀 머신으로 쓰지 않는 메모리 줄이기 # e2-standard-2는 2vCPU·메모리 8GiB다. CPU는 2vCPU가 필요하지만 메모리는 4GiB면 충분하다면, E2 Custom machine type에서 2vCPU·4GiB로 조절하는 방식을 검토할 수 있다.\n위 캡처는 조절 기능을 보여주기 위해 2vCPU·3.2GiB를 입력한 화면이다. 본문의 비용은 아래 4GiB 견적을 기준으로 계산했다. 실제 VM 생성 시에는 시리즈별 허용 범위와 메모리 설정 단위를 따라야 한다. E2 일반 커스텀은 2vCPU부터 2개 단위, 메모리는 256MiB 단위이므로, 계산기에 소수를 입력할 수 있다고 그 값 그대로 생성할 수 있다고 보지는 않는다. 커스텀 머신 생성 조건\n구성 vCPU 메모리 월 VM 요금 차이 e2-standard-2 2 8GiB $62.75 비교 기준 E2 커스텀 2 4GiB $54.43 월 $8.32, 약 13.3% 절감 출처는 e2-standard-2 견적과 E2 커스텀 견적이다. CPU를 줄인 것이 아니라 메모리를 8GiB에서 4GiB로 줄여 합계가 낮아진 예시다.\n커스텀이라고 항상 저렴한 것은 아니다. 공식 문서는 커스텀 온디맨드 단가에 정형 머신 대비 5% 할증이 있다고 안내한다. 필요한 자원량을 줄인 이익과 커스텀 단가를 함께 봐야 하며, 위 계산기의 표시 금액에 할증을 다시 더하지 않는다. 커스텀 요금 안내\nCPU·메모리를 조절하는 방식이 GCP에만 있는 것은 아니다. OCI도 Flexible Shapes를 제공한다. 이 글에서는 GCP의 커스텀 머신으로 불필요한 자원 비용을 줄이는 방법에 집중한다. OCI Flexible Shapes\n4GiB 추천은 작은 API 서버나 웹 애플리케이션의 초기 검토 사양이다. 같은 VM에 DB·캐시까지 함께 실행하거나 JVM 힙·컨테이너 메모리를 크게 잡는다면 부족할 수 있다. OS와 런타임 사용량을 남겨두고 실제 부하에서 확인한다.\n3. 더 저렴하게 쓰려면 e2-medium부터 비교하기 # e2-medium은 메모리 4GiB에 월 VM 요금 $31.38로, 커스텀 2vCPU·4GiB보다 저렴하다. 대신 지속 CPU 할당량이 다르다.\n상품 메모리 게스트 OS에 보이는 vCPU 지속 CPU 할당량 월 VM 요금 e2-small 2GiB 2개 두 vCPU 합산 0.5vCPU 상당 $15.69 e2-medium 4GiB 2개 두 vCPU 합산 1vCPU 상당 $31.38 E2 커스텀 2vCPU·4GiB 4GiB 2개 공유 코어 상품의 시간 분할 제한과 구분 $54.43 공유 코어는 짧은 시간 CPU를 더 사용할 수 있지만 그 성능을 계속 유지하는 상품은 아니다. 계산기의 e2-small: vCPUs 0.5, e2-medium: vCPUs 1 표기는 게스트 OS에 CPU가 각각 0.5개·1개만 보인다는 뜻이 아니다. E2 공유 코어와 CPU 버스트\n견적: e2-small + HDD 100GiB, e2-medium + HDD 100GiB.\n요청이 드물고 CPU 사용량이 낮으면 e2-medium을 먼저 검토한다. 요청 처리·배치·압축·이미지 변환 등으로 CPU를 지속적으로 사용한다면, 공유 코어와 일반 커스텀에서 응답 지연과 처리량을 비교한 뒤 선택한다.\n4. 디스크는 HDD와 Balanced SSD를 구분하기 # 서울 견적의 디스크 용량별 월 요금은 다음과 같다. 사용한 데이터 양이 아니라 할당한 용량을 기준으로 요금이 발생한다. 디스크 요금\n종류 GCP 이름 50GiB 100GiB 먼저 검토할 상황 HDD Standard Persistent Disk, pd-standard $2.60 $5.20 저장 비용 우선, 랜덤 I/O 요구가 낮음 SSD 기반 Balanced Persistent Disk, pd-balanced $6.50 $13.00 OS·패키지·컨테이너·DB 등의 디스크 응답성 고려 Balanced Persistent Disk는 SSD 기반이지만, 별도 상품인 SSD Persistent Disk(pd-ssd)와는 다르다. 이번 캡처의 SSD 후보는 Balanced다. pd-ssd가 필요하다면 별도 견적을 조회한다. Persistent Disk 종류\n용량이 많이 필요 없고 랜덤 I/O 요구가 낮으면 HDD 50GiB, 파일·이미지 저장 공간이 더 필요하면 HDD 100GiB를 검토한다. 단, 작은 HDD는 용량이 남아도 I/O 성능이 부족할 수 있다. 디스크 성능에는 종류뿐 아니라 용량·VM 사양도 영향을 준다. DB를 같은 VM에 두거나 디스크 대기가 관찰되면 Balanced 50GiB부터 비교한다. Google도 일반적인 부팅 디스크의 일관된 성능에는 Balanced를 권고한다. 부팅 디스크 안내, 성능 제한\n같은 커스텀 VM에서 HDD 50GiB를 Balanced 50GiB로 바꾸면 월 $3.90, 가정 환율로 5,460원이 추가된다. 비용 차이가 작다면 부팅·배포·DB 접근의 응답성을 함께 보고 결정한다. 용량을 100GiB로 늘리는 경우의 견적은 HDD 100GiB, Balanced 100GiB에서 확인할 수 있다.\n5. 공인 IPv4 비용 포함하기 # 일반 VM에서 사용하는 고정·임시 외부 IPv4는 시간당 $0.005다. VM 견적과 별도로 0.005 × 730 = $3.65를 월 예산에 포함한다. 무료 1시간의 세부 차감은 별도로 적용하지 않은 값이다. 공인 IPv4 요금\n외부 IP를 제거하는 구조는 접속 경로와 NAT·LB 등의 추가 비용까지 함께 계산해야 하므로 이 글의 직접 접속 VM과 구분한다.\n6. Standard 네트워크로 월 데이터 전송 비용 비교하기 # 서울 VM에서 한국 이용자에게 인터넷으로 직접 전송하는 경우의 요금이다.\n네트워크 등급 이 예시에 적용할 무료량 적용 구간 단가 Premium, 서울 → 한국 해당 구간 무료량 없음 $0.19/GiB Standard, 서울 출발 계정 전체·모든 리전 합산 월 200GiB 200GiB 초과~10,240GiB 구간 $0.119/GiB 가격표 캡처: Premium 서울 → 한국, Standard 서울 출발. Standard의 무료 200GiB는 현재 공식 네트워크 요금 안내를 따른다. 다른 자원에서 무료량을 사용하지 않았다고 가정하며, VM마다 200GiB를 중복 차감하지 않는다.\n아래는 E2 커스텀 2vCPU·4GiB + Balanced 50GiB + IPv4, 월 기본비 $64.58에 인터넷 아웃바운드(이그레스)를 더한 결과다. 데이터 용량은 GCP 요금표의 과금 단위인 GiB로 표시했다.\n월 아웃바운드(이그레스) Standard 전송 요금 Standard 월 총액 Premium 전송 요금 Premium 월 총액 1GiB $0.00 $64.58 / 90,412원 $0.19 $64.77 / 90,678원 10GiB $0.00 $64.58 / 90,412원 $1.90 $66.48 / 93,072원 100GiB $0.00 $64.58 / 90,412원 $19.00 $83.58 / 117,012원 Standard의 계산식은 max(월 GiB − 200, 0) × $0.119, Premium은 위 예시 구간에서 월 GiB × $0.19다. 더 큰 사용량에는 다른 구간 단가가 적용될 수 있다.\nStandard는 인터넷 통신 사업자의 경로를 더 활용하는 비용 최적화 등급이고, Premium은 Google 백본을 더 활용한다. 등급을 바꾸면 품질이 같다는 가정은 하지 않는다. 이용자 위치에서 응답 지연을 확인하고, VM에 연결하는 외부 IP도 Standard 등급으로 구성한다. 기존 IP는 등급 간에 그대로 이동할 수 없으므로 변경 시 IP와 DNS 영향도 확인한다. Network Service Tiers\n7. 장기 운영이 확실하면 1년·3년 약정 검토 # 계속 사용할 자원이 확인되면 약정 사용 할인(CUD, Committed Use Discounts)으로 VM 비용을 더 줄일 수 있다. Compute Engine에서는 리소스 기반과 Compute Flexible 두 방식을 구분한다.\n방식 무엇을 약정하나 먼저 검토할 상황 리소스 기반 CUD 특정 리전·머신 계열의 vCPU·메모리 등 자원량 같은 리전·계열의 자원을 꾸준히 사용 Compute Flexible CUD 대상 서비스·자원의 시간당 최소 지출 금액 리전·머신 계열을 바꿀 수 있지만 대상 지출은 꾸준함 둘 다 특정 VM 한 대를 고정하는 계약은 아니다. Flexible은 같은 결제 계정의 대상 사용량에 리전·머신 계열을 넘어서 적용할 수 있지만, 모든 서비스와 요금 항목에 적용되는 것은 아니다. E2의 대상 vCPU·메모리에는 Flexible 기준 1년 28%·3년 46% 할인이 안내되어 있으며, 이를 리소스 기반 CUD의 공통 할인율로 사용하지 않는다. 공식 CUD 비교\nE2 커스텀 2vCPU·메모리 4GiB + HDD 50GiB + 공인 IPv4, 서울 리전·월 730시간으로 통일했다. 디스크는 앞의 네트워크 총비용 예시에 사용한 Balanced가 아니라 HDD다.\n요금 방식 월 VM 요금 HDD 50GiB 공인 IPv4 월 기본비 원화 환산 온디맨드 $54.43 $2.60 $3.65 $60.68 84,952원 Flexible CUD 1년 $39.19 $2.60 $3.65 $45.44 63,616원 Flexible CUD 3년 $29.39 $2.60 $3.65 $35.64 49,896원 리소스 기반 CUD 1년 $33.82 $2.60 $3.65 $40.07 56,098원 리소스 기반 CUD 3년 $24.49 $2.60 $3.65 $30.74 43,036원 VM 요금은 각 방식의 계산기 표시값을 사용하고, HDD $2.60과 공인 IPv4 $3.65를 더했다. HDD·IPv4·인터넷 아웃바운드(이그레스) 비용에 VM 할인율을 일괄 적용하지 않는다. 이 견적에서는 리소스 기반이 같은 기간의 Flexible보다 저렴하지만, 사용할 리전·계열과 변경 가능성을 함께 보고 선택한다.\nCompute Flexible CUD 견적\nCompute Flexible CUD 3년 약정 견적 보기\n리소스 기반 CUD 견적\n리소스 기반 CUD 3년 약정 견적 보기\n표의 약정 금액은 이 VM 사용량을 충당하는 규모로 자원량 또는 지출 금액을 약정하고 매시간 활용한다는 가정의 월 환산 예산이다. 필요 이상으로 약정하거나 활용하지 못하면 이 예산과 달라진다. 두 방식 모두 VM을 중지·삭제해도 약정은 남으며, 사용하지 않은 시간에도 약정 비용을 낸다. 구매 후 취소할 수 없으므로 처음부터 할인율만 보고 3년 약정을 선택하지 않는다. 약정 적용 방식과 구매 조건\n처음에는 온디맨드로 운영하고, 필요한 사양과 지속 사용량을 확인한 뒤 계속 사용할 자원량이나 금액만 약정한다. 위 월 기본비에는 인터넷 데이터 전송과 백업 등 별도 비용이 포함되지 않는다.\n8. 실제 견적에 적용할 순서 # 서울 리전, Linux, 730시간, Regular·온디맨드 조건으로 계산한다. 낮은 CPU 사용량이면 e2-medium, 지속 CPU 자원이 필요하면 E2 커스텀 2vCPU·4GiB를 후보로 정한다. HDD 50GiB·100GiB와 Balanced 50GiB를 비교한다. 로컬 DB와 랜덤 I/O 요구를 함께 본다. 공인 IPv4를 별도 항목으로 더하고, 인터넷 아웃바운드(이그레스)는 Standard·Premium을 나눠 계산한다. 백업·관리형 DB·로그·DNS 등 실제 사용할 별도 서비스를 더한 뒤 예산 알림을 설정한다. 온디맨드로 운영하며 필요한 사양과 지속 사용량을 확인한 뒤, 계속 사용할 자원량이나 금액만 약정한다. 알림 전용 예산은 초과 지출을 자동으로 차단하는 설정이 아니다. 예상하지 못한 트래픽이나 로그 비용을 발견할 수 있도록 대상 프로젝트와 알림 기준을 확인한다. 예산과 알림 설정\n요청이 드문 서버는 e2-medium, 공유 코어의 지속 CPU 할당량이 부족하면 커스텀 2vCPU·4GiB부터 검토한다. HDD와 Balanced 50GiB의 기본비 차이는 월 $3.90이다. 메모리를 줄이는 효과만 보지 말고, 실제 부하에서 메모리 부족·CPU 사용량·디스크 대기·응답 시간을 확인해 사양을 결정한다.\n","date":"2026년 10월 16일","externalUrl":null,"permalink":"/posts/gcp-vm-cost-optimization/","section":"글 목록","summary":"","title":"GCP VM 비용 줄이기: 소규모 백엔드 서버의 사양과 요금 선택","type":"posts"},{"content":"2vCPU·메모리 8GB 서버를 한 달 운영하면 어느 클라우드가 저렴할까? VM 요금만으로는 답하기 어렵다. 디스크·공인 IP를 더하고 인터넷 아웃바운드(이그레스) 트래픽을 늘려보면 예상 비용이 달라진다.\nGCP·AWS·Azure·네이버클라우드의 공식 계산기와 요금표를 확인하고, VM·디스크·공인 IP의 기본 비용과 인터넷 데이터 전송 요금을 나누어 계산했다. CPU 크레딧 적용 여부와 아키텍처가 다른 후보도 함께 정리했다. 실제 성능 측정이나 청구서 비교가 아닌 공개 요금에 따른 예산 예시다.\n물리 서버 구매·임대까지 검토한다면 서버호스팅 vs 클라우드 비용 비교를 참고한다.\n비교 조건 # 가격 확인일은 2026년 10월 16일이다. 제공된 견적 캡처와 공식 가격표를 기준으로 계산했으며 실제 도입 시에는 같은 설정으로 다시 조회해야 한다.\n항목 적용 조건 리전 GCP 서울, AWS 서울, Azure Korea Central, NCP 한국 서버 Linux VM 1대, 2vCPU·메모리 8GB급 사용 시간 월 730시간. 실제 청구 월의 시간 수와 구분하는 비교 가정 VM 요금 시간당 온디맨드·종량제. 약정·Spot·가입 크레딧 제외 저장소 GCP OS 100GiB, AWS OS 화면 표기 100GB, Azure OS E10 128GiB. NCP는 OS SSD 50GB + 추가 SSD 50GB 공인 IP 사용 중인 외부 IPv4 1개 트래픽 목적지 국내 이용자에게 인터넷으로 직접 전송 아웃바운드(이그레스) 트래픽 예시 각 CSP 요금표의 계량 단위로 월 100·500·1,024. GCP는 GiB, 나머지는 표기상 GB 환율·세금 $1 = 1,400원 가정, VAT 제외 별도 비용 CPU 크레딧 추가 과금, 스토리지 트랜잭션·추가 성능, 백업·DB·LB·NAT Gateway·CDN·유료 지원·운영 인건비 같은 성능의 서버를 비교하는 표는 아니다. 메모리·디스크의 GB/GiB 표기, CPU 플랫폼과 할당 방식, 스토리지 성능이 다르다. 아웃바운드(이그레스) 트래픽 표도 각 공급사의 계량값을 넣은 예산 예시다. 동일한 파일을 정확히 같은 바이트 수만큼 전송한 요금 순위가 아니며, 확인되지 않은 GB의 바이트 정의를 임의로 통일하지 않았다.\nUSD 금액의 원화 환산은 비교용이다. 실제 원화 결제에는 공급사의 환율·통화별 가격 정책이 적용되며, GCP는 해당 통화의 SKU 가격을 적용한다고 안내한다.\n종합 비교표 # VM·디스크·공인 IPv4의 기본 유지비와 인터넷 데이터 전송 요금을 합산한 월 예산이다. 금액은 $1 = 1,400원 가정, VAT 제외다. 트래픽별 각 셀에는 적용한 GB·GiB 단위와 총비용을 함께 표시했다.\nGCP N2는 SUD 미적용이며, CPU 크레딧 추가 과금과 스토리지 트랜잭션 등은 제외다. NCP는 OS 50GB와 추가 데이터 볼륨 50GB 구성이다. 같은 성능·동일 바이트 전송의 최저가 순위가 아니라 상품별 비용 구조를 비교한다.\nCSP VM·CPU 조건 디스크 구성 네트워크 기본비\n(원/월) 100GB·GiB\n월 총비용 500GB·GiB\n월 총비용 1,024GB·GiB\n월 총비용 GCP E2 (x86)\n크레딧 미적용 Balanced 100GiB Premium 111,160원 100GiB\n137,760원 500GiB\n244,160원 1,024GiB\n383,544원 GCP E2 (x86)\n크레딧 미적용 Balanced 100GiB Standard 111,160원 100GiB\n111,160원 500GiB\n161,140원 1,024GiB\n248,438원 GCP N2 (x86)\n크레딧 미적용 Balanced 100GiB Premium 150,766원 100GiB\n177,366원 500GiB\n283,766원 1,024GiB\n423,150원 GCP N2 (x86)\n크레딧 미적용 Balanced 100GiB Standard 150,766원 100GiB\n150,766원 500GiB\n200,746원 1,024GiB\n288,044원 AWS T3 (x86)\n크레딧 적용 gp3 100GB 표기 인터넷 124,166원 100GB\n124,166원 500GB\n194,726원 1,024GB\n287,160원 AWS T3a (x86)\n크레딧 적용 gp3 100GB 표기 인터넷 113,540원 100GB\n113,540원 500GB\n184,100원 1,024GB\n276,534원 AWS T4g (Arm)\n크레딧 적용 gp3 100GB 표기 인터넷 103,054원 100GB\n103,054원 500GB\n173,614원 1,024GB\n266,048원 Azure B2as v2 (x86)\n크레딧 적용 E10 128GiB Microsoft Premium 114,212원 100GB\n114,212원 500GB\n181,412원 1,024GB\n269,444원 Azure D2as v5 (x86)\n크레딧 미적용 E10 128GiB Microsoft Premium 126,882원 100GB\n126,882원 500GB\n194,082원 1,024GB\n282,114원 NCP g2 (x86)\n크레딧 미적용 OS SSD 50GB\n+ SSD 50GB 인터넷 99,718원 100GB\n107,718원 500GB\n147,718원 1,024GB\n200,118원 아래에서 각 견적의 구성과 무료 트래픽 적용 과정을 확인할 수 있다.\n비교 대상 VM 사양 # CSP 비교 상품 CPU·아키텍처 CPU 크레딧 GCP e2-standard-2 Intel 또는 AMD x86, 자동 선정 미적용 GCP n2-standard-2 Intel x86 미적용 AWS t3.large Intel x86 적용 AWS t3a.large AMD x86 적용 AWS t4g.large AWS Graviton2, Arm 적용 Azure Standard_B2as_v2 AMD x86 적용 Azure Standard_D2as_v5 AMD x86 미적용 NCP s2-g2-s50 + 추가 SSD 50GB x86, g2 XEN 기반 미적용 버스트 가능(Burstable) VM은 CPU 크레딧을 사용해 기본 수준을 초과하는 CPU 성능을 낼 수 있는 VM이다. AWS 한국어 문서에서도 ‘버스트 가능 성능 인스턴스’라는 표현을 사용한다.\nAWS T3는 CPU를 적게 쓸 때 크레딧을 쌓고 기본 성능보다 많이 쓸 때 소모한다. t3.large의 기본 CPU 사용률은 30%이며, Unlimited에서는 장시간 부하에 따라 추가 CPU 요금이 발생할 수 있다. 반면 Azure B2as v2는 크레딧을 모두 쓰면 기본 CPU 성능으로 제한된다. 둘 다 버스트형이지만 크레딧 소진 후 동작이 같지는 않다. AWS T3, AWS Unlimited, Azure Basv2\nGCP e2-standard-2는 e2-micro·e2-small·e2-medium 같은 shared-core 상품과 다르다. 이름에 E2가 있다고 모두 CPU 버스트 상품으로 묶지 않는다. T4g는 사용하는 컨테이너 이미지·네이티브 라이브러리·실행 파일의 Arm 지원을 확인한 뒤 선택한다. GCP E2, AWS T4g\nAWS 후보 선택 화면에서도 같은 2vCPU·8GiB에 T3·T3a·T4g가 서로 다른 가격으로 표시된다. 네트워크의 ‘최대 5Gbps’가 같더라도 CPU 아키텍처와 성능 조건까지 같다는 뜻은 아니다.\n확보한 견적에 맞춰 GCP·Azure는 각각 2종, AWS는 3종을 사용했다. NCP는 SSD가 포함된 g2 한 종을 사용한다. g1·HDD 상품이나 디스크 비용이 빠진 g3를 같은 조건으로 넣지 않았다.\nVM과 OS 디스크 비용 # GCP: E2와 N2 # 서울 asia-northeast3, Linux, 730시간, Balanced persistent disk 100GiB 구성이다.\n항목 E2 e2-standard-2 N2 n2-standard-2 VM $62.75 $91.04 Balanced 디스크 100GiB $13.00 $13.00 VM + 디스크 $75.75 $104.04 N2 견적은 지속 사용 할인(SUD)을 적용하지 않은 표시값이다. 실제로 계속 운영하면 적용 대상 자원은 자동 할인 조건을 충족할 수 있으므로, 아래 N2 금액을 실제 청구액이라고 단정하면 안 된다. 이 글은 캡처의 할인 전 기준값을 유지한다. E2는 SUD 대상이 아니다. GCP 지속 사용 할인\nAWS: T3·T3a·T4g # 서울 ap-northeast-2, Linux, 공유 테넌시, VM 1대, 월 730시간의 온디맨드 구성이다. 디스크는 gp3이며 추가 IOPS·처리량 입력 없이 화면 표기 100GB를 선택했다.\n항목 T3 t3.large T3a t3a.large T4g t4g.large 시간당 VM $0.104 $0.0936 $0.0832 730시간 VM $75.92 $68.33 $60.74 gp3 디스크, 화면 표기 100GB $9.12 $9.12 $9.12 VM + 디스크, 계산기 표시값 $85.04 $77.45 $69.86 계산기의 월 합계를 사용했다. T3a·T4g는 시간당 단가를 곱한 VM 요금이 각각 $68.328·$60.736이며 표시는 센트 단위로 반올림된다. T3a 견적과 T4g 견적에서도 디스크 및 합계를 확인할 수 있다. CPU 크레딧 추가 과금은 위 합계에 포함하지 않았다.\nAzure: B2as v2와 D2as v5 # Korea Central, Linux, 종량제 730시간, E10 디스크 1개의 견적이다.\n항목 B2as v2 D2as v5 CPU 사용 방식 버스트형 일반 목적 VM 시간당 VM $0.0936 $0.106 730시간 VM, 반올림 전 $68.328 $77.38 E10 LRS 디스크 1개 $9.60 $9.60 VM + E10 디스크 1개 $77.93 $86.98 Azure 디스크는 Standard SSD E10, 128GiB다. 다른 상품의 100 단위 디스크와 용량·성능이 같지는 않으며, 별도 스토리지 트랜잭션 요금은 포함하지 않았다. Azure 디스크 종류\n견적의 리전 간 5GB 항목은 위 기본비에 추가 금액을 만들지 않는다. 공식 Retail Prices API에서 Korea Central의 Linux VM 시간당 단가와 E10 LRS 월 $9.60을 대조하면 0.0936 × 730 + 9.60 = $77.928, 0.106 × 730 + 9.60 = $86.98로 합계가 재현된다. 인터넷 데이터 전송 요금은 뒤에서 별도로 계산한다.\nNCP: g2 OS SSD 50GB + 추가 SSD 50GB # NCP g2는 공식 서버 생성 가이드의 Linux 부팅 디스크 50GB 조건에 맞춰 s2-g2-s50을 선택했다. 2vCPU·메모리 8GB, OS SSD 50GB에 추가 SSD 50GB를 더하는 구성이다. 가격표의 100GB 부팅 디스크 행을 Linux용으로 그대로 적용하지 않는다.\nVM + OS SSD 50GB = 시간당 123원 × 730시간 = 89,790원 추가 SSD 50GB = 0.16원/GB·시간 × 50GB × 730시간 = 5,840원 합계 = 95,630원 추가 SSD 단가는 공식 Block Storage 요금의 XEN SSD 계산 예시에서 확인했다. 추가 볼륨 요금은 캡처된 서버 요금에 별도로 산출한 값이다. OS 50GB + 데이터 50GB는 OS 디스크 하나가 100GB인 구성과 다르다. OS 볼륨 자체에 100GB가 필요하면 g3 등 다른 구성으로 견적을 다시 받아야 한다.\ns2-g2-s50의 표기 월 요금제는 88,000원이다. 이 글에서는 730시간의 시간 요금제로 맞췄으며, 실제로 월 요금제를 선택하면 추가 볼륨과 함께 다시 계산한다. g3의 115원/시간은 디스크가 빠진 요금이므로 이 합계와 직접 비교하지 않는다. NCP 한국 리전 요금표\n공인 IPv4까지 더하면 월 유지비는 얼마일까 # 월 기본 유지비 = VM + OS·추가 데이터 디스크 + 사용 중인 공인 IPv4 1개 GCP는 일반 VM에서 사용하는 외부 IP의 공식 월 표시값 $3.65를 사용했다. 미사용 고정 IP의 요금과 구분한다. 무료 1시간 적용 여부에 따른 세부 차이는 여기서 별도 차감하지 않았다. AWS와 Azure도 제공된 730시간 견적이 각각 $3.65다. GCP IP 가격표, AWS VPC 가격표\nAzure는 Korea Central·표준(ARM) 공인 IP 1개다. ARM은 여기서 CPU 아키텍처가 아니라 Azure Resource Manager라는 배포·관리 방식을 의미한다.\nNCP 계산기의 공인 IP 4,032원은 시간당 5.6원으로 720시간을 계산한 값이다. 730시간 기준으로는 5.6원 × 730 = 4,088원을 사용한다.\nCSP 상품 VM + 디스크 IPv4 월 기본 유지비 원화 환산 GCP E2 $75.75 $3.65 $79.40 111,160원 GCP N2, SUD 미적용 $104.04 $3.65 $107.69 150,766원 AWS T3 $85.04 $3.65 $88.69 124,166원 AWS T3a $77.45 $3.65 $81.10 113,540원 AWS T4g, Arm $69.86 $3.65 $73.61 103,054원 Azure B2as v2 $77.93 $3.65 $81.58 114,212원 Azure D2as v5 $86.98 $3.65 $90.63 126,882원 NCP g2 SSD 95,630원 4,088원 99,718원 99,718원 이 표에는 인터넷 데이터 전송 요금이 없다.\n인터넷 아웃바운드(이그레스) 트래픽 요금 # API 응답이나 이미지·파일 다운로드는 서버에서 인터넷으로 데이터를 내보내는 작업이다. 같은 VM이라도 아웃바운드(이그레스) 트래픽과 네트워크 등급에 따라 월 요금이 달라진다. 무료량은 다른 자원에서 사용하지 않았다고 가정하며, VM 수만큼 중복 적용하지 않는다.\nCSP 네트워크 경로 적용 무료량 이번 예시에 적용할 단가 단위 GCP Premium, 서울 → 한국 해당 구간 무료량 없음 $0.19 GiB GCP Standard, 서울 출발 월 200GiB 무료량 이후 $0.119 GiB AWS 서울 → 인터넷 월 100GB 무료량 이후 $0.126 GB 표기 Azure 한국 → 인터넷, Microsoft Premium Global Network 월 100GB 무료량 이후 $0.12 GB 표기 NCP 한국 → 인터넷 월 20GB 무료량 이후 100원 GB 표기 단가는 아래의 월 100·500·1,024 계량값에 적용되는 구간만 정리했다. 더 큰 사용량에는 별도 구간 단가가 적용될 수 있다. GCP 네트워크 요금, AWS EC2 데이터 전송 안내, Azure Bandwidth, NCP 요금표\nGCP Premium과 Standard는 별도 시나리오다 # GCP는 기본 Premium과 명시적으로 선택하는 Standard를 분리했다. Premium은 Google 백본을 활용하고, Standard는 인터넷 통신 사업자의 경로를 더 활용한다. 가격뿐 아니라 서비스가 지원하는 등급과 실제 지연·품질을 확인해야 한다. Standard 시나리오는 VM 외부 IP 등 관련 리소스도 그 등급으로 설정한다는 가정이다. Network Service Tiers\nStandard는 첫 200GiB 무료 이후 위 단가를 적용한다. 무료량은 계정 내 사용량의 집계 조건을 따르므로 리소스마다 200GiB를 반복 차감하지 않는다.\nPremium의 한국 목적지 단가는 서울 → 한국 가격표 캡처로 확인할 수 있다. 목적지를 선택할 때 대한민국이 제외된 다른 아시아 구간과 구분한다.\nAWS 데이터 전송 요금과 무료 트래픽 # 전체 아웃바운드(이그레스) 트래픽이 1,024GB이고 무료 100GB가 남았다는 가정이면 (1,024 − 100) × $0.126 = $116.424다. 무료량 차감 없이 전체 아웃바운드(이그레스) 트래픽에 단가를 곱한 금액을 월 총액에 더하지 않는다.\nAWS 무료량은 대상 서비스와 리전을 합산한다. 다른 VM이나 서비스에서 이미 사용했다면 그만큼 무료량이 줄어든다. AWS 무료 아웃바운드(이그레스) 트래픽 안내\nAzure와 NCP # Azure의 월 100GB는 인터넷 아웃바운드(이그레스) 무료량이다. 계산기에 안내되는 리전 간 무료 전송과 별개이며, 인터넷 아웃바운드(이그레스) 트래픽에서 105GB를 빼는 계산은 하지 않는다. 이 글에서는 Microsoft Premium Global Network 경로의 단가를 사용한다. Azure 인터넷 아웃바운드(이그레스) 요금\nNCP는 첫 20GB 이후 첫 유료 구간이 100원/GB다. 제공된 계산기의 1,024GB 견적도 (1,024 − 20) × 100 = 100,400원과 일치한다.\n월 트래픽에 따른 총비용 # 공급사별 계량 단위 기준이며 동일 바이트 비교가 아니다. GCP 행은 GiB, 나머지는 GB 표기다. 다른 행의 1,024GB를 1TiB라고 부르지 않는다.\n월 예상 비용 = 월 기본 유지비 + 해당 상품·경로의 인터넷 데이터 전송 요금 GCP Standard = max(월 트래픽 GiB − 200, 0) × $0.119 AWS = max(월 트래픽 GB − 100, 0) × $0.126 Azure = max(월 트래픽 GB − 100, 0) × $0.12 NCP = max(월 트래픽 GB − 20, 0) × 100원 앞의 종합 비교표는 위 계산식을 각 상품의 기본 유지비에 적용한 결과다.\nUSD 단가의 계산은 중간 반올림 전 값을 유지하고 가정 환율 1,400원을 곱한 뒤 원 단위로 반올림했다. 기본 유지비는 앞의 계산기 표시값을 사용했다. 별도 비용과 세금은 비교 조건에서 제외한 그대로다.\n예를 들어 GCP E2에서 500GiB를 전송하면 VM·디스크·IP는 두 경로 모두 $79.40이다. Premium은 데이터 전송 요금 $95를 더해 244,160원, Standard는 무료 200GiB 이후 $35.70을 더해 161,140원이다. 같은 VM이라도 네트워크 등급에 따라 이 예시에서는 월 83,020원이 달라진다.\nNCP g2에서 500GB를 전송하면 기본 유지비 99,718원에 데이터 전송 요금 48,000원을 더해 147,718원이다. 서버 가격만 보던 단계에서 빠져 있던 비용이 월 총액에 추가된다.\n어떤 조건에서 무엇을 먼저 검토할까 # 사용 조건 먼저 검토할 것 CPU 평균 사용량이 낮고 간헐적으로 요청이 몰림 AWS T3·T3a, Azure B2as v2. CPU 크레딧 잔량·추가 요금·제한 동작 확인 CPU를 장시간 많이 사용함 GCP E2·N2, Azure D2as v5 등 일반 목적 VM과 AWS M 계열의 추가 견적. 실제 부하 테스트로 처리량 확인 Arm으로 실행 가능한 서비스 T4g를 별도 후보로 검토. 사용하는 이미지·라이브러리·에이전트 호환성 확인 GCP에서 인터넷 데이터 전송 요금 비중이 큼 Standard 지원 여부와 지연·품질을 확인한 뒤 Premium과 별도 견적 한 달 내내 운영할 NCP 서버 시간 요금제와 월 요금제 비교. g2 SSD 포함 요금과 g3의 디스크 별도 요금 구분 이미지·파일 다운로드가 많음 CPU보다 먼저 월 아웃바운드(이그레스) 트래픽·단위·무료량·네트워크 경로·CDN 포함 비용 확인 서버를 고를 때는 실행 가능한 CPU 아키텍처와 지속 부하 조건부터 정한다. 그다음 OS 디스크와 공인 IP를 더하고, 예상 아웃바운드(이그레스) 트래픽에 맞춰 월 총액을 계산한다. CPU 크레딧 추가 과금이나 스토리지 트랜잭션이 발생하는 서비스라면 이 글의 예산표에 해당 비용을 추가해야 한다.\n공식 가격표와 계산기 # GCP: 가격 계산기, VM 가격표, 네트워크·IP 가격표. AWS: 가격 계산기, EC2 온디맨드·데이터 전송 요금, EBS, VPC·Public IPv4. Azure: 가격 계산기, 공인 IP, Bandwidth. NCP: 한국 리전 요금표, 가격 계산기, 서버 사양. ","date":"2026년 10월 16일","externalUrl":null,"permalink":"/posts/cloud-vm-cost-comparison/","section":"글 목록","summary":"","title":"GCP·AWS·Azure·네이버클라우드 VM 비용 비교: 서버 요금부터 이그레스까지","type":"posts"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/naver-cloud/","section":"Tags","summary":"","title":"Naver-Cloud","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/region/","section":"Tags","summary":"","title":"Region","type":"tags"},{"content":"","date":"2026년 10월 16일","externalUrl":null,"permalink":"/tags/vm/","section":"Tags","summary":"","title":"Vm","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/cloud/","section":"Tags","summary":"","title":"Cloud","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/colocation/","section":"Tags","summary":"","title":"Colocation","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/compaction/","section":"Tags","summary":"","title":"Compaction","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/kafbat-ui/","section":"Tags","summary":"","title":"Kafbat-Ui","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/kafka/","section":"Tags","summary":"","title":"Kafka","type":"tags"},{"content":"Kafka offset 글에서는 메시지를 읽고 commit해도 토픽의 데이터가 남는다고 설명했다. 그렇다면 그 데이터는 언제 삭제될까?\n기간 기반 삭제에서는 소비자의 처리 여부가 아니라, 토픽의 보관 기간과 용량 설정이 삭제를 결정한다. 이번에는 메시지 3개를 발행하고 보관 기간을 1시간에서 20초로 줄여, UI에서 실제 삭제 전·후를 확인했다.\n1. 읽기·commit·삭제는 다른 동작이다 # 동작 바뀌는 것 소비자가 읽음 실행 중 소비자의 읽기 위치가 진행됨 소비자가 offset을 commit 그룹의 재개 위치가 저장됨 보관 정책으로 삭제 토픽에서 오래된 데이터를 더 이상 읽을 수 없게 됨 모든 소비자가 읽을 때까지 Kafka가 메시지를 보관하는 것은 아니다. 소비자가 아직 읽지 않았더라도 삭제 조건을 충족하면 데이터가 사라질 수 있다. 그룹마다 읽기 위치가 다르다는 것과, 토픽에 데이터가 남아 있다는 것은 별개의 조건이다. Kafka 토픽 보관 설정\n읽기 위치와 commit의 상세 동작은 Kafka offset 이해하기를 참고한다. 이번 실습은 업무 소비자를 실행하지 않고 UI로 조회만 했다.\n2. 기간과 용량으로 삭제하는 delete 정책 # cleanup.policy=delete에서는 다음 설정으로 오래된 데이터의 삭제 기준을 정한다.\n설정 의미 retention.ms 보관 기간, 밀리초 단위 retention.bytes 파티션별 보관 용량 제한, 바이트 단위 예를 들어 보관 기간을 7일로 설정해도 용량 제한이 함께 있으면, 데이터가 많이 쌓여 7일보다 먼저 오래된 데이터가 삭제될 수 있다. retention.bytes는 토픽 전체가 아니라 파티션별 제한이다.\n삭제는 메시지 한 건마다 별도 타이머를 붙여 실행하는 방식이 아니다. Kafka는 데이터를 로그 세그먼트라는 파일 단위로 관리하고, 삭제 조건을 충족한 오래된 세그먼트를 정리한다. 따라서 보관 기간이 끝나는 순간 모든 메시지가 정확히 삭제된다고 해석하지 않는다. Kafka delete 정책과 retention\n3. 메시지 3개를 넣고 삭제 전 확인 # 실행 파일 준비 # Windows의 Docker Desktop과 WSL Ubuntu를 사용했다. 설치가 필요하면 Docker Desktop 설치와 WSL 연동을 먼저 진행한다.\n아래 두 파일을 다운로드해 같은 작업 디렉터리에 둔다.\ncompose.yaml: Kafka 4.3.1과 Kafbat UI v1.5.0 실행 설정 history-initial.jsonl: 센서 측정값 테스트 메시지 3개 kafka-message-retention-guide/ compose.yaml history-initial.jsonl 이번 Compose 프로젝트 이름은 blog-kafka-cleanup-lab이다. 기존 Kafka UI·consumer group 실습과 별도 컨테이너와 데이터 볼륨을 사용하므로 이전 토픽은 변경하지 않는다. 단일 노드의 로컬 실습용 구성이며 운영 환경에 그대로 적용하지 않는다.\nDocker Desktop을 켜고 작업 디렉터리의 WSL Bash 터미널에서 실행한다. 캡처에서는 ~/kafka-message-retention-guide를 사용했다.\ncd ~/kafka-message-retention-guide docker compose up -d --wait --wait-timeout 180 docker compose ps Windows 브라우저에서 **http://localhost:18081/**을 열면 cleanup-demo 클러스터가 표시된다. 이전 실습의 18080과 다른 포트다. UI는 읽기 전용으로 설정하고 127.0.0.1에만 연결했다. 로그인 없는 이 구성을 인터넷에 공개하지 않는다.\n토픽 생성과 메시지 발행 # 첫 캡처를 여유 있게 찍을 수 있도록 처음에는 보관 기간을 1시간으로 설정한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-topics.sh \\ --bootstrap-server kafka:9092 --create --topic sensor-history \\ --partitions 1 --replication-factor 1 \\ --config cleanup.policy=delete \\ --config retention.ms=3600000 \\ --config retention.bytes=-1 \\ --config segment.ms=5000 \\ --config file.delete.delay.ms=1000 용량 제한은 retention.bytes=-1로 두어 이번에는 기간에 따른 삭제만 확인한다. segment.ms와 파일 삭제 지연은 짧은 실습을 위한 값이다. 제공한 Compose에서도 삭제 점검 주기를 5초로 설정했다. 운영 권장값이 아니라, 작은 데이터로 결과를 오래 기다리지 않도록 구성한 것이다.\n토픽이 이미 존재한다고 나오면 먼저 상태를 확인한다. 처음부터 다시 진행하려면 이후 명령의 토픽 이름을 모두 sensor-history-02 등으로 바꿔 기존 데이터와 구분한다.\nhistory-initial.jsonl에는 실제 센서 정보가 아닌 다음 테스트 데이터를 넣었다.\n{\u0026#34;event\u0026#34;:\u0026#34;sensor.reading\u0026#34;,\u0026#34;reading_id\u0026#34;:\u0026#34;reading-001\u0026#34;,\u0026#34;value\u0026#34;:10} {\u0026#34;event\u0026#34;:\u0026#34;sensor.reading\u0026#34;,\u0026#34;reading_id\u0026#34;:\u0026#34;reading-002\u0026#34;,\u0026#34;value\u0026#34;:11} {\u0026#34;event\u0026#34;:\u0026#34;sensor.reading\u0026#34;,\u0026#34;reading_id\u0026#34;:\u0026#34;reading-003\u0026#34;,\u0026#34;value\u0026#34;:12} 다음 명령은 한 번만 실행한다. 반복 실행하면 메시지 3개가 추가된다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \\ --bootstrap-server kafka:9092 --topic sensor-history --sync \\ \u0026lt; history-initial.jsonl UI에서 Topics → sensor-history → Messages를 열어 메시지를 조회한다.\n캡처는 Newest로 조회해 최신순으로 표시됐다. 파티션은 하나이며, 메시지의 offset은 0·1·2였다. 첫 메시지의 상세 값에서도 reading-001, value: 10을 확인할 수 있었다.\n4. 보관 기간을 줄이니 메시지가 사라졌다 # 삭제 전 화면을 확인한 뒤, 토픽의 보관 기간을 20초로 변경했다. 다음 명령은 기존 메시지도 삭제 대상에 포함시키므로 테스트 토픽에서만 실행한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-configs.sh \\ --bootstrap-server kafka:9092 --entity-type topics \\ --entity-name sensor-history --alter --add-config retention.ms=20000 잠시 기다린 뒤 UI를 새로 조회한다. 독자가 재현할 때는 다음처럼 먼저 30초 정도 기다리고, 메시지가 남아 있으면 조금 더 기다려 확인하면 된다.\nsleep 30 설정을 바꾼 시점부터 정확히 20초 뒤 삭제된다는 뜻은 아니다. 이미 저장된 데이터의 시간도 기준에 포함되고, 세그먼트 상태와 삭제 점검 주기에 따라 실제 시점이 달라진다. 이번 캡처로 정확한 삭제 소요 시간을 측정한 것은 아니다.\nOverview: 메시지 0개, 시작 위치와 끝 위치는 3 # 삭제 후 Overview에는 다음 값이 표시됐다.\n항목 삭제 후 값 Message Count 0 First Offset 3 Next Offset 3 이 토픽에서는 offset 0·1·2에 있던 레코드가 사라지고, 읽을 데이터의 시작과 끝 위치가 모두 3이 됐다. 데이터가 삭제돼도 위치가 0으로 초기화되지는 않았다. 이 값은 토픽의 위치이며 소비자 그룹의 committed offset을 조회한 결과는 아니다.\nMessages: 새 조회 결과도 비어 있음 # Messages를 다시 조회해도 No messages found였다. 단순히 화면의 행을 숨긴 것이 아니라, 같은 토픽에서 이전 메시지를 더 이상 읽을 수 없는 상태임을 확인했다.\n이번에는 업무 소비자를 실행하지 않았는데도 데이터가 삭제됐다. 업무 소비자의 수신이나 commit을 기다리는 것이 아니라, 토픽의 보관 정책이 적용된 결과다. 이미 삭제된 메시지는 다른 그룹으로 읽거나 이전 offset을 지정한다고 복구되지 않는다.\n실습을 끝낼 때는 같은 작업 디렉터리에서 이번 환경만 중지한다. 데이터 볼륨은 유지한다.\ndocker compose stop 5. Compaction은 무엇이 다를까? # cleanup.policy=compact는 기간 기준으로 오래된 데이터를 지우는 것과 달리, 같은 파티션 안에서 같은 key의 최신 값을 남기도록 과거 값을 정리한다. 다른 파티션에 저장된 같은 key의 레코드까지 비교해 하나만 남기는 것은 아니다.\n아래는 같은 파티션에 기록한 설명용 예시다. 이번 실습에서 compaction을 실행해 얻은 결과는 아니다.\n같은 key에 기록한 순서 key value 정리 후 첫 번째 sensor-A 10 과거 값으로 제거 대상 두 번째 sensor-A 11 과거 값으로 제거 대상 세 번째 sensor-A 12 최신 값으로 유지 이 예시에서 최신 값은 기록 순서상 마지막 값이다. 값의 크기가 가장 큰 것을 선택한다는 뜻은 아니다. 정리도 발행 직후 즉시 수행되는 것은 아니며, 과거 값이 한동안 함께 조회될 수 있다. Kafka log compaction\n센서의 모든 측정 이력을 일정 기간 조회해야 한다면 delete 정책의 보관 기간을 정하는 방식이 맞다. 반면 센서별 현재 상태를 구성하기 위해 최신 값이 필요하다면 compact를 검토할 수 있다. compact,delete로 두 정책을 함께 적용하는 구성도 가능하다. Kafka cleanup.policy\n마무리 # 메시지 3개를 넣은 토픽에서 보관 기간을 줄였더니, UI의 메시지 개수가 0이 되고 새 조회에서도 데이터를 읽을 수 없었다. 소비자의 처리 여부와 데이터 보관은 별도로 관리된다는 점을 확인했다.\ncommit은 그룹의 재개 위치를 저장하고, retention은 데이터를 얼마나 보관할지 정한다. 재처리할 수 있어야 하는 기간을 기준으로 보관 설정을 정하되, 용량 제한이나 소비자의 처리 지연 때문에 필요한 데이터를 먼저 잃지 않는지도 함께 확인해야 한다.\n","date":"2026년 10월 12일","externalUrl":null,"permalink":"/posts/kafka-message-retention-guide/","section":"글 목록","summary":"","title":"Kafka 메시지는 언제 삭제될까? 읽기·commit·보관 기간의 차이","type":"posts"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/retention/","section":"Tags","summary":"","title":"Retention","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/server/","section":"Tags","summary":"","title":"Server","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/server-hosting/","section":"Tags","summary":"","title":"Server-Hosting","type":"tags"},{"content":"","date":"2026년 10월 12일","externalUrl":null,"permalink":"/tags/vps/","section":"Tags","summary":"","title":"Vps","type":"tags"},{"content":"서버를 직접 구매해 IDC에 둘까, 월 요금을 내고 물리 서버를 빌릴까, 클라우드 가상서버를 사용할까? 서버 한 대를 운영하더라도 장비 구매비, 회선 요금, 디스크와 트래픽이 청구되는 방식은 다르다.\n비용을 비교할 때는 월 요금뿐 아니라 장비 구매비와 사용 기간 동안의 총액을 함께 봐야 한다. 물리 서버 한 대를 여러 VM으로 나눠 쓸 수 있다면, 클라우드에서 VM을 각각 임대하는 경우와 비용 구조도 달라진다. 공개 상품의 월 기본요금과 5년 합계, VM 2–3개 운영 예시를 통해 선택 기준을 살펴본다.\n서버 구매·서버호스팅·클라우드는 무엇이 다를까 # 구성 사용하는 자원 기본 비용 구조 직접 관리할 부분 A. 서버 구매 + IDC 이용 구매한 물리 서버를 IDC에 설치. 코로케이션 또는 구매형 서버호스팅 이용 장비 구매비 + 월 상면·전력·회선 요금 OS·프로그램·백업, 장비 고장 시 교체와 지원 계약 B. 물리 서버 임대 사업자가 제공하는 물리 서버 한 대 서버와 IDC 이용이 포함된 월 요금 OS·프로그램·백업. 하드웨어 지원 범위는 계약 확인 C. 월정액 가상서버(VPS) 물리 서버 위에서 실행되는 VM VM·디스크·트래픽을 묶은 월정액 OS·프로그램·백업, 포함량과 초과 사용 관리 자원별 과금 퍼블릭 클라우드 선택한 리전의 VM·디스크와 부가 서비스 VM·디스크·IP·전송량 등 자원별 요금 OS·프로그램·백업, 자원 및 과금 설정 ‘서버호스팅’이라는 상품명만으로 물리 서버 임대인지 판단하면 안 된다. 구매형 서버호스팅과 가상서버 호스팅도 같은 이름을 사용한다. 물리 서버인지 VM인지, 장비 구매비가 있는지, 월 요금에 어떤 자원이 포함되는지 확인한다.\nC의 비용 예시는 클라우드 기반 월정액 VPS 상품이다. 자원을 나누어 과금하는 퍼블릭 클라우드의 비용 항목과 이그레스는 뒤에서 별도로 계산한다.\n서버 한 대, 월 비용과 5년 비용 예시 # 아래는 2026년 10월 12일 확인한 공개 상품 요금을 이용한 계산이다. A/B/C는 운영 방식의 예시 이름이며, 실제 업체 링크와 선택한 상품은 글 아래 출처에 정리했다.\n각 구성은 사양과 서버 위치가 다른 상품이다. 범위는 선정 상품의 가격이며 동일 성능 견적이나 시장 전체 가격대가 아니다. 비용표 아래의 사양·네트워크 조건을 함께 확인한다.\n물리 서버 한 대 또는 VM 한 대를 Linux 서버로 5년, 즉 60개월 유지한다. 선택한 상품의 기본 디스크·네트워크 조건 안에서 사용하고, 기간 중 사양과 요금이 유지된다고 가정한다. 금액은 부가세 제외로 맞춘다. VAT 포함 월정액은 1.1로 나누어 계산했다. 백업 저장소, 운영 인건비, 상용 소프트웨어, 별도 설치·관리비와 초과 사용료는 합계에 포함하지 않았다. 구성 장비 구매비 월 기본요금 장비비 포함 5년 기본비용 A. 서버 구매 + IDC 이용 약 135만–190만 원 89,100–90,000원 6,696,000–7,300,000원 B. 물리 서버 임대 없음 90,000–110,000원 5,400,000–6,600,000원 C. 클라우드 기반 VM 월정액 없음 60,000–120,000원 3,600,000–7,200,000원 합계 계산은 단순하다.\nA 하한: 1,350,000원 + 89,100원 × 60개월 = 6,696,000원 A 상한: 1,900,000원 + 90,000원 × 60개월 = 7,300,000원 B·C: 월 기본요금 × 60개월 A는 선택한 구매형 상품·1대 상면 요금의 양 끝값으로 계산했다. B는 물리 서버 임대 상품 두 개, C는 VM 월정액 상품 두 개의 범위다. 각 범위는 선택한 상품들의 예시이며, 시장 전체의 최저가나 최대 비용은 아니다.\nC의 게시 요금은 VAT 포함 월 66,000–132,000원이며, 표에서는 VAT 제외 월 60,000–120,000원으로 환산했다.\n같은 가격표에서 사양과 네트워크도 확인하기 # 구성 선택한 상품의 사양 네트워크·위치 조건 A. 서버 구매 + IDC 이용 물리 CPU 4코어, 메모리 8GB, SATA HDD 1TB 선택한 구매형 상품 또는 1대 상면 이용료. 회선의 공유 여부와 보장 대역폭은 공급사별 확인 B. 물리 서버 임대 물리 CPU 4코어, 메모리 16–32GB, SSD 250GB + SATA 1TB 상품 표기상 10Mbps Bandwidth, 1Gbps Line C. 클라우드 기반 VM 월정액 가상 CPU 2–4코어, 메모리 4–8GB, 디스크 80–160GB 월 트래픽 4–5TB 포함. 공식 상품 안내상 서버 위치는 일본 물리 CPU와 가상 CPU는 코어 수만으로 처리 성능이 같다고 볼 수 없다. CPU 세대, 메모리 용량, HDD·SSD 구성도 다르므로 위 표는 같은 성능의 견적 비교가 아니다. 필요한 자원을 골랐을 때 비용이 어떻게 쌓이는지 보는 예시다.\nA의 공개 장비 목록에는 오래된 CPU 세대가 포함되어 있다. 실제 구입 시에는 현재 판매 여부와 보증 조건부터 확인해야 한다. A의 5년 합계는 기간 중 장비 교체가 없다는 가정이며, 5년 사용 가능성이나 무고장을 보장하는 금액이 아니다. 장비의 잔존 가치와 부품 교체비는 TCO에서 따로 반영한다.\nB의 1Gbps 회선 표기와 10Mbps 대역폭 표기는 구분해야 한다. 회선 연결 속도만 보고 기본요금에 1Gbps 사용량이 포함된 것으로 계산하면 안 된다. C는 월 누적 트래픽 기준이며, 기준을 넘으면 초과 사용료가 추가된다.\n서버 위치도 비용만큼 중요하다. C의 예시 상품은 일본에 있으므로 국내 데이터 보관이나 국내 서버 위치가 요구되는 서비스라면 다른 지역의 상품으로 견적을 다시 받아야 한다. 실제 지연 시간은 이용자 위치와 네트워크 경로를 고려해 확인한다.\n물리 서버 한 대에서 VM 2–3개를 운영한다면 # 구성 A와 B의 물리 서버에 Proxmox VE나 KVM 같은 하이퍼바이저를 설치하면 CPU·메모리·디스크를 나눠 여러 VM을 운영할 수 있다. 공급사의 하이퍼바이저 설치 허용 여부, OS 재설치 방법과 CPU 가상화 기능부터 확인한다. Proxmox VE 하드웨어 요구사항은 가상화 기능과 호스트·게스트에 필요한 메모리를 별도로 안내한다.\n예를 들어 B의 월 11만 원 상품은 물리 CPU 4코어, 메모리 32GB, SSD 250GB와 SATA 1TB를 제공한다. 여기에 각각 2vCPU·메모리 4GB·디스크 80GB인 VM을 2–3개 배치하는 자원 배분을 검토할 수 있다. 아래는 실제 구축·성능 검증 결과가 아닌 배분과 요금 계산 예시다.\nVM 3개에 메모리 12GB를 배정하면 나머지는 호스트와 캐시 등에 사용할 수 있다. 디스크는 VM 합계 240GB 외에 호스트 OS 공간도 필요하므로 SSD·HDD 배치를 함께 계획한다. 총 6vCPU는 물리 4코어를 공유한다. 모든 VM이 CPU를 계속 많이 사용하는 서비스라면 이 사양에서 원하는 처리량을 낼 수 있는지 테스트해야 한다.\nVM 디스크를 모두 SSD에 배치할 수 있다고 가정하지는 않는다. 포맷 후 사용 가능 용량과 GB·GiB 차이, 호스트·스냅샷 공간을 확인하고, HDD에 배치하는 VM의 I/O 성능은 별도로 검증한다.\n비교 대상은 C의 2vCPU·4GB·80GB VM 상품이며, VM 한 개당 VAT 제외 월 6만 원이다. 포함된 네트워크 조건과 서버 위치는 앞의 사양표와 같고, 가상화 운영비·추가 IP·트래픽 초과료는 아래 기본요금에 포함하지 않았다.\nB의 계약 대역폭 10Mbps는 호스트 내 모든 VM이 공유하고, C는 VM마다 월 트래픽 4TB가 제공된다. 아래 표는 VM 수에 따른 기본요금 차이를 보여주며, 처리 성능·저장소·네트워크·장애 범위가 같다는 뜻은 아니다.\n비용 항목 VM 2개 운영 VM 3개 운영 물리 호스트 한 대의 월 기본요금 110,000원 110,000원 클라우드 VM의 월 기본요금 합계 60,000원 × 2 = 120,000원 60,000원 × 3 = 180,000원 물리 호스트 한 대의 5년 기본요금 6,600,000원 6,600,000원 클라우드 VM의 5년 기본요금 합계 7,200,000원 10,800,000원 5년 기본요금 차액(클라우드 VM − 물리 호스트) 600,000원 4,200,000원 물리 서버의 자원 안에서 여러 VM을 운영할 수 있다면, VM 수가 늘어날수록 클라우드 VM의 기본요금 합계와 차이가 벌어질 수 있다. 이 예시의 차이는 VM 2개에서 월 1만 원, 3개에서 월 7만 원이다. 추가 운영·백업·IP 비용과 필요한 디스크 구성을 반영한 뒤 비용 이점을 판단한다. 2개 예시의 5년 차액은 60만 원이므로, 가상화 운영의 추가 비용이 이보다 크면 기본요금상의 이점은 사라진다.\n앞의 A 예시 장비는 메모리가 8GB다. 4GB VM을 2–3개 운영하려면 호스트 메모리까지 고려해 증설이나 상위 모델 구입비를 다시 계산해야 한다. VM 수만 늘리고 같은 장비 구매비를 그대로 적용할 수는 없다.\nVM마다 공인 IP가 필요하면 추가 IP 요금과 공급사의 네트워크 정책을 확인한다. 하나의 공인 IP를 공유하는 경우에는 NAT·포트 전달 또는 웹 서비스용 리버스 프록시 등의 구성이 필요하다. 물리 호스트가 고장 나거나 재부팅되면 그 안의 VM도 모두 영향을 받는다. 웹·DB를 VM으로 분리하더라도 같은 물리 호스트에 있으면 호스트 장애에 대한 이중화가 되지 않는다.\n퍼블릭 클라우드는 어떤 비용을 더해야 할까 # C는 VM·디스크·일정량의 트래픽이 포함된 월정액 가상서버 상품이다. Google Cloud Compute Engine처럼 자원을 나누어 과금하는 상품은 다음 항목을 합산한다.\n월 예상 비용 = VM + 디스크 + 공인 IP + 외부로 보내는 데이터 + 백업 + 추가로 사용하는 DB·로드 밸런서 등 이 방식에서는 리전, VM 사양, 디스크 종류·용량, 공인 IP 개수, 송신 데이터량을 먼저 정한다. 월 사용 시간은 730시간을 비교 가정으로 사용하고, 5년에는 730시간 × 60개월 = 43,800시간을 적용한다. 실제 청구는 해당 상품의 사용 시간과 요금 정책에 따른다. 할인 약정이나 무료 크레딧을 적용한다면 적용 전·후 금액을 구분한다.\n예를 들어 Google Cloud 가격 계산기에 서울 리전(asia-northeast3), VM 한 대, 월 730시간, 필요한 디스크와 IP, 송신량을 넣고 전체 금액을 산출한다. 원화로 비교할 때는 적용 환율과 VAT 기준도 함께 적는다.\nVM을 늘려 여러 존에 배치하거나 관리형 DB를 추가하면 단일 서버 예시보다 비용이 늘어난다. 한 대의 VM을 생성한 것만으로 애플리케이션과 DB의 장애 복구 구성이 완성되는 것도 아니므로, 복구 목표에 필요한 자원을 별도로 포함한다.\n이그레스가 많으면 네트워크 비용부터 계산하기 # 이그레스(egress)는 서버에서 외부로 나가는 데이터다. 사용자가 사진·동영상·설치 파일을 다운로드하거나 API 응답을 받을 때도 데이터가 외부로 전송된다. 이미지·동영상이나 대용량 파일을 제공할 서비스라면 VM 가격과 함께 월 송신량과 초과 전송 단가를 먼저 확인해야 한다.\n월정액 VM의 포함량과 초과 요금 # C의 월 6만 원 VM에는 월 트래픽 4TB가 포함된다. 공식 트래픽 안내의 초과 요금은 VAT 포함 GB당 110원, VAT 제외 GB당 100원이다. 공급사의 월 트래픽 계량값이 포함량을 넘었다고 가정하면 다음처럼 계산한다.\n공급사 계량 기준 초과분 월 추가 전송료 VM 기본요금 포함 월 합계 같은 사용량의 5년 합계 포함량 이내 0원 60,000원 3,600,000원 포함량 초과 1,000GB 100,000원 160,000원 9,600,000원 포함량 초과 5,000GB 500,000원 560,000원 33,600,000원 금액은 VAT 제외이며, 60개월 동안 같은 요금과 초과량이 유지된다는 가정이다. VM을 여러 개 쓰는 경우 포함량을 서로 합산해 공유할 수 있는지도 계약에서 확인한다.\n공급사의 FAQ는 1TB = 1,024GB로 계산 예시를 작성하지만 수신·송신 중 무엇을 합산하는지는 해당 안내에서 명확히 확인되지 않는다. 실제 계량 단위와 집계 범위를 확인해야 하므로 C의 포함 트래픽을 GCP 인터넷 송신량과 동일한 조건으로 직접 비교하지 않는다.\nGCP 서울 리전에서 한국으로 전송하는 예시 # Google Cloud 네트워크 가격표의 출발 리전을 서울(asia-northeast3)로 선택하고, 한국 목적지·Premium Tier 인터넷 송신을 기준으로 계산한다. 이 블로그의 서울 리전 공식 가격표 캡처에 표시된 구간은 월 0–10,240GiB에 $0.19/GiB, 10,240GiB를 넘는 부분에 $0.15/GiB다.\n아래는 해당 송신 SKU에 집계되는 월 사용량을 가정한 네트워크 요금만의 계산이다. 1TiB = 1,024GiB이며, 원화는 현재 환율이 아닌 $1 = 1,400원 가정, VAT 제외다. VM·디스크·IP·CDN·로드 밸런서 등의 요금은 포함하지 않았다. 원화는 USD 요금의 비교용 환산값이며 원화 결제 계정의 실제 청구액은 해당 통화의 SKU 가격을 확인한다.\n월 송신량 월 전송료(USD) 월 원화 환산 같은 사용량의 5년 전송료 100GiB $19.00 26,600원 1,596,000원 500GiB $95.00 133,000원 7,980,000원 1TiB(1,024GiB) $194.56 272,384원 16,343,040원 표의 송신량은 모두 첫 요금 구간에 해당하므로 월 송신량에 $0.19/GiB를 곱했다. 5년 열은 매월 같은 전송량·단가·환율이 유지된다는 가정으로 월 원화 환산액에 60을 곱했다. 원문 가격표에서 출발 리전과 목적지를 바꾸면 단가도 달라지므로 실제 서비스 조건으로 다시 산출한다.\n월정액 VM의 기본 포함량 안에서 쓰는 경우와, GCP처럼 전송량에 따라 청구되는 경우는 비용 구조가 크게 다르다. 위 GCP 전송료를 C의 기본요금에 그대로 더하는 계산은 하지 않는다. 각각의 공급사 요금 정책으로 VM과 전송료를 합산해야 한다.\n물리 서버 임대와 코로케이션도 네트워크가 무제한으로 포함되는 것은 아니다. 계약 대역폭, 트래픽 측정 방식, 추가 회선과 초과 요금을 확인한다. 큰 파일을 자주 제공한다면 필요한 피크 대역폭까지 견적에 넣는다. CDN을 이용할 계획이면 CDN 전송료와 원본 데이터 접근 비용도 함께 계산한다.\n5년 TCO에 추가할 비용 # TCO(Total Cost of Ownership, 총소유비용)는 장비비와 기본 사용료에 운영·유지·교체 비용 등을 더한 값이다. 앞의 표는 공개 요금으로 계산할 수 있는 기본비용이며, 실제 5년 TCO는 다음 항목을 포함한다.\n5년 TCO = 장비 구매비 + 60개월 기본 사용료 + 설치·자원 증설·부품 교체·유지보수 + 가상화 및 OS·프로그램 운영 인건비·라이선스 + 백업·복구·추가 IP·트래픽 초과 비용 - 5년 후 장비 처분액 구매형은 보증 종료 후 교체와 수리비를, 물리 서버에서 여러 VM을 운영한다면 하이퍼바이저 관리와 VM별 백업·복구 비용을 반영한다. 임대와 클라우드도 OS·프로그램 운영, 백업, 초과 사용료가 남는다. 장비 처분액은 소유 장비를 처분하는 경우에만 반영한다.\n운영 인건비는 예상 작업 시간 × 시간당 비용으로 산출하고, 백업·라이선스·추가 회선은 실제 견적을 넣는다. 확인되지 않은 항목은 별도 산정 대상으로 남겨둔다. 이중화가 필요해 물리 호스트를 추가한다면 구매비와 월 요금도 추가해 다시 비교한다.\n어떤 조건에서 무엇을 선택할까 # 필요한 조건 먼저 검토할 방식 견적에서 확인할 것 이미 서버가 있고 장비·OS를 직접 운영할 수 있음 코로케이션 장비 입고 규격, 전력·회선, 원격 작업, 부품 교체 지원 물리 서버 한 대의 CPU·메모리를 계속 쓰지만 직접 구매는 부담됨 물리 서버 임대 필요한 사양의 월 요금, 약정, 하드웨어 고장 지원과 이전 절차 여러 VM을 한 호스트에 배치하고 자원·가상화 관리를 직접 할 수 있음 서버 구매·물리 서버 임대 후 가상화 총 CPU 부하, 호스트 메모리, 디스크 성능, 추가 IP, 호스트 장애 시 복구 작은 VM으로 시작하고 물리 장비 구매·입고를 생략하려 함 월정액 가상서버 서버 위치, 디스크·트래픽 포함량, 상·하위 사양 변경 조건 사진·동영상·대용량 파일 등 외부 전송량이 많음 포함 트래픽이 있는 상품·회선 계약과 퍼블릭 클라우드를 함께 견적 월 송신량, 피크 대역폭, 초과 단가, CDN을 포함한 전체 전송비 특정 리전, 여러 존, 관리형 DB, 자동 확장이 필요함 퍼블릭 클라우드 요구 구성 전체의 VM·DB·네트워크·백업 비용 먼저 VM 개수와 필요한 자원, 서버 위치, 월 송신량·피크 대역폭, 허용 중단 시간과 복구 목표를 정한다. 이 조건으로 각 방식의 견적을 받고, 기본비용에 운영·백업·교체 비용을 더해 5년 예산을 비교한다. 물리 서버의 회선 견적에도 같은 송신량과 피크 대역폭을 전달해야 한다.\n예시 가격의 출처와 적용 범위 # 가격은 확인 시점의 공개 표시값이며 실제 견적·재고·약정에 따라 달라질 수 있다. 아래에는 각 예시의 계산 근거를 확인할 수 있도록 공식 페이지를 연결했다.\n구성 A의 하한: 두드림 서버 구매 가격표의 4코어·8GB·SATA 1TB 상품 중 135만 원 모델과 코로케이션 요금표의 1U 월 89,100원. VAT 별도이며 코로케이션은 2년 약정이다. 장비 입고와 서비스 결합 조건은 계약 전에 확인한다. 구성 A의 상한: 아이네임즈 IDC 요금 안내의 ASUS R300 E7 상품에 게시된 서버 비용 190만 원과 월 이용료 9만 원. 구매 신청 페이지는 VAT 별도를 안내한다. 장비 소유권과 현재 제공 사양은 계약에서 확인한다. 구성 B: 카페24 퀵서버호스팅의 Quick 라이트업 Pro와 Quick 스타트업 Pro. 게시된 월 요금은 9만–11만 원(VAT 별도)이며 설치비는 무료다. 구성 C: 카페24 이지업 서버호스팅의 EASY B와 EASY C. 게시된 월 요금은 66,000–132,000원(VAT 포함)이며 상품 FAQ에서 일본 서버 위치를 안내한다. 트래픽 안내에서 포함량과 초과 요금, 사양 변경 조건을 확인할 수 있다. 다른 물리 서버 견적을 받을 때 참고할 자료: 후이즈 IDC 서버호스팅. 상품 사양과 포함 지원이 달라 위 예시 합계에 섞지 않았다. 자원별 클라우드 요금 확인: Google Cloud Compute Engine 가격 안내, 가격 계산기, 네트워크 가격표. ","date":"2026년 10월 12일","externalUrl":null,"permalink":"/posts/app-backend-server-cost-comparison/","section":"글 목록","summary":"","title":"서버호스팅 vs 클라우드: VM 운영비와 5년 TCO 계산법","type":"posts"},{"content":"","date":"2026년 10월 9일","externalUrl":null,"permalink":"/tags/architecture/","section":"Tags","summary":"","title":"Architecture","type":"tags"},{"content":"","date":"2026년 10월 9일","externalUrl":null,"permalink":"/tags/high-availability/","section":"Tags","summary":"","title":"High-Availability","type":"tags"},{"content":"Kafka 클러스터를 설계할 때는 노드별 역할 배치, Controller 쿼럼, 토픽 복제 설정을 함께 결정해야 한다. 서버 한 대 장애 시 서비스 지속 여부와 데이터 복제본 수는 이 구성에 따라 달라지며, 서버를 증설하는 것만으로 장애 대응이나 복제본 수 증가가 보장되지는 않는다.\n이 글은 Apache Kafka 4.3의 KRaft 구성을 기준으로 서버 대수별 배치와 장애 대응 조건을 비교한다. 구성도는 설명용이며, 실제 물리 서버 설치나 장애 테스트를 수행한 결과는 아니다. 설치 명령 전체를 나열하기보다 어떤 구성을 선택할지에 집중한다.\n1. 물리 서버와 VM, 무엇을 기준으로 셀까? # 여기서 서버는 물리 서버 또는 VM이다. 1·2·3대 비교에서는 서버 한 대에 Broker와 Controller를 겸용하는 Kafka 노드 하나를 배치한다. 기본안은 독립된 서버 3대의 겸용 구성이고, 확장할 때는 기존 Controller를 유지하며 Broker 전용 서버를 추가한다. 역할 분리는 별도 운영 선택지로 설명한다.\n노드 수와 장애가 함께 발생하는 범위는 다르다.\n배치 한 곳의 장애가 미치는 영향 물리 서버 한 대에 Kafka 컨테이너 3개 호스트가 중단되면 노드 3개가 함께 중단 같은 물리 호스트 위의 VM 3대 호스트 장애로 VM 3대가 함께 중단될 수 있음 서로 다른 물리 서버 또는 호스트에 분산된 VM 한 호스트 장애가 모든 노드를 동시에 중단시키지 않도록 배치 가능 이처럼 함께 중단될 수 있는 범위를 장애 도메인(failure domain)이라고 한다. 물리 서버를 나눠도 같은 전원·랙·네트워크를 공유하면 공통 장애가 남는다. 클라우드에서도 VM 개수만 세지 말고, 가용 영역과 호스트 분산 등 실제 배치 조건을 확인해야 한다.\n한 PC에서 컨테이너 3개로 Kafka 노드 동작을 학습할 수는 있지만, 물리 서버 3대의 장애 격리를 검증했다고 볼 수는 없다.\n2. Broker와 Controller는 역할이 다르다 # 역할 담당하는 일 늘리는 목적 Broker 파티션 데이터를 저장하고 메시지 발행·조회 요청 처리 저장 용량과 데이터 처리 부하 분산 Controller 클러스터 메타데이터 관리, Broker 상태에 따른 파티션 리더 선출 등 클러스터 관리 기능의 장애 대응 KRaft에서는 Kafka 노드의 process.roles로 역할을 지정한다.\n# 역할별 설정 예시이며, 완성된 설치 설정 파일은 아니다. process.roles=broker # 또는 controller # 또는 broker,controller Broker가 고객 메시지를 저장하는 역할이라면, Controller는 어느 Broker가 어떤 파티션을 담당하는지 같은 관리 정보를 다룬다. Producer와 Consumer의 일반 메시지 발행·조회는 Broker로 연결하며, Controller를 경유하는 구조가 아니다. Kafka 역할 설정, Kafka Producer와 Consumer 동작\nController 쿼럼: 3개 중 2개가 필요한 이유 # Controller 집합은 메타데이터 쿼럼(metadata quorum)을 구성한다. 관리 정보의 변경을 합의해 확정하려면, 투표에 참여하는 Controller의 과반수가 정상적으로 통신할 수 있어야 한다.\n투표 Controller 수 필요한 과반수 견딜 수 있는 Controller 중단 1개 1개 0개 2개 2개 0개 3개 2개 1개 5개 3개 2개 과반수는 절반 이상이 아니라 절반을 초과하는 수다. 두 Controller 중 하나만 남으면 과반수가 아니다. 세 개 중 두 개가 남으면 과반수다. 단순히 프로세스가 실행 중인 것만으로 충분하지 않고, 필요한 Controller들이 서로 통신할 수 있어야 한다. Kafka Controller 쿼럼\n3. 서버 1대: 개발·실습용 구성 # 한 노드에 process.roles=broker,controller를 지정하는 겸용 구성이다. Broker 1개, Controller 1개이며, 토픽 복제본도 이 구성에서는 최대 1개다.\n서버를 중지하면 메시지 처리와 클러스터 관리가 함께 중단된다. 재시작할 때 기존 디스크가 정상이라면 데이터를 이어 사용할 수 있다. 저장 디스크를 잃으면 다른 Broker에서 복구할 복제본이 없다. 영속 볼륨은 복제나 백업을 대신하지 않는다. 개발 환경, 기능 확인, 재생성 가능한 테스트 데이터에 적합하다. 앞서 작성한 Kafka UI 설치 실습도 이런 단일 노드 구성이다.\n4. 서버 2대: 복제할 수 있어도 고가용성은 별개 # 두 서버에 겸용 노드를 하나씩 실행하면 Broker 2개, Controller 2개다. 토픽의 복제본을 2개로 구성할 수는 있지만, Controller 하나가 중단되면 과반수가 사라진다.\n또한 데이터 복제 쪽에도 선택이 필요하다. 복제본 수(replication factor, RF)가 2인 토픽에서 Producer가 acks=all로 발행한다고 가정한다.\n토픽 설정 Broker 하나가 중단돼 동기화된 복제본이 하나만 남으면 min.insync.replicas=2 최소 복제 조건을 충족하지 못해 새 메시지 발행 성공을 유지할 수 없음 min.insync.replicas=1 데이터 복제 조건은 충족할 수 있지만 새 메시지는 한 복제본에만 의존할 수 있음 이 표는 데이터 복제 조건만 비교한 것이다. 같은 서버에 둔 Controller도 함께 중단되므로, 두 번째 설정을 선택해도 전체 클러스터의 고가용성이 확보됐다는 뜻은 아니다.\nController 과반수가 사라진 순간 모든 기존 조회가 반드시 즉시 중단된다고 단정할 수는 없다. 다만 리더 재선출과 메타데이터 변경에 필요한 합의를 유지할 수 없으므로, 장애 후 정상 운영을 보장하는 구성으로 볼 수 없다.\n물리 서버 2대에 Kafka 노드 3개를 넣으면? # 겸용 노드 3개를 물리 서버 두 대에 2+1로 배치한다고 가정한다. Broker와 Controller는 각각 3개지만, 함께 중단되는 범위는 두 곳뿐이다.\n중단된 물리 서버 남은 Controller 관리 쿼럼 노드 2개가 있는 S1 1개 유지 불가 노드 1개가 있는 S2 2개 남은 두 개가 통신하면 유지 가능 RF 3으로 구성해도 복제본 두 개가 같은 물리 서버에 놓인다. S1이 중단되면 Controller 두 개와 데이터 복제본 두 개를 동시에 잃어, 앞서 설명한 min.insync.replicas=2 조건도 충족하지 못한다.\n이것은 특정 서버의 장애는 견디지만 어느 서버의 장애든 견디는 구성은 아니다. 기존 시스템에서 두 서버를 Active/Active로 운영했다고 해서 Kafka에도 같은 장애 대응 조건이 적용되지는 않는다. Kafka는 클러스터 전체를 Active/Standby 한 쌍으로 전환하는 대신, Controller 쿼럼과 파티션별 리더·복제본을 관리한다.\n5. 서버 3대: 관리 쿼럼과 데이터 복제를 함께 구성 # 세 서버에 겸용 노드를 하나씩 두면 Broker 3개, Controller 3개다. 한 서버가 중단돼도 나머지 Controller 두 개가 통신할 수 있다면 관리 쿼럼을 유지할 수 있다.\n그러나 Broker 3개가 있다고 모든 토픽이 자동으로 3개씩 복제되는 것은 아니다. 토픽별 복제본 수를 확인해야 한다. RF 3은 원본 외에 3개를 추가한다는 뜻이 아니라 파티션별 전체 복제본이 3개라는 뜻이다. 토픽 생성 명령에서는 --replication-factor 3 옵션으로 지정한다. 이는 --config replication.factor=3으로 설정하는 항목이 아니다.\n메시지 쓰기를 위한 설정 예시 # 위치 설정 의미 토픽 생성 옵션 --replication-factor 3 파티션을 서로 다른 Broker 3개에 저장 토픽 min.insync.replicas=2 쓰기 성공에 필요한 동기화된 복제본의 최소 수 Producer acks=all 현재 ISR의 복제 확인을 기다려 발행 결과 판단 ISR(In-Sync Replicas)은 리더와 동기화된 복제본 집합이다. acks=all이 항상 복제본 3개를 기다린다는 뜻은 아니다. 위 구성에서 ISR이 3개라면 3개의 확인을 기다리고, 한 Broker 중단 후 ISR이 2개라면 최소 조건을 충족한 2개로 쓰기를 지속할 수 있다. Kafka 최소 ISR 설정, Producer acks 설정\n이는 모든 복제본이 정상적으로 동기화돼 있었고, 한 서버 장애 후 남은 두 서버와 클라이언트 간 통신이 정상인 상황의 예시다. 전환 중 재시도나 일시적 오류가 없다는 보장은 아니다. Controller가 정상이어도 해당 파티션의 정상 복제본이 부족하면 쓰기는 실패할 수 있다.\nKafka 4.x의 ELR(Eligible Leader Replicas)은 장애 시 리더 후보를 관리하는 추가 기능이다. 여기서는 ISR이 충분한 정상 상태에서 한 서버가 중단되는 경우를 설명하며, ISR이 부족한 복구 상황의 리더 선출 조건까지 단순화하지 않는다. Kafka ELR 설명\n예산과 운영 단순화를 고려한 기본안: 독립된 서버 3대 # 이 글에서 제안하는 기본안은 서버 3대에 Kafka 겸용 노드를 하나씩 배치하는 구성이다. Kafka 프로세스는 총 3개이며, 각 프로세스가 Broker와 Controller 역할을 함께 수행한다. 서버마다 두 역할을 별도 프로세스로 실행하는 6프로세스 구성이 아니다.\n서버 Kafka 설정 역할 S1 node.id=1, process.roles=broker,controller Broker B1 + Controller C1 S2 node.id=2, process.roles=broker,controller Broker B2 + Controller C2 S3 node.id=3, process.roles=broker,controller Broker B3 + Controller C3 물리 서버라면 서로 다른 머신 3대, VM이라면 서로 다른 물리 호스트에 분산한다. VM 이동 후 같은 호스트에 모이지 않도록 배치 정책도 유지한다. 일반 토픽과 필요한 내부 토픽의 복제 설정, Producer의 acks, 장애 후 남은 두 서버의 처리 여력을 함께 확인한다.\n이것은 예산과 운영 단순화를 고려한 기본 구성안이지, 중요한 운영 환경에 대한 Kafka의 공식 권장 배치라는 뜻은 아니다. 겸용 모드는 Broker 부하가 Controller에 영향을 주며, 같은 노드의 두 역할을 독립적으로 재시작하기 어렵다. Kafka 공식 문서는 중요한 운영 환경에서는 겸용을 피하고 역할을 분리하도록 안내한다. 도입할 때 이 제한을 명시하고 부하·장애 전환·유지보수 조건을 검증해야 한다. Kafka 겸용 모드 운영 주의사항\n6. Scale-out: 네 번째 서버에는 Broker만 추가 # 저장 용량이나 메시지 처리 부하 때문에 확장한다면, 기존 서버 3대는 그대로 두고 새 서버 S4에 Broker 역할만 실행하는 Kafka 노드를 추가할 수 있다.\n서버 확장 후 역할 process.roles S1 Broker B1 + Controller C1 broker,controller S2 Broker B2 + Controller C2 broker,controller S3 Broker B3 + Controller C3 broker,controller S4 Broker B4 broker 새 서버에도 같은 Kafka를 설치하되, 고유한 node.id=4와 process.roles=broker로 기존 클러스터에 연결한다. 확장 후 Broker는 4개, Controller는 기존 3개다. Controller 쿼럼의 투표 멤버를 추가하는 작업은 아니다.\nS4에 Controller가 없다는 뜻은 Controller와 통신하지 않는다는 뜻이 아니다. 새 Broker도 기존 Controller 쿼럼의 주소를 알아야 하고, 기존 클러스터와 같은 Cluster ID, 접속 가능한 리스너와 영속 저장소를 준비해야 한다.\n이 방식으로 Broker가 5개, 6개로 늘어도 Controller 수를 같이 늘릴 필요는 없다. Controller는 관리 기능의 가용성과 메타데이터 부하를 기준으로 별도 계획한다. Broker를 추가해도 기존 S1~S3의 겸용 모드가 역할 분리 모드로 바뀌는 것은 아니다.\nBroker를 추가해도 기존 파티션은 자동 이동하지 않는다 # 기존 파티션이 B1·B2·B3에 배치된 상태에서 B4를 추가해도, 기존 데이터가 알아서 B4로 분산되지는 않는다. 확장 절차는 다음처럼 나뉜다.\n고유한 node.id를 가진 Broker를 기존 클러스터에 추가한다. 새 Broker가 정상 등록됐는지 확인한다. 이동할 파티션 복제본과 리더 부하를 고려해 재배치 계획을 만든다. kafka-reassign-partitions.sh 등의 관리 도구로 복제본 이동을 수행한다. 이동 완료와 ISR 정상 상태, Broker별 디스크·트래픽·리더 분포를 확인한다. 기존 데이터 이동은 디스크와 네트워크 부하를 추가로 발생시킨다. 이동 속도 제한과 여유 용량을 확보해야 한다. Broker를 추가해도 토픽의 복제본 수가 자동으로 3에서 4로 바뀌지는 않는다. Kafka Broker 확장과 파티션 재배치\nBroker 추가와 파티션 증설도 다른 작업이다 # 작업 바꾸는 것 Broker 추가 데이터를 배치할 서버 수 복제본 재배치 기존 파티션 데이터를 저장할 Broker 목록 파티션 증설 토픽의 파티션 수와 소비자 병렬 처리 가능 범위 복제본 수 변경 파티션별 저장 사본 수 파티션 2개짜리 토픽은 Broker를 5개로 늘려도, 그 토픽만 구독하는 일반 consumer group에서 파티션을 맡는 소비자는 최대 2명이다. Consumer group 실습과 연결되는 부분이다.\n파티션을 늘리면 key가 매핑되는 파티션이 바뀔 수 있으며 기존 메시지는 자동 재분배되지 않는다. 따라서 처리량을 늘리려면 Broker 수뿐 아니라 파티션 수, key별 부하, 소비자 처리 성능도 함께 봐야 한다. Kafka 파티션 증설 주의사항\n역할 분리는 별도 선택지다 # 겸용은 Kafka 프로그램 하나가 Broker와 Controller 역할을 모두 수행하는 구성이다. 역할 분리는 Kafka 프로그램을 각각 실행해 한쪽은 Broker만, 다른 쪽은 Controller만 수행하게 하는 구성이다.\n실행 방식 Kafka 프로그램 수 각 프로그램의 설정 겸용 1개 process.roles=broker,controller 역할 분리 2개 하나는 process.roles=broker, 다른 하나는 process.roles=controller 역할을 분리하면 프로그램마다 고유한 node.id, 설정 파일, 수신 포트와 저장 경로를 둔다. 같은 물리 서버에서 두 프로그램을 실행할 수도 있지만, CPU·메모리와 호스트 장애는 여전히 공유한다. 프로그램을 분리하는 것과 물리 서버를 분리하는 것은 다른 단계다.\nKafka 공식 지침은 중요한 운영 환경에서 프로그램 하나가 두 역할을 겸하지 않도록 하라는 것이다. 물리 서버 6대를 필수로 정하지는 않는다. 역할별 서버까지 나누는 Controller 전용 3대 + Broker 전용 3대는 자원·서버 장애 영향을 더 강하게 격리하는 배치 예시다. Kafka 공식 배치 지침\n이는 3대 겸용에서 Broker를 하나 추가하는 Scale-out과 다른 설계 변경이다. 서버를 6대로 늘리거나 기존 겸용 노드에서 Controller를 제거해야만 데이터 처리 용량을 확장할 수 있는 것은 아니다. 역할을 옮기는 작업은 쿼럼 방식과 멤버 변경 절차에 맞춰 별도로 계획한다.\n7. 설치 전에 정해야 할 설정 # 아키텍처를 정한 다음 실제 설치에서는 다음 항목을 맞춘다. 아래 표는 설정 점검 항목이며, 그대로 복사해 실행할 완성된 설정은 아니다.\n항목 확인할 내용 process.roles Broker·Controller 겸용인지 전용인지 node.id 같은 클러스터의 노드별 고유 ID Cluster ID 새 클러스터의 각 노드가 같은 ID로 저장소 초기화 listeners / advertised.listeners 실제로 수신할 주소와 클라이언트가 접속할 수 있는 Broker 주소 Controller 연결 설정 쿼럼 방식에 맞는 Controller 주소와 멤버 구성 데이터·메타데이터 저장 경로 영속 디스크, 용량, 백업과 복구 절차 토픽·내부 토픽의 복제 설정 일반 토픽뿐 아니라 offset·transaction 관련 내부 토픽도 점검 네트워크·인증 Broker 간, Controller 간, 클라이언트 연결의 방화벽과 TLS·SASL 등 멀티 서버 구성에서 Broker가 클라이언트에 localhost를 광고하면 클라이언트는 자기 자신의 서버로 접속하려 한다. bootstrap 주소만 맞추는 것이 아니라, 이후 반환되는 Broker 주소도 도달 가능해야 한다. Kafka Broker 연결 설정\nKRaft 쿼럼은 정적·동적 구성 방식이 있다. 정적 방식의 controller.quorum.voters와 동적 방식의 controller.quorum.bootstrap.servers를 혼용하지 않고, 클러스터의 KRaft 기능 버전과 저장소 초기화 방식에 맞춰 설정한다. 기존 실습 파일을 멀티 서버용으로 바꾸는 것만으로 쿼럼 방식이 자동 전환되는 것은 아니다. KRaft 정적·동적 쿼럼\n8. 요구사항별 구성 선택 # 요구사항 배치 예시 판단할 점 개발·기능 확인 1대 겸용 서버·디스크 장애를 다른 노드가 대신하지 못함 2대만 사용할 수 있음 2대 겸용 또는 3노드의 2+1 배치 임의의 물리 서버 1대 장애를 견디는 운영 HA 구성으로 볼 수 없음 예산·운영 단순화를 고려한 3대 기본안 서로 다른 호스트에 겸용 Kafka 노드 하나씩 쿼럼·복제·자원 여력 확인, 중요한 운영 환경의 공식 역할 분리 지침과 구분 역할별 자원·서버 장애까지 격리 Controller 전용 3대 + Broker 전용 3대 등의 배치 요구되는 격리 수준에 맞춰 추가 서버 확보 3대 기본안에서 저장·처리 부하 증가 네 번째 서버에 Broker만 추가 Broker 4개·Controller 3개, 기존 파티션 재배치와 소비자 병렬성 점검 마무리 # Kafka 서버 수를 정할 때는 Controller 쿼럼이 남는지, 파티션의 정상 복제본이 남는지, 그 노드들이 함께 고장 나지 않는지를 각각 확인해야 한다.\n이 글의 기본안은 독립된 서버 3대에 겸용 노드를 하나씩 두는 배치다. 용량이나 처리 부하 때문에 확장할 때는 네 번째 서버에 Broker만 추가하고 기존 파티션을 재배치한다. 중요한 운영 환경에서는 겸용의 제약과 공식 역할 분리 지침을 별도로 검토해야 하며, 물리 서버 2대에 노드 3개를 넣는 것을 독립 서버 3대와 같은 장애 대응 구성으로 보면 안 된다.\n","date":"2026년 10월 9일","externalUrl":null,"permalink":"/posts/kafka-cluster-architecture-guide/","section":"글 목록","summary":"","title":"Kafka 서버는 몇 대가 필요할까? 1대·2대·3대 구성과 Scale-out","type":"posts"},{"content":"","date":"2026년 10월 9일","externalUrl":null,"permalink":"/tags/kraft/","section":"Tags","summary":"","title":"Kraft","type":"tags"},{"content":"","date":"2026년 10월 9일","externalUrl":null,"permalink":"/tags/scale-out/","section":"Tags","summary":"","title":"Scale-Out","type":"tags"},{"content":"","date":"2026년 10월 8일","externalUrl":null,"permalink":"/tags/consumer-group/","section":"Tags","summary":"","title":"Consumer-Group","type":"tags"},{"content":"Kafka 소비자를 3개 실행하고 메시지 6개를 발행했는데, 두 소비자만 각각 3개를 받고 나머지 하나는 아무것도 받지 않았다. 오류가 아니라, 파티션 2개를 같은 그룹의 소비자들이 나눠 맡은 결과였다.\n이번에는 새 토픽 delivery-events로 같은 그룹의 작업 분배, 다른 그룹의 독립 수신, 소비자 종료 후 파티션 인계를 확인했다. 실습에서는 배송 처리를 수행하지 않고 콘솔에 테스트 이벤트를 출력했다.\n1. 실습 구성: 그룹 하나에 소비자 3개 # consumer는 메시지를 읽는 프로그램이고, consumer group은 같은 group.id로 동작하는 소비자들의 묶음이다. 소비자 3개를 실행했다고 그룹이 3개가 되는 것은 아니다.\n이번 실습은 다음과 같이 구성했다.\n항목 구성 환경 Windows + Docker Desktop + WSL Ubuntu Kafka / UI Kafka 4.3.1 / Kafbat UI v1.5.0 새 토픽 delivery-events, 파티션 2개, 복제본 1개 작업 분배 그룹 delivery-workers: worker-1, worker-2, worker-3 독립 수신 그룹 delivery-history: history-1 테스트 데이터 최초 배송 이벤트 6개 + 인계 후 이벤트 4개 확인 날짜 2026년 10월 8일 Kafka와 UI 설치는 Kafka UI 설치와 토픽·메시지 조회의 Compose 환경을 재사용한다. 기존 orders-demo 토픽과 ui-demo-a 그룹은 변경하지 않는다. 단일 브로커이므로 브로커 장애 전환이나 복제 내구성을 시험하는 구성은 아니다.\nCompose 파일 준비와 실행 # 처음 실습한다면 Docker Desktop과 WSL 연동을 먼저 준비한다. 설치 과정은 Docker Desktop 설치와 WSL 연동을 참고한다.\ncompose.yaml: Kafka와 Kafbat UI 실행 설정. 이전 UI 글에서 사용한 파일과 동일하다. Docker Desktop을 실행하고 작업 디렉터리를 만든 뒤, 다운로드한 compose.yaml을 그 안에 둔다.\nmkdir -p ~/kafka-ui-guide cd ~/kafka-ui-guide 이전 실습에서 해당 디렉터리와 Compose 환경을 이미 준비했다면 다시 다운로드하거나 덮어쓸 필요 없이 기존 파일을 사용한다. 이 파일의 Compose 프로젝트 이름은 blog-kafka-ui-lab으로 같으므로, 다른 디렉터리에 복사해도 별도 Kafka 환경이 생성되는 것은 아니다.\n캡처에서는 Compose 파일이 있는 ~/kafka-ui-guide에서 실행했다. 아래 명령도 해당 디렉터리의 WSL Bash 터미널에서 실행한다. 별도 lab.sh는 사용하지 않는다.\ncd ~/kafka-ui-guide docker compose config --quiet docker compose up -d --wait --wait-timeout 180 docker compose ps UI 주소는 http://localhost:18080/이다. 실습은 일반 consumer group을 사용하며, 별도 소비 모델인 Share Group은 다루지 않는다.\nUI는 로그인 없는 로컬 조회용이며 127.0.0.1:18080에만 연결한다. 이 구성을 인터넷에 공개하지 않는다.\n새 토픽 만들기 # docker compose exec -T kafka /opt/kafka/bin/kafka-topics.sh \\ --bootstrap-server kafka:9092 --create \\ --topic delivery-events --partitions 2 --replication-factor 1 docker compose exec -T kafka /opt/kafka/bin/kafka-topics.sh \\ --bootstrap-server kafka:9092 --describe --topic delivery-events PartitionCount: 2이며 파티션 0, 1이 생성됐다. 같은 이름의 토픽이 이미 있으면 생성 명령은 오류로 종료한다. 반복 실습은 토픽과 두 그룹 이름에 모두 -02 등을 붙여 구분하면 기존 데이터를 삭제하지 않아도 된다.\n메시지 파일 준비 # 아래 파일을 다운로드해 compose.yaml과 같은 작업 디렉터리에 둔다.\nmessages-initial.txt: 최초 이벤트 6개 messages-after.txt: 소비자 종료 후 추가할 이벤트 4개 캡처에서는 메시지 파일을 옆 디렉터리에 두었지만, 아래 명령은 같은 작업 디렉터리에 두는 기준이다. 파일 위치가 다르면 \u0026lt; 뒤의 경로만 조정한다.\n첫 파일에는 다음처럼 key와 JSON value를 함께 넣었다.\n1:{\u0026#34;event\u0026#34;:\u0026#34;delivery.requested\u0026#34;,\u0026#34;delivery_id\u0026#34;:\u0026#34;delivery-001\u0026#34;,\u0026#34;phase\u0026#34;:\u0026#34;initial\u0026#34;} 2:{\u0026#34;event\u0026#34;:\u0026#34;delivery.requested\u0026#34;,\u0026#34;delivery_id\u0026#34;:\u0026#34;delivery-002\u0026#34;,\u0026#34;phase\u0026#34;:\u0026#34;initial\u0026#34;} 1:{\u0026#34;event\u0026#34;:\u0026#34;delivery.requested\u0026#34;,\u0026#34;delivery_id\u0026#34;:\u0026#34;delivery-003\u0026#34;,\u0026#34;phase\u0026#34;:\u0026#34;initial\u0026#34;} 2:{\u0026#34;event\u0026#34;:\u0026#34;delivery.requested\u0026#34;,\u0026#34;delivery_id\u0026#34;:\u0026#34;delivery-004\u0026#34;,\u0026#34;phase\u0026#34;:\u0026#34;initial\u0026#34;} 1:{\u0026#34;event\u0026#34;:\u0026#34;delivery.requested\u0026#34;,\u0026#34;delivery_id\u0026#34;:\u0026#34;delivery-005\u0026#34;,\u0026#34;phase\u0026#34;:\u0026#34;initial\u0026#34;} 2:{\u0026#34;event\u0026#34;:\u0026#34;delivery.requested\u0026#34;,\u0026#34;delivery_id\u0026#34;:\u0026#34;delivery-006\u0026#34;,\u0026#34;phase\u0026#34;:\u0026#34;initial\u0026#34;} 1:과 2:의 숫자는 key이지 파티션 번호를 직접 지정하는 옵션이 아니다. 실제 결과에서 key 1은 파티션 1, key 2는 파티션 0에 저장됐다. 소량의 key 없는 메시지가 한 파티션에 몰리는 대신, 이번 데이터가 두 파티션에 나뉘도록 key를 사용했다.\n2. 같은 그룹: 파티션을 나눠 받는다 # WSL 터미널을 3개 열고 각각 cd ~/kafka-ui-guide로 이동한다. 다음 소비자들은 종료하지 않고 실행해 둔다.\n터미널 1: worker-1 # docker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh \\ --bootstrap-server kafka:9092 --topic delivery-events \\ --group delivery-workers --from-beginning \\ --command-property client.id=worker-1 \\ --command-property group.protocol=classic \\ --command-property enable.auto.commit=true \\ --command-property auto.commit.interval.ms=1000 \\ --formatter-property print.partition=true \\ --formatter-property print.offset=true \\ --formatter-property print.key=true 터미널 2: worker-2 # docker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh \\ --bootstrap-server kafka:9092 --topic delivery-events \\ --group delivery-workers --from-beginning \\ --command-property client.id=worker-2 \\ --command-property group.protocol=classic \\ --command-property enable.auto.commit=true \\ --command-property auto.commit.interval.ms=1000 \\ --formatter-property print.partition=true \\ --formatter-property print.offset=true \\ --formatter-property print.key=true 터미널 3: worker-3 # docker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh \\ --bootstrap-server kafka:9092 --topic delivery-events \\ --group delivery-workers --from-beginning \\ --command-property client.id=worker-3 \\ --command-property group.protocol=classic \\ --command-property enable.auto.commit=true \\ --command-property auto.commit.interval.ms=1000 \\ --formatter-property print.partition=true \\ --formatter-property print.offset=true \\ --formatter-property print.key=true --group은 세 소비자 모두 같고, client.id만 다르다. client.id는 조회 결과에서 각 소비자를 구별하기 위한 이름이며 그룹을 나누는 설정은 아니다. 이번에는 group.protocol=classic과 자동 offset commit을 명시했다. 출력 옵션으로 파티션·offset·key를 함께 확인한다.\n멤버와 할당 조회 # 별도 관리용 터미널에서 실행한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-workers --state docker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-workers \\ --members --verbose 실제 할당은 다음과 같았다.\n소비자 담당 파티션 할당 개수 worker-1 0 1 worker-2 1 1 worker-3 없음 0 일반 consumer group에서는 한 파티션을 동시에 한 멤버에게 할당한다. 이 토픽만 구독하는 그룹은 파티션이 2개이므로 3명 모두에게 파티션을 줄 수 없다. 어떤 소비자가 대기할지는 실행 순서와 할당 결과에 따라 달라질 수 있다. Kafka 그룹과 파티션 할당\n그룹이 Stable이고 멤버가 3명인지 확인한 뒤 다음 단계로 진행한다. 상태가 바뀌는 중이라면 잠시 기다리고 재조회한다. --members --verbose는 파티션이 없는 소비자까지 확인하는 데 유용하다. Kafka 그룹 조회 명령\n3. 메시지 6개: 두 소비자만 각각 3개 수신 # 관리용 터미널에서 최초 이벤트를 한 번만 발행한다. 반복 실행하면 같은 테스트 이벤트가 추가 저장된다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \\ --bootstrap-server kafka:9092 --topic delivery-events --sync \\ --reader-property parse.key=true --reader-property key.separator=: \\ \u0026lt; messages-initial.txt 수신 결과는 다음과 같았다.\n소비자 받은 배송 이벤트 메시지 수 worker-1 delivery-002, 004, 006 3 worker-2 delivery-001, 003, 005 3 worker-3 없음 0 메시지마다 세 소비자 중 한 명을 무작위로 고르는 것이 아니라, 각 소비자가 자신에게 할당된 파티션을 읽었다. 3개씩 받은 것도 이번 데이터가 파티션별로 3개씩 저장됐기 때문이다. 항상 소비자별 메시지 수나 처리 부하가 균등해지는 것은 아니다.\n첫 메시지의 커밋 위치 확인 # 소비자들을 실행해 둔 상태에서 관리용 터미널로 조회한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-workers 이번 실습에서는 두 파티션 모두 다음 값으로 확인됐다.\n파티션 CURRENT-OFFSET LOG-END-OFFSET LAG 0 3 3 0 1 3 3 0 각 파티션의 offset 0·1·2를 출력한 뒤 다음 읽기 위치 3이 그룹에 저장된 것이다. 자동 commit이 아직 반영되지 않았다면 잠시 기다리고 재조회한다. 이 값을 확인한 뒤 다음 단계로 진행하면, 소비자 종료 후 다른 멤버가 어디서 이어 읽는지도 비교할 수 있다.\n4. 다른 그룹: 같은 메시지를 모두 받는다 # 새 터미널에서 delivery-history 그룹을 실행한다. 작업 디렉터리는 동일하다.\ndocker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh \\ --bootstrap-server kafka:9092 --topic delivery-events \\ --group delivery-history --from-beginning \\ --command-property client.id=history-1 \\ --command-property group.protocol=classic \\ --command-property enable.auto.commit=true \\ --command-property auto.commit.interval.ms=1000 \\ --formatter-property print.partition=true \\ --formatter-property print.offset=true \\ --formatter-property print.key=true delivery-workers가 이미 읽고 commit한 메시지 6개를 history-1도 모두 받았다. 다른 그룹의 commit은 이 그룹의 읽기 위치를 진행시키지 않는다. delivery-history에는 소비자가 하나뿐이라 그 소비자가 파티션 2개를 담당했다.\n이번에는 처음 사용하는 그룹에 --from-beginning을 지정했다. 그룹 이름만 바꾸면 무조건 처음부터 받는 것은 아니다. 저장된 offset이 없는 그룹의 시작 위치도 설정에 따라 달라지고, 유효한 committed offset이 이미 있다면 이 옵션만으로 처음으로 되돌아가지 않는다. 자세한 내용은 Kafka offset과 그룹별 재개 위치에서 설명했다.\n그룹의 할당은 다음 명령으로 확인할 수 있다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-history \\ --members --verbose history-1의 할당 개수는 2, 담당 파티션은 delivery-events:0,1이었다. 다른 그룹에 속한 소비자들은 합쳐서 파티션을 나누지 않는다.\n캡처에서는 파티션 0의 이벤트가 먼저 출력되고 파티션 1의 이벤트가 뒤이어 출력됐다. 파티션별 offset 순서와 전체 배송 번호 순서는 다르다. 파티션 두 개를 읽는 결과가 delivery-001부터 전체 번호순으로 나온다고 기대해서는 안 된다.\n5. 소비자 종료: 대기하던 worker-3이 인계받는다 # 먼저 관리용 터미널에서 현재 파티션 할당을 다시 확인한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-workers \\ --members --verbose #PARTITIONS가 1인 소비자 하나를 선택해 그 터미널에서 Ctrl+C를 누른다. 소비자 이름과 파티션 번호는 독자의 실행 결과에 따라 달라질 수 있다. 파티션이 없는 소비자를 종료하면 담당 파티션이 다른 소비자로 이동하는 과정을 확인할 수 없다.\n이번 실습에서는 파티션 1을 담당하던 worker-2를 종료했다. 그 터미널에는 최초 메시지 3개를 출력했다는 종료 결과가 나타났다. Kafka 브로커나 UI를 종료한 것은 아니다.\n이후 관리용 터미널에서 그룹을 다시 조회한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-workers \\ --members --verbose docker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-workers --state 소비자 종료 전 종료 후 worker-1 파티션 0 파티션 0 worker-2 파티션 1 종료 worker-3 미할당 파티션 1 멤버 변화에 따라 파티션 할당이 바뀌는 것을 rebalance라고 한다. 이번에는 worker-3이 worker-2의 담당 파티션을 인계받았다. 조회 시 그룹 상태는 다시 Stable, 멤버 수는 2였다.\n독자 환경에서는 이름이 달라도, 종료한 소비자가 목록에서 빠지고 기존 미할당 소비자가 파티션을 맡았는지 확인하면 된다. 그룹 상태가 Stable로 돌아온 뒤 추가 메시지를 발행한다.\n새 메시지를 발행해 인계 후 수신 확인 # 할당이 안정된 뒤 다음 파일의 메시지 4개를 한 번만 추가한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \\ --bootstrap-server kafka:9092 --topic delivery-events --sync \\ --reader-property parse.key=true --reader-property key.separator=: \\ \u0026lt; messages-after.txt 이번에는 worker-1이 delivery-008, 010을, worker-3이 delivery-007, 009를 받았다. history-1도 독립적으로 추가 이벤트 4개를 모두 받았다.\nworker-3이 새로 파티션을 맡았다고 처음부터 읽는 것은 아니다. 같은 그룹이 파티션 1에 저장해 둔 재개 위치가 3이었으므로, 캡처에서도 기존 offset 0·1·2를 다시 출력하지 않고 3·4를 수신했다.\n이 실습은 Ctrl+C로 정상 종료한 경우다. 처리 후 commit 전에 중단되는 장애에서는 다음 소비자가 같은 메시지를 다시 받을 수 있다. 파티션 인계가 업무의 중복 실행까지 막아 주는 것은 아니다. Kafka commit과 재처리\n6. 최종 결과: 두 그룹의 offset과 UI 확인 # 자동 commit이 반영된 뒤 두 그룹을 조회한다. 값이 아직 진행 중이면 잠시 기다리고 재조회한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-workers docker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group delivery-history 두 그룹 모두 각 파티션의 CURRENT-OFFSET과 LOG-END-OFFSET이 5, LAG가 0이었다. 파티션별 최초 3개에 추가 2개를 더했기 때문이다. 토픽 전체 메시지 수는 10개지만 offset은 파티션별 위치이므로 10이 아니다.\nKafbat UI에서 Topics → delivery-events → Consumers를 열면 최종 그룹 구성을 함께 볼 수 있다.\n그룹 활성 소비자 Consumer Lag 상태 delivery-workers 2 0 STABLE delivery-history 1 0 STABLE 이 화면은 같은 토픽을 서로 다른 그룹이 읽고 있다는 것을 보여 준다. 각 소비자의 파티션 할당까지 보려면 앞서 사용한 --members --verbose로 확인한다. Lag 0은 이번 콘솔 실습의 커밋 결과이지, 실제 배송이나 이력 DB 저장이 완료됐다는 증거는 아니다.\n실습을 끝낼 때는 실행 중인 소비자 터미널마다 Ctrl+C를 누른다. 기존 실습과 Kafka를 공유하므로 토픽·볼륨을 삭제할 필요는 없다.\n7. 실제 서비스에서는 그룹을 어떻게 나눌까? # 이번 콘솔 실습을 실제 서비스에 적용한다면 다음처럼 구성할 수 있다. 아래는 구성 예시이며, 실습에서 실제 배송이나 DB 저장을 구현한 것은 아니다.\n요구사항 그룹 구성 동작 배송 처리 프로그램 여러 개가 작업을 나눠 맡아야 함 같은 delivery-workers 그룹 파티션을 나눠 읽고 담당 이벤트 처리 이력 저장 서비스도 모든 이벤트를 받아야 함 별도 delivery-history 그룹 작업 처리 그룹과 독립적으로 읽고 이력 저장 알림 서비스도 같은 이벤트를 받아야 함 추가 delivery-notifications 그룹 배송 처리·이력 그룹의 읽기 위치와 별도로 수신 세 서비스가 모두 이벤트를 받아야 하는데 같은 그룹으로 묶으면, 담당 파티션을 나눠 받아 각 서비스가 전체 이벤트를 받지 못한다. 반대로 같은 배송 작업을 나눠 처리할 worker마다 다른 그룹을 주면, 각 그룹이 같은 이벤트를 읽어 작업이 중복될 수 있다.\n소비자를 늘릴 때도 파티션 수를 함께 확인해야 한다. 이번 토픽에만 연결된 그룹에서는 소비자를 3개로 늘려도 읽기를 담당하는 멤버는 최대 2명이다. 파티션 증설은 key의 배치와 순서 유지에도 영향을 줄 수 있으므로, 단순히 소비자 수에 맞춰 변경하지 않는다. Kafka 파티션 증설 시 고려사항\n마무리 # 파티션 2개에 같은 그룹의 소비자 3개를 실행했더니 두 소비자만 메시지를 받았다. 별도 그룹은 기존 메시지를 모두 다시 받았고, worker-2 종료 후에는 대기하던 worker-3이 파티션을 인계받아 커밋된 위치부터 이어서 읽었다.\n같은 작업을 나눠 맡을 프로그램들은 같은 그룹으로, 같은 이벤트를 각자 받아야 할 서비스들은 다른 그룹으로 구성한다. 단, 그룹과 파티션 할당만으로 업무의 중복·누락까지 해결되는 것은 아니므로 처리 결과와 offset commit 시점도 함께 설계해야 한다.\n","date":"2026년 10월 8일","externalUrl":null,"permalink":"/posts/kafka-consumer-group-guide/","section":"글 목록","summary":"","title":"Kafka consumer group 이해하기: 같은 그룹과 다른 그룹은 어떻게 동작할까?","type":"posts"},{"content":"","date":"2026년 10월 8일","externalUrl":null,"permalink":"/tags/partition/","section":"Tags","summary":"","title":"Partition","type":"tags"},{"content":"","date":"2026년 10월 8일","externalUrl":null,"permalink":"/tags/rebalance/","section":"Tags","summary":"","title":"Rebalance","type":"tags"},{"content":"","date":"2026년 10월 7일","externalUrl":null,"permalink":"/tags/event-streaming/","section":"Tags","summary":"","title":"Event-Streaming","type":"tags"},{"content":"","date":"2026년 10월 7일","externalUrl":null,"permalink":"/tags/ibm-mq/","section":"Tags","summary":"","title":"Ibm-Mq","type":"tags"},{"content":"IBM MQ, TIBCO EMS, Apache ActiveMQ 같은 전통적인 MOM(Message-Oriented Middleware)의 큐를 사용했다면, 메시지를 수신하고 ACK 또는 트랜잭션 commit으로 처리를 확정하는 방식에 익숙할 것이다. 제품과 소비 모드에 따라 구체적인 동작은 다르다. TIBCO EMS 메시징 모델, ActiveMQ Classic 소개\nKafka에서는 메시지를 읽고 offset을 commit해도 토픽의 데이터가 제거되지 않는다. 그렇다면 offset은 무엇이고, 무엇을 commit하는 것일까?\nIBM MQ의 일반 큐에서 트랜잭션으로 메시지를 가져오는 방식과 Kafka의 일반 소비자 그룹을 비교한다. Kafka 캡처는 Kafbat UI 실습에서 확인한 결과이며, IBM MQ와 장애 상황 예시는 공식 동작을 설명하기 위한 것이다. 별도의 IBM MQ 실행이나 장애 테스트를 수행한 결과는 아니다.\n1. IBM MQ: 가져온 메시지의 제거를 확정한다 # IBM MQ에서는 메시지를 트랜잭션 안에서 가져온 뒤, 처리 성공 시 commit, 실패 시 rollback하는 흐름을 구성할 수 있다.\n사용하는 API에 따라 호출 이름은 다르다. **JMS의 로컬 트랜잭션 세션에서는 Session.commit() / Session.rollback()**을 사용한다. 네이티브 MQI에서는 MQCMIT / MQBACK을 사용하며, rollback에 해당하는 동작을 backout이라고 부른다. 이 글에서는 트랜잭션 취소를 rollback으로 표현한다. IBM MQ JMS 트랜잭션, IBM MQ MQBACK\n아래는 네이티브 MQI의 MQGMO_SYNCPOINT 옵션으로 MQGET을 호출하는 예시다.\n큐: 주문 A, 주문 B, 주문 C MQGET으로 주문 A 가져오기 → 주문 A 처리 성공: commit (MQCMIT) → 주문 A의 큐 제거 확정 실패: rollback (MQBACK) → 주문 A를 다시 가져올 수 있게 복원 메시지를 가져온 뒤 commit하기 전에는 다른 프로그램이 그 메시지를 가져갈 수 없다. commit하면 해당 큐에서 제거되고, rollback하면 다시 가져올 수 있다. 여기서는 트랜잭션 대상 메시지를 가져오는 경우를 설명하며, Browse와는 다르다. IBM MQ 트랜잭션 확정과 취소\ncommit한 주문 A를 같은 큐에서 다시 가져오는 기능은 아니다. 재처리에 필요한 원본은 별도로 저장하거나 다른 큐에 보관하는 등의 설계가 필요하다. 큐에서 메시지를 제거한다는 것이 주문 DB의 기록까지 삭제한다는 뜻은 아니다.\n2. Kafka: 메시지 위치와 소비자의 재개 위치를 구분한다 # Kafka 토픽은 하나 이상의 파티션으로 나뉘며, 레코드가 저장될 때 각 파티션 안에서 위치를 나타내는 offset을 부여받는다.\n새 파티션에 주문 이벤트 3개를 발행한 예시는 다음과 같다.\n토픽 파티션 메시지 offset 주문 번호 orders-demo 0 0 demo-001 orders-demo 0 1 demo-002 orders-demo 0 2 demo-003 캡처는 최신순으로 표시돼 있지만, 각 주문 이벤트의 offset은 표와 같다. offset은 주문 번호나 토픽 전체의 공통 번호가 아니다. 다른 파티션에도 offset 0인 메시지가 있을 수 있다. 삭제·compaction 등으로 중간 번호가 빠질 수도 있으므로 offset을 메시지 개수와 동일하게 취급하면 안 된다. Kafka 파티션과 offset\n메시지 offset과 committed offset # 0·1·2를 모두 처리한 그룹은 다음에 읽을 위치인 3을 commit한다.\n파티션에 저장된 메시지: [0: 주문 A] [1: 주문 B] [2: 주문 C] 그룹이 0, 1, 2를 처리하고 offset 3을 commit → 메시지 0, 1, 2는 토픽에 남음 → 재시작 시 다음 위치 3부터 읽기 재개 “offset 3을 commit했다”는 마지막으로 처리한 메시지가 3이라는 뜻이 아니다. 이 예시에서는 마지막 메시지의 offset이 2이고, commit한 값은 다음에 읽을 위치인 3이다.\n실행 중 소비자의 읽기 위치는 poll()로 레코드를 받으면 진행된다. 그 위치를 commit하지 않았다면 재시작 시 사용할 저장된 위치는 여전히 이전 값일 수 있다. 읽었다는 것, 업무 처리를 끝냈다는 것, offset을 commit했다는 것은 각각 구분해야 한다. Kafka 읽기 위치와 커밋된 위치\n3. 같은 commit이라는 말이지만 결과는 다르다 # 기준 IBM MQ의 트랜잭션 MQGET Kafka의 일반 소비자 그룹 소비자가 받는 것 큐에서 가져온 메시지 파티션에 저장된 레코드 commit 대상 트랜잭션 안의 MQGET·MQPUT 작업 그룹·파티션별 다음 읽기 위치 메시지를 가져온 뒤 commit 해당 큐에서 메시지 제거 확정 토픽 데이터는 제거하지 않음 장애 후 재처리 미확정 MQGET 작업이 rollback되면 다시 수신 가능 저장된 offset에서 읽기 재개 완료한 데이터 재조회 제거된 메시지는 해당 큐에 없음 데이터가 남아 있으면 읽기 위치를 조정해 가능 예를 들어 IBM MQ에서 주문 A를 가져온 트랜잭션을 commit하면, A의 제거가 확정된다. Kafka에서 주문 A의 offset이 0이고 다음 위치 1을 commit하면, A는 토픽에 남고 그 그룹의 재개 위치만 1로 저장된다. IBM MQ 트랜잭션 설명, Kafka offset 저장\n이 글의 Kafka commit은 consumer offset commit이다. Kafka의 producer transaction을 commit하는 기능과 혼동하면 안 된다.\n4. 여러 서비스는 각자의 위치에서 읽는다 # 알림 서비스와 매출 집계 서비스가 같은 주문 이벤트를 읽는다고 가정한다. 서로 다른 소비자 그룹을 사용하면 위치를 독립적으로 관리한다.\n소비자 그룹 순서대로 처리 완료한 메시지 offset commit할 다음 위치 알림 그룹 0, 1, 2 3 매출 집계 그룹 0 1 알림 그룹이 3을 commit해도 집계 그룹의 위치가 3으로 바뀌지는 않는다. 집계 그룹은 자신의 위치 1부터 이어서 읽을 수 있다. 같은 그룹 안에서는 파티션을 나눠 소비하고, 서로 다른 그룹은 같은 데이터를 독립적으로 읽는다. Kafka 소비자 그룹\nIBM MQ 같은 MOM도 큐·구독을 따로 구성해 여러 서비스에 메시지를 전달할 수 있다. 여러 서비스가 받는다는 것 자체가 Kafka만의 기능은 아니다. 차이는 Kafka에서 같은 보관 데이터를 그룹별 위치로 읽는다는 점이다.\n그룹을 바꾸면 처음부터 받을까? # 다른 그룹이 commit한 위치와는 무관하지만, 새 그룹도 자신의 읽기 위치가 필요하다. 메시지의 offset은 그대로이며, 그룹별로 다음에 읽을 위치만 따로 저장한다.\n주문 A·B·C가 offset 0·1·2에 남아 있고, 기존 그룹 A가 3을 commit한 상태를 가정한다.\n그룹 시작 기준 받는 메시지 기존 그룹 A 저장된 offset 3 새 메시지를 기다림 처음 사용하는 그룹 B auto.offset.reset=earliest 보관된 주문 A·B·C부터 읽음 처음 사용하는 그룹 C auto.offset.reset=latest 시작 위치를 정한 이후 추가되는 메시지부터 읽음 auto.offset.reset은 저장된 offset이 없거나, 데이터 삭제 등으로 그 위치가 유효하지 않을 때 시작 위치를 정하는 설정이다. earliest는 현재 보관된 가장 앞 위치이므로 반드시 0은 아니다. latest는 파티션 끝에서 시작하며, Java 소비자의 기본값이다. Kafka 시작 위치 설정\n그룹 B가 이전에 사용됐고 유효한 committed offset이 남아 있다면, earliest로 설정해도 저장된 위치에서 이어서 읽는다. 그룹 이름만 바꾼다고 무조건 처음부터 읽는 것은 아니다.\n실제 UI에서는 무엇을 확인했나 # Kafbat UI 실습의 ui-demo-a 그룹에서는 다음 값이 표시됐다.\nUI 항목 값 해석 Current Offset 3 그룹이 커밋한 다음 읽기 위치 End offset 3 파티션 끝의 다음 위치 Consumer Lag 0 이번 데이터에서 끝 위치와 커밋된 위치의 차이 이 캡처는 콘솔 소비자가 주문 이벤트 3개를 출력하고 offset을 commit한 결과다. 실제 결제나 집계 처리를 수행한 것은 아니다. Lag 0만으로 업무 처리 성공을 판단할 수는 없다.\n5. 프로그램이 중단되면 어디서 다시 시작할까 # 아래는 파티션 하나를 순서대로 처리하고, 실패 시 Kafka에 저장된 offset에서 재개하는 예시다. 장애를 직접 재현한 결과는 아니다.\n처리 후 commit 전에 중단: 중복 가능성 # 저장된 offset: 1 offset 1의 주문 B에 대한 메일 발송 완료 다음 위치 2를 commit하기 전에 프로그램 중단 재시작: 저장된 위치 1부터 읽음 결과: 주문 B의 메일이 다시 발송될 수 있음 처리 전에 commit하고 중단: 누락 가능성 # offset 1의 주문 B를 읽고 다음 위치 2를 먼저 commit 주문 B의 메일을 보내기 전에 프로그램 중단 재시작: 저장된 위치 2부터 읽음 결과: 주문 B는 자동 재개 과정에서 건너뛸 수 있음 누락 예시에서도 레코드가 토픽에서 삭제된 것은 아니다. 다만 그룹의 저장된 위치가 이미 진행돼 별도 재처리 없이는 건너뛰게 된다. 처리 완료 후 commit하고, 주문 번호 같은 작업 식별자로 중복 실행을 방지하는 설계가 필요하다. Kafka 처리와 commit 시점\nIBM MQ도 메일 발송과 MQ 트랜잭션이 별개라면, 발송 후 메시지를 가져온 트랜잭션을 rollback할 때 같은 메일을 다시 보낼 수 있다. 트랜잭션을 사용한다고 그 범위 밖의 외부 작업까지 자동으로 되돌아가는 것은 아니다. MQ와 DB를 함께 확정하려면 별도의 트랜잭션 연동 구성이 필요하다. IBM MQ 트랜잭션 범위와 조정\n6. IBM MQ Browse와 Kafka replay는 다르다 # IBM MQ의 Browse는 큐에 남아 있는 메시지를 제거하지 않고 조회하는 기능이다. MQGMO_BROWSE_FIRST, MQGMO_BROWSE_NEXT 등으로 browse cursor를 이동한다. 일반 MQGET 후 commit으로 제거한 메시지를 되살려 조회하는 기능은 아니다. IBM MQ browse cursor\nKafka replay는 보관된 레코드를 이전 위치부터 다시 읽는 것이다. 같은 그룹에서 읽기 위치를 조정하거나, 새 그룹으로 보관된 처음 위치부터 읽을 수 있다. 이는 업무 처리를 취소하는 rollback이 아니라 데이터를 다시 입력받는 동작이다. 외부 처리 결과는 재조회만으로 되돌아가지 않는다.\ncommit한 위치가 3이어도 이전 메시지를 받을 수 있다 # 주문 A·B·C가 아직 남아 있다면, 다음과 같이 해당 파티션의 읽기 시작 위치를 변경할 수 있다.\n변경할 읽기 위치 다시 받는 메시지 0 주문 A·B·C 1 주문 B·C 2 주문 C 3 기존 세 건은 읽지 않고 새 메시지를 기다림 이때 메시지에 부여된 offset을 수정하는 것이 아니라, 소비자가 읽기 시작할 위치를 바꾸는 것이다. 방법도 구분해야 한다.\nseek(): 실행 중 소비자에게 할당된 파티션의 읽기 위치를 변경한다. seek() 호출만으로 Kafka에 저장된 committed offset이 변경되지는 않는다. 이후 commit하거나 자동 commit이 수행되면 저장된 위치도 변경될 수 있다. Kafka 읽기 위치 변경 그룹 offset 재설정: 해당 그룹의 소비자들을 모두 중지하고, 관리 도구로 저장된 재개 위치를 변경한 뒤 다시 실행한다. 다른 그룹의 저장된 위치는 변경되지 않는다. Kafka 그룹 offset 재설정 예를 들어 집계 로직을 수정한 뒤 지난주 주문 이벤트를 다시 읽어 통계를 재계산할 수 있다. 단, 읽기 위치만 바꾼다고 기존 집계 결과가 자동 초기화되는 것은 아니므로, 결과를 덮어쓸지 중복을 제거할지도 정해야 한다.\n7. 메시지는 언제 삭제될까? # Kafka는 소비자가 읽거나 offset을 commit했을 때가 아니라, 토픽의 보관·정리 정책에 따라 데이터를 삭제한다.\ncleanup.policy 정리 기준 delete 보관 기간(retention.ms) 또는 파티션별 용량 제한(retention.bytes)에 따라 오래된 로그 세그먼트를 삭제 compact 같은 key의 최신 값을 남기도록 과거 값을 백그라운드에서 정리 compact,delete compaction과 기간·용량 기준 삭제를 함께 적용 예를 들어 delete 정책에서 보관 기간을 7일로 설정하고 용량 제한은 두지 않았다고 가정한다.\n오늘 읽고 commit한 메시지도, 아직 보관돼 있다면 다시 읽을 수 있다. 아무 소비자도 읽지 않은 메시지라도 기간 기준을 충족하면 삭제 대상이 된다. 용량 제한까지 설정하면, 데이터가 많이 쌓여 7일보다 먼저 오래된 데이터가 삭제될 수도 있다. 기간 기반 삭제는 메시지마다 정확히 7일 뒤 실행되는 타이머가 아니다. 로그 세그먼트 단위로 삭제하며, 실제 시점은 세그먼트 전환과 정리 주기 등에 따라 달라진다. compaction도 즉시 실행되지 않으므로 같은 key의 여러 값이 잠시 함께 남을 수 있다. Kafka 토픽 보관·정리 정책\n이미 삭제된 데이터는 읽기 위치를 옮기거나 새 그룹을 만들어도 복구할 수 없다. 지난주 데이터를 재처리해야 한다면, 그 데이터가 남아 있도록 보관 기간·용량과 compaction 적용 여부를 정해야 한다.\n마무리 # offset은 파티션에 저장된 레코드의 위치다. 소비자 그룹이 commit하는 값은 다음에 읽을 위치이며, commit 자체가 토픽 데이터를 삭제하거나 외부 업무 처리를 확정하지는 않는다.\nIBM MQ의 트랜잭션 MQGET은 가져온 메시지의 제거를 commit으로 확정한다. Kafka에서는 그룹별로 읽기 위치를 저장하고, 데이터 삭제는 보관 정책이 결정한다. 따라서 commit한 메시지도 남아 있다면 다시 읽을 수 있지만, 그 메시지로 이미 수행한 업무까지 취소되는 것은 아니다.\n","date":"2026년 10월 7일","externalUrl":null,"permalink":"/posts/kafka-offset-vs-ibm-mq/","section":"글 목록","summary":"","title":"Kafka offset 이해하기: IBM MQ의 GET·commit과 무엇이 다를까?","type":"posts"},{"content":"Kafka와 RabbitMQ 비교 실습에서는 Kafka 메시지를 터미널로 읽었다. 이번에는 Kafbat UI를 설치해 토픽에 저장된 메시지와 소비자 그룹의 읽기 위치를 웹에서 확인했다.\n주문 이벤트 3개를 발행하고 소비한 뒤, 그룹의 커밋된 offset은 3, lag는 0이 됐다. 반면 토픽의 메시지는 여전히 3개였다. 소비자를 종료해도 커밋된 offset은 남았다. 화면에 보이는 이 값들이 각각 무엇을 의미하는지 실행 과정과 함께 정리한다.\n1. 실습 구성 # Kafbat UI는 Kafka 클러스터의 토픽·메시지·소비자 그룹 등을 조회하는 별도 웹 도구다. Kafka 브로커 자체에 관리 화면을 추가하는 설정이 아니라, Kafka에 접속하는 UI 서비스를 함께 실행한다. Kafbat UI 공식 저장소\n항목 이번 실습 실행 환경 Windows + Docker Desktop + WSL Ubuntu Kafka apache/kafka:4.3.1, 단일 노드 KRaft Kafka UI ghcr.io/kafbat/kafka-ui:v1.5.0 웹 접속 http://localhost:18080/ 토픽 / 그룹 orders-demo / ui-demo-a 입력 데이터 order.created 메시지 3개 확인 날짜 2026년 10월 7일 Docker가 없다면 Docker Desktop 설치와 WSL 연동을 먼저 완료한다. Python과 Java는 호스트에 따로 설치하지 않는다. Kafka 명령은 컨테이너 안에서 실행한다.\n이번 구성은 이전 비교 실습과 별개의 Kafka다. 기존 컨테이너를 수정하지 않고, 새 토픽에 동일한 테스트 메시지 3개를 발행한다. 단일 노드이므로 복제·장애 전환을 검증하는 구성은 아니다.\n실습 파일 # 아래 두 파일을 같은 작업 디렉터리에 준비한다. 캡처에서는 ~/kafka-ui-guide를 사용했다. 이후 명령은 모두 해당 디렉터리의 WSL Bash 터미널에서 실행한다.\ncompose.yaml: Kafka와 Kafbat UI 설정 messages.jsonl: 주문 이벤트 테스트 데이터 kafka-ui-guide/ compose.yaml messages.jsonl messages.jsonl의 내용은 다음과 같다. 실제 주문 정보가 아닌 테스트 데이터다.\n{\u0026#34;event\u0026#34;:\u0026#34;order.created\u0026#34;,\u0026#34;order_id\u0026#34;:\u0026#34;demo-001\u0026#34;} {\u0026#34;event\u0026#34;:\u0026#34;order.created\u0026#34;,\u0026#34;order_id\u0026#34;:\u0026#34;demo-002\u0026#34;} {\u0026#34;event\u0026#34;:\u0026#34;order.created\u0026#34;,\u0026#34;order_id\u0026#34;:\u0026#34;demo-003\u0026#34;} 2. Docker Compose로 Kafka와 UI 실행 # compose.yaml은 다음과 같이 구성했다.\nname: blog-kafka-ui-lab services: kafka: image: apache/kafka:4.3.1 environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 KAFKA_LOG_DIRS: /var/lib/kafka/data volumes: - kafka-data:/var/lib/kafka/data healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;/opt/kafka/bin/kafka-topics.sh\u0026#34;, \u0026#34;--bootstrap-server\u0026#34;, \u0026#34;kafka:9092\u0026#34;, \u0026#34;--list\u0026#34;] interval: 10s timeout: 10s retries: 12 start_period: 30s kafka-ui: image: ghcr.io/kafbat/kafka-ui:v1.5.0 depends_on: kafka: condition: service_healthy ports: - \u0026#34;127.0.0.1:18080:8080\u0026#34; environment: KAFKA_CLUSTERS_0_NAME: local-demo KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 KAFKA_CLUSTERS_0_READONLY: \u0026#34;true\u0026#34; DYNAMIC_CONFIG_ENABLED: \u0026#34;false\u0026#34; volumes: kafka-data: Kafka의 broker와 controller를 한 컨테이너에서 실행하고, 데이터는 kafka-data named volume에 저장한다. ZooKeeper는 사용하지 않는다. Kafka의 포트는 호스트에 공개하지 않고, UI만 127.0.0.1:18080에 공개한다.\nCompose에서 Kafka 연결 주소를 kafka:9092로 설정한 이유 # compose.yaml의 KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092는 UI 컨테이너가 Kafka에 접속할 주소다. UI 컨테이너에서 localhost는 Kafka가 아니라 UI 자신을 가리키므로, 같은 Compose 네트워크의 Kafka 서비스 이름인 kafka와 포트 9092를 사용한다.\n반면 **Windows 브라우저에서 UI에 접속할 주소는 http://localhost:18080/**다. 브라우저의 UI 접속 주소와 Compose 내부의 Kafka 연결 주소를 구분해야 한다.\nbootstrap 주소만 맞추면 끝나는 것도 아니다. Kafka는 접속한 클라이언트에 브로커 주소를 알려 주므로, KAFKA_ADVERTISED_LISTENERS도 UI가 접근할 수 있는 kafka:9092로 지정했다. 이 주소는 컨테이너 간 접속용이며 Windows 브라우저에 입력하는 주소가 아니다. Kafka advertised.listeners\n조회 전용으로 설정 # KAFKA_CLUSTERS_0_READONLY=true로 UI의 변경 기능을 제한했다. 토픽 생성과 메시지 발행은 CLI에서 수행한다. 실제 화면에서도 Produce Message, 토픽 삭제 등의 버튼이 비활성화되어 있다. Kafbat UI 설정\n조회 전용 UI가 Kafka의 인증·ACL을 대신하는 것은 아니다. 이번 구성은 로그인과 TLS가 없는 로컬 실습용이다. 인터넷이나 LAN에 공개하지 않는다.\n실행과 접속 # docker compose config --quiet docker compose up -d --wait --wait-timeout 180 docker compose ps 이번 실행에서는 Kafka와 UI가 모두 healthy로 표시됐다. UI의 호스트 포트는 127.0.0.1:18080-\u0026gt;8080/tcp로 확인했다.\nWindows 브라우저에서 http://localhost:18080/에 접속한다. Dashboard에 Online 1 clusters, Offline 0 clusters가 표시됐고, local-demo 클러스터의 브로커 수는 1이었다. 버전 칸은 Unknown으로 표시됐지만, 이후 토픽·메시지 조회는 정상적으로 동작했다. 사용한 Kafka 버전은 Compose 이미지 태그로 확인한다.\n3. 토픽 생성과 메시지 조회 # 주문 이벤트 3개 발행 # 파티션 1개, 복제본 1개인 orders-demo 토픽을 만들고 파일의 세 줄을 발행한다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-topics.sh \\ --bootstrap-server kafka:9092 --create --if-not-exists \\ --topic orders-demo --partitions 1 --replication-factor 1 docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \\ --bootstrap-server kafka:9092 --topic orders-demo \u0026lt; messages.jsonl -T는 가상 터미널 할당을 끄는 옵션이다. producer는 파일 내용을 표준 입력으로 받아 발행하고 종료하므로, 별도 성공 메시지가 출력되지 않아도 다음 화면에서 데이터를 확인한다.\nproducer를 반복 실행하면 메시지가 3개씩 추가된다. 처음 실습에서는 한 번만 발행한다. --if-not-exists도 기존 토픽을 초기화하는 옵션은 아니다.\nTopics와 Overview # 왼쪽 Topics를 선택하면 orders-demo가 표시된다. Partitions 1, Replication Factor 1, Number of messages 3을 확인했다.\n토픽 이름을 클릭하면 Overview가 열린다. 아래 파티션 표에서 Partition ID 0, First Offset 0, Next Offset 3, Message Count 3을 확인했다.\noffset은 파티션 안에서 각 레코드의 위치를 나타낸다. 새 토픽에 발행한 세 메시지의 offset은 0, 1, 2이고, 끝의 다음 위치는 3이다. Next Offset 3이 offset 3인 메시지가 이미 있다는 뜻은 아니다. Kafka offset 설명\nMessages에서 본문 확인 # Messages 탭에서 주문 이벤트의 JSON 본문을 확인했다. 이번 화면은 Newest로 설정되어 있어 최신 메시지가 위에 표시됐다.\n주문 번호 Partition Offset demo-001 0 0 demo-002 0 1 demo-003 0 2 화면의 정렬이 최신순이라는 것과 Kafka의 저장 순서는 구분해야 한다. 3 messages consumed도 UI가 조회한 결과이지, 아래에서 사용할 ui-demo-a 그룹의 커밋 완료를 뜻하지 않는다.\n4. 소비자 그룹의 offset과 lag 확인 # 콘솔 소비자로 읽기 # ui-demo-a 그룹으로 메시지를 읽고 자동 offset 커밋을 활성화한다.\ndocker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh \\ --bootstrap-server kafka:9092 --topic orders-demo \\ --group ui-demo-a --from-beginning \\ --command-property enable.auto.commit=true \\ --command-property auto.commit.interval.ms=1000 이 명령은 메시지 3개를 읽은 뒤에도 새 메시지를 기다린다. 출력 직후 바로 종료하지 말고, 잠시 유지한 상태에서 UI의 그룹 정보를 확인한다. 이후 Ctrl+C로 종료한다. 캡처에서는 주문 이벤트 3개와 Processed a total of 3 messages가 출력됐다.\nKafka 4.3.1에서는 --consumer-property가 deprecated이므로 --command-property를 사용했다. KIP-848 문구는 소비자 프로토콜에 대한 안내이며 오류가 아니다. 이번 실습에서 별도로 프로토콜을 변경하지는 않았다. Kafka 콘솔 소비자 옵션\n이미 커밋된 offset이 있는 그룹은 --from-beginning만으로 처음부터 다시 읽지 않는다. 재조회할 때는 새 그룹명을 사용하거나 별도로 읽기 위치를 조정해야 한다.\nConsumers에서 그룹의 위치 확인 # 왼쪽 Consumers에서 ui-demo-a를 열고 orders-demo 행을 펼친다. 캡처에서는 State STABLE, Members 1이 표시됐으며, 파티션 0의 Current Offset 3, End offset 3, Consumer Lag 0을 확인했다.\n항목 이번 값 의미 Current Offset 3 그룹이 커밋한 다음 읽기 위치 End offset 3 파티션 끝의 다음 위치 Consumer Lag 0 이번 데이터에서 끝 위치와 커밋된 위치의 차이 마지막으로 읽은 메시지는 offset 2이고, 커밋된 다음 위치는 3이다. 파티션이 여러 개면 그룹의 커밋된 위치도 파티션별로 확인해야 한다. 실행 중 소비자의 현재 읽기 위치와 브로커에 커밋된 위치는 별개이며, 커밋 전에는 둘이 다를 수 있다. Kafka 현재 위치와 커밋된 위치\n이번 소비자는 내용을 터미널에 출력하는 예제다. Lag 0이 실제 결제·메일 발송·DB 저장까지 완료됐다는 뜻은 아니다. 애플리케이션이 처리를 완료한 뒤 해당 위치를 커밋하도록 설계해야 작업 완료 판단에 사용할 수 있다.\n종료 후에도 커밋된 offset은 남는다 # 소비자를 종료한 뒤 CLI에서도 확인했다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-consumer-groups.sh \\ --bootstrap-server kafka:9092 --describe --group ui-demo-a has no active members는 현재 실행 중인 그룹 멤버가 없다는 뜻이다. 오류나 offset 삭제를 의미하지 않는다. 실제 출력에서도 CURRENT-OFFSET 3, LOG-END-OFFSET 3, LAG 0이 남아 있었다. 따라서 같은 그룹을 다시 실행하면 저장된 위치에서 재개할 수 있다. 데이터와 그룹 offset이 보관 정책에 따라 남아 있는 조건에서 가능한 동작이다.\n5. 메시지를 읽어도 토픽은 비워지지 않는다 # 그룹 offset을 확인하는 과정에서 Topics 목록에는 orders-demo의 메시지 수가 여전히 3으로 표시됐다. offset 커밋은 그룹의 재개 위치를 저장하는 것이지, 토픽 메시지를 삭제하는 동작이 아니다.\n목록의 __consumer_offsets는 그룹의 커밋된 offset 등을 관리하는 Kafka 내부 토픽이다. Show Internal Topics를 활성화하면 함께 표시된다. 주문 이벤트를 발행할 대상이 아니므로 수정·삭제하지 않는다. Kafka 그룹 offset 관리\n토픽 데이터를 무기한 보관하는 것은 아니다. 이번 Overview에는 Clean Up Policy DELETE가 표시됐다. 보관 기간·용량에 따라 오래된 데이터가 삭제될 수 있으며, 소비자가 아직 읽지 않았다고 반드시 남겨 두는 것도 아니다. 재조회가 필요하면 필요한 기간에 맞춰 보관 정책을 정해야 한다. Kafka 토픽 보관 정책\n6. 접속 확인과 실습 정리 # 화면이나 클러스터가 열리지 않는다면 # 아래 항목은 문제 발생 시 확인할 기준이며, 이번 캡처에서 발생한 오류는 아니다.\n웹에 접속되지 않으면 docker compose ps와 docker compose logs --tail=100 kafka-ui로 UI 상태를 확인한다. 브라우저 주소는 http://localhost:18080/이다. UI는 열리지만 클러스터가 Offline이면 docker compose logs --tail=100 kafka와 bootstrap·advertised 주소를 확인한다. 이번 구성에서는 둘 다 kafka:9092를 사용한다. 18080 포트가 이미 사용 중이면 기존 서비스를 종료하지 말고 Compose의 호스트 포트만 변경한다. UI의 컨테이너 포트 8080은 그대로 둔다. 메시지 수나 offset이 캡처와 다르면 발행 횟수와 기존 그룹 offset을 먼저 확인한다. UI 메시지 조회만으로 그룹의 커밋 여부를 판단하지 않는다. 데이터 유지와 삭제 # 잠시 중지할 때는 다음 명령을 사용한다. 다시 실행할 때는 docker compose start를 사용한다.\ndocker compose stop 이번 구성의 Kafka 데이터는 kafka-data named volume에 저장된다. docker compose down으로 컨테이너를 제거해도 해당 볼륨은 유지된다. 실습 데이터까지 삭제할 때만, 이 Compose 작업 디렉터리에서 다음 명령을 실행한다.\ndocker compose down --volumes 이 명령은 이번 프로젝트의 Kafka 데이터 볼륨도 삭제한다. 캡처와 필요한 데이터를 확보한 뒤 실행한다. Docker Compose 정리 명령\n마무리 # Kafbat UI에서 주문 이벤트의 offset 0·1·2와 그룹의 커밋된 offset 3을 확인했다. 소비자를 종료해도 그룹 위치는 남았고, 토픽의 메시지 3개도 제거되지 않았다.\nMessages는 토픽에 무엇이 저장돼 있는지, Consumers는 각 그룹이 어디까지 커밋했는지 확인하는 화면이다. 두 화면을 함께 보면 데이터 보관 상태와 소비 진행 상태를 구분할 수 있다.\n","date":"2026년 10월 7일","externalUrl":null,"permalink":"/posts/kafka-ui-guide/","section":"글 목록","summary":"","title":"Kafka를 웹에서 확인하기: Kafka UI 설치와 토픽·메시지 조회","type":"posts"},{"content":"Kafka와 RabbitMQ는 모두 서비스 사이에서 메시지를 전달할 때 사용한다. 하지만 “RabbitMQ는 한 번만 읽고, Kafka는 여러 번 읽는다”로 구분하면 정확하지 않다. RabbitMQ도 미확인 메시지를 재전달할 수 있고, Streams로 보관된 메시지를 다시 읽을 수 있다.\n핵심은 처리 완료가 메시지 제거로 이어지는가, 데이터를 남겨 두고 읽기 위치를 관리하는가다.\nDocker에서 두 서비스를 실행하고 같은 주문 메시지 3개를 보냈다. RabbitMQ에서는 소비자가 명시적으로 ACK를 보낸 뒤 큐가 비었고, Kafka에서는 서로 다른 소비자 그룹이 같은 메시지 3개를 각각 읽었다. 먼저 이 차이를 짚고, 실제 실행 과정을 살펴본다.\n1. 재전달과 재조회는 다르다 # 조회·재전달·재조회 구분 # 메시지를 “다시 읽는다”는 표현에는 서로 다른 동작이 섞여 있다.\n동작 목적 예시 메시지 조회(Browse) 큐의 메시지를 제거하지 않고 내용 확인 IBM MQ의 queue browsing 재전달(redelivery) 완료 확인을 받지 못한 메시지를 다시 전달 RabbitMQ의 미확인 메시지 requeue 재조회(replay) 처리했던 데이터도 보관 범위에서 다시 읽기 Kafka의 offset 이동, RabbitMQ Streams IBM MQ의 queue browsing은 메시지를 제거하지 않는 조회다. 메시지를 가져온 뒤 ACK나 트랜잭션 커밋을 미루는 것과는 다른 기능이다. 여기서 IBM MQ는 Browse 개념을 설명하는 예시이며, 아래 실습 대상은 아니다. IBM MQ Browse 설명\nRabbitMQ 관리 HTTP API도 메시지를 가져와 다시 큐에 넣는 ack_requeue_true 옵션을 제공한다. 다만 가져오기와 requeue로 큐 상태가 바뀌므로, IBM MQ의 Browse와 같다고 보면 안 된다. 이 API는 개발·문제 확인용이며 운영 소비자 구현용으로 권장되지 않는다. RabbitMQ 메시지 조회 API\nACK·confirm·commit은 무엇을 확인하는가 # 용어 확인·확정하는 대상 RabbitMQ consumer ACK 소비자가 전달받은 메시지의 처리 완료를 브로커에 알림 RabbitMQ publisher confirm 브로커가 발행자에게 메시지 수신을 확인. 소비자의 처리 완료 확인은 아님 Kafka offset commit 그룹·파티션별로 다음에 재개할 읽기 위치를 저장. 토픽 데이터 삭제는 아님 IBM MQ 트랜잭션 commit 트랜잭션에 포함된 MQGET·MQPUT 작업을 확정 RabbitMQ의 **명시적 ACK(수동 ACK)**는 자동 ACK 대신 애플리케이션이 처리 후 basic_ack를 호출하는 방식이다. 사람이 직접 확인한다는 뜻은 아니다. publisher confirm은 반대 방향인 브로커의 발행 확인이며, consumer ACK와 독립적이다. RabbitMQ ACK와 publisher confirm\nIBM MQ에서는 MQGMO_SYNCPOINT로 메시지를 가져온 뒤 트랜잭션을 commit하면 제거가 확정되고, backout하면 메시지가 큐에 복원된다. 따라서 “메시지를 가져온 뒤 commit했는가”는 이 방식에 맞는 설명이다. 다만 Kafka의 offset commit이나 RabbitMQ의 consumer ACK와 동일한 기능은 아니다. IBM MQ 트랜잭션과 메시지 처리\n이번 실습의 비교 범위 # 이번 실습에서 비교하는 대상은 RabbitMQ classic queue와 Kafka의 일반 소비자 그룹이다.\n기준 RabbitMQ classic queue Kafka 일반 소비자 그룹 메시지 처리 후 ACK를 받으면 해당 메시지를 큐에서 제거 읽었다는 이유만으로 토픽에서 제거하지 않음 완료 확인·재개 기준 전달된 메시지별 ACK 그룹·파티션별 커밋된 offset 작업 분담 같은 큐의 소비자에게 메시지를 분배 같은 그룹의 소비자에게 파티션을 분배 미완료 처리 재시도 미확인 메시지를 requeue하여 재전달 커밋된 위치에서 재개하거나 읽기 위치를 이동 완료된 데이터 재조회 ACK로 제거된 메시지는 그 큐에서 다시 읽을 수 없음 커밋 후에도 데이터가 남아 있으면 가능 먼저 검토할 용도 메일 발송, 이미지 변환 등 작업 전달 주문 이벤트 보관, 서비스별 독립 소비와 재처리 이번 비교에서 RabbitMQ의 consumer ACK는 해당 큐의 메시지 제거로 이어지지만, Kafka의 offset commit은 토픽 데이터를 남겨 둔다. 또한 커밋하지 않아도 실행 중인 Kafka 소비자의 현재 위치는 poll()로 읽을 때 진행된다. RabbitMQ ACK 설명, Kafka 현재 위치와 커밋된 위치\nKafka는 이벤트를 토픽에 보관하고 소비자가 읽은 위치를 관리한다. 한 소비자 그룹이 읽어도 다른 그룹이 같은 이벤트를 읽을 수 있으며, 삭제 시점은 보관 기간·용량과 정리 정책에 영향을 받는다. cleanup.policy=delete는 오래된 데이터를 정리하고, compact는 같은 key의 최신 값을 남기는 방식이므로 모든 변경 이력을 보존하지는 않는다. 소비자가 아직 읽지 않았더라도 보관 정책에 따라 데이터가 삭제될 수 있다. Kafka 소개, 토픽 보관 설정\nRabbitMQ Streams도 메시지를 로그에 보관하고 offset을 지정해 다시 읽을 수 있다. classic/quorum queue와는 소비 방식이 다르며, 기존 큐를 그대로 쓰면서 ACK만 생략하면 Streams가 되는 것은 아니다. RabbitMQ Streams\n2. 실습 환경 준비 # Windows의 Docker Desktop을 WSL Ubuntu와 연동한 환경에서 진행했다. Docker가 없다면 Docker Desktop 설치와 WSL 연동을 먼저 완료한다.\n항목 이번 실습 실행 환경 Windows + Docker Desktop + WSL Ubuntu Docker Desktop / Engine 4.94.0 / 29.8.2 Kafka 이미지 apache/kafka:4.3.1 RabbitMQ 이미지 rabbitmq:4.3.6-management Python 클라이언트 pika==1.3.2 입력 데이터 order.created 메시지 3개 구성 Kafka 1노드, RabbitMQ 1노드 확인 날짜 2026년 10월 7일 서비스는 기능 비교용으로 각각 한 대만 실행한다. 장애 전환이나 처리량을 측정하는 실험은 아니다.\n실습 파일 # 다음 네 파일을 같은 작업 디렉터리에 준비한다. 아래 명령은 모두 해당 디렉터리의 WSL Bash 터미널에서 실행한다. 캡처에서는 ~/temp를 사용했다.\ncompose.yaml: 두 서비스와 포트 설정 messages.jsonl: 공통 주문 메시지 requirements.txt: Python 패키지 rabbitmq_demo.py: RabbitMQ 발행·명시적 ACK 실습 코드 kafka-rabbitmq-lab/ compose.yaml messages.jsonl requirements.txt rabbitmq_demo.py messages.jsonl은 실제 주문 데이터가 아니라 다음 세 줄의 테스트 데이터다.\n{\u0026#34;event\u0026#34;:\u0026#34;order.created\u0026#34;,\u0026#34;order_id\u0026#34;:\u0026#34;demo-001\u0026#34;} {\u0026#34;event\u0026#34;:\u0026#34;order.created\u0026#34;,\u0026#34;order_id\u0026#34;:\u0026#34;demo-002\u0026#34;} {\u0026#34;event\u0026#34;:\u0026#34;order.created\u0026#34;,\u0026#34;order_id\u0026#34;:\u0026#34;demo-003\u0026#34;} Docker Compose 구성 # compose.yaml의 내용은 다음과 같다.\nname: blog-messaging-lab services: kafka: image: apache/kafka:4.3.1 ports: - \u0026#34;127.0.0.1:9092:9092\u0026#34; healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;/opt/kafka/bin/kafka-topics.sh\u0026#34;, \u0026#34;--bootstrap-server\u0026#34;, \u0026#34;localhost:9092\u0026#34;, \u0026#34;--list\u0026#34;] interval: 10s timeout: 10s retries: 12 start_period: 30s rabbitmq: image: rabbitmq:4.3.6-management environment: RABBITMQ_DEFAULT_USER: bloglab RABBITMQ_DEFAULT_PASS: local-lab-only ports: - \u0026#34;127.0.0.1:5672:5672\u0026#34; - \u0026#34;127.0.0.1:15672:15672\u0026#34; healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;rabbitmq-diagnostics\u0026#34;, \u0026#34;-q\u0026#34;, \u0026#34;check_port_connectivity\u0026#34;] interval: 10s timeout: 10s retries: 12 start_period: 20s 포트는 127.0.0.1에만 공개했다. bloglab / local-lab-only는 로컬 실습 계정이며, 공개 서버나 운영 환경에 사용하지 않는다. Kafka 명령은 컨테이너 안에서 실행하므로 호스트에 Java나 Kafka를 따로 설치하지 않아도 된다. Kafka Docker 실행 안내\ndocker version docker compose config --quiet docker compose up -d --wait --wait-timeout 180 docker compose ps 첫 실행에서는 이미지를 내려받은 뒤 두 컨테이너가 모두 Healthy로 표시됐다.\n시작에 실패하면 docker compose logs kafka rabbitmq로 로그를 확인한다. 9092, 5672, 15672 포트를 다른 서비스가 사용 중인지도 살펴본다.\nPython 가상환경과 실제 발생한 오류 # RabbitMQ 실습 코드는 Python의 Pika 클라이언트를 사용한다.\npython3 -m venv .venv .venv/bin/python -m pip install -r requirements.txt 이번 Ubuntu에서는 첫 번째 명령이 ensurepip is not available 오류로 실패했다. 오류 메시지에 안내된 python3.14-venv를 설치한 뒤 가상환경을 다시 만들었다.\nsudo apt update sudo apt install -y python3.14-venv python3 -m venv .venv .venv/bin/python -m pip install -r requirements.txt python3.14-venv는 이번 Python 버전에 해당하는 패키지다. 다른 환경에서는 python3 --version과 오류 메시지를 확인해 버전에 맞는 venv 패키지를 설치한다. 설치 후에는 pika-1.3.2 설치 성공을 확인했다.\n3. RabbitMQ에서 메시지 발행과 ACK 확인 # 처리 전에는 Ready 3 # .venv/bin/python rabbitmq_demo.py publish 스크립트가 orders-demo classic queue를 만들고 메시지 3개를 발행한다. 터미널에는 주문 번호별 PUBLISHED와 READY 3이 출력됐다.\nWindows 브라우저에서 http://localhost:15672/에 접속하고 bloglab / local-lab-only로 로그인한다. Queues and Streams에서 orders-demo를 확인한다.\n항목 의미 확인한 값 Ready 소비자에게 전달할 수 있는 대기 메시지 3 Unacked 전달됐지만 아직 ACK를 받지 않은 메시지 0 Total Ready와 Unacked의 합계 3 처리 후에는 Ready 0 # .venv/bin/python rabbitmq_demo.py consume 이번 코드는 메시지를 하나씩 가져와 내용을 출력하고 명시적으로 consumer ACK를 보낸다. 실제 결제나 주문 처리는 수행하지 않는다. 핵심은 자동 ACK를 끄고, 처리 단계 뒤에 ACK를 보내는 부분이다.\nmethod, _, body = channel.basic_get(queue=QUEUE, auto_ack=False) # 메시지를 확인하고 예제 처리 결과를 출력한 뒤 실행 channel.basic_ack(delivery_tag=method.delivery_tag) 위 코드는 흐름을 설명하는 발췌이며, 빈 큐 확인 등을 포함한 전체 코드는 첨부한 rabbitmq_demo.py에 있다. 짧은 실습을 위해 basic_get을 사용했고, 운영용 worker 구현은 아니다.\n관리 화면에서도 Ready 0 / Unacked 0 / Total 0으로 바뀌었다.\n큐 자체가 사라진 것은 아니다. 해당 큐의 메시지 3개가 ACK 후 제거된 것이다. 같은 큐에서 이 메시지를 다시 읽으려면 별도 보관본으로 다시 발행하는 등의 방법이 필요하다.\nACK를 보내지 않으면 어떻게 될까 # 명시적 ACK 모드에서 가져온 메시지는 Unacked 상태가 된다. ACK를 생략한다고 다음 basic_get에서 같은 메시지가 바로 반환되는 것은 아니다. 다시 전달하려면 basic_nack(..., requeue=True) 등으로 돌려보내거나, ACK 없이 채널·연결이 닫혀 자동으로 requeue되는 상황이 필요하다. TTL·전달 제한 등 큐 정책에 따라 이후 처리는 달라질 수 있다. 이는 완료된 메시지의 재조회가 아니라 미확인 메시지의 재전달이다. 이 경로는 이번 캡처에서 실험하지 않았다. RabbitMQ 재전달 설명\nrequeue=True를 무조건 반복하면 처리할 수 없는 메시지가 계속 재전달될 수 있다. 재시도 횟수·간격을 정해야 하며, requeue=False는 설정된 dead-letter exchange로 보내거나, 설정이 없으면 폐기하는 동작이다. “실패하면 자동으로 안전하게 보관된다”는 뜻은 아니다. RabbitMQ 실패 메시지 처리\n스크립트는 durable queue, persistent message와 publisher confirms도 사용한다. durable/persistent는 재시작 시 보존을 위한 설정이고, publisher confirm은 발행자 → 브로커 전달 확인이다. 소비자의 처리 완료를 알리는 ACK와는 별개다. 어느 설정도 단일 노드를 고가용성 구성으로 바꾸지는 않는다. RabbitMQ 메시지 내구성 실습, publisher confirm과 소비자 ACK\n4. Kafka에서 같은 메시지 두 번 읽기 # 토픽 생성과 발행 # Kafka에는 메시지를 보관할 orders-demo 토픽을 만든다. 이번에는 파티션과 복제본 수를 각각 1로 지정했다.\n토픽은 이벤트를 담는 논리적인 단위이고, 파티션은 토픽을 나누는 저장·소비 단위다. offset은 각 파티션 안에서 레코드의 위치를 나타낸다. 그룹이 offset을 커밋할 때도 파티션별로 다음에 읽을 위치를 저장한다. 토픽 전체에 하나의 공통 offset이 있는 것은 아니다. Kafka 파티션과 offset\ndocker compose exec -T kafka /opt/kafka/bin/kafka-topics.sh \\ --bootstrap-server localhost:9092 --create --if-not-exists \\ --topic orders-demo --partitions 1 --replication-factor 1 docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \\ --bootstrap-server localhost:9092 --topic orders-demo \u0026lt; messages.jsonl -T는 가상 터미널 할당을 끄는 옵션이다. producer에 파일 내용을 표준 입력으로 전달해 세 줄을 발행했다. localhost:9092는 이 명령이 실행되는 Kafka 컨테이너 안의 주소다.\n서로 다른 소비자 그룹으로 읽기 # 먼저 demo-a 그룹에서 처음부터 메시지 3개를 읽는다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-console-consumer.sh \\ --bootstrap-server localhost:9092 --topic orders-demo \\ --group demo-a --from-beginning --max-messages 3 --timeout-ms 15000 이어서 producer를 다시 실행하지 않고, 다른 그룹인 demo-b로 같은 토픽을 읽는다.\ndocker compose exec -T kafka /opt/kafka/bin/kafka-console-consumer.sh \\ --bootstrap-server localhost:9092 --topic orders-demo \\ --group demo-b --from-beginning --max-messages 3 --timeout-ms 15000 두 그룹에서 모두 demo-001, demo-002, demo-003과 Processed a total of 3 messages가 출력됐다.\nRabbitMQ에서는 ACK 후 해당 큐가 비었지만, Kafka에서는 첫 번째 그룹이 읽은 뒤에도 두 번째 그룹이 같은 데이터를 읽었다. 읽기가 토픽의 메시지 삭제를 의미하지 않는다는 차이를 직접 확인한 것이다.\n일반 소비자 그룹은 그룹별로 읽기 위치를 관리한다. 따라서 다른 그룹의 처리가 기존 그룹의 읽기 위치를 대신 진행시키지는 않는다. 동일 그룹 내 여러 소비자와 서로 다른 그룹의 소비자를 구분해야 한다. Kafka 소비자 설정\n재실습할 때 주의할 점 # producer 또는 RabbitMQ의 publish를 반복하면 메시지가 3개씩 추가된다. 첫 실습에서는 각각 한 번만 발행한다. Kafka 그룹에 저장된 offset이 있으면 --from-beginning만으로 처음부터 다시 읽지 않는다. 재조회 실습은 demo-c처럼 사용하지 않은 그룹명으로 진행한다. 처음에는 --timeout-ms 1500으로 실행해 메시지 0개와 시간 초과 오류가 나왔다. 최종 캡처는 15000으로 성공한 결과다. 같은 오류가 발생하면 30000으로 늘려 재시도하고, 계속 실패하면 연결·토픽·발행 상태를 확인한다. 이 실습은 offset 커밋의 정확한 시점이나 exactly-once 처리를 검증한 것이 아니다. 재조회도 데이터가 보관 정책에 따라 남아 있는 범위에서 가능하다.\n5. 어떤 상황에서 무엇을 선택할까 # 메일 발송처럼 처리 후 메시지를 남길 필요가 없는 작업은 RabbitMQ 큐를, 주문 생성 이벤트처럼 처리 후에도 여러 시스템에서 다시 읽어야 하는 데이터는 Kafka나 RabbitMQ Streams를 먼저 검토한다. 아래는 선택 예시이며, 이번 실습에서 구현한 서비스는 아니다.\n여기서 로그 모델은 디버깅 로그가 아니라 이벤트를 순서대로 저장하고, 소비자가 읽을 위치를 지정하는 구조를 뜻한다. 큐 메시지를 제거한다는 것도 주문 DB나 메일 발송 기록까지 삭제한다는 의미는 아니다.\n상황별 선택표 # 필요한 것 먼저 검토할 구성 선택 이유 메일 발송·이미지 변환을 여러 worker가 나눠 처리 RabbitMQ 큐 메시지별 ACK로 완료 확인, 미확인 메시지 재전달 메시지 종류에 따라 서로 다른 처리 큐로 전달 RabbitMQ exchange + 큐 routing key와 패턴으로 전달 대상 구분 같은 이벤트를 알림·분석·검색 서비스에 각각 전달 RabbitMQ 서비스별 큐 또는 Kafka 서비스별 그룹 둘 다 독립적인 소비 가능. 이력 재조회 요구로 구분 처리한 주문 이벤트를 나중에 다시 읽어 통계 재계산 Kafka 보관된 토픽을 offset 기준으로 재조회 RabbitMQ를 이미 운영하며 이벤트 보관·재조회도 필요 RabbitMQ Streams 큐와 별도로 로그 모델 사용. Kafka와 함께 비교할 후보 노드 장애에 대비해 작업 큐를 복제 RabbitMQ quorum queue 큐 복제본 과반수가 정상인 범위에서 장애 대응. 이력 재조회 목적은 아님 RabbitMQ의 direct exchange는 routing key 일치, topic은 패턴 일치, fanout은 연결된 모든 큐로 전달한다. Streams는 보관된 데이터를 다시 읽을 수 있지만 일반 큐의 메시지 우선순위·dead-letter exchange 기능을 그대로 제공하지는 않는다. RabbitMQ exchange, 큐와 Streams 기능 비교\n예시: 하나의 주문 이벤트를 여러 서비스에서 활용한다면 # 주문이 생성되면 알림 발송, 매출 집계, 검색 색인 갱신이 필요하다고 가정한다.\n항목 RabbitMQ 큐 구성 Kafka 구성 전달 경로 주문 이벤트 → exchange → 알림·집계·검색 큐 주문 이벤트 → 토픽 → 알림·집계·검색 소비자 그룹 독립적인 처리 각 서비스가 자기 큐의 메시지를 처리하고 ACK 각 그룹이 자기 offset을 관리하며 처리 알림 처리 완료 알림 큐에서 제거. 집계·검색 큐에는 영향 없음 알림 그룹의 offset을 커밋. 다른 그룹에는 영향 없음 이미 집계한 지난주 주문을 다시 계산 집계 큐에서 ACK로 제거했다면 별도 저장본 등 필요 해당 데이터가 남아 있으면 offset을 이동하거나 새 그룹으로 재조회 같은 이벤트를 여러 서비스에 전달한다고 Kafka가 필수인 것은 아니다. 핵심은 처리 완료 후에도 과거 이벤트를 다시 읽어야 하는지다. RabbitMQ Streams를 사용하면 큐 구성과 달리 보관된 이력의 재조회도 가능하다. RabbitMQ Streams, Kafka 이벤트 스트리밍 개념\n어느 쪽도 중복 처리를 자동으로 없애 주지는 않는다. 예를 들어 메일을 보낸 뒤 ACK 전에 연결이 끊기면 재전달되어 메일이 다시 발송될 수 있다. Kafka도 외부 작업 수행 후 offset 커밋 전에 중단되면 재처리가 발생할 수 있다. 작업 ID 등으로 중복을 방지하고 실패·재시도 정책을 별도로 정해야 한다. RabbitMQ 재전달과 멱등성, Kafka 전달 보장\n반대로 처리 전에 ACK하거나 해당 레코드를 넘어서는 offset을 커밋하면, 이후 처리 실패 시 정상적인 재전달·재개 경로에서 누락될 수 있다. Kafka 토픽에 데이터가 남아 있다는 것과 애플리케이션의 처리가 완료됐다는 것은 별개다. RabbitMQ ACK 모드, Kafka 처리 후 offset 커밋\n병렬 처리·순서·장애 대응 확인 # 기준 RabbitMQ classic/quorum queue Kafka 일반 소비자 그룹 병렬 처리 같은 큐에 여러 소비자를 연결해 작업 분담 파티션 단위로 분담. 한 그룹에서 한 파티션은 한 소비자에게 할당 순서 우선순위·재전달·병렬 처리 시 관찰 순서와 완료 순서가 달라질 수 있음 파티션 내부의 저장·읽기 순서가 기준. 파티션 간 전체 순서나 병렬 처리 완료 순서는 보장하지 않음 실패 처리 requeue, 설정된 dead-letter exchange 등으로 설계 재개 위치, 재시도·실패용 토픽 등을 애플리케이션에서 설계 장애 대비 classic은 비복제 큐. 복제·고가용성이 필요하면 quorum queue 검토 복제본 수와 쓰기 확인·최소 동기 복제본 설정 등을 함께 검토 이번 Kafka 토픽은 파티션이 1개다. 같은 그룹에 소비자를 여러 개 추가해도 그 파티션을 동시에 나눠 받지는 않는다. RabbitMQ에서도 메시지 전달 순서가 유지된다고 여러 worker의 처리 완료 순서까지 같아지는 것은 아니다. Kafka 소비자 그룹과 파티션, RabbitMQ 큐 순서와 복제\nRabbitMQ quorum queue는 Raft 기반 복제 큐다. 예를 들어 큐 복제본 3개를 서로 다른 노드에 배치하면 1개 노드 장애를 견딜 수 있지만, 과반수를 잃으면 가용성을 유지할 수 없다. 복제는 장애 대응을 위한 기능이지 ACK된 메시지를 다시 읽는 기능이 아니다. 이번 단일 노드 classic queue 실습으로 quorum queue의 장애 대응이나 Kafka의 복제 성능을 비교할 수는 없다. RabbitMQ quorum queue, Kafka 복제와 전달 보장\n이번 메시지 3개 실습은 기능 차이 확인이며, 처리량이나 지연 시간의 우열을 보여 주는 결과가 아니다.\n6. 실습 종료와 정리 # 잠시 중지하고 데이터를 유지하려면 다음 명령을 사용한다.\ndocker compose stop 같은 컨테이너를 다시 시작할 때는 docker compose start를 실행한다.\n캡처와 필요한 결과를 확보하고, 이번 Compose 프로젝트의 컨테이너와 실습 데이터를 삭제할 때만 다음 명령을 실행한다.\ndocker compose down --volumes 현재 구성은 외부 데이터 볼륨을 따로 지정하지 않았다. 컨테이너 내부 데이터와 연결된 익명 볼륨도 보존용 저장소로 취급하지 않는다. 이 명령은 다른 Compose 프로젝트나 Docker 이미지 전체를 삭제하는 명령이 아니다. Docker Compose 정리 명령\n마무리 # 이번 실습에서는 같은 메시지 3개를 보내고 처리 후 상태를 비교했다. RabbitMQ classic queue는 consumer ACK 후 비었고, Kafka 토픽의 메시지는 다른 소비자 그룹에서도 읽을 수 있었다.\n이 결과를 “RabbitMQ는 다시 읽을 수 없다”로 해석하면 안 된다. 미완료 메시지를 다시 전달하는 것과, 완료된 데이터를 보관해 다시 읽는 것은 다르다. 작업별 전달·완료 확인이 중심이면 RabbitMQ 큐를, 과거 이벤트의 독립적인 소비·재처리가 필요하면 Kafka나 RabbitMQ Streams를 먼저 검토한다. 이후 라우팅, 보관 기간, 순서와 장애 대응 조건을 비교한다.\n","date":"2026년 10월 7일","externalUrl":null,"permalink":"/posts/kafka-vs-rabbitmq-guide/","section":"글 목록","summary":"","title":"Kafka와 RabbitMQ, 무엇이 다를까? 직접 실행하며 비교하기","type":"posts"},{"content":"","date":"2026년 10월 7일","externalUrl":null,"permalink":"/tags/messaging/","section":"Tags","summary":"","title":"Messaging","type":"tags"},{"content":"","date":"2026년 10월 7일","externalUrl":null,"permalink":"/tags/offset/","section":"Tags","summary":"","title":"Offset","type":"tags"},{"content":"","date":"2026년 10월 7일","externalUrl":null,"permalink":"/tags/rabbitmq/","section":"Tags","summary":"","title":"Rabbitmq","type":"tags"},{"content":"","date":"2026년 10월 7일","externalUrl":null,"permalink":"/tags/windows/","section":"Tags","summary":"","title":"Windows","type":"tags"},{"content":"Kafka와 RabbitMQ를 실습해보려다, 먼저 로컬에 Docker를 설치하기로 했다. 서비스를 하나씩 직접 설치하는 대신 컨테이너로 실행하면 테스트 환경을 구성하고 정리하기 편하다.\n이미 WSL Ubuntu를 사용하고 있어 Windows에 Docker Desktop을 설치하고 Ubuntu와 연동하는 방식으로 진행했다. WSL에서 컨테이너를 실행한 뒤 Windows의 Docker Desktop에서도 같은 컨테이너가 보이는지 확인했다. 설치부터 실행 확인과 정리까지의 과정을 정리한다.\n1. Docker Desktop과 WSL의 역할 # 이 구성에서 Docker Desktop은 컨테이너 실행 엔진과 관리 화면을 제공하고, WSL Ubuntu는 Docker 명령을 입력하는 개발 환경으로 사용한다.\nWSL Ubuntu 터미널 docker 명령 실행 ↓ Docker Desktop의 WSL 2 기반 엔진 Linux 컨테이너 실행 ↓ Windows의 Docker Desktop 화면에서도 확인 Ubuntu에 별도의 Docker Engine을 추가 설치하는 방식이 아니다. 이미 Ubuntu에 Docker Engine이나 CLI를 직접 설치했다면 기존 구성과 데이터를 확인하고 설치 방식을 정리한 뒤 진행한다. Docker 공식 문서도 두 설치 방식의 충돌을 주의하도록 안내한다. Docker WSL 2 연동 문서\n2. 설치 전 환경 확인 # WSL과 Ubuntu는 이미 설치되어 있었으므로 다시 설치하지 않았다. Docker 설치 전에는 Ubuntu가 WSL 2로 실행되는지만 먼저 확인했다.\n항목 사용 환경 운영체제 Windows 11 Home, x86_64 WSL 2.7.14.0, Ubuntu는 WSL 2로 실행 Linux 배포판 Ubuntu 26.04.1 LTS Docker Desktop 4.94.0 Docker Engine 29.8.2 Docker Compose 5.5.1 확인 날짜 2026년 10월 7일 Docker 관련 버전은 설치 후 실행 화면에서 확인한 값이다. 이후 버전에서는 설치 화면이나 메뉴가 달라질 수 있다.\nWSL과 Ubuntu가 아직 설치되어 있지 않다면, WSL 환경에서 Codex CLI 설치하기의 **\u0026lsquo;1. WSL과 Ubuntu 설치\u0026rsquo;**를 먼저 진행한다. Docker 실습에는 해당 절의 WSL·Ubuntu 설치 과정만 필요하며, Codex CLI는 설치하지 않아도 된다.\nWindows PowerShell 또는 명령 프롬프트에서 WSL 상태를 확인한다.\nwsl --version wsl --list --verbose Ubuntu 행의 VERSION이 2인지 확인한다. wsl --version에 표시되는 WSL 프로그램 버전과 배포판의 실행 방식인 VERSION 2는 서로 다른 정보다.\nDocker Desktop의 WSL 2 구성은 WSL 2.1.5 이상, 시스템 메모리 8GB, 하드웨어 가상화 활성화 등을 요구한다. Windows Home에서도 이 글처럼 Linux 컨테이너를 사용할 수 있다. 지원하는 Windows 버전과 설치 조건은 Windows 설치 문서에서 확인한다.\n3. Docker Desktop 설치 # 설치 파일 다운로드 # Docker Desktop Windows 설치 페이지에서 PC의 CPU 아키텍처에 맞는 설치 파일을 받는다. 이번 환경은 x86_64이므로 Docker Desktop for Windows - x86_64를 선택했다.\nDocker Desktop은 개인 사용에 무료로 제공되지만, 회사에서 사용하는 경우 조직 규모와 이용 조건에 따라 유료 구독이 필요할 수 있다. 설치 전에 Docker Desktop 이용 조건을 확인한다.\n설치 옵션 선택 # 다운로드한 설치 파일을 실행하고 **Per-user installation (Recommended)**을 선택한다.\n이번 설치 화면에는 Per-user와 All-users 두 가지 선택지가 있었다. WSL 2로 Linux 컨테이너를 실행할 목적이라 현재 사용자에게 설치하는 Per-user를 선택했다. 별도로 WSL 2 백엔드를 선택하는 체크박스는 이 화면에 없었다.\n설치가 끝나면 Windows 시작 메뉴에서 Docker Desktop을 실행한다. 처음 표시되는 이용 약관을 확인하고 동의한 뒤 엔진이 시작될 때까지 기다린다.\n4. WSL Ubuntu 연동 # Docker Desktop에서 Settings → Resources → WSL integration으로 이동한다.\n사용할 배포판인 Ubuntu를 활성화하고 Apply \u0026amp; restart를 클릭한다.\n여기서는 Enable integration with my default WSL distro 체크를 꺼둔 채 Ubuntu만 개별적으로 활성화했다. 이후 Ubuntu 터미널에서 Docker 명령이 정상적으로 실행됐다. 기본 배포판 연동 체크가 꺼져 있어도 사용할 배포판을 직접 활성화하면 된다.\nGeneral 메뉴에 Use WSL 2 based engine 옵션이 표시된다면 활성화되어 있는지 확인한다. WSL 2 사용 환경에서는 기본 적용되어 이 옵션이 보이지 않을 수도 있다. WSL 2 백엔드 설정\n5. WSL 터미널에서 실행 확인 # 이제 Windows 터미널에서 Ubuntu로 들어간다. 배포판을 명시하면 기본 배포판 설정과 관계없이 Ubuntu가 열린다.\nwsl -d Ubuntu 이후 명령은 모두 WSL Ubuntu 터미널에서 실행한다.\nDocker 클라이언트와 엔진 확인 # docker version Client와 Server 정보가 모두 출력되는지 확인한다. Client만 표시되고 Server 연결 오류가 나오면 명령줄 도구는 있지만 실행 엔진에 연결되지 않은 상태다.\n이번 환경에서는 Client와 Server의 Engine 버전이 모두 29.8.2로 표시되었다. Server 항목에는 Docker Desktop 4.94.0도 나타난다.\nDocker Compose 확인 # docker compose version 나중에 여러 컨테이너를 함께 실행할 때 사용할 Compose도 확인했다. 별도로 설치하지 않아도 Docker Compose version v5.5.1이 출력되었다.\nhello-world 실행 # docker run --rm hello-world 첫 실행에서는 로컬에 이미지가 없어 다운로드가 먼저 진행됐다. 이어서 Hello from Docker! 메시지가 출력됐다. 버전 정보만 확인하는 것에서 한 단계 더 나아가, 실제 이미지 다운로드와 컨테이너 실행까지 성공한 것이다.\n--rm은 실행이 끝난 컨테이너를 자동으로 삭제하는 옵션이다. 다운로드한 이미지는 남는다. docker container run 명령\n6. Windows 화면에서 같은 컨테이너 확인 # hello-world는 실행 후 바로 종료된다. Docker Desktop에서 실행 중인 컨테이너를 확인하기 위해 Alpine 컨테이너를 하나 더 만든다.\ndocker run -d --name test-container alpine sleep infinity 각 옵션의 의미는 다음과 같다.\n옵션 또는 인수 의미 -d 백그라운드에서 실행 --name test-container 컨테이너 이름 지정 alpine 사용할 이미지. 태그를 생략해 latest 사용 sleep infinity 테스트용 컨테이너가 바로 종료되지 않도록 대기 명령이 성공하면 컨테이너 ID가 출력된다. 실행 상태는 다음 명령으로도 확인할 수 있다.\ndocker ps Windows의 Docker Desktop에서 Containers를 열면 같은 test-container가 실행 중으로 표시된다.\nWSL에서 실행한 test-container가 Windows 화면에도 그대로 나타났다. 컨테이너 ID의 앞부분도 일치했다. Ubuntu 안에 따로 Docker 서버를 설치한 것이 아니라, WSL 터미널과 Docker Desktop이 같은 엔진을 사용하고 있음을 확인했다.\n명령은 WSL에서 실행하고, 실행 상태는 Windows 화면에서 확인하는 식으로 사용할 수 있다.\n이 예제는 포트를 공개하지 않았으므로 브라우저로 접속하는 서비스는 없다. 이후 웹 서버나 메시지 브로커를 실행할 때는 필요한 포트와 데이터 저장 설정을 별도로 구성한다.\n7. 테스트 컨테이너 정리 # 확인이 끝나면 테스트 컨테이너를 중지하고 삭제한다.\ndocker stop test-container docker rm test-container docker rmi alpine stop은 실행 중지, rm은 컨테이너 삭제, rmi는 이미지 삭제다. 다른 컨테이너가 Alpine 이미지를 사용하고 있다면 이미지 삭제가 거부될 수 있으므로 사용 여부를 먼저 확인한다.\n앞서 다운로드한 hello-world 이미지도 필요 없다면 별도로 삭제한다.\ndocker rmi hello-world 이 명령들은 테스트 컨테이너와 이미지를 정리하는 것이며, Docker 전체 데이터나 볼륨을 삭제하는 명령은 아니다.\n8. 자주 겪는 문제 # Ubuntu에서 docker 명령을 찾지 못하는 경우 # Docker Desktop이 실행 중인지 확인하고, Settings → Resources → WSL integration에서 현재 사용하는 배포판이 활성화되어 있는지 확인한다. 설정 적용 후 Ubuntu 터미널을 다시 열어 실행한다.\n곧바로 Ubuntu에 docker.io를 추가 설치하면 다른 Docker 구성과 섞일 수 있으므로, 먼저 Desktop 연동 설정을 확인한다.\nServer 연결 오류가 발생하는 경우 # Docker Desktop 화면에서 엔진이 실행 중인지 확인한다. Desktop을 종료한 상태에서는 이 구성의 컨테이너를 실행할 수 없다.\n기존 Docker 설정이 있다면 다음 명령으로 다른 엔진을 가리키고 있지 않은지도 확인한다.\ndocker context ls printenv DOCKER_HOST DOCKER_CONTEXT 설정을 무조건 삭제하지 말고 기존 프로젝트에서 사용하던 연결인지 먼저 확인한다.\nWSL integration 메뉴가 보이지 않는 경우 # Windows 컨테이너 모드가 아닌 Linux 컨테이너 모드인지 확인한다. 모드 전환 메뉴가 있는 설치 환경에서는 Docker 메뉴의 Switch to Linux containers를 선택한다. WSL 배포판 연동 안내\n컨테이너 이름이 이미 사용 중인 경우 # 같은 명령을 다시 실행하면 test-container라는 이름이 이미 존재한다는 오류가 발생할 수 있다.\ndocker ps -a 기존 컨테이너가 테스트용인지 확인하고, 필요 없다면 중지와 삭제를 거친 뒤 다시 실행한다. 보관할 컨테이너라면 새 이름을 사용한다.\n마무리 # 이번 구성에서는 Ubuntu에 Docker Engine을 따로 설치하지 않고 Desktop 연동만으로 컨테이너를 실행할 수 있었다. hello-world로 실제 동작을 확인하고, 계속 실행되는 Alpine 컨테이너로 Windows 관리 화면과의 연결까지 확인했다.\n테스트 컨테이너는 정리했고 Docker Desktop과 WSL 연동은 그대로 남겨두었다. 이제 이 환경에서 Kafka와 RabbitMQ 실습을 이어가려 한다.\n","date":"2026년 10월 7일","externalUrl":null,"permalink":"/posts/docker-desktop-wsl-setup/","section":"글 목록","summary":"","title":"Windows에서 Docker Desktop 설치하고 WSL과 연동하기","type":"posts"},{"content":"","date":"2026년 9월 30일","externalUrl":null,"permalink":"/tags/css/","section":"Tags","summary":"","title":"Css","type":"tags"},{"content":"","date":"2026년 9월 30일","externalUrl":null,"permalink":"/tags/png/","section":"Tags","summary":"","title":"Png","type":"tags"},{"content":"","date":"2026년 9월 30일","externalUrl":null,"permalink":"/tags/svg/","section":"Tags","summary":"","title":"Svg","type":"tags"},{"content":"","date":"2026년 9월 30일","externalUrl":null,"permalink":"/tags/web/","section":"Tags","summary":"","title":"Web","type":"tags"},{"content":"홈페이지 로고를 SVG로 적용한 뒤 실제 웹 화면에서 보니, 작은 글자의 가장자리가 기대만큼 또렷하지 않았다. 로고의 표시 크기와 글자 폭을 조정하면서 SVG를 유지할지 PNG로 바꿀지도 고민했다.\n벡터 이미지라면 항상 선명해야 하는 것 아닐까? 같은 로고를 SVG와 PNG로 준비해 같은 표시 크기에서 비교해봤다.\n벡터로 제작된 웹 로고는 SVG를 우선 선택하면 된다. 다만 작은 크기의 가독성까지 파일 형식이 해결해 주는 것은 아니다. PNG를 선택할 때는 원본 픽셀 수와 실제 표시 크기를 함께 확인해야 한다.\n1. SVG 로고인데도 흐릿하게 보일 수 있을까? # SVG는 곡선과 도형을 좌표로 표현하지만, 모니터는 픽셀로 화면을 표시한다. 브라우저는 SVG의 도형을 현재 크기와 화면 배율에 맞춰 그린다. 곡선이나 대각선의 경계에서는 주변 픽셀의 색을 섞어 가장자리를 부드럽게 만드는 안티앨리어싱이 적용될 수 있다.\n따라서 확대해도 원본 도형이 유지되는 것과 작게 표시했을 때 또렷해 보이는 것은 다르다. SVG 규격도 출력 장치와 구현에 따른 렌더링 차이를 허용한다. W3C: SVG 렌더링 모델\n작은 로고의 인상에는 다음 조건이 영향을 준다.\n표시 크기: 작은 글자의 얇은 획이나 좁은 틈이 충분한 픽셀로 표현되는가? 화면 배율과 픽셀 밀도: 같은 CSS 크기라도 실제로 사용하는 화면 픽셀 수가 다른가? 좌표와 변환: 축소나 위치 이동으로 획의 경계가 픽셀 사이에 걸리는가? 원본 구성: 벡터 도형인지, SVG 안에 PNG 같은 비트맵이 포함되어 있는가? 원본을 크게 열었을 때의 선명함만으로는 헤더에서 어떻게 보일지 알 수 없다. 브라우저 배율 100%에서 실제 사용 크기로 확인하는 것이 먼저다. 소수점 좌표를 정수로 바꾸는 것만으로 해결되지도 않는다. 화면 배율과 픽셀 밀도에 따라 출력 좌표가 다시 달라지기 때문이다.\n2. SVG와 PNG의 차이 # 항목 SVG PNG 기본 표현 좌표로 정의한 도형과 곡선 정해진 수의 픽셀 확대 벡터 도형을 새 크기로 렌더링 기존 픽셀을 확대·보간 투명 배경 지원 지원 색상 변경 인라인 SVG는 CSS로 제어 가능 보통 다른 이미지 파일이 필요 잘 맞는 용도 벡터 로고, 아이콘, 도형 중심 그림 스크린샷, 픽셀 기반 이미지 PNG는 무손실 압축 형식이다. 저장 과정에서 손실 압축을 하지 않는다는 의미이지, 원본보다 확대해도 세부 정보가 늘어난다는 뜻은 아니다. SVG는 도형 중심 이미지에 적합하지만, 복잡한 경로가 많으면 PNG보다 파일이 커질 수도 있다. SVG가 항상 더 가볍다는 규칙도 없다. MDN: 이미지 형식 가이드\n또한 PNG를 SVG의 \u0026lt;image\u0026gt; 요소에 넣고 확장자만 .svg로 만들었다면, 그 안의 PNG가 벡터 도형으로 바뀌지는 않는다. 로고 원본이 벡터인지부터 확인해야 한다. W3C: SVG의 image 요소\n3. 고해상도 PNG가 더 선명할까? # PNG도 적절한 픽셀 수를 준비하면 충분히 선명하게 표시할 수 있다. 중요한 것은 파일 크기가 아니라 이미지의 가로·세로 픽셀 수다.\n예를 들어 로고를 웹페이지에서 160 × 40 CSS px로 표시한다고 가정하자.\n이미지의 픽셀 크기 기준으로 삼는 화면 설명 160 × 40px DPR 1 CSS 크기와 같은 픽셀 수 320 × 80px DPR 2 가로·세로 각각 2배 480 × 120px DPR 3 가로·세로 각각 3배 DPR(Device Pixel Ratio)은 CSS 픽셀과 기기의 실제 픽셀 사이의 비율이다. 위 표는 크기 산정 예시이며, 브라우저 확대 등에 따라 유효 비율은 달라질 수 있다.\n아래처럼 해상도별 PNG를 준비하고 srcset으로 제공할 수 있다. 세 파일은 동일한 로고와 비율로 내보내며, 표시 크기는 160 × 40으로 유지한다.\n\u0026lt;img src=\u0026#34;/images/logo.png\u0026#34; srcset=\u0026#34;/images/logo.png 1x, /images/logo@2x.png 2x, /images/logo@3x.png 3x\u0026#34; width=\u0026#34;160\u0026#34; height=\u0026#34;40\u0026#34; alt=\u0026#34;사이트 이름\u0026#34;\u0026gt; 브라우저는 화면 조건 등에 따라 적절한 이미지를 선택한다. 320 × 80 PNG를 320 × 80으로 표시하는 것이 아니라, 160 × 40 영역에 더 많은 원본 픽셀을 제공하는 방식이다. MDN: 반응형 이미지와 해상도 선택\n다만 160 × 40 PNG를 단순히 확대해서 320 × 80으로 저장하면 원래 없던 세부 정보가 생기지는 않는다. 가능하면 벡터 원본이나 충분히 큰 원본에서 각 크기로 내보내야 한다. 큰 PNG가 SVG보다 언제나 선명하다는 뜻도 아니다. 실제 표시 크기에서 두 결과를 비교해야 한다.\n같은 로고를 같은 크기로 비교하면? # 비교에는 실제로 제작한 로고의 벡터 원본을 사용했다. 위쪽 SVG 미리보기는 342 × 150 CSS px로, 아래쪽 세 로고는 모두 114 × 50 CSS px로 표시했다. PNG는 같은 SVG 원본을 각각 114 × 50px과 228 × 100px로 렌더링했다.\n캡처 환경은 Chrome, DPR 2, 브라우저 배율 100%, 흰색 배경이다. PNG 2배 이미지는 더 크게 표시하는 것이 아니라, 같은 영역에 더 많은 픽셀을 제공한다. 이 예시의 결과를 모든 로고나 브라우저에 일반화할 수는 없다.\n위 비교 이미지 자체는 PNG다. 이미지를 확대한다고 캡처 안의 SVG가 벡터로 다시 렌더링되지는 않는다. 아래에는 실제 SVG와 PNG 파일을 동일한 크기로 넣었으므로, 사용 중인 화면에서 직접 비교할 수 있다. 같은 배경 조건을 유지하기 위해 이 비교 영역은 다크 모드에서도 흰색이다.\nSVG PNG 1배 · 114×50px PNG 2배 · 228×100px 원본을 따로 확인하려면 SVG, PNG 1배, PNG 2배를 열어 보면 된다. SVG의 viewBox=\u0026quot;0 0 128 56\u0026quot;는 좌표 영역을 정의한 값이지, PNG처럼 고정된 픽셀 해상도를 뜻하지 않는다.\n4. 용도별 선택 기준 # 용도 우선 검토할 형식 이유 웹사이트의 벡터 로고 SVG 다양한 크기에 대응하기 쉬움 단순한 UI 아이콘 SVG 크기와 색상을 제어하기 쉬움 화면 캡처, 픽셀 기반 그림 PNG 원본 픽셀과 투명도를 보존하기 좋음 SNS 공유용 대표 이미지 PNG 또는 JPEG 게시할 서비스의 지원 형식에 맞춤 사진 JPEG, WebP, AVIF 사진에 적합한 압축 형식을 검토 이 표는 용도별 권장 기준이다. 웹 로고는 SVG로 제공하고, 같은 벡터 원본에서 공유·업로드용 PNG를 별도로 내보내는 방식이면 두 형식의 장점을 함께 활용할 수 있다. 공유 이미지는 서비스가 요구하는 크기와 형식을 따르고, 사진은 PNG로 고정하기보다 압축 후 화질과 용량을 비교한다. MDN: 이미지 형식 선택\n5. 다크 모드에서 SVG 색상 바꾸기 # 단색 로고나 아이콘은 currentColor를 쓰면 CSS의 color 값을 따라 색상을 바꿀 수 있다. 다음은 동작을 설명하기 위한 간단한 심볼 예제다.\n\u0026lt;a class=\u0026#34;brand\u0026#34; href=\u0026#34;/\u0026#34; aria-label=\u0026#34;사이트 이름 홈\u0026#34;\u0026gt; \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; viewBox=\u0026#34;0 0 32 32\u0026#34; width=\u0026#34;32\u0026#34; height=\u0026#34;32\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;circle cx=\u0026#34;16\u0026#34; cy=\u0026#34;16\u0026#34; r=\u0026#34;12\u0026#34; fill=\u0026#34;currentColor\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;/a\u0026gt; .brand { color: #111827; } .brand svg { display: block; } html.dark .brand { color: #f9fafb; } 위 코드는 사이트가 \u0026lt;html\u0026gt;에 dark 클래스를 붙이는 방식으로 다크 모드를 전환한다고 가정한다. 전환 방식이 다르면 해당 선택자를 바꾸면 된다. fill=\u0026quot;currentColor\u0026quot;는 고정된 검은색이 아니라 현재 요소의 color를 사용한다. W3C: currentColor\nimg로 불러온 SVG도 똑같이 동작할까? # 아니다. HTML 안에 직접 넣은 인라인 SVG와 외부 이미지로 불러온 SVG는 다르다.\n\u0026lt;img class=\u0026#34;brand-image\u0026#34; src=\u0026#34;/images/logo.svg\u0026#34; alt=\u0026#34;사이트 이름\u0026#34;\u0026gt; 이 방식에서는 페이지의 .brand-image { color: ... } 값을 바꿔도, 외부 SVG 내부의 currentColor로 그대로 전달되지 않는다. 페이지의 CSS가 SVG 내부 경로에 직접 적용되는 것도 아니다. HTML의 스타일을 내부 SVG 요소에 적용하려면 인라인 방식 등을 사용해야 한다. W3C: HTML 안의 SVG 스타일 적용\n따라서 페이지의 다크 모드 상태에 맞춰 단색 로고를 바꾸려면 인라인 SVG가 편리하다. \u0026lt;img\u0026gt;를 유지하고 싶다면 밝은 배경용·어두운 배경용 파일을 따로 준비해 테마에 맞춰 선택할 수 있다. 여러 색이 들어간 로고는 모든 색을 currentColor로 통일하기보다 원래 브랜드 색상과 배경 대비를 확인한다.\n인라인으로 넣는 SVG는 직접 제작했거나 신뢰할 수 있는 파일을 사용한다. 외부에서 받은 SVG를 검증 없이 HTML에 삽입하지 않는다.\n6. 적용 전 확인할 것 # 원본: 벡터 도형인지, SVG 안에 비트맵이 들어 있는지 확인한다. 실제 크기: 원본을 크게 열어 보는 것뿐 아니라 헤더·모바일의 표시 크기로 확인한다. PNG 해상도: 표시 크기와 DPR을 고려해 원본에서 필요한 픽셀 수로 내보낸다. 스타일과 배경: CSS 확대·축소, 라이트·다크 모드, 배경 대비를 확인한다. 글꼴 의존성: SVG의 \u0026lt;text\u0026gt;로 만든 로고라면 다른 기기에서도 같은 글꼴이 적용되는지 확인한다. 고정된 형태가 필요하면 사용 권한을 확인한 뒤 글자를 경로로 변환한다. 접근성: \u0026lt;img\u0026gt;의 alt나 로고 링크의 접근 가능한 이름을 제공한다. 링크에 이름이 있다면 내부 SVG는 중복해서 읽히지 않도록 처리한다. 홈페이지와 블로그 로고에는 SVG를 유지했다. PNG로 변환하는 것보다 실제 헤더에서 읽기 좋은 크기와 글자 형태를 맞추는 것이 이번 작업의 중심이었다.\n벡터 로고는 SVG를 기본으로 쓰고, PNG가 필요한 업로드에는 같은 원본에서 적절한 해상도로 내보내면 된다. 위 비교 자료도 원본을 크게 보는 것과 실제 사용 크기로 확인하는 것이 다르다는 점을 보여주기 위한 예시다.\n","date":"2026년 9월 30일","externalUrl":null,"permalink":"/posts/svg-png-website-logo-guide/","section":"글 목록","summary":"","title":"웹사이트 로고, SVG와 PNG 중 무엇을 써야 할까?","type":"posts"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/apache-2.0/","section":"Tags","summary":"","title":"Apache-2.0","type":"tags"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/copyright/","section":"Tags","summary":"","title":"Copyright","type":"tags"},{"content":"누군가 내 GitHub 블로그 저장소를 fork한다면, 글과 캡처 이미지를 그대로 가져가 자기 사이트에 올려도 될까? 사용자 정의 CSS와 Blowfish 테마는 같은 조건일까?\n이 블로그를 GitHub Pages로 공개하면서 확인한 질문이다. Jinosoft 블로그 저장소는 Public이지만 루트에 오픈소스 LICENSE를 두지 않았다. 직접 만든 콘텐츠의 이용 조건은 COPYRIGHT.md에 적었다. 이 저장소를 사례로 공개 범위와 재사용 허가의 차이를 살펴본다.\nfork는 가능한데, 재게시도 가능할까? # GitHub의 서비스 약관은 Public 저장소에 대해 GitHub 기능을 통한 보기와 fork를 허용한다. fork는 GitHub 안에서 저장소의 사본을 자신의 계정에 만드는 기능이다.\n이 저장소의 파일을 이용하는 상황별로 보면 차이가 분명하다.\n하려는 일 확인할 조건 GitHub에서 저장소를 보고 fork하기 GitHub 서비스 약관상 가능 fork한 블로그를 다른 도메인에 그대로 배포하기 글·이미지 등 직접 만든 콘텐츠의 재게시에는 별도 허가 필요 블로그 글과 캡처를 자기 사이트에 그대로 게시하기 COPYRIGHT.md에 따른 별도 허가 필요 사용자 정의 CSS를 복사해 자기 사이트에 사용하기 COPYRIGHT.md에 따른 별도 허가 필요 Blowfish 테마를 자기 사이트에 사용하기 테마의 MIT License에 따라 사용 가능, 배포 시 저작권·허가문 유지 fork할 수 있다는 사실만으로 글을 다른 사이트에 재게시하거나 사용자 정의 코드를 자기 제품에 포함해 배포할 권리까지 받은 것은 아니다. GitHub도 공개 저장소의 보기·fork 권한과 별도의 재사용 허가를 구분한다. 저작권법상 예외가 적용되는 경우는 별개다. GitHub 저장소 라이선스 안내, Choose a License: No License\nLICENSE가 없으면 어떻게 되나 # 루트에 LICENSE 파일을 두는 것은 의무가 아니다. README나 개별 파일에서 재사용 조건을 명시할 수도 있다. 어디에도 재사용 허가가 없다면 원칙적으로 저작권자가 권리를 보유하며, 다른 사람에게 일반적인 수정·재배포 권한을 부여하지 않은 상태다. Copyright 문구가 없어도 저작권 보호는 기본적으로 발생한다. GitHub 라이선스 안내\n반대로 MIT 같은 오픈소스 라이선스를 적용하면 조건을 지키는 사람에게 사용·수정·배포 권한을 명시적으로 준다. 출처 표시가 재사용 허가를 대신하지는 않는다. 허가된 범위는 해당 파일의 라이선스나 권리자의 별도 허락으로 확인해야 한다.\nCOPYRIGHT.md는 이 저장소에서 누가 어떤 콘텐츠의 권리를 갖고 있고 어떤 이용을 허락하지 않는지 알리는 문서다. 이를 적었다고 GitHub 서비스 약관에 따른 보기·fork 권한이 사라지는 것은 아니다. 또 공개된 파일을 누군가 복사하는 것을 기술적으로 막아 주는 장치도 아니다.\n한 저장소 안에서도 라이선스는 다를 수 있다 # 이 블로그의 글·캡처 이미지·사용자 정의 설정과 CSS에는 COPYRIGHT.md의 조건이 적용된다. themes/blowfish는 별도 Git 서브모듈이며 테마 개발자의 MIT License를 따른다. Hugo 같은 외부 도구도 각자의 이용 조건을 가진다.\nBlowfish를 사용할 수 있다고 해서 블로그 글과 이미지까지 가져갈 수 있는 것은 아니다. 한 저장소에서 파일을 찾았더라도 파일을 누가 만들었고 어떤 조건으로 제공했는지 각각 확인해야 한다.\n누군가 실제로 가져다 썼다면 # GitHub에서 저장소를 fork만 했다면 그 사실만으로 무단 재게시라고 볼 수 없다. 글·이미지를 자기 사이트에 올리거나 사용자 정의 코드를 별도 제품에 배포했다면, 먼저 허가 여부와 실제 사용 범위를 확인한다.\n원본 글과 복제된 페이지의 URL, 일치하는 부분, 확인 날짜와 화면을 보관한다. Git 커밋 기록도 원본을 언제 작성했는지 보여주는 자료가 된다. 상대 사이트 운영자에게 원본과 복제된 콘텐츠의 구체적인 URL을 전달하고 삭제 또는 이용 허가 협의를 요청한다. GitHub에 무단 복제물이 게시됐다면 저작권 침해 신고 안내를 확인한다. GitHub는 침해를 주장하는 파일이나 URL을 구체적으로 적도록 요구하며, 상대방이 이의를 제기할 수도 있다. 단순한 fork만을 근거로 신고해서는 안 된다. 다른 웹사이트에 게시됐다면 해당 사이트나 호스팅 서비스의 저작권 신고 절차를 확인한다. Google 검색결과 삭제 요청도 가능하지만, 검색결과에서 빠져도 원본 웹사이트의 복제물은 그대로 남을 수 있다. 실제 게시물을 내리려면 게시 사이트 측에도 요청해야 한다. Google 검색 도움말\n내 저장소를 공개할 때 선택할 것 # GitHub의 보기·fork를 허용하고 일반적인 재사용은 허락하지 않을 때: Public으로 공개할 수 있다. 별도의 재사용 허가를 부여하지 않고 README나 저작권 안내에 이용 조건을 분명히 적는다. 비공개로 유지해야 하는 파일은 애초에 공개 저장소에 넣지 않는다. 코드 재사용은 허용하고 글·이미지는 보호할 때: 코드에 적용할 라이선스와 그 범위를 명시하고, 글·이미지의 조건은 따로 적는다. 저장소 루트에 LICENSE 하나만 두면 모든 파일에 적용된다고 오해할 수 있으므로 적용 대상 디렉터리나 파일을 README에도 밝혀 둔다. 전체를 재사용 가능하게 공개할 때: 코드, 글, 이미지 각각에 적절한 이용 조건을 정하고 타인이 만든 자료를 그 조건으로 다시 허가할 권리가 있는지도 확인한다. 결론은 간단하다. Public은 접근 범위를, LICENSE는 재사용 허가를 정한다. 이 블로그처럼 공개 저장소를 운영하면서 글과 이미지는 별도 조건으로 관리할 수도 있다. 다른 사람의 저장소를 이용할 때도 Public 표시만 보지 말고, 실제로 사용하려는 파일의 라이선스를 확인해야 한다.\n오픈소스를 사용하는 쪽의 조건은 상용 제품에 쓰는 오픈소스 라이선스 비교에서 이어서 다뤘다.\n","date":"2026년 9월 28일","externalUrl":null,"permalink":"/posts/github-public-repository-open-source-license/","section":"글 목록","summary":"","title":"GitHub Public이면 내 블로그를 가져다 써도 될까? fork와 LICENSE의 차이","type":"posts"},{"content":"GitHub 저장소에 올린 .env나 설정 파일에서 토큰을 발견했다면, 파일을 지우고 다시 push하는 것으로 해결될까?\n아니다. 먼저 토큰을 폐기하거나 교체해야 한다. 파일을 삭제한 새 커밋을 올려도 이전 커밋에는 값이 남을 수 있다. 이 글은 공개 저장소에 GitHub Personal Access Token(PAT) 또는 다른 서비스의 API 키를 올린 상황을 기준으로, 대응 순서를 정리한다. 실제 토큰을 사용한 실습은 하지 않는다.\n먼저 확인할 것: 어디까지 공개됐나 # 상황 우선 조치 로컬 파일에만 있고 Git에 추가하지 않음 파일을 Git 관리 대상에서 제외하고, 다른 곳에 공유한 적이 있는지 확인 로컬 커밋에만 있고 원격에 push하지 않음 push하기 전에 커밋에서 제거하고, 다른 경로로 노출됐는지 확인 공개 저장소에 push함 토큰 폐기·교체를 최우선으로 처리한 뒤 영향 범위 확인 비공개 저장소나 CI 로그에 노출됨 접근 가능한 사람과 로그 보존 범위를 확인하고 토큰 폐기·교체 공개 저장소에 올라갔다면 누가 봤는지 확인할 때까지 기다리지 않는다. GitHub도 비밀정보가 노출되면 먼저 해당 자격 증명을 폐기하거나 교체하도록 안내한다. GitHub: 저장소에서 민감정보 제거\n1. 노출된 토큰을 폐기하고 새로 발급한다 # GitHub PAT라면 GitHub의 Settings \u0026gt; Developer settings \u0026gt; Personal access tokens에서 해당 토큰을 찾아 삭제한다. Fine-grained token과 classic token의 목록이 분리되어 있으므로 발급한 종류를 확인한다. 다른 서비스의 API 키라면 그 서비스의 관리 화면에서 폐기하거나 교체한다. GitHub 저장소에서 파일을 삭제해도 외부 서비스의 키가 무효화되지는 않는다. GitHub: PAT 관리\n새 토큰은 필요한 저장소와 권한만 허용하고 만료일을 설정한다. 기존 토큰을 사용하던 로컬 도구나 자동화가 있다면 새 자격 증명으로 갱신한다.\nGitHub Actions에서 필요한 값은 코드에 직접 적지 말고 저장소의 Secrets에 보관하며, 워크플로에서 가능한 경우 기본 제공 GITHUB_TOKEN 사용도 검토한다. GitHub: PAT 보안 권장사항\n토큰을 이미 폐기했더라도, 노출된 동안 사용됐을 가능성은 별도로 확인해야 한다.\n2. 노출 범위와 사용 흔적을 확인한다 # 어떤 토큰이었는지, 읽기·쓰기 권한이 어디까지였는지, 처음 공개된 시점부터 폐기할 때까지 무엇을 할 수 있었는지 정리한다. 저장소의 커밋·PR·Actions 로그, 연결된 서비스의 접근 기록과 변경 이력을 살펴본다. 예상하지 못한 로그인, 리소스 변경이나 과금이 보이면 해당 서비스의 사고 대응 절차를 따른다.\nGitHub의 secret scanning 경고가 있다면 감지된 위치와 상태를 확인한다. 경고가 없다고 안전하다는 뜻은 아니다. 탐지 대상과 설정에 따라 감지하지 못하는 값도 있다. GitHub는 공개 저장소의 일부 토큰을 자동으로 무효화할 수 있지만, 자동 조치를 가정하지 말고 발급처에서 직접 상태를 확인한다. GitHub: secret scanning, GitHub: 경고 평가\n3. 현재 파일에서 제거한다 # 예를 들어 .env 파일 전체가 실수로 추적됐다면, 토큰을 폐기한 뒤 아래처럼 Git의 추적 대상에서 제외한다.\ngit rm --cached -- .env --cached는 로컬 파일은 남기고 Git의 추적 대상에서만 제거한다. 로컬 .env의 유출 토큰도 지우거나 새 토큰으로 교체하고, .gitignore에 .env를 추가한 뒤 커밋한다. .gitignore는 이미 커밋된 내용을 지우지는 않는다. Git: git-rm, GitHub: 저장소에서 민감정보 제거\n공유해야 할 다른 설정도 들어 있는 파일이라면 파일 전체를 제외하는 대신 비밀값만 제거한다. 필요한 비밀값은 환경변수나 별도 비밀 저장소로 옮긴다.\n4. Git 기록 삭제가 필요한지 판단한다 # 현재 파일에서 비밀값을 지워도 이전 커밋, PR, fork 또는 다른 사람의 clone에는 남아 있을 수 있다. 파일이 커밋 기록에 포함됐는지는 파일 경로를 기준으로 확인할 수 있다.\ngit log --all -- .env 이 명령은 .env라는 경로의 기록만 확인한다. 다른 파일에 토큰을 적었거나 파일명이 바뀌었다면 그 파일도 별도로 확인해야 하며, 이 결과만으로 저장소 전체에 비밀정보가 없다고 판단할 수는 없다.\n폐기된 토큰만 남아 있다면 Git 기록을 무조건 다시 쓰기보다, 토큰이 실제로 무효화됐는지와 다른 민감정보가 함께 노출됐는지를 먼저 확인한다. GitHub도 토큰을 폐기·교체했다면 기록 재작성까지는 필요하지 않을 수 있다고 설명한다.\n반면 폐기할 수 없는 개인정보나 다른 민감정보까지 공개됐다면 기록 정리를 검토해야 한다. git-filter-repo 같은 도구로 기록을 다시 쓰면 커밋 ID가 바뀌고 협업자의 clone, PR, fork에 영향이 생긴다. 강제 push만으로 모든 사본이 사라지지도 않는다. 작업 전 백업과 영향 범위를 확인하고 협업자와 조율한 뒤 GitHub 공식 절차를 따른다.\n5. 같은 실수를 막는다 # .env, 개인 키, 로컬 설정 파일은 저장소에 추가하기 전에 제외한다. 공유할 설정 형식이 필요하면 실제 값이 없는 .env.example을 사용한다. 커밋 전에 git diff --cached로 스테이징된 내용을 확인한다. 화면 공유나 블로그 캡처에도 토큰이 보이지 않는지 살핀다. GitHub의 push protection을 활용한다. 지원하는 비밀값을 push 단계에서 막아 주지만 모든 형태의 비밀정보를 탐지하는 것은 아니다. 토큰은 용도별로 분리하고 필요한 권한과 수명만 부여한다. 가능하다면 자격 증명 관리자나 서비스의 전용 비밀 저장 기능을 사용한다. 순서는 폐기·교체 → 영향 확인 → 현재 파일 정리 → 필요할 때 기록 정리 → 재발 방지다. 저장소를 깨끗하게 보이게 만드는 것보다, 유출된 토큰을 더 이상 사용할 수 없게 만드는 일이 먼저다.\n공개 저장소 자체의 재사용 권한이 궁금하다면 GitHub Public 저장소와 LICENSE의 차이도 참고할 수 있다.\n","date":"2026년 9월 28일","externalUrl":null,"permalink":"/posts/github-token-leak-response/","section":"글 목록","summary":"","title":"GitHub에 토큰을 올렸다면? 파일 삭제보다 먼저 해야 할 일","type":"posts"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/gpl/","section":"Tags","summary":"","title":"Gpl","type":"tags"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/license/","section":"Tags","summary":"","title":"License","type":"tags"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/mit/","section":"Tags","summary":"","title":"Mit","type":"tags"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/open-source/","section":"Tags","summary":"","title":"Open-Source","type":"tags"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"2026년 9월 28일","externalUrl":null,"permalink":"/tags/token/","section":"Tags","summary":"","title":"Token","type":"tags"},{"content":"MIT 코드를 넣은 제품은 소스를 비공개로 배포할 수 있을까? GPL 코드를 사용한 경우에도 같을까? 두 라이선스 모두 상용 이용이 가능하지만, 배포물에 포함해야 할 고지와 소스 제공 범위는 다르다.\n이 글에서 비교하는 0BSD, MIT, BSD-3-Clause, Apache-2.0, MPL-2.0, GPL-3.0, AGPL-3.0은 모두 상용 이용과 유료 배포를 허용한다. 무료로 배포해야 하는 라이선스도 아니다. 차이는 배포 시 무엇을 함께 제공해야 하는가에 있다. Open Source Initiative FAQ, GNU GPL FAQ\n아래 표로 기본 조건을 비교하고, 소스를 비공개로 유지하고 싶은 경우와 GPL 코드가 꼭 필요한 경우의 선택 기준을 살펴본다. 실제 사용 시에는 프로젝트의 라이선스 버전, 예외 조항, 함께 포함된 구성요소도 확인한다.\n라이선스별 의무 비교 # 표의 \u0026lsquo;소스 제공\u0026rsquo;은 주로 코드를 외부에 배포하는 경우를 기준으로 한다. 다만 AGPL-3.0은 수정한 프로그램을 네트워크 이용자에게 제공할 때도 소스 제공 조건이 적용된다.\n라이선스 배포 시 고지 소스 제공 0BSD 라이선스 자체에 고지 유지 조건 없음 요구 없음 MIT 저작권 표시와 허가문 포함 요구 없음 BSD-3-Clause 저작권·라이선스 조건·면책 문구 유지 요구 없음 Apache-2.0 라이선스 사본 제공, 해당 시 NOTICE 유지·수정 사실 표시 요구 없음 MPL-2.0 라이선스와 소스 입수 방법 안내 MPL 적용 파일의 소스 제공 GPL-3.0 라이선스·저작권 표시 등 유지 배포하는 GPL 적용 프로그램의 대응 소스 제공 AGPL-3.0 GPL과 유사한 조건 GPL 조건에 더해 수정한 프로그램의 네트워크 이용자에게도 소스 제공 여기서 고지는 저작권 표시나 라이선스 문구를 조건에 맞게 보존한다는 뜻이다. 반드시 제품 첫 화면에 저작권자를 표시하라는 뜻은 아니다. 소스 제공은 배포한 바이너리에 대응하는 소스 코드를 수령자가 받을 수 있게 한다는 뜻이며, 언제나 인터넷 전체에 공개 저장소를 만들어야 한다는 뜻은 아니다. GNU GPL FAQ: 소스 공개 범위\n꼭 알아둘 차이 # MIT의 고지는 단순한 출처 링크가 아니다. 배포물에 저작권 표시와 허가문을 포함해야 한다. 0BSD에는 이 고지 유지 조건이 없다. 0BSD 원문, MIT 원문\nApache-2.0은 MIT보다 확인할 항목이 많다. 라이선스 사본을 전달하고, 원본에 NOTICE가 있으면 해당 고지를 유지하며, 수정한 파일에는 변경 사실을 표시해야 한다. 기여자의 특허 허여 조건도 명시한다. Apache-2.0 원문\nMPL-2.0은 파일 단위, GPL-3.0은 결합된 프로그램의 범위가 중요하다. MPL 적용 파일을 수정해 배포하면 해당 파일의 소스를 제공해야 하지만, 함께 배포하는 별개의 새 파일까지 자동으로 MPL이 되는 것은 아니다. GPL 코드를 다른 코드와 하나의 프로그램으로 결합해 배포한다면, 그 결합된 프로그램을 GPL 조건에 맞게 배포해야 한다. 단순히 GPL 프로그램과 별개의 프로그램을 함께 배포하는 경우까지 동일하게 취급하지는 않는다. Mozilla MPL FAQ, GNU GPL FAQ: 프로그램 결합\nGPL은 내부 사용만으로 소스 제공을 요구하지 않는다. GPL 적용 프로그램을 외부에 배포할 때 대응 소스를 제공해야 한다. 원본을 수정해 바이너리를 배포했다면 원본 프로젝트의 링크만 남겨서는 부족하고, 배포한 바이너리에 해당하는 소스가 필요하다. GNU GPL FAQ: 내부 사용, GNU GPL FAQ: 대응 소스\nAGPL-3.0은 네트워크 사용에도 추가 조건이 있다. 일반 GPL 프로그램을 서버에서만 실행하는 것은 보통 프로그램 사본의 배포가 아니다. 반면 수정한 AGPL 적용 프로그램을 네트워크로 제공하면, 이용자가 그 프로그램의 대응 소스를 받을 방법을 제공해야 한다. 서비스에 사용한 모든 별도 프로그램의 소스를 무조건 공개한다는 뜻은 아니다. GNU GPL FAQ: 서버 사용, GNU AGPL-3.0 원문\n상황별 선택 기준 # 내 제품의 소스를 비공개로 유지하려면: 0BSD, MIT, BSD-3-Clause, Apache-2.0 라이선스의 코드를 우선 검토한다. 이들은 해당 코드를 포함해 배포해도 제품 전체의 소스 제공을 요구하지 않는다. 0BSD는 해당 코드에 대한 고지 유지 조건도 없지만, 함께 사용하는 다른 구성요소의 고지 의무까지 없어지는 것은 아니다. MIT·BSD·Apache-2.0에는 각각 고지 의무가 있다. 특허 허여 조항이 중요하다면 Apache-2.0의 조건도 살펴볼 만하다. MPL-2.0 코드는 별도 파일의 비공개를 허용하지만, MPL 적용 파일의 소스는 제공해야 한다. 0BSD, MIT, Apache-2.0, Mozilla MPL FAQ\nGPL 계열 코드가 꼭 필요하다면: 먼저 정확한 라이선스 버전과 링크 예외·이중 라이선스가 있는지 확인한다. LGPL-3.0은 GPL-3.0과 다르다. LGPL 라이브러리는 조건을 지키면 비공개 프로그램과 결합해 배포할 수 있지만, 라이브러리 소스 제공과 사용자가 수정한 라이브러리로 다시 연결할 수 있게 하는 등의 요건이 남는다. GNU LGPL-3.0 원문\n예외가 없는 GPL-3.0 적용 프로그램을 배포해야 한다면 다음을 확인한다.\n범위: GPL 코드와 다른 코드가 하나의 프로그램으로 결합되는지 확인한다. 결합된 프로그램에는 GPL 조건이 적용되지만, 별개의 독립 프로그램을 나란히 배포한다고 그 프로그램까지 GPL이 되지는 않는다. GNU GPL-3.0 제5조 및 집합물 정의 고지: GPL 라이선스 사본과 기존 저작권·면책 고지를 유지하고, 수정한 부분에는 변경 사실을 표시한다. 기존에 대화형 법적 고지가 있는 프로그램이라면 그 표시 의무도 확인한다. GNU GPL-3.0 제4·5조 소스 범위: 배포한 바이너리에 대응하는 소스를 준비한다. GPL 적용 코드의 수정본뿐 아니라 결합된 프로그램을 생성·설치·실행·수정하는 데 필요한 소스와 관련 스크립트가 포함된다. GPL이 정의한 시스템 라이브러리와 독립적인 별도 프로그램은 제외된다. GNU GPL-3.0 대응 소스 정의 제공 방법: 바이너리 수령자가 GPL 제6조에 맞는 방법으로 대응 소스를 받을 수 있게 한다. 다운로드로 바이너리를 제공한다면 대응 소스에도 같은 방식으로 추가 비용 없이 접근할 수 있게 하는 방법이 있다. 무조건 공개 GitHub 저장소를 만들어야 하는 것은 아니다. GNU GPL-3.0 제6조 이 조건을 지킬 수 없다면 GPL 코드를 비공개 제품에 그대로 결합해 배포하지 말고, 대체 구성요소나 저작권자가 제공하는 별도 상용 라이선스를 검토한다. AGPL-3.0이라면 바이너리 배포뿐 아니라 수정한 프로그램의 네트워크 제공 여부도 확인해야 한다. GNU AGPL-3.0 제13조\n배포 전 확인할 것 # 사용한 코드의 라이선스 이름과 버전, 별도 예외 조항을 확인한다. 배포한다면 저작권·라이선스 문구, NOTICE, 수정 표시, 소스 제공 조건을 확인한다. 라이선스가 없는 공개 저장소는 자유롭게 재사용해도 된다는 뜻이 아니다. 허가가 불분명하면 사용 전에 권리자에게 확인한다. GitHub 라이선스 안내 제품 소스를 비공개로 유지하려면 고지 의무가 있는지와 별개로 소스 제공을 요구하는 범위부터 확인한다. GPL 코드가 꼭 필요하다면 배포할 프로그램의 대응 소스까지 준비할 수 있는지 검토한 뒤 선택한다.\n","date":"2026년 9월 28일","externalUrl":null,"permalink":"/posts/open-source-license-commercial-use/","section":"글 목록","summary":"","title":"오픈소스, 상용 제품에 써도 될까? MIT·GPL·Apache 라이선스 비교","type":"posts"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/bgp/","section":"Tags","summary":"","title":"Bgp","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/classic-vpn/","section":"Tags","summary":"","title":"Classic-Vpn","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/cloud-router/","section":"Tags","summary":"","title":"Cloud-Router","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/cloud-vpn/","section":"Tags","summary":"","title":"Cloud-Vpn","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/cost-optimization/","section":"Tags","summary":"","title":"Cost-Optimization","type":"tags"},{"content":"GCP와 외부 데이터센터를 연결할 때 VPN으로 시작할지, 전용 연결을 사용할지 고민하게 된다. 특히 Partner Interconnect가 Dedicated보다 얼마나 저렴한지는 용량과 포함된 비용을 맞춰 봐야 알 수 있다.\n비교할 연결 방식은 크게 두 가지다.\n공개 인터넷을 경유하는 IPsec 기반 Cloud VPN 전용 회선 또는 통신사를 이용하는 Cloud Interconnect 서울 리전에서 국내 IDC로 월 2TiB를 전송하는 가정을 두고 계산했다. 10Gbps에서는 Partner와 Dedicated의 GCP 청구액 차이가 작지만, 1Gbps로 충분하다면 Partner의 고정 비용을 크게 낮출 수 있다. 단, 통신사와 물리 회선 비용은 별도다.\n가격은 2026년 9월 23일 Google Cloud 공식 가격 문서를 확인한 기준이다. 가격은 변경될 수 있으므로 실제 구성 전에는 Network Connectivity 가격표와 VPC 네트워크 가격표를 다시 확인해야 한다.\n1. 결론부터 보기 # 상황 추천 구성 테스트, 개발, 낮은 트래픽 HA VPN 신규 운영 환경에서 BGP와 이중화 필요 HA VPN + Cloud Router 콜로케이션 접근이 어렵거나 통신사 관리형 연결 필요 Partner Interconnect 대규모 트래픽과 직접 연결 필요 Dedicated Interconnect AWS와 전용 클라우드 연결 필요 Cross-Cloud Interconnect 또는 Partner Cross-Cloud Interconnect BGP를 지원하지 않는 오래된 장비 Classic VPN 정적 라우팅을 임시 또는 호환성 용도로 검토 신규 운영 환경에서 동적 라우팅과 고가용성이 필요하다면 기본 선택은 HA VPN + Cloud Router다. Classic VPN은 현재 레거시 장비와의 호환성이 필요한 경우에만 제한적으로 검토하는 편이 좋다.\n2. Cloud VPN 종류 # Cloud VPN은 Google Cloud VPC와 외부 네트워크 사이에 IPsec 사이트 간 터널을 만드는 관리형 서비스다. 사용자 PC가 접속하는 SSL VPN이나 Client VPN 용도는 아니다.\nHA VPN # HA VPN은 BGP 기반의 고가용성 VPN이다.\n동적 라우팅(BGP)만 지원 게이트웨이에 두 개의 인터페이스 제공 두 인터페이스에 터널을 구성하면 Google Cloud 쪽 99.99% SLA 조건 충족 가능 active-active 또는 active-passive 라우팅 구성 가능 IPv4와 IPv6 지원 외부 IP가 자동 할당되고 Classic VPN처럼 전달 규칙을 직접 만들 필요 없음 일반적인 온프레미스 연결은 다음처럼 구성한다.\nGCP HA VPN interface 0 \u0026lt;-\u0026gt; 온프레미스 VPN gateway 0 GCP HA VPN interface 1 \u0026lt;-\u0026gt; 온프레미스 VPN gateway 1 피어 장비가 외부 IP 하나만 제공해도 두 GCP 인터페이스에서 터널을 구성할 수 있다. 다만 피어 장비가 한 대뿐이면 해당 장비 장애까지 제거되는 것은 아니다. HA VPN 토폴로지\nAWS와 연결할 때는 AWS의 두 Site-to-Site VPN 연결에서 네 개의 터널을 구성하는 방식이 일반적이다. Google Cloud 공식 가격 문서도 AWS 피어 게이트웨이에서 99.99% 구성을 위해 네 개 터널을 안내한다.\nClassic VPN # Classic VPN은 정적 라우팅을 사용하는 이전 방식의 VPN이다.\n정적 라우팅만 지원 BGP 동적 라우팅 신규 구성 불가 HA VPN보다 낮은 99.9% SLA 외부 IP와 전달 규칙을 직접 구성 IPv6 미지원 Google Cloud는 2025년 8월 1일부터 Classic VPN의 신규 BGP 구성을 지원하지 않는다. 기존 BGP 터널은 계속 동작할 수 있지만 지원과 가용성 SLA가 적용되지 않는다. 따라서 BGP가 필요한 신규 환경에는 HA VPN을 사용해야 한다. Classic VPN 지원 중단 안내\n3. Cloud Router는 무엇인가 # Cloud Router는 VPN 종류가 아니라 BGP를 실행하는 관리형 라우터다. HA VPN과 함께 사용해 다음 경로를 동적으로 교환한다.\nGCP VPC 서브넷 경로를 피어 네트워크에 광고 피어 네트워크의 사설 경로를 GCP에서 학습 터널 장애 시 다른 터널로 경로 전환 BGP 우선순위에 따른 active-active 또는 active-passive 구성 Cloud Router 자체는 무료이며, BGP 제어 트래픽에 일반적인 네트워크 비용이 적용될 수 있다. Cloud Router 가격\n4. 비용 비교 시나리오 # 다음 조건을 동일하게 적용해 비교한다. Cloud VPN은 GCP 리전뿐 아니라 외부 피어의 위치에 따라 데이터 전송 요금이 달라질 수 있으므로, 이 글에서는 서울 리전의 VPC와 GCP 외부의 국내 온프레미스 IDC를 연결하는 상황으로 고정한다.\n가정 # GCP 리전: asia-northeast3(서울) 연결 대상: GCP 외부의 국내 온프레미스 IDC 기간: 1개월, 월 평균 730시간 GCP VPC에서 외부 데이터센터로 나가는 데이터: 월 2TiB = 2,048GiB VPN: HA VPN 터널 2개 네트워크 서비스 등급: Premium Tier(Cloud VPN gateway는 Standard Tier 미지원) Partner Interconnect: 10Gbps VLAN attachment 2개(99.9% 가용성 기준) Dedicated Interconnect: 10Gbps 회선 2개(99.9% 가용성 기준) 세금, 통신사 비용, 콜로케이션 비용, 장비 비용은 제외 환율 참고: 1달러 = 1,400원 가정 아래 모든 시간당 비용은 월 평균 730시간 기준으로 계산한다. 실제 청구액은 해당 월의 실제 사용 시간과 적용 중인 할인에 따라 달라질 수 있다.\n연결 대상은 Google Cloud 밖의 국내 온프레미스·코로케이션 데이터센터다. AWS 서울 리전 등 다른 클라우드에는 Cloud VPN 또는 Cross-Cloud/Partner Cross-Cloud Interconnect의 해당 가격표를 확인해야 하며, 아래 Interconnect 금액을 그대로 적용하지 않는다. GCP 내부 VM·VPC 사이의 연결도 이 시나리오와 다르다.\n월 2TiB의 평균 처리량은 약 6.7Mbps다. 먼저 Partner와 Dedicated를 각 연결 10Gbps로 맞춰 비교하고, 이후 1Gbps로 충분한 경우를 따로 계산한다. 필요한 용량은 평균값만 아니라 순간 피크와 한 경로의 장애까지 고려해 정한다.\nHA VPN은 10Gbps와 같은 고정 대역폭 상품이 아니다. 터널당 한도는 패킷 크기에 따라 약 1~3Gbps이며, 2개 터널을 구성해도 10Gbps 처리량이 보장되는 것은 아니다. 따라서 아래 표에서 HA VPN은 10Gbps 대체 상품이 아니라, 같은 데이터 전송량을 기준으로 한 저비용 기준선으로 표시한다.\n아래 Interconnect 구성은 99.9% 가용성 수준의 비교다. 99.99% 토폴로지에 필요한 연결 수는 표 아래에서 별도로 설명한다.\n월 비용 비교 # 구성 대역폭 또는 처리량 기준 고정 비용 데이터 전송 비용 Google Cloud 예상 합계 원화 참고 HA VPN 2개 터널 고정 대역폭 없음, 터널당 약 1~3Gbps $109.50 $389.12 $498.62 약 698,068원 Partner Interconnect 10Gbps 2개 각 VLAN attachment 10Gbps, 이중화 $3,445.60 $86.02 $3,531.62 약 4,944,262원 Dedicated Interconnect 10Gbps 2개 각 회선 10Gbps, 이중화 $3,544.88 $86.02 $3,630.90 약 5,083,254원 Partner와 Dedicated의 Google Cloud 고정 비용 차이는 약 $99.28이다. Partner는 10Gbps VLAN attachment 비용에 통신사 서비스 비용이 포함되지 않고, Dedicated는 Google Cloud 포트 비용 외에 콜로케이션, 물리 회선, cross-connect, 라우터 운영 비용이 포함되지 않는다. 따라서 실제 구매 비용은 Google Cloud 청구액만으로 판단할 수 없다. Partner Interconnect 공식 안내도 서비스 제공업체 비용이 별도라고 설명한다.\n또한 99.99% 가용성이 필요한 운영 환경에서는 두 연결만으로 충분하지 않을 수 있다. 공식 권장 토폴로지는 Partner Interconnect의 경우 최소 4개 VLAN attachment를 두 리전에 나누고, Dedicated Interconnect의 경우 최소 4개 연결을 두 개의 metro에 나누는 구성이다. 이 경우 Google Cloud 고정 비용만 단순 계산해도 Partner는 약 $6,891.20, Dedicated는 약 $7,089.76로 증가하며, 두 방식 모두 외부 회선 비용은 별도다. 두 리전으로 확장할 때 발생할 수 있는 추가 네트워크 비용과 애플리케이션 변경은 이 계산에 포함하지 않았다. Partner Interconnect 99.99% 구성과 Dedicated Interconnect 가용성 안내\n서울 리전의 Cloud VPN 가격표에서 게이트웨이 위치를 Seoul (asia-northeast3)로 선택하면 터널 단가를 확인할 수 있다.\nCloud VPN gateway는 Standard Tier를 지원하지 않으므로, HA VPN 데이터 전송 비용은 Premium Tier 기준으로 계산한다. 서울에서 국내 외부 IDC로 전송하는 경우 가격표의 대한민국 대상 구간은 0~10TiB까지 $0.19/GiB, 10TiB 초과분은 $0.15/GiB다.\nHA VPN 계산 # 서울(asia-northeast3) 게이트웨이의 터널 단가 $0.075/시간을 적용한다. Cloud VPN 가격표는 외부 VPN 게이트웨이로의 데이터 전송에 인터넷 데이터 아웃바운드 요율이 적용된다고 안내한다. 서울 출발·국내 목적지의 구체적인 요율은 VPC 네트워크 가격표에서 출발 리전을 서울로 선택해 확인한다.\n터널 비용: 2 x $0.075 x 730시간 = $109.50 데이터 전송: 2,048GiB x $0.19 = $389.12 합계: $498.62 HA VPN은 고정 비용이 작고, 별도 회선이나 콜로케이션이 필요하지 않다. 따라서 트래픽이 적거나 빠른 구축이 중요한 환경에 유리하다.\nPartner Interconnect 계산 # 공식 가격표에서 10Gbps Partner VLAN attachment 하나의 단가는 시간당 $2.36다. 이중화를 위해 2개를 사용한다. 서울과 국내 외부 IDC가 속한 아시아 연결의 Interconnect 데이터 전송 단가 $0.042/GiB를 적용한다.\nVLAN attachment: 2 x $2.36 x 730시간 = $3,445.60 데이터 전송: 2,048GiB x $0.042 = $86.016 합계: $3,531.616 Partner Interconnect는 VLAN attachment만 생성하면 완성되는 연결이 아니다. GCP에서는 VLAN attachment와 Cloud Router를 구성하고, 별도로 지원 서비스 제공업체를 통해 데이터센터와 통신사 네트워크를 연결해야 한다. 서비스 제공업체는 pairing key를 사용해 VLAN attachment를 연결하며, Layer 2 방식에서는 온프레미스 라우터와 BGP를 직접 구성하고 Layer 3 방식에서는 서비스 제공업체가 일부 BGP 구성을 담당한다. Partner Interconnect 구성 안내\nVLAN 용량을 낮추면 시간당 attachment 비용은 줄어든다. 하지만 GiB당 데이터 전송 단가는 용량과 무관하고, 통신사 서비스 비용도 별도로 발생한다. 다음은 730시간 기준으로 GCP VLAN attachment 비용만 비교한 예시다.\n이중화 구성 월 GCP attachment 비용 50Mbps 2개 약 $79.09 100Mbps 2개 $91.25 1Gbps 2개 약 $405.59 10Gbps 2개 $3,445.60 따라서 평균 트래픽이 낮더라도 순간 피크를 수용하고, 한 경로가 중단된 뒤 남은 attachment 하나로 필요한 트래픽을 처리할 수 있도록 용량을 정해야 한다. 이 글의 월 2TiB는 평균 약 6.7Mbps이지만, 50Mbps 2개가 모든 운영 환경에 충분하다는 뜻은 아니다.\nPartner 1Gbps와 Dedicated 10Gbps 비교 # Partner Interconnect는 VLAN attachment 용량을 1Gbps로 선택할 수 있다. Dedicated Interconnect는 10Gbps 또는 100Gbps 회선으로 연결할 수 있으며, 이 글의 비교에서는 10Gbps 회선 2개를 기준으로 한다. 월 평균 트래픽이 약 6.7Mbps이고 순간 트래픽도 1Gbps를 넘지 않는다면, Partner는 각 attachment를 1Gbps로 낮춰 Dedicated 10Gbps보다 작은 비용으로 구성할 수 있다. 아래는 두 방식 모두 2개 연결로 이중화하고, 앞의 시나리오와 같은 월 2TiB를 전송한다고 가정한 계산이다. Partner Interconnect의 1Gbps attachment 단가는 시간당 $0.2778이다.\n구성 GCP 고정 비용 데이터 전송 비용 Google Cloud 예상 합계 원화 참고 Partner Interconnect 1Gbps 2개 $405.59 $86.02 $491.61 약 688,254원 Dedicated Interconnect 10Gbps 2개 $3,544.88 $86.02 $3,630.90 약 5,083,254원 이 조건에서는 Partner 1Gbps 2개의 Google Cloud 비용이 Dedicated 10Gbps 2개보다 약 $3,139 낮다. 단, 한 경로가 장애를 일으키면 남은 attachment 하나로 처리해야 하므로 이 예시는 장애 시 최대 1Gbps까지 처리하면 되는 환경에 적합하다. 또한 Partner 서비스 제공업체 비용은 별도이며, VLAN 용량을 1Gbps로 낮춰도 통신사 비용이 같은 비율로 줄어든다는 보장은 없다.\n핵심은 필요한 용량을 낮춰 선택할 수 있느냐다. 10Gbps끼리 비교한 결과와 1Gbps로 충분한 환경의 결과를 구분해서 봐야 한다.\nDedicated Interconnect 계산 # 공식 가격표의 10Gbps Cloud Interconnect connection 단가 $2.328/시간과 10Gbps VLAN attachment 단가 $0.10/시간을 적용한다. 이중화를 위해 각각 2개를 사용한다.\n회선: 2 x $2.328 x 730시간 = $3,398.88 VLAN attachment: 2 x $0.10 x 730시간 = $146.00 데이터 전송: 2,048GiB x $0.042 = $86.016 합계: $3,630.896 Dedicated Interconnect는 고정 비용이 크지만 높은 처리량과 직접 연결을 제공한다. 위 계산에는 콜로케이션, 회선 설치, 라우터, cross-connect 비용이 포함되지 않았으므로 실제 비용은 더 높아질 수 있다.\n5. 비용표를 어떻게 해석할까 # 표의 합계는 전체 구축 견적이 아니라 Google Cloud 청구액이다. 이 전제에서 필요한 용량과 회선 운영 방식을 함께 판단한다.\n10Gbps에서 Partner와 Dedicated 선택 # 10Gbps 환경에서 Partner Interconnect를 선택하는 이유는 Google Cloud 요금이 저렴해서가 아니다. Google의 콜로케이션 시설에 직접 장비를 설치하기 어렵거나, 통신사가 제공하는 관리형 연결과 기존 회선망을 이용해야 할 때 Partner가 유리하다. 반대로 콜로케이션과 라우터를 직접 운영할 수 있고 물리 회선을 직접 관리하려면 Dedicated를 우선 검토할 수 있다.\n따라서 10Gbps 구성에서는 다음처럼 판단한다.\n비용만 비교: 두 방식의 Google Cloud 청구액은 거의 비슷함 Partner 선택: 통신사 관리, 위치 제약 완화, 자체 물리 회선 운영 부담 감소 Dedicated 선택: 직접 연결, 경로와 장비에 대한 통제, 통신사 중간 구간 최소화 트래픽이 적은 경우 # 월 데이터가 적고 별도 회선 계약을 원하지 않는다면 HA VPN이 현실적이다. 터널 비용과 데이터 전송 비용만 고려하면 되며, 구성도 빠르다.\n트래픽이 증가하는 경우 # VPN은 외부로 나가는 데이터 전송 단가가 커질 수 있고, 터널당 처리량 한도도 고려해야 한다. 트래픽이 꾸준히 증가하면 Partner Interconnect의 VLAN 비용과 파트너 회선 비용을 합산해 다시 비교해야 한다.\n매우 높은 처리량이 필요한 경우 # Dedicated Interconnect는 10Gbps 이상의 회선과 직접 연결이 필요한 환경에 적합하다. 일반적인 소규모 서비스에서 비용을 줄이기 위한 선택지는 아니다.\nAWS와 연결하는 경우 # 낮은 트래픽과 빠른 구축이 목적이면 HA VPN을 먼저 검토한다. AWS와 GCP 사이에 대규모 트래픽이 지속되거나 인터넷 경로를 사용하지 않아야 한다면 Cross-Cloud Interconnect 또는 Partner Cross-Cloud Interconnect를 비교한다.\n6. Cloud VPN과 Interconnect의 차이 # 항목 HA VPN Partner Interconnect Dedicated Interconnect 경로 인터넷 기반 IPsec 서비스 제공업체 전용 연결 Google과 직접 연결 시작 비용 낮음 중간 높음 구축 속도 빠름 파트너 일정 필요 회선과 시설 준비 필요 처리량 터널 PPS와 패킷 크기에 영향 선택한 VLAN 용량 10Gbps 이상 고정 회선 지연 시간 인터넷 경로에 따라 변동 비교적 예측 가능 가장 예측 가능 암호화 IPsec 기본 제공 필요하면 HA VPN over Interconnect 필요하면 HA VPN over Interconnect 적합한 환경 테스트, 일반 운영, 낮은 트래픽 콜로케이션이 어렵거나 파트너 회선을 활용하는 환경 직접 회선·콜로케이션 운영이 가능한 대규모 또는 핵심 연결 Partner Interconnect와 Dedicated Interconnect 자체는 전용 연결 경로를 제공하지만 IPsec 암호화를 기본 제공하는 VPN은 아니다. 암호화가 필요하면 HA VPN over Interconnect를 추가할 수 있으며, 이 경우 Interconnect와 VLAN attachment, HA VPN 터널 비용을 함께 계산해야 한다. 대신 해당 구성에서는 Cloud VPN 데이터 전송 비용이 아니라 Cloud Interconnect 데이터 전송 비용이 적용된다. HA VPN over Cloud Interconnect\nCloud VPN 터널의 한도는 평균 패킷 크기에 따라 달라진다. 현재 공식 제한은 터널당 인바운드와 아웃바운드 합계 250,000 packets per second이며, 대략 1~3Gbps에 해당할 수 있다. 고정된 3Gbps로 이해하면 안 된다. Cloud VPN 할당량과 제한\n7. 선택 체크리스트 # 다음 질문에 답하면 선택이 쉬워진다.\n피어 장비가 BGP를 지원하는가? VPN 터널 하나가 중단되어도 서비스가 계속되어야 하는가? 월간 외부 송신량이 얼마나 되는가? 인터넷 기반 IPsec으로 충분한가? 통신사 회선 계약과 콜로케이션을 운영할 수 있는가? 1Gbps 이상의 처리량이나 일정한 지연 시간이 필요한가? AWS와 연결할 때 전용 클라우드 간 연결이 필요한가? 구성 방식은 다음처럼 정리할 수 있다.\nBGP와 고가용성이 필요한가? ├─ 예 -\u0026gt; HA VPN + Cloud Router └─ 아니오 -\u0026gt; 기존 장비 호환성이 필요한 경우에만 Classic VPN 검토 높은 처리량 또는 인터넷 경로를 피해야 하는가? ├─ 아니오 -\u0026gt; HA VPN └─ 예 -\u0026gt; 연결 대상과 운영 방식 확인 ├─ 다른 클라우드 -\u0026gt; Cross-Cloud 또는 Partner Cross-Cloud Interconnect ├─ 직접 장비·물리 회선 운영 가능 -\u0026gt; Dedicated Interconnect └─ 통신사 관리형 연결 필요 -\u0026gt; Partner Interconnect 마무리 # 이 가정에서는 HA VPN으로 고정 비용을 작게 시작할 수 있다. 전용 연결이 필요하더라도 1Gbps로 충분하다면 Partner를 검토할 이유가 있고, 10Gbps가 필요하면 직접 회선을 운영할 수 있는지까지 판단해야 한다.\n실제 견적은 표의 Google Cloud 비용에 통신사·시설·장비 비용을 더해서 비교한다. 연결 대상이나 리전, 전송량이 다르면 데이터 전송 요금부터 다시 계산해야 한다.\n","date":"2026년 9월 23일","externalUrl":null,"permalink":"/posts/gcp-cloud-vpn-selection-guide/","section":"글 목록","summary":"","title":"GCP Cloud VPN 종류별 특징과 비용, 선택 가이드","type":"posts"},{"content":"내부 서비스가 VM 한 대뿐이고 트래픽도 적다면, 로드 밸런서 비용도 작을 것이라고 생각하기 쉽다. 하지만 Envoy 기반 내부 애플리케이션 로드 밸런서는 사용량과 별개로 최소 프록시 비용이 발생한다.\n이 글에서 다루는 대상은 INTERNAL_MANAGED 스킴을 사용하는 Envoy 기반 내부 애플리케이션 로드 밸런서다. 이 방식은 Google이 관리하는 프록시 계층을 통해 요청을 백엔드로 전달하며, 경로 기반 라우팅이나 HTTP(S) 처리 같은 기능을 제공한다.\n필요한 것이 요청 분산인지, VM 재생성 후에도 유지되는 접속 주소인지를 구분해 보자. 후자만 필요하다면 Stateful Managed Instance Group(MIG)의 내부 IP 보존을 대안으로 검토할 수 있다. 아래는 가격표와 구성 조건을 비교한 내용이며, Stateful MIG가 로드 밸런서와 같은 기능을 제공한다는 뜻은 아니다.\n1. Envoy 기반 내부 로드 밸런서의 동작 # Envoy 기반 로드 밸런서는 클라이언트와 백엔드 VM 사이에 Google 관리 프록시를 둔다.\n클라이언트 | v 내부 로드 밸런서 프록시(Envoy) | v 백엔드 VM 또는 NEG 클라이언트의 연결을 프록시가 먼저 종료하고, 프록시가 백엔드로 별도의 연결을 만든다. 이 구조를 이용하면 다음과 같은 기능을 사용할 수 있다.\nURL 경로와 호스트 기반 라우팅 HTTP와 HTTPS 처리 여러 백엔드 그룹으로 요청 분배 Google 관리형 프록시를 통한 확장 백엔드 서비스와 상태 확인 연계 프록시는 VPC와 리전 단위로 사용되며, 이를 위해 별도의 proxy-only subnet도 필요하다. 프록시 전용 서브넷은 백엔드 VM에 사용하는 일반 서브넷과 분리해야 한다. Proxy-only subnet 공식 문서\n2. 트래픽이 적어도 발생하는 최소 비용 # Google Cloud 공식 가격 문서에 따르면 로드 밸런서 하나에는 성능과 안정성을 위해 최소 3개의 프록시 인스턴스가 할당된다. 트래픽이 없어도 이 최소 프록시 인스턴스 비용은 발생하며, 전달 규칙이 INTERNAL_MANAGED로 배포된 시점부터 과금이 시작된다.\n사용자가 프록시 수를 1개로 줄이는 방식은 제공되지 않는다. 트래픽이 증가하면 Google Cloud가 필요한 프록시 수를 자동으로 늘린다.\n아래 가격은 2026년 9월 23일 Google Cloud 공식 가격 문서를 확인한 기준이다. 가격과 리전별 단가는 변경될 수 있으므로, 실제 구성 전에는 가격 문서와 Google Cloud Pricing Calculator에서 다시 확인해야 한다.\n확인 당시 가격 문서의 예시 단가는 프록시 인스턴스당 시간당 $0.025다.\n최소 비용 예시 3개 x $0.025 x 730시간 = 월 $54.75 1달러를 1,400원으로 가정하면 월 약 76,650원이다. 이는 비용 규모를 이해하기 위한 단순 환산 예시이며, 실제 원화 청구 금액은 결제 시점의 환율과 결제 계정 설정에 따라 달라진다.\n가격 문서의 최소 프록시 요금 설명에서도 시간당 최소 $0.075를 확인할 수 있다.\n서울 리전(asia-northeast3)의 가격표에는 프록시 인스턴스당 시간당 $0.025, 부하 분산기에서 처리하는 데이터 GiB당 $0.008이 표시된다. 아래 화면은 이 글을 작성한 시점에 확인한 서울 리전의 가격표다.\n프록시 인스턴스 비용과 별도로 전달 규칙 및 데이터 처리 비용이 발생할 수 있다. 실제 청구 금액은 리전별 단가, 세금, 데이터 처리량, 전달 규칙 비용 등에 따라 달라지므로, 구성 전에 Cloud Load Balancing 가격 문서와 Pricing Calculator에서 확인해야 한다.\n3. 프록시 인스턴스의 처리 기준 # 프록시 수는 사용자가 직접 지정하는 값이 아니라, 일정 기간 동안 측정된 트래픽 용량을 기준으로 자동 계산된다. Google Cloud는 10분 단위로 다음 항목을 측정하고, 필요한 프록시 수가 가장 많은 항목을 기준으로 사용량을 계산한다.\n기준 프록시 1개가 처리할 수 있는 예시 용량 대역폭 최대 18MB/s 신규 HTTP 연결 초당 600개 신규 HTTPS 연결 초당 150개 활성 연결 3,000개 요청 수 초당 1,400개, Cloud Logging 미사용 기준 Cloud Logging을 100% 샘플링하면 프록시 한 개의 요청 처리 용량이 예시상 초당 700개로 줄어들 수 있다. 실제 용량은 프로토콜, TLS, 로깅 설정 등 여러 조건에 영향을 받는다.\n연결 수와 요청 수는 서로 다른 기준이다. 같은 초당 1,000개라도 활성 연결인지 신규 HTTPS 연결인지에 따라 필요한 프록시 수가 달라진다.\n예를 들어 대역폭만 단순하게 계산하면 초당 100MB는 100 / 18이므로 약 6개의 프록시 용량이 필요할 수 있다. 반면 1,000개의 활성 연결은 프록시 1개 기준인 3,000개보다 작고, 초당 1,000개의 요청도 1,400개보다 작다. 그러나 초당 1,000개의 신규 HTTPS 연결은 프록시 1개 기준인 150개를 넘기므로 여러 프록시가 필요할 수 있다.\n프록시 비용을 예상할 때는 연결·요청·대역폭과 TLS·로깅 조건을 구분한다. 로드 밸런서의 필요성은 여기에 경로 라우팅과 백엔드 분산 요구를 더해 판단한다.\n4. 소규모 서비스에서 비용이 아쉬운 이유 # 다음과 같은 환경에서는 최소 프록시 비용이 서비스 규모에 비해 크게 느껴질 수 있다.\n내부 서비스의 백엔드 VM이 1대뿐인 경우 고정된 포트로 TCP 연결만 전달하면 되는 경우 URL 경로 기반 라우팅이 필요하지 않은 경우 월간 트래픽과 요청 수가 적은 경우 VM이 재생성되어도 동일한 내부 IP로 접근하면 되는 경우 이런 환경에서 Envoy 기반 애플리케이션 로드 밸런서를 사용하면 VM 비용 외에 프록시 인스턴스, 데이터 처리, 전달 규칙 비용이 추가된다. 특히 요청량이 거의 없어도 최소 프록시 비용이 발생하므로, “트래픽이 적으면 로드 밸런서 비용도 거의 없을 것”이라고 예상하면 실제 청구액과 차이가 생길 수 있다.\n5. Stateful MIG를 이용한 대안 # 대안으로 Managed Instance Group에 Stateful 구성을 적용하고, VM의 내부 IP를 보존하는 방식을 사용할 수 있다.\nStateful MIG는 VM이 재시작, 재생성, 자동 복구, 업데이트될 때 VM별 상태를 유지할 수 있다. Stateful IP로 지정한 내부 IP는 VM이 다시 만들어져도 같은 VM 인스턴스에 다시 연결된다. Stateful MIG 공식 문서\n구성 흐름은 다음과 같다.\n인스턴스 템플릿을 만든다. 내부 IP를 정적 주소로 예약한다. 인스턴스 그룹을 만든다. nic0의 내부 IP를 Stateful IP로 지정한다. 애플리케이션 상태 확인과 자동 복구를 구성한다. 내부 클라이언트가 해당 내부 IP 또는 내부 DNS 이름으로 접속하도록 구성한다. gcloud를 사용할 때는 MIG 생성 또는 업데이트 과정에서 --stateful-internal-ip 옵션을 사용해 내부 IP를 Stateful로 지정할 수 있다.\ngcloud compute instance-groups managed create APP_GROUP \\ --region REGION \\ --template APP_TEMPLATE \\ --base-instance-name app \\ --size 1 \\ --instance-redistribution-type NONE \\ --stateful-internal-ip interface-name=nic0 실제 명령어에는 리전 또는 영역, 기본 인스턴스 이름, IP 삭제 정책 등 환경에 맞는 옵션을 추가해야 한다. 정적 내부 IP를 영구 보존할지, 인스턴스가 영구 삭제될 때 주소 예약도 삭제할지는 운영 정책에 따라 결정한다. Stateful IP 주소 구성 문서\n6. 1대와 2대 구성의 차이 # VM 1대 # VM 1대가 서비스의 주 인스턴스이고, 장애가 발생하면 같은 내부 IP로 재생성되면 되는 경우에 적합하다.\n내부 클라이언트 -\u0026gt; 고정 내부 IP -\u0026gt; Stateful MIG VM 1대 이 구조는 로드 밸런서 없이도 VM 재생성 이후 접속 주소를 유지할 수 있다. 다만 로드 밸런서가 제공하는 요청 분산, L7 라우팅, 단일 진입점 고가용성을 대신 제공하지는 않는다.\nVM 2대 # VM 2대를 운영하면 각 VM에 고유한 Stateful 내부 IP가 필요하다.\n내부 클라이언트 -\u0026gt; VM A의 고정 IP -\u0026gt; VM B의 고정 IP 두 VM을 구성한다고 해서 자동으로 하나의 가상 IP가 만들어지거나 트래픽이 자동 분산되는 것은 아니다. 클라이언트가 두 주소를 모두 알고 있어야 하며, 분산이나 장애 전환은 애플리케이션, 클라이언트, DNS 또는 별도의 네트워크 구성에서 처리해야 한다.\n따라서 “VM 2대의 고정 IP”와 “단일 진입점으로 동작하는 로드 밸런서”는 서로 다른 요구사항이다. 하나의 주소로 안정적인 분산과 장애 전환이 필요하다면 로드 밸런서를 제거하기보다, 비용과 기능을 비교해 적절한 로드 밸런서 유형을 선택해야 한다.\n7. Stateful MIG 사용 시 주의사항 # Stateful MIG는 비용을 줄일 수 있는 만능 대체제가 아니다.\n첫째, Stateful IP는 주소 보존 기능이지 로드 밸런싱 기능이 아니다. IP가 유지되더라도 요청을 여러 VM으로 분배하거나, 여러 VM 중 정상 인스턴스를 하나의 주소로 선택해 주지는 않는다.\n둘째, Stateful MIG에서는 자동 확장을 사용할 수 없다. 인스턴스별 IP와 상태를 보존해야 하기 때문에 인스턴스 수를 자유롭게 늘리고 줄이는 Stateless MIG와 운영 방식이 다르다. Stateful MIG 구성 제한\n셋째, 리전 MIG에서 Stateful 구성을 사용할 때는 자동 재분배 정책을 신중하게 설정해야 한다. Google Cloud 문서에서는 Stateful 리전 MIG의 경우 자동 영역 재분배로 Stateful 인스턴스가 삭제되지 않도록 재분배 유형을 NONE으로 설정하는 내용을 안내한다.\n넷째, VM 내부에 애플리케이션 상태나 로컬 데이터를 저장한다면 IP만 보존해서는 충분하지 않다. 디스크, 데이터 복제, 백업, 장애 복구 절차까지 함께 설계해야 한다.\n8. 선택 기준 정리 # 요구사항 적합한 구성 VM 한 대에 고정된 내부 주소로 접근 Stateful MIG + Stateful 내부 IP VM 두 대를 각각 고정 주소로 운영 Stateful MIG 2대 + 애플리케이션 또는 클라이언트 측 제어 하나의 주소로 여러 백엔드에 분산 로드 밸런서 URL 경로 기반 분기 Envoy 기반 애플리케이션 로드 밸런서 트래픽이 적고 L7 기능이 필요 없음 Stateful MIG 또는 요구사항에 맞는 단순한 L4 구성 검토 요청량이 크고 자동 확장이 필요함 로드 밸런서 + Stateless MIG/NEG 마무리 # 고정 접속 주소만 필요한 서비스라면 Stateful MIG의 IP 보존을 검토할 수 있다. 하지만 하나의 주소로 여러 백엔드에 요청을 분산하거나 장애 시 정상 백엔드로 전환해야 한다면, IP 보존만으로는 부족하다.\n로드 밸런서를 없애서 절약하는 비용과, 함께 없어지는 기능을 같이 봐야 한다. 최소 프록시 요금은 로드 밸런서가 제공하는 기능이 필요한지 판단한 뒤 비교할 비용이다.\n","date":"2026년 9월 23일","externalUrl":null,"permalink":"/posts/gcp-internal-load-balancer-stateful-mig/","section":"글 목록","summary":"","title":"GCP 내부 애플리케이션 로드 밸런서 비용과 Stateful MIG 대안","type":"posts"},{"content":"Google Cloud와 AWS 사이에 VPN을 구성하면서, AWS VPN 연결 하나가 터널 두 개를 제공한다는 점을 기준으로 양쪽 리소스를 맞췄다. 이번 테스트에서는 AWS VPN 연결 두 개를 만들고 GCP HA VPN의 두 인터페이스에 총 네 개의 터널을 연결했다.\nHA VPN Gateway, Cloud Router, AWS Virtual Private Gateway(VGW), Customer Gateway를 만드는 순서와 외부 IP·BGP 주소의 대응 관계를 정리했다. 캡처는 현재 삭제된 테스트 환경의 기록이며, 실제 서비스 통신과 장애 전환 테스트는 별도 확인 항목으로 다룬다.\n1. 최종 구성 # 이번 예제의 구성은 다음과 같다.\n항목 Google Cloud AWS 네트워크 gateway-vpc test-vpc 네트워크 CIDR 10.1.1.0/24 10.2.2.0/24 리전 asia-northeast3 us-east-1 VPN Gateway HA VPN Gateway Virtual Private Gateway 라우팅 Cloud Router + BGP 동적 라우팅 + BGP ASN 65001 65002 VPN 연결 수 - 2개 VPN 터널 수 4개 4개 두 VPC의 CIDR은 서로 겹치지 않아야 한다. 실제 환경에서는 기존 VPC, 서브넷, 온프레미스 네트워크의 주소 대역까지 함께 확인한다.\nGoogle Cloud와 AWS의 공식 연동 방식에서도 AWS의 두 Site-to-Site VPN 연결에서 네 개의 외부 IP를 구성하고, Google Cloud의 HA VPN 인터페이스와 연결하는 구조를 사용한다. Google Cloud의 AWS HA VPN 연결 가이드\nHA VPN을 사용하는 이유 # Google Cloud HA VPN은 동적 라우팅(BGP)을 사용하는 고가용성 VPN 구성에 적합하다. Classic VPN은 현재 BGP 동적 라우팅을 사용하는 새 구성을 지원하지 않으므로, 이 글처럼 Cloud Router와 BGP를 사용하는 경우에는 HA VPN을 선택해야 한다. Classic VPN 동적 라우팅 지원 중단 안내\nGoogle Cloud 측 HA VPN의 99.99% SLA를 충족하려면 두 인터페이스 모두에 터널이 구성되어야 한다. AWS와 연결할 때는 AWS의 두 VPN 연결에서 제공하는 네 개의 터널을 사용해 양쪽 인터페이스의 장애에 대비한다.\n2. 사전에 확인할 내용 # 이 글의 캡처는 삭제된 테스트 환경에서 생성한 것이다. 캡처에 표시되는 프로젝트·VPC·VPN 리소스 정보와 IP 주소는 현재 사용 중인 구성이 아니며, 그대로 복사하지 않고 각자의 환경에서 새로 확인해야 한다.\n캡처에 보이는 공인 IP는 Google Cloud와 AWS가 VPN 외부 터널에 할당한 테스트 시점의 주소다. 공인 IP 대역과 외부 터널 주소의 사용 방식은 각 클라우드의 공식 문서에도 공개되어 있지만, 실제 구성에서는 고정된 값으로 가정하지 말고 각 콘솔에서 현재 할당된 주소를 확인한다.\n사전 공유 키도 테스트 구성에서 임시로 사용한 값이다. 실제 운영 환경에서는 다음 기준을 적용한다.\n터널마다 서로 다른 강한 사전 공유 키를 사용한다. AWS가 자동 생성한 키를 사용하거나 안전한 비밀 저장소에 보관한다. 키를 소스 코드, 문서, 화면 캡처에 기록하지 않는다. AWS Site-to-Site VPN은 PSK를 자동 생성할 수 있고, AWS Secrets Manager를 이용해 보관할 수도 있다. AWS Site-to-Site VPN 터널 인증 방식\n또한 다음 항목을 준비한다.\nGoogle Cloud VPC와 AWS VPC 서로 겹치지 않는 양쪽 네트워크 CIDR Google Cloud에서 VPN을 생성할 권한 AWS에서 VGW, Customer Gateway, Site-to-Site VPN을 생성할 권한 양쪽의 방화벽, Security Group, 네트워크 ACL 설정 계획 테스트할 Compute Engine VM과 EC2 인스턴스의 사설 IP VPN 터널이 생성됐더라도 방화벽이나 라우팅 테이블이 올바르지 않으면 실제 애플리케이션 통신은 되지 않는다. VPN 생성과 통신 검증은 별도의 단계로 생각하는 것이 좋다.\n3. Google Cloud Cloud Router 생성 # Google Cloud 콘솔의 하이브리드 연결 메뉴에서 Cloud Router를 생성한다.\nCloud Router 생성 화면에서 이름, VPC 네트워크, 리전을 입력한다. 이 예제에서는 gateway-vpc, asia-northeast3, ASN 65001을 사용했다.\nCloud Router는 VPN 터널 자체가 아니라 BGP를 통해 네트워크 경로를 교환하는 역할을 한다. HA VPN 터널을 생성할 때 이 Cloud Router를 연결하게 된다.\n4. Google Cloud HA VPN Gateway 생성 # Google Cloud의 VPN 메뉴에서 VPN 연결 생성을 시작한다.\nVPN 종류에서는 고가용성(HA) VPN을 선택한다.\nHA VPN Gateway 생성 화면에서 이름, VPC 네트워크, 리전을 지정한다. 이 예제에서는 gw-to-aws라는 이름과 gateway-vpc, asia-northeast3을 사용했다.\nHA VPN Gateway가 생성되면 Google Cloud가 두 개의 외부 IP를 자동으로 할당한다. 이 IP는 AWS의 Customer Gateway를 만들 때 사용하므로 기록해 둔다.\nGoogle Cloud HA VPN Gateway의 두 IP를 AWS의 Customer Gateway 두 개에 각각 연결할 예정이다.\n5. AWS Virtual Private Gateway 생성 및 VPC 연결 # AWS VPC 콘솔의 가상 사설 네트워크 메뉴에서 Virtual Private Gateway를 선택한다.\nVirtual Private Gateway를 생성하면서 이름과 ASN을 지정한다. 이 예제에서는 AWS ASN을 65002로 설정했다.\n생성된 VGW를 대상 VPC에 연결한다.\n이번 글은 AWS Virtual Private Gateway 기준이다. Transit Gateway를 사용하는 구성은 라우팅 테이블과 VPN 연결 방식이 달라지므로, 같은 절차로 간주하면 안 된다. AWS와 Google Cloud의 연동에서 Transit Gateway를 사용하면 ECMP를 활용할 수 있지만, 이 글의 구성은 VGW를 사용한다.\n6. AWS Customer Gateway 생성 # Customer Gateway는 AWS 외부에 있는 VPN 장치를 AWS에 등록하는 리소스다. 여기서는 Google Cloud HA VPN Gateway의 외부 IP를 Customer Gateway의 IP 주소로 사용한다.\nCustomer Gateway를 생성할 때 다음 항목을 입력한다.\n라우팅 옵션: Dynamic BGP ASN: Google Cloud Cloud Router의 ASN인 65001 IP 주소: Google Cloud HA VPN Gateway 인터페이스 0의 외부 IP Google Cloud HA VPN Gateway의 두 번째 인터페이스도 사용해야 하므로 Customer Gateway를 하나 더 생성한다. 두 번째 Customer Gateway에는 HA VPN Gateway 인터페이스 1의 외부 IP를 입력한다.\nAWS Customer Gateway 두 개는 각각 Google Cloud HA VPN Gateway의 서로 다른 인터페이스를 가리킨다.\n7. AWS Site-to-Site VPN 연결 생성 # AWS VPC 콘솔에서 Site-to-Site VPN 연결 생성을 시작한다.\n첫 번째 VPN 연결에서는 다음 값을 지정한다.\n대상 게이트웨이 유형: Virtual Private Gateway 대상 게이트웨이: 앞에서 만든 VGW Customer Gateway: 첫 번째 Customer Gateway 라우팅 옵션: Dynamic AWS VPN 연결 하나에는 두 개의 터널이 생성된다. 터널 옵션에서 IKE 버전은 IKEv2를 선택하고, 사전 공유 키는 AWS가 자동 생성하거나 운영 환경에 맞는 강한 값을 사용한다.\nGoogle Cloud 공식 문서에서는 AWS와의 HA VPN 구성에서 IKEv2를 사용하고, AWS 측 변환 세트를 너무 많이 선택하지 않도록 안내한다. 기본 설정을 그대로 사용하기보다 양쪽에서 호환되는 Phase 1·Phase 2 암호화, 무결성, DH 그룹을 명시적으로 확인하는 것이 좋다.\n첫 번째 VPN 연결이 생성되면 두 터널의 외부 IP와 내부 터널 CIDR을 확인할 수 있다.\n이제 두 번째 VPN 연결을 만든다. 두 번째 연결에서는 첫 번째와 다른 Customer Gateway를 선택한다.\n두 번째 VPN 연결의 두 터널에도 각각 IKEv2와 사전 공유 키를 설정한다. 키 관리 기준은 앞의 사전 확인 항목과 같다.\n두 번째 VPN 연결까지 생성하면 AWS 쪽에는 총 네 개의 터널이 준비된다.\n이 시점에 두 VPN 연결의 외부 IP 네 개를 확보했다. 다음 단계에서는 이 값과 터널별 BGP 주소를 GCP에 입력한다.\n8. AWS VPN 구성 파일에서 BGP 정보 확인 # 각 AWS VPN 연결에서 구성 다운로드를 선택한다.\n구성 다운로드 화면에서 피어 장비에 맞는 공급 업체와 장치 정보를 선택한다. 다운로드된 구성 파일은 실제 장비에 그대로 적용하는 파일이 아니라, 터널 외부 IP, 내부 BGP 주소, IKE 설정, 사전 공유 키 등의 값을 확인하는 자료로도 사용할 수 있다. 이 글에서는 구성 파일을 실제 장비에 적용하지 않고 BGP 정보를 확인하는 데만 사용하므로, 해당 선택 화면은 생략한다.\n구성 파일에서 각 터널의 내부 BGP 주소를 확인한다.\n이번 테스트 구성에서 확인한 BGP 주소는 다음과 같다. 주소는 AWS가 VPN 연결을 만들 때 할당하므로, 다른 환경에서는 반드시 새로 내려받은 구성 파일의 값을 사용해야 한다.\nAWS VPN 연결 AWS 터널 AWS BGP 주소 Google Cloud BGP 주소 vpn-to-gcp 터널 1 169.254.132.201 169.254.132.202 vpn-to-gcp 터널 2 169.254.34.249 169.254.34.250 vpn-to-gcp-2 터널 1 169.254.201.9 169.254.201.10 vpn-to-gcp-2 터널 2 169.254.114.65 169.254.114.66 169.254.0.0/16 대역은 터널 내부 BGP 통신에 사용되는 링크 로컬 주소다. 위 표의 주소를 그대로 재사용하지 말고, 각자 생성한 AWS VPN 구성 파일에서 확인한다.\n9. Google Cloud 외부 Peer VPN Gateway 생성 # Google Cloud VPN 터널 목록에서 VPN 터널 생성을 시작한다.\n먼저 생성한 HA VPN Gateway를 선택한다.\n피어 VPN Gateway에서 외부에 있는 피어 게이트웨이를 선택한다.\nAWS VPN 연결 두 개에서 생성된 네 개의 터널 외부 IP를 하나의 외부 Peer VPN Gateway에 네 개의 인터페이스로 등록한다.\nPeer interface 0: AWS VPN 연결 1의 터널 1 외부 IP Peer interface 1: AWS VPN 연결 1의 터널 2 외부 IP Peer interface 2: AWS VPN 연결 2의 터널 1 외부 IP Peer interface 3: AWS VPN 연결 2의 터널 2 외부 IP 외부 Peer VPN Gateway를 만든 뒤 HA VPN Gateway와 연결할 터널 수로 VPN 터널 4개 만들기를 선택한다.\nGoogle Cloud 공식 구성은 다음과 같이 두 HA VPN 인터페이스와 AWS 외부 인터페이스를 연결한다.\nGoogle Cloud HA VPN 인터페이스 AWS Peer 인터페이스 인터페이스 0 0, 1 인터페이스 1 2, 3 AWS 외부 IP와 인터페이스 번호를 잘못 연결하면 터널 상태가 올라오지 않을 수 있다. 외부 IP를 만든 뒤에는 주소를 변경할 수 없으므로, AWS에서 내려받은 구성 파일과 대조한 후 입력한다.\n10. 네 개의 VPN 터널 구성 # 네 개의 터널에 각각 다음 정보를 입력한다.\nGoogle Cloud HA VPN 인터페이스 AWS Peer VPN Gateway 인터페이스 IKE 버전 IKEv2 AWS와 동일한 사전 공유 키 각 터널의 키는 대응하는 AWS 터널의 키와 같아야 한다. 캡처의 임시 값 대신 자신의 구성에서 발급한 키를 입력한다.\n11. Cloud Router에 BGP 세션 구성 # VPN 터널을 만든 뒤 Cloud Router에서 BGP 세션을 구성한다.\n각 터널마다 BGP 세션을 하나씩 만들고 다음 값을 입력한다.\n피어 ASN: AWS VGW의 ASN 65002 Cloud Router BGP IPv4 주소: Google Cloud 쪽 BGP 주소 BGP 피어 IPv4 주소: AWS 쪽 BGP 주소 터널: 해당 VPN 터널 앞에서 확인한 BGP 주소 표의 Google Cloud 주소와 AWS 주소를 서로 반대 방향으로 입력하지 않도록 주의한다. Google Cloud 주소는 로컬 주소이고, AWS 주소는 피어 주소다.\nBGP 세션 구성을 저장하면 네 개의 터널과 BGP 세션 상태를 요약 화면에서 확인할 수 있다.\n12. AWS 라우팅 전파 활성화 # 마지막으로 AWS VPC에서 실제 워크로드가 사용하는 라우팅 테이블을 확인한다. 해당 라우팅 테이블에서 VGW의 라우팅 전파를 활성화하면 BGP를 통해 학습한 원격 네트워크 경로가 자동으로 반영된다.\nAWS 라우팅 전파를 활성화하는 것만으로 방화벽이 열리는 것은 아니다. 다음 항목도 함께 확인해야 한다.\nAWS 라우팅 테이블에 Google Cloud VPC CIDR이 전파됐는지 확인 Google Cloud VPC의 유효 경로에 AWS VPC CIDR이 표시되는지 확인 Google Cloud 방화벽에서 AWS VPC 대역의 필요한 인바운드 트래픽 허용 AWS Security Group과 Network ACL에서 Google Cloud VPC 대역 허용 운영체제 방화벽이 ICMP 또는 애플리케이션 포트를 차단하지 않는지 확인 AWS 콘솔에서 VGW 라우팅 전파를 활성화하는 방법은 AWS VPC 라우팅 테이블 문서에서 확인할 수 있다.\n13. 연결 상태 확인 # AWS VPN 연결 상세 화면에서 두 VPN 연결과 네 개 터널의 상태가 Up 또는 Available인지 확인한다.\nGoogle Cloud에서도 네 개의 VPN 터널과 BGP 세션이 정상 상태인지 확인한다.\n상태가 정상으로 보이더라도 실제 사설 IP 통신을 테스트해야 한다. 양쪽 VPC에 테스트 VM이 있다면 다음과 같이 테스트할 수 있다.\n# Google Cloud VM에서 AWS EC2의 사설 IP로 테스트 ping -c 4 \u0026lt;AWS_EC2_PRIVATE_IP\u0026gt; # AWS EC2에서 Google Cloud VM의 사설 IP로 테스트 ping -c 4 \u0026lt;GCP_VM_PRIVATE_IP\u0026gt; ICMP를 허용하지 않는 환경이라면 실제 서비스 포트를 확인한다.\nnc -vz \u0026lt;REMOTE_PRIVATE_IP\u0026gt; \u0026lt;PORT\u0026gt; Google Cloud 공식 문서도 VPN Gateway의 외부 IP를 ping하는 것은 터널을 통한 통신 테스트가 아니며, 양쪽 네트워크의 실제 시스템 사설 IP를 이용해 확인해야 한다고 안내한다. Cloud VPN 문제 해결 문서\n추가로 고가용성을 검증하려면, 테스트 환경에서 터널 하나를 일시적으로 중단한 뒤 통신이 계속되는지 확인한다. 테스트가 끝나면 원래 상태로 복구한다. 이 글의 캡처에는 실제 사설 IP 통신과 장애 전환 테스트 결과는 포함되어 있지 않다.\n14. 문제가 발생할 때 확인할 순서 # VPN 터널이 올라오지 않는 경우 # AWS 외부 IP와 Google Cloud Peer 인터페이스 매핑이 정확한지 확인한다. 양쪽 IKE 버전이 모두 IKEv2인지 확인한다. 양쪽 PSK가 일치하는지 확인한다. AWS와 Google Cloud의 Phase 1·Phase 2 암호화 설정이 호환되는지 확인한다. UDP 500, UDP 4500, IPsec 관련 트래픽이 차단되지 않는지 확인한다. 터널은 올라왔지만 BGP가 Established가 되지 않는 경우 # Google Cloud ASN과 AWS ASN이 서로 올바르게 입력됐는지 확인한다. 로컬 BGP 주소와 피어 BGP 주소를 반대로 입력하지 않았는지 확인한다. 각 주소가 해당 터널의 /30 대역에 포함되는지 확인한다. BGP 통신에 사용되는 TCP 179가 필요한 경로에서 허용되는지 확인한다. BGP는 정상인데 실제 통신이 되지 않는 경우 # AWS 라우팅 테이블에서 VGW 라우팅 전파가 활성화됐는지 확인한다. Google Cloud Cloud Router가 반대편 네트워크 CIDR을 광고하고 있는지 확인한다. Google Cloud 방화벽과 AWS Security Group·NACL을 확인한다. 테스트 대상 VM의 운영체제 방화벽과 애플리케이션 포트를 확인한다. 양쪽 네트워크의 CIDR이 겹치지 않는지 다시 확인한다. Google Cloud는 HA VPN에서 피어 라우터가 광고한 목적지로만 트래픽을 전달하므로, 터널 상태만 확인하지 말고 BGP 학습 경로와 광고 경로도 함께 확인해야 한다. Cloud VPN 방화벽 구성 문서\n15. 테스트 리소스 정리 # VPN 연결은 사용 시간과 데이터 전송량에 따라 비용이 발생할 수 있다. 테스트가 끝났다면 다음 리소스를 확인하고 삭제한다.\nAWS Site-to-Site VPN 연결 2개 AWS Customer Gateway 2개 AWS Virtual Private Gateway Google Cloud VPN 터널 4개 Google Cloud 외부 Peer VPN Gateway Google Cloud HA VPN Gateway Cloud Router 테스트용 VPC, 서브넷, VM 리소스를 삭제하기 전에 다른 서비스가 해당 네트워크를 사용하고 있지 않은지 확인한다. 운영 환경의 리소스를 테스트 리소스와 혼동하지 않도록 이름과 라벨을 분리하는 것도 중요하다.\n마무리 # Google Cloud HA VPN과 AWS Virtual Private Gateway를 연결하려면 한쪽의 VPN Gateway만 만드는 것으로 끝나지 않는다. 양쪽에 Customer Gateway와 VPN 연결을 구성하고, AWS에서 생성된 네 개의 외부 터널 IP와 내부 BGP 주소를 Google Cloud에 정확히 반영해야 한다.\n캡처에 기록한 구성 순서는 다음과 같다.\nCloud Router 생성 ↓ Google Cloud HA VPN Gateway 생성 ↓ AWS Virtual Private Gateway와 VPC 연결 ↓ AWS Customer Gateway 2개 생성 ↓ AWS Site-to-Site VPN 연결 2개 생성 ↓ AWS 구성 파일에서 외부 IP와 BGP 주소 확인 ↓ Google Cloud 외부 Peer VPN Gateway 생성 ↓ VPN 터널 4개 생성 ↓ Cloud Router BGP 세션 4개 구성 ↓ AWS 라우팅 전파 활성화 여기까지는 터널과 라우팅 설정의 기록이다. 운영에 적용하기 전에는 사설 IP 통신과 터널 장애 시의 동작까지 별도로 검증해야 한다. 테스트 캡처의 IP, 리소스 ID, 사전 공유 키는 재사용하지 않는다.\n","date":"2026년 9월 23일","externalUrl":null,"permalink":"/posts/gcp-aws-ha-vpn/","section":"글 목록","summary":"","title":"Google Cloud HA VPN과 AWS Site-to-Site VPN 연결하기","type":"posts"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/google-cloud/","section":"Tags","summary":"","title":"Google-Cloud","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/ha-vpn/","section":"Tags","summary":"","title":"Ha-Vpn","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/interconnect/","section":"Tags","summary":"","title":"Interconnect","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/internal-load-balancer/","section":"Tags","summary":"","title":"Internal-Load-Balancer","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/load-balancer/","section":"Tags","summary":"","title":"Load-Balancer","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/networking/","section":"Tags","summary":"","title":"Networking","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/site-to-site-vpn/","section":"Tags","summary":"","title":"Site-to-Site-Vpn","type":"tags"},{"content":"","date":"2026년 9월 23일","externalUrl":null,"permalink":"/tags/stateful-mig/","section":"Tags","summary":"","title":"Stateful-Mig","type":"tags"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/android/","section":"Tags","summary":"","title":"Android","type":"tags"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/android-studio/","section":"Tags","summary":"","title":"Android-Studio","type":"tags"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/codex-cli/","section":"Tags","summary":"","title":"Codex-Cli","type":"tags"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/dayping/","section":"Tags","summary":"","title":"Dayping","type":"tags"},{"content":"이번 글부터 실제로 사용할 리마인더 앱 DayPing을 만들기 시작한다. DayPing은 날짜와 시간을 지정하면 잊지 않도록 알려주는 일정 알림 앱이다.\nAndroid Studio와 Codex CLI를 준비하고 요구사항을 전달한 뒤, 약 17분 만에 초기 실행 버전이 만들어졌다. 이후 에뮬레이터에서 일정을 등록하고 지정한 시간에 알림이 나타나는 것까지 확인했다. 이번 1편은 그 과정을 기록한다.\n1. DayPing 앱 소개 # DayPing은 간단한 일정 관리와 알림 기능을 결합한 리마인더 앱이다.\n첫 번째 구현에서는 다음 기능을 포함한다.\n일정 추가 날짜와 시간 지정 반복 설정 정시·5분 전·10분 전·1시간 전·하루 전 알림 설정 일정 메모 로컬 데이터 저장 Android 13 이상 알림 권한 처리 지정 시간 알림 처음부터 모든 기능을 직접 설계하고 구현하기보다는, 요구사항을 정리한 뒤 Codex CLI에게 구현을 요청하고 생성된 코드를 직접 검토하는 방식으로 진행했다. 이번 글에서는 캡처로 확인할 수 있는 기본 일정 등록과 알림 흐름에 집중한다.\n2. 개발 환경 # 이번 글의 캡처는 WSL이 아닌 Windows 환경에서 진행했다.\n항목 캡처 기준 운영체제 Windows Node.js v24.19.0 npm 11.17.0 Android Studio Quail 4 (2026.1.4) 앱 이름 DayPing 패키지명 com.jinosoft.dayping UI Jetpack Compose 개발 도구 OpenAI Codex CLI 테스트 기기 Android Emulator Medium Phone 시스템 이미지 Android 16, API 36.1 버전과 화면은 캡처 시점의 값이다. Android Studio, Android SDK, Node.js와 Codex CLI는 계속 업데이트되므로 설치 시점에는 공식 문서의 최신 버전을 함께 확인한다.\n3. Node.js 설치와 확인 # Codex CLI를 npm으로 설치하기 위해 Node.js가 필요하다. Windows 터미널 또는 명령 프롬프트에서 다음 명령으로 Node.js LTS를 설치한다.\nwinget install OpenJS.NodeJS.LTS 설치가 끝나면 새 터미널을 열고 Node.js와 npm 버전을 확인한다.\nnode -v npm -v 이 글의 캡처에서는 Node.js v24.19.0, npm 11.17.0이 확인됐다. 버전이 출력되지 않으면 터미널을 다시 열거나 PATH 설정을 확인한다.\n4. Codex CLI 설치 # Codex CLI는 터미널에서 프로젝트 파일을 확인하고, 코드를 수정하고, 명령어를 실행할 수 있는 개발 도구다. 공식 문서에서는 프로젝트 디렉터리에서 codex를 실행해 작업을 시작하는 흐름을 안내한다. Codex CLI 공식 문서\nnpm으로 Codex CLI를 설치한다.\nnpm install -g @openai/codex 설치 결과를 확인한다.\ncodex --version 캡처에서는 codex-cli 0.155.1이 출력됐다. 이후 Codex CLI는 프로젝트 디렉터리 안에서 실행해야 프로젝트 파일을 읽고 수정할 수 있다.\n5. Android Studio 설치 # Android Studio는 Android 앱을 개발하고 빌드하는 공식 통합 개발 환경이다. Android Studio 공식 설치 페이지에서 Windows용 설치 파일을 내려받는다.\n설치할 때 Android SDK, Android SDK Platform, Android Virtual Device 항목을 함께 선택한다. 설치가 끝나면 Android Studio를 실행해 프로젝트를 생성할 준비를 한다.\n6. DayPing 프로젝트 생성 # Android Studio를 처음 실행하면 Welcome 화면이 표시된다. New Project를 선택해 새 프로젝트를 만든다.\n프로젝트 템플릿에서는 Phone and Large Screens의 Empty Activity를 선택한다. Empty Activity는 Jetpack Compose를 사용하는 가장 단순한 시작점이다.\n프로젝트 설정은 다음처럼 입력했다.\n항목 값 Name DayPing Package name com.jinosoft.dayping Minimum SDK API 24 (Android 7.0) Build configuration language Kotlin DSL UI toolkit Jetpack Compose 패키지명은 jinosoft를 사용해 com.jinosoft.dayping으로 정했다. 자신의 앱을 만들 때는 본인 도메인이나 조직 이름을 반영한 고유한 값을 사용한다. Google Play에 배포할 애플리케이션 ID도 확인해야 하므로 처음부터 신중하게 정하는 편이 좋다.\nMinimum SDK는 지원할 최소 Android 버전이다. 이번에는 API 24를 선택했다.\n프로젝트가 생성되면 Android Studio에서 Kotlin 코드와 Gradle 프로젝트 구조를 확인한다.\n7. Android Emulator 만들기 # 실제 Android 기기 없이 앱을 실행하려면 Android Emulator가 필요하다. Android Studio의 Device Manager를 열고 가상 기기를 추가한다.\nPhone을 선택한 뒤 사용할 기기 프로필을 고른다. 이번에는 Medium Phone을 선택했다.\n시스템 이미지 선택 # 시스템 이미지는 Android 16, API 36.1의 x86_64 이미지를 선택했다. 나중에 Play 관련 기능도 테스트할 수 있도록 Google Play가 포함된 이미지를 사용했다.\n현재 Google Play에 새 앱과 업데이트를 제출하려면 일정 수준 이상의 target API가 필요하다. 이 요구사항은 변경될 수 있으므로 배포 시점에는 Google Play 대상 API 수준 요구사항을 확인한다.\n에뮬레이터의 OS 버전과 앱의 target API 설정은 별개다. 이번에는 API 36 계열 기기로 먼저 실행을 확인했으며, 배포 전에는 다른 지원 버전에서도 테스트해야 한다.\n에뮬레이터 메모리 설정 # Additional settings에서는 에뮬레이터의 저장 공간과 메모리를 조정할 수 있다. 캡처에서는 RAM을 4GB로 설정했다.\n개발 PC의 메모리 여유를 고려한 값이지 모든 PC에 필요한 설정은 아니다. 에뮬레이터가 느리거나 실행되지 않으면 호스트의 메모리 여유, 그래픽 가속과 시스템 이미지 종류를 함께 확인한다.\n8. Codex CLI로 DayPing 개발 시작하기 # 프로젝트가 생성된 디렉터리로 이동한 뒤 Codex CLI를 실행한다.\ncd C:\\Users\\zinod\\AndroidStudioProjects\\DayPing codex 처음 실행하면 현재 디렉터리의 파일을 신뢰할 것인지 묻는다. 경로가 내가 만든 DayPing 프로젝트인지 확인한 뒤 계속 진행한다.\n신뢰할 수 없는 폴더에서 실행하면 프로젝트 설정이나 스크립트가 의도하지 않게 실행될 수 있으므로, 경로를 확인하지 않고 승인하지 않는다.\n모델 선택 # Codex CLI에서 /model 명령으로 모델과 추론 수준을 선택할 수 있다.\n/model 이번 작업에는 gpt-5.6-luna를 선택했다. 모델과 추론 수준, 사용량 한도는 계정과 시점에 따라 달라지므로 같은 이름을 찾기보다 현재 선택 화면의 모델과 사용량 안내를 확인한다.\n9. Codex에 앱 요구사항 전달하기 # Codex에게 먼저 앱의 이름, 패키지명, 목적과 핵심 기능을 전달했다.\nandroid app을 개발하려고 해. 앱 이름 : DayPing 패키지명: com.jinosoft.dayping 앱 설명: 날짜와 시간을 지정하고 일정을 입력하면, 잊지 않도록 알려주는 리마인더 앱 기능과 UI를 추천해줘. Codex는 초기 기능 후보로 일정 추가·수정·삭제, 날짜와 시간 지정, 반복 일정, 알림, 로컬 저장, 재부팅 후 알림 복원 등을 제안했다.\n초기 버전에서 너무 많은 기능을 한꺼번에 넣으면 검토와 테스트가 어려워진다. 이번 구현에서는 일정 추가, 날짜·시간 선택, 목록 표시, 로컬 저장, 알림 동작 확인을 우선 목표로 삼았다.\n요구사항을 확인한 뒤 개발을 요청했다.\n좋아. 개발해줘. Codex가 파일을 수정하거나 Gradle 명령을 실행하려고 할 때는 실행할 명령과 이유를 확인한 뒤 승인한다. 캡처에서는 Java 경로와 Gradle 프로젝트 경로를 지정해 assembleDebug를 실행하려는 내용을 확인할 수 있다.\nCodex 명령 승인\nCodex가 명령 실행을 물어보면 먼저 명령과 실행 경로를 확인한다. 내용이 안전하고 이번 작업에 필요한 명령이라면 1. Yes, proceed를 선택하고, 같은 유형의 명령을 계속 허용해도 되는 상황이면 2. Yes, and don't ask again...을 선택할 수 있다. 명령이 이해되지 않거나 예상과 다르면 3을 선택해 중단하고 Codex에 수정 내용을 전달한다.\nAI가 생성한 코드라도 그대로 배포하지 않고, 변경된 파일과 빌드 결과를 직접 확인하는 과정이 필요하다.\n10. 빌드 결과 확인 # Codex가 기능 구현을 마친 뒤 다음 결과를 확인했다.\nassembleDebug 성공 테스트 성공 디버그 APK 생성 주요 Kotlin 파일과 알림 관련 파일 확인 캡처의 작업 시간에는 Codex CLI가 요구사항을 확인하고 코드를 작성한 뒤, Gradle 빌드와 테스트를 실행한 시간이 포함되어 있다. 전체 작업에는 약 17분(16분 54초)이 걸렸고, 이 과정에서 DayPing의 초기 실행 버전이 만들어졌다.\n약 17분 만에 만들어진 것은 배포용 완성본이 아니라, 실행하고 테스트할 수 있는 초기 버전이다. 코드 검토와 실제 기기·지원 Android 버전별 테스트는 별도로 필요하다.\n캡처에 표시된 주요 파일은 다음과 같다.\napp/src/main/java/com/jinosoft/dayping/MainActivity.kt app/src/main/java/com/jinosoft/dayping/data/Reminder.kt app/src/main/java/com/jinosoft/dayping/data/ReminderRepository.kt app/src/main/java/com/jinosoft/dayping/notifications/ReminderReceiver.kt app/src/main/AndroidManifest.xml 파일 목록만으로 구현이 올바르다고 판단하지 말고, 실제 앱을 실행해 화면과 알림을 확인한다.\n11. Android Studio에서 앱 실행 # Android Studio 상단의 실행 기기에서 Medium Phone 에뮬레이터를 선택하고 Run 'app'을 클릭한다.\nRun 버튼이 비활성화된 경우\nRun 'app' 버튼이 비활성화되어 있다면 Gradle 동기화가 끝났는지 확인한다. Android Studio 상단의 Sync Project with Gradle Files 버튼을 클릭해 프로젝트를 다시 동기화한 뒤 실행한다. 동기화 오류가 표시되면 JDK, Android SDK, 네트워크 연결과 Gradle 오류 메시지를 순서대로 확인한다.\nAndroid 13 이상에서는 알림을 보내기 전에 사용자 권한을 요청해야 한다. DayPing을 처음 실행하면 알림 권한 요청이 표시된다.\n알림을 확인하려면 Allow를 선택한다. 권한을 거부한 경우에는 Android 설정에서 앱의 알림 권한을 다시 허용해야 할 수 있다.\n12. DayPing 화면 확인 # 앱이 실행되면 오늘의 리마인더와 일정 추가 버튼이 표시된다.\n일정 추가를 누르면 제목, 날짜, 시간, 반복 주기, 알림 시점, 메모를 입력할 수 있다.\n예제에서는 약먹기라는 일정을 입력하고 평일 오후 3시에 알림이 오도록 설정했다.\n저장한 일정은 오늘의 목록에 표시된다.\n13. 지정 시간 알림 확인 # 테스트를 위해 알림 시점을 가까운 시간으로 설정한 뒤 에뮬레이터를 기다렸다. 지정한 시간이 되자 Android 알림 영역에 DayPing 알림이 표시됐다.\n이 과정을 통해 단순히 화면이 표시되는 것뿐 아니라 다음 흐름이 실제로 동작하는 것을 확인했다.\n리마인더 입력 ↓ 로컬 저장 ↓ 알람 예약 ↓ 지정 시간 도달 ↓ Android 알림 표시 14. 확인한 내용과 남은 과제 # 이번 글에서는 다음을 완료했다.\nWindows에 Node.js와 npm 설치 Codex CLI 설치와 실행 Android Studio 설치 DayPing Compose 프로젝트 생성 Android Emulator 생성 Codex CLI로 앱 기능 구현 Gradle debug 빌드 성공 에뮬레이터에서 DayPing 실행 알림 권한 허용 지정 시간 알림 확인 아직 배포 전 단계이므로 다음 작업이 남아 있다.\n기능별 오류 처리 보완 다양한 Android 버전에서 테스트 앱 아이콘 제작 릴리스 서명 키 생성 Android App Bundle 생성 개인정보처리방침과 데이터 보안 정보 정리 Google Play Console 등록과 심사 제출 마무리 # 앱 이름과 핵심 요구사항을 전달하는 것으로 시작해, 에뮬레이터에 실제 알림이 뜨는 것까지 확인했다. 빌드 완료 메시지만 보는 것과 직접 일정을 등록하고 알림을 받는 것은 다르므로, 생성된 앱은 실행해서 확인하는 과정이 필요하다.\nGoogle Play 배포 과정은 DayPing의 기능과 테스트를 더 보완한 뒤 별도 글로 다룰 예정이다.\n","date":"2026년 9월 21일","externalUrl":null,"permalink":"/posts/dayping-android-studio-codex-setup/","section":"글 목록","summary":"","title":"DayPing 개발기 1 - Windows에서 Android Studio와 Codex CLI로 앱 개발하기","type":"posts"},{"content":"GCP 콘솔에서 디스크를 50GB에서 100GB로 늘렸지만, VM의 pvs에는 여전히 50GB로 표시됐다. 디스크 확장과 LVM이 사용할 수 있는 공간의 확장은 별도 작업이기 때문이다.\n이 실습에서는 Compute Engine VM에 Persistent Disk 3개를 연결해 LVM을 구성하고, 디스크를 늘린 뒤 PV, LV, XFS 파일시스템까지 확장했다. 마지막에는 확장 전에 만든 파일이 유지되는지도 확인했다.\n일반적인 클라우드 환경에서는 디스크 하나를 파일시스템으로 포맷해 사용하는 것만으로 충분한 경우가 많다. 하지만 기존 LVM 기반 서버를 클라우드로 이전했거나 여러 디스크를 하나의 저장 공간으로 관리해야 한다면, 클라우드 콘솔에서 디스크 크기를 늘린 뒤에도 PV, LV, 파일시스템을 차례로 확장해야 한다.\n이 글은 Linux 디스크와 파일시스템의 기본 개념을 알고 있으며, Compute Engine VM 생성과 터미널 접속 경험이 있는 사용자를 대상으로 한다.\n1. 실습 환경과 구성 # 이번 실습 환경은 다음과 같다.\n항목 설정 클라우드 Google Cloud Compute Engine 운영체제 Rocky Linux 8 VM 이름 linux-lvm 부팅 디스크 20GB 추가 디스크 10GB, 10GB, 50GB 볼륨 그룹 vg1 논리 볼륨 lv1 최초 LV 크기 30GB 파일시스템 XFS 마운트 경로 /mnt 확장 대상 세 번째 추가 디스크 50GB → 100GB 전체 작업 흐름은 다음과 같다.\nPersistent Disk ↓ PV(Physical Volume) ↓ VG(Volume Group) ↓ LV(Logical Volume) ↓ XFS 파일시스템 ↓ 마운트 경로 용량을 확장할 때도 기반 디스크부터 위 계층으로 차례대로 넓힌다.\nGCP 디스크 확장 → PV 확장 → LV 확장 → XFS 확장 → 결과 확인 2. 작업 전 주의사항 # pvcreate, vgcreate, mkfs.xfs는 디스크 구조를 새로 만드는 명령이다. 사용 중인 디스크를 잘못 지정하면 기존 데이터를 잃을 수 있으므로, 명령을 실행하기 전에 대상 디스크와 백업 상태를 반드시 확인한다.\n주의\n이 글의 /dev/sdb, /dev/sdc, /dev/sdd는 실습 VM에서 확인된 장치명이다. 다른 VM에서는 장치명이 다를 수 있으므로 그대로 입력하면 안 된다.\nGCP Linux VM의 /dev/sdX 또는 /dev/nvmeXnY 장치명은 재부팅이나 디스크 재연결 후 달라질 수 있다. 운영 환경에서는 다음 명령으로 영구 장치명을 확인하고 /dev/disk/by-id/google-* 경로를 사용하는 것이 안전하다.\nls -l /dev/disk/by-id/google-* Google Cloud에서도 Linux VM에서 영구 장치 이름 사용을 권장한다.\nPersistent Disk는 크기를 늘릴 수 있지만 직접 축소할 수는 없다. 파일시스템이나 파티션을 변경하기 전에는 스냅샷 또는 별도 백업을 만드는 것이 좋다. 자세한 제한은 Persistent Disk 크기 변경 문서에서 확인할 수 있다.\n3. Compute Engine에 추가 디스크 연결 # Compute Engine VM 생성 화면에서 Rocky Linux 8 부팅 디스크를 선택하고, 고급 옵션의 디스크 항목에 추가 디스크 3개를 구성했다.\nlinux-lvm-disk1: 10GB linux-lvm-disk2: 10GB linux-lvm-disk3: 50GB VM을 생성한 뒤 SSH로 접속하고 관리자 권한을 얻는다.\nsudo -i lsblk로 부팅 디스크와 추가 디스크를 구분한다.\nlsblk 실습 환경에서는 추가 디스크가 /dev/sdb, /dev/sdc, /dev/sdd로 표시됐다. fdisk -l로 각 디스크의 전체 경로와 크기를 한 번 더 확인한다.\nfdisk -l 4. LVM 패키지 설치 # Rocky Linux에서 LVM 명령을 사용하려면 lvm2 패키지가 필요하다.\ndnf install -y lvm2 Rocky Linux 8에서는 yum install lvm2 -y를 사용해도 동일하게 설치할 수 있다. 실습 캡처에서는 yum을 사용했다.\n5. PV 구성 # PV(Physical Volume)는 LVM이 사용할 수 있도록 초기화한 물리 디스크 또는 파티션이다. 이 실습에서는 파티션을 별도로 만들지 않고 추가 디스크 전체를 PV로 사용한다.\n대상 디스크가 비어 있고 장치명이 정확한지 마지막으로 확인한 뒤 PV를 생성한다.\npvcreate /dev/sdb /dev/sdc /dev/sdd 다음 명령으로 생성된 PV와 크기를 확인한다.\npvdisplay pvs pvscan 10GB, 10GB, 50GB 디스크가 각각 PV로 생성됐고, 아직 VG에는 포함되지 않은 상태다.\n6. VG 구성 # VG(Volume Group)는 여러 PV를 하나의 저장 공간 풀로 묶는다. 세 개의 PV로 vg1을 생성한다.\nvgcreate vg1 /dev/sdb /dev/sdc /dev/sdd 이미 존재하는 VG에 새 PV를 추가할 때는 vgextend를 사용한다.\nvgextend vg1 /dev/sdd 이번 실습에서는 처음부터 세 디스크를 모두 지정해 vgcreate를 실행했으므로 vgextend를 추가로 실행하지 않는다.\nVG 상태와 전체 여유 공간을 확인한다.\nvgdisplay vg1 vgs vgscan 세 PV를 합친 vg1의 크기가 약 70GiB로 표시된다.\n7. LV와 XFS 파일시스템 생성 # LV(Logical Volume)는 VG의 공간을 할당받아 파일시스템이나 애플리케이션에서 사용할 수 있게 만든 논리적인 블록 장치다.\nvg1에서 30GB를 할당해 lv1을 생성한다.\nlvcreate -n lv1 -L 30G vg1 생성 결과를 확인한다.\nlvs lvdisplay /dev/vg1/lv1 lvscan lsblk에서는 /dev/sdd 아래에 vg1-lv1이 표시된다.\n다만 이 트리만 보고 LV의 모든 데이터가 /dev/sdd에만 할당됐다고 판단하면 안 된다. LV의 물리 영역은 VG에 포함된 여러 PV에 분산될 수 있다. 실제 할당 위치는 다음 명령으로 확인한다.\nlvs -o lv_name,vg_name,lv_size,devices 이제 LV에 XFS 파일시스템을 만들고 /mnt에 마운트한다.\nmkfs.xfs /dev/vg1/lv1 mount /dev/vg1/lv1 /mnt mkfs.xfs는 대상 장치의 기존 파일시스템과 데이터를 초기화한다. 새 LV에 한 번만 실행한다.\ndf -h에서 약 30GB 크기의 /dev/mapper/vg1-lv1이 /mnt에 마운트된 것을 확인한다.\ndf -h 용량 확장 후에도 데이터가 유지되는지 확인하기 위해 테스트 파일을 만든다.\necho \u0026#34;Hello Google Cloud \u0026amp; LVM!\u0026#34; \u0026gt; /mnt/hello.txt cat /mnt/hello.txt 8. GCP Persistent Disk 용량 확장 # Compute Engine의 VM 세부정보에서 추가 디스크 목록을 확인하고, 확장할 linux-lvm-disk3을 선택한다.\n디스크 세부정보 화면에서 현재 크기가 50GB인지 확인한 뒤 상단의 수정 버튼을 클릭한다.\n크기를 100GB로 변경하고 저장한다.\n디스크 확장은 실행 중인 VM에 연결된 상태에서도 가능하다. 다만 확장 후 게스트 운영체제의 PV와 파일시스템까지 자동으로 늘어나는 것은 아니다.\nVM 터미널에서 lsblk와 pvs를 비교한다.\nlsblk pvs lsblk에서는 /dev/sdd가 100GB로 보이지만, pvs의 /dev/sdd는 여전히 50GB다. 클라우드 디스크 확장은 완료됐지만 LVM의 PV가 새 공간을 아직 인식하지 못한 상태다.\n9. 확장된 공간을 PV에 반영 # 이번 실습에서는 작업 과정을 명확히 확인하기 위해 파일시스템을 마운트 해제하고 LV를 비활성화한 뒤 PV를 확장했다.\numount /mnt lvchange -an /dev/vg1/lv1 pvresize를 실행해 /dev/sdd의 늘어난 크기를 PV에 반영한다.\npvresize /dev/sdd pvs /dev/sdd의 PV 크기가 약 50GiB에서 100GiB로 늘었고, 이 PV에는 약 70GiB의 여유 공간이 표시된다. 여기에 /dev/sdb와 /dev/sdc의 여유 공간 약 20GiB까지 더하면 VG 전체 여유 공간은 약 90GiB다. 따라서 기존 30GiB LV에 VG의 여유 공간을 모두 추가하면 최종 크기는 약 120GiB가 된다.\nLV를 다시 활성화한다.\nlvchange -ay /dev/vg1/lv1 10. LV와 XFS 파일시스템 확장 # VG에 남은 모든 여유 공간을 lv1에 할당하고 파일시스템도 함께 확장한다.\n실습에서는 다음 명령을 사용했다.\nlvresize -r -l +100%FREE /dev/vg1/lv1 -l +100%FREE: VG의 남은 모든 Physical Extent를 추가한다. -r: LV와 파일시스템을 함께 확장한다. 캡처에서는 LV가 30GiB에서 약 119.99GiB로 확장됐고 XFS 파일시스템 크기도 함께 변경됐다.\n디스크는 50GB만 늘렸는데 LV는 30GiB에서 약 120GiB가 됐다. +100%FREE가 이번에 추가된 공간뿐 아니라 VG에 원래 남아 있던 공간까지 모두 할당했기 때문이다. LV에 50GiB만 더하려면 다음처럼 증가량 앞에 +를 붙인다.\nlvextend -r -L +50G /dev/vg1/lv1 -L 100G처럼 + 없이 지정하면 100GB를 추가하는 것이 아니라 LV의 최종 크기를 100GB로 맞춘다.\nXFS를 별도 명령으로 확장하는 방법 # 환경에 따라 -r을 이용한 XFS 확장이 실패한다면 LV와 파일시스템을 나누어 확장할 수 있다.\nlvextend -l +100%FREE /dev/vg1/lv1 mount /dev/vg1/lv1 /mnt xfs_growfs /mnt XFS는 마운트된 상태에서 xfs_growfs로 확장한다. 옵션 없이 마운트 경로를 지정하면 기반 장치에서 사용할 수 있는 최대 크기까지 늘어난다. 자세한 내용은 Red Hat XFS 확장 문서에서 확인할 수 있다.\n11. 확장 결과와 데이터 확인 # -r 옵션으로 파일시스템까지 확장한 경우 LV를 다시 마운트하고 결과를 확인한다.\nmount /dev/vg1/lv1 /mnt df -h cat /mnt/hello.txt 확인 결과는 다음과 같다.\n/dev/mapper/vg1-lv1 용량이 약 30GB에서 120GB로 증가했다. 파일시스템이 /mnt에 정상적으로 마운트됐다. 확장 전에 만든 /mnt/hello.txt의 내용이 유지됐다. 마지막으로 각 계층의 크기를 함께 확인하면 누락된 확장 단계가 없는지 판단하기 쉽다.\nlsblk pvs vgs lvs df -hT /mnt 12. 재부팅 후에도 자동으로 마운트하기 # 지금까지 실행한 mount 명령은 재부팅 후 유지되지 않는다. 운영 환경에서는 파일시스템 UUID를 확인해 /etc/fstab에 등록한다.\nblkid /dev/vg1/lv1 출력된 UUID를 사용해 /etc/fstab에 다음 형식으로 추가한다.\nUUID=\u0026lt;확인한-UUID\u0026gt; /mnt xfs defaults,nofail 0 0 재부팅하기 전에 설정이 올바른지 반드시 검사한다.\nmount -av findmnt /mnt /mnt는 실습용 경로다. 실제 서버에서는 용도에 맞게 /data와 같은 별도 마운트 경로를 정하는 것이 관리하기 편하다.\n13. 자주 확인할 문제 # GCP에서는 100GB인데 pvs에는 50GB로 표시되는 경우 # 클라우드 디스크만 확장되고 PV에는 반영되지 않은 상태다. 대상 장치를 다시 확인한 뒤 pvresize를 실행한다.\npvresize /dev/sdd lvs 용량은 늘었는데 df 용량이 그대로인 경우 # LV만 확장되고 파일시스템이 확장되지 않았을 수 있다. XFS가 마운트된 상태에서 다음 명령을 실행한다.\nxfs_growfs /mnt 재부팅 후 디스크 장치명이 달라진 경우 # /dev/sdb 같은 장치명에 의존하지 말고 /dev/disk/by-id/google-* 경로나 파일시스템 UUID를 사용한다. /etc/fstab도 UUID 기준으로 구성한다.\n마운트 해제가 되지 않는 경우 # 현재 디렉터리가 /mnt 아래에 있거나 프로세스가 파일을 사용하고 있을 수 있다. 다른 경로로 이동한 뒤 사용 중인 프로세스를 확인한다.\ncd / fuser -vm /mnt 마무리 # GCP 콘솔에서 Persistent Disk 크기를 늘리는 것만으로 LVM과 파일시스템의 사용 가능 공간이 자동으로 확장되지는 않는다. 운영체제에서 디스크 크기를 확인한 뒤 pvresize로 PV를 확장하고, lvresize 또는 lvextend로 LV를 늘린 다음 XFS 파일시스템까지 확장해야 한다.\n이번 실습에서는 10GB, 10GB, 50GB 디스크로 VG를 구성하고 30GB LV를 만든 뒤, 50GB 디스크를 100GB로 확장했다. 마지막으로 VG의 남은 공간을 모두 LV에 할당해 약 120GB의 XFS 파일시스템으로 확장했고, 기존 데이터가 유지되는 것까지 확인했다.\n","date":"2026년 9월 21일","externalUrl":null,"permalink":"/posts/gcp-compute-engine-persistent-disk-lvm-resize/","section":"글 목록","summary":"","title":"GCP Compute Engine Persistent Disk를 LVM으로 구성하고 용량 확장하기","type":"posts"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/jetpack-compose/","section":"Tags","summary":"","title":"Jetpack-Compose","type":"tags"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/lvm/","section":"Tags","summary":"","title":"Lvm","type":"tags"},{"content":"","date":"2026년 9월 21일","externalUrl":null,"permalink":"/tags/persistent-disk/","section":"Tags","summary":"","title":"Persistent-Disk","type":"tags"},{"content":"","date":"2026년 9월 18일","externalUrl":null,"permalink":"/tags/codex/","section":"Tags","summary":"","title":"Codex","type":"tags"},{"content":"Hugo 블로그 소스를 GitHub에 올린 뒤에도 사이트는 바로 열리지 않았다. 처음에는 워크플로 파일을 push할 토큰 권한이 부족했고, 그다음에는 Pages 설정 때문에 Actions가 실패했다.\n두 문제를 해결하고 Cafe24 DNS와 HTTPS를 설정해 https://blog.jinosoft.com으로 연결했다. 저장소 생성부터 자동 배포와 도메인 연결까지 실제 진행 순서대로 정리한다.\n1. 사용 환경과 최종 결과 # 이번에 사용한 환경은 다음과 같다.\n항목 내용 정적 사이트 생성기 Hugo 0.165.0 Extended 테마 Blowfish 저장소 GitHub jinosoft/jinosoft-blog 배포 방식 GitHub Actions 도메인 blog.jinosoft.com DNS 관리 Cafe24 최종적으로 GitHub 저장소의 main 브랜치에 변경 사항을 push하면 GitHub Actions가 Hugo 사이트를 빌드하고 GitHub Pages에 배포하도록 구성했다.\n2. GitHub 저장소 준비 # 저장소 이름 정하기 # 기존 Android 앱 저장소와 구분하기 위해 블로그 저장소 이름을 jinosoft-blog로 정했다.\n개인 GitHub Pages 저장소의 이름을 반드시 jinosoft.github.io로 만들 필요는 없다. 프로젝트 저장소를 사용하면 기본 주소는 저장소 이름을 포함한 형태가 되지만, 커스텀 도메인을 연결하면 외부에 노출되는 주소는 별도로 사용할 수 있다.\n저장소 공개 범위 # GitHub Free 개인 계정에서는 GitHub Pages를 Public 저장소에서만 사용할 수 있다. GitHub 공식 문서 기준으로 GitHub Pro, Team, Enterprise에서는 Public 및 Private 저장소에서 GitHub Pages를 사용할 수 있다.\nGitHub 요금제 GitHub Pages 저장소 GitHub Free Public만 가능 GitHub Pro / Team / Enterprise Public 및 Private 가능 따라서 GitHub Free를 사용한다면 이 블로그 저장소는 Public으로 유지해야 한다. 저장소가 공개되어도 실제 블로그 사이트의 운영에는 문제가 없다. 다만 Personal Access Token, API 키, 비밀번호, 서버 접속 정보와 같은 비밀 값은 저장소에 넣으면 안 된다.\n3. 로컬 프로젝트를 GitHub에 올리기 # 아직 Git 저장소로 관리하지 않는 Hugo 프로젝트라면 프로젝트 디렉터리에서 Git 저장소를 초기화하고 GitHub 원격 저장소를 연결한다.\ngit init git branch -M main git remote add origin https://github.com/jinosoft/jinosoft-blog.git git add . git commit -m \u0026#34;Initial Hugo blog setup\u0026#34; git push -u origin main 이미 git init과 원격 저장소 연결을 완료한 프로젝트라면 초기화 명령을 다시 실행할 필요 없이 다음처럼 변경 사항만 commit하고 push한다.\ngit add . git commit -m \u0026#34;Initial Hugo blog setup\u0026#34; git push -u origin main HTTPS 방식으로 push하면 GitHub 사용자 이름과 Personal Access Token을 입력한다. GitHub는 일반 계정 비밀번호를 Git 작업의 비밀번호로 사용하지 않으므로, 비밀번호 입력란에 발급한 토큰을 붙여 넣어야 한다.\nPersonal Access Token 권한 # Fine-grained token을 만들 때는 저장소 접근 범위를 블로그 저장소로 제한하는 것이 좋다. 일반적인 소스 push에는 저장소의 Contents 권한이 필요하다. 아래 권한은 이 글에서처럼 소스와 GitHub Actions 워크플로 파일을 함께 처음 push하는 상황을 기준으로 한다.\n저장소 선택 후 Permissions에서는 Contents를 Read and write로 설정한다. .github/workflows/ 아래의 워크플로 파일을 push하거나 수정하려면 Workflows도 Read and write로 설정해야 한다. 계정 권한은 이 작업에 필요하지 않으므로 최소 권한 원칙에 따라 추가하지 않는다.\n처음 워크플로 파일을 함께 push했을 때는 다음 권한 오류로 거부됐다.\nrefusing to allow a Personal Access Token to create or update workflow without workflow scope 토큰에 워크플로 파일을 수정할 수 있는 권한을 추가한 뒤 다시 push하자 업로드가 성공했다. 위 오류의 workflow scope는 classic PAT의 권한 표현이다. Fine-grained token에서는 앞서 설명한 Workflows 권한을 확인한다. 토큰 값은 저장소나 글에 기록하지 않는다.\n4. GitHub Actions로 자동 배포 구성 # GitHub Pages는 저장소의 소스 파일을 그대로 보여주는 방식이 아니라, Hugo가 생성한 정적 파일을 배포하도록 구성한다. 프로젝트에는 .github/workflows/pages.yml 파일을 만들고 main 브랜치 push를 배포 트리거로 설정한다.\n일반적인 흐름은 다음과 같다.\nmain 브랜치에 push한다. GitHub Actions가 실행된다. Hugo 프로젝트를 빌드한다. 생성된 public 결과물을 GitHub Pages에 배포한다. 워크플로 파일에는 Hugo Extended 버전을 사용하도록 지정해야 한다. Blowfish 테마와 사용자 정의 CSS를 사용하는 경우 Extended 버전이 필요할 수 있다.\npush는 성공했지만 첫 Actions 실행은 configure-pages 단계에서 실패했다. Get Pages site failed라는 오류였고, 저장소의 Pages 빌드 소스를 GitHub Actions로 지정한 뒤 다시 실행해 해결했다. 다음 단계의 Pages 설정까지 완료해야 배포할 수 있다.\n5. GitHub Pages 활성화 # GitHub 저장소에서 다음 메뉴로 이동한다.\nSettings \u0026gt; Pages Build and deployment의 Source를 GitHub Actions로 선택한다.\nActions 실행이 성공하면 기본 GitHub Pages 주소에서 사이트를 확인할 수 있다. 프로젝트 저장소를 사용하는 경우 기본 주소는 다음과 같은 형태다.\nhttps://jinosoft.github.io/jinosoft-blog/ 이 주소가 열리지 않는다면 Actions 성공 여부, Pages 활성화 여부, baseURL 설정을 순서대로 확인한다.\n6. 커스텀 도메인 연결 # GitHub Pages 설정 화면의 Custom domain에 사용할 도메인을 입력한다.\nblog.jinosoft.com 저장하면 GitHub Pages 설정과 배포 방식에 따라 저장소의 CNAME 파일이 생성되거나, 배포 결과에 커스텀 도메인이 반영된다. 이미 CNAME 파일이 있다면 내용이 해당 도메인과 일치하는지 확인한다. Hugo 설정의 baseURL도 실제 주소와 일치하도록 지정한다.\nbaseURL = \u0026#34;https://blog.jinosoft.com/\u0026#34; DNS에 CNAME 레코드 추가 # 도메인을 구입한 등록기관의 DNS 관리 화면에서 다음 CNAME 레코드를 추가한다.\n항목 값 호스트 blog 레코드 종류 CNAME 대상 jinosoft.github.io DNS 설정 후 바로 반영되지 않을 수 있다. 등록기관 안내처럼 수십 분에서 몇 시간 정도 기다린 뒤 GitHub Pages 설정 화면에서 DNS check successful 표시를 확인한다.\n7. HTTPS 활성화 # 도메인 연결이 확인되면 GitHub Pages 설정 화면에서 Enforce HTTPS를 활성화한다.\n인증서 발급이 완료되기 전에는 해당 옵션이 바로 활성화되지 않을 수 있다. 잠시 기다린 뒤 다시 확인하고, 최종적으로 다음 주소가 HTTPS로 열리는지 확인한다.\nhttps://blog.jinosoft.com https://blog.jinosoft.com/posts/install-codexcli-with-wsl/ 8. 접속되지 않을 때 확인할 순서 # GitHub Pages 주소가 404인 경우 # GitHub Actions가 성공했는지 확인한다. Settings \u0026gt; Pages에서 Source가 GitHub Actions인지 확인한다. 커스텀 도메인과 baseURL이 일치하는지 확인한다. 배포가 완료된 뒤 브라우저 캐시와 DNS 반영 시간을 고려한다. DNS 설정 후에도 접속되지 않는 경우 # DNS 전파가 끝나지 않았을 수 있다. CNAME의 호스트가 blog인지, 대상이 jinosoft.github.io인지, 다른 충돌 레코드가 없는지 확인한다.\n9. 글 URL을 안정적으로 유지하기 # 블로그 글은 글마다 디렉터리를 만들고 그 안에 index.md와 이미지를 함께 저장한다.\ncontent/posts/deploy-hugo-github-pages/ index.md create_repository_public.PNG github_action.PNG Hugo 설정에서는 글 URL을 다음과 같이 고정했다.\n[permalinks] posts = \u0026#34;/posts/:slug/\u0026#34; 따라서 이 글의 주소는 다음과 같다.\n/posts/deploy-hugo-github-pages/ 향후 GitHub Pages에서 자체 서버로 이전하더라도 같은 도메인과 permalink 설정을 유지하면 기존 글 주소를 계속 사용할 수 있다.\n10. 로컬에서 확인하고 배포하기 # 글을 작성한 뒤에는 먼저 로컬 서버로 결과를 확인한다.\nhugo server -D 배포 전 정식 빌드도 확인한다.\nhugo --minify 문제가 없다면 변경 사항을 커밋하고 push한다.\ngit add . git commit -m \u0026#34;Add Hugo GitHub Pages deployment guide\u0026#34; git push origin main 이후 GitHub Actions 실행이 성공하면 몇 분 안에 사이트에 변경 사항이 반영된다.\n마무리 # 최종적으로 main에 push하면 Hugo 빌드와 Pages 배포가 자동으로 진행되고, 커스텀 도메인으로 글을 볼 수 있는 상태가 됐다.\n이번 설정에서 기억할 점은 push 성공과 배포 성공은 다르다는 것이다. 404가 보이면 DNS만 기다리기보다 Actions 실행 결과와 Pages 설정부터 확인한다.\n","date":"2026년 9월 18일","externalUrl":null,"permalink":"/posts/deploy-hugo-github-pages/","section":"글 목록","summary":"","title":"GitHub Pages에 Hugo 블로그 배포하고 커스텀 도메인 연결하기","type":"posts"},{"content":"","date":"2026년 9월 18일","externalUrl":null,"permalink":"/tags/github-actions/","section":"Tags","summary":"","title":"Github-Actions","type":"tags"},{"content":"","date":"2026년 9월 18일","externalUrl":null,"permalink":"/tags/nodejs/","section":"Tags","summary":"","title":"Nodejs","type":"tags"},{"content":"Windows에서 WSL과 Ubuntu를 준비한 뒤 Codex CLI를 설치하고 기본 설정을 진행했다. 설치 명령 자체는 짧지만, WSL에서 Windows에 설치된 npm이 먼저 잡히는 부분은 확인이 필요했다.\nWindows와 Linux의 실행 경로를 구분한 뒤 로그인, 모델 선택, 상태 표시줄 설정, 이전 대화 복원까지 확인했다. 아래는 그때의 설치 기록이다.\n설치 환경 # 아래는 2026년 9월 16일 캡처에서 확인한 환경이다. 버전 번호는 당시 설치 결과이며, 동일한 버전을 반드시 설치해야 한다는 뜻은 아니다.\n항목 캡처 기준 운영 환경 Windows의 WSL2 + Ubuntu Node.js v22.22.1 npm 11.17.0 Codex CLI 0.154.0 설치 방식 Ubuntu의 apt로 Node.js·npm 설치 후 npm으로 Codex 설치 로그인 방식 ChatGPT 계정 로그인 Codex 설치에는 여러 방법이 있다. 이 글은 캡처에 남긴 npm 설치 과정을 기준으로 설명한다. 현재 공식 문서는 별도의 설치 스크립트 방식도 안내하므로, npm을 사용하지 않는 경우에는 Codex CLI 설치 안내를 참고하면 된다.\n1. WSL과 Ubuntu 설치 # Windows에서 WSL 설치 # Windows 터미널이나 PowerShell을 관리자 권한으로 열고 실행한다.\nwsl --install 처음에는 WSL이 설치되어 있지 않다는 안내가 나왔고, 설치 명령을 실행한 뒤 WSL과 가상 머신 플랫폼 구성 요소가 설치되었다.\n설치 결과에 시스템을 다시 시작하라는 안내가 있다면 Windows를 재부팅한다.\nUbuntu 배포판 확인 # 재부팅 후 Windows 터미널에서 설치된 배포판을 확인한다.\nwsl -l -v 설치 과정에서는 다음과 같이 배포판이 없다는 안내도 확인했다. 이 메시지는 설치된 Linux 배포판이 없다는 의미이며, 메시지 자체가 재부팅 필요 여부를 알려 주는 것은 아니다.\nUbuntu가 목록에 없다면 설치한다. 이미 설치되어 있다면 이 단계는 건너뛴다.\nwsl --install -d Ubuntu 최초 실행에서 Linux 사용자 이름과 비밀번호 설정을 요청하면 입력한다. 이후 sudo 명령에 사용할 비밀번호다.\n다시 wsl -l -v를 실행해 Ubuntu의 VERSION이 2인지 확인한다.\nUbuntu로 진입 # Windows 터미널에서 다음 명령으로 Ubuntu 셸을 연다.\nwsl -d Ubuntu 기본 배포판이 Ubuntu라면 wsl만 입력해도 된다. Ubuntu 셸에서 exit를 입력하면 Windows 셸로 돌아온다.\n이후 설치 명령은 WSL의 Ubuntu 셸 안에서 실행한다. Windows의 C:\\Users\\...\u0026gt; 프롬프트와 Linux의 사용자@호스트:경로$ 프롬프트를 구분하면 명령을 잘못된 환경에서 실행하는 일을 줄일 수 있다.\n2. Node.js와 npm 경로 확인 # Ubuntu 셸에서 Node.js와 npm의 버전과 실행 경로를 확인한다.\nnode -v npm -v which node which npm 이 환경에서는 node가 없다는 메시지가 나왔는데도 npm -v는 버전을 출력했다. which npm으로 경로를 확인하니 Windows에 설치된 npm을 가리키고 있었다.\n/mnt/c/Program Files/nodejs/npm 따라서 npm 버전이 출력된다는 사실만으로 WSL 내부의 Node.js 환경이 준비되었다고 판단하면 안 된다. 실행 파일이 어느 환경에 설치되어 있는지 함께 확인해야 한다.\nUbuntu에 Node.js와 npm 설치 # 이 설치에서는 Ubuntu 패키지 관리자인 apt를 사용했다. 먼저 패키지 목록과 설치 후보 버전을 확인한다.\nsudo apt update apt policy nodejs 캡처에서는 Node.js 22 계열이 설치 후보로 표시되었다. 배포판과 저장소에 따라 후보 버전은 달라질 수 있다.\nsudo apt install -y nodejs npm 설치 후 버전과 경로를 다시 확인한다.\nwhich node which npm node -v npm -v 이 환경에서는 node와 npm이 각각 /usr/bin/node, /usr/bin/npm으로 확인되었다. nvm 같은 버전 관리 도구를 사용한다면 경로가 다를 수 있다. 핵심은 두 명령 모두 의도한 WSL 내부의 설치 경로를 사용하는 것이다.\n3. Codex CLI 설치 # 이번 환경은 apt로 설치한 시스템 npm을 사용했으며, 전역 설치 위치에 쓰기 권한이 필요해 다음과 같이 설치했다.\nsudo npm install -g @openai/codex 사용자 소유의 Node.js 환경이나 npm 전역 설치 경로를 사용한다면 sudo 없이 npm install -g @openai/codex로 설치한다. 위의 sudo는 이 글의 시스템 npm 설치 환경에 해당하며, 이후 Codex를 실행할 때 붙이는 옵션이 아니다.\n설치 결과를 확인한다.\nwhich codex codex --version 캡처에서는 /usr/local/bin/codex와 codex-cli 0.154.0이 출력되었다.\n4. 작업 폴더에서 실행하고 로그인 # 프로젝트 폴더 선택 # 설치 확인 화면에서는 /mnt/c/Users/...에서 Codex를 실행했다. 실제 코드 작업을 시작할 때는 작업할 프로젝트 폴더로 이동한 뒤 실행하는 편이 좋다.\n아직 프로젝트가 없다면 연습용 폴더를 만들 수 있다.\nmkdir -p ~/projects/codex-test cd ~/projects/codex-test codex 이미 프로젝트가 있다면 해당 폴더로 이동해 codex를 실행한다. WSL의 Linux 홈 디렉터리 아래에 저장소를 두는 방식은 OpenAI의 WSL 안내에서도 권장한다.\nChatGPT 계정으로 로그인 # 처음 실행하면 로그인 방법을 선택하는 화면이 나온다. 여기서는 Sign in with ChatGPT를 선택했다.\n브라우저에서 계정 로그인과 인증을 마친 뒤 터미널로 돌아온다. 아래처럼 ChatGPT 계정으로 로그인했다는 메시지가 표시되면 다음 단계로 진행한다.\n로그인 선택지와 이용 가능한 기능은 계정 및 버전에 따라 달라질 수 있다. 이 글은 ChatGPT 로그인 경로를 사용했으며, API 키 설정은 다루지 않는다.\n디렉터리 신뢰 확인 # 현재 디렉터리의 내용을 신뢰하는지 묻는 화면이 나오면 표시된 경로를 확인한다.\n캡처에는 Windows 사용자 폴더가 표시되어 있다. 그대로 따라 승인하기보다 자신이 작업하려는 프로젝트 경로인지 먼저 확인한다. 경로가 잘못되었다면 종료하고 올바른 프로젝트 폴더에서 다시 실행한다.\n5. 모델과 추론 수준 선택 # 여기부터 /로 시작하는 명령은 Ubuntu 셸이 아니라 Codex 대화 입력창에 입력한다.\n/model 모델 선택 화면에서 사용할 모델을 고른다.\n당시에는 gpt-5.5를 선택했다. 모델 목록과 사용량 안내는 계정·버전에 따라 달라지므로 현재 자신의 화면을 기준으로 선택한다.\n이어서 추론 수준을 선택하는 화면이 나왔다. 캡처에서는 Medium을 사용했다.\n명령별 옵션은 공식 CLI 명령 문서에서 확인할 수 있다.\nFast 모드 확인 # 캡처에서는 다음 명령도 확인했다.\n/fast 화면에는 속도 향상과 함께 사용량 증가 안내가 표시됐다. 빠른 응답이 필요할 때 검토할 수 있는 선택 사항이며, 활성화 전에 현재 모델의 지원 여부와 사용량 안내를 확인한다.\n6. 상태 표시줄 구성 # 모델, 현재 작업 경로, 남은 컨텍스트 등을 하단에서 확인하려면 상태 표시줄을 설정한다.\n/statusline 캡처에서는 다음 항목을 선택했다.\n항목 표시 내용 model-with-reasoning 모델 이름과 추론 수준 current-dir 현재 작업 디렉터리 context-remaining 남은 컨텍스트 비율 five-hour-limit 5시간 사용 한도의 남은 양 weekly-limit 주간 사용 한도의 남은 양 total-output-tokens 세션에서 사용한 출력 토큰 수 이 화면에서는 Space로 항목을 선택하거나 해제하고 Enter로 확정할 수 있다. 적용하면 하단에 선택한 정보가 표시된다.\nContext는 현재 대화의 컨텍스트 여유를 뜻한다. 계정의 사용 한도를 나타내는 5시간·주간 항목과는 다른 정보다. 사용 한도처럼 계정 정보를 필요로 하는 항목은 이용 환경에 따라 표시되지 않을 수 있다.\n현재 세션 상태 확인 # 보다 자세한 상태는 다음 명령으로 확인한다.\n/status 모델 선택과 상태 표시줄 구성을 마친 뒤에는 이 화면에서 작업 디렉터리와 적용된 설정을 한 번 더 확인했다.\n7. 이전 대화 이어가기 # Codex에서 짧은 메시지를 보내 응답을 확인한 뒤 /exit로 종료한다. 캡처에서는 안녕?이라는 메시지로 동작을 확인했고, 종료 시 세션을 이어가는 명령이 표시되었다.\n다시 이어갈 때는 Ubuntu 셸에서 실행한다.\ncodex resume 목록에서 이전 세션을 선택하거나, 현재 작업 디렉터리의 가장 최근 세션을 바로 이어갈 수 있다.\ncodex resume --last 특정 세션을 지정하려면 종료 화면에 표시된 codex resume 명령과 세션 ID를 사용한다. 캡처의 ID를 복사하는 대신 자신의 세션 ID를 사용해야 한다.\n세션이 목록에 보이지 않으면 원래 작업했던 프로젝트 폴더인지 확인한다. 다른 디렉터리의 세션까지 찾을 때는 codex resume --all을 사용할 수 있다. 옵션은 공식 resume 명령 설명에 정리되어 있다.\n설치 후 확인할 것 # wsl -l -v에서 Ubuntu가 WSL2로 구성되어 있는지 확인한다. which node, which npm, which codex로 WSL 내부 실행 파일이 잡히는지 확인한다. 실제 프로젝트 폴더에서 codex를 실행한다. /model, /statusline, /status로 현재 설정을 확인한다. 작업을 다시 이어갈 때는 해당 폴더에서 codex resume를 실행한다. 설치 과정에서 가장 헷갈렸던 부분은 WSL에서 Windows의 npm이 잡히는 상황이었다. 버전 출력과 실행 경로를 함께 확인하고 나니, Linux 환경 안에 Node.js와 Codex가 설치되었는지 구분할 수 있었다.\n","date":"2026년 9월 18일","externalUrl":null,"permalink":"/posts/install-codexcli-with-wsl/","section":"글 목록","summary":"","title":"WSL 환경에서 Codex CLI 설치하고 구성하기","type":"posts"},{"content":"이 블로그는 WSL Ubuntu 안에서 글을 작성하고, 로컬에서 확인한 뒤 GitHub에 push하는 방식으로 관리하고 있다. 본문과 캡처는 글별 디렉터리에 함께 보관한다.\n새 글을 만들 때 실제로 사용하는 디렉터리 구조와 명령을 정리했다. 아래의 example-post는 구조를 설명하기 위한 샘플 이름이다. 실제 글에는 deploy-hugo-github-pages처럼 주제를 나타내는 영문 소문자와 하이픈 조합을 사용한다.\n1. 사용 환경 # 이번 블로그는 다음 환경에서 관리한다.\n항목 내용 운영체제 Windows + WSL2 Linux 배포판 Ubuntu 정적 사이트 생성기 Hugo Extended 0.165.0 테마 Blowfish 저장소 GitHub 배포 GitHub Actions + GitHub Pages 기본 언어 한국어 Windows에서 명령줄 도구를 실행하더라도 실제 Hugo 작업은 WSL의 Ubuntu 셸 안에서 진행한다. which hugo, which node, which npm으로 실행 파일 경로를 확인하면 Windows와 WSL 환경을 혼동하는 일을 줄일 수 있다.\nHugo Extended를 사용하는 이유 # 이 블로그는 Blowfish 테마와 사용자 정의 CSS를 사용하므로 Hugo Extended 버전을 설치했다. 테마가 요구하는 스타일 처리 기능과 CSS 확장을 안정적으로 사용하려면 일반 버전이 아닌 Extended 버전을 선택하는 것이 좋다.\n2. WSL에서 프로젝트 디렉터리로 이동하기 # 저장소를 WSL의 Linux 홈 디렉터리 아래에 복제했다면 다음처럼 프로젝트로 이동한다.\ncd ~/projects/myblog 저장소 상태는 다음 명령으로 확인할 수 있다.\ngit status VS Code로 프로젝트 열기 # WSL 터미널에서 프로젝트 디렉터리로 이동한 뒤 다음 명령을 실행하면 VS Code로 현재 프로젝트를 열 수 있다.\ncode . VS Code에 WSL 확장이 설치되어 있으면 Linux 환경의 Hugo와 Git을 그대로 사용하면서 Windows 화면에서 파일을 편집할 수 있다. VS Code의 통합 터미널도 WSL 환경으로 열려 있는지 확인한다.\n처음 연 폴더는 Restricted Mode로 표시될 수 있다. 자신이 만든 프로젝트이고 파일을 신뢰할 수 있는 경우에만 Trust this folder를 선택한다. 출처가 불분명한 프로젝트라면 먼저 파일과 실행 설정을 확인한다.\n프로젝트를 /mnt/c 아래에 둘 수도 있지만, 파일 감시와 터미널 작업을 자주 사용하는 프로젝트라면 WSL의 Linux 파일 시스템 안에 두는 편이 편리하다. 어떤 위치를 선택하든 한 프로젝트에서 경로를 일관되게 사용하는 것이 중요하다.\n3. Hugo 프로젝트 구조 # 현재 블로그의 주요 디렉터리는 다음과 같다.\nmyblog/ archetypes/ assets/ config/ content/ layouts/ static/ themes/ .github/ hugo.toml 각 디렉터리의 역할은 다음과 같다.\n경로 역할 content/posts 기술 블로그 글 content/about 소개 페이지 config/_default Hugo와 Blowfish 설정 assets/css 사용자 정의 CSS archetypes 새 글의 기본 Front Matter .github/workflows GitHub Actions 배포 설정 Hugo는 콘텐츠와 설정, 테마를 분리해서 관리한다. 글을 작성할 때는 주로 content/posts를 수정하고, 사이트 전체 동작을 바꿀 때는 config/_default를 확인한다.\n4. 글 하나당 디렉터리 만들기 # 이 블로그는 글 하나당 하나의 디렉터리를 사용한다. 글 본문은 index.md로 저장하고, 해당 글의 캡처 이미지도 같은 디렉터리에 둔다.\ncontent/posts/example-post/ index.md hugo_post_directory_achi.PNG hugo_themes.PNG 이 구조를 사용하면 글과 이미지가 함께 이동하므로 관리가 쉽다. 글을 다른 서버로 옮기거나 저장소 구조를 확인할 때도 어떤 이미지가 어느 글에 사용되는지 바로 알 수 있다.\n실제 주제를 사용하는 글은 다음처럼 디렉터리 이름도 글의 slug에 맞춘다.\ncontent/posts/wsl-hugo-blog-environment/ index.md screenshot-01.png 5. Hugo 테마 선택 # 이 블로그에는 Blowfish 테마를 적용했다. 다른 테마를 고를 때는 공식 테마 사이트에서 목록과 미리보기를 확인할 수 있다.\n테마는 첫 화면뿐 아니라 긴 본문, 목차, 모바일 화면과 코드 블록도 함께 살펴본다.\n6. 새 글 작성하기 # 프로젝트 루트에서 Hugo 명령으로 새 글을 만들 수 있다.\nhugo new posts/example-post/index.md 프로젝트에 archetypes/default.md가 있으면 새 글에 기본 Front Matter가 적용된다. 현재 블로그의 archetype 파일은 다음과 같은 형식이다.\n현재 블로그는 다음과 같은 Front Matter 형식을 사용한다.\n+++ title = \u0026#39;글 제목\u0026#39; date = \u0026#39;2026-09-18T00:00:00+09:00\u0026#39; draft = true slug = \u0026#39;example-post\u0026#39; description = \u0026#39;글 설명\u0026#39; tags = [\u0026#39;hugo\u0026#39;] categories = [\u0026#39;IT개발\u0026#39;] +++ 실제로 배포할 글은 본문을 완성한 뒤 draft = false로 변경한다. slug는 영문 소문자와 하이픈을 사용해 /posts/\u0026lt;slug\u0026gt;/ URL이 안정적으로 유지되도록 한다.\n7. 이미지 추가하기 # 글에 사용하는 캡처는 글 디렉터리에 저장하고 Markdown에서 파일명으로 연결한다.\n![글별 디렉터리 구조](hugo_post_directory_achi.PNG) 이미지 파일명은 영문 소문자, 숫자, 하이픈 또는 언더스코어 조합으로 관리하면 경로 문제를 줄일 수 있다. 캡처를 추가하기 전에는 Personal Access Token, API 키, 이메일 주소와 같은 민감한 정보가 보이지 않는지 확인한다.\n8. 로컬에서 미리보기 # 초안까지 포함해 로컬 서버를 실행한다.\nhugo server -D 브라우저에서 다음 주소를 연다.\nhttp://localhost:1313/ -D 옵션은 draft 상태인 글도 미리보기에 포함한다. 글을 실제로 배포할 때는 draft = false인지 확인해야 한다.\n9. 배포 전 빌드 확인 # GitHub에 push하기 전에 정식 빌드를 실행한다.\nhugo --minify 빌드 중 오류가 발생하면 메시지에 표시된 파일과 줄을 먼저 확인한다. 빌드 성공만으로 이미지 표시나 모바일 레이아웃까지 검증되지는 않으므로 브라우저에서도 확인한다.\n10. Git으로 commit하고 배포하기 # 변경된 파일을 확인한 뒤 commit한다.\ngit status git add . git commit -m \u0026#34;Add new post\u0026#34; git push origin main main 브랜치에 push하면 GitHub Actions가 Hugo를 빌드하고 GitHub Pages에 배포한다. GitHub 저장소의 Actions 탭에서 빌드와 배포가 모두 성공했는지 확인한 뒤 실제 글 주소를 연다.\n배포가 끝난 뒤에는 다음 항목을 확인한다.\nGitHub Actions의 build와 deploy 단계가 모두 성공했는지 확인 새 글의 실제 URL이 열리는지 확인 본문 이미지가 정상적으로 표시되는지 확인 데스크톱과 모바일 화면에서 레이아웃 확인 제목, 읽기 시간, 목차와 코드 블록이 의도대로 표시되는지 확인 11. 반복해서 사용할 작업 흐름 # 새 글을 작성할 때는 다음 순서를 반복하면 된다.\nWSL 실행 ↓ 프로젝트 디렉터리 이동 ↓ 새 글 디렉터리와 index.md 생성 ↓ 본문과 이미지 작성 ↓ hugo server -D로 미리보기 ↓ hugo --minify로 빌드 확인 ↓ git add / commit / push ↓ GitHub Actions와 실제 사이트 확인 자주 발생하는 문제 # Hugo 명령을 찾을 수 없는 경우 # Hugo가 설치되지 않았거나 PATH에 등록되지 않은 상태일 수 있다.\nwhich hugo hugo version 이미지가 표시되지 않는 경우 # 이미지가 index.md와 같은 디렉터리에 있는지, Markdown의 파일명이 실제 파일명과 같은지 확인한다. Linux 환경에서는 대문자와 소문자를 구분한다.\n초안 글이 사이트에 보이지 않는 경우 # draft = true인 글은 기본 빌드에서 제외된다. 공개할 글은 draft = false로 변경한다.\n로컬에서는 보이지만 배포 후 404인 경우 # slug와 baseURL, permalink 설정을 확인하고 GitHub Actions가 최신 commit을 배포했는지 확인한다.\n마무리 # 글을 추가할 때 주로 수정하는 곳은 content/posts다. 본문과 이미지를 같은 디렉터리에 두고, 로컬 미리보기와 빌드를 확인한 뒤 push하는 흐름으로 관리하면 된다. GitHub Pages와 도메인을 처음 연결하는 과정은 Hugo 블로그 배포하기에 따로 정리했다.\n","date":"2026년 9월 18일","externalUrl":null,"permalink":"/posts/wsl-hugo-blog-environment/","section":"글 목록","summary":"","title":"WSL에서 Hugo 블로그 개발 환경 구성하기","type":"posts"},{"content":"지노소프트 기술 블로그는 Jinosoft(지노소프트)가 운영하는 개인 기술 블로그입니다. 이 페이지는 방문자가 블로그를 이용할 때 처리될 수 있는 개인정보와 관련 정보를 안내합니다.\n수집하는 정보 # 현재 이 블로그는 회원 가입, 댓글, 게시판, 직접 결제 기능을 제공하지 않습니다. 따라서 블로그 자체에서 방문자의 이름, 주소, 전화번호와 같은 개인정보를 직접 입력받아 저장하지 않습니다.\n다만 방문자가 이메일로 문의하는 경우, 답변을 위해 이메일 주소와 문의 내용을 확인할 수 있습니다.\n자동으로 처리될 수 있는 정보 # 블로그 접속 과정에서 웹 서버, GitHub Pages, 브라우저, 보안 및 네트워크 시스템에 의해 다음과 같은 정보가 자동으로 처리될 수 있습니다.\n접속 IP 주소 브라우저 및 운영체제 정보 접속 일시 방문한 페이지 주소 리퍼러 정보 이 정보는 사이트 운영, 보안, 오류 확인, 트래픽 분석 목적으로 사용될 수 있습니다.\n쿠키와 외부 서비스 # 현재 블로그는 Google AdSense 광고를 표시하지 않으며, Google Analytics 방문 분석 코드를 사용하지 않습니다. 광고나 방문 분석 서비스를 도입하면 실제 사용하는 서비스와 처리 방식에 맞춰 이 방침을 갱신합니다.\nGoogle AdSense 도입 시의 광고 쿠키 # Google AdSense를 도입할 경우, Google을 포함한 제3자 광고 사업자는 방문자가 이 블로그나 다른 웹사이트를 방문한 기록을 바탕으로 광고를 제공하기 위해 쿠키를 사용할 수 있습니다. Google의 광고 쿠키를 통해 Google과 파트너가 이러한 방문 기록에 따른 광고를 게재할 수 있습니다.\n방문자는 다음 페이지에서 광고 및 쿠키 설정을 확인할 수 있습니다.\nGoogle 광고 설정: 개인 맞춤 광고 설정 및 해제 AboutAds의 광고 선택 도구: 참여하는 제3자 광고 사업자의 개인 맞춤 광고 해제 Google이 파트너 사이트의 정보를 사용하는 방식 Google 쿠키 사용 안내 개인 맞춤 광고를 해제해도 광고 자체가 모두 사라지는 것은 아닙니다. 브라우저 설정에서 쿠키를 차단하거나 삭제할 수도 있으며, 브라우저나 기기를 변경하면 설정을 다시 확인해야 할 수 있습니다.\n광고 도입 시에는 실제 참여하는 광고 사업자와 광고 네트워크의 안내를 추가하고, 필요한 동의 절차를 적용합니다. 위 설명은 도입 시 처리 방식을 안내하기 위한 것으로, 현재 광고 쿠키를 사용 중이라는 의미는 아닙니다.\n개인정보의 이용 목적 # 처리될 수 있는 정보는 다음 목적을 위해 사용됩니다.\n블로그 운영 및 콘텐츠 제공 문의 답변 사이트 오류 확인 및 보안 유지 방문 통계 분석 향후 광고 서비스 운영 및 정책 준수 개인정보의 보관 # 이메일 문의 내용은 답변과 필요한 후속 대응을 위해 합리적인 기간 동안 보관할 수 있습니다. 보관이 더 이상 필요하지 않다고 판단되면 삭제합니다.\n외부 서비스가 처리하는 정보의 보관 기간과 방식은 각 서비스 제공자의 정책을 따릅니다.\n제3자 제공 # 블로그 운영자는 방문자의 개인정보를 별도로 판매하거나 임의로 제3자에게 제공하지 않습니다. 다만 법령에 따른 요청이 있거나, 사이트 운영에 필요한 외부 서비스가 자체 정책에 따라 정보를 처리하는 경우가 있을 수 있습니다.\n문의 # 개인정보 처리와 관련한 문의는 아래 이메일로 연락할 수 있습니다.\n이메일: jinosoft1@gmail.com 변경 # 이 개인정보처리방침은 사이트 운영 방식, 외부 서비스 도입, 관련 법령 또는 정책 변화에 따라 수정될 수 있습니다.\n최종 수정일: 2026년 10월 16일\n","date":"2026년 9월 18일","externalUrl":null,"permalink":"/privacy/","section":"지노소프트 기술 블로그","summary":"","title":"개인정보처리방침","type":"page"},{"content":"블로그 글, 기술 내용, 오류 제보, 기타 문의는 이메일로 연락할 수 있습니다.\n이메일: jinosoft1@gmail.com 문의할 때는 가능한 한 관련 글 주소, 실행 환경, 오류 메시지, 재현 방법을 함께 적어 주세요. 확인이 필요한 내용은 답변까지 시간이 걸릴 수 있습니다.\n","date":"2026년 9월 18일","externalUrl":null,"permalink":"/contact/","section":"지노소프트 기술 블로그","summary":"","title":"문의","type":"page"},{"content":"지노소프트 기술 블로그는 **Jinosoft(지노소프트)**가 운영하는 개인 기술 블로그다. 개발 도구를 설치하고, 서비스를 구성하고, 제품을 만드는 과정에서 확인한 내용을 기록한다.\n다루는 내용 # WSL, Docker, Android Studio 등 개발 환경 설치와 구성 Kafka 등 백엔드 기술의 동작 원리와 실행 예제 클라우드 인프라 구성과 비용 비교 GitHub 활용, 오픈소스 라이선스, 개발 중 확인한 문제와 해결 방법 글을 작성하는 방식 # 직접 실행한 내용은 명령어, 설정 파일, 캡처 화면과 함께 정리한다. 공식 문서에서 확인한 동작과 직접 확인한 결과를 구분하고, 비용 비교에는 기준일과 계산 조건을 적는다. 예시 금액이나 권장 구성은 모든 환경에 동일하게 적용되는 정답이 아니라, 자신의 환경에 맞춰 검토할 출발점으로 제시한다.\n글의 오류나 변경된 내용을 발견했다면 관련 글 주소와 확인한 내용을 문의 페이지의 이메일로 알려 주세요.\n관련 링크 # 글 목록 Jinosoft 홈페이지 ","date":"2026년 9월 18일","externalUrl":null,"permalink":"/about/","section":"지노소프트 기술 블로그","summary":"","title":"소개","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]