try-catch-finally 블록의 관련 코드가“얼마나 나쁜가?”


11

이것은 관련 Q : 반환 스타일이 나쁘거나 위험한 작업을 수행하기 위해 finally 절을 사용합니까?

참조 된 Q에서 finally 코드는 사용 된 구조 및 프리 페치의 필요성과 관련이 있습니다. 내 질문은 조금 다르며 더 많은 사람들에게 독창적이라고 생각합니다. 내 특정 예는 C # winform 앱이지만 C ++ / Java 최종 사용에도 적용됩니다.

블록에 묻힌 예외 및 예외 처리 / 정리와 관련이없는 많은 코드가있는 try-catch-finally 블록을 주목하고 있습니다. 그리고 예외 및 처리와 밀접하게 관련된 코드로 try-catch-finally 블록을 사용하는 것에 대한 편견을 인정할 것입니다. 여기 내가보고있는 몇 가지 예가 있습니다.

try 블록에는 던질 수있는 코드로 이어지는 많은 예비 호출 및 변수가 설정됩니다. 로깅 정보는 설정을 시도하고 try 블록에서도 실행됩니다.

마지막으로 블록은 형식 / 모듈 / 제어 형식 호출 (캐치 블록에 표시된대로 앱이 종료 되더라도)뿐만 아니라 패널과 같은 새 객체를 생성합니다.

대충:

    methodName (...)
    {
        시험
        {
            // 메소드에 대한 많은 코드 ...
            // 던질 수있는 코드 ...
            // 메소드에 대한 더 많은 코드와 리턴 ...
        }
        잡기 (뭔가)
        {// 예외 처리}
        드디어
        {
            // 예외로 인해 일부 정리가 완료되었습니다.
            // 생성 된 것들에 대한 더 많은 코드 (예외가 발생할 수 있음을 무시)
            // 더 많은 객체를 생성
        }
    }

코드가 작동하므로 가치가 있습니다. 잘 캡슐화되지 않았으며 논리가 약간 복잡합니다. 나는 코드를 리팩토링 할뿐만 아니라 코드를 전환 할 때의 위험에 대해 (고통 적으로) 익숙하기 때문에 비슷한 질문으로 구성된 코드에 대한 다른 사람들의 경험을 알고 싶어합니다.

나쁜 스타일은 변경을 정당화합니까? 비슷한 상황에서 심하게 화상을 입은 사람이 있습니까? 그 나쁜 경험의 세부 사항을 공유하고 싶습니까? 내가 과잉 반응하고 스타일이 그렇게 나쁘지 않기 때문에 그대로 두십시오. 정리하는 것의 유지 보수 이점을 얻습니까?


"C ++"태그는 여기에 속하지 않습니다. C ++에는 (와 필요하지 않음)이 없습니다 finally. 모든 올바른 사용은 RAII / RRID / SBRM (둘 중 원하는대로)에 적용됩니다.
David Thornley

2
@DavidThornley : 'finally'키워드가 사용 된 예를 제외하고 나머지 질문은 C ++에 완벽하게 적용됩니다. 나는 C ++ 개발자이고 그 키워드는 실제로 나를 혼동하지 않았다. 그리고 반 정규적으로 우리 팀과 비슷한 대화를하고 있다는 것을 고려할 때,이 질문은 C ++
로하

@ DXM : 이것을 시각화하는 데 문제가 있습니다 ( finallyC #에서 무엇을하는지 알고 있습니다 ). C ++은 무엇입니까? 내가 생각하는 것은의 코드 catch이며 C # 이후의 코드에도 동일하게 적용됩니다 finally.
David Thornley

@DavidThornley : 마침내 코드는 무엇이든 실행되는 코드입니다 .
orlp

1
@ nightcracker, 그건 정확하지 않습니다. 그렇게하면 실행되지 않습니다 Environment.FailFast(). 포착되지 않은 예외가 있으면 실행되지 않을 수 있습니다. 반복자 finally를 사용하여 수동으로 반복 하는 경우 훨씬 더 복잡해집니다 .
svick

답변:


10

개발자가 작성한 끔찍한 레거시 Windows Forms 코드를 처리해야 할 때와 비슷한 상황을 겪었습니다.

우선, 당신은 과잉 행동하지 않습니다. 이것은 잘못된 코드입니다. 당신이 말했듯이, 캐치 블록은 중단과 멈출 준비에 관한 것입니다. 객체 (특히 패널)를 만들 때가 아닙니다. 왜 이것이 나쁜지 설명조차 시작할 수 없습니다.

그 말은 ...

나의 첫번째 충고는 : 그것이 깨지지 않았다면 만지지 마십시오!

코드를 유지 관리하는 것이 직업이라면 코드를 어 기지 않도록 최선을 다해야합니다. 나는 그것이 고통 스럽다는 것을 알고 있지만 (나는 거기에 있었음) 이미 작동중인 것을 깨뜨리지 않기 위해 최선을 다해야합니다.

두 번째 조언은 더 많은 기능을 추가해야하는 경우 기존 코드 구조를 최대한 유지하여 코드를 손상시키지 않도록하십시오.

예 : 적절한 상속으로 대체 될 수 있다고 생각하는 끔찍한 스위치 케이스 진술이 있다면, 물건을 옮기기 시작하기 전에 조심하고 두 번 생각해야합니다.

리팩토링이 올바른 방법이지만 리팩토링 코드가 버그를 유발할 가능성이 더 높은 상황을 확실히 찾을 수 있습니다 . 개발자의 관점이 아니라 응용 프로그램 소유자의 관점에서 결정해야합니다. 따라서 문제를 해결하는 데 필요한 노력 (돈)이 리팩터링 가치가 있는지 생각해야합니다. 나는 개발자가 "코드가 못 생겼다"고 생각하기 때문에 실제로 깨지지 않은 것을 고치기 위해 며칠을 보내는 것을 여러 번 보았습니다.

세 번째 조언은 코드를 깰 경우 화상을 입을 수 있다는 것 입니다. 그것이 당신의 잘못인지 아닌지는 중요하지 않습니다.

유지 관리를 위해 고용 된 사람이라면 다른 사람이 잘못된 결정을했기 때문에 응용 프로그램이 분리되는지 여부는 중요하지 않습니다. 사용자 관점에서 이전과 지금은 작동하고있었습니다. 당신 은 그것을 파산!

Joel은 레거시 코드를 다시 작성해서는 안되는 몇 가지 이유를 설명하는 그의 기사를 잘 작성했습니다.

http://www.joelonsoftware.com/articles/fog0000000069.html

그래서 당신은 그런 종류의 코드에 대해 정말로 기분이 좋지 않아야합니다 (그리고 당신은 그런 것을 쓰지 않아야합니다). 그러나 그것을 유지하는 것은 완전히 다른 괴물입니다.

내 경험에 대해 : 나는 약 1 년 동안 코드를 유지해야했고 결국에는 코드를 처음부터 다시 작성할 수 있었지만 한 번에 전부는 아닙니다.

코드가 너무 나빠서 새로운 기능을 구현할 수 없었습니다. 기존 응용 프로그램에는 심각한 성능 및 유용성 문제가있었습니다. 결국 나는 3-4 개월이 걸리는 변경을 요청 받았다 (주로 그 코드로 작업하면 평소보다 더 많은 시간이 걸렸기 때문에). 나는 약 5-6 개월 만에 전체 조각 (원하는 새로운 기능 구현 포함)을 다시 쓸 수 있다고 생각했습니다. 나는이 제안을 이해 관계자들에게 가져 왔고 그들은 그것을 재 작성하기로 합의했다 (다행스럽게도).

이 글을 다시 쓴 후에 그들은 이미 가지고있는 것보다 훨씬 더 나은 것을 전달할 수 있다는 것을 이해했습니다. 그래서 전체 응용 프로그램을 다시 작성할 수있었습니다.

내 접근 방식은 그것을 하나씩 다시 작성하는 것이 었습니다. 먼저 전체 UI (Windows Forms)를 교체 한 다음 통신 계층 (웹 서비스 호출)을 교체하기 시작했으며 마지막으로 전체 서버 구현 (Thick Client / Server 종류의 응용 프로그램)을 교체했습니다.

몇 년 후이 응용 프로그램은 회사 전체에서 사용되는 아름다운 핵심 도구로 바뀌 었습니다. 나는 모든 것을 다시 쓰지 않았다면 결코 불가능했을 것이라고 100 % 확신합니다.

비록 내가 그것을 할 수 있었지만 중요한 부분은 이해 관계자가 그것을 승인하고 그들이 돈 가치가 있다고 확신 할 수 있다는 것입니다. 따라서 기존 응용 프로그램을 유지 관리하는 동안 아무것도 깨지 않도록 최선을 다하고 소유자에게 다시 쓰기의 이점에 대해 확신을 줄 수 있다면 Jack the Ripper와 같이 해보십시오.


1
+1. 그러나 마지막 문장은 연쇄 살인범을 말합니다. 정말 필요한가요?
MarkJ

나는 이것의 일부를 좋아하지만 동시에 깨진 디자인을 남겨두고 물건을 추가하면 종종 디자인이 악화됩니다. 물론 측정 방법이 있지만 문제를 해결하고 수정하는 방법을 알아내는 것이 좋습니다.
Ricky Clarkson

@RickyClarkson 동의합니다. 작업을 과도하게 수행 할 때 (리팩토링이 실제로 필요한 것은 아님), 변경 사항을 추가하여 응용 프로그램을 실제로 손상시키는시기를 아는 것은 매우 어렵습니다.
Alex

@RickyClarkson 나는 내가 참여했던 프로젝트를 다시 작성할 수있을 정도로 동의합니다. 그러나 오랫동안 가능한 한 최소한의 피해를 입히려 고 물건을 조심스럽게 추가해야했습니다. 주요 원인이 잘못된 설계 결정 (일반적으로는 그렇지 않음) 인 문제를 해결하기위한 요청이 아니라면 애플리케이션의 설계를 건드리지 말아야합니다.
Alex

@RickyClarkson 지역 사회 경험의 일부였습니다. 모든 레거시 코드를 잘못된 것으로 선언하는 것은 마찬가지로 안티 안티 패턴입니다. 그러나이 코드에는 실제로 finally 블록의 일부로 수행해서는 안되는 것들이 있습니다. Alex의 피드백과 응답은 제가 읽고 싶은 내용입니다.

2

코드를 변경할 필요가 없으면 그대로 두십시오. 그러나 버그를 수정하거나 일부 기능을 변경해야하므로 코드를 변경해야하는 경우 원하는 경우 변경해야합니다. IMHO 더 작고 이해하기 쉬운 부분으로 리팩토링 한 다음 기능을 추가하면 더 안전하고 오류가 적은 경향이 있습니다. 자동 리팩토링 도구를 사용하는 경우 새로운 버그가 발생할 위험이 매우 적으므로 비교적 안전합니다. Michael Feathers 책의 사본을 받아야합니다.

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

새로운 기능을 추가 할 때 코드를 더 테스트 가능하게 만들고, 단위 테스트를 추가하고, 코드를 중단하지 않는 방법에 대한 유용한 힌트를 제공합니다.

올바르게 수행하면 try-catch-finally에 잘못된 위치에 관련없는 코드가 더 이상 포함되지 않을 정도로 메소드가 간단 해지기를 바랍니다.


2

호출 할 메소드가 6 개 있는데 그 중 4 개가 첫 번째 오류없이 완료된 경우에만 호출해야한다고 가정하십시오. 두 가지 대안을 고려하십시오.

try {
     method1();  // throws
     method2();
     method3();
     method4();
     method5();
 } catch(e) {
     // handle error
 }
 method6();

vs

 try {
      method1();  // throws
 }
 catch(e) {
      // handle error
 }
 if(! error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

두 번째 코드에서는 리턴 코드와 같은 예외를 처리합니다. 개념적으로 이것은 다음과 같습니다.

rc = method1();
if( rc != error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

try / catch 블록에서 예외를 발생시킬 수있는 모든 메소드를 래핑하려는 경우 언어 기능에서 예외가 발생하는 지점은 무엇입니까? 이점이 없으며 더 많은 타이핑이 있습니다.

우리가 언어에서 처음으로 예외를 가지고있는 이유는 과거의 나쁜 리턴 코드의 경우 오류를 리턴 할 수있는 여러 메소드가있는 경우가 많았고 각 오류는 모든 메소드를 완전히 중단시키는 것을 의미했기 때문입니다. 경력이 시작될 때, 구 C 날에, 나는 모든 곳에서 다음과 같은 코드를 보았습니다.

rc = method1();
if( rc != error ) {
     rc = method2();
     if( rc != error ) {
         rc = method3();
         if( rc != error ) {
             rc = method4();
             if(rc != error ) {
                 method5();
             }
         }
     }
 }
 method6();

이것은 매우 일반적인 패턴이며 위의 첫 번째 예로 돌아가서 예외를 매우 깔끔하게 해결했습니다. 각 메소드에 자체 예외 블록이 있다고 말하는 스타일 규칙은이를 완전히 창 밖으로 내 보냅니다. 그렇다면 왜 귀찮게합니까?

try 블록이 개념적으로 하나의 코드 세트를 초과하도록 항상 노력해야합니다. 코드 단위에 오류가있는 경우 코드 단위에서 다른 코드를 실행할 필요가없는 코드를 둘러싸 야합니다.

예외의 원래 목적으로 돌아가서 : 실제로 코드가 실제로 발생할 때 오류를 쉽게 처리 할 수 ​​없기 때문에 예외가 생성되었음을 기억하십시오. 예외는 코드에서 나중에 오류를 처리하거나 스택을 추가로 처리 할 수 ​​있도록하는 것입니다. 사람들은 잊어 버리고 많은 경우 문제의 방법 으로이 예외를 던질 수 있다는 것을 문서화하는 것이 더 좋을 때 현지에서 잡습니다. 세상에는 다음과 같은 코드가 너무 많습니다.

void method0() : throws MyNewException 
{
    try {
        method1();  // throws MyOtherException
    }
    catch(e) {
        if(e == MyOtherException)
            throw MyNewException();
    }
    method2();
}

여기서는 레이어를 추가하고 레이어는 혼란을 더합니다. 그냥 이렇게 :

void method0() : throws MyOtherException
{
    method1();
    method2();
}

게다가, 내 느낌은 당신이 그 규칙을 따르고 메소드에서 몇 개 이상의 try / catch 블록으로 자신을 찾으면 메소드 자체가 너무 복잡하고 여러 메소드로 나눌 필요가 있는지 질문해야한다는 것입니다. .

테이크 아웃 : try 블록에는 블록의 어느 곳에서나 오류가 발생하면 중단되어야하는 코드 세트가 포함되어야합니다. 이 코드 세트가 한 줄인지 또는 1000 줄인지는 중요하지 않습니다. (방법에 수천 줄이 있지만 다른 문제가 있습니다.)


Steven-잘 준비해 주셔서 감사합니다. 나는 당신의 생각에 전적으로 동의합니다. 단일 try 블록에 합리적으로 관련되거나 순차적 인 유형 코드에 문제가 없습니다. 실제 코드 스 니펫을 게시 할 수 있었으면 첫 번째 던지기 호출 전에 5-10 개의 관련이없는 호출을 표시했을 것입니다. 하나의 특정 블록은 훨씬 나빠졌습니다. 몇 개의 새로운 스레드가 분리되고 추가의 비 정리 루틴이 호출되었습니다. 그리고 그 문제에 대해, 마지막으로 차단 된 모든 통화는 던질 수있는 것과 관련이 없었습니다.

이 접근 방식의 한 가지 주요 문제점 method0은 예외가 발생했을 가능성이 있다고 예상되는 경우와 그렇지 않은 경우를 구분하지 않는다는 것입니다. 예를 들어, 때때로을 method1던질 수 InvalidArgumentException있지만 그렇지 않은 경우 객체를 일시적으로 유효하지 않은 상태로 설정하여에 의해 수정되며 method2, 항상 객체를 유효한 상태로 되돌릴 것으로 예상됩니다. 만약 method2가을 던지면 InvalidArgumentException, 그런 예외를 잡을 것으로 예상되는 코드는 method1그것을 잡을 것이다.
supercat

... 시스템 상태는 코드의 기대치와 일치합니다. method1에서 발생한 예외를 method2에서 발생한 예외와 다르게 처리해야하는 경우,이를 랩핑하지 않고 현명하게 달성 할 수있는 방법은 무엇입니까?
supercat

이상적으로는 예외가 발생할 정도로 세밀한 예외가 있습니다.
로봇 고트
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.