slowlp
← 블로그
방법 2026.06.22 · 3분 읽기

처리할 수 있는 예외만 잡는다: 예외 처리 전략

복구 가능한 예외만 잡고 못 하면 위로 넘기는, 계층별 예외 처리 전략 정리

방법

복구 가능하면 잡고, 못 하면 위로 넘긴다

코드를 짜다 보면 빨간 줄이 무섭다. 그래서 처음엔 어디든 일이 터질 것 같은 곳마다 try...except를 둘렀다. 외부 API 부르는 곳, DB 조회하는 곳, 값 꺼내 쓰는 곳. 일단 감싸 놓으면 마음이 편하니까. 그런데 그렇게 깔아 놓은 except 블록들이 나중에 발목을 잡았다. 에러가 나긴 났는데 어디서 무슨 일이 있었는지 추적이 안 됐다. except가 예외를 조용히 삼켜 버린 거다.

그래서 규칙을 하나 세웠다. 각 함수는 자기가 책임질 수 있는 예외만 잡는다. 책임질 수 없는 건 잡지 말고 그대로 위로 흘려보낸다. 이걸 책임의 원칙이라고 부르기로 했다.

잡아도 되는 예외: 내가 복구할 수 있는 것

기준은 단순하다. “이걸 잡으면 내가 뭘 해 줄 수 있나?” 답이 있으면 잡는다. 대표적인 게 외부 API 호출이다. 네트워크는 가끔 흔들리니까, 한 번 실패했다고 끝내지 말고 몇 번 더 시도해 볼 수 있다. 이런 건 그 함수가 스스로 대처 가능한 상황이다.

MAX_RETRIES = 3

async def fetch_external_data(url: str):
    for attempt in range(MAX_RETRIES):
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(url)
                response.raise_for_status()
                return response.json()
        except httpx.RequestError as e:
            print(f"호출 실패 (시도 {attempt + 1}/{MAX_RETRIES}): {e}")
            if attempt == MAX_RETRIES - 1:
                raise AppException("외부 호출에 최종 실패했습니다.")

재시도하다가 마지막까지 실패하면? 이제 이 함수가 할 수 있는 게 없다. 그래서 잡고 있던 손을 놓고 raise로 위에 넘긴다. 끝까지 책임지는 척하지 않는 게 핵심이다.

안 잡는 예외: 내가 복구할 수 없는 것

반대 경우가 더 많다. 게시물을 찾으러 갔는데 없다. 이건 그 함수가 어찌해 볼 도리가 없는 상황이다. 없는 걸 만들어 낼 수도 없고, 기본값으로 때울 수도 없다. 그러면 잡지 않고 그냥 던진다.

async def get_post_by_id(post_id: int, session: Session) -> Post:
    post = await post_repository.get(post_id, session)
    if not post:
        raise NotFoundException("게시물을 찾을 수 없습니다.")
    return post

여기서 try...except로 감싸고 싶은 충동이 생기는데, 참아야 한다. 잡아 봤자 할 수 있는 게 없는 예외를 잡는 건 그냥 에러를 숨기는 짓이다. 이런 도메인 예외는 위로 자연스럽게 전파되도록 둔다.

마지막에 받아 주는 곳

그러면 위로 던진 예외는 누가 받나. 전역 핸들러 한 곳이 받는다. 서비스든 라우터든 처리 안 하고 올려보낸 예외를 여기서 전부 가로채, 클라이언트가 알아먹을 표준 형식으로 바꿔 준다.

핸들러는 두 종류다. AppException을 상속한 도메인 예외(NotFoundException은 404, ForbiddenException은 403처럼)면 그 예외가 들고 있는 상태 코드와 메시지를 그대로 응답에 싣는다. 그 외 예상 못 한 일반 예외는 전부 500으로 묶고, 운영 환경에서는 내부 정보를 마스킹한다. 클라이언트한테 스택 트레이스를 보여 줄 이유는 없으니까.

덕분에 서비스 코드는 깔끔해진다. 응답 포맷을 매번 신경 쓸 필요 없이, “여기서 못 다루는 일이다” 싶으면 의미에 맞는 예외를 raise하면 끝이다. 나머지는 한 곳이 알아서 한다.

정리하면

예외 처리는 결국 책임 분배 문제였다. 복구할 수 있으면 그 자리에서 잡아 복구하고, 못 하면 정직하게 위로 넘긴다. except 블록을 줄였더니 오히려 에러 추적이 쉬워졌다. 무서워서 다 감싸던 때보다, 잡을 것만 잡는 지금이 훨씬 마음 편하다.

관련 프로젝트
주식앱 (마이폴리오) LAB
개별 종목 분석 없이 포트폴리오 단위로 투자하는 앱 — KIS 계좌 연동 실주문까지, 본인 계좌 전용으로 운영 중.
보기 →
댓글