사내 AI 코딩 어시스턴트를 전사 배포하면서 겪은 트러블슈팅 기록이다.
"분명히 고쳤는데 왜 어떤 PC는 되고 어떤 PC는 안 되지?"부터 시작해서 결국 원인을 찾아낸 과정을 담았다.
(이 글에서 나타나는 모든 코드, 네이밍 등은 사내 보안상 핵심 개념을 바탕으로 임의로 재작성한 것이다.)
배포 구조 소개
사내에서 운영 중인 자체 Git 서버(Gitea)를 기반으로 AI 코딩 어시스턴트 도구를 배포하는 시스템을 만들었다. 구조는 단순하다.
웹 포털 (다운로드 버튼)
└─ Setup.exe 다운로드
└─ 실행 시 Git 서버에서 최신 바이너리 및 설정 파일 fetch
└─ 환경변수 등록 + 설치 완료
Inno Setup으로 Setup.exe를 만들고, 웹 포털에서 버튼 클릭 시 다운로드되는 구조다.
삽질 1 — LAN 데스크탑은 되는데 WiFi 노트북만 이상하다
증상
설치 후 도구 실행 파일과 스크립트들이 들어가야 할 경로가 PC마다 달랐다.
- LAN 유선 데스크탑 → %USERPROFILE%\mytool\ ✅
- WiFi 노트북 → %APPDATA%\Local\mytool\ ❌ (구버전 경로)
이전 버전에서 설치 경로를 %USERPROFILE%\mytool로 바꿔서 새로 배포했는데, 노트북에서만 계속 구버전 경로로 설치되고 있었다.
1차 시도 — 삭제하고 재설치
가장 먼저 한 건 기존 설치를 완전히 지우고 다시 설치하는 것이었다.
제어판 → 프로그램 제거 → MyTool 제거
→ %USERPROFILE%\mytool 폴더 수동 삭제
→ 재부팅
→ 웹 포털에서 Setup.exe 다시 다운로드 후 설치
결과: 여전히 구버전 경로로 설치됨. 재설치를 해도 똑같았다.
2차 시도 — 레지스트리 잔재 의심
구버전이 HKLM\...\Environment에 MYTOOL_HOME = AppData\Local\mytool을 남겨놨을 거라고 생각했다. 레지스트리 편집기를 직접 열어서 확인하고 삭제했다.
regedit
→ HKEY_LOCAL_MACHINE
→ SYSTEM\CurrentControlSet\Control\Session Manager\Environment
→ MYTOOL_HOME 값 확인 후 삭제
결과: 여전히 똑같았다. 레지스트리도 원인이 아니었다.
원인 파악 — LAN/WiFi 차이에서 힌트
이쯤에서 패턴이 보였다. 잘 되는 PC는 전부 LAN 유선 데스크탑, 안 되는 PC는 전부 WiFi 노트북이었다. 네트워크 경로가 다르다면 브라우저나 프록시 캐시가 개입했을 가능성이 있다고 봤다.
다운로드 버튼을 눌러서 받은 Setup.exe가 실제로 최신 파일인지 확인이 필요했다. 이미 받은 파일과 지금 서버에서 새로 받은 파일의 해시를 비교했다.
# 이미 다운받은 파일 해시
Get-FileHash "$env:USERPROFILE\Downloads\MyTool_Setup.exe" -Algorithm MD5
# Git 서버에서 지금 새로 받아서 비교
Invoke-WebRequest `
-Uri "http://gitserver/repo/raw/branch/main/MyTool_Setup.exe" `
-OutFile "$env:TEMP\MyTool_fresh.exe"
Get-FileHash "$env:TEMP\MyTool_fresh.exe" -Algorithm MD5
결과: 두 해시값이 달랐다.
노트북에서 다운받은 파일은 구버전이었다. 브라우저가 이전에 캐시해둔 파일을 서버에 재요청하지 않고 그대로 돌려주고 있었던 것이다. LAN 데스크탑은 캐시 개입 없이 항상 최신 파일을 받았고, WiFi 노트북은 중간 어딘가에서 캐시가 끼어든 셈이다.
해결 — 다운로드 URL에 타임스탬프 추가
파일명은 그대로 유지하면서 캐시를 무효화하는 방법이다.
// 웹 포털 다운로드 버튼 핸들러
const handleDownload = () => {
const timestamp = Date.now(); // 매 클릭마다 다른 값 생성
const url = `${import.meta.env.VITE_TOOL_FILE_URL}?t=${timestamp}`;
window.location.href = url;
};
?t=1712345678 형태로 매번 다른 쿼리스트링이 붙으면, 브라우저는 이를 새로운 URL로 인식해 캐시를 무시하고 서버에서 새로 받아온다. 다운로드되는 파일명은 MyTool_Setup.exe 그대로다.
이후 모든 노트북에서 정상 경로로 설치되는 걸 확인했다.
삽질 2 — 행망 환경에서 설치가 차단됐다
증상
사내 일반망에서는 잘 되던 설치가, 행정망(행망)이 적용된 PC에서는 설치 도중 보안 프로그램에 의해 차단됐다.
기존 구조
설치/업데이트/로그인을 모두 PowerShell 스크립트(.ps1)로 처리하고, .cmd 파일로 호출하는 구조였다.
:: mytool-login.cmd
powershell -ExecutionPolicy Bypass -File "%~dp0mytool-login.ps1"
:: mytool-update.cmd
powershell -ExecutionPolicy Bypass -File "%~dp0mytool-update.ps1"
그리고 Inno Setup 설치 스크립트 내부에서도 %TEMP%에 PS1 파일을 동적으로 생성한 뒤 실행하는 패턴이 있었다.
// Inno Setup 내부 — 설치 중 임시 PS1 동적 생성
PS1Path := ExpandConstant('{tmp}\install.ps1');
SaveStringsToFile(PS1Path, PSLines, False);
Exec('powershell.exe', '-ExecutionPolicy Bypass -File "' + PS1Path + '"', ...);
원인 파악
행망 보안 솔루션이 두 가지 패턴을 차단했다.
1. ExecutionPolicy Bypass 플래그
Bypass는 PowerShell 보안 정책을 완전히 무력화하는 플래그다. 행망 보안 솔루션은 이 키워드 자체를 악성코드 징후로 탐지하고 실행을 차단한다.
2. %TEMP%에 스크립트 동적 생성 후 실행
임시 폴더에 스크립트를 만들고 즉시 실행하는 패턴은 랜섬웨어나 악성코드가 자주 사용하는 행동 패턴과 동일하다. 행동 기반 탐지 엔진이 이를 차단했다.
해결 — PowerShell 완전 제거, Python exe로 대체
PowerShell 의존성을 완전히 없애고, 동일한 기능을 Python으로 구현한 뒤 PyInstaller로 단일 실행 파일로 빌드했다.
로그인 스크립트 Python으로 재작성
# mytool-login.py
import os, json, getpass, urllib.request
def main():
user_id = input(" User ID: ").strip()
password = getpass.getpass(" Password: ")
body = json.dumps({"user_id": user_id, "password": password}).encode("utf-8")
req = urllib.request.Request(
"http://internal-server/api/login",
data=body,
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=10) as r:
response = json.loads(r.read().decode("utf-8"))
token = response.get("secret_key")
auth_dir = os.path.join(os.environ["USERPROFILE"], ".local", "share", "mytool")
os.makedirs(auth_dir, exist_ok=True)
with open(os.path.join(auth_dir, "auth.json"), "w") as f:
json.dump({"MyTool LLM": {"type": "api", "key": token}}, f, indent=2)
print("[SUCCESS] 로그인 완료.")
os.system("mytool")
if __name__ == "__main__":
main()
업데이트 스크립트 Python으로 재작성 (핵심 로직)
# mytool-update.py
import shutil, zipfile, subprocess, urllib.request
def update():
# 실행 중인 프로세스 확인
result = subprocess.run(["tasklist", "/FI", "IMAGENAME eq mytool.exe"],
capture_output=True, text=True)
if "mytool.exe" in result.stdout:
print("[ERROR] 실행 중인 mytool을 먼저 종료해주세요.")
return
# Git 서버에서 ZIP 다운로드
urllib.request.urlretrieve(GIT_ZIP_URL, ZIP_PATH)
# 압축 해제 후 파일 교체
with zipfile.ZipFile(ZIP_PATH, "r") as z:
z.extractall(EXTRACT_BASE)
shutil.copy2(src_exe, target_exe)
shutil.copytree(src_dist, target_dist)
print("[SUCCESS] 업데이트 완료.")
PyInstaller로 단일 exe 빌드
pyinstaller --onefile --console --name mytool-login mytool-login.py
pyinstaller --onefile --console --name mytool-update mytool-update.py
이렇게 빌드하면 Python 환경이 없는 PC에서도 독립적으로 실행되는 exe가 나온다.
Inno Setup 제거 로직의 PS1도 제거
제거(Uninstall) 시 폴더를 삭제하는 로직에도 PS1 동적 생성 패턴이 남아 있었다. 알고 보니 여기도 같은 문제였다. Inno Setup 내장 함수로 대체했다.
// 🔴 기존 — PS1 동적 생성 후 실행
SaveStringsToFile(PS1Path, PSLines, False);
Exec('powershell.exe', '-ExecutionPolicy Bypass -File "' + PS1Path + '"', ...);
// ✅ 변경 — Inno Setup 내장 함수로 재귀 삭제
if DirExists(TargetDir) then
DelTree(TargetDir, True, True, True);
DelTree는 PowerShell 없이 Inno Setup 자체적으로 폴더를 재귀 삭제하는 내장 함수다. 이걸로 PS1이 코드 전체에서 완전히 사라졌다.
최종 Inno Setup 번들 구조
[Files]
; 설치 실행 파일
Source: "dist\Installer.exe"; DestDir: "{app}"; Flags: ignoreversion deleteafterinstall
; PS1 대신 Python exe 번들
Source: "dist\mytool-login.exe"; DestDir: "{%USERPROFILE}\mytool"; Flags: ignoreversion
Source: "dist\mytool-update.exe"; DestDir: "{%USERPROFILE}\mytool"; Flags: ignoreversion
빌드 파이프라인 정리
1. pyinstaller → mytool-login.exe
2. pyinstaller → mytool-update.exe
3. pyinstaller → Installer.exe (기존 설치 로직)
4. Inno Setup → MyTool_Setup.exe (위 3개 번들)
5. Git 서버에 MyTool_Setup.exe 업로드
이후 행망 PC에서도 보안 프로그램에 차단되지 않고 정상 설치됐다.
마무리
문제 원인 해결
| LAN/WiFi 간 설치 경로 불일치 | 브라우저 캐시가 구버전 Setup.exe 서빙 | 다운로드 URL에 ?t=timestamp 추가 |
| 행망 보안 환경에서 설치 차단 | ExecutionPolicy Bypass + %TEMP% 동적 PS1 생성 패턴 | Python exe로 완전 대체, Inno Setup 내장 함수 활용 |
첫 번째 문제는 증상만 보면 설치 코드 버그처럼 보였지만 실제론 네트워크 캐시 문제였다. LAN/WiFi 차이라는 힌트가 없었다면 한참을 더 헤맸을 것이다. 돌이켜보면 단순한 문제였다. 하지만 원인을 코드 안에서만 찾으려 했기 때문에 시간이 꽤 걸렸다. 증상이 이상하다 싶으면 코드보다 환경 차이부터 의심하는 습관이 필요하다는 걸 이번에 다시 느꼈다.
두 번째 문제는 일반망에서 멀쩡하게 동작하던 코드가 보안 정책이 강화된 환경에서 한 번에 차단되면서 Bypass 키워드 하나의 무게를 실감했다. PowerShell 스크립트 방식은 편리하지만, 보안 환경을 고려해야 하는 상황이라면 처음부터 exe 기반으로 설계하는 편이 낫다.
'트러블 슈팅' 카테고리의 다른 글
| FastAPI + PostgreSQL DB Connection Pool로 안정성 개선하기 (0) | 2026.04.09 |
|---|---|
| 폐쇄망에서의 Jenkins CI/CD 구축 삽질 기록 (0) | 2026.04.09 |
| OpenCode 포크했더니 @opencode-ai/plugin이 안 된다고? 나도 그랬다 (0) | 2026.03.17 |
| Docker + Bash + Python으로 이기종 DB 간 데이터 이관 자동화하기 (0) | 2026.03.16 |
| Python + Inno Setup으로 사내 AI 도구 배포 자동화하기 (0) | 2026.03.11 |