개발 slecs

비밀번호 초기화 시 계정 잠금 자동 해제 버그 수정

목차

member/password-reset SQL 매퍼 하나를 수정했는데, 고치고 나서 생각보다 많은 걸 다시 떠올리게 된 작업이었다.

증상은 단순했다. 비밀번호 초기화 플로우를 타면 LOCKED 상태인 계정의 잠금이 자동으로 풀려야 하는데, 안 풀리고 있었다. 초기화 자체는 성공 응답이 나오는데 계정 상태는 여전히 잠금 그대로. 사용자 입장에선 비밀번호는 바꿨는데 로그인이 여전히 안 되는 황당한 상황이다. 직접 재현해보면 금방 확인되는 버그인데, 어느 시점에 어떻게 생겼는지가 문제였다.

왜 이런 일이 생기나 - 계정 상태 머신의 함정

계정 잠금은 보통 상태 머신처럼 동작한다. ACTIVE, LOCKED, DORMANT 같은 상태가 있고 각 전환에 조건이 붙는다. 문제는 이 전환 로직이 여러 진입 경로에 퍼져 있을 때 생긴다.

비밀번호 초기화 경로가 하나만 있으면 괜찮다. 근데 실제론 이메일 링크 경유, 관리자 강제 초기화, 본인 인증 후 초기화처럼 경로가 여럿이고, 각 경로가 서로 다른 서비스 레이어나 매퍼를 탄다. 그러면 "비밀번호 바꿀 때 잠금도 같이 해제"라는 로직이 A 경로엔 있고 B 경로엔 없는 상황이 자연스럽게 생긴다. 처음 만든 사람은 신경 썼는데, 나중에 경로 하나 추가하면서 빠진 것이다. 아무도 의도적으로 뺀 게 아니라, 그냥 기억 밖으로 빠진 것.

이번 케이스도 비슷한 구조였다. 잠금 획득 경로가 코드 흐름에 따라 달라지면서 특정 상황에선 교착 상태가 발생하거나 계정 상태 갱신 자체가 누락됐다. SQL 쪽에서 조건이 의도와 다르게 걸려 있었고, 집계 처리 방식도 엣지 케이스에서 오작동했다.

원인 파악하면서 늘 느끼는 건, 버그 자체보다 "왜 이 코드가 이 모양이 됐나"를 추적하는 게 더 시간이 걸린다는 점이다. SQL 몇 줄 고치는 건 금방인데, 그 SQL이 어느 맥락에서 어떤 의도로 쓰이는지 파악하는 게 시간의 대부분을 잡아먹는다. 히스토리가 없으면 더하다.

수정 방향 - SQL 조건/집계 정리

변경 파일은 매퍼 하나. 핵심은 두 가지였다.

첫째, 비밀번호 초기화 업데이트 쿼리에 잠금 해제 조건을 명시적으로 추가했다. 기존엔 비밀번호 컬럼만 갱신하고 상태 컬럼은 건드리지 않았다.

-- 수정 전 (의사 코드)
UPDATE member
SET    password   = #{newPassword},
       updated_at = NOW()
WHERE  member_id  = #{memberId}

-- 수정 후
UPDATE member
SET    password   = #{newPassword},
       status     = CASE WHEN status = 'LOCKED' THEN 'ACTIVE' ELSE status END,
       lock_count = 0,
       updated_at = NOW()
WHERE  member_id  = #{memberId}

CASE WHEN으로 LOCKED일 때만 ACTIVE로 바꾸고, 그 외 상태는 손대지 않는다. DORMANTSUSPENDED 같은 다른 상태는 비밀번호 초기화로 건드리면 안 되기 때문에 이 방식이 안전하다. 무조건 ACTIVE로 세팅하는 것보다 훨씬 방어적이다.

둘째, 잠금 카운트 집계 쿼리의 조건을 정리했다. 기존 쿼리가 특정 상황에서 집계 범위를 잘못 잡고 있었는데, 수정 전후로 실제 데이터로 직접 확인하면서 진행했다.

수정 프로세스 - 증상 픽스가 아니라 원인 픽스

버그 고칠 때 습관적으로 돌아보는 체크가 있다.

체크 항목 이유
같은 로직이 다른 경로에도 있는지 한 곳만 고치면 다른 경로에서 같은 버그 재발
수정이 기존 정상 케이스를 깨지 않는지 고치면서 다른 거 망가뜨리는 게 최악
해당 화면/API에서 실제 동작 확인 "될 것 같다"는 착각이 생각보다 잦음
관련 화면과 숫자 cross-check 집계 버그는 특히 다른 화면이 감시 역할을 함

이번에도 비밀번호 초기화 경로가 이 매퍼 하나만 타는지 먼저 확인했다. 다른 경로가 있으면 거기도 같은 처리가 필요한지 봐야 하고, 위험한 패턴이 보이면 함께 수정하는 게 맞다. 다행히 이번엔 해당 매퍼 수정만으로 충분했다.

수정 후엔 버그를 직접 재현해서 정상 동작을 확인했다. LOCKED 상태 계정으로 비밀번호 초기화 진행 → 계정 상태 ACTIVE 전환 여부 → 로그인 가능 여부. 이 흐름을 손으로 밟아보는 게 귀찮아도 생략하면 안 된다. 코드 보고 "이러면 될 것 같은데"는 자주 틀린다. 특히 상태 전환처럼 사이드 이펙트가 있는 로직일수록 그렇다.

작업 후 남은 생각

사내 서비스를 키우다 보면 기능 하나가 화면 버튼 하나로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자한테 이상한 화면이 나타난다. 계정/인증 도메인은 특히 잠금 상태 하나가 사용자 경험에 직결되기 때문에 꼼꼼함이 기본값이어야 한다.

"대충 맞는 것 같다"로 넘기면 나중에 반드시 다시 돌아온다. 이건 경험으로 배우는 거라 머리로 아는 것과 체감하는 게 다르다. 고치고 나면 항상 "진작 꼼꼼히 볼걸" 싶다.

개발 습관 측면에서 계속 지키려는 것들:

  • 변경 전 현재 동작 수치나 스크린샷 메모해두기
  • 수정 후 동일 케이스로 재확인
  • 관련 화면이 있으면 숫자 cross-check
  • 커밋 메시지엔 "무엇을" 보다 "왜"를 담으려 노력
  • 논리적으로 독립된 단위로 커밋을 쪼개는 습관 유지 - 문제 생겼을 때 어느 변경에서 깨진 건지 찾기 훨씬 수월해짐

엣지 케이스 하나하나 따지는 게 느려 보여도, 같은 버그로 다시 오는 시간 비용이 훨씬 크다. 이번 수정 자체는 SQL 매퍼 하나였는데, 원인 파악하고 재발 경로 확인하고 검증하는 데 쓴 시간이 훨씬 길었다. 그래도 이 과정을 건너뛰고 "일단 증상만 픽스"했으면 다른 경로에서 같은 문제가 다시 올라왔을 것이다. 결국 그게 더 비싸다.

댓글 0

첫 댓글 달아줘.