시도 / 캐치 / 로그 / 다시 던지기-안티 패턴입니까?


19

중앙 위치 또는 프로세스 경계에서 예외 처리의 중요성이 try / catch 주위의 모든 코드 블록을 흩 뜨리지 않고 모범 사례로 강조된 여러 게시물을 볼 수 있습니다. 나는 대부분의 사람들이 그것의 중요성을 이해한다고 생각하지만 사람들은 여전히 ​​예외 상황에서 문제 해결을 쉽게하기 위해 더 많은 컨텍스트 특정 정보를 기록하기를 원하기 때문에 catch-log-rethrow 안티 패턴으로 끝나는 것을 볼 수 있습니다. 전달) 방법은 try / catch / log / rethrow 주위에 메소드를 래핑하는 것입니다.

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

예외 처리 모범 사례를 계속 유지하면서 이것을 달성하는 올바른 방법이 있습니까? PostSharp와 같은 AOP 프레임 워크에 대해 들었지만 이러한 AOP 프레임 워크와 관련된 단점이나 주요 성능 비용이 있는지 알고 싶습니다.

감사!


6
try / catch에서 각 메소드를 랩핑하고 예외를 기록하고 코드가 변경되도록하는 것에는 큰 차이가 있습니다. 추가 정보를 사용하여 예외를 시도 / 잡아 다시 던집니다. 첫 번째는 끔찍한 연습입니다. 두 번째는 디버깅 경험을 향상시키는 완벽한 방법입니다.
Euphoric

나는 각 방법을 시도 / 포착하고 단순히 catch 블록에 로그인하고 다시 던지라고 말하고 있습니다.
rahulaga_dev

2
아몬이 지적한대로. 언어에 스택 추적이 있으면 각 catch에 로그인하는 것이 의미가 없습니다. 그러나 예외를 래핑하고 추가 정보를 추가하는 것이 좋습니다.
Euphoric

1
@Liath의 답변을 참조하십시오. 내가 준 대답은 거의 그를 반영 할 것입니다 : 가능한 한 빨리 예외를 잡으십시오. 그 단계에서 할 수있는 모든 것이 유용한 정보를 기록하는 것이라면 그렇게하고 다시 던지십시오. 이것을 반 패턴으로 보는 것은 무의미합니다.
David Arno

1
Liath : 작은 코드 스 니펫이 추가되었습니다. 저는 C #
rahulaga_dev를 사용하고 있습니다.

답변:


19

문제는 로컬 catch 블록이 아니며 log 및 rethrow 입니다. 예외를 처리하거나 컨텍스트를 추가하고 새 예외를 추가하는 새 예외로 랩하십시오. 그렇지 않으면 동일한 예외에 대해 여러 개의 중복 로그 항목이 표시됩니다.

여기서 아이디어는 응용 프로그램을 디버깅하는 기능을 향상시키는 것입니다.

예 # 1 : 처리

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

예외를 처리하는 경우 예외 로그 항목의 중요도를 쉽게 다운 그레이드 할 수 있으며 해당 예외를 체인으로 확장 할 이유가 없습니다. 이미 다루었습니다.

예외 처리에는 문제가 발생했음을 사용자에게 알리거나 이벤트를 기록하거나 단순히 무시하는 것이 포함될 수 있습니다.

참고 : 의도적으로 예외를 무시하는 경우 빈 catch 절에 이유를 명확하게 나타내는 주석을 제공하는 것이 좋습니다. 이것은 미래의 관리자에게 실수 나 게으른 프로그래밍이 아니라는 것을 알려줍니다. 예:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

예제 # 2 : 추가 컨텍스트 추가 및 던지기

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

구문 분석 코드의 줄 번호 및 파일 이름과 같은 추가 컨텍스트를 추가하면 입력 파일을 디버깅하는 기능을 향상시키는 데 도움이 될 수 있습니다 (문제가 있다고 가정). 이것은 일종의 특별한 경우이므로 "ApplicationException"에서 예외를 다시 랩핑하여 브랜드를 변경하는 것만으로는 디버그에 도움이되지 않습니다. 추가 정보를 추가하십시오.

예 3 : 예외를 제외하고 아무 것도하지 마십시오

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

이 마지막 경우, 예외를 건드리지 않고 그대로 두십시오. 가장 바깥 쪽 레이어의 예외 처리기는 로깅을 처리 할 수 ​​있습니다. 이 finally절은 메소드에 필요한 모든 자원을 정리하는 데 사용되지만 예외가 발생했다는 것을 기록하는 장소는 아닙니다.


" 문제는 로컬 캐치 블록이 아닙니다. 문제는 로그와 다시 던지기입니다. " 그러나 결국 그것은 try / catch가 모든 방법에 흩어져있는 것이 괜찮다는 것을 의미합니다. 모든 방법을 사용하는 대신이 방법을 신중하게 준수 할 수 있도록 지침이 있어야한다고 생각합니다.
rahulaga_dev

나는 대답에 지침을 제공했다. 이것이 귀하의 질문에 대답하지 않습니까?
Berin Loritsch

@rahulaga_dev 나는 지침 /은 글 머리 기호가 없다고 생각 하므로이 문제는 컨텍스트에 따라 크게 달라지기 때문에이 문제를 해결하십시오. 예외를 처리 할 위치 또는 재발견시기를 알려주는 일반 지침은 없습니다. 내가 볼 수있는 유일한 지침 인 IMO는 로깅 / 처리를 최신 시간으로 연기하고 재사용 가능한 코드에 로깅하지 않도록하여 불필요한 종속성을 생성하지 않는 것입니다. 자신의 방식으로 처리 할 수있는 기회를주지 않고 사물을 기록한 경우 (예 : 처리 된 예외) 코드 사용자는 너무 즐겁지 않습니다. 그냥 내 두 센트 :)
andreee

7

로컬 캐치가 안티 패턴이라고 생각하지 않습니다. 실제로 올바르게 기억하면 실제로 Java로 적용됩니다!

오류 처리를 구현할 때 가장 중요한 것은 전체 전략입니다. 서비스 경계에서 모든 예외를 포착하는 필터를 원할 수도 있고 수동으로 예외를 가로 채기를 원할 수도 있습니다. 팀 전체의 코딩 표준에 해당하는 전체 전략이있는 한 둘 다 괜찮습니다.

개인적으로 나는 다음 중 하나를 수행 할 수있을 때 함수 내부에서 오류를 포착하고 싶습니다.

  • 상황에 맞는 정보 추가 (예 : 객체 상태 또는 진행 상황)
  • 예외를 안전하게 처리하십시오 (예 : TryX 메소드)
  • 시스템이 서비스 경계를 ​​넘어 외부 라이브러리 또는 API를 호출 중입니다.
  • 다른 유형의 예외를 포착하고 다시 던져야합니다 (아마도 원본을 내부 예외로 사용함)
  • 일부 저가 백그라운드 기능의 일부로 예외가 발생했습니다.

이 경우 중 하나가 아닌 경우 로컬 try / catch를 추가하지 않습니다. 그렇다면 시나리오에 따라 예외 (예 : false를 반환하는 TryX 메서드)를 처리하거나 다시 던질 수 있으므로 전역 전략에 의해 예외가 처리됩니다.

예를 들면 다음과 같습니다.

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

또는 다시 던지는 예 :

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

그런 다음 스택 상단의 오류를 포착하고 사용자에게 멋진 사용자 친화적 인 메시지를 제공합니다.

어떤 방법을 사용하든 항상이 시나리오에 대한 단위 테스트를 작성하는 것이 좋습니다. 따라서 기능이 변경되지 않고 나중에 프로젝트 흐름을 방해하지 않도록 할 수 있습니다.

현재 어떤 언어로 작업하고 있는지 언급하지 않았지만 .NET 개발자이며 언급하지 않는 것이 너무 많습니다.

쓰지 마세요:

catch(Exception ex)
{
  throw ex;
}

사용하다:

catch(Exception ex)
{
  throw;
}

전자는 스택 추적을 재설정하고 최상위 레벨 캐치를 전혀 쓸모 없게 만듭니다!

TLDR

로컬로 캐치는 것은 반 패턴이 아니며 종종 디자인의 일부일 수 있으며 오류에 컨텍스트를 추가하는 데 도움이 될 수 있습니다.


3
최상위 로거 예외 처리기에서 동일한 로거를 사용할 때 캐치의 로깅 지점은 무엇입니까?
Euphoric

스택 맨 위에서 액세스 할 수없는 추가 정보 (예 : 로컬 변수)가있을 수 있습니다. 설명을 위해 예제를 업데이트하겠습니다.
Liath

2
이 경우 추가 데이터와 내부 예외로 새 예외를 처리하십시오.
Euphoric

2
@Euphoric yep, 나는 개인적으로 팬이 아니라는 것을 알았습니다. 그러나 거의 모든 단일 방법 / 시나리오에 대해 새로운 유형의 예외를 만들어야하기 때문에 많은 오버 헤드가 있습니다. 여기에 로그 줄을 추가하면 문제를 진단 할 때 코드 흐름을 설명하는 데 도움이됩니다
Liath

4
Java는 예외를 처리하도록 강요하지 않으며 예외를 인식하도록합니다. 당신은 그것을 잡아서 무엇이든 할 수 있거나 그냥 함수가 그것을 던질 수 있고 무언가로 함수에서 아무것도 할 수없는 것으로 선언 할 수 있습니다.
Newtopian 2019

4

이것은 언어에 많이 의존합니다. 예를 들어 C ++은 예외 오류 메시지에 스택 추적을 제공하지 않으므로 잦은 catch-log-rethrow를 통해 예외를 추적하면 도움이 될 수 있습니다. 반대로 Java 및 유사한 언어는 매우 우수한 스택 추적을 제공하지만 이러한 스택 추적의 형식을 구성 할 수는 없습니다. 중요한 언어를 실제로 추가 할 수없는 경우 (예 : 저수준 SQL 예외를 비즈니스 로직 작업의 컨텍스트와 연결)를 제외하고 이러한 언어로 예외를 잡아서 다시 던지는 것은 의미가 없습니다.

리플렉션을 통해 구현되는 모든 오류 처리 전략은 언어에 내장 된 기능보다 거의 효율성이 떨어집니다. 또한 광범위한 로깅은 피할 수없는 성능 오버 헤드를 갖습니다. 따라서이 소프트웨어의 다른 요구 사항과 비교하여 얻은 정보 흐름의 균형을 맞출 필요가 있습니다. 즉, 컴파일러 수준 계측에 구축 된 PostSharp와 같은 솔루션은 일반적으로 런타임 리플렉션보다 훨씬 낫습니다.

나는 개인적으로 모든 것을 기록하는 것은 관련이없는 정보가 많이 포함되어 있기 때문에 도움이되지 않는다고 생각합니다. 따라서 자동화 된 솔루션에 회의적입니다. 좋은 로깅 프레임 워크가 주어지면 어떤 종류의 정보를 기록하고이 정보의 형식을 지정해야하는지에 대한 합의 된 코딩 지침을 마련하는 것으로 충분할 수 있습니다. 그런 다음 중요한 곳에 로깅을 추가 할 수 있습니다.

비즈니스 로직에 로그온하는 것이 유틸리티 기능에 로그온하는 것보다 훨씬 중요합니다. 또한 프로세스의 최상위 수준에서만 로깅이 필요한 실제 충돌 보고서의 스택 추적을 수집하면 로깅에서 가장 가치가 높은 코드 영역을 찾을 수 있습니다.


4

try/catch/log모든 방법에서 볼 때 개발자가 응용 프로그램에서 발생할 수있는 것과 그렇지 않은 것을 전혀 알지 못하고 최악의 상황을 가정했으며 예상되는 모든 버그로 인해 모든 곳을 선점 적으로 기록했습니다.

이것은 단위 및 통합 테스트가 불충분하고 개발자가 디버거에서 많은 코드를 단계별로 살펴 보는 데 익숙하며 많은 로깅을 통해 테스트 환경에서 버그가있는 코드를 배포하고 문제를 찾을 수 있기를 바랍니다. 로그.

예외를 발생 시키는 코드 예외를 잡아서 기록하는 중복 코드보다 유용 할 수 있습니다. 메소드가 예기치 않은 인수를 수신하고 서비스 경계에 기록 할 때 의미있는 메시지와 함께 예외를 발생시키는 경우, 유효하지 않은 인수의 부작용으로 발생한 예외를 즉시 기록하고 그 원인을 추측해야하는 것보다 훨씬 도움이됩니다. .

널이 예입니다. 인수 또는 메소드 호출의 결과로 값을 가져오고 널이 아니어야하는 경우 예외를 처리하십시오. NullReferenceException널 (NULL) 값으로 인해 나중에 던져진 5 줄 의 결과 만 기록하지 마십시오 . 어느 쪽이든 예외가 발생하지만 한 쪽은 무언가를 알려주고 다른 쪽은 무언가를 찾게합니다.

다른 사람들이 말했듯이 서비스 경계에서 또는 예외가 정상적으로 처리되어 예외가 다시 발생하지 않을 때마다 예외를 기록하는 것이 가장 좋습니다. 가장 중요한 차이점은 무언가와 아무것도 아닌 것입니다. 한 곳에서 예외가 기록되면 쉽게 찾을 수 있습니다. 필요할 때 필요한 정보를 찾을 수 있습니다.


고마워 스캇. " 메소드가 예상치 못한 인수를 받고 서비스 경계에이를 기록 할 때 의미있는 메시지로 예외를 던지는 경우 "실제로 파업하고 메소드 인수와 관련하여 내 주위에 떠오르는 상황을 시각화하는 데 도움이되었습니다. 필자는 인수 세부 정보를 포착하고 기록하는 것보다 안전 가드 조항을 가지고 ArgumentException을 던지는 것이 합리적이라고 생각합니다.
rahulaga_dev

스캇, 나도 같은 느낌이야 컨텍스트를 기록하기 위해 로그 및 rethow를 볼 때마다 개발자가 클래스의 변형을 제어 할 수 없거나 메소드 호출을 보호 할 수없는 것처럼 느낍니다. 대신 모든 방법이 비슷한 try / catch / log / throw에 래핑됩니다. 그리고 그것은 끔찍합니다.
Max

2

아직 예외에없는 컨텍스트 정보를 기록해야하는 경우 새 예외로 랩핑하고 원래 예외를로 제공하십시오 InnerException. 이렇게하면 원래 스택 추적이 유지됩니다. 그래서:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

Exception생성자에 대한 두 번째 매개 변수 는 내부 예외를 제공합니다. 그런 다음 모든 예외를 한곳에 기록 할 수 있으며 여전히 동일한 로그 항목에 전체 스택 추적 상황에 맞는 정보를 얻을 수 있습니다 .

사용자 정의 예외 클래스를 사용할 수 있지만 요점은 동일합니다.

try / catch / log / rethrow는 혼란스러운 로그로 이어질 수 있기 때문에 혼란스러워집니다. 예를 들어 컨텍스트 정보를 기록하고 최상위 핸들러에서 실제 예외를 기록하는 사이에 다른 스레드에서 다른 예외가 발생하면 어떻게됩니까? 새 예외가 원본에 정보를 추가하는 경우 try / catch / throw는 괜찮습니다.


원래 예외 유형은 어떻습니까? 우리가 포장하면 사라집니다. 이게 문제가 되나요? 누군가가 예를 들어 SqlTimeoutException에 의존하고있었습니다.
Max

@Max : 원래 예외 유형은 여전히 ​​내부 예외로 사용할 수 있습니다.
JacquesB

그게 내 뜻이야! 이제 SqlException을 잡는 호출 스택의 모든 사람은 결코 그것을 얻지 못할 것입니다.
Max

1

예외 자체는 메시지, 오류 코드 및 기타 정보를 포함하여 적절한 로깅에 필요한 모든 정보를 제공해야합니다. 따라서 예외를 다시 발생 시키거나 다른 예외를 throw하기 위해 예외를 포착 할 필요는 없습니다.

종종 DatabaseExceptionException, InvalidQueryException 및 InvalidSQLParameterException을 포착하고 DatabaseException을 다시 발생시키는 것과 같이 여러 예외 패턴이 포착되어 일반적인 예외로 다시 발생하는 것을 볼 수 있습니다. 그럼에도 불구하고, 나는이 모든 특정 예외가 먼저 DatabaseException에서 파생되어야한다고 주장하므로 다시 던질 필요가 없습니다.

불필요한 try catch 절 (순전히 로깅을위한 것)을 제거하면 실제로 작업이 더 쉬워지고 쉬워지지는 않습니다. 프로그램에서 예외를 처리하는 장소 만 예외를 기록해야하며, 프로그램을 정상적으로 종료하기 전에 예외를 기록하기위한 마지막 시도를위한 프로그램 전체 예외 처리기가 실패해야합니다. 예외에는 예외가 발생한 정확한 지점을 나타내는 전체 스택 추적이 있어야하므로 "컨텍스트"로깅을 제공 할 필요가없는 경우가 많습니다.

AOP는 일반적으로 약간의 속도 저하를 수반하지만 빠른 수정 솔루션 일 수 있습니다. 대신 값이 추가되지 않은 불필요한 try catch 절을 완전히 제거하는 것이 좋습니다.


1
" 예외 자체는 메시지, 오류 코드 및 기타 정보를 포함하여 적절한 로깅에 필요한 모든 정보를 제공 해야하지만 실제로는 Null 참조 예외가 고전적인 사례는 아닙니다. 예를 들어 복잡한 식에서 변수를 일으키는 변수를 알려주는 언어는 없습니다.
David Arno

1
@DavidArno True, 그러나 당신이 제공 할 수있는 어떤 상황도 그처럼 구체적 일 수는 없습니다. 그렇지 않으면을 가질 수 있습니다 try { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }. 스택 추적은 대부분의 경우 충분하지만 부족한 경우 일반적으로 오류 유형이 적합합니다.
Neil
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.