본문 바로가기

Coding, Testing, Challenge

대시보드가 멀쩡해 보이는데 영영 갱신이 안 됐다, 파이썬 조용한 except 잡는 법

 

갱신이 멈춘 대시보드와 조용한 except

오류가 나는 화면은 고치기 쉽습니다. 빨간 글씨가 어디가 문제인지 알려 주니까요.

정말 곤란한 건 아무 오류 없이 멀쩡해 보이는 화면입니다.

이번 포스팅에서는 AI 작업 대시보드가 겉으로는 정상인데 실시간 갱신만 영영 멈춰 있던 일, 그리고 그걸 로그 한 줄로 잡아낸 과정을 풀어 보도록 하겠습니다.

DB 가 바뀌면 화면이 따라 바뀌는 구조였습니다

Claude Code 기억 서버의 상태를 보는 대시보드를 만들었습니다. FastAPI 와 Jinja 템플릿으로 만든, 빌드 과정 없는 작은 화면입니다.

실시간 갱신은 PostgreSQL 의 LISTEN/NOTIFY 를 썼습니다.

DB 에 변경 → NOTIFY events
대시보드 서버가 LISTEN events 로 받음
→ SSE(EventSource) 로 열린 브라우저에 밀어 줌
→ 화면 일부만 다시 그림

할 일 상태가 바뀌면 새로고침 없이 화면이 따라 바뀌어야 했습니다.

화면은 떴는데 아무것도 바뀌지 않았습니다

실시간 갱신이 멈춘 자리

2026-09-18, Windows PC 에서 대시보드를 띄웠습니다.

첫 화면은 정상으로 나왔습니다. 표도, 카드도, 숫자도 다 맞았습니다.

그런데 DB 에서 상태를 바꿔도 화면이 그대로였습니다.

서버 콘솔에도 오류가 없었습니다.

범인은 말없이 잠만 자던 except 였습니다

LISTEN 을 돌리는 부분을 줄여 옮기면 원래 이런 모양이었습니다.

while True:
    try:
        await listen_and_forward()
    except Exception:
        await asyncio.sleep(5)    # 실패하면 5초 쉬고 다시

연결이 잠깐 끊겨도 죽지 않게 하려고 넣은 재시도였습니다.

그런데 이 except 는 실패를 어디에도 적지 않았습니다. 그래서 LISTEN 이 처음부터 단 한 번도 성공하지 못하고 5초마다 실패를 반복하는 동안, 겉으로는 아무 일도 없어 보였습니다.

except 에 로그 한 줄을 넣자마자 원인이 나왔습니다.

제가 가장 쓰게 느낀 대목입니다. 죽지 않게 만들겠다고 넣은 코드가, 죽었다는 사실까지 숨기고 있었습니다.

⚠️ "실패해도 계속 간다" 와 "실패를 말하지 않는다" 는 다른 것입니다. 앞의 것은 설계이고, 뒤의 것은 사고를 숨기는 장치입니다.

로그가 알려 준 원인: 이벤트 루프 종류

로그에 찍힌 것은 psycopg 비동기 연결의 InterfaceError 였습니다.

Windows 의 기본 이벤트 루프(ProactorEventLoop)에서는 psycopg 비동기 드라이버가 동작하지 않았던 것입니다.

이벤트 루프 정책을 바꿔 보는 방법도 있었습니다. 그런데 정책을 바꿔도 uvicorn 이 서버를 띄우면서 제 루프를 다시 깔아 버려서 소용이 없었습니다.

그래서 비동기를 버리고 스레드로 바꿨습니다

비동기 대신 스레드와 로그로 고친 전후

LISTEN 은 결국 알림이 올 때까지 기다리는 블로킹 한 줄입니다. 비동기일 필요가 없었습니다.

def listen(profile, dsn, loop):
    while True:
        try:
            with psycopg.connect(dsn, autocommit=True) as c:   # 동기 드라이버
                c.execute("LISTEN events")
                log.info("LISTEN 시작 %s", profile)
                for n in c.notifies():
                    loop.call_soon_threadsafe(push, n.payload)
        except Exception as e:
            log.error("LISTEN 실패 %s: %s", profile, e)     # 조용히 넘어가지 않는다
        time.sleep(5)

동기 psycopg 를 데몬 스레드에서 돌리고, 받은 알림만 이벤트 루프 쪽으로 넘깁니다. 이러면 어느 운영체제의 어느 루프에서도 같게 돕니다.

재시도는 그대로 두었습니다. 다만 이제 실패할 때마다 몇 번째 실패인지 로그에 남고, 다시 붙으면 "복구" 라고 남깁니다.

같은 성질의 실패가 계속 보였습니다

이 일 뒤로 코드에서 except: pass 나 말없이 넘어가는 재시도를 보면 먼저 멈춰서 봅니다.

"화면은 멀쩡한데 결과가 안 바뀐다", "테스트는 초록인데 동작이 다르다" 같은 증상은 대부분 어딘가에서 실패가 삼켜진 경우였습니다.

이 대시보드도 AI 에이전트와 함께 짠 코드였습니다. "죽지 않게" 라는 요구는 예외를 잡고 조용히 넘어가는 코드로 이어지기 쉽습니다.

조용한 실패를 막는 체크리스트

1. except 안에는 최소한 로그 한 줄을 둡니다. 무엇이, 몇 번째로 실패했는지.

2. 재시도 루프에는 "다시 붙었다" 는 로그도 둡니다. 실패와 복구가 짝으로 보여야 합니다.

3. 비동기 드라이버를 쓸 때는 운영체제별 이벤트 루프 차이를 먼저 확인합니다.

4. 기다리기만 하는 블로킹 작업은 스레드로 충분한지 따져 봅니다.

5. 화면이 "떴다" 와 "갱신된다" 를 따로 확인합니다. 첫 화면이 맞다고 실시간이 도는 것은 아닙니다.

6. AI 가 짠 코드에서 넓은 except 를 보면 무엇을 삼키는지 먼저 묻습니다.

겉으로 멀쩡한 것이 가장 오래 숨어 있었습니다. 실패를 막는 것보다 실패가 보이게 하는 쪽이 먼저였습니다.

지금 쓰고 계신 코드에 말없이 넘어가는 except 가 몇 개나 있는지, 한번 검색해 보시는 건 어떨까요?

그러면 오늘도 모두 스테이블 하세요.


만든 것들