PowerShell 스크립트가 느릴 때 볼 곳 ── 배열·파이프라인·매칭의 핵심

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

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

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

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
기사 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건) 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
`Invoke-KsAggregate` 등이 독자 처리로 바꿔 읽기 위한 가상의 함수 이름임을 명시하고, 대상 환경(5.1과 7 모두 대응)을 추가했습니다. 판단표에 본문 참조 열을 넣고, 5개 항목에 효과의 대략과 그 근거를 추가했습니다. 더불어 작성 방식에서 유일하게 정해지는 처리 횟수 표를 2개 추가했습니다(`+=`의 요소 복사 총횟수와 매칭 비교 횟수). 실행 시간의 실측값은 측정하지 않았으므로 싣지 않았습니다.
본문의 관련 기사 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 부분을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175012)

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

Go Komura (2026). 「PowerShell 스크립트가 느릴 때 볼 곳 ── 배열·파이프라인·매칭의 핵심」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-performance-tuning/

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

「처음에는 몇 분이면 끝나던 집계 스크립트가, 데이터가 늘면서 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를 쓰세요. provider 쪽에서 걸러 가져오므로, 가져온 뒤 Where-Object로 버리는 것보다 효율적이라고 공식 문서도 명시합니다.5
  • 문자열 연결은 -join 또는 StringBuilder. $s += "..."는 배열과 같은 이유로 느려집니다.6
  • Format-*는 화면 표시의 마지막만. 중간에 끼우면 이후 처리가 깨질 뿐 아니라, 불필요한 포맷 비용이 듭니다.7
  • 측정한 뒤에 고칩니다. Measure-Command는 출력을 버리므로 표시 비용을 포함하지 않습니다. 첫 실행 값은 쓰지 않습니다.8
  • 병렬화는 마지막입니다. 알고리즘을 고친 뒤에 검토합니다(「PowerShell의 병렬 처리」).

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

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

추측으로 튜닝하면 대개 효과가 없는 곳을 고칩니다. 처음에 할 일은 측정입니다.8

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

# 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건씩 조회한다」이므로 합계 건수에 비례합니다.

수주 건수 × 마스터 건수 이중 루프의 비교 횟수 해시 테이블의 처리 횟수
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개마다 cmdlet 사이를 흘리는 처리 비용이 있습니다. 수십만 건 규모의 안쪽 루프에서는 foreach 문이 더 빠른 경우가 많습니다.3

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

# foreach 문: 1건당 overhead가 작고, 대량 건수에서는 빠르다
$sum = 0
foreach ($r in $rows) { if ($r.Status -eq 'OK') { $sum += $r.Amount } }

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

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

다만 문자 코드의 기본값이 다르다는 점에 주의하세요. 인자 하나의 ReadLines($path)는 UTF-8로 읽습니다(BOM이 있으면 그것을 우선). 한편 Windows PowerShell 5.1의 Get-Content는 BOM이 없으면 현재 ANSI 코드 페이지(일본어 환경에서는 Shift_JIS)로 읽습니다. 즉 Shift_JIS 로그를 그대로 바꾸면 일본어가 깨집니다. 바꿀 때는 인코딩을 명시하는 overload를 쓰세요.

# Shift_JIS(CP932) 로그를 읽는 경우
# PowerShell 6.2 이후는 코드 페이지 provider가 등록되어 있으므로
# 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에서 다루는 한 인자 하나의 overload라도 결과가 일치합니다. 바꾸기 전에 대상 파일의 문자 코드와 실행 환경 버전을 반드시 확인하세요.

판단의 대략은 다음과 같습니다.

상황 선택
건수가 적음·가독성 우선 파이프라인
명령 결과를 넘기는 거대한 입력(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를 쓰세요. 공식 문서가 「필터는 provider가 객체를 가져올 때 적용하므로, 다른 매개변수보다 효율적」이라고 명시합니다.5

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

# 【OK】provider 쪽에서 걸러 가져온다
Get-ChildItem -Path $root -Recurse -File -Filter '*.log'

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

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

# 【NG】1만 줄 = 1만 회 open/close
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에 의한 provider 쪽 필터, 일괄 쓰기, 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 cmdlet은 파이프라인에서 순차 입력을 받는다는 점, 둘의 구분에 대해. 지연 열거의 예로는 File.ReadLines 메서드가, 파일 전체를 읽지 않고 한 줄씩 열거할 수 있다는 점(ReadAllLines와의 차이)을 설명합니다.  2 3

  4. Microsoft Learn, Get-Content. 기본에서는 줄바꿈 구분으로 한 줄씩 객체로 반환한다는 점, -Raw로 파일 전체를 하나의 문자열로 읽을 수 있다는 점, -ReadCount로 지정 줄 수씩 모아 파이프라인으로 보낼 수 있다는 점에 대해.  2 3

  5. Microsoft Learn, Get-ChildItem. -Filter가 provider에 의해 객체를 가져올 때 적용되므로, 가져온 뒤 필터하는 다른 매개변수보다 효율적이라는 점, -File이나 -Recurse와의 조합에 대해.  2 3

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

  7. Microsoft Learn, Format-Table. Format-* cmdlet이 표시용 포맷 객체를 만들므로, 이후 명령으로 파이프하는 용도에는 맞지 않고, 파이프라인의 마지막에서 써야 한다는 점에 대해.  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 컴파일 시간이 포함되므로, 1회차 측정값은 대부분 믿을 수 없습니다. 같은 처리를 몇 번 실행해 2회차 이후 값을 보고, 비교 대상은 같은 조건에서 측정한다는 두 가지를 지키세요.
CSV 두 개의 매칭이 끝나지 않습니다. 어디를 고치면 됩니까?
안쪽 루프에서 매번 Where-Object로 선형 탐색을 하고 있을 가능성이 높습니다. 건수가 각각 1만 건이면 비교 횟수는 1억 회가 됩니다. 한쪽을 해시 테이블(또는 Group-Object -AsHashTable)로 바꿔 키로 조회하면, 비교 횟수는 합계 건수에 비례하는 수준까지 떨어집니다. 건수가 늘수록 효과가 커지는, 비용 대비 효과가 가장 큰 개선입니다.
cmdlet보다 .NET API를 직접 호출하는 편이 빠르다고 들었습니다. 항상 그렇게 해야 합니까?
아니요, 구분해 쓰는 것입니다. .NET API(예: System.IO.File의 ReadLines나 EnumerateFiles)는 분명히 빠르지만, PowerShell의 provider 기능이나 와일드카드, 상대 경로 해석 같은 편의는 잃고 코드도 읽기 어려워집니다. 먼저 측정해서, 실제로 병목임이 확인된 곳만 바꾸세요. 수십만 파일 탐색이나 거대 파일 읽기처럼, 효과가 분명한 장면에 한정하는 것이 실무적입니다.
처리를 병렬화하면 빨라집니까?
대기 시간이 지배적인 처리라면 효과가 있지만, 순서상 마지막 문제입니다. 먼저 불필요한 처리를 줄이세요(가져오는 건수를 줄이고, 루프 안에서 변하지 않는 처리를 밖으로 빼고, 매칭을 해시 테이블화). 알고리즘이 O(n^2)인 채로 병렬화해도 코어 수만큼만 빨라집니다. 반대로 1건이 가벼운 처리를 병렬화하면 overhead 때문에 더 느려지기도 합니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기