작업 스케줄러 작업이 실행되지 않거나 0x1로 끝나는 경우 ── 원인 분리와 안전한 운영 설계

· 업데이트: · · 작업 스케줄러, Windows, PowerShell, 업무 자동화, 배치 처리, 운영, 문제 해결, 기술 상담

수정 이력(10건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
기사 앞부분에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
이벤트 ID로 조사 대상을 좁히는 원인 분리 흐름 그림과, 「마지막 실행 결과」에 섞여 들어오는 3계통의 값을 구분하는 그림을 추가했습니다. 더불어 읽는 법 분류를 한 곳 고쳤습니다. 이벤트 327과 328은 「시작하지 않은 이유」가 아니라 「시작한 뒤에 실행 중인 인스턴스를 멈춘 기록」이므로, 100이 없을 때 볼 대상에서, 100은 있지만 102가 없을 때 볼 대상으로 옮겼습니다.
외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
이름 있는 mutex 예에서, 이전 작업이 소유권을 가진 채로 강제 종료된 경우를 다루지 않았던 점을 고쳤습니다. 이때 `WaitOne`은 false를 반환하지 않고 `AbandonedMutexException`을 던지며, 소유권은 이쪽에 넘어옵니다. 잡지 않고 종료하면 해제되지 않은 채로 다음에도 같은 예외가 나와, 작업이 다시는 실행되지 않습니다. 잡은 뒤 뒷정리를 확인한 다음 계속하는 형태로 바꿨습니다.
이벤트 ID 빠른 참조표를 고쳤습니다. 프로그램이 `0x1`처럼 0이 아닌 값으로 끝나도, 작업 스케줄러 입장에서는 작업이 완료된 것이므로 201이 나옵니다. 202는 작업 스케줄러 쪽이 작업을 완료하지 못했을 때의 이벤트이며, 0이 아닌 종료를 받는 이벤트가 아닙니다. `0x1`은 201의 반환값과 「마지막 실행 결과」로 추적한다는 읽는 법으로 고쳤습니다.
기록 탭의 이벤트 ID로 무엇을 의심할지에 대한 빠른 참조표를 추가했습니다. 더불어 gMSA로 작업을 등록하는 최소 절차, 기록을 사용하는 절차, 복사 명령의 종료 코드를 올바르게 판정하는 래퍼 예도 추가했습니다.
본문의 관련 기사 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 부분을, 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635338)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「작업 스케줄러 작업이 실행되지 않거나 0x1로 끝나는 경우 ── 원인 분리와 안전한 운영 설계」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/windows-task-scheduler-reliable-scheduled-tasks/

DOI(등록된 아카이브)
10.5281/zenodo.21635338
DOI(마지막 등록 버전)
10.5281/zenodo.21635339

「PowerShell로 만든 집계 스크립트를 매일 아침 6시에 돌리고 싶다」, 「직접 실행하면 되는데 작업 스케줄러에 올리면 안 된다」. 업무 자동화 상담을 받다 보면, 마지막에는 거의 항상 이 이야기로 이어집니다.

이 블로그에서도, 로그 정리 자동화, Pester로 스크립트 테스트, C#에서 PowerShell 실행, Power Automate로 업무 자동화와 같이 자동화 이야기를 이어서 써 왔습니다. 이들 기사는 모두 최종적으로 「작업 스케줄러로 정기 실행한다」는 전제를 두고 있습니다. 그런데 그 작업 스케줄러 자체가 의외로 버릇이 강한 구조라서, 「수동이면 되는데 정기 실행이면 실패한다」, 「어느새 멈춰 있었다는 것을 아무도 몰랐다」는 사고가 끊이지 않습니다.

이 기사에서는 작업 스케줄러의 구조 가운데 운영 사고로 직결되는 부분 ── 실행 계정과 로그온 종류, 「실행되지 않을」 때의 원인 분리, 반환값 0x1의 전형적인 원인, 로그를 남기는 방법, 다중 실행 제어 ── 를 실무에서 막히는 순서대로 정리합니다.

이 기사의 전제

항목 내용
대상 OS Windows 10 / 11, Windows Server 2016 이후의 작업 스케줄러(Task Scheduler 2.0 계열)를 전제로 합니다. 더 오래된 at 명령에서 온 작업이 남아 있는 환경에서는, 먼저 그 목록을 정리하는 일부터 시작하십시오
대상 독자 PowerShell이나 배치를 작성할 수 있지만, 이를 정기 실행에 올리는 단계에서 막혀 있는 정보시스템·개발 담당
조작 GUI(taskschd.msc)와 PowerShell의 ScheduledTasks 모듈을 모두 다룹니다. 화면 캡처는 싣지 않습니다. 대신 탭 이름·항목 이름·단추 이름을 실제 표기 그대로 쓰므로, 직접 작업 스케줄러를 열어 두고 읽으십시오
도메인 환경의 전제 gMSA(제 3.3절)는 도메인 환경에 한정된 이야기입니다. 작업 그룹 환경인 경우 건너뛰십시오

1. 먼저 결론

  • 작업 스케줄러 문제의 대부분은 스크립트 자체가 아니라 「누구로, 어떤 세션에서 실행되는가」에 대한 이해의 어긋남에서 생깁니다. 「사용자의 로그온 여부에 관계없이 실행」을 고르는 순간, 대화형 세션과도 로그온 시의 환경과도 다른 세계에서 움직인다는 전제로 설계하십시오.1
  • 「실행되지 않음」의 조사는 기록(History) 탭과 이벤트 로그가 출발점입니다. 다만 작업 기록은 기본값이 사용 안 함이므로, 운영에 올리기 전에 「모든 작업 기록 사용」을 반드시 켜 두십시오.2
  • 「마지막 실행 결과」에 나오는 0x1은 작업 스케줄러의 오류가 아니라, 시작한 프로그램 자신이 종료 코드 1을 반환했다는 뜻입니다. 원인은 스크립트 쪽에 있으므로, 종료 코드를 설계하고 로그를 직접 남기는 구조를 먼저 만듭니다.3
  • PowerShell 스크립트는 -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "전체 경로" 형태로 호출하고, 스크립트 안의 경로는 $PSScriptRoot 기준으로 하는 것이 기본형입니다. 「시작 위치(옵션)」란에 따옴표를 넣으면 안 된다는 고전적인 함정도 여기서 빠지기 쉽습니다.
  • 암호 변경으로 작업이 조용히 죽는 사고를 피하려면, 실행 계정 설계(서비스 계정 점검, 도메인 환경이라면 gMSA 검토)가 필요합니다.4
  • 다중 실행 제어(기본값은 「새 인스턴스를 시작하지 않음」), 실행 시간 제한(기본값 3일), 전원 조건(기본값은 AC 전원일 때만)은 기본값을 그대로 둔 채 눈치채지 못하고 운영하는 대표적인 설정입니다. 등록할 때 반드시 명시적으로 정하십시오.5

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 19건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 작업 스케줄러의 구조 ── 트리거·동작·조건·설정

작업 스케줄러의 작업은 크게 4가지 요소로 이루어져 있습니다.

요소 내용 사고로 이어지기 쉬운 점
트리거 언제 시작할지(시각, 로그온 시, 이벤트 발생 시 등) 시각 트리거를 놓쳤을 때의 처리(후술하는 StartWhenAvailable)
동작 무엇을 실행할지(프로그램, 인수, 시작 폴더) 인수의 따옴표, 시작 폴더 지정 실수
조건 실행해도 되는 상황인지(전원, 네트워크, 유휴) 기본값으로 「AC 전원일 때만」이 사용됨
설정 실행 중의 동작(다중 실행, 시간 제한, 재시도) 기본값을 확인하지 않고 운영을 시작해 버리는 경우

GUI(taskschd.msc)로 만든 작업은 XML로 내보낼 수 있습니다. 작업 정의를 Git으로 관리하고 싶은 경우나, 여러 대에 같은 작업을 배포하는 경우에는 XML 내보내기+schtasks /Create /XML, 또는 PowerShell의 ScheduledTasks 모듈(New-ScheduledTaskAction / New-ScheduledTaskTrigger / New-ScheduledTaskSettingsSet / Register-ScheduledTask)로 스크립트화해 두는 것을 권합니다.5

$action   = New-ScheduledTaskAction -Execute 'pwsh.exe' `
    -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Jobs\Cleanup-Logs.ps1"' `
    -WorkingDirectory 'C:\Jobs'
$trigger  = New-ScheduledTaskTrigger -Daily -At '06:00'
# 전원 조건의 기본값은 「AC 전원일 때만 시작·배터리로 바뀌면 중지」.
# 노트북이나 현장 단말에서도 돌리는 작업이라면, 여기서 명시적으로 허용해 둔다
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
    -MultipleInstances IgnoreNew `
    -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
    -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
# 암호를 화면에 표시하지 않도록 Get-Credential을 거쳐 받는다
$cred = Get-Credential -UserName 'DOMAIN\svc-batch' -Message '실행 계정의 자격 증명'
Register-ScheduledTask -TaskName 'KS-Cleanup-Logs' `
    -Action $action -Trigger $trigger -Settings $settings `
    -User $cred.UserName -Password $cred.GetNetworkCredential().Password

작업 정의가 코드로 되어 있으면, 검증기에서 시험한 것을 그대로 운영으로 가져갈 수 있고, 「어떤 설정으로 실행되고 있는지 모른다」는 상태도 피할 수 있습니다.

여러 대에 배포한다면, GUI로 다듬은 작업을 XML로 내보낸 뒤 schtasks로 배포하는 방법도 실적이 있습니다.

rem 검증기에서 만든 작업을 내보내기
schtasks /Query /TN "KS-Cleanup-Logs" /XML > KS-Cleanup-Logs.xml

rem 각 단말에서 가져오기(실행 계정과 암호는 등록 시 지정)
schtasks /Create /TN "KS-Cleanup-Logs" /XML KS-Cleanup-Logs.xml /RU DOMAIN\svc-batch /RP *

XML에는 트리거·조건·설정이 모두 포함되므로, 리포지토리에 넣어 두면 작업 정의의 리뷰도 변경 이력(diff) 관리도 할 수 있습니다. 반대로, 손으로 10대에 같은 작업을 GUI로 등록하는 운영은, 한 대만 설정이 다른 「겉도는 작업」을 반드시 만듭니다. 대수가 두 자리가 되기 전에 코드화해 두는 것이 안전합니다.

3. 실행 계정과 로그온 종류 ── 가장 큰 사고 지점

작업 속성에서 고르는 「사용자가 로그온할 때만 실행」, 「사용자의 로그온 여부에 관계없이 실행」은, 내부적으로는 로그온 종류(LogonType)의 선택입니다. 여기 이해가 어긋나면, 「수동이면 되는데 정기 실행에서는 안 된다」의 대부분을 설명하지 못한 채 헤매게 됩니다.1

3.1 세 가지 모드의 차이

선택 내부 구조 특징·제약
사용자가 로그온할 때만 실행 대화형 토큰(InteractiveToken) 로그온 중인 화면에 창이 보입니다. 로그오프 중에는 애초에 시작되지 않습니다
로그온 여부에 관계없이 실행 암호 저장(Password) 암호를 등록 시에 저장합니다. 화면은 보이지 않습니다(비대화형). 암호 변경으로 시작이 실패하게 됩니다
위와 같음+「암호를 저장하지 않음」 S4U 암호를 저장하지 않는 대신, 네트워크상의 리소스 액세스와 암호화 파일(EFS) 액세스가 불가능합니다1

실무에서의 전형적인 사고는 다음과 같습니다.

  • 공유 폴더에 액세스하는 스크립트를 「암호를 저장하지 않음」(S4U)으로 등록했다. 로컬 테스트에서는 됐지만, 운영에서는 공유 폴더 액세스만 실패한다. → S4U에는 네트워크 자격 증명이 없기 때문입니다.
  • 「로그온 여부에 관계없이 실행」으로 등록한 몇 달 뒤, 도메인의 암호 유효 기간이 와서 암호를 변경했다. 이후 작업은 로그온 실패(0x8007052E)로 계속 멈췄지만, 아무도 알아채지 못했다.
  • GUI 앱을 시작하는 작업을 「관계없이 실행」으로 등록했다. 앱은 시작되지만 화면이 어디에도 표시되지 않아, 「움직이지 않는다」고 오해했다. → 비대화형 세션에서 실행되기 때문입니다. 대화형 화면이 필요한 처리는, 이 구성에서는 원칙적으로 돌릴 수 없습니다.

또한 Password / S4U로 실행하는 계정에는 「배치 작업으로 로그온」 권한(SeBatchLogonRight)이 필요합니다. Administrators에는 기본값으로 부여되어 있지만, 전용의 일반 사용자를 서비스 계정으로 쓰는 경우에는 로컬 보안 정책 쪽 설정도 확인하십시오.6

3.2 어느 계정으로 돌릴 것인가

  • SYSTEM: 암호 관리가 필요 없고 강력하지만, 권한이 너무 셉니다. 로컬에서 끝나는 보수 처리에는 편리해도, 업무 데이터를 다루는 작업을 무엇이든 SYSTEM으로 돌리는 것은 피해야 합니다. 관리자 권한이 정말 필요한지에 대한 사고방식은, 별도 기사 「Windows 앱에 관리자 권한이 필요한 것은 어떤 때인가」에서 정리하고 있습니다.
  • 전용 서비스 계정(일반 사용자): 최소 권한으로 만들 수 있는 반면, 암호를 바꿀 때마다 작업을 갱신하는 운영이 필요합니다. 암호 유효 기간과 작업 점검을 세트로 관리하십시오.
  • gMSA(그룹 관리 서비스 계정): 도메인 환경이라면 제1 후보입니다. 암호는 도메인 컨트롤러가 자동 관리하므로, 「암호 변경으로 작업이 죽는다」는 문제 자체가 사라집니다. 작업 스케줄러는 gMSA로 실행하는 것을 지원합니다.4

참고로 「가장 높은 수준의 권한으로 실행」 확인란은, UAC로 분할된 토큰 가운데 관리자 쪽(상승된) 토큰으로 실행한다는 뜻입니다. 관리자 권한이 필요 없는 작업에는 넣지 마십시오.

3.3 gMSA로 작업을 등록하는 최소 절차

gMSA를 「제1 후보」라고 쓴 이상, 실제로 어떻게 등록하는가까지 보입니다. 작업 스케줄러의 작업은, 공식적으로 gMSA를 지원하는 구성 중 하나로 명시되어 있습니다.4

전제 조건은 다음과 같습니다.4

  • 도메인과 포리스트의 기능 수준이 Windows Server 2012 이후일 것
  • 도메인에 KDS 루트 키가 이미 만들어져 있을 것(KdsSvc의 Operational 로그 이벤트 ID 4004로 생성을 확인할 수 있습니다)
  • gMSA의 작성·관리에는 Domain Admins 또는 Enterprise Admins의 구성원일 것
  • gMSA 이름은 도메인 단위가 아니라 포리스트 단위로 고유해야 합니다. 같은 이름이 다른 도메인에 있으면 작성에 실패합니다

절차는 3단계입니다. 1〜2는 도메인 관리자가, 3은 작업을 돌리는 단말마다 실행합니다.

# --- 1. 도메인 쪽: gMSA를 만든다. 암호를 가져와도 되는 호스트를 보안 그룹으로 지정한다 ---
#     <SecurityGroup>에는, 작업을 돌리는 서버의 컴퓨터 계정을 넣은 그룹을 지정한다
New-ADServiceAccount -Name 'svc-batch' -DNSHostName 'svc-batch.contoso.local' `
    -PrincipalsAllowedToRetrieveManagedPassword 'GG-BatchHosts'

# --- 2. 작업을 돌리는 단말마다: gMSA를 설치하고, 암호를 가져올 수 있는지 확인한다 ---
Install-ADServiceAccount -Identity 'svc-batch'
Test-ADServiceAccount   -Identity 'svc-batch'   # True가 반환되면 사용할 수 있다

3단계가 작업 등록입니다. 포인트는 2가지입니다.

# --- 3. 작업을 돌리는 단말에서: gMSA를 Principal로 하여 작업을 등록한다 ---
$action  = New-ScheduledTaskAction -Execute 'pwsh.exe' `
    -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Jobs\Cleanup-Logs.ps1"' `
    -WorkingDirectory 'C:\Jobs'
$trigger = New-ScheduledTaskTrigger -Daily -At '06:00'

# 포인트1: 계정 이름 끝에 $를 붙인다(gMSA 이름의 형식)
# 포인트2: LogonType은 Password. 다만 -Password는 넘기지 않는다
#            (gMSA의 암호는 도메인 컨트롤러가 관리하고, 호스트가 가져오기 때문)
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\svc-batch$' `
    -LogonType Password -RunLevel Limited

Register-ScheduledTask -TaskName 'KS-Cleanup-Logs' `
    -Action $action -Trigger $trigger -Principal $principal

New-ScheduledTaskPrincipal의 -LogonType에 지정할 수 있는 값은 None / Password / S4U / Interactive / Group / ServiceAccount / InteractiveOrPassword입니다.7 제 3.1절의 표에서 본 대로 S4U는 네트워크 리소스에 액세스할 수 없으므로, 공유 폴더를 다루는 작업에서 쉽게 고르지 마십시오.

보충을 2가지. 먼저, 이 gMSA에도 「배치 작업으로 로그온」 권한이 필요합니다(제 3.1절 끝과 같은 이야기이며, gMSA라고 면제되는 것은 아닙니다).6 다음으로, schtasks.exe는 실행 계정의 암호를 /RP로 넘기는 전제의 명령이며, gMSA로 등록하는 절차는 공식으로는 안내되어 있지 않습니다. gMSA를 쓴다면, 작업 등록은 PowerShell의 Register-ScheduledTask 쪽으로 모으는 것이 확실합니다. 제 2장에서 쓴 「XML을 내보내 schtasks로 배포한다」 방식과 함께 쓰는 경우에는, Principal 부분만 PowerShell로 덮어쓰는 구성이 됩니다.

4. 「실행되지 않음」을 원인 분리하는 절차

4.1 기록을 사용한 뒤에 의심한다

작업 스케줄러 오른쪽 창에 있는 「모든 작업 기록 사용」은 기본값이 사용 안 함입니다. 기록이 꺼진 상태면, 실패했다는 사실조차 기록에 남지 않습니다. 운영에 올리기 전에 반드시 사용하도록 설정하십시오. 절차는 다음과 같습니다.

  1. 작업 스케줄러를 시작합니다(taskschd.msc, 또는 시작 메뉴에서 「작업 스케줄러」를 검색). 관리자 권한으로 시작하십시오.
  2. 왼쪽 창 트리에서 최상위의 「작업 스케줄러(로컬)」를 선택합니다. 개별 작업을 고른 상태에서는 이 항목이 나오지 않습니다.
  3. 오른쪽 창의 「동작」 목록에 있는 「모든 작업 기록 사용」을 클릭합니다. 이미 사용 중이면, 이 항목은 「모든 작업 기록 사용 안 함」이라는 표시로 바뀌어 있습니다.

기록의 실체는 이벤트 뷰어의 응용 프로그램 및 서비스 로그 > Microsoft > Windows > TaskScheduler > Operational 로그입니다.2 개별 작업의 「기록」 탭은 이 로그를 대상 작업으로 좁혀 보여 주는 것이므로, 여러 작업을 가로질러 시계열로 추적하고 싶을 때는 이벤트 뷰어 쪽을 엽니다.

원인 분리의 기본 절차는 Microsoft의 문제 해결 가이드 흐름 그대로로도 충분히 기능합니다.2

  1. 스크립트를 단독으로 테스트한다 ── 작업에 올리기 전에, 실행 계정과 같은 조건(가능하면 runas나 검증기)으로 스크립트 자체가 끝까지 도는지 확인한다.
  2. 상태 열과 기록 탭을 본다 ── 애초에 트리거되었는지, 시작했지만 실패했는지를 구분한다. 트리거되지 않았다면 트리거 설정·조건(전원·네트워크)을 의심하고, 수동 실행(마우스 오른쪽 단추 클릭 → 실행)으로 동작 자체는 실행되는지 확인한다.
  3. 「사용자가 로그온할 때만 실행」으로 임시 변경해 본다 ── 이렇게 해서 되면, 원인은 비대화형 세션이나 자격 증명 쪽(앞 장)에 있다고 좁힐 수 있습니다.

4.2 기록 탭에 무엇이 나오면 무엇을 의심하는가 ── 이벤트 ID 빠른 참조표

기록 탭(과 Operational 로그)의 행은 모두 이벤트 ID를 가지고 있습니다. 이 ID를 보면 「어디까지 진행됐는지」를 한 번에 알 수 있습니다. 정상적인 작업 1회분은 대체로 「시작(100)→ 프로세스 시작(129)→ 동작 시작(200)→ 동작 완료(201)→ 완료(102)」 순으로 늘어섭니다.

이벤트 ID 메시지의 의미 나왔을 때 의심할 것
106 사용자가 작업을 등록했다8 등록 기록. 「언제 누가 바꿨는지」를 추적할 때의 출발점
140 / 141 사용자가 작업을 갱신했다 / 삭제했다8 「어제까지는 됐다」의 범인 찾기는 여기
113 작업은 등록됐지만, 일부 트리거가 작업을 시작하지 않는다8 트리거 정의의 미비. 등록 시점에 경고되어 있다
116 작업 구성은 저장됐지만, 실행에 쓰는 자격 증명을 저장하지 못했다8 실행 계정과 암호 지정. 여기가 나와 있으면 당연히 실행되지 않는다
100 작업을 시작했다9 여기가 없으면, 애초에 트리거되지 않았거나, 101로 시작에 실패한 것이다
101 작업을 시작하지 못했다. 오류 값 포함9 트리거는 발생했지만 시작에 실패했다. 실행 계정의 자격 증명(0x8007052E 등)이나 권한을 의심한다. 100은 나오지 않는다
129 프로세스 ID와 함께 작업을 시작했다9 프로세스는 생성됐다. 여기서부터는 스크립트 쪽 이야기
200 / 201 동작(action)을 시작했다 / 동작이 완료됐다9 201은 「시작한 프로그램이 종료됐다」는 뜻이며, 내용의 성패는 나타내지 않습니다. 현재 Windows의 201은 본문과 이벤트 데이터에 반환값(ResultCode)을 가지므로, 그곳을 봅니다(4.3절)
202 작업 스케줄러가 동작을 완료하지 못했다. 오류 값 포함9 작업 스케줄러 쪽의 실패. 프로그램이 0x1로 끝났을 때 여기가 나온다고는 할 수 없습니다
203 동작 시작 자체에 실패했다. 오류 값 포함9 실행 파일 경로 오류, 「시작 위치(옵션)」 지정 오류, 권한 부족
102 작업이 정상 종료됐다9 정상 흐름의 끝
111 실행 시간 상한을 넘어서 작업을 종료시켰다9 「중지할 때까지의 시간」(기본값 3일)에 도달. 7장으로
322 같은 작업의 다른 인스턴스가 실행 중이어서 시작하지 않았다10 다중 실행 제어가 동작 중이다. 이전이 끝나지 않았다. 7장으로
323 새 인스턴스를 시작하려고, 실행 중인 인스턴스를 중지했다9 MultipleInstances가 「기존 인스턴스 중지」로 되어 있다
327 전원이 배터리로 바뀌어서 인스턴스를 중지했다9 전원 조건(4.4절). 노트북·현장 단말에서 자주 나옴
328 컴퓨터가 유휴 상태가 아니게 되어서 인스턴스를 중지했다9 유휴 조건이 사용되고 있다
329 작업이 시간 초과되어 인스턴스를 중지했다9 111과 같이 실행 시간 설계를 다시 본다
330 사용자 요청으로 인스턴스를 중지했다9 누군가가 손으로 멈추고 있다

읽는 법의 요점은 3가지입니다.

  • 100이 없으면, 먼저 101(작업을 시작하지 못했다)이 나오지 않았는지 봅니다. 나와 있으면 트리거는 발생했고 시작에 실패한 것이므로, 오류 값과 실행 계정을 의심합니다(제3장). 101도 없으면 원인은 작업보다 앞(트리거·조건·작업 사용 안 함)이고, 322가 나와 있으면 이전 인스턴스가 끝나지 않은 것이 그대로 이유입니다.
  • 100은 있지만 102가 없으면, 시작은 했지만 끝나지 않았습니다. 111 / 329면 시간 초과, 203이면 시작 자체의 실패, 327 / 328이면 전원이나 유휴 조건으로 실행 중인 인스턴스가 멈춘 것입니다(327 / 328은 둘 다 「시작하지 않았다」가 아니라 「멈춘」 기록이므로, 100 뒤에 나옵니다).
  • 100도 102도 있는데 결과가 이상하면, 작업 스케줄러의 책임 범위는 끝까지 돌았습니다. 이후는 스크립트 쪽 로그(6장)로만 추적할 수 있습니다.
없음있음없음있음없음있음실행되지 않음·결과가 이상함이벤트 100(시작)이 있는가이벤트 101이 있는가트리거는 발생했지만 시작에 실패101의 오류 값과실행 계정을 본다(3장)원인은 작업보다 앞트리거·조건·사용 안 함(4.4절)322면 이전이 끝나지 않음이벤트 102(정상 종료)가 있는가시작은 했지만 끝나지 않음203 = 시작 자체의 실패111·329 = 시간 초과327·328 = 전원·유휴 조건으로 중지202 = 작업 스케줄러 쪽의 실패작업 스케줄러 쪽은 끝까지 돌았다0x1이면 스크립트가종료 코드 1을 반환하고 있다이후는 스크립트 쪽 로그로 추적한다(6장)

그림1: 이벤트 100과 102가 있는지에 따라, 조사할 곳이 작업 설정 쪽인지 스크립트 쪽인지로 갈린다

여기서 한 가지, 틀리기 쉬운 점이 있습니다. 프로그램이 0x1처럼 0이 아닌 값으로 끝나도, 작업 스케줄러 입장에서는 「시작해서 종료했다」이므로 201(ACTION_SUCCESS)이 나옵니다. 202는 「작업 스케줄러가 동작을 완료하지 못했다」일 때의 이벤트이며, 프로그램이 0이 아닌 값으로 끝난 경우를 받는 이벤트가 아닙니다.9 따라서 0x1을 추적할 때 202를 찾아도 나오지 않는 경우가 있습니다. 봐야 할 것은 201의 반환값과, 작업의 「마지막 실행 결과」 열(4.3절)입니다. 참고로 201의 ResultCode와 「마지막 실행 결과」는 반드시 일치하지는 않으므로, 확실한 것은 6장처럼 스크립트 쪽에서 자신의 종료 코드를 기록해 두는 것입니다.

4.3 「마지막 실행 결과」의 읽는 법

표시 의미
0x0 정상 종료(시작한 프로그램이 종료 코드 0을 반환했다)
0x1 시작한 프로그램이 종료 코드 1을 반환했다(작업 스케줄러 자체의 오류가 아니다)
0x41300 다음 예약 실행을 대기 중(SCHED_S_TASK_READY)
0x41301 현재 실행 중(SCHED_S_TASK_RUNNING)
0x41303 아직 한 번도 실행되지 않았다(SCHED_S_TASK_HAS_NOT_RUN)
0x8007010B 시작 폴더(「시작 위치(옵션)」) 지정이 잘못됨. 따옴표를 넣은 경우의 전형적인 증상
0x8007052E 로그온 실패. 저장된 암호가 오래됐거나, 권한이 없는 등

0x413xx 계통은 작업 스케줄러의 상태 코드, 0x8007xxxx는 Windows의 오류 코드, 그리고 0x1이나 0x2처럼 작은 값은 시작된 프로그램 자신의 종료 코드입니다.3 이 구분이 되면, 조사할 장소(작업 설정인지, 스크립트인지)를 처음부터 잘못 고르지 않게 됩니다.

0x0 / 0x1 / 0x2처럼 작은 값0x413xx0x8007xxxx시작되지 않음시작됨「마지막 실행 결과」에 나온 값시작된 프로그램 자신의 종료 코드→ 조사할 곳은 스크립트작업 스케줄러의 상태 코드(대기 중·실행 중·미실행)→ 애초에 실패를 나타내지 않음동작은 시작됐는가(201이 있는가, 203이 나오지 않았는가)Windows의 오류 코드(시작 폴더 지정 오류, 로그온 실패 등)→ 조사할 곳은 작업 설정자식 프로세스가 반환한 종료 코드.앱이 HRESULT 형식으로 반환하는 경우도 있다→ 조사할 곳은 스크립트

그림2: 같은 칸에 3계통의 값이 섞여 나온다. 다만 0x8007xxxx는 접두사만으로는 정해지지 않는다 ── 동작이 시작됐다면, 그것은 자식 프로세스가 반환한 값 쪽이므로, 이벤트 201과 203을 맞춰 보고 발생원을 판정한다

4.4 조건·설정의 기본값에 주의

  • 전원 조건: 기본값에서는 「컴퓨터를 AC 전원으로 사용할 때만 작업을 시작」이 사용됩니다. 노트북을 검증기로 쓰면, 배터리 구동 때만 안 되는 「재현되지 않는 결함」이 됩니다. 나아가 Windows 10 이후, 배터리 절약 기능이 사용되는 동안에는 많은 작업의 트리거가 지연됩니다.11
  • 시작 시각을 놓친 경우: PC가 종료되어 있어 시작 시각을 지난 경우, 기본값에서는 다음 예약까지 실행되지 않습니다. 「예약된 시각에 작업을 시작하지 못한 경우 즉시 작업 실행」(-StartWhenAvailable)을 명시적으로 사용하거나, 놓쳐도 되는 작업인지를 설계로 정해 둡니다.5
  • 절전 해제: 야간 작업을 절전 운영 PC에서 돌린다면 「작업을 실행하기 위해 컴퓨터 절전 모드 해제」(-WakeToRun)의 필요 여부도 정합니다.

여기까지 나온 설정이 GUI의 어디에 있는지를, 탭 단위로 정리해 둡니다. 작업을 마우스 오른쪽 단추로 클릭 → 「속성」으로 여는 대화 상자의 구성입니다.

탭 여기서 정하는 것 이 기사의 해당 부분
일반 작업 이름, 실행 계정(「사용자 또는 그룹 변경」), 「사용자가 로그온할 때만 실행」/「로그온 여부에 관계없이 실행」, 「암호를 저장하지 않음」, 「가장 높은 수준의 권한으로 실행」 3장
트리거 언제 시작할지. 「새로 만들기」에서 시각·로그온 시·이벤트 시 등을 추가 4.5절
동작 「프로그램/스크립트」「인수 추가(옵션)」「시작 위치(옵션)」의 3칸. 「시작 위치(옵션)」에 따옴표를 붙이지 않는 것은 여기 5장
조건 전원 조건(AC 전원일 때만/배터리로 바뀌면 중지), 유휴 조건, 네트워크 조건 위의 글머리 기호
설정 「예약된 시각에 작업을 시작하지 못한 경우 즉시 작업 실행」, 「작업이 이미 실행 중인 경우 적용할 규칙」, 「중지할 때까지의 시간」 위의 글머리 기호·7장
기록 그 작업의 이벤트 목록. 기본값은 사용 안 함이며, 4.1절 절차로 사용하기 전까지는 빈 채로 남는다 4.1〜4.2절

4.5 트리거 설계 자체의 주의

「실행되지 않는다」고 생각했더니, 애초에 트리거 설계가 의도와 어긋나 있던 경우도 있습니다.

  • 「매월 31일」은 31일이 없는 달에는 실행되지 않습니다. 월말 처리라면 「매월 마지막 날」을 의도한 설계(월초에 전월분을 처리하거나, 스크립트 쪽에서 날짜를 판정하는) 쪽으로 모으는 편이 안전합니다.
  • 시각은 작업을 등록한 머신의 로컬 시각입니다. 해외 거점 단말이나, 드물게 UTC 설정으로 운영되는 서버에 같은 XML을 배포하면, 실행 시각이 거점마다 어긋납니다. 「전 거점에서 일본 시간 아침 6시」인지 「각 거점의 아침 6시」인지를 사양으로 정해 두십시오.
  • 짧은 간격의 반복(5분마다 등)을 작업 스케줄러로 하기 시작했다면 주의가 필요합니다. 「하루에 한 번 배치」의 도구 구성으로는 뛰어나지만, 분 단위 폴링이나 상시 감시가 필요해졌다면, 그것은 상주 프로세스의 영역입니다(후술하는 제 8장).
  • 이벤트 트리거는 강력하지만, 대상 이벤트가 정말 안정적으로 기록되는지를 먼저 확인하십시오. 응용 프로그램 로그의 특정 이벤트 ID를 트리거로 하는 구성은, 앱 업데이트로 이벤트 나오는 방식이 바뀌면 조용히 멈추게 됩니다. 시각 트리거+스크립트 안의 조건 판정이 결과적으로 추적하기 쉬운 경우도 많습니다.

5. 0x1로 끝나는 전형적인 패턴과 PowerShell의 올바른 호출 방법

0x1은 스크립트가 실패했다는 결과에 지나지 않으므로, 원인은 스크립트 실행 환경의 차이에 있습니다. 수동 실행과 정기 실행에서 다른 것은, 주로 다음 점입니다.

  • 현재 디렉터리가 다르다: 「시작 위치(옵션)」를 지정하지 않으면 C:\Windows\System32 등에서 움직입니다. 상대 경로로 쓴 스크립트는 여기서 깨집니다. 스크립트 쪽은 $PSScriptRoot 기준으로 경로를 조립하고, 작업 쪽은 「시작 위치(옵션)」에 작업 폴더를 지정합니다. 이때 「시작 위치(옵션)」란에 따옴표를 붙이면 안 됩니다. 공백이 포함된 경로라도 따옴표 없이 씁니다(붙이면 0x8007010B로 실패합니다).
  • 환경 변수·프로필이 다르다: 로그온 스크립트나 사용자 프로필에서 설정되는 환경 변수, 매핑된 네트워크 드라이브(X: 등)는, 비대화형 세션에는 존재하지 않는다고 생각하십시오. UNC 경로(\\server\share\...)를 직접 쓰고, -NoProfile로 프로필 차이를 없앱니다.
  • 실행 정책이 다르다: 사용자에게는 RemoteSigned를 설정했어도, 서비스 계정에서는 미설정인 경우가 있습니다. 작업 인수에서 -ExecutionPolicy Bypass를 명시합니다.
  • 도구의 종료 코드 규약이 특수하다: 예를 들어 robocopy는 「모든 파일을 정상적으로 복사했다」는 경우에 1을 반환합니다. 종료 코드를 그대로 반환하는 래퍼면, 정상인데 0x1로 보이거나, 그 반대가 일어납니다. 쓰는 외부 명령의 종료 코드 규약은 반드시 확인하십시오.

robocopy의 경우, 공식 종료 코드 표에서는 0〜7이 「실패 없음」의 조합이며, 8 이상이 「복사 처리 중에 적어도 1건의 실패가 있었다」는 것을 나타냅니다.12 즉, 그대로 반환하는 것이 아니라 0/1로 정규화하는 래퍼를 끼우는 것이 정답입니다.

# 인수는 스스로 받는다. 호출 쪽 변수에 암묵적으로 의존하지 말 것
param(
    [Parameter(Mandatory)][string]$Source,
    [Parameter(Mandatory)][string]$Destination
)

# robocopy는 $LASTEXITCODE에 종료 코드를 반환한다.
# $ErrorActionPreference = 'Stop'을 설정해도, 네이티브 명령의
# 0이 아닌 종료는 예외가 되지 않으므로, 스스로 판정해야 한다
robocopy $Source $Destination /E /R:2 /W:5 /NP
$rc = $LASTEXITCODE

if ($rc -ge 8) {
    Write-Error "robocopy가 실패했습니다. 종료 코드: $rc"
    exit 1
}

# 0〜7은 실패 없음. 무엇이 일어났는지는 로그에 남기면서, 작업 스케줄러에는 성공을 반환한다
Write-Host "robocopy 정상 종료. 종료 코드: $rc"
exit 0

-ge 8의 한 줄이 본체입니다. if ($rc -ne 0)이라고 써 버리면, 정상적으로 복사된 경우(종료 코드 1)까지 실패로 취급합니다. 이것이 「매일 아침 백업이 실패했다고 알림이 오지만, 실제로는 파일은 복사되어 있다」는 단골 상담의 정체입니다.

PowerShell 호출의 기본형은 다음과 같습니다.

프로그램/스크립트:  pwsh.exe          (Windows PowerShell이라면 powershell.exe)
인수 추가:            -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Jobs\Cleanup-Logs.ps1"
시작 위치(옵션):      C:\Jobs           (따옴표 없음)

-Command가 아니라 -File을 쓰는 것은, 인수 이스케이프가 단순해지는 데 더해, 스크립트의 exit n이 그대로 프로세스의 종료 코드가 되어, 작업 스케줄러의 「마지막 실행 결과」에서 성패를 판별할 수 있게 되기 때문입니다. 스크립트 쪽에서도, 성공이면 0, 실패이면 0이 아닌 값을 명시적으로 반환하도록 설계해 둡니다.

# cmdlet의 「비종료 오류」도 catch로 떨어뜨리기 위해 Stop으로 설정한다.
# 이것이 없으면 Copy-Item 등의 실패가 그대로 지나쳐 exit 0이 될 수 있다
$ErrorActionPreference = 'Stop'

try {
    Main
    exit 0
}
catch {
    Write-Error $_
    exit 1
}

예외를 어디서 잡아 어떻게 기록할지에 대한 사고방식은, 별도 기사 「예외 포착과 로그 설계」에서도 자세히 쓰고 있습니다.

6. 로그는 스스로 남긴다

작업 스케줄러의 기록은 「시작했는지·종료 코드는 무엇인지」까지만 알려 줍니다. 스크립트가 무엇을 어디까지 했는지는, 스크립트 자신이 로그로 남겨야 합니다.

최소한으로는, 작업 인수에서 리디렉션하는 것이 아니라(작업 스케줄러의 「인수」란에서의 리디렉션 표기는 셸을 거치지 않으므로 동작하지 않습니다), 스크립트 안에서 트랜스크립트를 잡는 것이 간편합니다.

$logDir = 'C:\Jobs\logs'
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
Start-Transcript -Path (Join-Path $logDir ("cleanup-{0:yyyyMMdd}.log" -f (Get-Date))) -Append
try {
    # 본처리
}
finally {
    Stop-Transcript
}

로그 자체가 계속 쌓이는 문제에 대한 대응(세대 관리·아카이브)은, 바로 「PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화한다」에서 쓴 내용을 그대로 쓸 수 있습니다.

한 걸음 더 가면, Windows 이벤트 로그로의 기록도 검토하십시오. 파일 로그와 달리, 운영 쪽이 이미 보고 있는 장소(이벤트 뷰어, 기존 감시 도구)에 성패가 도착하는 것이 이점입니다.

# --- 설정 시에 한 번만, 관리자 권한으로 실행한다(설치 프로그램이나 초기 설정 스크립트)---
if (-not [System.Diagnostics.EventLog]::SourceExists('KS-Jobs')) {
    [System.Diagnostics.EventLog]::CreateEventSource('KS-Jobs', 'Application')
}

# --- 작업 본체(최소 권한 계정으로 실행)는 기록만 한다.
#     SourceExists는 전체 로그 탐색에 관리자 권한을 요구할 수 있으므로, 실행 시에는 호출하지 않는다.
#     이하는 Main의 실패 핸들러(catch) 안에서 호출하는 전제 ---
catch {
    $err = $_
    try {
        [System.Diagnostics.EventLog]::WriteEntry('KS-Jobs',
            "Cleanup-Logs failed: $($err.Exception.Message)",
            [System.Diagnostics.EventLogEntryType]::Error, 1001)
    }
    catch {
        # 로그에 쓰지 못했다는 것을, 원래 실패를 덮는 이유로 삼지 않는다
        Write-Warning "이벤트 로그 기록에 실패: $_"
    }
    exit 1
}

2가지를 보충합니다. 먼저, 이벤트 소스 등록(CreateEventSource)에는 관리자 권한이 필요합니다. 작업 본체에 등록 처리를 섞으면, 최소 권한 서비스 계정으로 움직이는 운영의 첫 실패 시에 「등록하려다 예외 → 정작 이벤트를 쓰지 못한다」는 이중 실패가 됩니다. 위처럼 등록은 설정 쪽으로 분리하고, 실행 시에는 기록만 하십시오. 다음으로, 이 기사의 작업 등록 예처럼 실행 엔진을 pwsh.exe로 한 경우, Windows PowerShell 5.1 시대의 New-EventLog / Write-EventLog cmdlet은 쓸 수 없습니다(명령을 찾지 못해 스크립트 전체가 실패합니다). 위처럼 .NET 클래스를 직접 호출하는 형태라면 5.1 / 7 어느 쪽에서든 동작합니다.

그 위에, 「실패하면 사람에게 닿는」 구조 ── 메일이나 Teams / Slack 알림 ── 을 하나 넣어 두면, 「몇 달 멈춰 있던 것을 점검에서 알아챈다」는 사고를 막을 수 있습니다. 복잡한 알림 기반은 필요 없고, 실패 때만 Webhook에 POST하는 몇 줄로도 충분히 기능합니다. 반대로 「성공할 때마다 알림」은 머지않아 읽히지 않게 되므로, 성공은 주간 요약 정도로 줄이고, 실패와 「실행되지 않음」(이전 실행 시각이 오래됨)을 감지 대상으로 하는 것을 권합니다. 로그에 무엇을 써야 하는지의 기준은 「예외 포착과 로그 설계」에서도 정리하고 있습니다.

7. 다중 실행과 장시간 실행의 제어

이전 실행이 길어지는 동안 다음 예약 시각이 오면 어떻게 되는가. 이것은 설정 탭의 「작업이 이미 실행 중인 경우 적용할 규칙」으로 정해지며, PowerShell에서는 -MultipleInstances에 대응합니다.5

설정값 동작 맞는 용도
IgnoreNew(GUI 기본값: 새 인스턴스를 시작하지 않음) 실행 중이면 신규 시작을 건너뜀 멱등한 정기 배치 전반. 우선 이것
Queue 실행 중이면 종료 후 차례로 실행 놓침이 허용되지 않는 집계 계통
Parallel 병행하여 시작 원칙적으로 피한다. 병행 안전이 보장되는 경우만

더불어 「중지할 때까지의 시간」(-ExecutionTimeLimit, 기본값 3일)을 현실적인 값(예상 실행 시간의 2〜3배 정도)으로 설정해 두면, 멈춘 프로세스가 다음 날 작업을 함께 끌고 가는 사고를 막을 수 있습니다.5

주의할 점은, IgnoreNew나 Queue가 지켜 주는 것은 같은 작업 정의 안만이라는 점입니다. 다른 작업이 같은 스크립트를 호출하는 경우나, 장애 대응으로 사람이 수동 실행한 경우의 충돌까지는 막지 못합니다. 같은 자원(파일, DB, 외부 시스템)을 다루는 경로가 여러 개라면, 스크립트 쪽에도 배타를 둡니다. 정석은 이름 있는 mutex입니다.

$mutex = New-Object System.Threading.Mutex($false, 'Global\KS-Cleanup-Logs')
$acquired = $false
try {
    try {
        $acquired = $mutex.WaitOne(0)
    }
    catch [System.Threading.AbandonedMutexException] {
        # 이전 작업이 mutex를 가진 채로 강제 종료됐다(작업의
        # 「중지할 때까지의 시간」 초과, 프로세스 kill, 전원 끊김 등).
        # 이때 WaitOne은 false를 반환하는 것이 아니라 예외를 던지며,
        # 소유권은 이쪽에 넘어와 있다. 잡지 않고 종료하면, 이후 실행이
        # 매번 여기서 멈추고, 작업이 다시는 실행되지 않게 된다
        $acquired = $true
        Write-Warning '이전 실행이 비정상 종료되었습니다. 중단된 작업의 뒷정리를 확인하십시오.'
        # 중간에까지 쓴 파일이나 어중간한 레코드가 남아 있지 않은지를,
        # 여기서 검사한 뒤 본처리로 진행한다
    }

    if (-not $acquired) {
        Write-Warning '다른 인스턴스가 실행 중이므로 종료합니다.'
        exit 0   # 「실행하지 않음」을 실패로 치지 않는 경우는 0, 실패 취급이면 0이 아닌 값으로
    }

    # 본처리
}
finally {
    if ($acquired) { $mutex.ReleaseMutex() }
    $mutex.Dispose()
}

AbandonedMutexException은 「이전 소유자가 해제하지 않은 채로 사라졌다」는 것을 알리는 예외이며, 던져진 시점에 소유권은 이쪽에 넘어와 있습니다. 그래서 여기서 exit해 버리면, 해제되지 않은 채로 다음에도 같은 예외가 나와, 작업이 영구히 실행되지 않게 됩니다. 잡은 뒤, 중단된 작업의 상태를 확인한 다음 계속하는 것이 올바른 처리입니다.

이름 앞에 Global\를 붙이면, 다른 세션(다른 사용자의 작업과 수동 실행 등) 사이에서도 배타가 적용됩니다. 잠금 획득을 기다릴지(WaitOne에 시간 초과를 넘김), 즉시 포기할지는, 작업 성질에 맞춰 정하십시오. 참고로 Global\의 이름 있는 개체는 머신상의 누구로부터도 보인다는 점에는 주의가 필요합니다. 여러 이용자가 로그온하는 공유 서버에서는, 악의나 사고로 같은 이름의 mutex를 먼저 쥐면, 작업이 영원히 건너뛰어집니다(게다가 exit 0이면 정상으로 보입니다). 그런 환경에서는 mutex에 ACL(MutexSecurity)을 설정해 획득할 수 있는 계정을 좁히거나, 적어도 「획득하지 못해 건너뛰었다」는 것을 앞 절의 알림·이벤트 로그에 올려, 건너뛰기의 연속을 감시로 감지할 수 있게 하십시오. 파일을 통한 연동에서의 배타 제어는 「파일 연동과 잠금의 모범 사례」에서 자세히 다룹니다.

8. 작업 스케줄러의 「그만둘 때」 ── 상주 서비스와의 구분

작업 스케줄러는 만능이 아닙니다. 요구 사항이 자라 왔을 때, 무리하게 계속 쓰기보다 구조를 갈아타는 편이 나은 경계가 있습니다.

요구 사항 맞는 구조
하루에 몇 번까지의 정시 배치 작업 스케줄러
시작 계기가 사람·이벤트·시각이 섞여 있고, 흐름 전체를 보이고 싶다 Power Automate(별도 기사)
분 단위 폴링, 상시 감시, 큐 처리 Windows 서비스 / 상주 프로세스
처리 간에 상태를 유지하고 싶다, 재시도·백오프를 세밀하게 제어하고 싶다 Windows 서비스 / 상주 프로세스

「5분마다 작업」으로 폴링을 시작하면, 시작할 때마다 프로세스 생성·모듈 로드 비용이 드는 데 더해, 이전 상태를 파일 등에 저장해 두는 구조가 필요해져, 실질적으로 상주 프로세스를 잘게 다시 구현하게 됩니다. 이 단계에 오면, .NET의 Generic Host와 BackgroundService로 상주화하는 것이 자연스럽습니다. 구현 패턴은 「Generic Host와 BackgroundService를 데스크톱 앱에서 쓰기」에서 해설하고 있습니다.

반대로, 월별·일별 배치를 굳이 서비스화해서 스스로 타이머를 관리하는 것도 과합니다. 「실행 간격이 시간 단위 이상·처리가 독립·상태를 갖지 않는다」면 작업 스케줄러, 그것을 벗어나기 시작했다면 상주화를 검토한다는 기준으로 크게 틀리지 않습니다.

9. 운영에 올리기 전의 체크리스트

등록 전에, 다음 항목을 한 번씩 확인할 것을 권합니다.

  • 실행 계정은 정했는가(SYSTEM을 관성으로 고르지 않았는가. 도메인이라면 gMSA를 검토했는가)
  • 로그온 종류의 제약을 이해했는가(S4U라면 네트워크 액세스 없음. Password라면 암호 변경 시의 운영을 정했는가)
  • 스크립트를 실행 계정에 해당하는 조건으로 단독 테스트했는가
  • -NoProfile -NonInteractive -ExecutionPolicy Bypass -File 형태로 호출하고 있는가
  • 스크립트 안의 경로는 $PSScriptRoot / UNC 기준인가(맵 드라이브·상대 경로에 의존하지 않는가)
  • 「시작 위치(옵션)」에 따옴표를 넣지 않았는가
  • 종료 코드를 설계했는가(성공 0 / 실패는 0이 아님. 외부 명령의 종료 코드 규약을 확인했는가)
  • 작업 기록을 사용했는가. 스크립트 자신의 로그와 실패 알림은 있는가
  • 전원 조건·StartWhenAvailable·다중 실행·실행 시간 제한을 명시적으로 설정했는가
  • 작업 정의를 XML 또는 PowerShell 스크립트로 리포지토리에 저장했는가

10. 정리

작업 스케줄러는 「스크립트를 쓰면 끝」이 아니라, 실행 계정·세션·기본값이라는 세 가지 전제를 설계해야 비로소 안정 운영에 오릅니다. 거꾸로 말하면, 이 기사에서 든 포인트 ── 로그온 종류의 선택, 기록 사용, 종료 코드와 로그 설계, 다중 실행과 시간 제한의 명시 ── 를 등록 시에 한 번 잡아 두면, 그 뒤에는 놀랄 만큼 손이 가지 않게 됩니다.

「수동이면 되는데 정기 실행이면 안 된다」는, 거의 확실하게 세션과 환경의 차이가 원인입니다. 무작정 설정을 만지기 전에, 기록 탭에서 어디까지 진행됐는지를 확인하고, 이 기사의 원인 분리 절차를 위에서부터 시험해 보십시오.

관련 기사

관련 상담 영역

合同会社小村ソフト에서는 PowerShell·작업 스케줄러에 의한 업무 자동화의 설계 리뷰나, 「움직이기는 하지만 아무도 고치지 못하는」 상태가 된 정기 작업의 재구축 상담을 다룹니다.

참고 링크

  1. Microsoft Learn, logonType Simple Type (Task Scheduler). 로그온 종류의 정의. S4U에서는 암호가 저장되지 않는 대신, 네트워크 및 암호화 파일에 액세스할 수 없다는 점에 대하여. ↩ ↩2 ↩3

  2. Microsoft Learn, Troubleshoot issues with scheduled tasks not running. 스크립트 단독 테스트 → 상태·기록 확인 → 보안 옵션 변경이라는 원인 분리 절차와, TaskScheduler Operational 이벤트 로그의 장소에 대하여. ↩ ↩2 ↩3

  3. Microsoft Learn, Task Scheduler error and success constants. SCHED_S_TASK_READY (0x41300), SCHED_S_TASK_RUNNING (0x41301), SCHED_S_TASK_HAS_NOT_RUN (0x41303) 등의 상태·오류 코드 정의에 대하여. ↩ ↩2

  4. Microsoft Learn, Manage group Managed Service Accounts. gMSA의 암호를 도메인 컨트롤러가 관리하고 호스트가 가져온다는 점, 작업 스케줄러의 작업이 gMSA를 지원한다는 점, 도메인·포리스트의 기능 수준이 Windows Server 2012 이후일 것, KDS 루트 키가 필요하다는 점(KdsSvc Operational 로그의 이벤트 ID 4004로 확인), gMSA 이름이 포리스트 단위로 고유해야 한다는 점, New-ADServiceAccount의 -PrincipalsAllowedToRetrieveManagedPassword, 각 호스트에서의 Install-ADServiceAccount와 Test-ADServiceAccount에 대하여. ↩ ↩2 ↩3 ↩4

  5. Microsoft Learn, New-ScheduledTaskSettingsSet. MultipleInstances(Parallel / Queue / IgnoreNew), StartWhenAvailable, ExecutionTimeLimit(기본값 3일) 등 작업 설정 개체의 매개변수에 대하여. ↩ ↩2 ↩3 ↩4 ↩5

  6. Microsoft Learn, Security Contexts for Tasks. 작업의 보안 컨텍스트와, Password / S4U로 등록한 작업 실행에 「배치 작업으로 로그온」 권한이 필요하다는 점에 대하여. ↩ ↩2

  7. Microsoft Learn, New-ScheduledTaskPrincipal. -UserId로 실행 계정을, -LogonType으로 로그온 방법(None / Password / S4U / Interactive / Group / ServiceAccount / InteractiveOrPassword)을 지정한다는 점, -RunLevel이 Limited와 Highest를 취한다는 점에 대하여. ↩

  8. Microsoft Learn(아카이브), General Task Registration. Microsoft-Windows-TaskScheduler의 이벤트 106(작업 등록)·113(등록됐지만 일부 트리거가 시작하지 않음)·116(구성은 저장됐지만 자격 증명을 저장할 수 없음)·140(갱신)·141(삭제)의 메시지 정의에 대하여. ↩ ↩2 ↩3 ↩4

  9. Microsoft Learn(아카이브), Task Monitoring and Control. Microsoft-Windows-TaskScheduler의 이벤트 100(작업 시작)·102(정상 종료)·111(실행 시간 초과에 의한 종료)·129(프로세스 ID가 붙은 시작)·200/201(동작의 시작·완료)·202/203(동작의 완료 실패·시작 실패)·323(새 인스턴스 시작을 위한 중지). 특히 201은 심볼 이름이 ACTION_SUCCESS이며 「Task Scheduler successfully completed task … and action …」, 202는 「Task Scheduler failed to complete the … instance of the … task with action … The error value is: …」이고, 202는 작업 스케줄러 쪽이 동작을 완료하지 못했음을 나타낸다는 점. 현재 Windows가 내는 201은 버전 2로, 본문이 「… with return code N」이 되며, 이벤트 데이터에 ResultCode를 가진다는 점(이 값이 작업의 「마지막 실행 결과」와 일치하지 않는 경우가 있다는 점을 포함)·327(배터리 전환에 의한 중지)·328(유휴가 아니게 된 것에 의한 중지)·329(시간 초과)·330(사용자 요청에 의한 중지)의 메시지 정의에 대하여. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14

  10. Microsoft Learn(아카이브), Event ID 322 — Task Properties. 이벤트 322(심볼 이름 NEW_INSTANCE_IGNORED)가 「같은 작업의 다른 인스턴스가 이미 실행 중이어서 시작하지 않았다」는 것을 나타낸다는 점, 조건·설정 재검토 절차에 대하여. ↩

  11. Microsoft Learn, What’s New in Task Scheduler. Windows 10 이후, 배터리 절약 기능이 사용되는 동안에는 대화형이 아닌 작업의 트리거가 지연된다는 점에 대하여. ↩

  12. Microsoft Learn, robocopy. 종료 코드 표(0은 복사 대상 없음, 1은 모든 파일을 정상 복사, 2 이후는 추가 파일·불일치의 조합)와, 8 이상이 복사 처리 중에 적어도 1건의 실패가 있었음을 나타낸다는 점에 대하여. ↩

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

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

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

자주 묻는 질문

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

작업 스케줄러의 「마지막 실행 결과」에 나오는 0x1은 무엇을 뜻합니까?
0x1은 작업 스케줄러 자체의 오류가 아니라, 시작한 프로그램이 종료 코드 1을 반환했다는 뜻입니다. 원인은 스크립트 쪽에 있습니다. 수동 실행과 정기 실행에서는 현재 디렉터리, 환경 변수나 프로필, 실행 정책이 다른 것이 전형적인 원인입니다. 또한 robocopy처럼 정상 시에도 1을 반환하는 외부 명령의 종료 코드 규약에도 주의가 필요합니다. 0x413xx 계통은 작업 스케줄러의 상태 코드, 0x8007xxxx는 Windows의 오류 코드라는 구분을 알아 두면, 조사할 장소를 잘못 고르지 않게 됩니다.
수동이면 되는데 작업 스케줄러에서는 안 되는 이유는 무엇입니까?
거의 확실하게 실행 계정과 세션·환경의 차이가 원인입니다. 「사용자의 로그온 여부에 관계없이 실행」을 고르면 비대화형 세션에서 움직이므로, 매핑된 네트워크 드라이브나 사용자 프로필의 환경 변수가 존재하지 않습니다. 나아가 「암호를 저장하지 않음」(S4U)에서는 네트워크상의 리소스에 액세스할 수 없습니다. 원인 분리는 기록 탭 확인, 스크립트 단독 테스트, 「로그온할 때만 실행」으로의 임시 변경이라는 절차가 유효합니다. 작업 기록은 기본값이 사용 안 함이므로, 운영 전에 반드시 사용하도록 설정하십시오.
작업 스케줄러에서 PowerShell 스크립트를 호출하는 올바른 작성법은?
프로그램에 pwsh.exe(또는 powershell.exe)를 지정하고, 인수는 -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "전체 경로" 형태가 기본입니다. -Command가 아니라 -File을 쓰면, 스크립트의 exit n이 그대로 프로세스의 종료 코드가 되어 성패를 판별할 수 있습니다. 스크립트 안의 경로는 $PSScriptRoot 기준으로 하고, 「시작 위치(옵션)」란에는 따옴표를 붙이면 안 됩니다(붙이면 0x8007010B로 실패합니다). 스크립트 쪽에서는 성공이면 0, 실패이면 0이 아닌 값을 명시적으로 반환하도록 설계합니다.
암호 변경으로 작업이 멈추는 사고는 어떻게 막습니까?
「로그온 여부에 관계없이 실행」으로 등록하면 암호가 저장되므로, 암호 변경 후에는 로그온 실패(0x8007052E)로 계속 멈춥니다. 도메인 환경이라면 암호를 도메인 컨트롤러가 자동 관리하는 gMSA(그룹 관리 서비스 계정)가 제1 후보이며, 이 문제 자체가 사라집니다. 전용 서비스 계정을 쓰는 경우에는 암호 유효 기간과 작업 점검을 세트로 관리하십시오. 더불어 실패 시 메일이나 Teams로 알리는 구조를 넣어 두면, 몇 달 동안 멈춰 있던 것을 알아채지 못하는 사고를 막을 수 있습니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기