Skip to content
isdnetworks
Go back

플래그 하나를 두 기능이 쓴다

(수정됨)

잠금이 걸린 회원의 로그인을 허용하는 변경이 배포됐다. 검증하다가 잠금 규모가 궁금해졌다.

Table of contents

Open Table of contents

증상 — 잠금이 수천 계정

먼저 규모를 셌다.

SELECT COUNT(*), COUNT(DISTINCT lock_reason) FROM members WHERE is_locked = 1;
-- 수천 계정 / lock_reason 은 대부분 NULL

is_locked 가 켜진 것이 수천 계정이었고 lock_reason 은 대부분 비어 있었다.

이 is_locked 가 무엇인지 처음에는 블랙리스트 차단으로 봤다. 봄에 수사 협조로 그런 기능을 붙인 것을 알고 있어서 그쪽으로 읽었는데 지적을 받았다.

원인 — 두 기능이 같은 컬럼을 썼다

실제 감사 로그 문구를 대조했다.

SELECT action, COUNT(*) FROM audit_logs
 WHERE target_type = 'member' AND target_id IN (...) GROUP BY action;

action 이 두 종류로 나왔다.

'출금 잠금 설정'
'블랙리스트 등록'

같은 is_locked 인데 audit_logs 에 남은 문구가 다르다.

코드를 보니 서로 다른 두 어드민 기능이 같은 컬럼을 쓰고 있었다.

// 블랙리스트 등록 (수사 협조·침해 시도 대응)
POST /admin/members/blacklist
  → $member->is_locked = 1;

// 출금 잠금 (의심 입금·본인 요청)
POST /admin/members/withdraw-lock-batch
  → $member->is_locked = 1;

blacklist 와 withdraw-lock-batch 가 같은 자리에 값을 쓴다.

blacklist 화면에서 차단하면 값이 1이 되고 withdraw-lock-batch 에서 잠금을 풀면 그 값이 0이 된다. 두 기능을 쓰는 사람이 다르면 서로 모른 채로 상대의 상태를 밟는다.

영향 범위 — 게이트가 넓었다

이 잠금이 무엇을 막는지 지점을 전수로 찾았다.

로그인 (2곳: 웹·API 키 인증)
출금 생성
주문 생성
API 키 발급

출금 생성 만 막는 것이 아니라 로그인 과 주문 생성 까지 막고 있었다.

그러면 출금만 잠그려던 회원이 문제가 된다. 로그인을 해도 튕기고 주문도 못 넣으니 사실상 계정 동결이다.

검증 — 위험 조합 수백 계정

잠긴 것 전부가 위험한 것은 아니었다.

잠금 상태
+ 최근 30일 로그인 시도
+ 잔액 보유

이미 탈퇴한 것과 잔액이 0 인 것은 동결이어도 영향이 없다.

셋이 겹치는 것만 세었다.

잠금만                    수천 계정
+ 최근 시도 + 잔액 보유   수백 계정

최근 시도 와 잔액 조건으로 좁히니 실제 대상이 수천에서 수백으로 갈렸다.

숫자를 세는 것이 이 조사에서 결정적이었다. 잠긴 계정이 좀 있다는 말과 수백 명이 잔액을 둔 채 로그인을 못 하고 있다는 말은 같은 사실인데 무게가 다르다.

결과 — 절반의 해소와 반대쪽의 개방

배포된 변경을 다시 봤다.

차단 경로 4개 중
  로그인 2개          →  열림 (출금 잠금 회원, 블랙리스트 회원 둘 다)
  주문·API 키 발급    →  그대로

네 경로 중 로그인 둘만 열렸고 주문 생성 과 API 키 발급 은 그대로였다.

열린 쪽이 더 문제였는데, 게이트가 is_locked 하나를 보니 출금 잠금 회원을 위해 연 문으로 블랙리스트 회원도 들어온다. 그 자리에서 되돌리는 것 대신 로그인 게이트에 lock_reason 조건을 덧대는 쪽을 골랐다. 이유는 되돌리면 수백 계정이 다시 동결되고 조건을 덧대면 두 문제가 같이 닫혀서다.

수백 계정은 여전히 주문을 못 넣는다는 뜻이라 그것을 별도로 보고했다. 부분 수정이면 무엇이 남았는지를 따로 적어야 다음 사람이 다 된 것으로 읽지 않는다.

대응 — 컬럼을 나누는 제안

이 컬럼을 다루는 작업에는 규칙을 남겼다.

두 기능이 같은 플래그를 공유한다
한 화면의 토글이 다른 화면의 상태를 뒤집는다
한쪽을 위해 게이트를 열면 다른 쪽도 열린다

값만 보면 어느 기능이 켠 것인지 알 수 없어서 audit_logs 의 문구를 봐야 한다.

근본은 컬럼을 나누는 것이었다.

is_blacklisted       블랙리스트 차단
is_withdraw_locked   출금 잠금

is_blacklisted 와 is_withdraw_locked 로 갈라 두면 서로를 뒤집을 자리가 없어진다.

컬럼을 더하는 것 자체는 무겁지 않다 — MySQL 5.7 참고 매뉴얼의 온라인 DDL 표는 컬럼 추가를 in-place 로, 동시 DML 허용으로 적는다(자동 증가 컬럼만 예외). 무거운 것은 is_locked 를 읽는 자리를 전부 고치는 쪽이라 별도 제안으로 적었다. 감사 로그가 남아 있어 지금은 구분이 되지만 로그가 없었으면 어느 기능이 켠 값인지 영영 몰랐을 것이다.

정리


Share this post on:

Previous Post
상태를 바꾸는 게터
Next Post
없는 레코드를 영원히 기다린다