PowerShell 스크립트가 느릴 때 살펴볼 곳 ── 배열・파이프라인・대조의 급소

· · PowerShell, Windows, 성능 개선, 자동화, 스크립트, 운영 개선, 튜닝, 데이터 처리

「처음에는 몇 분이면 끝나던 집계 스크립트가 데이터가 늘어나면서 3시간이 걸리게 되었다」── PowerShell을 이용한 자동화가 자리 잡은 현장에서 자주 듣는 이야기입니다. 그리고 대부분의 경우 느려진 원인은 PowerShell 자체의 실행 속도가 아니라 작성 방식에 있습니다.

PowerShell은 작성하기 쉬운 것을 우선하는 언어라서, 「솔직하게 쓴 결과로 계산량이 나빠지는」 작성법이 여럿 존재합니다. 대표격이 $array += $item입니다. 보기에는 자연스럽지만 내부에서는 매번 배열 전체 복사가 일어나며, 건수가 늘어날수록 급격히 느려집니다. 이런 대표적인 함정을 알고 있는지 여부에 따라, 같은 처리가 몇 시간이 될지 몇십 초가 될지가 갈립니다.

이 글에서는 느린 스크립트를 봤을 때 먼저 의심해야 할 곳을, 영향이 큰 순서대로 정리합니다. 아울러 짐작만으로 고쳐 쓰지 않기 위한 올바른 측정법도 다룹니다.

대상 환경은 Windows PowerShell 5.1과 PowerShell 7 양쪽 모두입니다. 두 버전에서 동작이 달라지는 부분(Get-Content의 기본 문자 인코딩 등)은 그때그때 본문에서 짚습니다. 이 글의 예제 코드는 PowerShell 7.6에서 실행하여 확인했습니다.

1. 먼저 결론

  • $array += $item은 사용하지 마세요. PowerShell의 배열은 고정 길이이며, +=는 매번 새 배열을 만들어 전체 요소를 복사합니다. 건수의 제곱에 비례해 느려집니다.1
  • 대신 List[T]를 사용하거나, 루프 전체의 출력을 변수로 받으세요. 후자는 PowerShell다우면서도 빠른 작성법입니다.
  • 대조・검색은 해시테이블화가 가장 효과적입니다. 이중 루프의 선형 탐색(O(n×m))이 키 참조(대략 O(n+m))가 됩니다.2
  • 파이프라인은 편리하지만 오브젝트 1개당 비용이 있습니다. 대량 건수의 내부 루프에서는 foreach 문이 더 빠른 경우가 많으며, 측정해볼 가치가 있습니다.3
  • 파일 읽기는 목적에 따라 구분해 씁니다. Get-Content -Raw로 일괄 읽기, -ReadCount로 묶어 읽기. 한 줄씩 오브젝트화하는 방식이 유효한 경우도 있습니다.4
  • Get-ChildItem-Filter를 사용하세요. 프로바이더 쪽에서 좁히기 때문에, 가져온 뒤 Where-Object로 버리는 것보다 효율적이라고 공식 문서에도 명시되어 있습니다.5
  • 문자열 연결은 -join이나 StringBuilder로. $s += "..."는 배열과 같은 이유로 느려집니다.6
  • Format-*는 화면 표시 맨 마지막에만. 중간에 끼우면 후속 처리가 깨질 뿐 아니라 불필요한 정형 비용도 듭니다.7
  • 측정한 뒤에 고칩니다. Measure-Command는 출력을 버리기 때문에 표시 비용이 포함되지 않습니다. 첫 실행 값은 사용하지 마세요.8
  • 병렬화는 마지막입니다. 알고리즘을 고친 뒤에 검토합니다(「PowerShell의 병렬 처리」).

2. 먼저 측정한다 ── Measure-Command의 올바른 사용법

짐작으로 튜닝하면 대부분 효과 없는 곳을 고치게 됩니다. 가장 먼저 할 일은 측정입니다.8

먼저 한 가지 짚고 넘어가겠습니다. 이 글의 코드 예제에 나오는 Invoke-KsAggregate, ConvertTo-KsRecord, Join-KsRecord독자 자신의 처리로 바꿔 읽기 위한 가상의 함수 이름입니다. PowerShell의 표준 코맨드릿이 아니므로, 그대로 붙여넣으면 「용어가 인식되지 않습니다」로 멈춥니다. 자신의 스크립트에 있는 함수 이름이나 처리로 바꿔서 읽어 주세요.

# 1회차는 모듈 로드와 JIT가 포함되므로 버린다
$null = Measure-Command { Invoke-KsAggregate }

# 2회차 이후를 여러 번 측정해서 편차도 본다
1..3 | ForEach-Object {
    (Measure-Command { Invoke-KsAggregate }).TotalSeconds
}

Measure-Command의 주의점은 두 가지입니다.

  • 스크립트 블록의 출력은 버려집니다. 화면에 정형・렌더링하는 비용은 포함되지 않습니다. 실제 체감이 느리다면 범인이 표시 쪽인 경우도 있습니다
  • 같은 조건에서 비교합니다. 파일 캐시가 데워져 있는지 여부에 따라 I/O를 포함한 처리의 측정값은 크게 달라집니다

처리 중 어디가 느린지 구간별로 나누고 싶을 때는 Stopwatch를 사용합니다.

$sw = [System.Diagnostics.Stopwatch]::StartNew()
$rows = Import-Csv $csvPath
Write-Verbose "읽기: $($sw.ElapsedMilliseconds) ms"; $sw.Restart()

$index = $master | Group-Object Code -AsHashTable -AsString
Write-Verbose "색인 생성: $($sw.ElapsedMilliseconds) ms"; $sw.Restart()

$result = foreach ($r in $rows) { Join-KsRecord $r $index }
Write-Verbose "대조: $($sw.ElapsedMilliseconds) ms"; $sw.Stop()

이렇게 구간별 시간을 뽑아 보면 「사실은 읽기가 80%였다」 같은 사실을 바로 알 수 있습니다. Write-Verbose를 쓰는 이유는 일반 실행에서는 출력하지 않고 조사할 때만 보기 위해서입니다(「PowerShell의 출력 스트림과 로그 설계」).

3. 가장 큰 범인 ── 배열의 +=

PowerShell의 배열은 고정 길이입니다. 요소를 추가할 수 없으며, +=는 「새 배열을 만들어 전체 요소를 복사하고 끝에 새 요소를 붙인다」는 처리로 전개됩니다.1 즉 n건을 추가하는 루프는 전체적으로 n의 제곱에 비례하는 복사를 수행합니다.

얼마나 늘어나는지는 작성법만으로 계산할 수 있습니다. n건을 추가할 때 발생하는 요소 복사의 총 횟수는 n(n-1)/2입니다.

추가하는 건수 +=로 발생하는 요소 복사 총 횟수 List[T].Add의 추가 횟수
1,000건 약 50만 회 1,000회
10,000건 약 5,000만 회 10,000회
100,000건 약 50억 회 100,000회

이는 실행 시간의 실측값이 아니라, 작성법만으로 하나로 정해지는 처리 횟수입니다. 1건당 실제 시간은 환경에 따라 달라지므로, 자신의 환경에서의 초 단위 수치는 배포 zip의 벤치마크로 측정해 주세요. 다만 건수를 10배로 하면 횟수가 100배가 된다는 증가 양상만큼은 환경과 무관하게 동일합니다.

# 【NG】 건수가 늘어나면 급격히 느려진다
$result = @()
foreach ($row in $rows) {
    $result += ConvertTo-KsRecord $row     # 매번 배열 전체가 복사된다
}

# 【OK-1】 List[T]를 사용한다(요소 추가가 상수 시간)
$result = [System.Collections.Generic.List[object]]::new()
foreach ($row in $rows) {
    $result.Add((ConvertTo-KsRecord $row))
}

# 【OK-2】 루프 전체의 출력을 변수로 받는다(PowerShell다우면서 빠르다)
$result = foreach ($row in $rows) {
    ConvertTo-KsRecord $row                # 출력이 그대로 집약된다
}

【OK-2】의 작성법은 익숙해지면 PowerShell에서 가장 자연스러운 형태입니다. foreach 문, ForEach-Object, if 등의 출력은 그대로 변수에 집약됩니다. 이 성질은 「출력 스트림」의 이해와 함께 익히게 됩니다.

같은 논리가 문자열에도 적용됩니다. 문자열은 불변(immutable)이므로 $s += "line"은 매번 새로운 문자열을 만듭니다.

# 【NG】
$text = ''
foreach ($line in $lines) { $text += "$line`r`n" }

# 【OK-1】 -join(가장 간결)
$text = $lines -join "`r`n"

# 【OK-2】 StringBuilder(조건 분기가 얽힌 복잡한 조립에 적합)
$sb = [System.Text.StringBuilder]::new()
foreach ($line in $lines) { [void]$sb.AppendLine($line) }
$text = $sb.ToString()

4. 이중 루프를 그만둔다 ── 대조는 해시테이블로

다음으로 효과가 큰 것이 대조(매칭)입니다. 「수주 데이터의 각 행에 대해 마스터에서 상품명을 가져온다」는 처리를 있는 그대로 쓰면 이렇게 됩니다.

# 【NG】 수주 1만 건 × 마스터 1만 건 = 1억 회 비교
foreach ($order in $orders) {
    $item = $master | Where-Object { $_.Code -eq $order.Code }   # 매번 전체를 훑는다
    $order | Add-Member NoteProperty ItemName $item.Name
}

이를 해시테이블로 바꾸면 비교 횟수가 극적으로 줄어듭니다.2

# 【OK】 색인을 한 번만 만들고, 이후에는 키로 조회한다
$index = $master | Group-Object -Property Code -AsHashTable -AsString

$result = foreach ($order in $orders) {
    $hit = $index[$order.Code]        # 키 참조는 건수에 의존하지 않는다
    [pscustomobject]@{
        Code     = $order.Code
        Qty      = $order.Qty
        ItemName = if ($hit) { $hit[0].Name } else { $null }   # 미등록도 감지할 수 있다
    }
}

이쪽도 횟수로 확인해 봅니다. 이중 루프의 비교 횟수는 수주 건수 × 마스터 건수이고, 해시테이블은 「색인을 한 번 만든다」+「한 건씩 조회한다」이므로 합계 건수에 비례합니다.

수주 건수 × 마스터 건수 이중 루프의 비교 횟수 해시테이블의 처리 횟수
1,000 × 1,000 100만 회 약 2,000회
10,000 × 10,000 1억 회 약 2만 회
100,000 × 100,000 100억 회 약 20만 회

건수가 10배가 되면 이중 루프는 100배, 해시테이블은 10배입니다. 건수가 많을수록 차이가 벌어지므로, 다루는 데이터가 늘어날 것으로 예상되는 처리일수록 먼저 고칠 가치가 있습니다.

Group-Object -AsHashTable은 같은 키의 요소를 배열로 묶기 때문에 값은 배열이 됩니다(위의 $hit[0]). 키가 유일하다는 것을 알고 있다면 직접 해시테이블을 조립해도 상관없습니다.

$index = @{}
foreach ($m in $master) { $index[$m.Code] = $m }    # 유일 키를 전제로 함

「포함되는지 여부」만 여러 번 판정하는 경우에는 HashSet이 유효합니다. 배열에 대한 -contains-in은 선형 탐색이므로, 판정 횟수가 많으면 효과가 나타납니다.

$known = [System.Collections.Generic.HashSet[string]]::new(
    [string[]]$master.Code, [System.StringComparer]::OrdinalIgnoreCase)

$unknown = $orders | Where-Object { -not $known.Contains($_.Code) }

CSV 간 대조의 실무 패턴은 「PowerShell로 Excel・CSV 업무 처리를 자동화한다」에서도 다루고 있습니다.

5. 파이프라인과 foreach 문

파이프라인은 PowerShell의 핵심 기능이지만, 오브젝트 1개마다 코맨드릿 사이를 흘려보내는 처리 비용이 있습니다. 수십만 건 규모의 내부 루프에서는 foreach 문이 더 빠른 경우가 많습니다.3

# 파이프라인: 읽기 쉽고, 순차 처리되므로 중간 결과를 쌓아두지 않는다
$rows | Where-Object { $_.Status -eq 'OK' } | ForEach-Object { $_.Amount } |
    Measure-Object -Sum

# foreach 문: 건당 오버헤드가 작아 대량 건수에서 빠르다
$sum = 0
foreach ($r in $rows) { if ($r.Status -eq 'OK') { $sum += $r.Amount } }

메모리에 관해서는 오해하기 쉬운 부분이 있어 보충합니다. foreach 문은 괄호 안의 식을 먼저 평가한 뒤 루프에 들어가므로, foreach ($line in (Get-Content $path))처럼 명령의 실행 결과를 넘기면 그 시점에 전체가 메모리에 올라갑니다.3 반면 지연 열거되는 IEnumerable을 넘기면 한 건씩 열거됩니다. 따라서 거대한 파일이라도 다음과 같이 쓰면 foreach 문 그대로 메모리를 쓰지 않고 처리할 수 있습니다.

# 전체를 메모리에 올리지 않고 foreach 문으로 처리한다(지연 열거)
foreach ($line in [System.IO.File]::ReadLines($path)) {
    if ($line.StartsWith('ERROR')) { $errors++ }
}

다만 문자 인코딩의 기본값이 다르다는 점에 주의하세요. 인수 1개짜리 ReadLines($path)는 UTF-8로 읽습니다(BOM이 있으면 그것을 우선). 반면 Windows PowerShell 5.1의 Get-Content는 BOM이 없으면 현재 ANSI 코드 페이지(일본어 환경에서는 Shift_JIS)로 읽습니다. 즉 Shift_JIS 로그를 그대로 교체하면 문자가 깨집니다. 교체할 때는 인코딩을 명시하는 오버로드를 사용하세요.

# Shift_JIS(CP932) 로그를 읽는 경우
# PowerShell 6.2 이후에는 코드 페이지 프로바이더가 등록되어 있으므로
# GetEncoding(932)을 그대로 호출할 수 있다(Windows PowerShell 5.1에서도 동일)
$enc = [System.Text.Encoding]::GetEncoding(932)
foreach ($line in [System.IO.File]::ReadLines($path, $enc)) {
    if ($line.StartsWith('ERROR')) { $errors++ }
}

또한 PowerShell 7에서는 Get-Content의 기본값이 UTF-8(BOM 없음)이므로, UTF-8 파일을 7에서 다루는 한 인수 1개짜리 오버로드로도 결과가 일치합니다. 교체하기 전에 대상 파일의 문자 인코딩과 실행 환경의 버전을 반드시 확인하세요.

판단 기준은 다음과 같습니다.

상황 선택
건수가 적다・가독성 우선 파이프라인
명령 결과를 넘기는 거대한 입력(Get-Content 등) 파이프라인, 또는 [IO.File]::ReadLines + foreach
이미 메모리 상에 있는 배열을 수십만 건 순회 foreach
배열에 대한 단순한 좁히기 .Where({...}) / .ForEach({...}) 메서드1

.Where().ForEach()는 PowerShell 4.0에서 추가된 배열의 메서드로, 파이프라인을 거치지 않는 만큼 가볍습니다.1 다만 대상이 이미 메모리 상의 컬렉션이라는 것이 전제입니다.

6. 파일과 디렉터리의 I/O

읽기는 목적에 따라 구분해서 씁니다. Get-Content는 기본값으로 한 줄씩 오브젝트를 생성하므로, 거대한 파일에서는 이 비용이 지배적이 됩니다.4

Get-Content $path                      # 한 줄씩(줄 수만큼 오브젝트 생성)
Get-Content $path -Raw                 # 전체를 하나의 문자열로 일괄 읽기
Get-Content $path -ReadCount 1000      # 1000줄씩 배열로 전달(오브젝트 생성 감소)
[System.IO.File]::ReadLines($path)     # .NET. 순차 열거로 가장 가볍다(기본값은 UTF-8)

디렉터리 스캔에서는 -Filter를 사용하세요. 공식 문서가 「필터는 프로바이더가 오브젝트를 가져올 때 적용하므로 다른 매개변수보다 효율적」이라고 명시하고 있습니다.5

# 【NG】 전체를 가져온 뒤 버린다
Get-ChildItem -Path $root -Recurse | Where-Object { $_.Extension -eq '.log' }

# 【OK】 프로바이더 쪽에서 좁힌다
Get-ChildItem -Path $root -Recurse -File -Filter '*.log'

# 수십만 개 파일 규모에서도 여전히 느리면 .NET의 열거를 검토한다
[System.IO.Directory]::EnumerateFiles($root, '*.log', 'AllDirectories')

쓰기에서는 루프 안에서의 매번 추가 기록이 전형적인 병목입니다. Add-Content는 호출할 때마다 파일을 열고 닫습니다.

# 【NG】 1만 줄 = 1만 회의 열기/닫기
foreach ($r in $result) { Add-Content -Path $out -Value ($r -join ',') }

# 【OK-1】 한꺼번에 한 번에 쓴다
$result | ForEach-Object { $_ -join ',' } | Set-Content -Path $out -Encoding utf8

# 【OK-2】 순차 출력이 필요하다면 StreamWriter를 열어 둔 채로 사용한다
$writer = [System.IO.StreamWriter]::new($out, $false, [System.Text.UTF8Encoding]::new($false))
try   { foreach ($r in $result) { $writer.WriteLine($r -join ',') } }
finally { $writer.Dispose() }

네트워크를 경유하는 파일 조작에서는 애초에 왕복 횟수가 지배적이 됩니다. UNC 경로 특유의 주의점은 「네트워크 공유・UNC 경로의 함정」을 참조하세요.

7. 놓치기 쉬운 작은 비용

어디부터 손을 대야 할지 고민될 때를 위해 효과 크기의 대략적인 기준을 덧붙입니다. 이는 실측값이 아니라 「1회당 비용 × 실행 횟수」로 정해지는 자릿수 감각입니다. 자신의 스크립트 건수에 대입해서 읽어 주세요.

  • Write-Progress의 갱신 빈도. 루프마다 진행 상황을 갱신하면 렌더링 비용이 처리 시간을 웃돌 수 있습니다. 100건마다 등으로 간격을 두거나, 무인 실행에서는 $ProgressPreference = 'SilentlyContinue'로 멈춥니다 ── 효과의 기준: 큼. 1건당 렌더링이 무겁고 루프 횟수가 그대로 효과에 반영됩니다. 수만 회 이상의 루프라면 최우선으로 봅니다
  • Format-Table / Format-List를 중간에 끼운다. 포맷용 오브젝트로 변환되므로 후속 처리가 깨지는 데다 불필요한 비용도 듭니다. 표시는 마지막에만 합니다7 ── 효과의 기준: 중간. 속도보다 「후속 처리가 깨진다」는 쪽이 실질적인 피해가 크며, 속도 목적이 아니어도 고칠 가치가 있습니다
  • 루프 안의 불변한 처리. Get-Date의 서식 문자열 생성, 정규식 컴파일, 모듈 재로드 등을 루프 밖으로 빼내기만 해도 효과가 있는 경우가 있습니다 ── 효과의 기준: 중간~큼. 빼내는 처리 1회의 비용에 따라 다르며, 모듈 재로드처럼 무거운 것이 섞여 있으면 극적으로 효과가 있습니다
  • Select-Object -Property의 남용. 새로운 PSCustomObject를 생성하므로 대량 건수에서는 무시할 수 없습니다. 필요한 단계까지 원래 오브젝트를 유지하는 편이 빠른 경우가 있습니다 ── 효과의 기준: 작음~중간. 수천 건에서는 체감할 수 없고, 수만 건을 넘어선 무렵부터 효과가 나타납니다
  • 예외의 빈발. try/catch를 대량으로 통과시키는 설계(존재하지 않는 파일을 매번 Get-Item해서 예외를 잡는 등)는 비용이 큽니다. 사전에 Test-Path로 분기합니다 ── 효과의 기준: 큼. 예외의 발생과 포착은 일반적인 분기보다 훨씬 무겁기 때문에, 건수 × 예외율이 클수록 지배적이 됩니다

8. 실무의 정석(판단표)

증상 의심할 곳 대처 자세히는
건수가 늘어나면 갑자기 느려진다 $array += / $string += List[T], 루프 출력의 변수 받기, -join1 3장
두 데이터의 대조가 끝나지 않는다 이중 루프의 선형 탐색 해시테이블 / Group-Object -AsHashTable2 4장
거대한 파일 읽기가 느리다 Get-Content의 줄 오브젝트 생성 -Raw / -ReadCount / [File]::ReadLines4 6장(문자 인코딩 주의는 5장)
폴더 스캔이 느리다 Where-Object에 의한 후단 좁히기 Get-ChildItem -Filter, 필요하면 EnumerateFiles5 6장
출력 파일 쓰기가 느리다 루프 안의 Add-Content 일괄 쓰기, 또는 StreamWriter6 6장
CPU는 한가한데 끝나지 않는다 네트워크・I/O 대기 병렬화가 나설 차례 별도 글 「병렬 처리
화면 표시가 느리다 포맷 처리・렌더링 Format-*는 마지막에만, $ProgressPreference 무효화7 7장
어디가 느린지 모른다 측정 부족 Stopwatch로 구간 측정, Measure-Command는 2회차 이후8 2장

9. 정리

  • 먼저 측정합니다. Measure-Command는 출력을 버려서 표시 비용을 포함하지 않는다는 점, 첫 실행 값은 쓸 수 없다는 점에 주의하세요.
  • $array += 는 건수의 제곱에 비례해 느려집니다. List[T]나 루프 출력의 변수 받기로 바꾸는 것이 최우선입니다.
  • 대조는 해시테이블화가 비용 대비 효과가 가장 큰 개선입니다. 이중 루프를 발견하면 색인을 만들 수 없는지 생각해 보세요.
  • 파일 I/O는 목적에 맞는 읽기・쓰기를 선택합니다. -Filter를 통한 프로바이더 쪽 좁히기, 일괄 쓰기, StreamWriter의 구분 사용이 기본입니다.
  • Format-*를 중간에 끼우지 않기, 진행 상황 갱신 간격 두기, 루프 안의 불변 처리 빼내기 ── 작은 누적도 건수가 많으면 효과가 있습니다.
  • 병렬화는 최후의 수단입니다. 알고리즘을 고친 뒤, 대기 시간이 지배적인 처리에 대해 적용하세요.

샘플 코드 다운로드

이 글에서 다룬 코드는 그대로 실행할 수 있는 형태로 정리해 배포하고 있습니다. 3종류의 벤치마크와 측정 헬퍼가 들어 있습니다.

샘플 코드 다운로드(zip)

이 글의 샘플은 PowerShell 7.6에서 실제로 실행하여 검증했습니다(Pester 8건). zip에 포함된 Invoke-SampleTests.ps1을 실행하면 여러분의 환경에서도 같은 검증을 재현할 수 있습니다.

# 구문 분석 + 정적 분석 + Pester 테스트
./Invoke-SampleTests.ps1

설정값(경로, 서버명, 테넌트 ID 등)은 예시입니다. 그대로 운영 환경에서 실행하지 말고, 자사 환경에 맞게 바꿔서 사용하세요.

관련 글

관련 상담 영역

합동회사 코무라소프트에서는 시간이 지나치게 오래 걸리는 집계・대조 처리의 고속화, 데이터양 증가를 견디는 처리 설계로의 재구성, 성능 저하 원인 조사를 다루고 있습니다.

참고 링크

  1. Microsoft Learn, about_Arrays. PowerShell의 배열이 고정 크기이며, += 연산자가 새 배열을 생성해 기존 요소를 복사한 뒤 요소를 추가하는 동작이 된다는 점, 요소 추가・삭제를 반복하는 경우에는 컬렉션 타입(List 등) 사용이 적합하다는 점, PowerShell 4.0에서 추가된 .Where() 및 .ForEach() 메서드에 관해.  2 3 4 5

  2. Microsoft Learn, Group-Object. -AsHashTable로 그룹화 결과를 해시테이블로 반환할 수 있다는 점, -AsString으로 키를 문자열로 다룰 수 있다는 점, 반환되는 해시테이블의 값이 각 그룹 요소의 배열이 된다는 점에 관해.  2 3

  3. Microsoft Learn, about_Foreach. foreach 문이 괄호 안의 식을 평가한 뒤 반복을 시작하므로, 명령의 실행 결과를 넘기면 그 결과가 메모리에 유지되는 반면, ForEach-Object 코맨드릿은 파이프라인에서 순차적으로 입력을 받는다는 점, 양쪽의 구분 사용에 관해. 지연 열거의 예시로는 File.ReadLines 메서드가 파일 전체를 읽어 들이지 않고 한 줄씩 열거할 수 있다는 점(ReadAllLines와의 차이)을 설명하고 있다.  2 3

  4. Microsoft Learn, Get-Content. 기본값으로 줄바꿈 단위로 한 줄씩 오브젝트로 반환한다는 점, -Raw로 파일 전체를 하나의 문자열로 읽을 수 있다는 점, -ReadCount로 지정한 줄 수만큼 묶어서 파이프라인으로 보낼 수 있다는 점에 관해.  2 3

  5. Microsoft Learn, Get-ChildItem. -Filter가 프로바이더에 의해 오브젝트를 가져올 때 적용되므로, 가져온 뒤 필터링하는 다른 매개변수보다 효율적이라는 점, -File이나 -Recurse와의 조합에 관해.  2 3

  6. Microsoft Learn, StringBuilder 클래스. String이 불변이라서 연결할 때마다 새 인스턴스가 생성되는 반면, StringBuilder는 가변 버퍼에서 문자열을 조립한다는 점에 관해. 아울러 StreamWriter 클래스의 스트림을 열어 둔 채 순차적으로 쓰는 동작에 관해.  2

  7. Microsoft Learn, Format-Table. Format 계열 코맨드릿이 표시용 포맷 오브젝트를 생성하므로 후속 명령으로 파이프하는 용도에는 적합하지 않으며, 파이프라인의 마지막에서 사용해야 한다는 점에 관해.  2 3

  8. Microsoft Learn, Measure-Command. 스크립트 블록이나 명령의 실행 시간을 측정해 TimeSpan을 반환한다는 점, 측정 대상의 출력 자체는 결과에 포함되지 않는다는 점에 관해. 아울러 Stopwatch 클래스를 이용한 구간 측정에 관해.  2 3

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

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

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

자주 묻는 질문

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

$array += $item 이라는 작성법은 왜 느린가요?
PowerShell의 배열은 고정 길이라서 요소를 추가할 수 없기 때문입니다. += 연산자는 「새 배열을 만들고 기존 요소를 전부 복사한 뒤 끝에 추가한다」는 처리로 전개됩니다. 1건을 추가할 때마다 전체 복사가 일어나므로, n건을 추가하면 계산량은 n의 제곱에 비례합니다. 100건이라면 눈치채지 못하지만 1만 건을 넘으면 체감할 정도로 느려지고, 10만 건이라면 실용적이지 않습니다. 대처법은 System.Collections.Generic.List[T]를 사용해 Add하거나, 루프 전체의 출력을 변수로 받는 방식으로 바꾸는 것입니다.
Measure-Command로 측정했는데 실제 실행 시간과 맞지 않습니다.
Measure-Command는 스크립트 블록의 실행 시간만 측정하며, 그 출력은 버려지기 때문입니다. 실제 실행에서는 결과를 화면에 정형하여 표시하는 비용(포맷 처리)과 콘솔에 그리는 시간이 더해집니다. 또한 첫 실행에는 모듈 로드와 JIT 컴파일 시간이 포함되므로, 첫 번째 측정값은 대부분 신뢰할 수 없습니다. 같은 처리를 여러 번 실행해서 두 번째 이후의 값을 보는 것, 비교 대상은 같은 조건에서 측정하는 것, 이 두 가지를 지켜 주세요.
두 CSV의 대조가 끝나지 않습니다. 어디를 고쳐야 하나요?
내부 루프에서 매번 Where-Object로 선형 탐색을 하고 있을 가능성이 높습니다. 각각 1만 건이라면 비교 횟수는 1억 회가 됩니다. 한쪽을 해시테이블(또는 Group-Object -AsHashTable)로 변환해서 키로 조회하는 형태로 바꾸면, 비교 횟수는 합계 건수에 비례하는 정도까지 줄어듭니다. 건수가 늘어날수록 효과가 커지는, 비용 대비 효과가 가장 큰 개선입니다.
코맨드릿보다 .NET API를 직접 호출하는 편이 빠르다고 들었습니다. 항상 그렇게 해야 하나요?
아니요, 상황에 맞게 구분해서 써야 합니다. .NET API(예: System.IO.File의 ReadLines나 EnumerateFiles)는 확실히 빠르지만, PowerShell 프로바이더 기능이나 와일드카드, 상대 경로 해석 같은 편의성을 잃고 코드도 읽기 어려워집니다. 먼저 측정하고, 실제로 병목임이 확인된 부분만 교체하세요. 수십만 개의 파일 스캔이나 거대한 파일 읽기처럼 확실히 효과가 있는 상황에 한정하는 것이 실무적입니다.
처리를 병렬화하면 빨라지나요?
대기 시간이 지배적인 처리라면 효과가 있지만, 순서상으로는 마지막입니다. 먼저 불필요한 처리를 줄이는 일(가져오는 건수를 좁히기, 루프 안에서 불변한 처리를 밖으로 빼기, 대조를 해시화하기)을 먼저 하세요. 알고리즘이 O(n^2)인 채로 병렬화해도 코어 수만큼만 빨라집니다. 반대로 1건이 가벼운 처리를 병렬화하면 오버헤드 때문에 오히려 느려지기도 합니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기