발행일

빨갛게 남아 있던 e2e 25건 중 코드 회귀는 1건이었다 — 낡은 계약·조용한 skip·잔재를 가르기

빨갛게 남아 있던 e2e 25건 중 코드 회귀는 1건이었다

9월 3일 기준 Playwright 시나리오 스위트는 235 통과 · 25 실패 · 11 skip이었습니다. 실패 25건은 그날 생긴 게 아니라 쌓여 온 것이었고, 그렇게 쌓인 빨간불은 "원래 그런 것"으로 읽힙니다.

25건을 하나씩 열어 갈랐습니다. 결과는 코드 회귀 1 · 데이터 없음 4 · 나머지 20건은 낡은 스펙이었어요. 그날 정리한 문서에 이렇게 적었습니다.

실패가 오래 쌓이면 "원래 그런 것"으로 읽혀 진짜 회귀 하나가 그 안에 묻힌다 — 실제로 그랬다.

이 글은 그 25건을 어떻게 갈랐는지, 그리고 같은 날 빨강도 초록도 아닌 채 아무것도 확인하지 않던 skip과 데이터를 남기던 스펙을 어떻게 걷어냈는지에 대한 기록입니다(abbcba0e·df0c5186·b68e9cf5·40677138·ae4242e0).

실패를 먼저 세 갈래로 나눴다

한 건씩 볼 때 물은 건 하나였습니다. 이 빨간불은 화면이 틀렸다는 뜻인가, 스펙이 틀렸다는 뜻인가. 스펙 쪽이면 다시 둘로 나눴습니다. 화면이 옮겨 갔는데 스펙만 남은 것, 그리고 데이터가 바뀌었는데 기대값이 상수로 남은 것.

화면이 옮겨 갔는데 스펙만 남은 것

스펙무엇이 옮겨 갔나
graph-legend-label 2건 · graph-explore:200관계도 범례가 SVG 좌상단에서 그림 아래 HTML 알약 줄로 옮겨 갔다(8월 24일)
meta-class-detail · property-detail-class-ref공개 클래스 상세 제목에서 링크가 떨어졌다(7e9047cf)
data-map-ontology-relations 2건클래스와 인스턴스 사이에 이름 첫 글자 묶음 단이 끼었다(8e9fbac9)

이쪽은 화면이 맞고 스펙이 옛 구조를 붙잡고 있었습니다. 선택자와 단계를 지금 화면에 맞춰 다시 썼어요.

기대값이 데이터를 못 따라간 것

graph-explore:107은 "카탈로그 직계에 Message는 없다"를 상수로 박아 두고 있었습니다. 8월 29일에 발행 단위가 kadif:Message가 되면서 카탈로그 직계에 메시지가 12건 붙었고, 범례 재계산은 정상인데 스펙만 빨개졌어요. 이 스펙이 지켜야 할 계약은 "특정 타입이 없다"가 아니라 "템플릿 고정 9종이 아니라 중심 직계에 실제로 서는 타입만 범례에 나온다"였습니다. 그래서 기대값을 API 응답에서 뽑도록 축을 돌렸습니다.

// 전 — 특정 타입의 부재를 상수로
await expect(legend).not.toContainText("Message");

// 후 — 중심 직계에 실제로 서는 타입을 응답에서 뽑아 범례와 비교
expect([...shown].sort()).toEqual([...expectedTypes].sort());

이건 8월 2일에 pk를 박아 둔 스펙을 정리할 때 "적재량에 매달린 단언"으로 한 번 다룬 종류입니다. 그때 고친 건 노드 수·키워드 같은 숫자였고, 이번에는 "무엇이 없다"는 부재 단언이었다는 차이뿐이에요.

더 찾기 어려웠던 건 모델 변경입니다. 데이터 프레임이 엘리먼트를 직접 물던 구조가 필드(kadif:DataFrameField) 경유로 바뀌면서 kadif:dataElement어느 클래스의 구성 속성도 아니게 됐습니다. 그런데 용어 자체는 남아 있어서 URI 조회 헬퍼는 통과하고, 화면에서 그 속성을 못 찾는 단계에서야 실패했어요. 실패 지점이 원인과 떨어져 있으니 원인이 잘 안 보였습니다. 헬퍼의 URI 상수를 필드 짝으로 옮겼고, 운영 미러링으로 들어온 용어 중 URI가 빈 것이 699개 중 3개라 이름으로 찾는 propertyIdByName을 따로 뒀습니다.

데이터가 없어 못 보는 것

데이터 없음 실패는 dev에 OID 잎이 하나도 없어서 생겼습니다. 잎이 없으면 화면은 목록 자리에 형제 버킷 3개를 그리고, "12개씩 페이지된다"는 스펙이 12 ≠ 3으로 죽어요. 기대값을 3으로 낮춰 쓰면 계약이 사라지니, 잎이 없을 때는 이유를 밝히고 건너뛰게 했습니다(oid-detail-sections·oid-search-tree-drilldown). 분류표에는 4건인데 이 조치를 적은 절에는 두 스펙 합쳐 6건으로 적혀 있어, 두 숫자가 맞지 않습니다. 같은 날 저녁 운영 OID 잎 1,489건을 번호 그대로 dev에 심는 명령을 만들어(f87ba1a1) 이 skip이 기다리던 데이터를 채웠습니다.

회귀로 읽은 것이 회귀가 아니었던 경우

관계도의 잎 노드에 "관계 펼치기"가 열려 있어 스펙 하나가 깨져 있었습니다. 팀원이 9월 1일 커밋(8e9fbac9)에서 말단(kadif:DataElement)에만 켜던 펼치기 게이트를 모든 인스턴스 노드로 넓힌 결과였어요. 넓히지 말라던 경고 주석이 같이 지워졌고 변경 기록이 없어서, 저는 처음에 이걸 회귀로 읽었습니다.

실측해 보니 둘이 갈렸습니다.

  • 넓힌 채로 두면 개념 잎을 펼칠 때 skos:inScheme으로 체계 노드가 딸려 옵니다. 옛 계약 "체계 노드는 개념 중심 화면에 서지 않는다"와 어긋나요.
  • 옛 허용 목록으로 되돌리면 데이터 프레임 관계도에서 아무 이웃도 못 폅니다. 지금 이웃은 필드 노드라 옛 명단이 낡았거든요.

개념 계층만 빼는 중간값도 만들어 봤지만 넣지 않았습니다. 이 게이트는 서버가 인스턴스 응답에 중심의 1단계만 담는다는 사정과 얽혀 있어서, 백엔드·DB와 얽힌 판단은 담당 팀원을 따른다는 원칙대로 넓힘을 그대로 두고 옛 계약을 접었습니다. 스펙은 "잎도 펼치기가 열린다"로 다시 썼고, 개념 잎에서 체계 노드가 딸려 오는 건 알고 두는 부작용으로 문서에 적었습니다.

같은 원칙으로 한때 넘겼던 게 하나 더 있습니다. dev의 kadif:DataFrame 이름 원천이 비어 있어 목록 첫 열이 표제 대신 이름으로 뜨고, 그걸 기대하던 스펙 둘이 죽어 있었어요. 원인 후보로 스펙이 먼저 걸렸고, 같은 날 밤 원인은 이 헬퍼로 확정됐습니다. 필수 등급을 바꿨다 되돌리는 헬퍼가 required_properties만 보내는데, 서버는 이름 원천 키를 안 받으면 필수에서 풀린 속성의 이름 원천 자격도 지웁니다. 복구 요청에도 그 키가 없으니 한 번 비면 돌아오지 않아요. 8월 2일 글에서 원복이 타임아웃에 같이 죽지 않게 고친 바로 그 헬퍼입니다.

const data = { required_properties: propertyIds };
if (namePropertyId !== undefined) data.name_property = namePropertyId;

헬퍼가 키를 함께 싣고, 바꾸기 전 값을 readCompositionFlags로 읽어 두었다가 그대로 되돌리게 해 재발은 막았습니다. 처음엔 이미 비어 버린 DB 값 복구를 팀원 판단으로 넘겼는데, 같은 날 밤 dev 값을 운영과 같게 되돌렸고 기록 문서의 절 제목도 "복구함, 재발 감시"로 바꿨습니다(ae4242e0).

묻혀 있던 회귀로 보이는 것

정리 문서는 코드 회귀 1건이 무엇인지 이름으로 적어 두지 않았습니다. 같은 문서 절에서 스펙이 아니라 코드를 고친 건 데이터 통계 워드클라우드 하나였고, 같은 시각 커밋(f12c63cf)으로 들어갔습니다. 세 탭 구름이 통째로 안 그려지던 문제로, 원인은 따로 정리했습니다.

이 글에서 적어 둘 사실은 따로 있습니다. 그 화면에는 8월 1일부터 첫 탭 구름이 그려지는지를 svg text로 단언하는 스펙이 있었습니다(wordcloud-keyword.spec.js). 구름이 안 그려지면 빨개지는 스펙이에요. 그러니 알림이 없던 게 아니라 다른 빨간불 스무 몇 건 사이에 섞여 있었을 가능성이 큽니다. 언제부터 빨갰는지는 기록이 없습니다. 워드클라우드 글에는 알림이 없었다고 적었지만, 첫 탭 구름을 단언하는 스펙은 이미 있었던 셈입니다. 25건 전체 목록을 남기지 않아 이 스펙이 그중 하나였다고 단정하지는 못하지만, 문서의 "실제로 그랬다"가 가리키는 자리는 여기로 보입니다.

빨강도 초록도 아니던 skip

8월 2일 글의 정렬 토글 스펙은 돌면서 아무것도 안 봤고, 이번에 찾은 건 아예 돌지 않는 스펙이었습니다. 25건을 가르기 전인 같은 날 오후, 표준데이터 상세를 겨냥한 스펙 12개 45건을 따로 돌렸는데(25건을 센 전체 실행과는 별개 실행입니다), 실패 6건은 전부 낡은 계약이었고 서버 규칙 테스트 34건은 모두 통과했습니다. 화면 결함은 0이었어요. 문제는 실패 쪽이 아니라 skip 쪽에서 나왔습니다.

영영 건너뛰던 스펙

instance-detail-identifier-oid는 OID가 붙은 행을 #propertyRows 안에서 찾았습니다. 그런데 상세 화면이 구획 구조로 바뀌면서 그 선택자가 사라졌어요. 대상이 없으면 건너뛰게 짜여 있었으니 OID를 부여해도 영영 skip이었습니다. 빨간불도 안 뜨고 통과도 안 하는, 가장 조용한 상태예요.

선택자를 OID 행으로 고쳤습니다. 오후에는 dev의 OID 노드가 9건뿐이라 통과 경로를 볼 수 없었고, 저녁에 OID 잎을 심은 뒤 밤 실행의 skip 목록에서 빠졌어요. 따로 돌려 통과를 확인한 건 다음 날입니다(OID 상태 표시 스펙과 합쳐 24/24). 같은 커밋에서 instance-referenced-by"대상이 없으면 skip"을 "대상이 없으면 실패"로 바꿨어요. 나눈 기준을 풀면, dev 시드에 있어야 정상인 대상(누군가 참조하는 Service)이 없으면 실패이고, dev에 없는 게 사실인 대상(다음 관계가 있는 이웃 등)은 사유를 단 skip입니다.

// 전
test.skip(!leaf, "시드에 DataElement 가 없다");

// 후 — 없으면 skip 이 아니라 실패다
const url = await referencedServiceUrl(page);
expect(url, "누군가 참조하는 kadif:Service 가 dev 시드에 있어야 한다").not.toBeNull();

메뉴 136종 스윕 글에서 skip을 남긴 이유를 "사람이 확인하라는 지시"로 설명하면서, 한계로 "스킵 사유가 문자열이라 아무도 세지 않는다"를 적었습니다. 이번에 그 후속으로 한 일은 두 가지예요. 있어야 정상인 데이터를 기다리던 skip은 실패로 바꿨고(OID 스펙은 반대로 이유를 밝혀 skip으로 돌렸습니다 — 기대값을 낮추면 계약이 사라져서요), 남은 skip은 사유와 함께 표로 문서에 셌습니다.

쌍둥이 데이터가 만든 거짓 판정

상세의 "활용·연결" 표 스펙은 같은 이름이 두 줄 서면 중복으로 판정했습니다. 그런데 dev에는 이름이 같은 데이터 프레임이 여러 쌍 있었어요. 이름으로 보면 다른 자원 둘이 한 자원으로 읽히니, 판정 키를 링크의 pk와 필드명으로 바꿨습니다.

이 쌍에 대한 기록은 같은 날 한 번 고쳐 썼습니다. 오후에는 "dev에 데이터 프레임 99쌍, 한쪽은 상태만 든 빈 껍데기"로 적었는데, 밤에 운영 REST로 다시 세어 보니 운영에도 같은 이름의 발행 인스턴스가 84종(데이터 프레임 50·엘리먼트 28·메시지 6) 있었고, 예로 든 쌍도 링크 11개와 5개로 둘 다 값을 든 자원이었습니다. 오후의 99쌍은 dev의 데이터 프레임만 센 값이고 밤에는 세 클래스를 운영 기준으로 다시 센 것이라, 데이터 프레임 숫자가 99에서 50으로 달라진 이유는 따로 확인하지 못했습니다. dev만의 찌꺼기가 아니라 운영 원천 데이터의 문제라, 앞의 서술을 지우고 다시 적었어요. 화면 버그는 아니고 원인은 미확인이며, 합칠지는 표준 목록을 맡은 팀원이 정할 일로 넘겼습니다.

건너뛰는 시점이 틀려 데이터를 남기던 스펙

그날 밤 마지막으로 고친 건 건너뛸 때 데이터를 남기던 스펙이었습니다. OID 자동 부여 스펙은 인스턴스를 저장한 뒤에 OID 슬롯이 없는 환경이면 건너뛰었습니다.

// 전 — 저장한 뒤에야 건너뛴다(만든 인스턴스는 남는다)
await page.locator("#drSaveBtn").click();
test.skip(!target.hasAutoSlot, "kadif:objectIdentifier 슬롯이 없는 환경");

// 후 — OID 슬롯과 이름 원천을 둘 다 가진 클래스를 먼저 고르고, 없으면 쓰기 전에 건너뛴다
test.skip(!target, "OID 슬롯과 이름 원천을 둘 다 가진 발행 클래스가 없는 환경");

skip 결과만 보면 문제가 없어 보이지만, 건너뛸 때마다 dev DB에 인스턴스가 하나씩 남았습니다. 이번에 막은 건 이 건너뛰는 경로의 잔재예요. 대상도 dcat:Dataset에서 발행 3클래스로 옮겼어요. dcat:Dataset은 8월 31일에 자동 부여 대상에서 빠졌거든요.

같은 커밋에서 함께 고친 게 둘 더 있습니다.

  • 문의 권한 스펙은 관리자 문의를 먼저 쓰고 나서 QA 계정 로그인을 확인했습니다. QA 계정이 없는 DB에서는 로그인 단계에서 죽어 finally에 닿지 못하고 문의가 실행마다 남았어요. 로그인 확인을 앞으로 옮겼습니다.
  • 페이지네이션이 없어야 하는 "작은 클래스"로 DataFrame을 쓰고 있었는데, 운영 미러 뒤 695건이 되어 전제를 잃었습니다. dev 13건·운영 18건인 Function으로 바꿨습니다.

OID 트리 스펙 2건도 같은 날 정리했습니다(b68e9cf5). 잎을 트리 뿌리로 세우던 옛 계약이 "잎은 트리 밖 카드 목록으로"로 바뀌었는데 두 개만 옛 계약으로 남아 있었고, 잎 이름을 DE_CargoWeight로 하드코딩하고 있어 실행 시점에 목록 첫 카드에서 고르게 했어요.

검증

  • 같은 날 밤 다시 돌린 시나리오 스위트 274건(앞의 271건에서 늘어 있었습니다): 통과 266 · 실패 0 · skip 8. skip 8건 중 7건은 시드 사유라 스펙과 사유를 표로 문서에 적었고, 1건은 같은 날 고쳐 통과했습니다.
  • 메뉴 스윕(e2e/qa): 통과 116 · skip 35. skip은 제외 범위(워크플로 상세·물리 아키텍처 상세)와 삭제 URL 등 문서에 적힌 건입니다.
  • 표준데이터 상세 스펙 12개 45건: 통과 39 · skip 6 · 실패 0. 같은 화면의 서버 규칙 테스트 34건 통과.
  • 해결된 항목은 QA 기록 문서에서 지웠습니다(정리 커밋 전체 +92 / −389줄). 해결 경위는 커밋과 각 화면의 정본 문서에 두고, 기록 문서에는 지금 열려 있는 것만 남기는 쪽으로요.

남은 것 · 한계

  • 코드 회귀 1건이 무엇이었는지 이름을 남기지 않았습니다. 25건 전체 목록을 안 적어 두어서, 워드클라우드였다는 건 같은 날 코드 수정과 스펙 단언으로 미루어 본 것입니다. 분류 숫자만 남기고 목록을 안 남긴 게 이번 기록에서 제일 아쉬운 대목입니다.
  • skip을 센 건 문서의 표입니다. 리포트가 사유별로 자동 집계하는 게 아니라, 다음에 누가 같은 표를 다시 만들어야 합니다. 메뉴 스윕 글에서 적은 한계가 절반만 풀렸어요.
  • OID 자동 부여 스펙은 통과할 때도 만든 인스턴스를 지우지 않습니다. 이번에 막은 건 건너뛰는 경로의 잔재뿐이라, 통과하는 실행마다 dev DB에 인스턴스가 1건씩 남아요.
  • 알고 두는 부작용이 하나 남았습니다. 개념 잎을 펼치면 체계 노드가 딸려 오는 것으로, 팀원 판단으로 넘긴 상태예요. 데이터 프레임 이름 원천은 되돌렸지만 다시 갈려도 알려 주는 장치는 없습니다. 같은 이름의 발행 인스턴스 84종도 원인 미확인인 채 판정 키만 바꿔 피해 갔습니다.
  • 8월 2일에 "원복이 필요한 스펙이 늘면 다시 계산해야 한다"고 적었는데, 이번에 데이터를 남기던 스펙이 둘 나왔습니다. e2e가 dev DB를 그대로 보는 트레이드오프를 다시 계산할 때가 온 셈인데, 이번에도 하지 않았어요. 두 판본을 동시에 돌리면 같은 SQLite 파일을 물어 서로를 깨뜨린다는 것도 문서에 "순차로 돌릴 것"을 적었을 뿐 막는 장치는 없습니다.

관련 글: pk를 박아둔 e2e 스펙 11개가 한꺼번에 빨개졌다 · Playwright로 메뉴 136종 QA 스윕 · 워드클라우드가 통째로 안 그려졌다