<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Prejudice</title>
    <link>https://prejudice.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 08:40:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>편견장</managingEditor>
    <image>
      <title>Prejudice</title>
      <url>https://tistory1.daumcdn.net/tistory/5103524/attach/9b9d0df295c64cbcac050a35d22b4787</url>
      <link>https://prejudice.tistory.com</link>
    </image>
    <item>
      <title>Linux PREEMPT_RT 실시간성(Jitter) 측정 및 성능 비교 - RTOS</title>
      <link>https://prejudice.tistory.com/42</link>
      <description>&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 SW PLC에서 관심 갖고 개발 중인 내용은 Linux PREEMPT_RT 커널을 이용해 실시간성을 보장하는 작업이다.&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;SW PLC Runtime은 특정 Task를 매 주기마다 실행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;OS에서 프로세스의 실행을 보장받지 못하거나, 처리 속도가 들쭉날쭉 하지 않도록 Jitter를 관리&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;PREEMPT_RT 커널로 실시간성을 확보하고 테스트하는 일련의 과정을 정리해 보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;용어 및 개념정리 : 2026.03.19 - [개발] - Real-Time, RTOS, PREEMPT_RT, CPU Isolation 개념 정복 - 실시간 처리&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775450232349&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;Real-Time, RTOS, PREEMPT_RT, CPU Isolation 개념 정복 - 실시간 처리&quot; data-og-description=&quot;들어가며'실시간(Real-time)'이라는 용어는 Embedded 및 FactoryAutomation 산업의 핵심이며,현장에서 다양한 관점과 의미로 혼용되곤 한다.또한 자연스럽게 따라오는 '처리 속도'는 모든 SW/HW의 핵심 지표&quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/38&quot; data-og-url=&quot;https://prejudice.tistory.com/38&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/g1ntC/dJMb82eNFXn/KssX2wSqIkSzysdjSfA7Kk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/0G39g/dJMb83Sjt9G/skrxkdyLV7oZaTq3AdMTqK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cECuOp/dJMb83Sjt9F/rvaLX1XyukP1debffM0kZ0/img.png?width=1710&amp;amp;height=884&amp;amp;face=0_0_1710_884&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/38&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/g1ntC/dJMb82eNFXn/KssX2wSqIkSzysdjSfA7Kk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/0G39g/dJMb83Sjt9G/skrxkdyLV7oZaTq3AdMTqK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cECuOp/dJMb83Sjt9F/rvaLX1XyukP1debffM0kZ0/img.png?width=1710&amp;amp;height=884&amp;amp;face=0_0_1710_884');&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;Real-Time, RTOS, PREEMPT_RT, CPU Isolation 개념 정복 - 실시간 처리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며'실시간(Real-time)'이라는 용어는 Embedded 및 FactoryAutomation 산업의 핵심이며,현장에서 다양한 관점과 의미로 혼용되곤 한다.또한 자연스럽게 따라오는 '처리 속도'는 모든 SW/HW의 핵심 지표&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 시나리오 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;㎱(나노초), ㎲(마이크로초) 단위로 처리하는 Real-Time을 제대로 구현하고 평가하기 위해 테스트 설계 과정이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 커널단에서 처리하는 과정은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p55Tv/dJMb990gwQN/MkNn5E0pNEl0E2oHvL9KU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p55Tv/dJMb990gwQN/MkNn5E0pNEl0E2oHvL9KU0/img.png&quot; data-alt=&quot;Linux 커널 처리 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p55Tv/dJMb990gwQN/MkNn5E0pNEl0E2oHvL9KU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp55Tv%2FdJMb990gwQN%2FMkNn5E0pNEl0E2oHvL9KU0%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;612&quot; height=&quot;90&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Linux 커널 처리 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHZYRA/dJMcahcS9AS/h9sUfqGklY1WCKxdvFvmBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHZYRA/dJMcahcS9AS/h9sUfqGklY1WCKxdvFvmBK/img.png&quot; data-alt=&quot;PREEMPT_RT 커널 처리 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHZYRA/dJMcahcS9AS/h9sUfqGklY1WCKxdvFvmBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHZYRA%2FdJMcahcS9AS%2Fh9sUfqGklY1WCKxdvFvmBK%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;602&quot; height=&quot;90&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PREEMPT_RT 커널 처리 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;PREEMPT_RT Kernel의 Inturrupt 처리 타임라인&quot; data-phocus=&quot;https://blog.kakaocdn.net/dna/q1HCE/dJMcag5Tluo/AAAAAAAAAAAAAAAAAAAAAAMcPDZoMFb_JstXaTR1iyLYQFk_O_2s40hsKO3R6TMc/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=AzniDFR5x1gCA8rHcxdQ0Zq7mhs%3D&quot; data-url=&quot;https://blog.kakaocdn.net/dna/q1HCE/dJMcag5Tluo/AAAAAAAAAAAAAAAAAAAAAAMcPDZoMFb_JstXaTR1iyLYQFk_O_2s40hsKO3R6TMc/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=AzniDFR5x1gCA8rHcxdQ0Zq7mhs%3D&quot;&gt;&lt;/span&gt;테스트 목표는 &lt;b&gt;Interrupt가 발생한 시점부터 Test Program(=TaskB) 이 호출될 때까지의 Jitter를 측정하고 비교하는 것이다.&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;HW&lt;/b&gt; &lt;b&gt;: &lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Linux PREEMPT_RT 커널이 올라간 BoxPC&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;측정 방법 :&lt;/b&gt; TaskB를 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;10㎳ 주기로 &lt;/span&gt;10,000회 반복 호출하며 깨어날 때의 시간을 정밀 측정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;측정 환경 :&lt;/b&gt; ① stress-ng 툴로 CPU와 I/O에 강제 부하 인가&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;② QTimer, non-RT Kernel, PREEMPT_RT Kernel 각각의 조건에서 실행 후 비교&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예상 결과 :&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;☐ QTimer는 내부의 QEventLoop 구조로 인해 Linux Basic Kernel 보다 Jitter가 클 것이다.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;☐ 강력한 시스템 부하로 인해 QTimer와 non-RT Kernel의 Jitter는 변동폭이 클 것이다.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;☐ 반면 PREEMPT_RT Kernel의 Jitter는 극한의 상황에서도 안정적인 수준을 유지할 것이다.&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;QTimer 프로그램 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Qt6를 설치하고 QTimer를 이용해 10㎳마다 timeout 되도록 프로그램을 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;Qt6.8에 도입된 &lt;/b&gt;Qt::PreciseTimer&lt;b&gt; 를 사용&lt;/b&gt;해 Jitter 특성을 최소화했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(*Precise Timer : &lt;a href=&quot;https://doc.qt.io/qt-6/ko/qtimer.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://doc.qt.io/qt-6/ko/qtimer.html&lt;/a&gt;)&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1775453547092&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;QCoreApplication&amp;gt;
#include &amp;lt;QFile&amp;gt;
#include &amp;lt;QTextStream&amp;gt;
#include &amp;lt;QTimer&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

struct LogEntry {
    long long tick;
    double actualRunTimeMs;
    double predictMs;
    double gitterMs;
};

int main(int argc, char* argv[]) {
    QCoreApplication app(argc, argv);

    constexpr double periodMs = 10.0;
    constexpr long long totalTicks = 10000;

    // 로그 버퍼 사전 할당
    std::vector&amp;lt;LogEntry&amp;gt; logBuffer;
    logBuffer.reserve(totalTicks);

    long long tick = 0;
    const auto startTime = std::chrono::steady_clock::now();

    QTimer timer;
    timer.setTimerType(Qt::PreciseTimer);
    timer.setInterval(static_cast&amp;lt;int&amp;gt;(periodMs));

    QObject::connect(&amp;amp;timer, &amp;amp;QTimer::timeout, [&amp;amp;]() {
        ++tick;

        const auto now = std::chrono::steady_clock::now();
        const std::chrono::duration&amp;lt;double, std::milli&amp;gt; elapsed = now - startTime;

        const double actualRunTimeMs = elapsed.count();
        const double predictMs = tick * periodMs;
        const double gitterMs = actualRunTimeMs - predictMs;

        // I/O 없이 메모리 버퍼에만 기록
        logBuffer.push_back({tick, actualRunTimeMs, predictMs, gitterMs});

        if (tick &amp;gt;= totalTicks) {
            timer.stop();
            app.quit();
        }
    });

    timer.start();
    std::cout &amp;lt;&amp;lt; &quot;Started 10ms periodic timer. Logging to timer_log.csv&quot; &amp;lt;&amp;lt; std::endl;

    app.exec();

    // 타이머 루프 종료 후 한 번에 파일 기록
    std::cout &amp;lt;&amp;lt; &quot;Timer stopped after &quot; &amp;lt;&amp;lt; tick &amp;lt;&amp;lt; &quot; ticks. Writing log...&quot; &amp;lt;&amp;lt; std::endl;

    QFile logFile(&quot;timer_log.csv&quot;);
    if (!logFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
        std::cerr &amp;lt;&amp;lt; &quot;Failed to open timer_log.csv&quot; &amp;lt;&amp;lt; std::endl;
        return 1;
    }

    QTextStream stream(&amp;amp;logFile);
    stream &amp;lt;&amp;lt; &quot;index,actual_run_time_ms,predict_ms,gitter_ms\n&quot;;
    for (const auto&amp;amp; e : logBuffer) {
        stream &amp;lt;&amp;lt; e.tick &amp;lt;&amp;lt; &quot;,&quot;
               &amp;lt;&amp;lt; QString::number(e.actualRunTimeMs, 'f', 3) &amp;lt;&amp;lt; &quot;,&quot;
               &amp;lt;&amp;lt; QString::number(e.predictMs, 'f', 3) &amp;lt;&amp;lt; &quot;,&quot;
               &amp;lt;&amp;lt; QString::number(e.gitterMs, 'f', 3) &amp;lt;&amp;lt; &quot;\n&quot;;
    }

    std::cout &amp;lt;&amp;lt; &quot;Log written to timer_log.csv (&quot; &amp;lt;&amp;lt; logBuffer.size() &amp;lt;&amp;lt; &quot; entries).&quot; &amp;lt;&amp;lt; std::endl;
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;PREEMPT_RT 프로그램 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PREEMPT_RT 프로그램도 QTimer와 동일하게,&lt;br /&gt;&lt;b&gt;10㎳ 마다 RT-kernel의 시스템 타이머 인터럽트를 받아 task가 깨어나도록 구현&lt;/b&gt;&lt;span style=&quot;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;프로그램의 main 함수 내 while문에서 &lt;span class=&quot;inline-em&quot;&gt;clock_nanosleep&lt;/span&gt; 함수에서 OS호출을 대기하며 블록킹 되는데,&lt;br /&gt;다음과 같은 상황에서는 실시간성을 보장받지 못하고 &lt;b&gt;일반 프로세서와 동일하게 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Linux kernel이 PREEMPT_RT 커널이 아닌 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 시 관리자(root) 권한으로 실행되지 않은 경우&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 조건을 역이용하여 동일하게 빌드된 단일 프로그램을 통해 non-RT 커널과 PREEMPT-RT 커널 테스트를 모두 진행했다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1775453950641&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;cerrno&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &amp;lt;iomanip&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;sched.h&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;sys/mman.h&amp;gt;
#include &amp;lt;time.h&amp;gt;
#include &amp;lt;vector&amp;gt;

namespace {

timespec addMs(const timespec&amp;amp; t, long ms) {
    timespec out = t;
    out.tv_sec += ms / 1000;
    out.tv_nsec += (ms % 1000) * 1000000L;
    if (out.tv_nsec &amp;gt;= 1000000000L) {
        out.tv_sec += 1;
        out.tv_nsec -= 1000000000L;
    }
    return out;
}

double diffMs(const timespec&amp;amp; a, const timespec&amp;amp; b) {
    const long sec = a.tv_sec - b.tv_sec;
    const long nsec = a.tv_nsec - b.tv_nsec;
    return static_cast&amp;lt;double&amp;gt;(sec) * 1000.0 + static_cast&amp;lt;double&amp;gt;(nsec) / 1000000.0;
}

struct LogEntry {
    long long tick;
    double actualRunTimeMs;
    double predictMs;
    double gitterMs;
};

}  // namespace

int main(int argc, char* argv[]) {
    constexpr long periodMs = 10;
    constexpr long long totalTicks = 10000;
    int rtPriority = 80;

    if (argc &amp;gt;= 2) {
        rtPriority = std::stoi(argv[1]);
    }

    // 메모리 페이지 폴트 방지: 모든 메모리를 RAM에 고정
    if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
        std::cerr &amp;lt;&amp;lt; &quot;Warning: mlockall failed (&quot; &amp;lt;&amp;lt; std::strerror(errno)
                  &amp;lt;&amp;lt; &quot;). Page faults may cause jitter.&quot; &amp;lt;&amp;lt; std::endl;
    } else {
        std::cout &amp;lt;&amp;lt; &quot;mlockall: memory locked.&quot; &amp;lt;&amp;lt; std::endl;
    }

    // 로그 버퍼 사전 할당 (루프 전에 메모리 확보)
    std::vector&amp;lt;LogEntry&amp;gt; logBuffer;
    logBuffer.reserve(totalTicks);

    sched_param schParam{};
    schParam.sched_priority = rtPriority;

    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &amp;amp;schParam) != 0) {
        std::cerr &amp;lt;&amp;lt; &quot;Warning: failed to set SCHED_FIFO priority=&quot; &amp;lt;&amp;lt; rtPriority
                  &amp;lt;&amp;lt; &quot; (&quot; &amp;lt;&amp;lt; std::strerror(errno)
                  &amp;lt;&amp;lt; &quot;). Continue with current scheduler.&quot; &amp;lt;&amp;lt; std::endl;
    } else {
        std::cout &amp;lt;&amp;lt; &quot;SCHED_FIFO enabled with priority=&quot; &amp;lt;&amp;lt; rtPriority &amp;lt;&amp;lt; std::endl;
    }

    timespec startTs{};
    if (clock_gettime(CLOCK_MONOTONIC, &amp;amp;startTs) != 0) {
        std::cerr &amp;lt;&amp;lt; &quot;clock_gettime failed&quot; &amp;lt;&amp;lt; std::endl;
        return 1;
    }

    std::cout &amp;lt;&amp;lt; &quot;Started RT periodic task at 10ms. Logging to rt_task_log.csv&quot; &amp;lt;&amp;lt; std::endl;

    long long tick = 0;

    // RT 루프: I/O 없이 메모리 버퍼에만 기록
    while (tick &amp;lt; totalTicks) {
        ++tick;

        const timespec targetTs = addMs(startTs, static_cast&amp;lt;long&amp;gt;(tick * periodMs));

        const int sleepRet = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &amp;amp;targetTs, nullptr);
        if (sleepRet != 0 &amp;amp;&amp;amp; sleepRet != EINTR) {
            std::cerr &amp;lt;&amp;lt; &quot;clock_nanosleep failed: &quot; &amp;lt;&amp;lt; std::strerror(sleepRet) &amp;lt;&amp;lt; std::endl;
            break;
        }

        timespec now{};
        clock_gettime(CLOCK_MONOTONIC, &amp;amp;now);

        logBuffer.push_back({
            tick,
            diffMs(now, startTs),
            static_cast&amp;lt;double&amp;gt;(tick * periodMs),
            diffMs(now, addMs(startTs, static_cast&amp;lt;long&amp;gt;(tick * periodMs)))
        });
    }

    // RT 루프 종료 후 한 번에 파일 기록
    std::cout &amp;lt;&amp;lt; &quot;task stopped after &quot; &amp;lt;&amp;lt; tick &amp;lt;&amp;lt; &quot; ticks. Writing log...&quot; &amp;lt;&amp;lt; std::endl;

    std::ofstream logFile(&quot;rt_task_log.csv&quot;, std::ios::trunc);
    if (!logFile.is_open()) {
        std::cerr &amp;lt;&amp;lt; &quot;Failed to open rt_task_log.csv&quot; &amp;lt;&amp;lt; std::endl;
        return 1;
    }

    logFile &amp;lt;&amp;lt; std::fixed &amp;lt;&amp;lt; std::setprecision(3);
    logFile &amp;lt;&amp;lt; &quot;index,actual_run_time_ms,predict_ms,gitter_ms\n&quot;;

    for (const auto&amp;amp; e : logBuffer) {
        logFile &amp;lt;&amp;lt; e.tick &amp;lt;&amp;lt; &quot;,&quot;
                &amp;lt;&amp;lt; e.actualRunTimeMs &amp;lt;&amp;lt; &quot;,&quot;
                &amp;lt;&amp;lt; e.predictMs &amp;lt;&amp;lt; &quot;,&quot;
                &amp;lt;&amp;lt; e.gitterMs &amp;lt;&amp;lt; &quot;\n&quot;;
    }

    std::cout &amp;lt;&amp;lt; &quot;Log written to rt_task_log.csv (&quot; &amp;lt;&amp;lt; logBuffer.size() &amp;lt;&amp;lt; &quot; entries).&quot; &amp;lt;&amp;lt; std::endl;
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;⚠️트러블 슈팅 : 로깅 방식으로 인한 지연&lt;br /&gt;&lt;/b&gt;위 예제 코드를 보면 vector::logBuffer에 결과를 담아두었다가 측정(10,000회)이 완전히 종료된 후 일관적으로 파일에 쓰도록 구현했다.&lt;br /&gt;Real-Time 프로그램 원칙 중 &lt;b&gt;disk I/O 작업을 지양해야 한다&lt;/b&gt;는 룰이 있다.&lt;br /&gt;㎛ 수준으로 즉각 동작해야 하는 시스템 특성상, 순간적인 파일 입출력 병목이 발생하면 수 ㎳ 이상의 스레드 지연을 초래할 수 있기 때문이다.&lt;br /&gt;&lt;br /&gt;초기에 측정 데이터를 실시간으로 보겠다는 마음으로 &lt;b&gt;while 반복문 안에서 매 틱마다 로깅을 수행했다가 지연이 발생했고&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;마치 Jitter가 발생한 것처럼 보이는 착시를 겪은 후 메모리 버퍼 방식으로 변경했다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;성능 비교 환경 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 BoxPC에 &lt;b&gt;PREEMPT_RT Linux kernel을 활성화&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux 6.12 버전부터는 PREEMPT_RT가 공식 커널에 합쳐졌기 때문에, 최신 배포판을 사용 중이라면 쉽게 활성화할 수 있다.&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;다만 내가 사용한 Ubuntu 24.04의 커널은 6.8 버전으로 공식적으로 통합되기 이전의 빌드였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 커널 소스와 해당하는 버전의 RT 패치를 다운로드한 뒤 직접 빌드하여 커널을 올려주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;테스트를 위한 전체적인 OS 환경 세팅은 AI 도구들을 사용하면 훨씬 수월하게 구축할 수 있다.&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;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;span class=&quot;inline-em&quot;&gt;stress-ng&lt;/span&gt;&lt;b&gt;를 이용해&lt;/b&gt; 시스템 자원 전체에 고의적인 부하를 걸고 측정했다.&lt;br /&gt;&lt;b&gt;Jitter가 튀기 가장 좋은 가혹 환경&lt;/b&gt;을 만들기 위해 아래 명령을 사용했다.&lt;/p&gt;
&lt;div style=&quot;margin: 30px 0; background: #1e1e1e; color: #00ff88; padding: 20px; border-radius: 10px; font-family: Consolas, monospace;&quot;&gt;
&lt;div style=&quot;white-space: pre-wrap;&quot;&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;stress-ng&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;--cpu&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b5cea8;&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;--vm&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b5cea8;&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;--vm-bytes&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;1G&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;--io&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b5cea8;&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;--timer&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b5cea8;&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;--switch&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b5cea8;&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;--timeout&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;120s&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b4b4b4;&quot;&gt;&amp;amp;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.7676%; height: 101px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.2541%; height: 21px;&quot;&gt;옵션&lt;/td&gt;
&lt;td style=&quot;width: 26.4849%; height: 21px;&quot;&gt;효과&lt;/td&gt;
&lt;td style=&quot;width: 78.1537%; height: 21px;&quot;&gt;&lt;span style=&quot;background-color: #008300; color: #ffffff; text-align: start;&quot;&gt;RT jitter 유발 메커니즘&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.2541%; height: 21px;&quot;&gt;--cpu 0&lt;/td&gt;
&lt;td style=&quot;width: 26.4849%; height: 21px;&quot;&gt;모든 코어 CPU 100% 연산&lt;/td&gt;
&lt;td style=&quot;width: 78.1537%; height: 21px;&quot;&gt;☑ 컨텍스트 스위치 유발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.2541%; height: 21px;&quot;&gt;--vm 4 --vm-bytes 1G&lt;/td&gt;
&lt;td style=&quot;width: 26.4849%; height: 21px;&quot;&gt;메모리 할당/해제 반복&lt;/td&gt;
&lt;td style=&quot;width: 78.1537%; height: 21px;&quot;&gt;☑ 페이지 폴트, TLB flush 유발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 23.2541%; height: 21px;&quot;&gt;--io 4&lt;/td&gt;
&lt;td style=&quot;width: 26.4849%; height: 21px;&quot;&gt;디스크 read/write 반복&lt;/td&gt;
&lt;td style=&quot;width: 78.1537%; height: 21px;&quot;&gt;☑ I/O IRQ 유발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.2541%; height: 17px;&quot;&gt;--timer 4&lt;/td&gt;
&lt;td style=&quot;width: 26.4849%; height: 17px;&quot;&gt;타이머 인터럽트 생성&lt;/td&gt;
&lt;td style=&quot;width: 78.1537%; height: 17px;&quot;&gt;☑ 하드웨어 타이머 ㅁ인터럽트 경합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2541%;&quot;&gt;--switch 4&lt;/td&gt;
&lt;td style=&quot;width: 26.4849%;&quot;&gt;컨텍스트 스위치 폭탄&lt;/td&gt;
&lt;td style=&quot;width: 78.1537%;&quot;&gt;☑ 프로세스 스케줄링 간섭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2541%;&quot;&gt;--timeout 120s&lt;/td&gt;
&lt;td style=&quot;width: 26.4849%;&quot;&gt;120초간 지속&lt;/td&gt;
&lt;td style=&quot;width: 78.1537%;&quot;&gt;☑ 테스트 기간(약 100초 = 10㎳ * 10,000) 동안 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째,&lt;/b&gt; CPU 동적 클럭 제어 비활성화 PREEMPT_RT에서 clock_nanosleep함수로 깨어나는 순간,&lt;br /&gt;절전 모드 등 cpu 코어의 동작 클럭이 낮아져 있다면 다시 연산 클럭을 끌어올리는 과정에서 미세한 지연 시간(Latency)이 발생할 수 있다. 따라서 CPU Governor 설정을 &lt;b&gt;performance 모드로 변경해, 항상 최대 클럭이 유지되도록 강제했다&lt;/b&gt;.&lt;/p&gt;
&lt;div style=&quot;margin: 30px 0; background: #1e1e1e; color: #00ff88; padding: 20px; border-radius: 10px; font-family: Consolas, monospace;&quot;&gt;
&lt;div style=&quot;color: #ffffff; font-weight: bold; margin-bottom: 10px;&quot;&gt;# CPU Performance 모드로 변경&lt;/div&gt;
&lt;div style=&quot;white-space: pre-wrap;&quot;&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;performance&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b4b4b4;&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;tee&lt;/span&gt;&lt;span style=&quot;color: #dadada;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;/sys/devices/system/cpu/cpu&lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;/cpufreq/scaling_governor&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;QTimer&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;vs&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;PREEMPT_RT 성능 비교&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;☑ QTimer는&amp;nbsp;내부의&amp;nbsp;QEventLoop&amp;nbsp;구조로&amp;nbsp;인해&amp;nbsp;Linux&amp;nbsp;Basic&amp;nbsp;Kernel&amp;nbsp;보다&amp;nbsp;Jitter가&amp;nbsp;클&amp;nbsp;것이다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1773&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKkTIn/dJMcaarjBBV/QmFBTKK1Sjf2QL5Cff5BBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKkTIn/dJMcaarjBBV/QmFBTKK1Sjf2QL5Cff5BBk/img.png&quot; data-alt=&quot;QTimer 3회 측정 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKkTIn/dJMcaarjBBV/QmFBTKK1Sjf2QL5Cff5BBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKkTIn%2FdJMcaarjBBV%2FQmFBTKK1Sjf2QL5Cff5BBk%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;1773&quot; height=&quot;452&quot; data-origin-width=&quot;1773&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;QTimer 3회 측정 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xg6d2/dJMcabKx7io/rxRHH9IV9EfLpmKTtwTsp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xg6d2/dJMcabKx7io/rxRHH9IV9EfLpmKTtwTsp0/img.png&quot; data-alt=&quot;Linux non-RT kernel 3회 측정 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xg6d2/dJMcabKx7io/rxRHH9IV9EfLpmKTtwTsp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXg6d2%2FdJMcabKx7io%2FrxRHH9IV9EfLpmKTtwTsp0%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;1772&quot; height=&quot;451&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Linux non-RT kernel 3회 측정 결과&lt;/figcaption&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;이상치에 해당하는 Jitter를 제외(상하위 10%를 제외)한 평균시간에서도 약 50배 가까이 차이가 확인되었다.&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;QTimer 평균 지연&lt;/b&gt; = 0.576987&amp;nbsp; ㎳&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Linux non-RT Kernel 평균 지연&lt;/b&gt;&amp;nbsp;= 0.011389 ㎳&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;☑ CPU부하로 인해 QTimer와 non-RT Kernel의 Jitter는 들쭉날쭉할 것이다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 non-RT kernel 환경의 테스트 결과를 보면, 시스템 컨디션에 따라 Jitter가 발생하는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 측의 경우 운이 좋게도 OS 스케줄러 간섭 없이 매끄럽게 통과했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 측정 결과(3rd)에서는 최대 0.65 ms까지 값이 튀는 것을 볼 수 있었다.&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;Inturrupt 발생 시점부터 Task 실행까지 운이 좋으면 11㎲, 운이 나쁠 때는 650㎲까지 지연될 수 있음&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;☑ RT Kernel의 Jitter는 시스템 부하에서도 안정적이어야 한다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEBGSw/dJMcag58eDK/5m9F0x5XygwiEQq2OhMUhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEBGSw/dJMcag58eDK/5m9F0x5XygwiEQq2OhMUhK/img.png&quot; data-alt=&quot;Linux PREEMPT_RT Kernel 3회 측정 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEBGSw/dJMcag58eDK/5m9F0x5XygwiEQq2OhMUhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEBGSw%2FdJMcag58eDK%2F5m9F0x5XygwiEQq2OhMUhK%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;1772&quot; height=&quot;451&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Linux PREEMPT_RT Kernel 3회 측정 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글의 목적인 &lt;b&gt;PREEMPT_RT Kernel에서는 Jitter가 확실하게 잡혀 안정적으로 프로그램이 실행&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;591&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/scWvd/dJMcab4Ogob/Kie2J5QOIYAbGcoy4ruI3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/scWvd/dJMcab4Ogob/Kie2J5QOIYAbGcoy4ruI3k/img.png&quot; data-alt=&quot;Linux PREEMPT_RT kKernel 3번째 측정 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/scWvd/dJMcab4Ogob/Kie2J5QOIYAbGcoy4ruI3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FscWvd%2FdJMcab4Ogob%2FKie2J5QOIYAbGcoy4ruI3k%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;591&quot; height=&quot;451&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Linux PREEMPT_RT kKernel 3번째 측정 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;non-RT 환경에서 실행 시간의 변동성이 가장 컸던 세 번째 케이스를 PREEMPT_RT 위에서 돌렸을 때,&lt;br /&gt;&lt;b&gt;평균 11㎲ 최악의 경우 70㎲ 실행속도를 측정했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;순간적으로 650㎲ 까지 지연됐던 범용 커널에 비해 훨씬 신뢰도 높고 예측 가능한 움직임을 보여주었다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 처리 성능(Throughput)이 중요한 비전(Vision) 프로그램을 다룰 때 주로 테스트 설계를 고민해 왔었는데,&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;내심 &quot;나노초 단위까지 제어할 수 있는 결과를 볼 수 있지 않을까?&quot;라는 기대와는 달라 아쉬운 점도 있었지만,&lt;br /&gt;무거운 범용 운영체제인 Linux에서 RTOS가 아님에도 커널 패치로 이 정도의 성능을 끌어낸다는 점에서 PREEMPT_RT 기술의 우수성을 엿볼 수 있었다.&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;CPU Isloation과 IRQ가 RT-CPU를 방해하지 않도록 하는 IRQ Affinity 조정방법을 통해 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;실행시간을 좀 더 단축할 수 있겠지만,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트의 PoC 차원에서 진행된 테스트 프로그램의 결과물로는 이 정도로 만족스럽다.&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;</description>
      <category>개발</category>
      <category>jitter</category>
      <category>Linux</category>
      <category>MuLiN</category>
      <category>preempt_rt</category>
      <category>Real-time</category>
      <category>RTOS</category>
      <category>SW_PLC</category>
      <category>실시간운영체제</category>
      <category>지터</category>
      <category>트러블슈팅</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/42</guid>
      <comments>https://prejudice.tistory.com/42#entry42comment</comments>
      <pubDate>Mon, 6 Apr 2026 21:35:34 +0900</pubDate>
    </item>
    <item>
      <title>Windows 한글 계정명에서 발생하는 CMake 빌드 에러 원인과 해결 방법</title>
      <link>https://prejudice.tistory.com/41</link>
      <description>&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 CMake를 C++ 프로젝트를 빌드하면서 &lt;b&gt;특정 Windows PC에서만 빌드 에러가 발생&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;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 빌드 에러는 다음과 같은 특정 환경에서만 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OS:&lt;/b&gt; Windos&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Windows 사용자 계정:&lt;/b&gt; 한글 이름 사용 (예: C:\Users\전인학\...)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OS 설정:&lt;/b&gt; &lt;b&gt;&quot;시간 및 언어 &amp;gt; 언어 및 지역 &amp;gt; Beta: ...Unicode UTF-8&quot;&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 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; 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;&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;예를 들어, MinGW를 이용해 컴파일 및 링킹을 수행하는 과정은 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;inline-em&quot;&gt;CMake&lt;/span&gt; &amp;rarr; &lt;span class=&quot;inline-em&quot;&gt;mingw32-make&lt;/span&gt; &amp;rarr; &lt;span class=&quot;inline-em&quot;&gt;g++&lt;/span&gt; &amp;rarr; &lt;span class=&quot;inline-em&quot;&gt;ld&lt;/span&gt; &amp;rarr; &lt;span class=&quot;inline-em&quot;&gt;실행파일(.exe) / 라이브러리(.dll)&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제는, 이 도구들이 문자열(경로, 인자 등)을 주고받을 때 작동하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;*인코딩 형식은 대부분 툴이 빌드될 때 결정되며, 최신 도구들은 자체적으로 UTF-8을 기본으로 사용하는 경우가 많다.&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;231&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhwc2s/dJMcai3Nad6/jbzKROsYRyST18p0yELCsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhwc2s/dJMcai3Nad6/jbzKROsYRyST18p0yELCsk/img.png&quot; data-alt=&quot;MinGW를 이용한 빌드과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhwc2s/dJMcai3Nad6/jbzKROsYRyST18p0yELCsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdhwc2s%2FdJMcai3Nad6%2FjbzKROsYRyST18p0yELCsk%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;231&quot; height=&quot;432&quot; data-origin-width=&quot;231&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MinGW를 이용한 빌드과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 알파벳, 숫자, 범용 특수문자(/, \, _, - 등)와 같은 &lt;b&gt;ASCII 문자&lt;/b&gt;로만 이루어진 경로라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽는 쪽과 보내는 쪽이 UTF-8이든 UTF-16이든 문자가 깨질 일이 없다.&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;파일을 찾지 못하는 치명적인 빌드 에러로 직결된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;버그 검증 및 재현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 먼저 Windows OS의 활성 코드 페이지 설정을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows11 기준으로, cmd창에서 &lt;span class=&quot;inline-em&quot;&gt;chcp &lt;/span&gt;&amp;nbsp;명령어를 입력하면 UTF-8 옵션 활성화 여부에 따라 코드페이지가 달라지는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWjBMi/dJMcagEZyCC/cGRcKjNDcE4ILBPSPi64Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWjBMi/dJMcagEZyCC/cGRcKjNDcE4ILBPSPi64Y1/img.png&quot; data-alt=&quot;Windows11 OS UTF-8 Codepage사용 옵션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWjBMi/dJMcagEZyCC/cGRcKjNDcE4ILBPSPi64Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWjBMi%2FdJMcagEZyCC%2FcGRcKjNDcE4ILBPSPi64Y1%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;655&quot; height=&quot;238&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Windows11 OS UTF-8 Codepage사용 옵션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;223&quot; data-origin-height=&quot;20&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMLFnV/dJMcaaY2uip/h9u8YkOWvzde1cIqGvmJV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMLFnV/dJMcaaY2uip/h9u8YkOWvzde1cIqGvmJV0/img.png&quot; data-alt=&quot;옵션 체크 시 (65501=UTF-8)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMLFnV/dJMcaaY2uip/h9u8YkOWvzde1cIqGvmJV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMLFnV%2FdJMcaaY2uip%2Fh9u8YkOWvzde1cIqGvmJV0%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;223&quot; height=&quot;20&quot; data-origin-width=&quot;223&quot; data-origin-height=&quot;20&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;옵션 체크 시 (65501=UTF-8)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;199&quot; data-origin-height=&quot;25&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvbsZO/dJMcahjBGqs/xXSYef5oOVBLFtiIqG43Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvbsZO/dJMcahjBGqs/xXSYef5oOVBLFtiIqG43Xk/img.png&quot; data-alt=&quot;옵션 해제 시 (949=EUC-KR 확장)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvbsZO/dJMcahjBGqs/xXSYef5oOVBLFtiIqG43Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvbsZO%2FdJMcahjBGqs%2FxXSYef5oOVBLFtiIqG43Xk%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;199&quot; height=&quot;25&quot; data-origin-width=&quot;199&quot; data-origin-height=&quot;25&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;옵션 해제 시 (949=EUC-KR 확장)&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;p data-ke-size=&quot;size16&quot;&gt;원인 파악 후, Windows에 한글 유저 계정을 추가하고 간단한 C++ HelloWorld 프로젝트를 구성하여 버그를 재현할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #fafafa; color: #383a42; text-align: start;&quot;&gt; &lt;/span&gt; CMakeLists.txt&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1774860372164&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cmake_minimum_required(VERSION 3.15)
project(TinyClassTest LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_executable(tiny_test
    src/main.cpp
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #383a42; text-align: start;&quot;&gt; &lt;/span&gt;&lt;/b&gt; src/main.cpp&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1774860594748&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main() {
    std::cout &amp;lt;&amp;lt; &quot;Hello, World!&quot; &amp;lt;&amp;lt; std::endl;
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 생성 후 다음 명령어로 빌드를 수행해 보면, &lt;b&gt;옵션 활성화 여부에 에러 양상이 갈리는 것을 확인&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;div style=&quot;margin: 30px 0; background: #1e1e1e; color: #00ff88; padding: 20px; border-radius: 10px; font-family: Consolas, monospace;&quot;&gt;
&lt;div style=&quot;color: #ffffff; font-weight: bold; margin-bottom: 10px;&quot;&gt;▶ cmake -S . -B build -G &quot;MinGW Makefiles&quot;&lt;/div&gt;
&lt;div style=&quot;white-space: pre-wrap;&quot;&gt;--&amp;nbsp;The&amp;nbsp;CXX&amp;nbsp;compiler&amp;nbsp;identification&amp;nbsp;is&amp;nbsp;GNU&amp;nbsp;8.1.0 &lt;br /&gt;--&amp;nbsp;Detecting&amp;nbsp;CXX&amp;nbsp;compiler&amp;nbsp;ABI&amp;nbsp;info &lt;br /&gt;--&amp;nbsp;Detecting&amp;nbsp;CXX&amp;nbsp;compiler&amp;nbsp;ABI&amp;nbsp;info&amp;nbsp;-&amp;nbsp;done &lt;br /&gt;--&amp;nbsp;Check&amp;nbsp;for&amp;nbsp;working&amp;nbsp;CXX&amp;nbsp;compiler:&amp;nbsp;C:/Qt/Tools/mingw810_32/bin/c++.exe&amp;nbsp;-&amp;nbsp;skipped &lt;br /&gt;--&amp;nbsp;Detecting&amp;nbsp;CXX&amp;nbsp;compile&amp;nbsp;features &lt;br /&gt;--&amp;nbsp;Detecting&amp;nbsp;CXX&amp;nbsp;compile&amp;nbsp;features&amp;nbsp;-&amp;nbsp;done &lt;br /&gt;--&amp;nbsp;Configuring&amp;nbsp;done&amp;nbsp;(1.0s) &lt;br /&gt;--&amp;nbsp;Generating&amp;nbsp;done&amp;nbsp;(0.0s) &lt;br /&gt;--&amp;nbsp;Build&amp;nbsp;files&amp;nbsp;have&amp;nbsp;been&amp;nbsp;written&amp;nbsp;to:&amp;nbsp;C:/Users/전인학/Desktop/cmake-utf16-test/build&lt;/div&gt;
&lt;div style=&quot;white-space: pre-wrap;&quot;&gt;&lt;span style=&quot;background-color: #1e1e1e; color: #ffffff; text-align: start;&quot;&gt;▶ cmake --build build&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;white-space: pre-wrap;&quot;&gt;CMake&amp;nbsp;Error:&amp;nbsp;Target&amp;nbsp;DependInfo.cmake&amp;nbsp;file&amp;nbsp;not&amp;nbsp;found &lt;br /&gt;mingw32-make.exe[2]:&amp;nbsp;***&amp;nbsp;No&amp;nbsp;rule&amp;nbsp;to&amp;nbsp;make&amp;nbsp;target&amp;nbsp;'C:/Users/전인학/Desktop/cmake-utf16-test/src/main.cpp',&amp;nbsp;needed&amp;nbsp;by&amp;nbsp;'CMakeFiles/tiny_test.dir/src/main.cpp.obj'.&amp;nbsp;&amp;nbsp;Stop. &lt;br /&gt;mingw32-make.exe[1]:&amp;nbsp;***&amp;nbsp;[CMakeFiles\Makefile2:82:&amp;nbsp;CMakeFiles/tiny_test.dir/all]&amp;nbsp;Error&amp;nbsp;2 &lt;br /&gt;mingw32-make.exe:&amp;nbsp;***&amp;nbsp;[Makefile:90:&amp;nbsp;all]&amp;nbsp;Error&amp;nbsp;2&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 고민해 본 몇 가지 방법들이 있고, 나는 &lt;b&gt;①번 방식&lt;/b&gt;으로 버그를 해결할 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;① ASCII 경로만 사용하기 (공용 폴더 활용 / ASCII Staging)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로젝트 경로와 인자에 한글이 포함되지 않는 &lt;b&gt;ASCII 문자열만 사용&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;table style=&quot;border-collapse: collapse; width: 98.7212%; height: 54px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.9335%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;예시 경로&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 103.666%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25.9335%; height: 20px;&quot;&gt;C:\Users\전인학\...&lt;/td&gt;
&lt;td style=&quot;width: 103.666%; height: 20px;&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;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.9335%; height: 17px;&quot;&gt;C:\ProgramData\TempStage\&lt;/td&gt;
&lt;td style=&quot;width: 103.666%; height: 17px;&quot;&gt;여러 사용자가 공유하는 범용 위치로 인식되며 경로에 한글이 없어 인코딩 충돌을 우회할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② Windows의 subst 명령어 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows 커맨드인 subst를 사용하여 기존의 긴 경로를 가상의 논리 드라이브로 간단하게 매핑할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;예: 커맨드 라인에 &lt;span class=&quot;inline-em&quot;&gt;subst M: C:\Users\전인학\...&lt;/span&gt; 등록&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;이후 빌드 툴에는 &quot;M:\TempStage&quot;처럼 ASCII로만 이루어진 맵핑 경로만 전달되므로 문제를 회피할 수 있음.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ &lt;span style=&quot;color: #666666;&quot;&gt;빌드 도구 인코딩 강제 일치시키기&lt;/span&gt;&amp;nbsp;(비추)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 빌드툴의 인코딩 방식을 강제로 일치시켜 해결하는 방법.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 MinGW 대신 Ninja로 툴을 전환한다거나, 내부 구현을 알 수 없는 서드파티 툴이 파이프라인에 섞일 경우 제어가 불가능해지기 때문에 현실적인 문제가 많아 옳은 방식은 아닌 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&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;빌드가 사용자 PC환경에서 실행&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;Windows 환경에서는 사용자 계정 명에 한글이 자유롭게 쓰일 수 있다는 예외 상황&lt;/b&gt;을 경험해 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 더해 빌드 툴들의 인코딩 처리 메커니즘, OS의 코드 페이지 동작 방식, 그리고 subst 나 공용 폴더 격리 같은 실용적인 회피 기법까지 다양하게 배울 수 있는 계기가 되었다.&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <category>CMake</category>
      <category>MinGW</category>
      <category>No rule to mak target</category>
      <category>troubleshooting</category>
      <category>UTF8</category>
      <category>Windows11</category>
      <category>빌드에러</category>
      <category>인코딩</category>
      <category>코드페이지</category>
      <category>한글경로</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/41</guid>
      <comments>https://prejudice.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 31 Mar 2026 10:59:32 +0900</pubDate>
    </item>
    <item>
      <title>Antigravity 구독 취소 방법(환불정책)</title>
      <link>https://prejudice.tistory.com/40</link>
      <description>&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발에 본격적으로 AI Agent를 사용하기 시작하면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor를 사용했다가 Antigravity로 넘어왔지만, 회사에서 Github Copilot을 구매지원을 해 주면서 불필요해졌다.&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도구는 워낙 빠르게 엎치락 덮치락 하다 보니 도구 변경이 잦고 계속 적응해야 살아남는 시대가 된 것 같다.&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Antigravity 환불 정책&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;26년 3월 26일 기준 &lt;b&gt;Antigravity는 월 구독 서비스&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Antigravity를 사용중이라면 한국 기준으론 부분환불 정책은 없어,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;한 달 단위로 요금을 지불해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;단, 요금제를 잘못 선택하거나 실수로 구매한 경우에는 구매한 방법에 따라 환불 정책이 다르게 적용된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GoogleOne으로 구매한 경우는 어렵다&lt;/b&gt;고 보면 되고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PlayStore에서 구매한 경우 환불 정책에 의해 48시간 이내, 문의를 통해 시도해 볼 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://support.google.com/googleplay/workflow/9813244?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://support.google.com/googleplay/workflow/9813244?hl=ko&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;*다만 최근에는 환불요청을 거절당하는 사례가 늘고있다고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Antigravity 구독 취소 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://antigravity.google/pricing&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://antigravity.google/pricing&lt;/a&gt; 에 접속해 로그인을 위해 &lt;span class=&quot;inline-em&quot;&gt;Pricing-Get plan&lt;/span&gt; 를 선택한다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;1031&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKPp9f/dJMcafMN2Nf/uG2Cs5Ctl8XIkp0GnqV1iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKPp9f/dJMcafMN2Nf/uG2Cs5Ctl8XIkp0GnqV1iK/img.png&quot; data-alt=&quot;1. Antigravity 접속&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKPp9f/dJMcafMN2Nf/uG2Cs5Ctl8XIkp0GnqV1iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKPp9f%2FdJMcafMN2Nf%2FuG2Cs5Ctl8XIkp0GnqV1iK%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;630&quot; height=&quot;339&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;1031&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;1. Antigravity 접속&lt;/figcaption&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;아래로 스크롤해 &lt;span class=&quot;inline-em&quot;&gt;멤버십 취소&lt;/span&gt; 를 선택하자.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kUboy/dJMcabQ8bgT/NGCaG3oCORR5KJKuhunet1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kUboy/dJMcabQ8bgT/NGCaG3oCORR5KJKuhunet1/img.png&quot; data-alt=&quot;2. 현재 구독 요금제 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kUboy/dJMcabQ8bgT/NGCaG3oCORR5KJKuhunet1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkUboy%2FdJMcabQ8bgT%2FNGCaG3oCORR5KJKuhunet1%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;639&quot; height=&quot;344&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2. 현재 구독 요금제 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nIMbW/dJMcadg7pFJ/qniIxPOKRo35CIrmg1TB21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nIMbW/dJMcadg7pFJ/qniIxPOKRo35CIrmg1TB21/img.png&quot; data-alt=&quot;3. 멤버십 취소&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nIMbW/dJMcadg7pFJ/qniIxPOKRo35CIrmg1TB21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnIMbW%2FdJMcadg7pFJ%2FqniIxPOKRo35CIrmg1TB21%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;544&quot; height=&quot;240&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3. 멤버십 취소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멤버십 정기결제 탭에서 &lt;span class=&quot;inline-em&quot;&gt;정기 결제 취소&lt;/span&gt;&amp;nbsp;로 멤버십을 취소할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1903&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egl1rH/dJMcaiJsv2p/XpTSRCp3uQ6X8b0m4elve0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egl1rH/dJMcaiJsv2p/XpTSRCp3uQ6X8b0m4elve0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egl1rH/dJMcaiJsv2p/XpTSRCp3uQ6X8b0m4elve0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fegl1rH%2FdJMcaiJsv2p%2FXpTSRCp3uQ6X8b0m4elve0%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;717&quot; height=&quot;170&quot; data-origin-width=&quot;1903&quot; data-origin-height=&quot;452&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;617&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9vHU/dJMcahKBLyq/R2vts00ouW4LW2l21ffq7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9vHU/dJMcahKBLyq/R2vts00ouW4LW2l21ffq7K/img.png&quot; data-alt=&quot;4. 정기 결제 취소&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9vHU/dJMcahKBLyq/R2vts00ouW4LW2l21ffq7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9vHU%2FdJMcahKBLyq%2FR2vts00ouW4LW2l21ffq7K%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;312&quot; height=&quot;354&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;4. 정기 결제 취소&lt;/figcaption&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;나의 경우 다음 결제일이 4.23일 인 것을 볼 수 있는데 환불정책에 의해 다음달까지 사용할 수 있다.&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Antigravity 사용 후기 : 2026.02.17 - [개발] - 구글 Antigravity 사용 후기 - QML프로젝트로 AI IDE 특징 알아보기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774507213232&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;구글 Antigravity 사용 후기 - QML프로젝트로 AI IDE 특징 알아보기&quot; data-og-description=&quot;들어가며요즘 AI를 활용한 이른바 '바이브 코딩(Vibe Coding)'은 더 이상 특별한 일이 아니다.간단한 프로젝트 생성부터 기능 추가, 빌드 자동화까지 AI에게 맡기는 흐름이 자연스러워지고 있다. 특&quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/31&quot; data-og-url=&quot;https://prejudice.tistory.com/31&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bCNs7g/dJMb84p8nHy/esbEwJp6tG2cPKtX86xWY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/IQMku/dJMb85vOE5a/Q24xcq4DMuDIah6QgTxf01/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/c9tdgV/dJMb8953jtL/z1EEOEoE5yktGhiWvXkWF1/img.png?width=1665&amp;amp;height=933&amp;amp;face=0_0_1665_933&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/31&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bCNs7g/dJMb84p8nHy/esbEwJp6tG2cPKtX86xWY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/IQMku/dJMb85vOE5a/Q24xcq4DMuDIah6QgTxf01/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/c9tdgV/dJMb8953jtL/z1EEOEoE5yktGhiWvXkWF1/img.png?width=1665&amp;amp;height=933&amp;amp;face=0_0_1665_933');&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;구글 Antigravity 사용 후기 - QML프로젝트로 AI IDE 특징 알아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며요즘 AI를 활용한 이른바 '바이브 코딩(Vibe Coding)'은 더 이상 특별한 일이 아니다.간단한 프로젝트 생성부터 기능 추가, 빌드 자동화까지 AI에게 맡기는 흐름이 자연스러워지고 있다. 특&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Antigravity 사용 예제 : 2026.02.26 - [개발] - AI와 공장 자동화(FA)연동 따라하기 - PLC 예제로 구현한 MCP&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774507237376&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;AI와 공장 자동화(FA)연동 따라하기 - PLC 예제로 구현한 MCP&quot; data-og-description=&quot;들어가며최근 AI가 빠르게 발달하면서 FA(Factory Automation) 업계에서도 AI의 적극적인 도입을 검토하는 움직임이 있다. 현장에서는 &amp;quot;우리 공장 설비(서비스)를 AI와 연동할 수 있을까?&amp;quot;에 대한 수요가&quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/34&quot; data-og-url=&quot;https://prejudice.tistory.com/34&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/78ZLQ/dJMb8868vsH/DGNTojTtcgXnyWKof5lfJk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/Pv684/dJMb85WS1CK/A66dRLKrXjGVTHPXtVoTi0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b96DxI/dJMb88e0kZ6/oribnMmYNsMI05bdbvPMrK/img.png?width=1033&amp;amp;height=537&amp;amp;face=0_0_1033_537&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/34&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/78ZLQ/dJMb8868vsH/DGNTojTtcgXnyWKof5lfJk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/Pv684/dJMb85WS1CK/A66dRLKrXjGVTHPXtVoTi0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b96DxI/dJMb88e0kZ6/oribnMmYNsMI05bdbvPMrK/img.png?width=1033&amp;amp;height=537&amp;amp;face=0_0_1033_537');&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;AI와 공장 자동화(FA)연동 따라하기 - PLC 예제로 구현한 MCP&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며최근 AI가 빠르게 발달하면서 FA(Factory Automation) 업계에서도 AI의 적극적인 도입을 검토하는 움직임이 있다. 현장에서는 &quot;우리 공장 설비(서비스)를 AI와 연동할 수 있을까?&quot;에 대한 수요가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.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;</description>
      <category>개발</category>
      <category>Agent</category>
      <category>AI IDE</category>
      <category>antigravity</category>
      <category>개발자일상</category>
      <category>결제해지</category>
      <category>구독관리</category>
      <category>구독취소</category>
      <category>구취</category>
      <category>환불방법</category>
      <category>환불정책</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/40</guid>
      <comments>https://prejudice.tistory.com/40#entry40comment</comments>
      <pubDate>Thu, 26 Mar 2026 16:05:49 +0900</pubDate>
    </item>
    <item>
      <title>붉은사막 컨트롤러 키조합 세팅 - 8BitDo 조작감 개선</title>
      <link>https://prejudice.tistory.com/39</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;부정적인 리뷰가 많아 남들에게 말하기 부끄럽지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 새로나온 펄어비스의 &lt;b&gt;붉은사막(Crimson Desert)&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;진짜 불쾌한 경험인 &lt;/span&gt;붉은사막의 조작감은 리뷰에서도 말이 많은데...&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;8BitDo ultimate 2 컨트롤러 후면버튼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 컨트롤러 : &lt;b&gt;8BitDo Ultimate 2&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컨트롤러의 특징은 &lt;b&gt;후면부에 위치한 &lt;/b&gt;&lt;span class=&quot;inline-em&quot;&gt;N&lt;/span&gt;&amp;nbsp;&lt;b&gt;4개의 추가 키(L4, R4, PL, PR)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biVNzj/dJMcad2oqPh/hpg7x0WuH0rcNbE4KdcLs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biVNzj/dJMcad2oqPh/hpg7x0WuH0rcNbE4KdcLs0/img.png&quot; data-alt=&quot;8Bitdo ultimate 2 전면부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biVNzj/dJMcad2oqPh/hpg7x0WuH0rcNbE4KdcLs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiVNzj%2FdJMcad2oqPh%2Fhpg7x0WuH0rcNbE4KdcLs0%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;425&quot; height=&quot;264&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;8Bitdo ultimate 2 전면부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/meq8N/dJMcadnN5Za/dpS4kbZaJZm4Yeer8jD89K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/meq8N/dJMcadnN5Za/dpS4kbZaJZm4Yeer8jD89K/img.png&quot; data-alt=&quot;8Bitdo ultimate 2 후면부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/meq8N/dJMcadnN5Za/dpS4kbZaJZm4Yeer8jD89K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmeq8N%2FdJMcadnN5Za%2FdpS4kbZaJZm4Yeer8jD89K%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;421&quot; height=&quot;270&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;8Bitdo ultimate 2 후면부&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;키조합 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 4개의 키는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;별도의 SW 다운로드 없이,&lt;b&gt;&lt;span&gt; 여러&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&amp;nbsp;키를 조합하여 매핑할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 &lt;span class=&quot;inline-em&quot;&gt;R4 = RB + A&lt;/span&gt; 이런식으로 매핑을 하면 &lt;b&gt;R4키가 RB버튼과 A버튼을 동시에 누른것으로 동작한다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&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;&lt;b&gt;조합키 등록 방법. (R4 버튼에 RB+A 조합 사용시)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; 1) &lt;/b&gt;&lt;span class=&quot;inline-em&quot;&gt;R4&lt;/span&gt;&amp;nbsp;+ &lt;span class=&quot;inline-em&quot;&gt;RB&lt;/span&gt;&amp;nbsp;+ &lt;span class=&quot;inline-em&quot;&gt;A&lt;/span&gt;&amp;nbsp; 버튼을 동시에 누르기&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; 2)&lt;/b&gt; 누른 상태로 전면부의 &lt;span class=&quot;inline-em&quot;&gt;■&lt;/span&gt; 버튼을 눌렀다 때기 (중앙 LED가 깜빡이면 등록됨)&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조합키 해제 방법. (R4 버튼에 등록된 기능 해제시)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; 1) &lt;/b&gt;&lt;span class=&quot;inline-em&quot;&gt;R4&lt;/span&gt;&amp;nbsp;버튼 누르기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; 2) &lt;/b&gt;누른 상태로 &lt;span class=&quot;inline-em&quot;&gt;&lt;span style=&quot;background-color: #2b2f36; color: #ffffff; text-align: start;&quot;&gt;■&lt;/span&gt;&lt;/span&gt; 버튼을 누르기&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;주의) 만약 컨트롤러 전용 SW(8BitDo Ultimate Software V2) 를 사용중이라면&lt;br /&gt;Profiel 을 비활성 했을 경우에만 조합키 사용이 가능하다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; 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;h3 data-ke-size=&quot;size23&quot;&gt;1. 섭리의 힘 : PL 버튼에 (L누름) 키를 매핑&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; L누름키다 보니 저상태로 이동하기가 굉장히 어색한데, 버튼 하나로 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfCRZh/dJMcah4Rctl/NJ3zjZUz4LKBpdkPXgEqj0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfCRZh/dJMcah4Rctl/NJ3zjZUz4LKBpdkPXgEqj0/img.gif&quot; data-alt=&quot;섭리의 힘 키 매핑&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfCRZh/dJMcah4Rctl/NJ3zjZUz4LKBpdkPXgEqj0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bfCRZh/dJMcah4Rctl/NJ3zjZUz4LKBpdkPXgEqj0/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;400&quot; height=&quot;510&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;섭리의 힘 키 매핑&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 찌르기 : R4 버튼에 (RB + Y) 키 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 찌르기키는 RB 일반공격키와 Y를 조합해 가끔 일반공격이 나갈때가 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 키조합을 사용하니 달리면서 쓰거나 모아쓰기에 확실히 편했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ATzBT/dJMcahjvDeD/RFihotLUr1VvmHX2S44kdk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ATzBT/dJMcahjvDeD/RFihotLUr1VvmHX2S44kdk/img.gif&quot; data-alt=&quot;찌르기 키 매핑&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ATzBT/dJMcahjvDeD/RFihotLUr1VvmHX2S44kdk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ATzBT/dJMcahjvDeD/RFihotLUr1VvmHX2S44kdk/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;400&quot; height=&quot;510&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;찌르기 키 매핑&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 회전가르기 : PR 버튼에 (RB+RT) 키 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 내 기준으로 가장 괴랄한 스킬인 회전가르기는, 오른쪽 검지로 누르는 RB와 오른쪽 검지로 당기는 RT키를 같이 눌러야 해&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;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;&amp;nbsp; &amp;nbsp; *영상에 평타를 섞어 쓴 회전가르기는, 평타중 바로 스킬을 쓸 수 있다는걸 보여주기 위함입니다...&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;400&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMTLk0/dJMcahqfLP2/UDW6ju2i5mYng5I51x81Tk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMTLk0/dJMcahqfLP2/UDW6ju2i5mYng5I51x81Tk/img.gif&quot; data-alt=&quot;회전가르기 키 매핑&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMTLk0/dJMcahqfLP2/UDW6ju2i5mYng5I51x81Tk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bMTLk0/dJMcahqfLP2/UDW6ju2i5mYng5I51x81Tk/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;400&quot; height=&quot;510&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회전가르기 키 매핑&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;붉은사막은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;왜&lt;span&gt; &lt;/span&gt;&lt;/span&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;&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;8BitDo 의 키매핑 기능을 처음 써봤는데 등록/해제가 쉽고 진짜 잘 만든 컨트롤러로 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;ps. 컨트롤러 광고나 게임광고 절대 아닙니다.&lt;/span&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;/p&gt;</description>
      <category>게임</category>
      <category>8Bitdo</category>
      <category>crimsondesert</category>
      <category>게임공략</category>
      <category>붉은사막</category>
      <category>조작감</category>
      <category>조작감개선</category>
      <category>컨트롤러세팅</category>
      <category>컨트롤러추천</category>
      <category>키매핑</category>
      <category>펄어비스</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/39</guid>
      <comments>https://prejudice.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 23 Mar 2026 21:47:42 +0900</pubDate>
    </item>
    <item>
      <title>Real-Time, RTOS, PREEMPT_RT, CPU Isolation 개념 정복 - 실시간 처리</title>
      <link>https://prejudice.tistory.com/38</link>
      <description>&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'실시간(Real-time)'이라는&lt;/b&gt; 용어는 Embedded 및 FactoryAutomation 산업의 핵심이며,&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;는 모든 SW/HW의 핵심 지표이지만, 실시간성과 도일한 개념으로 혼동되기 쉽다.&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: #333333; text-align: start;&quot;&gt;이번 글에서는 &lt;b&gt;개발자의 입장&lt;/b&gt;에서 &lt;/span&gt;&lt;b&gt;실시간성, RTOS, RT패치, CPU Isolation &lt;/b&gt;등의 개념을 정리하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 개발 시 고려해야 할 점들을 학습해 보려 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;범용 OS가 발전하고 소프트웨어 아키텍처가 복잡해짐에 따라&lt;br /&gt;OS 스케줄링, 인터럽트, 동시성(Concurrency)과 병렬성(Parallelism) 등 성능에 영향을 미치는 요소가 많아진다.&lt;br /&gt;이 글에서는 기초적인 운영체제 지식이 있다고 가정하며, 직관적인 이해를 위해 극단적인 상황을 가정한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;실시간성 (Real-time)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실시간성(Real-time)&lt;/b&gt;이란 데이터 처리나 시스템 응답이 &lt;b&gt;정해진 시간제한(Deadline)&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;예를 들어, 주기적으로 센서 값을 읽고 제어 신호를 출력하는 시스템의 데드라인이 1ms라면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 작업은 반드시 1ms 이내에 완료되어야 한다.&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;OS Scheduling 지연&lt;/li&gt;
&lt;li&gt;Interrupt 처리 지연&lt;/li&gt;
&lt;li&gt;Mutex 등 동기화 메커니즘에 의한 대기 시간&lt;/li&gt;
&lt;li&gt;Context Switching 비용&lt;/li&gt;
&lt;li&gt;Garbage Collector와 같은 메모리 등록 및 해제 시간&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 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Hard Real-Time System&lt;span style=&quot;color: #ef6f53;&quot;&gt; vs &lt;/span&gt;Soft Real-Time System&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 시스템의 차이는 &lt;b&gt;Deadline을 넘겼을 때 얼마나 치명적인 가로&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;Soft Real-Time&lt;/b&gt;: deadline가 품질 저하로 이어짐. (ex. 영상/음성 스트리밍, 게임 렌더링, 회복/대응이 가능한 공장시스템 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hard Real-Time&lt;/b&gt;: deadline miss가 곧 실패. (ex. 미사일, 항공, 의료, 인공위성 제어 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 Deadline을 만족하는지 확인하고 보장하는 방법에는 두 가지 측면이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;측정 기반(Measurement-based)&lt;br /&gt;&lt;/b&gt;다양한 조건과 시뮬레이션을 수없이 반복하여 경험적인 최악의 실행 시간(WCET, Worst Case Execution Time)을 도출한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계산 및 증명 기반(Static Analysis)&lt;br /&gt;&lt;/b&gt;코드 수준에서 구조적 실행 횟수가 확정적이라면 수학적으로 WCET를 엄밀하게 계산할 수 있다.&lt;br /&gt;예를 들어, 다음의 코드에서 최악의 조건 분기에서 반복문을 1,000번 수행함을 보장할 수 있으므로, 하드웨어 명령어 처리 시간을 기반으로 전체 소요 시간을 증명할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1773895470502&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 한 명령어 시간 = CPU 클럭 + 명려어 사이클 수 + 메모리 접근 조건
if(condition) {
  for (int i=0; i&amp;lt;10; i++)
  	sum += i;
} else {
  for (int i=0;i &amp;lt;1000; i++)
    sum += i;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;RTOS (Real-Time Operating System)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTOS는 실시간 작업에 대해 &lt;b&gt;예측 가능한 응답 시간(Determinism)을 지원하도록 설계된&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;범용 OS에 비해 &lt;b&gt;우선순위 기반 스케줄링, 짧고 예측 가능한 인터럽트/스케줄링 지연, 단순한 커널 구조&lt;/b&gt;를 갖추어 개발자가 실시간성을 분석하고 보장하기 아주 쉬운 구조와 API를 제공한다.&lt;/p&gt;
&lt;p data-end=&quot;1403&quot; data-start=&quot;1313&quot; data-ke-size=&quot;size16&quot;&gt;즉, RTOS는 &lt;span class=&quot;inline-em&quot;&gt;실시간성을 분석하고 보장하기 쉬운 구조&lt;/span&gt; 를 제공한다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YYFkA/dJMcac3rprx/pKI3nbbzGHznNqVOL6KEbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YYFkA/dJMcac3rprx/pKI3nbbzGHznNqVOL6KEbK/img.png&quot; data-alt=&quot;범용OS 와 RTOS의 차이&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YYFkA/dJMcac3rprx/pKI3nbbzGHznNqVOL6KEbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYYFkA%2FdJMcac3rprx%2FpKI3nbbzGHznNqVOL6KEbK%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;547&quot; height=&quot;283&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;범용OS 와 RTOS의 차이&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;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;RTOS에 대해 가지는 오해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 &lt;span class=&quot;inline-em&quot;&gt;❌RTOS는 무조건 빠를 것&lt;/span&gt;&lt;b&gt; &lt;/b&gt;이라는 생각이다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS 동작 측면에서 불필요한 스위칭이나 군더더기가 없어 즉각적이라고 할 수는 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 연산의 실행 속도는 CPU 클럭에 비례한다.&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;멀티코어 최적화 및 GPU 가속을 적극적으로 활용하는 범용 OS의 전체 처리량(Throughput)이 훨씬 높고 빠를 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 RTOS의 본질은 '단순 속도'가 아니라 '결정성(Determinism)'에 있다.&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 class=&quot;inline-em&quot;&gt;❌RTOS를 사용하면 Deadline을 절대 넘기지 않는다&lt;/span&gt;&amp;nbsp;는 바람이다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTOS 자체가 지연을 없애주지는 않고, 시스템 설계가 잘못되어 인터럽트가 폭주하면 RTOS도 당연히 지연된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 범용 OS는 이런 지연의 크기를 계산하기 불가능해 사후 장애로 인지하게 된다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTOS는 &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;범용 OS : 인터럽트 발생 시 얼마나 늦을지 계산이 어려움 &amp;rarr; 막연하게 기다리며 사후에 알게 됨&lt;/li&gt;
&lt;li&gt;RTOS : 인터럽트 실행 전 예측가능함 &amp;rarr; 대응 로직 설계 가능&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;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;RTOS의 종류와 빌드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 RTOS는 &lt;b&gt;FreeRTOS, VxWorks, Micrium, QNX &lt;/b&gt;등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 C/C++ 언어로 개발하며 OS별로 호환되는 특정 툴체인을 통해 펌웨어를 빌드하게 된다.&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;RTOS는 커널 코어가 공통된 스케줄링 및 락 메커니즘을 제공하지만, ARM Cortex-M, RISC-V, x86 등 타깃 시스템이 되는 CPU 아키텍처에 맞춘 &lt;b&gt;포팅 계층(Porting Layer)&lt;/b&gt;이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저수준의 콘텍스트 스위칭 기법, 타이머 Tick 발상 제어, 스택 프레임 구조 등 하드웨어 의존적인 파트를 이어줘야 하기 때문이다.&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;더불어 타깃 보드의 주변장치(UART, SPI, I2C, GPIO 등)를 제어하려면 하드웨어 초기화 코드와 드라이버 모음인&amp;nbsp;&lt;b&gt;BSP(Board Support Package)&lt;/b&gt; 혹은 &lt;b&gt;HAL(Hardware Abstraction Layer)&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;다행히 일반적인 실무 개발에서는 커널을 직접 짜는 대신, 이미 잘 만들어진 RTOS 커널과 벤더의 HAL을 융합해 그 위에서 애플리케이션 Task를 얹어 개발할 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;RT 패치 (PREEMPT_RT)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정식 명칭은 PREEMPT_RT로 &lt;b&gt;기존 리눅스(Linux) 커널의 구조를 수정&lt;/b&gt;하여 실시간성을 지원하도록 만든 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널의 &lt;b&gt;선점&amp;middot;락&amp;middot;인터럽트 동작 구성을 변경해&lt;/b&gt; &lt;b&gt;실시간성을 제공&lt;/b&gt;한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;RT 패치라고 용어는 Linux에 덧붙이던 '패치 셋'으로 출발했기 때문에 과거에 불리던 이름이다.&lt;br /&gt;Linux Kernel 6.12 버전부터 공식적으로 병합되어 PREEMPT_RT라는 이름으로 발표되었다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PREEMPT_RT의 기술적 핵심은 커널 내부의 &lt;b&gt;비선점 구간을 최소화&lt;/b&gt;하고,&amp;nbsp;&lt;span class=&quot;inline-em&quot;&gt;인터럽트 처리를 스레드로 처리&lt;/span&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;실시간성을 가져야 하는 'Task B'가 동작하는 Linux OS의 타임라인을 상상해 보자.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Linux 커널은 &lt;b&gt;Hardware IRQ 처리를 최우선시&lt;/b&gt;하여 모든 IRQ를 처리 한 뒤 Task B가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최악의 경우 Interrupt가 많이 발생한다면, Task B는 아예 실행되지 못할 수 있다.&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;612&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cahWl7/dJMcaduuuih/AftSJRbctNgCGEvbRxWRPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cahWl7/dJMcaduuuih/AftSJRbctNgCGEvbRxWRPK/img.png&quot; data-alt=&quot;기존 Linux Kernel의 Inturrupt 처리 타임라인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cahWl7/dJMcaduuuih/AftSJRbctNgCGEvbRxWRPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcahWl7%2FdJMcaduuuih%2FAftSJRbctNgCGEvbRxWRPK%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;612&quot; height=&quot;90&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존 Linux Kernel의 Inturrupt 처리 타임라인&lt;/figcaption&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;반면 PREEMPT_RT 커널은 &lt;b&gt;Kernel이 선점하는 구간을 줄이고 IRQ 후처리를 Thread로 처리하는 구조&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Task B의 우선순위가 높다면 IRQ thread보다 먼저 실행되도록 보장받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 인터럽트 발생 시 TaskB가 실행될 것을 예측할 수 있음을 의미한다. &lt;b&gt;(실시간성 보장)&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q1HCE/dJMcag5Tluo/iqp6ckCsIjn9TIClGq4vwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q1HCE/dJMcag5Tluo/iqp6ckCsIjn9TIClGq4vwk/img.png&quot; data-alt=&quot;PREEMPT_RT Kernel의 Inturrupt 처리 타임라인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q1HCE/dJMcag5Tluo/iqp6ckCsIjn9TIClGq4vwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq1HCE%2FdJMcag5Tluo%2Fiqp6ckCsIjn9TIClGq4vwk%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;612&quot; height=&quot;90&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PREEMPT_RT Kernel의 Inturrupt 처리 타임라인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Jitter (지터)&lt;/h2&gt;
Jitter의 사전적 의미는 전송 신호가 이상적인 기준 시간으로부터 미세하게 흔들리는 지연시간을 의미한다.&lt;br /&gt;즉, 기존 Linux 커널에서는 하드 IRQ나 커널의 비선점 락에 치여 Task B의 시작 주기가 일정하지 않게 흔들릴 수 있다.&lt;br /&gt;반면, PREEMPT_RT는 태스크를 선점가능하게 만들어 &lt;b&gt;Jitter를 줄이고 분석 가능하게 만들었다는 데 의미를 가진다&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;예시) 1ms 주기의 태스크 실행&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 78px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 26px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;Task 실행 1회 차&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;Task 실행 2회 차&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;Task 실행 3회 차&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 26px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;범용 OS&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;1.0ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;1.7ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;2.3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 26px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;RTOS 또는 PREEMPT_RT&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;1.0ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;1.01ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 26px;&quot;&gt;1.03ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;CPU Isolation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 Jitter에 대해 살펴본 내용을 기준으로 보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RTOS 또는 PREEMPT_RT는 &lt;b&gt;커널 내부의 비선점 구간과 인터럽트 처리 지연을 줄여 Kernel Jitter를 완화&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;OS Jitter가 발생할 수 있다&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;context switch 시간&lt;/li&gt;
&lt;li&gt;mutex, semaphore 등 동기화 객체 대기 시간&lt;/li&gt;
&lt;li&gt;같은 우선순위 태스크 간 round-robin 스케줄링&lt;/li&gt;
&lt;li&gt;주기적 timer tick&lt;/li&gt;
&lt;li&gt;IRQ thread, kworker, RCU callback 등 커널의 housekeeping 작업&lt;/li&gt;
&lt;li&gt;드라이버 및 백그라운드 시스템 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 수십 마이크로초 수준의 방해마저 용납할 수 없는 환경에서, 멀티코어 자원을 분할하는 CPU Isolation을 고려할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CPU Isolation란 &lt;/b&gt;&lt;span class=&quot;inline-em&quot;&gt;특정 CPU 코어를 특정 작업에 사용하도록 격리&lt;/span&gt;&lt;b&gt; &lt;/b&gt;하여 OS작업과 분리하는 기법이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 42px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.5116%; height: 21px;&quot;&gt;일반 CPU 코어&lt;/td&gt;
&lt;td style=&quot;width: 73.4884%; height: 21px;&quot;&gt;IRQ 처리, 커널 스레드, 백그라운드 작업 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.5116%; height: 21px;&quot;&gt;Isolated CPU 코어&lt;/td&gt;
&lt;td style=&quot;width: 73.4884%; height: 21px;&quot;&gt;중요한 실시간 태스크 위주로 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 태스크가 실행되는 환경을 더 단순하고 조용하게 만들어, OS Jitter를 좀 더 줄일 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &quot;실시간성&quot;이라는 단어로 시작해, 평소 막연하던 다가왔던 추상적 개념들을 공부해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요성이 대두될 때 가장 밀도 있게 성장하듯, 현재 개발 중인 SW PLC 프로젝트에 도입하기 위해 집중적으로 학습할 수 있었다.&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;최근에는 생산 설비가 정교하고 민감해짐에 따라, 과거 군사 제어기나 항공 우주 등급에서나 주로 논의되던 이러한 딥다이브 기술들이 일반 Factory Automation 산업에도 빠르게 들어오는 것을 느꼈다.&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: #333333; text-align: start;&quot;&gt;이번 기회를 통해 기초적인 &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;개념을 정확하게 잡았고, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;특히 &lt;b&gt;RealTime &amp;rarr; RTOS &amp;rarr; PREEMPT_RT&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&amp;rarr; CPU Isolation 과정&lt;/b&gt;으로 스토리를 가지고 개념을 확실하게 다질 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이제 앞으로 다가올 현장의 시스템 한계점과 장단점을 부딪혀가며 기술 영역을 보다 넓게 확장해 나가야겠다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&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: #333333; text-align: start; font-family: GungSeo, serif;&quot;&gt;ps. 작년 Qt Coco 툴을 사용한 Code Coverage 분석교육을 들었는데, 이번에 WCET(Worst Case Execution Time) 개념에서, 프로그램 코드를 통해 최악의 소요시간을 수학적으로 측정하고 증명한다는 포인트가 굉장히 흡사하고 흥미로웠다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <category>CPUIsolation</category>
      <category>jitter</category>
      <category>Linux</category>
      <category>preempt_rt</category>
      <category>realtime</category>
      <category>RTOS</category>
      <category>RT패치</category>
      <category>리눅스커널</category>
      <category>실시간</category>
      <category>임베디드</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/38</guid>
      <comments>https://prejudice.tistory.com/38#entry38comment</comments>
      <pubDate>Thu, 19 Mar 2026 17:41:05 +0900</pubDate>
    </item>
    <item>
      <title>SW USB 동글 라이선스 인증 구현하기 - Sentienl API와 C++ 클래스 설계</title>
      <link>https://prejudice.tistory.com/37</link>
      <description>&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서는 USB 동글 보안의 원리를 학습하고, Sentinel EMS를 통해 USB 동글을 프로비저닝 하는 과정을 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;해당 동글을 제어하는&lt;/b&gt; &lt;b&gt;C++ Class를 설계&lt;/b&gt;하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 &lt;b&gt;Software PLC 시스템에 적용할&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;USB 동글 기반 라이선스 인증 기능&lt;/b&gt;을 구현해 보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://prejudice.tistory.com/36&quot;&gt;2026.03.04 - [개발] - HW USB 동글 보안 시스템 구축하기 - Sentinel EMS 프로비저닝 실무&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773042717764&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;HW USB 동글 보안 시스템 구축하기 - Sentinel EMS 프로비저닝 실무&quot; data-og-description=&quot;들어가며지난 글에서 USB 동글 보안 원리를 학습하고, Sentinel 동글 솔루션의 구조를 분석했다.이번 글에서는 한 단계 더 나아가 실제 Sentinel USB 동글에 라이선스 정보를 저장해 보려고 한다. 최종 &quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/36&quot; data-og-url=&quot;https://prejudice.tistory.com/36&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iWsJ0/dJMb88F204u/359KRXrdADvk1PbRbuEwHK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UNLsh/dJMb88eYMA1/5MtCxnhZxNVdBVDH43wKh1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/q1qat/dJMb8866Ykt/sqHVW0hntac8nTYMWLsRGK/img.png?width=989&amp;amp;height=820&amp;amp;face=0_0_989_820&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/36&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/36&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iWsJ0/dJMb88F204u/359KRXrdADvk1PbRbuEwHK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UNLsh/dJMb88eYMA1/5MtCxnhZxNVdBVDH43wKh1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/q1qat/dJMb8866Ykt/sqHVW0hntac8nTYMWLsRGK/img.png?width=989&amp;amp;height=820&amp;amp;face=0_0_989_820');&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;HW USB 동글 보안 시스템 구축하기 - Sentinel EMS 프로비저닝 실무&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며지난 글에서 USB 동글 보안 원리를 학습하고, Sentinel 동글 솔루션의 구조를 분석했다.이번 글에서는 한 단계 더 나아가 실제 Sentinel USB 동글에 라이선스 정보를 저장해 보려고 한다. 최종&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;SW 요구사항 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발에 앞서 이번 라이선스 인증 모듈에서 처리해야 할 핵심 소프트웨어 요구사항은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;☑ &lt;b&gt;HMI 서비스:&lt;/b&gt; FeatureID 1 사용&lt;/li&gt;
&lt;li&gt;☑ &lt;b&gt;PLC(MuLiN) 서비스:&lt;/b&gt; FeatureID 2 사용&lt;/li&gt;
&lt;li&gt;☑ &lt;b&gt;통합 동글 지원:&lt;/b&gt; Feature1과 Feature2가 함께 존재하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HMI + PLC 통합 라이선스로 동작&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;☐&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;s&gt;PLC 서비스만의&lt;b&gt;고유 데이터 저장&lt;/b&gt;&lt;/s&gt; (후순위 고려)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;메모리 구조 및 클래스 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel USB 동글은 내부에 고유한 구조로 메모리가 저장된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;233&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKs15K/dJMcaioY5HT/kLwwkU85UC52vna5fhFKn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKs15K/dJMcaioY5HT/kLwwkU85UC52vna5fhFKn1/img.png&quot; data-alt=&quot;Sentinel USB 동글 메모리 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKs15K/dJMcaioY5HT/kLwwkU85UC52vna5fhFKn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKs15K%2FdJMcaioY5HT%2FkLwwkU85UC52vna5fhFKn1%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;233&quot; height=&quot;261&quot; data-origin-width=&quot;233&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Sentinel USB 동글 메모리 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Sentinel의 보안 스택을 살펴보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application Area와 RTE(Runtime Environment)가 분리되어 있는 &lt;b&gt;Server-Client 구조&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;Sentinel API가 &lt;span class=&quot;inline-em&quot;&gt;FeatureID&lt;/span&gt; 단위로 로그인 세션을 관리하는것을 확인해,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 바탕으로 통합 제어가 가능한 C++ 클래스를 설계했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;클래스의 주요 역할&lt;/b&gt;&lt;br /&gt;☑ HMI와 PLC 서비스의 FeatureID 구분 및 관리&lt;br /&gt;☑ 사내 고유 VENDOR_CODE의 안전한 저장&lt;br /&gt;☑ Sentinel API 호출 래핑 (로그인, 로그아웃, 메모리 제어 등)&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eOOuPb/dJMcabwBlWJ/560vlfdUGDC9uKd75pBaq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eOOuPb/dJMcabwBlWJ/560vlfdUGDC9uKd75pBaq0/img.png&quot; data-alt=&quot;SentinelClient Class&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eOOuPb/dJMcabwBlWJ/560vlfdUGDC9uKd75pBaq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeOOuPb%2FdJMcabwBlWJ%2F560vlfdUGDC9uKd75pBaq0%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;855&quot; height=&quot;406&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SentinelClient Class&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;SentinelClient C++ 클래스 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동글 제어의 핵심인 &lt;span class=&quot;inline-em&quot;&gt;SentinelClient&lt;/span&gt; 클래스 이다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel API의 x64 라이브러리 파일은 &lt;span class=&quot;inline-em&quot;&gt;Sentinel EMS-Developer-RTE Installer&lt;/span&gt;&amp;nbsp;에서 다운로드할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 개발 중인 프로젝트는 &lt;b&gt;MinGW 32bit 컴파일러&lt;/b&gt;를 사용하고 있으므로, 구버전 라이브러리를 사용하여 구현할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1773046281313&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; SentinelClient
┃  SentinelClient.cpp
┃  SentinelClient.h
┣  include
┃ ┗  hasp_api.h
┗  lib
  ┣  hasp_windows_109675.dll
  ┗  hasp_windows_109675.lib&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SentinelClient.h&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1773046731803&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once

#include &quot;include/hasp_api.h&quot;
#include &amp;lt;map&amp;gt;

namespace Sentinel {
enum FeatureID {
  HMI = 1,
  MulinStandard = 2,
};

const static unsigned char vendorCode[] =
    &quot;[Private Vendor Code...]&quot;;

class SentinelClient {
public:
  SentinelClient();
  ~SentinelClient();

  bool login(FeatureID id);
  void logout(FeatureID id);
  bool hasHandle(FeatureID id) const;
  bool checkAlive(FeatureID id);

  bool readReadWriteMemory(FeatureID id, unsigned int offset, unsigned int length, void *buffer);
  bool readReadOnlyMemory(FeatureID id, unsigned int offset, unsigned int length, void *buffer);

private:
  std::map&amp;lt;FeatureID, hasp_handle_t&amp;gt; m_handles;
};

} // namespace Sentinel&lt;/code&gt;&lt;/pre&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SentinelClient.cpp&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1773046836143&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;SentinelClient.h&quot;
#include &amp;lt;iostream&amp;gt;

using namespace Sentinel;

SentinelClient::SentinelClient() {}

SentinelClient::~SentinelClient() {
  for (auto it = m_handles.begin(); it != m_handles.end();) {
    hasp_logout(it-&amp;gt;second);
    it = m_handles.erase(it);
  }
}

bool SentinelClient::login(FeatureID id) {
  if (hasHandle(id)) {
        return true;
  }

  hasp_handle_t handle;
  const hasp_status_t status = hasp_login(id, vendorCode, &amp;amp;handle);

  if (status == HASP_STATUS_OK) {
    m_handles.emplace(id, handle);
    return true;
  }

  return false;
}

void SentinelClient::logout(FeatureID id) {
  if (!hasHandle(id)) {
    return;
  }
  hasp_logout(m_handles[id]);
  m_handles.erase(id);
}

bool SentinelClient::hasHandle(FeatureID id) const { return m_handles.find(id) != m_handles.end(); }

bool SentinelClient::checkAlive(FeatureID id) {
  if (!hasHandle(id)) {
    return false;
  }

  unsigned char buf[16] = {0};
  const hasp_status_t status = hasp_encrypt(m_handles.find(id)-&amp;gt;second, buf, sizeof(buf));

  if (status == HASP_STATUS_OK) {
    return true;
  }

  logout(id);
  return false;
}

bool SentinelClient::readReadWriteMemory(FeatureID id, unsigned int offset, unsigned int length, void *buffer) {
  if (!hasHandle(id)) {
    return false;
  }

  const hasp_status_t status = hasp_read(m_handles[id], HASP_FILEID_RW, offset, length, buffer);

  if (status == HASP_STATUS_OK) {
    return true;
  } else {
    std::cerr &amp;lt;&amp;lt; &quot;Sentinel Read Failed. Error Code: &quot; &amp;lt;&amp;lt; status &amp;lt;&amp;lt; std::endl;
    return false;
  }
}

bool SentinelClient::readReadOnlyMemory(FeatureID id, unsigned int offset, unsigned int length, void *buffer) {
  if (!hasHandle(id)) {
    return false;
  }

  const hasp_status_t status = hasp_read(m_handles[id], HASP_FILEID_RO, offset, length, buffer);

  if (status == HASP_STATUS_OK) {
    return true;
  } else {
    std::cerr &amp;lt;&amp;lt; &quot;Sentinel Read Failed. Error Code: &quot; &amp;lt;&amp;lt; status &amp;lt;&amp;lt; std::endl;
    return false;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;GoogleTest를 활용한 Unit Test 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어(USB 동글)에 직접 의존성을 가지는 API이므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 신뢰성을 높이기 위해 &lt;b&gt;GoogleTest&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;하기 때문에, AI의 도움으로 테스트 코드를 빠르게 작성할 수 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;테스트 전제 조건&amp;nbsp;&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;하드웨어 동글 기반 API 테스트이기 때문에, PC와 연결된 HW 동글 상태에 따라 결과가 다르게 나온다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;&amp;nbsp; &amp;nbsp; 1. Sentinel USB 동글의 연결 유무&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;&amp;nbsp; &amp;nbsp; 2. 동글 내부에 발급된 Feature Key의 존재 유무&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1773371749416&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; SentinelClient
┗  tests
  ┣  CMakeLists.txt
  ┗  SentinelClientTest.cpp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CMakeLists.txt&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1773372122469&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cmake_minimum_required(VERSION 3.10)
project(SentinelClientTests)

# Use C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Configure googletest from local directory
include(../../googletest/googletest.cmake)

# Add the test executable
add_executable(SentinelClientTests
    SentinelClientTest.cpp
    ../SentinelClient.cpp
)

# Include directories
target_include_directories(SentinelClientTests PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/..
    ${CMAKE_CURRENT_SOURCE_DIR}/../include
)

# Link libraries
target_link_libraries(SentinelClientTests PRIVATE
    gtest
    gtest_main
    &quot;${CMAKE_CURRENT_SOURCE_DIR}/../lib/hasp_windows_109675.lib&quot;
)

# For MinGW: statically link the standard libraries to avoid missing DLL errors at runtime (0xc0000135)
if(MINGW)
    target_link_options(SentinelClientTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()

# Copy the required DLL to the output directory so the test can run
add_custom_command(TARGET SentinelClientTests POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        &quot;${CMAKE_CURRENT_SOURCE_DIR}/../lib/hasp_windows_109675.dll&quot;
        $&amp;lt;TARGET_FILE_DIR:SentinelClientTests&amp;gt;
)

# Add test
include(GoogleTest)
gtest_discover_tests(SentinelClientTests
    PROPERTIES ENVIRONMENT &quot;PATH=${CMAKE_CURRENT_SOURCE_DIR}/../lib;$ENV{PATH}&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SentinelClientTest.cpp&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1773372195004&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;../SentinelClient.h&quot;
#include &amp;lt;gtest/gtest.h&amp;gt;

using namespace Sentinel;

TEST(SentinelClientTest, InitializationTest) {
  SentinelClient client;
  EXPECT_FALSE(client.hasHandle(MulinStandard)); // Should not be logged in initially
}

TEST(SentinelClientTest, CheckLogin) {
  SentinelClient client;
  bool plcResult = client.login(MulinStandard);
  std::cout &amp;lt;&amp;lt; &quot;PLC Login Result: &quot; &amp;lt;&amp;lt; plcResult &amp;lt;&amp;lt; std::endl;

  bool hmiResult = client.login(HMI);
  std::cout &amp;lt;&amp;lt; &quot;HMI Login Result: &quot; &amp;lt;&amp;lt; hmiResult &amp;lt;&amp;lt; std::endl;
}

TEST(SentinelClientTest, LoginLogoutTest) {
  SentinelClient client;
  bool plcResult = client.login(MulinStandard);

  bool plcCheckResult = client.checkAlive(MulinStandard);
  EXPECT_TRUE(plcCheckResult);

  if (plcResult) {
    EXPECT_TRUE(plcResult);
    client.logout(MulinStandard);
    EXPECT_FALSE(client.hasHandle(MulinStandard));
  }
}

TEST(SentinelClientTest, ReadTest) {
  SentinelClient client;
  bool result = client.login(MulinStandard);
  if (result) {
    unsigned char buffer[16] = {0};
    bool readSuccess = client.readReadWriteMemory(MulinStandard, 0, 16, buffer);
    EXPECT_TRUE(readSuccess);
    if (readSuccess) {
      std::cout &amp;lt;&amp;lt; &quot;Read Data (16 bytes at offset 0): &quot;;
      for (int i = 0; i &amp;lt; 16; ++i) {
        printf(&quot;%02X &quot;, buffer[i]);
      }
      std::cout &amp;lt;&amp;lt; std::endl;
    }
    client.logout(MulinStandard);
  } else {
    GTEST_SKIP() &amp;lt;&amp;lt; &quot;Login failed. Sentinel Dongle may not be connected.&quot;;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Unit Test 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 프로비저닝 해둔 &lt;b&gt;MuLiN (PLC) 라이선스 동글&lt;/b&gt;을 꽂고 테스트 시 유닛 테스트를 통과하는 것을 확인했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기반으로 &lt;b&gt;HMI 동글만 있는 경우, 둘 다 꽂혀있는 경우, 하나도 없는 경우 등&lt;/b&gt; 다양한 지 케이스에 대한 검증을 마쳤다.&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;670&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6sNi6/dJMcahjnxAm/vpqxX3SqEyh0LKLZOInFo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6sNi6/dJMcahjnxAm/vpqxX3SqEyh0LKLZOInFo0/img.png&quot; data-alt=&quot;MuLiN License USB연결 후 실행된 Test 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6sNi6/dJMcahjnxAm/vpqxX3SqEyh0LKLZOInFo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6sNi6%2FdJMcahjnxAm%2FvpqxX3SqEyh0LKLZOInFo0%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;670&quot; height=&quot;355&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MuLiN License USB연결 후 실행된 Test 결과&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Qt 연동 중 발견한 버그와 해결 과정 (Troubleshooting)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유닛 테스트 통과 후, 실제 애플리케이션과 결합하기 위해 QObject를 상속받는 &lt;span class=&quot;inline-em&quot;&gt;LicenseManage&lt;/span&gt; 클래스를 추가했다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0gikb/dJMcahKq23p/uzlLr4Wb93xRtojQmYYA6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0gikb/dJMcahKq23p/uzlLr4Wb93xRtojQmYYA6k/img.png&quot; data-alt=&quot;LicenseManager Class&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0gikb/dJMcahKq23p/uzlLr4Wb93xRtojQmYYA6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0gikb%2FdJMcahKq23p%2FuzlLr4Wb93xRtojQmYYA6k%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;1204&quot; height=&quot;445&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LicenseManager Class&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 물리적인 USB 탈착 시 실시간으로 라이선스를 재검증하기 위해 Windows 이벤트 콜백과 &lt;span class=&quot;inline-em&quot;&gt;LicenseManager::verifyLicense()&lt;/span&gt; 함수를 Signal/Slot으로 연결했다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1773381329832&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;QObject::connect(&amp;amp;usbNotifyWindow, SIGNAL(deviceArrived()), g_licenseManager, SLOT(verifyLicense()));
QObject::connect(&amp;amp;usbNotifyWindow, SIGNAL(deviceRemoved()), g_licenseManager, SLOT(verifyLicense()));&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;328&quot; data-start=&quot;26&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr data-end=&quot;79&quot; data-start=&quot;48&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;54&quot; data-start=&quot;48&quot;&gt;&lt;b&gt;버그명&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;79&quot; data-start=&quot;54&quot; data-col-size=&quot;md&quot;&gt;USB 이벤트 직후 라이선스 검증 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;141&quot; data-start=&quot;80&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;85&quot; data-start=&quot;80&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;141&quot; data-start=&quot;85&quot; data-col-size=&quot;md&quot;&gt;Windows 장치 콜백 이벤트가 Sentinel RTE 엔진보다 빨리 발생함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;190&quot; data-start=&quot;142&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;147&quot; data-start=&quot;142&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;190&quot; data-start=&quot;147&quot; data-col-size=&quot;md&quot;&gt;장치 인식 완료 전에 verifyLicense()가 호출될 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;226&quot; data-start=&quot;191&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;196&quot; data-start=&quot;191&quot;&gt;&lt;b&gt;영향&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;226&quot; data-start=&quot;196&quot; data-col-size=&quot;md&quot;&gt;정상 USB 동글이 &quot;동글없음&quot;으로 오인식 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;대응&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;윈도우 이벤트 발생 후 500ms 지연 후 검증 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 버그는 &lt;span class=&quot;inline-em&quot;&gt;LicenseManager::verifyLicenseDelayed()&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;b&gt;LicenseManager.h&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#ifndef&amp;nbsp;LICENSEMANAGER_H &lt;br /&gt;#define&amp;nbsp;LICENSEMANAGER_H &lt;br /&gt;&lt;br /&gt;#include&amp;nbsp;&amp;lt;QObject&amp;gt; &lt;br /&gt;#include&amp;nbsp;&amp;lt;QTimer&amp;gt; &lt;br /&gt;#include&amp;nbsp;&amp;lt;QElapsedTimer&amp;gt; &lt;br /&gt;&lt;br /&gt;#include&amp;nbsp;&quot;SentinelClient.h&quot; &lt;br /&gt;&lt;br /&gt;class&amp;nbsp;LicenseManager&amp;nbsp;:&amp;nbsp;public&amp;nbsp;QObject &lt;br /&gt;{ &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Q_OBJECT &lt;br /&gt;public: &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;explicit&amp;nbsp;LicenseManager(QObject&amp;nbsp;*parent&amp;nbsp;=&amp;nbsp;nullptr); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;startTimer(); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;stopTimer(); &lt;br /&gt;&lt;br /&gt;signals: &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;licenseActivated(); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;licenseDeactivated(); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;licenseExpired(); &lt;br /&gt;&lt;br /&gt;public&amp;nbsp;slots: &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;verifyLicense(); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;verifyLicenseDelayed();&amp;nbsp;//&amp;nbsp;USB인식을&amp;nbsp;위해,&amp;nbsp;500ms&amp;nbsp;대기후&amp;nbsp;라이선스를&amp;nbsp;실행 &lt;br /&gt;&lt;br /&gt;private&amp;nbsp;slots: &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void&amp;nbsp;onWatchdogTimeout(); &lt;br /&gt;&lt;br /&gt;private: &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static&amp;nbsp;const&amp;nbsp;quint64&amp;nbsp;k_interval&amp;nbsp;=&amp;nbsp;30*&amp;nbsp;60&amp;nbsp;*&amp;nbsp;1000;&amp;nbsp;//&amp;nbsp;라이선스&amp;nbsp;검사&amp;nbsp;주기 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;static&amp;nbsp;const&amp;nbsp;quint64&amp;nbsp;k_expire&amp;nbsp;=&amp;nbsp;30&amp;nbsp;*&amp;nbsp;60&amp;nbsp;*&amp;nbsp;1000;&amp;nbsp;//&amp;nbsp;라이선스가&amp;nbsp;없을 때&amp;nbsp;유지시간 &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Sentinel::SentinelClient&amp;nbsp;m_sentinelClient; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QTimer&amp;nbsp;m_watchdog; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QElapsedTimer&amp;nbsp;m_expireTimer; &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool&amp;nbsp;tryLicenseLogin(); &lt;br /&gt;}; &lt;br /&gt;&lt;br /&gt;extern&amp;nbsp;LicenseManager*&amp;nbsp;g_licenseManager; &lt;br /&gt;&lt;br /&gt;#endif&amp;nbsp;//&amp;nbsp;LICENSEMANAGER_H&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LicenseManager.cpp&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1773382516992&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;LicenseManager.h&quot;

using namespace Sentinel;

LicenseManager* g_licenseManager = NULL;

LicenseManager::LicenseManager(QObject *parent) :
    QObject(parent)
{
    m_watchdog.setInterval(k_interval);
    connect(&amp;amp;m_watchdog, &amp;amp;QTimer::timeout, this, &amp;amp;LicenseManager::onWatchdogTimeout);
}

void LicenseManager::startTimer()
{
    m_watchdog.start();
    verifyLicense();
}

void LicenseManager::stopTimer()
{
    m_watchdog.stop();
}

void LicenseManager::verifyLicense()
{
    if (tryLicenseLogin()) {
        if (m_expireTimer.isValid()) {
            m_expireTimer.invalidate();
            emit licenseActivated();
        }
    } else {
        if (!m_expireTimer.isValid()) {
            m_expireTimer.start();
            emit licenseDeactivated();
        } else {
            if (m_expireTimer.hasExpired(k_expire)) {
                emit licenseExpired();
            }
        }
    }

    if (m_watchdog.isActive()) {
        m_watchdog.start();
    }
}

void LicenseManager::verifyLicenseDelayed()
{
    QTimer::singleShot(500, this, SLOT(verifyLicense()));
}

void LicenseManager::onWatchdogTimeout()
{
    verifyLicense();
}

bool LicenseManager::tryLicenseLogin()
{
    if (m_sentinelClient.hasHandle(FeatureID::MulinStandard)) {
        return m_sentinelClient.checkAlive(FeatureID::MulinStandard);
    }

    return m_sentinelClient.login(FeatureID::MulinStandard);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel API를 래핑 하는 C++ 클래스 설계부터 GoogleTest 검증, 실제 Qt 프로젝트 연동 중 발생하는 타이밍 이슈까지 다뤘다.&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;USB 동글 메모리 구조와 보안 스택&lt;/b&gt;에 대해 새롭게 배웠고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel EMS를 통한 &lt;b&gt;라이선스&lt;/b&gt; &lt;b&gt;프로비저닝 방식&lt;/b&gt;은 산업용 소프트웨어 배포 구조에서 좋은 레퍼런스로 보인다.&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&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;&lt;a href=&quot;https://prejudice.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전글: 2026.02.12 - [개발] - USB 동글 보안 시스템 동작 원리 ㅡ KeyLock 보안 스택&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773389591478&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;USB 동글 보안 시스템 동작 원리 ㅡ KeyLock 보안 스택&quot; data-og-description=&quot;들어가며이번 프로젝트가 Open Beta 출시를 앞두고, HW USB 동글을 활용한 보안 기능 개발을 담당하게 되었다. 프로젝트는 MuLiN이라는 SW PLC로,WindowsLinuxARM 기반 HW환경에서 동작하며,USB 동글이 꽂혀 &quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/29&quot; data-og-url=&quot;https://prejudice.tistory.com/29&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TTyx7/dJMb9lL94aW/91ZjAAwJfHKjChnllpfeF0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bp8inf/dJMb9dHmAE7/E4xKSdcgWOSCsEHmm4Vsr1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dPRLEV/dJMb89517NZ/g3HCFKewR650oES1UzHO10/img.png?width=1168&amp;amp;height=304&amp;amp;face=0_0_1168_304&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/29&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TTyx7/dJMb9lL94aW/91ZjAAwJfHKjChnllpfeF0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bp8inf/dJMb9dHmAE7/E4xKSdcgWOSCsEHmm4Vsr1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dPRLEV/dJMb89517NZ/g3HCFKewR650oES1UzHO10/img.png?width=1168&amp;amp;height=304&amp;amp;face=0_0_1168_304');&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;USB 동글 보안 시스템 동작 원리 ㅡ KeyLock 보안 스택&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며이번 프로젝트가 Open Beta 출시를 앞두고, HW USB 동글을 활용한 보안 기능 개발을 담당하게 되었다. 프로젝트는 MuLiN이라는 SW PLC로,WindowsLinuxARM 기반 HW환경에서 동작하며,USB 동글이 꽂혀&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/36&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글: 2026.03.04 - [개발] - HW USB 동글 보안 시스템 구축하기 - Sentinel EMS 프로비저닝 실무&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773389627248&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;HW USB 동글 보안 시스템 구축하기 - Sentinel EMS 프로비저닝 실무&quot; data-og-description=&quot;들어가며지난 글에서 USB 동글 보안 원리를 학습하고, Sentinel 동글 솔루션의 구조를 분석했다.이번 글에서는 한 단계 더 나아가 실제 Sentinel USB 동글에 라이선스 정보를 저장해 보려고 한다. 최종 &quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/36&quot; data-og-url=&quot;https://prejudice.tistory.com/36&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBQ9Vw/dJMb85vNsBK/w3Ef87Ghemuaf2ztnHbvB1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/SQkSK/dJMb88eZayQ/HFp6kpN2LJxaDNOgBOZkWk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ZZaVG/dJMb9eTOdcZ/PKpGIu5ZJFIAwyRyF06TVK/img.png?width=991&amp;amp;height=709&amp;amp;face=0_0_991_709&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/36&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/36&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBQ9Vw/dJMb85vNsBK/w3Ef87Ghemuaf2ztnHbvB1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/SQkSK/dJMb88eZayQ/HFp6kpN2LJxaDNOgBOZkWk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ZZaVG/dJMb9eTOdcZ/PKpGIu5ZJFIAwyRyF06TVK/img.png?width=991&amp;amp;height=709&amp;amp;face=0_0_991_709');&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;HW USB 동글 보안 시스템 구축하기 - Sentinel EMS 프로비저닝 실무&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며지난 글에서 USB 동글 보안 원리를 학습하고, Sentinel 동글 솔루션의 구조를 분석했다.이번 글에서는 한 단계 더 나아가 실제 Sentinel USB 동글에 라이선스 정보를 저장해 보려고 한다. 최종&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.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;</description>
      <category>개발</category>
      <category>C++</category>
      <category>FactoryAutomation</category>
      <category>googletest</category>
      <category>MuLiN</category>
      <category>PLC</category>
      <category>QT</category>
      <category>sentinel</category>
      <category>unittest</category>
      <category>USB동글</category>
      <category>라이선스보안</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/37</guid>
      <comments>https://prejudice.tistory.com/37#entry37comment</comments>
      <pubDate>Fri, 13 Mar 2026 17:27:39 +0900</pubDate>
    </item>
    <item>
      <title>HW USB 동글 보안 시스템 구축하기 - Sentinel EMS 프로비저닝 실무</title>
      <link>https://prejudice.tistory.com/36</link>
      <description>&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 USB 동글 보안 원리를 학습하고, Sentinel 동글 솔루션의 구조를 분석했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 한 단계 더 나아가&amp;nbsp;&lt;b&gt;실제 Sentinel USB 동글에 라이선스 정보를 저장해 보려고 한다.&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;최종 목표는 현재 개발 중인 Software PLC 시스템에 &lt;b&gt;USB 동글 기반 라이선스 인증 기능&lt;/b&gt;을 추가하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글 : 2026.02.12 - [개발] - USB 동글 보안 시스템 동작 원리 ㅡ KeyLock 보안 스택&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772590695939&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;USB 동글 보안 시스템 동작 원리 ㅡ KeyLock 보안 스택&quot; data-og-description=&quot;들어가며이번 프로젝트가 Open Beta 출시를 앞두고, HW USB 동글을 활용한 보안 기능 개발을 담당하게 되었다. 프로젝트는 MuLiN이라는 SW PLC로,WindowsLinuxARM 기반 HW환경에서 동작하며,USB 동글이 꽂혀 &quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/29&quot; data-og-url=&quot;https://prejudice.tistory.com/29&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/E1e3Y/dJMb8WMndZK/uMKBAVvk1Ig6NWuxVsk5F0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UAdTI/dJMb8WexhRn/R3mUrgF4gCvwrGYAQj3eFK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/siGLl/dJMb8SXvqGS/kZCxPABZW7lAX7JCIyRVfK/img.png?width=1168&amp;amp;height=304&amp;amp;face=0_0_1168_304&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/29&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/E1e3Y/dJMb8WMndZK/uMKBAVvk1Ig6NWuxVsk5F0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UAdTI/dJMb8WexhRn/R3mUrgF4gCvwrGYAQj3eFK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/siGLl/dJMb8SXvqGS/kZCxPABZW7lAX7JCIyRVfK/img.png?width=1168&amp;amp;height=304&amp;amp;face=0_0_1168_304');&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;USB 동글 보안 시스템 동작 원리 ㅡ KeyLock 보안 스택&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며이번 프로젝트가 Open Beta 출시를 앞두고, HW USB 동글을 활용한 보안 기능 개발을 담당하게 되었다. 프로젝트는 MuLiN이라는 SW PLC로,WindowsLinuxARM 기반 HW환경에서 동작하며,USB 동글이 꽂혀&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;기존 동글 시스템 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 시작하기 전에 기존 HMI 서비스에서 사용 중인 USB 동글 구조를 먼저 분석했다.&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;Feature ID = &lt;b&gt;1&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;USB 동글 메모리에 &lt;b&gt;Application 고유 데이터 저장&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기반으로 새로운 PLC 서비스용 동글 설계를 위해 다음과 같은 체크리스트를 작성했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;☑ 기존 HMI용 USB와 &lt;b&gt;호환성 유지&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;☑ PLC 서비스용 &lt;b&gt;Feature ID = 2 사용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;☑ Feature 1 + Feature 2가 함께 존재하면 &lt;b&gt;HMI + PLC 통합 동글로 동작&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;☑ PLC 서비스만의 &lt;b&gt;고유 데이터 저장&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;USB 동글 보안 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel에서 제공하는 &lt;b&gt;hasp 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;License Manager : 실제 라이선스 로직 처리&lt;/li&gt;
&lt;li&gt;SentinelClient : Sentinel API호출 담당 및 &lt;b&gt;동글 데이터 접근&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel Client 클래스 설계를 통해 동글에 &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;741&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IpYPW/dJMb99ZP9sr/A3yVCjmyU10Lv4WWPyS7Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IpYPW/dJMb99ZP9sr/A3yVCjmyU10Lv4WWPyS7Z0/img.png&quot; data-alt=&quot;최소한의 기능만 갖춘 SentinelClient Class 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IpYPW/dJMb99ZP9sr/A3yVCjmyU10Lv4WWPyS7Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIpYPW%2FdJMb99ZP9sr%2FA3yVCjmyU10Lv4WWPyS7Z0%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;741&quot; height=&quot;336&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최소한의 기능만 갖춘 SentinelClient Class 설계&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Sentinel EMS 프로비저닝 환경 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel은 USB 동글 관리 및 라이서스 발급을 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EMS(Entitlement Management System)&lt;/b&gt; 라는 웹 기반 관리 시스템을&amp;nbsp;제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EMS 설치 후 &lt;span class=&quot;inline-em&quot;&gt;http://[ip]:8080/ems&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;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Feature 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 PLC 서비스에서 사용할 &lt;b&gt;Feature ID&lt;/b&gt;를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;inline-em&quot;&gt;Catalog-Features-NewFeatur&lt;/span&gt; 버튼으로 Feature ID = 2 를 등록했다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oGE7H/dJMcahwNcs0/hOQTppMik6VrlIp8vZkkFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oGE7H/dJMcahwNcs0/hOQTppMik6VrlIp8vZkkFk/img.png&quot; data-alt=&quot;새로운 서비스로 사용할 Feature&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oGE7H/dJMcahwNcs0/hOQTppMik6VrlIp8vZkkFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoGE7H%2FdJMcahwNcs0%2FhOQTppMik6VrlIp8vZkkFk%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;515&quot; height=&quot;181&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새로운 서비스로 사용할 Feature&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*이미 상품화되어 사용중인 Feature가 있다면, 삭제 또는 수정에 주의하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6ZkFZ/dJMb99ZQaxV/wZcKN4bpnwQuT18w7bIMcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6ZkFZ/dJMb99ZQaxV/wZcKN4bpnwQuT18w7bIMcK/img.png&quot; data-alt=&quot;새로운 Feature가 추가된 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6ZkFZ/dJMb99ZQaxV/wZcKN4bpnwQuT18w7bIMcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6ZkFZ%2FdJMb99ZQaxV%2FwZcKN4bpnwQuT18w7bIMcK%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;660&quot; height=&quot;218&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새로운 Feature가 추가된 상태&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;h3 data-ke-size=&quot;size23&quot;&gt;Dynamic Memory 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel USB 동글 메모리를 사용하는 방법은 두 가지가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fixed Memosy : 주소 기반으로 메모리를 직접 지정 (start address + size)&lt;/li&gt;
&lt;li&gt;Dynamic Memory : 파일 형태로 접근 (File ID 기반 접근)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 HMI 시스템은 Fixed Memory를 사용하고 있었지만 테스트를 위해 Dynamic Memory 방식을 사용해 보기로 했다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;inline-em&quot;&gt;Catalog-DynamicMemory-New Memory&lt;/span&gt; 를 선택하여&amp;nbsp; 100 Bytes의 크기를 갖도록 생성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xgOXO/dJMb99ZQfTe/Kng3CjnOFRjxmwIHPsJg11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xgOXO/dJMb99ZQfTe/Kng3CjnOFRjxmwIHPsJg11/img.png&quot; data-alt=&quot;Dynamic Memory 생성 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xgOXO/dJMb99ZQfTe/Kng3CjnOFRjxmwIHPsJg11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxgOXO%2FdJMb99ZQfTe%2FKng3CjnOFRjxmwIHPsJg11%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;632&quot; height=&quot;524&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Dynamic Memory 생성 화면&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;h3 data-ke-size=&quot;size23&quot;&gt;Product 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sentinel EMS에서 &lt;b&gt;Product&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;Feature&lt;/li&gt;
&lt;li&gt;Memory&lt;/li&gt;
&lt;li&gt;License 정책&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 PLC 시스템을 위해 MuLiN_Standard 라는 신규 Product를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 조금전 생성한 Feature2 와 Dynamic Memory를 등록했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cD4vNL/dJMcafFJJvr/HAeCpEKLo1XNZyzcFBV6F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cD4vNL/dJMcafFJJvr/HAeCpEKLo1XNZyzcFBV6F0/img.png&quot; data-alt=&quot;PLC Software용 Product 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cD4vNL/dJMcafFJJvr/HAeCpEKLo1XNZyzcFBV6F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcD4vNL%2FdJMcafFJJvr%2FHAeCpEKLo1XNZyzcFBV6F0%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;620&quot; height=&quot;514&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;821&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PLC Software용 Product 생성&lt;/figcaption&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;Product 설계 시 패키지구성이 중요한데 동글을 따로 팔기 위해서는 &lt;b&gt;각각의 Product를 가지도록 구성&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;span class=&quot;inline-em&quot;&gt;ProductA = Word + PPT + Excel&lt;/span&gt;&amp;nbsp; 로 구성할 경우, 각각의 제품을 USB로 제작할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 각각의 제품을 팔기 위해서는 다음과 같이 구성한 뒤, &lt;b&gt;USB 생산 과정에서 조합하는 방식&lt;/b&gt;이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;inline-em&quot;&gt;ProductA = Word&lt;/span&gt;,&lt;span class=&quot;inline-em&quot;&gt;ProductB = PPT&lt;/span&gt;,&lt;span class=&quot;inline-em&quot;&gt;ProductC = Excel&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;프로비저닝 버닝 작업&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Entitlements 생성 (주문서)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2gsb/dJMcaaLbXwh/oOkNQFqe7HXBzDFqZSchlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2gsb/dJMcaaLbXwh/oOkNQFqe7HXBzDFqZSchlK/img.png&quot; data-alt=&quot;USB 동글 시스템&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2gsb/dJMcaaLbXwh/oOkNQFqe7HXBzDFqZSchlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2gsb%2FdJMcaaLbXwh%2FoOkNQFqe7HXBzDFqZSchlK%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;540&quot; height=&quot;284&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;USB 동글 시스템&lt;/figcaption&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;Sentinel에서는 실제 USB 동글 생산을 &lt;b&gt;Entitlement (주문서)&lt;/b&gt; 단위로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 프로비저닝 환경구성이 끝났다면, &lt;b&gt;제조/운영 환경은 Entitlement 로 관리&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;span class=&quot;inline-em&quot;&gt;Entitlement-Entitlements-New Entitlement&lt;/span&gt; 로 새 주문서를 생성하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Product Details탭에서 새로 만든 MuLiN_Standard Product를 추가하고 Produce로 주문서를 발행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;*기존에 있던 HMI 서비스 권한과 새로 만든 PLC 서비스 권한을 가진 USB제품을 다음과 같이 제작할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;&lt;b&gt;All-In-One USB = HMI Product + PLC Product&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;단, 두 Product 간 고정메모리 영역을 사용할 경우 데이터가 덮어써지지 않도록 주의해야 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QxhQ5/dJMcacWwwoI/Ah53tsKAZPmKHpQIKwkjW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QxhQ5/dJMcacWwwoI/Ah53tsKAZPmKHpQIKwkjW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QxhQ5/dJMcacWwwoI/Ah53tsKAZPmKHpQIKwkjW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQxhQ5%2FdJMcacWwwoI%2FAh53tsKAZPmKHpQIKwkjW1%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;991&quot; height=&quot;709&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;709&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;h3 data-ke-size=&quot;size23&quot;&gt;USB Burn 작업&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문서 생성 후 실제 USB 동글을 연결 하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EMS에서 바로 &lt;b&gt;Burn 작업&lt;/b&gt;을 진행할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;text-align: center; caret-color: transparent; letter-spacing: 0px;&quot; src=&quot;https://blog.kakaocdn.net/dna/b3Ii23/dJMcagYVJcl/AAAAAAAAAAAAAAAAAAAAAL7S08map56Us_CsvNDXAp_rpptnu_D6H6Q0oIl5u5RZ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=LpU7xiDZHei9%2BMwrIDqg48q4ljw%3D&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;609&quot; data-is-animation=&quot;false&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;예상치 못한 문제&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;바로 Burn 작업을 진행했으나 회사에서 사용하는 &lt;span style=&quot;color: #ee2323;&quot;&gt;HL Pro 동글 모델은 Dynamic Memory를 지원하지 않았다.&lt;br /&gt;&lt;/span&gt;결국 임시로 &lt;span style=&quot;color: #ee2323;&quot;&gt;메모리 기능 없이 Feature만 기록&lt;/span&gt;해 USB를 구웠다...&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;inline-em&quot;&gt;Entitlements-Check In Key-Check In&lt;/span&gt; 로 동글 정보(Feature, Memory)를 확인할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&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;842&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tCCWJ/dJMcahQ4Ru2/mT43qekHCiOFk4dLzEYLB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tCCWJ/dJMcahQ4Ru2/mT43qekHCiOFk4dLzEYLB0/img.png&quot; data-alt=&quot;USB 동글에 구워진 MuLiN_Standard 정보&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tCCWJ/dJMcahQ4Ru2/mT43qekHCiOFk4dLzEYLB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtCCWJ%2FdJMcahQ4Ru2%2FmT43qekHCiOFk4dLzEYLB0%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;653&quot; height=&quot;472&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;USB 동글에 구워진 MuLiN_Standard 정보&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;style5&quot; /&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&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;827&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tadnr/dJMcabi3ATy/RF2DqDosQV7vVx79mx23p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tadnr/dJMcabi3ATy/RF2DqDosQV7vVx79mx23p1/img.png&quot; data-alt=&quot;완성된 보안 USB 동글&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tadnr/dJMcabi3ATy/RF2DqDosQV7vVx79mx23p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTadnr%2FdJMcabi3ATy%2FRF2DqDosQV7vVx79mx23p1%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;140&quot; height=&quot;149&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완성된 보안 USB 동글&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Sentinel EMS를 사용해 USB 동글을 실제로 프로비저닝 하는 과정을 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 동글 보안 구조를 공부했기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feature, Product, Entitlement 개념을 이해하는데 큰 도움이 되었다.&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;&lt;b&gt;개발자가 어디까지 담당해야 하는지&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 회사의 경우, ERP 주문관리 및 USB제작은 생산팀이 담당하는데&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;다음 글에서는 실제로 제작한 USB 동글을 이용 해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라이선스를 검증하는 C++ 클래스를 구현&lt;/b&gt;하는 과정을 정리해 보려고 한다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;.inline-em{  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;  background:#2b2f36; color:#fff;  border:1px solid rgba(255,255,255,0.14);  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;  font-size:0.95em; line-height:1.35;}&lt;/style&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;</description>
      <category>개발</category>
      <category>HaspAPI</category>
      <category>KeyLock</category>
      <category>sentinel</category>
      <category>SentinelEMS</category>
      <category>USB동글</category>
      <category>라이선스</category>
      <category>보안</category>
      <category>소프트웨어라이선스</category>
      <category>프로비저닝</category>
      <category>하드웨어보안</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/36</guid>
      <comments>https://prejudice.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 4 Mar 2026 20:48:57 +0900</pubDate>
    </item>
    <item>
      <title>프로그래밍 책 추천 - 코딩의 깊이를 더해준 인생 최고의 개발자 책 추천 BEST 3</title>
      <link>https://prejudice.tistory.com/35</link>
      <description>&lt;div&gt;
&lt;style&gt;
.inline-em{
  display:inline-block; padding:0.08em 0.45em; border-radius:0.35em;
  background:#2b2f36; color:#fff;
  border:1px solid rgba(255,255,255,0.14);
  font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Courier New&quot;,monospace;
  font-size:0.95em; line-height:1.35;
}
&lt;/style&gt;
&lt;/div&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C언어로 처음 프로그래밍을 처음 시작했던&amp;nbsp;중학생 때를 떠올려 보니, 어느덧 14년이라는 시간이 흘렀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;14년 동안 공부하면서 대학, 대학원, 국비지원 학원의 교육 과정을 거쳤고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 개발 경력도 만 3년이 돼가는 시점에서 &lt;b&gt;그동안 읽었던 최고의 IT 도서&lt;/b&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;/p&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;&lt;b&gt;C언어&lt;/b&gt; : 프로그래밍 기본 개념 (절차적 언어의 특성, 함수, 구조체 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;C++&lt;/b&gt; : 언어에 대한 확장 (객체지향의 도입)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JAVA&lt;/b&gt; : 객체지향 개념에 대한 확장 (상속, 캡슐화, 라이브러리) 및 JVM 개념&lt;/li&gt;
&lt;li&gt;&lt;b&gt;C++ &lt;/b&gt;: 메모리 관리 개념에 대한 심화 (포인터 &amp;amp; Modern C++)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Python, Kotlin, C#, JavaScript&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;객체지향의 사실과 오해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 내가 가지고 있는 &quot;객체 지향&quot;에 대한 개념을 확립해 준 책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머가 지향해야할 설계 방법을 현실에 기반한 예시로 알기 쉽게 설명해 준다.&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;UML작성, 아키텍처 설계 등에 있어 &quot;객체 지향&quot;으로 사고하는 방법&lt;/b&gt;을 알려주며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나아가 &lt;b&gt;CS(컴퓨터 과학)를 공부할 때 모든 개념을 객체로 상상할 수 있는 혜안을 준다&lt;/b&gt;는 점에서 강력 추천한다.&lt;/p&gt;
&lt;figure id=&quot;og_1772152914211&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;객체지향의 사실과 오해_2023.01.01&quot; data-og-description=&quot;모든 프로그래머라면 읽어보는 것을 추천하는 책. 객체지향에 대한 전반적인 개념을 깔끔하게 정리하여 좋았다.일명 토끼책으로 유명한 책이다. 객체지향 프로그래밍에 대해 전반적인 내용을..&quot; data-og-host=&quot;blog.aladin.co.kr&quot; data-og-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot; data-og-url=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bcrCrP/dJMb9iIElLv/WjWXWcsyDT3wUkKKKwl8e1/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273,https://scrap.kakaocdn.net/dn/h5VZe/dJMb87f3FwK/dIBCaRkNeGmU2scoEzIZkk/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273&quot;&gt;&lt;a href=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bcrCrP/dJMb9iIElLv/WjWXWcsyDT3wUkKKKwl8e1/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273,https://scrap.kakaocdn.net/dn/h5VZe/dJMb87f3FwK/dIBCaRkNeGmU2scoEzIZkk/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273');&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;객체지향의 사실과 오해_2023.01.01&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;blog.aladin.co.kr&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 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;패턴 그리고 객체지향적 코딩의 법칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출퇴근하며 가볍게 읽기 좋은 책&lt;/b&gt;이다. 디자인 패턴에 대해 스토리 텔링 형식으로 풀어낸 글이 기억에 남는다.&lt;/p&gt;
&lt;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;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;예제 코드가 C++ 기반으로 작성되어 있어 프로그래밍에 대한 기본적인 지식이 필요하다.&lt;/p&gt;
&lt;figure id=&quot;og_1772152265092&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;패턴 그리고 객체지향적 코딩의 법칙_2025.11.15&quot; data-og-description=&quot;다양한 디자인패턴을 알아보고, 객체지향의 눈높이를 올릴 수 있는 책출퇴근하며 간만에 재미있게 읽은 컴퓨터 책이다. C++기반으로 작성된 다양한 디자인 패턴을 보면서 OOP에 대한 ...&quot; data-og-host=&quot;blog.aladin.co.kr&quot; data-og-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866197&quot; data-og-url=&quot;https://blog.aladin.co.kr/Bbird/16866197&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xiVAm/dJMb88eXS0o/sAqz4WWrbmjJPTfEd7pizK/img.jpg?width=200&amp;amp;height=288&amp;amp;face=0_0_200_288,https://scrap.kakaocdn.net/dn/dh7lfX/dJMb85vL9bi/kd4HkEWcTwqlXb1HBjk3k1/img.jpg?width=200&amp;amp;height=288&amp;amp;face=0_0_200_288&quot;&gt;&lt;a href=&quot;https://blog.aladin.co.kr/Bbird/16866197&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866197&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xiVAm/dJMb88eXS0o/sAqz4WWrbmjJPTfEd7pizK/img.jpg?width=200&amp;amp;height=288&amp;amp;face=0_0_200_288,https://scrap.kakaocdn.net/dn/dh7lfX/dJMb85vL9bi/kd4HkEWcTwqlXb1HBjk3k1/img.jpg?width=200&amp;amp;height=288&amp;amp;face=0_0_200_288');&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;패턴 그리고 객체지향적 코딩의 법칙_2025.11.15&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;다양한 디자인패턴을 알아보고, 객체지향의 눈높이를 올릴 수 있는 책출퇴근하며 간만에 재미있게 읽은 컴퓨터 책이다. C++기반으로 작성된 다양한 디자인 패턴을 보면서 OOP에 대한 ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.aladin.co.kr&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 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;클린 코드 Clean Code&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;읽으면서 협업에 대한 생각과 코딩 스타일을 발전시켰던 책이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;네이밍 룰, 함수, 주석, 객체지향과 프로그래밍 이론까지 다양한 범위를 커버하는 꽤 두꺼운 책으로,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;책상 한편에 두고 필요한 상황마다 특정 부분만 발췌해서 읽기 좋다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;현재 자신이 겪고 있는 상황이나 배경지식에 따라 받아들이는 깊이가 다르며, 현업에 바로 적용할 수 있는 내용들로 구성되어 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1772159476698&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;클린 코드Clean Code_2024.01.01&quot; data-og-description=&quot;프로그래머라면 두고두고 읽으며, 협업에 대한 생각과 코딩스타일을 발전해 나갈 수 있다. 얼마나 알고있는지에 따라 새롭게 배우고 익히는 코드 리팩토링 기본서.가독성이 좋고 의미있는 ...&quot; data-og-host=&quot;blog.aladin.co.kr&quot; data-og-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866364&quot; data-og-url=&quot;https://blog.aladin.co.kr/Bbird/16866364&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bithF1/dJMb9kl92nd/HQygHhtRCfElVEeas3XOw1/img.jpg?width=200&amp;amp;height=263&amp;amp;face=0_0_200_263,https://scrap.kakaocdn.net/dn/lDLaQ/dJMb9iIElKa/qffu1okjLgxFvvATTGl9ak/img.jpg?width=200&amp;amp;height=263&amp;amp;face=0_0_200_263&quot;&gt;&lt;a href=&quot;https://blog.aladin.co.kr/Bbird/16866364&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866364&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bithF1/dJMb9kl92nd/HQygHhtRCfElVEeas3XOw1/img.jpg?width=200&amp;amp;height=263&amp;amp;face=0_0_200_263,https://scrap.kakaocdn.net/dn/lDLaQ/dJMb9iIElKa/qffu1okjLgxFvvATTGl9ak/img.jpg?width=200&amp;amp;height=263&amp;amp;face=0_0_200_263');&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;클린 코드Clean Code_2024.01.01&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;blog.aladin.co.kr&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 다른 좋은 책들도 많지만 &lt;span class=&quot;inline-em&quot;&gt;운영체제 공룡책&lt;/span&gt; 이나 &lt;span class=&quot;inline-em&quot;&gt;Effective C++&lt;/span&gt; 같은 책이 없어 의아할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 두 책 모두 중도 하차한 책이고, 일단 재미가 없어서 다 읽어도 평점 5점을 주기는 어려웠을 거라 생각한다.&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;별점 4점을 준 책 중에는 &lt;span class=&quot;inline-em&quot;&gt;테스트 주도 개발-켄트 벡&lt;/span&gt; 도 있다. TDD에 대한 저자의 생각(작은 Task도 단위 테스트를 먼저 작성)과 나의 생각(필요한 부분에 필요한 테스트)이 다르다는 점이 그 이유이다.&lt;/p&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;글에서 추천한 세 가지 책은 나만의 별점 5점을 준 책들로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;동료 개발자나 친구들이 꼭 한번쯤 읽어봤으면 하는 책들&lt;/b&gt;을 골라보았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;선정 기준은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&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;또한 중요하게 고려했으며,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;읽고 난 뒤 실제로 사고방식과 코드가 달라졌다고 느낀 책들이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;앞으로도 개발자들이 읽을만한 깊이있고 재미있는 책이 많아졌으면 좋겠다.&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;추천할만한 책이나 궁금한 책이 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 읽어보고 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif; color: #333333; text-align: start;&quot;&gt;오히려 요즘엔 인문학 책을 읽어보려 노력하고 있는데, 가끔 인문학 책에서 업무 관리 방법이나 프로그래밍 철학 등 개발자로서 깊게 생각해 볼 만한 인사이트를 얻는 경우가 많음을 느낀다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <category>IT도서추천</category>
      <category>개발자</category>
      <category>개발자책추천</category>
      <category>객체지향</category>
      <category>객체지향의사실과오해</category>
      <category>디자인패턴</category>
      <category>클린코드</category>
      <category>프로그래머</category>
      <category>프로그래머책추천</category>
      <category>프로그래밍책</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/35</guid>
      <comments>https://prejudice.tistory.com/35#entry35comment</comments>
      <pubDate>Fri, 27 Feb 2026 14:34:27 +0900</pubDate>
    </item>
    <item>
      <title>AI와 공장 자동화(FA)연동 따라하기 - PLC 예제로 구현한 MCP</title>
      <link>https://prejudice.tistory.com/34</link>
      <description>&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AI가 빠르게 발달하면서 FA(Factory Automation) 업계에서도 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;현장에서는 &quot;우리 공장 설비(서비스)를 AI와 연동할 수 있을까?&quot;에 대한 수요가 늘어가고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 따라 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;이 문제를 해결하기 위한 하나의 방법으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic에서 제안한 MCP(Model Context Protocol)을 공부해 보았다.&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;MCP의 개념을 살펴보고, &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;간단한 PLC 시스템 예시&lt;/b&gt;로 MCP를 어떻게 적용할 수 있을지 정리해 보려 한다.&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;MCP (Model Context Protocol) 이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP란 &lt;b&gt;AI 모델과 외부 데이터 또는 도구를 연결하기 위한 오픈 소스 프로토콜&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;Cluade나 Gemini, ChatGPT 같은 생성형 AI는 텍스트 생성에는 뛰어나지만,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공장 설비를 직접 제어하거나&lt;/li&gt;
&lt;li&gt;PLC 자원을 변경하거나&lt;/li&gt;
&lt;li&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;p data-ke-size=&quot;size16&quot;&gt;만약 AI가 PC의 시스템 도구들을 자유롭게 다룰 수 있다면, 다음과 같이 지시할&amp;nbsp; 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;내 PC에 있는 C:\ledOnOff.exe 프로그램에 &quot;true&quot; 혹은 &quot;false&quot; 변수를 입력하면 LED를 제어할 수 있어, LED를 켜봐&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;이처럼 AI가 이런 도구를 사용할 수 있도록 &lt;b&gt;시스템과 AI 사이의 다리 역할&lt;/b&gt;을 하는 것이 MCP이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;MCP Architecture&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP프로토콜은 기본적으로 &lt;b&gt;Server-Client 구조&lt;/b&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;MCP Server: 도구를 제공하고 실행하는 서비스 (ex: PLC 제어 프로그램)&lt;/li&gt;
&lt;li&gt;MCP Client: 도구를 사용하는 주체(AI Model).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/moTNP/dJMcacIXZtb/jvT2LuyWQMBwdeMCHSFVHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/moTNP/dJMcacIXZtb/jvT2LuyWQMBwdeMCHSFVHk/img.png&quot; data-alt=&quot;Server-Client 구조의 MCP Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/moTNP/dJMcacIXZtb/jvT2LuyWQMBwdeMCHSFVHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmoTNP%2FdJMcacIXZtb%2FjvT2LuyWQMBwdeMCHSFVHk%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;551&quot; height=&quot;245&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Server-Client 구조의 MCP Architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;LLM Model :&lt;/b&gt; 텍스트를 생성하는 추론엔진. ex) GPT, Claude, Gemini&amp;nbsp;&lt;br /&gt;&lt;b&gt;LLM Agent :&lt;/b&gt;&amp;nbsp;LLM Model + 도구(파일/터미널/프로그램 등) + 상태/메모리로 목표를 달성하는 시스템.&lt;br /&gt;&lt;b&gt;Agent 기반 IDE :&lt;/b&gt; 통합 개발 환경.&amp;nbsp;ex) Cursor, Antigravity, VSCode(Agent 확장 시)&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;MCP 예제 설계: FA산업에 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;산업 자동화 환경에서 AI를 활용한다고 가정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업자가 동료에게 지시하듯, &lt;b&gt;AI에게 PLC 자원을 조작하도록 지시&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;이번 예제에서는 LLM 에이전트로 &lt;b&gt;Antigravity&lt;/b&gt; 환경을, LLM 모델로 &lt;b&gt;Gemini 3.1&lt;/b&gt;을 사용하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Soft PLC Application&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;551&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xUjUC/dJMcahDs0py/hwsU2Dgm5pl9h2lBwXtzE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xUjUC/dJMcahDs0py/hwsU2Dgm5pl9h2lBwXtzE1/img.png&quot; data-alt=&quot;PLC App과 AI서비스 Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xUjUC/dJMcahDs0py/hwsU2Dgm5pl9h2lBwXtzE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxUjUC%2FdJMcahDs0py%2FhwsU2Dgm5pl9h2lBwXtzE1%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;551&quot; height=&quot;245&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PLC App과 AI서비스 Architecture&lt;/figcaption&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;SoftPlcApp 개발 따라 하기&lt;/h2&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;PLC State 구조 설계&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PLC Memory는 최대한 간단하게 구성했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3색 경광등 상태와 생산 공정(Mixing &amp;rarr; Coatinga &amp;rarr; Winding &amp;rarr; Slitting) 상태를&lt;/b&gt;&amp;nbsp;메모리에 저장한다고 가정해 보자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 사용자가 LED를 어떻게 사용할지 모르니, 매칭해 놓은 Comment가 있다고 생각하자.&lt;/p&gt;
&lt;pre id=&quot;code_1772007888843&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Simulates the PLC State
class PlcState {
public:
    bool redLed = false;
    bool yellowLed = false;
    bool greenLed = false;
    
    std::string redComment = &quot;red: 재료가 부족하거나 생산이 불가능함 (에러/정지)&quot;;
    std::string yellowComment = &quot;yellow: 이차전지 생산공정이 생산 중 (가동 중)&quot;;
    std::string greenComment = &quot;green: 생산공정이 정상적으로 대기 중 (정상 대기)&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&lt;b&gt;MCP에서 가장 중요한 것은 AI에게 도구의 의미를 설명하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순히 &quot;스위치를 이용해 redLED를 켤 수 있어&quot;라고만 알려주면 AI는 단순한 스위치로 인식할 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;하지만&amp;nbsp;&lt;b&gt;&quot;redLED는 재료가 부족하거나 생산이 불가능한 상태를 의미해&quot;&lt;/b&gt;라고 명확히 알려준다면,&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;현재 PLC상태가 어때?&quot;라는 질문에 AI가 스스로 판단하여 LED 상태를 확인하고 답변할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;나아가 AI에게 더 많은 제어 권한을 준다면, 스스로 빨간불을 감지하고 자동으로 재료를 보충하는 수준의 고도화된 자동화도 가능할 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MCP Server 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MCP는 JSON-RPC 메시지 기반이며, 동기 방식의&amp;nbsp; 메서드 구조를 따른다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&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-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;initialize : 클라이언트가 서버에 연결할 때 필수적으로 최초 1회 보내는 요청. 서버의 기능들을 서로 교환한다.&lt;/li&gt;
&lt;li&gt;tools/list : 서버가 가진 도구(Tool)들의 설명서 설명서.&amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;ex) LED 켜기/끄기, 공정상태 변경&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;tools/call : LLM이 서버의 도구를 사용할 때 호출하는 기능.&amp;nbsp; &amp;nbsp; &lt;b&gt;ex) set_red_led(true), start_process(&quot;mixing&quot;)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;resources/list : 서버가 제공하는 읽기 전용 자원(Resource) 목록.&amp;nbsp; &amp;nbsp; &lt;b&gt;ex) LED 상태, 현재 프로세스 상태&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;resources/read : LLM이 실제 데이터를 읽어오기 위해 호출하는 기능.&amp;nbsp; &amp;nbsp; &lt;b&gt;ex) redLed(), currentProcess()&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞에서도 말했듯, &lt;b&gt;메서드에서 가장 중요한 설명은 &lt;span class=&quot;inline-em&quot;&gt;Description&lt;/span&gt;&amp;nbsp;태그를 이용해 AI에게 전달된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;/li&gt;
&lt;li&gt;어떤 자원을 읽어야 할지&lt;/li&gt;
&lt;li&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SoftPlcApp.exe 코드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;plc_state.hpp&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1772074204665&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#ifndef PLC_STATE_HPP
#define PLC_STATE_HPP

#include &amp;lt;string&amp;gt;

class PlcState {
public:
    PlcState() {
        update_leds_from_process();
    }

    bool redLed = false;
    bool yellowLed = false;
    bool greenLed = false;
    
    std::string redComment = &quot;red: 재료가 부족하거나 생산이 불가능함 (에러/정지)&quot;;
    std::string yellowComment = &quot;yellow: 이차전지 생산공정이 생산 중 (가동 중)&quot;;
    std::string greenComment = &quot;green: 생산공정이 정상적으로 대기 중 (정상 대기)&quot;;
    
    std::string currentProcess = &quot;Idle&quot;;

    bool set_current_process(const std::string&amp;amp; process) {
        if (process == &quot;mixing&quot; || process == &quot;coating&quot; || 
            process == &quot;winding&quot; || process == &quot;slitting&quot; || process == &quot;Idle&quot;) {
            currentProcess = process;
            update_leds_from_process();
            return true;
        }
        return false;
    }

private:
    void update_leds_from_process() {
        if (currentProcess == &quot;Idle&quot;) {
            greenLed = true;
            yellowLed = false;
        } else {
            greenLed = false;
            yellowLed = true;
        }
    }
};

#endif // PLC_STATE_HPP&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;mcp_server.hpp&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1772008642391&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#ifndef MCP_SERVER_HPP
#define MCP_SERVER_HPP

#include &amp;lt;string&amp;gt;
#include &amp;lt;nlohmann/json.hpp&amp;gt;

#include &quot;plc_state.hpp&quot;

class McpServer {
public:
    McpServer();

    // Process a JSON-RPC 2.0 request and return a JSON-RPC 2.0 response
    nlohmann::json handle_request(const nlohmann::json&amp;amp; request);

    // Provide access to the state for testing purposes
    const PlcState&amp;amp; get_state() const { return plc; }

private:
    PlcState plc;

    nlohmann::json handle_initialize(const nlohmann::json&amp;amp; request);
    nlohmann::json handle_tools_list(const nlohmann::json&amp;amp; request);
    nlohmann::json handle_tools_call(const nlohmann::json&amp;amp; request);
    nlohmann::json handle_resources_list(const nlohmann::json&amp;amp; request);
    nlohmann::json handle_resources_read(const nlohmann::json&amp;amp; request);

    // Helpers to create JSON-RPC responses
    nlohmann::json make_response(const nlohmann::json&amp;amp; id, const nlohmann::json&amp;amp; result);
    nlohmann::json make_error(const nlohmann::json&amp;amp; id, int code, const std::string&amp;amp; message);
};

#endif // MCP_SERVER_HPP&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;mcp_server.cpp&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1772026009244&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;mcp_server.hpp&quot;

using json = nlohmann::json;

McpServer::McpServer() {}

json McpServer::handle_request(const json&amp;amp; request) {
    if (!request.contains(&quot;jsonrpc&quot;) || request[&quot;jsonrpc&quot;] != &quot;2.0&quot;) {
        return make_error(nullptr, -32600, &quot;Invalid Request&quot;);
    }
    
    bool is_notification = !request.contains(&quot;id&quot;);
    json req_id = nullptr;
    if (!is_notification) {
        req_id = request[&quot;id&quot;];
    }

    if (!request.contains(&quot;method&quot;)) {
        return is_notification ? json() : make_error(req_id, -32600, &quot;Invalid Request: missing method&quot;);
    }

    std::string method = request[&quot;method&quot;];

    try {
        if (method == &quot;initialize&quot;) {
            return handle_initialize(request);
        } else if (method == &quot;notifications/initialized&quot;) {
            return json();
        } else if (method == &quot;tools/list&quot;) {
            return handle_tools_list(request);
        } else if (method == &quot;tools/call&quot;) {
            return handle_tools_call(request);
        } else if (method == &quot;resources/list&quot;) {
            return handle_resources_list(request);
        } else if (method == &quot;resources/read&quot;) {
            return handle_resources_read(request);
        } else {
            if (is_notification) {
                return json();
            }
            return make_error(req_id, -32601, &quot;Method not found&quot;);
        }
    } catch (const std::exception&amp;amp; e) {
        return make_error(req_id, -32603, &quot;Internal error: &quot; + std::string(e.what()));
    }
}

json McpServer::make_response(const json&amp;amp; id, const json&amp;amp; result) {
    return {
        {&quot;jsonrpc&quot;, &quot;2.0&quot;},
        {&quot;id&quot;, id},
        {&quot;result&quot;, result}
    };
}

json McpServer::make_error(const json&amp;amp; id, int code, const std::string&amp;amp; message) {
    return {
        {&quot;jsonrpc&quot;, &quot;2.0&quot;},
        {&quot;id&quot;, id},
        {&quot;error&quot;, {
            {&quot;code&quot;, code},
            {&quot;message&quot;, message}
        }}
    };
}

json McpServer::handle_initialize(const json&amp;amp; request) {
    // Simplistic initialize response
    json result = {
        {&quot;protocolVersion&quot;, &quot;2024-11-05&quot;},
        {&quot;capabilities&quot;, {
            {&quot;tools&quot;, json::object()},
            {&quot;resources&quot;, json::object()}
        }},
        {&quot;serverInfo&quot;, {
            {&quot;name&quot;, &quot;plc-mcp-server&quot;},
            {&quot;version&quot;, &quot;1.0.0&quot;}
        }}
    };
    return make_response(request[&quot;id&quot;], result);
}

json McpServer::handle_tools_list(const json&amp;amp; request) {
    auto make_led_tool = [](const std::string&amp;amp; tool_name, const std::string&amp;amp; description, const std::string&amp;amp; color_cap) -&amp;gt; json {
        return {
            {&quot;name&quot;, tool_name},
            {&quot;description&quot;, description},
            {&quot;inputSchema&quot;, {
                {&quot;type&quot;, &quot;object&quot;},
                {&quot;properties&quot;, {
                    {&quot;state&quot;, {
                        {&quot;type&quot;, &quot;boolean&quot;},
                        {&quot;description&quot;, &quot;True면 &quot; + color_cap + &quot; LED를 켭니다. False면 해당 LED를 끕니다.&quot;}
                    }}
                }},
                {&quot;required&quot;, {&quot;state&quot;}}
            }}
        };
    };

    json tools = json::array({
        make_led_tool(&quot;set_red_led&quot;, plc.redComment, &quot;Red&quot;),
        make_led_tool(&quot;set_yellow_led&quot;, plc.yellowComment, &quot;Yellow&quot;),
        make_led_tool(&quot;set_green_led&quot;, plc.greenComment, &quot;Green&quot;),
        {
            {&quot;name&quot;, &quot;start_process&quot;},
            {&quot;description&quot;, &quot;Start a factory automation process&quot;},
            {&quot;inputSchema&quot;, {
                {&quot;type&quot;, &quot;object&quot;},
                {&quot;properties&quot;, {
                    {&quot;process_name&quot;, {
                        {&quot;type&quot;, &quot;string&quot;},
                        {&quot;enum&quot;, {&quot;mixing&quot;, &quot;coating&quot;, &quot;winding&quot;, &quot;slitting&quot;, &quot;Idle&quot;}},
                        {&quot;description&quot;, &quot;The name of the process to start&quot;}
                    }}
                }},
                {&quot;required&quot;, {&quot;process_name&quot;}}
            }}
        }
    });
    
    json result = {
        {&quot;tools&quot;, tools}
    };
    return make_response(request[&quot;id&quot;], result);
}

json McpServer::handle_tools_call(const json&amp;amp; request) {
    if (!request.contains(&quot;params&quot;) || !request[&quot;params&quot;].contains(&quot;name&quot;)) {
        return make_error(request[&quot;id&quot;], -32602, &quot;Invalid params: missing tool name&quot;);
    }

    std::string tool_name = request[&quot;params&quot;][&quot;name&quot;];
    json arguments = request[&quot;params&quot;].value(&quot;arguments&quot;, json::object());

    json content = json::array();

    if (tool_name == &quot;set_red_led&quot; || tool_name == &quot;set_yellow_led&quot; || tool_name == &quot;set_green_led&quot;) {
        if (!arguments.contains(&quot;state&quot;)) {
            return make_error(request[&quot;id&quot;], -32602, &quot;Invalid arguments for &quot; + tool_name);
        }
        
        bool led_state = arguments[&quot;state&quot;];

        std::string color;
        if (tool_name == &quot;set_red_led&quot;) {
            plc.redLed = led_state;
            color = &quot;red&quot;;
        } else if (tool_name == &quot;set_yellow_led&quot;) {
            plc.yellowLed = led_state;
            color = &quot;yellow&quot;;
        } else if (tool_name == &quot;set_green_led&quot;) {
            plc.greenLed = led_state;
            color = &quot;green&quot;;
        }

        content.push_back({
            {&quot;type&quot;, &quot;text&quot;},
            {&quot;text&quot;, &quot;Successfully set &quot; + color + &quot; LED to &quot; + (led_state ? &quot;on&quot; : &quot;off&quot;)}
        });
    } else if (tool_name == &quot;start_process&quot;) {
        if (!arguments.contains(&quot;process_name&quot;)) {
            return make_error(request[&quot;id&quot;], -32602, &quot;Invalid arguments for start_process&quot;);
        }
        
        std::string proc = arguments[&quot;process_name&quot;];
        if (plc.set_current_process(proc)) {
            content.push_back({
                {&quot;type&quot;, &quot;text&quot;},
                {&quot;text&quot;, &quot;Successfully started process: &quot; + proc}
            });
        } else {
            return make_error(request[&quot;id&quot;], -32602, &quot;Unknown process_name&quot;);
        }
    } else {
        return make_error(request[&quot;id&quot;], -32601, &quot;Tool not found&quot;);
    }

    json result = {
        {&quot;content&quot;, content}
    };
    return make_response(request[&quot;id&quot;], result);
}

json McpServer::handle_resources_list(const json&amp;amp; request) {
    json resources = json::array({
        {
            {&quot;uri&quot;, &quot;plc://state&quot;},
            {&quot;name&quot;, &quot;PLC State&quot;},
            {&quot;description&quot;, &quot;Current state of the PLC including LEDs and active process&quot;},
            {&quot;mimeType&quot;, &quot;application/json&quot;}
        }
    });
    
    json result = {
        {&quot;resources&quot;, resources}
    };
    return make_response(request[&quot;id&quot;], result);
}

json McpServer::handle_resources_read(const json&amp;amp; request) {
    if (!request.contains(&quot;params&quot;) || !request[&quot;params&quot;].contains(&quot;uri&quot;)) {
        return make_error(request[&quot;id&quot;], -32602, &quot;Invalid params: missing uri&quot;);
    }

    std::string uri = request[&quot;params&quot;][&quot;uri&quot;];
    
    if (uri == &quot;plc://state&quot;) {
        json state_json = {
            {&quot;redLed&quot;, plc.redLed},
            {&quot;yellowLed&quot;, plc.yellowLed},
            {&quot;greenLed&quot;, plc.greenLed},
            {&quot;currentProcess&quot;, plc.currentProcess}
        };

        json contents = json::array({
            {
                {&quot;uri&quot;, uri},
                {&quot;mimeType&quot;, &quot;application/json&quot;},
                {&quot;text&quot;, state_json.dump()}
            }
        });

        json result = {
            {&quot;contents&quot;, contents}
        };
        return make_response(request[&quot;id&quot;], result);
    } else {
         return make_error(request[&quot;id&quot;], -32602, &quot;Resource not found&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;main.cpp&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1772008535027&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;mcp_server.hpp&quot;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;

using json = nlohmann::json;

int main() {
    McpServer server;
    std::string line;

    // Read JSON-RPC requests from standard input line by line
    while (std::getline(std::cin, line)) {
        if (line.empty()) continue;

        try {
            json request = json::parse(line);
            json response = server.handle_request(request);
            
            // Output JSON-RPC response to standard output followed by a newline
            if (!response.is_null()) {
                std::cout &amp;lt;&amp;lt; response.dump() &amp;lt;&amp;lt; std::endl;
            }
        } catch (const json::parse_error&amp;amp; e) {
            // Setup an error response simulating stdio JSON-RPC transport requirements
            json error_response = {
                {&quot;jsonrpc&quot;, &quot;2.0&quot;},
                {&quot;id&quot;, nullptr},
                {&quot;error&quot;, {
                    {&quot;code&quot;, -32700},
                    {&quot;message&quot;, &quot;Parse error&quot;}
                }}
            };
            std::cout &amp;lt;&amp;lt; error_response.dump() &amp;lt;&amp;lt; std::endl;
        }
    }

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;SoftPlcApp.exe 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 작성된 &lt;span class=&quot;inline-em&quot;&gt;main.cpp&lt;/span&gt; 를 보면 &lt;span class=&quot;inline-em&quot;&gt;std::cin&lt;/span&gt; 을 통해 표준 입력으로 요청을 받는 것을 알 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;/* 인라인 강조(파일명/명령어/키워드) */.inline-em {  display: inline-block;  padding: 0.08em 0.45em;  border-radius: 0.35em;  background: #2b2f36;          /* 회색 박스 */  color: #ffffff;               /* 흰 글자 */  border: 1px solid rgba(255,255,255,0.14);  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,               &quot;Liberation Mono&quot;, &quot;Courier New&quot;, monospace;  font-size: 0.95em;  line-height: 1.35;  vertical-align: baseline;}&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;프로그램 빌드 한 뒤,&lt;/span&gt; &lt;b&gt;PowerShell에서 직접 JSON 포맷으로 요청(request)하여 &lt;/b&gt;&lt;b&gt;응답 메시지&lt;/b&gt;를 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1772063966594&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PS C:\Projects\MCP&amp;gt; echo '{&quot;jsonrpc&quot;: &quot;2.0&quot;, &quot;id&quot;: 1, &quot;method&quot;: &quot;initialize&quot;}' | .\build\SoftPlcApp.exe&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRr8nO/dJMcaaEn4rq/Ce1lvklKQ5BxKuFQj1Ykdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRr8nO/dJMcaaEn4rq/Ce1lvklKQ5BxKuFQj1Ykdk/img.png&quot; data-alt=&quot;SoftPlcApp의 response 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRr8nO/dJMcaaEn4rq/Ce1lvklKQ5BxKuFQj1Ykdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRr8nO%2FdJMcaaEn4rq%2FCe1lvklKQ5BxKuFQj1Ykdk%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;1213&quot; height=&quot;39&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SoftPlcApp의 response 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 콘솔 파이프라인으로 테스트하는 것은 번거롭기 때문에, &lt;b&gt;gtest를 이용해 단위 테스트&lt;/b&gt;를 구성했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;client_test.cpp&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1772029754154&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;mcp_server.hpp&quot;
#include &amp;lt;gtest/gtest.h&amp;gt;
#include &amp;lt;nlohmann/json.hpp&amp;gt;

using json = nlohmann::json;

class McpServerTest : public ::testing::Test {
protected:
  McpServer server;

  json invoke(const json &amp;amp;request) { return server.handle_request(request); }
};

TEST_F(McpServerTest, TestInitialize) {
  json req = {{&quot;jsonrpc&quot;, &quot;2.0&quot;}, {&quot;id&quot;, 1}, {&quot;method&quot;, &quot;initialize&quot;}};

  json resp = invoke(req);

  EXPECT_EQ(resp[&quot;jsonrpc&quot;], &quot;2.0&quot;);
  EXPECT_EQ(resp[&quot;id&quot;], 1);
  EXPECT_TRUE(resp.contains(&quot;result&quot;));
  EXPECT_EQ(resp[&quot;result&quot;][&quot;protocolVersion&quot;], &quot;2024-11-05&quot;);
  EXPECT_EQ(resp[&quot;result&quot;][&quot;serverInfo&quot;][&quot;name&quot;], &quot;plc-mcp-server&quot;);
}

TEST_F(McpServerTest, TestListTools) {
  json req = {{&quot;jsonrpc&quot;, &quot;2.0&quot;}, {&quot;id&quot;, 2}, {&quot;method&quot;, &quot;tools/list&quot;}};

  json resp = invoke(req);

  EXPECT_TRUE(resp.contains(&quot;result&quot;));
  EXPECT_TRUE(resp[&quot;result&quot;].contains(&quot;tools&quot;));

  auto tools = resp[&quot;result&quot;][&quot;tools&quot;];
  EXPECT_EQ(tools.size(), 4);
  EXPECT_EQ(tools[0][&quot;name&quot;], &quot;set_red_led&quot;);
  EXPECT_EQ(tools[1][&quot;name&quot;], &quot;set_yellow_led&quot;);
  EXPECT_EQ(tools[2][&quot;name&quot;], &quot;set_green_led&quot;);
  EXPECT_EQ(tools[3][&quot;name&quot;], &quot;start_process&quot;);
}

TEST_F(McpServerTest, TestCallToolSetLed) {
  json req = {
      {&quot;jsonrpc&quot;, &quot;2.0&quot;},
      {&quot;id&quot;, 3},
      {&quot;method&quot;, &quot;tools/call&quot;},
      {&quot;params&quot;, {{&quot;name&quot;, &quot;set_red_led&quot;}, {&quot;arguments&quot;, {{&quot;state&quot;, true}}}}}};

  json resp = invoke(req);

  EXPECT_TRUE(resp.contains(&quot;result&quot;));
  EXPECT_TRUE(resp[&quot;result&quot;].contains(&quot;content&quot;));
  EXPECT_EQ(resp[&quot;result&quot;][&quot;content&quot;][0][&quot;text&quot;],
            &quot;Successfully set red LED to on&quot;);

  // Verify PLC State actually changed
  EXPECT_TRUE(server.get_state().redLed);
  EXPECT_FALSE(server.get_state().yellowLed);
  EXPECT_FALSE(server.get_state().greenLed);
}

TEST_F(McpServerTest, TestCallToolStartProcess) {
  json req = {{&quot;jsonrpc&quot;, &quot;2.0&quot;},
              {&quot;id&quot;, 4},
              {&quot;method&quot;, &quot;tools/call&quot;},
              {&quot;params&quot;,
               {{&quot;name&quot;, &quot;start_process&quot;},
                {&quot;arguments&quot;, {{&quot;process_name&quot;, &quot;mixing&quot;}}}}}};

  json resp = invoke(req);

  EXPECT_TRUE(resp.contains(&quot;result&quot;));
  EXPECT_EQ(resp[&quot;result&quot;][&quot;content&quot;][0][&quot;text&quot;],
            &quot;Successfully started process: mixing&quot;);

  // Verify PLC State
  EXPECT_EQ(server.get_state().currentProcess, &quot;mixing&quot;);
}

TEST_F(McpServerTest, TestReadResourceState) {
  // First, set a known state
  json req_set = {
      {&quot;jsonrpc&quot;, &quot;2.0&quot;},
      {&quot;id&quot;, 5},
      {&quot;method&quot;, &quot;tools/call&quot;},
      {&quot;params&quot;,
       {{&quot;name&quot;, &quot;set_green_led&quot;}, {&quot;arguments&quot;, {{&quot;state&quot;, true}}}}}};
  invoke(req_set);

  // Now, read the state via resource
  json req_read = {{&quot;jsonrpc&quot;, &quot;2.0&quot;},
                   {&quot;id&quot;, 6},
                   {&quot;method&quot;, &quot;resources/read&quot;},
                   {&quot;params&quot;, {{&quot;uri&quot;, &quot;plc://state&quot;}}}};

  json resp = invoke(req_read);

  EXPECT_TRUE(resp.contains(&quot;result&quot;));
  EXPECT_TRUE(resp[&quot;result&quot;].contains(&quot;contents&quot;));
  EXPECT_EQ(resp[&quot;result&quot;][&quot;contents&quot;][0][&quot;uri&quot;], &quot;plc://state&quot;);

  // The text field contains the JSON string of the state
  std::string state_str = resp[&quot;result&quot;][&quot;contents&quot;][0][&quot;text&quot;];
  json state_json = json::parse(state_str);

  EXPECT_FALSE(state_json[&quot;redLed&quot;]);
  EXPECT_FALSE(state_json[&quot;yellowLed&quot;]);
  EXPECT_TRUE(state_json[&quot;greenLed&quot;]);             // We set this to true
  EXPECT_EQ(state_json[&quot;currentProcess&quot;], &quot;Idle&quot;); // Default value
}

// Entry point for tests built by gtest_main
int main(int argc, char **argv) {
  ::testing::InitGoogleTest(&amp;amp;argc, argv);
  return RUN_ALL_TESTS();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qpVJL/dJMcajuvOBE/IlgKOoUxLXhYa28EGsEsJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qpVJL/dJMcajuvOBE/IlgKOoUxLXhYa28EGsEsJK/img.png&quot; data-alt=&quot;SoftPlcApp.exe unittest 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qpVJL/dJMcajuvOBE/IlgKOoUxLXhYa28EGsEsJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqpVJL%2FdJMcajuvOBE%2FIlgKOoUxLXhYa28EGsEsJK%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;443&quot; height=&quot;327&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SoftPlcApp.exe unittest 결과&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Antigravity IDE 연동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MCP Server 프로그램 등록&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 완성된 &lt;span class=&quot;inline-em&quot;&gt;SoftPlcApp.exe&lt;/span&gt; 도구를 LLM 기반 IDE인 Antigravity에 등록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 경로의 설정 파일을 수정하여 MCP 프로그램을 등록할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C:\Users\[UserName]\.gemini\antigravity\mcp_config.json&lt;/p&gt;
&lt;pre id=&quot;code_1772065017221&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;mcpServers&quot;: {
        &quot;soft-plc-app&quot;: {
            &quot;command&quot;: &quot;C:/Projects/MCP/build/SoftPlcApp.exe&quot;,
            &quot;args&quot;: []
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 등록 후 &lt;b&gt;Antigravity IDE를 재실행&lt;/b&gt;하면, 백그라운드에서 &lt;span class=&quot;inline-em&quot;&gt;SoftPlcApp.exe&lt;/span&gt;SoftPlcApp.exe가 실행된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;경로설정에서 왜 Antigravity의 최상위 폴더가 .gemini인지 이해가 안 간다...&lt;br /&gt;Antigravity에서 다른 AI Model (Claude or ChatGPT)를 써도 .gemini폴더의 MCP설정 파일을 사용한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MCP Server 설정확인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 등록 여부는 &lt;span class=&quot;inline-em&quot;&gt;Antigravity: Manage MCP Servers&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;705&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhz9lr/dJMcafyVORB/OzIpY8Ju5rmGmibe5TI6fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhz9lr/dJMcafyVORB/OzIpY8Ju5rmGmibe5TI6fk/img.png&quot; data-alt=&quot;Antigravity의 MCP 사용여부 설정 파일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhz9lr/dJMcafyVORB/OzIpY8Ju5rmGmibe5TI6fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhz9lr%2FdJMcafyVORB%2FOzIpY8Ju5rmGmibe5TI6fk%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;705&quot; height=&quot;83&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Antigravity의 MCP 사용여부 설정 파일&lt;/figcaption&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;Manage MCPs 탭에서는 AI Model과 연결된 설정 현황과,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP Server가 JSON 응답을 통해 제공하는 &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;1033&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baNHo6/dJMcadHOQas/1SrDsJriZ5WkqfgnSAEU5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baNHo6/dJMcadHOQas/1SrDsJriZ5WkqfgnSAEU5k/img.png&quot; data-alt=&quot;Antigravity Manage MCPs 설정탭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baNHo6/dJMcadHOQas/1SrDsJriZ5WkqfgnSAEU5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaNHo6%2FdJMcadHOQas%2F1SrDsJriZ5WkqfgnSAEU5k%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;1033&quot; height=&quot;537&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Antigravity Manage MCPs 설정탭&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Gemini 이용한 PLC 조작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP Server가 연동됐다면 다음과 같이 AI에게 요청해 도구를 잘 사용하는지 확인할 수 있다.&lt;/p&gt;
&lt;div style=&quot;margin: 30px 0; background: #1e1e1e; color: #00ff88; padding: 20px; border-radius: 10px; font-family: Consolas, monospace;&quot;&gt;
&lt;div style=&quot;color: #ffffff; font-weight: bold; margin-bottom: 10px;&quot;&gt;▶ AI COMMAND INPUT&lt;/div&gt;
&lt;div style=&quot;white-space: pre-wrap;&quot;&gt;&amp;gt; 현재 PLC상태를 요약하고, 믹싱작업이 가능한지 확인해줘.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5pHRI/dJMcabQNTmJ/KqKyqxUfBbJqu1fNz8aAjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5pHRI/dJMcabQNTmJ/KqKyqxUfBbJqu1fNz8aAjK/img.png&quot; data-alt=&quot;SoftPlcApp 도구를 이용한 상태확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5pHRI/dJMcabQNTmJ/KqKyqxUfBbJqu1fNz8aAjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5pHRI%2FdJMcabQNTmJ%2FKqKyqxUfBbJqu1fNz8aAjK%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;573&quot; height=&quot;646&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SoftPlcApp 도구를 이용한 상태확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 모델의 강력함은 &lt;b&gt;여러 도구를 적절하게 엮어 스스로 최적의 판단을 내려 작업을 완료한다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;div style=&quot;margin: 30px 0; background: #1e1e1e; color: #00ff88; padding: 20px; border-radius: 10px; font-family: Consolas, monospace;&quot;&gt;
&lt;div style=&quot;color: #ffffff; font-weight: bold; margin-bottom: 10px;&quot;&gt;▶ AI COMMAND INPUT&lt;/div&gt;
&lt;div style=&quot;white-space: pre-wrap;&quot;&gt;&amp;gt; 5분뒤 믹싱작업 시작해&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;803&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAEGrI/dJMcafZXO0o/FGj0nPzVcEi3RttT4Ink2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAEGrI/dJMcafZXO0o/FGj0nPzVcEi3RttT4Ink2K/img.png&quot; data-alt=&quot;커맨드를 예약해 300초 후 mixing 작업 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAEGrI/dJMcafZXO0o/FGj0nPzVcEi3RttT4Ink2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAEGrI%2FdJMcafZXO0o%2FFGj0nPzVcEi3RttT4Ink2K%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;544&quot; height=&quot;803&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;803&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;커맨드를 예약해 300초 후 mixing 작업 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모종의 이유로 5분 뒤 명령실행에 실패했더라도 그 원인이 무엇인지 스스로 분석해 알려주었을 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP를 직접 공부하고 적용해 보면서 &lt;b&gt;개발의 편리함&lt;/b&gt;에 한 번 놀라고, &lt;b&gt;사람처럼 느껴지는 Antigravity&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 AI의 도움으로 막히는 부분 없이, &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;지금까지 MCP라는 기술은 다소 생소했지만,&amp;nbsp;이번 학습을 통해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Factory Automation 산업 구조와 굉장히 궁합이 잘 맞는 기술&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;특히 이번 예제에서 가상의 PLC를 구현했듯, 설비 데이터의 커스터마이징 영역을 고객들에게 넘겨준다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존의 레거시 시스템과의 연계와 IT/OT의 확장&lt;/b&gt;도 훨씬 유연할 것이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 IT/OT의 연계도 수월할 것이라 생각한다.&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&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;&lt;a href=&quot;https://prejudice.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글 : 2026.02.17 - [개발] - 구글 Antigravity 사용 후기 - QML프로젝트로 AI IDE 특징 알아보기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1772093594581&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;구글 Antigravity 사용 후기 - QML프로젝트로 AI IDE 특징 알아보기&quot; data-og-description=&quot;들어가며요즘 AI를 활용한 이른바 '바이브 코딩(Vibe Coding)'은 더 이상 특별한 일이 아니다.간단한 프로젝트 생성부터 기능 추가, 빌드 자동화까지 AI에게 맡기는 흐름이 자연스러워지고 있다. 특&quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/31&quot; data-og-url=&quot;https://prejudice.tistory.com/31&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eK4kj/dJMb81GUort/Sqdx6kKX8NpJ7YOelmFk40/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bvy4hc/dJMb84p5USP/NKMzJpfd2OKOoEYkNOOA9K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bimFvb/dJMb83kp7i5/OYVBLQKSkERcQ1UaFSGQJ0/img.png?width=1665&amp;amp;height=933&amp;amp;face=0_0_1665_933&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/31&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eK4kj/dJMb81GUort/Sqdx6kKX8NpJ7YOelmFk40/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bvy4hc/dJMb84p5USP/NKMzJpfd2OKOoEYkNOOA9K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bimFvb/dJMb83kp7i5/OYVBLQKSkERcQ1UaFSGQJ0/img.png?width=1665&amp;amp;height=933&amp;amp;face=0_0_1665_933');&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;구글 Antigravity 사용 후기 - QML프로젝트로 AI IDE 특징 알아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며요즘 AI를 활용한 이른바 '바이브 코딩(Vibe Coding)'은 더 이상 특별한 일이 아니다.간단한 프로젝트 생성부터 기능 추가, 빌드 자동화까지 AI에게 맡기는 흐름이 자연스러워지고 있다. 특&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.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;</description>
      <category>개발</category>
      <category>AI</category>
      <category>antigravity</category>
      <category>FactoryAutomation</category>
      <category>llm</category>
      <category>MCP</category>
      <category>PLC</category>
      <category>공장자동화</category>
      <category>산업자동화</category>
      <category>스마트팩토리</category>
      <category>시스템연동</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/34</guid>
      <comments>https://prejudice.tistory.com/34#entry34comment</comments>
      <pubDate>Thu, 26 Feb 2026 17:14:04 +0900</pubDate>
    </item>
    <item>
      <title>QML Stopwatch 구현 따라 하기 - MVVM 구조로 설계한 Qt 아키텍처</title>
      <link>https://prejudice.tistory.com/32</link>
      <description>&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 QML의 내부 동작 원리와 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;QML은 상태 기반 UI에 강점이 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;하지만 두 영역을 철저히 분리하지 않으면 금방 스파게티 코드가 되어버리고,&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;Stopwatch 예제&lt;/b&gt;를 통해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MVVM 원칙을 따르는 설계 방식&lt;/b&gt;을 정리해 보려 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;글을 작성하며 느낀 점은,&lt;br /&gt;MVVM을 설명하는 과정이 곧 객체지향 개념을 설명하는 과정과 매우 닮아 있다는 것이었다.&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;내가 객체지향을 이해하는데 가장 큰 도움을 준 책은&lt;br /&gt;&amp;lt;객체지향의 사실과 오해&amp;gt; 였다.&lt;br /&gt;이 책은 &quot;상태가 아니라 행동을 중심으로 설계하라&quot;는 메시지를 강조한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;이번 예제 역시 그 관점에서 접근했다.&lt;/span&gt;&lt;br /&gt;&lt;a href=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://blog.aladin.co.kr/Bbird/16866413&lt;/a&gt;&lt;/blockquote&gt;
&lt;figure id=&quot;og_1771564383769&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;객체지향의 사실과 오해_2023.01.01&quot; data-og-description=&quot;모든 프로그래머라면 읽어보는 것을 추천하는 책. 객체지향에 대한 전반적인 개념을 깔끔하게 정리하여 좋았다.일명 토끼책으로 유명한 책이다. 객체지향 프로그래밍에 대해 전반적인 내용을..&quot; data-og-host=&quot;blog.aladin.co.kr&quot; data-og-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot; data-og-url=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cBwTDq/dJMb8U8Qfka/krqnK15EJc7CH4B61eBYDK/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273,https://scrap.kakaocdn.net/dn/iYQc2/dJMb9fZrV8I/9Yujfr8DF6ny9QKTwIZ8Kk/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273&quot;&gt;&lt;a href=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.aladin.co.kr/Bbird/16866413&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cBwTDq/dJMb8U8Qfka/krqnK15EJc7CH4B61eBYDK/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273,https://scrap.kakaocdn.net/dn/iYQc2/dJMb9fZrV8I/9Yujfr8DF6ny9QKTwIZ8Kk/img.jpg?width=200&amp;amp;height=273&amp;amp;face=0_0_200_273');&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;객체지향의 사실과 오해_2023.01.01&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;blog.aladin.co.kr&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;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; 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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UI와 비즈니스 로직의 분리&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;Qt에 대입하면 다음과 같이 정리할 수 있다.&lt;/p&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;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.9767%; text-align: center;&quot;&gt;&lt;b&gt;패턴&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.5117%; text-align: center;&quot;&gt;&lt;b&gt;구현방법&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%; text-align: center;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%; text-align: center;&quot;&gt;&lt;b&gt;원칙&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;View&lt;/td&gt;
&lt;td style=&quot;width: 21.5117%;&quot;&gt;QML&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%;&quot;&gt;☑ UI 구성&lt;br /&gt;☑ 사용자 입력 전달&lt;br /&gt;☑ 상태 표현&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%;&quot;&gt;로직을 넣지 않는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;ViewModel&lt;/td&gt;
&lt;td style=&quot;width: 21.5117%;&quot;&gt;QObject 기반 클래스&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%;&quot;&gt;☑ UI 상태 제공&lt;br /&gt;☑ Signal / Slot 연결&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%;&quot;&gt;Model과 View를 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;Model&lt;/td&gt;
&lt;td style=&quot;width: 21.5117%;&quot;&gt;순수 C++ 로직&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%;&quot;&gt;☑ 비즈니스 로직 구현&lt;/td&gt;
&lt;td style=&quot;width: 30.7558%;&quot;&gt;View와 ViewModel을 알지 못한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;설계 접근 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 View보다 Model부터 설계했다.&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;&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;사용자의 행동은 다음 네 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Start&lt;/li&gt;
&lt;li&gt;Stop&lt;/li&gt;
&lt;li&gt;Reset&lt;/li&gt;
&lt;li&gt;Lap&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;행동에 필요한 최소한의 상태를 추가해 Model을 설계했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;Model (순수 C++)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StopwatchModel은 Qt와 의존성을 가지지 않고, 시간은 std::chrono::steady_clock으로 관리한다.&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;/li&gt;
&lt;li&gt;Qt에 종속되지 않는다 (개별 빌드가 가능하다)&lt;/li&gt;
&lt;li&gt;재사용 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Model은 오직 &lt;b&gt;시간 계산과 상태 관리&lt;/b&gt;만 담당한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;핵심: Model은 View와 ViewModel을 모른다.&lt;/blockquote&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;StopwatchModel.h&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771491315058&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;vector&amp;gt;

class StopwatchModel {
public:
    struct Lap {
        int index;
        std::chrono::milliseconds elapsed;
    };

    void start();
    void stop();
    void reset();
    void lap();

    const std::vector&amp;lt;Lap&amp;gt;&amp;amp; laps() const { return m_laps; }
    std::chrono::milliseconds elapsed() const {
        if (m_running)
            return m_accum + std::chrono::duration_cast&amp;lt;std::chrono::milliseconds&amp;gt;(Clock::now() - m_startPoint);
        return m_accum;
    }
    bool isRunning() const { return m_running; }

private:
    using Clock = std::chrono::steady_clock;

    bool m_running = false;
    Clock::time_point m_startPoint{};
    std::chrono::milliseconds m_accum{0};
    std::vector&amp;lt;Lap&amp;gt; m_laps;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1133&quot; data-start=&quot;1111&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;StopwatchModel.cpp&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1771491381829&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;StopwatchModel.h&quot;

void StopwatchModel::start() {
    if (m_running) return;
    m_running = true;
    m_startPoint = Clock::now();
}

void StopwatchModel::stop() {
    if (!m_running) return;
    m_running = false;
    m_accum += std::chrono::duration_cast&amp;lt;std::chrono::milliseconds&amp;gt;(Clock::now() - m_startPoint);
}

void StopwatchModel::reset() {
    m_running = false;
    m_accum = std::chrono::milliseconds{0};
    m_laps.clear();
}

void StopwatchModel::lap() {
    if (!m_running) return;
    Lap l;
    l.index = static_cast&amp;lt;int&amp;gt;(m_laps.size() + 1);
    l.elapsed = m_accum + std::chrono::duration_cast&amp;lt;std::chrono::milliseconds&amp;gt;(Clock::now() - m_startPoint);
    m_laps.push_back(l);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;일부러 StopwatchModel은 Qt에 의존하지 않도록 설계했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;하지만 프로젝트의 성격에 따라 Qt 타입(QElapsedTimer, QString 등)을 사용하거나, 경우에 따라 QObject를 상속해도 무방하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;&lt;b&gt;중요한 것은 의존성을 선택할 수 있는 구조&lt;/b&gt;를 만드는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;이 구조에서는 Model을 gtest 같은 프레임워크로 단위 테스트가 가능하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;UI 없이도 Stopwatch의 동작을 검증할 수 있다는 점이 구조적 분리의 가장 큰 장점이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;View (QML)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;QML은 명령을 수행하는 곳이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;상태를 반영하는 곳&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 View에서 로직을 작성하기 시작하는 순간 MVVM 구조는 무너지기 시작한다.&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;View를 설계할 때는 UI와 연결되는 &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;&quot;Start 버튼을 누르면 어떤 동작을 할까?&quot; &amp;rarr; &lt;span style=&quot;color: #ee2323;&quot;&gt;동작(로직)에 관한 잘못된 질문&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;b&gt;&quot;Start버튼이 활성화될 수 있는 상태는 무엇인가?&quot;&amp;nbsp;&lt;/b&gt;&amp;rarr; 상태에 관한 질문&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771568972924&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Button { 
    text: &quot;Start&quot;
    enabled: !vm.isRunning
    onClicked: vm.start()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에는 시간계산 로직도 없고, 상태변경 로직도 없다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;핵심: View는 상태를 표현하고 사용자의 입력을 ViewModel로 전달한다.&lt;/blockquote&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Main.qml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771562097735&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import antigravityTest

Window {
    width: 400; height: 500
    visible: true
    title: &quot;Stopwatch&quot;

    StopwatchViewModel { id: vm }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 12

        Text {
            Layout.alignment: Qt.AlignHCenter
            text: vm.elapsedText
            font.pixelSize: 48
            font.family: &quot;Courier New&quot;
        }

        RowLayout {
            Layout.alignment: Qt.AlignHCenter
            spacing: 10

            Button { text: &quot;Start&quot;; onClicked: vm.start(); enabled: !vm.isRunning }
            Button { text: &quot;Stop&quot;;  onClicked: vm.stop();  enabled: vm.isRunning }
            Button { text: &quot;Lap&quot;;   onClicked: vm.lap();   enabled: vm.isRunning }
            Button { text: &quot;Reset&quot;; onClicked: vm.reset() }
        }

	// 사용자가 Lap을 통해 기록한 상태를 어떻게 보여줄까? &amp;rarr; List로 보여주자
        ListView {
            Layout.fillWidth: true
            Layout.fillHeight: true
            model: vm.laps
            delegate: Text {
                text: &quot;Lap &quot; + modelData.index + &quot;  &quot; + modelData.elapsed
                font.family: &quot;Courier New&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;ViewModel&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel은 Model과 View를 연결하는 계층이다.&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;사용자가 UI로 호출하는 행동은 SLOT으로 정의하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q_PROPERTY와 시그널을 바인딩했다.&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;QML과 Model을 연결하다 보면 다음과 같이 부족한 점들을 발견하게 된다.&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Model의 &lt;span style=&quot;color: #ee2323;&quot;&gt;Laps() 행동&lt;/span&gt;은 std::vector 형태를 반환하는데 QML의 &lt;span style=&quot;color: #ee2323;&quot;&gt;labs상태&lt;span style=&quot;color: #333333;&quot;&gt;는 QVariantList 형태를 취급한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Model은 시간을 &lt;span style=&quot;color: #ee2323;&quot;&gt;&quot;계산&quot;&lt;/span&gt;할 수 있지만 QML이 그 변화를 감지할 수 있는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&quot;신호&quot;&lt;/span&gt;는 제공하지 않는다.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 생각하다 보면 ViewModel의&amp;nbsp;역할이 뚜렷해지고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊게는 최적화와 관련된 내용으로 이어지기도 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;핵심: View와 Model 사이에서 View에 맞게 데이터를 가공한다.&lt;/blockquote&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;StopwatchViewModel.h&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771522620436&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once
#include &amp;lt;QObject&amp;gt;
#include &amp;lt;QTimer&amp;gt;
#include &amp;lt;QVariantList&amp;gt;
#include &quot;StopwatchModel.h&quot;

class StopwatchViewModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool isRunning   READ isRunning   NOTIFY isRunningChanged)
    Q_PROPERTY(int  elapsedMs   READ elapsedMs   NOTIFY elapsedChanged)
    Q_PROPERTY(QString elapsedText READ elapsedText NOTIFY elapsedChanged)
    Q_PROPERTY(QVariantList laps READ laps NOTIFY lapsChanged)

public:
    explicit StopwatchViewModel(QObject *parent = nullptr);

    bool        isRunning()   const { return m_model.isRunning(); }
    int         elapsedMs()   const { return static_cast&amp;lt;int&amp;gt;(m_model.elapsed().count()); }
    QString     elapsedText() const { return formatMs(m_model.elapsed().count()); }
    QVariantList laps()       const;

public slots:
    void start();
    void stop();
    void reset();
    void lap();

signals:
    void isRunningChanged();
    void elapsedChanged();
    void lapsChanged();

private:
    StopwatchModel m_model;
    QTimer         m_timer;
    QString formatMs(qint64 ms) const;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;StopwatchViewModel.cpp&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771522654678&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;StopwatchViewModel.h&quot;

StopwatchViewModel::StopwatchViewModel(QObject *parent)
    : QObject(parent)
{
    // ~60fps update for smooth display
    m_timer.setInterval(16);
    connect(&amp;amp;m_timer, &amp;amp;QTimer::timeout, this, &amp;amp;StopwatchViewModel::elapsedChanged);
}

void StopwatchViewModel::start()
{
    if (m_model.isRunning()) return;
    m_model.start();
    m_timer.start();
    emit isRunningChanged();
    emit elapsedChanged();
}

void StopwatchViewModel::stop()
{
    if (!m_model.isRunning()) return;
    m_model.stop();
    m_timer.stop();
    emit isRunningChanged();
    emit elapsedChanged();
}

void StopwatchViewModel::reset()
{
    m_timer.stop();
    m_model.reset();
    emit isRunningChanged();
    emit elapsedChanged();
    emit lapsChanged();
}

void StopwatchViewModel::lap()
{
    if (!m_model.isRunning()) return;
    m_model.lap();
    emit lapsChanged();
}

QVariantList StopwatchViewModel::laps() const
{
    QVariantList result;
    const auto &amp;amp;laps = m_model.laps();
    for (const auto &amp;amp;lap : laps) {
        QVariantMap m;
        m[&quot;index&quot;]   = lap.index;
        m[&quot;elapsed&quot;] = formatMs(lap.elapsed.count());
        result.prepend(m); // most recent lap first
    }
    return result;
}

QString StopwatchViewModel::formatMs(qint64 ms) const
{
    int minutes = static_cast&amp;lt;int&amp;gt;(ms / 60000);
    int seconds = static_cast&amp;lt;int&amp;gt;((ms % 60000) / 1000);
    int centis  = static_cast&amp;lt;int&amp;gt;((ms % 1000) / 10);
    return QString(&quot;%1:%2.%3&quot;)
        .arg(minutes, 2, 10, QChar('0'))
        .arg(seconds, 2, 10, QChar('0'))
        .arg(centis,  2, 10, QChar('0'));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;border-left: 6px solid #525FE1; padding-left: 14px; margin: 1.2em 0 1.2em; color: #2c3e50; font-family: 'Noto Sans KR', 'Noto Sans'; font-weight: bold; line-height: 1.3;&quot; data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMIWn/dJMcacvmqtt/FuxOgKoJoUHnnXJJo02s71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMIWn/dJMcacvmqtt/FuxOgKoJoUHnnXJJo02s71/img.png&quot; data-alt=&quot;실행된 QML Stopwatch 프로그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMIWn/dJMcacvmqtt/FuxOgKoJoUHnnXJJo02s71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMIWn%2FdJMcacvmqtt%2FFuxOgKoJoUHnnXJJo02s71%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;394&quot; height=&quot;261&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행된 QML Stopwatch 프로그램&lt;/figcaption&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;p data-ke-size=&quot;size16&quot;&gt;처음에는 &quot;QML 프로젝트 따라 하기&quot;로 글을 쓰려했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 더 명확하게 설명하기 위해 MVVM 구조를 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 QML의 동작방식과 객체지향적 사고력을 키울 수 있었다.&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딸깍으로 프로젝트가 생성되기 때문에 SW 아키텍처가 더욱 중요시되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 프로젝트라도 직접 만들어보며 깊이 생각해 보는 시간이 필요한 것 같다.&lt;/p&gt;
&lt;div style=&quot;margin: 3em 0 1.5em; padding: 1.1em 1.3em; background: #F9F7F6; border-top: 2px solid #525FE1; border-bottom: 2px solid #E5E7EB; font-family: 'Noto Sans KR', 'Noto Sans'; line-height: 1.6; color: #374151; font-size: 15px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부족한 부분이나 더 궁금한 주제가 있다면 댓글로 남겨주세요.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 공부해서 다음 글로 정리해 보려고 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/26&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글 : 2026.02.10 - [Qt] - Qt6 QML 따라하기 ㅡ 프로젝트 생성부터 C++ 연동까지&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771571349985&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;Qt6 QML 따라하기 ㅡ 프로젝트 생성부터 C++ 연동까지&quot; data-og-description=&quot;들어가며이전 글에서 QML의 이론적 배경을 정리했다.런타임 리플렉션을 활용한 pub/sub 구조와,프레임 기반 렌더링을 통한 최적화 개념을 살펴보며QML이 QWidget보다 충분히 경쟁력 있는 기술이라는 &quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/26&quot; data-og-url=&quot;https://prejudice.tistory.com/26&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d55tcr/dJMb8SXuqp5/q7zWLybXsZG6pnvILkS4b1/img.png?width=800&amp;amp;height=800&amp;amp;face=213_206_254_251,https://scrap.kakaocdn.net/dn/F7kFW/dJMb8U8QfXy/5tqhNkIayfzrJVCGhc0ps0/img.png?width=800&amp;amp;height=800&amp;amp;face=213_206_254_251,https://scrap.kakaocdn.net/dn/bm2Az2/dJMb9hCXV8r/aZYXc0mg9ZqcsytEga7Yu1/img.png?width=882&amp;amp;height=552&amp;amp;face=0_0_882_552&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/26&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/26&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d55tcr/dJMb8SXuqp5/q7zWLybXsZG6pnvILkS4b1/img.png?width=800&amp;amp;height=800&amp;amp;face=213_206_254_251,https://scrap.kakaocdn.net/dn/F7kFW/dJMb8U8QfXy/5tqhNkIayfzrJVCGhc0ps0/img.png?width=800&amp;amp;height=800&amp;amp;face=213_206_254_251,https://scrap.kakaocdn.net/dn/bm2Az2/dJMb9hCXV8r/aZYXc0mg9ZqcsytEga7Yu1/img.png?width=882&amp;amp;height=552&amp;amp;face=0_0_882_552');&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;Qt6 QML 따라하기 ㅡ 프로젝트 생성부터 C++ 연동까지&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며이전 글에서 QML의 이론적 배경을 정리했다.런타임 리플렉션을 활용한 pub/sub 구조와,프레임 기반 렌더링을 통한 최적화 개념을 살펴보며QML이 QWidget보다 충분히 경쟁력 있는 기술이라는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/25&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글 : 2026.02.02 - [Qt] - Qt QML 내부 동작 원리 이해하기 ㅡ Property Binding, Event Loop, Scene Graph&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771571372649&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;Qt QML 내부 동작 원리 이해하기 ㅡ Property Binding, Event Loop, Scene Graph&quot; data-og-description=&quot;들어가며QtFramework에 대해 공부하며 QEventLoop, QObjec, moc(Meta Object Compiler)에 대해 공부했다.이 글은 지금까지의 내용을 배경지식을 활용하여 QML에 대해 공부해 보려 한다.2026.01.23 - [Qt] - Qt Event Loop &quot; data-og-host=&quot;prejudice.tistory.com&quot; data-og-source-url=&quot;https://prejudice.tistory.com/25&quot; data-og-url=&quot;https://prejudice.tistory.com/25&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/buUIhw/dJMb9lL8ce6/vleqW3JoQFLptKt9ajJ9k0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bDDPJo/dJMb82eJFV6/BvLYuC1cS0kFhhopkKiXyk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://prejudice.tistory.com/25&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://prejudice.tistory.com/25&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/buUIhw/dJMb9lL8ce6/vleqW3JoQFLptKt9ajJ9k0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bDDPJo/dJMb82eJFV6/BvLYuC1cS0kFhhopkKiXyk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;Qt QML 내부 동작 원리 이해하기 ㅡ Property Binding, Event Loop, Scene Graph&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며QtFramework에 대해 공부하며 QEventLoop, QObjec, moc(Meta Object Compiler)에 대해 공부했다.이 글은 지금까지의 내용을 배경지식을 활용하여 QML에 대해 공부해 보려 한다.2026.01.23 - [Qt] - Qt Event Loop&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;prejudice.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;</description>
      <category>Qt</category>
      <category>C++</category>
      <category>MVVM</category>
      <category>QML</category>
      <category>QML구조</category>
      <category>QT</category>
      <category>QtQuick</category>
      <category>UI아키텍처</category>
      <category>상태기반설계</category>
      <category>설계패턴</category>
      <category>클린코드</category>
      <author>편견장</author>
      <guid isPermaLink="true">https://prejudice.tistory.com/32</guid>
      <comments>https://prejudice.tistory.com/32#entry32comment</comments>
      <pubDate>Fri, 20 Feb 2026 16:40:55 +0900</pubDate>
    </item>
  </channel>
</rss>