파일 입출력 실습 — fopen·open 오류 패턴과 임시파일 + rename 안전 저장

NOTE

C 기초 강의의 파일 I/O 실습(10-01 ~ 10-04)에서 만난 오류 패턴을 정리하고, 이를 응용해 공백 제거 복사 프로그램을 open+fdopen → 반환값 검사 → 임시 파일 + rename 으로 단계적으로 개선한다.

환경은 Linux 컨테이너, gcc -g -O0 -Wall -Wextra.

IMPORTANT

코드는 컨테이너에서 컴파일·실행해 확인했고, 설명은 AI(Claude)와 함께 코드 리뷰하며 정리했다.

TIP

📂 원본 소스 (GitHub)

📌 한눈에 보기

실습문제처방
10-01이미 존재하는 파일을 덮어씀 / 존재 확인 후 열기의 경쟁 상태fopen(…, "wx") 또는 open(…, O_EXCL)
10-03rewind는 성공 여부를 알 수 없음, fclose 누락fseek 반환값 검사, 모든 경로에서 fclose
10-04-a바이너리로 연 파일에 buf[strlen-1]='\0'개행 확인 후 제거, 바이너리는 fread+바이트 수
10-04-b입력이 버퍼보다 길면 마지막 글자를 잘못 지움개행 위치를 strcspn으로 찾기

1. 파일 열기 — 10-01

존재 여부를 따로 확인하지 않는다

요약

  • 문제: fopen(path, "w")가 기존 파일을 경고 없이 지움 (10-01)
  • 원인: 존재 확인 후 열기는 그 사이에 파일이 바뀌는 TOCTOU
  • 처방: fopen(path, "wx")(C11) 또는 open(O_WRONLY|O_CREAT|O_EXCL, 0644)로 확인과 생성을 원자적으로
  • 결과: 이미 있으면 errno == EEXIST로 실패

2. 읽고 나서 — 10-03

요약

  • 문제: rewind 성공 여부를 알 수 없고 fclose 누락 (10-03)
  • 원인: rewind는 반환값이 없음. fgetc 결과를 char로 받으면 EOF와 구분 불가
  • 처방: fseek(fp, 0L, SEEK_SET) != 0으로 검사, fgetc는 int로 받기, 모든 경로에서 fclose

fseek의 반환값은 errno가 아니다

요약

  • 문제: ret_val == EBADF || ret_val == EINVAL 조건이 실패를 영원히 못 잡음
  • 원인: fseek는 성공 0 / 실패 -1을 반환하고, 원인은 errno에 담김
  • 처방: if (ret_val != 0) { perror("fseek"); goto CLEAN; }
  • 결과: 파이프 입력으로 실행하면 fseek: Illegal seek가 출력됨

3. 줄 끝 처리 — 10-04

요약

  • 문제: buf[strlen(buf) - 1] = '\0'가 멀쩡한 글자를 지움 (10-04)
  • 원인: 마지막 글자가 개행·길이가 0 아님·텍스트 파일이라는 세 가지 숨은 가정
  • 처방: buf[strcspn(buf, "\n")] = '\0', 바이너리는 fread와 읽은 바이트 수
  • 결과: 개행이 없었다면 입력이 길어 잘린 것이므로 남은 입력도 처리

4. 응용 1 — 공백 제거 복사 (fio_ex)

요약

  • 문제: 공백·탭·개행을 제거해 복사하는 fio_ex의 6가지 오류
  • 원인: O_CREATE 오타, 읽기 파일에 O_EXCL, fgetc(buffer), 버퍼 상한 없음, strlen(buffer), 반환값 무시
  • 처방: 버퍼가 차면 쓰고 비우고 계속 읽기, 모은 길이 i만큼 fwrite, 모든 반환값 검사, 정리 구간 한 곳
  • 결과: 입력 크기 0·1023·1024·1025·3000바이트 모두 출력 크기 일치

5. 응용 2 — 안전하게 저장하기 (fio_ex_v2)

조건을 추가해 다시 만들었다.

  1. 인자로 src, dst를 받는다
  2. src/dst의 파일 상태를 확인하고, 디렉터리면 에러
  3. 임시 파일에 쓴 뒤 rename 으로 교체
  4. 반환값을 모두 검사해서 예외 처리
  5. 에러 처리는 goto로 한 곳에 모으기
  6. fclose 시 버퍼를 비우며 생기는 오류도 처리

5-1. 파일 상태 확인 — stat

요약

  • 문제: src/dst가 디렉터리·특수 파일·같은 파일일 수 있음
  • 원인: dst가 없는 것은 정상, 그 외 이유의 stat 실패는 오류
  • 처방: stat + S_ISDIR/S_ISREG, st_dev+st_ino가 같으면 같은 파일

5-2. 임시 파일 + rename

요약

  • 문제: 저장 중 실패하거나 전원이 나가면 반쯤 쓰인 dst가 생김
  • 원인: dst에 직접 쓰기 때문
  • 처방: dst와 같은 디렉터리에 mkstemp → 쓰기 → fflush·fsync·fclose → rename
  • 결과: 기존 dst는 그대로, 임시 파일만 삭제
flowchart TD
    A["mkstemp: dst와 같은 디렉터리에 임시 파일 생성"] --> B["fchmod: src 권한 복사"]
    B --> C["임시 파일에 모두 쓰기"]
    C --> D["fflush → fsync → fclose<br/>반환값 검사"]
    D --> E{"모두 성공?"}
    E -- "예" --> F["rename(tmp, dst)<br/>원자적 교체"]
    E -- "아니오" --> G["임시 파일 삭제<br/>기존 dst는 그대로"]

5-3. goto 정리와 fclose의 반환값

요약

  • 문제: 쓰기 오류가 fwrite가 아니라 fflush·fsync·fclose에서 드러남, fdopen 뒤 close(fd) 중복
  • 원인: 디스크가 꽉 차도 fwrite는 버퍼에만 쓰고 성공할 수 있음
  • 처방: 비우는 시점의 반환값 검사, fdopen 성공 직후 fd = -1, 열려 있는 것만 닫는 CLEAN 구간

5-4. 테스트 결과


6. 응용 3 — 줄 번호 붙여 저장 (fio_ex2)

요약

  • 문제: 줄 번호를 붙여 저장하는 fio_ex2의 10가지 오류
  • 원인: while (… == EOF) 반대 조건, char out, 문자 수를 줄 수로 셈, fseek 누락 등
  • 처방: 첫 읽기에서 줄 수·최대 길이 → fseek로 되감기 → 두 번째 읽기에서 %*d로 번호 정렬

💡 보너스 — man 페이지에서 C 함수 찾기


✅ 정리 — 파일 I/O 점검 체크리스트