PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화하기

· · PowerShell, Windows, 자동화, 로그 조사, 운영 개선, 기존 자산 활용

1. 최초에 파악해야 할 것

이전 글에서는 PowerShell의 기본 명령, 파이프라인, CSV 출력, JSON, .ps1 스크립트, -WhatIf를 이용한 안전 확인을 정리했습니다. 이번에는 그 후속으로, 실무에서 쓰기 좋은 조금 더 큰 스크립트를 만듭니다. 다루는 주제는 다음과 같은 운영 작업입니다.

  1. 로그를 조사한다
  2. 에러 행을 CSV로 정리한다
  3. 오래된 로그를 아카이브 후보로 목록화한다
  4. 필요하면 오래된 로그를 이동한다
  5. 실행 결과를 증적으로 남긴다

PowerShell의 응용에서 중요한 것은 명령을 길게 만드는 것이 아니라, 다음과 같은 흐름을 만드는 것입니다.

  1. 설정을 분리한다
  2. 읽기 처리를 만든다
  3. 출력을 남긴다
  4. 변경 처리를 함수로 분리한다
  5. -WhatIf로 예행연습을 한다
  6. 마지막으로 자동 실행을 검토한다

자동화란 「사람의 확인을 없애는 것」이 아닙니다. 확인해야 할 지점을 고정하고, 매번 같은 증적이 남도록 하는 것입니다.

또한, 이 글에 나오는 코드는 그대로 실행할 수 있는 샘플 일체(완성판 스크립트, 설정 파일, 더미 로그 환경 생성 스크립트, 조사·예행·이동을 검증하는 Pester 테스트)로 GitHub에 공개하고 있습니다.

powershell-script-log-maintenance-automation - komurasoft-blog-samples (GitHub)

2. 이번에 만들 것

이번에는 Invoke-LogMaintenance.ps1이라는 스크립트를 만듭니다.

주요 기능은 다음과 같습니다.

기능 내용
로그 검색 지정 폴더 아래의 .log를 검색
기간 지정 최근 N일 이내에 갱신된 로그만 조사
에러 추출 ERROR, WARN, FATAL 등의 행을 추출
CSV 출력 검색 결과를 log-hits.csv에 저장
오래된 로그 목록 N일보다 오래된 로그를 archive-targets.csv에 저장
아카이브 오래된 로그를 다른 폴더로 이동
예행 실행 -Preview 지정 시 이동하지 않고 예정만 표시
실행 기록 transcript, summary JSON, 결과 CSV를 저장

또한, 삭제는 하지 않습니다. 첫 응용편에서는 삭제보다 안전한 「이동」까지로 제한합니다.

3. 폴더 구성

예로, 다음과 같은 구성으로 합니다.

C:\Ops
  Invoke-LogMaintenance.ps1
  log-maintenance.json

C:\App\Logs
  app.log
  batch.log
  old
    app-202401.log

C:\App\Reports
  20260602-030000
    log-hits.csv
    archive-targets.csv
    archive-result.csv
    summary.json
    transcript.txt

C:\App\Archive
  20260602-030000
    old
      app-202401.log

스크립트 본체와 설정 파일을 분리해두면, 환경별 교체가 쉬워집니다. 개발 환경에서는 C:\Test\Logs, 운영 환경에서는 D:\App\Logs처럼 경로만 바꾸는 방식으로 운용할 수 있습니다.

4. 설정 파일을 만든다

먼저 log-maintenance.json을 만듭니다.

{
  "LogPath": "C:\\App\\Logs",
  "OutputPath": "C:\\App\\Reports",
  "Days": 7,
  "Patterns": [
    "ERROR",
    "WARN",
    "FATAL"
  ],
  "ArchiveDays": 90,
  "ArchivePath": "C:\\App\\Archive"
}

의미는 다음과 같습니다.

항목 의미
LogPath 조사 대상 로그 폴더
OutputPath 리포트 출력 위치
Days 최근 며칠분의 로그를 조사할지
Patterns 검색할 문자열·패턴
ArchiveDays 며칠보다 오래된 로그를 아카이브 대상으로 할지
ArchivePath 아카이브 대상 폴더

JSON으로 해두면, 스크립트 본체를 편집하지 않고도 조건을 변경할 수 있습니다.

PowerShell에서는 ConvertFrom-Json으로 JSON을 객체로 다룰 수 있습니다. 반대로, 처리 결과를 JSON으로 남길 때는 ConvertTo-Json을 사용합니다. ConvertTo-Json은 객체를 JSON 문자열로 변환하는 커맨드릿으로, 깊은 계층을 다룰 때는 -Depth 지정이 중요해집니다.

5. 먼저 읽기만 만든다

처음부터 이동 처리를 작성하지 않고, 처음에는 로그를 찾아서 CSV로 만드는 것만 합니다.

$config = Get-Content .\log-maintenance.json -Raw -Encoding UTF8 | ConvertFrom-Json

$since = (Get-Date).AddDays(-[int]$config.Days)

$files = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
  Where-Object { $_.LastWriteTime -ge $since }

$files |
  Select-Object FullName, Length, LastWriteTime

다음으로, 로그의 내용을 검색합니다.

$patterns = [string[]]$config.Patterns

Select-String -LiteralPath ($files | Select-Object -ExpandProperty FullName) -Pattern $patterns |
  Select-Object Path, LineNumber, Pattern, Line |
  Export-Csv .\log-hits.csv -NoTypeInformation -Encoding UTF8

여기까지는 읽기만입니다. 운영 폴더에서 시험할 때도, 먼저 이 단계에서 멈춥니다.

6. 오래된 로그를 목록화한다

다음으로, 아카이브 대상을 목록화합니다.

$limit = (Get-Date).AddDays(-[int]$config.ArchiveDays)

$targets = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
  Where-Object { $_.LastWriteTime -lt $limit } |
  Sort-Object LastWriteTime

$targets |
  Select-Object FullName, Length, LastWriteTime |
  Export-Csv .\archive-targets.csv -NoTypeInformation -Encoding UTF8

이 단계에서도, 아직 이동하지 않습니다. archive-targets.csv를 보고, 대상이 너무 많지 않은지, 폴더가 잘못되지 않았는지를 확인합니다.

7. 변경 처리는 함수로 분리한다

이동과 같은 변경 처리는, 읽기 처리와 분리합니다.

PowerShell에서는, 함수에 SupportsShouldProcess를 붙이면 -WhatIf-Confirm을 다룰 수 있게 됩니다. -WhatIf는 실행하지 않고 「무엇을 변경할 예정인지」를 표시하고, -Confirm은 실행 전에 확인을 표시하기 위한 방법입니다. 자세한 내용은 Microsoft Learn의 about_Functions_CmdletBindingAttribute에서 확인할 수 있습니다.

function Move-OldLogFile {
  [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
  param(
    [Parameter(Mandatory)]
    [System.IO.FileInfo[]]$File,

    [Parameter(Mandatory)]
    [string]$SourceRoot,

    [Parameter(Mandatory)]
    [string]$ArchiveRoot
  )

  foreach ($item in $File) {
    $relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
    $destination = Join-Path $ArchiveRoot $relativePath
    $destinationDirectory = Split-Path -Path $destination -Parent

    if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
      if (-not [System.IO.Directory]::Exists($destinationDirectory)) {
        [System.IO.Directory]::CreateDirectory($destinationDirectory) | Out-Null
      }

      Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop

      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Moved"
        Message     = ""
      }
    }
    else {
      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Preview"
        Message     = ""
      }
    }
  }
}

포인트는, Move-Item 직전에 $PSCmdlet.ShouldProcess()를 두었다는 것입니다. 변경할지 여부의 판단은, 함수 바깥이 아니라 변경하기 직전에 둡니다.

8. 완성판 스크립트

지금까지의 내용을 정리한 완성판입니다. 파일명은 Invoke-LogMaintenance.ps1로 합니다.

# Invoke-LogMaintenance.ps1
#Requires -Version 7.0

[CmdletBinding()]
param(
  [ValidateNotNullOrEmpty()]
  [string]$ConfigPath = ".\log-maintenance.json",

  [switch]$Preview,

  [switch]$SkipArchive,

  [switch]$SkipTranscript
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Ensure-Directory {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$Path
  )

  if (-not [System.IO.Directory]::Exists($Path)) {
    [System.IO.Directory]::CreateDirectory($Path) | Out-Null
  }
}

function Import-LogMaintenanceConfig {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$Path
  )

  if (-not (Test-Path -LiteralPath $Path)) {
    throw "Config file not found: $Path"
  }

  $config = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json

  foreach ($name in @("LogPath", "OutputPath", "Days", "Patterns", "ArchiveDays", "ArchivePath")) {
    if (-not ($config.PSObject.Properties.Name -contains $name)) {
      throw "Config value missing: $name"
    }
  }

  if ([string]::IsNullOrWhiteSpace([string]$config.LogPath)) {
    throw "LogPath is empty."
  }

  if (-not (Test-Path -LiteralPath $config.LogPath)) {
    throw "LogPath not found: $($config.LogPath)"
  }

  if ([string]::IsNullOrWhiteSpace([string]$config.OutputPath)) {
    throw "OutputPath is empty."
  }

  if ([string]::IsNullOrWhiteSpace([string]$config.ArchivePath)) {
    throw "ArchivePath is empty."
  }

  if (@($config.Patterns).Count -eq 0) {
    throw "Patterns is empty."
  }

  if ([int]$config.Days -lt 1) {
    throw "Days must be 1 or greater."
  }

  if ([int]$config.ArchiveDays -lt 1) {
    throw "ArchiveDays must be 1 or greater."
  }

  return $config
}

function Export-CsvWithHeader {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [object[]]$InputObject,

    [Parameter(Mandatory)]
    [string]$Path,

    [Parameter(Mandatory)]
    [string[]]$Header
  )

  if ($InputObject.Count -gt 0) {
    $InputObject |
      Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8
  }
  else {
    ($Header -join ",") |
      Set-Content -LiteralPath $Path -Encoding UTF8
  }
}

function Get-LogHit {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$LogPath,

    [Parameter(Mandatory)]
    [ValidateRange(1, 3650)]
    [int]$Days,

    [Parameter(Mandatory)]
    [string[]]$Pattern
  )

  $since = (Get-Date).AddDays(-$Days)

  $files = @(
    Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
      Where-Object { $_.LastWriteTime -ge $since }
  )

  Write-Verbose "Recent log files: $($files.Count)"

  if ($files.Count -eq 0) {
    return @()
  }

  $paths = $files | Select-Object -ExpandProperty FullName

  Select-String -LiteralPath $paths -Pattern $Pattern -ErrorAction Stop |
    ForEach-Object {
      [pscustomobject]@{
        Path       = $_.Path
        LineNumber = $_.LineNumber
        Pattern    = $_.Pattern
        Line       = $_.Line.Trim()
      }
    }
}

function Get-OldLogFile {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$LogPath,

    [Parameter(Mandatory)]
    [ValidateRange(1, 3650)]
    [int]$ArchiveDays
  )

  $limit = (Get-Date).AddDays(-$ArchiveDays)

  Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
    Where-Object { $_.LastWriteTime -lt $limit } |
    Sort-Object LastWriteTime
}

function Move-OldLogFile {
  [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
  param(
    [Parameter(Mandatory)]
    [System.IO.FileInfo[]]$File,

    [Parameter(Mandatory)]
    [string]$SourceRoot,

    [Parameter(Mandatory)]
    [string]$ArchiveRoot
  )

  foreach ($item in $File) {
    $relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
    $destination = Join-Path $ArchiveRoot $relativePath
    $destinationDirectory = Split-Path -Path $destination -Parent

    if (Test-Path -LiteralPath $destination) {
      $name = [System.IO.Path]::GetFileNameWithoutExtension($item.Name)
      $ext = $item.Extension
      $destination = Join-Path $destinationDirectory ("{0}_{1:yyyyMMddHHmmss}{2}" -f $name, $item.LastWriteTime, $ext)
    }

    if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
      Ensure-Directory -Path $destinationDirectory

      Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop

      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Moved"
        Message     = ""
      }
    }
    else {
      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Preview"
        Message     = ""
      }
    }
  }
}

$config = Import-LogMaintenanceConfig -Path $ConfigPath

$runStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$reportDir = Join-Path ([string]$config.OutputPath) $runStamp
Ensure-Directory -Path $reportDir

$transcriptStarted = $false
$transcriptPath = Join-Path $reportDir "transcript.txt"

try {
  if (-not $SkipTranscript) {
    Start-Transcript -Path $transcriptPath -Force | Out-Null
    $transcriptStarted = $true
  }

  Write-Host "Report directory: $reportDir"

  $hits = @(
    Get-LogHit `
      -LogPath ([string]$config.LogPath) `
      -Days ([int]$config.Days) `
      -Pattern ([string[]]$config.Patterns)
  )

  $hitCsv = Join-Path $reportDir "log-hits.csv"
  Export-CsvWithHeader `
    -InputObject $hits `
    -Path $hitCsv `
    -Header @("Path", "LineNumber", "Pattern", "Line")

  $oldFiles = @(
    Get-OldLogFile `
      -LogPath ([string]$config.LogPath) `
      -ArchiveDays ([int]$config.ArchiveDays)
  )

  $archiveTargets = @(
    $oldFiles |
      Select-Object FullName, Length, LastWriteTime
  )

  $archiveTargetCsv = Join-Path $reportDir "archive-targets.csv"
  Export-CsvWithHeader `
    -InputObject $archiveTargets `
    -Path $archiveTargetCsv `
    -Header @("FullName", "Length", "LastWriteTime")

  $moveResults = @()

  if ($SkipArchive) {
    Write-Host "Archive skipped."
  }
  elseif ($oldFiles.Count -eq 0) {
    Write-Host "No archive targets."
  }
  else {
    $archiveRunRoot = Join-Path ([string]$config.ArchivePath) $runStamp

    $moveResults = @(
      Move-OldLogFile `
        -File $oldFiles `
        -SourceRoot ([string]$config.LogPath) `
        -ArchiveRoot $archiveRunRoot `
        -WhatIf:$Preview
    )
  }

  $archiveResultCsv = Join-Path $reportDir "archive-result.csv"
  Export-CsvWithHeader `
    -InputObject $moveResults `
    -Path $archiveResultCsv `
    -Header @("Source", "Destination", "Status", "Message")

  $summary = [pscustomobject]@{
    CheckedAt          = (Get-Date).ToString("s")
    ComputerName       = $env:COMPUTERNAME
    LogPath            = [string]$config.LogPath
    ReportDirectory    = $reportDir
    HitCount           = $hits.Count
    ArchiveTargetCount = $oldFiles.Count
    ArchiveResultCount = $moveResults.Count
    Preview            = [bool]$Preview
    SkipArchive        = [bool]$SkipArchive
  }

  $summaryPath = Join-Path $reportDir "summary.json"
  $summary |
    ConvertTo-Json -Depth 5 |
    Set-Content -LiteralPath $summaryPath -Encoding UTF8

  Write-Host "Finished."
  Write-Host "Hits: $($hits.Count)"
  Write-Host "Archive targets: $($oldFiles.Count)"
}
catch {
  $errorPath = Join-Path $reportDir "error.txt"
  $_ | Out-String | Set-Content -LiteralPath $errorPath -Encoding UTF8
  Write-Error "Failed: $($_.Exception.Message)"
  exit 1
}
finally {
  if ($transcriptStarted) {
    Stop-Transcript | Out-Null
  }
}

9. 실행 예

먼저, 아카이브를 실행하지 않고 로그 조사만 수행합니다.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -SkipArchive

다음으로, 아카이브 예정을 확인합니다.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview

-Preview를 붙이면, 오래된 로그의 이동 처리는 -WhatIf로 취급됩니다.

이 시점에서 확인할 파일은 다음 3가지입니다.

  • log-hits.csv
  • archive-targets.csv
  • archive-result.csv

문제가 없으면, -Preview를 빼고 실행합니다.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json

자세한 내용을 보고 싶다면 -Verbose를 붙입니다.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview -Verbose

10. 출력되는 파일

실행하면, OutputPath 아래에 C:\App\Reports\20260602-030000와 같은 날짜·시간이 붙은 폴더가 만들어집니다.

안에는 다음 파일이 출력됩니다.

파일 내용
log-hits.csv 에러·경고로 검출된 행
archive-targets.csv 아카이브 대상이 된 오래된 로그
archive-result.csv 이동 결과, 또는 Preview 결과
summary.json 건수나 실행 조건의 요약
transcript.txt PowerShell 세션의 기록
error.txt 에러 발생 시의 상세

Start-Transcript는, PowerShell 세션의 명령과 콘솔 출력을 텍스트 파일로 기록하기 위한 커맨드릿입니다. 운영 스크립트에서는, 나중에 「언제·어떤 조건으로·무엇이 나왔는지」를 확인하기 쉬워집니다.

11. 작업 스케줄러로 정기 실행한다

수동 실행에서 문제가 없다면, 작업 스케줄러로 정기 실행할 수 있습니다.

처음은 매일 새벽 3시에 실행하는 예입니다.

$scriptPath = "C:\Ops\Invoke-LogMaintenance.ps1"
$configPath = "C:\Ops\log-maintenance.json"

$action = New-ScheduledTaskAction `
  -Execute "pwsh.exe" `
  -Argument "-NoProfile -File `"$scriptPath`" -ConfigPath `"$configPath`"" `
  -WorkingDirectory "C:\Ops"

$trigger = New-ScheduledTaskTrigger -Daily -At 3:00

Register-ScheduledTask `
  -TaskName "AppLogMaintenance" `
  -Action $action `
  -Trigger $trigger `
  -Description "Collect app log errors and archive old logs"

New-ScheduledTaskAction은 작업이 실행할 명령을 나타내는 객체를 만들고, New-ScheduledTaskTrigger는 매일·매주·로그온 시 등의 시작 조건을 만듭니다. 마지막으로 Register-ScheduledTask로 로컬 컴퓨터에 작업을 등록합니다.

운영 환경에서는, 다음 사항도 확인합니다.

  • 실행 사용자에게 로그 폴더의 읽기 권한이 있다
  • 아카이브 대상 폴더로의 쓰기 권한이 있다
  • pwsh.exe의 경로가 설정되어 있다
  • 스크립트의 실행 정책이나 서명 규칙에 맞는다
  • 수동 실행과 작업 실행에서 같은 결과가 된다
  • 실패 시에 error.txt나 작업 기록을 확인할 수 있다

12. 자주 겪는 문제

증상 원인 대처
로그를 찾을 수 없다 LogPath가 잘못되었다 Test-PathGet-ChildItem으로 확인한다
CSV가 비어 있다 대상 기간에 해당하는 로그가 없다 Days를 넓혀서 확인한다
텍스트가 깨진다 로그의 문자 인코딩이 예상과 다르다 입출력 문자 인코딩을 확인한다
작업에서는 동작하지 않는다 실행 사용자나 작업 폴더가 다르다 WorkingDirectory와 권한을 확인한다
아카이브 대상이 너무 많다 ArchiveDays가 너무 짧다 archive-targets.csv를 보고 조정한다
이동 위치가 예상과 다르다 상대 경로 유지 규칙을 이해하지 못했다 -PreviewDestination을 확인한다
운영 환경에서만 실패한다 권한·정책·잠긴 파일의 차이 error.txttranscript.txt를 확인한다

특히, 작업 스케줄러로 실행할 경우 「직접 실행했을 때」와 「작업의 실행 사용자」가 다른 경우가 있습니다. 수동으로는 동작하는데 작업에서는 실패한다면, 먼저 권한과 작업 폴더를 의심해 보세요.

13. 개조할 때의 사고방식

이 스크립트는, 그대로 쓰기보다 현장에 맞춰 조금씩 개조하는 것을 전제로 합니다. 흔한 개조 예를 들어봅니다.

하고 싶은 것 개조 위치
.txt도 대상으로 하고 싶다 Get-ChildItem -Filter *.log를 변경
ERROR 전후 몇 행도 보고 싶다 Select-String의 결과를 바탕으로 Get-Content로 주변 행을 가져온다
압축한 뒤 아카이브하고 싶다 Move-OldLogFile 앞에 Compress-Archive를 추가
메일 알림을 하고 싶다 summary.json을 바탕으로 알림 처리를 추가
앱마다 설정을 나누고 싶다 JSON을 여러 개 준비해 작업을 나눈다
삭제까지 자동화하고 싶다 먼저 이동 운용으로 일정 기간 확인한 뒤 검토

다만, 처음부터 전부 넣지 않는 것이 안전합니다. 운영 스크립트는, 기능이 많은 것보다 실패했을 때 추적할 수 있는 것이 중요합니다.

14. 현장 운영 체크리스트

PowerShell 스크립트를 정기 실행하기 전에, 다음 사항을 확인합니다.

  • 먼저 -SkipArchive로 읽기 처리만 실행했다
  • 다음으로 -Preview로 이동 예정을 확인했다
  • archive-targets.csv의 대상이 타당했다
  • archive-result.csvDestination이 예상대로였다
  • 출력 위치 폴더에 날짜·시간이 붙은 증적이 남았다
  • 에러 시에 error.txt가 남는 것을 확인했다
  • 작업 실행 사용자의 권한을 확인했다
  • 실행 정책, 서명, 회사 내부 규칙을 확인했다
  • 바로 삭제하지 않고, 먼저 이동으로 운용한다
  • 복구할 경우의 되돌릴 위치를 정해두었다

15. 정리

PowerShell의 응용이라고 해도, 어려운 문법을 많이 쓸 필요는 없습니다. 실무에서 효과가 있는 것은, 이런 형태를 만들어 두는 것입니다.

  • 설정을 JSON으로 분리한다
  • 읽기 처리를 먼저 만든다
  • CSV와 JSON으로 증적을 남긴다
  • 변경 처리는 함수로 분리한다
  • -WhatIf에 해당하는 예행연습을 준비한다
  • transcript와 error.txt로 추적할 수 있게 한다
  • 수동 실행으로 확인한 뒤 정기 실행한다

이번 스크립트는, 로그 조사와 아카이브를 주제로 하고 있지만, 사고방식은 다른 업무에도 사용할 수 있습니다.

  • 파일 정리
  • 보고서 출력
  • CSV 집계
  • 배치 교체
  • 오래된 자산의 재고 조사
  • 일일·월간 운영 확인

PowerShell은 한 줄 명령으로도 편리하지만, 업무에서 사용한다면, 다음 순서를 지키는 것이 더 안전합니다.

본다 → 기록한다 → 예행한다 → 실행한다 → 증적을 남긴다

이 형태로 해두면, PowerShell은 단순한 작업 단축 도구가 아니라, 운영을 안정시키기 위한 작은 업무 애플리케이션으로 사용할 수 있게 됩니다.

참고 링크

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

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

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

자주 묻는 질문

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

PowerShell로 로그 조사를 자동화할 때 무엇부터 만들기 시작하면 좋을까요?
처음부터 이동이나 삭제 같은 변경 처리를 작성하지 말고, 먼저 읽기 처리만 만듭니다. Get-ChildItem으로 대상 로그를 찾고, Select-String으로 에러 행을 추출해 CSV로 출력하는 부분까지 먼저 완성합니다. 운영 폴더에서 시험할 때도 이 읽기 단계에서 한 번 멈추고 결과를 확인합니다. 그 후에 아카이브 대상 목록화, 이동 처리의 함수화로 단계적으로 진행하는 것이 안전합니다.
PowerShell의 -WhatIf는 스크립트에 어떻게 넣나요?
함수의 CmdletBinding 속성에 SupportsShouldProcess를 붙이면, 그 함수에서 -WhatIf와 -Confirm을 사용할 수 있게 됩니다. 실제로 변경하는 처리(Move-Item 등) 직전에 $PSCmdlet.ShouldProcess()를 두어, 변경할지 여부의 판단을 변경 직전에 하는 것이 포인트입니다. 이 글의 스크립트에서는 -Preview 스위치를 -WhatIf:$Preview로 전달하여, 이동하지 않고 예정만 확인할 수 있게 하고 있습니다.
PowerShell 스크립트가 수동으로는 동작하는데 작업 스케줄러에서는 실패하는 이유는 무엇인가요?
직접 실행했을 때와 작업의 실행 사용자가 다른 것이 대표적인 원인입니다. 실행 사용자에게 로그 폴더의 읽기 권한이나 아카이브 대상 폴더로의 쓰기 권한이 있는지, 작업 폴더(WorkingDirectory)가 올바른지, pwsh.exe의 경로가 설정되어 있는지, 실행 정책이나 서명 규칙에 맞는지를 확인합니다. 실패 시에는 error.txt나 transcript.txt, 작업 기록으로 추적할 수 있도록 해두는 것이 중요합니다.
오래된 로그의 삭제까지 자동화해도 되나요?
처음부터 삭제를 자동화하는 것은 권장하지 않습니다. 이 글의 스크립트도 삭제가 아니라 안전한 「이동」까지로 제한하고 있습니다. 먼저 이동 운용으로 일정 기간 확인하고, archive-targets.csv나 archive-result.csv 같은 증적을 보고 문제가 없다고 판단된 후에 삭제 자동화를 검토합니다. 복구할 경우의 되돌릴 위치를 사전에 정해두는 것도 중요합니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기