Go로 크로스 플랫폼 WiFi 스캐너
Go로 크로스 플랫폼 WiFi 스캐너 만들기
개발을 하다 보면 현재 접속 가능한 WiFi 네트워크 목록을 프로그래밍적으로 가져와야 할 때가 있습니다. 네트워크 관련 도구를 만들거나, IoT 디바이스 설정 프로그램을 개발하거나, 단순히 현재 주변의 WiFi 환경을 분석하고 싶을 때 말이죠.
오늘은 Go 언어를 사용해서 Windows, macOS, Linux 모든 운영체제에서 동작하는 WiFi 스캐너를 만들어보겠습니다. 각 운영체제의 네이티브 명령어를 활용하면서도, 깔끔한 Go 코드로 통합하는 방법을 살펴보겠습니다.
왜 Go를 선택했을까?
Go는 크로스 플랫폼 애플리케이션을 만들기에 완벽한 언어입니다. 단일 바이너리로 컴파일되고, 런타임 의존성이 적으며, 강력한 표준 라이브러리를 제공합니다. 특히 os/exec 패키지를 통해 시스템 명령어를 쉽게 실행할 수 있어서, 각 운영체제의 네이티브 WiFi 스캐닝 도구들을 활용하기에 적합합니다.
설계 철학
우리가 만들 WiFi 스캐너는 다음과 같은 원칙을 따릅니다:
- 크로스 플랫폼 호환성: 하나의 코드베이스로 모든 주요 운영체제 지원
- 확장 가능한 아키텍처: 새로운 스캐닝 방법을 쉽게 추가할 수 있는 구조
- 강건한 에러 처리: 각 운영체제별 다양한 상황에 대응
- 사용자 친화적: 명확한 출력과 유용한 기능들
핵심 구조체와 인터페이스
먼저 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에서는 두 가지 접근 방법을 사용합니다:
- airport 유틸리티: 더 상세한 정보 제공
- 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)
// 사용자 상호작용 기능들...
}
에러 처리와 예외 상황
실제 운영 환경에서는 다양한 예외 상황이 발생할 수 있습니다:
- 권한 부족: Linux에서 일부 명령어는 sudo 권한이 필요
- 도구 없음: 특정 명령어가 설치되지 않은 경우
- WiFi 어댑터 없음: 데스크톱 PC처럼 WiFi 기능이 없는 경우
- 출력 형식 변경: 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에서는 여러 명령어를 순차적으로 시도하므로 더욱 그렇습니다. 성능을 개선하기 위한 몇 가지 방법:
- 컨텍스트 타임아웃: 각 명령어 실행에 타임아웃 설정
- 캐싱: 짧은 시간 내의 중복 요청은 캐시된 결과 반환
- 병렬 처리: 여러 방법을 동시에 시도하고 가장 먼저 성공한 결과 사용
실제 사용 시나리오
이 WiFi 스캐너는 다음과 같은 상황에서 유용합니다:
- 네트워크 진단 도구: 현재 WiFi 환경 분석
- IoT 디바이스 설정: WiFi 연결 설정 프로그램
- 보안 감사: 주변 WiFi 네트워크의 보안 설정 확인
- 네트워크 모니터링: 정기적인 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 정보만 수집하지만, 다음과 같은 기능들을 추가할 수 있습니다:
- 연결 시도: 발견된 네트워크에 자동으로 연결
- 주기적 모니터링: 일정 간격으로 스캔하고 변화 감지
- 웹 인터페이스: HTTP 서버로 만들어 웹에서 접근
- 데이터베이스 저장: 시계열 데이터로 저장하고 분석
결론
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 네트워크를 깔끔하게 볼 수 있습니다. 한 번 시도해보세요!