---
title: "plink·PowerShell 5.1·SQL Server 2022에서 밟은 10가지 함정 ― AI 에이전트에게 Windows 실기 작업을 맡기고(SSH/문자 코드/T-SQL)"
description: AI 에이전트(Claude Code)로 Windows/SQL Server 자동화를 실기 운용하며 밟은 10가지 함정. plink 호스트 키 핀 고정, CP932/BOM, cmd 따옴표, RemoteSigned, SqlClient의 GO 분할·ARITHABORT·sys.partitions를 증상→원인→대처로 해설합니다.
url: "https://bestnetllc.co.jp/ko/docs/ai-agent-windows-sqlserver-pitfalls-kr/"
updated: "2026-10-19"
category: Tech Blog
---

# plink·PowerShell 5.1·SQL Server 2022에서 밟은 10가지 함정 ― AI 에이전트에게 Windows 실기 작업을 맡기고(SSH/문자 코드/T-SQL)

BESTNET TECH BLOG

plink·PowerShell 5.1·SQL Server 2022에서 밟은 10가지 함정

AI 에이전트에게 Windows 실기 작업을 맡기고 ― SSH 비대화화·문자 코드·T-SQL의 “이음매”에서 밟은 실전기

저자: Hideyuki Chinda / BESTNET LLC2026-06-13시리즈 기술 번외편

시리즈 본편에서는, Oracle 19c에서 SQL Server 2022로의 마이그레이션 전처리를, AI 코딩 에이전트(Claude Code)에 실데이터를 넘기지 않고 자동화하여, 마이그레이션 대상 실기에서의 적용·기능 검증까지 완료한 것을 보고했습니다.

본고는 그 이면입니다. AI 에이전트에게 “실기에서의 작업”을 맡기면, 발이 걸리는 것은 대개 **AI의 영리함이 아니라, AI와 셸과 OS의 문자 코드와 도구의 “이음매”**였습니다. SSH의 비대화화, Windows의 문자 깨짐, cmd의 따옴표, 코드로부터의 T-SQL 투입 ―― 어느 것이나 단독으로는 수수하지만, 자동화를 멈추기에는 충분합니다. 실전에서 밟은 함정 중, 본고에서는 기술 쪽 10가지를, 증상·원인·대처의 형태로 남깁니다(나머지 2가지는 AI의 안전 기구에 관한 거버넌스 이야기이므로, [번외편 그 2 “AI 가드레일 편”](https://bestnetllc.co.jp/ko/docs/ai-agent-guardrails-three-layer-governance-kr/)으로). 같은 종류의 자동화를 검토하고 있는 분의 몇 시간을 절약할 수 있다면 다행입니다.

> 본고의 명령 예시는, 접속 대상·인증 정보를 모두 플레이스홀더(`USER@HOST`, `-pw '****'`, `C:work`)로 치환하고 있습니다.

---

## 1. 접속 편 ― 핸즈프리의 SSH를 성립시킨다

AI 에이전트에게 작업시키는 전제로서, **사람이 한 번도 비밀번호를 치지 않고** SSH 명령이 통할 필요가 있습니다. 여기서 곧바로 3가지.

### ① OpenSSH는 비밀번호를 비대화로 넘길 수 없다

- **증상**: `ssh user@host "명령"`이 비밀번호 입력 대기로 멈춘다. 스크립트에서 자동 실행할 수 없다.
- **원인**: OpenSSH 클라이언트는 보안상, 비밀번호를 단말(tty)에서만 읽는다. 표준 입력에 파이프해도 받지 않는다. Windows에는 `sshpass`도 표준으로는 없다.
- **대처**: PuTTY의 `plink`를 사용하여 `-pw`로 비밀번호를 넘긴다.

```
plink -ssh -pw '****' USER@HOST "whoami"
```

> 보안적으로는, 운영 환경에서는 공개 키 인증(키 페어 + `pageant`나 `-i`)을 제일로 검토해 주십시오. `-pw`의 명령줄 넘김은 프로세스 일람이나 셸 이력에 남을 수 있으므로, 부득이하게 사용하는 경우에도 일회용 + 작업 후 로테이션이 전제입니다.

### ② plink의 초회 호스트 키 프롬프트에서 행한다

- **증상**: `echo y | plink ...`로 해도 진행되지 않는다. 타임아웃될 때까지 무반응.
- **원인**: plink는 초회 접속 시의 키 캐시 확인 “Store key in cache? (y/n)”을 단말(tty)에서 읽습니다. AI 에이전트 실행처럼 tty가 없는 환경(파이프/프로세스 기동 경유)에서는, 파이프한 `y`를 받지 못해 행합니다. 대화 콘솔상에서는 `echo y | plink`가 듣는 구성도 있지만, 자동 실행에서는 믿을 수 없습니다.
- **대처**: 먼저 `ssh-keyscan`으로 호스트 키를 취하고, SHA256 핑거프린트를 `-hostkey`로 핀 고정해 버린다. `-batch`로 프롬프트 자체를 무효화.

```
# 1) 호스트 키 지문 가져오기
ssh-keyscan -T 6 HOST 2>/dev/null | ssh-keygen -lf -
#   => 256 SHA256:<fingerprint> <host> (...)  — 키 종류별로 한 줄씩 출력된다

# 2) 가져온 값을 -hostkey로 고정하고 -batch로 비대화식 접속
#    서버가 여러 키 종류(ED25519/RSA 등)를 반환하는 환경에서는 협상될 키에 대비해 각 -hostkey를 나열한다
plink -ssh -hostkey SHA256:<fingerprint> -batch -pw '****' USER@HOST "명령"
```

핀 고정은 보안상으로도 플러스입니다(중간자 공격에 대해, 상정한 키 이외를 튕긴다).

### ③ 장시간 명령은 “던지고 기다리는” 설계로 한다

- **증상**: ISO 다운로드나 제품 인스톨 같은 장시간 처리를 동기 실행하면, 도구 측의 타임아웃에 걸려 끊긴다.
- **원인**: SSH 세션을 쥔 채 10분 이상 블록하는 처리는, 에이전트의 실행 제한에 걸리기 쉽다.
- **대처**: 장시간 처리는 **백그라운드에서 기동하고, 완료를 센티널 파일이나 종료 통지로 줍는다**. SSH 세션을 장시간 점유하지 않는다.

```
:: 비동기로 시작하고 완료 시 종료 코드를 센티널에 기록(같은 cmd /c 안이므로 & 는 순차적으로 평가된다)
start "" /b cmd /c "installer.exe /quiet & echo DONE=%errorlevel%> C:workdone.txt"
:: 별도 명령으로 완료 확인:  if exist C:workdone.txt type C:workdone.txt
```

---

## 2. 문자 코드 편 ― CP932와 BOM의 2단 구성

일본어 Windows를 상대하면, 문자 깨짐은 “겉모습의 문제”가 아니라 **처리를 망가뜨리는 버그**가 됩니다.

### ④ 리모트 출력이 Shift-JIS로 깨진다

- **증상**: 명령 결과가 `�w�肳�ꂽ�p�X...` 같은 문자 깨짐으로 읽을 수 없다.
- **원인**: 일본어 Windows는 CP932(Shift-JIS)로 출력한다. UTF-8 전제의 단말에 넘어가면 깨진다.
- **대처**: 리모트 측 명령의 선두에서 `chcp 65001`(UTF-8로 전환). PowerShell은 출력 인코딩도 명시.

```
chcp 65001>nul & 후속 명령
```

```
[Console]::OutputEncoding = [Text.UTF8Encoding]::new()
```

### ⑤ PowerShell 5.1은 BOM 없는 .ps1을 CP932로 읽는다[가장 시간을 녹인 함정]

- **증상**: 스크립트 중의 일본어 리터럴(`'すべて'` 등)이 `'鬟溷刀'` 같은 다른 것으로 깨지고, `'～' 付近に不適切な構文があります`로 SQL이나 처리가 떨어진다.
- **원인**: **PowerShell 5.1은, BOM이 없는 `.ps1`을 시스템 ANSI 코드 페이지(일본어 환경이라면 CP932)로 해석한다**. UTF-8로 저장한 스크립트의 일본어가 망가진다. ASCII만의 스크립트는 무사하므로, **발각이 늦어지는** 것이 골치 아프다.
- **대처**: 일본어를 포함하는 `.ps1`은 **UTF-8(BOM 첨부)**로 저장한다. PS5.1은 BOM을 보고 UTF-8로 판정한다.

```
# BOM 포함 UTF-8로 기록(기존의 BOM 없는 UTF-8을 변환하는 예)
$txt = [IO.File]::ReadAllText($src, [Text.UTF8Encoding]::new($false))
[IO.File]::WriteAllText($dst, $txt, [Text.UTF8Encoding]::new($true))  # $true = BOM 포함
```

이 2가지(④⑤)는 다른 계층의 문제(단말 출력 vs 스크립트 읽기)이지만, 증상이 같은 “문자 깨짐”이므로 혼동하기 쉽습니다. **출력은 chcp, 입력(스크립트)은 BOM**, 이라고 분리해서 외우면 빠릅니다.

---

## 3. 실행 패턴 편 ― cmd 따옴표 지옥으로부터의 탈출

### ⑥ cmd 경유의 인라인 PowerShell이 `|` `"` `;`로 망가진다

- **증상**: `powershell -Command "Get-ChildItem | Select-Object Name"`을 흘리면 `'Select-Object' は…認識されていません`. `"`의 중첩에서도 무너진다.
- **원인**: SSH→cmd→PowerShell로 다단으로 넘어가기 때문에, `|`를 **cmd가 파이프로서 먼저 해석**해 버린다. 큰따옴표의 중첩도 cmd 단계에서 망가진다. 표준 입력에 흘려 넣는 방식(`-Command -`)도, 긴 스크립트라면 도중에 끊기는 경우가 있었다.
- **대처**: 인라인을 포기하고, 로컬에서 `.ps1`을 쓴다→`pscp`로 전송→`powershell -NoProfile -File`로 실행한다. 본 작업의 환경(Windows Server 2022 기본의 `RemoteSigned`)에서는, 로컬의 미서명 `.ps1`이 그대로 동작했습니다. 이것으로 따옴표 문제도 표준 입력 끊김도 대부분이 해소됩니다.

> 본 작업에서는 이 패턴이 안정되었습니다. “복잡한 리모트 처리는 인라인으로 애쓰지 말고, 파일로 만들어 보내서 `-File`로 두드린다”. 실행 정책을 `Bypass`로 약화시킬 필요도 없습니다(그 조작은 오히려 안전 기구에 막혔습니다 ―― 번외편 그 2 참조).
>
>
>
> 다만 전제에 주의가 필요합니다. `RemoteSigned`로 전송 스크립트가 동작할지는, 조직의 그룹 정책이나 서명 정책(AllSigned/Constrained Language Mode/AppLocker·WDAC 등)에 따라 달라집니다. 또한 “pscp 전송 파일에 MOTW(Mark of the Web)가 붙지 않는다 = 서명 가드가 듣지 않는다”는 것은 이점이 아니라 트레이드오프이며, 제3자 유래의 스크립트에서는 오히려 리스크입니다. 사내에서 내용을 파악한 도구이므로 허용했다, 는 전제를 잊지 마십시오.

```
pscp -hostkey SHA256:xxxx -batch -pw '****' .task.ps1 USER@HOST:C:/work/task.ps1
plink -ssh -hostkey SHA256:xxxx -batch -pw '****' USER@HOST "powershell -NoProfile -File C:worktask.ps1"
```

---

## 4. SQL 투입 편 ― 코드에서 T-SQL을 흘릴 때의 함정

스키마 DDL을, sqlcmd를 거치지 않고 `System.Data.SqlClient`(.NET 표준의 SQL Server 접속 라이브러리. sqlcmd나 SSMS를 사용하지 않고 앱에서 직접 DDL/DML을 발행할 수 있는 반면, sqlcmd 전용 기능 = `GO` 구분 등은 갖지 않는다)로, 통합 인증으로 직접 흘렸을 때의 3연발입니다.

### ⑦ `GO` 분할이, 코멘트 내의 `GO`로 갈라진다

- **증상**: DDL을 배치 실행하면 `コメントの終了マーク '*/' がありません`이나 `'=' 付近に不適切な構文`으로 일부 배치가 떨어진다.
- **원인**: `System.Data.SqlClient`는 `GO`를 이해하지 못합니다(`GO`는 sqlcmd/SSMS의 배치 구분이며 T-SQL이 아니다). 직접 분할할 필요가 있지만, 소박하게 `^s*GOs*$`로 가르면, `/* … */` 코멘트 안에 인덴트로 쓰여진 `GO`(예시 코드 등)까지 구분해 버려, 코멘트가 분단됩니다.
- **대처**: 가르는 것은 행두(열0)의 `GO`만으로 한다. 코멘트 내의 `GO`가 인덴트되어 있으면(이 코드베이스는 그랬습니다) 이것으로 제외할 수 있습니다. 보다 엄밀하게는 “코멘트나 문자열 리터럴을 제외한 다음에 `GO` 분할한다”가 본래적입니다.

```
# 0열의 GO 에서만 분할(들여쓴 주석 안의 GO 는 무시)
$batches = [regex]::Split($script, '(?im)^GO[ t]*;?[ t]*r?$')
```

### ⑧ 인덱스 뷰의 작성이 `ARITHABORT`로 실패한다

- **증상**: `CREATE UNIQUE CLUSTERED INDEX`(인덱스 뷰화)가 `…SET options have incorrect settings: 'ARITHABORT'`로 실패.
- **원인**: `System.Data.SqlClient`는 기본으로 `ARITHABORT OFF`. 인덱스 뷰의 작성에는 복수의 SET 옵션이 ON일 필요가 있다.
- **대처**: 접속 직후에 필요한 SET를 투입하고 나서 DDL을 흘린다.

```
SET ANSI_NULLS ON; SET ANSI_PADDING ON; SET ANSI_WARNINGS ON;
SET ARITHABORT ON; SET CONCAT_NULL_YIELDS_NULL ON;
SET QUOTED_IDENTIFIER ON; SET NUMERIC_ROUNDABORT OFF;
```

### ⑨ 건수 취득에서 `列名 'rows' が無効です`

- **증상**: 테이블별 건수를 `SELECT SUM(p.rows) FROM sys.dm_db_partition_stats p …`로 취하려고 하면 `列名 'rows' が無効です（Invalid column name 'rows'）`로 떨어진다.
- **원인**: `sys.dm_db_partition_stats`(DMV = 동적 관리 뷰. 서버 내부 상태를 반환하는 시스템 뷰)의 행 수 열은 `row_count`이며 `rows`가 아닙니다. `rows` 열을 갖는 것은 `sys.partitions` 쪽. 잘못 알고 있었습니다.
- **대처**: 건수 집계는 `sys.partitions`를 사용한다(이것이 정석).

```
SELECT t.name, ISNULL(SUM(p.rows), 0) AS rows
FROM sys.tables t
LEFT JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1)
WHERE t.is_ms_shipped = 0
GROUP BY t.name ORDER BY t.name;
```

---

## 5. 뒷정리 편 ― 수수하지만 자동화를 멈추는 함정

### ⑩ `Get-ChildItem -Exclude`가 아무것도 반환하지 않아, 뒷정리가 진행되지 않는다

- **증상**: `Get-ChildItem 'C:work' -Exclude 'keep.sql' | Remove-Item -Force`를 실행해도 **1파일도 지워지지 않는다**(에러도 나지 않는다).
- **원인**: 디렉터리 지정의 `-Exclude`는, 경로 끝에 `*`를 붙이거나 `-Recurse`를 병용하지 않으면 **기대대로 열거되지 않고 빈 것을 반환하는** 경우가 있다(PowerShell의 알려진 버릇). `Remove-Item`에 넘어가는 입력이 비어 있으므로, 아무 일도 일어나지 않는다.
- **대처**: 게으름 피우지 말고, 대상을 명시 열거하여 `Remove-Item -LiteralPath -Force`.

```
foreach ($f in 'a.exe','b.iso','c.ps1') {
  Remove-Item -LiteralPath (Join-Path 'C:work' $f) -Force -ErrorAction SilentlyContinue
}
```

---

## 정리 ― 함정은 “영리함”이 아니라 “이음매”에 나온다

늘어놓고 보면, AI 에이전트 자체가 틀린 곳은 거의 없습니다. 막힌 것은 **거의 전부가 이음매**였습니다.

- AI와 셸의 이음매(cmd의 따옴표 해석, 호스트 키 프롬프트의 입력 경로)
- 셸과 OS의 이음매(CP932, BOM)
- 코드와 도구의 이음매(SqlClient가 `GO`를 모른다, DMV의 열명, ARITHABORT의 기본값)

뒤집어 말하면, 이음매를 표준화할수록 자동화는 안정됩니다(물론 다른 이음매가 남는 경우는 있습니다). 본 작업에서의 실용해는 다음 3가지로 집약되었습니다.

1. 접속은 `plink`/`pscp` + 호스트 키 핀 고정 + `-batch`로 핸즈프리화
2. 복잡한 처리는 인라인으로 쓰지 말고, `.ps1`을 보내서 `-File`로 실행(본 작업의 `RemoteSigned` 환경에서는 성립. 조직의 정책에 따라 요확인)
3. 문자 코드는 “출력 = chcp 65001/입력(스크립트) = UTF-8 BOM”으로 분리

또한, 이것들을 잡는 과정에서, AI 에이전트 측의 안전 기구가 AI 자신의 조작을 2회 차단하는 한 장면도 있었습니다. 실행 정책을 약화시키려 한 건과, 인증 정보를 파일에 쓰려고 한 건입니다. 이것은 “함정”이라기보다 **거버넌스 이야기**이므로, [번외편 그 2 “AI 가드레일 편”](https://bestnetllc.co.jp/ko/docs/ai-agent-guardrails-three-layer-governance-kr/)에서 다룹니다.

 베스트네트 합동회사에서는, AI 에이전트를 활용한 인프라·DB 마이그레이션 작업의 자동화 지원을 하고 있습니다. “자사 환경의 이음매를 어떻게 표준화할 것인가”부터 상담해 주십시오.
