재진입 가능 코드 (Reentrant Code / Pure Code)

Ⅰ. 개요

1. 정의

**재진입 가능 코드(Reentrant Code)**란 실행 중인 동일한 함수가 인터럽트(시그널, 하드웨어 인터럽트)에 의해 중단되더라도, 중단된 시점부터 다시 안전하게 재진입하여 실행할 수 있는 코드이다.

재진입 가능 코드의 핵심 개념
┌─────────────────────────────────────────────────┐
│                                                 │
│   함수 F() 실행 중                               │
│       │                                         │
│       ├─────── 인터럽트/시그널 발생              │
│       │                                         │
│       ▼                                         │
│   함수 F() 재진입 (새로운 호출)                  │
│       │                                         │
│       ▼                                         │
│   첫 번째 실행과 충돌 없이 정상 완료             │
│                                                 │
│   ★ 정적/전역 상태를 변경하지 않아야 가능        │
└─────────────────────────────────────────────────┘

2. 순수 함수와의 관계

  • 순수 함수(Pure Function): 동일한 입력에 항상 동일한 출력 반환, 부작용 없음
  • 모든 순수 함수는 재진입 가능하다
  • 하지만 모든 재진입 가능 함수가 순수 함수인 것은 아니다
순수 함수 ⊂ 재진입 가능 함수
┌───────────────────────────────────┐
│        재진입 가능 함수            │
│   ┌──────────────────────┐       │
│   │     순수 함수         │       │
│   │  (항상 같은 결과)     │       │
│   └──────────────────────┘       │
│   (순수하지 않아도 재진입 가능)    │
│   예: 호출자 제공 버퍼에 기록     │
└───────────────────────────────────┘

비유: 재진입 가능 코드는 "여러 조리대에서 동시에 같은 레시피로 요리해도 재료가 석이지 않는 것"과 같다. 순수 함수는 그중에서도 "재료가 항상 같으면 결과도 항상 같은 완벽한 레시피"이다.

Ⅱ. 재진입 가능 코드의 조건

1. 공유/정적 데이터 수정 금지

재진입 불가 예시 (정적 변수 사용)
┌─────────────────────────────────────────────────┐
│  int sum(int a, int b) {                        │
│      static int count = 0;  // 정적 변수!        │
│      count++;                                   │
│      return a + b + count;  // 호출마다 결과 다름│
│  }                                              │
│                                                 │
│  문제:                                          │
│  sum(1,2) → 4 (count=1)                         │
│  → 인터럽트 발생 →                              │
│  sum(1,2) → 5 (count=2)  // 재진입 시 값 변함   │
│  → 이전 실행도 count=2로 영향받음                │
└─────────────────────────────────────────────────┘

2. 모든 데이터는 호출자로부터 (매개변수, 지역변수)

재진입 가능 예시
┌─────────────────────────────────────────────────┐
│  int sum(int a, int b) {                        │
│      int result = a + b;   // 지역변수만 사용    │
│      return result;                             │
│  }                                              │
│                                                 │
│  특징:                                          │
│  - 정적/전역 변수 없음                           │
│  - 모든 데이터를 매개변수와 지역변수로 처리       │
│  - 스택에만 데이터 존재 (각 호출마다 독립)       │
│  - 언제든 재진입 가능                            │
└─────────────────────────────────────────────────┘

3. 동적 메모리 관리 주의

동적 메모리 재진입 문제
┌──────────────────────────────────────────────────┐
│  char* safe_buffer(const char* src, char* buf) { │
│      strcpy(buf, src);    // OK: 호출자 버퍼     │
│      return buf;                                  │
│  }                                               │
│                                                  │
│  char* unsafe_buffer(const char* src) {           │
│      char* buf = malloc(...); // 위험:            │
│      strcpy(buf, src);         // free 누락 가능  │
│      return buf;                // 글로벌 힙 사용  │
│  }                                               │
└──────────────────────────────────────────────────┘

Ⅲ. 재진입 가능 코드가 필수인 경우

1. 시그널 핸들러 (Signal Handler)

시그널 핸들러에서의 재진입 가능
┌─────────────────────────────────────────────────┐
│                                                 │
│  main() 실행 중                                 │
│  │                                             │
│  ├─ printf("Hello")  ← 비재진입 함수 실행 중     │
│  │                                             │
│  ▼ 시그널 (SIGINT) 발생!                        │
│  │                                             │
│  ├─ signal_handler()                            │
│  │   └─ printf("Caught") ← 재진입 시 충돌!      │
│  │       (내부 버퍼, 락 상태 오염 가능)          │
│  │                                             │
│  해결: write() 시스템 콜 등 재진입 가능 함수만 사용│
└─────────────────────────────────────────────────┘

POSIX에서 시그널 핸들러에서 안전하게 호출 가능한 함수(Async-Signal-Safe):

  • write(), read(), _exit(), sigprocmask(), kill()
  • printf() 호출 금지, malloc() 호출 금지

2. 인터럽트 서비스 루틴 (ISR)

ISR에서 재진입 가능의 필요성
┌─────────────────────────────────────────────────┐
│                                                 │
│  ISR_A 실행 중                                  │
│    │                                            │
│    ├── 공유 데이터 수정 중...                    │
│    │                                            │
│    ▼ 우선순위 높은 ISR_B 발생!                  │
│    │                                            │
│    ├── ISR_B 실행 (ISR_A와 같은 함수 호출)       │
│    │                                            │
│    ▼ ISR_B 완료 → ISR_A 재개                    │
│                                                 │
│  ★ 재진입 불가하면 데이터 손상                   │
└─────────────────────────────────────────────────┘

Ⅳ. 재진입 가능 vs 순수 함수 비교

1. 상세 비교표

┌──────────────────┬──────────────────┬──────────────────┐
│     특징         │  재진입 가능      │    순수 함수      │
├──────────────────┼──────────────────┼──────────────────┤
│ 정적 데이터 변경  │       X          │       X          │
│ 전역 데이터 변경  │       X          │       X          │
│ 동일 입력→동일 출력│  보장 안 함      │      보장         │
│ 부작용           │  없음 (안전)      │   완전히 없음     │
│ 호출자 버퍼 기록  │       O          │       X          │
│ 시그널 핸들러 사용│       O          │       O          │
└──────────────────┴──────────────────┴──────────────────┘

포함 관계: 순수 함수 ⊂ 재진입 가능 함수

2. 예제 코드 비교

재진입 가능하지만 순수 함수가 아닌 예
┌────────────────────────────────────────────────┐
│  // 호출자 버퍼에 해시 값을 기록               │
│  void compute_hash(const char* input,           │
│                    unsigned char* output) {     │
│      // input → output에 기록 (부작용 있음)     │
│      // 하지만 정적/전역 상태 변경 없음          │
│      // → 재진입 가능                            │
│      // → 하지만 순수 함수는 아님 (출력 버퍼 변경)│
│  }                                              │
└────────────────────────────────────────────────┘

Ⅴ. 지식 그래프 및 요약

1. 지식 그래프

[재진입 가능 코드]
├── [조건]
│   ├── 정적/전역 데이터 수정 금지
│   ├── 모든 데이터는 매개변수/지역변수로
│   └── 동적 메모리 할당 자제
├── [필수 영역]
│   ├── 시그널 핸들러 (async-signal-safe)
│   ├── 인터럽트 서비스 루틴 (ISR)
│   └── 실시간 운영체제 (RTOS)
├── [순수 함수]
│   ├── 재진입 가능의 특수한 경우
│   ├── 동일 입력 → 항상 동일 출력
│   └── 관찰 가능한 부작용 없음
└── [안전하지 않은 함수]
    ├── strtok, localtime, ctime, asctime
    ├── printf, malloc, free
    └── _r 접미사 함수로 대체

2. 준말

  • ISR: Interrupt Service Routine (인터럽트 서비스 루틴)
  • RT: Reentrant Thread-safe (재진입 가능 스레드 안전)

3. 어린이를 위한 3줄 설명

재진입 가능 코드는 같은 놀이를 여러 번 동시에 해도 서로 방해하지 않는 룰이에요. 시그널이나 인터럽트처럼 갑자기 다른 일이 생겨도 안전하게 멈췄다 다시 시작할 수 있어요. 순수 함수는 그중에서도 항상 같은 재료를 넣으면 같은 결과가 나오는 완벽한 레시피예요.