코야 사탕

우리집에서 “코야 사탕”은 공갈 젖꼭지를 부르는 말이다.

공갈 젖꼭지를 물고 있으면 쉽게 잠에 들고 스트레스 상황에서 빠르게 안정을 찾기에 부모 입장에선 편리한 면도 있지만, 매일 씻고 소독해야하고 외출할 때마다 챙기는 노고도 무시할 수 없다. 무엇보다 아이가 너무 의지하게 될까봐 걱정이 되었다.

어느날 아내가 잠을 자다가 아이 입에서 떨어진 코야 사탕을 큰 맘 먹고 숨겼는데 사단이 났다. 엄마가 코야 사탕을 안 주니 아빠에게 코야 사탕 달라고 소리를 지르며 울었다. 버텨볼까 하다가 아이가 혹시 잘못될까봐 지레 겁을 먹고 아내와 상의 끝에 코야 사탕을 다시 입에 물려주고 말았다.

그 후로 오랜 시간이 흘러 아내는 복직하고 내가 집에서 아이를 돌보게 되었다.

코야 사탕을 물려 주면 잠에 드는 시간은 빠르지만, 자다가 입에서 떨어지면 옆에서 자던 아내가 찾아서 입에 물려 주어야 했다. 이대로는 아내가 너무 피곤할 것 같아서 약 20일 전에 코야 사탕을 끊어야겠다고 결심하고 작전(?)에 들어갔다.

코야 사탕에 레몬을 바르거나 눈 앞에서 가위로 잘라버리는 극단적인 방법 대신에 말로 설득해보기로 했다. 스스로 끊어야겠다는 생각이 들 수 있도록, 치사한 거짓말로 코야 사탕을 계속 물고 있으면 입이 튀어 나와서 못생겨진다고 했더니 수긍하는 눈치였다. 그리고 거짓말처럼 그날부터 코야 사탕없이 잘 지내고 있다.

덕분에 아이와 아내의 수면의 질은 좋아졌지만 나의 삶은 척박해졌다. 코야 사탕과 함께 아이의 낮잠이 실종되었기 때문이다. 밤에는 코야 사탕 없이도 잘 자는데, 낮에는 잠이 잘 안 오는 모양이다. 혹시 아빠와 노는 게 너무 재미있어서일까?

낮잠을 못자니 엄마가 퇴근할 때 쯤 뻗는 일이 반복되었고, 차로 엄마를 마중나가면 카시트에서 잠만 자다가 집으로 돌아오곤 했다. 낮잠을 못자 밤잠을 일찍 자니 아침에 일어나는 시간은 점점 당겨져서 오전 7시 근처가 되었다.

그렇게 아빠의 단독 육아는 아이가 깨어나는 오전 7시부터 아내가 귀가하는 오후 6시 반까지 중간 휴식시간(아이의 낮잠) 없이 바쁘게 돌아가고 있다. 오늘은 힘들게 낮잠을 재우는데 성공하여 이 글을 쓰는 호사를 누리고 있다. 지금까지 성공률은 10% 정도.

지금의 나는 조금 더 힘들어도 아내가 꿀잠을 잘 수 있어서, 아이가 코야 사탕에 의지하지 않게 되어서 기쁘다. 코로나19로 어린이집에 못가고 있는데, 어린이집에서 친구들과 같은 시간에 낮잠을 자는 경험을 하게 되면 점점 더 좋아지지 않을까 생각한다.

아이가 크면 코야 사탕을 기억하는지 물어보고 싶다. 만약 기억한다면 아빠의 착한(?) 거짓말에 대해서 미안했다고 말해주고 싶다. 다 너를 위한 거였다는 뻔한 멘트와 함께.

아기가 말을 배우는 과정

이번 주에는 22개월 딸의 말하기 능력이 부쩍 향상된 것을 느꼈다. 최근에는 아침에 만나는 아이가 조금은 낯설게 느껴질 정도로 하루하루가 다른데, 이번 주는 더 도드라졌다.

방울 토마토 하나를 나에게 주면서 하는 말

“아빠 먹어 한입에”

최근에 아이가 한 말들 중 기억나는 것 몇가지

“낸니랑 아빠랑 엄마랑 아파트에 살아”
(낸니는 스스로 지은 별명)

“아빠가 운전을 하고 있어”

아이를 키우면서 ‘사람은 어떻게 말과 글을 배울까?’ 궁금했는데, 옆에서 지켜본 과정은 매우 점진적이었다.

처음에는 주어만 말한다.

“아빠”, “엄마”, “낸니”

조사가 추가된다.

“아빠도”, “엄마랑”, “낸니가”

대명사를 사용한다.

“이거”, “여기”

동사를 사용한다.

“아빠 같이 가자”, “아빠 집에 들어가”

목적어, 복합 동사를 사용한다.

“아빠 저거 먹고 싶어”

책을 정성스럽게 읽어주는 것은 기본, 아이와 함께 있을 때 마주치는 모든 상황을 쉬운 말로 들려주려고 노력한다.

무엇보다 중요한 것은 말을 배우고자 하는 아이의 의지다. 새로운 말을 들었을 때 반복해서 말해보고, 혼자 놀때도 배웠던 표현들을 중얼거리면서 자기 것으로 만든다. 그리고 조금 틀려도 새로운 표현을 써보려고 노력한다. 그때마다 아이가 사용한 표현을 정확한 표현과 발음으로 천천히 반복하여 말해준다.

아이에게 한글을 알려주며 ‘영어를 이렇게 배울 수 있다면 얼마나 좋을까’ 자주 생각한다.

배움의 과정은 점진적이고, 충분한 노력이 쌓여야 퀀텀 점프를 이룰 수 있다는 것을 아이를 통해 배운다.

아이는 배우려는 의지를 지닌채 세상에 나오는 것 같다. 그것을 잃지 않도록 곁에서 도와주는 것이 부모가 아이에게 해줄 수 있는 큰 선물 중 하나가 아닐까 생각한다.

학문의 즐거움

코로나19 바이러스로 도서관에서 책을 빌릴 수 없게 되어, 책장에서 나에게 의미가 컸던 책들을 꺼내 다시 읽고 있다. 이 책을 읽은 것은 이번이 세 번째다. 평범한 두뇌를 가진 나에게 노력의 중요성을 알려준 책이어서 평생 다시 읽어도 좋을 것 같다. 두 번째로 읽은 시점이 석사를 졸업하고 취업을 한 해였는데, ‘2년만 일찍 읽었더라면 석사과정에서의 성취나 진로가 바뀔 수도 있지 않았을까’ 하는 아쉬운 마음을 세 번째로 읽으면서 가져보았다.

나를 제일 잘 아는 사람은 누군가? 나 자신이다. 솔직히 나 자신이 볼 때 내가 뛰어난 재주를 가졌다고는 생각되지 않는다. 그렇지만 나는 노력하는 데 있어서는 절대적으로 자신이 있다. 바꾸어 말해서 끝까지 해내는 끈기에 있어서는 결코 남에게 지지 않는다.

느긋하게 기다리고, 기회를 잡을 행운이 오면, 나머지는 끈기이다. 나는 남보다 두 배의 시간을 들이는 것을 신조로 하고 있다. 그리고 끝까지 해내는 끈기를 의식적으로 키워 왔다.

노력이란 말은 남보다 더 많은 시간을 들인다는 것과 같은 말이다.

이번에 읽으면서 가장 마음에 들어온 내용은 ‘배움’과 ‘지혜’에 대한 것이다. 시간이 지나면 배운 것을 잊어버림에도 불구하고 끊임없이 배워야 하는 이유는 지혜를 얻기 위해서다. 실제로는 잊어버린 것이 아니다. 바로 꺼내 쓰기 어려울 뿐… 한 번 배운 지식은 작은 노력으로도 언제든 다시 꺼내 쓸 수 있다. 더 중요한 것은 ‘배움’은 인간만이 할 수 있는 통합적 사고의 바탕을 쌓는다. 당장의 필요를 찾을 수 없는 주제라 하더라도 끈기를 가지고 목표한 수준의 공부를 해낸다면 언젠가는 도움이 될 것이다.

책을 읽는것도 마찬가지 아닐까? 책에서 배운 교훈을 하나라도 놓치기 싫어서, Notion에 옮겨적고 이렇게 블로그에 후기를 남기기도 하지만, 책을 읽으며 생각하는 행위 자체로도 충분히 의미가 있다.

이렇게 생각하니 공부와 독서에 대한 진입장벽이 낮아지는 느낌이다. 고민할 시간에 노력하자.

텅빈 충만

소설 <천년의 질문>에서 글을 잘 쓰고 싶다는 황검사에게 장우진 기자가 추천했던 책 세 권 중 하나. 조정래 선생님의 추천이나 다를 바 없어 읽게 되었다. 아름다운 글을 읽는 즐거움에 더하여, 세상을 살아가는데 필요한 지혜를 얻을 수 있었다.

자기 자신을 탐구하는 일이야말로 인생에서 가장 보람있는 일이 아니겠는가. 옛 스승들의 가르침은 자기 탐구를 위한 길잡이요 과정일 뿐이다. 밖에서 얻어들려고만 하면 지혜의 눈이 열릴 수 없다.

우리가 지금까지 얻어들은 좋은 말씀이 얼마나 많은가. 그 좋은 말이 모자라 현재의 삶이 허술하단 말인가. 남의 말에 갇히게 되면 자기 자신의 삶을 잃어버린다.

가장 큰 깨달음은 모든 것을 내 안에서 찾아야 한다는 것이다. 무언가 현재의 삶이 만족스럽지 않고 무료함을 느낄 때, 밖에서 답을 찾으려고 애써보지만 결과는 신통치 않은 경우가 많았다. 책이나 사람은 계기를 제공해줄 뿐, 삶을 변화시키는 동력은 내면에서 찾아야 한다.

빈방에 홀로 앉아 있으면 모든 것이 넉넉하고 충분하다. 텅 비어 있기 때문에 오히려 가득 찼을 때보다도 더 충만하다.

나무 그늘 아래 앉아 산마루를 바라보고 있으면, 내 속뜰에서는 맑은 수액이 흐르고 향기로운 꽃이 피어난다. 혼자서 묵묵히 숲을 내다보고 있을 때 내 자신도 한 그루 정정한 나무가 된다. 아무 생각 없이 빈 마음으로 자연을 대하고 있으면, 그저 넉넉하고 충만할 뿐 결코 무료하지 않다.

따뜻한 햇살이 들어오는 빈방에 홀로 앉아있는 자신을 상상해보는 것만으로도 충만한 기분을 느낄 수 있다. 내면에 귀를 기울이려면 주변을 비워야 한다. 일상에서 눈에 들어오는 물건들을 줄이는 물리적인 비움도 필요하겠지만, 넓게 관심을 가지고 있는 가벼운 주제들과 가벼움을 주고 받는 수 많은 주변 사람들과 같이 정신을 비만하게 만드는 요소들을 줄이는 것이 더 중요하다는 생각을 해보았다. 빈 틈 없이 꽉 차 있었지만 한없이 무료했던 지난 시간들이 떠오른다.

이 책에서 말하는 ‘충만함’을 나는 다른 말로 ‘행복’ 또는 ‘만족감’이라고 부르고 싶다. 누군들 행복한 삶을 원하지 않을까. 충만함으로 가득한 삶을 위해서 외부의 소음을 차단하고 자신을 마주하는 시간을 자주 가져야겠다.

홀로 있는 것은 온전한 내가 존재하는 것. 발가벗은 내가 내 식대로 살고 있는 순간들이다.
아무에게도, 잠시라도 기대려 하지 말 것.
부엌과 고방에 쌓인 너절한 것들 모조리 치워 없애다.
절대로 간소하게 살 것.
날마다 버릴 것.

Go언어로 구현한 Dijkstra 알고리즘

Go언어로 구현된 코드와 한글로 작성된 설명을 인터넷에서 찾기 힘들기에, Go언어를 공부하고 있는 누군가에게는 약간의 도움이 되길 바라며, Graph Search, Shortest Paths, and Data Structures 강의에서 프로그래밍 숙제로 구현했던 Dijkstra 알고리즘 코드를 공유해본다.

수준이 낮은 코드를 인터넷에 공유하는 것은 부끄러운 일이지만, 이정도 코드밖에 만들 수 없는 것이 현재 나의 실력이라는 것을 인정해야만 더 높은 수준으로 나아갈 수 있다고 믿기에, 앞으로도 기회가 닿는대로 Go언어로 작성된 코드와 시행착오를 공유해보고자 한다.

저장소: http://gogs.reshout.com/reshout/dijkstra-heap

소스트리 구조는 아래와 같다.

├── dijkstraData.txt
├── graph
│   ├── graph.go
│   └── graph_test.go
├── heap
│   ├── heap.go
│   └── heap_test.go
└── main.go

graph 패지와 heap 패키지는 알고리즘 구현에 필요한 자료구조를 별도의 패키지로 구현한 것인데, 유닛테스트 코드를 포함하며 100%에 가까운 커버리지를 확보하였다. 유닛테스트를 통해 확보한 자료구조의 완성도 덕분에 이를 활용한 알고리즘 코드를 완성한 후에는 한 번에 원하는 답을 얻을 수 있었다. Go언어는 소스코드와 같은 폴더에 유닛테스트 코드(e.g., graph_test.go)를 작성할 수 있어서 유닛테스트를 작성하는 습관을 들이는데 도움이 되는 것 같다.

$ go test -cover ./...
?       dijkstra-heap   [no test files]
ok      dijkstra-heap/graph     (cached)        coverage: 94.4% of statements
ok      dijkstra-heap/heap      (cached)        coverage: 97.3% of statements

main.go에 구현된 Dijkstra 알고리즘은 Dasgupta가 쓴 알고리즘 책 115쪽에 있는 pseudocode를 약간 변형한 것으로 다음과 같다.

func dijkstra(g *graph.Graph) {
	for _, u := range g.GetVertices() {
		u.Distance = maxInt
	}
	g.GetVertex(1).Distance = 0

	h := heap.NewHeap()
	for _, u := range g.GetVertices() {
		h.Push(u)
	}

	for !h.IsEmpty() {
		u := h.Pop().(*graph.Vertex)
		for _, edge := range u.OutgoingEdges {
			v := edge.Head
			if v.Distance > u.Distance+edge.Length {
				v.Distance = u.Distance + edge.Length
				h.Delete(v)
				h.Push(v)
			}
		}
	}
}

heap 패키지는 다른 프로젝트에서도 사용할 수 있도록 범용적인 인터페이스를 제공한다. 아래와 같이 Less 함수를 구현한 어떤 객체도 Push(obj Object), Pop() Object 할 수 있도록 하였다.

package heap

type Object interface {
	Less(obj Object) bool
}

graph 패키지는 heap 패키지에 의존적이다. Vertexheap에 넣어놓고 Distance 기준으로 최소값을 추출해야 하기 때문이다.

package graph

import "dijkstra-heap/heap"

type Vertex struct {
	Label         int
	Distance      int
	OutgoingEdges []*Edge
}

func (v *Vertex) Less(obj heap.Object) bool {
	return v.Distance < obj.(*Vertex).Distance
}

graph 패키지는 이 프로젝트 전용이다. 알고리즘에서 사용할 연산에 최적화된 실행시간을 제공하려면 내부 자료구조나 내부 알고리즘도 달라질 수 밖에 없다.

그래프를 구성하는 Vertex들과 Edge들을 저장하는 다양한 방법이 존재할 수 있다. Dijkstra 알고리즘에서는 Heap에서 꺼낸 Vertex에서 밖으로 연결된 Edge들을 탐색하는 경우가 많으므로 VertexOutgoingEdges를 갖도록 하였다.

graph.go의 전체 코드는 아래와 같다.

package graph

import "dijkstra-heap/heap"

type Vertex struct {
	Label         int
	Distance      int
	OutgoingEdges []*Edge
}

func (v *Vertex) Less(obj heap.Object) bool {
	return v.Distance < obj.(*Vertex).Distance
}

type Edge struct {
	Tail     *Vertex
	Head     *Vertex
	Length   int
	Explored bool
}

type Graph struct {
	vMap map[int]*Vertex
}

func NewGraph() *Graph {
	g := &Graph{}
	g.vMap = map[int]*Vertex{}
	return g
}

func (g *Graph) AddEdge(tailLabel int, headLabel int, len int) {
	tail := g.GetVertex(tailLabel)
	head := g.GetVertex(headLabel)
	edge := &Edge{tail, head, len, false}
	tail.OutgoingEdges = append(tail.OutgoingEdges, edge)
}

func (g *Graph) GetVertexCount() int {
	return len(g.vMap)
}

func (g *Graph) GetVertex(label int) *Vertex {
	key := label
	if _, exists := g.vMap[key]; !exists {
		g.vMap[key] = &Vertex{Label: label, OutgoingEdges: []*Edge{}}
	}
	return g.vMap[key]
}

func (g *Graph) GetVertices() []*Vertex {
	vertices := []*Vertex{}
	for label := range g.vMap {
		vertex := g.vMap[label]
		vertices = append(vertices, vertex)
	}
	return vertices
}

Dijkstra 알고리즘에서 사용하는 Heap은 중간 객체를 찾아서 삭제하거나 업데이트하는 인터페이스가 필요한데, Go언어에 내장된 container/heap 패키지에서 이를 제공하지 않아 직접 구현했다.

type Heap struct {
	objArr []Object
	objMap map[Object]int
}

내부 자료구조로 array를 사용해야만 Leaf 노드를 찾아 치환하거나 삭제하는 연산이 O(1)에 가능하다.

중간 객체를 삭제하기 위해선 먼저 트리 안에서 해당 객체를 찾아야 하는데, Binary Search Tree가 아니므로 최악의 경우 모든 노드를 다 찾아보아야 해서 실행시간은 최악의 경우 O(n)이 된다. 이렇게 되면 Heap을 사용해 실행시간을 O(m * n)에서 O(m * log n)으로 향상시키려는 시도가 무의미해진다.

객체의 위치를 O(1)에 찾을 수 있도록 Hash Table을 도입하여 객체가 추가되거나 삭제되거나 위치가 바뀔때마다 objMap을 업데이트하도록 하여, Delete(obj Object) bool의 실행시간을 O(log n)에 맞췄다.

heap.go의 전체 코드는 아래와 같다. 더 간결하게 짤 수도 있을텐데, 지금은 이정도가 내 수준인 것 같다. 좋은 코드를 많이 볼 필요가 있을 것 같다.

package heap

type Object interface {
	Less(obj Object) bool
}

type Heap struct {
	objArr []Object
	objMap map[Object]int
}

func NewHeap() *Heap {
	return &Heap{objArr: []Object{}, objMap: map[Object]int{}}
}

func (h *Heap) Push(obj Object) {
	idx := h.insertAsLeaf(obj)
	h.bubbleUp(idx)
}

func (h *Heap) Pop() Object {
	rootIdx := 0
	root := h.objArr[rootIdx]
	leafIdx := h.getLeafIndex()

	h.swap(rootIdx, leafIdx)
	h.deleteLeaf()
	if len(h.objArr) > 0 {
		h.bubbleDown(0)
	}

	return root
}

func (h *Heap) Delete(obj Object) bool {
	if len(h.objArr) <= 0 {
		return false
	}

	objectIdx := h.getObjectIndex(obj)
	if objectIdx == -1 {
		return false
	}
	leafIdx := h.getLeafIndex()

	h.swap(objectIdx, leafIdx)
	h.deleteLeaf()
	if objectIdx != leafIdx {
		h.bubbleDown(objectIdx)
	}

	return true
}

func (h *Heap) IsEmpty() bool {
	return len(h.objArr) == 0
}

func (h *Heap) getParentIndex(idx int) int {
	if idx == 0 {
		return -1
	}
	return (idx - 1) / 2
}

func (h *Heap) getLeftChildIndex(idx int) int {
	leftChildIdx := 2*(idx+1) - 1
	if len(h.objArr) <= leftChildIdx {
		return -1
	}
	return leftChildIdx
}

func (h *Heap) getRightChildIndex(idx int) int {
	rightChildIndex := 2*(idx+1) + 1 - 1
	if len(h.objArr) <= rightChildIndex {
		return -1
	}
	return rightChildIndex
}

func (h *Heap) getLeafIndex() int {
	return len(h.objArr) - 1
}

func (h *Heap) getObjectIndex(obj Object) int {
	idx, exists := h.objMap[obj]
	if !exists {
		return -1
	}
	return idx
}

func (h *Heap) insertAsLeaf(obj Object) int {
	h.objArr = append(h.objArr, obj)
	idx := len(h.objArr) - 1
	h.objMap[obj] = idx
	return idx
}

func (h *Heap) deleteLeaf() {
	leafIdx := h.getLeafIndex()
	leafObj := h.objArr[leafIdx]
	delete(h.objMap, leafObj)
	h.objArr = h.objArr[:leafIdx]
}

func (h *Heap) swap(idx1 int, idx2 int) {
	h.objArr[idx1], h.objArr[idx2] = h.objArr[idx2], h.objArr[idx1]
	h.objMap[h.objArr[idx1]] = idx1
	h.objMap[h.objArr[idx2]] = idx2
}

func (h *Heap) bubbleUp(idx int) {
	pIdx := h.getParentIndex(idx)
	if pIdx == -1 {
		return
	}
	pObj := h.objArr[pIdx]
	obj := h.objArr[idx]
	if obj.Less(pObj) {
		h.swap(pIdx, idx)
		h.bubbleUp(pIdx)
	}
}

func (h *Heap) bubbleDown(idx int) {
	node := h.objArr[idx]
	leftIdx := h.getLeftChildIndex(idx)
	leftLess := (leftIdx != -1 && h.objArr[leftIdx].Less(node))
	rightIdx := h.getRightChildIndex(idx)
	rightLess := (rightIdx != -1 && h.objArr[rightIdx].Less(node))

	if leftLess && rightLess {
		if h.objArr[leftIdx].Less(h.objArr[rightIdx]) {
			h.swap(idx, leftIdx)
			h.bubbleDown(leftIdx)
		} else {
			h.swap(idx, rightIdx)
			h.bubbleDown(rightIdx)
		}
	} else if leftLess {
		h.swap(idx, leftIdx)
		h.bubbleDown(leftIdx)
	} else if rightLess {
		h.swap(idx, rightIdx)
		h.bubbleDown(rightIdx)
	}
}

유닛테스트는 집요할 수록 좋다. 아래는 heap_test.go의 전체 코드다.

package heap

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

type testObject struct {
	key int
}

func (t testObject) Less(obj Object) bool {
	return t.key < obj.(testObject).key
}

func TestHeap(t *testing.T) {
	h := NewHeap()
	assert.True(t, h.IsEmpty())
	h.Push(testObject{11})
	h.Push(testObject{12})
	h.Push(testObject{13})
	h.Push(testObject{6})
	h.Push(testObject{8})
	h.Push(testObject{20})
	h.Push(testObject{15})
	h.Push(testObject{10})
	h.Push(testObject{5})
	h.Push(testObject{3})
	assert.False(t, h.IsEmpty())
	assert.False(t, h.Delete(testObject{100}))
	assert.Equal(t, 3, h.Pop().(testObject).key)
	assert.True(t, h.Delete(testObject{10}))
	assert.True(t, h.Delete(testObject{6}))
	assert.Equal(t, 5, h.Pop().(testObject).key)
	assert.Equal(t, 8, h.Pop().(testObject).key)
	assert.Equal(t, 11, h.Pop().(testObject).key)
	assert.Equal(t, 12, h.Pop().(testObject).key)
	assert.Equal(t, 13, h.Pop().(testObject).key)
	assert.Equal(t, 15, h.Pop().(testObject).key)
	assert.Equal(t, 20, h.Pop().(testObject).key)
	assert.False(t, h.Delete(testObject{20}))
	assert.True(t, h.IsEmpty())
}

소프트웨어를 설계하거나 코드를 작성할 때면, optimal solution을 가진 신이 등장해서 피드백을 주면 좋겠다는 생각을 하게 된다. 신은 아니더라도 나보다 나은 누군가가 이 코드를 보고 문제점, 개선점을 알려 주었으면 하는 바램이다.