Devin.KR

개인 자료 보관 앱의 첫 요청

90분 안팎

학습 목표

시작 코드에서 자료 생성·조회 요청과 테스트 흐름을 확인합니다.

개념

첫 기능을 작은 계약으로 검증하기

개인 자료 보관 앱이 자료를 생성했다고 응답해도 실제로 다시 읽을 수 없다면 저장 기능은 완성되지 않았습니다. 화면에 성공 문구가 뜨는지만 보는 대신 생성 요청의 응답과 같은 식별자의 조회 응답을 비교합니다. 이번 레슨은 Node.js 시작 코드에서 그 연결을 완성하는 연습입니다. 테스트는 요청 처리 함수를 직접 호출하고, HTTP 어댑터는 같은 함수를 사용하도록 분리되어 있습니다.

요청 계약은 POST /documents에 title과 content를 담아 새 자료를 만들고 GET /documents/1처럼 id로 읽는 형태입니다. POST 성공은 201, 조회 성공은 200입니다. 비어 있는 제목이나 잘못된 자료 형태는 400, 없는 자료나 계약에 없는 경로는 404로 구분합니다. 이 숫자는 이번 앱의 응답 계약이며 성공 상태 하나만으로 데이터 내용이나 보안 통제가 맞다고 판단할 수 없습니다.

파일 역할을 찾아가기

app.cjs에는 createApp 함수가 있고 호출하면 handle 함수를 돌려줍니다. handle은 method, path, body를 받아 status와 body가 있는 객체를 반환합니다. app.test.cjs는 이 함수를 호출해 기대값과 비교합니다. demo.cjs는 정해진 합성 요청의 결과를 보여 줍니다. server.cjs는 node:http 요청을 받아 handle로 전달합니다. check.sh는 테스트를 실행하고 실패가 있으면 성공 표시 없이 종료합니다.

CommonJS 형식의 require는 표준 모듈이나 같은 폴더 파일을 불러옵니다. ./app.cjs의 점과 슬래시는 현재 테스트 파일 옆의 앱을 뜻합니다. module.exports = {createApp}는 다른 파일이 사용할 이름을 공개합니다. Cannot find module 오류가 나면 설치를 먼저 시도하지 않고 파일 이름과 상대 경로를 봅니다. 이 ZIP에는 필요한 코드와 Node.js 표준 모듈만 있습니다.

생성과 저장의 연결

createApp 안의 Map은 id를 키로 하고 자료 객체를 값으로 저장합니다. nextId는 1부터 시작합니다. POST에서 본문을 검사한 뒤 id, 정리된 title, content, 고정 owner로 자료를 만듭니다. starter의 TODO는 생성한 item을 records에 넣는 부분입니다. records.set(item.id, item)을 구현해야 다음 조회가 같은 자료를 찾습니다. 응답만 만드는 것과 상태를 저장하는 것은 별개의 동작입니다.

자료 객체에는 owner가 demo-alice로 고정됩니다. 이것은 합성 자료의 모양을 미리 준비한 값이며 사용자가 로그인했다는 뜻이 아닙니다. 이 앱에는 사용자별 세션과 접근 권한 검사가 아직 없습니다. 여러 사용자에게 공개하지 않고 합성 자료만 사용합니다. 후속 인증·인가 모듈에서 요청 주체와 소유자를 연결할 예정이므로 이 레슨에서 임의의 실제 계정을 추가하지 않습니다.

잘못된 입력은 id를 소비하기 전에 반환합니다. 공백뿐인 제목을 거절한 다음 정상 생성했을 때 첫 id가 1인지 검사하면 이 순서를 확인할 수 있습니다. content는 문자열이어야 하지만 빈 문자열은 허용합니다. 제목은 trim으로 앞뒤 공백을 정리하고 content는 그대로 보관합니다. 어떤 필드를 정리하는지는 테스트에 명시하여 개발자가 임의로 데이터를 바꾸지 않게 합니다.

조회와 경계 사례

조회 경로는 양의 정수 id를 포함한 /documents/숫자 형식입니다. /documents/01이나 쿼리 문자열이 붙은 경로는 현재 계약에서 404입니다. server.cjs는 req.url을 그대로 전달하므로 경로와 쿼리를 분리하는 기능은 아직 없습니다. 더 넓은 라우팅 기능이 필요하면 그 계약과 테스트를 함께 추가합니다. 이번 레슨의 목적은 모든 HTTP 경로를 처리하는 서버를 만드는 것이 아닙니다.

조회 결과와 생성 결과에는 객체 복사본을 담습니다. 테스트 코드가 응답의 title을 바꾸어도 Map의 원본이 바뀌지 않게 합니다. 현재 자료 값은 문자열과 숫자뿐이므로 얕은 복사로 이 사례를 다룰 수 있습니다. 나중에 중첩 객체를 넣으면 복사와 변경 규칙을 다시 검토해야 합니다. 응답을 사용한 호출자가 저장 상태를 직접 바꾸면 테스트와 실제 API의 동작이 달라질 수 있습니다.

createApp을 다시 호출하면 새 Map을 만듭니다. 서로 다른 테스트가 자료를 공유하지 않게 하는 구조입니다. 이 저장소는 메모리에 있으므로 프로세스 재시작 후 유지되지 않습니다. “저장했다”는 표현은 이 앱 인스턴스의 메모리에 넣었다는 뜻으로 한정합니다. 파일이나 데이터베이스에 보관하는 영속 저장은 후속 모듈에서 설계합니다. 현재 구현으로 실제 개인 자료를 보관하지 않습니다.

테스트가 알려 주는 범위

node:test와 node:assert/strict를 사용합니다. test는 사례 이름과 실행 함수를 등록하고 assert.equal은 값 하나, assert.deepEqual은 객체 구조를 비교합니다. create then read 사례에서 201이 통과하고 200이 실패하면 생성 응답은 만들었지만 조회에 연결할 상태가 없다는 단서를 얻습니다. assertion 오류의 expected와 actual을 읽어 먼저 차이가 난 상태 코드와 필드를 찾습니다.

여섯 테스트는 건강 확인, 생성 후 조회, 연속 id, 입력 거절, 경로 경계, 상태 격리와 응답 변경을 확인합니다. 테스트를 통과시키려고 조회가 항상 200을 반환하게 만들면 없는 자료 검사에 실패합니다. 모든 자료를 id 1에 저장하면 연속 생성 검사나 본문 비교가 실패합니다. 오류를 하나씩 없앨 때 정상 사례와 거절 사례가 함께 유지되는지 확인합니다.

실행과 HTTP의 차이

node demo.cjs는 네트워크를 열지 않고 handle을 호출해 JSON 결과를 출력합니다. 이 결과는 요청 처리 함수의 증거입니다. DNS, TCP 연결, HTTP 헤더, 리스닝 주소가 실제로 작동했다는 증거는 아닙니다. server.cjs를 본인 환경에서 실행한다면 화면의 루프백 주소와 응답 상태를 따로 확인합니다. 이 레슨의 출력은 직접 실행한 함수 호출 결과로 제한합니다.

HTTP 어댑터는 127.0.0.1:8765에 바인딩하고 JSON 본문을 읽습니다. 요청 크기는 수집 중 4096바이트를 넘으면 거절하며 잘못된 JSON은 400을 반환하도록 준비되어 있습니다. 이 코드를 전체 서비스의 안전성 보장으로 소개하지 않습니다. 인증, 동시 요청 제어, 자료 총량, 영속 저장은 남은 과제입니다. node:http의 일반적인 연결 처리는 더 읽기에서 확인하고 현재 레슨에서는 생성·조회 계약에 집중합니다.

수정 전후를 남기기

먼저 starter에서 bash check.sh를 실행해 create then read 실패를 확인합니다. 다음에 TODO 한 곳을 구현하고 같은 명령으로 여섯 사례가 통과하는지 봅니다. 출력에는 실행 시간처럼 환경에 따라 변하는 값도 있으므로 따라하기에서는 고정된 demo 결과를 비교합니다. 보고서에는 테스트 수, 실패한 사례 이름, 수정한 상태 저장 동작을 적어 변경 이유가 드러나게 합니다.

테스트 전체 성공은 현재 계약에 적은 사례가 통과했다는 의미입니다. 인증이 없는 상태에서 로그인 성공이라고 쓰거나 함수 호출만으로 HTTP 정상이라고 기록하지 않습니다. 다음 담당자에게는 app.cjs와 app.test.cjs를 함께 넘기고 메모리 저장과 고정 owner라는 제한을 적습니다. 기능을 작게 완성하되 어떤 검증이 빠졌는지 설명할 수 있는 것이 신입 개발자와 보안 점검자 모두에게 필요한 기준입니다.

테스트 API는 Node.js 테스트 문서, 비교 동작은 assert 문서를 근거로 합니다.

따라하기

실패 위치 찾기

starter ZIP 루트에서 bash check.sh를 실행합니다. create then read의 actual 404와 expected 200을 비교합니다. 여섯 사례 중 health·unique ids·reject blank·unknown paths는 통과하고 두 사례가 실패하도록 준비되어 있습니다. node --version도 기록합니다. 출력 시간과 버전은 본인 실행값으로 남깁니다.

객체와 Map 저장 연결

const records = new Map();
const item={id:1,title:"합성 메모"};
records.set(item.id,item);
console.log(JSON.stringify(records.get(1)));
console.log(records.has(9));

실행 결과

{"id":1,"title":"합성 메모"}
false

TODO 구현과 회귀 검사

app.cjs의 TODO에 records.set(item.id, item);을 넣습니다. bash check.sh를 다시 실행해 tests 6, pass 6, fail 0과 마지막 PASS: app을 확인합니다. 실행 시간은 비교 기준이 아닙니다.

최초 요청 기록

node demo.cjs를 실행합니다. 세 JSON 줄의 method·path·status·body를 기록하고 POST 자료와 GET 자료의 id·title·content·owner가 같은지 비교합니다. 이 실행은 함수 호출이며 HTTP 소켓 연결을 증명하지 않습니다.

실행 결과

{"method":"POST","path":"/documents","status":201,"body":{"id":1,"title":"합성 메모","content":"demo only","owner":"demo-alice"}}
{"method":"GET","path":"/documents/1","status":200,"body":{"id":1,"title":"합성 메모","content":"demo only","owner":"demo-alice"}}
{"method":"GET","path":"/documents/9","status":404,"body":{"error":"not_found"}}

확인 문제

실습

starter를 풀고 bash check.sh로 두 실패 사례를 확인합니다. app.cjs에서 생성 item을 records Map에 저장하는 TODO를 완성합니다. 제목 검증, id 증가, 없는 자료 거절을 유지합니다. 여섯 테스트가 통과하고 node demo.cjs의 생성·조회 자료가 일치하면 완료입니다. 이 단계는 네트워크 없는 요청 처리 테스트입니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

node:test 6개 통과, fail 0, 마지막 PASS: app

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 취약점 수정 전후의 테스트 내용을 설명합니다.
  • 요청 처리 함수 테스트와 실제 HTTP 테스트의 차이는 무엇인가요?