Software Engineering 11 - Code Coverage & Exception Handling

 Code coverage - white box test
특정 테스트 suit로 소스코드의 어느 부분, 어느 정도가 커버되는지를 나타내는 것. function/statement/branch/condition/decision 등등 다양한 component의 coverage가 존재하며, 각 테스트 케이스가 어느 정도를 커버하는지 확인할 수 있다. (해당 컴포넌트를 실제로 실행하여 확인해보는가)
- statement coverage : 테스트 케이스로 실행되는 statement를 표기. 여러 개의 test case가 있을 때, 퍼센트는 각각의 합이 아니라 전체 statement 중에 실행되는 것을 나타낸다.
- branch coverage : 각 테스트 케이스가 조건문의 T/F를 시행하는지를 확인. 전체 coverage는 위와 동일하게 덧셈이 아니라 전체에 대비하여 테스트 케이스들로 실행되는 portion을 나타낸다. branch coverage는 if 등 조건문 전체를 하나의 unit으로 보고 T/F만 판별한다.
* branch coverage는 statement coverage를 포함한다. 따라서 100% branch coverage를 달성하면 100% statement coverage가 달성된다. 왜냐하면 각각의 조건에 따라 분기하며 statement들을 진행한 것이 branch인데, 이것이 100%라면, 해당 프로그램 내의 모든 문구를 한번씩은 실행해보았다는 의미이기 때문이다.
- Condition coverage : 상기의 경우와 다르게 각 조건문의 조건들을 모두 고려한다. 예를 들어 (a>1 && b<1)이라는 것이 있을 때 branch는 그냥 해당 조건의 T/F만 따지지만, condition coverage에서는 a>1인 경우와 b<1인 경우를 따로 분리하여 생각한다.
* 단, 그렇다고 condition coverage가 달성된다고 branch coverage를 보장하지는 않는다. (&& , || 등 의 조건이 교차하는 경우가 있기 때문) 따라서 이 경우에는 어느 하나가 다른 하나를 포함하는 관계가 되지 않는다.
- condition / decision coverage : 위의 두 가지가 동시에 서로를 만족하지 않으므로 둘을 동시에 고려하여 해당 테스트 케이스를 통해 확인할 수 있는 condition과 decision의 portion을 확인하는 것. 이 값은 condition과 branch를 각각 계산하고, 부모는 분모끼리 더하고, 분자는 분자끼리 더하여 얻을 수 있다.
- multiple condition coverage : 앞서 살펴본 것을 보면, 조건문의 T/F를 따지거나, 각각의 조건의 값을 넣는 것 만으로는 모든 가능한 경우의 수를 확인해볼 수 없다는 것을 알 수 있다. 따라서 해당 coverage는 주어진 조건문에 대해 가능한 모든 경우의 수를 고려하는 것이다. 예를 들어 (a>1 && b<1) 이라면 TT, TF, FT, FF 의 4가지를 모두 고려한다는 것이다.
* 따라서 이 경우 multiple condition coverage가 100%달성되면 decision 과 condition 만족된다는 것을 알 수 있다. 따라서 이들은 포함관계에 있다. 그래서 이 커버리지만 만족하면 지금까지의 다른 것들을 모두 만족한다.
그러나 해당 방법은 condition의 수가 늘어난다면 필요한 테스트 케이스의 수는 2n승 개로 급격하게 상승하여 실전적으로 사용하기가 어렵다. 따라서 이를 조금 변형하여 사용해야 한다.
- MCDC(modified condition decision coverage) : 상기 방법을 개선하여 n+1~2*n개 사이의 테스트 케이스를 통해 위와 동일한 결과를 낸다. (최적설계에서 진행했던 2k factorial, 혹은 taguchi와 비슷한 느낌) 따라서 조건문의 결과에 독립적으로 영향을 미치는 condition 셋을 확인하는 것이다. (해당 condition의 결과에 따라 output이 바뀌는 pair를 선택하여 테스트 케이스를 제작.) 예를 들어 (a>1 && b<1 && c=1)이라는 조건문이 있을 때 첫번째와 두번째가 모두 참이라면, 해당 조건문의 결과는 오직 c의 값에 따라 결과가 결정되게 된다. 따라서 c의 값을 확인하려는 테스트 케이스는 c의 T/F에 종속적이므로 해당 pair를 c를 판별하는 test case set으로 사용하는 것이다.
이 때 선택하는 조합은 여러 개가 될 수 있고, 이에 따라 MCDC test case의 개수가 달라질 수 있다.
주로 자동차, 항공 분야 등 안전에 대한 요구가 높아질 때 해당 방식을 이용하여 철저하게 검사한다.

Exception handling
- exception : 정상적인 흐름을 멈추는 이벤트. 대부분 프로그래밍 언어에는 에러 상황에 대한 정보를 담고 있는 exception object로 구현된다. (throw)
exception handling을 사용할 때 좋은 점은 error handling code를 regular code로 부터 분리할 수 있다는 것이다. 어떤 코드에 대해 발생할 수 있는 예외 상황을 main flow에 모두 구현하여 코드를 짜면 전체 코드가 매우 복잡하고 읽기 어려워진다. 따라서 exception handling을 사용하여 try-catch의 구조를 사용해 정상 흐름은 try문 안에, 각 오류가 발생했을 때 throw 되는 에러를 catch문에서 잡아 해결하면 더 깔끔한 코드를 만들 수 있다.
또한 중첩된 여러 method call이 있을 때 각각의 단계에서 에러를 제대로 처리하지 않으면 호출자로 에러가 전파될 수 있다. 따라서 이를 throws exception 으로 선언하고 호출자에서 catch를 하면 해당 에러들을 처리해줄 수 있다. 이는 언어 상에서 자동으로 propagation 되므로 훨씬 편리하게 사용할 수 있다. (exception propagate를 따로 해주지 않더라도 자동으로 각 단계에마다 적절한 에러 핸들러가 있는지 확인함)
마지막으로 객체 지향 언어처럼, exception도 object처럼 취급하고 구현하기 때문에 type이 존재하고, 상속과 구현 등 다양한 작업을 할 수 있게 된다. (grouping - super type, specific - sub type)

* Exception etiquette : 내가 handle할 수 없는 에러는 catch하지 않는다. 만약 부분적으로만 해결하고 문제가 지속된다면, 이를 re-throw 해야 한다. (catch & release)
또한 catch를 하고 ignore 하는 것도 하면 안된다. catch(Exception e) {} 해당 exception type Exception은 가장 높은 예외의 타입이므로 모든 에러가 다 여기서 잡히고 처리가 안되게 된다. (모든 예외는 Exception을 상속하여 IOException, FileOpenException 등을 구현하기 때문)
따라서 내가 해결할 수 없는 에러는 잡지 않도록 설계한다는 것을 꼭 기억하자.

Exception handling keyword
java의 경우 try, catch, throw, throws, finally 가 있고, C#은 try, catch, throw, finally만 존재한다. 해당 키워드를 사용하는 흐름은 다음과 같다.
normal flow를 실행하는 program statement들은 try block 내부에 작성하여 monitor 되도록 한다. 만약 해당 블록 내에서 에러가 발생된다면 throw 된다. catch block은 알맞은 exception type에 따라 throw된 에러를 잡아 해결한다. 만약 test를 위해 exception 객체를 manual 하게 만들고 싶다면 throw a  와 같이 exception을 써주면 된다. 그리고 자바의 경우, 해당 메서드 바깥으로 throw 되는 exception (해당 메소드 내에서 처리하지 않고 외부에 처리를 맡기는 경우)은 throws라고 따로 표기해주어야 한다. 또한 해당 메소드가 return 하기 전에 반드시 수행되어야 하는 statement들은 finally block안에 포함시키면 된다. (만약 try문 내부를 진행하다가 에러가 발생하여 throw되고, catch된다면, 어떤 문구는 실행이 되지 않을 수 있으므로 반드시 실행되어야 하는 부분을 분리하여 실행을 보장. 주로 resource management에 관련한 부분을 수행)
Exception Types
최상위 타입은 Throwable, 그 아래에 Exception과 Error가 존재하며 실제로 사용되는 exception들의 type은 대부분 Exception의 하위 클래스들이다.
Exception의 가장 중요한 것은 RuntimeException이다. (0으로 나누는 등 대부분의 버그 발생) Error는 예외긴 하지만 내 프로그램에서 해결할 수 없는 부류이다. (stack overflow 등)
- Checked exception : Error와 RuntimeException을 제외한 남은 Exception의 하위 클래스들을 checked exception이라 한다. (throwable 자체도 포함) 이 종류는 잘 정의된 프로그램이라면 만족해야 하는 조건들로, 발생할 수 있는 문제를 예측하고 이를 복구하고 수정하는 것들로 구성된다. 예를 들어 java.io.FileNotFoundException 등이 있다. 이 에러는 반드시 해결되어야 하므로 catch하거나 다른 메소드에서 해결될 수 있도록 specify해주어야 한다. (subject to catch or specify)
- Error : application 단에서 예측/해결/복구할 수 없는 것들. 따라서 catch 하거나 specify해도 별로 의미가 없기 때문에 프로그래머들의 관심 범위 밖에 있다. (java.io.IOError 등)
- RuntimeException : 프로그램 내부의 버그, logic error, API의 잘못된 사용 등으로 발생되는 에러. NullPointerException 등, 이런 버그는 따로 잡아서 해결할 수 있는 것이 아니라 코드 작성할 때부터 발생하는 버그나 논리적 오류에 기인하므로 프로그램 실행 중에 내부적으로 해결할 수 있는 종류는 아니다.
종합해보면, 어떤 에러가 발생했을 대 runtimeException인지, checkException인지를 판별/설계하는 것이 중요하다. 만약 client가 해당 에러를 예측하고, 이에서 회복할 수 있다면 checkException으로, 만약 회복할 수 없으며 예측하기도 어렵다면 이는 uncheckedException으로 처리하는 것이 옳다. (회복가능한지에 따라 exception을 분리)
만약 exception을 잡지 않고 실행하게 되면, java의 run time system에서 default handler가 이를 catch하고 exception 발생 위치를 반환하며 프로그램을 종료해준다. 따라서 이런 상황을 방지하기 위해 최소한 main에는 error catch가 있어야 한다.



Try & catch
try 블록을 실행하다가 에러가 발생하면, 알맞은 catch 블록이 수행되고 try-catch block전체를 빠져나가게 된다. 따라서 앞서 살펴본 것 처럼 try의 남은 부분은 실행되지 않으므로 반드시 실행되어야 하는 것들은 finally block에 따로 표기해주어야 한다.
그리고 catch 문을 실행하면 해당 exception은 해소되므로 다음 문장들은 정상적으로 수행된다.
여러 개의 catch 가 존재할 수도 있다. 이 때 catch들은 작성된 순서대로 검사되며 이 중 하나가 catch되면 나머지들은 확인하지 않고 건너뛰게 된다. 따라서 만약 subclass / superclass의 catch문을 동시에 작성하였다면, subclass(specific한 경우)를 먼저 적어주어야 한다. 그렇지 않으면 superclass에서 모두 catch되므로 subclass는 무용하기 때문이다.
nested try문도 가능한데, 이 경우 if-else문과 동일하게 수행된다. 단, 어떤 try의 catch가 없거나 해당 scope 내부에서 해결할 수 없다면 해당 블록 바깥의 가장 가까운 catch와 매치된다. 이는 메소드 내부에서 호출되는 try 문도 동일하다.

Throw
앞서 말한 것 처럼 명시적으로 오류를 생성할 수 있다. 이는 new keyword를 사용하여 throw new NullPointerException("demo"); 나 throw new Exception(); 과 같이 할 수도 있고(exception type 객체 생성), catch문에 정의된 e를 사용하여 throw e; 와 같이 이미 정의된 exception을 다시 호출할 수도 있다.

Throws
자신에게 발생할 수 있지만, 자신 내부에서 해결하지 않고 외부에 맡기는 에러들이 있을 때 caller에게 해당 사실을 알려야 하므로 int methodA() throws exceptionList{}와 같이 명시해주어야 한다. 단, 이 때 알려야 하는 에러는 Error나 RuntimeException을 제외한 오직 checkException만 포함하면 된다.
자바는 catch 되지 않는 error가 형성되는 것을 막기 위해 자신 내부에서 해결하지 않는 에러가 존재하고, caller에서 이를 catch 하지 않는 경우에는 compile 자체가 안된다. 따라서 어떤 함수를 사용할 때 throws에 명시된 error handling을 모두 해주어야 한다.
Finally
에러가 발생하거나 return 하더라도 반드시 수행되어야 하는 부분을 명시. 따라서 catch를 진행하거나 return, break 문들이 존재해도 해당 부분은 반드시 수행되고 try catch 블록이 끝난다. (단, system.exit()는 자바 버추얼 머신 자체를 꺼버리므로 종료됨)

댓글

이 블로그의 인기 게시물

IIKH Class from Timothy Budd's introduction to OOP

Compiler 9 - Efficient Code generation

Software Engineering 10 - V&V, SOLID principle