<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet type="text/xsl" href="/sheet.xsl"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" version="2.0"><channel><title>넷마블 기술 블로그</title><atom:link href="https://netmarble.engineering/feed/" rel="self" type="application/rss+xml"/><link>https://netmarble.engineering/</link><description>netmarble engineering</description><lastBuildDate>Tue, 09 Jul 2024 01:20:36 +0000</lastBuildDate><language>ko-KR</language><sy:updatePeriod>
	hourly	</sy:updatePeriod><sy:updateFrequency>
	1	</sy:updateFrequency><image><url>https://netmarble.engineering/wp-content/uploads/2021/11/cropped-netmarble_engineering_fav-32x32.png</url><title>넷마블 기술 블로그</title><link>https://netmarble.engineering/</link><width>32</width><height>32</height></image><item><title>Figma 플러그인, 디자이너가 직접 만들어 보기</title><link>https://netmarble.engineering/create-figma-plugin-by-designer/</link><dc:creator>이동규</dc:creator><pubDate>Mon, 08 Jul 2024 23:00:00 +0000</pubDate><category>Onstage</category><category>Figma</category><category>TypeScript</category><category>사업디자인팀</category><category>영상디자인실</category><category>플러그인</category><guid isPermaLink="false">https://netmarble.engineering/?p=42968</guid><description>&lt;p&gt;Figma는 기존의 다른 툴과 비교했을 때 가볍고 다양한 플러그인을 업무에 적용할 수 있다는 장점이 있습니다. 이 글은 디자이너 시각으로 Figma의 장점을 업무에 적용하는 과정에서 ChatGPT와 Figma 공식 문서를 참고해 Figma 플러그인을 제작한 경험을 공유합니다.&lt;/p&gt;
&lt;p&gt;The post &lt;a href="https://netmarble.engineering/create-figma-plugin-by-designer/"&gt;Figma 플러그인, 디자이너가 직접 만들어 보기&lt;/a&gt; appeared first on &lt;a href="https://netmarble.engineering"&gt;넷마블 기술 블로그&lt;/a&gt;.&lt;/p&gt;
</description><enclosure url="https://netmarble.engineering/wp-content/uploads/2024/07/009.mp4" length="2087770" type="video/mp4"/><enclosure url="https://netmarble.engineering/wp-content/uploads/2024/07/010.mp4" length="1220666" type="video/mp4"/><content:encoded>&lt;div class="et_pb_with_border et_pb_module et_pb_post_content et_pb_post_content_0_tb_body n7e-post" morss_own_score="5.633262260127932" morss_score="173.61992813615578"&gt;
&lt;p&gt;안녕하세요, 넷마블 사업디자인팀 이동규입니다.&lt;/p&gt;
&lt;p&gt;넷마블 영상·디자인실은 업무 효율 향상의 목적과 최신 디자인 트렌드에 맞추고자 웹 기반의 인터페이스 디자인 프로토타이핑 툴인 Figma를 사용하고 있습니다. Figma는 기존의 다른 툴과 비교했을 때 가볍고, 다양한 플러그인을 업무에 적용할 수 있다는 장점이 있습니다. 이러한 장점을 업무에 적용하려던 과정에서 직접 Figma 플러그인을 제작해 보았고, 이 글을 통해 경험을 공유해 보고자 합니다.&lt;/p&gt;
&lt;p&gt;무겁고 깊이 있는 내용보다는 비전공자 입장에서 ChatGPT를 활용하여 실제 플러그인 제작까지 진행한 과정을 소개해 누구나 업무 효율을 높일 수 있다는 자신감을 가졌으면 하는 바람을 담았습니다.&lt;/p&gt;
&lt;h2&gt;편리한 툴이지만 그래도 불편한 부분이 있어서 만들기 시작함&lt;/h2&gt;
&lt;p&gt;전 세계 유저들에게 다양한 게임을 선보이는 넷마블의 디자인 업무 중에는 동일한 디자인 산출물을 다양한 이미지 사이즈와 언어로 변형(variation)해야 하는 일이 많습니다. Figma는 포토샵(Photoshop) 대비 단순한 UI 작업에는 가볍고 편하지만, 세밀하고 복잡한 그래픽 작업에는 무겁고, 세세한 타이포그래피 조정이 어렵습니다. Figma는 활용 가능한 공개 플러그인은 많지만 내가 원하는 작업에 완벽하게 적용하기가 쉽지 않아 추가 과정들을 거치며 업무를 진행해야만 했습니다. 예를 들면 다음 그림과 같은 작업입니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/001-1.png"&gt;&lt;figcaption&gt;공수가 드는 업무 목록&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;방금 소개한 것은 꼭 해야 하지만, 공수가 많이 드는 작업입니다. 그런데 TypeScript를 활용한 매크로 수준의 플러그인을 만들면 쉽게 해결할 부분으로 생각되었습니다. 그래서 업무에 보조적인 수단으로 활용할 수 있는 Figma 플러그인을 만들어 보기로 합니다.&lt;/p&gt;
&lt;p&gt;사업디자인팀에서 대표 MZ를 맡고 있는 만큼, 무작정 시작하기보다 공식 가이드를 보고 차근차근 개발 환경을 설정했습니다. 개발 환경 설정에 참고한 자료는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;피그마 플러그인 공식 가이드: &lt;a href="https://www.figma.com/plugin-docs/"&gt;https://www.figma.com/plugin-docs/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;TypeScript 코드 작성 도구: &lt;a href="https://code.visualstudio.com/"&gt;Visual Studio Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Figma 플러그인과 Figma가 통신하는 구조는 다음 그림과 같습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/002-1.png"&gt;&lt;figcaption&gt;Figma 플러그인의 통신 구조&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Figma 플러그인은 Figma의 동작을 직접적으로 제어하는 code.ts와 UI를 담당하는 ui.html라는 파일로 구성되어 있습니다. code.ts와 ui.html 사이는 직접적인 통신이 차단되어 있고 UI의 &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; 창(window)에 메시지를 전송하는 &lt;a href="https://www.figma.com/plugin-docs/api/properties/figma-ui-postmessage/"&gt;&lt;code&gt;postMessage&lt;/code&gt;&lt;/a&gt;와 UI의 &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; 창(window)에 들어오는 메시지의 핸들러를 등록하는 &lt;a href="https://www.figma.com/plugin-docs/api/properties/figma-ui-onmessage/"&gt;&lt;code&gt;onmessage&lt;/code&gt;&lt;/a&gt;라는 Figma의 API를 사용해 제한적으로 통신할 수 있습니다.&lt;/p&gt;
&lt;p&gt;개발 환경도 설정했고 기본 구조도 알았으니 MVP(Minimum Viable Product, 최소 기능 제품)를 만들어 보기로 합니다.&lt;/p&gt;
&lt;h2&gt;첫 개발은 계획대로 되지 않아&lt;/h2&gt;
&lt;h3&gt;ChatGPT를 활용해 첫 MVP까지&lt;/h3&gt;
&lt;p&gt;공통으로 사용하는 디자인 요소를 컴포넌트로 지정해도 언어와 UI 디자인을 구성하는 이미지 사이즈마다 필연적으로 일부 수정이 필요했고, 그때마다 컴포넌트화한 공통 디자인 요소들을 일일히 해제하여 작업하는 부분이 불편했습니다. 따라서 첫 번째 MVP로는 컴포넌트화하지 않고 선택한 객체와 동일한 이름을 가진 요소들의 스타일을 동기화하는 기능을 개발하기로 합니다.&lt;/p&gt;
&lt;p&gt;원하는 기능이 개발 가능한지를 빠르게 알아보기 위해서 ChatGPT의 도움을 받아 코드 작성을 요청했습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/003-1.png"&gt;&lt;figcaption&gt;ChatGPT를 활용한 첫 프롬프트&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;순조롭게 코드가 생성되고 컴파일하면 바로 실행될 줄 알았으나 오류가 발생합니다. 앞 프롬프트로 처음 생성된 코드는 대략 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;figma.ui.onmessage = msg =&amp;gt; {
  // 중간 생략

  if (msg.type === 'paste-styles') {
    if (!copiedStyles) {
      figma.notify("No styles copied. Please copy styles first.");
      return;
    }

    const layers = figma.currentPage.findAll(node =&amp;gt; node.name === figma.currentPage.selection[0].name);

    for (const layer of layers) {
      if (copiedStyles.fills) layer.fills = copiedStyles.fills;
      if (copiedStyles.strokes) layer.strokes = copiedStyles.strokes;
      if (copiedStyles.effects) layer.effects = copiedStyles.effects;
      if (layer.type === 'TEXT' &amp;amp;&amp;amp; copiedStyles.textStyle) {
        layer.textStyleId = copiedStyles.textStyle;
        layer.fontSize = figma.getStyleById(copiedStyles.textStyle).fontSize;
        layer.textAlignHorizontal = figma.getStyleById(copiedStyles.textStyle).textAlignHorizontal;
        layer.textAlignVertical = figma.getStyleById(copiedStyles.textStyle).textAlignVertical;
      }
    }

    figma.notify("Styles pasted!");
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ChatGPT와 오류 메시지를 주고받으며 씨름해도 답이 없어 보였습니다. 직접 코드를 들여다보며 API 문서와 대조해 본 결과, ChatGPT의 Figma API 정보가 최신이 아니었고, 존재하지 않는 &lt;code&gt;getStyleById&lt;/code&gt;라는 API를 존재하는 것처럼 코드를 생성해서 생긴 문제였습니다.&lt;/p&gt;
&lt;p&gt;결국 시행착오의 반복이라는 고통의 시간 끝에 개발이 어려운 요소들은 ChatGPT의 도움을 받되, 생성된 코드를 부분적으로만 참고해야 하고 (당연하지만) 전체 구조에 대해서 잘 알고 직접 코드를 작성해야 한다는 것을 깨달았습니다.&lt;/p&gt;
&lt;p&gt;이후 ChatGPT와 API 문서, 검색을 통해서 첫 MVP를 완성했습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/004.gif"&gt;&lt;figcaption&gt;첫 MVP 기능을 활용한 스타일 동기화&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;앞 그림을 보면 ‘타이틀 강조 문구’에 해당하는 객체와 동일한 이름을 가진 ‘제발 좀 ~ 가나다라마바사’ 요소가 ‘타이틀 강조 문구’ 스타일에 동기화됨을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;플러그인 디자인&lt;/h3&gt;
&lt;p&gt;첫 기능이 동작하는 것에 신이 나 업무하면서 불편한 점을 보완할 수 있는 기능들을 생각하고 구현하기 시작했습니다. 그리고 플러그인 디자인을 함께 진행했습니다. 이때부터는 빠르게 플러그인을 제작하면서도 일정 부분 이상의 완성도를 보장하기 위한 목적으로 &lt;a href="https://github.com/thomas-lowry/figma-plugin-ds?tab=readme-ov-file#onboarding-tip"&gt;Figma Plugin DS&lt;/a&gt; 라이브러리를 사용해 Figma와 유사한 Look &amp;amp; Feel로 디자인했습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/005-1.png"&gt;&lt;figcaption&gt;Figma 플러그인 디자인&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;이후 플러그인을 만들면서 여러 가지 난관에 부딪혔고 해결한 부분도, 해결하지 못한 채 넘어간 부분도 있었습니다. 주요 난관을 소개하면 다음과 같습니다.&lt;/p&gt;
&lt;h3&gt;난관 1: ui.html 내의 TypeScript 분리&lt;/h3&gt;
&lt;p&gt;Figma 플러그인에서는 상대 경로를 지원하지 않고 절대 경로만 지원하는 문제가 있습니다. 이 문제를 해결하려고 &lt;a href="https://engineering.linecorp.com/ko/blog/create-figma-translation-plugin-with-vuejs"&gt;감자탕 먹고 Vue.js로 Figma 번역 플러그인 만든 이야기&lt;/a&gt;를 참고해 플러그인의 webpack.config.js 파일에 &lt;a href="https://www.npmjs.com/package/html-inline-css-webpack-plugin"&gt;html-inline-css-webpack-plugin&lt;/a&gt;을 적용했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;덕분에 code.ts에는 다음과 같이 상대 경로로 CSS 파일을 사용할 수 있었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Style Sheets */
import "../node_modules/figma-plugin-ds/dist/figma-plugin-ds.css";
import "./static/css/reset.css";
import "./static/css/fonts.css";&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또한 ui.html 내에서 컨트롤하는 스크립트와 요소들도 별도의 파일로 분리하여 code.ts에 &lt;code&gt;import&lt;/code&gt;하고 싶었지만 잘되지 않았고, html-inline-css-webpack-plugin처럼 스크립트 파일의 상대 경로를 지원하는 &lt;a href="https://www.npmjs.com/package/html-inline-script-webpack-plugin"&gt;html-inline-script-webpack-plugin&lt;/a&gt;도 제대로 동작하지 않았습니다. 어쩔 수 없이 code.ts 내에서 TypeScript를 작성하되 함수와 주석으로 기능별 구분을 지었습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/006.png"&gt;&lt;figcaption&gt;한 파일 안에서 긴 주석을 작성해 기능별로 코드를 분리&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3&gt;난관 2: loadFontAsync&lt;/h3&gt;
&lt;p&gt;폰트와 관련된 기능이 동작하려면 항상 &lt;a href="https://www.figma.com/plugin-docs/api/properties/figma-loadfontasync/"&gt;&lt;code&gt;loadFontAsync&lt;/code&gt;&lt;/a&gt;로 로컬에 설치된 폰트를 동기화해야 합니다. 그런데 플러그인 실행 후 작업자가 폰트를 추가했을 경우에 실시간으로 반영되지 않아 이를 해결하고자 관련 기능이 동작할 때 실행하도록 했으나 플러그인 첫 실행에서만 동작하여 어쩔 수 없이 플러그인을 재실행하도록 안내하기로 했습니다.&lt;/p&gt;
&lt;h3&gt;난관 3: FontName의 style(weight) 타입과 StyledTextSegment의 fontweight 타입이 다름&lt;/h3&gt;
&lt;p&gt;텍스트의 스타일 속성을 복제할 때 읽어 오는 &lt;a href="https://www.figma.com/plugin-docs/api/FontName/"&gt;&lt;code&gt;FontName&lt;/code&gt;&lt;/a&gt;의 &lt;code&gt;style&lt;/code&gt;(weight)은 &lt;code&gt;Regular&lt;/code&gt;, &lt;code&gt;Bold&lt;/code&gt; 등의 텍스트 형식으로 가져오나, 직접 굵기를 지정할 때 사용하는 &lt;a href="https://www.figma.com/plugin-docs/api/StyledTextSegment/"&gt;&lt;code&gt;StyledTextSegment&lt;/code&gt;&lt;/a&gt;의 &lt;a href="https://www.figma.com/plugin-docs/api/StyledTextSegment/#fontweight"&gt;&lt;code&gt;fontweight&lt;/code&gt;&lt;/a&gt; 데이터 타입은 &lt;code&gt;fontweight: 400&lt;/code&gt;, &lt;code&gt;fontweight: 700&lt;/code&gt; 등의 숫자 형식으로 되어 있습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/007-1.png"&gt;&lt;figcaption&gt;Figma API 문서에서 설명하는 FontName과 StyledTextSegment&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;따라서 &lt;code&gt;FontName&lt;/code&gt;의 &lt;code&gt;style: Regular&lt;/code&gt;에 해당하는 설정을 &lt;code&gt;fontweight: 400&lt;/code&gt;, &lt;code&gt;style: Bold&lt;/code&gt;에 해당하는 설정을 &lt;code&gt;fontweight: 700&lt;/code&gt;으로 치환하여 처리하려 했으나, &lt;code&gt;FontName&lt;/code&gt;의 &lt;code&gt;style&lt;/code&gt;은 폰트 제작사마다 다르게 표기(Bold, bold, BOLD 등)되어 있었습니다. 다른 폰트에 텍스트 스타일을 복제할 경우 굵기가 다르면 굵기를 제외한 나머지 스타일만 적용하도록 예외 처리했습니다.&lt;/p&gt;
&lt;p&gt;글을 쓰는 지금 &lt;code&gt;FontName&lt;/code&gt; 대신 &lt;code&gt;TextNode&lt;/code&gt;에서 &lt;code&gt;fontWeight&lt;/code&gt;를 가져오면 해결할 수 있는 것을 알게 되었습니다…. 😢 좀 더 개선해 볼 여지가 남은 셈입니다.&lt;/p&gt;
&lt;h2&gt;평소 하고 싶었던 것을 원 없이 다 해봄&lt;/h2&gt;
&lt;p&gt;각종 문제와 지식의 한계에 부딪혀 어려움은 있었지만 Figma 플러그인을 완성해 가면서 평소 업무를 진행할 때 일정과 의견 차이로 반영하지 못한 부분들을 마음껏 반영할 수 있었습니다. 그중 대표적인 사례 몇 가지를 소개합니다.&lt;/p&gt;
&lt;h3&gt;기능 동작과 입력 폼 검증&lt;/h3&gt;
&lt;p&gt;Figma는 내부 디자이너뿐만 아니라 다양한 부서 사람들과 함께 사용하기 때문에 플러그인을 사용할 때 발생하는 문제나 기능 동작에 대한 문의 사항에 모두 대응할 수 없겠다는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;따라서 ui.html 파일 안의 모든 기능은 특정 조건에 부합해야만 활성화가 되도록 했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ((btn_apply.id === "btn--esc--apply" &amp;amp;&amp;amp; selectedElementNum === 1 &amp;amp;&amp;amp; 
  selectedElementName !== "#highlight") ||
  (btn_apply.id === "btn--lv--apply" &amp;amp;&amp;amp; isRegexValid) &amp;amp;&amp;amp; selectedElementNum &amp;gt; 0 ||
  (btn_apply.id === "btn--lv--align" &amp;amp;&amp;amp; isRegexValid) &amp;amp;&amp;amp; selectedElementNum &amp;gt; 0) {
  btn_apply.disabled = false;
} else {
  btn_apply.disabled = true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;입력 폼은 데이터를 검증하여 원하는 형식이 아닌 경우 오류 메시지가 나타나 무조건 안내된 가이드대로 해야만 동작하도록 설계했습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/07/008-1.png"&gt;&lt;figcaption&gt;버튼은 정해진 조건을 만족해야만 활성화되며, 입력 내용에 대한 피드백을 주도록 설계&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;또한 UI 부분인 ui.html뿐만 아니라 code.ts에서도 한 번 더 체크하도록 해두었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(languages.length &amp;lt; 2) {
  figma.notify("⚠️ " + actionName + "할 언어 개수는 최소 2개 국어 이상이어야 합니다.");
  return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;평소 프로젝트 진행 시 입력 단계가 아닌 폼 전송을 해야만 데이터 검증이 이루어지는 부분이 개인적으로 아쉬웠는데, 플러그인을 만들면서 입력 단계에서부터 검증할 수 있게 되었습니다.&lt;/p&gt;
&lt;h3&gt;웹 접근성 &amp;amp; 시맨틱 웹&lt;/h3&gt;
&lt;p&gt;프로젝트 일정이 촉박하거나 디자인이 자주 바뀌어 웹 접근성에 맞춰 UI를 디자인하기 어려운 경우가 많습니다. 단순 Figma 플러그인에 접근성이 필요할 일은 없지만 플러그인 제작을 기회 삼아 기능 전환, 토글 등의 단계에서 WAI-ARIA(Web Accessibility Initiative – Accessible Rich Internet Applications)를 적용했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 메뉴(기능)를 전환할 때 aria-current를 제어하는 부분
btn_menu.forEach(menu =&amp;gt; {
  menu.addEventListener("click", () =&amp;gt; {
    btn_menu.forEach(btn =&amp;gt; btn.removeAttribute("aria-current"));
    menu.setAttribute("aria-current", "page");
    panels.forEach(panel =&amp;gt; panel.classList.remove("active"));
    panels[Array.from(btn_menu).indexOf(menu)].classList.add("active");
    selectedElement.style.display = (menu.id === "menu__info") ? "none" : "block";}
  );
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;단순하지만 있으면 좋은 UX&lt;/h3&gt;
&lt;p&gt;플러그인 첫 배포 이후에는 작업할 때 있으면 좋겠다 싶은 UX들을 플러그인에 반영했습니다.&lt;/p&gt;
&lt;p&gt;플러그인을 실행할 때 마지막으로 열었던 메뉴가 활성화된 상태로 실행되거나, 이전에 입력한 입력 폼 데이터를 기억하되 최대 하루까지만 기억하는 등 각종 반복 작업을 하면서 동일한 메뉴로 이동하거나 데이터를 입력하는 수고를 덜 수 있도록 했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// clientStoreage에 저장된 시각과 현재 시각 차이 구분을 통한 입력 폼 저장 기간을 설정하는 부분
// 현재 날짜와 저장된 날짜 사이의 시간 차이 계산(밀리초 단위)
async function CheckLanguageListSavedTimeDiff() {
  const currentDate = new Date();
  let daysDiff = 0;
  try {
    const savedDate = await figma.clientStorage.getAsync("languageList-savedDate");
    const timeDiff = currentDate.getTime() - savedDate;
    daysDiff = timeDiff / (1000 * 3600 * 24); // 변수에 값 할당
  } catch (error) {
    console.error(error);
  }
  return daysDiff;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 기능을 개발하면서 사용자 컴퓨터에 데이터를 저장하는 &lt;a href="https://www.figma.com/plugin-docs/api/figma-clientStorage/"&gt;&lt;code&gt;clientStorage&lt;/code&gt;&lt;/a&gt;를 사용하고, 또 날짜 간의 차이를 계산하여 처리하는 방식 등을 생각해 볼 수 있어서 좋은 경험이 되었습니다.&lt;/p&gt;
&lt;h2&gt;배포 및 업무 적용&lt;/h2&gt;
&lt;p&gt;이후에도 여러 가지 테스트를 통해 사소한 버그들을 수정하면서 플러그인의 완성도를 높여갔습니다. 또한 플러그인 사용 가이드 작성과 함께 정식으로 Figma 플러그인을 사내에 소개했고 업무에 보조적으로 활용하기 시작했습니다.&lt;/p&gt;
&lt;p&gt;Figma 플러그인을 만들 때는 그동안 업무를 진행하며 불편했던 부분들을 개선하고자 여러 가지 기능을 담았으나 막상 Figma 플러그인을 소개한 후 상황을 보면 특정 기능 위주로만 사용한다는 점이 흥미로웠습니다. 그래도 특정 기능을 사용하여 기존 대비 약 50% 이상 작업 속도와 효율이 향상될 수 있어 다행으로 생각합니다. 기능 사용에 대한 트래킹을 지속한다면 추후 유용한 핵심 기능들 위주로 플러그인을 구성할 수 있지 않을까 싶습니다.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;다국어 프레임 복제 및 이름 변경(※ 이해를 돕기 위한 예시 영상입니다)&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;figcaption&gt;강조 구문 효과 적용(※ 이해를 돕기 위한 예시 영상입니다)&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2&gt;마치면서&lt;/h2&gt;
&lt;p&gt;평소 머릿속으로 “이렇게 하면 더 효율적으로 작업할 수 있지 않을까?” 혹은 “이런 건 자동화가 가능하면 좋을 텐데…”라는 생각을 자주 했었습니다. 하지만 직접 맨땅에 헤딩하기엔 엄두가 나질 않아 시도해 보지 못했던 것도 사실입니다.&lt;/p&gt;
&lt;p&gt;그런데 ChatGPT가 등장하고, 이를 활용한 덕분에 그간 생각에 머물렀던 플러그인 제작을 구현해 낼 수 있었습니다. 저에게는 매우 뜻깊은 경험이었습니다. 특히 비전공자가 코드를 작성할 때는 이해도와 자료 검색의 한계가 분명히 있는데 이러한 부분을 ChatGPT가 해결해 준다는 점은 매우 유용했습니다. 단, ChatGPT가 잘못된 정보를 제공하는 경우도 많기 때문에 ChatGPT가 제공하는 정보를 기반으로 올바른 관련 지식도 함께 탐구해야 한다는 점은 많은 분이 꼭 주의해야 할 사실이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 디자이너의 Figma 플러그인 제작 과정을 소개했지만, 꼭 Figma 플러그인이 아니더라도 평소 업무 환경에서 ChatGPT를 잘 활용한다면 Google Apps Script나 간단한 명령 프롬프트 스크립트를 통해 업무 효율성을 높일 수 있으리라 기대합니다.&lt;/p&gt;
&lt;/div&gt;</content:encoded></item><item><title>실행 시간 효율을 위한 클래스 데이터 공유(CDS)와 Layered Jar</title><link>https://netmarble.engineering/class-data-sharing-cds-and-layered-jar/</link><dc:creator>이동근</dc:creator><pubDate>Tue, 20 Feb 2024 23:00:00 +0000</pubDate><category>Onstage</category><category>CDS</category><category>GraalVM</category><category>JAVA</category><category>JVM</category><category>Layered Jar</category><category>QA시스템팀</category><category>QA실</category><category>spring</category><category>springboot</category><category>네이티브 이미지</category><category>스프링부트</category><guid isPermaLink="false">https://netmarble.engineering/?p=42946</guid><description>&lt;p&gt;자바는 바이트코드 형태로 패키징돼 JVM을 통해서 실행 환경에 맞는 기계어로 변환되는 과정을 거쳐 실행됩니다. 그렇기에 JVM이 설치된 곳이라면 어디든 동일한 결과가 나오도록 실행할 수 있습니다. 이런 장점은 동시에 단점이 되기도 합니다. 실행 환경에 맞춰 변환하기 위해 많은 시간...&lt;/p&gt;
&lt;p&gt;The post &lt;a href="https://netmarble.engineering/class-data-sharing-cds-and-layered-jar/"&gt;실행 시간 효율을 위한 클래스 데이터 공유(CDS)와 Layered Jar&lt;/a&gt; appeared first on &lt;a href="https://netmarble.engineering"&gt;넷마블 기술 블로그&lt;/a&gt;.&lt;/p&gt;
</description><content:encoded>&lt;div class="et_pb_with_border et_pb_module et_pb_post_content et_pb_post_content_0_tb_body n7e-post" morss_own_score="4.442105263157895" morss_score="96.86834416942062"&gt;
&lt;p&gt;안녕하세요, 넷마블 QA실 QA시스템팀 이동근입니다.&lt;/p&gt;
&lt;p&gt;백엔드 개발에서 사용하는 프로그래밍 언어는 C++, C#, Java, Python, Go, Javascript 등 매우 다양합니다. 제가 담당하는 크래시리포트는 이 중에서 자바를 이용해 백엔드 애플리케이션을 개발했습니다. 자바는 오픈소스 소프트웨어 생태계가 매우 광범위하고 역사도 오래됐습니다. 많은 개발자들의 선택을 받는 이유 중 하나가 아닐까 합니다.) 크래시리포트의 개별 서브 시스템 역시, 다양한 오픈소스 소프트웨어 중에 스프링부트(Spring Boot) 프레임워크를 활용해 개발했습니다.&lt;/p&gt;
&lt;p&gt;자바로 개발한 애플리케이션이 다른 언어 대비 좋은 점만 있는 것은 아닙니다. 자바로 만들어졌기 때문에 감내해야 하는 단점을 갖고 있습니다. 본 글에서는 자바 애플리케이션의 단점 중 하나인 초기 구동 시간이 오래 걸리는 문제에 대해서 이야기하고자 합니다. &lt;/p&gt;
&lt;h2&gt;실행 시간 효율을 향상하는 3가지 기술&lt;/h2&gt;
&lt;p&gt;자바는 바이트코드 형태로 패키징돼 JVM을 통해서 실행 환경에 맞는 기계어로 변환되는 과정을 거쳐 실행됩니다. 그렇기에 JVM이 설치된 곳이라면 어디든 동일한 결과가 나오도록 실행할 수 있습니다. 이런 장점은 동시에 단점이 되기도 합니다. 실행 환경에 맞춰 변환하기 위해 많은 시간이 소요됩니다. 일부 라이브러리는 이를 보완하기 위해 필요에 따라 로딩하는 절차가 추가되기도 합니다.&lt;/p&gt;
&lt;p&gt;자바를 이용하는 개발자 진영에서는 이런 초기 시작 시간이 오래 걸리는 문제를 해소하기 위해서 여러 접근 및 해소 방법을 제안하고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GraalVM Native Image&lt;/li&gt;
&lt;li&gt;JVM Checkpoint Restore: Project CRaC&lt;/li&gt;
&lt;li&gt;Project Leyden&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; 위 3가지 방법의 특징을 간단히 정리해 보겠습니다.&lt;/p&gt;
&lt;p&gt;첫 번째인 GraalVM Native Image는 미리 컴파일해서 바로 실행 가능한 상태로 만들어 주는 방법입니다. 바이트 코드를 기계 언어로 바꾸는 작업이 없어짐으로 인해, 초기 실행 시간이 단축되고 성능을 개선할 수 있습니다. 하지만, 네이티브 이미지는 생각보다 제약사항이 많습니다. 컴파일 시간이 오래 걸리고, 컴파일 과정에서 오류도 발생합니다. 컴파일이 된다고 하더라도, 실행 시 많은 오류가 발생합니다. &lt;/p&gt;
&lt;p&gt;특히 Mybatis 프레임워크를 사용한 애플리케이션의 경우, 네이티브 이미지를 만들기가 더욱 어렵습니다. 기존 Mybatis를 사용한 코드를 네이티브 이미지에서 실행할 수 있도록 대대적인 수정을 해야 합니다. 기존 서비스 코드를 수정하면서까지 네이티브 이미지를 적용하는 것은 큰 부담이 됩니다. GraalVM Native Image에 대한 좀더 자세한 내용은 지난번 글 “&lt;a href="https://netmarble.engineering/spring-boot-3-0-native-image-on-kubernetes/"&gt;쿠버네티스가 스프링부트 3.0 네이티브 이미지를 만났네&lt;/a&gt;”를 참고해 주세요.&lt;/p&gt;
&lt;p&gt;두 번째 방법은 &lt;a href="https://openjdk.org/projects/crac/"&gt;Project CRaC&lt;/a&gt;입니다. Project CRaC은 간단하게 설명하면, GraalVM처럼 Azul Systems에서 확장 개발한 JDK 안에 포함된 CRIU 프로그램을 사용해서 실행 중인 인스턴스의 스냅샷(Snapshot) 이미지를 생성하고, 이를 새로운 애플리케이션이 시작할 때 활용하게 해서  실행 시간과 초기 성능을 개선하는 방법입니다. 현재는 리눅스 환경만 지원하며, 스프링부트 3.2부터 지원하기 시작한 기능이라, 아직은  트러블슈팅에서 어려움을 겪을 수 있습니다. 저도 시도하다가 멈춘 상태입니다. &lt;/p&gt;
&lt;p&gt;세 번째 방법인 Project Leyden은 정적 이미지를 활용해 초기 시작 시간과 성능을 개선한 방법입니다. 핫스팟 JVM(HotSpot JVM), C2 컴파일러(C2 compiler), 애플리케이션 클래스 데이터 공유(Application Class-Data Sharing), 제이링크 코드 도구(jlink code tool), JDK의 기본 구성요소를 활용할 것이라고만 알려져 있습니다.&lt;/p&gt;
&lt;p&gt;위와 같은 방법들이 지속해서 제안되고 개발되는 것은, 최근 서비스 환경이 클라우드로 전환되고 순간 급증하는 트래픽에 대응하기 위함일지도 모릅니다. 오토스케일링으로 신규 서버 자원을 추가했을 때, 신규 인스턴스의 서비스 준비 시간을 단축해야 클라우드 자원을 효율적으로 사용할 수 있고, 더불어 비용도 절감할 수 있다고 생각합니다. (실제로 Project CRaC는 AWS Lambda와 IBM OpenLiberty의 지원을 받습니다.)&lt;/p&gt;
&lt;p&gt;위 방법들은 예측하기 어려운 대량 인입 트래픽에 대응할 접근 방법들입니다. 지난번 글 “&lt;a href="https://netmarble.engineering/spring-boot-3-0-native-image-on-kubernetes/"&gt;쿠버네티스가 스프링부트 3.0 네이티브 이미지를 만났네&lt;/a&gt;”에서 적용했던 네이티브 이미지는 지금도 잘 동작하며 늘어난 트래픽에 잘 대응하고 있습니다. &lt;/p&gt;
&lt;p&gt;위에서 언급한 네이티브 이미지와 Project CRaC은 효과는 강력하지만, 적용 과정이 복잡하고 제약 사항도 많습니다. 그래서 &lt;a href="https://openjdk.org/projects/leyden/"&gt;Project Leyden&lt;/a&gt;이 시작된 것 같습니다. 좀더 범용적이고 단순하게 문제를 해결하고 싶었을 것 같습니다. 가상 스레드(Virtual Thread)를 만든 &lt;a href="https://openjdk.org/projects/loom/"&gt;Project Loom&lt;/a&gt;처럼요. 가상 스레드는 아직 가능성에 비해 제한 요소가 많습니다. 하지만, 지속해서 생태계가 대응한다면 큰 변화를 줄 것이라고 생각합니다. (나중에 기회가 되면 가상 스레드를 적용했던 실험 과정과 결과를 공유하겠습니다.)&lt;/p&gt;
&lt;h2&gt;Project Leyden의 CDS&lt;/h2&gt;
&lt;p&gt;이번 글에서는 Project Leyden에서 활용하는 애플리케이션 클래스 데이터 공유(Application Class-Data Sharing)를 활용해, Mybatis가 적용된 시스템의 초기 실행 시간 개선을 해보고자 합니다.&lt;/p&gt;
&lt;h3&gt;클래스 데이터 공유의 적용 및 결과&lt;/h3&gt;
&lt;p&gt;애플리케이션 클래스 데이터 공유(application class-data sharing) 기능은 OpenJDK 12버전부터 제공된 기능입니다. 최근 스프링 프레임워크(Spring Framework) 6.1.3 버전에서 &lt;a href="https://spring.io/blog/2023/12/04/cds-with-spring-framework-6-1"&gt;정식 지원을 시작해서&lt;/a&gt; 좀 더 쉽게 사용할 수 있게 됐습니다. 기존 스프 링부트 애플리케이션에서 어떻게 적용하는지 살펴보겠습니다. &lt;/p&gt;
&lt;p&gt;저의 개발 환경은 JDK 21, 스프링부트 3.2.2, Mybatis 3.0.3 그리고 기타 등등의 라이브러리로 구성돼 있습니다. 위에서 언급했던 것처럼 Mybatis는 GraalVM을 이용해 네이티브 이미지로 만들기가 상당히 어렵습니다. 해내신 분들은 정말 고수이십니다. 저는 어려운 길보다 효과는 떨어지지만 안정적이고 간단한 방법으로 시도해 보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -XX:ArchiveClassesAtExit=application.jsa -jar -Dspring.context.exit=onRefresh -Dserver.port=8081 demo.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 명령어를 실행하면, 프로그램 초기 실행이 끝나면서 바로 종료됩니다. 그리고 &lt;code&gt;application.jsa&lt;/code&gt;라는 파일이 생성됩니다. 이렇게 생성한 파일과 다음 명령어를 이용해 실제 애플리케이션을 실행합니다. &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -Xlog:cds:file=dynamic-cds.log -Xlog:class+load:file=cds.log -XX:SharedArchiveFile=application.jsa -jar -Dserver.port=8081 demo.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 명령어를 실행하면, 실행 결과가 두 종류의 로그 파일로 남습니다. 둘 파일 중, &lt;a href="https://github.com/snicoll/cds-log-parser"&gt;CDS Log Parser&lt;/a&gt;를 활용해서 &lt;code&gt;cds.log&lt;/code&gt; 파일을 분석해 보겠습니다. 아래는 로그 내용입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Loading Report:
     18034 classes and JDK proxies loaded
     14653 (81.25%) from cache
      3381 (18.75%) from classpath


Categories:
   Lambdas  2064 (11.45%): 9.84% from cache
   Proxies   198 ( 1.10%): 51.52% from cache
   Classes 15774 (87.47%): 90.97% from cache


Top 10 locations from classpath:
       755 /home/appuser/unpack/BOOT-INF/lib/byte-buddy-1.14.11.jar
       435 __JVM_LookupDefineClass__
        95 __dynamic_proxy__
        84 jrt:/java.management
        69 org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer
        56 org.mariadb.jdbc.client.DataType
        53 jrt:/jdk.jfr
        47 jrt:/java.base
        31 org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryCustomizer
        31 org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper


Top 10 packages:
      5755 org.springframework (78.11% from cache)
      2742 org.hibernate (91.72% from cache)
       935 java.lang (54.12% from cache)
       805 net.bytebuddy (4.97% from cache)
       790 sun.security (99.62% from cache)
       778 org.apache (93.96% from cache)
       751 java.util (98.00% from cache)
       635 com.fasterxml (96.54% from cache)
       417 ch.qos (83.21% from cache)
       372 jdk.internal (93.55% from cache)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;81.25% 가 캐시를 통해서 로딩됐습니다. Top 10 package에 &lt;code&gt;org.springframework&lt;/code&gt;가 보입니다. 실제 소요된 실행시간을 아래 차트로 확인해 보겠습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/02/image1.png"&gt;&lt;figcaption&gt;실행 유형별 초기 실행 시간&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;위 차트를 보면 &lt;a href="https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3#layered-jars"&gt;Layered-jar&lt;/a&gt;가 보입니다. 기본 Jar는 압축 상태의 파일입니다. 이 Jar 파일의 압축을 풀어서 계층 형태로 분배하고 도커(Docker)로 복사하면, 도커 내 개별 레이어를 갖습니다. 이렇게 되면 라이브러리 영역과 사용자 코드 영역 등으로 분리돼, 신규 도커 이미지를 추가할 때 중복이 제거되므로 관리되는 도커 이미지의 크기를 줄일 수 있습니다. 즉, 전체가 101MB인 도커 파일에서 라이브러리는 100MB고 실제 코드는 1MB라면, 레이어로 분리된 애플리케이션을 신규 도커 이미지로 만들 때 변동되는 부분은 1MB가 됩니다. 그래서 관리 측면 이외에 추가로 실행 시간도 개선됩니다. Layered-jar는 &lt;a href="https://spring.io/blog/2020/08/14/creating-efficient-docker-images-with-spring-boot-2-3"&gt;스프링부트 2.3&lt;/a&gt; 이후 버전부터 정식 지원이 됩니다.&lt;/p&gt;
&lt;p&gt;Layered jar를 생성하고 실행하는 방법은 다음과 같습니다. &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN java -Djarmode=layertools -jar /build/target/demo.jar extract


COPY --from=builder /build/dependencies/ ./unpack/
COPY --from=builder /build/spring-boot-loader/ ./unpack/
COPY --from=builder /build/snapshot-dependencies/ ./unpack/
COPY --from=builder /build/application/ ./unpack/

WORKDIR /home/appuser/unpack
CMD ["java", "org.springframework.boot.loader.launch.JarLauncher"]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;멀티 스테이지로 도커파일(Dockerfile)을 구성하면, 빌더(builder) 스테이지에서 생성한 파일을 실행 스테이지에 위와 같이 복사해서 실행할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;고려해볼만한 선택지&lt;/h2&gt;
&lt;p&gt;위 적용 결과를 보면 최초 83초에서 56초로 실행 시 소요되는 시간을 30% 정도 단축했습니다. GraalVM을 이용했을 때보다는 단축 시간 개선율이 떨어지지만, 간단한 노력(적은 리소스 투입)으로도 30% 정도 향상할 방법이 있다는 의미가 됩니다. 있었습니다. 오토스케일링이 빈번해 초기 실행 시간을 단축하고 싶지만, GraalVM 네이티브 이미지를 사용할 수 없는 환경에서는 한 번쯤 고려해볼만한 선택지라고 생각합니다. &lt;/p&gt;
&lt;p&gt;Project Leyden도 클래스 데이터 공유 기능을 활용하기 때문에, 향후 정식 공개될 때는 아마도 유사한 절차로 사용할 수 있을 것이라 예상합니다. 자바가 세상에 나온 지 20년이 넘었습니다. 짧지 않은 시간 동안 쌓인 문제를 해결하기 위한 사용자들의 노력이 계속 유지되는 덕분에, 저를 포함한 많은 개발자가 꾸준히 자바를 개발 언어로 선택하는 것 같습니다. &lt;/p&gt;
&lt;p&gt;여러분의 서비스 환경 개선에 이번 글도 도움이 되길 바랍니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;참고자료&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;</content:encoded></item><item><title>리스와 헤이즐캐스트로 구성한 쿠버네티스 파드 클러스터링</title><link>https://netmarble.engineering/k8s-pod-clustering-lease-and-hazelcast/</link><dc:creator>이동근</dc:creator><pubDate>Sun, 07 Jan 2024 23:00:00 +0000</pubDate><category>Onstage</category><category>camel</category><category>Hazelcast</category><category>K8s</category><category>kubernetes</category><category>Lease</category><category>QA시스템팀</category><category>QA실</category><category>symbolicating</category><category>리스</category><category>서비스 디스커버리</category><category>심볼리케이팅</category><category>쿠버네티스</category><category>크래시리포트</category><category>클러스터링</category><category>파드</category><category>헤이즐캐스트</category><guid isPermaLink="false">https://netmarble.engineering/?p=42910</guid><description>&lt;p&gt;쿠버네티스에서는 개별 파드에서 발생한 데이터가 주변 파드에 영향을 주지 않는 것이 기본 구성입니다. 하지만 사용자가 필요하다면 공유할 수 있는 방법을 제공하고 있습니다. Apache Camel과 헤이즐캐스트와 같은 오픈 소스들은 이미 쿠버네티스에서 제공하는 클러스터링 절차에...&lt;/p&gt;
&lt;p&gt;The post &lt;a href="https://netmarble.engineering/k8s-pod-clustering-lease-and-hazelcast/"&gt;리스와 헤이즐캐스트로 구성한 쿠버네티스 파드 클러스터링&lt;/a&gt; appeared first on &lt;a href="https://netmarble.engineering"&gt;넷마블 기술 블로그&lt;/a&gt;.&lt;/p&gt;
</description><content:encoded>&lt;div class="et_pb_with_border et_pb_module et_pb_post_content et_pb_post_content_0_tb_body n7e-post" id="적용결과" morss_own_score="3.3939393939393936" morss_score="144.25930735930734"&gt;
&lt;p&gt;안녕하세요, 넷마블 QA실 QA시스템팀 이동근입니다. &lt;/p&gt;
&lt;p&gt;스프링 부트 3.0 네이티브 이미지를 쿠버네티스에 적용했던 후기글(&lt;a href="https://netmarble.engineering/spring-boot-3-0-native-image-on-kubernetes/"&gt;쿠버네티스가 스프링 부트 3.0 네이티브 이미지를 만났네&lt;/a&gt;) 이후, 두 번째 글이네요. 이번에는 쿠버네티스 파드 클러스터링 방법과 활용 예시를 공유해 드립니다.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;쿠버네티스&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;쿠버네티스는 대표적인 무상태(Stateless) 아키텍처 시스템입니다. 새로 생성된 파드는 이미 생성된 이전 파드에게 영향을 주지도 않고, 이전 파드에서 영향을 받지도 않습니다. 그렇기 때문에 급증하는 트래픽에 스케일아웃으로 원활히 대응하는 큰 장점을 가지고 있습니다. MSA(Microservice Architecture)로 구성한 시스템이라면 적극적으로 적용하기 좋습니다. 제가 담당하는 크래시리포트 또한 쿠버네티스를 사용하고 있으며, 트래픽에 기반한 HPA(Horizontal Pod Autoscaling)를 사용하고 있습니다. 하지만, 여러 파드 중에 일부 파드에서만 기능을 활성화하거나, 주변 파드에서 생성한 데이터를 공유해야 할 기능이 필요하다면 추가 설정을 해야 합니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;서비스 디스커버리&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;크래시리포트 서비스 아키텍처를 모놀리식(Monolithic)에서 MSA로 변경하는 과정에서 부하 분산을 위해 신규 인스턴스를 추가할 때, 이를 전체 서비스 목록에 자동으로 추가하고 트래픽을 전달하는 자동화 절차가 필요했습니다. &lt;/p&gt;
&lt;p&gt;서비스 디스커버리 구현 패턴에는 클라이언트 기반 패턴과 서버 기반 패턴이 있습니다. 클라이언트 기반 패턴에서는 &lt;a href="https://spring.io/projects/spring-cloud-netflix"&gt;Spring Cloud Netflix Eureka&lt;/a&gt;가, 서버 기반 패턴에서는 쿠버네티스가 대표적인 오픈 소스(OSS)입니다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.nginx.com/blog/service-discovery-in-a-microservices-architecture/"&gt;Service Discovery in a Microservices Architecture – NGINX&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;본 글에서는 쿠버네티스 서비스에서 제공하는 서비스 디스커버리 절차를 사용해 파드 클러스터링을 구성하고 실시간 스트리밍 서비스 구성과 캐시 데이터 공유를 통한 메시지 처리 성능을 개선해 보려 합니다. 이를 적용한 애플리케이션 두 가지에 대해서 좀 더 자세히 알아보겠습니다. &lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;크래시리포트 실시간 5분 통계 기능&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;크래시리포트에서는 최근 5분간 발생한 데이터를 스트리밍 데이터 처리 기능을 활용해서 실시간으로 대시보드에 푸시해 사용자에게 제공하고 있습니다. &lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image6.png"&gt;&lt;figcaption&gt;서비스 예시&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3&gt;&lt;strong&gt;기존 서비스 구성의 한계&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;기존 구성은 실시간 데이터 생성을 위해 GCP(Google Cloud Platform)의 Dataflow를 사용했었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/dataflow?hl=ko"&gt;Dataflow | Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dataflow는 서버리스 제품으로, 레디스(Redis) 인스턴스에 연결하고 푸시하는 절차가 복잡합니다. 또한 대시보드에서 웹소켓(Websocket) 구독을 연결하기에도 적합하지 않습니다. 그래서 ‘레디스에 푸시하는 기능’과 ‘레디스를 구독하고 웹소켓 서버를 구성하는 기능’을 위한 별도의 쿠버네티스 클러스터를 추가로 구성해야 했습니다. &lt;/p&gt;
&lt;p&gt;다만, Apache Beam 기반인 Dataflow에 신규 데이터 항목을 추가하기 위한 학습량이 많았으며, 빈번한 Apache Beam 버전 업데이트와 &lt;a href="https://cloud.google.com/dataflow/docs/support/sdk-version-support-status?hl=ko"&gt;GCP의 지원 만료&lt;/a&gt; 등으로 인해 유지보수의 어려움도 있었습니다. Dataflow를 대체할 수 있는 적절한 라이브러리를 이용해서 분산된 인스턴스들을 단일 쿠버네티스 기반 실시간 푸시 서비스로 재구성할 방법이 필요했습니다. (실시간 대시보드 푸시 기능을 구현한 기존 모습은 아래 그림을 참고해 주세요.)&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image4.png"&gt;&lt;figcaption&gt;기존 서비스 구성도&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;기존 구성에서 쿠버네티스는 다수의 파드가 Pub/Sub에서 수신하는 메시지를 Key/Value로 매핑 처리한 후 마스터 파드로 전달해 최종 값을 계산하도록 구성했습니다. 그리고 계산된 마지막 값을 레디스에 푸시해 웹소켓으로 구독 중인 대시보드에 전달했었습니다. &lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;신규 구성&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;신규 구성에서는 Apache Camel과 스프링 부트(Spring Boot)를 활용했습니다. Apache Camel은 데이터를 수신하고 처리한 후 보내야 하는 목적지를 정의하는 데이터 경로 설정을 기반으로 애플리케이션을 생성할 수 있습니다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://camel.apache.org/camel-core/getting-started/index.html#BookGettingStarted-Routes"&gt;Getting Started :: Apache Camel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;파드 내에는 개별 메시지에 대한 사전 통계 계산을 하는 스트리밍 라우터와 사전 계산된 데이터를 기반으로 최종 통계를 계산하는 스트리밍 라우터로 구성했습니다. 결과적으로 &lt;a href="https://www.geeksforgeeks.org/mapreduce-architecture/"&gt;하둡(Hadoop)의 MapReduce 아키텍처&lt;/a&gt;와 유사하게 구성했습니다. 스트리밍 처리 방법은 Apache Camel에서 제공하는 &lt;a href="https://camel.apache.org/components/3.20.x/eips/aggregate-eip.html"&gt;Camel Aggregation Strategy&lt;/a&gt;를 활용했습니다. &lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image3.png"&gt;&lt;figcaption&gt;파드 내 라우터 구성 예시&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Apache Camel은 쿠버네티스 환경에서의 클러스터링을 지원합니다. &lt;a href="https://developer.broadleafcommerce.com/production/configuration/camel-cluster-service"&gt;Camel-Kubernetes&lt;/a&gt;는 마스터 노드(API endpoint)에서 제공하는 파드 정보를 활용해 클러스터 내에 서비스 디스커버리를 제공합니다. Camel-master는 쿠버네티스의 리스(Lease) 객체를 활용해 리더(Master)를 선출하고, Active – Standby 서비스를 구성할 수 있게 해줍니다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.redhat.com/articles/2021/09/23/leader-election-kubernetes-using-apache-camel"&gt;Leader election in Kubernetes using Apache Camel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;리스(Lease) &lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;리스(Lease)는 분산 시스템에서 공유 리소스를 잠그고 노드 간의 활동을 조정하는 메커니즘입니다. 쿠버네티스에서는 ‘리스’ 개념을 &lt;code&gt;coordination.k8s.io&lt;/code&gt; API 그룹에 있는 리스 객체로 표현하며, 노드 하트비트나 컴포넌트 수준의 리더 선출 같은 시스템 핵심 기능에서 사용합니다.&lt;/p&gt;
&lt;p&gt;Camel-kubernetes 클러스터링을 위해서는 다음과 같이 설정해야 합니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-1024x688.png"&gt;&lt;figcaption&gt;애플리케이션 설정 예시&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;또한, 쿠버네티스 내 서비스 디스커버리와 리스 객체 점유를 위해서는 아래와 같이 계정과 권한을 설정해야 합니다. &lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-7-726x1024.png"&gt;&lt;figcaption&gt;ServiceAccount 설정 예시&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;위 설정을 적용하면, 아래 로그에서 보이는 것처럼 클러스터 목록과 최초 선출된 리더를 확인할 수 있습니다. 또한, 최초 선정된 리더 파드를 종료하면서 기존 파드 목록 중에서 신규 리더가 선출되는 것도 확인할 수 있습니다. &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2023-10-30 16:03:27.868 TimedLeaderNotifier - L:166 The cluster has a new leader: Optional[stream-aggregator-5c6b74f548-p672z]
2023-10-30 16:03:27.962 TimedLeaderNotifier - L:178 The list of cluster members has changed: [stream-aggregator-5c6b74f548-p672z, stream-aggregator-5c6b74f548-pjvsj]
2023-10-30 16:09:56.947 TimedLeaderNotifier - L:166 The cluster has a new leader: Optional[stream-aggregator-5c6b74f548-pjvsj]
2023-10-30 16:09:56.953 TimedLeaderNotifier - L:178 The list of cluster members has changed: [stream-aggregator-5c6b74f548-b4b7b, stream-aggregator-5c6b74f548-pjvsj]
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-3.png"&gt;&lt;figcaption&gt;&lt;code&gt;kubectl&lt;/code&gt;로 확인한 리더 변경 내역&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;kubectl&lt;/code&gt;을 통해서도 리스 객체를 점유하고 있는 파드의 아이디를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get leases leaders-lock2 -o yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kubectl get leases leaders-lock2 -o yaml&lt;/code&gt; 명령어를 사용하면 좀 더 상세한 내용을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;적용 결과&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;아래 그림은 신규 구성을 적용 이전과 이후의 현황입니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-4.png"&gt;&lt;figcaption&gt; 아키텍처 전환 이전과 이후 Pub/Sub 메시지 처리 성능 비교&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;기존 구성만큼 안정적으로 서비스를 제공하면서, 기존 구성 대비 미확인 메시지 수와 가장 오래된 미확인 메시지 기간이 감소한 것을 확인할 수 있었습니다. &lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;심볼리케이팅 캐시 데이터 공유&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;심볼리케이팅(Symbolicating)은 크래시리포트의 주요 기능 중 하나로, 크래시가 발생한 스레드(Thread)에서 나오는 백트레이스(BackTrace)에 포함된 프레임워크(Framework)의 내용을 사람이 읽을 수 있는 형태로 변환하는 작업입니다. &lt;/p&gt;
&lt;p&gt;심볼리케이팅 전/후에 대한 예시는 아래 이미지들을 참고해 주시면 됩니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image12.png"&gt;&lt;figcaption&gt;심볼리케이팅 전: 변환 대상 – mahya framework&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image11.png"&gt;&lt;figcaption&gt;심볼리케이팅 후: 결과&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3&gt;&lt;strong&gt;기존 캐시 시스템의 한계&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;크래시를 수집하다 보면 동일한 크래시가 반복해서 수집되는 현상이 나타납니다. 같은 크래시가 수집되기 때문에, 발생한 프레임워크 이름과 메모리 주소 정보 등을 개별 파드 안에 캐시 데이터에 저장하고 재사용하면 심볼리케이팅 소요 시간을 단축할 수 있습니다. 하지만, 캐시 데이터를 각각 파드에 쌓아야 하기 때문에 모든 파드에 캐시 데이터를 쌓기 전까지는 캐시를 활용한 성능 개선에 한계가 있었습니다. 그래서 파드끼리 데이터를 공유해 캐시 데이터 수집 시간을 단축할 수 있다면, 캐싱 효율성을 높이고 신규 배포 시 캐시가 초기화되는 문제도 일정 부분 해소할 수 있어 보였습니다. 즉, 파드 클러스터링을 이용해서 개별 파드에 있는 캐시 데이터를 공유하도록 구성을 변경할 방법이 필요했습니다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;헤이즐캐스트(Hazelcast)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;기존 구성에서는 embedded H2DB를 활용해서 JPA 기반 캐싱을 사용했습니다. 이 경우, 파드끼리 데이터를 공유하기 어려워 캐싱 효과를 보기가 어려웠습니다. 이를 해소하기 위해, 신규 구성에서는 헤이즐캐스트(Hazelcast)를 이용한 Key-Value 캐싱을 사용하기로 했습니다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.hazelcast.com/hazelcast/5.1/kubernetes/kubernetes-auto-discovery#preventing-data-loss-during-upgrades"&gt;Kubernetes Auto Discovery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;헤이즐캐스트를 활용하면 쿠버네티스 내 서비스 디스커버리를 간단한 설정만으로 클러스터링을 통한 데이터 공유를 쉽게 구성할 수 있습니다. 구체적인 서비스 설정은 다음과 같습니다. &lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-5-1024x672.png"&gt;&lt;figcaption&gt; 스프링 부트 내 쿠버네티스 클러스터링용 헤이즐캐스트 설정 예시&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-1-779x1024.png"&gt;&lt;figcaption&gt;헤이즐캐스트 클러스터를 위한 계정 및 권한 설정 예시&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-8-1024x692.png"&gt;&lt;figcaption&gt;헤이즐캐스트 간 통신을 위한 포트 설정&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;위 설정들을 적용 후 배포를 하면 아래와 같은 로그가 출력됩니다. 출력된 로그를 통해서 설정이 정상 작동하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-9-1024x302.png"&gt;&lt;figcaption&gt;헤이즐캐스트 클러스터링 성공 로그&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3&gt;&lt;strong&gt;적용 결과&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;아래는 헤이즐캐스트 적용 이전과 이후를 비교한 화면입니다. 파드에서 발생한 캐시 데이터가 주변 파드의 캐시에 공유됨으로써, 크래시의 심볼리케이팅을 수행하는 평균 시간이 감소한 것을 확인할 수 있었습니다. 또한, 미확인 메시지 기간 값이 최댓값 기준으로 기존 대비 평균 50% 정도 성능 개선이 있었습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2024/01/image-11-1024x414.png"&gt;&lt;figcaption&gt;헤이즐캐스트 적용 이전과 이후 비교&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;헤이즐캐스트는 쿠버네티스 배포 과정에서 기존 캐시 데이터를 신규 파드에 동기화하는 옵션을 제공합니다. 그 덕분에 신규 기능을 배포해도 기존 캐시 데이터를 유지할 수 있으므로, 배포에 따른 초기 성능 지연 문제도 해소할 수 있었습니다. &lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;파드 클러스터링 설정을 마치고&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;쿠버네티스에서는 개별 파드에서 발생한 데이터가 주변 파드에 영향을 주지 않는 것이 기본 구성입니다. 하지만 사용자가 필요하다면 공유할 수 있는 방법을 제공하고 있습니다. Apache Camel과 헤이즐캐스트와 같은 오픈 소스들은 이미 쿠버네티스에서 제공하는 클러스터링 절차에 맞는 클러스터링 기능을 제공하고 있었습니다. 저는 이를 이용해 기존 구성에서 나오던 병목 부분을 적절히 해소할 수 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파드 클러스터링과 리더 선출을 통해서 MapReduce 아키텍처를 구성할 수 있었고, 이를 통해 실시간 스트리밍 데이터 처리 구성이 가능했습니다. 쿠버네티스가 갖는 스케일아웃의 장점을 유지하면서 실시간 통계 계산도 가능하게 되었습니다. &lt;/li&gt;
&lt;li&gt;파드 클러스터링을 통해 캐시 데이터가 공유되도록 구성했습니다. 모든 파드에 캐시 데이터가 쌓이는 시간이 단축돼, 전체적으로 심볼리케이팅에서 소모하는 시간이 감소했습니다. 이는 메시지 처리량이 증대되는 개선으로 이어졌습니다. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;저는 이번 기회에 파드 클러스터링 설정을 접하면서, 쿠버네티스에 대해서 좀 더 자세히 알게 됐습니다. 그리고 크래시리포트 시스템이 갖고 있었던, 작지만 작지 않았던 문제를 개선했습니다. 위 2가지 사례 외에도 클러스터링을 활용한 개선 사례는 더 있으리라 생각합니다. 이 사례들이 여러분의 시스템에 있는 작은 문제들을 해결하는 또 다른 방법 중 하나가 될 수 있으면 좋겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;참고자료&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;</content:encoded></item><item><title>검은 토끼(2023년)와 이별을 준비하는 넷마블 기술 블로그</title><link>https://netmarble.engineering/adios-2023-n7e/</link><dc:creator>조병승</dc:creator><pubDate>Tue, 26 Dec 2023 23:00:00 +0000</pubDate><category>Backstage</category><category>2023년</category><category>ChatGPT</category><category>기술관리실</category><category>기술블로그</category><category>넷마블</category><category>회고</category><guid isPermaLink="false">https://netmarble.engineering/?p=42895</guid><description>&lt;p&gt;드디어 2023년 마지막 주네요. 다들 2023년 마무리 잘하고 계신가요? 넷마블 기술 블로그의 2022년 회고글인 생후 400일을 넘긴 넷마블 기술 블로그 육아일기를 다시 보면 감회가 새롭습니다. 넷마블 기술 블로그의 컨셉이나 초기 이야기는 작년 회고글에서 풀었으니, 그 후의...&lt;/p&gt;
&lt;p&gt;The post &lt;a href="https://netmarble.engineering/adios-2023-n7e/"&gt;검은 토끼(2023년)와 이별을 준비하는 넷마블 기술 블로그&lt;/a&gt; appeared first on &lt;a href="https://netmarble.engineering"&gt;넷마블 기술 블로그&lt;/a&gt;.&lt;/p&gt;
</description><content:encoded>&lt;div class="et_pb_with_border et_pb_module et_pb_post_content et_pb_post_content_0_tb_body n7e-post" morss_own_score="5.486146095717884" morss_score="83.41471752428932"&gt;
&lt;p&gt;안녕하세요, 넷마블 기술관리실 조병승입니다.&lt;/p&gt;
&lt;p&gt;드디어 2023년 마지막 주네요. 다들 2023년 마무리 잘하고 계신가요? 넷마블 기술 블로그의 2022년 회고글인 &lt;a href="https://netmarble.engineering/n7e-do-400d-celebrate/"&gt;생후 400일을 넘긴 넷마블 기술 블로그 육아일기&lt;/a&gt;를 다시 보면 감회가 새롭습니다. 넷마블 기술 블로그의 컨셉이나 초기 이야기는 작년 회고글에서 풀었으니, 그 후의 이야기에 집중해 보겠습니다.&lt;/p&gt;
&lt;h2&gt;넷마블 기술 블로그 현황&lt;/h2&gt;
&lt;h3&gt;발행 현황&lt;/h3&gt;
&lt;p&gt;발행 현황은 누적 카운트와 2023년의 카운트로 나눠서 봐야겠군요.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image3-1.png"&gt;&lt;figcaption&gt;월별 발행 현황&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;집계일 기준 누적 발행량은 총 69건, 2023년 발행량은 20건입니다. (이 글은 집계되지 않았으니, 엄밀하게는 2023년 발행량에 1건 더 추가해야 하는군요.) 발행량이 우하향하는 느낌이 살짝 들 수 있습니다. 2023년 3월과 9월에는 발행 글 자체가 없기도 했고, 실제로 2022년 대비 총량이 줄기도 했습니다.&lt;/p&gt;
&lt;p&gt;실제로, 글쓰기에 도전하셨다가 중도에 포기하신 분들이 여럿 계셨습니다. 2024년에는 중도 포기하시는 분들이 줄어들 수 있도록, 더 직접적으로 서포트를 해드려야겠습니다.&lt;/p&gt;
&lt;h3&gt;방문자, 조회수&lt;/h3&gt;
&lt;p&gt;GA 측정 기준, 넷마블 기술 블로그에 방문해서 28만회가 넘는 조회를 해주신 15.3만명께 감사 인사드립니다. 🙇‍♀️🙇‍♂️ &lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image4-1.png"&gt;&lt;figcaption&gt;월별 방문자 및 조회수 추이&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;발행량이 작년 대비 줄었음에도 불구하고, 더 많이 찾아와주셨었네요. 올해가 아니라 작년에 발행한 글을 계속 찾아봐 주신 덕분이라 생각합니다. 내년에도 폭발적이진 않더라도 길게 오래도록 보실 수 있는 내용을 찾아보겠습니다.&lt;/p&gt;
&lt;h3&gt;콘텐츠별 조회수&lt;/h3&gt;
&lt;p&gt;콘텐츠 내용을 기준으로 직군과 직무를 가리지 않는 범용성과 일반성이 높을수록 조회수는 절대적으로 많을 수밖에 없습니다. 그래서 조회수로 콘텐츠의 우열을 가리는 것은 부적절하다고 생각합니다. 아래 내용은 단순 참고용으로만 보는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;먼저, 작년과 올해 상위 10개를 먼저 뽑았습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image1-1-1024x378.png"&gt;&lt;figcaption&gt;2022년과 2023년 상위 조회수 10건&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;무려 7건이 작년과 올해 상위 10개에 겹치는군요. 글마다 발행일이 달라서 노출 기간도 다르므로 단순 비교로 특징을 잡아내기에는 무리가 있습니다. 다만 2년 연속 등장한 7건은 장수하고 있다는 의미이므로, 콘텐츠 관리자로서는 흐뭇해집니다.&lt;/p&gt;
&lt;p&gt;다음으로 전체 기간 누적 상위 20개를 뽑았습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image2-1.png"&gt;&lt;figcaption&gt;콘텐츠별 누적 조회수&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;그간 기술 블로그 내부 작업을 하면서, 각 저자나 태그별 URL로 독립 생성되도록 업데이트했던 결과가 전체 URL이 976개라는 것으로 나오는군요. ChatGPT로 상위 20개 글에 있는 주요 키워드 10개를 뽑았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;도커 데스크톱&lt;/li&gt;
&lt;li&gt;WSL2 (Windows Subsystem for Linux 2)&lt;/li&gt;
&lt;li&gt;고정 IP 설정&lt;/li&gt;
&lt;li&gt;언리얼 엔진 개발 환경&lt;/li&gt;
&lt;li&gt;ChatGPT&lt;/li&gt;
&lt;li&gt;OWASP Top 10 – 2021&lt;/li&gt;
&lt;li&gt;C/C++ 빌드 속도&lt;/li&gt;
&lt;li&gt;게임 서버 시스템&lt;/li&gt;
&lt;li&gt;HikariCP 옵션 및 설정&lt;/li&gt;
&lt;li&gt;데이터 파이프라인 원리와 원칙&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;내년에 새로 등장하는 키워드가 있을지 지켜봐야겠습니다.&lt;/p&gt;
&lt;h2&gt;운영자 후일담&lt;/h2&gt;
&lt;h3&gt;첫번째 후일담&lt;/h3&gt;
&lt;p&gt;넷마블 기술 블로그에 리퍼러를 달거나 엄청난 웹 추적 기술을 쓴다거나 하지 않기에, 외부에서 어떻게 비치고 있는지 정확히 알기는 어렵습니다. 그래도 휴먼테크로 오가며 둘러보는 중에 찾은 개인적으로 뿌듯했던 링크 2개를 소개합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://develop-jen.tistory.com/15"&gt;넷마블 기술 블로그를 보고 나도 기술 블로그를 작성하고 싶어졌다! &lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따로 부연 설명이 필요 없을 것 같지만, 누군가에게 귀감이 될 수 있어서 정말 기뻤습니다. 그래서 방명록에 찾아가서 인사드리고, 정말 간단한 채팅도 나눠보는 기회도 가졌습니다. 이후로도 계속 열심히 학습하시는 내용을 짤막하게라도 꾸준히 남기고 계시는데, 앞으로도 지치지 않고 잘 이어 나가셨으면 좋겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gall.dcinside.com/mgallery/board/view/?id=kmjgall&amp;amp;no=119330"&gt;[🐶🐻] DNS와 BGP 하이재킹 설명: ‘뉴진스 하입보이’&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 일이 생기기도 하는군요. 뉴진스 민지 갤러리라니… &lt;/p&gt;
&lt;h3&gt;두번째 후일담&lt;/h3&gt;
&lt;p&gt;내부에서 기술 블로그 운영을 위해서 끝나지 않는 에픽 2개를 만들어 놨습니다. ‘기술 블로그 콘텐츠’와 ‘기술 블로그 홈페이지’라고 이름을 붙여놨습니다. (끝나지 않는 에픽이라니, 에픽을 잘못 쓰고 있는거죠?)&lt;/p&gt;
&lt;p&gt;이 에픽을 추적해서 2023년을 묶어보니, 이슈 타입이 스토리인 태스크는 총 110건(콘텐츠 67, 홈페이지 43)이었네요. 모든 기록을 남겨놓진 않았지만, 콘텐츠에서는 303.3시간, 홈페이지에서는 85.6시간을 보냈군요. (2022년 기록을 남겨놓지 않아서, 비교해보지 못하는 것이 아쉽습니다.)&lt;/p&gt;
&lt;p&gt;매일 출근하면 두근대는 마음으로 가장 먼저 시작하는 업무가 위에 나온 현황 확인입니다. 간밤에 특별한 이벤트가 있었는지 혹시나 어제 재미난 에피소드가 있었는지 매일 10분씩 살펴보는 그 순간은, 마치 택배 상자를 열어볼 때와 같은 기분이죠. 분명히 무엇을 주문했는지 알고 있음에도, 항상 상자를 열 때 설레는 느낌. &lt;/p&gt;
&lt;p&gt;이 느낌을 내년에도 소중히 이어가겠습니다.&lt;/p&gt;
&lt;h2&gt;풍성한 연말 보내시고, 새해 복 많이 받으세요&lt;/h2&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/adios-2023-n7e-1024x585.png"&gt;&lt;figcaption&gt;adios, 2023!&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;2024년에도 ‘주제와 형식에 구애받지 않고 뭐든지 소화’하는 넷마블 기술 블로그로 성장하겠습니다. &lt;/p&gt;
&lt;p&gt;새해 복 많이 받으세요!&lt;/p&gt;
&lt;/div&gt;</content:encoded></item><item><title>워드프레스 백업과 복원은 웹 파일과 Db를 한 쌍으로 맺어야 한다</title><link>https://netmarble.engineering/wordpress-backup-and-restore/</link><dc:creator>조병승</dc:creator><pubDate>Thu, 21 Dec 2023 23:00:00 +0000</pubDate><category>Onstage</category><category>기술관리실</category><category>백업</category><category>복원</category><category>셸스크립트</category><category>스냅샷</category><category>업데이트</category><category>워드프레스</category><category>인프라실</category><category>인프라운영팀</category><guid isPermaLink="false">https://netmarble.engineering/?p=42877</guid><description>&lt;p&gt;1년에 4번 정도 마주치는 워드프레스 업데이트 테스트 상황에 조금 더 편하게 대응하고  있습니다. 다만, 편하게 만나는 환경에 직면한 작업자는 저를 포함해 극소수밖에 되지 않아, 단순히 시간 효용을 계산하자면 큰 이득을 얻진 못했다고도 볼 수도 있습니다. 그래도 업데이트 테스트를...&lt;/p&gt;
&lt;p&gt;The post &lt;a href="https://netmarble.engineering/wordpress-backup-and-restore/"&gt;워드프레스 백업과 복원은 웹 파일과 DB를 한 쌍으로 맺어야 한다&lt;/a&gt; appeared first on &lt;a href="https://netmarble.engineering"&gt;넷마블 기술 블로그&lt;/a&gt;.&lt;/p&gt;
</description><content:encoded>&lt;div class="et_pb_with_border et_pb_module et_pb_post_content et_pb_post_content_0_tb_body n7e-post" morss_own_score="5.985120892746435" morss_score="204.54762089274644"&gt;
&lt;p&gt;안녕하세요, 넷마블 기술관리실 조병승입니다.&lt;/p&gt;
&lt;p&gt;연말이 다가오니 여기저기 한해를 되돌아보는 회고글이 보이기 시작합니다. 넷마블 기술 블로그의 지난 1년에서 개인적으로 가장 뿌듯했던 순간이 떠올라, 그 이야기를 먼저 꺼내볼까 합니다.&lt;/p&gt;
&lt;h2&gt;1년에 8번 업데이트하는 워드프레스 코어&lt;/h2&gt;
&lt;p&gt;넷마블에는 워드프레스로 관리하는 콘텐츠 사이트가 2가지 있습니다. 외부로 공개되는 넷마블 기술 블로그와 내부 개발자가 참고하는 개발자 사이트죠. 몇몇 매뉴얼 제공용 사이트가 더 있었습니다만, 틈틈이 개발자 사이트로 통합하고 있습니다.&lt;/p&gt;
&lt;p&gt;워드프레스 코어 버전은 매우 자주 업데이트됩니다. &lt;a href="https://wordpress.org/news/category/releases/"&gt;릴리스 히스토리&lt;/a&gt;를 보면, 마이너 버전까지 포함했을 때 2023년에만 8번이나 출시됐습니다. 2022년을 포함하면 20번이네요. 마이너 업데이트도 주로 보안 취약점에 대응하고 있기 때문에, 최대한 함께 커버하는 것이 좋다고 생각합니다. 그래서 분기에 최소 1번 이상 코어 버전 업데이트에 대응하고 있습니다. &lt;/p&gt;
&lt;h3&gt;업데이트 주기를 알 수 없는 플러그인&lt;/h3&gt;
&lt;p&gt;워드프레스 코어를 업데이트한 후, 최우선으로 사이트 외적 구조 영향 여부를 확인합니다. 이때 꼭 워드프레스 플러그인을 함께 챙겨야 합니다. 플러그인으로 구성한 외적 요소가 있기 때문에, 혹시라도 코어 업데이트로 인해 API가 바뀌거나 지원이 끊어질 수 있습니다.&lt;/p&gt;
&lt;p&gt;플러그인은 직접 개발하거나 유지보수 하지 않더라도 필요한 기능을 구현하는 가장 빠른 수단 중 하나입니다. 워드프레스 생태계에는 오래도록 지속한 세월과 많은 사용자만큼 무수한 플러그인이 있습니다. ‘워드프레스 생태계의 꽃’이라 할 수 있죠. &lt;/p&gt;
&lt;p&gt;더욱이, 플러그인 생태계에 들어와 있는 여러 업체와 개인 개발자들 이외에도 사용자들까지 합세해서 워드프레스 업데이트에 맞춰서 플러그인 동작에 이상이 없는지 확인하고 사용 후기를 남겨놓습니다. 다만, 워드프레스 코어 업데이트에 영향을 받았을 때 바로바로 대응해 주지 못하는 플러그인도 있기 때문에 ‘양날의 검’이 되기도 합니다. 의존도가 너무 큰 경우에는 플러그인이 업데이트될 때까지는 코어를 업데이트할 수 없는 결과로 이어지기도 하니까요. &lt;/p&gt;
&lt;p&gt;현재 넷마블 기술 블로그에서 사용하는 플러그인은 11개, 개발자 사이트에서 사용하는 플러그인은 25개 정도입니다. (차일드 테마에서 수작업으로 활성화한 기능까지 플러그인으로 친다면 개수는 좀 더 늘어날 수 있습니다.) 워드프레스 코어 업데이트에서 플러그인으로 구현한 기능이 나오면, 플러그인을 제거하면서 의존도를 줄이기도 합니다. 그나마 커머스 기능이 붙어있지 않아서 플러그인이 많지 않아 다행이다 싶기도 합니다.&lt;/p&gt;
&lt;h2&gt;업데이트했더니 문제가 생겼다면!&lt;/h2&gt;
&lt;p&gt;워드프레스 코어와 플러그인 업데이트를 하려면, 잘 동작하는지 여부를 꼭 확인해야 합니다. 아마도 업데이트가 긴장되는 이유는, 남들은 다 된다는데 나만 안 될 때가 생기기 때문일 겁니다. 만약 문제가 생긴다면, 이전 상태로 롤백해서 대처하는 것이 사용자에게 영향을 가장 적게 주면서 가장 빠른 방법입니다. 그래서 불시 상황에 대비해 항상 롤백 대책을 마련해야 합니다.&lt;/p&gt;
&lt;h3&gt;라이브와 완전히 똑같은 환경으로 테스트하려면&lt;/h3&gt;
&lt;p&gt;분명히 테스트 서버에서는 아무런 문제가 없었는데, 라이브 서버에서는 뭔가 꼭 문제가 생기는 경우가 있습니다. 여러 의존성을 탈피하기 위해, 도커나 컨테이너 환경 등을 통해 배포하라는 조언을 주는 분들도 많으시죠. 아래에서 이야기가 나오겠지만, 워드프레스 웹 파일과 DB를 한 쌍으로 맞춰서 배포해야 합니다. 라이브 서버에서 서비스 중단 지점이 생겨야 안정적이고 편한 배포가 가능하단 의미죠. 워드프레스의 기본 구조를 완전히 분해해서 사용하지 않는 이상, 실현하기 쉽지 않은 계획이라 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;사이트 운영 초기에는 테스트 서버에 비슷한 구성으로 사이트를 올려놓고 여러 테스트를 했었습니다. 테스트 횟수가 늘수록, 테스트 사이트 구조가 점점 라이브와 차이가 커질 수밖에 없었죠. 라이브 서버와 테스트 서버 사이에 차이가 커질 수록 테스트 결과에 대한 신뢰도는 떨어집니다.&lt;/p&gt;
&lt;p&gt;테스트 서버는 라이브 사이트의 워드프레스 DB는 그대로 두고 테스트 서버용 DB를 따로 사용했습니다. 그리고 웹 파일을 테스트 서버로 복사해서 구성했습니다. 안에 있는 글은 몇몇 개만 옮겨서 비슷하게 만들어 쓰고 있었습니다. 나름 필수 요소인 글과 페이지는 복사해서 넣었는데도, 자꾸 복사로는 완전히 옮겨지지 않는 부분이 생겼습니다. 그 차이를 찾아야 했습니다.&lt;/p&gt;
&lt;p&gt;그러던 중 워드프레스 DB에서 몇몇 테이블과 옵션값이 눈에 들어왔습니다. 순수한 워드프레스 상태에서는 없었던 테이블과 옵션값이었죠. 플러그인이 단순히 웹 파일로만 설정값이나 로그 데이터를 가지고 있는 것이 아니라, 워드프레스 DB에 그들의 공간을 별도로 할애해서 자리를 잡고 있었습니다. 별도 테이블이 생성된 것 이외에도, 기존 &lt;code&gt;wp_options&lt;/code&gt; 테이블 안에 새 값이 저장돼 있기도 했습니다. &lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image4-1024x559.png"&gt;&lt;figcaption&gt;워드펜스 같은 몇몇 플러그인은 필요한 데이터를 DB에 저장함.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;라이브 서버와 테스트 서버의 웹 파일만 옮겨서는, DB 정합성을 맞출 수 없다는 뜻입니다. 즉, 라이브와 완전히 똑같은 환경으로 테스트하려면, 웹 파일과 DB를 한 쌍으로 만들어서 복사를 해야 한다는 의미입니다. &lt;/p&gt;
&lt;p&gt;사이트가 커지고 무거워지면 그 자체도 다시 시간이 많이 들겠지만, 어차피 롤백 대책을 위해서라도 특정 시점의 파일과 DB는 백업을 떠야 합니다. 백업 파일은 피할 수 없는 일인거죠. &lt;/p&gt;
&lt;h2&gt;백업과 복원&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;요즘 많이 쓰는 깃허브 페이지나 블로그 서비스는 SaaS 형태로 사용하므로, 백업 방식은 각 서비스에 따라 다를 것입니다. 헤드리스 CMS나 워드프레스를 웹 호스팅 등으로 가볍게 사용하는 경우에도 호스팅 서비스 제공자에 맞춘 가이드가 따로 있으리라 생각합니다. 만약, K8s 환경에 워드프레스를 올렸거나 따로 CI/CD를 세팅한 경우에도 그 패턴에 맞춰서 웹과 DB를 한 쌍으로 만들 방법을 찾으셔야 합니다. 저는 단순한 VM에 IaaS 형태로 구성돼 있으므로, 이 기준에 따라 해소 방안을 찾으려 했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;인프라 관점에서 스냅샷을 뜨는 형태로 백업하는 방법은 어떨지 인프라운영팀 김태훈님께 문의를 했습니다. &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;조:&lt;/strong&gt; 김태훈님, VM에 있는 웹 파일과 MariaDB를 그냥 인프라 통째로 스냅샷을 뜨면, 테스트 서버로 복사하고 필요할 때 롤백하면서 쓸 수 있을까요?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김:&lt;/strong&gt; 스냅샷은 효율적인 백업과 롤백 방법은 될 수 있어요. 그런데, 복제한 테스트 서버 속에 있는 파일이나 데이터를 따로 찾아보려면, 스냅샷 시점마다 VM 자체를 늘려야 할 수도 있어요. OS 구조에 대한 제약, 메모리에 있는 데이터의 보장 방안까지 챙기는 견고한 백업 대책이 될 순 없거든요. 파일이나 데이터 보관 목적으로 스냅샷을 쓰기엔 부담이 좀 있어 보이네요. 만약 소스코드 자체가 깃(Git) 같은 곳에서 형상 관리가 되고 있다면, 차라리 그걸로 관리하는 게 좋아요. 스냅샷 시점마다 VM을 늘려가면서 쓸 정도로 크게 일 벌이려는 건 아니죠?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;조:&lt;/strong&gt; 그러게요. 파일이나 데이터를 시점별로 능동적으로 찾을 편의를 생각하면 스냅샷은 오버 엔지니어링이 될 수도 있겠네요. 그러면 따로 파이썬 같은 걸 써서 프로그래밍해야 하는 거면, ChatGPT 도움을 받아도 제 손에서 끝내기는 쉽지 않을 느낌인데요. 제일 간단한 대안은 뭐가 있을까요?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김:&lt;/strong&gt; 마침 사용하고 계신 VM이 전부다 리눅스니까, 셸 스크립트로 해봐요. 조건별 상황이 많은 것도 아니고, 단순 선형 실행만 하면 될 정도니까 셸 스크립트면 충분할 거예요. 이미 셸 스크립트로 짜면 될 내용을 조병승님이 매뉴얼로 만들어서 손으로 복붙해서 쓰고 계시잖아요. 그거 손으로 넣는 명령어를 다 한 통에 넣어버리면 돼요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;조:&lt;/strong&gt; 오, 이걸 한 통에… 한번 도전해 볼게요.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;워드프레스 플러그인 중에는 백업 관련 플러그인도 여럿 있습니다. 다만, 원하는 기능을 갖춘 플러그인이 맞는지 하나하나 테스트하가 쉽지 않았습니다. 시간도 시간이지만, 애초에 플러그인 자체도 백업에 포함되는 항목인데, 그걸 플러그인으로 쓴다는 자체가 뭔가 이상하단 느낌도 들었고, 제가 별도로 커스텀 세팅해 둔 값까지 완벽히 다 되는지 확인하기가 매우 번거로웠습니다. 또한 백업 파일을 보관하는 스토리지가 대부분 외부 클라우드 서비스 위주였던터라, 사내 인프라에서 동작하는 파일을 외부로 올리는 부담도 컸습니다.&lt;/p&gt;
&lt;p&gt;그래서 김태훈님의 조언대로, 셸 스크립트를 사용해 백업과 복원 스크립트 파일을 만들기로 했습니다. 마침, 평소에 손으로 실행하던 명령어를 매뉴얼로 잘 정리해둔 문서도 있었기에 맨바닥에서 시작하는 것도 아니었으니까요.&lt;/p&gt;
&lt;h3&gt;백업&lt;/h3&gt;
&lt;p&gt;백업 자체는 간단합니다. 먼저, DB나 웹 업데이트가 발생하지 않는 시간을 정합니다. 그리고 MariaDB는 덤프 파일로, 워드프레스 웹 파일은 압축 파일로 만듭니다. 그리고 어딘가에 생성한 저장용 서버로 두 파일을 복사합니다.&lt;/p&gt;
&lt;p&gt;먼저 백업을 위해 디렉토리나 DB 정보를 변수로 설정합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 백업 디렉토리 설정
backup_dir="/backup"

# 워드프레스 설치 디렉토리 설정
wordpress_dir="/wordpress"

# 백업 파일 이름 생성
wordpress_file="wordpress_$(date +%Y%m%d_%H%M%S).tar.gz"
wordpress_db="wordpress_DB_$(date +%Y%m%d_%H%M%S).sql"

# 데이터베이스 정보 설정
db_host="db_host"
db_name="wordpress DB name"
db_user="DB account"
db_password="DB password"

# 백업 파일을 저장할 서버 정보
backup_server_host="server host"
backup_server_user="user account"
backup_server_dir="/backup"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 워드프레스 웹 파일 압축 함수와 DB 백업 함수를 만듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;backup_web() {
sudo tar -zcf "$backup_dir/$wordpress_file" "$wordpress_dir/"
}

backup_db() {
mysqldump --single-transaction -h "$db_host" -u "$db_user" -p"$db_password" "$db_name" &amp;gt; "$backup_dir"/"$wordpress_db"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;압축한 파일과 백업한 파일을 복사할 함수를 만듭니다. 저는 서버에서 서버로 복사하는 명령어로 &lt;code&gt;scp&lt;/code&gt;를 용했습니다만, 저장 공간이 깃(Git)이라거나 다른 스토리지를 사용하신다면 그에 맞는 복사 명령어를 사용하시면 됩니다. 혹시나 &lt;code&gt;scp&lt;/code&gt;로 복사할 때 네트워크 대역폭을 혼자 다 점유하지 않도록 &lt;code&gt;-l&lt;/code&gt; 옵션으로 제한을 걸었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;backup_web_copy() {
scp -l 200000 "$backup_dir/$wordpress_file" "$backup_server_user"@"$backup_server_host":"$backup_server_dir"
}

backup_db_copy() {
scp -l 200000 "$backup_dir/$wordpress_db" "$backup_server_user"@"$backup_server_host":"$backup_server_dir"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 여기까지 만든 함수를 순서대로 실행할 함수를 만들고 호출합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;run_backup() {
backup_web
backup_db
backup_web_copy
backup_db_copy
}

run_backup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;최초 실행해 보고 나니, 단계별까진 아니더라도 전체 실행을 위해 소요된 시간을 추후에 확인할 수 있도록 최소한의 기록을 남기고 싶었습니다. 스크립트를 시작하자마자, 스크립트를 시작한 시각을 남기고 각 함수 실행이 끝난 자리마다 추가했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 로그 설정
logfile="/backup_log_$(date +%Y%m%d_%H%M%S).log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" &amp;gt;&amp;gt; "$logfile"
}

# 시작
log "백업을 시작합니다."

# backup_web()
log "웹 파일 백업을 완료했습니다."

# backup_db()
log "데이터베이스 백업을 완료했습니다."

# backup_web_copy()
log "웹 파일 복사를 완료했습니다."

# backup_db_copy()
log "데이터베이스 파일 복사를 완료했습니다."
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;로그 파일을 열어보면, 대략 소요 시간을 볼 수 있습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image1-1024x253.png"&gt;&lt;figcaption&gt;3분 21초 정도 걸린 넷마블 기술 블로그 백업&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;백업을 하면서 백업 파일 자체를 다른 서버로 옮겼으므로, 계속 이 서버에 백업 파일을 남겨둘 필요는 없습니다. (계속 쌓으면 서버 저장공간이 금방 부족해집니다.) 생성한 파일명 자체에 시간 값을 붙여놨기에, 파일명을 기준으로 60일이 지난 파일은 지우는 스크립트도 같이 붙였습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 60일이 지난 백업 파일 정리
sudo find "$backup_dir" -name "wordpress_*" -mtime +60 -exec rm {} \;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 대충 백업 쪽 정리가 끝났습니다. 더 정교하거나 다듬어야 할 부분은 떠오를 때마다 각 단계별로 함수를 나눴으므로, 단계에 맞춰서 수정 또는 추가하면 됩니다.&lt;/p&gt;
&lt;h3&gt;복원&lt;/h3&gt;
&lt;p&gt;다음은 복원입니다. 백업과 달리, 복원은 조금 고민이 필요합니다. 크게는 롤백을 위한 복원인지, 테스트 서버 구성을 위한 복원인지로 나눠지겠네요. 당장 이 둘은 서로 사용할 도메인이 다르기 때문에, 도메인 설정이 바뀌는 것까지 맞춰서 스크립트에 넣어야 합니다.&lt;/p&gt;
&lt;p&gt;저는 테스트 서버를 구성하는 기준으로 복원 스크립트를 만들었습니다. 우선 저장된 백업 파일 중에 가장 최근 파일을 인식하는 것부터 시작해야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ls 명령으로 최신 파일을 찾아서 변수에 저장
sqlfile=$(ls -t /tmp *.sql | head -n 1)
targzfile=$(ls -t /tmp *.tar.gz | head -n 1)
sqlname=$(basename "$sqlfile" | sed 's/\..*//')
targzname=$(basename "$targzfile" | sed 's/\..*//')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이미 테스트 서버에는 지금 쓰고 있는 파일이 있습니다. 웹 서버 설정이 이 디렉터리를 가리키고 있기도 하니, 복원할 파일이 이 디렉터리에 오도록 기존 디렉터리의 이름을 바꾸기로 했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 기존 디렉터리 이름 변경
today=$(date +%Y%m%d)
sudo mv /wordpress /wordpress_$today
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 백업한 워드프레스 웹 파일을 그대로 해제하면 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tar -zxvf /backup$targzfile -C /.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DB 백업 파일은 밀어 넣으려면, 빈 DB가 먼저 생성돼 있어야 합니다. 만약 테스트 서버에 이미 DB가 있다면, 밀어 넣는 DB를 이름이 다른 DB에 넣어야 데이터가 꼬이지 않습니다. 저는 맨 처음 최신 파일을 변수에 저장할 때 그 파일 이름 자체를 DB명으로 쓰기로 했으니, DB 생성할 때부터 그 변수를 호출해서 사용하도록 했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# DB 생성
sudo mysql -u account -ppassword -e "CREATE DATABASE $sqlname"

# DB 밀어넣기
sudo mysql -u account -ppassword $sqlname &amp;lt; /tmp/$sqlfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DB를 다 밀어 넣었으니, 테스트 서버로 쓰는 도메인으로 DB안에 있는 값을 바꿔야 합니다. 대략, &lt;code&gt;wp_posts&lt;/code&gt;, &lt;code&gt;wp_postmeta&lt;/code&gt;, &lt;code&gt;wp_options&lt;/code&gt; 테이블에 있는 도메인만 치환하면 99%가 커버됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# DB안에서 도메인값 변경
sudo mysql -u account -ppassword -e "update $sqlname.wp_posts set post_content = replace(post_content, 'netmarble.engineering', 'test.test') where post_content like '%netmarble.engineering%'; update $sqlname.wp_posts set guid = replace(guid, 'netmarble.engineering', 'test.test') where guid like '%netmarble.engineering%'; update $sqlname.wp_postmeta set meta_value = replace(meta_value, 'netmarble.engineering', 'test.test') where meta_value like '%netmarble.engineering%'; UPDATE $sqlname.wp_options SET option_value='https://test.test' WHERE option_name='siteurl'; UPDATE $sqlname.wp_options SET option_value='https://test.test' WHERE option_name='home';"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 워드프레스 설정 파일에서 도메인과 DB 정보를 변경하면 끝납니다. 워드프레스 설정 파일은 각자마다 사용하는 규칙이나 선언자가 다를 수 있으니, 본인 설정에 맞게 바꾸셔야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sed -i "s|define('WP_HOME','https://netmarble.engineering');|define('WP_HOME','https://test.test');|g" /wordpress/wp-config.php
sed -i "s|define('WP_SITEURL','https://netmarble.engineering');|define('WP_SITEURL','https://test.test');|g" /wordpress/wp-config.php
sed -i "s|define( 'DB_NAME', 'wordpress_db' );|define( 'DB_NAME', '$sqlname' );|g" /wordpress/wp-config.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;백업 때와 마찬가지로, 복원에 소요되는 시간이 궁금하다면 단계별로 시간 기록을 남기면 됩니다. 복원에는 특별히 시간 기록을 남기지 않았는데, 막상 지금 이 글을 쓰고 보니 복원은 시간이 얼마나 걸리는 지 궁금하긴 하네요.&lt;/p&gt;
&lt;h3&gt;Cron&lt;/h3&gt;
&lt;p&gt;백업과 복원 스크립트를 모두 완성했습니다. 이제 스케줄러에 넣어서 돌리면, 저는 테스트 서버에서 주기적으로 라이브와 동일한 상황을 볼 수 있습니다. 사실상 라이브와 똑같은 샌드박스가 생긴 셈이죠.&lt;/p&gt;
&lt;p&gt;요즘 스케줄러 쓴다고 하면 &lt;code&gt;Airflow&lt;/code&gt;부터 떠올리시는 분이 더 많은 것 같습니다. 당장은 &lt;code&gt;Crontab&lt;/code&gt;만으로도 충분해 보여서 매주 토요일 자정마다 한 번씩 돌도록 스케줄링을 걸어놨습니다. 넷마블 기술 블로그는 백업과 복원 과정이 대략 10분 안에 끝나는데, 개발자 사이트는 약 1시간 정도 걸리더군요. &lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image2-1024x752.png"&gt;&lt;figcaption&gt;들쭉날쭉하던 디렉터리와 DB가 가지런히 정리됨&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2&gt;남은 과제&lt;/h2&gt;
&lt;p&gt;위 백업과 복원은 사실상, 도메인만 바꾸고 완전히 복제를 뜬 정도라 &lt;code&gt;wp-content&lt;/code&gt; 디렉터리에 있는 미디어 파일을 재생성하지 않아도 모든 섬네일까지 완벽히 연결됩니다. 특히 파일 베이스로 움직이기 때문에, 구글 드라이브 등의 스토리지를 활용하는 방식으로도 연계할 여지가 많습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://netmarble.engineering/wp-content/uploads/2023/12/image3-1024x650.png"&gt;&lt;figcaption&gt;워드프레스 백업과 복원 방향&lt;/figcaption&gt;&lt;/figure&gt;
&lt;blockquote&gt;
&lt;p&gt;백업과 복원을 찾을 때 도커로 워드프레스 사이트를 구성해서 올렸다 내리는 분들도 보긴 했지만, 워드프레스 사이트를 CI/CD로 완벽히 구성하고 업데이트 반영하면서 쓰는 사례는 거의 못 본 것 같습니다. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이제 1년에 4번 정도 마주치는 워드프레스 업데이트 테스트 상황에 조금 더 편하게 대응하고  있습니다. 다만, 편하게 만나는 환경에 직면한 작업자는 저를 포함해 극소수밖에 되지 않아, 단순히 시간 효용을 계산하자면 큰 이득을 얻진 못했다고도 볼 수도 있습니다. 그래도 업데이트 테스트를 빨리 마치고, 안정적인 서비스를 더 빨리 사용자에게 제공할 수 있다는 것만으로도 충분한 이득이 아닐까 합니다. 개발자 사이트는 직접 워드프레스에 로그인해서 글을 쓰는 사용자가 꽤 많이 있습니다. 업데이트로 구텐베르크 에디터의 버그가 줄어들수록, 편의 기능이 늘어날수록, 그 이점을 함께 누릴 수 있는 사람이 많아진다는 의미도 되니까요.&lt;/p&gt;
&lt;p&gt;여기까지 글을 정리하면서 되돌아보니, 복원 스크립트는 백업 스크립트처럼 함수화하지 않고 바로 직렬 실행하도록 구성했었네요. 스크립트 함수화 또는 &lt;code&gt;Airflow&lt;/code&gt; 적용은 남은 과제 중 하나가 됐네요. (뭔가 한가는 2024년에 새 태스크로 마주칠 것 같은 기분이 듭니다.)&lt;/p&gt;
&lt;p&gt;2023년을 돌아보다가 급작스레 옆길로 샜습니다. 조만간 2023년 넷마블 기술 블로그 회고로 돌아오겠습니다.&lt;/p&gt;
&lt;/div&gt;</content:encoded></item></channel></rss>