13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[작성자:] admin

  • [HomeLabs] OPNsense 마이그레이션: 홈 네트워크 업그레이드 전략

    [HomeLabs] OPNsense 마이그레이션: 홈 네트워크 업그레이드 전략

    OPNsense 마이그레이션으로 홈 네트워크 업그레이드하는 법

    OPNsense 마이그레이션을 고민하게 되는 순간은 단순히 인터넷 속도가 아쉬울 때가 아니었습니다. NAS 붙이고, 홈서버 돌리고, 외부 접속 열고, IoT를 분리하고 싶어지면 속도보다 구조가 먼저 병목이 되더라고요. 저도 처음엔 소비자 공유기 설정 화면에서 포트포워딩 몇 개만 더 열면 끝날 줄 알았는데, 어느 순간부터는 규칙보다 예외가 더 많아졌습니다. 그때부터 장비 교체가 아니라 네트워크 역할을 다시 나누는 쪽으로 생각이 바뀌었습니다.

    막상 해보면 핵심은 의외로 분명합니다. 라우팅, DHCP, DNS, 방화벽 책임을 어디에 둘지 다시 설계하는 일입니다. 만족도는 높지만 접근 순서가 틀리면 가족 와이파이부터 바로 영향을 받습니다. 그래서 이 글은 설치 후기가 아니라, 언제 넘어가야 하고 어디서 망가지며 어떻게 다운타임을 줄일지에 집중해 정리했습니다.

    특히 아래 조건에 가깝다면 비싼 소비자 공유기 업그레이드보다 OPNsense 마이그레이션이 더 잘 맞습니다.

    • 내부망을 메인 PC, 서버, IoT, 게스트로 나누고 싶다
    • 포트포워딩과 VPN을 함께 운영해야 한다
    • 문제가 생겼을 때 로그와 상태로 원인을 추적하고 싶다
    • 기존 공유기를 버리기보다 AP로 재활용하고 싶다

    기존 공유기 단일 구조와 OPNsense 중심 분리 구조를 한눈에 보여주는 개요 이미지입니다.

    왜 OPNsense 마이그레이션이 필요한가

    소비자 공유기는 기본값이 강합니다. 인터넷 연결, NAT, DHCP, 무선, 간단한 방화벽이 한 장비 안에서 돌아가고, 대부분은 그게 장점이죠. 반대로 OPNsense는 기본값보다 의도가 중요합니다. 어떤 세그먼트가 어떤 DNS를 쓰는지, 어떤 VLAN이 어디까지 접근 가능한지, 어떤 포트포워딩이 상위 NAT와 충돌하는지까지 사용자가 구조를 정해야 합니다.

    제가 판단 기준으로 삼는 차이는 기능 수가 아니라 정책의 수입니다. 네트워크를 지나는 트래픽에 예외가 계속 늘어난다면, 고급형 소비자 공유기로 버티는 것보다 OPNsense 쪽이 장기적으로 덜 피곤합니다. 반대로 인터넷, 스트리밍, 무선만 안정적이면 되는 집이라면 마이그레이션이 오히려 관리 부담이 될 수 있습니다.

    • OPNsense가 유리한 경우: VLAN 분리, 내부 DNS 이름해결, VPN, 포트포워딩, 트래픽 추적, 정책 기반 제어가 필요한 경우
    • 기존 공유기가 나은 경우: 무선 커버리지가 핵심이고, 외부 공개 서비스가 없고, 네트워크를 손댈 일이 거의 없는 경우
    • 애매한 경우: 라우팅만 OPNsense로 넘기고 무선은 기존 장비를 AP 모드로 유지하는 하이브리드 구성이 현실적입니다

    중요한 건, OPNsense는 단순히 더 강한 공유기가 아니라 장애 원인을 분해해서 볼 수 있게 해주는 플랫폼이라는 점입니다. 이 차이를 알고 들어가면 꽤 편합니다. 모르고 들어가면 초반 피로도가 확 올라갑니다.

    OPNsense 마이그레이션 전에 먼저 정리할 것들

    1. 현재 네트워크를 도식화하지 않으면 장애 시간이 길어집니다

    OPNsense로 넘어갈 때 흔한 실수는 새 기능에 신나서 기존 상태를 정확히 안 남기는 겁니다. 이 단계에서 필요한 건 멋진 설계도가 아니라 현재 인터넷이 왜 되는지 설명 가능한 상태입니다. 아래 항목은 최소한 적어두는 편이 좋습니다.

    • 회선 연결 방식: DHCP, PPPoE, 고정 IP
    • 현재 LAN 대역과 게이트웨이 주소
    • 고정 IP 장비: NAS, 프린터, 홈서버, NVR, 카메라
    • 외부 공개 포트와 실제 목적지 장비
    • DNS를 누가 맡고 있는지: 공유기, Pi-hole, NAS, 외부 DNS
    • 무선 AP가 라우팅까지 하는지, 순수 AP인지
    • 상위 장비가 모뎀인지, 모뎀 겸 공유기인지

    이걸 안 적어두면 나중에 “인터넷은 되는데 NAS 이름이 안 풀린다” 같은 문제에서 시간을 크게 잃습니다. 그 증상이 DNS 문제인지, DHCP 정적 매핑 누락인지, AP 뒤에 남아 있는 옛 서브넷 때문인지 바로 구분이 안 되거든요.

    2. 전부 교체하지 말고 역할만 먼저 분리하세요

    마이그레이션에서 가장 생산적인 판단은 “무엇을 새로 살까”보다 “누가 어떤 역할을 맡을까”입니다. 특히 홈 네트워크는 무선 품질 때문에 기존 공유기를 완전히 버리는 게 꼭 답은 아닙니다. 저는 보통 아래처럼 나눠서 봅니다.

    역할 기존 소비자 공유기 유지 OPNsense로 전환 권하는 기준
    라우팅/NAT 설정은 쉽지만 예외 처리가 약함 정책 기반 제어와 상태 추적 가능 포트포워딩, VLAN, VPN 중 2개 이상 필요하면 OPNsense
    무선 즉시 사용 가능, 관리 쉬움 별도 AP 필요 가능 무선 품질이 괜찮으면 기존 장비를 AP로 재활용
    DHCP/DNS 단순 환경에 적합 정적 매핑, 내부 이름해결, 접근제어에 유리 NAS나 홈서버를 이름으로 접근해야 하면 OPNsense 쪽
    방화벽 기본 허용/차단 수준 규칙 순서, 로그, 상태 기반 추적 가능 IoT 격리나 외부 공개 서비스가 있으면 OPNsense
    장애 분석 원인 추적이 어려움 pf 상태, NAT 규칙, 라이브 로그 확인 가능 문제 원인을 직접 좁혀가고 싶다면 OPNsense
    운영 난이도 낮음 초기 설계와 검증 필요 네트워크를 거의 손대지 않을 집이면 굳이 안 옮겨도 됨

    핵심은 간단합니다. 무선은 유지해도 되고, 라우팅은 분리하는 게 맞습니다. 이 경계만 잘 그어도 전환 난이도가 확 내려갑니다.

    OPNsense 마이그레이션 전략: 한 번에 갈아엎지 마세요

    집 네트워크는 사무실보다 장애 허용도가 낮습니다. 서버가 잠깐 죽는 건 참아도 TV와 휴대폰 와이파이가 안 되면 바로 티가 납니다. 그래서 저는 병행 전환만 권합니다. 기존 공유기를 빼고 새 장비를 바로 올리는 방식은, 장애가 생겼을 때 비교 기준 자체가 사라집니다.

    1. OPNsense 장비 준비: 최소 2개 NIC가 있는 x86 장비를 준비합니다.
    2. 분리된 테스트 환경에서 먼저 부팅: WAN에는 회선 또는 상위 장비, LAN에는 테스트 PC 1대만 붙입니다.
    3. 기본 인터넷과 관리 UI 확인: WAN 주소, 기본 게이트웨이, LAN DHCP, 웹 UI 접근부터 검증합니다.
    4. 기존 공유기는 AP 모드로 전환: DHCP와 NAT는 끄고 무선만 맡깁니다.
    5. 고정 IP와 포트포워딩 이전: NAS, 홈서버처럼 영향 큰 장비부터 옮깁니다.
    6. VLAN과 세부 정책은 마지막: 기본 LAN이 멀쩡하다는 게 확인된 뒤에 분리 정책을 얹습니다.

    여기서 순서를 뒤집으면 대부분 꼬입니다. 특히 초반부터 VLAN, DNS 오버라이드, VPN, IDS/IPS까지 한 번에 켜는 건 추천하지 않습니다. 문제는 기능이 많아서가 아니라, 증상이 서로 비슷하게 보이기 때문입니다. DNS ACL 문제, VLAN 태깅 오류, AP의 잔존 DHCP는 전부 “어떤 기기만 인터넷이 안 됨”으로 보일 수 있습니다.

    실전 구현: 최소 다운타임으로 옮기는 순서

    1. 기존 공유기 설정 백업과 현재 상태 점검

    백업 파일만 믿지 말고, 클라이언트 기준 상태도 남겨두세요. 전환 후 비교할 때 훨씬 빠릅니다. 코드 블록은 설정을 바꾸는 용도가 아니라, 현재 상태를 기록하고 전환 전후를 비교하는 용도입니다.

    # Linux 클라이언트 기준: 전환 전 상태 저장
    ip addr
    ip route
    cat /etc/resolv.conf
    arp -an
    
    # 전환 후 비교용
    ping -c 4 1.1.1.1
    ping -c 4 google.com
    traceroute 1.1.1.1
    

    해석 포인트는 단순합니다. 숫자 IP는 되는데 도메인이 안 되면 DNS를 먼저 봅니다. 첫 홉이 새 OPNsense LAN 주소가 아니면 클라이언트가 아직 예전 게이트웨이를 보고 있는 겁니다. 이 단계에서 이미 문제를 절반은 줄일 수 있습니다.

    2. 전환 후 검증용 테스트 세트는 서비스별로 준비하세요

    제가 실제로 쓰는 검증 기준은 단순 인터넷 연결이 아닙니다. 외부 IP, 외부 DNS, 내부 DNS, 내부 서비스, 외부 진입을 구분해서 봅니다. 그래야 어디가 깨졌는지 바로 분류됩니다.

    # 외부 IP / 외부 DNS / 내부 이름해결을 한 번에 점검
    for host in 1.1.1.1 google.com nas.home.arpa; do
      echo "== $host =="
      ping -c 2 "$host"
    done
    
    # TCP 서비스 확인 예시
    nc -vz nas.home.arpa 445
    nc -vz opnsense.lan 443
    

    여기서는 내부 로컬 이름을 일부러 넣는 편이 좋습니다. OPNsense로 옮긴 뒤 가장 늦게 발견되는 문제가 바로 내부 DNS 등록 누락이기 때문입니다. 인터넷은 되는데 NAS 이름만 안 풀리면 사용자 입장에선 거의 망가진 것처럼 느껴집니다.

    3. OPNsense 초기 설정은 기본값을 존중하되 자동 마법에 기대진 마세요

    설치 직후엔 욕심내서 꾸미지 말고 아래 순서만 확인합니다.

    1. WAN 인터페이스에 회선 연결
    2. LAN 인터페이스에 테스트 PC 연결
    3. LAN 대역 지정
    4. DHCP 서버 활성화
    5. 웹 UI 접근 확인
    6. 외부 IP와 도메인 해석 확인

    이 시점에서 특히 중요한 건 DNS입니다. OPNsense는 기본 설치에서 Unbound DNS가 기본 리졸버로 활성화되는 구성이 일반적입니다. 내부 이름해결까지 생각하면 Services > Unbound DNS에서 아래 항목을 먼저 확인해 두는 편이 좋습니다.

    • Register ISC DHCP4 Leases: ISC DHCP를 쓰는 경우, DHCP 호스트명을 Unbound에 반영
    • Register DHCP Static Mappings: 정적 매핑 이름을 내부 DNS에 반영
    • Access Lists: 클라이언트 네트워크가 리졸버에 질의할 수 있는지 확인
    • Network Interfaces / Outgoing Interfaces: 특별한 이유가 없으면 수동 지정보다 기본값을 유지

    다만 여기에는 중요한 예외가 있습니다. Kea DHCP를 쓰는 경우에는 ISC DHCP처럼 동적 임대 호스트명이 바로 Unbound에 자동 등록되지 않습니다. 그래서 초반에는 정적 매핑과 내부 도메인 정책부터 먼저 맞추는 쪽이 더 안전합니다. 실제로 OPNsense 초반 장애는 방화벽보다 DNS에서 많이 나더라고요.

    OPNsense 마이그레이션 실전 구성과 VLAN 네트워크 구성 다이어그램

    인터넷 회선, OPNsense, 스위치, AP, 서버, IoT 네트워크가 어떻게 분리되는지 보여주는 구성 이미지입니다.

    4. AP 모드 전환은 체크박스가 아니라 이중 라우팅 제거 작업입니다

    기존 소비자 공유기를 AP로 재활용할 때는 아래 네 가지를 꼭 확인합니다.

    • DHCP 비활성화: 주소를 두 군데서 주면 클라이언트가 서브넷을 섞어 받습니다.
    • NAT 비활성화: 이중 NAT가 남으면 외부 진입과 VPN이 꼬입니다.
    • LAN 포트 uplink: 상위 연결은 보통 WAN이 아니라 LAN 포트로 넣습니다.
    • 관리 IP 고정: AP 관리 주소를 새 대역 안의 고정 IP로 잡아둡니다.

    여기서 자주 나오는 사고 시나리오가 있습니다. AP라고 해놨는데 라우터 기능이 일부 남아 있어서 휴대폰은 새 대역을 받고 TV는 옛 대역을 받는 식입니다. 겉으로 보면 무선 문제 같지만, 근본 원인은 DHCP 서버가 둘인 경우가 많습니다.

    5. VLAN은 기능이 아니라 운영 규칙입니다

    VLAN을 넣는 순간 네트워크는 깔끔해지지만 동시에 추적 포인트도 늘어납니다. 그래서 저는 기본 LAN이 완전히 안정된 다음에만 넣습니다. 그리고 VLAN ID보다 먼저 접근 정책 문장을 적어둡니다. 예를 들면 이런 식입니다.

    • 메인 LAN은 서버 VLAN과 관리 IP에 접근 가능
    • IoT VLAN은 인터넷만 가능, 메인 LAN과 서버 VLAN은 차단
    • 게스트 VLAN은 내부망 전체 차단, 인터넷만 허용
    • 관리용 네트워크는 OPNsense UI와 스위치/AP 관리 IP 접근 허용

    이 문장이 먼저 없으면 VLAN은 분리가 아니라 혼란이 됩니다. 태깅이 맞아도 방화벽 규칙이 애매하면 결국 운영자가 더 헷갈립니다. 홈랩에서 가장 위험한 상태는 “분리했다고 믿지만 실제로는 접근이 열려 있는 상태”입니다.

    OPNsense 마이그레이션 중 설정과 검증에 바로 쓰는 명령어

    웹 UI만으로도 운영은 되지만, 마이그레이션 시점에는 셸에서 현재 상태를 보는 편이 빠릅니다. 아래 명령은 설정을 바꾸는 게 아니라 관찰용이라 부담이 적고, 어디서 막히는지 가닥을 잡는 데 꽤 유용합니다.

    # OPNsense 셸에서 현재 상태 빠르게 확인
    ifconfig
    netstat -rn
    sockstat -4 -6 -l | egrep ':(53|67|80|443)\b'
    pfctl -sr
    pfctl -s nat
    pfctl -ss
    

    제가 보는 포인트는 이렇습니다.

    • <code>netstat -rn: 기본 경로가 WAN 쪽으로 제대로 잡혔는지
    • sockstat: DNS(53), DHCP(67), 웹 UI(80/443) 같은 핵심 서비스가 실제로 리슨 중인지
    • pfctl -sr: 필터 규칙이 생각한 순서대로 로드됐는지
    • pfctl -s nat: 포트포워딩과 아웃바운드 NAT가 실제로 반영됐는지
    • pfctl -ss: 연결 상태가 생기고 있는지 확인할 수 있는지

    포트포워딩이나 NAT reflection 관련해서는 pfctl -s nat가 특히 유용합니다. GUI에 규칙이 있어 보여도 적용 버튼을 놓친 경우, 셸에서 활성 NAT 규칙을 보면 바로 티가 납니다.

    # 외부 진입 문제를 볼 때 최소 확인 세트
    pfctl -s nat
    pfctl -ss | egrep '(:80|:443|:51820|:22)'
    tcpdump -ni <wan_if> host <public_or_remote_ip>
    tcpdump -ni <lan_if> host <internal_server_ip>
    

    이 블록은 트래픽 흐름을 좁혀보는 용도입니다. 패킷이 WAN에는 들어오는데 LAN으로 안 넘겨지면 NAT나 필터 규칙 문제를 먼저 의심합니다. WAN과 LAN 양쪽에 SYN이 보이는데 서버에서 SYN/ACK가 안 나오면, 그땐 서버 방화벽이나 서버 기본 게이트웨이를 먼저 보는 쪽이 맞습니다.

    트러블슈팅: OPNsense 마이그레이션에서 자주 막히는 지점들

    관리 UI는 열리는데 인터넷이 안 될 때

    이건 단순히 인터넷 불가라고 뭉뚱그리면 안 됩니다. OPNsense UI가 열리면 최소한 LAN, DHCP, 웹 UI는 살아 있다는 뜻입니다. 따라서 문제는 대개 WAN, 기본 경로, DNS 중 하나로 좁혀집니다.

    증상 가장 흔한 근본 원인 우선 확인할 항목 제가 보통 내리는 판단
    숫자 IP는 되는데 도메인이 안 됨 Unbound 설정, ACL, 상위 DNS 경로 문제 Unbound 활성화, Access Lists, 클라이언트 DNS 주소 방화벽보다 DNS 가능성이 높음
    어느 사이트도 안 열림 WAN 주소 미할당, 기본 게이트웨이 불량 WAN 상태, netstat -rn, 상위 장비 연결 방식 회선 인증 또는 상위 장비 문제부터 봄
    일부 기기만 안 됨 중복 DHCP, 예전 임대 정보, AP 오구성 클라이언트 IP 대역, 게이트웨이, DNS 서버 주소 라우팅보다 DHCP 서버 충돌을 먼저 의심
    내부 이름만 안 풀림 정적 매핑 미등록, DHCP 이름 등록 미반영 Register DHCP Static Mappings, Host Override, DHCP 백엔드 종류 인터넷은 정상, 내부 DNS 설계 문제

    제가 실제로 자주 본 케이스는 인터넷은 되는데 nas.home.arpa 같은 내부 이름만 안 풀리는 경우입니다. 이건 사용자 입장에선 반쯤 망가진 상태에 가깝습니다. 그런데 운영자가 이걸 가볍게 넘기면 이후 백업, 미디어 서버, 프린터 검색이 줄줄이 꼬입니다.

    포트포워딩이 안 먹을 때

    이쪽은 놀랄 만큼 자주 상위 NAT가 원인입니다. OPNsense WAN에 공인 IP가 아니라 사설 IP가 붙어 있다면, 그 위에 이미 모뎀 겸 공유기나 ISP 장비가 라우팅을 하고 있을 가능성이 큽니다. 이 상태에서 OPNsense에만 포워딩을 만들면 외부에서 안 들어옵니다.

    • 우선 확인: OPNsense WAN IP가 사설 대역인지 공인 IP인지
    • 다음 확인: 상위 장비가 브리지 모드인지, 아니면 별도 포워딩이 필요한지
    • 셸 확인: pfctl -s nat로 실제 로드된 NAT 규칙 확인
    • 로그 확인: 라이브 로그에서 생성한 규칙 라벨이 매치되는지 확인

    여기서 또 하나 많이 헷갈리는 게 헤어핀 NAT입니다. 집 안에서 공인 도메인으로 자기 서버에 접속하려는 경우죠. 외부에서는 되는데 내부에서는 안 되면, 서버가 죽은 게 아니라 NAT reflection이나 split DNS 설계 문제일 수 있습니다. 저는 reflection을 무조건 기본값처럼 기대하기보다, 내부에서는 내부 이름으로 접근하게 DNS를 정리하는 쪽이 더 편하더라고요.

    특정 기기만 이상하게 느릴 때

    이 부분은 네트워크 글에서 제일 뭉뚱그려 설명되지만 실제 원인은 몇 갈래로 나뉩니다.

    • 예전 DHCP 임대 정보: 새 게이트웨이 반영이 늦는 기기가 있습니다
    • MTU/MSS 문제: PPPoE나 특정 상위 장비 환경에서 일부 서비스만 유독 느릴 수 있습니다
    • DNS 재바인딩 보호나 로컬 DNS 충돌: 특정 내부 웹 UI 접속이 꼬일 수 있습니다
    • Wi-Fi 자체 문제: 라우팅이 아니라 무선 로밍이나 채널 혼잡일 수 있습니다

    여기서 중요한 건, 모든 느림을 OPNsense 탓으로 돌리면 안 된다는 점입니다. 실제로는 OPNsense로 옮긴 뒤 네트워크가 더 잘 보이기 때문에 원래 있던 무선 문제를 이제서야 발견하는 경우도 많습니다. 특정 기기만 느리면 저는 먼저 그 기기의 IP, 게이트웨이, DNS를 보고 그다음에 무선 연결 위치와 AP를 확인합니다.

    OPNsense 마이그레이션 트러블슈팅과 방화벽 구축 점검 장면

    관리 화면에서 규칙과 상태 정보를 확인하며 문제를 좁혀가는 과정의 이미지입니다.

    검증: OPNsense 마이그레이션이 끝났는지 판단하는 기준

    저는 웹 한 번 열렸다고 완료 판정하지 않습니다. 아래 체크리스트를 전부 통과해야 실제 전환 완료로 봅니다.

    1. 유선 클라이언트가 의도한 DHCP 범위에서 새 IP를 받는다
    2. 기본 게이트웨이가 OPNsense LAN 주소로 잡힌다
    3. 외부 IP 통신과 외부 DNS 조회가 모두 정상이다
    4. 내부 서비스(NAS, 프린터, 홈서버)가 IP와 이름 둘 다로 접근된다
    5. 포트포워딩 또는 VPN 진입이 의도한 서비스에만 열린다
    6. IoT와 게스트망이 메인 LAN이나 서버망에 임의 접근하지 못한다
    7. AP 관리 IP, 스위치 관리 IP 같은 운영 경로가 끊기지 않는다

    이 단계에서 저는 일부러 실패 테스트도 합니다. 예를 들어 IoT VLAN에서 NAS 관리 포트 접속을 시도해 보고, 막혀야 정상인지 확인하는 식입니다. 허용 테스트만 하면 분리 정책은 검증되지 않습니다.

    # 클라이언트 검증 예시
    ip route
    ping -c 2 1.1.1.1
    ping -c 2 google.com
    ping -c 2 nas.home.arpa
    nc -vz nas.home.arpa 445
    nc -vz opnsense.lan 443
    

    그리고 OPNsense 쪽에서는 규칙이 실제로 상태를 만들고 있는지 봅니다. 규칙 설명을 사람이 읽을 수 있게 써두면 라이브 로그와 상태 추적에서 시간이 꽤 절약됩니다. 이거 진짜 편하더라고요.

    OPNsense 마이그레이션 완료 후 홈 네트워크 업그레이드 결과 이미지

    게이트웨이 정상, DHCP 분배 정상, 세그먼트 분리가 완료된 상태를 시각적으로 보여주는 결과 이미지입니다.

    운영 팁: OPNsense 마이그레이션 직후 바로 해두면 편한 것

    • 구성 백업: 포트포워딩, VLAN, DHCP 정적 매핑을 바꿀 때마다 설정 백업을 남기세요.
    • 관리 문서 분리: VLAN ID, 서브넷, 게이트웨이, DHCP 범위, 고정 IP, AP 관리 주소를 표로 남기세요.
    • 규칙 이름 정리: “allow any” 같은 이름보다 목적이 드러나는 설명을 쓰세요.
    • 변경 단위 축소: DHCP를 바꾼 날에는 DNS까지 같이 손대지 않는 편이 좋습니다.
    • 내부 DNS 우선: 포트포워딩보다 먼저 내부 이름해결을 안정화하세요.

    추가로 홈랩에서는 성능보다도 되돌리기 쉬운 구조가 중요합니다. VLAN을 멋지게 쪼개는 것보다, 문제가 생겼을 때 10분 안에 원인을 좁힐 수 있는 구성이 더 낫습니다. 저는 그래서 초기에 자동 생성 규칙에 과하게 기대기보다, 나중에 봐도 이해되는 명시적 규칙을 선호합니다.

    이전 글에서 스위치 포트 태깅과 AP SSID 분리까지 정리해두셨다면 그 글도 함께 보세요. OPNsense만 제대로 세팅해도 네트워크가 자동으로 정리되진 않거든요. 병목은 늘 경계면에서 생깁니다. 방화벽과 AP 사이, DHCP와 DNS 사이, 상위 모뎀과 WAN 사이에서요.

    FAQ와 최종 권고

    소비자 공유기를 완전히 버려야 하나요?

    그럴 필요 없습니다. 무선 품질이 괜찮다면 AP로 계속 쓰는 게 가장 현실적입니다. 다만 라우팅, NAT, DHCP는 한 곳으로 모아야 합니다. 저는 보통 그 역할을 OPNsense에 몰아주는 쪽을 권합니다.

    작은 집 네트워크에도 필요할까요?

    기기 수가 적고 외부 공개 서비스도 없고 분리할 네트워크도 없다면 굳이 필요 없습니다. OPNsense는 만능 업그레이드라기보다 정책이 늘어난 환경을 위한 도구에 가깝습니다.

    어떤 경우에 특히 추천하나요?

    NAS, 홈서버, 원격 접속, IoT 분리, 포트포워딩 운영이 동시에 들어간다면 추천합니다. 반대로 와이파이만 안정적이면 된다면 기존 공유기를 유지하거나 좋은 AP를 추가하는 쪽이 더 낫습니다.

    제가 실제 운영 기준으로 내리는 결론은 명확합니다. 외부 공개 서비스가 있거나, 내부망을 둘 이상으로 나눠야 하거나, 장애 원인을 직접 추적해야 한다면 OPNsense로 가는 편이 맞습니다. 이 경우 소비자 공유기 업그레이드는 대개 임시 연장선에 그칩니다. 반대로 네트워크를 설계할 생각이 없고 관리 포인트를 늘리고 싶지 않다면 기존 공유기를 유지하세요. 그 편이 운영 품질이 더 높습니다.

    즉, 선택 기준은 장비 성능이 아니라 내가 감당해야 할 정책의 복잡도입니다. 그 복잡도가 이미 공유기 UI를 넘었다면, OPNsense 마이그레이션은 꽤 만족도 높은 홈 네트워크 업그레이드가 됩니다. 아직 그 단계가 아니라면 AP 재배치나 무선 분리만으로도 충분할 수 있습니다.

    소비자 공유기 대체와 OPNsense 선택 기준 요약 인포그래픽

    어떤 환경에서 기존 공유기를 유지하고, 어떤 경우 OPNsense로 넘어가면 좋은지 정리한 요약 이미지입니다.

  • [HomeLabs] Tailscale 가격 분석: 홈랩·소규모 비즈니스 비용 전략

    [HomeLabs] Tailscale 가격 분석: 홈랩·소규모 비즈니스 비용 전략

    Tailscale 가격 분석: 홈랩·소규모 비즈니스 비용 전략

    Tailscale 가격을 볼 때 가장 많이 헷갈리는 지점은 하나더라고요. 핵심은 “기기 수가 많으면 비싸지는가”가 아니라, “누가 seat를 점유하고 어떤 인프라를 tagged resource로 운영하느냐”입니다. 홈랩에선 거의 무료처럼 느껴지지만, 팀 계정·서브넷 라우터·exit node·CI 러너가 붙는 순간 계산 기준이 확 달라집니다. 이 글은 2026년 8월 28일 기준 Tailscale 공식 가격 페이지와 공식 문서를 바탕으로, 실제 설계할 때 먼저 보는 비용 포인트만 골라 다시 정리한 내용입니다.

    제 기준은 단순합니다. 사람 비용은 seat, 인프라 비용은 tagged resource, 변동성 비용은 ephemeral minutes로 나눠서 보는 거예요. 이렇게 분리하면 플랜 선택이 꽤 선명해집니다. 반대로 한데 섞어 보면 “무료로 되겠지” 하고 시작했다가 월말에 권한 설계와 청구 기준이 같이 꼬이기 쉽습니다. 특히 소규모 비즈니스 VPN은 구독료보다 권한 설계 실수와 seat 정리 실패가 더 비싸게 돌아오는 경우가 많습니다.

    홈랩 장비, 사용자 좌석(seat), tagged resources, exit node 구성을 한 장으로 요약한 개요 이미지입니다.

    Tailscale 가격 구조, 실제 청구 기준으로 보면 이렇게 읽힙니다

    현재 공식 요금제 기준으로 신규 가입 tailnet은 seat-based pricing을 따릅니다. 사용자 노트북이나 폰이 여러 대여도, 그 사람은 보통 seat 하나를 점유합니다. 반대로 서버, 서브넷 라우터, app connector, exit node처럼 사람이 아니라 tag가 소유하는 장비는 tagged resource로 계산합니다. 다만 여기서 한 가지는 꼭 짚고 가야 합니다. 일부 기존 legacy tailnet은 예전 월간 활성 사용자 방식이 남아 있을 수 있어서, 이미 오래 운영 중인 계정이라면 현재 플랜 체계를 먼저 확인하는 편이 안전합니다.

    실무에서 특히 중요한 건 seat가 실제로 tailnet에 참여한 사용자 기준으로 소비된다는 점입니다. 초대만 해둔 사용자는 seat를 점유하지 않고, 처음 로그인하거나 처음 장비를 붙일 때 seat를 차지합니다. 공식 FAQ 기준으로 seat는 재사용 가능하지만, 월중에 seat를 줄여도 그달 비용이 바로 내려가지는 않습니다. 반대로 seat를 추가하면 일할 계산이 붙습니다. 이 부분, 월말 정산 때 꽤 체감되더라고요.

    • User device: 사람 계정에 귀속된 노트북, 폰, 워크스테이션 같은 일반 단말입니다. 수량 자체는 과금 기준이 아닙니다.
    • Seat: tailnet에 참여한 사용자가 차지하는 청구 단위입니다. 비용 최적화의 첫 번째 축입니다.
    • Tagged resource: tag가 소유하는 서버, subnet router, exit node, app connector 같은 공유 인프라입니다. 기본 50개가 포함되고, 추가분은 개당 월 1달러입니다.
    • Ephemeral resource: 짧게 뜨고 사라지는 CI/CD 러너, 쿠버네티스 워크로드 같은 장비입니다. 분 단위 풀을 쓰고, tailnet에 4시간 이상 남아 있으면 일반 tagged resource로 계산됩니다.

    Tailscale 가격 비교: 플랜표만 보면 놓치기 쉬운 판단 포인트

    플랜 공식 가격 사용자 ACL Groups Ephemeral resources 제가 보는 적합한 상황
    Personal $0 최대 6명 최대 3개 월 1,000분 비상업용 홈랩, 개인 장비 연결, 가족 단위 원격 접속
    Standard 사용자당 월 $8 무제한 최대 10개 월 1,000분 작은 팀, 업무용 계정 운영, SCIM/MDM, 역할 분리 시작점
    Premium 사용자당 월 $18 무제한 최대 300개 월 10,000분 감사 로그, log streaming, advanced Tailscale SSH, JIT access가 필요한 조직
    Enterprise Custom 맞춤 맞춤 맞춤 대규모 조직, 맞춤 계약, 다중 tailnet이나 확장 기능 협의가 필요한 경우

    플랜표에서 진짜 중요한 건 가격표 숫자보다 무료 한계가 어디서 실무 한계로 바뀌느냐입니다. Personal은 최대 6명까지 무료이고 tagged resource 50개도 포함이라 홈랩 기준으로 꽤 넉넉합니다. 그런데 작은 회사 프로젝트에 그대로 가져가면 얘기가 달라집니다. 공식 문서 기준으로 Personal은 commercial use를 전제로 한 플랜이 아니기 때문입니다. 성능보다 소유권과 운영 정책에서 먼저 막히는 셈이죠.

    또 하나, tagged resource 50개 포함은 넉넉해 보여도 소규모 비즈니스에선 금방 닿을 수 있습니다. 사무실 subnet router, 재택 근무용 exit node, NAS, 백업 서버, staging VM, 모니터링 박스, app connector, 고정 러너 노드를 분리해 두면 생각보다 빨리 늘어납니다. 사람 수보다 인프라 역할 수가 더 빨리 커지는 팀이라면 seat보다 tag 구조를 먼저 계산하는 편이 맞습니다. 이 차이를 놓치면 Tailscale 비용이 예상보다 빨리 불어나더라고요.

    홈랩 VPN에서 Tailscale 비용 줄이는 방법

    홈랩은 대체로 Personal 플랜에서 오래 버틸 수 있습니다. 그렇다고 아무렇게나 써도 된다는 뜻은 아니에요. 오히려 무료를 오래 유지하려면 사람 계정은 최소화하고, 공유 인프라는 역할이 겹치지 않게 묶고, 태그는 접근 제어 때문에 꼭 필요할 때만 늘리는 편이 좋습니다. 이거 생각보다 차이가 큽니다.

    홈랩에서 먼저 줄여야 할 건 tagged resource의 과도한 분해입니다. NAS, Proxmox 호스트, 리버스 프록시, 미디어 서버, 백업 서버를 전부 다른 태그와 정책으로 잘게 나누면 처음엔 있어 보입니다. 그런데 운영이 길어질수록 관리 포인트만 늘어나더라고요. 홈랩에선 복잡한 보안 모델보다, 나중에 내가 이해할 수 있는 구조가 더 중요할 때가 많습니다.

    • 개인 사용자 1~3명 중심이면 Personal 유지가 보통 맞습니다.
    • 공유 인프라는 tag 하나로 시작하고, ACL 분리가 실제로 필요할 때만 세분화하는 편이 낫습니다.
    • 서브넷 라우터는 대역 전체를 무심코 광고하지 말고, 필요한 세그먼트만 내보내는 편이 안전합니다.
    • exit node는 1대 대표 노드로 먼저 검증하고, 가용성이나 지역 분산 요구가 생길 때 늘리는 편이 비용과 운영 모두 안정적입니다.

    예를 들어 사용자 2명, 노트북 3대, 모바일 2대, NAS 1대, 미니 PC 1대, subnet router 1대, exit node 1대 정도라면 Personal 안에서 충분히 정리되는 경우가 많습니다. 여기서 비용이 새기 시작하는 건 디바이스 수 자체가 아닙니다. 가족이나 지인을 정식 사용자로 계속 추가하거나, 서버 역할마다 태그를 쪼개서 공유 인프라 수를 불리는 순간부터 계산이 달라집니다.

    Tailscale 가격 최적화를 위한 홈랩 VPN 단일 exit node 구성 이미지

    무료 또는 저비용으로 유지하기 좋은 홈랩 구성 예시입니다. 사용자 수는 적게, 공유 인프라는 단순하게 가져가는 그림입니다.

    소규모 비즈니스 VPN에서 Tailscale 가격, 언제 Standard가 맞을까요?

    팀 운영은 기준이 다릅니다. 여기서는 무료냐 유료냐보다 누가 계정을 만들고, 누가 회수하고, 누가 승인하는가가 먼저예요. 구성원 수가 6명을 넘기기 시작하거나, 업무용 계정과 개인 계정을 섞어 쓰기 싫거나, 입사·퇴사 흐름을 IdP나 SCIM으로 묶고 싶다면 Personal을 오래 붙잡을 이유가 거의 없습니다. 이때부터 Standard는 단순한 비용 증가라기보다 운영 통제 비용을 선제적으로 지불하는 선택에 가깝습니다.

    특히 Standard의 가치는 seat 가격 8달러 자체보다, 권한 구조를 사람 손으로 덜 만지게 해주는 점에 있습니다. 홈랩에서는 seat 하나가 사용자 1명으로 끝나지만, 팀 환경에서는 seat 하나가 곧 라이프사이클 관리 단위가 됩니다. 이게 정리되지 않으면 퇴사자 계정, 외주 계정, 테스트 계정이 tailnet 안에 남습니다. 청구서보다 보안 쪽이 먼저 문제 되기 쉽습니다.

    상황 추천 플랜 이유 굳이 올리지 말아도 되는 경우
    개인 홈랩, 비상업용, 사용자 6명 이하 Personal 무료 범위가 넓고 user device 수가 과금 기준이 아님 태그·그룹 설계를 과도하게 복잡하게 만들지 않는다면 충분합니다
    작은 팀, 업무용 계정 운영, SCIM/MDM 필요 Standard seat 기반 관리와 역할 분리, 상업적 사용 맥락에 더 잘 맞음 감사 로그·흐름 로그·JIT 접근이 아직 필요 없다면 Premium까지는 과한 경우가 많습니다
    접속 기록 보존, 외부 SIEM 연동, 세밀한 SSH 통제 필요 Premium network flow logs, log streaming, advanced Tailscale SSH, JIT access가 포함됨 원격 접속과 내부 웹 접근이 주된 목적이라면 비용 대비 체감이 낮을 수 있습니다

    실무에서 자주 던지는 질문은 딱 두 개입니다. “퇴사자 seat를 그날 바로 비울 수 있는가?”, “사고가 나면 누가 언제 어디에 붙었는지 남겨야 하는가?” 첫 번째에 예라고 답해야 하면 Standard, 두 번째에 예라고 답해야 하면 Premium 검토가 빨라집니다.

    Tailscale 비용 최적화 구현: 최소 구성부터 확인해보겠습니다

    비용 최적화는 대개 노드를 덜 사는 것보다 역할을 덜 쪼개는 것에서 시작합니다. 홈랩이든 작은 팀이든 먼저 대표 노드 1대로 subnet router와 exit node를 함께 맡길 수 있는지 확인해보세요. 되면 tagged resource 수, 승인 포인트, 장애 추적 포인트가 같이 줄어듭니다. 단순한 구성이 진짜 오래 갑니다.

    여기서 중요한 건 정책 파일과 CLI를 같이 설계하는 겁니다. 태그를 붙이려면 tagOwners가 있어야 하고, subnet route나 exit node를 수동 승인 없이 운영하려면 autoApprovers가 필요합니다. 이걸 안 잡아두면 노드는 살아 있는데 승인 대기 상태로 남는 경우가 생깁니다. 처음엔 멀쩡해 보여도 나중에 꼭 한 번 발목을 잡더라고요.

    {
      "tagOwners": {
        "tag:infra": ["group:netops"]
      },
      "groups": {
        "group:netops": ["[email protected]", "[email protected]"]
      },
      "autoApprovers": {
        "routes": {
          "192.168.10.0/24": ["tag:infra"]
        },
        "exitNode": ["tag:infra"]
      }
    }

    이 구성의 장점은 분명합니다. 사람 계정이 바뀌어도 tag가 승인 주체로 남기 때문에, 담당자 교체나 사용자 비활성화 때문에 route 광고가 갑자기 끊길 가능성을 줄일 수 있습니다. 공식 정책 문서도 이 시나리오에선 tag 기반 auto-approver를 고려하라고 안내합니다. 소규모 팀에서 특히 편하더라고요.

    sudo tailscale up \
      --advertise-tags=tag:infra \
      --advertise-routes=192.168.10.0/24 \
      --advertise-exit-node \
      --accept-dns=false

    명령 자체는 간단하지만, 해석 포인트는 세 가지입니다.

    1. 태그를 먼저 설계하고 노드에 붙이세요. 태그는 단순 라벨이 아니라 그 노드의 정체성입니다.
    2. 광고할 대역을 최소화하세요. 192.168.10.0/24처럼 필요한 CIDR만 지정하는 편이 낫습니다.
    3. tailscale up은 이전 플래그를 기억하지 않는다고 보고 항상 전체 플래그를 다시 적는 습관이 안전합니다. 공식 CLI 문서도 실행할 때마다 필요한 플래그를 모두 지정하라고 안내합니다.

    현장에서 많이 보는 실패 모드는 이겁니다. 처음엔 --advertise-tags만 넣고, 며칠 뒤엔 --advertise-routes만 넣는 식이죠. 운영자는 설정을 추가했다고 생각하지만, 실제로는 이전 의도를 빠뜨린 상태로 다시 올린 셈이 됩니다. 요즘 CLI가 경고를 주긴 하지만, 자동화 스크립트에선 여전히 전체 플래그를 명시하는 편이 더 안전합니다.

    서브넷 라우터 설정이나 Tailscale SSH 운영이 궁금하시면 관련 글도 같이 보시면 흐름이 더 잘 잡힙니다.

    Tailscale 가격과 tagged resource 관리 흐름을 보여주는 설정 화면 이미지

    tagged resource, advertised routes, exit node 승인 흐름을 이해하기 쉽게 보여주는 구성 이미지입니다.

    검증과 결과 해석: Tailscale 비용 오판을 막는 체크 포인트

    비용 최적화에서 검증이 중요한 이유는 단순합니다. 느리다고 해서 플랜 업그레이드가 답은 아니고, 노드를 더 만든다고 해서 경로가 좋아지는 것도 아니기 때문입니다. 먼저 direct 연결이 되는지, DERP relay에 오래 머무는지, NAT가 까다로운지부터 읽어야 합니다. 그래야 불필요한 exit node 추가나 tagged resource 증설을 막을 수 있습니다.

    tailscale status
    
    tailscale status --json
    
    tailscale netcheck
    
    tailscale ping nas
    
    tailscale ping --tsmp nas

    읽는 기준은 보통 이렇게 잡습니다.

    • tailscale status: 공유 노드가 계속 비슷한 상태로만 남는다면, 실제로 쓰지 않는 tagged resource인지 먼저 확인합니다.
    • status –json: 월말 점검은 사람 눈보다 자동화가 낫습니다. 온라인 여부나 피어 상태를 정리해 보면 안 쓰는 인프라가 드러납니다.
    • netcheck: UDP: false면 direct 연결 기대치를 낮추고, 방화벽이나 사내망 정책부터 의심하는 편이 맞습니다.
    • MappingVariesByDestIP: true면 hard NAT 가능성이 높습니다. 이 경우 플랜 문제가 아니라 네트워크 환경 문제일 가능성이 큽니다.
    • PortMapping이 비어 있거나 false면 라우터가 포트 매핑을 못 도와주는 상태일 수 있습니다. 홈랩에선 이게 은근한 병목입니다.
    • tailscale ping: 처음엔 DERP를 타다가 direct로 붙는 건 자연스러운 흐름입니다. 계속 DERP만 유지되면 NAT나 UDP 경로를 다시 봐야 합니다.
    • tailscale ping –tsmp: OS의 ICMP 제약을 우회해서 Tailscale 경로 자체를 보기 좋습니다. 원인 분리에 꽤 유용합니다.

    비용 관점에서 꼭 덧붙이고 싶은 해석이 하나 있습니다. DERP relay를 쓴다고 바로 Premium이 필요한 건 아닙니다. Premium은 로그와 통제 요구에 대한 답이지, NAT 문제를 요금제로 해결하는 플랜은 아닙니다. 연결이 느린데 곧장 플랜을 올리는 판단은 의외로 헛돈이 되기 쉽습니다.

    Tailscale 가격 분석과 함께 보는 direct 연결 및 DERP relay 검증 이미지

    direct 연결과 DERP relay 상태 차이를 한눈에 볼 수 있는 검증 결과 시각화입니다.

    자주 겪는 함정과 트러블슈팅

    여러 번 본 패턴을 압축하면, 문제는 기능 부족보다 정체성 관리가 섞일 때 생깁니다. 사람 장비에 태그를 붙여 사용자처럼 쓰려 하거나, 오래 살아 있는 서버를 ephemeral처럼 취급하거나, 상업용 tailnet을 무료 플랜 감각으로 운영하면 나중에 정리가 정말 어려워집니다. 초반엔 편해 보여도 뒤로 갈수록 비용과 운영이 같이 무거워집니다.

    • 함정 1: 무료 플랜으로 업무용 운영을 길게 끌기. 근본 원인은 비용 절감이 아니라 계정 소유권과 상업적 사용 정책 정리를 미루는 데 있습니다.
    • 함정 2: tagged resource를 서버 수만큼 잘게 쪼개기. 근본 원인은 보안 모델 과설계입니다. 태그 수가 늘면 ACL, autoApprovers, 운영 인수인계가 같이 복잡해집니다.
    • 함정 3: ephemeral resource를 일반 서버처럼 오래 띄우기. 공식 문서 기준으로 4시간 이상 존재하면 일반 tagged resource로 계산됩니다.
    • 함정 4: seat를 비우지 않고 사람만 교체하기. 근본 원인은 IdP deprovision 미흡입니다. seat 재사용은 가능하지만 계정 비활성화가 실제로 돌아가야 의미가 있습니다.
    • 함정 5: route auto-approval 없이 subnet router를 여러 대 늘리기. 승인 절차를 콘솔 수작업에 의존하면 운영자 부재 시 라우팅 장애가 납니다.
    • 함정 6: 사람 장비에 태그를 붙여 권한을 흉내 내기. 공식 문서 기준으로 태그를 적용하면 사용자 기반 인증이 제거됩니다. 태그는 사람용 메타데이터가 아니라 머신 정체성입니다.

    월말 점검은 저는 꽤 기계적으로 합니다.

    1. seat: 실제 로그인한 사용자 수와 현재 구매 seat 수가 맞는지 확인합니다.
    2. tagged resource: 더 이상 쓰지 않는 subnet router, exit node, app connector, 고정 서버가 남아 있지 않은지 봅니다.
    3. ephemeral usage: CI/CD나 쿠버네티스 워크로드가 4시간 이상 tailnet에 잔류하지 않는지 확인합니다.
    4. relay 상태: direct로 바꿀 수 있는 연결인데 DERP에만 머무는 장비가 없는지 봅니다. 이건 비용보다 체감 품질 문제를 줄이는 데 효과적입니다.

    FAQ: Tailscale 가격에서 많이 헷갈리는 포인트

    Tailscale 비용은 기기 수가 많으면 바로 올라가나요?

    보통은 아닙니다. 현재 공식 요금제 기준으로 핵심은 seat 수입니다. 다만 서버·subnet router·exit node처럼 tag가 붙은 공유 인프라는 tagged resource로 따로 관리해야 합니다.

    홈랩 VPN 용도라면 무료 플랜으로 충분한가요?

    대부분은 충분합니다. 사용자 6명 이내, 비상업용, tagged resource 50개 안쪽이면 Personal이 꽤 넉넉합니다. 다만 태그와 라우팅을 과하게 쪼개면 무료라도 운영 난도는 확 올라갑니다.

    소규모 비즈니스 VPN은 언제 Premium까지 가야 하나요?

    보안 감사, 접속 흐름 추적, 외부 로그 적재, 고급 SSH 통제, JIT access가 필요할 때입니다. 단순 원격 접속과 내부 웹 접근 위주라면 Standard에서 끝나는 팀도 많습니다.

    ephemeral minutes가 부족하면 무조건 Premium으로 올려야 하나요?

    그 전에 먼저 워크로드 수명을 보셔야 합니다. 짧게 떠야 할 러너가 오래 남아 있다면 플랜 문제보다 설계 문제일 가능성이 큽니다. 4시간 이상 존재한 장비는 일반 tagged resource로 계산된다는 점도 같이 보셔야 합니다.

    마무리: 저는 이렇게 고르라고 말씀드립니다

    개인 홈랩이라면 Personal부터 시작하시면 됩니다. 사용자 6명 이내, 비상업용, 공유 인프라 수가 많지 않다면 구조를 단순하게 유지하는 쪽이 이득입니다. 새 노드를 추가하기 전에 대표 subnet router 1대와 exit node 1대로 충분한지 먼저 검증해보세요. 무료 유지의 핵심은 기기 수보다 역할 수를 늘리지 않는 것입니다.

    작은 팀이라면 Standard를 출발점으로 보는 게 맞습니다. 업무용 계정 수명주기, 역할 분리, SCIM/MDM이 필요하면 여기서부터가 운영적으로 안정적입니다. 그리고 누가 언제 어디에 접근했는지 남겨야 하는 조직이면 Premium 검토를 미루지 않는 편이 낫습니다. 그 구간에선 요금보다 사고 대응 비용이 더 크게 돌아오거든요.

    한 줄로 정리하면 이렇습니다. 사람이 적고 구조가 단순하면 Personal, 계정 운영이 시작되면 Standard, 감사와 추적이 필수면 Premium. Tailscale 가격은 복잡해 보여도 seat·tagged resource·ephemeral minutes를 따로 보면 의외로 답이 선명합니다.

    Tailscale 가격 기준으로 홈랩과 소규모 비즈니스 플랜 선택을 요약한 이미지

    Personal, Standard, Premium 중 어떤 상황에서 무엇을 고르면 되는지 빠르게 판단할 수 있는 요약 이미지입니다.

  • [3D Printer] 3D 프린팅 프로젝트로 스마트 홈 DIY 기기 만드는 법

    [3D Printer] 3D 프린팅 프로젝트로 스마트 홈 DIY 기기 만드는 법

    [메이커] 3D 프린팅 프로젝트로 스마트 홈 DIY 기기 만드는 법

    3D 프린팅 프로젝트를 처음 시작할 때 많은 분들이 출력 품질부터 보시는데요, 집에서 오래 돌릴 스마트 홈 장치는 기준이 조금 다릅니다. 외형이 예쁜 것과 운영이 편한 건 꽤 다르더라고요. 저도 홈랩에서 온습도 노드, 도어 상태 센서, 전원 모니터링 박스를 여러 번 만들면서 느낀 게 하나 있습니다. 실패 원인은 펌웨어만이 아니라, 의외로 기구 설계와 조립성에서 자주 터집니다. 버튼 홀이 0.4~0.6mm만 타이트해도 스위치가 눌린 채로 붙고, USB 커넥터 뒤 공간이 부족하면 케이블 장력 때문에 재부팅이 나기도 합니다.

    이번 글은 책상 위 데모가 아니라, 벽에 붙여 두고 몇 달 동안 손 덜 가게 굴릴 수 있는 스마트 홈 노드를 기준으로 정리했습니다. 예시는 ESP32 기반 보드, 온습도 센서, 상태 LED, 물리 버튼, USB 전원, MQTT 연동 조합입니다. 핵심은 네 가지예요. 설치 위치를 먼저 정하고, 열과 공기 흐름을 분리하고, 나중에 다시 열 수 있게 만들고, 출력 전에 가조립 기준으로 검증하는 것. 이 순서만 지켜도 시행착오가 확 줄어듭니다.

    3D 프린팅 프로젝트 기반 스마트 홈 DIY 기기 아키텍처 이미지

    ESP 기반 센서 노드, MQTT 브로커, 홈 오토메이션 서버, 3D 프린팅 케이스의 연결 구조를 한눈에 보여주는 개요 이미지입니다.

    왜 3D 프린팅 프로젝트가 스마트 홈 DIY와 잘 맞는가

    3D 프린터의 진짜 가치는 케이스를 예쁘게 만드는 데만 있지 않습니다. 스마트 홈 DIY에서는 보드, 센서, 배선, 고정 구조, 유지보수 동선까지 한 번에 통제할 수 있다는 점이 더 중요하거든요. 시중 플라스틱 박스에 구멍을 뚫는 방식은 빠르긴 한데, 센서 위치와 통풍 경로를 설계하기 어렵고 한 번 삐끗하면 다음 장치를 똑같이 재현하기도 힘듭니다.

    제가 실제 3D 프린팅 프로젝트를 하면서 체감한 장점은 이런 쪽이었습니다.

    • 센서가 읽어야 할 공기와 보드가 내는 열을 분리하기 쉽습니다.
    • 케이블이 꺾이는 지점을 설계 단계에서 줄일 수 있습니다.
    • 벽걸이 브라켓, 키홀, 자석 자리, 나사 기둥을 한 몸으로 넣기 편합니다.
    • 펌웨어 재플래시, 버튼 테스트, 분해 청소를 고려한 구조를 초기에 반영할 수 있습니다.
    • 무엇보다 같은 설계로 두 번째, 세 번째 장치를 복제하기 쉬워집니다.

    반대로 이 장점을 못 살리면 출력물은 멀쩡한데 장치는 불안정해집니다. 그래서 저는 겉모습보다 운영성을 먼저 보게 되더라고요.

    3D 프린팅 프로젝트 시작 전에 먼저 정해야 할 세 가지

    부품 쇼핑부터 시작하면 거의 항상 한 번은 되돌아오게 됩니다. 실제로는 아래 세 가지를 먼저 정하는 편이 훨씬 낫습니다.

    1. 설치 위치: 책상 위, 벽면, 금속 배전함 옆, 창가 근처는 요구사항이 완전히 다릅니다.
    2. 전원 방식: USB 전원은 디버깅이 쉽고, 배터리는 설치 자유도가 높지만 절전 설계가 따라옵니다.
    3. 개방 방식: 자주 열 장치인지, 한 번 닫고 오래 둘 장치인지에 따라 나사 체결과 스냅핏 판단이 달라집니다.

    저는 벽걸이형 센서 노드라면 보통 ESP32 + 외부 노출형 센서 위치 + 상태 LED + 리셋 또는 사용자 버튼 + 나사 체결식 하우징으로 시작합니다. 첫 장치에서는 외관보다 원인 추적 속도가 중요해서예요. 스마트 홈 쪽은 문제가 생기면 펌웨어, Wi-Fi, 브로커, 전원, 센서, 물리 간섭을 다 의심해야 해서 최소한 기구 쪽은 빨리 열어볼 수 있어야 편합니다.

    결정 항목 A를 고를 때 B를 고를 때 실무 판단
    케이스 결합 나사 체결 스냅핏 자주 열거나 첫 프로토타입이면 나사 체결이 편하고, 최종 외관 우선이며 내부 접근 빈도가 낮으면 스냅핏도 괜찮습니다.
    출력 재질 PLA PETG 실내용 일반 노드는 PLA로 시작해도 충분한 경우가 많고, 열기·습기·직사광선 영향이 걱정되면 PETG가 더 안전합니다.
    전원 방식 USB 배터리 첫 장치와 디버깅 중심 프로젝트는 USB, 설치 위치 자유도와 배선 최소화가 목표면 배터리를 검토하되 절전 설계를 따로 잡아야 합니다.
    센서 노출 격자형 통풍 홀 측면 슬릿 측정 안정성이 우선이면 격자형이 무난하고, 외관 통일감이 중요하면 측면 슬릿도 좋지만 내부 공기 정체가 없는지 꼭 확인해야 합니다.
    벽면 고정 양면테이프 브라켓 또는 나사 가벼운 장치 임시 설치는 테이프, 장기 설치나 유지보수 반복 가능성이 있으면 분리 가능한 브라켓 쪽이 낫습니다.

    제 경험상 후회가 가장 적은 선택은 첫 번째 장치를 USB 전원 + 나사 체결 + 통풍 우선 설계로 가는 방식이었습니다. 보기엔 조금 덜 세련돼도 문제를 빨리 잡을 수 있거든요. 반대로 스냅핏과 배터리를 처음부터 같이 가져가면 기구 공차, 전력 관리, 슬립 복귀, 배터리 교체성까지 한 번에 얽혀서 난도가 확 올라갑니다.

    3D 프린팅 프로젝트 설계 개념: 케이스는 예쁘게보다 공기가 흐르게

    온습도 노드처럼 환경을 읽는 장치는 케이스가 측정값에 과하게 개입하면 안 됩니다. 그런데 실제로는 케이스가 제일 많이 개입하더라고요. MCU, 레귤레이터, USB 전원부, LED가 내는 열이 작은 하우징 안에 갇히면 센서가 방 온도보다 내부 온도를 더 잘 읽게 됩니다. 그래서 저는 센서 박스를 설계할 때 아래 원칙을 거의 고정으로 씁니다.

    • 센서 주변에는 막힌 장식 면보다 공기 통로를 먼저 만듭니다.
    • 센서와 MCU 전원부는 가능하면 같은 평면에 바짝 붙이지 않고 거리나 칸막이를 둡니다.
    • USB 커넥터 쪽에는 케이블 헤드와 굴곡 공간을 남깁니다.
    • 버튼은 누르는 감보다 먼저 상시 간섭이 없는지를 확인합니다.
    • 나사 기둥은 강도보다 먼저 홀 정렬과 체결 접근성을 봅니다.

    재현 가능한 시나리오를 하나 말씀드리면요. 벽면 부착형 노드에서 전면 중앙에 센서 홀을 예쁘게 뚫고, 뒤쪽 바로 안쪽에 ESP 보드와 전원부를 붙여 넣은 적이 있었습니다. 책상 위에서 10분 돌릴 때는 멀쩡했는데, 벽에 붙이고 MQTT 로그를 길게 보니 값이 느리게 올라갔다 내려오기를 반복하더라고요. 센서가 방 공기를 읽는 게 아니라 케이스 안에서 데워졌다 식는 공기층을 읽고 있었던 겁니다. 그 뒤부터는 센서 위치를 디자인 중심이 아니라 공기 흐름 중심으로 배치합니다. 눈에 확 띄는 변화는 아니어도 결과 차이는 꽤 큽니다.

    또 하나, 출력 강도를 과하게 올리면 무조건 좋은 줄 아시는 분들이 많은데 스마트 홈 노드는 꼭 그렇지 않습니다. 외벽을 두껍게 잡고 내부를 꽉 채우면 단단해 보이긴 해도 센서 박스에서는 통풍과 배선 여유를 해칠 수 있습니다. 이럴 땐 전체를 무겁게 만들기보다 하중이 걸리는 부분만 보강하는 편이 낫습니다. 예를 들면 브라켓 체결부, 나사 기둥, 키홀 둘레만 보강하고 센서 챔버는 가볍게 가는 식이죠.

    실전 구현 1: MQTT 브로커를 먼저 정상화하기

    장치가 안 붙는 상황에서 펌웨어부터 뒤집어보는 경우가 많은데요, 저는 반대로 갑니다. 브로커가 듣고 있는지, 클라이언트가 붙을 수 있는지, 토픽이 보이는지를 먼저 확인합니다. 스마트 홈 DIY에서 시간을 아끼는 습관 중 하나예요.

    아래 예시는 Mosquitto 브로커를 Docker Compose로 띄우고, 최소 설정 파일까지 만들어 바로 확인하는 흐름입니다. 다만 <code>allow_anonymous true는 로컬 테스트용으로만 쓰고, 외부 접근 환경에서는 인증 설정을 꼭 추가하는 편이 안전합니다.

    mkdir -p ~/homelab/mosquitto/{config,data,log}
    cd ~/homelab/mosquitto
    
    cat > config/mosquitto.conf <<'EOF'
    persistence true
    persistence_location /mosquitto/data/
    log_dest stdout
    listener 1883
    allow_anonymous true
    EOF
    
    cat > docker-compose.yml <<'EOF'
    services:
      mosquitto:
        image: eclipse-mosquitto:2
        container_name: mosquitto
        ports:
          - "1883:1883"
        volumes:
          - ./config:/mosquitto/config
          - ./data:/mosquitto/data
          - ./log:/mosquitto/log
        restart: unless-stopped
    EOF
    
    docker compose up -d
    docker compose ps
    docker compose logs --tail=50 mosquitto

    여기서 중요한 건 단순히 컨테이너가 떠 있느냐가 아닙니다. docker compose ps가 Up이어도 설정 경로나 포트 노출이 기대와 다를 수 있거든요. 그래서 저는 아래처럼 포트 확인과 publish/subscribe 테스트를 바로 붙여서 봅니다.

    ss -lnt | grep ':1883'
    mosquitto_sub -h 127.0.0.1 -t 'homelab/test/#' -C 1 -v &
    SUB_PID=$!
    sleep 1
    mosquitto_pub -h 127.0.0.1 -t 'homelab/test/ping' -m 'broker-ok'
    wait "$SUB_PID"

    로그 해석 기준도 같이 잡아두면 편합니다.

    • ss -lnt에서 :1883가 안 보이면 브로커 기동이나 포트 바인딩 문제를 먼저 봅니다.
    • mosquitto_pub는 성공하는데 mosquitto_sub에서 안 보이면 토픽 오타나 셸 따옴표 문제를 의심합니다.
    • 장치에서는 실패하고 로컬 publish/subscribe는 되면 네트워크 경로, 방화벽, 브로커 주소, 인증 설정 쪽으로 범위를 줄일 수 있습니다.
    • 브로커 로그에 연결과 끊김이 반복되면 장치 재부팅이나 keepalive 관련 증상을 함께 의심해볼 만합니다.

    현장에서는 여기서 이미 절반이 갈립니다. 브로커를 먼저 검증해두면 나중에 장치가 안 붙을 때 “네트워크냐 펌웨어냐”를 훨씬 빨리 잘라낼 수 있습니다. 브로커 구성을 더 깊게 다루는 글과 ESPHome 자동화 글을 내부 링크로 이어두면 블로그 흐름도 좋아집니다.

    3D 프린터 활용 스마트 홈 DIY MQTT 연결 구성도

    센서 노드가 Wi-Fi를 통해 MQTT 브로커와 연결되고, 홈 오토메이션 서버가 이를 구독하는 흐름을 설명하는 구성 이미지입니다.

    실전 구현 2: ESPHome 설정은 하드웨어 배치와 같이 봐야 합니다

    ESPHome의 장점은 YAML이 단순해서가 아니라, 핀 배치와 동작 의도를 파일에 남기기 쉽다는 점입니다. 같은 케이스를 두 대 더 만들 때 특히 편합니다. 다만 여기서 흔한 실수가 하나 있습니다. YAML만 맞으면 된다고 생각하고 실제 하드웨어 배치를 파일과 따로 보는 겁니다. 스마트 홈 노드는 그러면 꼭 어긋나더라고요.

    아래 예시는 MQTT, 상태 LED, 버튼, 센서 업데이트 주기를 명시한 기본형입니다.

    esphome:
      name: room-sensor-node
      friendly_name: Room Sensor Node
    
    esp32:
      board: esp32dev
    
    logger:
    ota:
    
    wifi:
      ssid: "YOUR_WIFI_SSID"
      password: "YOUR_WIFI_PASSWORD"
      ap:
        ssid: "room-sensor-fallback"
        password: "CHANGE_ME"
    
    mqtt:
      broker: 192.168.0.10
      topic_prefix: homelab/room_sensor
      birth_message:
        topic: homelab/room_sensor/status
        payload: online
      will_message:
        topic: homelab/room_sensor/status
        payload: offline
    
    sensor:
      - platform: dht
        pin: GPIO4
        model: DHT22
        temperature:
          name: "Room Temperature"
        humidity:
          name: "Room Humidity"
        update_interval: 30s
    
    binary_sensor:
      - platform: gpio
        pin:
          number: GPIO0
          mode: INPUT_PULLUP
          inverted: true
        name: "Room Button"
    
    status_led:
      pin:
        number: GPIO2
        inverted: true

    여기서 제가 꼭 같이 보는 항목은 세 가지입니다.

    • 센서 핀 위치: 케이스의 통풍 홀 방향과 맞는지.
    • 상태 LED 위치: 그대로 노출할지, 얇은 확산창 뒤에 둘지.
    • 버튼 위치: 손가락 접근성과 내부 스위치 간섭을 함께 만족하는지.

    특히 버튼은 GPIO 설정보다 기구 간섭이 더 흔한 문제입니다. 입력 풀업과 반전 설정이 맞아도 버튼 캡이 너무 길면 항상 눌린 상태가 됩니다. 저는 조립 전에 아래처럼 케이스를 닫지 않은 상태에서 먼저 이벤트가 정상인지 확인합니다.

    mosquitto_sub -h 192.168.0.10 -t 'homelab/room_sensor/#' -v

    이 상태에서 버튼을 눌렀을 때만 이벤트가 변해야 합니다. 뚜껑을 덮는 순간 상태가 바뀌면 소프트웨어보다 기구 문제일 가능성이 훨씬 큽니다.

    출력 전 체크리스트도 실무적으로 정리하면 이렇습니다.

    1. 보드 실측과 CAD 치수를 대조합니다. 데이터시트보다 실제 보드 편차가 더 문제인 경우가 있습니다.
    2. USB 커넥터뿐 아니라 케이블 헤드 두께와 꺾임 방향까지 봅니다.
    3. 나사 기둥이 PCB를 떠받치는지, 반대로 PCB를 휘게 만드는지 확인합니다.
    4. 센서 통풍 홀 주변에 서포트 제거가 어려운 형상이 없는지 봅니다.
    5. 벽걸이 키홀이나 브라켓이 조립 후에도 접근 가능한지 확인합니다.
    6. 뚜껑을 닫았을 때 안테나 주변이 완전히 막히지 않는지 봅니다.

    포인트는 “출력 가능하냐”보다 “조립 후 다시 열 수 있냐”입니다. 첫 출력에서 완성형을 노리기보다, 가조립용 얇은 프로토타입으로 치수와 간섭만 먼저 확인하는 편이 전체 시간을 줄여줍니다. 이거 진짜 편하더라고요.

    실전 구현 3: 출력 후 검증은 물리, 네트워크, 데이터 순으로

    조립이 끝났다고 바로 벽에 붙이면 디버깅이 길어집니다. 저는 항상 물리적 간섭 → 네트워크 연결 → 데이터 안정성 순서로 봅니다. 이 순서를 바꾸면 로그는 복잡한데 원인은 단순한 상황을 놓치기 쉽거든요.

    ping -c 4 192.168.0.50
    mosquitto_sub -h 192.168.0.10 -t 'homelab/room_sensor/#' -v
    docker compose logs --tail=100 mosquitto

    각 명령어는 보는 포인트가 다릅니다.

    • ping이 흔들리면 장치 불량보다 설치 위치, 전원 불안정, Wi-Fi 환경을 먼저 의심합니다.
    • mosquitto_sub는 메시지 존재 여부뿐 아니라 간격이 일정한지를 봐야 합니다. 값이 오긴 오는데 주기가 불안정하면 전원이나 재접속 증상일 수 있습니다.
    • docker compose logs는 브로커 연결과 끊김 패턴을 빠르게 확인할 때 편합니다. Docker 엔진 자체 로그가 systemd로 수집되는 환경이라면 journalctl -u docker를 추가로 볼 수도 있습니다.

    실무적으로 자주 맞닥뜨리는 패턴은 이런 식입니다.

    • 재부팅 직후 몇 분간 정상인데 시간이 지나면 값이 뜨는 경우: 발열 축적 또는 케이스 내부 압박 가능성이 큽니다.
    • 책상 위에서는 정상인데 벽에 붙이면 끊기는 경우: 안테나 방향, 벽 재질, 주변 금속 구조물 영향을 먼저 봐야 합니다.
    • 온도는 안정적인데 습도만 튀는 경우: 센서 위치는 통풍되지만 내부 공기 웅덩이가 생기는 구조일 수 있습니다.
    • 값은 오는데 버튼 이벤트만 이상한 경우: 버튼 캡 길이, 축 정렬, 뚜껑 압박 쪽이 원인인 경우가 많습니다.

    Wi-Fi 신호가 약할 때는 숫자 하나만 보고 단정 짓기보다, 메시지 지연과 재접속 패턴을 같이 보시는 게 낫습니다. RSSI가 좋지 않은 위치에서는 센서 노드가 살아 있어도 publish 간격이 흔들리기 쉽습니다. 이런 경우 펌웨어를 계속 만지는 것보다 설치 위치를 바꾸는 편이 훨씬 빠릅니다.

    3D 프린팅 프로젝트 조립 과정과 내부 부품 배치 이미지

    ESP 보드, 센서, 나사 기둥, 배선 동선이 어떻게 들어가는지 보여주는 조립 중간 단계 이미지입니다.

    ⚠️ 실제로 자주 만나는 문제와 해결법

    여기부터는 설명보다 판단이 중요합니다. 원인 후보가 여러 개여도 현장에서는 먼저 잘 걸리는 쪽부터 쳐내야 하거든요.

    1. 버튼이 계속 눌린 것으로 인식되는 문제

    가장 흔한 원인은 펌웨어가 아니라 물리 간섭입니다. 버튼 캡 길이가 길거나, 뚜껑 안쪽 리브가 스위치를 누르거나, 조립하면서 기판이 휘어서 스위치 위치가 올라온 경우가 많습니다. 해결은 버튼 스트로크를 줄이고, 뚜껑을 덮기 전과 후의 GPIO 상태를 비교하는 겁니다. 조립 전 정상, 조립 후 비정상이면 코드 디버깅보다 기구 수정이 맞습니다.

    2. 센서값이 이상하게 높거나 낮은 문제

    통풍 홀 개수 부족보다 발열 부품과의 거리 부족이 더 근본 원인인 경우가 많습니다. MCU, 레귤레이터, LED 저항 근처에 센서를 붙이면 값이 틀어집니다. 이럴 땐 홀을 더 많이 뚫기보다 센서 위치를 옮기거나, 센서만 별도 챔버로 빼는 편이 효과적입니다. 예쁘게 중앙 정렬하려다가 많이 생기는 문제라 더 조심하게 됩니다.

    3. 벽에 붙이면 Wi-Fi가 약해지는 문제

    책상 위에서 잘 되던 장치가 벽에 붙이면 나빠지는 건 흔합니다. 금속함, 콘크리트, 전선 밀집 구간, AP와의 방향 변화가 영향을 줍니다. 이럴 땐 펌웨어 재시도보다 최종 설치 위치에서 먼저 장시간 테스트하는 게 맞습니다. 프로토타입을 책상에서만 통과시키면 실제 운영 환경에서 다시 무너집니다.

    4. 출력물은 맞는데 조립 중 갈라지는 문제

    나사 기둥 벽이 얇거나 체결부 여유가 너무 타이트한 경우입니다. 프로토타입 단계에서는 미관보다 조립 공차를 넉넉히 주는 쪽이 낫습니다. 스마트 홈 장치는 한 번 조립하고 끝나는 물건이 아니라, 수정과 재플래시, 점검을 반복하는 물건이기 때문입니다.

    5. 전원은 들어오는데 가끔씩 재부팅되는 문제

    이건 의외로 케이블 스트레인 릴리프가 부족해서 생기는 경우가 많습니다. USB 포트 근처 공간이 모자라면 케이블이 비스듬히 눌리고, 벽에 붙인 뒤 장력이 달라지면서 접촉이 불안정해집니다. 케이스에 케이블이 지나가는 방향과 굴곡 여유를 설계하는 이유가 바로 이것입니다.

    이런 문제를 줄이는 가장 현실적인 방법은 출력 검증을 두 단계로 나누는 겁니다.

    1. 빈 케이스 또는 외벽이 얇은 샘플로 먼저 보드, 케이블, 버튼, 나사 위치를 맞춥니다.
    2. 구조가 맞는 것이 확인되면 그다음에 통풍 패턴, 외관 디테일, 브라켓 마감을 넣습니다.

    순서를 뒤집으면 멋진 실패작이 늘어납니다. 처음부터 완성형을 뽑는 방식은 시간도 많이 쓰고, 무엇이 문제였는지 분리하기도 어렵습니다.

    완성 후 무엇을 보면 잘 만든 장치인지 판단할까

    잘 만든 스마트 홈 DIY 장치는 단순히 켜지는 장치가 아닙니다. 저는 아래 기준을 통과해야 비로소 성공으로 봅니다.

    • 장시간 붙어 있어도 재접속이나 재부팅이 잦지 않은가
    • 센서값이 주변 환경 변화와 맞는 방향으로 움직이는가
    • 나중에 다시 열어서 점검하거나 수정할 수 있는가
    • 배선과 커넥터가 케이스에 눌리지 않는가
    • 같은 설계로 한 대 더 만들었을 때 같은 품질이 나오는가

    여기서 특히 마지막 항목이 중요합니다. 메이커 프로젝트는 첫 대가 겨우 돌아가면 성공처럼 느껴지기 쉽지만, 실제 완성도는 재현성에서 갈립니다. 두 번째 장치를 같은 순서로 조립할 수 있어야 하고, 같은 YAML과 같은 브라켓 규격으로 비슷한 결과가 나와야 합니다. 그래야 그 설계가 취미를 넘어 운영 가능한 템플릿이 됩니다.

    스마트 홈 DIY 3D 프린팅 프로젝트 결과 대시보드 이미지

    온도, 습도, 버튼 이벤트가 홈 대시보드에 정상적으로 표시되는 결과 검증 이미지입니다.

    운영 팁: 장치 하나보다 템플릿 하나를 만든다는 생각

    제가 요즘 3D 프린팅 프로젝트를 할 때는 개별 작품을 만든다기보다, 재사용 가능한 하드웨어 템플릿을 만든다는 생각으로 접근합니다. 한번 기준을 정해두면 다음 프로젝트 난도가 확 내려갑니다. 이 방식이 생각보다 오래 갑니다.

    예를 들면 이런 식입니다.

    • 벽걸이형 소형 노드는 브라켓 규격을 통일합니다.
    • 나사 종류를 한 가지로 묶어서 공구와 예비 부품을 줄입니다.
    • USB 케이블 출구 폭과 위치를 공통 규격으로 유지합니다.
    • ESPHome YAML은 장치별 이름과 핀만 바꾸는 구조로 맞춥니다.
    • 센서 챔버와 메인 보드 챔버를 분리하는 설계 원칙을 반복 적용합니다.

    이 습관이 중요한 이유는 단순합니다. 3D 프린팅 프로젝트는 장비보다 반복성이 실력을 끌어올립니다. 첫 번째 장치에서 생긴 판단을 다음 설계에 그대로 가져갈 수 있어야 출력물 더미가 아니라 운영 자산이 쌓입니다. 블로그 운영 측면에서도 이 템플릿 사고방식은 좋습니다. MQTT 구성 글, 센서 노드 글, 자동화 규칙 글, 유지보수 글이 내부 링크 구조로 자연스럽게 이어지거든요.

    자주 묻는 질문과 바로 적용할 권고

    PLA로 시작해도 될까요?

    실내용이고 직사광선이나 고온 노출이 크지 않은 위치라면 시작용으로 충분합니다. 다만 창가, 천장 근처, 열원 주변처럼 온도 스트레스가 걱정되면 PETG가 더 마음 편합니다. 저는 첫 프로토타입은 PLA로 보고, 장기 설치 최종본은 환경에 따라 다시 판단하는 편입니다.

    배터리형이 좋을까요, USB 전원이 좋을까요?

    첫 장치라면 USB 전원을 권합니다. 문제 분리가 쉽고, MQTT 끊김이나 센서값 이상이 전력 예산 때문인지 헷갈릴 일이 줄어듭니다. 배터리는 깔끔하지만 슬립 전략, 배터리 교체 동선, 저전력 센서 선택까지 함께 설계해야 합니다.

    케이스부터 예쁘게 만들어야 할까요?

    아니요. 구조 검증용 케이스를 먼저 뽑고, 외관은 그다음이 낫습니다. 조립 공차와 발열, 통풍, 케이블 동선이 확인되기 전의 미려한 디자인은 수정 비용이 큽니다. 저도 초반에는 외형부터 다듬었다가 같은 케이스를 다시 여러 번 뽑았어요.

    스냅핏이 더 고급스럽지 않나요?

    최종 제품 느낌은 더 좋을 수 있습니다. 다만 센서 노드처럼 중간에 한 번이라도 열 가능성이 있는 장치는 나사 체결이 훨씬 실용적입니다. 스냅핏은 반복 분해에서 피로가 쌓이고, 공차가 조금만 어긋나도 체감 난도가 급격히 올라갑니다.

    취미 3D 프린팅 스마트 홈 DIY 체크포인트 요약 이미지

    설치 위치, 전원 방식, 통풍, 로그 검증, 유지보수성을 한 장으로 정리한 요약 이미지입니다.

    마지막 권고: 이런 경우엔 이렇게 가시면 됩니다

    집에서 바로 써먹을 3D 프린팅 프로젝트를 원하신다면, 첫 작품은 USB 전원 + ESP32 + 환경 센서 + 나사 체결 케이스 + MQTT 상태 토픽 조합으로 가는 편이 좋습니다. 이 구성이 좋은 이유는 멋져서가 아니라, 문제가 생겼을 때 어디부터 볼지 명확하기 때문입니다. 전원은 분리하기 쉽고, 케이스는 다시 열 수 있고, 브로커에서는 온라인/오프라인 상태를 확인할 수 있고, 센서 위치는 통풍 위주로 수정하기 쉽습니다.

    반대로 이런 경우라면 판단을 바꾸시면 됩니다.

    • 처음 만드는 장치라면: USB 전원과 나사 체결이 잘 맞습니다.
    • 외관보다 안정성이 중요하다면: 센서 중앙 정렬보다 발열 분리를 우선하시면 됩니다.
    • 최종 설치 위치가 까다롭다면: 책상 테스트보다 벽면 장시간 테스트를 먼저 하셔야 합니다.
    • 여러 대 복제할 계획이라면: 브라켓, 나사, 케이블 출구, YAML 구조를 표준화하는 편이 이득입니다.
    • 배터리형을 꼭 써야 한다면: 첫 프로토타입은 USB로 검증한 뒤 전원부만 분기하는 방식이 안전합니다.

    스마트 홈 DIY는 전자, 네트워크, 물리 구조가 만나는 작업입니다. 여기서 3D 프린터는 단순히 케이스를 만드는 도구가 아니라, 운영 가능한 하드웨어를 설계하는 도구로 봐야 성과가 납니다. 여러 번 해보니 제일 오래 살아남는 장치는 가장 화려한 장치가 아니라, 다시 열 수 있고 원인 추적이 빠르고 같은 방식으로 한 대 더 만들 수 있는 장치였습니다. 시작하실 땐 센서 하나짜리 노드부터 가세요. 그걸 안정화한 뒤에 액추에이터나 디스플레이를 붙이는 편이 훨씬 덜 힘듭니다.

  • [보안] 웹 애플리케이션 보안 체크리스트: 개발자 배포 전 점검

    [보안] 웹 애플리케이션 보안 체크리스트: 개발자 배포 전 점검

    웹 애플리케이션 보안 체크리스트를 벽에 붙여두기만 하고 배포 게이트로 쓰지 않으면, 사고는 대개 어려운 취약점보다 빼먹은 기본 설정에서 납니다. 저도 여러 팀에서 비슷한 장면을 반복해서 봤습니다. 로그인은 붙었는데 세션 쿠키 속성이 비어 있고, 파일 업로드는 되는데 저장 위치가 웹 루트 안쪽이고, API는 인증이 있는데 객체 단위 권한 검사가 빠져 있는 식이었죠. WAF(Web Application Firewall, 웹 애플리케이션 방화벽)나 클라우드 보안 서비스가 있어도 이 층의 누락은 결국 애플리케이션 팀이 책임지게 되더라고요.

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 여기서는 웹 애플리케이션 보안 체크리스트를 단순 항목 나열이 아니라, 배포 직전 무엇을 어떤 기준으로 통과시킬지에 맞춰 다시 묶어보겠습니다. 특히 개발자 입장에서 자주 헷갈리는 지점, 예외를 허용해도 되는 조건, 장애를 만들지 않고 강도를 높이는 순서를 같이 적었습니다.

    웹 애플리케이션 보안 체크리스트 전체 흐름 아키텍처 이미지

    개발, 배포, 운영 단계에서 인증, 입력값 검증, 비밀정보 관리, 로깅, 취약점 스캔이 연결되는 전체 흐름을 보여주는 이미지입니다.

    웹 애플리케이션 보안 체크리스트가 왜 필요한가

    체크리스트는 문서가 아니라 릴리스 품질 게이트여야 합니다. 제 기준으로는 세 번 걸어야 의미가 있습니다. 첫째, PR(Pull Request) 리뷰에서 설계 결함을 걸러냅니다. 둘째, CI/CD(Continuous Integration/Continuous Deployment, 지속적 통합/배포)에서 자동으로 확인 가능한 항목을 실패 처리합니다. 셋째, 운영 반영 직전에 환경 차이로 누락된 설정을 다시 봅니다. 이 셋 중 하나라도 빠지면 개발 환경에서는 괜찮았는데 운영에서만 새는 문제가 남습니다.

    현장에서 자주 터지는 실패 모드는 생각보다 단순합니다. 임시 우회 설정이 운영까지 남아 있거나, 프레임워크 기본값을 과신하거나, 스캔 도구 결과를 숫자로만 읽고 실제 실행 경로를 보지 않는 경우가 많습니다. 근본 원인은 거의 늘 같습니다. 보안 항목이 기능 정의서에는 없는데 운영 책임에는 포함돼 있기 때문입니다. 그래서 체크리스트는 책임 소재를 분명히 하는 문서이기도 합니다. 누가 보고, 언제 막고, 어떤 경우 예외를 허용하는지까지 있어야 흔들리지 않습니다.

    웹 애플리케이션 보안 체크리스트에서 보는 OWASP Top 10

    OWASP Top 10을 그대로 외우는 건 실무에서 큰 도움이 안 될 때가 많습니다. OWASP도 이 목록을 최소 기준이자 출발점에 가깝다고 설명합니다. 개발자는 위험 범주를 코드와 설정의 점검 포인트로 번역해서 보는 편이 훨씬 실용적입니다. 제가 팀 문서로 옮길 때는 아래처럼 바꿔 적습니다.

    • Broken Access Control(접근 제어 실패): 요청 단위가 아니라 객체 단위로 권한을 확인하는지 봅니다.
    • Cryptographic Failures(암호화 실패): TLS 종료 지점, 쿠키 보안 속성, 저장 시 암호화 대상이 맞는지 봅니다.
    • Injection(인젝션): 쿼리 생성, 명령 실행, 템플릿 렌더링에서 입력이 코드로 해석될 여지가 있는지 봅니다.
    • Security Misconfiguration(보안 설정 오류): 디버그 모드, 기본 계정, 관리 포트 노출, 과한 CORS(Cross-Origin Resource Sharing) 허용을 봅니다.
    • Vulnerable and Outdated Components(취약하거나 오래된 구성요소): 취약 버전 자체보다 실행 경로에 실제로 닿는지, 직접 의존성인지 먼저 봅니다.
    • Identification and Authentication Failures(식별 및 인증 실패): 세션 고정, 토큰 만료, 재인증 정책, 관리자 기능 보호를 봅니다.

    이렇게 바꿔 놓으면 보안팀 용어를 개발 작업으로 연결하기 쉬워집니다. 제 경험상 팀이 가장 빨리 개선하는 영역도 여기입니다. 보안 이슈라고 적는 순간 막연해지는데, 이 API는 객체 소유자 검사가 빠져 있다고 쓰면 바로 수정 단위가 되거든요.

    실무에서 바로 쓰는 웹 애플리케이션 보안 체크리스트

    항목은 많아 보여도, 배포 전에 정말 체크할 것만 남기면 의외로 운영 가능합니다. 저는 아래 10개를 기본 축으로 둡니다. 관리자 페이지, 일반 사용자 서비스, 내부 API까지 거의 공통으로 먹힙니다.

    1. 인증과 세션: 세션 쿠키에 HttpOnly, Secure, SameSite가 있는지, 세션 만료와 재발급 정책이 분리돼 있는지 봅니다.
    2. 인가와 권한: 화면 숨김과 별개로 서버가 리소스별 권한을 검증하는지 봅니다.
    3. 입력값 검증: 허용 목록 기준인지, 길이 제한과 타입 검사가 서버에서 강제되는지 봅니다.
    4. 출력 인코딩: HTML, URL, JavaScript 문자열, JSON 응답 문맥을 섞지 않는지 봅니다.
    5. 비밀정보 관리: 코드 저장소, 빌드 로그, 런타임 환경 변수 출력, 에러 리포트에 시크릿이 남지 않는지 봅니다.
    6. 의존성 관리: 취약 패키지 유무보다 수정 가능 여부, 직접 의존성 여부, 런타임 노출 여부를 같이 봅니다.
    7. 보안 헤더: Content-Security-Policy, X-Content-Type-Options, Referrer-Policy가 실제 응답에 붙는지 확인합니다.
    8. 로깅과 모니터링: 로그인 실패, 권한 거부, 관리자 기능 실행, 비정상 오류는 남기되 민감정보는 마스킹합니다.
    9. 에러 처리: 사용자 응답은 단순하게, 내부 로그는 추적 가능하게 분리돼 있는지 봅니다.
    10. 파일 업로드: 확장자뿐 아니라 MIME 타입, 저장 경로, 후처리 파이프라인, 실행 권한을 같이 봅니다.

    웹 애플리케이션 보안 체크리스트 우선순위와 판정 기준

    영역 지금 바로 볼 것 배포 차단 기준 예외를 허용해도 되는 경우 권장 조치
    인증 세션 쿠키 속성, 세션 만료 정책 운영 HTTPS에서 Secure 누락, 관리자 세션 장기 유지, 재로그인 없이 민감 작업 가능 개발 로컬 환경의 비TLS 테스트 정도 프레임워크 설정에서 강제하고, 관리자 기능은 재인증 또는 짧은 세션 적용
    인가 객체 단위 접근 제어 다른 사용자 데이터가 서버 응답에 포함됨 없음 리소스 소유자 검증을 서비스 계층 또는 정책 미들웨어로 공통화
    입력값 서버 검증, 길이 제한 클라이언트 검증 제거 시 서버가 그대로 수용 내부 전용 도구라도 서버 검증은 유지 DTO/스키마 검증과 DB 제약조건을 함께 사용
    시크릿 코드 저장소, 로그, 환경 변수 노출 토큰·비밀번호가 저장소 또는 로그에 평문 존재 없음 시크릿 매니저 사용, 로그 마스킹 필터 적용
    의존성 직접 의존성의 high/critical 취약점 인터넷 노출 런타임 경로에서 수정 가능한 고위험 취약점 방치 패치가 없을 때 보완 통제와 만료일을 명시한 단기 예외 직접 의존성 우선 업데이트, 간접 의존성은 상위 패키지 교체 검토
    로깅 민감정보 포함 여부, request id 토큰 전문, 비밀번호, 주민등록번호류가 그대로 기록 없음 마스킹 규칙과 구조화 로그 키 표준화

    체크리스트를 돌릴 때 제가 고정으로 보는 결정 포인트

    보안 점검은 좋은 설정을 모으는 일이 아니라 충돌하는 목표 사이에서 우선순위를 정하는 일에 가깝습니다. 예를 들어 CSP(Content Security Policy)는 강하게 잠글수록 안전하지만, 이미 인라인 스크립트와 다중 CDN에 기대고 있는 서비스라면 한 번에 강제 적용하면 장애가 납니다. 반대로 SameSite 쿠키는 보수적으로 잡을수록 좋지만 외부 인증 연동이 있으면 리다이렉트 흐름을 깨뜨릴 수 있습니다. 이 지점에서 성급하게 밀어붙이면 보안팀도 개발팀도 둘 다 지치더라고요.

    상황 A를 택할 때 B를 택할 때 제 추천
    CSP 바로 강제 vs Report-Only부터 시작 리소스 출처가 단순하고 인라인 스크립트 제거가 끝난 경우 외부 스크립트가 많고 프런트 구조를 아직 정리 못한 경우 신규 서비스는 강제, 기존 서비스는 Content-Security-Policy-Report-Only로 먼저 수집 후 강제 전환
    세션 기반 인증 vs 토큰 기반 인증 서버 렌더링 중심, 브라우저 세션 관리가 단순한 경우 다수의 API 클라이언트, 모바일 앱, 서비스 간 호출이 있는 경우 브라우저 웹앱은 세션 쿠키 우선, 다중 클라이언트 API는 짧은 만료 토큰과 서버 측 권한 검증 병행
    취약점 스캔 즉시 차단 vs 예외 후 추적 직접 의존성, 수정 가능, 인터넷 노출 런타임인 경우 패치가 없고 실제 경로 노출이 없으며 보완 통제가 있는 경우 기계적으로 전부 차단하지 말고, 예외는 만료일과 대체 통제를 문서화
    파일 업로드 즉시 공개 저장 vs 비공개 저장 후 서빙 공개 정적 자산이며 서버 측 재처리가 필요 없는 경우 사용자 업로드, 개인정보, 후처리 검사가 필요한 경우 사용자 업로드는 웹 루트 밖 또는 비공개 버킷 저장 후 검증된 경로로만 제공

    실전 구현 1: HTTP 보안 헤더와 쿠키 설정

    헤더는 비용 대비 효과가 좋습니다. 다만 추가했다와 제대로 적용됐다는 완전히 다른 얘기입니다. 리버스 프록시에서 넣었는데 애플리케이션 서버가 덮어쓰는 경우도 있고, CDN이 일부 헤더를 제거하는 경우도 있습니다. 제 기준으로는 응답에 실제로 존재하는지, 브라우저 콘솔에서 정책 충돌이 없는지까지 봐야 끝입니다. 이거 확인 안 하면 겉으로만 안전해 보이는 상태가 은근 자주 남습니다.

    server {
        listen 443 ssl http2;
        server_name example.com;
    
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Frame-Options "DENY" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
        add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests" always;
    
        # nginx 1.19.3+ 기준
        proxy_cookie_flags ~ secure httponly samesite=lax;
    
        location / {
            proxy_pass http://app_upstream;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Request-Id $request_id;
        }
    }

    여기서 주의할 점도 분명합니다. X-Frame-Options: DENY는 관리 콘솔이나 외부 임베드 요구가 있으면 충돌할 수 있습니다. SameSite=Strict는 가장 보수적이지만 외부 로그인 연동이나 결제 리다이렉트 흐름을 깨뜨릴 수 있어, 일반적인 웹앱은 Lax부터 검토하는 편이 현실적입니다. style-src 'unsafe-inline'은 이상적이진 않지만, 기존 서비스에서 인라인 스타일 제거가 안 끝났다면 임시로 두고 범위를 줄여가는 식이 보통 더 안전합니다.

    검증은 아래 정도면 충분합니다. 중요한 건 한 번 찍고 끝내지 않고, 로그인 전후 응답과 주요 페이지 응답을 각각 보는 겁니다. 쿠키 속성은 인증 이후에야 보이는 경우가 많거든요.

    curl -I https://example.com
    curl -s -D - -o /dev/null https://example.com | grep -Ei 'content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy'
    
    # 로그인 후에는 브라우저 개발자 도구 또는 프록시에서 Set-Cookie 속성을 확인
    # 확인 포인트: HttpOnly; Secure; SameSite=Lax 또는 SameSite=Strict

    판단 기준도 애매하게 두지 않는 편이 좋습니다. 보안 헤더가 아예 없으면 설정 누락으로 보고 바로 수정합니다. CSP 위반 로그가 다수 보이면 정책이 앱 구조와 맞지 않는다는 뜻이지, 무조건 완화하라는 의미는 아닙니다. 어떤 스크립트나 스타일이 어디서 로드되는지부터 분류해야 합니다. 관련 글이 있다면 여기서 보안 헤더 가이드나 OWASP Top 10 해설로 내부 링크를 연결해 두면 검색 유입에도 도움이 됩니다.

    웹 애플리케이션 보안 체크리스트에서 보안 헤더 구성을 설명하는 이미지

    리버스 프록시에서 보안 헤더를 추가하고, 뒤쪽 애플리케이션으로 요청을 전달하는 구조를 설명하는 이미지입니다.

    실전 구현 2: 의존성 취약점 점검과 결과 읽는 법

    의존성 스캔은 돌렸느냐보다 읽을 줄 아느냐가 더 중요합니다. 현업에서 진짜 시간을 잡아먹는 건 경고 숫자보다 우선순위 판단입니다. 저는 세 가지만 먼저 봅니다. 직접 의존성인가, 수정 가능한가, 운영 런타임에 실제로 실리나. 이 순서로 보면 노이즈가 꽤 줄어듭니다.

    # Node.js
    npm audit --audit-level=high
    npm outdated
    npm ls --depth=2
    
    # Python 3.10+ 환경 예시
    python -m pip install --upgrade pip
    python -m pip install pip-audit
    pip-audit
    python -m pip list --outdated

    npm audit 결과에서 fix available가 있으면 예외 등록보다 업데이트 가능성부터 보는 게 맞습니다. 다만 자동 수정이 항상 정답은 아닙니다. 메이저 버전 상승이 포함되면 빌드나 런타임 동작이 바뀔 수 있으니까요. 저는 직접 의존성이면 changelog를 확인하고 테스트가 있는 범위에서 먼저 올리고, 간접 의존성이면 상위 패키지 업데이트로 해결되는지부터 봅니다.

    초반에 제가 가장 많이 실수했던 것도 여기였습니다. CI에서 high 이상이면 전부 배포 차단으로 걸어두니, 결국 개발팀이 예외 파일만 늘리더라고요. 그 뒤부터는 룰을 바꿨습니다. 운영 노출 경로 + 수정 가능 + 직접 의존성이면 차단, 그 외는 보완 통제와 일정 관리로 보되 만료일 없는 예외는 금지. 이 기준이 있어야 스캔 도구가 업무를 막는 장치가 아니라 우선순위 도구가 됩니다.

    결과 해석 기준

    • high 또는 critical이라도 개발 전용 패키지인지 운영 경로인지 먼저 구분합니다.
    • fix available가 있으면 예외보다 업데이트를 우선합니다.
    • 같은 루트 패키지에서 파생된 경고는 묶어서 봐야 실제 작업량이 보입니다.
    • 패치가 없으면 차단보다 보완 통제를 문서화하고 재검토 일정을 박아두는 편이 낫습니다.

    실전 구현 3: 시크릿 관리와 로그 마스킹

    시크릿은 코드에만 안 넣으면 끝이라고 생각하기 쉬운데, 실제 누출 경로는 훨씬 많습니다. CI 로그, 컨테이너 시작 스크립트, 예외 스택, 디버그 엔드포인트, 운영자가 급히 남긴 임시 로그가 다 후보입니다. 특히 장애 대응 중에 잠깐 보자고 찍은 로그가 오래 남는 경우가 정말 많습니다. 이 부분은 한 번 터지면 수습이 꽤 번거롭더라고요.

    # 런타임 환경 변수 이름만 점검
    printenv | cut -d= -f1 | grep -Ei 'token|secret|password|key'
    
    # 저장소 내 하드코딩 흔적 점검
    git grep -nE '(API_KEY|SECRET_KEY|PASSWORD|TOKEN|ACCESS_KEY|PRIVATE_KEY)'
    
    # 로그 샘플에서 민감 키 이름 확인
    grep -RniE 'authorization:|bearer |set-cookie|password|secret|token' ./logs 2>/dev/null

    이 명령은 어디까지나 자신이 관리하는 시스템의 방어 점검용입니다. 결과를 외부에 공유하거나 협업 채널에 그대로 붙이면 안 됩니다. 제 경험상 가장 안전한 운영 방식은 두 가지입니다. 첫째, 시크릿 값 자체를 로그에 남기지 않도록 애플리케이션 레벨에서 필터링합니다. 둘째, 장애 분석에 필요한 식별자는 토큰 전체가 아니라 해시나 앞뒤 일부만 남깁니다.

    예를 들어 로그에 Authorization 헤더 전체를 남기면 분석은 편하지만 유출 리스크가 너무 큽니다. 반대로 아무것도 안 남기면 장애 추적이 막힙니다. 저는 보통 request id, 사용자 내부 식별자, 권한 결과, 실패 원인 코드까지만 남기고, 비밀번호·세션 ID·Bearer 토큰 전문은 금지합니다. 여기서 중요한 건 민감정보를 빼자가 아니라 분석 가능한 최소 정보만 남기자는 원칙입니다.

    재현 가능한 점검 시나리오: 권한 검증 누락 찾기

    추상적인 원칙보다 실제 점검 절차 하나가 더 오래 남습니다. 그래서 저는 배포 전 권한 검증 확인을 꼭 시나리오로 남깁니다. 대상은 관리자 페이지, 주문 조회, 프로필 수정, 첨부파일 다운로드처럼 사용자별 소유권이 분명한 기능입니다. 절차는 복잡하지 않습니다. 내가 관리하는 테스트 환경에서 정상 계정으로 로그인하고, 네트워크 탭에서 해당 요청의 URL, 메서드, 응답 코드를 확인합니다. 그런 다음 같은 계정 상태에서, 시스템이 허용하지 않아야 하는 다른 리소스에 대한 접근 요청이 서버에서 차단되는지만 봅니다.

    여기서 핵심은 우회 기법을 늘어놓는 게 아니라 서버가 객체 소유권을 기준으로 차단하는지 확인하는 데 있습니다. 정상이라면 403 Forbidden 또는 서비스 정책에 맞는 거부 응답이 나와야 합니다. 만약 화면에서는 버튼이 숨겨져 있는데 응답 자체는 나온다면, 그건 거의 전형적인 Broken Access Control입니다. 프런트엔드 조건 분기와 서버 권한 검증을 같은 것으로 취급한 결과죠.

    이 시나리오가 좋은 이유는 재현성이 있기 때문입니다. QA가 다시 돌리기 쉽고, 개발자가 수정 후 검증 기준으로 삼기 쉽습니다. 또 성능 부담도 거의 없습니다. 별도 스캐너 없이 브라우저 개발자 도구와 애플리케이션 로그만으로 충분히 확인 가능한 경우가 많습니다. 저는 이런 체크가 문서에 한 줄로만 적혀 있을 때보다, 어떤 화면에서 무엇을 봐야 하는지 적혀 있을 때 팀이 훨씬 덜 놓친다고 봅니다.

    웹 애플리케이션 보안 체크리스트의 접근 제어 검증 흐름 이미지

    정상 사용자 요청과 권한 없는 요청을 구분하고, 서버가 403으로 차단하는 흐름을 보여주는 이미지입니다.

    주의사항과 트러블슈팅

    보안 설정은 강하게 넣을수록 좋아 보이지만, 운영에서는 늘 부작용과 같이 옵니다. 그래서 저는 장애를 만드는 설정과 리스크를 줄이는 설정을 구분해서 적용합니다. 이 선을 잘 잡아야 팀이 보안을 싫어하지 않습니다.

    • CSP 적용 후 화면이 깨짐: 대개 근본 원인은 인라인 스크립트, 서드파티 CDN, 레거시 위젯입니다. 이럴 땐 정책을 즉시 느슨하게 풀기보다 Report-Only로 위반 출처를 수집하고, 인라인 제거 작업부터 시작하는 편이 낫습니다.
    • SameSite 설정 후 로그인 연동 문제: OAuth 2.0, SSO(Single Sign-On), 결제 리다이렉트가 있으면 Strict가 과할 수 있습니다. 외부 리다이렉트가 있는 웹앱은 먼저 Lax를 검토하세요.
    • 취약점 스캔 결과가 너무 많음: 근본 원인은 중복 경고와 간접 의존성입니다. 루트 패키지별로 묶고, 직접 의존성부터 줄이는 편이 실제 효과가 큽니다.
    • 에러를 숨겼더니 운영 분석이 어려움: 사용자 응답과 내부 로그를 분리하지 않았기 때문입니다. 외부 응답은 일반화하고, 내부에는 request id, 예외 클래스, 상위 원인만 남기면 됩니다.

    파일 업로드는 특히 낙관적으로 보면 안 됩니다. 확장자 검사만으로는 부족하고, 저장 위치가 웹 루트 안쪽이면 더 위험합니다. 제 추천은 분명합니다. 사용자 업로드는 웹에서 직접 실행되지 않는 위치에 저장하고, 필요하면 별도 서빙 계층이나 비공개 스토리지의 서명 URL 같은 방식으로 노출하세요. 이미지라면 업로드 직후 원본을 그대로 노출하기보다 후처리 파이프라인을 거친 파생본만 서빙하는 편이 더 안전합니다. 원본 메타데이터(EXIF 등) 처리 여부도 같이 확인하셔야 합니다.

    웹 애플리케이션 보안 체크리스트 검증: 배포 전에 무엇을 확인하면 되나

    배포 직전 검증은 길게 할 필요 없습니다. 대신 짧아도 빠뜨리면 안 되는 순서가 있습니다. 제가 운영 중인 서비스에서 주로 쓰는 순서는 아래와 같습니다. 이 순서가 좋은 이유는 헤더, 권한, 의존성, 로그 노출처럼 서로 다른 계층을 짧은 시간 안에 교차 검증할 수 있기 때문입니다.

    1. 주요 응답 헤더 확인: curl -I와 실제 로그인 후 응답에서 헤더와 쿠키 속성을 봅니다.
    2. 로그인/권한 흐름 확인: 일반 사용자와 관리자 요청이 서버에서 의도대로 구분되는지 봅니다.
    3. 취약점 스캔 결과 확인: 직접 의존성, 수정 가능, 런타임 노출 여부로 우선순위를 정합니다.
    4. 로그 샘플 확인: 토큰, 비밀번호, 개인정보가 평문으로 남지 않는지 확인합니다.
    5. 에러 페이지 확인: 내부 경로, 프레임워크 버전, 스택 트레이스 노출이 없는지 봅니다.

    이때 기준을 분명히 적어두면 팀이 덜 흔들립니다. 예를 들어 인증·권한 실패는 예외 없이 수정 대상, 직접 의존성의 수정 가능한 고위험 취약점은 배포 전 우선 조치, 로그 민감정보 노출은 기능 영향과 무관하게 즉시 수정, CSP 경고는 실제 차단 영향과 위반 출처를 확인한 뒤 단계 적용. 이렇게 써두면 이번엔 바쁘니까 넘어가자는 말이 줄어듭니다.

    # 배포 전 방어 점검 예시 순서
    curl -I https://example.com
    curl -s -D - -o /dev/null https://example.com/login | grep -Ei 'set-cookie|content-security-policy|x-frame-options|x-content-type-options'
    npm audit --audit-level=high
    pip-audit
    git grep -nE '(API_KEY|SECRET_KEY|PASSWORD|TOKEN)'
    # 로그 샘플은 운영 정책에 따라 마스킹된 사본으로만 확인

    여기서 하나 더 중요합니다. 자동화 가능한 항목과 사람 판단이 필요한 항목을 섞지 않는 게 좋습니다. 헤더 존재 여부, 의존성 스캔, 하드코딩 흔적 검색은 CI에서 자동화하기 좋습니다. 반면 권한 설계의 타당성, CSP 위반 허용 범위, 예외 승인 타당성은 여전히 사람이 봐야 합니다. 둘을 구분하지 않으면 자동화가 과신으로 바뀌거나, 반대로 전부 수작업이 되어 금방 지칩니다.

    웹 애플리케이션 보안 체크리스트 검증 결과 대시보드 이미지

    헤더 점검, 의존성 스캔, 권한 검증, 로그 마스킹 상태를 한눈에 보여주는 결과 요약 이미지입니다.

    운영 문서로 남길 때 좋은 형태

    체크리스트는 문서함에 들어가면 죽습니다. 살아 있으려면 PR 템플릿, 배포 파이프라인, 장애 대응 문서에 각각 연결돼야 합니다. 제가 추천하는 방식은 문서를 길게 쓰는 게 아니라, 각 접점에 질문을 짧게 심는 겁니다. 예를 들어 PR 템플릿에는 새 API 추가 시 인증/인가 확인, 민감정보 로그 출력 없음, 시크릿 하드코딩 없음 정도만 있어도 효과가 큽니다. 배포 파이프라인에는 헤더 검사, 의존성 스캔, 기본 grep 검사를 자동화하고요.

    반대로 하지 말아야 할 것도 있습니다. 체크리스트를 보안팀 전용 문서로 분리해 두는 방식입니다. 그러면 개발자는 릴리스 압박을 먼저 받고, 보안 항목은 마지막 승인 절차로만 남습니다. 그 구조에서는 늘 늦습니다. 개발자가 수정할 수 있는 형태로, 배포 전에 다시 확인할 수 있는 문장으로 바꿔야 실제로 작동합니다.

    FAQ: 현업에서 자주 나오는 질문

    보안 도구를 도입했는데도 왜 불안할까요?

    도구는 탐지해주지만, 권한 설계와 예외 판단을 대신해주지는 않습니다. 특히 객체 단위 권한, 로그 민감정보, 운영 설정 차이는 도구만으로 다 걸러지지 않습니다. 그래서 웹 애플리케이션 보안 체크리스트가 필요합니다.

    개발 속도가 느려지지 않나요?

    초반에는 조금 느려집니다. 다만 운영 사고가 나면 기능 개발, 고객 대응, 로그 조사, 롤백까지 한 번에 붙습니다. 제가 체감한 차이는 단순했습니다. 초기에 배포 기준을 분명히 둔 팀이 결국 더 빨랐습니다. 재작업이 줄기 때문입니다.

    OWASP Top 10을 다 외워야 하나요?

    그럴 필요 없습니다. 인증, 권한, 입력값, 설정, 의존성 같은 개발 작업 단위로 번역해서 보시면 됩니다. 외우는 것보다 배포 체크 질문으로 바꾸는 편이 훨씬 실용적입니다.

    인증, 인가, 입력값 검증, 보안 헤더, 의존성 관리, 로그 마스킹을 핵심만 정리한 요약 이미지입니다.

    마무리: 상황별로 이렇게 가시면 됩니다

    이미 운영 중인 서비스에서 빠르게 리스크를 줄여야 한다면, 저는 권한 검증 확인 → 보안 헤더와 쿠키 속성 점검 → 의존성 스캔 우선순위 정리 → 시크릿·로그 점검 순서로 가시라고 말씀드립니다. 개인정보 처리 API나 관리자 기능이 있으면 접근 제어가 첫 번째입니다. 정적 페이지 비중이 큰 서비스라면 CSP와 배포 헤더 설정이 먼저고요. 외부 로그인 연동이 많다면 SameSite와 리다이렉트 흐름부터 조심해서 보셔야 합니다.

    신규 프로젝트라면 더 단순합니다. PR 템플릿에 체크 항목을 심고, CI에 자동 검사 가능한 부분을 붙이고, 운영 반영 직전에 권한과 로그 샘플만 사람이 다시 보면 됩니다. 이 순서가 가장 덜 지치고, 가장 오래 갑니다.

    보안은 거대한 장비보다 작은 누락에서 먼저 무너집니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적이며, 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 체크리스트를 문서로만 두지 말고 배포 흐름에 연결해 두세요. 그 순간부터 품질 관리가 아니라 사고 예방 장치가 됩니다.

  • 홈랩에서 주식 봇 2대를 4개월 굴려보고 — 솔직한 운영 회고 (Proxmox LXC)

    개인 투자용 보조 도구를 남의 클라우드에 올리기가 영 내키지 않았습니다. API 키, 알림, 관심 종목… 사소해 보여도 결국 내 판단 흔적이 남는 데이터라, 그냥 집에 있는 홈랩에 올리자 싶었죠. 그렇게 시그널 봇 1대 + 포트폴리오 봇 1대를 Proxmox LXC로 올려 4개월을 굴렸습니다. 성공담이 아니라 — 솔직히 잘된 것도, 벽에 부딪힌 것도 다 적어봅니다.

    1. 왜 클라우드가 아니라 홈랩이었나

    • 프라이버시: 보유 현황·관심 종목은 개인 투자 성향 그 자체입니다. 남의 서버에 두기 싫었어요.
    • 비용 0: 이미 돌아가는 홈랩(Intel i5-8500 · 6코어 · 31GB RAM · Proxmox VE 8.4)에 LXC 하나 더 얹는 건 공짜에 가깝습니다.
    • 완전한 통제: 언제든 콘솔 열고 뜯어볼 수 있습니다.

    2. 두 봇은 역할이 완전히 다릅니다

    코드가 아니라 ‘용도’ 기준으로 나눠보면 이렇습니다.

    • 시그널 봇 — 관심 종목의 리스크 뉴스 키워드(리콜·결함 등)를 감지하면 텔레그램으로 “검토 알림”을 보냅니다. 핵심 원칙은 “자동 매매가 아니라, 조건 충족 사실만 전달 — 매매 결정은 본인 판단” 입니다.

    ▲ 시그널 봇의 텔레그램 리스크 알림 예시

    • 포트폴리오 봇 — Core-Satellite 방식으로 보유 현황을 웹 대시보드에 보여주고, 트레일링스톱·트렌드브레이크·리스크뉴스 같은 신호를 표시합니다. “매주 적립, 매도는 목표 수익 도달 또는 펀더멘털 훼손 시에만” 같은 규칙 기반이고요.

    ▲ 포트폴리오 봇 대시보드 (실제 보유 수치는 가림)

    3. 굳이 LXC 2대로 나눈 이유 — 완전 격리

    둘을 한 서버에 몰지 않고 LXC 2대로 물리적으로 분리했습니다. 각각 별도 텔레그램 토큰, 별도 DB, 별도 코드 저장소로요.

    이유는 단순합니다. 한쪽이 죽거나 꼬여도 다른 쪽은 멀쩡해야 하니까요. 토큰이 섞이면 알림이 엉키고, DB가 섞이면 디버깅이 지옥이 됩니다. 봇 하나당 스펙도 소박합니다 — 2코어 / 1GB RAM / 8GB 디스크면 충분했습니다.

    # LXC 114 (stock-signal-bot)
    cores: 2
    memory: 1024
    swap: 512
    rootfs: local-zfs:subvol-114-disk-0, size=8G
    
    # LXC 115 (stock-portfolio-bot) — 동일 스펙

    이 “작게 쪼개서 격리하는” 패턴은 나중에 다른 자동화(예: 블로그 발행 봇)에도 그대로 재사용했습니다. 한 번 몸에 익히니 새 프로젝트를 올릴 때 고민이 줄더라고요.

    4. 배포·운영

    각 LXC 안에서 Docker로 돌리고, onboot=1로 호스트 재부팅 시 자동 기동, 상태는 텔레그램 알림으로 확인합니다. 24/7 가동이지만 리소스가 작아 호스트에 부담이 거의 없습니다.

    5. 솔직히 — 가장 큰 벽은 ‘내 계좌 데이터’였습니다

    여기가 이 글의 핵심입니다. 인프라(LXC·Docker·알림)는 오히려 쉬웠고, 진짜 벽은 데이터였어요. 그런데 그 데이터가 시세가 아니라 ‘내 실제 보유 현황’ 이었다는 게 포인트입니다.

    • 시세(현재가·추이)는 어떻게든 흘러들어옵니다. 문제는 내가 어떤 종목을 몇 주, 평단 얼마에 갖고 있는지를 자동으로 못 가져온다는 거였습니다.
    • 증권사 계좌 연동이 벽이었습니다. 개인이 증권사 API를 붙이는 건 문턱이 높거나 제약이 많더라고요.
    • 그래서 주식을 사고팔 때마다 보유 수량·평단을 손으로 반영해야 합니다. 자동화 도구를 만들어놓고, 정작 제일 자주 바뀌는 데이터(내 포지션)를 수동으로 넣고 있는 거죠. 이게 제일 귀찮고, 자동화의 핵심을 못 채운 부분입니다.
    • 결국 “매매하면 손으로 업데이트”라는 수동 루프가 남았습니다. 대시보드는 멀쩡히 도는데 그 앞단에 사람 손이 계속 들어가니, “완전 자동”과는 거리가 멀어졌어요.

    6. 그래도 남은 것

    • LXC 격리·Docker·헬스체크·텔레그램 알림 같은 홈랩 운영 패턴을 제대로 체득했습니다.
    • “소규모 봇은 2코어/1GB면 충분하다”는 감이 생겨서, 이후 프로젝트의 리소스 산정이 훨씬 쉬워졌습니다.
    • 무엇보다 “뭐가 자동화의 진짜 병목인지”를 몸으로 배웠습니다. 서버가 아니라 데이터 소스라는 걸요.

    7. 홈랩에서 이런 봇 굴리려는 분께

    클라우드 홈랩(자가 호스팅)
    비용 월 과금 지속 전기세 수준(사실상 0)
    프라이버시 제3자 서버에 데이터 내 손 안
    유지보수 관리형(편함) 본인 몫
    데이터 접근 동일하게 API 필요 동일하게 API 필요

    핵심 조언 두 가지입니다.

    • 포트폴리오 자동화의 진짜 관문은 시세가 아니라 ‘내 계좌(보유 현황) 연동’입니다. 증권사 API가 붙는지부터 확인하세요. 안 되면 결국 수동 입력이 남고, 그게 자동화의 발목을 잡습니다.
    • 개인 금융 도구라면 클라우드보다 홈랩이 프라이버시·비용에서 유리합니다. 대신 유지보수는 온전히 본인 몫이라는 걸 감안하세요.

    ⚠️ 이 글은 홈랩 운영 기록이며 투자 조언이 아닙니다. 언급된 종목·수치는 예시일 뿐이며, 어떤 매매도 권유하지 않습니다.

  • [보안] KVM 보안 강화: 게스트-호스트 격리와 CVE 방어 전략

    [보안] KVM 보안 강화: 게스트-호스트 격리와 CVE 방어 전략

    KVM 보안 강화: 게스트-호스트 격리와 CVE 방어 전략

    KVM 보안 강화는 옵션이 아니라 운영 품질에 가깝습니다. VM이 몇 대 안 될 때는 성능과 편의가 먼저 눈에 들어오지만, 실제 사고를 줄이는 건 늘 게스트-호스트 격리, 관리면 축소, 업데이트 절차더라고요. 저도 홈랩과 테스트 호스트를 굴리면서 비슷하게 느꼈습니다. 문제는 대개 화려한 제로데이보다, 편의상 열어 둔 공유 경로, 느슨한 libvirt 권한, 꺼 둔 SELinux/AppArmor, 방치된 장치 에뮬레이션에서 시작됐거든요. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.

    KVM 보안 강화 아키텍처와 게스트-호스트 격리 개요 이미지

    호스트 커널, KVM, QEMU, libvirt, 가상 네트워크가 어떻게 분리되는지 한눈에 보여주는 개요 이미지입니다.

    KVM 보안 강화가 중요한 이유

    KVM 환경은 흔히 하이퍼바이저 하나로 뭉뚱그려 말하지만, 실제 위험은 계층마다 다르게 생깁니다. 커널 쪽은 KVM 모듈과 메모리 관리가, 사용자 공간은 QEMU 장치 에뮬레이션이, 운영면은 libvirt 소켓과 정책이, 네트워크 쪽은 브리지와 필터가 각각 문제를 만듭니다. 그래서 보안은 제품 하나를 더 붙이는 작업이 아니라, 어느 레이어가 깨져도 다음 레이어에서 피해를 줄이는 구조를 만드는 일에 더 가깝습니다.

    운영하면서 특히 자주 보는 위험 지점은 아래 네 가지였습니다.

    • 장치가 과한 VM: 안 쓰는 USB, 사운드, CD-ROM, 그래픽 장치가 남아 있으면 QEMU 공격면만 늘어납니다.
    • 편의성 때문에 열린 공유: 호스트 경로를 광범위하게 마운트하거나 9p, virtiofs를 무분별하게 열면 격리 이점이 빠르게 줄어듭니다.
    • 관리면 노출: libvirt 관리 소켓, 원격 API, SSH 키 관리가 느슨하면 VM 자체보다 운영 채널이 먼저 흔들립니다.
    • 격리 정책 해제: SELinux나 AppArmor를 꺼 두면 당장은 편하지만, 다음 장애 때 원인 추적이 더 어려워집니다.

    최근 공개되는 취약점 대응도 같은 맥락으로 봐야 합니다. CVE 번호를 외우는 것보다 중요한 건, 어떤 이슈가 나오더라도 장치 최소화, 프로세스 confinement, 네트워크 분리, 패치 검증 루틴으로 폭발 반경을 줄여 두는 겁니다. 실무에서는 이 차이가 진짜 크게 납니다.

    KVM 보안 강화 관점에서 이해하는 KVM, QEMU, libvirt, sVirt

    구조를 이해할 때 가장 도움이 됐던 기준은 단순했습니다. KVM은 커널 안에서 CPU 가상화 가속을 맡고, QEMU는 실제 VM 프로세스를 띄우며, libvirt는 그 수명주기와 정책을 관리합니다. 여기에 sVirt와 SELinux 또는 AppArmor가 붙어서, VM 프로세스가 접근할 수 있는 파일·장치 범위를 제한합니다. 결국 보안의 핵심은 “VM이 뜨는가”보다 “QEMU 프로세스가 어디까지 닿을 수 있는가”에 있습니다.

    계층 주요 역할 현실적인 실패 모드 우선 점검 포인트 이럴 때 특히 중요
    KVM 커널 레벨 가상화 가속 커널 패치 지연, 불필요한 기능 노출 호스트 커널 업데이트, LSM 활성화 멀티테넌트, 외부 노출 워크로드
    QEMU 장치 에뮬레이션, VM 실행 안 쓰는 가상 장치 유지, 광범위한 파일 접근 디바이스 최소화, seccomp, 비루트 실행 여부 확인 GUI 없는 서버 VM, CI 러너
    libvirt 정책 및 수명주기 관리 과한 권한, 소켓 접근 통제 부재 polkit, 소켓 권한, XML 템플릿 표준화 여러 관리자가 함께 운영
    sVirt / SELinux / AppArmor 프로세스·파일 격리 문제 해결을 핑계로 비활성화 Enforcing 유지, 라벨·프로파일 조정 이미지 경로 변경, 패스스루 구성
    가상 네트워크 브리지, NAT, 필터링 관리망과 서비스망 혼재, 스푸핑 방어 부재 nwfilter, 브리지 분리, 방화벽 존 구분 동일 브리지에 다수 VM 수용

    여기서 흔한 오해가 하나 있습니다. “내부망인데 괜찮지 않나”라는 판단이죠. 실제로는 내부망일수록 브리지 하나에 관리 트래픽, 스토리지, 게스트 서비스 트래픽을 한꺼번에 올려 두는 경우가 많아서, 사고가 나면 범위가 더 넓게 번집니다. 특히 테스트 VM과 운영 보조 VM이 섞인 호스트라면 이 구분을 빨리 해 두는 편이 훨씬 낫습니다.

    실전 1: KVM 보안 강화의 시작, 호스트 기본 상태 점검

    KVM 보안 강화의 출발점은 현재 상태를 숫자와 로그로 확인하는 겁니다. 여기서 중요한 건 “설치돼 있다”가 아니라 “격리 기능이 실제로 살아 있느냐”예요. 저는 새 호스트를 받으면 아래 순서대로 봅니다.

    1. CPU 가상화와 KVM 모듈이 정상 로드됐는지 확인
    2. QEMU, libvirt, 커널 버전과 배포판 보안 업데이트 상태 확인
    3. SELinux 또는 AppArmor가 enforcing 성격으로 동작 중인지 확인
    4. libvirt가 monolithic 데몬인지, modular 데몬인지 확인
    5. 기본 검증 도구와 로그에서 이미 실패 신호가 나오는지 확인
    uname -r
    lscpu | egrep 'Virtualization|Vendor ID|Model name'
    lsmod | grep -E '^kvm(_intel|_amd)?'
    virt-host-validate qemu
    
    # RHEL, Rocky, AlmaLinux 계열
    rpm -q qemu-kvm libvirt selinux-policy-targeted
    getenforce
    systemctl status virtqemud.socket virtqemud libvirtd --no-pager
    journalctl -u virtqemud -u libvirtd -b --no-pager | tail -n 80
    
    # Debian, Ubuntu 계열
    dpkg -l | egrep 'qemu-system|libvirt-daemon|libvirt-clients|apparmor'
    aa-status
    systemctl status libvirtd virtqemud --no-pager
    journalctl -u libvirtd -u virtqemud -b --no-pager | tail -n 80

    해석 기준도 명확해야 합니다. <code>virt-host-validate에서 FAIL이 뜨면 그냥 넘기지 않는 편이 맞습니다. 겉보기엔 기능 문제처럼 보여도, 커널 옵션이나 cgroup 구성이 격리 기능에 영향을 줄 때가 있거든요. getenforce가 Enforcing이 아니거나 aa-status에서 적용 중인 프로파일이 거의 보이지 않으면, 그 상태에서는 나머지 보안 논의가 반쯤 힘을 잃습니다.

    이 단계에서 같이 봐야 할 건 패치 절차입니다. 최근 취약점 방어를 실무적으로 하려면 “뉴스를 빨리 읽는다”보다 “보안 업데이트를 언제 어떤 순서로 반영하는가”가 더 중요합니다. 테스트 호스트 한 대에서 VM 기동, 저장소 접근, 마이그레이션, 백업 경로를 먼저 확인하고 운영 호스트에 넘기는 흐름이 있으면 대응 품질이 훨씬 안정적입니다. 관련해서 호스트 방화벽이나 리눅스 권한 모델을 따로 정리한 글이 있다면, 그 글도 함께 읽어 두면 운영 기준을 잡기 편합니다.

    실전 2: 게스트-호스트 격리를 실제 설정으로 묶기

    보안 수준이 확 갈리는 구간이 여기입니다. VM이 잘 뜬다는 이유로 XML을 방치하면, 몇 달 뒤에는 왜 그런 장치가 붙어 있는지 아무도 설명 못 하는 환경이 됩니다. 저는 템플릿을 만들 때 “필요한 것만 남기기”를 먼저 하고, 그다음에 보안 라벨과 네트워크 필터를 확인합니다. 이 순서가 생각보다 편하더라고요.

    KVM 보안 강화용 sVirt 라벨과 네트워크 필터 적용

    libvirt domain XML에서는 최소한 seclabel, 디스크 경로, 인터페이스 필터, 장치 수를 같이 봐야 합니다. 단일 호스트라도 이 부분을 표준화해 두면 운영 품질이 꽤 올라갑니다.

    virsh dumpxml hardened-vm > /tmp/hardened-vm.xml
    virsh nwfilter-list
    virsh nwfilter-dumpxml clean-traffic
    virsh domiflist hardened-vm
    virsh domblklist hardened-vm --details
    <domain type='kvm'>
      <name>hardened-vm</name>
      <memory unit='MiB'>4096</memory>
      <vcpu placement='static'>2</vcpu>
      <os>
        <type arch='x86_64' machine='q35'>hvm</type>
        <boot dev='hd'/>
      </os>
      <features>
        <acpi/>
        <apic/>
      </features>
      <cpu mode='host-model' check='partial'/>
      <seclabel type='dynamic' model='selinux' relabel='yes'/>
      <devices>
        <emulator>/usr/bin/qemu-system-x86_64</emulator>
        <disk type='file' device='disk'>
          <driver name='qemu' type='qcow2' discard='unmap'/>
          <source file='/var/lib/libvirt/images/hardened-vm.qcow2'/>
          <target dev='vda' bus='virtio'/>
        </disk>
        <interface type='bridge'>
          <source bridge='br0'/>
          <model type='virtio'/>
          <filterref filter='clean-traffic'>
            <parameter name='IP' value='192.0.2.10'/>
          </filterref>
        </interface>
        <serial type='pty'>
          <target port='0'/>
        </serial>
        <console type='pty'>
          <target type='serial' port='0'/>
        </console>
        <memballoon model='virtio'/>
      </devices>
    </domain>

    clean-traffic는 MAC spoofing, IP spoofing, ARP spoofing을 줄이는 데 실용적입니다. 특히 같은 브리지에 여러 VM을 올리는 환경에서는 비용 대비 효과가 좋습니다. 다만 고정 IP를 전제로 운영하는 편이 더 편해서, DHCP로 자주 바뀌는 임시 VM에는 관리 부담이 생길 수 있습니다. 이럴 땐 필터를 통째로 빼기보다, 관리망용 브리지와 실험용 브리지를 분리하는 쪽이 더 안전합니다.

    KVM 보안 강화용 libvirt 격리 설정 다이어그램

    domain XML의 seclabel, bridge, nwfilter, virtio 디바이스 최소화 구성을 묶어 보여주는 설정 다이어그램입니다.

    QEMU 보안 옵션은 기본값을 존중하는 쪽이 안전합니다

    운영 중 가장 위험한 판단 중 하나가 “일단 뜨게 하자”입니다. 패스스루나 특수 마운트가 안 될 때 security_driver를 비우거나 confinement를 낮추는 경우가 있는데, 그 순간부터는 문제를 해결한 게 아니라 탐지 능력을 버린 셈이 됩니다. 저는 먼저 로그를 보고 어떤 제약이 걸렸는지 확인한 뒤, 필요한 범위만 예외를 만듭니다. 이거 한번 습관 붙이면 훨씬 덜 흔들립니다.

    grep -E '^(#\s*)?(security_driver|security_default_confined|user|group|namespaces|seccomp_sandbox|dynamic_ownership)' /etc/libvirt/qemu.conf
    
    # 소켓 및 권한 확인
    systemctl cat virtqemud.socket libvirtd.socket --no-pager
    getfacl /var/run/libvirt/libvirt-sock /var/run/libvirt/virtqemud-sock 2>/dev/null
    
    # 최근 부팅 이후 libvirt/QEMU 거부·오류 로그 확인
    journalctl -u virtqemud -u libvirtd -b --no-pager | grep -Ei 'denied|avc|apparmor|seccomp|qemu'
    # /etc/libvirt/qemu.conf 예시
    # security_default_confined = 1
    # user = "qemu"
    # group = "qemu"
    # dynamic_ownership = 1
    # seccomp_sandbox = 1

    여기서 판단 기준은 이렇습니다.

    • security_default_confined를 껐다면, 이유가 문서화돼 있지 않은 이상 과거 임시 조치가 굳어졌을 가능성을 먼저 의심하는 편이 맞습니다.
    • user와 group가 루트 권한으로 과하게 올라가 있으면, 편의는 늘어도 사고 범위가 커집니다.
    • dynamic_ownership는 스토리지 경로 운영 방식과 같이 봐야 합니다. 여러 관리자가 수동으로 파일 권한을 건드리는 환경이라면 표준 경로를 먼저 정리하는 쪽이 낫습니다.
    • seccomp_sandbox를 끄는 건 마지막 수단이어야 합니다. 실제 병목은 이 옵션보다 스토리지 캐시, 백엔드 네트워크, CPU 모델 선택에 있는 경우가 더 많습니다.

    여러 환경을 다뤄보면 공통점이 있습니다. QEMU 보안 옵션은 성능을 깎는 장식이 아니라, 문제를 지역화하는 장치라는 점이죠. 보호 장치를 꺼 버리면 장애는 잠깐 사라져도 다음엔 더 크게 돌아옵니다.

    실전 3: 최신 취약점 방어는 패치, 최소화, 분리로 갑니다

    최신 CVE 대응을 운영 언어로 바꾸면 세 가지입니다. 첫째, 호스트 커널과 QEMU, libvirt를 배포판 보안 공지 기준으로 빠르게 갱신합니다. 둘째, 사용하지 않는 장치와 공유 기능을 제거합니다. 셋째, 관리면과 데이터면을 분리해 한 지점의 실패가 전체 확산으로 이어지지 않게 합니다. 이 세 가지가 쌓이면, 개별 취약점 세부 사항을 전부 몰라도 방어력이 꽤 올라갑니다.

    특히 장치 에뮬레이션 쪽은 “안 쓰면 줄어드는 위험”이 분명합니다. 아래 표는 실제 운영에서 자주 고민하는 선택지입니다.

    구성 요소 유지해도 되는 경우 빼는 편이 나은 경우 보안상 판단 이유
    USB redirection VDI, 데스크톱 VM에서 실제 장치 전달이 필요할 때 서버 VM, 헤드리스 워크로드 불필요하면 장치 관련 공격면만 늘어납니다.
    사운드 장치 멀티미디어 테스트, GUI 앱 검증 웹 서버, DB, 배치, CI 러너 서버 VM에는 기능 이득이 거의 없습니다.
    CD-ROM 장치 초기 설치, 드라이버 주입, 복구 작업 직후 설치 완료 후 상시 운영 남겨 둘 이유가 사라지면 제거하는 편이 낫습니다.
    호스트 경로 공유 명확한 목적의 제한된 경로 공유가 필요할 때 전역 경로, 다목적 공유 폴더 게스트에서 호스트 파일 체계로 닿는 면적이 커집니다.
    원격 libvirt 관리 전용 관리망과 인증 체계를 갖춘 경우 일반 서비스망과 혼재한 경우 운영 채널이 먼저 노려질 가능성이 큽니다.

    새 VM 템플릿을 만들 때 저는 다음 기준으로 자릅니다. 서버 VM이면 GUI 장치, 사운드, USB, 광학 장치는 특별한 이유가 없으면 빼고 시작합니다. 파일 공유는 “편해서”가 아니라 목적과 경로가 분명할 때만 넣습니다. 관리 소켓과 API는 로컬 우선으로 두고, 원격 관리가 필요하면 전용 관리망과 접근 제어를 같이 설계합니다. 스냅샷과 백업 이미지는 운영자가 일반 작업하는 경로와 섞지 않는 편이 좋고요. 이런 기본기가 나중에 권한, 라벨, 삭제 사고를 꽤 많이 줄여 줍니다.

    ⚠️ 제가 겪었던 문제: SELinux를 끄지 말고 라벨을 바로잡으세요

    이 문제는 생각보다 자주 나옵니다. 디스크 이미지를 /var/lib/libvirt/images 밖으로 옮긴 뒤 XML만 고치고 끝내면, VM 기동 시 QEMU가 파일 접근을 거부당하는 경우가 많습니다. 이때 정책이 너무 빡세다고 느끼기 쉬운데, 사실은 정책이 정상 동작 중인 겁니다. 막혀야 할 접근을 막고 있는 거니까요.

    제가 실제로 겪었던 재현 시나리오는 이랬습니다. 저장소 정리를 하려고 이미지를 /srv/vm-images로 옮겼고, domain XML의 <source file='...'/>만 수정했습니다. 기동은 실패했고, 로그에는 AVC deny만 남았습니다. 원인은 단순했습니다. 파일 경로는 바뀌었는데 보안 라벨은 새 경로 기준으로 맞추지 않았던 겁니다. 이거 처음 겪으면 꽤 당황스럽습니다.

    virsh start hardened-vm
    journalctl -xe --no-pager | grep -Ei 'denied|avc|apparmor|qemu|libvirt'
    ls -l /srv/vm-images
    ls -Z /srv/vm-images 2>/dev/null || true
    restorecon -Rv /srv/vm-images
    
    # 경로 자체를 영구적으로 표준 라벨 대상으로 추가해야 할 때 예시
    semanage fcontext -a -t virt_image_t '/srv/vm-images(/.*)?'
    restorecon -Rv /srv/vm-images

    해석 기준도 분명합니다.

    • 예상한 경로에서 AVC deny가 반복되면, 첫 번째 가설은 거의 항상 라벨 문제입니다.
    • 파일 소유권과 경로가 정상인데도 실패하면, domain XML의 디스크 경로와 seclabel, 그리고 libvirt 보안 드라이버 설정을 같이 봐야 합니다.
    • setenforce 0는 원인 확인용 임시 진단으로만 제한해야 합니다. 영구 대응으로 쓰면 다음 장애 때 격리도 원인도 동시에 잃게 됩니다.

    이 경험을 한 번 하고 나면 시각이 조금 달라집니다. SELinux는 일을 방해하는 게 아니라, 잘못된 경로 변경을 초기에 드러내 주는 장치였거든요. 운영에서는 이런 시끄러운 정직함이 훨씬 낫습니다.

    KVM 보안 강화 검증을 위한 로그와 정책 확인 이미지

    journalctl, AVC deny 로그, 파일 보안 라벨 확인 흐름을 시각화한 검증 이미지입니다.

    검증: KVM 보안 강화 후 무엇을 보고 안전하다고 판단할까

    설정은 넣는 것보다 검증이 더 중요합니다. 저는 보안 강화가 끝난 뒤 아래 순서로 확인합니다. 핵심은 정상 동작만 보는 게 아니라, 예상 밖 접근이 실제로 막히는지까지 보는 겁니다.

    1. QEMU 프로세스가 의도한 이미지와 인터페이스만 사용하는지 확인
    2. SELinux/AppArmor 로그에서 정상 차단과 업무 장애를 구분
    3. nwfilter와 브리지 구성이 실제 인터페이스에 반영됐는지 확인
    4. 업데이트 후 부팅뿐 아니라 스냅샷, 백업, 마이그레이션 경로까지 확인
    virsh dominfo hardened-vm
    virsh domiflist hardened-vm
    virsh dumpxml hardened-vm | sed -n '/<seclabel/,/<\/seclabel>/p;/<interface/,/<\/interface>/p'
    ps -ef | grep '[q]emu-system'
    virsh nwfilter-list
    journalctl -b --no-pager | grep -Ei 'avc:|apparmor|seccomp|virtqemud|libvirtd' | tail -n 100

    이 결과를 볼 때는 숫자보다 의도와 실제가 일치하는지를 따집니다. XML에는 없는 장치가 QEMU 실행 파라미터에 많다면 템플릿이 지저분하다는 뜻입니다. 보안 로그가 전혀 없는 상태가 무조건 이상적인 것도 아닙니다. 정책이 살아 있으면 정상 차단 로그가 가끔 보일 수 있습니다. 반대로 VM 기동 때마다 같은 거부가 반복되고 서비스가 실제로 실패한다면, 그건 바로 조정해야 할 신호입니다.

    여기서 많이 놓치는 부분이 업데이트 후 부팅만 확인하는 습관입니다. 라이브 마이그레이션, 스냅샷, 백업, 이미지 증설 같은 운영 동작은 평소엔 덜 보이지만 장애 때 가장 먼저 필요해집니다. 그래서 저는 패치 검증을 할 때 평소 쓰는 관리 작업 하나 이상을 반드시 같이 돌립니다. 이 루틴 하나만 있어도 운영 안정성이 꽤 달라집니다.

    자주 묻는 포인트와 운영 팁

    1. 네트워크를 완전히 분리해야 하나요?

    무조건 물리적으로 쪼개야 한다는 뜻은 아닙니다. 다만 관리망과 게스트 서비스망은 논리적으로라도 분리하는 편이 좋습니다. 단일 브리지 하나에 libvirt 관리, 스토리지, 일반 트래픽을 모두 태우면 나중에 로그 분석과 정책 적용이 훨씬 까다로워집니다. 홈랩은 VLAN이나 브리지 분리부터, 소규모 서버는 최소한 방화벽 존 분리부터 시작하는 편이 현실적입니다.

    2. 성능 때문에 보안 기능을 일부 끄고 싶을 때는요?

    저라면 순서를 바꿉니다. 먼저 스토리지 캐시 정책, 디스크 포맷, CPU 모델, vCPU pinning 필요성, 네트워크 백엔드를 확인합니다. 실제 병목은 seccomp나 sVirt보다 다른 데 있을 때가 훨씬 많았습니다. 보호 기능을 끄는 건 마지막 판단이어야 하고, 꼭 필요하면 대상 VM, 이유, 기간을 문서화해 두는 게 맞습니다.

    3. CVE 방어는 어디까지 자동화해야 하나요?

    최소한 패키지 업데이트 확인, 재부팅 필요 여부 확인, 핵심 VM 기동 테스트까지는 자동화 가치가 높습니다. 다만 운영 반영은 테스트 단계를 거치는 편이 안전합니다. 특히 커널과 QEMU 업데이트는 재부팅 또는 VM 재시작 전략까지 함께 생각해야 합니다.

    • 홈랩: 수동 검토와 테스트 호스트 1대면 충분한 경우가 많습니다.
    • 소규모 팀: 보안 공지 수집, 패키지 점검, 핵심 VM 스모크 테스트를 스크립트화하면 효율이 좋습니다.
    • 서비스 운영: 변경 창구, 롤백 절차, 로그 보존, 책임자 승인 흐름까지 포함해야 오래 버팁니다.

    마무리: 이런 환경이면 이렇게 가시면 됩니다

    KVM 보안 강화는 대단한 신제품을 도입하는 작업보다, 기본 격리를 복구하고 불필요한 요소를 줄이는 운영 discipline에 더 가깝습니다. 여러 번 부딪혀 보니 결국 결과를 가르는 건 세 가지였습니다. QEMU 장치를 줄였는지, SELinux나 AppArmor를 끄지 않고 다뤘는지, 패치와 검증을 절차로 만들었는지요.

    환경별 추천도 꽤 분명합니다.

    • 홈랩이나 단일 호스트라면: SELinux/AppArmor 활성화, XML 장치 최소화, clean-traffic 적용, 이미지 저장 경로 표준화부터 시작하는 편이 좋습니다.
    • 여러 VM을 운영하는 소규모 서버라면: 관리망 분리, 템플릿 표준화, libvirt 소켓 접근 통제, 업데이트 검증 절차까지 같이 가져가세요.
    • 외부 서비스가 올라가는 환경이라면: 테스트 호스트, 변경 절차, 로그 점검 자동화, 백업·마이그레이션 검증 없이 오래 운영하기 어렵습니다.

    한 줄로 줄이면 이렇습니다. 편의 때문에 격리를 끄는 쪽보다, 조금 불편하더라도 원인을 좁혀 수정하는 쪽이 결국 더 싸게 먹힙니다. 최신 취약점 방어도 그 연장선입니다. 패치를 빠르게 넣고, 공격면을 줄이고, 관리면을 분리하면 CVE 하나하나에 덜 흔들리는 환경이 됩니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.

    패치, 격리, 네트워크 필터, 정책 검증, 운영 절차를 한 장으로 정리한 요약 인포그래픽입니다.

  • [Linux] firewalld 보안 체크리스트: Linux 서버 방화벽 설정 모범 사례

    [Linux] firewalld 보안 체크리스트: Linux 서버 방화벽 설정 모범 사례

    firewalld 보안 체크리스트: Linux 서버 방화벽 설정 모범 사례

    리눅스 서버를 오래 운영하다 보면, 서비스는 멀쩡한데 방화벽이 애매하게 열려 있어서 뒤늦게 식은땀이 나는 순간이 꼭 옵니다. 저도 홈랩이랑 운영 서버를 같이 보면서 몇 번이나 겪었거든요. 특히 배포 직후에는 패키지 설치, TLS, 애플리케이션 기동에 신경이 쏠리다 보니 firewalld 보안 체크리스트가 뒤로 밀리기 쉽습니다. 그런데 실제 사고는 대개 여기서 납니다. CVE가 바로 터지지 않더라도, 불필요하게 열린 관리 포트 하나가 공격 표면을 넓히고, 나중에는 “왜 이 포트가 열려 있지?”를 추적하는 운영 비용으로 돌아오더라고요.

    이 글은 명령어만 늘어놓는 글이 아닙니다. 저는 firewalld를 볼 때 “무엇을 열까”보다 “왜 이 경계가 필요한가, 어디서 망가지기 쉬운가, 변경 후 어떻게 검증할까”를 더 중요하게 봅니다. 그래서 이번 글은 Linux 방화벽 설정을 점검하는 순서, 자주 꼬이는 실패 모드의 원인, 단일 NIC 서버와 듀얼 NIC 서버에서 판단이 갈리는 지점까지 실무 기준으로 묶었습니다. 체크박스만 채우는 요약이 아니라, 실제로 복붙해서 점검하고 운영 습관까지 바꿀 수 있는 내용으로 정리해보겠습니다.

    firewalld 보안 체크리스트를 설명하는 Linux 서버 방화벽 아키텍처 이미지

    firewalld의 Zone(존), Service(서비스), Port(포트)가 어떻게 맞물려 동작하는지 한눈에 보여주는 개요 이미지입니다.

    1. 왜 firewalld 보안 체크리스트가 먼저냐

    제가 운영 초기에 가장 많이 본 실수는 “애플리케이션이 정상 응답하니 네트워크도 정리된 줄 아는 상태”였습니다. 실제로는 반대인 경우가 많습니다. 앱은 443만 써야 하는데, 테스트 때 열어둔 8080, 관리용 9090, 배포판 기본 서비스까지 같이 살아 있는 식이죠. firewalld는 이런 상태를 정리하기에 꽤 좋은 도구입니다. 다만 편한 도구라고 해서 자동으로 안전해지지는 않습니다. 규칙을 추가하기 쉬운 도구일수록, 나중에 덜어내는 기준이 더 중요합니다.

    제가 실무에서 기준으로 삼는 건 네 가지입니다. 첫째, 인터넷에 공개할 이유가 없는 포트는 기본적으로 닫는다. 둘째, 관리 트래픽은 서비스 트래픽과 다른 경계로 본다. 셋째, 런타임 실험과 영구 반영을 섞지 않는다. 넷째, 명령 성공이 아니라 실제 패킷 경로 기준으로 검증한다. 방화벽은 설정 자체보다 운영 중 일관성이 더 중요합니다.

    • 최소 허용: 서비스가 실제로 수신해야 하는 프로토콜과 포트만 엽니다.
    • 경계 분리: 공개망, 관리망, 백업망을 같은 존 정책으로 처리하지 않습니다.
    • 상태 분리: 런타임 테스트와 --permanent 반영을 구분합니다.
    • 검증 우선: firewall-cmd 출력만 보지 말고 리슨 상태와 외부 접속 결과까지 확인합니다.

    2. firewalld 보안 체크리스트의 핵심 개념

    처음에 많이 헷갈리는 부분이 Zone, Service, Port, Rich Rule이 각각 뭘 책임지는지입니다. 저는 이걸 “정책의 단위가 다르다”로 이해하는 편이 가장 덜 꼬였습니다. Zone은 경계, Service는 의미 있는 허용 단위, Port는 예외 처리, Rich Rule은 조건부 허용입니다. 이걸 섞어 쓰더라도 각자 맡는 역할이 분명해야 나중에 규칙 해석이 쉬워집니다.

    2-1. Zone(존, 신뢰 수준별 정책 그룹)

    Zone은 인터페이스 또는 소스 대역에 붙는 정책 경계입니다. 중요한 건 “서버 한 대 = 정책 한 개”가 아니라는 점입니다. NIC가 하나여도 출발지 대역이 다르면 다른 경계로 볼 수 있고, NIC가 둘 이상이면 더더욱 분리하는 게 맞습니다. 운영하면서 제일 위험한 패턴은 외부 공개 NIC와 관리자 전용 네트워크를 둘 다 public에 묶어두는 경우였습니다. 이렇게 해두면 나중에 SSH, 백업, 모니터링 예외가 전부 public에 쌓이면서 정책이 지저분해집니다.

    2-2. Service(서비스, 미리 정의된 포트 집합)

    ssh, http, https 같은 표준 서비스는 Service로 여는 편이 낫습니다. 숫자 포트보다 의도가 남기 때문입니다. 나중에 누가 봐도 “이 서버가 왜 이 포트를 열었는지”를 해석하기 쉽습니다. 다만 모든 걸 Service로 해결하려고 하면 오히려 애매해질 수 있습니다. 예를 들어 커스텀 앱 8443/tcp를 장기 운영하면서 이름 없는 포트로만 남겨두면 의미를 잃고, 반대로 잠깐 쓸 포트까지 서비스 XML로 만드는 건 과합니다.

    2-3. Runtime과 Permanent

    여기가 제일 많이 사고 나는 지점입니다. 런타임은 현재 커널에 적용된 메모리 상태이고, 영구 설정은 디스크에 저장된 상태입니다. 즉, 테스트할 때는 런타임이 빠르지만 운영 기준은 결국 영구 설정입니다. 문제는 두 상태가 잠시 어긋나도 눈으로는 잘 안 보인다는 점이죠. 그래서 저는 “테스트는 런타임, 반영은 영구, 마감은 reload 후 재검증” 순서를 고정해둡니다. 이 흐름을 지키면 재부팅 뒤 접속 불가 같은 사고가 확 줄어듭니다.

    선택지 언제 쓰면 좋은가 장점 언제 피해야 하는가 운영상 주의점
    Service 추가 SSH, HTTP, HTTPS 같은 표준 포트 의도가 명확하고 읽기 쉽습니다 실제 포트 구성이 표준과 다를 때 --info-service로 정의 내용을 확인해두면 좋습니다
    Port 직접 허용 커스텀 앱, 단기 테스트, 비표준 포트 즉시 반영이 쉽습니다 장기 운영 포트를 설명 없이 누적할 때 운영 문서에 포트 용도를 같이 남겨야 합니다
    Rich Rule 출발지 IP 제한, 로그, 세밀한 예외 처리 서비스 공개 범위를 정확히 좁힐 수 있습니다 단순한 허용 규칙까지 전부 Rich Rule로 만들 때 규칙이 많아지면 읽기 어려워지므로 목적별로 묶어야 합니다
    Zone 분리 NIC가 둘 이상이거나 관리망/서비스망이 분리된 환경 경계가 명확해지고 예외가 줄어듭니다 작은 단일 NIC 서버에서 과도하게 복잡도를 키울 때 인터페이스 매핑과 소스 대역 매핑을 혼동하지 않아야 합니다
    커스텀 Service XML 장기 운영하는 사내 표준 앱 포트 포트 의미를 이름으로 보존할 수 있습니다 일회성 포트 하나만 잠깐 열 때 /etc/firewalld/services/에 정의를 남기고 변경 이력을 관리하면 편합니다

    3. 실전 체크리스트 1단계: 현재 상태부터 정확히 파악하기

    방화벽 작업에서 제일 위험한 건 규칙 추가 자체가 아니라, 현재 상태를 오해한 채 손대는 겁니다. 원격 SSH 세션 하나로 작업 중인데 현재 세션이 어느 존 정책에 의해 살아 있는지, 같은 서버에 다른 관리 포트가 떠 있는지, 런타임과 영구 설정이 같은지 모르고 건드리면 끊어먹기 쉽습니다. 그래서 저는 항상 “서비스 상태 → 활성 존 → 인터페이스 매핑 → 허용 항목 → 리슨 상태” 순서로 봅니다. 이 순서가 의외로 사고를 많이 줄여줍니다.

    1. firewalld 서비스가 실제로 올라와 있는지 확인합니다.
    2. 기본 Zone과 활성 Zone이 같은지 봅니다.
    3. 어떤 인터페이스가 어느 Zone에 붙었는지 확인합니다.
    4. Zone별 허용 서비스, 포트, Rich Rule을 나눠 봅니다.
    5. 실제 프로세스가 어떤 주소와 포트에 바인딩되어 있는지 비교합니다.
    sudo systemctl status firewalld
    sudo firewall-cmd --state
    sudo firewall-cmd --get-default-zone
    sudo firewall-cmd --get-active-zones
    sudo firewall-cmd --zone=public --list-all
    sudo firewall-cmd --list-all-zones
    sudo ss -tulpn

    여기서 읽는 기준이 중요합니다. --get-active-zones는 “정책 이름”보다 “어느 인터페이스나 소스가 어디에 매핑됐는지”를 보는 명령이라고 생각하시면 됩니다. --list-all에서는 services, ports, rich rules를 따로 봐야 합니다. 운영 현장에서 진짜 자주 보는 실수가 서비스만 보고 안심하는 겁니다. 예전에 열어둔 포트가 ports:에 남아 있는데 services:만 보고 지나가면 정책 오판이 생깁니다.

    한 가지 더 보셔야 할 게 있습니다. ss -tulpn 결과와 firewalld 허용 목록이 맞는지 비교해야 합니다. 앱이 0.0.0.0:9090에 떠 있는데 방화벽에서 9090이 닫혀 있으면 외부 노출은 막히겠지만, 내부 망이나 로컬 프록시 경로에서는 다른 문제가 생길 수 있습니다. 반대로 앱이 127.0.0.1에만 바인딩되어 있으면 포트를 아무리 열어도 외부 접속은 안 됩니다. 보안 점검과 장애 점검이 이 지점에서 만납니다.

    제가 실무에서 자주 쓰는 확인 패턴은 아래처럼 “방화벽 상태와 실제 리슨 상태를 한 번에 비교 가능한 형태”로 보는 방식입니다.

    sudo firewall-cmd --get-active-zones
    sudo firewall-cmd --zone=public --list-services
    sudo firewall-cmd --zone=public --list-ports
    sudo firewall-cmd --zone=public --list-rich-rules
    sudo ss -lntp
    sudo ss -lnup
    firewalld 보안 체크리스트 점검을 위한 Linux 방화벽 설정 터미널 이미지

    활성 Zone, 인터페이스 매핑, 허용 서비스와 포트를 점검하는 실제 운영자 시점의 터미널 화면을 표현한 이미지입니다.

    4. 실전 체크리스트 2단계: 필요한 것만 열고 나머지는 닫기

    이 단계부터는 정책을 정리합니다. 기준은 단순합니다. “지금 열려 있는 것”이 아니라 “이 서버 역할에 꼭 필요한 것”만 남기는 겁니다. 웹 서버라면 대개 공개 포트는 80/443 중 필요한 것만 남고, SSH는 관리 경로로만 제한합니다. 이때 중요한 건 보안을 이유로 운영성을 망치지 않는 겁니다. 예를 들어 SSH 전체 차단은 멋있어 보일 수 있지만, 콘솔 대체 수단이 없으면 사고 복구 시간을 키웁니다. 그래서 저는 외부 공개 서버에서 가장 현실적인 기본형을 HTTPS 공개 + SSH 출발지 제한 + 불필요한 기본 서비스 제거로 잡습니다.

    4-1. 기본 서비스만 먼저 허용

    sudo firewall-cmd --permanent --zone=public --add-service=https
    sudo firewall-cmd --permanent --zone=public --add-service=http
    sudo firewall-cmd --permanent --zone=public --remove-service=cockpit
    sudo firewall-cmd --permanent --zone=public --remove-service=dhcpv6-client
    sudo firewall-cmd --reload
    sudo firewall-cmd --zone=public --list-all

    여기서 판단 기준을 분명히 하셔야 합니다. http는 80에서 443으로 리다이렉트하는 프런트가 있거나 ACME HTTP-01 검증이 필요하면 열고, 그렇지 않으면 빼도 됩니다. cockpit은 명시적으로 운영할 때만 여는 편이 안전합니다. 다만 cockpit이나 dhcpv6-client가 실제로 기본 등록되어 있는지는 배포판과 초기 설정에 따라 다를 수 있으니, 제거 전에 --list-all로 현재 상태를 먼저 보세요. 운영 보안은 결국 기본값을 그대로 믿지 않는 데서 시작합니다.

    4-2. SSH는 전체 공개보다 출발지 IP 제한

    SSH는 “열까 말까”보다 “누구에게 열까”가 더 중요합니다. 운영 서버에서 22/tcp 전체 공개는 공격 표면을 넓히는 데 비해 얻는 편의가 적습니다. 고정 관리 IP나 VPN 대역이 있다면 Rich Rule로 제한하는 쪽이 좋습니다. 특히 fail2ban 같은 보조 수단을 쓰더라도, 방화벽에서 먼저 줄이는 게 낫습니다. 로그인 실패를 막는 것보다 애초에 소켓 도달 범위를 좁히는 편이 더 근본적이거든요.

    sudo firewall-cmd --permanent --zone=public \
      --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" service name="ssh" accept'
    
    sudo firewall-cmd --permanent --zone=public --remove-service=ssh
    sudo firewall-cmd --reload
    sudo firewall-cmd --zone=public --list-rich-rules

    이 규칙의 핵심은 단순합니다. ssh 서비스를 전체 공개하지 않고, 특정 출발지에서만 받아줍니다. 관리 대역이 VPN 서브넷이라면 203.0.113.10/32 대신 실제 CIDR을 넣으시면 됩니다. 다만 여기서 흔한 실패는 “회사 공인 IP라고 생각했던 주소가 실제로는 NAT 뒤에서 바뀌는 주소”인 경우입니다. 그러면 방화벽 문제처럼 보이는데 사실은 출발지 식별 가정이 틀린 겁니다. 그래서 이 단계는 현재 세션을 유지한 채 새 터미널에서 재접속 테스트까지 같이 하시는 게 안전합니다.

    4-3. 커스텀 포트는 무조건 열지 말고, 수명에 따라 처리 방식 나누기

    8443, 9090, 3000 같은 커스텀 포트는 운영에서 자주 나옵니다. 저는 이걸 수명 기준으로 나눕니다. 일시적인 디버그 포트면 짧게 열고 바로 닫습니다. 장기 운영 포트면 처음부터 이름 있는 서비스로 승격할지, 포트로 두되 문서화할지 정합니다. 이걸 안 정하면 몇 달 뒤 포트 목록이 기술 부채가 됩니다.

    sudo firewall-cmd --permanent --zone=public --add-port=8443/tcp
    sudo firewall-cmd --permanent --zone=public --remove-port=9090/tcp
    sudo firewall-cmd --reload
    sudo firewall-cmd --zone=public --list-ports

    장기 운영 포트가 있고 팀원이 여러 명이라면 커스텀 서비스 정의를 고려할 만합니다. 포트를 숫자로만 남겨두면 “8443이 무슨 용도였지?”가 반복됩니다. 반대로 운영 수명이 짧은 포트는 XML 정의까지 만들 필요는 없습니다. 이런 선택 기준이 실무에서 시간을 아껴줍니다.

    <?xml version="1.0" encoding="utf-8"?>
    <service>
      <short>internal-api</short>
      <description>Internal API exposed via TCP 8443</description>
      <port protocol="tcp" port="8443"/>
    </service>

    위 같은 파일을 /etc/firewalld/services/internal-api.xml에 두고 reload한 뒤 --add-service=internal-api로 관리하면, 숫자 포트보다 정책 의미가 오래 남습니다. 저는 포트가 배포 문서, 모니터링, 보안 정책에 반복 등장하기 시작하면 이 단계로 올립니다.

    5. 실전 체크리스트 3단계: Zone 분리와 인터페이스 매핑

    NIC가 둘 이상인 서버에서는 여기서 보안 수준이 꽤 갈립니다. 공개 트래픽과 관리 트래픽을 같은 존에 넣어두면, 나중에 허용 규칙이 죄다 public으로 몰립니다. 그 결과 “관리 편의를 위해 public 예외를 하나 더 추가”하는 패턴이 반복되고, 결국 원래 분리했어야 할 경계가 사라집니다. 제가 듀얼 NIC 서버에서 가장 먼저 정리하는 게 이 부분입니다.

    sudo firewall-cmd --permanent --new-zone=management
    sudo firewall-cmd --permanent --zone=management --add-service=ssh
    sudo firewall-cmd --permanent --zone=management --add-source=192.168.10.0/24
    sudo firewall-cmd --permanent --zone=public --change-interface=eth0
    sudo firewall-cmd --permanent --zone=management --change-interface=eth1
    sudo firewall-cmd --reload
    sudo firewall-cmd --get-active-zones

    이 구성이 좋은 이유는 규칙을 줄여주기 때문입니다. 외부 서비스망 eth0는 공개 포트만 남기고, 내부 관리망 eth1은 SSH, 백업, 모니터링처럼 성격이 다른 트래픽을 따로 받습니다. 운영하면서 예외가 생기더라도 public을 더럽히지 않고 management 쪽에서 해결할 수 있습니다.

    다만 언제나 Zone 분리가 정답은 아닙니다. NIC가 하나뿐이고 관리도 VPN 하나로만 들어온다면, 억지로 존을 많이 만드는 것보다 public 하나에 출발지 제한 Rich Rule을 두는 편이 단순하고 덜 위험합니다. 정책은 강한 것보다 읽기 쉬운 것이 오래 갑니다. 제가 권하는 기준은 이렇습니다. 인터페이스가 다르면 Zone 분리, 인터페이스는 같고 출발지만 다르면 Rich Rule 우선. 이게 대부분의 중소형 서버에서 가장 덜 꼬입니다.

    한 가지 더 실무 팁을 드리면, NetworkManager가 인터페이스-존 매핑을 함께 관리하는 환경에서는 firewalld 쪽 변경과 네트워크 연결 프로필 설정이 엇갈리지 않게 맞춰야 합니다. 그래서 저는 관리망이 명확한 환경에서는 “관리 NIC는 management로 고정, VPN이나 사내 대역은 source로 보강” 정도까지만 씁니다. 규칙 해석이 복잡해지는 순간, 보안 자체보다 운영 오류가 더 큰 리스크가 됩니다.

    Linux 방화벽 설정에서 firewalld 존 분리를 보여주는 서버 보안 강화 이미지

    외부 서비스망과 내부 관리망을 분리한 듀얼 NIC 환경의 Zone 설계를 이해하기 위한 다이어그램입니다.

    6. 실제로 많이 꼬이는 포인트와 트러블슈팅

    이 섹션은 공식 문서보다 운영에서 더 많이 필요합니다. 명령 자체는 맞는데 결과가 기대와 다를 때, 대개 원인은 문법보다 가정이 틀린 데 있습니다. 제가 실제로 자주 본 실패 모드만 추렸습니다. 이 부분을 미리 알고 들어가면 대응 속도가 확실히 빨라집니다.

    6-1. 재부팅 후 규칙이 사라진다

    대부분 원인은 단순합니다. 런타임에만 넣고 영구 반영을 안 했거나, 반대로 런타임과 영구 상태가 이미 달라져 있었는데 작업자가 둘을 같은 것으로 착각한 경우입니다. 근본 원인은 “테스트 상태와 배포 상태를 구분하지 않는 습관”입니다.

    sudo firewall-cmd --list-all
    sudo firewall-cmd --permanent --zone=public --list-all
    sudo firewall-cmd --runtime-to-permanent
    sudo firewall-cmd --reload

    --runtime-to-permanent는 편하지만 무심코 쓰면 임시로 열어둔 포트까지 저장해버릴 수 있습니다. 그래서 저는 이 명령을 “좋은 상태가 이미 런타임에 올라와 있다는 걸 확인한 뒤”에만 씁니다. 확신이 없으면 아예 영구 설정에 다시 명시하는 편이 낫습니다.

    6-2. SSH 접속이 갑자기 끊긴다

    이건 규칙 문법 문제보다 작업 순서 문제입니다. 보통은 --remove-service=ssh를 너무 일찍 적용했거나, 허용해야 할 출발지 IP를 잘못 넣었거나, 현재 접속 세션이 예상과 다른 경로를 타고 있었던 경우입니다. 예를 들어 평소엔 VPN으로 들어오지만 오늘은 외부 회선에서 접속한 상태라면, 머릿속 관리 IP와 실제 출발지 IP가 다를 수 있습니다.

    • 현재 SSH 세션을 끊지 말고, 새 터미널에서 재접속 테스트를 먼저 합니다.
    • 허용 규칙 추가 후 --remove-service=ssh를 마지막에 실행합니다.
    • 클라우드 서버면 웹 콘솔이나 시리얼 콘솔 경로를 먼저 확보합니다.
    • who, ss -tnp | grep :22 같은 보조 확인으로 현재 세션 상태를 같이 봅니다.

    6-3. 애플리케이션은 떠 있는데 외부 접속이 안 된다

    이럴 때 방화벽만 붙잡고 있으면 오래 갑니다. 실제 원인은 앱이 127.0.0.1에만 바인딩되어 있거나, 컨테이너 프록시와 호스트 포트가 다르거나, 다른 존에 인터페이스가 붙어 있어서 예상한 정책을 안 타는 경우가 많습니다. 즉, 근본 원인은 “포트 개방”과 “서비스 바인딩”을 같은 것으로 보는 데 있습니다.

    sudo ss -tulpn
    sudo firewall-cmd --get-active-zones
    sudo firewall-cmd --zone=public --list-ports
    sudo firewall-cmd --zone=public --list-services

    읽는 기준은 이렇습니다. ss에서 앱이 실제 외부 IP 또는 0.0.0.0에 바인딩되어 있어야 하고, 해당 포트가 맞는 존에서 허용되어 있어야 합니다. 둘 중 하나라도 빠지면 접속이 안 됩니다. 간단해 보이는데, 장애 대응에서는 이 두 층을 분리해서 보지 않아서 시간이 오래 가더라고요.

    6-4. 규칙은 맞는 것 같은데 이상하게 다른 트래픽도 통과한다

    이건 종종 방화벽이 아니라 경로 문제입니다. 예를 들어 애플리케이션이 같은 호스트의 로컬 프록시를 통해 우회하고 있거나, 컨테이너 네트워크 규칙이 별도로 적용되거나, 클라우드 보안 그룹이 firewalld보다 앞단에서 이미 허용하거나 차단하고 있을 수 있습니다. firewalld는 유일한 경계가 아닙니다. 제가 운영 점검할 때 항상 같이 묻는 질문이 “이 트래픽이 진짜 호스트 ingress 경로를 타는가?”입니다.

    7. firewalld 보안 체크리스트의 검증 방법

    보안 설정은 명령 성공으로 끝나면 안 됩니다. 핵심은 “허용된 대상에게만, 필요한 포트만, 예상한 존을 통해 열려 있는가”입니다. 저는 검증을 네 층으로 나눕니다. 방화벽 상태, 프로세스 리슨 상태, 출발지별 접속 결과, 운영 문서와의 일치 여부입니다. 이 네 개가 맞아야 비로소 설정이 끝난 겁니다.

    1. 서버 내부에서 활성 Zone과 허용 목록을 확인합니다.
    2. 프로세스가 실제 어떤 주소/포트에 리슨하는지 확인합니다.
    3. 허용된 네트워크와 비허용 네트워크에서 각각 접속 테스트를 합니다.
    4. 문서화된 서비스 목록과 실제 오픈 포트가 일치하는지 봅니다.
    sudo firewall-cmd --get-active-zones
    sudo firewall-cmd --zone=public --list-all
    sudo firewall-cmd --zone=management --list-all
    sudo ss -tulpn

    외부 테스트는 같은 서버 안이 아니라 관리 PC, 점프 호스트, VPN 외부 클라이언트처럼 서로 다른 출발지에서 해보는 게 좋습니다. 예를 들어 HTTPS는 어디서나 접속되어야 하지만, SSH는 허용된 관리 IP에서만 붙어야 합니다. 허용되지 않은 네트워크에서 SSH가 붙는다면 Rich Rule이나 Zone 매핑이 기대와 다르게 동작하는 겁니다. 반대로 HTTPS가 내부에서만 되고 외부에서 안 되면 firewalld보다 앞단 LB, 보안 그룹, 라우팅까지 같이 봐야 합니다.

    검증 결과 해석 기준도 애매하게 두지 않는 편이 좋습니다.

    • list-all에 문서화하지 않은 서비스나 포트가 보이면 정리 대상입니다.
    • public Zone에 SSH, Cockpit, DB 포트가 남아 있으면 노출 과다로 봅니다.
    • ss -tulpn에서 외부 바인딩된 포트가 의도보다 많으면 앱 설정부터 줄여야 합니다.
    • 허용 대상이 아닌 출발지에서도 접속되면 Zone 매핑, Rich Rule, 앞단 네트워크 정책을 다시 확인합니다.
    • 재부팅 후 결과가 바뀌면 영구 설정 관리에 구멍이 있는 겁니다.

    실무에서는 “포트가 열렸는가”보다 “왜 열려 있는가”를 물어야 합니다. 이 질문이 빠지면 방화벽은 언젠가 예외 규칙 창고가 됩니다. 관련 글로 SELinux, SSH 하드닝, Nginx TLS 점검 가이드도 내부 링크로 함께 묶어두면 검색 유입과 체류 시간 관리에 꽤 도움이 됩니다.

    firewalld 보안 체크리스트 검증 결과를 보여주는 서버 보안 강화 이미지

    허용된 서비스만 남고 관리 포트는 제한된 상태를 검증하는 결과 중심의 대시보드/터미널 이미지입니다.

    8. 운영하면서 유지보수할 때 체크할 항목

    방화벽은 한 번 잠갔다고 끝나는 장치가 아닙니다. 서비스가 늘고 담당자가 바뀌고, 장애 대응 중 임시 예외가 생기면 규칙은 반드시 불어납니다. 그래서 저는 월간 점검이나 배포 체크리스트에 firewalld 보안 체크리스트 항목을 따로 넣습니다. 보안을 강화하는 가장 싼 방법은 새로운 솔루션을 들이는 게 아니라, 이미 열린 예외를 줄이는 겁니다.

    • 사용하지 않는 서비스 제거: 예전에 열었던 포트가 아직 실제 트래픽을 받는지 확인합니다.
    • 출발지 제한 재검토: 퇴역한 사무실 IP, 종료된 VPN 대역이 남아 있지 않은지 봅니다.
    • Zone-인터페이스 매핑 확인: NIC 추가, 이름 변경, 가상 인터페이스 생성 후 매핑이 흐트러지지 않았는지 확인합니다.
    • 앱 배포 문서 동기화: 애플리케이션 포트 변경이 방화벽 정책과 함께 업데이트됐는지 맞춰봅니다.
    • reload 후 재검증: 설정 반영 뒤 실제 접속 테스트까지 끝내야 점검 완료로 봅니다.
    • 커스텀 서비스 정리: /etc/firewalld/services/ 아래 정의가 현재 운영과 맞는지 점검합니다.

    작은 팀일수록 문서화가 귀찮게 느껴질 수 있는데, 방화벽은 문서화하지 않으면 팀이 바뀌는 순간 바로 리스크가 됩니다. 특히 포트를 숫자로만 열어둔 환경은 인수인계 비용이 큽니다. 저는 장기 운영 서비스라면 “서비스명, 포트, 출발지, 이유” 네 가지는 최소한 남겨두는 편이 좋다고 봅니다. 이거 해두면 나중에 진짜 편합니다.

    운영 중 반복 점검해야 할 항목을 한 장으로 압축한 요약 인포그래픽 이미지입니다.

    9. 자주 묻는 질문과 바로 적용할 추천안

    9-1. SSH는 아예 닫는 게 맞을까요?

    콘솔 대체 수단이 안정적으로 있고, 배포 자동화와 장애 복구 루틴이 충분히 성숙했다면 가능합니다. 하지만 대부분의 서버 운영에서는 완전 차단보다 출발지 제한이 더 현실적입니다. 보안과 복구 시간을 같이 봐야 하니까요. 제 추천은 단순합니다. 인터넷 전체 공개는 피하고, 고정 IP 또는 VPN 대역으로 묶으세요.

    9-2. 서비스 추가와 포트 추가 중 뭐가 더 좋나요?

    표준 포트면 서비스가 낫고, 커스텀 앱이면 포트가 빠릅니다. 다만 커스텀 포트가 장기 운영 대상이라면 서비스 XML로 승격하거나 최소한 운영 문서에 의미를 남기세요. “지금 빨리 열기”와 “나중에 이해 가능하기”는 다른 문제입니다.

    9-3. 서버 보안 강화를 시작하는 최소 기준은?

    제가 운영 서버에서 최소선으로 잡는 기준은 이렇습니다. 외부 공개는 HTTPS 중심, SSH는 특정 출발지 제한, 관리망은 공개망과 분리, 불필요한 기본 서비스 제거, reload 후 외부 검증. 이 다섯 개만 지켜도 방화벽 상태는 훨씬 예측 가능해집니다.

    환경별로 추천을 딱 정리해보면 이렇습니다. 단일 NIC 소형 서버라면 public Zone 하나 + HTTPS 공개 + SSH Rich Rule 제한 조합이 가장 실용적입니다. 규칙이 단순하고 운영자가 바뀌어도 이해가 쉽습니다. 반대로 NIC가 둘 이상이거나 관리망이 분리된 서버라면 Zone 분리를 바로 적용하는 편이 낫습니다. public에는 서비스 트래픽만, management에는 관리 트래픽만 남기세요. 이 구조가 예외 규칙 누적을 가장 잘 막아줍니다.

    한 문장으로 요약하면, firewalld 보안 체크리스트의 핵심은 “포트를 여는 기술”이 아니라 “열린 이유를 끝까지 설명할 수 있는 상태를 유지하는 것”입니다. 저는 방화벽을 잘 짠 서버가 보안적으로만 좋은 게 아니라 운영도 덜 아프다고 봅니다. 필요한 것만 열고, 경계를 분리하고, 재부팅 후에도 같은 결과가 나오게 만들고, 마지막엔 실제 접속으로 확인하세요. 이 네 가지를 습관으로 만들면 리눅스 서버는 눈에 띄게 단단해집니다.

  • [Linux] Debian Snap 비교: APT와 Snap 장단점 분석

    [Linux] Debian Snap 비교: APT와 Snap 장단점 분석

    Debian Snap 비교: Debian 환경에서 Snap 사용, 장점과 단점 분석

    Debian Snap 비교를 찾는 분들은 대개 비슷한 고민을 합니다. 시스템은 Debian답게 안정적으로 굴리고 싶은데, 필요한 앱은 APT 저장소 버전이 느리거나 설치 가이드가 Snap 기준으로 적혀 있는 경우가 있거든요. 그래서 결론도 단순하지 않습니다. Snap은 Debian에서 못 쓰는 선택지가 아니라, 운영 방식이 달라지는 선택지에 더 가깝습니다.

    중요한 건 설치 성공 여부보다 그다음입니다. 업데이트를 누가 주도하는지, 장애가 났을 때 어디부터 봐야 하는지, 패키지 하나 추가했을 때 디스크와 서비스 구조가 어떻게 바뀌는지 같은 부분이 운영 체감에 더 크게 남더라고요. 이 글에서는 공식 문서와 실제 운영 관점 기준으로, APT 중심 Debian에 Snap을 얹었을 때 달라지는 지점만 추려서 정리해보겠습니다.

    Debian Snap 비교를 설명하는 APT와 Snap 동작 구조 개요 이미지

    Debian 기본 패키지 관리와 Snap 런타임이 함께 동작하는 전체 구조를 보여주는 개요 이미지입니다.

    왜 Debian Snap 비교가 필요한가

    APT와 Snap의 차이는 패키지 형식 정도로 끝나지 않습니다. APT는 Debian 시스템 전체의 일관성을 전제로 움직입니다. 라이브러리를 공유하고, 관리자가 원하는 시점에 업데이트를 묶어서 적용하고, 서비스 구성도 전통적인 리눅스 흐름과 잘 맞습니다. 반면 Snap은 애플리케이션 단위로 배포와 갱신을 분리하는 방식에 가깝습니다.

    이 구조가 빛나는 순간은 분명 있습니다. 특정 GUI 도구나 개발자 유틸리티를 빨리 검증해야 할 때는 저장소 추가와 충돌 검토 부담을 줄이고 바로 테스트에 들어가기 좋거든요. 반대로 서버 운영에서는 얘기가 달라집니다. 패키지 하나를 설치했는데 <code>snapd 런타임, 자동 갱신 정책, 별도 마운트, 권한 인터페이스까지 함께 관리해야 해서 생각보다 손이 갑니다.

    Snap 패키지 관리란 무엇인가

    Debian에서 Snap을 쓰는 흐름은 보통 이렇습니다. 먼저 apt로 snapd를 설치하고, 이후 애플리케이션 설치와 갱신, 채널 선택, 연결 상태 확인은 snap 명령으로 처리합니다. 즉, Debian에 Snap을 도입한다는 건 APT 위에 별도 배포 계층을 하나 더 올리는 일입니다.

    • APT: 시스템 패키지와 서비스 운영의 기본 축입니다.
    • snapd: Snap 설치, 마운트, 갱신, 인터페이스 연결을 담당하는 데몬입니다.
    • Confinement: 앱이 파일, 장치, 소켓 등에 접근하는 범위를 제한하는 모델입니다.
    • Channel: stable, candidate, beta, edge처럼 배포 리듬을 선택하는 축입니다.

    실무적으로는 정의보다 판단 기준이 더 중요합니다. APT는 시스템에 자연스럽게 녹아드는지 보면 되고, Snap은 이 앱을 시스템과 어느 정도 분리해서 다뤄도 되는지 보면 됩니다. 저는 보통 이 질문 하나로 정리합니다. 운영체제 일부처럼 다룰 패키지인가, 독립 앱처럼 다룰 패키지인가. 전자면 APT, 후자면 Snap 쪽이 더 잘 맞습니다.

    Debian Snap 비교: APT와 뭐가 다를까

    아래 표는 기능 자체보다 운영 영향 중심으로 정리했습니다. 현장에서는 이 차이가 더 크게 느껴집니다.

    항목 APT Snap
    패키지 출처 Debian 저장소, 서드파티 APT 저장소 Snap Store 중심
    업데이트 주도권 관리자가 apt update, apt upgrade 시점을 제어 기본값은 자동 갱신, 필요 시 hold 또는 timer 정책 조정
    의존성 모델 시스템 라이브러리 공유 앱별 번들 또는 base snap 의존
    장애 분석 시작점 apt, dpkg, systemd 로그 snap changes, snap tasks, sudo snap logs, journalctl -u snapd
    디스크 사용 경향 중복이 적어 효율적 리비전 보관과 번들 구조로 커지기 쉬움
    권한 제어 방식 전통적 유닉스 권한과 서비스 계정 중심 인터페이스 연결과 confinement 정책 확인 필요
    서비스 배치감 시스템 패키지와 동일 흐름 별도 마운트와 snap 서비스 단위가 추가됨
    적합한 용도 장기 운영 서버, 일관성 중시 환경 최신 앱 확보, 빠른 검증, 데스크톱 도구

    Debian Snap 비교에서 가장 크게 봐야 할 건 속도보다 통제력입니다. Debian은 원래 패키지 흐름이 예측 가능한 편입니다. 반면 Snap은 최신성 확보가 쉬운 대신, 관리자가 구조를 모르면 왜 이 시점에 갱신됐는지, 왜 설치는 됐는데 장치 접근은 막히는지 같은 질문이 생기기 쉽습니다.

    실전 구현: Debian에 Snap 설치하고 기본 동작 확인하기

    설치 자체는 어렵지 않습니다. 다만 공식 문서 기준으로는 설치 후 경로 반영을 위해 로그아웃 후 다시 로그인하거나 시스템을 재시작하는 단계를 꼭 염두에 두는 게 좋습니다. 이거 빼먹으면 설치는 됐는데 명령이 안 잡혀서 괜히 헷갈리더라고요.

    1. snapd 설치: Debian에 Snap 데몬을 올립니다.
    2. 서비스 상태 확인: 런타임이 정상인지 먼저 봅니다.
    3. 경로 반영 확인: /snap/bin이 PATH에 들어왔는지 확인합니다.
    4. 테스트 Snap 실행: 설치뿐 아니라 실행까지 검증합니다.
    sudo apt update
    sudo apt install -y snapd
    sudo systemctl enable --now snapd.socket
    systemctl status snapd.socket --no-pager
    snap version

    여기서 systemctl status snapd.socket는 최소한 Loaded와 Active를 확인하는 용도로 쓰면 좋습니다. 다만 설치 직후 명령 경로가 바로 반영되지 않을 수 있으니, 세션을 다시 열거나 재부팅한 뒤 테스트하는 편이 더 깔끔합니다. 필요하면 공식 권장 흐름처럼 최신 기능 확보를 위해 아래처럼 snapd snap 자체를 한 번 더 설치하는 방법도 있습니다.

    sudo snap install snapd

    경로 반영은 이렇게 확인하면 됩니다.

    echo "$PATH"
    ls -ld /snap || true
    ls /snap/bin || true
    command -v snap
    command -v hello-world || true

    그다음은 테스트 패키지입니다.

    sudo snap install hello-world
    snap list
    snap run hello-world

    중요한 건 snap list에 이름이 보이는지만 보는 게 아니라, 실제 실행까지 확인하는 겁니다. 설치 메타데이터는 생겼는데 실행 경로나 마운트 단계에서 막히는 경우가 있어서요. 이 단계까지 통과해야 최소 검증이 끝났다고 봐도 무난합니다.

    Debian Snap 비교 실습을 위한 snapd 설치와 검증 터미널 이미지

    Debian 터미널에서 snapd 설치, 서비스 활성화, 테스트 패키지 검증을 수행하는 흐름을 보여주는 이미지입니다.

    조금 더 깊게: 채널, 서비스, 연결 상태 확인

    Snap을 한두 번만 쓸 게 아니라면 install보다 info, services, connections, changes를 익혀두는 편이 훨씬 낫습니다. 설치는 대부분 됩니다. 문제는 그다음에 이상 증상을 어떻게 읽느냐입니다.

    snap info hello-world
    snap services
    snap connections hello-world
    snap changes
    snap refresh --time
    • snap info: 현재 어떤 채널을 추적하는지 확인합니다.
    • snap services: 서비스형 Snap이 어떤 상태인지 봅니다.
    • snap connections: 파일, 네트워크, 장치 접근에 필요한 인터페이스 연결 상태를 점검합니다.
    • snap changes: 설치나 갱신 작업이 중간에 멈췄는지 추적합니다.
    • snap refresh --time: 자동 갱신 주기를 확인합니다.

    여기서 많이 하는 오해가 하나 있습니다. 설치가 성공했으니 실행 문제는 앱 버그라고 단정하는 경우죠. 실제로는 기능 일부만 안 되면 인터페이스 연결부터 의심하는 쪽이 훨씬 빠르고, 설치나 갱신이 걸려 있으면 snap changes와 snap tasks를 먼저 보는 게 맞습니다.

    장점: 언제 Snap이 꽤 쓸 만하냐

    Snap의 장점은 단순히 최신 앱을 빨리 설치할 수 있다는 데서 끝나지 않습니다. 더 중요한 건 배포판 수명주기와 애플리케이션 수명주기를 분리해준다는 점입니다. Debian이 보수적으로 움직여도 특정 앱만 별도 리듬으로 가져갈 수 있다는 뜻이죠. 이거, 데스크톱이나 검증 환경에서는 진짜 편할 때가 있습니다.

    • 최신 버전 확보가 빠릅니다: 배포판 릴리스 주기와 무관하게 앱 공급자가 업데이트를 밀어줄 수 있습니다.
    • 실험 비용이 낮습니다: 저장소 우선순위와 라이브러리 충돌 부담을 줄이고 테스트하기 좋습니다.
    • 앱 단위 회수가 단순합니다: 검증 후 제거 흐름이 비교적 명확합니다.
    • 권한을 구조적으로 나눠 보기 쉽습니다: 모든 보안 문제를 해결하진 않지만, 접근 범위를 한 번 더 점검하게 만듭니다.

    특히 데스크톱 도구, 개발자 유틸리티, 빠른 검증이 필요한 앱에서는 효율이 좋습니다. 반대로 시스템 깊숙이 붙는 핵심 서비스는 Snap의 장점이 생각보다 줄어듭니다. 설치가 쉬운 것과 운영이 쉬운 건 다른 얘기라서요.

    단점: Debian에서 더 크게 느껴지는 지점

    Snap 단점은 Debian에서 더 선명하게 드러나는 편입니다. Ubuntu 계열에서는 비교적 자연스러운 선택지로 보일 수 있지만, Debian에서는 원래 질서 위에 별도 런타임을 추가하는 셈이라 이질감이 있습니다.

    • 업데이트 통제력이 약해질 수 있습니다: 기본값이 자동 갱신이라 운영 창구를 별도로 설계해야 합니다.
    • 디스크 사용이 커지기 쉽습니다: 이전 리비전 보관, base snap, 번들 구조가 겹칩니다.
    • 실행 체감이 일정하지 않을 수 있습니다: 첫 실행이나 초기 마운트 구간에서 전통 패키지와 감각이 다릅니다.
    • 디버깅 동선이 늘어납니다: APT와 systemd만 보던 흐름에서 snapd와 인터페이스 상태까지 확인해야 합니다.
    • 시스템 일관성이 흐려질 수 있습니다: Debian 특유의 단순한 운영 감각을 중시하면 거슬릴 수 있습니다.

    서버에서 특히 보수적으로 보게 되는 이유도 여기에 있습니다. 문제를 빨리 푸는 사람에게는 도구 하나 더 늘어나는 일이 감당 가능한 수준일 수 있습니다. 하지만 장기 운영은 결국 문제가 적게 생기는 구조가 더 강합니다.

    Debian Snap 비교에서 장단점을 보여주는 APT와 Snap 비교 이미지

    APT와 Snap의 업데이트, 격리, 디스크 사용, 운영 편의성을 비교하는 시각 자료입니다.

    운영 관점에서 꼭 점검할 설정

    Snap을 Debian에 올렸다면 설치보다 먼저 갱신 정책과 보관 정책을 확인하는 편이 좋습니다. 특히 서버나 장시간 켜 두는 워크스테이션이라면 기본 자동 갱신을 그대로 둘지 의식적으로 결정해야 합니다.

    snap refresh --time
    sudo snap refresh --hold=24h
    sudo snap set system refresh.timer=mon,02:00-04:00
    sudo snap set system refresh.retain=2
    sudo snap get system refresh.timer
    sudo snap get system refresh.retain

    데스크톱은 기본 자동 갱신을 크게 건드리지 않고, 업무 시간 충돌이 싫을 때 refresh.timer만 조정해도 충분한 경우가 많습니다. 서버는 아예 방치하기보다 refresh.timer로 창을 좁히거나, 검증 전까지 snap refresh --hold를 걸어두는 편이 더 낫습니다. 중요한 건 Snap을 쓰면서도 운영 창구를 직접 만든다는 감각입니다.

    디스크 측면에서는 refresh.retain도 꽤 중요합니다. Snap은 이전 리비전을 남겨 롤백 가능성을 확보하는 대신, 오래 두면 공간을 제법 먹습니다. 저장공간이 빠듯한 VM에서는 초기에 이 값부터 점검하는 게 편하더라고요.

    트러블슈팅: Debian에서 자주 막히는 포인트

    이 섹션은 증상보다 원인 축 위주로 보시면 좋습니다. Debian에서 Snap이 막히는 경우는 대체로 런타임 준비 부족, PATH 문제, 자동 갱신 충돌, 인터페이스 미연결 쪽으로 정리됩니다.

    1. classic confinement 패키지가 설치되지 않는 경우

    일부 Snap은 --classic이 필요합니다. 이때 일부 배포판이나 환경에서는 classic Snap이 /snap 경로를 기대하기 때문에, 해당 경로가 없으면 설치가 막힐 수 있습니다. Debian에서도 환경에 따라 확인할 가치가 있습니다.

    ls -ld /snap
    sudo ln -s /var/lib/snapd/snap /snap
    sudo snap install <package-name> --classic

    다만 이건 항상 필요한 고정 절차는 아닙니다. 먼저 /snap 경로가 이미 있는지 확인하고, classic 설치 오류가 실제로 발생할 때만 적용하는 편이 안전합니다.

    2. 명령은 설치됐는데 실행 파일을 못 찾는 경우

    이건 대개 PATH 반영 문제입니다. 특히 기존 셸 세션을 계속 쓰는 경우 자주 보입니다. 원인은 단순합니다. 링크는 생겼는데 현재 세션의 환경 변수에 /snap/bin이 반영되지 않은 겁니다.

    echo "$PATH"
    ls /snap/bin
    command -v hello-world
    snap run hello-world

    ls /snap/bin에 실행 링크가 있는데 command -v가 비면 PATH 문제일 가능성이 큽니다. 이 경우는 재로그인이나 셸 재시작으로 정리되는 일이 많습니다. 반대로 snap run도 실패하면 경로가 아니라 런타임이나 권한, 마운트 쪽을 봐야 합니다.

    3. 설치나 업데이트가 중간에 걸려 있는 경우

    이럴 때는 감으로 넘기지 말고 작업 이력을 따라가면 됩니다. 원인은 보통 네트워크 문제, 이전 작업 미완료, 마운트 실패, 데몬 상태 이상 쪽입니다.

    snap changes
    snap tasks <change-id>
    journalctl -u snapd --no-pager -n 100
    sudo snap logs snapd -n=50

    snap changes에서 오래 머무는 작업을 찾고, 해당 change-id를 snap tasks로 내려가면 어느 단계에서 멈췄는지 보입니다. 로그는 그냥 읽기보다 store, mount, interface, timeout 같은 키워드로 먼저 분류하면 훨씬 빨리 감이 잡힙니다.

    4. 앱은 실행되는데 일부 기능만 안 되는 경우

    이 경우는 거의 항상 인터페이스나 confinement 쪽입니다. 파일 열기, USB 장치 접근, 특정 시스템 리소스 접근이 안 되는 식으로 보일 때가 많습니다. 얼핏 보면 앱 버그 같지만, 실제로는 필요 권한이 자동 연결되지 않았거나 strict 제약 안에서 막힌 경우가 많습니다.

    snap connections <package-name>
    snap interfaces
    snap info <package-name>

    이 증상이 보이면 재설치부터 반복하기보다 권한 모델을 먼저 확인하는 편이 훨씬 낫습니다. 설치 성공 여부와 기능 완전성은 별개거든요.

    검증: 설치가 끝난 뒤 무엇을 확인해야 하나

    운영 환경에서는 한 번 실행된 걸로 끝내면 부족합니다. 적어도 아래 네 줄기는 보고 넘어가는 편이 안전합니다.

    1. 런타임 준비: systemctl status snapd.socket, 필요 시 snap version
    2. 설치 결과: snap list로 패키지, 채널, 리비전 확인
    3. 업데이트 흐름: snap refresh --time, snap changes
    4. 권한과 실행 경로: snap connections <패키지명>, command -v <명령어>

    합격 기준도 꽤 단순합니다. 설치가 완료됐는지, CLI 또는 GUI가 실제로 실행되는지, 필요 기능이 권한 제약 없이 동작하는지, 재부팅 뒤에도 같은 상태가 유지되는지. 이 네 가지 중 하나라도 흔들리면, 특히 서버에서는 Snap 유지 여부를 다시 보는 편이 낫습니다.

    Debian Snap 비교 후 설치 결과를 검증하는 운영 점검 대시보드 이미지

    설치 후 패키지 목록, 서비스 상태, 변경 이력을 검증하는 운영 점검 화면 이미지입니다.

    어떤 경우에 Snap을 선택하면 좋을까

    여기서는 애매하게 말하지 않겠습니다. Debian Snap 비교의 결론은 의외로 분명합니다.

    상황 추천 이유
    Debian 데스크톱에서 최신 앱이 급함 Snap 우선 검토 배포판 주기와 분리된 최신 버전 확보가 빠름
    짧은 검증용 도구를 올렸다 내릴 예정 Snap 적합 저장소 추가와 충돌 검토 부담이 적음
    장기 운영 서버에서 변경 통제가 최우선 APT 우선 업데이트 타이밍과 시스템 일관성을 잡기 쉬움
    디스크 효율과 단순한 장애 분석이 중요 APT 우선 중복 보관과 별도 런타임 관리 부담이 적음
    앱 단위 격리와 독립 배포가 필요 Snap 후보 confinement와 채널 전략을 함께 가져가기 좋음

    한 줄로 정리하면 이렇습니다. 시스템 일부처럼 다뤄야 하는 패키지는 APT, 독립 앱처럼 다뤄도 되는 패키지는 Snap. 이 기준이 있어야 나중에 운영 기록을 봐도 왜 그 패키지를 Snap으로 골랐는지 설명이 됩니다.

    마무리: 제 추천은 이렇게 정리됩니다

    Debian에 Snap을 붙이는 건 충분히 현실적인 선택입니다. 다만 기본값으로 넓게 쓰기보다는, 필요한 앱에 한해 선택적으로 도입하는 편이 더 안정적입니다. Debian의 장점은 원래 패키지 관리가 조용하고 예측 가능하다는 데 있는데, Snap을 넓게 쓰기 시작하면 최신성은 얻는 대신 그 흐름이 조금 흐려지거든요.

    추천은 명확합니다. 최신 데스크톱 앱, 개발 도구, 빠른 실험이 목적이면 Snap이 꽤 유용합니다. 대신 장기 운영 서버, 작은 디스크, 강한 변경 통제, 단순한 장애 분석이 중요하면 APT를 기본 축으로 두는 편이 낫습니다. 관련해서 패키징 비교 글이 더 필요하시면, 다음에는 Debian 기준 Flatpak과 Snap 비교도 함께 읽어보시면 흐름이 더 잘 잡힙니다.

    Debian Snap 비교를 바탕으로 APT 우선 Snap 보조 전략을 요약한 이미지

    Debian 운영에서 APT를 기본으로 두고 Snap을 보조적으로 선택하는 권장 전략을 요약한 이미지입니다.

  • [Linux] Linux 커널 vRAM 성능 개선: zswap·zram·PSI 가이드

    [Linux] Linux 커널 vRAM 성능 개선: zswap·zram·PSI 가이드

    Linux 커널 vRAM 성능 개선: zswap·zram·PSI 모니터링 가이드

    Linux 커널 vRAM 성능이라는 표현은 늘 헷갈리더라고요. GPU VRAM 얘기로 흘러가기도 하고, 어떤 분은 swap만 떠올리시죠. 이 글에서는 후자, 그러니까 메모리 압박이 왔을 때 커널이 어떤 계층으로 시간을 벌고 그 대가를 CPU·RAM·I/O 어디에 치르는지를 다룹니다. 홈랩에서 VM 밀도를 올리다 보면 메모리 사용률보다 stall이 먼저 서비스 응답을 무너뜨리는 경우가 꽤 자주 보입니다. 그래서 저는 이제 swappiness부터 만지지 않습니다. 먼저 압박이 어디서 발생하는지 보고, 그다음에 zswap인지 zram인지, 마지막으로 정책값을 조정합니다.

    이 주제가 실무에서 어려운 이유도 여기 있습니다. 기능이 많아서라기보다, 증상은 비슷한데 원인은 다를 때가 많기 때문입니다. SwapUsed가 늘어도 괜찮은 경우가 있고, free 메모리가 남아 보이는데도 reclaim이 꼬여 tail latency가 튀는 경우도 있거든요. 그래서 이 글은 정의 설명보다 관측 순서, 선택 기준, 실패 모드의 근본 원인에 초점을 맞춰 정리해보겠습니다.

    Linux 커널 vRAM 성능, PSI, zswap, zram, swap 디스크의 관계를 한눈에 보여주는 개요 이미지입니다.

    1. Linux 커널 vRAM 성능에서 말하는 vRAM의 정체

    운영 현장에서 말하는 vRAM은 공식 커널 용어가 아닙니다. 보통은 디스크 swap에 닿기 전에 메모리 압박을 완충하는 계층을 묶어 부를 때가 많습니다. 이름보다 중요한 건 계층의 위치입니다.

    • swap: 최종 퇴로입니다. 느린 디스크라면 여기서 지연이 바로 드러납니다.
    • zswap: 기존 swap 앞단에 붙는 압축 캐시입니다. 페이지를 바로 디스크로 내보내지 않고 RAM 안의 압축 풀에 잠깐 머물게 합니다.
    • zram: RAM 안에 만드는 압축 블록 디바이스입니다. 그 장치를 swap으로 써서 디스크 접근을 줄이거나 늦춥니다.
    • PSI: 메모리 부족 여부 자체보다, 그 부족이 실제로 태스크를 얼마나 멈추게 했는지 보여줍니다.

    제 판단 기준은 단순합니다. 문제는 메모리 사용량이 아니라 stall의 비용입니다. 페이지 캐시를 적극적으로 쓰는 서버는 메모리가 꽉 차도 정상일 수 있습니다. 반대로 reclaim scan이 많이 돌고 swap I/O가 겹치면 사용자는 CPU나 디스크 수치보다 먼저 응답 지연으로 체감합니다.

    2. Linux 커널 vRAM 성능 모니터링은 사용률보다 압박 형태가 먼저입니다

    대시보드를 열기 전에 커널 기본 인터페이스부터 보는 편이 훨씬 정확합니다. 아래 다섯 군데만 읽어도 방향이 꽤 선명해집니다.

    1. /proc/pressure/memory: 메모리 때문에 작업이 멈춘 시간 비율
    2. /proc/vmstat: pswpin, pswpout, pgscan, pgsteal 같은 reclaim 흔적
    3. /proc/meminfo: MemAvailable, SwapFree 등 현재 상태
    4. /sys/module/zswap/parameters/*: zswap 활성화 여부와 정책값
    5. /sys/block/zram0/*: zram 압축 효율, I/O 이상 징후, 메모리 오버헤드

    아래 명령 정도만 확인해도 1차 진단은 충분합니다.

    uname -r
    cat /proc/pressure/memory
    grep -E 'MemTotal|MemAvailable|SwapTotal|SwapFree' /proc/meminfo
    grep -E 'pswpin|pswpout|pgscan|pgsteal|pgfault|pgmajfault' /proc/vmstat
    vmstat 1 10

    해석은 사용률 표보다 훨씬 냉정하게 하셔야 합니다.

    • memory full이 부하 시 계속 상승: 시스템 전체가 메모리 때문에 전진하지 못하는 상태에 가깝습니다. 단순 부족이라기보다 thrashing 징후로 보는 편이 맞습니다.
    • pgscan은 큰데 pgsteal이 기대만큼 따라오지 않음: 회수 후보는 많이 뒤지는데 건질 게 적다는 뜻입니다.
    • pswpout 증가 + I/O 대기 증가: 디스크 swap 비용이 사용자 지연으로 번질 가능성이 큽니다.
    • SwapUsed 증가만 있고 PSI는 낮음: 꼭 나쁜 신호는 아닙니다. 커널이 완충 계층을 잘 쓰는 상황일 수도 있습니다.

    실제로 자주 보는 시나리오도 이렇습니다. VM 호스트에서 야간 백업과 압축 작업이 겹치면 free 메모리는 아직 남아 보이는데 memory full이 먼저 튑니다. 이유는 단순 메모리 부족이 아니라 익명 메모리와 캐시가 동시에 압박받으면서 reclaim과 writeback이 서로 발목을 잡기 때문입니다. 이때 swappiness만 낮추면 해결되기보다, 익명 메모리를 디스크로 더 늦게 밀어 결국 stall이 더 커질 때도 많습니다.

    3. zswap과 zram은 비슷해 보여도 선택 기준이 다릅니다

    둘 다 압축을 쓰지만, 운영 관점에서는 거의 다른 도구에 가깝습니다. 차이는 기존 swap 경로를 유지하느냐, RAM을 swap 장치로 재구성하느냐에 있습니다.

    옵션 동작 방식 이럴 때 우선 검토 피해야 할 경우 운영 포인트
    swap only 디스크 swap만 사용 구조 단순성이 더 중요할 때, 메모리 압박이 드물 때 느린 스토리지, burst성 메모리 압박이 잦을 때 PSI와 swap I/O 상관관계 확인이 핵심입니다
    zswap swap 직전 페이지를 RAM 압축 풀에 저장 기존 swap은 유지하되 디스크 쓰기를 줄이고 싶을 때, VM 호스트나 일반 서버 CPU가 이미 바쁘고 압축 이득이 낮은 워크로드 compressor, max_pool_percent, accept_threshold_percent, shrinker_enabled를 같이 봅니다
    zram RAM 기반 압축 블록 디바이스를 swap으로 사용 디스크가 약한 미니 PC, 홈랩, 엣지 노드, 임시 burst 흡수가 필요할 때 RAM 자체가 너무 타이트하고 압축률이 낮은 워크로드 comp_algorithm, disksize, mm_stat, mem_used_total, huge_pages를 봅니다

    제 경험상 판단은 이렇게 가져가면 덜 틀립니다. 디스크 swap은 이미 있고 운영 변경 폭을 작게 가져가야 한다면 zswap, 디스크 병목이 뚜렷하거나 swap 장치를 RAM 안에 고립시키는 편이 낫다면 zram입니다. 둘을 동시에 쓸 수도 있지만, 처음부터 같이 만지면 원인 분리가 어려워집니다.

    4. zswap은 기존 swap 경로를 보존하면서 지연을 깎을 때 유리합니다

    운영 서버에서 먼저 검토하는 쪽은 대체로 zswap입니다. swap 체인을 갈아엎지 않고도 디스크 write pressure를 줄일 수 있기 때문이죠. 특히 SSD swap이 있는 VM 호스트나 컨테이너 밀도가 높은 서버에서 접근성이 좋습니다. 다만 배포판 기본값과 커널 설정에 따라 일부 파라미터 노출 여부가 다를 수 있으니, 먼저 파일 존재 여부부터 확인하는 게 안전합니다.

    # 현재 상태 확인
    cat /sys/module/zswap/parameters/enabled
    cat /sys/module/zswap/parameters/compressor
    cat /sys/module/zswap/parameters/max_pool_percent
    cat /sys/module/zswap/parameters/accept_threshold_percent
    cat /sys/module/zswap/parameters/shrinker_enabled
    
    # 런타임 조정 예시
    sudo sh -c 'echo 1 > /sys/module/zswap/parameters/enabled'
    sudo sh -c 'echo lzo > /sys/module/zswap/parameters/compressor'
    sudo sh -c 'echo 20 > /sys/module/zswap/parameters/max_pool_percent'
    sudo sh -c 'echo 80 > /sys/module/zswap/parameters/accept_threshold_percent'
    sudo sh -c 'echo Y > /sys/module/zswap/parameters/shrinker_enabled'

    각 값은 이렇게 해석하시면 됩니다.

    • compressor: 압축률보다 CPU 비용 대비 전체 지연 관점으로 봐야 합니다. CPU 여유가 적은 노드에서는 조금 덜 압축돼도 빠른 알고리즘이 낫습니다.
    • max_pool_percent: 풀이 커지면 디스크 쓰기는 줄어들 수 있지만 RAM 경쟁이 심해질 수 있습니다.
    • accept_threshold_percent: 풀이 한 번 가득 찬 뒤 언제 다시 페이지를 받기 시작할지 정합니다.
    • shrinker_enabled: 차가운 페이지가 zswap에 오래 쌓이는 환경에서는 메모리를 되돌리는 데 도움이 됩니다.

    여기서 흔한 실패는 세 가지입니다. 첫째, 압축 풀이 성능 향상 수단이 아니라 메모리 은닉 수단으로만 쓰이는 경우입니다. 둘째, CPU가 이미 꽉 찬 노드에서 무거운 압축기를 쓰는 경우입니다. 셋째, writeback을 막아놓고 store failure가 반복되는 경우입니다. 이때는 압축되지 않는 페이지를 계속 거절하면서 reclaim 효율이 떨어질 수 있습니다.

    swappiness도 같은 맥락에서 봐야 합니다. 예전처럼 무조건 낮출 값은 아닙니다. 커널 문서 기준으로 vm.swappiness는 0~200 범위를 쓰며, zram이나 zswap 같은 in-memory swap에서는 100을 넘는 값도 검토할 수 있습니다. 즉, 아래 예시는 출발점일 뿐이고 정답처럼 고정하시면 안 됩니다.

    # 현재 정책 확인
    sysctl vm.swappiness
    cat /proc/sys/vm/swappiness
    
    # 실험용 런타임 변경
    sudo sysctl -w vm.swappiness=100
    
    # 영구 반영 예시
    sudo tee /etc/sysctl.d/99-vm-tuning.conf >/dev/null <<'EOF'
    vm.swappiness = 100
    EOF
    sudo sysctl --system

    권고를 짧게 정리하면 이렇습니다. zswap을 켰다면 swappiness를 너무 낮게 고정하지 말고, PSI와 pswpout 패턴을 보면서 다시 잡으세요. 디스크 swap만 있는 서버에서 쓰던 감각을 그대로 가져오면 의외로 손해를 봅니다.

    Linux 커널 vRAM 성능 개선을 위한 zswap 설정과 PSI 모니터링 이미지

    zswap 활성화, compressor 선택, max_pool_percent 조정, PSI 관측 흐름을 함께 보여주는 구성 이미지입니다.

    5. zram은 느린 디스크를 우회할 때 강하지만 RAM 예산 관리가 더 까다롭습니다

    zram은 홈랩이나 저전력 노드에서 체감이 큰 편입니다. 이거 잘 맞으면 진짜 편하더라고요. 다만 장점이 분명한 만큼 함정도 분명합니다. 디스크 I/O를 아끼는 대신 RAM과 CPU를 더 정교하게 써야 한다는 점입니다. 그래서 저는 zram을 켤 때 늘 압축 이득과 메타데이터·단편화 오버헤드를 같이 봅니다.

    # 새 장치 초기화 전에는 comp_algorithm을 먼저 고릅니다.
    sudo modprobe zram num_devices=1
    cat /sys/block/zram0/comp_algorithm
    sudo sh -c 'echo lz4 > /sys/block/zram0/comp_algorithm'
    
    # 크기를 정한 뒤 swap으로 만듭니다.
    sudo sh -c 'echo 8G > /sys/block/zram0/disksize'
    sudo mkswap /dev/zram0
    sudo swapon -p 100 /dev/zram0
    
    # 상태 확인
    zramctl
    cat /sys/block/zram0/mm_stat
    cat /sys/block/zram0/io_stat

    여기서 놓치기 쉬운 실무 포인트가 있습니다.

    • comp_algorithm은 장치 초기화 뒤에는 변경할 수 없습니다. 이미 disksize를 설정했다면 순서를 다시 잡아야 합니다.
    • disksize는 실제 메모리 사용량이 아닙니다. 논리 크기일 뿐이고, 실제 소비는 mm_stat와 mem_used_total을 같이 봐야 감이 옵니다.
    • 우선순위(swapon -p)를 안 잡으면 기대와 다른 swap 경로로 흘러갈 수 있습니다.

    mm_stat는 꼭 읽어보셔야 합니다.

    • orig_data_size: 원본 데이터 크기
    • compr_data_size: 압축 후 데이터 크기
    • mem_used_total: 실제로 zram이 먹는 메모리. 메타데이터와 단편화 비용이 반영됩니다
    • huge_pages: 압축 이득이 낮은 페이지 수를 가늠할 때 참고할 수 있는 값입니다

    판단법은 단순합니다. orig_data_size 대비 compr_data_size가 의미 있게 줄고, 그에 비해 mem_used_total이 과하게 불어나지 않으면 성공에 가깝습니다. 반대로 huge_pages가 계속 늘면 이 워크로드는 압축 친화적이지 않을 가능성이 큽니다. 그 상태에서 zram 크기만 키우면 메모리 버퍼를 늘린 게 아니라 비효율을 키운 것에 가까워집니다.

    zram을 피하는 편이 나은 경우도 분명합니다. 메모리 자체가 매우 타이트하고, 워크로드가 이미 CPU를 바쁘게 쓰며, 페이지 내용이 압축에 잘 안 걸리는 경우입니다. 이런 노드는 오히려 작은 zswap + 보수적 swappiness가 덜 흔들릴 때가 있습니다.

    6. 트러블슈팅은 증상이 아니라 원인 축으로 봐야 빨리 풀립니다

    제가 실무에서 가장 많이 헷갈렸던 지점도 여기였습니다. 겉으로는 다 메모리 문제처럼 보이는데, 실제로 손대야 할 축이 다르거든요.

    • PSI avg10만 보고 안심: 짧은 spike는 평균보다 total 증가와 순간 지연에서 먼저 드러납니다.
    • swap 사용량 증가를 실패로 해석: swap을 얼마나 썼는지가 아니라, 그 결과로 stall이 줄었는지를 봐야 합니다.
    • zswap 파라미터가 안 보임: 커널 설정, 배포판 기본값, 부팅 옵션 차이일 수 있습니다.
    • zram이 있는데 체감 개선이 없음: 압축률 부족, huge_pages 증가, 우선순위 misconfig, 또는 실제 병목이 메모리보다 CPU일 수 있습니다.
    • writeback을 막았더니 오히려 느려짐: 압축 불가 페이지가 반복 거절되며 reclaim만 더 많이 돌기 때문입니다.

    컨테이너 환경에서는 시스템 평균만 봐서는 자주 틀립니다. 어떤 워크로드 하나가 압박을 만들고 있는데 전체 노드는 멀쩡해 보일 수 있거든요. 그래서 cgroup v2 파일을 같이 읽는 편이 좋습니다. 다만 memory.zswap.* 계열은 비교적 최신 커널과 해당 기능 지원이 필요하니, 파일이 없으면 기능 미지원부터 의심하세요.

    CG=/sys/fs/cgroup/my-workload
    cat $CG/memory.pressure
    cat $CG/memory.current
    cat $CG/memory.high
    cat $CG/memory.swap.max
    cat $CG/memory.zswap.current
    cat $CG/memory.zswap.max
    cat $CG/memory.zswap.writeback

    이 파일들의 조합이 중요한 이유는 시스템 전체 메모리 정책과 워크로드별 완충 정책이 서로 다를 수 있기 때문입니다.

    • memory.high: OOM 전에 압박을 일찍 걸어 reclaim을 유도합니다.
    • memory.swap.max: cgroup 단위 swap 허용 범위를 제한합니다.
    • memory.zswap.max: zswap 사용량의 상한입니다.
    • memory.zswap.writeback: 0으로 두면 zswap writeback과 store failure에 따른 swapout 경로를 막습니다. 대신 반복 거절이 생기면 reclaim 효율이 무너질 수 있습니다.

    권하는 방식은 시스템 전체 평균 대신 문제가 되는 cgroup의 memory.pressure와 zswap 사용량을 같이 보는 것입니다. 멀티테넌트 환경에서는 전체 PSI보다 특정 서비스 cgroup의 압박이 장애와 더 직접적으로 연결될 때가 많습니다.

    7. 검증은 swap 숫자가 아니라 지연 구조가 바뀌었는지로 끝냅니다

    설정 적용 후에 보는 순서도 정해두는 편이 좋습니다. 안 그러면 우연한 캐시 상태를 개선으로 착각하기 쉽습니다.

    1. 부하 전후 /proc/pressure/memory의 some, full, total 비교
    2. /proc/vmstat에서 pswpout, pgscan, pgsteal 패턴 비교
    3. zram이면 mm_stat에서 압축 이득과 실제 메모리 사용량 확인
    4. zswap이면 pool 크기 정책과 cgroup별 사용량, writeback 정책 확인
    5. 마지막으로 애플리케이션 지연 로그나 요청 처리 시간과 맞춰 보기

    좋은 방향의 신호는 대체로 이렇습니다.

    • 메모리 PSI의 full 증가 속도가 둔화됩니다.
    • swap activity가 남아 있어도 응답 지연의 튐이 줄어듭니다.
    • zram의 compr_data_size가 의미 있게 작고, mem_used_total이 통제됩니다.
    • 특정 cgroup의 압박이 다른 워크로드로 덜 번집니다.

    반대로 실패 신호도 분명합니다. swap은 줄었는데 PSI가 그대로 높다, zram 사용량은 늘었는데 huge_pages도 함께 오른다, writeback을 막은 뒤 reclaim scan만 폭증한다. 이런 경우는 설정을 더 밀어붙일 게 아니라, 선택 자체를 다시 봐야 합니다.

    Linux 커널 vRAM 성능 개선 결과를 보여주는 PSI와 zram zswap 대시보드 이미지

    메모리 PSI, swap activity, zram 압축 효율, cgroup 단위 메모리 압박 개선 결과를 시각화한 대시보드 이미지입니다.

    8. 운영 판단을 바로 내릴 수 있게 시나리오별로 끊어보겠습니다

    Q1. swappiness는 낮을수록 좋은가요?

    그렇게 보면 자주 틀립니다. 디스크 swap만 주로 쓰는 서버라면 낮은 값이 도움이 될 수 있습니다. 하지만 zswap이나 zram처럼 메모리 안쪽에 완충 계층이 있을 때는 너무 낮은 값이 익명 메모리를 오래 붙잡아 reclaim 비용을 키울 수 있습니다. 실무에서는 결국 사용률이 아니라 PSI와 지연으로 판단하는 게 덜 틀립니다.

    Q2. zswap과 zram 중 무엇부터 볼까요?

    운영 변경 폭을 작게 가져가야 하는 일반 서버, VM 호스트, SSD swap 환경이면 zswap부터 보시면 됩니다. 디스크가 약한 미니 PC, 홈랩, 엣지 노드라면 zram이 먼저입니다. 둘 다 가능해도 처음엔 한 단계씩 바꾸세요. 그래야 어느 계층이 실제 이득을 냈는지 분리해서 볼 수 있습니다.

    Q3. 컨테이너 환경에서는 무엇이 가장 중요할까요?

    노드 평균보다 cgroup별 pressure와 상한입니다. memory.high, memory.swap.max, memory.zswap.max, memory.zswap.writeback를 같이 보셔야 합니다. 평균이 멀쩡해도 특정 서비스 하나만 느려지는 이유가 여기서 자주 나옵니다.

    Q4. 언제 쓰지 말아야 하나요?

    CPU가 이미 바쁘고, 압축률이 낮고, 메모리 예산도 빠듯한 노드에서는 과한 압축 계층이 손해일 수 있습니다. 이런 경우는 zram을 크게 잡기보다, zswap을 보수적으로 두거나 메모리 상한 정책부터 정리하는 편이 낫습니다.

    Linux 커널 vRAM 성능 선택 기준을 정리한 zswap zram 비교 인포그래픽

    zswap과 zram의 선택 기준, Linux 커널 vRAM 성능 모니터링 체크포인트, 상황별 추천안을 정리한 요약 이미지입니다.

    실무 권고를 딱 잘라 정리하면 이렇습니다.

    • SSD swap이 있고 서버 역할이 많은 시스템: zswap부터 켜고, compressor와 max_pool_percent를 보수적으로 시작한 뒤 PSI와 pswpout으로 확인하세요.
    • 저전력 홈랩, 미니 PC, 느린 디스크: zram swap을 우선 검토하세요. 다만 mm_stat에서 mem_used_total과 huge_pages를 꼭 같이 보셔야 합니다.
    • 컨테이너 밀도가 높은 환경: 시스템 전체 평균보다 memory.pressure, memory.high, memory.swap.max, memory.zswap.*를 우선 보세요.
    • CPU 여유가 적은 노드: 압축률보다 압축 비용을 먼저 계산하세요. 디스크 I/O 절감이 CPU 손실을 상쇄하지 못하면 방향을 다시 잡아야 합니다.

    제가 이 주제에서 끝까지 붙드는 한 문장은 이것입니다. Linux 커널 vRAM 성능 개선은 swap 숫자 줄이기 게임이 아니라, stall이 발생하는 위치를 바꾸고 그 비용을 통제하는 작업이라는 점입니다. 그래서 설정값 자체보다 PSI, reclaim 흔적, cgroup 경계, 압축 계층의 실제 이득이 더 중요합니다.

    관련 글도 함께 보면 이해가 빨라집니다. 예를 들어 Linux 메모리 관리, 커널 튜닝, 성능 모니터링 주제를 내부 문서로 이어두면 운영 기준을 잡는 데 도움이 됩니다. 한 줄 추천으로 마무리하면, 범용 서버는 zswap부터, 느린 디스크 노드는 zram부터, 멀티테넌트 환경은 cgroup pressure부터 보시면 됩니다. 그다음에야 swappiness가 의미를 갖습니다.

  • [Proxmox] Proxmox LLM Ollama 성능 측정 방법론: 재현 가능한 벤치마크

    [Proxmox] Proxmox LLM Ollama 성능 측정 방법론: 재현 가능한 벤치마크

    Proxmox LLM Ollama 환경에서 LLM 추론 성능 측정 방법론

    Proxmox LLM Ollama 조합으로 홈랩 AI 서버를 굴리다 보면, 제일 먼저 막히는 지점이 성능 자체보다 성능 해석이더라고요. 응답이 느릴 때 GPU 가속 경로가 문제인지, 모델 파일을 읽어오는 스토리지가 문제인지, 아니면 VM의 CPU 배치와 메모리 상태가 흔들리는지 한 번에 안 보입니다. 저도 처음엔 모델만 바꿔 가며 체감으로 판단했는데, 그 방식은 기록이 안 남고 원인 분리도 어렵더라고요. 이 글은 Proxmox + Ollama 조합에서 재현 가능한 LLM 성능 측정 방법을 만드는 데 집중합니다.

    핵심은 단순합니다. 총 응답 시간만 보면 거의 항상 잘못된 결론으로 갑니다. Ollama가 내려주는 load_duration, prompt_eval_duration, eval_duration을 분리하고, 같은 시점의 CPU, 메모리, 디스크, GPU 상태를 같이 봐야 합니다. 홈랩에서는 특히 이 구분이 중요합니다. 같은 하드웨어라도 다른 VM의 백업 작업, 메모리 ballooning, 스토리지 캐시 상태 때문에 결과가 꽤 쉽게 흔들리거든요.

    Proxmox 호스트와 게스트 VM, GPU 패스스루, Ollama API, 모니터링 명령 흐름을 한눈에 보는 구성도입니다.

    왜 Proxmox LLM Ollama 환경은 따로 측정해야 할까요?

    베어메탈에서 잘 나오던 수치가 Proxmox VM에서는 다르게 나오는 이유는 단순히 가상화 오버헤드 한 줄로 끝나지 않습니다. 실제로는 가상화 계층이 병목을 숨기거나 증폭하는 방식이 더 문제예요. 예를 들어 GPU 패스스루가 붙어 있어도 VM의 CPU affinity나 NUMA 배치가 어긋나면 생성 구간이 늘어질 수 있고, 모델 파일이 느린 스토리지에 있으면 첫 호출만 유난히 길어집니다. 이걸 분리하지 않으면 괜히 GPU만 탓하게 됩니다.

    • GPU Passthrough 상태: 장치가 VM에 보이는 것과 실제로 추론 가속에 쓰이는 건 다릅니다.
    • CPU affinity와 NUMA 배치: vCPU가 불리한 코어 배치에 놓이면 토큰 생성 지연이 출렁일 수 있습니다.
    • 스토리지 지연시간: 모델 교체가 잦을수록 첫 로딩 시간이 과장되기 쉽습니다.
    • ballooning, swap, 메모리 압박: 평소엔 티가 안 나다가 긴 프롬프트에서 갑자기 드러납니다.
    • Ollama 메트릭 해석 실수: 로딩 시간과 생성 시간을 한 덩어리로 보면 튜닝 방향이 엇나갑니다.

    제가 기준으로 삼는 문장은 하나입니다. 벤치마크는 순위를 매기는 작업이 아니라, 손해가 나는 구간을 특정하는 작업입니다. 이 관점으로 가면 Proxmox 위 LLM 성능 측정이 훨씬 덜 꼬입니다.

    LLM 성능 측정에서 반드시 분리해야 할 항목

    Ollama는 API 응답에 꽤 유용한 타이밍 필드를 포함합니다. 문제는 이 숫자를 읽는 방식입니다. 저는 최소한 아래 네 축을 분리하지 않으면 비교값으로 잘 안 봅니다.

    항목 의미 이 수치가 커질 때 먼저 볼 것 실무 해석
    load_duration 모델을 메모리로 올리는 시간 스토리지, 모델 상주 여부, 첫 호출 여부 첫 호출만 크면 정상 특성일 수 있지만, 반복 호출에서도 크면 적재가 반복되거나 메모리 압박이 있다는 뜻입니다.
    prompt_eval_duration 입력 프롬프트 토큰 처리 시간 프롬프트 길이, 컨텍스트 길이, CPU 상태 RAG나 긴 시스템 프롬프트를 쓰는 환경에서는 이 값이 체감 대기시간의 큰 비중을 차지할 수 있습니다.
    eval_duration 출력 토큰 생성 시간 GPU 가속, CPU 병목, 모델 크기 토큰/초 계산의 핵심 구간입니다. 장비 비교는 대부분 이 값 중심으로 보는 편이 맞습니다.
    시스템 지표 CPU, 메모리, 디스크, GPU 사용량 vmstat, iostat, nvidia-smi API 숫자만으로는 원인을 못 찍습니다. 같은 시점의 시스템 지표가 있어야 근본 원인을 좁힐 수 있습니다.

    여기서 많이 놓치는 포인트가 하나 있습니다. 짧은 프롬프트에서 빠른 모델이 실제 운영 워크로드에서도 빠르다는 보장은 없습니다. 특히 홈랩에서 RAG를 붙이거나 시스템 프롬프트가 길어지면 prompt_eval_duration이 병목으로 튀어나오는 경우가 적지 않았습니다. 그래서 저는 측정 목적을 먼저 나눕니다. 장비 비교인지, 실제 응답 대기시간 확인인지, GPU 패스스루 검증인지에 따라 봐야 할 값이 달라집니다.

    측정 전에 Proxmox 쪽에서 먼저 고정할 것들

    게스트 VM 안에서만 계측을 정교하게 해도, Proxmox 설정이 흔들리면 결과는 들쭉날쭉해집니다. 측정 전에 아래 조건부터 고정해두는 편이 낫습니다.

    1. 같은 모델 태그, 같은 프롬프트, 같은 출력 길이 조건으로 반복합니다.
    2. 테스트 시간대에는 다른 VM의 백업, 미디어 인덱싱, 대용량 복사 같은 잡음을 줄입니다.
    3. GPU 패스스루를 쓴다면 항상 같은 VM, 같은 게스트 커널 상태에서만 비교합니다.
    4. ballooning target, swap 개입 여부를 먼저 확인하고, 가능하면 벤치 구간에서는 메모리 조건을 고정합니다.
    5. 모델 파일 위치가 로컬 SSD인지, 네트워크 스토리지인지, ZFS ARC나 페이지 캐시 영향이 있는지 기록합니다.

    여기서 실전적으로 중요한 구분이 cold run과 warm run입니다. 첫 요청은 적재 비용이 섞이고, 두 번째부터는 메모리 상주 효과가 붙습니다. 둘을 한 표에 섞어두면 얼핏 데이터가 많아 보여도 해석에는 별 도움이 안 됩니다.

    Proxmox 쪽에서 제가 특히 먼저 확인하는 건 세 가지입니다. CPU affinity, NUMA 사용 여부, ballooning 조건 고정 여부입니다. CPU 배치가 계속 흔들리면 짧은 벤치에서는 티가 덜 나도 반복 측정 편차가 커집니다. ballooning도 운영 효율에는 도움이 되지만, 추론 성능 비교에는 변수로 끼어들 수 있습니다. 홈랩에서는 운영 효율과 벤치 재현성이 종종 충돌하는데, 벤치 시간에는 재현성을 우선하는 편이 낫더라고요.

    실전 구현 1: Ollama API로 응답 시간과 토큰 생성 속도 수집

    CLI 체감은 참고용으로는 괜찮지만, 기록용 벤치마크에는 조금 아쉽습니다. 반복 측정과 후처리를 생각하면 Ollama API의 원본 메트릭을 그대로 저장하는 편이 훨씬 낫습니다. 아래처럼 한 번 호출해서 핵심 필드를 바로 계산해보면 감이 빨리 옵니다.

    curl -s http://127.0.0.1:11434/api/generate \
      -H 'Content-Type: application/json' \
      -d '{
        "model": "llama3:8b",
        "prompt": "Explain the difference between CPU bottleneck and GPU bottleneck in one short paragraph.",
        "stream": false,
        "options": {
          "temperature": 0,
          "num_predict": 128
        }
      }' | jq '{
        model,
        created_at,
        total_duration_ns: .total_duration,
        load_duration_ns: .load_duration,
        prompt_eval_count,
        prompt_eval_duration_ns: .prompt_eval_duration,
        eval_count,
        eval_duration_ns: .eval_duration,
        tokens_per_second: (if .eval_duration > 0 then (.eval_count / (.eval_duration / 1000000000)) else 0 end)
      }'

    여기서 temperature를 0으로 두는 이유는 성능 측정에서 출력 다양성을 줄이기 위해서입니다. num_predict를 고정하는 이유도 같고요. 벤치마크에서 가장 흔한 실수가 출력 길이를 매번 다르게 두는 건데, 그렇게 되면 eval_count가 흔들리고 결국 토큰/초 비교도 흐려집니다.

    반복 측정은 최소 5회 정도는 권합니다. 평균만 보지 말고 최솟값, 최댓값, 편차까지 같이 남겨야 합니다. 홈랩에서는 한 번 빠르게 나온 결과보다 얼마나 덜 흔들리는지가 더 중요할 때가 많거든요.

    for i in 1 2 3 4 5; do
      curl -s http://127.0.0.1:11434/api/generate \
        -H 'Content-Type: application/json' \
        -d '{
          "model": "llama3:8b",
          "prompt": "Write exactly five bullet points about Linux memory pressure.",
          "stream": false,
          "options": {
            "temperature": 0,
            "num_predict": 96
          }
        }' | jq -r --arg run "$i" '[
          $run,
          .created_at,
          .total_duration,
          .load_duration,
          .prompt_eval_count,
          .prompt_eval_duration,
          .eval_count,
          .eval_duration,
          (if .eval_duration > 0 then (.eval_count / (.eval_duration / 1000000000)) else 0 end)
        ] | @csv'
    done >> ollama-bench.csv

    이 CSV 로그는 보기보다 강력합니다. 나중에 VM 설정을 바꿨을 때, 모델 파일 위치를 옮겼을 때, GPU를 교체했을 때 비교 기준이 그대로 남습니다. 저는 이 단계에서 대시보드부터 만들지 않는 편을 권합니다. 원본 로그가 안정적으로 쌓이는 체계가 먼저고, 시각화는 그 다음이더라고요.

    하나 더요. 벤치마크 목적이라면 stream: false를 유지하는 편이 낫습니다. 스트리밍 출력은 체감 속도 확인에는 좋지만, 수집 포맷이 지저분해지고 메트릭 해석이 섞이기 쉽습니다. 체감용 테스트와 기록용 테스트를 분리해두면 나중에 덜 꼬입니다.

    Ollama 벤치마크에서 LLM 성능 측정 지표를 설명하는 이미지

    Ollama API 응답에서 어떤 필드를 뽑아 성능 지표로 쓰는지 설명하는 시각 자료입니다.

    실전 구현 2: 시스템 자원 사용량을 같이 보세요

    Ollama API 숫자만 보면 원인 추정이 자꾸 빗나갑니다. 같은 요청이라도 CPU 압박인지, 디스크 대기인지, GPU가 실제로 일하는지 구분이 안 되기 때문입니다. 저는 최소한 아래 세 축은 같이 봅니다.

    vmstat 1
    
    iostat -xz 1
    
    nvidia-smi dmon -s pucm
    

    읽는 기준은 단순하지만, 해석은 꽤 실전적입니다.

    • vmstat 1: r가 계속 높고 si/so가 움직이면 CPU 경쟁이나 swap 개입을 먼저 의심합니다.
    • iostat -xz 1: 모델 로딩 시점에 await와 %util이 튀면 load_duration 상승과 연결해서 봅니다.
    • nvidia-smi dmon -s pucm: 생성 중에도 GPU 사용률과 메모리 사용량이 거의 반응하지 않으면 GPU 가속 경로를 다시 확인합니다.

    제가 자주 보는 실패 패턴은 이겁니다. GPU는 VM에서 보이는데, 실제 생성 구간에서는 GPU가 거의 일하지 않는 경우예요. 이때는 패스스루 성공과 추론 가속 성공을 분리해서 봐야 합니다. 장치 인식 자체는 성공했지만 드라이버 상태, 메모리 부족, 런타임 제약 때문에 기대한 만큼 가속이 안 붙는 경우가 있습니다. 반대로 GPU 사용률이 높다고 바로 좋은 것도 아닙니다. 메모리 이동 비용이나 CPU 병목이 남아 있으면 eval_duration 개선이 생각보다 작을 수 있습니다.

    재현 가능한 시나리오: cold run과 warm run 분리 측정

    제가 홈랩에서 가장 자주 쓰는 측정 패턴은 cold run과 warm run을 아예 별도 시나리오로 나누는 방식입니다. 같은 모델, 같은 프롬프트여도 첫 요청과 반복 요청은 성격이 다릅니다. 이 둘을 분리하면 적어도 로딩 비용과 생성 비용은 꽤 깔끔하게 갈라집니다.

    1. Ollama 서비스를 재시작하거나 충분히 유휴 상태를 만든 뒤 첫 요청을 보냅니다.
    2. 첫 요청의 load_duration, total_duration을 기록합니다.
    3. 같은 프롬프트를 3~5회 반복하고, 이 구간에서는 eval_duration과 토큰/초를 중심으로 봅니다.
    4. 동시에 vmstat, iostat, nvidia-smi dmon의 변화를 같은 시각 기준으로 맞춰둡니다.

    실제로는 아래처럼 분리 저장해두면 후처리가 편합니다.

    PROMPT='Explain why swap activity can distort LLM benchmark results in a VM.'
    MODEL='llama3:8b'
    
    # cold run
    curl -s http://127.0.0.1:11434/api/generate \
      -H 'Content-Type: application/json' \
      -d "{\"model\":\"${MODEL}\",\"prompt\":\"${PROMPT}\",\"stream\":false,\"options\":{\"temperature\":0,\"num_predict\":120}}" \
      | jq -r '["cold", .total_duration, .load_duration, .prompt_eval_duration, .eval_duration, .eval_count] | @csv' \
      >> bench-split.csv
    
    # warm runs
    for i in 1 2 3; do
      curl -s http://127.0.0.1:11434/api/generate \
        -H 'Content-Type: application/json' \
        -d "{\"model\":\"${MODEL}\",\"prompt\":\"${PROMPT}\",\"stream\":false,\"options\":{\"temperature\":0,\"num_predict\":120}}" \
        | jq -r --arg run "warm-${i}" '[$run, .total_duration, .load_duration, .prompt_eval_duration, .eval_duration, .eval_count] | @csv' \
        >> bench-split.csv
    done

    이 방식의 장점은 단순한데 실용적입니다. 첫 요청만 느리면 적재 비용 성격, 반복 요청도 계속 느리면 생성 경로 병목으로 빠르게 가설을 세울 수 있습니다. 홈랩에서는 이 구분만 제대로 해도 괜히 GPU부터 바꾸는 일을 꽤 줄일 수 있더라고요.

    홈랩 AI 서버에서 Proxmox와 Ollama 벤치마크를 수행하는 터미널 이미지

    cold run과 warm run을 비교하면서 시스템 자원 모니터링을 병행하는 실전 측정 화면 예시입니다.

    VM, LXC, 베어메탈 중 무엇을 기준점으로 삼아야 할까요?

    이 질문은 자주 나오는데, 저는 목적에 따라 답을 다르게 합니다. 무조건 하나가 낫다고 하기보다 무엇을 검증하려는지 먼저 정하는 편이 맞습니다.

    환경 이럴 때 추천 장점 주의할 점
    베어메탈 절대 성능 기준점이 필요할 때 가상화 변수가 적어 기준선 만들기 좋습니다. 실제 운영 환경이 Proxmox라면 결과 이식성이 떨어질 수 있습니다.
    Proxmox VM GPU 패스스루와 재현성 검증이 목적일 때 운영 환경과 같은 조건으로 측정하기 좋습니다. CPU affinity, NUMA, ballooning 같은 변수를 같이 관리해야 합니다.
    LXC 경량화와 운영 편의가 더 중요할 때 리소스 오버헤드가 적고 관리가 가볍습니다. GPU 활용 방식과 권한 구성이 환경에 따라 더 예민할 수 있어, 벤치 기준점으로는 다소 까다롭습니다.

    제 판단 기준은 이렇습니다. 운영도 Proxmox VM에서 할 예정이면 VM에서 잰 수치를 기준으로 삼는 편이 맞습니다. 베어메탈 수치가 더 좋아도 실제 배포 환경과 다르면 의사결정에는 덜 도움이 됩니다. 반대로 이 하드웨어가 어디까지 나오는지 보고 싶다면 베어메탈 기준선을 한 번 찍어두는 게 좋습니다.

    자주 겪는 문제와 해결 방법

    이 부분은 공식 문서만 봐서는 감이 잘 안 오는 영역입니다. 실제로 벤치를 돌리면 아래처럼 삐끗하는 경우가 많습니다.

    1. 프롬프트 길이가 매번 달라서 비교가 무너집니다

    prompt_eval_duration은 입력 길이에 직접 반응합니다. 질문을 바꾸거나 출력 길이를 열어두면, 모델 자체보다 워크로드 차이가 더 크게 반영됩니다. 벤치 프롬프트는 고정, 출력 길이는 num_predict 등으로 제한하는 편이 낫습니다.

    2. 스트리밍 체감 속도를 성능으로 착각합니다

    터미널에 토큰이 빨리 흘러나오면 체감상 빨라 보입니다. 그런데 그건 계측 기준으로는 불안정합니다. 수집용 벤치는 stream: false로 고정하고, 체감 테스트는 별도로 분리하세요. 둘을 섞어두면 나중에 기록이 비교 불가능해집니다.

    3. 디스크 병목이 숨어 있는데 GPU만 의심합니다

    모델을 자주 바꾸며 테스트할 때 특히 그렇습니다. load_duration이 갑자기 커지고 같은 시점 iostat의 await가 치솟는다면, 그건 GPU보다 모델 적재 경로를 먼저 봐야 합니다. 홈랩에서는 NVMe, SATA SSD, 네트워크 스토리지 차이가 생각보다 크게 드러납니다.

    4. Proxmox에서 다른 VM 부하가 끼어듭니다

    백업 작업, 미디어 트랜스코딩, ZFS scrub, 테스트용 쿠버네티스 VM이 동시에 돌면 반복 측정 편차가 커집니다. 이런 경우 평균값보다 분산이 커지는 패턴이 먼저 나타납니다. 숫자가 안 예쁜 게 아니라, 측정 조건이 불안정한 겁니다.

    5. GPU 사용률만 높고 성능 향상은 미미합니다

    이건 대개 GPU 연산 자체보다 CPU 준비 단계, 메모리 이동, 컨텍스트 처리가 먼저 막히는 경우입니다. 그래서 저는 GPU utilization을 목표값으로 보지 않습니다. eval_duration이 실제로 줄었는지를 먼저 확인합니다.

    6. 첫 요청이 계속 느린데 warm run처럼 해석합니다

    메모리 압박이나 unload 정책 때문에 모델이 자주 내려가는 환경에서는 매번 사실상 cold run이 됩니다. 이때는 첫 요청만 느린 건 정상이라고 넘기면 안 됩니다. 반복 호출인데도 load_duration이 매번 크게 잡히는지를 꼭 확인해보세요. 그건 상주 실패일 가능성이 큽니다.

    검증과 결과 해석: 숫자를 어떻게 읽어야 할까요?

    숫자를 예쁘게 정리하는 것보다 중요한 건, 어떤 패턴에서 어떤 결론을 내릴지 미리 정해두는 겁니다. 저는 보통 아래처럼 읽습니다.

    • load_duration만 크다: 모델 적재, 스토리지, 최초 호출 비용을 먼저 봅니다.
    • prompt_eval_duration이 길다: 긴 프롬프트, 큰 컨텍스트, CPU 토큰 처리 경로를 의심합니다.
    • eval_duration이 길다: 실제 생성 성능 문제입니다. GPU 가속 상태, CPU 할당, 모델 크기 적합성을 봐야 합니다.
    • GPU 사용률이 낮고 CPU만 바쁘다: 패스스루 성공 여부보다 실제 추론 가속 경로를 재확인합니다.
    • 반복 측정 편차가 크다: 다른 VM 간섭, 메모리 압박, 백그라운드 I/O를 먼저 정리합니다.

    여기서 중요한 결정 포인트가 있습니다. 무엇을 최적화할지 먼저 정해야 수치 해석이 쉬워집니다. 사용자 체감 첫 응답을 줄이는 게 목표라면 total_duration과 load_duration이 더 중요합니다. 장비 비교가 목적이면 eval_duration과 토큰/초가 핵심이고요. 긴 컨텍스트 운영이 목표라면 prompt_eval_duration이 병목인지부터 봐야 합니다.

    로그 포맷은 거창할 필요 없습니다. 다만 나중에 조건을 복원할 수 있을 만큼은 남겨두는 편이 좋습니다.

    timestamp,host,vm,model,prompt_name,run_type,total_duration,load_duration,prompt_eval_count,prompt_eval_duration,eval_count,eval_duration
    

    이 한 줄이 중요한 이유는 숫자와 환경을 같이 묶어두기 때문입니다. 벤치 결과만 남기고 VM 설정 이력을 빼먹으면 몇 주 뒤에는 왜 빨라졌는지, 왜 느려졌는지 복기가 잘 안 됩니다. 관련해서 Proxmox GPU 패스스루 설정 글이나 Ollama 설치 가이드를 같이 정리해두면 나중에 내부 링크 연결에도 꽤 유용합니다.

    Proxmox Ollama 환경의 LLM 추론 성능 결과를 시각화한 대시보드 이미지

    모델 로딩 시간과 실제 생성 시간을 분리해 해석하는 결과 대시보드 예시입니다.

    운영 팁: 측정 환경을 문서화해야 나중에 안 꼬입니다

    벤치마크는 숫자보다 맥락이 먼저입니다. 특히 홈랩 AI 서버는 장비를 자주 만지게 되니까, 어떤 조건에서 잰 수치인지 생각보다 빨리 잊어버립니다. 최소한 아래 항목은 같이 기록해두는 편이 좋습니다.

    • 모델 이름과 태그
    • 게스트 OS 종류와 커널 상태
    • vCPU 개수, 메모리 크기
    • GPU 패스스루 유무와 장치 종류
    • 모델 파일 저장 위치
    • 테스트 프롬프트 이름
    • cold run인지 warm run인지

    여기서 제 권장 방식은 단순합니다. 설정 변경이 있는 날에는 벤치마크를 다시 찍고, 변경 사유를 한 줄 메모로 남겨두세요. CPU affinity를 바꿨는지, 메모리를 늘렸는지, 모델 저장 위치를 옮겼는지 정도만 적어도 나중에 비교가 됩니다. 숫자만 예쁘게 모아두는 것보다 이게 훨씬 실용적이더라고요.

    FAQ: 현장에서 많이 헷갈리는 부분

    Q. Proxmox LLM 테스트는 VM이 낫나요, LXC가 낫나요?

    GPU 패스스루 검증과 재현성을 우선하면 저는 VM 쪽을 더 자주 씁니다. 운영 편의만 보면 LXC가 가벼울 수 있지만, 벤치 기준점을 만들 때는 변수를 줄이기 쉬운 쪽이 VM이었습니다.

    Q. Ollama 벤치마크는 CLI만으로 충분한가요?

    단발성 확인은 가능합니다. 다만 반복 측정, CSV 저장, 후처리, cold/warm 구분까지 생각하면 API 기반 수집이 훨씬 낫습니다.

    Q. 숫자는 좋은데 체감은 별로입니다

    짧은 프롬프트 위주로만 재면 실제 운영 패턴과 어긋날 수 있습니다. 특히 RAG, 긴 시스템 프롬프트, 멀티턴 대화가 붙으면 prompt_eval_duration 비중이 커집니다. 운영 워크로드와 비슷한 입력으로 다시 재보는 게 맞습니다.

    Q. 토큰/초가 높으면 무조건 좋은 건가요?

    장비 비교에는 유용하지만, 첫 응답 대기시간이 중요한 환경에서는 부족합니다. 사용자가 느끼는 체감은 종종 load_duration과 prompt_eval_duration에 더 크게 좌우됩니다.

    마지막으로: 이런 상황이면 이렇게 고르시면 됩니다

    제가 실제로 추천하는 선택 기준은 아래처럼 명확합니다.

    • 장비 비교가 목적: 같은 프롬프트와 같은 출력 길이로 고정하고 eval_duration과 토큰/초 중심으로 보세요.
    • 첫 응답 체감이 중요: load_duration을 포함한 total_duration을 같이 봐야 합니다. 모델 상주 전략과 스토리지 위치가 더 중요해질 수 있습니다.
    • GPU 튜닝이 목적: Ollama API 결과만 보지 말고 nvidia-smi dmon과 vmstat를 반드시 붙이세요.
    • 운영 전 검증이 목적: CSV 형태로 누적 저장하고, cold run과 warm run을 분리 기록하세요.
    • 결과가 자꾸 흔들린다: 모델을 바꾸기 전에 다른 VM 간섭, ballooning, swap, 스토리지 대기를 먼저 정리하세요.

    조금 더 직설적으로 말하면 이렇습니다. 첫 응답만 느리면 스토리지와 적재 전략부터, 반복 응답도 느리면 CPU/GPU 경로부터, 숫자가 출렁이면 Proxmox 자원 간섭부터 보시면 됩니다. 이 순서가 실제로 가장 덜 돌아가는 편이었습니다.

    제 경험상, Ollama 숫자만 보고 결론 내리면 절반은 빗나가고, Proxmox 호스트와 게스트의 시스템 지표까지 같이 보면 비로소 조정 포인트가 보입니다. Proxmox LLM Ollama 벤치마크는 장비 스펙보다 측정 설계가 더 중요할 때가 많습니다. 한 번만 잘 만들어두면, 그다음부터는 모델 교체든 VM 튜닝이든 훨씬 덜 감으로 움직이게 됩니다.

    Proxmox와 Ollama 기반 GPU 가속 성능 측정 요약 인포그래픽

    Proxmox 기반 Ollama 벤치마크 절차와 해석 기준을 요약한 인포그래픽입니다.