자바 개발자의 go-ethereum 소스 읽기: Day 5
kr·@woojin.joe·
0.000 HBD자바 개발자의 go-ethereum 소스 읽기: Day 5
# 자바 개발자의 go-ethereum 소스 읽기: Day 5

이 글은 자바 개발자의 go-ethereum(geth 클라이언트) 소스 분석기 시리즈의 연재 중 다섯 번째 글입니다. 앞으로 다음과 같은 내용으로 연재를 계획하고 있습니다.
1. [Day 01: Geth 1.0 소스 받기 및 코드 분석을 위한 개발환경 셋팅(VS Code)](https://steemit.com/kr/@woojin.joe/go-ethereum-geth-day-01)
2. [Day 02: CLI 라이브러리 기반 `geth`의 전체 실행 구조](https://steemit.com/kr/@woojin.joe/go-ethereum-geth-day-02)
3. [Day 03: VS Code를 사용한 `geth ` 디버깅](https://steemit.com/kr/@woojin.joe/go-ethereum-geth-day-3)
4. [Day 04 `geth` 노드 실행 로직](https://steemit.com/kr/@woojin.joe/go-ethereum-day-4)
5. **(본 글)** Day 05 `geth`의 실행과 종료
전체 연재 목록은 아래 페이지에서 확인해 주세요
http://www.notforme.kr/block-chain/geth-code-reading
## 대상 독자 및 연재 목표
이 연재는 먼저 독자 분들이 적어도 Java와 같은 OOP 계열의 언어로 프로그래밍 경험이 있다는 것을 가정합니다. 또한 계정, 채굴 등 블록체인과 이더리움과 관련된 기초적인 개념을 알고 있다고 가정합니다.
더불어 이 연재는 다음 3가지 목적을 염두하고 쓴 것입니다.
1. 새로운 언어(`Go`)를 오픈소스 코드를 읽으며 배운다.
2. 오픈소스를 읽으며 코드리딩 능력을 배양한다.
3. 블록체인의 기술을 직접 코드를 통해서 익힌다.
## 다루는 내용
오늘은 [지난 글](https://steemit.com/kr/@woojin.joe/go-ethereum-day-4)에서 미뤄두었던 `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](https://github.com/ethereum/go-ethereum/commit/577d375a0df08710e52b1c38720f98a7f25d206a) 입니다. 참고 부탁드립니다.
## utils.StartNode(stack) 함수
`startNode` 함수의 코드를 보면 첫 줄은 `debug`로 시작하는 코드가 나오고 이어서 다음과 같이 `utils` 패키지에 있는 `StartNode`함수를 호출합니다.
```go
// Start up the node itself
utils.StartNode(stack)
```
\
따라서 실제 노드의 실행은 `utils.StartNode`로 들어가야 합니다. `utils.StartNode`함수는 아래와 같이 다행히 길지 않습니다. 코드를 차근히 살펴봅시다.
```go
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.Start`와 `stack.Stop`을 보겠습니다.
## Node의 Start 함수
두 함수 모두 `node` 패키지의 `node.go` 파일에 선언되어 있습니다. 실질적으로 이더리움 네트워크의 노드를 실행하고 종료시키는 로직을 담당하는 함수라고 보면 됩니다. 이번 글은 이 두 함수가 핵심입니다. 가능하면 논리적인 단위로 코드를 쪼개서 살펴보고자 합니다. 그럼 먼저 `Start`부터 차근히 살펴봅시다.
### Start 함수: 시그니쳐
함수를 읽으려고 하는데 함수의 시그니쳐 부터 뭔가 이상합니다.
```go
func (n *Node) Start() error {
```
\
함수를 선언하는 키워드 `func` 뒤에 따라오는 `(n * Node)`가 뭘까요? 이 문법은 `golang`의 리시버입니다.
#### 리시버
`golang`에는 자바 개발자인 제게 익숙한 클래스가 없습니다. 대신 구조체를 선언하고 메서드의 [리시버](https://golang.org/ref/spec#Method_declarations)를 이용하면 됩니다. 단어의 의미가 설명하듯 리시버는 함수에 전달할 수 있는 추가적인 파라미터입니다. 리시버가 붙은 함수를 우리에게 익숙한 메서드라는 용어로 대체할 수 있습니다.
> 지금까지 연재에서는 제가 정밀하지 못하게 함수와 메서드를 혼용했을수도 있습니다. ㅜㅜ
위 `Start` 메서드를 예제로 보면 함수를 리시버와 함께 선언했기 때문에 다음과 같은 형태의 호출이 가능하게 된 겁니다.
```go
var node Node = /* 뭔가 초기화 */
node.Start() // <----- 바로 이렇게
```
\
리시버 어렵지 않지요? ^^ 그럼 이제 본격적으로 `Start` 함수… 아니 `Start` 메서드의 코드를 읽어봅시다.
### Start 함수: 출발은 락 잡기
`Start` 메서드는 일단 락부터 잡고 시작합니다.
```go
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` 키워드를 사용하여 함수의 시작과 종료시 락을 잡고 해제하는 것을 알았습니다. 락을 잡은 후에는 이미 노드가 실행 중인지 확인하는 코드가 따라옵니다.
```go
// Short circuit if the node's already running
if n.server != nil {
return ErrNodeRunning
}
```
\
주석의 `Short circuit`은 혹시라도 서버가 실행 중일 경우 빠르게 에러를 리턴하면서 이후 로직을 실행하지 않겠다는 하나의 패턴을 말합니다.
다음에는 노드에서 사용할 데이터 보관을 위하여 디렉토리를 엽니다.
```go
if err := n.openDataDir(); err != nil {
return err
}
```
\
이어서 네트워크에 참여할 실제 서버 노드 설정을 `n.serverConfig`라는 변수에 담습니다.
```go
n.serverConfig = n.config.P2P
```
\
여기서 `n.config.P2P`는 지난 글에서 `makeConfigNode` 함수를 실행하면서 `stack`에 4가지 유형으로 초기설정한 것중 하나와 관련있습니다. 바로 Node의 설정입니다. Node 설정은 지난 글에서 `defaultNodeConfig`함수의 반환값으로 셋팅한다고 설명했습니다. 이 함수를 찾아서 들어가다 보면 `node` 패키지의 `defaults.go` 파일에서 다음과 같이 `P2P` 설정 초기값을 볼 수 있습니다.
```go
// 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`에 할당한 후 추가로 여러 설정을 더하는 것을 볼 수 있습니다.
```go
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 노드를 시작한다고 로그를 찍습니다.
```go
running := &p2p.Server{Config: n.serverConfig}
n.log.Info("Starting peer-to-peer node", "instance", n.serverConfig.Name)
```
\
다음으로 services라는 변수에 `make`함수를 이용하여 어떤 값을 셋팅하는 듯한 코드가 한줄 따라옵니다.
```go
services := make(map[reflect.Type]Service)
```
\
이 코드에서 살펴볼 개념이 무려 3개나 있습니다. 먼저는 `make` 함수입니다.
#### make 함수
분명 지난 시간에 고루틴과 채널을 살펴보면서 채널변수를 생성할 때 `make` 함수를 사용했는데요. 이 코드는 `map`이란 타입의 변수를 생성하는 것으로 보입니다. 그렇다면 `make`는 채널 변수만을 위한 함수는 아니라는 의미겠지요?
[공식 매뉴얼](https://golang.org/pkg/builtin/#make)을 참조하면 `make`는 `golang`에서 내장 지원하는 함수로 `slice`, `map`과 채널 변수의 생성과 초기화를 할 수 있다고 설명합니다. 특히 채널은 `only`라는 키워드가 붙은 것으로 봐서 `make` 함수를 사용하는 것이 유일한 방법이라는 것을 알 수 있습니다. 자세한 내용은 매뉴얼과 구글 번역기에 위임합니다.
> https://golang.org/pkg/builtin/#make
#### map 타입
다음은 `map` 타입입니다. 사실 키워드 만으로 어떤 용도인지 유추가 됩니다. 자바개발자에게 익숙한 `HashMap`과 같은 키와 값의 쌍으로 구성된 자료구조일 것입니다. 그리고 실제로 그러합니다. `golang`에서 `map`은 다음과 같이 표현합니다.
```go
map[키의 타입]값의 타입
```
\
구체적인 예를 들어봅시다. x,y 좌표를 필드로 갖는 `Point`라는 구조체를 선언하고 각 포인트를 표현하는 키를 `string`으로 표현한다면 다음과 같이 코드를 작성하면 됩니다. 참 쉽죠? ^^...
```go
type Point struct {
X, Y float64
}
positions := make(map[string]Point)
```
\
자 `map`도 기본적인 내용을 알면 소스를 읽는데 큰 문제가 없습니다. 아래 링크를 통해서 `map`도 추가로 더 깊이 알아보면 좋습니다.
* https://tour.golang.org/moretypes/19
* https://blog.golang.org/go-maps-in-action
#### Reflection
자 이제 한줄의 코드에 숨겨진 3가지 `golang`의 개념 중 남은 하나는 코드에서 `map`의 키로 사용된 `reflect.Type`과 관련된 개념입니다.
먼저 `Reflection`부터 알아봅시다. 리플렉션(`Reflection`)은 보통 프로그래밍 언어에서 런타임시에 타입을 다루는 고오급 기술에 해당하는데요. 자바 개발자라면 자바에도 리플렉션을 활용해서 런타임의 객체의 타입을 동적으로 다룰 수 있다는 것을 기억하실 겁니다. `golang`에서도 이처럼 언어차원에서 리플렉션을 지원하고 있고 `reflect` 패키지에 관련 내장함수를 갖고 있습니다. 다음이 공식 매뉴얼입니다.
> https://golang.org/pkg/reflect/
그럼 `reflect.Type`은 뭘까요? 이건 마치 자바의 [Class](https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html)와 비교할 수 있습니다. 말 그대로 `golang`안에서 변수의 [타입](https://golang.org/pkg/reflect/#Type) 자체를 의미합니다. 임의의 변수 `a`가 있다고 할 때 이 변수의 타입은 다음의 코드로 얻을 수 있답니다.
```go
typeOfA := reflect.TypeOf(a)
```
\
하나의 동작하는 [예제](https://play.golang.org/p/USgSL9MRFUC)로 간단한 퀴즈를 내보겠습니다. 다음 코드의 출력결과 `Type of a: ` 뒤에 타입은 뭐가 나올까요?
```go
package main
import (
"fmt"
"reflect"
)
func main() {
a := 9999
var typeOfA reflect.Type = reflect.TypeOf(a)
fmt.Println("Type of a: ", typeOfA)
}
```
\
정답은 `int`입니다. 리플렉션 별거 아니죠? 추가로 리플렉션에 대해 궁금한 부분은 아래 링크를 참조해 주세요
* https://blog.golang.org/laws-of-reflection
* https://www.slideshare.net/dahlmoon/reflect-package
### Start 함수: 서비스 실행을 위한 준비
이제 위에서 살펴본 아래 한줄의 코드의 의미는 `Service`를 값타입으로 하고 `reflect.Type`을 키타입으로 하는 `map`을 `services` 변수에 초기화 했다는 것을 알 수 있습니다.
```go
services := make(map[reflect.Type]Service)
```
\
이제 `services` 변수에 무언가를 담는 코드가 뒤따라 나오겠지요? 맞습니다. 바로 이어지는 코드에서 `for` 문으로 `Node`의 `ServiceFuncs` 변수를 순회하는 로직을 보실수 있습니다.
```go
for _, constructor := range n.serviceFuncs {
```
\
`for` 문에서 순회할 대상은 `n.serviceFuncs`이고 순회 결과는 `_`, `constructor` 2개가 나오네요. 파이썬이나 스칼라 등의 언어를 접해본 분이라면 코드만 보고 결과를 유추하실 수 있을겁니다. 그러나 가볍게 이 부분의 구문을 살펴보겠습니다.
#### for와 range
`golang`의 `for`의 기본적인 구문은 다른 프로그래밍 언어와 크게 차이가 나지 않습니다. 다음의 Tour Of Go 몇부분만 따라가보면 쉽게 구문을 이해할 수 있습니다.
> https://tour.golang.org/flowcontrol/1
마찬가지로 `range`또한 다른 프로그래밍 언어와 비슷한 기능입니다. 순회의 대상이 배열인 경우 배열의 인덱스와 요소를, 맵인 경우에는 키와 값을 반환합니다. 구체적인 예제는 다음 링크의 코드를 보면 구문을 이해하는데 도움이 될겁니다.
> https://gobyexample.com/range
다시 코드로 돌아와서 인덱스는 필요하지 않아서 `_`로 표현했고 `n.serviceFuncs`의 요소를 `constructor`로 받았습니다. 변수명에서 서비스의 생성자 함수라는 것을 유추할 수 있군요. 이 `for`문이 빈 데이터를 순회하지는 않을겁니다. 분명 그럼 지난 글의 초기화 로직 어딘가에 `Node`의 `serviceFuncs`에 값을 넣는 곳이 존재할 것으로 보입니다. 코드를 추적하기 위해 `serviceFuncs`를 참조하는 곳을 검색해보니 역시 힌트가 보입니다.

우선 최초 Node 생성시에는 다음과 같이 빈 인터페이스 배열이 할당되어 있음
```go
[]ServiceConstructor{}
```
\
이후 실제 배열에 값을 넣는 곳은 Node의 `Register` 메서드에서 일어납니다. 바로 위 그림에서 `n.serviceFuncs`에 `append` 함수로 `constructor`를 포함시키는 부분이 `Register` 메서드의 코드입니다.
### Node의 Register 메서드
지금 우리는 `Node`의 `Start`메서드를 읽고 있는 중이었습니다만 `Register` 메서드를 자연스럽게 살펴보게 되었네요. 이 메서드의 코드는 간단합니다.
```go
// 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` 메서드와 동일하게 락부터 셋팅 하고 서버가 실행 중인 것을 확인 후 인자로 주어진 `constructor`를 `Node`의 `serviceFuncs`의 포함시키고 있습니다.
여기서 제가 두루뭉실하게 넘어가고 싶지만 마지막으로 다뤄야 할 `golang`의 개념이 남았습니다. 아무런 설명 없이 `append` 함수를 배열에 요소를 붙이는 것으로 가정했습니다만… 실제 `append`함수는 `golang`의 `slice`를 위한 함수입니다. 어려운 개념은 아니니 아주 가볍게 훑고 넘어가보겠습니다.
#### array와 slice
배열(array)은 `golang`의 내장타입으로 보통의 프로그래밍 언어와 동일한 타입입니다. 배열이 가지는 중요한 특징은 배열의 사이즈가 고정되어 있다는 점입니다. 참으로 불편하지요? 그래서 우리는 `slice`를 씁니다.
`slice`는 사이즈가 동적인 배열이라고 보시면 됩니다. 그리고 동적인 배열 `slice`에 요소를 추가할 때 `append` 함수를 씁니다. 엄밀하게 `slice`는 배열의 포인터라고 볼 수 도 있을 겁니다. 자세한 내용은 공식 블로그의 친절한 설명으로 대체하겠습니다. ^^
> https://blog.golang.org/go-slices-usage-and-internals
### 누가 Register 하는가?
바로 직전까지 우리는 `Start` 메서드에서 `for` 문으로 `Node`의 `serviceFuncs` 변수를 순회하는 부분을 봤습니다. `serviceFuncs`는 `Node` 의 `Register` 메서드를 통해서 추가된다는 것도 확인했구요. 그럼 남은 질문 하나는 어디서 누가 `Register` 메서드를 호출하여 `serviceFuncs` 에 `constructor`를 넣고 있을까요?
지난 글에서 `makeFullNode` 함수를 기억하시나요? 이 함수에서는 `Node` 변수의 생성과 설정값을 초기화한 뒤에 4개의 서비스를 등록하는 코드가 나오는데요. 이 중에서 터미널에 옵션과 상관없이 무조건 호출되는 한개의 함수가 있었습니다.
```go
utils.RegisterEthService(stack, &cfg.Eth)
```
\
바로 이 함수 안에서 `stack.Register` 메서드를 호출하여 서비스의 생성자 함수를 전달하고 있습니다. 그럼 생성자 함수를 봐야겠지요?... 이 부분은 다음 연재에서 다뤄 보도록 하겠습니다. 왜냐하면 바로 이 생성자 함수로부터 파생되는 코드가 결국 이더리움 네트워크에 참여하는 핵심 코드가 될 것 같기 때문입니다.
### Start 함수: 서비스의 실행
남은 코드는 실제 `serviceFuncs`의 포함된 `constructor`를 호출하여 서비스 변수를 생성 및 초기화하고 이를 실행하는 것과 연관된 코드입니다. 먼저 이미 살펴봤던 첫번째 `for`문의 다음 코드는 생성자를 호출하고 이를 `services` 맵에 담아서 서비스 변수를 관리하는 로직입니다.
```go
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` 변수에 포함시킵니다.
```go
// 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 서버를 실행한다고 이해하고 넘어가야겠습니다.
```go
if err := running.Start(); err != nil {
return convertFileLockError(err)
}
```
\
드디어 위에서 생성한 각 서비스를 순차적으로 실행하고, 혹시 하나라도 시작하지 못하면 모두 종료시키는 코드가 나옵니다.
```go
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`가 지원하는 여러 엔드포인트용 서비스를 실행합니다.
```go
// 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` 에 저장합니다.
```go
// Finish initializing the startup
n.services = services
n.server = running
n.stop = make(chan struct{})
return nil
```
## Node의 Stop 메서드
조립은 분해의 역순! 서비스의 종료도 시작한 것을 순차적으로 종료시키면 됩니다. `Stop` 메서드의 코드는 상대적으로 간단합니다. 이전에 살펴본 `Node`의 `Start`, `Register`와 동일하게 락을 잡고 서버의 실행 여부 확인 코드는 동일합니다.
이어서 서비스를 순차적으로 종료하고 중간에 종료에 실패한 서비스가 있다면 따로 담아두는 코드가 나옵니다.
```go
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`가 사용한 디렉토리에 대한 락을 해제합니다.
```go
// 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 함수를 호출합니다.
```go
// unblock n.Wait
close(n.stop)
```
\
이후에는 임시로 등록한 계정 파일이 있으면 지우고 위에서 종료하다 실패한 서비스가 있으면 반환하는 등의 부수적인 처리로 함수의 실행을 마무리합니다.
```go
// 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() {...}()`와 같이 익명함수로 바로 고루틴을 실행시키는 것을 알 수 있습니다. 그럼 이 익명함수는 무슨 일을 하는지 익명함수 내부의 코드를 가져와서 자세히 보겠습니다.
```go
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`을 전달하고 있습니다. 운영체제로부터 `SIGINT`나 `SIGTERM` 시그널을 받으면 `sigc` 채널에 무언가 알려달라고 등록하는 것 같군요. 모두 `golang`의 빌트인 패키지 입니다. 내용을 찾아보면 실제로도 그러합니다.
이어서 `defer`키워드가 나오네요. 이미 함께 공부했듯이 `signal.Stop(sigc)`로직을 이 익명함수 실행의 마지막으로 미뤄뒀습니다.
이후 로직은 지난시간에 살펴본 채널의 문법을 사용하고 있습니다.터미널에서 SIGINT, SIGTERM 시그널을 기다리다가 시그널 받으면 stack.Stop 호출합니다. 바로 앞서 살펴본 `Stop` 메서드를 실행하라는 의미겠지요? 그리고 아래 추가로 `for`문이 나옵니다만 이 로직은 강제로 서비스를 중지하는 기능으로 보입니다.
```go
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`의 코어인 이더리움 객체를 생성하고 실행하는 부분이 이어집니다.👍 cicada0014, hr1, anomaly, moby-dick, doctorbme, sensation, steemitboard, woojin.joe, endiyou, minampusan, opsteemit, pairplay, krwhale, ingee, ryukato,