Windows 업무 앱의 인쇄와 PDF 출력 - System.Drawing.Printing / WPF / 보고서 라이브러리 구분 사용법
· 小村 豪 · CSharp, .NET, WinForms, WPF, 인쇄, PDF, 보고서, Windows 개발, 기술 상담
Windows 업무 앱의 인쇄는 「PrintDocument로 적당히 그리면 끝」이라는 이야기가 되지 않습니다. 페이지 나눔이 작동하지 않는다, 여백이 어긋난다, 미리보기와 실제 출력이 다르다, PDF만 원하는데 인쇄 대화상자가 끼어든다 ── 이런 문제들은 대부분 인쇄 구조 자체를 오해한 채 구현을 시작한 것이 원인입니다.
이 글에서는 WinForms의 System.Drawing.Printing, WPF의 FlowDocument/FixedDocument, PDF 출력 라이브러리, Excel/Word를 경유하는 보고서라는 4가지 선택지를, 요구사항별로 어떻게 구분해서 쓸지의 관점에서 정리합니다.
1. 먼저 결론
- 간단한 목록이나 기존 WinForms 자산의 연장이라면
PrintDocument로 충분합니다. 페이지 나눔은PrintPage이벤트를HasMorePages가false가 될 때까지 반복 호출하는 구조이며, 이를 잊으면 2페이지 이후가 출력되지 않습니다. 12 - 인쇄의 DPI는 화면의 DPI와는 별개입니다.
PageSettings.PrinterResolution은 프린터의 해상도를 나타내며, 여백은PageSettings.HardMarginX/HardMarginY(프린터가 물리적으로 가진 인쇄 불가 영역)와Margins(앱이 지정하는 논리적 여백)의 2단 구조로 되어 있습니다. 화면의 좌표를 그대로 인쇄에 사용하면, 이 두 DPI·좌표계의 차이로 레이아웃이 어긋납니다. 345 - WPF에서는 Windows 표준 인쇄 경로가 GDI 인쇄 경로와 XPS 인쇄 경로 2가지로 나뉩니다. WPF 애플리케이션은 원래 XPS 인쇄 경로를 사용하며, XPSDrv를 지원하지 않는 프린터로 보낼 경우 자동으로 GDI 형식으로 변환됩니다. 67
- WPF의 문서는 「흐름형」인
FlowDocument와 「고정 레이아웃」인FixedDocument가 설계 사상이 다릅니다.FlowDocument는 가독성을 우선하여 창 크기나 해상도에 따라 다시 배치되고,FixedDocument는 표시·인쇄 장치에 대한 충실도를 우선하는 구성입니다. 89 - 용지나 트레이 지정은 WPF의
PrintDialog와PrintTicket/PrintQueue로 수행합니다. 인쇄 전에PrintQueue.MergeAndValidatePrintTicket으로 프린터의 실제 성능에 맞춰 검증·병합하는 것이 정석입니다. 1011 - Microsoft Print to PDF는 대화형 조작을 전제로 한 인쇄 드라이버입니다. OS에 기본 탑재된 인쇄 큐와 드라이버 패키지라는 위치이며, 실행할 때마다 저장 위치를 선택하는 대화상자가 끼어드는 구성이 기본이라 무인 배치 처리에는 적합하지 않습니다. 12
- 서버나 서비스에서의 Office COM 자동화는 Microsoft가 공식적으로 비지원·비권장으로 명시하고 있습니다. Office는 대화형 데스크톱과 사용자 프로필을 전제로 설계되어 있으며, 무인·비대화형 환경에서는 불안정한 동작이나 데드락을 일으킬 수 있습니다. 13
- Windows 서비스는 세션 0에서 동작하기 때문에 대화상자를 표시할 수 없고, 사용자 세션에 연결된 기본 프린터에도 의존할 수 없습니다. 인쇄를 수반하는 상주 처리를 설계할 때는 이 경계를 가장 먼저 의식해야 합니다. 14
- 장기 가동 중 GDI 객체(핸들)를 해제하지 않고 방치하면 프로세스당 한도에 도달해 크래시가 발생합니다. 이 한도는 레지스트리의
GDIProcessHandleQuota로 조정할 수 있는 유한한 값이며, 무한히 확보할 수 있는 것이 아닙니다. 15
판단표 ── 요구사항 × 수단
| 요구사항 | 적합한 수단 | 이유 |
|---|---|---|
| 간단한 목록·전표를 몇 장 인쇄하고 싶다(WinForms) | PrintDocument |
표준 클래스만으로 완결되며, 기존 GDI+ 드로잉 자산을 그대로 활용할 수 있음 |
| 테두리가 많은 보고서, 여러 용지·복잡한 레이아웃 | 보고서 라이브러리, 또는 FixedDocument를 이용한 고정 레이아웃 |
좌표 계산·페이지 나눔 제어·일본어 조판을 직접 작성하는 비용이 큼 |
| 대량 배치 인쇄(야간 무인 실행) | 프로그램적인 PrintDocument.Print() / XpsDocumentWriter로의 직접 전송 |
대화상자를 전혀 표시하지 않고 완결해야 하며, 기본 프린터 의존도 피하고 싶음 |
| PDF 저장만 하면 된다(인쇄는 사용하지 않음) | PDF 생성 라이브러리로 직접 PDF를 만든다 | 인쇄 대화상자나 기본 프린터, 스풀러 상태에 의존하지 않음 |
| 미리보기가 필수, 사내 확인 흐름이 중요 | PrintPreviewDialog(WinForms), DocumentViewer(WPF) |
인쇄 전 확인용 표준 UI를 그대로 사용할 수 있음 |
| 기존 Excel 템플릿을 재사용하고 싶다 | Open XML SDK 등으로 직접 생성, 또는 제한적인 COM 자동화 | 자산을 재사용할 수 있지만, 상주 서비스에서의 COM 자동화는 피해야 함 |
이하에서는 각 항목의 상세 내용을 살펴봅니다.
2. Windows 인쇄 구조를 최소한만
Windows의 인쇄 아키텍처는 인쇄 스풀러와 각 프린터용 드라이버로 구성되어 있습니다. 애플리케이션은 장치에 의존하지 않는 함수만 호출하면 레이저 프린터, 벡터 플로터, 라스터 프린터, 팩스 등 다양한 출력 대상에 인쇄 작업을 보낼 수 있습니다. 16
인쇄 경로는 크게 2가지 계통으로 나뉩니다.
- GDI 인쇄 경로. Win32 GDI 애플리케이션이 인쇄하면 GDI 그래픽 엔진이 그리기 명령을 EMF(확장 메타파일)로 스풀하거나, 프린터 드라이버와 연계하여 인쇄 가능한 이미지를 직접 렌더링해 스풀러로 보냅니다. 전통적인 WinForms 앱은 이 경로를 거칩니다. 166
- XPS 인쇄 경로. XPS(XML Paper Specification) 기반 프린터 드라이버를 위한 경로로, WPF 애플리케이션은 원래 이 경로를 사용하여 XPS 문서 형식으로 스풀러에 보냅니다. 대상 프린터가 XPSDrv 드라이버가 아니면 GDI 형식으로 자동 변환된 후 전송됩니다. 67
화면의 겉모습과 인쇄 결과가 왜 어긋나는가. 가장 흔한 원인은 DPI를 혼동하는 것입니다. 화면의 Graphics는 보통 96 DPI 전후로 좌표계가 구성되지만, 프린터의 Graphics는 PageSettings.PrinterResolution으로 결정되는, 대부분 600 DPI 전후의 훨씬 높은 해상도로 동작합니다. 3 화면용으로 계산한 픽셀 좌표를 그대로 인쇄용 Graphics에 넘기면, 의도한 것보다 훨씬 작게(또는 고해상도 설정에 따라서는 크게) 그려집니다. 인쇄에서는 항상 100분의 1인치나 실제 치수(밀리미터·인치)를 기준으로 좌표를 구성하여, Graphics의 해상도에 의존하지 않는 설계로 만드는 것이 안전합니다.
또 하나 놓치기 쉬운 것이 프린터가 가진 물리적인 인쇄 불가 영역입니다. 많은 프린터는 용지 끝에서 몇 밀리미터 범위는 인쇄할 수 없습니다. 이 물리적 제약은 PageSettings.HardMarginX/HardMarginY로 확인할 수 있으며, 공식 문서에서도 「프린터가 설정하는 물리적 여백을 나타낸다」고 설명하고 있습니다. 4 앱 쪽에서 지정하는 논리적 여백은 PageSettings.Margins(기본값은 상하좌우 1인치)이지만, 이를 0으로 설정해도 프린터의 하드 마진보다 안쪽으로는 출력할 수 없습니다. 5 「여백을 0으로 했는데 끝이 잘린다/생각보다 어긋난다」는 문제의 대부분은 이 하드 마진의 존재를 의식하지 않은 것이 원인입니다.
3. WinForms: System.Drawing.Printing 실무
WinForms의 인쇄는 PrintDocument 컴포넌트를 중심으로 구성합니다. 기본 흐름은 PrintDocument를 만들고, PrinterSettings나 DefaultPageSettings를 설정하고, PrintPage 이벤트에서 실제 그리기를 수행하고, Print() 메서드로 인쇄를 시작하는 것입니다. 1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
public sealed class ReportPrinter
{
// 인쇄 대상 데이터. 1행에 1건의 전표 항목을 가정
private readonly IReadOnlyList<string> _lines;
private int _lineIndex;
public ReportPrinter(IReadOnlyList<string> lines) => _lines = lines;
public void Print(string printerName)
{
using var document = new PrintDocument();
document.DocumentName = "수주 명세";
if (!string.IsNullOrEmpty(printerName))
{
document.PrinterSettings.PrinterName = printerName;
}
// 알 수 없는 프린터 이름을 지정한 경우 여기서 false가 됨(오프라인 감지에는 사용할 수 없음)
if (!document.PrinterSettings.IsValid)
{
throw new InvalidOperationException(
$"지정한 프린터를 찾을 수 없습니다: {printerName}");
}
_lineIndex = 0;
document.PrintPage += Document_PrintPage;
document.Print();
}
private void Document_PrintPage(object? sender, PrintPageEventArgs e)
{
// MarginBounds는 논리적 여백(Margins)을 반영한 그리기 가능 영역입니다.
// PageBounds는 용지 전체이며, 하드 마진보다 안쪽에 그려도 잘림
using var font = new Font("Meiryo", 10);
float lineHeight = font.GetHeight(e.Graphics);
float y = e.MarginBounds.Top;
while (_lineIndex < _lines.Count)
{
if (y + lineHeight > e.MarginBounds.Bottom)
{
// 이 페이지는 여기까지. 이어지는 내용이 있으므로 다음 페이지로
e.HasMorePages = true;
return;
}
e.Graphics.DrawString(_lines[_lineIndex], font, Brushes.Black,
e.MarginBounds.Left, y);
y += lineHeight;
_lineIndex++;
}
// 모든 행을 다 그렸으므로 이것이 마지막 페이지
e.HasMorePages = false;
}
}
「2페이지가 나오지 않는다」는 문제의 전형적인 패턴은 HasMorePages를 명시적으로 false로 설정하는 것을 잊는 경우(기본값이 이미 false)이거나, 반대로 조건 분기를 잘못 작성해 항상 false인 채로 빠져나오는 경우입니다. PrintPage 이벤트는 이 속성이 false가 될 때까지 반복 호출되는 구조라는 점을 고려하여, 「아직 그려야 할 행이 남아 있는지」를 명시적으로 판정한 뒤 설정하십시오. 12 페이지마다 다른 설정(용지 크기나 방향 등)을 반영하고 싶다면 PrintPage와 함께 QueryPageSettings 이벤트도 활용할 수 있습니다.
「여백이 어긋난다」는 문제의 전형적인 패턴은 e.Graphics.VisibleClipBounds나 e.PageBounds를 기준으로 좌표를 구성하여 MarginBounds(논리적 여백을 반영한 영역)와 HardMarginX/HardMarginY(프린터의 물리적 제약)를 무시하는 것입니다. 4 특히 개발 머신의 프린터는 하드 마진이 작아 우연히 문제가 드러나지 않고, 납품처의 다른 기종에서는 하드 마진이 커서 테두리나 항목이 잘려 보이는 사고가 발생하기 쉽습니다. 여러 기종에서의 인쇄 검증은 개발 시 놓치기 쉬운 공정입니다.
미리보기를 제공하려면 PrintPreviewDialog를 사용하고, Document 속성에 동일한 PrintDocument를 할당하기만 하면 PrintPage 이벤트의 로직을 그대로 재사용할 수 있습니다. 1 프린터 해상도에 따라 출력을 전환하고 싶다면 PrinterSettings.PrinterResolutions에서 선택지를 가져와 PageSettings.PrinterResolution에 설정합니다. 3
4. WPF: FlowDocument / FixedDocument와 PrintDialog
WPF에서는 문서의 성격에 따라 FlowDocument와 FixedDocument 중 어느 쪽을 축으로 할지가 달라집니다.
FlowDocument. 창 크기나 폰트 설정 같은 실행 시 변수에 따라 내용을 동적으로 다시 배치하는, 가독성을 우선하는 문서입니다. 검색, 페이징, 표시 모드 전환 등의 기능을 기본으로 갖추고 있습니다. 8FixedDocument. 애플리케이션 쪽이 레이아웃을 완전히 제어하는, 충실도를 우선하는 WYSIWYG 문서입니다. 표시·인쇄 장치에 대한 고정밀 재현성이 필요한 경우에 적합합니다. 9
흐름형으로 가독성을 우선하는 사내용 보고서라면 FlowDocument, 테두리 위치나 항목 위치를 픽셀 단위로 고정하고 싶은 전표·라벨이라면 FixedDocument라는 판단이 기본 방침입니다.
기본적인 인쇄는 System.Windows.Controls.PrintDialog로 수행합니다. 이 클래스는 System.Windows.Forms.PrintDialog와는 별개이며, WPF 전용입니다. 10
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
public static class FlowDocumentPrinter
{
public static void Print(FlowDocument document, string jobName)
{
var printDialog = new PrintDialog();
// 사용자가 OK를 눌렀을 때만 인쇄
if (printDialog.ShowDialog() != true)
{
return;
}
// FlowDocument는 화면 표시용과 인쇄용의 페이지 크기 전제가 다르므로,
// 인쇄 대상의 페이지 크기·여백을 프린터의 인쇄 가능 영역에 맞춤
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
paginator.PageSize = new Size(
printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
printDialog.PrintDocument(paginator, jobName);
}
}
용지 크기·급지 트레이·양면 인쇄 등을 세밀하게 지정하고 싶다면 PrintQueue와 PrintTicket/PrintCapabilities를 사용합니다. PrintTicket은 인쇄 작업에 대한 지시, PrintCapabilities는 프린터가 실제로 지원하는 기능을 나타내며, 지정한 설정은 PrintQueue.MergeAndValidatePrintTicket으로 프린터 고유의 유효한 값으로 병합·검증한 뒤 사용하는 것이 정석입니다. 711
using System.Linq;
using System.Printing;
public static class DuplexPrintTicketBuilder
{
public static PrintTicket BuildDuplexTicket(PrintQueue printQueue)
{
PrintCapabilities capabilities = printQueue.GetPrintCapabilities();
// 양면 인쇄(장변 제본)를 지원하는 프린터인지 사전에 확인
bool supportsDuplex = capabilities.DuplexingCapability
.Contains(Duplexing.TwoSidedLongEdge);
var requestedTicket = new PrintTicket();
if (supportsDuplex)
{
requestedTicket.Duplexing = Duplexing.TwoSidedLongEdge;
}
// 사용자의 기본 PrintTicket에 양면 인쇄 요청만 병합.
// 지원하지 않는 항목은 유효한 값으로 자동 대체됨
ValidationResult result = printQueue.MergeAndValidatePrintTicket(
printQueue.UserPrintTicket, requestedTicket);
return result.ValidatedPrintTicket;
}
}
고정 레이아웃 보고서를 FixedDocument로 구성하여 XPS 인쇄 경로로 직접 전송하는 경우에는 XpsDocumentWriter의 Write/WriteAsync 메서드로 PrintQueue에 작업을 추가합니다. 대상이 XPSDrv를 지원하지 않는 프린터라면 내부에서 GDI 형식으로 자동 변환된 뒤 전송되므로, 호출하는 쪽에서 경로를 의식할 필요는 없습니다. 7
5. PDF 출력의 선택지
「PDF로 출력하고 싶다」는 요구사항은 실제로는 다음 3가지 중 어느 것을 가리키는지에 따라 설계가 달라집니다.
- 인쇄의 연장으로서 사용자가 수동으로 PDF 저장을 선택할 수 있으면 됨
- 앱 내부에서 인쇄 대화상자를 거쳐 PDF를 생성하고 싶음
- 인쇄는 전혀 하지 않고, 무인 배치로 PDF 파일만 대량 생성하고 싶음
Microsoft Print to PDF는 1과 2를 위한 기능입니다. Windows 10 이후에 기본 탑재된, 인쇄 큐와 드라이버 패키지라는 위치의 기능으로, 인쇄 대상 앱에서는 보통의 프린터 중 하나로 보입니다. 12 다만 실체는 「인쇄할 때마다 저장 위치 파일명을 묻는」 대화형 동작을 전제로 한 구조이며, 공식적으로도 관리자가 해당 인쇄 큐와 드라이버 패키지 자체를 이미지에서 제거할 수 있다는 단위로만 제어 수단이 제공됩니다. 12 무인·대량 배치 처리에서 파일 경로를 고정하고 조용히 PDF를 출력하는 3번 용도에는 설계상 적합하지 않습니다. 3번을 실현하고 싶다면 인쇄 파이프라인을 거치지 않고 PDF를 직접 생성하는 라이브러리를 쓰는 것이 자연스럽습니다.
PDF 생성 라이브러리를 선택할 때의 비교 관점은, 특정 제품을 추천하기 전에 다음 축으로 자사의 요구사항을 정리해 두면 선정이 흔들리지 않습니다.
| 관점 | 확인할 것 |
|---|---|
| 라이선스 | OSS(MIT/Apache 계열인지, GPL/AGPL 계열인지)인지 상용 라이선스인지. 자사 앱을 외부에 배포·판매하는 경우, 라이브러리의 라이선스 조건(소스코드 공개 의무 유무, 재배포 조건, 상업적 이용 가능 여부)이 자사의 배포 형태와 상충하지 않는지 반드시 1차 정보(라이선스 조문·제공사의 공식 사이트)로 확인 |
| 일본어 폰트 처리 | 일본어 폰트의 서브셋 임베딩을 지원하는지, 임베딩 없이 PDF 쪽에 폰트 실체를 담을 수 있는지(열람 환경에 폰트가 없을 때 글자가 깨지지 않는지), 세로쓰기나 이형자(IVS)가 필요한 업무라면 그 대응 현황 |
| 보고서 관련 기능 | 템플릿 기반 보고서 정의를 지원하는지, 바코드·2차원 코드 생성, 전자 서명, PDF/A(장기 보존 규격) 대응, 기존 종이 보고서의 레이아웃을 그대로 이식하기 쉬운지 |
| 종속성·실행 환경 | 서버 환경(GUI 없는 Windows Server, 컨테이너)에서도 동작하는지, 네이티브 라이브러리에 대한 종속성이 있는지, .NET Core/.NET(비 Framework) 환경에서의 동작 실적 |
| 성능 | 대량 페이지·대량 작업의 배치 생성 시 메모리 사용량, 병렬 생성 시 동작 |
라이선스 조건은 제품이나 버전에 따라 달라지므로, 반드시 제공사의 공식 문서나 라이선스 조문 자체를 확인한 뒤 도입 여부를 판단하십시오. 이 글에서는 특정 라이브러리명이나 제품 추천은 하지 않으며, 비교를 위한 「관점」만 제시합니다.
6. Excel / Word를 경유하는 보고서라는 선택지
이미 Excel 템플릿으로 운영 중인 보고서가 있다면, 이를 처음부터 다시 만드는 것보다 기존 자산을 활용하는 구성이 더 현실적인 경우가 있습니다. 방식은 크게 2가지입니다.
- Open XML SDK 등으로 xlsx/docx를 직접 생성·편집한다. Excel/Word의 실행 파일을 실행하지 않고 파일 형식(ZIP + XML)을 직접 읽고 쓰기 때문에 서버 환경에서도 안정적으로 동작합니다. 템플릿의 테두리·서식을 그대로 살리면서 값만 채워 넣는 구성과 잘 맞으며, 자세한 내용은 「Excel 보고서 출력 만드는 법 - COM/Open XML/템플릿」에 정리되어 있습니다.
- Excel COM Interop으로 실제
Excel.Application을 조작한다. 매크로나 복잡한 재계산을 수반하는 기존 Excel 워크북을 그대로 재사용하고 싶을 때 선택되지만, COM 참조 해제 누락으로 EXCEL.EXE 프로세스가 계속 남는 문제가 발생하기 쉬우며, 대책과 판단 기준은 「C#의 Excel 조작에서 EXCEL.EXE가 남는 문제」에서 다루고 있습니다.
여기서 중요한 것은, 서버 사이드나 Windows 서비스에서의 Office COM 자동화는 Microsoft가 공식적으로 「비권장·비지원」이라고 명시하고 있다는 점입니다. Office 애플리케이션은 대화형 데스크톱과 로그온된 사용자 프로필을 전제로 설계되어 있으며, 무인·비대화형 환경에서 실행하면 불안정한 동작이나 데드락을 일으킬 수 있다고 되어 있습니다. 13 라이선스상으로도 서버 쪽 자동화로 Office 기능을 클라이언트에 제공하는 사용 방식은 일반적인 EULA에서 상정하고 있지 않습니다. 13
따라서 보고서 출력을 상주 서비스나 배치 작업에 포함시키고 싶다면, 기존 Excel 템플릿을 재사용하더라도 실행 시 실제로 Excel.exe를 실행하는 구성(COM Interop)은 피하고, Open XML SDK 등을 이용한 파일 직접 생성 쪽으로 방향을 잡는 것이 안전합니다. COM Interop이 반드시 필요하다면, 대화형으로 로그온한 사용자 세션에서 동작하는 상주 애플리케이션(서비스가 아님)으로 구성하는 것이 현실적인 구분입니다.
7. 실무의 함정
Windows 서비스·세션 0에서의 인쇄
인쇄 처리를 상주 서비스에 포함시키고 싶다는 상담은 적지 않지만, Windows 서비스는 세션 0에서 동작합니다. 세션 0은 Windows Vista 이후 서비스나 대화형 사용자와 관계없는 프로세스 전용으로 예약된 세션으로, 사용자의 대화형 세션과는 분리되어 있습니다. 14 이 분리로 인해 서비스는 대화상자를 표시해도 사용자에게 보이지 않으며, 사용자의 입력을 기다리는 코드(인쇄 대화상자 표시 대기 등)를 작성하면 멈춘 것처럼 보입니다. 14
여기에 더해 실무상 함정이 되기 쉬운 것이 네트워크 프린터나 사용자별로 매핑된 프린터가 보이는 방식입니다. 사용자가 로그온하여 연결한 네트워크 프린터는 그 사용자의 대화형 세션에 연결되어 보이는 경우가 많으며, 다른 세션(세션 0에서 동작하는 서비스의 프로세스 컨텍스트)에서는 동일하게 보이지 않습니다. 인쇄를 수반하는 상주 처리는 「어떤 계정·어떤 세션에서 어떤 프린터가 보이는지」를 설계 단계에서 가장 먼저 확인해야 합니다. 세션 분리의 전체적인 그림은 「Windows의 세션 분리를 어떻게 이해할까」에, 서비스로서의 구현·운용 기초는 「Windows 서비스 만드는 법과 운용」에 정리되어 있습니다.
프린터 부재·오프라인 시의 동작
PrinterSettings.PrinterName에 존재하지 않는 프린터 이름을 지정하거나, 기본 프린터가 설정되지 않은 환경에서 실행하면 인쇄를 시도하는 순간 예외나 실패가 발생합니다. PrinterSettings.IsValid로 사전에 검증한 뒤 진행하는 것, 인쇄 실패를 가정한 재시도·오류 알림 설계를 해두는 것, 이 두 가지는 최소한 챙겨두어야 합니다. 네트워크 프린터가 일시적으로 오프라인인 경우 스풀러에 작업이 쌓여도 실제 출력은 멈춘 상태로 남을 수 있으므로, 인쇄 결과를 「전송에 성공했다」로 끝내지 말고 필요에 따라 작업 상태를 확인하는 구조도 검토하십시오.
기본 프린터 의존의 위험
PrinterSettings.PrinterName을 명시적으로 지정하지 않고 항상 「기본 프린터」에 의존하는 구현은 운용 기간이 길어질수록 위험이 커집니다. 사용자가 다른 프린터를 기본으로 변경하거나, 노트북을 다른 장소로 가져가 기본 프린터가 바뀌는 등의 환경 변화로 의도하지 않은 출력 대상으로 전송되기 때문입니다. 업무상 중요한 보고서는 설정 파일이나 앱 내 설정 화면에서 인쇄 대상 프린터 이름을 명시적으로 유지하여, 기본 프린터의 변화에 영향받지 않는 설계로 만들어야 합니다.
장기 가동 시의 GDI 핸들 누수
Graphics, Font, Brush, Pen 등의 GDI/GDI+ 리소스를 using이나 Dispose()로 확실히 해제하지 않는 코드를 인쇄 처리 안에 작성하면, 장기 가동 후 GDI 객체 핸들이 고갈되어 그리기 관련 API 호출이 실패하기 시작합니다. GDI 객체 핸들은 프로세스마다 기본 상한이 있으며, 레지스트리의 GDIProcessHandleQuota로 256에서 65,536 범위에서 조정할 수 있는 유한한 리소스입니다. 15 인쇄를 빈번하게 수행하는 상주 애플리케이션일수록 이런 종류의 누수는 운영 환경에서만 드러나는 문제가 되기 쉽습니다. 핸들 누수를 조사하고 원인을 분리하는 방법은 산업용 카메라의 장기 가동 크래시 사례를 다룬 「산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편」에서 구체적으로 설명하고 있습니다.
관련 글
- Excel 보고서 출력 만드는 법 - COM/Open XML/템플릿
- C#의 Excel 조작에서 EXCEL.EXE가 남는 문제 ── COM 참조 해제 패턴과 대체 판단
- Windows 서비스 만드는 법과 운용 ── 태스크 스케줄러와의 구분 사용부터 BackgroundService의 서비스화까지
- Windows의 세션 분리를 어떻게 이해할까 ── Session 0·RDP·다중 사용자 동시 실행
- 산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편
관련 상담 분야
합동회사 코무라소프트는 Windows 업무 앱의 인쇄·보고서 출력 기능 설계, 기존 Excel 자산을 활용한 보고서 재설계, 상주 서비스에서의 인쇄·출력 설계에 관한 기술 상담을 다루고 있습니다.
참고 자료
-
Microsoft Learn, PrintDocument Class.
PrintDocument의 역할(DocumentName·PrinterSettings를 설정하고Print()로 인쇄를 시작하는 것)과, WPF에서 인쇄할 경우System.Printing네임스페이스를 사용해야 한다는 안내에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, PrintPageEventArgs.HasMorePages Property. 기본값이
false라는 점,PrintPage이벤트는 이 속성이false가 될 때까지 반복 발생한다는 점에 대해. ↩ ↩2 -
Microsoft Learn, PageSettings.PrinterResolution Property. 페이지의 인쇄 해상도 기본값이 프린터의 기본 해상도라는 점,
PrinterSettings.PrinterResolutions에서 선택 가능한 해상도 목록을 가져올 수 있다는 점에 대해. ↩ ↩2 ↩3 -
Microsoft Learn, PageSettings.HardMarginX Property. 하드 마진이 프린터에 의해 설정되는 물리적 여백을 나타낸다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, PageSettings.Margins Property. 페이지 여백의 기본값이 상하좌우 1인치라는 점,
PrintPage이벤트에서Bounds속성과 조합하여 인쇄 가능 영역을 계산할 수 있다는 점에 대해. ↩ ↩2 -
Microsoft Learn, Windows Print Path Overview. Windows에 GDI 인쇄 경로(Win32 애플리케이션 기반)와 XPS 인쇄 경로(WPF 애플리케이션 또는 XPS Print API 기반)라는 2가지 주요 인쇄 경로가 존재한다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Printing documents overview. WPF 애플리케이션이 XPS 인쇄 경로를 사용한다는 점,
PrintDialog를 이용한 기본 인쇄,PrintTicket/PrintCapabilities/PrintQueue/XpsDocumentWriter를 이용한 고급 인쇄, XPSDrv를 지원하지 않는 프린터로의 GDI 자동 변환에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Flow Document Overview.
FlowDocument가 창 크기나 해상도 같은 실행 시 변수에 따라 콘텐츠를 다시 배치하는, 가독성을 우선하는 문서라는 점에 대해. ↩ ↩2 -
Microsoft Learn, FixedDocument Class.
FixedDocument가 표시·인쇄 장치에 대한 충실한 재현을 우선하는 WYSIWYG 설계이며,FlowDocument와는 설계 사상이 다르다는 점에 대해. ↩ ↩2 -
Microsoft Learn, PrintDialog Class.
System.Windows.Controls.PrintDialog가 사용자 입력에 따라PrintTicket과PrintQueue를 구성하는 대화상자라는 점,System.Windows.Forms.PrintDialog와는 별개의 클래스라는 점에 대해. ↩ ↩2 -
Microsoft Learn, How to: Validate and Merge PrintTickets.
PrintQueue.GetPrintCapabilities로 프린터의 지원 기능을 확인하고,MergeAndValidatePrintTicket으로 요청 내용을 프린터 고유의 유효한PrintTicket으로 병합·검증하는 절차에 대해. ↩ ↩2 -
Microsoft Learn, RemoveMPDW. Microsoft Print to PDF가 기본적으로 설치되는 옵션 기능이며, 인쇄 큐와 드라이버 패키지 단위로 구성·제거된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Support, Considerations for server-side Automation of Office. Microsoft가 ASP/ASP.NET/DCOM/NT 서비스를 포함한 무인·비대화형 클라이언트 애플리케이션에서의 Office 자동화를 권장·지원하지 않는다는 점, Office가 대화형 데스크톱과 사용자 프로필을 전제로 설계되어 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Service Changes for Windows Vista - Session 0 Isolation. Windows Vista 이후 세션 0이 서비스나 대화형 사용자와 관계없는 애플리케이션 전용으로 예약되어, 서비스가 대화상자를 직접 표시할 수 없게 되었다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, GDI Objects. GDI 객체 핸들이 프로세스마다 기본 상한을 가진다는 점,
GDIProcessHandleQuota레지스트리 값으로 256에서 65,536 범위에서 상한을 조정할 수 있다는 점에 대해. ↩ ↩2 -
Microsoft Learn, Introduction to printing. Windows의 인쇄 아키텍처가 스풀러와 프린터 드라이버로 구성된다는 점, Win32 GDI 애플리케이션의 그리기 명령이 EMF로 스풀되는 구조에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
PerfView와 dotnet-trace로 “느림”의 원인을 찾는다 ── .NET 성능 조사 실무 입문
업무 앱이 “느리다”, “CPU를 다 써버린다”, “가끔 멈춘다”고 할 때, 어떤 도구로 무엇을 봐야 할까요. PerfView와 dotnet-trace의 역할 분담, CPU 샘플링을 읽는 방법(inclusive/exclusive), ThreadT...
appsettings.json만으로는 부족하다 ── Windows 업무 앱 구성 관리 실무(환경별 설정・비밀 정보・쓰기 위치)
Windows 업무 앱의 구성 관리를 appsettings.json의 계층화, IConfiguration과 IOptions/IOptionsSnapshot/IOptionsMonitor의 구분 사용, 환경별 설정, 쓰기 가능한 설정의 배치 위치, 비...
WinForms/WPF 앱에 Entra ID 인증 넣기 ── MSAL.NET과 WAM 브로커 실무 구성
WinForms/WPF 데스크톱 앱에 Entra ID(구 Azure AD) 인증을 도입하는 절차를 실무 관점에서 정리합니다. 퍼블릭 클라이언트의 개념, ROPC 폐지 현황, 앱 등록, MSAL.NET의 AcquireTokenSilent 패턴, W...
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리해야 할 것
Windows 앱의 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 개수, 장치 연계, COM/ActiveX, 배포·업데이트, 보수와 관련해 정리해야 할 포인트를 설명합니다.
Generic Host / BackgroundService를 데스크톱 앱에 가지고 들어오는 이유 - 기동・수명・graceful shutdown의 정리가 꽤 편해진다
WPF나 WinForms 같은 데스크톱 앱에서 Generic Host와 BackgroundService를 도입해 기동, 상주 처리, graceful shutdown, DI, 로그, 설정의 입구를 한 곳으로 모으는 설계 정리법과 안티패턴을 실무 시...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- PrintDocument와 WPF의 인쇄, 어느 쪽으로 만들어야 하나요?
- 화면이 WinForms라면 PrintDocument, WPF라면 FlowDocument/FixedDocument를 쓰는 것이 자연스럽습니다. 기술을 혼재시킬 필요는 없으며, 기존 앱의 화면 프레임워크에 맞추는 것이 실무적인 판단 기준입니다. 새로 WPF를 선택한다면, 보고서가 흐름 중심인지 레이아웃 고정인지에 따라 FlowDocument와 FixedDocument 중 어느 쪽을 축으로 할지 먼저 정해두면 헤매지 않습니다.
- 보고서 라이브러리는 구매해야 하나요?
- 테두리가 많은 복잡한 보고서, 여러 용지 크기 대응, 일본어 폰트 임베딩이나 세로쓰기 등을 자체 GDI+/WPF 드로잉으로 직접 구현하는 비용이 큰 경우에는, 보고서 라이브러리 도입 비용이 더 저렴한 경우가 많습니다. 반대로 A4 목록표 정도의 단순한 보고서라면 PrintDocument나 FixedDocument의 자체 구현으로도 충분한 경우가 많으므로, 먼저 요구사항의 복잡도를 가늠해 본 뒤 판단하는 것이 안전합니다.
- PDF만 만들고 인쇄는 쓰지 않는 구성도 가능한가요?
- 충분히 가능합니다. PDF는 어디까지나 출력 형식의 하나이며, 인쇄 대화상자나 기본 프린터에 의존하지 않는 PDF 라이브러리로 직접 생성하는 방식이 배치 처리나 자동화와 더 잘 맞는 경우가 많습니다. Microsoft Print to PDF는 대화형 조작을 전제로 한 드라이버이며, 무인 PDF 생성에는 적합하지 않습니다.
- 라벨 프린터나 영수증 프린터에도 대응할 수 있나요?
- 대응은 가능하지만, 범용적인 PrintDocument/FixedDocument의 연장으로는 대응하기 어려운 경우가 있습니다. 많은 라벨 프린터가 독자적인 명령어 언어나 SDK를 가지고 있으며, Windows의 표준 인쇄 경로와는 별도의 경로로 제어하는 경우도 많기 때문입니다. 먼저 대상 기종이 Windows의 일반적인 프린터 드라이버로 동작하는지, 전용 SDK를 통한 제어가 필요한지를 확인하는 데서 시작하십시오.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크