
코드를 작성하다 보면 예상치 못한 버그가 발생하는 건 정말 흔한 일이죠.
이럴 때 필요한 것이 바로 디버깅(Debugging)입니다.
디버깅(Debugging)은 프로그램이 실행되는 동안 내부 상태를
자세히 들여다보고, 버그의 원인을 찾아내 수정하는 과정을 말해요.
이번 글에서는 코드의 흐름을 추적하는 디버깅에 대해 알아보겠습니다.
아주 간단해요. 변수 값이나 코드의 실행 여부를 콘솔 창에 출력해 보는 것입니다.
마치 어두운 동굴을 탐험할 때 곳곳에 WriteLine()이라는 횃불을 놓아
내가 어디까지 왔는지, 주변에 무엇이 있는지 확인하는 것과 같아요.
for문, 메서드 같은 특정 코드 블록이 실행되는지 알고 싶을 때1부터 5까지의 합계를 구하는 간단한 코드가 있다고 상상해 봅시다.
[코드]
int GetSum(int maxNumber)
{
int sum = 0;
for (int i = 1; i < maxNumber; i++)
{
sum += i;
}
return sum;
}
int result = GetSum(5);
Console.WriteLine($"1부터 5까지의 합은? {result}");
[실행 결과]
1부터 5까지의 합은? 10
그런데 어찌 된 일인지 코드를 실행하니 10이라는 엉뚱한 결과가 나왔습니다.
어디가 문제일까요? 반복문이 가장 의심스럽네요. '횃불'을 비춰 확인합니다.
[코드]
int GetSumWithDebug(int maxNumber)
{
int sum = 0;
Console.WriteLine(">> 함수 시작! 초기 sum 값: " + sum); // 시작 지점 확인
for (int i = 1; i < maxNumber; i++)
{
sum += i;
// 반복문 내부의 변수 값을 실시간으로 추적!
Console.WriteLine($"현재 i: {i}, 현재까지의 합계(sum): {sum}");
}
Console.WriteLine(">> 함수 종료! 최종 sum 값: " + sum); // 종료 지점 확인
return sum;
}
int result = GetSumWithDebug(5);
Console.WriteLine($"1부터 5까지의 합은? {result}");
[실행 결과]
>> 함수 시작! 초기 sum 값: 0
현재 i: 1, 현재까지의 합계(sum): 1
현재 i: 2, 현재까지의 합계(sum): 3
현재 i: 3, 현재까지의 합계(sum): 6
현재 i: 4, 현재까지의 합계(sum): 10
>> 함수 종료! 최종 sum 값: 10
1부터 5까지의 합은? 10
출력을 확인해보니 범인이 보이네요! i가 1, 2, 3, 4까지만 실행되고
5일 때는 실행되지 않았어요. 반복문의 조건이 i < maxNumber라서
i가 5가 되는 순간 반복문이 종료되었던 것입니다.
원인을 찾았으니 코드를 수정하는 건 간단합니다.
반복문 조건을 i <= maxNumber로 바꿔주면 되겠죠.
[코드]
int GetSum(int maxNumber)
{
int sum = 0;
// i < maxNumber --> // i <= maxNumber 로 수정!
for (int i = 1; i <= maxNumber; i++)
{
sum += i;
}
return sum;
}
int result = GetSum(5);
Console.WriteLine($"1부터 5까지의 합은? {result}");
[실행 결과]
1부터 5까지의 합은? 15
이제 기대했던 15가 정상적으로 출력됩니다.
이 방법은 정말 간단하고 빠르지만, 몇 가지 단점이 있습니다.
가독성 저하: 디버깅을 위해 추가한 WriteLine()코드가 많아지면 지저분해집니다.
삭제의 번거로움: 디버깅이 끝나면 추가했던 코드를 일일이 찾아 지워야 합니다.
기능적 한계: 프로그램 실행을 중간에 멈추거나, 복잡한 객체 내부의 값을 확인하기 어렵습니다.
C# 코드가 점점 복잡해지면, 횃불(WriteLine)만으로는 부족할 때가 옵니다.
디버깅이 끝나면 심어둔 코드를 일일이 지워야 하고, 프로그램 실행을
잠시 멈추고 변수들의 상태를 실시간으로 확인하고 싶다는 생각이 간절해지죠.
WriteLine()이 횃불이었다면, 비주얼 스튜디오의 디버거는 마치
시간을 멈추고 모든 것을 분석할 수 있는 최첨단 탐지기와 같습니다.
먼저, 디버거 기능을 확인하기 위해 간단한 예제를 준비해 볼게요.
[코드]
int GetSum(int maxNumber)
{
int sum = 0;
Console.WriteLine(">> 함수 시작! 초기 sum 값: " + sum); // 시작 지점 확인
for (int i = 1; i <= maxNumber; i++)
{
sum += i; // 여기에 중단점을 설정해 봅시다! (F9)
Console.WriteLine($"현재 i: {i}, 현재까지의 합계(sum): {sum}");
}
Console.WriteLine(">> 함수 종료! 최종 sum 값: " + sum); // 종료 지점 확인
return sum;
}
int result = GetSum(5);
Console.WriteLine($"1부터 5까지의 합은? {result}");
중단점은 프로그램의 실행을 특정 지점에서 '일시 정지'시켜주는 기능입니다.
해당 줄에 커서를 두고 F9 키를 누르면 빨간색 원이 생깁니다. 이게 바로 중단점입니다.
이제 F5 키를 누르면 '디버그 모드'로 프로그램이 시작됩니다.

코드가 실행되다가 중단점을 설정한 sum += i; 라인 앞에서 정확히 멈춥니다.
디버그 모드에서는 아래 단축키를 통해 제어해 보세요.
| 기능 | 단축키 | 설명 |
|---|---|---|
| 계속(Continue) | F5 | 다음 중단점을 만날 때까지 프로그램을 계속 실행합니다. |
| 중단점 설정/해제 | F9 | 해당 줄의 커서 위치에서 중단점을 설정/해제합니다. |
| 프로시저 단위 실행 | F10 | 현재 줄을 실행하고 다음 줄로 이동합니다. (메서드 안으로 X) |
| 한 단계씩 코드 실행 | F11 | 현재 줄을 실행하며, 메서드가 있다면 그 안으로 들어갑니다. |
| 간략한 조사식 | Shift + F9 | 마우스로 클릭한 변수의 값을 바로 확인합니다. |
| 프로시저 나가기 | Shift + F11 | 현재 메서드를 빠져나와 호출한 곳으로 돌아갑니다. |
프로그램을 멈추고 한 줄씩 실행하는 진짜 이유는 바로
그 순간의 변수 값이 어떻게 변하는지를 보기 위함입니다.

실행이 멈춘 상태에서 궁금한 변수 위로 마우스 커서를 가져가 보세요.
마치 돋보기로 들여다보듯 현재 변수에 담긴 값을 바로 보여줍니다.
변수를 체계적으로 관찰하고 싶다면 디버깅 중에 나타나는 창을 활용해야 합니다.

로컬(Locals): 현재 실행 중인 코드 범위 내에서 접근 가능한 모든 변수를
자동으로 보여주는 대시보드입니다. 값이 어떻게 변하는지 한눈에 파악할 수 있죠.

조사식(Watch): 내가 감시하고 싶은 변수만 볼 수 있는 맞춤형 도구입니다.
심지어 i * 5 같은 연산식의 결과도 실시간으로 추적할 수 있습니다.

예외가 발생하면 비주얼 스튜디오는 자동으로 실행을 멈추고
문제가 생긴 정확한 줄과 예외 메시지를 보여줍니다.
반복문을 돌 때 모든 단계에서 멈추면 너무 비효율적이겠죠?
이럴 때는 조건부 중단점을 사용합니다.

해당 줄에 커서를 두고 우클릭 → [중단점] → [조건부 중단점 삽입] 또는 [조건]

조건식 입력란에 i == 3을 입력하면 i가 3일 때만 프로그램이 멈춥니다.
호출 스택(Call Stack)은 메서드가 호출될 때마다 기록을 쌓아두는
특별한 메모리 공간이자, 시각적으로 보여주는 디버깅 도구입니다.
마치 책을 쌓는 것처럼, 호출된 메서드 정보가 맨 위에 쌓입니다.
이 과정을 통해 전체적인 흐름과 경로를 쉽게 파악할 수 있습니다.
다음 코드는 호출 스택(Call Stack)을 테스트하려는 간단한 예제입니다.
[코드]
class Program
{
static void Main()
{
MethodA();
static void MethodA()
{
MethodB();
}
static void MethodB()
{
MethodC();
}
static void MethodC()
{
int zero = 0;
int result = 10 / zero; // 예외 발생
}
}
}

디버깅 시 호출 스택(Call Stack) 창을 보면 다음과 같은 정보를 알 수 있습니다.
맨 위: 현재 프로그램이 멈춰있는 메서드
아래로 갈수록: 먼저 호출되었던 메서드들 (과거 기록)
최신 버전의 비주얼 스튜디오라면 GitHub Copilot이 자동으로 포함되어 있습니다.
GitHub Copilot은 단순한 코드 완성 도구를 넘어서 디버깅 과정 전반에서
심도 있는 분석과 코드 수정 제안을 해주는 전문 수사관입니다.
해당 기능을 사용하려면 반드시 비주얼 스튜디오에 로그인하셔야 합니다.
여러분이 몇 글자만 입력해도, Copilot이 코드를 파악하고 제안을 합니다.
Copilot은 주석을 이해하고 있어서 만들고 싶은 기능을 주석으로 작성하면
주석을 참고해서 C# 코드로 만들어 줍니다.
Copilot이 회색 글씨로 완성된 코드를 미리 보여줍니다.
코드가 마음에 들면 Tab 키를 눌러서 바로 적용할 수 있습니다.
Copilot과의 채팅을 통해 질문을 하거나 코드를 수정하는 기능입니다.
우측 상단에서 Copilot 아이콘 → [채팅 창 열기]를 선택합니다.
GitHub Copilot 채팅 창에서 Copilot과 대화할 수 있습니다.
여기서 Copilot과 더 빠르게 채팅하는 방법이 있어요.
우클릭 → [Copilot에게 물어보기] 또는 단축키(Alt+/)를 누르면
Copilot과 더 빠르게 채팅할 수 있는 인라인 채팅이 나타납니다.
이번에는 실제로 예외를 발생시켜 Copilot의 진가를 확인해 보겠습니다.
아래 코드를 작성하고 F5 키를 눌려 '디버그 모드'로 프로그램을 시작합니다.
[코드]
using System;
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // 예외 발생
예외가 발생하면 [Copilot으로 분석]을 선택합니다.
이 예제에서는 Copilot 답변을 끝까지 스크롤하고 후속 질문을 선택합니다.
후속 질문을 던지면 Copilot은 코드의 수정 사항을 보여줍니다.
[적용]을 눌려 예외가 발생한 코드를 수정할 수 있습니다.
Tap 키를 누르면 코드 수정이 적용됩니다.
코드가 수정되었습니다. 이제 예외가 발생하지 않습니다.
WriteLine()이라는 횃불과 비주얼 스튜디오 디버거라는 최첨단 탐지기는
공통적인 한계가 있습니다. 바로 '휘발성'이라는 점이죠.
Console.WriteLine()은 프로그램이 종료되면 사라지고,
비주얼 스튜디오 디버거는 우리가 직접 실행하고 있을 때만 의미가 있습니다.
만약 서버에서 아무도 모르게 발생했던 오류의 원인을 찾아야 한다면 어떨까요?
이럴 때 필요한 것이 코드의 블랙박스 역할을 해 줄 로깅(Logging) 시스템입니다.
대표적인 C# 로깅 라이브러리 중 하나인 Serilog를 통해 알아보겠습니다.
Serilog는 체계적으로 정리된 사건 파일과 같습니다. Serilog가 특별한 이유는
바로 '구조화된 로깅(Structured Logging)'을 지원하기 때문입니다.
[일반적인 로그]
"오류 발생: 사용자 123의 주문 처리 실패"
[구조화된 로그]
{"Level":"Error", "Message":"주문 처리 실패", "UserId":123}
Serilog를 사용하면 다음과 같은 장점들을 얻을 수 있습니다.
[로그 레벨]
로그의 심각도에 따라 등급을 매기고, 원하는 레벨의 로그만 필터링해서 볼 수 있습니다.
[Sink]
로그를 콘솔, 파일, 데이터베이스, 전문 로그 분석 도구(Seq 등)로 보낼 수 있습니다.
[유연한 설정]
코드 한두 줄만 바꾸거나 설정 파일 수정만으로 로깅 방식을 쉽게 변경할 수 있습니다.
간단한 콘솔 프로젝트에 Serilog를 설치하고 설정해 봅시다.
이때 프로젝트 이름을 Serilog 패키지와 동일하게 지으면 오류가 발생합니다.
먼저 필요한 Serilog 패키지들을 설치해 보겠습니다.
솔루션 탐색기 창 → 솔루션 항목에 마우스를 올리고 우클릭합니다.
NuGet 패키지 관리자에서 아래 패키지들을 검색하여 설치해 주세요.
패키지 이름을 검색하고 [설치]를 선택합니다.
패키지를 설치하기 위해 [적용]을 선택합니다.
이 상태가 되면 Program.cs 파일을 열어 다음 코드를 작성합니다.
[코드]
using System;
using Serilog; // Serliog 기능을 사용하기 위해 필요합니다.
class Program
{
static void Main()
{
// 1. Serilog 로거 설정
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() // 가장 낮은 로그 레벨을 Debug로 설정
.WriteTo.Console() // 로그를 콘솔에 출력
.WriteTo.File("logs/myapp-.txt", rollingInterval: RollingInterval.Day) // 로그를 파일에 출력
.CreateLogger();
try
{
Log.Information("애플리케이션 시작!");
// ... 이곳에 원래 프로그램 코드가 들어갑니다 ...
ProcessOrder();
Log.Information("애플리케이션 정상 종료.");
}
catch (Exception ex)
{
Log.Fatal(ex, "애플리케이션 처리 중 치명적인 오류 발생!");
}
finally
{
// 2. 프로그램 종료 전, 기록되지 않은 로그가 있다면 모두 기록하고 종료
Log.CloseAndFlush();
}
}
public static void ProcessOrder()
{
var orderId = 101;
var customerName = "velopert";
// {OrderId}, {CustomerName} 형태로 구조화된 데이터를 기록
Log.Information("주문 처리 시작: OrderId={OrderId}, Customer={CustomerName}",
orderId, customerName);
// 가상의 경고 상황
Log.Warning("재고 부족! 남은 수량: {StockCount}", 5);
// 가상의 오류 상황
try
{
int zero = 0;
int result = 10 / zero;
}
catch (Exception ex)
{
// 예외(Exception) 객체를 함께 기록하여 상세 정보(스택 트레이스 등)를 남김
Log.Error(ex, "주문 처리 중 오류 발생. OrderId={OrderId}", orderId);
}
}
}
[코드 분석]
LoggerConfiguration:
여기서 로깅 규칙을 정합니다.
MinimumLevel:
설정한 레벨 이상의 로그만 기록하겠다는 의미입니다.
Debug로 설정하면 Debug, Information, Warning, Error, Fatal이 모두 기록됩니다.
WriteTo:
로그를 어디에 보낼지(Sink)를 정합니다. 여기서는 콘솔과 파일에 사용했습니다.
rollingInterval: RollingInterval.Day:
로그 파일(myapp-20250809.txt)을 매일 하나씩 새로 생성하라는 아주 유용한 옵션입니다.
Log.CloseAndFlush():
프로그램이 갑자기 종료될 때 로그가 유실되는 것을 막기 위해,
남아있는 로그를 모두 기록하고 깔끔하게 종료하는 중요한 코드입니다.
[로그 레벨]
Serilog에서 로그 레벨은 중요도에 따라 6가지로 나뉩니다.
| 레벨(Level) | 설명 | 주요 사용 사례 |
|---|---|---|
| Verbose | 가장 상세한 추적 정보 (성능에 영향) | 개발 중 내부 흐름 확인 (일시적) |
| Debug | 개발 및 디버깅 과정에서 유용한 정보 | 메서드 진입/종료, 주요 분기점 통과 |
| Information | 정상적인 동작 흐름 | 서비스 시작/종료, 작업 완료 로그 |
| Warning | 잠재적인 문제나 예상치 못한 상황 | 성능 저하 경고, 서비스 재시도 |
| Error | 기능이 실패하거나 예외가 발생한 상황 | try-catch에서 잡힌 예외 로깅 |
| Fatal | 가장 심각한 수준의 오류 | 시스템 다운, 메모리 손상 |
[실행 결과]

해당 예제를 실행하면 콘솔 창에는 색상이 입혀진 깔끔한 로그가 표시되고,
프로젝트 폴더 아래 logs 폴더에는 텍스트 파일이 생성된 것을 확인할 수 있습니다.
[텍스트 파일 내용]
2025-08-09 20:19:44.288 +09:00 [INF] 애플리케이션 시작!
2025-08-09 20:19:44.362 +09:00 [INF] 주문 처리 시작: OrderId=101, Customer=velopert
2025-08-09 20:19:44.368 +09:00 [WRN] 재고 부족! 남은 수량: 5
2025-08-09 20:19:44.382 +09:00 [ERR] 주문 처리 중 오류 발생. OrderId=101
System.DivideByZeroException: Attempted to divide by zero.
at Program.ProcessOrder() in C:\9주차\MyApp\Program.cs:line 51
2025-08-09 20:19:44.420 +09:00 [INF] 애플리케이션 정상 종료.
파일에 JSON 형태로 구조화된 데이터가 저장되어 나중에 분석하기 용이합니다.
디버깅은 상태를 추적하고 문제의 원인을 분석하는 데 필수적인 역할을 합니다.
| 도구 | 특징 | 비유 |
|---|---|---|
Console.WriteLine | 간단하고 즉각적인 값 확인 | 횃불 |
| 비주얼 스튜디오 디버거 | 실시간 코드 흐름 및 상태 분석 | 최첨단 탐지기 |
| Serilog | 영구적이고 체계적인 이벤트 기록 | 블랙박스 / 비행기록장치 |