셀렉터 자가학습 버그 수정과 Rate Limit
목차
셀렉터 자가학습 쪽 Codex 검증 피드백을 반영하는 작업이었다. 수정 범위는 내부 클래스 하나였는데, 이런 케이스가 항상 그렇듯 "작아 보인다"는 느낌이 오히려 함정이다. 내부 클래스는 외부에서 직접 호출되지 않기 때문에 문제가 생겨도 표면에 드러나기까지 시간이 걸린다. 증상이 전혀 다른 곳에서 나타나서 추적이 번거로워지는 유형.
재현부터 시작했다. 재현이 안 되면 고칠 수가 없고, 재현이 됐다는 건 원인 범위를 좁힐 수 있다는 뜻이다. 로그를 먼저 훑고 스택 트레이스 패턴을 확인했다. 원인 파악 → 최소 범위 수정 → 배포. 이 순서가 루틴처럼 잡혀가는 게 팀 입장에서는 긍정적인 신호다. 과거엔 "일단 다 뜯어보자"는 식으로 접근해서 수정 범위가 커지고 사이드 이펙트가 따라오는 경우가 있었는데, 지금은 최소 범위를 먼저 찾고 거기서 시작하는 쪽으로 정착됐다.
자주 나오는 버그 패턴
운영하면서 반복적으로 만나는 버그 유형이 어느 정도 수렴된다. 이번 케이스도 아래 패턴 중 하나였다.
| 패턴 | 증상 |
|---|---|
| null 체크 순서 오류 | NPE (NullPointerException) |
| 경로 예외 처리 누락 | 필터에서 정상 요청 차단 |
| YAML 공백 혼용 | 설정값 파싱 실패 |
| import 누락 | 컴파일 에러 |
null 체크 순서는 특히 체이닝이 길어질수록 놓치기 쉽다. a.getB().getC().getValue() 같은 구조에서 중간이 null이면 NPE가 터지는데, 코드 리뷰 때 눈에 잘 안 들어오는 게 현실이다. 내부 클래스에서 이런 패턴이 있었고, Codex 검증 피드백에서 잡혔다. 검증 단계에서 잡힌 게 다행이지, 운영 트래픽 받으면서 터졌으면 추적 비용이 훨씬 높았을 거다.
수정 자체는 크지 않았지만 이게 쌓이는 게 중요하다. 큰 장애는 대부분 작은 버그들이 누적된 상태에서 어느 임계점을 넘어서 터지는 구조다. 작은 수정들을 빠르게 처리하는 리듬이 잡혀 있으면 그 임계점 자체가 높아진다.
Rate Limit 운영 실제
Rate Limit을 잘못 잡으면 두 가지 방향으로 실패한다. 너무 타이트하면 정상 사용자가 차단당하고, 너무 느슨하면 공격 트래픽을 걸러내지 못한다. 둘 다 나쁘지만 전자가 체감 피해가 더 직접적이다.
Redis 슬라이딩 윈도우 방식을 쓰는 이유는 고정 윈도우의 경계 버스팅 문제 때문이다. 고정 윈도우에서 1분당 100개 제한을 걸면, 00:59에 100개 + 01:00에 100개, 이렇게 2초 안에 200개 요청이 통과할 수 있다. 슬라이딩 윈도우는 이걸 막는다.
# 슬라이딩 윈도우 예시 (Redis ZSET 기반)
def is_allowed(user_id: str, limit: int, window_sec: int) -> bool:
now = time.time()
key = f"rate:{user_id}"
pipe = redis.pipeline()
pipe.zremrangebyscore(key, 0, now - window_sec)
pipe.zcard(key)
pipe.zadd(key, {str(now): now})
pipe.expire(key, window_sec)
_, count, _, _ = pipe.execute()
return count < limit
다만 Redis가 단일 장애점이 되는 건 별도로 챙겨야 한다. Rate Limit 처리 중 Redis가 응답이 없으면 어떻게 할 건지 정책이 필요하다. 실패 개방(fail-open, 허용)과 실패 폐쇄(fail-closed, 차단) 중 선택해야 하는데, 서비스 특성에 따라 다르다. 일반적인 웹 서비스라면 Redis 다운 시 정상 트래픽 차단이 더 나쁜 결과이므로 fail-open을 쓰고 알림만 빠르게 가도록 한다. 연결 풀 크기와 타임아웃 설정은 기본 중의 기본이고, WAIT 커맨드나 복제 설정을 같이 고려해야 하는 상황도 있다.
블랙리스트 자동 등록 구조
공격성 요청을 감지하고 수동으로 대응하는 구조는 속도가 안 나온다. 탐지에서 차단까지 사람이 개입하는 시간 동안 요청은 계속 들어온다. DB에 자동으로 등록하고 다음 요청부터 즉시 차단하는 흐름이 실시간 대응에 더 맞다.
자동 등록 설계에서 놓치면 안 되는 지점이 세 가지다.
- 오탐 처리: 로드 테스트, 배치 작업, 모니터링 헬스체크 같은 내부 트래픽이 봇으로 분류되지 않도록 IP/헤더 기반 예외 목록을 관리해야 한다. 이걸 빠뜨리면 배포 당일에 자기 서비스를 자기가 막는 상황이 생긴다.
- TTL 설정: 영구 차단 대신 만료 시간을 둬야 한다. 오탐이 발생했을 때 자동으로 복구되는 경로가 있어야 하고, 차단된 사용자가 일정 시간 후 재시도할 수 있는 구조가 사용자 경험에도 낫다.
- 알림 연동: 자동 차단이 발생할 때 슬랙이나 디스코드로 즉시 알림이 가야 "지금 이상 트래픽이 잡히고 있다"는 걸 실시간으로 인지할 수 있다. 자동화가 잘 되어 있어도 사람이 맥락을 파악하고 있어야 한다. 알림 없는 자동화는 블랙박스가 된다.
작업 규모로 보면 크지 않은 하루였는데, 셀렉터 쪽 안정성 확보 + Rate Limit 구조 정리가 같이 들어가면서 운영 레이어가 한 층 두꺼워진 느낌이다. 이런 날들이 쌓이는 거다.
댓글 0
첫 댓글 달아줘.