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

in kr •  6 months ago

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

main_logo

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

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

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

대상 독자

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

더불어 이 연재는 다음 3가지 목적을 염두하고 쓴 것입니다.

  1. 새로운 언어(Go)를 오픈소스 코드를 읽으며 배운다.
  2. 오픈소스를 읽으며 코드리딩 능력을 배양한다.
  3. 블록체인의 기술을 직접 코드를 통해서 익힌다.

다루는 내용

지난 글에서는 geth 의 실행 로직을 분석하기 위해 VS Code를 사용하여 디버깅하는 방법을 다뤘습니다. 오늘은 드디어 본격적으로 geth의 실행 로직을 읽어 보려고 합니다. 터미널에서 geth 를 실행하면서 일어나는 로직 중 초기 부분을 오늘 글에서 살펴볼 계획입니다.

geth의 초기 실행 코드에는 이전에 보지 못했던 새로운 golang의 구문을 만날 수 있습니다. golang을 실무에서 써 본적이 없는 개발자로서 오늘도 다음 열거한 golang의 문법도 가볍게 살펴볼 예정입니다.

  1. multiple return
  2. 고루틴과 채널

오늘 읽는 코드의 커밋 해쉬는 577d375 입니다. 참고 부탁드립니다.

시작은 app.Action에서

지난 두 번째 연재에서 우리는 geth를 실행할 때 메인이 되는 로직은 app.Action에 등록된다는 것을 알았습니다. 본격적인 우리의 코드 읽기 첫 여정은 바로 이 지점에서 시작합니다.

func init() {
    // Initialize the CLI app and start Geth
    app.Action = geth
    // 다른 코드는 생략...
}


app.Action에 할당한 함수명이 geth군요. 친절하게 주석으로도 geth 함수가 CLI 애플리케이션의 초기화와 실행을 담당한다고 알려줍니다. 그럼 이제 뒤돌아보지 않고 geth를 찾아가 봅시다.

VS Code(혹은 golang을 지원하는 IDE)를 사용한다면 Go to Definition으로 바로 찾아갈 수 있습니다.

같은 main.go 파일에 geth 함수는 다음과 같이 선언되어 있습니다.

// geth is the main entry point into the system if no special subcommand is ran.
// It creates a default node based on the command line arguments and runs it in
// blocking mode, waiting for it to be shut down.
func geth(ctx *cli.Context) error { 
    node := makeFullNode(ctx)
    startNode(ctx, node)
    node.Wait()
    return nil
}


이번에도 아주 친절하게 geth 함수를 주석으로 설명해 줍니다. 우리의 친구 구글 번역기의 도움을 빌려보면 주석의 내용은 다음과 같습니다.

특수 부속 명령이 실행되지 않은 경우 geth는 시스템에 대한 기본 진입점입니다. 명령 행 인수를 기반으로 기본 노드를 작성하고 이를 실행합니다. 블로킹 모드는 종료 될 때까지 기다린다.

주석의 설명처럼 geth 함수는 가장 기본이 되는 진입점입니다. 이 함수는 ctx라는 인자를 받고 있는데요. ctx를 간단히 짚고 넘어가겠습니다.

ctx *cli.Context

ctxgeth함수에서 인자로 받는 cli.Context 객체입니다. 이 객체의 타입은 지난 글에서 살펴본 cli 라이브러리에 선언되어 있습니다. 굳이 해당 라이브러리의 코드를 모두 보지 않아도 터미널에 입력한 명령을 파싱한 결과와 사전에 등록된 Command와 Flag 정보가 포함되어 있을 것을 추측할 수 있습니다.

실제 라이브러리에 선언된 cli.Context 는 구조체로 다음과 같습니다.

// Context is a type that is passed through to
// each Handler action in a cli application. Context
// can be used to retrieve context-specific Args and
// parsed command-line options.
type Context struct {
    App           *App
    Command       Command
    shellComplete bool
    flagSet       *flag.FlagSet
    setFlags      map[string]bool
    parentContext *Context
}


유추한대로 터미널 명령에서 입력한 인자를 파싱한 결과를 포함하여 app 인스턴스, Command 등을 모두 가지고 있네요. Java 와 Spring 프레임워크가 익숙한 저에게는 Spring의 applicationContext를 연상하게 하는 객체입니다.

이제 다시 함수의 본문으로 돌아옵시다. geth 함수의 코드는 return을 제외하면 3줄이 전부입니다.

  1. node := makeFullNode(ctx)
  2. startNode(ctx, node)
  3. node.Wait()

각 함수의 이름만 봐도 어떤 역할을 하는지 짐작을 할 수 있습니다. 오늘은 이 세줄의 코드 가운데 첫번째와 세번째 줄의 코드를 보고자합니다. startNode(ctx, node)는 다뤄야 할 내용이 많아 다음 글에서 독립적으로 다루겠습니다.

makeFullNode 함수

이 함수는 main.go와 같은 폴더(패키지)에 속하는 config.go 파일에 선언되어 있습니다. 함수의 이름과 파일명을 근거로 이 함수의 주요 목적이 클라이언트 노드로 실행될 geth의 기본 설정을 할 것으로 보입니다. 먼저 함수의 시그니쳐를 봅시다.

func makeFullNode(ctx *cli.Context) *node.Node {


반환값이 node.Node 타입이군요. 이는 앞서 node := makeFullNode(ctx) 코드에 맞게 node.Node 타입의 객체를 반환한다고 볼 수 있겠습니다. geth가 이더리움 프로토콜을 따르는 클라이언트 노드 애플리케이션이라는 것을 감안하면, node.Node가 바로 논리적으로 이더리움 네트워크에 참여하는 노드라 추측할 수 있습니다.

Node 폴더에 node.go 파일에서 Node 타입을 확인할 수 있습니다. 이 객체의 정의는 다음과 같습니다.

// Node is a container on which services can be registered.
type Node struct {
    eventmux *event.TypeMux // Event multiplexer used between the services of a stack
    config   *Config
    accman   *accounts.Manager

    ephemeralKeystore string         // if non-empty, the key directory that will be removed by Stop
    instanceDirLock   flock.Releaser // prevents concurrent use of instance directory

    serverConfig p2p.Config
    server       *p2p.Server // Currently running P2P networking layer

    serviceFuncs []ServiceConstructor     // Service constructors (in dependency order)
    services     map[reflect.Type]Service // Currently running services

    rpcAPIs       []rpc.API   // List of APIs currently provided by the node
    inprocHandler *rpc.Server // In-process RPC request handler to process the API requests

    ipcEndpoint string       // IPC endpoint to listen at (empty = IPC disabled)
    ipcListener net.Listener // IPC RPC listener socket to serve API requests
    ipcHandler  *rpc.Server  // IPC RPC request handler to process the API requests

    httpEndpoint  string       // HTTP endpoint (interface + port) to listen at (empty = HTTP disabled)
    httpWhitelist []string     // HTTP RPC modules to allow through this endpoint
    httpListener  net.Listener // HTTP RPC listener socket to server API requests
    httpHandler   *rpc.Server  // HTTP RPC request handler to process the API requests

    wsEndpoint string       // Websocket endpoint (interface + port) to listen at (empty = websocket disabled)
    wsListener net.Listener // Websocket RPC listener socket to server API requests
    wsHandler  *rpc.Server  // Websocket RPC request handler to process the API requests

    stop chan struct{} // Channel to wait for termination notifications
    lock sync.RWMutex

    log log.Logger
}


다소 많은 필드를 담고 있어 복잡해 보일 수 있으나 필드명과 각 필드에 달린 주석을 보면 추측한 대로 네트워크에 참여하는 노드로서의 기능과 API를 다양한 프로토콜로 제공하고 있는 것을 알 수 있습니다. 이제 makeFullNode함수의 본문중 일부를 보겠습니다.

func makeFullNode(ctx *cli.Context) *node.Node {
    stack, cfg := makeConfigNode(ctx)

    utils.RegisterEthService(stack, &cfg.Eth)

    // 다른 Register...Service 코드 생략...
    return stack
}


본문은 기능적으로 2부분으로 나눠서 이해할 수 있습니다. 첫번째 부분은 바로 2번 라인 한줄입니다.

stack, cfg := makeConfigNode(ctx)


makeConfigNode가 반환한 stackmakeFullNode 에서 반환하는 Node 타입의 변수라는 것을 함수의 return 결과를 보면 알 수 있습니다. 생략된 코드를 포함한 이후의 로직은 makeConfigNode가 반환한 cfg의 있는 설정값과 ctx에 저장된 설정을 기반으로 일련의 기능을 등록 또는 활성화하는 코드입니다. 정리하면 makeFullNode의는 다음과 같습니다.

  1. 네트워크에 참여할 노드의 생성과 초기화
  2. 기본 설정 및 터미널 명령과 함께 주어진 인수에 따라 노드 설정 적용 및 기타 서비스 등록

그럼 구체적으로 stack 변수가 어떻게 초기화 되는지 또 한 단계 들어가야 합니다. 잠깐 코드를 읽기 전에 길을 잃을 수 있으니 우리가 현재 어떻게 makeConfigNode까지 탐색하게 되었는지 정리해 보면 다음과 같습니다.

main > App.Run 실행되면 App.action에 등록된 함수 실행 > geth > makeFullNode > makeConfigNode

makeConfigNode 함수

makeConfigNode 함수의 정의는 makeFullNode 함수 위에 선언되어 있습니다. 코드는 다음과 같습니다.

func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
    // Load defaults.
    cfg := gethConfig{
        Eth:       eth.DefaultConfig,
        Shh:       whisper.DefaultConfig,
        Node:      defaultNodeConfig(),
        Dashboard: dashboard.DefaultConfig,
    }

    // Load config file.
    if file := ctx.GlobalString(configFileFlag.Name); file != "" {
        if err := loadConfig(file, &cfg); err != nil {
            utils.Fatalf("%v", err)
        }
    }

    // Apply flags.
    utils.SetNodeConfig(ctx, &cfg.Node)
    stack, err := node.New(&cfg.Node)
    if err != nil {
        utils.Fatalf("Failed to create the protocol stack: %v", err)
    }
    utils.SetEthConfig(ctx, stack, &cfg.Eth)
    if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) {
        cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name)
    }

    utils.SetShhConfig(ctx, stack, &cfg.Shh)
    utils.SetDashboardConfig(ctx, &cfg.Dashboard)

    return stack, cfg
}


이 함수의 역할도 호출자인 makeFullNode와 마찬가지로 stack 변수와 관련 설정 초기화 및 등록으로 구분됩니다. 먼저 stack 변수의 초기화는 코드 중반부를 자세히 보면 바로 아래와 같이 stack 의 초기화가 이뤄지는 것을 알 수 있습니다.

stack, err := node.New(&cfg.Node)


설정은 cfg라는 변수에 담고 있습니다. 최초에 다음과 같이 gethConfig 구조체를 cfg 변수로 초기화 합니다.

// Load defaults.
cfg := gethConfig{
    Eth:       eth.DefaultConfig,
    Shh:       whisper.DefaultConfig,
    Node:      defaultNodeConfig(),
    Dashboard: dashboard.DefaultConfig,
}


위 코드처럼 cfg 안에 4가지의 설정정보를 디폴트값으로 초기화한 뒤 각각의 설정을 터미널에서 입력받은 값과 디폴트 설정을 조합하여 셋팅합니다. 4가지 영역의 설정을 자세히 설명하면 다음과 같습니다.

  1. Eth: 이더리움 메인넷의 기본 설정, eth 패키지의 config.tsDefaultConfig 변수 선언되어 있음
  2. Shh: 이더리움의 P2P 프로토컬인 whisper와 관련된 설정, whisperv6 패키지의 config.goDefaultConfig 변수 선언되어 있음
  3. Node: geth 노드를 위한 기본 설정, makeConfigNode 함수 바로 위에 defaultNodeConfig함수 있음
  4. Dashboard: 이더리움 웹 대시보드 관련 설정, dashboard 패키지의 config.go에 함수 있음

여담이지만, 이 코드를 보고 Dashboard 패키지 존재를 알았습니다. 실제 해당 패키지에 가서 프론트엔드쪽 패키지를 로컬에 다운받은 yarn dev 명령을 실행하면 8081 포트로 대시보드 웹앱이 아래와 같이 실행되는 것을 확인할 수 있습니다. Work in progress가 쓰여 있는 걸 봐서는 개발 진행중인 작업으로 보입니다.

d04_dashboard.png

다음은 실제 코드에서 해당 로직을 4가지 설정으로 나눠서 정리한 코드입니다.

// Apply flags.
// Node 설정을 셋팅하고 난 뒤 stack 변수에 node를 초기화하여 할당
utils.SetNodeConfig(ctx, &cfg.Node)
stack, err := node.New(&cfg.Node)

if err != nil {
    utils.Fatalf("Failed to create the protocol stack: %v", err)
}

// Eth관련 설정 진행
utils.SetEthConfig(ctx, stack, &cfg.Eth)
if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) {
    cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name)
}

// Shh 관련 설정 진행
utils.SetShhConfig(ctx, stack, &cfg.Shh)

// 이더리움의 대시보드 관련 설정 진행
utils.SetDashboardConfig(ctx, &cfg.Dashboard)


이제 4가지 설정을 마친 후 stack 변수를 반환하는 것이 makeConfigNode 함수의 마지막입니다.

    return stack, cfg


이 반환값은 그대로 makeFullNode에서 stack, cfg := makeConfigNode(ctx) 으로 값을 받는 것을 위에서 이미 확인했습니다.

Multiple Return

지금까지 코드를 읽으면서 아무렇지 않게 넘어가 구문이 하나 있는데 그것은 함수의 반환 값을 복수로 사용한 부분입니다. 무언가 언급할 만큼 구문이 특별히 어려운 것이 없지만 golang은 함수에서 복수의 반환값을 지원한다는 사실만 인지하고 있으면 됩니다. 공식 예제인 A tour of Go코드를 차용하여 간단히 예제만으로 구문을 확인하겠습니다.

func swap(x, y string) (string, string) {
    return y, x
}

func main() {
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}


이제 makeConfigNode 함수를 호출했던 호출자 makeFullNode 함수의 문맥으로 돌아갑니다. stack 변수와 cfg를 얻었으니 남은 일은 앞서 cfg에 들어 있던 설정을 활용하여 필요한 서비스로 등록하는 일입니다. 앞에서 생략했던 makeFullNode 함수의 서비스 등록 로직을 여기서 살펴보겠습니다.

// 앞 부분 생략

// cfg.Eth를 사용하여 이더리움 네트워크 서비스 등록   
utils.RegisterEthService(stack, &cfg.Eth)

// 터미널로 --dashboard 플래그를 옵션을 준 경우
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
    // dashboard 서비스 등록
    utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
}

// 코드가 복잡한 듯 보이나... 여러 설정 등을 기반으로 whisper 프로토콜 셋팅을 하고... 설정이 enabled 이면
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
shhEnabled := enableWhisper(ctx)
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)
if shhEnabled || shhAutoEnabled {
    if ctx.GlobalIsSet(utils.WhisperMaxMessageSizeFlag.Name) {
        cfg.Shh.MaxMessageSize = uint32(ctx.Int(utils.WhisperMaxMessageSizeFlag.Name))
    }
    if ctx.GlobalIsSet(utils.WhisperMinPOWFlag.Name) {
        cfg.Shh.MinimumAcceptedPOW = ctx.Float64(utils.WhisperMinPOWFlag.Name)
    }
    // whisper 서비스를 등록한다.
    utils.RegisterShhService(stack, &cfg.Shh)
}

// Add the Ethereum Stats daemon if requested.
if cfg.Ethstats.URL != "" {
    // 앞서 설명에는 빠져 있지만 EthStat이란 설정도 URL이 있으면 서비스 등록
    utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
}


위 코드 안에 설명이 필요한 부분에 주석을 남겨 놓았습니다. 특별히 어려운 내용은 없으며 모두 비슷한 과정으로 설정값이나 터미널 옵션에 따라 서비스 등록 여부를 결정할 뿐입니다. 다만 한가지 참고할 만한 부분은 지난 시간에 살펴 봤던 cli 라이브러리의 Flag 기능입니다. 위 코드에서 대시보드 서비스 등록과 관련된 코드만 다시 발췌해 보겠습니다.

if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
    // dashboard 서비스 등록
    utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
}


여기서 ctx.GlobalBoolctx에 저장된 Flag의 값을 찾아서 반환합니다. 이 때 찾을 Flag 의 이름이 바로 utils.DashboardEnabledFlag.Name 입니다. 이 값은 utils 패키지의 flags.go 파일에 다음과 같이 선언되어 있습니다.

// Dashboard settings
DashboardEnabledFlag = cli.BoolFlag{
    Name:  "dashboard",
    Usage: "Enable the dashboard",
}


위에서 Name은 우리가 터미널에 입력할 옵션의 이름이고 Usage는 터미널의 help 명령의 설명 정보입니다.

makeFullNode 함수 하나를 보는데서 설명할 내용이 적지 않았습니다. 저희가 지금 어디서 코드를 추적해왔는지 기억하시나요? 아래와 같이 geth 함수의 3줄 중 첫번째 코드를 읽었을 뿐입니다.

  1. node := makeFullNode(ctx)
  2. startNode(ctx, node)
  3. node.Wait()

이제 두 줄이 남았습니다. 앞서 예고한 대로 2번줄의 startNode(ctx, node)는 다룰 것이 훨씬 더 많은 코드입니다. 이 함수의 설명은 다음 글에서 다루기로 하고 오늘은 간단히 세번째 줄 node.Wait() 을 마지막으로 살펴봅시다.

node.Wait 함수

이 함수는 이름에서 드러나듯 전 단계인 startNode(ctx, node)에서 실행한 로직들이 모두 종료되기를 기다리는 함수입니다. 다음 글에서 다루겠지만 startNode 함수에서는 golang의 채널을 사용하여 스레드로 여러 로직을 실행합니다. 이러한 로직들이 정상적으로 종료되기를 기다리면서 geth 프로세스가 죽지않도록 메인 프로세스를 대기하도록 하는 것이 이 함수가 하는 전부입니다.

이 함수는 node 패키지의 node.go에 선언되어 있으며 코드는 다음과 같습니다.

// Wait blocks the thread until the node is stopped. If the node is not running
// at the time of invocation, the method immediately returns.
func (n *Node) Wait() {
    n.lock.RLock()
    if n.server == nil {
        n.lock.RUnlock()
        return
    }
    stop := n.stop
    n.lock.RUnlock()

    <-stop
}


역시 친절하게 주석이 이 함수의 역할을 설명해 줍니다. 코드는 간단하지만 동시성 처리를 위한 락과 golang의 채널 개념이 들어가 있어서 한 눈에 로직을 파악하기가 쉽지는 않습니다. 따라서 먼저 golang의 고루틴과 채널에 대해서 살펴봐야 할 것 같습니다.

고루틴과 채널

고루틴은 동시성을 지원하기 위한 golang의 경량 스레드를 말합니다. 채널은 스레드간의 통신을 위한 golang의 기능으로 프로세스간 통신에서 사용하는 파이프와 유사한 개념입니다.

고루틴

그럼 먼저 A tour of Go의 고루틴 코드를 참고해서 간단하게 고루틴을 생성하는 법을 다음 예제를 통해서 살펴보겠습니다.

package main

import (
    "fmt"
    "time"
)

func printWithDelay(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s, " called")
    }
}

func main() {
    go printWithDelay("sub routine")
    printWithDelay("main routine")
}


고루틴을 생성하는 방법은 함수 앞에 키워드 go 를 붙여서 호출하는 것이 전부입니다. 위 코드에서는 printWithDelay 라는 함수를 각각 고루틴과 main 스레드에서 호출하였습니다. 코드를 실행하면 main 스레드가 실행되는 루틴과 go 키워드로 실행되는 루틴이 독립적으로 printWithDelay 함수를 호출하게 됩니다.

채널

이번에는 채널을 사용해서 스레드 간의 통신을 구현해 보겠습니다. 간단히 메인스레드와 별도의 고루틴 사이에 핑퐁하는 예제입니다.

package main

import "fmt"

func pingPong(strChannel chan string) {
    counter := 0

    for counter < 5 {
        strChannel <- "ping"
        called := <-strChannel
        fmt.Println("[sub]: ", called)
        counter++
    }

    close(strChannel)
}

func main() {
    strChannel := make(chan string)
    go pingPong(strChannel)

    for {
        called, more := <-strChannel

        if !more {
            break
        }
        fmt.Println("[main]: ", called)
        strChannel <- "pong"
    }

}


먼저 pingPong 함수 전에 main 함수 안의 첫번째 줄이 바로 채널 변수를 선언하는 부분입니다. 채널은 golangmake 함수를 사용하면 생성할 수 있습니다. 위 예제에서는 string 타입의 데이터를 교환하기 위해 다음과 같이 채널을 생성했습니다.

strChannel := make(chan string)


생성된 채널은 <- 키워드를 사용하여 데이터를 주고 받습니다. <- 좌측은 데이터를 전달받는 부분이고 우측은 데이터를 전달하는 쪽입니다. 데이터를 전달 받는 쪽은 데이터를 수신할 때까지 대기상태로 머물러 있게 됩니다.

위 예제에서는 고루틴으로 호출한 pingPong 함수에서 먼저 ping을 보낸 후 메인스레드에서 pong으로 응답합니다. 다섯 번의 핑퐁을 주고 받은 후 pingPong 함수에서 먼저 close 함수로 채널을 닫았습니다. 메인스레드에서는 채널의 두번째 반환값을 사용하여 종료여부를 파악한 후 루프를 종결시킵니다.

아주 간단하게 고루틴과 채널의 기본적인 사용법만 살펴봤습니다. 좀 더 자세한 내용은 아래 링크에서 좀 더 자세히 확인할 수 있습니다.

  1. https://mingrammer.com/go-codelab/goroutine-and-channel
  2. https://tour.golang.org/concurrency/1
  3. http://codingnuri.com/golang-book/10.html
  4. https://gobyexample.com/goroutines
  5. https://gobyexample.com/channels

이제 다시 본래 node.Wait 함수 코드로 돌아갈 시간입니다. 먼저 이 함수에서 본질이 아닌 락 부분 먼저 정리해 봅시다. 정상적인 흐름의 락 관련 코드만 보면 다음과 같습니다.

func (n *Node) Wait() {
    n.lock.RLock()
    // ...
    n.lock.RUnlock()
   // ...
}


코드를 보면 node.Wait 함수가 로직을 실행하기 위해서 읽기용 락을 처리하는 과정임을 유추할 수 있습니다. 이제 핵심은 다음 코드 블럭입니다.

stop := n.stop
n.lock.RUnlock()

<-stop


이 코드는 채널을 이용합니다. 먼저 노드 변수의 stop 필드를 획득한 후 <-stop 코드를 통해서 stop 으로부터 응답이 올 때가지 스레드의 흐름을 블럭킹하고 있습니다. <-를 사용한 것만 봐도 stop이 채널로 선언되었을 것이라 알 수 있습니다. 정말 그러한지 앞에서 이미 Node 구조체의 정의를 봤었는데 stop 을 다시 확인해 볼까요?

    stop chan struct{} // Channel to wait for termination notifications


다행히 예상한대로 채널이 맞군요. 그렇다면 한가지 궁금증이 떠오르네요. stop 채널에 데이터를 전달하는 로직은 어디에 있는걸까요? VS Code의 Find All Reference 기능을 사용하면 손 쉽게 알 수 있습니다. stop 변수를 마우스 우측 버튼으로 클릭한 후 Find All Reference를 실행하면 다음과 같은 결과를 확인할 수 있네요.

d04_stop.png

총 4군데서 stop 변수를 참조하고 있는 것을 알 수 있습니다. 이 중 첫 번째는 stop을 선언한 Node 구조체 부분이고, 마지막은 지금 봤던 node.Wait 함수 부분이었습니다. 2번째 참조 n.stop = make(chan struct{})는 채널을 생성하는 부분으로 보입니다. 이 코드는 우리가 오늘 다루지 않았던 startNode의 로직 중 하나로 node.Start 함수 안에서 stop 채널을 생성하는 부분 입니다. 우리가 찾고 싶었던 부분은 세번째 참조 close(n.stop) 입니다. 이 로직은 node.Stop 함수에 있는 부분입니다. 해당 코드를 찾아가면 친절하게 다음과 같은 주석이 있습니다.

// unblock n.Wait
close(n.stop)


주석에서 알려주듯이, node.Stop 함수를 실행하여 일련의 종료 처리를 마친 후에 node.Wait의 블럭을 해제하는 것임을 이제 정확히 알수 있게 되었습니다.

마치며

오늘은 정말 많은 것을 살펴봤지만 코드자체로는 그리 많지 않은 내용이기도 합니다. 오늘 살펴본 내용을 정리해봅시다. 우리는 먼저 geth 실행 진입점인 main.go 파일에 선언된 App.action의 핸들러가 무엇인지 살펴봤습니다. 이 핸들러는 geth라는 3줄짜리 함수였습니다.

  1. node := makeFullNode(ctx)
  2. startNode(ctx, node)
  3. node.Wait()

오늘 글에서는 이 함수에서 첫번째와 세번째 로직을 살펴봤습니다. makeFullNode 노드 변수의 초기화 및 geth 실행시 입력받은 옵션들을 처리하는 로직이이 있었습니다. node.Waitgolang의 채널을 사용하여 실제 서비스가 종료될때까지 대기하는 간단한 함수였습니다.

코드를 읽으면서 하나의 글로 정리하는 일이 쉽지는 않네요. 읽으면서 모호하거나 궁금한 부분은 언제든 댓글 부탁드립니다. 그럼 다음 연재에서 뵙겠습니다. :)

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:  

어려운데 언제 날잡아서 실습해보아야 겠어요
출근길에 읽기엔 너무 어렵네요
즐거운 하루되세요🍀

·

@noisysky 네 이번 글부터 본격적인 코드레벨에 들어가니 글로 표현하기가 쉽지 않네요. ㅠ
아직은 초기 실행로직이라 코드를 왔다갔다 할 일이 많아 더욱 그렇게 느끼실 것 같습니다.
핵심 로직들은 조금 더 쉽게 읽고 설명해보도록 노력해 보겠습니다 :)

go-ethereum에 관심이 있었는데 이렇게 자세히 써주시다니, 이해하는데 많은 도움이 될거 같아요.
앞으로 연재글 기대되네요 ^^

@홍보해

감사드립니다^^