Clean Architecture 정리 - 5부 29장 클린 임베디드 아키텍처
2025. 7. 29. 11:03

기능 동작에만 집중한 개발의 한계

임베디드 개발자의 일부는 요구된 기능이 정상 동작하는 것에만 집중하여 코드를 작성하는 경우가 있음.
처음부터 특정 하드웨어에만 사용될 것이라는 전제로 개발하게 되어 모든 기능이 해당 하드웨어에 밀접하게 결합
그 결과, 코드는 재사용이 어려워지고, 다른 하드웨어 환경에서는 이식이 불가능한 구조가 되어버림.


타깃-하드웨어 병목현상

특정 하드웨어에 종속된 개발은 개발 병목을 유발함
→ 하드웨어가 도착하지 않았거나, 하드웨어에 결함이 있을 경우
→ 해당 상황에 종속된 소프트웨어 개발도 지연될 수밖에 없음
→ 즉, 개발의 병목은 하드웨어 상태에 의해 발생함

 

하드웨어가 변경되면, 코드도 함께 영향을 받음
→ 일부 하드웨어 변경이 있어도 전체 기능 흐름이 깨질 수 있음
→ 하드웨어에 맞춰 코드를 맞물려 작성했기 때문

 

하드웨어-소프트웨어가 강결합된 구조는 유연성을 떨어뜨림
→ 사용자의 요구사항이 바뀌거나 코드상 문제가 생겨도 쉽게 대응할 수 없음
→ 구조 자체가 서로 너무 강하게 결합되었기 때문

 

테스트도 병목에 빠짐
→ 소프트웨어만으로 테스트 불가능
→ 하드웨어가 동작하는 전 과정을 기다려야 하므로 테스트 주기도 느려짐


세부사항의 분리

 

[ 하드웨어는 세부사항이다 ]

소프트웨어와 펌웨어 사이의 경계를 잘 나누어야 한다.

HAL(Hardware Abstraction Layer)


HAL은 소프트웨어가 사용한다는 관점으로 만들어져야한다.

예시:

소프트웨어는 key-value 형태로 데이터를 저장/활용하며, HAL의 정의된 데이터 저장 기능을 활용

  • HAL 구현체인 펌웨어가 플래시 메모리, 하드디스크, 클라우드 등 무엇을 활용하여 어떻게 저장하는지 모름

소프트웨어는 배터리가 부족할 때, HAL의 정의된 Indicate_LowBattery()를 사용

  • 펌웨어가 이에 대한 특정 LED를 켜는 기능을 구현

 

[ 프로세스는 세부사항이다 ]

 

임베디드 개발에서 특정 프로세서에 종속된 코드는 개발의 유연성을 떨어뜨림

제조사들이 제공하는 컴파일러와 헤더 파일은 해당 칩에만 최적화되어 있는 경우가 많음.

예를 들어 acmetypes.h는 그 칩에서만 사용 가능한 자료형(type)을 정의

#if defined(_ACME_X42)
typedef unsigned int      Uint_32;
typedef unsigned short    Uint_16;
typedef unsigned char     Uint_8;

typedef int               Int_32;
typedef short             Int_16;
typedef char              Int_8;

#elif defined(_ACME_A42)
typedef unsigned long     Uint_32;
typedef unsigned int      Uint_16;
typedef unsigned char     Uint_8;

typedef long              Int_32;
typedef int               Int_16;
typedef char              Int_8;

#else
#error <acmetypes.h> is not supported for this environment
#endif

이런 헤더 파일을 그대로 사용하는 순간, 코드는 특정 하드웨어에 종속됨.

다른 하드웨어에서는 컴파일조차 되지 않음.

 

표준 타입 정의(stdint.h)를 사용하는 것이 더 안전하고 재활용성이 좋음

하지만 타깃 컴파일러에 따라 stdint.h를 사용할 수 없을 수 있는데,

이 경우에는 acmetypes.h를 기반으로 우리가 직접 stdint.h처럼 보이도록 재정의한 헤더 파일을 만들 수 있음:

// stdint.h가 없는 환경에서 대체 정의
#ifndef _STDINT_H_
#define _STDINT_H_
#include <acmetypes.h>

typedef Uint_32 uint32_t; // acmetype.h에서 정의된 type -> stdint.h 형태로 재정의
typedef Uint_16 uint16_t;
typedef Uint_8  uint8_t;

typedef Int_32 int32_t;
typedef Int_16 int16_t;
typedef Int_8  int8_t;
#endif

 

특정 하드웨어에 묶인 코드의 위험성
다음 예제는 특정 마이크로컨트롤러의 레지스터를 직접 제어하는 코드로, "hi"를 출력함.

코드만 보면 C 같지만 실제로는 특정 하드웨어에 강하게 묶인 펌웨어임:

void say_hi() {
  IE = 0b11000000;
  SBUF0 = (0x68);
  while(TI_0 == 0);
  TI_0 = 0;
  SBUF0 = (0x69);
  while(TI_0 == 0);
  TI_0 = 0;
  SBUF0 = (0x0a);

  while(TI_0 == 0);
  TI_0 = 0;
  SBUF0 = (0x0d);
  while(TI_0 == 0);
  TI_0 = 0;
  IE = 0b11010000;
}

이 코드의 문제는 다음과 같음:

  • IE, SBUF0, TI_0 같은 레지스터는 특정 칩에서만 존재함
  • 0b11000000 같은 2진수 표기나 직접적인 레지스터 제어는 C에서 제공하지 않는 기능임
  • 결과적으로 이 코드는 일반 C 코드가 아닌 펌웨어에 가까움

이런 접근은 추상화 계층(PAL) 아래로 내려보내야 함
→ 직접 레지스터를 조작하는 코드는 별도의 Processor Abstraction Layer로 감싸고,

위의 로직은 일반 함수로 표현해야 다른 하드웨어로의 이식, 테스트, 유지보수가 쉬워짐

 

[ 운영체제는 세부사항이다 ]

OS를 사용하는 임베디드 시스템도 존재한다.
코드의 수명을 늘리려면, 운영체제 또한 세부사항으로 취급하여 의존하지 말아야한다.

Operating System Abstraction Layer

OS를 의존했을 때:

  • OS 사용료가 올랐다면
  • 새로운 기능이 필요한데, OS가 제공하지 않는다면? → 다른 OS를 고려해야 하지만, 의존적인 코드 때문에 쉽지 않은 일이 된다.

OSAL을 통해 소프트웨어를 운영체제로 격리시자.

 

OSAL을 사용하면 생기는 장점

  1. 운영체제 교체가 쉬워진다
  2. 공통 구조를 설계할 수 있다
  3. 테스트가 쉬워진다

→ OSAL은 단순한 운영체제 추상화가 아니라, 유지보수성과 테스트 가능성을 높여주는 핵심 계층이다.


조건부 컴파일을 줄이기 위한 추상화 방법

#ifdef BOARD_V2
  BOARD_V2 일때 필요한 코드
#else
  BOARD_DEFAULT 일때 필요한 코드
#endif

이런 조건부 분기가 반복되면(수천개) 유지보수가 어려워짐.

대신, HAL을 구성해서 하드웨어별 구현을 감추고 공통된 인터페이스만 노출하는 것이 좋다.

 

1. HAL 정의 (헤더 파일)

// board_uart.h
void uart_init(void);

 

2. 보드별 구현 파일들

// board_uart_v2.c
#include "board_uart.h"
void uart_init(void) {
    uart_v2_configure();
}
// board_uart_default.c
#include "board_uart.h"
void uart_init(void) {
    uart_default_configure();
}

 

3. 메인 코드 (공통 인터페이스만 호출)

#include "board_uart.h"
void setup() {
    uart_init();
}

→ 보드별 분기 처리는 빌드 시스템 또는 HAL 내부에서만 수행함.


강한 결합은 임베디드 개발자만의 것이 아니다

강한 결합의 문제는 하드웨어 제어를 다루는 임베디드 개발자만의 문제라고 생각하지만 꼭 그렇지도 않다.

예를 들어 SQL을 코드에 깊이 박거나, Android API 등 특정 플랫폼에 종속적인 방식으로 앱을 작성한다면,

해당 플랫폼이 없이는 그 시스템이 작동할 수 없다.

이런 구조는 마치 하드웨어에 종속된 '펌웨어'처럼 동작하게 되며, 결국 강한 결합을 만들게 된다.

 

 

※ 본 글은 『Clean Architecture』(로버트 C. 마틴 저) 5부 29장을 기반으로 학습 목적으로 요약한 글입니다.

※ 이 글은 책의 내용을 요약한 것으로, 원문 없이 읽을 경우 오해의 여지가 있을 수 있습니다. 정확한 이해를 위해 원서의 정독을 권장합니다.