# 다중 노드 Launch 예시

ROS2 Humble에서 여러 노드를 동시에 실행하기 위해서는 하나의 Launch 파일 내에서 여러 개의 Node 액션을 정의하고 이를 LaunchDescription에 추가하는 방식으로 구성한다. 이를 통해 각 노드별 파라미터, 환경 변수, 리마핑, 콜백 그룹 설정 등을 일괄적으로 관리할 수 있다. 또한 Launch 시스템에 존재하는 다양한 액션과 조건문, 이벤트 핸들러를 통해 노드 간의 종속성이나 실행 순서를 제어할 수도 있다.

#### 기본 구조 이해

ROS2의 Python Launch 파일은 일반적으로 다음과 같은 형태를 갖는다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    # 여러 Node, Action, Event 등을 정의
    # 예: talker_node, listener_node 등
    
    return LaunchDescription([
        # 정의된 Node/Action/Event를 추가
    ])
```

위와 같은 기본 구조에서, 여러 노드를 동시에 실행하려면 `Node` 액션을 여러 개 만들고 `LaunchDescription` 객체에 차례로 나열하면 된다. 예를 들어 메시지를 퍼블리시하는 노드와 구독하는 노드를 동시에 기동하려면 아래와 같이 작성할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    talker_node = Node(
        package='demo_nodes_cpp',
        executable='talker',
        name='talker',
        output='screen'
    )

    listener_node = Node(
        package='demo_nodes_py',
        executable='listener',
        name='listener',
        output='screen'
    )

    return LaunchDescription([
        talker_node,
        listener_node
    ])
```

이렇게 만들어진 Launch 파일을 통해 `$ ros2 launch <패키지명> <launch파일이름>` 명령어로 여러 노드를 동시에 실행할 수 있다.

#### 노드별 파라미터 전달

다중 노드 환경에서 노드마다 다른 파라미터를 주어야 하는 경우가 많다. ROS2 Humble에서 파라미터를 설정하기 위해서는 `Node` 액션에 `parameters` 인자를 사용하면 된다. 예시:

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    camera_node = Node(
        package='my_camera_pkg',
        executable='camera_driver',
        name='camera',
        output='screen',
        parameters=[
            {'camera_fps': 30},
            {'resolution_width': 1280},
            {'resolution_height': 720}
        ]
    )

    object_detector_node = Node(
        package='my_vision_pkg',
        executable='object_detector',
        name='object_detector',
        output='screen',
        parameters=[
            {'model_path': '/home/user/models/detector.onnx'},
            {'threshold': 0.5}
        ]
    )

    return LaunchDescription([
        camera_node,
        object_detector_node
    ])
```

위와 같이 `parameters`에 리스트 형태의 딕셔너리를 사용하여 노드에 전달할 파라미터를 기술할 수 있다. 이를 통해 카메라 노드와 오브젝트 디텍터 노드에 각각 다른 설정값을 부여할 수 있다.

#### YAML 파일을 통한 파라미터 관리

파라미터가 많은 경우에는 YAML 파일에 파라미터를 정리해두고, Launch 파일에서 해당 YAML 파일 경로를 지정해주는 방식이 편리하다. 예시:

```python
camera_node = Node(
    package='my_camera_pkg',
    executable='camera_driver',
    name='camera',
    output='screen',
    parameters=['config/camera_params.yaml']
)
```

이렇게 하면 `camera_params.yaml` 파일 안에 정의된 모든 파라미터가 `camera_driver` 노드에 자동으로 로드된다. 이를 통해 대규모 시스템에서 파라미터를 YAML 파일로 효율적으로 관리할 수 있다.

#### 다중 노드 구조 시각화

아래 다이어그램은 하나의 Launch 파일에서 여러 노드를 동시에 실행하는 구조를 간략히 나타낸다.

{% @mermaid/diagram content="flowchart TB
A\[Launch File] --> B\[Node: Camera]
A\[Launch File] --> C\[Node: Object Detector]
A\[Launch File] --> D\[Node: Image Viewer]" %}

* **Camera**: 카메라 스트림을 퍼블리시하는 노드
* **Object Detector**: 카메라 데이터를 받아서 오브젝트 검출을 수행하는 노드
* **Image Viewer**: 처리된 이미지를 화면에 뿌려주는 노드

이러한 구조는 Launch 시스템에서 노드 수에 제한이 없으므로, 원하는 개수만큼 Node 액션을 추가함으로써 확장할 수 있다.

#### Launch 변수와 리마핑

여러 노드를 동시에 실행할 때, 노드 이름이나 토픽 이름이 충돌하는 것을 방지하기 위해 리마핑(Remapping) 기능이 자주 쓰인다. 또한 LaunchConfiguration과 DeclareLaunchArgument를 통해 런치 파일 실행 시 인자를 받아와서 노드에 할당할 수도 있다. 예시:

```python
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():
    camera_name_arg = DeclareLaunchArgument(
        'camera_name',
        default_value='camera',
        description='Camera node name'
    )
    camera_name = LaunchConfiguration('camera_name')

    camera_node = Node(
        package='my_camera_pkg',
        executable='camera_driver',
        name=camera_name,
        output='screen',
        remappings=[
            ('/raw_image', '/camera/image_raw')
        ],
        parameters=['config/camera_params.yaml']
    )

    return LaunchDescription([
        camera_name_arg,
        camera_node
    ])
```

위 예시에서 `$ ros2 launch <패키지명> <launch파일이름> camera_name:=stereo_camera` 와 같이 인자를 주어 실행하면 노드 이름을 유연하게 바꿀 수 있다. 또한 `remappings` 옵션을 사용하면 원래 노드가 사용하는 토픽명을 다른 이름으로 매핑할 수 있다.

#### 상호 종속 노드 런치

때로는 특정 노드가 먼저 준비된 상태에서 다른 노드를 실행해야 하는 경우가 있다. 예컨대, DB 서버 노드가 먼저 실행되어야만 이후에 의존성 있는 노드들이 정상 동작할 수 있다. 이러한 상황을 제어하기 위해서는 Launch 이벤트와 조건문을 사용할 수 있다.

```python
from launch import LaunchDescription
from launch.actions import RegisterEventHandler
from launch.event_handlers.on_process_start import OnProcessStart
from launch_ros.actions import Node

def generate_launch_description():
    db_server_node = Node(
        package='my_database_pkg',
        executable='db_server',
        name='db_server',
        output='screen'
    )

    client_node = Node(
        package='my_client_pkg',
        executable='client_app',
        name='client_app',
        output='screen'
    )

    # db_server가 시작된 이후에 client_node를 시작
    event_handler = RegisterEventHandler(
        OnProcessStart(
            target_action=db_server_node,
            on_start=[client_node]
        )
    )

    return LaunchDescription([
        db_server_node,
        event_handler
    ])
```

이 코드는 `db_server_node`가 실제로 실행을 시작한 후(`OnProcessStart` 이벤트 발생 시) `client_node`를 실행하도록 설정한다. 즉, 다중 노드 환경에서 발생할 수 있는 순서 종속성을 Launch 시스템 레벨에서 제어하여, 안전하고 확실한 애플리케이션 실행 순서를 보장할 수 있다.

#### Launch 파일 분할과 IncludeLaunchDescription

시스템 규모가 커지면, 하나의 Launch 파일에 모든 노드를 정의하기가 복잡해진다. 이때는 여러 개의 Launch 파일로 분할하여 유지보수성을 높일 수 있으며, `IncludeLaunchDescription` 액션을 통해 상위 Launch 파일에서 하위 Launch 파일을 가져와 실행할 수 있다.

```python
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    # 예: my_camera_pkg의 camera.launch.py
    camera_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            [get_package_share_directory('my_camera_pkg'), '/launch/camera.launch.py']
        )
    )

    # 예: my_vision_pkg의 vision.launch.py
    vision_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            [get_package_share_directory('my_vision_pkg'), '/launch/vision.launch.py']
        )
    )

    return LaunchDescription([
        camera_launch,
        vision_launch
    ])
```

위 예시에서는 `camera.launch.py`와 `vision.launch.py`를 각각 독립된 파일로 관리하고, 상위 Launch 파일에서 두 파일을 모두 포함해 실행하는 방식이다. 이렇게 분할된 구조를 통해 노드 세팅이나 파라미터 정의를 모듈 단위로 나눠서 관리할 수 있다.

#### GroupAction을 통한 논리적 그룹화

여러 노드를 서로 관련된 그룹으로 묶고 싶다면 `GroupAction`을 사용할 수 있다. 예를 들어, 카메라 노드와 관련된 노드를 한 그룹으로 묶고, 비전(vision) 노드와 관련된 노드를 다른 그룹으로 묶는 식으로 구분할 수 있다. 또한 그룹 단위로 리마핑, 파라미터 설정, 환경 변수 지정 등을 적용할 수도 있다.

```python
from launch import LaunchDescription
from launch.actions import GroupAction
from launch_ros.actions import Node
from launch_ros.actions import PushRosNamespace

def generate_launch_description():
    camera_group = GroupAction([
        PushRosNamespace('camera_ns'),
        Node(
            package='my_camera_pkg',
            executable='camera_driver',
            name='camera_driver',
            output='screen'
        ),
        Node(
            package='my_camera_pkg',
            executable='camera_info_publisher',
            name='camera_info_pub',
            output='screen'
        )
    ])

    vision_group = GroupAction([
        PushRosNamespace('vision_ns'),
        Node(
            package='my_vision_pkg',
            executable='image_processor',
            name='image_processor',
            output='screen'
        ),
        Node(
            package='my_vision_pkg',
            executable='detector',
            name='detector',
            output='screen'
        )
    ])

    return LaunchDescription([
        camera_group,
        vision_group
    ])
```

* **PushRosNamespace**: 그룹 내에서 모든 노드가 공유하는 네임스페이스를 지정한다. 위 코드에서는 `camera_ns`와 `vision_ns`라는 네임스페이스로 각각 그룹화했다.
* **GroupAction**: 내부 액션(여기서는 Node 액션들)을 묶어주는 역할을 하며, 그룹 레벨에서 공통 적용될 설정을 포괄한다.

#### LaunchConfiguration 비교 연산과 조건부 실행

런치 파일을 실행할 때 전달되는 인자(LaunchArgument)를 비교하여 특정 노드만 실행하거나, 혹은 특정 파라미터 설정을 달리 적용해야 할 때가 있다. 이 경우 조건문을 사용하여 유연하게 노드 실행 여부를 결정할 수 있다. 예시:

```python
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, OpaqueFunction
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def conditional_nodes(context, *args, **kwargs):
    use_vision = LaunchConfiguration('use_vision').perform(context)
    
    nodes_to_start = []
    
    # 공통 노드
    nodes_to_start.append(
        Node(
            package='my_camera_pkg',
            executable='camera_driver',
            name='camera',
            output='screen'
        )
    )
    
    # 조건부 노드
    if use_vision.lower() == 'true':
        nodes_to_start.append(
            Node(
                package='my_vision_pkg',
                executable='vision_node',
                name='vision_node',
                output='screen'
            )
        )
    
    return nodes_to_start

def generate_launch_description():
    use_vision_arg = DeclareLaunchArgument(
        'use_vision',
        default_value='false',
        description='Whether to use vision_node or not'
    )
    
    return LaunchDescription([
        use_vision_arg,
        OpaqueFunction(function=conditional_nodes)
    ])
```

* `use_vision` 런치 인자를 통해 `vision_node`를 실행할지 여부를 결정한다.
* `OpaqueFunction` 액션 내에서 실제 런치 시점의 인자값을 확인하고 노드 실행 목록을 동적으로 생성한다.

#### TimerAction으로 지연 실행

어떤 노드는 조금 늦게 실행해야 할 수도 있다. 예를 들어, 초기화 시간이 걸리는 하드웨어 노드가 먼저 띄워진 뒤에 다른 노드를 기동해야 한다면, `TimerAction`을 사용하여 특정 시간 뒤에 노드를 실행할 수 있다.

```python
from launch import LaunchDescription
from launch.actions import TimerAction
from launch_ros.actions import Node

def generate_launch_description():
    init_node = Node(
        package='hardware_init_pkg',
        executable='init_driver',
        name='init_driver',
        output='screen'
    )

    delayed_node = TimerAction(
        period=5.0,
        actions=[
            Node(
                package='app_pkg',
                executable='app_main',
                name='app_main',
                output='screen'
            )
        ]
    )

    return LaunchDescription([
        init_node,
        delayed_node
    ])
```

위 예시에서는 `init_driver`가 먼저 실행되고, 5초 뒤에 `app_main` 노드가 실행되도록 구성했다. 이 방식은 하드웨어 장치 초기화나 네트워크 연결 대기 등, 지연이 필요한 상황에 유용하다.

#### 노드 재시작(Respawn) 설정

ROS2 노드가 예기치 않게 종료(Crash)되거나 특정 조건에서 종료되었을 때 자동으로 재시작하고 싶다면, `Node` 액션에 `respawn=True` 옵션을 줄 수 있다. 이때 재시작 간격을 `respawn_delay`로 지정하면 일정 시간 후 노드를 재시작하도록 설정할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    unstable_node = Node(
        package='my_unstable_pkg',
        executable='unstable_node',
        name='unstable_node',
        output='screen',
        respawn=True,
        respawn_delay=2.0
    )

    return LaunchDescription([
        unstable_node
    ])
```

* **respawn**: `True`로 설정하면 노드가 죽었을 때 자동으로 다시 띄운다.
* **respawn\_delay**: 노드가 종료된 후 재시작하기까지 대기하는 시간(초 단위)을 지정한다.

#### 조건부 노드 실행 (IfCondition/UnlessCondition)

런치 파일에서 특정 노드를 실행할지 말지를 런치 시점에 단순 불린 변수로 결정해야 할 수도 있다. 이 경우 `Node` 액션의 `condition` 인자에 `IfCondition` 또는 `UnlessCondition`을 줄 수 있다.

```python
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch.conditions import IfCondition, UnlessCondition
from launch_ros.actions import Node

def generate_launch_description():
    enable_sensor_arg = DeclareLaunchArgument(
        'enable_sensor',
        default_value='true',
        description='Enable sensor node'
    )

    sensor_node = Node(
        package='my_sensors_pkg',
        executable='sensor_driver',
        name='sensor_driver',
        output='screen',
        condition=IfCondition(LaunchConfiguration('enable_sensor'))
    )

    debug_node = Node(
        package='my_debug_pkg',
        executable='debug_tool',
        name='debug_tool',
        output='screen',
        condition=UnlessCondition(LaunchConfiguration('enable_sensor'))
    )

    return LaunchDescription([
        enable_sensor_arg,
        sensor_node,
        debug_node
    ])
```

* **IfCondition**: 인자로 주어진 조건이 참일 때만 액션을 실행한다.
* **UnlessCondition**: 인자로 주어진 조건이 거짓일 때만 액션을 실행한다.

위 예시에서 `enable_sensor`가 `true`면 `sensor_node`가 기동되고, `debug_node`는 실행되지 않는다. 반대로 `enable_sensor`가 `false`라면 `sensor_node`는 실행되지 않고, `debug_node`가 실행된다.

#### 환경 변수 설정

ROS2 노드 실행 시 필요한 환경 변수를 세팅하는 것도 가능하다. `Node` 액션의 `env` 인자를 통해 노드가 실행되는 프로세스 레벨에서만 적용되는 환경 변수를 지정할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    sensor_node = Node(
        package='my_sensors_pkg',
        executable='sensor_driver',
        name='sensor_driver',
        output='screen',
        env={
            'SENSOR_MODE': 'high_accuracy',
            'LOG_LEVEL': 'DEBUG'
        }
    )

    return LaunchDescription([
        sensor_node
    ])
```

이렇게 하면 `sensor_driver` 실행 시에 `SENSOR_MODE=high_accuracy`와 `LOG_LEVEL=DEBUG`가 적용된 상태로 프로세스가 시작된다.

#### 로깅 설정 및 출력 관리

다중 노드가 동시에 실행될 때, 로그가 뒤섞여서 보기 어려운 경우가 발생한다. 이를 해결하기 위해 로그 레벨을 적절히 조절하거나, 로깅 출력을 파일로 분리할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    talker_node = Node(
        package='demo_nodes_cpp',
        executable='talker',
        name='talker',
        output='screen',
        # --ros-args는 CLI로 node에 인자 전달할 때와 같은 기능
        arguments=['--ros-args', '--log-level', 'INFO']
    )

    listener_node = Node(
        package='demo_nodes_py',
        executable='listener',
        name='listener',
        output='log',
        arguments=['--ros-args', '--log-level', 'DEBUG']
    )

    return LaunchDescription([
        talker_node,
        listener_node
    ])
```

* **output='screen'**: 노드 로그를 터미널 콘솔에 출력.
* **output='log'**: 노드 로그를 ROS2 로깅 시스템 경로(기본적으로 \~/.ros/log/)에 파일로 기록.
* **arguments=\['--ros-args', '--log-level', 'DEBUG']**: 노드 기동 시 로깅 레벨을 DEBUG로 지정.

#### Composable Node 구성

ROS2는 `Composable Node` 개념을 통해 하나의 프로세스 안에 여러 노드를 동적 로드/언로드할 수 있도록 한다. Launch 파일에서 `ComposableNodeContainer`와 `ComposableNode` 액션을 사용하면, 프로세스 하나에 여러 컴포저블 노드를 올릴 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

def generate_launch_description():
    container = ComposableNodeContainer(
        name='my_container',
        namespace='',
        package='rclcpp_components',
        executable='component_container_mt',
        composable_node_descriptions=[
            ComposableNode(
                package='my_camera_pkg',
                plugin='my_camera_pkg::CameraNode',
                name='camera_node',
                parameters=[{'camera_fps': 30}]
            ),
            ComposableNode(
                package='my_vision_pkg',
                plugin='my_vision_pkg::VisionProcessor',
                name='vision_processor'
            )
        ],
        output='screen'
    )

    return LaunchDescription([
        container
    ])
```

* **ComposableNodeContainer**: 컴포저블 노드를 담는 컨테이너 프로세스 생성.
* **ComposableNode**: 실제로 로드될 노드 정의로, 클래스(`plugin`) 형태로 등록되어야 한다.
* `component_container_mt`는 멀티스레드용 기본 컨테이너 실행 파일이며, 싱글 스레드용은 `component_container`를 사용한다.

위와 같이 하나의 프로세스에 여러 노드를 올림으로써 프로세스간 통신 오버헤드를 줄이거나, 노드 간 메시지 교환을 좀 더 효율적으로 처리할 수 있다. 단, 노드들이 자원을 공유하기 때문에 충돌이 없는지 주의해야 한다.

#### Constellation 예시 (Mermaid)

아래 다이어그램 예시는 "카메라 -> 비전 -> 퍼블리셔 -> 데이터 로거"가 모두 컴포저블 노드 형태로 하나의 컨테이너 안에서 실행되고, 그 밖의 별도 노드(예: UI)도 함께 실행되는 구조를 나타낸다.

{% @mermaid/diagram content="flowchart TB
A\[ComposableNodeContainer] --> B\[CameraNode]
A\[ComposableNodeContainer] --> C\[VisionProcessor]
A\[ComposableNodeContainer] --> D\[PublisherNode]
A\[ComposableNodeContainer] --> E\[DataLogger]
F\[UI Node] -->|Separate Process| F" %}

* **CameraNode**: 카메라 영상을 획득하여 토픽 퍼블리시
* **VisionProcessor**: 영상 처리 알고리즘 수행
* **PublisherNode**: 처리된 결과를 퍼블리시
* **DataLogger**: ROS2 로그 혹은 DB에 결과 저장
* **UI Node**: 다른 프로세스에서 유저 인터페이스 담당

모두 별도 노드로 띄워도 되지만, 하나의 프로세스에서 컴포저블 노드로 묶으면 상호 통신이 빠를 수 있다.

#### Lifecycle Node와 Launch 시스템

ROS2에서 제공하는 Lifecycle Node는 노드의 상태(UNCONFIGURED, INACTIVE, ACTIVE 등)를 명시적으로 관리하고, 각 상태 전이마다 특정 콜백을 실행하도록 하는 노드이다. 이 Lifecycle Node를 Launch 파일로 구동할 때는 일반 Node 액션이 아닌, Lifecycle Node 전용 액션이 필요하거나, 노드 내에서 서비스 콜을 통해 상태 전이를 트리거해야 한다.

예를 들어, `lifecycle_talker`라는 데모 노드가 있다면, 이를 Launch로 실행하는 기본적인 방법은 다음과 비슷하다.

```python
from launch import LaunchDescription
from launch_ros.actions import LifecycleNode
from launch.actions import RegisterEventHandler
from launch.event_handlers.on_process_exit import OnProcessExit
from launch.actions import EmitEvent
from launch_ros.events.lifecycle import ChangeState
from launch_ros.event_handlers import OnStateTransition
from launch_ros.lifecycle import LifecycleNode as LifecycleNodeAction
from lifecycle_msgs.msg import Transition

def generate_launch_description():
    lifecycle_talker_node = LifecycleNode(
        package='lifecycle_pkg',
        executable='lifecycle_talker',
        name='lifecycle_talker',
        output='screen'
    )

    # UNCONFIGURED -> INACTIVE 전이 요청
    configure_event = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == lifecycle_talker_node, 
            transition_id=Transition.TRANSITION_CONFIGURE
        )
    )

    # INACTIVE -> ACTIVE 전이 요청
    activate_event = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == lifecycle_talker_node, 
            transition_id=Transition.TRANSITION_ACTIVATE
        )
    )

    # 노드 프로세스가 시작된 직후 Configure와 Activate를 순차적으로 수행
    on_node_start_configure_activate = RegisterEventHandler(
        OnStateTransition(
            target_lifecycle_node=lifecycle_talker_node,
            start_state='unconfigured',
            goal_state='inactive',
            on_transition=activate_event
        )
    )

    # 혹은, 노드 프로세스가 시작되면 Configure 이벤트를 바로 Emit
    # Configure 완료 후 자동 활성화를 원하는 경우엔 다른 이벤트 핸들러를 연결

    return LaunchDescription([
        lifecycle_talker_node,
        configure_event,
        on_node_start_configure_activate
    ])
```

* **LifecycleNode**: 일반 `Node`와 달리 라이프사이클 관리를 위한 노드 액션이다.
* **EmitEvent**: 특정 이벤트(이 경우 라이프사이클 상태 전이 이벤트)를 발생시킨다.
* **ChangeState**: 라이프사이클 노드가 특정 상태 전이를 하도록 요청한다.
* **OnStateTransition**: 특정 노드가 어느 상태에서 다른 상태로 전이될 때 트리거되는 이벤트 핸들러.

위 예시는 개념적으로 라이프사이클 노드가 띄워진 후, `CONFIGURE` -> `ACTIVATE` 순서로 노드 상태 전이를 진행하도록 구성했다. 실제로는 노드에서 각각의 상태 전이가 가능하도록 구현되어 있어야 하며, 상태 전이가 실패하는 경우도 처리해야 한다.

#### 라이프사이클 노드의 다중 구성

다중 노드 환경에서 여러 라이프사이클 노드를 동시에 실행할 수 있다. 예를 들어 센서 노드 2개, 처리 노드 1개가 모두 라이프사이클 관리가 필요하다면, 다음과 같이 각 노드별 `LifecycleNode` 액션과 상태 전이 이벤트를 구성할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import LifecycleNode
from launch.actions import EmitEvent
from launch_ros.events.lifecycle import ChangeState
from lifecycle_msgs.msg import Transition

def generate_launch_description():
    sensor1 = LifecycleNode(
        package='my_sensor_pkg',
        executable='sensor_driver',
        name='sensor1',
        output='screen'
    )
    sensor2 = LifecycleNode(
        package='my_sensor_pkg',
        executable='sensor_driver',
        name='sensor2',
        output='screen'
    )
    processor = LifecycleNode(
        package='my_processor_pkg',
        executable='data_processor',
        name='processor',
        output='screen'
    )

    configure_sensor1 = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == sensor1,
            transition_id=Transition.TRANSITION_CONFIGURE
        )
    )
    configure_sensor2 = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == sensor2,
            transition_id=Transition.TRANSITION_CONFIGURE
        )
    )
    configure_processor = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == processor,
            transition_id=Transition.TRANSITION_CONFIGURE
        )
    )

    # 모든 노드가 Configure -> Activate 되는 시나리오
    activate_sensor1 = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == sensor1,
            transition_id=Transition.TRANSITION_ACTIVATE
        )
    )
    activate_sensor2 = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == sensor2,
            transition_id=Transition.TRANSITION_ACTIVATE
        )
    )
    activate_processor = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node == processor,
            transition_id=Transition.TRANSITION_ACTIVATE
        )
    )

    return LaunchDescription([
        sensor1,
        sensor2,
        processor,
        configure_sensor1,
        configure_sensor2,
        configure_processor,
        activate_sensor1,
        activate_sensor2,
        activate_processor
    ])
```

이렇게 구성하면, 런치 파일을 실행하자마자 3개 노드 모두 Configure 이벤트를 받고, 이후 Activate 이벤트를 받아서 활성화 상태로 진입한다. 실제로는 노드 간의 순차적 의존관계가 있을 수 있으므로, 이벤트 핸들러 등을 사용해서 한 노드가 활성화된 후 다른 노드를 활성화하도록 할 수 있다.

#### ROS Bag 연동

다중 노드 환경에서는 데이터 기록(rosbag)도 자주 쓰인다. ROS2의 `ros2 bag record` 기능을 Launch 파일에서 함께 실행하려면, `ExecuteProcess` 액션을 사용하여 명령어 형태로 실행한다.

```python
from launch import LaunchDescription
from launch.actions import ExecuteProcess
from launch_ros.actions import Node

def generate_launch_description():
    camera_node = Node(
        package='my_camera_pkg',
        executable='camera_driver',
        name='camera',
        output='screen'
    )

    # ros2 bag record /camera/image_raw /odom
    rosbag_record = ExecuteProcess(
        cmd=[
            'ros2', 'bag', 'record',
            '/camera/image_raw',
            '/odom'
        ],
        output='screen'
    )

    return LaunchDescription([
        camera_node,
        rosbag_record
    ])
```

* **ExecuteProcess**: ROS2 Launch에서 임의의 쉘 명령어를 실행할 수 있게 해준다.
* **cmd** 인자: 실제 쉘에서 실행할 명령 리스트.

이제 이 런치 파일을 실행하면 `camera_node`와 동시에 rosbag 기록 프로세스도 시작되어 `/camera/image_raw`와 `/odom` 토픽을 저장한다.

#### 복잡한 대규모 시스템에서의 사례

대규모 시스템에서는 다음과 같은 노드들이 서로 연결되어 있을 수 있다.

* 여러 종류의 센서 노드(카메라, LiDAR, IMU 등)
* 데이터 처리 노드(퍼셉션, 로컬라이제이션, 매핑, SLAM 등)
* 제어 및 경로 계획 노드
* UI/시각화 노드(RViz 등)
* 로깅 및 모니터링 노드
* 일부 노드는 라이프사이클 노드이며, 일부는 일반 노드

이를 모두 한 파일에 담기보다는, 다음과 같은 구조로 분할하여 Launch를 구성하는 것이 일반적이다.

1. **sensor.launch.py**: 센서 노드(카메라, LiDAR 등)를 하나의 그룹 또는 별도 컴포저블 컨테이너로 실행.
2. **perception.launch.py**: 이미지 처리, 객체 인식, 로컬라이제이션, SLAM 등에 관련된 노드들 실행.
3. **control.launch.py**: 제어, 경로 계획, 모션 플래너 노드들 실행.
4. **ui.launch.py**: 시각화 노드(RViz), 사용자 인터페이스 노드 등 실행.
5. **main.launch.py** (혹은 **system.launch.py**): 위의 4개 파일을 `IncludeLaunchDescription`으로 모두 불러와서 실행.

이렇게 계층화하면 각 서브시스템 단위로 Launch를 관리할 수 있어, 유지보수와 확장에 유리하다.

#### 추가적인 주의 사항

* **토픽 이름 충돌**: 동일한 이름의 토픽을 퍼블리시하는 노드가 여러 개 있을 경우, 네임스페이스, 리마핑 등을 통해 충돌을 방지해야 한다.
* **QoS 설정**: 다중 노드가 상호 통신할 때는 QoS(신뢰성, 히스토리, 지연 등)를 일치시키는 것이 중요하다. Launch 파일에서도 QoS 매개변수를 파라미터나 인자로 주어 노드에 전달할 수 있다.
* **시스템 자원 관리**: 카메라가 여러 대 붙은 경우, CPU/GPU 자원 사용이 매우 커질 수 있어 노드 병렬 실행 시 부하를 신중히 고려해야 한다.
* **Deploy 환경**: 실제 로봇이나 임베디드 환경에서 Launch 파일을 자동 실행하려면, systemd 서비스나 Docker 컨테이너 내에서 `ros2 launch` 명령어를 스크립트 형태로 구동하는 방식을 검토해야 한다.

#### 멀티 머신(Multi-Machine) Launch 구성

ROS2는 멀티 머신(멀티 호스트) 환경에서 여러 노드가 분산 실행되는 시나리오를 지원한다. 예를 들어 로봇에 장착된 온보드 컴퓨터에서 일부 노드를 구동하고, 데스크톱 PC나 서버에서 다른 노드를 구동하려는 경우가 있을 수 있다. 이때도 Launch 파일을 활용해 일괄 관리를 시도할 수 있으나, 실제로는 물리적으로 다른 머신에서 각각 `ros2 launch`를 실행하거나 Docker 컨테이너를 기동하는 식으로 나누어 실행하는 방식이 일반적이다.

다만, 여러 머신에서 동일한 Launch 파일을 실행해야 할 경우에는 다음과 같은 사항에 주의해야 한다.

1. **ROS\_DOMAIN\_ID**
   * 서로 다른 머신끼리 ROS2 통신을 하려면, ROS\_DOMAIN\_ID가 동일해야 한다.
   * 환경 변수 또는 Docker 컨테이너 내 설정 파일을 통해 통일해야 한다.
2. **파라미터 및 파일 경로**
   * 물리적으로 다른 머신에 존재하는 파일 경로가 일치하지 않을 수 있다. 예: `/home/user/config.yaml` 파일이 로봇 머신과 데스크톱 머신에서 다를 수 있다.
   * Launch 파일에서 로컬 경로를 지정할 경우, 각 머신별로 경로가 유효한지 확인해야 한다.
3. **네트워크 설정**
   * QoS 설정과 함께 DDS에서 사용하는 포트, 방화벽 설정 등이 통일되어야 원활한 통신이 가능하다.
   * 로컬 네트워크 혹은 VPN 등을 통해 통신할 때, multicast나 unicast 설정도 고려해야 한다.
4. **분산 Launch 관리**
   * 한 머신에서 다른 머신을 원격으로 Launch할 수도 있으나, 일반적으로는 SSH 등을 통해 스크립트를 실행하거나, Kubernetes/Docker Swarm 등 컨테이너 오케스트레이션 툴을 사용하는 방식이 더 실용적이다.
   * ROS2 Launch 자체에 “멀티 머신을 한 번에 실행”하는 표준 기법은 아직 제한적이며, 대부분은 개별 머신에서 독립적으로 Launch를 실행하는 형태로 구성한다.

#### Launch Testing으로 다중 노드 테스트

ROS2에는 `launch_testing`이라는 기능이 있어서, Launch 파일을 통해 여러 노드를 기동한 뒤 자동화된 단위 테스트(또는 통합 테스트)를 수행할 수 있다. 예를 들어, 다중 노드가 모두 기동된 다음 특정 주제(Topic)가 잘 퍼블리시되는지, 서비스 콜이 정상적으로 응답하는지 등을 테스트 스크립트에서 확인할 수 있다.

아래는 간단한 예시 구조이다.

```python
import pytest
from launch import LaunchDescription
from launch_ros.actions import Node
from launch_testing.actions import ReadyToTest
import launch_testing
import rclpy

@pytest.mark.launch_test
def generate_test_description():
    talker_node = Node(
        package='demo_nodes_cpp',
        executable='talker',
        name='talker'
    )
    listener_node = Node(
        package='demo_nodes_py',
        executable='listener',
        name='listener'
    )
    
    return LaunchDescription([
        talker_node,
        listener_node,
        ReadyToTest()
    ]), {
        'talker_node': talker_node,
        'listener_node': listener_node
    }

@pytest.mark.launch_test
def test_topic_communication(launch_service, talker_node, listener_node):
    rclpy.init()
    # 여기서 ROS2 노드 통신 여부를 확인하는 로직 수행
    assert True
    rclpy.shutdown()
```

* **launch\_testing**: Launch 파일로 기동된 노드들이 모두 준비된 상태가 되면, 테스트 함수들이 순차적으로 실행된다.
* **ReadyToTest**: 노드 기동 후 테스트 준비가 완료되었다고 알려주는 액션이다.
* **test\_topic\_communication**: 실제로 토픽 데이터를 송수신하는지 확인하거나, 노드 상태를 체크하는 테스트 코드를 작성한다.

이 방법은 CI(지속적 통합) 환경에서 여러 노드가 서로 상호작용하는지를 자동으로 검증하는 데 특히 유용하다.

#### 노드 간 통신의 부하 테스트(Stress Test)

다중 노드 구조에서 메시지 주고받는 양이 매우 많다면, Launch 파일을 통해 테스트 환경을 구성하고 부하 테스트를 진행할 수도 있다. 예컨대 다음과 같은 시나리오를 구성할 수 있다.

1. **N개의 퍼블리셔 노드**: 일정 주기로 토픽을 발행.
2. **M개의 서브스크라이버 노드**: 해당 토픽을 받아 처리 후, 처리 시간을 로깅.
3. **모니터링 노드**: CPU, 메모리 사용량, 네트워크 트래픽 등을 주기적으로 분석.

이 전체를 하나의 Launch 파일(혹은 여러 파일)에서 정의하고, `$ ros2 launch ...`로 실행한 뒤 측정 결과를 수집하면, 다중 노드 시스템에서의 병목 현상이나 QoS, 네트워크 설정 문제를 발견하기 쉬워진다.

#### 노드 종료 제어와 Cleanup

다중 노드가 실행 중일 때, 특정 노드를 먼저 종료하거나, 전체 노드를 모두 동시에 종료하는 절차도 고려해야 한다. 일반적으로 `$ ros2 launch`로 실행된 런치 프로세스를 `Ctrl+C`로 중단하면, 모든 자식 노드는 함께 종료된다.

* **개별 노드 종료**: 특정 노드만 종료하고 싶다면, 해당 노드 프로세스 PID를 찾아 수동으로 `kill` 하거나, Launch 이벤트를 통해 노드를 종료(Shutdown)할 수도 있다.
* **종료 순서 제어**: 라이프사이클 노드처럼 상태 전이 기반으로 종료를 제어하는 방법도 있지만, 일반 노드에서는 Launch에서 종료 순서를 엄격하게 지정하기가 쉽지 않다.
* **Cleanup**: 종료 시점을 포함해, 임시 파일(예: 로깅, rosbag 등)이나 하드웨어 자원(카메라, 센서 등)을 정리해줄 필요가 있다면, 노드 내부 로직이나 시그널 핸들러에서 처리하도록 설계해야 한다.
