Devin.KR

컴포넌트와 제로카피 - 프로세스 내 통신

개발자KR 조회 5

이 장에서 배우는 것

앞 장에서 두리의 센서를 준비하고 활성화하는 순서를 다뤘다. 이제 센서가 내보내는 큰 데이터를 어느 프로세스에서 처리할지 결정한다. 실외 주행에서는 영상, 거리 정보, 인식 결과가 연이어 흐른다. 계산 자체가 빨라도 데이터를 전달하면서 복사와 직렬화를 반복하면 CPU 사용량과 지연이 늘어날 수 있다.

이 장의 핵심은 노드를 한 프로세스에 배치하는 일과 데이터를 복사 없이 전달하는 일을 구별하는 데 있다. 특히 ROS 2 Jazzy의 Python 클라이언트인 rclpy는 C++ 클라이언트인 rclcpp의 컴포넌트 기능과 같은 범위를 제공하지 않는다. 이 차이를 먼저 확인한 뒤, Python으로 가능한 구성과 성능 검증 방법을 익힌다.

  • 컴포저블 노드(composable node)와 컨테이너(container)의 역할을 설명한다.
  • 프로세스 내 통신(intra-process communication)과 제로카피(zero-copy)의 차이를 구별한다.
  • rclpy 노드 여러 개를 한 프로세스에서 실행하고 통신 결과를 확인한다.
  • 순수 Python 예제로 복사 전달과 공유 전달의 비용 차이를 재현한다.

문제 상황

두리의 전방 카메라가 만든 영상을 장애물 인식 노드와 기록 노드가 함께 구독한다고 가정한다. 처음에는 세 노드를 각각 실행 파일로 시작했다. 구조를 이해하기 쉽고, 기록 노드가 종료되어도 카메라 프로세스는 살아남는다. 그러나 영상 해상도와 발행 빈도를 높이자 데이터 전달에 쓰이는 CPU 시간이 눈에 띄기 시작했다.

1920×1080 크기의 3채널 8비트 영상은 화소 데이터만 프레임당 6,220,800바이트다. 초당 30프레임이면 한 데이터 흐름의 화소량이 초당 186,624,000바이트다. 여기에 메시지 메타데이터는 포함하지 않았다. 이 수치는 실제 네트워크 전송량도, 실제 메모리 복사량도 아니다. 다만 큰 데이터를 여러 번 만지는 작업이 왜 부담이 되는지 보여 주는 출발점이다.

개발자는 세 노드를 하나의 실행 파일에 넣으면 전달 비용이 사라질 것으로 예상한다. 하지만 프로세스가 같다는 사실만으로 직렬화나 복사가 없어지지는 않는다. 사용하는 클라이언트 라이브러리, 통신 경로, 메시지 소유권이 함께 맞아야 한다. 두리의 구조를 바꾸기 전에 무엇이 줄어드는지부터 구분해야 한다.

컴포넌트와 컨테이너의 역할

ROS 2에서 컴포넌트(component)는 컨테이너가 실행 중에 불러와 노드 인스턴스를 만들 수 있도록 등록한 구성 단위다. Jazzy의 표준 컴포넌트 구성 체계는 rclcpp_components를 중심으로 제공된다. 일반적으로 C++ 노드를 공유 라이브러리로 빌드하고 등록하면, 컨테이너가 그 라이브러리를 불러와 노드를 생성한다.

컨테이너는 노드들의 실행 공간을 제공하는 프로세스다. 컴포넌트 관리자는 로드와 언로드 요청을 처리하고, 실행기는 생성된 노드의 콜백을 실행한다. 컨테이너를 고를 때는 어떤 실행기를 사용하는지도 확인해야 한다. 프로세스 하나에 노드가 여러 개 있다는 설명만으로 콜백이 병렬로 실행되는지는 알 수 없다.

컴포넌트를 로드하는 것은 노드 인스턴스를 만드는 작업이다. 앞 장에서 다룬 활성화 상태로 전환하는 작업과는 다르다. 수명주기 기능을 가진 노드를 컴포넌트로 제공할 수도 있지만, 그 경우에도 로드와 활성화는 서로 다른 요청이다.

Python에서도 노드 객체 여러 개를 생성해 한 실행기에 추가할 수 있다. 다만 이렇게 만든 일반적인 rclpy 노드는 표준 rclcpp_components 컨테이너에 로드하는 플러그인이 아니다. launch에 실행 항목 여러 개를 적는 것도 한 프로세스 배치를 뜻하지 않는다. 일반적인 노드 실행 항목은 각각 별도 프로세스를 시작한다.

비슷하게 보이는 세 가지 배치의 차이
배치생성 방법프로세스확인할 점
독립 노드 실행실행 파일을 각각 시작보통 여러 개장애 격리와 전달 비용
rclpy 노드 공동 배치Python에서 여러 노드 생성하나복사 제거를 보장하지 않음
rclcpp 컴포넌트 구성등록한 라이브러리를 로드컨테이너 하나에 여러 노드통신 옵션과 소유권 조건
한 프로세스에 노드를 함께 배치해도 복사 제거는 별도 조건이다

공동 배치는 관리 단위를 줄이지만 장애 범위도 바꾼다. 프로세스가 종료되면 그 안의 노드가 함께 종료된다. 두리에서는 영상 전처리와 인식처럼 큰 데이터를 긴밀하게 교환하는 기능을 묶을 이유가 있다. 반면 별도로 재시작해야 하는 기록 기능은 분리할 이유가 있다. 통신량뿐 아니라 재시작과 배포 단위까지 함께 판단해야 한다.

같은 주소 공간과 복사 없는 전달

서로 다른 프로세스는 보통 상대 프로세스의 객체 참조를 그대로 사용할 수 없다. 통신 계층은 메시지를 전달 가능한 표현으로 바꾸고 수신 측에서 다시 사용할 수 있게 처리한다. 직렬화(serialization)는 이런 표현 변환을 가리킨다. 같은 프로세스에서는 객체의 주소 공간을 공유하므로, 조건이 맞으면 데이터 본문을 다시 만들지 않고 참조나 소유권을 전달할 수 있다.

rclcpp는 발행자와 구독자가 같은 프로세스에 있고 관련 조건을 만족할 때 전용 경로를 사용하는 기능을 제공한다. 이 경로의 사용 여부는 노드 옵션 등에 의해 결정된다. 컴포넌트를 같은 컨테이너에 넣었다고 자동으로 모든 연결이 복사 없는 경로가 되는 것은 아니다.

또한 프로세스 내 통신을 사용한다는 설명과 메시지 전체에서 복사가 한 번도 없다는 설명은 다르다. 단일 수신자에게 소유권을 넘기는 경우, 여러 수신자가 읽기 전용 데이터를 함께 보는 경우, 각 수신자가 수정 가능한 사본을 요구하는 경우는 비용이 다르다. 프로세스 밖의 구독자가 추가되면 그쪽을 위한 전달 작업도 필요할 수 있다.

제로카피라는 표현을 사용할 때는 구간을 함께 말해야 한다. 카메라 장치에서 드라이버로 가져오는 구간, ROS 메시지를 만드는 구간, 발행자에서 구독자로 넘기는 구간, Python 배열로 변환하는 구간은 서로 다르다. 가운데 한 구간에서 복사를 없앴어도 앞뒤 변환이 데이터를 복사하면 전체 처리 비용은 남는다.

대여 메시지(loaned message)와 공유 메모리 전송도 구분해야 한다. 대여 메시지는 통신 계층 등이 관리하는 저장 공간을 빌려 사용하는 방식이며, 지원 범위는 구현과 메시지 형식에 영향을 받는다. 공유 메모리는 프로세스 사이에서도 사용할 수 있다. 둘 다 단순히 노드를 같은 프로세스에 배치했다는 뜻은 아니다.

공유 전달은 데이터 본문 복사를 줄이지만 수신자가 원본을 수정하지 않는 조건이 필요하다

Jazzy의 rclpy에는 rclcpp의 프로세스 내 통신 옵션에 대응하는 공개 노드 설정이 없다. 따라서 Python 노드를 한 실행기에 추가한 결과를 rclcpp의 제로카피 실험이라고 해석해서는 안 된다. Python 객체를 콜백에 직접 넘기는 별도 경로를 만들 수는 있지만, 그 경로는 ROS 토픽의 전달 기능과 관찰 가능성을 그대로 제공하지 않는다.

두리의 Python 구성에서는 우선 공동 배치의 효과를 측정한다. 그다음 큰 데이터의 전달 비용이 실제 병목으로 확인되면 해당 구간의 C++ 컴포넌트화 등을 검토한다. 언어 변경은 메시지 크기만 보고 결정할 일이 아니다. 계산 시간, 변환 시간, 전달 시간이 각각 얼마나 드는지 구분한 근거가 필요하다.

기능 범위를 확인할 때는 Jazzy의 컴포넌트 구성 설명과 rclpy 노드 API를 참고할 수 있다. 여기의 예제는 이 문서들의 예제 코드를 사용하지 않고, 공동 배치와 데이터 공유를 따로 관찰하도록 구성한다.

성능 비교에서 고정해야 할 조건

성능 실험은 같은 입력을 같은 수신자가 처리하도록 맞춰야 한다. 메시지 크기나 구독자 수가 달라지면 배치 변경의 효과와 작업량 변경의 효과가 섞인다. 두리의 영상 전달 실험에서는 프레임 크기, 발행 빈도, 구독자 수, 통신 설정, 구독 콜백의 작업량을 고정한다.

비교 지표도 하나로 줄이지 않는다. 평균 전달 시간만 낮아져도 드물게 발생하는 긴 지연은 남을 수 있다. 수신 개수와 지연 분포, CPU 사용량, 메모리 사용량을 함께 기록한다. 시작 직후에는 노드 발견과 초기 할당이 영향을 주므로 준비 구간과 측정 구간을 나눈다. 매 메시지마다 터미널에 출력하는 동작도 측정 결과를 바꾼다.

공동 배치의 효과를 판단하기 위한 관찰 항목
항목기록할 내용해석할 때의 주의점
수신량발행 수와 수신 수누락으로 작업이 줄어든 결과를 개선으로 보지 않음
전달 지연같은 시간 기준의 지연 분포서로 다른 장비의 시계 차이를 섞지 않음
CPU와 메모리관련 프로세스 전체의 사용량한 프로세스의 수치만 비교하지 않음
복사 범위어느 구간에서 무엇을 복사하는지객체 참조 공유를 전체 경로의 제로카피로 확대하지 않음

다음 순수 Python 프로그램은 시간을 측정하지 않는다. 대신 전달 정책에 따른 데이터 본문 복사 횟수와 바이트 수를 일정하게 재현한다. 작은 실험에서 몇 마이크로초의 차이를 출력하는 것보다, 무엇을 세었는지 분명한 결과를 먼저 얻기 위한 선택이다. 이어지는 ROS 프로그램은 실제 rclpy 노드의 공동 배치와 수신 결과를 확인한다.

완성 코드

두 파일은 서로 독립적이다. 첫 파일은 표준 라이브러리만 사용하므로 macOS와 Linux의 Python 3에서 실행할 수 있다. 두 번째 파일은 rclpy와 std_msgs를 사용할 수 있는 Jazzy 환경에서 실행한다. 운영체제 이름만으로 ROS 실행 환경이 갖춰지는 것은 아니며, macOS에서는 해당 Python에서 ROS 패키지를 불러올 수 있는 별도 설치 환경이 필요하다.

delivery_model.py

from dataclasses import dataclass

FRAME_COUNT = 3
PAYLOAD_SIZE = 4096


@dataclass(frozen=True)
class Frame:
    sequence: int
    payload: bytes


class Sink:
    def __init__(self):
        self.received = 0
        self.checksum = 0

    def accept(self, frame):
        expected = self.received + 1
        if frame.sequence != expected:
            raise RuntimeError("sequence mismatch")
        if len(frame.payload) != PAYLOAD_SIZE:
            raise RuntimeError("payload size mismatch")
        self.received += 1
        self.checksum += sum(frame.payload)


def run_case(mode):
    if mode not in ("copy", "share"):
        raise ValueError("unknown delivery mode")

    sinks = [Sink(), Sink()]
    payload_copies = 0
    copied_bytes = 0
    source_shares = 0

    for sequence in range(1, FRAME_COUNT + 1):
        source = Frame(
            sequence=sequence,
            payload=bytes([sequence]) * PAYLOAD_SIZE,
        )

        for sink in sinks:
            if mode == "copy":
                payload = memoryview(source.payload).tobytes()
                delivered = Frame(source.sequence, payload)
                payload_copies += 1
                copied_bytes += len(payload)
            else:
                delivered = source

            if delivered.payload is source.payload:
                source_shares += 1
            sink.accept(delivered)

    received = sum(sink.received for sink in sinks)
    checksum = sum(sink.checksum for sink in sinks)
    expected_checksum = (
        len(sinks)
        * PAYLOAD_SIZE
        * sum(range(1, FRAME_COUNT + 1))
    )

    assert received == len(sinks) * FRAME_COUNT
    assert checksum == expected_checksum
    assert source_shares == (received if mode == "share" else 0)

    print(
        f"mode={mode} received={received} "
        f"payload_copies={payload_copies} "
        f"copied_bytes={copied_bytes} "
        f"source_shares={source_shares} "
        f"checksum={checksum}"
    )


def main():
    run_case("copy")
    run_case("share")


if __name__ == "__main__":
    main()

duri_same_process.py

import time

import rclpy
from rclpy.executors import SingleThreadedExecutor
from rclpy.node import Node
from std_msgs.msg import UInt8MultiArray

FRAME_COUNT = 3
PAYLOAD_SIZE = 4096
TOPIC = "/duri/demo/frame"


class Source(Node):
    def __init__(self):
        super().__init__("duri_frame_source")
        self.publisher = self.create_publisher(
            UInt8MultiArray, TOPIC, 10
        )
        self.sent = 0
        self.timer = self.create_timer(0.1, self.publish_frame)

    def publish_frame(self):
        if self.sent >= FRAME_COUNT:
            return
        if self.publisher.get_subscription_count() < 2:
            return

        value = self.sent + 1
        message = UInt8MultiArray()
        message.data = [value] * PAYLOAD_SIZE
        self.publisher.publish(message)
        self.sent = value


class Sink(Node):
    def __init__(self, name):
        super().__init__(name)
        self.received = 0
        self.checksum = 0
        self.subscription = self.create_subscription(
            UInt8MultiArray, TOPIC, self.accept, 10
        )

    def accept(self, message):
        expected = self.received + 1
        if len(message.data) != PAYLOAD_SIZE:
            raise RuntimeError("payload size mismatch")
        if any(value != expected for value in message.data):
            raise RuntimeError("payload content mismatch")
        self.received += 1
        self.checksum += sum(message.data)


def main():
    rclpy.init()
    executor = SingleThreadedExecutor()
    nodes = []

    try:
        nodes.append(Source())
        nodes.append(Sink("duri_perception_sink"))
        nodes.append(Sink("duri_record_sink"))

        for node in nodes:
            executor.add_node(node)

        source, perception, recorder = nodes
        sinks = (perception, recorder)
        deadline = time.monotonic() + 10.0

        while not all(
            sink.received == FRAME_COUNT for sink in sinks
        ):
            if time.monotonic() >= deadline:
                raise TimeoutError("message delivery timed out")
            executor.spin_once(timeout_sec=0.1)

        total = sum(sink.checksum for sink in sinks)
        expected_total = (
            len(sinks)
            * PAYLOAD_SIZE
            * sum(range(1, FRAME_COUNT + 1))
        )
        if total != expected_total:
            raise RuntimeError("checksum mismatch")

        print(
            f"sent={source.sent} "
            f"perception={perception.received} "
            f"recorder={recorder.received} "
            f"checksum={total}"
        )
    finally:
        executor.shutdown()
        for node in reversed(nodes):
            node.destroy_node()
        rclpy.shutdown()


if __name__ == "__main__":
    main()

줄별 해설

변하지 않는 데이터와 명시적인 복사

첫 파일의 Frame은 순번과 데이터 본문을 묶는다. frozen=True는 필드 재할당을 막는다. 본문에는 변경할 수 없는 bytes를 사용하므로, 공유한 수신자가 내용을 제자리에서 바꿀 수 없다. 이 두 조건을 함께 사용해야 예제가 의도한 읽기 전용 공유가 성립한다.

Sink.accept는 수신 순서와 길이를 먼저 검사한다. 그 뒤 바이트 값을 더해 누적한다. 합계를 계산하는 작업은 두 전달 모드에서 동일하다. 공유 모드도 데이터를 읽는 비용과 콜백 실행 비용은 그대로 지불한다.

run_case는 매번 수신자 두 개를 새로 만든다. 따라서 앞선 복사 실험의 카운터가 공유 실험에 남지 않는다. 바깥 반복문은 세 프레임을 생성하고, 안쪽 반복문은 같은 프레임을 두 수신자에게 전달한다.

memoryview(...).tobytes()는 본문의 새 bytes 사본을 만든다. 반면 delivered = source는 기존 Frame을 가리키는 참조를 사용한다. 이 예제에서는 데이터가 비어 있지 않으므로, 본문의 객체 동일성을 검사해 원본 공유 여부를 확인할 수 있다.

payload_copies와 copied_bytes는 전달을 위해 명시적으로 만든 본문 사본만 센다. 원본 생성, Frame 객체 생성, 참조 관리에 필요한 비용까지 센 결과는 아니다. source_shares도 실제 ROS 통신 내부를 계측한 값이 아니라 이 프로그램의 전달 정책을 확인하는 값이다.

마지막 검증식은 두 모드가 같은 데이터를 같은 횟수만큼 처리했는지 확인한다. 수신 이벤트는 3×2로 6개이며, 전체 합계는 4096×(1+2+3)×2로 49,152다. 복사 모드의 전달용 복사량은 4096×3×2로 24,576바이트다.

Python 노드 세 개를 한 실행기에 배치하기

두 번째 파일의 Source는 토픽 발행자와 타이머를 가진 노드다. get_subscription_count()로 구독자가 둘 이상 발견된 뒤 전송을 시작한다. 이 검사는 시작 직후 구독자가 아직 발견되지 않아 샘플을 놓치는 상황을 줄인다. 임의의 시스템에서 모든 수신자의 준비 상태를 확인하는 일반적인 절차를 대신하지는 않는다.

UInt8MultiArray는 이 예제에서 바이트 배열을 운반하기 위한 메시지다. 실제 영상 메시지의 구조를 흉내 내기 위한 선택은 아니다. 각 메시지에는 1, 2, 3 중 하나를 4096개 채우므로 수신자가 길이뿐 아니라 내용과 순서를 검사할 수 있다.

main은 발행 노드 하나와 수신 노드 둘을 같은 Python 프로세스에서 생성한다. executor.add_node는 노드의 콜백을 실행 대상에 넣는다. 이 호출은 컴포넌트 플러그인을 로드하거나 프로세스 내 통신 최적화를 켜는 설정이 아니다.

SingleThreadedExecutor를 사용하므로 이 프로그램의 콜백은 한 실행 스레드에서 순차적으로 처리된다. 공유 데이터 구조에 대한 별도 잠금은 필요하지 않지만, 수신 콜백이 오래 걸리면 다른 콜백도 기다린다. 이번 코드는 전달 확인용이므로 콜백의 처리량을 성능 결과로 제시하지 않는다.

time.monotonic은 제한 시간을 판단하는 데 사용한다. 두 수신자가 각각 세 번 받으면 반복을 끝내고 결과 한 줄을 출력한다. finally에서는 실행기를 정지하고 노드를 정리한다. 제한 시간 초과나 데이터 검사 실패가 발생해도 정리 경로를 거친다.

실행 결과

먼저 두 파일의 문법을 확인한다. 다음 명령은 ROS 모듈을 불러오지 않으므로 ROS가 없는 환경에서도 두 파일을 컴파일할 수 있다. 경고를 오류로 취급하며, 성공하면 출력이 없다. 파일이 있는 디렉터리에 캐시 파일을 쓸 수 있어야 한다.

python3 -W error -m py_compile delivery_model.py duri_same_process.py

순수 Python 예제의 명령과 예상 표준 출력은 다음과 같다. 이 결과는 실행 시간을 포함하지 않으므로 정상 실행에서는 동일한 숫자가 나온다.

python3 delivery_model.py
mode=copy received=6 payload_copies=6 copied_bytes=24576 source_shares=0 checksum=49152
mode=share received=6 payload_copies=0 copied_bytes=0 source_shares=6 checksum=49152

ROS 예제는 Jazzy 환경을 적용한 셸에서 실행한다. 같은 토픽을 사용하는 다른 발행자와 구독자가 없는 조건으로 시험한다. 정상 수신 시 프로그램이 출력하는 결과는 다음과 같다.

python3 duri_same_process.py
sent=3 perception=3 recorder=3 checksum=49152

첫 결과는 본문을 공유해도 계산 결과가 같다는 사실을 보여 준다. 두 번째 결과는 한 프로세스의 rclpy 노드들이 토픽으로 메시지를 주고받았다는 사실을 보여 준다. 두 결과를 합쳐 rclpy가 본문을 복사 없이 전달했다고 결론 내릴 수는 없다.

제시한 출력은 코드에서 도출한 예상값이며 이 원고에서 실제 실행을 확인한 기록은 아니다. 특히 ROS 실행 결과는 설치 환경과 노드 발견이 정상이라는 전제에 따른다. 문법 검사 성공도 ROS 의존성 설치나 통신 성공까지 확인하지는 않는다.

실무에서 자주 틀리는 것

rclcpp의 옵션을 rclpy 생성자에 넣는다

다음 코드는 Jazzy의 rclpy.Node가 지원하지 않는 키워드 인자를 사용한다. 다른 클라이언트 라이브러리의 옵션 이름을 Python 생성자에 그대로 옮기면 실행 시 오류가 난다.

from rclpy.node import Node

# 잘못된 생성자 인자다.
node = Node("duri_camera", use_intra_process_comms=True)

rclpy에서는 지원하는 인자로 노드를 만들고 공동 배치를 구성한다. 다음 수정은 API 사용을 바로잡는 것이며, 제로카피를 켜는 대체 설정은 아니다. 두 조각 모두 노드 생성 전에 rclpy.init()이 호출된 문맥을 전제로 한다.

from rclpy.executors import SingleThreadedExecutor
from rclpy.node import Node

node = Node("duri_camera")
executor = SingleThreadedExecutor()
executor.add_node(node)

불변 객체의 재생성을 실제 복사로 센다

다음 코드는 bytes 생성자를 호출했으므로 본문이 복사되었다고 가정한다. 하지만 bytes를 다시 bytes에 전달하는 동작은 기존 객체를 재사용할 수 있다. 생성자 호출 횟수를 복사 횟수로 세면 실험의 전제가 흔들린다.

payload = b"\x01" * 4096

# 새 사본이 생겼다고 가정하면 안 된다.
copied = bytes(payload)

복사를 의도한 실험에서는 동작이 분명한 경로를 사용한다. 다음 코드는 새 본문을 만들고 내용과 객체 동일성을 따로 확인한다.

payload = b"\x01" * 4096
copied = memoryview(payload).tobytes()

assert copied == payload
assert copied is not payload

읽기 전용 뷰가 원본 변경까지 막는다고 생각한다

읽기 전용 뷰는 그 뷰를 통한 쓰기를 막는다. 원본 bytearray를 가진 다른 코드의 수정까지 막지는 않는다. 다음 예제에서 관찰자는 생산자의 변경을 그대로 보게 된다.

storage = bytearray([1, 1, 1])
observer = memoryview(storage).toreadonly()
storage[0] = 9

assert observer[0] == 9

수신 내용이 바뀌면 안 되는 계약이라면 불변 스냅샷을 전달할 수 있다. 다음 수정에는 bytes를 만드는 시점의 복사 비용이 있다. 비용을 줄이려면 생성 단계부터 불변 데이터를 사용하거나, 원본을 수정하지 못하도록 소유권과 수명을 관리해야 한다.

storage = bytearray([1, 1, 1])
snapshot = bytes(storage)
observer = memoryview(snapshot)
storage[0] = 9

assert observer[0] == 1

두리의 카메라 드라이버가 버퍼를 재사용한다면 이 구분이 특히 중요하다. 구독자가 읽는 동안 생산자가 같은 저장 공간을 다음 프레임으로 덮어쓰지 않도록 계약해야 한다. 데이터 본문을 공유하는 순간부터 수명 관리가 통신 설계의 일부가 된다.

한눈에 보기

배치와 전달 방식을 판단하는 핵심 기준
개념의미두리에서의 판단
컴포저블 노드컨테이너가 생성할 수 있도록 제공한 노드Jazzy의 표준 구성은 rclcpp 중심
컨테이너여러 컴포넌트를 실행하는 프로세스통신량과 공동 장애 범위를 함께 검토
rclpy 공동 배치한 Python 프로세스에서 여러 노드 실행실행기 추가만으로 복사 제거를 주장하지 않음
프로세스 내 통신같은 주소 공간을 활용하는 전달 경로사용 조건과 실제 적용 여부 확인
제로카피정한 구간에서 본문 사본을 만들지 않는 전달구간, 소유권, 수명을 명시
성능 검증동일한 작업의 지연과 자원 사용 비교수신량을 먼저 맞추고 전체 비용 관찰

두리의 배치는 데이터가 이동하는 양과 운영 요구를 함께 보고 정한다. 본문을 공유하는 설계에서는 수정 권한과 저장 공간의 수명을 먼저 정한다. 다음 장에서는 이렇게 실행되는 노드들이 공통으로 사용할 로봇 모델을 URDF로 표현한다.

연습 문제

  1. 순수 Python 예제에서 수신자를 세 개로 늘린다. 프레임 수와 본문 크기는 유지할 때 두 모드의 received, payload_copies, copied_bytes, source_shares, checksum 값을 계산한다.
  2. frozen=True인 Frame의 payload 형식을 bytearray로 바꾸면 본문 내용도 변경할 수 없게 되는지 설명한다. 공유 전달에 필요한 조건을 함께 적는다.
  3. rclpy 노드 세 개를 한 실행기에 추가한 뒤 평균 지연이 줄었다. 이 결과만으로 제로카피가 적용되었다고 말할 수 있는지 설명하고, 추가로 확인할 항목 두 가지를 적는다.
  4. 두리의 인식 노드는 낮은 지연이 필요하고 기록 노드는 독립적인 재시작이 필요하다. 두 기능을 같은 컨테이너에 넣을지 판단하고, 프로세스 밖 구독자가 있을 때 복사 없는 전달의 범위를 어떻게 설명할지 적는다.

정답과 해설

  1. 수신 이벤트는 3프레임×3수신자로 9개다. 복사 모드는 payload_copies=9, copied_bytes=36864, source_shares=0이다. 공유 모드는 payload_copies=0, copied_bytes=0, source_shares=9이다. 두 모드 모두 received=9, checksum=73728이다. 수신자를 늘려도 공유 모드의 본문 복사는 늘지 않지만, 각 수신자의 합계 계산과 함수 호출은 늘어난다.
  2. 변경할 수 있다. frozen=True는 필드를 다른 객체로 재할당하는 작업을 제한하며, 필드가 가리키는 가변 객체의 내부 변경까지 막지 않는다. bytearray의 요소는 수정할 수 있다. 공유 전달에는 수신 중 수정하지 않는 규칙과 데이터 수명 보장이 필요하다. 이 예제처럼 bytes를 사용하면 내용 변경을 자료형 수준에서 막을 수 있다.
  3. 말할 수 없다. 프로세스 수가 줄면서 스케줄링이나 다른 실행 비용이 달라졌을 수 있다. 먼저 같은 메시지 수가 실제로 처리되었는지 확인하고, 전달 경로에서 본문 복사나 직렬화가 어떻게 발생하는지 계측해야 한다. CPU와 메모리 사용량, 지연 분포를 함께 비교하면 개선의 범위도 더 분명해진다.
  4. 기록 기능의 독립적인 재시작이 중요하다면 별도 프로세스로 두는 선택이 타당하다. 큰 데이터를 주고받는 전처리와 인식 구간은 지원 조건을 확인한 뒤 함께 배치할 수 있다. 이때 복사 없는 전달이 성립하더라도 해당 내부 구간에 한정해 설명해야 한다. 기록 노드로 나가는 경로의 직렬화와 복사 여부는 별도로 확인한다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.