엔지니어링 노트설치·운영과 품질

구축형 제품의 기능은 설정 플래그가 아니라 버전으로 켠다

구축형 제품의 설정 파일에 기능을 켜고 끄는 불리언 칸을 두면, 그 값은 고객사 서버마다 따로 정해집니다. 기본값으로만 테스트한 버전이 어느 고객사에서만 실패하고, 개발팀은 그 고객사의 설정을 받아 오기 전까지 재현하지 못합니다. 업그레이드한 설치에서는 새 키를 적지 않았다는 이유만으로 새 기능이 오류 없이 꺼진 채 뜹니다.

OwlAI는 기능을 켜는 일을 버전에 맡깁니다. 채택한 기능은 코드에 넣어 그 버전을 설치하면 켜져 있고, 설정 파일에는 보존 기간·타임아웃 같은 수치와 모델 사양만 둡니다. 설치마다 달라야 하는 것은 관리 화면에서 고치는 데이터로 두고, 구축형과 OwlAI SaaS의 차이는 첫 기동 때 고정되는 에디션 값 하나로 가릅니다. 이 글은 그렇게 나눈 이유와, 그 나눔을 테스트로 지키는 방법을 설명합니다.

플래그 값이 고객사 서버에 있으면 조합마다 다른 제품이 된다

피처 플래그(feature flag)의 장점은 분명합니다. 기능을 꺼 둔 채 배포하고, 일부 사용자에게만 10%, 50%, 100% 순으로 넓혀 가며 켜고, 문제가 생기면 다시 배포하지 않고 스위치만 끕니다. 이 방식은 플래그 값을 운영팀이 한곳에서 쥘 때 성립합니다. SaaS라면 플래그가 열 개여도 지금 운영 환경에 켜진 조합은 하나이고, 운영팀은 그 조합을 알고 테스트합니다.

구축형에서는 플래그 값이 고객사 서버의 설정 파일에 있습니다. 고객사 A는 기본값 그대로 쓰고, B는 두 개를 끄고, C는 몇 버전 전 문서를 보고 하나를 켭니다. 불리언 칸이 n개면 현장에 깔릴 수 있는 제품은 2ⁿ가지이고, 열 개면 1,024가지입니다.

플래그 수에 따라 2ⁿ으로 늘어나는 제품 조합과 테스트하는 하나
그림 1. 플래그 수에 따라 2ⁿ으로 늘어나는 제품 조합과 테스트하는 하나

개발팀이 테스트하는 것은 기본값 조합 하나입니다. 플래그 A가 꺼져 있다고 보고 짠 기능 B는 A를 켠 고객사에서만 실패합니다. 그 결함은 고객사에서 처음 드러나고, 받아 온 설정 파일이 실제로 돌던 설정과 같은지 확인할 방법도 없습니다.

꺼진 갈래는 코드에도 남습니다. 플레이북 엔진을 켜고 끄는 플래그 하나만 해도 꺼진 갈래를 위해 엔드포인트 28곳의 가드, 서비스와 워커 곳곳의 조기 반환, 플래그를 읽는 코드 세 벌, 꺼진 엔진만 시험하는 테스트 26건이 필요합니다. 기본값이 켜짐인 플래그라면 이 코드는 거의 모든 설치가 한 번도 타지 않는 갈래를 위한 것입니다. 그래서 플레이북 엔진은 늘 켠 채로 출시합니다.

업그레이드한 설치에서는 새 기능이 꺼진 채 뜬다

조합 수보다 찾기 어려운 문제는 기능이 아무 신호 없이 꺼지는 경우입니다. 새 기능을 기본값 꺼짐 플래그 뒤에 두었다고 합시다. 새 버전의 설정 예시 파일에는 새 키가 있지만, 고객사가 고쳐 쓴 설정 파일에는 그 키가 저절로 들어가지 않습니다. 고객사는 키를 모르니 적지 않고, 기능은 오류도 경고도 없이 꺼져 있습니다. 릴리스 노트에서 본 기능이 왜 없느냐는 문의가 오고, 지원 담당자는 설정 파일을 열어 본 뒤에야 원인을 압니다.

「값이 있으면 켠다」로 가르는 기능도 같은 함정을 만듭니다. 메일 발송을 「.env에 발신 좌표가 있으면 보낸다」로 정하면, 좌표 한 줄을 빠뜨린 설치는 오류 없이 메일 대신 화면에 링크를 보여 주는 방식으로 뜹니다. 그래서 메일 발송 여부는 에디션이 정하고, 메일을 보내는 에디션은 기동할 때 발신 좌표와 공개 주소를 확인해 없으면 기동하지 않습니다.

설정 키 철자도 같은 결과를 냅니다. 설정 모델은 모르는 키를 버립니다. 몇 버전 전 설정 파일로도 새 버전이 뜨게 하려는 선택인데, 키를 틀리게 적으면 그 칸은 기본값으로 돌고 기동 검증도 통과합니다. 그래서 키를 버리는 동작은 그대로 두고, 모델에 없는 키는 기동할 때 키 경로와 함께 config.unknown_key 경고 로그로 남깁니다. 철자가 틀린 키와 더는 읽지 않는 옛 키가 모두 여기에 찍힙니다.

채택한 기능은 코드에 넣고, 설정 파일에는 수치만 둔다

규칙은 하나입니다. 채택한 기능은 코드에 넣고, 기능이 있는지는 버전이 정합니다. 그 버전을 설치하면 그 기능이 켜져 있습니다. 기능을 시험하는 일은 개발·테스트 환경에서 하고, 준비를 마친 기능만 머지합니다.

설정 파일(config.yaml)에 남는 칸은 운영 수치입니다. 다음은 그 예입니다.

  • playbook.runRetentionMaxPerPlaybook: 플레이북마다 남길 실행 기록 수(기본 100, 0이면 지우지 않음)
  • observability.accessLogRetentionDays: 접속기록 감사의 보존 일수(기본 1,095일, 이보다 짧으면 기동 거부)
  • timeouts.dbQuerySeconds: 조회 하나의 시간 예산(기본 600초)
  • deployJudge.regressionFailureLimit: 배포 뒤 이 수만큼 회귀로 실패한 턴이 나오면 배포 실패로 본다(「배포가 성공했는지는 헬스 체크가 아니라 배포 뒤 실패한 턴으로 판정한다」)

수치는 조합으로 늘어나지 않습니다. 실행 기록을 100개 남기든 1,000개 남기든 코드 경로는 같고 지우는 기준만 다릅니다. 그래서 테스트는 0처럼 동작이 바뀌는 경계와 하한을 봅니다.

모델 칸에 남는 불리언은 모델이 이미지 입력을 받는지(imageInput) 하나입니다. 이 값은 운영자가 켜고 끄는 스위치가 아니라, 서빙하는 가중치가 정하는 사양입니다. 같은 모델을 빠른 처리 등급(speed: fast)으로도 쓰고 싶으면 켜고 끄는 칸 대신 모델 행을 하나 더 적습니다. 두 행은 모델 관리 화면에 따로 보이고, fast를 지원하지 않는 모델에 그 값을 적으면 기동이 거부됩니다.

설치마다 다른 것은 데이터로, 에디션은 첫 기동에 고정한다

설치마다 정말 달라야 하는 것은 데이터로 둡니다. 스위치는 「이 동작을 할지」를 정하고, 데이터는 「무엇에 대해 할지」를 정합니다. 커넥터 도구 가운데 어떤 것을 막고 어떤 것에 승인을 걸지는 고객사마다 붙이는 서버가 달라서, 관리자가 설치별 도구 정책으로 정합니다(「커넥터 도구의 위험은 고객사가 안다, 도구 정책은 설치마다 둔다」). 이 값은 관리 화면에서 고치고 DB에 저장되며, 정책이 없는 도구는 코드에 정한 기본 규칙을 따릅니다.

구축형과 OwlAI SaaS의 차이는 에디션 하나로 가릅니다. .env의 OWLAI_EDITION=onprem|saas를 기동할 때 읽고, 첫 기동 때 그 값을 DB에 적습니다. 이후 기동은 적어 둔 값과 대조해 다르면 기동을 거부합니다. 에디션은 서명한 라이선스에도 들어 있어서, .env만 고쳐서는 다른 에디션으로 뜨지 않습니다.

다른 모듈은 에디션 프로필이 정한 능력만 읽습니다. 능력은 「조직 여러 개 만들기」, 「메일 발송」, 「관리 화면에서 업데이트」처럼 이름이 붙은 동작입니다. 화면도 서버가 내려 주는 능력 목록을 보고 메뉴를 엽니다. 갈림길이 에디션 두 개로 끝나므로 테스트 환경도 구축형과 OwlAI SaaS 두 곳이면 됩니다.

인프라 좌표, 비밀값, 서비스 간 인증 토큰, DB 접속 문자열은 설정 파일 대신 compose와 .env에 둡니다. 설정을 읽는 코드보다 먼저 기동하는 구성 요소가 읽어야 하는 값이기 때문입니다. 이 값들은 동작을 가르는 스위치가 아니라 설치의 주소입니다.

버전·설정 파일·데이터·.env 로 나눈 차이를 두는 네 자리
그림 2. 버전·설정 파일·데이터·.env 로 나눈 차이를 두는 네 자리

새 불리언 칸이 생기면 테스트가 실패한다

이 규칙은 리뷰어의 기억에 맡기면 무너집니다. 그래서 테스트 두 개가 지킵니다.

첫째는 설정 모델의 불리언 칸 검사입니다. 테스트가 설정 모델을 중첩 모델·목록·dict 값까지 돌며 불리언 칸을 모두 뽑고, 사유가 달린 허용 목록과 집합이 같은지 봅니다. 지금 목록에는 imageInput 하나만 있습니다. 새 불리언 칸이 생기면 목록에 없어서 실패하고, 칸을 지웠는데 목록에 남아 있어도 실패합니다. 부분집합이 아니라 집합 동일성으로 비교하는 이유가 이것입니다.

둘째는 에디션 사용처 검사입니다. 제품 코드에서 에디션 값을 아는 곳은 에디션 모듈 하나여야 하고, 선언한 능력과 코드가 실제로 쓰는 능력이 같아야 합니다. 어느 모듈이 "saas"를 직접 비교하면 테스트가 실패합니다.

설계에서 고른 것

  • 킬 스위치 대신 수정 버전과 되돌리기를 골랐습니다. 킬 스위치가 있으면 문제가 난 기능을 다시 배포하지 않고 바로 끌 수 있습니다. 그런데 그 스위치 자체가 테스트하지 않은 갈래이고, 한 번 켜고 끈 값은 고객사 설정 파일에 남습니다. 그래서 문제가 생긴 기능은 수정 버전을 내거나 이전 버전으로 되돌려 대응하고, 되돌리기 절차(「업데이트가 실패하면 스키마 변경에 따라 코드만 되돌리거나 덤프로 복원한다」)와 배포 판정(「배포가 성공했는지는 헬스 체크가 아니라 배포 뒤 실패한 턴으로 판정한다」)을 엄격하게 두었습니다.
  • 점진 출시 대신 출시 전 테스트를 골랐습니다. 일부 고객사에 먼저 켜고 반응을 보며 넓히는 방식은 고객사 현장을 실험군으로 나눕니다. 새 코드는 합쳐질 때마다 구축형·OwlAI SaaS 테스트 환경에 자동으로 배포되고, 그 뒤에 버전에 실립니다. 대가로 출시가 느리고, 고객사 데이터에서만 드러나는 문제는 고객사에서 처음 봅니다.
  • 모르는 설정 키는 기동 거부 대신 경고를 골랐습니다. 거부하면 철자 오류를 바로 잡지만, 옛 키가 남은 설정 파일로는 새 버전이 뜨지 않습니다. 업그레이드가 설정 파일 정리를 기다리지 않게 하려고 경고 로그를 택했고, 그만큼 로그를 보는 일은 운영자의 몫으로 남습니다. 쓸 수 없는 값(보존 하한보다 짧은 감사 기록 보존 일수, fast를 지원하지 않는 모델의 fast 등급)은 기동을 거부합니다.
  • 에디션은 불리언이 아니라 두 값 가운데 하나로 두고 설치 수명 동안 고정했습니다. 구축형 데이터(조직 하나, 관리자가 직접 만든 계정)는 OwlAI SaaS의 전제와 맞지 않아서, 값 하나를 바꿔 다른 에디션으로 뜨는 길을 막았습니다.

고객사가 설정 파일을 직접 고치는 제품이라면, 불리언 칸 하나를 더하기 전에 그 값을 누가 쥐게 되는지부터 따져 볼 만합니다. 운영팀이 한곳에서 쥐면 플래그는 출시 도구로 남습니다. 고객사 서버마다 흩어지면 칸 하나가 테스트하지 않은 제품의 수를 두 배로 늘립니다.