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

· · 작업 스케줄러, Windows, PowerShell, 업무 자동화, 배치 처리, 운영, 트러블슈팅, 기술 상담

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

이 블로그에서도 로그 정리 자동화, Pester를 통한 스크립트 테스트, C#에서의 PowerShell 실행, Power Automate를 통한 업무 자동화까지, 자동화 이야기를 계속해서 다뤄 왔습니다. 이 글들은 모두 최종적으로 「작업 스케줄러로 정기 실행한다」는 것을 전제로 합니다. 그런데 그 작업 스케줄러 자체가 의외로 까다로운 특성을 가진 구조여서, 「수동으로는 동작하는데 정기 실행에서는 실패한다」「어느샌가 멈춰 있었는데 아무도 알아차리지 못했다」와 같은 사고가 끊이지 않습니다.

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

1. 먼저 결론

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

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에는 트리거・조건・설정이 모두 포함되어 있으므로, 저장소에 넣어 두면 작업 정의의 리뷰와 변경 사항 관리도 할 수 있습니다. 반대로, 수작업으로 10대에 같은 작업을 GUI로 등록하는 운영 방식은 1대만 설정이 다른 「미아 작업」을 반드시 만들어 냅니다. 대수가 두 자리가 되기 전에 코드화해 두는 것이 안전합니다.

덧붙여, 이 글의 내용은 Windows 10 / 11, Windows Server 2016 이후의 작업 스케줄러(Task Scheduler 2.0 계열)를 전제로 합니다. 더 오래된 at 명령에서 유래한 작업이 남아 있는 환경에서는, 먼저 그것들을 점검하는 것부터 시작하십시오.

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(그룹 관리 서비스 계정): 도메인 환경이라면 첫 번째 후보입니다. 암호는 도메인 컨트롤러가 자동으로 관리하기 때문에, 「암호 변경으로 작업이 죽는」 문제 자체가 사라집니다. 작업 스케줄러는 gMSA를 통한 실행을 지원합니다.4

또한 「최고 권한으로 실행」 체크는 UAC로 분리된 토큰 중 관리자 쪽(상승된)토큰으로 실행한다는 의미입니다. 관리자 권한이 필요 없는 작업에서는 체크하지 마십시오.

4. 「실행되지 않는」 문제를 원인 분리하는 절차

4.1 기록을 활성화한 뒤 의심한다

작업 스케줄러 오른쪽 창에 있는 「모든 작업 기록 사용」은 기본적으로 비활성화되어 있습니다. 기록이 비활성화된 상태로 두면, 실패했다는 사실조차 기록에 남지 않습니다. 운영에 올리기 전에 반드시 활성화하고, 이벤트 로그(이벤트 뷰어 → 응용 프로그램 및 서비스 로그 → Microsoft → Windows → TaskScheduler → Operational)와 함께 확인할 수 있는 상태로 만들어 둡니다.2

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

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

4.2 「마지막 실행 결과」를 읽는 방법

표시 의미
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 이 구분이 되면, 조사할 곳(작업 설정인지, 스크립트인지)을 처음부터 잘못 짚지 않게 됩니다.

4.3 조건・설정의 기본값에 주의

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

4.4 트리거 설계 자체에 대한 주의

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

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

5. 0x1로 끝나는 전형적인 패턴과 PowerShell을 올바르게 호출하는 방법

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

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

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이 아닌 값을 명시적으로 반환하도록 설계해 둡니다.

# 명령의 "비종료 오류"도 catch로 떨어지도록 Stop으로 설정한다.
# 이것이 없으면 Copy-Item 등의 실패가 그대로 통과해 exit 0이 될 수 있다
$ErrorActionPreference = 'Stop'

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

예외를 어디서 잡고 어떻게 기록할지에 대한 사고방식은 다른 글 「예외 처리 catch 위치와 로그 설계」에서도 자세히 다루었습니다.

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
}

두 가지를 보충합니다. 먼저, 이벤트 소스 등록(CreateEventSource)에는 관리자 권한이 필요합니다. 작업 본체에 등록 처리를 섞으면, 최소 권한 서비스 계정으로 동작하는 프로덕션 환경에서 첫 실패 시 「등록을 시도하다 예외 발생 → 정작 중요한 이벤트를 기록할 수 없음」이라는 이중 실패가 됩니다. 위와 같이 등록은 설정 단계로 분리하고, 실행 시에는 기록만 하도록 하십시오. 다음으로, 이 글의 작업 등록 예시처럼 실행 엔진을 pwsh.exe로 하는 경우, Windows PowerShell 5.1 시절의 New-EventLog / Write-EventLog 명령은 사용할 수 없습니다(명령을 찾지 못해 스크립트 자체가 실패합니다). 위와 같이 .NET 클래스를 직접 호출하는 방식이라면 5.1 / 7 어느 쪽에서도 동작합니다.

이와 함께 「실패하면 사람에게 전달되는」 체계──이메일이나 Teams / Slack 알림──를 하나 넣어 두면, 「몇 개월째 멈춰 있었던 것을 점검 중에 알아차리는」 사고를 막을 수 있습니다. 정교한 알림 기반이 필요한 것은 아니며, 실패 시에만 Webhook에 POST하는 몇 줄이면 충분히 기능합니다. 반대로 「성공할 때마다 알림」은 머지않아 읽히지 않게 되므로, 성공은 주간 요약 정도로 억제하고, 실패와 「실행되지 않음」(마지막 실행 시각이 오래됨)을 감지 대상으로 삼는 것을 추천합니다. 로그에 무엇을 기록해야 하는지에 대한 기준은 「예외 처리 catch 위치와 로그 설계」에서도 정리했습니다.

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

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

설정값 동작 적합한 용도
IgnoreNew(GUI 기본값: 새 인스턴스를 시작하지 않음) 실행 중이면 새 시작을 건너뜀 멱등적인 정기 배치 전반. 우선 이것
Queue 실행 중이면 종료 후 순서대로 실행 놓치면 안 되는 집계 계열
Parallel 병행하여 시작 원칙적으로 피함. 병행 안전성이 보장되는 경우에만

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

주의해야 할 것은, IgnoreNewQueue가 지켜주는 것은 같은 작업 정의 안에서만이라는 점입니다. 다른 작업이 같은 스크립트를 호출하는 경우나, 장애 대응으로 사람이 수동 실행한 경우의 충돌까지는 막지 못합니다. 같은 리소스(파일, DB, 외부 시스템)를 다루는 경로가 여러 개 있다면, 스크립트 쪽에도 상호 배제를 두어야 합니다. 정석은 이름이 지정된 뮤텍스입니다.

$mutex = New-Object System.Threading.Mutex($false, 'Global\KS-Cleanup-Logs')
if (-not $mutex.WaitOne(0)) {
    Write-Warning '다른 인스턴스가 실행 중이므로 종료합니다.'
    exit 0   # "실행하지 않음"을 실패로 취급하지 않을 경우 0, 실패로 취급할 경우 0이 아닌 값으로
}
try {
    # 본처리
}
finally {
    $mutex.ReleaseMutex()
}

이름 앞에 Global\을 붙이면, 다른 세션(다른 사용자의 작업과 수동 실행 등)간에도 상호 배제가 적용됩니다. 잠금 획득을 기다릴지(WaitOne에 시간 제한을 전달), 즉시 포기할지는 작업의 성격에 맞추어 결정하십시오. 다만 Global\이 붙은 이름 지정 객체는 머신상의 누구에게나 보인다는 점에 주의가 필요합니다. 여러 사용자가 로그온하는 공유 서버에서는, 악의나 사고로 같은 이름의 뮤텍스를 먼저 선점당하면, 작업이 영원히 계속 건너뛰어지는(게다가 exit 0이라면 정상처럼 보이는)상황이 됩니다. 그러한 환경에서는 뮤텍스에 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. 정리

작업 스케줄러는 「스크립트를 작성하면 끝」이 아니라, 실행 계정・세션・기본값이라는 3가지 전제를 설계해야 비로소 안정적인 운영에 오릅니다. 반대로 말하면, 이 글에서 다룬 포인트──로그온 종류의 선택, 기록 활성화, 종료 코드와 로그의 설계, 중복 실행과 시간 제한의 명시──를 등록 시 한 번 짚어 두면, 그 이후로는 놀라울 정도로 손이 가지 않게 됩니다.

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

관련 기사

관련 상담 영역

합동회사 코무라소프트에서는 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를 지원한다는 점에 대해.  2

  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로 등록한 작업의 실행에 「배치 작업으로 로그온」권한이 필요하다는 점에 대해. 

  7. Microsoft Learn, What’s New in Task Scheduler. Windows 10 이후로 배터리 절약 기능이 활성화된 동안 대화형이 아닌 작업의 트리거가 지연된다는 점에 대해. 

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

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

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

자주 묻는 질문

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

작업 스케줄러의 「마지막 실행 결과」에 표시되는 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(그룹 관리 서비스 계정)가 첫 번째 후보이며, 이 문제 자체가 사라집니다. 전용 서비스 계정을 사용하는 경우에는 암호 유효기간과 작업 점검을 함께 관리하십시오. 아울러 실패 시 이메일이나 Teams로 알리는 체계를 넣어 두면, 몇 개월째 멈춰 있었다는 사실을 알아차리지 못하는 사고를 막을 수 있습니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기