로그인 상태 전이
75분 안팎
학습 목표
로그아웃·세션 만료 뒤 보호 자원 접근을 검증합니다.
개념
한 화면 대신 사건의 순서를 봅니다
로그인이 성공하는 사례만 확인하면 로그아웃한 세션이 계속 접근하는 문제를 놓칩니다. 같은 프로필 요청도 앞서 어떤 사건이 있었는지에 따라 기대 결과가 달라집니다. 상태 전이 설계는 현재 상태와 사건을 짝지어 다음 상태와 관찰 결과를 정합니다. QA는 화면 이동만 적기보다 인증 상태가 유지되는지 사라지는지 확인해, 사용자 흐름의 시간 순서를 테스트로 바꿉니다.
이번 계산 모델의 상태는 ANON, AUTH, EXPIRED입니다. ANON은 인증되지 않음, AUTH는 유효한 인증, EXPIRED는 한때 로그인했지만 세션이 만료된 상태입니다. ANON과 EXPIRED는 모두 보호 자원 접근을 거절하지만 원인과 이전 사건은 다릅니다. 결과가 같다는 이유로 두 상태를 하나로 뭉치면 만료 후 재로그인과 로그아웃 처리를 따로 확인하기 어렵습니다. 실제 세션의 저장 기술은 이 모듈에서 선택하지 않습니다.
사건과 접근을 구분합니다
사건은 LOGIN_OK, LOGIN_FAIL, LOGOUT, EXPIRE, ACCESS입니다. 계약 v2에서 LOGIN_OK는 어느 상태에서든 AUTH로 갑니다. LOGIN_FAIL은 상태를 유지합니다. LOGOUT은 어느 상태에서든 ANON으로 갑니다. EXPIRE는 AUTH만 EXPIRED로 바꾸며 ANON과 EXPIRED에서는 그대로입니다. ACCESS는 현재 상태를 바꾸지 않고 AUTH에서만 ALLOW, 나머지는 DENY를 출력합니다.
LOGIN_FAIL이 AUTH에서 상태를 유지한다는 규칙은 이미 유효한 세션을 둔 채 실패한 추가 인증 시도를 넣는 계산 모델의 약속입니다. 기존 REQ-06은 로그아웃 상태에서 틀린 비밀번호를 넣는 경우이므로 모순되지 않습니다. 실제 앱이 로그인 화면에서 어떤 재인증을 허용하는지는 별도 계약이 필요합니다. 이번 미션에서 앱 기능을 추측해서 보편적인 세션 정책으로 적지 않습니다.
ACCESS는 관찰 사건입니다. 접근 거절 자체가 다시 로그인하거나 만료를 해제해서는 안 됩니다. EXPIRED에서 ACCESS를 여러 번 보내도 EXPIRED DENY가 나와야 합니다. 만료 상태가 자동으로 AUTH로 돌아오는 구현을 잡으려면 한 번 거절하는 사례와 반복 거절하는 사례를 구분합니다. 다음 올바른 LOGIN_OK 이후 ACCESS는 AUTH ALLOW이므로 복구 경로도 함께 설계합니다.
전이표와 시퀀스로 설계합니다
행은 출발 상태, 열은 사건인 전이표를 그립니다. 각 칸에 다음 상태를 쓰고 ACCESS 칸에는 판정도 붙입니다. LOGIN_OK·LOGOUT처럼 여러 출발 상태에서 같은 도착 상태로 가는 사건도 출발 상태를 확인합니다. 이벤트를 한 번씩 사용했다고 전이를 모두 확인한 것은 아닙니다. AUTH의 EXPIRE와 ANON의 EXPIRE는 같은 이벤트 이름이지만 서로 다른 전이입니다.
시퀀스는 처음 상태부터 여러 사건을 순서대로 적용하는 사례입니다. ANON→LOGIN_OK→ACCESS→LOGOUT→ACCESS는 로그인 성공 뒤 접근 허용과 로그아웃 뒤 접근 거절을 함께 확인합니다. ANON→LOGIN_OK→EXPIRE→ACCESS→LOGIN_OK→ACCESS는 만료 뒤 차단과 재로그인 복구를 확인합니다. 마지막 결과만 적지 말고 사건마다 기대 상태를 적으면 잘못된 전이가 어디서 시작되었는지 찾기 쉽습니다.
사례를 독립적으로 실행하려면 시작 상태를 매번 ANON으로 초기화합니다. 앞 사례에서 로그인한 상태를 다음 사례가 물려받으면 LOGIN_FAIL 뒤 접근이 허용되어 예상과 다를 수 있습니다. 이 차이를 제품 결함으로 단정하기 전에 사전 조건을 확인합니다. 미션의 이벤트 배열도 한 사례마다 독립적인 ANON에서 출발하며 상태를 공유하지 않습니다. 뒤 데이터 격리 모듈에서 이 원칙을 실제 실행에 적용합니다.
시간을 대신하는 사건의 한계를 씁니다
EXPIRE라는 사건을 직접 넣는 계산 연습은 기다리지 않고 만료 상태의 기대값을 학습하기 위한 모델입니다. 이 실습으로 실제 세션 만료 시간이나 쿠키 제거 동작을 검증했다고 할 수 없습니다. 미션의 사전 조건에는 교육용 만료 사건을 주입한다고 명시합니다. 다음 앱 검증에서 실제 만료 조건과 재현 방법을 확보한 뒤 시간 경계나 요청 처리 시점 차이를 별도 사례로 확장합니다.
로그아웃 뒤 화면에서 이름이 사라졌다는 결과만으로 세션이 무효화되었다고 판단하지 않습니다. 보호 자원 재요청의 거절과 회원 데이터 무변경을 추가 관찰로 둡니다. 반대로 세션 만료 뒤 회원 자체가 삭제되어야 한다고 기대하지 않습니다. 인증 상태 변화와 회원 저장 상태는 서로 다른 축입니다. 미션 expected의 state에는 회원 데이터가 유지된다는 항목을 써 이 혼동을 줄입니다.
만료 후 거절 결과에 실제 API 코드나 특정 안내 문구를 임의로 쓰지 않습니다. 계약은 로그인 필요·접근 거절의 의미와 무변경만 정합니다. 화면 문구와 API 코드는 후속 모듈에서 확인할 세부사항입니다. 보류 중인 값을 빈 칸으로 두고 통과로 처리하기보다 review에 대상·질문·영향·다음 조치를 기록해 확정되지 않은 범위를 보이게 합니다.
실패한 줄에서 이전 사건을 찾습니다
브라우저 입력은 ["LOGIN_OK","EXPIRE","ACCESS"] 같은 JSON 사건 배열이고 시작은 ANON입니다. 매 사건마다 사건 이름, 처리 뒤 상태, 판정을 공백으로 구분해 한 줄 출력합니다. 접근 외의 사건에서는 판정 자리에 -를 씁니다. 빈 배열이면 출력이 없습니다. 지원하지 않는 사건은 입력 계약 밖이며 실제 앱 오류 응답 규칙을 뜻하지 않습니다. 출력과 미션의 시퀀스 표기를 같은 순서로 유지합니다.
starter는 EXPIRE를 처리하지 않아 만료 후에도 AUTH가 남습니다. ACCESS 줄만 보고 판정 비교를 고치면 앞선 상태 변화 오류가 그대로 남을 수 있습니다. 첫 불일치 줄이 EXPIRE AUTH -라면 전이 처리부터 수정합니다. JSON의 따옴표를 빠뜨리면 실행 오류이므로 사건 정책과 분리해 입력을 고칩니다. 현재 상태와 사건 두 값을 함께 읽는 습관이 결함 재현 절차에서도 유용합니다.
마지막 리뷰에서는 로그아웃·만료·실패 로그인·복구와 반복 접근이 포함되었는지 묻습니다. 사건 목록을 외우는 대신 한 사건을 삭제했을 때 어떤 위험이 미검증으로 남는지 설명합니다. 만료 후 ACCESS가 빠지면 인증 차단을, 재로그인이 빠지면 복구를 확인하지 못합니다. 상태 모델의 범위와 실제 서버 실행 전이라는 한계를 함께 설명하면 이 레슨의 목표를 충족합니다.
따라하기
로그아웃 흐름 계산
초기 ANON부터 사건을 실행하고 사건별 상태와 판정을 읽습니다.
let s='ANON';
for(const e of ['LOGIN_OK','ACCESS','LOGOUT','ACCESS']){if(e==='LOGIN_OK')s='AUTH';if(e==='LOGOUT')s='ANON';console.log(e,s,e==='ACCESS'?(s==='AUTH'?'ALLOW':'DENY'):'-');}실행 결과
LOGIN_OK AUTH - ACCESS AUTH ALLOW LOGOUT ANON - ACCESS ANON DENY
만료와 복구 계산
EXPIRE 이후 접근이 거절되고 재로그인 이후 허용되는지 아래 출력에서 확인합니다.
let s='ANON';
for(const e of ['LOGIN_OK','EXPIRE','ACCESS','LOGIN_OK','ACCESS']){if(e==='LOGIN_OK')s='AUTH';if(e==='EXPIRE'&&s==='AUTH')s='EXPIRED';console.log(e,s,e==='ACCESS'?(s==='AUTH'?'ALLOW':'DENY'):'-');}실행 결과
LOGIN_OK AUTH - EXPIRE EXPIRED - ACCESS EXPIRED DENY LOGIN_OK AUTH - ACCESS AUTH ALLOW
실패 로그인과 반복 접근
실패 로그인은 ANON을 유지하고 접근이 상태를 바꾸지 않는지 실행합니다.
let s='ANON';for(const e of ['LOGIN_FAIL','ACCESS','ACCESS']) console.log(e,s,e==='ACCESS'?'DENY':'-');실행 결과
LOGIN_FAIL ANON - ACCESS ANON DENY ACCESS ANON DENY
시퀀스를 미션에 연결
각 state-transition 사례를 ANON에서 시작하도록 precondition을 적고 input.events와 expected.trace를 채웁니다. 세션 변화와 회원 데이터 무변경은 별도로 설명합니다.
확인 문제
실습
JSON 사건 배열을 읽고 ANON에서 시작합니다. LOGIN_OK→AUTH, LOGOUT→ANON, AUTH에서 EXPIRE→EXPIRED이며 그 밖에는 유지합니다. LOGIN_FAIL과 ACCESS도 상태를 유지합니다. 사건마다 사건명 상태 판정을 출력하며 ACCESS는 AUTH에서 ALLOW, 나머지 DENY입니다. 접근 외 판정은 -입니다. 빈 배열에는 출력하지 않습니다.
모범 답안
const fs=require('fs');
const events=JSON.parse(fs.readFileSync(0,'utf8'));
let state='ANON';
for(const event of events){
if(event==='LOGIN_OK')state='AUTH';
if(event==='LOGOUT')state='ANON';
if(event==='EXPIRE'&&state==='AUTH')state='EXPIRED';
console.log(event,state,event==='ACCESS'?(state==='AUTH'?'ALLOW':'DENY'):'-');
}
더 읽기
면접 질문
- 가입 폼의 테스트 조건을 정하는 과정을 설명해 주시면 됩니다.