검색/멀티 서비스 앱에 백엔드 코드와 알맞은 테스트를 추가하기

멀티 서비스 앱에 백엔드 코드와 알맞은 테스트를 추가하기

백그라운드 워커가 있고 여러 서비스로 나뉜 앱을 AI 에이전트가 수정할 때 사용한다. 환경과 실행 중인 서비스를 확인하고, 데이터베이스 코드와 태스크 코드를 정해진 곳에 두고, 작업과 오류를 프로젝트 방식대로 정의하고, 알맞은 테스트 한 종류를 고르고, 코딩 전에 계획을 세운다.

이 AI 규칙을 사용하려면
## 작업 전에 환경을 준비하고 서비스가 실행 중인지 확인하기 - 백엔드 의존성은 저장소 루트에 있는, 락파일로 관리되는 가상 환경 안에 있다. 아직 없다면 고정된 락파일에서 생성하고, 무엇이든 실행하기 전에 활성화한다. - 테스트에 필요한 자격 증명과 키는 저장소의 로컬 환경 파일에 있다. 값을 임의로 만들어내거나 새로 요청하지 말고 그 파일에서 읽는다. - 모든 애플리케이션 서비스가 이미 실행 중이라고 가정하되, 믿지 말고 검증한다. 로그 디렉터리를 확인해 해당 서비스가 여전히 로그를 남기고 있는지 확인한다. - 데이터베이스에는 직접 임의로 만든 연결이 아니라, 프로젝트에 문서화된 클라이언트 호출 방식을 사용해 실행 중인 컨테이너를 통해 접근한다. - 브라우저 자동화로 웹 애플리케이션을 조작할 때는 엔드투엔드 설정이 생성하는 계정으로 로그인한다. 그 계정이 없으면 가입 페이지에서 등록한다. 가장 먼저 등록된 계정이 관리자가 되기 때문이다. ## 데이터베이스 쿼리는 한 계층에 두고 테스트 호출은 프런트엔드를 거쳐 보내기 - 모든 데이터베이스 작업은 지정된 데이터 접근 디렉터리 안에 둔다. 그 외의 어떤 곳에서도 쿼리를 실행하지 않는다. 그래야 스키마 지식이 한 계층에 머물고 한 곳에서 리뷰할 수 있다. - 수동 테스트나 스크립트 테스트에서 백엔드를 호출할 때는 백엔드 포트로 직접 가지 말고 항상 프런트엔드 오리진을 거친다. 프런트엔드를 건너뛴 요청은 실제 트래픽이 통과하는 프록시·세션·인증 동작까지 함께 건너뛰므로, 그 결과는 대표성이 없다. ## 새 작업을 알맞은 백그라운드 워커 풀에 넣기 비동기 작업은 하나의 범용 큐가 아니라 특화된 여러 워커 풀로 나뉘어 있다. 새 작업은 그 성격에 맞는 프로필의 풀에 배치한다. - 기본(primary) 풀은 시스템 전체에 걸친 작업을 조율한다. 리소스 삭제, 동기화, 정리(pruning), 주기적 점검이 여기에 해당한다. - 페치(fetch) 풀은 외부 소스에서 데이터를 가져오고 배치마다 처리 태스크를 생성하며, 워치독 모니터링으로 멈춘 소스를 감지한다. - 처리(processing) 풀은 인제스션 파이프라인을 실행한다. 레코드 저장, 청크 분할, 보강, 임베딩, 검색 인덱스 기록, 메타데이터 갱신이 여기에 해당한다. - 라이트 풀은 짧은 작업을 높은 동시성으로 처리한다. 헤비 풀은 리소스를 많이 쓰는 작업을 낮은 동시성으로 처리한다. - 모니터링 풀은 모니터링에 병렬성이 필요하지 않으므로 단일 스레드이며, 큐 깊이·메모리·헬스 지표를 수집한다. - 스케줄러 풀은 초 단위로 측정되는 인터벌 점검부터 시간 단위 정리까지 주기적 트리거를 담당하고, 예약된 각 태스크의 테넌트를 결정한다. 여러 풀에 공통되는 속성은 다음과 같다. 안정성을 위해 프로세스가 아닌 스레드 풀을 쓰는 것, 테넌트별 격리, 우선순위 큐, 하트비트와 생존 확인, 자동 재시도와 실패 복구, 프로세스 간 조율을 위한 빠른 공유 저장소, 그리고 지속적인 태스크 상태를 담는 관계형 데이터베이스. ## 모든 백그라운드 태스크에 만료 시간과 자체 타임아웃 넣기 - 태스크는 구체적인 애플리케이션 인스턴스에 묶는 대신 프레임워크의 애플리케이션 비의존 데코레이터로 선언한다. 그래야 태스크를 임포트할 때 애플리케이션 생성이 함께 딸려 오지 않는다. - 태스크 모듈은 해당 에디션이나 티어에 지정된 태스크 디렉터리 아래에 둔다. - 만료 시간 없이 태스크를 큐에 넣는 일은 절대 하지 않는다. 스케줄에서 보내든 다른 태스크에서 보내든 반드시 만료 시간을 지정한다. 만료 시간 없이 큐에 넣는 코드를 제출하는 것은 허용되지 않는다. 큐가 한없이 커질 수 있기 때문이다. - 태스크는 스레드 풀에서 실행되므로 프레임워크 자체의 시간 제한 기능은 조용히 비활성화된다. 타임아웃 로직은 태스크 안에서 직접 구현한다. - 워커는 코드가 바뀌어도 다시 로드하지 않는다. 워커 코드를 변경한 뒤에는 테스트 전에 관리자에게 워커 재시작을 요청하고, 새 코드가 반영되어 있다고 가정하지 않는다. ## 엔드포인트를 추가하고 프로젝트 고유의 오류 타입으로 오류 던지기 - 엔드포인트를 추가할 때는 별도의 응답 모델 필드를 선언하는 대신 핸들러 함수에 직접 타입을 지정한다. - 프레임워크의 범용 HTTP 예외 대신 프로젝트 고유의 오류 타입을 발생시키고, 상태 코드를 하드코딩하거나 프레임워크에서 상태 상수를 임포트하는 일은 절대 하지 않는다. - 전역 핸들러가 프로젝트 오류를, 오류 코드와 상세 정보를 담은 하나의 표준 응답 본문으로 변환한다. 이로써 보일러플레이트가 사라지고 백엔드 전반의 오류 처리가 일관되게 유지된다. - 오류 코드는 중앙 레지스트리에서 가져온다. 새 분류가 필요하면 먼저 레지스트리에 추가한다. 호출 지점에서 코드를 임의로 만들어내지 않는다. - 상태가 업스트림 응답에서 오는 업스트림 실패를 전달할 때는, 오류 타입을 포기하지 말고 오류 타입의 상태 재정의 인자로 그 상태를 그대로 넘긴다. ## 엄격한 타입을 지키고 오래 쓸모 있는 주석만 쓰기 - 변경을 제안하기 전에 저장소의 pre-commit 훅을 설치하고 모든 파일에 대해 실행한다. - 백엔드와 프런트엔드 양쪽 모두 엄격하게 타입을 유지한다. 타입을 생략하는 우회 수단은 용납할 만한 지름길이 아니다. - 주석은 짧게 유지하고 장기적으로 유효한 정보로만 제한한다. 방금 무엇을 추가했는지, 삭제했는지, 리팩터링했는지처럼 진행 중인 변경을 설명하는 주석은 쓰지 않는다. 그런 주석은 변경이 반영되는 순간 잡음이 된다. ## 데이터베이스 마이그레이션을 직접 쓰고 프라이빗 스키마에도 적용하기 - 리비전 파일은 마이그레이션 도구로 생성한 뒤, 생성된 파일 안에 마이그레이션 본문을 직접 손으로 작성한다. 의도를 표현하는 데 자동 생성에 의존하지 않는다. - 멀티테넌트 배포에서는 기본 설정에 더해 프라이빗 스키마 설정에 대해서도 마이그레이션을 적용해야 하며, 리비전을 만들 때도 같은 구분이 적용된다. - 마이그레이션은 나중에 따로 적용하는 뒷일이 아니라, 리뷰 대상인 변경의 일부로 다룬다. ## 모킹을 줄이고 변경을 증명하는 테스트 한 종류 고르기 네 가지 수준이 있다. 과도한 모킹보다 충실도가 높은 수준을 우선하고, 그 동작을 실제로 증명해 주는 것을 고른다. - 단위 테스트는 어떤 서비스에도 접근할 수 없다고 가정하고 외부 세계와의 모든 상호작용을 모킹한다. 로직 자체를 못 박아 둘 가치가 있는 복잡하고 고립된 모듈에 대해서만 작성한다. - 의존성 기반 단위 테스트는 데이터베이스, 캐시, 오브젝트 스토리지, 검색 인덱스, 외부 네트워크 같은 외부 의존성은 실행 중이고 애플리케이션 컨테이너는 실행 중이 아니라고 가정한다. 함수를 직접 호출하며 일부 컴포넌트는 여전히 모킹할 수 있다. 모킹을 최소화하면서도 불안정한 동작을 제어하거나 내부 호출이 특정 인자로 이루어졌는지 검증하는 능력을 유지하고 싶을 때 사용한다. 순수한 통합 테스트로는 그것이 불가능하다. - 통합 테스트는 실제 배포 환경에서 실행되며 아무것도 모킹하지 않는다. 이것을 우선하고, 모킹이나 내부 검증이 정말로 필요할 때만 의존성 기반 단위 테스트로 물러선다. - 브라우저 기반 테스트는 전체 배포 위에 웹 서버를 더한다. 프런트엔드와 백엔드 사이의 상당한 협업이 필요한 동작에 사용한다. 과도하게 테스트하지 않는다. 하나의 변경에는 보통 네 가지 전부가 아니라 한 종류의 테스트만 필요하다. ## 통합 테스트를 쓸 때 기존 픽스처와 헬퍼를 다시 쓰기 - 통합 테스트를 작성하기 전에, 사용할 수 있는 픽스처를 알기 위해 루트 테스트 설정을 읽고, 헬퍼를 알기 위해 공용 유틸리티 디렉터리를 읽는다. - 원시 HTTP 라이브러리로 엔드포인트를 직접 호출하기보다 그 유틸리티에 있는 매니저 헬퍼를 우선하고, 헬퍼를 직접 호출하기보다 픽스처를 우선한다. 관리자 픽스처가 있는데도 관리자를 수동으로 만드는 것은 설정을 중복시키고 나머지 테스트 스위트와 어긋나게 만든다. - 테스트는 디렉터리 단위로 병렬 실행되므로, 한 디렉터리 안의 파일들은 공유된 가변 상태에 의존해서는 안 된다. - 테스트가 실제 모델을 호출할 때는 플래그십 모델이 아니라 각 제공자의 저렴하고 빠른 티어를 사용한다. 테스트 비용은 스위트 규모에 비례해 늘어나고, 검증하려는 대상은 모델의 성능에 좌우되지 않는다. - 새로운 구조를 만들어내기보다 같은 범주에 있는 모범적인 기존 테스트의 형태를 그대로 따라 쓴다. ## 실패 원인을 짐작하기 전에 서비스별 로그 읽기 - 통합 테스트를 작성하거나 실행 중인 시스템을 직접 조작할 때는 로그 디렉터리에 있는 서비스별 디버그 로그 파일을 읽는다. 모든 서비스가 그곳으로 출력을 흘려보낸다. - 실패에 대한 가설을 세우기 전에 먼저 로그를 읽는다. 서비스가 실행되고 있지 않다는 것 같은 무관한 원인은, 로그를 읽기 전까지는 진짜 결함과 똑같아 보인다. ## API 키를 저장소에 넣지 않고 새 엔드포인트 보호하기 - API 키나 비밀 값을 저장소에 커밋하는 일은 절대 하지 않는다. - 연동용 자격 증명은 평문 설정이 아니라 반드시 암호화된 자격 증명 저장소를 통해 보관한다. - 기능을 추가할 때는 확립된 역할 기반 접근 제어 패턴을 따른다. 새 엔드포인트는 기본적으로 아무런 보호도 물려받지 않는다. - 입력은 경계에서 스키마 모델로 검증한다. - 구조적으로 인젝션이 불가능하도록 파라미터화된 쿼리를 사용하고, 문자열 연결로 쿼리를 조립하는 일은 절대 하지 않는다. ## 모든 AI 모델 호출에 등록된 작업 이름 태그를 붙여 추적하기 언어 모델, 임베딩 모델, 리랭커, 이미지 생성기, 음성 컴포넌트, 인텐트 분류기에 대한 모든 호출은 중앙 플로우 레지스트리의 값으로 태그된 생성(generation) 스팬을 열어야 한다. 호출 경로에 맞는 래퍼를 사용한다. 하나는 모델 추상화를 거치는 호출용이고, 다른 하나는 그것을 우회하는 직접 제공자 호출이나 HTTP 호출용이다. - 새 작업을 계측하기 전에 레지스트리 값을 먼저 추가한다. 원시 문자열을 태그로 넘기는 일은 절대 하지 않는다. - 태그는 제공자가 아니라 리랭킹이나 이미지 편집처럼 작업의 이름을 나타낸다. 제공자는 모델 설정 메타데이터에 들어간다. - 자동 폴백은 명시적인 스팬 없이 추상화에 도달한 호출에 대해 태그 없는 센티널 값을 내보낸다. 그 센티널 값은 대시보드에서 보이며 계측이 빠져 있다는 뜻이다. 폴백에 기대지 말고 호출 지점을 고친다. ## 코드를 먼저 살펴본 뒤 일정과 코드 없이 구현 계획 쓰기 작성하기 전에 조사한다. 코드베이스의 관련 부분을 먼저 살펴본다. 추측으로 쓴 계획은 계획이 없느니만 못하다. 계획에는 최소한 다음 요소가 들어간다. - 다룰 이슈: 이 변경이 무엇을 하려는 것인지. - 중요 참고 사항: 조사 중에 발견한 것 중 구현에 영향을 주는 사항. - 구현 전략: 변경을 실현하기 위한 큰 틀의 접근 방식. - 테스트: 올바른 동작을 검증할 테스트를 의도적으로 골라 최소한으로 유지한 것. 일정 섹션은 넣지 않고, 롤백 절차에 대한 서술도 넣지 않는다. 계획의 일부로 코드를 작성하지 않는다. 큰 틀에서 유지하되, 특정 파일이나 함수를 언급하는 것은 유용하다. 이것은 최소 목록이므로 도움이 된다면 더 추가한다. ## 하위 프로젝트에서 일하기 전에 그곳 전용 규칙 읽기 - 루트 지침은 공통 관심사를 다룬다. 각 하위 프로젝트는 자기 코드 옆에 고유한 표준 문서를 두며, 그 문서가 자기 트리를 관장한다. - 해당 영역에서 작업하기 전에 그 범위의 문서를 읽는다. 한 클라이언트의 규칙이 다른 클라이언트에도 적용된다고 가정하지 않는다. 문서 객체 모델이 없고, 간격 값이 상대적인 단계 척도가 아니라 절대값인 스타일링 프리미티브를 쓰며, 자체 라우팅과 뷰 프리미티브를 가진 모바일 클라이언트는 웹 클라이언트와 다른 규칙을 따른다. - 더 넓은 엔지니어링 관행은 저장소의 기여 가이드에 있다. 이 문서에 더해 그 가이드도 읽고 따른다.