.NET/C#

[C# 13과 .NET 9] 4. 함수의 작성과 Debugging 그리고 Testing

클리엘 2025. 6. 23. 15:29
728x90

이번 장은 code의 재사용을 위해 함수를 작성하고, 개발과정에서 Debugging 하는 방법과 application의 안정성과 신뢰성을 향상하기 위해 code를 testing 하고 bug를 제거하는 것에 관한 것입니다. 또한 runtime시에도 발생할 수 있는 예외를 logging 하는 방법에 관해서도 살펴볼 것입니다.

1. 함수

 

Programming에서의 기본적인 원칙은 반복하지 않는 것입니다.

 

Programming과정에서 만약 동일한 구문을 반복해서 작성하고 있다면 이들 구문을 함수로 전환할 수 있습니다. 함수는 동일한 작업을 수행하는 작은 program단위에 해당합니다. 예를 들어 부가세 계산을 위한 함수를 작성해놓으면 상업 application의 여러 부분에서 해당 함수를 재사용할 수 있을 것입니다.

 

Program처럼 함수역시 입력과 출력을 가지는데 때로는 함수를 black box로 정의되기도 합니다. 어느 한쪽에 원재료를 투입하면 다른 쪽에서 완성된 item이 나오게 되는 것입니다/ 일단 함수를 생성한 뒤 이들을 철저히 Test 하고 Debugging 하고 나면 더 이상 이들의 작동방식은 생각할 필요 없이 필요한 곳에 사용하기만 하면 됩니다.

 

1) Top-level Program에서의 함수와 Namespace

 

처음 다른 장을 통해 우리는 C# 10/.NET 6부터 console app의 기본 project template으로 C# 9에서 도입된 top-level program기능을 사용할 수 있었습니다.

 

함수를 작성할때는 top-level program에서 자동으로 생성되는 Program class와 <Main>$ method가 어떻게 작동하는지 이해하는 것이 중요합니다.

 

함수를 정의할때 top-level program기능이 어떻게 작동하는지를 알아보기 위해 우선 Visual Studio에서 'TopLevel'이름의 project를 다음과 같이 생성합니다. 이때 Do not use top-level statements와 Enable native AOT (ahead-of-time) publish부분을 uncheck 하고 생성합니다.

 

Program.cs에서 기존의 구문을 모두 삭제하고 file 맨 아래 부분에서 지역변수를 정의한뒤 이를 호출하는 예제를 아래와 같이 작성합니다.

PrintThisNamespace();

void PrintThisNamespace()
{
    Console.WriteLine($"Namespace : {typeof(Program).Namespace ?? "null" }");
}
함수가 반드시 file안에서 마지막에 위치할 필요는 없습니다. 하지만 함수를 다른 program 구문과 섞어놓기보다는 file의 밑으로 분리해서 정의하는 것이 좋습니다. Class와 같은 type 역시 Program.cs의 밑에서 선언되어야 하며 그렇지 않으면 CS8803 compiler error를 발생시키게 됩니다.(Class를 정의할 때는 별도의 file로 분리하는 것을 권장합니다.)
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs8803

 

예제를 실행하면 다음과 같이 Namespace가 null로 표시됨을 알 수 있습니다.

 

(1) 지역함수를 생성하면 top-level program에서 무슨일이 벌어질까?

 

Top-level program에서 compile을 시도하면 compiler는 <Main>$ 함수와 함께 Program class를 생성하고 개발자가 작성한 구문을 <Main>$ method안으로 집어넣게 됩니다. 이때 생성한 지역함수도 포함되며 함수를 이동한 뒤에는 함수의 명칭 역시 바뀌게 됩니다.

partial class Program
{
    static void <Main>$(String[] args)
    {
        <<Main>$>g__PrintThisNamespace|0_0();
        void <<Main>$>g__PrintThisNamespace|0_0()
        {
            Console.WriteLine($"Namespace : {typeof(Program).Namespace ?? "null"}");
        }
    }
}

 

Compiler가 어떤 구문을 어디로 이동시킬지에 대해서는 정해져 있으므로 아래와 같은 규칙을 지켜야 합니다.

  • using과 같은 Import 문은 Program.cs의 가장 위치쪽에 위치해야 합니다.
  • <Main>$ method안으로 들어갈 구문은 Program.cs의 중간에서 함수가 같이 사용될 수 있습니다. 이때 무든 함수는 <Main>$ method안에서 지역함수가 됩니다.

지역함수는 문서화를 위한 XML을 사용할 수 없는등, 일부 제한을 가지며 이러한 제한사항이 그대로 top-level program에도 적용되기 때문에 함수를 정의할 때 특히 두 번째 규칙이 중요할 수 있습니다.

.NET SDK 또는 다른 도구에서 생성한 code가 필요하다면 compiler가 filesystem에 찾아 이를 compile 하는 source code file안에 해당 code가 있어야 합니다. Compiler가 자체적으로 생성한 code는 별도의 file을 필요로 하지 않으므로 외부 code가 연결될 여지가 없기 때문입니다. Compiler가 무엇을 하는지 알아내는 유일한 방법은 assembly상에서 decompiler를 사용해 본래 code로 reverse engineering을 수행하는 것입니다. 또는 함수와 method에서 특정 정보를 확인하기 위해 예외를 throw 할 수도 있습니다.

 

예제에서 사용된 partial C# keyword는 추후에 자세히 알아볼 것입니다.

 

2) 정적함수로 partial Program class의 정의하기

 

함수를 작성하기에 더 좋은 방법은 분리된 file에서 함수를 작성하고 이를 Program class의 정적 member로 정의하는 것입니다.

 

관련 예제를 작성하기 위해 Program.Functions.cs 이름의 class file을 추가하고(File이름이 문제가 되는 것은 아니지만 되도록 이면 이러한 naming규칙을 따르는 것이 좋습니다.) 기존의 구문을 모두 제거한 뒤 partial Program class를 정의하는 문을 추가합니다. 그리고 Program.cs file에서 이전에 작성한 PrintThisNamespace함수를 잘라내기 한뒤 Program.Functions.cs file에 붙여 넣기하고 여기에서 static keyword를 적용합니다.

partial class Program
{
    static void PrintThisNamespace()
    {
        Console.WriteLine($"Namespace : {typeof(Program).Namespace ?? "null"}");
    }
}

 

현재 Program.cs에서는 오로지 PrintThisNamespace함수를 호출하는 구문 하나만 존재해야 합니다. 예제를 실행하고 이전과 결과가 같은지 확인합니다.

 

(1) 정적함수를 생성하면 top-level program에서 무슨일이 벌어질까?

 

분리된 file에서 partial Program class를 정의하고 그 안에 정적함수를 작성하고 나면 compiler는 Program class를 <Main>$ 함수로 정의하고 사용자 함수를 Program class의 member로서 아래와 같이 병합합니다.

partial class Program
{
    static void <Main>$(String[] args)
    {
        PrintThisNamespace();
    }

    static void PrintThisNamespace()
    {
        Console.WriteLine($"Namespace : {typeof(Program).Namespace ?? "null"}");
    }
}

 

이와 같은 사실은 Visual Studio의 Solution Explorer에서도 확인할 수 있는데 다음과 같이 Program.Functions.cs file이 자동으로 생성된 partial Program class의 partial Program과 병합되었음을 표시하고 있습니다.

 

partial Program class안에서 함수를 생성하고 별도의 file로 분리된 Program.cs에서 해당 함수를 호출하도록 하면 <Main>$ 안에서 지역함수로 이를 추가하는 대신 함수는 <Main>$ method와 동일한 수준으로 자동으로 생성된 Program class와 병합할 것입니다.

 

예제에서는 namespace 선언이 없다는 점의 주목할 필요가 있습니다. 자동적으로 생성된 Prgoram class와 명시적으로 선언된 Program class둘다 기본적으로 namespace는 null입니다.

partial Program class에서 namespace는 선언하지 않는것이 좋습니다. 만약 그렇게 한다면 이것은 별개의 namespace가 되고 결국 자동으로 생성된 partial Program class와 병합되지 않을 수 있기 때문입니다.

 

선택적으로 Program class의 모든 정적 method를 명시적으로 private으로 선언할 수 있지만 그렇게 하지 않아도 기본적으로 private이 적용됩니다. 모든 함수는 Program class안에서 자체적으로 호출되기 때문에 접근한정자는 그리 중요하지 않습니다.

 

3) 함수로 구구단 구현하기

 

초등학생의 구구단 학습을 돕기위해 특정 숫자에 대한 구구단표를 출력하는 application을 만들어야 한다고 가정해 보겠습니다. 예를 들어 5단이라면 아래와 같이 표시할 수 있습니다.

 

5 × 1 = 5
5 × 2 = 10
5 × 3 = 15
5 × 4 = 20
5 × 5 = 25
5 × 6 = 30
5 × 7 = 35
5 × 8 = 40
5 × 9 = 45

 

학생의 학습정도에 따라 구구단은 10이나 12 심지어 20까지의 곱셈 계산을 표시할 수도 있습니다.

 

이전에 우리는 for문에 대해 알아보았는데 이를 이용해 규칙적인 pattern으로 반복되는 처리를 수행할 수 있습니다. 예를 들어 5에 대한 12까지의 곱셈계산을 표시하려면 다음 예제와 비슷하게 구현할 수 있을 것입니다.

for (int mtp = 1; mtp <= 12; mtp++)
{
    Console.WriteLine($"5 × {mtp} = {5 * mtp}");
}

 

그러나 위 예제처럼 5에 대한 12까지의 구구단만 표시하는 대신 이를 더 개선하여 지정한 숫자에 대한 특정 횟수만큼의 구구단을 표시할 수 있는데, 이는 함수를 통해 해결할 수 있습니다.

 

아래 예제는 이러한 구구단을 표시하는 것으로서 255까지 모든 숫자에 대해서 255까지 지정한 수의 구구단(기본값은 12)을 표시하도록 합니다.

 

예제 작성을 위해 Progra.Functions.cs의 partial Program class에서 Gugudan이름의 함수를 아래와 같이 작성합니다.

partial class Program
{
    static void Gugudan(byte number, byte size = 12)
    {
        for (int mtp = 1; mtp <= size; mtp++)
        {
            Console.WriteLine($"{number} x {mtp} = {number * mtp}");
        }

        Console.WriteLine();
    }
}

 

위 예제에서는 아래 사항에 주목할 필요가 있습니다.

  • Gugudan 함수에서는 number라는 매개변수로 byte의 값을 전달하고 있습니다.
  • Gugudan 함수는 byte값을 받는 size라는 매개변수를 갖고 있으나 기본값 12가 지정되어 있으므로 필요한 경우에만 값을 선택적으로 전달할 수 있습니다.
  • Gugudan 함수는 정적 method인 <Main>$에서 호출되어야 하므로 정적 method로 구현되었습니다.
  • Gugudan 함수는 호출자에게 값을 반환하지 않습니다. 따라서 함수이름 앞에 void keyword가 사용되었습니다.
  • Gugudan 함수는 for문을 사용해 number에 대한 구구단을 size와 동일한 수만큼 표시합니다.

Program.cs에서는 아래와 같이 함수를 호출하는 문을 작성합니다. 이때 number 매개변수로 전달할 byte값을 예제의 5와 같이 지정합니다.

Gugudan(5);

 

예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

이번에는 size 매개변수에 대한 값을 20으로 설정합니다.

Gugudan(5, 20);

 

예제를 실행하고 5에 대한 20까지의 구구단이 표시되는지 확인합니다.

함수가 하나 또는 그 이상의 매개변수를 가지고 있고 여기에 값을 전달하는 것만으로 충분한 의미를 전달하지 못하는 경우 필요에 따라 값과 함께 매개변수의 이름을 아래와 같이 지정할 수 있습니다.

Gugudan(number: 5, size: 20);

 

함수로 전달되는 number의 값을 0에서 255사이의 다른 byte값으로 변경하여 정확한 식을 나타내는지 확인합니다. 만약 값을 전달할 때 byte를 벗어나는 값(예를 들어 int나 double과 같은)을 지정한다면 다음과 같이 예외가 발생할 것입니다.

 

(1) 인수와 매개변수

 

많은 수의 개발자는 인수와 매개변수라는 용어를 거의 동일한 의미로 같이 사용하는 경우가 많습니다. 물론 이게 큰 문제가 되지는 않지만 엄밀히 말하면 이 둘은 서로 다른 의미를 가지고 있습니다. 매개변수는 함수에서 정의한 변수를 말합니다. 예를 들어 number와 size는 Gugudan함수의 매개변수가 됩니다.

void Gugudan(byte number, byte size = 12);

 

Method를 호출할때 인수는 매개변수로 전달하는 data자체를 말합니다. 예를 들어 Gugudan 함수를 호출할 때 함수로 전달한 5와 20은 인수가 됩니다.

Gugudan(5, 20);

 

어쩌면 함수로 값을 전달할때 아래와 같이 매개변수의 이름을 지정할 수 있는데

int n = 5;
int s = 20;
Gugudan(number: n, size: s);

 

위와 같은 경우도 number는 매개변수, n은 인수라고 할 수 있습니다.

Microsoft의 공식문서를 보면 명명된/선택적 인수(named and optional arguments)와 명명된/선택적 매개변수(named and optional parameters)라는 문구를 같이 혼용해서 사용하고 있음을 볼 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments

 

그런데 하나의 개체가 매개변수와 인수 둘 다 되는 경우에는 이를 문맥에 따라 판단해야 합니다. 예를 들어 Gugudan함수의 number매개변수가 Display와 같이 다른 함수의 인수로서 전달되는 경우

void Gugudan(byte number, byte size)
{
    ...

    Display(number);
}

 

number는 Gugudan의 매개변수와 Display의 인수가 되는 것입니다. 약간 다른 이야기인데 어떠한 대상에 이름을 짓는 것은 programming에서 가장 어려운 것 중 하나에 해당합니다. 이에 대한 고전적인 예로 C#에서 가장 중요한 함수인 Main을 들 수 있습니다. Main은 args라는 매개변수를 정의하고 있는데 args는 arguments를 줄인 것입니다.

static void Main(string[] args)
{
    ...
}

 

어쨌건 요약하자면 매개변수는 함수의 입력을 정의하는 것이고 인수는 함수를 호출할때 함수에 전달되는 것입니다.

문맥에 따라 정확한 용어를 사용하는 것이 좋지만 다른 개발자가 특정 용어의 사용을 실수하는 경우 그들을 평가절하해서는 안됩니다.

 

4) 값을 반환하는 함수 만들기

 

이전의 예제는 어떠한 동작을 수행하긴 하지만 (동작 후 결과를 console에 표시) 아무런 값도 반환하지 않습니다. 여러분이 만약 부가세 계산을 위한 무엇인가를 만들어야 한다면 부가세를 계산한 결과를 반환하는 함수가 필요할지도 모릅니다.

 

이러한 함수를 만들어 보기 위해 Program.Function.cs에서 CalulateTax라는 이름의 함수를 아래와 같이 작성합니다.

static decimal CalculateTax(decimal amount)
{
    decimal rate = 0.1M;
    return amount += (amount * rate);
}

 

해당 함수는 amount라는 매개변수로 금액에 대한 값이 입력되고 내부에서 0.1(10%)의 부가세를 계산하고 그 결과를 반환합니다. 이 값은 decimal type이 되는데, 이 때문에 함수의 이름 앞에 decimal이라는 data type을 정의하였습니다.

 

Program.cs에서는 아래와 같이 CalculateTax함수를 예제처럼 1000이라는 값을 전달하여 호출합니다.

decimal result = CalculateTax(1000);
Console.WriteLine(result);

예제를 실행하면 아래와 같은 결과를 표시할 것입니다.

 

5) 기수에서 서수로 변환하기

 

어떤것을 세어볼 때 예를 들어 1, 2, 3... 과 같이 사용되는 수를 우리는 기수라고 합니다. 이에 반해 순서를 말할 때는 예를 들어 첫 번째, 두 번째와 같이 서수를 사용합니다. (예제는 1st, 2nd로 대신 사용할 것입니다.) 이와 관련해 함수의 또 다른 예제로 기수에서 서수로 변환하는 기능의 함수를 만들어 볼 것입니다.

 

Program.Function.cs에서 아래와 같이 CardinalToOrdinal이라는 이름의 함수를 작성합니다. 이 함수는 uint값의 기수를 string 값의 서수로 변환하는데, 예를 들어 1이라는 uint값은 문자열인 1st로, 2라는 uint값은 문자열인 2nd등으로 변환합니다.

static string CardinalToOrdinal(uint number)
{
    uint lastTwoDigits = number % 100;
    switch (lastTwoDigits)
    {
        case 11:
        case 12:
        case 13:
            return $"{number:N0}th";
        default:
            uint lastDigit = number % 10;
            string suffix = lastDigit switch
            {
                1 => "st",
                2 => "nd",
                3 => "rd",
                _ => "th"
            };
            return $"{number:N0}{suffix}";
    }
}

 

위 예제는 uint type인 number라는 이름의 하나의 매개변수를 갖고 있습니다. uint type을 사용한 이유는 서수로의 변환에서 음수를 취급할 수 없기 때문입니다.(취급할 필요가 없습니다..) 또한 함수가 실행되고 나면 반환하는 값의 type은 문자열 type입니다. 예제는 내부에서 swicth문을 사용해 11, 12, 13과 같은 특정한 값을 개별적으로 처리되도록 유도하고 있는데 이 외에 다른 값은 swicth 표현식을 사용하여 처리하고 있습니다. 여기서 숫자가 1이면 st접미사를 붙이고 2라면 nd라는 접미사를 붙여줍니다. 이와 동일하게 3이면 rd를, 그 외 다른 값이면 th를 사용합니다.

 

이어서 Program.cs에서는 아래와 같이 해당 함수를 호출하도록 합니다. 이 방식은 1부터 150까지 for문을 통해 순회하면서 각각의 숫자에 CardinalToOrdinal함수를 호출하고 그 결과를 Console에 출력하도록 합니다.

for (uint number = 1; number <= 150; number++)
{
    Console.Write($"{CardinalToOrdinal(number)} ");
}

 

예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

6) 재귀 함수를 통한 계승 계산하기

 

5의 계승은 120이 됩니다. 계승은 대상이 되는 수에서 1작은 것부터 시작해 1씩 줄이면서 최종적으로 1이 될 때까지 반복적으로 곱셈을 수행하기 때문입니다. 따라서 5의 계승은 5×4×3×2×1 = 120이 됩니다.

 

만약 계승을 구현하는 함수를 만들어야 한다면 0, 1, 2, 3처럼 음수가 아닌 수를 계산하도록 정의되어야 합니다. 따라서 함수는 CardinalToOrdinal함수를 만들때처럼 입력 매개변수를 uint를 사용하여 정의함으로써 함수가 음수를 받을 수 없도록 할 수 있으나 이번에 만들 예제에서는 인수 관련 예외를 던져 음수에 대한 처리를 진행하고자 합니다.

 

수학에서 계승은 5!와 같이 나타냅니다. 여기서 느낌표(!)는 "Bang"으로 읽으므로 5! = 120에서 5뱅은 120과 같다고 해석할 수 있습니다. Bang이라는 용어는 폭발한다는 의미와 같이 size의 급속적인 증가를 의미하므로 계승과 꽤 어울리는 용어인 듯합니다.

 

예제로 작성할 함수는 Factorial이라는 이름의 함수로 매개변수를 통해 int형으로 전달되는 값의 계승을 계산할 것입니다. 함수는 또한 재귀라는 기법을 사용하는데 이 기법은 함수가 구현되는 곳에서 자기 자신을 직접적으로 혹은 간접적으로 호출하는 것을 말합니다.

Program.Functions.cs에서 Factorial함수를 아래와 같이 작성합니다.

static int Factorial(int number)
{
    if (number < 0)
    {
        throw new ArgumentOutOfRangeException(message: $"함수의 매개변수는 음수를 사용할 수 없습니다. : {number}", paramName: nameof(number));
    }
    else if (number == 0)
    {
        return 1;
    }
    else
    {
        return number * Factorial(number - 1);
    }
}

 

상기예제에서는 아래와 같이 몇 가지 주목할만한 내용을 포함하고 있습니다.

 

● 입력 매개변수의 수가 음수면 Factorial은 예외를 발생시킵니다.
● 입력 매개변수의 수가 0이면 Factorial은 1을 반환합니다.
● 입력 매개변수의 수가 0보다 크면(상기 외에 다른 모든 경우) Factorial은 자기 자신을 호출한 결과에 대해 곱셈을 수행하고 매개변수의 수보다 1적은 수를 전달합니다. 이러한 동작으로 함수를 재귀적으로 만들게 됩니다.

재귀함수는 지루한 반복을 수행하는 편리한 방법이지만 함수를 호출할 때마다 data를 memory에 저장하게 되므로 너무 많은 함수의 호출이 이루어지면 stack overflow와 같은 문제를 일으키기 쉽습니다. 아래글을 통해 더 자세한 사항을 확인해 보시기 바랍니다.
https://en.wikipedia.org/wiki/Recursion_(computer_science)#Recursion_versus_iteration

 

Program.cs에서는 아래와 같이 구문을 작성합니다. 아래 예제는 for문을 통해 1에서 15까지 수에 대한 계승을 출력하도록 하는데 for문 내부에서 Factorial함수를 반복적으로 호출하고 N0 format code를 사용해 소수점 이하 자릿수가 0인 수에 대한 천 단위 구분결과를 출력합니다.

for (int i = 1; i <= 15; i++)
{
    Console.WriteLine($"{i}! = {Factorial(i):N0}");
}

 

예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

위 실행결과로는 분명하게 드러나지 않지만 13 이상의 계승은 너무 큰 값이므로 int type에 대한 overflow가 발생하였습니다. 12! 의 값은 479,001,600으로 10억의 절반입니다. int변수가 저장할 수 있는 최대정수값은 대략 20억 정도인데 13! 은 60억 정도인 6,227,020,800 값입니다. 이 값을 32bit로 저장을 시도할 때 overflow가 발생하였고 어떠한 오류도 없이 이에 대한 결과만을 출력한 것입니다.

 

만약 overflow가 발생했을 때 이에 대한 예외를 보고자 한다면 이전에 알아보았던 방법인 checked를 사용할 수 있을 것입니다.(물론 32bit int를 사용하는 대신 64bit long을 사용하면 예외걱정 없이 application을 사용할 수 있겠지만 계승을 구하고자 하는 수를 늘리게 되면 머지않아 overflow를 또다시 마주하게 될 것입니다.)

 

Factorial함수에서 return부분을 아래와 같이 변경합니다.

checked
{
    return number * Factorial(number - 1);
}

 

그리고 Program.cs에서도 for문의 1을 -1로 변경하고 Factorial함수를 호출할 때 overflow및 다른 예외를 처리하기 위해 아래와 같이 code를 변경합니다.

for (int i = -1; i <= 15; i++)
{
    try {
        Console.WriteLine($"{i}! = {Factorial(i):N0}");
    }
    catch (OverflowException)
    {
        Console.WriteLine($"{i}!는 32bit int에서 overflow가 발생합니다.");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Application 실행 중 오류발생 : {ex.Message}");
    }
}

 

예제를 실행하면 다음과 같은 결과를 발생시킬 것입니다.

 

7) XML Comment로 함수 문서화 하기

 

기본적으로 위 예제와 같은 함수를 호출할때 code editor에서는 기본적인 정보만을 tooltip으로 표시합니다.

 

이 상태에서 필요하다면 일부 정보를 추가하여 함수에 대해 더 자세한 내용이 tooltip을 통해 표시되도록 할 수 있습니다. 이를 우리는 C# XML documentation comments라고 합니다.

 

만약 C# extention이 설치된 VS Code를 사용한다면 View -> Command Palette -> Preferences: Open Settings (UI)에서 formatOnType을 검색하고 해당 기능이 활성화되어 있는지 확인합니다. C# XML documentation comments는 Visual Studio와 Rider에서는 기본기능에 해당하므로 별다른 설정이 필요하지 않습니다.

 

위에서 예제로 작성한 CardinalToOrdinal함수 바로 위에서 /// (slashe 3개)를 입력합니다. 그러면 아래와 같이 XML 주석을 입력할 준비가 자동으로 완성됩니다. 예제에서는 number라는 단일 매개변수가 함수에 존재함을 알려주고 있습니다.

 

그리고 아래와 같이 XML 주석으로 적당한 정보를 입력합니다. 예제에서는 summary와 param을 통한 입력매개변수, returns에서 반환값에 대한 설명을 추가하였습니다.

/// <summary>
/// 정수값을 서수로 변환합니다.
/// </summary>
/// <param name="number">음수값을 제와한 정수를 전달합니다.</param>
/// <returns>문자열형태의 서수가 반환됩니다.</returns>

 

위와 같이 정의한 뒤 함수를 호출해 보면 입력한 정보가 tooltip으로 표시될 것입니다.

 

이와 같은 기능은 Sandcastle처럼 주석을 문서화하는 데 사용할 수 있도록 설계된 것으로 아래 link에서 더 자세한 사항을 확인하실 수 있습니다.

Tooltip은 단지 code를 입력하거나 함수 이름에 mouse pointer를 올려두면 나타나는 부가적인 기능에 해당합니다.
https://github.com/EWSoftware/SHFB

 

지역 함수의 경우에는 외부에서 member로 접근할 수 없기 때문에 XML 주석을 지원하지 않으므로 문서화하는 것은 불가능하며 tooltip으로 관련정보를 확인하는 기능 역시 지원하지 않습니다.

지역함수를 제외하고 되도록 이면 함수모두에 XML주석을 추가할 것을 권장합니다.

 

8) 함수에 lambda사용하기

 

F#은 Microsoft의 강력한 함수형 Programming언어이며 C#와 마찬가지로 IL(Intermediate Language)로 compile되어 .NET에서 실행됩니다. 함수형 언어는 함수를 기반으로 하는 계산 system인 lambda 계산식에서 발달한 것으로 code는 기존 절차적 언어보다는 수학적 함수에 가깝습니다.

 

힘수형 언어의 특징으로는 다음과 같은 것을 얘기할 수 있습니다.

 

  • Modularity : C#에서 함수를 정의하는 것과 동일한 이익이 함수형 언어에도 적용됩니다. 이 것은 복잡한 code기반의 큰 덩어리를 더 작은 조각으로 분해할 수 있게 합니다.
  • Immutablity : C#의미의 변수는 존재하지 않습니다. 함수내부의 모든 data값은 바뀔 수 없습니다. 대신 새로운 data값이 기존의 것으로부터 생성되는데 이것은 bug를 줄여줍니다.
  • Maintainability : 함수는 code를 더 명확하고 깔끔하게 유지할 수 있도록 해줍니다.(특히 수학적 경향을 가진 개발자에게 더 적합합니다.)

C# 6부터는 언어에 더 함수적인 접근법을 사용하기 위해서 필요한 기능을 꾸준히 추가해 왔습니다. 예를 들어 C# 7에서 tuple와 pattern matching이 추가되었으며 C# 8에서 non-null 참조 type이 추가되고 C# 9에서 pattern matching을 향상하고 잠재적으로 불변 개체인 record를 추가하였습니다.

 

또한 C# 6에서 Microsoft는 expression-bodied function members를 위한 지원을 추가하였는데 우리는 이에 대한 예제를 곧 살펴볼 것입니다. C#에서는 lambda는 함수로부터 반환값을 나타내기 위해 => 문자를 사용합니다.

 

Fibonacci 수열은 항상 0과 1로 시작하며 수열의 나머지는 이전 2개의 숫자를 더하는 규칙으로 아래와 같이 생성됩니다.

0 1 1 2 3 5 8 13 21 34 55...

 

수열의 다음항은 34 + 55가 되며 이는 89입니다.

 

우리는 Fibonacci 수열을 사용하여 명령형과 선언형에 대한 차이를 나타낼 수 있습니다.

Program.Function.cs에서 FibImperative이름의 함수를 아래와 같이 작성합니다. 이는 명령형 style에 해당합니다.

static int FibImperative(uint term)
{
    if (term == 0)
    {
        throw new ArgumentOutOfRangeException();
    }
    else if (term == 1)
    {
        return 0;
    }
    else if (term == 2)
    {
        return 1;
    }
    else
    {
        return FibImperative(term - 1) + FibImperative(term - 2);
    }
}

 

그리고 Program.cs에서는 아래와 같이 for문 안에서 1부터 30까지 순회하면서 FibImperative함수가 호출되도록 합니다.

for (uint i = 1; i <= 30; i++)
{
    Console.WriteLine("Fibonacci 수열 {0} 항은 {1:N0}.",
        arg0: CardinalToOrdinal(i),
        arg1: FibImperative(term: i)
    );
}

 

예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

이번에는 Program.Functions.cs에서 아래와 같이 FibFunctional이름의 함수를 작성합니다. 이는 선언형 style에 해당합니

다.

static int FibFunctional(uint term) => term switch {
    0 => throw new ArgumentOutOfRangeException(),
    1 => 0,
    2 => 1,
    _ => FibFunctional(term - 1) + FibFunctional(term - 2)
};

 

Program.cs에서는 기존 FibImperative대신 FibFunctional를 호출하도록 변경합니다.

for (uint i = 1; i <= 30; i++)
{
    Console.WriteLine("Fibonacci 수열 {0} 항은 {1:N0}.",
        arg0: CardinalToOrdinal(i),
        arg1: FibFunctional(term: i)
    );
}

 

예제를 실행하면 기존과 같은 결과를 확인할 수 있습니다.

2. Debugging

 

Application 개발을 진행하는 동안에는 application에 많은 문제가 발생할 수 있으며 이를 해결하기 위해 Debug라는 과정을 거치게 됩니다. 그리고 이를 위해 Visual Studio나 VS Code는 이를 위한 훌륭한 도구를 내장하고 있습니다.

 

1) Bug를 가진 예제 만들기

 

Debugging 하는 과정이 어떻게 진행되는지를 알아보기 위해 약간의 문제점을 가진 예제를 만들어 볼 것입니다. Debugging에는 code editor의 debugging 도구를 사용할 것이며 이를 이용해 code의 문제점을 추적하고 수정할 수 있습니다.

 

예제를 만들기 위해 console app project를 생성하고 Program.cs에서 기존의 구문을 모두 제거한 뒤 다음과 같은 함수를 작성합니다.

int Add(int i, int j)
{
    return i * j;
}

 

그리고 작성한 함수 위에서 몇 가지 변수를 선언하고 값을 설정한 뒤 덧셈을 구하기 위해 해당 함수를 아래와 같이 호출합니다.

int a = 10;
int b = 20;

int r = Add(a, b);

Console.WriteLine(r);

 

예제를 실행하면 다음과 같은 결과를 확인하실 수 있습니다.

 

10과 20의 합은 30일 거라고 예상했지만 실행결과는 200이 되었습니다. 이제 debugging도구를 사용하여 해당 bug를 추적해 적절히 수정할 것입니다.

 

2) Breakpoint를 설정하고 debugging 하기

 

Breakpoint는 code의 특정 line에 표시하는 것으로 application이 실행되고 처리를 진행하는 중 해당 line에 도달하면 실행을 멈추게 됩니다. 이를 이용해 우리는 application의 상태를 확인하고 필요한 조치를 취할 수 있습니다.

 

(1) Visual Studio

 

Visual Studio에서 breakpoint를 설정하고 debugging을 시작하기 위해 우선 변수 a를 선언한 첫 번째 line에서 Debug -> Toggle Brakpoint를 선택하거나 F9 key를 눌러줍니다. 그러면 붉은색 원형이 왼쪽 여백 부분에 표시되고 해당 line의 구문이 붉은색으로 강조되어 여기에 breakpoint가 설정되었음을 나타내게 됩니다.

Breakpoint는 동일한 동작을 통해 on/off를 전환할 수 있습니다. 또한 line의 왼쪽 여백을 click 함으로써 breakpoint를 설정하거나 해제할 수 있고 mouse 오른쪽 button으로 click 하면 삭제, 해제나 조건 편집 또는 기존 breakpoint에 대한 동작등 사용가능한 더 많은 option이 표시됩니다.

 

Breakpoint가 설정되면 Debug -> Start Debugging을 선택하거나 F5 key를 눌러줍니다. 그러면 Visual Studio는 에제를 실행시키고 breakpoint에 실행시점이 도달하는 순간 실행을 멈출 것입니다(break mode). 또한 이때는 Debugging 도구와 함께 지역변수의 현재 값을 표시하는 Locals나 정의된 모든 표현식을 나타내는 Watch 1, Call Stack, Exception Settings, Immediate Window 등 여러 창이 함께 나타나는 것을 확인할 수 있습니다. 이 상태에서 화면을 보면 다음에 실행될 line은 왼쪽 여백 부분에 노란색 화살표와 함께 line자체가 노란색으로 강조되어 표시되어 있음을 알 수 있습니다.

 

(2) Visual Studio Code

 

VS Code에서 breakpoint를 설정하고 debugging을 시작하기 위해 변수 a를 선언한 첫 번째 line에서 Run -> Toggle Breakpoint를 선택하거나 F9 key를 눌러줍니다. 그러면 붉은색 원형이 왼쪽여백에 표시됨으로써 breakpoint가 설정되었음을 알려주게 됩니다.

Breakpoint는 동일한 동작을 통해 on/off를 전환할 수 있습니다. 또한 왼쪽 여백에 mouse를 click 함으로써 on/off를 동일하게 전환할 수 있고 mouse오른쪽 button으로 click 하면 remove, edit나 기존 breakpoint의 비활성, breakpoint 추가, 조건부 breakpoint 또는 아직 breakpoint가 존재하지 않을 때 Logpoints 등의 더 많은 option을 볼 수 있습니다. Logpoints는 Tracepoints라는 것으로 해당 지점에서 code의 실행을 멈추지 않고도 일부 필요한 정보를 기록해 둘 수 있는 기능입니다.

 

이 상태에서 View -> Run을 click 하거나 왼쪽 navigation bar에서 Run and Debug icon(삼각형 모양의 Play와 Bug)을 click, 또는 Ctrl + Shift + D를 눌러줍니다. 그리고 RUN AND DEBUG의 상단에서 Run and Debug button을 click한뒤 Debugging project를 선택합니다.

이렇게 하면 VSCode는 예제를 실행하고 실행시점이 breakpoint에 도달하면 실행을 멈추게 됩니다(break mode). 또한 다음에 실행될 line은 여백 부분에 삼각형모양과 노란색표시로 강조되어 표시될 것입니다.

 

(3) Debugging Toolbar

 

Visual Studio는 표준 toolbar에 debugging과 관련한 2개의 button을 갖고 있으며 이를 통해 debugging을 시작하고 실행 중인 code에 대한 hot reload를 사용할 수 있습니다. 역기서 debug toolbar는 별개로 분리되어 있습니다. VS Code 역시 아래와 같이 debugging기능을 사용하기 위한 button이 탑재된 toolbar를 제공하고 있습니다.

Toolbar에서 button의 역할은 다음과 같습니다.

  • Start/Continue(F5) : 해당 button은 상황에 따라 project를 실행하거나 현재 위치에서부터 다음 breakpoint 혹은 project의 종료 시까지 project를 계속실행합니다.
  • Hot Reload : Project를 재실행하지 않고서도 compile 된 code의 변경사항을 다시 load 합니다.
  • Break All : 실행 중인 project에서 가능한 다음 code로 이동합니다.
  • Stop Debugging/Stop(Shift + F5) : Debugging을 중지합니다.
  • Restart(Ctrl + Shift + F5) : Debugging중인 Project의 실행을 중지하고 바로 다시 Debugging을 실행합니다.
  • Show Next Statement : 현재 cursor를 실행할 다음 문으로 이동합니다.
  • Step Into(F11), Step Over(F10), Step Out(Shift + F11) : Button에 따른 다양한 방식으로 code구문을 단계적으로 실행합니다.
  • Show Threads in Source : Debugging중인 application의 thread를 확인합니다.

(4) Debugging Windows

 

Debugging 중에는 Visual Studio와 VS Code에서 아래와 같은 다양한 화면을 통해 code를 절차적으로 확인하는 동안 변수와 같은 유용한 정보를 monitoring 할 수 있습니다.

  • VARIABLES : Locals를 포함해 모든 지역 변수의 type이름과 값을 자동적으로 표시합니다.
  • WATCH/Watch 1 : 변수의 값 혹은 임의로 입력한 표현식의 값을 표시합니다.
  • CALL STACK : 함수를 호출한 stack을 표시합니다.
  • BREAKPOINTS : 모든 breakpoint를 표시하며 이들을 제어할 수 있습니다.

Break mode에서는 또한 editor영역 하단에 아래에서 다음 화면을 찾아볼 수 있습니다.

  • DEBUG CONSOLE / Immediate Window : code의 상호작용을 수행하는 곳으로써 변수의 이름을 입력하는 등의 방법으로 program의 상태를 질의할 수 있습니다. 예를 들어 1+2를 입력하면 해당 결과가 즉시 표시됩니다.

3) Code를 절차적으로 확인하기

 

Visual Studio 또는 VS Code를 사용하는 경우 code를 절차적으로 확인하기 위한 방법을 알아보고자 합니다.

Debugging을 위한 menu명령은 Visual Studio의 Debug menu나 VS Code 및 Rider의 실행 menu상에 존재합니다.

 

Program.cs의 첫 번째 line에 break point를 지정하고 debugging을 시작합니다. 첫번째 line에서 실행이 대기 중일 때 Run 또는 Debug의 Step Into나 toolbar에서 Step Into를 click 하거나 F11 key를 누르면 노란색 강조선이 다음 line에 표시될 것입니다.

 

계속해서 Run 또는 Debug에서 Step Over 혹은 toolbar에서 Step Over를 click 하거나 F10 key를 눌러줍니다. 그러면 이전과 마찬가지로 노란색 강조선이 다음 line에 표시될 것입니다. 여기까지는 단일문만을 실행하고 있으므로 Step Over와 Step Into사이에 차이가 없을 것입니다.

 

여기까지 진행하면 해당 시점부터는 아마도 Add method를 호출하는 부분까지 도달해 있을 것입니다.

Step Into와 Step Over의 차이는 method의 호출을 실행하는 시점에 알 수 있습니다. Step Into를 click 하면 debugger는 method의 안으로 진입하게 되므로 method의 모든 line을 절차적으로 실행할 수 있습니다. 그렇지 않고 Step Over를 click하면 method자체를 실행한 후 그다음 line으로 절차를 진행하게 됩니다.

 

Step Into를 click 하여 Add method안으로 진입합니다.

 

Code editor화면에서 a와 b매개변수에 mouse pointer를 올려두면 tootip을 통해 현재 값이 표시됩니다.

 

Add method안에서 a * b표현식을 선택하고 mouse오른쪽 button을 눌러 Add to Watch 또는 Add Watch를 선택합니다. 그러면 해당 표현식은 WATCH 혹은 Watch 1 window에 추가되고 해당 표현식의 실행결과가 11.25 임을 표시하게 됩니다.

 

WATCH 또는 Watch 1 window에서 표현식을 mouse오른쪽 button으로 click 한 후 'Remove Expression'또는 'Delete Watch'를 선택합니다.

 

이제 Add method에서 *를 +로 변경하고 원형 화살표 모양의 Restart button을 click 하거나 Ctrl + Shift + F5 key를 눌러 debugging을 재시작합니다.

 

이번에는 Step Over를 사용하여 함수 전체를 실행하고 결과가 정확한지를 확인한 뒤 Continue button이나 F5 key를 눌러줍니다.

 

VS Code라면 console에 결과를 출력할 때 TERMINAL window대신 DEBUG CONSOLE에 결과가 표시되었는지를 확인해 보시기 바랍니다.

 

4) VS Code의 내장 Terminal 사용하기

 

Debugging 하는 동안에 console은 기본적으로 내부 DEBUG CONSOLE을 사용하도록 설정되는데 문제는 ReadLine method와 같이 어떤 입력을 필요로 하는 상황에서 상호작용이 불가능하다는 것입니다. 때문에 원한다면 내부 termnal을 대신 사용하도록 이 설정을 변경할 수 있습니다. 우선 사용자와의 상호작용이 요구되는 예제를 작성하기 위해 Program.cs에서 사용자에게 숫자를 입력하도록 한 뒤 변수 d로 double값을 읽어 들이는 다음의 예제를 작성합니다.

Console.WriteLine("숫자를 입력하세요.");
string number = Console.ReadLine();

double d = double.Parse(number);

 

사용자에게 숫자입력을 요청하는 첫 번째 line에 breakpoint를 설정하고 RUN AND DEBUG Window에서 Run and Debug button을 click 합니다. 그런 뒤 Debugging project를 선택합니다.

 

만약 사용자에게 숫자입력을 요청하는 message자체가 TERMINAL이나 DEBUG CONSOLE에 표시되지 않고 숫자 입력 후 Enter를 누를 때까지 대기하게 된다면 Debugging을 중지하고 RUN AND DEBUG Window에 있는 톱니바퀴모양을 clcick 해 줍니다.

 

그러면 launch.json file을 편집하기 위한 화면이 열리게 되는데 이 상태에서 RUN AND DEBUG의 오른쪽 drop down menu에 있는 'Add configuration..'항목을 선택하고 이어서 '.NET: Launch Executable file (Console)'을 선택하면 launch.json에서는 다음과 같은 추가적인 설정정보가 추가될 것입니다.

 

program path에서 workspaceFolder환경변수 다음에 Debugging Project folder를 확인하고 <target-framework> 부분을 9.0으로, <project-name.dll>을 임의의 DLL file값으로 변경합니다.

 

이제 마지막으로 console 설정을 internalConsole에서부터 integratedTerminal로 변경하면, 아래와 같거나 비슷하게 설정이 완성될 것입니다.

"version": "0.2.0",
"configurations": [
    {
        "name": ".NET Core Launch (console)",
        "type": "coreclr",
        "request": "launch",
        // "preLaunchTask": "build",
        "program": "${workspaceFolder}/bin/Debug/net9.0/Sample.dll",
        "args": [],
        "cwd": "${workspaceFolder}",
        "stopAtEntry": false,
        "console": "integratedTerminal"
    }
]

 

다시 RUN AND DEBUG 화면으로 돌아가 오른쪽에 있는 녹색 삼각형모양 button인 Start Debugging button을 click 합니다. 그리고 하단의 TERMINAL window가 Sample.dll에 첨부되었음을 확인합니다. 이제 2회에 걸쳐 Step Over를 진행시킨 뒤 10.5를 입력하고 Enter를 눌러줍니다.

 

이후 남은 구분에 대한 절차를 계속 진행하거나 F5 또는 Continue button을 click 하여 내부 terminal에 결과가 표시되었는지 확인합니다.

 

5) Breakpoint 사용하기

 

이전 debugging을 계속진행 중이라면 debugging toolbar에서 Stop button을 click 하거나 Run 또는 Debug에서 Stop Debugging을 선택합니다. 아니면 Shift + F5 단축 key를 통해서도 debugging을 중단할 수 있습니다.

 

그런 뒤 Run -> Remove All Breakpoints 또는 Debug -> Delete All Breakpoints를 선택하여 현재 존재하는 모든 breakpoint를 제거합니다.

 

예제를 통해 Breakpoint를 사용하기 위한 방법을 알아보기 위해 다음과 같은 예제를 작성합니다.

int a = 10;
int b = 20;

int r = Add(a, b);

Console.WriteLine(r);

int Add(int i, int j)
{
    return i * j;
}

 

예제에서 WriteLine부분을 click 하고 F9 또는 Run이나 Debug에서 Toggle Breakpoint를 선택해 breakpoint를 설정합니다.

 

설정된 breakpoint에서 mouse오른쪽 button을 누르고 VS Code인 경우 Edit Breakpoint를, Visual Studio인 경우 Conditions를 선택합니다.

 

그런 다음 아래와 같이 r변수가 30이 된다는 의미의 표현식을 작성합니다. 따라서 breakpoint가 작동하기 위해서는 해당 표현식의 결과가 true가 되어야 합니다.

 

설정을 완료하고 debugging을 시작합니다. 아마도 설정한 breakpoint가 작동하지 않았을 것입니다.

 

Breakpoint 혹은 Conditions에서 r의 값이 30 이상인 경우로 표현식을 변경합니다.

 

다시 debugging을 시작하면 이번에는 breakpoint가 작동되어 실행이 중지됨을 알 수 있습니다.

 

참고로 Breakpoint 혹은 Conditions에서 Hit Count를 선택하고 3과 같은 값을 입력하면 해당 설정한 값에 따라 지정한 breakpoint로 3번 정도 도달할 때(설정한 조건에 맞는 경우에만)에만 breakpoint가 작동되도록 합니다.

 

설정한 breakpoint의 조건은 breakpoint로 mouse를 올려두면 나타나는 요약정보를 통해 알 수 있습니다.

 

6) Hot Reload

 

Hot Reload는 application이 실행 중인 동안 개발자가 code의 변경사항을 바로 적용할 수 있도록 하는 기능으로서 즉각적으로 변경된 결과를 확인할 수 있습니다. 이 기능의 목적은 bug의 수정을 빠르게 진행하기 위한 것으로 이를 편집하며 계속하기(Edit and Continue)라고도 합니다. 다만 모든 code의 변경사항을 지원하는 것은 아니고 아직까지는 가능한 유형이 정해져 있습니다. 자세한 사항은 아래 link를 참고하시기 바랍니다. https://learn.microsoft.com/en-us/visualstudio/debugger/supported-code-changes-csharp?view=vs-2022

 

참고로 .NET 6가 release 되기 전에 Microsoft의 한 관리자는 이 기능을 Visual Studio안에서만 적용하도록 시도하였으나 Microsoft내 open-source team에 의해 이 계획은 바뀌게 되었고 그로 인해 Hot Reload는 command-line도구에서도 사용가능하도록 남아 있게 되었습니다.

 

Hot Reload를 사용해 보기 위해 아래와 같은 간단한 예제를 작성합니다.

while (true)
{
    Console.WriteLine("Hot Reload");
    await Task.Delay(1000);
}

 

(1) Visual Studio에서 Hot Reload사용하기

 

Visual Studio에서 Hot Reload는 사용자 interface에서 사용할 수 있습니다.

 

Visual Studio에서 상기 예제의 project를 실행하여 message가 1초에 한 번씩 표시되는지 확인합니다. 그리고 Project의 실행을 중지하지 않고 Program.cs에서 message의 내용을 Hot Reload에서 every one second로 변경한 뒤 Debug -> Apply Code Change 또는 toolbar에서 Hot Reload button을 click 합니다. 그 상태에서 project를 재실행하지 않고도 변경사항이 적용되었는지 확인합니다.

 

이번에는 Hot Reload button의 drop down menu를 열고 Hot Reload on File Save를 선택합니다.

 

message를 다시 Hot Reload로 변경한 뒤 이번에는 file을 저장만 하고 변경사항이 자동으로 반영되는지 확인합니다.

 

(2) VS Code와 dotnet watch를 통해 Hot Reload사용하기

 

VS Code의 경우 Hot Reload를 사용하기 위해서는 project를 실행할 때 특정명령어를 사용해야 합니다. VS Code의 TERMINAL에서 아래와 같이 dotnet watch를 사용해 project를 실행하고 Hot Reload를 사용할 준비가 되었는지 확인합니다.

 

VS Code에서도 이전과 동일하게 every one second로 message를 변경합니다. 그리고 곧장 변경사항이 적용되는지 확인합니다.

 

Ctrl + C key를 눌러 실행을 중지하면 아래와 같이 Hot Reload기능이 종료되는지 확인합니다.

 

지금까지는 개발단계에서 application의 bug를 찾고 이를 어떻게 개선할 수 있는지에 대한 방법을 알아보았습니다. 이제 다음 단계로 test를 작성함으로써 개발단계와 실제사용단계에서 발생할 수 있는 잠재적인 문제점들을 어떤 방식으로 찾아낼 수 있는지를 알아볼 것입니다.

 

4. 단위 TEST

 

Code의 bug를 수정하는 것은 비용이 많이 들지만 개발과정에서 bug를 조기에 찾아 이를 수정하면 비용을 훨씬 줄일 수 있습니다. 이와 같은 이유로 단위 test는 해당 기능이 대단위 기능으로 통합되거나 사용자의 test로 발견되기 전에 작은 단위의 test를 수행함으로써 bug를 조기에 발견하기 위한 방법으로 활용되고 있습니다. 일부 개발자의 경우에는 실제 code를 작성하기 전에 단위 test를 먼저 생성하도록 하는 원칙을 따르고 있는데 이를 TDD(TEST-Driven develpment:TEST 주도 개발)라고 합니다.

 

단위 test framework 중에는 Microsoft사의 MSTest라는 단위 test framework가 있으며 이 외에 NUnit라는 framework 또한 많이 사용되고 있습니다. 하지만 지금 사용해 볼 예제에서는 무료이자 open-source인 xUnit라는 framework를 단위 test로 사용해볼 것입니다. 사실 xUnit는 NUnit를 build 한 동일한 team에서 개발한 것인데 확장성이 뛰어나며 community에 의한 꾸준한 지원이 계속해서 이뤄지고 있습니다.

만약 이와 같은 test system에 관해 관심이 있다면 google에서 검색해 보는 것만으로 동일한 관심사를 갖는 사람들에 의해 작성된 수많은 글을 찾아볼 수 있습니다. https://www.google.com/search?q=xunit+vs+nunit

 

1) TEST 유형

 

Application을 Test 하는 방법에는 다양한 유형이 존재하며 단위 test는 이러한 방법 중 하나일 뿐입니다.

TEST 유형 목적
단위 Code에서의 최소 단위를 test하는 것으로 일반적으로 method 혹은 함수등이 이에 속합니다. 단위 test는 필요한 경우 의존성으로 부터 분리된 code의 단위에 대해 모의 test를 수행하는 것으로 각 단위는 여러 경우에 대한 test를 거칠 수 있습니다.(일반적인 입력을 통한 예상된 결과의 확인, 극한의 입력을 통한 가용성 확인, 의도적인 입력 오류를 통한 예외 처리 확인등)
통합 작은 단위와 대규모 component가 하나의 단위로 결합되어 동작하는 경우에 대한 test로 경우에 따라 source code가 없는 외부 component와의 통합이 이루어져야 하는 경우도 있습니다.
system Application이 실행될 전체 system환경의 test입니다.
성능 Application에 대한 성능 test입니다. 예를 들어 Application의 code가 사용자의 요청에 대해 2초이내로 응답하는가를 측정하여 확인합니다.
부하 Application이 제성능을 유지하면서 얼마나 많은 요청을 처리할 수 있는가에 대한 것입니다. 예를 들어 1만명 이상의 사용자가 Application에 동시에 요청을 보내는 경우등을 의미합니다.
사용자 실제 사용자가 자신이 필요로 하는 작업에 Application을 사용하면서 얼마나 많은 불만사항이 제기되는지를 확인합니다.

 

2) TEST를 위한 class library 생성

 

우선 test를 위한 함수를 만들어야 하는데 이를 위해 현재 project와 다른 새로운 class library project를 생성할 것입니다. Class library는 code의 package로서 독립적으로 배포가능하고 필요하다면 다른 .NET application에서도 사용될 수 있습니다.

 

Project의 이름은 TestingLib로 할 것이며 Class Library유형의 Project입니다.

박스 : 이번예제에서는 여러 project를 포함하는 solution(sln)이라는 개념을 사용할 것입니다. Solution안에서 필요한 project를 추가할 때는 목적에 따라 project template만 다르게 선택될 뿐 나머지 절차는 모두 동일하게 이루어집니다.

 

예제를 위해 project를 생성하는 경우 Visual Studio라면

  • 상단 Menu에서 File -> New -> Project를 선택합니다.
  • Create a new project dialog에서 Class Library(C#)을 찾아 선택한 뒤 Next를 눌러줍니다.
  • Configure your new project dialog안에서 Project name에 TestingLib를 입력하고 location은 원하는 folder의 위치를 아무 곳이나 선택해 줍니다. (제 경우는 D:\Study로 설정하였습니다.)
  • Additional information dialog에서 .NET 9.0 (Standard Team Support)를 선택하고 Create를 눌러줍니다.

VS Code의 경우에는

  • Project를 생성할 임의의 folder를 만들고 VS Code의 TERMINAL에서 해당 경로로 folder의 위치를 이동합니다. (제 경우는 D:\Study\TestingLib로 이동하였습니다.)
  • dotnet CLI를 사용해야 하므로 해당 명령을 통해 TestingLib라는 solution을 생성합니다. -> dotnet new sln -n TestingLib
  • 그런 다음 TestingLib라는 이름의 class library project를 생성합니다. -> dotnet new classlib -o TestingLib
  • Project가 생성되면 아래 명령을 통해 solution에 새로운 project folder를 추가합니다. -> dotnet sln add TestingLib

위 명령을 실행결과 'Project `TestingLib\TestingLib.csproj` added to the solution.'이라는 결과가 표시되는지 확인합니다.

 

TestingLib라는 project를 생성하고 나면 기본적으로 Class1.cs라는 이름의 file이 만들어지는데 이 file의 이름을 Testing.cs로 변경하고 아래와 같이 예제를 작성합니다. 참고로 아래 예제는 의도적으로 잘못된 동작을 수행하도록 한 것입니다.

namespace TestingLib;

public class Testing
{
    public int Add(int x, int y)
    {
        return x * y;
    }
}

 

Visual Studio를 사용한다면 Build -> Build TestingLib를 선택하고 VS Code라면 TERMINAL(TERMINAL의 위치는 반드시 project가 존재하는 folder여야 함)에서 dotnet build 명령을 사용합니다. 참고로 build명령은 solution file이 있는 folder에서도 실행할 수 있으나 이때는 solution전체를 build 하게 되는데 예제상으로는 아무런 문제가 없으나 solution전체단위로 build 하는 건 불필요한 동작입니다.

 

build가 정상적으로 진행되었다면 이번에는 TestingLibUnitTest라는 이름으로 xUnit Test Project(C#)를 추가합니다. 이를 위해 Visual Studio에서는

  • Solution Explorer에서 Mouse오른쪽 button을 눌러 Add -> New Project를 선택합니다.
  • Create a new project dialog에서 xUnit Test Project(C#)을 찾아 선택한 뒤 Next를 눌러줍니다.
  • Configure your new project dialog안에서 Project name에 TestingLibUnitTest를 입력하고 Next를 눌러줍니다.
  • Additional information dialog에서 .NET 9.0 (Standard Team Support)를 선택하고 Create를 눌러줍니다.

Visual Studio Code에서는 아래 명령을 사용해 test project를 추가합니다.

dotnet new xunit -o TestingLibUnitTest
dotnet sln add TestingLibUnitTest

 

TestingLibUnitTest가 생성되면 해당 project에서 TestingLib project를 참조추가해야 합니다. Visual Studio에서는 Solution Explorer에서 TestingLibUnitTest를 선택하고 Project -> Add Project Referece를 선택한 뒤 TestingLib의 checkbox를 선택하고 OK button을 눌러주면 됩니다. VS Code에서는 dotnet add reference명령을 사용하거나 TestingLibUnitTest.csproj의 project file에서 ItemGroup을 사용하여 TestingLib project 참조를 아래와 같이 추가해 줍니다.

<ItemGroup>
	<ProjectReference Include="..\TestingLib\TestingLib.csproj" />
</ItemGroup>

 

Project의 참조 경로는 .NET SDK에 의해 현재 운영체제에 맞게 바뀌게 되므로 forwad(/)나 back slash(\) 모두 사용할 수 있습니다.

 

project를 build 하여 정상적으로 진행되는지 확인합니다.

 

3) TEST 작성하기

 

단위 test는 아래 3가지 부분으로 나뉘어 작성되어야 합니다.

  • Arrange : 입력과 출력에 필요한 변수를 선언하고 초기화합니다.
  • Act : 실제 단위 test를 수행하는 부분입니다. 예제상으로는 여기서 우리가 test 하고자 하는 method를 여기서 호출할 것입니다.
  • Assert : 여기서 결과에 대한 하나 또는 그 이상의 예상값을 지정합니다. 예상값과 실제 출력되는 값이 다르면 test는 실패한 것으로 간주됩니다. 예를 들어 2와 2 값을 더할 때 우리는 이 결과를 4로 예상할 수 있고 이 예상값을 실제값과 비교하는 것입니다.

이제 위해서 작성했던 TestingLib의 단위 test를 작성해 볼 것입니다.

TestingLibUnitTest의 UnitTest1.cs file(원하는 경우 해당 file이름을 다른 이름으로 변경할 수 있습니다.)에서 TestingLib namespace를 import 하고 아래와 같이 2개의 test method를 정의합니다. 하나는 2와 2를 더하는 것이고 다른 하나는 2와 3을 더하는 것입니다.

using TestingLib;

namespace TestingLibUnitTest;

public class UnitTest1
{
    [Fact]
    public void TestAdding2And2()
    {
        // Arrange
        int a = 2;
        int b = 2;
        int result = 4;

        Testing addCalc = new();

        // Act
        int actual = addCalc.Add(a, b);

        // Assert
        Assert.Equal(result, actual);
    }

    [Fact]
    public void TestAdding2And3()
    {
        // Arrange
        int a = 2;
        int b = 3;
        int result = 5;

        Testing addCalc = new();

        // Act
        int actual = addCalc.Add(a, b);

        // Assert
        Assert.Equal(result, actual);
    }
}
Visual Studio에서 단위 test project를 생성한 경우라면 UnitTest1.cs에서 이전 방식의 중첩된 namespace를 사용하고 있음을 알 수 있을 것입니다. 반면 dotnet new를 사용했다면 file-scoped namespace를 기본으로 사용하게 됩니다.

 

위와 같이 작성한 후 TestingLibUnitTest project를 build 합니다.

 

4) Visual Studio에서 Test실행하기

 

상기 절차까지 모두 마쳤으면 이제 TEST를 실행해 볼 준비가 된 것입니다.

 

Visual Studio를 사용한다면 Test -> Run All Tests를 선택합니다. 그러면 test를 수행한 후 곧 Test Explorer에서 2개의 test결과가 표시됨을 알 수 있습니다. 예제 test에서 하나는 성공, 다른 하나는 실패로 나타날 것입니다.

 

5) VS Code에서 Test실행하기

 

우선 예제로 작성한 TestingLibUnitTest project를 build 하지 않았다면 해당 project를 build 합니다. 이렇게 하는 이유는 C# Dev Kit extension의 새로운 testing기능이 작성한 단위 test를 명확하게 인식하도록 하기 위한 것입니다.

 

VS Code에서 View -> Testing을 선택합니다. 그러면 Refresh Tests, Run Tests, Debug Tests 등 몇 가지 button을 가진 작은 toolbar를 탑재한 TESTING화면이 표시될 것입니다. 여기에서 TestingLibUnitTest project를 확장하여 2개의 test가 존재하는지 확인합니다.

 

TestingLibUnitTest에 mouse pointer를 올려두면 3개의 삼각형 button이 나오는데 여기서 제일 왼쪽 Run Tests button을 click 합니다.

 

Test가 완료되고 나면 TEST RESULTS tab으로 들어가 2개의 test결과가 표시되어 있음을 확인합니다. 예제 test에서 하나는 성공, 다른 하나는 실패로 나타날 것입니다.

 

6) Bug 고치기

 

이제 위에서 실행한 test의 결과를 바탕으로 bug를 수정해야 합니다. 실제 test결과에서 예상값은 5라고 했지만 실제 값은 6이 되었으므로 Add method를 확인해 잘못된 부분을 확인하고 이를 수정한 뒤 다시 test를 실행하여 모든 test의 결과를 성공으로 표시되는지 확인합니다.

 

7) 다중 매개변숫값 지정하기

 

예제에서는 2개의 단위 test method를 생성했는데 첫 번째는 2와 2의 합을 구하는 것이고 두 번째는 2와 3의 합을 구하는 것입니다. [fact] attribute를 사용한 이러한 방법으로 2개의 method를 구분하는 대신 우리는 [Theory] attribute를 사용해 하나의 method만을 구현하고 [InlineData] attribute를 사용하여 서로 다른 매개변숫값을 전달할 수 있습니다.

[Theory]
[InlineData(2, 2, 4)]
[InlineData(2, 3, 5)]
public void TestAdding(int a, int b, int expected)
{
    Testing addCalc = new();

    int actual = addCalc.Add(a, b);
    Assert.Equal(expected, actual);
}

 

이때 [InlineData] attribute로 전달한 매개변수의 순서는 test method에서의 매개변수 순서대로 지정됩니다.

5. 함수에서 예외 처리하기

 

예외가 무엇인지 그리고 예외가 발생하면 try~catch문을 사용해 이를 어떻게 처리할 수 있는지에 대해서는 이미 아래 글을 통해 소개한 바 있습니다.

 

[.NET/C#] - [C# 13과 .NET 9] 3. 흐름제어, Type변환, 예외처리

 

하지만 문제점을 해결할 수 있는 충분한 정보를 가지고 있다면 그저 발생한 예외를 적절히 처리하기만 하면 되지만 그렇지 않다면 경우에 따라 발생한 예외가 호출 stack을 따라 더 높은 수준으로 올라가도록 해줄 필요가 있습니다.

 

1) 사용오류(Usage error)와 실행오류(execution error)의 이해

 

사용오류는 개발자가 함수를 잘못 사용한 경우를 말하는 것으로 대개 잘못된 값을 매개변수로 전달하는 경우가 대표적입니다. 이런 경우에는 개발자가 정확한 값이 전달될 수 있도록 code를 변경함으로써 문제점을 해결할 수 있습니다. 특히 대부분의 경우 application이 배포되기 전 발견되는 경우가 많지만 이전에 단위 test예제처럼 모른 채 지나칠 수도 있어서 주의가 필요합니다.

 

실행오류는 말 그대로 application이 실행 중일때 즉, runtime중에 발생하는 오류를 말하는 것으로 이미 배포되어 사용될때를 포함합니다. 대부분의 경우 이러한 오류는 개발자가 더 개선된 code를 작성하는 것으로 해결될 수 없는데, 실행중 나타나는 이러한 실행오류는 그 특징에 따라 program error와 system error로 다시 나뉠 수 있습니다.

 

예를 들어 application에서 network resource로의 접근을 시도하는 경우 어떤 이유로 network가 단절된 상태라면 예외가 발생할 수 있습니다. 그리고 이를 system error로 판단할 수 있으며 실제 이런 경우가 발생하면 가능한 한 code의 실행을 중지하고 일정시간 후 다시 시도하는 방법으로 예외를 처리할 수 있을 것입니다. 하지만 memory부족과 같은 어떤 system error는 자체적인 처리가 불가능할 수도 있습니다. 또 다른 경우로는 존재하지 않는 file로의 접근이 시도되는 경우인데 memory부족과는 다르게 이런 경우에는 새로운 file을 생성함으로써 좀 더 쉽게 문제를 해결할 수 있기도 합니다. 대게 program error는 좀더 개선된 code를 작성함으로써 즉각적으로 문제를 해결하지만 system error는 다소 다른 방식의 해결방법이 필요합니다.

 

2) 함수에서 일반적으로 발생하는 예외

 

일반적인 사용오류를 나타내기 위해 예외에 대한 새로운 type을 정의하는 경우는 드뭅니다. .NET은 이미 사용가능한 많은 type을 정의하고 있기 때문입니다.

 

매개변수를 가진 함수를 직접 정의할 때 내부에서 매개변수의 값을 확인하고 만약 함수실행에 문제가 되는 경우에는 적절한 예외를 발생시킬 수 있습니다.

 

예를 들어 함수의 매개변수가 null이 되어서는 안 된다면 ArgumentNullException예외를 발생시키거나 그 외 다른 문제라면 해당 문제점에 따라 ArgumentException, NotSupportedException 혹은 InvalidOperationException 등 다양한 예외 type을 사용할 수 있습니다.

 

또한 모든 예외사항에 대해서는 누구든(보통 개발자나 GUI환경에서의 최상위 수준이라면 최종 사용자가 될 수 있음) 해당 문제점을 눈으로 읽을 수 있도록 하는 문제점의 서술사항을 포함합니다.

private void Withdraw(string param1, int param2)
{
    if (string.IsNullOrWhiteSpace(param1))
    {
        throw new ArgumentException(paramName: nameof(param1));
    }
    
    if (param2 <= 0)
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(param2), message: $"매개변수 {nameof(param2)}는 0이나 음수가 될 수 없습니다.");
    }
    
    //처리진행...
}
함수가 성공적으로 동작을 수행하지 못한 경우 이를 실패로 간주하고 필요한 경우 예외를 발생시켜 이를 사용자에게 표시할 수 있습니다.

 

3) Guard절을 사용해 예외 발생시키기

 

new를 사용하여 예외에 대한 intance를 생성하는 대신 예외 자체의 정적 method를 사용할 수 있습니다.

 

인수의 값을 확인하는 구현이 함수에서 사용될 때 이를 guard절이라고 하며 일부는 .NET 6에서 도입되었고 다른 일부는 .NET 8에서 추가되었습니다.

 

일반적인 guard절은 아래와 같습니다.

예외 Guard절 method
ArgumentException ThrowIfNullOrEmpty, ThrowIfNullOrWhiteSpace
ArgumentNullException ThrowIfNull
ArgumentOutOfRangeException ThrowIfEqual, ThrowIfGreaterThan, ThrowIfGreaterThanOrEqual, ThrowIfLessThan,  ThrowIfLessThanOrEqual, ThrowIfNegative, ThrowIfNegativeOrZero, ThrowIfNotEqual, ThrowIfZero

 

위의 guard절을 사용하면 if문을 작성하고 예외를 발생시키는 대신 이전에 예외를 아래와 같이 간소화할 수 있습니다.

private void Withdraw(string param1, int param2)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(param1, paramName: nameof(param1));
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(param2, paramName: nameof(param2));
    //처리진행...
}
Microsoft가 자신들의 source code에서 얼마나 많이 guard절을 사용하는지 알고 싶다면 아래 link의 글을 참고할 수 있습니다. 예를 들어 ThrowIfNull은 dotnet/runtime의 core library에 대한 src 내에서 4,795번 호출되었습니다.

Performance Improvements in .NET 8 - .NET Blog

 

Performance Improvements in .NET 8 - .NET Blog

.NET 7 was super fast, .NET 8 is faster. Take an in-depth tour through over 500 pull requests that make that a reality.

devblogs.microsoft.com

 

4) Call stack

 

.NET console application의 주요 진입점은 Program class에 있는 Main method(명시적으로 해당 class를 정의한 경우) 혹은 <Main>$(top-level program기능을 사용한 경우)입니다.

 

따라서 Application시작 시 Main method가 가정먼저 호출되고 그다음 Main method안에서 또 다른 method가 호출되고 해당 method안에서 또 다른 method가 호출되는 식으로 application이 실행됩니다. 그리고 이들 method는 현재 project나 참조된 project, 또는 NuGet Package가 될 수도 있습니다.

 

예외의 발생과 이를 처리할 수 있는 위치를 알아보기 위해 간단히 method의 호출이 연결되는 예제를 작성해 보고자 합니다. 이를 위해 우선 Class Library유형의 project를 SampleLibrary이름으로 생성합니다.

 

Project가 생성되면 Class1.cs file의 이름을 DoProcess.cs로 변경하고 아래와 같이 code를 작성합니다.

namespace SampleLibrary;

public class WorkLib
{
    public static void MethodD()
    {
        Console.WriteLine("MethodD");
        MethodF();
    }
    
    private static void MethodF()
    {
        Console.WriteLine("MethodF");
        File.OpenText("file open!!");
    }
}

 

이제 위에서 만든 library를 사용할 console app project를 Sample이라는 이름으로 생성하고 해당 csproj project file을 열어 아래와 같은 방법으로 이전에 만든 SampleLibrary를 참조추가합니다.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

   <ItemGroup>
  <ProjectReference Include="..\SampleLibrary\SampleLibrary.csproj" />
 </ItemGroup>

</Project>

 

이후 Sample project를 build 하여 의존성 project를 compile 하고 bin folder안으로 library가 복사되도록 합니다.

 

그다음 Sample project의 Program.cs에서 기존의 문을 모두 삭제하고 2개의 method를 추가한 뒤 해당 method와 Sample Library의 method가 순차적으로 호출되도록 아래와 같이 작성합니다.

using SampleLibrary;

Console.WriteLine("Main");
MethodA();

void MethodA()
{
    Console.WriteLine("MethodA");
    MethodB();
}

void MethodB()
{
    Console.WriteLine("MethodB");
    WorkLib.MethodD();
}

 

이제 예제를 실행하여 아래와 같은 결과를 확인합니다.

Main
MethodA
MethodB
MethodD
MethodF
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'D:\Study\sample\file open!!'.
File name: 'D:\Study\sample\file open!!'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at Systehttp://m.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)  
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at Systehttp://m.IO.File.OpenText(String path)
   at SampleLibrary.WorkLib.MethodF() in D:\Study\SampleLibrary\DoProcess.cs:line 14
   at SampleLibrary.WorkLib.MethodD() in D:\Study\SampleLibrary\DoProcess.cs:line 8
   at Program.<
$>g__MethodB|0_1() in D:\Study\sample\Program.cs:line 15

   at Program.<
$>g__MethodA|0_0() in D:\Study\sample\Program.cs:line 9

   at Program.
$(String[] args) in D:\Study\sample\Program.cs:line 4

 

Call stack의 순서는 아래에서부터 위로 거슬러 올라갈 수 있는데, 위의 결과를 통해 우리는 다음의 사항을 확인할 수 있습니다.

  • 가장 먼저 호출되는 것은 자동으로 생성된 Program class에 있는 <Main>$ 주요 진입 함수입니다. 그리고 해당 함수는 문자열배열로서 전달된 args 매개변수를 갖습니다.
  • 두 번째로 <Main>$>g__MethodA|0_0()가 호출됩니다.(C# compiler는 해당 함수를 지역함수로 추가할 때 함수의 이름을 변경하게 됩니다.)
  • 세 번째로 MethodB가 호출됩니다.
  • 네 번째로 MethodD가 호출됩니다.
  • 다섯 번째로 MethodF가 호출됩니다. 해당 함수는 내부에서 고의적으로 잘못 전달된 file의 위치를 참조하여 해당 file을 열려고 시도합니다. 하지만 실제 file은 존재하지 않기 때문에 예외가 발생하게 되는데 이때 try-catch문안에 존재하는 모든 함수에서는 해당 예외를 잡을 수 있는 상황이 됩니다. 그런데 만약 그럴 수 없다면 예외는 자동적으로 최상위까지 call stack을 거슬러 올라가게 되는데, 최상위까지 도달하게 되면 .NET은 해당 예외의 정보와 call stack의 상세사항을 표시하게 됩니다.
Code를 단계별로 살펴보면서 debug 할 필요가 없다면 debugger를 붙이지 않고 code를 실행할 수 있습니다. 예제의 경우 예외가 잡히게 되면 위의 예제처럼 결과를 표시하는 대신 GUI dialog box를 통해 예외정보가 표시될 것입니다. 그러나 이러한 방법으로 굳이 결과를 확인할 필요가 없으므로 debugger를 붙여 project를 실행할 필요가 없습니다.

 

5) 예외 재 발생시키기

 

어떤 경우에는 예외를 잡고 이를 log한뒤 다시 해당 예외를 재발생시켜야 하는 경우가 있습니다. 예를 들어 application에서 호출되는 저수준의 class library를 작성한다면 예외가 발생했을 때 code자체에서는 이를 처리할 수 있는 충분한 정보를 갖지 못할 수 있지만 application자체에서는 이를 처리하기 위한 더 많은 정보를 가질 수 있습니다. 이런 경우 code에서는 이를 log 하고 호출하는 application에서 이를 더 잘 처리할 수 있도록 call stack의 상위로 재 발생시켜야 합니다.

 

catch block 내에서 예외를 재발생시키는 방법은 다음과 같은 3가지가 있습니다.

 

  • 잡힌 예외를 본래 call stack과 함께 발생시키려면 throw를 호출합니다.
  • 잡힌 예외를 call stack의 현재 수준에서 발생한 것처럼 발생시키려면 잡힌 예외와 함께 throw를 'throw ex'처럼 호출합니다. 그러나 이 방법은 debugging에 사용될 수 있는 일부 정보를 잠재적으로 잃을 수 있기 때문에 주의가 필요합니다. 다만 민감한 정보가 포함될 수 있는 경우라면 고의적으로 해당 정보를 삭제하기 위해서는 유용하게 사용될 수 있습니다.
  • 발생한 예외를 더 많은 정보를 포함하는 다른 예외로 감싸놓고 이를 새로운 예외로 발생시킵니다. 이때 innerException매개변수를 통해 발생한 예외를 전달할 수 있습니다. 이 방법은 감싸놓으려는 예외가 호출자가 문제점을 이해하는데 도움을 줄 수 있는 message를 포함하는 경우에 유용하게 사용될 수 있습니다.

만약 예외가 특정함수를 호출할 때 발생했다면 다음과 같이 예외를 잡고 위의 3가지 방법 중 하나를 수행하여 예외를 재 발생시키면 됩니다.

try
{
    TEST();
}
catch (IOException ex)
{
    LogException(ex);

    // 잡힌 예외를 여기서 발생한것 처럼 재 발생시키기
    // 이 방법은 본래 call stack의 정보를 잃을 수 있음
    throw ex;

    // 잡힌 예외를 재발생시키고 본래 call stack을 유지하기
    throw;
    
    // 잡한 예외를 다른 새로운 예외로 중첩시켜 발생시키기
    throw new InvalidOperationException(message: "Operation failed. See inner exception", innerException: ex);
}

 

이를 예제를 통해 살펴보면 Sample project의 Program.cs에서 MethodD함수를 호출하는 부분에 try-catch문을 추가하여 함수를 호출하도록 만들 수 있습니다

void MethodB()
{
    Console.WriteLine("MethodB");

    try
    {
        WorkLib.MethodD();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}
code editor에서는 'throw ex'부분에 물결모양밑줄을 그어 code analyzer message로 call stack 정보가 손실될 수 있음을 경고할 수 있습니다.

 

예제를 실행시키면 다음과 같이 call stack에 대한 일부 정보를 제외한 결과를 표시할 것입니다.

D:\Study\Sample\Program.cs(23,9): warning CA2200: Re-throwing caught exception changes stack information (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200) 
Main
MethodA
MethodB
MethodD
MethodF
Could not find file 'D:\Study\Sample\file open!!'.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'D:\Study\Sample\file open!!'.
File name: 'D:\Study\Sample\file open!!'
   at Program.<
$>g__MethodB|0_1() in D:\Study\Sample\Program.cs:line 23

   at Program.<
$>g__MethodA|0_0() in D:\Study\Sample\Program.cs:line 9

   at Program.
$(String[] args) in D:\Study\Sample\Program.cs:line 4

 

예제의 throw ex에서 throw만 삭제한 후 다시 실행하면 예외에 대한 모든 정보를 표시할 것입니다.

 

6) Tester-doer와 try pattern구현하기

 

Tester-doer pattern은 함수 2개가 쌍으로 구성되며 하나는 test를 수행하고 다른 하나는 실제 action을 실행하는 역할을 수행합니다.(test함수를 통과했다고 하더라도 실제 action에서 실패할 수 있습니다.)

 

.NET은 이 pattern을 자체적으로 구현하는데 예를 들어 Add method를 호출함으로써 collection에 item을 추가하기 전에 Add가 실패할 수 있는 read-only인지, 그래서 예외가 발생하는지의 여부를 test 해 볼 수 있습니다.

 

만약 신용카드를 통해 결제가 이루어질 때 신용카드가 한도를 벗어났는지 여부를 확인해 보려면 아래와 같이 구현할 수 있을 것입니다.

if (!CreditCard.IsOver())
{
    CreditCard.Pay(Amount);
}

 

Tester-doer pattern은 자칫 성능저하의 원인이 될 수 있는데 이런 이유 때문에 try pattern을 같이 구현하여 test와 실제 action부분을 하나의 함수로 결합함으로써 TryParse와 비슷한 구조를 갖게 합니다.

 

Tester-doer의 또 다른 문제점은 다수의 thread를 사용할 때 발생하는데, 이와 같은 경우 하나의 thread에서 test함수를 호출하여 성공을 나타내는 값을 반환하지만 다른 thread에서 상태를 변경하는 경우가 발생할 수 있습니다. 본래 thread에서는 test를 통과한 것으로 간주하고 실행을 지속하지만 실제로는 그렇지 않을 수도 있는 것입니다. 이러한 현상을 경쟁상태(Race Condition)라고 합니다.

박스 : tester-doer pattern보다는 가급적 try pattern사용을 권장합니다.

 

자체적으로 try pattern함수를 구현하는 경우 해당 함수의 동작이 실패하는 경우에 대한 처리를 진행해야 한다면 out매개변수를 type의 기본값으로 설정하고 false를 반환하도록 구현합니다.

static bool TryParse(string? input, out Car value)
{
    //fail
    if (!isSuccess)
    {
        value = default(Car);
        return false;
    }

    //Success
    value = new Car() {...};
    return true;
}
예외에 관한 더 자세한 사항은 아래 Microsoft의 공식문서에서 확인해 볼 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/standard/exceptions/
 

Handling and throwing exceptions in .NET - .NET

Learn how to handle and throw exceptions in .NET. Exceptions are how .NET operations indicate failure to applications.

learn.microsoft.com

728x90