Table of Contents
“소프트웨어는 닳지 않지만, 펌웨어와 하드웨어에 대한 의존성을 관리하지 않으면 안으로부터 파괴될 수 있다.”
펌웨어는 무엇에 의존하는지, 그리고 하드웨어 발전에 맞춰 수정하기가 얼마나 어려운지에 따라 정의된다.
하드웨어는 발전할 수밖에 없고 그러한 현실을 염두에 두고 임베디드 코드를 구조화할 수 있어야 한다.
우리가 정말로 원하는 건 펌웨어는 더 적게 만들고, 소프트웨어는 더 많이 만드는 것이다.
임베디드 엔지니어가 아닌 당신도 코드에 SQL을 심어 놓거나 개발하는 코드 전반에 플랫폼 의존성을 퍼뜨려 놓는다면, 본질적으로 펌웨어를 작성하는 셈이다.
펌웨어를 수없이 양산하는 일을 멈추고, 코드에게 유효 수명을 길게 늘릴 수 있는 기회를 주어라.
앱-티튜드 테스트
켄트백은 소프트웨어를 구축하는 세 가지 활동을 다음과 같이 기술했다.
- “먼저 동작하게 만들어라”
- “그리고 올바르게 만들어라” : 코드를 리펙터링해서 당신을 포함한 나머지 사람들이 이해할 수 있게 만들고, 요구가 변경되거나 요구를 더 잘 이해하게 되었을 때 코드를 개선할 수 있게 만들어라.
- “그리고 빠르게 만들어라”
프레드 브룩스는 “맨먼스 미신”에서 “버리기 위한 계획을 세우라”고 제안했다.
켄트와 프레드는 사실상 똑같은 충고를 하고 있다. 동작하는 것을 배워라. 그리고 나서 더 나은 해결책을 만들어라.
현장에서 지켜본 수많은 임베디드 시스템 소프트웨어는 “동작하게 하라”는 활동만을 염두에 두고 작성된 것처럼 보인다. 또는 “빠르게 만들어라”라는 목표에도 집착하는 것처럼 보인다.
이러한 문제들은 임베디드 소프트웨어만 국한되지 않는다. 임베디드가 아닌 대다수의 앱들도 그저 동작하도록 만들어진다.
앱이 동작하도록 만드는 것을 “개발자용 앱-티튜드 테스트(App-titude test)라고 부른다.
이 애플리케이션이 동작한다. 엔지니어는 앱-티튜드 테스트를 통과했다.
하지만 이 애플리케이션이 클린 임베디드 아키텍처를 가진다고 말하기는 어렵다.
타깃-하드웨어 병목현상
임베디드 개발자들은 임베디드가 아니었다면 다루지 않아도 될 특수한 관심사를 많이 가지고 있다.
임베디드가 지닌 특수한 문제 중 하나는 타깃-하드웨어 병목현상이다.
임베디드 코드가 클린 아키텍처 원칙과 실천법을 따르지 않고 작성된다면, 대개의 경우 코드를 테스트할 수 있는 환경이 해당 특정 타깃으로 국한될 것이다.
그리고 그 타깃이 테스트가 가능한 유일한 장소라면 타깃-하드웨어 병목현상이 발생하여 진척이 느려질 것이다.
클린 임베디드 아키텍처는 테스트하기 쉬운 임베디드 아키텍처다
몇 가지 아키텍처 원칙을 임베디드 소프트웨어와 펌웨어에 적용하여 타깃-하드웨어 병목현상을 줄이는 방법을 살펴보자.
계층
계층에는 여러 가지가 있다. 일단 아래의 그림에서 나타낸 세 개의 계층부터 시작하자.
최소한 하드웨어가 정의된 이후라면 하드웨어와 나머지 시스템 사이의 분리는 주어진다.(아래 그림 참고)
이 상태에서 앱-티튜드 테스트를 해보면 대체로 문제가 발생한다. 하드웨어 관련 정보가 코드 전체를 오염시키지 못하게 막을 방법이 전혀 없다.
소프트웨어와 펌웨어가 서로 섞이는 일은 안티 패턴이다. 이 안티 패턴을 보이는 코드는 변화에 저항하게 된다.
변경하기 어려울 뿐 아니라 변경하는 일 자체가 위험을 수반하여, 때로는 의도치 않은 결과를 불러온다.
하드웨는 세부사항이다.
소프트웨어와 펌웨어 사이의 경계는 코드와 하드웨어 사이의 경계와는 달리 잘 정의하기가 대체로 힘들다.
임베디드 소프트웨어 개발자가 해야 할 일 하나는 이 경계를 분명하게 만드는 것이다.
소프트웨어와 펌웨어 사이의 경계는 하드웨어 추상화 계청(Hardware Abstraction Layer, HAL)이라고 부른다.
HAL은 자신보다 위에 있는 소프트웨어를 위해 존재하므로, HAL의 API는 소프트웨어의 필요에 맞게 만들어져야 한다.
소프트웨어는 이름/값 쌍을 저장하기 위해 어떤 장치에 저장되는지를 전혀 개의치 않는다.
이러한 서비스는 HAL이 제공하며, 어떻게 저장하는지에 대해서는 소프트웨어에게 드러내지 않는다.
어떤 장치로 구현하느냐는 소프트웨어로부터 반드시 숨겨야 하는 세부사항 이다.
계층은 또 다른 계층을 포함할 수도 있다. 그래서 계층의 수가 정해진 상태로 구성되기보다는 프랙털(fractal) 패턴에 더 가깝다.
HAL 사용자에게 하드웨어 세부사항을 드러내지 말라
클린 임베디드 아키텍처로 설계된 소프트웨어는 타깃 하드웨어에 관계없이 테스트가 가능하다.
HAL을 제대로 만들었다면, HAL은 타깃에 상관없이 테스트할 수 있는 경계층 또는 일련의 대체 지점을 제공한다.
프로세서는 세부사항이다.
프로세서 제작 업체가 제공하는 C 컴파일러는 종종 전역 변수처럼 보이는 것들을 제공하여 해당 프로세서에 한정된 함수 등을 제공할수 있다.
클린 임베디드 아키텍처라면 이들 장치 접근 레지스터를 직접 사용하는 코드는 소수의, 순전히 펌웨어로만 한정시켜야 한다.
펌웨어가 저수준 함수들을 프로세서 추상화 계층(Processor Abstraction Layer, PAL)의 형태로 격리시켜줄 수 있다.
PAL 상위에 위치하는 펌웨어는 타깃-하드웨어에 관계없이 테스트할 수 있게 되어 펌웨어 자체도 덜 딱딱해질 수 있다.
운영체제는 세부사항이다.
작성한 코드의 수명을 늘리려면, 무조건 운영체제를 세부사항으로 취급하고 운영체제에 의존하는 일을 막아야 한다.
소프트웨어는 운영체제를 통해 운영 환경이 제공하는 서비스에 접근한다. OS는 소프트웨어를 펌웨어로부터 분리하는 계층이다.
클린 임베디드 아키텍처는 운영체제 추상화 계층(OS Abstraction Layer, OSAL)을 통해 소프트웨어를 운영체제로부터 격리시킨다.
소프트웨어가 OS에 직접적으로 의존하는 대신 OSAL에 의존한다면, 이식 작업의 대부분은 기존 OSAL과 호환되도록 새로운 OSAL을 작성하는 데 소요될 것이다.
이제 코드 비대화(code bloat) 문제가 염려되기 시작할 수도 있다. 실제로 OSAL은 OS를 사용하는 데 따른 수많은 중복이 격리되어 있는 장소다.
하지만 이러한 중복이 그다지 큰 비용을 추가로 초래하지는 않는다.
OSAL을 정의하는 일은 결국 애플리케이션에서도 공통 구조를 가지도록 힘쓰는 일이기도 하다.
OSAL은 테스트 지점을 만드는 데 도움이 되며, 그 덕분에 소프트웨어 계층의 귀중한 애플리케이션 코드를 타깃이나 OS에 관계없이 테스트할 수 있게 된다.
클린 임베디드 아키텍처를 따른 소프트웨어는 타깃 운영체제에 관계없이 테스트할 수 있다.
제대로 만든 OSAL은 타깃과는 별개로 테스트할 수 있도록 해주는 경계층 또는 일련의 대체 지점을 제공한다.
인터페이스를 통하고 대체 가능성을 높이는 방향으로 프로그래밍하라
HAL을 추가하거나 때로는 OSAL을 추가해야 할 뿐만 아니라, 모든 주요 계층(소프트웨어, OS 펌웨어, 하드웨어) 내부에는 이 책에서 설명한 원칙들을 적용할 수 있다.
이들 원칙은 관심사를 분리시키고, 인터페이스를 활용하며, 대체 가능성을 높이는 방향으로 프로그래밍하도록 유도한다.
계층형 아키텍처(layered architecture)는 인터페이스를 통해 프로그래밍하자는 발상을 기반 으로 한다.
모듈들이 서로 인터페이스를 통해 상호작용한다면 특정 서비스 제공자를 다른 제공자로 대체할 수 있다.
경험법칙에 따르면 인터페이스 정의는 헤더 파일에 해야 한다. 하지만 이 경우 헤더 파일에 무엇을 포함시켜야 할지는 신중하게 정해야 한다.
헤더 파일에는 함수 선언과 그 함수에서 사용하는 상수와 구조체 이름만 포함시켜야 한다.
오직 구현체에서만 필요한 데이터 구조, 상수, 타입 정의들로 인터페이스 헤더 파일을 어지럽히지 말라. 이는 단순히 어수선해지는 문제로 끝나지 않고, 결국 원치 않는 의존성을 만들어낼 것이다.
구현 세부사항의 가시성을 제한하라. 구현 세부사항은 변경될 거라고 가정하라. 세부사항을 알고 있는 부분이 적을수록 추적하고 변경해야 할 코드도 적어진다.
클린 임베디드 아키텍처에서는 모듈들이 인터페이스를 통해 상호작용하기 때문에 각각의 계층 내부에서 테스트가 가능하다. 각 인터페이스는 타깃과는 별개로 테스트할 수 있도록 해주는 경계층 또는 대체 지점을 제공한다.
DRY 원칙: 조건부 컴파일 지시자를 반복하지 말라
임베디드 프로그램에서는 조건부 컴파일을 사용해서 특정 코드 블록을 활성화하거나 비활성화한다.
특히 기억에 남는 문제 사례는 통시 ㄴ관련 애플리케이션이었는데, “#ifdef BOARDV2” 구문이 수천 번이나 사용되었다.
이처럼 코드를 반복하는 일을 반복하지 말라(Don’t Repeat Yourself, DRY)는 원칙을 위배한다.
만약 HAL이 조건부 컴파일 대신 사용할 수 있는 일련의 인터페이스를 제공한다면, 우리는 링커 또는 어떤 형태의 실시간 바인딩을 사용해서 소프트웨어를 하드웨어와 연결할 수 있다.
결론
모든 코드가 펌웨어가 되도록 내버려두면 제품이 오래 살아남을 수 없게 된다.
오직 타깃 하드웨어에서만 테스트할 수 있는 제품도 마찬가지다.
클린 임베디드 아키텍처는 제품이 장기간 생명력을 유지하는 데 도움을 준다.