에이전트가 전체 함수를 try/catch로 감싸고 에러 핸들링을 호출하는 패턴
요약
에이전트 개발 시 흔히 발생하는 `try/catch` 패턴의 문제점을 지적합니다. 단순히 에러를 포착하여 로그로 남기거나 `None`을 반환하는 것은 근본적인 실패를 해결하지 못하며, 실제 장애 발생 시 경고 알림이나 적절한 전파가 차단될 수 있습니다.
핵심 포인트
- 에러 핸들링은 단순 포착이 아닌, 예상되는 실패와 호출자의 대응 방안 결정이 핵심입니다.
- 모든 예외를 잡는 것은 문제를 해결하는 것이 아니라, 장애 정보를 숨기는 행위일 수 있습니다.
- 예외 처리는 '무엇을 잡을지'보다 '어떻게 전파할지(re-raise)'에 초점을 맞춰야 합니다.
- AI 에이전트에게도 포괄적 예외 처리 대신 실패 모드 추론을 강제하는 프롬프팅 기법이 필요합니다.
모든 에이전트 PR에서 나타나는 패턴
당신은 스택 트레이스(stack trace)를 당신의 에이전트에 붙여넣습니다. 20초 후, 다음과 같은 diff가 돌아옵니다:
def get_user_profile(user_id):
try:
row = db.query(SQL, user_id)
...
스택 트레이스가 멈춥니다. 테스트는 통과합니다. PR 설명에는 "에러 핸들링을 추가했다"고 쓰여 있습니다.
2주 후, 사용자가 빈 프로필 페이지를 보고한다는 보고가 들어옵니다. 로그에는 그날의 에러 라인 하나만 있고, 다른 것은 아무것도 없습니다. 호출자(caller)는 반환 값을 확인하지 않았기 때문에 None이 다운스트림으로 전달되었고, 이 실패는 두 단계 떨어진 곳에서 렌더링 에러로 표면화되었습니다. 원래 데이터베이스 예외는 온콜 담당자 누구에게도 전파되지 못했습니다.
try/catch가 실제로 해결한 것
개발자의 화면에 있는 스택 트레이스를 조용히 만들었을 뿐입니다. 실패를 해결하지는 않았습니다.
데이터베이스는 여전히 실패했습니다. 사용자는 여전히 프로필을 받지 못했습니다. 온콜 엔지니어는 경고 알림(alert)을 받을 수 없었습니다. 왜냐하면 에러가 INFO 레벨로 한 번 로깅되었고 요청이 200으로 반환되었기 때문입니다.
호출을 try/catch로 감싸고 신호 값(sentinel value)을 반환하는 것은 에러 핸들링이 아닙니다. 그것은 뚜껑을 열어둔 채 실패를 가두는 것입니다. 예외 자체는 여전히 존재하지만, 그것을 고칠 수 있는 사람에게 전파되는 것만 막았을 뿐입니다.
진정한 에러 핸들링이 결정해야 할 것
포착된 모든 예외(caught exception)는 세 가지 질문에 답해야 하며, 광범위한 except Exception은 그 어떤 것도 답하지 못합니다:
- 이 실패가 이 계층에서 예상되는 것인가? 만약 데이터베이스가 다운되었다면, 그것을 흡수하는 것이
get_user_profile함수의 문제가 아닙니다. 호출자와 재시도(retry) 계층이 봐야 할 500 에러입니다. - 대신 호출자가 무엇을 받아야 하는가? 다운스트림으로 전달되는
None은 던져진 예외보다 더 나쁩니다. 왜냐하면 버그를 한 단계 떨어진 곳으로 옮기기 때문입니다. - 누구에게 페이지 알림(paged)이 가야 하는가? 경고 알림 없이 로깅된 에러 라인은 아무 데도 가지 않습니다. 프레임워크가 적절한 레벨로 로그하도록 다시 발생시키거나(re-raise), 인간의 개입을 요구하는 곳으로 신호를 보내야 합니다.
실제로 예상하는 특정 예외(행을 찾을 수 없음, 유효성 검사 오류 등)에 대한 좁은 except 블록과 나머지 모든 것에 대한 재발행(re-raise)이 일반적으로 올바른 형태입니다. 또한 이는 에이전트가 피해야 하는 형태인데, 왜냐하면 시스템에서 어떤 예외가 예상되는지 알아야 하기 때문이며, 이는 스택 트레이스만으로는 추론할 수 없는 것이기 때문입니다.
결정을 강제하는 프롬프트
에이전트에게 "오류 처리를 추가해 달라"고 요청하면 일반적인 패턴을 얻게 됩니다. 대신 다음과 같이 요청하십시오.
이 함수에 대해, 이 계층에서 예상되는 특정 예외 목록을 작성해 주세요. 각각의 경우 호출자가 무엇을 받아야 하는지, 그리고 사람이 알림을 받아야 하는지 명시해 주세요. 그 외의 모든 경우는 재발행(re-raise)하세요. 호출자가 이미
None을 처리하는 경우가 아니라면None을 반환하지 마세요.
이것은 에이전트가 포괄적인 예외 처리를 찾기보다 실패 모드에 대해 추론하도록 강제합니다. 여전히 스택 트레이스를 멈추게 하지만, 재발행된 예외들은 계속 전파되어 이를 처리할 방법을 아는 계층으로 전달됩니다.
찾아봐야 할 검토상의 이상 징후 (Review Smell)
PR에서 try/catch를 볼 때, 첫 번째 질문은 "컴파일되는가?"가 아닙니다. 그것은 다음과 같습니다.
- 실제로 어떤 예외를 잡고 있는가?
- 대신 호출자에게 무엇을 돌려주는가?
- 로그 라인 이후에 오류는 어떻게 되는가?
이 중 어느 하나라도 답이 "아무것도 없다"라면, 해당 try/catch 블록은 시스템을 더 안전하게 만드는 것이 아니라 오히려 악화시키고 있는 것입니다. 원래의 스택 트레이스는 기능(feature)이었습니다. 그것은 시스템이 버그가 어디에 있는지 알려주는 신호였기 때문입니다. 이를 무시하는 것은 실패를 제거하지 않으면서 신호를 제거하는 행위입니다.
API 측면에서도 동일한 본능이 나타난다
에이전트들은 HTTP 경계(HTTP boundary)에서도 비슷한 행동을 하는 경향이 있습니다. 핸들러를 광범위한 예외로 감싸고, 빈 본문과 함께 200 응답 코드를 반환하며, 이를 "우아한 저하(graceful degradation)"라고 부릅니다. 클라이언트는 성공적인 응답을 받고, 모니터링 시스템은 아무것도 감지하지 못하며, 클라이언트가 기대하는 것과 서버가 반환하는 것 사이의 계약 불일치(contract drift)는 사용자가 보고하기 전까지 발견되지 않습니다.
저희가 로컬에서 선택한 더 저렴한 방어책은 에이전트가 변경 사항을 호출하기 전에 스펙(spec)을 라이브 엔드포인트에 재실행하는 것입니다. 잘못된 형태를 가진 200 응답은 검사를 통과하지 못하게 하여, 조용히 'API가 성공적으로 반환되었다'고 되는 것을 막아줍니다. 이것이 좁은 예외 처리(narrow-exception) 습관을 대체하는 것은 아니며, 같은 실패 모드의 다른 절반을 잡아내는 것입니다.
내일 변경할 것들
다음번에 에이전트가 새로운 try/catch와 함께 PR을 열면, 위에서 언급한 세 가지 질문을 하도록 요청하세요. 만약 답변하지 못한다면, 그 캐치(catch)는 너무 광범위합니다. 진정한 오류 처리는 해당 계층에서 실패가 무엇을 의미하는지에 대한 결정이지, 전체 함수를 감싸는 것이 아닙니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기