C 언어를 공부하다 보면 다음과 같은 코드를 자주 보게 된다.
#define TRUE 1U
#define FALSE 0U
또는
#define MOTOR_MAX_SPEED 3000U
처음에는 단순히 “값을 정의하는 기능” 정도로 생각하기 쉽다.
하지만 실제로 #define 은 단순 상수 선언 이상의 의미를 가진다.
특히 임베디드 시스템이나 AUTOSAR 프로젝트에서는 전처리기(Preprocessor)를 매우 많이 사용한다.
예를 들면 다음과 같은 곳에서 등장한다.
- MCU 기능 ON/OFF 설정
- 디버그 코드 활성화
- 플랫폼별 코드 분리
- 헤더 파일 중복 방지
- 메모리 맵 설정
- AUTOSAR 모듈 설정
- 조건부 컴파일
실제 프로젝트에서는 전처리기를 이해하지 못하면 코드 흐름 자체를 이해하기 어려운 경우도 많다.
1. 전처리기(Preprocessor)란 무엇인가?
C 언어 프로그램은 소스 코드를 작성했다고 해서 바로 실행 파일이 만들어지는 것이 아니다.
실제로는 여러 단계를 거쳐 컴파일이 진행된다.
대표적으로 다음과 같은 과정을 수행한다.
전처리 → 컴파일 → 어셈블 → 링킹 → 실행 파일 생성
여기서 #define, #include, #if 같은 명령들은 컴파일 전에 먼저 처리된다.
이 작업을 수행하는 기능이 바로 전처리기(Preprocessor)이다.
즉, 전처리기는 다음과 같은 역할을 수행한다.
컴파일 전에 소스 코드를 미리 가공하는 기능
쉽게 말하면 코드 치환, 헤더 파일 삽입, 조건별 코드 선택 등을 컴파일 전에 처리하는 기능이라고 생각하면 된다.
2. #define 이란 무엇인가?
#define 은 특정 문자나 값을 다른 내용으로 치환하는 기능이다.
예제를 먼저 보자.
#define MAX_SPEED 3000U
이 코드는 다음 의미를 가진다.
"소스 코드에서 MAX_SPEED 라는 이름이 나오면 컴파일 전에 3000U 로 변경한다."
즉, 아래 코드가 있다면
uint16 MotorSpeed = MAX_SPEED;
전처리 이후에는 내부적으로 다음처럼 변경된다.
uint16 MotorSpeed = 3000U;
즉, #define 은 실제 변수를 생성하는 것이 아니라, 단순 문자열 치환 방식으로 동작한다.
3. 왜 #define 을 사용하는가?
처음에는 다음과 같은 생각이 들 수 있다.
그냥 숫자를 직접 쓰면 되는 거 아닌가?
물론 가능하다.
하지만 실제 프로젝트에서는 같은 값을 여러 곳에서 반복 사용하는 경우가 매우 많다.
예를 들어 다음 코드를 보자.
if (Speed > 3000U)
{
}
MotorLimit = 3000U;
CanData[0] = 3000U;
이렇게 숫자를 직접 사용하는 코드를 매직 넘버(Magic Number)라고 부른다.
문제는 코드 의미를 파악하기 어렵다는 것이다.
또한 나중에 값을 수정해야 할 경우 모든 위치를 직접 찾아 수정해야 한다.
반면 #define 을 사용하면 다음처럼 작성 가능하다.
#define MOTOR_MAX_SPEED 3000U
if (Speed > MOTOR_MAX_SPEED)
{
}
MotorLimit = MOTOR_MAX_SPEED;
이렇게 하면 코드 의미가 명확해지고, 유지보수가 쉬워지며, 값 수정이 간단해진다.
특히 실무에서는 숫자 자체보다 “의미 있는 이름” 으로 관리하는 것이 매우 중요하다.
4. 실무에서 자주 사용하는 #define 형태
임베디드 코드에서는 다음과 같은 형태를 매우 자주 본다.
#define TRUE 1U
#define FALSE 0U
#define STD_ON 1U
#define STD_OFF 0U
#define CAN_TIMEOUT 100U
#define MOTOR_MAX_RPM 5000U
특히 AUTOSAR 코드에서는
STD_ON
STD_OFF
를 매우 많이 사용한다.
예를 들어
#if (CAN_DEV_ERROR_DETECT == STD_ON)
#endif
처럼 기능 활성화 여부를 설정한다.
5. const 와 #define 차이점
초보자들이 가장 많이 헷갈리는 부분 중 하나이다.
다음 두 코드를 보자.
#define MAX_SPEED 3000U
const uint16 MaxSpeed = 3000U;
둘 다 상수처럼 보이지만 내부 동작은 완전히 다르다.
#define 특징
#define MAX_SPEED 3000U
는 전처리 단계에서 단순 문자열 치환으로 동작한다.
즉
MAX_SPEED → 3000U
로 변경된다.
따라서
- 자료형(Type)이 존재하지 않는다.
- 디버깅 시 변수처럼 확인하기 어렵다.
- 메모리를 사용하지 않을 수도 있다.
const 특징
const uint16 MaxSpeed = 3000U;
는 실제 변수를 생성한다.
즉
- 자료형 존재
- 메모리 공간 사용 가능
- 디버거에서 변수 확인 가능
이라는 특징이 있다.
어떤 것을 사용하는 것이 좋은가?
상황에 따라 다르다.
보통 실무에서는
고정 상수 값 → const 사용
조건 컴파일 → #define 사용
형태를 많이 사용한다.
특히 최근 코드 스타일에서는 단순 숫자 상수는 const 사용을 권장하는 경우도 많다.
6. 매크로 함수(Macro Function)
#define 은 함수처럼 사용할 수도 있다.
예제를 보자.
#define SQUARE(x) ((x) * (x))
사용
result = SQUARE(5);
전처리 이후
result = ((5) * (5));
처럼 변경된다.
즉, 함수 호출처럼 보이지만 실제 함수는 아니다.
7. 매크로 함수 사용 시 주의점
매크로 함수는 매우 편리하지만 주의하지 않으면 위험한 버그가 발생할 수 있다.
예를 들어 다음 코드를 보자.
#define SQUARE(x) x * x
그리고
result = SQUARE(1 + 2);
전처리 이후 실제 코드는
result = 1 + 2 * 1 + 2;
가 된다.
개발자는 (1 + 2) * (1 + 2) 를 기대했지만 실제 결과는 다르게 계산된다.
이러한 문제를 방지하기 위해 매크로 함수는 반드시 괄호를 사용하는 습관이 중요하다.
#define SQUARE(x) ((x) * (x))
8. 매크로 함수와 부작용(Side Effect)
매크로 함수는 인자가 여러 번 평가될 수 있기 때문에 주의해야 한다.
예를 들어
#define INCREASE(x) ((x) + (x))
다음 코드를 보자.
result = INCREASE(i++);
전처리 이후에는
result = ((i++) + (i++));
가 된다.
즉, i 값이 두 번 증가하게 된다.
이러한 문제를 매크로의 부작용(Side Effect)이라고 한다.
따라서 복잡한 연산은 일반 함수 사용이 더 안전한 경우가 많다.
9. 조건부 컴파일(#if)
전처리기의 가장 강력한 기능 중 하나이다.
예제를 보자.
#define DEBUG_MODE STD_ON
#if (DEBUG_MODE == STD_ON)
printf("Debug Message\n");
#endif
이 코드는 DEBUG_MODE 가 ON 일 때만 컴파일된다.
즉, 특정 기능을 선택적으로 포함할 수 있다.
10. 조건부 컴파일이 중요한 이유
임베디드 시스템에서는 메모리와 성능이 매우 중요하다.
따라서 필요 없는 기능은 제외하는 경우가 많다.
예를 들면
- 디버그 코드 제거
- 개발용 로그 비활성화
- 특정 MCU 기능 선택
- 차량 옵션별 기능 분리
- 개발 모드 / 양산 모드 구분
등을 조건부 컴파일로 처리한다.
실제 AUTOSAR 코드에서도 매우 자주 사용된다.
11. #ifdef 와 #ifndef
다음과 같은 문법도 매우 자주 사용한다.
#ifdef DEBUG_MODE
의미는 다음과 같다.
"DEBUG_MODE 가 정의되어 있으면"
반대로
#ifndef DEBUG_MODE
는 다음 의미이다.
"DEBUG_MODE 가 정의되어 있지 않으면"
즉, 특정 매크로 정의 여부에 따라 코드를 선택적으로 포함할 수 있다.
12. 헤더 가드(Header Guard)
헤더 파일에서는 다음 코드를 매우 자주 본다.
#ifndef MOTOR_H
#define MOTOR_H
void Motor_Run(void);
#endif
이것을 헤더 가드(Header Guard)라고 한다.
왜 필요한가?
헤더 파일이 여러 번 include 되면 함수 선언이나 변수 선언이 중복될 수 있다.
예를 들어 redefinition error, multiple definition error 같은 에러가 발생할 수 있다.
헤더 가드는 이런 문제를 방지한다.
즉, 한 번 include 된 헤더 파일을 다시 include 하지 않도록 막는 기능이다.
실제 프로젝트에서는 거의 모든 헤더 파일에 사용된다.
13. #include 도 전처리기 기능이다
많은 초보자들이 놓치는 부분 중 하나이다.
#include "Motor.h"
이 코드 역시 전처리기 기능이다.
즉, 컴파일 전에 Motor.h 파일 내용을 현재 위치에 삽입한다.
쉽게 말하면 "헤더 파일 내용을 복사해서 붙여넣는 기능"과 비슷하게 동작한다고 이해하면 된다.
14. 실무에서 전처리기를 많이 사용하는 이유
실제 임베디드 프로젝트에서는 전처리기를 매우 많이 사용한다.
예를 들면
- MCU 종류별 코드 분리
- 기능 활성화 설정
- 디버그 로그 제어
- AUTOSAR 모듈 설정
- 메모리 맵 구성
- 컴파일 옵션 설정
등이다.
특히 AUTOSAR 코드에서는 다음과 같은 형태를 자주 본다.
#if (STD_ON == DCM_DEV_ERROR_DETECT)
#endif
처음에는 복잡해 보이지만 대부분 전처리기 기반 설정 코드이다.
정리
이번 장에서는 #define 과 전처리기(Preprocessor)에 대해 알아보았다.
전처리기는 컴파일 전에 소스 코드를 미리 처리하는 기능이며,
#define 은 특정 값을 다른 내용으로 치환하는 기능이다.
처음에는 단순 상수 선언처럼 보일 수 있지만,
실제 임베디드 시스템에서는
- 조건부 컴파일
- 기능 ON/OFF 제어
- 코드 재사용
- 헤더 보호
- 메모리 최적화
- 플랫폼별 코드 분리
등 다양한 목적으로 사용된다.
특히 AUTOSAR 및 차량 SW 프로젝트에서는 거의 모든 파일에서 사용될 정도로 매우 중요한 개념이다.
다음 장에서는 실무 코드 구조를 이해하기 위해 반드시 필요한 .h 파일 과 .c 파일 구조에 대해 알아본다.
'C 언어 > 기초 문법' 카테고리의 다른 글
| [18장] C언어 typedef 와 enum — 실무 코드의 가독성과 유지보수를 높이는 핵심 문법 (0) | 2026.05.22 |
|---|---|
| [17장] C언어 const 와 volatile — 임베디드 개발에서 가장 중요한 키워드 이해하기 (0) | 2026.05.21 |
| [16장] C언어 메모리 구조(Stack / Heap / Data / BSS / Text) (0) | 2026.05.20 |
| [15장] C언어 구조체 포인터와 -> 연산자 (1) | 2026.05.18 |
| [14장] C언어 구조체(struct) - 서로 다른 자료형 데이터를 하나로 묶는 방법 (0) | 2026.05.18 |