PowerShell로 REST API와 연동하기 ── Invoke-RestMethod의 실무

· 업데이트: · · PowerShell, REST API, Windows, 자동화, 업무 시스템, 연동, JSON, 운영 개선

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

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

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
기사 서두에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대한 대응으로 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
`Retry-After`가 HTTP 날짜 형식일 때의 대기 시간을 올림 처리하도록 했습니다. `[int]`로의 캐스트는 버림이 아니라 사사오입(소수부가 정확히 0.5이면 짝수 쪽)이므로, 응답이 도착할 때까지의 시간을 뺀 나머지가 0.5초 미만이면 0이 됩니다. `Start-Sleep -Seconds 0`은 즉시 반환되므로, 서버가 지정한 시각보다 먼저 재전송하고, 다시 429를 받아 시도 횟수만 소진합니다. `[math]::Ceiling`을 넣었고, 이유도 본문에 추가했습니다.
멱등의 의미를 처음 등장하는 위치에서 한 줄로 정의하고, 보내는 JSON과 돌아오는 JSON의 왕복 예나 재시도 시의 메시지 등 확인의 근거를 4곳에 추가했습니다. 아울러 전제 환경 표와, 프록시 환경에서의 증상별 대응 표를 추가했습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175024)

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

Go Komura (2026). 「PowerShell로 REST API와 연동하기 ── Invoke-RestMethod의 실무」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-rest-api-invoke-restmethod/

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

「기간 시스템의 Web API에서 수주 데이터를 가져와 사내 Excel 장표로 내보낸다」「SaaS 근태 API를 매일 아침 호출해 당일 출근 예정을 출력한다」── PowerShell이 업무에서 쓰이는 장면 가운데, 최근 몇 년 사이 분명히 늘어난 것이 REST API 연동입니다. 전용 도구를 살 정도는 아니지만, 수작업으로는 감당할 수 없습니다. 그 틈을 메우는 도구로서 Invoke-RestMethod는 매우 강력합니다.

한편, 동작하는 데까지는 쉽더라도 운영에 올리면 갑자기 어려워지는 것이 API 연동의 특징이기도 합니다. 일본어가 깨집니다, 오류 응답 내용을 읽을 수 없습니다, 가끔 429로 실패합니다, 프록시 환경에서 통하지 않습니다, Windows PowerShell 5.1만 TLS에서 막힙니다. 모두 「상대가 있는 시스템」 특유의 문제입니다.

이 기사에서는 사내에서 PowerShell로 API를 호출하는 정보시스템·개발자를 향해, 인증, JSON 송수신, 오류 처리, 재시도, 페이징, 그리고 5.1 고유의 함정까지를 실무에 필요한 순서로 정리합니다.

전제 환경·검증 환경

항목 내용
대상 버전 Windows PowerShell 5.1과 PowerShell 7 모두를 대상으로 합니다. -SkipHttpErrorCheck -Authentication -MaximumRetryCount -NoProxy 등 PowerShell 6 이후에서만 쓸 수 있는 기능은 본문에서 그때그때 명시합니다(5.1 고유 제약은 §8에 모아 두었습니다)1
샘플 검증 환경 기사 말미에서 배포하는 샘플 코드는 PowerShell 7.6에서 실행해 검증했습니다(Pester 21건). §8의 5.1 고유 서술은 5.1 사양에 따른 정리입니다
예에 쓰는 API https://api.example.co.jp/...는 가상의 엔드포인트입니다. 그대로는 동작하지 않습니다. 응답 JSON 예도 설명을 위해 둔 가상입니다

1. 먼저 결론

  • JSON/XML API라면 Invoke-RestMethod. 응답을 자동으로 객체화합니다. 상태 코드나 헤더가 필요하면 Invoke-WebRequest, 또는 -StatusCodeVariable / -ResponseHeadersVariable을 사용합니다. 23
  • 인증은 헤더로 넘기는 방법이 가장 범용적입니다. PowerShell 6 이후는 -Authentication Bearer -Token(SecureString)도 쓸 수 있습니다. 2
  • 일본어 JSON은 UTF-8 바이트 배열로 보내는 것이 확실합니다. -ContentTypecharset=utf-8을 명시합니다.
  • ConvertTo-Json의 기본 깊이는 2입니다. 중첩이 깊은 객체는 -Depth를 지정하지 않으면 잘립니다. 4
  • 4xx/5xx는 종료 오류가 됩니다. PowerShell 7 이후는 $_.ErrorDetails.Message로 본문을 읽을 수 있습니다. -SkipHttpErrorCheck로 예외로 만들지 않는 선택지도 있습니다. 2
  • 429는 Retry-After에 따라 기다립니다. -MaximumRetryCount를 지정하면 내장 기능이 자동으로 따릅니다. 다만 내장 재시도는 400〜599(와 304) 전부가 대상이며, 401이나 404도 재전송됩니다. 코드별 처리나 지수 백오프가 필요할 때만 직접 구현합니다. 2
  • POST처럼 비멱등한 요청은 자동 재시도하지 마세요. 멱등이란 같은 요청을 몇 번 보내도 결과가 바뀌지 않는 성질을 말하며, GET PUT DELETE는 멱등, POST는 비멱등입니다. 통신 오류나 5xx여도 서버 측은 처리가 끝난 경우가 있어, 재전송은 이중 등록이 됩니다.
  • Link 헤더 방식 페이징은 -FollowRelLink로 자동화할 수 있습니다. 커서 방식은 직접 루프합니다. 2
  • Windows PowerShell 5.1에는 고유한 함정이 있습니다. -UseBasicParsing의 필요 여부, TLS 1.2의 명시적 활성화, 인코딩 처리의 3점입니다. 1
  • -SkipCertificateCheck는 상시 운영에서 쓰지 않습니다. 사내 CA를 신뢰하게 하는 것이 올바른 대응입니다.

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

2. Invoke-RestMethod와 Invoke-WebRequest

먼저 차이를 짚습니다. 23

  Invoke-RestMethod Invoke-WebRequest
응답 처리 JSON/XML을 자동으로 객체화 WebResponseObject(원시 본문·헤더·코드)
주요 용도 REST API HTML 가져오기, 상태나 헤더 참조
상태 코드 -StatusCodeVariable로 가져오기(PS7+) .StatusCode
헤더 -ResponseHeadersVariable로 가져오기(PS6+) .Headers

API를 호출한다면 기본은 Invoke-RestMethod입니다. 헤더나 코드가 필요한 장면에서도 전용 변수 매개변수로 가져올 수 있습니다.

$data = Invoke-RestMethod -Uri 'https://api.example.co.jp/v1/orders' `
        -Headers @{ Authorization = "Bearer $token" } `
        -StatusCodeVariable status -ResponseHeadersVariable headers -TimeoutSec 30

"HTTP $status / 남은 요청 수: $($headers['X-RateLimit-Remaining'])"
$data.items | Select-Object orderId, customerName, amount

3. 인증을 넘기는 방법

가장 범용적인 것은 헤더에 직접 쓰는 방법이며, 5.1에서도 7에서도 같이 동작합니다.

# (1) Bearer토큰(가장 일반적)
$headers = @{ Authorization = "Bearer $accessToken"; Accept = 'application/json' }
Invoke-RestMethod -Uri $uri -Headers $headers

# (2) API키(헤더 이름은 제공 측 사양에 따름)
$headers = @{ 'X-Api-Key' = $apiKey }

# (3) Basic 인증(PowerShell 6 이후는 -Authentication을 쓸 수 있음)
Invoke-RestMethod -Uri $uri -Authentication Basic -Credential $cred

# (4) Bearer를 -Token으로 넘김(PowerShell 6 이후. SecureString으로 다룰 수 있음)
Invoke-RestMethod -Uri $uri -Authentication Bearer -Token $secureToken

# (5) 클라이언트 인증서
Invoke-RestMethod -Uri $uri -Certificate $cert

-Authentication을 쓰는 경우, PowerShell은 HTTPS 이외에서의 사용을 기본으로 거부합니다(-AllowUnencryptedAuthentication으로 우회할 수 있지만, 평문으로 자격 정보가 흐르므로 쓰지 말아야 합니다). 2

토큰이나 API 키를 스크립트에 직접 쓰지 말 것은 대전제입니다. SecretManagement을 사용한 보관 방법은 「PowerShell에서의 자격 정보의 안전한 취급」에 정리했습니다.

4. JSON을 보내기 ── 일본어와 -Depth의 함정

송신에서 반드시 밟는 것이 문자 깨짐중첩 잘림입니다.

ConvertTo-Json-Depth기본이 2이며, 그보다 깊은 계층은 펼쳐지지 않고 형 이름 문자열 등으로 바뀝니다. 4 중첩이 있는 요청 본문에서는 반드시 지정하세요.

$body = @{
    order = @{
        customer = @{ code = 'C001'; name = '株式会社サンプル' }   # 3계층째
        lines    = @( @{ item = 'A-100'; qty = 3 } )
    }
}

# 【NG】기본 -Depth 2에서는 customer나 lines의 내용이 사라진다
$json = $body | ConvertTo-Json

# 【OK】충분한 깊이를 지정한다
$json = $body | ConvertTo-Json -Depth 10

문자 깨짐 대책은 UTF-8 바이트 배열로 만든 뒤 보내는 것이 가장 확실합니다.

$json  = $body | ConvertTo-Json -Depth 10
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)

$res = Invoke-RestMethod -Uri $uri -Method Post `
       -Headers @{ Authorization = "Bearer $token" } `
       -ContentType 'application/json; charset=utf-8' `
       -Body $bytes -TimeoutSec 60

PowerShell 7에서는 문자열을 그대로 넘겨도 UTF-8로 보내지만, 5.1과 공용하는 스크립트에서는 바이트 배열로 두면 환경 차이를 흡수할 수 있습니다. Windows 전반의 문자 코드 사정은 「Windows의 문자 코드와 줄바꿈 코드」를 참조하세요.

보내기 전과 받은 뒤를 왕복으로 확인합니다. 위의 $body에서 만들고 싶은 JSON은 다음 형태입니다.

{
  "order": {
    "customer": { "code": "C001", "name": "株式会社サンプル" },
    "lines": [ { "item": "A-100", "qty": 3 } ]
  }
}

-Depth 지정을 빠뜨리면 3계층째에 해당하는 customerlines의 내용이 펼쳐지지 않아 이 형태가 되지 않습니다. 송신 전에 $json을 그대로 화면에 출력해 계층이 남아 있는지 눈으로 확인하는 것이 가장 빠른 확인 방법입니다(해시테이블에서 만들고 있으므로 키 나열 순서는 열거 순서에 의존합니다. 순서를 고정하고 싶으면 [ordered]@{}를 사용합니다).

이 요청에 대해 API가 다음과 같이 응답한다고 가정합니다(가상 예입니다).

{
  "orderId": "2026-000123",
  "status": "accepted",
  "customer": { "code": "C001", "name": "株式会社サンプル" }
}

Invoke-RestMethod는 응답을 자동으로 객체로 만들므로, 수신 측은 다음과 같이 쓸 수 있습니다.

$res.orderId              # 2026-000123
$res.customer.name        # 株式会社サンプル ── 여기가 깨져 있으면 수신 측 문제

수신 문자 깨짐은 $res.customer.name을 읽을 수 있는지로 원인을 가릅니다. 깨져 있으면 응답의 Content-Typecharset이 올바르게 선언되어 있는지 의심하세요(-ResponseHeadersVariable로 헤더를 가져올 수 있습니다). 송신 측과 수신 측 중 어느 쪽이 원인인지를 이 왕복으로 가를 수 있습니다.

5. 오류 처리 ── 본문을 읽지 못하면 조사할 수 없다

Invoke-RestMethod는 4xx/5xx 응답을 종료 오류로 다룹니다. 즉 try/catch로 잡을 수 있지만, API가 반환한 오류 메시지 본문을 어떻게 읽을지가 문제가 됩니다.

try {
    $res = Invoke-RestMethod -Uri $uri -Method Post -Body $bytes `
           -ContentType 'application/json; charset=utf-8' -TimeoutSec 30
}
catch {
    $status = $_.Exception.Response.StatusCode      # 예: BadRequest / 400
    # PowerShell 7 이후는 응답 본문이 여기에 들어감(API의 오류 메시지)
    $detail = $_.ErrorDetails.Message
    Write-Warning "API 실패 ($status): $detail"
    throw
}

확인 관점. $status에는 HTTP 상태가 열거값으로 들어가고(주석대로 BadRequest 같은 표시가 됩니다), $detail에는 API가 반환한 본문이 문자열 그대로 들어갑니다. JSON을 반환하는 API라면 $detail | ConvertFrom-Json으로 항목을 꺼낼 수 있어, 오류 코드나 필드 이름으로 분기할 수 있습니다. 반대로 $detail이 빈 채로라면 API가 본문을 반환하지 않았거나 Windows PowerShell 5.1에서 실행하고 있는 것입니다(5.1에서는 응답 스트림을 직접 읽어야 합니다. §8).

「HTTP 400이 돌아왔지만 왜 거부됐는지 모른다」는 조사에 시간을 쓰지 않으려면, 오류 본문은 반드시 로그에 남기는 설계로 하세요.

상태 코드로 분기하고 싶은 처리에서는 -SkipHttpErrorCheck를 써서 예외로 만들지 않는 편이 더 단순하게 쓸 수 있습니다(PowerShell 7 이후). 2

$res = Invoke-RestMethod -Uri $uri -SkipHttpErrorCheck -StatusCodeVariable code -TimeoutSec 30
switch ($code) {
    200     { $res.items }
    404     { Write-Warning '대상이 존재하지 않습니다'; @() }
    { $_ -ge 500 } { throw "서버 측 오류: $code" }
    default { throw "예상 밖 응답: $code" }
}

6. 재시도 ── 429와 일시적 실패

PowerShell 6 이후의 Invoke-RestMethod에는 -MaximumRetryCount-RetryIntervalSec가 있어, 실패 시 재시도합니다. 게다가 429 응답에 Retry-After가 포함되면 지정한 간격이 아니라 그 헤더 값이 사용됩니다. 2 즉 「레이트 제한에 걸리면 지시대로 기다렸다가 재시도한다」만이면 내장 기능으로 충분합니다.

# 레이트 제한 대응만이면 이것으로 충분한 경우가 많다
Invoke-RestMethod -Uri $uri -Headers $headers -MaximumRetryCount 4 -RetryIntervalSec 5

다만 재시도 대상은 429만이 아닙니다. 문서는 「400〜599(및 304)의 실패 코드를 받았을 때 재시도한다」고 정합니다. 2 즉 401(인증 오류), 403(권한 부족), 404(URL 오류)처럼 몇 번 보내도 고쳐지지 않는 요청까지 재시도됩니다. 토큰 설정을 틀린 채로 무인 실행하면, 실패로 알기까지 -MaximumRetryCount × -RetryIntervalSec 초를 허비하고, API에는 같은 무효 요청이 반복해서 도착합니다. 영구적 오류에서 즉시 멈추고 싶을 때는 이후의 자체 구현이 필요합니다.

자체 재시도 함수가 필요한 것은 다음과 같은 요구가 있는 경우입니다.

  • 상태 코드마다 처리를 바꾸고 싶다(400번대는 즉시 실패, 5xx만 재시도 등)
  • 지수 백오프로 하고 싶다(내장은 지정 간격으로 재시도)
  • 실패 시 API 오류 본문을 로그에 남기고 싶다

이하는 그 형태입니다.

function Invoke-KsApi {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Uri,
        [string] $Method = 'Get',
        [object] $Body,
        [hashtable] $Headers = @{},
        [ValidateRange(1, 10)] [int] $MaxAttempts = 4,
        # Retry-After가 이보다 긴 대기를 지시하면, 기다리지 않고 중단한다
        [ValidateRange(1, 86400)] [int] $MaxWaitSeconds = 300,
        # API가 멱등 키에 대응하는 경우에만 POST 등의 재시도를 허용한다
        [string] $IdempotencyKey
    )

    # 재시도해도 되는 것은, 같은 요청을 2번 받아도 결과가 바뀌지 않는 경우만.
    # POST/PATCH는 「서버 측에서는 성공했지만 응답이 도착하지 않은」 경우가 있어,
    # 단순히 재전송하면 이중 등록이 된다
    $idempotentMethods = 'Get', 'Head', 'Options', 'Put', 'Delete'
    $canRetry = ($Method -in $idempotentMethods) -or $IdempotencyKey
    if ($IdempotencyKey) { $Headers = $Headers + @{ 'Idempotency-Key' = $IdempotencyKey } }

    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        $params = @{
            Uri                     = $Uri
            Method                  = $Method
            Headers                 = $Headers
            TimeoutSec              = 60
            SkipHttpErrorCheck      = $true          # 코드로 분기하고 싶으므로 예외로 만들지 않는다
            StatusCodeVariable      = 'code'
            ResponseHeadersVariable = 'resHeaders'
        }
        # $Body 값이 $false나 0, 빈 문자열이어도 본문으로 보낼 수 있도록,
        # 참/거짓이 아니라 「인수가 전달됐는지」로 판정한다
        if ($PSBoundParameters.ContainsKey('Body')) {
            # 파이프로 넘기면 빈 배열 @()이 「입력 0건」이 되어 $null이 반환되므로,
            # -InputObject로 열거시키지 않고 그대로 변환한다(@()는 []가 된다)
            $json = ConvertTo-Json -InputObject $Body -Depth 10
            $params.Body        = [System.Text.Encoding]::UTF8.GetBytes($json)
            $params.ContentType = 'application/json; charset=utf-8'
        }

        # -SkipHttpErrorCheck가 억제하는 것은 HTTP 오류 응답만.
        # 타임아웃·이름 확인 실패·연결 재설정·TLS 오류 등, 응답이
        # 돌아오지 않는 통신 오류는 예외로 튀므로, 여기서 잡아 재시도한다
        try {
            $code = $null
            $res  = Invoke-RestMethod @params
        }
        catch {
            # 비멱등한 요청은, 응답이 도착하지 않았을 뿐 서버 측은 성공한
            # 가능성이 있다. 자동 재전송하지 않고, 호출 측에 판단을 맡긴다
            if (-not $canRetry) {
                throw "통신 오류($Method 는 재시도하지 않습니다. 처리 완료 여부를 확인하세요): $($_.Exception.Message)"
            }
            if ($attempt -eq $MaxAttempts) { throw }
            $wait = [math]::Min([math]::Pow(2, $attempt), 60)
            Write-Warning "통신 오류: $($_.Exception.Message) ── $wait 초 후 재시도합니다 ($attempt/$MaxAttempts)"
            Start-Sleep -Seconds $wait
            continue
        }

        if ($code -lt 400) { return $res }                    # 성공

        # 재시도할 가치가 있는 것은 일시적 오류만. 허용 목록 방식으로 명시한다
        # (405나 415 같은 영구적 오류를 재시도하면, 시간을 낭비한 끝에
        #  마지막은 「재시도 상한」이라는 무관한 메시지만 남는다)
        $retryable = @(408, 429, 500, 502, 503, 504)
        if ($code -notin $retryable) {
            throw "API 오류 ($code): $($res | ConvertTo-Json -Compress -Depth 3)"
        }
        # 5xx는 「서버 측에서 처리된 뒤 실패한」 가능성이 있으므로,
        # 비멱등한 요청에서는 재전송하지 않는다. 429도 같다(처리 전에 걸러졌다는 보장은 없다)
        if (-not $canRetry) {
            throw "API 오류 ($code). $Method 는 자동 재시도하지 않습니다: $($res | ConvertTo-Json -Compress -Depth 3)"
        }

        if ($attempt -eq $MaxAttempts) {
            throw "재시도 상한에 도달했습니다 ($code): $($res | ConvertTo-Json -Compress -Depth 3)"
        }

        # Retry-After는 「초」뿐 아니라 HTTP 날짜 형식으로 올 수도 있다.
        # 그대로 [int]로 캐스트하면 예외가 되어, 재시도마다 실패한다
        $wait = $null
        $retryAfter = if ($resHeaders) { $resHeaders['Retry-After'] | Select-Object -First 1 }
        if ($retryAfter) {
            $seconds = 0
            $date    = [datetime]::MinValue
            if ([int]::TryParse($retryAfter, [ref] $seconds)) {
                $wait = $seconds
            }
            elseif ([datetime]::TryParse($retryAfter,
                        [cultureinfo]::InvariantCulture,
                        [System.Globalization.DateTimeStyles]::AdjustToUniversal, [ref] $date)) {
                # 소수부는 반드시 올림한다. [int]는 사사오입(짝수 반올림)이므로,
                # 남은 0.4초를 0초로 반올림해 서버 기한 전에 재전송해 버린다
                $remaining = ($date - [datetime]::UtcNow).TotalSeconds
                $wait = if ($remaining -gt 0) { [int][math]::Ceiling($remaining) } else { 0 }
            }
        }
        if ($null -eq $wait) {
            $wait = [math]::Min([math]::Pow(2, $attempt), 60)             # 지수 백오프(상한 60초)
        }
        elseif ($wait -gt $MaxWaitSeconds) {
            # 서버 지시를 임의로 잘라 빨리 재전송하면, 429를 반복해
            # 상한에 도달할 뿐이다. 기다릴 수 없는 길이는, 대기 시간을 붙여 호출 측에 반환한다
            throw "레이트 제한 중입니다. 서버가 지시한 대기 시간 $wait 초가 상한 $MaxWaitSeconds 초를 넘으므로 중단했습니다. 시간을 두고 다시 실행하세요 ($code)"
        }
        Write-Warning "HTTP $code ── $wait 초 후 재시도합니다 ($attempt/$MaxAttempts)"
        Start-Sleep -Seconds $wait
    }
}

재시도하고 있는지를 확인합니다. 이 함수가 재시도에 들어가면 시도마다 Write-Warning이 한 줄 나옵니다(HTTP 429 ── 30 초 후 재시도합니다 (1/4) 형태입니다). 볼 것은 3점으로, 대기 초가 서버 지시대로인지, 시도 횟수가 -MaxAttempts 범위 안인지, 401이나 404 같은 영구적 오류에서 재시도하지 않고 즉시 예외가 되는지입니다. 무인 실행에서는 화면이 남지 않으므로 경고 스트림도 로그에 남기세요(「Write-Host를 그만두기 ── PowerShell의 출력 스트림과 로그 설계」).

멱등성(같은 요청을 몇 번 보내도 결과가 바뀌지 않는 성질) 처리가 두 번째 포인트입니다. GET이나 PUT은 같은 요청을 2번 받아도 결과가 바뀌지 않지만, POST는 다릅니다. 특히 「서버 측에서는 등록에 성공했지만, 응답이 돌아오기 전에 통신이 끊긴」 경우에는 단순히 재전송하면 이중 등록이 됩니다. 위 구현에서는 멱등한 메서드이거나 API가 멱등 키(Idempotency-Key)에 대응하는 경우에만 자동 재시도를 허용하고, 그 외에는 「처리 완료 여부를 확인하세요」라고 명시하고 멈춥니다.

Retry-AfterHTTP 날짜 형식으로 왔을 때는 남은 시간을 올림하세요. [int]로의 캐스트는 버림이 아니라 사사오입(소수부가 정확히 0.5이면 짝수 쪽)이므로, 응답이 도착할 때까지의 시간을 뺀 나머지가 0.5초 미만이면 0이 됩니다. Start-Sleep -Seconds 0은 즉시 반환되므로 서버가 지정한 시각보다 먼저 재전송하게 되고, 돌아오는 것은 다시 429입니다. 이를 시도 횟수만큼 반복해 「재시도 상한」으로 끝납니다. 위 구현에서 [math]::Ceiling을 넣은 것은 이 때문입니다.

Retry-After를 잘라 내지 않는 것도 중요한 점입니다. 서버가 「30분 뒤에 오라」고 지시하는데 상한을 이유로 5분에 재전송해도, 돌아오는 것은 같은 429뿐입니다. 쓸모없는 요청으로 시도 횟수를 소진한 끝에, 마지막에는 「재시도 상한」이라는 메시지만 남습니다. 위 구현에서는 지시된 대기 시간이 -MaxWaitSeconds를 넘으면 짧게 다시 기다리지 않고, 대기 시간을 붙여 즉시 실패시킵니다. 배치 처리라면 이 예외를 받아 다음 실행으로 넘길지, 지시대로의 시간만 기다릴지를 호출 측에서 고를 수 있습니다.

또 하나의 포인트는 재시도 대상을 허용 목록으로 명시하는 것입니다. 「영구적 오류만 나열해 제외한다」는 쓰기로 하면, 거기에 빠뜨린 코드(405 Method Not Allowed, 415 Unsupported Media Type 등)가 일시적 오류 취급이 되어, 고쳐질 리 없는 요청을 반복한 끝에 마지막에는 「재시도 상한」이라는 원인 불명의 메시지만 남습니다. 일시적이라고 아는 코드만 재시도하고, 그 외에는 API 오류 내용과 함께 즉시 실패시키는 것이 정답입니다. 재시도 설계의 일반론은 「PowerShell의 오류 처리와 재실행 설계」를 참조하세요.

7. 페이징

API가 전체 건을 한 번에 반환하는 일은 거의 없습니다. 방식은 주로 2가지입니다.

(1) Link 헤더 방식(GitHub 등이 채택)은 -FollowRelLink로 다음 페이지를 자동으로 따라갈 수 있습니다. 2

# 다음 페이지를 자동으로 따라간다(가져올 페이지 수 상한도 지정할 수 있다)
$all = Invoke-RestMethod -Uri $uri -Headers $headers -FollowRelLink -MaximumFollowRelLink 20

(2) 커서/오프셋 방식은 직접 루프합니다. 다음 코드는 §6에서 정의한 Invoke-KsApi를 쓰고 있다는 점에 주의하세요. 이 절만 시험한다면 Invoke-KsApi -Uri $u -Headers $headersInvoke-RestMethod -Uri $u -Headers $headers로 바꿔도 동작합니다(재시도와 오류 본문 기록이 사라질 뿐입니다).

$items    = [System.Collections.Generic.List[object]]::new()
$cursor   = $null
$page     = 0
$maxPages = 100

do {
    $page++

    # 원래 URI에 이미 쿼리가 붙어 있는지에 따라 구분 문자가 바뀐다.
    # 항상 ?를 붙이면 .../items?status=active?cursor=... 가 되어,
    # 서버에서는 커서가 status 값의 일부로 보인다.
    # 커서 자체도 불투명한 문자열(+ & = # 등을 포함할 수 있음)이므로 반드시 인코딩한다
    $u = if ($cursor) {
        $sep = if ($uri.Contains('?')) { '&' } else { '?' }
        "$uri$sep" + "cursor=$([uri]::EscapeDataString($cursor))"
    }
    else { $uri }

    $res = Invoke-KsApi -Uri $u -Headers $headers
    $items.AddRange([object[]]$res.items)
    $cursor = $res.nextCursor

    # 중단은 반드시 알린다. 조용히 빠져나가면 「전체를 가져왔다」고 오해된다
    if ($page -ge $maxPages -and $cursor) {
        Write-Warning "페이지 수 상한 ($maxPages)에 도달해 중단했습니다. 누락 가능성이 있습니다"
        break
    }
} while ($cursor)

"가져온 건수: $($items.Count)"

$itemsList[T]를 쓰는 것은 +=에 의한 배열 재작성을 피하기 위해서입니다(「PowerShell 스크립트가 느릴 때 볼 곳」).

확인은 마지막 한 줄로 합니다. 가져온 건수: ...에 나온 수를 API 측 총건수(많은 API는 응답에 total 같은 항목을 갖고 있습니다)나 관리 화면 건수와 맞춰 보세요. 맞지 않으면 먼저 중단 경고가 나오지 않았는지를 봅니다. 경고도 없이 건수가 부족하면 nextCursor 항목 이름이 사양과 맞는지 의심합니다.

페이지 수 상한을 반드시 두는 것도 잊지 마세요. 서버가 같은 커서를 계속 반환하거나, nextCursor를 비우는 것을 잊는 장애는 실제로 일어납니다. 상한이 없으면 그때 스크립트는 멈추지 않은 채 요청을 계속 던집니다. 그리고 중단한 사실은 경고로 드러내야 합니다. 조용히 break하면 호출 측은 불완전한 결과를 전체 건으로 알고 처리해 버립니다.

8. Windows PowerShell 5.1 고유의 함정

5.1이 남아 있는 환경에서는 다음 3점을 먼저 의심합니다. 1

(1) TLS 1.2가 켜져 있지 않다. 오래된 기본 설정 그대로면 TLS 1.2 이상만 받는 API에 연결하지 못하고, 「기반 연결이 닫혔습니다」라는 오류가 됩니다.

# Windows PowerShell 5.1에서의 필수 설정(스크립트 맨 앞)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

(2) -UseBasicParsing이 필요해지는 경우가 있다. 5.1의 Invoke-WebRequest는 기본으로 Internet Explorer 엔진을 써서 HTML을 파싱하므로, IE가 초기화되지 않은 계정(서비스 계정 등)에서 실패합니다. PowerShell 6 이후에는 이 의존이 없어지고, -UseBasicParsing은 지정해도 무시됩니다. 1

(3) -SkipHttpErrorCheck-Authentication이 없다. 5.1에서는 오류 본문을 읽으려면 응답 스트림을 직접 읽어야 합니다. API 연동을 본격적으로 하려면 PowerShell 7을 도입하는 것이 가장 비용이 적습니다(「Windows PowerShell 5.1과 PowerShell 7의 차이」).

9. 프록시와 인증서

사내에서 인터넷상의 API를 호출하는 경우, 프록시 통과가 첫 관문입니다. -Proxy를 지정하지 않으면 인터넷 설정(인터넷 옵션)이나 환경 변수로 구성된 프록시가 사용됩니다. 2 즉 「아무것도 지정하지 않음」은 「프록시를 쓰지 않음」이 아닙니다. 여기가 실행 계정에 따라 결과가 바뀌는 원인입니다.

증상 원인으로 볼 곳 대응
407 Proxy Authentication Required 프록시가 인증을 요구한다 -Proxy와 함께 -ProxyCredential 또는 -ProxyUseDefaultCredentials를 지정한다 2
로컬에서는 통하는데 야간 배치만 실패한다 실행 계정마다 다른 인터넷 설정을 가져온다 스크립트 측에서 -Proxy를 명시한다. 실행 계정을 맞춰 검증한다
사내 API까지 프록시에 빨려 들어간다 기본 프록시 구성이 사내 대상에도 적용된다 -NoProxy로 명시적으로 우회한다(PowerShell 6 이후) 2
프록시 자격 정보의 보관 위치 스크립트에 직접 쓰기 SecretManagement 보관함에서 꺼낸다
# 프록시를 명시하고, 로그온 사용자 자격 정보로 인증한다
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyUseDefaultCredentials

# 인증 있는 프록시에 전용 계정으로 통과한다.
# -ProxyCredential은 -Proxy와 세트로 쓴다. -ProxyUseDefaultCredentials와는 병용할 수 없다
$proxyCred = Get-Secret -Name 'ProxyAccount'   # 보관함에서 꺼낸다
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyCredential $proxyCred

# 사내향 API는 프록시를 통하지 않는다(PowerShell 6 이후)
Invoke-RestMethod -Uri 'https://api.internal.example.local/v1/ping' -NoProxy

-ProxyCredential-ProxyUseDefaultCredentials둘 다 -Proxy 지정이 전제이며 동시에 쓸 수 없습니다. 2 Windows PowerShell 5.1에는 -NoProxy가 없습니다. 5.1에서 사내 대상 통신만 프록시에서 빼려면, 실행 계정의 인터넷 설정 쪽에 있는 예외 목록(프록시를 쓰지 않는 주소)으로 대응하게 됩니다.

작업 스케줄러의 서비스 계정으로 돌리면 대화형 로그온 때와 프록시 설정이 다른 경우가 있습니다. 「로컬에서는 도는데 야간 배치만 실패한다」의 전형입니다. 실행 계정을 맞춰 검증하세요(「작업 스케줄러의 작업이 실행되지 않는다」). 프록시 자격 정보를 스크립트에 직접 쓰지 않는 것은 API 토큰과 같습니다(「PowerShell에서의 자격 정보의 안전한 취급」).

인증서 오류에 -SkipCertificateCheck를 쓰는 것은 검증 환경의 일시적 우회에 한정하세요. 상시 대응은 사내 CA 인증서를 신뢰할 수 있는 루트 인증 기관 저장소에 배치하고, 서버 인증서를 올바르게 발급하는 것입니다.

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

논점 선택지 판단 기준
cmdlet Invoke-RestMethod / Invoke-WebRequest JSON API는 전자. 헤더·코드는 전용 변수로 가져오기 23
인증 헤더 직접 쓰기 / -Authentication 5.1과 공용하면 헤더 방식. 값은 SecretManagement으로 보관
JSON 송신 문자열 / UTF-8 바이트 배열 + charset 명시 환경 차에 의한 문자 깨짐을 피할 수 있다
ConvertTo-Json 기본 / -Depth를 명시 기본은 2. 중첩은 반드시 지정 4
오류 try/catch만 / 본문도 로그에 남긴다 $_.ErrorDetails.Message(PS7). 원인 규명 가능 여부가 바뀐다 2
분기가 많다 catch / -SkipHttpErrorCheck + 코드 분기 상태로 처리를 나누려면 후자가 단순하다 2
재시도 -MaximumRetryCount / 자체 429의 Retry-After에는 내장이 자동으로 따른다. 다만 400〜599 전부를 재시도하므로, 영구적 오류에서 즉시 실패시키려면 자체 2
POST 재시도 멱등 키가 있는 경우만 응답이 도착하지 않았을 뿐 등록은 성공한 경우가 있어, 단순한 재전송은 이중 등록이 된다
페이징 -FollowRelLink / 자체 루프 Link 헤더 방식이면 전자 2
5.1 환경 그대로 / TLS 1.2 명시 + PS7 도입 검토 연결 오류의 상당수는 TLS 설정이 원인 1
인증서 오류 -SkipCertificateCheck / 사내 CA를 신뢰 상시 운영에서 검증을 무효화하지 않는다

11. 정리

  • JSON API는 Invoke-RestMethod가 기본입니다. 헤더나 상태 코드는 -ResponseHeadersVariable / -StatusCodeVariable로 가져올 수 있습니다.
  • 일본어 JSON은 UTF-8 바이트 배열로 보내고, charset=utf-8을 명시합니다. ConvertTo-Json -Depth 지정 누락은 중첩 누락을 초래합니다.
  • 4xx/5xx는 종료 오류입니다. $_.ErrorDetails.Message로 본문을 읽고 로그에 남기세요. 분기가 많으면 -SkipHttpErrorCheck가 다루기 쉬워집니다.
  • POST처럼 비멱등한 요청을 자동 재시도하지 마세요. 통신 오류나 5xx는 「서버 측에서는 처리 완료」일 가능성이 있어, 재전송하면 이중 등록이 됩니다. 재시도하려면 멱등 키 메커니즘이 필요합니다.
  • 429는 Retry-After에 따라 기다리는 것이 기본입니다. -MaximumRetryCount를 쓰면 이 대기는 내장 기능이 처리합니다. 다만 내장은 400〜599 전부를 재시도 대상으로 하므로, 401이나 404를 즉시 실패시키려면 자체 재시도가 필요합니다. 코드별 처리나 지수 백오프가 필요할 때도 같으며, 영구적 오류는 재시도하지 않고 즉시 실패시킵니다.
  • 페이징은 Link 헤더 방식이면 -FollowRelLink, 커서 방식은 자체 루프. 집계에 +=를 쓰지 마세요.
  • 5.1 환경에서는 TLS 1.2 명시, IE 엔진 의존, 기능 부족의 3점이 벽이 됩니다. API 연동을 이어가려면 PowerShell 7 도입이 가장 짧은 해결입니다.

샘플 코드 다운로드

이 기사에서 다룬 코드는 그대로 돌릴 수 있는 형태로 묶어 배포합니다. 재시도·멱등성·페이징을 구현한 API 호출과, 검증용 HTTP 서버가 들어 있습니다.

샘플 코드 다운로드(zip)

이 기사의 샘플은 PowerShell 7.6에서 실제로 실행해 검증했습니다(Pester 21건). zip에 포함된 Invoke-SampleTests.ps1을 실행하면 로컬에서도 같은 검증을 재현할 수 있습니다.

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

zip에는 검증용 HTTP 서버가 들어 있으므로, 외부 API를 치지 않고 로컬에서 왕복을 재현할 수 있습니다. §5(오류 본문)·§6(429에서의 재시도 경고)·§7(가져온 건수)에서 든 확인 관점을 그대로 시험해 보세요. 실제 API에 돌리기 전에 이 3가지를 확인해 두면, 프로덕션에서 「동작하지 않는 이유를 모르겠다」는 상태가 되기 어려워집니다.

설정값(경로, 서버 이름, 테넌트 ID 등)은 예입니다. 그대로 프로덕션 환경에서 실행하지 말고, 자사 환경에 맞게 바꿔 읽으세요.

관련 기사

관련 상담 영역

合同会社小村ソフト에서는 기간 시스템이나 SaaS API를 사용한 사내 연동의 설계·구현, 기존 수작업을 API 연동으로 바꾸는 자동화, 통신 주변의 장애 조사를 다룹니다.

참고 링크

  1. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Web 관련 cmdlet의 동작 차, Windows PowerShell 5.1 고유의 제약, PowerShell 7에서 추가된 기능에 대해. 아울러 ServicePointManager.SecurityProtocol 속성에 의한 TLS 버전 지정에 대해.  2 3 4 5

  2. Microsoft Learn, Invoke-RestMethod. REST 엔드포인트에 요청을 보내고 응답의 JSON/XML을 PowerShell 객체로 변환해 반환하는 것, -Headers / -Body / -ContentType / -Method 지정, -Authentication(Basic / Bearer / OAuth)과 -Token, HTTPS 이외에서의 인증이 기본으로 거부되는 것, -SkipHttpErrorCheck에 의한 4xx/5xx를 예외로 만들지 않는 것, -StatusCodeVariable 및 -ResponseHeadersVariable로 가져오기, -MaximumRetryCount / -RetryIntervalSec에 의한 재시도, -FollowRelLink / -MaximumFollowRelLink에 의한 Link 헤더 페이징, -Proxy / -ProxyCredential / -ProxyUseDefaultCredentials(모두 -Proxy 지정이 전제이며 -ProxyCredential과 -ProxyUseDefaultCredentials는 병용할 수 없는 것), PowerShell 6.0에서 추가된 -NoProxy(인터넷 설정이나 환경 변수로 구성된 프록시를 우회하는 것), -SkipCertificateCheck, -TimeoutSec에 대해.  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

  3. Microsoft Learn, Invoke-WebRequest. 응답을 WebResponseObject로 반환하고 StatusCode·Headers·Content에 접근할 수 있는 것, Windows PowerShell 5.1에서는 기본으로 Internet Explorer 엔진을 써서 HTML을 파싱하고 -UseBasicParsing으로 우회할 수 있는 것, PowerShell 6 이후에는 IE 의존이 없어지고 -UseBasicParsing이 무시되는 것에 대해.  2 3

  4. Microsoft Learn, ConvertTo-Json. -Depth의 기본값이 2이며 그보다 깊은 계층이 변환되지 않는 것, -Compress에 의한 공백 제거에 대해.  2 3

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

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

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

자주 묻는 질문

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

Invoke-RestMethod와 Invoke-WebRequest는 어떻게 구분해서 쓰나요?
API의 JSON이나 XML을 다룰 때는 Invoke-RestMethod입니다. 응답 본문을 자동으로 파싱해 PowerShell 객체로 변환해 주므로 ConvertFrom-Json을 직접 호출할 필요가 없습니다. Invoke-WebRequest는 응답을 HtmlWebResponseObject로 반환하며, 상태 코드·헤더·원시 본문에 접근할 수 있습니다. 상태 코드나 헤더를 보고 싶을 때, 또는 HTML을 그대로 다루고 싶을 때는 Invoke-WebRequest를 선택합니다. 참고로 PowerShell 6 이후의 Invoke-RestMethod에는 -ResponseHeadersVariable과 -StatusCodeVariable이 있으므로, 헤더나 코드만 필요하면 Invoke-RestMethod 그대로도 가져올 수 있습니다.
일본어가 포함된 JSON을 보내면 상대 쪽에서 문자가 깨집니다.
본문의 바이트 열과 Content-Type 선언이 어긋나 있기 때문입니다. 확실한 방법은 ConvertTo-Json으로 만든 문자열을 UTF-8 바이트 배열로 변환해 -Body에 넘기고, -ContentType에 charset=utf-8을 명시하는 것입니다. Windows PowerShell 5.1에서는 문자열을 그대로 넘기면 기본 인코딩으로 보내지는 경우가 있어 이 대응이 특히 유효합니다. 수신 측 문자 깨짐도 마찬가지로, 응답의 charset이 올바르게 선언되어 있는지 의심해 보세요.
API가 404나 500을 반환했을 때 응답 본문의 오류 메시지를 읽고 싶습니다.
PowerShell 7 이후라면 catch 블록에서 $_.ErrorDetails.Message를 보면 응답 본문이 들어 있습니다. 상태 코드는 $_.Exception.Response.StatusCode로 가져올 수 있습니다. 또한 -SkipHttpErrorCheck를 붙이면 4xx/5xx여도 예외로 만들지 않고 일반 응답으로 받을 수 있어, 상태 코드로 분기하고 싶은 처리에서는 다루기 쉬워집니다. Windows PowerShell 5.1에서는 응답 스트림을 직접 읽어야 하므로, 이 점에서도 7 사용을 권장합니다.
API에서 429(레이트 제한)가 돌아옵니다. 어떻게 대응해야 하나요?
응답의 Retry-After 헤더가 가리키는 초만큼 기다린 뒤 재시도하는 것이 기본입니다. 헤더가 없으면 지수 백오프(2초, 4초, 8초…)로 간격을 늘립니다. PowerShell 6 이후에는 -MaximumRetryCount와 -RetryIntervalSec가 있고, 429 응답에 Retry-After가 포함되면 지정한 간격이 아니라 그 헤더 값이 사용되므로, 레이트 제한 대응만이면 내장 기능으로 충분합니다. 상태 코드마다 처리를 바꾸고 싶을 때나 지수 백오프가 필요할 때만 자체 재시도 함수를 사용하세요. 다만 POST처럼 비멱등한 요청의 재시도는 이중 등록 우려가 있으므로, 멱등 키 메커니즘이 없으면 피하세요. 근본적으로는 호출 횟수 자체를 줄이는(필요한 항목만 가져오기, 일괄 조회 API 사용) 편이 확실합니다.
사내 API의 자체 서명 인증서에서 오류가 납니다. -SkipCertificateCheck를 써도 되나요?
상시 운영에서는 피하세요. 인증서 검증을 끈다는 것은 통신 상대가 진짜인지 확인하지 않는다는 뜻이며, 사내 네트워크라도 중간자 공격의 여지를 남깁니다. 올바른 대응은 사내 CA 인증서를 실행 환경의 신뢰할 수 있는 루트 인증 기관 저장소에 배치하고, 서버 인증서를 올바르게 발급하는 것입니다. 검증 환경에서 일시적으로 쓰는 경우에도 프로덕션 스크립트에 섞여 들어가지 않도록, 설정 파일이나 매개변수로 명시적으로 전환하는 형태로 하세요.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기