배경
사내에서 AI 코드 어시스턴트 도구를 도입하기로 했다. 오픈소스 CLI 도구인 OpenCode를 사내 환경에 맞게 커스터마이징한 버전을 만들어, 개발자들이 로컬에서 AI 어시스턴트를 사용할 수 있도록 하는 것이 목표였다. 이 글에서는 사내 보안상 실제 도구명 대신 mycode라는 이름으로 표기하며, 핵심 개념을 바탕으로 임의로 재작성한 것이다.
mycode는 개발팀이 사내 Git 서버(Gitea)에서 관리하며, 업데이트가 생기면 Gitea에 새 버전을 올리는 방식으로 운영된다. 여기에 더해 API Key를 발급하고 관리하는 포털 앱(MycodePortal)도 함께 개발했다. 직원들은 포털에 로그인해서 API Key를 발급받고, 이를 mycode에 등록해서 사용하는 구조다.
문제는 배포였다. mycode와 포털 앱을 어떻게 직원들에게 쉽게 설치시킬 것인가.
기술 스택
- Python + CustomTkinter: 포털 앱 및 설치 GUI
- PyInstaller: Python 앱을 exe로 빌드
- Inno Setup: Windows 설치 패키지 생성
- Gitea: mycode 버전 관리 및 배포
1. exe 직접 배포 → 설치 패키지로 전환
구현
초기에는 PyInstaller로 빌드한 exe 파일을 직접 전달하는 방식으로 배포했다. 직원들이 받아서 원하는 위치에 두고 실행하면 됐다.
문제
PyInstaller로 빌드된 exe는 실행 시 임시 파일들을 실행 위치에 생성한다. 실행 경로가 제각각이다 보니 임시 파일이 여기저기 생겨 지저분해졌다. 또한 환경변수 등록, 바탕화면 바로가기 생성 등을 사용자가 직접 해야 하는 번거로움도 있었다.
해결
Inno Setup을 도입해 설치 패키지를 만들었다. 사용자는 MycodePortal_Setup.exe 하나만 실행하면 된다. 아래는 Inno Setup 스크립트의 핵심 설정이다.
[Setup]
AppName=MycodePortal
DefaultDirName={autopf}\MycodePortal
PrivilegesRequired=admin ; 환경변수 등록을 위한 관리자 권한
DisableDirPage=no ; 설치 경로 선택 화면 표시
[Icons]
Name: "{autodesktop}\MycodePortal"; Filename: "{app}\MycodePortal.exe" ; 바탕화면 바로가기
[Files]
Source: "dist\MycodePortal.exe"; DestDir: "{app}"; Flags: ignoreversion
설치 경로 선택, 바탕화면 바로가기 생성, 프로그램 추가/제거 등록이 자동으로 처리된다.
2. mycode 번들링 방식 → Gitea 다운로드 방식으로 전환
구현
처음에는 빌드 시점의 mycode 바이너리와 설정 파일을 Setup에 직접 포함(번들링)시켰다. 설치 패키지 안에 mycode가 통째로 들어있는 방식이었다.
; 초기 방식: mycode를 Setup에 직접 번들링
[Files]
Source: "mycode\mycode.exe"; DestDir: "{%USERPROFILE}\mycode"; Flags: ignoreversion
Source: "mycode\.config\*"; DestDir: "{%USERPROFILE}\.config"; Flags: ignoreversion recursesubdirs
문제
mycode는 개발팀이 지속적으로 업데이트하는 도구다. 번들링 방식에서는 mycode가 업데이트될 때마다 Setup을 새로 빌드해서 전 직원에게 재배포해야 했다. 관리 비용이 너무 컸다.
해결
Setup 설치 시 Gitea 서버에서 최신 버전을 직접 받아오도록 변경했다. 백그라운드에서 실행되는 별도의 설치 프로그램(MycodeInstaller.exe)이 Gitea에 접속해 ZIP을 다운로드하고 압축을 해제한 뒤 복사한다.
# installer_core.py - Gitea에서 최신 버전 다운로드 및 설치
GITEA_ZIP_URL = "http://mycode-gitea-server/repo/archive/refs/heads/main.zip"
def install_mycode(log_callback=None):
def log(msg):
if log_callback:
log_callback(msg)
cwd = os.environ["USERPROFILE"]
zip_path = os.path.join(cwd, "mycode.zip")
extract_base = os.path.join(cwd, "mycode-extract")
log("최신 버전 다운로드 중...")
urllib.request.urlretrieve(GITEA_ZIP_URL, zip_path)
log("압축 해제 중...")
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(extract_base)
# mycode.exe 및 .config 복사
target_dir = os.path.join(cwd, "mycode")
shutil.copy2(os.path.join(extract_base, "mycode.exe"), target_dir)
shutil.copytree(os.path.join(extract_base, ".config"),
os.path.join(cwd, ".config"), dirs_exist_ok=True)
log("설치 완료!")
Inno Setup에서는 번들링 대신 설치 후 MycodeInstaller.exe를 실행하도록 변경했다.
[Files]
Source: "dist\MycodePortal.exe"; DestDir: "{app}"; Flags: ignoreversion
; MycodeInstaller.exe는 설치 후 자동 삭제
Source: "dist\MycodeInstaller.exe"; DestDir: "{app}"; Flags: ignoreversion deleteafterinstall
[Run]
Filename: "{app}\MycodeInstaller.exe"; Flags: waituntilterminated shellexec
이후 mycode 업데이트는 개발팀이 Gitea에 새 버전을 푸시하는 것으로 끝난다. 직원들은 포털 앱 내 업데이트 버튼 하나로 언제든 최신 버전을 받을 수 있다.
3. 환경변수 등록 방식 개선
구현
mycode를 터미널 어디서든 실행하려면 시스템 PATH에 등록해야 한다. 처음에는 설치 완료 후 PowerShell 명령어로 시스템 환경변수를 등록했다.
[Run]
Filename: "powershell"
Parameters: "-Command ""[Environment]::SetEnvironmentVariable('Path', $p + ';' + $t, 'Machine')"""
Flags: runhidden waituntilterminated
문제
두 가지 문제가 있었다.
첫째, PrivilegesRequired=admin으로 Setup 자체는 관리자 권한으로 실행되지만, 하위 프로세스인 PowerShell이 권한을 제대로 상속받지 못해 레지스트리 접근 오류가 발생했다.
"요청한 레지스트리에 액세스할 수 없습니다."
둘째, 재설치 시 PATH에 동일한 경로가 중복으로 추가되는 문제가 있었다. 중복 체크 로직에서 Inno Setup 상수({%USERPROFILE})가 실제 경로로 변환되지 않은 채로 비교되었기 때문이었다.
해결
PowerShell을 거치지 않고 Inno Setup의 [Registry] 섹션에서 직접 레지스트리를 수정하도록 변경했다. Inno Setup 엔진이 직접 처리하므로 권한 문제가 사라진다.
[Registry]
Root: HKLM
Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
ValueType: expandsz; ValueName: "Path"
ValueData: "{olddata};{%USERPROFILE}\mycode"
Check: NeedsAddPath('{%USERPROFILE}\mycode')
중복 등록 방지는 [Code] 섹션의 Pascal 함수로 처리했다. ExpandConstant()로 Inno Setup 상수를 실제 경로로 변환한 후 비교하는 것이 핵심이다.
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
ExpandedParam: string;
begin
ExpandedParam := ExpandConstant(Param); // {%USERPROFILE} → 실제 경로로 변환
if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
'Path', OrigPath)
then begin
Result := True;
exit;
end;
Result := Pos(Lowercase(ExpandedParam), Lowercase(OrigPath)) = 0;
end;
마치며
처음엔 단순히 exe 하나 전달하면 되겠지 싶었는데, 막상 배포 자동화를 구현하다 보니 권한 문제, 중복 등록, 버전 관리 등 생각보다 다양한 이슈가 있었다. 특히 PowerShell 대신 Inno Setup [Registry] 섹션을 직접 활용하는 방법은 Windows 배포 자동화에서 알아두면 유용한 패턴인 것 같다.
Gitea 기반 자동 다운로드 구조 덕분에 이후 도구 업데이트는 서버에 푸시하는 것만으로 끝나게 됐고, 재배포 없이 사용자들이 항상 최신 버전을 쓸 수 있는 환경이 만들어졌다.
더 나은 방법이 분명 존재할 것이라고 생각하지만, 프로젝트에는 기한이라는 현실적인 제약이 있다. 의도한 대로 정상적으로 동작하는 지금, 일단은 여기서 마무리하려 한다.
'트러블 슈팅' 카테고리의 다른 글
| OpenCode 포크했더니 @opencode-ai/plugin이 안 된다고? 나도 그랬다 (0) | 2026.03.17 |
|---|---|
| Docker + Bash + Python으로 이기종 DB 간 데이터 이관 자동화하기 (0) | 2026.03.16 |
| RTR을 활용한 토큰 탈취 안정성에 대한 나의 생각 (2) | 2024.12.18 |
| Spring Batch ItemReader 성능개선기 (2) | 2024.12.16 |
| [Spring Batch] ItemProcessor에서의 데이터 필터링 문제 해결 (3) | 2024.10.14 |