PDB(프로그램 데이터베이스)란 무엇인가 - 디버그 정보·심볼·Source Link 이해하기

· · .NET, C#, Visual Studio, PDB, 디버깅, 심볼, Source Link, 진단, 운영, 기존 자산 활용

1. 가장 먼저 알아둘 것

.NET이나 C++ 애플리케이션을 빌드하면 .dll이나 .exe와 함께 .pdb 파일이 생성되는 경우가 있습니다.

예를 들면 이런 출력입니다.

MyApp.exe
MyApp.dll
MyApp.pdb

.pdb가 무엇인지 모른 채 개발하다 보면 이런 의문이 생깁니다.

  • .pdb는 운영 환경에 두어도 되는가
  • .pdb가 없으면 애플리케이션이 동작하지 않는가
  • Release 빌드인데 .pdb가 나오는 것은 이상한가
  • .pdb에는 소스 코드가 통째로 들어 있는가
  • .pdb가 있으면 반드시 브레이크포인트를 걸 수 있는가
  • 덤프 분석이나 장애 조사에서 왜 .pdb가 필요해지는가
  • NuGet 패키지에서는 .pdb를 어떻게 다루어야 하는가
  • Source Link, 심볼 서버, .snupkg는 무엇이 다른가

PDB는 평소 구현에서는 주역이 되지 않습니다. 하지만 장애 조사, 덤프 분석, 라이브러리 배포, 운영 로그의 가독성, 디버깅 경험에서는 상당히 중요합니다.

먼저 결론을 적어 둡니다.

PDB는 실행 파일이나 어셈블리와 소스 코드를 연결하기 위한 디버그 정보 파일입니다. 애플리케이션을 동작시키기 위한 본체가 아니라, “어느 명령이 어느 소스 줄에 대응하는가”, “어느 로컬 변수가 무엇인가”, “어느 소스를 보면 되는가”를 디버거나 진단 도구에게 알려주기 위한 것입니다.

이 글에서는 PDB를 단순한 “디버깅용 부속 파일”로 보지 않고, 실무에서 어떻게 다루어야 할 산출물인지라는 관점에서 정리합니다.

2. PDB란 무엇인가

PDB는 Program Database의 약어로, 한국어로는 프로그램 데이터베이스로 번역됩니다. .pdb 파일은 흔히 심볼 파일이라고도 불립니다.

심볼이란 대략적으로 말하면, 프로그램 안의 이름이나 위치에 관한 정보입니다. 예를 들면 다음과 같은 것들입니다.

  • 함수명
  • 메서드명
  • 로컬 변수명
  • 인자명
  • 타입 정보
  • 소스 파일명
  • 소스 줄 번호
  • 소스상의 위치와 컴파일 후 명령의 대응
  • 디버거가 브레이크포인트를 두기 위한 정보
  • Source Link용 소스 취득 정보

소스 코드를 작성할 때는 이렇게 인간이 이해할 수 있는 이름이 있습니다.

public decimal CalculateTotalPrice(Order order)
{
    var subtotal = order.Lines.Sum(x => x.Price * x.Quantity);
    var tax = subtotal * 0.10m;
    return subtotal + tax;
}

그러나 빌드 후의 .dll이나 .exe는 소스 코드 그 자체가 아닙니다. .NET이라면 IL과 메타데이터, 네이티브 C++라면 기계어에 가까운 바이너리가 됩니다.

그 결과 다음과 같은 정보는 실행 파일만으로는 충분히 알 수 없거나 알기 어려워집니다.

이 기계어 / IL은 어느 .cs의 몇 번째 줄인가
이 주소는 어느 함수의 어느 위치인가
이 로컬 변수의 이름은 무엇이었는가
이 브레이크포인트는 실제로 어느 명령 위치에 두어야 하는가
이 스택 프레임은 어느 소스에 대응하는가

PDB는 이 간극을 메우기 위한 파일입니다.

3. PDB는 실행에 필요한가

일반적으로 PDB는 애플리케이션 실행에 필요하지 않습니다. .dll이나 .exe만 있으면 애플리케이션은 실행할 수 있습니다. .pdb가 없다고 해서 통상적인 처리가 실행되지 않는 것은 아닙니다.

다만 PDB가 없으면 다음과 같은 일이 어려워집니다.

하고 싶은 일 PDB가 없을 때 곤란한 점
Visual Studio에서 정확하게 단계 실행하고 싶다 소스 줄과 실행 위치의 대응을 취할 수 없다
브레이크포인트를 걸고 싶다 대응하는 명령 위치를 알 수 없어 브레이크포인트가 미해결 상태가 되는 경우가 있다
예외 스택 트레이스에 파일명·줄 번호를 표시하고 싶다 줄 번호 정보가 나오지 않거나 불충분해진다
덤프 파일을 분석하고 싶다 스택이나 변수, 타입을 읽기 어려워진다
외부 라이브러리 안으로 단계 진입하고 싶다 라이브러리의 소스와 연결할 수 없다
네이티브 크래시를 읽는다 주소만 남고 함수명이나 위치를 알 수 없다

즉, PDB는 “동작시키기 위한 파일”이 아니라 “조사하기 위한 파일”입니다.

이 차이는 중요합니다. 운영 장애가 발생했을 때 애플리케이션은 PDB 없이도 동작하지만, 조사자는 PDB가 없으면 곤란합니다. 그래서 PDB는 실행 환경에 둘지 여부와는 별개로, 빌드 산출물로서 반드시 보관해 두어야 합니다.

4. PDB가 있으면 무엇이 좋은가

PDB가 있으면 디버거나 진단 도구는 바이너리를 인간이 읽을 수 있는 정보로 되돌리기 쉬워집니다.

예를 들어 PDB가 없는 크래시 정보는 이런 표시가 되는 경우가 있습니다.

MyApp.dll!0x00007ff9a1234567
MyApp.dll!0x00007ff9a1234abc
MyApp.dll!0x00007ff9a1234def

PDB가 올바르게 로드되면 여기까지 볼 수 있게 됩니다.

MyApp.Services.OrderService.CalculateTotalPrice(Order order) Line 42
MyApp.Controllers.OrderController.Post(CreateOrderRequest request) Line 87
MyApp.Program.Main(string[] args) Line 16

이 차이는 큽니다.

전자는 주소부터 조사를 시작해야 합니다. 후자는 처음부터 “어느 메서드의 어느 줄인가”에 도달할 수 있습니다.

장애 조사에서는 처음 30분 안에 원인에 가까워질 수 있는지가 중요합니다. PDB가 남아 있는 것만으로도 조사의 시작 지점이 완전히 달라집니다.

5. PDB에 들어 있는 것

PDB에 들어가는 정보는 언어, 컴파일러, PDB의 형식, 빌드 설정에 따라 달라집니다. 그러므로 “PDB에는 반드시 이것이 들어 있다”라고 단순하게 말할 수는 없습니다.

다만 .NET 개발자의 감각으로는 주로 이 정도의 정보를 기대합니다.

소스 파일과 컴파일 후 코드의 대응
소스 줄 번호
메서드나 함수의 심볼
로컬 변수명
스코프 정보
소스 파일의 경로나 체크섬
Source Link 정보
경우에 따라 임베드된 소스

특히 중요한 것은 소스상의 위치와 실행 시 위치의 대응입니다.

C#으로 작성한 한 줄이 IL이나 JIT 후의 네이티브 코드에서는 여러 명령이 되는 경우가 있습니다. 반대로 최적화에 의해 여러 소스 줄이 합쳐지거나 사라지거나 순서가 바뀐 것처럼 보이는 경우도 있습니다.

디버거는 PDB의 정보를 사용해 “지금 표시해야 할 소스 줄”을 판단합니다.

6. PDB에 들어 있지 않은 것

PDB에 대해서는 들어 있는 것보다 들어 있지 않은 것을 이해하는 쪽이 오해를 줄여줍니다.

PDB는 일반적으로 다음과 같은 것이 아닙니다.

  • 애플리케이션 본체가 아니다
  • 실행에 필수적인 런타임 파일이 아니다
  • 소스 코드 전체의 완전한 백업이 아니다
  • Git 리포지토리를 대신하는 것이 아니다
  • 모든 빌드 설정이나 환경 정보를 완전히 복원해 주는 것이 아니다
  • 그것만으로 결함 원인을 자동으로 설명해 주는 것이 아니다

다만 주의할 점이 있습니다.

PDB에는 소스 파일 경로, 타입명, 함수명, 로컬 변수명, 경우에 따라 Source Link 정보나 임베드된 소스가 포함될 수 있습니다.

그러므로 PDB는 “소스 코드 그 자체가 아니니까 신경 쓰지 않고 공개해도 된다”라고 할 수 없습니다.

내부 프로젝트명, 사용자명을 포함한 경로, 사내 디렉터리 구성, 비공개 타입명, 업무 로직을 추측할 수 있는 이름 등이 포함될 가능성이 있습니다.

7. 흔한 오해 1: PDB가 있으면 운영 환경이 느려진다

PDB가 옆에 놓여 있는 것만으로 통상적인 애플리케이션 처리가 느려지는 것은 아닙니다.

PDB는 디버거나 진단 도구가 심볼 정보를 필요로 할 때 사용하는 것입니다. 애플리케이션의 통상 처리가 매번 PDB를 읽으면서 동작하는 것은 아닙니다.

물론 예외 스택 트레이스에서 파일명이나 줄 번호를 해석하거나, 디버거를 연결하거나, 프로파일러나 진단 도구가 심볼을 읽는 등의 상황에서는 사용됩니다.

그러나 “PDB를 두면 항상 느려지니 절대로 운영 환경에 두어서는 안 된다”라는 이해는 대략적입니다.

실무상으로는 이렇게 생각하는 것이 안전합니다.

실행 성능만을 이유로 PDB를 삭제할 필요는 크지 않다
공개 범위·정보 유출·산출물 크기·운영 정책의 이유로 배치 방침을 정한다
배치하지 않는 경우에도 같은 빌드의 PDB는 반드시 보관한다

8. 흔한 오해 2: Release 빌드라면 PDB는 필요 없다

Release 빌드에서도 PDB는 유용합니다. 오히려 운영 장애 조사에서 필요해지는 것은 Release 빌드의 PDB입니다.

운영 환경에서 동작하는 것이 Release 빌드라면 Debug 빌드의 PDB를 가지고 있어도 의미가 없습니다. 디버거가 필요로 하는 것은 그 운영 바이너리를 만들었을 때 생성된 PDB입니다.

여기서 중요한 것은 다음과 같은 구별입니다.

항목 의미
Debug / Release 최적화, 조건부 컴파일, 출력 설정 등의 빌드 구성
PDB의 유무 디버그 정보를 생성·보존하는가
디버깅 용이성 최적화 여부, PDB 내용, 소스 일치, JIT 동작 등에 의해 결정된다

Release 빌드는 최적화되어 있는 경우가 많아, Debug 빌드보다 단계 실행이 알아보기 어려워집니다. 로컬 변수가 최적화로 사라지거나, 소스 줄 순서대로 멈추지 않는 경우가 있습니다.

그래도 PDB가 있으면 이러한 정보를 얻기 쉬워집니다.

  • 예외가 발생한 소스 줄
  • 스택상의 메서드명
  • 덤프 분석 시의 대응 위치
  • 프로파일링 결과의 함수명
  • 로그와 소스의 대조

Release라서 PDB가 불필요한 것이 아닙니다. Release이기 때문에 그 빌드에 대응하는 PDB를 남겨 두어야 합니다.

9. 흔한 오해 3: PDB가 있으면 어떤 바이너리든 디버깅할 수 있다

PDB는 어떤 .dll이나 .exe에나 재사용할 수 있는 것이 아닙니다.

디버거는 대상 바이너리와 PDB가 일치하는지를 확인합니다. 일치하지 않는 PDB를 억지로 사용하면 소스 줄, 함수, 변수의 대응이 어긋나 버리기 때문입니다.

예를 들어 이런 상황에서는 PDB가 있어도 사용할 수 없거나 도움이 되지 않는 경우가 있습니다.

로컬에서 다시 빌드한 PDB를 운영 DLL에 적용하려고 한다
같은 버전 번호이지만 실제로는 다른 커밋에서 빌드되었다
hotfix 후의 DLL에 hotfix 전의 PDB를 로드시키려고 한다
최적화 설정이나 조건부 컴파일이 다르다

PDB는 “같은 소스라면 대체로 맞는다”가 아니라 “같은 빌드 산출물에 대응해야 한다”라고 생각해야 합니다.

그러므로 CI/CD에서는 이 단위로 보존하는 것이 기본입니다.

커밋 ID
빌드 번호
산출물의 버전
.dll / .exe
.pdb
소스 참조 정보

이 조합을 무너뜨리지 않는 것이 중요합니다.

10. 흔한 오해 4: PDB가 있으면 소스 코드가 없어도 완전히 읽을 수 있다

PDB가 있어도 소스 코드가 반드시 들어 있는 것은 아닙니다.

PDB는 주로 소스와 바이너리를 대응시키는 정보를 가지고 있습니다. 소스 파일의 경로나 체크섬, Source Link 정보는 가질 수 있지만, 일반적인 PDB가 항상 소스 본문을 통째로 가지고 있다고는 할 수 없습니다.

그러므로 디버거에서 외부 라이브러리로 단계 진입하고 싶은 경우, 다음 중 하나가 필요해집니다.

로컬에 같은 소스 파일이 있다
Source Link로 올바른 커밋의 소스를 취득할 수 있다
PDB에 소스가 임베드되어 있다
디컴파일한 소스로 대체한다

Visual Studio에는 .NET 어셈블리를 디컴파일해서 표시하는 기능도 있습니다. 다만 디컴파일 결과는 원래 소스 그 자체가 아닙니다. 주석, 공백, 로컬 변수명, 원래의 작성 방식, 전처리기 조건 등은 사라지거나 바뀌게 됩니다.

조사에 사용하기에는 편리하지만, 원본 소스의 대체물로 지나치게 신뢰하지 않는 것이 좋습니다.

11. PDB와 스택 트레이스

.NET의 예외 스택 트레이스에서는 PDB가 있는지 여부에 따라 표시 방식이 달라집니다.

PDB가 없는 경우에도 메서드명이나 타입명이 표시되는 경우는 있습니다. .NET 어셈블리에는 메타데이터가 있기 때문입니다.

그러나 파일명이나 줄 번호까지 표시하고 싶다면 PDB가 중요해집니다.

예를 들어 PDB가 없는 경우의 스택 트레이스는 이렇게 되기 쉽습니다.

System.InvalidOperationException: Order is invalid
   at MyApp.Services.OrderService.Validate(Order order)
   at MyApp.Controllers.OrderController.Post(CreateOrderRequest request)

PDB가 있고 줄 번호를 해석할 수 있는 경우는 이렇게 바뀝니다.

System.InvalidOperationException: Order is invalid
   at MyApp.Services.OrderService.Validate(Order order) in /src/MyApp/Services/OrderService.cs:line 42
   at MyApp.Controllers.OrderController.Post(CreateOrderRequest request) in /src/MyApp/Controllers/OrderController.cs:line 87

운영 중에 이 차이가 나면 조사 속도가 크게 달라집니다.

다만 파일 경로가 노출되는 것 자체가 정보 공개가 되는 경우도 있습니다. 외부에 반환하는 에러 응답에 스택 트레이스를 표시하지 않는다, 로그 보관처를 제한한다와 같은 별도의 대책도 필요합니다.

12. PDB와 덤프 분석

PDB가 가장 고맙게 느껴지는 것은 덤프 분석을 할 때입니다.

운영 환경에서 이런 문제가 발생했다고 가정합니다.

  • 프로세스가 크래시했다
  • CPU 사용률이 계속 높게 유지되었다
  • 데드락처럼 보인다
  • 메모리가 계속 증가한다
  • 응답이 돌아오지 않는다
  • 네이티브 라이브러리와의 경계에서 죽었다

이때 덤프 파일을 취득해서 분석합니다.

그러나 덤프만으로는 충분하지 않습니다. 덤프에 포함되는 것은 그 시점의 프로세스 상태입니다. 거기에 찍힌 스택이나 모듈을 인간이 읽을 수 있는 이름이나 소스 줄로 변환하려면 대응하는 PDB가 필요합니다.

.NET에서는 dotnet-dump, Visual Studio, WinDbg, SOS 등을 사용해 분석합니다. 네이티브를 포함하는 경우에는 WinDbg의 심볼 설정이 중요해집니다.

덤프 분석에서 흔히 발생하는 실패를 들어봅니다.

운영 DLL은 남아 있지만 PDB가 없다
PDB는 있지만 로컬에서 다시 빌드한 다른 것이었다
Windows / .NET 런타임의 심볼을 로드하지 못하고 있다
자사 애플리케이션의 PDB는 있지만 서드파티 라이브러리의 심볼이 없다
심볼 경로가 설정되어 있지 않아 디버거가 PDB를 찾지 못한다

덤프 분석은 문제가 발생한 후에 준비하면 늦는 경우가 있습니다. 빌드 시점에 PDB를 보존하고, 조사 시에 꺼낼 수 있도록 해 두는 것이 중요합니다.

13. Windows PDB와 Portable PDB

PDB에는 여러 형식이 있습니다.

실무에서 챙겨 두어야 할 대표적인 것은 다음 두 가지입니다.

종류 주요 맥락 특징
Windows PDB Visual C++, 기존 Windows 디버깅 Windows 네이티브 개발에서 자주 사용되는 형식
Portable PDB .NET / .NET Core 이후 크로스 플랫폼으로 다룰 수 있는 .NET 대상 형식

.NET Core 이후에는 Portable PDB가 중요합니다. Portable PDB는 Windows뿐만 아니라 Linux나 macOS에서도 다룰 수 있는 형식입니다.

.NET Framework 시대의 프로젝트나 오래된 Visual Studio 설정에서는 Windows PDB를 볼 기회도 있습니다. 현재의 .NET SDK 스타일 프로젝트에서는 일반적으로 Portable PDB를 전제로 생각하는 경우가 늘고 있습니다.

여기서 주의해야 할 점은 확장자가 어느 쪽이든 .pdb라는 것입니다.

확장자만으로는 형식의 차이를 알 수 없습니다. “PDB”라고 이야기가 나왔을 때는 어느 맥락의 이야기인지 확인합니다.

.NET의 Portable PDB 이야기인가
Visual C++의 Windows PDB 이야기인가
.NET Framework의 오래된 프로젝트 이야기인가
NuGet 배포용 PDB 이야기인가
WinDbg에서 사용하는 심볼 이야기인가

14. .NET의 DebugType

C# 프로젝트에서는 DebugType으로 디버그 정보의 출력 방식을 지정할 수 있습니다.

대표적인 값은 다음과 같습니다.

DebugType 의미
portable Portable PDB를 별도 파일로 생성한다
embedded Portable PDB에 상당하는 디버그 정보를 .dll / .exe에 임베드한다
full 현재 플랫폼의 기본 형식으로 PDB를 생성한다
pdbonly C# 6.0 이후에는 full과 실질적으로 차이가 없는 취급
none PDB를 생성하지 않는다

현재의 .NET SDK 스타일 프로젝트에서는 C#의 DebugType이 Debug / Release 어느 쪽이든 기본값으로 portable입니다. 그러므로 통상적으로는 Portable PDB를 출력하기 위해서만 DebugType을 명시할 필요는 없습니다.

명시적으로 작성하는 의미가 있는 것은 프로젝트나 조직으로서 “이 형식으로 고정한다”라고 나타내고 싶은 경우, 오래된 프로젝트와의 차이를 눈에 띄게 하고 싶은 경우, 또는 embedded / none 등 기본값과는 다른 방침을 선택하는 경우입니다.

NuGet 라이브러리나 외부 배포에서는 portable, embedded, .snupkg 중 어느 것을 선택할지를 검토합니다.

예를 들어 명시적으로 Portable PDB를 출력한다면 이렇게 작성합니다.

<PropertyGroup>
  <DebugType>portable</DebugType>
</PropertyGroup>

PDB를 별도 파일로 만들지 않고 어셈블리에 임베드한다면 이렇습니다.

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

Release 빌드에서 아무래도 PDB를 출력하고 싶지 않다면 이렇게 작성할 수 있습니다.

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <DebugType>none</DebugType>
</PropertyGroup>

다만 이것은 신중하게 결정해야 합니다. Release PDB를 출력하지 않는 설정으로 하면 운영 장애 조사에서 자신들이 곤란해질 가능성이 있습니다.

15. DebugSymbols=false만으로 충분한가

PDB 생성을 멈추고 싶은 경우, DebugSymbolsfalse로 하는 예를 볼 수 있습니다.

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <DebugSymbols>false</DebugSymbols>
</PropertyGroup>

다만 PDB를 확실히 생성하지 않으려는 의도라면 DebugTypenone으로 하는 쪽이 명확합니다.

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <DebugType>none</DebugType>
</PropertyGroup>

이 설정은 라이브러리나 애플리케이션의 배포 정책으로 사용하는 경우가 있습니다. 그러나 다시 말하지만, PDB를 만들지 않는 것은 장애 조사 능력을 떨어뜨립니다.

실무에서는 이런 구분이 많습니다.

PDB는 빌드 산출물로서 반드시 생성한다
운영 서버에 배치할지는 별도로 판단한다
배치하지 않는 경우에도 CI 산출물이나 심볼 서버에 보관한다

16. C++의 PDB는 .NET과 조금 다르다

C++의 PDB는 .NET의 PDB와는 맥락이 조금 다릅니다.

Visual C++에서는 /Zi/ZI와 같은 옵션으로 PDB가 생성됩니다. 또한 컴파일러가 사용하는 PDB와, 링커가 최종적으로 .exe / .dll용으로 만드는 PDB가 관련됩니다.

C++에서는 네이티브 코드의 주소, 함수, 타입, 로컬 변수, 인라인 확장, 최적화 후의 위치 등을 읽기 위해 PDB가 매우 중요합니다.

또한 네이티브 장애 조사에서는 PDB가 없으면 이런 상태가 되기 쉽습니다.

예외 주소는 알 수 있다
모듈명은 알 수 있다
그러나 함수명이나 소스 줄을 알 수 없다

C++와 C#이 혼재하는 애플리케이션, P/Invoke, C++/CLI, 네이티브 DLL을 호출하는 .NET 애플리케이션에서는 .NET 측의 PDB뿐만 아니라 네이티브 측의 PDB도 필요해집니다.

17. public symbol과 private symbol

Windows 심볼의 세계에서는 public symbolsprivate symbols라는 구별이 있습니다.

대략적으로 말하면 이런 차이입니다.

종류 포함되는 정보의 이미지
private symbols 로컬 변수, 타입, 인자, 상세한 내부 정보 등을 포함하는 완전한 쪽에 가까운 정보
public symbols 공개용으로 축소된 함수명이나 주소 등의 정보

외부에 배포하는 심볼에서는 private symbols를 제거하고 public symbols만으로 하는 경우가 있습니다.

이는 디버깅 가능성과 정보 공개 범위의 균형을 맞추기 위해서입니다.

예를 들어 자사 제품의 크래시 분석을 위해 최소한의 함수명은 노출하고 싶습니다. 그러나 내부의 로컬 변수명이나 타입 정보까지는 노출하고 싶지 않습니다. 그런 경우에 stripped PDB를 사용하는 경우가 있습니다.

Windows 대상 네이티브 개발에서는 PDBCopy 등으로 private symbol을 제거한 PDB를 만드는 경우가 있습니다.

한편 사내 장애 조사용으로는 완전한 PDB를 보관해 두어야 합니다. 외부용으로 축소한 PDB만 남아 있으면 깊은 조사에서 곤란해집니다.

18. 디버거는 PDB를 어디에서 찾는가

Visual Studio나 WinDbg는 여러 위치에서 PDB를 찾습니다.

대표적인 위치는 다음과 같습니다.

프로젝트의 출력 폴더
.dll / .exe와 같은 폴더
.dll / .exe에 기록된 PDB의 원래 경로
Visual Studio의 심볼 설정에서 지정한 폴더
로컬 심볼 캐시
사내 심볼 서버
Microsoft Symbol Server
NuGet.org Symbol Server
Azure Artifacts 등의 심볼 서버

PDB가 있는데도 로드되지 않는 경우에는 다음 항목을 순서대로 확인합니다.

PDB가 대상 바이너리와 일치하는가
PDB가 디버거의 검색 경로에 들어 있는가
심볼 서버에 접근할 수 있는가
로컬 캐시에 오래된 것이 남아 있지 않은가
대상 모듈의 심볼 로드가 비활성화되어 있지 않은가
Just My Code 설정에서 외부 코드로 취급되고 있지 않은가

Visual Studio에서는 디버깅 중에 Modules 창을 보면 각 모듈의 심볼 로드 상태를 확인할 수 있습니다.

Debug
  Windows
    Modules

여기서 대상 DLL의 Symbol Status를 확인합니다.

흔히 보이는 것은 다음 네 가지 표시입니다.

Symbols loaded.
Cannot find or open the PDB file.
PDB does not match image.
Skipped loading symbols.

PDB 관련으로 막히면 먼저 Modules 창을 보는 것이 가장 빠른 길입니다.

19. 심볼 서버란 무엇인가

심볼 서버는 PDB 등의 심볼 파일을 디버거가 필요할 때 취득할 수 있도록 하는 체계입니다.

단순히 공유 폴더에 PDB를 두는 것만으로도 어느 정도는 운영할 수 있습니다. 그러나 버전이 늘어나면 바로 무너집니다.

MyApp v1.0.0의 PDB
MyApp v1.0.1의 PDB
MyApp v1.0.1 hotfix의 PDB
MyApp v1.1.0-beta의 PDB
고객A용으로만 설정이 다른 PDB

같은 파일명 MyApp.pdb가 수십 개 나타납니다.

심볼 서버는 PDB를 단순한 파일명이 아니라 바이너리와의 일치 정보에 기반하여 정리합니다. 그러므로 디버거는 “이 DLL에 맞는 PDB”를 찾기 쉬워집니다.

실무에서 고려할 수 있는 구성의 예입니다.

Microsoft Symbol Server
  Windows나 .NET 런타임 등의 심볼 취득에 사용한다

NuGet.org Symbol Server
  공개 NuGet 패키지의 심볼 취득에 사용한다

사내 심볼 서버
  자사 애플리케이션·자사 라이브러리의 PDB를 보존한다

로컬 심볼 캐시
  한 번 취득한 PDB를 재사용해 디버깅을 빠르게 한다

사내 서비스에서 운영 장애를 조사한다면 CI가 PDB를 사내 심볼 스토어로 발행하는 체계를 마련해 두면 편리합니다.

20. Source Link란 무엇인가

Source Link는 PDB와 소스 관리 시스템을 연결하는 체계입니다.

PDB가 “이 위치는 이 소스 파일에 대응한다”라고 알고 있어도, 그 소스 파일이 로컬에 없으면 디버거는 표시할 수 없습니다.

Source Link가 있으면 디버거는 PDB에 들어 있는 정보를 사용해 GitHub, Azure Repos, GitLab, Bitbucket 등에서 대응하는 커밋의 소스 파일을 취득할 수 있습니다.

즉, Source Link가 해결하는 것은 다음과 같은 문제입니다.

NuGet으로 취득한 라이브러리 안으로 단계 진입하고 싶다
로컬에 그 라이브러리의 소스를 clone하지 않았다
그러나 PDB와 리포지토리 정보는 있다
디버거가 올바른 커밋의 소스를 가져오러 간다

이는 라이브러리 사용자의 경험을 크게 개선합니다.

Source Link의 핵심은 “최신 main 브랜치”가 아니라 “그 바이너리를 만든 커밋”을 참조한다는 점입니다.

최신 소스가 아니라 빌드 시점의 소스와 연결되는 데 의미가 있습니다.

.NET 8 이후의 SDK에서는 Source Link의 처리가 개선되었습니다.

GitHub, Azure Repos, GitLab, Bitbucket 등 자주 사용되는 제공자에서는 .NET SDK 측에 Source Link 체계가 포함되게 되었습니다.

그러므로 이전처럼 반드시 Microsoft.SourceLink.GitHub 등을 명시적으로 추가해야 한다는 이해는 점차 낡은 것이 되고 있습니다.

다만 확인이 필요한 경우도 있습니다.

.NET 8 SDK 미만으로 빌드하고 있다
SDK 스타일이 아닌 오래된 프로젝트다
온프레미스의 독자적인 Git 호스팅을 사용하고 있다
표준 대응 외의 Source Link 제공자를 사용하고 있다
NuGet 패키지의 메타데이터까지 정비하고 싶다

NuGet 패키지에서 리포지토리 정보를 표시하고 싶은 경우에는 다음 설정을 사용합니다.

<PropertyGroup>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
</PropertyGroup>

추적되지 않는 파일을 PDB에 임베드해야 하는 경우에는 다음 설정을 검토합니다.

<PropertyGroup>
  <EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

다만 임베딩은 정보 공개 범위에 영향을 줍니다. 무엇을 PDB에 포함할지는 공개 대상과 운영 방침에 맞추어 결정해야 합니다.

22. embedded PDB란 무엇인가

DebugTypeembedded를 지정하면 Portable PDB의 디버그 정보가 .dll이나 .exe에 임베드됩니다. 이 경우 별도 파일인 .pdb는 생성되지 않습니다.

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

이것이 편리한 것은 다음과 같은 상황입니다.

  • 단일 파일에 가까운 형태로 배포하고 싶다
  • PDB를 빼놓고 잊어버리는 것을 피하고 싶다
  • 소규모 내부 도구에서 디버그 정보도 함께 가지고 있고 싶다
  • NuGet 패키지에서 PDB 배포의 수고를 줄이고 싶다

한편 단점도 있습니다.

  • 어셈블리 크기가 커진다
  • 배포물에 디버그 정보가 항상 포함된다
  • 정보 공개 범위의 제어가 거칠어진다
  • 큰 라이브러리에서는 restore나 배포 크기에 영향을 준다

embedded PDB는 편리하지만 “일단 전부 embedded로 해 두면 된다”라는 것은 아닙니다.

특히 외부 공개 라이브러리에서는 .snupkg를 통한 심볼 패키지, Source Link, 일반 PDB 동봉, embedded 중 어느 것이 좋을지를 생각합니다.

23. .snupkg란 무엇인가

.snupkg는 NuGet의 심볼 패키지 형식입니다.

일반적인 NuGet 패키지는 .nupkg입니다. 심볼 패키지는 .snupkg입니다.

MyLibrary.1.2.3.nupkg
MyLibrary.1.2.3.snupkg

.nupkg는 사용자가 참조하는 라이브러리 본체를 포함합니다. .snupkg는 디버깅용 PDB를 배포하기 위해 사용합니다.

여기서 중요한 것은 .snupkg가 기본적으로 관리 코드의 Portable PDB용이라는 점입니다. 적어도 NuGet.org의 심볼 서버에서는 Portable PDB만 지원되며, C++ 등의 네이티브 프로젝트가 생성하는 Windows PDB는 받아들여지지 않습니다. Windows PDB를 배포·보관하고 싶은 경우에는 레거시 .symbols.nupkg, 사내 심볼 서버, CI artifact 등 다른 경로를 검토합니다.

작성하려면 예를 들어 이렇게 작성합니다.

<PropertyGroup>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

명령줄에서 지정할 수도 있습니다.

dotnet pack -c Release -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg

공개 NuGet 라이브러리라면 PDB를 .nupkg 본체에 넣기보다 .snupkg와 Source Link를 사용하는 쪽이 배포 크기와 디버깅 경험의 균형을 잡기 쉽습니다.

다만 피드나 도구의 대응 상황에는 주의합니다. 사내 NuGet 피드가 .snupkg에 대응하지 않는 경우에는 다른 방법을 선택해야 합니다.

24. PDB를 운영 환경에 두어야 하는가

“PDB를 운영 환경에 두어야 하는가”는 단순한 예/아니오가 아닙니다.

판단 축은 네 가지입니다.

장애 조사의 편의성
정보 공개 위험
배포 크기
운영 규칙

사내 시스템이라면 .dll.pdb를 같은 폴더에 두는 운영도 현실적입니다. 예외 로그에 줄 번호가 나오기 쉬워지고, 덤프 분석도 하기 쉬워집니다.

외부 배포 애플리케이션이라면 PDB를 그대로 동봉할지는 신중히 판단합니다. 내부 구조나 로컬 변수명, 소스 경로가 노출되는 경우가 있습니다. 필요하다면 public symbols로 한정한다, 심볼 서버를 경유한다, 지원용으로 보관만 한다와 같은 선택지를 취합니다.

웹 서비스라면 서버에 PDB를 둘지 여부와 함께 로그에 나오는 스택 트레이스의 처리도 고려합니다. 외부 응답에 스택 트레이스를 반환해서는 안 됩니다. 내부 로그에는 남기더라도 열람 권한과 보존 기간을 정해야 합니다.

실무에서의 권장은 다음과 같습니다.

PDB는 반드시 생성한다
PDB는 빌드 산출물로서 보관한다
운영 배치는 시스템의 공개 범위에 맞추어 결정한다
외부 공개라면 정보 공개 범위를 확인한다
덤프 분석에서 꺼낼 수 있는 위치에 둔다

25. PDB는 기밀 정보인가

PDB는 소스 코드 본체나 비밀 키와 같은 취급이 필요하다고 할 수는 없습니다. 그러나 무해한 파일로 취급하는 것도 위험합니다.

PDB로 알 수 있는 가능성이 있는 것을 들어봅니다.

  • 개발자의 로컬 경로
  • 사내 폴더 구성
  • 프로젝트명
  • 클래스명, 메서드명
  • 로컬 변수명
  • 내부 API명
  • 업무 용어
  • Source Link의 리포지토리 URL
  • 임베드된 소스
  • Source Server 설정

특히 오래된 Source Server 방식이나 네이티브 PDB의 일부 기능에서는 디버거가 소스 취득을 위해 명령을 실행하는 체계가 관련되는 경우가 있습니다. 신뢰할 수 없는 PDB나 심볼 서버를 무조건 사용하는 것은 피해야 합니다.

또한 PDB를 해석하는 도구나 라이브러리에 부정한 PDB를 입력시키는 경우도 주의가 필요합니다. 외부에서 받은 PDB를 자동으로 처리하는 체계에서는 입력을 신뢰하지 않는 설계로 합니다.

정리하면 PDB는 다음과 같이 다루는 것이 안전합니다.

사내 완전판 PDB는 내부 산출물로서 보호한다
외부 공개하는 PDB는 내용과 공개 범위를 확인한다
Source Link 대상이 private repository라면 인증과 권한을 관리한다
신뢰할 수 없는 심볼 서버를 디버거에 등록하지 않는다

26. CI/CD에서의 PDB 보관 방침

PDB는 개발자의 로컬 PC에만 남아 있어서는 의미가 없습니다. 운영 장애가 발생했을 때 필요한 것은 실제로 릴리스된 빌드에 대응하는 PDB입니다.

그러므로 CI/CD에서는 다음과 같은 형태로 보관합니다.

빌드 번호: 2026.06.10.1234
커밋 ID: abcdef123456...
산출물:
  MyApp.dll
  MyApp.pdb
  MyApp.deps.json
  MyApp.runtimeconfig.json
  package.zip
  container image digest

또한 가능하면 다음 정보도 연결해 둡니다.

Git commit
Git tag
릴리스 번호
환경명
빌드 설정
대상 프레임워크
RID
컨테이너 이미지 digest
NuGet lock file

중요한 것은 PDB를 단독으로 보관하는 것이 아니라 어느 바이너리에 대응하는 PDB인지를 잃지 않는 것입니다.

파일 공유에 MyApp.pdb만 계속 두는 운영은 언젠가 무너집니다. 빌드 단위, 버전 단위, 커밋 단위로 꺼낼 수 있도록 합니다.

27. PDB 배치 패턴

PDB 취급에는 몇 가지 실무 패턴이 있습니다.

패턴 1: DLL과 같은 폴더에 PDB를 두는 방식

가장 단순합니다.

publish/
  MyApp.dll
  MyApp.pdb

장점은 설정이 간단하다는 것입니다. 디버거나 런타임이 찾기 쉽고, 예외 로그의 줄 번호도 표시하기 쉬워집니다.

단점은 배포물에 디버그 정보가 포함된다는 것입니다. 외부 배포에는 적합하지 않은 경우가 있습니다.

패턴 2: PDB는 배치하지 않고 CI 산출물에 보관하는 방식

운영 서버에는 PDB를 두지 않고 CI의 artifact에 남깁니다.

release-artifacts/
  app.zip
  symbols.zip

운영 장애 시에는 덤프나 로그를 취득하고, 대응하는 빌드 번호의 PDB를 꺼내 분석합니다.

정보 공개 범위를 억제하기 쉬운 한편, 조사 시에 꺼내는 절차가 필요합니다.

패턴 3: 사내 심볼 서버에 발행하는 방식

대규모 팀에서는 이것이 다루기 쉽습니다.

CI가 빌드 시에 PDB를 심볼 스토어로 발행합니다. 개발자나 조사자의 Visual Studio / WinDbg는 그 심볼 서버를 참조합니다.

장점은 여러 버전의 PDB를 안전하게 다루기 쉽다는 것입니다. 단점은 초기 구축과 접근 제어가 필요하다는 것입니다.

패턴 4: NuGet의 .snupkg로 배포하는 방식

공개 라이브러리에서는 유력합니다.

MyLibrary.1.2.3.nupkg
MyLibrary.1.2.3.snupkg

사용자는 일반적인 패키지만 복원하고, 디버깅 시에 필요한 심볼만 취득됩니다.

Source Link와 조합하면 외부 라이브러리에서도 소스로 단계 진입하기 쉬워집니다.

패턴 5: embedded PDB로 하는 방식

PDB를 빼놓고 잊어버리는 것을 피하는 데 편리합니다.

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

다만 어셈블리 크기와 정보 공개 범위에 주의합니다.

28. 기존 프로젝트에서 먼저 확인할 것

기존 .NET 프로젝트에서 PDB 취급을 재검토한다면 먼저 여기서부터 확인합니다.

Release 빌드에서 PDB가 생성되고 있는가
생성된 PDB는 어디에 보존되고 있는가
운영에 배포한 DLL과 PDB를 대응시켜 보관하고 있는가
덤프 분석 시에 PDB를 꺼낼 수 있는가
Source Link가 활성화되어 있는가
NuGet 라이브러리라면 .snupkg를 출력하고 있는가
PDB에 불필요한 정보를 지나치게 포함하고 있지 않은가

csproj에서는 다음 설정을 확인합니다.

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <DebugType>portable</DebugType>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
  <EmbedUntrackedSources>true</EmbedUntrackedSources>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

NuGet 패키지라면 이것도 후보입니다.

<PropertyGroup>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

다만 모든 프로젝트에 같은 설정을 넣으면 되는 것은 아닙니다. 내부 애플리케이션, 외부 라이브러리, 온프레미스 제품, SaaS, OSS에서 최적의 해법은 달라집니다.

29. PDB가 로드되지 않을 때 확인하는 방법

PDB가 로드되지 않을 때는 감으로 다시 빌드하기 전에 순서대로 원인을 좁혀 갑니다.

1. 대상 모듈을 확인한다

Visual Studio의 Modules 창에서 대상 DLL / EXE를 찾습니다.

Debug > Windows > Modules

여기서 봐야 할 열은 다음과 같습니다.

Module
Path
Symbol Status
Symbol File
Version
Timestamp

2. 심볼 상태를 본다

각각의 표시는 대체로 이렇게 해석합니다.

표시 의미
Symbols loaded 로드됨
Cannot find or open the PDB file PDB를 찾을 수 없음
PDB does not match image PDB는 있지만 대상 바이너리와 일치하지 않음
Skipped loading symbols 설정에 의해 로드하지 않았을 가능성

3. 로컬의 PDB가 같은 빌드의 것인지 확인한다

흔히 발생하는 실수는 로컬에서 다시 빌드한 PDB를 사용해 버리는 것입니다.

같은 소스라도 빌드 조건이 다르면 일치하지 않는 경우가 있습니다. CI 산출물에서 운영에 배포한 것과 같은 PDB를 취득합니다.

4. 심볼 경로를 확인한다

Visual Studio라면 여기를 확인합니다.

Tools > Options > Debugging > Symbols

WinDbg라면 예를 들어 이렇게 확인합니다.

.sympath
.reload
!sym noisy

Microsoft의 공개 심볼을 사용한다면 로컬 캐시도 지정해 두면 편리합니다.

srv*C:\Symbols*https://msdl.microsoft.com/download/symbols

5. 캐시를 의심한다

오래된 PDB나 손상된 캐시를 잡고 있는 경우도 있습니다. 심볼 캐시를 삭제한다, 다른 캐시를 지정한다, 로드 로그를 자세히 본다와 같은 확인을 합니다.

30. PDB와 “Just My Code”

Visual Studio에는 Just My Code라는 설정이 있습니다.

이는 디버깅 대상을 “자신의 코드”로 한정하여 외부 코드를 단계 실행하기 어렵게 만드는 기능입니다. 평소 개발에서는 편리하지만, PDB나 Source Link 확인 시에는 혼란의 원인이 되는 경우가 있습니다.

예를 들어 외부 NuGet 라이브러리에 PDB와 Source Link가 있는데 단계 진입할 수 없는 경우, 다음 항목을 확인합니다.

Just My Code가 활성화되어 외부 코드로 취급되고 있지 않은가
Enable Source Link support가 활성화되어 있는가
NuGet.org Symbol Server가 활성화되어 있는가
대상 패키지가 PDB / .snupkg를 공개하고 있는가
소스 취득처에 접근할 수 있는가

“PDB가 나쁘다”라고 단정하기 전에 디버거 설정도 확인합니다.

31. PDB와 디컴파일

최근의 Visual Studio는 .NET 어셈블리를 디컴파일해서 디버깅에 사용할 수 있습니다.

이를 통해 PDB나 소스가 없는 외부 라이브러리라도 내부를 어느 정도 추적할 수 있는 경우가 있습니다.

다만 디컴파일은 만능이 아닙니다.

원래의 주석은 돌아오지 않는다
원래의 공백이나 구조는 돌아오지 않는다
로컬 변수명이 바뀌는 경우가 있다
async / iterator / pattern matching 등은 원래 형태와 다르게 보인다
최적화된 코드에서는 대응을 알아보기 어렵다

PDB와 Source Link가 있다면 기본적으로는 그것을 사용하는 것이 자연스럽습니다. 디컴파일은 “PDB나 소스가 없을 때의 보조 수단”이라고 생각하면 좋습니다.

32. 로그 설계와 PDB

PDB는 로그 설계와도 관련됩니다.

예를 들어 예외 로그에 파일명·줄 번호가 나오면 조사하기 쉬워집니다. 그러나 로그만 의존하는 것은 위험합니다.

운영 장애에서는 이런 일이 일어납니다.

로그에 나온 줄 번호가 지금의 main 브랜치와 맞지 않는다
hotfix 후에 같은 버전 번호로 다시 배포되었다
PDB가 남아 있지 않아 줄 번호의 의미를 검증할 수 없다
컨테이너 이미지는 남아 있지만 대응하는 소스 커밋을 알 수 없다

그러므로 로그에는 줄 번호뿐만 아니라 빌드 정보도 표시해 두면 좋습니다.

ApplicationVersion: 1.8.3
GitCommit: abcdef1234567890
BuildNumber: 20260610.12
Environment: Production

PDB, 소스, 로그, 덤프, 배포 이력이 연결되면 장애 조사는 상당히 편해집니다.

33. 컨테이너 운영에서의 PDB

컨테이너에서 .NET 애플리케이션을 실행하는 경우, PDB의 취급을 명확히 해 두어야 합니다.

예를 들어 Docker 이미지에 PDB를 포함할지 여부입니다.

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY publish/ .
ENTRYPOINT ["dotnet", "MyApp.dll"]

publish/에 PDB가 들어 있으면 그대로 이미지에도 포함됩니다.

이것에는 장점이 있습니다.

  • 컨테이너 안에서 스택 트레이스의 줄 번호가 나오기 쉽다
  • 덤프 취득 시에 같은 파일 시스템에서 찾기 쉽다
  • 조사 시의 대응이 간단하다

한편 우려도 있습니다.

  • 이미지 크기가 커진다
  • 디버그 정보가 운영 이미지에 포함된다
  • 이미지를 외부에 배포하는 경우 정보 공개 범위가 넓어진다

사내 전용 SaaS라면 PDB를 이미지에 포함하는 선택은 충분히 가능합니다. 외부 고객에게 전달하는 온프레미스 제품이라면 PDB는 별도 보관으로 하는 쪽이 좋은 경우가 있습니다.

어느 쪽이든 PDB를 이미지에 포함하지 않는 경우에도 대응하는 PDB를 artifact로 보관하는 것은 필수입니다.

34. 단일 파일 게시와 PDB

.NET에는 단일 파일 게시가 있습니다.

dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true

이 경우에도 디버그 정보의 취급을 확인해야 합니다.

단일 파일로 만들었다고 해서 장애 조사가 필요 없어지는 것은 아닙니다. 오히려 배포 형태가 특수해질수록 대응하는 심볼과 소스를 어떻게 보존할지가 중요해집니다.

방침으로 정해야 할 것은 다음과 같습니다.

PDB를 별도 파일로 배포할 것인가
DebugType=embedded로 할 것인가
심볼은 사내에만 보존할 것인가
크래시 덤프를 어떻게 분석할 것인가

단일 파일 게시, 트리밍, AOT 등을 사용하는 경우에는 일반적인 IL 어셈블리와 조사 경험이 달라지는 경우가 있습니다. 릴리스 전에 “크래시가 나면 어떻게 읽을 것인가”를 한 번 시험해 두는 것이 안전합니다.

35. PDB와 트리밍 / AOT

현재의 .NET에서는 트리밍이나 Native AOT를 사용하는 경우가 있습니다.

이 경우 PDB뿐만 아니라 생성되는 네이티브 심볼이나 플랫폼별 디버그 정보도 관련됩니다.

예를 들어 Linux라면 DWARF, macOS라면 dSYM, Windows라면 PDB와 같이 네이티브 측의 디버그 정보도 생각해야 합니다.

.NET 애플리케이션에서도 이러한 구성에서는 심볼 설계가 복잡해집니다.

Native AOT
Self-contained publish
PublishSingleFile
ReadyToRun
P/Invoke로 네이티브 DLL을 호출한다
C++/CLI를 포함한다

일반적인 웹 애플리케이션이나 클래스 라이브러리에서는 먼저 Portable PDB와 Source Link를 이해하면 충분합니다. 다만 배포 형태를 고도화할수록 “이 크래시를 어떻게 분석할 것인가”를 빌드 설계에 포함해야 합니다.

36. .NET Framework 프로젝트에서의 주의점

.NET Framework의 오래된 프로젝트에서는 SDK 스타일 프로젝트와 설정이나 기본값이 다른 경우가 있습니다.

예를 들어 이런 차이입니다.

csproj의 형식이 오래되었다
packages.config를 사용하고 있다
DebugType의 기본값이 현재의 .NET과 다르다
Windows PDB가 사용되고 있다
Source Link 설정에 추가 패키지나 MSBuild 설정이 필요하다
CI의 MSBuild 버전이 오래되었다

.NET Framework에서도 PDB에 대한 사고방식은 동일합니다.

실행에는 필수가 아니다
디버깅과 장애 조사에는 중요하다
대상 바이너리와 일치하는 PDB가 필요하다
Release 빌드의 PDB를 보관해야 한다

다만 현재 .NET의 설명을 그대로 적용하면 오래된 프로젝트에서는 예상대로 동작하지 않는 경우가 있습니다.

기존 자산에서는 먼저 실제 출력을 확인합니다.

msbuild MyApp.csproj /p:Configuration=Release
Get-ChildItem bin\Release -Filter *.pdb -Recurse

그런 다음 빌드 설정과 CI artifact를 정비합니다.

37. OSS 라이브러리에서는 어떻게 하는가

OSS .NET 라이브러리라면 기본적으로 다음 구성을 권장합니다.

Release에서도 PDB를 생성한다
Portable PDB를 사용한다
Source Link를 활성화한다
NuGet에 .snupkg를 공개한다
Repository 메타데이터를 설정한다
결정적 빌드를 의식한다

예입니다.

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <DebugType>portable</DebugType>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

SDK나 호스팅 대상에 따라 Source Link의 추가 패키지가 불필요한 경우도 있습니다. 오래된 SDK나 특수한 호스팅에서는 대응하는 Microsoft.SourceLink.* 패키지를 추가합니다.

OSS에서는 사용자가 라이브러리 안으로 단계 진입할 수 있다는 것 자체가 품질이 됩니다. “문제가 발생했을 때 읽을 수 있는 라이브러리”는 그것만으로도 신뢰받기 쉬워집니다.

38. 사내 라이브러리에서는 어떻게 하는가

사내 라이브러리에서도 Source Link와 PDB는 유효합니다.

오히려 사내 라이브러리일수록 업무 애플리케이션에서 단계 진입할 수 있으면 도움이 됩니다.

사내 NuGet 피드를 사용하고 있다면 다음 항목을 검토합니다.

사내 피드가 .snupkg에 대응하고 있는가
대응하지 않는다면 PDB를 .nupkg에 포함할 것인가
사내 심볼 서버를 마련할 것인가
Git 리포지토리에 대한 인증을 어떻게 할 것인가
퇴사자나 부서 이동 후에도 소스에 접근할 수 있는가

내부용이라고 해서 로컬 PC에만 PDB가 있는 상태는 피합니다.

CI에서 생성하고 팀이 꺼낼 수 있는 위치에 보존합니다.

39. 온프레미스 제품에서는 어떻게 하는가

고객 환경에 배포하는 온프레미스 제품에서는 PDB 취급이 어려워집니다.

PDB를 동봉하면 고객 측에서 취득한 덤프나 로그를 읽기 쉬워집니다. 그러나 내부 구조가 노출되기 쉬워집니다.

흔한 선택지는 다음과 같습니다.

PDB는 동봉하지 않지만 벤더 측에서 완전판을 보관한다
고객 지원용으로 public symbols만 제공한다
장애 시에 덤프를 회수하고 벤더 측에서 PDB를 사용해 분석한다
중요 고객용으로 한정적인 심볼 패키지를 제공한다

중요한 것은 릴리스 후에 PDB를 잃지 않는 것입니다.

온프레미스 제품에서는 몇 년 전 버전의 장애를 조사하는 경우도 있습니다. 그때 대응하는 PDB가 없으면 조사 능력이 크게 떨어집니다.

40. PDB를 지워버리면 어떻게 되는가

PDB를 지워버려도 애플리케이션은 동작합니다. 그러나 이후에 곤란해집니다.

같은 커밋에서 다시 빌드하면 재현할 수 있다고 생각할지도 모릅니다. 그러나 완전한 재현은 의외로 어렵습니다.

SDK 버전이 다르다
NuGet 해석 결과가 다르다
빌드 시각이나 환경 변수가 다르다
생성 코드가 다르다
조건부 컴파일이 다르다
CI에서만 들어가는 설정이 있다
의존하는 네이티브 도구가 다르다

결정적 빌드를 정비해 두면 재현성은 높아지지만, 그래도 “처음부터 산출물로서 보존해 둔다” 쪽이 안전합니다.

PDB는 보험에 가깝습니다. 필요해졌을 때 처음으로 가치가 나옵니다. 필요해졌을 때 없으면 되돌릴 수 없습니다.

41. 실무 권장 설정

모든 프로젝트에 해당하는 유일한 설정은 없습니다. 다만 일반적인 .NET 애플리케이션이라면 먼저 이것을 기준으로 삼을 수 있습니다.

사내 애플리케이션

<PropertyGroup>
  <DebugType>portable</DebugType>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

방침은 다음과 같습니다.

Release에서도 PDB를 생성한다
운영에 배치할지는 운영 정책으로 결정한다
CI artifact에 반드시 보존한다
덤프 분석 절차를 마련해 둔다

공개 NuGet 라이브러리

<PropertyGroup>
  <DebugType>portable</DebugType>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

방침은 다음과 같습니다.

Source Link를 활성화한다
.snupkg를 공개한다
불필요한 소스 임베드는 피한다
공개되는 메타데이터를 확인한다

소규모 내부 도구

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

방침입니다.

PDB를 빼놓고 잊어버리는 것을 피한다
배포물의 크기 증가를 허용한다
내부 이용으로 한정한다

외부 배포 제품

완전판 PDB를 사내에서 보관한다
필요하다면 public symbols를 별도로 만든다
고객 배포물에 포함할 PDB의 내용을 확인한다
지원 시의 덤프 회수와 분석 절차를 정한다

42. PDB를 볼 때의 체크리스트

마지막으로 PDB 취급에 대해 고민될 때의 체크리스트를 정리해 둡니다.

이 PDB는 어느 DLL / EXE에 대응하는가
그 DLL / EXE는 어느 커밋에서 빌드되었는가
Debug 빌드용이 아니라 운영 Release용 PDB인가
PDB는 CI artifact로서 보관되어 있는가
심볼 서버 또는 취득 절차가 있는가
Source Link는 활성화되어 있는가
소스 취득처의 권한은 적절한가
PDB에 공개하고 싶지 않은 정보가 포함되어 있지 않은가
외부 배포라면 public/private symbol의 방침이 있는가
덤프 분석 시에 PDB를 로드할 수 있음을 확인했는가

특히 중요한 것은 다음 세 가지입니다.

PDB는 실행 파일이 아니라 조사용 정보이다
PDB는 대상 바이너리와 일치해야 한다
PDB는 운영 장애가 발생하기 전에 보관해 두어야 한다

43. 정리

PDB는 .dll이나 .exe 옆에 나타나는 “잘 모르는 파일”이 아니라, 빌드된 바이너리와 개발자가 읽을 수 있는 소스 코드를 연결하기 위한 디버그 정보입니다.

애플리케이션의 통상 실행에는 필수가 아닙니다. 그러나 디버깅, 예외 조사, 덤프 분석, 프로파일링, 외부 라이브러리로의 단계 진입에서는 중요합니다.

Release 빌드에서도 PDB는 유용합니다. 오히려 운영 장애에서 필요해지는 것은 Release 빌드에 대응하는 PDB입니다.

PDB를 운영 환경에 둘지 여부는 정보 공개 범위나 운영 방침에 따라 결정합니다. 그러나 PDB를 생성하고 빌드 산출물로서 보관하는 것은 대부분의 프로젝트에서 필요합니다.

실무에서는 다음 방침을 기본으로 삼으면 좋습니다.

Release에서도 PDB를 생성한다
PDB를 CI/CD의 산출물로서 보관한다
바이너리, PDB, 커밋 ID, 빌드 번호를 연결한다
Source Link로 소스까지 추적할 수 있도록 한다
NuGet 라이브러리에서는 .snupkg를 검토한다
외부 배포에서는 공개하는 심볼 정보를 확인한다

PDB는 문제가 발생하지 않았을 때는 눈에 띄지 않습니다. 그러나 문제가 발생했을 때 조사자를 소스 코드로 되돌려주는 중요한 실마리입니다.

“PDB는 지워도 동작한다”가 아니라 다음과 같이 생각하는 것이 좋습니다.

PDB는 미래의 장애 조사에 필요한 지도이다.

참고 자료

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

PDB 파일이란 무엇인가요?
PDB는 Program Database(프로그램 데이터베이스)의 약어로, 심볼 파일이라고도 불리는 디버그 정보 파일입니다. 빌드된 .dll이나 .exe와, 개발자가 읽을 수 있는 소스 코드를 연결하는 역할을 합니다. 함수명과 메서드명, 로컬 변수명, 소스 파일명과 줄 번호, 소스상의 위치와 컴파일 후 명령의 대응, Source Link용 소스 취득 정보 등이 들어 있으며, 디버거나 진단 도구가 '이 명령이 어느 소스 줄에 대응하는지'를 판단하는 데 사용합니다.
PDB 파일이 없어도 앱은 동작하나요? 삭제해도 괜찮나요?
일반적으로 PDB는 애플리케이션 실행에 필요하지 않으며, .dll이나 .exe만 있으면 실행할 수 있습니다. 다만 PDB가 없으면 브레이크포인트나 단계 실행, 예외 스택 트레이스의 파일명·줄 번호 표시, 덤프 분석, 외부 라이브러리로의 단계 진입이 어려워집니다. PDB는 '실행하기 위한 파일'이 아니라 '조사하기 위한 파일'이므로, 운영 환경에 배치할지 여부와는 별개로 빌드 산출물로서 반드시 보관해 두어야 합니다. 지워버리면 같은 커밋에서 다시 빌드해도 완전한 재현은 의외로 어려워, 장애 조사에서 되돌릴 수 없게 되는 경우가 있습니다.
Release 빌드에서도 PDB가 필요한가요?
필요합니다. 오히려 운영 장애 조사에서 필요해지는 것은 Release 빌드의 PDB입니다. 운영 환경에서 동작하는 것이 Release 빌드라면 Debug 빌드의 PDB를 가지고 있어도 의미가 없고, 디버거가 필요로 하는 것은 그 운영 바이너리를 만들었을 때 생성된 PDB입니다. PDB는 대상 바이너리와 일치해야 하므로, CI/CD에서는 커밋 ID·빌드 번호·.dll/.exe·.pdb를 세트로 보관하는 것이 기본입니다. 참고로 현재의 .NET SDK 스타일 프로젝트에서는 Debug / Release 모두 기본값으로 Portable PDB가 생성됩니다.
PDB 파일을 운영 환경에 두어도 되나요?
단순한 예/아니오가 아니라, 장애 조사의 편의성, 정보 공개 위험, 배포 크기, 운영 규칙이라는 4가지 축으로 판단합니다. 사내 시스템이라면 .dll과 .pdb를 같은 폴더에 두면 예외 로그에 줄 번호가 나오기 쉬워져 현실적입니다. 외부 배포에서는 PDB로부터 로컬 경로, 사내 폴더 구성, 타입명이나 로컬 변수명, Source Link의 리포지토리 URL 등을 알 수 있는 경우가 있으므로 신중히 판단하고, 필요하다면 public symbols로 한정하거나 심볼 서버를 경유하는 등의 선택지를 취합니다. 배치하지 않는 경우에도 같은 빌드의 PDB 보관은 필수입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기