남이 바꾼 문자열 하나가 우리 파이프라인을 조용히 깨뜨립니다

가장 설명하기 어려운 장애가 있습니다. 우리 코드는 한 줄도 바뀌지 않았고, 배포도 없었고, 인프라 변경도 없었는데 어느 날 아침부터 회원가입이 실패합니다. 로그를 뒤져 보면 이메일 형식 검증에서 걸러지고 있습니다. 검증 로직은 3년 전에 작성된 것이고 그동안 아무 문제가 없었습니다. 바뀐 것은 우리가 아니라 상대편입니다.

이런 종류의 결함에 대응하는 방식은 셋 정도입니다. 첫째, 터진 뒤에 고칩니다. 대부분의 팀이 여기 있습니다. 둘째, 외부 서비스의 상태 페이지를 구독합니다. 도움은 되지만 이런 변경은 상태 페이지에 안 올라옵니다. 셋째, 우리가 하드코딩한 외부 상수의 목록을 만들고 그 목록에 대해 감시와 테스트를 붙입니다.

결론을 먼저 말씀드리면 셋째만이 실제로 작동합니다. 그리고 8월 24일 하루에 나온 공지 두 건이 이 작업을 지금 시작할 이유를 만들어 줍니다. 하나는 로그인 이메일 도메인이 바뀐다는 예고이고, 다른 하나는 CI 명령어 플래그가 3주 뒤에 사라진다는 통보입니다. 둘 다 우리 코드를 건드리지 않고 우리를 깨뜨릴 수 있는 변경입니다.

1. 8월 24일에 나온 두 건

1-1. Sign in with Apple 이메일 도메인 변경

Apple Developer News에 2026-08-24 올라온 "Update: New domain for Sign in with Apple"의 문구를 원문 그대로 옮깁니다.

"Starting later this year, new Sign in with Apple addresses, previously issued on privaterelay.appleid.com, will be issued on private.icloud.com."

기존 주소는 계속 동작합니다. "Existing addresses on privaterelay.appleid.com will continue to work and forward mail to users without interruption."

그리고 개발자에게 직접 지시하는 문장이 있습니다.

"Developers with apps or websites that use Sign in with Apple should ensure that their account systems, email validation logic, and allowlists accept addresses on the new private.icloud.com domain in addition to the existing privaterelay.appleid.com domain."

한 가지 흥미로운 대목은 iCloud+ Hide My Email 쪽 방향을 되돌렸다는 부분입니다. "After further consideration and reviewing community feedback, iCloud+ Hide My Email addresses will remain on icloud.com." 즉 이 공지 자체가 이전 공지의 정정입니다. 외부 정책은 한 번 확인하고 끝나는 것이 아니라는 실례입니다.

1-2. CircleCI Smarter Testing CLI 플래그 개명

같은 날 CircleCI 공식 changelog에 올라온 공지에는 마감 기한이 박혀 있습니다.

"We've consolidated the Smarter Testing CLI experience under the CircleCI CLI. Please apply these changes by September 14, 2026."

변경 내역을 원문 표기 그대로 정리하면 이렇습니다.

구분 이전 이후
설치 brew uninstall circleci/tap/circleci-testsuite circleci extension install testsuite
실행 명령 circleci run testsuite circleci testsuite run
테스트 선별 플래그 --test-selection / --select-tests --run-tests
분석 플래그 --test-analysis --analyze-tests

원문의 설명은 이렇습니다. "Test impact analysis flags have been renamed for consistency: --test-selection / --select-tests--run-tests", "--test-analysis--analyze-tests".

커버리지 플러그인도 공식 레지스트리로 정리되었습니다. npm 쪽은 @circleci/jest-circleci-coverage, @circleci/cypress-circleci-coverage, @circleci/mocha-circleci-coverage, @circleci/vitest-circleci-coverage, @circleci/v8-coverage-collector이고, PyPI는 pytest-circleci-coverage, RubyGems는 rspec-circleci-coverage입니다.

1-3. 세 번째 사례 — 기본값이 켜지는 쪽

같은 날 CodeRabbit changelog에는 방향이 반대인 변경이 올라왔습니다. 없어지는 게 아니라 없던 게 켜지는 쪽입니다.

"CodeRabbit now runs Vale on changed .md, .markdown, and .txt files... Vale is enabled by default when your repository has a supported root configuration."

끄는 방법도 명시되어 있습니다. "Teams can disable it with reviews.tools.vale.enabled: false in .coderabbit.yaml."

세 건의 성격이 다릅니다. 이 차이가 대응 방식을 결정합니다.

사례 변경 유형 우리에게 오는 방식 감지 시점
Apple 도메인 새 값이 추가됨 신규 사용자만 실패 몇 달 뒤, 산발적
CircleCI 플래그 기존 값이 제거됨 파이프라인 전체 실패 마감일 직후, 즉시
CodeRabbit Vale 기본값이 켜짐 리뷰 노이즈 급증 다음 문서 PR

2. 이 세 유형이 왜 각각 다르게 위험한가

2-1. 추가형 — 가장 늦게 발견되고 가장 오래 방치됩니다

Apple 도메인 케이스가 여기 속합니다. 기존 값이 계속 동작하기 때문에 기존 사용자는 아무 문제가 없습니다. 새 도메인으로 가입하는 신규 사용자만 실패합니다.

이게 왜 최악인지는 지표를 보면 알 수 있습니다. 전체 로그인 성공률은 거의 떨어지지 않습니다. 신규 가입 전환율만 조금 떨어집니다. 그리고 신규 가입 전환율은 마케팅 캠페인, 앱스토어 노출, 시즌성 같은 변수에 원래 많이 흔들립니다. 그래서 몇 주 동안 "이번 달 전환율이 좀 낮네" 정도로 넘어갑니다.

제 해석으로는 추가형 변경의 진짜 비용은 장애 시간이 아니라 원인 규명에 드는 시간입니다. 지표가 완만하게 나빠지면 누구도 배포 이력을 뒤지지 않습니다.

2-2. 제거형 — 가장 시끄럽고 가장 다루기 쉽습니다

CircleCI 플래그 개명이 여기 속합니다. 9월 14일 이후 --select-tests를 쓰는 파이프라인은 그냥 실패합니다. 전원이 즉시 알게 됩니다.

역설적으로 이 유형이 가장 안전합니다. 마감일이 공개되어 있고, 대응 방법이 문서에 있고, 실패가 명확합니다. 위험은 하나뿐입니다. 공지를 못 보는 것. 그리고 그 위험은 실재합니다. CI 설정 파일을 마지막으로 만진 사람이 퇴사했거나, 벤더 changelog를 아무도 구독하지 않는 팀에서 이런 일이 벌어집니다.

2-3. 기본값 변경형 — 신뢰를 깎아먹습니다

CodeRabbit Vale이 여기 속합니다. 파이프라인이 깨지지는 않습니다. 대신 문서 PR을 올렸을 때 갑자기 산문 스타일 지적이 스무 개 붙습니다.

기술적 피해는 0입니다. 그런데 조직적 피해가 있습니다. AI 리뷰 도구에 대한 팀의 신뢰가 여기서 깎입니다. "얘 원래 안 그랬는데 왜 갑자기 시끄러워졌지"라는 경험이 두세 번 쌓이면 리뷰 코멘트를 읽지 않게 됩니다. 도구를 껐다 켜는 것보다 이 신뢰 회복이 더 비쌉니다.

3. 감시 목록을 만드는 방법

3-1. 먼저 우리가 하드코딩한 것을 찾습니다

감시할 대상을 정하려면 우리 코드베이스에 박혀 있는 외부 상수를 먼저 알아야 합니다. 완벽하지 않아도 됩니다. 첫 목록은 grep 한 번으로 만듭니다.

#!/usr/bin/env bash
# 목적: 코드베이스에 하드코딩된 외부 도메인·상수 후보를 뽑습니다.
# 완벽한 탐지가 목표가 아니라 "감시 목록의 초안"이 목표입니다.
set -euo pipefail

ROOT="${1:-.}"
EXCL='--glob=!node_modules --glob=!.git --glob=!dist --glob=!*.lock --glob=!vendor'

echo "=== 1. 외부 도메인 리터럴 (상위 40개) ==="
rg -oIN $EXCL \
  '[a-z0-9][a-z0-9.-]*\.(com|io|net|org|dev|app|cloud|co\.kr)' "$ROOT" \
  | sort | uniq -c | sort -rn | head -40

echo
echo "=== 2. 이메일 검증 정규식이 있는 위치 ==="
rg -nI $EXCL -e '@[a-z0-9.-]+\\?\.' -e 'EMAIL_REGEX' -e 'isValidEmail' "$ROOT" | head -30

echo
echo "=== 3. allowlist / whitelist 로 보이는 배열 선언 ==="
rg -niI $EXCL -e 'allow_?list' -e 'white_?list' -e 'ALLOWED_DOMAINS' "$ROOT" | head -30

echo
echo "=== 4. CI 설정에서 쓰는 외부 CLI 플래그 ==="
rg -nI --glob='.github/workflows/*' --glob='.circleci/*' --glob='*.gitlab-ci.yml' \
  -e '--[a-z][a-z-]{3,}' "$ROOT" | head -40

1번과 4번 결과가 특히 중요합니다. 1번은 추가형 변경에 노출된 지점이고, 4번은 제거형 변경에 노출된 지점입니다. 저희는 이걸 돌려 보고 이메일 도메인 리터럴이 검증 로직과 테스트 픽스처 두 곳에 각각 박혀 있는 걸 발견했습니다. 검증 로직만 고치면 테스트가 실패하고, 테스트만 고치면 운영이 실패하는 구조였습니다.

3-2. 감시 목록 형식

찾아낸 것을 파일 하나로 관리합니다. 형식이 단순해야 유지됩니다.

# team-docs/external-constants.yml
# 목적: 우리 코드가 의존하는 "남이 정한 값"의 목록입니다.
# 규칙: 항목을 추가할 때 반드시 공지 채널(watch)을 함께 적습니다.
#       watch 가 비어 있는 항목은 감시되지 않는 항목입니다.

- name: Sign in with Apple 이메일 도메인
  values:
    - privaterelay.appleid.com   # 기존, 계속 유효
    - private.icloud.com         # 2026-08-24 공지, 올해 후반 발급 시작
  used_in:
    - src/auth/email-validator.ts
    - test/fixtures/apple-users.json
  watch: https://developer.apple.com/news/
  type: 추가형
  last_checked: 2026-08-25

- name: CircleCI Smarter Testing CLI 플래그
  values:
    - "--run-tests"       # 신규
    - "--analyze-tests"   # 신규
  deprecated:
    - "--select-tests"    # 2026-09-14 마감
    - "--test-selection"
    - "--test-analysis"
  used_in:
    - .circleci/config.yml
  watch: https://circleci.com/changelog/
  type: 제거형
  deadline: 2026-09-14
  last_checked: 2026-08-25

- name: CodeRabbit 기본 활성 도구
  values:
    - "reviews.tools.vale.enabled"   # 2026-08-24부터 기본 true
  used_in:
    - .coderabbit.yaml
  watch: https://docs.coderabbit.ai/changelog
  type: 기본값변경형
  last_checked: 2026-08-25

watch 필드를 필수로 만든 것이 핵심입니다. 목록은 쉽게 만들 수 있지만 갱신은 안 됩니다. 공지 채널이 항목마다 붙어 있어야 분기 점검이 가능합니다.

3-3. 추가형에 대한 테스트

추가형 변경은 테스트로 막을 수 있습니다. 핵심은 검증 로직을 도메인 목록과 분리해서, 목록을 데이터로 주입받게 만드는 것입니다.

# 목적: 이메일 검증이 "특정 도메인 하드코딩"에 의존하지 않는지 확인합니다.
# 이 테스트는 도메인이 추가될 때 프로덕션 코드 수정 없이 통과해야 합니다.
import pytest
from src.auth.email_validator import is_acceptable_email

# 감시 목록(external-constants.yml)과 같은 값을 씁니다.
APPLE_RELAY_DOMAINS = [
    "privaterelay.appleid.com",  # 기존
    "private.icloud.com",        # 2026-08-24 공지분
]

@pytest.mark.parametrize("domain", APPLE_RELAY_DOMAINS)
def test_apple_relay_주소를_거부하지_않는다(domain):
    # 실패하면: 도메인 allowlist 에 하드코딩이 남아 있다는 신호입니다.
    assert is_acceptable_email(f"abc123def@{domain}") is True

def test_모르는_relay_도메인도_형식만_맞으면_통과한다():
    # 목적: 미래에 또 도메인이 추가될 때 조용히 깨지지 않도록 합니다.
    # allowlist 방식이라면 이 테스트가 실패합니다. 그것이 이 테스트의 존재 이유입니다.
    assert is_acceptable_email("abc123def@some-future-relay.icloud.com") is True

def test_검증_로직이_형식_위반은_여전히_거른다():
    # 위 두 테스트 때문에 검증이 무력화되지 않았는지 확인합니다.
    for bad in ["nope", "no@", "@no.com", "a b@example.com"]:
        assert is_acceptable_email(bad) is False

두 번째 테스트가 의도적으로 도발적입니다. allowlist 방식을 쓰고 있다면 이 테스트는 실패합니다. 그때 결정해야 합니다. 정말 allowlist가 필요한 요구사항인지, 아니면 편의상 그렇게 짠 것인지. 후자라면 지금 고치는 게 나중에 고치는 것보다 훨씬 쌉니다.

4. 함정 셋

4-1. 첫 번째 함정: 목록만 만들고 감시 채널을 안 붙인다

증상은 6개월 뒤 external-constants.ymllast_checked가 전부 반년 전 날짜로 남아 있는 것입니다. 원인은 목록 작성이 일회성 작업으로 끝난 것입니다. 대응은 항목마다 watch URL을 필수로 만들고, 분기 1회 점검을 캘린더에 고정 일정으로 넣는 것입니다. 점검 자체는 30분입니다.

4-2. 두 번째 함정: 테스트 픽스처만 고치고 검증 로직을 안 고친다

증상이 아주 얄궂습니다. CI는 초록색인데 운영에서만 실패합니다. 원인은 새 도메인을 테스트 픽스처에 추가하면서 실제 검증 로직의 allowlist는 그대로 둔 것입니다. 테스트가 검증 로직을 우회하는 경로로 통과하는 경우도 있습니다.

대응은 위 코드의 두 번째 테스트처럼 목록에 없는 값을 일부러 넣어 보는 케이스를 두는 것입니다. 픽스처 추가로는 통과할 수 없는 테스트가 하나 있어야 합니다.

4-3. 가장 비싼 함정: 마감이 있는 변경을 마감 당일에 처리한다

증상은 9월 14일 오후에 모든 파이프라인이 실패하고, 그 상태에서 플래그를 고치는데 개명된 플래그가 이전과 정확히 같게 동작하는지 확인할 시간이 없는 것입니다.

원인은 마감 기한을 알림으로만 처리한 것입니다. --select-tests--run-tests로 바뀌는 것은 이름만 바뀌는 것처럼 보이지만, 테스트 선별 도구는 어느 테스트를 실행하지 않을지를 결정합니다. 잘못 적용되면 실행되지 않은 테스트가 생기고, 파이프라인은 초록색입니다. 제거형 변경이 조용히 추가형 문제로 바뀌는 지점입니다.

가장 비싼 함정이라고 부르는 이유가 여기 있습니다. 시끄러운 실패로 시작했는데 서둘러 고치는 과정에서 조용한 실패로 바뀝니다. 대응은 마감 2주 전에 브랜치를 하나 파서, 이전 플래그와 새 플래그로 각각 실행했을 때 선별되는 테스트 목록이 동일한지 비교하는 것입니다.

함정 증상 원인 대응
감시 채널 미부착 last_checked가 반년 전 일회성 작업으로 종료 watch 필수화 + 분기 점검 일정 고정
픽스처만 수정 CI 초록, 운영 실패 검증 로직 allowlist 잔존 목록에 없는 값으로 실패해야 하는 테스트 배치
마감 당일 처리 급하게 고치다 조용한 실패 유발 마감을 알림으로만 관리 2주 전 브랜치에서 선별 결과 diff 비교

5. 도입 평가

QA 4~6명, 자동화 초기 단계 기준으로 평가하면 이렇습니다.

작업 도입 난이도 유지보수 비용 팀 학습곡선 판단
grep 스크립트로 초안 목록 뽑기 낮음 (2시간) 없음 (일회성) 낮음 즉시
external-constants.yml 작성 낮음 (반나절) 낮음 (분기 30분) 낮음 즉시
CircleCI 플래그 마이그레이션 낮음 (2시간) 없음 낮음 9월 1일까지
플래그 전후 선별 결과 diff 검증 중간 (1일) 없음 (일회성) 중간 9월 1일까지
Apple 도메인 대응 테스트 추가 낮음 (2시간) 낮음 낮음 즉시
allowlist → 형식 검증 리팩터링 중간~높음 낮음 중간 요구사항 확인 후
벤더 changelog 구독 체계 낮음 (2시간) 중간 (읽는 사람 필요) 낮음 즉시

마지막 줄의 유지보수 비용을 중간으로 잡은 것은 구독 설정이 어려워서가 아니라 읽는 사람이 필요하기 때문입니다. 담당자를 정하지 않은 구독은 필터링된 메일함으로 끝납니다. 저는 주간 회의 첫 5분을 여기에 쓰는 방식을 씁니다.

6. 권할 만한 경우 / 권하지 않는 경우

권할 만한 경우

  • Sign in with Apple, 소셜 로그인, 외부 결제 중 하나라도 쓰는 서비스 — Apple 공지는 지금 확인하십시오
  • CircleCI의 test impact analysis를 쓰는 팀 — 9월 14일 마감이 실재합니다
  • "우리 코드는 안 바뀌었는데 장애가 났다"를 최근 6개월 안에 경험한 팀
  • AI 리뷰 도구를 도입했고 기본값을 확인한 적이 없는 팀 — .coderabbit.yaml 한 번 열어 보십시오
  • 외부 API 연동이 다섯 개 이상인 서비스

권하지 않는 경우

  • 외부 의존성이 사실상 없는 내부 도구 — 목록이 세 줄이면 파일을 만들 이유가 없습니다
  • 이번 주에 릴리스가 있는 경우 — allowlist 리팩터링은 릴리스 직후에 하십시오. 인증 경로 변경은 릴리스 직전에 손댈 영역이 아닙니다
  • 목록 작성을 담당할 사람을 정할 수 없는 상태 — 주인 없는 목록은 만들지 않는 것이 낫습니다
  • 이미 계약 기반 테스트(contract test)가 갖춰진 조직 — 이 글의 내용은 그 안에 포함됩니다

오늘 30분 안에 끝낼 수 있는 것

[ 30분 안에 ]
□ .circleci/config.yml 을 열어 --select-tests / --test-selection / --test-analysis 검색
   → 하나라도 있으면 9월 14일 마감 대상. 캘린더에 9월 1일로 등록
□ 이메일 검증 로직에서 privaterelay.appleid.com 검색
   → 있으면 private.icloud.com 을 함께 허용하는지 확인
□ .coderabbit.yaml 존재 여부 확인
   → 없으면 Vale 이 기본 활성. 문서 PR 리뷰 노이즈 증가를 팀에 미리 공지

[ 이번 주 안에 ]
□ 3-1의 grep 스크립트를 돌려 외부 도메인 리터럴 상위 40개를 확보
□ 그중 인증·결제·알림 경로에 걸린 것만 골라 external-constants.yml 초안 작성
□ 항목마다 watch URL 을 채운다. 채울 수 없는 항목은 목록에서 뺀다
□ 분기 점검 일정을 캘린더에 반복 일정으로 등록 (30분)

[ 9월 1일까지 ]
□ CircleCI 플래그 마이그레이션 브랜치 생성
□ 이전 플래그와 새 플래그로 각각 실행해 선별된 테스트 목록을 파일로 저장
□ 두 파일을 diff 하고 차이가 0인지 확인. 차이가 있으면 머지하지 않는다

마지막 줄이 제일 중요합니다. 플래그 개명은 이름만 바뀌는 변경처럼 보이지만, 확인 없이 넘기면 실행되지 않는 테스트가 생기고 그 사실은 아무 신호도 만들지 않습니다.

댓글

이 블로그의 인기 게시물

옵시디언 3기기 동기화, 유료 결제 없이 구글 드라이브로 끝내기

CI가 30분이면 AI 도구를 붙여도 소용없습니다

반값 모두의카드가 끝나도 환급 감소는 수도권 기준 월 3만 2천원이 최대입니다