자바 개발자의 go-ethereum 소스 읽기: Day 2

in kr •  7 months ago

자바 개발자의 go-ethereum 소스 읽기: Day 2

main_logo

이 글은 자바 개발자의 go-ethereum(geth 클라이언트) 소스 분석기 시리즈의 연재 중 두 번째 글입니다. 앞으로 다음과 같은 내용으로 연재를 계획하고 있습니다.

  1. Day 01: Geth 1.0 소스 받기 및 코드 분석을 위한 개발환경 셋팅(VS Code)
  2. (본 글) Day 02: CLI 라이브러리 기반 geth의 전체 실행 구조
  3. Day 03: VS Code를 사용한 geth 디버깅
    ...

전체 연재 목록은 아래 페이지에서 확인해 주세요
http://www.notforme.kr/block-chain/geth-code-reading

Eng Version: Java developer's adventure to analyze go-ethereum(geth): Day 02

대상 독자

이 연재는 먼저 독자 분들이 적어도 Java와 같은 OOP 계열의 언어로 프로그래밍 경험이 있다는 것을 가정합니다. 또한 계정, 채굴 등 블록체인과 이더리움과 관련된 기초적인 개념을 알고 있다고 가정합니다.

다루는 내용

이 글에서는 geth 실행과 관련된 구조를 살펴봅니다. gethcli 라이브러리를 통해서 CLI기반의 인터페이스를 제공합니다. cli 라이브러리의 구성은 geth의 아키텍쳐와 깊게 관련되어 있습니다. 따라서 geth가 어떻게 cli 라이브러리를 사용하여 실행하는지 이해하는 일은 geth의 전체 구조를 이해하는 핵심이 됩니다.

이번 글에서는 golang의 문법과 관련해서 short variable declaration과 구조체도 살펴볼 것입니다.

그럼 지난 시간에 마지막으로 살펴보았던 utils.NewApp() 함수에서부터 시작해 봅시다.

NewApp 함수의 정의

utils.NewApp 함수는 cmd/utils/flags.go 파일에 선언되어 있습니다. 다음은 함수의 전체 코드입니다. 먼저 코드부터 봅시다.

// NewApp creates an app with sane defaults.
func NewApp(version, usage string) *cli.App {
    app := cli.NewApp()
    app.Name = filepath.Base(os.Args[0])
    app.Author = ""
    //app.Authors = nil
    app.Email = ""
    app.Version = version
    app.Usage = usage
    return app
}


함수의 시그니쳐를 보면 NewApp 함수는 cli.App 타입의 포인터를 반환하는 함수라는 것을 알 수 있습니다. 함수의 구현에서 가장 중요한 부분은 3번 라인입니다. 이 코드를 보면 app 인스턴스가 cli.NewApp 함수의 호출 결과로 초기화 된 것을 알 수 있습니다. 여기서 app := cli.NewApp()를 보면 뭔가 어색하지 않으신가요? (저만 그런 것인가요…) app 변수는 패키지에 선언된 변수가 아님에도 불구하고 아무런 선언 없이 이 함수 안에서 사용하고 있습니다.이제 golang의 새로운 구문을 하나 가볍게 살펴볼 떄가 되었습니다.

Golang의 변수 선언

방금 본 코드에서, 다소 어색한 := 연산자를 만났습니다. 이 연산자는 golangshort variable declaration 입니다. 변수를 선언할 때 golangvar 키워드를 먼저 쓰고 변수의 이름과 타입이 따라옵니다. 이와 관련된 예제 코드를 한 번 보겠습니다.

var foo int = 10;

func bar() {
    foo := 10;  // You can omit `var` keyword and type information!
}


첫 문장의 변수 선언 방식과 달리 함수 내에서는 특별히 var 키워드와 타입 정보를 생략하고 변수를 선언할 수 있습니다. 다만 반드시 short variable declaration로 변수를 선언하려면 := 로 변수에 값을 할당해야 합니다.

자 이제 golang 의 변수 선언방법을 확인했으니 다시 utils.NewApp 로 돌아가겠습니다.

Cli.NewApp 함수의 래퍼

사실 utils.NewApp 함수에서 우리는 app 인스턴스에 대한 구체적인 정보를 얻지 못했습니다. 이 함수는 그저 cli.NewApp 함수의 결과를 app 할당한 것이 전부입니다. 다른 말로 표현하면 utils.NewApp 함수는 cli.NewApp 함수의 래퍼 함수로 cli.NewApp의 반환결과를 추상화한 것이 전부입니다. 따라서 우리가 지난시간부터 조사했던 app 인스턴스의 본질은 cli.NewApp 함수 안에 있습니다.

그런데 cli.NewApp 함수의 구현은 어디에 있을까요? cmd/utils/flags.go파일의 상단부 import 부분을 보면 다음과 같은 힌트를 얻을 수 있습니다.

"github.com/codegangsta/cli"


cligeth에 있는 구현체가 아닙니다. 외부 라이브러리입니다. 이제 app 인스턴스의 구현부를 확인하기 위해서 위 주소로 Github에 들어가서 직접 확인해 보고자 합니다.

CLI 라이브러리

먼저 https://github.com/codegangsta/cli에 방문하면 아래 페이지로 리다이렉트 됩니다.

이 리파지토리의 README.md 에 다음과 같은 공지가 있습니다.:

This is the library formerly known as github.com/codegangsta/cli -- Github will automatically redirect requests to this repository, but we recommend updating your references for clarity.

공지내용은 우리가 확인하려던 라이브러리가 리다이렉트된 라이브러리로 변경되었다는 것을 말합니다. 사실 지난 시간부터 살펴본 geth 코드는 v1.0.0으로 과거 버전입니다. 따라서 외부 라이브러리의 변경사항이 충분히 있을 수 있습니다. codegangsta/cli 케이스도 마찬가지로 urfave/cli 로 변경되었습니다. 하지만 cli 라이브러리의 경우 인터페이스의 큰 차이가 없어 현재 시점에 geth 코드 v1.0.0 을 기준으로 살펴봐도 문제가 없습니다. 편한 마음으로 urface/cli 의 내부를 살펴봅시다.

실제 NewApp() 함수의 정의

NewApp 함수의 정의는 실제로는 urfave/cli 라이브러리 app.go 파일에 있습니다. 다음 함수의 구현부를 함께 봅시다.

// NewApp creates a new cli Application with some reasonable defaults for Name,
// Usage, Version and Action.
func NewApp() *App {
    return &App{
        Name:         filepath.Base(os.Args[0]),
        HelpName:     filepath.Base(os.Args[0]),
        Usage:        "A new cli application",
        UsageText:    "",
        Version:      "0.0.0",
        BashComplete: DefaultAppComplete,
        Action:       helpCommand.Action,
        Compiled:     compileTime(),
        Writer:       os.Stdout,
    }
}


이 함수는 App 타입의 레퍼런스를 반환합니다. App 타입은 같은 파일 27번 라인에 아래와 같이 선언되어 있습니다.

// App is the main structure of a cli application. It is recommended that
// an app be created with the cli.NewApp() function
type App struct {
    // The name of the program. Defaults to path.Base(os.Args[0])
    Name string
    // Full name of command for help, defaults to Name
    HelpName string
    // Description of the program.
    Usage string
    // Text to override the USAGE section of help
    UsageText string
    // Description of the program argument format.
    ArgsUsage string
    // Version of the program
    Version string
    // Description of the program
    Description string
    // List of commands to execute
    Commands []Command
    // List of flags to parse
    Flags []Flag
    // Boolean to enable bash completion commands
    ...


이 코드와 주석을 읽어보면 App 타입의 주요 필드와 역할을 이해할 수 있습니다. 이 필드는 실제 geth의 CLI 환경을 지원하는데 사용되는 정보입니다. 이 시점에서 코드를 더 깊게 들어가기 전에 간단히 golang 의 구조체( struct ) 문법을 알아보겠습니다.

Golang 구조체

구조체( struct )는 복수의 필드의 모음입니다. 다음 코드는 golang 의 구조체 선언 예입니다.

type User struct {
    name sting
    age int
}


C 언어의 구조체 문법과 크게 다르지 않습니다. User 타입의 객체 하나를 생성하기 위해서 아래 예제와 같이 구조체 이름과 함께 { } 안에 필드의 이름과 실제 값의 쌍을 선언하면 됩니다.

person{name: "Alice", age: 30}


golang 의 구조체와 관련된 좀 더 자세한 정보는 다음 경로의 컨텐츠를 참조해 주세요:

여기서 이해할 수 있는 cli.NewApp() 의 역할은 App 구조체를 구현하고 레퍼런스로 반환하는 것이 전부입니다.

목표 재점검

지금까지 우리는 geth가 어떻게 동작하는지 이해하기 위해 긴 여정을 떠나 왔씁니다. 이제 거의 다 와갑니다. 마지막 종착점에 이르기 전에 우리의 여정을 정리해 봅시다.

우리는 먼저 geth의 최초 실행 포인트인 메인 함수를 찾았습니다. 이어서 main 함수에서 사용되는 app 인스턴스의 존재를 찾아 해맸습니다. app 인스턴스가 중요한 이유는 main 함수 안에서 Run 메서드를 실행하는 것이 geth의 출발점이기 때문입니다. app 인스턴스는 겉으로는 utils.NewApp() 의 결과 값이지만 실제로는 외부라이브러리인 cli가 반환한 결과라는 것을 알아냈습니다.

geth 는 CLI 환경의 도구 입니다. cli 라이브러리는 CLI 기반의 소프트웨어에 필요한 공통적인 기능을 제공하는 일종의 프레임워크와 같습니다. README.md 파일의 내용에서 cli 라이브러리에 대한 소개를 다음과 같이 볼 수 있습니다.

cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.

소개를 한마디로 요약하면 CLI 개발을 위한 효율적인 툴입니다. 이제 마지막으로 geth실행의 근간이 되는 cli 라이브러를 살펴봅시다.

CLI 라이브러리의 주요 컴포넌트

우리는 전에 App 타입의 선언을 살펴봤습니다. geth 의 실행과 관련해서 꾸준히가장 중요한 2개의 컴포넌트가 있습니다. 각각에 대해서 한번 살펴봅시다.

  1. []Command
  2. []Flag

Command

Command 타입은 metadata와 캡쳐 함수가 있습니다. 예를 들어 여러분이 geth가 설치된 컴퓨터의 터미널에서 geth version 을 입력하면 다음과 같은 응답을 볼 수 있습니다.

$ geth version
Geth
Version: 1.8.5-unstable
Git Commit: b15eb665ee3c373a361b050cd8fc726e31c4a750
Architecture: amd64
Protocol Versions: [63 62]
Network Id: 1
Go Version: go1.10
Operating System: darwin
GOPATH=(Your own GOPATH...)
GOROOT=(Your own GOROOT...)


이 명령은 cli's Command 타입을 사용해서 출력된 것입니다. 전체 코드는 최신 커밋 기준(744428c)으로 다음과 같습니다.

    versionCommand = cli.Command{
        Action:    utils.MigrateFlags(version),
        Name:      "version",
        Usage:     "Print version numbers",
        ArgsUsage: " ",
        Category:  "MISCELLANEOUS COMMANDS",
        Description: `
The output of this command is supposed to be machine-readable.
`,
    }


코드에서 중요한 부분은 라인 2-3번 입니다. 2번 라인의 Action 필드는 실제 version 명령요청을 실행할 핸들러 함수로 선언되어 있습니다. 3번 라인의 Name 은 CLI 환경에서 실행할 명령의 이름을 의미합니다.

version 외에 geth에서 사용되는 모든 CLI 명령은 위와 유사한 형태로 cli 라이브러리를 사용하여 등록됩니다. 이제 우리는 손쉽게 geth의 각 명령의 구현체가 어디인지 찾을 수 있습니다.

Flag

Flag 타입은 Command보다 이해하기 더 쉽습니다. Flag는 단순히 불리언 타입의 flag 역할입니다. geth 명령을 CLI에서 실행할 때, 다양한 인자를 옵션으로 주어 특정 기능의 활성화/비활성화를 제어할 수 있습니다. 바로 이 때 각 인자가 geth 코드 안에서는 Flag 타입으로 맵핑됩니다. 마찬가지로 Flag의 코드 역시 Command와 유사한 구조를 같습니다. 다음은 rpc 기능을 사용할 때 줄 수 있는 rpc 인자에 대응하는 Flag 선언 코드 입니다. 역시 다음 코드 또한 커밋 744428c 기준입니다.

    RPCEnabledFlag = cli.BoolFlag{
        Name:  "rpc",
        Usage: "Enable the HTTP-RPC server",
    }

Hook Interface

cli 는 애플리케이션의 실행과 관련하여 3가지 이벤트의 훅 인터페이스를 제공합니다.

  1. app.Before: app 인스턴스가 시작하기 전의 후킹할 수 있는 인터페이스.
  2. app.Action: 애플리케이션의 본 로직
  3. app.After: app 인스턴스가 종료되기 전의 후킹할 수 있는 인터페이스.

앞으로 각 핸들러가 실제로 geth 에서 어떻게 사용되는지 살펴보게 될 것입니다. 여기서는 app.Action 이 제일 중요한 부분이라는 점만 기억하면 됩니다. 왜냐하면 app.Action 가 실제 주어진 인자와 상관없이 geth가 실행하는 메인 함수이기 때문입니다.

트리거

마지막으로 남은 건 cli 에 등록한 CommandFlag 와 후킹한 구현체를 언제 실행하는지가 중요합니다. 지난 글에서 최초 진입점인 main 함수를 떠올려볼까요?

func main() {
    if err := app.Run(os.Args); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}


코드를 보면 main 함수에서 app.Run 함수를 실행하는 걸 지난 글에서 확인했습니다. main 함수가 터미널에서 인자로 받은 인자인 osArgs를 전달하면서 Run 함수를 실행합니다. 바로 이 코드가 실제 cli.App을 트리거하는 액션입니다. 결론적으로 터미널에서 geth 명령과 함께 전달한 인자들이 app.Run 함수를 통해 전달되어 등록된 CommandFlag에 맞게 geth 가 동작하게 됩니다.

정리하면 지금까지 살펴본 gethcli 라이브러리는 다음의 그림으로 요약할 수 있습니다.

d02_cur_arch.png

Conclusion

오늘은 cli 라이브러리를 기반으로 하는 geth 실행 구조를 살펴봤습니다. 우리는 또한 golangshort variable declaration 문법과 구조체도 가볍게 알아봤습니다.

오늘의 내용은 앞으로 geth 소스를 분석하는 아주 중요한 틀이 됩니다. geth의 전체 실행 흐름을 파악했기 때문에 굳이 v1.0.0 버전에 머물러 있을 필요가 없습니다. 따라서 이제 geth의 최신 소스로 돌아가고자 합니다.

$ git checkout master

다음 시간에는, geth를 직접 빌드하고 매뉴얼에 따라 로컬 네트워크를 실행해 볼 예정입니다. 그리고 테스트 한 내용을 직접 코드에서 어떻게 구현했는지 살펴보겠습니다.

꾸준히 포기하지 않고 공부한 내용을 연재로 이어나갈 수 있도록 응원(?) 부탁 드립니다. ^^

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

좋은 글 감사드립니다