Step 95. Level 1 종합 프로젝트 — 네트워크 감시 도구 완성하기

Step 95. Level 1 종합 프로젝트 — 네트워크 감시 도구 완성하기

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★★☆ | 예상 소요 시간 4시간

전제: Step 41~94 전체. 특히 Step 79(포트 스캐너), Step 92~93(SQLite), Step 94(Flask·JSON)를 재활용합니다.

  • 준비물: 파이썬, 그리고 지금까지 만든 코드 조각들(스캐너, DB 연동). 이 챕터의 실측은 파이썬 3.12, 스캔 대상은 127.0.0.1(내 컴퓨터)뿐입니다.
  • 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 스캔 대상은 내 컴퓨터(127.0.0.1)와 내 소유의 랩 대역뿐입니다. 이 챕터는 테스트입니다 — 요구사항 명세만 주어지고, 따라할 코드는 주어지지 않습니다.

지금까지 50개가 넘는 스텝을 지나왔습니다. 파이썬, 웹, 네트워크, 데이터베이스, Git, 문서화. 그런데 진짜 실력은 "따라하기를 해냈다"가 아니라 여기서 드러납니다 — 요구사항만 듣고 완성품을 만드는 것, 그것이 실력입니다. 중간에 막히는 부분이 나오면 축하할 일입니다. 여러분의 구멍을 정확히 알려 주는 신호이니, 그 스텝으로 돌아가 복습하고 다시 오면 됩니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 요구사항 명세만 보고 완성품을 만들 수 있다
  • 큰 문제를 파이프라인(스캔→저장→리포트→비교)으로 쪼개고 각 단계를 독립 검증한다
  • 소켓 스캔 + SQLite 저장 + 집계 리포트 + 집합 비교를 한 프로그램으로 통합한다
  • "첫 실행" 같은 가장자리 경우(edge case)를 스스로 발견하고 처리한다
  • README와 회고 문서로 작업을 마무리하는 습관을 완성한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3 (socket, sqlite3, sys, datetime — 전부 내장 모듈)
오늘의 재료 Step 79의 포트 스캔, Step 93의 바인딩·lastrowid·GROUP BY, Step 94의 JSON 감각
필요한 개념 IP 대역 순회, 1대多 테이블 설계, 집합 연산(set), 명령행 인자(sys.argv), 변화 탐지(change detection)
오늘의 산출물 netwatch.py + netwatch.db + README.md + 위키 회고

2-1. 우리가 만들 것 — "넷와치"

기업 보안팀에는 "우리 네트워크에 어제 없던 포트가 오늘 열렸다"를 잡아내는 감시 도구가 있습니다. 침입자가 뒷문(백도어)을 열거나, 누군가 허락 없이 서버를 띄우면 — 변화로 잡아냅니다. 오늘 여러분이 만들 것은 그것의 축소판, NetWatch(넷와치)입니다. 주소 대역을 스캔하고, 결과를 DB에 저장하고, 리포트를 출력하고, 지난번 스캔과 비교해 "새로 열린 포트"를 경고하는 도구입니다. 축소판이라고 얕보지 마세요 — 뼈대의 구조는 실무 도구와 동일합니다.

2-2. 큰 문제를 쪼개는 법 — 파이프라인으로 생각하기

명세를 한 번에 짜려 하면 반드시 무너집니다. 고수의 비결은 쪼개기입니다. 아래 네 단계는 각각이 독립된 작은 프로젝트라고 생각하세요.

[1단계] 스캔만 — 대역을 받아 열린 포트 목록을 메모리에
[2단계] DB 저장 — 그 목록을 SQLite에 시각과 함께
[3단계] 리포트 — DB에서 읽어 요약 출력
[4단계] 비교 — 직전 스캔과 대조해 새 포트 경고

각 단계는 독립적으로 검증합니다. 1단계가 출력을 찍어 확인한 뒤에야 2단계로 갑니다. "전부 짜고 한 번에 실행"은 초보의 함정이고, "조금 짜고 계속 확인"이 프로의 리듬입니다.

2-3. 요구사항 명세 (이것이 오늘의 교과서 전부입니다)

■ 프로그램명: netwatch.py
■ 실행: python netwatch.py 127.0.0.1 127.0.0.1
  (시작 IP와 끝 IP를 인자로 받는다. 인자 없으면 사용법 출력)

■ 기능 1 — 스캔
  · 대역의 각 IP에 대해 주요 포트(22, 80, 443, 445, 3389 등 10개 내외)를 연결 시도
  · 열린 포트는 배너(서비스가 먼저 보내는 인사말) 수집을 시도 (0.5초 제한)

■ 기능 2 — 저장 (netwatch.db)
  · 테이블 scans: id, scan_time(스캔 시각)
  · 테이블 findings: id, scan_id(어느 스캔인지), ip, port, banner
  · 바인딩(?) 필수 — 문자열 결합 금지 (Step 93의 교훈)

■ 기능 3 — 리포트
  · 스캔 완료 후 출력: 발견 기기 수, 열린 포트 총계, 기기별 열린 포트 목록

■ 기능 4 — 비교
  · 직전 스캔과 비교해 이번에 "새로 열린 포트(IP+포트 조합)"가 있으면
    "[!] NEW: 127.0.0.1:8001" 형태로 경고 출력
  · 첫 스캔이면 "기준선 생성" 메시지

■ 마무리
  · README.md: 도구 소개, 사용법, 동작 원리, 주의사항(합법 사용)
  · Git 저장소에 의미 단위로 커밋

시간 배분 가이드

예상 소요 4시간의 권장 배분: 1단계 스캔 50분, 2단계 DB 50분, 3단계 리포트 30분, 4단계 비교 60분, 입력 검증·정리·문서화 50분. 계획보다 오래 걸리는 단계가 나오는 것은 지극히 정상입니다 — 그 단계가 바로 여러분의 복습 포인트이니, 넘어가기 전에 위키에 한 줄 적어 두세요. 이 테스트의 점수는 완성품이 아니라 그 지도에서 나옵니다.


3. 따라 하기

이번에는 따라할 코드가 없습니다. 대신, 각 단계를 통과했는지 스스로 검증하는 체크 포인트와, 검증 환경을 만드는 법을 드립니다.

3-1. 검증 환경 준비 — 피험자를 띄워 두기

스캔 도구를 검증하려면 "열린 포트"가 미리 있어야 합니다. 테스트용 서버를 두세 개 띄워 두세요.

python -m http.server 8000 --bind 127.0.0.1     # 터미널 1
python -m http.server 9000 --bind 127.0.0.1     # 터미널 2

읽는 법: 터미널 창을 여러 개 열어 각각 띄웁니다. 실험이 끝나면 각 창에서 Ctrl+C로 반드시 종료하세요.

: 도구를 믿기 전에, 도구를 시험하는 환경을 만드는 것 — 이 순서가 실무 검증의 철칙입니다.

3-2. 체크 포인트 1 — 스캔 단계

  • Step 79의 스캐너 코드를 꺼내 함수로 포장하세요. scan(ip_list, ports) → 열린 것들의 리스트 반환.
  • IP 대역 만들기: 마지막 옥텟(마침표로 나눈 네 번째 숫자)만 돌리면 됩니다. ip.split('.')range()의 조합.
  • 검증: 127.0.0.1 하나만 스캔해, 여러분이 일부러 띄운 서버(8000, 9000)가 잡히는지 확인. 포트 목록에 8000, 9000을 추가하는 것을 잊지 마세요.

3-3. 체크 포인트 2 — DB 저장 단계

  • 테이블 두 개를 만드는 CREATE 문을 프로그램 시작 부분에 두세요. IF NOT EXISTS를 붙이면 매번 실행해도 안전합니다.
  • 스캔 한 번 = scans에 행 하나. 그 스캔의 id를 꺼내는 방법은 cur.lastrowid(방금 넣은 행의 번호)입니다.
  • 검증: 실행 후 sqlite3 netwatch.db로 들어가 .tablesSELECT * FROM findings;로 눈 확인.

3-4. 체크 포인트 3 — 리포트 단계

  • Step 93의 GROUP BY가 여기서 빛을 발합니다. "기기별 포트 수"는 SELECT ip, COUNT(*) FROM findings WHERE scan_id=? GROUP BY ip.
  • 검증: 출력이 사람이 읽기 편한 모양인가? "기기 1대, 열린 포트 총 4개"처럼 숫자가 먼저 나와야 합니다.

3-5. 체크 포인트 4 — 비교 단계 (오늘의 두뇌 운동)

  • "직전 스캔"은 어떻게 찾을까요? scans에서 두 번째로 최신 것 — ORDER BY id DESC LIMIT 2를 생각해 보세요.
  • 집합 연산: 이번 스캔의 (ip, port) 모음에서 직전 것을 빼면 "새로 열린 것"입니다. 파이썬의 set이 이 일을 잘합니다.
  • 검증: 스캔 → 아무 변화 없이 다시 스캔(경고 없어야 함) → 테스트 서버를 하나 더 띄우고 스캔([!] NEW가 떠야 함). 이 세 컷이 모두 맞아야 통과입니다.

3-6. 예측해 보기 — 비교 로직의 함정

예측: "새로 열린 포트"를 계산할 때, 이번 스캔과 직전 스캔의 결과를 단순히 비교하는 코드를 짰다고 합시다. 직전 스캔이 없는 경우(프로그램 생애 첫 실행)에는 무슨 일이 일어날까요?

  • (a) 모든 포트가 "NEW"로 경고된다
  • (b) 오류가 나며 죽는다
  • (c) 조용히 넘어간다

직접 확인: DB를 지우고 첫 실행을 해 보세요. 여러분의 코드에 따라 (a)나 (b)가 될 것입니다. 그래서 명세에 "첫 스캔이면 기준선 생성 메시지"가 있는 것입니다 — 예외 상황을 먼저 처리하는 분기를 넣어야 합니다. 실무에서도 "첫 실행", "빈 파일", "목록이 0개" 같은 가장자리 경우가 버그의 절반을 만듭니다.

3-7. 체크 포인트 5 — 입력 검증과 사용법

테스트에는 "사용자가 이상하게 쓰는 경우"도 포함됩니다.

  • 인자가 없거나 하나뿐일 때: 사용법을 출력하고 조용히 종료하세요(sys.argv 길이 검사).
  • IP 형식이 아닌 인자(abc)가 들어올 때: 오류 메시지와 함께 종료. ip.split('.')의 결과가 0~255 숫자 네 개인지 검사합니다.
  • 시작 IP가 끝 IP보다 큰 경우: 뒤집어 주거나 거부하세요 — 어떤 결정이든 README에 적습니다.

검증: 세 가지 이상한 입력을 실제로 던져 보고, 프로그램이 죽지 않고 "사람이 읽을 수 있는 안내"를 뱉는지 확인합니다. 공격자는 항상 이상한 입력을 던집니다 — 입력 검증은 편의 기능이 아니라 보안 기능입니다.

3-8. 체크 포인트 6 — 코드 정리와 커밋 단위

기능이 다 되어도 끝이 아닙니다. 남이 읽을 코드로 다듬는 것이 마지막 단계입니다.

  • 함수마다 한 줄 주석 — "이 함수는 무엇을 반환하는가".
  • 숫자 상수(포트 목록, 타임아웃)는 파일 위쪽에 모으세요. 나중에 고치기 쉬워집니다.
  • 커밋은 의미 단위로: "스캔 기능", "DB 저장", "비교 경고", "README"처럼 나누어 찍으면, 이 프로젝트의 git log 자체가 포트폴리오의 한 페이지가 됩니다.

검증: git log --oneline을 찍어 보며, 각 커밋이 "무엇을 한 커밋인지" 한눈에 읽히는지 확인합니다.


4. 미션과 연습문제

미션 — NetWatch 완성과 세 시나리오 검증

  1. 명세의 기능 1~4가 전부 동작하는 netwatch.py를 완성합니다.
  2. 세 시나리오를 모두 검증합니다: ① 첫 실행(기준선 생성) ② 변화 없음(경고 없음) ③ 새 포트 등장([!] NEW).
  3. SQL은 전부 바인딩, 인자 검증과 사용법 출력이 있습니다.
  4. README.md에 소개·사용법·원리·합법 사용 주의를 적고, Git 저장소에 의미 단위로 커밋합니다.
  5. 위키에 회고를 작성합니다 — 어디서 막혔는지, 어떻게 뚫었는지.

연습문제

문제 1. scans와 findings를 두 테이블로 나눈 이유를 "1대多 관계"와 "중복"이라는 말로 설명해 보세요.

문제 2. "새로 열린 포트"를 구할 때 왜 리스트가 아니라 집합(set)의 뺄셈(now - prev)이 편한지 설명해 보세요.

문제 3. 배너 수집이 특정 포트에서 멈추는 현상의 원인과, 소켓에 걸어야 할 두 가지 안전장치를 말해 보세요.

문제 4. 이 도구의 구조(기준선 저장 → 주기적 비교 → 차이 경고)가 포트 감시 외에 어떤 실무 감시 도구들에도 통하는지 두 가지 예를 들어 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

여러분의 코드가 스스로 동작했다면 그것이 정답입니다. 아래는 참고용 해설 — 이 챕터의 실측 검증에 실제로 사용한 구현입니다. 먼저 보지 말고, 막힌 단계의 것만 골라 읽으세요.

"""NetWatch — 내 네트워크의 포트 변화를 감시하는 도구 (교육용)"""
import socket, sqlite3, sys
from datetime import datetime

PORTS = [21, 22, 23, 25, 80, 110, 443, 445, 3306, 3389, 8000, 8001, 9000]
TIMEOUT = 0.3
DB = "netwatch.db"

def make_ip_range(start, end):
    """'127.0.0.1'~'127.0.0.3' 같은 대역을 IP 리스트로 만든다."""
    s, e = start.split("."), end.split(".")
    if len(s) != 4 or len(e) != 4:
        raise ValueError("IP 형식이 아닙니다.")
    if not all(p.isdigit() and 0 <= int(p) <= 255 for p in s + e):
        raise ValueError("IP 형식이 아닙니다.")
    prefix = ".".join(s[:3])
    return [f"{prefix}.{i}" for i in range(int(s[3]), int(e[3]) + 1)]

def scan(ip_list, ports):
    """열린 포트를 (ip, port, banner) 튜플 리스트로 반환한다."""
    found = []
    for ip in ip_list:
        for port in ports:
            s = socket.socket()
            s.settimeout(TIMEOUT)
            try:
                s.connect((ip, port))
                banner = ""
                try:
                    banner = s.recv(64).decode("utf-8", "replace").strip()
                except socket.timeout:
                    pass
                found.append((ip, port, banner))
            except OSError:
                pass
            finally:
                s.close()
    return found

def save_scan(conn, found):
    """스캔 한 번을 scans + findings에 저장하고 scan id를 반환한다."""
    cur = conn.cursor()
    cur.execute("INSERT INTO scans (scan_time) VALUES (?)",
                (datetime.now().isoformat(timespec="seconds"),))
    scan_id = cur.lastrowid
    for ip, port, banner in found:
        cur.execute(
            "INSERT INTO findings (scan_id, ip, port, banner) VALUES (?, ?, ?, ?)",
            (scan_id, ip, port, banner))
    conn.commit()
    return scan_id

def compare(conn, scan_id):
    """직전 스캔과 비교해 새로 열린 포트를 경고한다."""
    ids = [r[0] for r in conn.execute(
        "SELECT id FROM scans ORDER BY id DESC LIMIT 2").fetchall()]
    if len(ids) < 2:
        print("기준선 생성 — 첫 스캔이므로 비교할 과거가 없습니다.")
        return
    now = {(ip, p) for ip, p in conn.execute(
        "SELECT ip, port FROM findings WHERE scan_id=?", (ids[0],))}
    prev = {(ip, p) for ip, p in conn.execute(
        "SELECT ip, port FROM findings WHERE scan_id=?", (ids[1],))}
    for ip, port in sorted(now - prev):
        print(f"[!] NEW: {ip}:{port}")
    if now == prev:
        print("변화 없음 — 직전 스캔과 동일합니다.")

세 시나리오 실측 (2026-09-09, 대상 127.0.0.1, 피험자 서버 8000·9000 기동):

### 첫 스캔 (8000, 9000 열림)
대상 1개 IP, 포트 13개 스캔 중...
발견 기기 1대, 열린 포트 총 4개
  127.0.0.1: 22, 445, 8000, 9000
기준선 생성 — 첫 스캔이므로 비교할 과거가 없습니다.

### 두 번째 스캔 (변화 없음)
발견 기기 1대, 열린 포트 총 4개
  127.0.0.1: 22, 445, 8000, 9000
변화 없음 — 직전 스캔과 동일합니다.

### 세 번째 스캔 (8001을 새로 띄운 뒤)
발견 기기 1대, 열린 포트 총 5개
  127.0.0.1: 22, 445, 8000, 8001, 9000
[!] NEW: 127.0.0.1:8001

읽는 법: 두 가지를 눈여겨보세요. 첫째, 22번과 445번 — 이것은 제가 띄운 게 아니라 실측 컴퓨터에 원래 열려 있던 포트입니다. 내가 모르는 열린 문이 내 컴퓨터에도 있었다는 것, 이것이 감시 도구의 존재 이유입니다. 둘째, banner 칸이 비어 있습니다 — 파이썬의 http.server는 먼저 인사말을 보내지 않는 서비스라 배너가 없는 것이 정상입니다 (2026-09-09 실측). 배너는 SSH나 FTP처럼 "먼저 말을 거는" 서비스에서만 수집됩니다.

입력 검증도 실측했습니다 — 인자 없이 실행하면 사용법: python3 netwatch.py <시작IP> <끝IP>, abc를 넣으면 입력 오류: IP 형식이 아닙니다.를 출력하고 조용히 종료합니다 (2026-09-09).

연습문제 해답

문제 1 해답. 스캔 시각은 스캔마다 하나인데 발견된 포트는 스캔마다 여러 개 — 1대多 관계입니다. 한 장의 표에 욱여넣으면 시각이 모든 행에 중복됩니다. 그래서 scans(1)와 findings(多)로 나누고 scan_id 번호로 잇습니다. 리포트에서 JOIN으로 다시 붙일 수 있는 것이 이 설계의 증명입니다.

문제 2 해답. "A에는 있고 B에는 없는 것"은 집합의 차집합이 정확히 그 뜻이기 때문입니다. 리스트로 하면 이중 반복문 안에서 "직전 목록에 이게 있었나?"를 하나씩 물어야 하고, 집합은 now - prev 한 줄에 순서와 중복 걱정까지 없애 줍니다. 실측 구현도 {튜플들} 두 집합의 뺄셈으로 끝냈습니다.

문제 3 해답. 서비스가 배너를 보내지 않는데 recv가 무한 대기하기 때문입니다. 안전장치 두 가지: s.settimeout(0.3)으로 "최대 기다림"을 정하고, try/except로 감싸 실패 시 배너를 빈 문자열로 처리합니다. 기다리는 코드에는 항상 "최대 기다림"이 있어야 합니다. 그리고 실측에서 보았듯, 인사말을 안 하는 서비스(http.server)에서는 배너가 비는 것이 정상입니다 — 빈 배너를 오류로 취급하지 않는 것도 설계입니다.

문제 4 해답. 같은 구조의 사촌들: ① 파일 무결성 감시 — 서버의 파일 목록·해시를 기준선으로 저장하고 몰래 바뀐 파일을 경고. ② 계정 감시 — 어제 없던 관리자 계정이 생겼는지 비교. 관찰 대상만 바뀔 뿐, 기준선 저장 → 주기적 비교 → 차이 경고의 뼈대는 동일합니다.

완료 기준 체크리스트

  • [ ] 명세만 보고 네 기능을 갖춘 도구를 완성할 수 있다
  • [ ] 프로그램을 단계별로 쪼개 개발하고 각 단계를 검증할 수 있다
  • [ ] "첫 실행" 등 가장자리 경우를 스스로 발견하고 처리할 수 있다
  • [ ] SQL은 전부 바인딩이고, 인자 검증과 사용법 출력이 있다
  • [ ] README와 회고 문서로 작업을 마무리했다
  • [ ] Level 1의 어느 스텝이 내 약점인지 말할 수 있다
  • [ ] 미션: 세 시나리오 검증까지 완료했다

6. 흔한 실수와 해결

벽 1. 스캔이 너무 느려요

증상: 대역 전체에 수 분이 걸립니다.

원인: 닫힌 포트는 연결 거절까지 기다립니다. 타임아웃이 길면 곱하기로 느려집니다.

해결: 소켓의 타임아웃을 0.2~0.5초로 짧게(Step 79 복습). 그래도 느리면 스캔 포트 수를 줄여 개발하고, 완성 후 늘리세요. 실측 구현은 127.0.0.1 × 13포트에 타임아웃 0.3초로 몇 초 안에 끝났습니다.

벽 2. lastrowid가 None이 나와요

증상: scans에 넣었는데 id를 못 가져옵니다.

원인: conn.execute로 바로 실행하면 커서를 못 잡습니다. cur = conn.cursor()로 커서를 만들고 cur.execute(...)한 뒤 cur.lastrowid를 읽어야 합니다.

해결: 커서를 명시적으로 만드는 흐름으로 바꾸세요 (모범 답안의 save_scan 참조).

벽 3. 비교가 엉켜요 — 다 NEW로 뜨거나, 떠야 할 게 안 떠요

증상: 변화가 없는데 경고가 뜹니다.

원인: 튜플 비교 시 자료형 불일치(포트가 한쪽은 숫자, 한쪽은 문자열)이거나, scan_id를 잘못 잡았거나.

해결: 두 스캔의 findings를 각각 SELECT로 출력해 눈으로 대조해 보세요. 집합에 넣기 전 print(now, prev)로 찍어 보는 것이 가장 빠른 진단입니다.

벽 4. 배너 수집에서 프로그램이 멈춰요

증상: 특정 포트에서 영원히 기다립니다.

원인: 서비스가 배너를 보내지 않는데 recv가 무한 대기합니다. 실측에서도 파이썬 http.server는 배너를 보내지 않았습니다 — 타임아웃이 없었다면 거기서 멈췄을 것입니다.

해결: recv 전에 소켓 타임아웃을 걸고, try/except로 감싸서 실패 시 배너를 빈 문자열로. 기다리는 코드에는 항상 "최대 기다림"이 있어야 합니다.

벽 5. 막힘이 코드가 아니라 마음에서 와요

증상: "명세만 있고 답이 없다"는 상황 자체가 낯설어 손이 멈춥니다. 이 챕터에서 가장 많은 포기가 나오는 지점입니다.

원인: 정상입니다. 모범 답안이 보이는 연습과, 명세만 있는 작업은 다른 근육을 씁니다.

해결: 세 가지 전술. 첫째, 막힘은 데이터입니다 — "배너 recv에서 30분"처럼 위키에 적으면 약점 지도가 완성됩니다. 둘째, 군소 스크립트로 실험하세요 — test_lastrowid.py처럼 다섯 줄짜리 실험 파일에서 궁금한 동작만 떼어 확인하고 본편에 이식합니다. 셋째, 한 문제에 40분 이상 붙잡히면 그 스텝의 본편으로 돌아가 따라하기를 다시 하는 쪽이 빠릅니다. 자존심의 문제가 아니라 전략의 문제입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
변화 탐지(change detection) 기준선 저장 → 주기적 비교 → 차이 경고. 실무 감시 도구의 공통 뼈대
파이프라인 분할 스캔→저장→리포트→비교를 독립 검증 가능한 단계로 쪼개는 설계
1대多 관계 스캔 1번 : 발견 N건 — 표를 둘로 나누고 번호로 잇는다
집합 차집합 now - prev — "새로 열린 것"의 정확한 정의
가장자리 경우(edge case) 첫 실행, 빈 목록 — 버그의 절반을 만드는 예외 상황들
공격 표면의 자기 점검 내가 모르는 열린 포트(실측의 22, 445)가 내 컴퓨터에도 있다

오늘의 명령어·코드

코드 하는 일
sys.argv 명령행 인자 — 길이 검사가 입력 검증의 시작
socket.socket() + settimeout + connect 포트 연결 시도 (Step 79 재활용)
CREATE TABLE IF NOT EXISTS 매번 실행해도 안전한 테이블 준비
cur.lastrowid 방금 INSERT한 행의 번호 — scans↔findings의 고리
GROUP BY ip + COUNT(*) 기기별 포트 수 요약
ORDER BY id DESC LIMIT 2 최신 스캔 두 개 — 비교의 재료
set(...) - set(...) 새로 열린 포트 계산

명령어보다 중요한 감각

요구사항 명세만으로 완성품을 만드는 것 — 그것이 이 테스트의 이름이자 실력의 정의입니다. 큰 문제는 파이프라인으로 쪼개고, 각 단계는 독립적으로 검증하고, 가장자리 경우를 먼저 생각합니다. 그리고 도구를 믿기 전에 도구를 시험하는 환경을 만듭니다. 이 네 가지는 이 프로젝트가 끝나도 남는 습관입니다.

완성했다면 스스로 채점해 보세요 — 스캔 정확도, DB 설계, 리포트 가독성, 비교 로직, 문서화의 다섯 항목 각 0~2점, 총 7점 이상이면 통과입니다. 설계는 정답이 아니라 선택과 이유입니다. 명세와 다른 설계를 택했다면, 그 이유를 README에 적으세요 — 이유 있는 다른 설계는 틀린 게 아닙니다.

통과하셨습니까? Level 1, 완주입니다. 컴퓨터 안쪽을 열어 보고, 네트워크의 대화를 엿듣고, 창고지기와 대화하고, 서버를 직접 만들어 보고, 마지막으로 명세만으로 도구를 완성했습니다. 오늘 만든 감시견은 작지만, 그 뼈대는 실무 변화 탐지 시스템 그대로입니다. 이 도구의 스캔 대상은 어디까지나 내 컴퓨터와 내 랩뿐이라는 약속과 함께, Level 1의 문을 닫으세요.


전부 체크되면 Step 95 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.