하나의 서비스를 여러 고객사가 각자 쓰도록 바꾸는 멀티 테넌트 SaaS 전환 프로젝트에서, 테넌트에 줄 앱을 관리자 화면의 토글로 켜고 끄는 기능을 만들었다. 기능 전체는 앱 목록을 정하는 자리를 한 계층 위로 옮겼다에 있고, 이건 그중 한 줄짜리 분기 이야기다.
운영자가 앱을 전부 끄고 저장하면 어떻게 되어야 할까. 처음 짠 스크립트는 켜진 앱이 0개면 아무것도 하지 않고 끝났다. 할 일이 없으니 끝내는 게 맞다고 생각했다.
켜진 앱이 없으면 그냥 끝내던 스크립트
앱 목록은 앱그룹 레코드의 docker_infos 배열에 들어 있고, 앱마다 토글 상태 is_use와 템플릿 폴더 이름 template_dir이 붙는다. 파이프라인은 이 값을 DOCKER_INFOS 환경변수로 받는데, 원본이 있는 DynamoDB가 아니라 저장할 때 함께 갱신되는 Secrets Manager 사본에서 읽는다. 전파 때 도는 app-update.sh의 첫 버전은 이랬다.
# ❌ 처음 버전 (app-update.sh)
ACTIVE_TEMPLATE_DIRS=$(echo "$DOCKER_INFOS" | jq -r '.[] | select(.is_use == true) | .template_dir' 2>/dev/null || echo "")
if [ -z "$ACTIVE_TEMPLATE_DIRS" ]; then
echo "활성 앱이 없습니다. 종료."
exit 0
fi
새 테넌트를 만드는 onboarding-v6.sh도 설정이 없을 때, 빈 배열일 때, 전부 꺼져 있을 때를 모두 “전체 앱 유지”로 처리했다. 두 스크립트 모두 0개를 “건드리지 말라”로 읽었다. 하지만 운영자가 화면에서 토글을 전부 내리고 저장했다면 그건 전부 내리라는 지시다. 이대로면 운영자는 앱을 다 껐는데 테넌트에는 앱이 그대로 떠 있게 된다.
null은 “정한 적 없음”, 빈 배열은 “아무것도 안 쓰기로 함”
그렇다고 0개를 무조건 “전부 내림”으로 바꿀 수도 없었다. 이 토글 기능이 생기기 전에 만든 앱그룹은 앱을 고른 적이 없다. 거기서 테넌트를 만들 때 앱을 전부 지워 버리면 기존 동작이 깨진다. 설정 자체가 없으면(null) 기존 방식대로 전체 앱을 유지하고, 빈 배열([])이거나 전부 꺼져 있으면 실제로 전부 내려야 했다.
이 둘은 jq를 거치기 전에 갈라야 한다. 시크릿에 docker_infos 키가 없으면 빌드 스펙의 jq -r '.docker_infos'는 null이라는 네 글자 문자열을 돌려준다. 그런데 .[] | select(.is_use == true)를 거치고 나면 null도, []도, 전부 꺼진 배열도 똑같이 빈 문자열이 된다. 그래서 원래 문자열부터 확인하도록 조건을 뒤집었다.
# ✓ 고친 버전 (onboarding-v6.sh)
# - DOCKER_INFOS 미설정/null → legacy fallback (전체 앱 유지, 하위 호환)
# - DOCKER_INFOS = "[]" 또는 활성 앱 0개 → 사용자 의도(모든 앱 제외)로 처리: 전체 앱 디렉토리 제거
# - DOCKER_INFOS에 활성 앱 있음 → 해당 앱만 유지
if [ -z "${DOCKER_INFOS:-}" ] || [ "$DOCKER_INFOS" = "null" ]; then
echo "DOCKER_INFOS 미설정. 전체 앱 대상으로 진행 (기존 동작/하위 호환)."
else
ACTIVE_APPS=$(echo "$DOCKER_INFOS" | jq -r '.[] | select(.is_use == true) | .template_dir // empty' 2>/dev/null || echo "")
# 활성 앱 목록에 없는 디렉토리 제거 (ACTIVE_APPS가 비어있으면 전체 제거됨)
for APP_DIR in "${APPS_DIR}"/*/; do
[ -d "$APP_DIR" ] || continue
APP_NAME=$(basename "$APP_DIR")
if [ -z "$ACTIVE_APPS" ] || ! echo "$ACTIVE_APPS" | grep -qx "$APP_NAME"; then
echo "비활성 앱 제거: $APP_NAME"
rm -rf "$APP_DIR"
fi
done
fi
app-update.sh는 exit 0을 지우고, 0개면 지금 배포된 앱을 전부 제거 대상으로 넘기게 했다. 이쪽에는 null 분기가 없다. 전파는 화면에서 저장을 거친 앱그룹에서만 출발하고, 저장할 때 docker_infos는 늘 배열로 들어가기 때문이다.
0개가 진짜 0개가 되자 작은 뒷일이 따라왔다. 앱을 하나도 안 켠 채로 만든 테넌트는 saas-tenants 레포에 apps/ 디렉터리가 아예 없었다. Git은 빈 디렉터리를 추적하지 않는다. app-update.sh는 apps/가 없으면 “테넌트 apps 디렉토리가 없습니다”를 찍고 exit 1로 멈추게 되어 있었으니, 그런 테넌트에 나중에 앱을 켜면 전파가 실패한다. 테넌트 디렉터리만 확인하고 apps/는 mkdir -p로 만들어 두도록 고쳤다. 온보딩 쪽에서 앱 목록을 만들던 ls도 디렉터리가 비어 있어도 안전하게 돌도록 find로 바꿨다. 한 줄짜리 분기를 고쳤는데 그 뒤로 이런 정리가 두어 개 더 붙었다.