실행별 데이터 식별자
70분 안팎
학습 목표
고정 계정 재사용 대신 실행별 이름 공간을 설계합니다.
개념
고정 계정이 순서 의존을 만듭니다
가입 테스트가 항상 qa@example.test를 쓰면 첫 실행은 성공하고 다음 실행은 중복 거절될 수 있습니다. 로그인 테스트가 앞선 가입 테스트의 계정을 기대하면 혼자 실행했을 때 실패합니다. 재시도 횟수를 늘리기 전에 각 사례가 필요한 계정과 세션을 직접 준비하는지 확인합니다. 데이터 팩토리는 테스트 실행의 식별자와 사례의 식별자로 합성 계정을 만드는 작은 함수입니다.
실행 이름 공간 runId는 같은 저장소를 동시에 사용하는 실행끼리 다르게 예약합니다. caseId는 그 실행 안의 독립 사례를 구별합니다. 여기서는 소문자 영문과 숫자로 된 비어 있지 않은 문자열만 허용하고 runId.caseId@example.test를 반환합니다. 함수가 실행 식별자를 자동 예약하는 것은 아닙니다. 호출자가 중복 runId를 전달하면 같은 계정이 생성되므로 CI 실행 번호나 별도 예약 장치를 준비해야 합니다.
충돌 조건을 먼저 적습니다
두 식별자를 구분자 없이 붙이면 ab와 c, a와 bc가 같은 abc가 됩니다. 점을 구분자로 쓰면서 식별자에도 점을 허용하면 a.b와 c, a와 b.c가 같은 문자열이 됩니다. 따라서 구분자를 입력 문자 집합에서 제외합니다. 문자 제한과 구분자 규칙을 함께 검사해야 문자열 생성 함수의 독립성을 설명할 수 있습니다. 실행 시간이 다르다는 사실만으로 계정 값이 다르다고 가정하지 않습니다.
같은 runId와 caseId는 같은 이메일을 돌려줍니다. 이것은 실패를 재현하기 위한 결정성입니다. 독립 반복 실행은 새 runId를 예약하고, 같은 실행에서 같은 계정을 다시 보내는 중복 가입 검사는 기존 caseId를 재사용합니다. 모든 호출을 랜덤 계정으로 바꾸면 중복 가입 시나리오의 사전 조건을 잃습니다. 독립 사례와 의도적인 상태 전이를 구분하여 설계합니다.
팩토리는 준비 성공과 다릅니다
이메일을 생성했다고 DB에 행이 생긴 것은 아닙니다. 팩토리는 계획된 값을 만들고 준비 함수는 API를 호출합니다. API 성공과 SQL 관찰을 확인한 뒤 소유 목록에 생성된 식별자를 등록합니다. 준비 요청이 실패했는데 존재한다고 가정하면 로그인 실패의 원인을 잘못 읽게 됩니다. 문자열 생성, 저장 준비, 검증 실행의 책임을 나누면 실패 단계가 분명해집니다.
이번 브라우저 입력은 runId 문자열과 cases 문자열 배열입니다. 유효한 입력에 대해서 배열 순서를 유지해 이메일을 한 줄씩 출력합니다. 빈 배열은 출력이 없습니다. 중복 caseId는 INVALID로 거절합니다. 잘못된 식별자, 배열이 아닌 cases, JSON 파싱 실패도 INVALID를 한 번 출력합니다. 일부 정상 항목을 먼저 출력하고 나중에 INVALID를 덧붙이지 않도록 전체 입력을 확인한 뒤 결과를 만듭니다.
출력 계약과 경계 입력입니다
console.log에 배열을 직접 넘기면 대괄호와 따옴표가 함께 찍힐 수 있습니다. 이 실습의 출력 계약은 이메일 문자열 자체의 줄 목록이므로 반복문으로 각 문자열을 출력합니다. 결과를 정렬하면 입력 사례 순서를 유지하라는 요구를 어깁니다. 함수 내부에서 받은 배열을 sort로 바꾸면 이후 실행 순서 검사의 입력까지 바뀔 수 있으므로 필요할 때는 복사한 배열을 다룹니다.
빈 runId, 점이 들어간 caseId, 중복 caseId, 빈 cases를 경계 입력으로 둡니다. 유효 사례 두 개만 통과해도 모든 입력 계약을 구현했다고 말할 수 없습니다. 입력 검증은 허용하지 않는 값을 조용히 수정하는 대신 INVALID로 알려 줍니다. 임의로 소문자로 바꾸면 서로 다른 원본 식별자를 합칠 수 있으므로 이번 규칙에서는 대문자를 거절합니다.
실행 추적 정보를 보존합니다
오류 기록에는 runId와 caseId, 생성 이메일을 함께 남깁니다. 이메일에 실행 정보가 있으므로 어떤 실행이 만든 행인지 확인하기 쉽지만 삭제 권한의 증거는 생성 완료 목록에서 얻습니다. 접두사만 보고 자신의 데이터라고 확정하지 않습니다. 사용자 입력이나 다른 실행이 비슷한 문자열을 사용할 수 있으므로 실제 준비 과정에서 관찰한 식별자를 정리 기준으로 삼습니다.
난수는 충돌 확률을 줄이는 도구일 수 있지만 DB 유일 제약과 소유 추적을 대신하지 않습니다. 현재 레슨은 문자 조합 함수의 규칙을 검사하며 시간 기반 ID의 분산 예약, 병렬 워커의 실행 번호 발급, 재실행 정책은 환경별 설계 대상입니다. 제한된 실습의 보장을 실제 CI 전체 보장으로 확대하지 않습니다. 실패를 다시 실행할 때는 같은 seed와 새 이름 공간을 각각 기록합니다.
테스트에서 사용합니다
가입 사례는 자신의 계정으로 가입하고 SQL로 한 행 증가를 확인합니다. 중복 사례는 자기 계정의 첫 가입을 준비 단계에 두고 두 번째 가입을 검증합니다. 로그인 사례도 별도로 계정을 준비한 뒤 자기 자격 증명으로 로그인합니다. 앞 테스트가 남긴 회원을 사용하지 않으면 정방향과 역방향 실행의 사전 조건이 같아집니다. 로그인 결과 세션 역시 사례 밖 전역 변수로 공유하지 않습니다.
실습 함수가 INVALID를 출력하면 runId와 cases 타입부터 확인합니다. expected와 actual에 같은 이메일이 반복되어 있으면 중복 caseId 또는 실행 식별자 재사용을 조사합니다. 값을 바꾸며 우연히 통과한 실행만 보고하지 않습니다. 어떤 입력 계약을 바꿨는지와 동일 seed에서 재현되는지를 함께 남겨야 수정의 근거가 됩니다.
팀 리뷰에서 묻는 질문입니다
이 이름 공간을 누가 예약하는지, 한 실행을 다시 시작할 때 기존 이름을 재사용하는지, 동시에 도는 워커가 같은 caseId를 쓰는지 질문합니다. 같은 실행 ID를 사용하더라도 워커 ID를 별도 구성요소로 넣는 계약을 선택할 수 있습니다. 그때는 생성 규칙과 허용 문자, 길이 상한, 기록 형식을 함께 바꾸고 경계 테스트도 추가합니다. 문자열 조합만 바꾸면 기존 정리 도구가 새 이름을 해석하지 못할 수 있습니다.
이번 팩토리의 출력 길이를 그대로 운영 입력 상한의 근거로 삼지 않습니다. 실제 앱의 email 길이 제한과 테스트 ID 길이 제한을 합의해야 합니다. 합성 계정은 실재 사용자와 구별되는 example.test 도메인을 사용하고, 비밀번호를 이메일에서 생성하지 않습니다. 계정 식별과 인증 비밀값의 책임을 섞지 않아야 공개 증거를 안전하게 공유할 수 있습니다. 배열과 객체의 일반 사용법은 더 읽기로 이어 갑니다.
따라하기
모호한 연결을 재현합니다
독립적으로 실행하여 결과를 비교합니다.
console.log('ab'+'c'==='a'+'bc');console.log('ab.c'==='a.bc');실행 결과
true false
사례 순서를 유지합니다
독립적으로 실행하여 결과를 비교합니다.
const runId='r17';for(const id of ['login','signup'])console.log(`${runId}.${id}@example.test`);실행 결과
r17.login@example.test r17.signup@example.test
경계 입력을 확인합니다
독립적으로 실행하여 결과를 비교합니다.
const fs=require('node:fs');
try {const x=JSON.parse(fs.readFileSync(0,'utf8'));const valid=s=>typeof s==='string'&&/^[a-z0-9]+$/.test(s);if(!x||!valid(x.runId)||!Array.isArray(x.cases)||!x.cases.every(valid)||new Set(x.cases).size!==x.cases.length)throw Error('invalid');for(const id of x.cases)console.log(`${x.runId}.${id}@example.test`);}catch{console.log('INVALID');}실행 결과
INVALID
확인 문제
실습
JSON {runId,cases}를 읽어 입력 순서대로 runId.caseId@example.test를 한 줄씩 출력합니다. 두 식별자는 소문자 영문·숫자 한 글자 이상이며 cases는 중복 없는 문자열 배열입니다. 빈 배열은 빈 출력, 잘못된 JSON·타입·문자는 INVALID 한 줄입니다.
모범 답안
const fs=require('node:fs');
try {const x=JSON.parse(fs.readFileSync(0,'utf8'));const valid=s=>typeof s==='string'&&/^[a-z0-9]+$/.test(s);if(!x||!valid(x.runId)||!Array.isArray(x.cases)||!x.cases.every(valid)||new Set(x.cases).size!==x.cases.length)throw Error('invalid');for(const id of x.cases)console.log(`${x.runId}.${id}@example.test`);}catch{console.log('INVALID');}더 읽기
면접 질문
- 실행 순서에 따라 결과가 달라지는 테스트를 확인하는 방법을 설명해 주시면 됩니다.