# Launch 파일 구조와 구성 요소

ROS2 Humble에서 Launch 시스템은 다양한 노드와 프로세스를 효율적으로 실행하고 관리하기 위한 핵심 도구이다. Launch 파일은 노드 실행, 파라미터 설정, 동적인 조건 분기, 환경 변수 세팅 등을 한 곳에서 통합적으로 설정할 수 있도록 해준다. 이때, Launch 파일의 구조와 구성 요소를 정확히 이해하고 작성하는 것이 전체 ROS2 시스템의 가시성과 유지보수성을 높이는 데 매우 중요하다.

본 섹션에서는 주로 Python 기반의 `.py` Launch 파일 작성을 예시로 다루며, YAML 형태 등 다른 형식을 통한 파라미터 정의 역시 필요하면 간단히 언급한다. 그러나 핵심은 Python Launch API를 통해 노드를 정의하고 파라미터를 할당하며, 다양한 액션(Action)과 이벤트(Event)를 처리하는 방법이다.

#### Launch 파일의 핵심 요소

ROS2 Humble에서 제공하는 Launch 파일 구조는 크게 다음과 같은 핵심 요소로 구성된다.

1. **LaunchDescription**:
   * Launch 파일에서 가장 상위 레벨 구조로, 여러 액션(Action)을 하나로 묶은 컨테이너 역할을 한다.
   * LaunchDescription 객체 안에 Node, IncludeLaunchDescription 등 다양한 액션을 담아둔 뒤 `generate_launch_description()` 함수를 통해 최종 반환한다.
2. **Action**:
   * LaunchDescription에 추가하여 실제 수행할 동작(예: 노드 실행, 로깅 설정, 파라미터 로드 등)을 정의한다.
   * Node, LogInfo, ExecuteProcess 등이 대표적인 액션이다.
   * 특정한 조건(Conditional)을 통해 액션 실행 여부를 결정할 수도 있다.
3. **Event Handler**:
   * 런타임에서 발생하는 이벤트(예: 노드 종료, SIGINT, 타이머 종료 등)에 따라 추가 액션을 수행하거나 시스템을 종료시키는 로직을 정의한다.
   * EventHandler를 적절히 사용하면 특정 노드가 죽었을 때 자동으로 재실행하거나, 로깅 설정을 동적으로 바꾸는 동작을 구현할 수 있다.
4. **IncludeLaunchDescription**:
   * 다른 Launch 파일을 중첩으로 불러와서 관리할 수 있게 해주는 요소다.
   * 복잡한 구조의 시스템을 작은 Launch 파일 단위로 나누어 재사용할 때 유용하다.
5. **Substitution**:
   * 환경 변수 혹은 Launch 파일 내 변수 등을 통해 실행 시점을 기준으로 값을 결정해야 하는 경우 사용한다.
   * 예를 들어 환경 변수에 따라 노드 이름 혹은 파라미터를 다르게 설정하거나, 패키지 경로 등을 동적으로 읽어온다.
   * 대표적으로 `LaunchConfiguration`, `Command`, `PathJoinSubstitution` 등이 있다.

#### 기본적인 Launch 파일 작성 흐름

일반적으로 Python 기반 Launch 파일의 골격은 다음과 같은 형태를 가진다:

```python
# my_robot_bringup.launch.py
import os
from launch import LaunchDescription
from launch.actions import ExecuteProcess, IncludeLaunchDescription
from launch.actions import DeclareLaunchArgument, LogInfo
from launch.substitutions import LaunchConfiguration, ThisLaunchFileDir
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node

def generate_launch_description():
    # (1) 필요한 LaunchArgument 혹은 Substitution 선언
    robot_name_arg = DeclareLaunchArgument(
        'robot_name',
        default_value='my_robot',
        description='Name of the robot'
    )
    
    # (2) Node(Action) 정의
    robot_state_node = Node(
        package='my_robot_state',
        executable='robot_state_publisher',
        name=LaunchConfiguration('robot_name'),
        output='screen'
    )
    
    # (3) IncludeLaunchDescription 등 다른 Launch 파일 불러오기
    other_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            [ThisLaunchFileDir(), '/other_launch_file.launch.py']
        )
    )
    
    # (4) LaunchDescription에 액션들을 추가
    return LaunchDescription([
        robot_name_arg,
        robot_state_node,
        other_launch,
        LogInfo(msg='ROS2 Launch file started...')
    ])
```

위 예시에서 주목해야 할 점은 다음과 같다.

* `DeclareLaunchArgument`를 통해 런타임 시 사용자로부터 입력을 받을 수 있도록 설정했다.
* `Node` 액션은 ROS2에서 노드를 실행하기 위한 가장 핵심적인 구성 요소다. 여기서 `package`, `executable`, `name`, `output` 등 여러 인자를 통해 실행 옵션을 세밀하게 조정한다.
* `IncludeLaunchDescription`는 다른 Launch 파일을 재활용할 수 있도록 해준다.
* 맨 마지막에 `LaunchDescription` 객체를 생성하여 등록한 액션들을 차례대로 넣어준다. `generate_launch_description()` 함수에서 이를 반환하는 것이 ROS2 Launch 파일의 관례다.

#### Node 액션의 구조

`Node` 액션은 ROS2에서 노드를 실행하기 위해 자주 사용된다. 주요 인자로는 다음이 있다.

* `package`: 노드가 속한 ROS2 패키지 이름
* `executable`: 실제 실행 파일(노드) 이름
* `namespace`: 노드 네임스페이스 설정
* `name`: 노드 이름(런타임 오버라이드 가능)
* `output`: 콘솔 출력을 어떻게 처리할지 결정(`screen`, `log` 등)
* `parameters`: YAML 파일 혹은 Python dict로부터 노드 파라미터를 주입할 때 사용
* `remappings`: ROS 토픽/서비스 등의 이름을 재매핑할 때 사용

예를 들어 노드에 파라미터를 직접 넘겨주고 싶다면 아래와 같이 정의할 수 있다.

```python
robot_state_node = Node(
    package='my_robot_state',
    executable='robot_state_publisher',
    name='robot_state_node',
    parameters=[{'robot_description': '<robot_urdf>'}],
    output='screen'
)
```

`parameters` 항목에 Python 딕셔너리 리스트를 넣으면 해당 노드에서 사용할 파라미터가 설정된다. 이 파라미터는 런타임에서 `ros2 param` 명령을 통해 확인 및 수정할 수도 있다.

#### Substitution 예시

Launch 파일을 작성하다 보면, 특정 경로나 파일 이름 등을 동적으로 받아와야 하는 경우가 빈번히 발생한다. 예를 들어 패키지 디렉토리를 자동으로 찾기 위해 `get_package_share_directory()` 함수를 사용할 수도 있으나, Launch Substitution API를 활용해 더 확장성 있는 구조를 만들 수 있다.

```python
from ament_index_python.packages import get_package_share_directory
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution

package_dir = get_package_share_directory('my_package')

config_file_path = PathJoinSubstitution([
    package_dir,
    'config',
    LaunchConfiguration('config_file_name')
])
```

이렇게 정의해두면, `config_file_name`이라는 Launch Argument로 YAML 파일 이름 등을 런타임에 전달할 수 있고, 실제 경로는 Launch 시스템이 알아서 조합해준다.

#### 조건부 액션(Conditional Actions)

Launch 시스템을 통해 액션(Action)을 정의할 때, 특정 조건에 따라 액션을 실행하거나 실행하지 않을 수 있다. 이를 위해 `if/else` 구문을 Python 코드에서 직접 사용할 수도 있지만, Launch가 제공하는 조건부 액션 기능을 사용하면 더욱 명확하게 의도를 표현할 수 있다. 예를 들어, `IfCondition`, `UnlessCondition` 등의 조건 객체를 사용할 수 있다.

```python
from launch.conditions import IfCondition, UnlessCondition
from launch.actions import LogInfo
from launch.substitutions import LaunchConfiguration

debug_arg = DeclareLaunchArgument(
    'debug', 
    default_value='False',
    description='Enable debug mode'
)

log_for_debug = LogInfo(
    msg='Debug mode is enabled',
    condition=IfCondition(LaunchConfiguration('debug'))
)

log_for_normal = LogInfo(
    msg='Normal mode',
    condition=UnlessCondition(LaunchConfiguration('debug'))
)
```

위 예시에서 `debug`가 `'True'`면 `IfCondition(LaunchConfiguration('debug'))`이 참이 되어 `log_for_debug`가 실행되고, `'False'`면 `UnlessCondition(LaunchConfiguration('debug'))`이 참이 되어 `log_for_normal`이 실행된다. 이를 통해 한 Launch 파일 안에서 상황에 따라 노드나 프로세스를 유연하게 분기할 수 있다.

#### EventHandler 활용

ROS2 Launch 시스템은 런타임 시점의 이벤트(노드 종료, 프로세스 종료, SIGINT 등)를 포착하고, 이를 통해 후속 동작을 수행하도록 설정할 수 있다. `RegisterEventHandler`와 `OnProcessExit`, `OnShutdown`, `OnExecutionComplete` 같은 핸들러를 사용해 특정 액션을 트리거한다.

예시로, 특정 노드가 종료했을 때 다른 노드를 재시작하거나, 종료 이벤트가 발생하면 로깅 액션을 추가로 실행하는 방법이 있다.

```python
from launch import LaunchDescription
from launch.actions import RegisterEventHandler, LogInfo
from launch.event_handlers import OnProcessExit
from launch_ros.actions import Node

def generate_launch_description():
    my_node = Node(
        package='my_package',
        executable='my_executable',
        name='my_node'
    )

    event_handler = RegisterEventHandler(
        OnProcessExit(
            target_action=my_node,
            on_exit=[
                LogInfo(msg='my_node has exited. Taking some action...'),
                Node(
                    package='my_package',
                    executable='my_executable',
                    name='my_node_restarted'
                )
            ]
        )
    )

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

이처럼 이벤트를 활용하면 시스템이 동작 중에 발생하는 여러 상황에 대해 대응 로직을 구축할 수 있다.

#### IncludeLaunchDescription

다수의 노드를 실행하는 대형 프로젝트에서는 Launch 파일이 점점 복잡해진다. 이럴 때 작은 단위의 Launch 파일로 쪼개어 재사용하고, 필요 시 메인 Launch 파일에서 이를 호출(Include)하는 구조가 효과적이다. `IncludeLaunchDescription`는 다른 Launch 파일(파이썬, XML, YAML 등)을 포함할 때 사용된다.

```python
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import ThisLaunchFileDir

other_launch = IncludeLaunchDescription(
    PythonLaunchDescriptionSource(
        [ThisLaunchFileDir(), '/other_nodes.launch.py']
    ),
    launch_arguments={'arg_name': 'arg_value'}.items()
)
```

* `PythonLaunchDescriptionSource` 외에도 `XMLLaunchDescriptionSource`, `YamlLaunchDescriptionSource` 등이 있다.
* `launch_arguments` 파라미터에 딕셔너리 형태로 인자를 전달하면, 포함되는 Launch 파일 안의 `DeclareLaunchArgument`와 매핑되어 런타임 시 인자를 전달할 수 있다.

#### 환경 변수와 LaunchConfiguration

Launch 파일에서 시스템 환경 변수를 그대로 사용하거나, 런타임 인자로부터 특정 변수를 전달받아 활용할 수 있다. 아래는 몇 가지 방법이다.

**직접 Python 코드로 환경 변수를 가져오는 방법**:

```python
import os

my_env_var = os.environ.get('MY_ENV_VAR', 'default_value')
```

이 경우, Launch 시스템의 Substitution 기능과는 무관하게 OS 레벨에서 환경 변수를 직접 읽는다.

**LaunchConfiguration을 통한 런타임 인자**:

```python
debug_level_arg = DeclareLaunchArgument(
    'debug_level', 
    default_value='INFO'
)
debug_level = LaunchConfiguration('debug_level')
```

이후 `debug_level`을 `Node` 액션의 인자로 전달할 수도 있고, 다른 액션의 조건으로 활용할 수도 있다.

**EnvironmentVariable Substitution**: Launch에서는 `EnvironmentVariable(name='MY_ENV_VAR')` 형태로 환경 변수를 Launch 변수로 가져올 수 있다.

```python
from launch.substitutions import EnvironmentVariable

env_var_substitution = EnvironmentVariable('MY_ENV_VAR')
```

이를 통해 Launch 파일 내부에서 Substitution 객체처럼 활용 가능하다.

#### 파라미터 파일(Parameter File) 사용

ROS2에서는 파라미터를 YAML 파일 등 외부 파일로 관리할 수 있으며, 이를 Launch 파일에서 직접 불러와 적용할 수 있다. 아래 예시는 `my_robot_params.yaml` 파일을 불러와 `Node`에 주입하는 방법을 보여준다.

```python
robot_node = Node(
    package='my_robot_pkg',
    executable='robot_controller',
    name='robot_controller',
    parameters=[
        '/path/to/my_robot_params.yaml'
    ]
)
```

YAML 파일에서는 다음과 같이 파라미터를 정의할 수 있다.

```yaml
my_robot_pkg:
  ros__parameters:
    max_speed: 1.0
    sensor_range: 10
```

이 경우 런타임에 노드 `robot_controller`가 위 파라미터를 읽어들인다. 파라미터 파일은 여러 노드가 공유하도록 설계할 수도 있으며, Launch 파일 내에서 개별 노드마다 서로 다른 파일을 로드하는 것도 가능하다.

#### 파라미터 중첩 구조

YAML 파라미터 파일에서는 종종 중첩된 구조를 사용하기도 한다. 예를 들어, 아래처럼 계층 구조를 둘 수 있다.

```yaml
my_robot_pkg:
  ros__parameters:
    controllers:
      pid:
        p_gain: 2.0
        i_gain: 0.1
        d_gain: 0.01
    use_sim_time: True
```

이를 Launch 파일에서 로드하면, 해당 노드는 `controllers.pid.p_gain` 등과 같이 중첩된 키를 통해 파라미터에 접근할 수 있다.

#### 커맨드라인 인자와 Launch 파일

Launch 파일에서 `DeclareLaunchArgument`를 사용하면, 커맨드라인 인자를 통해 노드 이름, 파라미터 파일, 디버그 모드 등 다양한 설정값을 동적으로 주입할 수 있다. 예를 들어 다음과 같은 Launch 파일이 있다고 하자.

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

def generate_launch_description():
    robot_name_arg = DeclareLaunchArgument(
        'robot_name',
        default_value='r2d2',
        description='Name of the robot'
    )

    robot_name = LaunchConfiguration('robot_name')

    robot_node = Node(
        package='robot_pkg',
        executable='robot_exe',
        name=robot_name
    )

    info_msg = LogInfo(msg=['Robot name set to: ', robot_name])

    return LaunchDescription([
        robot_name_arg,
        robot_node,
        info_msg
    ])
```

이를 실행할 때 커맨드라인에서 다음처럼 인자를 줄 수 있다:

```bash
ros2 launch my_pkg my_launch_file.launch.py robot_name:=bb8
```

이 경우 런타임에 `robot_name`이 `bb8`로 설정되어, 위 코드에서 `Node` 이름이 `bb8`로 부여된다. 이처럼 Launch 파일이 제공하는 인자(Launch Arguments)는 ROS2 시스템을 유연하게 설정하는 핵심 요소다.

#### 여러 노드의 파라미터 파일 동시 사용

프로젝트가 커지면 여러 노드에 대해 서로 다른 파라미터 파일을 사용해야 하는 경우가 많다. 이를 위해 Launch 파일에서 각 노드별로 `parameters` 옵션에 해당 파일을 지정하면 된다.

```python
controller_node = Node(
    package='my_controller_pkg',
    executable='controller_exe',
    name='my_controller',
    parameters=[
        '/path/to/controller_params.yaml'
    ]
)

sensor_node = Node(
    package='my_sensor_pkg',
    executable='sensor_exe',
    name='my_sensor',
    parameters=[
        '/path/to/sensor_params.yaml'
    ]
)

ld = LaunchDescription([
    controller_node,
    sensor_node
])
```

상황에 따라, 동일한 YAML 파일 내에 여러 노드의 파라미터를 함께 정의하고, 노드별로 섹션을 구분해 사용할 수도 있다. 다만 파라미터 파일은 노드명 혹은 패키지명을 기준으로 매핑되기 때문에, YAML 작성 시 구조를 잘 구분해야 한다.

#### GroupAction을 통한 노드 그룹화

`GroupAction`을 사용하면 여러 노드를 하나의 그룹으로 묶어서, 공통된 네임스페이스나 조건을 적용할 수 있다. 예를 들어, 아래 예시는 특정 네임스페이스 아래에서 노드 두 개를 실행한다.

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

group_ns = GroupAction([
    PushRosNamespace('my_robot'),
    Node(
        package='my_pkg',
        executable='node1_exe',
        name='node1'
    ),
    Node(
        package='my_pkg',
        executable='node2_exe',
        name='node2'
    )
])

ld = LaunchDescription([
    group_ns
])
```

`PushRosNamespace` 액션을 통해 그룹 내부에 속한 노드는 `/my_robot/node1`, `/my_robot/node2`와 같은 네임스페이스가 적용된다. 필요하다면 LaunchArgument나 IfCondition 등을 GroupAction 내부에 넣어 그룹 전체의 동작을 제어할 수도 있다.

#### LaunchConfiguration 간 연산

Launch 파일에서 `LaunchConfiguration`끼리 간단한 문자열 결합 등의 연산을 할 수도 있다. 이를 위해 `PythonExpression`, `TextSubstitution` 등을 활용한다. 예시로 두 개의 Launch Argument를 합쳐 경로를 만드는 상황을 살펴보자.

```python
from launch.substitutions import LaunchConfiguration, PythonExpression, TextSubstitution

base_path_arg = DeclareLaunchArgument(
    'base_path',
    default_value='/tmp'
)
filename_arg = DeclareLaunchArgument(
    'filename',
    default_value='data.txt'
)

combined_path = PythonExpression([
    '""',
    LaunchConfiguration('base_path'),
    '/',
    LaunchConfiguration('filename'),
    '"'
])

log_path = LogInfo(msg=['Combined path: ', combined_path])
```

위와 같은 코드에서 `combined_path`는 `"/tmp/data.txt"`와 같이 런타임에 문자열이 합쳐진다. 이후 이를 `Node`의 `parameters`나 `env` 등 다양한 곳에 활용할 수 있다.

#### 다양한 Launch 액션

지금까지 `Node`, `IncludeLaunchDescription`, `LogInfo` 등을 다뤘지만, ROS2 Launch에는 다음과 같은 액션도 존재한다.

* **ExecuteProcess**: 운영체제 레벨에서 임의의 명령어를 실행한다.
* **TimerAction**: 일정 시간이 지난 후 특정 액션을 실행한다.
* **ShutdownAction**: 시스템을 종료하거나, 노드 종료 신호를 보낼 수 있다.

예를 들어, 5초 후에 특정 스크립트를 실행하고자 한다면:

```python
from launch.actions import TimerAction, ExecuteProcess

timer_action = TimerAction(
    period=5.0,
    actions=[
        ExecuteProcess(
            cmd=['/usr/bin/python3', '/path/to/script.py'],
            output='screen'
        )
    ]
)
```

이처럼 Launch 액션을 적절히 조합하면, ROS2 애플리케이션에서 실행할 수 있는 프로세스나 작업을 매우 유연하게 스케줄링할 수 있다.

#### OpaqueFunction 액션

ROS2 Launch 시스템에서 `OpaqueFunction` 액션은 런타임 시점에 임의의 Python 코드를 실행할 수 있도록 해준다. 이는 특정 조건이나 환경에 따라 유연하게 액션을 구성해야 할 때 유용하다. 예를 들어, 노드를 동적으로 생성하거나, 조건에 따라 파라미터를 가공한 뒤 반환하는 작업을 수행할 수 있다.

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

def dynamic_node_creation(context, *args, **kwargs):
    # 런타임 시점에 로직을 통해 Node를 만들거나, 파라미터를 가공하는 등의 작업을 수행
    some_condition = True  # 실제론 LaunchConfiguration이나 환경 변수를 기반으로 판단
    if some_condition:
        return [
            Node(
                package='demo_package',
                executable='demo_executable',
                name='dynamic_node'
            )
        ]
    else:
        return []

def generate_launch_description():
    opaque_action = OpaqueFunction(function=dynamic_node_creation)
    return LaunchDescription([opaque_action])
```

위와 같이 `OpaqueFunction` 액션은 `function` 인자로 지정된 함수를 런타임에 호출하고, 그 결과로 Launch 액션(예: Node, LogInfo 등)을 반환한다. 이를 통해 Launch 파일에서 동적인 노드 생성을 비롯해 더욱 복잡한 제어 로직을 구현할 수 있다.

#### SetEnvironmentVariable 액션

노드를 실행하기 전, 특정 환경 변수를 설정해야 할 때 `SetEnvironmentVariable` 액션을 사용할 수 있다. 이를 통해 런타임에 Launch 시스템 관점에서 필요한 OS 환경 변수를 주입할 수 있다.

```python
from launch.actions import SetEnvironmentVariable
from launch.substitutions import TextSubstitution

set_env_action = SetEnvironmentVariable(
    name='MY_APP_MODE',
    value=TextSubstitution(text='production')
)
```

`SetEnvironmentVariable` 액션이 LaunchDescription에 들어가면, 해당 시점 이후로 실행되는 노드들은 `MY_APP_MODE=production` 환경에서 동작하게 된다.

#### Remap ROS Topic/Service

ROS2 Launch 파일에서 노드끼리 통신 주제를 재매핑(Remap)해야 하는 경우가 많다. 이를 위해 `Node` 액션의 `remappings` 인자를 활용한다.

```python
sensor_node = Node(
    package='my_sensor_pkg',
    executable='my_sensor_exe',
    name='my_sensor',
    remappings=[
        ('/raw_sensor_data', '/processed_sensor_data')
    ]
)
```

이처럼 `('/기존_토픽', '/새_토픽')` 형식의 튜플 리스트를 `remappings`에 지정하면, 런타임에 노드 내에서 기존에 사용하던 ROS 토픽 이름이 새로운 이름으로 변경된다. 서비스나 액션 이름 역시 동일한 방식으로 재매핑할 수 있다.

#### LifecycleNode 액션

ROS2에는 노드의 생명주기를 관리하기 위한 `lifecycle_msgs` 인터페이스가 존재하며, 이를 지원하는 노드를 “Lifecycle Node”라고 한다. Launch 시스템에서도 이를 지원하기 위해 `launch_ros.actions.LifecycleNode` 액션을 제공한다. 일반 노드와 거의 동일한 방식으로 사용할 수 있으나, 라이프사이클 관련 이벤트와 트랜지션을 고려해 추가 액션을 넣을 수 있다.

```python
from launch_ros.actions import LifecycleNode

lifecycle_node = LifecycleNode(
    package='my_lifecycle_pkg',
    executable='my_lifecycle_exe',
    name='lifecycle_demo',
    output='screen'
)
```

런타임에 이 노드는 “Unconfigured” 상태에서 시작된 뒤, “configure” 요청을 받으면 “Inactive” 상태로, “activate” 요청을 받으면 “Active” 상태로 전환되는 일련의 라이프사이클을 따른다. Launch 파일 안에서 이벤트 핸들러를 등록해 특정 상태에서 노드에게 자동으로 트랜지션을 걸어줄 수도 있다.

#### LaunchTest(테스트)와 pytest 연동

ROS2 Launch는 시스템 통합 테스트를 위한 `LaunchTest` 기능을 별도로 제공한다. 이를 통해 pytest와 같은 Python 테스트 프레임워크에서 Launch 파일을 구동하고, 일정 시점에 노드 상태 또는 토픽 메시지를 검증할 수 있다. 다음은 개념적인 예시로, `pytest`를 사용해 Launch 파일을 테스트한다:

```python
import os
import pytest
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.testing.actions import ReadyToTest
from launch.testing.launch_test import LaunchTestService

@pytest.mark.launch_test
def generate_test_description():
    test_node = Node(
        package='my_test_pkg',
        executable='my_test_node',
        name='test_node'
    )

    return LaunchDescription([
        test_node,
        ReadyToTest()
    ])

@pytest.mark.launch_test
@pytest.mark.parametrize('input_value', [0, 1, 2])
def test_node_behavior(input_value):
    # pytest 내부에서 노드 동작을 검증하는 로직 작성
    assert True  # 실제 테스트에서는 메시지 수신 여부, 파라미터 값 등을 체크
```

이처럼 Launch 파일의 구조와 동작 과정을 자동화된 테스트로 검증할 수 있기 때문에, 복잡한 시스템에서 안정성을 높이고 회귀 테스트를 쉽게 수행할 수 있다.

#### Launch 디버깅 방법

* **Launch 파일에서 LogInfo 활용**: Launch 파일 안 곳곳에 `LogInfo(msg='...')` 액션을 넣어, 특정 시점 또는 조건에서 확인 메시지를 출력하도록 하면, 런타임 동작 흐름을 파악하기가 한결 수월하다.
* ROS2 명령어:
  * `ros2 launch --show-args <pkg> <launch_file>`: Launch 파일의 모든 인자를 확인할 수 있다.
  * `ros2 launch --print-description <pkg> <launch_file>`: Launch 파일이 정의하는 구조(액션, Substitution 등)를 화면에 출력한다.
* **오탈자 혹은 import 오류**: Python Launch 파일을 작성할 때, 단순한 문법 오류나 import 경로 문제로 인해 실행이 안 되는 경우가 있으니, `python3 my_launch_file.launch.py`로 먼저 문법 검사를 해볼 수도 있다.

#### 대규모 Launch 구성 시 권장 사항

1. **모듈화**: 여러 노드를 한 번에 실행하는 복잡한 Launch 파일을 한 군데에 모두 작성하기보다, 기능별로 작은 Launch 파일로 나누고 `IncludeLaunchDescription`을 적극적으로 활용한다.
2. **적절한 변수/인자 사용**: 파라미터 파일 경로, 노드 이름 등은 `DeclareLaunchArgument`와 `LaunchConfiguration`을 사용하여 유연하게 처리한다.
3. **공통 코드 재사용**: 반복적으로 등장하는 Substitution, EventHandler, 환경 변수 설정 등을 별도 Python 모듈로 만들어 두고, 필요한 Launch 파일에서 import해 사용한다.
4. **YAML 파라미터 파일 분리**: 노드별 혹은 기능별 파라미터를 YAML 파일로 분리해 두면, Launch 파일 구조가 깔끔해지고 유지보수성이 올라간다.
