“null이라도 오류가 나지 않게 했습니다.” AI가 반환한 수정 코드는 단 한 줄이었습니다. 실제로 null을 전달해도 에러가 발생하지 않는 것을 보고 그대로 적용하고 싶다는 생각이 들었습니다.
하지만 이번 사례는 에러를 제거하는 대신, 일반 사용자까지 허용해 버립니다. 수정되기를 바랐던 입력으로 성공하더라도, 지켜야 할 거부 조건이 남아있지 않을 수 있습니다. 차이가 작다고 해서 안심할 수는 없는 예입니다.
단순화된 인가 함수(authorization function)를 사용하여 수정 대상과 유지해야 할 동작을 동일한 입력 표로 비교합니다. 목표는 AI의 설명을 읽고 끝내는 것이 아니라, 조건식을 자신의 언어로 되돌리고, 적용할 수 없는 수정을 구체적인 입력을 통해 보여주는 것입니다.
JavaScript 함수와 Git 차이(diff)를 읽을 수 있는 분들을 대상으로 합니다. 2026년 11월 11일에 macOS 및 Node.js v26.8.1에서 게시 코드를 재실행하여 다음 출력을 확인했습니다. 소재는 가상 데이터를 사용하는 로컬 함수이며, 실제 운영용 인증/인가 시스템은 아닙니다.
원래의 사양은 role이 admin인 사용자만 허용하는 것이었습니다. 문제는 user가 null일 경우 user.role을 읽지 못해 예외(exception)가 발생한다는 점입니다.
여기서 수정 요청을 ‘예외를 제거’하는 것만으로 한다면, 사양의 절반이 빠지게 됩니다. 이번에 필요한 상태는 다음과 같습니다.
| 입력 | 기대 결과 | 역할 |
|---|
{role:'admin'} | true | 허용하던 사람은 계속 허용한다 |
{role:'member'} | false | 거부하던 사람은 계속 거부한다 |
{} | false | 권한 정보가 없으면 허용하지 않는다 |
| null / undefined | false | 이번에 고치고 싶은 예외를, 거부로 취급한다 |
null 행만 통과한다고 해서 이 표 전체를 만족하는 것은 아닙니다. 변경 리뷰에서는 수정하고 싶은 증상 옆에, 망가뜨려서는 안 되는 동작을 나열합니다.
원래 코드와 제안 사례의 차이(diff)는 다음 한 줄입니다.
- return user.role === 'admin';
+ return Boolean(user);
원래 식은 “role이 admin과 일치하는가”였습니다. 제안 사례는 “user가 truthy한 값인가”로 바뀌었습니다. JavaScript에서는 빈 객체도 truthy하기 때문에, member나 권한 정보가 없는 객체도 true가 됩니다.
예외가 사라진 이유는 null로부터 속성(property)을 읽지 않게 되었기 때문입니다. 하지만 동시에, role을 보는 것 자체를 포기하고 있습니다. “null에 대응했다”는 설명만으로는 이 두 번째 변경은 보이지 않습니다.
수정판의 user?.role === 'admin'
은 user가 null이거나 undefined이면 role의 획득을 중단하고, 결과인 undefined와 admin을 비교하여 false가 됩니다. 이번 사양에서는 허용 조건을 유지한 채 결손(missing data)에 대응할 수 있습니다.
새로운 연습 폴더에 다음 내용을 review.mjs로 저장하고, node review.mjs를 실행합니다. 추가 패키지나 외부 계정은 필요하지 않습니다.
import assert from 'node:assert/strict';
const before = user => user.role === 'admin';
const proposed = user => Boolean(user);
...
실제 결과입니다.
$ node review.mjs
admin: proposed=true, fixed=true, expected=true
member: proposed=true, fixed=false, expected=false
...
assert 중 하나라도 기대와 다르다면, AssertionError가 표시되며 0이 아닌 종료 코드로 멈춥니다. 마지막 PASS 줄까지 나오면, 모든 검사를 통과했다는 의미입니다.
주목해야 할 것은 member와 empty 행입니다. 제안 사례는 true이고, 수정판과 기대값은 false입니다. null의 예외를 재현했을 뿐만 아니라, “수정으로 인해 이전의 거부가 망가졌다”는 것을 관찰할 수 있습니다.
마지막 PASS는 위험한 제안 사례를 채택해도 좋다는 의미가 아닙니다. 원래의 결함을 재현하고, 제안 사례의 회귀(regression)를 검출했으며, 수정판의 5가지 케이스를 확인했다는 교재적인 검사 결과입니다.
리뷰할 변경이 크더라도, 처음에 하나의 조건식으로 좁힐 수 있습니다. 변경 전에는 무엇을 판별했는지, 변경 후에는 무엇을 판별하는지 한 문장씩 적어보고, 두 답변이 다른 입력을 찾아봅니다.
이번 경우라면 'role이 admin'과 '객체가 존재하는 것'은 다르기 때문에, member나 빈 객체가 후보가 됩니다. 상한 체크의 경우라면, 상한에 딱 맞는 입력값과 초과하는 입력값이 후보입니다. 미입력 대응의 경우라면, 공백 문자(empty string)와 0 등이 후보입니다. 입력 선택 방식이 변경된 의미를 도출해낼 수 있습니다.
Git에서는 git status --short로 대상 파일을 확인하고, 스테이징되지 않은 파일은 git diff --로, 스테이징된 파일은 git diff --cached --로 읽습니다. 추적되지 않는 신규 파일(status에서 ?? 줄)은 일반적인 diff에 나타나지 않기 때문에, 직접 열어서 확인해야 합니다.
git diff --check는 줄 끝 공백이나 충돌 마커(conflict marker)의 탐지에 유용하지만, 이번 권한 조건의 변화를 발견할 수는 없습니다(이번 한 줄 차이에도 종료 코드 0으로 아무것도 보고하지 않습니다). 형식 검사와 조건을 읽고 실행하는 검사는 역할이 다릅니다.
| 관찰된 결과 | 판단 및 다음 행동 |
|---|
| null은 수정되었으나 member가 true | 거부 조건의 회귀. 수정을 그대로 적용하지 않고, 허용 조건을 유지하는 변경으로 되돌린다 |
| ... | |
| 이 표는 구현에 적용할 때 조사 순서입니다. 실제 API 연결이나 인증 기반을 이번 로컬 실험에서 검증한 것은 아닙니다. | |
본 예시의 role은 신뢰할 수 있는 서버 측 사용자 정보로부터 얻었다는 전제입니다. 브라우저에서 전송된 role:'admin'을 그대로 믿는다면, 함수의 비교가 정확하더라도 권한(authorization)을 지킬 수 없습니다. 실제 서비스에서는 본인 확인과 서버 측 권한 정보를 기반으로 한 판정이 필요합니다.
이번에 가져가고 싶은 것은 수정 버전의 한 줄뿐만 아니라, 리뷰하는 방식입니다. 수정하고 싶은 실패 사례 하나와 유지하고 싶은 허용 및 거부 조건을 각각 작성한다. 이 세 가지를 같은 입력표로 보면, 수정 설명 속에 숨겨진 의미 변화를 구체적으로 지적할 수 있습니다.