요금제

아키텍처 개요

관련 소스 파일

이 페이지의 내용은 다음 소스 파일을 기반으로 생성되었습니다:

res-downloader는 네트워크 리소스 스니핑과 고속 다운로드 기능을 통합한 크로스 플랫폼 데스크톱 애플리케이션이다. 이 프로젝트는 Go 언어로 작성된 백엔드와 Vue.js 기반 프론트엔드가 Wails 프레임워크를 통해 긴밀하게 결합된 하이브리드 아키텍처를 채택하고 있다.

전체 기술 스택 및 애플리케이션 진입점

이 프로젝트는 Wails v2 프레임워크를 기반으로 구축되었으며, Go 백엔드와 Vue 3 프론트엔드가 단일 바이너리로 패키징된다. 메인 진입점인 main.go:1-56에서 애플리케이션의 핵심 초기화가 수행된다. 여기에는 에셋 서버 구성, 메뉴 설정, 윈도우 옵션(크기, 프레임리스 모드 등)이 포함된다.

Go 백엔드의 핵심 의존성은 go.mod:1-42에 정의되어 있으며, 주요 모듈로는 GUI 프레임워크인 wails/v2, MITM 프록시 구현을 위한 goproxy, 구조화된 로깅을 위한 zerolog 등이 있다.

프론트엔드 진입점인 frontend/src/main.ts:1-14에서는 Vue 3 애플리케이셝始化가 이루어지며, Pinia 상태 관리, i18n 다국어 지원, Vue Router가 초기화된다.

正在加载图表渲染器...

아키텍처 다이어그램 설명:

  1. 빌드 타임 구조: //go:embed 지시문을 통해 프론트엔드 빌드 산출물이 Go 바이너리에 임베드된다 (main.go:20-21)
  2. 백엔드 모듈: App 싱글톤을 중심으로 Proxy, HttpServer, FileDownloader가 협력한다
  3. 프론트엔드 구조: Pinia Store가 중앙 집중식 상태 관리를 담당하며, Wails Bindings를 통해 백엔드와 통신한다
  4. Wails 런타임: AssetServer가 임베드된 프론트엔드 리소스를 서빙하고, IPC 브리지가 Go-JavaScript 간 통신을 중계한다

백엔드 아키텍처 모듈화된 코어 구조

백엔드는 core/ 디렉토리 내에 모듈화된 구조로 설계되어 있다. 각 모듈은 싱글톤 패턴을 통해 관리되며, init() 함수를 통해 초기화 순서가 제어된다.

App 모듈: 애플리케이션 생명주기 관리

core/app.go:42-172에 정의된 App 구조체는 애플리케이션의 핵심 상태와 생명주기를 관리한다. 주요 필드로는 앱 이름, 버전, 내장 CA 인증서, 사용자 디렉토리 경로 등이 있다.

go
1type App struct {
2    AppName     string
3    Version     string
4    PublicCrt   []byte
5    PrivateKey  []byte
6    IsProxy     bool
7    ctx         context.Context
8}

Startup(ctx context.Context) 메서드는 Wails 애플리케이션이 시작될 때 호출되며, 내부 HTTP 서버를 고루틴으로 실행한다 (core/app.go:129-132). OnExit() 메서드는 애플리케이션 종료 시 시스템 프록시 설정을 해제하고 로거를 종료한다 (core/app.go:134-141).

Proxy 모듈: MITM 프록시 서버

core/proxy.go:25-110goproxy 라이브러리를 활용한 MITM(Man-in-the-Middle) 프록시 서버를 구현한다. 이 모듈의 핵심 기능은:

  1. CA 인증서 구성: 내장된 인증서를 로드하여 HTTPS 트래픽 가로채기를 가능하게 한다 (core/proxy.go:95-108)
  2. 플러그인 레지스트리: 도메인별로 다른 처리 로직을 적용할 수 있는 플러그인 시스템을 제공한다 (core/proxy.go:26-62)
  3. MITM 규칙 엔진: RuleSet을 통해 어떤 호스트에 대해 MITM을 수행할지 결정한다 (core/proxy.go:84-89)
go
1p.Proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
2    if ruleOnce.shouldMitm(host) {
3        return goproxy.MitmConnect, host
4    }
5    return goproxy.OkConnect, host
6})

HttpServer 모듈: 로컬 API 서버

core/http.go:29-94는 로컬 HTTP 서버를 구현하여 프론트엔드와의 통신 채널을 제공한다. 이 서버는 두 가지 역할을 수행한다:

  1. API 요청 처리: 127.0.0.1:포트로 오는 요청을 API 핸들러로 라우팅한다 (core/http.go:45-46)
  2. 프록시 요청 전달: 그 외 요청은 MITM 프록시로 전달한다 (core/http.go:48)
go
1if r.Host == "127.0.0.1:"+globalConfig.Port && HandleApi(w, r) {
2    // API 처리됨
3} else {
4    proxyOnce.Proxy.ServeHTTP(w, r) // 프록시
5}

모듈 간 의존 관계

正在加载图表渲染器...

의존 관계 설명:

  1. App → Config/Proxy/HttpServer/Logger/SystemSetup: App이 핵심 모듈들을 초기화하고 조율한다
  2. Proxy → RuleSet/Config: 프록시는 규칙 엔진과 설정을 참조하여 MITM 대상을 결정한다
  3. HttpServer → Proxy/App: HTTP 서버는 요청을 프록시로 전달하거나 App의 상태에 접근한다
  4. Config → Storage: 설정은 파일 시스템 스토리지를 통해 영속화된다
  5. SystemSetup → AESCipher/App: 시스템 설정은 암호화 모듈을 사용하며 App의 인증서에 접근한다

데이터 흐름 및 프론트엔드-백엔드 통신

Wails 바인딩 메커니즘

Wails 프레임워크는 Go 메서드를 JavaScript에서 호출 가능한 함수로 자동 노출하는 바인딩 시스템을 제공한다. core/bind.go:9-25에 정의된 Bind 구조체는 프론트엔드에 노출할 메서드들을 그룹화한다.

go
1type Bind struct{}
2
3func (b *Bind) Config() *ResponseData {
4    return httpServerOnce.buildResp(1, "ok", globalConfig)
5}
6
7func (b *Bind) AppInfo() *ResponseData {
8    return httpServerOnce.buildResp(1, "ok", appOnce)
9}
10
11func (b *Bind) ResetApp() {
12    appOnce.IsReset = true
13    runtime.Quit(appOnce.ctx)
14}

이 메서드들은 frontend/wailsjs/go/core/Bind.js:1-15에서 자동 생성된 JavaScript 래퍼를 통해 호출된다.

javascript
1export function AppInfo() {
2  return window['go']['core']['Bind']['AppInfo']();
3}
4
5export function Config() {
6  return window['go']['core']['Bind']['Config']();
7}

Pinia 상태 관리

frontend/src/stores/index.ts:1-99는 Pinia를 활용한 중앙 집중식 상태 관리를 구현한다. 이 스토어는 앱 정보, 전역 설정, 프록시 상태 등을 관리하며 백엔드와의 통신을 추상화한다.

typescript
1export const useIndexStore = defineStore("index-store", () => {
2    const appInfo = ref<appType.App>({...})
3    const globalConfig = ref<appType.Config>({...})
4    const isProxy = ref(false)
5    
6    const init = async () => {
7        // Wails 바인딩을 통해 백엔드에서 데이터 로드
8        const res = await bind.AppInfo()
9        appInfo.value = res.data
10        // ...
11    }
12    
13    const openProxy = async () => {
14        return appApi.openSystemProxy().then(handleProxy)
15    }
16})

이벤트 기반 데이터 흡수

프록시 모듈에서 감지된 미디어 리소스는 브릿지 패턴을 통해 HttpServer로 전달되고, 이를 프론트엔드에 실시간으로 푸시한다. core/proxy.go:51-53에서 Send 콜백이 정의된다:

go
1Send: func(t string, data interface{}) {
2    httpServerOnce.send(t, data)
3},
正在加载图表渲染器...

시퀀스 다이어그램 설명:

  1. 요청 가로채기: 사용자 브라우저의 HTTPS 요청이 로컬 프록시를 통해 전달된다
  2. MITM 복호화: 프록시는 내장 CA 인증서를 사용하여 트래픽을 복호화한다 (core/proxy.go:95-108)
  3. 플러그인 분석: 도메인별 플러그인이 응답에서 미디어 URL을 추출한다 (core/proxy.go:56-61)
  4. 이벤트 푸시: 추출된 데이터가 HttpServer를 통해 프론트엔드로 전송된다
  5. 상태 업데이트: Pinia Store가 새로운 리소스를 상태에 추가하고 UI가 갱신된다

핵심 기능 모듈 다운로더 및 보안

FileDownloader: 멀티파트 다운로드

core/downloader.go:55-147는 범용 파일 다운로더를 구현한다. 주요 특징은:

  1. 컨텍스트 기반 취소: context.Context를 통해 다운로드 중단을 지원한다 (core/downloader.go:57-68)
  2. 프록시 지원: 업스트림 프록시를 통한 다운로드가 가능하다 (core/downloader.go:135-140)
  3. 헤더 필터링: 특정 헤더(인코딩, 호스트 등)를 자동으로 제거하여 다운로드 안정성을 높인다 (core/downloader.go:85-107)
go
1var forbiddenDownloadHeaders = map[string]struct{}{
2    "accept-encoding":   {},
3    "content-length":    {},
4    "host":              {},
5    "connection":        {},
6    // ...
7}
8
9func (fd *FileDownloader) setHeaders(request *http.Request) {
10    for key, value := range fd.Headers {
11        if globalConfig.UseHeaders == "default" {
12            lk := strings.ToLower(key)
13            if _, forbidden := forbiddenDownloadHeaders[lk]; forbidden {
14                continue
15            }
16            request.Header.Set(key, value)
17        }
18    }
19}

AESCipher: 민감 데이터 암호화

core/aes.go:16-72는 AES-CBC 모드를 사용한 암호화/복호화를 제공한다. 이 모듈은 주로 비밀번호 캐싱에 사용된다.

go
1func (a *AESCipher) Encrypt(plainText string) (string, error) {
2    block, err := aes.NewCipher(a.key)
3    // PKCS7 패딩
4    padding := block.BlockSize() - len(plainText)%block.BlockSize()
5    // IV 생성 및 암호화
6    mode := cipher.NewCBCEncrypter(block, iv)
7    mode.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))
8    return base64.StdEncoding.EncodeToString(cipherText), nil
9}

암호화된 비밀번호는 core/system.go:45-57에서 파일로 저장되며, 1개월이 지나면 자동으로 삭제된다 (core/system.go:66-70).

RuleSet: 유연한 MITM 필터링

core/rule.go:22-126는 도메인 기반 MITM 규칙 엔진을 구현한다. 규칙 문법은 다음을 지원한다:

  1. 와일드카드: *.example.com 형식으로 서브도메인 매칭
  2. 부정 규칙: !example.com 형식으로 특정 도메인 제외
  3. 전역 규칙: * 로 모든 도메인에 MITM 적용
go
1func (r *RuleSet) shouldMitm(host string) bool {
2    action := false
3    for _, rule := range r.rules {
4        if rule.isAll {
5            action = !rule.isNeg
6            continue
7        }
8        if rule.isWildcard {
9            if h == rule.domain || strings.HasSuffix(h, "."+rule.domain) {
10                action = !rule.isNeg
11            }
12            continue
13        }
14        if h == rule.domain {
15            action = !rule.isNeg
16        }
17    }
18    return action
19}

설정 및 영속성 관리

Config: 중앙 집중식 설정 관리

core/config.go:46-221는 애플리케이션의 모든 설정을 관리한다. 기본 설정값은 다음과 같다:

설정 항목기본값설명
Host127.0.0.1로컬 서버 바인딩 주소
Port8899로컬 서버 포트
TaskNumberCPU 코어 × 2동시 다운로드 작업 수
DownNumber3단일 파일 분할 다운로드 수
UserAgentChrome 129기본 User-Agent
Rule*MITM 대상 규칙 (기본: 모든 도메인)

MIME 타입 매핑은 core/config.go:119-157에 정의되어 있으며, 이미지, 오디오, 비디오 등 40개 이상의 미디어 타입을 지원한다.

Storage: 파일 시스템 추상화

core/storage.go:1-41는 범용 파일 스토리지 계층을 제공한다. 이 모듈은 설정 파일, 캐시 데이터 등의 영속성을 담당한다.

go
1type Storage struct {
2    fileName string
3    def      []byte  // 기본값
4}
5
6func (l *Storage) Load() ([]byte, error) {
7    if !shared.FileExist(l.fileName) {
8        // 파일이 없으면 기본값으로 생성
9        err := os.WriteFile(l.fileName, l.def, 0644)
10        return l.def, nil
11    }
12    return os.ReadFile(l.fileName)
13}

Logger: 구조화된 로깅

core/logger.go:1-68는 Zerolog를 활용한 구조화된 로깅 시스템을 구현한다. 프로덕션 모드에서는 파일로, 개발 모드에서는 콘솔로 출력된다.

go
1func NewLogger(logFile bool, logPath string) *Logger {
2    var out io.Writer
3    if logFile {
4        logfile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
5        out = logfile
6    } else {
7        out = os.Stdout
8    }
9    logger.Logger = zerolog.New(zerolog.ConsoleWriter{
10        Out:        out,
11        TimeFormat: "2006-01-02 15:04:05",
12    }).With().Timestamp().Logger()
13    return logger
14}

핵심 설계 결정 및 기술적 트레이드오프

1. 싱글톤 패턴 채택

모든 코어 모듈(App, Proxy, HttpServer, Config 등)이 싱글톤으로 구현되었다. 이는 전역 상태 공유를 단순화하지만, 테스트 가능성을 저하시킬 수 있다. 대신 init() 함수를 통한 초기화 순서 제어로 모듈 간 의존성을 명시적으로 관리한다.

2. MITM 프록시 내장

CA 인증서를 애플리케이션에 내장하고, 사용자 시스템에 프록시 설정을 자동으로 구성하는 방식을 선택했다. 이는 HTTPS 트래픽 분석을 가능하게 하지만, 보안 감사가 필요한 환경에서는 제한될 수 있다.

3. Wails 프레임워크 선택

Electron 대신 Wails를 선택함으로써:

  • 장점: 더 작은 바이너리 크기, 낮은 메모리 사용량, Go 생태계 활용
  • 단점: Node.js 생태계 호환성 제한, 더 작은 커뮤니티

4. 플러그인 아키텍처

도메인별 리소스 추출 로직을 플러그인으로 분리했다 (core/proxy.go:27-30). 이는 새로운 플랫폼 지원을 쉽게 추가할 수 있게 하지만, 브릿지 패턴으로 인한 간접 호출 오버헤드가 발생한다.

5. 파일 시스템 기반 설정 저장

데이터베이스 대신 JSON 파일로 설정을 저장한다. 이는 단순성을 제공하지만, 동시 접근 보호가 필요한 시나리오에서는 제한적이다.

6. RuleSet 기반 MITM 필터링

정규식 대신 단순 문자열 매칭과 와일드카드를 사용한다. 이는 성능상 이점이 있지만, 복잡한 매칭 규칙에는 제한이 있다.

기술 스택 상세

기술용도선택 이유대안
Wails v2GUI 프레임워크Go + 웹 기술, 작은 바이너리Electron, Fyne
Vue 3프론트엔드Composition API, 성능React, Svelte
Pinia상태 관리Vue 3 최적화, TypeScript 지원Vuex, Redux
goproxyMITM 프록시Go 네이티브, 커스터마이징 용이mitmproxy
zerolog로깅구조화 로깅, 제로 할당logrus, zap
go-nanoidID 생성짧은 길이, URL 안전UUID
AES-CBC암호화표준 알고리즘, 충분한 보안AES-GCM, ChaCha20

라우팅 및 화면 구성

frontend/src/router/index.ts:1-31에 정의된 라우팅 구조는 다음과 같다:

경로컴포넌트keepAlive설명
/Layout-루트 레이아웃
/indexIndextrue메인 리소스 목록 화면
/settingSettingfalse설정 화면

해시 기반 라우팅(createWebHashHistory)을 사용하여 정적 파일 서빙 환경에서도 동작하도록 설계되었다.

에러 처리 및 복구 전략

다운로더 에러 처리

FileDownloader는 다음과 같은 에러 시나리오를 처리한다:

  1. URL 파싱 실패: 명확한 에러 메시지와 함께 즉시 반환 (core/downloader.go:127-130)
  2. HEAD 요청 실패: 다운로드 불가능한 리소스로 표시
  3. 컨텍스트 취소: 진행 중인 다운로드의 안전한 중단

프록시 에러 처리

CA 인증서 로드 실패 시 다이얼로그로 사용자에게 알리고 프록시 시작을 중단한다 (core/proxy.go:73-77).

애플리케이션 종료 처리

OnExit()에서 시스템 프록시 설정을 복원하고, IsReset 플래그가 설정된 경우 앱 초기화를 수행한다 (core/app.go:134-141).

正在加载图表渲染器...

런타임 흐름 설명:

  1. 초기화: main → App → Config → Proxy → HttpServer 순서로 초기화
  2. 요청 처리: 로컬 API 요청과 프록시 요청을 분기 처리
  3. MITM 처리: RuleSet에 따라 선택적으로 HTTPS 복호화 수행
  4. 종료: 시스템 프록시 해제 후 필요시 앱 초기화 실행