<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>민규의 블로그✍️</title>
    <link>https://min-nine.tistory.com/</link>
    <description>개발 공부 및 사회생활 정보 기록  블로그 &amp;zwj; </description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 14:07:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>MingyuKim</managingEditor>
    <image>
      <title>민규의 블로그✍️</title>
      <url>https://tistory1.daumcdn.net/tistory/2812292/attach/3576555875794fe38c9f5b444e62a3ba</url>
      <link>https://min-nine.tistory.com</link>
    </image>
    <item>
      <title>바쁜 팀을 위한 자동 코드리뷰: MR 열리면 AI가 먼저 본다 - Gemini CLI + GitLab MCP로 &amp;ldquo;귀찮음 덜기&amp;rdquo; 실전 구축기</title>
      <link>https://min-nine.tistory.com/entry/%EB%B0%94%EC%81%9C-%ED%8C%80%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9E%90%EB%8F%99-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0-MR-%EC%97%B4%EB%A6%AC%EB%A9%B4-AI%EA%B0%80-%EB%A8%BC%EC%A0%80-%EB%B3%B8%EB%8B%A4-Gemini-CLI-GitLab-MCP%EB%A1%9C-%E2%80%9C%EA%B7%80%EC%B0%AE%EC%9D%8C-%EB%8D%9C%EA%B8%B0%E2%80%9D-%EC%8B%A4%EC%A0%84-%EA%B5%AC%EC%B6%95%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 시작했나: 사람이 아닌, 로봇이 먼저 보자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드리뷰는 중요하지만 항상 시간과 에너지가 남아돌 때만 잘 된다. 내가 속한 팀도 예외가 아니었다. 스프린트 막바지에는 리뷰가 밀리고, 급한 기능부터 합치다 보면 &amp;ldquo;나중에 손보자&amp;rdquo;가 쌓였다. 그래서 발상을 바꿨다.&lt;b&gt; 반복적이고 귀찮은 1차 검수는 AI에게 맡기고, 사람은 진짜 어려운 판단과 합의에 집중해 보자는 것&lt;/b&gt;. MR이 열리는 순간, AI가 먼저 변경 요약을 만들고 보안&amp;middot;성능&amp;middot;테스트 관점에서 핵심만 골라 코멘트를 남기면, 우리는 그걸 토대로 곧장 본질적인 토론으로 들어갈 수 있다. 그렇게 하면 컨텍스트가 사라지기 전에 더 빨리 결론에 닿는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사용 도구 및 오류 해결 과정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구는 단순하게 가져갔다. CI 환경에서 프롬프트만으로 동작하는 Gemini CLI, 그리고 GitLab의 변경 사항을 읽고 논의를 달 수 있는 MCP 서버 조합이다. 구현 자체는 어렵지 않았다. 하지만 첫 시도에서는 기대했던 것보다 훨씬 많은 시간을 소모했다. 로그에 401 Unauthorized가 연달아 찍히며 아무 것도 진행되지 않았기 때문이다. 더 곤란했던 건 토큰 자체는 살아 있고 권한도 충분했는데, 요청이 자꾸 우리의 사내 GitLab이 아닌 &lt;span&gt;gitlab.com&lt;/span&gt;으로 날아간다는 사실이었다. 사전 점검용 &lt;span&gt;curl&lt;/span&gt;로는 내부 GitLab API가 멀쩡히 200을 주는데, MCP를 통하면 401이 나온다. 토큰이나 권한 문제가 아니라, 애초에 엉뚱한 곳을 두드리고 있었던 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 금방 명확해졌다. 내가 사용한 MCP 구현은 기본 GitLab API 엔드포인트를 &lt;span&gt;&lt;a href=&quot;https://gitlab.com/api/v4&quot;&gt;https://gitlab.com/api/v4&lt;/a&gt;&lt;/span&gt;로 가정하고 있었다. 우리 환경은 &lt;a href=&quot;https://mgt-gitlab.xxx.com/api/v4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mgt-gitlab.xxx.com/api/v4&lt;/a&gt;다. 엔드포인트를 명시적으로 지정하지 않는 한, 어떤 토큰을 넣든 결과는 같았다. 해결은 간단했다. MCP가 읽는 환경변수 이름에 맞춰 &lt;span&gt;GITLAB_URL&lt;/span&gt;을 우리 도메인의 v4 엔드포인트로 지정하고, 토큰은 &lt;span&gt;GITLAB_TOKEN&lt;/span&gt;으로 전달했다. 그리고 경로 파싱으로 프로젝트를 식별하게 두지 않고, 숫자 &lt;span&gt;project_id&lt;/span&gt;만 쓰도록 모델에게 강제했다. OAuth를 꺼서(명시적으로 &lt;span&gt;--oauth=false&lt;/span&gt;) PAT만 사용하게 한 것도 안정화에 도움이 됐다. 이렇게 바꾸고 나니, 요청이 정확히 &lt;a href=&quot;https://mgt-gitlab.xxx.com/api/v4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mgt-gitlab.xxx.com/api/v4&lt;/a&gt;로 향했고, 디스커션과 코멘트가 정상적으로 생성되기 시작했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;gitlab-ci.yml 파일에 review 관련 stages 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 설정은 아래 한 파일로 정리했다. 핵심은 세 가지다. 사전 &lt;span&gt;curl&lt;/span&gt;로 토큰과 권한을 점검하고, &lt;span&gt;GITLAB_URL&lt;/span&gt;을 내부 엔드포인트로 못 박고, 프롬프트에서 &lt;span&gt;&lt;b&gt;project_id와 MR IID만&lt;/b&gt;&lt;/span&gt; 사용하도록 분명히 지시하는 것. 그 외에는 도구를 최소 권한으로 열어두고, 스팸성 코멘트를 만들지 않도록 프롬프트를 절제하는 정도다.&lt;/p&gt;
&lt;pre id=&quot;code_1755662960959&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stages:
  - gemini_code_review

code_review:
  stage: gemini_code_review
  image: us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.19
  variables:
    GIT_DEPTH: &quot;0&quot;
  before_script:
    - echo &quot;Starting Gemini code review&amp;hellip;&quot;
    # 키 존재 확인
    - |
      [ -n &quot;$GEMINI_API_KEY&quot; ] || { echo &quot;Error: GEMINI_API_KEY is not set.&quot;; exit 1; }
      [ -n &quot;$GITLAB_REVIEW_PAT&quot; ] || { echo &quot;Error: GITLAB_REVIEW_PAT is not set.&quot;; exit 1; }

    # fforster MCP 바이너리
    - |
      GITLAB_MCP_VERSION=&quot;1.31.0&quot;
      curl -sSL -o gitlab-mcp.tgz &quot;https://gitlab.com/fforster/gitlab-mcp/-/releases/v${GITLAB_MCP_VERSION}/downloads/gitlab-mcp_${GITLAB_MCP_VERSION}_Linux_x86_64.tar.gz&quot;
      tar -xzf gitlab-mcp.tgz &amp;amp;&amp;amp; chmod +x gitlab-mcp

    # 사전 토큰 체크(이건 항상 너 내부 GitLab URL로 호출됨)
    - |
      CODE=$(curl -s -o /dev/null -w &quot;%{http_code}&quot; \
        -H &quot;PRIVATE-TOKEN: $GITLAB_REVIEW_PAT&quot; \
        &quot;$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID&quot;)
      if [ &quot;$CODE&quot; != &quot;200&quot; ]; then
        echo &quot;GitLab token precheck failed (HTTP $CODE) for MR $CI_MERGE_REQUEST_IID.&quot;; exit 1
      fi

    #   MCP가 참조할 환경변수들 (fforster는 GITLAB_URL / GITLAB_TOKEN을 사용)
    - export GITLAB_TOKEN=&quot;$GITLAB_REVIEW_PAT&quot;
    - export GITLAB_URL=&quot;$CI_API_V4_URL&quot;             # https://mgt-gitlab.xxx.com/api/v4 로 설정됨
    - export CI_PROJECT_ID=&quot;$CI_PROJECT_ID&quot;          # 프롬프트에서 사용

    # Gemini 설정 (args에 --oauth=false 권장)
    - |
      mkdir -p &quot;$HOME/.gemini&quot;
      cat &amp;gt; &quot;$HOME/.gemini/settings.json&quot; &amp;lt;&amp;lt;EOF
      {
        &quot;coreTools&quot;: [&quot;LSTool&quot;,&quot;ReadFileTool&quot;,&quot;GrepTool&quot;,&quot;GlobTool&quot;,&quot;ReadManyFilesTool&quot;],
        &quot;mcpServers&quot;: {
          &quot;gitlab&quot;: {
            &quot;command&quot;: &quot;${CI_PROJECT_DIR}/gitlab-mcp&quot;,
            &quot;args&quot;: [&quot;--oauth=false&quot;],
            &quot;env&quot;: {
              &quot;GITLAB_TOKEN&quot;: &quot;${GITLAB_REVIEW_PAT}&quot;,
              &quot;GITLAB_URL&quot;: &quot;${CI_API_V4_URL}&quot;
            },
            &quot;timeout&quot;: 5000,
            &quot;includeTools&quot;: [
              &quot;discussion_add_note&quot;,
              &quot;discussion_list&quot;,
              &quot;get_merge_request&quot;,
              &quot;get_merge_request_changes&quot;,
              &quot;get_merge_request_commits&quot;,
              &quot;list_merge_request_diffs&quot;,
              &quot;get_merge_request_participants&quot;,
              &quot;get_merge_request_reviewers&quot;,
              &quot;get_repository_file_contents&quot;
            ]
          }
        }
      }
      EOF

  script:
    # 5) 실제 리뷰 수행
    - |
      gemini --yolo &amp;lt;&amp;lt;EOF
      당신은 숙련된 코드리뷰어다. 아래 GitLab MR에 대해 간결한 요약과 개선 제안을 남겨라.
      - 프로젝트 경로: ${CI_PROJECT_PATH}
      - 반드시 이 MR IID만 사용: ${CI_MERGE_REQUEST_IID}
      원칙:
        1) 스팸 금지, 실제 품질에 의미 있는 지적만.
        2) Java/Spring, PHP/Laravel, Node/Nest/Express, SQL/MySQL, JPA, Redis/RabbitMQ, K8s/Helm, GitOps/ArgoCD 관점의 베스트프랙티스 반영.
        3) 보안(입력검증/시크릿/SQLi), 성능(N+1/인덱스/Filesort/캐싱), 트랜잭션/동시성, 테스트(단위/통합), 로그/모니터링 관점 포함.
        4) 가능하면 라인 인용과 간단한 패치 예시(코드블록)를 제시.
        5) 본인이 이전에 남긴 코멘트 중복 금지.
      작업:
        - 변경 요약: 필요 시 단일 코멘트로 핵심 변경 요약.
        - 개선 제안: 우선순위 높은 3~7개 항목을 개별 코멘트로 작성.
      EOF

  rules:
    # MR 이벤트에서만 실행
    - if: '$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;'
      when: on_success&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작은 단순하다. MR이 열리면 파이프라인이 기동하고, Gemini CLI가 프롬프트에 따라 MCP를 호출한다. MCP는 우리 GitLab의 v4 API로 MR 개요와 변경 내용을 읽고, 필요한 경우 디스커션과 코멘트를 생성한다. 나는 그 결과를 확인하면서 정말 중요한 논점만 빠르게 파고들면 된다. 터무니없는 잔소리나 과장된 포맷을 만들지 않도록 프롬프트를 계속 다듬었고, 팀의 스택에 맞춰 관점을 좁혔다. JPA의 N+1, MySQL 인덱싱과 Filesort, 캐싱과 동시성, RabbitMQ 재처리 같은 항목은 우리 상황에서 반복적으로 마주치는 지점이라 우선순위를 높였다. 덕분에 AI가 남기는 코멘트가 잡다한 스타일 이슈에 매달리지 않고 진짜 위험도를 먼저 건드리기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 명확하다. 누군가가 항상 바쁘다는 사실을 인정하고, 그 상태에서도 품질을 일정 수준으로 끌어올리는 길을 만든다. AI가 남긴 코멘트는 초벌 분류에 가깝지만, 그 초벌이 있는 것과 없는 것의 차이는 크다. 리뷰를 시작할 때의 진입 장벽이 낮아지고, 사람이 판단해야 할 이슈로 더 빨리 닿는다. 무엇보다도 &amp;ldquo;귀찮음&amp;rdquo;을 줄이는 것에서 큰 만족을 느낀다. 테스트 누락이나 사소한 누수처럼 반복적으로 발견되는 지점은 AI가 먼저 긁어 주고, 나는 배경지식과 도메인 맥락이 필요한 논점을 다듬는다. 결과적으로 리뷰의 밀도가 높아지고, 팀 전체의 응답 속도가 빨라졌다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkpBC0/btsPX19BghD/ssTTZknU5cIbyBHQZiQCm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkpBC0/btsPX19BghD/ssTTZknU5cIbyBHQZiQCm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkpBC0/btsPX19BghD/ssTTZknU5cIbyBHQZiQCm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkpBC0%2FbtsPX19BghD%2FssTTZknU5cIbyBHQZiQCm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;1318&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;1318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;여기서 끝인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 여기서 끝은 아니다. 줄 단위 포지션을 정교하게 찍는 초안 코멘트 흐름으로 바꿔 보면, 더 밀도 높은 리뷰가 가능하다. Draft Notes API를 쓰면 여러 코멘트를 한 번에 공개할 수도 있어 운영이 깔끔해진다. 프롬프트도 팀별로 계속 튜닝할 수 있다. 우리처럼 JPA를 많이 쓰는 팀이라면 엔티티 그래프나 배치 사이즈 같은 키워드를 아예 프롬프트에 박아 두는 것도 방법이다. 계정도 봇을 분리하면, 기록과 추적성이 좋아진다. 포크 MR에서 변수가 주입되지 않는 문제는 정책으로 풀 수 있고, 필요하면 허용 범위를 신중히 열면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 하면서 가장 크게 배운 건, &amp;ldquo;기본값&amp;rdquo;을 절대 믿지 말자는 점이다. 특히 엔드포인트 같은 건 항상 명시해야 한다. &lt;span&gt;GITLAB_URL&lt;/span&gt; 한 줄이 빠졌을 뿐인데, 토큰도 권한도 무의미해졌다. 반대로 그 한 줄을 정확히 넣었더니 모든 게 제자리를 찾았다. 자동화는 결국 사람의 시간을 되찾아 주기 위한 장치다. MR을 열면 AI가 먼저 보고, 사람은 중요한 일에 더 오래 머문다. 내가 하고 싶었던 건 그 단순한 분업이었고, 지금은 그 방향으로 잘 굴러가고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmAX3f/btsPXBXuYod/613GNWuEBPHZLcdAqJKwkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmAX3f/btsPXBXuYod/613GNWuEBPHZLcdAqJKwkK/img.png&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;1703&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.3147%; margin-right: 10px;&quot; data-widthpercent=&quot;49.89&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmAX3f/btsPXBXuYod/613GNWuEBPHZLcdAqJKwkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmAX3f%2FbtsPXBXuYod%2F613GNWuEBPHZLcdAqJKwkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;1703&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbeUKG/btsPVIiKLx3/BqeKxQiXrNLdYkJhp8FErk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbeUKG/btsPVIiKLx3/BqeKxQiXrNLdYkJhp8FErk/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1699&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.5225%;&quot; data-widthpercent=&quot;50.11&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbeUKG/btsPVIiKLx3/BqeKxQiXrNLdYkJhp8FErk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbeUKG%2FbtsPVIiKLx3%2FBqeKxQiXrNLdYkJhp8FErk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1699&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Infrastructure/Git</category>
      <category>gemini cli codereview 적용기</category>
      <category>git ai codereview 적용</category>
      <category>gitlab ai code review 예제</category>
      <category>gitlab ai codereview</category>
      <category>gitlab ai codereview 예제</category>
      <category>gitlab gemini cli code review</category>
      <category>gitlab gemini cli mr</category>
      <category>gitlab gemini codereview 예제</category>
      <category>gitlab mr ai review</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/356</guid>
      <comments>https://min-nine.tistory.com/entry/%EB%B0%94%EC%81%9C-%ED%8C%80%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9E%90%EB%8F%99-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0-MR-%EC%97%B4%EB%A6%AC%EB%A9%B4-AI%EA%B0%80-%EB%A8%BC%EC%A0%80-%EB%B3%B8%EB%8B%A4-Gemini-CLI-GitLab-MCP%EB%A1%9C-%E2%80%9C%EA%B7%80%EC%B0%AE%EC%9D%8C-%EB%8D%9C%EA%B8%B0%E2%80%9D-%EC%8B%A4%EC%A0%84-%EA%B5%AC%EC%B6%95%EA%B8%B0#entry356comment</comments>
      <pubDate>Wed, 20 Aug 2025 13:17:14 +0900</pubDate>
    </item>
    <item>
      <title>복붙 지옥에서 헥사고날까지 &amp;ndash; 실무에서 적용 기록</title>
      <link>https://min-nine.tistory.com/entry/%EB%B3%B5%EB%B6%99-%EC%A7%80%EC%98%A5%EC%97%90%EC%84%9C-%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0%EA%B9%8C%EC%A7%80-%E2%80%93-%ED%98%84%EC%8B%A4-%EA%B0%9C%EB%B0%9C%ED%8C%80%EC%9D%98-%EC%84%B1%EC%9E%A5-%EA%B8%B0%EB%A1%9D</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;또 아키텍처를 바꾸자고..?&quot; 솔직히 이 말, 개발자라면 한 번쯤 속으로 해봤을 거라 생각한다. 나도 예외는 아니었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중복의 늪과 아키텍처 실험&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리팀은 백엔드 개발자 총 4명으로, 관리하는 API Application만 20개가 넘는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Spring Boot API Application마다 같은 스키마를 참조하는 Entity나 DTO, Repository 로직이 JDK 버전이나 Spring Boot 버전만 다를 뿐 사실상 복붙 수준으로 중복되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 처음엔 &quot;중복된 Entity와 DTO, Repository를 공통 라이브러리로 분리해서 Nexus Repository에 배포하자!&quot;라고 생각했다. 하지만 실제로 진행해보니 예상치 못한 문제들이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity, Repository를 공통화하니 각 서비스의 도메인 경계가 모호해졌고, 한 서비스에서 Entity를 수정할 때마다 다른 모든 서비스에 영향을 미치게 되었다. 또한 공통 라이브러리의 버전 관리가 생각보다 복잡했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 공통화도 마찬가지였다. API 스펙이 변경될 때마다 의존성 지옥에 빠졌고, 서비스별로 요구사항이 달라서 불필요한 필드들이 계속 추가되었다. 가장 큰 문제는 각 서비스의 독립적인 배포와 개발 사이클이 방해받는다는 점이었다. 결국 &quot;&lt;b&gt;공통화&quot;보다는 &quot;표준화&quot;가 더 나은 접근이었다는 것을 깨달았다&lt;/b&gt;.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;찍어먹어본 MSA(마이크로서비스 아키텍처)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준화를 진행하며 각각의 서비스가 가진 특징을 살려보겠다고 OpenFeign(내부 API 연동), Eureka(Service Discovery), Spring Cloud Gateway(API 게이트웨이), Spring Cloud Config(분산 환경 설정 관리) 이런&amp;nbsp;기술들도&amp;nbsp;팀에서&amp;nbsp;열심히&amp;nbsp;공부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;각 API Application을 작은 서비스로 쪼개서, 필요한 부분만 내부적으로 통신하면 중복도 자연스럽게 줄고, 서비스별로 관리도 쉬워지지 않을까?&quot; 이런 논리였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 우리가 제공하는 여러 서비스 중 한 도메인(예를 들어 '회원' 관련)만 따로 떼서 3개의 어플리케이션을 Multimodule로 구성하고, 각각의 Service를 독립적으로 띄운 후 Eureka에 등록해서 OpenFeign으로 내부 통신하는 '작은&amp;nbsp;MSA'&amp;nbsp;환경을&amp;nbsp;만들어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;오, 이거 되는 거 아냐?&quot; 실제로 내부에서 서비스간 REST 통신도 잘 되고, 각 모듈을 따로 띄웠다 내렸다 하면서 Eureka&amp;nbsp;대시보드에서&amp;nbsp;Service&amp;nbsp;Discovery도&amp;nbsp;테스트해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇데, 문제는 그 다음이었다.&lt;/b&gt; 막상 조금만 복잡한 비즈니스 로직이나 실제 운영 환경으로 생각해보면 여러 문제들이 한꺼번에 터졌다. 중복 로직을 공유하는 패키지 관리가 생각보다 어려웠고, 공통 Entity/Repository/DTO의 버전 관리 복잡성도 여전했다. 무엇보다 서비스간 통신 오버헤드와 장애 전파 문제가 심각했다. 한 서비스에 문제가 생기면 연쇄적으로 다른 서비스들까지 영향을 받았다. 그리고 배포, 롤백, 테스트 등 현실적인 운영 관리 포인트들이 오히려 더 복잡해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;중복은 계속 생기는데, 이젠 라이브러리 의존성이나 버전 관리가 오히려 더 까다로워진 것 같고, 서비스가 많아지면 모니터링이나 장애 대응도 결국 또 다른 관리 포인트가 되더라.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OpenFeign, Eureka, Spring Cloud 모두 공부와 PoC(Proof of Concept) 단계에선 신기하고 재밌었지만 &quot;이걸 우리가 지금 '우리 서비스 현실'에 바로 가져갈 수 있을까?&quot; 의문이 남았다&lt;/b&gt;.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서, 처음 고민이 뭐였지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실&amp;nbsp;우리&amp;nbsp;팀의&amp;nbsp;아키텍처&amp;nbsp;실험은&amp;nbsp;여기서&amp;nbsp;끝이&amp;nbsp;아니었다.&lt;br /&gt;MSA에 대한 기대와 환상, 그리고 실전에서 겪은 복잡함을 온몸으로 경험하고 나니,&lt;b&gt; &quot;이&amp;nbsp;많은&amp;nbsp;서비스들과&amp;nbsp;공통&amp;nbsp;로직을&amp;nbsp;효율적으로&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있는,&amp;nbsp;우리만의&amp;nbsp;확실한&amp;nbsp;기준이&amp;nbsp;필요하다&quot;&lt;/b&gt;는&amp;nbsp;이야기가&amp;nbsp;자연스럽게&amp;nbsp;나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 팀원들 사이에서 &lt;b&gt;처음부터 다시 진지하게 논의한 것&lt;/b&gt;은 &quot;&lt;b&gt;우리가 진짜로 해결하고 싶은 게 '물리적인 서비스 분리(MSA)'냐, 아니면&amp;nbsp;각자&amp;nbsp;맡은&amp;nbsp;코드의&amp;nbsp;책임과&amp;nbsp;관심사가&amp;nbsp;명확하게&amp;nbsp;분리되는&amp;nbsp;구조냐&quot;는&amp;nbsp;본질적인&amp;nbsp;질문&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 실험 과정에서 깨달은 것은, &lt;b&gt;단순히 애플리케이션을 여러 개로 나눈다고 해서 '관심사의 분리'가 저절로 따라오는 게 아니라는 점&lt;/b&gt;이었다. 공통 Entity 하나 고치려 해도 각 서비스마다 버전 관리가 따로라 실수하기 쉽고, DTO나 공통 로직을 라이브러리로 관리해도 API&amp;nbsp;응답이&amp;nbsp;달라지거나,&amp;nbsp;한&amp;nbsp;쪽에만&amp;nbsp;버그가&amp;nbsp;생기면&amp;nbsp;트러블슈팅도&amp;nbsp;배로&amp;nbsp;힘들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이런 경험 끝에, 진짜 우리가 추구해야 할 건 &lt;b&gt;서비스의 갯수나 네트워크 통신 방식이 아니라,&lt;/b&gt;&lt;b&gt;코드와 책임, 의존성, 변경의 파급 효과 자체를 '명확하게 구획하는 것'&lt;/b&gt;이라는 결론에 이르렀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다시 &quot;관심사의 분리&quot;라는 고전적인 개발 원칙을 붙잡았고, 이걸 현실적으로 실현해줄 수 있는 방법을 찾아보니 자연스럽게 &lt;b&gt;헥사고날 아키텍처(Hexagonal Architecture)&lt;/b&gt;에 눈길이 갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처를 진지하게 적용해보자는 논의가 이어졌다. 처음엔 생소할 수도 있었지만, 실제로 프로젝트에서 마주쳤던 문제들을 하나씩 대입해보면 헥사고날&amp;nbsp;아키텍처가&amp;nbsp;준&amp;nbsp;'구조적&amp;nbsp;이점'이&amp;nbsp;분명하게&amp;nbsp;보이기&amp;nbsp;시작했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리가 정의한 헥사고날 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처를 실제로 적용하면서 가장 먼저 고민했던 것은 &quot;도메인이 겹치는 N개의 API 애플리케이션을 어떻게 일관성 있게 구조화할 것인가&quot;였다. 단순히 이론적인 레이어 분리가 아니라, 실제 개발자 4명이 매일 코드를 작성하고 리뷰하면서도 헷갈리지 않을 구조를 만들어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멀티모듈을 사용한 물리적 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 하나의 애플리케이션 안에서 패키지로만 분리하려 했지만, 곧 한계를 느꼈다. 개발자들이 여전히 도메인 레이어에서 JPA Entity를 import하거나, 인프라 레이어의 구현체를 직접 참조하는 실수를 반복했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아예 물리적으로 모듈을 분리했다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;module-domain: 순수 비즈니스 로직만&lt;/li&gt;
&lt;li&gt;module-application: 유스케이스와 포트 정의&lt;/li&gt;
&lt;li&gt;module-adapter: 인바운드/아웃바운드 어댑터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니 컴파일 단계에서 의존성 위반이 바로 걸렸다. 도메인 레이어에서 JPA Entity를 import하려 하면 아예 빌드가 실패하는 구조가 된 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트/어댑터 - 추상화의 진정한 힘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 극적인 변화는 외부 연동 부분이었다. 기존에는 서비스 클래스에서 직접 JPA Repository를 호출하고, 암호화 유틸을 직접 사용했다. 요구사항이 바뀔 때마다 비즈니스 로직과 기술 구현이 뒤섞여서 수정 범위를 파악하기 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 애플리케이션 레이어에서 EncryptionPort, RepositoryPort라는 추상 인터페이스만 사용한다. 실제 구현은 인프라 레이어의 어댑터에 숨어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 개인정보 암호화 방식을 AES에서 다른 방식으로 바꾸더라도 비즈니스 로직은 전혀 건드리지 않는다. EncryptionPort의 구현체만 교체하면 된다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 리뷰의 질적 변화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 결정적인 변화는 &lt;b&gt;코드 리뷰 관점이 달라진 것&lt;/b&gt;이다. 예전엔 &quot;이 코드가 동작하나?&quot;에 집중했다면, &lt;b&gt;이제는 &quot;이 코드가 올바른 레이어에 있나?&quot;를 먼저 본다&lt;/b&gt;.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;이 validation 로직이 왜 Controller에 있지? Domain으로 내려야 하는 거 아닌가?&quot;&lt;/li&gt;
&lt;li&gt;&quot;JPA Entity의 변환 로직이 Service에 있네. Adapter로 옮겨야겠다.&quot;&lt;/li&gt;
&lt;li&gt;&quot;이 business rule이 Infrastructure layer에 있으니 Domain으로 올려야 한다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들이 자연스럽게 &quot;관심사의 분리&quot;를 체크하는 습관이 생겼다. 권한 체크, 예외 처리, 데이터 변환까지 모두 &quot;이건 어느 레이어 책임인가?&quot;를 명확하게 정할 수 있게 된 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 운영에서의 효과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처 형식의 애플리케이션을 관리하면서 &lt;b&gt;가장 큰 변화는 &quot;예측 가능성&quot;&lt;/b&gt;이었다. 새로운 기능을 추가할 때도, 버그를 수정할 때도 &lt;b&gt;&quot;어디를 봐야 하는지&quot;&lt;/b&gt; 바로 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 스키마가 바뀌면 JPA Entity와 Repository만 보면 되고, 비즈니스 규칙이 바뀌면 Domain과 Application layer만 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boolean 타입이나 암호화 처리 같은 기술적 규칙도 더 이상 &quot;팀의 고민거리&quot;가 아니라 &quot;각 레이어에서 자연스럽게 따르는 명확한 규칙&quot;이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 우리는 &lt;b&gt;헥사고날 아키텍처를 통해&lt;/b&gt; 단순히 &quot;깨끗한 코드&quot;를 얻은 게 아니라, &lt;b&gt;&quot;예측 가능하고 유지보수 가능한 시스템&quot;을 만들 수 있었다&lt;/b&gt;.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리는 헥사고날 아키텍처를 선택했다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국, 우리는 헥사고날 아키텍처를 선택했다. 그리고 그 선택은 지금도 여전히 발전 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 여러 기술 스택을 직접 손에 익혀가며, 실제로 MSA식 내부 통신 구조도 실험했다. 중복된 코드와 서비스 분리의 어려움도, 그리고 수많은 회고와 토론을 반복하면서 우리에게 가장 현실적인 해답이 무엇일지 고민했다. 그렇게 내린 결론이 바로 &amp;ldquo;이제부터 우리 서비스의 개발과 설계를 헥사고날 아키텍처를 기준으로 통일하자&amp;rdquo;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더는 각 서비스마다 어설프게 코드를 나누거나, 중복 라이브러리 때문에 복잡하게 얽히는 대신, 이제는 레이어별 역할과 책임을 명확히 구분하고, 데이터 변환, 권한 처리, 예외 처리, 네이밍 컨벤션까지 팀 전체가 합의한 원칙 아래서 표준화하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 무엇보다 중요한 것은, 이렇게 만들어진 명확한 아키텍처 기준이 우리 팀의 성장 기반이 되었다는 점이다. 이 기준 위에 실제 구현 경험과 시행착오가 차곡차곡 쌓이면서, 팀원 누구나 코드의 의도와 역할을 쉽게 이해할 수 있고, 새로운 멤버가 들어와도 자연스럽게 우리의 문화를 익혀갈 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 모든 게 끝난 것은 아니다. 실제 운영을 하다 보면 아직도 해결해야 할 고민이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티모듈 구조에서 빌드 시간이 점점 길어지는 문제, 도메인 로직과 애플리케이션 로직의 경계가 애매해지는 상황, 새로운 요구사항이 들어왔을 때 기존 아키텍처 안에서 이를 어떻게 자연스럽게 녹여낼지에 대한 고민 등은 여전히&amp;nbsp;반복되고&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 최근에는 &amp;ldquo;이 validation은 정말 도메인 규칙에 들어가야 할까, 아니면 단순히 입력값 검증에 불과한 걸까?&amp;rdquo; 같은 고민이 자주 생긴다. 또,&amp;nbsp;복잡한&amp;nbsp;비즈니스&amp;nbsp;로직이&amp;nbsp;여러&amp;nbsp;도메인에&amp;nbsp;걸쳐&amp;nbsp;분산될&amp;nbsp;때&amp;nbsp;책임&amp;nbsp;분리를&amp;nbsp;어떻게&amp;nbsp;해야&amp;nbsp;할지도&amp;nbsp;팀&amp;nbsp;내에서&amp;nbsp;계속&amp;nbsp;시행착오를&amp;nbsp;겪는&amp;nbsp;중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우리 아키텍처는 지금 이 순간에도 &amp;lsquo;진화 중&amp;rsquo;이다. 매일 점심시간 끝나기 10분 전, 누가 시키지 않아도 자리로 모여서 &amp;ldquo;아키텍처 관점에서 애매하거나 고민이 컸던 사례&amp;rdquo;를 서로 공유한다. 그 자리에서 새로운 기준이나 규칙을 제안하고, 이미&amp;nbsp;정한&amp;nbsp;규칙이라도&amp;nbsp;필요하면&amp;nbsp;기꺼이&amp;nbsp;보완해&amp;nbsp;나간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BooleanYn 타입 처리, 암호화 규칙 등도 마찬가지다. 처음엔 꽤 완벽하다고 생각했지만, 실제로 써보니 예외 케이스가 등장하고(예를 들어 BooleanYn을 도메인에서 직접 참조했는데 애그리거트 루트 참조규칙에 위반된다 생각하여 JpaConverter를 도입해서 사용중),&lt;br /&gt;그때마다&amp;nbsp;규칙을&amp;nbsp;다듬거나&amp;nbsp;더&amp;nbsp;나은&amp;nbsp;방식으로&amp;nbsp;바꿔가고&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 &amp;ldquo;완벽한 아키텍처를 찾았다&amp;rdquo;는 생각 대신, &amp;ldquo;우리 팀만의 기준을 만들고 그 기준을 계속 발전시켜가는 문화&amp;rdquo;를 갖게 된 것이 가장&amp;nbsp;큰&amp;nbsp;변화이자&amp;nbsp;성과라고&amp;nbsp;느낀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 &amp;ldquo;이건 대체 어디다 둬야 하지?&amp;rdquo;라며 막막해했다면, 지금은 &amp;ldquo;지금 기준에선 이렇게 하는 게 맞아. 만약 문제가 보이면 다음 회의에서 기준을 더 보완하자&amp;rdquo;라는 유연하고 실용적인 태도로 접근할 수 있게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헥사고날 아키텍처는 우리 팀의 최종 목적지가 아니라, 더 나은 팀으로 성장하기 위한 새로운 출발점이 되어주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &amp;ldquo;또 아키텍처를 바꾸자고..?&amp;ldquo;라는 말에 부담부터 느꼈던 내가, 지금은 오히려 &amp;ldquo;이건 이렇게 한 번 바꿔볼까요?&amp;rdquo;라며 변화를 즐기고 있다.&lt;/p&gt;</description>
      <category>생활 로그/회고록</category>
      <category>ddd 헥사고날</category>
      <category>ddd 헥사고날 실무</category>
      <category>ddd 헥사고날 예제</category>
      <category>msa 실무 적용기</category>
      <category>spring 헥사고날</category>
      <category>spring 헥사고날 구현</category>
      <category>springboot 헥사고날 ddd 구현</category>
      <category>springboot로 헥사고날</category>
      <category>헥사고날 spring 구현</category>
      <category>헥사고날 실무 적용</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/355</guid>
      <comments>https://min-nine.tistory.com/entry/%EB%B3%B5%EB%B6%99-%EC%A7%80%EC%98%A5%EC%97%90%EC%84%9C-%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0%EA%B9%8C%EC%A7%80-%E2%80%93-%ED%98%84%EC%8B%A4-%EA%B0%9C%EB%B0%9C%ED%8C%80%EC%9D%98-%EC%84%B1%EC%9E%A5-%EA%B8%B0%EB%A1%9D#entry355comment</comments>
      <pubDate>Tue, 8 Jul 2025 12:41:51 +0900</pubDate>
    </item>
    <item>
      <title>JPA에서 N+1 문제를 어떻게 해결하고 계신가요?</title>
      <link>https://min-nine.tistory.com/entry/JPA%EC%97%90%EC%84%9C-N1-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B3%A0-%EA%B3%84%EC%8B%A0%EA%B0%80%EC%9A%94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;최근 타사 개발팀 팀장님과 대화를 할 기회가 생겼다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화 중 &amp;ldquo;JPA에서 N+1 문제를 어떻게 해결하고 계신가요?&amp;rdquo;라는 질문을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연차가 쌓이면 이런 기초적인 질문을 받는 기회가 적어지기 때문에 굉장히 참신하게 다가온 질문이였고, 내가 오랜 시간 ORM을 사용하며 고민하고 실천해온 철학을 되돌아보게 만들었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;ldquo;저는 연관관계를 무작정 맺지 않습니다. 예를 들어 이미지나 파일과 같은 엔티티는 ComAttachFile처럼 별도로 두고, 직접 연관관계를 맺어 데이터를 수정하지 않습니다.&lt;br /&gt;@ManyToOne(fetch&amp;nbsp;=&amp;nbsp;LAZY)와&amp;nbsp;insertable&amp;nbsp;=&amp;nbsp;false,&amp;nbsp;updatable&amp;nbsp;=&amp;nbsp;false&amp;nbsp;조합으로&amp;nbsp;읽기&amp;nbsp;전용&amp;nbsp;단방향&amp;nbsp;관계를&amp;nbsp;걸어두고,&amp;nbsp;실질적인&amp;nbsp;DB&amp;nbsp;쓰기는&amp;nbsp;식별자&amp;nbsp;컬럼을&amp;nbsp;통해&amp;nbsp;처리합니다.&lt;br /&gt;조회&amp;nbsp;시에는&amp;nbsp;Projection이나&amp;nbsp;DTO&amp;nbsp;기반으로&amp;nbsp;필요한&amp;nbsp;정보만&amp;nbsp;명확하게&amp;nbsp;가져오도록&amp;nbsp;쿼리를&amp;nbsp;분리합니다.&amp;rdquo;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이&amp;nbsp;방식은&lt;/b&gt;&amp;nbsp;&amp;lsquo;내가&amp;nbsp;만든&amp;nbsp;엔티티를&amp;nbsp;누가,&amp;nbsp;어떤&amp;nbsp;방식으로&amp;nbsp;사용할지까지는&amp;nbsp;예측할&amp;nbsp;수&amp;nbsp;없다&amp;rsquo;는&amp;nbsp;가정&amp;nbsp;하에&lt;br /&gt;불필요한&amp;nbsp;의존성을&amp;nbsp;최소화하고,&amp;nbsp;지연&amp;nbsp;로딩에&amp;nbsp;의한&amp;nbsp;N+1&amp;nbsp;문제를&amp;nbsp;사전에&amp;nbsp;방지하기&amp;nbsp;위한&amp;nbsp;&lt;b&gt;일종의&amp;nbsp;방어적인&amp;nbsp;설계다.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;예전에는&amp;nbsp;JPA의&amp;nbsp;연관관계를&amp;nbsp;그대로&amp;nbsp;활용해&amp;nbsp;코드를&amp;nbsp;우아하게&amp;nbsp;구성하고&amp;nbsp;싶었다.&lt;br /&gt;하지만&amp;nbsp;연관관계를&amp;nbsp;깊게&amp;nbsp;맺어두면,&amp;nbsp;의도치&amp;nbsp;않은&amp;nbsp;지연&amp;nbsp;로딩으로&amp;nbsp;다수의&amp;nbsp;쿼리가&amp;nbsp;발생하고,&amp;nbsp;실무에서는&amp;nbsp;그게&amp;nbsp;곧&amp;nbsp;성능&amp;nbsp;이슈로&amp;nbsp;이어졌다.&lt;br /&gt;그래서&amp;nbsp;지금은&amp;nbsp;DB&amp;nbsp;저장은&amp;nbsp;식별자(ID)&amp;nbsp;기반으로&amp;nbsp;처리하고,&amp;nbsp;연관&amp;nbsp;객체는&amp;nbsp;조회&amp;nbsp;용도로만&amp;nbsp;명확하게&amp;nbsp;사용하는&amp;nbsp;패턴을&amp;nbsp;주로&amp;nbsp;사용하고&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그 팀장님의 반응은 다소 당황스러웠다. 마치 &lt;b&gt;&amp;ldquo;그건 좋은 접근은 아닌데요?&amp;ldquo;라는 인상을 받았기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 나는 이렇게 설계하게 되었는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의&amp;nbsp;가장&amp;nbsp;큰&amp;nbsp;장점은&amp;nbsp;객체&amp;nbsp;간&amp;nbsp;연관관계를&amp;nbsp;통해&amp;nbsp;도메인&amp;nbsp;모델을&amp;nbsp;풍부하게&amp;nbsp;표현할&amp;nbsp;수&amp;nbsp;있다는&amp;nbsp;점이다.&lt;br /&gt;하지만&amp;nbsp;그&amp;nbsp;강점은&amp;nbsp;동시에&amp;nbsp;가장&amp;nbsp;큰&amp;nbsp;위험이기도&amp;nbsp;하다.&lt;br /&gt;JPA의 연관관계는 결국 SQL 쿼리로 변환되며, 이를 제대로 통제하지 못하면 지연 로딩이 반복되면서 N+1 문제가 터지기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;특히&amp;nbsp;&lt;b&gt;엔티티를&amp;nbsp;여러&amp;nbsp;사람이&amp;nbsp;함께&amp;nbsp;사용하는&amp;nbsp;팀&amp;nbsp;환경에서&lt;/b&gt;는&lt;br /&gt;내가&lt;b&gt; 연관관계를 맺어둔 엔티티가 어디선가 반복 호출되며 성능 병목을 유발하는 사례를 여럿 경험&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서 나는 다음 &lt;b&gt;두 가지 원칙&lt;/b&gt;을 실천하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터 저장은 ID 기반으로 하고, 연관 객체는 조회용으로만 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; @ManyToOne(fetch = LAZY) + insertable = false, updatable = false 조합으로 연관 객체를 읽기 전용으로 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;조회 시에는 명확한 쿼리와 Projection 기반으로 필요한 정보만 가져오기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr;&amp;nbsp;필요한&amp;nbsp;관계만&amp;nbsp;fetch&amp;nbsp;join&amp;nbsp;또는&amp;nbsp;DTO&amp;nbsp;형태로&amp;nbsp;명시적으로&amp;nbsp;로딩&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 생각하는 N+1 문제의 실질적 해결책 몇가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 JPA와 같은 ORM에서는 N+1 문제를 완전히 제거하기는 어렵다. 하지만 다음과 같은 전략을 병행하면 대부분의 문제를 효과적으로 예방할 수 있다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째는 Fetch Join을 명시적으로 사용하는 것.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch Join은 N+1 문제를 해결하는 가장 대표적이고 간단한 방법이다. JPA 지연 로딩(LAZY)은 기본적으로 성능 최적화에 유력하지만, 조회 대상이 다수의 연관 엔티티를 포함할 때는 각 엔티티마다 추가 쿼리가 발생하며 N+1 문제가 발생하기 때문인데 이를 하나의 쿼리로 연관된 엔티티까지 함께 조회하는 Fetch Join을 사용하여 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만, Fetch Join을 무조건 남용해서는 안된다. 1:N 관계를 Fetch Join 할 경우 결과가 곱집합(Cartesian Product)처럼 늘어나게되어 성능 저하나 ㅍㅔ이징 실패 등의 부작용이 발생되는 것을 많이 봐왔기 때문.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 1:1, N:1 관계에서는 Fetch Join을 사용하고 1:N 관계에서는 필요한 경우에 QeuryDSL의 fetchJoin() + distinct() 조합이나 EntityGraph 등을 대체로 고려해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째는 읽기 전용 연관관계를 통해 저장과 조회를 분리하는 등 Entity 설계를 신중히 하는 것.&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 저장은 colum 으로 처리하고 객체 참조는 Domain 객체로 읽기 전용 조회를 하는 것, 내가 선호하는 방식이다. 관계는 맺되, 쓰기 연산은 막아두어 예상할 수 없는 N+1을 방지하는 방법이다. 내 생각에는 이 방식이 연관관계를 완전히 없애지 않으면서도, 설계의 명확성과 성능 안정성을 동시에 추구할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1751603784499&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class JpaComAttachFileEntity {
    @Id
    @Comment(&quot;FILE_ID&quot;)
    @Column(name = &quot;file_id&quot;, columnDefinition = &quot;BINARY(16)&quot;)
    private UUID fileId;
}

## 나는 되도록 연관관계를 맵핑지어 사용하지 않는다.
class JpaUserEntity {
    ## 무조건 User 데이터를 가져올 때 profileImageFile은 꼭 가져온다면 아래와 같이 사용한다.
    @Comment(&quot;프로필 이미지 FILE_ID&quot;)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;profile_image_file_id&quot;, insertable = false, updatable = false)
    private JpaComAttachFileEntity profileImageFile;
    ## 아래 Image는 특정 API에서만 select함으로 연관관계를 설정하지 않는다.
    private UUID mainImageFileId;
    private UUID introImageFileId;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계를 너무 객체지향적으로 맺는 것은 오히려 퍼포먼스를 망칠 수 있고 특히 이미지, 첨부파일, 로그 등과 같은 다양한 도메인에서 참조되는 엔티티는 연관관계를 맺지 않고 ID로만 들고 있도록 하고있다. 이는 단순히 N+1을 방지하기 위함이 아니라, 도메인의 독립성과 책임을 분리를 명확히 하려는 목적(이 개념은 DDD의 Aggregate Root간 ID 참조 규칙과도 맞닿아 있음)이기도 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세 번째로 쿼리 전용 DTO 혹은 Projection을 활용하는 것.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스/프론트에&amp;nbsp;필요한&amp;nbsp;데이터는&amp;nbsp;엔티티가&amp;nbsp;아닌&amp;nbsp;전용&amp;nbsp;DTO나&amp;nbsp;Projection으로&amp;nbsp;받아오는&amp;nbsp;것이&amp;nbsp;N+1&amp;nbsp;회피와&amp;nbsp;성능&amp;nbsp;측면에서&amp;nbsp;훨씬&amp;nbsp;유리하다.&lt;br /&gt;Spring Data JPA에서는 Interface 기반 Projection 또는 Class 기반 DTO Projection이 모두 가능하다. QueryDSL&amp;nbsp;사용&amp;nbsp;시&amp;nbsp;더&amp;nbsp;유연한&amp;nbsp;DTO&amp;nbsp;Projection이&amp;nbsp;가능하며,&amp;nbsp;성능&amp;nbsp;측정&amp;nbsp;도구와의&amp;nbsp;궁합도&amp;nbsp;좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityGraph, BatchSize 등의 어노테이션 기능들을 활용해서 쿼리 정의 없이 특정 연관 엔티티를 함께 로딩하거나 여러 엔티티를 batch select 하는 방법 등도 있지만 나는 위의 세 가지 방법을 주로 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 틀렸던 부분이 무엇일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌이켜보면, 그 팀장님의 반응은&lt;b&gt; &quot;연관관계를 너무 배제하면 JPA의 철학을 무시하는 것 아닌가?&quot;&lt;/b&gt; 라는 우려였을 수도 있겠다. 실제로 JPA의 철학은 객체 그래프 탐색을 통해 풍부한 도메인 모델을 표현하고, 애플리케이션 코드에서 관계를 따라가며 로직을 구현하는 것에 있을테니깐.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 장점은 팀 전체가 ORM의 동작 방식, 영속성 컨텍스트, 지연 로딩 등 개념에 대한 공통된 이해를 바탕으로 할 때 비로소 빛을 발한다고 생각하고, 유지보수 난이도와 복잡도 증가를 감안하면 의도적인 ID 기반 저장과 제한적 조회 방식을 사용하는 현실적인 절충안이 충분히 가치 있는 선택이라는게 내 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 대화를 계기로 다시 한 번 ORM과 연관관계의 본질에 대한 고민을 하게 되었다. 항상 느끼는 것이지만, 개발에 있어서 정답은 없는 것 같다. 마치 서울에서 부산으로 가는 방법이 수많은 것과 같이.&lt;/p&gt;</description>
      <category>생활 로그/회고록</category>
      <category>jpa n+1</category>
      <category>JPA N+1 문제</category>
      <category>jpa n+1 질문</category>
      <category>jpa n+1 해결</category>
      <category>jpa n+1문제</category>
      <category>jpa 면접질문</category>
      <category>jpa 양방향 n+1</category>
      <category>jpa 연간관계 질문</category>
      <category>N+1 문제</category>
      <category>n+1 문제 해결</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/354</guid>
      <comments>https://min-nine.tistory.com/entry/JPA%EC%97%90%EC%84%9C-N1-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B3%A0-%EA%B3%84%EC%8B%A0%EA%B0%80%EC%9A%94#entry354comment</comments>
      <pubDate>Fri, 4 Jul 2025 13:56:06 +0900</pubDate>
    </item>
    <item>
      <title>나는 이제, 기술자가 되고 싶다</title>
      <link>https://min-nine.tistory.com/entry/%EB%82%98%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EA%B8%B0%EC%88%A0%EC%9E%90%EA%B0%80-%EB%90%98%EA%B3%A0-%EC%8B%B6%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2018년&amp;nbsp;11월,&amp;nbsp;나는&amp;nbsp;처음으로&amp;nbsp;PHP&amp;nbsp;웹&amp;nbsp;개발자의&amp;nbsp;직무를&amp;nbsp;맡으며&amp;nbsp;당당하게&amp;nbsp;&amp;ldquo;저는&amp;nbsp;개발자입니다&amp;rdquo;라고&amp;nbsp;말할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;되었다.&lt;br /&gt;서울 소재 전문학사 졸업생이었고, 연봉은 2,800만 원. 지금 생각해도 결코 화려한 시작은 아니었다.&lt;br /&gt;&lt;br /&gt;Java&amp;nbsp;Spring이&amp;nbsp;주류인&amp;nbsp;대한민국&amp;nbsp;개발&amp;nbsp;생태계에서,&amp;nbsp;나의&amp;nbsp;기술&amp;nbsp;스택은&amp;nbsp;주변에서&amp;nbsp;&amp;lsquo;비주류&amp;rsquo;에&amp;nbsp;가까웠다.&lt;br /&gt;그래서 오히려 신입의 진입장벽이 낮은 PHP와 Laravel을 택했고, 운이 좋게도 한국을 대표하는 메이저 언론사에서 커리어를 시작할 수 있었다.&lt;br /&gt;&lt;br /&gt;당시&amp;nbsp;25살.&amp;nbsp;나보다&amp;nbsp;열&amp;nbsp;살&amp;nbsp;이상&amp;nbsp;많은&amp;nbsp;선배들과의&amp;nbsp;협업은&amp;nbsp;낯설고&amp;nbsp;긴장의&amp;nbsp;연속이었다.&lt;br /&gt;네이트온에 &amp;ldquo;출근했습니다&amp;rdquo;라고 인사하는 것조차 어색하던 시절,&lt;br /&gt;&amp;ldquo;이 코드가 맞나?&amp;rdquo; 싶으면서도 용기 내어 푸시하고, 떨리는 손으로 배포 버튼을 눌렀다.&lt;br /&gt;그렇게 나는 매일 눈앞의 기능을 구현하며, 버그를 수정하고, 요청된 작업을 해결하는 데 집중하며 하루하루를 보냈다.&lt;br /&gt;&lt;br /&gt;하지만 어느 순간부터 개발이라는 일이 단순히 기능을 만드는 것을 넘어서고 있다는 걸 느끼기 시작했다.&lt;br /&gt;지하철에서 누군가 내가 만든 서비스를 보고 있는 모습을 우연히 마주쳤을 때,&lt;br /&gt;말로&amp;nbsp;설명할&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;뿌듯함과&amp;nbsp;고양감이&amp;nbsp;밀려왔다.&lt;br /&gt;&lt;br /&gt;&amp;ldquo;내가&amp;nbsp;만든&amp;nbsp;게,&amp;nbsp;누군가의&amp;nbsp;하루를&amp;nbsp;채우고&amp;nbsp;있구나.&amp;rdquo;&lt;br /&gt;&lt;br /&gt;그 짧은 순간이 나에게는 개발자로서 처음 느껴본 자부심이었고, 그 기분은 지금도 기억에 남는다.&lt;br /&gt;그 경험 이후로 나는 단지 &amp;lsquo;코드를 짜는 사람&amp;rsquo;이 아니라, 사람들이 사용하는 무언가를 만드는 사람이라는 자각을 갖기 시작했다.&lt;br /&gt;&lt;br /&gt;PHP Laravel Web 개발자로 시작한 나의 백엔드 커리어는 2022년 4월, 금융&amp;middot;증권 전문 언론사로의 이직과 함께 새로운 전환점을 맞이했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 뉴스 서비스의 기능을 고도화하고 운영하는 과정에서, Legacy PHP를 Laravel 기반의 MVC 구조로 리팩토링하거나 RestAPI로 리팩토링 하였고, Redis를 활용해 조회수 어뷰징을 방지하는 로직도 직접 설계했다.&lt;br /&gt;&lt;br /&gt;이 시기부터 나는 매일같이 자문했다. &amp;ldquo;왜 이렇게 되어 있을까?&amp;rdquo;, &amp;ldquo;더 나은 방법은 없을까?&amp;rdquo;&lt;br /&gt;&lt;br /&gt;기술에 대한 갈증이 커졌고, 나는 주도적으로 다양한 티켓을 가져와 새로운 기술을 탐색하고 적용했다.&lt;br /&gt;PHP Laravel만 사용하던 내가 Java, EDA를 활용한 FCM Push 전송 프로그램을 만들기도 하였고, Swift를 사용해 2개의 ios 앱을 신규 개발하기도 하였다.&lt;br /&gt;&lt;br /&gt;언어나 도구가 달라져도 문제를 해결하는 방식의 본질은 같다는 것을 느끼며, 새로운 기술을 익히는 데 점점 두려움이 사라졌다.&lt;br /&gt;이후에는 단순히 코드를 작성하는 것을 넘어,&lt;br /&gt;서비스의 구조는 어떻게 설계되어야 하는지, 리소스를 얼마나 효율적으로 사용할 수 있을지,&lt;br /&gt;배포는 얼마나 안전하게 자동화할 수 있을지, 장애가 나면 어떻게 대응할 수 있을지,&lt;br /&gt;그&amp;nbsp;모든&amp;nbsp;것들을&amp;nbsp;고민하게&amp;nbsp;되었다.&lt;br /&gt;&lt;br /&gt;자연스럽게&amp;nbsp;DevOps,&amp;nbsp;GitOps,&amp;nbsp;클라우드&amp;nbsp;네이티브&amp;nbsp;환경,&amp;nbsp;멀티&amp;nbsp;모듈&amp;nbsp;구조,&amp;nbsp;헥사고날&amp;nbsp;아키텍처&amp;nbsp;같은&amp;nbsp;키워드들이&amp;nbsp;내&amp;nbsp;일상&amp;nbsp;속으로&amp;nbsp;들어왔다.&lt;br /&gt;이 기술들을 단순히 트렌드로 받아들인 게 아니라, 실제로 팀과 서비스에 가치를 더하는 도구로 만들기 위해 스스로 실험하고, 적용하고, 설명하며 설득했다.&lt;br /&gt;&lt;br /&gt;그러면서 점점 더 강하게 깨달았다.&lt;br /&gt;&amp;ldquo;기술을 잘 쓰는 것만으로 좋은 개발자라고 할 수 있을까?&amp;rdquo;&lt;br /&gt;&amp;ldquo;설계를 잘하면, 그게 전부일까?&amp;rdquo;&lt;br /&gt;&lt;br /&gt;결국&amp;nbsp;기술의&amp;nbsp;시작과&amp;nbsp;끝에는&amp;nbsp;&amp;lsquo;사람&amp;rsquo;이&amp;nbsp;있었다.&lt;br /&gt;기획자, 디자이너, 운영자, 그리고 사용자.&lt;br /&gt;그들과 함께 문제를 정의하고, 해답을 만들어가는 커뮤니케이터이자 연결자.&lt;br /&gt;그게&amp;nbsp;진짜&amp;nbsp;내가&amp;nbsp;되고&amp;nbsp;싶은&amp;nbsp;개발자의&amp;nbsp;모습이라는&amp;nbsp;생각이&amp;nbsp;들었다.&lt;br /&gt;&lt;br /&gt;이젠, 내가 그 기술자의 모습에 가까워지고자 한다.&lt;br /&gt;아직은 미숙하고, 잘 안 될 수도 있다. 낯설고, 어려울 수도 있다.&lt;br /&gt;하지만 해보려고 한다.&lt;br /&gt;7년 전, 개발자란 직업이 너무 낯설고 선배들과의 협업이 두려웠던 내가 결국 그 시절을 지나왔듯이,&lt;br /&gt;앞으로도 그렇게 흘러갈 것이라고 믿는다.&lt;/p&gt;</description>
      <category>생활 로그/회고록</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/353</guid>
      <comments>https://min-nine.tistory.com/entry/%EB%82%98%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EA%B8%B0%EC%88%A0%EC%9E%90%EA%B0%80-%EB%90%98%EA%B3%A0-%EC%8B%B6%EB%8B%A4#entry353comment</comments>
      <pubDate>Fri, 4 Jul 2025 09:35:37 +0900</pubDate>
    </item>
    <item>
      <title>대규모 데이터에서 페이징 조회 쿼리 튜닝해보기 - 인덱스 사용법</title>
      <link>https://min-nine.tistory.com/entry/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%A1%B0%ED%9A%8C-%EC%BF%BC%EB%A6%AC-%ED%8A%9C%EB%8B%9D%ED%95%B4%EB%B3%B4%EA%B8%B0-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 대규모 시스템 설계에 대한 스터디를 진행하면서, DB에서 데이터를 조회해올 때 어떤 방법들을 사용하여 효율적으로 튜닝할 수 있는지 학습하였고, 해당 내용을 포스팅니다. 이번 포스팅에서는 인덱스가 조회쿼리에 얼마나 큰 영향을 줄 수있는지 확인해보도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 실행 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 mysql:latest 버전을 사용하여 docker로 구동시켰습니다. 포스팅 기준 9.3.0-1 버전임으로 참고 부탁드립니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2686&quot; data-origin-height=&quot;1112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/COKKw/btsOMlHJZdn/Vb5O2uTYGtaYCZKEhHsT5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/COKKw/btsOMlHJZdn/Vb5O2uTYGtaYCZKEhHsT5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/COKKw/btsOMlHJZdn/Vb5O2uTYGtaYCZKEhHsT5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCOKKw%2FbtsOMlHJZdn%2FVb5O2uTYGtaYCZKEhHsT5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2686&quot; height=&quot;1112&quot; data-origin-width=&quot;2686&quot; data-origin-height=&quot;1112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;article 이라는 테이블을 생성하였고, 해당 테이블에는 사전에 1200만건의 가짜 데이터를 삽입해두었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750640061158&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE `article` (
  `article_id` bigint NOT NULL,
  `title` varchar(100) NOT NULL,
  `content` varchar(3000) NOT NULL,
  `board_id` bigint NOT NULL,
  `writer_id` bigint NOT NULL,
  `created_at` datetime NOT NULL,
  `modified_at` datetime NOT NULL,
  PRIMARY KEY (`article_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count 조회만 했음에도 2.06초 이상이 걸리는것을 아래와 같이 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;115&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNCksw/btsOLDbcrDo/9NpUTUiucRLJpPviR3kIak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNCksw/btsOLDbcrDo/9NpUTUiucRLJpPviR3kIak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNCksw/btsOLDbcrDo/9NpUTUiucRLJpPviR3kIak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNCksw%2FbtsOLDbcrDo%2F9NpUTUiucRLJpPviR3kIak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;115&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;115&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 페이징 조회 쿼리 및 explain 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;limit과 offset을 사용한 페이징 쿼리를 실행해봅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750640432972&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from article where board_id=1 order by created_at desc limit 30 offset 90;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1200만건이라는 크지 않은 규모의 데이터임에도 페이징 조회 쿼리 한번에 3.4초 이상이 걸림을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brvxRh/btsOMSd6tnR/FgqhpYjPAKoTAtMkd7Vpz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brvxRh/btsOMSd6tnR/FgqhpYjPAKoTAtMkd7Vpz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brvxRh/btsOMSd6tnR/FgqhpYjPAKoTAtMkd7Vpz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrvxRh%2FbtsOMSd6tnR%2FFgqhpYjPAKoTAtMkd7Vpz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;519&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;519&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같은 조회쿼리 앞에 explain 키워드를 입력하면 해당 쿼리의 실행계획을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750640579925&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain select * from article where board_id=1 order by created_at desc limit 30 offset 90;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfOM6/btsOLQBj2j4/JhrO24K7ewRnKlawWQIwik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfOM6/btsOLQBj2j4/JhrO24K7ewRnKlawWQIwik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfOM6/btsOLQBj2j4/JhrO24K7ewRnKlawWQIwik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfOM6%2FbtsOLQBj2j4%2FJhrO24K7ewRnKlawWQIwik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;869&quot; height=&quot;105&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;type이 ALL로&amp;nbsp; 표기되어있는데, 이는 테이블 전체를 읽는 풀스캔이 이뤄진다는 뜻이고, Extra의 Using filesort는 데이터가 메모리에서 정렬을 수행할 수 없을 정도가 되기 때문에 파일디스크에서 데이터를 정렬함을 의미합니다. 때문에 단순한 조회임에도 3.4초라는 시간이 걸리는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Index 생성 후 페이징 조회 쿼리 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 생성해봅니다. board_id와 article_id를 기준으로 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750640360544&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create index indx_board_id_article_id on article(board_id asc, article_id desc);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFlfuE/btsONbdsfCd/vblgKQkyCHf6kHNA9Wc5R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFlfuE/btsONbdsfCd/vblgKQkyCHf6kHNA9Wc5R1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFlfuE/btsONbdsfCd/vblgKQkyCHf6kHNA9Wc5R1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFlfuE%2FbtsONbdsfCd%2FvblgKQkyCHf6kHNA9Wc5R1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;535&quot; height=&quot;51&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 1200만건이라는 데이터를 삽입해두었기 때문에 단순한 인덱스 생성에도 13.8초라는 시간이 걸립니다. 현업에서 운영하는 테이블에 인덱스를 추가 및 수정할때는 서비스 무중단 플랜을 잘 세워둔 후 작업하는 것을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 explain 명령을 통해 인덱스 생성 전과 같은 쿼리플랜을 출력해봅니다. order by만 article_id로 바꿔줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750640807672&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;explain select * from article where board_id=1 order by article_id desc limit 30 offset 90;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caTMoo/btsOMOitlPq/wfdwufLpDrNRXIEzhVdHA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caTMoo/btsOMOitlPq/wfdwufLpDrNRXIEzhVdHA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caTMoo/btsOMOitlPq/wfdwufLpDrNRXIEzhVdHA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaTMoo%2FbtsOMOitlPq%2FwfdwufLpDrNRXIEzhVdHA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;919&quot; height=&quot;106&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;type이 ref로 바뀌었고, possible_keys와 key 값에 우리가 추가한 인덱스가 보이는 것이 확인됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;513&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxVJtb/btsOMPPgHNX/2mSEosMnIvQChWCbqPnCb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxVJtb/btsOMPPgHNX/2mSEosMnIvQChWCbqPnCb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxVJtb/btsOMPPgHNX/2mSEosMnIvQChWCbqPnCb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxVJtb%2FbtsOMPPgHNX%2F2mSEosMnIvQChWCbqPnCb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;703&quot; height=&quot;513&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;513&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 내용의 쿼리가 기존 인덱스 적용 전에 3.4초에서 0.1초로 3.3초 이상이 줄어든 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. offset 기준 페이지네이션 조회쿼리의 단점 파악 - 인덱스에 대한 이해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 offset을 1499970으로 주고 조회를 다시 시작해봅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750642935873&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from article where board_id=1 order by article_id desc limit 30 offset 1499970;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 쿼리 실행 시간이 1.9초가 넘어가는 것을 확인할 수 있습니다. 여기서 인덱스에 대한 이해가 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b26D0j/btsOLoynvi1/56C51p2nb4c5NhtfInKCh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b26D0j/btsOLoynvi1/56C51p2nb4c5NhtfInKCh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b26D0j/btsOLoynvi1/56C51p2nb4c5NhtfInKCh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb26D0j%2FbtsOLoynvi1%2F56C51p2nb4c5NhtfInKCh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;512&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mysql의 기본 스토리지 엔진은 InnoDB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mysql에서 사용하는 기본 스토리지 엔진은 InnoDB입니다. 해당 엔진은 테이블마다 Clustered Index를 자동으로 생성하게 되는데 이는 PrimaryKey를 기준으로 생성되기 때문에 Primary key를 이용한 조회는 자동으로 생성된 Clustered Index로 수행되는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 별도로 인덱스 생성한 것이 아님에도 불구하고 PrimaryKey에 자동으로 생성된 Clustered Index가 사용된 것을 아래와 같이 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cm0itt/btsOLoynx9H/hSRe8sVf2p6RQI1Fh57aL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cm0itt/btsOLoynx9H/hSRe8sVf2p6RQI1Fh57aL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cm0itt/btsOLoynx9H/hSRe8sVf2p6RQI1Fh57aL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcm0itt%2FbtsOLoynx9H%2FhSRe8sVf2p6RQI1Fh57aL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;121&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;121&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 별도로 생성한 인덱스는 Secondary Index(보조 인덱스) 혹은 Non-Clustered Index라고 불리는데, Clustered Index와는 다르게 leaf node에 직접적인 데이터를 담고있는 것이 아니라, 인덱스 컬럼 데이터 및&lt;u&gt;&lt;b&gt; 데이터에 접근하기 위한 포인터&lt;/b&gt;&lt;/u&gt;를 가지게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2058&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pGFDw/btsOM5qOz9c/XMmK7lVvQe6JvGfN1WKwkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pGFDw/btsOM5qOz9c/XMmK7lVvQe6JvGfN1WKwkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pGFDw/btsOM5qOz9c/XMmK7lVvQe6JvGfN1WKwkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpGFDw%2FbtsOM5qOz9c%2FXMmK7lVvQe6JvGfN1WKwkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2058&quot; height=&quot;776&quot; data-origin-width=&quot;2058&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clustered Index와 Secondary Index를 한눈에 비교해봅니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.938%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;Clustered Index&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;Secondary Index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.938%;&quot;&gt;생성&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;테이블의 Primary Key로 자동생성&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;테이블의 컬럼으로 직접 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.938%;&quot;&gt;데이터&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;행 데이터(row data)&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;데이터에 접근하기 위한 포인터, 인덱스 컬럼 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.938%;&quot;&gt;개수&lt;/td&gt;
&lt;td style=&quot;width: 38.7984%;&quot;&gt;테이블당 1개&lt;/td&gt;
&lt;td style=&quot;width: 44.2635%;&quot;&gt;테이블당 여러 개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, Secondary Index를 이용한 데이터 조회는 Secondary Index에서 데이터에 접근하기 위한 포인터를 찾은 후, Clustered Index에서 데이터를 찾는 인덱스 트리를 두 번 타게되는 것입니다. 이 개념을 바탕으로 페이징 쿼리를 다시 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750643820316&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from article where board_id=1 order by article_id desc limit 30 offset 1499970;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;board_id, article_id에 생성된 Secondary Index에서 article_id를 찾은 후, Clustered Index에서 article 데이터를 찾으며 offset 1499970을 만날 때 까지 반복하며 skip을 하고 limit 30개를 추출하는 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터는 offset 1499970부터 30개만 필요한데, 해당 offset을 만날 때 까지 데이터에 접근하고 있는 비효율적이고 무의마한 과정이 생긴다는 것 입니다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 만든 Secondary Index는 board_id와 article_id를 포함하고 있습니다. 그렇다면 Secondary Index에서 필요한 30건에 대해서 article_id만 먼저 추출하고 그 30건에 대해서만 Clustered Index에 접근하면 충분하지 않을까 라는 생각을 하게됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조회 컬럼의 지정 - Covering Index&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Select 해오는 컬럼 자체를 board_id, article_id 2개만 추출하도록 바꿔서 쿼리를 수행해봅니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750644113481&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select board_id,article_id from article where board_id=1 order by article_id desc limit 30 offset 1499970;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVeh9h/btsOKQoGnXz/WLrs2aoNk54ikPZ7cBDC3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVeh9h/btsOKQoGnXz/WLrs2aoNk54ikPZ7cBDC3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVeh9h/btsOKQoGnXz/WLrs2aoNk54ikPZ7cBDC3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVeh9h%2FbtsOKQoGnXz%2FWLrs2aoNk54ikPZ7cBDC3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;697&quot; height=&quot;512&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 데이터를 가져올 때는 약 1.n초 이상이 소요되었는데, board_id 및 article_id만 추출하는 것은 0.3초로 소요시간이 많이 줄어들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjVApA/btsONMxJRYO/Mt9lo0Tz6uvj44kLV7hKrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjVApA/btsONMxJRYO/Mt9lo0Tz6uvj44kLV7hKrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjVApA/btsONMxJRYO/Mt9lo0Tz6uvj44kLV7hKrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjVApA%2FbtsONMxJRYO%2FMt9lo0Tz6uvj44kLV7hKrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;955&quot; height=&quot;109&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;explain을 사용해 쿼리 플랜을 살펴보면 인덱스는 동일하게 사용되었는데, Extra=Using index 내용이 추가되었음을 확인하게 됩니다. 인덱스만 사용해서 데이터를 조회했음을 의미하는데 이렇게 &lt;u&gt;&lt;b&gt;인덱스의 데이터만으로 조회를 수행할 수 있는 인덱스를 Covering Index라고 합니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Covering Index란 인덱스만으로 쿼리의 모든 데이터를 처리할 수 있는 인덱스로, Clustered Index를 읽지 않고 Secondary Index가 포함된 정보만으로 쿼리 사용이 가능하게 하는 인덱스를 뜻합니다. 용어가 너무 어렵게 느껴진다면 이렇게 생각하면 편합니다. 우리가 인덱스를 생성할 때 지정한 컬럼들이 Covering Index가 된다고 생각하면 말이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 추출된 30건의 article_id에 대해서만 Clustered Index에 접근하게 쿼리를 수정하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750644502601&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from (
 select article_id from article where board_id = 1 order by article_id desc limit 30 offset 1499970
) t left join article on t.article_id = article.article_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BwPQ6/btsOLzmnc4I/K6l5XE7tOD0K7o7Tr71AWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BwPQ6/btsOLzmnc4I/K6l5XE7tOD0K7o7Tr71AWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BwPQ6/btsOLzmnc4I/K6l5XE7tOD0K7o7Tr71AWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBwPQ6%2FbtsOLzmnc4I%2FK6l5XE7tOD0K7o7Tr71AWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;798&quot; height=&quot;537&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 원하는 offset까지 계속 스킵된 시간을 줄여서 2초가 넘어가던 쿼리를 다시한번 0.2초대로 단축시키는 것이 가능해집니다. 여기서 끝일까요???&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 대용량 대이터 기준 마지막 페이지 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 사용한 offset 1499970은 대략 5000번째 페이지를 뜻합니다. 하지만 이게 더 뒤로가서 30000번째 페이징이 되었을 경우는 또 어떨지 조회해봅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750644857141&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from ( select article_id from article where board_id = 1 order by article_id desc limit 30 offset 8999970 ) t left join article on t.article_id = article.article_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ea6bn3/btsOL645i8z/EK4IWSQAUrkmQCCCxSSGF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ea6bn3/btsOL645i8z/EK4IWSQAUrkmQCCCxSSGF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ea6bn3/btsOL645i8z/EK4IWSQAUrkmQCCCxSSGF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fea6bn3%2FbtsOL645i8z%2FEK4IWSQAUrkmQCCCxSSGF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1072&quot; height=&quot;528&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다시 쿼리 수행 시간이 1초 이상이 넘어가게 됩니다. Covering Index를 가지고 Clustered Index에서 데이터를 가져오는건 똑같은데, 뒷 페이지로 갈 수록 속도가 느려지는 현상은 여전합니다. 왜 그럴까 생각해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 article_id (Covering Index) 추출을 위해 Secondary Index만 조회된다고 하더라도, offset 만큼 Index Scan이 필요하게 되는 것입니다.즉, Clustered Index의 데이터에 직접 접근하지 않아도 offset이 늘어날 수록 느려질 수 밖에 없는 상황인 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 방법은 다양합니다. 게시글을 1년 단위로 테이블을 분리하여 개별 테이블의 크기를 작게 만들어 각 단위에 대해 전체 게시글 수를 관리하는 방법도 있고, offset을 인덱스 페이지 단위 skip하는 것이 아니라, 1년 동안 작성된 게시글 수 단위로 skip하는 여러가지 검색 조건을 더 넣을수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은, 300,000 번 페이지를 조회하는건 일반 사용자가 아닌 데이터 수집을 목적으로 하는 크롤링 혹은 어뷰저일 수 있으니 서비스 운영 정책을 새우는 방법(게시글 목록 조회는 최근 기준으로 10,000번 페이지까지 제한하는 등) 도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC/DB</category>
      <category>DB index</category>
      <category>index 튜닝</category>
      <category>index 활용방법</category>
      <category>MySQL 인덱스</category>
      <category>쿼리 튜닝</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/352</guid>
      <comments>https://min-nine.tistory.com/entry/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%A1%B0%ED%9A%8C-%EC%BF%BC%EB%A6%AC-%ED%8A%9C%EB%8B%9D%ED%95%B4%EB%B3%B4%EA%B8%B0-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%82%AC%EC%9A%A9%EB%B2%95#entry352comment</comments>
      <pubDate>Mon, 23 Jun 2025 11:23:02 +0900</pubDate>
    </item>
    <item>
      <title>[GitLab] Spring Multi Module Project gitlab-ci.yml 작성방법</title>
      <link>https://min-nine.tistory.com/entry/GitLab-Spring-Multi-Module-Project-gitlab-ciyml-%EC%9E%91%EC%84%B1%EB%B0%A9%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Gitlab을 사용하면서, 내장된 CI/CD 워크플로우를 사용하기 위해 우리는 Application Repository에. gitlab-ci.yml 파일을 작성합니다. 오늘은 Spring Boot로 개발된 Multi Module Project에서 어떻게 gitlab-ci.yml 파일을 작성해야 원하는 모듈만 빌드되거나 혹은 모든 모듈이 병렬처리로 빌드시킬 수 있는지에 대한 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/576Yi/btsMJ3P7Ww4/VKl9g1aFWglh9LgCkA6FP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/576Yi/btsMJ3P7Ww4/VKl9g1aFWglh9LgCkA6FP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/576Yi/btsMJ3P7Ww4/VKl9g1aFWglh9LgCkA6FP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F576Yi%2FbtsMJ3P7Ww4%2FVKl9g1aFWglh9LgCkA6FP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;668&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Multi Module 구조 파악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스팅에서 사용할 Multi Module의 구조는 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 모듈은 데이터를 입력받아 queue에 쌓는 Producer 역할을, sms 및 mail 모듈은 rabbitmq를 listen 하여 mail과 sms 및 push를 보내주는 consumer의 역할을, batch 모듈은 부가로 처리해야 할 batch 작업을 실행하는 역하을 맡고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741924461062&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├── Dockerfile
├── README.md
├── build
├── build.gradle.kts
├── gradle
├── gradlew
├── gradlew.bat
├── http-test
├── log.config.path_IS_UNDEFINED
├── logs
├── module-api
├── module-core
├── module-database
├── module-mail
├── module-batch
├── module-rabbitmq
├── module-redis
├── module-sms
└── settings.gradle.kts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 모듈들은 공통적인 core, database, redis, rabbitmq의 config를 주입받아 사용하기 때문에 해당 모듈 안에 코드가 변경될 경우에만 pipeline 작업을 실행하게 하고 싶었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Build Stage&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 기능은, 특정 파일에 대한 변경 사항을 확인하여 언제 작업을 파이프라인에 추가할지를 지정하는 것인데, 해당 기능은 gitlab-ci의 키워드 중에서 rulues:changes를 사용하여 구현할 수 있었습니다. gitlab-ci의 자세한 사용법은 GitLab 공식 기술 문서 한글판을 지원하는 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;인포그랩 블로그에 상세히 기술되어 있어서&lt;/b&gt;&lt;/span&gt; 어렵지 않았습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741924936774&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;GitLab 공식 기술 문서 한글판 by 인포그랩 | 인포그랩 | GitLab 기반 DevSecOps 구축,컨설팅,교육,CICD Pipe&quot; data-og-description=&quot;GitLab의 Selected 파트너 인포그랩에서 OpenAI 기술 기반으로 자체 개발한 자동화 번역 프로그램을 통해 GitLab 공식 기술 문서의 한글판을 국내 최초로 제공합니다.&quot; data-og-host=&quot;gitlab-docs.infograb.net&quot; data-og-source-url=&quot;https://gitlab-docs.infograb.net/ee/ci/yaml/#ruleschanges&quot; data-og-url=&quot;https://gitlab-docs.infograb.net&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zOVKJ/hyYrQD5w3B/hMtPwxZlLx6LfKGzsjaw90/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bdaAds/hyYqXKzn0G/FR7VoVM8oL4ct2Dl2i4pX0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/cHBmKC/hyYqNgSruE/kCPOIlAluUuOuLMfb3wF4k/img.png?width=1189&amp;amp;height=232&amp;amp;face=0_0_1189_232&quot;&gt;&lt;a href=&quot;https://gitlab-docs.infograb.net/ee/ci/yaml/#ruleschanges&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gitlab-docs.infograb.net/ee/ci/yaml/#ruleschanges&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zOVKJ/hyYrQD5w3B/hMtPwxZlLx6LfKGzsjaw90/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bdaAds/hyYqXKzn0G/FR7VoVM8oL4ct2Dl2i4pX0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/cHBmKC/hyYqNgSruE/kCPOIlAluUuOuLMfb3wF4k/img.png?width=1189&amp;amp;height=232&amp;amp;face=0_0_1189_232');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitLab 공식 기술 문서 한글판 by 인포그랩 | 인포그랩 | GitLab 기반 DevSecOps 구축,컨설팅,교육,CICD Pipe&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitLab의 Selected 파트너 인포그랩에서 OpenAI 기술 기반으로 자체 개발한 자동화 번역 프로그램을 통해 GitLab 공식 기술 문서의 한글판을 국내 최초로 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gitlab-docs.infograb.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1741925046475&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;build_module_api:
  stage: build-and-push-image
  rules:
    - if: '$CI_COMMIT_BRANCH == &quot;main&quot;'
      changes:
        paths:
          - module-api/**/*
      variables:
        NCP_REGISTRY_IMAGE: &quot;module-api-prod&quot;
    - if: '$CI_COMMIT_BRANCH == &quot;develop&quot;'
      changes:
        paths:
          - module-api/**/*
      variables:
        NCP_REGISTRY_IMAGE: &quot;module-api-dev&quot;
    - when: never

  before_script:
    - mkdir -p /kaniko/.docker
    - echo &quot;{\&quot;auths\&quot;:{\&quot;$NCP_REGISTRY\&quot;:{\&quot;username\&quot;:\&quot;$NCP_REGISTRY_USER\&quot;,\&quot;password\&quot;:\&quot;$NCP_REGISTRY_PASSWORD\&quot;}}}&quot; &amp;gt; /kaniko/.docker/config.json

  script:
    - /kaniko/executor \
      --context &quot;$CI_PROJECT_DIR&quot; \
      --dockerfile &quot;$CI_PROJECT_DIR/Dockerfile&quot; \
      --build-arg &quot;MODULE_NAME=module-api&quot; \
      --build-arg &quot;APP_PORT=8080&quot; \
      --destination &quot;$NCP_REGISTRY/$NCP_REGISTRY_IMAGE:$IMAGE_TAG&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 stage에서는 rules.changes 키워드를 사용하여 paths에 module-api/**/* 를 지정하였습니다. 해당 내용의 의미는 module-api 하위 디렉터리 전체에 변경사항이 감지하면 rules를 충족시키기 때문에 when:never에 걸리지 않고 빌드가 실행된다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 k8s cluster에 배포하게 하기 위해 build를 docker image로 만들었고, kubernetes container 내부에서 동작하는 runner pod에서 dind 사용을 하지 않기 위해 kaniko를 사용하였지만, 빌드 스크립트는 원하는 환경에 맞추어 변경하시면 됩니다.&amp;nbsp; 이제 위 build stage 내용을 복사하여 나머지 module들에 해당하는 내용으로 커스텀하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 자세한 내용은 이전에 작성한 포스팅을 참고해 주세요.&lt;/p&gt;
&lt;figure id=&quot;og_1741925319991&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;GitLab &amp;amp; ArgoCD를 활용한 GitOps방식의 CI/CD 구축하기 (3) - GitlabCI &amp;amp; ArgoCD 연동하여 CI/CD 구축 완료하기.&quot; data-og-description=&quot;GitlabCI 및 ArgoCD 설계GitlabCI에서는 이전 포스팅에서 설명한 대로, Application을 Build 하여 Conatiner Image로 만든 후 Docker Image Registry인 NCP Container Resitry에 Upload 합니다. 그 후 helm repository에 Image Tag값을 &quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-3-GitlabCI-ArgoCD-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-3-GitlabCI-ArgoCD-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RLZbW/hyYqL4pqFH/U2McekXx99K4ZZgXbd6t6k/img.png?width=800&amp;amp;height=616&amp;amp;face=0_0_800_616,https://scrap.kakaocdn.net/dn/8dYTq/hyYrWqLbbw/0ZmDnBd3L43pvjAdbRn2X0/img.png?width=800&amp;amp;height=616&amp;amp;face=0_0_800_616,https://scrap.kakaocdn.net/dn/Uiw8u/hyYr34t9yY/fGlSbuOYCdkXMuZpMaE4hK/img.png?width=2340&amp;amp;height=1804&amp;amp;face=0_0_2340_1804&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-3-GitlabCI-ArgoCD-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-3-GitlabCI-ArgoCD-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RLZbW/hyYqL4pqFH/U2McekXx99K4ZZgXbd6t6k/img.png?width=800&amp;amp;height=616&amp;amp;face=0_0_800_616,https://scrap.kakaocdn.net/dn/8dYTq/hyYrWqLbbw/0ZmDnBd3L43pvjAdbRn2X0/img.png?width=800&amp;amp;height=616&amp;amp;face=0_0_800_616,https://scrap.kakaocdn.net/dn/Uiw8u/hyYr34t9yY/fGlSbuOYCdkXMuZpMaE4hK/img.png?width=2340&amp;amp;height=1804&amp;amp;face=0_0_2340_1804');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitLab &amp;amp; ArgoCD를 활용한 GitOps방식의 CI/CD 구축하기 (3) - GitlabCI &amp;amp; ArgoCD 연동하여 CI/CD 구축 완료하기.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitlabCI 및 ArgoCD 설계GitlabCI에서는 이전 포스팅에서 설명한 대로, Application을 Build 하여 Conatiner Image로 만든 후 Docker Image Registry인 NCP Container Resitry에 Upload 합니다. 그 후 helm repository에 Image Tag값을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Deploy Stage&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 스테이지에서 중요한 것은 resource_group, needs 키워드입니다. 아래와&amp;nbsp; 똑같은 내용을 각 모듈에 맞게 복사하여 하나의 gitlab-ci.yml 파일에서 관리하게 되면 문제점이 생기는 게 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;u&gt;&lt;b&gt;'만약 동시에 여러 멀티모듈이 helm charts의 repository에 push하게되면 어떻게 하지?'였습니다.&lt;/b&gt;&lt;/u&gt;&lt;/span&gt; 병렬로 build 처리를 진행하지만, 우연히 동시에 helm repository의 각 image tag가 업데이트된다면 push가 꼬여서 오류가 발생할 것이 물 보듯 뻔했습니다. 때문에 resource_group을 동일한 &quot;multi-module-application&quot; 이란 내용으로 지정하게 된다면, 비관적 락이 발생하여 해당 stage가 작업중일때는 다른 stage에서는 waiting 할 수 있게 처리할 수 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741925543324&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;deploy_module_api:
  stage: update-helm-chart
  resource_group: multi-module-application
  rules:
    - if: '$CI_COMMIT_BRANCH == &quot;main&quot;'
      changes:
        paths:
          - module-api/**/*
      variables:
        NCP_REGISTRY_IMAGE: $MODULE_API_PROD_IMAGE_NAME
        HELM_VALUES_FILE: &quot;prod-values.yaml&quot;
    - if: '$CI_COMMIT_BRANCH == &quot;develop&quot;'
      changes:
        paths:
          - module-api/**/*
      variables:
        NCP_REGISTRY_IMAGE: $MODULE_API_DEV_IMAGE_NAME
        HELM_VALUES_FILE: &quot;develop-values.yaml&quot;
    - when: never

  image: alpine:latest
  variables:
    GIT_STRATEGY: none

  before_script:
    - apk add --no-cache git yq openssh-client
    - mkdir -p ~/.ssh
    - ssh-keyscan gitlab-sh.mingyu.co.kr &amp;gt;&amp;gt; ~/.ssh/known_hosts
    - chmod 700 ~/.ssh
    - chmod 644 ~/.ssh/known_hosts
    - eval $(ssh-agent -s)
    - echo &quot;$MINGYU_GITLAB_SH_SSH_PRIVATE_KEY&quot; | tr -d '\r' | ssh-add -

  script:
    - git clone &quot;$GITLAB_HELM_MULTIMODULE_APPLICATION_GIT_PATH&quot;
    - cd &quot;$GITLAB_HELM_MULTIMODULE_APPLICATION_DIR_NAME&quot;
    - git fetch origin &quot;$CI_COMMIT_BRANCH&quot;
    - git checkout &quot;$CI_COMMIT_BRANCH&quot;
    - cd charts/module-api

    - echo &quot;IMAGE_REPOSITORY = $NCP_REGISTRY/$NCP_REGISTRY_IMAGE&quot;
    - echo &quot;IMAGE_TAG = $IMAGE_TAG&quot;
    - yq -i &quot;.image.repository = \&quot;$NCP_REGISTRY/$NCP_REGISTRY_IMAGE\&quot;&quot; &quot;$HELM_VALUES_FILE&quot;
    - yq -i &quot;.image.tag = \&quot;$IMAGE_TAG\&quot;&quot; &quot;$HELM_VALUES_FILE&quot;
    - cd ../../

    - git config user.email &quot;mingyu@mingyu.co.kr&quot;
    - git config user.name &quot;mingyu&quot;
    - git add &quot;charts/module-api/$HELM_VALUES_FILE&quot;
    - git commit -m &quot;Update image tag to $IMAGE_TAG&quot;
    - git push origin &quot;$CI_COMMIT_BRANCH&quot;

  needs:
    - build_module_api&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build stage가 종료된 이후 deploy stage가 실행되어야 하기 때문에 needs 키워드를 활용하여 해당 스테이지는 build 스테이지 이후에 실행되도록 설정하였습니다.&lt;/p&gt;</description>
      <category>Infrastructure/Git</category>
      <category>gitalb ci 작성</category>
      <category>gitalbci multi module</category>
      <category>gitlab ci multi module 작성</category>
      <category>gitlab ci pipeline 구축</category>
      <category>gitlab multi module cicd</category>
      <category>gitlab multi module cicd 방법</category>
      <category>multi module cicd</category>
      <category>spring boot multi module cicd</category>
      <category>spring boot multi module gitlab</category>
      <category>spring multimodule gitlabci</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/350</guid>
      <comments>https://min-nine.tistory.com/entry/GitLab-Spring-Multi-Module-Project-gitlab-ciyml-%EC%9E%91%EC%84%B1%EB%B0%A9%EB%B2%95#entry350comment</comments>
      <pubDate>Sat, 15 Mar 2025 13:51:48 +0900</pubDate>
    </item>
    <item>
      <title>[Terraform] MacOs Local에 Terraform 설치 및 NCP가이드 안내</title>
      <link>https://min-nine.tistory.com/entry/Terraform-MacOs-Local%EC%97%90-Terraform-%EC%84%A4%EC%B9%98-%EB%B0%8F-NCP%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼은 인프라를 코드로 관리하는 도구(IaC)로, 클라우드 리소스를 선언적으로 정의하고 자동으로 프로비저닝 할 수 있도록 도와주는 open source 소프트웨어입니다. 테라폼은 로컬 컴퓨터나 별도의 관리 서버에 프로그램을 설치해서 사용할 수도 있고, 선언적 코드만 작성하여 gitlab 혹은 github으로 관리하며 파이프라인에서 자동으로 실행하는 환경을 구성할 수도 있다고 하는데, 저는 첫 발자국을 내딛을 겸 MacOS 로컬 컴퓨터에서 Terraform을 설치하여 현재 사용하고 있는 Naver Cloud Platform과 연동해보려고 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Terraform Install In Local MacOs&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 hashicorp 공식 문서의 terraform 설치 가이드를 참고하였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741932694443&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Install Terraform | Terraform | HashiCorp Developer&quot; data-og-description=&quot;Install Terraform on Mac, Linux, or Windows by downloading the binary or using a package manager (Homebrew or Chocolatey). Then create a Docker container locally by following a quick-start tutorial to check that Terraform installed correctly.&quot; data-og-host=&quot;developer.hashicorp.com&quot; data-og-source-url=&quot;https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli&quot; data-og-url=&quot;https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dqxMvS/hyYqaC7Waq/D7aA8PqOWF66nNAwpMQCw0/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/bWdcGX/hyYqWLFq5n/6dcBM4kpoxh7kwkz0DXhdk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800&quot;&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dqxMvS/hyYqaC7Waq/D7aA8PqOWF66nNAwpMQCw0/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800,https://scrap.kakaocdn.net/dn/bWdcGX/hyYqWLFq5n/6dcBM4kpoxh7kwkz0DXhdk/img.jpg?width=3200&amp;amp;height=1800&amp;amp;face=0_0_3200_1800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Install Terraform | Terraform | HashiCorp Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Install Terraform on Mac, Linux, or Windows by downloading the binary or using a package manager (Homebrew or Chocolatey). Then create a Docker container locally by following a quick-start tutorial to check that Terraform installed correctly.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.hashicorp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1741932838202&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew update
brew tap hashicorp/tap
brew install hashicorp/tap/terraform&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1747&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dB1lFi/btsMK84B9tH/k4PDHtx1P4EA9aUp42yiU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dB1lFi/btsMK84B9tH/k4PDHtx1P4EA9aUp42yiU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dB1lFi/btsMK84B9tH/k4PDHtx1P4EA9aUp42yiU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdB1lFi%2FbtsMK84B9tH%2Fk4PDHtx1P4EA9aUp42yiU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1747&quot; height=&quot;879&quot; data-origin-width=&quot;1747&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NaverCloudPlatform(NCP) 연동 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼 공식 레지스트리에 들어가보면 파트너를 맺은 클라우드 밴더사들이 보이는데, 저는 ncloud를 찾아서 해당 가이드 문서를 참고하였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741937275952&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Terraform Registry&quot; data-og-description=&quot;&quot; data-og-host=&quot;registry.terraform.io&quot; data-og-source-url=&quot;https://registry.terraform.io/providers/NaverCloudPlatform/ncloud/latest&quot; data-og-url=&quot;https://registry.terraform.io/providers/NaverCloudPlatform/ncloud/latest&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://registry.terraform.io/providers/NaverCloudPlatform/ncloud/latest&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://registry.terraform.io/providers/NaverCloudPlatform/ncloud/latest&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Terraform Registry&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;registry.terraform.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1741937351827&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;terraform-provider-ncloud/examples/vpc/scenario01/main.tf at main &amp;middot; NaverCloudPlatform/terraform-provider-ncloud&quot; data-og-description=&quot;Terraform NaverCloud provider. Contribute to NaverCloudPlatform/terraform-provider-ncloud development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/NaverCloudPlatform/terraform-provider-ncloud/blob/main/examples/vpc/scenario01/main.tf&quot; data-og-url=&quot;https://github.com/NaverCloudPlatform/terraform-provider-ncloud/blob/main/examples/vpc/scenario01/main.tf&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4cxOP/hyYqRXVRL9/0FWciXt1DEAy8Z7oPMUZg1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dYmYWP/hyYrUmc19N/sijucxYORuHYV1hUMpL7z0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/NaverCloudPlatform/terraform-provider-ncloud/blob/main/examples/vpc/scenario01/main.tf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/NaverCloudPlatform/terraform-provider-ncloud/blob/main/examples/vpc/scenario01/main.tf&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4cxOP/hyYqRXVRL9/0FWciXt1DEAy8Z7oPMUZg1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dYmYWP/hyYrUmc19N/sijucxYORuHYV1hUMpL7z0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;terraform-provider-ncloud/examples/vpc/scenario01/main.tf at main &amp;middot; NaverCloudPlatform/terraform-provider-ncloud&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Terraform NaverCloud provider. Contribute to NaverCloudPlatform/terraform-provider-ncloud development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 테라폼을 사용해서 NCP의 resource들을 생성 및 관리하는 방법들을 자세히 알아보도록 하겠습니다.&lt;/p&gt;</description>
      <category>Infrastructure/Terraform</category>
      <category>ncp terraform install</category>
      <category>ncp terraform 사용방법</category>
      <category>nks terraform 설치</category>
      <category>terrafom 개념</category>
      <category>terraform nks 설치</category>
      <category>Terraform 설치</category>
      <category>테라폼 nks 연동</category>
      <category>테라폼 강의</category>
      <category>테라폼 사용법</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/351</guid>
      <comments>https://min-nine.tistory.com/entry/Terraform-MacOs-Local%EC%97%90-Terraform-%EC%84%A4%EC%B9%98-%EB%B0%8F-NCP%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0#entry351comment</comments>
      <pubDate>Fri, 14 Mar 2025 17:08:05 +0900</pubDate>
    </item>
    <item>
      <title>GitOps를 위한 Secret 관리하기 (3) - SealedSecrets 생성 Application 구현</title>
      <link>https://min-nine.tistory.com/entry/GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-3-SealedSecrets-%EC%83%9D%EC%84%B1-Application-%EA%B5%AC%ED%98%84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 입력값을 토대로 SealedSecret을 생성하여 yaml 파일 형식으로 제공해주는 프로그램을 만들어보고, 해당 프로그램을 사용하여 현재 &lt;b&gt;제가 현업에서 어떻게 gitops 방식으로 secret을 git repository에서 관리하는지&lt;/b&gt;까지 알려드리겠습니다. 저는 간단한 단일 API Application을 만들 때 &lt;b&gt;Express.js를 사용&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SealedSecret에 대한 설치 및 CLI에서의 사용 방법은 이전글을 참고해주시기 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741825353813&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;GitOps를 위한 Secret 관리하기 (2) - SealedSecrets 설치 및 사용하기 (CLI)&quot; data-og-description=&quot;K8s Cluster 내부 및 Local PC 내부 각각에 SealedScrets 관련 소프트웨어를 설치하고, CLI를 통해 사용하는 방법을 알아보겠습니다. SealedSecrets의 개념은 이전 포스팅을 참고해 주세요.&amp;nbsp;[Cloud Kubernetes 환경 &quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-2-SealedSecrets-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-CLI&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-2-SealedSecrets-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-CLI&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3rTRM/hyYqTnwTRy/75QqO2V0FlBKAcLBhzd0jk/img.png?width=800&amp;amp;height=730&amp;amp;face=0_0_800_730,https://scrap.kakaocdn.net/dn/bR57RX/hyYqRQM0bv/ykKzk0LdOeNpkNykhPJBr1/img.png?width=800&amp;amp;height=730&amp;amp;face=0_0_800_730,https://scrap.kakaocdn.net/dn/b6jWXI/hyYqXi9B4G/aC3IDcWpFnqfgPU6JNAvvk/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-2-SealedSecrets-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-CLI&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-2-SealedSecrets-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-CLI&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3rTRM/hyYqTnwTRy/75QqO2V0FlBKAcLBhzd0jk/img.png?width=800&amp;amp;height=730&amp;amp;face=0_0_800_730,https://scrap.kakaocdn.net/dn/bR57RX/hyYqRQM0bv/ykKzk0LdOeNpkNykhPJBr1/img.png?width=800&amp;amp;height=730&amp;amp;face=0_0_800_730,https://scrap.kakaocdn.net/dn/b6jWXI/hyYqXi9B4G/aC3IDcWpFnqfgPU6JNAvvk/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitOps를 위한 Secret 관리하기 (2) - SealedSecrets 설치 및 사용하기 (CLI)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;K8s Cluster 내부 및 Local PC 내부 각각에 SealedScrets 관련 소프트웨어를 설치하고, CLI를 통해 사용하는 방법을 알아보겠습니다. SealedSecrets의 개념은 이전 포스팅을 참고해 주세요.&amp;nbsp;[Cloud Kubernetes 환경&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 NCP에서 제공해주는 kubernetes Management System인 NKS를 사용합니다. 때문에 NCP에 접근할 수 있는 ncp-iam-authenticator 설치가 필요하고, NKS의 Prod, Dev 각각의 Cluster에 설치되어있는 Server-Side SealedSecret Contoller와 통신할 때 필요한 Client-Side Application인 kubeseal도 함께 설치가 필요합니다. 마지막으로 각각의 클러스터와 연동할 수 있는 kubeconfig.yaml 파일이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 저는 해당 Application을 Build하여 Container Image를 만들고, 해당 Container Image를 활용할 때 Argument 값에 따라 Cluster UUID값을 다르게 설정하여 ncp-iam-authenticator에서 kubeconfig 파일을 생성하여 환경변수에 설정해주는 형태로 설계하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 어플리케이션의 이름은 secret-manager(가칭)라고 지었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;861&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ber18a/btsMJecJtQk/B7hUu0a89X0cQXTq63QFv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ber18a/btsMJecJtQk/B7hUu0a89X0cQXTq63QFv0/img.png&quot; data-alt=&quot;SecretManager(가칭) Application의 설계도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ber18a/btsMJecJtQk/B7hUu0a89X0cQXTq63QFv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fber18a%2FbtsMJecJtQk%2FB7hUu0a89X0cQXTq63QFv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;771&quot; height=&quot;861&quot; data-origin-width=&quot;771&quot; data-origin-height=&quot;861&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SecretManager(가칭) Application의 설계도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;secret-manager Application 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Express 설치 방법은 공식 docs에 자세히 안내되어 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1741826766444&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Express 설치&quot; data-og-description=&quot;Learn how to install Express.js in your Node.js environment, including setting up your project directory and managing dependencies with npm.&quot; data-og-host=&quot;expressjs.com&quot; data-og-source-url=&quot;https://expressjs.com/ko/starter/installing.html&quot; data-og-url=&quot;https://expressjs.com/ko/starter/installing.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pbcdf/hyYqapltjH/MG3PO8tjShoGiXDLCiafDK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bfXACn/hyYq0GWX1s/mtb1a5jaDy2PEshQpcD1yK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://expressjs.com/ko/starter/installing.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://expressjs.com/ko/starter/installing.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pbcdf/hyYqapltjH/MG3PO8tjShoGiXDLCiafDK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bfXACn/hyYq0GWX1s/mtb1a5jaDy2PEshQpcD1yK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Express 설치&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to install Express.js in your Node.js environment, including setting up your project directory and managing dependencies with npm.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;expressjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 공식문서에 나와있는 것과 똑같이 설치한 후에, 아래 파일들만 생성해주었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Dockerfile - Application을 Conatiner로 만들기 위한 Build용도&lt;/li&gt;
&lt;li&gt;controllers/sealController.js - servcie.js에서 구현한 기능들을 활용&lt;/li&gt;
&lt;li&gt;services/sealService.js - 입력 데이터를 SealedSecret 관련 Yaml로 변환해주는 기능을 직접적으로 구현&lt;/li&gt;
&lt;li&gt;request-sealedSecet-example.http - 직접 http method를 호출하여 응답값을 확인하는 파일 (gitignore로 차단)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1741826743218&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├── Dockerfile
├── README.md
├── bin
│&amp;nbsp;&amp;nbsp; └── www
├── controllers
│&amp;nbsp;&amp;nbsp; └── sealController.js
├── index.js
├── package-lock.json
├── package.json
├── public
│&amp;nbsp;&amp;nbsp; └── stylesheets
│&amp;nbsp;&amp;nbsp;     └── style.css
├── request-sealedSecret-example.http
├── routes
│&amp;nbsp;&amp;nbsp; ├── index.js
│&amp;nbsp;&amp;nbsp; └── users.js
├── services
│&amp;nbsp;&amp;nbsp; └── secretService.js
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;secret-manager Application Source&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 직접적으로 코드를 구현해보도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;index.js&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sealController를 require 받아 &quot;/seal&quot; 이라는 URI가 POST Method로 들어올 때 Controller를 바라볼 수 있도록 라우트를 등록해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741827227562&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.js
const express = require(&quot;express&quot;);
const { sealController } = require(&quot;./controllers/sealController&quot;);

const app = express();
app.use(express.json());

// /seal 라우트 등록
app.post(&quot;/seal&quot;, sealController);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&amp;gt; {
  console.log(`secret-manager is running on port ${PORT}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sealController.js&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sealService에서 구현한 function들을 사용하고, 응답값을 retrun해줍니다. 저는 응답값을 그대로 복사하여 git repository에 등록해줄 것이기 때문에 text/plain으로 응답해줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741827387506&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// controllers/sealController.js
const { generateBaseSecretYAML, sealSecret } = require(&quot;../services/secretService&quot;);

/**
 * /seal : Key/Value를 받아 SealedSecret YAML 반환
 */
async function sealController(req, res) {
    try {
        const { secretName, namespace, data } = req.body;

        if (!secretName || !namespace || !data) {
            return res.status(400).json({
                error: &quot;secretName, namespace, data 필드가 필요합니다.&quot;,
            });
        }

        // 1) 일반 Secret YAML 생성
        const baseSecret = generateBaseSecretYAML(secretName, namespace, data);

        // 2) SealedSecret 변환
        const sealedSecretYAML = await sealSecret(baseSecret);

        // 3) YAML로 응답 (text/plain)
        res.setHeader(&quot;Content-Type&quot;, &quot;text/plain&quot;);
        return res.status(200).send(sealedSecretYAML);

        // 만약 JSON으로 응답하려면:
        // res.json({ sealedSecret: sealedSecretYAML });
    } catch (error) {
        console.error(error);
        return res.status(500).json({ error: error.message || &quot;서버 에러&quot; });
    }
}

module.exports = {
    sealController,
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;secretService.js&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자로부터 입력받은 평문 Value값을 base64로 인코딩하여 Kubernetes Secret Object를 생성할 수 있는 yaml 파일로 만드는 기능과, kubeseal 명령어를 사용하여 정말로 cluster 내부의 SealedSecret Controller와 통신하여 암호화된 sealedsecret.yaml파일을 응답해주는 기능을 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741827525576&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// services/secretService.js
const { spawn } = require(&quot;child_process&quot;);

/**
 * 입력받은 Key/Value 데이터를 일반 Secret YAML로 변환 (base64 인코딩)
 */
function generateBaseSecretYAML(secretName, namespace, dataObj) {
    const encodedData = Object.entries(dataObj).reduce((acc, [key, value]) =&amp;gt; {
        acc[key] = Buffer.from(value).toString(&quot;base64&quot;);
        return acc;
    }, {});

    return `apiVersion: v1
kind: Secret
metadata:
  name: ${secretName}
  namespace: ${namespace}
type: Opaque
data:
${Object.entries(encodedData)
        .map(([k, v]) =&amp;gt; `  ${k}: ${v}`)
        .join(&quot;\n&quot;)}
`;
}

/**
 * kubeseal 명령어를 통해 Secret YAML -&amp;gt; SealedSecret YAML 변환
 */
function sealSecret(secretYAML) {
    return new Promise((resolve, reject) =&amp;gt; {
        const kubeseal = spawn(&quot;kubeseal&quot;, [
            &quot;--format&quot;,
            &quot;yaml&quot;,
            &quot;--controller-name&quot;,
            &quot;sealed-secrets&quot;, // helm install시 지정한 이름
            &quot;--controller-namespace&quot;,
            &quot;default&quot;, // helm install시 지정한 namespace
        ]);

        let stdoutData = &quot;&quot;;
        let stderrData = &quot;&quot;;

        kubeseal.stdout.on(&quot;data&quot;, (data) =&amp;gt; {
            stdoutData += data.toString();
        });

        kubeseal.stderr.on(&quot;data&quot;, (data) =&amp;gt; {
            stderrData += data.toString();
        });

        kubeseal.on(&quot;close&quot;, (code) =&amp;gt; {
            if (code === 0) {
                resolve(stdoutData);
            } else {
                reject(stderrData);
            }
        });

        kubeseal.stdin.write(secretYAML);
        kubeseal.stdin.end();
    });
}

// 서비스 메서드들을 export
module.exports = {
    generateBaseSecretYAML,
    sealSecret,
};&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dockerfile 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build stage에서는 kubeseal, ncp-iam-authenticator를 설치합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741827891198&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ---- 1) Build Stage ----
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm install --production

COPY . .

# kubeseal 설치
RUN apk update &amp;amp;&amp;amp; apk add --no-cache wget \
    &amp;amp;&amp;amp; wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.20.2/kubeseal-0.20.2-linux-amd64.tar.gz \
    &amp;amp;&amp;amp; tar -zxvf kubeseal-0.20.2-linux-amd64.tar.gz kubeseal \
    &amp;amp;&amp;amp; mv kubeseal /usr/local/bin/kubeseal \
    &amp;amp;&amp;amp; chmod +x /usr/local/bin/kubeseal \
    &amp;amp;&amp;amp; rm kubeseal-0.20.2-linux-amd64.tar.gz

# ncp-iam-authenticator 설치
RUN apk update &amp;amp;&amp;amp; apk add --no-cache curl libc6-compat \
    &amp;amp;&amp;amp; curl -L -o /usr/local/bin/ncp-iam-authenticator \
       https://github.com/NaverCloudPlatform/ncp-iam-authenticator/releases/latest/download/ncp-iam-authenticator_linux_amd64 \
    &amp;amp;&amp;amp; chmod +x /usr/local/bin/ncp-iam-authenticator&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;runtime stage에서는 build stage에서 설치한 kubeseal, ncp-iam-authenticator를 가져오고, kubeconfig파일을 생성하여 환경변수로 등록해주는 작업을 진행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741828566673&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ---- 2) Production Stage ----
FROM node:20-alpine

WORKDIR /app

# 빌드 시 전달받은 BUILD_ENV에 따라 cluster uuid를 분기 처리하기 위한 ARG 설정
ARG BUILD_ENV
ENV BUILD_ENV=${BUILD_ENV}

# NCLOUD 관련 빌드 인자
ARG NCLOUD_ACCESS_KEY
ARG NCLOUD_SECRET_KEY
ENV NCLOUD_ACCESS_KEY=${NCLOUD_ACCESS_KEY} \
    NCLOUD_SECRET_KEY=${NCLOUD_SECRET_KEY} \
    NCLOUD_API_GW=https://ncloud.apigw.ntruss.com

# builder 스테이지에서 필요한 파일들을 모두 복사
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /usr/local/bin/kubeseal /usr/local/bin/kubeseal
COPY --from=builder /usr/local/bin/ncp-iam-authenticator /usr/local/bin/ncp-iam-authenticator
COPY . .

# BUILD_ENV 값에 따라 CLUSTER_UUID를 결정하고 kubeconfig를 생성
RUN if [ &quot;$BUILD_ENV&quot; = &quot;prod&quot; ]; then \
      CLUSTER_UUID=&quot;nks-prod-cluster-uuid&quot;; \
    else \
      CLUSTER_UUID=&quot;nks-dev-cluster-uuid&quot;; \
    fi &amp;amp;&amp;amp; \
    echo &quot;Using Cluster UUID: $CLUSTER_UUID&quot; &amp;amp;&amp;amp; \
    ncp-iam-authenticator create-kubeconfig --region KR --clusterUuid &quot;$CLUSTER_UUID&quot; --output ./kubeconfig.yaml

ENV KUBECONFIG=/app/kubeconfig.yaml

EXPOSE 3000
CMD [&quot;node&quot;, &quot;index.js&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Dockerfile에서 Build시 secret key, access key 등을 arguments로 활용하는것은 권장하지 않습니다만,&lt;/b&gt;&lt;/span&gt; 위 방법이 아니라면 미리 생성된 kubeconfig.yml 파일을 git repository로 등록하고 관리를 해줘야하기 때문에 권장하지 않는 방법을 사용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 dev 클러스터와 통신하는 secret-manager-dev 라는 컨테이너 이미지를 생성해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741828814443&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build -t secret-manager-dev:0.1 \ 
  --build-arg BUILD_ENV=dev \ 
  --build-arg NCLOUD_ACCESS_KEY=${YOUR_NCLOUD_ACCESS_KEY} \
  --build-arg NCLOUD_SECRET_KEY=${YOUR_NCLOUD_SECRET_KEY} \
  -f Dockerfile .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;983&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQrba/btsMI5UBRUF/LbtVXIpgetBmxU86FFpFk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQrba/btsMI5UBRUF/LbtVXIpgetBmxU86FFpFk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQrba/btsMI5UBRUF/LbtVXIpgetBmxU86FFpFk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQrba%2FbtsMI5UBRUF%2FLbtVXIpgetBmxU86FFpFk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;988&quot; height=&quot;983&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;983&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 완료되면 위의 사진과 같이 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;sensetive한 내용은 build시 사용하지 말라는 경고문구가 함께 출력&lt;/b&gt;&lt;/span&gt;됩니다. 꼭 사용해야 할 이유가 있는지, 타당한 이유인지 한 번쯤 생각하고 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드된 이미지를 실행시킵니다. 저는 dev 클러스터는 localhoast의 3000번 포트를, prod 클러스터는 3001번 포트로 접근하게 포트바인딩을 해줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741829222809&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -it --rm -p 3000:3000 secret-manager-dev:0.1
docker run -it --rm -p 3001:3000 secret-manager-prod:0.1&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request-sealedSecret-example.htttp&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 파일을 생성하여 secret 오브젝트의 name과 namespace, 그리고 사용할 data를 아래와 같은 형식으로 입력하여 전송합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741829477824&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;### Mingyu-Api-Secret-Dev
POST http://localhost:3000/seal
Content-Type: application/json

{
  &quot;secretName&quot;: &quot;mingyu-api-secret-dev&quot;,
  &quot;namespace&quot;: &quot;test&quot;,
  &quot;data&quot;: {
    &quot;DBPASSWORD&quot;: &quot;abcd0987!&quot;,
    &quot;SECRET_KEY&quot;: &quot;testSecret!!&quot;,
    &quot;ACCESS_KEY&quot;: &quot;testAcess@@&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 아래와 같은 결과물을 확인할 수 있습니다. 저는 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;해당 결과물을 argocd와 연동시켜놓은 helm charts의 custom-values에 sealedsecret 태그&lt;/b&gt;&lt;/span&gt;를 만들어서 response로 출력된&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt; text/plain의 metadata, spec을 그대로 복사하여 사용&lt;/b&gt;&lt;/span&gt;하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SpuFg/btsMIuUSnwW/Iu8uD0gIQmKRs9PGkVUWu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SpuFg/btsMIuUSnwW/Iu8uD0gIQmKRs9PGkVUWu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SpuFg/btsMIuUSnwW/Iu8uD0gIQmKRs9PGkVUWu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSpuFg%2FbtsMIuUSnwW%2FIu8uD0gIQmKRs9PGkVUWu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;961&quot; height=&quot;908&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>gitops env 관리</category>
      <category>gitops sealedsecret 사용법</category>
      <category>gitops secret 관리</category>
      <category>k8s secret git 관리</category>
      <category>kubernetes secret 관리</category>
      <category>sealedsecret 사용방법</category>
      <category>sealedsecret 사용법</category>
      <category>sealedsecret 프로그램</category>
      <category>sealedsecret 현업</category>
      <category>sealedsecret 현업 사용법</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/349</guid>
      <comments>https://min-nine.tistory.com/entry/GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-3-SealedSecrets-%EC%83%9D%EC%84%B1-Application-%EA%B5%AC%ED%98%84#entry349comment</comments>
      <pubDate>Thu, 13 Mar 2025 12:53:27 +0900</pubDate>
    </item>
    <item>
      <title>GitOps를 위한 Secret 관리하기 (2) - SealedSecrets 설치 및 사용하기 (CLI)</title>
      <link>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-2-SealedSecrets-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-CLI</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;K8s Cluster 내부 및 Local PC 내부 각각에 SealedScrets 관련 소프트웨어를 설치하고, CLI를 통해 사용하는 방법을 알아보겠습니다. SealedSecrets의 개념은 이전 포스팅을 참고해 주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1741825108225&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;GitOps를 위한 Secret 관리하기 (1) - SealedSecrets 개념&quot; data-og-description=&quot;최근 GitOps를 통한 Kubernetes 클러스터 관리가 확산되면서, 애플리케이션의 배포와 환경 구성을 코드로 관리하는 방식이 주목받고 있습니다. 이와 함께 중요한 정보인 Secret의 안전한 관리 역시 큰 &quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-1-SealedSecrets-%EA%B0%9C%EB%85%90&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-1-SealedSecrets-%EA%B0%9C%EB%85%90&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CchY7/hyYr3QJixx/STtfWP5EXEDZekO04yWp3k/img.jpg?width=686&amp;amp;height=386&amp;amp;face=0_0_686_386,https://scrap.kakaocdn.net/dn/Bccr5/hyYqZgYyYX/xwDDtKXkRsokg1v9EJQyCK/img.jpg?width=686&amp;amp;height=386&amp;amp;face=0_0_686_386,https://scrap.kakaocdn.net/dn/cp9osM/hyYrYhyqwK/i6easGV5bJaYbSWe9JRtJK/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-1-SealedSecrets-%EA%B0%9C%EB%85%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-1-SealedSecrets-%EA%B0%9C%EB%85%90&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CchY7/hyYr3QJixx/STtfWP5EXEDZekO04yWp3k/img.jpg?width=686&amp;amp;height=386&amp;amp;face=0_0_686_386,https://scrap.kakaocdn.net/dn/Bccr5/hyYqZgYyYX/xwDDtKXkRsokg1v9EJQyCK/img.jpg?width=686&amp;amp;height=386&amp;amp;face=0_0_686_386,https://scrap.kakaocdn.net/dn/cp9osM/hyYrYhyqwK/i6easGV5bJaYbSWe9JRtJK/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitOps를 위한 Secret 관리하기 (1) - SealedSecrets 개념&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최근 GitOps를 통한 Kubernetes 클러스터 관리가 확산되면서, 애플리케이션의 배포와 환경 구성을 코드로 관리하는 방식이 주목받고 있습니다. 이와 함께 중요한 정보인 Secret의 안전한 관리 역시 큰&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kubernetes Cluster에 Server-Side SealedSecrets 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Helm을 통해 쉽게 Kubernetes에 Server-Side SealedSecrets을 설치할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741754167981&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# hm-mgt는 제가 사용하는 management 용도의 kubernetes cluster에 접근하는 helm alias입니다.
hm-mgt repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
hm-mgt install sealed-secrets sealed-secrets/sealed-secrets&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;984&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm61yQ/btsMGPyc7j4/rtRD4gZrMNyWk7LvkK8tV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm61yQ/btsMGPyc7j4/rtRD4gZrMNyWk7LvkK8tV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm61yQ/btsMGPyc7j4/rtRD4gZrMNyWk7LvkK8tV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm61yQ%2FbtsMGPyc7j4%2FrtRD4gZrMNyWk7LvkK8tV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;984&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;984&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 Client-side SoftWare 설치 방법부터 사용 방법이 자세히 console에 출력됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Local PC에 Client-Side SealedSecrets 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설치 완료 후 출력되는 1번 항목의 git 주소로 접속하면 각 운영체제마다 Kubeseal을 설치하는 방법이 기술되어 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741754467399&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - bitnami-labs/sealed-secrets: A Kubernetes controller and tool for one-way encrypted Secrets&quot; data-og-description=&quot;A Kubernetes controller and tool for one-way encrypted Secrets - bitnami-labs/sealed-secrets&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/bitnami-labs/sealed-secrets?tab=readme-ov-file#kubeseal&quot; data-og-url=&quot;https://github.com/bitnami-labs/sealed-secrets&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/w35gG/hyYqTOnTvs/jG5Sp6blgCHt6zZZ8U1cT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/rAVaz/hyYr1yqSRK/3wi5JO0w8oUTGLaLAW3K50/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/bitnami-labs/sealed-secrets?tab=readme-ov-file#kubeseal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/bitnami-labs/sealed-secrets?tab=readme-ov-file#kubeseal&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/w35gG/hyYqTOnTvs/jG5Sp6blgCHt6zZZ8U1cT0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/rAVaz/hyYr1yqSRK/3wi5JO0w8oUTGLaLAW3K50/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - bitnami-labs/sealed-secrets: A Kubernetes controller and tool for one-way encrypted Secrets&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Kubernetes controller and tool for one-way encrypted Secrets - bitnami-labs/sealed-secrets&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 MacOS를 사용하기 때문에 brew로 간단하게 설치해 주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741754490449&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install kubeseal&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kubeseal command로 SealedSecrets yaml 파일 생성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 내용의 my-secret.yaml파일을 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741754859785&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  namespace: my-namespace
type: Opaque
data:
  username: bXl1c2Vy      # base64 인코딩: &quot;myuser&quot;
  password: bXlwYXNzd29yZA==  # base64 인코딩: &quot;mypassword&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubeseal 명령어를 사용하여 위의 일반 base64 인코딩 된 secret 오브젝트의 파일을 토대로 my-sealedsecret.yaml 파일을 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741755087144&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubeseal --format yaml &amp;lt; my-secret.yaml &amp;gt; my-sealedsecret.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵션 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1735&quot; data-start=&quot;1588&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1630&quot; data-start=&quot;1588&quot;&gt;--format yaml: 결과물을 YAML 형식으로 출력합니다.&lt;/li&gt;
&lt;li data-end=&quot;1685&quot; data-start=&quot;1633&quot;&gt;&amp;lt; my-secret.yaml: 표준 입력으로 일반 Secret 파일을 전달합니다.&lt;/li&gt;
&lt;li data-end=&quot;1735&quot; data-start=&quot;1688&quot;&gt;&amp;gt; my-sealedsecret.yaml: 암호화된 결과를 파일로 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 kubernetes server에 설치된 server-side SealedSecret 소프트웨어의 암호화를 통하여 아래와 같은 my-sealedsecret.yaml 파일을 얻을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741755128270&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: my-secret
  namespace: my-namespace
spec:
  encryptedData:
    username: AgCnsd3H4...==   # 암호화된 데이터
    password: AgDFs4kL9...==
  template:
    metadata:
      name: my-secret
      namespace: my-namespace
    type: Opaque&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 my-sealedsecret.yaml 파일을 kubectl apply로 kubernetes에 적용하면 Server-Side SealedSecret에서 복호화를 통해 일반적인 secret 오브젝트로 변환하여 미리 정의한 name, namepsace에 알맞게 적용해 줍니다. 그렇다면 저희는 암호화된 my-sealedsecret.yaml 파일을 git으로 관리하여 gitops에 한 발자국 더 다가갈 수 있게 됩니다.&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>k8s sealed secret</category>
      <category>k8s secret git</category>
      <category>k8s secret 관리</category>
      <category>k8s secret 암호화</category>
      <category>kubernetes secret git</category>
      <category>kubernetes secret 관리</category>
      <category>sealedsecret</category>
      <category>sealedsecret 사용법</category>
      <category>쿠버네티스 sealed secret</category>
      <category>쿠버네티스 secret 관리</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/347</guid>
      <comments>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-2-SealedSecrets-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-CLI#entry347comment</comments>
      <pubDate>Wed, 12 Mar 2025 18:57:09 +0900</pubDate>
    </item>
    <item>
      <title>재능 기부</title>
      <link>https://min-nine.tistory.com/notice/348</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;말로 듣는 것보다 직접 경험하는 것이 훨씬 큰 깨달음을 준다고 믿습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;102&quot; data-start=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;저는 지금까지 쌓은 학습과 경험을 바탕으로, 다양한 환경을 구축하고 여러 어플리케이션을 개발해보고자 합니다.&lt;/p&gt;
&lt;p data-end=&quot;102&quot; data-start=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;169&quot; data-start=&quot;104&quot; data-ke-size=&quot;size16&quot;&gt;스타트업이나 중소기업처럼 개발자와 엔지니어 채용이 어려운 상황에서 도움이 필요하시다면 언제든지 연락 부탁드립니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;169&quot; data-start=&quot;104&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;169&quot; data-start=&quot;104&quot; data-ke-size=&quot;size16&quot;&gt;rlaalsrb0466@naver.com&lt;/p&gt;</description>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/notice/348</guid>
      <pubDate>Wed, 12 Mar 2025 16:52:16 +0900</pubDate>
    </item>
    <item>
      <title>GitOps를 위한 Secret 관리하기 (1) - SealedSecrets 개념</title>
      <link>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-1-SealedSecrets-%EA%B0%9C%EB%85%90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 GitOps를 통한 Kubernetes 클러스터 관리가 확산되면서, 애플리케이션의 배포와 환경 구성을 코드로 관리하는 방식이 주목받고 있습니다. 이와 함께 중요한 정보인 Secret의 안전한 관리 역시 큰 고민거리로 떠오르는데요. 이번 포스팅에서는 GitOps 환경에서 Secret을 안전하게 다룰 수 있는 방법 중 하나인 &lt;b&gt;SealedSecrets&lt;/b&gt; 개념에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5QH2F/btsMEVZPx99/0Q4CB92YGvu68KDkQGIoR1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5QH2F/btsMEVZPx99/0Q4CB92YGvu68KDkQGIoR1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5QH2F/btsMEVZPx99/0Q4CB92YGvu68KDkQGIoR1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5QH2F%2FbtsMEVZPx99%2F0Q4CB92YGvu68KDkQGIoR1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;386&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 포스팅은 커피고래님의 &lt;b&gt;&quot;GitOps Secret 관리&quot;&lt;/b&gt; 포스팅을 참고하였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741657502426&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;GitOps Secret 관리&quot; data-og-description=&quot;GitOps에서 Secret 관리가 고민이시라구요? 그래서 준비했습니다, SealedSecret! GitOps는 우리의 삶을 편리하게 만들어 줍니다. 어플리케이션의 배포 상태를 완벽하게 반영해주어 Git에 저장된 배포 정의&quot; data-og-host=&quot;coffeewhale.com&quot; data-og-source-url=&quot;https://coffeewhale.com/sealedsecret&quot; data-og-url=&quot;https://coffeewhale.com/sealedsecret&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b57WNg/hyYmWZ5Skx/rsW5HTL8dt03yVG5S5321k/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/bk1S5J/hyYm87hg55/0SeuD6dwIanK9kIu1prkbk/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://coffeewhale.com/sealedsecret&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coffeewhale.com/sealedsecret&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b57WNg/hyYmWZ5Skx/rsW5HTL8dt03yVG5S5321k/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/bk1S5J/hyYm87hg55/0SeuD6dwIanK9kIu1prkbk/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitOps Secret 관리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitOps에서 Secret 관리가 고민이시라구요? 그래서 준비했습니다, SealedSecret! GitOps는 우리의 삶을 편리하게 만들어 줍니다. 어플리케이션의 배포 상태를 완벽하게 반영해주어 Git에 저장된 배포 정의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;coffeewhale.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;428&quot; data-start=&quot;400&quot; data-ke-size=&quot;size26&quot;&gt;GitOps와 Secret 관리의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps는 선언형 인프라와 지속적 배포를 위해 Git 저장소를 단일 진실의 원천(single source of truth)으로 사용하는 접근법입니다. 모든 애플리케이션 설정, 인프라 코드, 그리고 Secret까지 Git에 기록되면 버전 관리와 추적이 용이해지는 장점이 있습니다. 그러나 Secret 정보는 민감한 데이터이므로 평문으로 Git에 저장할 수 있지만 하면 안되는 행위이며, Git으로 관리하게 될 경우에는 암호화된 상태로 관리해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;686&quot; data-start=&quot;660&quot; data-ke-size=&quot;size26&quot;&gt;SealedSecrets란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SealedSecrets&lt;/b&gt;는 Bitnami에서 개발한 Kubernetes 커스텀 리소스(CRD)로, 민감한 정보를 암호화하여 Git 저장소에 안전하게 저장할 수 있게 해줍니다. SealedSecrets 오브젝트는 암호화된 데이터를 포함하고 있으며, 클러스터 내에 배포된 SealedSecrets 컨트롤러가 이를 감시하여 복호화 후 일반 Secret 리소스로 변환합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1243&quot; data-start=&quot;899&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1064&quot; data-start=&quot;899&quot;&gt;&lt;b&gt;암호화와 복호화&lt;/b&gt;&lt;br /&gt;SealedSecrets는 공개키/개인키 암호화 방식을 사용합니다. 개발자가 공개키로 암호화된 SealedSecret YAML 파일을 생성하면, 클러스터에 설치된 컨트롤러는 미리 설정된 개인키를 이용해 복호화한 뒤 Kubernetes Secret으로 변환합니다.&lt;/li&gt;
&lt;li data-end=&quot;1243&quot; data-start=&quot;1068&quot;&gt;&lt;b&gt;GitOps와의 연계&lt;/b&gt;&lt;br /&gt;암호화된 SealedSecrets 파일은 Git 저장소에 안전하게 커밋할 수 있으므로, GitOps 파이프라인에서 Secret 관리가 용이해집니다. 코드 리뷰 및 버전 관리가 가능해지고, 추후 변경 이력을 통해 누가 언제 어떤 Secret이 변경되었는지 추적할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1276&quot; data-start=&quot;1250&quot; data-ke-size=&quot;size26&quot;&gt;SealedSecrets의 동작 원리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1647&quot; data-start=&quot;1278&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1400&quot; data-start=&quot;1278&quot;&gt;&lt;b&gt;암호화 단계&lt;/b&gt;&lt;br /&gt;개발자는 kubeseal CLI 도구를 사용해 일반 Kubernetes Secret을 암호화합니다. 이 때 클러스터에 설치된 SealedSecrets 컨트롤러의 공개키를 사용합니다.&lt;/li&gt;
&lt;li data-end=&quot;1499&quot; data-start=&quot;1405&quot;&gt;&lt;b&gt;저장 단계&lt;/b&gt;&lt;br /&gt;암호화된 SealedSecret YAML 파일을 Git 저장소에 커밋합니다. 이 파일은 평문이 아니므로 민감한 정보가 노출되지 않습니다.&lt;/li&gt;
&lt;li data-end=&quot;1647&quot; data-start=&quot;1504&quot;&gt;&lt;b&gt;복호화 및 배포&lt;/b&gt;&lt;br /&gt;클러스터에서 SealedSecrets 컨트롤러가 해당 CRD를 감지하면, 내부의 개인키를 사용해 복호화한 뒤 실제 Secret으로 생성합니다. 이 과정은 자동으로 이루어지며, GitOps 도구가 이를 트리거하게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1680&quot; data-start=&quot;1654&quot; data-ke-size=&quot;size26&quot;&gt;SealedSecrets 사용의 장점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1740&quot; data-start=&quot;1682&quot;&gt;&lt;b&gt;보안 강화&lt;/b&gt;&lt;br /&gt;민감한 데이터가 암호화된 형태로 저장되어 Git 저장소에 노출되지 않습니다.&lt;/li&gt;
&lt;li data-end=&quot;1831&quot; data-start=&quot;1744&quot;&gt;&lt;b&gt;GitOps와의 통합&lt;/b&gt;&lt;br /&gt;모든 클러스터 설정이 Git에 저장되므로, 배포 파이프라인에 자연스럽게 녹아들어 자동화된 배포 및 롤백이 가능합니다.&lt;/li&gt;
&lt;li data-end=&quot;1923&quot; data-start=&quot;1835&quot;&gt;&lt;b&gt;버전 관리 및 감사 추적&lt;/b&gt;&lt;br /&gt;SealedSecrets 파일 역시 Git으로 관리되므로, 언제 누가 어떤 변경을 했는지 쉽게 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고려사항 및 한계&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2048&quot; data-start=&quot;1947&quot;&gt;&lt;b&gt;키 관리&lt;/b&gt;&lt;br /&gt;SealedSecrets 컨트롤러의 개인키와 공개키가 일치해야 하며, 키 회전 시 기존 SealedSecret 파일을 재생성해야 하는 번거로움이 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2162&quot; data-start=&quot;2052&quot;&gt;&lt;b&gt;복잡한 Secret 구조&lt;/b&gt;&lt;br /&gt;복잡한 Secret 구성이나 동적 값이 필요한 경우, 관리에 주의가 필요하며 별도의 템플릿화나 CI/CD 파이프라인 내 자동화 작업이 요구될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps 환경에서 Secret 관리 문제는 보안과 운영 효율성을 동시에 고려해야 하는 중요한 과제입니다. SealedSecrets는 민감한 정보를 안전하게 암호화하여 Git 저장소에 보관할 수 있도록 도와줌으로써, GitOps 방식의 이점을 극대화할 수 있는 강력한 도구입니다. 앞으로 이어질 포스팅에서는 SealedSecrets의 설치, 활용법, 그리고 이를 효과적으로 사용하기위한 Application을 제작하여 제가 실무에 적용했던 사례 등을 통해 더 구체적인 사용 방법을 포스팅하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>bitnami sealedsecrets</category>
      <category>gitops k8s secret</category>
      <category>gitops k8s secrets 관리</category>
      <category>gitops secrets 관리하기</category>
      <category>k8s secret 관리</category>
      <category>k8s secrets 관리하기</category>
      <category>k8s secrets 암호화</category>
      <category>kubernetes secrets git관리</category>
      <category>sealedsecrets</category>
      <category>secrets 관리</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/346</guid>
      <comments>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitOps%EB%A5%BC-%EC%9C%84%ED%95%9C-Secret-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-1-SealedSecrets-%EA%B0%9C%EB%85%90#entry346comment</comments>
      <pubDate>Tue, 11 Mar 2025 12:45:43 +0900</pubDate>
    </item>
    <item>
      <title>GitLab &amp;amp; ArgoCD를 활용한 GitOps방식의 CI/CD 구축하기 (3) - GitlabCI &amp;amp; ArgoCD 연동하여 CI/CD 구축 (Manual)</title>
      <link>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-3-GitlabCI-ArgoCD-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitlabCI 및 ArgoCD 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitlabCI에서는 이전 포스팅에서 설명한 대로, Application을 Build 하여 Conatiner Image로 만든 후 Docker Image Registry인 NCP Container Resitry에 Upload 합니다. 그 후 helm repository에 Image Tag값을 변경해 주면 ArgoCD의 Sync 옵션이 Auto일 경우 OutOfSync일 경우 helm repo와 자동으로 Sync 하여 배포를 완료해 주지만, 저의 경우 Sync 옵션을 Manual로 지정하였기 때문에 GitlabCI에서 ArgoCD의 Sync API까지 호출해 주도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;1804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0cxI7/btsMFkErwCu/xkcDwkVlDJkHbr3HLWE0Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0cxI7/btsMFkErwCu/xkcDwkVlDJkHbr3HLWE0Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0cxI7/btsMFkErwCu/xkcDwkVlDJkHbr3HLWE0Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0cxI7%2FbtsMFkErwCu%2FxkcDwkVlDJkHbr3HLWE0Dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2340&quot; height=&quot;1804&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;1804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitLab CI/CD 메커니즘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab의 CICD PipeLine 메커니즘은 GitLab 저장소에 내장된 자동화 도구로, 코드의 빌드, 테스트, 배포를 자동화하여 지속적인 통합(CI)과 지속적인 배포(CD)를 실현해 주는데, 주요 구성요소는 다음과 같습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;117&quot; data-start=&quot;105&quot; data-ke-size=&quot;size23&quot;&gt;주요 구성 요소&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;505&quot; data-start=&quot;119&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;210&quot; data-start=&quot;119&quot;&gt;&lt;b&gt;. gitlab-ci.yml 파일&lt;/b&gt;&lt;br /&gt;파이프라인의 정의 파일로, 각 스테이지(build, test, deploy 등)와 그 안의 job들을 선언합니다.&lt;/li&gt;
&lt;li data-end=&quot;376&quot; data-start=&quot;212&quot;&gt;&lt;b&gt;스테이지(Stage) &amp;amp; Job&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;376&quot; data-start=&quot;240&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;305&quot; data-start=&quot;240&quot;&gt;&lt;b&gt;스테이지:&lt;/b&gt; 파이프라인의 전체적인 단계(예: 빌드, 테스트, 배포)로, 각 스테이지는 순차적으로 실행됩니다.&lt;/li&gt;
&lt;li data-end=&quot;376&quot; data-start=&quot;308&quot;&gt;&lt;b&gt;Job:&lt;/b&gt; 각 스테이지 내에서 실행되는 작업 단위로, 병렬 실행이 가능하며, 특정 스크립트나 명령어를 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;505&quot; data-start=&quot;378&quot;&gt;&lt;b&gt;GitLab Runner&lt;/b&gt;&lt;br /&gt;정의된 job을 실제로 실행하는 에이전트입니다. Runner는 Docker, VM, 또는 실제 머신 등 다양한 환경에서 동작할 수 있어, 유연하게 CI/CD 환경을 구성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;516&quot; data-start=&quot;507&quot; data-ke-size=&quot;size23&quot;&gt;작동 방식&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;862&quot; data-start=&quot;518&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;600&quot; data-start=&quot;518&quot;&gt;&lt;b&gt;트리거(Trigger)&lt;/b&gt;&lt;br /&gt;코드 커밋, Merge Request, 스케줄 등 특정 이벤트에 의해 파이프라인이 자동으로 시작됩니다.&lt;/li&gt;
&lt;li data-end=&quot;697&quot; data-start=&quot;602&quot;&gt;&lt;b&gt;파이프라인 실행&lt;/b&gt;&lt;br /&gt;. gitlab-ci.yml에 정의된 스테이지 순서대로 job들이 실행되며, 각 job은 지정된 Runner에서 독립적으로 실행됩니다.&lt;/li&gt;
&lt;li data-end=&quot;791&quot; data-start=&quot;699&quot;&gt;&lt;b&gt;결과 보고&lt;/b&gt;&lt;br /&gt;각 job의 성공, 실패 여부와 로그가 GitLab UI에 표시되어, 개발자와 시스템 엔지니어가 빌드 상태를 모니터링할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;862&quot; data-start=&quot;793&quot;&gt;&lt;b&gt;아티팩트와 캐시&lt;/b&gt;&lt;br /&gt;job 간에 결과물을 공유하거나, 빌드 속도를 높이기 위해 캐시를 활용할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kaniko&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kaniko는 쿠버네티스 환경에서 컨테이너 이미지를 안전하고 효율적으로 빌드할 수 있도록 고안된 도구입니다. 기존의 Docker 빌드 방식은 Docker 데몬에 의존하기 때문에, 이미지 빌드를 위해 데몬을 실행해야 하고 이 과정에서 보안 취약점이나 권한 문제 등이 발생할 수 있습니다. 특히, 쿠버네티스 클러스터와 같은 분산 환경에서는 Docker 데몬을 실행하는 컨테이너가 추가적인 보안 리스크를 내포하게 되어 관리가 복잡해질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;490&quot; data-start=&quot;249&quot; data-ke-size=&quot;size16&quot;&gt;이에 비해 Kaniko는 Docker 데몬 없이 Dockerfile을 읽어 이미지 레이어를 순차적으로 생성하는 방식으로 동작합니다. 이로 인해 빌드 과정에서 특권 모드가 필요 없으며, 권한이 낮은 상태에서도 안전하게 이미지를 생성할 수 있습니다. 또한, Kaniko는 컨테이너 내부에서 직접 빌드 작업을 수행하고, 완성된 이미지를 외부 레지스트리로 직접 푸시하는 기능을 제공하므로 CI/CD 파이프라인에 통합하기에 매우 적합합니다.&lt;/p&gt;
&lt;p data-end=&quot;490&quot; data-start=&quot;249&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;688&quot; data-start=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;뿐만 아니라 Kaniko는 리소스 사용 효율성이 뛰어나며, 클러스터 내부의 다른 작업에 미치는 영향이 적습니다. 이는 대규모 분산 환경이나 자동화된 빌드 시스템에서 중요한 요소로 작용합니다. 보안, 효율성, 그리고 관리의 용이성 측면에서 Kaniko는 쿠버네티스 환경에서 컨테이너 이미지 빌드를 수행할 때 선택해야 할 강력한 도구라고 할 수 있습니다. 때문에 저는 Kaniko를 사용하여 Container Image를 빌드하였습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;688&quot; data-start=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;688&quot; data-start=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;사용방법은 gitlab 공식 docs에서 확인 가능합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741584846096&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Use kaniko to build Docker images | GitLab Docs&quot; data-og-description=&quot;Use kaniko to build Docker images Tier: Free, Premium, UltimateOffering: GitLab.com, GitLab Self-Managed, GitLab Dedicated kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster. kaniko solves two problems wi&quot; data-og-host=&quot;docs.gitlab.com&quot; data-og-source-url=&quot;https://docs.gitlab.com/ci/docker/using_kaniko/&quot; data-og-url=&quot;https://docs.gitlab.com/ci/docker/using_kaniko/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.gitlab.com/ci/docker/using_kaniko/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.gitlab.com/ci/docker/using_kaniko/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Use kaniko to build Docker images | GitLab Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Use kaniko to build Docker images Tier: Free, Premium, UltimateOffering: GitLab.com, GitLab Self-Managed, GitLab Dedicated kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster. kaniko solves two problems wi&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.gitlab.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;build-and-push-image&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gitlab-ci.yaml 파일에서는 원하는 형태의 stages를 설정할 수 있습니다. 저는 &quot;build-and-push-image&quot;, &quot;update-helm-chart&quot; 2가지의 stage로 구분 지어 gitlab-ci.yaml 파일을 작성하였는데, 첫 번째로 build-and-push-image를 구현해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kaniko를 사용하기 위해서는 &quot;gcr.io/kaniko-project/executor:v1.23.2-debug&quot; 이미지를 사용하여야 합니다. debug이미지를 사용하는 이유는 shell이 debug 이미지에만 존재하기 때문입니다. 공식 문서에서도 debug image를 recommended 하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MvnBq/btsMFxXSlpC/obJ4ggsiOsuQSgVzegCqxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MvnBq/btsMFxXSlpC/obJ4ggsiOsuQSgVzegCqxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MvnBq/btsMFxXSlpC/obJ4ggsiOsuQSgVzegCqxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMvnBq%2FbtsMFxXSlpC%2FobJ4ggsiOsuQSgVzegCqxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;201&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1741584999383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;image:
  name: gcr.io/kaniko-project/executor:v1.23.2-debug
  entrypoint:
    - &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stage는 위에서 설명한 것과 같이 두 가지로 구분 지었습니다. 이미지 태그는 GitLab CI에서 제공해 주는 Default Variables를 참고하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741585150786&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stages:
  - build-and-push-image
  - update-helm-chart
  
variables:
  DOCKER_CONFIG: /kaniko/.docker/
  IMAGE_TAG: $CI_COMMIT_SHORT_SHA&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prod와 develop 환경의 container image명이 다르기 때문에 main과 develop 브랜치로 구분 지어 추가 variables를 설정하였고, NCP의 Container Registry에 접근하기 위해 Login 관련 정보를 GitLab Groups에 환경변수로 설정해 두었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741585273510&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1) Build 스테이지: kaniko를 사용하여 Docker 이미지를 빌드하고 GitLab Registry로 푸시
build:
  stage: build-and-push-image
  rules:
    - if: '$CI_COMMIT_BRANCH == &quot;main&quot;'
      variables:
        NCP_REGISTRY_IMAGE: &quot;mingyu-api&quot;
        DOCKER_ENV: &quot;prod&quot;
    - if: '$CI_COMMIT_BRANCH == &quot;develop&quot;'
      variables:
        NCP_REGISTRY_IMAGE: &quot;mingyu-api-dev&quot;
        DOCKER_ENV: &quot;develop&quot;
    # rules에 해당하지 않는 브랜치에서는 실행하지 않음, only와 rules 중 하나만 사용해야 하기 때문에 when never 추가
    - when: never
  script:
    # Docker config 생성
    - mkdir -p /kaniko/.docker
    - echo &quot;{\&quot;auths\&quot;:{\&quot;$NCP_REGISTRY\&quot;:{\&quot;username\&quot;:\&quot;$NCP_REGISTRY_USER\&quot;,\&quot;password\&quot;:\&quot;$NCP_REGISTRY_PASSWORD\&quot;}}}&quot; &amp;gt; /kaniko/.docker/config.json

    # Kaniko로 빌드
    - /kaniko/executor
      --context &quot;$CI_PROJECT_DIR&quot;
      --dockerfile &quot;$CI_PROJECT_DIR/Dockerfile&quot;
      --build-arg &quot;ENVIRONMENT=$DOCKER_ENV&quot;
      --destination &quot;$NCP_REGISTRY/$NCP_REGISTRY_IMAGE:$IMAGE_TAG&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Stage에서 1~4번 항목을 처리합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;1804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/55Gvz/btsMENtJrMW/nVPo1YtoQmtTphK8VuPXP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/55Gvz/btsMENtJrMW/nVPo1YtoQmtTphK8VuPXP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/55Gvz/btsMENtJrMW/nVPo1YtoQmtTphK8VuPXP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F55Gvz%2FbtsMENtJrMW%2FnVPo1YtoQmtTphK8VuPXP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2340&quot; height=&quot;1804&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;1804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Update-helm-chart&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 stage에서는 Helm repository Git 저장소를 clone 받아서 Image Tag를 Update 한 후 commit &amp;amp; push를 진행합니다. 또한 ArgoCD에 Sync API를 호출하여 변동된 OutOfSync를 Sync 하게 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741586017944&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;deploy:
  stage: update-helm-chart
  rules:
    - if: '$CI_COMMIT_BRANCH == &quot;main&quot;'
      variables:
        NCP_REGISTRY_IMAGE: &quot;mingyu-api&quot;
        HELM_VALUES_FILE: &quot;custom-values/mingyu-api/prod-values.yaml&quot;
        ARGOCD_APP: &quot;mingyu-api-prod&quot;
    - if: '$CI_COMMIT_BRANCH == &quot;develop&quot;'
      variables:
        NCP_REGISTRY_IMAGE: &quot;mingyu-api-dev&quot;
        HELM_VALUES_FILE: &quot;custom-values/mingyu-api/develop-values.yaml&quot;
        ARGOCD_APP: &quot;mingyu-api-dev&quot;
    # 다른 브랜치에서는 global 변수 값(&quot;latest&quot;)을 사용하기 위해 fallback rule 추가
    - when: always
  image: alpine:latest  # 혹은 git, yq, helm 등 툴을 사용할 수 있는 이미지
  variables:
    GIT_STRATEGY: none   # 빌드 속도 향상 (필요 시 변경)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 항목에서는 Commit 한 브랜치명에 따라 개발 혹은 운영 환경에 맞추어 변수를 초기화해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741586112873&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  before_script:
    # 필수 패키지 설치
    - apk add --no-cache git yq openssh-client curl jq

    # SSH 디렉토리 및 known_hosts 파일 준비
    - mkdir -p ~/.ssh
    - ssh-keyscan gitlab-sh.mingyu.co.kr &amp;gt;&amp;gt; ~/.ssh/known_hosts
    - chmod 700 ~/.ssh
    - chmod 644 ~/.ssh/known_hosts

    # SSH Private Key를 ssh-agent에 추가
    - eval $(ssh-agent -s)
    - echo &quot;$MINGYU_GITLAB_SH_SSH_PRIVATE_KEY&quot; | tr -d '\r' | ssh-add -&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 script가 실행되기 전에 before_script를 사용하여 필요한 필수 패키지 및 디렉터리, ssh 설정 작업 등 사전작업을 처리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741586268113&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  script:
    # 1) GitOps 리포지토리 클론
    - git clone &quot;$GITLAB_HELM_SPRING_BOOT_GIT_PATH&quot; # git@gitlab-sh.mingyu.co.kr:charts/backend/mingyu-spring-boot.git
    - cd &quot;$GITLAB_HELM_SPRING_BOOT_DIR_NAME&quot; # mingyu-spring-boot
    - git fetch origin &quot;$CI_COMMIT_BRANCH&quot;
    - git checkout &quot;$CI_COMMIT_BRANCH&quot;

    # 2) values.yaml 업데이트 (예시)
    - echo &quot;IMAGE_REPOSITORY = $NCP_REGISTRY/$NCP_REGISTRY_IMAGE&quot;
    - echo &quot;IMAGE_TAG = $IMAGE_TAG&quot;
    - yq -i &quot;.image.repository = \&quot;$NCP_REGISTRY/$NCP_REGISTRY_IMAGE\&quot;&quot; &quot;$HELM_VALUES_FILE&quot;
    - yq -i &quot;.image.tag = \&quot;$IMAGE_TAG\&quot;&quot; &quot;$HELM_VALUES_FILE&quot;

    # 3) Git 커밋 &amp;amp; 푸시
    - git config user.email &quot;mingyu@mingyu.co.kr&quot;
    - git config user.name &quot;mingyu&quot;
    - git add &quot;$HELM_VALUES_FILE&quot;
    - git commit -m &quot;Update image tag to $IMAGE_TAG&quot;
    - git push origin &quot;$CI_COMMIT_BRANCH&quot;

    # 4) argocd sync
    - echo &quot;ArgoCD Server = $ARGOCD_SERVER&quot;
    - echo &quot;ArgoCD USERNAME = $ARGOCD_USERNAME&quot;
    - echo &quot;ArgoCD Password = $ARGOCD_PASSWORD&quot;
    - |
      # ArgoCD 로그인하여 토큰 받기 (인증서 검증 문제가 있다면 -k 옵션 사용)        
        TOKEN=$(curl -sk -X POST &quot;https://$ARGOCD_SERVER/api/v1/session&quot; \
          -H &quot;Content-Type: application/json&quot; \
          -d &quot;{\&quot;username\&quot;: \&quot;${ARGOCD_USERNAME}\&quot;, \&quot;password\&quot;: \&quot;${ARGOCD_PASSWORD}\&quot;}&quot; | jq -r '.token')
        echo &quot;ArgoCD token: $TOKEN&quot;
      
      # Application sync API 호출 (revision은 HEAD 혹은 원하는 값으로 지정)
        curl -sk -X POST &quot;https://$ARGOCD_SERVER/api/v1/applications/$ARGOCD_APP/sync&quot; \
          -H &quot;Authorization: Bearer $TOKEN&quot; \
          -H &quot;Content-Type: application/json&quot; \
          -d &quot;{\&quot;revision\&quot;: \&quot;${CI_COMMIT_BRANCH}\&quot;}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script 태그에서 해당 stage에서 처리해야 할 작업들을 기술하여 처리하도록 합니다. 필요한 Variables들은 GitLab Settings &amp;gt; CI/CD &amp;gt; Variables에서 설정 및 관리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ocOcQ/btsMHnmlKzL/v731qmz816kwkzFkz9LAhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ocOcQ/btsMHnmlKzL/v731qmz816kwkzFkz9LAhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ocOcQ/btsMHnmlKzL/v731qmz816kwkzFkz9LAhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FocOcQ%2FbtsMHnmlKzL%2Fv731qmz816kwkzFkz9LAhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;911&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 완료하면, 해당 Stage에서 5~8번 항목을 완료하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;1804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czQHlr/btsMGr3UIoF/ktOvXGhZFOXGE8PhhfTAe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czQHlr/btsMGr3UIoF/ktOvXGhZFOXGE8PhhfTAe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czQHlr/btsMGr3UIoF/ktOvXGhZFOXGE8PhhfTAe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczQHlr%2FbtsMGr3UIoF%2FktOvXGhZFOXGE8PhhfTAe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2340&quot; height=&quot;1804&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;1804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;commit &amp;amp; push를 진행하면 gitlab PipeLies에서 작업 내역을 아래와 같이 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHUZgr/btsMGCRJmj3/FDxTDiz6myzAiZc9fipk70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHUZgr/btsMGCRJmj3/FDxTDiz6myzAiZc9fipk70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHUZgr/btsMGCRJmj3/FDxTDiz6myzAiZc9fipk70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHUZgr%2FbtsMGCRJmj3%2FFDxTDiz6myzAiZc9fipk70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1249&quot; height=&quot;253&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab Runner가 생성한 새로운 Build Stage를 처리해 주는 파드가 생성된 것을 확인할 수 있습니다. Build Stage가 끝나면 Deploy Stage에 해당하는 Pod가 생성돼서 Develop Stage를 처리하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo0UtH/btsMFvMBWJB/xy1pcGzDGN2fAVsSWrKl20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo0UtH/btsMFvMBWJB/xy1pcGzDGN2fAVsSWrKl20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo0UtH/btsMFvMBWJB/xy1pcGzDGN2fAVsSWrKl20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo0UtH%2FbtsMFvMBWJB%2Fxy1pcGzDGN2fAVsSWrKl20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1622&quot; height=&quot;220&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Build Stage까지 프로세스가 정상적으로 완료되면 아래와 같이 Stages의 두 개의 아이콘이 초록색으로 표시됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cw8IU/btsMGsn9ffl/NAJhDqwfx1KSq66YOg9v21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cw8IU/btsMGsn9ffl/NAJhDqwfx1KSq66YOg9v21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cw8IU/btsMGsn9ffl/NAJhDqwfx1KSq66YOg9v21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCw8IU%2FbtsMGsn9ffl%2FNAJhDqwfx1KSq66YOg9v21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1249&quot; height=&quot;138&quot; data-origin-width=&quot;1249&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음, ArgoCD UI로 접속하여 Sync가 정상적으로 작동하였는지 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/99GnF/btsMKdK11Sq/N8JG0kUvDeGkzhQ1Vatjt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/99GnF/btsMKdK11Sq/N8JG0kUvDeGkzhQ1Vatjt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/99GnF/btsMKdK11Sq/N8JG0kUvDeGkzhQ1Vatjt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F99GnF%2FbtsMKdK11Sq%2FN8JG0kUvDeGkzhQ1Vatjt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;900&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타깃 Cluster의 대시보드에 접속하거나 kubectl 명령으로 새롭게 생성된 파드가 정상적으로 동작하는지, 이미지는 최신 이미지 태그를 사용하였는지 등을 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjMkLc/btsMEQKGZLV/2CacCTiT4bGUcu09DREEQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjMkLc/btsMEQKGZLV/2CacCTiT4bGUcu09DREEQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjMkLc/btsMEQKGZLV/2CacCTiT4bGUcu09DREEQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjMkLc%2FbtsMEQKGZLV%2F2CacCTiT4bGUcu09DREEQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1654&quot; height=&quot;236&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCw9Dc/btsMFexEcjQ/UaqkiXb0qzRybx3axJHZs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCw9Dc/btsMFexEcjQ/UaqkiXb0qzRybx3axJHZs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCw9Dc/btsMFexEcjQ/UaqkiXb0qzRybx3axJHZs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCw9Dc%2FbtsMFexEcjQ%2FUaqkiXb0qzRybx3axJHZs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1598&quot; height=&quot;344&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 설계하고 구현한 PipeLine이 꼭 정답이라고는 할 수 없습니다. Application Project 1개당 Helm Repo 1개로 설정한다면 Auto Sync로 설정하면 더 간결해지기도 하고, Build를 다른 서드파티 툴 (Jenkins 등)을 활용할 수도 있습니다. 여러 방향성을 탐구해보고 상황에 맞는 CI/CD를 구축해보는 것. 그러면서 많은 안목을 넓히고 다양한 방법을 터득하는 길을 걷고 싶습니다.&lt;/p&gt;</description>
      <category>Infrastructure/CICD</category>
      <category>argocd gitlab</category>
      <category>argocd k8s 예제</category>
      <category>gitalb argo</category>
      <category>gitlab argocd</category>
      <category>gitlab argocd 연동</category>
      <category>gitlab ci argo cd</category>
      <category>gitlab k8s argocd</category>
      <category>gitlabci</category>
      <category>k8s gitlab argo</category>
      <category>kubernetes gitlab argocd</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/345</guid>
      <comments>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-3-GitlabCI-ArgoCD-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0#entry345comment</comments>
      <pubDate>Mon, 10 Mar 2025 20:47:44 +0900</pubDate>
    </item>
    <item>
      <title>GitLab &amp;amp; ArgoCD를 활용한 GitOps방식의 CI/CD 구축하기 (2) - ArgoCd에 Cluster 추가하기</title>
      <link>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-2-ArgoCd%EC%97%90-Cluster-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;ArgoCd에 application이 배포될 dev-cluster와 prod-cluster를 연동해 보겠습니다. Arogcd Setting 메뉴에 들어가면 Cluster 추가는 UI를 사용하여 진행할 수 없고, &lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-management/#adding-a-cluster&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Documentaion을 통해 진행&lt;/a&gt;하라는 내용이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1919&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzkJEG/btsMCuVgXBi/bkZ1dR9TpA7LijfQNGbs61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzkJEG/btsMCuVgXBi/bkZ1dR9TpA7LijfQNGbs61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzkJEG/btsMCuVgXBi/bkZ1dR9TpA7LijfQNGbs61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzkJEG%2FbtsMCuVgXBi%2FbkZ1dR9TpA7LijfQNGbs61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1919&quot; height=&quot;489&quot; data-origin-width=&quot;1919&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ArgoCD CLI Install&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 MacOS 기준으로 포스팅하겠습니다. 각 클러스터에 접속할 수 있는 configure 설정값들이 들어있는 kubeconfig.yaml 파일이 로컬에 있다는 가정하에 local pc에 argocd cli 프로그램을 brew로 쉽게 설치할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741309101916&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 nks를 사용하고 있고, ncp의 ncp-iam-authenticator을 설치하여 &lt;i&gt;&lt;b&gt;&lt;u&gt;dev, mgt, prod 각 클러스터의 kubeconfig파일을 만들어서 하나로&lt;/u&gt; merge 시킨 &lt;span style=&quot;color: #ee2323;&quot;&gt;merged-kubeconfig.yaml 파일&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;을 가지고 있다는 전제하에 포스팅을 이어가겠습니다.&amp;nbsp; NKS를 사용 중이신 분들은 &lt;a href=&quot;https://guide.ncloud-docs.com/docs/k8s-iam-auth-ncp-iam-authenticator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크의&lt;/a&gt; 내용을 토대로 kubeconfig파일 생성에 참고하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ArgoCD CLI Login&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어로 UI환경이 아닌 CLI 환경에서 argocd에 로그인할 수 있습니다. 저는 DNS와 Ingress로 먼저 생성시켜놓은 ALB를 연결해놨기 때문에 아래와 같이 도메인으로 접속이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741309441078&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;argocd login https://argocd.mingyu.co.kr&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 설정을 하지 않아도 port foward 기능을 사용하여 로그인이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741309623918&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl port-forward svc/argocd-server -n argocd 8080:443
argocd login localhost:8080&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2610&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbYH4m/btsMCZOdDgO/oG6a1rQFiI0N49qckkVpd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbYH4m/btsMCZOdDgO/oG6a1rQFiI0N49qckkVpd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbYH4m/btsMCZOdDgO/oG6a1rQFiI0N49qckkVpd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbYH4m%2FbtsMCZOdDgO%2FoG6a1rQFiI0N49qckkVpd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2610&quot; height=&quot;322&quot; data-origin-width=&quot;2610&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Add Cluster In ArgoCD&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Merged-kubeconfig.yaml 파일을 KUBECONFIG로 export 해두었다면, 일반 kubeconfig 명령어로 cluster name을 확인할 수 있습니다. kubeconfig 파일을 merge하는 방법은 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741310157719&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# config파일들이 있는 폴더로 접근
cd ~/Workspace/kubeconfig

# KUBECONFIG 설정
export KUBECONFIG=$(pwd)/mgt-kubeconfig.yaml:$(pwd)/dev-kubeconfig.yaml:$(pwd)/prod-kubeconfig.yaml

# 설정된 config 설정을 merged-kubeconfig.yaml 파일로 생성
kubectl config view --flatten &amp;gt; merged-kubeconfig.yaml

# KUBECONFIG 재설정
export KUBECONFIG=$(pwd)/merged-kubeconfig.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후에 config get contexts 명령을 사용하여 설정된 cluster의 이름을 가져올 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741310001912&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl config get-contexts&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7BXP6/btsMDHMR5Ou/lLLqSMv5iUkRlYwp9Mdz9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7BXP6/btsMDHMR5Ou/lLLqSMv5iUkRlYwp9Mdz9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7BXP6/btsMDHMR5Ou/lLLqSMv5iUkRlYwp9Mdz9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7BXP6%2FbtsMDHMR5Ou%2FlLLqSMv5iUkRlYwp9Mdz9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1394&quot; height=&quot;248&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mgt cluster는 argocd가 설치된 in-cluster로 이미 설정되어 있으니, dev 및 prod 클러스터를 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741310458741&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;argocd cluster add nks_kr_dev-cluster_{hash}
argocd cluster add nks_kr_prod-cluster_{hash}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/CICD</category>
      <category>add cluster in argocd</category>
      <category>argocd cluster add</category>
      <category>argocd cluster 멀티</category>
      <category>argocd cluster 여러개</category>
      <category>argocd cluster 연동</category>
      <category>argocd cluster 추가</category>
      <category>argocd multi cluster</category>
      <category>argocd 클러스터 추가</category>
      <category>cluster argocd 추가하기</category>
      <category>다중 cluster argocd 추가</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/344</guid>
      <comments>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-2-ArgoCd%EC%97%90-Cluster-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0#entry344comment</comments>
      <pubDate>Sat, 8 Mar 2025 15:23:35 +0900</pubDate>
    </item>
    <item>
      <title>GitLab &amp;amp; ArgoCD를 활용한 GitOps방식의 CI/CD 구축하기 (1) - CI/CD 설계 및 Helm 생성</title>
      <link>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1-CICD-%EC%84%A4%EA%B3%84-%EB%B0%8F-Helm-%EC%83%9D%EC%84%B1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서는 &lt;u&gt;&quot;&lt;b&gt;개발환경 없는 회사에서 NKS로 개발환경 구성하기&quot;&lt;/b&gt;&lt;/u&gt;라는 키워드를 기준으로 포스팅하였다면, 본 포스팅부터는 프로덕션환경과 개발환경 모두를 다루기 때문에 &lt;u&gt;&lt;b&gt;&quot;Cloud Kubernetes 환경 구축&quot;&lt;/b&gt;&lt;/u&gt;이라는 키워드로 변경하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게나마 GitOps에 대한 개념을 알아본 후에 mgt-cluster에 설치한 ArgoCD, GitLab을 토대로 GitOps방식의 CI/CD를 설계 및 구현해 보겠습니다. ArgoCD 및 gitLab 설치는 이전 포스팅을 참고해 주시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1741235871944&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kubernetes 도입] 개발환경 없는 회사에서 NKS로 개발환경 구성하기 Chapter 7. gitops와 argocd 도입을 위&quot; data-og-description=&quot;서론지금까지는 Naver Cloud Platform(이하 NCP)에서 제공해 주는 Source Series(Commit, Build,Deploy, PipeLine)을 사용하여 CI/CD를 진행했다면, 쿠버네티스의 장점을 고루 살린 GitOps와 ArgoCd를 사용하여 CI/CD를 마&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cGq5M4/hyYmVMUxxQ/Cb7NRXRSnaHPinI5g9hjk0/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/v2T37/hyYmKdxNKU/1K0NLFhEgccDkilhGKQWdK/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/4vGpy/hyYm8rQ6PI/ztSDUwVt8U0HoToty8kF70/img.png?width=1920&amp;amp;height=990&amp;amp;face=0_0_1920_990&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cGq5M4/hyYmVMUxxQ/Cb7NRXRSnaHPinI5g9hjk0/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/v2T37/hyYmKdxNKU/1K0NLFhEgccDkilhGKQWdK/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/4vGpy/hyYm8rQ6PI/ztSDUwVt8U0HoToty8kF70/img.png?width=1920&amp;amp;height=990&amp;amp;face=0_0_1920_990');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kubernetes 도입] 개발환경 없는 회사에서 NKS로 개발환경 구성하기 Chapter 7. gitops와 argocd 도입을 위&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론지금까지는 Naver Cloud Platform(이하 NCP)에서 제공해 주는 Source Series(Commit, Build,Deploy, PipeLine)을 사용하여 CI/CD를 진행했다면, 쿠버네티스의 장점을 고루 살린 GitOps와 ArgoCd를 사용하여 CI/CD를 마&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1741235854695&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kubernetes 도입] 개발환경 없는 회사에서 NKS로 개발환경 구성하기 Chapter 8. NKS에 GitLab, GitLab-SSH, GitL&quot; data-og-description=&quot;서론소스코드 저장소 호스팅의 대표주자는 GitLab, GitHub가 있으며 기본 기능은 source의 버전 관리를 해준다는 것으로 같지만 차이가 확실합니다. 깃헙과 깃랩의 가장 큰 차이점은 데브옵스에 있는&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-8-NKS%EC%97%90-GitLab-GitLab-SSH-GitLab-Runner-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-8-NKS%EC%97%90-GitLab-GitLab-SSH-GitLab-Runner-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/duYXl5/hyYmKq4aHx/FjzDNL31IOL3kINwuyrf61/img.png?width=720&amp;amp;height=652&amp;amp;face=0_0_720_652,https://scrap.kakaocdn.net/dn/4ELkS/hyYm1M1rCu/wKaYKK9IiBgeJOfyx2GlGk/img.png?width=720&amp;amp;height=652&amp;amp;face=0_0_720_652,https://scrap.kakaocdn.net/dn/BzolK/hyYm0gioZx/YK7KQP7giIHdfrFgdUUUKK/img.png?width=1914&amp;amp;height=869&amp;amp;face=0_0_1914_869&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-8-NKS%EC%97%90-GitLab-GitLab-SSH-GitLab-Runner-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-8-NKS%EC%97%90-GitLab-GitLab-SSH-GitLab-Runner-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/duYXl5/hyYmKq4aHx/FjzDNL31IOL3kINwuyrf61/img.png?width=720&amp;amp;height=652&amp;amp;face=0_0_720_652,https://scrap.kakaocdn.net/dn/4ELkS/hyYm1M1rCu/wKaYKK9IiBgeJOfyx2GlGk/img.png?width=720&amp;amp;height=652&amp;amp;face=0_0_720_652,https://scrap.kakaocdn.net/dn/BzolK/hyYm0gioZx/YK7KQP7giIHdfrFgdUUUKK/img.png?width=1914&amp;amp;height=869&amp;amp;face=0_0_1914_869');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kubernetes 도입] 개발환경 없는 회사에서 NKS로 개발환경 구성하기 Chapter 8. NKS에 GitLab, GitLab-SSH, GitL&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론소스코드 저장소 호스팅의 대표주자는 GitLab, GitHub가 있으며 기본 기능은 source의 버전 관리를 해준다는 것으로 같지만 차이가 확실합니다. 깃헙과 깃랩의 가장 큰 차이점은 데브옵스에 있는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps와 DevOps에 대한 개념은 아래 포스팅을 참고해 주시기 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741236863678&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;알기 쉽게 정의한 DevOps와 GitOps&quot; data-og-description=&quot;서론소프트웨어 개발과 운영이 점점 더 빠르게 변화하는 환경에서, 개발팀과 운영팀이 협업하여 안정적이고 신속하게 서비스를 제공할 수 있도록 돕는 방법론들이 주목받고 있습니다. 이 포스&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EC%9D%98%ED%95%9C-DevOps%EC%99%80-GitOps&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EC%9D%98%ED%95%9C-DevOps%EC%99%80-GitOps&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ekFLQU/hyYneexLHj/y2x6329odvrOyxPRp8kQBK/img.jpg?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/GAuC8/hyYmTBALFA/nXhyftQBkAIEf4pQf24Xl0/img.jpg?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/QD4yC/hyYmQkwd1Q/vc2fHrqd3adScHX4fWO4Z1/img.png?width=1000&amp;amp;height=701&amp;amp;face=0_0_1000_701&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EC%9D%98%ED%95%9C-DevOps%EC%99%80-GitOps&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EC%9D%98%ED%95%9C-DevOps%EC%99%80-GitOps&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ekFLQU/hyYneexLHj/y2x6329odvrOyxPRp8kQBK/img.jpg?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/GAuC8/hyYmTBALFA/nXhyftQBkAIEf4pQf24Xl0/img.jpg?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/QD4yC/hyYmQkwd1Q/vc2fHrqd3adScHX4fWO4Z1/img.png?width=1000&amp;amp;height=701&amp;amp;face=0_0_1000_701');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;알기 쉽게 정의한 DevOps와 GitOps&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론소프트웨어 개발과 운영이 점점 더 빠르게 변화하는 환경에서, 개발팀과 운영팀이 협업하여 안정적이고 신속하게 서비스를 제공할 수 있도록 돕는 방법론들이 주목받고 있습니다. 이 포스&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitOps PipeLine Example&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 GitOps 파이프라인은 아래 사진과 같은 형식으로 설계합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btONHR/btsMB1ZKXgP/YXIUOvY0xdJpn8f5ucFASK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btONHR/btsMB1ZKXgP/YXIUOvY0xdJpn8f5ucFASK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btONHR/btsMB1ZKXgP/YXIUOvY0xdJpn8f5ucFASK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtONHR%2FbtsMB1ZKXgP%2FYXIUOvY0xdJpn8f5ucFASK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;975&quot; height=&quot;494&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. application Source Code를 수정 및 Push/MR or PR 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. SourceCode Repository의 Push/MR or PR Event를 감지하여 변경된 내용을 기반으로 Build 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Build 된 Application을 Container화 하여 Image로 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4 만들어진 Image를 Container Registry에 Push 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 변경된 Image Tag 혹은 Config 등을 관리하는 별도의 Repository에 해당 사항을 Hook이나 기타 Trigger를 통하여 update 해줍니다. (저의 경우 Helm으로 관리하기에 Helm Repository라고 칭하겠습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. Helm Repository를 관찰하고 있는 Deploy Operator(저의 경우 ArgoCD)에서 변경 사항을 감지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. (Auto 설정 경우) 감지된 내용을 기반으로 ArgoCD에서 설정한 K8s Cluster의 Namespce에 알맞게 배포합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. (Manual 설정 경우) 별도의 Trigger를 기반으로 ArgoCD에서 설정한 K8s Cluster의 Namespce에 알맞게 배포합니다. 저는 Manual 방식으로 설계했는데 이유는 아래에서 언급하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CI/CD Flow 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 Docker, Jenkins를 활용한 CI/CD 구축 포스팅에서도 언급했던 것과 같이 구축하는 방법은 아주 많습니다. 설계자가 상황에 적합한 툴과 기술들을 사용해서 설계하는 게 중요하다고 생각하고, 저는 GitLab과 ArgoCD를 활용하여 아래 그림과 같이 cloud kubernetes 환경에서의 CI/CD를 설계하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2306&quot; data-origin-height=&quot;1763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oMwIy/btsMB1ldID8/Nqux1BM12oY4RisurSDTu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oMwIy/btsMB1ldID8/Nqux1BM12oY4RisurSDTu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oMwIy/btsMB1ldID8/Nqux1BM12oY4RisurSDTu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoMwIy%2FbtsMB1ldID8%2FNqux1BM12oY4RisurSDTu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2306&quot; height=&quot;1763&quot; data-origin-width=&quot;2306&quot; data-origin-height=&quot;1763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;저는 Manual 방식으로 설계&lt;/b&gt;&lt;/u&gt;하였는데, 저희 회사의 Application에 해당하는 k8s obejct 생성 관련 Helm 양식이 매우 비슷합니다. 사용할 이미지 명칭, Container, Deploy, Service 등의 명칭만 제외한다면 나머지 설정값은 너무나도 동일했습니다. Auto방식의 경우 Application Source Repository와 Helm Repository가 1:1 관계여야 한다고 생각하는데, &lt;b&gt;&lt;u&gt;동일한 내용의 많은 helm 레포지토리를&lt;/u&gt; 관리하는 게 불필요하다고 판단&lt;/b&gt;하여 비슷한 형식의 application은 한 개의 helm repository로 관리하기로 정했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Helm Charts 생성 (Spring Boot Application)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Springboot 기반의 API Application을 기준으로 spring_boot_helm이라는 명칭의 Helm을 생성하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741242628654&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# helm chart 생성
helm create spring_boot_helm

# custom-values 폴더 생성 및 초기 셋팅
cd spring_boot_helm
mkdir custom-values
cd custom-values

# 중복되는 springboot application에 맞는 폴더 생성
mkdir mingyu-api #mingyu2-api mingyu3-api ...
cd mingyu-api

# 각 브랜치 및 환경에 맞는 설정을 갖는 values 생성
touch develop-values.yaml
touch prod-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 기본 구조는 아래와 같습니다. 이제 이 helm을 토대로 mingyu-api의 develop 환경에는 develop-values.yaml을, production 환경에는 prod-values.yaml을 토대로 deployment와 service, ingress 등을 배포하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741242710138&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├── Chart.yaml
├── README.md
├── custom-values
│&amp;nbsp;&amp;nbsp; └── mingyu-api
│&amp;nbsp;&amp;nbsp;     ├── develop-values.yaml
│&amp;nbsp;&amp;nbsp;     └── prod-values.yaml
├── templates
│&amp;nbsp;&amp;nbsp; ├── NOTES.txt
│&amp;nbsp;&amp;nbsp; ├── _helpers.tpl
│&amp;nbsp;&amp;nbsp; ├── deployment.yaml
│&amp;nbsp;&amp;nbsp; ├── hpa.yaml
│&amp;nbsp;&amp;nbsp; ├── ingress.yaml
│&amp;nbsp;&amp;nbsp; ├── service.yaml
│&amp;nbsp;&amp;nbsp; ├── serviceaccount.yaml
│&amp;nbsp;&amp;nbsp; └── tests
│&amp;nbsp;&amp;nbsp;     └── test-connection.yaml
└── values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Temlates 수정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, Deployment와 Service만 사용해 줄 것이기 때문에 Deployment.yaml 파일과 service.yaml 파일을 custom-values의 내용으로 빌드될 수 있도록 아래와 같이 수정해 줬습니다. helm의 template 생성 가이드는 HELM 공식 문서에 상세히 기술되어 있습니다. 저의 경우 values 파일을 기반의 빌드를 설계하여 아래처럼 작성하였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741243643700&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Chart Template Guide&quot; data-og-description=&quot;Helm - The Kubernetes Package Manager.&quot; data-og-host=&quot;helm.sh&quot; data-og-source-url=&quot;https://helm.sh/docs/chart_template_guide/&quot; data-og-url=&quot;https://helm.sh/docs/chart_template_guide/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MdTzJ/hyYp9QpHBD/WUHzAfjWeZhFoRKk6Wkj1k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://helm.sh/docs/chart_template_guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://helm.sh/docs/chart_template_guide/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MdTzJ/hyYp9QpHBD/WUHzAfjWeZhFoRKk6Wkj1k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Chart Template Guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Helm - The Kubernetes Package Manager.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;helm.sh&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;deployment.yaml&lt;/h4&gt;
&lt;pre id=&quot;code_1741243533914&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.deployment.name }}
  namespace: {{ .Values.deployment.namespace }}
  labels:
    {{- toYaml .Values.deployment.labels | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- toYaml .Values.replicasetLabels | nindent 6 }}
  template:
    metadata:
      labels:
        {{- toYaml .Values.podLabels | nindent 8 }}
      annotations:
        {{- toYaml .Values.podAnnotations | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: &quot;{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}&quot;
          imagePullPolicy: {{ .Values.image.pullPolicy }}

          # 원본에서 사용한 command
          command:
          {{- toYaml .Values.command | nindent 12 }}

          ports:
            - name: http
              containerPort: {{ .Values.service.containerPort }}
              protocol: TCP

          envFrom:
            {{- toYaml .Values.envFrom | nindent 12 }}

          # LivenessProbe / ReadinessProbe
          {{- with .Values.livenessProbe }}
          livenessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}


          {{- with .Values.readinessProbe }}
          readinessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}

          resources:
            {{- toYaml .Values.resources | nindent 12 }}

          # 볼륨 마운트 추가 필요 시
          {{- with .Values.volumeMounts }}
          volumeMounts:
            {{- toYaml . | nindent 12 }}
          {{- end }}

      imagePullSecrets:
        {{ toYaml .Values.imagePullSecrets | nindent 8 }}

      # 볼륨 추가 필요 시
      {{- with .Values.volumes }}
      volumes:
        {{- toYaml . | nindent 8 }}
      {{- end }}

      # Node selector, Toleration, Affinity 등
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;service.yaml&lt;/h4&gt;
&lt;pre id=&quot;code_1741243569800&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.service.name }}
  namespace: {{ .Values.service.namespace }}
spec:
  type: {{ .Values.service.type }}
  selector:
    {{- toYaml .Values.service.selector | nindent 4 }}
  ports:
    - name: http
      port: {{ .Values.service.servicePort }}
      targetPort: http
      protocol: TCP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ingress.yaml&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우 ingress는 application당 하나씩 생성하지 않고, aws의 ALB를 활용하기 때문에 values의 ingress.enabled 속성을 false로 추가해 주었습니다. 그리고 ingress.yaml파일의 위아래에 if를 통하여 사용하지 않도록 설정해 주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741243733696&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
....
{{- end }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;custom-values.yaml 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 custom-values/mingyu-api/develop-values.yaml 파일 내용입니다. 아래 기술한 값들을 토대로 deployment, service, ingress 등의 template을 통해 k8s object를 컨트롤할 수 있는 yaml 파일이 생성되고 배포되며 관리됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741243960592&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ------------------------------------------------------------------------------
# [기본 설정: 배포 이름/네임스페이스/라벨 등]
nameOverride: &quot;mingyu-api&quot; # Helm Chart 이름
deployment:
  name: mingyu-api-deployment # Deployment 이름
  namespace: test # 배포할 namespace
  labels:
    app: mingyu-api-deployment
# ------------------------------------------------------------------------------
# [Pod/컨테이너 개수]
replicaCount: 1
# ------------------------------------------------------------------------------
# [이미지 설정]
image:
  repository: vjnibbzs.kr.private-ncr.ntruss.com/mingyu-api-dev
  pullPolicy: Always
  # Chart.yaml의 appVersion이 &quot;1.0.2&quot;지만, 필요 시 명시적으로 아래 tag를 지정할 수도 있습니다.
  tag: &quot;latest&quot;
# ------------------------------------------------------------------------------
# [이미지 풀 시크릿] - Private Registry 사용 시 필요한 경우
imagePullSecrets:
  - name: regcred
# ------------------------------------------------------------------------------
# [service 설정]
service:
  name: mingyu-api-service
  namespace: test
  type: NodePort
  # Service가 바라보는(매핑하려는) 컨테이너 포트
  containerPort: 80
  # Service 자체가 노출될 포트 (클라이언트가 접근하는 포트)
  servicePort: 80
  selector:
    app: mingyu-api-deployment
# ------------------------------------------------------------------------------
# [Ingress 설정]
ingress:
  enabled: false
  className: &quot;alb&quot;
  annotations:
    alb.ingress.kubernetes.io/listen-ports: '[{&quot;HTTP&quot;: 80}, {&quot;HTTPS&quot;: 443}]'
    alb.ingress.kubernetes.io/ssl-certificate-no: &quot;{your-ncp-ssl-certificate-no}&quot;
    alb.ingress.kubernetes.io/description: &quot;ingress controller&quot;
    alb.ingress.kubernetes.io/ssl-redirect: &quot;443&quot;
    alb.ingress.kubernetes.io/healthcheck-path: &quot;/health&quot;
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: &quot;120&quot;
  hosts:
    - host: dev-mingyu-api.test.co.kr
      paths:
        - path: /*
          pathType: Prefix
  tls: []
  # 예) tls:
  #   - secretName: chart-example-tls
  #     hosts:
  #       - dev-mingyu-api.test.co.kr
# ------------------------------------------------------------------------------
# [리소스 설정]
resources:
  requests:
    cpu: &quot;300m&quot;
    memory: &quot;256Mi&quot;
  limits:
    cpu: &quot;500m&quot;
    memory: &quot;512Mi&quot;
# ------------------------------------------------------------------------------
# [Probes]
# 원본에 맞추어 /health로 체크
livenessProbe:
  httpGet:
    path: /health/liveness
    port: http
  initialDelaySeconds: 90
  periodSeconds: 30
  timeoutSeconds: 5
  failureThreshold: 3
readinessProbe: 
  httpGet:
    path: /health/readiness
    port: http
  initialDelaySeconds: 20
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3
# ------------------------------------------------------------------------------
# [Pod, SA, SecurityContext 등]
serviceAccount:
  create: false
  automount: true
  name: &quot;&quot;
podAnnotations: {}
replicasetLabels: # replicaset이 관리할 pod 라벨
  app: mingyu-api-deployment
podLabels:
  app: mingyu-api-deployment # pod의 라벨
podSecurityContext: {}
securityContext: {}
# ------------------------------------------------------------------------------
# [추가적으로 Autoscaling, Volumes, Toleration, Affinity 설정 등 필요 시]
autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
volumes: []
volumeMounts: []
nodeSelector: {}
tolerations: []
affinity: {}
# ------------------------------------------------------------------------------
# [컨테이너 실행 명령]
# 원본 Deployment에서 사용했던 command.
command:
  - &quot;/bin/sh&quot;
  - &quot;-c&quot;
  - &quot;java -Dspring.profiles.active=dev -jar app.jar&quot;
# ------------------------------------------------------------------------------
# [envFrom 등 환경변수 세팅 (ConfigMap 사용 시)]
envFrom:
  - configMapRef:
      name: mingyu-api-config-dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gitlab에서 Helm Repository 생성 및 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 생성한 spring_boot_helm 폴더를 관리할 git repository와 helm chart 전용 package repository를 생성해 줍니다. 생성 방법은 공식 문서에 자세히 나와있습니다. 저의 경우 helm cm-push를 활용하였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741244220896&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Helm charts in the package registry | GitLab Docs&quot; data-og-description=&quot;Helm charts in the package registry Tier: Free, Premium, UltimateOffering: GitLab.com, GitLab Self-Managed, GitLab DedicatedStatus: Beta The Helm chart registry for GitLab is under development and isn&amp;rsquo;t ready for production use due to limited functionali&quot; data-og-host=&quot;docs.gitlab.com&quot; data-og-source-url=&quot;https://docs.gitlab.com/user/packages/helm_repository/&quot; data-og-url=&quot;https://docs.gitlab.com/user/packages/helm_repository/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.gitlab.com/user/packages/helm_repository/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.gitlab.com/user/packages/helm_repository/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Helm charts in the package registry | GitLab Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Helm charts in the package registry Tier: Free, Premium, UltimateOffering: GitLab.com, GitLab Self-Managed, GitLab DedicatedStatus: Beta The Helm chart registry for GitLab is under development and isn&amp;rsquo;t ready for production use due to limited functionali&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.gitlab.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1741244442709&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm repo add --username HELM_TOKEN \
  --password {HELM_TOKEN_VALUES} \
  https://gitlab.mingyu.co.kr/api/v4/project/{helm-rpository-project-id}/packages/helm/stable
  
helm cm-push spring_boot_helm spring_boot_helm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업로드한 helm charts를 다운로드 받고 싶을 때는 아래 주소를 활용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741244495452&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;https://gitlab.mingyu.co.kr/api/v4/projects/{helm-repository-project-id}/packages/helm/stable&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 복잡하고 어려운 것 처럼 느껴지지만, 이것이 왜 필요하고 왜 사용되었는지 생각해 가면서 하나하나 차근차근 설계하고 설치하고 설정하다 보면 재미를 느끼게 되고 차츰 본인의 지식이 될 것입니다. 저 스스로에게 하는 말입니다 :)&lt;/p&gt;</description>
      <category>Infrastructure/CICD</category>
      <category>argocd 사용법</category>
      <category>gitlab argocd</category>
      <category>gitlab argocd gitops</category>
      <category>helm charts create</category>
      <category>helm create</category>
      <category>helm 차트 생성</category>
      <category>kubernetes argocd</category>
      <category>ncp k8s argocd</category>
      <category>nks argocd</category>
      <category>쿠버네티스 cicd</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/343</guid>
      <comments>https://min-nine.tistory.com/entry/Cloud-Kubernetes-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-GitLab-ArgoCD%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-GitOps%EB%B0%A9%EC%8B%9D%EC%9D%98-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1-CICD-%EC%84%A4%EA%B3%84-%EB%B0%8F-Helm-%EC%83%9D%EC%84%B1#entry343comment</comments>
      <pubDate>Fri, 7 Mar 2025 08:42:33 +0900</pubDate>
    </item>
    <item>
      <title>알기 쉽게 정의한 DevOps와 GitOps</title>
      <link>https://min-nine.tistory.com/entry/%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EC%9D%98%ED%95%9C-DevOps%EC%99%80-GitOps</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 개발과 운영이 점점 더 빠르게 변화하는 환경에서, 개발팀과 운영팀이 협업하여 안정적이고 신속하게 서비스를 제공할 수 있도록 돕는 방법론들이 주목받고 있습니다. 이 포스팅에서는 DevOps와 GitOps에 대해 쉽게 풀어 설명하고, 각각의 핵심 개념과 장점을 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YHFNw/btsMBA9gmBH/jKQBHjUfQwkaYaKJLDbxo0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YHFNw/btsMBA9gmBH/jKQBHjUfQwkaYaKJLDbxo0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YHFNw/btsMBA9gmBH/jKQBHjUfQwkaYaKJLDbxo0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYHFNw%2FbtsMBA9gmBH%2FjKQBHjUfQwkaYaKJLDbxo0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;700&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알기 쉽게 정의한 DevOps&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BE5tx/btsMC8Dq9cw/LFYs9wGQegbIjHDdf4Ejy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BE5tx/btsMC8Dq9cw/LFYs9wGQegbIjHDdf4Ejy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BE5tx/btsMC8Dq9cw/LFYs9wGQegbIjHDdf4Ejy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBE5tx%2FbtsMC8Dq9cw%2FLFYs9wGQegbIjHDdf4Ejy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;701&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. DevOps란 무엇인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DevOps는 Development(개발), Operations(운영)의 합성어로 두 팀 간의 장벽을 허물고 협업을 촉진하여 소프트웨어를 보다 빠르고 안정적으로 제공하는 문화이자 방법론입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;672&quot; data-start=&quot;439&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;498&quot; data-start=&quot;439&quot;&gt;&lt;b&gt;협업과 소통:&lt;/b&gt; 개발자와 운영자가 같은 목표를 공유하며, 서로의 작업 방식을 이해하고 조율합니다.&lt;/li&gt;
&lt;li data-end=&quot;608&quot; data-start=&quot;499&quot;&gt;&lt;b&gt;자동화와 지속적 개선:&lt;/b&gt; CI/CD(지속적 통합 및 지속적 배포) 파이프라인, 인프라 자동화, 모니터링 등을 통해 반복 작업을 자동화하고, 문제 발생 시 빠르게 대응할 수 있도록 합니다.&lt;/li&gt;
&lt;li data-end=&quot;672&quot; data-start=&quot;609&quot;&gt;&lt;b&gt;빠른 피드백:&lt;/b&gt; 릴리즈 후 모니터링 및 로그 분석을 통해 즉각적인 피드백을 받아 개선 사항을 반영합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. DevOps의 주요 구성 요소&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;700&quot;&gt;&lt;b&gt;CI/CD 파이프라인:&lt;/b&gt; 소스 코드 변경이 있을 때마다 자동으로 빌드, 테스트, 배포를 진행하여 변경 사항을 신속하게 프로덕션에 반영합니다.&lt;/li&gt;
&lt;li data-end=&quot;873&quot; data-start=&quot;784&quot;&gt;&lt;b&gt;인프라 자동화:&lt;/b&gt; Terraform, Ansible, Puppet 등과 같은 도구를 사용해 인프라를 코드로 관리하고, 신속하게 환경을 프로비저닝합니다.&lt;/li&gt;
&lt;li data-end=&quot;964&quot; data-start=&quot;874&quot;&gt;&lt;b&gt;모니터링 및 로깅:&lt;/b&gt; Prometheus, Grafana, ELK 스택 등을 통해 시스템의 상태를 실시간으로 감시하고, 문제 발생 시 경고를 발송합니다.&lt;/li&gt;
&lt;li data-end=&quot;1036&quot; data-start=&quot;965&quot;&gt;&lt;b&gt;협업 도구:&lt;/b&gt; Jira, Slack, Git 등 다양한 협업 도구를 활용해 개발과 운영 간의 소통을 원활하게 만듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.DevOps의 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1277&quot; data-start=&quot;1058&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1107&quot; data-start=&quot;1058&quot;&gt;&lt;b&gt;신속한 배포:&lt;/b&gt; 자동화된 파이프라인 덕분에 코드 변경 사항이 빠르게 배포됩니다.&lt;/li&gt;
&lt;li data-end=&quot;1165&quot; data-start=&quot;1108&quot;&gt;&lt;b&gt;높은 안정성:&lt;/b&gt; 모니터링과 자동 롤백 기능을 통해 장애 발생 시 신속하게 대응할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;1226&quot; data-start=&quot;1166&quot;&gt;&lt;b&gt;효율적 리소스 관리:&lt;/b&gt; 인프라 자동화로 운영 비용을 절감하고, 리소스 사용을 최적화할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;1277&quot; data-start=&quot;1227&quot;&gt;&lt;b&gt;지속적인 개선:&lt;/b&gt; 빠른 피드백 루프를 통해 문제를 지속적으로 개선할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알기 쉽게 정의한 GitOps&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l7zhX/btsMDu0Ba2l/DvdzUnUn7IvjSQ1RHONVbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l7zhX/btsMDu0Ba2l/DvdzUnUn7IvjSQ1RHONVbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l7zhX/btsMDu0Ba2l/DvdzUnUn7IvjSQ1RHONVbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl7zhX%2FbtsMDu0Ba2l%2FDvdzUnUn7IvjSQ1RHONVbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1057&quot; height=&quot;521&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. GitOps란 무엇인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps는 &lt;b&gt;Git&lt;/b&gt;을 단일 소스의 진실(Source of Truth)로 삼아 인프라와 애플리케이션 배포를 자동화하는 접근 방식입니다. 기본 아이디어는 모든 시스템 상태(애플리케이션 코드, 인프라 설정 등)를 Git 저장소에 기록하고, 이 저장소의 변경 사항을 기반으로 실제 환경에 자동으로 반영하는 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1758&quot; data-start=&quot;1510&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1595&quot; data-start=&quot;1510&quot;&gt;&lt;b&gt;Git을 소스 오브 레코드(Source of Record)로 활용:&lt;/b&gt; 모든 인프라 설정, 애플리케이션 배포 설정 등을 Git에 버전 관리합니다.&lt;/li&gt;
&lt;li data-end=&quot;1684&quot; data-start=&quot;1596&quot;&gt;&lt;b&gt;자동화된 배포:&lt;/b&gt; Git 저장소의 변경 사항이 감지되면, CI/CD 파이프라인 또는 전용 에이전트가 이를 실제 클러스터나 서버에 자동으로 적용합니다.&lt;/li&gt;
&lt;li data-end=&quot;1758&quot; data-start=&quot;1685&quot;&gt;&lt;b&gt;관찰 가능성 및 감사:&lt;/b&gt; Git에 모든 변경 사항이 기록되므로 언제, 누가, 어떤 변경을 했는지 쉽게 추적할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. GitOps와 DevOps의 관계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps는 DevOps의 확장으로 볼 수 있습니다. DevOps가 개발과 운영 간의 협업과 자동화를 촉진한다면, GitOps는 그 자동화의 범위를 &lt;b&gt;Git 기반의 선언적 인프라 관리&lt;/b&gt;로 확장합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2069&quot; data-start=&quot;1904&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1969&quot; data-start=&quot;1904&quot;&gt;&lt;b&gt;DevOps의 자동화 원칙:&lt;/b&gt; CI/CD, 인프라 자동화, 모니터링 등 기존 DevOps 도구를 활용합니다.&lt;/li&gt;
&lt;li data-end=&quot;2069&quot; data-start=&quot;1970&quot;&gt;&lt;b&gt;GitOps의 확장:&lt;/b&gt; Git을 통해 인프라와 애플리케이션 상태를 선언적으로 관리하여, 변경 사항이 발생할 때마다 자동으로 반영되고, 이력 관리 및 롤백이 용이해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. GitOps의 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2150&quot; data-start=&quot;2091&quot;&gt;&lt;b&gt;일관성과 재현성:&lt;/b&gt; 모든 설정이 코드로 관리되기 때문에, 동일한 상태를 쉽게 재현할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2210&quot; data-start=&quot;2151&quot;&gt;&lt;b&gt;감사와 추적:&lt;/b&gt; Git의 버전 관리 기능 덕분에, 변경 이력 및 원인을 쉽게 파악할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2282&quot; data-start=&quot;2211&quot;&gt;&lt;b&gt;자동화된 배포 및 롤백:&lt;/b&gt; 변경 사항이 자동으로 배포되고, 문제가 발생하면 Git 히스토리로 쉽게 롤백할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2360&quot; data-start=&quot;2283&quot;&gt;&lt;b&gt;협업 강화:&lt;/b&gt; 개발자와 운영자가 동일한 Git 저장소를 통해 협업하며, 변경 사항에 대한 리뷰와 승인 절차를 진행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DevOps는 개발과 운영 팀이 협력하여 소프트웨어 배포의 속도와 안정성을 높이는 문화이자 방법론이며, GitOps는 이러한 DevOps 원칙을 한층 더 발전시켜 Git을 단일 소스로 활용해 인프라 및 애플리케이션의 배포를 자동화하고 감사 가능한 상태로 관리하는 접근 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2708&quot; data-start=&quot;2532&quot; data-ke-size=&quot;size16&quot;&gt;두 방법론 모두 자동화와 협업, 그리고 지속적인 개선을 핵심으로 하고 있으며, 실제 운영 환경에서는 DevOps와 GitOps를 적절히 혼합하여 사용하는 경우가 많습니다. 이러한 접근 방식은 코드 변경부터 프로덕션 배포까지의 프로세스를 명확하게 관리하고, 시스템의 안정성과 신뢰성을 향상시키는 데 큰 도움을 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/Git</category>
      <category>devops gitops</category>
      <category>devops gitops 차이</category>
      <category>devops 개념</category>
      <category>devops란</category>
      <category>GitOps</category>
      <category>gitops devops</category>
      <category>gitops 개념</category>
      <category>gitops란</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/342</guid>
      <comments>https://min-nine.tistory.com/entry/%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EC%9D%98%ED%95%9C-DevOps%EC%99%80-GitOps#entry342comment</comments>
      <pubDate>Thu, 6 Mar 2025 13:53:43 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 8. NKS에 GitLab, GitLab-SSH, GitLab-Runner 설치하기</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-8-NKS%EC%97%90-GitLab-GitLab-SSH-GitLab-Runner-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스코드 저장소 호스팅의 대표주자는 GitLab, GitHub가 있으며 기본 기능은 source의 버전 관리를 해준다는 것으로 같지만 차이가 확실합니다. 깃헙과 깃랩의 가장 큰 차이점은 데브옵스에 있는데, 깃헙은 사용자가 직접 CI/CD 도구 혹은 서드파티 툴들을 선택하여 연동하는 방향으로 CI/CD를 구축하지만, GitLab은 자체적인 CI/CD 워크플로우가 내장되어 있다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 깃랩을 사용하기로 결정하였고, kubernetes에 gitlab을 helm으로 설치하는 과정을 간단하게 포스팅하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[이전글]&lt;/p&gt;
&lt;figure id=&quot;og_1741913813144&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NCP] NKS로 개발환경 구성하기 Chapter 7. gitops와 argocd 도입을 위한 argocd 설치하기&quot; data-og-description=&quot;서론지금까지는 Naver Cloud Platform(이하 NCP)에서 제공해 주는 Source Series(Commit, Build,Deploy, PipeLine)을 사용하여 CI/CD를 진행했다면, 쿠버네티스의 장점을 고루 살린 GitOps와 ArgoCd를 사용하여 CI/CD를 마&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ssszR/hyYqbaX5AK/GjWWRI2zzT7SxcIDz81q3K/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/B1kGg/hyYqOUlpk0/cUt43yOwJKNYTlWXBkPds1/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/lYTAg/hyYqVTtjzk/7d1Jy6c4ihfG3DA69U8xd1/img.png?width=1920&amp;amp;height=990&amp;amp;face=0_0_1920_990&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ssszR/hyYqbaX5AK/GjWWRI2zzT7SxcIDz81q3K/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/B1kGg/hyYqOUlpk0/cUt43yOwJKNYTlWXBkPds1/img.jpg?width=474&amp;amp;height=474&amp;amp;face=0_0_474_474,https://scrap.kakaocdn.net/dn/lYTAg/hyYqVTtjzk/7d1Jy6c4ihfG3DA69U8xd1/img.png?width=1920&amp;amp;height=990&amp;amp;face=0_0_1920_990');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[NCP] NKS로 개발환경 구성하기 Chapter 7. gitops와 argocd 도입을 위한 argocd 설치하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론지금까지는 Naver Cloud Platform(이하 NCP)에서 제공해 주는 Source Series(Commit, Build,Deploy, PipeLine)을 사용하여 CI/CD를 진행했다면, 쿠버네티스의 장점을 고루 살린 GitOps와 ArgoCd를 사용하여 CI/CD를 마&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gitlab.io에서는 gitlab을 효율적으로 k8s에 설치할 수 있도록 공식 helm charts를 지원합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1741062306708&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;GitLab Helm Charts&quot; data-og-description=&quot;GitLab Helm Charts This repository collects GitLab&amp;rsquo;s official Helm charts from their individual repos and automatically publish them to our Helm repo, located at charts.gitlab.io. Helm is a package manager for Kubernetes, making it easier to deploy, upgr&quot; data-og-host=&quot;charts.gitlab.io&quot; data-og-source-url=&quot;https://charts.gitlab.io/&quot; data-og-url=&quot;https://charts.gitlab.io/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://charts.gitlab.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://charts.gitlab.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitLab Helm Charts&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitLab Helm Charts This repository collects GitLab&amp;rsquo;s official Helm charts from their individual repos and automatically publish them to our Helm repo, located at charts.gitlab.io. Helm is a package manager for Kubernetes, making it easier to deploy, upgr&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;charts.gitlab.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. GitLab Helm Repository&amp;nbsp; 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1741061478468&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm repo add gitlab https://charts.gitlab.io/
helm repo update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. helm temlate을 이용한 rendered.yaml 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm 명령어중 template 키워드를 사용하면 helm install시 설치되는 모든 항목을 yaml 파일로 만들어서 볼 수 있습니다. 기본 values.yaml 파일을 가지고 helm 설치를 할 경우 어떤 내용들이 default로 생성되는지 custom_values.yaml 로 만들어 확인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741062583994&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm template gitlab gitlab/gitalb --values values.yaml -n gitlab &amp;gt; ./custom_values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. custom_values 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 gitlab , shell, runner만 커스텀하였습니다. NKS의 대부분의 K8S Objecte들은 AWS의 오브젝트를 그대로 지원합니다. 때문에 alb나 s3 같은 aws 서비스들의 공식 문서를 참고하면 쉽게 설정이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741061637043&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# AWS Load Balancer Controller using ALB Ingress
# minimal settings needed to run gitlab on ALB
# Note that when using an ALB ingress controller we need to use a separate NLB for gitlab-shell (ssh) connections.

# Disable nginx-ingress
nginx-ingress:
  enabled: false
# Common settings for AWS Load Balancer Controller
global:
  externalUrl: &quot;https://gitlab.mingyu.co.kr/&quot;
  hosts:
    domain: mingyu.co.kr
    # we need a different dns endpoint for webservice and ssh
    ssh: gitlab-sh.mingyu.co.kr
  ingress:    
    # Common annotations used by kas, registry, and webservice
    annotations:
      alb.ingress.kubernetes.io/backend-protocol: HTTP
      alb.ingress.kubernetes.io/ssl-certificate-no: &quot;${YOUR CN}&quot;
      alb.ingress.kubernetes.io/ssl-redirect: &quot;443&quot;
      alb.ingress.kubernetes.io/group.name: gitlab
      alb.ingress.kubernetes.io/listen-ports: '[{&quot;HTTP&quot;: 80, &quot;TargetPort&quot;: 8080},{&quot;HTTPS&quot;:443}]'
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      nginx.ingress.kubernetes.io/connection-proxy-header: &quot;keep-alive&quot;
    class: none
    configureCertmanager: false
    enabled: true
    path: /*
    pathType: Prefix
    backend:
      service:
        name: gitlab-webservice-default
        port:
          number: 8080
    tls:
      enabled: false
    spec:
      ingressClassName: alb
    storageClass: nks-block-storage
gitlab:
  kas:
    enabled: true
    ingress:
      # Specific annotations needed for kas service to support websockets
      annotations:
        alb.ingress.kubernetes.io/healthcheck-path: /liveness
        alb.ingress.kubernetes.io/healthcheck-port: &quot;8151&quot;
        alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
        alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=4000,routing.http2.enabled=false
        alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=86400
        alb.ingress.kubernetes.io/target-type: ip
        kubernetes.io/tls-acme: &quot;true&quot;
        nginx.ingress.kubernetes.io/connection-proxy-header: &quot;keep-alive&quot;
        nginx.ingress.kubernetes.io/x-forwarded-prefix: &quot;/path&quot;
    # k8s services exposed via an ingress rule to an ELB need to be of type NodePort
    service:
      type: NodePort
  webservice:
    enabled: true
    service:
      type: NodePort
  # gitlab-shell (ssh) needs an NLB
  gitlab-shell:
    enabled: true
    service:
      annotations:
        external-dns.alpha.kubernetes.io/hostname: &quot;gitlab-sh.mingyu.co.kr&quot;
        service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: &quot;ip&quot;
        service.beta.kubernetes.io/aws-load-balancer-scheme: &quot;internet-facing&quot;
        service.beta.kubernetes.io/aws-load-balancer-type: &quot;external&quot;
      type: LoadBalancer
  gitaly:
    persistence:
      size: 200Gi
  time_zone: Asia/Seoul

## Installation &amp;amp; configuration of gitlab/gitlab-runner
## See requirements.yaml for current version
gitlab-runner:
  install: true
  rbac:
    create: true
  gitlabUrl: &quot;http://gitlab-webservice-default.gitlab.svc.cluster.local:8080/&quot;
  securityContext:
      privileged: true
      allowPrivilegeEscalation: true
  runners:
    privileged: true
    locked: null
    secret: &quot;nonempty&quot;
    env:
      - name: CI_SERVER_URL
        value: &quot;http://gitlab-webservice-default.gitlab.svc.cluster.local:8080/&quot;
      - name: NCP_ACCESS_KEY_ID
        valueFrom:
          secretKeyRef:
            name: ncp-s3-secret
            key: accesskey
      - name: NCP_SECRET_ACCESS_KEY
        valueFrom:
          secretKeyRef:
            name: ncp-s3-secret
            key: secretkey
      - name: DOCKER_TLS_CERTDIR
        value: &quot;&quot;
      - name: DOCKER_HOST
        value: &quot;unix:///var/run/docker.sock&quot;
      
    config: |
      [[runners]]
        url = &quot;http://gitlab-webservice-default.gitlab.svc.cluster.local:8080/&quot;
        [runners.kubernetes]
          image = &quot;ubuntu:22.04&quot;
          privileged = true
          [runners.kubernetes.volumes]
            [[runners.kubernetes.volumes.host_path]]
              name = &quot;docker-socket&quot;
              mount_path = &quot;/var/run/docker.sock&quot;
              host_path = &quot;/var/run/docker.sock&quot;
        [runners.cache]
          Type = &quot;s3&quot;
          Path = &quot;gitlab-runner&quot;
          Shared = true
          [runners.cache.s3]
            ServerAddress = &quot;kr.object.ncloudstorage.com&quot;
            BucketName = &quot;runner-cache&quot;
            BucketLocation = &quot;kr-standard&quot;
            Insecure = false
            AccessKey = &quot;${ACCESS_KEY_ID}&quot;
            SecretKey = &quot;${SECRET_ACCESS_KEY}&quot;
  podAnnotations:
    gitlab.com/prometheus_scrape: &quot;true&quot;
    gitlab.com/prometheus_port: 9252
  podSecurityContext:
    seccompProfile:
      type: &quot;RuntimeDefault&quot;
  volumes:
    - name: docker-socket
      hostPath:
        path: /var/run/docker.sock
  volumeMounts:
    - name: docker-socket
      mountPath: /var/run/docker.sock
  
registry:
  enabled: true
  service:
    type: NodePort

traefik:
  install: false
  ports:
    gitlab-shell:
      expose: true
      port: 2222
      exposedPort: 22&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. helm install&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 작성한 values 파일을 기준으로 gitlab을 k8s에 설치합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741061929350&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm install gitlab gitlab/gitlab \
  -n gitlab \
  --values custom-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 깃랩 접속 및 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service 이름이 &quot;gitlab-webservice-default&quot; 인 NodePort type의 service를 포트포워딩 하거나 DNS Ingress LB 설정을 통하여 서비스에 접근합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1741062953423&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt port-forward svc/gitlab-webservice-default -n gitlab 8080:8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우, 해당 포트포워딩 후 localhost:8080으로 접속하면 gitlab의 css가 깨져서 보이는 현상이 발생하는데, DNS의 ALB 맵핑으로 접속하면 css가 안깨지고 정상적으로 실행됩니다. 아마 내부적으로 설정한 external Url을 endpoint 삼아 통신하는 무엇인가 있는 것 같은데, 좀 더 디테일한 분석이 필요할 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZBXoM/btsMz0sQjyJ/KW1n4kctMbUu8ROofsbmDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZBXoM/btsMz0sQjyJ/KW1n4kctMbUu8ROofsbmDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZBXoM/btsMz0sQjyJ/KW1n4kctMbUu8ROofsbmDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZBXoM%2FbtsMz0sQjyJ%2FKW1n4kctMbUu8ROofsbmDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1914&quot; height=&quot;869&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;869&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gitlab을 k8s에 설치하면서 css가 깨져서 나오는 현상때문에 시간을 허비하며 삽질을 하였지만, 아직까지 이유를 찾지 못하였습니다. 혹시 이유를 아시는 분이 계신다면 댓글로 알려주시길 바랍니다.&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>gitalb helm</category>
      <category>gitlab helm intsall</category>
      <category>gitlab runner helm</category>
      <category>helm gitlab</category>
      <category>k8s gitlab helm install</category>
      <category>k8s gitlab install</category>
      <category>k8s helm gitlab</category>
      <category>nks gitlab helm install</category>
      <category>nks gitlab install</category>
      <category>nks helm gitalb</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/341</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-8-NKS%EC%97%90-GitLab-GitLab-SSH-GitLab-Runner-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0#entry341comment</comments>
      <pubDate>Tue, 4 Mar 2025 19:50:16 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 7. gitops와 argocd 도입을 위한 argocd 설치하기</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 Naver Cloud Platform(이하 NCP)에서 제공해 주는 Source Series(Commit, Build,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deploy, PipeLine)을 사용하여 CI/CD를 진행했다면, 쿠버네티스의 장점을 고루 살린 GitOps와 ArgoCd를 사용하여 CI/CD를 마이그레이션 해보려고 합니다. 간단하게 GitOps가 무엇인지, ArgoCd가 무엇인지 설명한 후에 ArgoCd를 설치해 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[이전글]&lt;/p&gt;
&lt;figure id=&quot;og_1741913769738&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NCP] NKS로 개발환경 구성하기 Chapter 6. Loki, Promtail, Grafana를 활용한 로그 통합 모니터링 구축 - Witho&quot; data-og-description=&quot;서론이전에는 loki-stack을 활용한 loki-promtail-grafana 스택을 구축해 보았다면, 이제는 loki-stack 없이 구축해 보도록 하겠습니다. Loki-Stack을 활용하여 통합 모니터링을 구축해보고 싶으신 분들은 이전&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-6-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Without-Loki-Stack&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-6-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Without-Loki-Stack&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bcF2ad/hyYqWSgXfl/Ahe1xEecJBPRcqWv8m5AP1/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/kGrzG/hyYqbaX5kg/MbQ5X3FLfocPHVsY21Kydk/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bSwMlO/hyYrSu54ko/rGu6unwwmJ44YBgPEOiyhK/img.png?width=2980&amp;amp;height=1458&amp;amp;face=0_0_2980_1458&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-6-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Without-Loki-Stack&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-6-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Without-Loki-Stack&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bcF2ad/hyYqWSgXfl/Ahe1xEecJBPRcqWv8m5AP1/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/kGrzG/hyYqbaX5kg/MbQ5X3FLfocPHVsY21Kydk/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bSwMlO/hyYrSu54ko/rGu6unwwmJ44YBgPEOiyhK/img.png?width=2980&amp;amp;height=1458&amp;amp;face=0_0_2980_1458');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[NCP] NKS로 개발환경 구성하기 Chapter 6. Loki, Promtail, Grafana를 활용한 로그 통합 모니터링 구축 - Witho&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론이전에는 loki-stack을 활용한 loki-promtail-grafana 스택을 구축해 보았다면, 이제는 loki-stack 없이 구축해 보도록 하겠습니다. Loki-Stack을 활용하여 통합 모니터링을 구축해보고 싶으신 분들은 이전&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. GitOps, ArgoCd&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitOps는 Git을 활용하여 쿠버네티스 기반의 클라우드 네이티브 환경에서 소프트웨어 애플리케이션을 배포하고 인프라를 관리하는 방식을 말합니다. 즉, &lt;u&gt;&lt;b&gt;GitOps는 방법론&lt;/b&gt;&lt;/u&gt;으로서 애플리케이션 코드부터 시작하여 인프라, 네트워킹, CI/CD 파이프라인 등&lt;u&gt;&lt;b&gt; 전체적인 애플리케이션 생태계에 대한 구성들을 모두 git에 저장하여 관리하는 방식&lt;/b&gt;&lt;/u&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;ArgoCd는 GitOps를 구현하기 위한 도구 중&lt;/b&gt;&lt;/u&gt; 하나입니다. Kubernetes 애플리케이션의 자동 배포를 위한 오픈소스 도구로써 CI/CD 파이프라인 중 CD(지속적인 배포)를 담당하며&lt;u&gt;&lt;b&gt; Git 저장소에서 변경 사항을 감지하여 자동으로 Kubernetes 클러스터에 애플리케이션을 배포해 주는 software&lt;/b&gt;&lt;/u&gt;라고 생각하면 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. ArgoCd 설치하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어로 간단하게 원하는 네임스페이스에 설치할 수 있습니다. 저의 경우 argocd 라는 네임스페이스를 생성하여 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 명령어로 설치하기&lt;/h4&gt;
&lt;pre id=&quot;code_1739941613681&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt create ns argocd&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739941643908&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt apply -k https://github.com/argoproj/argo-cd/manifests/crds\?ref\=stable -n argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 명령어로 설치하게되면 히스토리 파악도 안 될뿐더러 어떻게 설치했는지, 어떻게 삭제하는지에 대한 의문도 생기기 마련이기에 저는 helm 패키지 매니저를 활용하여 설치하려 합니다. (사실 delete -k 명령어로 삭제하긴 쉽습니다)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2 helm 으로 설치하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm repo에 argocd를 추가해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739942078490&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;hm-mgt repo add argo https://argoproj.github.io/argo-helm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm 문법 검증 및 매니페스트 출력은 아래의 명령어로 진행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739942392771&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;hm-mgt -n argocd template argocd argo/argo-cd&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm install 명령어로 쉽게 argocd를 설치 및 삭제할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739942468388&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# install
hm-mgt -n argocd install argocd argo/argo-cd

# delete
hm-mgt -n argocd uninstall argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. 접속하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치할 때 초기 admin 명령어를 확인할 수 있게 아래와 같은 command를 입력하라는 문구가 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1735&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctxHNj/btsMoEWOfla/7u4ozZMtVtciHIis1XAKyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctxHNj/btsMoEWOfla/7u4ozZMtVtciHIis1XAKyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctxHNj/btsMoEWOfla/7u4ozZMtVtciHIis1XAKyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctxHNj%2FbtsMoEWOfla%2F7u4ozZMtVtciHIis1XAKyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1735&quot; height=&quot;525&quot; data-origin-width=&quot;1735&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1739942688541&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | base64 -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl port forwad 기능을 사용하여 로컬에서 접속할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739942925304&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt -n argocd port-forward svc/argocd-server 8080:443&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 local에서 접속하게 되면 아래와 같이 정상적으로 argocd 접속이 가능합니다. 위에서 찾은 admin password를 입력하고 들어가면 argocd에 접근이 가능해집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFbuag/btsMnBzYusE/F3xwjNidnQkcfsu2aGKjZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFbuag/btsMnBzYusE/F3xwjNidnQkcfsu2aGKjZK/img.png&quot; data-origin-width=&quot;1913&quot; data-origin-height=&quot;990&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.1777%; margin-right: 10px;&quot; data-widthpercent=&quot;47.73&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFbuag/btsMnBzYusE/F3xwjNidnQkcfsu2aGKjZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFbuag%2FbtsMnBzYusE%2FF3xwjNidnQkcfsu2aGKjZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1913&quot; height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC4SUn/btsMn6M0Jok/K6fgBI0ZGxBc6aK9ZKv4t0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC4SUn/btsMn6M0Jok/K6fgBI0ZGxBc6aK9ZKv4t0/img.png&quot; data-filename=&quot;스크린샷 2025-02-19 오후 2.30.23.png&quot; data-origin-height=&quot;906&quot; data-origin-width=&quot;1917&quot; data-is-animation=&quot;false&quot; style=&quot;width: 51.6595%;&quot; data-widthpercent=&quot;52.27&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC4SUn/btsMn6M0Jok/K6fgBI0ZGxBc6aK9ZKv4t0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC4SUn%2FbtsMn6M0Jok%2FK6fgBI0ZGxBc6aK9ZKv4t0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1917&quot; height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.&amp;nbsp; DNS와 Ingress를 활용하여 도메인으로 접속하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제까지고 local에서 포트포워드로 접속할 수 없는 노릇입니다. 때문에 DNS와 Ingress를 이용하여 도메인으로 argocd 툴에 접속할 수 있게 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1. alb ingress 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 alb ingress를 사용하여 로드밸런서까지 함께 생성해 줍니다. ingress 설명에 대한 부분은 이전 글을 참고해 주세요.&lt;/p&gt;
&lt;figure id=&quot;og_1739943342956&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kubernetes 도입] 개발환경 없는 회사에서 NKS로 개발환경 구성하기 Chapter 4. Service, Ingress, Ingress Contr&quot; data-og-description=&quot;서론이전 포스팅들을 통해 cluster 내부에 deployment로 생성된 pod들로 트래픽을 보내야 합니다. 우리는 파드로의 접근을 이전 포스팅에서 서비스라는 쿠버네티스 오브젝트를 사용하여 이미 완성시&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ckCpK1/hyYjt9r5uF/4sdjD22mgTa4mklYUmY4UK/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/zIJkB/hyYjo1mZxQ/r6MAO7iCRkD8Mm1adlKHW1/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/bxZOWr/hyYfPM4boM/mlpZwVU2dnzilI5oeAfKv0/img.png?width=1902&amp;amp;height=917&amp;amp;face=0_0_1902_917&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ckCpK1/hyYjt9r5uF/4sdjD22mgTa4mklYUmY4UK/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/zIJkB/hyYjo1mZxQ/r6MAO7iCRkD8Mm1adlKHW1/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/bxZOWr/hyYfPM4boM/mlpZwVU2dnzilI5oeAfKv0/img.png?width=1902&amp;amp;height=917&amp;amp;face=0_0_1902_917');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kubernetes 도입] 개발환경 없는 회사에서 NKS로 개발환경 구성하기 Chapter 4. Service, Ingress, Ingress Contr&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론이전 포스팅들을 통해 cluster 내부에 deployment로 생성된 pod들로 트래픽을 보내야 합니다. 우리는 파드로의 접근을 이전 포스팅에서 서비스라는 쿠버네티스 오브젝트를 사용하여 이미 완성시&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1739943237872&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-ingress
  namespace: argocd
  annotations:
    alb.ingress.kubernetes.io/listen-ports: '[{&quot;HTTP&quot;: 80}, {&quot;HTTPS&quot;: 443}]'
    alb.ingress.kubernetes.io/ssl-certificate-no: &quot;인증서번호&quot;
    alb.ingress.kubernetes.io/description: &quot;argocd-ingress controller&quot;
    alb.ingress.kubernetes.io/ssl-redirect: &quot;443&quot;
    alb.ingress.kubernetes.io/healthcheck-path: &quot;/&quot;
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: &quot;120&quot;
spec:
  ingressClassName: alb
  rules:
    - host: argocd.mingyu.co.kr
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ingress.yaml 파일을 생성하였으면, 아래 명령어를 통해 적용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739943492589&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt apply -f ingress.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2. service를 NodePort로 변경 및 insecure 설정, 기존 파드 삭제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;alb는 이전 포스팅에서도 말했지만 NodePort타입의 서비스만 지원합니다. 기본적으로 argocd-server는 ClusterIP타입이니 nodeport type으로 변경해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739946285356&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;kn-mgt patch svc argocd-server -n argocd -p '{&quot;spec&quot;: {&quot;type&quot;: &quot;NodePort&quot;}}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;alb에서 인증서를 추가할 예정이니, 아래 명령어를 통해 server.insecure: &quot;true&quot; 값으로 설정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739946607457&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt edit cm argocd-cmd-params-cm -n argocd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 값들을 변경해주었다면 기존 argocd-server가 동작하고있는 pod를 제거하여 재성생해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739946805654&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt get po -n argocd
kn-mgt delete -n argocd po argocd-server-{pod-hash}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-3. DNS 레코드 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 ncp의 global DNS를 사용합니다.&amp;nbsp; 원하는 도메인을 클릭하여 레코드 추가를 선택한 후, 원하는 호스트명을 기입합니다. 그리고 Alias 기능을 LB VPC로 사용하여 Ingress로 생성된 로드밸런서를 선택해 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1911&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2p8de/btsMpl3ppT2/rlfsjPQMImm3W5WbEO0Iw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2p8de/btsMpl3ppT2/rlfsjPQMImm3W5WbEO0Iw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2p8de/btsMpl3ppT2/rlfsjPQMImm3W5WbEO0Iw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2p8de%2FbtsMpl3ppT2%2FrlfsjPQMImm3W5WbEO0Iw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1911&quot; height=&quot;903&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1911&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;번외 (같은 도메인을 사용할 경우 ingress 하나로 다른 네임스페이스의 서비스 컨트롤하기)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;번외 1. External Name을 사용한 Ingress 통합하기 ( nginx Ingress의 경우)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이전에 mgt-cluster의 monitoring에 alb ingress를 생성한 적이 있습니다. 로드밸런서를 생성하는 만큼 비용이 발생하게되고, 비용을 최소화하기 위해 기존에 생성한 ingress 로드밸런서에 argocd 트래픽을 넘기려합니다. 3-3 방법은 nginx 기반의 ingress에서 사용 가능한 1개의 인그레스로 여러 네임스페이스의 서비스를 적용시키는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 namespace에서 ingress를 공유하고자 할 때 service의 ExternalName Type을 활용할 수 있습니다. 우선 아래에 있는 argocd-external-service.yaml 을 사용하여 monitoring namespace에&amp;nbsp; service를 생성해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739944594745&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#argocd-external-service.yaml

apiVersion: v1
kind: Service
metadata:
  annotations: {}
  name: argocd-external-service
  namespace: monitoring
spec:
  type: ExternalName
  externalName: argocd-server.argocd.svc.cluster.local&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739944630972&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kn-mgt apply -f mgt-cluster/monitoring/argocd-external-service.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 생성하였던 monitoring-ingress.yaml에 아래와 같이 host를 추가합니다. 이때, servie.name은 external service의 이름으로 지정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739944676072&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    - host: argocd.mingyu.co.kr
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-external-service
                port:
                  number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;번외 2. alb group annotations를 활용한 Ingress 통합하기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 alb ingress를 사용합니다. alb에서 하나의 인그레스에서 다중화된 네임스페이스의 서비스를 지원하는 기능이 있을거라 믿고 찾아봤는데 정말 있었습니다. 우선 monitoring-ingress.yaml 파일에 아래의 내용을 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739945315359&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;metadata:
  annotations:
    abl.ingress.kubernetes.io/group.name: &quot;mgt-ingress&quot;
    alb.ingress.kubernetes.io/group.order: &quot;1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 argocd-ingress.yaml 파일에도 위 내용을 추가하여 배포해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 위에서 이야기한 것과 같이 도메인이 같고 path로 설정을 나누게 될 경우에 해당 내용을 참고하시면 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 argocd.mingyu.co.kr 같은 설정한 도메인으로 접속하면 정상적으로 argocd에 접속되는것을 확인할 수 있습니다. 다음 포스팅에서는 기존 ncp의 source series를 활용해 구성된 CI/CD pipeline을 gitops &amp;amp; argocd로 전환하는 내용으로 찾아뵙겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2025-02-19 오후 3.35.13.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9QbZE/btsMoi0TAdI/9rkskq5stG1IHRRkeHOUtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9QbZE/btsMoi0TAdI/9rkskq5stG1IHRRkeHOUtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9QbZE/btsMoi0TAdI/9rkskq5stG1IHRRkeHOUtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9QbZE%2FbtsMoi0TAdI%2F9rkskq5stG1IHRRkeHOUtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;990&quot; data-filename=&quot;edited_스크린샷 2025-02-19 오후 3.35.13.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>argocd install nks</category>
      <category>argocd nks</category>
      <category>argocd 설치하기</category>
      <category>install argocd in nks</category>
      <category>ncp argocd 설치</category>
      <category>nks argo install</category>
      <category>nks argocd</category>
      <category>nks argocd 설치</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/340</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-7-gitops%EC%99%80-argocd-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-argocd-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0#entry340comment</comments>
      <pubDate>Thu, 20 Feb 2025 09:01:07 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 6. Loki, Promtail, Grafana를 활용한 로그 통합 모니터링 구축 - Without Loki-Stack</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-6-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Without-Loki-Stack</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 loki-stack을 활용한 loki-promtail-grafana 스택을 구축해 보았다면, 이제는 loki-stack 없이 구축해 보도록 하겠습니다. Loki-Stack을 활용하여 통합 모니터링을 구축해보고 싶으신 분들은 이전 포스팅을 참고해 주시기 바랍니다,.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[이전글]&lt;/p&gt;
&lt;figure id=&quot;og_1741913718111&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NCP] NKS로 개발환경 구성하기 Chapter 5. Loki, Promtail, Grafana를 활용한 로그 통합 모니터링 구축 - Feat &quot; data-og-description=&quot;서론loki-stack은 grafana labs에서 좀 더 쉽게 loki-promtail-grafana 스택을 구축할 수 있도록 개발한 하나의 스택입니다. 때문에 loki-stack을 활용한 통합 모니터링 구축은 정말 쉽습니다. 다만 하나의 클러&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-5-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Feat-Loki-Stack&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-5-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Feat-Loki-Stack&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/8pboK/hyYr2YM0t8/nz0rlQ0grSftco958X6RMk/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/lAkH4/hyYqcuaUlY/2ewCqUqKDyHIEyjAcee3QK/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/FTRqO/hyYp9jTPZa/CKIKLW2rhPee5aMqSILm50/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-5-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Feat-Loki-Stack&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-5-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Feat-Loki-Stack&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/8pboK/hyYr2YM0t8/nz0rlQ0grSftco958X6RMk/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/lAkH4/hyYqcuaUlY/2ewCqUqKDyHIEyjAcee3QK/img.gif?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/FTRqO/hyYp9jTPZa/CKIKLW2rhPee5aMqSILm50/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[NCP] NKS로 개발환경 구성하기 Chapter 5. Loki, Promtail, Grafana를 활용한 로그 통합 모니터링 구축 - Feat&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론loki-stack은 grafana labs에서 좀 더 쉽게 loki-promtail-grafana 스택을 구축할 수 있도록 개발한 하나의 스택입니다. 때문에 loki-stack을 활용한 통합 모니터링 구축은 정말 쉽습니다. 다만 하나의 클러&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Helm chart를 이용해서 손쉽게 설치할 수 있습니다. helm이란 쿠버네티스 패키지 매니저로 원하는 패키지를 쿠버네티스에 쉽게 설치할 수 있도록 도와주는 역할을 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.&amp;nbsp; Helm 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1739927205099&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Mac OS
brew install helm

# Linux OS (Script)
$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
$ chmod 700 get_helm.sh
$ ./get_helm.sh

# Linux OS (Debian/Ubuntu)
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg &amp;gt; /dev/null
sudo apt-get install apt-transport-https --yes
echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main&quot; | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

# Windows OS (Chocolatey)
choco install kubernetes-helm

# Windows OS (Scoop)
scoop install helm

# Windows OS (Winget)
winget install Helm.Helm&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739927071647&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm repo add grafana https://grafana.github.io/helm-charts
helm repo update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Grafana 제공 helm-carts Pull&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Loki, Promtail 모두 GrafanaLab에서 개발 및 운영 유지보수를 진행하고 있기 때문에 grafana의 github에서 helm-charts를 가져올 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739927329570&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git clone https://github.com/grafana/helm-charts.git&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 &quot;helm-charts/charts/&quot; 디렉터리 내부에는 많은 소프트웨어 관련 디렉터리가 존재하는데, 우리는 그중 loki-distributed, grafana, promtail 3개의 소프트웨어를 사용하여 통합 모니터링을 구축할 것이기 때문에 해당 소프트웨어 이름의 디렉터리 내부에 있는 vaules.yaml 파일을 이용할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 설계하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 수집하여 저장소에 보내는 &lt;b&gt;&lt;u&gt;Promtail은&lt;/u&gt; 애플리케이션 동작하는 파드가 있는 모든 노드들에 설치&lt;/b&gt;가 되어야 합니다. promtail의 values.yaml 파일을 확인해 보면 daemonset.enabled: true로 설정되어 있는데 이는, 쿠버네티스의 DaemonSets 오브젝트를 통해 각 노드에 설치하게끔 하기 위함입니다. 그렇기 때문에 &lt;u&gt;&lt;b&gt;Promtail은 애플리케이션이 동작하는 dev-cluster에 설치&lt;/b&gt;&lt;/u&gt;하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 받아서 저장 및 보관하는&lt;u&gt;&lt;b&gt; Loki와&lt;/b&gt; &lt;/u&gt;모니터링을 제공할 &lt;b&gt;&lt;u&gt;Grafana는 어플리케이션 단위와는 별개로 구분되어야 하기 때문에 mgt-cluster에 설치&lt;/u&gt;&lt;/b&gt;하도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgdDM5/btsMmfdfDX9/bx3jBNX2ikSpWMH0sczZiK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgdDM5/btsMmfdfDX9/bx3jBNX2ikSpWMH0sczZiK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgdDM5/btsMmfdfDX9/bx3jBNX2ikSpWMH0sczZiK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bgdDM5/btsMmfdfDX9/bx3jBNX2ikSpWMH0sczZiK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;810&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Dev-Cluster에 Promtail 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 신경써야 할 부분은 promtail/values.yaml 파일에서 config.clients.url 부분입니다. 프롬테일은 해당 url에 적힌 곳으로&amp;nbsp; 로그들을 전송합니다. 때문에 저는 로키를 설치하고 해당 로키 service를 ingress alb로 띄운 후 dns와 맵핑 지었습니다. 추후에 각 클러스터끼리 통신을 가능케 하는 방법으로 수정할 수도 있지만, 지금 당장은 이렇게 진행하도록 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[promtail-values.yaml]&lt;/p&gt;
&lt;pre id=&quot;code_1739928146940&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
...

config:
  enabled: true
  clients:
    - url: http://mgt-loki.mingyu.co.kr/loki/api/v1/push
    
...
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, helm 명령어를 통해 수정한 values.yaml 파일을 참고하여 promtail을 설치할 수 있습니다. 저는 dev-tools라는 네임스페이스에 프롬테일을 배포하도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739928318716&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm install -n dev-tools promtail grafana/promtail --values helm-charts/charts/promtail/promtail-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드에 들어가면 아래와 같이 DaemonSets이 정상적으로 동작하면서 파드가 3/3으로 보이는 것을 확인할 수 있습니다. 저는 노드를 3개로 구성한 dev-cluster에 배포했기 때문에 각각의 노드에 파드가 하나씩 생성된 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2980&quot; data-origin-height=&quot;1458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFJqUJ/btsMnSgyAzF/3FYGTMQECKAsZ5sFRniOSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFJqUJ/btsMnSgyAzF/3FYGTMQECKAsZ5sFRniOSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFJqUJ/btsMnSgyAzF/3FYGTMQECKAsZ5sFRniOSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFJqUJ%2FbtsMnSgyAzF%2F3FYGTMQECKAsZ5sFRniOSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2980&quot; height=&quot;1458&quot; data-origin-width=&quot;2980&quot; data-origin-height=&quot;1458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Mgt-Cluster에 Liki 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Loki를 mgt 클러스터에 설치하도록 하겠습니다. values.yaml을 이용하여 helm을 설치할 때, 비어져있는 값은 알아서 default 값으로 대체해주기 때문에 꼭 필요한 사항만 명시해도 괜찮습니다. Loki를 설치하는 방법은 grafana 공식 문서에 나와있습니다. 모놀리틱, 마이크로서비스,스케일러블 3가지의 버전을 설치할 수 있는데 저의 경우는 추후 마이크로소프트로 전환하기 용이한 scalable Loki를 설치하였습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1739928653038&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Install the simple scalable Helm chart |  Grafana Loki documentation&quot; data-og-description=&quot;Install the simple scalable Helm chart This Helm Chart deploys Grafana Loki in simple scalable mode within a Kubernetes cluster. This chart configures Loki to run read, write, and backend targets in a scalable mode. Loki&amp;rsquo;s simple scalable deployment mode&quot; data-og-host=&quot;grafana.com&quot; data-og-source-url=&quot;https://grafana.com/docs/loki/latest/setup/install/helm/install-scalable/&quot; data-og-url=&quot;https://grafana.com/docs/loki/latest/setup/install/helm/install-scalable/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VyqTz/hyYjyv5pHh/merVpOVyeFxoP2qWaAKkqk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/D4cot/hyYfVT4NRt/OzKjYYUKaTm9U3xyXYkfZK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://grafana.com/docs/loki/latest/setup/install/helm/install-scalable/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://grafana.com/docs/loki/latest/setup/install/helm/install-scalable/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VyqTz/hyYjyv5pHh/merVpOVyeFxoP2qWaAKkqk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/D4cot/hyYfVT4NRt/OzKjYYUKaTm9U3xyXYkfZK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Install the simple scalable Helm chart | Grafana Loki documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Install the simple scalable Helm chart This Helm Chart deploys Grafana Loki in simple scalable mode within a Kubernetes cluster. This chart configures Loki to run read, write, and backend targets in a scalable mode. Loki&amp;rsquo;s simple scalable deployment mode&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;grafana.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1739928745106&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  loki:
    schemaConfig:
      configs:
        - from: &quot;2024-04-01&quot;
          store: tsdb
          object_store: s3
          schema: v13
          index:
            prefix: loki_index_
            period: 24h
    ingester:
      chunk_encoding: snappy
    querier:
      # Default is 4, if you have enough memory and CPU you can increase, reduce if OOMing
      max_concurrent: 4
    pattern_ingester:
      enabled: true
    limits_config:
      allow_structured_metadata: true
      volume_enabled: true

  deploymentMode: SimpleScalable

  backend:
    replicas: 2
  read:
    replicas: 2
  write:
    replicas: 3 # To ensure data durability with replication

  # Enable minio for storage
  minio:
    enabled: true

  gateway:
    service:
      type: LoadBalancer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 mgt-cluster의 monitoring 네임스페이스에 로키를 설치했는데, 계속 오류가 발생했습니다. 이유를 살펴보니 loki 설치시 default dnsService가 &quot;kube-dns&quot;로 되어있는데, NKS의 dnsSerivce는 &quot;coredns&quot;라는 명칭이기 때문이었습니다. 때문에 global dnsService값을 변경해줍니다. 또한, ipv6를 사용할 필요성이 없기 때문에 gateway의 nginxconfig도 변경해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739928903076&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;global:
  dnsService: &quot;coredns&quot;
  
gateway:
  nginxConfig:
    enableIPv6: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 helm 명령어를 통해 배포해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739929012082&quot; class=&quot;shell&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;helm install --values values.yaml loki grafana/loki -n monitoring&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H3INo/btsMmcnijam/0tnpqx1hbVxQZQK4msl8P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H3INo/btsMmcnijam/0tnpqx1hbVxQZQK4msl8P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H3INo/btsMmcnijam/0tnpqx1hbVxQZQK4msl8P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH3INo%2FbtsMmcnijam%2F0tnpqx1hbVxQZQK4msl8P1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1884&quot; height=&quot;934&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Mgt-Cluster에 Grafana 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합 모니터링을 관리하는 grafana를 mgt-cluster에 설치하도록 하겠습니다. values.yaml 파일의 datasources 밑에 loki관련 서비스를 맵핑지어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739930876295&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;datasources:
  datasources.yaml:                                              
    apiVersion: 1                                                
    datasources:                                                 
    - name: devCluster                                           
      type: loki                                                 
      url: http://loki-read.monitoring.svc.cluster.local:3100    
      access: proxy                                              
      isDefault: true                                            
      jsonData:                                                  
        httpHeaderName1: X-Scope-OrgID                           
      orgId: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm 명령어를 사용하여 grafana를 배포합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739930992607&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm install grafana grafana/grafana -n monitoring --values grafana-values.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 접속 비밀번호는 아래 명령어로 찾을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739931035697&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get secret --namespace monitoring grafana -o jsonpath=&quot;{.data.admin-password}&quot; | base64 --decode ; echo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 dns에 grafana 접속 관련 도메인을 생성 후 mgt ingress alb에 연결지어줬습니다. 해당 도메인으로의 접속은 곧 grafana service로 path설정 해두었기 때문에 grafana.mingyu.co.kr 형식의 도메인으로 그라파나에 접속 가능하게 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dns 설정 없이 그라파나에 접속하고싶으시면 kubectl 의 port forward를 사용하여 로컬에서 접속할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739931262062&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl port-forward --address=0.0.0.0 {grafana-pod-name} -n monitoring {grafana-port}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GntBo/btsMn8cqVeW/USZeTueQsCQfdQc4rFMjC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GntBo/btsMn8cqVeW/USZeTueQsCQfdQc4rFMjC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GntBo/btsMn8cqVeW/USZeTueQsCQfdQc4rFMjC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGntBo%2FbtsMn8cqVeW%2FUSZeTueQsCQfdQc4rFMjC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3022&quot; height=&quot;1434&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UXbqd/btsMnECVRXw/k0HDmGO4mY6rgHKpLOYqZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UXbqd/btsMnECVRXw/k0HDmGO4mY6rgHKpLOYqZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UXbqd/btsMnECVRXw/k0HDmGO4mY6rgHKpLOYqZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUXbqd%2FbtsMnECVRXw%2Fk0HDmGO4mY6rgHKpLOYqZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1371&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각각의 클러스터에 알맞는 소프트웨어를 설치하여 Loki-Promtail-Grafana 스택을 구성해봤습니다. 다만 로키에 저장되고있는 로그는 별도로 저장값을 설정하지 않았기 때문에 휘발성입니다. 로그를 s3나 objectstorage 등과 같은 저장소에 저장시키고 몇일까지 보관할지 설정하는 것들또한 모두 values.yaml에서 설정할 수 있습니다. 본인의 상황에 맞에 설정하여 통합로그를 관리하고 유용하게 사용해보도록 합시다!&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>k8s loki 구축</category>
      <category>k8s 통합 로그 모니터링 구축</category>
      <category>loki grafana</category>
      <category>loki promtail</category>
      <category>loki promtail grafana</category>
      <category>lpg스택</category>
      <category>nks loki promtail grafana</category>
      <category>without loki-stack</category>
      <category>로키스택없이 로키 구축</category>
      <category>통합 로그 모니터링</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/339</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-6-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Without-Loki-Stack#entry339comment</comments>
      <pubDate>Wed, 19 Feb 2025 12:45:22 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 5. Loki, Promtail, Grafana를 활용한 로그 통합 모니터링 구축 - Feat Loki-Stack</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-5-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Feat-Loki-Stack</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;loki-stack은&lt;/b&gt;&lt;/u&gt; grafana labs에서 좀 더 쉽게 loki-promtail-grafana 스택을 구축할 수 있도록 개발한 하나의 스택입니다. 때문에 loki-stack을 활용한 통합 모니터링 구축은 정말 쉽습니다. 다만 하나의 클러스터에 모든 자원이 설치되기 때문에, &lt;u&gt;&lt;b&gt;현업에서 사용하기에는 추천할 수 없는 방법이라고 생각&lt;/b&gt;&lt;/u&gt;하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;본 포스팅&lt;/b&gt;&lt;/u&gt;에서는 &lt;u&gt;&lt;b&gt;loki-stack을 활용해서 하나의 클러스터에 통합 모니터링을 구축&lt;/b&gt;&lt;/u&gt;하는 방법부터 시작하여, 두 개 이상의 클러스터에서 (mgt-cluster 및 dev-cluster) 알맞은 소프트웨어를 설치하여 그 상호작용을 통해 제가 Loki-promtail-grafana 스택을 구축한 방법을 포스팅해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dev-cluster 및 mgt-cluster가 궁금하신 분들은 이전 포스팅을 참고해 주시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[이전글]&lt;/p&gt;
&lt;figure id=&quot;og_1741913682048&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NCP] NKS로 개발환경 구성하기 Chapter 4. Service, Ingress, Ingress Controller를 이용한 DNS의 네트워킹 구성 w&quot; data-og-description=&quot;서론이전 포스팅들을 통해 cluster 내부에 deployment로 생성된 pod들로 트래픽을 보내야 합니다. 우리는 파드로의 접근을 이전 포스팅에서 서비스라는 쿠버네티스 오브젝트를 사용하여 이미 완성시&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btHN2T/hyYqMhWnCm/Ce67rBvHOcK7nBqX1DCXKK/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/2DnRL/hyYqUmHtb1/l9EJ7zzkD9UBfjR2KekSQ1/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/lQSAn/hyYr0s58o9/eIISYfNxtifVz6Wy2xLnrk/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btHN2T/hyYqMhWnCm/Ce67rBvHOcK7nBqX1DCXKK/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/2DnRL/hyYqUmHtb1/l9EJ7zzkD9UBfjR2KekSQ1/img.jpg?width=800&amp;amp;height=586&amp;amp;face=0_0_800_586,https://scrap.kakaocdn.net/dn/lQSAn/hyYr0s58o9/eIISYfNxtifVz6Wy2xLnrk/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=708_425_791_517');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[NCP] NKS로 개발환경 구성하기 Chapter 4. Service, Ingress, Ingress Controller를 이용한 DNS의 네트워킹 구성 w&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론이전 포스팅들을 통해 cluster 내부에 deployment로 생성된 pod들로 트래픽을 보내야 합니다. 우리는 파드로의 접근을 이전 포스팅에서 서비스라는 쿠버네티스 오브젝트를 사용하여 이미 완성시&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. ELK/EFK, Loki-Promtail-Grafana 스택의 장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 Kubernetes 클러스터의 로그 모니터링 및 분석 도구로 ELK/EFK 스택이 널리 사용되지만, 저는 &lt;b&gt;Loki-Promtail-Grafana&lt;/b&gt; 스택을 선택했습니다. 그 이유는 여러 측면에서 보다 &lt;b&gt;경량화&lt;/b&gt;되고 &lt;b&gt;운영이 용이&lt;/b&gt;하며, 특히 &lt;b&gt;Kubernetes 환경에 최적화&lt;/b&gt;되어 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-1. ELK/EFK 스택의 장점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;685&quot; data-start=&quot;358&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;512&quot; data-start=&quot;358&quot;&gt;&lt;b&gt;강력한 검색 및 분석 기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;512&quot; data-start=&quot;385&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;455&quot; data-start=&quot;385&quot;&gt;ElasticSearch를 기반으로 하여 복잡한 쿼리와 필터링, 그리고 대규모 로그 데이터에 대한 빠른 검색이 가능합니다.&lt;/li&gt;
&lt;li data-end=&quot;512&quot; data-start=&quot;459&quot;&gt;Kibana를 활용한 다양한 시각화 및 대시보드 구성으로 심도 있는 로그 분석이 용이합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;594&quot; data-start=&quot;514&quot;&gt;&lt;b&gt;확장성과 유연성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;594&quot; data-start=&quot;535&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;594&quot; data-start=&quot;535&quot;&gt;대량의 로그 데이터 처리에 적합하며, 확장성이 뛰어나 대규모 클러스터에서도 안정적인 운영이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;685&quot; data-start=&quot;596&quot;&gt;&lt;b&gt;다양한 플러그인과 생태계&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;685&quot; data-start=&quot;622&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;685&quot; data-start=&quot;622&quot;&gt;Elastic Stack은 다양한 플러그인과 오픈소스 도구와의 연동이 잘 되어 있어 커스터마이징이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-2. ELK/EFK 스택의 단점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1012&quot; data-start=&quot;695&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;816&quot; data-start=&quot;695&quot;&gt;&lt;b&gt;높은 리소스 소비&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;816&quot; data-start=&quot;717&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;816&quot; data-start=&quot;717&quot;&gt;ElasticSearch와 Kibana는 메모리와 CPU 등 리소스를 많이 사용합니다. 특히 Kubernetes와 같이 리소스 제약이 있는 환경에서는 부담이 될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;925&quot; data-start=&quot;818&quot;&gt;&lt;b&gt;운영 및 유지보수의 복잡성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;925&quot; data-start=&quot;845&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;925&quot; data-start=&quot;845&quot;&gt;클러스터 구성, 인덱스 관리, 샤딩 및 리플리카 설정 등 운영에 필요한 관리 포인트가 많아 초기 설정 및 유지보수에 많은 노력이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1012&quot; data-start=&quot;927&quot;&gt;&lt;b&gt;설정 및 최적화의 어려움&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1012&quot; data-start=&quot;953&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1012&quot; data-start=&quot;953&quot;&gt;효율적인 로그 저장 및 검색을 위해서는 복잡한 설정과 주기적인 최적화가 필요해 운영 부담이 늘어납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-3. Loki-Promtail-Grafana 스택의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1182&quot; data-start=&quot;1061&quot;&gt;&lt;b&gt;경량화된 로그 저장소&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1182&quot; data-start=&quot;1085&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1182&quot; data-start=&quot;1085&quot;&gt;Loki는 로그 메시지를 그대로 저장하면서 메타데이터(라벨)를 이용해 인덱싱 하기 때문에, ElasticSearch처럼 모든 로그를 인덱싱 하지 않아 리소스 소비가 적습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1334&quot; data-start=&quot;1184&quot;&gt;&lt;b&gt;Kubernetes 환경에 최적화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1334&quot; data-start=&quot;1215&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1292&quot; data-start=&quot;1215&quot;&gt;Promtail은 Kubernetes Pod 로그 수집에 특화되어 있으며, 자동으로 라벨을 부여하여 로그를 손쉽게 분류할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;1334&quot; data-start=&quot;1296&quot;&gt;Pod 기반의 동적 환경에서도 로그 수집이 원활하게 이루어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1459&quot; data-start=&quot;1336&quot;&gt;&lt;b&gt;쉬운 설치와 운영&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1459&quot; data-start=&quot;1358&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1408&quot; data-start=&quot;1358&quot;&gt;설정이 상대적으로 단순하며, 클러스터에 부담을 주지 않아 운영과 유지보수가 용이합니다.&lt;/li&gt;
&lt;li data-end=&quot;1459&quot; data-start=&quot;1412&quot;&gt;Grafana와의 네이티브 연동을 통해 시각화 대시보드 구성이 매우 직관적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1537&quot; data-start=&quot;1461&quot;&gt;&lt;b&gt;비용 효율성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1537&quot; data-start=&quot;1480&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1537&quot; data-start=&quot;1480&quot;&gt;불필요한 인덱싱 비용과 리소스 소비가 적어, 클러스터 리소스를 보다 효율적으로 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-4. Loki-Promtail-Grafana 스택의 단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1654&quot; data-start=&quot;1547&quot;&gt;&lt;b&gt;제한적인 검색 기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1654&quot; data-start=&quot;1570&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1654&quot; data-start=&quot;1570&quot;&gt;ElasticSearch 기반의 강력한 쿼리 기능에 비해, Loki는 메타데이터 기반 검색으로 일부 복잡한 로그 분석에는 한계가 있을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1743&quot; data-start=&quot;1656&quot;&gt;&lt;b&gt;고급 분석 기능 부재&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1743&quot; data-start=&quot;1680&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1743&quot; data-start=&quot;1680&quot;&gt;로그의 심층 분석이나 머신 러닝 기반의 이상 징후 탐지 등 고급 기능은 별도의 도구나 추가 개발이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 제가 Loki 스택을 선택한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1958&quot; data-start=&quot;1773&quot;&gt;&lt;b&gt;Kubernetes 네이티브 통합&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1958&quot; data-start=&quot;1804&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1915&quot; data-start=&quot;1804&quot;&gt;Kubernetes 환경에서는 Pod 로그가 빠르게 증발(에페멀)되기 때문에, 로그 수집 에이전트인 Promtail이 Pod 메타데이터를 그대로 활용하여 로그를 수집하는 방식이 매우 효율적입니다.&lt;/li&gt;
&lt;li data-end=&quot;1958&quot; data-start=&quot;1919&quot;&gt;클러스터 전체에 대한 로그 모니터링을 간편하게 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2117&quot; data-start=&quot;1960&quot;&gt;&lt;b&gt;리소스 최적화 및 운영 효율성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2117&quot; data-start=&quot;1989&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2065&quot; data-start=&quot;1989&quot;&gt;ElasticSearch를 운영하기 위한 추가적인 인프라 부담 없이, 로그 저장과 검색에 필요한 리소스 소비를 최소화할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2117&quot; data-start=&quot;2069&quot;&gt;운영 및 유지보수 측면에서 단순함은 실제 프로덕션 환경에서 큰 장점으로 작용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2279&quot; data-start=&quot;2119&quot;&gt;&lt;b&gt;Grafana와의 완벽한 통합&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2279&quot; data-start=&quot;2148&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2235&quot; data-start=&quot;2148&quot;&gt;NKS에서는 이미 Grafana를 기본 노드 모니터링 툴로 제공해주고 있었습니다. 때문에 동일한 대시보드 환경 내에서 로그와 메트릭을 통합해 볼 수 있어 운영 효율성을 높일 수 있다고 판단했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2378&quot; data-start=&quot;2281&quot;&gt;&lt;b&gt;비용 효율성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2378&quot; data-start=&quot;2300&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2378&quot; data-start=&quot;2300&quot;&gt;클라우드나 온프레미스 환경에서 리소스 비용이 중요한 경우, 불필요한 인덱싱 오버헤드를 줄인 Loki의 경량화된 특성이 매우 매력적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELK/EFK 스택은 강력한 검색과 분석 기능을 제공하지만, 리소스 사용량, 운영 복잡성 등에서 단점이 있습니다. 반면, &lt;b&gt;Loki-Promtail-Grafana&lt;/b&gt; 스택은 Kubernetes 환경에 최적화된 경량화된 로그 솔루션으로, 설치와 운영이 간편하고 비용 효율적입니다.&lt;br /&gt;저는 이러한 이유로 &lt;b&gt;Kubernetes 클러스터에 Loki 스택을 선택&lt;/b&gt;하게 되었으며, 이를 통해 보다 안정적이고 효율적인 통합 모니터링 시스템을 구축할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Loki, Promtail, Grafana 각각의 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로키, 프롬테일, 그라파나 각각 어떤 역할을 담당하는 친구들인지 먼저 간단하게 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬테일은 각 노드(서버)에서 쿠버네티스 파드에서 출력되는 로그들을 수집하여 로키라는 친구에게 보내주는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로키는 프롬테일로부터 받은 로그들을 관리 및 보관하는 역할을 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그라파나는 로키와 연동하여 관리 및 보관되고 있는 로그들을 UI적으로 친숙하게 보여주어 모니터링을 용이하게 해주는 친구들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Loki-stack을 활용한 단일 클러스터에 통합 모니터링 구축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 1개의 values.yaml 파일을 사용하여 loki-stack helm에 맞는 설정값들을 세팅할 수 있습니다. 저는 loki, promtail, grafana 만 사용하였지만 실제로는 prometheus, filebeat 등의 더 많은 오픈소스들을 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739843925315&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;loki:
  enabled: true
  isDefault: true
  url: http://{{(include &quot;loki.serviceName&quot; .)}}:{{ .Values.loki.service.port }}
  readinessProbe:
    httpGet:
      path: /ready
      port: http-metrics
    initialDelaySeconds: 45
  livenessProbe:
    httpGet:
      path: /ready
      port: http-metrics
    initialDelaySeconds: 45
  datasource:
    jsonData: &quot;{}&quot;
    uid: &quot;&quot;

promtail:
  enabled: true
  config:
    logLevel: info
    serverPort: 3101
    clients:
      - url: http://{{ .Release.Name }}:3100/loki/api/v1/push

grafana:
  enabled: true
  sidecar:
    datasources:
      label: &quot;&quot;
      labelValue: &quot;&quot;
      enabled: true
      maxLines: 1000
  image:
    tag: 10.3.3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 파일을 작성하였다면, helm 명령어를 통해 grafana/loki-stack을 사용하여 원하는 네임스페이스에 통합 모니터링 툴을 설치할 수 있게 됩니다. 저는 monitoring이라는 namespace에 loki-stack이라는 이름으로 위에서 만들어놓은 values를 사용해 grafka/loki-stack을 설치하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739844240655&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;helm install loki-stack grafana/loki-stack --values ./loki-stack-values.yaml -n monitoring&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm에 대해서는 추후 자세히 포스팅하도록 하겠습니다. 지금은 그냥 helm이라는 쿠버네티스 전용 오픈소스 패키지 매니저라고만 알고 계시면 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 통해서 내가 원하는 클러스터에 로키, 프롬테일, 그라파나 모두를 설치하고 각각 로그 수집, 보존 및 관리, 모니터링할 수 있게 연관 짓는 것까지 모든 것을 끝맞힐 수 있게 되었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Loki-stack을 추천할 수 없는 이유&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-1. 쿠버에서 로그를 관리하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스에서 각 파드가 실행 중인 Node에 Log가 저장됩니다. 저장 위치는 /var/log/pods/{namespace}_{pod_name}/{container-name}/{count-number}. log 형식으로 저장됩니다. 하지만 위 위치로 접속하면 각 파드에 따라 디렉터리별로 나뉘어 저 저장되어 있기 때문에 로그를 한눈에 보기가 어렵긴 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 이러한 로그들의 연결파일들을 모아놔 준 곳이 있는데 그곳이 /var/log/containers입니다. 이곳에 모든 pod에 대한 로그파일들이 {pod_name}_{namespace}_{container-name}. logs 형태로 존재하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-2. 프롬테일, 로키, 그라파나의 설치 노드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬테일은 k8s의 DaemonSets라는 오브젝트를 이용하여 설치됩니다. 때문에 각 노드별로 최소 1개는 무조건 설치가 되고, 해당 프롬테일이 위에서 이야기한 로그에 접근하여 로그들을 수집 및 로키로 전송할 수 있는 역할을 하게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 실행되는 파드가 존재하는 노드에서 로그를 수집하는 프롬테일이 설치되는 것은 어찌보면 당연합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 어플리케이션이 실행 중인 파드가 존재하는 노드에 로그를 저장 및 보관하는 로키와 모니터링툴을 지원하는 그라파나가 설치되는 건 저는 아니라고 생각하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹여 많은 트래픽이 몰리거나 런타임 장애들이 연달아 발생하면서 해당 노드가 죽게 된다면 모니터링 자체를 할 수 없어지기 때문입니다. 그래서 첫 글에서 생성한 mgt-cluster를 활용하기로 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 loki와 grafana는 mgt-cluster에 각각 설치하고, dev-cluster에 promtail을 설치하여 어떻게 로그를 mgt-cluster에 설치되어 있는 loki에게 보내고, 어떻게 grafana와 loki를 연동하는지, 그렇게 하여 전체적인 Loki-Promtail-Garafana 로그 통합 모니터링 스택을 구축하였는지 상세히 포스팅해 보도록 하겠습니다.&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>loki promtail garafana</category>
      <category>loki-stack</category>
      <category>loki-stack 구축하기</category>
      <category>loki-stack 예제</category>
      <category>nks loki promtail garafana</category>
      <category>nks loki-stack</category>
      <category>로키 프롬테일</category>
      <category>로키 프롬테일 그라파나</category>
      <category>로키 프롬테일 사용법</category>
      <category>로키스택</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/338</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-5-Loki-Promtail-Grafana%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%ED%86%B5%ED%95%A9-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95-Feat-Loki-Stack#entry338comment</comments>
      <pubDate>Tue, 18 Feb 2025 12:40:59 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 오브젝트 - Deployment에서 Pod,Container 리소스 조절하기</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-Deployment%EC%97%90%EC%84%9C-PodContainer-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%A1%B0%EC%A0%88%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 kubernetes를 사용하는 목적은 많지만, 가장 큰 목적은 &lt;u&gt;&lt;b&gt;&quot;효율적인 자원의 관리&quot;라고&lt;/b&gt;&lt;/u&gt; 생각합니다. 쿠버네티스 deployment 오브젝트를 통해 pod를 생성할 때 파드의 리소스를 효율적으로 관리하는 방법을 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.. spec.replicas&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디플로이먼트에서는. spec.replicas 스펙을 통해서 replicasets을 조절 가능합니다. 아래와 같이 spec 하위에 replicas의 개수로 배포되는 파드의 레플리카 수를 조절할 수 있는 건 다들 아실 텝니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739765637317&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 각 파드들은 어떻게 노드로부터 리소스(cpu, memory 등)를 할당받아서 사용할 수 있을까요??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 파드 및 컨테이너 리소스 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 저희는 파드를 지정할 때, 컨테이너에 필요한 각 리소스의 양을 선택적으로 지정할 수 있습니다. 가장 일반적인 리소스는 CPU와 메모리(RAM)입니다. 파드에서 컨테이너에 대한 &lt;u&gt;&lt;b&gt;리소스 요청(request)을 지정&lt;/b&gt;&lt;/u&gt;하게 되면, &lt;u&gt;&lt;b&gt;kube-scheduler는 이 정보를 사용하여 파드가 배치될 노드를 결정&lt;/b&gt;&lt;/u&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너에 대한 &lt;u&gt;&lt;b&gt;리소스 제한(limit)을 지정&lt;/b&gt;&lt;/u&gt;하면, &lt;u&gt;&lt;b&gt;kubelet은&lt;/b&gt;&lt;/u&gt; 실행 중인 컨테이너가 설정한 제한보다 많은 리소스를 사용할 수 없도록 해당 제한을 적용하여 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 요청 및 제한하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드가 실행 중인 노드에 사용 가능한 리소스가 충분하다면, 컨테이너가 해당 리소스에 지정한 request보다 더 많은 리소스를 사용할 수 있도록 허용하는 게 기본이고, 컨테이너는 limit보다 더 많은 리소스를 사용할 수 없게 됩니다. 예를 들어서, 컨테이너에 대해 256 Mib의 memory 요청을 설정하고, 해당 컨테이너가 8 Gib의 메모리를 가진 노드로 스케줄 된 파드에 있고 다른 파드는 없는 경우, 컨테이너는 더 많은 RAM을 사용할 수 있게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 컨테이너에 4 Gib 메모리 제한을 설정하면 kubelet, 컨테이너 런타임들이 제한을 적용하게 됩니다. 런타임은 컨테이너가 구성된 리소스 제한을 초과하여 사용하지 못하게 막는데, 예를 들어서 컨테이너의 프로세스가 허용된 양보다 많은 메모리를 사용하려고 한다면 시스템 커널은 out of memory (OOM) 오류와 함께 할당을 시도한 프로세스를 종료시키게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 제한은 반응적 혹은 강제적으로 구현할 수 있고, 런타임마다 다른 방식으로 동일한 제약을 구현하는 것도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. 파드와 컨테이너의 리소스 요청 제한&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 컨테이너에 대해, 다음과 같은 리소스 제한(limit) 및 요청(request)을 지정할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #222222; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spec.containers[].resources.limits.cpu&lt;/li&gt;
&lt;li&gt;spec.containers[].resources.limits.memory&lt;/li&gt;
&lt;li&gt;spec.containers[].resources.limits.hugepages-&amp;lt;size&amp;gt;&lt;/li&gt;
&lt;li&gt;spec.containers[].resources.requests.cpu&lt;/li&gt;
&lt;li&gt;spec.containers[].resources.requests.memory&lt;/li&gt;
&lt;li&gt;spec.containers[].resources.requests.hugepages-&amp;lt;size&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1739767615311&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: mingyu-api-deployment   # deployment의 이름
  namespace: test
  labels:
    app: mingyu-api-deployment  # deployment의 라벨
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mingyu-api-deployment
  template:
    metadata:
      labels:
        app: mingyu-api-deployment
    spec:
      containers:
        - name: mingyu-api
          image: hub.docker.com/mingyu-api-dev:latest
          resources:
            requests:
              memory: &quot;256Mi&quot;
              cpu: &quot;250m&quot;
            limits:
              memory: &quot;512Mi&quot;
              cpu: &quot;500m&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 deployment.yaml을 작성할 경우, 컨테이너 리소스에 대해 cpu는 250m (1 코어 = 1000m), 메모리는 256 Mib를 요청하여 생성하기에 최소 자원으로 보장받게 됩니다. 다만 사용량이 많아질 경우에는 각 2배씩 향상된 500m cpu에 512 Mib 메모리까지 유동적으로 가용 가능한 리소스가 있다면 사용 가능하게 하겠다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. &lt;span style=&quot;color: #ff481b; text-align: start;&quot;&gt;0/3 nodes are available 오류&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터들의 리소스들을 request 하여 최소 필요 용량을 점유하다 보면, 분명 아래와 같은 오류를 맞닥뜨리게 될 것입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;0/3 nodes are available: 3 Insufficient cpu. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오류는 &lt;b&gt;새로운 파드가 요청한 CPU 리소스를 클러스터 내 어떤 노드에서도 충족시킬 수 없기 때문에&lt;/b&gt; 발생합니다. 구체적으로:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;284&quot; data-start=&quot;76&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;159&quot; data-start=&quot;76&quot;&gt;&lt;b&gt;Insufficient cpu&lt;/b&gt;: 새 파드가 정의한 CPU 요청량(requests)이 클러스터의 노드들에 남아있는 가용 CPU보다 많습니다.&lt;/li&gt;
&lt;li data-end=&quot;284&quot; data-start=&quot;160&quot;&gt;&lt;b&gt;No preemption victims found&lt;/b&gt;: 스케줄러가 자원을 확보하기 위해 낮은 우선순위의 파드를 강제로 종료(preemption)하려 했지만, 강제로 종료할 만한 후보 파드를 찾지 못했다는 의미입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;366&quot; data-start=&quot;286&quot; data-ke-size=&quot;size16&quot;&gt;즉, 파드가 요청한 CPU 리소스가 현재 클러스터(3개 노드)에 남아있는 자원보다 많아서 스케줄링이 불가능한 상태입니다.&lt;br /&gt;해결 방법으로는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-is-last-node=&quot;&quot; data-end=&quot;444&quot; data-start=&quot;367&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;392&quot; data-start=&quot;367&quot;&gt;파드의 CPU 리소스 요청을 줄이거나,&lt;/li&gt;
&lt;li data-is-last-node=&quot;&quot; data-end=&quot;444&quot; data-start=&quot;393&quot;&gt;클러스터에 더 많은 CPU 자원을 가진 노드를 추가하여 용량을 확장하는 방법이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버테니스에서 파드 및 컨테이너 리소스를 관리하는 방법은 쿠버네티스 공식 문서에 자세히 나와있습니다. 아래 사이트를 참고하여 원하는 방향성에 맞게 애플리케이션 컨테이너의 리소스를 제한하여 안전한 k8s 환경을 운영해 보아요 :)&lt;/p&gt;
&lt;figure id=&quot;og_1739768056397&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;파드 및 컨테이너 리소스 관리&quot; data-og-description=&quot;파드를 지정할 때, 컨테이너에 필요한 각 리소스의 양을 선택적으로 지정할 수 있다. 지정할 가장 일반적인 리소스는 CPU와 메모리(RAM) 그리고 다른 것들이 있다. 파드에서 컨테이너에 대한 리소&quot; data-og-host=&quot;kubernetes.io&quot; data-og-source-url=&quot;https://kubernetes.io/ko/docs/concepts/configuration/manage-resources-containers/&quot; data-og-url=&quot;https://kubernetes.io/ko/docs/concepts/configuration/manage-resources-containers/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1wdRv/hyYfW54N33/UjXhjh7PrADOgEFGSov76K/img.png?width=1727&amp;amp;height=373&amp;amp;face=0_0_1727_373&quot;&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/configuration/manage-resources-containers/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kubernetes.io/ko/docs/concepts/configuration/manage-resources-containers/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1wdRv/hyYfW54N33/UjXhjh7PrADOgEFGSov76K/img.png?width=1727&amp;amp;height=373&amp;amp;face=0_0_1727_373');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;파드 및 컨테이너 리소스 관리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;파드를 지정할 때, 컨테이너에 필요한 각 리소스의 양을 선택적으로 지정할 수 있다. 지정할 가장 일반적인 리소스는 CPU와 메모리(RAM) 그리고 다른 것들이 있다. 파드에서 컨테이너에 대한 리소&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kubernetes.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>k8s limits 사용</category>
      <category>k8s requests limists</category>
      <category>k8s requests 사용</category>
      <category>k8s resource</category>
      <category>k8s resource 관리</category>
      <category>k8s 리소스 관리</category>
      <category>k8s 리소스 관리 예제</category>
      <category>k8s 리소스 설정</category>
      <category>k8s 컨테이너 리소스 관리</category>
      <category>k8s 파드 리소스</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/337</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-Deployment%EC%97%90%EC%84%9C-PodContainer-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%A1%B0%EC%A0%88%ED%95%98%EA%B8%B0#entry337comment</comments>
      <pubDate>Mon, 17 Feb 2025 13:57:44 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 4. Service, Ingress, Ingress Controller를 이용한 DNS의 네트워킹 구성 with Aws ALB</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅들을 통해 cluster 내부에 deployment로 생성된 pod들로 트래픽을 보내야 합니다. 우리는 파드로의 접근을 이전 포스팅에서 서비스라는 쿠버네티스 오브젝트를 사용하여 이미 완성시켜 놨습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[이전글]&lt;/p&gt;
&lt;figure id=&quot;og_1741913632081&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NCP] NKS로 개발환경 구성하기 Chapter 3. NextJs Application 배포하기 (Feat Ncp SourcePipeLine)&quot; data-og-description=&quot;서론NextJs 프레임워크로 제작한 Front Application을 cluster에 배포하는 일련의 과정을 포스팅하고자 합니다. 본 포스팅은 NCP 서비스를 기반으로 작성되는 점 참고 부탁드립니다. 빌드는 NCP의 SourceBuild&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-3-NextJs-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-3-NextJs-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bpv3E6/hyYrVehGeP/ko0ekRzpjEc9jz48bLkkNk/img.png?width=800&amp;amp;height=390&amp;amp;face=0_0_800_390,https://scrap.kakaocdn.net/dn/ymv0K/hyYrZHLEPa/yN2kSMzlq8QUcoRCOjqORK/img.png?width=800&amp;amp;height=390&amp;amp;face=0_0_800_390,https://scrap.kakaocdn.net/dn/bcfB2K/hyYr0fA7E2/CMTwp2AKsg26zImDxore1K/img.png?width=3000&amp;amp;height=1464&amp;amp;face=0_0_3000_1464&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-3-NextJs-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-3-NextJs-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bpv3E6/hyYrVehGeP/ko0ekRzpjEc9jz48bLkkNk/img.png?width=800&amp;amp;height=390&amp;amp;face=0_0_800_390,https://scrap.kakaocdn.net/dn/ymv0K/hyYrZHLEPa/yN2kSMzlq8QUcoRCOjqORK/img.png?width=800&amp;amp;height=390&amp;amp;face=0_0_800_390,https://scrap.kakaocdn.net/dn/bcfB2K/hyYr0fA7E2/CMTwp2AKsg26zImDxore1K/img.png?width=3000&amp;amp;height=1464&amp;amp;face=0_0_3000_1464');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[NCP] NKS로 개발환경 구성하기 Chapter 3. NextJs Application 배포하기 (Feat Ncp SourcePipeLine)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론NextJs 프레임워크로 제작한 Front Application을 cluster에 배포하는 일련의 과정을 포스팅하고자 합니다. 본 포스팅은 NCP 서비스를 기반으로 작성되는 점 참고 부탁드립니다. 빌드는 NCP의 SourceBuild&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Service&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-1. 기존 service.yaml 설명&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 이전 NextJs 프로젝트를 배포할때 사용했던 service.yaml 파일입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739517998947&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: mingyu-cms-service
  namespace: test
spec:
  type: NodePort
  selector:
    app: mingyu-cms-deployment
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 3000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 내용을 확인해보면, mingyu-cms-service라는 이름의 Service 리소스를 test 네임스페이스에 생성하는데, type을 NodePort로 지정하여 클러스터 외부에서 노드 IP와 할당된 포트를 통해 접근할 수 있도록 하고, mingyu-cms-deployment 라벨을 가지는 파드들로 트래픽을 라우팅 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 Service는 클러스터 내의 mingyu-cms-deployment 라벨이 붙은 파드들에 대해 외부에서 접근할 수 있도록, 노드의 지정된 포트를 통해 HTTP 요청(80번 포트)을 받아 내부 파드의 3000번 포트로 전달하는 역할을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 생성된 서비스들을 직접 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739518547058&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl get svc -n namespace&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8TCBG/btsMjOkUzEO/yMqJqwkRwwCp7lVKGuONBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8TCBG/btsMjOkUzEO/yMqJqwkRwwCp7lVKGuONBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8TCBG/btsMjOkUzEO/yMqJqwkRwwCp7lVKGuONBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8TCBG%2FbtsMjOkUzEO%2FyMqJqwkRwwCp7lVKGuONBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;944&quot; height=&quot;160&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-2. NordPort 타입의 서비스 사용 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서비스를 NodePort로 지정한 이유는 Ingress를 사용하는 구성은 주로 &lt;b&gt;외부 트래픽을 안정적으로 클러스터 내부의 서비스로 전달&lt;/b&gt;하기 위한 전략으로 Ingress는 NodePort, LoadBalncer 유형의 의 서비스만 사용할 수 있기 때문입니다. 저는 평범한 Ingress가 아닌 AWS의 ALB Ingress를 사용해서 구성했고, 이 alb ingress는 자동으로 Application Load Balancer가 프로비저닝 되기 때문에 NodePort 타입으로 지정하면 되기 때문입니다. ( 아니 근데, 왜 네이버에서 aws 서비스를,,,라고 하기엔 objectstorage도 s3 api를 활용하고,,  ncp 너네 혹시..? ㅎ)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Ingress &amp;amp; Ingress Controller&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. Ingress&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 인그레스가 도대체 뭐 하는 친구일인가 하면, 클러스터 외부에서 클러스터 내부 Service로 HTTP와 HTTPS 경로를 노출하는 친구입니다. 트래픽 라우팅의 경우 인그레스 리소스를 생성할 때 정의할 수 있습니다. 아래는 인그레스가 트래픽을 하나의 서비스로 보내는 예시입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qvC4e/btsMiITAObY/QqRsR95fk9YvoBEEbbV9qK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qvC4e/btsMiITAObY/QqRsR95fk9YvoBEEbbV9qK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qvC4e/btsMiITAObY/QqRsR95fk9YvoBEEbbV9qK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqvC4e%2FbtsMiITAObY%2FQqRsR95fk9YvoBEEbbV9qK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;754&quot; height=&quot;218&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 이 인그레스가 동작하기 위해서는 전제조건이 있는데, 그것은 바로 인그레스 컨트롤러가 있어야만 한다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. Ingress Controller&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 내의 인그레스가 동작하려면 인그레스 컨트롤러가 실행되고 있어야 합니다. 최소 하나의 인그레스 컨트롤러를 선택하여 클러스터 내에 설치하는데, 쿠버네티스는 aws, gce, nginx 인그레스 컨트롤러를 지원하고 유지해 줍니다. 저는 여기서 aws의 alb를 사용할 것입니다. 더 자세한 내용은 쿠버네티스 공식 문서를 확인하면 쉽게 확인할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1739519591337&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;인그레스 컨트롤러&quot; data-og-description=&quot;클러스터 내의 [인그레스](/ko/docs/concepts/services-networking/ingress/)가 작동하려면, 인그레스 컨트롤러가 실행되고 있어야 한다. 적어도 하나의 인그레스 컨트롤러를 선택하고 이를 클러스터 내에 설&quot; data-og-host=&quot;kubernetes.io&quot; data-og-source-url=&quot;https://kubernetes.io/ko/docs/concepts/services-networking/ingress-controllers/&quot; data-og-url=&quot;https://kubernetes.io/ko/docs/concepts/services-networking/ingress-controllers/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bW2AIo/hyYfzWFTGn/EjKQRLVYjwffzCRfT6Lpb0/img.png?width=1727&amp;amp;height=373&amp;amp;face=0_0_1727_373&quot;&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/services-networking/ingress-controllers/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kubernetes.io/ko/docs/concepts/services-networking/ingress-controllers/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bW2AIo/hyYfzWFTGn/EjKQRLVYjwffzCRfT6Lpb0/img.png?width=1727&amp;amp;height=373&amp;amp;face=0_0_1727_373');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;인그레스 컨트롤러&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;클러스터 내의 [인그레스](/ko/docs/concepts/services-networking/ingress/)가 작동하려면, 인그레스 컨트롤러가 실행되고 있어야 한다. 적어도 하나의 인그레스 컨트롤러를 선택하고 이를 클러스터 내에 설&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kubernetes.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. ingress.yaml 파일 작성 및 ingress 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 ingressClassName을 alb로 지정하여 alb ingress anotations를 활용할 수 있습니다. 저는 80,443 포트를 열고있는 test-ingress를 무조건 https로 redirect시키고 /health uri로 healthcheck를 하는 인그레스를 생성하려 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739519763999&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  namespace: test
  annotations:
    alb.ingress.kubernetes.io/listen-ports: '[{&quot;HTTP&quot;: 80}, {&quot;HTTPS&quot;: 443}]'
    alb.ingress.kubernetes.io/ssl-certificate-no: &quot;인증서번호&quot;
    alb.ingress.kubernetes.io/description: &quot;test-ingress controller&quot;
    alb.ingress.kubernetes.io/ssl-redirect: &quot;443&quot;
    alb.ingress.kubernetes.io/healthcheck-path: &quot;/health&quot;
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: &quot;120&quot;
spec:
  ingressClassName: alb
  rules:
    - host: dev-www.test.co.kr
      http:
        paths:
          - path: /*
            pathType: Prefix
            backend:
              service:
                name: test-service
                port:
                  number: 80
    - host: dev-m.test.co.kr
      http:
        paths:
          - path: /*
            pathType: Prefix
            backend:
              service:
                name: test-service
                port:
                  number: 80

    - host: dev-api.test.co.kr
      http:
        paths:
          - path: /*
            pathType: Prefix
            backend:
              service:
                name: test-api-service
                port:
                  number: 80
    - host: dev-cms.test.co.kr
      http:
        paths:
          - path: /*
            pathType: Prefix
            backend:
              service:
                name: test-cms-service
                port:
                  number: 80
    - host: dev-cms2.test.co.kr
      http:
        paths:
          - path: /*
            pathType: Prefix
            backend:
              service:
                name: test-cms2-service
                port:
                  number: 80
    - host: dev-cms2-api.test.co.kr
      http:
        paths:
          - path: /*
            pathType: Prefix
            backend:
              service:
                name: cms2-api-service
                port:
                  number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 kubectl 명령어를 사용하여 해당 ingress를 apply해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739519935682&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl apply -f ingress.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 kubernetes 대시보드의 서비스 &amp;gt; 인그레스 탭에 접속하면 인그레스가 아래와 같이 정상적으로 잘 생성된것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;917&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FkTcB/btsMia4ctHw/t2AJYNksfttiMr9lyZrqkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FkTcB/btsMia4ctHw/t2AJYNksfttiMr9lyZrqkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FkTcB/btsMia4ctHw/t2AJYNksfttiMr9lyZrqkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFkTcB%2FbtsMia4ctHw%2Ft2AJYNksfttiMr9lyZrqkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1902&quot; height=&quot;917&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1902&quot; data-origin-height=&quot;917&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. LB 생성 확인 및 DNS 설정 (NCP)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Naver Cloud Platform의 Load Balncer 메뉴에 들어가보면 ingress.yaml로 생성한 로드벨런서가 정상적으로 잘 생성된것들을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1911&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpTKtY/btsMkcFTbO9/rlGc9keJsgo4p6hVVKUQo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpTKtY/btsMkcFTbO9/rlGc9keJsgo4p6hVVKUQo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpTKtY/btsMkcFTbO9/rlGc9keJsgo4p6hVVKUQo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpTKtY%2FbtsMkcFTbO9%2FrlGc9keJsgo4p6hVVKUQo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1911&quot; height=&quot;847&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1911&quot; data-origin-height=&quot;847&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Global DNS 메뉴로 접속하여 도메인에 맞는 호스트(2차도메인)을 설정해주고, 레코드 타입을 A (LB VPC)로 설정하여 생성된 LoadBalancer를 설정해주면 모든 준비는 끝납니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아맞다!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경은 사내망(내부 네트워크)에서만 접근 가능하도록 구성해야 합니다. 이를 위해 dev 클러스터를 생성할 때, 해당 클러스터가 위치한 서브넷에 적용된 &lt;b&gt;Network ACL&lt;/b&gt;에서 inbound 및 outbound 규칙을 사내 IP(내부망 IP)만 허용하고 나머지 트래픽은 모두 차단하도록 설정하여, 외부 접근을 원천 봉쇄할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;818&quot; data-start=&quot;619&quot; data-ke-size=&quot;size16&quot;&gt;물론 &lt;b&gt;보안그룹(Security Group)&lt;/b&gt; 단위로도 접근 제어를 할 수 있지만, 보안그룹은 인스턴스(또는 파드) 단위에서 상태 기반(stateful)으로 작동하는 반면, Network ACL은 서브넷 단위에서 stateless하게 작동하여 네트워크 레벨에서 최초부터 접근을 차단할 수 있다는 점에서 보다 근본적인 봉쇄 효과를 기대할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;925&quot; data-start=&quot;824&quot; data-ke-size=&quot;size16&quot;&gt;따라서, 만약 서버 단에서 추가적인 제어를 원한다면 보안그룹으로도 설정할 수 있지만, 네트워크 자체에서 접근을 아예 막으려면 Network ACL로 원천 봉쇄하는 것이 효과적입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;925&quot; data-start=&quot;824&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 api나 front application은 이 정도로 개발환경 구성을 끝마칠 수 있었습니다. 다만 문자나 알림톡, 이메일 전송기능을 담당하는 전송 관련 어플리케이션이나 배치 어플리케이션의 경우 좀 다른 방법으로 개발환경을 구성하였었는데, 그 내용은 추후 포스팅 하도록 하겠습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>alb ingress 사용방법</category>
      <category>aws ingress</category>
      <category>ncp alb ingress</category>
      <category>ncp alb 사용</category>
      <category>ncp nacl</category>
      <category>nks alb 사용</category>
      <category>nks ingress 설정</category>
      <category>nks 개발</category>
      <category>nks 구축</category>
      <category>개발 환경 구성</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/336</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-4-Service-Ingress-Ingress-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-DNS%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EA%B5%AC%EC%84%B1-with-Aws-ALB#entry336comment</comments>
      <pubDate>Sat, 15 Feb 2025 08:30:27 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 3. NextJs Application 배포하기 (Feat Ncp SourcePipeLine)</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-3-NextJs-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJs 프레임워크로 제작한 Front Application을 cluster에 배포하는 일련의 과정을 포스팅하고자 합니다. 본 포스팅은 NCP 서비스를 기반으로 작성되는 점 참고 부탁드립니다. 빌드는 NCP의 SourceBuild, 배포는 SourceDeploy, 파이프라인은 SourcePipeLine을 사용하며 Kubernetes는 NKS로 작성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;[이전글]&lt;/p&gt;
&lt;figure id=&quot;og_1741913587584&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NCP] NKS로 개발환경 구성하기 Chapter 2. Spring Boot Application 배포하기 (Feat Ncp SourcePipeLine)&quot; data-og-description=&quot;서론SpringBoot 프레임워크로 제작한 API Application을 cluster에 배포하는 일련의 과정을 포스팅하고자 합니다. 본 포스팅은 NCP 서비스를 기반으로 작성되는 점 참고 부탁드립니다. 빌드는 NCP의 SourceBui&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-2-Spring-Boot-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-2-Spring-Boot-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4VeNn/hyYqNA6mZ2/deJqVKKGcPiWcOA89xYEg1/img.png?width=800&amp;amp;height=393&amp;amp;face=0_0_800_393,https://scrap.kakaocdn.net/dn/bn6Ng7/hyYqUUylLN/YOyeTbsW8tz8oiuQUi8Syk/img.png?width=800&amp;amp;height=393&amp;amp;face=0_0_800_393,https://scrap.kakaocdn.net/dn/zAmGZ/hyYr0Nrx2v/NilAT6tnwd8wnMuSoGk7R1/img.png?width=3024&amp;amp;height=1484&amp;amp;face=0_0_3024_1484&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-2-Spring-Boot-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-2-Spring-Boot-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4VeNn/hyYqNA6mZ2/deJqVKKGcPiWcOA89xYEg1/img.png?width=800&amp;amp;height=393&amp;amp;face=0_0_800_393,https://scrap.kakaocdn.net/dn/bn6Ng7/hyYqUUylLN/YOyeTbsW8tz8oiuQUi8Syk/img.png?width=800&amp;amp;height=393&amp;amp;face=0_0_800_393,https://scrap.kakaocdn.net/dn/zAmGZ/hyYr0Nrx2v/NilAT6tnwd8wnMuSoGk7R1/img.png?width=3024&amp;amp;height=1484&amp;amp;face=0_0_3024_1484');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[NCP] NKS로 개발환경 구성하기 Chapter 2. Spring Boot Application 배포하기 (Feat Ncp SourcePipeLine)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론SpringBoot 프레임워크로 제작한 API Application을 cluster에 배포하는 일련의 과정을 포스팅하고자 합니다. 본 포스팅은 NCP 서비스를 기반으로 작성되는 점 참고 부탁드립니다. 빌드는 NCP의 SourceBui&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CI/CD 파이프라인 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인 설계는 이전 포스팅과 동일합니다. Naver Cloud Platform에서 제공해주는 Source Build와 Deploy, Pipeline 서비스의 구성이기 때문에&lt;u&gt;&lt;b&gt; &quot;Chapter 2. Spring Boot Application 배포하기&quot;&lt;/b&gt;&lt;/u&gt; 포스팅을 참고하여주세요. 다만, Build시 매니페스트에 deployment.yaml과 더불어 service.yaml 파일을 추가해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Next.js 빌드 프로세스 살펴보기&lt;/h3&gt;
&lt;p data-end=&quot;423&quot; data-start=&quot;188&quot; data-ke-size=&quot;size16&quot;&gt;Next.js는 React 기반의 프레임워크로서, CSR(Client-Side Rendering), SSR(Server-Side Rendering), SSG(Static Site Generation) 등 다양한 렌더링 방식을 제공하고 있습니다. 이러한 기능들을 효율적으로 수행하기 위해, Next.js에서는 개발 모드(dev)와 &lt;b&gt;프로덕션 빌드(production)&lt;/b&gt; 시점에 각각 최적화 과정을 거치게 됩니다.&lt;/p&gt;
&lt;h4 data-end=&quot;447&quot; data-start=&quot;425&quot; data-ke-size=&quot;size20&quot;&gt;1. next build 명령&lt;/h4&gt;
&lt;p data-end=&quot;505&quot; data-start=&quot;449&quot; data-ke-size=&quot;size16&quot;&gt;Next.js는 프로덕션 환경에서 동작하기 전 반드시 next build 프로세스를 거칩니다. 제가 구성하고싶은 개발 환경은 스테이지 환경처럼 구성하고 싶기 때문에 저는 프로덕션 모드의 빌드를 진행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;918&quot; data-start=&quot;506&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;695&quot; data-start=&quot;506&quot;&gt;&lt;b&gt;개발 모드(Development mode)&lt;/b&gt;: next dev
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;695&quot; data-start=&quot;552&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;601&quot; data-start=&quot;552&quot;&gt;Webpack의 Hot Reload 기능과 빠른 빌드 속도에 최적화되어 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;639&quot; data-start=&quot;604&quot;&gt;파일이 변경될 때마다 새로 빌드해주는 방식을 사용합니다.&lt;/li&gt;
&lt;li data-end=&quot;695&quot; data-start=&quot;642&quot;&gt;production용 최적화가 적용되지 않기 때문에 빌드 결과물 용량이 클 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;918&quot; data-start=&quot;697&quot;&gt;&lt;b&gt;프로덕션 모드(Production mode)&lt;/b&gt;: next build 후 start
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;918&quot; data-start=&quot;761&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;784&quot; data-start=&quot;761&quot;&gt;배포용 프로덕션 빌드를 생성합니다.&lt;/li&gt;
&lt;li data-end=&quot;840&quot; data-start=&quot;787&quot;&gt;사용되지 않는 코드 제거, 코드 압축, 이미지 최적화 등 다양한 최적화 과정을 거칩니다.&lt;/li&gt;
&lt;li data-end=&quot;918&quot; data-start=&quot;843&quot;&gt;각 페이지가 server 혹은 static으로 처리되는지에 따라 다른 형태로 빌드 아티팩트(Artifacts)가 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;939&quot; data-start=&quot;920&quot; data-ke-size=&quot;size20&quot;&gt;2. 빌드 결과물 구조 이해&lt;/h4&gt;
&lt;p data-end=&quot;1004&quot; data-start=&quot;941&quot; data-ke-size=&quot;size16&quot;&gt;next build를 수행하면 .next 폴더가 생성되고, 그 안에 다음과 같은 빌드 산출물이 포함됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1345&quot; data-start=&quot;1006&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1145&quot; data-start=&quot;1006&quot;&gt;&lt;b&gt;server 폴더&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1145&quot; data-start=&quot;1026&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1066&quot; data-start=&quot;1026&quot;&gt;서버 렌더링에 필요한 번들 파일들과 페이지들이 여기에 위치합니다.&lt;/li&gt;
&lt;li data-end=&quot;1145&quot; data-start=&quot;1069&quot;&gt;_app.js, _document.js, _error.js 등 Next.js 내부에서 사용하는 핵심 파일도 포함됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1284&quot; data-start=&quot;1146&quot;&gt;&lt;b&gt;static / build-xxxxx 폴더&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1284&quot; data-start=&quot;1180&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1220&quot; data-start=&quot;1180&quot;&gt;정적으로 제공될 수 있는 리소스(이미지, 폰트 등)가 위치합니다.&lt;/li&gt;
&lt;li data-end=&quot;1284&quot; data-start=&quot;1223&quot;&gt;Webpack, Babel 등에 의해 번들링된 자바스크립트, CSS 등이 최적화된 형태로 배포 준비됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1345&quot; data-start=&quot;1285&quot;&gt;&lt;b&gt;.next/cache&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1345&quot; data-start=&quot;1307&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1345&quot; data-start=&quot;1307&quot;&gt;빌드 결과물을 캐싱해두어 재빌드 시 성능을 높이는 데 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Dockerfile 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJs의 빌드용 도커파일을 작성합니다. 아래 포스팅을 참고해주세요.&lt;/p&gt;
&lt;figure id=&quot;og_1739756252552&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Docker을 이용해 NextJs Application 빌드하기&quot; data-og-description=&quot;서론NextJs Framework 기반의 Application을 build하는 방법중 많이 쓰이는 방법은 패키지 매니저를 활용한 빌드입니다.  는  패키지 매니저와 더불어 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-NextJs-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-NextJs-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwUIfm/hyYfZ2K3CV/9vyKnwJTouc7HxRl87js01/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/clon89/hyYfB1PrGr/LhuwFP4rfGvU5nhbcI4Vkk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/ekvro0/hyYfJr2ZAc/vNrWyIMPL1Bak6zRidr1Hk/img.png?width=1876&amp;amp;height=907&amp;amp;face=0_0_1876_907&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-NextJs-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-NextJs-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwUIfm/hyYfZ2K3CV/9vyKnwJTouc7HxRl87js01/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/clon89/hyYfB1PrGr/LhuwFP4rfGvU5nhbcI4Vkk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/ekvro0/hyYfJr2ZAc/vNrWyIMPL1Bak6zRidr1Hk/img.png?width=1876&amp;amp;height=907&amp;amp;face=0_0_1876_907');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Docker을 이용해 NextJs Application 빌드하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론NextJs Framework 기반의 Application을 build하는 방법중 많이 쓰이는 방법은 패키지 매니저를 활용한 빌드입니다.  는  패키지 매니저와 더불어 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. deployment.yaml 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 사용하여 만들어놓은 이미지를 가지고 cluster에 deployment하는 내용의 yaml 파일을 작성합니다. NextJs를 프로덕션처럼 배포하였으니 기본 3000번 포트를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739509550361&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: mingyu-cms-deployment   # deployment의 이름
  namespace: test
  labels:
    app: mingyu-cms-deployment  # deployment의 라벨
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mingyu-cms-deployment # replicaset이 관리할 pod 라벨
  template:    # 하위에 pod 정보 설정
    metadata:
      labels:
        app: mingyu-cms-deployment # pod의 라벨
    spec: # 컨테이너 설정
      containers:
        - name: mingyu-cms
          image: vjnibbzs.kr.private-ncr.ntruss.com/mingyu-cms-dev:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
              protocol: TCP
      imagePullSecrets:
        - name: regcred&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. service.yaml 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 트래픽으로부터 deployment에 네트워크 연동을 위해 Service 오브젝트를 아래와 같이 생성해줍니다. http 프로토콜인 80 포트의 트래픽을 deployment로 생성된 pod의 3000번 포트로 타겟을 지정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739509740034&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: mingyu-cms-service
  namespace: test
spec:
  type: NodePort
  selector:
    app: mingyu-cms-deployment
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 3000&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;develop branch에 push를 하여 파이프라인을 동작시킵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGamn/btsMiB0XyTV/WzwuIGMm6AH75rU62vY0hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGamn/btsMiB0XyTV/WzwuIGMm6AH75rU62vY0hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGamn/btsMiB0XyTV/WzwuIGMm6AH75rU62vY0hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGamn%2FbtsMiB0XyTV%2FWzwuIGMm6AH75rU62vY0hK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1464&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k8s cluster 대시보드에 접속하여 해당 이미지로 디플로이먼트가 잘 되었는지 체크해봅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vahNv/btsMhN18ePC/7wzKn94zMGzOfFCrcDMoik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vahNv/btsMhN18ePC/7wzKn94zMGzOfFCrcDMoik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vahNv/btsMhN18ePC/7wzKn94zMGzOfFCrcDMoik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvahNv%2FbtsMhN18ePC%2F7wzKn94zMGzOfFCrcDMoik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1464&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>k8s nextjs</category>
      <category>k8s nextjs 배포</category>
      <category>nextjs k8s</category>
      <category>nextjs k8s 배포</category>
      <category>nextjs kubernetes</category>
      <category>nextjs kubernetes 배포</category>
      <category>nks nectjs 배포</category>
      <category>nks nextjs</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/334</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-3-NextJs-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine#entry334comment</comments>
      <pubDate>Fri, 14 Feb 2025 19:22:00 +0900</pubDate>
    </item>
    <item>
      <title>Docker을 이용해 NextJs Application 빌드하기</title>
      <link>https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-NextJs-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJs Framework 기반의 Application을 build하는 방법중 많이 쓰이는 방법은 패키지 매니저를 활용한 빌드입니다.  는  패키지 매니저와 더불어 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스팅하려 합니다. 본 포스팅에서는 yarn 패키지 매니저를을 활용합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. Node 버전 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nextJs는 react기반의 프레임워크로써 버전에 따라 최소 지원되는 NodeJs버전이 다릅니다. 저는 nextJs 14 버전의 어플리케이션을 빌드할 예정이고 Node.js 버전은 최소 18.17.0 버전을 사용해야하는데 저는 20버전을 활용합니다. package manager로 yarn을 사용하기 때문에 docker hub에서 node 20버전중 yarn이 함께 설치되어있는 apline 버전을 사용하려 합니다.&lt;/p&gt;
&lt;figure id=&quot;og_1739513003141&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;https://hub.docker.com/layers/library/node/20-alpine/images/sha256-fd261b08aa280adee9ba4ea89afe80a7c248799b01084cde9d418c7429f821f1&quot; data-og-description=&quot;&quot; data-og-host=&quot;hub.docker.com&quot; data-og-source-url=&quot;https://hub.docker.com/layers/library/node/20-alpine/images/sha256-fd261b08aa280adee9ba4ea89afe80a7c248799b01084cde9d418c7429f821f1&quot; data-og-url=&quot;https://hub.docker.com/layers/library/node/20-alpine/images/sha256-fd261b08aa280adee9ba4ea89afe80a7c248799b01084cde9d418c7429f821f1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/w7KAg/hyYfWqMttS/u5m7l4zUqQLzni54wj6L4k/img.png?width=3372&amp;amp;height=1896&amp;amp;face=0_0_3372_1896&quot;&gt;&lt;a href=&quot;https://hub.docker.com/layers/library/node/20-alpine/images/sha256-fd261b08aa280adee9ba4ea89afe80a7c248799b01084cde9d418c7429f821f1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hub.docker.com/layers/library/node/20-alpine/images/sha256-fd261b08aa280adee9ba4ea89afe80a7c248799b01084cde9d418c7429f821f1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/w7KAg/hyYfWqMttS/u5m7l4zUqQLzni54wj6L4k/img.png?width=3372&amp;amp;height=1896&amp;amp;face=0_0_3372_1896');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;https://hub.docker.com/layers/library/node/20-alpine/images/sha256-fd261b08aa280adee9ba4ea89afe80a7c248799b01084cde9d418c7429f821f1&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hub.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. .env.k8s-dev 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 포스팅은 k8s 클러스터에 NextJs 프로젝트를 배포할 때 필요한 이미지를 만들기 위해 작성하는 것이기 때문에 프로젝트 디렉토리에 .env.k8s-dev 파일을 생성해줘야 합니다. 해당 env 안에는 nextJs에서 사용할 환경변수들을 설정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739513215174&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# .env.k8s-dev
NEXT_PUBLIC_API_MINGYU=&quot;https://dev-api.mingyu.co.kr&quot;
NEXT_PUBLIC_API_MINGYU2=&quot;https://dev-api.mingyu2.co.kr&quot;
NEXT_PUBLIC_MINGYU_ACCESS_TOKEN_KEY=&quot;token&quot;
NEXT_PUBLIC_COOKIE=&quot;dev-www.mingyu.co.kr&quot;
NEXT_PUBLIC_CRYPTO_SECRET=&quot;secret&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 도커파일을 작성할 때 보통 Build Stage와 Runtime Stage를 나눠서 작성하는데, 그 이유는 이전 포스팅에 적어놨으니 참고하여주시기 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1739513374762&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Docker을 이용해 SpringBoot Application 빌드하기&quot; data-og-description=&quot;서론SpringBoot Framwork 기반의 Application을 build하는 방법은 많이 있습니다. 그 중 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스팅하려 합니다. 본 포스팅에서는 gradle build tool을 활용합니다.&amp;nbsp;&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bZJO63/hyYfLW4JED/QnKMrDTAlzwXr5N32cP87K/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/bUVDZL/hyYfDkrRGD/dGQwNkCBX4PaxJJz4bM9kk/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/iv6hz/hyYfNgimRh/AQlFnZcoUk3SteI1vYjKPk/img.png?width=1918&amp;amp;height=860&amp;amp;face=0_0_1918_860&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bZJO63/hyYfLW4JED/QnKMrDTAlzwXr5N32cP87K/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/bUVDZL/hyYfDkrRGD/dGQwNkCBX4PaxJJz4bM9kk/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/iv6hz/hyYfNgimRh/AQlFnZcoUk3SteI1vYjKPk/img.png?width=1918&amp;amp;height=860&amp;amp;face=0_0_1918_860');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Docker을 이용해 SpringBoot Application 빌드하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론SpringBoot Framwork 기반의 Application을 build하는 방법은 많이 있습니다. 그 중 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스팅하려 합니다. 본 포스팅에서는 gradle build tool을 활용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Dockerfile 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1739513439596&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM node:20-alpine AS builder
WORKDIR &quot;/app&quot;
COPY . .
RUN yarn install
RUN yarn add sharp --ignore-engines
COPY .env.k8s-dev .env
RUN yarn build

FROM node:20-alpine AS development
WORKDIR &quot;/app&quot;
ENV NODE_ENV development
COPY .env.k8s-dev .env.development
COPY --from=builder /app/next.config.mjs /app/next.config.mjs
COPY --from=builder /app/public /app/public
COPY --from=builder /app/.next/standalone /app
COPY --from=builder /app/.next/static /app/.next/static

# health check 용으로 추가 설치 및 타임존 설정
RUN apk --no-cache add curl tzdata &amp;amp;&amp;amp; \
    cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime &amp;amp;&amp;amp; \
    echo &quot;Asia/Seoul&quot; &amp;gt; /etc/timezone &amp;amp;&amp;amp; \
    apk del tzdata

# run server
CMD [&quot;node&quot;, &quot;server.js&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. Docker 빌드하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Docker 명령어를 사용하여 Dockerfile을 기준으로 mingyu- cms 라는 이름의 0.1 버전의 이미지를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739513481679&quot; class=&quot;html xml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;$ docker build -t mingyu-cms:0.1 -f Dockerfile .&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;빌드가 정상적으로 완료되는것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1876&quot; data-origin-height=&quot;907&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMo8O6/btsMiIFOl0W/txy42eUqA6wr0G7NTZym00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMo8O6/btsMiIFOl0W/txy42eUqA6wr0G7NTZym00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMo8O6/btsMiIFOl0W/txy42eUqA6wr0G7NTZym00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMo8O6%2FbtsMiIFOl0W%2Ftxy42eUqA6wr0G7NTZym00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1876&quot; height=&quot;907&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1876&quot; data-origin-height=&quot;907&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5. Docker 이미지 확인하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;docker 명령어를 사용하여 이미지가 잘 생성됬는지 확인합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739513814573&quot; class=&quot;elixir&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ docker images&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같이 0.1버전의 mingyu- cms 이미지가 정상적으로 생성된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Lm0z/btsMit9XEyP/BleqmNKmCPsd7o2k1fTeK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Lm0z/btsMit9XEyP/BleqmNKmCPsd7o2k1fTeK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Lm0z/btsMit9XEyP/BleqmNKmCPsd7o2k1fTeK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Lm0z%2FbtsMit9XEyP%2FBleqmNKmCPsd7o2k1fTeK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;857&quot; height=&quot;72&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각자의 Node 버전 및 NextJs 버전, 패키지 매니저에 따라 취향껏 커스텀하며 도커파일을 작성할 수 있습니다. 도커의 확장성은 무궁무진합니다. 너무 어려워 하지 말고 많이 작성해보고 빌드해보면서 사용해보도록 합시다.&lt;/p&gt;</description>
      <category>Infrastructure/Docker</category>
      <category>docker build</category>
      <category>docker nextjs 빌드</category>
      <category>docker nextjs 빌드하기</category>
      <category>nextjs docker</category>
      <category>nextjs docker build</category>
      <category>nextjs docker로 빌드</category>
      <category>넥스트 도커</category>
      <category>도커 넥스트</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/335</guid>
      <comments>https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-NextJs-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0#entry335comment</comments>
      <pubDate>Fri, 14 Feb 2025 18:30:50 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 2. Spring Boot Application 배포하기 (Feat Ncp SourcePipeLine)</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-2-Spring-Boot-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;서론&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot 프레임워크로 제작한 API Application을 cluster에 배포하는 일련의 과정을 포스팅하고자 합니다. 본 포스팅은 NCP 서비스를 기반으로 작성되는 점 참고 부탁드립니다. 빌드는 NCP의 SourceBuild, 배포는 SourceDeploy, 파이프라인은 SourcePipeLine을 사용하며 Kubernetes는 NKS로 작성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[이전글]&lt;/p&gt;
&lt;figure id=&quot;og_1741913537052&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NCP] NKS로 개발환경 구성하기 Chapter 1. 개발 환경이 없었다&quot; data-og-description=&quot;서론우리 회사에는 개발환경이 존재하지 않았습니다. local에서 개발하고, production에 바로 배포하고 오류가 발생하면 롤백하며 프로덕션 서비스를 운영하는 것이였습니다. 맞습니다. 흔히 말하&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-1-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B4-%EC%97%86%EC%97%88%EB%8B%A4&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-1-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B4-%EC%97%86%EC%97%88%EB%8B%A4&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pYRqr/hyYqVFT1pn/JeGmmlFSSAaAFmkC90mIV1/img.png?width=800&amp;amp;height=534&amp;amp;face=0_0_800_534,https://scrap.kakaocdn.net/dn/4xgrr/hyYqP6M2ir/Yn4e7DBrr5jtI5ml4MKwRK/img.png?width=800&amp;amp;height=534&amp;amp;face=0_0_800_534,https://scrap.kakaocdn.net/dn/V8vdT/hyYqQYVeEW/UEd7QdKBZIKHzKpAabIQrK/img.png?width=2400&amp;amp;height=1604&amp;amp;face=0_0_2400_1604&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-1-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B4-%EC%97%86%EC%97%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-1-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B4-%EC%97%86%EC%97%88%EB%8B%A4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pYRqr/hyYqVFT1pn/JeGmmlFSSAaAFmkC90mIV1/img.png?width=800&amp;amp;height=534&amp;amp;face=0_0_800_534,https://scrap.kakaocdn.net/dn/4xgrr/hyYqP6M2ir/Yn4e7DBrr5jtI5ml4MKwRK/img.png?width=800&amp;amp;height=534&amp;amp;face=0_0_800_534,https://scrap.kakaocdn.net/dn/V8vdT/hyYqQYVeEW/UEd7QdKBZIKHzKpAabIQrK/img.png?width=2400&amp;amp;height=1604&amp;amp;face=0_0_2400_1604');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[NCP] NKS로 개발환경 구성하기 Chapter 1. 개발 환경이 없었다&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론우리 회사에는 개발환경이 존재하지 않았습니다. local에서 개발하고, production에 바로 배포하고 오류가 발생하면 롤백하며 프로덕션 서비스를 운영하는 것이였습니다. 맞습니다. 흔히 말하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CI/CD 파이프라인 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 GitOps와 ArgoCd를 사용한 쿠버 파이프라인을 설계한다면 너무 좋겠지만, 현재 사용하고 있는 운영 서비스와 최대한 비슷하면서도 같게 설계하기 위해 NCP에서 제공하는 서비스인 Source Service(Commit, Build, Deploy, PipeLine)를 사용해서 파이프라인을 설계해 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SourceCommit :&lt;/b&gt; git repository 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SourceBuild :&lt;/b&gt; 각기 다른 언어로 구성된 application들을 알맞게 build해주는 build tool&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SourceDeploy :&lt;/b&gt; build 된 application을 알맞게 배포해 주는 deploy tool&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SourcePipeline : &lt;/b&gt;trigger를 받아서 build와 deploy의 순서 및 연동을 도와주는 pipeline tool&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFuWXt/btsMhnuFKat/klYrAf5QilK0cZmTgDMsoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFuWXt/btsMhnuFKat/klYrAf5QilK0cZmTgDMsoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFuWXt/btsMhnuFKat/klYrAf5QilK0cZmTgDMsoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFuWXt%2FbtsMhnuFKat%2FklYrAf5QilK0cZmTgDMsoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1392&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot 단일 Application 경우 Build 이후 Deploy가 되도록 파이프라인을 설계하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. SourceBuild&amp;nbsp;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SourceBuild 메뉴에 접속하여 빌드 프로젝트 생성하기를 클릭하면 아래와 같이 빌드 프로젝트를 생성하는 설정창을 볼 수 있습니다. 프로젝트 이름과 설명은 애플리케이션에 맞게 api-k8s-dev라고 설정하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YAmdX/btsMgkTeQ43/viWxRhKtOOkLD7gSkl04w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YAmdX/btsMgkTeQ43/viWxRhKtOOkLD7gSkl04w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YAmdX/btsMgkTeQ43/viWxRhKtOOkLD7gSkl04w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYAmdX%2FbtsMgkTeQ43%2FviWxRhKtOOkLD7gSkl04w0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1472&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 대상에서 저는 SourceCommit을 사용하기 때문에 SourceCommit을 선택한 후, 리포지토리에서 빌드하고 싶은 repository name을 설정하면 됩니다. 현재는 SpringBoot 기반의 Application Source를 관리하는 api repository를 입력합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tIMob/btsMgJypRPZ/xivXt814bo3I154Ss9yu40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tIMob/btsMgJypRPZ/xivXt814bo3I154Ss9yu40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tIMob/btsMgJypRPZ/xivXt814bo3I154Ss9yu40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtIMob%2FbtsMgJypRPZ%2FxivXt814bo3I154Ss9yu40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1484&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드환경은 SourceBuild에서 관리되는 이미지 중 기본 Ubuntu 운영체제에 Base 런타임 이미지를 활용하였습니다. Docker build 명령을 수행하여야 하는데, 설치 패키지 목록에서 Docker가 설치되어 있는 이미지를 활용하면 어떤 빌드 런타임 이미지이건 무방합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 우리는 도커 이미지 빌드를 사용하기 때문에 docker build 체크박스를 선택해 줍니다. 그리고 빌드 명령어 보기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuWb8Y/btsMgPrGmd3/2uboQNU7CnHFCcLAyQgaF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuWb8Y/btsMgPrGmd3/2uboQNU7CnHFCcLAyQgaF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuWb8Y/btsMgPrGmd3/2uboQNU7CnHFCcLAyQgaF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuWb8Y%2FbtsMgPrGmd3%2F2uboQNU7CnHFCcLAyQgaF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1472&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드는 빌드 전 명령어 &amp;gt; 빌드 명령어 &amp;gt; 도커 이미지 빌드 설정 &amp;gt; 빌드 후 명령어 순서로 실행됩니다. 저희는 이전에 생성해 두었던 도커 파일을 사용하여 빌드를 진행할 것이고, 빌드가 완료된 이미지를 ContainerRegistry(container image를 저장 및 관리할 수 있는 ncp service 중 하나)에 자동으로 push 해줄 수 있습니다. 때문에 Conainer Registry를 지정하고 이미지 이름과 태그를 설정합니다. 이미지 태그의 경우 &quot;*&quot; 키워드를 입력하면 ncp가 자동 증가를 시켜주지만, dev 환경 구성이기에 이전 이미지를 관리하고 싶지 않아서 저는 1.0.1 1개의 태그만 생성 및 관리하기로 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커파일은 아래 포스팅을 참고해 주시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.02.13 - [Docker] - Docker을 이용해 SpringBoot Application 빌드하기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739433528793&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Docker을 이용해 SpringBoot Application 빌드하기&quot; data-og-description=&quot;서론SpringBoot Framwork 기반의 Application을 build하는 방법은 많이 있습니다. 그 중 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스팅하려 합니다. 본 포스팅에서는 gradle build tool을 활용합니다.&amp;nbsp;&quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dumZmS/hyYap2vdBF/M9vG9dHc1X3O5WDXukNUAk/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/b1d5DJ/hyYfEXP4PH/V1g1ghQHcWUZR9g705REgk/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/cg4K7M/hyYadt8YCN/235SfmCxIVKzAWGC4VoIs0/img.png?width=1918&amp;amp;height=860&amp;amp;face=0_0_1918_860&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dumZmS/hyYap2vdBF/M9vG9dHc1X3O5WDXukNUAk/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/b1d5DJ/hyYfEXP4PH/V1g1ghQHcWUZR9g705REgk/img.jpg?width=800&amp;amp;height=630&amp;amp;face=0_0_800_630,https://scrap.kakaocdn.net/dn/cg4K7M/hyYadt8YCN/235SfmCxIVKzAWGC4VoIs0/img.png?width=1918&amp;amp;height=860&amp;amp;face=0_0_1918_860');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Docker을 이용해 SpringBoot Application 빌드하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론SpringBoot Framwork 기반의 Application을 build하는 방법은 많이 있습니다. 그 중 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스팅하려 합니다. 본 포스팅에서는 gradle build tool을 활용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Source Deploy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Source Deploy에서는 배포 Stage를 기준으로 실제 환경에 배포되는 내용, 개발 환경에 배포되는 내용 등을 구분할 수 있습니다. 저는 k8s-dev cluster에 배포되게 하기 위해서 k8s-dev 라는 명칭의 배포 Stage를 생성하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lyZ8W/btsMhlKsLfs/Cw7cG2tiXAPzD5BkvGrXXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lyZ8W/btsMhlKsLfs/Cw7cG2tiXAPzD5BkvGrXXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lyZ8W/btsMhlKsLfs/Cw7cG2tiXAPzD5BkvGrXXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlyZ8W%2FbtsMhlKsLfs%2FCw7cG2tiXAPzD5BkvGrXXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1472&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 타겟은 일반 Instance Server부터 ObjectStorage까지 본인의 환경 구성에 맞게 고르실 수 있습니다. 저는 k8s cluster에 배포하기 위해 Ncloud Kubernetes Service를 배포 타겟으로 잡았고, Cluster는 dev-cluster로 구성하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chjz85/btsMgUmf7PE/xeOAYWsZnIz7RKGMR32uWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chjz85/btsMgUmf7PE/xeOAYWsZnIz7RKGMR32uWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chjz85/btsMgUmf7PE/xeOAYWsZnIz7RKGMR32uWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchjz85%2FbtsMgUmf7PE%2FxeOAYWsZnIz7RKGMR32uWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1484&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 시나리오는 SourceCommit의 api application repository의 develop 브랜치를 토대로 프로젝트의 root 디렉토리에 있는 deployment-dev.yaml 파일을 실행하게 구성하였습니다. 그로인해 kubernetes의 deployment 오브젝트를 통해 클러스터에 어플리케이션이 배포될 수 있도록 설정하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6dsaS/btsMiwRTCee/KXf4guj0d6Z2XtUjezwux1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6dsaS/btsMiwRTCee/KXf4guj0d6Z2XtUjezwux1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6dsaS/btsMiwRTCee/KXf4guj0d6Z2XtUjezwux1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6dsaS%2FbtsMiwRTCee%2FKXf4guj0d6Z2XtUjezwux1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1472&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. deployment-dev.yaml 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deployment-dev.yaml 파일이라고 설정했지만, Service까지 함께 작성해줬습니다. 지금보니 분리가 필요할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 아래의 deployment 파일이 실행되려면 선행조건이 필요합니다. cluster에 test 라는 이름의 namespace가 생성되어 있어야 하고, 이미지를 가져오기 위해 ncp의 ContainerRegistry에 접근해야하기 때문에 test namespace에 Secret을 생성해줘야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739434471957&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl --kubeconfig ~/config/ncpDevKubeconfig.yaml create -n test&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739434359636&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl --kubeconfig ~/config/ncpDevKubeconfig.yaml create secret docker-registry regcred -n test --docker-server=vjnibbzs.kr.private-ncr.ntruss.com --docker-username={iamKey} --docker-password={iamSecret}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739434158719&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: mingyu-api-deployment   # deployment의 이름
  namespace: test
  labels:
    app: mingyu-api-deployment  # deployment의 라벨
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mingyu-api-deployment # replicaset이 관리할 pod 라벨
  template:    # 하위에 pod 정보 설정
    metadata:
      labels:
        app: mingyu-api-deployment # pod의 라벨
    spec: # 컨테이너 설정
      containers:
        - name: mingyu-api
          image: vjnibbzs.kr.private-ncr.ntruss.com/mingyu-api-dev:latest
          command: [&quot;/bin/sh&quot;,&quot;-c&quot;,&quot;java -Dspring.profiles.active=dev -jar app.jar&quot;]
          imagePullPolicy: Always
          ports:
            - containerPort: 8080     # 애플리케이션이 동작하는 포트로 스프링 기본포트는 8080
              protocol: TCP
          envFrom:
            - configMapRef:
                name: mingyu-api-config-dev
      imagePullSecrets:
        - name: regcred

---
apiVersion: v1
kind: Service
metadata:
  name: mingyu-api-service
  namespace: test
spec:
  type: NodePort
  selector:
    app: mingyu-api-deployment
  ports:
    - name: http
      port: 80       # 클러스터 내부에서의 접근 포트
      protocol: TCP
      targetPort: 10080  # Pod에서 실행중인 포트&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 develop 브랜치를 origin으로 push하게되면 아래와 같이 pipeline이 성공한 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oyagl/btsMiILlBp6/B2Rfv6iCn0PsiqC0OYSAi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oyagl/btsMiILlBp6/B2Rfv6iCn0PsiqC0OYSAi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oyagl/btsMiILlBp6/B2Rfv6iCn0PsiqC0OYSAi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyagl%2FbtsMiILlBp6%2FB2Rfv6iCn0PsiqC0OYSAi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1472&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 dev-cluster의 dashboard에 접속해보면 아래와 같이 deployment, replicaset, pod가 정상적으로 생성된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6OlhQ/btsMijynz9P/AuWi2220mfL1RnPcL5Smx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6OlhQ/btsMijynz9P/AuWi2220mfL1RnPcL5Smx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6OlhQ/btsMijynz9P/AuWi2220mfL1RnPcL5Smx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6OlhQ%2FbtsMijynz9P%2FAuWi2220mfL1RnPcL5Smx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1476&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>k8s deployment</category>
      <category>k8s springboot</category>
      <category>kubernetees springboot</category>
      <category>kubernetes spring 배포</category>
      <category>nks deployment</category>
      <category>nks sourcepipeline</category>
      <category>nks spring application 배포</category>
      <category>nks 강좌</category>
      <category>nks 사용법</category>
      <category>쿠버네티스 스프링 배포</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/333</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-2-Spring-Boot-Application-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Feat-Ncp-SourcePipeLine#entry333comment</comments>
      <pubDate>Thu, 13 Feb 2025 17:22:32 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes Object 알아보기 2 &amp;ndash; StatefulSet, DaemonSet, Job, CronJob</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-Object-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-2-%E2%80%93-StatefulSet-DaemonSet-Job-CronJob</link>
      <description>&lt;h2 data-end=&quot;299&quot; data-start=&quot;294&quot; data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-end=&quot;469&quot; data-start=&quot;300&quot; data-ke-size=&quot;size16&quot;&gt;쿠버네티스에는 워크로드를 배포하고 관리하기 위해 다양한 오브젝트가 존재합니다. 그중에서도 일반적인 서비스(웹, API 등)를 배포할 때 주로 쓰이는 Deployment와는 달리 &lt;b&gt;StatefulSet&lt;/b&gt;, &lt;b&gt;DaemonSet&lt;/b&gt;, &lt;b&gt;Job&lt;/b&gt;, &lt;b&gt;CronJob&lt;/b&gt;은 각각 특수한 목적을 갖습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;706&quot; data-start=&quot;471&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;547&quot; data-start=&quot;471&quot;&gt;&lt;b&gt;StatefulSet&lt;/b&gt;: 상태ful(상태가 중요한) 애플리케이션을 위해, 안정적인 네트워크 ID 및 스토리지 관리 등을 제공&lt;/li&gt;
&lt;li data-end=&quot;597&quot; data-start=&quot;548&quot;&gt;&lt;b&gt;DaemonSet&lt;/b&gt;: 모든 노드마다 반드시 실행되어야 하는 워크로드(Pod)&lt;/li&gt;
&lt;li data-end=&quot;652&quot; data-start=&quot;598&quot;&gt;&lt;b&gt;Job&lt;/b&gt;: 특정 작업을 한 번(또는 정해진 횟수) 실행 후 종료해야 하는 배치성 작업&lt;/li&gt;
&lt;li data-end=&quot;706&quot; data-start=&quot;653&quot;&gt;&lt;b&gt;CronJob&lt;/b&gt;: 스케줄러 역할을 수행하며, 정해진 시간/주기에 맞춰 Job을 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;788&quot; data-start=&quot;708&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 위 4가지 오브젝트가 각각 어떤 역할을 하고, 어느 상황에서 사용되는지, 그리고 예시를 들어 좀 더 구체적으로 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;800&quot; data-start=&quot;795&quot; data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-end=&quot;820&quot; data-start=&quot;802&quot; data-ke-size=&quot;size23&quot;&gt;1. StatefulSet&lt;/h3&gt;
&lt;p data-end=&quot;842&quot; data-start=&quot;822&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;StatefulSet이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1034&quot; data-start=&quot;843&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;908&quot; data-start=&quot;843&quot;&gt;쿠버네티스에서 &lt;b&gt;상태가 필요한 애플리케이션&lt;/b&gt;(Stateful Application)을 위한 컨트롤러입니다.&lt;/li&gt;
&lt;li data-end=&quot;965&quot; data-start=&quot;909&quot;&gt;&lt;b&gt;Pod에 고유한 정체성(이름, 순번 등)과 퍼시스턴트 스토리지&lt;/b&gt;를 안정적으로 부여합니다.&lt;/li&gt;
&lt;li data-end=&quot;1034&quot; data-start=&quot;966&quot;&gt;Pod이 순차적으로 배포&amp;middot;종료되며, 각 Pod는 고정된 호스트네임(pod-0, pod-1 등)과 연결됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1054&quot; data-start=&quot;1036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1295&quot; data-start=&quot;1055&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1107&quot; data-start=&quot;1055&quot;&gt;DB, 메시지 큐, 캐시 서버 등 스토리지(데이터)가 중요한 애플리케이션에 적합합니다.&lt;/li&gt;
&lt;li data-end=&quot;1229&quot; data-start=&quot;1108&quot;&gt;Pod 간에 순서(ordinal index)가 필요하거나, 네트워크 ID가 변하지 않아야 하는 경우에 사용합니다. 예: Cassandra, Zookeeper, Elasticsearch 등 클러스터를 구성할 때.&lt;/li&gt;
&lt;li data-end=&quot;1295&quot; data-start=&quot;1230&quot;&gt;PVC(퍼시스턴트 볼륨 클레임)와 함께 사용하여 Pod별로 독립된 스토리지 볼륨을 영구적으로 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1305&quot; data-start=&quot;1297&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1542&quot; data-start=&quot;1306&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1379&quot; data-start=&quot;1306&quot;&gt;&lt;b&gt;MySQL&lt;/b&gt; 주요 데이터베이스를 StatefulSet으로 구성해 Pod가 재시작돼도 스토리지 연결이 유지되도록 보장.&lt;/li&gt;
&lt;li data-end=&quot;1468&quot; data-start=&quot;1380&quot;&gt;&lt;b&gt;Kafka&lt;/b&gt; 클러스터를 StatefulSet으로 구성해 broker-0, broker-1, broker-2 식으로 안정적 네트워크 ID를 제공.&lt;/li&gt;
&lt;li data-end=&quot;1542&quot; data-start=&quot;1469&quot;&gt;&lt;b&gt;Redis&lt;/b&gt; 같은 캐시 서버를 다중 노드 형태로 운영할 때, 노드 간 호스트네임과 스토리지를 명확히 고정하기 위해 사용.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;1547&quot; data-start=&quot;1544&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1565&quot; data-start=&quot;1549&quot; data-ke-size=&quot;size23&quot;&gt;2. DaemonSet&lt;/h3&gt;
&lt;p data-end=&quot;1585&quot; data-start=&quot;1567&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DaemonSet이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1743&quot; data-start=&quot;1586&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1664&quot; data-start=&quot;1586&quot;&gt;&lt;b&gt;클러스터의 모든 노드(또는 특정 조건을 만족하는 노드)에 반드시 1개의 Pod가 동작&lt;/b&gt;하도록 보장하는 쿠버네티스 오브젝트입니다.&lt;/li&gt;
&lt;li data-end=&quot;1743&quot; data-start=&quot;1665&quot;&gt;노드가 새롭게 추가되면, DaemonSet은 해당 노드에도 자동으로 Pod를 배포하고, 노드가 제거되면 자동으로 Pod를 정리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1763&quot; data-start=&quot;1745&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1943&quot; data-start=&quot;1764&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1827&quot; data-start=&quot;1764&quot;&gt;노드 단위로 반드시 실행해야 하는 시스템 에이전트나 로그 수집, 모니터링 에이전트 등에서 자주 사용합니다.&lt;/li&gt;
&lt;li data-end=&quot;1883&quot; data-start=&quot;1828&quot;&gt;컨테이너 런타임 수준의 보안 모듈 또는 노드 수준에서 필요한 네트워크 에이전트가 필요할 때.&lt;/li&gt;
&lt;li data-end=&quot;1943&quot; data-start=&quot;1884&quot;&gt;노드가 추가&amp;middot;삭제될 때마다 자동으로 스케일되며, 노드별로 1개씩 구동되어야 하므로 동작이 간단해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1953&quot; data-start=&quot;1945&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2245&quot; data-start=&quot;1954&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2064&quot; data-start=&quot;1954&quot;&gt;&lt;b&gt;Fluentd&lt;/b&gt;, &lt;b&gt;Filebeat&lt;/b&gt; 등 로그 수집 에이전트를 모든 노드에 설치하여 Pod 로그를 중앙 수집 시스템(ElasticSearch, Splunk 등)으로 보내야 할 때.&lt;/li&gt;
&lt;li data-end=&quot;2168&quot; data-start=&quot;2065&quot;&gt;&lt;b&gt;Prometheus Node Exporter&lt;/b&gt;, &lt;b&gt;Datadog Agent&lt;/b&gt; 등 서버별 CPU, 메모리, 디스크 사용률 등을 수집하는 모니터링 에이전트가 필요한 경우.&lt;/li&gt;
&lt;li data-end=&quot;2245&quot; data-start=&quot;2169&quot;&gt;**CNI 플러그인(네트워크 플러그인)**이 자체적으로 DaemonSet으로 구성되어 노드마다 필수 Pod로 배포되는 경우도 많음.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;2250&quot; data-start=&quot;2247&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2262&quot; data-start=&quot;2252&quot; data-ke-size=&quot;size23&quot;&gt;3. Job&lt;/h3&gt;
&lt;p data-end=&quot;2276&quot; data-start=&quot;2264&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Job이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2483&quot; data-start=&quot;2277&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2338&quot; data-start=&quot;2277&quot;&gt;&lt;b&gt;한 번 실행 후 완료되어야 하는 작업&lt;/b&gt;(배치 작업 등)을 수행하기 위한 쿠버네티스 오브젝트입니다.&lt;/li&gt;
&lt;li data-end=&quot;2396&quot; data-start=&quot;2339&quot;&gt;Pod를 하나 이상 실행하여 작업을 완료하면 Job 오브젝트가 성공 상태를 기록하고 종료합니다.&lt;/li&gt;
&lt;li data-end=&quot;2483&quot; data-start=&quot;2397&quot;&gt;재시도가 필요한 경우(backoffLimit)나 여러 Pod 병렬 실행(parallelism) 등을 설정해 유연하게 작업을 구성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2503&quot; data-start=&quot;2485&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2686&quot; data-start=&quot;2504&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2562&quot; data-start=&quot;2504&quot;&gt;DB 마이그레이션, 일회성 스크립트 실행, 대량 데이터 처리를 위한 일회성 작업 등에 사용합니다.&lt;/li&gt;
&lt;li data-end=&quot;2617&quot; data-start=&quot;2563&quot;&gt;특정 함수나 로직을 실행하고 그 결과를 저장하면 끝나는 파이프라인의 한 단계로 사용 가능.&lt;/li&gt;
&lt;li data-end=&quot;2686&quot; data-start=&quot;2618&quot;&gt;장시간 소모되는 작업이나 실패 가능성이 높은 작업은 재시도(backoffLimit)를 통해 자동 복구를 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2696&quot; data-start=&quot;2688&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2945&quot; data-start=&quot;2697&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2765&quot; data-start=&quot;2697&quot;&gt;&lt;b&gt;DB 마이그레이션&lt;/b&gt; 스크립트를 Job으로 작성해 애플리케이션이 배포되기 전/후에 필요한 스키마 변경을 수행.&lt;/li&gt;
&lt;li data-end=&quot;2861&quot; data-start=&quot;2766&quot;&gt;&lt;b&gt;ETL(Extract, Transform, Load)&lt;/b&gt; 처리를 위해 일괄 데이터 변환 작업을 Job으로 실행하고, 완료 후 결과를 S3나 외부 DB에 저장.&lt;/li&gt;
&lt;li data-end=&quot;2945&quot; data-start=&quot;2862&quot;&gt;결산 작업처럼 &lt;b&gt;매달 1일에 한 번&lt;/b&gt; 실행 후 종료되는 배치 작업(단, 정확히 &amp;ldquo;매달 1일&amp;rdquo; 자동 실행은 CronJob을 사용할 수 있음).&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;2950&quot; data-start=&quot;2947&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2966&quot; data-start=&quot;2952&quot; data-ke-size=&quot;size23&quot;&gt;4. CronJob&lt;/h3&gt;
&lt;p data-end=&quot;2984&quot; data-start=&quot;2968&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CronJob이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3137&quot; data-start=&quot;2985&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3072&quot; data-start=&quot;2985&quot;&gt;&lt;b&gt;스케줄러&lt;/b&gt; 역할을 하는 쿠버네티스 오브젝트로, 특정 Cron 표현식(schedule)에 따라 &lt;b&gt;Job을 주기적으로 생성&lt;/b&gt;하여 실행합니다.&lt;/li&gt;
&lt;li data-end=&quot;3137&quot; data-start=&quot;3073&quot;&gt;Linux의 cron과 유사한 방식으로 동작하며, 시간을 지정하면 지정된 시간에 Job 리소스를 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3157&quot; data-start=&quot;3139&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3352&quot; data-start=&quot;3158&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3212&quot; data-start=&quot;3158&quot;&gt;매일, 매주, 매월 특정 시간에 반복해서 실행해야 하는 배치/백그라운드 작업을 관리할 때.&lt;/li&gt;
&lt;li data-end=&quot;3278&quot; data-start=&quot;3213&quot;&gt;로그 정리, 백업, 레포트 생성, 모니터링 지표 수집 등 &lt;b&gt;주기적으로 필요한 작업&lt;/b&gt;을 자동화하고 싶을 때.&lt;/li&gt;
&lt;li data-end=&quot;3352&quot; data-start=&quot;3279&quot;&gt;스케줄이 변경되거나, 실패 시 재시도가 필요할 경우 등, &amp;ldquo;특정 시점에 Job을 실행한다&amp;rdquo;는 요구 사항이 있는 경우 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3362&quot; data-start=&quot;3354&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3536&quot; data-start=&quot;3363&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3415&quot; data-start=&quot;3363&quot;&gt;&lt;b&gt;매일 자정에 DB 백업&lt;/b&gt;을 수행하는 CronJob 설정(0 0 * * *).&lt;/li&gt;
&lt;li data-end=&quot;3482&quot; data-start=&quot;3416&quot;&gt;&lt;b&gt;매주 월요일 오전 9시에 데이터 처리&lt;/b&gt; Job을 실행해 리포트를 생성하고, 그 결과를 사내 서버에 전달.&lt;/li&gt;
&lt;li data-end=&quot;3536&quot; data-start=&quot;3483&quot;&gt;특정 API 엔드포인트를 주기적으로 호출해 데이터 갱신 또는 캐시 무효화를 수행하는 경우.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;3541&quot; data-start=&quot;3538&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3548&quot; data-start=&quot;3543&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-end=&quot;3646&quot; data-start=&quot;3550&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;b&gt;StatefulSet, DaemonSet, Job, CronJob&lt;/b&gt;이라는 네 가지 쿠버네티스 오브젝트에 대해 알아봤습니다. 각각의 특징을 요약해보면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4003&quot; data-start=&quot;3648&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3753&quot; data-start=&quot;3648&quot;&gt;&lt;b&gt;StatefulSet&lt;/b&gt;: 상태ful 애플리케이션(특히 데이터베이스나 분산 클러스터)이 안정적으로 동작하도록, 고유 네트워크 식별자와 퍼시스턴트 스토리지 관리 기능을 제공합니다.&lt;/li&gt;
&lt;li data-end=&quot;3866&quot; data-start=&quot;3754&quot;&gt;&lt;b&gt;DaemonSet&lt;/b&gt;: 모든 노드(또는 특정 조건에 맞는 노드)에 공통 Pod가 반드시 배포되도록 하는 컨트롤러로, 로그 수집/모니터링/보안 에이전트 등의 시스템 레벨 워크로드에 적합합니다.&lt;/li&gt;
&lt;li data-end=&quot;3935&quot; data-start=&quot;3867&quot;&gt;&lt;b&gt;Job&lt;/b&gt;: 일회성(혹은 한정된 횟수) 작업을 수행하고, 완료 후 종료되어야 하는 배치 작업을 위해 사용됩니다.&lt;/li&gt;
&lt;li data-end=&quot;4003&quot; data-start=&quot;3936&quot;&gt;&lt;b&gt;CronJob&lt;/b&gt;: Cron 스케줄에 따라 Job을 주기적으로 실행해야 할 때, 스케줄러로서 유용하게 활용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4199&quot; data-start=&quot;4005&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발자 겸 시스템 엔지니어 관점에서, 이러한 특수 오브젝트들은 쿠버네티스 활용 범위를 크게 넓혀 줍니다. 꼭 웹 서비스나 API 서버만 배포하는 게 아니라, 데이터베이스와 같은 상태ful 서비스, 노드별로 필요한 데몬 작업, 일정 주기에 자동으로 수행해야 하는 배치 작업 등 다양한 요구사항을 쿠버네티스 안에서 통합 관리할 수 있게 됩니다.&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>daemonset이란</category>
      <category>k8s cronjob</category>
      <category>k8s daemonset</category>
      <category>k8s job</category>
      <category>k8s statefulset</category>
      <category>kubernetes job cronjob</category>
      <category>kubernetes object</category>
      <category>kubernetes statefulset</category>
      <category>statefulset이란</category>
      <category>쿠버네티스 오브젝트</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/332</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-Object-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-2-%E2%80%93-StatefulSet-DaemonSet-Job-CronJob#entry332comment</comments>
      <pubDate>Thu, 13 Feb 2025 16:27:28 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes Object 알아보기 1 - Pod, Namespace, Service, Deployment, ConfigMap, Secret, Ingress</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-Object-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스에는 pod, namespace, service, deployment, configMap, secret, ingress 등 엄청나게 많은 Object들이 존재합니다. Kubernetes를 사용하려면 꼭 알아야 할 Object들에 대해서 상세히 알아볼 겸 본 포스팅을 작성합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;152&quot; data-start=&quot;147&quot; data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-end=&quot;164&quot; data-start=&quot;154&quot; data-ke-size=&quot;size23&quot;&gt;1. Pod&lt;/h3&gt;
&lt;p data-end=&quot;176&quot; data-start=&quot;165&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pod란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;330&quot; data-start=&quot;177&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;210&quot; data-start=&quot;177&quot;&gt;쿠버네티스에서 가장 작은 배포 단위(Unit)입니다.&lt;/li&gt;
&lt;li data-end=&quot;249&quot; data-start=&quot;211&quot;&gt;하나 이상의 컨테이너가 모여서 동작하는 논리적인 집합체입니다.&lt;/li&gt;
&lt;li data-end=&quot;330&quot; data-start=&quot;250&quot;&gt;일반적으로 하나의 Pod에는 하나의 주 컨테이너가 들어가지만, 사이드카 컨테이너(예: 로그 수집, 프록시 등)를 함께 두는 경우도 많습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;350&quot; data-start=&quot;332&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;540&quot; data-start=&quot;351&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;392&quot; data-start=&quot;351&quot;&gt;실제 애플리케이션을 컨테이너 단위로 배포하고 실행할 때 사용합니다.&lt;/li&gt;
&lt;li data-end=&quot;468&quot; data-start=&quot;393&quot;&gt;로그 수집 에이전트나 모니터링 에이전트를 같이 배포해야 하는 경우, 사이드카 컨테이너를 추가해 하나의 Pod로 묶어 관리합니다.&lt;/li&gt;
&lt;li data-end=&quot;540&quot; data-start=&quot;469&quot;&gt;Pod 자체로는 잘 사용하지 않고, 보통은 Deployment나 ReplicaSet 등의 상위 오브젝트로 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;542&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;686&quot; data-start=&quot;551&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;638&quot; data-start=&quot;551&quot;&gt;nginx 웹 서버를 실행하는 컨테이너 1개와 로그 수집 에이전트(Fluentd 등)를 넣은 사이드카 컨테이너를 하나의 Pod로 구성하여 배포.&lt;/li&gt;
&lt;li data-end=&quot;686&quot; data-start=&quot;639&quot;&gt;DB 마이그레이션 작업이 필요한 컨테이너를 임시로 Pod로 띄워 사용하는 경우.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;691&quot; data-start=&quot;688&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;709&quot; data-start=&quot;693&quot; data-ke-size=&quot;size23&quot;&gt;2. Namespace&lt;/h3&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;710&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Namespace란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;832&quot; data-start=&quot;728&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;782&quot; data-start=&quot;728&quot;&gt;하나의 쿠버네티스 클러스터에서 리소스(오브젝트)들을 논리적으로 구분해주는 가상 공간입니다.&lt;/li&gt;
&lt;li data-end=&quot;832&quot; data-start=&quot;783&quot;&gt;팀 간, 개발&amp;middot;테스트&amp;middot;프로덕션 환경 간 자원을 분리하여 관리하기 위해 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;852&quot; data-start=&quot;834&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1018&quot; data-start=&quot;853&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;934&quot; data-start=&quot;853&quot;&gt;여러 환경을 한 클러스터에서 분리해 운용할 때, 예: dev, test, stage, prod 네임스페이스를 분리하여 관리.&lt;/li&gt;
&lt;li data-end=&quot;1018&quot; data-start=&quot;935&quot;&gt;서로 다른 프로젝트 팀이 하나의 쿠버네티스 클러스터를 공유할 때, 각 팀의 자원을 안전하게 분리하여 충돌을 방지하고 리소스 제한을 적용할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;1020&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1166&quot; data-start=&quot;1029&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1096&quot; data-start=&quot;1029&quot;&gt;개발용 Namespace에서 자유롭게 테스트를 진행하고, 프로덕션용 Namespace에서만 실제 서비스를 운영.&lt;/li&gt;
&lt;li data-end=&quot;1166&quot; data-start=&quot;1097&quot;&gt;다수의 프로젝트가 동시에 진행되는 대규모 조직에서, 프로젝트별 네임스페이스를 만들어 접근 권한과 리소스 쿼터를 설정함.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;1171&quot; data-start=&quot;1168&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1187&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size23&quot;&gt;3. Service&lt;/h3&gt;
&lt;p data-end=&quot;1203&quot; data-start=&quot;1188&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1380&quot; data-start=&quot;1204&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1259&quot; data-start=&quot;1204&quot;&gt;쿠버네티스 클러스터 내부 또는 외부에서 Pod(들)에 접근하기 위한 네트워킹 오브젝트입니다.&lt;/li&gt;
&lt;li data-end=&quot;1318&quot; data-start=&quot;1260&quot;&gt;Pod가 동적으로 생성&amp;middot;종료되어도 변하지 않는 고정된 엔드포인트(IP, DNS 등)를 제공합니다.&lt;/li&gt;
&lt;li data-end=&quot;1380&quot; data-start=&quot;1319&quot;&gt;대표적으로 ClusterIP, NodePort, LoadBalancer 타입을 많이 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1400&quot; data-start=&quot;1382&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1574&quot; data-start=&quot;1401&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1457&quot; data-start=&quot;1401&quot;&gt;서비스 내부 컴포넌트들 간 통신 시, Pod의 IP가 바뀌어도 접근 경로가 유지되어야 할 때.&lt;/li&gt;
&lt;li data-end=&quot;1529&quot; data-start=&quot;1458&quot;&gt;외부에서 HTTP/HTTPS 요청을 받아 특정 Pod로 분산시키고 싶을 때(주로 LoadBalancer 타입 사용).&lt;/li&gt;
&lt;li data-end=&quot;1574&quot; data-start=&quot;1530&quot;&gt;NodePort로 각 노드의 특정 포트를 열어 간단히 외부와 통신할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1584&quot; data-start=&quot;1576&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1772&quot; data-start=&quot;1585&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1645&quot; data-start=&quot;1585&quot;&gt;ClusterIP 타입을 이용해 내부에서만 접근 가능한 DB 서비스나 내부 API 서비스 구성.&lt;/li&gt;
&lt;li data-end=&quot;1724&quot; data-start=&quot;1646&quot;&gt;ALB, NLB와 같은 클라우드 로드밸런서와 연동을 위해 LoadBalancer 타입의 서비스를 생성해 웹 트래픽 처리를 담당.&lt;/li&gt;
&lt;li data-end=&quot;1772&quot; data-start=&quot;1725&quot;&gt;사설 환경에서 외부 로드밸런서 없이 NodePort를 활용해 임시 서비스 오픈.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;1777&quot; data-start=&quot;1774&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1796&quot; data-start=&quot;1779&quot; data-ke-size=&quot;size23&quot;&gt;4. Deployment&lt;/h3&gt;
&lt;p data-end=&quot;1815&quot; data-start=&quot;1797&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Deployment란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2016&quot; data-start=&quot;1816&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1867&quot; data-start=&quot;1816&quot;&gt;Pod와 ReplicaSet을 더욱 편리하게 관리하기 위한 쿠버네티스 오브젝트입니다.&lt;/li&gt;
&lt;li data-end=&quot;1970&quot; data-start=&quot;1868&quot;&gt;선언형(Declarative)으로 원하는 스펙(컨테이너 이미지, Pod 개수 등)을 정의하고, Deployment가 ReplicaSet을 통해 Pod를 일관성 있게 유지합니다.&lt;/li&gt;
&lt;li data-end=&quot;2016&quot; data-start=&quot;1971&quot;&gt;롤링 업데이트와 롤백 등, 애플리케이션 배포/업데이트 과정 전반을 책임집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2036&quot; data-start=&quot;2018&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2189&quot; data-start=&quot;2037&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2067&quot; data-start=&quot;2037&quot;&gt;스케일링(확장/축소)을 간편하게 하고 싶을 때.&lt;/li&gt;
&lt;li data-end=&quot;2138&quot; data-start=&quot;2068&quot;&gt;애플리케이션을 새 버전으로 무중단 업데이트(롤링 업데이트)하거나, 문제 발생 시 이전 버전으로 쉽게 되돌릴(롤백) 때.&lt;/li&gt;
&lt;li data-end=&quot;2189&quot; data-start=&quot;2139&quot;&gt;지속적으로 업데이트가 필요한 웹/백엔드 애플리케이션을 구성할 때 일반적으로 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2199&quot; data-start=&quot;2191&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2384&quot; data-start=&quot;2200&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2286&quot; data-start=&quot;2200&quot;&gt;Java Spring Boot 웹 애플리케이션의 Pod 3개를 운영하려고 할 때, replicas: 3으로 설정하여 Deployment 생성.&lt;/li&gt;
&lt;li data-end=&quot;2335&quot; data-start=&quot;2287&quot;&gt;최신 버전이 잘못 올라갔다면, 배포 이력을 이용해 간단히 이전 버전으로 롤백.&lt;/li&gt;
&lt;li data-end=&quot;2384&quot; data-start=&quot;2336&quot;&gt;트래픽 증가로 인한 확장 시, replicas 수를 늘려 빠르게 스케일 아웃.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;2389&quot; data-start=&quot;2386&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2407&quot; data-start=&quot;2391&quot; data-ke-size=&quot;size23&quot;&gt;5. ConfigMap&lt;/h3&gt;
&lt;p data-end=&quot;2426&quot; data-start=&quot;2408&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ConfigMap이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2546&quot; data-start=&quot;2427&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2485&quot; data-start=&quot;2427&quot;&gt;애플리케이션에 필요한 환경 설정값(환경 변수, 설정 파일 등)을 분리하여 관리하는 오브젝트입니다.&lt;/li&gt;
&lt;li data-end=&quot;2546&quot; data-start=&quot;2486&quot;&gt;Pod 스펙과 분리해서 환경 설정을 관리함으로써, 이미지 재빌드 없이 설정만 교체할 수 있도록 해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2566&quot; data-start=&quot;2548&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2784&quot; data-start=&quot;2567&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2632&quot; data-start=&quot;2567&quot;&gt;개발&amp;middot;테스트&amp;middot;프로덕션 환경별로 다른 설정을 사용해야 할 때, 이미지는 동일하게 두고 ConfigMap만 교체.&lt;/li&gt;
&lt;li data-end=&quot;2717&quot; data-start=&quot;2633&quot;&gt;애플리케이션 설정 파일(예: application.properties)을 쿠버네티스 설정으로 관리하면서, 지속적인 설정 변경이 필요한 경우.&lt;/li&gt;
&lt;li data-end=&quot;2784&quot; data-start=&quot;2718&quot;&gt;외부 서비스 Endpoint, 포트 번호, 기타 플래그 등 자주 바뀌는 설정 요소를 코드에서 분리하고 싶을 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2794&quot; data-start=&quot;2786&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3012&quot; data-start=&quot;2795&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2872&quot; data-start=&quot;2795&quot;&gt;Spring Boot 앱에서 DB 연결 정보를 application.properties 파일 대신 ConfigMap으로 관리.&lt;/li&gt;
&lt;li data-end=&quot;2942&quot; data-start=&quot;2873&quot;&gt;Nginx의 설정 파일(nginx.conf)을 ConfigMap에 저장 후, 여러 Pod에서 공통 설정을 공유.&lt;/li&gt;
&lt;li data-end=&quot;3012&quot; data-start=&quot;2943&quot;&gt;다양한 API 엔드포인트 주소를 환경별로 분리하여 ConfigMap에 담고, Deployment에서는 해당 값을 참조.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;3017&quot; data-start=&quot;3014&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;3032&quot; data-start=&quot;3019&quot; data-ke-size=&quot;size23&quot;&gt;6. Secret&lt;/h3&gt;
&lt;p data-end=&quot;3048&quot; data-start=&quot;3033&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Secret이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3166&quot; data-start=&quot;3049&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3103&quot; data-start=&quot;3049&quot;&gt;ConfigMap과 유사한 형태지만, 민감한 정보를 안전하게 보관하기 위한 오브젝트입니다.&lt;/li&gt;
&lt;li data-end=&quot;3166&quot; data-start=&quot;3104&quot;&gt;쿠버네티스 내부적으로 Base64 인코딩을 통해 저장되며, RBAC 설정 등을 통해 접근 제어가 이뤄집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3186&quot; data-start=&quot;3168&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3359&quot; data-start=&quot;3187&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3253&quot; data-start=&quot;3187&quot;&gt;데이터베이스 비밀번호, API 키, 인증서, TLS 키 등 노출되면 안 되는 민감 정보를 관리할 때 사용합니다.&lt;/li&gt;
&lt;li data-end=&quot;3306&quot; data-start=&quot;3254&quot;&gt;Pod가 실행되면서 해당 Secret을 참조해 환경 변수나 볼륨 등으로 마운트해 사용.&lt;/li&gt;
&lt;li data-end=&quot;3359&quot; data-start=&quot;3307&quot;&gt;ConfigMap을 사용하기에는 민감도가 높은 경우 반드시 Secret 사용을 권장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3369&quot; data-start=&quot;3361&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3546&quot; data-start=&quot;3370&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3432&quot; data-start=&quot;3370&quot;&gt;외부 서비스 연동을 위한 API 토큰을 Secret으로 관리하고, 필요한 Pod에서 환경 변수로 주입.&lt;/li&gt;
&lt;li data-end=&quot;3500&quot; data-start=&quot;3433&quot;&gt;TLS 인증서와 키를 Secret으로 관리하여 Ingress Controller 등에서 HTTPS 설정에 사용.&lt;/li&gt;
&lt;li data-end=&quot;3546&quot; data-start=&quot;3501&quot;&gt;DB 접속 정보를 Secret에 담아 Pod 실행 시 참조해 보안성을 강화.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;3551&quot; data-start=&quot;3548&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;3567&quot; data-start=&quot;3553&quot; data-ke-size=&quot;size23&quot;&gt;7. Ingress&lt;/h3&gt;
&lt;p data-end=&quot;3583&quot; data-start=&quot;3568&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ingress란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3814&quot; data-start=&quot;3584&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3649&quot; data-start=&quot;3584&quot;&gt;쿠버네티스 클러스터 외부의 HTTP/HTTPS 트래픽을 내부 서비스(Pod)로 라우팅하기 위한 오브젝트입니다.&lt;/li&gt;
&lt;li data-end=&quot;3717&quot; data-start=&quot;3650&quot;&gt;L7(애플리케이션 레벨)에서 트래픽을 분산하고, 도메인 기반 라우팅, SSL/TLS 종료 등의 기능을 제공합니다.&lt;/li&gt;
&lt;li data-end=&quot;3814&quot; data-start=&quot;3718&quot;&gt;Ingress Controller(Nginx, HAProxy, Istio Gateway 등)가 실제 트래픽 처리를 맡으며, Ingress는 그 라우팅 규칙을 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3834&quot; data-start=&quot;3816&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제/어떻게 사용하나?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4043&quot; data-start=&quot;3835&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3901&quot; data-start=&quot;3835&quot;&gt;여러 서비스가 각각 다른 경로나 서브도메인을 통해서 하나의 로드밸런서 IP/도메인을 공유하여 외부에 노출될 때.&lt;/li&gt;
&lt;li data-end=&quot;3964&quot; data-start=&quot;3902&quot;&gt;HTTPS 종단(TLS Termination)을 수행하고, 내부 트래픽은 HTTP로 라우팅하고 싶을 때.&lt;/li&gt;
&lt;li data-end=&quot;4043&quot; data-start=&quot;3965&quot;&gt;경로(/api, /images 등)에 따라 서로 다른 서비스로 트래픽을 분산하여 API Gateways 역할을 간단히 대체할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4053&quot; data-start=&quot;4045&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;4342&quot; data-start=&quot;4054&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;4160&quot; data-start=&quot;4054&quot;&gt;example.com 도메인을 통해 여러 백엔드 서비스를 연결: /api -&amp;gt; API 서비스, /auth -&amp;gt; 인증 서비스, /static -&amp;gt; 정적 파일 서비스 등.&lt;/li&gt;
&lt;li data-end=&quot;4248&quot; data-start=&quot;4161&quot;&gt;클라우드 환경에서 Nginx Ingress Controller 설치 후, SSL 인증서를 Secret으로 관리하여 HTTPS(443) 트래픽 처리.&lt;/li&gt;
&lt;li data-end=&quot;4342&quot; data-start=&quot;4249&quot;&gt;Istio, Linkerd 등 Service Mesh 환경에서 Gateway(혹은 Ingress Controller)와 함께 내부 마이크로서비스로 트래픽 라우팅.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;4347&quot; data-start=&quot;4344&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;4354&quot; data-start=&quot;4349&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-end=&quot;4490&quot; data-start=&quot;4356&quot; data-ke-size=&quot;size16&quot;&gt;쿠버네티스에는 정말 다양한 오브젝트들이 있고, 이 포스팅에서 다룬 Pod, Namespace, Service, Deployment, ConfigMap, Secret, Ingress는 가장 기초적이면서도 중요하게 다뤄지는 오브젝트들입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4977&quot; data-start=&quot;4492&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4561&quot; data-start=&quot;4492&quot;&gt;&lt;b&gt;Pod&lt;/b&gt;: 하나 이상의 컨테이너가 동작하는 가장 작은 배포 단위로, 실제 실행 환경을 구성하는 기본 요소입니다.&lt;/li&gt;
&lt;li data-end=&quot;4630&quot; data-start=&quot;4562&quot;&gt;&lt;b&gt;Namespace&lt;/b&gt;: 프로젝트나 환경을 논리적으로 구분할 수 있게 해주어 대규모 클러스터 운영 시 유용합니다.&lt;/li&gt;
&lt;li data-end=&quot;4701&quot; data-start=&quot;4631&quot;&gt;&lt;b&gt;Service&lt;/b&gt;: 동적으로 생성&amp;middot;제거되는 Pod를 안정적으로 접근할 수 있는 네트워크 엔드포인트를 제공해 줍니다.&lt;/li&gt;
&lt;li data-end=&quot;4795&quot; data-start=&quot;4702&quot;&gt;&lt;b&gt;Deployment&lt;/b&gt;: Pod와 ReplicaSet을 선언형으로 관리하여 롤링 업데이트, 롤백, 스케일링 등 중요한 배포 관련 기능을 간편하게 제공합니다.&lt;/li&gt;
&lt;li data-end=&quot;4858&quot; data-start=&quot;4796&quot;&gt;&lt;b&gt;ConfigMap&lt;/b&gt;: 환경 설정 파일과 같은 비민감한 정보를 이미지와 분리하여 관리할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;4917&quot; data-start=&quot;4859&quot;&gt;&lt;b&gt;Secret&lt;/b&gt;: 암호나 토큰, 인증서처럼 민감한 정보를 안전하게 관리하여 보안성을 높입니다.&lt;/li&gt;
&lt;li data-end=&quot;4977&quot; data-start=&quot;4918&quot;&gt;&lt;b&gt;Ingress&lt;/b&gt;: 외부 트래픽을 내부 서비스로 라우팅하기 위한 L7 레벨의 설정을 정의합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;5402&quot; data-start=&quot;5253&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;5402&quot; data-start=&quot;5253&quot; data-ke-size=&quot;size16&quot;&gt;이상으로 쿠버네티스의 주요 오브젝트에 대해 알아보았습니다. 각 오브젝트가 어떤 역할을 맡고, 언제 어떻게 사용하는지 이해하면, 더욱 체계적이고 유연한 클라우드 네이티브 인프라 운영이 가능해집니다. 향후에는 여기서 소개한 기본 오브젝트들을 기반으로 StatefulSet, DaemonSet, Job, CronJob 등 더 다양한 리소스도 살펴보고, Helm, ArgoCD 등의 배포 자동화 툴과 연계해 확장하는 포스팅도 진행해보도록 하겠습니다.&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>ingress란</category>
      <category>k8s configmap이란</category>
      <category>k8s namespace 설명</category>
      <category>k8s object</category>
      <category>k8s object 설명</category>
      <category>k8s secret이란</category>
      <category>k8s 오브젝트</category>
      <category>kubernetes</category>
      <category>kubernetes 오브젝트</category>
      <category>kubernetes 오브젝트 설명</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/331</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-Object-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0#entry331comment</comments>
      <pubDate>Thu, 13 Feb 2025 15:27:02 +0900</pubDate>
    </item>
    <item>
      <title>Docker을 이용해 SpringBoot Application 빌드하기</title>
      <link>https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot Framwork 기반의 Application을 build하는 방법은 많이 있습니다. 그 중 Docker를 활용하여 빌드 및 이미지화 하는 방법을 포스팅하려 합니다. 본 포스팅에서는 gradle build tool을 활용합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. JDK , Spring Boot 버전 및 빌드 툴 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application에서 사용하는 java 및 springboot 버전을 확인하려면 build.gradle 파일에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4Iu7A/btsMh5s0f3e/s7TogE94iTr9JgUCSS7de1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4Iu7A/btsMh5s0f3e/s7TogE94iTr9JgUCSS7de1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4Iu7A/btsMh5s0f3e/s7TogE94iTr9JgUCSS7de1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4Iu7A%2FbtsMh5s0f3e%2Fs7TogE94iTr9JgUCSS7de1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1032&quot; height=&quot;364&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. docker hub에서 jdk 이미지 찾기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션을 컨테이너 이미지로 만들때에 중요한건 아무래도 경량화라고 생각합니다. 때문에 저는 jdk 버전에 맞는 slim(경량화된 버전) 이미지를 활용하여 빌드를 시켜줄 예정입니다. Docker Hub에 접속하여 본인이 사용하는 jdk를 검색하면 그에 맞는 이미지를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1739420915320&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;https://hub.docker.com/layers/library/openjdk/17-slim/images/sha256-779635c0c3d23cc8dbab2d8c1ee4cf2a9202e198dfc8f4c0b279824d9b8e0f22&quot; data-og-description=&quot;&quot; data-og-host=&quot;hub.docker.com&quot; data-og-source-url=&quot;https://hub.docker.com/layers/library/openjdk/17-slim/images/sha256-779635c0c3d23cc8dbab2d8c1ee4cf2a9202e198dfc8f4c0b279824d9b8e0f22&quot; data-og-url=&quot;https://hub.docker.com/layers/library/openjdk/17-slim/images/sha256-779635c0c3d23cc8dbab2d8c1ee4cf2a9202e198dfc8f4c0b279824d9b8e0f22&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ONGN6/hyYap9cvPU/DR3b2A7Yh5JEe5F5vE5RLK/img.png?width=3372&amp;amp;height=1896&amp;amp;face=0_0_3372_1896&quot;&gt;&lt;a href=&quot;https://hub.docker.com/layers/library/openjdk/17-slim/images/sha256-779635c0c3d23cc8dbab2d8c1ee4cf2a9202e198dfc8f4c0b279824d9b8e0f22&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hub.docker.com/layers/library/openjdk/17-slim/images/sha256-779635c0c3d23cc8dbab2d8c1ee4cf2a9202e198dfc8f4c0b279824d9b8e0f22&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ONGN6/hyYap9cvPU/DR3b2A7Yh5JEe5F5vE5RLK/img.png?width=3372&amp;amp;height=1896&amp;amp;face=0_0_3372_1896');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;https://hub.docker.com/layers/library/openjdk/17-slim/images/sha256-779635c0c3d23cc8dbab2d8c1ee4cf2a9202e198dfc8f4c0b279824d9b8e0f22&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hub.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 openjdk를 사용할 예정이고, 그에 맞게 openjdk:17-slim 이미지를 빌드 및 런타임 이미지로 활용할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/osdTy/btsMgsDhe6s/BCNqfhU4rspORU0c1rqUI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/osdTy/btsMgsDhe6s/BCNqfhU4rspORU0c1rqUI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/osdTy/btsMgsDhe6s/BCNqfhU4rspORU0c1rqUI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FosdTy%2FbtsMgsDhe6s%2FBCNqfhU4rspORU0c1rqUI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1918&quot; height=&quot;860&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.Dockerfile 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 도커파일을 작성할 때 보통 Build Stage와 Runtime Stage를 나눠서 작성하는데, 그 이유는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소화된&amp;nbsp;이미지&amp;nbsp;크기:&lt;br /&gt;빌드&amp;nbsp;단계에서&amp;nbsp;필요한&amp;nbsp;도구(예:&amp;nbsp;Gradle,&amp;nbsp;소스&amp;nbsp;코드,&amp;nbsp;빌드&amp;nbsp;스크립트&amp;nbsp;등)는&amp;nbsp;최종&amp;nbsp;런타임&amp;nbsp;이미지에&amp;nbsp;포함되지&amp;nbsp;않으므로,&amp;nbsp;불필요한&amp;nbsp;파일과&amp;nbsp;라이브러리가&amp;nbsp;제거되어&amp;nbsp;이미지&amp;nbsp;크기를&amp;nbsp;크게&amp;nbsp;줄일&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;보안&amp;nbsp;강화:&lt;br /&gt;빌드&amp;nbsp;도구나&amp;nbsp;디버깅에&amp;nbsp;사용되는&amp;nbsp;툴들이&amp;nbsp;런타임&amp;nbsp;이미지에&amp;nbsp;포함되지&amp;nbsp;않으므로,&amp;nbsp;공격&amp;nbsp;표면이&amp;nbsp;줄어들어&amp;nbsp;보안&amp;nbsp;측면에서&amp;nbsp;이점이&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;환경&amp;nbsp;분리:&lt;br /&gt;빌드&amp;nbsp;환경과&amp;nbsp;실행&amp;nbsp;환경을&amp;nbsp;명확히&amp;nbsp;구분함으로써,&amp;nbsp;빌드에&amp;nbsp;필요한&amp;nbsp;의존성은&amp;nbsp;빌드&amp;nbsp;스테이지에서만&amp;nbsp;관리되고,&amp;nbsp;런타임&amp;nbsp;스테이지는&amp;nbsp;오로지&amp;nbsp;실행에&amp;nbsp;필요한&amp;nbsp;요소들만&amp;nbsp;포함하게&amp;nbsp;됩니다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;유지보수가&amp;nbsp;용이해집니다.&lt;br /&gt;&lt;br /&gt;캐싱&amp;nbsp;활용:&lt;br /&gt;변경되지&amp;nbsp;않는&amp;nbsp;빌드&amp;nbsp;단계의&amp;nbsp;캐시를&amp;nbsp;효과적으로&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;있어,&amp;nbsp;이후&amp;nbsp;빌드&amp;nbsp;시&amp;nbsp;전체&amp;nbsp;프로세스의&amp;nbsp;속도를&amp;nbsp;개선할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile&amp;nbsp;복잡도&amp;nbsp;증가:&lt;br /&gt;멀티&amp;nbsp;스테이지&amp;nbsp;빌드는&amp;nbsp;Dockerfile&amp;nbsp;작성&amp;nbsp;시&amp;nbsp;각&amp;nbsp;단계의&amp;nbsp;역할과&amp;nbsp;파일&amp;nbsp;복사,&amp;nbsp;경로&amp;nbsp;관리&amp;nbsp;등을&amp;nbsp;신경써야&amp;nbsp;하므로&amp;nbsp;단일&amp;nbsp;스테이지에&amp;nbsp;비해&amp;nbsp;복잡해질&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;빌드&amp;nbsp;시간&amp;nbsp;및&amp;nbsp;리소스:&lt;br /&gt;모든&amp;nbsp;스테이지를&amp;nbsp;거치기&amp;nbsp;때문에&amp;nbsp;빌드&amp;nbsp;시간이&amp;nbsp;다소&amp;nbsp;늘어날&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;특히&amp;nbsp;복잡한&amp;nbsp;빌드&amp;nbsp;프로세스의&amp;nbsp;경우&amp;nbsp;빌드&amp;nbsp;서버&amp;nbsp;리소스&amp;nbsp;사용이&amp;nbsp;증가할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;디버깅&amp;nbsp;어려움:&lt;br /&gt;빌드&amp;nbsp;단계와&amp;nbsp;런타임&amp;nbsp;단계가&amp;nbsp;분리되어&amp;nbsp;있기&amp;nbsp;때문에,&amp;nbsp;빌드&amp;nbsp;시&amp;nbsp;발생하는&amp;nbsp;문제를&amp;nbsp;런타임&amp;nbsp;이미지에서&amp;nbsp;직접&amp;nbsp;확인하기&amp;nbsp;어려울&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;문제&amp;nbsp;발생&amp;nbsp;시&amp;nbsp;어느&amp;nbsp;단계에서&amp;nbsp;오류가&amp;nbsp;발생했는지&amp;nbsp;파악하는&amp;nbsp;데&amp;nbsp;추가적인&amp;nbsp;노력이&amp;nbsp;필요할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 스테이지 빌드는 최종 이미지의 경량화, 보안 강화 및 환경 분리를 통한 유지보수 용이성 등의 큰 장점을 제공하지만, Dockerfile의 복잡도 증가와 빌드 과정에서의 리소스 사용, 디버깅의 어려움 등 단점도 존재합니다. 따라서, 프로젝트의 요구사항과 개발/운영 환경에 맞춰 적절히 선택하여 작성하시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739421329427&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# --- Build Stage ---
FROM openjdk:17 AS build

WORKDIR /app/api.mingyu.co.kr
COPY . .

RUN chmod +x ./gradlew

# Gradle 빌드 시 병렬 워커 최소화
RUN ./gradlew clean build -x test --no-daemon

# --- Runtime Stage ---
FROM openjdk:17 AS runtime

WORKDIR /app/api.mingyu.co.kr

RUN mkdir -p logs
RUN mkdir -p config

COPY --from=build /app/api.mingyu.co.kr/build/libs/*.jar app.jar
COPY --from=build /app/api.mingyu.co.kr/src/main/resources/application-dev.yml config/application-dev.yml

EXPOSE 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 작성한 Dockerfile은 멀티스테이지 빌드로서, 빌드 환경(build stage)과 실행 환경(runtime stage)을 분리하여 최종 이미지의 크기를 줄이고, 불필요한 빌드 도구나 소스 코드를 런타임 이미지에서 제외합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;빌드 단계에서는 OpenJDK 17 기반의 빌드 환경 설정, 소스 코드 복사 및 Gradle 빌드 실행, 테스트는&amp;nbsp;제외하여&amp;nbsp;빠른&amp;nbsp;빌드&amp;nbsp;진행&lt;br /&gt;런타임 단계에서는 OpenJDK 17 기반의 경량 실행 환경 설정, 로그 및 설정 파일 저장을 위한 디렉터리 생성, 빌드 단계에서 생성된 JAR 파일과 설정 파일 복사, 애플리케이션이 8080 포트에서 실행됨을 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우, spring boot 2.7버전을 사용하고, &lt;u&gt;&lt;b&gt;plain.jar 생성을 방지했기 때문에 *.jar를 app.jar로 변경해도 오류가 발생하진 않지만&lt;/b&gt;&lt;/u&gt;, 보통 plain.jar도 함께 생성되기 때문에 사전에 본인 버전에 맞는 방법으로 plain.jar 생성을 방지하거나, .jar 파일의 이름을 설정하여 COPY하도록 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Docker 빌드하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 명령어를 사용하여 Dockerfile을 기준으로 mingyu-api 라는 이름의 0.1 버전의 이미지를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739421569258&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker build -t mingyu-api:0.1 -f Dockerfile .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 정상적으로 완료되는것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8MmP4/btsMheEjYSf/O0HkcqKf51Z9YkUEM4YiyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8MmP4/btsMheEjYSf/O0HkcqKf51Z9YkUEM4YiyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8MmP4/btsMheEjYSf/O0HkcqKf51Z9YkUEM4YiyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8MmP4%2FbtsMheEjYSf%2FO0HkcqKf51Z9YkUEM4YiyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1840&quot; height=&quot;595&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;595&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Docker 이미지 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker 명령어를 사용하여 이미지가 잘 생성됬는지 확인합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739421814902&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker images&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 0.1버전의 mingyu-api 이미지가 정상적으로 생성된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkhiok/btsMh5UbuB3/g5wbyGOKCYjaLLlnlOuYRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkhiok/btsMh5UbuB3/g5wbyGOKCYjaLLlnlOuYRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkhiok/btsMh5UbuB3/g5wbyGOKCYjaLLlnlOuYRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkhiok%2FbtsMh5UbuB3%2Fg5wbyGOKCYjaLLlnlOuYRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;852&quot; height=&quot;73&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자의 java 버전 및 spring boot에 따라 취향껏 커스텀하며 도커파일을 작성할 수 있습니다. 도커의 확장성은 무궁무진합니다. 너무 어려워 하지 말고 많이 작성해보고 빌드해보면서 사용해보도록 합시다.&lt;/p&gt;</description>
      <category>Infrastructure/Docker</category>
      <category>docker build</category>
      <category>docker spring 빌드</category>
      <category>docker springboot</category>
      <category>docker springboot build</category>
      <category>dockerfile spring 빌드</category>
      <category>spring docker build</category>
      <category>spring docker 빌드</category>
      <category>spring dockerfile</category>
      <category>springboot docker</category>
      <category>springboot docker 빌드</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/330</guid>
      <comments>https://min-nine.tistory.com/entry/Docker%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-SpringBoot-Application-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0#entry330comment</comments>
      <pubDate>Thu, 13 Feb 2025 13:50:05 +0900</pubDate>
    </item>
    <item>
      <title>[NCP] NKS로 개발환경 구성하기 Chapter 1. 개발 환경이 없었다</title>
      <link>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-1-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B4-%EC%97%86%EC%97%88%EB%8B%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 회사에는 개발환경이 존재하지 않았습니다. local에서 개발하고, production에 바로 배포하고 오류가 발생하면 롤백하며 프로덕션 서비스를 운영하는 것이였습니다. 맞습니다. 흔히 말하는 stg나 dev 환경의 구성에 적용하여 진행하는 알파테스트 개념이 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 개발환경 구성은 꼭 필요하다고 생각되었고, Kubernetes를 적용해서 개발 환경을 구성하면 추후 production 서비스도 쉽게 k8s로 마이그레이션이 가능할 것이라 생각하였습니다. 그리고 &lt;b&gt;솔직히 k8s를 공부하고 실제로 사용해보고 싶기도 하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀장님께 k8s의 도입의 필요성을 보고를 드린 후, k8s를 사용해서 개발환경 구성을 진행하기로 하였고 본 포스팅에서는 도입과정과 부딪힌 문제들을 해결해 나가는 일련의 과정들을 작성할 예정입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 초기 구상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상황을 체크해 보자면, 운영 서비스들은 전부 Naver Cloud Platform의 서비스들로 구성되어 있습니다. CI/CD는 Source Build, Deploy, PipeLine을 이용하고 있고 git 리포지토리 또한 Source Commit이라는 네이버 클라우드 플랫폼의 서비스를 이용하고 있는 상황이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IcsmS/btsMgR91R8q/5cW5z7kcZEMaNeXsM1yekk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IcsmS/btsMgR91R8q/5cW5z7kcZEMaNeXsM1yekk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IcsmS/btsMgR91R8q/5cW5z7kcZEMaNeXsM1yekk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIcsmS%2FbtsMgR91R8q%2F5cW5z7kcZEMaNeXsM1yekk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;920&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 Kubernets 또한, Naver Cloud Platform의 NKS를 사용하기로 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WUq6m/btsMflxNXhn/cFRo5sq8tJDFzPdmkvZkMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WUq6m/btsMflxNXhn/cFRo5sq8tJDFzPdmkvZkMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WUq6m/btsMflxNXhn/cFRo5sq8tJDFzPdmkvZkMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWUq6m%2FbtsMflxNXhn%2FcFRo5sq8tJDFzPdmkvZkMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2400&quot; height=&quot;1604&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. NKS Cluster 생성하기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 클러스터 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NCloud Kubernetes Service (NKS) 메뉴에 접속하여 [클러스터 생성하기] 를 통해 손쉽게 클러스터를 구성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2025-02-13 오전 10.53.30.png&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df7zMP/btsMgSHPgrq/MqBkTSqEpaJCZ2uV0nVQL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df7zMP/btsMgSHPgrq/MqBkTSqEpaJCZ2uV0nVQL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df7zMP/btsMgSHPgrq/MqBkTSqEpaJCZ2uV0nVQL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf7zMP%2FbtsMgSHPgrq%2FMqBkTSqEpaJCZ2uV0nVQL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1918&quot; height=&quot;934&quot; data-filename=&quot;edited_스크린샷 2025-02-13 오전 10.53.30.png&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 이야기하고 싶지만 사실 손쉽게 설정하기는 어렵습니다. 네트워크 타입 설정부터 시작하여 Subnet과 LB Private Subnet, LB Public Subnet, 그리고 NAT GateWay 등을 사전에 만들어둬야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 클라우드 환경 구성의 목적성은 &lt;b&gt;개발환경 구성&lt;/b&gt;이기 때문에 저는 dev vpc를 생성하였고, 그 vpc 대역에 맞는 클러스터 subnet과 lb의 private, public subnet을 각각 미리 생성해 두었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbyUhC/btsMg3PWiYU/KszXiOqcpIK8ZeAKb4HLMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbyUhC/btsMg3PWiYU/KszXiOqcpIK8ZeAKb4HLMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbyUhC/btsMg3PWiYU/KszXiOqcpIK8ZeAKb4HLMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbyUhC%2FbtsMg3PWiYU%2FKszXiOqcpIK8ZeAKb4HLMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1914&quot; height=&quot;560&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nat Gateway는 아웃바운드 인터넷 트래픽 활성을 위해 필요한 존재로서, NAT Gateway 생성 이후&amp;nbsp; Route Table을 생성하여 위에서 생성한 private lb subnet과 연관 짓고 Routes를 0.0.0.0/0으로 지정하여 생성한 NAT Gateway를 타깃으로 설정해 주면 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 노드풀 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 그룹을 이루는 노드풀을 설정합니다. production 환경의 구성이였다면 각 서비스마다 (예: 뉴스 서비스, 로그인 서비스 등의 2차 도메인을 기준으로?) 노드풀을 구성하여 해당 서비스에 관련된 애플리케이션들을 구성시키는 것이 맞겠지만, 현재는 개발 환경 구성에 초점이 맞춰져 있기 때문에 저는 dev라는 이름의 노드풀을 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1901&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NKSzx/btsMgTGMSSr/D9oUPeh7MPmMMn4HxWHdOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NKSzx/btsMgTGMSSr/D9oUPeh7MPmMMn4HxWHdOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NKSzx/btsMgTGMSSr/D9oUPeh7MPmMMn4HxWHdOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNKSzx%2FbtsMgTGMSSr%2FD9oUPeh7MPmMMn4HxWHdOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1901&quot; height=&quot;903&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1901&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 타입은 제일 기본사양의 Standard s2-g3 (CPU 2EA, Memory 8GB)를 사용하였고, 노드수는 3개로 구성하였습니다. 노드수는 최소 1개부터 설정할 수 있는데, 3개를 사용한 이유는 다음과&amp;nbsp;같습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;고가용성(High&amp;nbsp;Availability):&lt;/b&gt;&lt;br /&gt;3개의&amp;nbsp;노드를&amp;nbsp;구성하면&amp;nbsp;하나의&amp;nbsp;노드에&amp;nbsp;장애가&amp;nbsp;발생하더라도&amp;nbsp;나머지&amp;nbsp;노드들이&amp;nbsp;계속해서&amp;nbsp;서비스를&amp;nbsp;제공할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;단일&amp;nbsp;장애점(Single&amp;nbsp;Point&amp;nbsp;of&amp;nbsp;Failure)을&amp;nbsp;제거하여&amp;nbsp;전체&amp;nbsp;시스템의&amp;nbsp;안정성을&amp;nbsp;보장할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;부하&amp;nbsp;분산&amp;nbsp;및&amp;nbsp;성능&amp;nbsp;향상:&lt;/b&gt;&lt;br /&gt;여러&amp;nbsp;노드에&amp;nbsp;트래픽과&amp;nbsp;처리를&amp;nbsp;분산시키면&amp;nbsp;특정&amp;nbsp;노드에&amp;nbsp;과도한&amp;nbsp;부하가&amp;nbsp;걸리는&amp;nbsp;것을&amp;nbsp;방지할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;이로&amp;nbsp;인해&amp;nbsp;전체&amp;nbsp;시스템의&amp;nbsp;응답&amp;nbsp;시간이&amp;nbsp;개선되고,&amp;nbsp;예상치&amp;nbsp;못한&amp;nbsp;트래픽&amp;nbsp;급증에도&amp;nbsp;유연하게&amp;nbsp;대처할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;유연한&amp;nbsp;확장성:&lt;/b&gt;&lt;br /&gt;초기 구성은 3개 노드로 시작하지만, 서비스가 성장하거나 트래픽이 증가할 경우 추가 노드를 쉽게 도입할 수 있습니다. 이는 클러스터 환경에서 자원 할당과 관리에 있어 큰 장점으로 작용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 인증키 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증키는 보유하고 있는 인증키를 설정하거나 새로운 인증키를 생성할 수 있습니다. 인증키를 가지고 매니저 노드 접속 관리자 비밀번호를 얻을 수 있는데, 저는 vpc를 만들때 생성된 key-vpc-dev 인증키를 사용하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzWS0S/btsMe9qLwGQ/w6HcYpCL36KuZvJvm0ghKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzWS0S/btsMe9qLwGQ/w6HcYpCL36KuZvJvm0ghKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzWS0S/btsMe9qLwGQ/w6HcYpCL36KuZvJvm0ghKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzWS0S%2FbtsMe9qLwGQ%2Fw6HcYpCL36KuZvJvm0ghKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1904&quot; height=&quot;872&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 같은 방식으로 mgt-cluster 생성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 로그 통합 환경구성, argocd 도입 등을 위해 management 역할을 담당하는 mgt-cluster를 생성해둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 생성된 cluster 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 dev-cluster 및 mgt-cluster가 설정한 node수와 서버 사양에 맞게 구성하여 정상적으로 생성된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHQoaA/btsMgNzNBWR/IbPMEpMbGLYbMtYKuNcFSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHQoaA/btsMgNzNBWR/IbPMEpMbGLYbMtYKuNcFSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHQoaA/btsMgNzNBWR/IbPMEpMbGLYbMtYKuNcFSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHQoaA%2FbtsMgNzNBWR%2FIbPMEpMbGLYbMtYKuNcFSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1917&quot; height=&quot;890&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 dev-cluster에 java(spring boot) application, nextJs application, php(Legacy) application 등을 배포하고, mgt-cluster에 로그를 수집하여 모니터링하는 스택 구성 등의 &lt;b&gt;본격 k8s 도입&lt;/b&gt; 포스팅을 이어나가겠습니다.&lt;/p&gt;</description>
      <category>Infrastructure/Kubernetes</category>
      <category>kubernetes 도입하기</category>
      <category>nks</category>
      <category>nks cluster 생성</category>
      <category>nks 강의</category>
      <category>nks 도입 방법</category>
      <category>nks 도입하기</category>
      <category>nks도입</category>
      <category>네이버 클라우드 쿠버네티스</category>
      <category>쿠버 쉽게 알기</category>
      <category>쿠버네티스 따라하기</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/329</guid>
      <comments>https://min-nine.tistory.com/entry/Kubernetes-%EB%8F%84%EC%9E%85-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%97%86%EB%8A%94-%ED%9A%8C%EC%82%AC%EC%97%90%EC%84%9C-NKS%EB%A1%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-Chapter-1-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9D%B4-%EC%97%86%EC%97%88%EB%8B%A4#entry329comment</comments>
      <pubDate>Thu, 13 Feb 2025 11:25:25 +0900</pubDate>
    </item>
    <item>
      <title>[Mac Mini 홈 서버 구축] 외부 접속 테스트 - SKB 포트 포워딩으로 외부에서 접속해본 후기</title>
      <link>https://min-nine.tistory.com/entry/Mac-Mini-%ED%99%88-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-1-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D-%ED%85%8C%EC%8A%A4%ED%8A%B8-SKB-%ED%8F%AC%ED%8A%B8-%ED%8F%AC%EC%9B%8C%EB%94%A9%EC%9C%BC%EB%A1%9C-%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-%EC%A0%91%EC%86%8D%ED%95%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 슬슬 나만의 프로덕션 서비스를 만들어 보자 생각하며, 클라우드 서버 임대는 비용적인 문제가 많으므로 제외하고 서버 호스팅 같은 차선책을 생각하다가 회사에서 한 과장님이 서버 및 네트워크 공부할 겸 GMKtec G5 mini pc 16g Ram / 512g SSD 를 26만원 주고 구매하신 것을 같이 구경하며 '나는 mac mini를 사용해보자!' 라는 생각과 함께 바로 오늘! 당근으로 Mac Mini M1 깡통 모델을 35만원에 구입했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집에서 굴러다니는 노트북으로 홈 서버 구축해본게 10년 전이기는 하지만, 검색해봤을 때 많은 양의 블로그 포스팅들을 발견하여서 쉽게 구축할 수 있을것이라 자신했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 생각이 틀렸다는 것을 4시간 가까이 삽질하며 깨닫게 되었고, 삽질한 후기겸 본 포스팅을 작성합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. SKB 모뎀과 공유기의 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 판 부터 장난질이냐? 라고 생각 할 정도로 SK Broadband 인터넷을 사용하는 저의 집에서 큰 난관에 봉착했습니다. 여러 블로그 포스팅을 봐도 KT는 쉽게 하던데 SKB는 22,80,8080 등 대중화된 포트를 이미 inbound로 설정하는 것을 막아놨기 때문입니다. 먼저 머리속에 그려놓은 흐름도는 아래와 같았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDvdtO/btsKCkNQYQ2/Zp0t7ihHydLJIbKsK5T1y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDvdtO/btsKCkNQYQ2/Zp0t7ihHydLJIbKsK5T1y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDvdtO/btsKCkNQYQ2/Zp0t7ihHydLJIbKsK5T1y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDvdtO%2FbtsKCkNQYQ2%2FZp0t7ihHydLJIbKsK5T1y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1687&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 공인 ip는 말 그대로 외부에서 나의 pc를 찾아올 수 있는 대표적인 ip로서 네이버에서 &quot;내 ip 확인하기&quot; 혹은 아래 명령어 등으로 쉽게 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731081908834&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl ifconfig.me&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 localhost에 8080 포트를 기본으로 사용하는 laravel 기본 프로젝트를 설치한 후, 열심히 모뎀과 공유기에 포트포워딩을 해줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 LTE를 사용하는 휴대폰에서 접속을 시도하는데,,,,,&amp;nbsp; &lt;b&gt;무한 로딩이 걸리다 튕겨버립니다..!!&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제점 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한로딩이 걸렸다는 말은 외부 아이피를 토대로 분명 무언가를 찾고있는 어디까지 도달해서 계속 찾다가 실패했다는 것인데, 디버깅을 할 수 있는 것도 아니고 로그를 찍을 수 있는 것도 아니고 SKB 모뎀과 공유기 어드민에 접속하여 시스템 로그만 계속 살펴봤지만 로그로 잡히는 것이 하나도 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 포트포워딩을 하지 않은 8001 포트는 어떨까? 생각하고 휴대폰에서 LTE로 xxx.xxx.75.13:8001로 접속해보니 로딩은 1도 걸리지 않은체 바로 찾을 수 없는 웹페이지가 나와야 하는데,,, 이것도 내 생각과 다르게 무한 로딩이 걸려버렸습니다. 무엇이 문제인지 모른다면 문제를 바라보는 관점을 달리 할 수 밖에..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 휴대폰을 wifi에 연결하여 내부 ip에서 내부 ip인 192.168.45.121:8000로&amp;nbsp; 접속해보았습니다. 그치만 안나왔습니다. 분명 mac mini 로컬에는 8000포트로 잘 띄워져있는 라라벨 웹 페이지가 잘 띄워져 있는데 말입니다...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fd6Zr/btsKDPFj8B1/oxG3mNfMGE5Oxeb6kzvOD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fd6Zr/btsKDPFj8B1/oxG3mNfMGE5Oxeb6kzvOD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fd6Zr/btsKDPFj8B1/oxG3mNfMGE5Oxeb6kzvOD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFd6Zr%2FbtsKDPFj8B1%2FoxG3mNfMGE5Oxeb6kzvOD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3840&quot; height=&quot;2160&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 그러다 머리속을 코난이 범인을 짐작했을 때 처럼 급하게 스쳐지나가는 생각 한 가지. 저것은 artisan이 띄운 라라벨 프로젝트 안에 있는 내부 웹서버에 지나지 않는거 아닌가,,?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각이 끝나게 무섭게, 도커를 사용하여 nginx를 8000:80 포트바인딩 해줘서 띄웠습니다. 그리고 옆 macbook으로 192.168.45.121로 접속해보는 순간..!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVz0gE/btsKBTioIrm/HilyTyjxSMAnj0TUK73I40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVz0gE/btsKBTioIrm/HilyTyjxSMAnj0TUK73I40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVz0gE/btsKBTioIrm/HilyTyjxSMAnj0TUK73I40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVz0gE%2FbtsKBTioIrm%2FHilyTyjxSMAnj0TUK73I40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1964&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무나도 아름답고 부드럽고&lt;b&gt; 깔끔하게 포트포워딩 처리가 잘 된것을 확인할 수 있었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfyFDS/btsKD1lpmak/E9DYWc4zhdr7kkMavzyh8k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfyFDS/btsKD1lpmak/E9DYWc4zhdr7kkMavzyh8k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfyFDS/btsKD1lpmak/E9DYWc4zhdr7kkMavzyh8k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfyFDS%2FbtsKD1lpmak%2FE9DYWc4zhdr7kkMavzyh8k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;374&quot; height=&quot;338&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스팅 하면서 정리하니깐 별 것 아닌것 처럼 느껴지지만 사실 4시간동안 집 안에 꽁꽁 숨겨진 모뎀을 찾는데 30분이 넘게 걸렸고, 공유기가 1대인 줄 알고 있었는데 사실 두 대나 있었고,,? 이런 저런 잡스러운 것 때문에 삽질을 많이 하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 대망의 외부 LET에서의 접속..! 휴대폰으로 wifi 설정을 해제한 체 LTE만으로 외부 공인 IP로 접속을 해보았습니다. 결과는... 역시 성공이였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNuCeK/btsKBJAmQoa/ijRFdUlEnq7OvbP3CNYOBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNuCeK/btsKBJAmQoa/ijRFdUlEnq7OvbP3CNYOBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNuCeK/btsKBJAmQoa/ijRFdUlEnq7OvbP3CNYOBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNuCeK%2FbtsKBJAmQoa%2FijRFdUlEnq7OvbP3CNYOBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;650&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. SKB 공유기 및 모뎀 어드민 주소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 정말 쉬운데, 내부 ip를 찾아서 뒷 자리 1로 바꿔주면 그것이 공유기의 게이트웨이 주소이고 그것이 곳 공유기 어드민의 접속 url이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 모뎀은 어떨까요? 공유기에 ip를 찔러주는 곳이 공유기 입장에서의 wan이 되겠고, 그 wan의 게이트웨이 값이 모뎀의 게이트웨이 주소이자 어드민 접속 url이 되는것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SKB의 경우 모뎀 및 공유기에 admin 접속 비밀번호가 숨겨져 있으니 이점은 검색해보시면 상세히 찾을 수 있을겁니다. 저의 경우 위의 구성도대로 계산했을 때 아래와 같이 접속할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유기 어드민 : 192.168.45.1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모뎀 어드민 : 192.168.55.1&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와이프랑 불금이랍시고 삼겹살에 소주 한 잔 마시고 와서 반은 췻김에, 반은 장난감 새로산 어린이의 심정으로 재미있게 삽질하며 놀았습니다. 앞으로 틈틈히 네트워크 공부겸 방화벽 설정이나 DMZ 설정 등과 같은 내용을 적용할 때 마다 본 토픽에 포스팅을 이어가도록 하겠습니다.&lt;/p&gt;</description>
      <category>Infrastructure/Network</category>
      <category>sk 공유기 외부 접속 허용</category>
      <category>sk 공유기 포트 포워딩</category>
      <category>sk 공유기 홈 서버</category>
      <category>sk 모뎀 포트 포워딩</category>
      <category>sk 모뎀 홈 서버</category>
      <category>sk 포트 포워딩 외부 접속</category>
      <category>sk 홈서버 구축</category>
      <category>skb 모뎀 외부 접속 허용</category>
      <category>skb 포트 포워딩</category>
      <category>skb 홈서버 만들기</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/328</guid>
      <comments>https://min-nine.tistory.com/entry/Mac-Mini-%ED%99%88-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-1-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D-%ED%85%8C%EC%8A%A4%ED%8A%B8-SKB-%ED%8F%AC%ED%8A%B8-%ED%8F%AC%EC%9B%8C%EB%94%A9%EC%9C%BC%EB%A1%9C-%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-%EC%A0%91%EC%86%8D%ED%95%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0#entry328comment</comments>
      <pubDate>Sat, 9 Nov 2024 01:26:52 +0900</pubDate>
    </item>
    <item>
      <title>[Query Tuning] 요청사항을 듣고, 그대로 구현하면 안되는 이유 - feat DataJPA,JPQL</title>
      <link>https://min-nine.tistory.com/entry/Query-Tuning-%EC%9A%94%EC%B2%AD%EC%82%AC%ED%95%AD%EC%9D%84-%EB%93%A3%EA%B3%A0-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-feat-DataJPAJPQL</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자들이 흔히 저지르는 실수 중 하나는 요구사항을 듣고 이를 곧바로 비즈니스 로직이나 쿼리로 구현하는 것입니다. 이게 왜 문제가 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 이미지는 팀의 백엔드 개발자 채널 내용 중 일부입니다. 주목할 부분은 빨간색으로 표시된 요청사항입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXZHRc/btsKBjAeifu/rybuNsrKkxDUCI4zCa3uR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXZHRc/btsKBjAeifu/rybuNsrKkxDUCI4zCa3uR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXZHRc/btsKBjAeifu/rybuNsrKkxDUCI4zCa3uR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXZHRc%2FbtsKBjAeifu%2FrybuNsrKkxDUCI4zCa3uR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;619&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청사항:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5년이 지난 결제 정보를 찾아, 개인정보 관련 특정 컬럼(id, 이름, 필명, 연락처)만 null로 업데이트하기.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청사항만 보고 그대로 Spring Data JPA로 구현한다면, 보존 기간이 지난 데이터를 findAll()로 조회하고, 데이터가 많다면 청크 단위로 나눠서 saveAll()로 저장하면 된다고 생각할 수 있습니다. 하지만 이렇게 접근할 경우 큰 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA의 saveAll은 개별 update 쿼리를 실행하는데, 특히 엔티티에 연관 관계(@OneToMany 등)가 설정된 경우에는 N+1 문제가 발생할 수 있습니다. 초기에는 데이터가 적어서 문제를 느끼지 못할 수 있지만, 데이터가 수만, 수십만, 혹은 수백만 건으로 증가하면 쿼리 실행 시간이 급격히 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 요구사항을 그대로 구현하기보다는, 이 요청이 무엇을 해결하고자 하는지 깊이 있게 고민해 볼 필요가 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. JPA FindAll() saveAll() 사용의 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 예시 코드의 일부입니다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;pre id=&quot;code_1731029543296&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void deleteUserArchive() {
    //보존기간 만료된 데이터 추출
    List&amp;lt;User&amp;gt; users = userRepository.findAllByArchiveLimitDatetimeLessThan(DateUtils.currentDate(-5));
    users.forEach(user -&amp;gt; {
        //edu_member 계정 개인정보 삭제
        log.info(&quot;삭제될 분리보관된 테이블 유저 : [{}]&quot;,user.getUserId());
        List&amp;lt;Ticket&amp;gt; ticket = ticketRepository.findByUserId(user.getUserId());
        log.info(&quot;개인정보가 삭제될 유저인원 : [{}]&quot;,ticket.size());
        ticket.forEach(Ticket::deleteInfo);
        eduMemberRepository.saveAll(ticket);
    });
    //보존기간이 만료된 분리보관된 데이터 제거
    userRepository.deleteAllInBatch(users);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 매우 강력한 ORM 도구이지만, 잘못 사용하면 서비스의 성능 저하를 일으킬 수 있습니다. 위 코드에서 &lt;b&gt;deleteAllInBatch&lt;/b&gt;는 &lt;b&gt;deleteAll&lt;/b&gt;과 달리 &lt;b&gt;select&lt;/b&gt; 쿼리 없이 오로지 &lt;b&gt;delete&lt;/b&gt; 쿼리만 실행하여 성능 면에서 더 유리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;deleteAll()와 deleteAllInBatch() 비교&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;deleteAll(): 각 엔티티에 대해 개별 삭제 처리를 진행하기 때문에, 엔티티가 많을 경우 성능 저하가 발생합니다. 연관된 엔티티의 조회 쿼리도 함께 실행될 수 있습니다.&lt;/li&gt;
&lt;li&gt;deleteAllInBatch(): select 쿼리를 수행하지 않고 단일 delete 쿼리만 실행해 성능이 훨씬 빠릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 saveAll, findAll과 마찬가지로 deleteAllInBatch()도 연관 관계가 설정된 엔티티가 포함되어 있으면 조인 쿼리가 추가적으로 실행될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 위 로직에서 User 엔티티에 연관관계가 설정되어 있다면 전체 행이 100건인 경우, findByUserId 메서드는 사용자당 Ticket 데이터를 조회하기 때문에 100번 실행될 수 있습니다. 여기에 saveAll 호출이 연관된 엔티티마다 개별 쿼리로 실행된다면, 총 100건의 saveAll 쿼리가 추가로 실행될 수 있습니다. 따라서, 연관 관계가 있는 경우 N+1 문제가 발생할 가능성이 높아 성능 저하를 초래할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용할 때는 이러한 연관 관계와 쿼리 최적화 문제를 반드시 고려해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 접근 방식의 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대량 데이터 업데이트&lt;/b&gt;: 5년이 지난 결제 정보를 일괄 update하기 위해서는 JPA의 @Modifying과 @Query를 사용해 단일 SQL 쿼리로 한 번에 처리하는 것이 바람직합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1731030927623&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Modifying
@Query(&quot;UPDATE Payment p SET p.userName = null, p.userId = null WHERE p.paymentDate &amp;lt; :date&quot;)
int nullifyUserInfoOlderThan(@Param(&quot;date&quot;) LocalDate date);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대량 삭제 시 성능 최적화&lt;/b&gt;: 대량 데이터 삭제는 deleteAllInBatch를 활용하되, 필요한 경우 JPA가 아닌 네이티브 SQL 쿼리를 직접 사용하는 것도 좋은 방법입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청사항을 그대로 구현하기보다는, 성능과 확장성에 문제가 없는지 고민하고 최적화된 방법을 선택해야 합니다. 특히 대용량 데이터를 다룰 때는 JPA의 편리함만을 믿고 처리하기보다는, 구체적인 쿼리 동작 방식을 이해하고 성능에 최적화된 접근 방식을 선택하는 것이 필요합니다.&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>jpa bulk update</category>
      <category>jpa deleteall</category>
      <category>jpa deleteall deleteallinbatch</category>
      <category>jpa deleteallinbatch</category>
      <category>jpa saveall</category>
      <category>JPA 단점</category>
      <category>jpql bulk update</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/327</guid>
      <comments>https://min-nine.tistory.com/entry/Query-Tuning-%EC%9A%94%EC%B2%AD%EC%82%AC%ED%95%AD%EC%9D%84-%EB%93%A3%EA%B3%A0-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-feat-DataJPAJPQL#entry327comment</comments>
      <pubDate>Fri, 8 Nov 2024 12:40:33 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 기반 다수의 프로젝트를 하나의 Multi Module 프로젝트로 통합하기 (3) - 인증과 인가는 Gateway로, 토큰 발급은 Auth Api로</title>
      <link>https://min-nine.tistory.com/entry/Spring-Boot-%EA%B8%B0%EB%B0%98-%EB%8B%A4%EC%88%98%EC%9D%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EC%9D%98-Multi-Module-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-3-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80%EB%8A%94-Gateway%EB%A1%9C-%ED%86%A0%ED%81%B0-%EB%B0%9C%EA%B8%89%EC%9D%80-Auth-Api%EB%A1%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Request는 WebGateway가 받아서 각 Reqeust에 맞는 Service들로 매핑시키도록 아래와 같이 구성도를 작성하였습니다. 해당 Request가 올바른 권한과 인증을 부여받은 Request인가를 판단하기 위해, 앞으로 인증과 인가는 WebGateway에서, 인증과 인가 없이 요청할 수 있는 예외의 Request(Login, SignUp 등)는 Filter를 구현받지 않고 바로 Auth Service로 통과시켜주는 Gateway를 구현해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Playground라는 프로젝트 명칭은 학습 및 연구하며 실제로 적용해보는 놀이터용도로써 만든 프로젝트로서 큰 의미는 없습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3rmlT/btsKyx6zFiV/pwRXsxTpeNkHfRu09PCDg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3rmlT/btsKyx6zFiV/pwRXsxTpeNkHfRu09PCDg1/img.png&quot; data-alt=&quot;Playground (가칭) 서비스 구성도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3rmlT/btsKyx6zFiV/pwRXsxTpeNkHfRu09PCDg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3rmlT%2FbtsKyx6zFiV%2FpwRXsxTpeNkHfRu09PCDg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1583&quot; height=&quot;954&quot; data-origin-width=&quot;1583&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Playground (가칭) 서비스 구성도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. module-webGateway 역할 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;webGateway에서 모든 web 관련 Request를 컨트롤 할 예정이기 때문에 역할 부여가 중요합니다. 저는 Request가 올바른 사용자로부터 (인증) 올바른 권한에 맞게 (인가) 요청된 것인지 확인하는 역할을 부여하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 모든 MicroService (RestAPI 등)에 대한 명세서를 확인할 수 있도록 Swagger UI를 설치하여 각각의 서비스에 대한 API 문서 확인도 담당하는 역할을 부여하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 역할에 맞는 외부 의존성을 주입받아야 하는데, 저는 아래와 같이 module-webGateway의 dependencies를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730942579616&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bootJar {
    enabled = true
}
jar {
    enabled = true
}

dependencies {
    implementation project(':module-core')
    implementation project(':module-redis')

    implementation 'org.springframework.boot:spring-boot-starter-actuator'

    // Spring Cloud
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-security'

    // Mac Os Dependency
    implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64'

    // Swagger
    implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.6.0'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;core 모듈에는 전역에서 사용할 예외 관련 Class, 상황에 맞게 사용하기 위한 여러가지 Custom Annotation (Json 형식 판단하는 JsonMatches 등)을 구현하였고, redis 모듈에서는 redis를 사용하기 위한 설정과 기능 구현이(Service 및 Util 단위) 완료되어있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Security Configuration&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증과 인가를 처리하기 위해서 SpringSecurity를 활용하기로 하였고, Spring Cloud Gateway의 최신 버젼부터는 MVC 방식이 아닌 Reactive 방식만을 지원하기 때문에 WebFluxSecurity를 사용하여 설정을 진행하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730942917096&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class WebFluxSecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity, JwtTokenizer jwtTokenizer) {

        serverHttpSecurity
                .cors(Customizer.withDefaults())
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
                .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
                .anonymous(ServerHttpSecurity.AnonymousSpec::disable);

        serverHttpSecurity.authorizeExchange((authorize) -&amp;gt; authorize
                // Gateway 에서는 AuthorizationHeaderFilter 에서 jwt를 체크하므로 authenticated 설정 불 필요. 때문에 전체 PermitAll로 설정
                .anyExchange().permitAll()
        )
                .securityContextRepository(new StatelessWebSessionSecurityContextRepository())
        ;

        serverHttpSecurity.logout(logoutSpec -&amp;gt; {
            logoutSpec.logoutUrl(&quot;/api/v1/auth/logout&quot;)
                    .requiresLogout(new PathPatternParserServerWebExchangeMatcher(&quot;/api/v1/auth/logout&quot;, HttpMethod.POST));
        });
        return serverHttpSecurity.build();
    }

    private static class StatelessWebSessionSecurityContextRepository implements ServerSecurityContextRepository {

        private static final Mono&amp;lt;SecurityContext&amp;gt; EMPTY_CONTEXT = Mono.empty();

        @Override
        public Mono&amp;lt;Void&amp;gt; save(ServerWebExchange exchange, SecurityContext context) {
            return Mono.empty();
        }

        @Override
        public Mono&amp;lt;SecurityContext&amp;gt; load(ServerWebExchange exchange) {
            return EMPTY_CONTEXT;
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석을 살펴보면 알겠지만, security에서 authenticated()를 사용하여 인증인가를 구현하는 방식이 아닌, 모든 Request에 대한 permitAll()을 해준 후에, AuthorizationHeaderFilter 이름의 Filter를 만들어서 인증 인가가 필요한 uri에 해당하는 service에만 적용하여 filter chain방식으로 구현하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. AuthorizationHeaderFileter 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Filter에서는 request의 Header에 포함되어있는 JWT Token의 검증을 구현합니다. Token이 정상적인 토큰인지, 만료된 토큰이 아닌지, 아직 유효한 토큰이지만 로그아웃을 진행했던 토큰이 아닌지 등에 대해서 검증합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730943148299&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory&amp;lt;AuthorizationHeaderFilter.Config&amp;gt; {

    private final JwtTokenizer jwtTokenizer;
    private final AuthLoginService authLoginService;

    @Autowired
    public AuthorizationHeaderFilter(JwtTokenizer jwtTokenizer, AuthLoginService authLoginService) {
        super(Config.class);
        this.jwtTokenizer = jwtTokenizer;
        this.authLoginService = authLoginService;
    }

    @Override
    public GatewayFilter apply(AuthorizationHeaderFilter.Config config) {
        return (exchange, chain) -&amp;gt; {

            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info(&quot;request uri : {}&quot;, request.getURI());

            log.warn(&quot;request headers : {}&quot;, request.getHeaders());

            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
                response.setStatusCode(HttpStatus.UNPROCESSABLE_ENTITY);
                return response.setComplete();
            }

            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
            String jwt = authorizationHeader.replace(&quot;Bearer &quot;, &quot;&quot;);
            log.info(&quot;jwt : {}&quot;, jwt);

            // JWT 검증
            if (!isJwtValid(jwt)) {
                response.setStatusCode(HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS);
                return response.setComplete();
            }

            // Logout Token 검증
            if (isLogoutToken(jwt)) {
                response.setStatusCode(HttpStatus.FORBIDDEN);
                return response.setComplete();
            }

            return chain.filter(exchange);

        };
    }

    private Mono&amp;lt;Void&amp;gt; onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        log.error(err);
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        return response.setComplete();
    }

    private boolean isJwtValid(String jwt) {

        Claims claims = jwtTokenizer.parseAccessToken(jwt); // 토큰에서 클레임을 파싱

        if (claims == null || Strings.isBlank(claims.getSubject())) { // 클레임이 없거나 이메일이 없으면 false
            return false;
        }

        String email = claims.getSubject(); // 이메일을 가져옴
        String id = claims.get(&quot;id&quot;, String.class); // 사용자 ID를 가져옴
        String name = claims.get(&quot;name&quot;, String.class); // 이름을 가져옴
        return true;
    }

    private boolean isLogoutToken(String jwt) {
        return !authLoginService.isLogin(jwt);
    }

    public static class Config {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(참고)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 로그아웃을 할 경우 만료되지 않은 토큰이라도 사용하지 못 하게 하는 블랙리스트 방식의 기능을 구현했습니다. 해당 내용은 별도로 포스팅 할 예정입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Application Yml 설정을 통한 Gateway 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 사용하며 참 좋다고 생각들었던 것은, 모든 구현이 비지니스로직을 통해서 이뤄지는 것이 아닌, 설정파일(Yml 등)에 설정만 잘 해주면 알아서 완성시켜주는 각 spring project(cloud, security 등)가 가지고 있는 매커니즘이였습니다. 비지니스로직을 통해서 구현하는 방법도 있었지만, 저는 yml 파일 설정을 통해 webGateway를 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. JWT 관련 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Jwt 토큰을 해석하기 위해 토큰을 발행하는 Auth Module에 있는 jwt 설정 키가 필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730943524884&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jwt:
  access:
    secret: auth-service-jwt-access-secret
    expire: 86400000
  refresh:
    secret: auth-service-jwt-refresh-secret
    expire: 2592000000&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Spring Cloud Gateway 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Eureka Server를 사용하여 모든 어플리케이션을 관리하고 있습니다. 이 점 참고하여서 Eureka 사용을 안 할 경우에는 uri에 각 서비스의 절대경로를 사용하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730943724017&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  application:
    name: playground-web-gateway
  config:
    import: &quot;optional:configserver:&quot;
  cloud:
    gateway:
      # 먼저 선언한 순서대로 필터 적용.
      routes:
        - id: auth-api
          # Eureka 사용할 경우
          uri: lb://auth-api
          # Eureka 사용 안할 경우
          #uri: http://localhost:18085
          # /api/v1/member/** 경로로 들어오는 요청을 member-api로 라우팅
          predicates:
            - Path=/api/v1/auth/login, /api/v1/auth/signup
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2

        - id : auth-api
          uri: lb://auth-api
          predicates:
            - Path=/api/v1/member/**, /api/v1/auth/logout
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2
            - AuthorizationHeaderFilter

        - id: article-api
          uri: lb://article-api
          predicates:
            - Path=/api/v1/article/test
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2

        - id: article-api
          uri: lb://article-api
          predicates:
            - Path=/api/v1/article/**
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2
            - AuthorizationHeaderFilter

        - id: mail-api
          uri: lb://mail-api
          predicates:
            - Path=/api/v1/mail/**
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2
            - AuthorizationHeaderFilter
  main:
    web-application-type: reactive

management:
  endpoints:
    web:
      exposure:
        include: gateway
  endpoint:
    gateway:
      enabled: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용을 잘 보시면 filters쪽에 인증 및 인가가 필요한 Request에는 AuthorizationHeaderFilter를 적용한 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Swaager를 위한 springdoc 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestAPI로 사용되는 모든 어플리케이션의 spring docs url과 맵핑지어 webgateway/swagger-ui.html 에서 한 번에 확인할 수 있도록 설정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730943932672&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;springdoc:
  swagger-ui:
    urls[0]:
      name: Auth API
      url: ${server.host}:${app.ports.auth-api}/api/v1/auth/v3/api-docs
    urls[1]:
      name: Article API
      url: ${server.host}:${app.ports.article-api}/api/v1/article/v3/api-docs
    urls[2]:
      name: Mail API
      url: ${server.host}:${app.ports.mail-api}/api/v1/mail/v3/api-docs
    use-root-path: true
    enabled: true
    path: /swagger-ui.html
    config-url: /v3/api-docs/swagger-config&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 구동 및 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka application, WebGateway Application, Auth Api Application, Article Api Application, Mail Api Application, Rabbitmq Consumer Application 총 6개의 Application을 구동시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Eureka 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8761 주소로 접속하면 현재 로컬에 구동되어있는 Eureka Server 현황을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-07 오전 10.49.51.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jcup9/btsKzeGkLen/I9jJuNskzRpmOwXTOh5v41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jcup9/btsKzeGkLen/I9jJuNskzRpmOwXTOh5v41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jcup9/btsKzeGkLen/I9jJuNskzRpmOwXTOh5v41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJcup9%2FbtsKzeGkLen%2FI9jJuNskzRpmOwXTOh5v41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1964&quot; data-filename=&quot;스크린샷 2024-11-07 오전 10.49.51.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Application이 잘 구동되어있는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swaager 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/swagger-ui.html 주소로 접속하면 우측 상단에 Select a definition 셀렉트박스를 통해서 구성된 springdocs를 전부 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z03rW/btsKymrABys/OvK9bPGNezXPLcj4CnKwwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z03rW/btsKymrABys/OvK9bPGNezXPLcj4CnKwwk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot; data-filename=&quot;스크린샷 2024-11-07 오전 10.49.14.png&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z03rW/btsKymrABys/OvK9bPGNezXPLcj4CnKwwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ03rW%2FbtsKymrABys%2FOvK9bPGNezXPLcj4CnKwwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1964&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uMEPt/btsKzrL3kHl/s3MVSGREkA6Dk5gypeaUSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uMEPt/btsKzrL3kHl/s3MVSGREkA6Dk5gypeaUSk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot; data-filename=&quot;스크린샷 2024-11-07 오전 10.49.27.png&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uMEPt/btsKzrL3kHl/s3MVSGREkA6Dk5gypeaUSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuMEPt%2FbtsKzrL3kHl%2Fs3MVSGREkA6Dk5gypeaUSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1964&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLmyf6/btsKArqULmS/WQzgYdAdThXBrIBkWRxJB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLmyf6/btsKArqULmS/WQzgYdAdThXBrIBkWRxJB0/img.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2024-11-07 오전 10.49.37.png&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLmyf6/btsKArqULmS/WQzgYdAdThXBrIBkWRxJB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLmyf6%2FbtsKArqULmS%2FWQzgYdAdThXBrIBkWRxJB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1964&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP Test&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 Http Request를 통한 실제 API들을 점검해봅니다. host를 webGateway 포트인 8080포트로 지정한 후에 테스트를 진행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HiDOw/btsKxQsMOAS/fDg4hudNZoNOiDtEu0eMQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HiDOw/btsKxQsMOAS/fDg4hudNZoNOiDtEu0eMQk/img.png&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;844&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.1165%; margin-right: 10px;&quot; data-widthpercent=&quot;52.73&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HiDOw/btsKxQsMOAS/fDg4hudNZoNOiDtEu0eMQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHiDOw%2FbtsKxQsMOAS%2FfDg4hudNZoNOiDtEu0eMQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;995&quot; height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blQ8PE/btsKAtvu8iY/agmy5mBPEpJoSdqnuYUov0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blQ8PE/btsKAtvu8iY/agmy5mBPEpJoSdqnuYUov0/img.png&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;985&quot; data-is-animation=&quot;false&quot; style=&quot;width: 46.7207%;&quot; data-widthpercent=&quot;47.27&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blQ8PE/btsKAtvu8iY/agmy5mBPEpJoSdqnuYUov0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblQ8PE%2FbtsKAtvu8iY%2Fagmy5mBPEpJoSdqnuYUov0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1041&quot; height=&quot;985&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>eureka gateway security 연동</category>
      <category>spring cloud gateway jwt</category>
      <category>spring cloud gateway security</category>
      <category>spring cloud gateway 인증인가</category>
      <category>spring eureka gateway security</category>
      <category>spring gateway auth</category>
      <category>spring gateway setting</category>
      <category>spring gateway spring security</category>
      <category>spring gateway yml</category>
      <category>spring security gateway</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/326</guid>
      <comments>https://min-nine.tistory.com/entry/Spring-Boot-%EA%B8%B0%EB%B0%98-%EB%8B%A4%EC%88%98%EC%9D%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EC%9D%98-Multi-Module-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-3-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80%EB%8A%94-Gateway%EB%A1%9C-%ED%86%A0%ED%81%B0-%EB%B0%9C%EA%B8%89%EC%9D%80-Auth-Api%EB%A1%9C#entry326comment</comments>
      <pubDate>Thu, 7 Nov 2024 19:58:19 +0900</pubDate>
    </item>
    <item>
      <title>[Query Tuning] Spring Batch 삭제 로직 성능 최적화</title>
      <link>https://min-nine.tistory.com/entry/Spring-Batch-%EB%B0%B0%EC%B9%98%EC%9D%98-%EC%82%AD%EC%A0%9C-%EB%A1%9C%EC%A7%81-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Spring Batch를 사용하여 대량의 데이터를 삭제할 때 발생하는 성능 문제와 이를 개선한 과정을 공유하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 코드와 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 일정 시간이 지난 UserCertLog 엔티티의 데이터를 삭제하는 작업을 진행하고 있었습니다. 초기에는 다음과 같이 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730771786464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * nice 인증 결과 로그인 UserCertLog 삭제 처리
 * 매일 새벽 12시 30분에 도는 스케줄러 입니다.
 */
@Scheduled(cron = &quot;0 30 0 * * ?&quot;)
public void certLogRemove() {
    log.info(&quot;---certLogRemove start!!---&quot;);
    deleteService.deleteUserCertLog();
    log.info(&quot;---certLogRemove finish!!---&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730771768229&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void deleteUserCertLog() {
    // 현재 기준 regDateTime 값이 24시간 이전인 내용의 삭제
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    userCertLogRepository.deleteByRegDatetimeBefore(cutoffDateTime);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730771798093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserCertLogRepository extends JpaRepository&amp;lt;UserCertLog, UUID&amp;gt; {
    void deleteByRegDatetimeBefore(LocalDateTime regDatetime);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 regDatetime이 24시간 이전인 모든 데이터를 삭제하는 기능을 수행합니다. 그러나 실행해 보니 데이터가 많은 경우 삭제 속도가 매우 느렸습니다. 로그를 확인해 보니 한 번에 하나의 행(row)씩 삭제가 이루어지고 있었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서 제공하는 deleteBy... 메소드는 기본적으로 각 엔티티를 개별적으로 조회한 후 삭제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 영속성 컨텍스트를 통해 엔티티 상태를 관리하기 때문에 발생하는 현상으로, 대량의 데이터를 삭제할 때는 성능 저하를 일으키는 주요 원인입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 데이터량이 많을수록 이러한 방식은 비효율적이며, 트랜잭션 시간이 길어져 데이터베이스에 부하를 줄 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개선 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 벌크 삭제 (Bulk Delete) 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA는 JPQL 또는 네이티브 쿼리를 사용하여 벌크 연산을 지원합니다. 이를 활용하면 한 번의 쿼리로 대량의 데이터를 삭제할 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JPQL 사용&lt;/h4&gt;
&lt;pre id=&quot;code_1730772881392&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserCertLogRepository extends JpaRepository&amp;lt;UserCertLog, UUID&amp;gt; {

    @Modifying
    @Query(&quot;DELETE FROM UserCertLog u WHERE u.regDatetime &amp;lt; :cutoffDateTime&quot;)
    int deleteByRegDatetimeBefore(@Param(&quot;cutoffDateTime&quot;) LocalDateTime cutoffDateTime);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730772893565&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void deleteUserCertLog() {
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    userCertLogRepository.deleteByRegDatetimeBefore(cutoffDateTime);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;네이티브 쿼리 사용&lt;/h4&gt;
&lt;pre id=&quot;code_1730772927593&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserCertLogRepository extends JpaRepository&amp;lt;UserCertLog, UUID&amp;gt; {

    @Modifying
    @Query(value = &quot;DELETE FROM mtn_account.usr_user_cert_log WHERE reg_datetime &amp;lt; :cutoffDateTime&quot;, nativeQuery = true)
    int deleteByRegDatetimeBeforeNative(@Param(&quot;cutoffDateTime&quot;) LocalDateTime cutoffDateTime);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730772933813&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void deleteUserCertLog() {
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    userCertLogRepository.deleteByRegDatetimeBeforeNative(cutoffDateTime);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 배치(Batch) 삭제 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벌크 삭제가 아닌 배치 처리를 통해 데이터를 일정량씩 나누어 삭제할 수도 있습니다. 이 방법은 비즈니스 로직상 한 번에 모든 데이터를 삭제하면 안 되는 경우에 유용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730772978514&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void deleteUserCertLog() {
    // 현재 기준 regDateTime 값이 24시간 이전인 내용의 삭제
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    List&amp;lt;UserCertLog&amp;gt; idsToDelete = userCertLogRepository.findIdsByRegDatetimeBefore(cutoffDateTime);

    log.info(&quot;deleteUserCertLog 처리할 총인원 수 : [{}]&quot;,idsToDelete.size());

    int batchSize = 100;
    for (int i = 0; i &amp;lt; idsToDelete.size(); i += batchSize) {
        int end = Math.min(i + batchSize, idsToDelete.size());
        List&amp;lt;UserCertLog&amp;gt; batchUserCertLog = idsToDelete.subList(i, end);
        log.info(&quot;deleteUserCertLog 처리할 총인원 수 : [{}]&quot;,batchUserCertLog.size());
        log.info(&quot;batchIds : [{}]&quot;,batchUserCertLog);

        userCertLogRepository.deleteAllInBatch(batchUserCertLog);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730772984956&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;SELECT u FROM UserCertLog u WHERE (u.regDatetime &amp;lt; :cutoffDateTime or u.regDatetime is null)&quot;)
List&amp;lt;UserCertLog&amp;gt; findIdsByRegDatetimeBefore(@Param(&quot;cutoffDateTime&quot;) LocalDateTime cutoffDateTime);&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개선 후 성능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선된 코드를 적용한 후, 삭제 속도가 현저하게 빨라졌습니다. 벌크 삭제의 경우 데이터베이스에 단 한 번의 삭제 쿼리만 전달되므로, 수만 건의 데이터를 삭제하는 데도 몇 초밖에 걸리지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 삭제를 사용한 경우에도 이전의 한 행씩 삭제하던 방식에 비해 성능이 크게 향상되었으며, 필요에 따라 배치 크기를 조절하여 시스템 부하를 관리할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저의 경우 필요에 따라 배치 크기를 조절하기 위해 배치 삭제를 최종적으로 적용&lt;/b&gt;하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA를 사용할 때 대량의 데이터를 삭제해야 하는 경우, 기본적인 deleteBy... 메소드는 성능 저하를 일으킬 수 있습니다. 이때 JPQL 또는 네이티브 쿼리를 활용한 벌크 삭제를 통해 효율적으로 데이터를 삭제할 수 있습니다. 또한, 비즈니스 로직에 따라 배치 처리를 통해 삭제를 진행할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 데이터 처리 시에는 항상 성능을 고려하여 적절한 방법을 선택하는 것이 중요합니다. 이번 경험을 통해 애플리케이션의 성능을 최적화하는 방법에 대해 더욱 깊이 이해하게 되었습니다.&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>delete query 튜닝</category>
      <category>spring batch delete</category>
      <category>spring boot batch delete</category>
      <category>spring bulk delete</category>
      <category>spring 벌크 삭제</category>
      <category>spring 성능 튜닝</category>
      <category>배치 삭제</category>
      <category>벌크 삭제</category>
      <category>삭제 쿼리 튜닝</category>
      <category>스프링 성능 튜닝</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/325</guid>
      <comments>https://min-nine.tistory.com/entry/Spring-Batch-%EB%B0%B0%EC%B9%98%EC%9D%98-%EC%82%AD%EC%A0%9C-%EB%A1%9C%EC%A7%81-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94#entry325comment</comments>
      <pubDate>Tue, 5 Nov 2024 12:45:56 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 기반 다수의 프로젝트를 하나의 Multi Module 프로젝트로 통합하기 (2) - 인증과 인가를 담당하는  Auth Api</title>
      <link>https://min-nine.tistory.com/entry/Spring-Boot-%EA%B8%B0%EB%B0%98-%EB%8B%A4%EC%88%98%EC%9D%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EC%9D%98-Multi-Module-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-2-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80%EB%A5%BC-%EB%8B%B4%EB%8B%B9%ED%95%98%EB%8A%94-Auth-Api</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인증과 인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증과 인가. 많이 들어 본 말이지만 들을 때 마다 햇갈리는 단어입니다. 때문에 쉽게 생각해야 할 필요성이 있었고 저는 이와 같이 정의를 내렸습니다. &lt;span style=&quot;color: #000000; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;인증은 사용자가 자신의 신원을 확인하는 모든 행위라고 생각합니다. 인가는 인증을 통해 신원이 확인된 사용자에게 특정 액세스 권한을 부여하는 모든 행위를 뜻 한다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;더 쉽게 개발적인 이야기로 얘기하자면, 인증은 ID/PW, JWT Token 등과 같은 모든 자신의 신원을 확인하는 신분증이고 인가는 그런 인증에 대해 엑세스 권한을 체크 및 부여하는 행위라고 생각하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;저는 &lt;b&gt;인증과 인가는 Gateway에서 처리&lt;/b&gt;하고, 해당 인증과 인가에 필요한 &lt;b&gt;회원 확인 및 JWT Token 발급은 Auth Service(이하 Auth API)에서 처리&lt;/b&gt;하도록 설계하려 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 &lt;b&gt;Gateway 구현 이전에는 Auth API에서 모든 인증,인가,토큰발급 등을 관리&lt;/b&gt;하게 구현합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Auth API&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member 및 Token 관련 Entity, Repository, Service, Controller로 이뤄진 Auth API를 아래와 같이 설계하였습니다. 인증과 인가를 더욱 쉽게 만들어주는 Spring Security를 함께 사용하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1730421522006&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# module-api/build.gradle
subprojects {
    dependencies {
        implementation project(':module-core')

        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    }
}

# module-api/auth-api/build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 설정하면 module-api 모듈의 하위에 있는 모든 모듈에게 starter-web, data-jpa가 주입되어 하위 모듈에서 일일이 의존성을 주입받을 필요가 없이 꼭 필요한 것만 주입받으면 되기 때문에 gradle.build 파일이 더욱 간결해지고 제 기준에서 가독성있게 파악할 수 있게 되는 장점으로 다가왔습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1730421701672&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 18081

spring:
  application:
    name: auth-api
  profiles:
    include:
      - database

# JWT Configuration
jwt:
  access:
    secret: 9295d3232d3428c866241359d89485431868fa3c0207ab2fef5b2dff1a371ea95a9df0b74588f1d34bb6c2ce0078e39d8b9faf32c36cf97b56c09e8876007416
    expire: 86400000
  refresh:
    secret: e3545b171440dc2bddb1757e49c8b1470221d243f986a19dec44669d3c0d0f9437b4cf769756b9a2197e605e9b1924bb58faac5f578424be00ea92369fcc521d
    expire: 2592000000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt의 access secret 값과 refresh secret값은 임의로 위와같이 만들어두었지만, 실제 운영에서는 개발자도 알 수 없게 감추어두고 사용해야 한다는 것을 꼭 알아야 합니다. 저 값들로 정보를 토큰화하고, 토큰을 인증할 수 있기 때문에 아주 중요하게 다루고 관리해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UUID 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Entity를 생성할 때 BINARY(16) 형태의 UUID로 primary 컬럼을 설정하는것을 좋아합니다. 많은 장점이 있지만 대표적인 장점 몇가지를 적어보겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;nbsp;저장 공간 효율성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일반적으로 UUID는 CHAR(36) 형식으로 저장되며, 36바이트를 차지합니다. BINARY(16)로 저장할 경우 16바이트만 사용하므로, 공간 효율성이 약 56% 향상됩니다.&lt;/li&gt;
&lt;li&gt;대량의 데이터를 저장하는 테이블에서 Primary Key로 BINARY(16) UUID를 사용하면, 인덱스 크기가 줄어들어 메모리 및 디스크 사용량을 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터베이스 성능 향상
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;BINARY(16)&amp;nbsp;UUID는&amp;nbsp;숫자&amp;nbsp;기반으로&amp;nbsp;정렬이&amp;nbsp;더&amp;nbsp;효율적이며,&amp;nbsp;인덱스에서도&amp;nbsp;빠른&amp;nbsp;조회&amp;nbsp;성능을&amp;nbsp;제공합니다.&lt;br /&gt;UUID의 난수 특성상 대량의 데이터가 쌓일 경우 인덱스의 균형을 유지하는 데 도움이 되며, 삽입 시 충돌이 적어 B-Tree 인덱스의 성능 저하를 줄여줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보안 및 추적성 강화
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;UUID는&amp;nbsp;일반적인&amp;nbsp;Auto&amp;nbsp;Increment&amp;nbsp;ID와&amp;nbsp;달리&amp;nbsp;추측하기&amp;nbsp;어려우며,&amp;nbsp;외부에&amp;nbsp;노출되더라도&amp;nbsp;의미를&amp;nbsp;유추할&amp;nbsp;수&amp;nbsp;없습니다.&lt;br /&gt;BINARY 형식의 UUID는 예측이 어렵고, 각 레코드의 고유성이 보장되므로 보안성을 높일 수 있습니다. 다중 시스템 간 통합 시에도 각 시스템의 고유성을 유지할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다중 시스템 간 데이터 통합에 유리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;분산&amp;nbsp;시스템이나&amp;nbsp;마이크로서비스&amp;nbsp;아키텍처에서는&amp;nbsp;각&amp;nbsp;서비스가&amp;nbsp;고유한&amp;nbsp;UUID를&amp;nbsp;생성하여&amp;nbsp;데이터를&amp;nbsp;저장하는&amp;nbsp;것이&amp;nbsp;중요합니다.&lt;br /&gt;BINARY(16) UUID는 이러한 고유성을 보장하면서도 빠른 전송과 최소한의 네트워크 대역폭 사용을 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;간편한 인덱스 유지 관리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;BINARY(16)&amp;nbsp;UUID는&amp;nbsp;일반적인&amp;nbsp;텍스트&amp;nbsp;형식보다&amp;nbsp;작고&amp;nbsp;정렬이&amp;nbsp;용이하기&amp;nbsp;때문에,&amp;nbsp;대규모&amp;nbsp;인덱스에서&amp;nbsp;관리가&amp;nbsp;더&amp;nbsp;수월합니다.&lt;br /&gt;자주&amp;nbsp;삽입되고&amp;nbsp;조회되는&amp;nbsp;테이블에서는&amp;nbsp;인덱스의&amp;nbsp;크기를&amp;nbsp;줄이고&amp;nbsp;데이터베이스의&amp;nbsp;캐시&amp;nbsp;사용을&amp;nbsp;효율화할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Entity 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간혹 Entity를 생성할 때 Setter를 만들어 사용하시는 분들이 계실던데 저는 프로그래머 간의 실수가 자주 일어나게 되고 원치 않는 값이 들어가는 등의 이유로 Mapper 사용을 좋아합니다. 이 점 참고하여 이 포스팅을 토대로 구현하고 계시는 분들 중 Setter를 사용해야 하시는 분들은 입맞에 맞게 수정해서 사용하시면 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Member Entity&lt;/h4&gt;
&lt;pre id=&quot;code_1730422424742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@Table(name = &quot;members&quot;)
@NoArgsConstructor
public class Member extends DefaultTime {

    @Id
    @UuidGenerator(style = UuidGenerator.Style.TIME)
    @GeneratedValue(generator = &quot;uuid2&quot;)
    @Column(columnDefinition = &quot;BINARY(16)&quot;)
    private UUID id;
    @Column(columnDefinition = &quot;varchar(100)&quot;, nullable = false)
    private String name;
    @Column(columnDefinition = &quot;varchar(100)&quot;, nullable = false, unique = true)
    private String email;
    @Column(columnDefinition = &quot;varchar(100)&quot;, nullable = false)
    private String password;
    @Column(columnDefinition = &quot;varchar(100)&quot;, nullable = false, unique = true)
    private String phone;
    @Column(columnDefinition = &quot;varchar(300)&quot;, nullable = false)
    private String address;
    @Enumerated(EnumType.STRING)
    @ColumnDefault(&quot;'USER'&quot;)
    @Column(nullable = false)
    private Authority role;
    @ColumnDefault(&quot;false&quot;)
    @Column(columnDefinition = &quot;TINYINT(1)&quot;, nullable = false)
    private boolean isActive;

    public Member(String name, String email, String password, String phone, String address) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.phone = phone;
        this.address = address;
        this.role = Authority.USER;
    }

    public void update(UpdateMemberRequestDto updateMemberRequestDto) {
        this.name = updateMemberRequestDto.getName();
        this.password = updateMemberRequestDto.getPassword();
        this.phone = updateMemberRequestDto.getPhone();
        this.address = updateMemberRequestDto.getAddress();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Token Entity&lt;/h4&gt;
&lt;pre id=&quot;code_1730423133114&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;
@Getter
@Entity
@Builder
@Table(name = &quot;tokens&quot;)
@NoArgsConstructor
@AllArgsConstructor
public class Token extends DefaultTime {
    @Id
    @UuidGenerator(style = UuidGenerator.Style.TIME)
    @GeneratedValue(generator = &quot;uuid2&quot;)
    @Column(columnDefinition = &quot;BINARY(16)&quot;)
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @Column(columnDefinition = &quot;varchar(500)&quot;, nullable = false)
    private String accessToken;

    @Column(columnDefinition = &quot;varchar(500)&quot;, nullable = false)
    private String refreshToken;

    @Column(nullable = false)
    private String grantType;

    public Token update(String accessToken) {
        this.accessToken = accessToken;
        return this;
    }

    public Token update(String accessToken, String refreshToken, String grantType) {
        return Token.builder()
                .id(this.id)
                .member(this.member)
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .grantType(grantType)
                .build();
    }

    public Token update(UUID id, Member member, String accessToken, String refreshToken, String grantType) {
        return Token.builder()
                .id(id)
                .member(member)
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .grantType(grantType)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우 위에 언급 한 것 처럼 Entity를 생성할 때 Setter 패턴은 사용하지 않지만, 꼭 필요할 경우 Builder패턴을 사용하여 생성할 수 있도록 개발하는 편입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT Token생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 토큰 및 Request에서 토큰을 추출하여 인증하는 Filter 등 JWT 관련 Class를 생성합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JwtTokenizer&lt;/h4&gt;
&lt;pre id=&quot;code_1730423411856&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class JwtTokenizer {

    private static String  accessSecret;
    private static String refreshSecret;
    public static Long accessTokenExpire;
    public static Long refreshTokenExpire;

    @Value(&quot;${jwt.access.secret}&quot;)
    private String  accessSecretValue;
    @Value(&quot;${jwt.refresh.secret}&quot;)
    private String refreshSecretValue;
    @Value(&quot;${jwt.access.expire}&quot;)
    public Long accessTokenExpireValue;
    @Value(&quot;${jwt.refresh.expire}&quot;)
    public Long refreshTokenExpireValue;

    @PostConstruct
    public void init() {
        accessSecret = accessSecretValue;
        refreshSecret = refreshSecretValue;
        accessTokenExpire = accessTokenExpireValue;
        refreshTokenExpire = refreshTokenExpireValue;
    }

    /**
     * AccessToken 생성
     *
     * @param id
     * @param email
     * @param name
     * @param authority
     * @return AccessToken
     */
    public String createAccessToken(UUID id, String email, String name, Authority authority) {
        return createToken(id, email, name, authority, accessTokenExpire, accessSecret);
    }

    /**
     * RefreshToken 생성
     *
     * @param id
     * @param email
     * @param name
     * @param authority
     * @return RefreshToken
     */
    public String createRefreshToken(UUID id, String email, String name, Authority authority) {
        return createToken(id, email, name, authority, refreshTokenExpire, refreshSecret);
    }

    /**
     * Jwts 빌더를 사용하여 token 생성
     *
     * @param id
     * @param email
     * @param name
     * @param authority
     * @param expire
     * @param secretKey
     * @return
     */
    private String createToken(UUID id, String email, String name, Authority authority, Long expire, String secretKey) {
        byte[] secretByte = getSecretByte(secretKey);


        Claims claims = Jwts.claims().setSubject(email)
                .add(&quot;authority&quot;, authority)
                .add(&quot;id&quot;, id)
                .add(&quot;name&quot;, name)
                .build();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(new Date(new Date().getTime() + expire))
                .signWith(getSigningKey(secretByte))
                .compact();
    }

    /**
     * 토큰에서 유저 아이디 얻기
     *
     * @param token 토큰
     * @return userId
     */
    public String getUserIdFromToken(String token) {
        String[] tokenArr = token.split(&quot; &quot;);
        token = tokenArr[1];
        Claims claims = parseToken(token, getSecretByte(accessSecret));
        return claims.get(&quot;id&quot;) == null ? null : claims.get(&quot;id&quot;).toString();
    }

    public Claims parseAccessToken(String accessToken) {
        return parseToken(accessToken, getSecretByte(accessSecret));
    }

    public Claims parseRefreshToken(String refreshToken) {
        return parseToken(refreshToken, getSecretByte(refreshSecret));
    }

    public Claims parseToken(String token, byte[] secretKey) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(getSigningKey(secretKey))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

        } catch (SignatureException e) { // 토큰 유효성 체크 실패 시
            throw new PlayGroundCommonException(PlayGroundErrorCode.JWT_INVALID_ERROR.getCode(), PlayGroundErrorCode.JWT_INVALID_ERROR.getMessage());
        }

        return claims;
    }

    /**
     * @param secretKey - byte형식
     * @return Key 형식 시크릿 키
     */
    public static Key getSigningKey(byte[] secretKey) {
        return Keys.hmacShaKeyFor(secretKey);
    }

    private static byte[] getSecretByte(String secret) {
        return secret.getBytes(StandardCharsets.UTF_8);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Class는 JWT Token을 발행 및 인증하는 중요한 역할을 담당합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ToeknAuthenticationFilter&lt;/h4&gt;
&lt;pre id=&quot;code_1730423523751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenizer jwtTokenizer;

    /**
     * 필터 메서드
     * 각 요청마다 JWT 토큰을 검증하고 인증을 설정
     *
     * @param request     요청 객체
     * @param response    응답 객체
     * @param filterChain 필터 체인
     * @throws ServletException 서블릿 예외
     * @throws IOException      입출력 예외
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getToken(request); // 요청에서 토큰을 추출

        if (StringUtils.hasText(token)) {
            try {
                // 토큰을 사용하여 인증 설정
                getAuthentication(token);
            } catch (ExpiredJwtException e) { // 토큰 만료 시
                request.setAttribute(&quot;exception&quot;, &quot;EXPIRED_TOKEN&quot;);
                log.error(&quot;Expired Token : {}&quot;, token, e);
                throw new PlayGroundCommonException(PlayGroundErrorCode.JWT_EXPIRED_ERROR.getCode(), PlayGroundErrorCode.JWT_EXPIRED_ERROR.getMessage(), e);
            } catch (UnsupportedJwtException e) { // 지원하지 않는 토큰 사용 시
                request.setAttribute(&quot;exception&quot;, &quot;UNSUPPORTED_TOKEN&quot;);
                log.error(&quot;Unsupported Token: {}&quot;, token, e);
                throw new PlayGroundCommonException(PlayGroundErrorCode.JWT_UNSUPPORTED_ERROR.getCode(), PlayGroundErrorCode.JWT_UNSUPPORTED_ERROR.getMessage(), e);
            } catch (MalformedJwtException e) { // 유효하지 않은 토큰 사용 시
                request.setAttribute(&quot;exception&quot;, &quot;INVALID_TOKEN&quot;);
                log.error(&quot;Invalid Token: {}&quot;, token, e);
                throw new PlayGroundCommonException(PlayGroundErrorCode.JWT_INVALID_ERROR.getCode(), PlayGroundErrorCode.JWT_INVALID_ERROR.getMessage(), e);
            } catch (IllegalArgumentException e) { // 올바르지 않은 파라미터 전달 시
                request.setAttribute(&quot;exception&quot;, &quot;NOT_FOUND_TOKEN&quot;);
                log.error(&quot;Token not found: {}&quot;, token, e);
                throw new PlayGroundCommonException(PlayGroundErrorCode.JWT_TOKEN_NOT_FOUND.getCode(), PlayGroundErrorCode.JWT_TOKEN_NOT_FOUND.getMessage(), e);
            } catch (Exception e) { // 알 수 없는 예외 발생 시
                request.setAttribute(&quot;exception&quot;, &quot;NOT_FOUND_TOKEN&quot;);
                log.error(&quot;JWT Filter - Internal Error: {}&quot;, token, e);
                throw new PlayGroundCommonException(PlayGroundErrorCode.JWT_COMMON_ERROR.getCode(), PlayGroundErrorCode.JWT_COMMON_ERROR.getMessage(), e);
            }
        }

        filterChain.doFilter(request, response); // 다음 필터로 요청을 전달
    }

    /**
     * 토큰을 사용하여 인증 설정
     *
     * @param token JWT 토큰
     */
    private void getAuthentication(String token) {
        Claims claims = jwtTokenizer.parseAccessToken(token); // 토큰에서 클레임을 파싱
        String email = claims.getSubject(); // 이메일을 가져옴
        String id = claims.get(&quot;id&quot;, String.class); // 사용자 ID를 가져옴
        String name = claims.get(&quot;name&quot;, String.class); // 이름을 가져옴
        Authority authority = Authority.valueOf(claims.get(&quot;authority&quot;, String.class)); // 사용자 권한을 가져옴

        Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities = Collections.singletonList(authority);

        CustomUserDetails userDetails = new CustomUserDetails(email, &quot;&quot;, (List&amp;lt;GrantedAuthority&amp;gt;) authorities);
        Authentication authentication = new JwtAuthenticationToken(authorities, userDetails, null); // 인증 객체 생성
        SecurityContextHolder.getContext().setAuthentication(authentication); // SecurityContextHolder에 인증 객체 설정
    }

    /**
     * 요청에서 토큰을 추출
     *
     * @param request 요청 객체
     * @return JWT 토큰
     */
    private String getToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies(); // 쿠키에서 토큰을 찾음

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (&quot;accessToken&quot;.equals(cookie.getName())) {
                    return cookie.getValue(); // accessToken 쿠키에서 토큰 반환
                }
            }
        }

        return null; // 토큰을 찾지 못한 경우 null 반환
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Class는 Spring Security에서 Filter Chain 방식으로 사용합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Service Class&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service Class를 활용하는 방법은 개발자별로 천지차이입니다. 혹자는 Repository 사용법을 그대로 사용하되 여러 Repository를 묶기 위해 Service Class를 활용하기도 하고, 저처럼 Controller를 목차처럼 사용하기 위해 Service Class에서 비지니스로직을 구현하는 개발자도 있습니다. 취향에 맞게 개발하시면 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AuthService&lt;/h4&gt;
&lt;pre id=&quot;code_1730424089024&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class AuthService {

    private final PasswordEncoder passwordEncoder;
    private final JwtTokenizer jwtTokenizer;
    private final TokenService tokenService;
    private final MemberRepository memberRepository;

    public AuthLoginResponseDto login(AuthLoginRequestDto authLoginRequestDto) {
        // 인증 처리
        Member member = this.checkAuthentication(authLoginRequestDto);
        // 토큰 생성 및 저장
        Token token = this.getToken(member);

        return AuthLoginResponseDto.builder()
                .accessToken(token.getAccessToken())
                .refreshToken(token.getRefreshToken())
                .email(member.getEmail())
                .name(member.getName())
                .build();
    }

    /**
     * 인증 처리
     * @param authLoginRequestDto AuthLoginRequestDto
     * @return Member
     */
    private Member checkAuthentication(AuthLoginRequestDto authLoginRequestDto) {
        Member member = memberRepository.findByEmail(authLoginRequestDto.getEmail())
                .orElseThrow(() -&amp;gt; new PlayGroundCommonException(PlayGroundErrorCode.AUTH_INVALID.getCode(), PlayGroundErrorCode.AUTH_INVALID.getMessage()));

        if (member == null) {
            throw new PlayGroundCommonException(PlayGroundErrorCode.AUTH_INVALID.getCode(), PlayGroundErrorCode.AUTH_INVALID.getMessage());
        }

        // 비밀번호 일치 여부 체크
        if (!passwordEncoder.matches(authLoginRequestDto.getPassword(), member.getPassword())) {
            throw new PlayGroundCommonException(PlayGroundErrorCode.AUTH_PASSWORD_MISMATCH.getCode(), PlayGroundErrorCode.AUTH_PASSWORD_MISMATCH.getMessage());
        }

        return member;
    }

    /**
     * 토큰 발급
     * @param member Member
     * @return Token
     */
    private Token getToken (Member member) {

        // 토큰 발급
        String accessToken = jwtTokenizer.createAccessToken(member.getId(), member.getEmail(), member.getName(), member.getRole());
        String refreshToken = jwtTokenizer.createRefreshToken(member.getId(), member.getEmail(), member.getName(), member.getRole());

        // 토큰 저장
        Token token = Token.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .member(member)
                .grantType(&quot;Bearer&quot;)
                .build();
        tokenService.saveOrUpdate(token);

        return token;
    }

    /**
     * 회원가입
     * @param authSignUpRequestDto AuthSignUpRequestDto
     */
    public void signup(AuthSignUpRequestDto authSignUpRequestDto) {
        if (memberRepository.existsByEmail(authSignUpRequestDto.getEmail())) {
            throw new PlayGroundCommonException(PlayGroundErrorCode.COMMON_ALREADY_EXISTS.getCode(), PlayGroundErrorCode.COMMON_ALREADY_EXISTS.getMessage());
        }

        if (memberRepository.existsByPhone(authSignUpRequestDto.getPhone())) {
            throw new PlayGroundCommonException(PlayGroundErrorCode.COMMON_ALREADY_EXISTS.getCode(), PlayGroundErrorCode.COMMON_ALREADY_EXISTS.getMessage());
        }

        authSignUpRequestDto.setPassword(passwordEncoder.encode(authSignUpRequestDto.getPassword()));
        memberRepository.save(authSignUpRequestDto.toEntity());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Token Service&lt;/h4&gt;
&lt;pre id=&quot;code_1730425046556&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TokenService {

    private final TokenRepository tokenRepository;

    /**
     * 토큰 사용자의 토큰이 저장되어 있을 경우 update, 없을 경우에는 create
     */
    @Transactional
    public void saveOrUpdate(Token token) {
        Token fineToken = tokenRepository.findByMemberId(token.getMember().getId());

        if (fineToken == null) {
            tokenRepository.save(token);
        } else {
            Token saveToken = token.update(
                    fineToken.getId(),
                    fineToken.getMember(),
                    token.getAccessToken(),
                    token.getRefreshToken(),
                    token.getGrantType()
            );
            tokenRepository.save(saveToken);
        }
    }

    /**
     * access token으로 Token 데이터를 가져와 삭제하는 메소드
     *
     * @param token
     */
    @Transactional
    public void deleteByAccessToken(String token) {
        tokenRepository.findByAccessToken(token).ifPresent(tokenRepository::delete);
    }

    /**
     * access token으로 Token 데이터를 가져오는 메소드
     *
     * @param token access token
     * @return Token 데이터
     */
    @Transactional(readOnly = true)
    public Optional&amp;lt;Token&amp;gt; findByAccessToken(String token) {
        return tokenRepository.findByAccessToken(token);
    }

    /**
     * refresh token으로 Token 데이터를 가져오는 메소드
     *
     * @param token refresh token
     * @return Token 데이터
     */
    @Transactional(readOnly = true)
    public Optional&amp;lt;Token&amp;gt; findByRefreshToken(String token) {
        return tokenRepository.findByRefreshToken(token);
    }

    /**
     * refresh token으로 Token 데이터를 가져와 access token 값을 업데이트하는 메소드
     *
     * @param refreshToken
     * @param accessToken
     */
    @Transactional
    public void updateByRefreshToken(String refreshToken, String accessToken) {
        Token token = tokenRepository.findByRefreshToken(refreshToken)
                .orElseThrow(() -&amp;gt; new PlayGroundCommonException(PlayGroundErrorCode.JWT_TOKEN_NOT_FOUND.getCode(), PlayGroundErrorCode.JWT_TOKEN_NOT_FOUND.getMessage()));

        token = token.update(accessToken);
        tokenRepository.save(token);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style4&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Controller Class&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Auth기능의 RestAPI를 구현한 RestController인 AuthController를 구현해주면 Auth API는 완료됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730425202456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Validated
@Tag(name = &quot;Auth&quot;, description = &quot;Auth 관련 Controller 입니다. 로그인 및 로그아웃, 회원가입을 담당합니다.&quot;)
@RestController
@RequestMapping(&quot;/api/v1/auth&quot;)
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;
    private final TokenService tokenService;

    @Operation(summary = &quot;로그인&quot;, description = &quot;회원 아이디와 비밀번호를 가지고 로그인 후 access 토큰을 발급합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;OK&quot;,
                    content = @Content(schema = @Schema(implementation = PlayGroundResponse.class))),
            @ApiResponse(responseCode = &quot;500&quot;, description = &quot;INTERNAL SERVER ERROR&quot;)
    })
    @Description(&quot;Login&quot;)
    @PostMapping(&quot;/login&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; login(@Valid @RequestBody AuthLoginRequestDto authLoginRequestDto, HttpServletResponse httpServletResponse) {
        // 인증처리
        AuthLoginResponseDto authLoginResponseDto = authService.login(authLoginRequestDto);

        // Token 쿠키 저장
        Cookie accessTokenCookie = new Cookie(&quot;accessToken&quot;, authLoginResponseDto.getAccessToken());
        accessTokenCookie.setHttpOnly(true);
        accessTokenCookie.setPath(&quot;/&quot;);
        accessTokenCookie.setMaxAge(Math.toIntExact(JwtTokenizer.accessTokenExpire / 1000));
        httpServletResponse.addCookie(accessTokenCookie);

        Cookie refreshTokenCookie = new Cookie(&quot;refreshToken&quot;, authLoginResponseDto.getRefreshToken());
        refreshTokenCookie.setHttpOnly(true);
        refreshTokenCookie.setPath(&quot;/&quot;);
        refreshTokenCookie.setMaxAge(Math.toIntExact(JwtTokenizer.refreshTokenExpire / 1000));
        httpServletResponse.addCookie(refreshTokenCookie);

        return PlayGroundResponse.build(authLoginResponseDto);
    }

    @Operation(summary = &quot;로그아웃&quot;, description = &quot;로그아웃 처리를 합니다. access token과 refresh token을 삭제합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;OK&quot;),
            @ApiResponse(responseCode = &quot;500&quot;, description = &quot;INTERNAL SERVER ERROR&quot;)
    })
    @Description(&quot;Logout&quot;)
    @DeleteMapping(&quot;/logout&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        String accessToken = null;

        // access / refresh Token cookie 삭제
        Cookie[] cookies = httpServletRequest.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                switch (cookie.getName()) {
                    case &quot;accessToken&quot;:
                        accessToken = cookie.getValue();
                    case &quot;refreshToken&quot;:
                        cookie.setValue(&quot;&quot;);
                        cookie.setPath(&quot;/&quot;);
                        cookie.setMaxAge(0);
                        httpServletResponse.addCookie(cookie);
                        break;
                }
            }
        }

        // tokens 데이터 삭제
        tokenService.deleteByAccessToken(accessToken);
        return PlayGroundResponse.ok();
    }

    @Operation(summary = &quot;회원가입&quot;, description = &quot;회원가입을 진행합니다.&quot;)
    @ApiResponses({
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;OK&quot;,
                    content = @Content(schema = @Schema(implementation = PlayGroundResponse.class))),
            @ApiResponse(responseCode = &quot;500&quot;, description = &quot;INTERNAL SERVER ERROR&quot;)
    })
    @PostMapping(&quot;/signup&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; signup(@Valid @RequestBody AuthSignUpRequestDto authSignUpRequestDto) {
        authService.signup(authSignUpRequestDto);
        return PlayGroundResponse.ok();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Srping Security 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티 5.7버전 이후로는 &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;WebSecurityConfigurerAdapter&lt;/span&gt; 방식에서 Filter Chain 방식으로 바뀌었습니다. 때문에 저는 글 작성 기준 최신 버전인 6.3.3 버전을 사용하여 Fillter chain방식으로 Spring Security Config를 구현하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebSecurityConfig&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기능들에 대해서는 주석으로 설명을 다뤄두었으니 참고하여주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1730425826239&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    // All Allowed Pages
    String[] allAllowPages = {
            &quot;/swagger-ui/**&quot;,     // Swagger UI 관련 리소스
            &quot;/v3/api-docs/**&quot;,     // Swagger API 문서 리소스
            &quot;/swagger-resources/**&quot; // Swagger 추가 리소스
    };

    // Un Login User Allowed Pages
    String[] unLoginUserAllowedPages = {
            &quot;/api/v1/auth/login&quot;, // 로그인 API,
            &quot;/api/v1/auth/signup&quot;, // 회원가입 API
    };

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, JwtTokenizer jwtTokenizer) throws Exception {
        httpSecurity
                .cors(Customizer.withDefaults()) // cors 설정
                .csrf(AbstractHttpConfigurer::disable); // csrf 비활성화

        // Session 미사용
        httpSecurity.sessionManagement((session) -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // 로그인 폼 비활성화
        httpSecurity.formLogin(AbstractHttpConfigurer::disable);
        // http 기본 인증(헤더) 비활성화
        httpSecurity.httpBasic(AbstractHttpConfigurer::disable);

        // 요청 URI별 권한 설정
        httpSecurity.authorizeHttpRequests((authorize) -&amp;gt; authorize
                // 전체 접근 허용
                .requestMatchers(allAllowPages).permitAll()
                // 로그인하지 않은 사용자 접근 허용
                .requestMatchers(unLoginUserAllowedPages).permitAll()
                // 이외의 모든 요청은 인증 정보 필요
                .anyRequest().authenticated()
        );

        // JWT 필터 사용
        httpSecurity.addFilter(this.corsFilter());
        httpSecurity.addFilterBefore(new TokenAuthenticationFilter(jwtTokenizer), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.addAllowedHeader(&quot;Authorization&quot;);
        config.addAllowedHeader(&quot;access_token_key&quot;);
        config.addAllowedHeader(&quot;X-Requested-With&quot;);
        config.addAllowedHeader(&quot;Server Authorization&quot;);
        config.addAllowedHeader(&quot;Content-Type&quot;);
        config.addAllowedHeader(&quot;Content-Length&quot;);
        config.addAllowedHeader(&quot;Cache-Control&quot;);

        config.addAllowedMethod(&quot;GET&quot;);
        config.addAllowedMethod(&quot;POST&quot;);
        config.addAllowedMethod(&quot;PUT&quot;);
        config.addAllowedMethod(&quot;DELETE&quot;);
        config.addAllowedMethod(&quot;OPTIONS&quot;);
        config.addAllowedMethod(&quot;HEAD&quot;);

        config.addAllowedOriginPattern(&quot;*&quot;);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, config);

        return new CorsFilter(source);
    }

    @Bean
    public StrictHttpFirewall allowUrlEncodedDoubleSlashHttpFirewall() {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        // 더블슬레시 요청 허용
        firewall.setAllowUrlEncodedDoubleSlash(true);
        return firewall;
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -&amp;gt; web.httpFirewall(allowUrlEncodedDoubleSlashHttpFirewall());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 로그인 및 회원가입을 제외한 모든 요청은 인증 및 인가처리가 되어야 실행될 수 있도록 설계하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. HTTP Test&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ의 Request Http 기능을 사용하여 만들어진 내용에 대해 테스트를 진행합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Login Test&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WfyGv/btsKsaqfSfV/kDMm8sxgF43Qj0cdMJoDT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WfyGv/btsKsaqfSfV/kDMm8sxgF43Qj0cdMJoDT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WfyGv/btsKsaqfSfV/kDMm8sxgF43Qj0cdMJoDT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWfyGv%2FbtsKsaqfSfV%2FkDMm8sxgF43Qj0cdMJoDT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1332&quot; height=&quot;842&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;LogOut 테스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;security에서 설정한 것과 같이 logout은 인증 인가가 통과된 token을 사용할 때만 사용이 가능하도록 처리해두었습니다. 때문에 로그인 없이 테스트를 진행하면 아래와 같이 403 오류가 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KWUNk/btsKsvt6XX6/kq7SBnjzrvznmy2EpQw6FK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KWUNk/btsKsvt6XX6/kq7SBnjzrvznmy2EpQw6FK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KWUNk/btsKsvt6XX6/kq7SBnjzrvznmy2EpQw6FK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKWUNk%2FbtsKsvt6XX6%2Fkq7SBnjzrvznmy2EpQw6FK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;815&quot; height=&quot;736&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 로그인 후에 로그아웃 테스트를 진행하면 정상적인 로그아웃 처리가 완료된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gkhmz/btsKtm4gTGH/KEQLleCih9gtcbxy2cPa10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gkhmz/btsKtm4gTGH/KEQLleCih9gtcbxy2cPa10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gkhmz/btsKtm4gTGH/KEQLleCih9gtcbxy2cPa10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGkhmz%2FbtsKtm4gTGH%2FKEQLleCih9gtcbxy2cPa10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;714&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>multi module security</category>
      <category>spring cloud security</category>
      <category>spring multi module auth 구현</category>
      <category>spring multi module security</category>
      <category>spring security 6 예제</category>
      <category>Spring Security 구현</category>
      <category>spring security 예제</category>
      <category>인증과 인가를 담당하는  auth api</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/324</guid>
      <comments>https://min-nine.tistory.com/entry/Spring-Boot-%EA%B8%B0%EB%B0%98-%EB%8B%A4%EC%88%98%EC%9D%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EC%9D%98-Multi-Module-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-2-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80%EB%A5%BC-%EB%8B%B4%EB%8B%B9%ED%95%98%EB%8A%94-Auth-Api#entry324comment</comments>
      <pubDate>Fri, 1 Nov 2024 20:54:55 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 기반 다수의 프로젝트를 하나의 Multi Module 프로젝트로 통합하기 (1) - 프로젝트를 시작하며</title>
      <link>https://min-nine.tistory.com/entry/Spring-Boot-%EA%B8%B0%EB%B0%98-%EB%8B%A4%EC%88%98%EC%9D%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EC%9D%98-Multi-Module-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 모듈 프로젝트는 쉽게 말해, 하나의 큰 프로젝트 아래에 여러 기능 혹은 프로젝트를 모듈화하여 관리하는 방식을 말합니다. 각 회사마다 다르겠지만, 대부분의 회사에서는 여러 서비스들을 개발 및 운영하고, 같은 기능(인증 및 인가 등)이나 같은 형식의 데이터베이스 엔티티를 사용하다 보니 회사 규모가 커지면 커질수록 더 많은 리포지토리가 생겨나고, 같은 코드와 같은 기능들이 중복되기 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우에는 동시다발적으로 진행되는 프로젝트 및 유지보수 운영 때문에 IDE에 열려있는 프로젝트는 점점 많아지고 개발자들 간의 소통 또한 어려워졌으며, 코드 컨벤션(Code Convention)들도 어긋나게 되는 상황이 발생하였습니다. 이를 해결하기 위해 같은 기능을 담당하는 코드들을 통합하는 기회를 만들 겸 멀티 모듈 프로젝트를 시작하게 되었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 모듈 (Module)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 학생 시절 모듈의 &lt;b&gt;높은 응집도와 낮은 결합도&lt;/b&gt;가 좋은 품질의 소프트웨어를 만드는 데 기여한다는 것을 배웠습니다. 다들 한 번쯤 들어봤을 &quot;우논시절통순기 내공외제스자&quot;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭣도 모르는 시절에 외웠던 그 단어들이 멀티 모듈 프로젝트를 진행해보며 현실적으로 와닿았습니다. 모듈과 모듈 간의 의존 정도를 얘기하는 &lt;b&gt;결합도&lt;/b&gt;와 모듈 내 구성 요소들 간의 연관 정도를 의미하는 &lt;b&gt;응집도&lt;/b&gt;. 낮은 결합도와 높은 응집도는 좋은 모듈을 만드는 하나의 마법의 주문과 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 모듈 프로젝트를 진행하며 각 모듈은 이름에 걸맞게 최소한의 기능만 담당하도록 만들고 싶었고, 공통 모듈을 다른 모듈에서 원활하게 사용하기 위해 &lt;b&gt;의존성(dependency)&lt;/b&gt;을 적절히 활용하였으며, 비즈니스 로직을 담당하는 모듈끼리는 &lt;b&gt;Spring Cloud OpenFeign&lt;/b&gt;을 사용하여 통신하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API들의 모든 인증과 인가 처리를 한곳으로 묶기 위해 &lt;b&gt;Spring Cloud Gateway&lt;/b&gt;를 사용하였고, 각 모듈들이 실행되어 만들어지는 애플리케이션을 관리하기 위해 &lt;b&gt;Spring Cloud Eureka&lt;/b&gt;를 함께 도입하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1730275261741&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Cloud OpenFeign&quot; data-og-description=&quot;Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable &quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/&quot; data-og-url=&quot;https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud OpenFeign&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1730275241061&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Cloud Netflix&quot; data-og-description=&quot;This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few simple annotations you can quickly enable and configure the common pat&quot; data-og-host=&quot;cloud.spring.io&quot; data-og-source-url=&quot;https://cloud.spring.io/spring-cloud-netflix/reference/html/&quot; data-og-url=&quot;https://cloud.spring.io/spring-cloud-netflix/reference/html/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://cloud.spring.io/spring-cloud-netflix/reference/html/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cloud.spring.io/spring-cloud-netflix/reference/html/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Netflix&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few simple annotations you can quickly enable and configure the common pat&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cloud.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1730275068314&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Cloud Gateway&quot; data-og-description=&quot;This project provides a libraries for building an API Gateway on top of Spring WebFlux or Spring WebMVC. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitor&quot; data-og-host=&quot;spring.io&quot; data-og-source-url=&quot;https://spring.io/projects/spring-cloud-gateway&quot; data-og-url=&quot;https://spring.io/projects/spring-cloud-gateway&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cE7zif/hyXpzRzdT9/vIfeupDmutVNevBIYY8bQK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/coyOQp/hyXpyLTXez/RwEU6cuh03K4tQavQNvcW0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://spring.io/projects/spring-cloud-gateway&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://spring.io/projects/spring-cloud-gateway&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cE7zif/hyXpzRzdT9/vIfeupDmutVNevBIYY8bQK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/coyOQp/hyXpyLTXez/RwEU6cuh03K4tQavQNvcW0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Gateway&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This project provides a libraries for building an API Gateway on top of Spring WebFlux or Spring WebMVC. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitor&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 아키텍처 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 설계는 정말로 어려운 과정입니다. 제가 좋아하는 웹툰 **'화산귀환'**의 주인공 &lt;b&gt;청명&lt;/b&gt;은 이렇게 말했습니다. &quot;&lt;b&gt;바닥이 얼마나 튼튼한지에 따라 높은 탑을 쌓을 수 있느냐가 결정되지.&lt;/b&gt;&quot; 이는 소프트웨어 개발에서도 기본적인 설계의 중요성을 잘 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부족한 CS 지식으로 만든 설계는 프로젝트 중반에도 개발자들을 곤란하게 만들 수 있다고 생각합니다. 그래서 세밀한 설계가 아니더라도 큰 틀에서의 올바른 설계는 매우 중요하다고 느꼈고, 실제 프로젝트를 진행하기 전에 충분한 &lt;b&gt;학습&lt;/b&gt;과 &lt;b&gt;연구&lt;/b&gt;에 몰두했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재의 아키텍처는 &lt;b&gt;A, B, C, D&lt;/b&gt;라는 REST API 프로젝트와 &lt;b&gt;E&lt;/b&gt;라는 RabbitMQ 기반의 Notification 프로젝트로 구성되어 있습니다. 각 프로젝트는 &lt;b&gt;Redis&lt;/b&gt;와 &lt;b&gt;RDB&lt;/b&gt;를 사용하고 있으며, A와 C 프로젝트는 &lt;b&gt;JWT 기반의 인증 및 인가&lt;/b&gt;를, B와 D 프로젝트는 &lt;b&gt;세션 기반의 인증 및 인가&lt;/b&gt;를 사용하고 있었습니다. 이러한 인증 방식의 일관성 부족은 유지보수와 확장성 측면에서 문제를 일으킬 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 인증 및 인가 방식을 통합하기로 결정했습니다. &lt;b&gt;Spring Security&lt;/b&gt;와 &lt;b&gt;JWT&lt;/b&gt;를 활용하여 &lt;b&gt;Auth 모듈&lt;/b&gt;을 구현하고, &lt;b&gt;Spring Cloud Gateway&lt;/b&gt;에서 인증과 인가를 처리하기 위한 &lt;b&gt;필터&lt;/b&gt;를 적용하였습니다. 이렇게 함으로써 모든 인증 및 인가 로직은 Gateway와 Auth 모듈에서 중앙 집중적으로 관리되며, 토큰의 발행과 검증은 Auth 모듈이 담당하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 공통적으로 사용되는 Redis를 하나의 &lt;b&gt;Redis 모듈&lt;/b&gt;로 분리하여 필요로 하는 모듈에서 의존성으로 포함할 수 있도록 하였습니다. 공통적으로 사용되는 데이터베이스 엔티티나 유틸리티 클래스들은 &lt;b&gt;Core 모듈&lt;/b&gt;로 분리하여 재사용성을 높였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Eureka 서버&lt;/b&gt;를 통한 서비스 등록에서는 실제 마이크로서비스로 동작하는 모듈들만 등록하도록 설계하였습니다. 따라서 Core 모듈이나 Redis 모듈처럼 라이브러리 형태로 제공되는 모듈들은 Eureka에 등록하지 않았습니다. 이는 서비스 디스커버리에서 불필요한 혼선을 방지하고, 실제로 통신이 필요한 서비스들만 관리하기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 아키텍처 설계를 통해 각 모듈의 역할과 책임을 명확히 하고, 공통 기능의 재사용성을 높여 유지보수성과 확장성을 향상시킬 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가로 고려한 사항들:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모듈 간 통신 방식:&lt;/b&gt; 비즈니스 로직을 담당하는 모듈들 간에는 &lt;b&gt;Spring Cloud OpenFeign&lt;/b&gt;을 사용하여 RESTful하게 통신하도록 하였습니다. 이를 통해 모듈 간 결합도를 낮추고 확장성을 높였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가로 고려해야 할 사항들 :&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;환경 설정의 일원화:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 모듈에서 공통적으로 사용되는 설정들은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Spring Cloud Config&lt;/b&gt;를 활용하여 중앙에서 관리하도록 하였습니다. 이는 환경 설정의 일관성을 유지하고 변경 사항을 신속하게 반영할 수 있게 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로깅 및 모니터링:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전체 시스템의 상태를 효과적으로 모니터링하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Spring Boot Actuator&lt;/b&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ELK Stack&lt;/b&gt;을 도입하였습니다. 이는 문제 발생 시 신속한 대응을 가능하게 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습과 연구를 토대로 하나의 멀티 모듈 프로젝트를 만들어보면서 경험한 내용을 정리 및 복기하기 위해 본 포스팅 시리즈를 기획하고 작성하게 되었습니다. 차근차근 포스팅 해나가며 정리하며 이 지식을 온전히 저의 것으로 만들기 위해 노력할 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>multi module 프로젝트</category>
      <category>multi module 프로젝트로 통합</category>
      <category>spring boot multi module</category>
      <category>spring multi module</category>
      <category>spring multi module 설계</category>
      <category>스프링 멀티 모듈</category>
      <category>스프링 멀티모듈 설계</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/323</guid>
      <comments>https://min-nine.tistory.com/entry/Spring-Boot-%EA%B8%B0%EB%B0%98-%EB%8B%A4%EC%88%98%EC%9D%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%95%98%EB%82%98%EC%9D%98-Multi-Module-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-1#entry323comment</comments>
      <pubDate>Wed, 30 Oct 2024 18:49:19 +0900</pubDate>
    </item>
    <item>
      <title>Redis를 활용한 조회수 시스템 최적화와 동시성 이슈 해결</title>
      <link>https://min-nine.tistory.com/entry/Redis%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%A1%B0%ED%9A%8C%EC%88%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%B5%9C%EC%A0%81%ED%99%94%EC%99%80-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글을 제공하는 모든 서비스에서는 게시글의 조회수를 다양한 방법으로 제공하고 있습니다. 저희 회사에서도 게시글의 조회수를 집계하기 위해 여러 방식을 사용해 왔습니다. 기존에는 게시글 상세 조회 시마다 접속 정보(게시글, 접속 장치, 접속 시간)를 RDB에 로그 형태로 단순 삽입하고, 일정 시간마다 배치 작업을 통해 실제 조회수 RDB를 업데이트하는 방식이었습니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;이&amp;nbsp;방식은&amp;nbsp;시간이&amp;nbsp;지남에&amp;nbsp;따라&amp;nbsp;심각한&amp;nbsp;문제점을&amp;nbsp;드러냈습니다.&amp;nbsp;게시글의&amp;nbsp;수가&amp;nbsp;하루에도&amp;nbsp;몇십에서&amp;nbsp;몇백&amp;nbsp;개씩&amp;nbsp;증가하면서,&amp;nbsp;상세&amp;nbsp;조회마다&amp;nbsp;RDB에&amp;nbsp;Insert되는&amp;nbsp;횟수가&amp;nbsp;기하급수적으로&amp;nbsp;늘어났습니다.&amp;nbsp;이는&amp;nbsp;데이터베이스에&amp;nbsp;부하를&amp;nbsp;주고&amp;nbsp;성능&amp;nbsp;저하를&amp;nbsp;유발했습니다.&lt;br /&gt;&lt;br /&gt;이를&amp;nbsp;해결하기&amp;nbsp;위해&amp;nbsp;조회수&amp;nbsp;데이터를&amp;nbsp;Redis에&amp;nbsp;저장하고,&amp;nbsp;배치&amp;nbsp;작업을&amp;nbsp;통해&amp;nbsp;RDB를&amp;nbsp;업데이트하는&amp;nbsp;방식으로&amp;nbsp;변경했습니다.&amp;nbsp;하지만&amp;nbsp;Redis를&amp;nbsp;도입하면서&amp;nbsp;새로운&amp;nbsp;동시성&amp;nbsp;이슈가&amp;nbsp;발생했고,&amp;nbsp;이를&amp;nbsp;해결하기&amp;nbsp;위해&amp;nbsp;여러&amp;nbsp;방법을&amp;nbsp;모색했습니다.&amp;nbsp;이번&amp;nbsp;포스팅에서는&amp;nbsp;Redis를&amp;nbsp;활용한&amp;nbsp;조회수&amp;nbsp;시스템&amp;nbsp;최적화&amp;nbsp;과정과&amp;nbsp;그&amp;nbsp;과정에서&amp;nbsp;마주친&amp;nbsp;동시성&amp;nbsp;이슈&amp;nbsp;및&amp;nbsp;해결&amp;nbsp;방법에&amp;nbsp;대해&amp;nbsp;다뤄보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 상세보기마다 조회수 RDB Insert시 문제점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 설명&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터베이스 부하 증가&lt;/b&gt;: 게시글 상세 조회 시마다 RDB에 Insert 작업을 수행하면, 트래픽이 많을 때 데이터베이스에 과부하가 걸릴 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 저하&lt;/b&gt;: Insert 작업은 비교적 비용이 큰 연산이며, 특히 대량의 데이터가 쌓일 경우 쿼리 처리 속도가 느려질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스케일링의 어려움&lt;/b&gt;: 서비스가 성장함에 따라 게시글과 사용자 수가 증가하면, RDB에 대한 의존성이 병목현상을 유발할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템에서는 다음과 같은 방식으로 조회수를 처리했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729234647392&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 게시글 상세 조회 시마다 조회 기록 삽입
INSERT INTO article_view_logs (article_id, device_type, view_time)
VALUES (:articleId, :deviceType, NOW());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 배치 작업을 통해 일정 시간마다 실제 조회수를 집계하여 업데이트했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729234662485&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 배치 작업에서 조회수 집계 및 업데이트
UPDATE articles a
JOIN (
    SELECT article_id, COUNT(*) AS view_count
    FROM article_view_logs
    WHERE view_time BETWEEN :startTime AND :endTime
    GROUP BY article_id
) v ON a.id = v.article_id
SET a.view_count = a.view_count + v.view_count;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식은 초기에는 문제가 없었지만, 게시글과 사용자 수가 증가하면서 데이터베이스에 부담이 가중되었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring Data Redis의 활용과 그 문제점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis를 활용한 조회수 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 인메모리 데이터 저장소로서 높은 성능과 빠른 응답 속도를 제공합니다. 이에 따라 조회수 데이터를 Redis에 저장하고, 일정 시간마다 RDB로 옮기는 방식을 고려했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729234814104&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class ArticleHitService {

    @Autowired
    private ArticleHitRepository articleHitRepository;

    public void incrementHitCount(String articleId) {
        // Redis에 조회수 증가
        ArticleHit articleHit = articleHitRepository.findById(articleId)
                .orElse(new ArticleHit(articleId, 0));
        articleHit.increment();
        articleHitRepository.save(articleHit);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 이슈 발생&lt;/h3&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 findById()와 save() 사이에 다른 스레드에서 조회수가 증가하면 데이터가 덮어써지는 동시성 문제가 발생했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 상황&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드 A가 findById()로 조회하여 조회수 10을 가져옴.&lt;/li&gt;
&lt;li&gt;스레드 B가 findById()로 동일한 조회수 10을 가져옴.&lt;/li&gt;
&lt;li&gt;스레드 A가 조회수 1 증가하여 11로 저장.&lt;/li&gt;
&lt;li&gt;스레드 B가 조회수 1 증가하여 11로 저장.&lt;/li&gt;
&lt;li&gt;실제로는 조회수가 2 증가해야 하지만, 최종 조회수는 11이 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. RedisTemplate을 활용한 increment 구현으로 동시성 이슈 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis의 원자적 연산을 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 원자적 증가 연산을 지원합니다. 이를 활용하면 동시성 문제를 해결할 수 있습니다. RedisTemplate의 increment() 메서드를 사용하여 조회수를 증가시킵니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729234962975&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class ArticleHitService {

	@Autowired
    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    private static final String KEY_PREFIX = &quot;article-hit&quot;;

    public int incrementPcHitCount(String articleId, Integer hits) {
        String key = KEY_PREFIX + &quot;:&quot; + articleId;
        Long updatedValue = redisTemplate.opsForHash().increment(key, &quot;hitCount&quot;, hits);
        redisTemplate.opsForSet().add(KEY_PREFIX, articleId);

        return updatedValue.intValue();
    }
    
    public int incrementMobileHitCount(String articleId, Integer hits) {
        String key = KEY_PREFIX + &quot;:&quot; + articleId;
        Long updatedValue = redisTemplate.opsForHash().increment(key, &quot;mobileHitCount&quot;, hits);
        redisTemplate.opsForSet().add(KEY_PREFIX, articleId);

        return updatedValue.intValue();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용해야 하는 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원자성 보장&lt;/b&gt;: Redis의 HINCRBY 명령어는 원자적으로 실행되므로 동시성 문제가 발생하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 향상&lt;/b&gt;: 별도의 조회 없이 바로 증가시키므로 성능이 향상됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 간결화&lt;/b&gt;: findById()와 save()를 사용할 필요가 없어 코드가 간결해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. RDB Insert시 Redis 데이터 Select 및 Delete에서 동시성 이슈 발견&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 작업에서 Redis에 저장된 조회수 데이터를 가져와 RDB에 반영하고, Redis의 해당 데이터를 삭제하는 과정에서 동시성 문제가 발생했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729235021406&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class ArticleHitStorageService {

    @Autowired
    private ArticleHitRepository articleHitRepository;
    @Autowired
    private ArticleHitStorageRepository articleHitStorageRepository;

    public void updateHitStorage() {
        // Redis에서 모든 조회수 데이터 가져오기
        List&amp;lt;ArticleHit&amp;gt; articleHits = articleHitRepository.findAll();

        // Redis에서 데이터 삭제
        articleHitRepository.deleteAll(articleHits);

        // RDB에 조회수 업데이트
        for (ArticleHit articleHit : articleHits) {
            articleHitStorageRepository.incrementHitCount(
                articleHit.getArticleId(),
                articleHit.getHitCount()
            );
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 문제 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findAll()과 deleteAll() 사이에 다른 스레드가 조회수를 증가시키면, 해당 데이터는 배치 작업에서 처리되지 않고 Redis에 남게 됩니다. 이는 데이터 불일치로 이어질 수 있습니다. 실제로 테스트 결과 예상치랑 몇개의 근사한 차이로 값이 일치하지 않는 것을 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0bCk8/btsKasllpUy/mjXEFaXKVX3zr4H2IjxOVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0bCk8/btsKasllpUy/mjXEFaXKVX3zr4H2IjxOVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0bCk8/btsKasllpUy/mjXEFaXKVX3zr4H2IjxOVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0bCk8%2FbtsKasllpUy%2FmjXEFaXKVX3zr4H2IjxOVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1103&quot; height=&quot;225&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;5. Lua Script를 활용한 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;동시성 이슈 해결&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lua Script를 사용한 원자적 연산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에서 Lua 스크립트를 사용하면 여러 명령을 원자적으로 실행할 수 있습니다. 이를 활용하여 조회수 데이터를 가져오고 삭제하는 작업을 원자적으로 수행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729235119075&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- Lua 스크립트 정의
local keys = redis.call('SMEMBERS', KEYS[1])
local result = {}
for _, key in ipairs(keys) do
    local data = redis.call('HGETALL', key)
    table.insert(result, key)
    for i, v in ipairs(data) do
        table.insert(result, v)
    end
    redis.call('DEL', key)
end
redis.call('DEL', KEYS[1])
return result&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1729235148963&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public List&amp;lt;ArticleHit&amp;gt; getAndDeleteAllArticleHits() {
    String script = &quot;...&quot; // 위 Lua 스크립트 내용
    RedisScript&amp;lt;List&amp;gt; redisScript = RedisScript.of(script, List.class);
    List&amp;lt;Object&amp;gt; result = redisTemplate.execute(redisScript, Collections.singletonList(&quot;article-hit&quot;));

    // 결과 파싱 및 ArticleHit 객체 생성
    List&amp;lt;ArticleHit&amp;gt; articleHits = parseResult(result);
    return articleHits;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원자성 보장&lt;/b&gt;: Lua 스크립트 내의 모든 명령이 단일 명령으로 실행되므로 동시성 문제가 발생하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 일관성 유지&lt;/b&gt;: 조회와 삭제가 동시에 이루어져 데이터의 일관성을 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 향상&lt;/b&gt;: 네트워크 오버헤드를 줄이고 성능을 최적화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스크립트 유지보수&lt;/b&gt;: Lua 스크립트는 복잡할 수 있으므로 주석과 문서를 통해 관리해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 처리&lt;/b&gt;: 스크립트 실행 중 발생할 수 있는 예외를 적절히 처리해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Lua 스크립트를 활용한 getAndDeleteAllArticleHits()를 사용했을 때 원하던 값을 명확히 얻을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5y19K/btsKasllCEA/75a0dqLng1maHqgp4r3aVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5y19K/btsKasllCEA/75a0dqLng1maHqgp4r3aVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5y19K/btsKasllCEA/75a0dqLng1maHqgp4r3aVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5y19K%2FbtsKasllCEA%2F75a0dqLng1maHqgp4r3aVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;490&quot; height=&quot;202&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 테스트 케이스 작성 및 테스트 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pc, Mobile 조회수 증가 동시성 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 케이스를 작성하는 것은 이번 프로젝트에서 가장 어려운 부분 중 하나였습니다. 단위 테스트와 기능 테스트로 진행하려 했지만, 실제로 Redis와 RDB에 데이터가 예상대로 쌓이는지 확인하고, 동시성을 테스트할 수 있는지에 초점을 맞추다 보니 통합 테스트 형태로 작성할 수밖에 없었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트의&amp;nbsp;어려움과&amp;nbsp;원인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동시성 시나리오 재현의 복잡성&lt;/b&gt;: 여러 스레드나 프로세스에서 동시에 조회수를 증가시키는 상황을 재현해야 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 데이터 저장소 사용 필요성&lt;/b&gt;: 단위 테스트로는 Redis와 RDB 간의 데이터 일관성을 충분히 검증하기 어려웠습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경 설정의 복잡성&lt;/b&gt;: 통합 테스트를 위해 Redis와 RDB를 포함한 전체 애플리케이션 환경을 구축해야 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;통합 테스트를 통한 동시성 검증&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExecutorService와 CountDownLatch를 활용하여 다수의 스레드에서 동시에 조회수 증가 메서드를 호출하도록 했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1729235828376&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
    @DisplayName(&quot;동시성 테스트 - PC, Mobile 히트 카운트 증가 및 저장&quot;)
    void incrementHitCountConcurrentlyWithUpdateHitStorage() throws InterruptedException, ExecutionException {

        Random random = new Random();
        int loopCount = 100;

        // 스레드 풀 생성
        int numberOfThreads = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads / 10);

        // 전체 작업 수에 대한 CountDownLatch 설정
        CountDownLatch latch = new CountDownLatch(loopCount * numberOfThreads);

        // updateHitStorage 스레드 종료를 위한 플래그
        AtomicBoolean isRunning = new AtomicBoolean(true);

        // updateHitStorage를 실행하는 스레드 추가
        ExecutorService updateExecutorService = Executors.newSingleThreadExecutor();
        Future&amp;lt;?&amp;gt; updateFuture = updateExecutorService.submit(() -&amp;gt; {
            try {
                // 조회수가 몰리는 동안 주기적으로 updateHitStorage 실행
                while (isRunning.get()) {
                    articleHitStorageService.updateHitStorage();
                    Thread.sleep(500); // 500ms마다 실행
                }
                // 마지막으로 남은 데이터 처리
                articleHitStorageService.updateHitStorage();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        for (int z = 0; z &amp;lt; loopCount; z++) {
            // Given
            int randomNum = 10000000 + random.nextInt(90000000);
            String articleId = String.valueOf(randomNum);

            int incrementPerThread = 1;

            // 조회수 증가 작업
            for (int i = 0; i &amp;lt; numberOfThreads; i++) {
                executorService.execute(() -&amp;gt; {
                    try {
                        articleHitRepository.incrementPcHitCount(articleId, incrementPerThread);
                        articleHitRepository.incrementMobileHitCount(articleId, incrementPerThread);
                    } finally {
                        latch.countDown();
                    }
                });
            }
        }

        // 모든 조회수 증가 작업 완료 대기
        latch.await();
        executorService.shutdown();

        // 조회수 증가 작업이 모두 완료되었으므로 updateHitStorage 스레드 종료 플래그 설정
        isRunning.set(false);

        // updateHitStorage 스레드 종료 대기
        updateExecutorService.shutdown();
        updateFuture.get(); // 혹은 updateExecutorService.awaitTermination() 사용

        // Then
        // 최종적으로 Redis와 RDB에 데이터가 일관되게 저장되었는지 확인
        List&amp;lt;ArticleHit&amp;gt; articleHitList = articleHitRepository.findAllBy();
        log.info(&quot;articleHitList (Redis): {}&quot;, articleHitList);

        List&amp;lt;ArticleHitStorage&amp;gt; articleHitStorageList = articleHitStorageRepository.findAll();
        log.info(&quot;articleHitStorageList (RDB): {}&quot;, articleHitStorageList);

        // Assertions를 통해 데이터 검증
        // Redis에 남아 있는 데이터와 RDB에 저장된 데이터의 합이 기대한 값과 일치하는지 확인
        int totalHitCount = articleHitStorageList.stream()
                .mapToInt(ArticleHitStorage::getHitCount)
                .sum();
        int totalMobileHitCount = articleHitStorageList.stream()
                .mapToInt(ArticleHitStorage::getMobileHitCount)
                .sum();

        int expectedTotalCount = loopCount * numberOfThreads;

        Assertions.assertThat(totalHitCount).isEqualTo(expectedTotalCount);
        Assertions.assertThat(totalMobileHitCount).isEqualTo(expectedTotalCount);

        // Redis에 남아 있는 데이터가 없는지 확인 (모두 RDB로 이동되었으므로)
        Assertions.assertThat(articleHitList).isEmpty();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를&amp;nbsp;활용하여&amp;nbsp;조회수&amp;nbsp;시스템을&amp;nbsp;최적화하면서&amp;nbsp;발생한&amp;nbsp;동시성&amp;nbsp;이슈를&amp;nbsp;해결하기&amp;nbsp;위해&amp;nbsp;다양한&amp;nbsp;방법을&amp;nbsp;모색했습니다.&amp;nbsp;Redis의&amp;nbsp;원자적&amp;nbsp;연산과&amp;nbsp;Lua&amp;nbsp;스크립트를&amp;nbsp;활용하여&amp;nbsp;동시성&amp;nbsp;문제를&amp;nbsp;효과적으로&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있었습니다.&lt;br /&gt;&lt;br /&gt;이번&amp;nbsp;경험을&amp;nbsp;통해&amp;nbsp;대용량&amp;nbsp;트래픽&amp;nbsp;환경에서&amp;nbsp;데이터&amp;nbsp;일관성을&amp;nbsp;유지하면서&amp;nbsp;성능을&amp;nbsp;최적화하는&amp;nbsp;방법에&amp;nbsp;대해&amp;nbsp;깊이&amp;nbsp;있게&amp;nbsp;이해하게&amp;nbsp;되었습니다.&amp;nbsp;앞으로도&amp;nbsp;Redis의&amp;nbsp;다양한&amp;nbsp;기능을&amp;nbsp;적극&amp;nbsp;활용하여&amp;nbsp;더욱&amp;nbsp;안정적이고&amp;nbsp;효율적인&amp;nbsp;시스템을&amp;nbsp;구축할&amp;nbsp;예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>lua 스크립트</category>
      <category>redis</category>
      <category>redis 동시성 이슈</category>
      <category>spring data redis</category>
      <category>spring redis increment</category>
      <category>데이터 일관성</category>
      <category>데이터베이스 최적화</category>
      <category>성능 개선</category>
      <category>조회수 처리</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/322</guid>
      <comments>https://min-nine.tistory.com/entry/Redis%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%A1%B0%ED%9A%8C%EC%88%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%B5%9C%EC%A0%81%ED%99%94%EC%99%80-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0#entry322comment</comments>
      <pubDate>Fri, 18 Oct 2024 16:24:31 +0900</pubDate>
    </item>
    <item>
      <title>SQLD 취득 후기</title>
      <link>https://min-nine.tistory.com/entry/SQLD-%EC%B7%A8%EB%93%9D-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLD 시험 범위와 출제 형태가 처음 바뀐 시험에 뛰어들었습니다. 좀 더 실무에서 사용할 수 있는 내용들이 출제된다고 생각되었고, 정보처리기사 데이터베이스 과목을 공부하면서 함께 취득하면 좋을 것 같았습니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;너무 늦었지만&lt;span&gt; SQLD 취득 후기겸 포스팅 작성합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Od6i9/btsJYTJ7VJN/xgDMIUepkB9SFWzwaMf4aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Od6i9/btsJYTJ7VJN/xgDMIUepkB9SFWzwaMf4aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Od6i9/btsJYTJ7VJN/xgDMIUepkB9SFWzwaMf4aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOd6i9%2FbtsJYTJ7VJN%2FxgDMIUepkB9SFWzwaMf4aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;805&quot; height=&quot;252&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lUn4j/btsJ0bbGCMr/8EMjeRLPVOxNAXPfwWrlhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lUn4j/btsJ0bbGCMr/8EMjeRLPVOxNAXPfwWrlhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lUn4j/btsJ0bbGCMr/8EMjeRLPVOxNAXPfwWrlhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlUn4j%2FbtsJ0bbGCMr%2F8EMjeRLPVOxNAXPfwWrlhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;804&quot; height=&quot;475&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현업자로서 이론파트 부분과 새로 출제되는 윈도우 함수 부분, 그리고 SQL 활용 부분을 중점적으로 공부하였고 앞서 생각한데로 정보처리기사 공부와 겹치는 내용이 많았기 때문에 학습에 있어서 어려움은 없었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부 기간은 사람마다 다르겠지만 현업에서 개발을 하고 계시는 분들이라면 이론적인 부분과 실무에서 잘 사용하지 않았던 SQL 활용 파트 부분을 공부하는데 중점을 맞춰서 2주정도 공부하면 무난하게 합격하실 것 같습니다. 비전공자 분들께서는 한 달 여유를 잡고 공부하시는것을 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이런 자격증 하나에 2주에서 한 달 씩이나..? 라고 생각하실 수 있습니다. 시험 응시료를 보기 전 까지는요.. 응시료가 상당히 비쌉니다. 무려 &lt;b&gt;50,000원&lt;/b&gt; 이나 하는 거금이기 때문에 한 번에 취득을 목표로 잡으시는 것을 추천드립니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLD 자격증은 본인의 데이터베이스 개념 지식을 좀 더 강화하기에 좋은 자격증 같습니다. 유효기간은 5년이지만 만료때 네트워크관리사와 마찬가지로 온라인 교육을 수료하면 영구 자격증으로 바뀌는 것 같은데, 그 때 가서 해보고 후기 추가하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmkSHL/btsJYFefnGV/wlvsQbjtoSKKJMz56gY2pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmkSHL/btsJYFefnGV/wlvsQbjtoSKKJMz56gY2pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmkSHL/btsJYFefnGV/wlvsQbjtoSKKJMz56gY2pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmkSHL%2FbtsJYFefnGV%2FwlvsQbjtoSKKJMz56gY2pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;699&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;699&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>CapacityBuilding/SQLD 취득</category>
      <category>SQLD 공부</category>
      <category>SQLD 독학</category>
      <category>sqld 독학 방법</category>
      <category>sqld 독학 후기</category>
      <category>sqld 범위</category>
      <category>sqld 시험 독학</category>
      <category>SQLD 취득</category>
      <category>sqld 취득 후기</category>
      <category>SQLD 합격</category>
      <category>sqld 후기</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/320</guid>
      <comments>https://min-nine.tistory.com/entry/SQLD-%EC%B7%A8%EB%93%9D-%ED%9B%84%EA%B8%B0#entry320comment</comments>
      <pubDate>Thu, 10 Oct 2024 11:48:05 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에서 발생하는 직렬화 오류 해결 가이드</title>
      <link>https://min-nine.tistory.com/entry/Spring-Boot%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션 개발 중 Redis나 RabbitMQ와 같은 메시지 브로커를 사용하다 보면 예기치 않은 직렬화 오류를 마주할 수 있습니다. 특히 데이터 직렬화 및 역직렬화 과정에서 발생하는 문제는 시스템의 안정성을 해칠 수 있습니다. 이번 포스팅에서는 이러한 오류의 발생 원인과 해결 방법, 그리고 이를 이해하기 위한 이론적인 배경까지 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot로 개발된 문자 발송 서비스에서 다음과 같은 직렬화 오류가 발생했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1728351426949&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Could not read JSON: Unrecognized token 'TokenResDto': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오류는 Redis에서 데이터를 역직렬화하는 과정에서 발생했으며, JSON 파싱 중 인식할 수 없는 토큰 TokenResDto가 발견되었다는 내용입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 오류의 원인 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 직렬화와 역질렬화 개념&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;직렬화(Serialization)&lt;/b&gt;: 객체를 바이트 스트림으로 변환하여 저장하거나 전송할 수 있도록 하는 과정입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역직렬화(Deserialization)&lt;/b&gt;: 바이트 스트림을 다시 객체로 변환하는 과정입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 오류 발생의 근본적인 원인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 형식 불일치&lt;/b&gt;: Redis에 저장된 데이터가 JSON 형식이 아닌, 객체의 toString() 결과로 저장되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역직렬화 실패&lt;/b&gt;: Spring Data Redis는 데이터를 JSON으로 역직렬화하려고 시도하지만, 실제 데이터는 JSON 형식이 아니어서 파싱에 실패합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 기존 코드 분석&lt;/h3&gt;
&lt;pre id=&quot;code_1728351503999&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redisTemplate.opsForValue().set(&quot;token&quot;, redisTokenDtoValue(tokenResDto));

public static String redisTokenDtoValue(TokenResDto tokenResDto) throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    String json = objectMapper.writeValueAsString(tokenResDto);
    return objectMapper.readValue(json, TokenResDto.class).toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;redisTokenDtoValue 메서드에서 객체를 JSON으로 직렬화했다가 다시 객체로 역직렬화한 후 toString()을 호출하여 Redis에 저장하고 있습니다.&lt;/li&gt;
&lt;li&gt;toString() 메서드의 결과는 JSON 형식이 아니므로, Redis에 저장된 데이터가 JSON이 아닙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 불필요한 역질렬화와 toString() 제거하기&lt;/h3&gt;
&lt;pre id=&quot;code_1728351551082&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static String redisTokenDtoValue(TokenResDto tokenResDto) throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    return objectMapper.writeValueAsString(tokenResDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 JSON 문자열로 직렬화한 후 바로 반환합니다.&lt;/li&gt;
&lt;li&gt;Redis에는 이 JSON 문자열을 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 RedisTemplate의 Serializer 설정 변경&lt;/h3&gt;
&lt;pre id=&quot;code_1728351622844&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public RedisTemplate&amp;lt;?, ?&amp;gt; redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate&amp;lt;byte[], byte[]&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
    template.setConnectionFactory(connectionFactory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    return template;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GenericJackson2JsonRedisSerializer&lt;/b&gt;를 사용하여 RedisTemplate이 자동으로 객체를 JSON으로 직렬화/역직렬화하도록 설정합니다.&lt;/li&gt;
&lt;li&gt;이를 통해 코드에서 별도의 직렬화 로직이 필요 없어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 데이터 저장 및 조회 코드&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 저장&lt;/h4&gt;
&lt;pre id=&quot;code_1728351702362&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TokenResDto tokenResDto = new TokenResDto(...);
redisTemplate.opsForValue().set(&quot;token&quot;, tokenResDto);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 조회&lt;/h4&gt;
&lt;pre id=&quot;code_1728351716366&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TokenResDto tokenResDto = redisTemplate.opsForValue().get(&quot;token&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RedisTemplate의 Serializer 설정으로 인해 객체를 직접 저장하고 조회할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 이론적 배경 및 추가 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Jackson 라이브러리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ObjectMapper&lt;/b&gt;: Jackson 라이브러리에서 JSON 직렬화와 역직렬화를 담당하는 핵심 클래스입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의사항&lt;/b&gt;: toString() 메서드는 JSON 직렬화와 무관하므로, 객체를 JSON으로 변환하려면 반드시 writeValueAsString()을 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Rdis의 데이터 형식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis는 기본적으로 바이트 배열로 데이터를 저장합니다.&lt;/li&gt;
&lt;li&gt;데이터의 직렬화 형식을 통일하지 않으면 역직렬화 시 오류가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 Serializer 설정의 중요성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RedisTemplate의 Serializer 설정은 데이터의 직렬화와 역직렬화를 자동으로 처리해주므로, 올바른 설정이 필요합니다.&lt;/li&gt;
&lt;li&gt;키와 값의 Serializer를 명시적으로 설정하여 예기치 않은 오류를 방지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 예외 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 처리 중 예외가 발생할 경우를 대비하여 적절한 예외 처리 로직을 구현해야 합니다.&lt;/li&gt;
&lt;li&gt;특히 메시지 리스너나 데이터 접근 로직에서의 예외 처리는 시스템 안정성에 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션에서 Redis나 메시지 브로커를 사용할 때, 데이터의 직렬화 형식을 정확히 이해하고 일관성 있게 유지하는 것이 중요합니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;객체를 JSON으로 직렬화할 때는 ObjectMapper의 writeValueAsString() 메서드를 사용한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RedisTemplate의 Serializer 설정을 통해 데이터 직렬화/역직렬화를 자동화한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 저장 및 조회 시 일관된 형식을 유지하여 역직렬화 오류를 방지한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FasterXML/jackson-databind&quot;&gt;&lt;span&gt;Jackson&lt;/span&gt;&lt;span&gt; Databind&lt;/span&gt;&lt;span&gt; Documentation&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/docs/current/reference/html/&quot;&gt;&lt;span&gt;Spring&lt;/span&gt;&lt;span&gt; Data&lt;/span&gt;&lt;span&gt; Redis&lt;/span&gt;&lt;span&gt; Documentation&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>spring boot 직렬화 오류 해결</category>
      <category>spring boot에서 발생하는 직렬화 오류 해결</category>
      <category>spring boot에서 발생하는 직렬화 오류 해결 가이드</category>
      <category>could not read json: unrecognized token</category>
      <category>springboot could not read json: unrecognized token</category>
      <category>springboot redis serialization</category>
      <category>springboot serialization</category>
      <category>springboot 직렬화</category>
      <category>springboot 직렬화 오류</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/319</guid>
      <comments>https://min-nine.tistory.com/entry/Spring-Boot%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-%EA%B0%80%EC%9D%B4%EB%93%9C#entry319comment</comments>
      <pubDate>Tue, 8 Oct 2024 10:48:05 +0900</pubDate>
    </item>
    <item>
      <title>RabbitMQ 활용하기</title>
      <link>https://min-nine.tistory.com/entry/RabbitMQ-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근&amp;nbsp;&lt;b&gt;RabbitMQ&lt;/b&gt;를 활용하여 이메일, SMS, 카카오톡 푸시 알림 등을 처리하는 Notification Application에 대해 개발하게 되었습니다. 때문에 본 포스팅에서는 RabbitMQ를 활용하여 Nofification Apllication에서 주로 사용하는 기능들에 대해 알아보는 시간을 갖도록 하겠습니다. 특히 &lt;b&gt;Spring Boot&lt;/b&gt;와의 연동을 통해 실무에서 많이 사용되는 큐 전략과 예제 코드를 상세히 다루어, 처음 접하시는 분들도 쉽게 이해할 수 있도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RabbitMQ란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는&amp;nbsp;오픈&amp;nbsp;소스&amp;nbsp;메시지&amp;nbsp;브로커&amp;nbsp;소프트웨어로,&amp;nbsp;다양한&amp;nbsp;프로토콜을&amp;nbsp;지원하며&amp;nbsp;메시지의&amp;nbsp;송신자와&amp;nbsp;수신자&amp;nbsp;사이에서&amp;nbsp;큐잉을&amp;nbsp;담당합니다.&amp;nbsp;시스템&amp;nbsp;간의&amp;nbsp;비동기&amp;nbsp;통신을&amp;nbsp;가능하게&amp;nbsp;하여,&amp;nbsp;애플리케이션의&amp;nbsp;확장성과&amp;nbsp;유연성을&amp;nbsp;높여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 Notification Application에 RabbitMQ를 사용하나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notification Application은 대량의 메시지를 외부로 발송해야 하며, 이러한 작업은 시간이 많이 소요될 수 있습니다. RabbitMQ를 사용하면 다음과 같은 이점이 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비동기 처리&lt;/b&gt;: 메시지 발송을 비동기로 처리하여 애플리케이션의 응답 속도를 향상시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하 분산&lt;/b&gt;: 큐를 통해 작업을 분산하여 서버 부하를 줄입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신뢰성&lt;/b&gt;: 메시지의 안전한 전달과 재처리를 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. RabbitMQ 설정 및 SpringBoot 연동하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 RabbitMQ 설치하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ를 사용하려면 로컬 또는 서버에 RabbitMQ 서버를 설치해야 합니다. 보통 도커를 활용해서 설치 및 운영합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727686541475&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --hostname my-rabbit --name some-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 프로젝트 설정 (Spring Initializr)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;start.spring.io 사이트에서 최신 버전의 스프링 부트를 설치합니다 (포스팅 기준 3.3.3). Dependencies는 Spring Web, Spring for RabbitMQ, Lombok 등을 주입해줍니다. 저는 Maven 빌드툴 보다는 Gradle을 선호합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727686673099&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-amqp'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 설정파일 구성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml vkdlfdp RabbitMQ 연결 설정을 추가해봅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727686708594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 큐 전략 및 관리하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Exchange, Qeue, Routing Key 이해&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Exchange:&lt;/b&gt; 메시지를 받아서 적절한 큐로 라우팅하는 역할을 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Queue:&lt;/b&gt; 메시지가 저장되는 버퍼입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Routing Key:&lt;/b&gt; 메시지를 특정 큐로 라우팅하기 위한 키 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Direct, Topic, Fanout Exchange 사용 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Direct Exchange&lt;/b&gt;: 정확한 Routing Key 매칭이 필요할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Topic Exchange&lt;/b&gt;: 패턴 매칭을 통해 다수의 큐에 메시지를 전달할 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fanout Exchange&lt;/b&gt;: 모든 바인딩된 큐에 메시지를 브로드캐스팅합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일은 &lt;b&gt;Direct Exchange&lt;/b&gt;를 사용하여 특정 큐로 전달.&lt;/li&gt;
&lt;li&gt;공지사항은 &lt;b&gt;Fanout Exchange&lt;/b&gt;를 사용하여 모든 사용자에게 전달.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 Dead Letter Queue 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 처리 중 에러가 발생했을 때 해당 메시지를 별도의 큐로 보내어 추후에 재처리하거나 로그를 분석할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727828072195&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Queue deadLetterQueue() {
    return QueueBuilder.durable(&quot;deadLetterQueue&quot;).build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 Priority Queue 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지의 우선순위를 지정하여 중요한 메시지를 먼저 처리할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727828125389&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Queue priorityQueue() {
    return QueueBuilder.durable(&quot;priorityQueue&quot;)
            .withArgument(&quot;x-max-priority&quot;, 10)
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실전 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 이메일 발송 구현 예제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로듀서 (메시지 발송자)&lt;/h4&gt;
&lt;pre id=&quot;code_1727828251447&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class EmailSender {

    private final AmqpTemplate amqpTemplate;

    @Value(&quot;${email.exchange}&quot;)
    private String exchange;

    @Value(&quot;${email.routingkey}&quot;)
    private String routingKey;

    public void sendEmail(Email email) {
        amqpTemplate.convertAndSend(exchange, routingKey, email);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컨슈머 (메시지 수신자)&lt;/h4&gt;
&lt;pre id=&quot;code_1727828275275&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class EmailReceiver {

    @RabbitListener(queues = &quot;${email.queue}&quot;)
    public void receiveEmail(Email email) {
        // 이메일 발송 로직 구현
        System.out.println(&quot;Received Email: &quot; + email.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Config Class&lt;/h4&gt;
&lt;pre id=&quot;code_1727828292979&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class EmailRabbitConfig {

    @Value(&quot;${email.queue}&quot;)
    private String queueName;

    @Value(&quot;${email.exchange}&quot;)
    private String exchange;

    @Value(&quot;${email.routingkey}&quot;)
    private String routingKey;

    @Bean
    public Queue emailQueue() {
        return new Queue(queueName, false);
    }

    @Bean
    public DirectExchange emailExchange() {
        return new DirectExchange(exchange);
    }

    @Bean
    public Binding emailBinding(Queue emailQueue, DirectExchange emailExchange) {
        return BindingBuilder.bind(emailQueue).to(emailExchange).with(routingKey);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;application.yml&lt;/h4&gt;
&lt;pre id=&quot;code_1727828321232&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;email.queue=emailQueue
email.exchange=emailExchange
email.routingkey=emailRoutingKey&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 SMS 발송 구현 예제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로듀서&lt;/h4&gt;
&lt;pre id=&quot;code_1727828355315&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class SmsSender {

    private final AmqpTemplate amqpTemplate;

    @Value(&quot;${sms.exchange}&quot;)
    private String exchange;

    @Value(&quot;${sms.routingkey}&quot;)
    private String routingKey;

    public void sendSms(Sms sms) {
        amqpTemplate.convertAndSend(exchange, routingKey, sms);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컨슈머&lt;/h4&gt;
&lt;pre id=&quot;code_1727828380297&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class SmsReceiver {

    @RabbitListener(queues = &quot;${sms.queue}&quot;)
    public void receiveSms(Sms sms) {
        // SMS 발송 로직 구현
        System.out.println(&quot;Received SMS: &quot; + sms.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config Class, Properties 파일은 이메일 예제와 유사하게 구성하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 카카오톡 푸시 알림 구현 예제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로듀서&lt;/h4&gt;
&lt;pre id=&quot;code_1727828427249&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class KakaoPushSender {

    private final AmqpTemplate amqpTemplate;

    @Value(&quot;${kakao.exchange}&quot;)
    private String exchange;

    @Value(&quot;${kakao.routingkey}&quot;)
    private String routingKey;

    public void sendKakaoPush(KakaoPush kakaoPush) {
        amqpTemplate.convertAndSend(exchange, routingKey, kakaoPush);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컨슈머&lt;/h4&gt;
&lt;pre id=&quot;code_1727828438909&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class KakaoPushReceiver {

    @RabbitListener(queues = &quot;${kakao.queue}&quot;)
    public void receiveKakaoPush(KakaoPush kakaoPush) {
        // 카카오톡 푸시 알림 발송 로직 구현
        System.out.println(&quot;Received Kakao Push: &quot; + kakaoPush.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실전 활용 팁 및 주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 멀티스레드 환경에서의 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머는 기본적으로 멀티스레드로 동작하므로, 스레드 안정성을 보장해야 합니다. 필요한 경우 @RabbitListener의 concurrency 속성을 조절하여 스레드 수를 관리합니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1727828499275&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RabbitListener(queues = &quot;${email.queue}&quot;, concurrency = &quot;5&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 메시지 재처리 및 에러 핸들링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 처리 중 예외가 발생하면 기본적으로 메시지가 재시도됩니다. 이를 방지하거나 커스텀 재시도 로직을 구현하려면 SimpleRetryPolicy와 RetryTemplate을 활용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727828532872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public RetryOperationsInterceptor interceptor() {
    return RetryInterceptorBuilder.stateless()
        .maxAttempts(3)
        .backOffOptions(1000, 2.0, 10000)
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 성능 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 가져오는 메시지 수를 (Prefetch Count) 조절하여 메모리 사용을 최적화 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727828591163&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RabbitListener(queues = &quot;${email.queue}&quot;, prefetch = &quot;10&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 메시지의 경우 압축을 통해 전송량을 줄이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 RabbitMQ를 활용하여 Notification Application을 개발하는 방법에 대해 알아보았습니다. 큐 전략부터 실전 예제, 그리고 활용 팁까지 다루어 보았는데, RabbitMQ를 통해 효율적이고 확장성 있는 애플리케이션을 개발하시는데 도움이 되기를 바래요 ㅎ&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>rabbitmq</category>
      <category>rabbitmq email</category>
      <category>rabbitmq queue 전략</category>
      <category>rabbitmq sms</category>
      <category>rabbitmq spring boot</category>
      <category>rabbitmq 활용하기</category>
      <category>springboot rabbitmq</category>
      <category>springboot에서 rabbitmq 사용</category>
      <category>strping에서 rabbitmq</category>
      <category>스프링에서 mq</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/318</guid>
      <comments>https://min-nine.tistory.com/entry/RabbitMQ-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0#entry318comment</comments>
      <pubDate>Mon, 30 Sep 2024 18:01:47 +0900</pubDate>
    </item>
    <item>
      <title>정보처리기사 독학으로 필기 실기 합격한 후기</title>
      <link>https://min-nine.tistory.com/entry/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EB%8F%85%ED%95%99%EC%9C%BC%EB%A1%9C-%ED%95%84%EA%B8%B0-%EC%8B%A4%EA%B8%B0-%ED%95%A9%EA%B2%A9%ED%95%9C-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보처리기사 자격증을 준비하시는, 혹은 취득을 위해 공부를 진행중인 모든 분들에게 팩트 한 말씀 드리겠습니다. 독학으로 정보처리기사 필기 혹은, 실기 합격 후기를 찾아 볼 시간에 책 한장이라도 더 보고, 요약본 한 번 더 보면 무조건 합격합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이런 뻔한 내용을 위해 구글링을 하신것이 아니라는 것을 알기에, 제가 공부했던 방법을 소개하며 합격한 후기를 짧게나마 포스팅 합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 공부 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 정보처리기사 필기 독학 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필기 합격만 노리고 계신 분들은&lt;/b&gt; 이론책 사지 마시고, 구글링을 통해 (혹은 요약본 구매 사이트를 통해서) 정보처리기사 요약본 PDF를 구매하여 3회독 하신 후, CBT 기출문제를 계속 반복하여 풀이하시면 됩니다. 하루 출퇴근 2시간 기준으로 잡고 cbt 문제풀이만 계속 진행하신다면 전공자 기준 5~15일, 비 전공자 기준 15~30일 이면 필기는 커트라인으로나마 합격 가능할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실기 합격 동회차를 노리고 계신 분들은&lt;/b&gt; 시험 2달 전 부터, &lt;b&gt;시나공 정보처리기사 실기 이론서&amp;nbsp;&amp;nbsp;&lt;/b&gt;를 구매하셔서 1달 안에 2회독 정도 진행하시는 것을 추천드립니다. 그 이후에는 CBT 문제를 반복하여 풀이하시면 필기는 무난하게 합격 가능할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 정보처리기사 실기 독학 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 번에 붙고싶은 분들은&lt;/b&gt; C언어 포인터 및 이중 포인터, Java 상속관계 및 접근제어자,기본적인 Sql CRUD(삽입,읽기,수정,삭제) 에 대해서 공부하시고, 그 이외에 선점형/비선점형 스케쥴링을 꼭 공부하시기 바랍니다. 그 이외에 부수적인 부분들도 당연히 공부해야 하겠지만 앞서 언급한 내용은 확실히 이해하고 있으셔야 흔히 말하는 일트 성공이 가능할듯 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;맛 보기로 시험 보시는 분들은 &lt;/b&gt;프로그래밍 기본적인 반복문 및 조건문을 사용하는 코딩 공부 (C,Java,파이썬)를 위주로 진행하시고, 폭 넓은 이해를 위해 이론서를 n회독 하는걸 추천드려요.&amp;nbsp; 특히 비전공자의 경우 n회독의 n이 m이 될수록 합격률이 올라갈 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전공자 분들은&lt;/b&gt; 정보처리기사 실기 시험을 너무 쉽게 생각하지 마시고, 이론적인 부분과 C언어 및 Java 상속관계에 대한 오버로딩 오버라이딩 부분의 개념을 더욱 확실히 챙겨가신다면 합격하실 수 있으실 겁니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 필기 합격 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우 필기는 15일 정도 공부하였으며, 5일은 이론요약 PDF를 구매하여 m회독 하였고, 10일정도 CBT 문제를 출퇴근 길에 풀었습니다.&amp;nbsp; 과락은 없었으며 평균 75점으로 비교적 낮은 점수로 합격했습니다. 필기는 CBT로 실시하였고 최종 정답 제출버튼을 클릭하면 합/불 여부를 가채점 기준으로 확인할 수 있었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실기 합격 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필기와 비슷하게 총 15일정도, 학습법도 비슷하게 실기 요약본 PDF를 구매하여 m회독 후, 여러 카페에서 복원된 실기 기출 문제를 3년치 풀이하고 시험을 봤습니다만, 45점으로 1트 취득은 실패했었습니다. 생각보다 문제가 어려웠고 생소한 부분도 많았습니다(이론적인 부분).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부족한 이론적인 부분을 채우기 위해 시나공 정보처리기사 실기 이론서 를 당근으로 얻어서 n회독 하였고, 업무중에 중간중간 햇갈리는 개념들은 gpt에게 물어서 그떄그때 계속 다시 암기하는 형식으로 한 달 공부를 진행했습니다. 더불어 Java와 C언어의 난해한 부분과 스케줄링 부분의 계산법에 대해 더 공부했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적다면 적고 많다면 많은 준비를 했었지만, 시험 범위가 너무 넓고 이해했다고 생각했던 대부분의 것들은 사실 이해한것이 아니라 눈에 익은것일 뿐이었고 90점 이상 노렸던 점수는 80점 언더로 합격하게 되었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 정보처리기사 취득 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 현업자인 저로써는 취득 전과 후의 달라진 점을 느끼지 못하고 있습니다. 때문에 저와 같이 경력이 이미 있으신 현업자 분들이 저에게 &quot;그 정도로 기회비용을 들여서 취득할만 한가?&quot; 라고 질문하신다면 &quot;취득하세요&quot; 라고 말씀드릴게요. 확실히 정보처리기사 취득 과정은 기회비용이였습니다. 그 시간에 놀지도 못 하고 쉬지도 못 하고, Skill적인 공부도 못 하였습니다. 그럼에도 이론적인 부분에서 이룰 수 있는 성취는 분명히 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주니어 개발자 혹은 비전공자 및 취준생 여러분들께도 무조건 취득하라고 말씀드리고 싶습니다. 정보처리기사가 없는 시니어 분들 입장에서는 &quot;허우대 뿐인 자격증&quot; &quot;있으나 마나 한 자격증&quot; 일 수도 있지만, 취득 과정에서 자기주도학습하는 방법을 알게되고, 취득후에는 성취감을 느낄 수 있고 그 모든 감정들은 취업에 대한 자신감을 높여줄 것이라 생각하니깐요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;1414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wK82O/btsJNetm0i6/TuXslBotKsPzuIM1kW2K6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wK82O/btsJNetm0i6/TuXslBotKsPzuIM1kW2K6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wK82O/btsJNetm0i6/TuXslBotKsPzuIM1kW2K6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwK82O%2FbtsJNetm0i6%2FTuXslBotKsPzuIM1kW2K6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;970&quot; height=&quot;1414&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;1414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 다 읽으셨다면 이제 책 보러 가세요 :) 당신의 정보처리기사 자격증 취득을 응원합니다. 다음번 포스팅에는 정보보안기사 취득 후기를 작성할 수 있도록 저도 공부하겠습니다.&lt;/p&gt;</description>
      <category>CapacityBuilding/기사 취득</category>
      <category>정보처리기사 독학</category>
      <category>정보처리기사 독학 후기</category>
      <category>정보처리기사 실기 독학</category>
      <category>정보처리기사 취득</category>
      <category>정보처리기사 취득 후기</category>
      <category>정보처리기사 필기 독학</category>
      <category>정처기 독학</category>
      <category>정처기 독학 후기</category>
      <category>정처기 실기 독학</category>
      <category>정처기 필기 독학</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/317</guid>
      <comments>https://min-nine.tistory.com/entry/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EB%8F%85%ED%95%99%EC%9C%BC%EB%A1%9C-%ED%95%84%EA%B8%B0-%EC%8B%A4%EA%B8%B0-%ED%95%A9%EA%B2%A9%ED%95%9C-%ED%9B%84%EA%B8%B0#entry317comment</comments>
      <pubDate>Wed, 25 Sep 2024 21:00:58 +0900</pubDate>
    </item>
    <item>
      <title>[Legacy to Modernization] 3. Language Context Switching에 따른 작업 효율성 고려하기</title>
      <link>https://min-nine.tistory.com/entry/Legacy-to-Modernization-3-Language-Context-Switching%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%9E%91%EC%97%85-%ED%9A%A8%EC%9C%A8%EC%84%B1-%EA%B3%A0%EB%A0%A4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근, &lt;a href=&quot;https://www.codeigniter.com/userguide3/installation/upgrading.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CodeIgnitor 1.0 beta&lt;/a&gt; &amp;amp; &lt;a href=&quot;https://tpl.xtac.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;언더바 템플릿&lt;/a&gt; 2개의 고대 유물적인 Framework를 섞어서 하나로 만들어 놓은 요상한 FrameWork 기반의 Legacy PHP Web Application을 Next.Js(Front)와 SpringBoot(Back)으로 분리하며 Converting 작업을 진행하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨버팅&amp;nbsp;작업&amp;nbsp;자체는&amp;nbsp;큰&amp;nbsp;리소스를&amp;nbsp;요구하지&amp;nbsp;않지만,&amp;nbsp;유지보수도&amp;nbsp;함께&amp;nbsp;진행해야&amp;nbsp;한다는&amp;nbsp;점에서&amp;nbsp;문제가&amp;nbsp;발생합니다.&amp;nbsp;코드&amp;nbsp;컨벤션이&amp;nbsp;전혀&amp;nbsp;지켜지지&amp;nbsp;않은&amp;nbsp;과거의&amp;nbsp;스파게티&amp;nbsp;코드와&amp;nbsp;에일리언&amp;nbsp;코드를&amp;nbsp;분석하고,&amp;nbsp;이를&amp;nbsp;Java&amp;nbsp;Spring&amp;nbsp;진영에서&amp;nbsp;재구현하는&amp;nbsp;과정에서&amp;nbsp;유지보수&amp;nbsp;작업이&amp;nbsp;들어오면&amp;nbsp;다시&amp;nbsp;PHP로&amp;nbsp;돌아가야&amp;nbsp;하는&amp;nbsp;반복적인&amp;nbsp;행위가&amp;nbsp;이어집니다.&amp;nbsp;이러한&amp;nbsp;언어&amp;nbsp;컨텍스트&amp;nbsp;스위칭은&amp;nbsp;작업&amp;nbsp;효율성과&amp;nbsp;업무&amp;nbsp;집중도를&amp;nbsp;크게&amp;nbsp;떨어뜨려&amp;nbsp;극한의&amp;nbsp;비효율성을&amp;nbsp;체감하게&amp;nbsp;만들었습니다.&lt;br /&gt;&lt;br /&gt;이에&amp;nbsp;따라,&amp;nbsp;언어&amp;nbsp;컨버팅을&amp;nbsp;동반하는&amp;nbsp;고도화&amp;nbsp;또는&amp;nbsp;리팩토링&amp;nbsp;작업&amp;nbsp;시&amp;nbsp;작업&amp;nbsp;효율성을&amp;nbsp;고려할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;방향성을&amp;nbsp;제시하고자&amp;nbsp;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 언어 컨텍스트 스위칭의 문제점 파악&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어&amp;nbsp;컨텍스트&amp;nbsp;스위칭은&amp;nbsp;개발자가&amp;nbsp;작업&amp;nbsp;중인&amp;nbsp;언어&amp;nbsp;또는&amp;nbsp;프레임워크를&amp;nbsp;변경할&amp;nbsp;때&amp;nbsp;발생하는&amp;nbsp;인지적&amp;nbsp;부담을&amp;nbsp;의미합니다.&amp;nbsp;PHP에서&amp;nbsp;Java로,&amp;nbsp;다시&amp;nbsp;JavaScript로&amp;nbsp;전환하는&amp;nbsp;과정에서&amp;nbsp;개발자는&amp;nbsp;각&amp;nbsp;언어의&amp;nbsp;문법,&amp;nbsp;라이브러리,&amp;nbsp;개발&amp;nbsp;환경&amp;nbsp;등을&amp;nbsp;다시&amp;nbsp;적응해야&amp;nbsp;합니다.&amp;nbsp;이러한&amp;nbsp;반복은&amp;nbsp;작업&amp;nbsp;흐름을&amp;nbsp;끊고&amp;nbsp;생산성을&amp;nbsp;저하시킬&amp;nbsp;뿐만&amp;nbsp;아니라,&amp;nbsp;실수&amp;nbsp;발생의&amp;nbsp;가능성도&amp;nbsp;높입니다.&lt;br /&gt;&lt;br /&gt;특히&amp;nbsp;레거시&amp;nbsp;시스템에서는&amp;nbsp;코드의&amp;nbsp;가독성과&amp;nbsp;구조가&amp;nbsp;불량한&amp;nbsp;경우가&amp;nbsp;많아&amp;nbsp;분석에&amp;nbsp;더&amp;nbsp;많은&amp;nbsp;시간이&amp;nbsp;소요됩니다.&amp;nbsp;스파게티&amp;nbsp;코드와&amp;nbsp;에일리언&amp;nbsp;코드를&amp;nbsp;이해하고&amp;nbsp;이를&amp;nbsp;현대적인&amp;nbsp;언어와&amp;nbsp;프레임워크로&amp;nbsp;변환하는&amp;nbsp;과정은&amp;nbsp;상당한&amp;nbsp;인지적&amp;nbsp;부담을&amp;nbsp;가중시킵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 작업 효율성 향상을 위한 전략&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1 현실적인 작업 분담과 문서화의 중요성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상적으로는&amp;nbsp;여러&amp;nbsp;개발자가&amp;nbsp;투입될&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;상황에서&amp;nbsp;작업&amp;nbsp;단위를&amp;nbsp;잘게&amp;nbsp;쪼개어&amp;nbsp;모듈화하고,&amp;nbsp;유지보수와&amp;nbsp;컨버팅&amp;nbsp;작업을&amp;nbsp;명확히&amp;nbsp;구분하여&amp;nbsp;각&amp;nbsp;작업에&amp;nbsp;대한&amp;nbsp;목표와&amp;nbsp;범위를&amp;nbsp;설정한&amp;nbsp;후&amp;nbsp;분배하면&amp;nbsp;효율적입니다.&amp;nbsp;하지만&amp;nbsp;대부분의&amp;nbsp;회사는&amp;nbsp;그런&amp;nbsp;여력이나&amp;nbsp;상황이&amp;nbsp;주어지지&amp;nbsp;않습니다.&lt;br /&gt;&lt;br /&gt;따라서&amp;nbsp;현실적인&amp;nbsp;대안으로&amp;nbsp;유지보수를&amp;nbsp;진행하면서&amp;nbsp;기존&amp;nbsp;레거시&amp;nbsp;코드에&amp;nbsp;대한&amp;nbsp;철저한&amp;nbsp;문서화를&amp;nbsp;진행하는&amp;nbsp;것이&amp;nbsp;중요합니다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;향후&amp;nbsp;언어&amp;nbsp;컨텍스트&amp;nbsp;스위칭&amp;nbsp;시&amp;nbsp;유지보수에&amp;nbsp;참여하지&amp;nbsp;않았던&amp;nbsp;개발자도&amp;nbsp;비즈니스&amp;nbsp;플로우를&amp;nbsp;이해할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;됩니다.&amp;nbsp;문서화에는&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;요소를&amp;nbsp;포함해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직의 흐름도 작성&lt;/b&gt;: 기능 단위로 로직이 어떻게 진행되는지 시각화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주석 및 코드 설명 추가:&lt;/b&gt; 복잡한 코드나 이해하기 어려운 부분에 상세한 주석을 남깁니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 명세서 작성:&lt;/b&gt; 기존 시스템의 입력과 출력, 처리 과정을 문서화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경&amp;nbsp;이력&amp;nbsp;관리:&lt;/b&gt;&amp;nbsp;코드&amp;nbsp;변경&amp;nbsp;사항과&amp;nbsp;그&amp;nbsp;이유를&amp;nbsp;기록하여&amp;nbsp;추후&amp;nbsp;추적이&amp;nbsp;가능하도록&amp;nbsp;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2 기능 단위의 모듈화와 스크럼 방식의 도입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 시스템에서는 기능에 대한 &lt;b&gt;히스토리 관리 이력&lt;/b&gt;이 남아있지 않은 경우가 많습니다. 이러한 상황에서 새로운 UI를 담은 클라이언트 애플리케이션을 개발하는 기획자, 디자이너, 프론트엔드 개발자들은 &lt;b&gt;기능 단위로 모듈화된 비즈니스 로직&lt;/b&gt;을 하나씩 &lt;b&gt;RESTful한 API로 구현&lt;/b&gt;해 나가는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 다음과 같은 전략을 도입할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기능별 우선순위 설정&lt;/b&gt;: 비즈니스 중요도에 따라 구현해야 할 기능의 우선순위를 정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스크럼 방식의 애자일 도입&lt;/b&gt;: 짧은 개발 주기로 기능을 개발하고 검토하는 스크럼 방식을 통해 팀원 간의 협업을 강화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공동의 코드 스타일과 규칙 수립&lt;/b&gt;: 코드 컨벤션을 통일하여 협업 시 발생하는 혼란을 최소화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 주도 개발(TDD)&lt;/b&gt;: 각 기능에 대한 테스트 코드를 작성하여 안정성을 확보합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.3 개발 환경의 일관성 유지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어 컨텍스트 스위칭으로 인한 부담을 줄이기 위해 &lt;b&gt;개발 환경의 일관성&lt;/b&gt;을 유지하는 것도 중요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;통합 개발 환경(IDE) 사용&lt;/b&gt;: PHP, Java, JavaScript를 모두 지원하는 IDE를 사용하여 언어 전환 시에도 익숙한 환경을 유지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동화된 빌드 및 배포 파이프라인 구축&lt;/b&gt;: CI/CD 도구를 활용하여 빌드와 배포 과정을 자동화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 코드 작성:&amp;nbsp;&lt;/b&gt;인터프리터 기반 언어에서 컴파일 기반 언어로의 컨버팅에서 가장 중요하다고 생각하는 것이 저는 테스트 코드 작성이라고 생각합니다. 유지보수를 진행하면서도 틈틈히 작성해놓은 테스트 코드는, 타 언어로 재작성할 때 빛을 봅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.4 팀 내 지식 공유와 교육&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;정기적인 코드 리뷰 세션&lt;/b&gt;: 팀원들이 서로의 코드를 리뷰하면서 지식을 공유하고 코드 품질을 높이는 것도 중요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;교육 프로그램 운영&lt;/b&gt;: 새로운 언어와 프레임워크에 대한 교육은 팀원들의 역량을 강화할 수 있고, 이런 교육을 운영하는 회사의 팀원 이탈율은 그렇지 않은 회사보다 상당히 적을 것이라 확신합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기술 블로그나 위키 운영&lt;/b&gt;: 팀 내에서 발견한 문제와 해결 방법을 공유하여 collective intelligence를 형성하는 문화를 가져보는것을 추천드립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 시스템의 현대화 작업에서 언어 컨텍스트 스위칭은 피할 수 없는 도전이라 생각합니다. 그리고 모든 기업체가 그렇겠지만, 제한된 자원과 인력으로 이 문제를 극복하기 위해서는 현실적인 전략이 필요합니다. 유지보수 과정에서의 철저한 문서화, 기능 단위의 모듈화, 스크럼 방식의 도입, 개발 환경의 일관성 유지, 그리고 팀 내 지식 공유와 교육을 통해 작업 효율성을 높일 수 있습니다.&lt;br /&gt;&lt;br /&gt;궁극적으로&amp;nbsp;이러한&amp;nbsp;노력은&amp;nbsp;개발자들의&amp;nbsp;인지적&amp;nbsp;부담을&amp;nbsp;줄이고,&amp;nbsp;생산성을&amp;nbsp;향상시키며,&amp;nbsp;높은&amp;nbsp;품질의&amp;nbsp;소프트웨어를&amp;nbsp;제공하는&amp;nbsp;데&amp;nbsp;기여할&amp;nbsp;것입니다.&amp;nbsp;레거시&amp;nbsp;시스템의&amp;nbsp;현대화는&amp;nbsp;단순히&amp;nbsp;기술적인&amp;nbsp;변환을&amp;nbsp;넘어&amp;nbsp;조직&amp;nbsp;문화와&amp;nbsp;작업&amp;nbsp;방식의&amp;nbsp;혁신을&amp;nbsp;요구합니다.&amp;nbsp;지속적인&amp;nbsp;개선과&amp;nbsp;협업을&amp;nbsp;통해&amp;nbsp;성공적인&amp;nbsp;전환을&amp;nbsp;이뤄낼&amp;nbsp;수&amp;nbsp;있을&amp;nbsp;것입니다.&lt;/p&gt;</description>
      <category>Server Language/PHP</category>
      <category>context switching 효율성</category>
      <category>from php to java</category>
      <category>from php to spring</category>
      <category>language context switching</category>
      <category>language context switching 효율성</category>
      <category>legacy to modernization</category>
      <category>php to java converting</category>
      <category>php to spring</category>
      <category>php to spring converting</category>
      <category>[legacy to modernization] 3. language context switching에 따른 작업 효율성 고려하기</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/315</guid>
      <comments>https://min-nine.tistory.com/entry/Legacy-to-Modernization-3-Language-Context-Switching%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%9E%91%EC%97%85-%ED%9A%A8%EC%9C%A8%EC%84%B1-%EA%B3%A0%EB%A0%A4%ED%95%98%EA%B8%B0#entry315comment</comments>
      <pubDate>Wed, 25 Sep 2024 19:58:39 +0900</pubDate>
    </item>
    <item>
      <title>ACL과 SG(ACG)에 대하여</title>
      <link>https://min-nine.tistory.com/entry/ACL%EA%B3%BC-SGACG%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대의 클라우드 환경에서는 보안이 최우선 과제로 떠오르고 있습니다. 특히 네트워크 레벨에서의 접근 제어는 시스템의 안정성과 신뢰성을 유지하는 데 필수적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;b&gt;Network ACL&lt;/b&gt;과 &lt;b&gt;SG(Securiy Group) - ACG(Access Control Group)&lt;/b&gt;에 대해 알아보고, 두 개념의 차이점과 활용 방법을 이해하기 쉽게 설명해보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Network ACL이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Network ACL(Network Access Control List)&lt;/b&gt;은 서브넷 수준에서 적용되는 &lt;b&gt;보안 레이어&lt;/b&gt;로, 네트워크 트래픽의 인바운드(Inbound)와 아웃바운드(Outbound)를 제어합니다. 각 규칙은 특정한 &lt;b&gt;포트&lt;/b&gt;, &lt;b&gt;프로토콜&lt;/b&gt;, &lt;b&gt;소스 또는 대상 IP 주소&lt;/b&gt;에 대한 허용(Allow) 또는 거부(Deny)를 정의합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스테이트리스(Stateless)&lt;/b&gt;: 각 패킷을 개별적으로 평가하며, 이전의 접속 상태를 기억하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서브넷 레벨에서 적용&lt;/b&gt;: VPC 내의 서브넷에 연결되어 해당 서브넷의 모든 인스턴스에 적용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 번호에 따른 평가 순서&lt;/b&gt;: 낮은 번호의 규칙부터 순차적으로 평가됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 방식&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트래픽이 네트워크에 도달하면, Network ACL은 설정된 규칙에 따라 해당 트래픽을 허용하거나 거부합니다.&lt;/li&gt;
&lt;li&gt;규칙은 번호 순서대로 평가되며, 해당하는 규칙이 발견되면 그 즉시 허용 또는 거부가 결정됩니다.&lt;/li&gt;
&lt;li&gt;모든 규칙에 해당하지 않는 경우, 기본적으로 &lt;b&gt;모든 트래픽을 거부&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.SG(Securiy Group) - ACG(Access Control Group) 이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACG(Access Control Group)은 일반적으로 &lt;b&gt;Security Group&lt;/b&gt;을 의미하며, 인스턴스 수준에서의 &lt;b&gt;가상 방화벽&lt;/b&gt; 역할을 합니다. AWS에서는 &lt;b&gt;Security Group&lt;/b&gt;이라는 용어를 주로 사용하지만, 일부 환경에서는 ACG로도 불립니다. 저는 최근 NCP(Naver Cloud Platform)을 사용하기 때문이 이후에는 ACG로 칭하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스테이트풀(Stateful)&lt;/b&gt;: 트래픽의 상태를 추적하며, 인바운드 규칙에 의해 허용된 트래픽의 응답은 자동으로 허용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인스턴스 레벨에서 적용&lt;/b&gt;: 각 인스턴스에 직접 연결되어 해당 인스턴스에만 적용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;허용 규칙만 존재&lt;/b&gt;: 명시적으로 허용된 트래픽만 통과시키며, 그 외에는 모두 차단합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 방식&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인스턴스에 대한 트래픽이 있을 때, Security Group은 설정된 인바운드 규칙에 따라 해당 트래픽을 허용합니다.&lt;/li&gt;
&lt;li&gt;인스턴스에서 외부로 나가는 트래픽은 아웃바운드 규칙에 따라 결정됩니다.&lt;/li&gt;
&lt;li&gt;응답 트래픽은 스테이트풀 특성에 의해 자동으로 허용됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Network ACL과 ACG의 차이점&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.6589%;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;width: 35.5426%;&quot;&gt;Nework ACL&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%;&quot;&gt;ACG(SG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.6589%;&quot;&gt;적용 레벨&lt;/td&gt;
&lt;td style=&quot;width: 35.5426%;&quot;&gt;서브넷 수준&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%;&quot;&gt;인스턴스 수준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.6589%;&quot;&gt;상태 유지&lt;/td&gt;
&lt;td style=&quot;width: 35.5426%;&quot;&gt;스테이트리스(StateLess)&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%;&quot;&gt;스테이트풀(StateFul)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.6589%;&quot;&gt;규칙 유형&lt;/td&gt;
&lt;td style=&quot;width: 35.5426%;&quot;&gt;허용 및 거부 규칙 모두 지원&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%;&quot;&gt;허용 규칙만 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.6589%;&quot;&gt;평가 순서&lt;/td&gt;
&lt;td style=&quot;width: 35.5426%;&quot;&gt;규칙 번호 순서대로 평가&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%;&quot;&gt;모든 규칙을 동시에 평가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.6589%;&quot;&gt;기본 동작&lt;/td&gt;
&lt;td style=&quot;width: 35.5426%;&quot;&gt;명시적으로 허용되지 않으면 거부&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%;&quot;&gt;명시적으로 허용되지 않으면 거부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 차이점 설명&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스테이트리스 vs 스테이트풀&lt;/b&gt;: Network ACL은 이전의 접속 상태를 기억하지 않기 때문에 인바운드와 아웃바운드에 각각 규칙을 설정해야 합니다. 반면 Security Group은 스테이트풀하여 인바운드 트래픽에 대한 응답은 자동으로 허용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용 범위&lt;/b&gt;: Network ACL은 서브넷 전체에 적용되므로 해당 서브넷의 모든 인스턴스에 동일한 규칙이 적용됩니다. Security Group은 인스턴스별로 적용 가능하여 더 세밀한 제어가 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 설정 방식&lt;/b&gt;: Network ACL은 허용과 거부 규칙을 모두 설정할 수 있지만, Security Group은 허용 규칙만 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실제 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;웹 서버&lt;/b&gt;와 &lt;b&gt;데이터베이스 서버&lt;/b&gt;로 구성된 애플리케이션이 있습니다.&lt;/li&gt;
&lt;li&gt;웹 서버는 인터넷에서 HTTP 요청을 받아들이고, 데이터베이스 서버는 웹 서버에서만 접근 가능합니다.&lt;/li&gt;
&lt;li&gt;보안을 강화하기 위해 적절한 Network ACL과 Security Group을 설정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단계별 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Security Group 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;웹 서버용 Security Group&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HTTP(포트 80)&lt;/b&gt;: 모든 IP 주소(0.0.0.0/0)에서 허용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSH(포트 22)&lt;/b&gt;: 관리용 IP 주소에서만 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아웃바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 모든 아웃바운드 트래픽 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터베이스 서버용 Security Group&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MySQL(포트 3306)&lt;/b&gt;: 웹 서버의 Security Group에서만 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아웃바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 모든 아웃바운드 트래픽 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Network ACL 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브넷 A (웹 서버가 위치한 서브넷)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 100&lt;/b&gt;: 소스 0.0.0.0/0, 포트 80, 허용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 110&lt;/b&gt;: 소스 관리용 IP, 포트 22, 허용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 **&lt;/b&gt;: 그 외 모든 트래픽 거부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아웃바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 100&lt;/b&gt;: 대상 0.0.0.0/0, 모든 포트, 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브넷 B (데이터베이스 서버가 위치한 서브넷)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 100&lt;/b&gt;: 소스 서브넷 A, 포트 3306, 허용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 **&lt;/b&gt;: 그 외 모든 트래픽 거부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아웃바운드 규칙&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 100&lt;/b&gt;: 대상 서브넷 A, 모든 포트, 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서는 웹 서버의 포트 80으로만 접근 가능합니다.&lt;/li&gt;
&lt;li&gt;웹 서버는 데이터베이스 서버에 접근할 수 있으며, 데이터베이스 서버는 외부에서 직접 접근이 불가능합니다.&lt;/li&gt;
&lt;li&gt;관리용 IP에서만 웹 서버에 SSH 접속이 가능합니다.&lt;/li&gt;
&lt;li&gt;Network ACL과 Security Group을 조합하여 다중 레이어의 보안을 구축했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 최고의 보안 설정을 위한 권장 사항&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;기본값 확인&lt;/b&gt;: Network ACL과 Security Group의 기본 설정은 모든 트래픽을 거부합니다. 필요한 트래픽만 명시적으로 허용하세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소 권한 원칙 적용&lt;/b&gt;: 필요한 포트와 IP 주소만 허용하여 공격 표면을 최소화합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 정기 점검&lt;/b&gt;: 주기적으로 규칙을 검토하여 불필요한 허용이나 거부가 없는지 확인합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서브넷 및 인스턴스 분리&lt;/b&gt;: 역할에 따라 서브넷과 인스턴스를 분리하고, 각기 다른 보안 설정을 적용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 활용&lt;/b&gt;: VPC Flow Logs나 기타 모니터링 도구를 사용하여 트래픽을 분석하고, 이상 징후를 빠르게 감지합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Network ACL과 ACG(Security Group)는 클라우드 환경에서 네트워크 보안을 강화하기 위한 핵심 도구입니다. 두 개념은 적용 범위와 동작 방식에서 차이가 있으므로, 이를 정확히 이해하고 적절하게 활용하는 것이 중요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Network ACL&lt;/b&gt;은 서브넷 수준에서 기본적인 트래픽 제어를 담당하며, 스테이트리스 특성으로 인해 인바운드와 아웃바운드 규칙을 모두 설정해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Security Group&lt;/b&gt;은 인스턴스 수준에서 세밀한 접근 제어를 가능하게 하며, 스테이트풀 특성으로 인해 응답 트래픽이 자동으로 허용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 환경에서는 두 가지를 조합하여 다층적인 보안 구조를 설계하는 것이 가장 효과적입니다. 이를 통해 외부 공격으로부터 시스템을 보호하고, 내부 자원의 안전한 통신을 보장할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Q&amp;amp;A로 알아보는 설정 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q1:&lt;/b&gt; Network ACL과 Security Group 중 어느 것을 먼저 설정해야 하나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A1:&lt;/b&gt; 일반적으로 &lt;b&gt;Security Group&lt;/b&gt;을 먼저 설정하여 인스턴스 수준의 보안을 확보하고, 필요에 따라 &lt;b&gt;Network ACL&lt;/b&gt;을 추가하여 서브넷 수준의 보안을 강화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q2:&lt;/b&gt; 스테이트리스와 스테이트풀의 차이는 무엇인가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A2:&lt;/b&gt; **스테이트리스(Stateless)**는 트래픽의 상태 정보를 저장하지 않으며, 각 패킷을 개별적으로 처리합니다. 반면 **스테이트풀(Stateful)**은 트래픽의 상태 정보를 유지하여 응답 트래픽을 자동으로 허용하거나 차단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q3:&lt;/b&gt; Network ACL과 Security Group을 동시에 사용해야 하나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A3:&lt;/b&gt; 반드시 동시에 사용해야 하는 것은 아니지만, 두 가지를 조합하면 보안을 더욱 강화할 수 있습니다. &lt;b&gt;다층 보안&lt;/b&gt;은 잠재적인 위협으로부터 시스템을 더 효과적으로 보호합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Network ACL과 ACG(Security Group)의 개념과 차이점을 이해하고 정리할겸 이번 포스팅 작성을 시작하였습니다. 네트워크 보안은 복잡해 보이지만, 핵심 원리를 이해하면 효과적인 보안 설정을 구현할 수 있습니다. 하지만 본 포스팅을 쓰고있는 저조차 아직 잘 모르겠습니다. 꾸준한 학습과 복습만이 살길입니다.&lt;/p&gt;</description>
      <category>Infrastructure/Network</category>
      <category>acg란</category>
      <category>acl acg</category>
      <category>acl acg 개념</category>
      <category>acl acg 이해</category>
      <category>acl sg</category>
      <category>acl sg 개념</category>
      <category>acl sg 이해</category>
      <category>acl이란</category>
      <category>sg acg</category>
      <category>sg란</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/316</guid>
      <comments>https://min-nine.tistory.com/entry/ACL%EA%B3%BC-SGACG%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC#entry316comment</comments>
      <pubDate>Wed, 25 Sep 2024 14:03:23 +0900</pubDate>
    </item>
    <item>
      <title>스프링에서 TransactionManager에 대한 상세한 이해하기</title>
      <link>https://min-nine.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-TransactionManager%EC%97%90-%EB%8C%80%ED%95%9C-%EC%83%81%EC%84%B8%ED%95%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은&amp;nbsp;데이터베이스&amp;nbsp;작업에서&amp;nbsp;일관성과&amp;nbsp;무결성을&amp;nbsp;보장하기&amp;nbsp;위해&amp;nbsp;필수적인&amp;nbsp;개념입니다.&amp;nbsp;스프링&amp;nbsp;프레임워크에서는&amp;nbsp;이러한&amp;nbsp;트랜잭션&amp;nbsp;관리를&amp;nbsp;쉽게&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;TransactionManager라는&amp;nbsp;추상화된&amp;nbsp;개념을&amp;nbsp;제공합니다.&amp;nbsp;이&amp;nbsp;글에서는&amp;nbsp;TransactionManager가&amp;nbsp;무엇인지,&amp;nbsp;어떻게&amp;nbsp;동작하는지,&amp;nbsp;그리고&amp;nbsp;어떻게&amp;nbsp;설정하고&amp;nbsp;사용하는지에&amp;nbsp;대해&amp;nbsp;기초부터&amp;nbsp;상세하게&amp;nbsp;알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.&amp;nbsp;트랜잭션(Transaction)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 데이터베이스에서 **원자적(Atomic)**으로 수행되어야 하는 일련의 작업을 의미합니다. 즉, 여러 작업이 하나의 단위로 처리되어야 하며, 모두 성공하거나 모두 실패해야 합니다. 이는 데이터의 무결성을 유지하는 데 중요합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.1 트랜잭션의 4가지 특성(ACID)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Atomicity(원자성): 트랜잭션 내의 모든 작업이 완벽하게 수행되거나, 전혀 수행되지 않아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistency(일관성): 트랜잭션 전후에 데이터의 일관성이 유지되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Isolation(격리성): 동시에 실행되는 트랜잭션들이 서로의 작업에 영향을 주지 않아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Durability(지속성):&amp;nbsp;트랜잭션이&amp;nbsp;성공적으로&amp;nbsp;완료되면&amp;nbsp;그&amp;nbsp;결과는&amp;nbsp;영구적으로&amp;nbsp;반영되어야&amp;nbsp;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&amp;nbsp;TransactionManager란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionManager는 스프링에서 트랜잭션을 관리하기 위한 인터페이스입니다. 다양한 데이터 접근 기술(JDBC, JPA, Hibernate 등)에 대해 일관된 트랜잭션 관리 기능을 제공합니다. 이를 통해 개발자는 구체적인 트랜잭션 관리 로직을 신경 쓰지 않고도 트랜잭션을 제어할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;스프링은&amp;nbsp;PlatformTransactionManager&amp;nbsp;인터페이스를&amp;nbsp;통해&amp;nbsp;다양한&amp;nbsp;구현체를&amp;nbsp;제공하며,&amp;nbsp;각&amp;nbsp;구현체는&amp;nbsp;특정&amp;nbsp;데이터&amp;nbsp;접근&amp;nbsp;기술에&amp;nbsp;맞게&amp;nbsp;동작합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 스프링의 TransactionManager 종류&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1 DataSourceTransactionManager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC를 사용하여 데이터베이스에 접근하는 경우로 MyBatis나 순수 MyBatis나 순수 JDBC를 사용하는 어플리케이션에서 사용하는 Manager입니다. DataSrouce를 반드시 설정해야 하며, JDBC 커넥션을 직접 관리하는 것을 주의해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727142511814&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.2 JpaTransactionManager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA(Java Persistence AP)를 사용하여 데이터베이스에 접근하는 경우 사용되며 Spring Data Jpa나 Hibernate를 JPA 구현체로 사용하는 어플리케이션에서 주로 사용됩니다. EntityManagerFactory를 통해 엔티티 매니저를 생성하며, JPA 표준에 따라 동작합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727142578028&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTranscationManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory);
    return transactionManager;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3 HinbernateTransactionManager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate를 JPA 없이 직접 사용하여 데이터베이스에 접근하는 경우에 주로 사용합니다. Hinernate 고유의 기능을 사용할 때 적합하며 SessionFactory를 필요로 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727142770490&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) {
    return new HibernateTransactionManager(sessionFactory);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.4 JtaTransactionManager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 트랜잭션이나 글로벌 트랜잭션이 필요한 경우 사용하며, 여러개의 데이터 소스나 리소스 매니저를 사용하는 어플리케이션에서 사용합니다. JTA (Java Transaction API)를 구현한 어플리에키션 서버나 분산 트랜잭션 관리 시스템이 필요하여 사용시 충분한 학습이 필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727142934997&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PlatformTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager) {
    return new JtaTransactionManager(userTransaction, transactionManager);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 스프링 부트에서 TransactionManager 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링&amp;nbsp;부트에서는&amp;nbsp;자동&amp;nbsp;설정(auto-configuration)을&amp;nbsp;통해&amp;nbsp;대부분의&amp;nbsp;경우에&amp;nbsp;적절한&amp;nbsp;TransactionManager를&amp;nbsp;자동으로&amp;nbsp;설정해줍니다.&amp;nbsp;하지만&amp;nbsp;여러&amp;nbsp;개의&amp;nbsp;데이터&amp;nbsp;소스를&amp;nbsp;사용하는&amp;nbsp;경우나&amp;nbsp;커스텀&amp;nbsp;설정이&amp;nbsp;필요한&amp;nbsp;경우에는&amp;nbsp;명시적으로&amp;nbsp;설정해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.1 단일 데이터 소스의 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링&amp;nbsp;부트는&amp;nbsp;기본적으로&amp;nbsp;DataSourceTransactionManager나&amp;nbsp;JpaTransactionManager를&amp;nbsp;자동으로&amp;nbsp;설정합니다.&amp;nbsp;별도의&amp;nbsp;설정&amp;nbsp;없이도&amp;nbsp;@Transactional&amp;nbsp;어노테이션을&amp;nbsp;사용하여&amp;nbsp;트랜잭션을&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.2 다중 데이터 소스의 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 데이터 소스를 사용하는 경우 각 데이터 소스에 대해 별도의 TransactionManager를 설정해야 합니다. @Primary 어노테이션을 사용하여 기본 TransactionManager를 지정할 수 있습니다. 각 트랜젝션 매니저는 해당하는 데이터 소스를 사용하도록 설정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727143067063&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSource firstDataSource() {
        // 첫 번째 데이터 소스 설정
    }

    @Bean
    public DataSource secondDataSource() {
        // 두 번째 데이터 소스 설정
    }

    @Bean
    @Primary
    public PlatformTransactionManager firstTransactionManager() {
        return new DataSourceTransactionManager(firstDataSource());
    }

    @Bean
    public PlatformTransactionManager secondTransactionManager() {
        return new DataSourceTransactionManager(secondDataSource());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.3 @Transactional에서 트랜잭션 매니저 지정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 TransactionManager가 있는 경우 @Transactional 어노테이션에서 transactionManager 속성을 사용하여 어떤 매니저를 사용할지 지정할 수 있습니다. value로 지정하는 transactionManager 속성에는 TransactionManager를 bean으로 등록한 이름을 문자열로 기입합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727143160245&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MyService {

    @Transactional(value=&quot;firstTransactionManager&quot;)
    public void methodUsingFirstDataSource() {
        // 첫 번째 데이터 소스 사용
    }

    @Transactional(value=&quot;secondTransactionManager&quot;)
    public void methodUsingSecondDataSource() {
        // 두 번째 데이터 소스 사용
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. @Transactional과 TransactionManager의 관계 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional&amp;nbsp;어노테이션은&amp;nbsp;트랜잭션&amp;nbsp;범위를&amp;nbsp;선언적으로&amp;nbsp;지정하기&amp;nbsp;위한&amp;nbsp;방법입니다.&amp;nbsp;스프링은&amp;nbsp;TransactionManager를&amp;nbsp;사용하여&amp;nbsp;해당&amp;nbsp;범위&amp;nbsp;내의&amp;nbsp;트랜잭션을&amp;nbsp;관리합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.1 @Transactional 어노테이션의 주요 속성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;propagation: 트랜잭션 전파 방식을 지정합니다.&lt;/li&gt;
&lt;li&gt;isolation: 트랜잭션 격리 수준을 지정합니다.&lt;/li&gt;
&lt;li&gt;timeout: 트랜잭션의 최대 지속 시간을 지정합니다(초 단위).&lt;/li&gt;
&lt;li&gt;readOnly: 트랜잭션이 읽기 전용인지 여부를 지정합니다.&lt;/li&gt;
&lt;li&gt;rollbackFor: 롤백을 유발할 예외를 지정합니다.&lt;/li&gt;
&lt;li&gt;noRollbackFor:&amp;nbsp;롤백하지&amp;nbsp;않을&amp;nbsp;예외를&amp;nbsp;지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.2 트랜젝션 전파 (propagation)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;REQUIRED: 기본 값으로, 현재 트랜잭션이 존재하면 참여하고, 없으면 새로운 트랜잭션을 시작합니다.&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하며, 기존 트랜잭션은 보류됩니다.&lt;/li&gt;
&lt;li&gt;SUPPORTS: 현재 트랜잭션이 존재하면 참여하고, 없으면 트랜잭션 없이 실행합니다.&lt;/li&gt;
&lt;li&gt;NOT_SUPPORTED: 트랜잭션 없이 실행하며, 기존 트랜잭션은 보류됩니다.&lt;/li&gt;
&lt;li&gt;MANDATORY: 현재 트랜잭션이 존재해야 하며, 없으면 예외를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;NEVER: 트랜잭션이 존재하면 예외를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;NESTED:&amp;nbsp;중첩&amp;nbsp;트랜잭션을&amp;nbsp;시작합니다(특정&amp;nbsp;TransactionManager에서만&amp;nbsp;지원).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.3 트랜젝션 격리 수준&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DEFAULT: 데이터베이스의 기본 격리 수준을 사용합니다.&lt;/li&gt;
&lt;li&gt;READ_UNCOMMITTED: 커밋되지 않은 데이터를 읽을 수 있습니다(Dirty Read 허용).&lt;/li&gt;
&lt;li&gt;READ_COMMITTED: 커밋된 데이터만 읽을 수 있습니다.&lt;/li&gt;
&lt;li&gt;REPEATABLE_READ: 동일한 트랜잭션 내에서 동일한 데이터를 읽으면 항상 같은 결과를 반환합니다.&lt;/li&gt;
&lt;li&gt;SERIALIZABLE:&amp;nbsp;가장&amp;nbsp;엄격한&amp;nbsp;격리&amp;nbsp;수준으로,&amp;nbsp;동시성은&amp;nbsp;떨어지지만&amp;nbsp;데이터&amp;nbsp;일관성은&amp;nbsp;높습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1727143517363&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
transactionManager: 사용할 트랜잭션 매니저를 지정합니다.
propagation: 트랜잭션 전파 방식을 REQUIRED로 설정합니다.
isolation: 격리 수준을 READ_COMMITTED로 설정합니다.
timeout: 트랜잭션 최대 지속 시간을 30초로 설정합니다.
readOnly: 읽기 전용이 아님을 표시합니다.
rollbackFor: Exception이 발생하면 롤백합니다.
*/

@Transactional(
    transactionManager = &quot;firstTransactionManager&quot;,
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    timeout = 30,
    readOnly = false,
    rollbackFor = Exception.class
)
public void transactionalMethod() {
    // 트랜잭션이 필요한 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 프로그래밍 방식의 트랜잭션 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은&amp;nbsp;선언적&amp;nbsp;트랜잭션&amp;nbsp;관리(@Transactional)&amp;nbsp;외에도&amp;nbsp;프로그래밍&amp;nbsp;방식으로&amp;nbsp;트랜잭션을&amp;nbsp;제어할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;방법을&amp;nbsp;제공합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6.1 TransactionTemplate 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionTemplate을 사용하면 프로그래밍 방식으로 트랜잭션을 관리할 수 있습니다. transactionTemplate.execute() 메소드는 트랜잭션을 시작하고, 콜백 내부의 로직을 실행합니다. 예외 발생시 자동으로 롤백합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727143590486&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MyService {

    private final TransactionTemplate transactionTemplate;

    public MyService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void executeInTransaction() {
        transactionTemplate.execute(status -&amp;gt; {
            // 트랜잭션 내에서 실행할 로직
            return null;
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6.2 TransactionManager 직접 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlatformTransactionManager를 직접 사용하여 트랜잭션을 제어할 수 있습니다. getTransaction() 메소드로 직접 트랜잭션을 시작하며 commit() 메소드로 트랜잭션을 커밋 혹은, rollback() 메소드로 롤백 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727143655620&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MyService {

    private final PlatformTransactionManager transactionManager;

    public MyService(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void executeInTransaction() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 트랜잭션 내에서 실행할 로직

            transactionManager.commit(status);
        } catch (Exception ex) {
            transactionManager.rollback(status);
            throw ex;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 심화 주제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7.1 ChainedTransactionManager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 TransactionManager를 동시에 사용할 때 ChainedTransactionManager를 사용하여 트랜잭션을 체인으로 묶을 수 있습니다. ChainedTransactionManager는&amp;nbsp;순차적으로&amp;nbsp;트랜잭션을&amp;nbsp;관리하며,&amp;nbsp;하나의&amp;nbsp;트랜잭션이라도&amp;nbsp;실패하면&amp;nbsp;전체를&amp;nbsp;롤백합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727143720349&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PlatformTransactionManager chainedTransactionManager() {
    return new ChainedTransactionManager(firstTransactionManager(), secondTransactionManager());
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7.2 Custom TransactionManager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정한 요구사항이 있는 경우 커스텀 트랜잭션 매니저를 구현할 수 있습니다. 트랜잭션 관리의 복잡성을 고려하여 충분한 이해가 있을 때만 구현하는 것을 권장드립니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727143765770&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomTransactionManager implements PlatformTransactionManager {
    @Override
    public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
        // 커스텀 로직
    }

    @Override
    public void commit(TransactionStatus status) throws TransactionException {
        // 커밋 로직
    }

    @Override
    public void rollback(TransactionStatus status) throws TransactionException {
        // 롤백 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 실전 팁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션&amp;nbsp;관리는&amp;nbsp;애플리케이션의&amp;nbsp;안정성과&amp;nbsp;데이터의&amp;nbsp;무결성을&amp;nbsp;유지하는&amp;nbsp;데&amp;nbsp;핵심적인&amp;nbsp;역할을&amp;nbsp;합니다.&amp;nbsp;실무에서&amp;nbsp;트랜잭션을&amp;nbsp;효과적으로&amp;nbsp;관리하기&amp;nbsp;위해서는&amp;nbsp;몇&amp;nbsp;가지&amp;nbsp;중요한&amp;nbsp;사항들을&amp;nbsp;고려해야&amp;nbsp;합니다.&amp;nbsp;아래에서는&amp;nbsp;트랜잭션&amp;nbsp;관리&amp;nbsp;시&amp;nbsp;유의해야&amp;nbsp;할&amp;nbsp;실전&amp;nbsp;팁을&amp;nbsp;구체적인&amp;nbsp;상황과&amp;nbsp;예시를&amp;nbsp;통해&amp;nbsp;자세히&amp;nbsp;설명하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.1 적절한 TransactionManager 선택하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 데이터 접근 기술에 맞는 TransactionManager를 선택해야 합니다. 예를 들어, JPA를 사용하면서 DataSourceTransactionManager를 사용하면 예상치 못한 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144128555&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 설정은 DataSourceTransactionManager를 사용하여 트랜잭션을 관리합니다. 그런데 애플리케이션에서 JPA를 사용하고 있다면, 이 설정은 적절하지 않습니다. JPA는 내부적으로 엔티티 매니저를 사용하며, DataSourceTransactionManager는 JDBC 커넥션을 직접 제어하기 때문에 트랜잭션이 제대로 동작하지 않을 수 있습니다. 때문에 JPA를 사용하는 경우 JpaTransactionManager를 사용해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144172488&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하므로써 JPA의 영속성 컨텍스트와 트랜잭션이 올바르게 연동될 수 있습니다. 각&amp;nbsp;데이터&amp;nbsp;접근&amp;nbsp;기술은&amp;nbsp;트랜잭션을&amp;nbsp;처리하는&amp;nbsp;방식이&amp;nbsp;다릅니다.&amp;nbsp;따라서&amp;nbsp;사용하는&amp;nbsp;기술에&amp;nbsp;맞는&amp;nbsp;TransactionManager를&amp;nbsp;선택해야&amp;nbsp;트랜잭션이&amp;nbsp;예상대로&amp;nbsp;동작하고&amp;nbsp;데이터의&amp;nbsp;무결성을&amp;nbsp;보장할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링&amp;nbsp;부트를&amp;nbsp;사용하면&amp;nbsp;대부분의&amp;nbsp;경우&amp;nbsp;자동으로&amp;nbsp;적절한&amp;nbsp;TransactionManager를&amp;nbsp;설정해주지만,&amp;nbsp;여러&amp;nbsp;데이터&amp;nbsp;소스를&amp;nbsp;사용하거나&amp;nbsp;복잡한&amp;nbsp;설정이&amp;nbsp;필요한&amp;nbsp;경우에는&amp;nbsp;직접&amp;nbsp;명시적으로&amp;nbsp;설정해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.2 트랜잭션 범위 최소화하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은&amp;nbsp;가능한&amp;nbsp;한&amp;nbsp;짧게&amp;nbsp;유지하는&amp;nbsp;것이&amp;nbsp;좋습니다.&amp;nbsp;트랜잭션&amp;nbsp;범위가&amp;nbsp;길어지면&amp;nbsp;데이터베이스&amp;nbsp;락(lock)으로&amp;nbsp;인해&amp;nbsp;동시성&amp;nbsp;문제가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144235617&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void processOrder() {
    // 사용자 입력 대기
    String userInput = getUserInput();

    // 비즈니스 로직 처리
    performBusinessLogic(userInput);

    // 외부 서비스 호출
    externalService.call();

    // 데이터베이스 업데이트
    updateDatabase();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 메소드에서는 트랜잭션이 시작된 이후 사용자 입력을 기다리고, 외부 서비스도 호출합니다. 이는 트랜잭션 범위를 불필요하게 넓혀 데이터베이스 커넥션과 락이 오래 유지되어 성능에 악영향을 미칩니다. 때문에 트랜잭션이&amp;nbsp;필요한&amp;nbsp;부분만&amp;nbsp;트랜잭션으로&amp;nbsp;감싸도록&amp;nbsp;메소드를&amp;nbsp;분리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144286575&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void processOrder() {
    // 사용자 입력 대기
    String userInput = getUserInput();

    // 비즈니스 로직 처리
    performBusinessLogic(userInput);

    // 외부 서비스 호출
    externalService.call();

    // 데이터베이스 업데이트
    updateDatabaseTransactional();
}

@Transactional
public void updateDatabaseTransactional() {
    // 데이터베이스 업데이트 로직
    updateDatabase();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게&amp;nbsp;하면&amp;nbsp;트랜잭션&amp;nbsp;범위가&amp;nbsp;데이터베이스&amp;nbsp;업데이트&amp;nbsp;부분으로&amp;nbsp;제한되어&amp;nbsp;자원&amp;nbsp;사용을&amp;nbsp;최소화할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 활성화된 동안에는 데이터베이스 커넥션과 자원이 점유되며, 데이터베이스 락이 걸릴 수 있습니다. 이는 다른 트랜잭션이 해당 자원에 접근하는 것을 방해하여 동시성 문제가 발생할 수 있습니다. 때문에 트랜잭션이&amp;nbsp;필요한&amp;nbsp;최소한의&amp;nbsp;범위를&amp;nbsp;식별하고,&amp;nbsp;그&amp;nbsp;부분만&amp;nbsp;트랜잭션으로&amp;nbsp;처리합니다.&amp;nbsp;이를&amp;nbsp;위해&amp;nbsp;비즈니스&amp;nbsp;로직을&amp;nbsp;잘&amp;nbsp;분리하고&amp;nbsp;설계하는&amp;nbsp;것이&amp;nbsp;중요합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.3 예외 처리 주의하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 RuntimeException과 Error가 발생할 때 트랜잭션이 롤백됩니다. 체크&amp;nbsp;예외(Exception)에&amp;nbsp;대해서도&amp;nbsp;롤백이&amp;nbsp;필요하면&amp;nbsp;rollbackFor&amp;nbsp;속성을&amp;nbsp;사용해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144368679&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void transferFunds(Account from, Account to, BigDecimal amount) {
    try {
        from.withdraw(amount);
        to.deposit(amount);
    } catch (InsufficientFundsException e) {
        // 잔액 부족 예외 처리
        log.error(&quot;잔액이 부족합니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의&amp;nbsp;코드에서&amp;nbsp;InsufficientFundsException은&amp;nbsp;체크&amp;nbsp;예외이며,&amp;nbsp;트랜잭션은&amp;nbsp;기본적으로&amp;nbsp;체크&amp;nbsp;예외가&amp;nbsp;발생해도&amp;nbsp;롤백되지&amp;nbsp;않습니다.&amp;nbsp;또한&amp;nbsp;예외를&amp;nbsp;catch하여&amp;nbsp;처리했기&amp;nbsp;때문에&amp;nbsp;예외가&amp;nbsp;밖으로&amp;nbsp;전파되지&amp;nbsp;않아&amp;nbsp;트랜잭션이&amp;nbsp;커밋됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144395809&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void transferFunds(Account from, Account to, BigDecimal amount) throws InsufficientFundsException {
    from.withdraw(amount);
    to.deposit(amount);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 이처럼 예외를 다시 던져서 InsufficientFundsException을 호출자에게 전달하여 트랜잭션이 롤백되도록 하는 방법이 있고,&lt;/p&gt;
&lt;pre id=&quot;code_1727144424320&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(rollbackFor = InsufficientFundsException.class)
public void transferFunds(Account from, Account to, BigDecimal amount) {
    try {
        from.withdraw(amount);
        to.deposit(amount);
    } catch (InsufficientFundsException e) {
        // 추가적인 예외 처리
        log.error(&quot;잔액이 부족합니다.&quot;);
        throw e; // 예외를 다시 던져 롤백되도록 함
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rollbackFor 속성을 사용하여 특정 체크 예외에 대해서도 롤백되도록 설정하는 방법 등이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 예외 발생 시 자동으로 롤백되지만, 체크 예외에 대해서는 기본적으로 롤백되지 않습니다. 또한 예외를 catch하여 내부에서 처리하면 트랜잭션은 커밋됩니다. 이는 데이터의 무결성을 해칠 수 있습니다. 개인적인 생각이지만 예외를 적절히 처리하고, 필요한 경우 rollbackFor를 사용하여 롤백 대상을 명시적으로 지정합니다. 예외를 catch한 후 로그만 남기고 다시 예외를 던지는 것이 좋다고 생각합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.4 테스트환경에서의 트랜잭션 관리 주의하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드에서 @Transactional을 사용하면 기본적으로 테스트가 끝나면 롤백됩니다. 데이터베이스&amp;nbsp;상태를&amp;nbsp;확인하려면&amp;nbsp;@Rollback(false)&amp;nbsp;어노테이션을&amp;nbsp;사용하여&amp;nbsp;롤백을&amp;nbsp;방지할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.5 데이터베이스 설정 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스의 autoCommit 설정이 false로 되어 있어야 합니다. JDBC&amp;nbsp;드라이버와&amp;nbsp;데이터베이스&amp;nbsp;버전이&amp;nbsp;호환되는지&amp;nbsp;확인해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.6 트랜잭션 전파(propagation) 속성 이해하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한&amp;nbsp;서비스&amp;nbsp;계층&amp;nbsp;구조에서&amp;nbsp;메소드&amp;nbsp;간에&amp;nbsp;트랜잭션&amp;nbsp;전파&amp;nbsp;방식이&amp;nbsp;잘못&amp;nbsp;설정되면&amp;nbsp;트랜잭션이&amp;nbsp;예상치&amp;nbsp;못하게&amp;nbsp;동작할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144563599&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OuterService {

    @Autowired
    private InnerService innerService;

    @Transactional
    public void outerMethod() {
        // 일부 로직
        innerService.innerMethod();
        // 추가 로직
    }
}

@Service
public class InnerService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void innerMethod() {
        // 트랜잭션이 필요한 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의&amp;nbsp;코드에서&amp;nbsp;innerMethod()는&amp;nbsp;Propagation.REQUIRES_NEW로&amp;nbsp;설정되어&amp;nbsp;새로운&amp;nbsp;트랜잭션을&amp;nbsp;시작합니다.&amp;nbsp;만약&amp;nbsp;innerMethod()에서&amp;nbsp;예외가&amp;nbsp;발생하여&amp;nbsp;롤백되더라도&amp;nbsp;outerMethod()의&amp;nbsp;트랜잭션은&amp;nbsp;영향을&amp;nbsp;받지&amp;nbsp;않고&amp;nbsp;커밋될&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 트랜잭션&amp;nbsp;전파&amp;nbsp;속성을&amp;nbsp;적절히&amp;nbsp;설정하여&amp;nbsp;의도한&amp;nbsp;대로&amp;nbsp;동작하도록&amp;nbsp;합니다.&amp;nbsp;만약&amp;nbsp;innerMethod()에서&amp;nbsp;예외가&amp;nbsp;발생하면&amp;nbsp;전체&amp;nbsp;트랜잭션을&amp;nbsp;롤백하고&amp;nbsp;싶다면&amp;nbsp;Propagation.REQUIRED를&amp;nbsp;사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144621205&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class InnerService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void innerMethod() {
        // 트랜잭션이 필요한 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 전파 속성은 메소드 간 트랜잭션의 전파 방식을 결정하며, 잘못 설정하면 데이터의 무결성이 훼손될 수 있습니다. 때문에 기본적으로&amp;nbsp;Propagation.REQUIRED를&amp;nbsp;사용하고,&amp;nbsp;특별한&amp;nbsp;이유가&amp;nbsp;있는&amp;nbsp;경우에만&amp;nbsp;다른&amp;nbsp;전파&amp;nbsp;속성을&amp;nbsp;사용합니다.&amp;nbsp;전파&amp;nbsp;속성을&amp;nbsp;설정할&amp;nbsp;때는&amp;nbsp;트랜잭션의&amp;nbsp;경계를&amp;nbsp;명확히&amp;nbsp;이해하고&amp;nbsp;설정해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.7 트랜잭션 격리 수준 (isolation level) 고려하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 여러 트랜잭션이 수행될 때 데이터의 일관성을 유지하기 위해 트랜잭션 격리 수준을 조정해야 할 수 있습니다. 이 부분은 특정 상황을 예로 들어 보겠습니다. 아래 코드는 재고 수량을 조회하고 업데이트하는 트랜잭션이 동시에 발생하여 일관성 문제가 생기는 경우입니다. 두 개의 트랜잭션이 동시에 실행되면 currentStock이 동일하게 조회되어 재고가 음수가 될 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144743701&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updateStock(Long productId, int quantity) {
    int currentStock = stockRepository.findStockByProductId(productId);
    if (currentStock &amp;gt;= quantity) {
        stockRepository.updateStock(productId, currentStock - quantity);
    } else {
        throw new InsufficientStockException();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 격리 수준을 Isolation.SERIALIZABLE로 설정하여 트랜잭션 간 격리를 강화합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144841381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(isolation = Isolation.SERIALIZABLE)
public void updateStock(Long productId, int quantity) {
    // 동일한 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리 수준은 트랜잭션 간에 얼마나 데이터를 공유할 수 있는지를 결정합니다. 격리 수준이 낮으면 동시성은 높아지지만, 일관성 문제가 발생할 수 있습니다. 때문에 필요에&amp;nbsp;따라&amp;nbsp;격리&amp;nbsp;수준을&amp;nbsp;조정하되,&amp;nbsp;높은&amp;nbsp;격리&amp;nbsp;수준은&amp;nbsp;성능&amp;nbsp;저하를&amp;nbsp;유발할&amp;nbsp;수&amp;nbsp;있으므로&amp;nbsp;주의해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.8 Lazy Initialization 예외 방지하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional&amp;nbsp;메소드&amp;nbsp;내에서&amp;nbsp;지연&amp;nbsp;로딩된&amp;nbsp;엔티티를&amp;nbsp;서비스&amp;nbsp;계층&amp;nbsp;밖에서&amp;nbsp;접근하려고&amp;nbsp;하면&amp;nbsp;LazyInitializationException이&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144955564&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public Order getOrder(Long orderId) {
    Order order = orderRepository.findById(orderId);
    return order; // Order 엔티티의 연관 엔티티들은 아직 로딩되지 않음
}

// 컨트롤러나 뷰에서 order.getItems()를 호출하면 LazyInitializationException 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대한 해결 방법은 여러가지가 있는데 3가지 방법을 소개하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째, 트랜젝션 범위 내에서 필요한 데이터를 모두 로드하는 방법입니다. size() 메소드는 연관 엔티티 로딩 이후 사용이 가능하기 때문에 적절하게 사용해준다면 lazy initialzation 예외를 방지하는데 도움이 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727145048692&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public Order getOrder(Long orderId) {
    Order order = orderRepository.findById(orderId);
    order.getItems().size(); // 연관 엔티티 로딩
    return order;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째,&amp;nbsp; 페치 조인을 사용하는 방법입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727145094919&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public Order getOrder(Long orderId) {
    return orderRepository.findWithItemsById(orderId);
}

// OrderRepository
@Query(&quot;SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :orderId&quot;)
Order findWithItemsById(@Param(&quot;orderId&quot;) Long orderId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째, DTO로 변환하여 반환하는 방법입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727145118871&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public OrderDto getOrder(Long orderId) {
    Order order = orderRepository.findById(orderId);
    return new OrderDto(order);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 범위를 벗어난 후에 지연 로딩된 연관 엔티티에 접근하면 LazyInitializationException이 발생합니다. 이는 트랜잭션 관리와 엔티티의 생명 주기에 대한 이해 부족에서 비롯됩니다. 서비스&amp;nbsp;계층에서&amp;nbsp;필요한&amp;nbsp;데이터를&amp;nbsp;모두&amp;nbsp;로드하거나,&amp;nbsp;DTO를&amp;nbsp;사용하여&amp;nbsp;필요한&amp;nbsp;데이터만&amp;nbsp;반환하는&amp;nbsp;것이&amp;nbsp;좋습니다.&amp;nbsp;필요하다면&amp;nbsp;트랜잭션&amp;nbsp;범위를&amp;nbsp;조정합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8.9 프록시 모드 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 기본적으로 JDK 동적 프록시를 사용하며, 인터페이스 기반으로 프록시를 생성합니다. 클래스 기반 프록시(CGLIB)를 사용하려면 추가 설정이 필요합니다. 때문에 아래 코드는 인터페이스 없이 클래스를 직접 사용하기 때문에 트랜잭션이 적용되지 않을 수 있고, JDK 동적 프로시는 인터페이스가 없으므로 프록시를 생성하지 못하게 되는 현상이 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727145306641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderService {

    @Transactional
    public void processOrder() {
        // 주문 처리 로직
    }
}

// 주입받는 곳
@Autowired
private OrderService orderService;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 인터페이스를 도입하거나 프록시 모드를 CGLIB로 변경하는 등의 해결방법이 있고, 본인의 경우 인터페이스 기반 프로그래밍을 선호하는 방향성을 추천합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727145442059&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 인터페이스 도입
public interface OrderService {
    void processOrder();
}

@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @Transactional
    public void processOrder() {
        // 주문 처리 로직
    }
}


// 프록시모드 CGLIB로 변경
@EnableTransactionManagement(proxyTargetClass = true)
public class AppConfig {
    // 기타 설정
}

// 프록시모드 CGLIB로 변경2 - xml 형식의 spring
// properties file
spring.aop.proxy-target-class=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionManager는&amp;nbsp;스프링에서&amp;nbsp;트랜잭션&amp;nbsp;관리를&amp;nbsp;추상화하여&amp;nbsp;제공하는&amp;nbsp;중요한&amp;nbsp;구성&amp;nbsp;요소입니다.&amp;nbsp;이&amp;nbsp;글에서는&amp;nbsp;TransactionManager의&amp;nbsp;역할과&amp;nbsp;종류,&amp;nbsp;그리고&amp;nbsp;설정과&amp;nbsp;사용&amp;nbsp;방법에&amp;nbsp;대해&amp;nbsp;기초부터&amp;nbsp;상세히&amp;nbsp;알아보았습니다.&amp;nbsp;트랜잭션은&amp;nbsp;애플리케이션의&amp;nbsp;데이터&amp;nbsp;무결성과&amp;nbsp;일관성을&amp;nbsp;유지하는&amp;nbsp;핵심적인&amp;nbsp;요소이므로,&amp;nbsp;정확한&amp;nbsp;이해와&amp;nbsp;적절한&amp;nbsp;사용이&amp;nbsp;필수적입니다.&lt;br /&gt;&lt;br /&gt;이 글이 TransactionManager에 대한 이해를 높이고, 실무에서 올바르게 트랜잭션을 관리하는 데 도움이 되기를 바랍니다.&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>@transactional 작동원리</category>
      <category>@transactional과 transactionmanager의 관계</category>
      <category>jpa transactionmanager</category>
      <category>spring transactionmanager 이해하기</category>
      <category>transactionmanager 사용</category>
      <category>transactionmanager 이해하기</category>
      <category>스프링 @transactional 이해</category>
      <category>스프링 트랜잭션 이해하기</category>
      <category>트랜잭션 이해하기</category>
      <category>프록시모드 cglib로 변경</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/314</guid>
      <comments>https://min-nine.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-TransactionManager%EC%97%90-%EB%8C%80%ED%95%9C-%EC%83%81%EC%84%B8%ED%95%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#entry314comment</comments>
      <pubDate>Tue, 24 Sep 2024 20:54:16 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에서 @Transactional 어노테이션이 작동하지 않을 때 해결 방법</title>
      <link>https://min-nine.tistory.com/entry/Spring-Boot%EC%97%90%EC%84%9C-Transactional-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EC%9D%B4-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 필요한 서비스 메소드에 아무리 @Transactional 어노테이션을 적용해도 트랜잭션이 발생하지 않는 상황에 처할 수 있습니다. 이런 경우, 문제의 원인을 파악하고 해결하기 위해 여러 가지 요소를 점검해야 합니다. @Transactional은 Spring 프레임워크에서 트랜잭션 관리를 위해 매우 중요한 어노테이션이지만, 설정이나 코드 구조에 따라 예상과 다르게 동작할 수 있습니다. 이 글에서는 @Transactional이 작동하지 않는 일반적인 원인과 그 해결 방법을 구체적인 예시와 함께 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 메소드 접근 제어자 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional이 적용된 메소드는 public 접근 제어자를 가져야 합니다. Spring의 AOP는 기본적으로 프록시 기반으로 동작하며, public 메소드에만 적용됩니다. 따라서 private, protected, default(package-private) 접근 제어자를 가진 메소드에는 트랜잭션이 적용되지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138181563&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderService {

    @Transactional
    void processOrder() {
        // 주문 처리 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 processOrder() 메소드는 접근 제어자가 명시되지 않았으므로 default입니다. 이 경우, @Transactional이 효과를 발휘하지 않습니다. 때문에 메소드의 접근 제어자를 public으로 사용해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138223117&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderService {

    @Transactional
    public void processOrder() {
        // 주문 처리 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring&amp;nbsp;AOP는&amp;nbsp;기본적으로&amp;nbsp;프록시&amp;nbsp;패턴을&amp;nbsp;사용하여&amp;nbsp;대상&amp;nbsp;객체를&amp;nbsp;감싸서&amp;nbsp;부가&amp;nbsp;기능을&amp;nbsp;제공합니다.&amp;nbsp;&lt;/b&gt;이때,&amp;nbsp;프록시는&amp;nbsp;대상&amp;nbsp;객체의&amp;nbsp;public&amp;nbsp;메소드만&amp;nbsp;오버라이드하여&amp;nbsp;기능을&amp;nbsp;추가할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;따라서&amp;nbsp;public이&amp;nbsp;아닌&amp;nbsp;메소드는&amp;nbsp;프록시의&amp;nbsp;영향을&amp;nbsp;받지&amp;nbsp;않아&amp;nbsp;트랜잭션이&amp;nbsp;적용되지&amp;nbsp;않습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 자기 자신 호출 (Self-invocation) 문제 검토하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스&amp;nbsp;내에서&amp;nbsp;자신의&amp;nbsp;메소드를&amp;nbsp;호출할&amp;nbsp;때,&amp;nbsp;@Transactional이&amp;nbsp;적용된&amp;nbsp;메소드라도&amp;nbsp;프록시를&amp;nbsp;거치지&amp;nbsp;않기&amp;nbsp;때문에&amp;nbsp;트랜잭션이&amp;nbsp;적용되지&amp;nbsp;않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138309443&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        // 사용자 생성 로직
    }

    public void registerUser(User user) {
        // 기타 처리 로직
        createUser(user); // 자기 자신 메소드 호출
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;registerUser() 메소드에서 createUser()를 호출할 때 트랜잭션이 적용되지 않습니다. 때문에 외부에서 호출하도록 구조적으로 createUser() 메소드를 다른 클래스에서 호출하도록 변경합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138353226&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class UserRegistrationService {

    private final UserService userService;

    public UserRegistrationService(UserService userService) {
        this.userService = userService;
    }

    public void registerUser(User user) {
        // 기타 처리 로직
        userService.createUser(user); // 프록시를 통해 호출
    }
}

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        // 사용자 생성 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 자신을 빈으로 주입받아서 사용하게 할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138375149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class UserService {

    private final UserService self;

    @Autowired
    public UserService(UserService self) {
        this.self = self;
    }

    public void registerUser(User user) {
        // 기타 처리 로직
        self.createUser(user); // 프록시를 통해 호출
    }

    @Transactional
    public void createUser(User user) {
        // 사용자 생성 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 자기&amp;nbsp;자신을&amp;nbsp;주입받을&amp;nbsp;때&amp;nbsp;순환&amp;nbsp;참조&amp;nbsp;문제가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있으므로&amp;nbsp;@Lazy&amp;nbsp;어노테이션을&amp;nbsp;사용하거나&amp;nbsp;구조를&amp;nbsp;재설계하는&amp;nbsp;것이&amp;nbsp;좋습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프록시 대상 객체 타입 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 객체를 직접 캐스팅하거나 구체적인 클래스 타입에 의존하면 트랜잭션이 예상대로 동작하지 않을 수 있습니다.&amp;nbsp;paymentService는 프록시 객체이기 때문에 instanceof 검사가 예상대로 동작하지 않을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138456876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class PaymentService {

    @Transactional
    public void processPayment() {
        // 결제 처리 로직
    }
}

public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    public void initiatePayment() {
        if (paymentService instanceof PaymentService) {
            // ...
        }
        paymentService.processPayment();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 인터페이스를 사용하여 의존성을 주입하고, 구현 클래스에 의존하지 않게 해야 합니다. 프록시&amp;nbsp;객체는&amp;nbsp;실제&amp;nbsp;구현&amp;nbsp;클래스가&amp;nbsp;아닌&amp;nbsp;인터페이스를&amp;nbsp;기반으로&amp;nbsp;생성되기&amp;nbsp;때문에,&amp;nbsp;인터페이스를&amp;nbsp;사용하면&amp;nbsp;이러한&amp;nbsp;문제를&amp;nbsp;회피할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;또한,&amp;nbsp;CGLIB&amp;nbsp;프록시를&amp;nbsp;사용하도록&amp;nbsp;설정하면&amp;nbsp;클래스&amp;nbsp;기반&amp;nbsp;프록시를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있지만,&amp;nbsp;이는&amp;nbsp;추가&amp;nbsp;설정이&amp;nbsp;필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138592273&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface PaymentService {
    void processPayment();
}

@Service
public class PaymentServiceImpl implements PaymentService {

    @Override
    @Transactional
    public void processPayment() {
        // 결제 처리 로직
    }
}

public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    public void initiatePayment() {
        paymentService.processPayment();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 예외 처리 방식 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은&amp;nbsp;기본적으로&amp;nbsp;RuntimeException&amp;nbsp;또는&amp;nbsp;Error가&amp;nbsp;발생할&amp;nbsp;때만&amp;nbsp;트랜잭션을&amp;nbsp;롤백합니다.&amp;nbsp;체크&amp;nbsp;예외(Exception)는&amp;nbsp;트랜잭션&amp;nbsp;롤백을&amp;nbsp;유발하지&amp;nbsp;않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138628130&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updateAccount() throws Exception {
    // 계좌 업데이트 로직
    if (someCondition) {
        throw new Exception(&quot;예외 발생&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 Exception이 발생해도 트랜잭션이 롤백되지 않습니다. 때문에 롤백 대상 예외를 명시적으로 지정할 수 있는 rollbackFor 속성을 사용하여 예외를 명시적으로 지정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138668015&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)
public void updateAccount() throws Exception {
    // 계좌 업데이트 로직
    if (someCondition) {
        throw new Exception(&quot;예외 발생&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는, 예외를 RuntimeException으로 변경하거나 새로운 커스텀 예외를 만들어 RuntimeException을 상속받게 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138728417&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}

@Transactional
public void updateAccount() {
    // 계좌 업데이트 로직
    if (someCondition) {
        throw new CustomException(&quot;예외 발생&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional&amp;nbsp;어노테이션의&amp;nbsp;rollbackFor&amp;nbsp;속성을&amp;nbsp;사용하여&amp;nbsp;롤백할&amp;nbsp;예외를&amp;nbsp;지정할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;반대로,&amp;nbsp;noRollbackFor&amp;nbsp;속성을&amp;nbsp;사용하여&amp;nbsp;롤백하지&amp;nbsp;않을&amp;nbsp;예외를&amp;nbsp;지정할&amp;nbsp;수도&amp;nbsp;있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 트랜잭션 매니저 설정 확인하기 (2개 이상의 데이터 소스 사용시 필수확인)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 데이터 소스를 사용하는 경우 또는 Spring Boot의 자동 설정을 사용하지 않는 경우, 적절한 트랜잭션 매니저가 설정되지 않으면 트랜잭션이 작동하지 않습니다. 여러&amp;nbsp;데이터&amp;nbsp;소스를&amp;nbsp;사용하는&amp;nbsp;경우&amp;nbsp;각&amp;nbsp;데이터&amp;nbsp;소스에&amp;nbsp;대한&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;설정하고,&amp;nbsp;@Transactional에서&amp;nbsp;사용할&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;지정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727138859521&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PlatformTransactionManager firstTransactionManager() {
    return new DataSourceTransactionManager(firstDataSource());
}

@Bean
public PlatformTransactionManager secondTransactionManager() {
    return new DataSourceTransactionManager(secondDataSource());
}

@Transactional(&quot;firstTransactionManager&quot;)
public void methodUsingFirstDataSource() {
    // ...
}

@Transactional(&quot;secondTransactionManager&quot;)
public void methodUsingSecondDataSource() {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring&amp;nbsp;Boot를&amp;nbsp;사용하면&amp;nbsp;기본적으로&amp;nbsp;하나의&amp;nbsp;데이터&amp;nbsp;소스와&amp;nbsp;트랜잭션&amp;nbsp;매니저가&amp;nbsp;자동으로&amp;nbsp;설정됩니다.&amp;nbsp;그러나&amp;nbsp;여러&amp;nbsp;데이터&amp;nbsp;소스를&amp;nbsp;사용하거나&amp;nbsp;커스텀&amp;nbsp;설정이&amp;nbsp;필요한&amp;nbsp;경우에는&amp;nbsp;명시적으로&amp;nbsp;설정해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Spring AOP 설정 확인하기 (XML 기반의 Spring 사용시 유의)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 AOP 설정이 누락되거나 잘못되면 @Transactional이 적용되지 않습니다. 특히 XML 설정을 사용하는 경우 tx:annotation-driven 태그가 누락될 수 있습니다. Spring Boot를 사용하는 경우는 자동으로 설정되므로 특별한 설정이 필요 없습니다. @SpringBootApplication이 있는지 확인만 해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 프레임워크를 사용하는 경우 @EnableTransactionManagement 어노테이션을 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139006058&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableTransactionManagement
public class AppConfig {
    // 기타 설정
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML 설정을 사용하는 경우 Bean 등록을 아래와 같이 해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139335655&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;
       ...&amp;gt;
    &amp;lt;tx:annotation-driven /&amp;gt;
    &amp;lt;!-- 기타 빈 설정 --&amp;gt;
&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EnableTransactionManagement&amp;nbsp;어노테이션은&amp;nbsp;트랜잭션&amp;nbsp;어노테이션을&amp;nbsp;활성화하며,&amp;nbsp;Spring의&amp;nbsp;AOP&amp;nbsp;기능을&amp;nbsp;사용하여&amp;nbsp;트랜잭션을&amp;nbsp;적용할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;합니다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 빈(Bean) 등록 여부 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional이&amp;nbsp;적용된&amp;nbsp;클래스가&amp;nbsp;Spring&amp;nbsp;컨테이너에서&amp;nbsp;관리되지&amp;nbsp;않으면&amp;nbsp;어노테이션이&amp;nbsp;효과가&amp;nbsp;없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139384846&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NotificationService {

    @Transactional
    public void sendNotification() {
        // 알림 발송 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 클래스는 아무런 어노테이션이 없으므로 Spring의 빈으로 등록되지 않습니다. 클래스에 적절한 어노테이션을 추가하여 빈으로 등록해 줘야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139415311&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class NotificationService {

    @Transactional
    public void sendNotification() {
        // 알림 발송 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727139445051&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// xml 기반 spring의 경우
&amp;lt;bean id=&quot;notificationService&quot; class=&quot;com.example.NotificationService&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring&amp;nbsp;컨테이너에서&amp;nbsp;관리되지&amp;nbsp;않는&amp;nbsp;객체는&amp;nbsp;@Transactional&amp;nbsp;뿐만&amp;nbsp;아니라&amp;nbsp;다른&amp;nbsp;Spring의&amp;nbsp;기능들도&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;없습니다.&amp;nbsp;반드시&amp;nbsp;빈으로&amp;nbsp;등록되어야&amp;nbsp;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 데이터베이스 설정 및 JDBC URL 확인하기.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 연결 설정이 잘못되었거나 자동 커밋 모드로 설정되어 있으면 트랜잭션이 적용되지 않을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139489356&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// properties setting file
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?autoCommit=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;autoCommit=true로 설정되어 있으면 트랜잭션이 즉시 커밋되어 롤백이 불가능합니다. 때문에 autoCommit을 false로 설정하거나 기본값을 사용해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139553558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// properties setting file
spring.datasource.url=jdbc:mysql://localhost:3306/mydb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부&amp;nbsp;데이터베이스는&amp;nbsp;자동&amp;nbsp;커밋&amp;nbsp;모드가&amp;nbsp;기본값으로&amp;nbsp;설정되어&amp;nbsp;있을&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;DataSource&amp;nbsp;설정&amp;nbsp;시&amp;nbsp;autoCommit&amp;nbsp;옵션을&amp;nbsp;명시적으로&amp;nbsp;설정하는&amp;nbsp;것이&amp;nbsp;좋습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 테스트 코드에서의 트랜젝션 적용 확인하기.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit이나&amp;nbsp;테스트&amp;nbsp;프레임워크에서&amp;nbsp;트랜잭션이&amp;nbsp;예상과&amp;nbsp;다르게&amp;nbsp;동작할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139675089&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testCreateUser() {
        userService.createUser(new User());
        // 데이터베이스에 커밋되지 않을 수 있음
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우 테스트 메소드에 @Transactional 어노테이션을 추가하거나 @Rollback 어노테이션을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139722497&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@Transactional
public void testCreateUser() {
    userService.createUser(new User());
    // 테스트 종료 시 롤백됨
}

@Test
@Transactional
@Rollback(false)
public void testCreateUser() {
    userService.createUser(new User());
    // 테스트 종료 시 커밋됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경에서는 기본적으로 각 테스트 메소드마다 트랜잭션이 적용되고, 테스트가 끝나면 롤백됩니다. 따라서 데이터베이스 상태를 확인하려면 @Rollback(false)를 사용하여 커밋해야 확인이 가능하지만 테스트케이스에서 실제 사용하는 RDB로의 CRUD는 권장하지는 않습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 트랜잭션 전파(propagation) 설정 확인하기.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션&amp;nbsp;전파&amp;nbsp;속성이&amp;nbsp;예상과&amp;nbsp;다르게&amp;nbsp;설정되어&amp;nbsp;있으면&amp;nbsp;트랜잭션이&amp;nbsp;제대로&amp;nbsp;적용되지&amp;nbsp;않을&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139820025&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.NEVER)
public void method() {
    // 트랜잭션을 사용하지 않음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 적절한 전파 속성을 사용해야 하고, 기본값인 Propagation.REQUIRED를 사용하거나 필요한 전파 속성을 설정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1727139845888&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRED)
public void method() {
    // 트랜잭션이 필요한 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션&amp;nbsp;전파&amp;nbsp;속성은&amp;nbsp;메소드&amp;nbsp;간&amp;nbsp;트랜잭션의&amp;nbsp;전파&amp;nbsp;방식을&amp;nbsp;결정합니다.&amp;nbsp;상황에&amp;nbsp;맞는&amp;nbsp;전파&amp;nbsp;속성을&amp;nbsp;사용하여&amp;nbsp;트랜잭션이&amp;nbsp;올바르게&amp;nbsp;적용되도록&amp;nbsp;해야&amp;nbsp;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional&amp;nbsp;어노테이션이&amp;nbsp;작동하지&amp;nbsp;않을&amp;nbsp;때는&amp;nbsp;여러&amp;nbsp;가지&amp;nbsp;원인이&amp;nbsp;있을&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;그&amp;nbsp;중&amp;nbsp;일부는&amp;nbsp;미묘한&amp;nbsp;차이로&amp;nbsp;인해&amp;nbsp;쉽게&amp;nbsp;놓칠&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;접근&amp;nbsp;제어자,&amp;nbsp;자기&amp;nbsp;자신&amp;nbsp;호출,&amp;nbsp;예외&amp;nbsp;처리&amp;nbsp;방식,&amp;nbsp;트랜잭션&amp;nbsp;매니저&amp;nbsp;설정,&amp;nbsp;AOP&amp;nbsp;설정,&amp;nbsp;빈&amp;nbsp;등록&amp;nbsp;여부,&amp;nbsp;데이터베이스&amp;nbsp;설정&amp;nbsp;등을&amp;nbsp;꼼꼼히&amp;nbsp;점검해야&amp;nbsp;합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 데이터의 무결성과 일관성을 유지하는 데 핵심적인 역할을 하므로, 정확한 설정과 이해가 필수적입니다. 위에서 소개한 각 예시와 해결 방법이 @Transactional 관련 문제를 해결하는 데 도움이 되기를 기원합니다.&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>@transactional 미작동</category>
      <category>@transactional 비활성화 체크</category>
      <category>@transactional 사용법</category>
      <category>@transactional 오작동</category>
      <category>@transactional 작동 안함</category>
      <category>@transactional 적용 안됨</category>
      <category>@transactional 적용하기</category>
      <category>@transactional 활성화</category>
      <category>spring @transactional 작동 안함</category>
      <category>spring boot @transactional 작동 안함</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/313</guid>
      <comments>https://min-nine.tistory.com/entry/Spring-Boot%EC%97%90%EC%84%9C-Transactional-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98%EC%9D%B4-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95#entry313comment</comments>
      <pubDate>Tue, 24 Sep 2024 10:08:18 +0900</pubDate>
    </item>
    <item>
      <title>[Legacy to Modernization] 2. 형식없는 Appliction API를 표준화된 RestAPI로 교체하기.</title>
      <link>https://min-nine.tistory.com/entry/Legacy-to-Modernization-2-%ED%98%95%EC%8B%9D%EC%97%86%EB%8A%94-Appliction-API%EB%A5%BC-%ED%91%9C%EC%A4%80%ED%99%94%EB%90%9C-RestAPI%EB%A1%9C-%EA%B5%90%EC%B2%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 소프트웨어 개발 환경에서는 API(Application Programming Interface)가 중요한 역할을 합니다. API는 다양한 소프트웨어 시스템 간의 상호 작용을 가능하게 하며, 특히 REST API(Representational State Transfer API)와 JSON API는 유연성과 확장성이 뛰어나 많은 시스템에서 채택되고 있습니다. 그러나 많은 기존 시스템에서는 여전히 오래된 형태의 API를 사용하고 있으며, 이를 REST API 및 JSON API로 전환하는 것이 필요합니다. 본 논문에서는 기존 시스템의 API가 REST API가 아닌 일반 Application API임을 분석하고, 이를 REST API 및 JSON API로 전환하는 과정과 장단점을 설명합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기존 시스템 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템은 흔히 Application API로 구축되어 있으며, 다음과 같은 특징을 가집니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;상태 유지(Stateful)&lt;/b&gt;: 서버가 클라이언트의 상태를 기억하고 관리합니다. 이는 서버에 부하를 증가시키고, 확장성을 저해할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 URI 구조&lt;/b&gt;: 자원에 접근하는 URI가 직관적이지 않거나 비효율적입니다. 예를 들어, domain.com/member/get_member_list/1234와 같은 형식입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성 없는 데이터 포맷&lt;/b&gt;: JSON, XML, Plain Text 등 다양한 포맷이 혼재되어 데이터 처리의 일관성을 저해합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드의 비표준 사용&lt;/b&gt;: 주로 GET과 POST 메서드를 사용하여 모든 CRUD 작업을 진행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. REST API 및 JSON API의 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API 및 JSON API로 전환함으로써 얻을 수 있는 주요 장점은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무상태성(Stateless)&lt;/b&gt;: 각 요청은 독립적이며, 서버는 클라이언트의 상태를 유지하지 않습니다. 이는 확장성과 관리의 용이성을 높입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 데이터 포맷&lt;/b&gt;: JSON 포맷을 사용하여 데이터 일관성을 유지하며, 이를 통해 클라이언트와 서버 간의 상호 운용성이 개선됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;표준화된 HTTP 메서드 사용&lt;/b&gt;: GET, POST, PUT, DELETE 등 HTTP 메서드를 명확하게 구분하여 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명확한 URI 구조&lt;/b&gt;: 자원 기반의 URI 설계로 접근성을 높입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시 가능성&lt;/b&gt;: HTTP 캐시 메커니즘을 통해 성능을 향상시킬 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 개선 과정 및 순서도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템을 REST API 및 JSON API로 전환하기 위한 단계별 과정은 다음과 같습니다:&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1 현재 API 분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 API의 URI 구조 분석&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 사용 중인 API의 URI 구조를 분석합니다. 예를 들어, domain.com/member/get_member_list/1234와 같은 URI는 자원(Resource) 기반이 아닌 동작(Action) 기반의 구조를 가지고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용되는 데이터 포맷 파악&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 JSON, XML, Plain Text 등 다양한 포맷으로 전송되고 있는지 파악합니다. 일관성 없는 데이터 포맷은 처리의 복잡성을 증가시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP 메서드 사용 현황 점검&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET과 POST 메서드 외에 PUT, DELETE 등 다른 HTTP 메서드가 적절하게 사용되고 있는지 점검합니다. 일반적으로 GET은 데이터를 조회할 때, POST는 데이터를 생성할 때, PUT은 데이터를 수정할 때, DELETE는 데이터를 삭제할 때 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.2 RESTful 설계 원칙 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자원(Resource) 정의&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템에서 관리하는 자원을 식별하고 정의합니다. 예를 들어, 회원(member), 게시물(post) 등.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP 메서드 매핑&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 자원에 대해 적절한 HTTP 메서드를 매핑합니다. 예를 들어:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GET /members&lt;/b&gt;: 회원 목록 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;POST /members&lt;/b&gt;: 새로운 회원 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET /members/1234&lt;/b&gt;: 특정 회원 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PUT /members/1234&lt;/b&gt;: 특정 회원 정보 수정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DELETE /members/1234&lt;/b&gt;: 특정 회원 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;URI 재설계&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자원 기반의 명확한 URI 구조로 재설계합니다. 예를 들어, domain.com/member/get_member_list/1234를 domain.com/members/1234로 변경합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3 JSON 포맷으로의 전환&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 포맷 일관성 확보&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 데이터 전송을 JSON 포맷으로 통일합니다. 이는 데이터의 일관성을 유지하고, 클라이언트와 서버 간의 상호 운용성을 높이는 데 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요한 경우 JSON 스키마 정의&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 구조를 명확히 정의하기 위해 JSON 스키마를 도입할 수 있습니다. 이는 데이터 검증과 문서화에 유용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.4 구현 및 테스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;REST API 엔드포인트 구현&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정의된 자원과 HTTP 메서드 매핑을 기반으로 REST API 엔드포인트를 구현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 시스템과의 호환성 테스트&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 시스템과 새로운 REST API 간의 호환성을 테스트합니다. 이를 통해 전환 과정에서 발생할 수 있는 문제를 최소화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;API 문서화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현된 API에 대한 명확한 문서를 작성합니다. 이는 개발자와 사용자 모두에게 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.5 배포 및 모니터링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;API 배포&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현된 REST API를 프로덕션 환경에 배포합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모니터링 시스템 구축&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API의 성능과 오류를 모니터링하기 위한 시스템을 구축합니다. 이는 운영 중 발생할 수 있는 문제를 신속하게 감지하고 대응하는 데 필수적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 및 오류 모니터링&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API의 성능과 오류를 지속적으로 모니터링하고, 필요시 개선 작업을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pnDqv/btsIHgsiC5n/UgwoJdQH9sCsPkLWlDpIDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pnDqv/btsIHgsiC5n/UgwoJdQH9sCsPkLWlDpIDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pnDqv/btsIHgsiC5n/UgwoJdQH9sCsPkLWlDpIDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpnDqv%2FbtsIHgsiC5n%2FUgwoJdQH9sCsPkLWlDpIDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1486&quot; height=&quot;714&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템에서 REST API 및 JSON API로 전환하는 것은 단순한 기술적 변경을 넘어, 시스템의 확장성, 유지보수성, 상호 운용성을 크게 향상시킬 수 있는 중요한 작업입니다. 무상태성과 표준화된 HTTP 메서드, 일관된 데이터 포맷의 사용은 더욱 효율적이고 안정적인 시스템을 구축하는 데 기여합니다. 따라서, 기존 시스템의 문제점을 정확히 파악하고 체계적으로 전환 과정을 수행하는 것이 중요합니다. 이러한 현대화를 통해 개발자와 사용자는 더욱 높은 품질의 서비스를 제공받을 수 있을 것입니다.&lt;/p&gt;</description>
      <category>Server Language/PHP</category>
      <category>api 분석</category>
      <category>application api to rest api</category>
      <category>lagacy add restapi</category>
      <category>legacy api 분석</category>
      <category>legacy system modernization</category>
      <category>morden api</category>
      <category>rest api</category>
      <category>restapi 구현</category>
      <category>레거시 restapi 구현</category>
      <category>레거시api에서 rest api로</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/312</guid>
      <comments>https://min-nine.tistory.com/entry/Legacy-to-Modernization-2-%ED%98%95%EC%8B%9D%EC%97%86%EB%8A%94-Appliction-API%EB%A5%BC-%ED%91%9C%EC%A4%80%ED%99%94%EB%90%9C-RestAPI%EB%A1%9C-%EA%B5%90%EC%B2%B4%ED%95%98%EA%B8%B0#entry312comment</comments>
      <pubDate>Sun, 21 Jul 2024 14:30:08 +0900</pubDate>
    </item>
    <item>
      <title>[Legacy to Modernization] 1. PHP Legacy Project에 ORM 도입하기</title>
      <link>https://min-nine.tistory.com/entry/PHP-Legacy-Project-%EC%82%B4%EB%A6%AC%EA%B8%B0-1-Legacy-PHP-Project%EC%97%90-ORM-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 id=&quot;서론&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;레거시 프로젝트는 종종 오래된 코드베이스와 기술 부채를 포함하고 있어 유지보수와 확장에 어려움을 겪는 경우가 많습니다. 특히, 데이터베이스와의 상호작용을 위해 PDO를 이용한 SQL 쿼리문 작성 방식은 다양한 단점을 가지고 있습니다. 이러한 문제점을 해결하고, 개발 생산성과 코드 품질을 향상시키기 위해 ORM(Object-Relational Mapping) 도입이 필수적입니다. 본 보고서에서는 PDO 기반 SQL 쿼리문 작성의 단점을 분석하고, ORM 도입의 필요성과 그 이점을 체계적으로 설명하겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;PDO-기반-SQL-쿼리문-작성의-단점&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;291&quot; data-ke-size=&quot;size26&quot;&gt;PDO 기반 SQL 쿼리문 작성의 단점&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;1.-코드의-가독성-및-유지보수성-저하&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;314&quot; data-ke-size=&quot;size23&quot;&gt;1. 코드의 가독성 및 유지보수성 저하&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;337&quot; data-ke-size=&quot;size16&quot;&gt;PDO를 이용한 SQL 쿼리문 작성은 SQL 문법이 직접 코드에 포함되기 때문에, 코드의 가독성이 떨어집니다. 복잡한 쿼리일수록 코드가 길어지고, 이를 이해하고 수정하는 데 많은 시간이 소요됩니다. 또한, SQL 쿼리가 분산되어 있으면, 데이터베이스 구조 변경 시 이를 일일이 찾아 수정해야 하는 비효율적인 상황이 발생합니다.&lt;/p&gt;
&lt;h3 id=&quot;2.-보안-취약성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;521&quot; data-ke-size=&quot;size23&quot;&gt;2. 보안 취약성&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;532&quot; data-ke-size=&quot;size16&quot;&gt;직접 SQL 쿼리를 작성하는 방식은 SQL 인젝션 공격에 취약할 수 있습니다. 물론, PDO의 준비된 문(Prepared Statements)을 통해 이를 어느 정도 방지할 수 있지만, 개발자가 항상 올바르게 사용하지 않을 위험이 존재합니다. 실수로 인해 발생할 수 있는 보안 취약점을 완전히 방지하기는 어렵습니다.&lt;/p&gt;
&lt;h3 id=&quot;3.-재사용성-부족&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;710&quot; data-ke-size=&quot;size23&quot;&gt;3. 재사용성 부족&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;722&quot; data-ke-size=&quot;size16&quot;&gt;SQL 쿼리는 특정한 데이터베이스와 밀접하게 결합되어 있어, 재사용성이 떨어집니다. 동일한 데이터 접근 로직을 다른 부분에서 다시 사용하려면, 동일한 쿼리를 중복 작성해야 하는 경우가 많습니다. 이는 코드 중복을 초래하고, 유지보수 비용을 증가시킵니다.&lt;/p&gt;
&lt;h3 id=&quot;4.-테스트-어려움&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;865&quot; data-ke-size=&quot;size23&quot;&gt;4. 테스트 어려움&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;877&quot; data-ke-size=&quot;size16&quot;&gt;직접 SQL 쿼리를 작성하는 방식은 테스트 코드 작성에 어려움을 줍니다. 데이터베이스 상태에 의존하는 쿼리는 단위 테스트를 작성하기 어렵고, 통합 테스트를 작성하더라도 테스트 환경 설정에 많은 노력이 필요합니다.&lt;/p&gt;
&lt;h3 id=&quot;5.-데이터베이스-독립성-부족&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;997&quot; data-ke-size=&quot;size23&quot;&gt;5. 데이터베이스 독립성 부족&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1015&quot; data-ke-size=&quot;size16&quot;&gt;PDO는 여러 데이터베이스 드라이버를 지원하지만, 직접 작성된 SQL 쿼리는 특정 데이터베이스에 종속적인 경우가 많습니다. 예를 들어, MySQL에서 사용된 특정 SQL 구문이나 함수는 PostgreSQL, SQLite 등 다른 데이터베이스에서는 호환되지 않을 수 있습니다. 이러한 종속성은 데이터베이스를 변경하거나 다중 데이터베이스 지원을 추가할 때 큰 장애물이 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;6.-코드-중복-증가&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1225&quot; data-ke-size=&quot;size23&quot;&gt;6. 코드 중복 증가&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1238&quot; data-ke-size=&quot;size16&quot;&gt;직접 SQL 쿼리를 작성하는 방식은 동일한 로직을 여러 곳에서 반복하게 만들 수 있습니다. 예를 들어, 특정 테이블에서 데이터를 가져오는 쿼리를 여러 파일에서 중복 작성하게 되면, 유지보수 시 이 모든 곳을 수정해야 합니다. 이는 코드 중복을 초래하고, 유지보수성을 떨어뜨립니다.&lt;/p&gt;
&lt;h3 id=&quot;7.-에러-처리-복잡성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1396&quot; data-ke-size=&quot;size23&quot;&gt;7. 에러 처리 복잡성&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1410&quot; data-ke-size=&quot;size16&quot;&gt;PDO는 에러 처리를 위한 메커니즘을 제공하지만, 각 쿼리에 대한 에러 처리가 반복적으로 작성되어야 합니다. 이는 코드의 복잡성을 증가시키고, 일관된 에러 처리 방식을 유지하기 어렵게 만듭니다. ORM을 사용하면 공통된 에러 처리 로직을 중앙에서 관리할 수 있어 코드가 간결해지고, 유지보수가 용이해집니다.&lt;/p&gt;
&lt;h3 id=&quot;8.-SQL-인젝션-방지의-번거로움&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1583&quot; data-ke-size=&quot;size23&quot;&gt;8. SQL 인젝션 방지의 번거로움&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1604&quot; data-ke-size=&quot;size16&quot;&gt;PDO는 준비된 문장을 통해 SQL 인젝션을 방지할 수 있지만, 이를 모든 쿼리에 적용하는 것은 개발자의 책임입니다. 모든 SQL 쿼리에 대해 준비된 문을 올바르게 사용하는지 확인하고 관리하는 것은 번거롭고, 실수로 인해 보안 취약점이 발생할 수 있습니다. ORM은 이러한 보안 취약점을 체계적으로 방지해 줍니다.&lt;/p&gt;
&lt;h3 id=&quot;9.-성능-최적화의-어려움&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1781&quot; data-ke-size=&quot;size23&quot;&gt;9. 성능 최적화의 어려움&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1797&quot; data-ke-size=&quot;size16&quot;&gt;PDO 기반의 SQL 쿼리는 성능 최적화를 위해 쿼리를 수동으로 튜닝해야 하는 경우가 많습니다. 쿼리 최적화, 인덱스 사용, 조인 최적화 등을 개발자가 직접 관리해야 합니다. ORM은 성능 최적화 기능을 제공하여, 개발자가 쿼리 최적화에 소요되는 시간을 줄이고, 더 중요한 비즈니스 로직에 집중할 수 있게 합니다.&lt;/p&gt;
&lt;h3 id=&quot;10.-관계형-데이터-조작의-번거로움&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1974&quot; data-ke-size=&quot;size23&quot;&gt;10. 관계형 데이터 조작의 번거로움&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1996&quot; data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스에서는 여러 테이블 간의 관계를 다루는 일이 빈번합니다. 직접 SQL을 작성하는 경우, 이러한 관계를 관리하는 쿼리는 복잡해질 수 있으며, 관계를 명확하게 정의하고 유지하는 것이 어렵습니다. ORM은 객체 간의 관계를 명확하게 정의하고 관리할 수 있는 기능을 제공하여, 복잡한 쿼리 작성과 관계 관리의 번거로움을 줄여줍니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2189&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;ORM-도입의-필요성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2191&quot; data-ke-size=&quot;size26&quot;&gt;ORM 도입의 필요성&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;1.-코드의-가독성-및-유지보수성-향상&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2204&quot; data-ke-size=&quot;size23&quot;&gt;1. 코드의 가독성 및 유지보수성 향상&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2227&quot; data-ke-size=&quot;size16&quot;&gt;ORM을 도입하면 데이터베이스와의 상호작용을 객체 지향적으로 처리할 수 있습니다. 이는 코드의 가독성을 크게 향상시키고, 데이터베이스 구조 변경 시 ORM 설정만 변경하면 되어 유지보수성이 높아집니다. 복잡한 SQL 쿼리를 객체 지향적으로 표현할 수 있어, 코드를 이해하고 수정하는 데 드는 시간이 줄어듭니다.&lt;/p&gt;
&lt;h3 id=&quot;2.-보안-강화&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2402&quot; data-ke-size=&quot;size23&quot;&gt;2. 보안 강화&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2412&quot; data-ke-size=&quot;size16&quot;&gt;ORM은 내부적으로 준비된 문을 사용하여 SQL 인젝션 공격을 방지합니다. 개발자가 직접 SQL 쿼리를 작성하지 않으므로, 보안 취약점이 발생할 확률이 줄어듭니다. 또한, ORM은 데이터 검증과 같은 추가 보안 기능도 제공하여, 보다 안전한 애플리케이션을 개발할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;3.-재사용성-증대&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2568&quot; data-ke-size=&quot;size23&quot;&gt;3. 재사용성 증대&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2580&quot; data-ke-size=&quot;size16&quot;&gt;ORM을 사용하면 데이터 접근 로직을 재사용 가능한 객체로 캡슐화할 수 있습니다. 이는 코드 중복을 줄이고, 동일한 데이터 접근 로직을 여러 곳에서 쉽게 재사용할 수 있도록 합니다. 또한, ORM은 쿼리 빌더 기능을 제공하여 동적으로 쿼리를 생성할 수 있어, 다양한 요구사항에 유연하게 대응할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;4.-테스트-용이성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2753&quot; data-ke-size=&quot;size23&quot;&gt;4. 테스트 용이성&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2765&quot; data-ke-size=&quot;size16&quot;&gt;ORM은 데이터베이스 상호작용을 객체 단위로 처리하므로, 단위 테스트를 작성하기가 용이합니다. 가짜 객체(Mock Objects)를 사용하여 데이터베이스와의 상호작용을 테스트할 수 있으며, 통합 테스트 작성 시에도 ORM이 제공하는 추상화를 통해 테스트 환경 설정이 간편해집니다.&lt;/p&gt;
&lt;h3 id=&quot;5.-신규-개발자의-이탈-방지&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2923&quot; data-ke-size=&quot;size23&quot;&gt;5. 신규 개발자의 이탈 방지&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2941&quot; data-ke-size=&quot;size16&quot;&gt;ORM 도입은 신규 개발자의 이탈을 방지하는 데에도 중요한 역할을 합니다. 레거시 프로젝트에서 PDO를 이용한 직접 SQL 쿼리 작성 방식은 신규 개발자가 프로젝트에 적응하는 데 큰 장벽이 될 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;학습-곡선-완화&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3058&quot; data-ke-size=&quot;size23&quot;&gt;학습 곡선 완화&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3068&quot; data-ke-size=&quot;size16&quot;&gt;PDO 기반 SQL 쿼리 작성 방식은 데이터베이스와 SQL에 대한 깊은 이해가 필요하며, 신규 개발자가 이러한 방식을 익히고 효율적으로 활용하기까지 상당한 시간이 걸립니다. 반면, ORM은 객체 지향 프로그래밍 패러다임에 익숙한 개발자라면 비교적 쉽게 적응할 수 있습니다. 이를 통해 신규 개발자가 프로젝트에 빠르게 적응하고 생산성을 발휘할 수 있게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;코드의-일관성과-가독성-향상&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3270&quot; data-ke-size=&quot;size23&quot;&gt;코드의 일관성과 가독성 향상&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3287&quot; data-ke-size=&quot;size16&quot;&gt;ORM을 사용하면 데이터베이스와의 상호작용을 객체 지향적으로 처리할 수 있어 코드의 일관성과 가독성이 크게 향상됩니다. 신규 개발자는 명확하고 일관된 코드를 통해 프로젝트 구조를 쉽게 이해하고, 수정 및 확장이 용이한 코드를 작성할 수 있습니다. 이는 개발 과정에서의 혼란을 줄이고, 작업 효율성을 높여줍니다.&lt;/p&gt;
&lt;h3 id=&quot;개발-경험의-현대화&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3462&quot; data-ke-size=&quot;size23&quot;&gt;개발 경험의 현대화&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3474&quot; data-ke-size=&quot;size16&quot;&gt;ORM을 도입하면 최신 개발 패턴과 도구를 사용할 수 있어, 프로젝트가 더 현대적이고 매력적으로 보일 수 있습니다. 신규 개발자는 최신 기술과 도구를 사용하고 싶어하며, 이를 통해 자신의 기술 스택을 확장하고자 합니다. ORM은 이러한 요구를 충족시켜 주며, 프로젝트에 대한 만족도를 높여줍니다.&lt;/p&gt;
&lt;h3 id=&quot;협업과-코드-리뷰의-용이성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3641&quot; data-ke-size=&quot;size23&quot;&gt;협업과 코드 리뷰의 용이성&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3657&quot; data-ke-size=&quot;size16&quot;&gt;ORM은 표준화된 방법으로 데이터베이스와 상호작용하기 때문에 협업과 코드 리뷰가 용이해집니다. 신규 개발자는 팀 내에서 효율적으로 협업할 수 있고, 코드 리뷰 과정에서도 일관된 코딩 스타일과 명확한 쿼리 구조를 유지할 수 있습니다. 이는 팀 전체의 생산성을 높이고, 신규 개발자가 팀에 원활하게 통합될 수 있도록 돕습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3839&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;PHP-레거시-프로젝트에-Eloquent-ORM-도입-방안&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3841&quot; data-ke-size=&quot;size26&quot;&gt;PHP 레거시 프로젝트에 Eloquent ORM 도입 방안&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3875&quot; data-ke-size=&quot;size16&quot;&gt;PHP 레거시 프로젝트에 Eloquent ORM을 도입하는 것은 프로젝트의 유지보수성과 생산성을 향상시키는 중요한 단계입니다. 다음은 Eloquent ORM을 도입하는 구체적인 단계입니다.&lt;/p&gt;
&lt;h3 id=&quot;1.-Composer-설치-및-설정&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3982&quot; data-ke-size=&quot;size23&quot;&gt;1. Composer 설치 및 설정&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4003&quot; data-ke-size=&quot;size16&quot;&gt;Composer는 PHP의 의존성 관리 도구로, Eloquent ORM을 설치하는 데 사용됩니다. 프로젝트 루트 디렉토리에 Composer를 설치하고 설정합니다.&lt;/p&gt;
&lt;h4 id=&quot;Composer-설치&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4095&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Composer 설치&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4108&quot; data-ke-size=&quot;size16&quot;&gt;먼저, Composer를 설치합니다. 설치 방법은 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://getcomposer.org/&quot; data-renderer-mark=&quot;true&quot; data-testid=&quot;link-with-safety&quot;&gt;Composer 공식 사이트&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;2.-Eloquent-ORM-및-관련-패키지-설치&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4167&quot; data-ke-size=&quot;size23&quot;&gt;2. Eloquent ORM 및 관련 패키지 설치&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4196&quot; data-ke-size=&quot;size16&quot;&gt;composer.json 파일에 Eloquent ORM을 포함한 필요한 패키지를 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721174001806&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;require&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;illuminate/database&quot;: &quot;^8.0&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;illuminate/events&quot;: &quot;^8.0&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4358&quot; data-ke-size=&quot;size16&quot;&gt;이제 다음 명령어를 실행하여 패키지를 설치합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-layout=&quot;custom&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1721174023701&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;composer install&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;3.-Eloquent-ORM-초기화-파일-생성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4411&quot; data-ke-size=&quot;size23&quot;&gt;3. Eloquent ORM 초기화 파일 생성&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4438&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트의 루트 디렉토리에 bootstrap.php 파일을 생성하여 Eloquent ORM을 초기화합니다. 이 파일은 데이터베이스 설정과 Eloquent 초기화를 담당합니다. 레거시 프로젝트에서 기존 사용하였던 dbinfo.php 정보가 있다는 가정하에 그 정보를 가지고 자동으로 load하도록 만들어 두었습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4438&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4438&quot; data-ke-size=&quot;size16&quot;&gt;dbinfo.php&lt;/p&gt;
&lt;pre id=&quot;code_1721174199845&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?
$dbinfo = array();

$dbinfo[&quot;default&quot;][&quot;host&quot;] = &quot;defaultHost&quot;;
$dbinfo[&quot;default&quot;][&quot;db&quot;] = &quot;db&quot;;
$dbinfo[&quot;default&quot;][&quot;user&quot;] = &quot;mysql&quot;;
$dbinfo[&quot;default&quot;][&quot;pwd&quot;] = &quot;111111&quot;;
$dbinfo[&quot;default&quot;][&quot;port&quot;] = &quot;3306&quot;;

$dbinfo[&quot;log&quot;][&quot;host&quot;] = &quot;logHost&quot;;
$dbinfo[&quot;log&quot;][&quot;db&quot;] = &quot;logdb&quot;;
$dbinfo[&quot;log&quot;][&quot;user&quot;] = &quot;mysql&quot;;
$dbinfo[&quot;log&quot;][&quot;pwd&quot;] = &quot;2222&quot;;
$dbinfo[&quot;log&quot;][&quot;port&quot;] = &quot;3306&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4438&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4438&quot; data-ke-size=&quot;size16&quot;&gt;bootstrap.php&lt;/p&gt;
&lt;pre id=&quot;code_1721174115151&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
require 'vendor/autoload.php';
&amp;nbsp;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Events\Dispatcher;
use Illuminate\Container\Container;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\DB;
&amp;nbsp;
// 기존 dbinfo.php 정보 활용
require_once 'class/config/dbinfo.php';
$ormDbInfo = [];
foreach ($dbinfo as $key =&amp;gt; $value) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$ormDbInfo[$key] = [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'driver'&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'mysql',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'host'&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; $value['host'] ?? 'localhost',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'port'&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; $value['port'] ?? '3306',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'database'&amp;nbsp; =&amp;gt; $value['db'] ?? 'mydb',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'username'&amp;nbsp; =&amp;gt; $value['user'] ?? 'user',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'password'&amp;nbsp; =&amp;gt; $value['pwd'] ?? '',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'charset'&amp;nbsp;&amp;nbsp; =&amp;gt; 'utf8',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'collation' =&amp;gt; 'utf8_unicode_ci',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'prefix'&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; '',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;];
}
&amp;nbsp;
//var_dump($ormDbInfo);exit();
// 애플리케이션 컨테이너 생성 및 설정
$app = new Container();
$app-&amp;gt;instance('app', $app);
$app-&amp;gt;singleton('config', function () use ($ormDbInfo) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'database.default' =&amp;gt; 'default',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'database.connections' =&amp;gt; $ormDbInfo,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;];
});
&amp;nbsp;
// 이벤트 디스패처 설정
$events = new Dispatcher($app);
$app-&amp;gt;instance('events', $events);
&amp;nbsp;
// Facade 초기화
Facade::setFacadeApplication($app);
&amp;nbsp;
// Eloquent ORM 설정
$capsule = new Capsule;
foreach ($ormDbInfo as $key =&amp;gt; $value) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;$capsule-&amp;gt;addConnection($app['config']['database.connections'][$key], $key);
}
$capsule-&amp;gt;setEventDispatcher($events);
$capsule-&amp;gt;setAsGlobal();
$capsule-&amp;gt;bootEloquent();
&amp;nbsp;
// Facades 설정
$app-&amp;gt;instance('db', $capsule-&amp;gt;getDatabaseManager());
DB::setFacadeApplication($app);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4.-모델-클래스-생성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6141&quot; data-ke-size=&quot;size23&quot;&gt;4. 모델 클래스 생성&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;Eloquent ORM을 사용하여 데이터베이스와 상호작용할 모델 클래스를 생성합니다. 모델 클래스는 데이터베이스 테이블을 나타내며, 각 클래스는 한 테이블과 매핑됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;본 문서는 ORM 도입을 위한 제안 및 초기 사용을 목적으로 제작되었습니다. 때문에 &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-text-custom-color=&quot;#bf2600&quot; data-renderer-mark=&quot;true&quot;&gt;예시에서 사용할 내용은 실제로 Legacy Project에서 사용하는 기능을 기반으로 작성&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;하였음을 안내드립니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;모델 클래스 : &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;legacy-project/app/Models/Member.php&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721174429993&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

namespace  app\Models;

use Illuminate\Database\Eloquent\Model;

class Member extends Model
{
    protected $table = 'member';
    protected $primaryKey = 'index';
    public $timestamps = false;
    const CREATED_AT = 'create_date';
    const UPDATED_AT = 'update_date';

    protected $fillable = []; // 대량 할당시 필요한 컬럼.
    protected $guarded = []; // 대량 할당시 제외할 컬럼.

    public function lectureUser()
    {
        return $this-&amp;gt;hasMany(LectureUser::class,'member_index','index');
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;legacy-project/app/Models/Lecture.php&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721174516697&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

namespace app\Models;

use Illuminate\Database\Eloquent\Model;

class Lecture extends Model
{
    protected $table = 'lecture';
    protected $primaryKey = 'index';
    public $timestamps = false;
    const CREATED_AT = 'create_date';
    const UPDATED_AT = 'update_date';

    protected $fillable = []; // 대량 할당시 필요한 컬럼.
    protected $guarded = []; // 대량 할당시 제외할 컬럼.

    public function lectureUser()
    {
        return $this-&amp;gt;hasMany(LectureUser::class,'lecture_index','index');
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;legacy-project/app/Models/LectureUser.php&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721174730675&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

namespace ctm\rest\Models;

use Illuminate\Database\Eloquent\Model;

class LectureUser extends Model
{
    protected $table = 'lecture_user';
    protected $primaryKey = 'index';
    public $timestamps = false;
    const CREATED_AT = 'create_date';
    const UPDATED_AT = 'update_date';

    protected $guarded = [];

    public function member()
    {
        return $this-&amp;gt;belongsTo(Member::class,'member_index','index');
    }

    public function lecture()
    {
        return $this-&amp;gt;belongsTo(Lecture::class,'lecture_index','index');
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5.-프로젝트의-엔트리-포인트-수정&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8036&quot; data-ke-size=&quot;size23&quot;&gt;5. 프로젝트의 엔트리 포인트 수정&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8057&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트의 엔트리 포인트 파일 &lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;(기존 PDO Query문을 사용하였던 파일)&lt;span&gt; &lt;/span&gt;&lt;/span&gt;(예:lecture.php) 에서 bootstrap.php 파일을 불러와 Eloquent ORM을 초기화합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1721174938362&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

require 'bootstrap.php';

use app\Repositories\LectureUserRepository;

class Lecture {

    protected $lectureUserRepository;

    function __construct(?LectureUserRepository $lectureUserRepository) {
        
        $this-&amp;gt;lectureUserRepository = $lectureUserRepository ?? new LectureUserRepository();

        // ... 이하 생략
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8924&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8924&quot; data-ke-size=&quot;size16&quot;&gt;Repository 패턴은 데이터 접근 로직과 비즈니스 로직을 분리하는 디자인 패턴입니다. 이 패턴은 데이터베이스 쿼리나 데이터 액세스 관련 로직을 하나의 클래스에 모아서 처리함으로써 코드의 재사용성과 유지보수성을 높이는 데 목적이 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;9061&quot; data-ke-size=&quot;size16&quot;&gt;단순히 모델만을 사용하여 비즈니스 로직을 구현하던 시대는 지났습니다. Repository 패턴을 통해 코드의 재사용성을 높이고, 데이터 접근 로직을 깔끔하게 관리할 수 있습니다. 또한, 이 패턴은 비즈니스 로직을 독립적으로 테스트할 수 있게 해주며, 데이터 저장소의 변경에 따른 코드 변경을 최소화할 수 있습니다.&lt;/p&gt;
&lt;h4 id=&quot;Repository-패턴의-장점&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;9238&quot; data-ke-size=&quot;size20&quot;&gt;Repository 패턴의 장점&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;코드 재사용성&lt;/b&gt;: 데이터 접근 로직을 한 곳에 모아 재사용할 수 있으므로, 중복 코드를 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수성 향상&lt;/b&gt;: 데이터 접근 로직이 비즈니스 로직과 분리되어 있어 유지보수가 용이합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용이성&lt;/b&gt;: Repository 패턴을 사용하면 데이터 접근 로직을 목(Mock)으로 대체하여 비즈니스 로직을 쉽게 테스트할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성&lt;/b&gt;: 데이터 저장소가 변경되더라도 Repository 클래스만 수정하면 되므로, 비즈니스 로직에 대한 영향을 최소화할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;legacy-project/app/Repositories/LectureUserRepository.php&lt;/p&gt;
&lt;pre id=&quot;code_1721175012767&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

namespace app\Repositories;

use app\Models\LectureUser;
use Illuminate\Support\Facades\DB;

class LectureUserRepository extends BaseRepository
{
    protected $cacheTtl = 60;
    protected $cacheChangeTtl = 10;

    public function __construct(LectureUser $model = null)
    {
        if ($model == null) {
            $model = new LectureUser();
        }
        parent::__construct($model);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lagacy-project/app/Repositories/BaseRepository.php&lt;/p&gt;
&lt;pre id=&quot;code_1721175246520&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

namespace app\Repositories;

use app\Repositories\RepositoryInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class BaseRepository implements RepositoryInterface
{
    protected $model;

    protected $connection = null;

    protected $cache_prefix = 'model';

    protected $cacheTtl = 60;

    protected $cacheChangeTtl = 10;

    public $pageSize = 50;

    public function __construct(Model $model)
    {
        $this-&amp;gt;model = $model;
    }

    public function setConnection($connection)
    {
        $this-&amp;gt;connection = $connection;
        return $this;
    }

    protected function getModel()
    {
        if (empty($this-&amp;gt;connection)) {
            return $this-&amp;gt;model;
        }

        return $this-&amp;gt;model-&amp;gt;setConnection($this-&amp;gt;connection);
    }

    protected function query()
    {
        $this-&amp;gt;newQuery();
    }

    protected function getQuery()
    {
        if (empty($this-&amp;gt;connection)) {
            return $this-&amp;gt;model-&amp;gt;query();
        }

        return $this-&amp;gt;model-&amp;gt;on($this-&amp;gt;connection)-&amp;gt;getQuery();
    }

    protected function newQuery()
    {
        if (empty($this-&amp;gt;connection)) {
            return $this-&amp;gt;model-&amp;gt;newQuery();
        }

        return $this-&amp;gt;model-&amp;gt;setConnection($this-&amp;gt;connection)-&amp;gt;newQuery();
    }

    public function findById($id, array $columns = ['*'], array $relations = [])
    {
        return $this-&amp;gt;getModel()-&amp;gt;with($relations)-&amp;gt;find($id, $columns);
    }

    public function all(array $columns = ['*'], array $relations = [])
    {
        return $this-&amp;gt;getModel()-&amp;gt;with($relations)-&amp;gt;get($columns);
    }

    public function paginate($perPages, array $columns = ['*'], array $relations = [])
    {
        return $this-&amp;gt;getModel()-&amp;gt;select($columns)-&amp;gt;with($relations)-&amp;gt;orderByDesc($this-&amp;gt;model-&amp;gt;getKeyName())-&amp;gt;paginate($perPages);
    }

    public function create(array $attributes)
    {
        return $this-&amp;gt;newQuery()-&amp;gt;create($attributes);
    }

    public function firstOrNew(array $attributes = [], array $values = [])
    {
        return $this-&amp;gt;newQuery()-&amp;gt;firstOrNew($attributes, $values);
    }

    public function firstOrCreate(array $attributes = [], array $values = [])
    {
        return $this-&amp;gt;newQuery()-&amp;gt;firstOrCreate($attributes, $values);
    }

    public function updateOrCreate(array $attributes, array $values = [])
    {
        return $this-&amp;gt;newQuery()-&amp;gt;updateOrCreate($attributes, $values);
    }

    public function insert($arrItems)
    {
        return $this-&amp;gt;newQuery()-&amp;gt;insert($arrItems);
    }

    public function insertGetId($arrItems)
    {
        return $this-&amp;gt;newQuery()-&amp;gt;insertGetId($arrItems);
    }

    public function update($id, array $attributes)
    {
        // id는 수정할 수 없음 - $fillable에 id가 들어갈경우 처리 해줘야됨
        if (isset($attributes[$this-&amp;gt;model-&amp;gt;getKeyName()])) {
            unset($attributes[$this-&amp;gt;model-&amp;gt;getKeyName()]);
        }

        return $this-&amp;gt;newQuery()-&amp;gt;where($this-&amp;gt;model-&amp;gt;getKeyName(), $id)-&amp;gt;update($attributes);
    }

    public function log($id, $logType, array $attributes)
    {
        // id는 수정할 수 없음 - $fillable에 id가 들어갈경우 처리 해줘야됨
        if (isset($attributes[$this-&amp;gt;model-&amp;gt;getKeyName()])) {
            unset($attributes[$this-&amp;gt;model-&amp;gt;getKeyName()]);
        }

        return true;
    }

    public function delete($ids)
    {
        if (is_array($ids)) {
            return $this-&amp;gt;newQuery()-&amp;gt;whereIn($this-&amp;gt;model-&amp;gt;getKeyName(), $ids)-&amp;gt;delete();
        }

        return $this-&amp;gt;newQuery()-&amp;gt;where($this-&amp;gt;model-&amp;gt;getKeyName(), $ids)-&amp;gt;delete();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;lagacy-project/app/Repositories/RepositoryInterface.php&lt;/p&gt;
&lt;pre id=&quot;code_1721175280595&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php

namespace  app\Repositories;

interface RepositoryInterface
{
    public function findById($id , array $columns = ['*'], array $relations = []);

    public function all(array $columns = ['*'], array $relations = []);

    public function paginate($perPages, array $columns = ['*'], array $relations = []);

    public function create(array $attributes);

    public function firstOrNew(array $attributes = [], array $values = []);

    public function firstOrCreate(array $attributes = [], array $values = []);

    public function updateOrCreate(array $attributes, array $values = []);

    public function insert($arrItems);

    public function update($id, array $attributes);

    public function delete($id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;6.-기존-코드의-Eloquent-ORM으로의-변환&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;13198&quot; data-ke-size=&quot;size23&quot;&gt;6. 기존 코드의 Eloquent ORM으로의 변환&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;13228&quot; data-ke-size=&quot;size16&quot;&gt;위에서 구현한 Repository와 ORM Relationship을 활용하여 PDO 기반 SQL Legacy Query를 걷어내며 Eloquent ORM의 도입을 진행할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;13228&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18969&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18973&quot; data-ke-size=&quot;size16&quot;&gt;PDO를 이용한 SQL 쿼리문 작성 방식은 레거시 프로젝트에서 여전히 많이 사용되고 있지만, 여러 단점으로 인해 유지보수와 확장에 어려움을 겪고 있습니다. ORM을 도입함으로써 코드의 가독성과 유지보수성을 향상시키고, 보안을 강화하며, 재사용성을 증대시킬 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18973&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18973&quot; data-ke-size=&quot;size16&quot;&gt;또한, 테스트 용이성을 높여 보다 안정적인 애플리케이션을 개발할 수 있습니다. 따라서, 레거시 프로젝트의 기술 부채를 해소하고, 효율적인 개발 환경을 조성하기 위해 ORM 도입은 필수적입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18973&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;19233&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 프로젝트의 성공 가능성을 높이고, 향후 저희 개발연구소 팀원들의 생산성을 극대화할 수 있습니다. ORM 도입은 초기 학습 곡선이 존재할 수 있지만, 장기적으로는 큰 이점을 제공할 것입니다.&lt;/p&gt;</description>
      <category>Server Language/PHP</category>
      <category>legacy php add orm</category>
      <category>legacy php project orm</category>
      <category>legacy sql to orm</category>
      <category>legacy system modernization</category>
      <category>php legacy orm</category>
      <category>php project orm</category>
      <category>php project에 orm 도입</category>
      <category>php 레거시 orm 도입</category>
      <category>set orm in php project</category>
      <category>레거시 php orm</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/311</guid>
      <comments>https://min-nine.tistory.com/entry/PHP-Legacy-Project-%EC%82%B4%EB%A6%AC%EA%B8%B0-1-Legacy-PHP-Project%EC%97%90-ORM-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0#entry311comment</comments>
      <pubDate>Wed, 17 Jul 2024 19:40:59 +0900</pubDate>
    </item>
    <item>
      <title>SwiftUI - MVVM 패턴을 사용해 만들어보는 알림 페이지</title>
      <link>https://min-nine.tistory.com/entry/SwiftUI-MVVM-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EC%95%8C%EB%A6%BC-%ED%8E%98%EC%9D%B4%EC%A7%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nZdJS/btsFMzIy04B/kU7KXkJxYCgmL2PACu7Af1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nZdJS/btsFMzIy04B/kU7KXkJxYCgmL2PACu7Af1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nZdJS/btsFMzIy04B/kU7KXkJxYCgmL2PACu7Af1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnZdJS%2FbtsFMzIy04B%2FkU7KXkJxYCgmL2PACu7Af1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVVM이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM (Model-View-ViewModel) 패턴은 소프트웨어 개발에 있어서 사용자 인터페이스를 구현하기 위한 아키텍처 패턴 중 하나 다. 이 패턴은 주로 &lt;b&gt;UI 애플리케이션에서 사용되며,&lt;/b&gt; MVC (Model-View-Controller) 패턴을 발전시킨 형태로 볼 수 있다. MVVM은 아래 세 부분으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Model&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 데이터와 비즈니스 로직을 담당 . 데이터베이스, 유효성 검사, 객체 등이 여기에 해당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;View&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 보여지는 UI 부분으로 사용자의 입력을 받고, 표현하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ViewModel&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View를 표현하기 위한 데이터와 명령을 가지고 있으며, Model과 View 사이의 중재자 역할을 한다. View에 바인딩하여 데이터의 변화를 자동으로 반영하도록 설계된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤&amp;nbsp;상황에서&amp;nbsp;사용해야&amp;nbsp;하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM 패턴은 특히 UI의 복잡성이 높고, 사용자 인터페이스와 비즈니스 로직의 분리가 중요한 대규모 애플리케이션 또는 동적인 애플리케이션에서 유용하다. 데이터의 표현과 데이터 처리를 분리함으로써, 코드의 가독성과 유지보수성을 높일 수 있게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 관리는 어떻게 해야 하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체로 ViewModel 내에서 상태 관리를 수행한다. ViewModel은 View에 표시될 데이터와 UI 상태를 포함하며, 이 데이터는 View와 바인딩되어 UI가 데이터의 변화를 반영하도록 한다. 상태 관리를 위해, Observable 객체나 프레임워크에서 제공하는 상태 관리 도구 (예: SwiftUI의 @State, @ObservableObject 등)를 사용할 수 있다. ViewModel 내에서 데이터 변화를 감지하고, 필요한 로직 처리 후 View에 데이터를 업데이트하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB에 데이터 입력은 어디서 구현하는지?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 데이터 입력은 Model에서 구현한다. ViewModel은 View로부터 사용자의 입력을 받아 Model에 데이터를 저장하거나 수정하는 명령을 내릴 수 있다. 이 과정에서 ViewModel은 필요한 데이터 처리 (유효성 검사, 데이터 변환 등)를 수행하고, Model은 실제 데이터베이스에 데이터를 입력하거나 업데이트한다. Model은 DB와의 직접적인 통신을 담당하며, 데이터의 저장, 조회, 업데이트, 삭제 등의 작업을 실행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알림리스트 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI와 MVVM 패턴을 사용하여 SQLite 데이터베이스에 알림 데이터를 삽입하고, 조회 및 업데이트하는 기능을 구현해보자. 먼저, SQLite 데이터베이스 작업을 위한 간단한 래퍼 클래스를 구현한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NotificationModel.swift&lt;/h3&gt;
&lt;pre id=&quot;code_1710399630018&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Foundation

struct NotificationModel: Identifiable {
    var id: Int64
    var title: String
    var message: String
    var date: Date
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DatabaseManager.swift&lt;/h3&gt;
&lt;pre id=&quot;code_1710399749261&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Foundation
import SQLite

class DatabaseManager {
    static let shared = DatabaseManager()
    private let db: Connection?

    private let notifications = Table(&quot;notifications&quot;)
    private let id = Expression&amp;lt;Int64&amp;gt;(&quot;id&quot;)
    private let title = Expression&amp;lt;String&amp;gt;(&quot;title&quot;)
    private let message = Expression&amp;lt;String&amp;gt;(&quot;message&quot;)
    private let date = Expression&amp;lt;Date&amp;gt;(&quot;date&quot;)

    private init() {
        let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        do {
            db = try Connection(&quot;\(path)/appdb.sqlite3&quot;)
            createTable()
        } catch {
            db = nil
            print(&quot;Unable to open database&quot;)
        }
    }

    func createTable() {
        do {
            try db?.run(notifications.create(ifNotExists: true) { t in
                t.column(id, primaryKey: .autoincrement)
                t.column(title)
                t.column(message)
                t.column(date)
            })
        } catch {
            print(&quot;Unable to create table&quot;)
        }
    }
    
    // Fetch all notifications
    func fetchNotifications() -&amp;gt; [NotificationModel] {
        var notificationsList = [NotificationModel]()
        
        do {
            for notification in try db!.prepare(notifications) {
                let notificationModel = NotificationModel(id: notification[id], title: notification[title], message: notification[message], date: notification[date])
                notificationsList.append(notificationModel)
            }
        } catch {
            print(&quot;Select failed&quot;)
        }
        
        return notificationsList
    }
    
    // Insert new notification
	func insertNotification(title: String, message: String, date: Date) {
    let insert = notifications.insert(self.title &amp;lt;- title, self.message &amp;lt;- message, self.date &amp;lt;- date)
        do {
            let _ = try db?.run(insert)
        } catch {
            print(&quot;Insert failed: \(error)&quot;)
        }
    }

    // Delete a notification
	func deleteNotification(id: Int64) {
    let notification = notifications.filter(self.id == id)
    do {
        let _ = try db?.run(notification.delete())
    } catch {
        print(&quot;Delete failed: \(error)&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 SQLite 데이터베이스에서 알림 데이터를 삽입하고 조회하는 기본적인 작업을 수행한다. DatabaseManager 클래스는 싱글톤 패턴으로 구현되어 앱 전체에서 하나의 인스턴스만 사용된다 .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NotificationViewModel.swift&lt;/h3&gt;
&lt;pre id=&quot;code_1710399899123&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Foundation
import Combine

class NotificationViewModel: ObservableObject {
    @Published var notifications = [NotificationModel]()
    
    init() {
        loadNotifications()
    }
    
    func loadNotifications() {
        notifications = DatabaseManager.shared.fetchNotifications()
    }
    
    func addNotification(title: String, message: String, date: Date) {
        DatabaseManager.shared.insertNotification(title: title, message: message, date: date)
        loadNotifications() // Reload the notifications to reflect changes
    }

    func removeNotification(id: Int64) {
        DatabaseManager.shared.deleteNotification(id: id)
        loadNotifications() // Reload the notifications to reflect changes
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NotificationViewModel 클래스는 ObservableObject 프로토콜을 준수하며, 알림 목록을 관찰 가능한 속성으로 가진다. SwiftUI 뷰는 이 속성에 대한 변경 사항을 관찰하고, 데이터가 변경될 때 UI를 업데이트한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NotificationListView.swift&lt;/h3&gt;
&lt;pre id=&quot;code_1710399997729&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import SwiftUI

struct NotificationListView: View {
    @ObservedObject var viewModel = NotificationViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.notifications, id: \.id) { notification in
                    VStack(alignment: .leading) {
                        Text(notification.title).font(.headline)
                        Text(notification.message).font(.subheadline)
                        Text(&quot;\(notification.date)&quot;).font(.caption)
                    }
                }
                .onDelete(perform: delete)
            }
            .navigationBarItems(trailing: Button(action: addNotification) {
                Text(&quot;Add&quot;)
            })
            .navigationTitle(&quot;Notifications&quot;)
        }
    }
    
    func addNotification() {
        viewModel.addNotification(title: &quot;New Notification&quot;, message: &quot;This is a test&quot;, date: Date())
    }

    func delete(at offsets: IndexSet) {
        for index in offsets {
            let notification = viewModel.notifications[index]
            viewModel.removeNotification(id: notification.id)
        }
    }
}

struct NotificationListView_Previews: PreviewProvider {
    static var previews: some View {
        NotificationListView()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;SwiftUI의 List에 .onDelete(perform:) 메서드를 사용하여 스와이프하여 삭제 기능을 구현하고, navigationBarItems를 사용하여 알림 추가 기능을 구현했다. 실제 애플리케이션에서는 Delegate 등에서 Push 이벤트 발생시 알림 데이터를 동적으로 추가할 수 있도록 구성한다.&lt;/p&gt;</description>
      <category>Framework/SwiftUI</category>
      <category>mvvm 예제</category>
      <category>MVVM 패턴</category>
      <category>swift mvvm</category>
      <category>swiftUI</category>
      <category>swiftui mvvm</category>
      <category>swiftui mvvm 패턴</category>
      <category>swiftui mvvm 패턴 예제</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/309</guid>
      <comments>https://min-nine.tistory.com/entry/SwiftUI-MVVM-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EC%95%8C%EB%A6%BC-%ED%8E%98%EC%9D%B4%EC%A7%80#entry309comment</comments>
      <pubDate>Thu, 14 Mar 2024 18:50:48 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka 서버 구축] Kafka Clustering, Kafka Partitioning 구현</title>
      <link>https://min-nine.tistory.com/entry/Kafka-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-Kafka-Clustering-And-Partitioning</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBIdJu/btsETiUhAnW/x5QOcrRa0MdmXHwUt9cqC1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBIdJu/btsETiUhAnW/x5QOcrRa0MdmXHwUt9cqC1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBIdJu/btsETiUhAnW/x5QOcrRa0MdmXHwUt9cqC1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBIdJu%2FbtsETiUhAnW%2Fx5QOcrRa0MdmXHwUt9cqC1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기술을 접할 때, 생소한 단어 때문에 학습하기 전부터 두려움을 느끼는 것은 개발자라면 한번쯤 가졌을 것이다. 나또한 지금 카프카 학습을 하면서 매우 당황스러울때가 많다. 때문에 Kafka Clustering과 Kafka Partitioning을 구현하기 전에, 각각의 용어에 대해 이해할겸 설명하는 과정부터 시작해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 클러스터, 브로커, 토픽, 파티션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Kafka의 주요 구성 요소인 주피터 클러스터(Zookeeper Cluster), 클러스터(Kkafka Cluster), 브로커(Broker), 토픽(Topic), 파티션(Partition)에 대해 아래와 같이 간략하게 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주피터&amp;nbsp;클러스터&amp;nbsp;(Zookeeper&amp;nbsp;Cluster)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터의 메타데이터 관리, 브로커 간의 동기화, 클러스터 상태 유지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터의 정확한 작동을 위해 메타데이터(토픽, 파티션 정보 등)를 저장하고 관리한다. 브로커 장애 및 네트워크 분할 시 클러스터 상태를 유지하며, 브로커들 간의 리더 선출 등을 관리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클러스터&amp;nbsp;(Kafka&amp;nbsp;Cluster)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 메시지 데이터의 분산 처리 및 저장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터는 높은 처리량과 확장성을 제공하며, 대용량의 실시간 데이터 스트리밍 처리를 가능하게 한다. 클러스터 구성을 통해 단일 시스템의 부하와 한계를 극복하고, 데이터의 병렬 처리 및 분산 저장을 지원한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;브로커&amp;nbsp;(Broker)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터 내의 서버 노드로, 메시지 데이터의 수신, 저장, 전달 담당.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 브로커는 독립적으로 작동하여 클라이언트의 요청(데이터의 발행 및 구독)을 처리한다. 브로커들은 데이터의 복제, 부하 분산 및 고가용성을 위해 협력한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토픽&amp;nbsp;(Topic)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 데이터를 카테고리화하는 논리적 채널.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽은 데이터를 구조화하는 방법을 제공한다. 예를 들어, 서로 다른 유형의 메시지(예: 로그, 주문, 이벤트 등)를 서로 다른 토픽에 저장하여 처리한다. 토픽을 통해 데이터의 효율적인 관리와 접근이 가능해진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파티션&amp;nbsp;(Partition)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽 내에서 데이터를 나누어 저장하는 단위. 각 파티션은 순차적인 메시지 시퀀스를 유지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션을 통해 데이터는 병렬로 처리되고, 브로커 간에 분산 저장된다. 이는 데이터 처리의 확장성을 제공하며, 특정 토픽의 데이터 처리량이 증가해도 시스템 전체의 성능을 유지할 수 있게 한다. 또한, 파티션별로 데이터 복제를 통해 데이터의 내구성과 가용성을 향상시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 다중 브로커 생성 및 클러스터 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 도커 이미지를 사용하여 단일 브로커를 생성하여 클러스터에 연결하였었는데, 이번에는 주피터 클러스터 컨테이너를 생성하여 두 개의 브로커 컨테이너를 만들어 연결시켜보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클러스터 컨테이너 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 도커 명령어를 사용하여 Zookeeper Cluster 컨테이너를 생성 및 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707972149402&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name zookeeper -p 2181:2181 zookeeper&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 전 고려 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 대규모 트래픽이 발생하는 서비스의 경우, 여러 서버에 분산된 클러스터를 구축하는 것이 일반적&lt;/b&gt;이다. 이렇게 하면 단일 시스템의 부하를 분산시키고, 시스템의 장애나 다운타임이 전체 서비스에 미치는 영향을 줄일 수 있을 것이다. 또한, 데이터의 병렬 처리와 분산 저장을 보다 효과적으로 수행할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, &lt;b&gt;클러스터는 일반적으로 여러 브로커를 포함&lt;/b&gt;한다. 포스팅 처음에서 설명한 것과 같이 클러스터는 데이터의 병렬 처리와 분산 저장을 위해 여러 브로커를 포함하며 이를 통해 고가용성과 확장성을 달성하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 &lt;b&gt;서버가 하나만 있는 나와 같은 환경에서 학습을 위해 하나의 클러스터에 두 개의 브로커를 구성&lt;/b&gt;해보는 것이다. 이 경우, 두 브로커가 동일한 물리적 서버의 리소스를 공유하기 때문에 &lt;b&gt;고가용성이나 장애 복구 측면에서 이점이 많지 않음을 명확히 알고 있어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소규모 서비스를 운영하는 회사의 경우에도&lt;b&gt; 단일 서버에서 여러 브로커를 운영할 경우, 리소스 할당과 관리에 주의&lt;/b&gt;해야 한다. 각 브로커가 충분한 메모리와 CPU 자원을 할당받도록 구성해야 하며, 네트워크 대역폭 또한 적절히 관리할 필요가 있다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;첫 번째 카프카 브로커 생성 및 실행 및 클러스터 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 도커 명령어를 사용하여 위에서 생성된 클러스터와 새로 생성하는 kafka-1 이름의 브로커를 연결한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707972308927&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name kafka-1 -p 9092:9092 \
  --env KAFKA_BROKER_ID=1 \
  --env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
  --env KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT \
  --env KAFKA_LISTENERS=INTERNAL://kafka-1:9093,EXTERNAL://:9092 \
  --env KAFKA_ADVERTISED_LISTENERS=INTERNAL://kafka-1:9093,EXTERNAL://&amp;lt;카프카 서버 IP&amp;gt;:9092 \
  --env KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
  --env KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL \
  --link zookeeper:zookeeper wurstmeister/kafka&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 아래 도커 명령어를 사용하여 클러스터와 새로 생성하는 kafka-2 이름의 브로커를 연결한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707973764780&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name kafka-2 -p 9093:9092 \
  --env KAFKA_BROKER_ID=2 \
  --env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
  --env KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT \
  --env KAFKA_LISTENERS=INTERNAL://kafka-2:9094,EXTERNAL://:9093 \
  --env KAFKA_ADVERTISED_LISTENERS=INTERNAL://kafka-2:9094,EXTERNAL://&amp;lt;카프카 서버 IP&amp;gt;:9093 \
  --env KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
  --env KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL \
  --link zookeeper:zookeeper wurstmeister/kafka&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 docker run 명령어에서 --link 플래그를 사용하여 컨테이너 간의 연결을 설정한다. 이는 Docker Compose의 depends_on 설정과 유사한 역할을 한다. 또한, KAFKA_ADVERTISED_LISTENERS에 지정된 외부 IP는 실제 사용 환경에 맞게 조정해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 클러스터 관리 도구 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafdrop은 Apache Kafka 클러스터를 위한 웹 기반 UI 도구이다. 이 도구는 Kafka 클러스터의 상태를 시각적으로 모니터링하고 관리할 수 있도록 설계되었다. Kafdrop을 사용하면 Kafka의 토픽, 파티션, 메시지, 소비자 그룹 등을 쉽게 파악하고 검토할 수 있게 된다. 때문에 개발 및 테스트, 운영 모니터링 등에 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KafDrop 설치 및 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 명령어를 통해 kafdrop 이미지를 pull받아 실행시킬 수 있다. SERVER_SERVLET_CONTEXTPATH 환경변수를 설정하여 본인이 kafDrop 웹 인터페이스에 접근할 때 사용할 URI를 정의할 수 있다. 나는 단순하게 /kafdrop 이라고 설정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1707974550285&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name kafdrop -p 9000:9000 \
  --env KAFKA_BROKERCONNECT=kafka-1:9092,kafka-2:9093 \
  --env JVM_OPTS=&quot;-Xms32M -Xmx64M&quot; \
  --env SERVER_SERVLET_CONTEXTPATH=&quot;/kafdrop&quot; \
  --link kafka-1:kafka-1 \
  --link kafka-2:kafka-2 \
  obsidiandynamics/kafdrop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 클러스터링 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker process가 잘 실행되었는지 확인해본다. 나의 경우 정상적으로 주피터 클러스터, 브로커 1, 브로커 2, kafdrop 컨테이너들이 작동하는것을 확인할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1707974777129&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bX0Tt5/btsEViGJ96B/3iZOlxngbyKPXunZlCBgHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bX0Tt5/btsEViGJ96B/3iZOlxngbyKPXunZlCBgHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bX0Tt5/btsEViGJ96B/3iZOlxngbyKPXunZlCBgHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbX0Tt5%2FbtsEViGJ96B%2F3iZOlxngbyKPXunZlCBgHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1045&quot; height=&quot;305&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 서버에서 인바운드 규칙이 허용되어있다면 본인의 서버 ip 혹은 endpoint 주소 뒤에 :9000/kafdrop/ 로 접속하면 아래와 같이 정상적으로 연결 및 작동되고 있는 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우 AWS EC2 인스턴스의 인바운드 규칙에 9000(kafDrop), 9002(브로커1), 9003(브로커2) 3가지 포트를 허용해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/luhdB/btsES25FwzD/KKyJk3PMLZOe2xbcoNt0v1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/luhdB/btsES25FwzD/KKyJk3PMLZOe2xbcoNt0v1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/luhdB/btsES25FwzD/KKyJk3PMLZOe2xbcoNt0v1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FluhdB%2FbtsES25FwzD%2FKKyJk3PMLZOe2xbcoNt0v1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2400&quot; height=&quot;1356&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1356&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 파티셔닝 (Partitioning)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝(Partitioning)은 Kafka Topic 데이터를 물리적으로 분할하고 관리하는 방법을 지칭하는 개념이다. 파티션은 Kafka Topic의 데이터를 물리적으로 분할하는 단위로, 데이터의 병렬 처리 및 확장성을 제공하기 때문에 성능,확장성,내구성을 위한 핵심적인 전략이며, 토픽의 데이터 관리와 시스템 운영에 중요한 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카프카 토픽 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서 토픽과 파티션을 생성하는 방법은 몇 가지 있지만, 가장 일반적인 방법은 Kafka의 커맨드 라인 도구를 사용하는 것이다. 아래 명령어는 카프카 컨테이너 내에서 특정 토픽을 생성하는 명령이다. &lt;b&gt;이 명령은 토픽 이름과 더불어 파티션 및 복제본의 수를 설정&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1707980813933&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it kafka-1 kafka-topics.sh --create --topic mingyu --partitions 3 --replication-factor 2 --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;--create: 토픽을 생성.&lt;/b&gt;&lt;br /&gt;--topic: 생성할 토픽의 이름을 지정.&lt;br /&gt;&lt;b&gt;--partitions: 생성할 토픽의 파티션 수를 지정.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;--replication-factor: 각 파티션의 복제본 수를 지정.&lt;/b&gt;&lt;br /&gt;--bootstrap-server: Kafka 브로커의 주소를 지정.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특정 브로커에서 토픽을 생성해도 되는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어는 kafka-kafka-1-1 도커 컨테이너에 접속해서 토픽을 생성하는 것이다. 그렇다면,&lt;b&gt; kafka-1 브로커에만 토픽이 생성되는거 아닌가? 라는 의문점&lt;/b&gt;이 생긴다. 하지만 실상은 그렇지 않다는 것을 명령어를 실행해보면 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서 토픽을 생성할 때, 토픽은 클러스터 전체에 걸쳐 생성되고 관리된다. 토픽 생성 명령을 특정 Kafka 브로커 컨테이너에서 실행하더라도, 해당 토픽은 Kafka 클러스터의 모든 브로커에 걸쳐 사용할 수 있다는 것이다. 이는 Kafka의 분산 시스템 아키텍처의 일부이다.&lt;br /&gt;&lt;br /&gt;토픽 생성 시 --bootstrap-server 옵션으로 지정하는 Kafka 브로커 주소는 토픽을 생성하거나 관리하기 위해 연결할 브로커를 지정하는 것이다. 때문에 &lt;b&gt;위의 토픽 생성 명령&lt;/b&gt;은 &lt;b&gt;kafka-1 브로커에 연결하여 my-topic이라는 토픽을 생성&lt;/b&gt;하지만, &lt;b&gt;실제로&lt;/b&gt; 이 토픽은 &lt;b&gt;클러스터의 모든 브로커에 걸쳐 생성&lt;/b&gt;되게 된다. 이후 이 토픽은 클러스터의 모든 브로커에서 사용할 수 있으며, 다른 브로커에서도 토픽의 데이터를 읽고 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 파티셔닝 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 입력하여 실제로 토픽이 잘 생성되는지 확인해보자. 나의 경우 여러번 테스트 하면서 kafka-1 브로커를 가지고 있는 도커 컨테이너의 이름이 kafka-kafka-1-1 이 되었기 때문에 exec 옵션의 컨테이너명만 변경해주고 실행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvMzMq/btsEQLcJFL4/2pLLkOOQ94ZAkobXZXA8VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvMzMq/btsEQLcJFL4/2pLLkOOQ94ZAkobXZXA8VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvMzMq/btsEQLcJFL4/2pLLkOOQ94ZAkobXZXA8VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvMzMq%2FbtsEQLcJFL4%2F2pLLkOOQ94ZAkobXZXA8VK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1038&quot; height=&quot;113&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 KafDrop에 접속해서 두 개의 브로커에 3개의 파티션이 골고루 분배되었는지 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;895&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coNzyN/btsEVczUdr1/O331fNaCW71AkDpmDCeAX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coNzyN/btsEVczUdr1/O331fNaCW71AkDpmDCeAX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coNzyN/btsEVczUdr1/O331fNaCW71AkDpmDCeAX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoNzyN%2FbtsEVczUdr1%2FO331fNaCW71AkDpmDCeAX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1150&quot; height=&quot;895&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;895&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서 토픽을 생성할 때, &lt;b&gt;파티션의 배분은 Kafka 클러스터의 브로커들 사이에서 이루어지며, 이 과정은 Kafka의 파티션 할당 알고리즘에 의해 처리&lt;/b&gt;된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파티션 할당 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;균등 분배&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 가능한 한 모든 브로커에 파티션을 균등하게 분배하려고 한다. 이는 단일 브로커에 과도한 부하가 걸리는 것을 방지하기 위함이다.&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리더 분배&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 파티션에는 리더가 있으며, 리더는 해당 파티션의 읽기 및 쓰기 작업을 관리한다. Kafka는 리더 파티션도 가능한 한 균등하게 브로커들 사이에 분배한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;복제본 분산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션의 복제본은 원본 파티션과 다른 브로커에 위치한다. 이는 데이터의 내구성과 가용성을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클러스터에 3개의 브로커(kafka-1, kafka-2, kafka-3)가 있고, 3개의 파티션을 가진 토픽을 생성한다면, Kafka는 다음과 같이 파티션을 할당할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션 0: 리더는 kafka-1, 복제본은 kafka-2, kafka-3&lt;/li&gt;
&lt;li&gt;파티션 1: 리더는 kafka-2, 복제본은 kafka-3, kafka-1&lt;/li&gt;
&lt;li&gt;파티션 2: 리더는 kafka-3, 복제본은 kafka-1, kafka-2&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번외&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 학습을 위해서 &lt;s&gt;(사실 돈이 없어서)&lt;/s&gt; 프리 티어를 가지고 구현을 했다. 하지만 이게 왠걸. ec2에는 도커 패키지만 설치했고, 작동하는 컨테이너는 꼴랑 4개 (zookeeper, kafdrop, kafka-1, kafka-2)인데도 불구하고, 아무것도 하지 않았는데 메모리 사용량이 한계점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가동 프로세스중 상위 11개의 pid를 확인해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1707983013539&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ps -eo pid,ppid,rss,size,pmem,pcpu,time --sort -rss | head -n 11&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ7M5b/btsETpmf0b9/K13HKleNjfgsqBFK0r9hK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ7M5b/btsETpmf0b9/K13HKleNjfgsqBFK0r9hK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ7M5b/btsETpmf0b9/K13HKleNjfgsqBFK0r9hK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ7M5b%2FbtsETpmf0b9%2FK13HKleNjfgsqBFK0r9hK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1183&quot; height=&quot;306&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;top 명령어로 확인해본 결과 zookeeper,kafka-1,kafka-2를 동작시키기 위한 java가 memory 사용량이 높은것을 확인했고, 자그마치 60%는 사용하는듯 했다. 토픽 생성 명령어 하나에도 3~5초 딜레이가 되는것이라면,&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt; AWS EC2 프리티어로 카프카에 대한 학습을 진행하는 것은 구축 단계까지만 하는것&lt;/b&gt;&lt;/span&gt;이 심상에 이로울 듯 하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2182&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDwT3/btsEVKXqyxZ/09kuIL2R8OKRClENeBTQo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDwT3/btsEVKXqyxZ/09kuIL2R8OKRClENeBTQo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDwT3/btsEVKXqyxZ/09kuIL2R8OKRClENeBTQo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDwT3%2FbtsEVKXqyxZ%2F09kuIL2R8OKRClENeBTQo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2182&quot; height=&quot;1128&quot; data-origin-width=&quot;2182&quot; data-origin-height=&quot;1128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 찾아보았다. 카프카 클러스터 하드웨어 구성시 &lt;b&gt;최소 사양&lt;/b&gt;은 메모리 4GB, 최소 CPU 2코어, 디스크 50GB (SSD 권장), 1Gbps의 이더넷 네트워크가 요구된다. &lt;b&gt;권장 요구 사양은 메모리 8GB, 8코어 이상의 CPU, 32GB 이상의 Ram&lt;/b&gt;, RAID 구성의 SSD, 10Gbps의 이더넷 네트워크가 요구된다. 서버 구성시 참고하여 구성하자.&lt;/p&gt;
&lt;figure id=&quot;og_1708042462026&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Kafka Hardware Requirements&quot; data-og-description=&quot;Kafka Hardware Requirements&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@akash.d.goel/kafka-hardware-requirements-9328886fe88f&quot; data-og-url=&quot;https://medium.com/@akash.d.goel/kafka-hardware-requirements-9328886fe88f&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dwKIok/hyVmVOzrgN/4CYBIaUgjOPltwpdaA3Yk0/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742,https://scrap.kakaocdn.net/dn/45YMW/hyVmPOlZBS/kMeXRKaVMAVmDvQKG8bTW1/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742,https://scrap.kakaocdn.net/dn/bsb2pX/hyVmP1S2Dp/zAzeeCeLkisf7Q4UVjd650/img.png?width=1150&amp;amp;height=354&amp;amp;face=0_0_1150_354&quot;&gt;&lt;a href=&quot;https://medium.com/@akash.d.goel/kafka-hardware-requirements-9328886fe88f&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@akash.d.goel/kafka-hardware-requirements-9328886fe88f&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dwKIok/hyVmVOzrgN/4CYBIaUgjOPltwpdaA3Yk0/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742,https://scrap.kakaocdn.net/dn/45YMW/hyVmPOlZBS/kMeXRKaVMAVmDvQKG8bTW1/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742,https://scrap.kakaocdn.net/dn/bsb2pX/hyVmP1S2Dp/zAzeeCeLkisf7Q4UVjd650/img.png?width=1150&amp;amp;height=354&amp;amp;face=0_0_1150_354');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Kafka Hardware Requirements&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kafka Hardware Requirements&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/Kafka</category>
      <category>docker kafdrop 예제</category>
      <category>docker kafka cluster 구축</category>
      <category>kafka clustering</category>
      <category>kafka partitioning</category>
      <category>kafka 클러스터 docker</category>
      <category>kafka 클러스터 구축</category>
      <category>카프카 도커 클러스터 구축</category>
      <category>카프카 클러스터</category>
      <category>카프카 파티셔닝 방법</category>
      <category>카프카 파티션</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/308</guid>
      <comments>https://min-nine.tistory.com/entry/Kafka-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-Kafka-Clustering-And-Partitioning#entry308comment</comments>
      <pubDate>Thu, 15 Feb 2024 17:42:05 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka 서버 구축] AWS EC2 인스턴스에 Docker를 사용하여 Kafka와 Zookeeper를 연동해서 Kafka 서버 구축하기</title>
      <link>https://min-nine.tistory.com/entry/Kafka-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EC%97%90-Docker%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-Kafka%EC%99%80-Zookeeper%EB%A5%BC-%EC%97%B0%EB%8F%99%ED%95%B4%EC%84%9C-Kafka-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIzsy2/btsEKGh0VGB/JWy3un51vNsLK46adcrOMK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIzsy2/btsEKGh0VGB/JWy3un51vNsLK46adcrOMK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIzsy2/btsEKGh0VGB/JWy3un51vNsLK46adcrOMK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIzsy2%2FbtsEKGh0VGB%2FJWy3un51vNsLK46adcrOMK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 활용방법은 정말 무궁무진하다. 특히 실시간 데이터 처리를 요구하는 모든 상황에서 Kafka는 빛을  발한다. 앞으로 카프카의 사용법을 공부하고, 관련 프로그램을 만들어보기 위해 AWS EC2 프리티어에 Kafka 및 Zookeeper를 설치 및 연동하여 Kafka 서버를 만들어보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 정의 및 구조에 대한 간략한 설명은 예전에 작성해 놓은 아래 게시물을 참고하길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1707887865697&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Apache Kafka란? - 아파치 카프카에 대한 학습&quot; data-og-description=&quot; 대학생 시절에는 프로그래밍 언어를 위주로 공부하였고, 개발자가 되어 4년차가 된 지금, 프로그래밍 언어의 장벽은 낮아졌고 오히려 프로그래밍 아키텍쳐, 디자인 패턴, 파이프라인 구축 등 &quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/Apache-Kafka%EB%9E%80-%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4%EC%97%90-%EB%8C%80%ED%95%9C-%ED%95%99%EC%8A%B5-1&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/Apache-Kafka%EB%9E%80-%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4%EC%97%90-%EB%8C%80%ED%95%9C-%ED%95%99%EC%8A%B5-1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cttu4l/hyVjms4SQ6/bUo8NLpscep175DPNZNsn0/img.jpg?width=552&amp;amp;height=483&amp;amp;face=0_0_552_483,https://scrap.kakaocdn.net/dn/c2XLRg/hyVjn6zTFc/fkLQSwLMC4krKd1TJKV9s1/img.jpg?width=552&amp;amp;height=483&amp;amp;face=0_0_552_483,https://scrap.kakaocdn.net/dn/tnSjh/hyVjfm84HB/cinkX7fsu1Kp4O7YzLRakK/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/Apache-Kafka%EB%9E%80-%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4%EC%97%90-%EB%8C%80%ED%95%9C-%ED%95%99%EC%8A%B5-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/Apache-Kafka%EB%9E%80-%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4%EC%97%90-%EB%8C%80%ED%95%9C-%ED%95%99%EC%8A%B5-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cttu4l/hyVjms4SQ6/bUo8NLpscep175DPNZNsn0/img.jpg?width=552&amp;amp;height=483&amp;amp;face=0_0_552_483,https://scrap.kakaocdn.net/dn/c2XLRg/hyVjn6zTFc/fkLQSwLMC4krKd1TJKV9s1/img.jpg?width=552&amp;amp;height=483&amp;amp;face=0_0_552_483,https://scrap.kakaocdn.net/dn/tnSjh/hyVjfm84HB/cinkX7fsu1Kp4O7YzLRakK/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka란? - 아파치 카프카에 대한 학습&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt; 대학생 시절에는 프로그래밍 언어를 위주로 공부하였고, 개발자가 되어 4년차가 된 지금, 프로그래밍 언어의 장벽은 낮아졌고 오히려 프로그래밍 아키텍쳐, 디자인 패턴, 파이프라인 구축 등&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AWS EC2 인스턴스 설정 및 Docker 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EC2 인스턴스 설정 및 Docker 관련 패키지들을 설치해야 한다. 인스턴스 생성 방법 및 Docker 최신 패키지 설치 방법은 이전 포스팅에서 다뤘기 때문에 생략하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1707888033764&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[CI/CD 구축] AWS EC2에 Docker를 활용한 Jenkins 파이프라인 구축&quot; data-og-description=&quot; CI/CD를 구축하는 방법은 수백가지는 된다고 생각한다. 때문에 본인이 가지고있는 서버 환경이나 운영 되고있는 서비스에 따라 CI/CD 구축을 효율적으로 구축할 수 있어야 한다. 본 포스팅에서는 &quot; data-og-host=&quot;min-nine.tistory.com&quot; data-og-source-url=&quot;https://min-nine.tistory.com/entry/CICD-%EA%B5%AC%EC%B6%95-AWS-EC2%EC%97%90-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95&quot; data-og-url=&quot;https://min-nine.tistory.com/entry/CICD-%EA%B5%AC%EC%B6%95-AWS-EC2%EC%97%90-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/M5HQz/hyVjodliad/WJNdavX2Fa85gT321HvPrk/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/LzAwc/hyVi9HgrZZ/bXBJz1f9jlfIwnkldVsF6K/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/gVoAj/hyVi8uQxeM/hk8xp7KkFrJTLXnyNgKegK/img.png?width=3024&amp;amp;height=1430&amp;amp;face=0_0_3024_1430&quot;&gt;&lt;a href=&quot;https://min-nine.tistory.com/entry/CICD-%EA%B5%AC%EC%B6%95-AWS-EC2%EC%97%90-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://min-nine.tistory.com/entry/CICD-%EA%B5%AC%EC%B6%95-AWS-EC2%EC%97%90-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/M5HQz/hyVjodliad/WJNdavX2Fa85gT321HvPrk/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/LzAwc/hyVi9HgrZZ/bXBJz1f9jlfIwnkldVsF6K/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/gVoAj/hyVi8uQxeM/hk8xp7KkFrJTLXnyNgKegK/img.png?width=3024&amp;amp;height=1430&amp;amp;face=0_0_3024_1430');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[CI/CD 구축] AWS EC2에 Docker를 활용한 Jenkins 파이프라인 구축&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt; CI/CD를 구축하는 방법은 수백가지는 된다고 생각한다. 때문에 본인이 가지고있는 서버 환경이나 운영 되고있는 서비스에 따라 CI/CD 구축을 효율적으로 구축할 수 있어야 한다. 본 포스팅에서는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;min-nine.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 패키지까지 설치했다면 AWS EC2 인스턴스의 보안 그룹 설정에서 인바운드 규칙을 설정해야 한다. SSH(22포트), Kafka(9092포트), Zookeeper(2181포트)에 대한 액세스를 허용해준다. 어디까지나 각각의 Default 포트로 지정하는 것이니 만약, 해당 포트들을 이미 서버에서 사용하고 있다면 다른 포트로 지정해줘도 무방하다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2282&quot; data-origin-height=&quot;1072&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rBvkn/btsEKDMrHqX/n3ZeP2vNT4fgYrK9zbqYYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rBvkn/btsEKDMrHqX/n3ZeP2vNT4fgYrK9zbqYYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rBvkn/btsEKDMrHqX/n3ZeP2vNT4fgYrK9zbqYYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrBvkn%2FbtsEKDMrHqX%2Fn3ZeP2vNT4fgYrK9zbqYYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2282&quot; height=&quot;1072&quot; data-origin-width=&quot;2282&quot; data-origin-height=&quot;1072&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Zookeeper 및 Kafka 컨테이너 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zookeeper 컨테이너 생성 및 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어는 Zookeeper 이미지가 없다면 pull 받아 실행하고, 2181 포트를 할당한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707888492873&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run -d --name zookeeper -p 2181:2181 zookeeper&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2748&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iji6a/btsEP4WKtc0/A11bOK9V5QvfpyZi43ov9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iji6a/btsEP4WKtc0/A11bOK9V5QvfpyZi43ov9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iji6a/btsEP4WKtc0/A11bOK9V5QvfpyZi43ov9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIji6a%2FbtsEP4WKtc0%2FA11bOK9V5QvfpyZi43ov9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2748&quot; height=&quot;1118&quot; data-origin-width=&quot;2748&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 이미지가 없던 나의 경우, docker run 명령어 하나로 image Pulling 및 container Create가 정상적으로 이루어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka 컨테이너 생성 및 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령은 Kafka 이미지가 없다면 pull 받아 실행하고, Zookeeper와의 연결을 설정한다. '&amp;lt;EC2-Instance-IP&amp;gt;'를 EC2 인스턴스의 Public IP 주소로 대체하여 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707888999151&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name kafka -p 9092:9092 \
-e KAFKA_ZOOKEEPER_CONNECT=&amp;lt;EC2-Instance-IP&amp;gt;:2181 \
-e KAFKA_LISTENERS=INTERNAL://:9093,EXTERNAL://:9092 \
-e KAFKA_ADVERTISED_LISTENERS=INTERNAL://localhost:9093,EXTERNAL://&amp;lt;EC2-Instance-IP&amp;gt;:9092 \
-e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT \
-e KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL \
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
wurstmeister/kafka&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령에서 KAFKA_LISTENERS는&amp;nbsp;내부(INTERNAL)&amp;nbsp;및&amp;nbsp;외부(EXTERNAL)&amp;nbsp;리스너를&amp;nbsp;설정 다.&amp;nbsp;내부&amp;nbsp;리스너는&amp;nbsp;Kafka&amp;nbsp;브로커&amp;nbsp;간의&amp;nbsp;통신에&amp;nbsp;사용되고,&amp;nbsp;외부&amp;nbsp;리스너는&amp;nbsp;클라이언트의&amp;nbsp;연결에&amp;nbsp;사용 다.&lt;br /&gt;&lt;br /&gt;KAFKA_ADVERTISED_LISTENERS는 클라이언트가 Kafka 서버에 연결하기 위해 사용하는 주소를 설정하는데 여기에서 EXTERNAL 리스너에 EC2 인스턴스의 공개 IP 주소를 지정한다.&lt;br /&gt;&lt;br /&gt;이러한 설정은 Kafka 클라이언트가 EC2 인스턴스의 공개 IP를 통해 Kafka 서버에 접근할 수 있도록 하며, 동시에 Kafka 브로커 간의 통신을 위한 내부 리스너를 구성할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvZJqD/btsENR4Iaju/u4EutfVardM1hhUqowFYO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvZJqD/btsENR4Iaju/u4EutfVardM1hhUqowFYO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvZJqD/btsENR4Iaju/u4EutfVardM1hhUqowFYO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvZJqD%2FbtsENR4Iaju%2Fu4EutfVardM1hhUqowFYO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2866&quot; height=&quot;490&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 이미지가 없던 나의 경우, docker run 명령어 하나로 image Pulling 및 container Create가 정상적으로 이루어지지 않았다. 이미지 Pulling이 실패했었는데, 이유가 disk 용량 부족이었다. 때문에 Jenkins CI/CD 구축 포스팅에서 설치한 이미지들은 전부 삭제하여주었다.&amp;nbsp; 그 후에 정상적으로 이미지 pull 및 container create가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Kafka 서버 테스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka에 토픽 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 통해 Kafka에 새로운 토픽을 생성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1707889934977&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it kafka kafka-topics.sh --create --topic mingyu --partitions 1 --replication-factor 1 --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eALhSd/btsEOVeL8FW/wXe3cPt7lpWCY0jHahqiJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eALhSd/btsEOVeL8FW/wXe3cPt7lpWCY0jHahqiJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eALhSd/btsEOVeL8FW/wXe3cPt7lpWCY0jHahqiJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeALhSd%2FbtsEOVeL8FW%2FwXe3cPt7lpWCY0jHahqiJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1340&quot; height=&quot;318&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성된 토픽에 메시지 전송&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 통해 새로 만든 토픽에 메세지를 전송해보자. kafka-console-producer.sh의 경우 표준 입력(stdin)을 통해 매세지를 받아 Kafka 토픽에 전송한다. 때문에 테스트 메세지의 경우 나는 아래와 같이 echo와 파이프라인을 사용하여 메시지를 프로듀서에 전달했다.&lt;/p&gt;
&lt;pre id=&quot;code_1707890195474&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo &quot;Hello!  Myname Is Mingyu!&quot; | docker exec -i kafka kafka-console-producer.sh --topic mingyu --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 수신 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 통해 지정한 특정 토픽에 있는 메시지를 수신할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1707890286849&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it kafka kafka-console-consumer.sh --topic mingyu --from-beginning --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n3Krv/btsES1ktIdg/s7AIVG4FMyYYiK1cGymlG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n3Krv/btsES1ktIdg/s7AIVG4FMyYYiK1cGymlG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n3Krv/btsES1ktIdg/s7AIVG4FMyYYiK1cGymlG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn3Krv%2FbtsES1ktIdg%2Fs7AIVG4FMyYYiK1cGymlG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1324&quot; height=&quot;346&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 kafka-console-consumer.sh의 경우 표준 출력(stdout)을 통해 메세지를 수신받기 위해 대기중일테니 ^C 를 입력하여 Process를 종료할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d64WsM/btsEQc8mbji/oQCYM5JaGRSsduMutqcAa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d64WsM/btsEQc8mbji/oQCYM5JaGRSsduMutqcAa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d64WsM/btsEQc8mbji/oQCYM5JaGRSsduMutqcAa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd64WsM%2FbtsEQc8mbji%2FoQCYM5JaGRSsduMutqcAa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;86&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 도커를 사용하여 간단하게 Kafka &amp;amp; Zookeeper 연동의 카프카 메세지 서버를 구축할 수 있었다. 앞으로 실시간 데이터 처리 학습에 잘 이용해보기로 한다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;docker-compose로 어떻게 Kafka,Zookeeper를 연동할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 여러 컨테이너의 설정과 관리를 단순화하는 도구 다. Kubernetes와 비교했을 때, Docker Compose는 보다 간단한 시나리오와 소규모 프로젝트에 적합하며, YAML 파일을 사용하여 컨테이너 환경을 정의하고 실행한다. 때문에 개발 학습 목적으로 Kafka와 Zookeeper를 연동하는 경우, Docker Compose는 이 두 서비스를 함께 구성하고 관리하는 데 유용한 도구임에는 틀림이 없다.&lt;br /&gt;&lt;br /&gt;그러나 Docker Compose는 모든 환경에 완벽하게 적합한 만능 도구는 아닌걸 알아야 한다. 특히 대규모, 복잡한 분산 시스템을 운영할 때는 Kubernetes와 같은 보다 고급 오키케스트레이션 도구가 필요할 수 있다. Docker Compose를 사용할 때는 간편함과 편의성에만 의존하지 말고, 컨테이너화된 환경의 작동 원리와 네트워크 설정 등에 대한 이해를 깊게 하는 것이 중요다고 생각한다.&lt;br /&gt;&lt;br /&gt;Docker Compose의 남발은 개발자의 운영 능력을 향상시키는 데 방해가 될 수 있으므로, 도구 사용에 있어 균형 잡힌 접근 방식을 취하는 것이 중요다는 개인적인 생각을 얘기해보면서 아래 compose yml 파일을 작성해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1707898813763&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.9'

services:
  zookeeper:
    image: zookeeper
    ports:
      - &quot;2181:2181&quot;

  kafka:
    image: wurstmeister/kafka
    ports:
      - &quot;9092:9092&quot;
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_LISTENERS: INTERNAL://:9093,EXTERNAL://:9092
      KAFKA_ADVERTISED_LISTENERS: INTERNAL://localhost:9093,EXTERNAL://&amp;lt;EC2-Instance-IP&amp;gt;:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    depends_on:
      - zookeeper&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Zookeeper를 설치했는가?&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zookeeper는 분산 시스템에서 주로 사용되는 오픈 소스 서비스로, 분산된 환경에서의 데이터의 일관성과 동기화를 관리하는 데 사용되며&amp;nbsp; Kafka에서 Zookeeper는 주로 다음과 같은 목적으로 사용된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터의 상태를 관리하는 데 사용된다. Kafka 브로커(서버)들의 가입과 탈퇴를 추적하고, 클러스터의 상태 정보를 유지한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리더 선출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 토픽의 각 파티션에는 리더가 있으며, 모든 읽기와 쓰기 작업은 리더를 통해 이루어 다. Zookeeper는 이 리더 선출 과정을 관리한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메타데이터 저장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 중요한 메타데이터, 예를 들어 토픽, 파티션 정보, 그리고 각 브로커의 상태와 같은 정보를 Zookeeper에 저장한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분산 동기화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터 내에서 발생하는 여러 작업들의 동기화를 위해 사용된다. 예를 들어, 여러 브로커 간의 정보 동기화가 이에 해당된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고가용성을 위한 복구 메커니즘&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 브로커가 실패했을 때, Zookeeper를 통해 클러스터의 나머지 부분이 이를 감지하고 적절히 대응할 수 있다.&lt;br /&gt;&lt;br /&gt;Kafka 0.9 버전 이전에는 Zookeeper에 많은 의존성이 있었지만, 최근 버전의 Kafka는 Zookeeper 의존성을 줄이고 자체 메타데이터 관리 시스템을 구축하는 방향으로 발전하고 있다. 예를 들어, Kafka 2.8 부터는 Zookeeper 없이 Kafka를 사용할 수 있는 KIP-500이 도입되었다고 한다. 이는 Kafka의 확장성과 유지보수성을 향상시키기 위한 중요한 발전이다. 그러나 여전히 많은 기존 시스템들이 Zookeeper를 사용하고 있으며, &lt;b&gt;Zookeeper를 사용하는 것이 일반적인 설정이기 때문에 나는 Zookeeper와 연동했다. 블로그 포스팅 기준, 나는 2.8.1 Version의 Kafka 이미지를 사용했다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Infrastructure/Kafka</category>
      <category>aws ec2 kafka 구축</category>
      <category>AWS Kafka 서버</category>
      <category>docker kafka 구축</category>
      <category>ec2 docker kafka 구축</category>
      <category>ec2 kafka server</category>
      <category>ec2 kafka 구축</category>
      <category>ec2 인스턴스 docker로 kafka</category>
      <category>Kafka 서버 구축</category>
      <category>도커 카프카 서버 구축</category>
      <category>카프카 서버 구축</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/307</guid>
      <comments>https://min-nine.tistory.com/entry/Kafka-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EC%97%90-Docker%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-Kafka%EC%99%80-Zookeeper%EB%A5%BC-%EC%97%B0%EB%8F%99%ED%95%B4%EC%84%9C-Kafka-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0#entry307comment</comments>
      <pubDate>Wed, 14 Feb 2024 20:00:55 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD 구축] AWS EC2에 Docker를 활용한 Jenkins 파이프라인 구축</title>
      <link>https://min-nine.tistory.com/entry/CICD-%EA%B5%AC%EC%B6%95-AWS-EC2%EC%97%90-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2JnoZ/btsEBXbEXnr/hHFBcwlFnDESrN5bcB0R4K/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2JnoZ/btsEBXbEXnr/hHFBcwlFnDESrN5bcB0R4K/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2JnoZ/btsEBXbEXnr/hHFBcwlFnDESrN5bcB0R4K/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2JnoZ%2FbtsEBXbEXnr%2FhHFBcwlFnDESrN5bcB0R4K%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; CI/CD를 구축하는 방법은 수백가지는 된다고 생각한다. 때문에 본인이 가지고있는 서버 환경이나 운영 되고있는 서비스에 따라 CI/CD 구축을 효율적으로 구축할 수 있어야 한다. 본 포스팅에서는 CI/CD에 대해, 그리고 Docker라는 것에 대해 막연하게 생각하고 있는 사람들에게 도움이 되고자 AWS EC2 인스턴스에 Docker를 활용하여 간단하게 CI/CD 구축하는 방법을 안내한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AWS EC2 인스턴스 생성 및 INOUND 포트 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EC2 인스턴스는 요즘 클릭 몇번에 해결된다. 물론 생성 이후 보안규칙에서 INBOUND(EC2 인스턴스로 들어오는 데이터 혹은 요청에 대한 내용)에 대해 사전 지식이 필요하다. 나는 Ubuntu 22.04/t2.micro(프리티어)를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2인스턴스에는 docker만 설치했다. 파이프라인 구축에 필요한 Jenkins, spring application을 구동시킬 JDK, 향후 사용할 Redis 및 Nginx는 각각의 Docker Container로 구동시킬 것이고, spring application 및 Jenkins GUI에 접속, SSH 접속 및 SSL 보안설정을 하기 위해 Inbound 규칙을 80,8080,22,8089,443 포트를 추가하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c42C1h/btsEClpTyc1/JxkZe7iNdGA0KSZFsdgmz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c42C1h/btsEClpTyc1/JxkZe7iNdGA0KSZFsdgmz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c42C1h/btsEClpTyc1/JxkZe7iNdGA0KSZFsdgmz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc42C1h%2FbtsEClpTyc1%2FJxkZe7iNdGA0KSZFsdgmz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2312&quot; height=&quot;952&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;952&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8080&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring boot 기반의 Web Application에 접근하기 위한 포트. 추후 8080 포트를 Spring boot App에 접근하기 위한 8081 포트로 포트바인딩 해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;22&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스로 SSH 접속을 하기위한 포트.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8089&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins Dashboard (GUI)로 접근을 하기위한 포트. 추후 8089 포트를 Jenkins 도커 컨테이너의 8080포트로 포트바인딩 해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;80&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 Nginx를 사용하여 Front Project에 접근하기 위한 포트. 추후 Nginx 도커 컨테이너의 80 포트로 포트바인딩 해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;443&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL 보안설정을 통해 추후 Https로 접근하기 위한 포트.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 도커 설치 및 도커 컴포즈 파일 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EC2 인스턴스에 SSH로 접속을 하건 Console로 접속을 하건, Termial로 접근하여 도커를 설치한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;APT 도커 저장소 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1707310635040&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;amp;&amp;amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
sudo apt-get update&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최신 도커 패키지 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1707310687907&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커 컴포즈 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나는 EC2의 /app/docker/ path에 모든 파일을 생성할 것이다.&amp;nbsp;&lt;/b&gt;우선 docker-compose.yml 파일을 아래와 같이 생성한다. weakwil-api-project는 글 작성 기준 현재 내가 참여하고 있는 의지박약 탈출 스터디의 프로젝트를 진행하기 위해 지은 이름으로 각자 원하는 이름을 작성하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1707310812822&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.9'

services:
  nginx:
    image: nginx:latest
    ports:
      - &quot;80:80&quot;
    container_name: nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/sites-enabled:/etc/nginx/sites-enabled #virtual host
      - /app:/app
    networks:
      - weakwill-api-project

  redis:
    image: redis:latest
    container_name: redis
    ports:
      - &quot;6379:6379&quot;
    networks:
      - weakwill-api-project

  # Jenkins 서비스 추가
  jenkins:
    image: jenkins/jenkins:latest
    container_name: jenkins
    ports:
      - &quot;8089:8080&quot;  # Jenkins 웹 인터페이스 포트
      - &quot;50000:50000&quot;  # Jenkins 에이전트 포트
    volumes:
      - ./jenkins_home:/var/jenkins_home  # Jenkins 데이터 저장 볼륨
    networks:
      - weakwill-api-project

networks:
  weakwill-api-project:&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타 config 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 사용할 nginx 설정 관련 폴더를 /app/docker/nginx/ 하위에 생성한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;nginx.conf&lt;/h4&gt;
&lt;pre id=&quot;code_1707311015322&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] &quot;$request&quot; '
                      '$status $body_bytes_sent &quot;$http_referer&quot; '
                      '&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;';


    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;

    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
    #include /etc/nginx/sites-enabled/*;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;conf.d/default.conf&lt;/h4&gt;
&lt;pre id=&quot;code_1707311084146&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80;

    location / {
        proxy_pass http://당신의 Aws Ec2 Endpoint:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커 컴포즈로 컨테이너 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 생성하기 전에, 나처럼 프리티어로 진행하는 분들의 경우 프리티어의 인스턴스 메모리는 우리가 넉넉히 사용하지 못하는 양이기에 스왑 메모리를 설정해주자. 구글에 Swap Memory Setting in Ubunut 등의 키워드로 검색하면 수많은 방법들이 나와있을 것이다.&lt;b&gt; 나는 2GB의 스왑메모리를 확보&lt;/b&gt;하였다. 아래 도커 컴포즈 명령어를 사용하여 docker-compose.yml 파일에 명시되어있는 내용을 기준으로 컨테이너를 생성해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1707311273436&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/app/docker$ docker-compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1707311394116&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/app/docker$ docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED       STATUS        PORTS                                                                                      NAMES
295f4c4709b4   nginx:latest             &quot;/docker-entrypoint.&amp;hellip;&quot;   2 seconds     Up 37 hours   0.0.0.0:80-&amp;gt;80/tcp, :::80-&amp;gt;80/tcp                                                          nginx
4bbf7aa15872   redis:latest             &quot;docker-entrypoint.s&amp;hellip;&quot;   2 seconds     Up 37 hours   0.0.0.0:6379-&amp;gt;6379/tcp, :::6379-&amp;gt;6379/tcp                                                  redis
afb6f3754528   jenkins/jenkins:latest   &quot;/usr/bin/tini -- /u&amp;hellip;&quot;   2 seconds     Up 37 hours   0.0.0.0:50000-&amp;gt;50000/tcp, :::50000-&amp;gt;50000/tcp, 0.0.0.0:8089-&amp;gt;8080/tcp, :::8089-&amp;gt;8080/tcp   jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Spring Boot Project에 JenkinsFile 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스할 예정인 프로젝트면 Springboot던 Laravel이던 상관이 없다. 본인이 배포하고싶은 프로젝트의 최상위 패스에 JenkinsFile을 아래와 같이 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707311540122&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any

    //triggers {
    //    // This will trigger the pipeline on every push to the repository
    //    pollSCM('H * * * *')
    //}

    stages {
        stage('Checkout') {
            steps {
                // Checking out the code from the repository
                checkout scm
            }
        }

        stage('Build') {
            steps {
                // Run the gradle build
                sh './gradlew clean build'
            }
        }

        stage('Test') {
            steps {
                echo 'Testing the application...'
                // Run tests (if any)
                sh './gradlew test'
            }
        }

        stage('Deploy') {
            steps {
                // Deploy the application                
                sh '''
                cp build/libs/*SNAPSHOT.jar ~/deploy/
                echo $(date +&quot;%Y-%m-%d %H:%M:%S&quot;) &amp;gt; ~/deploy/deploy.txt
                '''

            }
        }
    }

    post {
        always {
            // Clean up the workspace to free up space
            cleanWs()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pipeline&amp;nbsp;구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Agent&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;agent any는 이 파이프라인이 Jenkins의 임의의 사용 가능한 에이전트에서 실행될 수 있음을 의미한다.&lt;br /&gt;&lt;b&gt;Triggers&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;triggers 섹션에서는 pollSCM('H * * * *')를 사용하여 소스 코드 관리(SCM) 시스템을 매시 정각마다 폴링하도록 설정한다. 이는 새로운 커밋이 있는지 확인하고, 있으면 파이프라인을 트리거하지만 우리는 웹훅을 사용할테니 주석처리 한다. 웹훅을 사용하지 않고 주기적으로 폴딩할때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Checkout&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'Checkout' 스테이지에서는 checkout scm 명령을 사용하여 소스 코드를 체크아웃한다. 이는 파이프라인이 실행되는 위치에서 Jenkinsfile을 포함한 저장소의 최신 코드를 가져온다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Build&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'Build' 스테이지에서는 sh './gradlew clean build' 명령을 통해 Gradle 빌드를 실행 다. 이 단계에서는 프로젝트가 클린 빌드 되며, 컴파일 및 기타 빌드 관련 태스크가 수행된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'Test' 스테이지에서는 sh './gradlew test' 명령으로 프로젝트의 테스트를 실행한다. 이는 단위 테스트나 다른 종류의 자동화된 테스트가 포함될 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Deploy&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'Deploy' 스테이지에서는 sh 스크립트를 사용하여 빌드된 JAR 파일을 ~/deploy/ 디렉토리로 복사하고, 현재 시간을 deploy.txt 파일에 기록하게 한다. 이는 배포의 일환으로 application을 컨테이너화 시킬때 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Post-processing&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 실행되는 후처리 단계로, 위 스크립트는 젠킨스 작업 공간을 정리한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Application을 포함하는 도커파일 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 JDK 17버전을 사용하는 Spring boot 기반의 Application을 배포할 것이기 때문에 아래와 같이 작성하였다. /app/docker/springboot/ 밑에 Dockerfile을 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1707312308535&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 기본 이미지로 OpenJDK가 포함된 공식 Java 이미지를 사용한다.
FROM openjdk:17

# 애플리케이션 JAR 파일을 컨테이너 내부로 복사한다.
COPY ../jenkins_home/deploy/weakwill-api-0.0.1-SNAPSHOT.jar /app/weakwill-api.jar

# 컨테이너가 시작될 때 애플리케이션을 실행한다.
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app/weakwill-api.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 /app/docker/jenkins_home을 jenkins 컨테이너의 /var/jenkins_home과 볼륨마운트 시켜놨기 때문에 위처럼 작성했다. 본인의 젠킨스 설정에 맞게 바꿔 사용하면 된다. 꼭 젠킨스 홈이 아니더라도, 컨테이너 외부의 ec2에서 접근이 가능한 폴더로 이동시키면 되니 참고하여 설정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. deploy.sh 파일 생성 및 크론 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히들 Jenkins를 CI/CD 도구로 알고있는데, 내가 생각했을 때 Jenkins는 CI (지속적인 통합 및 테스트) 까지를 구성하는 파이프라인 도구로 활용하면 좋겠지만, 그 다음의 배포 프로세스는 Docker Image를 만들어 DockerHub에 Pull하고, 그 트리거를 서비스 운영 서버에서 캐치하여 pull 및 run을 하는 형식으로 가야한다고 생각한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서는 하나의 Ec2 서버에서 모든것 (CI 부터 CD 까지)을 하는 목적을 두기 때문에 나는 서비스 어플리케이션 배포를 deploy.sh라는 쉘스크립트 파일을 만들었다. 주석을 보면 설명을 달아두었으니 이해하기 쉬울 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1707312733017&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

# deploy.txt 파일의 경로
DEPLOY_FILE=&quot;./jenkins_home/deploy/deploy.txt&quot;

# JAR 파일의 경로
JAR_FILE=&quot;./jenkins_home/deploy/weakwill-api-0.0.1-SNAPSHOT.jar&quot;

# deploy 폴더의 경로
DEPLOY_DIR=&quot;./jenkins_home/deploy&quot;

# Docker 이미지 이름
IMAGE_NAME=&quot;weakwill-api&quot;

# Docker 이미지 버전 태그
IMAGE_TAG=&quot;latest&quot;

# Docker 컨테이너 이름
CONTAINER_NAME=&quot;weakwill-api-container&quot;

# deploy.txt 파일이 존재하는지 확인
if [ -f &quot;$DEPLOY_FILE&quot; ]; then
    # Docker 이미지 빌드
    DOCKER_BUILDKIT=1 docker build -t $IMAGE_NAME:$IMAGE_TAG -f ./springboot/Dockerfile .

    # 기존 컨테이너가 실행 중인지 확인
    # 기존 컨테이너 이름 변경
    if docker ps -q -f name=$CONTAINER_NAME; then
        OLD_CONTAINER_NAME=&quot;${CONTAINER_NAME}-old-$(date +%Y%m%d%H%M%S)&quot;
        docker stop $CONTAINER_NAME
        docker rename $CONTAINER_NAME $OLD_CONTAINER_NAME
        #docker rm -f $OLD_CONTAINER_NAME
    fi

    # 새 Docker 컨테이너 실행
    docker run -d -p 8080:8081 --name $CONTAINER_NAME $IMAGE_NAME:$IMAGE_TAG

    # 기존 컨테이너 삭제 (선택 사항: 안정성 확인 후 수행)
    docker rm -f $OLD_CONTAINER_NAME


    # deploy 폴더 안의 내용 삭제
    rm -rf &quot;$DEPLOY_DIR&quot;/*
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1~4번까지 모든 프로세스를 구축하였다면 본인이 사용하는 git 저장소와  jenkins 컨테이너와의 webhook을 구성을 잘 맞췄다는 가정 하에 push 이벤트를 트리거로 삼아서 자동으로 빌드부터 테스트, application container를 통한 실 서비스 배포까지 정상적으로 작동할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Jenkins를 검색해서 본 포스팅을 읽는 독자분의 경우 대부분 GitLab보다는 GitHub를 사용할테니 (GitLab은 독자적인 CI/CD 및 데브옵스 워크플로우를 내장해서 Jenkins없이도 수 많은 방법이 있음) 웹훅 설정이 안되어 있다면 google에 &quot;Git Hub Jenkins WebHook Setting&quot; 등의 키워드로 검색하여 웹훅을 설정할 수 있으니 참고 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.  Source Code Update&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XSzzx/btsEA4iacPP/qxby3dLC6kZ2ScgilNQolk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XSzzx/btsEA4iacPP/qxby3dLC6kZ2ScgilNQolk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XSzzx/btsEA4iacPP/qxby3dLC6kZ2ScgilNQolk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXSzzx%2FbtsEA4iacPP%2Fqxby3dLC6kZ2ScgilNQolk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1710&quot; height=&quot;696&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Git Push&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkURgb/btsEBoOdx7D/XnWs9Yt6qk51RLrAyIGkIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkURgb/btsEBoOdx7D/XnWs9Yt6qk51RLrAyIGkIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkURgb/btsEBoOdx7D/XnWs9Yt6qk51RLrAyIGkIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkURgb%2FbtsEBoOdx7D%2FXnWs9Yt6qk51RLrAyIGkIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1617&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1617&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 젠킨스 Stage VIew 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젠킨스에서&amp;nbsp;application에&amp;nbsp;명시한&amp;nbsp;jenkinsfile의&amp;nbsp;스크립트를&amp;nbsp;실행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCmGB0/btsEAQdponF/NwHM7cfhFuOQYnpVFggveK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCmGB0/btsEAQdponF/NwHM7cfhFuOQYnpVFggveK/img.png&quot; data-origin-width=&quot;3022&quot; data-origin-height=&quot;1422&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.5409%; margin-right: 10px;&quot; data-widthpercent=&quot;50.12&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCmGB0/btsEAQdponF/NwHM7cfhFuOQYnpVFggveK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCmGB0%2FbtsEAQdponF%2FNwHM7cfhFuOQYnpVFggveK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3022&quot; height=&quot;1422&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4Wz5L/btsEAinFifP/jzVBVTIwm2gOzK21cKn6kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4Wz5L/btsEAinFifP/jzVBVTIwm2gOzK21cKn6kK/img.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1430&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.2963%;&quot; data-widthpercent=&quot;49.88&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4Wz5L/btsEAinFifP/jzVBVTIwm2gOzK21cKn6kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4Wz5L%2FbtsEAinFifP%2FjzVBVTIwm2gOzK21cKn6kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1430&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 배포 성공 파일 생성여부 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포&amp;nbsp;관련&amp;nbsp;파일이&amp;nbsp;생성되었는지&amp;nbsp;확인하고&amp;nbsp;기존&amp;nbsp;컨테이너&amp;nbsp;시작시간이&amp;nbsp;6시간&amp;nbsp;전인걸&amp;nbsp;확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2486&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qY1ra/btsEx6nYoPw/8Ii1s9xxwq2X6AsKLJsW9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qY1ra/btsEx6nYoPw/8Ii1s9xxwq2X6AsKLJsW9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qY1ra/btsEx6nYoPw/8Ii1s9xxwq2X6AsKLJsW9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqY1ra%2FbtsEx6nYoPw%2F8Ii1s9xxwq2X6AsKLJsW9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2486&quot; height=&quot;230&quot; data-origin-width=&quot;2486&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 크론 동작 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1분&amp;nbsp;뒤&amp;nbsp;크론이&amp;nbsp;동작하면서&amp;nbsp;새로운&amp;nbsp;컨테이너로&amp;nbsp;바뀐&amp;nbsp;것을&amp;nbsp;확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2598&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edpR73/btsEyPM3RQ6/RT7UPETiQxFQbcypeJkmrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edpR73/btsEyPM3RQ6/RT7UPETiQxFQbcypeJkmrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edpR73/btsEyPM3RQ6/RT7UPETiQxFQbcypeJkmrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedpR73%2FbtsEyPM3RQ6%2FRT7UPETiQxFQbcypeJkmrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2598&quot; height=&quot;166&quot; data-origin-width=&quot;2598&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 서비스 적용 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ec2의&amp;nbsp;엔드포인트&amp;nbsp;혹은&amp;nbsp;public&amp;nbsp;IP로&amp;nbsp;접속하여&amp;nbsp;확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/002kR/btsEx9yciTL/nSwTnLXCboaox8xSP53GR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/002kR/btsEx9yciTL/nSwTnLXCboaox8xSP53GR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/002kR/btsEx9yciTL/nSwTnLXCboaox8xSP53GR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F002kR%2FbtsEx9yciTL%2FnSwTnLXCboaox8xSP53GR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1912&quot; height=&quot;722&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infrastructure/CICD</category>
      <category>AWS ec2 CI/CD</category>
      <category>CI/CD</category>
      <category>ci/cd 구축</category>
      <category>docker jenkins</category>
      <category>docker jenkins ci/cd</category>
      <category>ec2 docker jenkins</category>
      <category>EC2 Jenkins</category>
      <category>ec2 jenkins docker</category>
      <category>jenkins docker ci/cd 예제</category>
      <category>도커 젠킨스</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/306</guid>
      <comments>https://min-nine.tistory.com/entry/CICD-%EA%B5%AC%EC%B6%95-AWS-EC2%EC%97%90-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95#entry306comment</comments>
      <pubDate>Wed, 7 Feb 2024 22:57:10 +0900</pubDate>
    </item>
    <item>
      <title>[SQLD] SQL 기본 - 함수, 조인, 표준 조인 요약</title>
      <link>https://min-nine.tistory.com/entry/SQLD-SQL-%EA%B8%B0%EB%B3%B8-%ED%95%A8%EC%88%98-%EC%A1%B0%EC%9D%B8-%ED%91%9C%EC%A4%80-%EC%A1%B0%EC%9D%B8-%EC%9A%94%EC%95%BD</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E08uK/btsEAXQmNGw/khWyt5NdAXuildNRro5Kc1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E08uK/btsEAXQmNGw/khWyt5NdAXuildNRro5Kc1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E08uK/btsEAXQmNGw/khWyt5NdAXuildNRro5Kc1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE08uK%2FbtsEAXQmNGw%2FkhWyt5NdAXuildNRro5Kc1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 함수 (Function)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 관리에서 SQL 함수는 데이터를 조회, 변환, 계산하는 데 사용되는 강력한 도구중 하나이다. SQL 함수는 입력 값을 받아 처리하고, 결과를 반환하는 일련의 SQL 문을 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL 함수의 기본&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 함수는 크게 내장 함수(built-in function)와 사용자 정의 함수(user-defined function, UDF) 두 가지 유형으로 분류된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;내장함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스&amp;nbsp;시스템에&amp;nbsp;사전에&amp;nbsp;정의된&amp;nbsp;함수로,&amp;nbsp;문자열&amp;nbsp;처리,&amp;nbsp;날짜&amp;nbsp;계산,&amp;nbsp;수학적&amp;nbsp;계산&amp;nbsp;등&amp;nbsp;다양한&amp;nbsp;기능을&amp;nbsp;제공한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용자 정의 함수 (User-Defined-Function)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 특정 작업을 수행하기 위해 직접 작성한 함수로, 복잡한 로직이나 반복적인 작업을 처리하는 데 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 사용하는 내장 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL에서 자주 사용되는 내장 함수들은 크게 여러 카테고리로 나눌 수 있으며, 각 카테고리에 따라 다양한 기능을 제공한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자열 처리 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPPER:&amp;nbsp;문자열을&amp;nbsp;대문자로&amp;nbsp;변환한다.&lt;br /&gt;LOWER:&amp;nbsp;문자열을&amp;nbsp;소문자로&amp;nbsp;변환한다.&lt;br /&gt;TRIM:&amp;nbsp;문자열&amp;nbsp;앞뒤의&amp;nbsp;공백을&amp;nbsp;제거한다.&lt;br /&gt;SUBSTRING&amp;nbsp;(또는&amp;nbsp;SUBSTR):&amp;nbsp;문자열에서&amp;nbsp;특정&amp;nbsp;부분을&amp;nbsp;추출한다.&lt;br /&gt;LENGTH:&amp;nbsp;문자열의&amp;nbsp;길이를&amp;nbsp;반환한다.&lt;br /&gt;CONCAT:&amp;nbsp;두&amp;nbsp;개&amp;nbsp;이상의&amp;nbsp;문자열을&amp;nbsp;결합한다.&lt;br /&gt;REPLACE:&amp;nbsp;문자열&amp;nbsp;내의&amp;nbsp;특정&amp;nbsp;문자를&amp;nbsp;다른&amp;nbsp;문자로&amp;nbsp;바꾼다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;날짜 및 시간 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NOW (또는 CURRENT_TIMESTAMP): 현재 날짜와 시간을 반환한다.&lt;br /&gt;DATE:&amp;nbsp;날짜&amp;nbsp;부분만&amp;nbsp;추출한다.&lt;br /&gt;TIME: 시간 부분만 추출한다.&lt;br /&gt;DATEDIFF: 두 날짜 간의 차이를 일 수로 반환한다.&lt;br /&gt;DATE_ADD: 특정 날짜에 일정 기간을 더한다.&lt;br /&gt;YEAR, MONTH, DAY: 날짜에서 년, 월, 일을 추출한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;수학적 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROUND: 숫자를 반올림한다.&lt;br /&gt;FLOOR: 숫자를 내림한다.&lt;br /&gt;CEIL 또는 CEILING: 숫자를 올림한다.&lt;br /&gt;ABS: 절대값을 반환한다.&lt;br /&gt;RAND: 임의의 숫자를 생성한다.&lt;br /&gt;SUM: 집합의 합계를 계산한다.&lt;br /&gt;AVG:&amp;nbsp;평균을&amp;nbsp;계산한다.&lt;br /&gt;MIN:&amp;nbsp;최소값을&amp;nbsp;찾는다.&lt;br /&gt;MAX:&amp;nbsp;최대값을&amp;nbsp;찾는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;집계 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT: 특정 조건을 만족하는 행의 개수를 계산한다.&lt;br /&gt;SUM:&amp;nbsp;숫자&amp;nbsp;컬럼의&amp;nbsp;총합을&amp;nbsp;계산한다.&lt;br /&gt;AVG:&amp;nbsp;숫자&amp;nbsp;컬럼의&amp;nbsp;평균값을&amp;nbsp;계산한다.&lt;br /&gt;MAX:&amp;nbsp;컬럼의&amp;nbsp;최대값을&amp;nbsp;찾는다.&lt;br /&gt;MIN:&amp;nbsp;컬럼의&amp;nbsp;최소값을&amp;nbsp;찾는다.&lt;br /&gt;GROUP_CONCAT:&amp;nbsp;그룹별로&amp;nbsp;문자열을&amp;nbsp;결합한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;변환 함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAST:&amp;nbsp;하나의&amp;nbsp;데이터&amp;nbsp;타입을&amp;nbsp;다른&amp;nbsp;데이터&amp;nbsp;타입으로&amp;nbsp;변환한다.&lt;br /&gt;CONVERT:&amp;nbsp;CAST와&amp;nbsp;유사하게&amp;nbsp;데이터&amp;nbsp;타입을&amp;nbsp;변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정의 함수 (UDF) 사용법&lt;/h3&gt;
&lt;pre id=&quot;code_1707284463207&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE FUNCTION AddNumbers (a INT, b INT)
RETURNS INT
AS BEGIN
    RETURN a + b;
END;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 함수를 사용하여 10과 20의 합을 구하는 쿼리는 다음과 같이 작성이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1707284489770&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT AddNumbers(10, 20) AS Result;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 조인 (Join)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인(Join)은 SQL에서 두 개 이상의 테이블을 연결하여 데이터를 조회하는 방법이다. 각각의 조인 방법은 데이터를 결합하는 방식에 따라 다르며, 특정한 요구 사항에 맞춰 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 조인 (INNER JOIN)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 조인은 두 테이블 간에 일치하는 행만 반환한다. 두 테이블에서 공통된 조건을 만족하는 데이터만 결과로 나타낸다. 아래 쿼리는 'employees' 테이블과 'departments'테이블을 'dept_id'와 'id'를 기준으로 조인하여 해당 부서에 속하는 모든 직원의 이름과 부서명을 반환하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1707285399633&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT A.name, B.department
FROM employees A
INNER JOIN departments B ON A.dept_id = B.id;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부조인 (OUTER JOIN)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 조인은 지정된 테이블의 모든 데이터와 다른 테이블의 일치하는 데이터를 반환한다. 외부 조인에는 좌측(L), 우측(R), 전체(FULL) 외부 조인이 있다. 아래 쿼리는 'employess' 테이블의 모든 직원과 'departments'테이블의 일치하는 부서를 반환한다. 부서가 없는 직원도 포함된다.&lt;/p&gt;
&lt;pre id=&quot;code_1707285473200&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT A.name, B.department
FROM employees A
LEFT OUTER JOIN departments B ON A.dept_id = B.id;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;교차조인 (CROSS JOIN)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교차 조인은 두 테이블 간의 모든 가능한 조합을 생성한다. 즉, 한 테이블의 모든 행이 다른 테이블의 모든 행과 결합된다. 아래 쿼리는 'employees' 테이블의 각 직원과 'departments' 테이블의 모든 부서간의 가능한 모든 조합을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707285561312&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT A.name, B.department
FROM employees A
CROSS JOIN departments B;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자연 조인 (NATURAL JOIN)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연 조인은 두 테이블 간에 같은 이름을 가진 모든 열을 기준으로 자동으로 조인한다. 명시적으로 조인 조건을 지정하지 않아도 된다. 아래 쿼리는 'employees'와 departments' 테이블을 자동으로 조인하여 동일한 이름의 열을 기준으로 일치하는 행을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707285620502&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM employees
NATURAL JOIN departments;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CapacityBuilding/SQLD 취득</category>
      <category>sql 조인</category>
      <category>sqld</category>
      <category>SQLD 독학</category>
      <category>SQLD 독학자료</category>
      <category>SQLD 시험자료</category>
      <category>SQLD 요약</category>
      <category>SQLD 요약본</category>
      <category>SQLD 조인 요약</category>
      <category>SQLD 함수</category>
      <category>SQLD 함수 요약</category>
      <author>MingyuKim</author>
      <guid isPermaLink="true">https://min-nine.tistory.com/305</guid>
      <comments>https://min-nine.tistory.com/entry/SQLD-SQL-%EA%B8%B0%EB%B3%B8-%ED%95%A8%EC%88%98-%EC%A1%B0%EC%9D%B8-%ED%91%9C%EC%A4%80-%EC%A1%B0%EC%9D%B8-%EC%9A%94%EC%95%BD#entry305comment</comments>
      <pubDate>Wed, 7 Feb 2024 19:50:27 +0900</pubDate>
    </item>
  </channel>
</rss>