자율주행 시스템의 요구사항과 프로그램 구조 비교

이 문서는 제가 학부생 자율차 대회에 참여하고 난 뒤 프로그램 구조 측면에서 아쉬운 점을 정리한 글입니다. 참고로 자꾸 자율차 글 써서 제가 자동차에 관심 있나 생각하실지도 모르겠는데, 저는 자동차에 관심이 있는 것이 아니라 소프트웨어 공학에 관심이 있습니다. 자율차 프로젝트는 공부를 위한 수단이었을 뿐입니다…

먼저 자율주행 프로그램의 요구사항을 소개드리고, 두 팀의 프로그램 구조 비교를 통해 요구사항을 어떻게 달성했나실패했나를 설명드리겠습니다.

자율주행 프로그램의 요구사항

실시간 시스템을 지향하는 자율주행 자동차는 몇 가지 필수적인 요구사항이 있습니다. 그 중 제가 프로젝트에 참여하며 생각해 본 것들을 소개해 드립니다.

요구사항

동일 시간 데이터 보장

여러 센서에서 받은 데이터의 시간 순서가 꼬이면 안 됩니다. 제가 참여한 2018팀의 프로그램을 예시로 들어보겠습니다. 2018팀의 프로그램은 센서로 웹캠 세 대와 LiDAR, 총 네 개의 센서를 사용했습니다. 그리고 이 프로그램은 한 루프 내에서 차선 데이터(웹캠 두 대), 표지판 데이터(웹캠 한 대), 장애물 데이터(라이다)를 받아서 처리하는 세 개의 코드를 먼저 실행시킵니다. 이 때 각 코드가 받아오는 네 개 센서의 데이터는 전부 동일 시점이거나, 그에 준할 만큼 가까이 있는 시점의 데이터여야 합니다. 이것은 구현이 어렵지 않았습니다. 이미 운영체제가 각 센서를 병렬로 운영하고 있을 테니, 그 시점에서 우리의 처리 코드만 같은 시점에 돌리면 그 시점의 데이터를 모두 얻어서 처리할 수 있었습니다.

그러나 2018팀의 문제는 센서 파싱과 같은 1st-tier 프로그램에서 처리를 마친 뒤, 판단이나 제어 등을 관리하는 2nd-tier 프로그램으로 값을 넘겨줄 때 있었던 것 같습니다. (대회 주행 로그를 보고 추측했습니다) 한 루프가 돌고 나면 모든 값이 다시 현재 시점의 데이터로 초기화되어야 하는데, 표지판 인식 프로그램과 관련된 부분에서 자꾸 이전에 받은 데이터를 참조하는 버그가 발생했습니다. 이는 프로그램 구조가 달랐던 2017팀에서는 없었던 문제입니다.

정리하면, 임의의 시간에 센서에서 받은 데이터에 접근할 때, 그 데이터를 이용하는 모든 세부 프로그램이 현재 시점의 데이터를 받아와 연산하고 반환해야 합니다. 그리고 그것들이 반환한 데이터을 받아서 처리하는 2nd-tier 프로그램 또한 동일한 현재 시점의 데이터로만 연산을 수행하여야 합니다.

최악의 경우에도 운영 가능

또한, 하나의 세부 프로그램에서 데이터 처리에 실패했을 경우, 전체 시스템은 그 데이터를 무시하고도 상식적인 수준에서 정상적으로 운영되어야 합니다. 예를 들어, 2018팀의 프로그램은 표지판 인식 코드에서 정상적인 결과가 나오지 않았을 때, 장애물에 미리 대비하는 코드가 없었습니다. (지금 생각해도 역시 엄청 바보같군요) 이런 알고리즘은 표지판과 같은 트리거가 없이, 혹은 어떤 이유로 트리거를 인지하지 못하고 등장하는 장애물에 대처할 수 없게 합니다. 여기에서 상식적인 수준이라는 표현의 뜻은, 위의 예시에서 이해했듯이 적은 데이터를 가지고도 일반적으로 대부분의 상황에 대처할 수 있는 알고리즘으로 시스템을 설계해야 한다는 뜻입니다.

가장 좋은 것은 센서 및 코드 신뢰도가 높은 부분에 더 의존하고, 그렇지 않은 부분에 덜 의존하도록 시스템을 설계하는 것입니다. 그러기 위해선 각 부분의 신뢰도를 확률 변수 등을 이용해 적절히 예측할 수 있는 방법도 중요할 것 같습니다. 학부 대회 수준이면 그냥 테스트 많이 해 보고 감으로 예측해도 됩니다 일반적으로 센서 하드웨어의 정확도가 일정 이상 확보되었다는 가정 하에, 데이터 처리 과정이 복잡하면 아닌 것에 비해 처리 후 신뢰도가 낮은 것 같습니다. 과정이 복잡할수록 개발자가 코드에서 미처 신경쓰지 못한 부분이 존재할 수 있는 것입니다. 제가 참여했던 프로젝트를 생각해 보면, LiDAR 센서 처리가 가장 간단했기에 오류가 거의 없었고, 차선이나 표지판의 Vision 알고리즘은 작성한 내용에 따라 정확도가 크게 차이났습니다.

연산 속도 최적화

자율주행 시스템은 주변 상황을 인지함과 동시에 그에 맞는 대처가 필요한 실시간 시스템입니다. 따라서 한 loop 의 연산 속도를 최적화하는 작업이 필요합니다. 각 세부 알고리즘의 속도를 빠르게 하는 것도 중요하고, 전체 여러 작업들의 순서를 어떻게 계획하고 예측할지도 실시간 대처에 영향을 끼칠 것입니다.

개별 작업 속도 최적화

2018팀의 경우 개별 작업의 알고리즘을 작성할 때 최대한 연산 속도가 날 만한 방법을 적용하려 노력했습니다. python의 list를 사용하기보다는, numpy의 ndarray를 사용하는 식으로 말이죠. 그리고 일부 작업에는 PyCUDA를 이용해 그래픽카드 병렬 연산을 적용하기도 했습니다. 더 세부적으로는, cache hit를 고려한 설계 등도 가능할 것 같습니다. 방법은 다양하므로 많은 공부가 필요했습니다.

슈도코드 및 스켈레톤 코드 단계에서 최적화를 우선 진행한 후 프로그램을 작성했더라도, 실제 걸리는 절대적인 시간이 중요하기 때문에 이후 시간 측정 실험을 진행합니다. 저희 팀의 한 개발자 분은 프로그램 작성 시 코드 한 줄 한 줄을 추가할 때마다 얼마나 느려지는지 측정해보고, 마음에 안 들면 대안을 찾아 수정해보았다고 합니다.

전체 작업 속도 최적화

작업들의 전체 순서를 계획해 연산 속도를 늘리는 방법도 여러 가지가 있습니다. 파이썬에 존재하는 multiprocessing 모듈은 작업의 병렬 처리를 지원해 연산 속도를 빠르게 합니다. 그래서 프로젝트 초기에 담당 개발자 분은 이것을 공부하고 사용해보려 시도했었습니다. 그러나 싱글 프로세스 내에서 스레드로 도는 것을 지원하는 threading 모듈과 달리, 멀티프로세싱은 프로세스가 여러 개이므로 데이터 전달을 계획해야 했고, 까다로웠습니다. 그래서 결국 2018팀의 최종 프로그램에는 사용되지 않았습니다.

빠른 실패 지향

이것은 2018팀의 프로그램이 동작할 때는 없었던 문제였으나, 최악의 상황을 생각해본다면 가정할 수 있는 또 다른 상황을 방지하기 위한 요구사항입니다.

어느 한 센서에 대한 통신이 잠시 끊길 수 있습니다. 이 때 시스템은 센서와의 통신이 안 되는 것을 미리 인지하거나, 혹은 적절한 타임아웃을 기다려서 그 데이터에 대한 대기를 포기할 수 있어야 합니다. 한 루프의 종료 시점을 예측할 수 있다는 것을 보장하기 위해서입니다. 포기한 다음에는 앞서 말씀드렸듯이 적은 데이터를 가지고 일반적으로 주행할 수 있도록 설계합니다. 이후 계속 연결 상태를 지켜보다 다시 통신될 경우 정상 상태로 돌아옵니다. 일정 시간동안 다시 통신이 연결되지 않으면 안전을 위해 적절한 방법으로 예외처리를 진행합니다.

저희 프로젝트에서 구현하지는 않았으나, 파이썬의 except 구문을 사용하면 설계할 수 있지 않을까 생각해 보았습니다. 하지만 학부 대회를 출전하기 위한 프로그램을 작성하는 중이고 담당 개발자 분이 예외 처리에 관심이 있는 것이 아니라면 크게 고려하지 않아도 될 것 같습니다. 대회와 같은 제한된 환경에서, 센서 데이터를 받아오지 못할 경우는 깜빡하고 케이블을 안 꽂은 정도밖에 없을 것이기 때문입니다.

추가로…

사실 자율차는 모든 센서의 데이터가 정상적으로 입력되지 않는 최악의 경우가 발생할 경우, 인간 운전자에게 이를 빠르게 알리고 안전하게 운전 권한을 넘겨야 합니다. 이를 제어권 이양이라고 하며, HCI와 관련이 있습니다. 이것은 제가 참여했던 프로젝트에서는 다루지 않았던 내용이므로, 본 문서에서도 중요하게 다루지 않습니다. (사실 저도 잘 모르니 궁금하시면 검색해 보세요) 따라서 실제 차량에서는, 위에서 설명 드린 최악의 경우를 고려한 설계를 할 경우 적어도 제어권이 이양되기 전까지는 안전한 주행을 보장할 수 있어야 합니다. 실제 자율차는 파이썬으로 작성하지도 않을 것 같은데

두 팀의 프로그램 구조 비교

이 글에서 설명하는 2017팀과 2018팀은 해당 년도에 국제대학생 창작자동차 경진대회의 자율주행차 부문에 참여한 성균관대학교 팀을 지칭하는 것입니다. 2018팀의 자세한 이야기에 대해서는 제가 작성한 다른 글을 참고해 주세요. 후기

2017팀

2017팀은 글로벌 변수를 써서 한 프로그램에서 통짜로 데이터를 관리했습니다. 이 방법의 장점은 데이터를 공통 공간에 올려 두고 사용하므로 데이터가 시간에 따라 꼬일 일이 없다는 점입니다. 그러나 그런 방식은 팀 내 분업이나 프로젝트 유지보수에 매우 치명적입니다.

2017팀의 프로그램은 다음과 같은 구조로 이루어져 있었습니다. (파일명 및 함수명은 가독성을 위해 재구성했으며, 핵심 함수만을 표시했습니다.)

combination.py
 def open_cam()
 def detect_signboardName()  # 7개 표지판에 대해 7개 함수
 def read_lidar()
 def read_platform()
 def write_platform()
 def main()

 lane_vision.py
  def lane_detection(img)

 a_star.py
  def find_path(...)

 steering.py
   def steering(...)

2017팀의 코드는 위와 같은 구조에서, 대회 미션 수행에 필요한 모든 데이터, 즉, 센서에서 1차로 받은 데이터와 가공된 후의 데이터를 모두 combination.py 내의 global variable로 선언하여 사용했습니다. 세부 프로그램 세 가지 lane_vision.py a_star.py steering.py 의 각 함수에 인자로 넘겨주는 데이터가 센서에서 1차로 받은 데이터이고, 그 함수가 반환하는 데이터가 가공된 후의 데이터입니다. 그 모두를 combination.py의 글로벌 변수로 관리하도록 구현한 것입니다. 따라서 combination.py 내의 모든 함수들은 항상 업데이트되는 같은 데이터에 접근하므로, 데이터가 꼬일 일이 없습니다.

그러나 이 프로그램을 여럿이서 작성한다면 어떻게 될까요? 실제로 2017팀의 개발 인원은 8명이었습니다. 개발자가 자신이 담당한 함수를 작성할 때 다른 개발자가 설계한 구조를 고려해야 하므로, 팀 내에서 빠른 소통이 필요합니다. 그래서 팀원들이 항상 물리적으로 같은 공간 내에서 개발을 진행해야지만 효율적인 코딩이 가능하다는 단점이 있습니다. 또한 한 사람이라도 전체 프로그램의 구조를 이해하지 못한다면 그 개발자는 프로그램에 기여하기가 어렵게 됩니다. 프리라이더 탄생

2017팀 프로그램 구조의 특징

2018팀

저는 그러한 문제를 방지하기 위해서 구조를 바꾸기로 마음먹었습니다. 재택근무 하고 싶어서… 저는 작년 겨울 프로젝트를 설계할 당시, 수업에서 배웠던 파이썬의 객체지향 프로그래밍 개념을 적용해 보고 싶었습니다. 그리고 이 대회를 준비하는 것이 목표하는 바가 무엇인지 생각해 보면서, 대략 다음과 같은 운영 철학을 속으로 생각하고 있었습니다.

학부생 대회 참여의 목표

2018팀 운영 철학

실제로 개발이 완료된 뒤의 2018팀 프로그램 구조는 다음과 같습니다. (역시 핵심 구조만을 표시했습니다.)

main.py
 communication.py  # 차량 통신
  serial_packet.py
 motion_planner.py  # 판단
  lidar.py  # 인지: 라이다
  lane_cam.py
   video_stream.py  # 인지: 차선 웹캠
  sign_cam.py
    shape_detection.py  # 인지: 표지판 웹캠
 car_control.py  # 제어
 monitor.py  # 모니터링 시스템

각 세부 프로그램들은 실행에 필요한 데이터를 얻기 위해 공통된 데이터에 접근하는 것이 아니라, 필요한 인자를 주고 받는 식으로 설계되어 있습니다. 자율주행 프로그램에 대한 요구사항 분석이 철저하지 못했기 때문에 설계해버린 구조라고 할 수 있습니다. 앞서 말씀드렸듯이, 그리고 제가 작성한 후기에서 알 수 있듯이, 문제가 발생했습니다.

그러나 개발 과정에서는 처음부터 끝까지 모든 팀원이 각자 개발에 1인분 이상 기여했습니다. 저를 제외한 9명 모두가 각각 관여하여 끝까지 완성한 프로그램이 하나 이상 존재합니다. 이러한 구조는 각자 자신이 원하는 분야를 선택해 개발하는 것이 용이했습니다. 저는 이 점에서 학부생 대회의 의의를 어느 정도 달성했다고 생각합니다. 결과보다 과정이 중요하지

또한 각 세부 프로그램의 개발자가 명확하기 때문에, 디버깅 시에도 누가 이 문제를 해결할 수 있는지가 명확합니다. 대회 직전 연습 주행장을 왔다갔다하며 실험을 하던 시기가 가장 중요했습니다. 주차 알고리즘 테스트에서 문제가 있었을 때, car_control.py의 개발자는 연습주행장땡볕 아래에서 디버깅을 했습니다. 표지판 모양 인식의 정확도를 높이기 위해, shape_detection.py의 개발자들이 고민하고 해결했습니다. 그리고 최종 문제는 데이터 전달 구조에 있었음을 알았으니, 이 문제는 구조를 설계한 제가 해결해야 하는 것이었던 식으로 말이지요. 대회 일주일 전에만이라도 알았으면 더 좋았을 걸…

2018팀의 문제는 자율주행 아키텍처의 필수 요구사항 중 하나를 너무 늦게 깨달은 것입니다. 그래서 실험은 일찍부터 많이 하면 할수록 좋습니다.

2018팀 프로그램 구조의 특징

보완?

따라서 두 방법을 상호 보완하는 새로운 방법이 필요합니다. 그것은 2019팀이 풀어야 할 숙제일까요? 하하

사실 글을 다 쓰고 나니, ‘데이터 클래스를 설계해서 각 센서 별로 데이터 인스턴스를 만들고 프로그램들이 항상 그 네임스페이스에 접근하게 설계하면 되는 거 아닌가?’ 하는 생각이 그냥 들어버리네요. 이래서 요구 분석이 중요한가 봅니다… 구현하면 또 어떻게 될지 모르겠지만, 갑자기 든 생각은 그렇습니다. 아니면 그냥 대회에서 은상을 수상한 2017팀의 방식대로 글로벌 변수를 두려워하지 않는 것도 방법일 수 있겠습니다. 이 프로젝트가 필요로 하는 게 크게 복잡한 프로그램이 아니라고 생각한다면, 굳이 객체지향 개념을 적용할 필요가 없는 것이지요. 그러면 이전 팀들보다 적은 수의 팀원이 더 열심히 개발해서 1등할 수 있는 코드를 만들 수도 있을 것 같습니다. 무엇이든 분명 더 좋은 방법이 있을 것입니다.

어떤 방식을 선택할지는 팀이 해결해야 할 문제입니다. 그러나 이 글을 통해 내년 대회를 준비하실 분들이 제가 했던 것보다 더 쉽게 선택하실 수 있다면 좋겠습니다. 저도 이 글을 작성하면서 제가 부족했던 점에 대해 정리할 수 있는 좋은 계기가 되었습니다. 앞으로도 열심히 공부합시다…