검색/두 런타임에 걸친 플러그인 기반 데이터 파이프라인을 망가뜨리지 않고 고치기

두 런타임에 걸친 플러그인 기반 데이터 파이프라인을 망가뜨리지 않고 고치기

두 런타임에 걸친 플러그인 기반 백엔드에서 코딩 에이전트에게 코드를 고치게 할 때 쓴다. 버전을 한 파일에서 관리하고, 테스트 필터를 올바른 태스크에 걸고, 플러그인·동시성·서비스 레이어 규칙을 지키며, 새 테스트가 해당 변경 없이는 실패하는지 확인한다.

이 AI 규칙을 사용하려면
## 버전은 한 파일에서 관리하고 항상 같은 루트에서 빌드하기 버전과 툴체인 좌표를 정본으로 유지하고, 항상 동일한 위치에서 빌드한다. - 모든 버전 번호와 툴체인 좌표는 하나의 정본 파일에 저장한다. 빌드 스크립트, 헬퍼, 테스트, 패키지 디스크립터 등 이를 사용하는 모든 대상은 이 파일을 참조하며, 수정 역시 이 파일 하나만 하면 된다. - 런타임 호환성 경로 세그먼트처럼 값이 여러 곳에 의도적으로 하드코딩되거나 중복되어 있다면, 이를 버그로 취급하거나 동적으로 계산하려 하지 말고 정본 위치의 현재 값을 찾아 그대로 맞춘다. - 모든 빌드·테스트 명령은 항상 동일한 루트 경로에서 실행해, 상대 경로 연결이 매번 동일하게 해석되도록 한다. - 전역으로 설치된 언어나 버전 관리자보다는, 부트스트랩 단계에서 제공되는 벤더링된(고정 버전) 툴체인을 우선 사용해 모든 환경이 동일한 방식으로 빌드되게 한다. ## 테스트 필터는 실제로 그 테스트를 가진 태스크에 걸기 필터링된 일부 테스트를 실행하기 전에, 테스트 태스크들이 어떻게 연결되어 있는지부터 이해한다. - 상위(umbrella) 테스트 태스크는 클래스 자체를 전부 제외하고 하위 태스크에 위임만 하는 경우가 많다. 그래서 이 상위 태스크에 이름 필터를 적용하면 아무것도 매칭되지 않은 채 조용히 통과한 것처럼 보인다. 필터는 실제로 테스트를 소유하는 구체적인 태스크에 적용해야 한다. - 테스트 스위트 분리는 의도적이다. 예를 들어 두 번째 런타임을 부팅하는 테스트는, 그 런타임을 제공하는 스위트 쪽으로 옮겨져 있는 등 원래 예상되는 스위트에서 빠져 있는 경우가 있다. 필터로 아무것도 찾지 못했다면 대상은 다른 스위트에 있을 가능성이 높다. - 단위 테스트, 통합 테스트, 인수 테스트가 각각 어디에 있는지 파악해두어야 올바른 진입점을 호출할 수 있다. ## 새 케이스도 처음부터 계획한 것처럼 코드를 다시 짜기 코드를 수정할 때는 기존 코드에 단순히 덧붙이지 않는다. 자신의 변경 이후에도 주변 코드가 여전히 말이 되는지 평가한다. 어떤 추상화가 하나의 케이스만을 염두에 두고 설계되어 있는데 여기에 두 번째 케이스를 추가하는 상황이라면, 마치 처음부터 두 케이스 모두를 의도했던 것처럼 읽히도록 구조를 다시 잡는다. 서로 다른 개념에는 각각 명확한 이름을 가진 독립된 단위를 부여하고, 그 단위들을 상위 레벨 코드에서 조합해 각 구현을 일일이 읽지 않아도 의도가 분명히 드러나게 한다. ## 프레임워크의 베이스 클래스와 레지스트리 위에서 플러그인 만들기 확장 기능은 프레임워크가 제공하는 확장 지점을 우회하지 말고 그 위에 구축한다. - 해당 타입 전용 베이스 클래스를 상속하고, setup/register 훅과 그 타입의 진입점을 오버라이드한다. - 설정은 원시 파라미터를 직접 읽지 말고 프레임워크의 선언적 옵션 DSL을 통해 선언한다. 그러면 검증, 타입 변환, 문서 생성이 자동으로 처리된다. - 동시성은 클래스 레벨에서 설정한다. 여러 워커 스레드가 동시에 인스턴스를 호출할 수 있다면 shared로, 한 번에 하나의 스레드만 실행하도록 직렬화해야 한다면 single로 설정한다. - 모든 플러그인 조회는 중앙 레지스트리를 거치도록 하고, 새로운 내장 플러그인도 그곳에 등록하며, 외부 플러그인은 패키지 단위로 탐지되도록 한다. ## 메트릭 같은 공통 처리는 플러그인을 감싸는 델리게이터에 두기 횡단 관심사는 감싸는 레이어에 두고, 협력 객체는 프레임워크로부터 받아 사용한다. - 모든 플러그인은 메트릭이나 스레드 안전성 같은 횡단 관심사를 추가하는 델리게이터 뒤에서 실행한다. 동작을 변경할 때는 그 변경이 델리게이터에 속하는지 플러그인 자체에 속하는지 먼저 판단한다. - 메트릭은 네임스페이스 계층 구조를 따르게 하고, 하위 네임스페이스는 namespace 호출을 통해 생성한다. - 메트릭이 비활성화되었거나 테스트 중일 때를 위해, 동일한 인터페이스를 구현하지만 데이터를 버리는 null 객체 메트릭을 제공한다. - 메트릭 객체를 직접 인스턴스화하지 말고 프레임워크로부터 전달받아 사용한다. 로깅이나 설정 같은 표준 믹스인은 자동으로 포함되지만, 더 무거운 기능은 옵트인 방식으로 남겨둔다. ## 워커 루프를 가볍게 유지하고 시작·정지 순서 지키기 핫 패스를 보호하고, 시작·종료 순서를 반드시 지킨다. - 워커 루프는 핫 패스로 취급한다. 이 루프는 배치를 가져와 컴파일된 파이프라인을 통과시키고 완료를 시그널링한다. 이 안에는 블로킹 호출이나 무거운 할당을 추가하지 않는다. - 엄격한 라이프사이클 순서를 따른다. 출력과 필터가 먼저 등록되고, 그다음 워커가 시작되며, 입력은 가장 마지막에 시작된다. 종료 시에는 입력이 먼저 멈추고, 워커가 남은 작업을 비운(drain) 뒤, 필터와 출력이 닫힌다. 이 순서를 어기면 데이터 손실이나 교착 상태가 발생한다. - 스레드 간에 공유되는 상태에는 일반 boolean 필드가 아니라 원자적(atomic) 프리미티브를 사용한다. 그렇지 않으면 스레드 간 메모리 가시성이 보장되지 않는다. ## 파이프라인 설정을 코드로 컴파일하고 디버깅용으로 내보내기 일부 엔진은 선언적 설정을 단계적 파이프라인을 통해 실행 가능한 클래스로 런타임에 컴파일한다. 먼저 설정 텍스트를 문법(grammar)으로 파싱해 구문 트리로 만들고, 이 트리를 노드와 조건문으로 이루어진 유향 비순환 그래프(DAG) 형태의 중간 표현으로 낮춘다. 이어서 각 노드의 소스를 생성해 전용 네임스페이스 안에서 런타임에 클래스로 컴파일하고, 마지막으로 컴파일된 단위들을 실행 루프에 연결한다. 조건문은 분기 노드에 연결되는 프레디케이트 함수로 컴파일된다. 실제로 무엇이 실행되는지 확인할 수 있도록, 생성된 소스를 디렉터리에 덤프하는 디버그 플래그를 제공한다. ## 에러·null·로깅 방식을 코드 전체에서 맞추기 코드베이스 전체에 걸쳐 구조, 에러, null, 로깅 컨벤션을 일관되게 적용한다. - 모듈은 아키텍처 레이어가 아니라 기능 도메인 단위로 구성하고, 새로 추가하는 타입은 그것이 속한 도메인에 배치한다. - 네이밍은 일관되게 유지한다. 확장(extension), 델리게이터, 추상(abstract) 타입에는 정해진 접미사·접두사를 사용하고, 서브클래싱을 의도한 경우가 아니라면 클래스는 final로 표시한다. - 언체크(unchecked) 런타임 예외를 우선 사용한다. 체크(checked) 예외는 불가피한 경우에만 사용하되, 항상 설명적인 메시지와 원인 체인을 함께 보존한다. - null 처리와 관련해서는, 생성자 인자를 명시적인 non-null 검사로 검증하고, 의도적으로 nullable한 계약에는 주석(어노테이션)을 달며, 실패할 수 있는 조회에는 optional 타입을 사용하되 이를 null의 일반적인 대체제로 남용하지 않는다. - 클래스마다 static final 로거를 하나씩 선언하고, 로그 레벨은 신중하게 선택한다. 세부 진단 정보는 낮은 레벨로, 복구 가능한 문제는 warning으로, 잡히지 않은 실패는 error로 기록한다. - 불변(immutable)인 final 필드를 우선 사용하고, 공유 캐시에는 동시성 지원 맵 타입을 사용하며, 여러 곳에서 공유되는 타입에는 스레드 안전성을 문서화한다. ## API 응답은 하나의 헬퍼로 보내고 로직은 커맨드 클래스에 두기 라우팅, 응답, 로직이 각각 하나의 자리를 갖도록 서비스를 레이어로 나눈다. - API 모듈은 웹 프레임워크의 베이스 클래스를 확장하고 생성자를 통해 공통 의존성을 받는 공유 베이스 클래스를 상속하도록 한다. - 엔드포인트는 하나의 중앙 네임스페이스 맵에 선언된 경로 접두사에 마운트한다. 새로운 네임스페이스는 즉흥적인 라우팅이 아니라 이 맵에 추가한다. - 모든 응답은 페이로드를 기본 메타데이터로 감싸는 단일 응답 헬퍼를 거쳐 보낸다. 원시 응답을 직접 작성하지 않는다. - 에러는 not-found, bad-request, timeout처럼 소수의 타입화된 에러 서브클래스로 표현한다. 핸들러에서 이 에러들을 발생시키고, 일관된 형식의 응답 포맷은 베이스 클래스가 담당하게 한다. - 비즈니스 로직은 라우트 핸들러가 아니라 전용 커맨드 클래스에 둔다. 핸들러는 커맨드를 생성하고 그 결과를 응답 헬퍼로 전달하는 역할만 한다. ## 두 런타임을 잇는 부분은 양쪽을 거치는 왕복으로 테스트하기 호스트 가상 머신이 두 번째 언어를 구동하는 경우처럼 코드가 두 런타임을 연결할 때는, 프레임워크가 지정한 등록 지점에 브리지된 클래스를 등록하고 그곳에 이미 있는 패턴을 따른다. 스레드별 컨텍스트 객체는 사용하는 시점에 가져와야 하며, 생성 시점에 저장해두면 안 된다. 저장해둔 컨텍스트를 다른 스레드에서 재사용하면 실패하기 때문이다. 양쪽을 각각 따로 테스트하기보다는 두 레이어를 관통하는 왕복(round trip)을 테스트해, 단일 레이어 단위 테스트로는 잡아내지 못하는 타입 변환·등록 관련 버그를 발견한다. 두 번째 런타임을 로드하는 테스트는 모듈 조회 경로가 벤더링된 위치를 가리키도록 해야 한다. 이 경로는 의도적으로 지정된 것이므로, 해당 테스트를 수정할 때도 이 경로는 바꾸지 않는다. ## 실제 파이프라인 헬퍼로 테스트하고 상태를 남기지 않기 목적에 맞게 만들어진 헬퍼로 실제 동작을 검증하고, 상태를 뒤에 남기지 않는다. - 파이프라인 객체를 직접 만드는 대신, 실제 파이프라인을 구동하고 결과를 캡처해주는 제공된 파이프라인 헬퍼를 우선 사용한다. - 격리를 위해, 스펙 파일 안에 일회용 플러그인 클래스를 정의하고 실제 레지스트리에 등록한다. - 인터페이스 계약에는 공유 예제(shared examples)를, 공통 셋업에는 공유 컨텍스트(shared context)를 사용한다. 통합 테스트는 픽스처 파일로부터 부트스트랩하는 픽스처-서비스 패턴으로 진행한다. - 기존의 rescue/catch 블록이 새로 추가한 코드 경로까지 실제로 커버하는지 확인한다. - 전역 상태나 가상 머신 상태는 teardown에서 정리한다. 설정한 프로퍼티는 반드시 대응하는 clear를 짝지어, 상태가 다른 테스트로 새어나가지 않도록 한다. - 범용 목(mock) 객체보다는 실제 베이스 클래스를 상속한 구체적이고 목적에 맞는 더블을 우선 사용한다. mock은 인프라 경계에서만 쓰고, 스텁이 셋업 용도일 뿐이라면 실제 메서드가 실행되게 둔다. ## 새 테스트가 해당 변경 없이는 실패하는지 확인하기 각 테스트가 자신이 지켜야 할 변경 사항에 실제로 의존하고 있는지 확인한다. - 새로 작성한 테스트는 해당 변경 사항 없이는 실패하는지 반드시 확인한다. 수정이 있든 없든 통과하는 테스트는 아무것도 증명하지 못한다. - 기존에 이미 성립하던 조건이 아니라, 그 변경이 새로 도입한 구체적인 동작을 검증(assert)한다. 새 필드, 변경된 값, 이전에는 없던 로그 라인처럼 관찰 가능한 차이에 검증을 고정시킨다. - 버그 수정의 경우, 먼저 테스트에서 그 버그를 재현한다. 버그가 나타나는 것과 동일한 이유로 테스트가 실패하는 것을 확인한 뒤, 수정을 적용해 통과하는 것을 확인한다. - 동어반복적인 mock을 조심한다. 어떤 메서드가 반환하도록 스텁한 값을 그대로 검증하는 것은 순환 논리이며, 구현을 삭제하고 mock만 남겨도 여전히 테스트가 통과한다면 그 테스트는 mock만 검증하고 있는 것이다. ## 호환이 깨지는 변경은 단계적으로 넣고 시크릿은 로그에 남기지 않기 동작 변경은 단계적으로 진행하고, 민감한 값은 보호하며, 문서를 항상 최신 상태로 맞춘다. - 호환성을 깨는 변경은 단계적으로 진행한다. 마이너 릴리스에서는 새 동작을 옵트인으로 도입하고 폐기 예정(deprecation) 경고를 추가한다. 그다음 옵트아웃 수단을 유지한 채 기본값을 변경하고, 최종적으로 메이저 릴리스에서 기존 동작을 제거한다. - 민감한 값을 담는 설정에는 프레임워크의 전용 시크릿 타입이나 래퍼를 적용하고, 문자열 표현을 오버라이드해 값이 마스킹되도록 한다. 이런 값은 어떤 로그 레벨에서도 기본적으로 로그에 기록하지 않는다. - 문서를 변경 작업의 일부로 취급한다. 동작이 바뀔 때마다 관련 레퍼런스 문서와 기여자 문서를 업데이트하고, 각 포맷의 컨벤션을 따르며, 사용자에게 노출되는 기능은 문서화 없이는 머지하지 않는다.