같은 컨트롤러도 부팅 설정이 다르면 다른 응답을 낸다

API 테스트에서 필드가 보인다고 실제 응답에도 남는 것은 아니다. AI Chat의 앱 부팅은 excludeExtraneousValues: true인 전역 ClassSerializerInterceptor를 등록했지만, E2E 앱을 만드는 도우미에는 이 설정이 없었다. raw Mongoose 문서를 반환하는 워크스페이스 API에서 응답 필드가 빠지는 문제를 테스트가 가릴 수 있는 차이였다.

이 수정은 독립 챗봇에서 응답 DTO와 테스트 부팅 설정을 맞춘 사례다. 제품 통합 전의 워크스페이스 기능에 적용한 사례이며, 전체 제품의 응답 계약까지 검증한 결과는 아니다.

컨트롤러는 워크스페이스·봇·접근 규칙 문서를 @Expose로 노출 필드를 명시한 DTO로 변환한다. 테스트 앱에도 실제 앱과 같은 전역 serializer를 넣었다. 문서를 가져오는 서비스의 결과와 클라이언트에 전달할 응답을 따로 정하고, 그 응답이 실제 변환을 통과한 뒤에도 필요한 필드를 갖는지 검사할 수 있게 한 것이다. 다음은 핵심 설정과 응답 변환을 발췌한 코드다.

// 실제 앱과 E2E 앱의 공통 직렬화 옵션
new ClassSerializerInterceptor(app.get(Reflector), {
  excludeExtraneousValues: true,
});

// 컨트롤러의 응답 경계
const doc = await this.workspaceService.create({
  ...dto, user_login: req.user.user_login,
});
return toResponseDto(WorkspaceResponseDto, doc);

전역 serializer를 제거하면 raw 필드를 보이게 만들 수 있지만, 노출 필드를 제한하는 정책도 약해진다. DTO를 유지하는 선택은 응답 필드와 변환을 명시적으로 관리하는 비용을 감수한다. 타입 이름이 DTO라는 사실만으로 노출 계약이 생기지는 않으므로 실제 decorator와 변환 호출이 함께 필요하다.

테스트 데이터가 잘못된 조회에 맞춰져 있었다

함께 수정한 다른 문제는 저장 타입이었다. 실제 생성 API는 봇의 workspace_id를 ObjectId로 저장하는데 테스트 팩토리는 문자열을 넣었다. 문자열을 그대로 조회하는 코드가 팩토리 데이터에는 맞지만 실제 생성 경로에서는 봇을 찾지 못할 수 있었다. 그러면 설정 갱신을 건너뛰거나 자료 업로드가 404로 끝나는 경로가 생긴다.

변경은 findOneByWorkspaceId()의 조회 값을 new Types.ObjectId(workspaceId)로 정규화하고 팩토리도 실제 저장 타입으로 바꿨다. 접근 규칙은 저장·조회 양쪽에 같은 정규화를 적용했다. 문자열로 전달되는 URL 매개변수와 저장소의 타입을 같은 계약으로 잇는 수정이다.

  1. POST로 워크스페이스 생성

    팩토리 대신 API를 통해 워크스페이스와 봇을 만들고 응답 ID를 받는다.

  2. PATCH 후 GET 대조

    모델·지침을 변경하고 조회 응답에 변경값이 남는지 검사한다.

  3. 같은 생성 경로로 업로드

    생성된 워크스페이스에 자료를 올려 성공 상태와 배열 응답을 검사한다.

실제 생성 경로를 사용하는 테스트는 팩토리보다 준비 과정이 무겁다. 대신 팩토리가 생략하거나 잘못 흉내 낸 저장 타입과 생성 부수 효과를 통과한다. 설정 저장은 상태 코드만 확인하지 않고 다시 읽은 값을 비교해, 갱신이 조용히 빠지는 경우를 드러내도록 했다.

E2E라는 이름보다 어떤 경계를 통과했는지가 중요하다

검증 수단별로 확인하는 계약
수단확인하는 내용남는 경계
봇 서비스 단위 테스트조회 ID의 문자열 변환값과 mock 반환값ObjectId 타입 자체와 HTTP 응답 직렬화
수정된 E2E 앱API 생성·수정·조회와 전역 직렬화실제 인증 미들웨어와 브라우저 동선
자료 업로드 시나리오API 생성 뒤 자료 경로의 성공 응답mock OpenAI와 테스트 저장 경로 밖의 연동
컨트롤러 lint 규칙반환 타입에 표기된 *Document 제한raw 문서 반환을 모두 찾는 의미 분석

봇 서비스 단위 테스트는 전달한 ID를 String(...)으로 바꿔 비교한다. ObjectId와 같은 문자열 ID가 모두 이 비교를 통과하므로, 이 assertion만으로 저장 타입의 차이를 구별할 수는 없다.

추가한 lint 규칙은 컨트롤러 메서드의 반환 타입에 Document로 끝나는 식별자가 있는지 제한한다. 타입을 다르게 적거나 다른 모양으로 반환하는 모든 경우를 탐지하는 도구는 아니다. 또한 ObjectId 정규화는 저장·조회 타입을 맞추는 변경이며, 잘못된 ID 입력의 모든 오류 처리를 입증하지는 않는다.

두 수정의 공통점은 테스트가 실제 계약을 느슨하게 바꾸고 있었다는 점이다. 한쪽은 응답 변환을 빼고, 다른 쪽은 저장 타입을 달리 만들었다. 서비스 mock의 성공, API 경로의 성공, 실제 외부 연동의 성공을 각각 확인해야 테스트가 보장하는 범위를 정확히 읽을 수 있다.