클라우드 Mac CI에서 Address Sanitizer로 메모리 오류 차단하기

클라우드 Mac CI에서 Address Sanitizer로 메모리 오류 차단하기

Swift, Objective-C, C/C++ 의존성이 혼재된 iOS 프로젝트는 일반 테스트를 계속 통과하더라도 실행 빈도가 낮은 특정 경로에서 배열 범위 초과, 이중 해제, 해제 후 사용 문제가 발생할 수 있습니다. 작업을 클라우드 Mac으로 이전한 뒤 기존 테스트만 반복해서 실행해서는 안 됩니다. Address Sanitizer(ASan) 파이프라인을 추가해 메모리 접근 오류가 발생하면 즉시 빌드를 중단하고, 재현 가능한 결과 번들을 보존하는 편이 더 효과적입니다.

먼저 게이트에서 탐지할 항목 정하기

ASan은 주로 스택 및 힙 범위 초과, 해제 후 사용, 이중 해제, 일부 잘못된 포인터 접근을 탐지합니다. 특히 Objective-C 브리징 계층, 이미지 처리 라이브러리, 데이터베이스 래퍼, 직접 작성한 C 인터페이스를 검사하는 데 적합합니다. 순수 Swift 코드도 도움을 받을 수 있지만, 한 번 통과했다고 해서 메모리 문제가 없다는 뜻은 아닙니다. 테스트가 실행하지 않은 분기는 검사되지 않으며, 메모리 누수와 데이터 경합은 다른 도구로 보완해야 합니다.

먼저 안정적이고 수동 조작이 필요 없는 테스트 세트를 게이트 진입점으로 선택하는 것이 좋습니다. 처음부터 모든 UI 테스트를 포함하지 마세요. 핵심 파서, 캐시 계층, 언어 간 인터페이스를 최우선으로 검사해야 합니다.

테스트 계층 실행 빈도 실패 처리
핵심 단위 테스트 모든 병합 요청 병합 차단
통합 테스트 기본 브랜치 또는 병합 요청 릴리스 차단
전체 UI 테스트 정기 실행 또는 릴리스 후보 단계 결함 등록 후 재현

Sanitizer의 가치는 작업을 실패 상태로 만드는 데 그치지 않습니다. 동일한 입력으로 동일한 오류를 다시 일으킬 수 있어야 하며, 호출 스택과 테스트 이름, 빌드 환경도 함께 남겨야 합니다.

독립된 Scheme과 디렉터리 만들기

Xcode에서 기존 테스트 Scheme을 복제해 App-ASan으로 이름을 지정하고 Shared로 설정합니다. 개발자가 일상적으로 사용하는 Scheme을 직접 수정하면 로컬 디버깅, 성능 측정, CI 매개변수가 서로 영향을 줄 수 있습니다. Scheme은 다음 경로에 커밋해야 합니다.

App.xcodeproj/xcshareddata/xcschemes/App-ASan.xcscheme

각 작업에서는 독립된 DerivedData를 사용합니다. 이렇게 하면 일반 빌드에서 생성된 오브젝트 파일이 ASan 빌드에 잘못 사용되는 것을 방지하고, 병렬 작업 두 개가 인덱스와 중간 산출물을 두고 경합하는 문제도 피할 수 있습니다. 결과 번들도 고정된 파일을 반복해서 덮어쓰지 말고 작업 번호별 디렉터리에 저장합니다.

테스트 대상 고정하기

먼저 현재 사용할 수 있는 시뮬레이터를 확인합니다.

xcrun simctl list devices available

CI에서는 기기 유형과 운영체제의 주 버전을 고정하되, 설치된 런타임에 따라 패치 버전이 업데이트되는 것은 허용하는 편이 좋습니다. 여러 운영체제 버전을 반드시 검증해야 한다면 매트릭스 작업으로 분리하고, 하나의 명령에서 자동 선택에 의존하지 마세요.

xcodebuild로 검사 실행하기

다음 스크립트는 독립된 빌드 디렉터리와 결과 번들을 사용합니다. 워크스페이스, Scheme, 시뮬레이터 이름은 실제 프로젝트 값으로 변경해야 합니다.

#!/bin/zsh
set -u

ROOT="${PWD}"
OUT="${ROOT}/artifacts/asan"
DERIVED="${ROOT}/.ci/DerivedData-ASan"

rm -rf "${OUT}" "${DERIVED}"
mkdir -p "${OUT}"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App-ASan \
  -configuration Debug \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -derivedDataPath "${DERIVED}" \
  -resultBundlePath "${OUT}/App-ASan.xcresult" \
  -enableAddressSanitizer YES \
  CODE_SIGNING_ALLOWED=NO \
  >"${OUT}/xcodebuild.log" 2>&1

status=$?
exit ${status}

테스트 명령에 무조건 성공하도록 만드는 || true를 추가하지 마세요. CI 플랫폼에서 로그 후처리가 필요하다면 먼저 종료 코드를 저장하고, 아카이브를 완료한 뒤 해당 코드를 그대로 반환해야 합니다. CODE_SIGNING_ALLOWED=NO는 시뮬레이터 테스트에 적합합니다. 실제 기기나 아카이브가 포함된 작업에는 별도의 서명 구성을 사용해야 하며, 이 설정을 그대로 적용해서는 안 됩니다.

ASan을 사용하면 메모리 사용량과 실행 시간이 증가하므로 동일한 머신에서의 동시 실행 수를 일반 단위 테스트보다 낮게 설정해야 합니다. 프로세스가 시스템에 의해 종료되면 먼저 동시 실행 수를 줄인 다음, 테스트 대상 애플리케이션의 결함인지 판단합니다.

실패 증거를 인계 가능한 형태로 보존하기

작업이 실패하면 최소한 xcodebuild.log.xcresult를 보존해야 합니다. 전자는 Sanitizer 보고서를 검색하기 쉽고, 후자는 테스트 계층 구조, 실패 첨부 파일, 활동 로그를 포함합니다. 아카이브하기 전에 다음과 같이 무결성을 확인할 수 있습니다.

test -s artifacts/asan/xcodebuild.log
test -d artifacts/asan/App-ASan.xcresult
xcrun xcresulttool get test-results summary \
  --path artifacts/asan/App-ASan.xcresult \
  > artifacts/asan/summary.json

Xcode 버전에 따라 xcresulttool 하위 명령이 달라질 수 있으므로 도구 호출 방식도 Xcode 버전과 함께 관리해야 합니다. 명령이 호환되지 않더라도 원본 결과 번들을 삭제해서는 안 됩니다.

보고서를 조사할 때는 먼저 ERROR: AddressSanitizer를 찾은 뒤 오류 유형, 첫 번째 애플리케이션 코드 스택 프레임, 오류를 일으킨 테스트와 입력 데이터를 기록합니다. 스택 상단의 시스템 라이브러리 프레임은 대개 충돌이 발생한 위치만 나타냅니다. 실제로 메모리를 손상시킨 동작은 그보다 앞서 발생했을 수 있습니다. 오류가 안정적으로 재현되지 않으면 해당 테스트만 반복 실행하고 무작위 테스트 순서를 비활성화해 여러 변수가 동시에 바뀌지 않도록 합니다.

계층별로 실행하고 흔한 실수 방지하기

일상적인 게이트에서는 위험도가 높은 테스트 세트만 실행하고, 기본 브랜치에서 전체 ASan 테스트 모음을 실행할 수 있습니다. 파일 이름을 조합해 명령을 만드는 방식보다 테스트 태그나 Test Plan을 사용하는 편이 유지보수하기 쉽습니다. 새로운 C 인터페이스, 포인터 연산, 안전하지 않은 버퍼 처리를 추가할 때는 관련 테스트도 빠른 테스트 세트에 함께 포함해야 합니다.

흔한 실수는 세 가지입니다. 첫째, 일반 빌드 캐시를 재사용해 계측이 불완전해지는 경우입니다. 둘째, 텍스트 로그만 업로드하고 결과 번들을 저장하지 않는 경우입니다. 셋째, ASan 실패를 간헐적 테스트 실패로 간주해 곧바로 재시도하는 경우입니다. 재시도로 재현 안정성을 확인할 수는 있지만 최초 보고서는 반드시 보존해야 하며, 연속 재시도가 통과하더라도 이미 탐지된 잘못된 메모리 접근을 자동으로 승인해서는 안 됩니다.

마지막으로 승인 조건을 명확하게 정의합니다. 테스트 프로세스의 종료 코드가 0이고, 결과 번들이 존재하며, 로그에 ASan 오류가 없어야 하고, 작업이 시간 초과나 리소스 부족으로 중단되지 않아야 합니다. 그래야 게이트가 성공처럼 보이는 한 줄짜리 테스트 요약이 아니라 전체 실행 결과를 기준으로 판단할 수 있습니다.

자주 묻는 질문

Address Sanitizer를 커밋마다 실행해야 하나요?

핵심 단위 테스트와 위험도가 높은 모듈은 변경마다 실행하는 편이 좋습니다. 긴 UI 테스트는 병합 요청, 기본 브랜치 또는 예약 작업으로 분리할 수 있습니다.

검사가 통과하면 메모리 문제가 없다는 뜻인가요?

아닙니다. 해당 실행에서 통과한 경로와 일부 메모리 오류만 검사합니다. 누수 분석, 데이터 경쟁 검사와 실제 기기 검증도 별도로 필요합니다.

독점 물리 Mac mini

작업 기간에 맞는 클라우드 Mac 이용 기간 선택

두 가지 M4 구성을 일·주·월·분기 단위로 이용할 수 있으며, 노드와 실제 사용 가능 여부는 콘솔에서 실시간으로 확인되는 정보를 기준으로 합니다.

구성 선택 및 주문