제2편
개발자를 위한 실전 설치 및 활용 매뉴얼
이 편은 세 대를 사기로 한 다음의 회의부터 시작한다. Ollama1를 왜 설치해야 하는지, 이클립스2에서 VS Code3로 건너올 때 막히는 곳, 개발기4 없이 테스트기5에서 함께 개발하고, SVN6과 젠킨스7를 거쳐 운영기8로 넘어가는 순서를 적는다.
연구소가 사기로 한 장비는 M5 Ultra 탑재 Mac Studio 3대다. M2 울트라 견적은 폐기한다. M2는 지금 현업에 넣기 힘든 옛 장비다.
장비: M5 Ultra 탑재 Mac Studio
칩: M5 Ultra (ARM. GPU가 칩 안에 있다)
메모리: 통합 메모리 96GB
CPU와 GPU가 이 메모리를 공용으로 쓴다
수량: 3대
합계: 2,847만 원부터 (대당 949만 원부터)
실행: MLX
역할은 나눈다.
정보시스템팀 여섯 명이 한 줄을 기다리지 않게 하기 위한 대수다. 그래픽 카드를 따로 꽂지 않는다. 96GB를 사면 그 96GB가 모델의 자리다.
| 안 | 막힌 이유 |
|---|---|
| GPU10 서버를 직접 구축 | 랙11, 전원, 반입. 만들다가 한 계절이 간다 |
| NVIDIA12 GPU 서버 군 | 장비값에 전원·냉각·랙이 붙고, 수억 원으로 열린다 |
| 일반 서버·PC + RTX 509013 | 그래픽 메모리 32GB 안에 모델이 갇힌다. 시스템 메모리가 남아도 GPU는 그 자리를 쓰지 못한다 |
| H10014 | 수억 원, 서버실과 공조가 전제. 여섯 명의 코드 보조에 맞지 않는다 |
| M5 Ultra 탑재 Mac Studio × 3 | 2,847만 원부터. MLX15와 통합 메모리16. 사무실에 둘 수 있다 |
싼 이유는 맥이라는 이름 자체가 아니다. GPU가 칩 안에 있고, 메모리를 공용으로 쓰므로 그래픽 메모리 값을 메모리 값으로 대체한다. MLX는 그 구조를 쓰라고 나온 도구다.
화요일 오후 2시. 소회의실.
화이트보드의 5번 동그라미는 아직 지워지지 않았다. M5 Ultra 탑재 Mac Studio 세 대. MLX. 합계는 기본 구성 기준 2,847만 원부터다.
이코딩 주무관이 먼저 물었다.
"장비는 정해지지 않았습니까. 그런데 오늘 안건이 프로그램을 하나 더 설치하자는 겁니까."
김전산 주무관이 노트북을 열지 않고 대답했다.
"MLX로 확인한 것은 이것입니다. 맥 스튜디오의 M5 Ultra는 ARM17 칩 안에 GPU가 있고, 96GB 통합 메모리를 CPU18와 GPU가 같이 씁니다. 모델은 그 메모리에 한 번 올라갑니다. 카드 32GB 안으로 우겨 넣지 않아도 됩니다."
"그러면 그걸로 된 것 아닙니까."
"된 것은 기계입니다. 사람이 여섯입니다."
박개발 사무관이 마커를 집어 들었다.
"김 주무관 자리에서는 모델이 답합니다. 이 주무관 에디터19에서는 그 모델의 주소가 없습니다. 사람마다 파이썬을 띄우면, 이클립스 워크스페이스20 때처럼 되는 사람과 안 되는 사람으로 갈라집니다. 우리는 그 길을 버리려고 소스 원본을 한곳에 모으기로 했습니다. 모델도 마찬가지입니다. 부르는 곳이 하나여야 합니다."
이코딩 주무관이 팔짱을 끼었다.
"그 하나를 우리가 짜면 되지 않습니까. 사내 주소 열고, 맥에 붙어 있는 모델을 대신 대답하게."
최정보 팀장이 고개를 저었다.
"그 코드가 장비보다 길어집니다. 폐쇄망21에서 새로 만든 창구는 우리가 끝까지 고쳐야 하고, 에디터마다 말을 다시 가르쳐야 합니다."
김전산 주무관이 보드에 한 줄을 더 적었다.
에디터 → 내부망 주소 하나 → 맥 스튜디오의 Ollama → 통합 메모리의 모델
"편집기 쪽에는 Continue22라는 확장 프로그램을 붙일 겁니다. VS Code 안에 앉는 AI 도우미입니다. 옆에 대화창을 열어 주고, 고른 코드를 고쳐 주고, 치는 중에 다음 줄을 미리 써 줍니다. 그런데 Continue에는 머리가 없습니다. 질문을 받아 어딘가의 모델에 전하고, 답을 받아 코드 위에 놓는 손일 뿐입니다."
"그 어딘가가 Ollama입니다. Continue가 이미 아는 말이 Ollama입니다. 모델을 인터넷에 물어보는 프로그램이 아닙니다. 맥 안에 모델을 켜 두고, 연구소 망에서만 그 답을 듣게 하는 창구입니다. MLX가 담는 방식을 보여 줬고, Ollama는 그 기계를 팀의 주소로 만듭니다."
정시스템 주무관이 그림을 보다 물었다.
"밖에서 받아 오면 그다음 업데이트는요. VS Code 서버처럼 인터넷을 보다 멈추는 것 아닙니까."
"멈춥니다. 그래서 자동으로 올리지 않습니다." 김전산 주무관이 말했다. "처음 한 번은 외부망 PC에서 설치 파일을 받아 보안 검사를 하고, USB로 맥 스튜디오에 넣습니다. 그 버전으로 고정합니다. 새 버전이 필요하면 반입을 처음부터 다시 합니다. 맥이 스스로 바깥에 물어보게 두지 않습니다."
박개발 사무관이 숫자를 적었다.
"듣는 곳은 내부망뿐입니다. 개발자 PC는 모델을 들고 있지 않습니다. 에디터 설정에 맥 스튜디오 주소만 넣습니다. 모델 파일도 같은 방식으로 반입합니다. 인터넷 문은 열리지 않습니다."
이코딩 주무관이 한참 만에 팔짱을 풀었다.
"정리하면, Ollama는 더 똑똑한 AI를 하나 더 사는 게 아닙니다. 세 대에 올라간 모델을, 여섯 명이 같은 주소로 부르게 하는 프로그램입니다."
최정보 팀장이 마커 뚜껑을 닫았다.
"그 이유로 설치합니다. 김 주무관은 외부에서 받아 검사까지 끝내고, 장비 들어오는 날 맥 스튜디오에 올리세요. 버전은 그 파일로 고정합니다."
회의에서 정한 순서다. 외부망에서 한 번 받고, 검사하고, 맥 스튜디오에 넣는다. 그 다음 업데이트는 같은 반입을 다시 할 때만 한다.
# 외부 인터넷 연결된 Mac에서
# 방법 1: 웹사이트에서 다운로드
open https://ollama.com/download
# 방법 2: Homebrew
brew install ollama
# 설치 파일 위치 확인
ls /Applications/Ollama.app
# 설치 파일 백업 (USB 반입용)
cp -r /Applications/Ollama.app ~/Desktop/
# USB에서 복사
cp -r /Volumes/USB/Ollama.app /Applications/
# 실행 권한 부여
xattr -cr /Applications/Ollama.app
# Ollama 실행
open /Applications/Ollama.app
# 터미널에서 확인
ollama --version
# Output: ollama version is 0.x.x
# 자동 시작 설정 (선택사항)
# ~/Library/LaunchAgents/com.ollama.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ollama</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/ollama</string>
<string>serve</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>OLLAMA_HOST</key>
<string>0.0.0.0:11434</string>
</dict>
</dict>
</plist>
# 서비스 등록
launchctl load ~/Library/LaunchAgents/com.ollama.plist
# 서비스 시작
launchctl start com.ollama
# 확인
curl http://localhost:11434
# Output: Ollama is running
# 외부에서 다운로드
ollama pull deepseek-coder:33b
# 모델 크기: 20GB
# 메모리 사용: 22GB
# 속도: 중간 (5-10 tok/s)
# 품질: 최고
장점:
✅ 코드 생성 품질 최고
✅ 다양한 언어 지원 (Java, Python, JS...)
✅ 주석 작성 우수
✅ 버그 찾기 뛰어남
단점:
❌ 다소 느림
❌ 메모리 많이 사용
추천 용도:
- 복잡한 로직 작성
- 알고리즘 구현
- 아키텍처 설계
ollama pull codellama:34b
# 모델 크기: 19GB
# 메모리 사용: 20GB
# 속도: 빠름 (10-15 tok/s)
# 품질: 우수
장점:
✅ 빠른 속도
✅ 코드 설명 능력 우수
✅ Python에 강함
단점:
❌ 한국어 주석 약함
추천 용도:
- 레거시 코드 분석
- 코드 리뷰
- Python 개발
ollama pull qwen2.5-coder:32b
# 모델 크기: 18GB
# 메모리 사용: 18GB
# 속도: 빠름 (12-18 tok/s)
# 품질: 우수
장점:
✅ 한국어 주석 완벽
✅ 빠른 속도
✅ 코드 품질 우수
추천 용도:
- 한국어 주석 필요한 코드
- 빠른 응답 필요
- 일반 웹 개발
ollama pull qwen2.5-coder:7b
# 모델 크기: 4.7GB
# 메모리 사용: 5GB
# 속도: 매우 빠름 (30-50 tok/s)
# 품질: 보통
장점:
✅ 매우 빠름
✅ 메모리 적게 사용
✅ 동시 사용자 많이 지원 (Mac Studio 1대에 8명)
단점:
❌ 복잡한 로직은 부족
❌ 긴 코드 생성 어려움
추천 용도:
- 간단한 CRUD
- 코드 자동완성
- 다수 개발자 동시 사용
# ===== 외부 인터넷 연결된 Mac에서 =====
# 1. 모델 다운로드
ollama pull deepseek-coder:33b
ollama pull codellama:34b
ollama pull qwen2.5-coder:32b
ollama pull qwen2.5-coder:7b
ollama pull nomic-embed-text # Continue의 코드 색인용 (제7장)
# 2. 모델 파일 위치 확인
cd ~/.ollama/models
ls -lh
# 3. 전체 압축 (약 60GB)
tar -czf ollama-models.tar.gz ~/.ollama/models
# 4. USB SSD에 복사 (128GB USB 권장)
cp ollama-models.tar.gz /Volumes/USB/
# ===== 폐쇄망 Mac Studio에서 =====
# 1. 보안팀 바이러스 검사 통과 후
# 2. 압축 해제
cd /Volumes/USB
tar -xzf ollama-models.tar.gz -C ~/
# 3. 모델 확인
ollama list
# Output:
# NAME SIZE
# deepseek-coder:33b 20GB
# codellama:34b 19GB
# qwen2.5-coder:32b 18GB
# qwen2.5-coder:7b 4.7GB
# nomic-embed-text 274MB
# 4. 테스트
ollama run deepseek-coder:33b "print hello world in python"
폐쇄망에서 오래 일해 온 개발자에게 이 이동은 도구를 하나 더 설치하는 일이 아니다. 손에 밴 습관과, 그 습관이 지켜 온 불안을 함께 옮기는 일이다.
목요일 밤 9시 20분. 정보시스템팀.
박개발 사무관의 모니터 오른쪽 아래에서 같은 글자가 20분째 돌고 있었다.
Building workspace (0%)
이클립스였다. 전자정부 표준프레임워크23 개발환경을 처음 깔았던 날부터 쓰던 것이다. 왼쪽 프로젝트 탐색기에는 프로젝트 마흔일곱 개가 세로로 늘어서 있었고, 절반에는 빨간 X가 붙어 있었다.
박개발 사무관은 익숙한 순서로 손을 움직였다. 프로젝트를 고르고, F5로 새로 고치고, Project 메뉴에서 Clean을 누른다. 그래도 안 되면 이클립스를 껐다 켠다.
"그래도 안 되면요?"
옆에서 보던 김전산 주무관이 물었다.
"워크스페이스를 새로 만들지. 그러면 하루가 가." 박개발 사무관은 화면에서 눈을 떼지 않았다. "그래서 웬만하면 세 번째까지는 안 가."
김전산 주무관은 탐색기 맨 위의 경로를 봤다. D:\workspace_2014.
"사무관님, 이 워크스페이스 몇 년 된 겁니까."
"2014년. 내가 들어온 다음 해." 박개발 사무관이 웃었다. "이거 날아가면 나는 아무것도 못 해. 진짜로."
그는 프로젝트 하나를 펼쳤다. WEB-INF/lib 폴더에 jar24 파일이 백 개 넘게 들어 있었다. ojdbc6.jar가 이름만 조금씩 다르게 세 번 있었고, commons-lang은 버전 두 개가 나란히 있었다.
"이 jar가 왜 여기 있는지 아는 사람은 다 퇴직했어. 빼면 깨지고, 넣으면 돌아가. 그게 전부야."
코드 지옥은 코드에만 있지 않았다. 그 코드가 박개발 사무관의 PC에서만 빌드25된다는 사실이 더 무서웠다. 신규 직원이 오면 첫 주는 박개발 사무관의 워크스페이스를 압축해서 넘겨받아 푸는 데 썼다. 경로가 하나만 달라도 빨간 X가 번졌다. 이코딩 주무관의 워크스페이스는 3년째 박개발 사무관의 것과 똑같이 돌아간 적이 없었다.
다음 날 점심. 구내식당.
김전산 주무관이 숟가락을 내려놓았다.
"사무관님, VS Code로 옮기시죠. 맥 스튜디오의 모델을 부르는 Continue가 VS Code에서 돕니다."
박개발 사무관은 국을 한 숟가락 더 뜨고 나서야 대답했다.
"김 주무관. Ctrl+Shift+R, F3, Ctrl+Alt+H. 이게 뭔지 알지?"
"파일 열기, 선언으로 이동, 호출 계층이죠."
"12년 동안 내 손가락이 외운 거야. 생각하기 전에 눌러져." 그는 손가락으로 식탁을 두드렸다. "그리고 서버 뷰에서 톰캣26 두 번 누르면 뜨잖아. 디버그도 거기서 걸고. VS Code에는 그런 거 없잖아."
김전산 주무관은 부정하지 않았다. 거짓말로 설득할 일이 아니었다.
"맞습니다. 많이 다릅니다."
그는 휴대폰 메모를 열지 않고 하나씩 짚었다.
pom.xml이나 build.gradle을 보고 프로젝트를 알아본다. .project와 .classpath만으로는 자동완성이 살아나지 않는다.launch.json)로 띄운다."그리고 여기는 폐쇄망이야." 박개발 사무관이 마지막 카드를 냈다. "플러그인30 업데이트 사이트 막혀서 고생한 거 기억 안 나? 마켓플레이스 하나 못 여는데 뭘 옮겨."
"그래서 하나씩 들여오려고 합니다. 들어와서 되는 것과 안 되는 것을 먼저 가려 보겠습니다."
박개발 사무관은 대답 대신 식판을 들고 일어났다.
월요일 오전 10시. 소회의실. 주간 업무 회의.
안건 맨 끝에 김전산 주무관이 한 줄을 올려 두었다.
기타. 개발 도구 전환 검토 (이클립스 → VS Code)
앞의 안건이 다 끝나고 그 줄에 이르자, 이코딩 주무관이 먼저 손을 들었다.
"이건 취향 문제가 아니지 않습니까. 우리가 이클립스를 쓰는 건 전자정부 프레임워크31 때문입니다. 개발환경이 이클립스로 묶여서 나오고, 사업 발주할 때도 '전자정부 표준프레임워크 적용'이 들어갑니다. 우리만 다른 도구를 쓰다가 감리32에서 걸리면 어떻게 합니까."
회의실이 조용해졌다. 맞는 말이었다.
최정보 팀장이 볼펜을 내려놓았다.
"그 프레임워크가 왜 생겼는지부터 이야기합시다. 이 주무관은 그 전을 모르죠."
"네."
"2000년대에는 큰 SI33 회사들이 저마다 자기 프레임워크를 들고 들어왔습니다. 이름도 회사마다 달랐어요. 시스템을 다 만들고 계약이 끝나면, 그 프레임워크를 아는 건 그 회사뿐입니다. 고치려면 다시 그 회사를 불러야 했고, 부르는 값은 그 회사가 정했습니다. 기관이 자기 시스템의 인질이 된 겁니다."
박개발 사무관이 낮게 말을 이었다.
"제가 들어오기 직전에 이 연구소가 그랬습니다. 인사 시스템 하나 고치는데 원래 업체 말고는 아무도 소스를 못 읽었어요. 다른 업체는 견적조차 못 냈고요."
"그래서 정부가 공통 틀을 만들어 공개했습니다." 최정보 팀장이 이었다. "스프링34 위에 공통 규칙과 기능을 얹어서, 누가 만들든 같은 구조로 만들게 했죠. 그러면 작은 회사도 유지보수에 들어올 수 있고, 우리 같은 기관 직원도 소스를 읽을 수 있습니다. 대기업이 공공 개발 시장을 자기 틀로 묶어 두는 걸 막는 방패였습니다. 그 시절에는 분명히 그랬어요."
"그럼 지금도 지켜야 하는 것 아닙니까." 이코딩 주무관이 물었다.
최정보 팀장은 바로 답하지 않고 박개발 사무관을 봤다. 박개발 사무관이 천천히 입을 열었다.
"지금은…… 그 방패가 우리 손에 묶여 있는 것 같습니다."
그는 손가락을 하나씩 접었다.
"개발환경이 이클립스 묶음으로 나오니까, 다들 그 묶음을 깔고 그대로 씁니다. 묶음 버전이 바뀔 때까지 플러그인도, 자바 버전도 거기 맞춰 멈춰 있고요. 바깥 개발자들은 다른 도구로 넘어간 지 오래인데, 신규 직원은 밖에서 배운 걸 내려놓고 2014년 워크스페이스부터 풉니다. 발주서에는 이유도 모르고 '표준프레임워크 적용'을 복사해 넣고요. 처음엔 시장을 지키려고 만든 틀이, 이제는 우리가 바깥으로 나가지 못하게 막는 울타리가 됐습니다."
"AI도 마찬가지입니다." 김전산 주무관이 조심스럽게 덧붙였다. "맥 스튜디오의 모델을 부르는 확장들은 대부분 VS Code 쪽에서 먼저 나옵니다. 이클립스에 묶여 있으면, 세 대를 사 놓고도 개발자 자리까지 답이 오지 않습니다."
이코딩 주무관이 다시 물었다.
"그래도 감리는요."
김전산 주무관이 화이트보드에 두 줄을 적었다.
전자정부 표준프레임워크 = 스프링 위의 공통 규칙과 라이브러리 (pom.xml로 받는 jar)
이클립스 = 그 소스를 편집하는 도구 중 하나
"프레임워크와 개발 도구는 다릅니다. 감리가 보는 건 소스가 표준 구조를 따르는지, 표준 라이브러리를 쓰는지입니다. 그 소스를 어떤 편집기로 썼는지는 산출물35에 남지 않습니다. 프로젝트는 그대로 표준프레임워크로 두고, 편집기만 바꾸자는 겁니다."
최정보 팀장이 볼펜 뚜껑을 닫았다.
"정리합시다. 표준프레임워크는 지킵니다. 규정이고, 그 틀 덕분에 우리가 이 소스를 읽을 수 있으니까. 그런데 이클립스까지 지키라는 규정은 없습니다." 그가 잠깐 말을 골랐다. "방패를 내려놓자는 게 아닙니다. 방패에 묶인 손을 풀자는 겁니다."
그는 김전산 주무관을 봤다.
"반입부터 해 보세요. 프로젝트 구조는 건드리지 말고, 편집기만 바꿔서 같은 소스가 같은 산출물을 내는지 확인하는 것까지. 박 사무관이 옆에서 봐 주세요. 이클립스에서 깨지던 자리를 제일 잘 아는 사람이니까."
박개발 사무관은 이번에는 자리에서 일어나지 않았다. 대신 노트에 짧게 적었다.
손을 푼다. 방패는 둔다.
그 주 목요일 오후.
보안 검사에 사흘이 걸렸다. 김전산 주무관은 USB에서 VS Code 설치 파일과 .vsix 파일 묶음을 꺼냈다. Java 확장 팩, Continue, 이클립스 단축키 확장.
설치는 순조로웠다. 프로젝트 폴더를 열었다.
빨간 줄이 없었다. 자동완성도 없었다. 오른쪽 아래 상태 표시줄에 글자 하나가 떠 있었다.
Java: 다운로드 중...
10분이 지나도 그대로였다. 뒤에서 커피를 들고 지나가던 박개발 사무관이 멈춰 섰다.
"봐. 고장 났잖아."
김전산 주무관은 출력 창을 열었다. 언어 서버를 받지 못했다는 줄이 반복되고 있었다.
"고장이 아니라, 인터넷을 찾고 있는 겁니다." 그가 화면을 가리켰다. "Java 확장은 처음 실행될 때 이클립스 JDT 언어 서버를 따로 받아 옵니다. 확장 파일은 들어왔는데, 그 안에서 또 바깥을 부르는 거죠."
박개발 사무관이 커피를 내려놓고 의자를 끌어왔다.
"이클립스 때도 그랬어. 플러그인 깔면, 그 플러그인이 또 뭘 받으러 나가. 오류도 안 내고 그냥 기다려. 사람들은 망 장애인 줄 알고 전산실에 전화하고."
"네. 화면에는 오류 대신 '다운로드 중'만 떠 있습니다. 폐쇄망에서는 그 다운로드가 정책상 안 되는 거고요."
박개발 사무관은 한동안 상태 표시줄을 바라보았다.
"그럼 밖에서 한 번 돌려서, 받아 온 걸 통째로 들고 오면 되겠네. 예전에 이클립스 플러그인을 dropins 폴더에 넣던 것처럼."
김전산 주무관이 고개를 돌렸다. 처음으로 박개발 사무관이 "안 된다"가 아니라 "이러면 된다"를 말했다.
금요일 저녁 7시. 소회의실.
화이트보드 앞에 선 사람은 박개발 사무관이었다.
"12년 동안 반입하면서 깨진 건 내가 제일 많이 알아. 목록부터 쓰자."
그가 보드를 반으로 갈랐다.
| 들여와서 되는 것 | 들여와도 안 되는 것 |
|---|---|
| VS Code 프로그램 자체 | 확장 마켓 검색, 자동 업데이트, Settings Sync |
.vsix 안에 실행 파일이 다 들어 있는 확장 |
첫 실행 때 언어 서버를 따로 받는 기능 |
settings.json, 키 바인딩 같은 설정 파일 |
GitHub36 로그인, Live Share, Copilot 같은 외부 계정 기능 |
| 밖에서 미리 받아 온 언어 서버 폴더 | 서버에 원격 접속할 때 받는 VS Code Server (따로 반입해야 함) |
"VS Code Server는 뭐야?" 박개발 사무관이 마지막 줄에서 마커를 멈췄다.
"서버에 원격으로 붙어서 개발하게 되면, 그 서버 쪽에 VS Code와 똑같은 버전의 짝 프로그램이 깔려야 합니다. 그것도 인터넷에서 받습니다. 버전이 하나라도 다르면 접속이 '서버를 받는 중'에서 멈추고요."
"그럼 버전을 고정해." 박개발 사무관이 바로 말했다. "팀 전체가 하나로. 이클립스 때 사람마다 버전이 달라서 워크스페이스가 안 맞았던 거, 그거 다시 하지 말자. 자동 업데이트는 끄고, 올릴 때는 다 같이 반입부터 다시 해."
김전산 주무관은 그 말을 그대로 받아 적었다.
박개발 사무관이 보드 구석에 한 줄을 더 썼다.
pom.xml 없는 옛날 프로젝트는?
"우리 프로젝트 절반은 메이븐37도 없어. WEB-INF/lib에 jar 넣고 .classpath로 버틴 거야."
"그 폴더를 직접 알려 주면 됩니다. VS Code Java 설정에 jar 폴더 경로를 적는 항목이 있습니다." 김전산 주무관이 보드에 적었다.
"java.project.referencedLibraries": ["WebContent/WEB-INF/lib/**/*.jar"]
박개발 사무관은 그 줄을 오래 보았다.
"이 한 줄이 워크스페이스를 대신하는 거네. 그럼 이건 내 PC가 아니라 소스랑 같이 형상관리38에 올라가겠고."
"네. 신규 직원은 폴더만 받으면 됩니다."
다음 주 화요일 오전 10시.
언어 서버 폴더를 함께 반입하고 나서야 빨간 줄이 돌아왔다. 박개발 사무관의 PC에 VS Code가 깔렸다. 이클립스는 지우지 않았다. "한 달은 둘 다 켜 두자. 지울지는 그다음에 정하자." 그가 먼저 정한 규칙이었다.
그는 VS Code 창을 앞에 두고 습관처럼 Ctrl+Shift+R을 눌렀다.
파일 찾기 창이 열렸다.
그는 잠시 손을 멈췄다가, 클래스 이름 위에서 F3을 눌렀다. 선언으로 넘어갔다. Ctrl+Alt+H. 호출 계층이 떴다.
"……반은 돌아왔네."
"나머지 반은 맥 스튜디오가 채울 겁니다."
김전산 주무관이 Continue 창을 열었다. 박개발 사무관은 잠깐 망설이다가, 몇 해 동안 아무도 건드리지 못한 급여 계산 메서드를 드래그했다.
이 메서드가 무엇을 계산하는지 설명해 줘. 123456은 왜 빼는 거야?
답은 연구소 내부망 안에서 왔다. 맥 스튜디오 세 대 중 한 대가 통합 메모리에 올려 둔 모델이었다. 패킷39은 건물 밖으로 나가지 않았다.
박개발 사무관은 답을 끝까지 읽고 의자에 등을 기댔다.
"김 주무관. 내가 12년 동안 이클립스를 못 떠난 게, 이클립스가 좋아서였을까?"
김전산 주무관은 대답하지 않았다.
"다른 데로 가면 처음부터 다시 외워야 할까 봐 무서웠던 거야. 워크스페이스가 깨지면 나는 아무것도 아닌 사람이 될까 봐."
그는 톰캣을 띄우는 실행 구성을 직접 만들어 launch.json으로 저장했다. 그리고 그 파일을 팀 공유 문서에 첨부했다. 제목은 "서버 뷰 대신"이었다.
그날 저녁, D:\workspace_2014는 지워지지 않았다. 압축되어 공유 폴더 한쪽에 들어갔을 뿐이다.
그래도 다음 신규 직원은 첫 주를 그 압축 파일을 푸는 데 쓰지 않을 것이다.
두 사람이 화이트보드에 남긴 내용을 실무 순서로 옮긴다.
# 외부망 PC
# VS Code 설치 파일 (.dmg 또는 Windows .exe)
# https://code.visualstudio.com/download
# 필요한 확장은 마켓에서 .vsix 로 받는다
# 예: Continue, Java 확장 팩, 이클립스 단축키(Eclipse Keymap)
# 폐쇄망 PC
code --install-extension continue-*.vsix
code --install-extension java-pack-*.vsix
code --install-extension eclipse-keybindings-*.vsix
.vsix만 넣으면 빨간 줄과 자동완성이 비어 있다..vsix와 함께 반입한다.{
"java.project.referencedLibraries": ["WebContent/WEB-INF/lib/**/*.jar"]
}
이 설정을 .vscode/settings.json에 두고 소스와 함께 SVN에 올린다. 워크스페이스를 압축해 넘기던 일이 이 파일 하나로 바뀐다.
제6장에서 정한 대로 각자 테스트기의 개인 계정에 원격으로 붙어 개발한다. 이때 개발자 PC의 VS Code와 같은 커밋40의 서버 파일이 테스트기에 있어야 하고, 그 파일은 인터넷 배포41라 따로 반입한다.
code --version 출력의 둘째 줄이 커밋이다.server-linux-x64다.~/.vscode-server/bin/<커밋>/에 풀어 둔다.# 외부망. COMMIT 은 code --version 의 둘째 줄
curl -L "https://update.code.visualstudio.com/commit:${COMMIT}/server-linux-x64/stable" \
-o vscode-server.tar.gz
# 폐쇄망 테스트기. 계정은 개발자 개인 계정(dev_이름)
mkdir -p ~/.vscode-server/bin/${COMMIT}
tar -xzf /Volumes/USB/vscode-server.tar.gz \
-C ~/.vscode-server/bin/${COMMIT} --strip-components 1
{
"update.mode": "none",
"extensions.autoUpdate": false,
"remote.SSH.localServerDownload": "always"
}
localServerDownload는 개발자 PC가 서버 파일을 대신 받아 복사하라는 설정이다. PC도 폐쇄망이면 이 값만으로는 파일이 생기지 않는다. 파일은 위의 반입이 있어야 한다.
launch.json)은 한 사람이 만들어 공유한다. 서버 뷰를 대신한다..vscode/settings.json을 형상관리에 올리면, 신규 직원은 폴더만 받으면 된다.Mac Studio는 개발 서버42가 아니다. 세 대는 AI 모델이 답을 내는 장비이고, 업무 시스템의 소스를 두거나 빌드하거나 띄우는 자리가 아니다. 그러면 소스는 어디에 두고, 누가 빌드하고, 어떻게 운영까지 가져갈 것인가. 이 장은 그 자리를 찾다가 한 번 크게 넘어진 이야기다.
월요일 오후 3시. 정보시스템팀.
김전산 주무관은 구매 요청서의 '사유' 칸을 세 번째 고쳐 쓰고 있었다.
품명: 개발용 서버(개발기) 1식
사유: 운영기와 같은 환경의 테스트기를 개발 서버로 겸용하고 있어,
개발 중인 소스와 시험 대상 소스가 한 서버에 섞임.
소스 원본과 빌드 기준을 별도 장비로 분리하고자 함.
견적: 68,700,000원
연구소에는 운영기와 똑같이 꾸민 테스트기가 이미 있었다. OS도, WAS43도, JDK44도, DB 버전도 운영과 같았다. 규정대로라면 그 테스트기에서 개발하고, 검사를 거쳐 운영기에 올리면 된다.
하지만 김전산 주무관은 그 길이 불안했다. 여러 사람이 한 서버에서 동시에 고치면 누가 무엇을 바꿨는지 흐려진다. 고치다 만 코드와 검사받을 코드가 섞인다. 테스트기를 재기동하면 개발과 시험이 같이 멈춘다. 소스 원본이 어디 있는지도 분명하지 않다. 대부분의 현장이 개발기를 한 대 더 두는 이유가 그것이었다.
이코딩 주무관이 모니터 너머로 고개를 내밀었다.
"맥 스튜디오가 세 대인데, 한 대를 개발기로 쓰면 안 됩니까. 7천만 원 아끼잖아요."
"안 됩니다." 김전산 주무관이 바로 답했다. "운영기는 리눅스에 자바입니다. 맥에서 되는 게 운영에서 된다는 보장이 없습니다. 모델이 통합 메모리를 크게 쓰는 동안 빌드까지 올리면 둘 다 느려지고요. 그리고 그 한 대가 서면 AI랑 개발이 같이 섭니다."
박개발 사무관이 요청서를 넘겨받아 읽었다.
"사유는 이 정도면 됐어. 견적이 좀 세긴 한데, 팀장님이 위로 올려 보신다고 했으니까."
최정보 팀장은 그날 저녁 요청서를 결재판에 끼웠다.
3주 뒤 목요일 오전.
업체에서 견적서가 다시 왔다. 결재가 늦어진 사이에 유효기간이 지난 것이다.
김전산 주무관은 숫자를 두 번 읽었다.
견적: 131,500,000원
"사무관님."
박개발 사무관이 와서 화면을 봤다. 한참 말이 없었다.
"메모리야." 그가 낮게 말했다. "AI 데이터센터들이 서버 메모리를 다 쓸어 가고 있다더니. 같은 사양인데 메모리 값만 몇 배가 됐네."
6천만 원대 후반이던 서버가 3주 만에 1억 3천만 원대가 됐다. 사양은 한 줄도 바뀌지 않았다.
오후에 최정보 팀장이 김전산 주무관을 불렀다.
"올해 남은 장비 예산으로는 안 됩니다. 소장님께 1억 3천을 다시 올리면, 맥 스튜디오를 산 지 얼마나 됐다고 또 장비냐는 말부터 나올 겁니다." 팀장은 견적서를 가지런히 접었다. "개발기는 접읍시다. 올해는."
김전산 주무관은 고개를 끄덕였다. 할 말이 없었다.
자리로 돌아오자 박개발 사무관이 커피 한 잔을 그의 책상에 내려놓았다.
"맥 스튜디오 견적을 먼저 받아 둔 게 천만다행이다. 그거까지 이번 달이었으면 세 대가 두 대 됐어."
"……개발기 없이 어떻게 합니까."
"있는 걸로 하는 거지." 박개발 사무관이 의자를 끌어와 앉았다. "우리가 언제 다 갖추고 일했어."
다음 주 월요일 오전 10시. 소회의실.
화이트보드에는 박개발 사무관이 먼저 한 줄을 적어 두었다.
개발기 없음. 테스트기에서 같이 개발한다. 대신 불안한 걸 하나씩 없앤다.
김전산 주무관이 그 아래에, 개발기를 사려던 이유를 그대로 옮겨 적었다.
1. 누가 무엇을 바꿨는지 흐려진다
2. 고치다 만 코드와 검사받을 코드가 섞인다
3. 빌드 기준이 사람마다 다르다
4. 운영에 무엇이 올라갔는지 증명하기 어렵다
"하나씩 지웁시다." 최정보 팀장이 말했다.
정운영 주무관이 먼저 손을 들었다. 테스트기 관리는 그의 담당이었다.
"1번과 2번은 계정으로 풉니다. 테스트기에 사람마다 계정을 하나씩 만들겠습니다. dev_kim, dev_lee, dev_park. 각자 자기 계정 홈에 소스를 따로 받아서, 거기서만 고칩니다. 남의 작업 사본45은 건드릴 수 없게 권한을 막고요."
"소스 원본은?" 박개발 사무관이 물었다.
"SVN입니다." 김전산 주무관이 답했다. "각자 자기 저장소 사본에서 개발하고, 다 되면 SVN에 커밋합니다. 커밋에는 이름과 시각과 내용이 남습니다. 누가 무엇을 바꿨는지는 SVN이 기억합니다."
이코딩 주무관이 3번을 가리켰다.
"빌드 기준은 제가 맡겠습니다. 젠킨스를 올리죠. SVN에 커밋이 들어오면 젠킨스가 그걸 받아서 빌드합니다. 사람 손으로 빌드한 파일은 어디에도 배포하지 않습니다."
"빌드가 깨지면요?" 최정보 팀장이 물었다.
"커밋한 사람에게 바로 알림이 갑니다. 자기 계정으로 돌아가서 다시 고치고, 다시 커밋합니다. 빌드가 성공하면 젠킨스가 그 산출물을 테스트 WAS에 배포하고요."
김전산 주무관이 보드에 길을 그렸다.
각자 계정 (dev_kim / dev_lee / dev_park …)
│ 자기 작업 사본에서 개발 ← VS Code + Continue (맥 스튜디오)
▼
SVN 커밋
▼
젠킨스 빌드 ──실패──▶ 다시 자기 계정에서 수정
│ 성공
▼
테스트 WAS 배포 → 테스트기에서 개발 시험
│ 이상 없음
▼
일정 확정 → 운영기 배포
"4번은 내가 지웁니다." 최정보 팀장이 마커를 받아 들었다. "운영 배포는 날짜를 잡아서 합니다. 테스트기에서 시험이 끝난 그 빌드 번호만 올립니다. 날짜는 내가 정하고, 결재도 내가 올립니다."
역할을 나누는 데 30분이 걸렸다. 대부분은 서로 일을 더 가져가겠다는 말이었다.
"젠킨스 빌드 실패 알림은 첫 달 동안 내 메일로도 오게 해 줘." 박개발 사무관이 말했다.
"사무관님 것까지 받으시면 밤에 쉬지를 못하십니다." 김전산 주무관이 말렸다.
"그러니까 받겠다는 거야." 박개발 사무관이 웃었다. "12년 동안 내 워크스페이스는 나 혼자 붙들고 있었어. 깨지면 나 혼자 밤새웠고. 이제 소스가 한곳에 모이잖아. 그럼 같이 붙드는 거지. 처음 한 달은 내가 제일 앞에 설게."
이코딩 주무관이 펜을 굴리다 말했다.
"그럼 테스트 WAS 배포 스크립트는 제가 밤에 한 번씩 돌려 보겠습니다. 저도 맥 스튜디오 안 된다고 제일 먼저 떠든 사람이라, 이번엔 빚 좀 갚아야죠."
정운영 주무관이 손을 들었다.
"SVN 저장소는 테스트기에 같이 올라가니까, 매일 밤 백업은 제가 다른 저장 장치로 빼 두겠습니다. 테스트기가 서도 소스는 살아 있게요."
최정보 팀장은 그 말들을 한참 듣고만 있다가, 마지막에 짧게 말했다.
"운영 배포에서 사고가 나면 제가 먼저 불려 갑니다. 그건 팀장 몫입니다. 여러분은 테스트기에서 끝까지 확인만 해 주세요. 거기까지 해 주면, 나머지는 내가 막습니다."
잠깐 아무도 말하지 않았다.
이코딩 주무관이 분위기를 깼다.
"규칙 하나 추가하죠. 젠킨스 빌드 깨뜨린 사람이 그날 커피 삽니다."
"찬성." 박개발 사무관이 제일 먼저 손을 들었다.
그 규칙의 첫 번째 희생자는 이틀 뒤, 박개발 사무관 본인이었다. 이클립스에서 쓰던 자바 버전 설정을 그대로 커밋한 탓이었다. 그는 빨간 빌드 화면을 캡처해 단체 대화방에 올리고, 여섯 잔을 사 왔다.
"원래 대장이 먼저 맞는 거야."
2주 뒤 금요일 오후 5시 40분.
계정 여섯 개, SVN 저장소, 젠킨스, 테스트 WAS. 정운영 주무관이 마지막으로 방화벽46 설정을 확인하고 엄지를 들었다.
김전산 주무관이 자기 계정에서 마지막 수정을 마쳤다. 환경연구부의 안전교육 이수 화면이었다. 엑셀 업로드 부분은 Continue가 맥 스튜디오의 모델에서 초안을 받아 왔고, 그는 그 초안을 이틀 동안 고쳤다.
svn commit -m "[환경연구부] 안전교육 이수 엑셀 업로드 검증 추가"
여섯 명이 그의 모니터 뒤로 모였다. 젠킨스 화면에서 빌드 번호 옆의 동그라미가 돌기 시작했다.
컴파일47. 테스트. 패키징.
동그라미가 초록색으로 바뀌었다.
Build #27 SUCCESS
Deploy to test-was SUCCESS
누군가 작게 "오" 하고 말했다. 김전산 주무관이 테스트기 주소를 열었다. 방금 커밋한 화면이 그대로 떠 있었다. 엑셀 파일을 올리자, 잘못된 행 세 줄에 빨간 표시가 붙었다.
"됐네." 박개발 사무관이 말했다. 목소리가 조금 떨렸다.
최정보 팀장이 시계를 봤다.
"다들 오늘 일정 없죠. 고기 먹으러 갑시다."
같은 날 저녁 7시 반. 연구소 앞 고깃집.
불판 위에서 삼겹살이 익어 갔다. 여섯 개의 잔이 채워졌다.
최정보 팀장이 잔을 들었다.
"개발기는 내년 본예산에 다시 올립니다. 메모리 값이 내려오면, 그때는 꼭 삽니다." 그가 잠깐 웃었다. "그런데 오늘 보니까, 개발기가 없어서 생긴 빈자리를 여러분이 다 메웠습니다. 장비보다 사람이 먼저 갖춰진 팀입니다. 고맙습니다."
"건배!"
이코딩 주무관이 잔을 내려놓으며 말했다.
"솔직히 처음엔 맥 스튜디오도, VS Code도, 젠킨스도 다 반대했잖아요. 근데 오늘 초록불 뜨는 거 보니까…… 이번 달 안에 차량 예약 시스템도 이 길로 내보내고 싶어졌습니다."
"다음 주에 바로 커밋해." 박개발 사무관이 고기를 뒤집으며 말했다. "깨뜨리면 커피고."
정운영 주무관은 벌써 다음 이야기를 하고 있었다.
"연구장비 쪽 레거시48도 SVN에 올리면 어떨까요. 장비 담당 선생님들이 매번 USB로 소스를 들고 오시는데, 그것도 젠킨스로 돌리면……"
"한 잔 더 마시고 이야기하자." 최정보 팀장이 그의 잔을 채웠다.
김전산 주무관은 휴대폰을 꺼냈다. 단체 대화방 맨 위에 젠킨스 알림이 떠 있었다.
Build #27 SUCCESS
그는 화면을 끄지 않고 잠시 바라보았다. 옆자리의 박개발 사무관이 그의 어깨를 툭 쳤다.
"내일부터가 진짜야."
"압니다."
개발기는 끝내 오지 않았다. 대신 여섯 개의 계정과 하나의 저장소, 그리고 초록불 하나가 생겼다. 그날 밤 고깃집을 나서는 여섯 명은, 각자 다음 주에 커밋할 화면을 머릿속에 그리고 있었다.
회의에서 정한 내용을 실무 순서로 옮긴다. 개발기를 둘 수 있게 되더라도 SVN, 젠킨스, 계정 분리 원칙은 그대로 쓴다.
| 자리 | 하는 일 | 두지 말아야 할 것 |
|---|---|---|
| 개발자 PC | VS Code, Continue로 맥 스튜디오 호출, 테스트기 개인 계정에 원격 접속 | 배포 기준이 되는 빌드 |
| 테스트기 개인 계정 | 자기 SVN 작업 사본에서 개발, 컴파일 확인 | 남의 작업 사본, 직접 배포 |
| SVN (테스트기) | 소스 원본과 변경 이력 | 빌드 산출물, 개인 설정 파일 |
| 젠킨스 (테스트기) | SVN 커밋을 받아 빌드, 성공 시 테스트 WAS 배포 | 사람이 손으로 만든 산출물 |
| 테스트 WAS | 운영과 같은 환경에서 개발 시험 | 운영 데이터 |
| 운영기 | 일정을 잡아, 시험이 끝난 빌드만 배포 | 시험하지 않은 빌드 |
| Mac Studio | AI 모델 응답(Ollama/MLX) | 업무 시스템 소스, 빌드, WAS |
dev_이름). 남의 홈 디렉터리는 읽거나 쓸 수 없게 권한을 막는다.svn checkout) 그 안에서만 개발한다. VS Code로 원격 접속해 편집하고, Continue는 맥 스튜디오의 모델을 부른다.맥 스튜디오 세 대가 머리가 되었고, Ollama가 그 머리를 부르는 창구가 되었다. 이클립스를 내려놓고 VS Code로 건너왔고, 소스가 지나갈 길도 SVN과 젠킨스로 깔았다. 그런데 아직 개발자의 손끝에 닿은 것은 하나도 없었다. 이 장의 주인공은 그 마지막 한 조각, Continue다.
젠킨스가 첫 초록불을 켠 다음 주 월요일 오전 9시 반. 소회의실.
최정보 팀장이 자리에 앉자마자 물었다.
"장비 사고, 프로그램 깔고, 편집기 바꾸고, 길까지 깔았습니다. 그런데 솔직히 묻겠습니다. 그래서 Continue가 정확히 뭡니까. 소장님이 물어보시면 한 문장으로 답해야 합니다."
김전산 주무관은 화이트보드에 상자 세 개를 그렸다.
[개발자 PC의 VS Code] [맥 스튜디오]
└ Continue ──질문──▶ Ollama ──▶ 모델
◀─답(코드)─
"한 문장으로 하면, VS Code 안에 앉아 있는 AI 동료입니다. 맥 스튜디오가 머리이고, Ollama가 그 머리로 통하는 창구라면, Continue는 우리 편집기 안에서 질문을 받아 창구로 들고 가고, 답을 받아 코드 위에 놓아 주는 손입니다."
"머리는 없다는 겁니까?" 팀장이 물었다.
"없습니다. Continue 자체는 똑똑하지 않습니다. 어느 모델에 물을지는 우리가 정합니다. 그래서 우리가 쓸 수 있는 겁니다. 머리를 맥 스튜디오로 정해 주면, 질문도 답도 연구소 안에서만 오갑니다."
박개발 사무관이 팔짱을 끼고 말했다.
"결국 VS Code로 옮긴 것도, 그 고생을 한 것도 이거 하나 때문이라는 거네."
"네." 김전산 주무관이 숨기지 않았다. "이클립스에는 이 손이 붙지 않습니다."
그는 상자 아래에 Continue가 하는 일을 네 줄로 적었다.
| 하는 일 | 부르는 법 | 우리 일로 치면 |
|---|---|---|
| 대화 | Ctrl+L (맥은 Cmd+L) |
옆자리 선배에게 "이 메서드 뭐 하는 거예요?" 하고 묻기 |
| 고치기 | 코드를 고르고 Ctrl+I |
"여기 빈 행 검사 좀 넣어 줘" 하면, 고친 줄을 초록·빨강으로 보여 주고 내가 받을지 정하기 |
| 자동완성 | 타이핑하면 회색 글씨, Tab으로 받기 |
내가 칠 다음 줄을 미리 쳐 두기 |
| 문맥 붙이기 | 대화창에서 @ |
"이 파일이랑, 이 오류 로그랑, 저 폴더 보고 답해 줘" |
이코딩 주무관이 손을 들었다.
"밖에서는 다들 깃허브 코파일럿49이나 커서50 쓰던데요. 유명한 걸 쓰면 안 됩니까."
"그 도구들은 코드를 업체의 클라우드51로 보내서 답을 받습니다. 로그인과 인터넷이 전제예요. 폐쇄망에서는 켜지지도 않고, 켜진다 해도 우리 소스를 밖으로 보내는 순간 보안 규정 위반입니다."
김전산 주무관이 표를 하나 더 그렸다.
| 클라우드형 AI 도구 | Continue + 맥 스튜디오 | |
|---|---|---|
| 인터넷 | 반드시 필요 | 필요 없음 |
| 코드가 가는 곳 | 업체 서버 | 연구소 안 맥 스튜디오 |
| 모델 | 업체가 정함 | 우리가 정함 (Ollama의 모델) |
| 비용 | 사람마다 월 구독 | 확장은 무료, 장비는 이미 삼 |
| 소스 공개 | 비공개 | 오픈소스52, 보안 검토 가능 |
정운영 주무관이 조심스럽게 물었다. 보안성 검토 서류는 결국 그의 책상으로 올 것이었다.
"오픈소스라도, 확장이 몰래 밖으로 뭘 보내는 건 아닙니까?"
"익명 사용 통계를 보내는 기능이 기본으로 켜져 있습니다. 폐쇄망이라 어차피 나가지는 못하지만, 설정에서 꺼서 시도조차 안 하게 하겠습니다. 그리고 모델 주소에는 맥 스튜디오만 적습니다. 그 밖의 주소는 설정 파일에 한 줄도 없습니다."
"그 설정 파일을 보안성 검토에 첨부하죠." 정운영 주무관이 고개를 끄덕였다.
김전산 주무관이 노트북을 프로젝터에 연결했다. 환경연구부의 안전교육 이수 화면 소스였다.
먼저 엑셀 업로드 메서드를 드래그하고 Ctrl+L을 눌렀다. 오른쪽에 대화창이 열리고, 고른 코드가 질문에 붙었다.
이 메서드가 하는 일을 순서대로 설명해 줘.
몇 초 뒤 한국어 답이 올라왔다. 파일을 읽고, 행마다 사번을 꺼내고, 이수일을 날짜로 바꾸고, DB에 넣는다. 그리고 마지막 줄.
빈 행이 있으면 사번이 null인 채로 insert가 호출되어 오류가 날 수 있습니다.
"이건 지난달에 실제로 났던 오류입니다." 김전산 주무관이 말했다.
이번에는 같은 메서드를 고르고 Ctrl+I를 눌러 한 줄을 쳤다.
빈 행은 건너뛰고, 사번 형식이 틀린 행은 오류 목록에 모아 줘.
코드 위에 초록 줄과 빨간 줄이 겹쳐 나타났다. 지워질 줄은 빨강, 들어갈 줄은 초록. 그는 한 줄씩 읽어 내려가다 하나를 거절하고, 나머지를 받았다.
마지막으로 새 메서드를 치기 시작했다. public List< 까지 치자, 회색 글씨가 나머지 한 줄을 미리 채웠다. Tab을 누르자 그 줄이 진짜 코드가 되었다.
박개발 사무관이 낮게 말했다.
"이클립스 자동완성은 내가 친 걸 이어 줬는데, 이건 내가 칠 걸 미리 쳐 두네."
최정보 팀장은 다른 것을 물었다.
"틀리면요?"
"틀립니다." 김전산 주무관이 바로 답했다. "그래서 초안입니다. 커밋 전에 사람이 읽고, 젠킨스가 빌드하고, 테스트기에서 확인합니다. 지난주에 깐 그 길을 똑같이 지나갑니다."
팀장이 화이트보드 맨 아래에 직접 적었다.
AI가 쓴 코드도, 커밋한 사람이 책임진다.
"이 한 줄 지키면 소장님께도 설명이 됩니다. 깝시다."
회의가 끝나고 김전산 주무관이 정리해 돌린 순서다. VS Code 반입과 버전 고정(제5장), 테스트기 개인 계정(제6장)은 끝났다고 본다.
① 외부망에서 설치 파일을 받는다. Continue는 GitHub의 Releases 페이지에서 .vsix 파일로 받는다. 이 파일은 운영체제마다 다르다. 우리는 테스트기 계정에 원격으로 붙어 개발하는데, 원격 접속 중에는 확장이 대부분 **서버 쪽(테스트기)**에서 돈다. 그래서 두 개를 받는다.
# 개발자 PC용 (윈도우)
continue-win32-x64-<버전>.vsix
# 테스트기용 (리눅스) ← 원격 개발에서 실제로 도는 쪽
continue-linux-x64-<버전>.vsix
② 보안 검사를 거쳐 반입한다. 제5장의 반입 목록에 한 줄을 더하고, VS Code처럼 버전을 고정한다. 팀 전원이 같은 버전을 쓴다.
③ 설치한다. 개발자 PC에서는 명령 한 줄이면 된다. 테스트기 쪽은 VS Code로 자기 계정에 원격 접속한 상태에서, 확장 보기의 … 메뉴 → VSIX에서 설치로 리눅스용 파일을 고르면 테스트기에 설치된다.
code --install-extension continue-win32-x64-<버전>.vsix
④ 익명 통계를 끈다. VS Code 설정(settings.json)에 한 줄을 넣는다. 회의에서 정운영 주무관에게 약속한 그 설정이다.
"continue.telemetryEnabled": false
⑤ 테스트기에서 맥 스튜디오로 가는 길을 연다. 확장이 테스트기에서 돌기 때문에, 질문은 테스트기에서 맥 스튜디오로 간다. 테스트기에서 세 대의 11434 포트에 닿는지 먼저 확인한다.
# 테스트기 개인 계정에서
curl http://mac-studio-1.internal:11434/api/tags
모델 목록이 돌아오면 길이 열린 것이다. 첫 실행 때 Continue가 로그인을 권해도 넘어간다. 우리는 인터넷 계정을 쓰지 않고, 다음 절의 설정 파일만 쓴다.
Continue가 무엇을 하든 그 근거는 설정 파일 하나다. 확장이 도는 쪽 계정, 즉 테스트기 개인 계정의 ~/.continue/config.yaml이다. 예전 판의 config.json은 더 이상 쓰지 않는다. 팀 표준 파일을 SVN에 두고 각자 자기 계정에 복사한다.
길어 보이지만 네 덩어리뿐이다. 덩어리마다 무엇을 정하는지 보자.
첫째, 대화와 고치기에 쓸 모델. Ctrl+L, Ctrl+I가 부르는 머리다. 제4장에서 정한 대로 큰 모델은 1번, 한국어에 강한 모델은 2번 맥 스튜디오에 있다. roles가 그 모델에게 맡길 일이다.
models:
- name: DeepSeek Coder 33B (복잡한 로직)
provider: ollama
model: deepseek-coder:33b
apiBase: http://mac-studio-1.internal:11434
roles: [chat, edit, apply]
- name: Qwen 32B (한국어 설명)
provider: ollama
model: qwen2.5-coder:32b
apiBase: http://mac-studio-2.internal:11434
roles: [chat, edit, apply]
대화창 위쪽 목록에서 그때그때 고를 수 있다. 박개발 사무관은 설명을 들을 때는 Qwen53, 로직을 짤 때는 DeepSeek54를 골랐다.
둘째, 자동완성 모델. 회색 글씨는 1초 안에 떠야 의미가 있다. 그래서 가장 작고 빠른 7B55를 3번 맥 스튜디오에 따로 둔다. 큰 모델과 같은 줄에 세우면, 누가 긴 질문을 던질 때마다 모두의 자동완성이 멈춘다.
- name: Qwen 7B (자동완성)
provider: ollama
model: qwen2.5-coder:7b
apiBase: http://mac-studio-3.internal:11434
roles: [autocomplete]
셋째, 코드 찾기용 모델. @codebase로 "급여 계산하는 곳 전부 찾아 줘"라고 물으려면, Continue가 미리 소스를 읽어 색인56을 만들어 둬야 한다. 그 색인을 만드는 작은 모델(임베딩57 모델)이다. 이 모델 파일도 제4장의 모델들과 함께 반입한다.
- name: 코드 색인
provider: ollama
model: nomic-embed-text
apiBase: http://mac-studio-1.internal:11434
roles: [embed]
넷째, @로 붙일 수 있는 것과 팀 규칙. context는 대화창에서 @를 눌렀을 때 나오는 목록이다. rules는 모든 질문 앞에 자동으로 붙는 당부다. 우리 팀이 매번 하던 잔소리를 여기에 한 번만 적는다.
context:
- provider: code # @code 특정 함수·클래스
- provider: diff # @diff 아직 커밋 안 한 내 변경
- provider: terminal # @terminal 터미널에 찍힌 오류
- provider: problems # @problems 빨간 줄 목록
- provider: folder # @folder 폴더 하나 통째로
- provider: codebase # @codebase 프로젝트 전체에서 찾기
rules:
- 답과 주석은 한국어로 쓴다.
- 이 프로젝트는 전자정부 표준프레임워크와 Java 8, MyBatis 기준이다.
- 실제 개인정보를 예시 데이터로 쓰지 않는다.
네 덩어리를 합치고 맨 위에 이름표 세 줄을 붙이면 완성이다.
name: 정보시스템팀 표준
version: 1.0.0
schema: v1
models:
# 첫째·둘째·셋째 덩어리
context:
# 넷째 덩어리
rules:
# 넷째 덩어리
저장하면 Continue가 바로 다시 읽는다. 대화창 위쪽 목록에 모델 이름이 보이면 성공이다.
Ollama는 처음 설치하면 맥 스튜디오 자기 자신의 질문만 받는다. 제3장의 자동 시작 설정에 넣은 OLLAMA_HOST=0.0.0.0:11434가 그 문을 연구소 망 쪽으로 여는 한 줄이다. 세 대 모두 들어 있는지 확인한다.
# 맥 스튜디오에서. 0.0.0.0:11434 또는 *:11434 로 떠 있어야 한다
lsof -iTCP:11434 -sTCP:LISTEN
한 가지는 꼭 기억한다. Ollama에는 비밀번호가 없다. 주소를 아는 누구나 물을 수 있다. 그래서 망에서 막는다. 네트워크 장비에서 11434 포트로 들어오는 길을 테스트기와 정보시스템팀 PC에서만 열고, 나머지는 닫는다.
.vsix를 둘 다 받고, VS Code처럼 버전을 고정한다.continue.telemetryEnabled를 false로 둔다.~/.continue/config.yaml 하나. 팀 표준본을 SVN에 두고, 모델 주소에는 맥 스튜디오만 적는다.설정이 끝난 다음 주, 팀원들은 각자 밀려 있던 일을 하나씩 Continue와 함께 했다. 이 장은 그 닷새의 기록이다. 장면마다 어떤 기능을 썼는지, 어떻게 물었는지, 무엇을 직접 확인했는지를 적는다. AI의 답은 길어서 핵심만 싣는다.
쓴 기능: 대화(
Ctrl+L) +@folder
총무과에서 넘어온 지 두 달 된 요청이었다. 관용차 예약 화면. 등록, 조회, 수정, 취소. 전형적인 CRUD58였다.
이코딩 주무관은 처음에 대화창에 이렇게만 쳤다.
차량 예약 CRUD 만들어 줘. 예약일, 차량, 신청자, 목적지.
답은 3초 만에 왔다. 그런데 Spring Boot59와 JPA로 짠 코드였다. 우리 시스템은 전자정부 표준프레임워크와 MyBatis60다. 그대로는 한 줄도 쓸 수 없었다.
"질문을 잘못했네." 그가 중얼거렸다.
두 번째에는 이미 있는 회의실 예약 기능 폴더를 @로 붙였다.
@folder meetingroom
이 회의실 예약 기능과 같은 구조(Controller, Service, ServiceImpl,
Mapper, Mapper XML, VO, JSP)로 차량 예약을 만들어 줘.
- 같은 차량, 같은 시간대 중복 예약은 막을 것
- 취소는 삭제 대신 상태값을 '취소'로 바꿀 것
이번에는 우리 코드처럼 생긴 답이 왔다. 서비스 구현의 핵심은 이랬다.
@Override
public void insertReservation(CarReservationVO vo) throws Exception {
if (carReservationMapper.countOverlap(vo) > 0) {
throw processException("car.reservation.overlap");
}
carReservationMapper.insertReservation(vo);
}
그는 일곱 개 파일을 하나씩 읽었다. 중복 검사 쿼리에서 시간이 겹치는 조건 하나가 빠져 있어 직접 고쳤다. 메시지 키도 프로퍼티 파일에 추가했다. 그리고 커밋했다. 젠킨스는 초록불을 켰다.
평소라면 하루가 걸렸을 일이 점심 전에 끝났다. 그는 점심을 먹으며 말했다.
"AI가 똑똑한 게 아니라, 우리 코드를 보여 줘야 똑똑해지더라고요."
쓴 기능: 고치기(
Ctrl+I) +@terminal
테스트기에서 연구장비 사용 신청 화면이 500 오류61를 냈다. 정운영 주무관은 테스트 WAS 로그를 터미널에 띄웠다.
java.lang.NullPointerException
at EquipRequestServiceImpl.updateRequest(EquipRequestServiceImpl.java:45)
대화창에 @terminal을 붙여 물었다. "이 오류 원인이 뭐야?" Continue는 45번째 줄을 짚었다. 조회 결과가 없을 때 null을 그대로 받은 다음, 그 위에서 메서드를 부르고 있었다.
EquipRequestVO request = equipRequestMapper.selectRequest(id);
request.setStatus(vo.getStatus()); // 45번째 줄. 조회 결과가 없으면 여기서 터진다
그는 그 메서드를 고르고 Ctrl+I를 눌렀다.
조회 결과가 없으면 "신청 내역을 찾을 수 없습니다" 예외를 던지게 고쳐 줘.
빨간 줄 한 줄, 초록 줄 세 줄이 떴다. 읽어 보고 받았다. 다만 Continue가 덤으로 제안한 로그 출력 줄은 거절했다. 신청자 이름이 로그에 찍히게 되어 있었기 때문이다.
"고치는 건 AI가 했는데, 받을지 말지는 내가 정한 거죠." 정운영 주무관이 말했다.
쓴 기능: 대화 +
@codebase
제5장에서 박개발 사무관이 처음 드래그했던 그 메서드였다. 몇 해 동안 아무도 건드리지 못한 500줄짜리 서블릿.
이번에는 메서드 하나가 아니라 프로젝트 전체에 물었다.
@codebase
급여 계산 서블릿이 어떤 순서로 계산하는지 설명해 주고,
같은 계산을 하는 곳이 다른 데도 있는지 찾아 줘.
답은 길었다. 핵심만 옮기면 이렇다.
1. 기본급: 호봉 테이블 조회 → 근속연수로 산정 → 가족수당 가산
2. 수당: 직급수당, 초과근무수당(평일·휴일 배율 다름)
3. 공제: 4대 보험, 소득세(간이세액표), 지방소득세
4. 명세서: PDF 생성 → 메일 발송 → 이력 저장
같은 계산이 있는 곳: 연말정산 화면의 SalaryUtil.calc() (약 80% 중복)
주의할 점:
- 세율이 2015년 기준으로 코드에 박혀 있습니다
- 58번째 줄에서 사용자 입력을 SQL에 그대로 붙입니다
박개발 사무관은 한참 화면을 보았다. 중복 계산이 있다는 건 어렴풋이 알았지만, 어디인지는 몰랐다. 세율이 박혀 있다는 건 알았지만, 2015년 것인지는 몰랐다.
그는 아무것도 고치지 않았다. 대신 그 답을 출력해 급여 담당자에게 들고 갔다. 세율이 지금 실제로 어떻게 적용되고 있는지 먼저 확인해야 했다. 그리고 질문할 때 실제 급여 데이터는 한 줄도 붙이지 않았다.
"12년 동안 무서워서 못 열던 걸 오늘 처음 끝까지 읽었네." 그가 돌아와서 말했다.
쓴 기능: 대화 + 팀 공용 명령(
/테스트)
안전교육 엑셀 업로드에 테스트가 하나도 없었다. 김전산 주무관은 매번 같은 당부를 치는 대신, 설정 파일에 팀 공용 명령을 하나 만들었다.
prompts:
- name: 테스트
description: 고른 코드의 JUnit 테스트 작성
prompt: |
고른 코드의 JUnit 4 테스트를 작성해 줘.
성공 경우와 실패 경우(빈 값, 잘못된 형식, 중복)를 모두 다루고,
테스트 이름은 한국어로 무엇을 확인하는지 알 수 있게 지어 줘.
이제 메서드를 고르고 대화창에 /테스트만 치면 된다. 받은 테스트 중 두 개는 이렇게 생겼다.
@Test
public void 빈_행은_건너뛰고_나머지만_저장한다() throws Exception {
List<EduRecordVO> rows = uploadService.parse(엑셀("빈행_포함.xlsx"));
assertEquals(3, rows.size());
}
@Test
public void 사번_형식이_틀리면_오류_목록에_담긴다() throws Exception {
UploadResult result = uploadService.validate(엑셀("사번오류.xlsx"));
assertEquals(1, result.getErrors().size());
}
여덟 개 중 두 개는 실제 동작과 맞지 않아 지웠다. 나머지 여섯 개를 SVN에 커밋했다. 이제 젠킨스는 빌드할 때마다 이 테스트를 돌린다. 누군가 업로드 로직을 망가뜨리면 빨간불이 먼저 알려 줄 것이다.
쓴 기능: 대화
조사연구부 이조사 부장이 메일을 보냈다. 지난 6개월 동안 지역별로 측정값이 10% 넘게 오른 지점만, 높은 순으로 뽑아 달라는 요청이었다.
김전산 주무관은 테이블 구조를 붙이고 물었다.
측정 테이블 구조는 아래와 같아. (컬럼 목록 붙임)
지난 6개월, 지역별·지점별 평균 측정값과 직전 측정 대비 증가율을 구하고,
10% 넘게 오른 지점만 증가율 높은 순으로 뽑는 SQL 짜 줘.
받은 쿼리는 직전 값과 비교하는 창 함수를 썼다. 핵심은 이 부분이었다.
LAG(m.value) OVER (PARTITION BY m.site_id ORDER BY m.measured_at) AS prev_value
그는 그 쿼리를 바로 운영 DB에 돌리지 않았다. 테스트기 DB에서 먼저 돌려 결과 몇 줄을 손으로 계산해 맞춰 보고, 실행 계획을 확인했다. 한 번 더 고쳐 달라고 해서 증가율이 0으로 나뉘는 경우를 막았다. 결과는 오후 4시에 이조사 부장에게 갔다.
쓴 기능: 자동완성(
Tab)
닷새 내내 가장 많이 쓴 기능은 따로 이름을 붙일 필요도 없는 것이었다. 박개발 사무관이 VO에 필드를 하나 추가하면, getter와 setter가 회색으로 미리 떠 있었다. 매퍼 XML에서 <select id=" 까지 치면, 서비스에서 부르는 이름이 이미 와 있었다.
필요 없으면 그냥 계속 치면 된다. 회색 글씨는 사라진다. 받고 싶을 때만 Tab.
금요일 오후, 이코딩 주무관이 농담을 했다.
"사무관님이 오늘 Tab 누른 횟수, 이클립스에서 Ctrl+Space 누른 횟수보다 많을 거예요."
박개발 사무관은 부정하지 않았다.
금요일 5시, 팀은 화이트보드 앞에 다시 모였다. 잘 된 것과 안 된 것을 적다 보니, 결국 전부 어떻게 물었느냐의 문제였다.
| 이렇게 물으면 헛돈다 | 이렇게 물으면 통한다 |
|---|---|
| "CRUD 만들어 줘" | "@folder 회의실 예약과 같은 구조로 차량 예약 만들어 줘" |
| "왜 안 돼?" | "@terminal 이 오류의 원인과 고칠 곳을 알려 줘" |
| "이 코드 고쳐 줘" | 고칠 부분만 골라 Ctrl+I, 원하는 결과를 한 문장으로 |
| 500줄을 통째로 붙여 넣기 | @codebase로 찾게 하고, 메서드 단위로 나눠 묻기 |
| 실제 데이터를 예시로 붙이기 | 테이블 구조와 가짜 데이터만 붙이기 |
최정보 팀장이 그 아래에 다섯 줄을 덧붙였다. 다음 주부터의 팀 규칙이 되었다.
@로 우리 코드를 먼저 보여 준다.첫 2주 동안 팀이 실제로 부딪힌 문제들이다. 각 문제마다 누가 겪었는지, 무엇이 원인이었는지, 어떻게 풀었는지 순서로 적는다.
누가: 설치 첫날, 이코딩 주무관.
원인: 셋 중 하나다. 설정 파일의 주소가 틀렸거나, 맥 스튜디오의 Ollama가 꺼져 있거나, 테스트기에서 맥 스튜디오로 가는 길이 막혀 있다. 이코딩 주무관의 경우는 세 번째였다. 원격 개발 중에는 Continue가 테스트기에서 돌기 때문에, 자기 PC에서는 닿아도 테스트기에서 안 닿으면 실패한다.
해결: Continue가 도는 자리, 즉 테스트기 계정에서 직접 확인한다.
curl http://mac-studio-1.internal:11434/api/tags
config.yaml의 apiBase와 모델 이름 오타를 본다.누가: 정전 다음 날 아침, 김전산 주무관.
원인: 맥 스튜디오가 재부팅되면서 Ollama가 자동으로 뜨지 않았다. 자동 시작 설정을 한 대에만 넣어 두었던 것이다.
해결: 맥 스튜디오에서 Ollama가 떠 있는지 보고, 꺼져 있으면 띄운다. 그리고 세 대 모두에 제3장의 자동 시작 설정을 넣는다.
curl http://localhost:11434 # "Ollama is running" 이 나와야 한다
launchctl start com.ollama # 꺼져 있으면 띄운다
누가: 박개발 사무관. "회색 글씨가 내가 다 치고 나서야 나온다."
원인: 자동완성 모델이 큰 모델과 같은 맥 스튜디오에 있었다. 누가 33B에 긴 질문을 던지면, 그동안 자동완성이 줄을 섰다. 또는 config.yaml에서 7B 모델에 roles: [autocomplete]를 빠뜨리면 아예 뜨지 않는다.
해결: 자동완성은 가장 작은 모델을, 아무도 긴 질문을 던지지 않는 장비에 따로 둔다(제7장 둘째 덩어리). 상태 표시줄의 Continue 아이콘에서 자동완성이 켜져 있는지도 본다.
누가: 목요일 오후, 거의 모두.
원인: 여섯 명이 동시에 DeepSeek 33B에 물었다. 큰 모델은 한 번에 한두 사람의 답만 빠르게 낸다. 나머지는 기다린다.
해결: 간단한 질문은 Qwen 32B나 7B로 돌린다(제8장 팀 규칙 5번). 맥 스튜디오에서 지금 어떤 모델이 메모리에 올라 있는지 확인한다.
ollama ps
그래도 느리면, 무거운 일을 맡는 모델과 장비를 나누는 제10장의 배치를 다시 본다.
누가: 정운영 주무관. 연구장비 레거시 폴더를 연 날.
원인: Continue가 @codebase용 색인을 만들려고 폴더 전체를 읽고 있었다. 그 폴더에는 장비가 쏟아 낸 수 기가바이트의 로그와 데이터 파일이 함께 있었다. 원격 접속 위에서 그걸 다 읽느라 연결이 버티지 못했다.
해결: 색인할 필요 없는 폴더를 프로젝트 맨 위의 .continueignore에 적는다. 형식은 .gitignore와 같다. 큰 프로젝트라면 VS Code 설정에서 시작할 때 색인을 바로 하지 않게 해 둔다.
# .continueignore
logs/
data/
target/
*.jar
"continue.pauseCodebaseIndexOnStart": true
누가: 이코딩 주무관. 윈도우용 파일 하나만 반입한 경우.
원인: 원격 접속 중에는 Continue가 테스트기 쪽에서 돌아야 하는데, 테스트기에는 리눅스용이 아닌 윈도우용 .vsix가 들어가 있었다. Continue의 설치 파일은 운영체제마다 다르다.
해결: 확장 보기에서 Continue가 원격(SSH62) 쪽에 설치되어 있는지 본다. 테스트기에는 linux-x64 파일을 설치한다. 반입할 때부터 두 종류를 같이 받는다(제7장 ①).
누가: 테스트기 계정을 처음 만든 날, 거의 모두.
원인: 원격 접속을 하면 VS Code가 테스트기에 짝 프로그램(VS Code Server)을 인터넷에서 받아 깔려고 한다. 폐쇄망에는 그 파일이 없다. 개발자 PC의 VS Code를 올리면 짝 프로그램의 버전도 달라져서 같은 일이 다시 생긴다.
해결:
update.mode를 none으로).code --version을 쳐서 둘째 줄의 커밋 번호를 확인한다.server-linux-x64 파일을 받아 보안 검사를 거친다.~/.vscode-server/bin/<커밋>/에 풀어 둔다.누가: 박개발 사무관. 제5장의 첫 번째 실패와 같은 증상.
원인: Java 확장은 설치된 뒤에 언어 서버를 인터넷에서 따로 받는다. 폐쇄망에서는 그 다운로드가 실패해서, 편집기가 자바를 이해하지 못한다. 이것은 Continue의 자동완성과는 다른 기능이다. 둘 다 있어야 편하다.
해결: 외부망에서 같은 확장을 한 번 실행해 언어 서버가 받아 놓은 폴더를 확인하고, 그 폴더를 .vsix와 함께 반입한다.
누가: 첫 운영 배포 날, 최정보 팀장이 가슴을 쓸어내린 일.
원인: 운영기에 올린 파일이 테스트기에서 시험한 파일과 달랐다. 누군가 급하게 자기 계정에서 다시 빌드한 파일을 올린 것이다.
해결: 제6장의 원칙으로 돌아간다.
누가: 큰 모델 두 개를 한 장비에 동시에 올려 본 날, 김전산 주무관.
원인: 96GB도 무한하지 않다. 큰 모델 여러 개가 동시에 메모리에 올라가고, 여러 사람의 긴 대화가 겹치면 모자란다.
해결: 지금 쓰지 않는 모델을 내린다. 그래도 부족하면 그 장비에 올리는 모델을 줄이고, 역할을 다른 장비로 나눈다.
ollama ps # 지금 올라가 있는 모델
ollama stop deepseek-coder:33b # 쓰지 않는 모델 내리기
처음 일주일은 무엇이든 빨랐다. 한 달이 지나자 팀 전원이 하루 종일 Continue를 열어 두었고, 맥 스튜디오 세 대는 처음으로 붐비기 시작했다. 이 장은 장비를 더 사지 않고 그 붐빔을 푼 이야기다.
월요일 오전 8시 50분. 정보시스템팀.
박개발 사무관이 커피를 내려놓고 대화창에 첫 질문을 쳤다. 답이 오지 않았다. 10초, 20초. 40초가 지나서야 첫 글자가 올라왔다.
"김 주무관, 이거 고장 난 거 아니야?"
그런데 두 번째 질문은 2초 만에 답이 왔다. 세 번째도 마찬가지였다.
김전산 주무관은 맥 스튜디오 2번에 접속해 확인했다.
ollama ps
금요일 저녁 이후로 아무도 묻지 않았으니, Ollama가 모델을 메모리에서 내려 둔 상태였다. 월요일 첫 질문은 20GB 가까운 모델 파일을 다시 메모리에 올리는 시간까지 기다린 것이다. 모델은 일정 시간 질문이 없으면 메모리에서 내려간다. Continue는 기본으로 30분을 요청한다.
"출근 전에 데워 두면 됩니다." 김전산 주무관이 말했다.
두 가지를 했다. 먼저 Continue 설정에서 모델이 메모리에 머무는 시간을 하루로 늘렸다. 맥 스튜디오의 메모리는 넉넉하니, 내릴 이유가 없었다.
- name: Qwen 32B (한국어 설명)
provider: ollama
model: qwen2.5-coder:32b
apiBase: http://mac-studio-2.internal:11434
roles: [chat, edit, apply]
defaultCompletionOptions:
keepAlive: 86400 # 마지막 질문 뒤 24시간 동안 메모리에 둔다
그리고 주말을 넘긴 월요일을 위해, 매일 아침 8시 반에 각 맥 스튜디오가 자기 모델을 미리 올려 두게 했다. 빈 질문을 하나 보내면 Ollama는 답 없이 모델만 메모리에 올린다.
# 맥 스튜디오 2번의 예열 스크립트. 매일 08:30 자동 실행
curl -s http://localhost:11434/api/generate \
-d '{"model": "qwen2.5-coder:32b", "keep_alive": -1}'
다음 주 월요일, 박개발 사무관의 첫 질문은 3초 만에 답이 왔다.
한 달째 목요일 오후 2시.
이번에는 이코딩 주무관이었다.
"오후만 되면 Qwen이 줄을 서요. 오전엔 금방 오는데."
김전산 주무관은 먼저 숫자부터 쟀다. 아무도 쓰지 않는 저녁에, 혼자 질문을 던져 속도를 확인했다.
ollama run qwen2.5-coder:32b --verbose "자바로 이진 탐색 짜 줘"
# 마지막 줄의 eval rate 가 초당 글자 조각(tok/s) 수
혼자일 때는 충분히 빨랐다. 문제는 속도가 아니라 차례였다. 점심 직후 여섯 명이 동시에 물으면, Ollama는 기본 설정에서 질문을 한두 개씩만 받아 처리하고 나머지를 줄 세웠다. 맨 뒤에 선 사람은 앞사람 답이 다 끝나기를 기다렸다.
Ollama에는 모델 하나가 동시에 몇 개의 질문을 받을지 정하는 설정이 있다. 동시에 받으면 한 사람의 답은 조금 느려지지만, 아무도 줄을 서지 않는다. 대신 동시에 받는 수만큼 메모리를 더 쓴다.
김전산 주무관은 장비마다 숫자를 다르게 정했다.
| 장비 | 맡은 모델 | 동시에 받는 질문 | 이유 |
|---|---|---|---|
| 1번 | DeepSeek 33B, CodeLlama63 34B, 코드 색인 | 2 | 무거운 로직 질문. 자주 오지 않지만 길다 |
| 2번 | Qwen 32B | 4 | 팀 질문이 가장 많이 몰리는 곳 |
| 3번 | Qwen 7B (자동완성) | 8 | 짧은 요청이 쉴 새 없이 온다 |
설정은 제3장에서 만든 자동 시작 파일의 환경 변수에 몇 줄을 더하면 된다. 2번 맥 스튜디오는 이렇다.
<key>EnvironmentVariables</key>
<dict>
<key>OLLAMA_HOST</key> <string>0.0.0.0:11434</string>
<key>OLLAMA_NUM_PARALLEL</key> <string>4</string>
<key>OLLAMA_KEEP_ALIVE</key> <string>24h</string>
<key>OLLAMA_NO_CLOUD</key> <string>1</string>
</dict>
마지막 줄은 Ollama의 클라우드 기능을 아예 끄는 설정이다. 폐쇄망이라 어차피 나가지 못하지만, 정운영 주무관의 보안성 검토 서류에 한 줄을 더 적을 수 있었다.
설정을 바꾼 다음 날 오후 2시, 이코딩 주무관은 아무 말도 하지 않았다. 불만이 없다는 뜻이었다.
그 주 금요일, 이코딩 주무관이 짧은 코드를 들고 왔다.
const macStudios = ['mac-studio-1', 'mac-studio-2', 'mac-studio-3'];
let i = 0;
const next = () => macStudios[i++ % macStudios.length];
"질문을 세 대에 차례로 돌리면 어떨까요. 한 대에 몰리지 않게요."
김전산 주무관은 코드를 한참 보다가 고개를 저었다.
"세 대에 올라간 모델이 서로 다릅니다. DeepSeek 질문이 3번으로 가면, 3번에는 DeepSeek가 없어요. 차례로 돌리는 건 세 대가 똑같을 때만 의미가 있습니다."
"그럼 Qwen이 계속 붐비면요?"
"그때는 1번에도 Qwen 32B를 하나 더 올리고, 팀을 반으로 나눠 설정 파일의 주소만 다르게 주면 됩니다. 프로그램을 하나 더 만들 필요가 없어요. 프로그램이 하나 늘면, 그걸 고칠 사람도 우리입니다."
이코딩 주무관은 웃으며 코드를 지웠다. 제2장에서 최정보 팀장이 했던 말과 똑같았기 때문이다.
박개발 사무관의 불만은 반대 방향이었다.
"회색 글씨가 너무 자주 떠. 생각 좀 하려고 멈추면 바로 튀어나와서 정신이 없어."
자동완성은 빠를수록 좋지만, 너무 성급하면 방해가 된다. 김전산 주무관은 자동완성 모델에 네 가지를 조였다.
- name: Qwen 7B (자동완성)
provider: ollama
model: qwen2.5-coder:7b
apiBase: http://mac-studio-3.internal:11434
roles: [autocomplete]
autocompleteOptions:
debounceDelay: 500 # 타이핑을 멈추고 0.5초 기다린 뒤에 묻는다
maxPromptTokens: 1024 # 앞뒤 코드를 너무 많이 보내지 않는다
onlyMyCode: true # 라이브러리 말고 우리 소스만 참고한다
useCache: true # 같은 자리에서 같은 요청은 다시 묻지 않는다
"이제 좀 사람 같네." 박개발 사무관이 말했다.
정운영 주무관은 다른 벽에 부딪혔다. 연구장비 연계 오류 로그를 @terminal로 통째로 붙여 물었더니, 답이 로그 앞부분은 모르는 것처럼 굴었다.
모델이 한 번에 읽을 수 있는 양에는 한계가 있다. 이를 문맥 길이라고 한다. 그 한계를 넘으면 앞부분이 잘려 나간다. 문맥 길이는 늘릴 수 있지만 공짜가 아니다. 늘린 만큼, 그리고 동시에 받는 질문 수만큼 메모리를 더 쓴다.
김전산 주무관은 대화 모델의 문맥 길이를 넉넉하게 늘렸다. 96GB에서 감당할 수 있는 선이었다.
defaultCompletionOptions:
contextLength: 16384 # 한 번에 읽는 양. 늘린 만큼 메모리를 더 쓴다
그리고 팀 규칙에 한 줄을 더했다.
6. 로그는 오류가 난 부분 앞뒤 50줄만 붙인다. 통째로 붙이면 모델도 사람처럼 앞을 잊는다.
한 달째 마지막 날 오후. 팀장실.
최정보 팀장이 김전산 주무관을 불렀다.
"소장님께 한 달 결과를 보고해야 합니다. 느낌 말고 숫자로요."
김전산 주무관은 한 달 동안 매주 금요일 같은 질문으로 잰 기록을 내밀었다. 우리 팀 장비와 우리 팀 질문으로 잰 숫자였다.
| 항목 | 조정 전 | 조정 후 |
|---|---|---|
| 월요일 첫 질문 응답 | 40초 안팎 | 3초 안팎 |
| 오후 2시 Qwen 32B 대기 | 1분 넘을 때 있음 | 10초 이내 |
| 자동완성이 뜨는 시점 | 멈칫할 때마다 | 확실히 멈췄을 때 |
| 추가 장비 구매 | 없음 | 없음 |
"마지막 줄이 제일 좋네요." 팀장이 말했다.
보고서는 그날 저녁 연구소장에게 올라갔다. 다음 날 아침, 답장이 한 줄 왔다.
다른 부서들도 써 볼 수 있습니까?
최정보 팀장은 그 메일을 팀 대화방에 그대로 올렸다. 맥 스튜디오 세 대가 정보시스템팀 밖으로 나가는 이야기는, 다음 편에서 이어진다.
ollama ps로 무엇이 메모리에 있는지 보고, ollama run <모델> --verbose로 혼자일 때의 속도를 잰다. 느린 것인지, 줄을 선 것인지부터 가린다.keepAlive를 늘리고, 출근 전에 빈 요청으로 모델을 올려 둔다.OLLAMA_NUM_PARALLEL을 장비 역할에 맞게 정한다. 질문이 몰리는 장비일수록 크게, 대신 메모리를 확인한다.OLLAMA_NO_CLOUD로 Ollama가 바깥을 찾지 않게 한다.끝.
COMMENTS · 제2편
Mac Studio 기반 폐쇄망 AI 코딩 환경 구축 가이드 댓글 0
아직 댓글이 없습니다. 읽다가 궁금한 점이나 보탤 내용을 남겨 주세요.