Mbtigram / 11 /

The login was fine

"로그인이 안 돼요"의 진짜 원인

로그인 요청은 200이었음. 그런데 사용자는 계속 로그인 화면으로 돌아갔음. 부가 기능의 작은 실패가 로그아웃으로 번진 과정과, 그 밑에서 풀리지 않고 남아 있던 SQLite 잠금 이야기.

로그인은 성공하고 있었음

어느 날 운영 서버에서 "로그인이 안 된다"는 증상이 났음. 서버 로그를 보니 로그인 요청은 200, 성공이었고 토큰도 정상으로 나갔음.

문제는 그다음 요청이었음. 로그인 직후 화면은 "내 정보"(GET /api/me)를 불러오는데, 이게 500을 냈음. 화면은 내 정보를 못 불러오면 토큰을 지우고 로그인 화면으로 돌려보내게 되어 있었음. 사용자 눈에는 로그인이 안 되는 걸로 보였지만, 실제로는 로그인한 직후에 화면이 스스로 로그아웃하고 있었음.

500은 접속 시각 기록에서 났음

내 정보를 불러올 때 서버는 그 김에 "마지막 접속 시각"을 갱신함. 그 쓰기가 데이터베이스 잠금에 걸려 실패했고, 그 때문에 요청 전체가 500이 됐음.

정리하면, 있으면 좋은 정도의 부가 기능(접속 시각 기록)이 실패했고, 그게 요청을 실패시켰고, 화면은 그걸 "세션이 끝났다"로 받아들였음. 서버의 사소한 오류 하나가 로그아웃으로 이어진 것임.

잠금이 안 풀린 이유

잠금 자체는 흔한 일임. SQLite는 쓰기를 한 번에 하나씩만 받아서, 동시에 쓰려는 요청이 있으면 잠깐 기다림. 그런데 이번 잠금은 서버를 다시 켜기 전까지 풀리지 않았음.

원인은 파이썬 sqlite3 모듈의 기본값에 있었음.

  1. 기본 설정(isolation_level="")에서는 쓰기 문장 앞에 트랜잭션이 자동으로 열림.
  2. 잠금 대기 때문에 commit()이 실패하면 그 트랜잭션은 열린 채 남음.
  3. 데이터베이스 연결은 스레드마다 만들어 두고 계속 다시 씀. 그래서 열린 트랜잭션을 들고 있는 연결이 그대로 재사용됨.

그 결과 한 번의 일시적인 경합이 잠금을 쥔 채 굳어 버렸음. 다른 요청들은 그 잠금을 기다리다 실패했음.

고친 곳 세 군데

  • 근본 원인: 연결을 만들 때 isolation_level=None, 즉 자동 커밋으로 바꿨음. 자동으로 열리는 트랜잭션이 없으니 열린 채 남을 것도 없음. 여러 문장을 꼭 한 번에 묶어야 하는 곳(표를 다시 만드는 작업)만 BEGIN; … COMMIT;으로 따로 묶었음.
  • 부가 기능 실패: 접속 시각 갱신이 실패해도 내 정보 요청은 그대로 성공하게 감쌌음. 접속 시각이 한 번 안 찍혀도 문제 될 건 없음.
  • 화면의 로그아웃 조건: 401 · 403일 때만 로그아웃하게 바꿨음. 500은 "서버가 아프다"는 뜻이고 "세션이 끝났다"는 뜻이 아님. 둘을 구분하지 않으면 서버 오류가 전부 로그아웃으로 이어짐.

"로그인이 안 된다"를 로그인 코드에서만 찾았으면 한참 헤맸을 듯함. SQLite는 이 뒤에도 한동안 잘 썼음. 쓰기가 많아질 걸 대비해 PostgreSQL로 옮긴 건 열흘쯤 뒤의 일임.