<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Bleeding edge</title>
    <link>https://codevil.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 12 Jul 2026 06:39:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>codevil</managingEditor>
    <item>
      <title>RRD Viewer 로딩 줄이기: HTTP Range, Segment Manifest, 그리고 현실적인 한계</title>
      <link>https://codevil.tistory.com/367</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Rerun 기반의 RRD viewer를 웹에서 사용하다 보면, 가장 먼저 체감되는 문제는 &amp;ldquo;데이터를 어떻게 그릴 것인가&amp;rdquo;보다 &amp;ldquo;첫 화면이 언제 뜨는가&amp;rdquo;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 자체는 동작하고 있었다. 서버는 .rrd 파일을 만들고, 프론트는 그 파일을 받아 decode한 뒤 canvas에 그렸다. 하지만 세션 데이터가 커질수록 첫 로딩이 길어졌다. 사용자는 특정 구간만 보고 싶은데, viewer는 꽤 큰 RRD 데이터를 받은 뒤 decode해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 떠올린 비교 대상은 MP4였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 MP4를 재생할 때는 전체 영상을 모두 다운로드한 뒤 재생하지 않는다. 브라우저는 필요한 byte 구간을 Range 요청으로 가져오고, 서버는 206 Partial Content로 일부 데이터만 내려준다. 그래서 큰 영상 파일도 첫 재생이 빠르고, seek도 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 작업의 출발점은 단순했다.&lt;/p&gt;
&lt;pre class=&quot;objectivec&quot;&gt;&lt;code&gt;RRD도 MP4처럼 HTTP 위에서 필요한 구간만 가져올 수 있지 않을까?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 결론부터 말하면, &lt;b&gt;HTTP Range 지원만으로는 충분하지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 프록시는 Range 가능한 구조가 되었지만, 현재 custom RRD viewer는 아직 RRD 내부의 시간과 byte offset 관계를 이해하지 못한다. 그래서 체감 성능 개선은 HTTP byte range 자체보다는 &lt;b&gt;segment manifest + 작은 RRD segment + background prefetch + decoded cache 정책&lt;/b&gt;에서 더 크게 나왔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 목표는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;RRD 파일 전체 다운로드/로딩 병목을 줄이고,
MP4처럼 HTTP range/부분 전송 기반으로 RRD viewer가 필요한 구간을 가져오게 만들자.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 기술적으로 표현하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;HTTP byte-range streaming for RRD
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 기능 이름처럼 말하면:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Range-based segmented RRD streaming
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 당시 서버에는 이미 recording.index.json과 /data/segments/.../recording.rrd 구조가 있었고, 프론트에도 manifest &amp;rarr; 첫 세그먼트 &amp;rarr; 다음 세그먼트 prefetch 흐름이 일부 구현되어 있었다. 그래서 완전히 새 프로토콜을 만드는 것보다는, 기존 segment 기반 구조에 HTTP Range 계약을 추가하는 방향으로 진행했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP Range Request란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Range Request는 클라이언트가 서버에게 &amp;ldquo;파일 전체가 아니라 이 byte 구간만 주세요&amp;rdquo;라고 요청하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클라이언트가 다음과 같이 요청한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;GET /data/cell1/2026-07-03/session1/recording.rrd
Range: bytes=0-1023
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 서버는 전체 파일이 아니라 0번 byte부터 1023번 byte까지만 응답한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 0-1023/104857600
Content-Length: 1024
Content-Type: application/octet-stream
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 헤더는 세 가지다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Accept-Ranges: bytes
Content-Range: bytes 0-1023/104857600
Content-Length: 1024
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Accept-Ranges는 서버가 byte range 요청을 지원한다는 뜻이고, Content-Range는 이번 응답이 전체 파일 중 어느 구간인지 알려준다. 응답 상태 코드는 200 OK가 아니라 206 Partial Content가 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 데이터가 Range Streaming에 잘 맞을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Range를 적용한다고 해서 모든 데이터가 갑자기 빠르게 열리는 것은 아니다. Range는 결국 &amp;ldquo;파일의 특정 byte 구간만 가져오는 기술&amp;rdquo;이다. 따라서 데이터 포맷이 byte 단위 부분 접근과 잘 맞아야 효과가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 잘 맞는 대표적인 예는 MP4 같은 미디어 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MP4는 브라우저와 player가 포맷 구조를 이해한다. 파일 안에 시간 정보, keyframe, metadata가 있고, player는 특정 시점으로 이동할 때 어느 근처 byte를 읽어야 할지 판단할 수 있다. 그래서 사용자가 10분짜리 영상의 8분 지점으로 seek해도, 브라우저는 전체 파일을 처음부터 끝까지 다시 받지 않고 필요한 구간만 Range 요청으로 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 데이터는 Range Streaming과 잘 맞는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;- MP4, WebM 같은 미디어 파일
- 큰 로그 파일
- index가 있는 binary container
- chunk 단위로 독립 decode 가능한 데이터
- footer/header에 metadata가 있고, 일부 구간만 읽어도 의미가 있는 파일
- Parquet처럼 row group 단위 접근이 가능한 데이터
- MCAP처럼 message/chunk index를 통해 특정 시간대 접근이 가능한 데이터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Range와 잘 맞지 않는 데이터도 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 앞부분부터 순차적으로 모두 읽어야 의미가 생기는 데이터
- 압축 스트림 하나로 길게 묶여 있는 데이터
- 특정 byte 구간만 잘라 읽으면 decode가 불가능한 데이터
- 파일 전체 metadata를 읽어야만 해석할 수 있는 데이터
- 시간이나 record 위치와 byte offset의 관계를 알 수 없는 데이터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 하나의 큰 gzip 파일은 Range와 궁합이 좋지 않다. 중간 byte부터 가져와도 압축 해제 상태가 없으면 바로 decode하기 어렵다. JSON도 비슷하다. 큰 JSON 파일에서 중간 byte만 가져와도 문법적으로 완전한 JSON 조각이 아닐 가능성이 높다. 결국 처음부터 파싱하거나 별도 index가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Range가 잘 먹히는 데이터의 핵심 조건은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;1. 파일의 일부 byte 구간만 읽어도 의미가 있어야 한다.
2. 클라이언트가 원하는 시간/record와 byte offset의 관계를 알 수 있어야 한다.
3. 해당 구간을 독립적으로 decode할 수 있어야 한다.
4. 서버가 파일 전체를 다시 만들지 않고, 저장된 파일에서 seek/read할 수 있어야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RRD는 왜 Range를 적용해볼 만하다고 봤나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RRD는 MP4처럼 브라우저가 기본적으로 이해하는 포맷은 아니다. &amp;lt;video&amp;gt; 태그에 .rrd를 넣는다고 브라우저가 알아서 시간 seek를 해주지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 RRD에 Range를 적용해볼 만하다고 본 이유가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, RRD는 결국 하나의 binary artifact다. 서버 입장에서는 .rrd 파일을 만들어 응답하고 있었고, 이 파일은 HTTP 위에서 application/octet-stream으로 전달되고 있었다. 즉, transport layer 관점에서는 MP4와 마찬가지로 &amp;ldquo;큰 binary file&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 이미 cache layer가 있었다. 매 요청마다 RRD를 새로 만들기보다는 캐시된 .rrd 파일을 재사용할 수 있는 구조였다. 이 말은 서버가 bytes를 통째로 만들어 응답하지 않고, 파일 경로를 기준으로 seek해서 특정 byte만 읽는 구조로 바꿀 수 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 이미 segment 개념이 있었다. 서버에는 recording.index.json과 /data/segments/.../recording.rrd가 있었고, 프론트에도 manifest &amp;rarr; 첫 세그먼트 &amp;rarr; 다음 세그먼트 prefetch 흐름이 일부 구현되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RRD 전체 파일 하나에 Range만 적용하면, 클라이언트가 &amp;ldquo;10초 지점은 몇 byte부터 읽어야 하는지&amp;rdquo; 알기 어렵다. 하지만 segment manifest가 있으면 이야기가 달라진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;recording.index.json
  ├─ segment 0: 0초 ~ 1.1초
  ├─ segment 1: 1초 ~ 2.1초
  ├─ segment 2: 2초 ~ 3.1초
  └─ segment 3: 3초 ~ 4.1초
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 시간 구간과 파일 단위가 어느 정도 매핑된다. 클라이언트는 전체 episode를 몰라도, 우선 첫 번째 segment만 받아서 viewer를 띄울 수 있다. 그리고 남은 segment는 백그라운드에서 순차적으로 받아 cache에 채워 넣을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RRD에 Range를 적용할 수 있을 것 같았던 이유는 &amp;ldquo;RRD 자체가 완벽히 random access 가능한 포맷이라서&amp;rdquo;라기보다는, 다음 조건들이 이미 어느 정도 맞아 있었기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- RRD가 하나의 binary file로 서빙되고 있었다.
- 서버에 RRD cache layer가 있었다.
- 캐시된 파일을 path 기반으로 다룰 수 있었다.
- 이미 segment RRD와 manifest 구조가 있었다.
- 프론트도 segment 기반 로딩 흐름을 일부 갖고 있었다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 작업의 방향은 &amp;ldquo;RRD를 MP4처럼 완전히 자동 seek 가능한 포맷으로 만들자&amp;rdquo;가 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 정확한 목표는 이랬다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;RRD 파일도 HTTP 위에서 media-like file처럼 다룰 수 있게 만들고,
segment manifest와 조합해서 초기 로딩과 구간 이동 비용을 줄이자.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Range를 지원해도 Range 요청이 없으면 200 OK다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 프록시에 Range 지원을 넣은 뒤 실제 브라우저 Network를 확인해보니 중요한 한계가 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 헤더에는 분명히 다음이 있었다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Accept-Ranges: bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버는 Range 요청을 받을 준비가 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 요청 헤더에는 이게 없었다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;Range: bytes=...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 응답은 206 Partial Content가 아니라 여전히 200 OK였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 서버 응답 헤더에는 Accept-Ranges: bytes가 있음
- 하지만 브라우저 요청 헤더에는 Range: bytes=...가 없음
- 그래서 응답은 206 Partial Content가 아니라 200 OK
- 현재 custom RRD decoder는 fetch(...).arrayBuffer()로 segment 전체를 받은 뒤 decode함
&lt;/code&gt;&lt;/pre&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;Range 지원은 기반 계약일 뿐이다.&lt;/b&gt; 서버가 Accept-Ranges: bytes를 내려준다고 해서 클라이언트가 자동으로 byte range 요청을 보내는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MP4는 브라우저와 video element가 포맷을 이해하기 때문에 필요한 byte range를 알아서 요청한다. 하지만 현재 custom RRD viewer는 RRD 내부에서 시간과 byte offset이 어떻게 연결되는지 모른다. 따라서 viewer가 직접 Range 요청을 만들지 못하고, segment .rrd 전체를 받아서 WASM decoder로 decode한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, MP4 같은 효과가 나려면 다음이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;1. viewer가 시간 &amp;rarr; byte offset 관계를 알아야 한다.
2. 필요한 byte range만 요청해야 한다.
3. 받은 byte range만으로 독립 decode할 수 있어야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조는 아직 여기에 도달하지 않았다. 그래서 이번 작업은 backend와 proxy의 기반 계약을 만든 작업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실제 3G 환경에서 체감 성능을 좌우한 것은 Range 헤더 자체보다 segment의 크기였다. 현재 viewer는 segment RRD를 통째로 받아 decode하기 때문에, segment를 얼마나 작게 자르느냐가 첫 화면 시간에 직접 영향을 줬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 병목은 전체 recording.rrd가 아니었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 &amp;ldquo;전체 recording.rrd가 커서 느리다&amp;rdquo;라고 생각하기 쉬웠다. 하지만 segment manifest 구조를 쓰는 상태에서는 실제 첫 화면 병목이 조금 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병목은 전체 episode RRD가 아니라 다음 흐름에 있었다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;첫 segment RRD 전체 다운로드
  &amp;rarr; WASM decoder가 segment 전체 decode
  &amp;rarr; JPEG bitmap 생성
  &amp;rarr; canvas에 첫 frame 그리기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 segment 정책은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;segmentSeconds: 10.0
overlapSeconds: 1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정책에서는 첫 segment가 샘플 기준 약 15MB 수준까지 나왔다. 3G 환경에서 테스트해보면 첫 화면까지 여전히 오래 걸렸다. 이유는 단순했다. 첫 화면을 그리려면 첫 segment 전체를 다운로드해야 했고, WASM decoder가 그 segment 전체를 decode해야 했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버가 Range를 지원하더라도 viewer가 Range 요청을 보내지 않는 현재 구조에서는 첫 segment 크기가 곧 첫 화면 대기 시간에 직접 영향을 줬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Segment 정책 줄이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 추가 개선은 segment를 더 작게 자르는 방향으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 개선은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;기존:
segmentSeconds: 10.0
overlapSeconds: 1.0
cache key suffix: policy-s10-o1

변경:
segmentSeconds: 2.0
overlapSeconds: 0.25
cache key suffix: policy-s2-o0p25
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 샘플 첫 segment 크기는 대략 다음처럼 줄었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;약 15.5MB &amp;rarr; 약 4.3MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 한 번 더 줄였다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;변경:
segmentSeconds: 1.0
overlapSeconds: 0.1
cache key suffix: policy-s1-o0p1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정책에서는 샘플 첫 segment 크기가 약 2.1MB 수준까지 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10.0s + 1.0s overlap
  &amp;rarr; policy-s10-o1
  &amp;rarr; 첫 segment 약 15.5MB

2.0s + 0.25s overlap
  &amp;rarr; policy-s2-o0p25
  &amp;rarr; 첫 segment 약 4.3MB

1.0s + 0.1s overlap
  &amp;rarr; policy-s1-o0p1
  &amp;rarr; 첫 segment 약 2.1MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cache key suffix가 정책별로 달라지는 것도 중요하다. 기존 캐시와 새 정책의 캐시가 같은 이름을 쓰면, segment 정책을 바꿔도 예전 segment가 재사용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 정책을 cache key에 포함했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;기존: policy-s10-o1
중간: policy-s2-o0p25
현재: policy-s1-o0p1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 기존 캐시와 충돌하지 않고 새 정책의 segment가 생성된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;overlapSeconds 0.1의 의미&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 헷갈리기 쉬운 부분이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;overlapSeconds: 0.1이라고 해서 viewer가 0.1초만 받고 그리는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조에서 첫 segment 범위는 대략 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0s ~ 1.1s
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, segmentSeconds: 1.0에 overlapSeconds: 0.1이 붙어서 첫 segment는 대략 1.1초 구간을 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 현재 custom RRD viewer는 이 segment .rrd 파일 전체를 받은 뒤 decode한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 첫 화면 대기 단위는 &amp;ldquo;0.1초&amp;rdquo;가 아니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;첫 화면 대기 단위 = 1초 segment + 0.1초 overlap 전체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 구조에서 overlapSeconds는 &amp;ldquo;받는 최소 단위&amp;rdquo;가 아니라 segment 경계에서 데이터가 끊기지 않도록 조금 겹쳐 담는 보정값에 가깝다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Backend 구현 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조에서는 .rrd 파일을 요청하면 서버가 bytes를 만들고, 그 bytes를 응답하는 흐름이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로 보면 이런 식이다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;data = cache.get_or_build(...)
return Response(data)
&lt;/code&gt;&lt;/pre&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;첫째, 서버가 큰 RRD 파일을 메모리에 올릴 수 있다. 둘째, 클라이언트도 전체 파일을 받은 뒤 decode해야 하므로 첫 화면이 늦어진다. 셋째, 사용자는 일부 구간만 보고 싶은데 전체 데이터를 전송하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 캐시 계층을 bytes 중심에서 file path 중심으로 바꿨다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;path = cache.get_or_build_path(...)
return range_file_response(path, request)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 서버는 캐시된 파일 경로를 얻고, 실제 응답은 파일 스트림으로 처리할 수 있다. Range 요청이 없으면 전체 파일을 스트리밍하고, Range 요청이 있으면 해당 byte 구간만 읽어서 내려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 필요한 핵심 함수는 세 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 Range 헤더를 파싱하는 함수다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;_RANGE_RE = re.compile(r&quot;^bytes=(\d*)-(\d*)$&quot;)

def parse_range(range_header: str, size: int) -&amp;gt; tuple[int, int] | None:
    match = _RANGE_RE.match(range_header.strip())
    if not match or size &amp;lt;= 0:
        return None

    start_raw, end_raw = match.groups()

    if start_raw == &quot;&quot; and end_raw == &quot;&quot;:
        return None

    if start_raw == &quot;&quot;:
        suffix_length = int(end_raw)
        if suffix_length &amp;lt;= 0:
            return None
        start = max(0, size - suffix_length)
        end = size - 1
    else:
        start = int(start_raw)
        end = int(end_raw) if end_raw else size - 1

        if start &amp;gt;= size or end &amp;lt; start:
            return None

        end = min(end, size - 1)

    return start, end
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 파일의 특정 구간만 읽는 generator다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;def iter_file_range(path: Path, start: int, end: int) -&amp;gt; Iterator[bytes]:
    remaining = end - start + 1

    with path.open(&quot;rb&quot;) as handle:
        handle.seek(start)

        while remaining &amp;gt; 0:
            chunk = handle.read(min(1024 * 1024, remaining))
            if not chunk:
                break

            remaining -= len(chunk)
            yield chunk
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 실제 응답을 만드는 함수다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;def rrd_file_response(path: Path, etag: str, request: Request) -&amp;gt; Response:
    size = path.stat().st_size

    headers = {
        &quot;Accept-Ranges&quot;: &quot;bytes&quot;,
        &quot;Cache-Control&quot;: &quot;public, max-age=3600&quot;,
        &quot;ETag&quot;: f'&quot;{etag}&quot;',
        &quot;Content-Disposition&quot;: f'inline; filename=&quot;{path.name}&quot;',
    }

    range_header = request.headers.get(&quot;range&quot;)

    if not range_header:
        headers[&quot;Content-Length&quot;] = str(size)
        return StreamingResponse(
            iter_file_range(path, 0, max(0, size - 1)),
            media_type=&quot;application/octet-stream&quot;,
            headers=headers,
        )

    byte_range = parse_range(range_header, size)

    if byte_range is None:
        return Response(
            status_code=416,
            headers={
                **headers,
                &quot;Content-Range&quot;: f&quot;bytes */{size}&quot;,
            },
        )

    start, end = byte_range

    headers[&quot;Content-Range&quot;] = f&quot;bytes {start}-{end}/{size}&quot;
    headers[&quot;Content-Length&quot;] = str(end - start + 1)

    return StreamingResponse(
        iter_file_range(path, start, end),
        status_code=206,
        media_type=&quot;application/octet-stream&quot;,
        headers=headers,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 .rrd endpoint는 더 이상 bytes 응답을 직접 만들지 않고, 캐시된 파일을 HTTP Range 가능한 파일처럼 제공한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@router.get(&quot;/data/{cell}/{date}/{session}/recording.rrd&quot;)
def get_recording(
    request: Request,
    cell: str,
    date: str,
    session: str,
):
    path = disk.get_or_build_path(
        cell,
        date,
        session,
        builder=lambda: build_rrd_bytes(cell, date, session),
    )

    return rrd_file_response(
        path,
        disk.key(cell, date, session),
        request,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;416도 중요하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Range 요청을 구현할 때 206만 생각하면 안 된다. 클라이언트가 파일 크기를 벗어난 구간을 요청할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 파일 크기가 1000 bytes인데 다음과 같이 요청하면 유효하지 않다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Range: bytes=1000-
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 서버는 416 Range Not Satisfiable로 응답해야 한다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;HTTP/1.1 416 Range Not Satisfiable
Accept-Ranges: bytes
Content-Range: bytes */1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 처리를 해두면 클라이언트나 프록시가 잘못된 Range를 보냈을 때도 명확하게 실패한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Next.js Proxy에서 Range를 잃지 않기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 구조에서는 backend 앞에 tdp-web의 /api/cache 프록시가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 backend만 Range를 지원한다고 끝이 아니다. 중간의 Next.js route handler가 Range, If-Range 같은 헤더를 upstream으로 전달하지 않으면 브라우저 입장에서는 Range가 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 프록시에서도 요청 헤더를 보존해야 한다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;function forwardedRequestHeaders(request: NextRequest) {
  const headers = new Headers();

  headers.set(&quot;accept&quot;, request.headers.get(&quot;accept&quot;) ?? &quot;*/*&quot;);

  for (const name of [&quot;range&quot;, &quot;if-range&quot;, &quot;if-none-match&quot;]) {
    const value = request.headers.get(name);
    if (value) headers.set(name, value);
  }

  return headers;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 upstream 응답의 Range 관련 헤더도 브라우저까지 그대로 내려줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;for (const name of [
  &quot;accept-ranges&quot;,
  &quot;cache-control&quot;,
  &quot;content-disposition&quot;,
  &quot;content-length&quot;,
  &quot;content-range&quot;,
  &quot;content-type&quot;,
  &quot;etag&quot;,
]) {
  const value = response.headers.get(name);
  if (value) headers.set(name, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 중요한 점은 proxy에서 upstream body를 arrayBuffer()로 읽지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피해야 하는 방식은 이렇다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return new Response(await response.arrayBuffer(), {
  status: response.status,
  headers,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 프록시가 응답을 다시 통째로 메모리에 올리게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 다음처럼 stream body를 그대로 전달한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return new Response(response.body, {
  status: response.status,
  headers,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이가 꽤 중요하다. Range를 지원한다고 해놓고 프록시에서 다시 전체 버퍼링을 해버리면, 구조상 이점이 줄어든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UI에서 Loading을 언제 풀 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능을 줄이는 것만큼 중요했던 문제가 하나 더 있었다. 바로 stale frame 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 manifest가 도착하거나 첫 HTTP 응답이 시작되면 Loading episode를 풀려고 했다. 이렇게 하면 UI상으로는 로딩이 빨리 끝난 것처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 episode의 첫 화면이 아직 실제로 canvas에 그려지기 전에 Loading episode가 사라지면, 이전 episode의 마지막 화면이 잠깐 남아 보일 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 viewer에서 이것은 꽤 위험한 UX다. 사용자는 새 episode를 보고 있다고 생각하지만, 실제 화면에는 이전 episode의 frame이 남아 있을 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종 정책은 다음처럼 바꿨다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- episode가 바뀌면 기존 canvas를 즉시 검은 화면으로 비움
- Loading episode는 manifest 도착 시점에 풀지 않음
- HTTP 응답 시작 시점에도 풀지 않음
- 새 episode의 첫 decoded frame들이 실제 canvas에 그려진 뒤에만 Loading episode를 해제함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 빠르게 로딩을 푸는 것보다 stale frame을 보여주지 않는 것이 더 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정책 덕분에 사용자는 조금 더 명확한 상태를 보게 된다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;아직 새 episode가 그려지지 않았으면 검은 화면 + Loading episode
새 frame이 실제로 그려진 뒤에만 Loading 해제
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Loading segment의 의미&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Loading segment는 사용자가 아직 로드되지 않은 시간대로 seek하거나, 재생이 다음 segment로 넘어갔는데 해당 segment가 아직 준비되지 않았을 때 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사라지는 시점은 단순히 HTTP 응답이 시작됐을 때가 아니다. 현재 구조에서는 다음 단계가 모두 끝나야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 해당 timestamp가 포함된 segment를 찾음
2. segment .rrd fetch
3. segment 전체 다운로드 완료
4. WASM decoder가 segment 전체 decode
5. decoded recording이 segment cache에 저장됨
6. 해당 timestamp의 frame을 canvas에 그림
7. 그 뒤 Loading segment가 사라짐
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Loading segment도 byte 일부가 도착했다고 사라지는 것이 아니다. 현재 custom RRD viewer는 segment 전체 다운로드와 decode가 끝나야 해당 timestamp를 그릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 background prefetch가 진행 중이면 시간이 지나면서 buffered range가 뒤쪽으로 계속 확장된다. 사용자가 아직 prefetch되지 않은 구간으로 seek하면 Loading segment가 뜨고, 해당 segment가 다운로드/디코드되면 사라진다. 이후에는 그 seek 지점부터 뒤쪽 segment들을 다시 백그라운드에서 순차 prefetch한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 이미 background prefetch로 받아둔 구간은 다시 seek해도 재다운로드 없이 바로 그릴 수 있다. 이 구조에서는 Range보다 segment cache와 prefetch 정책이 실제 viewer UX에 더 직접적인 영향을 준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추천 구조: Range + 작은 Segment + Background Prefetch&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 현재 RRD viewer에서 가장 현실적인 구조는 Range만 단독으로 쓰는 것이 아니라, 작은 segment와 background prefetch를 함께 쓰는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 구조는 다음에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;1. recording.index.json으로 전체 segment 목록을 제공한다.
2. 첫 화면에는 첫 번째 segment.rrd만 우선 로드한다.
3. segment 크기는 1초 단위로 작게 유지한다.
4. 첫 segment가 준비되면 남은 segment를 백그라운드에서 순차 prefetch한다.
5. 사용자가 중간으로 seek하면 해당 segment를 우선 로드하고, 그 지점부터 뒤쪽 segment를 다시 순차 prefetch한다.
6. 한 번 decode한 segment는 episode 전체 길이만큼 브라우저 메모리에 유지한다.
7. 각 segment.rrd 응답은 HTTP Range를 지원한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 다음 segment 1개 정도를 미리 가져오는 정도의 prefetch로 생각했다. 하지만 1초 segment로 잘게 나누고 나니, 사용자가 기다리는 동안 뒤쪽 segment들을 계속 채워두는 편이 더 자연스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조는 다음처럼 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;다운로드 단위: 1초 segment + 0.1초 overlap
초기 로딩: 첫 segment만 우선
background prefetch: 남은 segment를 순차적으로 끝까지
seek 시: seek된 segment 우선, 이후 그 지점부터 끝까지 순차 prefetch
decoded cache: episode 전체 segment 유지
bitmap cache: 별도 제한 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 전체 segment를 한꺼번에 병렬 요청하지 않았다는 것이다. 느린 네트워크에서는 과한 병렬 prefetch가 오히려 현재 필요한 segment 다운로드를 방해할 수 있다. 3G 같은 환경에서 첫 화면이나 seek 지점의 segment가 가장 중요한데, 모든 segment를 동시에 요청하면 네트워크 대역폭을 서로 경쟁하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WASM decoder도 마찬가지다. 여러 segment를 동시에 많이 decode하면 UI thread나 worker 부하가 커질 수 있다. 그래서 background prefetch는 현재 필요한 segment를 먼저 처리한 뒤, 남은 segment는 백그라운드에서 하나씩 순차적으로 채우는 쪽이 더 안정적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 장점은 명확하다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 첫 화면은 작은 segment만 받아 빠르게 뜬다.
- 사용자가 멈춰 있어도 뒤쪽 구간이 백그라운드에서 계속 준비된다.
- 이미 prefetch된 구간으로 seek하면 재다운로드 없이 바로 그릴 수 있다.
- episode 전환 시 stale frame 방지 정책도 유지된다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 trade-off도 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 사용자가 바로 다른 episode로 넘어가면 일부 prefetch는 낭비될 수 있다.
- episode 전체 decoded segment를 유지하므로 긴 episode에서는 메모리 사용량이 늘 수 있다.
- 1초 segment는 요청 수와 decoder 호출 수를 늘린다.
- 아주 긴 영상이나 일반 미디어 플레이어라면 LRU eviction이나 adaptive prefetch가 필요하다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 viewer에서 다루는 episode는 길이가 아주 길지 않을 예정이었다. 그래서 다운로드 단위는 작게 가져가되, 한 번 decode한 segment는 episode 전체 길이만큼 유지하는 쪽이 더 단순하고 UX도 자연스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 단계의 타협점은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;작게 받아서 첫 화면을 빨리 띄우고,
기다리는 동안 뒤쪽을 순차적으로 채우고,
한 번 decode한 segment는 episode 안에서 계속 재사용한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트와 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 변경에서는 backend, proxy, frontend viewer를 각각 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend에서는 다음을 검증했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;- 일반 recording.rrd 요청이 Accept-Ranges: bytes를 반환하는지
- Range: bytes=0-15 요청 시 206 Partial Content를 반환하는지
- Content-Range가 bytes 0-15/{전체크기} 형태인지
- 응답 content가 실제 전체 파일의 앞 16 bytes와 같은지
- 유효하지 않은 Range 요청에 416 Range Not Satisfiable을 반환하는지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy에서는 다음을 검증했다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;- 클라이언트의 Range 헤더가 upstream fetch에 전달되는지
- If-Range 헤더가 전달되는지
- upstream의 206 status가 유지되는지
- Content-Range, Accept-Ranges, Content-Length가 브라우저까지 보존되는지
- response.body를 arrayBuffer로 다시 버퍼링하지 않고 stream으로 전달하는지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Frontend viewer에서는 다음을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;- frontend viewer 테스트 통과
- episode 변경 시 기존 canvas를 즉시 비우는지
- 첫 decoded frame이 실제로 그려진 뒤 Loading episode가 해제되는지
- 아직 준비되지 않은 구간으로 이동하면 Loading segment가 표시되는지
- segment decode와 frame render 이후 Loading segment가 사라지는지
- paused 상태에서도 남은 segment가 background prefetch되는지
- seek 이후 background prefetch가 seek된 segment 기준으로 재시작되는지
- 전체 segment를 decoded cache에 유지하는지
- buffered range가 segment load에 따라 확장되는지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 실제 manifest와 네트워크 동작도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;- segmentSeconds: 1.0
- overlapSeconds: 0.1
- cache key suffix: policy-s1-o0p1
- 샘플 첫 segment가 policy-s1-o0p1로 내려오는 것 확인
- 첫 segment 크기가 약 2.1MB 수준으로 감소
- maxResidentSegments가 전체 segment 수 기준으로 내려오는 것 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 로그 기준으로 backend Range 테스트, proxy Range header forwarding 테스트, frontend viewer 테스트가 통과했고, 프록시 관련 ESLint도 통과했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업은 RRD viewer의 로딩 병목을 줄이기 위해, .rrd 파일을 일반 다운로드 파일이 아니라 HTTP 위에서 range 가능한 media-like file로 다루는 시도였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 최종 결론은 조금 더 현실적이다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Backend와 proxy는 Range 가능한 구조가 되었다.
하지만 custom RRD viewer는 아직 RRD byte offset index를 이해하지 못한다.
그래서 현재 체감 개선은 HTTP byte range 자체보다
segment manifest + 작은 RRD segment + background prefetch + decoded cache 정책에서 나온다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 MP4처럼 동작하려면 viewer가 시간 &amp;rarr; byte offset 관계를 알고, 필요한 byte range만 요청하고, 그 range를 독립 decode할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 단계에서는 1초 단위 segment RRD가 가장 현실적인 타협이었다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;segmentSeconds: 1.0
overlapSeconds: 0.1
cache key suffix: policy-s1-o0p1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 Range 자체보다 segment 단위 설계가 더 중요했다. 작은 segment로 첫 화면 단위를 줄이고, background prefetch로 뒤쪽 구간을 계속 채우며, decoded cache를 episode 전체로 유지하는 방식이 현재 custom RRD viewer에서 가장 현실적인 절충점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 완벽한 random access streaming은 아니지만, 큰 RRD를 한 번에 받는 방식보다는 낫다. 첫 화면에 필요한 단위를 줄이고, 사용자가 보는 동안 뒤쪽 데이터를 조용히 준비하며, 이미 본 구간은 다시 다운로드하지 않고 재사용할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 범위에서는 썸네일 sidecar는 제외했다. 썸네일을 별도로 두면 첫 화면 placeholder나 timeline preview를 더 빠르게 만들 수 있겠지만, 이번 작업의 핵심은 RRD 파일 서빙 계약과 segment 로딩 정책을 정리하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이번 작업을 한 문장으로 정리하면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;RRD를 MP4처럼 만들기 위해 HTTP Range를 붙였지만,
실제 체감 성능은 Range 자체보다 segment를 얼마나 작게 자르고,
어떻게 미리 채우고,
얼마나 오래 재사용하느냐에 달려 있었다.
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/367</guid>
      <comments>https://codevil.tistory.com/367#entry367comment</comments>
      <pubDate>Fri, 3 Jul 2026 17:18:04 +0900</pubDate>
    </item>
    <item>
      <title>SSH 접속 시 안내 메시지 띄우기</title>
      <link>https://codevil.tistory.com/366</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;공용으로 사용하는 장비나 특정 프로젝트에 할당된 PC를 운영하다 보면, 접속하는 사람에게 꼭 알려줘야 하는 내용들이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 것들입니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 이 PC가 어떤 용도로 사용되는지
- 현재 누가 SSH로 접속 중인지
- 언제 전원이 꺼질 예정인지
- 장시간 작업 전에 공유가 필요한지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 내용은 문서나 메신저로 공유해도 되지만, 실제로는 접속하는 순간에 바로 보이는 것이 가장 확실합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 SSH 접속 시점에 아래와 같은 안내 메시지를 띄우는 방법을 정리해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;==================================================
 Data Foundry PC 안내
==================================================

현재 SSH 접속 목록

 - IP: 10.0.19.110              세션 수: 1

장비 이름 : DATAFOUNDRY-PC4

안내 메모
 - 이 PC는 Data Foundry에 할당된 PC입니다.
 - 저녁 7시부터 아침 9시까지 전원을 끌 예정입니다.
 - 해당 시간대에는 SSH 접속 및 원격 작업이 불가능할 수 있습니다.
 - 장시간 작업이 필요한 경우, 사전에 담당자에게 공유해주세요.
==================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux와 Windows는 SSH 접속 이후의 처리 방식이 조금 다르기 때문에, 각각 나누어 정리하겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. Linux에서 SSH 안내 메시지 띄우기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 계열 Linux에서는 SSH 접속 시 MOTD(Message of the Day)를 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MOTD는 보통 아래 경로의 스크립트들로 구성됩니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;/etc/update-motd.d/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 디렉토리에 실행 가능한 스크립트를 넣으면, SSH 로그인 시 해당 스크립트가 실행되고 그 출력 결과가 로그인 메시지로 표시됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 Ubuntu 안내 메시지 줄이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu에 접속하면 기본적으로 이런 메시지가 나올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Welcome to Ubuntu 24.04.4 LTS

Applications를 위한 확장된 보안 유지보수 비활성화됨.
51개의 업데이트가 즉시 적용 가능합니다.
97 추가 보안 업데이트는 ESM Apps에 적용될 수 있습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 장비에서는 이런 메시지가 필요한 경우도 있지만, 별도로 관리하고 있다면 로그인 화면에서는 숨기고 싶을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령으로 기본 MOTD 일부를 비활성화할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sudo chmod -x /etc/update-motd.d/00-header 2&amp;gt;/dev/null || true
sudo chmod -x /etc/update-motd.d/10-help-text 2&amp;gt;/dev/null || true
sudo chmod -x /etc/update-motd.d/50-motd-news 2&amp;gt;/dev/null || true
sudo chmod -x /etc/update-motd.d/88-esm-announce 2&amp;gt;/dev/null || true
sudo chmod -x /etc/update-motd.d/90-updates-available 2&amp;gt;/dev/null || true
sudo chmod -x /etc/update-motd.d/91-contract-ua-esm-status 2&amp;gt;/dev/null || true
sudo chmod -x /etc/update-motd.d/91-release-upgrade 2&amp;gt;/dev/null || true
sudo chmod -x /etc/update-motd.d/95-hwe-eol 2&amp;gt;/dev/null || true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은, 위 명령은 업데이트 자체를 끄는 것이 아니라 &lt;b&gt;SSH 로그인 시 출력되는 안내 문구만 끄는 것&lt;/b&gt;입니다.&lt;br /&gt;실제 패키지 업데이트 관리는 별도로 해주어야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Data Foundry 안내 메시지 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 원하는 안내 메시지를 출력하는 스크립트를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;sudo tee /etc/update-motd.d/99-data-foundry-info &amp;gt; /dev/null &amp;lt;&amp;lt;'EOF'
#!/bin/bash

BOLD=&quot;\033[1m&quot;
RESET=&quot;\033[0m&quot;
YELLOW=&quot;\033[33m&quot;
CYAN=&quot;\033[36m&quot;
RED=&quot;\033[31m&quot;

echo -e &quot;${CYAN}==================================================${RESET}&quot;
echo -e &quot;${BOLD}${YELLOW} Data Foundry PC 안내${RESET}&quot;
echo -e &quot;${CYAN}==================================================${RESET}&quot;
echo
echo -e &quot;${BOLD}현재 SSH 접속 목록${RESET}&quot;
echo

SSH_LIST=$(ss -Htn state established '( sport = :22 )' 2&amp;gt;/dev/null | awk '
{
  peer=$4

  gsub(/^\[/, &quot;&quot;, peer)
  gsub(/\]:[0-9]+$/, &quot;&quot;, peer)
  gsub(/:[0-9]+$/, &quot;&quot;, peer)

  if (peer != &quot;&quot; &amp;amp;&amp;amp; peer != &quot;127.0.0.1&quot; &amp;amp;&amp;amp; peer != &quot;::1&quot;) {
    print peer
  }
}' | sort | uniq -c)

if [ -z &quot;$SSH_LIST&quot; ]; then
  echo &quot; - 현재 확인 가능한 SSH 접속 IP가 없습니다.&quot;
else
  echo &quot;$SSH_LIST&quot; | awk '
  {
    count=$1
    ip=$2
    printf &quot; - IP: %-24s 세션 수: %s\n&quot;, ip, count
  }'
fi

echo
echo -e &quot;${BOLD}장비 이름${RESET} : $(hostname)&quot;
echo
echo -e &quot;${BOLD}${RED}안내 메모${RESET}&quot;
echo -e &quot; - ${BOLD}이 PC는 Data Foundry에 할당된 PC입니다.${RESET}&quot;
echo -e &quot; - ${BOLD}저녁 7시부터 아침 9시까지 전원을 끌 예정입니다.${RESET}&quot;
echo &quot; - 해당 시간대에는 SSH 접속 및 원격 작업이 불가능할 수 있습니다.&quot;
echo &quot; - 장시간 작업이 필요한 경우, 사전에 담당자에게 공유해주세요.&quot;
echo -e &quot;${CYAN}==================================================${RESET}&quot;
EOF

sudo chmod +x /etc/update-motd.d/99-data-foundry-info&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 여부는 아래 명령으로 바로 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;run-parts /etc/update-motd.d/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 SSH를 다시 접속하면 안내 메시지가 표시됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ssh tommoro@10.0.0.122&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 현재 SSH 접속 목록을 확인하는 방식&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;who&lt;/code&gt; 명령을 사용해서 현재 접속 목록을 보여주려고 했습니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;who&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 &lt;code&gt;who&lt;/code&gt;는 현재 로그인한 사용자를 보여주기 때문에 적절해 보입니다.&lt;br /&gt;하지만 MOTD가 실행되는 시점에서는 현재 SSH 세션이 아직 &lt;code&gt;who&lt;/code&gt; 결과에 잡히지 않는 경우가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제 TCP 연결 상태를 보는 방식으로 변경했습니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;ss -Htn state established '( sport = :22 )'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH는 기본적으로 22번 포트를 사용하므로, 22번 포트에 &lt;code&gt;ESTABLISHED&lt;/code&gt; 상태로 연결된 세션을 보면 현재 SSH 접속 IP를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 로그인 기록이 아니라 네트워크 연결 상태를 기준으로 보기 때문에, MOTD 실행 시점에서도 비교적 안정적으로 현재 접속 중인 IP를 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. Windows에서 SSH 안내 메시지 띄우기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows OpenSSH에는 Linux의 &lt;code&gt;/etc/update-motd.d/&lt;/code&gt;와 같은 구조가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Windows에서는 SSH 접속 후 어떤 쉘이 실행되는지를 먼저 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트가 아래처럼 나오면 CMD입니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;tommoro@DATAFOUNDRY-PC4 C:\Users\tommoro_window&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 나오면 PowerShell입니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;PS C:\Users\tommoro_window&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 CMD로 접속되는 환경을 기준으로 정리하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CMD에서는 PowerShell 명령을 그대로 실행할 수 없습니다.&lt;br /&gt;예를 들어 아래와 같은 명령은 PowerShell 문법입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;New-Item
Write-Host
Where-Object&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CMD에서 실행하면 다음과 같은 에러가 납니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;'New-Item'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.
'Write-Host'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 CMD 환경에서는 CMD의 AutoRun 기능을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CMD는 실행될 때 레지스트리에 등록된 AutoRun 명령을 자동으로 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 경로는 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;HKCU\Software\Microsoft\Command Processor&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 배치 파일을 등록하면, 해당 사용자로 CMD가 시작될 때마다 배치 파일이 실행됩니다.&lt;br /&gt;Windows OpenSSH가 CMD를 기본 쉘로 실행한다면, SSH 접속 시에도 같은 방식으로 안내 메시지를 띄울 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;SSH 접속
  &amp;darr;
CMD 실행
  &amp;darr;
Command Processor AutoRun 실행
  &amp;darr;
banner.cmd 실행
  &amp;darr;
banner.ps1 실행
  &amp;darr;
안내 메시지 출력&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Windows CMD 환경에 적용하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기존에 등록된 AutoRun이 있다면 제거합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;reg delete &quot;HKCU\Software\Microsoft\Command Processor&quot; /v AutoRun /f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 아래 명령을 Windows SSH로 접속한 CMD 창에 그대로 붙여넣습니다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;mkdir &quot;%USERPROFILE%\ssh-login-banner&quot; 2&amp;gt;nul

(
echo $sshConnections = Get-NetTCPConnection -LocalPort 22 -State Established -ErrorAction SilentlyContinue ^| Where-Object { $_.RemoteAddress -and $_.RemoteAddress -ne '127.0.0.1' -and $_.RemoteAddress -ne '::1' } ^| Group-Object RemoteAddress
echo Write-Host '==================================================' -ForegroundColor Cyan
echo Write-Host ' Data Foundry PC 안내' -ForegroundColor Yellow
echo Write-Host '==================================================' -ForegroundColor Cyan
echo Write-Host ''
echo Write-Host '현재 SSH 접속 목록' -ForegroundColor White
echo Write-Host ''
echo if ^(-not $sshConnections^) {
echo   Write-Host ' - 현재 확인 가능한 SSH 접속 IP가 없습니다.'
echo } else {
echo   foreach ^($conn in $sshConnections^) {
echo     Write-Host ^(' - IP: {0,-24} 세션 수: {1}' -f $conn.Name, $conn.Count^)
echo   }
echo }
echo Write-Host ''
echo Write-Host ^('장비 이름 : {0}' -f $env:COMPUTERNAME^) -ForegroundColor White
echo Write-Host ''
echo Write-Host '안내 메모' -ForegroundColor Red
echo Write-Host ' - 이 PC는 Data Foundry에 할당된 PC입니다.' -ForegroundColor Yellow
echo Write-Host ' - 저녁 7시부터 아침 9시까지 전원을 끌 예정입니다.' -ForegroundColor Yellow
echo Write-Host ' - 해당 시간대에는 SSH 접속 및 원격 작업이 불가능할 수 있습니다.'
echo Write-Host ' - 장시간 작업이 필요한 경우, 사전에 담당자에게 공유해주세요.'
echo Write-Host '==================================================' -ForegroundColor Cyan
) &amp;gt; &quot;%USERPROFILE%\ssh-login-banner\banner.ps1&quot;

(
echo @echo off
echo powershell -NoProfile -ExecutionPolicy Bypass -File &quot;%%USERPROFILE%%\ssh-login-banner\banner.ps1&quot;
) &amp;gt; &quot;%USERPROFILE%\ssh-login-banner\banner.cmd&quot;

reg add &quot;HKCU\Software\Microsoft\Command Processor&quot; /v AutoRun /t REG_SZ /d &quot;\&quot;%USERPROFILE%\ssh-login-banner\banner.cmd\&quot;&quot; /f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 테스트하려면 아래 명령을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;&quot;%USERPROFILE%\ssh-login-banner\banner.cmd&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 적용되면 다음과 같이 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;==================================================
 Data Foundry PC 안내
==================================================

현재 SSH 접속 목록

 - IP: 10.0.19.110              세션 수: 1

장비 이름 : DATAFOUNDRY-PC4

안내 메모
 - 이 PC는 Data Foundry에 할당된 PC입니다.
 - 저녁 7시부터 아침 9시까지 전원을 끌 예정입니다.
 - 해당 시간대에는 SSH 접속 및 원격 작업이 불가능할 수 있습니다.
 - 장시간 작업이 필요한 경우, 사전에 담당자에게 공유해주세요.
==================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 SSH를 다시 접속하면 CMD가 시작되면서 안내 메시지가 자동으로 표시됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. Windows 방식에서 주의할 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;code&gt;HKCU&lt;/code&gt;에 등록됩니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;HKCU\Software\Microsoft\Command Processor&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 현재 사용자 계정에만 적용됩니다.&lt;br /&gt;다른 Windows 사용자 계정으로 SSH 접속한다면, 그 계정에서도 별도로 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 주의할 점은 CMD가 시작될 때마다 실행된다는 점입니다.&lt;br /&gt;SSH 접속뿐 아니라 해당 사용자로 CMD를 직접 열어도 같은 안내 메시지가 표시될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점이 부담된다면, SSH 전용으로 분기하는 로직을 추가할 수도 있지만, 단순한 공용 장비 안내 목적이라면 AutoRun 방식만으로도 충분히 사용할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. 제거 방법&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Linux&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 권한만 제거하려면 다음 명령을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo chmod -x /etc/update-motd.d/99-data-foundry-info&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 삭제하려면 다음과 같이 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo rm /etc/update-motd.d/99-data-foundry-info&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 기본 MOTD를 다시 켜고 싶다면 필요한 스크립트에 실행 권한을 다시 부여하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;sudo chmod +x /etc/update-motd.d/00-header
sudo chmod +x /etc/update-motd.d/10-help-text
sudo chmod +x /etc/update-motd.d/90-updates-available&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Windows&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AutoRun 등록을 제거합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;reg delete &quot;HKCU\Software\Microsoft\Command Processor&quot; /v AutoRun /f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 파일까지 삭제하려면 다음 명령을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;rmdir /s /q &quot;%USERPROFILE%\ssh-login-banner&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. 보안 기능은 아니다&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 보안 기능이라기보다는 운영 편의를 위한 안내 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속 권한을 제어하려면 SSH 키, 비밀번호 정책, 방화벽, 계정 권한을 별도로 관리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 배너는 다음과 같은 상황에 도움이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 장비의 용도를 명확히 알리고 싶을 때
- 운영 시간을 접속자에게 알려야 할 때
- 현재 SSH 접속 중인 IP를 확인하고 싶을 때
- 공용 장비에서 작업이 겹치는 것을 줄이고 싶을 때&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 아래와 같은 문제를 직접 해결하지는 못합니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 허가되지 않은 계정의 접속
- SSH 키 유출
- 잘못된 sudo 권한
- 방화벽 미설정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 보안은 보안대로 관리하고, 이 배너는 운영 실수를 줄이는 용도로 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux에서는 &lt;code&gt;/etc/update-motd.d/&lt;/code&gt;를 사용하면 SSH 로그인 시 안내 메시지를 쉽게 띄울 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows에서는 Linux와 같은 MOTD 구조가 없기 때문에 기본 쉘을 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CMD로 접속되는 환경에서는 &lt;code&gt;Command Processor AutoRun&lt;/code&gt;을 사용하면 비교적 간단하게 동일한 목적을 달성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;Linux
  SSH 로그인
    &amp;darr;
  /etc/update-motd.d/ 스크립트 실행
    &amp;darr;
  안내 메시지 출력

Windows
  SSH 로그인
    &amp;darr;
  CMD 실행
    &amp;darr;
  HKCU Command Processor AutoRun 실행
    &amp;darr;
  banner.cmd 실행
    &amp;darr;
  banner.ps1 실행
    &amp;darr;
  안내 메시지 출력&lt;/code&gt;&lt;/pre&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;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/366</guid>
      <comments>https://codevil.tistory.com/366#entry366comment</comments>
      <pubDate>Mon, 15 Jun 2026 17:52:46 +0900</pubDate>
    </item>
    <item>
      <title>ROS 2에서 ROS_DOMAIN_ID를 나눴는데도 Multicast Flooding이 발생하는 이유</title>
      <link>https://codevil.tistory.com/365</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;ROS 2를 여러 대의 로봇, 여러 장비, 또는 여러 테스트 환경에서 동시에 사용하다 보면 ROS_DOMAIN_ID를 나누는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 환경을 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Robot A: ROS_DOMAIN_ID=0
Robot B: ROS_DOMAIN_ID=1
Robot C: ROS_DOMAIN_ID=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 우리는 이렇게 기대한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Domain 0과 Domain 1은 서로 보이지 않는다.
Domain 1과 Domain 2도 서로 보이지 않는다.
따라서 네트워크도 분리된 것처럼 동작할 것이다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS 2 애플리케이션 관점에서는 이 기대가 어느 정도 맞다.&lt;br /&gt;서로 다른 ROS_DOMAIN_ID를 가진 노드들은 기본적으로 서로 discovery되지 않고, 토픽이나 서비스도 보이지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제 네트워크 장비에서 보면 이상한 현상이 나타날 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;모든 장비가 239.255.0.1 multicast group에 가입되어 있음
다른 ROS_DOMAIN_ID의 discovery traffic도 네트워크 상에서 전달됨
스위치나 라우터에서 multicast flooding처럼 보임
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 일이 생길까?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ROS 2 Discovery와 DDS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS 2는 내부 통신 미들웨어로 DDS를 사용한다.&lt;br /&gt;DDS에서는 노드들이 서로를 찾기 위해 discovery 과정을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 discovery 과정에서 기본적으로 multicast가 사용될 수 있다.&lt;br /&gt;대표적으로 자주 보이는 multicast 주소는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;239.255.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 주소는 DDS participant discovery, 즉 &amp;ldquo;나 여기 있다&amp;rdquo;는 정보를 네트워크에 알리기 위해 사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ROS_DOMAIN_ID는 무엇을 분리하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS_DOMAIN_ID는 ROS 2 시스템을 논리적으로 분리하기 위한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;ROS_DOMAIN_ID=0
ROS_DOMAIN_ID=1
ROS_DOMAIN_ID=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 있으면, 각 domain은 서로 다른 ROS graph를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, domain 0의 노드는 domain 1의 노드를 보지 못하고, domain 2의 토픽도 보이지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 중요한 점이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS_DOMAIN_ID는 네트워크 multicast group IP를 기본적으로 분리하는 값이 아니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDS에서는 domain ID에 따라 주로 &lt;b&gt;UDP port&lt;/b&gt;가 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 개념적으로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ROS_DOMAIN_ID=0  &amp;rarr; 239.255.0.1:7400
ROS_DOMAIN_ID=1  &amp;rarr; 239.255.0.1:7650
ROS_DOMAIN_ID=2  &amp;rarr; 239.255.0.1:7900
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, multicast IP는 동일하게 239.255.0.1일 수 있고, domain 구분은 UDP port와 DDS 내부 domain 정보로 이루어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네트워크 장비는 ROS_DOMAIN_ID를 모른다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 여기서 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS 2/DDS는 다음 기준으로 domain을 구분한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Multicast IP
UDP port
DDS Domain ID
Participant 정보
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 스위치나 L3 장비의 multicast forwarding은 보통 다음 기준으로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Multicast group IP
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 네트워크 장비 입장에서는 다음 세 개가 서로 다른 ROS domain이라는 사실을 모른다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;239.255.0.1:7400
239.255.0.1:7650
239.255.0.1:7900
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장비는 단순히 이렇게 본다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;239.255.0.1 group에 가입한 장비들
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ROS_DOMAIN_ID가 달라도, 네트워크 장비의 multicast group table에는 모두 같은 239.255.0.1 group member로 등록될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 값은 Domain ID에 따라 오고 안 오는데, 가입은 같이 보일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 현상은 계층별 처리 기준이 다르기 때문에 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치나 L3 장비는 이렇게 판단한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;239.255.0.1 group에 가입한 포트가 있다.
따라서 239.255.0.1 multicast traffic을 해당 포트들로 전달한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 장비의 OS와 DDS는 더 세밀하게 본다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;UDP port가 내 domain에 해당하는가?
DDS Domain ID가 일치하는가?
Participant 정보가 맞는가?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다른 domain의 discovery packet이 물리적으로 네트워크 인터페이스까지 도달하더라도, ROS 2/DDS 레벨에서는 무시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 다음과 같은 상태가 된다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;네트워크 장비 기준:
- 모두 239.255.0.1 multicast group에 가입한 것으로 보임

ROS 2 기준:
- 같은 ROS_DOMAIN_ID끼리만 discovery됨
- 다른 domain의 노드는 보이지 않음

네트워크 부하 기준:
- 다른 domain의 multicast packet도 물리적으로 전달될 수 있음
- 따라서 대역폭은 이미 소모됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, ROS graph는 분리되어 있지만, 네트워크 traffic은 완전히 분리되지 않은 상태가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 DDS는 Domain ID를 포트로 분리했을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자연스럽게 이런 의문이 생긴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 domain별로 multicast IP를 다르게 쓰면 되지 않았을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 방식이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ROS_DOMAIN_ID=0  &amp;rarr; 239.255.0.1
ROS_DOMAIN_ID=1  &amp;rarr; 239.255.1.1
ROS_DOMAIN_ID=2  &amp;rarr; 239.255.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 DDSI-RTPS 표준은 기본적으로 domain ID를 UDP well-known port 계산에 반영하는 방식을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 다음과 같은 계산식이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;port = PB + DG * domainId + offset
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 많이 보이는 값은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;PB = 7400
DG = 250
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 domain ID가 바뀌면 port가 일정 간격으로 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;domain 0 &amp;rarr; 7400 계열
domain 1 &amp;rarr; 7650 계열
domain 2 &amp;rarr; 7900 계열
domain 7 &amp;rarr; 9150 계열
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 DDS 입장에서는 합리적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, OS와 프로세스 입장에서 UDP port는 자연스러운 demultiplexing 단위다.&lt;br /&gt;같은 머신 안에서 여러 DDS domain이 떠 있어도, 커널은 destination UDP port를 보고 어느 소켓으로 전달할지 구분할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 서로 다른 DDS vendor 간 상호운용성을 맞추기 쉽다.&lt;br /&gt;DDS는 Fast DDS, Cyclone DDS, RTI Connext, OpenDDS 등 여러 구현체가 존재한다. 이들이 같은 계산식을 알고 있으면, domain ID만 맞춰도 discovery port를 예측할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 별도 네트워크 설정 없이 자동 discovery가 가능하다.&lt;br /&gt;사용자는 ROS_DOMAIN_ID만 설정하면 되고, multicast 주소 계획이나 group mapping을 따로 관리하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DDS의 기본 설계는 &amp;ldquo;네트워크 장비에서 domain별 multicast group을 깔끔하게 분리한다&amp;rdquo;보다는, &amp;ldquo;DDS 애플리케이션들이 별도 설정 없이 자동으로 discovery될 수 있게 한다&amp;rdquo;에 더 가깝다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데 왜 지금 환경에서는 문제가 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설계는 호스트와 DDS 프로세스 입장에서는 잘 동작한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;같은 multicast IP를 쓰더라도 UDP port가 다르다.
OS가 port 기준으로 구분할 수 있다.
DDS가 domain ID 기준으로 한 번 더 구분할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 네트워크 장비 입장에서는 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치나 L3 multicast 장비는 보통 multicast group IP 기준으로 membership과 forwarding을 관리한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;239.255.0.1 group
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 DDS는 이렇게 구분한다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;239.255.0.1:7400
239.255.0.1:7650
239.255.0.1:7900
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 mismatch가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDS는 L4 UDP port까지 보고 domain을 나누지만, 네트워크 장비의 multicast forwarding은 대개 L3 multicast group IP만 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음과 같은 문제가 생긴다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ROS_DOMAIN_ID는 다르다.
UDP port도 다르다.
하지만 multicast group IP는 같다.

네트워크 장비는 모두 같은 group으로 본다.
결과적으로 다른 domain의 discovery traffic도 같은 group 안에서 퍼질 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 multicast flooding 또는 discovery flooding처럼 보이는 핵심 원인이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;ldquo;Multicast group을 domain별로 나누면 더 복잡해진다&amp;rdquo;는 뜻&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 domain별 multicast group을 쓰면 해결되는 것 아닐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ROS_DOMAIN_ID=0  &amp;rarr; 239.255.0.1
ROS_DOMAIN_ID=1  &amp;rarr; 239.255.1.1
ROS_DOMAIN_ID=2  &amp;rarr; 239.255.2.1
ROS_DOMAIN_ID=7  &amp;rarr; 239.255.7.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 네트워크 장비도 group IP 기준으로 domain을 구분할 수 있다.&lt;br /&gt;현재처럼 239.255.0.1 하나에 모든 domain이 묶이는 문제를 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방식은 운영 측면에서 복잡도가 증가한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. IGMP snooping table이 늘어난다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식에서는 스위치가 하나의 group만 관리하면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;239.255.0.1
 ├─ port 1
 ├─ port 2
 └─ port 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain별 multicast group을 사용하면 group이 여러 개로 늘어난다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;239.255.0.1
 └─ port 1

239.255.1.1
 └─ port 2

239.255.2.1
 └─ port 3

239.255.7.1
 └─ port 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치 입장에서는 IGMP membership을 group별로 관리해야 한다.&lt;br /&gt;로봇 수, domain 수, VLAN 수가 많아지면 snooping table과 membership 관리 포인트가 늘어난다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. IGMP querier 설정이 더 중요해진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IGMP snooping은 multicast group membership을 유지하기 위해 IGMP query/report를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크에 querier가 없거나 VLAN별 querier 설정이 빠져 있으면, 스위치가 membership을 제대로 유지하지 못할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;group이 하나일 때는 문제가 덜 드러날 수 있지만, group이 많아지면 어떤 group은 정상 전달되고, 어떤 group은 timeout되거나 flood되는 식의 애매한 문제가 생길 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. L3 multicast routing 정책이 복잡해진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 VLAN 안에서만 multicast를 쓴다면 주로 IGMP snooping 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 VLAN 간, 서브넷 간, 라우터를 넘어 multicast를 전달해야 한다면 multicast routing이 관여한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 group별 정책이 필요해질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;239.255.0.1  &amp;rarr; VLAN 10까지만 허용
239.255.1.1  &amp;rarr; VLAN 20까지만 허용
239.255.7.1  &amp;rarr; VLAN 30과 40에 허용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 239.255.0.1 하나만 허용하거나 차단하면 됐지만, domain별 group을 사용하면 어떤 group을 어디까지 전달할지 별도로 설계해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. ACL과 방화벽 정책도 늘어난다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방화벽이나 ACL 정책도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값 하나만 사용할 때는 이렇게 관리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;239.255.0.1 허용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 domain별 group을 사용하면 다음과 같이 관리 대상이 늘어난다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;239.255.0.1 허용
239.255.1.1 허용
239.255.2.1 허용
239.255.7.1 허용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대역으로 넓게 허용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;239.255.0.0/16 허용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 하면 허용 범위가 너무 넓어질 수 있고, 다시 보안/운영 정책의 문제가 생긴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 주소 계획이 필요하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;239.255.&amp;lt;ROS_DOMAIN_ID&amp;gt;.1 방식은 직관적이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;domain 7  &amp;rarr; 239.255.7.1
domain 42 &amp;rarr; 239.255.42.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 domain ID가 255를 넘으면 문제가 생긴다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;domain 300 &amp;rarr; 239.255.300.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP octet에는 300을 넣을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 domain ID 범위를 제한하거나, 별도 매핑 규칙을 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;domain 300 &amp;rarr; 239.254.44.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식의 매핑을 만들 수 있지만, 이때부터는 사람이 직관적으로 이해하기 어려워지고 운영 문서와 자동화가 필요해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. DDS 설정 파일 배포가 필요하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS 2 기본값에서는 보통 환경변수 하나만 맞추면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export ROS_DOMAIN_ID=7
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 multicast group까지 domain별로 바꾸려면 DDS 설정 파일을 별도로 배포해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cyclone DDS 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;CycloneDDS&amp;gt;
  &amp;lt;Domain id=&quot;any&quot;&amp;gt;
    &amp;lt;General&amp;gt;
      &amp;lt;AllowMulticast&amp;gt;true&amp;lt;/AllowMulticast&amp;gt;
    &amp;lt;/General&amp;gt;

    &amp;lt;Discovery&amp;gt;
      &amp;lt;SPDPMulticastAddress&amp;gt;239.255.7.1&amp;lt;/SPDPMulticastAddress&amp;gt;
    &amp;lt;/Discovery&amp;gt;

    &amp;lt;Tracing&amp;gt;
      &amp;lt;Verbosity&amp;gt;warning&amp;lt;/Verbosity&amp;gt;
    &amp;lt;/Tracing&amp;gt;
  &amp;lt;/Domain&amp;gt;
&amp;lt;/CycloneDDS&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실행 환경도 맞춰야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
export ROS_DOMAIN_ID=7
export CYCLONEDDS_URI=file:///path/to/cyclonedds.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은, 같은 domain에 속한 모든 장비가 같은 설정을 가져야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 장비는 239.255.7.1을 쓰고, 다른 장비는 기본값인 239.255.0.1을 쓰면 서로 discovery되지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그럼에도 domain별 multicast group 분리는 유효한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다.&lt;br /&gt;현재 문제처럼 네트워크 장비가 UDP port가 아니라 multicast group IP 기준으로만 forwarding을 처리하는 환경이라면, domain별 multicast group 분리는 충분히 유효한 해결책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;domain 0 &amp;rarr; 239.255.0.1:7400
domain 1 &amp;rarr; 239.255.0.1:7650
domain 2 &amp;rarr; 239.255.0.1:7900
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 장비 관점:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;전부 239.255.0.1 group
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 구조:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;domain 0 &amp;rarr; 239.255.0.1:7400
domain 1 &amp;rarr; 239.255.1.1:7650
domain 2 &amp;rarr; 239.255.2.1:7900
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 장비 관점:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;domain 0 &amp;rarr; 239.255.0.1 group
domain 1 &amp;rarr; 239.255.1.1 group
domain 2 &amp;rarr; 239.255.2.1 group
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 스위치나 L3 장비도 group 단위로 traffic을 분리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DDS 기본 설계보다 운영 복잡도는 올라가지만, 현재 네트워크 환경에서는 더 적합한 구조가 될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방향 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법은 크게 네 가지다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Domain별 multicast group 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ROS_DOMAIN_ID=7 &amp;rarr; 239.255.7.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;네트워크 장비가 group IP 기준으로 domain을 구분할 수 있음
기존 multicast discovery 구조를 유지할 수 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;DDS XML 설정 필요
주소 계획 필요
모든 장비에 동일 설정 배포 필요
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. VLAN 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로봇 그룹이나 서비스 그룹별로 VLAN을 분리한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Robot group A &amp;rarr; VLAN 10
Robot group B &amp;rarr; VLAN 20
Robot group C &amp;rarr; VLAN 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;broadcast/multicast domain 자체가 분리됨
가장 명확한 네트워크 레벨 격리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;네트워크 설계 변경 필요
라우팅/방화벽 정책 추가 필요
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Fast DDS Discovery Server 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fast DDS Discovery Server를 사용하면 분산 multicast discovery 대신 중앙 discovery server를 통해 discovery 정보를 교환할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Node A ┐
Node B ├── Discovery Server
Node C ┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;multicast discovery traffic을 크게 줄일 수 있음
대규모 시스템이나 fleet 환경에 유리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Fast DDS 설정 필요
Discovery Server 운영 필요
서버 장애 대비 필요
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Cyclone DDS peer list와 multicast off&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cyclone DDS를 사용하는 경우 multicast를 끄고 peer IP를 명시하는 방식도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Multicast discovery 사용 안 함
지정된 peer IP로 unicast discovery
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;multicast traffic 자체를 줄일 수 있음
네트워크 장비의 multicast 처리 이슈를 회피 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;peer IP 관리 필요
장비 추가/변경 시 설정 업데이트 필요
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS_DOMAIN_ID는 ROS 2 애플리케이션 레벨에서는 domain을 잘 분리한다.&lt;br /&gt;하지만 네트워크 레벨에서 multicast traffic까지 완전히 분리해주는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 DDS discovery에서는 여러 domain이 같은 multicast group, 예를 들어 239.255.0.1을 사용할 수 있다.&lt;br /&gt;이때 domain 구분은 주로 UDP port와 DDS 내부 정보로 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 스위치나 L3 장비가 multicast forwarding을 UDP port가 아니라 multicast group IP 기준으로 처리한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음과 같은 상황이 발생한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ROS_DOMAIN_ID는 다르다.
ROS graph도 분리된다.
하지만 multicast group은 같다.
네트워크 장비는 같은 group으로 본다.
다른 domain의 discovery traffic도 물리 네트워크에 전달될 수 있다.
DDS는 받은 뒤 버린다.
하지만 네트워크 대역폭은 이미 사용된다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 ROS 2 환경에서 ROS_DOMAIN_ID를 나눴는데도 multicast flooding 또는 discovery flooding이 발생할 수 있는 이유다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 환경처럼 네트워크 장비가 multicast group IP 기준으로만 forwarding하고, UDP port 단위로 분리하지 못하는 경우라면 ROS_DOMAIN_ID만으로는 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 다음 중 하나 이상의 방법을 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;domain별 multicast group 분리
VLAN 분리
Fast DDS Discovery Server
Cyclone DDS peer discovery + multicast off
IGMP snooping / querier / unknown multicast 정책 점검
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 239.255.&amp;lt;ROS_DOMAIN_ID&amp;gt;.1처럼 domain별 multicast group을 분리하는 방식은, 운영 복잡도는 증가하지만 현재와 같은 네트워크 환경에서는 매우 현실적인 해결책이 될 수 있다.&lt;/p&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/365</guid>
      <comments>https://codevil.tistory.com/365#entry365comment</comments>
      <pubDate>Tue, 19 May 2026 14:48:50 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 한 PC를 여러 대처럼? ipc: host 옵션으로 네트워크 경계 허물기</title>
      <link>https://codevil.tistory.com/364</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;도커(Docker) 환경에서 고성능 통신을 구축할 때 핵심인 &lt;b&gt;ipc: host&lt;/b&gt; 옵션에 대해 정리합니다. &lt;b&gt;&quot;한 대의 PC에서 여러 대의 PC가 네트워크로 연결된 것처럼 테스트하면서도, 로컬 PC의 자원을 공유하는 환경&quot;&lt;/b&gt;을 만드는 것이 목적입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# 왜 ipc: host 설정이 필요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 도커 컨테이너는 독립된 환경을 가집니다. 자기들만의 격리된 통신 공간(IPC)을 할당받기 때문입니다. 하지만 여러 대의 PC가 같은 네트워크망에 물려있으면서도 실제로는 로컬 자원을 공유하는 효율적인 테스트 환경을 만들려면 이 격리를 해제해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ipc: host&lt;/code&gt; 옵션을 사용하면 컨테이너의 격리된 담장을 허물고 호스트 PC의 메모리 공간을 직접 공유하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# 이 옵션으로 할 수 있는 것들 (Feat. 다중 네트워크 테스트)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 호스트 OS에서 컨테이너 내부 실시간 모니터링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션을 적용하면 컨테이너 안에서 발생하는 통신 흔적들이 호스트 PC의 &lt;code&gt;/dev/shm&lt;/code&gt; 경로에 그대로 노출됩니다. 이는 호스트 시스템에서 컨테이너 내부를 들여다볼 수 있는 투명성을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;확인 방법:&lt;/b&gt; &lt;code&gt;ls -l /dev/shm/habilis_shm_*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용:&lt;/b&gt; 외부 모니터링 도구나 호스트 터미널을 통해 컨테이너 간 데이터 교환이 정상적인지 실시간으로 감시합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. &quot;제로-카피&quot;로 물리적 한계 돌파&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 PC를 실제 랜선으로 연결하면 데이터 복사와 전송 과정에서 지연(Latency)이 발생합니다. 하지만 &lt;code&gt;ipc: host&lt;/code&gt;를 사용하면 메모리 주소만 전달하는 &lt;b&gt;Zero-copy&lt;/b&gt; 통신이 가능합니다. 덕분에 한 PC 내에서 여러 컨테이너 노드를 실행해도 CPU 부하를 최소화하며 고성능 환경을 유지합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# 옵션을 껐을 때(Default) 벌어지는 일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션을 설정하지 않으면 도커는 철저하게 &lt;b&gt;&quot;물리적으로 완전히 떨어진 다른 PC&quot;&lt;/b&gt;처럼 동작합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;통신 폴백(Fallback):&lt;/b&gt; 공유 메모리 접근이 차단되므로 일반적인 &lt;b&gt;네트워크 소켓(UDP/TCP)&lt;/b&gt; 방식으로 자동 전환됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용도:&lt;/b&gt; 시스템이 실제 네트워크 환경(물리적으로 멀리 떨어진 환경)에서 어느 정도의 지연이 발생하는지 시뮬레이션하고 싶을 때 적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# Docker Compose 설정 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 PC 내에서 여러 대의 기기가 네트워크로 연동된 것처럼 구성할 때 사용하는 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;version: '3.8'
services:
  my_node:
    image: habilis_node:latest
    # 호스트와 메모리를 공유하여 외부 모니터링 및 제로-카피 활성화
    ipc: host
    # 포트 매핑 없이 로컬 네트워크 환경처럼 연동
    network_mode: host
    privileged: true&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;# 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ipc: host&lt;/code&gt;는 성능 최적화와 테스트 가시성 측면에서 매우 유리하지만, 호스트 메모리에 직접 접근하므로 보안이 중요한 운영 환경에서는 신중한 검토가 필요합니다. 하지만 &lt;b&gt;개발 단계나 로컬 네트워크 시뮬레이션 환경&lt;/b&gt;에서는 매우 효율적인 옵션입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 &lt;code&gt;/dev/shm&lt;/code&gt; 폴더 내의 메모리 세그먼트 파일들을 확인하며 호스트와 컨테이너 간의 데이터 흐름을 추적해 보는 것이 좋습니다.&lt;/p&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/364</guid>
      <comments>https://codevil.tistory.com/364#entry364comment</comments>
      <pubDate>Sun, 19 Apr 2026 15:30:16 +0900</pubDate>
    </item>
    <item>
      <title>JPEG 안의 FF C0와 FF C2는 무엇일까?</title>
      <link>https://codevil.tistory.com/363</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;SOF0, SOF2, 그리고 Baseline JPEG / Progressive JPEG 쉽게 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPEG 파일을 다루다 보면 가끔 이런 말을 보게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FF C0 = SOF0 &amp;rarr; baseline DCT&lt;/li&gt;
&lt;li&gt;FF C2 = SOF2 &amp;rarr; progressive DCT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 무슨 암호처럼 보입니다.&lt;br /&gt;하지만 알고 나면, 이 값들은 &lt;b&gt;JPEG 이미지가 어떤 방식으로 저장되었는지 알려주는 아주 중요한 단서&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 FF C0, FF C2, SOF0, SOF2, 그리고 baseline JPEG와 progressive JPEG의 차이를 쉽게 정리해보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPEG 파일 안에는 &amp;ldquo;마커&amp;rdquo;가 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPEG는 단순히 픽셀만 저장하는 포맷이 아닙니다.&lt;br /&gt;파일 내부에는 여러 종류의 &lt;b&gt;마커(marker)&lt;/b&gt; 가 들어 있고, 디코더는 이 마커를 읽으면서 이미지를 해석합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPEG 마커는 보통 이런 형태입니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;FF xx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 FF는 &amp;ldquo;여기서부터 마커가 시작된다&amp;rdquo;는 뜻이고,&lt;br /&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;FF D8 : SOI(Start Of Image, 이미지 시작)&lt;/li&gt;
&lt;li&gt;FF DA : SOS(Start Of Scan, 스캔 시작)&lt;/li&gt;
&lt;li&gt;FF D9 : EOI(End Of Image, 이미지 끝)&lt;/li&gt;
&lt;/ul&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;FF C0 : SOF0&lt;/li&gt;
&lt;li&gt;FF C2 : SOF2&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 SOF는 &lt;b&gt;Start Of Frame&lt;/b&gt;의 약자입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SOF(Start Of Frame)는 뭘 의미할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOF 마커는 JPEG 디코더에게&lt;br /&gt;&amp;ldquo;이 이미지가 어떤 방식으로 압축되어 있는지&amp;rdquo;를 알려주는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOF 구간에는 이런 정보가 들어 있습니다.&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;샘플 정밀도(보통 8-bit)&lt;/li&gt;
&lt;li&gt;컬러 컴포넌트 수&lt;/li&gt;
&lt;li&gt;그리고 가장 중요하게는&lt;br /&gt;&lt;b&gt;이 JPEG가 baseline인지 progressive인지&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, FF C0인지 FF C2인지 보면&lt;br /&gt;그 JPEG의 디코딩 방식이 거의 결정된다고 봐도 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FF C0 = SOF0 는 무슨 뜻일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FF C0는 &lt;b&gt;SOF0&lt;/b&gt; 마커입니다.&lt;br /&gt;이건 곧 &lt;b&gt;Baseline DCT JPEG&lt;/b&gt;를 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;FF C0 = SOF0 = Baseline DCT JPEG
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Baseline JPEG는 우리가 가장 흔하게 접하는 일반적인 JPEG 형식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Baseline JPEG의 특징&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;가장 보편적이다&lt;/b&gt;&lt;br /&gt;대부분의 JPEG 뷰어, 라이브러리, 하드웨어 디코더가 기본적으로 잘 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디코딩이 단순하다&lt;/b&gt;&lt;br /&gt;구조가 비교적 단순해서 빠르게 처리하기 좋습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한 번에 완성된 이미지로 디코드된다&lt;/b&gt;&lt;br /&gt;progressive처럼 여러 번 나눠서 선명해지는 방식이 아니라, 일반적으로 최종 이미지가 바로 복원됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 Baseline JPEG는&lt;br /&gt;&lt;b&gt;&amp;ldquo;가장 표준적이고 범용적인 JPEG&amp;rdquo;&lt;/b&gt; 라고 이해하면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FF C2 = SOF2 는 무슨 뜻일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FF C2는 &lt;b&gt;SOF2&lt;/b&gt; 마커입니다.&lt;br /&gt;이건 &lt;b&gt;Progressive DCT JPEG&lt;/b&gt;를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;FF C2 = SOF2 = Progressive DCT JPEG
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Progressive JPEG의 특징&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;여러 번에 나눠서 이미지가 완성된다&lt;/b&gt;&lt;br /&gt;처음에는 흐릿하게 전체 윤곽이 보이고,&lt;br /&gt;데이터를 더 읽을수록 점점 선명해집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹 환경에서 한때 장점이 있었다&lt;/b&gt;&lt;br /&gt;네트워크가 느릴 때, 이미지를 위에서 아래로 천천히 보여주는 대신&lt;br /&gt;&amp;ldquo;전체 그림을 먼저 흐릿하게라도 보여주고, 점점 또렷하게 만드는&amp;rdquo; 방식이 사용자 경험 측면에서 유리할 수 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디코더 구현이 baseline보다 조금 더 복잡하다&lt;/b&gt;&lt;br /&gt;여러 스캔을 조합해야 하므로 처리 경로가 더 복잡해질 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Progressive JPEG는&lt;br /&gt;&lt;b&gt;&amp;ldquo;이미지를 점진적으로 완성해 나가는 JPEG&amp;rdquo;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Baseline JPEG와 Progressive JPEG의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘의 차이를 가장 쉽게 정리하면 이렇습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Baseline JPEG&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번에 디코드&lt;/li&gt;
&lt;li&gt;구조가 단순&lt;/li&gt;
&lt;li&gt;지원 범위가 넓음&lt;/li&gt;
&lt;li&gt;일반적인 JPEG의 기본형&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Progressive JPEG&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 스캔을 통해 점차 선명해짐&lt;/li&gt;
&lt;li&gt;구조가 더 복잡함&lt;/li&gt;
&lt;li&gt;웹 로딩 경험 개선 목적으로 쓰이기도 함&lt;/li&gt;
&lt;li&gt;일부 환경에서는 baseline보다 처리 경로가 까다로울 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 하필 FF C0, FF C2로 구분할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 JPEG 표준에서 SOF 종류마다 서로 다른 코드를 할당했기 때문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FFC0 &amp;rarr; SOF0 &amp;rarr; Baseline DCT&lt;/li&gt;
&lt;li&gt;FFC2 &amp;rarr; SOF2 &amp;rarr; Progressive DCT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JPEG 디코더는 파일을 읽다가 SOF 마커를 만나면&lt;br /&gt;&amp;ldquo;아, 이 이미지는 baseline 방식이구나&amp;rdquo;&lt;br /&gt;혹은&lt;br /&gt;&amp;ldquo;이 이미지는 progressive 방식이구나&amp;rdquo;&lt;br /&gt;라고 판단하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 확인하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스나 macOS 환경이라면 JPEG 파일이 baseline인지 progressive인지 비교적 쉽게 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) file 명령어 사용&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;file sample.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 예시:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;sample.jpg: JPEG image data, JFIF standard 1.01, baseline, precision 8, ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 baseline이 보이면 Baseline JPEG입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로:&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;sample.jpg: JPEG image data, Exif standard, progressive, precision 8, ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 나오면 Progressive JPEG입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) identify 명령어 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ImageMagick이 설치돼 있다면:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;identify -verbose sample.jpg | grep -i Interlace
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Interlace: None
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면 보통 baseline 쪽이고,&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Interlace: JPEG
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면 progressive인 경우가 많습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 바이너리에서 직접 마커 찾기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 low-level하게 보려면 파일 안에서 SOF 마커를 직접 찾을 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;xxd -g 1 sample.jpg | grep -i 'ff c0\|ff c2'
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ff c0가 보이면 baseline&lt;/li&gt;
&lt;li&gt;ff c2가 보이면 progressive&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 JPEG가 다른 컨테이너 안에 들어 있는 경우에도 유용합니다.&lt;br /&gt;예를 들어 FlatBuffer나 custom binary blob 안에 JPEG 바이트가 포함돼 있을 때도&lt;br /&gt;마커를 찾아서 대략적인 판별이 가능합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 최적화에서 왜 중요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이는 단순한 파일 포맷 지식으로 끝나지 않습니다.&lt;br /&gt;특히 영상 처리, 하드웨어 가속, GPU 디코드, 인코딩 파이프라인 최적화 같은 영역에서는 꽤 중요합니다.&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;baseline JPEG에 최적화&lt;/b&gt;되어 있을 수 있음&lt;/li&gt;
&lt;li&gt;progressive JPEG는 별도 처리 경로가 필요할 수 있음&lt;/li&gt;
&lt;li&gt;decode 파이프라인을 설계할 때 baseline만 지원한다고 가정해도 되는지 먼저 확인해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 입력 JPEG가 baseline인지 progressive인지 아는 것은&lt;br /&gt;&lt;b&gt;지원 가능 여부&lt;/b&gt;, &lt;b&gt;성능&lt;/b&gt;, &lt;b&gt;구현 복잡도&lt;/b&gt;를 모두 좌우할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쉽게 비유해보면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 형식을 이렇게 생각해도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Baseline JPEG&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진을 인화소에서 한 번에 뽑아 오는 느낌입니다.&lt;br /&gt;파일을 다 읽으면 바로 최종 결과가 나옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Progressive JPEG&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케치를 먼저 보여주고,&lt;br /&gt;그 위에 점점 디테일을 덧칠해 가며 완성하는 느낌입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄 요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FF C0는 &lt;b&gt;SOF0&lt;/b&gt;, 즉 &lt;b&gt;Baseline DCT JPEG&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;FF C2는 &lt;b&gt;SOF2&lt;/b&gt;, 즉 &lt;b&gt;Progressive DCT JPEG&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;Baseline&lt;/b&gt;: 일반적인 JPEG, 단순하고 널리 지원됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Progressive&lt;/b&gt;: 여러 단계로 점점 선명해지는 JPEG&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 FF C0, FF C2 같은 값이 낯설지만,&lt;br /&gt;사실은 JPEG의 성격을 알려주는 아주 중요한 표식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 처리 코드를 다루거나, 바이너리 포맷을 분석하거나,&lt;br /&gt;하드웨어 디코더/인코더 성능을 최적화할 때&lt;br /&gt;이 두 마커를 이해하고 있으면 꽤 큰 도움이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에 JPEG 바이너리를 보게 된다면,&lt;br /&gt;이제 FF C0와 FF C2가 단순한 숫자가 아니라&lt;br /&gt;&lt;b&gt;&amp;ldquo;이 이미지가 어떤 방식으로 저장되었는지 알려주는 신호&amp;rdquo;&lt;/b&gt; 라는 점이 보일 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/363</guid>
      <comments>https://codevil.tistory.com/363#entry363comment</comments>
      <pubDate>Tue, 14 Apr 2026 16:37:41 +0900</pubDate>
    </item>
    <item>
      <title>각 코덱이 생긴 이유</title>
      <link>https://codevil.tistory.com/362</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;영상 코덱을 보다 보면 H.264, HEVC, AV1 이렇게 계속 나오는데, 처음엔 그냥 &amp;ldquo;세대 올라간거&amp;rdquo; 정도로만 생각했다. 근데 하나씩 보면 이게 단순 발전이 아니라, &lt;b&gt;각 시점에서 해결하려던 문제가 다르다&lt;/b&gt;.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름 (한 번에 보면 이거다)&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;H.264 &amp;rarr; &amp;ldquo;영상 전송 가능하게 만들자&amp;rdquo;
HEVC  &amp;rarr; &amp;ldquo;용량 더 줄이자 (4K 대응)&amp;rdquo;
AV1   &amp;rarr; &amp;ldquo;돈 안 내고 더 줄이자&amp;rdquo;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. H.264 &amp;mdash; 영상이 &amp;ldquo;가능해진&amp;rdquo; 시점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날에는 영상 자체가 너무 커서 인터넷으로 보내는 게 힘들었다. 그래서 나온 게 H.264다. 핵심은 단순하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;&amp;ldquo;영상 크기 줄여서 전송 가능하게 만들자&amp;rdquo;
&lt;/code&gt;&lt;/pre&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;이전 프레임 활용 (inter)&lt;/li&gt;
&lt;li&gt;기본적인 motion estimation&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Before : 영상 = 너무 큼 (전송 불가)
After  : 영상 = 스트리밍 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  느낌: &lt;b&gt;&amp;ldquo;영상 시대를 연 코덱&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. HEVC (H.265) &amp;mdash; 해상도가 올라가서 생김&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H.264로 잘 쓰고 있었는데 문제가 생긴다. 해상도가 계속 올라간다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;HD &amp;rarr; Full HD &amp;rarr; 4K
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 상황:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;화질 &amp;uarr; &amp;rarr; 용량 폭발
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 HEVC가 나온다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;&amp;ldquo;같은 화질인데 절반으로 줄여보자&amp;rdquo;
&lt;/code&gt;&lt;/pre&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;더 복잡한 partition&lt;/li&gt;
&lt;li&gt;더 많은 모드 비교&lt;/li&gt;
&lt;li&gt;더 정교한 motion estimation&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;H.264 : ████████████████████
HEVC  : ██████████████      (~30~50% 감소)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  근데 문제:&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;라이선스 (돈 문제)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  느낌: &lt;b&gt;&amp;ldquo;기술은 좋은데 쓰기 불편한 코덱&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. AV1 &amp;mdash; 비용 문제에서 시작된 코덱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HEVC는 좋은데, 기업 입장에서 문제가 크다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;&amp;ldquo;쓸 때마다 돈 내야됨&amp;rdquo;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 구글, 넷플릭스, 아마존 같은 애들이 모여서 만든 게 AV1이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;&amp;ldquo;돈 안 내고 + 더 잘 압축하자&amp;rdquo;
&lt;/code&gt;&lt;/pre&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;더 깊은 탐색 (RDO)&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;pre class=&quot;ada&quot;&gt;&lt;code&gt;HEVC : ██████████████
AV1  : ████████████   (~20~30% 추가 감소)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  대신:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;속도 &amp;darr;&amp;darr;&amp;darr;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  느낌: &lt;b&gt;&amp;ldquo;시간 써서 압축하는 코덱&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;압축 효율 비교 (한눈에)&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;H.264 : ████████████████████ (1.0x)
HEVC  : ██████████████       (~0.6~0.7x)
AV1   : ████████████         (~0.5~0.6x)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 차이 (진짜 중요한 부분)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 기술적으로 보면 결국 하나다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;얼마나 많이 비교하느냐
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;H.264 &amp;rarr; 적당히&lt;/li&gt;
&lt;li&gt;HEVC &amp;rarr; 더 많이&lt;/li&gt;
&lt;li&gt;AV1 &amp;rarr; 거의 다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비용 관점에서 보면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 실제로 중요한 포인트다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;H.264 / NVENC &amp;rarr; compute 싸고 / 용량 큼
AV1           &amp;rarr; compute 비싸고 / 용량 작음
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 뭐가 더 좋은게 아니라 방향이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;H.264 &amp;rarr; &amp;ldquo;영상 가능하게&amp;rdquo;&lt;/li&gt;
&lt;li&gt;HEVC &amp;rarr; &amp;ldquo;용량 줄이기&amp;rdquo;&lt;/li&gt;
&lt;li&gt;AV1 &amp;rarr; &amp;ldquo;비용 + 압축 둘 다&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄 요약&lt;/h2&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;H.264 : 시작
HEVC  : 개선
AV1   : 최적화 + 비용 해결
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/362</guid>
      <comments>https://codevil.tistory.com/362#entry362comment</comments>
      <pubDate>Sat, 4 Apr 2026 04:08:04 +0900</pubDate>
    </item>
    <item>
      <title>AV1와 NVENC의 차이</title>
      <link>https://codevil.tistory.com/361</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 영상 인코딩 쪽을 보다 보면 계속 부딪히는 질문이 있다.&lt;br /&gt;&amp;ldquo;NVENC가 빠르다는데 AV1은 왜 쓰지?&amp;rdquo; / &amp;ldquo;NVENC 최고 옵션이면 이미 좋은거 아닌가?&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 그냥 속도 vs 품질 정도로 이해하고 있었는데, 조금 깊게 보니까 이건 그런 문제가 아니라&lt;br /&gt;&lt;b&gt;애초에 설계 철학이 다른 문제&lt;/b&gt;였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구조부터 다름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NVENC는 GPU 안에 들어있는 &lt;b&gt;전용 인코딩 칩(ASIC)&lt;/b&gt;이다. 프로그램이 아니라 하드웨어다.&lt;br /&gt;그래서 특징이 명확하다: 빠름 / 일정함 / 대신 유연성 없음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 AV1 (libaom, SVT, 그리고 GPU 기반 구현들)은 그냥 코드다.&lt;br /&gt;CPU든 GPU든 위에서 돌아가는 알고리즘이다. 그래서 느릴 수 있지만, 대신 &lt;b&gt;얼마든지 복잡하게 만들 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 차이: 얼마나 많이 비교하느냐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상 압축은 결국 이 문제다.&lt;br /&gt;&amp;ldquo;이 블록을 어떻게 표현해야 제일 작아지냐?&amp;rdquo;&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;NVENC &amp;rarr; 후보 몇 개만 보고 빠르게 결정&lt;/li&gt;
&lt;li&gt;AV1 &amp;rarr; 가능한 후보 거의 다 보고 계산해서 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 그림으로 보면 이 느낌이다:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;NVENC:  [후보 3개] &amp;rarr; 빠르게 선택
AV1:    [후보 30개] &amp;rarr; 다 계산 &amp;rarr; 최적 선택
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 어디서 용량 차이가 나냐&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 블록 분할 (Partition)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상은 블록으로 쪼개서 압축한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AV1: 128 &amp;rarr; 64 &amp;rarr; 32 &amp;rarr; 16 &amp;rarr; 8 &amp;rarr; 4까지 계속 쪼개보면서 최적 선택&lt;/li&gt;
&lt;li&gt;NVENC: 몇 개만 시도하고 빨리 끝냄&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 중요하냐면, &lt;b&gt;디테일 표현력&lt;/b&gt;이 여기서 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  여기서 이미 &lt;b&gt;10~20% 차이&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 모드 선택 (Mode Decision)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 블록을 intra(자기 프레임)로 할지, inter(이전 프레임)로 할지 결정하는 단계&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AV1: intra 여러 개 + inter 여러 개 &amp;rarr; 다 비교&lt;/li&gt;
&lt;li&gt;NVENC: 몇 개만 보고 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  여기서 &lt;b&gt;5~15%&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 모션 추정 (Motion Estimation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임 간 움직임을 얼마나 정확히 찾느냐&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AV1: 멀리까지 찾고 + 정밀하게 맞추고 + 여러 후보 비교&lt;/li&gt;
&lt;li&gt;NVENC: 범위 제한 + 빠르게 끝냄&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  여기서 &lt;b&gt;5~10%&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 요소들을 다 합치면:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;NVENC (max)    : ████████████████████  (1.0)
AV1 (중간)     : ███████████████       (~0.6~0.7)
AV1 (고품질)   : █████████████         (~0.5~0.6)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;20~40% 용량 차이&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;속도는 왜 이렇게 차이나냐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 더 단순하다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;NVENC &amp;rarr; 고정 파이프라인 &amp;rarr; 분기 없음 &amp;rarr; 계속 흘러감
AV1   &amp;rarr; 재귀 탐색 &amp;rarr; 분기 많음 &amp;rarr; 경우의 수 폭발
&lt;/code&gt;&lt;/pre&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;NVENC = 그리디&lt;/li&gt;
&lt;li&gt;AV1 = 완전탐색 + pruning&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비용 관점에서 보면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 실제로 중요한 포인트다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;NVENC &amp;rarr; compute &amp;darr; / storage + network &amp;uarr;
AV1   &amp;rarr; compute &amp;uarr; / storage + network &amp;darr;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예시 (직관적으로)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1TB 트래픽 기준:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;NVENC : 1.0 TB
AV1   : 0.6 ~ 0.7 TB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 하루 0.3TB 절감&lt;br /&gt;&amp;rarr; 규모 커지면 비용 차이 크게 남&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 뭐가 더 좋은 문제가 아니라, &lt;b&gt;뭘 포기했냐의 차이&lt;/b&gt;다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NVENC &amp;rarr; 탐색을 줄이고 속도를 얻음&lt;/li&gt;
&lt;li&gt;AV1 &amp;rarr; 시간을 쓰고 압축을 얻음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄 정리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NVENC는 &amp;ldquo;빨리 괜찮은 선택&amp;rdquo;&lt;br /&gt;AV1은 &amp;ldquo;느리지만 최적 선택&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/361</guid>
      <comments>https://codevil.tistory.com/361#entry361comment</comments>
      <pubDate>Sat, 4 Apr 2026 03:53:23 +0900</pubDate>
    </item>
    <item>
      <title>ROS 2에서 카메라 노드 제어: Inactive vs Subprocess</title>
      <link>https://codevil.tistory.com/360</link>
      <description>&lt;h1&gt;ROS 2에서 카메라 노드 제어: Inactive vs Subprocess&lt;/h1&gt;
&lt;p&gt;로봇 시스템을 개발하다 보면, &lt;strong&gt;센서나 카메라 노드를 켰다 껐다(on/off)&lt;/strong&gt; 해야 하는 경우가 많습니다. 예를 들어, 카메라를 계속 켜두면 전력이나 리소스를 많이 차지할 수 있고, 다른 프로세스에서 카메라 장치를 접근해야 하는 경우도 있죠. 이때 ROS 2에서는 크게 두 가지 접근 방식이 있습니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Lifecycle Node의 inactive 상태 활용&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스(subprocess) 단위로 노드를 완전히 종료 후 재실행&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;1. Lifecycle Node의 &lt;code&gt;inactive&lt;/code&gt; 상태&lt;/h2&gt;
&lt;p&gt;ROS 2에는 &lt;strong&gt;Managed Lifecycle Node&lt;/strong&gt;라는 개념이 있습니다.&lt;br&gt;Lifecycle Node는 노드가 여러 상태(&lt;code&gt;unconfigured → inactive → active → finalized&lt;/code&gt;)를 가지며, 필요에 따라 상태 전환을 할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;active&lt;/code&gt; : 토픽 publish/subscription, 타이머, 서비스 등 모든 콜백이 동작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inactive&lt;/code&gt; : 파라미터 설정만 가능, 퍼블리시/콜백은 멈춤&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deactivate()&lt;/code&gt; 호출 시 노드가 리소스를 잠시 놓고 대기하는 느낌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;노드 프로세스를 유지한 채 빠르게 on/off 가능&lt;/li&gt;
&lt;li&gt;연속적인 토글이 필요한 경우 유리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ros2 lifecycle set /node_name deactivate&lt;/code&gt; 명령어나 서비스 호출로 제어 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;디바이스 핸들을 해제하지 않을 수 있음&lt;/strong&gt; (카메라 드라이버 구현에 따라 다름)&lt;/li&gt;
&lt;li&gt;즉, inactive 상태여도 여전히 카메라 리소스를 독점할 수 있음&lt;/li&gt;
&lt;li&gt;전력 절감 효과가 제한적일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. Subprocess 기반 실행/종료&lt;/h2&gt;
&lt;p&gt;두 번째 방법은 아예 카메라 노드를 &lt;strong&gt;프로세스 단위로 실행/종료&lt;/strong&gt;하는 방식입니다.&lt;br&gt;즉, 버튼 이벤트나 서비스 호출 시:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시작&lt;/strong&gt; → &lt;code&gt;ros2 launch my_pkg camera_launch.py&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중지&lt;/strong&gt; → 해당 프로세스 &lt;code&gt;terminate()&lt;/code&gt; 또는 &lt;code&gt;kill&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Python 예시:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import subprocess

process = None

def start_camera():
    global process
    process = subprocess.Popen(
        [&amp;quot;ros2&amp;quot;, &amp;quot;launch&amp;quot;, &amp;quot;my_camera_pkg&amp;quot;, &amp;quot;camera_launch.py&amp;quot;]
    )

def stop_camera():
    global process
    if process:
        process.terminate()
        process = None&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;카메라 디바이스 핸들을 &lt;strong&gt;확실히 해제&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;메모리, CPU, GPU 등 &lt;strong&gt;자원 완전 반환&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;다른 노드나 프로그램이 카메라를 다시 사용할 수 있음  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;단점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;다시 실행할 때 &lt;strong&gt;초기화 시간이 걸림&lt;/strong&gt; (드라이버 로드, 장치 open)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;토글이 잦을 경우 지연 발생&lt;/strong&gt;  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;어떤 방식을 선택해야 할까?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;리소스 독점이 중요한 센서(카메라, LiDAR 등)&lt;/strong&gt;&lt;br&gt;→ &lt;strong&gt;Subprocess 방식&lt;/strong&gt;이 더 적합합니다.&lt;br&gt;→ 프로세스를 종료하면 디바이스 핸들이 완전히 닫혀 충돌이 방지됩니다.  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;빠른 전환만 필요하고 리소스 충돌 위험이 적은 경우&lt;/strong&gt;&lt;br&gt;→ &lt;strong&gt;Lifecycle Node &lt;code&gt;inactive&lt;/code&gt;&lt;/strong&gt; 사용도 가능.  &lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;USB/MIPI 카메라처럼 장치 리소스를 독점하는 하드웨어&lt;/strong&gt;라면 → &lt;em&gt;프로세스 종료/재실행&lt;/em&gt;  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단순히 토픽 퍼블리시만 잠시 멈추고 싶을 때&lt;/strong&gt;라면 → &lt;em&gt;Lifecycle inactive&lt;/em&gt;  &lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/360</guid>
      <comments>https://codevil.tistory.com/360#entry360comment</comments>
      <pubDate>Wed, 3 Sep 2025 01:55:08 +0900</pubDate>
    </item>
    <item>
      <title>ROS2 DDS 환경 설정 확인하기: `ps` 명령어 활용 가이드</title>
      <link>https://codevil.tistory.com/358</link>
      <description>&lt;h1&gt;  ROS2 DDS 환경 설정 확인하기: &lt;code&gt;ps&lt;/code&gt; 명령어 활용 가이드&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS2를 실행하다 보면&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;DDS 설정 파일 (&lt;code&gt;CYCLONEDDS_URI&lt;/code&gt;, &lt;code&gt;FASTRTPS_DEFAULT_PROFILES_FILE&lt;/code&gt;)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ROS Domain ID (&lt;code&gt;ROS_DOMAIN_ID&lt;/code&gt;)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RMW 선택 (&lt;code&gt;RMW_IMPLEMENTATION&lt;/code&gt;)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 환경 변수들이 실제 실행된 프로세스에 어떻게 적용되었는지 확인하고 싶을 때가 있습니다.&lt;br /&gt;이럴 때 유용하게 쓸 수 있는 도구가 바로 &lt;b&gt;&lt;code&gt;ps&lt;/code&gt;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 실행 중인 프로세스 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 특정 노드 프로세스를 찾습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ps -ef | grep &amp;lt;프로세스명&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 (talker 노드 실행 중):&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ps -ef | grep talker&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &lt;code&gt;ps --pid&lt;/code&gt; 로 특정 프로세스 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--pid&lt;/code&gt; 옵션을 사용하면 특정 PID의 프로세스 상태를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ps --pid &amp;lt;PID&amp;gt; -ef&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;PID&amp;gt;&lt;/code&gt;: 프로세스 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-ef&lt;/code&gt;: 풀 포맷 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 실행 환경 변수 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 변수는 &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/environ&lt;/code&gt; 에 기록되어 있습니다.&lt;br /&gt;이를 확인하면 실제 노드가 어떤 DDS 설정과 Domain ID로 실행되고 있는지 알 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;cat /proc/&amp;lt;PID&amp;gt;/environ | tr '\0' '\n'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 출력:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;CYCLONEDDS_URI=file:///root/ros_ws/cyclonedds.xml
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ROS_DOMAIN_ID=10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이 과정을 통해 &lt;b&gt;실제 노드 실행 시 적용된 DDS 설정, RMW 종류, Domain ID&lt;/b&gt; 를 검증할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. &lt;code&gt;ps --ppid&lt;/code&gt; 로 자식 프로세스 추적하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROS2에서 &lt;code&gt;launch&lt;/code&gt; 명령을 쓰면 여러 노드가 부모 프로세스(&lt;code&gt;launch&lt;/code&gt;) 아래에서 실행됩니다.&lt;br /&gt;이때 &lt;code&gt;--ppid&lt;/code&gt; 옵션을 쓰면 특정 Launch 프로세스의 자식들을 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ps --ppid &amp;lt;Launch_PID&amp;gt; -ef&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 (Launch PID = 91):&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;ps --ppid 91 -ef&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 어떤 노드들이 launch에 의해 실행되었는지 한눈에 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실제 활용 시나리오&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;DDS 설정 검증&lt;/b&gt;&amp;rarr; &lt;code&gt;CYCLONEDDS_URI&lt;/code&gt;, &lt;code&gt;RMW_IMPLEMENTATION&lt;/code&gt; 확인&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;ps -ef | grep talker
cat /proc/&amp;lt;PID&amp;gt;/environ | tr '\0' '\n'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ROS Domain 확인&lt;/b&gt;&amp;rarr; 노드들이 같은 Domain ID를 쓰는지 비교&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /proc/&amp;lt;PID&amp;gt;/environ | tr '\0' '\n' | grep ROS_DOMAIN_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Launch 트리 확인&lt;/b&gt;&amp;rarr; launch에 의해 실행된 노드 전체 확인&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;ps --ppid &amp;lt;Launch_PID&amp;gt; -ef&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ps --pid&lt;/code&gt;: 특정 프로세스 상태 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps --ppid&lt;/code&gt;: 부모 PID 기준으로 자식 프로세스 추적&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/proc/&amp;lt;PID&amp;gt;/environ&lt;/code&gt;: 프로세스 실행 환경 변수 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법을 활용하면 &lt;b&gt;ROS2 노드가 실제 어떤 DDS 설정과 Domain ID에서 동작하는지 손쉽게 확인&lt;/b&gt;할 수 있습니다.&lt;/p&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/358</guid>
      <comments>https://codevil.tistory.com/358#entry358comment</comments>
      <pubDate>Tue, 19 Aug 2025 20:02:41 +0900</pubDate>
    </item>
    <item>
      <title>ROS 2 통신의 기반, DDS(Data Distribution Service)란</title>
      <link>https://codevil.tistory.com/357</link>
      <description>&lt;h2 data-end=&quot;183&quot; data-start=&quot;167&quot; data-ke-size=&quot;size26&quot;&gt;1. DDS란 무엇인가?&lt;/h2&gt;
&lt;p data-end=&quot;330&quot; data-start=&quot;184&quot; data-ke-size=&quot;size16&quot;&gt;DDS(Data Distribution Service)는 분산 시스템에서 데이터를 효율적이고 신뢰성 있게 주고받기 위한 퍼블리시&amp;ndash;서브스크라이브(Pub-Sub) 기반 미들웨어 표준입니다.&lt;br /&gt;ROS 2는 노드 간 통신을 위해 DDS를 기본 기반 기술로 사용합니다.&lt;/p&gt;
&lt;p data-end=&quot;387&quot; data-start=&quot;332&quot; data-ke-size=&quot;size16&quot;&gt;기존 TCP/UDP 소켓 통신과 달리, DDS는 단순 데이터 송수신을 넘어 다음 기능을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;577&quot; data-start=&quot;389&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;457&quot; data-start=&quot;389&quot;&gt;QoS(Quality of Service) 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;457&quot; data-start=&quot;422&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;457&quot; data-start=&quot;422&quot;&gt;신뢰성, 지연 허용 범위, 데이터 유지 기간 등을 설정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;519&quot; data-start=&quot;458&quot;&gt;자동 Discovery
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;519&quot; data-start=&quot;477&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;519&quot; data-start=&quot;477&quot;&gt;네트워크에 새로운 참여자(Participant)가 나타나면 자동으로 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;538&quot; data-start=&quot;520&quot;&gt;멀티캐스트 / 유니캐스트 지원&lt;/li&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;539&quot;&gt;보안 기능 (암호화, 인증)&lt;/li&gt;
&lt;li data-end=&quot;577&quot; data-start=&quot;557&quot;&gt;실시간 데이터 전송을 위한 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;604&quot; data-start=&quot;584&quot; data-ke-size=&quot;size26&quot;&gt;2. DDS의 주요 구현체 종류&lt;/h2&gt;
&lt;p data-end=&quot;699&quot; data-start=&quot;605&quot; data-ke-size=&quot;size16&quot;&gt;DDS는 OMG(Objects Management Group)가 정의한 표준이므로, 다양한 구현체가 존재합니다. ROS 2에서는 이 중 몇 가지를 선택적으로 사용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;구현체특징라이선스
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1046&quot; data-start=&quot;701&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1046&quot; data-start=&quot;750&quot;&gt;
&lt;tr data-end=&quot;816&quot; data-start=&quot;750&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;772&quot; data-start=&quot;750&quot;&gt;Fast DDS (eProsima)&lt;/td&gt;
&lt;td data-end=&quot;802&quot; data-start=&quot;772&quot; data-col-size=&quot;sm&quot;&gt;ROS 2에서 널리 사용, 속도 빠름, 설정 유연&lt;/td&gt;
&lt;td data-end=&quot;816&quot; data-start=&quot;802&quot; data-col-size=&quot;sm&quot;&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;888&quot; data-start=&quot;817&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;841&quot; data-start=&quot;817&quot;&gt;Cyclone DDS (Eclipse)&lt;/td&gt;
&lt;td data-end=&quot;877&quot; data-start=&quot;841&quot; data-col-size=&quot;sm&quot;&gt;안정성, 낮은 지연, ROS 2 Humble 이후 기본 채택&lt;/td&gt;
&lt;td data-end=&quot;888&quot; data-start=&quot;877&quot; data-col-size=&quot;sm&quot;&gt;EPL 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;936&quot; data-start=&quot;889&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;907&quot; data-start=&quot;889&quot;&gt;RTI Connext DDS&lt;/td&gt;
&lt;td data-end=&quot;930&quot; data-start=&quot;907&quot; data-col-size=&quot;sm&quot;&gt;상용, 고성능, 산업&amp;middot;항공&amp;middot;의료 분야&lt;/td&gt;
&lt;td data-end=&quot;936&quot; data-start=&quot;930&quot; data-col-size=&quot;sm&quot;&gt;상용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;987&quot; data-start=&quot;937&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;948&quot; data-start=&quot;937&quot;&gt;GurumDDS&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;969&quot; data-start=&quot;948&quot;&gt;한국 개발, 소형 디바이스 최적화&lt;/td&gt;
&lt;td data-end=&quot;987&quot; data-start=&quot;969&quot; data-col-size=&quot;sm&quot;&gt;상용(커뮤니티 버전 있음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1046&quot; data-start=&quot;988&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;998&quot; data-start=&quot;988&quot;&gt;OpenDDS&lt;/td&gt;
&lt;td data-end=&quot;1032&quot; data-start=&quot;998&quot; data-col-size=&quot;sm&quot;&gt;오픈소스, CORBA 기반, ROS 2에서는 드물게 사용&lt;/td&gt;
&lt;td data-end=&quot;1046&quot; data-start=&quot;1032&quot; data-col-size=&quot;sm&quot;&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1068&quot; data-start=&quot;1053&quot; data-ke-size=&quot;size26&quot;&gt;3. DDS 동작 구조&lt;/h2&gt;
&lt;p data-end=&quot;1096&quot; data-start=&quot;1069&quot; data-ke-size=&quot;size16&quot;&gt;DDS의 핵심 개념은 데이터 중심 통신입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1362&quot; data-start=&quot;1098&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1098&quot;&gt;Domain
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1135&quot; data-start=&quot;1113&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1113&quot;&gt;통신 그룹을 구분하는 논리적 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1182&quot; data-start=&quot;1136&quot;&gt;Participant
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1182&quot; data-start=&quot;1156&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1182&quot; data-start=&quot;1156&quot;&gt;Domain에 참여하는 애플리케이션 단위&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1254&quot; data-start=&quot;1183&quot;&gt;Publisher / Subscriber
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1254&quot; data-start=&quot;1214&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1254&quot; data-start=&quot;1214&quot;&gt;데이터를 송신하는 Publisher와 수신하는 Subscriber&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1331&quot; data-start=&quot;1255&quot;&gt;DataWriter / DataReader
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1331&quot; data-start=&quot;1287&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1331&quot; data-start=&quot;1287&quot;&gt;Publisher와 Subscriber 내부에서 실제 데이터 전송을 담당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1362&quot; data-start=&quot;1332&quot;&gt;Topic
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1362&quot; data-start=&quot;1346&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1362&quot; data-start=&quot;1346&quot;&gt;데이터의 이름과 타입 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;출처: eProsima Fast DDS Docs&lt;/p&gt;
&lt;hr data-end=&quot;1486&quot; data-start=&quot;1483&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1513&quot; data-start=&quot;1488&quot; data-ke-size=&quot;size26&quot;&gt;4. OS별 ROS 2 기본 DDS 설정&lt;/h2&gt;
&lt;p data-end=&quot;1605&quot; data-start=&quot;1514&quot; data-ke-size=&quot;size16&quot;&gt;ROS 2는 OS와 배포판(Distro)에 따라 기본 DDS 구현체가 다릅니다.&lt;br /&gt;기본값은 RMW_IMPLEMENTATION 환경 변수로 확인할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;OS / Distro기본 DDS
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1821&quot; data-start=&quot;1607&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1821&quot; data-start=&quot;1659&quot;&gt;
&lt;tr data-end=&quot;1715&quot; data-start=&quot;1659&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1677&quot; data-start=&quot;1659&quot;&gt;리눅스 (Humble 이후)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1715&quot; data-start=&quot;1677&quot;&gt;Cyclone DDS (rmw_cyclonedds_cpp)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1774&quot; data-start=&quot;1716&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1741&quot; data-start=&quot;1716&quot;&gt;리눅스 (Foxy, Galactic 등)&lt;/td&gt;
&lt;td data-end=&quot;1774&quot; data-start=&quot;1741&quot; data-col-size=&quot;sm&quot;&gt;Fast DDS (rmw_fastrtps_cpp)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1798&quot; data-start=&quot;1775&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1783&quot; data-start=&quot;1775&quot;&gt;MacOS&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1798&quot; data-start=&quot;1783&quot;&gt;Cyclone DDS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1821&quot; data-start=&quot;1799&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1809&quot; data-start=&quot;1799&quot;&gt;Windows&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1821&quot; data-start=&quot;1809&quot;&gt;Fast DDS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;1488&quot; data-end=&quot;1513&quot; data-ke-size=&quot;size26&quot;&gt;5. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 DDS 관련 이슈가 많아서, DDS에 대해 간단히 정리하였다.&amp;nbsp;&lt;/p&gt;</description>
      <category>CS</category>
      <author>codevil</author>
      <guid isPermaLink="true">https://codevil.tistory.com/357</guid>
      <comments>https://codevil.tistory.com/357#entry357comment</comments>
      <pubDate>Sun, 10 Aug 2025 19:41:02 +0900</pubDate>
    </item>
  </channel>
</rss>