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

in #kr7 years ago (edited)

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

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 노드 실행 로직
  5. (본 글) Day 05 geth의 실행과 종료

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

대상 독자 및 연재 목표

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

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

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

다루는 내용

오늘은 지난 글에서 미뤄두었던 geth 함수의 startNode(ctx, node)함수를 분석할 예정입니다. 오늘은 다룰 내용이 만만치 않습니다. 새로운 golang의 구문들이 마구 등장하기 때문입니다…. golang의 구문 자체도 다룰 내용이 많은데 코드도 읽어야 하니 오늘 글은 다소 산만할 수도 있습니다. 이미 golang이 익숙한 분들은 해당 내용은 빠르게 지나가셔도 좋습니다.

구체적으로 다음의 내용을 코드를 읽는데 필요한 수준으로 함께 살펴볼 예정입니다. 아마 오늘 글에서 살펴본 개념이면 기본적인 golang의 문법의 80%정도는 다뤘다고 봐도 될거 같습니다. 다음 연재부터는 golang의 구문으로 코드 읽는 고생은 거의 없지 않을까 싶습니다.

  1. 리시버
  2. defer
  3. make 함수
  4. map
  5. Reflection
  6. for와 range
  7. array와 slice

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

utils.StartNode(stack) 함수

startNode 함수의 코드를 보면 첫 줄은 debug로 시작하는 코드가 나오고 이어서 다음과 같이 utils 패키지에 있는 StartNode함수를 호출합니다.

// Start up the node itself
utils.StartNode(stack)


따라서 실제 노드의 실행은 utils.StartNode로 들어가야 합니다. utils.StartNode함수는 아래와 같이 다행히 길지 않습니다. 코드를 차근히 살펴봅시다.

func StartNode(stack *node.Node) {
    if err := stack.Start(); err != nil {
        Fatalf("Error starting protocol stack: %v", err)
    }
    go func() {
        sigc := make(chan os.Signal, 1)
        signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
        defer signal.Stop(sigc)
        <-sigc
        log.Info("Got interrupt, shutting down...")
        go stack.Stop()
        for i := 10; i > 0; i-- {
            <-sigc
            if i > 1 {
                log.Warn("Already shutting down, interrupt more to panic.", "times", i-1)
            }
        }
        debug.Exit() // ensure trace and CPU profile data is flushed.
        debug.LoudPanic("boom")
    }()
}


사실 이 함수도 실제 노드를 실행하는 로직은 없습니다. stack.Start() 호출로 노드의 실행로직을 위임할 뿐입니다. 조금 더 아래부분의 코드를 훑어보면 stack.Stop 코드도 발견할 수 있습니다. 눈치가 빠르신 분들은 stack.Stop은 OS 시그널을 대기하다 호출되는 것이라는 것을 유추할 수 있을 것입니다. 좀 더 디테일한 설명은 잠시 뒤로 하고 노드의 시작과 종료로직을 갖는 stack.Startstack.Stop을 보겠습니다.

Node의 Start 함수

두 함수 모두 node 패키지의 node.go 파일에 선언되어 있습니다. 실질적으로 이더리움 네트워크의 노드를 실행하고 종료시키는 로직을 담당하는 함수라고 보면 됩니다. 이번 글은 이 두 함수가 핵심입니다. 가능하면 논리적인 단위로 코드를 쪼개서 살펴보고자 합니다. 그럼 먼저 Start부터 차근히 살펴봅시다.

Start 함수: 시그니쳐

함수를 읽으려고 하는데 함수의 시그니쳐 부터 뭔가 이상합니다.

func (n *Node) Start() error {


함수를 선언하는 키워드 func 뒤에 따라오는 (n * Node)가 뭘까요? 이 문법은 golang의 리시버입니다.

리시버

golang에는 자바 개발자인 제게 익숙한 클래스가 없습니다. 대신 구조체를 선언하고 메서드의 리시버를 이용하면 됩니다. 단어의 의미가 설명하듯 리시버는 함수에 전달할 수 있는 추가적인 파라미터입니다. 리시버가 붙은 함수를 우리에게 익숙한 메서드라는 용어로 대체할 수 있습니다.

지금까지 연재에서는 제가 정밀하지 못하게 함수와 메서드를 혼용했을수도 있습니다. ㅜㅜ

Start 메서드를 예제로 보면 함수를 리시버와 함께 선언했기 때문에 다음과 같은 형태의 호출이 가능하게 된 겁니다.

var node Node = /* 뭔가 초기화 */
node.Start() // <----- 바로 이렇게


리시버 어렵지 않지요? ^^ 그럼 이제 본격적으로 Start 함수… 아니 Start 메서드의 코드를 읽어봅시다.

Start 함수: 출발은 락 잡기

Start 메서드는 일단 락부터 잡고 시작합니다.

n.lock.Lock()
defer n.lock.Unlock()


코드 도입부에서 락을 잡는 것은 이해가 되는데 락을 잡자마자 락을 해제하는 다음 줄의 코드는 뭘까요? defer 라는 새로운 키워드에 힌트가 보이네요. 또 새로운 개념입니다!

defer

defer 키워드는 뒤따라오는 함수를 현재 실행중인 함수의 모든 로직을 수행한 후 실행하도록 미루는 역할을 합니다. 하나의 스택을 만들고 여기에 defer 키워드를 붙인 함수를 넣어둔 뒤 현재 함수의 종료시점에 순차적으로 꺼내서 실행한다고 보면 됩니다. defer를 쓰는 이유가 단순히 코드의 실행을 지연시키는 것만 있는 것은 아니지만 지금은 이 정도의 개념으로도 코드를 읽는데 충분한 것 같습니다. 자세한 내용은 아래 링크를 참조해 주세요.

  1. https://tour.golang.org/flowcontrol/12
  2. https://tour.golang.org/flowcontrol/13
  3. https://blog.golang.org/defer-panic-and-recover

Start 함수: 실행을 위한 준비

defer 키워드를 사용하여 함수의 시작과 종료시 락을 잡고 해제하는 것을 알았습니다. 락을 잡은 후에는 이미 노드가 실행 중인지 확인하는 코드가 따라옵니다.

// Short circuit if the node's already running
if n.server != nil {
    return ErrNodeRunning
}


주석의 Short circuit은 혹시라도 서버가 실행 중일 경우 빠르게 에러를 리턴하면서 이후 로직을 실행하지 않겠다는 하나의 패턴을 말합니다.

다음에는 노드에서 사용할 데이터 보관을 위하여 디렉토리를 엽니다.

if err := n.openDataDir(); err != nil {
    return err
}


이어서 네트워크에 참여할 실제 서버 노드 설정을 n.serverConfig라는 변수에 담습니다.

n.serverConfig = n.config.P2P


여기서 n.config.P2P는 지난 글에서 makeConfigNode 함수를 실행하면서 stack에 4가지 유형으로 초기설정한 것중 하나와 관련있습니다. 바로 Node의 설정입니다. Node 설정은 지난 글에서 defaultNodeConfig함수의 반환값으로 셋팅한다고 설명했습니다. 이 함수를 찾아서 들어가다 보면 node 패키지의 defaults.go 파일에서 다음과 같이 P2P 설정 초기값을 볼 수 있습니다.

// DefaultConfig contains reasonable default settings.
var DefaultConfig = Config{
    DataDir:          DefaultDataDir(),
    HTTPPort:         DefaultHTTPPort,
    HTTPModules:      []string{"net", "web3"},
    HTTPVirtualHosts: []string{"localhost"},
    WSPort:           DefaultWSPort,
    WSModules:        []string{"net", "web3"},
    P2P: p2p.Config{
        ListenAddr: ":30303",
        MaxPeers:   25,
        NAT:        nat.Any(),
    },
}


다시 Start메서드의 코드로 돌아오면 P2P 설정을 기본값으로 n.serverConfig에 할당한 후 추가로 여러 설정을 더하는 것을 볼 수 있습니다.

    n.serverConfig.PrivateKey = n.config.NodeKey()
    n.serverConfig.Name = n.config.NodeName()
    n.serverConfig.Logger = n.log
    if n.serverConfig.StaticNodes == nil {
        n.serverConfig.StaticNodes = n.config.StaticNodes()
    }
    if n.serverConfig.TrustedNodes == nil {
        n.serverConfig.TrustedNodes = n.config.TrustedNodes()
    }
    if n.serverConfig.NodeDatabase == "" {
        n.serverConfig.NodeDatabase = n.config.NodeDB()
    }


그리고 이러한 설정을 바탕으로 Server라는 구조체의 변수를 생성한 후 INFO 레벨로 p2p 노드를 시작한다고 로그를 찍습니다.

    running := &p2p.Server{Config: n.serverConfig}
    n.log.Info("Starting peer-to-peer node", "instance", n.serverConfig.Name)


다음으로 services라는 변수에 make함수를 이용하여 어떤 값을 셋팅하는 듯한 코드가 한줄 따라옵니다.

services := make(map[reflect.Type]Service)


이 코드에서 살펴볼 개념이 무려 3개나 있습니다. 먼저는 make 함수입니다.

make 함수

분명 지난 시간에 고루틴과 채널을 살펴보면서 채널변수를 생성할 때 make 함수를 사용했는데요. 이 코드는 map이란 타입의 변수를 생성하는 것으로 보입니다. 그렇다면 make는 채널 변수만을 위한 함수는 아니라는 의미겠지요?

공식 매뉴얼을 참조하면 makegolang에서 내장 지원하는 함수로 slice, map과 채널 변수의 생성과 초기화를 할 수 있다고 설명합니다. 특히 채널은 only라는 키워드가 붙은 것으로 봐서 make 함수를 사용하는 것이 유일한 방법이라는 것을 알 수 있습니다. 자세한 내용은 매뉴얼과 구글 번역기에 위임합니다.

https://golang.org/pkg/builtin/#make

map 타입

다음은 map 타입입니다. 사실 키워드 만으로 어떤 용도인지 유추가 됩니다. 자바개발자에게 익숙한 HashMap과 같은 키와 값의 쌍으로 구성된 자료구조일 것입니다. 그리고 실제로 그러합니다. golang에서 map은 다음과 같이 표현합니다.

map[키의 타입]값의 타입


구체적인 예를 들어봅시다. x,y 좌표를 필드로 갖는 Point라는 구조체를 선언하고 각 포인트를 표현하는 키를 string으로 표현한다면 다음과 같이 코드를 작성하면 됩니다. 참 쉽죠? ^^...

type Point struct {
    X, Y float64
}

positions := make(map[string]Point)


map도 기본적인 내용을 알면 소스를 읽는데 큰 문제가 없습니다. 아래 링크를 통해서 map도 추가로 더 깊이 알아보면 좋습니다.

Reflection

자 이제 한줄의 코드에 숨겨진 3가지 golang의 개념 중 남은 하나는 코드에서 map의 키로 사용된 reflect.Type과 관련된 개념입니다.

먼저 Reflection부터 알아봅시다. 리플렉션(Reflection)은 보통 프로그래밍 언어에서 런타임시에 타입을 다루는 고오급 기술에 해당하는데요. 자바 개발자라면 자바에도 리플렉션을 활용해서 런타임의 객체의 타입을 동적으로 다룰 수 있다는 것을 기억하실 겁니다. golang에서도 이처럼 언어차원에서 리플렉션을 지원하고 있고 reflect 패키지에 관련 내장함수를 갖고 있습니다. 다음이 공식 매뉴얼입니다.

https://golang.org/pkg/reflect/

그럼 reflect.Type은 뭘까요? 이건 마치 자바의 Class와 비교할 수 있습니다. 말 그대로 golang안에서 변수의 타입 자체를 의미합니다. 임의의 변수 a가 있다고 할 때 이 변수의 타입은 다음의 코드로 얻을 수 있답니다.

typeOfA := reflect.TypeOf(a)


하나의 동작하는 예제로 간단한 퀴즈를 내보겠습니다. 다음 코드의 출력결과 Type of a: 뒤에 타입은 뭐가 나올까요?

package main

import (
    "fmt"
    "reflect"
)

func main() {
    a := 9999

    var typeOfA reflect.Type = reflect.TypeOf(a)

    fmt.Println("Type of a: ", typeOfA)
}


정답은 int입니다. 리플렉션 별거 아니죠? 추가로 리플렉션에 대해 궁금한 부분은 아래 링크를 참조해 주세요

Start 함수: 서비스 실행을 위한 준비

이제 위에서 살펴본 아래 한줄의 코드의 의미는 Service를 값타입으로 하고 reflect.Type을 키타입으로 하는 mapservices 변수에 초기화 했다는 것을 알 수 있습니다.

services := make(map[reflect.Type]Service)


이제 services 변수에 무언가를 담는 코드가 뒤따라 나오겠지요? 맞습니다. 바로 이어지는 코드에서 for 문으로 NodeServiceFuncs 변수를 순회하는 로직을 보실수 있습니다.

for _, constructor := range n.serviceFuncs {


for 문에서 순회할 대상은 n.serviceFuncs이고 순회 결과는 _, constructor 2개가 나오네요. 파이썬이나 스칼라 등의 언어를 접해본 분이라면 코드만 보고 결과를 유추하실 수 있을겁니다. 그러나 가볍게 이 부분의 구문을 살펴보겠습니다.

for와 range

golangfor의 기본적인 구문은 다른 프로그래밍 언어와 크게 차이가 나지 않습니다. 다음의 Tour Of Go 몇부분만 따라가보면 쉽게 구문을 이해할 수 있습니다.

https://tour.golang.org/flowcontrol/1

마찬가지로 range또한 다른 프로그래밍 언어와 비슷한 기능입니다. 순회의 대상이 배열인 경우 배열의 인덱스와 요소를, 맵인 경우에는 키와 값을 반환합니다. 구체적인 예제는 다음 링크의 코드를 보면 구문을 이해하는데 도움이 될겁니다.

https://gobyexample.com/range

다시 코드로 돌아와서 인덱스는 필요하지 않아서 _로 표현했고 n.serviceFuncs의 요소를 constructor로 받았습니다. 변수명에서 서비스의 생성자 함수라는 것을 유추할 수 있군요. 이 for문이 빈 데이터를 순회하지는 않을겁니다. 분명 그럼 지난 글의 초기화 로직 어딘가에 NodeserviceFuncs에 값을 넣는 곳이 존재할 것으로 보입니다. 코드를 추적하기 위해 serviceFuncs를 참조하는 곳을 검색해보니 역시 힌트가 보입니다.

d05_service_func.png

우선 최초 Node 생성시에는 다음과 같이 빈 인터페이스 배열이 할당되어 있음

[]ServiceConstructor{}


이후 실제 배열에 값을 넣는 곳은 Node의 Register 메서드에서 일어납니다. 바로 위 그림에서 n.serviceFuncsappend 함수로 constructor를 포함시키는 부분이 Register 메서드의 코드입니다.

Node의 Register 메서드

지금 우리는 NodeStart메서드를 읽고 있는 중이었습니다만 Register 메서드를 자연스럽게 살펴보게 되었네요. 이 메서드의 코드는 간단합니다.

// Register injects a new service into the node's stack. The service created by
// the passed constructor must be unique in its type with regard to sibling ones.
func (n *Node) Register(constructor ServiceConstructor) error {
    n.lock.Lock()
    defer n.lock.Unlock()

    if n.server != nil {
        return ErrNodeRunning
    }
    n.serviceFuncs = append(n.serviceFuncs, constructor)
    return nil
}


Start 메서드와 동일하게 락부터 셋팅 하고 서버가 실행 중인 것을 확인 후 인자로 주어진 constructorNodeserviceFuncs의 포함시키고 있습니다.

여기서 제가 두루뭉실하게 넘어가고 싶지만 마지막으로 다뤄야 할 golang의 개념이 남았습니다. 아무런 설명 없이 append 함수를 배열에 요소를 붙이는 것으로 가정했습니다만… 실제 append함수는 golangslice를 위한 함수입니다. 어려운 개념은 아니니 아주 가볍게 훑고 넘어가보겠습니다.

array와 slice

배열(array)은 golang의 내장타입으로 보통의 프로그래밍 언어와 동일한 타입입니다. 배열이 가지는 중요한 특징은 배열의 사이즈가 고정되어 있다는 점입니다. 참으로 불편하지요? 그래서 우리는 slice를 씁니다.

slice는 사이즈가 동적인 배열이라고 보시면 됩니다. 그리고 동적인 배열 slice에 요소를 추가할 때 append 함수를 씁니다. 엄밀하게 slice는 배열의 포인터라고 볼 수 도 있을 겁니다. 자세한 내용은 공식 블로그의 친절한 설명으로 대체하겠습니다. ^^

https://blog.golang.org/go-slices-usage-and-internals

누가 Register 하는가?

바로 직전까지 우리는 Start 메서드에서 for 문으로 NodeserviceFuncs 변수를 순회하는 부분을 봤습니다. serviceFuncsNodeRegister 메서드를 통해서 추가된다는 것도 확인했구요. 그럼 남은 질문 하나는 어디서 누가 Register 메서드를 호출하여 serviceFuncsconstructor를 넣고 있을까요?

지난 글에서 makeFullNode 함수를 기억하시나요? 이 함수에서는 Node 변수의 생성과 설정값을 초기화한 뒤에 4개의 서비스를 등록하는 코드가 나오는데요. 이 중에서 터미널에 옵션과 상관없이 무조건 호출되는 한개의 함수가 있었습니다.

utils.RegisterEthService(stack, &cfg.Eth)


바로 이 함수 안에서 stack.Register 메서드를 호출하여 서비스의 생성자 함수를 전달하고 있습니다. 그럼 생성자 함수를 봐야겠지요?... 이 부분은 다음 연재에서 다뤄 보도록 하겠습니다. 왜냐하면 바로 이 생성자 함수로부터 파생되는 코드가 결국 이더리움 네트워크에 참여하는 핵심 코드가 될 것 같기 때문입니다.

Start 함수: 서비스의 실행

남은 코드는 실제 serviceFuncs의 포함된 constructor를 호출하여 서비스 변수를 생성 및 초기화하고 이를 실행하는 것과 연관된 코드입니다. 먼저 이미 살펴봤던 첫번째 for문의 다음 코드는 생성자를 호출하고 이를 services 맵에 담아서 서비스 변수를 관리하는 로직입니다.

for _, constructor := range n.serviceFuncs {
        // Create a new context for the particular service
        ctx := &ServiceContext{
            config:         n.config,
            services:       make(map[reflect.Type]Service),
            EventMux:       n.eventmux,
            AccountManager: n.accman,
        }
        for kind, s := range services { // copy needed for threaded access
            ctx.services[kind] = s
        }
        // Construct and save the service
        service, err := constructor(ctx)
        if err != nil {
            return err
        }
        kind := reflect.TypeOf(service)
        if _, exists := services[kind]; exists {
            return &DuplicateServiceError{Kind: kind}
        }
        services[kind] = service
    }


코드 한 가운데 생성자를 호출하는 부분이 보이시나요?

service, err := constructor(ctx)

또한 앞서 golang의 구문을 설명했기 때문에 코드 중간에 kind := reflect.TypeOf(service)가 의미하는 바를 아실 수 있을겁니다. kind라는 변수는 service 타입을 담고 있을 겁니다. 마지막 줄에 결국 services 맵에 키를 kind로 값을 service로 하여 저장하네요.

다음 for 문에서는 서비스마다 갖고 있는 프로토콜을 running 변수에 포함시킵니다.

// Gather the protocols and start the freshly assembled P2P server
for _, service := range services {
    running.Protocols = append(running.Protocols, service.Protocols()...)
}


자세한 내용은 나중에 다시 보더라도 지금은 P2P 서버 역할을 할 running에 각 서비스별 정보를 전달한다고 이해하면 될 것 같군요. 이어서 running 변수를 실행합니다. 이 Start 메서드도 들여다 보고 싶지만… 글의 제약상 기능적으로 P2P 서버를 실행한다고 이해하고 넘어가야겠습니다.

if err := running.Start(); err != nil {
    return convertFileLockError(err)
}


드디어 위에서 생성한 각 서비스를 순차적으로 실행하고, 혹시 하나라도 시작하지 못하면 모두 종료시키는 코드가 나옵니다.

started := []reflect.Type{}
for kind, service := range services {
    // Start the next service, stopping all previous upon failure
    if err := service.Start(running); err != nil {
        for _, kind := range started {
            services[kind].Stop()
        }
        running.Stop()

        return err
    }
    // Mark the service started for potential cleanup
    started = append(started, kind)
}


거의 다 왔습니다. 마지막으로 노드에 접속할 엔드포인트로 RPC 관련 기능을 실행합니다. startRPC를 들여다 보면 HTTP, WebSocket등 geth가 지원하는 여러 엔드포인트용 서비스를 실행합니다.

// Lastly start the configured RPC interfaces
if err := n.startRPC(services); err != nil {
    for _, service := range services {
        service.Stop()
    }
    running.Stop()
    return err
}


Start 메서드의 마지막입니다. 성공적으로 실행한 변수를 Node 에 저장합니다.

// Finish initializing the startup
n.services = services
n.server = running
n.stop = make(chan struct{})

return nil

Node의 Stop 메서드

조립은 분해의 역순! 서비스의 종료도 시작한 것을 순차적으로 종료시키면 됩니다. Stop 메서드의 코드는 상대적으로 간단합니다. 이전에 살펴본 NodeStart, Register와 동일하게 락을 잡고 서버의 실행 여부 확인 코드는 동일합니다.

이어서 서비스를 순차적으로 종료하고 중간에 종료에 실패한 서비스가 있다면 따로 담아두는 코드가 나옵니다.

n.stopWS()
n.stopHTTP()
n.stopIPC()
n.rpcAPIs = nil
failure := &StopError{
    Services: make(map[reflect.Type]error),
}
for kind, service := range n.services {
    if err := service.Stop(); err != nil {
        failure.Services[kind] = err
    }
}
n.server.Stop()
n.services = nil
n.server = nil


이후 geth가 사용한 디렉토리에 대한 락을 해제합니다.

// Release instance directory lock.
if n.instanceDirLock != nil {
    if err := n.instanceDirLock.Release(); err != nil {
        n.log.Error("Can't release datadir lock", "err", err)
    }
    n.instanceDirLock = nil
}


지난 글에서 node.Wait의 코드를 읽으면서 잠시 언급했었던 stop 채널을 닫는 close 함수를 호출합니다.

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


이후에는 임시로 등록한 계정 파일이 있으면 지우고 위에서 종료하다 실패한 서비스가 있으면 반환하는 등의 부수적인 처리로 함수의 실행을 마무리합니다.

// Remove the keystore if it was created ephemerally.
var keystoreErr error
if n.ephemeralKeystore != "" {
    keystoreErr = os.RemoveAll(n.ephemeralKeystore)
}

if len(failure.Services) > 0 {
    return failure
}
if keystoreErr != nil {
    return keystoreErr
}
return nil

드디어 Stop 메서드까지 봤네요. 여기까지 따라오셨다면 node 패키지의 node.go 파일의 상당수 로직을 읽은 것과 다름 없습니다.

utils.StartNode(stack) 함수 남은 부분

앞서 stack.Start 호출과 함께 우리는 Node 타입의 Start, Register, Stop까지 봤었습니다. 이제 글의 서두에서 첫 시작이었던 utils.StartNode에서 후반부 남아 있던 로직을 마지막으로 봅시다.

아랫 부분 코드를 보면 지난 시간에 살펴본 고루틴이 나오네요. 여기서는 go func() {...}()와 같이 익명함수로 바로 고루틴을 실행시키는 것을 알 수 있습니다. 그럼 이 익명함수는 무슨 일을 하는지 익명함수 내부의 코드를 가져와서 자세히 보겠습니다.

sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigc)
<-sigc
log.Info("Got interrupt, shutting down...")
go stack.Stop()


먼저 Signal 타입의 채널 sigc를 생성하네요. os.Signal을 본 적은 없지만 느낌상 golang에서 지원하는 운영체제의 시그늘을 핸들링하는 타입으로 보입니다. 마찬가지로 이어서 signal 패키지의 Notify라는 함수에 sigc 채널변수와 상수처럼 보이는 SIGINT, SIGTERM을 전달하고 있습니다. 운영체제로부터 SIGINTSIGTERM 시그널을 받으면 sigc 채널에 무언가 알려달라고 등록하는 것 같군요. 모두 golang의 빌트인 패키지 입니다. 내용을 찾아보면 실제로도 그러합니다.

이어서 defer키워드가 나오네요. 이미 함께 공부했듯이 signal.Stop(sigc)로직을 이 익명함수 실행의 마지막으로 미뤄뒀습니다.

이후 로직은 지난시간에 살펴본 채널의 문법을 사용하고 있습니다.터미널에서 SIGINT, SIGTERM 시그널을 기다리다가 시그널 받으면 stack.Stop 호출합니다. 바로 앞서 살펴본 Stop 메서드를 실행하라는 의미겠지요? 그리고 아래 추가로 for문이 나옵니다만 이 로직은 강제로 서비스를 중지하는 기능으로 보입니다.

for i := 10; i > 0; i-- {
    <-sigc
    if i > 1 {
        log.Warn("Already shutting down, interrupt more to panic.", "times", i-1)
    }
}
debug.Exit() // ensure trace and CPU profile data is flushed.
debug.LoudPanic("boom")


이렇게 함수가 종료되면 defer로 미뤄뒀던 signal.Stop(sigc)이 실행될 겁니다. 프로세스를 종료할 것이기 때문에 Notify로 등록한 SIGINT, SIGTERM신호를 더이상 기다릴 필요가 없다는 의미의 호출이란 것을 알 수 있습니다.

마치며

오늘 정말 많은 내용을 다뤘습니다. 특히 golang의 여러 구문들을 더 중점적으로 봤습니다. 벌써 오늘까지 5번째 연재로 geth 소스를 읽으면서 우리는 이제 golang의 기본적인 구문과 함께 geth의 기본적인 실행과 종료 로직을 다 본것입니다.

다음시간에는 오늘 다뤘던 serviceFuncs에 포함되어 있는 서비스의 생성자에서 출발할 예정입니다. 특히 지난 시간에 가볍게 지나쳤던 RegisterEthService 메서드를 봅니다. 바로 이 코드에서 드디어 geth의 코어인 이더리움 객체를 생성하고 실행하는 부분이 이어집니다.

Sort:  

Congratulations @woojin.joe! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of posts published

Click on any badge to view your own Board of Honor on SteemitBoard.

To support your work, I also upvoted your post!
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

Upvote this notification to help all Steemit users. Learn why here!

pairplay 가 kr-dev를 응원합니다!

좋은 글 감사합니다.