본문 바로가기
Code/PHP

PHP ftp_put 고정장 전문 오프셋 깨짐 해결 — 범인은 FTP_ASCII 모드였다

by CoLife 2026. 4. 22.

PHP ftp_put 고정장 전문 오프셋 깨짐 완전 해결
범인은 FTP_ASCII 모드였다

PHP FTP 전송 · 고정장(Fixed-Length) 전문 · CRLF 트러블슈팅

개발을 하다 보면 "범인이 왜 거기서 나와?" 싶은 황당한 트러블슈팅을 만날 때가 있습니다. 특히 레거시 시스템과 연동하거나, 바이트 단위의 칼 같은 정확성을 요구하는 고정장(Fixed-Length) 전문을 다룰 때면 더욱 그렇죠. 이번 글에서는 리눅스 서버에서 윈도우 서버로 고정장 전문 파일을 FTP 전송할 때 발생한 치명적인 오류와, 그 허탈하지만 확실한 해결 방법을 공유합니다.

01 발단 — 전문 오프셋이 뒤로 갈수록 밀린다

리눅스 서버에서 생성한 픽스드 랭스(Fixed-Length) 전문 텍스트 파일을 윈도우 서버로 FTP 전송하는 연동 작업 중이었습니다. 전송 후 타겟 서버에서 파일을 읽어 들이는데, 문자열이 뒤로 갈수록 밀리면서 규격이 완전히 깨져버리는 현상이 발생했습니다.

🚨 증상 요약

고정장 전문 파일의 각 필드 오프셋(시작 위치)이 파일 내 줄이 늘어날수록 점점 더 어긋나기 시작. 10번째 레코드쯤부터는 필드 값이 완전히 다른 곳에서 읽힘. 수신 측 파서(Parser)가 뻗어버림.

02 1차 의심 — 항상 의심받는 인코딩 문제

이런 레거시 연동에서 바이트 수가 안 맞으면 십중팔구 EUC-KR과 UTF-8 간의 한글 바이트 수 차이(EUC-KR: 2바이트, UTF-8: 3바이트)가 원인이라고 생각하기 쉽습니다. 저 역시 그 부분을 먼저 파고들었죠.

  • 파일 생성 시 인코딩 확인 → EUC-KR로 통일
  • mb_strlen으로 바이트 길이 재검증
  • 수신 측 파서 인코딩 설정 재확인

하지만 아무리 인코딩을 맞춰도 길이는 계속 어긋났습니다. 그리고 결정적인 단서가 보였는데, 오프셋이 레코드 수에 비례해서 딱 1바이트씩 밀린다는 점이었습니다. 그 순간, "이건 인코딩 문제가 아니라 줄바꿈 문자 문제다"라는 직감이 들었습니다.

03 진짜 원인 — ASCII 모드의 과잉친절

이 문제는 두 가지 시스템적 특징이 만나면서 발생했습니다.

① OS별 줄바꿈 문자의 차이

OS 줄바꿈 문자 바이트 수
Linux / macOS \n (LF) 1 byte
Windows \r\n (CRLF) 2 bytes

② FTP_ASCII 모드의 자동 변환

PHP 스크립트에서 FTP 전송 모드가 FTP_ASCII로 설정되어 있었습니다. ASCII(텍스트) 모드는 이기종 OS 간에 텍스트 파일이 올바르게 보이도록 줄바꿈 문자를 자동으로 변환해 주는 기능을 합니다.

"아, 리눅스에서 윈도우로 가는 텍스트 파일이네? \n\r\n으로 바꿔줘야지!" — FTP 데몬의 독백

만약 파일 생성 단계에서 수신 측(윈도우) 규격에 맞추기 위해 이미 \r\n으로 끝을 맺어 두었다면, 전송 과정에서 \r\r\n이 되어버립니다. 윈도우는 이를 두 번의 줄바꿈으로 해석하고, 레코드마다 1바이트씩 누적되어 오프셋을 통째로 꼬아버리게 됩니다.

💡 레코드 100개 기준 예시

100개 레코드 × 1바이트(중복 \r) = 마지막 레코드의 오프셋이 100바이트 밀려 있음. 고정장 전문 규격상 치명적인 파싱 오류 발생.

04 해결 방법 — FTP_BINARY 모드로 전환

고정장 전문은 텍스트 형태를 띠고 있더라도, 시스템 관점에서는 단 1바이트의 오차도 허용되지 않는 바이너리 데이터처럼 취급해야 합니다. 해결책은 파일 전송 함수의 모드를 1:1 원본 전송을 보장하는 FTP_BINARY로 변경하는 것입니다.

ftp_transfer.php

// ❌ 기존 코드 (문제 발생: ASCII 모드의 자동 줄바꿈 변환 개입)
ftp_put($ftp_conn, $remote_file, $local_file, FTP_ASCII);

// ✅ 수정된 코드 (해결: 원본 바이트 그대로, 1:1 전송)
ftp_put($ftp_conn, $remote_file, $local_file, FTP_BINARY);

✅ 결과

파라미터 하나 바꿨을 뿐인데, 줄바꿈 증식 현상이 깔끔하게 사라지고 모든 레코드의 오프셋이 정확하게 맞아떨어졌습니다. 수신 측 파서도 에러 없이 정상 동작.

05 노하우 — ASCII vs Binary, 언제 뭘 써야 하나?

20년간 FTP 연동을 수도 없이 해오면서 정리된 기준입니다. 이 기준 하나만 기억해두면 됩니다.

🔤 ASCII 모드 (FTP_ASCII)

  • PHP, Shell 등 소스 코드 파일
  • INI, YAML 등 설정 파일
  • 사람이 직접 읽는 일반 텍스트
  • 이기종 OS 간 텍스트 호환성이 중요할 때

📦 Binary 모드 (FTP_BINARY)

  • 고정장(Fixed-Length) 전문 파일
  • 이미지, 동영상, 압축 파일
  • 실행 파일, DB 덤프
  • 바이트 무결성이 생명인 모든 데이터

💡 CoLife의 판단 기준

전송하는 데이터가 "사람을 위한 텍스트"인가, "기계를 위한 바이트 덩어리"인가? 전자면 ASCII, 후자면 항상 Binary. 헷갈리면 Binary가 안전합니다. ASCII 모드는 도와주려다 오히려 데이터를 망칩니다.

06 마무리

늘 인코딩 문제만 의심하다가 전송 프로토콜의 특성 때문에 전문 길이가 달라질 수 있다는 사실은 꽤나 신선한 충격이었습니다. 결국 FTP_ASCIIFTP_BINARY로 바꾸는 단 한 줄 수정으로 며칠간의 삽질이 끝났습니다.

시스템 간 연동 작업에서는 내가 다루는 데이터의 성격을 먼저 명확히 정의하는 것이 기본 중의 기본입니다. PHP로 FTP 연동을 개발할 때는 고정장 전문이나 바이너리 데이터에 반드시 FTP_BINARY 모드를 사용하세요. 이 글이 비슷한 삽질을 하고 있는 분께 조금이라도 도움이 되길 바랍니다.


👨‍💻

CoLife

20년 차 현직 개발자 · code & life 운영자

레거시부터 최신 기술까지, 현장의 삽질을 솔직하게 공유합니다.

💬

같은 문제로 삽질하셨나요?

비슷한 경험이 있으시거나, 더 좋은 해결 방법을 알고 계신다면 댓글로 공유해 주세요!
FTP 연동, 레거시 트러블슈팅 관련 궁금한 점도 편하게 질문 주시면 아는 한에서 답해드리겠습니다. 😊

👍 도움이 되셨다면 공감 버튼 한 번 눌러주시면 블로그 운영에 큰 힘이 됩니다!