PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화하기
· 小村 豪 · PowerShell, Windows, 자동화, 로그 조사, 운영 개선, 기존 자산 활용
1. 최초에 파악해야 할 것
이전 글에서는 PowerShell의 기본 명령, 파이프라인, CSV 출력, JSON, .ps1 스크립트, -WhatIf를 이용한 안전 확인을 정리했습니다. 이번에는 그 후속으로, 실무에서 쓰기 좋은 조금 더 큰 스크립트를 만듭니다. 다루는 주제는 다음과 같은 운영 작업입니다.
- 로그를 조사한다
- 에러 행을 CSV로 정리한다
- 오래된 로그를 아카이브 후보로 목록화한다
- 필요하면 오래된 로그를 이동한다
- 실행 결과를 증적으로 남긴다
PowerShell의 응용에서 중요한 것은 명령을 길게 만드는 것이 아니라, 다음과 같은 흐름을 만드는 것입니다.
- 설정을 분리한다
- 읽기 처리를 만든다
- 출력을 남긴다
- 변경 처리를 함수로 분리한다
-WhatIf로 예행연습을 한다- 마지막으로 자동 실행을 검토한다
자동화란 「사람의 확인을 없애는 것」이 아닙니다. 확인해야 할 지점을 고정하고, 매번 같은 증적이 남도록 하는 것입니다.
또한, 이 글에 나오는 코드는 그대로 실행할 수 있는 샘플 일체(완성판 스크립트, 설정 파일, 더미 로그 환경 생성 스크립트, 조사·예행·이동을 검증하는 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.csvarchive-targets.csvarchive-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-Path와 Get-ChildItem으로 확인한다 |
| CSV가 비어 있다 | 대상 기간에 해당하는 로그가 없다 | Days를 넓혀서 확인한다 |
| 텍스트가 깨진다 | 로그의 문자 인코딩이 예상과 다르다 | 입출력 문자 인코딩을 확인한다 |
| 작업에서는 동작하지 않는다 | 실행 사용자나 작업 폴더가 다르다 | WorkingDirectory와 권한을 확인한다 |
| 아카이브 대상이 너무 많다 | ArchiveDays가 너무 짧다 |
archive-targets.csv를 보고 조정한다 |
| 이동 위치가 예상과 다르다 | 상대 경로 유지 규칙을 이해하지 못했다 | -Preview로 Destination을 확인한다 |
| 운영 환경에서만 실패한다 | 권한·정책·잠긴 파일의 차이 | error.txt와 transcript.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.csv의Destination이 예상대로였다- 출력 위치 폴더에 날짜·시간이 붙은 증적이 남았다
- 에러 시에
error.txt가 남는 것을 확인했다 - 작업 실행 사용자의 권한을 확인했다
- 실행 정책, 서명, 회사 내부 규칙을 확인했다
- 바로 삭제하지 않고, 먼저 이동으로 운용한다
- 복구할 경우의 되돌릴 위치를 정해두었다
15. 정리
PowerShell의 응용이라고 해도, 어려운 문법을 많이 쓸 필요는 없습니다. 실무에서 효과가 있는 것은, 이런 형태를 만들어 두는 것입니다.
- 설정을 JSON으로 분리한다
- 읽기 처리를 먼저 만든다
- CSV와 JSON으로 증적을 남긴다
- 변경 처리는 함수로 분리한다
-WhatIf에 해당하는 예행연습을 준비한다- transcript와
error.txt로 추적할 수 있게 한다 - 수동 실행으로 확인한 뒤 정기 실행한다
이번 스크립트는, 로그 조사와 아카이브를 주제로 하고 있지만, 사고방식은 다른 업무에도 사용할 수 있습니다.
- 파일 정리
- 보고서 출력
- CSV 집계
- 배치 교체
- 오래된 자산의 재고 조사
- 일일·월간 운영 확인
PowerShell은 한 줄 명령으로도 편리하지만, 업무에서 사용한다면, 다음 순서를 지키는 것이 더 안전합니다.
본다 → 기록한다 → 예행한다 → 실행한다 → 증적을 남긴다
이 형태로 해두면, PowerShell은 단순한 작업 단축 도구가 아니라, 운영을 안정시키기 위한 작은 업무 애플리케이션으로 사용할 수 있게 됩니다.
참고 링크
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
C#(CSharp)에서 PowerShell을 실행하고 결과를 객체로 받는 방법
C#에서 PowerShell을 실행하고 결과를 문자열이 아닌 PSObject로 받는 방법을, PowerShell SDK, AddCommand, AddParameter, BaseObject, Properties, 오류 처리까지 실무 관점에서 정리합니다.
Pester로 하는 PowerShell 테스트 정비 ── 운영 스크립트를 쉽게 망가지지 않게 만드는 실무 패턴
PowerShell 스크립트를 Pester v5로 테스트하여 날짜 처리, 파일 작업, 삭제 처리, Mock, CI 실행까지 안전하게 정비하는 실무 절차를 정리합니다.
PowerShell 실용 명령어 모음 ── 일상 업무에서 자주 쓰는 작은 기능 늘리기
PowerShell로 일상 업무에 쓰는 실용 명령어로서, Measure-Object, Group-Object, Select-String, Compare-Object, Tee-Object, Start-Transcript 등의 활용 시점을 정리합니다.
PowerShell 명령어의 기초 — 가장 먼저 익힐 조작과 안전한 사용법
PowerShell 초보자가 실무에서 헤매지 않도록, 명령어(cmdlet)를 찾는 방법부터 파이프라인, 파일 조작, CSV 처리, 실행 정책, 사고를 막기 위한 확인 절차까지 정리합니다.
VBScript 폐지에 대비하는 VBA·사내 도구 점검 가이드
VBScript 단계적 폐지에 대비해 VBA·Excel 매크로·사내 도구의 재고 조사, 정적 검출, 실행 로그, 대체 기술 선정, 테스트, 단계적 전개를 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 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 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크