기술

로그인 자동 풀림의 두 원인 — refresh 미사용과 외부 4xx 오인 매핑

2026.06.067분 읽기

어떤 PM 도구에서 사용자 보고가 들어왔습니다. "로그인이 자꾸 풀린다"는 내용이었습니다. 이런 신호는 원인이 한 군데가 아닌 경우가 많아서, 증상만 보고 토큰 만료 시간을 늘리는 식으로 대응하면 진짜 원인을 놓칩니다.

파보니 원인은 두 갈래였습니다. 하나는 예상 가능한 것이었고, 다른 하나는 훨씬 교활했습니다.

1.jpeg


첫 번째 원인은 예상대로였습니다

access 토큰이 24시간 뒤 만료되는데, refresh 흐름이 실제로 동작하지 않고 있었습니다. 토큰이 만료되면 곧바로 로그인이 풀렸습니다. refresh 토큰은 발급되는데, 401이 떨어졌을 때 그걸로 토큰을 갱신하고 원래 요청을 다시 보내는 흐름이 비어 있던 셈입니다.

이건 표준 패턴으로 메우면 됩니다. 401이 오면 refresh를 한 번 시도하고, 성공하면 원 요청을 재시도하면 됩니다. 문제는 두 번째 원인이었습니다.


두 번째 원인이 더 교활했습니다

이 도구는 외부 연동 API를 호출합니다. 외부 캘린더 같은 서비스입니다. 그런데 이 외부 API가 인증 만료로 4xx(401)를 돌려줄 때, 그 응답을 우리 서비스의 인증 만료(401)로 그대로 매핑하던 흔적이 있었습니다.

결과적으로 외부 서비스의 토큰이 만료됐을 뿐인데, 사용자 입장에서는 우리 서비스 로그인이 풀린 것처럼 보였습니다. 외부의 문제가 내부의 인증 만료 신호로 둔갑한 겁니다.

이게 교활한 이유는, 우리 인증은 멀쩡한데도 로그아웃이 발생한다는 점입니다. 토큰 만료 시간을 아무리 늘려도 이 경로로는 계속 풀립니다. 외부와 내부의 경계를 흐리면 이런 오진이 생깁니다. 전에 백엔드를 번역 계층으로 둬야 하는 이유를 정리한 적이 있는데, 같은 원칙이 여기서도 적용됐습니다.


401을 어떻게 처리할지, 세 가지 길이 있었습니다

401 응답을 다루는 방법은 세 가지였습니다.

1번은 401이면 무조건 로그아웃하는 방식입니다. 안전하지만 사용자 마찰이 큽니다. refresh 흐름이 아예 없습니다.

2번은 401이면 refresh를 한 번 시도하는 표준 패턴입니다. 다만 refresh 요청 자체가 또 401을 받으면 무한 루프에 빠질 위험이 있습니다.

3번은 여기에 가드를 더 얹는 방식입니다. refresh 1회 시도에 더해, 외부 4xx를 우리 인증 만료와 분리하고, 동시에 쏟아지는 401에 대한 race 방어까지 넣습니다. 3번을 골랐습니다.

2.jpeg


가드는 백엔드와 프런트에 나눠 넣었습니다

백엔드에는 세 가지를 넣었습니다.

먼저 JWT에 토큰 용도를 구분하는 type claim을 추가했습니다. access 토큰으로 refresh를 시도하는 오용을 막기 위해서입니다. 다음으로 refresh 전용 엔드포인트를 만들어 토큰 검증, 회전(rotation), 계정 상태 확인을 함께 처리했습니다. 마지막으로 외부 인증 만료 전용 에러 코드를 따로 만들었습니다. 상태 코드는 502를 썼습니다. 외부 게이트웨이 너머의 문제라는 의미를 살리면, 외부 인증 만료가 우리 401과 섞이지 않습니다.

프런트엔드는 axios 인터셉터를 다시 썼습니다. 401이 오면 refresh를 시도하고, 성공하면 원 요청을 재시도하고, refresh까지 실패할 때만 로그아웃합니다. 여기에 동시에 여러 요청이 401을 받을 때를 대비해, refresh 호출을 하나의 promise로 공유했습니다. 그래야 토큰 회전이 동시 호출로 깨지지 않습니다. 무한 루프는 세 겹으로 막았습니다. refresh 요청 자체가 401이면 바로 로그아웃, 한 요청당 재시도는 1회로 제한, refresh 엔드포인트 호출 자체의 401도 별도 처리입니다.

동시성과 안정성을 위해 가드를 겹쳐 까는 접근은, 가드의 적정 강도를 위협 모델링으로 정한 경험과 같은 맥락입니다.


외부의 401과 우리의 401은 다릅니다

이 디버깅에서 핵심은 진단을 두 갈래로 나눈 것이었습니다. 로그인이 풀린다는 같은 증상이라도, 내부 토큰 흐름의 문제인지 외부 API 응답의 오인 매핑인지는 완전히 다른 문제입니다.

특히 외부 API의 401을 우리 인증 만료로 매핑하면 안 됩니다. 외부 서비스의 토큰이 만료된 것과, 우리 서비스의 세션이 끝난 것은 사용자에게 줘야 할 신호가 다릅니다. 별도 에러 코드로 분리해야 잘못된 로그아웃을 막을 수 있습니다.

토큰 용도를 type claim으로 나누는 것, 동시 다발 401에 refresh promise를 공유해 race를 막는 것, 무한 루프를 여러 겹으로 막는 것은 모두 표준 패턴에 현실의 빈틈을 메우는 작업이었습니다. refresh 1회 시도라는 표준만으로는 동시성과 외부 오인이라는 현실을 다 막지 못합니다.


점검 체크리스트

로그인 자동 풀림을 다룰 때 다음을 점검하면 진짜 원인을 좁힐 수 있습니다.

증상(로그인 풀림)의 원인을 내부 토큰 흐름과 외부 API 오인 매핑 두 갈래로 나눠 봤는가

외부 API의 4xx를 우리 인증 만료와 별도 에러 코드로 분리했는가

access 토큰으로 refresh를 시도하는 오용을 type claim 등으로 막았는가

동시 다발 401에 refresh 호출이 하나로 수렴하는가 (race 방어)

refresh 자체의 401과 재시도 횟수로 무한 루프를 막았는가


증상이 같아도 원인은 다릅니다

로그인이 풀린다는 신호 하나에 원인은 둘이었고, 그중 외부 API 오인 매핑은 토큰 시간을 늘려도 풀리지 않는 종류였습니다. 표준 refresh 패턴을 넣는 것으로 절반을 메웠고, 외부 4xx를 별도 코드로 분리하는 것으로 나머지 절반을 잡았습니다. 같은 증상이라도 원인을 갈라 보는 것이, 잘못된 대증요법을 피하는 출발점이었습니다.

#JWT#refresh토큰#axios인터셉터#에러매핑#외부API#인증#운영의기술