CI 로그에는 서비스 프로세스가 시작된 것으로 나오지만 클라이언트에서는 connect failed, No such file or directory 오류가 반복되고, 재시도해도 문제가 해결되지 않을 수 있습니다. 권한과 프로세스 상태가 모두 정상이라면 실제 장애 원인은 작업 디렉터리에 숨어 있을 가능성이 있습니다. 도구가 여러 단계로 중첩된 저장소 경로와 작업 번호, 임시 디렉터리 아래에 Unix 소켓을 만들면서 최종 절대 경로가 macOS에서 허용하는 길이를 초과하는 경우입니다.
이 문제는 병렬 테스트 프로세스, Ruby 도구, Node.js 서비스, 빌드 보조 프로그램, 로컬 프로세스 간 통신에서 흔히 발생합니다. 오류 메시지에 “경로가 너무 김”이라는 원인이 직접 표시되지 않을 수도 있습니다. 따라서 재시도 횟수를 늘리기보다 소켓이 실제로 생성되는 위치와 해당 경로를 UTF-8로 인코딩했을 때의 바이트 수를 확인해야 합니다.
먼저 소켓 경로가 원인인지 확인하기
프로세스, 소켓 파일, 열려 있는 Unix 연결을 먼저 함께 확인합니다. 시작 스크립트의 종료 코드만 확인해서는 안 됩니다. 부모 프로세스가 성공했더라도 수신을 담당하는 자식 프로세스가 바인딩을 완료했다는 의미는 아닙니다.
pgrep -fl 'xcodebuild|swift|ruby|node'
find "${TMPDIR:-/private/tmp}" -type s -print 2>/dev/null
lsof -U -n -P | grep "$USER"
로그에 .sock 경로가 표시된다면 해당 경로의 상위 디렉터리를 직접 확인합니다. 파일이 없는 이유는 세 가지일 수 있습니다. 수신 프로세스가 아직 파일을 만들지 않았거나, 생성 후 너무 일찍 정리되었거나, 경로 검증 단계에서 bind()가 실패했을 수 있습니다. 이때 동일한 도구를 짧은 경로에서 재현해 볼 수 있습니다. 짧은 경로에서는 성공하고 원래 경로에서는 실패한다면 권한을 반복해서 조정하는 것보다 원인을 더 명확하게 설명할 수 있습니다.
macOS의 Unix 소켓 경로 제한은 화면에 보이는 문자 수가 아니라 바이트 수를 기준으로 합니다. 한글 디렉터리, 조합 문자, 긴 작업 식별자는 바이트 수를 더 빠르게 늘립니다.
절대 경로의 UTF-8 바이트 수 측정하기
${#path}만으로 결론을 내려서는 안 됩니다. 이 값은 일반적으로 문자 수를 나타냅니다. 다음 스크립트는 먼저 절대 경로로 확장한 다음 파일 시스템 인코딩을 기준으로 바이트 수를 계산합니다.
candidate="$PWD/.ci/runtime/worker/session/control.sock"
python3 - "$candidate" <<'PY'
import os
import sys
path = os.path.abspath(sys.argv[1])
encoded = os.fsencode(path)
print(f"bytes={len(encoded)}")
print(f"path={path}")
PY
Darwin의 sockaddr_un.sun_path 공간은 제한되어 있으므로 디렉터리 길이를 한계에 가깝게 설계해서는 안 됩니다. 도구가 추가하는 프로세스 ID, 임의 접미사, 하위 디렉터리를 위한 여유를 확보하도록 90바이트를 경고 기준으로 설정할 수 있습니다. 이 숫자는 내부 안전 기준이지 모든 도구에 공통으로 적용되는 공식 상한은 아닙니다. 일부 프레임워크는 전달받은 경로 뒤에 파일명을 추가로 붙이기도 합니다.
실패한 경로의 구성 요소도 함께 기록합니다. 작업 공간 루트, 저장소 이름, 파이프라인 작업 이름, 병렬 샤드 이름, 도구 자체의 실행 디렉터리를 확인해야 합니다. 끝부분의 control.sock만 줄여서는 충분한 공간을 확보하기 어렵습니다.
작업마다 짧은 루트 디렉터리 만들기
안정적인 해결 방법은 기존의 깊은 디렉터리에 심볼릭 링크를 덧씌우는 것이 아니라 작업을 짧은 루트 디렉터리 아래에서 직접 실행하는 것입니다. 작업마다 독립된 디렉터리를 사용하고 권한을 700으로 설정한 뒤 임시 파일, 캐시, 빌드 출력 경로를 해당 디렉터리로 명시적으로 지정합니다.
job_root="$(mktemp -d /private/tmp/vmown-ci.XXXXXX)"
chmod 700 "$job_root"
export TMPDIR="$job_root/tmp"
export XDG_CACHE_HOME="$job_root/cache"
export DERIVED_DATA="$job_root/dd"
mkdir -p "$TMPDIR" "$XDG_CACHE_HOME" "$DERIVED_DATA"
cleanup() {
rm -rf -- "$job_root"
}
trap cleanup EXIT HUP INT TERM
Xcode 빌드에는 DerivedData 경로를 명시적으로 전달할 수 있고, SwiftPM에는 별도의 scratch 디렉터리를 사용할 수 있습니다. 이렇게 하면 경로를 줄일 수 있을 뿐 아니라 병렬 작업이 동일한 빌드 데이터베이스를 두고 경합하는 문제도 방지할 수 있습니다.
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-derivedDataPath "$DERIVED_DATA" \
build
swift build --scratch-path "$job_root/spm"
저장소 자체도 $job_root/src와 같은 짧은 경로에 체크아웃해야 합니다. 계속해서 매우 깊은 원래 디렉터리에서 실행한다면 TMPDIR만 옮겨서는 효과가 없을 수 있습니다. 일부 도구는 프로젝트의 절대 경로를 기준으로 소켓을 생성하기 때문입니다.
심볼릭 링크와 공유 디렉터리의 함정 피하기
심볼릭 링크는 빠른 검증에는 적합하지만 유일한 해결책으로 사용해서는 안 됩니다. 일부 프로그램은 realpath를 호출해 결국 링크 뒤에 있는 긴 경로를 사용합니다. 다른 프로그램은 실제 경로를 캐시 키에 기록하기 때문에 동일한 작업에 긴 디렉터리와 짧은 디렉터리가 동시에 생길 수 있습니다.
공유 /private/tmp/ci를 사용하는 방식도 적절하지 않습니다. 병렬 작업이 같은 이름의 소켓을 만들 수 있으며, 이전 작업이 남긴 수신 파일 때문에 새 작업이 서비스를 이미 사용할 수 있다고 잘못 판단할 수도 있습니다. 더 안전한 원칙은 “작업 하나당 루트 디렉터리 하나”를 적용하고 소켓 파일명은 짧고 예측 가능하게 유지하는 것입니다.
정리할 때 광범위한 와일드카드를 사용하거나 /private/tmp 아래의 공용 접두사 전체를 삭제해서는 안 됩니다. mktemp가 반환한 디렉터리는 현재 프로세스의 변수에 저장하고 동일한 작업에서 trap을 통해 정확히 회수해야 합니다. 작업이 강제로 종료되어 정리 작업을 실행할 수 없다면 별도의 정리 작업이 디렉터리 소유자와 수정 시간을 기준으로 처리할 수 있습니다. 단, 먼저 디렉터리 안에 활성 프로세스가 없는지 확인해야 합니다.
검사를 파이프라인 사전 게이트로 만들기
한 번 수정하는 것만으로는 충분하지 않습니다. 저장소 이름, 브랜치 이름, 작업 템플릿이 길어지면 문제가 다시 발생할 수 있습니다. 테스트 서비스를 시작하기 전에 가능한 모든 소켓 경로를 검사하고 내부 경고 기준을 초과하면 즉시 실패하도록 구성하는 것이 좋습니다.
check_socket_path() {
python3 - "$1" <<'PY'
import os
import sys
limit = 90
path = os.path.abspath(sys.argv[1])
size = len(os.fsencode(path))
if size > limit:
print(f"socket path exceeds guard: {size} bytes")
raise SystemExit(1)
print(f"socket path accepted: {size} bytes")
PY
}
check_socket_path "$TMPDIR/test-worker.sock"
검증할 때는 최소한 네 가지 항목을 확인해야 합니다. 경로가 경고 기준보다 짧은지, 병렬 작업의 루트 디렉터리가 서로 다른지, 실패하거나 중단된 뒤 디렉터리가 정상적으로 정리되는지, 도구 로그에 사용된 경로가 예상과 일치하는지 확인합니다. 마지막으로 두 작업을 병렬 실행해 소켓 이름 충돌, 캐시 혼용, 다른 작업의 디렉터리를 삭제하는 문제가 없는지 확인합니다.
경로 관리의 목적은 모든 파일을 임시 디렉터리에 몰아넣는 것이 아닙니다. 프로세스 간 통신이 필요한 부분에 짧고 전용이며 수명 주기가 명확한 루트 경로를 제공하는 것입니다. 이 제약을 적용하면 무작위로 보이던 많은 연결 실패를 재현 가능하고 사전에 차단할 수 있는 구성 오류로 바꿀 수 있습니다.
자주 묻는 질문
소켓 파일 이름만 줄여도 오류가 계속되는 이유는 무엇인가요?
커널은 파일 이름이 아니라 전체 절대 경로의 UTF-8 바이트 수를 확인합니다. 저장소 경로, 작업 ID, 임시 디렉터리와 자동 생성된 하위 경로도 모두 포함됩니다.
심볼릭 링크로 긴 소켓 경로를 완전히 해결할 수 있나요?
항상 해결되지는 않습니다. 일부 도구는 링크를 실제 경로로 변환한 뒤 소켓을 만듭니다. 짧은 루트 아래에 작업 공간을 직접 생성하는 편이 안전합니다.
병렬 CI 작업이 하나의 짧은 임시 디렉터리를 공유해도 되나요?
공유하지 않는 것이 좋습니다. 작업마다 mktemp로 전용 디렉터리를 만들고 권한을 700으로 제한한 뒤 정상 종료와 중단 시 모두 삭제해야 합니다.
개발, 빌드 및 원격 데스크톱을 위한 독점 물리 머신을 선택하세요
Apple Silicon 구성 3가지를 비교하고, 주문 시 노드, 대여 기간 및 스토리지 추가 옵션을 선택할 수 있습니다.