
생성 수정
IBM * 레드햇 AX 과정 · 21강
로컬과 클라우드 — 세 쌍의 대칭, 그리고 주소의 문제
2026년 7월 6일(월) ~ 7월 10일(금)
1주차의 첫 표에 축 2가 있었습니다. "내 컴퓨터(로컬) ↔ 남의 컴퓨터(원격·클라우드)" 입니다. 그때는 예시가 딱 하나였습니다. Git과 GitHub. 그리고 그 표 아래에 이런 줄이 있었습니다.
축 2 (로컬 ↔ 원격) | SAVE/LOAD ↔ 업로드/다운로드 (3주차), Docker ↔ Docker Hub, Model ↔ Hugging Face (11주차)
이번 주가 그 11주차입니다. 예고했던 자리에 도착했습니다. 이 장은 두 덩어리로 이루어져 있습니다.
- 앞쪽은 축 2의 완성입니다. Git↔GitHub 한 쌍이 아니라 세 쌍이 정확히 같은 모양이라는 것, 그리고 셋 다
push/pull로 오간다는 것을 봅니다. - 뒤쪽은 축 2의 역전입니다. 지금까지 "남의 컴퓨터"는 늘 남이 운영해주는 것이었습니다. 이번 주에는 내 두 번째 PC를 직접 "남의 컴퓨터"로 만듭니다. SSH로 들어가서 씁니다. 그러자 지금까지 한 번도 생각해본 적 없던 문제가 튀어나옵니다. 전원을 껐다 켜면 그 컴퓨터의 주소가 바뀐다는 것.
이 두 번째 문제가 이 주의 과제입니다.
이 장을 다 읽으면
- Git↔GitHub / Docker↔Docker Hub / Model↔Hugging Face 세 쌍이 같은 구조의 반복임을 설명하고, 각 쌍을 다섯 항목(개념·인터넷·핵심 기능·해결하는 문제·비유)으로 비교할 수 있다.
- 로컬을 엔진, 클라우드를 유통망이라는 한 쌍의 말로 정리하고,
push/pull이 세 곳에서 똑같이 생긴 이유를 말할 수 있다. - 객체 검출의 One-shot과 Two-shot을 구분하고, YOLO가 왜 그 축의 오른쪽에서 태어났는지 설명할 수 있다.
- ANN·DNN·CNN·RNN을 포함 관계와 "무엇을 보존하는가" 축으로 정리할 수 있다.
- "도커라서 GPU를 못 쓴다"가 왜 틀린 진단인지, 진짜 원인을 어디서 찾아야 하는지 안다.
- 접속PC와 원격PC를 구분하고, WSL·OpenSSH 서버·포트포워딩·방화벽까지 SSH 접속 한 줄이 성립하기 위한 조건을 순서대로 나열할 수 있다.
- 원격 PC의 IP가 세 층(WSL 내부 / LAN / 공인)으로 나뉜다는 것을 알고, 그중 어느 층이 왜 바뀌는지 설명할 수 있다.
- 유동 IP 문제의 해결 방향 여섯 가지를 "어느 층을 고정하는가" 로 분류하고, 각각의 트레이드오프를 말할 수 있다.
- FastAPI 백엔드 폴더만 도커 이미지로 빌드해 도커허브에 올리고, 원격 PC에서
pull해서 띄우는 전 과정을 재현할 수 있다. - Human-in-the-Loop와 2단계 즉시 파기 정책이 각각 어떤 한계를 인정한 설계인지 설명할 수 있다.
0. 이 장의 판별 축
앞에서 배운 대로, 새 용어를 만나면 비유를 찾기 전에 아래 축 중 어디에 꽂히는지부터 정하세요.
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 |
|---|---|---|---|---|
| 1 | 어디에 있는가 | 로컬 (내 컴퓨터) | 클라우드 (남의 컴퓨터) | Git ↔ GitHub / Docker ↔ Docker Hub / Model ↔ Hugging Face |
| 2 | 주소가 유지되는가 | 고정 (Static) | 유동 (Dynamic) | 공인 IP ↔ DHCP 임대 IP / WSL 내부 IP / Elastic IP ↔ 일반 IP |
| 3 | 무엇을 하는가 | 엔진 (연산·변경) | 유통망 (배포·협업) | 로컬 실행 ↔ 레지스트리·허브 |
| 4 | 어느 방향인가 | push (올린다) | pull (내린다) | 세 쌍 전부 / docker push ↔ docker pull |
| 5 | 접속하는가 받는가 | 접속PC (클라이언트) | 원격PC (서버) | ssh 를 치는 쪽 ↔ sshd 가 도는 쪽 |
축 1이 이 장의 중심이고, 그 축은 이 교재의 1주차 축 2입니다. 새로 만든 축이 아니라 되찾아온 축입니다.
앞 장에서 이어집니다
어디서 그때 이름 이번 장에서 1주차 축 2 내 컴퓨터 ↔ 남의 컴퓨터 (Git ↔ GitHub) 축 1. 예시가 하나에서 셋으로 늘어나 완성됩니다 (2장) 3주차 2.4 SAVE/LOAD ↔ 업로드/다운로드 2.7절. 그때 "축이 다르다"고 경고했던 그 구분을 여기서 씁니다 7주차 14.5 도커허브 = 로컬↔원격의 한 쌍 2장에서 한 쌍이 아니라 세 쌍의 표로 확장됩니다 8주차 4장 Image=클래스 / Container=인스턴스 8장. 그 이미지를 어디에 보관하고 어떻게 옮기는가로 넘어갑니다 10주차 11장 AWS EC2·RDS·S3 6장. "남의 컴퓨터를 빌린다" ↔ "내 컴퓨터를 서버로 만든다"의 대비 10주차 12.11 Cloudflare Zero Trust 터널 6.7절. 그때 편해서 쓴 도구가, 여기서는 유동 IP 문제의 해답으로 다시 나옵니다
축 3과 축 4는 축 1의 자식 축입니다. 어디에 있는지(축 1)가 정해지면 무엇을 하는지(축 3)가 따라오고, 둘 사이를 오가는 동작에는 자동으로 방향(축 4)이 생깁니다. 축 1 하나만 잡으면 셋이 같이 옵니다.
1. 축 2를 회수한다 — 예고했던 자리
1.1 1주차가 남긴 빚
1주차에서 Git과 GitHub를 배울 때 이런 표를 그렸습니다.
| Git | GitHub | |
|---|---|---|
| 무엇인가 | 프로그램 (버전 관리 도구) | 웹 서비스 (원격 저장소) |
| 어디 있나 | 내 컴퓨터 | 남의 컴퓨터 (클라우드) |
| 인터넷 | 없어도 됨 | 필요 |
그리고 이렇게 정리했습니다. "여섯 줄 중 다섯 줄이 로컬에서 끝납니다. 인터넷이 필요한 건 마지막 한 줄뿐입니다."
그때는 이것이 Git이라는 도구의 특징처럼 보였습니다. 아니었습니다. 이것은 소프트웨어 세계 전체가 채택한 하나의 구조였고, Git은 그 구조의 첫 번째 사례일 뿐이었습니다.
1.2 같은 모양이 세 번 반복된다
[ 로컬 (내 컴퓨터 환경) ] [ 클라우드 (인터넷/공유 플랫폼) ]
┌─────────────────────────┐ ┌─────────────────────────┐
│ Git │ ─── push ───▶ │ GitHub │
│ (코드 버전 관리) │ ◀── pull ──── │ (코드 원격 저장·협업) │
└─────────────────────────┘ └─────────────────────────┘
│ │
┌─────────────────────────┐ ┌─────────────────────────┐
│ Docker │ ─── push ───▶ │ Docker Hub │
│ (개발 환경 컨테이너화) │ ◀── pull ──── │ (도커 이미지 저장·공유) │
└─────────────────────────┘ └─────────────────────────┘
│ │
┌─────────────────────────┐ ┌─────────────────────────┐
│ Model │ ─── push ───▶ │ Hugging Face │
│ (AI 가중치 파일·추론) │ ◀── pull ──── │ (AI 모델 허브·호스팅) │
└─────────────────────────┘ └─────────────────────────┘
세 줄이 완전히 같은 모양입니다. 왼쪽 상자의 내용물만 코드 → 환경 → 가중치로 바뀔 뿐입니다.
1.3 로컬은 엔진, 클라우드는 유통망 ⭐
로컬(Local) — 기술의 '엔진' 이자 작업 공간입니다. 인터넷 환경과 무관하게 실제 연산과 변경 사항이 일어나는 물리적인 장소입니다.
클라우드(Cloud) — 기술의 '유통망' 이자 협업 공간입니다. 로컬의 한계를 넘어 백업·공유·협업·대규모 배포를 가능하게 만들어 줍니다.
이 한 쌍이 왜 중요한가 하면, "둘 중 어느 쪽이 더 중요한가" 라는 잘못된 질문을 없애주기 때문입니다. 엔진 없는 유통망은 팔 물건이 없고, 유통망 없는 엔진은 만든 걸 아무도 못 씁니다. 둘은 층위가 다릅니다.
- 로컬(Local) — [실행 환경]인데, [연산이 실제로 일어나며 인터넷 없이도 성립한다]는 점에서 클라우드와 구별된다.
- 클라우드(Cloud) — [실행 환경]인데, [타인의 인프라 위에 있고 네트워크를 통해서만 닿는다]는 점에서 로컬과 구별된다.
⚠️ "클라우드 = 인터넷"이 아닙니다. 인터넷은 길이고, 클라우드는 그 길 끝에 있는 남의 컴퓨터입니다. 10주차에서 배운 온프레미스(On-Premise) 가 정확히 그 반대쪽 개념이었습니다. premise 는 "전제"가 아니라 "건물·구내" 라는 것도 그때 짚었습니다.
2. 세 쌍의 대칭 ⭐
이 장의 중심입니다. 세 쌍을 각각 다섯 항목으로 뜯어본 다음, 마지막에 하나의 표로 겹칩니다.
2.1 첫 번째 쌍 — 코드 관리: Git ↔ GitHub
| 구분 | Git (로컬) | GitHub (클라우드) |
|---|---|---|
| 개념 | 소스코드의 변경 이력을 기록하는 버전 관리 도구 | Git 리포지토리를 업로드하는 원격 저장소 플랫폼 |
| 인터넷 | 인터넷 연결이 없어도 로컬에서 작동 가능 | 인터넷 연결 필수 |
| 핵심 기능 | 커밋(Commit), 브랜치(Branch), 롤백(Rollback) | 협업(Pull Request), 코드 리뷰, 오픈소스 공유 |
| 해결 문제 | "언제 무엇을 왜 바꿨는지 남는 게 없다" | "다른 사람과 같은 코드를 어떻게 같이 고치나" |
| 비유 | 내 컴퓨터에 쓰는 개인 비밀 일기장 | 모두가 볼 수 있는 온라인 블로그·게시판 |
대체 가능성이 다릅니다. Git은 사실상 대체 불가지만, GitHub는 GitLab·Bitbucket 등으로 바꿀 수 있습니다. 왼쪽(엔진)은 표준이 하나로 수렴하고, 오른쪽(유통망)은 경쟁자가 여럿입니다. 이 비대칭은 세 쌍 모두에서 반복됩니다.
2.2 두 번째 쌍 — 환경 관리: Docker ↔ Docker Hub
| 구분 | Docker (로컬) | Docker Hub (클라우드) |
|---|---|---|
| 개념 | 프로그램과 실행 환경을 하나로 묶는 가상화 플랫폼 | 작성된 도커 이미지를 보관하고 배포하는 레지스트리 |
| 인터넷 | 이미지가 이미 있으면 없어도 됨 | 필수 |
| 핵심 기능 | 컨테이너 생성, 로컬 환경 독립 실행 | 이미지 업로드/다운로드 (push / pull) |
| 해결 문제 | "내 컴퓨터에서는 잘 되는데 왜 서버에선 안 되지?" | 전 세계 어디서나 명령어 한 줄로 동일한 환경 복제 |
| 비유 | 음식을 담아두는 밀폐 용기(컨테이너) | 반찬 밀키트를 모아두고 파는 마켓·앱스토어 |
레지스트리(Registry) 라는 단어를 눈여겨보세요. "저장소"가 아니라 "등록소" 입니다.
아무 파일이나 넣는 창고가 아니라 이름(태그)을 붙여 등록해두는 곳이라는 뜻이 담겨 있습니다.
그래서 도커허브에서 받을 때 python:3.13-slim 처럼 이름:버전 형식을 씁니다.
앞에서 배운 것과 이어집니다 — 8주차 4장에서 Image = 클래스, Container = 인스턴스, Volume = 디스크 라는 대응을 세웠습니다. 여기에 한 칸이 더 붙습니다. Docker Hub = 클래스 파일을 올려두는 원격 저장소입니다. 즉 "설계도(이미지)를 만들어 등록소에 올려두면, 아무 컴퓨터나 그걸 받아서 실체(컨테이너)를 찍어낼 수 있다"가 됩니다.
2.3 세 번째 쌍 — AI 모델 관리: Model ↔ Hugging Face
| 구분 | Model (로컬) | Hugging Face (클라우드) |
|---|---|---|
| 개념 | 학습이 완료되어 저장된 AI 신경망 파일 자체 | AI 모델·데이터셋·웹 데모를 공유하는 AI 생태계 플랫폼 |
| 형태 | .safetensors, .bin, .pt 등 대용량 파일 | 전 세계 연구자가 모델을 올리고 토론하는 커뮤니티 |
| 사용 방식 | 로컬 GPU 인프라로 직접 추론·파인튜닝 | API 호출, 모델 다운로드, 클라우드 내 호스팅(Spaces) |
| 해결 문제 | 내 데이터를 밖으로 내보내지 않고 쓰기 | 수십 GB 가중치를 각자 학습하지 않고 받아 쓰기 |
| 비유 | 내 컴퓨터에 설치된 게임 실행 파일 | 게임을 받고 평가하는 스팀(Steam) 플랫폼 |
왜 모델 파일의 확장자가 여러 개인가
세 확장자는 같은 것을 담지만 담는 방식이 다릅니다.
| 확장자 | 정체 | 특징 |
|---|---|---|
.bin | 가중치 덩어리 | 초기 Hugging Face 표준. 내부는 역시 pickle 기반 |
.safetensors | Hugging Face가 만든 안전한 포맷 | 코드 실행 없이 숫자만 읽음. 로딩도 빠름. 현재 사실상 표준 |
⭐
.safetensors라는 이름 자체가 "safe + tensors" 입니다. "텐서(숫자 덩어리)만 안전하게 담는다"는 선언입니다. 모델을 남에게서 받아 쓰는 시대가 되자 "받은 파일을 여는 행위 자체가 위험하다" 는 문제가 생겼고, 그 답이 이 포맷입니다.
텐서(Tensor) 는 3주차에서 배웠습니다. 스칼라(0차원) → 벡터(1차원) → 매트릭스(2차원) → 텐서(3차원 이상). 모델 파일은 결국 거대한 텐서들의 모음입니다. 그 이상도 이하도 아닙니다.
Spaces — 유통망이 실행까지 해준다
Hugging Face가 다른 두 쌍과 다른 점이 하나 있습니다. Spaces 입니다. GitHub는 코드를 보관만 하고(실행은 Vercel 같은 다른 서비스가), Docker Hub도 이미지를 보관만 합니다. 그런데 Hugging Face는 모델을 올리면 그 자리에서 데모 웹앱까지 띄워줍니다.
| 보관 | 실행 | |
|---|---|---|
| GitHub | ✅ | ❌ (Vercel·Netlify 등이 담당) |
| Docker Hub | ✅ | ❌ (내 서버·클라우드가 담당) |
| Hugging Face | ✅ | ✅ Spaces |
⚠️ 이 차이 때문에 "허깅페이스는 깃허브 같은 거다"라고만 외우면 Spaces를 만났을 때 무너집니다. 보관과 실행은 다른 축이라는 것을 같이 기억하세요.
2.4 세 쌍을 한 표로 겹치기 ⭐
이 표가 이 장에서 가장 오래 남을 표입니다.
| Git ↔ GitHub | Docker ↔ Docker Hub | Model ↔ Hugging Face | |
|---|---|---|---|
| 로컬에 있는 것 | .git 폴더 (변경 이력) | 이미지·컨테이너 | .safetensors 가중치 파일 |
| 로컬에서 하는 일 | 커밋·브랜치·롤백 | 빌드·실행·격리 | 추론·파인튜닝 |
| 올리는 명령 | git push | docker push | huggingface-cli upload / push_to_hub() |
| 내리는 명령 | git pull / git clone | docker pull | from_pretrained() / huggingface-cli download |
| 클라우드가 주는 것 | 협업(PR)·이슈·리뷰 | 공개 레지스트리·태그 관리 | 모델 카드·데이터셋·Spaces |
| 이름 붙이는 단위 | 브랜치·태그 | repo:tag | 조직/모델명 |
| 왼쪽이 없으면 | 올릴 이력이 없음 | 올릴 이미지가 없음 | 올릴 가중치가 없음 |
| 오른쪽이 없으면 | 나 혼자만 씀 | 다른 서버에 못 옮김 | 남이 못 받아 씀 |
마지막 두 줄이 핵심입니다. 왼쪽이 없으면 존재가 없고, 오른쪽이 없으면 확산이 없습니다.
2.5 push와 pull — 방향에는 이름이 있다 (축 4)
세 쌍이 전부 같은 두 단어를 씁니다. 우연이 아닙니다.
| 방향 | 단어 | 문자 그대로의 뜻 | 하는 일 |
|---|---|---|---|
| 로컬 → 클라우드 | push | 밀어 올린다 | 내가 만든 것을 유통망에 등록 |
| 클라우드 → 로컬 | pull | 끌어 내린다 | 남이 만든 것을 내 엔진으로 가져옴 |
# 세 쌍에서 똑같이 생긴 명령
git push origin main
docker push myaccount/example-backend:0.1.0
huggingface-cli upload myaccount/my-model ./model # 개념상 push
git pull origin main
docker pull myaccount/example-backend:latest
huggingface-cli download myaccount/my-model # 개념상 pull
⚠️ 3주차에서 경고한 것을 다시 확인하세요. SAVE/LOAD 는 내 컴퓨터 안에서 메모리 ↔ 디스크를 오가는 것입니다(직렬화·역직렬화). push/pull(업로드/다운로드) 은 인터넷 너머 남의 서버와 주고받는 것입니다.
model.save()와huggingface-cli upload는 완전히 다른 축의 동작입니다. 헷갈리면 "인터넷이 필요한가"를 물으세요.
2.6 반례 — 이 쌍이 아닌 것들
미국식 분류로 배울 때 가장 효과가 큰 것이 반례입니다. 아래는 축 1처럼 보이지만 아닙니다.
| 이것 | 이 쌍인가? | 왜 아닌가 |
|---|---|---|
| VS Code ↔ GitHub Codespaces | ⚠️ 비슷하지만 아님 | 같은 에디터를 어디서 돌리느냐의 문제. push/pull 하는 산출물이 없음 |
| PostgreSQL ↔ Neon | ⚠️ 절반만 맞음 | 로컬 DB와 클라우드 DB는 같은 것의 두 위치. 로컬에서 만들어 올리는 구조가 아님 |
| Vercel | ❌ | 오른쪽만 있는 서비스. 대응하는 "로컬 Vercel"이 없음 (vercel dev 는 흉내) |
| Ollama ↔ Hugging Face | ✅ 맞음 | Ollama는 로컬 실행 엔진, 허깅페이스는 유통망. 10주차에서 이미 이 구조를 썼습니다 |
| ChatGPT | ❌ | 로컬 대응물이 없는 완제품 서비스. 축 1이 아니라 "API냐 로컬 모델이냐" 축 |
⭐ 판별 질문 하나로 끝납니다: "왼쪽 없이 오른쪽이 성립하는가?" 성립하면 그건 축 1의 쌍이 아니라 그냥 클라우드 서비스입니다.
2.7 이 대칭을 외우면 무엇이 이득인가
아직 안 배운 것을 추론할 수 있게 됩니다. 이것이 이 교재가 축을 세우는 이유입니다.
| 만나게 될 것 | 축 1로 미리 추론되는 것 |
|---|---|
| npm / npm registry | 로컬에 node_modules, 원격에 패키지 등록소. npm publish 가 push |
PyPI / pip | pip install 이 pull, twine upload 가 push |
| AWS ECR | 도커허브의 AWS판. 같은 자리, 다른 회사 |
| Ollama 모델 저장소 | ollama pull — 이름부터 같습니다 |
새 도구를 만났을 때 "이건 왼쪽인가 오른쪽인가" 한 번만 물으면 절반은 이해가 끝납니다.
3. 모델이라는 파일 — 컴퓨터 비전과 YOLO
축 1의 왼쪽 세 번째 칸이 모델 파일이었습니다. 그럼 그 파일 안에는 무엇이 들었고, 어떻게 만들어지는가. 이번 주 첫날에 YOLO를 조사한 것이 그 답입니다.
3.1 왜 이 주에 YOLO가 나오는가
두 가지 이유입니다. 첫째, 모델 파일의 실물을 보기 위해서입니다. yolo11n.pt 는 몇 MB짜리 파일 하나입니다. 이 파일을 내려받고(pull), 파인튜닝해서 새 파일을 만들고, 다시 올리는(push) 전 과정이 축 1 그 자체입니다. 둘째, 컴퓨터 비전은 "로컬 GPU가 필요한 대표 작업" 이기 때문입니다. 이미지·영상은 데이터가 크고, 실시간이 필요하고, 밖으로 내보내기 곤란한 경우가 많습니다.
3.2 신경망 사형제 — ANN · DNN · CNN · RNN
먼저 소속 범주부터 정리합니다. 이 넷은 대등한 넷이 아닙니다.
ANN (인공신경망) ─── 전부의 상위 범주
└── DNN (심층신경망) ← 은닉층을 여러 겹 쌓은 것 = '딥'러닝의 '딥'
├── CNN (합성곱 신경망) ← 공간을 보존한다
└── RNN (순환 신경망) ← 순서를 보존한다
| 약자 | 영문 | 한글 | 종차(무엇이 다른가) | 주 용도 |
|---|---|---|---|---|
| ANN | Artificial Neural Network | 인공신경망 | 입력층–은닉층–출력층으로 이루어진 기본형 | 모든 신경망의 뿌리 |
| DNN | Deep Neural Network | 심층신경망 | 은닉층이 여러 겹. 특징을 단계적으로 추출 | 일반적인 딥러닝 |
| CNN | Convolutional Neural Network | 합성곱 신경망 | 필터(커널)를 밀며 지역 특징을 뽑아 공간 구조를 보존 | 이미지·영상 |
| RNN | Recurrent Neural Network | 순환 신경망 | 직전 시점의 은닉 상태를 다음 시점으로 넘겨 순서를 보존 | 문장·음성·시계열 |
⭐ CNN과 RNN을 가르는 축은 "무엇을 보존하는가"입니다. CNN은 공간, RNN은 시간(순서) 입니다. 그림은 옆 픽셀이 중요하고, 문장은 앞 단어가 중요합니다. 그 차이가 구조를 갈랐습니다.
왜 CNN이 이미지에서 이기는가 — 이미지를 한 줄로 펴서 일반 신경망(DNN)에 넣으면 "옆에 있다"는 정보가 사라집니다. CNN은 작은 필터를 이미지 위에서 미끄러뜨리며 계산하므로 위치 관계가 살아남고, 같은 필터를 재사용하니 파라미터도 훨씬 적습니다.
RNN은 어디로 갔는가 — 10주차에서 배운 어텐션과 트랜스포머가 RNN의 자리를 대부분 가져갔습니다. RNN은 앞에서부터 순서대로만 볼 수 있어 긴 문장에서 앞부분을 잊어버리는 문제(장기 의존성) 가 있었고, 어텐션은 모든 토큰을 한꺼번에 보며 주목도를 계산해서 그 문제를 우회했습니다.
앞에서 배운 것과 이어집니다 — 1주차의 축 1(포함 관계)이 여기서 또 나옵니다. AI ⊃ 머신러닝 ⊃ 딥러닝 이었고, 딥러닝 안이 다시 ANN ⊃ DNN ⊃ (CNN, RNN) 입니다. 프랙탈입니다.
3.3 객체 검출의 역사 — 왜 이렇게 어려웠는가
객체 검출(Object Detection) 은 "이 사진에 무엇이 있는가"(분류)에 "어디에 있는가"(위치) 까지 답해야 하는 작업입니다. 답의 형태가 레이블 + 사각형 좌표(Bounding Box) 인 셈입니다.
| 시기 | 방식 | 한계 |
|---|---|---|
| 컴퓨팅 파워 향상 이전 | 영상처리 기법으로 해결 (윤곽선·색·특징점) | 조명·각도가 바뀌면 무너짐 |
| 2012년 AlexNet 이후 | 딥러닝으로 전환 | 연산량이 폭증 |
| 초기 딥러닝 접근 | 단일 윈도우 영역분할 | 정확도가 낮음 |
| 개선 | 다계층 중첩 윈도우 + 교차영역 지정 | 정확도는 올랐으나 연산량이 더 커짐 |
핵심 딜레마는 "어디를 볼지 모른다" 였습니다. 사진 어디에 물체가 있는지 모르니 여기저기 다 잘라서 확인해야 했고, 자를수록 정확해지지만 그만큼 느려집니다. 정확도와 속도의 트레이드오프입니다.
3.4 One-shot과 Two-shot ⭐ — 이 절의 판별 축
이 축 하나가 객체 검출 모델 전부를 두 줄로 가릅니다.
| Two-shot (2단계) | One-shot (1단계) | |
|---|---|---|
| 대표 | R-CNN 계열 (R-CNN → Fast → Faster) | YOLO, SSD, RetinaNet |
| 절차 | ① RPN(Selective Search) 으로 후보 영역 약 2,000개 추림 → ② 그걸 전부 CNN에 전달 | 이미지가 하나의 신경망을 한 번 통과하며 bounding box와 class를 동시 예측 |
| 연산량 | 매우 큼 | 작음 |
| 실시간성 | 거의 zero | 실시간 가능 |
| 정확도 | 초기에는 더 높았음 | 초기에는 낮았으나 빠르게 따라잡음 |
RPN(Region Proposal Network) 은 "여기쯤 뭔가 있을 것 같다"는 후보 영역을 먼저 뽑아주는 단계입니다. Two-shot이라는 이름이 여기서 나옵니다. 한 번은 후보를 뽑고(shot 1), 한 번은 그 후보를 분류합니다(shot 2).
⭐ One-shot의 발상 전환: "후보를 고른 다음에 본다"가 아니라 "보면서 동시에 고른다" 입니다. 이미지를 격자로 나누고 각 칸이 직접 "내 칸에 물체 중심이 있는가 / 있다면 무슨 물체이고 상자는 얼마나 큰가"를 한 번에 답합니다.
3.5 YOLO의 정의
YOLO(You Only Look Once) — [객체 검출 모델]인데, [이미지를 신경망에 단 한 번 통과시켜 위치와 종류를 동시에 예측한다]는 점에서 R-CNN 계열과 구별된다.
- 누가: Joseph Redmon
- 언제: 2015년 v1 논문 발표
- 당시 상황: 최고 성능 모델은 Faster R-CNN 이었습니다. YOLO가 처음으로 One-shot Detection을 고안했습니다.
이름이 그대로 설명입니다. "한 번만 본다." 사람이 사진을 볼 때 2,000군데를 잘라 보지 않는다는 직관에서 출발했습니다.
3.6 R-CNN 계열과의 비교
| 모델 | 상대 속도 | 특징 |
|---|---|---|
| R-CNN | 기준 | 후보 2,000개를 각각 CNN에 통과 |
| Fast R-CNN | 약 10배 빠름 | 이미지를 한 번만 CNN에 통과시키고 특징 맵을 공유 |
| Faster R-CNN | 다시 약 10배 | 후보 영역 추출까지 신경망으로 대체(RPN) |
| YOLO | 다시 약 10배 | 후보 추출 단계 자체를 없앰 |
R-CNN → Fast R-CNN → Faster R-CNN → YOLO는 대략 10배씩 속도 차이가 납니다.
YOLO의 성능 수치: 45프레임(초당), 빠른 버전은 155프레임. 초당 30프레임이면 사람 눈에 영상으로 보입니다. 45fps는 실시간 영상 처리가 가능하다는 뜻입니다.
YOLO의 단점 — 장점만 외우면 안 됩니다.
| 단점 | 내용 |
|---|---|
| 학습 정도에 따른 성능 저하 | 학습 데이터가 부족하면 급격히 나빠짐 |
| 이미지 크기에 따른 성능 저하 | 아주 작은 객체를 놓침 |
| 겹친 객체 예측 불확실성 | 한 격자 칸에 여러 물체 중심이 몰리면 흔들림 |
3.7 v1부터 v5까지 — 무엇이 더해졌는가
| 버전 | 핵심 변화 | 왜 그렇게 했는가 |
|---|---|---|
| v1 | GoogleNet 기반. 컨볼루션 24층 + 완전연결 2층, 풀링 미사용 | 최초 구현. 구조를 단순하게 유지 |
| v2 | 일괄 정규화(Batch Normalization) 추가, 완전연결 대신 앵커박스(Anchor Box) | 학습 안정화 + 상자 크기를 처음부터 추측하지 않고 기준 상자에서 보정 |
| v3 | 로지스틱 회귀로 Objectness Score 예측, 반복 검출 방지 | "여기에 물체가 있을 확률"을 명시적으로 계산해 같은 물체를 여러 번 잡는 문제를 줄임 |
| v4 | SPP 모듈 적용 — 추론시간 약 7% 증가, 정확도 5.7% 향상 | 여러 크기의 풀링을 합쳐 다양한 크기의 객체에 대응 |
| v5 | YOLOv3의 PyTorch 구현, BottleneckCSP 사용 | 프레임워크가 Darknet에서 PyTorch로 — 쓰기 쉬워짐 |
⭐ v4의 숫자를 기억하세요. 시간 +7%를 내고 정확도 +5.7%를 샀습니다. 이것이 3주차에서 배운 트레이드오프의 교과서적 사례입니다. "좋아졌다"가 아니라 "무엇을 주고 무엇을 받았는가" 로 읽는 습관이 이 표에서 훈련됩니다.
⚠️ v5부터는 논문 저자가 바뀝니다. v1~v3은 Joseph Redmon, v4는 다른 연구팀, v5는 Ultralytics사의 구현입니다. 그래서 "버전 숫자가 크다 = 더 좋다"가 항상 성립하지는 않습니다. 계보가 갈라져 있습니다.
3.8 조사 보고서의 부록 개념 네 가지
| 개념 | 무엇인가 |
|---|---|
| Feature Correlation | 서로 다른 특징 맵 사이의 상관 관계. 두 지점이 얼마나 닮았는지를 재서 매칭·추적에 씀 |
| Bilinear Interpolation | 격자 사이의 중간 값을 주변 값들의 거리 비율로 섞어 계산하는 방법 |
| Segmentation | 사각 상자가 아니라 픽셀 단위로 물체 영역을 칠하는 것. 검출보다 한 단계 정밀 |
| Online Model Aggregation | 여러 모델의 예측을 실시간으로 합쳐 하나의 결론을 내는 방식 |
Bilinear Interpolation 계산 예제 — 말로만 들으면 어렵고, 숫자를 넣으면 한 줄입니다.
A(2, 1) ────────── E(4.9, 2.74) ────────── B(7, 4)
x축 전체 거리 d = 7 − 2 = 5
A에서 E까지 거리 d1 = 4.9 − 2 = 2.9
비율 d1 / d = 2.9 / 5 = 0.58
검산: y = 1 + 0.58 × (4 − 1) = 1 + 1.74 = 2.74 ✅
이 0.58이 "E는 A에서 B 쪽으로 58% 지점에 있다"는 뜻이고, E의 값은 A값 × (1 − 0.58) + B값 × 0.58 입니다. 이미지를 확대·축소할 때 없는 픽셀을 이런 식으로 채웁니다. YOLO가 입력 이미지를 640×640으로 맞출 때 실제로 쓰이는 계산입니다.
3.9 YOLO와 PyTorch는 무엇이 다른가
가장 자주 나오는 혼동입니다. 둘은 같은 줄에 놓고 비교할 대상이 아닙니다.
| PyTorch | YOLO | |
|---|---|---|
| 상위 범주 | 딥러닝 프레임워크 | 모델 아키텍처(+ 학습된 가중치) |
| 하는 일 | 텐서 연산, 자동 미분, GPU 할당, 학습 루프 제공 | "이미지에서 객체를 찾는다"는 구조와 가중치 |
| 관계 | YOLO가 그 위에서 돈다 | PyTorch 없이는 못 돈다(v5 이후) |
| 비유 | 엔진과 변속기 | 그 위에 올린 차체 설계도 |
앞에서 배운 것과 이어집니다 — 1주차의 라이브러리 ↔ 프레임워크 축입니다. "누가 누구를 부르는가"로 갈렸습니다. PyTorch는 내 코드를 자기 규칙 안에서 부르는 쪽입니다. YOLO는 그 규칙 안에 얹히는 내용물입니다.
실무에서는 ultralytics 패키지가 YOLO를 감싸서 제공합니다. 그래서 우리가 실제로 부르는 것은 다음 한 줄입니다.
from ultralytics import YOLO
3.10 모델 크기 문자 — n / s / m / l / x
YOLOv8·YOLOv11에는 같은 구조를 크기만 다르게 만든 다섯 형제가 있습니다. n(Nano) → s(Small) → m(Medium) → l(Large) → x(Extra Large) 순으로 커지고, 느려지고, 정확해집니다.
⭐ 가장 가벼운 훈련용 모델은 YOLOv8/YOLOv11의 Nano(n) 입니다. 처음 파인튜닝을 시도할 때는 무조건
n으로 시작하세요. 큰 모델로 시작하면 에러인지 느린 건지 구분이 안 됩니다. 7주차에서 배운 "문제를 좁혀라" 와 같은 원칙입니다.
3.11 얼굴 인식 — 사전학습 모델을 그대로 쓰는 경우
객체 검출과 달리, 얼굴 인식은 이미 잘 학습된 모델을 그대로 가져다 쓰는 것이 일반적입니다.
- 패키지
keras_vggface, 모델 호출VGGFace(model='resnet50') - 학습 데이터: VGGFace2 데이터셋 — 셀럽 9,131명, 약 331만 장
from keras_vggface.vggface import VGGFace
model = VGGFace(model='resnet50') # ResNet-50 백본, VGGFace2 사전학습
331만 장이라는 숫자를 눈여겨보세요. 개인이 모을 수 있는 규모가 아닙니다. 이것이 축 1의 오른쪽(유통망)이 존재하는 이유 그 자체입니다. 누군가 331만 장으로 학습한 결과를 파일 하나로 받아 쓰는 것 — 그게 pull 입니다.
3.12 "도커라서 GPU를 못 쓴다"는 오해 ⭐
이번 주에 실제로 겪은 문제이자, 진단의 교과서입니다.
증상: 도커 컨테이너 안에서 YOLO 학습을 돌리려 하니 GPU를 못 쓴다는 에러. 흔한 오진: "도커는 격리되어 있으니까 GPU에 접근을 못 하는구나."
❌ "Docker라서 GPU를 못 쓴다"는 말은 사실이 아닙니다.
진짜 원인: Dockerfile 안에 이런 줄이 있었습니다.
# 문제의 줄 — requirements.txt 에서 무거운 패키지들을 걸러내고 설치했다
RUN grep -v -E 'torch|torchvision|ultralytics' requirements.txt > slim.txt \
&& pip install --no-cache-dir -r slim.txt
grep -v -E ... 는 "이 패턴에 맞는 줄을 빼라" 는 명령입니다.
이미지 용량을 줄이려고 torch·torchvision·ultralytics를 빌드에서 제외해버린 것입니다.
즉, GPU에 접근 못 한 게 아니라 컨테이너 안에 딥러닝 라이브러리 자체가 없었습니다.
진짜로 GPU를 쓰려면 ① NVIDIA Container Toolkit(컨테이너가 호스트의 GPU 드라이버에 접근하게 해주는 다리) 설치 ② 실행 시 --gpus all 플래그 ③ 이미지 안에 CUDA 대응 torch가 들어 있을 것, 이 셋이 모두 필요합니다.
docker run --gpus all -it myaccount/yolo-train:latest
⭐ 이 사건에서 가져갈 교훈은 GPU 설정법이 아닙니다. "에러 메시지가 가리키는 곳"과 "원인이 있는 곳"이 다를 수 있다는 것입니다. 에러는 GPU를 말했지만 원인은 Dockerfile 한 줄에 있었습니다. 컨테이너 안에서 문제가 생기면 먼저 컨테이너 안에 들어가서
pip list를 확인하세요.
docker exec -it <컨테이너이름> bash
pip list | grep -E 'torch|ultralytics' # 정말 설치되어 있는가부터 확인
4. 데이터셋을 아키텍처에 연결하기 — 로컬이냐 S3냐
YOLO를 파인튜닝하려면 데이터셋이 필요합니다. 그런데 그 데이터셋이 내 PC에 있을 수도, S3에 있을 수도 있습니다. 축 1이 그대로 설계 문제가 되는 순간입니다.
4.1 문제 정의
model.train(data="???") ← 여기에 들어갈 경로가 실행 환경마다 다르다
내 PC에서는 C:/projects/dataset/data.yaml, 원격 우분투에서는 /home/desktop/dataset/data.yaml, S3에서 받아 쓸 때는 버킷에서 내려받아 임시 폴더에 푼 경로입니다. 이 경로를 학습 코드 안에 적으면, 환경이 바뀔 때마다 학습 코드를 고쳐야 합니다. 6주차에서 배운 대로, 자주 바뀌는 것을 안쪽에 두면 안 됩니다.
4.2 포트 — 도메인이 요구하는 것만 적는다
Outbound 포트를 하나 만듭니다.
# app/ports/outbound/face_dataset_port.py
from abc import ABC, abstractmethod
class FaceDatasetPort(ABC):
"""학습에 쓸 데이터셋 설정 파일의 '절대경로'를 돌려준다."""
@abstractmethod
def get_dataset_config_path(self) -> str:
"""data.yaml 의 절대경로를 반환한다."""
...
⭐ 포트가 요구하는 것은 딱 하나입니다. "data.yaml 의 절대경로를 다오."
어디서 어떻게 구했는지는 묻지 않습니다. 이것이 6주차의 포트(추상) 입니다.
4.3 어댑터 — 구하는 방법은 두 가지
# app/resource_adapters/local_yolo_dataset_adapter.py
class LocalYoloDatasetAdapter(FaceDatasetPort):
def get_dataset_config_path(self) -> str:
# 이미 로컬 디스크에 있으므로 경로만 조립해서 돌려준다
return str(Path(self._base_dir, "data.yaml").resolve())
# app/resource_adapters/s3_dataset_adapter.py
class S3DatasetAdapter(FaceDatasetPort):
def get_dataset_config_path(self) -> str:
# ① S3에서 내려받고 ② 캐시 폴더에 풀고 ③ 그 경로를 돌려준다
self._download_if_absent()
return str(Path(self._cache_dir, "data.yaml").resolve())
| 어댑터 | 데이터가 있는 곳 | 축 1에서의 위치 |
|---|---|---|
LocalYoloDatasetAdapter | 내 디스크 | 왼쪽 (로컬) |
S3DatasetAdapter | AWS S3 버킷 | 오른쪽 (클라우드) |
⭐ 축 1이 코드 구조로 내려앉은 자리입니다. "로컬이냐 클라우드냐"는 이제 개념이 아니라 어댑터 두 개입니다.
4.4 유스케이스 — 학습 코드는 바뀌지 않는다
# app/use_cases/train_face_model.py
class TrainFaceModelUseCase:
def __init__(self, dataset_port: FaceDatasetPort) -> None:
self._dataset_port = dataset_port # ⭐ DI — 밖에서 주입받는다
def execute(self, epochs: int = 50, imgsz: int = 640) -> None:
data_path = self._dataset_port.get_dataset_config_path()
model = YOLO("yolo11n.pt")
model.train(data=data_path, epochs=epochs, imgsz=imgsz, device="cpu")
데이터 소스가 바뀌어도 App 코드는 수정할 필요가 없습니다. 조립하는 곳에서 어댑터만 갈아끼우면 됩니다.
# 조립소 — 여기 한 줄만 바뀐다
use_case = TrainFaceModelUseCase(LocalYoloDatasetAdapter(base_dir="./dataset"))
# use_case = TrainFaceModelUseCase(S3DatasetAdapter(bucket="...", prefix="...", cache_dir="./.cache"))
4.5 폴더 이름 확정 — resource_adapters
이번 주에 자원 어댑터 폴더명을 resource_adapters 로 확정했습니다.
app/
├── domain/ # 순수 도메인 (가장 안쪽)
├── ports/
│ ├── inbound/ # 밖에서 들어오는 문 (API·MCP 등)
│ └── outbound/ # 밖으로 나가는 문 (DB·S3·데이터셋 등)
├── use_cases/ # ⭐ 훈련 코드가 사는 곳
└── resource_adapters/ # outbound 포트의 구현체들
⚠️ 왜 폴더 이름을 확정하는 것이 중요한가 — 이름이 흔들리면 AI가 코드를 생성할 때마다 다른 자리에 파일을 만듭니다.
9주차에서 배운 하네스의 규칙이 이런 것입니다. CLAUDE.md 에 폴더 규칙을 적어두는 이유가 여기 있습니다.
5. 리눅스와 SSH — 내 두 번째 PC를 남의 컴퓨터로 만들기
여기서부터 이 장의 후반부입니다. 축 1의 오른쪽을 직접 만듭니다.
5.1 접속PC와 원격PC (축 5)
두 대의 컴퓨터에 역할 이름을 붙입니다. 이 이름을 흔들지 않는 것이 전체 실습의 절반입니다.
| 접속PC (클라이언트) | 원격PC (서버) | |
|---|---|---|
| 하는 일 | ssh 명령을 친다 | sshd(SSH 데몬)가 떠 있다 |
| OS 제약 | 윈도우·맥OS·우분투 무엇이든 무관 | SSH로 접속하려면 반드시 우분투(리눅스) |
| 접근 경로 | 내가 직접 앉아서 씀 | 접속PC를 통해서만 접근 |
| 브랜치 이름 | laptop | desktop |
⭐ 원격PC는 SSH로 반드시 우분투여야 하고, 원격은 접속PC를 통해서만 접근합니다. "원격PC 앞에 가서 직접 조작"하는 순간 이 실습의 의미가 사라집니다. 매일 아침 전원을 켜러 갈 때를 제외하면 원격PC의 화면은 보지 않는다는 규칙으로 진행합니다.
5.2 SSH의 정의
SSH(Secure Shell) — [원격 접속 프로토콜]인데, [통신 구간 전체를 암호화한다]는 점에서 텔넷(Telnet, 평문·23번)과 구별된다. 기본 포트는 22번입니다.
ssh <사용자이름>@<주소>
ssh desktop@192.168.0.228 # 기본 포트 22
ssh -p 2222 desktop@192.168.0.228 # 포트를 바꿨다면 -p 로 지정
이 한 줄이 성립하려면 네 가지가 동시에 참이어야 합니다. 이번 주 실습의 대부분이 이 넷을 만드는 일입니다.
- 원격PC에 SSH 서버가 설치되어 있다
- 그 서버가 실행 중이다 (
active (running)) - 원격PC까지 가는 네트워크 경로가 뚫려 있다 (포트포워딩·방화벽)
- 그 주소가 지금도 유효하다 ← 이 넷째 조건이 이 주의 과제입니다
5.3 리눅스 기초 — ~ 는 home입니다
원격PC에 들어가면 프롬프트에 ~ 가 보입니다. (desktop@DESKTOP-PC:~$)
| 표기 | 뜻 |
|---|---|
~ | 홈 디렉터리(home). 그 사용자의 개인 폴더 = /home/<사용자이름> |
/ | 루트. 파일 시스템의 최상단 |
. / .. | 현재 디렉터리 / 상위 디렉터리 |
⚠️
~를 "홈페이지"나 "루트"로 오해하지 마세요. 루트(/)는 시스템 전체의 꼭대기이고,~(홈)는 나에게 할당된 방입니다. 윈도우의C:\Users\사용자이름에 해당합니다. 이동은cd ~(홈),cd /(루트),cd -(직전 위치), 현재 위치 확인은pwd입니다.
5.4 WSL — 윈도우 안의 우분투
원격PC가 윈도우여도 됩니다. WSL(Windows Subsystem for Linux) 을 깔면 그 안에 우분투가 들어갑니다. 7주차에서 도커를 깔 때 이미 한 번 설치했던 그것인데, 이번에는 도커의 부속품이 아니라 그 자체가 서버가 됩니다.
wsl --install # 관리자 권한 PowerShell. 설치 후 재부팅
┌──────────────────────── 원격 PC (물리적인 컴퓨터) ────────────────────────┐
│ 윈도우 (호스트) │
│ ├── IP: 192.168.0.228 ← 공유기가 나눠준 LAN 주소 │
│ └── WSL2 (가상 네트워크 안) │
│ └── 우분투 │
│ ├── IP: 172.30.174.137 ← WSL 내부 주소 (NAT 뒤) │
│ └── sshd (SSH 서버) ← 우리가 접속하고 싶은 대상 │
└──────────────────────────────────────────────────────────────────────────┘
⭐ 이 그림이 이번 주 모든 삽질의 원인이자 해답입니다. 접속하고 싶은 대상은 안쪽(우분투) 에 있는데, 밖에서 보이는 주소는 바깥쪽(윈도우) 것입니다.
5.5 사용자 만들기 — 이름을 브랜치와 맞춘다
우분투를 처음 켜면 루트 사용자로 시작합니다. 작업용 사용자를 따로 만듭니다.
adduser desktop # 사용자 생성 (비밀번호를 물어봄)
usermod -aG sudo desktop # 'sudo' 그룹에 추가 = 관리자 권한 '자격' 부여
ubuntu config --default-user desktop # 다음부터 이 사용자로 시작
| 명령 | 하는 일 | 몇 번 하나 |
|---|---|---|
adduser <이름> | 사용자와 홈 디렉터리(/home/<이름>) 생성 | 1번 |
usermod -aG sudo <이름> | 관리자 권한의 '자격'을 부여 | 1번(일회성) |
sudo <명령> | 그 자격을 실제로 발동 | 매번 |
⭐
usermod -aG sudo는 권한을 '주는' 게 아니라 '자격을 부여하는' 것입니다. 신분증을 발급받는 것(일회성)과 그 신분증을 꺼내 보이는 것(매번)의 차이입니다.-aG의a는 append이며, 이걸 빼면 기존 그룹이 전부 날아갑니다.
⭐ 이름 규칙 — PC별 유저 이름은 깃허브 브랜치 이름과 일치시킨다
| PC | 우분투 사용자 이름 | 깃허브 브랜치 이름 |
|---|---|---|
| 원격 SSH 서버 | desktop | desktop |
| 로컬 WSL(접속PC) | laptop | laptop |
두 대에서 같은 저장소를 동시에 만지기 때문입니다. 터미널 프롬프트에 desktop@... 이 떠 있으면 지금 어느 PC에 있고 어느 브랜치를 만져야 하는지 눈으로 확인됩니다. 9주차에서 배운 대로, 규칙을 사람이 기억하게 하지 말고 화면이 알려주게 만드는 것입니다.
5.6 OpenSSH 서버 설치 — 클라이언트가 아닙니다
가장 자주 틀리는 지점입니다.
| OpenSSH 클라이언트 | OpenSSH 서버 | |
|---|---|---|
| 하는 일 | 내가 나가서 접속 | 남이 들어오게 받아줌 |
| 필요한 쪽 | 접속PC | 원격PC |
| 윈도우 기본값 | 대부분 이미 설치됨 | 설치 안 되어 있음 |
윈도우: 설정 > 시스템 > 선택적 기능 > [기능 보기] > "openssh" 검색 > OpenSSH 서버 > 설치
sudo apt update && sudo apt install -y openssh-server # 우분투(WSL) 쪽
⚠️ "SSH가 이미 되는데 왜 또 깔지?" — 그건 클라이언트입니다. ssh 명령을 칠 수 있다는 것과, 남이 나에게 ssh 로 들어올 수 있다는 것은 완전히 다른 이야기입니다. 축 5입니다.
5.7 윈도우 터미널 설치와 기본 프로필
winget install Microsoft.WindowsTerminal # 윈도우키 + R → cmd
설치 후 작업표시줄에 고정하고, 터미널 기본 프로필을 Ubuntu로 설정합니다(설정 → 시작 → 기본 프로필).
💡 기본 프로필을 우분투로 바꾸면 터미널을 열 때마다 바로 리눅스입니다. PowerShell과 우분투를 오가며 헷갈리는 일이 크게 줄어듭니다. 이것도 하네스입니다. 실수할 여지를 설정으로 없애는 것입니다.
5.8 포트포워딩 — 왜 이게 필요한가 ⭐
5.4절 그림을 다시 보세요. 문제는 이렇습니다.
접속PC ──── ssh desktop@192.168.0.228:22 ────▶ 윈도우 (192.168.0.228)
│ ??? 여기서 끊긴다
▼
우분투 (172.30.174.137) 의 sshd
192.168.0.228 은 윈도우의 주소입니다. 우분투는 그 안쪽 가상 네트워크(NAT) 뒤에 있어 밖에서 직접 보이지 않습니다. 그래서 윈도우에게 "22번 포트로 들어온 요청은 172.30.174.137의 22번으로 넘겨라" 라고 시킵니다. 이것이 포트포워딩(Port Forwarding), 윈도우 용어로는 portproxy 입니다.
# 관리자 권한 PowerShell
netsh interface portproxy add v4tov4 listenport=22 listenaddress=0.0.0.0 connectport=22 connectaddress=172.26.13.99
| 조각 | 뜻 |
|---|---|
netsh interface portproxy add v4tov4 | IPv4 → IPv4 전달 규칙을 추가한다 |
listenport=22 | 밖에서 들어오는 22번 포트를 |
listenaddress=0.0.0.0 | 모든 랜카드에서 받는다 |
connectport=22 | 안쪽 22번 포트로 |
connectaddress=172.26.13.99 | 우분투의 내부 IP로 넘긴다 |
⚠️ connectaddress 에 넣을 우분투 내부 IP는 우분투에서 ip addr(또는 hostname -I)로 확인합니다.
5.9 방화벽 — 규칙을 만들어도 막혀 있으면 못 들어온다
포트포워딩은 "들어오면 어디로 보낼지" 를 정할 뿐, "들어와도 되는지" 는 방화벽이 정합니다. 둘 다 필요합니다.
netsh advfirewall firewall add rule name="WSL_SSH" dir=in action=allow protocol=TCP localport=22
dir=in(들어오는 방향) / action=allow(허용) / protocol=TCP / localport=22(22번 포트) / name(나중에 찾아 지울 수 있게 알아볼 이름으로).
⚠️ 접속이 안 될 때 점검 순서는 항상 안쪽 → 바깥쪽입니다. ① 우분투에서
sshd가 떠 있는가 → ② 포트포워딩 규칙이 있는가 → ③ 방화벽이 열려 있는가 → ④ 주소가 맞는가. 순서를 바꾸면 시간을 훨씬 많이 씁니다.
5.10 규칙이 꼬였을 때 — 지우고 다시 등록한다
며칠 실습하면 포트 규칙이 여러 개 쌓여서 꼬입니다. 지우고 다시 등록하는 것이 가장 빠릅니다.
# 관리자 권한 PowerShell — .\ 를 반드시 붙입니다
.\netsh.exe interface portproxy delete v4tov4 listenport=22 listenaddress=0.0.0.0
.\netsh.exe interface portproxy add v4tov4 listenport=2222 listenaddress=0.0.0.0 connectport=2222 connectaddress=172.30.174.137
netsh interface portproxy show all # 현재 규칙 확인
.\ 는 왜 붙이는가 — PowerShell에서 netsh 만 치면 PowerShell의 별칭이나 함수가 먼저 잡힐 수 있습니다. .\netsh.exe 는 "지금 이 경로의 진짜 실행파일" 을 명시적으로 지목하는 표기입니다. "분명 명령을 쳤는데 아무 일도 안 일어난다"의 흔한 원인입니다.
왜 22번이 아니라 2222번인가 — 22번은 SSH의 표준 포트라 자동 공격 스캔이 가장 먼저 두드립니다. 2222번은 관례적인 대체 포트로, 충돌을 피하고 스캔도 조금 줄어듭니다.
⚠️ 포트를 바꾸면 네 곳을 전부 같이 바꿔야 합니다. ① sshd_config 의 Port ② portproxy의 listenport·connectport ③ 방화벽 localport ④ 접속 명령의 -p. 하나라도 22로 남아 있으면 안 됩니다.
5.11 sshd 설정 — 서버가 무엇을 허용할지 정한다
sudo nano /etc/ssh/sshd_config
Port 2222
PasswordAuthentication yes
AllowUsers desktop
ChallengeResponseAuthentication no
| 설정 | 뜻 | 왜 이렇게 두었는가 |
|---|---|---|
Port 2222 | 서버가 귀 기울일 포트 | 표준 포트 회피 |
PasswordAuthentication yes | 비밀번호 로그인 허용 | 실습 단계라 키 배포 전 |
AllowUsers desktop | 이 사용자만 접속 가능 | 나머지는 아예 시도조차 못 함 |
ChallengeResponseAuthentication no | 대화형 추가 인증 끔 | 인증 경로를 좁혀 원인 추적을 쉽게 |
수정 후 반드시 재시작합니다. 설정 파일은 읽는 시점이 시작할 때입니다.
sudo service ssh restart
⚠️
PasswordAuthentication yes는 실습용 설정입니다. 실제 서버에서는 공개키 인증만 허용하고 비밀번호 로그인을 끄는 것이 원칙입니다. 비밀번호는 추측 공격의 대상이 되지만 키는 그렇지 않습니다.앞으로 다시 만납니다 — 14주차에서 AWS EC2에 붙을 때
.pem키 파일을 씁니다. 그때가 공개키 인증입니다.
5.12 SSH 서버가 이상할 때 — 깨끗이 다시 깐다
sudo apt update && sudo apt remove -y openssh-server && sudo apt install -y openssh-server
세 명령을 && 로 이었습니다. && 는 앞 명령이 성공했을 때만 다음을 실행하므로, update 가 실패하면 지우지도 않습니다. 안전장치가 명령어 안에 들어 있는 것입니다.
⚠️ 재설치하면 sshd_config 가 기본값으로 돌아갈 수 있습니다. 5.11절 설정을 다시 넣어야 합니다.
LangTailor