개발/go

Go로 크로스 플랫폼 WiFi 스캐너

xwing 2025. 5. 28. 11:04

Go로 크로스 플랫폼 WiFi 스캐너 만들기

개발을 하다 보면 현재 접속 가능한 WiFi 네트워크 목록을 프로그래밍적으로 가져와야 할 때가 있습니다. 네트워크 관련 도구를 만들거나, IoT 디바이스 설정 프로그램을 개발하거나, 단순히 현재 주변의 WiFi 환경을 분석하고 싶을 때 말이죠.

오늘은 Go 언어를 사용해서 Windows, macOS, Linux 모든 운영체제에서 동작하는 WiFi 스캐너를 만들어보겠습니다. 각 운영체제의 네이티브 명령어를 활용하면서도, 깔끔한 Go 코드로 통합하는 방법을 살펴보겠습니다.

왜 Go를 선택했을까?

Go는 크로스 플랫폼 애플리케이션을 만들기에 완벽한 언어입니다. 단일 바이너리로 컴파일되고, 런타임 의존성이 적으며, 강력한 표준 라이브러리를 제공합니다. 특히 os/exec 패키지를 통해 시스템 명령어를 쉽게 실행할 수 있어서, 각 운영체제의 네이티브 WiFi 스캐닝 도구들을 활용하기에 적합합니다.

설계 철학

우리가 만들 WiFi 스캐너는 다음과 같은 원칙을 따릅니다:

  1. 크로스 플랫폼 호환성: 하나의 코드베이스로 모든 주요 운영체제 지원
  2. 확장 가능한 아키텍처: 새로운 스캐닝 방법을 쉽게 추가할 수 있는 구조
  3. 강건한 에러 처리: 각 운영체제별 다양한 상황에 대응
  4. 사용자 친화적: 명확한 출력과 유용한 기능들

핵심 구조체와 인터페이스

먼저 WiFi 네트워크 정보를 담을 구조체를 정의합니다:

type WiFiNetwork struct {
    SSID     string `json:"ssid"`
    Signal   int    `json:"signal"`
    Security string `json:"security"`
    Channel  int    `json:"channel,omitempty"`
    BSSID    string `json:"bssid,omitempty"`
}

그리고 각 운영체제별 구현체가 따라야 할 인터페이스를 정의합니다:

type WiFiScanner interface {
    ScanNetworks() ([]WiFiNetwork, error)
}

이런 설계를 통해 운영체제별로 다른 구현체를 만들면서도, 사용하는 쪽에서는 동일한 인터페이스로 접근할 수 있습니다.

Windows 구현: netsh 활용하기

Windows에서는 netsh 명령어를 사용합니다. 이는 Windows에 기본으로 설치된 네트워크 관리 도구입니다:

func (w *WindowsScanner) ScanNetworks() ([]WiFiNetwork, error) {
    cmd := exec.Command("netsh", "wlan", "show", "profiles")
    output, err := cmd.Output()
    if err != nil {
        return nil, fmt.Errorf("netsh 명령 실행 실패: %v", err)
    }

    var networks []WiFiNetwork
    lines := strings.Split(string(output), "\n")
    
    for _, line := range lines {
        line = strings.TrimSpace(line)
        if strings.Contains(line, "모든 사용자 프로필") || 
           strings.Contains(line, "All User Profile") {
            // SSID 추출 로직
            // ...
        }
    }
    
    return networks, nil
}

Windows의 경우 netsh wlan show profiles로 저장된 프로필을 가져오고, netsh wlan show interface로 현재 사용 가능한 네트워크를 스캔합니다.

macOS 구현: airport와 networksetup

macOS에서는 두 가지 접근 방법을 사용합니다:

  1. airport 유틸리티: 더 상세한 정보 제공
  2. networksetup: airport가 실패할 경우의 대안
func (m *MacScanner) ScanNetworks() ([]WiFiNetwork, error) {
    // airport 유틸리티 시도
    cmd := exec.Command("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport", "-s")
    output, err := cmd.Output()
    if err != nil {
        // 실패 시 networksetup 사용
        return m.scanWithNetworkSetup()
    }

    return parseMacAirportOutput(string(output))
}

macOS는 보안이 강화되면서 WiFi 스캐닝에 제약이 생겼지만, 여전히 이 방법들로 기본적인 정보를 얻을 수 있습니다.

Linux 구현: 다양한 도구 지원

Linux는 배포판과 환경에 따라 사용 가능한 도구가 다르므로, 여러 방법을 시도합니다:

func (l *LinuxScanner) ScanNetworks() ([]WiFiNetwork, error) {
    // 1. nmcli (NetworkManager) 시도
    networks, err := l.scanWithNmcli()
    if err == nil && len(networks) > 0 {
        return networks, nil
    }

    // 2. iwlist 시도
    networks, err = l.scanWithIwlist()
    if err == nil && len(networks) > 0 {
        return networks, nil
    }

    // 3. iw 시도
    return l.scanWithIw()
}

각각의 도구는 다른 출력 형식을 가지므로, 별도의 파싱 함수가 필요합니다:

func parseNmcliOutput(output string) ([]WiFiNetwork, error) {
    var networks []WiFiNetwork
    lines := strings.Split(output, "\n")
    
    for _, line := range lines {
        parts := strings.Split(line, ":")
        if len(parts) >= 3 {
            ssid := strings.TrimSpace(parts[0])
            signalStr := strings.TrimSpace(parts[1])
            security := strings.TrimSpace(parts[2])
            
            signal, _ := strconv.Atoi(signalStr)
            
            network := WiFiNetwork{
                SSID:     ssid,
                Signal:   signal,
                Security: security,
            }
            networks = append(networks, network)
        }
    }
    
    return networks, nil
}

팩토리 패턴으로 운영체제 감지

사용자는 복잡한 구현 세부사항을 알 필요 없이, 간단하게 스캐너를 생성할 수 있어야 합니다:

func GetWiFiScanner() WiFiScanner {
    switch runtime.GOOS {
    case "windows":
        return &WindowsScanner{}
    case "darwin":
        return &MacScanner{}
    case "linux":
        return &LinuxScanner{}
    default:
        return &LinuxScanner{} // 기본값
    }
}

사용자 경험 향상하기

단순히 데이터를 가져오는 것뿐만 아니라, 사용자가 편리하게 사용할 수 있는 기능들을 추가했습니다:

1. 신호 강도순 정렬

func SortBySignalStrength(networks []WiFiNetwork) {
    sort.Slice(networks, func(i, j int) bool {
        return networks[i].Signal > networks[j].Signal
    })
}

2. SSID 필터링

func FilterBySSID(networks []WiFiNetwork, ssid string) []WiFiNetwork {
    var filtered []WiFiNetwork
    for _, network := range networks {
        if strings.Contains(strings.ToLower(network.SSID), 
                           strings.ToLower(ssid)) {
            filtered = append(filtered, network)
        }
    }
    return filtered
}

3. 예쁜 테이블 출력

func PrintNetworks(networks []WiFiNetwork) {
    fmt.Printf("%-30s %-8s %-10s\n", "SSID", "Signal", "Security")
    fmt.Println(strings.Repeat("-", 50))
    
    for _, network := range networks {
        fmt.Printf("%-30s %-8d %-10s\n", 
                  network.SSID, network.Signal, network.Security)
    }
}

4. JSON 저장 기능

func SaveToJSON(networks []WiFiNetwork, filename string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")
    return encoder.Encode(networks)
}

실제 사용 예시

메인 함수에서는 이 모든 기능을 조합해서 사용자 친화적인 인터페이스를 제공합니다:

func main() {
    fmt.Println("WiFi 네트워크 스캔 중...")
    
    scanner := GetWiFiScanner()
    networks, err := scanner.ScanNetworks()
    if err != nil {
        fmt.Printf("WiFi 스캔 실패: %v\n", err)
        return
    }
    
    // 신호 강도순으로 정렬
    SortBySignalStrength(networks)
    
    fmt.Printf("\n발견된 WiFi 네트워크: %d개\n\n", len(networks))
    PrintNetworks(networks)
    
    // 사용자 상호작용 기능들...
}

에러 처리와 예외 상황

실제 운영 환경에서는 다양한 예외 상황이 발생할 수 있습니다:

  1. 권한 부족: Linux에서 일부 명령어는 sudo 권한이 필요
  2. 도구 없음: 특정 명령어가 설치되지 않은 경우
  3. WiFi 어댑터 없음: 데스크톱 PC처럼 WiFi 기능이 없는 경우
  4. 출력 형식 변경: OS 업데이트로 명령어 출력이 바뀌는 경우

이런 상황들을 처리하기 위해 다음과 같은 전략을 사용합니다:

if err != nil {
    fmt.Printf("WiFi 스캔 실패: %v\n", err)
    
    // 수동 명령어 가이드 제공
    fmt.Println("\n다음 명령어를 수동으로 실행해보세요:")
    switch runtime.GOOS {
    case "windows":
        fmt.Println("netsh wlan show profiles")
    case "darwin":
        fmt.Println("networksetup -scanwirelessnetworks en0")
    case "linux":
        fmt.Println("nmcli dev wifi 또는 iwlist scan")
    }
    return
}

성능 고려사항

WiFi 스캐닝은 시간이 걸리는 작업입니다. 특히 Linux에서는 여러 명령어를 순차적으로 시도하므로 더욱 그렇습니다. 성능을 개선하기 위한 몇 가지 방법:

  1. 컨텍스트 타임아웃: 각 명령어 실행에 타임아웃 설정
  2. 캐싱: 짧은 시간 내의 중복 요청은 캐시된 결과 반환
  3. 병렬 처리: 여러 방법을 동시에 시도하고 가장 먼저 성공한 결과 사용

실제 사용 시나리오

이 WiFi 스캐너는 다음과 같은 상황에서 유용합니다:

  1. 네트워크 진단 도구: 현재 WiFi 환경 분석
  2. IoT 디바이스 설정: WiFi 연결 설정 프로그램
  3. 보안 감사: 주변 WiFi 네트워크의 보안 설정 확인
  4. 네트워크 모니터링: 정기적인 WiFi 환경 변화 추적

빌드와 배포

Go의 크로스 컴파일 기능을 활용하면 한 번에 모든 플랫폼용 바이너리를 생성할 수 있습니다:

# Windows용
GOOS=windows GOARCH=amd64 go build -o wifi-scanner-windows.exe

# macOS용
GOOS=darwin GOARCH=amd64 go build -o wifi-scanner-macos

# Linux용
GOOS=linux GOARCH=amd64 go build -o wifi-scanner-linux

확장 가능성

현재 구현은 기본적인 WiFi 정보만 수집하지만, 다음과 같은 기능들을 추가할 수 있습니다:

  1. 연결 시도: 발견된 네트워크에 자동으로 연결
  2. 주기적 모니터링: 일정 간격으로 스캔하고 변화 감지
  3. 웹 인터페이스: HTTP 서버로 만들어 웹에서 접근
  4. 데이터베이스 저장: 시계열 데이터로 저장하고 분석

결론

Go 언어의 강력한 표준 라이브러리와 크로스 플랫폼 특성을 활용하면, 복잡한 시스템 레벨 작업도 깔끔하고 유지보수하기 쉬운 코드로 구현할 수 있습니다.

이번 WiFi 스캐너 프로젝트를 통해 다음을 배울 수 있었습니다:

  • 인터페이스 패턴으로 플랫폼별 구현체 추상화
  • 팩토리 패턴으로 런타임 객체 생성
  • 명령어 실행과 출력 파싱 기법
  • 사용자 경험을 고려한 CLI 설계

무엇보다 Go의 "간단함이 복잡함을 이긴다"는 철학이 실제 프로젝트에서 어떻게 구현되는지 보여주는 좋은 예시라고 생각합니다. 여러분도 이 코드를 기반으로 자신만의 네트워크 도구를 만들어보시기 바랍니다!

전체 코드

완전한 코드는 다음과 같습니다:

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "os"
    "os/exec"
    "regexp"
    "runtime"
    "sort"
    "strconv"
    "strings"
)

// WiFiNetwork WiFi 네트워크 정보를 저장하는 구조체
type WiFiNetwork struct {
    SSID     string `json:"ssid"`
    Signal   int    `json:"signal"`
    Security string `json:"security"`
    Channel  int    `json:"channel,omitempty"`
    BSSID    string `json:"bssid,omitempty"`
}

// WiFiScanner WiFi 스캐너 인터페이스
type WiFiScanner interface {
    ScanNetworks() ([]WiFiNetwork, error)
}

// 각 플랫폼별 구현체와 핵심 로직들...
// (전체 코드는 너무 길어서 핵심 부분만 표시)

func main() {
    fmt.Println("WiFi 네트워크 스캔 중...")
    
    scanner := GetWiFiScanner()
    networks, err := scanner.ScanNetworks()
    if err != nil {
        fmt.Printf("WiFi 스캔 실패: %v\n", err)
        return
    }
    
    SortBySignalStrength(networks)
    fmt.Printf("\n발견된 WiFi 네트워크: %d개\n\n", len(networks))
    PrintNetworks(networks)
    
    // 추가 기능들...
}

이제 go run main.go만 실행하면 현재 주변의 모든 WiFi 네트워크를 깔끔하게 볼 수 있습니다. 한 번 시도해보세요!