수정 이력(5건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 기사 맨 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참고합니다.
- 100줄을 넘던 코드 예제를, 먼저 골격(호출하는 API는 4개)을 보인 뒤 완전판을 두는 구성으로 나눴습니다. 아울러 RmGetList가 반환하는 정보의 형태와 앱 종류 목록, 서비스에서 사용자 세션을 넘는 세 가지 방법, 처리 흐름 그림을 추가했습니다.
- 참고 링크 등에서 세로줄(파이프) 기호가 들어 있는 행이 표로 렌더링되어 링크를 누를 수 없던 표시 오류를 고쳤습니다. 본문 내용은 바꾸지 않았습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22174933)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「사용 중인 exe/DLL을 어떻게 교체할까 ── Restart Manager와 자동 업데이트의 「파일 사용 중」 문제」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/restart-manager-file-in-use-update-guide/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22174933
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22174934
「앱을 업데이트하니 전원 × 버튼으로 닫아 주세요, 라고 업데이트할 때마다 사내 방송을 한다」「밤에 파일 서버의 exe를 교체하는 배치를 넣어 두었는데, 아침에 보니 『프로세스가 파일에 액세스할 수 없습니다』로 실패해 있었다」── 업무 앱의 배포·업데이트 상담에서 이 「파일 사용 중」 문제는 정말 자주 나옵니다. 인스톨러가 마지막에 내놓는 「재부팅이 필요합니다」도 근본은 같고, 누군가가 붙잡고 있는 파일을 교체하지 못하는 것이 모든 원인입니다.
성가신 점은 이 문제가 「가끔만 일어난다」는 것입니다. 개발 PC에서는 자신이 앱을 닫고 나서 업데이트하므로 재현되지 않고, 운영 공유 환경에서는 「한 사람이 앱을 켜 둔 채 퇴근했다」는 것만으로 야간 업데이트가 전부 실패합니다. 그리고 누가 붙잡고 있는지는 오류 메시지만으로는 알 수 없습니다.
사실 Windows에는 이 문제를 위한 OS 표준 장치가 있습니다. 「누가 붙잡고 있는지」를 열거하고, 절차에 맞게 종료시킨 뒤, 업데이트 후 재시작까지 해 주는 Restart Manager API입니다. 나아가 「실행 중인 exe는 이름을 바꿀 수 있다」는 특성을 쓰면, 프로세스를 멈추지 않고 다음 실행부터 새 버전으로 바꾸는 업데이트도 설계할 수 있습니다. 이 기사에서는 업무 앱의 자동 업데이트·인스톨러에서 「파일이 사용 중입니다」로 고민하는 개발자를 대상으로, 잠금 구조부터 Restart Manager 사용법, 앱 쪽 규칙, 자동 업데이트 구현 패턴까지를 공식 문서의 근거와 C# 진단 코드와 함께 정리합니다.
1. 먼저 결론
- 실행 중인 exe·로드된 DLL은 「덮어쓰기」와 「삭제」는 할 수 없지만, 같은 볼륨 안의 「이름 변경(이동)」은 가능합니다. 기존 파일을 대피용 이름으로 바꾼 뒤 새 파일을 두는 rename-then-replace가 자체 업데이터의 기본형입니다.
- Restart Manager는 Windows Vista 이후의 OS 표준 API이며, 「사용 중 파일 문제」를 위해 존재합니다. 업데이트 대상 파일을 등록하면 점유 중인 앱·서비스를 열거하고, 종료와 재시작까지 맡깁니다. 목적은 OS 재부팅의 감소·제거입니다.1
- API 흐름은 RmStartSession → RmRegisterResources → RmGetList → RmShutdown → (업데이트) → RmRestart → RmEndSession입니다. 정지는 GUI 앱 → 콘솔 앱 → 서비스 → Explorer 순이고, 재시작은 역순입니다.12
- RmGetList만으로도 「누가 붙잡고 있는지」를 보여 주는 진단 도구를 만들 수 있습니다. C#에서 P/Invoke로 수십 줄입니다(후술 코드 참조).3
- MSI(Windows Installer 4.0 이후)는 Restart Manager를 자동으로 사용합니다. 패키지에 MsiRMFilesInUse 대화 상자를 넣어 두면, 사용자에게 「앱을 자동으로 닫고 재시작한다」는 선택지를 제시할 수 있습니다.4
- 앱 쪽에도 지켜야 할 규칙이 있습니다. RegisterApplicationRestart로 재시작 명령줄을 등록하고, WM_QUERYENDSESSION(lParam=ENDSESSION_CLOSEAPP)에 응답하고, WM_ENDSESSION에서 미저장 데이터를 대피시킨 뒤 종료합니다. 이 세 가지를 구현한 앱은 「업데이트를 위해 닫혀도, 업데이트 후 원래대로 다시 뜹니다」.56
- 재시작 루프를 막기 위해, 시작 후 60초가 지나지 않은 앱은 재시작되지 않습니다. 또한 Restart Manager의 강제 종료 타임아웃은 앱 30초·서비스 20초입니다.62
- 도저히 교체하지 못할 때의 최후 수단이 MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT(OS 재부팅 시 치환 예약)입니다. 관리자 권한이 필요하고, 예약은 레지스트리의 PendingFileRenameOperations에 기록됩니다.7
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 22건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 사용 중인 파일은 왜 교체하지 못하는가 ── 그리고 「이름 변경은 통한다」는 우회
Windows가 exe를 실행하거나 DLL을 로드할 때, OS는 그 파일을 메모리 맵 파일(이미지 섹션)로 매핑합니다. 페이징 구조상 실행 중에도 파일 실체가 계속 참조되므로, 내용의 덮어쓰기와 삭제는 차단됩니다. Explorer에서 복사하려 하면 「다른 프로세스가 사용 중입니다」, File.Copy면 IOException(공유 위반), File.Delete면 UnauthorizedAccessException이 돌아오는, 그 동작입니다.
여기서 중요한 것이 Windows의 잘 알려지지 않은 특성입니다. 파일의 「내용」은 잠겨 있어도, 디렉터리상의 「이름」은 바꿀 수 있습니다. 실행 중인 exe라도 같은 볼륨 안이면 이름 변경(이동)은 성공합니다. 실행 이미지가 붙잡고 있는 것은 파일 실체이지 경로 이름이 아니기 때문입니다.
이 비대칭성에서 자체 업데이트의 기본형인 rename-then-replace 패턴이 나옵니다.
1. MyApp.exe(실행 중)를 MyApp.exe.old로 이름 변경한다 ← 실행 중이라도 성공한다
2. 새 MyApp.exe를 원래 경로에 배치한다 ← 이름이 비었으므로 성공한다
3. 다음 실행부터 새 MyApp.exe가 사용된다
4. 기존 프로세스가 끝난 뒤, 어느 시점에 .old를 삭제한다
(실행 중에는 삭제할 수 없으므로, 다음 업데이트 시나 시작 시 정리 처리로 지운다)
프로세스를 멈추지 않고 「다음 실행부터 새 버전」을 만들 수 있는 것이 이 패턴의 가치이며, Chrome 업데이터나 Squirrel/Velopack 같은 업데이트 프레임워크도 본질적으로는 이 특성 위에 성립합니다. 주의점은 세 가지입니다. 이름 변경은 같은 볼륨 안에서만 쓸 수 있다는 것(다른 볼륨으로의 이동은 복사+삭제가 되어 삭제에서 실패합니다), .old 파일 정리를 설계에 넣어야 한다는 것, 그리고 「지금 돌아가는 기존 프로세스」는 어디까지나 기존 버전 그대로이므로 이전 버전과 새 버전 프로세스가 섞이는 시간대가 있다는 것입니다.
한편 「애초에 누가 붙잡고 있는지」를 장애 조사로 손으로 찾고 싶다면, Process Explorer나 Handle 명령으로 핸들·로드된 DLL을 검색하는 것이 빠릅니다. 절차는 같은 날 공개한 자매 기사 「Process Explorer / Handle / VMMap으로 hang과 leak을 추적하기」에 정리했습니다. 이 기사에서는 이를 프로그램에서, 즉 업데이터 자체에 넣는 방법으로 Restart Manager를 사용합니다.
3. Restart Manager의 구조 ── RmStartSession부터 RmRestart까지
Restart Manager는 Windows Vista / Windows Server 2008 이후에 표준 탑재된 API 그룹(rstrtmgr.dll)이며, 목적은 문서 맨 앞에 분명하게 적혀 있습니다. 「설치나 업데이트가 시스템 재부팅을 요구하는 주된 이유는 업데이트 대상 파일이 실행 중인 애플리케이션이나 서비스에 사용되고 있기 때문이며, Restart Manager는 중요한 것을 제외한 앱·서비스를 종료·재시작시켜 이 재부팅을 줄이거나 없앤다」1. MSI 패키지 설치 중에 「다음 애플리케이션이 사용 중인 파일을…」이라며 프로세스 목록 대화 상자가 뜨는 것을 본 적이 있을 텐데, Visual Studio나 Office 같은 대형 제품의 인스톨러·업데이트도 이 장치를 사용합니다.
API 흐름은 일직선입니다.
| 단계 | 함수 | 하는 일 |
|---|---|---|
| 1 | RmStartSession | 세션을 시작하고, 핸들과 세션 키(GUID 문자열)를 얻는다 |
| 2 | RmRegisterResources | 업데이트할 파일 경로(프로세스·서비스 이름도 가능)를 등록한다 |
| 3 | RmGetList | 등록 리소스를 사용 중인 앱·서비스를 열거한다3 |
| 4 | RmShutdown | 그것들을 종료시킨다(보통은 무난하게, 지정하면 강제)2 |
| 5 | ── | 이 사이에 파일을 교체한다 |
| 6 | RmRestart | 재시작 등록된 앱을 재시작한다 |
| 7 | RmEndSession | 세션을 닫는다 |
「언제 파일을 교체하는가」가 가장 틀리기 쉬운 점이므로, 시간축으로 늘어 둡니다. 교체해도 되는 것은 RmShutdown이 반환한 뒤, RmRestart를 호출하기 전의 짧은 구간뿐입니다.
sequenceDiagram
participant U as 업데이터
participant RM as Restart Manager<br/>(rstrtmgr.dll)
participant A as 사용 중인 앱<br/>(GUI/서비스)
U->>RM: ① RmStartSession
RM-->>U: 세션 핸들 + 세션 키
U->>RM: ② RmRegisterResources(업데이트 대상 파일)
U->>RM: ③ RmGetList
RM-->>U: 사용 중인 프로세스 목록 + RebootReasons
Note over U: 여기서 「누가 붙잡고 있는지」가 확정된다<br/>RebootReasons≠0 이면 OS 재부팅이 필요
U->>RM: ④ RmShutdown
RM->>A: WM_QUERYENDSESSION / WM_ENDSESSION<br/>(콘솔에는 CTRL_C_EVENT)
A-->>RM: 저장하고 종료
RM-->>U: 종료 완료
Note over U,A: ⑤ 여기서 파일을 교체한다<br/>← 잠금이 풀려 있는 것은 이 구간뿐
U->>RM: ⑥ RmRestart
RM->>A: 등록된 앱을 역순으로 재시작
U->>RM: ⑦ RmEndSession
알아 둘 사양이 몇 가지 있습니다.
- 정지 순서와 재시작 순서. GUI 앱 → 콘솔 앱 → 서비스 → Explorer 순으로 정지하고, 업데이트 후에는 등록된 앱을 역순으로 재시작합니다.1
- 종료 방식은 「절차에 맞는 순서」입니다. GUI 앱에는 WM_QUERYENDSESSION/WM_ENDSESSION(lParam=ENDSESSION_CLOSEAPP)을 보내고, 응하지 않는 앱에는 WM_CLOSE도 보냅니다. 콘솔 앱에는 CTRL_C_EVENT, 서비스는 SCM을 통해 정지됩니다. RmForceShutdown을 지정한 경우에도 먼저 무난한 종료를 시도한 뒤, 응답하지 않는 앱을 30초(서비스는 20초)에 강제 종료합니다.52
- 재시작할 수 있는 것은 RegisterApplicationRestart로 등록된 앱뿐입니다. RmShutdown에 RmShutdownOnlyRegistered를 지정하면 「전원이 재시작 등록된 때만 종료한다」는 안전한 쪽으로 움직일 수 있습니다.25
- 세션을 넘어 종료할 수는 없습니다. LocalSystem 서비스로 돌아가는 인스톨러는 사용자 세션에서 돌아가는 앱을 종료·재시작하지 못합니다. 야간 무인 업데이트를 설계할 때 가장 걸리는 지점이 여기입니다(구체 대책은 다음 항).2
- 중요한 시스템 서비스와 critical 프로세스는 대상 밖입니다. 이 경우에는 OS 재부팅이 필요하다는 판정(RM_REBOOT_REASON)이 돌아옵니다.13
3.1 세션 경계를 어떻게 넘을 것인가
「밤에 서비스에서 업데이트를 돌린다」는 설계에서 반드시 부딪히는 제약이 이것입니다. LocalSystem(세션 0)에서는 로그온 중인 사용자 세션의 앱을 종료·재시작하지 못합니다.2 우회는 「세션 0에서 손을 뻗는」 것이 아니라, 대상 세션 안에서 실행하게 하는 것으로 귀결됩니다. 현실적인 선택지는 세 가지입니다.
| 방법 | 무엇을 하는가 | 맞음·안 맞음 |
|---|---|---|
| (A) 작업 스케줄러로 대상 사용자로서 시작 | 업데이트 에이전트 작업을 대상 사용자 계정으로 「사용자가 로그온해 있을 때만 실행」으로 등록하고, 업데이트 신호로 시작한다 | 가장 손쉽다. 로그온 중인 사용자를 특정할 수 있는 사내 PC에 맞다 |
| (B) 로그온 시 상주 에이전트를 시작 | 각 사용자 세션에서 작은 상주 프로세스를 돌리고, 공유 폴더나 이벤트로 업데이트 신호를 기다린다 | 대상 사용자가 정해져 있지 않거나 동시 다중 로그온이어도 통한다. 상주 프로세스 유지 비용이 든다 |
| (C) 서비스에서 사용자 세션에 프로세스를 띄운다 | LocalSystem 서비스가 대상 세션의 토큰을 얻어, 그 토큰으로 업데이터를 시작한다 | 자유도는 가장 높지만, 구현과 권한 관리가 가장 무겁다 |
(A)는 schtasks로 이렇게 등록합니다. /RU에 대상 사용자, /IT를 붙여 「그 사용자가 로그온해 있을 때만 대화형으로 실행」하는 작업으로 만드는 것이 핵심입니다.
:: 업데이트 에이전트를 「대상 사용자의 세션 안에서」 돌리는 작업을 등록한다
:: 자격 증명 지정(/RP)이나 기존 작업 덮어쓰기(/F)는 환경 정책에 맞춰 조정한다
schtasks /create /TN "MyApp Update Agent" /TR "C:\App\Updater.exe /shutdown-and-update" ^
/SC ONCE /ST 02:00 /RU CORP\taro /IT
:: 야간 배치 쪽에서는 시각을 기다리지 않고 즉시 실행시킬 수도 있다
schtasks /run /TN "MyApp Update Agent"
(C)를 고를 때는 WTSGetActiveConsoleSessionId나 WTSEnumerateSessions로 대상 세션 ID를 구하고, WTSQueryUserToken으로 그 사용자의 primary 토큰을 꺼내, CreateProcessAsUser로 업데이터를 시작합니다. WTSQueryUserToken 호출에는 LocalSystem 계정으로 돌아가고 있는 것과 SE_TCB_NAME 권한이 필요하며, 공식 문서도 「고도로 신뢰된 서비스용」「토큰을 흘리지 않도록 주의하고, 다 쓰면 반드시 핸들을 닫을 것」이라고 명시합니다.8 다루기를 잘못하면 권한 상승 구멍이 되므로, (A)나 (B)로 충분하면 무리해서 고를 필요는 없습니다.
어느 방법이든, 세션 안에서 돌아가는 프로세스가 RmStartSession부터 RmRestart까지를 담당한다는 구도는 같습니다. 세션 0의 서비스는 「업데이트 파일을 배포한다」「신호를 낸다」까지를 맡습니다.
MSI와의 관계도 정리해 둡니다. Windows Installer 4.0 이후의 MSI는 Restart Manager를 자동으로 사용합니다. 기본 동작은 「OS를 재부팅하는 것이 아니라, 가능하면 앱을 종료·재시작한다」입니다. 패키지 작성자가 할 수 있는 것은, 전체 UI일 때 「애플리케이션을 자동으로 닫고 재시작한다」는 선택지를 사용자에게 내는 MsiRMFilesInUse 대화 상자 추가(오래된 Installer에서는 기존의 FilesInUse 대화 상자로 폴백), MSIRESTARTMANAGERCONTROL 등 속성으로 동작 제어, 그리고 custom action에서 MsiRestartManagerSessionKey 속성을 통해 RmJoinSession을 호출해 리소스를 추가 등록하는 것입니다(이 custom action은 사용 중 파일 검출이 이루어지는 InstallValidate 액션보다 앞에 둡니다). 무인 설치에서는 항상 Restart Manager가 사용되어 앱이 자동 종료됩니다.4
즉 MSI로 배포하고 있다면 「Restart Manager를 호출하는 코드」를 직접 쓸 필요는 거의 없고, 앱 쪽 규칙(5절)을 갖추는 것이 핵심입니다. 자체 업데이터를 쓸 때에만 이 API를 직접 호출할 가치가 생깁니다. 배포 방식 자체의 선택은 「Windows 앱 배포 방식 고르기 - MSI/MSIX/ClickOnce/xcopy/자체 업데이트」에서 다룹니다.
4. C#으로 「누가 붙잡고 있는지」를 출력하기 ── RmGetList 진단 코드
Restart Manager 안에서 업데이트 처리를 쓰지 않는 사람에게도 도움이 되는 것이 RmGetList입니다. 「이 파일을 사용 중인 프로세스와 서비스 목록」을 API로 가져올 수 있으므로, 업데이터의 오류 메시지를 「파일이 사용 중입니다」에서 「경리 단말의 MyApp.exe(PID 4132)가 붙잡고 있습니다」로 바꿀 수 있습니다.
4.1 골격 ── 호출하는 것은 네 개뿐
P/Invoke 선언과 구조체 정의를 빼면, 처리의 본체는 이것뿐입니다. 먼저 이 형태를 머리에 넣은 뒤, 다음 완전판을 읽기 바랍니다.
// 【골격】 구조체 정의와 P/Invoke 선언은 4.3 완전판에 실었습니다
var sessionKey = new StringBuilder(CCH_RM_SESSION_KEY + 1);
// ① 세션을 시작한다
int rc = RmStartSession(out uint session, 0, sessionKey);
if (rc != 0) throw new Win32Exception(rc);
try
{
// ② 조사할 파일을 등록한다(전체 경로여야 함)
var fullPaths = Array.ConvertAll(args, Path.GetFullPath);
rc = RmRegisterResources(session, (uint)fullPaths.Length, fullPaths, 0, null, 0, null);
if (rc != 0) throw new Win32Exception(rc);
// ③ 사용 중인 프로세스/서비스를 열거한다
// 버퍼가 부족하면 ERROR_MORE_DATA와 필요 건수가 돌아오므로, 확보한 뒤 다시 가져온다
uint count = 0;
RM_PROCESS_INFO[] apps = null;
while (true)
{
rc = RmGetList(session, out uint needed, ref count, apps, out uint reasons);
if (rc == 0) break;
if (rc != ERROR_MORE_DATA) throw new Win32Exception(rc);
count = needed;
apps = new RM_PROCESS_INFO[needed];
}
// apps[0] 〜 apps[count-1]에 「붙잡고 있는 누군가」가 들어 있다
}
finally
{
RmEndSession(session); // ④ 반드시 닫는다
}
4.2 무엇이 돌아오는가 ── 출력 형태와 RM_APP_TYPE
완전판(4.3)을 WhoLocks.exe C:\App\MyApp.exe처럼 실행하면 출력은 다음 형태가 됩니다. 값은 설명을 위한 예이며, 실제 내용은 환경에 따라 달라집니다.
사용 중인 프로세스/서비스: 2건 (RebootReasons=0)
PID=4132 종류=1 재시작 가능=True 세션=2 이름=MyApp 서비스=
PID=6284 종류=3 재시작 가능=False 세션=0 이름=MyAppAgent 서비스=MyAppAgent
읽는 법은 세 가지입니다. 종류(RM_APP_TYPE)로 종료 방식이 정해지고, 재시작 가능(bRestartable)이 False이면 업데이트 후 자동으로 뜨지 않으며, 세션(TSSessionId)이 자신과 다르면 3절의 「세션을 넘지 못한다」는 제약에 걸립니다.
종류의 수치와 의미는 다음과 같습니다.9
| 값 | 이름 | 의미 |
|---|---|---|
| 0 | RmUnknownApp | 다른 어느 쪽에도 분류되지 않는 앱. 강제 종료로만 멈출 수 있다 |
| 1 | RmMainWindow | 단독 프로세스로 동작하고 최상위 창을 가진 Windows 앱 |
| 2 | RmOtherWindow | 단독 프로세스가 아니며, 최상위 창도 갖지 않는 Windows 앱 |
| 3 | RmService | Windows 서비스 |
| 4 | RmExplorer | Windows Explorer |
| 5 | RmConsole | 단독 콘솔 앱 |
| 1000 | RmCritical | 정지할 수 없어 설치 완료에 OS 재부팅이 필요하다. critical 프로세스, 권한 부족, Restart Manager를 시작한 본인 프로세스 중 하나 |
실무에서는 0과 1000이 나온 시점에 「무난한 업데이트는 불가」로 판단할 수 있습니다. 1·5라면 3절의 규칙(WM_QUERYENDSESSION / CTRL_C_EVENT)으로 닫히고, 3이라면 서비스 제어 관리자를 통해 멈춥니다.
4.3 완전판
C#에서 P/Invoke로 쓰면 다음과 같습니다(.NET Framework 4.8 / .NET 8 어느 쪽에서든 동작합니다).
// WhoLocks.cs ── 지정 파일을 「누가 붙잡고 있는지」를 Restart Manager로 열거한다
// 사용법: WhoLocks.exe C:\App\MyApp.exe C:\App\MyLib.dll
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
internal static class Program
{
private const int CCH_RM_SESSION_KEY = 32; // 세션 키는 GUID 문자열(32자+종단)
private const int CCH_RM_MAX_APP_NAME = 255;
private const int CCH_RM_MAX_SVC_NAME = 63;
private const int ERROR_MORE_DATA = 234;
[StructLayout(LayoutKind.Sequential)]
private struct RM_UNIQUE_PROCESS
{
public uint dwProcessId;
public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
}
// RM_APP_TYPE: 1=GUI 앱, 2=다른 창, 3=서비스, 4=Explorer, 5=콘솔, 1000=critical
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct RM_PROCESS_INFO
{
public RM_UNIQUE_PROCESS Process;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
public string strAppName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
public string strServiceShortName;
public int ApplicationType;
public uint AppStatus;
public uint TSSessionId;
[MarshalAs(UnmanagedType.Bool)]
public bool bRestartable;
}
// Restart Manager API는 GetLastError가 아니라 반환값으로 Win32 오류 코드를 돌려준다
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
private static extern int RmStartSession(
out uint pSessionHandle, int dwSessionFlags, StringBuilder strSessionKey);
[DllImport("rstrtmgr.dll")]
private static extern int RmEndSession(uint dwSessionHandle);
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
private static extern int RmRegisterResources(uint dwSessionHandle,
uint nFiles, string[] rgsFileNames,
uint nApplications, RM_UNIQUE_PROCESS[] rgApplications,
uint nServices, string[] rgsServiceNames);
[DllImport("rstrtmgr.dll")]
private static extern int RmGetList(uint dwSessionHandle,
out uint pnProcInfoNeeded, ref uint pnProcInfo,
[In, Out] RM_PROCESS_INFO[] rgAffectedApps, out uint lpdwRebootReasons);
private static void Main(string[] args)
{
if (args.Length == 0)
{
Console.Error.WriteLine("사용법: WhoLocks <파일경로> ...");
Environment.Exit(2);
}
var sessionKey = new StringBuilder(CCH_RM_SESSION_KEY + 1);
int rc = RmStartSession(out uint session, 0, sessionKey);
if (rc != 0) throw new Win32Exception(rc, $"RmStartSession실패 (rc={rc})");
try
{
// API 계약상 등록하는 파일 이름은 전체 경로여야 한다.
// 상대 경로로 호출되어도 동작하도록, 등록 전에 정규화한다
var fullPaths = Array.ConvertAll(args, Path.GetFullPath);
rc = RmRegisterResources(session, (uint)fullPaths.Length, fullPaths, 0, null, 0, null);
if (rc != 0) throw new Win32Exception(rc, $"RmRegisterResources실패 (rc={rc})");
// 필요 건수를 조회→배열을 확보→취득. 두 호출 사이에 프로세스가
// 늘어나면 ERROR_MORE_DATA가 다시 돌아오므로, 루프로 재시도한다
uint count = 0;
uint rebootReasons;
RM_PROCESS_INFO[] apps = null;
while (true)
{
rc = RmGetList(session, out uint needed, ref count, apps, out rebootReasons);
if (rc == 0) break;
if (rc != ERROR_MORE_DATA) throw new Win32Exception(rc, $"RmGetList실패 (rc={rc})");
count = needed;
apps = new RM_PROCESS_INFO[needed];
}
Console.WriteLine($"사용 중인 프로세스/서비스: {count}건 (RebootReasons={rebootReasons})");
for (int i = 0; i < count; i++)
{
RM_PROCESS_INFO a = apps[i];
Console.WriteLine(
$" PID={a.Process.dwProcessId,-6} 종류={a.ApplicationType} " +
$"재시작 가능={a.bRestartable} 세션={a.TSSessionId} " +
$"이름={a.strAppName} 서비스={a.strServiceShortName}");
}
}
finally
{
RmEndSession(session); // 세션 해제는 반드시 finally에서
}
}
}
요점은 세 가지입니다. 첫째, Restart Manager API는 반환값이 그대로 Win32 오류 코드이며, GetLastError는 쓰지 않습니다(DllImport에 SetLastError = true는 필요 없습니다). 둘째, RmGetList는 「버퍼가 부족하면 ERROR_MORE_DATA(234)와 필요 건수를 반환한다」는 호출 규약이므로, 조회→확보→취득 루프로 씁니다.3 셋째, lpdwRebootReasons가 0(RmRebootReasonNone)이 아니면 「앱 종료로는 안 되고 OS 재부팅이 필요」라는 판정입니다.3 구조체 문자열을 ByValTStr로 마샬링하는 쓰기법이나, 이런 P/Invoke를 안전하게 쓰는 일반론은 「C#에서 Win32 API를 안전하게 호출하기 ── P/Invoke 실무 가이드」를 참고하기 바랍니다.
이 코드에 RmShutdown/RmRestart 두 P/Invoke를 더하면 「열거→무난하게 종료→교체→재시작」 자체 업데이터의 골격이 됩니다. 다만 RmShutdown을 호출해도 되는 것은 자신이 RmStartSession한 프로세스뿐이며, MSI custom action에서는 호출하면 안 됩니다(MSI 자신이 세션을 관리하기 때문입니다).4
5. 앱 쪽 규칙 ── 「절차에 맞게 닫히고, 원래대로 다시 뜨기」위한 3점 세트
Restart Manager도 MSI도, terminate로 무조건 프로세스를 죽이지는 (강제를 지정하지 않는 한) 않습니다. 앱 쪽이 응답하지 않으면, 결국 「사용 중인 파일」 대화 상자로 사용자 손을 빌리게 됩니다. 업무 앱을 「업데이트에 강하게」 만들려면, 앱 쪽에 다음 3점 세트를 구현합니다.5
(1) RegisterApplicationRestart로 재시작을 등록한다. 시작 직후에 한 번 호출해 둡니다. Restart Manager가 재시작할 수 있는 것은 등록된 앱뿐이며, 이것이 「재시작 시 명령줄을 OS에 전하는」 유일한 수단입니다. 명령줄에 exe 이름은 넣지 않는다(OS가 붙여 줍니다), 최대 길이는 RESTART_MAX_CMD_LINE, 그리고 시작 후 60초 동안은 재시작 루프 방지 때문에 재시작되지 않는다, 가 주요 사양입니다. 크래시·hang 시 재시작이 필요 없으면 RESTART_NO_CRASH | RESTART_NO_HANG을 지정해 「업데이트 때만 재시작」으로 좁힐 수 있습니다.6
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int RegisterApplicationRestart(string commandLine, int flags);
// 시작 시 등록해 둔다. 「/restored」 플래그로 재시작 후 복원 처리로 분기할 수 있다
// RESTART_NO_CRASH(1) | RESTART_NO_HANG(2) = 업데이트로 인한 종료일 때만 재시작한다
RegisterApplicationRestart("/restored", 1 | 2);
(2) WM_QUERYENDSESSION(lParam=ENDSESSION_CLOSEAPP)에 응답한다. Restart Manager는 종료 전에 이 메시지로 「닫아도 되는가」를 묻습니다. 여기서는 아직 종료하지 않고, 준비가 되어 있으면 TRUE를 반환합니다(다른 앱의 준비가 끝나지 않았을 수 있기 때문입니다). FALSE를 반환하면 종료를 취소할 수 있지만, 강제 지정 시에는 결국 종료되므로 FALSE에 의존하는 설계는 피합니다. 업데이트 국면에서 RegisterApplicationRestart를 다시 호출할 마지막 기회도 이 타이밍입니다. 복원에 필요한 상태(열려 있던 장표 ID 등)를 명령줄 인수에 담아 다시 등록하면, 재시작 후 「원래 화면」까지 되돌릴 수 있습니다.56
(3) WM_ENDSESSION에서 미저장 데이터를 대피시키고 종료한다. 실제 종료 지시는 WM_ENDSESSION(wParam=TRUE, lParam=ENDSESSION_CLOSEAPP)으로 도착합니다. 타임아웃(강제 시 앱 30초) 안에 미저장 데이터 자동 저장·작업 상태 직렬화·연결 닫기를 마치고 종료합니다. 공식 가이드라인은 「애초에 사용자 데이터를 정기적으로 저장해 둘 것」을 권합니다. 콘솔 앱의 경우에는 WM_ 대신 CTRL_C_EVENT가 오므로, SetConsoleCtrlHandler(C#이라면 Console.CancelKeyPress)로 같은 일을 합니다.52
WPF/WinForms에서는 Application.SessionEnding / Form.FormClosing(CloseReason.WindowsShutDown)이 입구가 되지만, ENDSESSION_CLOSEAPP 플래그(OS 재부팅이 아니라 「앱만 닫고 나중에 재시작」의 구분)까지 보려면 윈도우 프로시저 훅이 필요합니다. 또한 재시작 후 프로세스가 「종료 직전의 이전 자신」과 부딪치지 않도록, 다중 실행 방지 Mutex 설계도 함께 다시 보기 바랍니다(「Windows 앱의 다중 실행 방지」).
이 3점 세트가 갖춰지면, MSI 업데이트 경험은 「전원에게 사내 방송으로 닫아 달라고 하기」에서 「업데이트가 돌면 앱이 자동으로 닫히고, 업데이트 후 원래 화면까지 자동 복귀한다」로 바뀝니다. Office의 「재시작 후 문서를 다시 열어 주는」 동작과 같은 장치입니다.
6. 자동 업데이트 구현 패턴 ── 별도 프로세스, rename-then-replace, 재부팅 시 치환
자체 자동 업데이트를 짤 때의 설계 패턴을 정리합니다. 대전제는 하나, 자기 자신(실행 중인 exe)을 스스로 덮어쓸 수는 없다는 것이므로, 업데이트 처리는 어딘가에서 「별도 프로세스」나 「다른 이름」으로 빼내야 합니다.
패턴 A: 별도 프로세스 업데이터 + 기다렸다가 교체. 메인 앱이 업데이트를 감지하면 업데이터 exe(또는 복사한 임시 exe)를 시작하고 자신은 종료합니다. 업데이터는 메인 프로세스의 종료를 기다린 뒤(Process.WaitForExit나 Mutex 해제 대기) 파일을 교체하고 메인 앱을 재시작합니다. 단순하고 확실하지만, 「메인 앱이 쉽게 끝나지 않는다」「다중 프로세스 구성이라 기다릴 상대가 많다」는 경우에 대비해, 앞 절의 RmGetList로 점유자를 확인한 뒤 RmShutdown으로 닫는 조합이 유효합니다.
패턴 B: rename-then-replace(멈추지 않는 업데이트). 2절대로, 실행 중인 exe/DLL의 이름을 바꾸고 새 버전을 두어 「다음 실행부터 새 버전」으로 만듭니다. 사용자 작업을 끊지 않는 것이 이점이며, 상주형·장시간 가동형 업무 앱에 맞습니다. Squirrel이나 Velopack 같은 .NET용 업데이트 프레임워크도 버전별 폴더 + 엔트리 exe 교체라는 형태로 이 특성을 이용해, 「업데이트는 뒤에서 적용, 다음 실행부터 새 버전」을 틀 그대로 제공합니다. 직접 구현한다면 다운로드→검증→압축 해제→이름 변경으로 빼 두기→배치→다음 시작 시 대피 파일 정리, 라는 절차로 떨어집니다.
패턴 C: MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT(재부팅 시 치환). 서비스 호스트 프로세스나 셸 확장처럼 「도저히 멈추지 못하고·이름 변경으로도 빠지지 못하는」 것의 최후 수단이 OS 재부팅 시 치환 예약입니다. 예약 내용은 레지스트리 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations에 기록되며, 다음 부팅의 이른 단계(AUTOCHK 직후, 페이지 파일 생성 전)에 등록 순서대로 실행됩니다. Administrators 그룹 또는 LocalSystem에서만 호출할 수 있다는 것, MOVEFILE_COPY_ALLOWED와 함께 쓸 수 없다는 것(=같은 볼륨 한정), 치환 대상 파일이 이미 있으면 MOVEFILE_REPLACE_EXISTING을 함께 써야 한다는 것(함께 쓰지 않으면 예약 자체는 성공해도 재부팅 시 치환이 이루어지지 않습니다), 그리고 함수의 성공은 「예약의 성공」이지 실제 치환 결과는 알 수 없다는 것을 이해한 위에서 사용합니다. 인스톨러가 「재부팅이 필요합니다」라고 말할 때, 내부에서는 바로 이것이 쌓입니다.7
어느 패턴이든 공통 주의가 두 가지 있습니다. 하나는 서비스 업데이트로, Windows 서비스는 스스로 자신을 교체하지 못하므로 「SCM에서 Stop→교체→Start」를 수행하는 업데이트용 프로세스(또는 업데이트 전용 서비스)를 분리하는 것이 정석입니다(서비스 설계의 기본은 「Windows 서비스 만드는 법과 운용」). 다른 하나는 업데이트 파일 검증입니다. 교체 장치를 직접 만든다는 것은 「임의의 exe를 배포해 실행시키는 장치」를 직접 만든다는 것이기도 해서, 서명 검증이나 다운로드 경로 보호를 빼면 업데이트 기구 자체가 공격 경로가 됩니다. 이 논점은 「자동 업데이트의 보안 설계 - HTTPS만으로는 부족한 이유」에서 따로 다룹니다.
7. 업데이트 방식 판단표
| 방식 | 맞는 상황 | 주의점 |
|---|---|---|
| MSI + OS 재부팅을 허용 | 배포 빈도가 낮다(연 수 회), 야간·휴일에 유지보수 시간을 잡을 수 있다 | 구현이 가장 쉽다. 다만 「재부팅이 필요합니다」를 사용자에게 계속 보여 주게 된다. 재부팅 시 치환(PendingFileRenameOperations)에 의존7 |
| MSI + Restart Manager 통합(MsiRMFilesInUse + 앱 쪽 3점 세트) | MSI 배포를 유지한 채 업데이트 경험을 개선하고 싶다. 사내 배포의 표준 앱 | 인스톨러 쪽은 거의 자동. 효과는 앱 쪽 RegisterApplicationRestart/WM_QUERYENDSESSION 구현에 달림45 |
| 자체 업데이터(별도 프로세스 + rename-then-replace, Squirrel/Velopack 포함) | 업데이트 빈도가 높다(주 단위 이상), 사용자에게 관리자 권한이 없다, 작업을 끊고 싶지 않다 | 정리 처리·서명 검증·실패 시 롤백을 직접 만들 각오가 필요하다. RmGetList/RmShutdown으로 「붙잡고 있는 누군가」 대책을 넣으면 단단하다3 |
| ClickOnce | 사내 Windows 클라이언트에서 「시작 시 자동 업데이트」만 필요하다 | 시작 시 업데이트 모델이면 사용 중 문제 자체가 일어나기 어렵다. 제약은 「ClickOnce란 무엇인가」 참조 |
| 서비스 쪽 자기 업데이트(업데이트용 프로세스 분리) | 무인 환경·장치 PC에서 24시간 도는 서비스를 멈추지 않고는 유지할 수 없다 | 서비스 자신은 자신을 교체하지 못한다. Stop→교체→Start를 맡는 별도 프로세스가 필수. 세션 경계(LocalSystem에서 사용자 앱은 닫지 못함)에 주의2 |
망설이면 먼저 「MSI + Restart Manager 통합」을 검토하기 바랍니다. OS 표준 장치에 올라타므로 구현량이 최소이고, 앱 쪽 3점 세트는 자체 업데이터로 옮길 때도 그대로 자산이 됩니다.
8. 정리
- 사용 중인 exe/DLL은 덮어쓰기·삭제할 수 없지만, 같은 볼륨 안의 이름 변경은 가능합니다. 이 비대칭성을 쓴 rename-then-replace가 「멈추지 않는 업데이트」의 기본형입니다.
- Restart Manager는 「누가 붙잡고 있는지」의 열거(RmGetList), 무난한 종료(RmShutdown), 업데이트 후 재시작(RmRestart)까지 맡기는 OS 표준 API이며, Windows Installer 4.0 이후의 MSI는 이를 자동으로 사용합니다.
- 앱 쪽 규칙은 3점 세트입니다. RegisterApplicationRestart로 재시작을 등록하고, WM_QUERYENDSESSION(ENDSESSION_CLOSEAPP)에 TRUE를 반환하고, WM_ENDSESSION에서 미저장 데이터를 대피시킨 뒤 종료합니다. 이것만으로 「업데이트 후 원래대로 다시 뜨는 앱」이 됩니다.
- 60초 규칙(시작 직후는 재시작되지 않음), 강제 종료 타임아웃(앱 30초·서비스 20초), 세션 경계(LocalSystem에서 사용자 앱은 닫지 못함)가 실제 운용의 걸림돌입니다.
- 도저히 교체하지 못할 때는 MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT로 OS 재부팅 시 치환을 예약합니다. 관리자 권한 필수·같은 볼륨 한정·성패는 예약의 성패입니다.
- 업데이트 기구를 직접 만드는 것은 「임의의 exe를 배포하는 장치」를 직접 만드는 것입니다. 서명 검증·경로 보호·롤백까지 포함해 설계해야 합니다.
관련 기사
- Windows 앱 배포 방식 고르기 - MSI/MSIX/ClickOnce/xcopy/자체 업데이트
- 자동 업데이트의 보안 설계 - HTTPS만으로는 부족한 이유
- Process Explorer / Handle / VMMap으로 hang과 leak을 추적하기
- C#에서 Win32 API를 안전하게 호출하기 ── P/Invoke 실무 가이드
- Windows 앱의 다중 실행 방지
- Windows 서비스 만드는 법과 운용
관련 상담 영역
合同会社小村ソフト에서는 업무 앱 자동 업데이트 기구의 설계·구현(Restart Manager 통합, 자체 업데이터, Squirrel/Velopack 도입), 「업데이트할 때마다 전원에게 앱을 닫게 하는」 운용의 개선, 기존 인스톨러의 「파일 사용 중」「재부팅이 필요합니다」 문제의 조사와 대책을 다룹니다.
참고 링크
-
Microsoft Learn, About Restart Manager. Restart Manager의 목적(사용 중 파일로 인한 재부팅의 감소·제거), GUI 앱→콘솔 앱→서비스→Explorer 순으로 정지하고 역순으로 재시작하는 것, 세션을 넘는 shutdown이 미지원인 것, Windows Installer 4.0이 Restart Manager를 자동으로 쓰는 것, 중요한 시스템 서비스는 OS 재부팅 없이는 정지할 수 없는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, RmShutdown function. RmForceShutdown 지정 시에도 응답하지 않는 앱은 30초·서비스는 20초에 강제 종료되는 것, RmShutdownOnlyRegistered로 「전원이 재시작 등록된 때만 종료」로 만들 수 있는 것, LocalSystem 서비스는 다른 사용자 세션의 앱을 종료·재시작하지 못하는 것, ERROR_FAIL_NOACTION_REBOOT 등 반환값에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, RmGetList function. 등록된 리소스를 사용 중인 앱·서비스를 RM_PROCESS_INFO 배열로 반환하는 것, 버퍼 부족 시 ERROR_MORE_DATA(234)와 필요 건수를 반환하는 호출 규약, lpdwRebootReasons로 OS 재부팅이 필요한 이유(RM_REBOOT_REASON)가 돌아오는 것, Windows Vista 이후 rstrtmgr.dll이 제공하는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Using Windows Installer with Restart Manager. Windows Installer 4.0이 Restart Manager를 자동으로 쓰고, 기본값으로 OS 재부팅보다 앱의 종료·재시작을 우선하는 것, MsiRMFilesInUse 대화 상자로 전체 UI일 때 자동 닫기·재시작 선택지를 낼 수 있는 것(구환경에서는 FilesInUse로 폴백), 무인 설치에서는 항상 Restart Manager가 사용되어 앱이 종료되는 것, custom action은 InstallValidate보다 앞에 두고 MsiRestartManagerSessionKey를 통해 RmJoinSession을 호출해야 하며 RmShutdown 등은 호출하면 안 되는 것, MSIRESTARTMANAGERCONTROL 등 제어 속성에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Guidelines for Applications (Restart Manager). GUI 앱에 WM_QUERYENDSESSION(lParam=ENDSESSION_CLOSEAPP)이 보내지고, 준비가 되어 있으면 TRUE를 반환하며 그 시점에는 종료하지 않는 것, 실제 종료는 WM_ENDSESSION에서 하는 것, 응하지 않는 앱에는 WM_CLOSE도 보내지는 것, 콘솔 앱에는 CTRL_C_EVENT가 보내지는 것, 재시작에는 RegisterApplicationRestart 등록이 필수인 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, RegisterApplicationRestart function. 재시작 시 명령줄 등록(exe 이름은 넣지 않음, 최대 RESTART_MAX_CMD_LINE), RESTART_NO_CRASH/NO_HANG/NO_PATCH/NO_REBOOT 플래그, 업데이트로 인한 재시작은 자동이고 크래시·hang 시에는 사용자 동의가 필요한 것, 루프 방지를 위해 시작 후 60초가 지나지 않으면 재시작되지 않는 것, 업데이트 시 마지막 등록 기회가 WM_QUERYENDSESSION 처리 중인 것에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MoveFileExW function. MOVEFILE_DELAY_UNTIL_REBOOT이 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager의 PendingFileRenameOperations(REG_MULTI_SZ)에 예약을 쓰고, AUTOCHK 실행 후·페이지 파일 생성 전에 등록 순으로 실행되는 것, Administrators 그룹 또는 LocalSystem 권한이 필요한 것, MOVEFILE_COPY_ALLOWED와 병용 불가인 것, 이동 대상 파일이 이미 있으면 MOVEFILE_REPLACE_EXISTING 지정이 필요한 것, 반환값이 예약의 성패이지 실제 이동의 성패가 아닌 것, lpNewFileName에 NULL을 넘기면 재부팅 시 삭제가 되는 것에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, WTSQueryUserToken function (wtsapi32.h). 세션 ID를 지정해 로그온 중 사용자의 primary access token을 얻는 함수인 것, 호출에는 LocalSystem 계정 컨텍스트에서 동작 중일 것과 SE_TCB_NAME 권한이 필요한 것, 고도로 신뢰된 서비스용이며 토큰을 흘리지 않도록 주의하고 얻은 핸들은 반드시 CloseHandle로 닫아야 하는 것, 세션 ID 열거에 WTSEnumerateSessions를 쓸 수 있는 것에 대해. ↩
-
Microsoft Learn, RM_APP_TYPE enumeration (restartmanager.h). RM_PROCESS_INFO 구조체가 가리키는 애플리케이션 종류로서, RmUnknownApp(0, 다른 데 분류되지 않아 강제 종료로만 정지할 수 있음), RmMainWindow(1, 최상위 창을 가진 단독 프로세스 Windows 앱), RmOtherWindow(2, 단독 프로세스도 최상위 창도 없는 Windows 앱), RmService(3, Windows 서비스), RmExplorer(4, Windows Explorer), RmConsole(5, 단독 콘솔 앱), RmCritical(1000, 프로세스를 정지할 수 없어 설치 완료에 시스템 재부팅이 필요. 이유는 critical 프로세스, 권한 부족, Restart Manager를 시작한 인스톨러 자신 중 하나)가 정의되어 있는 것에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows on Arm에서 업무 앱은 동작하는가 ── x64 에뮬레이션(Prism)과 네이티브 DLL·COM의 현실
「Windows on Arm에서 업무 앱은 동작하는가」에 개발자와 사내 IT를 위해 답합니다. x64 에뮬레이션(Prism)의 구조, 드라이버처럼 동작하지 않는 계층, .NET의 AnyCPU와 P/Invoke 문제, Arm 대응 체크리스트까지 정...
C#에서 Win32 API를 안전하게 호출하기 ── P/Invoke 실무 가이드(DllImport / LibraryImport / CsWin32)
C#에서 Win32 API를 P/Invoke로 호출할 때 실무에서 짚을 포인트를 정리합니다. DllImport와 LibraryImport의 차이, CsWin32를 이용한 자동 생성, 문자열 마샬링의 함정, SafeHandle, 32/64bit 차...
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리할 점
Windows 앱 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 수정, 장치 연동, COM/ActiveX, 배포·업데이트, 유지보수를 정리하는 포인트를 설명합니다.
Windows 앱 배포 방식 고르기 - MSI/MSIX/ClickOnce/xcopy/자체 updater
Windows 앱의 배포 방식은 인스톨러 형식의 취향이 아니라, OS와의 결합도와 업데이트 책임을 누가 질지의 선택입니다. MSI / MSIX / ClickOnce / xcopy / 자체 updater를 실무 관점에서 정리합니다.
인수는 왜 깨지는가 ── Windows 명령줄 인수의 규칙
Windows에는 인수의 배열이 존재하지 않으며, CreateProcess에 전달되는 것은 한 줄의 문자열이고 분할은 받는 쪽이 합니다. CommandLineToArgvW·CRT·.NET의 분할 규칙과 .NET의 ArgumentList, C++에...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 실행 중인 exe나 로드된 DLL은 왜 덮어쓸 수는 없는데 이름은 바꿀 수 있나요?
- Windows는 실행 중인 exe나 로드된 DLL을 메모리 맵(이미지 섹션)으로 붙잡고 있어서, 파일 내용의 덮어쓰기와 삭제는 오류(공유 위반이나 접근 거부)가 됩니다. 반면 디렉터리상의 「이름」은 파일 내용과는 독립적이기 때문에, 같은 볼륨 안에서의 이름 변경(이동)은 실행 중이라도 성공합니다. 이 비대칭성을 이용한 것이 rename-then-replace 패턴입니다. 기존 exe를 대피용 이름으로 바꾼 뒤 새 exe를 원래 이름으로 두면, 다음 실행부터 새 버전이 사용됩니다. Chrome이나 Squirrel/Velopack 계열의 자동 업데이트도 본질적으로는 이 특성 위에 성립합니다.
- Restart Manager는 무엇을 해 주는 API인가요?
- 업데이트하려는 파일을 등록하면 「그 파일을 지금 사용 중인 애플리케이션·서비스 목록」을 열거하고, 가능하면 종료시킨 뒤 업데이트 후 재시작까지 시켜 주는 Windows 표준 API(Windows Vista 이후)입니다. 흐름은 RmStartSession으로 세션을 만들고, RmRegisterResources로 대상 파일을 등록하고, RmGetList로 점유 프로세스를 열거하고, RmShutdown으로 종료시키고, RmRestart로 재시작한 뒤 마지막으로 RmEndSession입니다. GUI 앱 → 콘솔 앱 → 서비스 → Explorer 순으로 정지하고, 재시작은 역순입니다. 「재부팅이 필요합니다」를 줄이기 위한 장치이며, Windows Installer 4.0 이후의 MSI는 이를 자동으로 사용합니다.
- RegisterApplicationRestart를 호출해 두면 무슨 일이 일어나나요?
- 앱이 자신의 재시작 명령줄을 OS에 등록할 수 있고, 업데이트로 종료된 뒤 Restart Manager가 그 명령줄로 앱을 자동 재시작해 줍니다(Restart Manager가 재시작할 수 있는 것은 등록된 앱뿐입니다). 크래시나 hang 시에도 재시작 대상이 되지만, 그 경우에는 사용자 동의가 끼어들고, 업데이트로 인한 재시작은 자동으로 이루어집니다. 루프를 막기 위해 시작 후 60초가 지나지 않으면 재시작되지 않는다는 점, 명령줄에 exe 이름을 넣으면 안 된다는 점에 주의해야 합니다. 복원에 필요한 상태(열려 있던 파일 등)는 명령줄 인수에 담아 다시 등록하는 것이 관례입니다.
- MoveFileEx의 MOVEFILE_DELAY_UNTIL_REBOOT는 어떤 경우에 사용하나요?
- 프로세스를 도저히 멈출 수 없고 rename-then-replace도 쓸 수 없는 경우의 최후 수단으로, OS 재부팅 시 파일 이동·삭제를 예약합니다. 예약은 레지스트리의 PendingFileRenameOperations(HKLM\SYSTEM\CurrentControlSet\Control\Session Manager)에 기록되며, 다음 부팅의 이른 단계(AUTOCHK 이후, 페이지 파일 생성 전)에 등록 순서대로 실행됩니다. 호출에는 Administrators 그룹 또는 LocalSystem 권한이 필요하고, MOVEFILE_COPY_ALLOWED와는 함께 쓸 수 없어 다른 볼륨으로의 이동에는 사용할 수 없습니다. 함수의 성공 여부는 「예약의 성공 여부」이지 실제 치환의 성공 여부가 아니라는 점도 알아 두어야 합니다.
- MSI 인스톨러에서 「사용 중인 파일」을 무난하게 처리하려면 어떻게 하나요?
- Windows Installer 4.0 이후는 Restart Manager와 자동으로 연동하며, 기본값으로 OS 재부팅보다 앱의 종료·재시작을 우선합니다. 패키지에 MsiRMFilesInUse 대화 상자를 추가해 두면, 전체 UI 설치 때 「애플리케이션을 자동으로 닫고 재시작한다」는 선택지가 사용자에게 제시됩니다. 오래된 Windows Installer로 실행된 경우에는 기존의 FilesInUse 대화 상자로 폴백하므로, 둘 다 넣어 두는 것이 정석입니다. 동작은 MSIRESTARTMANAGERCONTROL 등의 속성으로 제어할 수 있으며, 무인 설치에서는 항상 Restart Manager가 사용되어 앱이 자동 종료됩니다. 앱 쪽에서 RegisterApplicationRestart와 WM_QUERYENDSESSION 응답을 구현해 두면, 업데이트 후 앱이 원래대로 다시 뜨는 「무중단에 가까운 업데이트」를 만들 수 있습니다.