1. 함수(메서드)
어떤 처리를 위해 작성하는 코드가 반복되어 작성되고 있다면 해당 코드를 특정한 함수로 만들어 놓는 방법이 필요합니다. 이는 동일한 처리가 필요한 여러 부분에서 해당 함수를 호출하게 함으로써 코드의 반복을 줄일 수 있습니다. 예를 들어 회계프로그램 등에서 부가세를 계산해야 하는 경우라면 이 기능 자체를 함수로 만들어 부가세 계산이 필요한 모든 부분에 공통적으로 적용하게 되면 부가세 계산이 필요한 부문마다 동일한 코드가 작성되는 낭비를 줄일 수 있는 것입니다.
//자동차
int car = 10000;
//비행기
int air = 20000;
Console.WriteLine($"자동차 부가세 : {car / 10}");
Console.WriteLine($"비행기 부가세 : {air / 10}");
//자동차 부가세 : 1000
//비행기 부가세 : 2000
예를 들어 위와 같이 각 물품에 대한 부가세 계산이 각각마다 마련되어 있다면 다른 물품이 추가되는 경우 또 다시 부가세 계산 부분을 추가함으로써 계산 구현이 중복되는 문제가 발생할 수 있고, 나중에 부가세 계산을 위한 등식이 바뀌는 경우 지금까지의 모든 부가세 계산 방식을 모두 찾아 일일이 바꿔줘야 하는 문제가 발생할 수 있습니다.
//자동차
int car = 10000;
//비행기
int air = 20000;
Console.WriteLine($"자동차 부가세 : {tax(car)}");
Console.WriteLine($"비행기 부가세 : {tax(air)}");
int tax(int price)
{
return price / 10;
}
//자동차 부가세 : 1000
//비행기 부가세 : 2000
따라서 부가세 계산을 위한 함수를 분리하는 것이 현재 상태나 추후 유지보수에도 도움이 될 것입니다.
예제에서의 함수는 int형의 값을 받아 해당 값에서 /10을 수행한 뒤 그 결과를 int형으로 반환하고 있습니다. tax라는 이름앞에 int는 곧 해당 함수가 int형의 값을 반환한다는 것을 의미하며 함수 내부의 return은 특정 값을 반환하기 위한 것입니다. 물론 함수가 string형식을 반환한다면 string을 double형식을 반환한다면 double키워드가 사용될 수 있을 것입니다. 그러나 만약 아무런 값도 반환하지 않는 함수라면 int와 같은 형식 대신 void를 사용할 수 있습니다.
void tax(int price)
{
Console.WriteLine($"부가세 : {price / 10}");
}
함수에서 사용된 price와 같이 함수가 필요로 하는 값을 전달하는 변수를 '매개변수'라고 합니다. 그리고 '매개변수'에 담겨진 값을 '인수'라고 합니다. 특정 함수에서 많은 매개변수가 사용되는 경우 내가 전달하는 값이 어떤 '매개변수'에서 받을지는 매개변수와 전달하는 값의 순서에 따라 결정됩니다. 하지만 아래와 같이 특정 매개변수가 내가 원하는 값을 받도록 지정해 줄 수도 있습니다.
//자동차
int car = 10000;
//비행기
int air = 20000;
tax(price: car);
void tax(int price)
{
Console.WriteLine($"부가세 : {price / 10}");
}
● 재귀함수
특정 함수가 자기 자신을 다시 호출하는 경우를 말합니다.
int result = factorial(5);
Console.WriteLine(result);
int factorial(int number)
{
if (number == 0)
return 0;
else if (number == 1)
return 1;
return number * factorial(number - 1);
}
예제에서는 factorial값을 얻기 위해 factorial이라는 함수를 만들었는데 함수 내부에서 자신을 다시 호출하여 주어진 값의 factorial값을 구하고 있습니다. 재귀 함수는 필요한 상황에서는 잘 작동하고 원하는 결과를 얻게 되겠지만 잘못 사용될 경우 너무 많은 재귀 호출로 인해 예외가 발생할 수 있으니 주의해야 합니다.
● 함수에 XML 주석 적용하기
함수를 호출하기 위해서는 해당 함수가 어떤 형식의 값을 반환하고 어떤 형식의 값을 전달해야 하는지를 정확히 숙지하고 있어야 하며 이를 위해 Visual Studio Code나 Visual Studio 2022에서는 함수의 기본적인 정보를 툴팁으로 표시해 줍니다.
그러나 해당 함수가 어떤 동작을 수행하며 값을 넘기는 매개변수가 정확히 무슨 용도인지까지는 확인할 수 없는데 이와 같은 상황에서는 XML 주석을 사용해 해당 함수에 대한 자세한 정보를 제공해 줄 수 있습니다.
Visual Studio 2022 사용자라면 기본적으로 XML 주석을 처리할 수 있는 기능을 제공하지만 Visual Studio Code의 경우는 그렇지 않기 때문에 아래의 추가적인 설정을 진행해야 합니다.
Visual Studio Code에서 'Ctrl + Shift + P'를 동시에 누르고 'Preferences: Open Settings (UI)'설정으로 들어갑니다. 그리고 해당 설정에서 'formatOnType'을 찾아 체크표시를 해줍니다.
이제 이전에 작성한 factorial 함수 바로 위에서 '/'문자를 연속으로 세 번 입력하게 되면 자동으로 함수를 설명하는 summary와 매개변수를 설명하는 param, 그리고 결괏값을 설명할 수 있는 return에 대한 XML구조가 만들어지는 것을 확인할 수 있습니다.
/// <summary>
///
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
static int factorial(int number)
위 상태에서 간략하게 함수에 관한 내용을 적어놓게 되면 함수를 호출할 때 관련 정보가 툴팁으로 표시될 것입니다.
※ 주의 : top-level program구조에서는 XML 주석이 제대로 작동하지 않으며 .NET Interactive notebooks에서도 글을 작성하는 현재 시점에서는 XML 주석을 지원하지 않습니다.
● 람다식
함수형 언어는 수학에서의 람다식에서 발전해온 것으로 다른 언어와는 다른 함수형 언어만의 독특한 특징이 있으며 C#에서도 람다식을 구현해 함수를 기반으로 한 처리체계를 구현할 수 있습니다.
- 모듈화 : 크고 복잡한 코드를 여러 조각으로 분리합니다.
- 불변성 : 기존에 존재하던 형식에서 새로운 데이터 값을 생성할 수는 있지만 어떤 형식이라도 함수 안에서 사용되는 변수는 원칙적으로 불변입니다.
- 관리성 : 깔끔하고 명확한 코드
C# 6.0 이후로 마이크로소프트는 C#언어에 함수적인 접근을 지원하기 위해 여러 기능을 추가해 왔습니다. 예를 들어 C# 7에서 패턴 매칭과 튜플을 추가했고 C# 8에서는 non-null참조 형식을, C# 9에서는 패턴 매칭 기능 향상과 불변성 객체인 record가 추가되었습니다.
식 본문 함수 멤버(expression-bodied function members)는 C# 6에서 추가된 것으로 함수를 람다식으로 표현할 수 있게 합니다. 예를 들어 이전에 구현했던 함수를
static int factorial(int number)
{
if (number == 0)
return 0;
else if (number == 1)
return 1;
return number * factorial(number - 1);
}
람다식으로 표현하면 다음과 같이 구현할 수 있습니다.
static int factorial(int number) =>
number switch
{
0 => 0,
1 => 1,
_ => number * factorial(number - 1)
};
람다식에 관한 더 자세한 사항은 다른 글에서 추가적으로 다룰 수 있도록 할 것입니다.
2. 디버깅
디버깅은 개발 중 코드의 문제점을 찾아 수정하는 과정을 의미합니다. 예를 들어 factorial을 구하기 위한 다음과 같은 코드를 만들었다고 한다면
static int factorial(int number) =>
number switch
{
0 => 0,
1 => 1,
_ => number * factorial(number)
};
고의적으로 'number - 1'에서 '-1'을 제거했으므로 이 코드는 정확한 결과를 도출할 없을 것입니다.(overflow예외가 발생합니다.) 예제를 위해 고의적인 버그를 만들었지만 실전에서 실수로 '-1'을 누락해 문제가 된 것이라고 가정하고 디버깅하는 과정을 거쳐 보고자 합니다.
Visual Studio Code를 사용하는 경우라면 문제가 된다고 판단되는 부분에서 다음과 같이 코드 입력창의 왼쪽 여백을 클릭하여 Breakpoint를 설정합니다.
기본적으로 같은 동작을 반복하면 Breakpoint의 설정을 on/off 할 수 있고 마우스 오른쪽 버튼을 누르면 기존의 Breakpoint를 삭제, 편집, 비활성 하거나 새로운 Breakpoint의 추가할 수 있고, Breakpoint의 작동 조건 설정, Logpoint 등을 추가할 수 있습니다. 참고로 Logpoint는 tracepoint라고도 하며 코드 실행의 정지 없이 필요한 정보를 기록할 수 있도록 합니다.
이 상태에서 Visual Studio Code의 'View - Run'메뉴를 선택하거나 왼쪽 'Run and Debug'아이콘을 선택합니다.
위와 같은 디버깅 창에서 다음과 같이 '.NET Core Launch (console) ...'항목 선택을 확인하고
삼각형 버튼(Start Debugging)을 누르거나 Run -> Start Debugging메뉴를 선택, 또는 F5키를 누르면 Visual Studio Code는 현재의 코드를 실행하고 처리의 흐름이 breakpoint에 도달하게 되면 처리를 멈추게 됩니다.
화면상 노란색 표시 부분은 다음부터 실행할 지점을 나타내는 것으로 상단 디버깅 툴바의 'Continue'버튼을 누르거나 F5키를 누르면 코드의 종료까지 혹은 다음 Breakpoint지점까지 처리를 진행하게 되고 'Step Over' 또는 F10, 'Step Into' 또는 F11, 'Step Out' 또는 'Shift + F11'키의 입력으로 단계별 처리를 진행할 수 있습니다. 'Restart' 혹은 'Ctrl + Shift + F5'키는 디버깅을 재시작하도록 하며 'Stop' 또는 'Shift + F5'키로는 디버깅을 중단할 수 있습니다.
또한 왼쪽 'Locals'을 포함한 VARIABLES나 WATCH, CALL STACK창에서는 디버깅에 필요한 몇 가지 정보를 확인할 수 있게 하는데 VARIABLES은 모든 지역변수의 이름과 값 Type을, WATCH에서는 임의로 특정한 변수나 식의 값을 확인할 수 있습니다. CALL STACK은 함수가 호출된 순서의 Stack을 표시하며 BREAKPOINTS에서는 breakpoint에 대한 정보를 표시합니다.
그밖에 코드 창 하단의 DEBUG CONSOLE에서는 임의의 코드를 입력하여 그 결과를 즉시 확인할 수 있도록 하는데 예를 들어 특정 변수를 입력해 해당 변숫값을 바로 확인하거나 100+200과 같은 식을 입력하고 그 결과를 바로 확인해 볼 수 있습니다.
'Step Into'버튼 혹은 F11키를 눌러 다음 과정으로 처리를 진행합니다.
단순 코드라인에서는 'Step Into(F11)'과 'Step Over(F10)'의 동작 차이가 없지만 다음 실행 과정이 특정 메서드(함수)를 실행하는 경우라면 'Step Into(F11)'은 실행할 함수 안으로 직접 들어가 절차적인 디버깅을 가능하게 하지만 'Step Over(F10)'은 함수 자체를 그대로 실행한 뒤 그다음 절차부터 디버깅을 계속 실행한다는 차이가 있습니다.
현재 시점에서는 number변수가 있는 실행 라인으로 이동되었는데 이 상태에서는 number와 같은 변수에 마우스를 올려 현재 값을 학 인할 수 있고 필요한 경우 특정 구문을 'Add to Watch'로 선택하여
해당 식의 상태를 디버깅 동안 확인할 수 있습니다.
상태를 보면 Overflow 예외가 발생했음을 알 수 있습니다. 디버깅을 중지하고 관련 문제점을 수정해 다시 디버깅을 시도합니다.
이번에는 값이 정상적으로 표시되고 있습니다.
필요하다면 breakpoint가 작동할 수 있는 조건을 만들어 원하는 시점에 breakpoint가 잡히도록 조정할 수도 있습니다. 예를 들어 예제에서 factorial() 메서드로 number값이 넘어오는 경우 정확히 number가 10이 되는 경우에만 debugging을 시도하고 싶다고 가정해 보겠습니다. 이를 위해 Visual Studio Code에서 breakpoint를 마우스 오른쪽 버튼으로 클릭해 'Eidt Breakpoint'를 선택하고
'Edit Breakpoint'에서 number값이 정확히 10이 되는 조건을 다음과 같이 지정합니다.
붉은색 breakpoint에 '='가 표시되는 게 보이면 그대로 디버깅을 실행합니다.
breakpoint의 중단 없이 그대로 실행이 완료되는 걸 볼 수 있습니다. 이는 factorial() 메서드를 호출할 때 10이 아닌 다른 값을 지정했기 때문이므로 아래와 같이 10의 값을 넘겨줄 수 있도록 수정하고
int result = factorial(10);
다시 debugging을 시도합니다. 그러면 조건에 따라 breakpoint가 잡히는 것을 확인할 수 있습니다. 참고로 Expression 외에
'hit count'라는 걸 설정할 수 있는데
이 조건은 해당 breakpoint가 정확히 지정한 값만큼 순회하는 경우 breakpoint가 잡힐 수 있도록 조건을 지정하는 것입니다. 이러한 조건 설정은 Visual Studio 2022에서는 'Conditions...'이라는 이름의 설정으로 동일하게 진행할 수 있습니다.
3. 로깅
디버깅이 모두 완료되면 실제 프로그램을 사용할 수 있도록 사용자에게 제공되어야 하는데 문제는 디버깅을 거친 경우라 하더라도 미처 예상하지 못한 예외는 언제든지 발생할 수 있다는 점입니다.
왜 예외가 발생했는지를 정확하게 파악하려면 당시 사용자가 어떤 프로그램의 기능에 어떤 시도를 했는지를 정확히 파악해야 하지만 사용자로 하여금 해당 정보를 취득하는 데는 한계가 있으므로 실제 프로그램이 동작하는 것을 기록하는 로깅을 수행할 수 있습니다.
로깅을 위한 몇 가지 설루션이 존재하지만 기본적으로 내장된 Debug와 Trace로 필요한 로깅을 수행할 수 있습니다. Debug는 말 그대로 디버깅할 때의 로깅을 위한 것이지만 Trace는 디버깅 와 런타임에서 모두 사용됩니다. Debug와 Trace의 구분은 기본적으로 프로젝트에서 Debug의 실행인가 혹은 Release에서의 실행 인가로 구분됩니다.
Debug와 Trace는 trace listener를 통해 로그를 기록하게 되는데 trace listener는 들어온 로그기록을 WriteLine() 메서드처럼 필요한 어느 곳이든지 표현할 수 있으며 TraceListener를 상속하게 되면 자신만의 trace listener를 생성할 수도 있습니다. 가장 기본적인 trace listener의 동작은 기본적으로 Visual Studio Code의 'DEBUG CONSOLE'창에 로그를 기록하며 Visual Studio 2022의 경우 Debug창에 로그를 표현하는 것입니다.
위 예제는 프로젝트를 디버깅으로 동작시킨 것으로 Visual Studio Code의 'DEBUG CONSOLE'에서 로그가 기록되는 걸 살펴볼 수 있는데 이와 같이 디버깅할 수 없는 경우라면 대체적으로 파일에 로그를 기록하게 하는 동작으로 변경할 수 있습니다.
using System.Diagnostics;
class TestClass
{
static void Main(string[] args)
{
Trace.Listeners.Add(new TextWriterTraceListener(File.CreateText(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "log.txt"))));
Trace.AutoFlush = true;
Debug.WriteLine("로그 출력 - Debug");
Trace.WriteLine("로그 출력 - Trace");
}
}
예제에서 Trace Listener에는 새롭게 정의되는 'TextWriterTraceListener'(지정한 파일에 로그를 쓰게 하는)를 추가하였습니다. 따라서 로그가 발생하면 해당 로그의 내용을 log.txt라는 이름으로 바탕화면에 파일을 생성할 것입니다.
참고로 파일에 데이터를 기록하는 경우 대부분은 성능 문제로 인해 일정 양의 데이터를 우선 버퍼라는 메모리에 기록했다가 버퍼가 모두 채워지면 그때 해당 내용을 기록하는 동작을 수행합니다. 예제에서는 AutoFlush의 설정을 true로 지정했는데 이 설정은 기록할 데이터가 발생할 때마다 버퍼를 기다리지 않고 즉시 Flush() 메서드를 수행하여 즉각적으로 파일에 필요한 데이터를 기록하도록 하는 설정입니다.
대략의 데이터를 파일로 기록하는 경우 AutoFlush의 설정은 신중해야 합니다.
● trace level 사용
trace level을 사용하면 프로그램에서 발생하는 모든 이벤트를 기록하는 대신 상대적으로 중요하다고 판단되는 이벤트에서만 로그가 기록될 수 있도록 조정할 수 있습니다.
설정 가능한 trace level과 적용범위는 다음과 같으며 값은 숫자나 문자열을 사용할 수 있습니다.
값(숫자) | 값(문자열) | 범위 |
0 | Off | 아무것도 로깅하지 않습니다. |
1 | Error | Error가 발생한 경우만 로깅합니다. |
2 | Warning | Error와 Warning의 경우에만 로깅합니다. |
3 | Info | Error, Warning, Info의 경우에만 로깅합니다. |
4 | Verbose | 상기 모든 레벨을 모두 포함합니다. |
해당 설정은 프로젝트의 'appsettings.json'에 적용될 수 있는데 이 설정을 읽기 위해서는 프로젝트에 다음과 같은 NuGet 패키지가 추가로 필요합니다.
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration.Binder
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Configuration.FileExtensions
Visual Studio 2022의 경우 'NuGet Package Manager'를 통해 검색하여 설치할 수 있으며
Visual Studio Code라면 'dotnet add package'명령을 통해 해당 NuGet Package를 설치할 수 있습니다.(설치는 해당 프로젝트의 *. csproj파일이 있는 곳에서 실행되어야 합니다.)
필요한 패키지를 설치하고 나서 프로젝트의 csproj파일을 열어보면 아래와 같이 정상적으로 프로젝트에 해당 패키지가 추가된 것을 확인할 수 있습니다.
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
</ItemGroup>
그다음 프로젝트의 최상위 경로에 'appsettings.json'이라는 json파일을 추가하고 아래 내용으로 파일을 작성합니다.
{
"logSettings" : {
"Level":"Info"
}
}
만약 Visual Studio 2022에서 json파일을 추가한 경우라면 'Solution Explorer'에서 해당 파일을 선택한 뒤 'Properties'창의 'Copy to Output Directory'설정을 'Copy if newer'로 변경합니다. Visual Studio Code에서와는 달리 Visual Studio 2022에서는 프로젝트를 '<프로젝트 경로>\bin\Debug\net6.0'이나 '<프로젝트 경로>\bin\Release\net6.0'에서 실행하기 때문에 설정 파일이 실행 폴더에 같이 복사되어야 합니다.
마지막으로 Program.cs를 수정하여 위에서 추가한 json파일의 설정을 가져와 적용할 수 있도록 합니다.
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
class TestClass
{
static void Main(string[] args)
{
ConfigurationBuilder builder = new();
builder.SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
IConfigurationRoot configuration = builder.Build();
TraceSwitch ts = new(displayName: "logSettings", description: "log level");
configuration.GetSection("logSettings").Bind(ts);
Trace.WriteLineIf(ts.TraceError, "error");
Trace.WriteLineIf(ts.TraceWarning, "warning");
Trace.WriteLineIf(ts.TraceInfo, "information");
Trace.WriteLineIf(ts.TraceVerbose, "verbose");
}
}
ConfigurationBuilder는 현재 폴더에서 'appsettings.json'파일을 찾아 필요한 설정을 빌드하고 바인드 하여 실제 설정값을 적용하게 됩니다.
예제에서는 Level을 'Info'로 하였으므로 아래와 같은 결과를 볼 수 있습니다.
error warning information |
4. 단위 테스트
이미 만들어진 코드의 문제점을 파악하고 해당 문제를 수정하는 작업은 당연한 일이지만 가능하면 개발과정에서 미리 문제점을 파악하는 것이 훨씬 더 도움이 될 것입니다. '단위 테스트'는 바로 이러한 목적을 달성하기 위한 단계에 해당합니다.
어떤 경우는 개발자가 코드를 작성하기 전 미리 단위 테스를 만들어야 한다는 원칙을 세우기도 하는데 이를 TDD(Test-Driven Development) 즉, 테스트 주도 개발이라고 합니다. 현재 단위 테스트를 위한 마이크로소프트의 'MS Test'나 'NUnit'과 같은 몇 가지 프레임워크가 존재하는데 이번 예제에서는 'NUnit'팀에서 개발한 'xUnit'을 사용해 볼 것입니다.
참고로 '단위 테스트'는 이미 존재하는 몇가지 테스트 유형중 하나에 불과하며 실제 아래와 같이 테스트를 위한 다양한 유형이 존재합니다.
테스트 유형 | 목적 |
Unit | 메서드나 함수같은 가능한한 작은 단위의 코드를 테스트합니다. 테스트에서 코드는 외부에 의존적인 부분을 Mocking함으로서 독립적으로 테스트되며 일반적인 입력에 예상된 값이 나오는지, 잘못된 입력에 예외처리가 어떻게 이루어지는지등을 확인하기 위한 다수의 테스트가 진행될 수 있습니다. |
Integration | 작은 단위의 코드와 상대적으로 규모가 큰 컴포넌트와의 결합으로 실행되는 경우나 외부 컴포넌트와의 결합으로 작동하는 소프트웨어를 테스트하는 경우입니다. |
System | 실제 소프트웨어가 동작할 환경에서 시스템 전체를 테스트하는 과정입니다. |
Performance | 소프트웨어에 대한 성능을 테스트하는 것으로 한번의 요청이 발생할때 마다 정해진 시간(예를 들어 1초이내와 같은)안에 응답할 수 있는지에 대한 테스트입니다. |
Load | 소프트웨어가 얼마나 많은 요청을 동시에 처리할 수 있는지에 대한 테스트입니다. 예를 들어 10만명이 접속을 시도했을때 소프트웨어 본래의 성능이 계속 유지될 수 있는가를 확인합니다. |
User Acceptance | 사용자가 소프트웨어를 사용해 무난하게 목적한 바를 완료할 수 있는가에 대한 테스트입니다. |
단위 테스트를 위해서는 우선 테스트할 코드가 들어간 프로젝트가 먼저 만들어야 합니다. 예를 들어 간단한 덧셈을 수행하는 메서드가 있고 이 메서드가 제대로 작동하는지를 테스트해야 하는 경우라면 우선 해당 코드를 담을 프로젝트를 아래와 같이 클래스 라이브러리 형태로 만들어 둡니다.
dotnet new classlib -o myCal
|
프로젝트의 이름은 myCal이며 프로젝트를 생성하고 나면 해당 프로젝트에 Class 1.cs파일이 만들어지게 되는데 해당 파일을 Cal.cs로 수정하고 아래와 같이 테스트할 덧셈 메서드를 추가합니다.
namespace myCal;
public class Cal
{
public int Sum(int i, int j) => i + j;
}
그다음 실제 위의 코드를 테스트할 수 있는 프로젝트를 추가로 생성해야 하는데 이때는 프로젝트를 xunit형식의 프로젝트로 생성하여야 합니다.
dotnet new xunit -o myTest
|
예제에서 프로젝트명을 myTest로 하였으며 해당 프로젝트 폴더에서 'myTest.csproj'을 다음과 같이 수정하여 이전에 만든 myCal프로젝트를 참조하도록 합니다.
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\myCal\myCal.csproj" />
</ItemGroup>
그리고 myTest 프로젝트를 빌드합니다.
dotnet build
|
이제 테스트할 코드를 작성해야 하며 테스트 코드는 전체적으로 아래와 같은 3가지 부분으로 구성될 수 있습니다.
Arrange | 입력과 출력을 위한 변수등이 위치하는 부분입니다. |
Act | 실제 테스트메서드가 실행되는 부분입니다. 예제의 경우 위에서 추가한 myCal의 Sum()메서드가 대상이 됩니다. |
Assert | 출력결과를 예상값과 비교하여 메서드(또는 함수)가 정상적으로 실행되었는지를 판단하는 부분입니다. |
myTest프로젝트의 'UnitTest1.cs'파일을 열어 우선 myCal 프로젝트의 Namespace를 선언하고
using myCal;
Test1()메서드에서 myCal의 Sum() 함수를 실행하는 테스트를 구현합니다.
public class UnitTest1
{
[Fact]
public void Test1()
{
//Arrange
int i = 10;
int j = 20;
int e = 30; //결과 예상값
Cal c = new();
//act
int r = c.Sum(i, j);
//assert
Assert.Equal(e, r);
}
}
코드는 위에서 말씀드린 3가지 부분으로 구성되어 있으며 Cal의 Sum() 메서드를 호출하여 예상 값과 결괏값이 일치하는지를 Assert의 Equal로 확인하고 있습니다.
테스트 코드를 위와 같이 완성하고 나면 아래 명령으로 테스트를 실행해 볼 수 있습니다.
dotnet test
|
결과를 보면 결괏값이 예상 값과 일치하여 정상적으로 처리되었음을 표시하고 있습니다. 이번에는 예상 값을 변경해 다시 테스트를 진행해 보겠습니다.
결과에서 예상 값은 40인데 실제로는 30의 값이 출력되었고 따라서 테스트가 실패하였음을 표시하고 있습니다.
5. 예외처리
예외에 관해서는 아래 글에서 이미 다루어본 경험이 있습니다.
따라서 기본적인 건 넘어가고 약간 다른 관점에서 예외가 다뤄질 수 있는 부분에 대해 알아보고자 합니다.
메서드(또는 함수)를 호출하는 경우 원하지 않는 잘못된 값을 전달하게 되면 당연히 오류가 발생하게 됩니다. 이런 오류를 흔히 사용 오류(Usage Errors)라고 표현합니다. 사용 오류는 필요한 정확한 값을 전달하면 자연스럽게 해결될 수 있지만 어디까지나 '사용자'의 입장에서 정확한 사용만을 요구해야 하므로 그럴 수 없는 상황이라면 '잘못 사용'하는 경우까지도 예외 처리에 적용되어야 합니다.
using System;
class myApp
{
static void Main(string[] args)
{
Print(null);
}
static void Print(string s)
{
if (s == null)
{
throw new ArgumentNullException(paramName: nameof(s), message: $"{nameof(s)}의 값은 null일 수 없습니다");
}
Console.WriteLine(s);
}
}
실행오류(Execution errors)는 말 그대로 Application이 실행될 때 즉, runtime에서 발생하는 오류를 의미합니다. 이 오류는 대체로 프로그램 오류(Program errors)와 시스템 오류(System errors)로 나누어지게 되는데 예를 들어 Application에서 Network접속을 시도해 뭔가를 처리하려고 하는 경우 컴퓨터의 이더넷이 연결되어 있지 않거나 네트워크 자체가 단절된 상항이라면 이런 경우는 '시스템 오류'로서 취급될 수 있고 Application에서는 해당 오류를 적절히 제어하여 사용자에게 무엇이 문제인지를 알려주고 문제가 해결되고 나서 다시 시도할 수 있도록 프로그램이 동작하도록 해야 합니다. 다른 측면으로 Application에서 존재하지 않는 파일로의 접근을 시도하는 경우는 프로그램 오류(Program errors)로 취급될 수 있고 Application에서는 새로운 파일을 생성함으로써 관련 오류가 적절히 제어될 수 있어야 합니다. 즉, Application에서 발생된 오류를 자체적으로 해결해 줄 수 있는지의 여부에 따라 프로그램 오류와 시스템 오류로 구분되는 것입니다.
● Call Stack
예제에서의 Console Application을 실행하면 가장 처음 Main Method가 호출되고 Main Method는 다시 내부에서 Print()라는 메서드를 호출하게 됩니다. 혹은 호출의 대상이 다른 프로젝트나 외부 라이브러리의 메서드가 될 수도 있습니다.
이렇게 여러 과정을 거쳐 프로그램 실행에 필요한 메서드가 단계적으로 호출되면 이들 메서드는 순서대로 Stack에 적재되어 관리되는데 이를 'Call Stack'이라고 합니다. 만약 특정 메서드 안에서 예외가 발생하게 되면 해당 메서드안에서 try~catch구문을 통해 예외를 처리할 수 있는 장치가 마련된 경우 작성된 try~catch를 통해 예외를 처리하지만
그렇지 않은 경우
static void Print(string s)
{
int i = 10 / 0;
Console.WriteLine(s);
}
예외는 .NET이 예외(그리고 콜 스택의 세부 정보)를 받을 수 있는 최상위까지 콜 스택 위로 전달하게 됩니다.
● Rethrowing Exception
어떤 예외가 발생했을 때 해당 예외를 Log 같은 것으로 남겨두고자 한다면 예외를 Rethrowing 하여 Application에서 여전히 예외가 발생했음을 표현할 필요가 있습니다. catch안에서 예외를 다시 던지는 방법에는 3가지가 있는데
- 본래 Call Stack에서 예외를 던지기 위해 throw를 호출하는 방법
- Call Stack의 현재 단계에서 발생한 예외라면 'throw ex'처럼 발생한 예외를 같이 호출하는 방법(이 방법은 디버깅을 위한 중요한 정보가 손실될 수 있음에 주의가 필요합니다.)
- 문제점을 이해하는데 더 많은 정보를 제공해 줄 수 있는 정보를 포함하여 새로운 예외 객체로 래퍼 하여 전달하는 방법
아래의 경우
using System;
class myApp
{
static void Main(string[] args)
{
try {
Copy();
}
catch(Exception ex)
{
throw ex;
}
}
static void Copy()
{
try{
File.Copy("C:\\aaa.txt", "C:\\bbb.txt");
}
catch(FileNotFoundException ex)
{
Log(ex);
throw ex;
// throw;
// throw new InvalidOperationException(message: "0으로 나누기 실패", innerException: ex);
}
}
static void Log(Exception ex)
{
Console.WriteLine("로그처리");
}
}
Copy()는 예외처리에서 처음 로그 작업을 수행한 후 다시 throw ex;를 통해 예외를 Rethrowing 하고 있습니다. 다른 방법으로 새로운 예외 형식으로 기존 예외를 감싸서 던져주는 경우도 있는데 이 둘의 경우를 비교해 보면
예상대로 Call Stack정보에서 차이가 남을 확인할 수 있습니다.
'.NET > C#' 카테고리의 다른 글
[C#] Entity Framework Core - 2. 모델링(Modeling) (0) | 2022.06.24 |
---|---|
[C#] Entity Framework Core - 1. 시작/설정하기 (0) | 2022.06.24 |
[C#] TCP/IP 통신 (0) | 2021.10.28 |
[C#] Thread(스레드)와 Task(태스크) (2) | 2021.10.24 |
[C#] 파일과 디렉터리 다루기 (0) | 2021.10.21 |