TIL/Claude Code

[TIL][Python] except Exception이 코드 버그를 "요소를 못 찾았다"로 위장시킨다

아람2 2026. 4. 12. 16:59
반응형

Page Object에서 이런 코드를 발견했다

def click_save_button(self):
    try:
        button = self.page.locator(self.locators.SAVE_BUTTON)
        button.clikc()  # ← 오타! click이 아니라 clikc
    except Exception:
        logger.warning("저장 버튼을 찾을 수 없음")
        return None

button.clikc()AttributeError를 발생시킨다

except Exception이 이걸 잡아서 "저장 버튼을 찾을 수 없음"으로 로깅한다

로그만 보면 로케이터 문제 같다

실제로는 오타다

이 문제가 왜 오래 숨어 있었는가

except Exception은 Python의 거의 모든 예외를 잡는다

Playwright 에러만 잡으려고 작성한 코드가 코드 자체의 버그도 같이 삼킨다

잡으려는 예외 같이 잡히는 예외
`PlaywrightTimeoutError` (요소 대기 실패) `AttributeError` (오타, 없는 속성)
`PlaywrightError` (브라우저 연결 끊김) `NameError` (정의 안 된 변수)
  `TypeError` (잘못된 인자 전달)
  `KeyError` (없는 딕셔너리 키)

디버깅 난이도가 급격히 올라간다

코드 버그인데 UI 문제로 보이니까 로케이터를 바꿔보고, 대기 시간을 늘려보고, MCP snapshot을 찍어본다

원인은 오타 한 글자인데 2시간을 쓸 수 있다

해결: 예외 범위를 Playwright로 한정

from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import Error as PlaywrightError

def click_save_button(self):
    try:
        button = self.page.locator(self.locators.SAVE_BUTTON)
        button.click()
    except PlaywrightTimeoutError:
        logger.warning("저장 버튼 대기 시간 초과")
        return None
    except PlaywrightError as e:
        logger.warning(f"Playwright 오류: {e}")
        return None

AttributeError는 잡히지 않는다

오타가 있으면 스택 트레이스가 즉시 터진다

바로 수정할 수 있다

9개 파일 전수 교체한 과정

30곳 이상의 except Exception을 한 번에 교체해야 했다

한 번에 하면 중간에 깨지는 코드가 생길 수 있다

2단계로 나눴다

Step 1: ElementNotFoundError 시그니처 유연화

기존:

class ElementNotFoundError(Exception):
    def __init__(self, element_name: str, locator_used: str):
        # locator_used가 필수 → 단일 메시지로 호출 불가

변경:

class ElementNotFoundError(Exception):
    def __init__(self, element_name: str, locator_used: str = ""):
        if locator_used:
            message = f"요소 '{element_name}' 찾기 실패 (locator: {locator_used})"
        else:
            message = element_name  # 단순 메시지로 사용

이제 raise ElementNotFoundError("단순 메시지")로도 호출할 수 있다

기존 2인자 호출도 그대로 동작한다

Step 2: 실제 교체

# Before
raise Exception("프로젝트를 찾을 수 없습니다")

# After
raise ElementNotFoundError("프로젝트를 찾을 수 없습니다")
# Before
except Exception:
    pass

# After
except (PlaywrightTimeoutError, PlaywrightError):
    pass

이 순서가 중요하다

Step 1 없이 Step 2만 하면, ElementNotFoundError("메시지") 호출에서 TypeError: __init__() missing 1 required positional argument 가 발생한다

부수 효과: 모달 dismiss 통합

except Exception 교체 작업 중에 모달 dismiss 코드가 3곳에 흩어져 있는 걸 발견했다

# 페이지 이동마다 이 3줄이 반복
self.dismiss_free_trial_modal()
self.dismiss_feature_release_modal()
self.close_tutorial_overlay()

한 메서드로 통합했다

def ensure_no_blocking_overlay(self) -> None:
    self.dismiss_free_trial_modal()
    self.dismiss_feature_release_modal()
    self.close_tutorial_overlay()

각 메서드는 모달이 없으면 즉시 반환하므로 성능 영향이 거의 없다

체크리스트

항목 확인
`except Exception` 대신 구체적 예외 타입을 사용하는가
`except (PlaywrightTimeoutError, PlaywrightError)`로 범위를 한정하는가
`raise Exception` 대신 도메인 예외(`ElementNotFoundError`)를 사용하는가
대규모 시그니처 변경은 2단계(인터페이스 확장 → 호출부 교체)로 나누는가

except Exception은 편하다

하지만 코드 버그를 숨겨서 디버깅 비용을 10배로 만든다

반응형