자바 개발자의 go-ethereum 소스 읽기: Day 5
자바 개발자의 go-ethereum 소스 읽기: Day 5
이 글은 자바 개발자의 go-ethereum(geth 클라이언트) 소스 분석기 시리즈의 연재 중 다섯 번째 글입니다. 앞으로 다음과 같은 내용으로 연재를 계획하고 있습니다.
- Day 01: Geth 1.0 소스 받기 및 코드 분석을 위한 개발환경 셋팅(VS Code)
- Day 02: CLI 라이브러리 기반
geth
의 전체 실행 구조 - Day 03: VS Code를 사용한
geth
디버깅 - Day 04
geth
노드 실행 로직 - (본 글) Day 05
geth
의 실행과 종료
전체 연재 목록은 아래 페이지에서 확인해 주세요
http://www.notforme.kr/block-chain/geth-code-reading
대상 독자 및 연재 목표
이 연재는 먼저 독자 분들이 적어도 Java와 같은 OOP 계열의 언어로 프로그래밍 경험이 있다는 것을 가정합니다. 또한 계정, 채굴 등 블록체인과 이더리움과 관련된 기초적인 개념을 알고 있다고 가정합니다.
더불어 이 연재는 다음 3가지 목적을 염두하고 쓴 것입니다.
- 새로운 언어(
Go
)를 오픈소스 코드를 읽으며 배운다. - 오픈소스를 읽으며 코드리딩 능력을 배양한다.
- 블록체인의 기술을 직접 코드를 통해서 익힌다.
다루는 내용
오늘은 지난 글에서 미뤄두었던 geth
함수의 startNode(ctx, node)
함수를 분석할 예정입니다. 오늘은 다룰 내용이 만만치 않습니다. 새로운 golang
의 구문들이 마구 등장하기 때문입니다…. golang
의 구문 자체도 다룰 내용이 많은데 코드도 읽어야 하니 오늘 글은 다소 산만할 수도 있습니다. 이미 golang
이 익숙한 분들은 해당 내용은 빠르게 지나가셔도 좋습니다.
구체적으로 다음의 내용을 코드를 읽는데 필요한 수준으로 함께 살펴볼 예정입니다. 아마 오늘 글에서 살펴본 개념이면 기본적인 golang
의 문법의 80%정도는 다뤘다고 봐도 될거 같습니다. 다음 연재부터는 golang
의 구문으로 코드 읽는 고생은 거의 없지 않을까 싶습니다.
- 리시버
- defer
- make 함수
- map
- Reflection
- for와 range
- 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.Start
와 stack.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
를 쓰는 이유가 단순히 코드의 실행을 지연시키는 것만 있는 것은 아니지만 지금은 이 정도의 개념으로도 코드를 읽는데 충분한 것 같습니다. 자세한 내용은 아래 링크를 참조해 주세요.
- https://tour.golang.org/flowcontrol/12
- https://tour.golang.org/flowcontrol/13
- 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
는 채널 변수만을 위한 함수는 아니라는 의미겠지요?
공식 매뉴얼을 참조하면 make
는 golang
에서 내장 지원하는 함수로 slice
, map
과 채널 변수의 생성과 초기화를 할 수 있다고 설명합니다. 특히 채널은 only
라는 키워드가 붙은 것으로 봐서 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
패키지에 관련 내장함수를 갖고 있습니다. 다음이 공식 매뉴얼입니다.
그럼 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
을 키타입으로 하는 map
을 services
변수에 초기화 했다는 것을 알 수 있습니다.
services := make(map[reflect.Type]Service)
이제 services
변수에 무언가를 담는 코드가 뒤따라 나오겠지요? 맞습니다. 바로 이어지는 코드에서 for
문으로 Node
의 ServiceFuncs
변수를 순회하는 로직을 보실수 있습니다.
for _, constructor := range n.serviceFuncs {
for
문에서 순회할 대상은 n.serviceFuncs
이고 순회 결과는 _
, constructor
2개가 나오네요. 파이썬이나 스칼라 등의 언어를 접해본 분이라면 코드만 보고 결과를 유추하실 수 있을겁니다. 그러나 가볍게 이 부분의 구문을 살펴보겠습니다.
for와 range
golang
의 for
의 기본적인 구문은 다른 프로그래밍 언어와 크게 차이가 나지 않습니다. 다음의 Tour Of Go 몇부분만 따라가보면 쉽게 구문을 이해할 수 있습니다.
마찬가지로 range
또한 다른 프로그래밍 언어와 비슷한 기능입니다. 순회의 대상이 배열인 경우 배열의 인덱스와 요소를, 맵인 경우에는 키와 값을 반환합니다. 구체적인 예제는 다음 링크의 코드를 보면 구문을 이해하는데 도움이 될겁니다.
다시 코드로 돌아와서 인덱스는 필요하지 않아서 _
로 표현했고 n.serviceFuncs
의 요소를 constructor
로 받았습니다. 변수명에서 서비스의 생성자 함수라는 것을 유추할 수 있군요. 이 for
문이 빈 데이터를 순회하지는 않을겁니다. 분명 그럼 지난 글의 초기화 로직 어딘가에 Node
의 serviceFuncs
에 값을 넣는 곳이 존재할 것으로 보입니다. 코드를 추적하기 위해 serviceFuncs
를 참조하는 곳을 검색해보니 역시 힌트가 보입니다.
우선 최초 Node 생성시에는 다음과 같이 빈 인터페이스 배열이 할당되어 있음
[]ServiceConstructor{}
이후 실제 배열에 값을 넣는 곳은 Node의 Register
메서드에서 일어납니다. 바로 위 그림에서 n.serviceFuncs
에 append
함수로 constructor
를 포함시키는 부분이 Register
메서드의 코드입니다.
Node의 Register 메서드
지금 우리는 Node
의 Start
메서드를 읽고 있는 중이었습니다만 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
메서드와 동일하게 락부터 셋팅 하고 서버가 실행 중인 것을 확인 후 인자로 주어진 constructor
를 Node
의 serviceFuncs
의 포함시키고 있습니다.
여기서 제가 두루뭉실하게 넘어가고 싶지만 마지막으로 다뤄야 할 golang
의 개념이 남았습니다. 아무런 설명 없이 append
함수를 배열에 요소를 붙이는 것으로 가정했습니다만… 실제 append
함수는 golang
의 slice
를 위한 함수입니다. 어려운 개념은 아니니 아주 가볍게 훑고 넘어가보겠습니다.
array와 slice
배열(array)은 golang
의 내장타입으로 보통의 프로그래밍 언어와 동일한 타입입니다. 배열이 가지는 중요한 특징은 배열의 사이즈가 고정되어 있다는 점입니다. 참으로 불편하지요? 그래서 우리는 slice
를 씁니다.
slice
는 사이즈가 동적인 배열이라고 보시면 됩니다. 그리고 동적인 배열 slice
에 요소를 추가할 때 append
함수를 씁니다. 엄밀하게 slice
는 배열의 포인터라고 볼 수 도 있을 겁니다. 자세한 내용은 공식 블로그의 친절한 설명으로 대체하겠습니다. ^^
누가 Register 하는가?
바로 직전까지 우리는 Start
메서드에서 for
문으로 Node
의 serviceFuncs
변수를 순회하는 부분을 봤습니다. serviceFuncs
는 Node
의 Register
메서드를 통해서 추가된다는 것도 확인했구요. 그럼 남은 질문 하나는 어디서 누가 Register
메서드를 호출하여 serviceFuncs
에 constructor
를 넣고 있을까요?
지난 글에서 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
메서드의 코드는 상대적으로 간단합니다. 이전에 살펴본 Node
의 Start
, 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
을 전달하고 있습니다. 운영체제로부터 SIGINT
나 SIGTERM
시그널을 받으면 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
의 코어인 이더리움 객체를 생성하고 실행하는 부분이 이어집니다.
Congratulations @woojin.joe! You have completed some achievement on Steemit and have been rewarded with new badge(s) :
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
pairplay 가 kr-dev를 응원합니다!
좋은 글 감사합니다.