## 1. Project Overview & Quickstart (mingrammer/LICENSE)

# LICENSE

Open-source repository mingrammer/LICENSE

### Repository Details
- **Repository:** [mingrammer/LICENSE](https://github.com/mingrammer/LICENSE)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (mingrammer/mingrammer.github.io)

## File: README.md

# mingrammer.github.io

https://mingrammer.com

## Setup

```bash
git clone -b hugo git@github.com:mingrammer/mingrammer.github.io

# Add submodule for master branch (for publish)
git submodule add -b master git@github.com:mingrammer/mingrammer.github.io.git public
```

## Deployment

```bash
./deploy "<commit-message>"
```

---

## File: archetypes/default.md

+++
title = "{{ replace .TranslationBaseName "-" " " | title }}"
date = {{ .Date }}
tags = [""]
categories = [""]
draft = true
+++

---

## File: content/about/_index.md

+++
date = "2020-01-02T00:00:00"
title = "About"
hidden = true
+++

# MinJae Kwon

## mingrammer

---

### Profile

###### Contact

[mingrammer@gmail.com](mingrammer@gmail.com)

###### Websites

[Homepage](https://mingrammer.com), [GitHub](https://github.com/mingrammer), [Medium](https://medium.com/@mingrammer)

###### Work

Server Engineer (3y) @ [Daangn Market](https://www.daangn.com/)

---

### Skills

###### Programming Languages

Go, Python

###### Backend Development

Linux, AWS, Docker, MySQL, PostgreSQL, Redis, Memcached, Kafka, gRPC

---

### Interests

- Designing system architectures
- Build something for developers
- Automating the boring tasks
- Open source software

---

### Open Source Experience

###### [diagrams](https://github.com/mingrammer/diagrams)
##### Diagram as Code for prototyping cloud system architectures

Diagrams lets you draw the cloud system architecture in Python code. It was born for prototyping a new system architecture design without any design tools. You can also describe or visualize the existing system architecture as well.

###### [flog](https://github.com/mingrammer/flog)
##### A fake log generator for common log formats

Flog provides a log generator for 6 often used log formats (for now). It was created for testing some tasks which require log data like amazon kinesis log stream test. Its docker image was pulled 212k times from docker hub by other developers.

###### [pyreportcard](https://github.com/mingrammer/pyreportcard)
##### A report card web service for Python applications

Pyreportcard analyzes the source code quality of the Python projects which are hosted on GitHub, checks for license and readme files, and provides some statistics.

###### [casec](https://github.com/mingrammer/casec)
##### Text case converter for programmers

Casec is a text case converter for programmers. casec now supports upper, lower, title, camel, pascal, snake, kebab (or lisp) cases, providing also case conversion library.


##### Contributions

[go#24074](https://github.com/golang/go/pull/24074), [go-redis#952](https://github.com/go-redis/redis/pull/952), [airflow#5749](https://github.com/apache/airflow/pull/6418), [airflow#5758](https://github.com/apache/airflow/pull/6432), [airflow#5769](https://github.com/apache/airflow/pull/6443), [dd-trace-go#647](https://github.com/DataDog/dd-trace-go/pull/647), [cli/cli#532](https://github.com/cli/cli/pull/532), [mvdan/sh#388](https://github.com/mvdan/sh/pull/388), [tqdm#615 (wip)](https://github.com/tqdm/tqdm/pull/615) and etc.

---

### Translations

- Korean translator of [Head First Go](https://www.hanbit.co.kr/store/books/look.php?p_code=B1938330682), 2019 

---

### Presentation

- [Redis data migration from IDC to AWS](https://speakerdeck.com/mingrammer/idceseo-awsro-redis-deiteo-ijeonhagi) at AWSKRUG Architecture Fab 2019
- [Build reproducible infrastructure on legacy](https://speakerdeck.com/mingrammer/regeosi-wieseo-jaehyeon-ganeunghan-hwangyeong-gucughagi) at HashiCorp Korea User Group 2018
- [Serverless on-demand image resizing server](https://speakerdeck.com/mingrammer/building-a-serverless-application-with-aws) at AWSKRUG Architecture Aug 2017
- [On-demand image server with Python](https://speakerdeck.com/mingrammer/on-demand-image-server-with-python) at Python Seminar Seoul May 2017

---

## File: content/post/2016-09-04-welcome-mingrammer.md

---
comments: true
date: 2016-09-04T00:00:00Z
title: Welcome!
url: /welcome-mingrammer
---

mingrammer의 기술 블로그 (<del>삽질 이야기</del>) 를 시작합니다. :)

---

## File: content/post/2016-09-13-dev-commit-alarm-bot.md

[Binary file]

---

## File: content/post/2016-10-03-translation-asynchronous-python.md

---
categories:
- translation
- python
comments: true
date: 2016-10-03T00:00:00Z
tags:
- python
- asynchronous
title: '[번역] 비동기 파이썬'
url: /translation-asynchronous-python
---

> [Asynchronous Python](https://hackernoon.com/asynchronous-python-45df84b82434#.irtdum9dx)을 번역한 글입니다. 

파이썬에서의 비동기 프로그래밍은 최근 점점 더 많은 인기를 끌고있다. 비동기 프로그래밍을 위한 파이썬 라이브러리는 많다. 그 중 하나는 Python 3.4에서 추가된 *asyncio* 라는 라이브러리이다. *Asyncio* 는 파이썬에서 가장 인기 있는 라이브러리중 하나이다. 이번 글에서는 비동기 프로그래밍이 무엇인지 설명하면서 라이브러리들을 비교할 것이다. 먼저 역사를 살펴보고 파이썬에서 비동기 프로그래밍이 어떻게 발전했는지 알아보자.



# 차근 차근 살펴보기

프로그램은 각 라인 별로 순서대로 실행되는 특성이 있다. 예를 들어, 리소스를 가져오기 위해 원격 서버로 접속하는 코드 라인을 가지고 있다는건 서버 연결을 대기하는동안 프로그램이 아무것도 하지 못함을 의미한다. 진행하기 위해서는 응답을 기다려야한다. 이는 몇 몇 상황에서는 괜찮지만, 많은 경우엔 그렇지 않다. 물론 이런 상황에서의 표준 해결책은 스레딩(threading)이다. 프로그램은 여러 스레드를 돌릴 수 있으며, 각 스레드는 동시에 실행된다.

스레드 여러개를 동시에 사용하면 프로그램은 동시에 여러가지 일을 할 수 있다. 물론 스레딩은 많은 주의사항을 가지고있다. 다중 스레드 프로그램은 복잡하며, 경쟁 조건(race conditions). 데드락(dead-locks), 라이브 잠금(live-locks) 그리고 기아 상태(resource-starvation) 등의 까다로운 문제들을 포함한 많은 에러가 발생하기 쉽다.



## 컨텍스트 스위칭

비동기 프로그래밍은 이러한 모든 문제들을 방지할 순 있지만, 이는 사실 CPU 컨텍스트 스위칭이라는 전혀 다른 문제를 위해 설계되었다. 다중 스레드가 실행중일 때, 각 CPU 코어는 한 번에 하나의 스레드만 돌릴 수 있다. 모든 스레드와 프로세스가 자원을 공유하기 위해 CPU 컨텍스트 전환이 자주 일어난다. 일을 매우 단순화하기 위해, CPU는 임의의 간격으로 스레드의 모든 컨텍스트 정보를 저장하고 다른 스레드로 전환된다. CPU는 정해지지 않은 간격마다 지속적으로 스레드간 전환을 한다. 스레드들 또한 자원이며, 이는 공짜가 아니다.

비동기 프로그래밍은 기본적으로 CPU보다 스레드와 컨텍스트 스위칭을 관리하는 애플리케이션이 존재하는 소프트웨어/유저스페이스의 스레딩이다. 기본적으로, 비동기 세상에서 컨텍스트는 임의의 간격이 아닌 오직 정의된 전환점에서만 전환이된다.



## 믿을 수 없을만큼 효율적인 비서

이제 이 개념들을 현실 세계에서의 예시와 비교해보자. 믿을 수 없을 정도로 효율적이고, 한 시라도 시간을 낭비하지 않는 - 항상 모든 일을 잘 마치며, 매사에 최선을 다하는 비서가 있다고 상상해보자. 이 비서는 - 'Bob'이라고 부르자 - 이를 달성하기위해 미친듯이 멀티태스킹을 할 것이다. Bob은 한 번에 해야하는 5개의 일을 가지고 있다 - 전화 받기, 손님들 접수 받기, 항공편 예약하기, 미팅 스케쥴 조정하기 그리고 서류 제출하기. 이제 이 상황이 낮은 트래픽의 환경에 있다고 상상해보자. 따라서 전화 받기, 방문객, 그리고 미팅 요청들이 적으며 각 업무들은 서로 떨어져있다. Bob의 대부분의 시간은 서류를 제출하는동안 항공사와 전화를 하는데 소비될 것이다. 이 모든건 꽤 일반적이며 상상하기 쉽다. 전화가 오면, Bob은 잠시 항공사와의 전화를 보류하고, 전화에 응답을 한 후, 다시 항공사와의 전화로 돌아갈 것이다. 언제 어떤 업무가 Bob에게 도착하면, 서류 제출은 뒤로 미뤄질 수 있다. 왜냐하면 이는 지금 당장 해결해야할 필요가 없기 때문이다. 이것이 사람이 동시에 여러가지 일을 하는 방식이며, 적절한 때에 컨텍스트 스위칭이 일어난다. Bob은 비동기적이다.

이것의 스레딩 버전은 5명의 Bob이 각각 하나의 업무만 가진 것처럼 보일 것이다. 하지만 어느 주어진 시간에는  단 한 가지 일을 하는것만 허용된다. 여기엔  Bob이 일을 할 수 있게 제어하는 장치가 있을텐데, 이는 작업들에 대해 아무것도 이해하지 못한다. 이  장치는 업무들이 발생하게된 사건(이벤트)을 이해하지 못하기 때문에, 만일 3개의 작업이 아무것도 하고있지 않더라도 5개의 작업들 사이를 지속적으로 전환할 것이다. 예를 들어, 서류 제출 담당의 Bob에 인터럽트가 생기면 전화 받기 담당의 Bob이 무언가를 할 수 있게된다. 하지만 전화 받기 담당의 Bob은 아무 할 일도 없으며, 따라서 그는 잠자기 상태로 돌아간다. 아무것도 하지 않는 3개의 업무들마저 찾아내기 위해 모든 업무들간의 전환을 함으로써 시간 낭비가 발생한다. 컨텍스트 스위칭의 약 57% (3/5보다 약간 낮음)가 무의미한 작업이 될 것이다. 물론, CPU 컨텍스트 스위칭이 매우 빠르기는하지만 결코 공짜는 아니다.



## 그린 스레드 (Green Threads)

그린 스레드 (Green Threads)는 비동기 프로그래밍의 가장 기초이다. 그린 스레드는 스케쥴링을 하드웨어가 아닌 애플리케이션 코드가 대신 한다는 점을 제외하면, 일반 스레드와 정확히 같아보인다. *[Gevent](http://www.gevent.org/)* 는 그린 스레드를 사용하는 유명한 파이썬 라이브러리이다. *Gevent* 는 기본적으로 그린 스레드 + eventlet (non-blocking I/O 네트워킹 라이브러리)이다. 다음은 *gevent* 를 사용하여 한 번에 여러개의 url에 요청을 날리는 예시이다.

```python
import gevent
from gevent import monkey

monkey.patch_all()
urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

def print_head(url):
    print('Starting {}'.format(url))
    data = urlopen(url).read()
    print('{}: {} bytes: {}'.format(url, len(data), data))

jobs = [gevent.spawn(print_head, _url) for _url in urls]

gevent.wait(jobs)
```

위에서 볼 수 있듯이, gevent API는 스레딩과 유사해보인다. 그러나, 구현을 보면 이는 실제 스레드 대신 코루틴을 사용하며, 스케쥴링을 위해 코루틴들을 [이벤트 루프](http://www.gevent.org/intro.html#event-loop)위에서 실행한다. 이는 코루틴에 대한 이해 없이도 경량 스레딩의 이점을 얻을 수 있음을 의미하지만, 아직도 스레딩이 가져오는 다른 문제점들을 가지고있다. Gevent는 스레딩을 이미 이해하고 있고, 경량 스레딩을 원하는 사람들에겐 좋은 라이브러리이다.



# 이벤트 루프? 코루틴? 천천히 살펴봅시다

비동기 프로그래밍이 어떻게 동작하는지에 대해 정리해보자. 비동기 프로그래밍을 하기위한 한 가지 방법은 이벤트 루프를 사용하는 것이다. 이벤트 루프란, 이벤트/잡(events/jobs)을 관리하는 큐가 있을 때, 단지 큐에서 지속적으로 잡을 빼내고 이들을 실행해주는 루프와 정확히 같은 말이다. 이 잡들을 코루틴이라고 부른다. 이들은 큐에 넣을 수 있는 그 어떤 이벤트들을 포함하는 명령어들의 작은 집합이다.



## 콜백 스타일의 비동기

파이썬엔 많은 비동기 라이브러리들이 있지만, 그 중 가장 유명한 것들은 아마 *Tornado* 와 *gevent* 일 것이다. 우리는 이미 *gevent* 에 대한 얘기를 했으므로, Tornado가 어떻게 동작하는지에 대해 조금 더 초점을 맞춰보자. Tornado는 비동기 네트워크 I/O를 위해 콜백 스타일을 사용하는 비동기 웹 프레임워크이다. 콜백은 함수이며, 이는 "이 작업이 완료되면, 이 함수를 실행시켜줘"라는 의미이다. 이는 기본적으로 코드에 대한 "완료" 훅(hook)이다. 다시 말하면, 콜백은 당신이 고객 서비스 라인을 호출하는 즉시, 전화 번호를 남긴 후 전화를 끊으면, 전화가 올 때까지 무한정 대기하는게 아닌 그들이 전화를 사용할 수 있을 때 당신에게 다시 전화를 걸 수 있는 상황과 같은것이다.

tornado를 이용하여 위에서 말한 것들을 어떻게 할 수 있는지 살펴보자.

```python

from tornado.httpclient import AsyncHTTPClient
urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

def handle_response(response):
    if response.error:
        print("Error:", response.error)
    else:
        url = response.request.url
        data = response.body
        print('{}: {} bytes: {}'.format(url, len(data), data))
    
http_client = AsyncHTTPClient()
for url in urls:
    http_client.fetch(url, handle_response)
```

코드를 조금 설명하면, 제일 마지막 라인은 non-blocking 방식으로 url를 가져오는 *AsyncHTTPClient.fetch* 라는 tornado 메서드를 호출하고있다. 이 메서드는 기본적으로 네트워크 호출을 기다리는 동안 프로그램이 다른 일을 수행할 수 있도록 실행하고 즉시 리턴이 된다. url을 가져오기전에 그 다음 라인에 도착하기 때문에, 메서드로부터 리턴 객체를 얻을 수가 없다. 이 문제에 대한 해결책은 *fetch* 메서드에서 객체를 리턴하는 대신, 결과값을 받는 함수 또는 콜백을 호출하는 것이다. 이 예제에서 콜백은 *handle_response* 이다.



## 콜백 지옥

이전 예시에서, 콜백함수의 맨 처음 라인에서 에러를 확인하고 있음을 볼 수 있다. 이는 예외를 발생시킬 수 없기 때문에 꼭 필요한 부분이다. 만약 예외가 발생한다면, 이벤트 루프로 인해 코드의 적절한 부분들이 제대로 처리되지 않을 것이다. *fetch* 가 실행될 때, 이는 http 호출을 시작한 후, 결과를 처리하는 함수를 이벤트 루프에 보낸다. 에러를 발견하는 순간, 호출 스택은 오직 예외를 처리하는 코드가 없는 이벤트 루프와 해당 함수뿐일 것이다. 콜백에서 어떠한 예외라도 발생한다면 이는 이벤트 루프와 프로그램을 빠져나갈 것이다. 따라서 모든 에러들은 예외를 일으키는 대신 객체로서 전달 되어야한다. 이는 만약 당신이 에러 확인하는 걸 깜빡했을 경우, 에러는 묻혀갈 것임을 의미한다. golang에 익숙한 사람들은 이런 스타일을 인지하고 있는데 언어 자체가 이러한 스타일을 모든 부분에서 강요하고 있다. 이는 golang의 측면에서 가장 불만인 부분이다.

비동기 세계에서 콜백의 다른 문제점은, 블로킹을 없애는 방법이 유일하게 콜백뿐이라는 것이다. 이는 콜백의 콜백의 콜백같은 매우 긴 콜백 체인을 만들 수도 있다. 당신은 스택과 변수에 접근할 수 없기 때문에, 결국  모든 콜백에 큰 객체들을 밀어 넣는다. 그러나 만약 서드파티 API를 사용한다면, 당신은 예기치 못하게 콜백에 아무것도 전달할 수가 없다. 이 또한 문제가 되는데, 왜냐하면 모든 콜백이 마치 스레드처럼 동작하기 때문이다. 그러나 태스크를 수집할 방법은 없다. 예를 들어 말하자면, 당신이 세 개의 API를 호출하고자 하면, 그 세 개의 작업이 완료될 때까지 기다린 후 집계 결과를 반환한다. *gevent* 세계에서 당신은 이 작업을 수행할 수 있지만, 콜백을 사용할 순 없다. 이를 구현하기 위해선 결과값들을 글로벌 전역 변수에 저장을 한 뒤, 콜백에서 이 값들이 마지막 결과값인지 아닌지를 확인해야할 것이다. 



# 비교

지금까지의 내용들을 비교 해보자. 만약 I/O를 블로킹으로부터 방지하고 싶다면, 스레드 혹은 비동기를 사용하면 된다. 스레드는 기아 상태, 데드락 그리고 경쟁 조건등의 문제점들을 가지고 있다. 이는 또한 CPU에 대한 컨텍스트 스위칭 오버헤드를 만들기도 한다. 비동기 프로그래밍은 이러한 컨텍스트 스위칭 문제를 해결할 순 있지만, 자체적인 문제점들을 가지고 있다. 파이썬에서 비동기 프로그래밍을 위한 선택사항은 *그린 스레드* 와 *콜백* 스타일이 있다. 



## 그린 스레드 스타일

* 스레드들이 하드웨어 대신 애플리케이션 레벨에서 관리됨
* 일반 스레드와 유사함 : 스레드를 이해하고 있는 사람들이 사용하기 좋음
* CPU 컨텍스트 스위칭 문제를 제외한 일반 스레드 프로그래밍이 가진  모든 문제점들을 가지고 있음 



## 콜백 스타일

* 스레드 프로그램과 많은 부분이 다름
* 프로그래머는 스레드와 코루틴을 직접 볼 수 없음
* 콜백은 예외를 발생시키지 않음
* 콜백은 수집할 수 없음
* 콜백의 콜백은 복잡하며 디버깅이 어려움



# 어떻게 개선할 수 있을까?

python 3.3까지는 이것이 가장 최선의 방법이었다. 더 나은 방법을 위해선 언어 자체의 지원이 필요하다. 더 나은 비동기 프로그래밍을 위해선, 파이썬은 메서드를 부분적으로 실행시키고, 실행을 중단시키고, 그리고 스택 객체와 예외를 전역적으로 관리할 수 있는 방법이 필요할 것이다. 만약  파이썬이 익숙하다면, 제너레이터가 힌트가 될 수 있다는걸 깨달을 수 있을 것이다. 제너레이터는 함수가  리스트를 리턴하는데 한 번에 하나의 아이템만 리턴할 수 있으며, 다음 아이템이 필요할 때까지 실행이 중지된다. 제너레이터의 문제점은 함수가 이를 호출해야만 수행될 수 있다는 것이다. 즉, 제너레이터는 제너레이터를 호출할 수 없고, 서로의 실행을 중지시킬 수도 없다. 그러나 이는 [PEP 380](https://docs.python.org/3/whatsnew/3.3.html#pep-380)이 제너레이터가 다른 제너레이터의 결과값을 yield할 수 있게 해주는 *yield from* 문법을 추가할 때 까지만이다. 비동기가 정말 제너레이터의 의도는 아니지만, 이는 비동기가 잘 돌아가는데에 필요한 모든 기능을 제공한다. 제너레이터는 스택을 유지하며 예외를 발생시킬 수 있다. 만약 당신이 제너레이터를 실행하는 이벤트 루프를 작성한다면, 훌륭한 비동기 라이브러리를 가질 수 있다. 그리고 따라서, *asyncio* 라이브러리가 탄생했다. 당신이 해야 할 모든 것들은 *@coroutine* 데코레이터를 추가하는 것이며 *asyncio*는 제너레이터를 코루틴 안으로 패치할 것이다. 여기에 우리가 전에 봤듯이 세 개의 url을 호출하는 예시가 있다.

```python
import asyncio
import aiohttp

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

@asyncio.coroutine
def call_url(url):
    print('Starting {}'.format(url))
    response = yield from aiohttp.get(url)
    data = yield from response.text()
    print('{}: {} bytes: {}'.format(url, len(data), data))
    return data

futures = [call_url(url) for url in urls]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))
```

주목할 부분들이 있다.

1. 에러를 볼 수가 없다. 왜냐하면 에러는 스택으로 제대로 전달되기 때문이다
2. 원할 때 객체를 리턴할 수 있음
3. 모든 코루틴을 시작하고 나중에 이들을 수집할 수 있음
4. 콜백이 없다
5. 10번째 라인은 9번째 라인이 완전히 끝나기전까지 실행되지 않는다 (동기(synchronous)와 유사하다)

훌륭하다! 유일한 문제는 *yield from* 이 제너레이터와 매우 유사해보인다는 것이고, 만약 이것이 실제로 제너레이터라면 문제가 발생할 수도 있다.



## 비동기(Async)와 대기(Await)

*asyncio* 라이브러리는 많은 지지를 얻었고, 파이썬은 이를 코어 라이브러리로 만들기로 결정하였다. 코어 라이브러리의 도입과 함께, Python 3.5에는 `async`와 `await` 키워드 또한 추가되었다. 이 키워드들은 코드가 비동기임을 더욱 명확하게 알 수 있도록 디자인 되었다. 따라서 당신의 메서드가 제너레이터로 혼동되는 일이 없다. *async* 키워드는 메서드가 비동기임을 알 수 있도록 *def* 앞에 위치한다. *await* 키워드는 *yield from* 를 대신하며 코루틴이 끝날때까지 대기하고 있음을 좀 더 명확하게 알 수 있다. 여기에 async/await 키워드를 사용한 위와 똑같은 예시가 있다.

```python
import asyncio
import aiohttp

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

async def call_url(url):
    print('Starting {}'.format(url))
    response = await aiohttp.get(url)
    data = await response.text()
    print('{}: {} bytes: {}'.format(url, len(data), data))
    return data

futures = [call_url(url) for url in urls]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))
```

기본적으로 여기서 일어나고 있는것은 async 메서드가 실행될 때, 대기 가능한 코루틴을 리턴하는 것이다.



# 마무리

파이썬은 *asyncio* 라는 훌륭한 비동기 프레임워크를 가지고 있다. 이제 스레딩의 문제점들을 짚어보고 이들을 어떻게 해결했는지를 보자.

* **CPU 컨텍스트 스위칭** : *asyncio* 는 비동기이며 이벤트 루프를 사용한다. 이는 I/O를 대기하는 동안 애플리케이션이 컨텍스트 스위치를 관리할 수 있도록 한다. CPU 스위칭이 없다!
* **경쟁 조건 (Race Conditions)** : *asyncio* 는 한 번에 오직 하나의 코루틴만 실행하며 정의된 지점에서만 스위칭이 일어나기 때문에, 코드는 경쟁 조건으로부터 안전하다.
* **데드락/라이브 잠금 (Dead-Locks/Live-Locks)** : 경쟁 조건에 대해 걱정할 필요가 없기 때문에, 잠금을 사용할 필요가 없다. 이는 데드락으로부터 매우 안전하게 만들어준다. 만약 두 개의 코루틴이 서로를 깨워야(wake) 할 필요가 있을 경우엔 여전히 데드락이 발생할 가능성이 있지만, 이런 일을 해야할 경우는 매우 드물 것이다. 
* **기아 상태 (Resource Starvation)** : 모든 코루틴이 하나의 스레드에서 실행되고, 추가적인 소켓이나 메모리를 필요로하지 않기때문에, 되려 리소스가 부족하기가 힘들 것이다. 그러나 *Asyncio* 는 기본적인 스레드 풀인 "executor pool"을 하나 가지고 있다. 만약 매우 많은 일들을 하나의 "executor pool"에서 실행한다면, 여전히 리소스 부족에 대한 문제가 발생할 수 있다. 하지만, 매우 많은 실행 프로그램을 사용하는것은 안티 패턴이며, 아마 이런 일을 자주 하지는 않을 것이다.

공평하게도, *asyncio* 는 매우 훌륭하지만, 자체적인 문제점들을 가지고 있다. 먼저, *asyncio* 는 파이썬의 새로운 개념이다. 몇 몇의 이상한 엣지 케이스들은 당신을 더욱 궁금한 상태로 만들 수도 있다. 두번째로, 완전한 비동기를 구현하려고하면 모든 코드베이스가 비동기여야 한다. 모든 작은 코드 조각까지 하나하나 다. 이것은 동기(synchronous) 함수가 너무 많은 시간이 걸려 이벤트 루프를 블로킹할 수도 있기 때문이다. asyncio를 위한 라이브러리는 여전히 초기 단계이기 때문에, 종종 당신의 스택의 일부분을 위한 비동기 버전을 찾기가 어렵다.



# 모든건 준비되어 있다

비동기 파이썬의 여정이 끝났다. 파이썬에는 비동기 프로그래밍을 하기 위한 몇가지 옵션이 있다. 당신은 그린 스레드, 콜백 또는 진정한 코루틴을 사용할 수 있다. 옵션들이 많지만, 그 중 단연 최고는 asyncio이다. 만약 Python 3.5를 사용할 수 있다면, 파이썬 코어에 내장된걸 사용하길 바란다. 다음 프로젝트에서는 스레딩 대신 asyncio를 사용해보라.

---

## File: content/post/2016-10-10-translation-structuring-applications-in-go.md

[Binary file]

---

## File: content/post/2016-10-26-review-deview-cookierun-ai.md

[Binary file]

---

## File: content/post/2016-10-26-translation-go-walkthrough-io-package.md

[Binary file]

---

## File: content/post/2016-11-02-translation-go-walkthrough-bytes-strings-packages.md

---
categories:
- translation
- go
comments: true
date: 2016-11-02T00:00:00Z
tags:
- go walkthrough
- bytes
- strings
title: '[번역] Go 둘러보기 - bytes + strings 패키지'
url: /translation-go-walkthrough-bytes-strings-packages
---

> [Go Walkthrough](https://medium.com/go-walkthrough) 시리즈의 [Go Walkthrough: bytes + strings packages](https://medium.com/go-walkthrough/go-walkthrough-bytes-strings-packages-499be9f4b5bd#.4jbor0c85)를 번역한 글입니다.

우린 [지난번 포스트](https://medium.com/go-walkthrough/go-walkthrough-io-package-8ac5e95a9fbd#.vjz46d3ss)에서 바이트 스트림을 다뤄봤는데 가끔은 제한적인 범위에서, 인메모리 바이트 슬라이스를 가지고 작업해야 할 수도 있다. 바이트의 리스트로 작업 하는 것은 충분히 간단해 보일 수 있지만, [bytes](https://golang.org/pkg/bytes/) 패키지를 유용하게 활용할 수 있는 많은 엣지 케이스와 일반적인 연산들이 있다. 우리는 또한 [strings](https://golang.org/pkg/strings/) 패키지는 문자열 작업을 위한것임에도 불구하고 이 API와 거의 동일하기 때문에 이 포스트에서는 이 [strings]([strings](https://golang.org/pkg/strings/)) 패키지에 대해 파헤쳐 볼 것이다.

이 포스트는 표준 라이브러리를  이해하는데 도움을 주기위한 Go 둘러보기 시리즈의 일부이다. 기존에 생성된 문서(자동으로 생성된 Go 문서)는 많은 정보를 제공하지만, 이는 패키지를 실제 상황에서 이해하기에는 어려울 수 있다.
이 시리즈는 일상적으로 사용되는 애플리케이션에서 표준 패키지들이 어떻게 사용되는지에 대한 컨텍스트를 제공할 수 있도록 도와준다. 질문이나 코멘트가 있다면 트위터에서 [@benbjohnson](https://twitter.com/benbjohnson)로 찾아오면 된다.



# 바이트와 스트링의 간단한 비교

Rob Pike는 [strings, bytes, runes, and characters](https://blog.golang.org/strings)에 대한 훌륭한 포스트를 가지고 있지만 이 포스트에서는 애플리케이션 개발자의 관점에서 좀 더 간결한 정의를 제공하려고 한다.

바이트 슬라이스는 변형 가능하고, 크기 조절이 가능한 연속적인 바이트 리스트를 나타낸다. 그럼 이제 이게 무슨 뜻인지를 알아보자.

바이트 슬라이스가 하나 주어졌다:

```go
buf := []byte{1,2,3,4}
```
이는 변형 가능하므로 원소 갱신이 가능하다:

```go
buf[3] = 5  // []byte{1,2,3,5}
```
이는 크기 조절이 가능하므로 축소 또는 확장이 가능하다:

```go
buf = buf[:2]           // []byte{1,2}           
buf = append(buf, 100)  // []byte{1,2,100}
```

그리고 이는 연속적이므로 메모리상에서 각 바이트는 다른 바이트의 바로 뒤에 위치한다:

```go
1|2|3|4
```

반면에, 문자열은 변형 불가능하고, 고정된 크기의 연속적인 바이트 리스트를 나타낸다. 이는 문자열은 갱신이 불가능함을 의미한다 - 새로운 문자열을 생성하는 수 밖에 없다. 이는 성능 관점에서 중요하다. 고성능 코드에서, 계속해서 새로운 문자열을 생성하면 가비지 컬렉터에 많은 부하가 추가된다.

애플리케이션 개발 관점에서, 문자열은 UTF-8 데이터로 작업 할 때 사용하기 쉬우며 바이트 슬라이스를 사용할 수 없는 맵(map)의 키값으로도 쓰일 수 있다. 그리고 대다수의 API는 문자 데이터를 포함하는 인자에 문자열을 사용한다. 반면에, 바이트 슬라이스는 바이트 스트림을 처리하는 등의 로우(raw) 바이트를 다룰 때 사용하기 좋다. 이는 또한 새로 할당을 하지 않고 데이터를 재사용 해야할 경우에 유용하다.



# 스트림을 위한 문자열&슬라이스 개조하기

바이트와 문자열 패키지의 가장 중요한 특징중 하나는 io.[Reader](https://golang.org/pkg/io/#Reader)와 io.[Writer](https://golang.org/pkg/io/#Writer)로 인메모리 바이트 슬라이스와 문자열을 인터페이싱할 수 있는 방법을 제공한다는 것이다.



## 인메모리 Reader

Go 표준 라이브러리에서 가장 많이 사용되는 툴 두 가지는 bytes.[NewReader](https://golang.org/pkg/bytes/#NewReader)와 strings.[NewReader](https://golang.org/pkg/strings/#NewReader) 함수이다:

```go
func NewReader(b []byte) *Reader
func NewREader(s string) *Reader
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), (byte)Reader : [Reader](https://golang.org/pkg/bytes/#Reader), string : [string](https://golang.org/pkg/builtin/#string), (string)Reader : [Reader](https://golang.org/pkg/strings/#Reader)

이 함수들은 인메모리 바이트 슬라이스 또는 문자열을 래핑하는 io.[Reader](https://golang.org/pkg/io/#Reader) 구현체를 반환한다. 그러나 이들은 단순한 Reader가 아니다. 이들은 [io](https://golang.org/pkg/io/)의 io.[ReaderAt](https://golang.org/pkg/io/#ReaderAt), io.[WriterTo](https://golang.org/pkg/io/#WriterTo), io.[ByteReader](https://golang.org/pkg/io/#ByteReader), io.[ByteScanner](https://golang.org/pkg/io/#ByteScanner), io.[RuneReader](https://golang.org/pkg/io/#RuneReader), io.[RuneScanner](https://golang.org/pkg/io/#RuneScanner), & io.[Seeker](https://golang.org/pkg/io/#Seeker)를 포함하는 읽기와 관련된 모든 인터페이스를 구현하고 있다.

나는 바이트 슬라이스 또는 문자열이 bytes.[Buffer](https://golang.org/pkg/bytes/#Buffer)에 쓰여지고 이 버퍼가 Reader로써 사용되는 코드를 자주 본다:

```go
var buf bytes.Buffer
buf.WriteString("foo")
http.Post("http://example.com/", "text/plain", &buf)
```

그러나, 이 방법은 힙 메모리 할당을 발생시켜 느려지고 추가적인 메모리를 사용하게 된다. 더 좋은 방안은 strings.[Reader](https://golang.org/pkg/strings/#Reader)을 사용하는 것이다:

```go
r := strings.NewReader("foobar")
http.Post("http://example.com", "text/plain", r)
```

이 방법은 io.MultiReader를 사용함으로써 여러개의 문자열 또는 바이트 슬라이스를 가지고 있을 때에도 잘 동작한다:

```go
r := io.MultiReader(
        strings.NewReader("HEADER"),
        bytes.NewReader([]byte{0,1,2,3,4}),
        myFile,
        strings.NewReader("FOOBAR"),
)
```



## 인메모리 Writer

바이트 패키지는 [Buffer](https://golang.org/pkg/bytes/#Buffer)라고 부르는 io.[Writer](https://golang.org/pkg/io/#Writer)의 인메모리 구현체를 포함하고 있다. 이는 io.[Closer](https://golang.org/pkg/io/#Closer)와 io.[Seeker](https://golang.org/pkg/io/#Seeker)를 제외한 [io](https://golang.org/pkg/io/) 인터페이스의 거의 모든 것을 구현하고 있다. 여기엔 버퍼의 끝에 문자열을 쓰기위한 [WriteString](https://golang.org/pkg/bytes/#Buffer.WriteString)()이라는 헬퍼 메서드도 있다.

나는 유닛 테스트에서 서비스에서 나오는 로그들을 캡쳐하기위해 [Buffer](https://golang.org/pkg/bytes/#Buffer)를 광범위하게 사용한다. 여러분은 이를 log.[New](https://golang.org/pkg/log/#New)()의 인자로써 전달할 수 있으며 나중에 출력값들을 검증할 수 있다:

```go
var buf bytes.Buffer
myService.Logger = log.New(&buf, "", log.LstdFlags)
myService.Run()

if !strings.Contains(buf.String(), "service failed") {
        t.Fatal("expected log message")
}
```
> Buffer : [Buffer](https://golang.org/pkg/bytes/#Buffer), LstdFlags : [LstdFlags](https://golang.org/pkg/log/#LstdFlags), Contains : [Contains](https://golang.org/pkg/strings/#Contains), String : [String](https://golang.org/pkg/bytes/#Buffer.String), Fatal : [Fatal](https://golang.org/pkg/testing/#T.Fatal)

그러나, 나는 프로덕션 코드에서는 [Buffer](https://golang.org/pkg/bytes/#Buffer)를 거의 사용하지 않는다. 이름이 Buffer임에도 불구하고, 해당 목적에 좀 더 특화된 [bufio](https://golang.org/pkg/bufio/) 패키지가 있기 때문에 Buffer를 버퍼 읽기와 쓰기에 사용하지 않는다.



# 패키지 구조

바이트와 문자열 패키지의 첫인상은 큰 패키지처럼 보이지만 이는 단지 그냥 간단한 헬퍼 함수들의 컬렉션이다. 우리는 이들을 몇 개의 카테고리로 그룹핑 할 수 있다.

* 비교 함수 (Comparison functions)
* 검사 함수 (Inspection functions)
* 접두/접미 함수 (Prefix/suffix functions)
* 교체 함수 (Replacement functions)
* 분할 & 결합 함수 (Splitting & joining functions)

함수들이 어떻게 함께 그룹핑 되는지를 이해한다면, 대형 API에 접근하기가 훨씬 쉬워질 것이다.



# 비교 함수

두 개의 바이트 슬라이스 또는 문자열을 가지고 있을 때 다음 두 질문중 하나를 질문해야 할 수도 있다. 첫째, 두 객체가 동일한가? 둘째, 정렬시 어떤게 앞쪽에 위치하는가?



## 동일성

[Equal](https://golang.org/pkg/bytes/#Equal)() 함수는 첫번째 질문에 대한 답을 준다:

```go
func Equal(a, b []byte) bool
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), bool : [bool](https://golang.org/pkg/builtin/#bool)

이 함수는 바이트 패키지에만 존재하는데 문자열은 == 연산자로 비교가 가능하기 때문이다.

동일함을 확인하는건 쉬워 보일 수 있지만, 한 가지 일반적으로 나타나는 실수는 대소문자에 무관하게 동일함을 확인하기 위해 strings.[ToUpper](https://golang.org/pkg/strings/#ToUpper)()를 사용하는 것이다.

```go
if strings.ToUpper(a) == strings.ToUpper(b) {
    return True
}
```

이는 새로운 문자열에 대한 2번의 할당이 필요하기 때문에 결점이 있다. 좀 더 나은 방법은 [EqualFold](https://golang.org/pkg/bytes/#EqualFold)()를 사용하는 것이다:

```go
func EqualFold(s, t []byte) bool
func EqualFold(s, t string) bool
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string), bool : [bool](https://golang.org/pkg/builtin/#bool)

"Fold"라는 용어는 [Unicode case-folding](https://www.w3.org/International/wiki/Case_folding)을 말한다. 이는 A-Z에 대한 일반적인 대소문자 규칙뿐만 아니라 ϕ의 φ로의 변환과 같은 다른 언어에 대한 규칙까지 포함한다.



# 비교

우리는 두 개의 바이트 슬라이스 또는 문자열의 정렬 순서를 결정하기 위해 [Compare](https://golang.org/pkg/bytes/#Compare)()를 사용할 것이다.

```go
func Compare(a, b []byte) int
func Compare(a, b string) int
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string), int : [int](https://golang.org/pkg/builtin/#int)

이 함수는 *a* 가 *b* 보다 작을 땐 -1, *a* 가 *b* 보다 클 땐 1, 그리고 *a* 와 *b* 가 같을 땐 0을 반환한다. 이 함수는 [strings](https://golang.org/pkg/strings/) 패키지에도 존재하는데 이는 단지 바이트 패키지와의 대칭을 이루기 위해서이다. Russ Cox도 함수의 주석에 써있는 "[기본적으로 그 누구도 strings.Compare를 사용해선 안된다](https://golang.org/src/strings/compare.go?s=697:741)"라는 말을 언급하며, 내장 연산자인 < 와 >를 대신 사용한다.

 *"기본적으로 그 누구도 strings.Compare를 사용해선 안된다", Russ Cox* 

일반적으로, 정렬을 하기 위해 바이트 슬라이스가 다른 바이트 슬라이스보다 작은지에 대한 여부를 알고 싶을 것이다. sort.[Interface](https://golang.org/pkg/sort/#Interface)는 Less() 함수를 위해 이를 필요로한다. Less()는 [Compare](https://golang.org/pkg/bytes/#Compare)()의 세 가지 값을 갖는 반환값의 부울값으로의 변환을 필요로한다. 우리는 단순히 이 값이 -1과 같은지를 확인한다:

```go
type ByteSlices [][]byte

func (p ByteSlices) Less(i, j int) bool {
        return bytes.Compare(p[i], p[j]) == -1
}
```



# 검사(Inspection) 함수

바이트와 문자열 패키지는 바이트 슬라이스와 문자열내에서 데이터를 찾기 위한 몇 가지 방법들을 제공한다.



## 카운팅

만약 여러분이 유저의 입력의 유효성을 검증하고 있다면, 특정 바이트가 존재하는지 (또는 존재하지 않는지)를 검증하는건 중요하다. 하나 이상의 부분 슬라이스 또는 부분 문자열의 존재를 확인하기 위해 [Contains](https://golang.org/pkg/bytes/#Contains)() 함수를 사용할 수 있다:

```go
func Contains(b, subslice []byte) bool
func Contains(s, substr string) bool
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string), bool : [bool](https://golang.org/pkg/builtin/#bool)

예를 들면, 여러분이 부적절한 특정 단어들을 가진 입력을 허용하지 않을 수도 있다:

```go
if strings.Contains(input, "darn") {
        return errors.New("inappropriate input")
}
```

부분 슬라이스나 부분 문자열이 정확히 몇 번 사용되었는지를 알기 위해선, [Count](https://golang.org/pkg/bytes/#Count)()를 사용할 수 있다:

```go
func Count(s, sep []byte) int
func Count(s, sep string) int
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string), int : [int](https://golang.org/pkg/builtin/#int)

[Count](https://golang.org/pkg/bytes/#Count)()의 또 다른 사용은 문자열에 있는 룬의 갯수를 반환하는 것이다. *sep* 인자에 빈 슬라이스나 빈 문자열을 전달하면 [Count](https://golang.org/pkg/bytes/#Count)()는 룬의 갯수 + 1을 반환할 것이다. 이는 바이트 수를 반환하는 [len](https://golang.org/pkg/builtin/#len)()과는 다르다. 이 차이점은 멀티바이트 유니코드 문자들을 다룰 때 중요하다:

```go
strings.Count("I ❤ ☃", "")  // 6
len("I ❤ ☃")                // 9
```

첫번째 줄은 룬이 5개만 있기 때문에 이상하게 보일 수 있지만 [Count](https://golang.org/pkg/bytes/#Count)()는 룬 갯수 + 1을 반환한다는 걸 기억하라.



## 인덱싱

내용을 단언하는건 중요하지만 가끔은 부분 슬라이스나 부분 문자열의 정확한 위치를 찾아낼 필요가 있다. 인덱스 함수를 사용하면 가능하다:

```go
Index(s, sep []byte) int
IndexAny(s []byte, chars string) int
IndexByte(s []byte, c byte) int
IndexFunc(s []byte, f func(r rune) bool) int
IndexRune(s []byte, r rune) int
```

각기 다른 상황을 위한 여러개의 인덱스 함수들이 있다. [Index](https://golang.org/pkg/bytes/#Index)()는 멀티바이트 부분 슬라이스를 찾는다. [IndexByte](https://golang.org/pkg/bytes/#IndexByte)()는 슬라이스에서 단일 바이트를 찾는다. [IndexRune](https://golang.org/pkg/bytes/#IndexRune)()은 UTF-8로 해석된 바이트 슬라이스 내에서 유니코드 코드 포인트를 찾는다. [IndexAny](https://golang.org/pkg/bytes/#IndexAny)()는 [IndexRune](https://golang.org/pkg/bytes/#IndexRune)()처럼 동작하지만 한 번에 여러 개의 코드 포인트를 검색한다. 마지막으로, [IndexFunc](https://golang.org/pkg/bytes/#IndexFunc)()는 바이트 슬라이스에서 매칭될 때까지 각각의 룬을 검사하는 커스텀 함수를 전달할 수 있도록 해준다.

또한 바이트 슬라이스 또는 문자열의 끝의 첫번째 인스턴스를 검색하기 위한 함수의 매칭 세트도 있다.

```go
LastIndex(s, sep []byte) int
LastIndexAny(s []byte, chars string) int
LastIndexByte(s []byte, c byte) int
LastIndexFunc(s []byte, f func(r rune) bool) int
```

나는 인덱스 함수를 많이 사용하지 않는다, 왜냐하면 나는 보통 파서와 같은 좀 더 복잡한 것을 만들어야 하기 때문이다.



# 접두사, 접미사 & 트리밍(Trimming)

바이트 슬라이스 또는 문자열의 처음이나 끝부분의 내용을 가지고 작업하는 일은 검사의 특수한 경우지만 이는 한 섹션으로 다룰만큼 중요하다.



## 접두사 & 접미사 확인하기

접두사는 프로그래밍에서 자주 등장한다. 예를 들면, HTTP 경로는 보통 공통 접두사를 갖는 기능별로 그룹핑된다. 또 다른 예는 유저를 멘션하기 위한 "@"와 같은 문자열 첫부분의 특수 문자들이다.

[HasPrefix](https://golang.org/pkg/bytes/#HasPrefix)()와 [HasSuffix](https://golang.org/pkg/bytes/#HasSuffix)() 함수로 이러한 상황을 확인할 수 있다:

```go
func HasPrefix(s, prefix []byte) bool
func HasPrefix(s, prefix string) bool

func HasSuffix(s, prefix []byte) bool
func HasSuffix(s, suffix string) bool
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string), bool : [bool](https://golang.org/pkg/builtin/#bool)

이 함수들은 너무 간단해 보일 수 있지만 내가 본 한가지 흔한 실수는 개발자들이 길이가 0인 값을 확인하는 것을 잊어버릴 때이다:

```go
if str[0] == '@' {
        return true
}
```

이 코드는 간단해 보이지만 만약 *str*이 빈 문자열이라면 프로그램은 패닉을 발생 시킬 것이다. [HasPrefix](https://golang.org/pkg/bytes/#HasPrefix)() 함수는 여러분을 위해 이 유효성 검증을 포함하고 있다:

```
if strings.HasPrefix(str, "@") {
        return true
}
```
> HasPrefix : [HasPrefix](https://golang.org/pkg/bytes/#HasPrefix)()



## 트리밍(Trimming)

[bytes](https://golang.org/pkg/builtin/#byte)와 [strings](https://golang.org/pkg/builtin/#string) 패키지에서의 "trimming"이라는 용어는 바이트 슬라스나 문자열의 첫부분 그리고/또는 끝부분에 있는 바이트 또는 룬들을 제거한다는 의미이다. 이를 위한 가장 일반적인 함수는 [Trim](https://golang.org/pkg/bytes/#Trim)()이다:

```go
func Trim(s []byte, cutset string) []byte
func Trim(s string, cutset string) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

이는 문자열의 첫부분과 끝부분에서 *cutset*의 모든 룬을 제거할 것이다. 여러분은 또한 [TrimLeft](https://golang.org/pkg/bytes/#TrimLeft)()와 [TrimRight](https://golang.org/pkg/bytes/#TrimRight)()를 사용하면 각각 문자열의 첫부분에서만 또는 끝부분에서만 룬을 제거할 수도 있다:

그러나 일반적인 트리밍은 정말 흔하지 않다. 대다수의 경우는 공백 문자를 없애는 일이며 이를 위해 [TrimSpace](https://golang.org/pkg/bytes/#TrimSpace)()를 사용할 수 있다:

```
func TrimSpace(s []byte) []byte
func TrimSpace(s string) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

"\n\t" cutset을 가지고 트리밍을 하는걸로도 충분하다고 생각할 수 있지만 [TrimSpace](https://golang.org/pkg/bytes/#TrimSpace)()는 모든 유니코드로 정의된 공백을 트리밍 한다. 이는 스페이스, 개행, 그리고 탭 문자 뿐만 아니라 [thin space](https://en.wikipedia.org/wiki/Thin_space) 또는 [hair space](http://www.fileformat.info/info/unicode/char/200a/index.htm)와 같은 특이한 공백도 포함한다,

[TrimSpace](https://golang.org/pkg/bytes/#TrimSpace)()는 그냥 트리밍을 위해 전행(leading)과 후행(trailing) 룬들을 평가하는 함수인 [TrimFunc](https://golang.org/pkg/bytes/#TrimFunc)()의 [씬 래퍼](https://golang.org/src/strings/strings.go#L614)이다.

```go
func TrimSpace(s string) string {
        return TrimFunc(s, unicode.IsSpace)
}
```

이는 여러분이 자체적으로 후행 문자만을 위한 공백 트리머를 생성하는걸 쉽게 만들어준다.

```go
TrimRightFunc(s, unicode.IsSpace)
```
> TrimRightFunc : [TrimRightFunc](https://golang.org/pkg/bytes/#TrimRightFunc)

마지막으로, 문자셋 대신 접두사나 접미사만을 트리밍 하고싶은 경우엔 [TrimPrefix](https://golang.org/pkg/bytes/#TrimPrefix)()와 [TrimSuffix](https://golang.org/pkg/bytes/#TrimSuffix)() 함수를 사용하면 된다:

```go
func TrimPrefix(s, prefix []byte) []byte
func TrimPrefix(s, prefix string) string

func TrimSuffix(s, suffix []byte) []byte
func TrimSuffix(s, suffix string) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

이들은 만약 여러분이 접두사나 접미어를 교체하고 싶은 경우 [HasPrefix](https://golang.org/pkg/bytes/#HasPrefix)()와 [HasSuffix](https://golang.org/pkg/bytes/#HasSuffix)() 함수와 함께 사용할 수 있다. 예를 들면, 나는 나의 설정 파일 경로에 대한 배쉬 스타일의 홈 디렉토리 완성을 구현하기 위해 이를 사용한다.

```go
// 유저의 홈 디렉토리를 찾는다.
u, err := user.Current()
if err != nil {
    return err
} else if u.HomeDir == "" {
    return errors.New("hoem directory does not exist")
}

// 물결표 대시 접두사를 홈 디렉토리로 교체.
if strings.HasPrefix(path, "~/") {
    path = filepath.Join(u.HomeDir, strings.TrimPrefix(path, "~/"))
}
```



# 교체 함수



## 기본 교체

부분 슬라이스나 부분 문자열을 교체하는건 때때로 필수적이다. 대부분의 단순한 경우엔, [Replace](https://golang.org/pkg/bytes/#Replace)() 함수면 다 된다:

```go
func Replace(s, old, new []byte, n int) []byte
func Replace(s, old, new string, n int) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

이는 문자열에서 *old* 에 해당하는 모든 인스턴스를 *new* 로 바꿀 것이다. 교체의 횟수를 제한하려면 *n* 을 음이 아닌 정수로 지정하면 된다. 이 함수는 사용자 정의 템플릿에서 간단한 플레이스홀더를 가지고 있을 때 유용하다. 예를 들면, 유저가 "$NOW"를 지정하면 이를 현재 시각으로 교체하도록 할 수 있다;

```go
now := time.Now().Format(time.Kitchen)
println(strings.Replace(data, "$NOW", now, -1))
```
> Now : [Now](https://golang.org/pkg/time/#Now), Format : [Format](https://golang.org/pkg/time/#Time.Format), Kitchen : [Kitchen](https://golang.org/pkg/time/#Kitchen), Replace : [Replace](https://golang.org/pkg/strings/#Replace)

만약 다중 매핑을 가지고 있다면 strings.[Replacer](https://golang.org/pkg/strings/#Replacer)를 사용해야 할 것이다. 이는 strings.[NewReplacer](https://golang.org/pkg/strings/#NewReplacer)()에 old/new 쌍을 지정하여 사용한다.

```go
r := strings.NewReplacer("$NOW", now, "$USER", "mary")
println(r.Replace("Hello $USER, it is $NOW"))

// 출력값: Hello mary, it is 3:04PM
```
> NewReplacer : [NewReplacer](https://golang.org/pkg/strings/#NewReplacer), Replace : [Replace](https://golang.org/pkg/bytes/#Replace)



## 대소문자 교체

여러분은 대소문자 케이싱(casing: 문자 케이스 캐스팅)이 간단하다고 생각할 수도 있다. 그러나 Go는 유니코드를 가지고 동작하며 유니코드는 결코 간단하지 않다. 문자 케이싱은 3가지 타입이 있다: 대문자(upper case), 소문자(lower case) 그리고 타이틀 케이스(title case).

대문자와 소문자는 대다수의 언어에서 간단하며 여러분은 [ToUpper](https://golang.org/pkg/bytes/#ToUpper)()와 [ToLower](https://golang.org/pkg/bytes/#ToLower)() 함수를 사용할 수 있다:

```go
func ToUpper(s []byte) []byte
func ToUpper(s string) string

func ToLower(s []byte) []byte
func ToLower(s string) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

그러나 몇몇 언어에서는 케이싱의 룰이 다르다. 예를 들면, 터키어는 **i**의 대문자가 **İ**이다. 이러한 특수한 언어들을 위한 함수들의 특수한 버전이 존재한다.

```go
strings.ToUpperSpecial(unicode.TurkishCase, "i")
```
> ToUpperSpecial : [ToUpperSpecial](https://golang.org/pkg/strings/#ToUpperSpecial)

다음으로 타이틀 케이스는 [ToTitle](https://golang.org/pkg/bytes/#ToTitle)() 함수가 있다:

```go
func ToTitle(s []byte) []byte
func ToTitle(s string) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

그러나, 여러분이 [ToTitle](https://golang.org/pkg/bytes/#ToTitle)()를 사용하게 되면 놀랄 것이다. 모든 문자열이 대문자가 되어버렸다:

```go
println(strings.ToTitle("the count of monte cristo"))

// 출력값 : THE COUNT OF MONTE CRISTO
```
> ToTitle : [ToTitle](https://golang.org/pkg/bytes/#ToTitle)

왜냐하면 유니코드에서, 타이틀 케이스는 케이싱의 특정한 유형이며 각 단어의 첫번째 문자를 대문자화 시키는 방법이 아니다. 대부분의 경우, 타이틀 케이스와 대문자화는 동일하지만 차이점을 갖는 몇 개의 코드 포인트들이 존재한다. 예를 들면, **[lj](https://www.compart.com/en/unicode/U+01C9)** 코드 포인트 (맞다, 이는 하나의 코드 포인트이다)는 대문자화가 되면 **LJ**가 되지만 타이틀 케이스는 **Lj**이다.

여러분이 아마 찾고 있는 것은 [Title](https://golang.org/pkg/bytes/#Title)()이다:

```go
func Title(s []byte) []byte
func Title(s string) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

이 함수의 출력값은 예상했던 결과값이다:

```go
println(strings.Title("the count of monte cristo"))

// 출력값 : The Count Of Monte Cristo
```
> Title : [Title](https://golang.org/pkg/strings/#Title)



## 룬 매핑하기

바이트 슬라이스 또는 문자열에서 데이터를 교체하기 위한 또 다른 함수는 [Map](https://golang.org/pkg/bytes/#Map)()이다:

```go
func Map(mapping func(r rune) rune, s []byte) []byte
func Map(mapping func(r rune) rune, s string) string
```
> rune : [rune](https://golang.org/pkg/builtin/#rune), byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

이 함수는 모든 룬을 평가하고 이를 교체하는 함수를 전달하도록 한다. 인정하건데, 나는 이 포스트를 쓰기 시작할때까지 이 함수의 존재조차 모르고 있었기에 그 어떤 개인적인 일화도 알려줄 수가 없다.



# 분할(Splitting) & 결합(Joining) 함수

여러 번 우리는 서로 분할시킬 필요가 있는 문자열들을 구분하고있다. 예를 들면, 유닉스에서 경로들은 콜론으로 합쳐져 있으며 CSV 파일 포맷은 기본적으로 데이터 필드를 콤마로 구분하고 있다.



## 부분 문자열 분할

간단한 부분 슬라이스와 부분 문자열 분할을 위해, 우리는 Split() 함수를 사용한다:

```go
func Split(s, sep []byte) [][]byte
func SplitAfter(s, sep []byte) [][]byte
func SplitAfterN(s, sep []byte, n int) [][]byte
func SplitN(s, sep []byte, n int) [][]byte

func Split(s, sep string) []string
func SplitAfter(s, sep string) []string
func SplitAfterN(s, sep string, n int) []string
func SplitN(s, sep string, n int) []string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

이렇게 구분자로 바이트 슬라이스와 문자열을 쪼개면 부분 슬라이스들과 문자열들이 반환된다. "After" 함수는 서브 문자열의 끝에 구분자를 포함한다. "N" 함수는 발생할 수 있는 분할 횟수를 제한한다:

```go
strings.Split("a:b:c", ":")       // ["a", "b", "c"]
strings.SplitAfter("a:b:c", ":")  // ["a:", "b:", "c"]
strings.SplitN("a:b:c", ":", 2)   // ["a", "b:c"]
```

데이터를 분할하는 것은 매우 일반적인 작업이다. 하지만, 이는 보통 CSV와 같은 파일 포맷에서의 컨텍스트나 경로 분할의 컨텍스트에서 수행된다. 이 작업들을 위해, 나는 [encoding/csv](https://golang.org/pkg/encoding/csv/) 또는 [path](https://golang.org/pkg/path/) 패키지를 대신 사용한다.



## 카테고리성 분할

가끔 구분자로 룬의 열 대신 룬의 집합을 지정하고 싶을 때가 있다. 이 상황의 가장 좋은 예시는 가변적인 공백이 있는 상태에서 단어들을 쪼개는 것이다. 공백 구분 함수로 단순히 [Split](https://golang.org/pkg/bytes/#Split)()을 사용하게되면 연속적인 다중 공백이 있을 경우 빈 부분 문자열도 반환할 것이다. 대신 Fields() 함수를 사용할 수 있다:

```go
func Fields(s []byte) [][]byte
```
> byte : [byte](https://golang.org/pkg/builtin/#byte)

이는 연속된 공백 문자들을 하나의 구분자로 여긴다:

```go
func FieldsFunc(s []byte, f func(rune) bool) [][]byte
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), rune : [rune](https://golang.org/pkg/builtin/#rune)



## 문자열 결합

구분된 데이터를 쪼개는 대신, Join() 함수를 이용해 이를 하나로 합칠 수도 있다:

```go
func Join(s [][]byte, sep []byte) []byte
func Join(a []string, sep string) string
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), string : [string](https://golang.org/pkg/builtin/#string)

개발자들이 join 함수를 직접 구현할 때 내가 봤던 한가지 일반적인 실수가 있는데 이는 다음과 같다:

```go
var output string
for i, s := range a {
        output += s
        if i < len(a) - 1 {
            output += ","
        }
}
return output
```

이 코드의 결점은 많은 수의 할당을 생성하고 있다는 것이다. 문자열은 변형 불가능하기 때문에, 각 반복은 각 문자열 추가를 위해 새로운 문자열을 생성하고 있다. 반면에, strings.[Join](https://golang.org/pkg/strings/#Join)() 함수는 생성시에는 바이트 슬라이스 버퍼를 사용하며 이를 반환할 때 문자열로 변환한다. 이는 힙 메모리 할당을 최소화한다.



# 이외 함수들

카테고리를 찾지 못한 두 개의 함수가 있는 이는 여기서 다룬다. 첫째, [Repeat](https://golang.org/pkg/bytes/#Repeat)() 함수는 반복된 바이트 슬라이스 또는 문자열을 생성할 수 있도록 해준다. 솔직히, 나는 이를 터미널에서 내용을 구분하기 위해 라인을 만들때 사용했던것만 기억난다:

```go
println(strings.Repeat("-", 80))
```

또 다른 함수는 UTF-8으로 해석된 바이트 슬라이스 또는 문자열에 있는 모든 룬들의 슬라이스를 반환하는 [Runes](https://golang.org/pkg/bytes/#Runes)()이다. 나는 이 함수의 필요성을 전혀 느낀적이  없는데 이는 새로운 할당 없이 *string* 위에서 *for loop* 를 돌려 같은 일을 할 수 있기 때문이다.



# 결론

바이트 슬라이스와 문자열은 Go에서 가장 기본적인 요소이다. 이들은 바이트와 룬의 열을 위한 인메모리 표현이다. 바이트와 문자열 패키지는 io.Reader와 io.Writer 인터페이스의 개조형뿐만 아니라 수 많은 유용한 헬퍼 함수들을 제공한다.

API의 사이즈가 크지 않기 때문에 이 패키지들의 많은 유용한 툴들을 살펴보는일은 쉽지만 나는 이 포스트가 여러분이 이 패키지가 제공해줄 수 있는 모든 것들을 이해하는데 도움이 되었으면 한다.

---

## File: content/post/2016-11-05-underscore-in-python.md

---
categories:
- python
comments: true
date: 2016-11-05T00:00:00Z
tags:
- deep dive
- underscore
title: 파이썬 언더스코어(_)에 대하여
url: /underscore-in-python
---

파이썬에서 언더스코어(underscore, \_)는 특별하다.

타 언어에서 언더스코어(\_)는 단지 스네이크 표기법의 변수나 함수명을 위해서만 사용되어지는 반면 (물론 그렇지 않은 언어도 있다), 파이썬에서는 이 문자의 의미가 다양하다. 아마 파이썬 프로그래머라면 `for _ in range(10)`나 `__init__(self)`등의 문법들이 굉장히 익숙할 것이다. 이번 포스트에서는 이 언더스코어(\_)가 언제 어떤 의미로 쓰이는지에 대해 다루어보려고 한다. 크게 기술적인 내용은 아니지만 파이썬 프로그래머로서 알아두면 좋을 것 같아 정리해보려고 한다.

파이썬에서 언더스코어(\_)는 다음과 같은 상황에서 사용되는데 크게 5가지의 경우가 있다.

* 인터프리터(Interpreter)에서 마지막 값을 저장할 때
* 값을 무시하고 싶을 때 (흔히 "I don't care"라고 부른다.)
* 변수나 함수명에 특별한 의미 또는 기능을 부여하고자 할 때
* 국제화(Internationalization, i18n)/지역화(Localization, l10n) 함수로써 사용할 때
* 숫자 리터럴값의 자릿수 구분을 위한 구분자로써 사용할 때

그럼 이제 각 경우에 대해서 언더스코어(\_)가 어떻게 쓰이는지 살펴보자.



# 1. 인터프리터에서 사용되는 경우

파이썬 인터프리터에선 마지막으로 실행된 결과값이 _라는 변수에 저장된다. 이는 표준 CPython 인터프리터에서 먼저 사용되었으며 다른 파이썬 인터프리터 구현체에서도 똑같이 사용할 수 있다.

```shell
>>> 10
10
>>> _
10
>>> _ * 3
30
>>> _ * 20
600
```



# 2. 값을 무시하고 싶은 경우

\_는 또한 어떤 특정값을 무시하기 위한 용도로 사용되기도한다. 값이 필요하지 않거나 사용되지 않는 값을 \_에 할당하기만 하면된다.


```python
# 언패킹시 특정값을 무시
x, _, y = (1, 2, 3) # x = 1, y = 3

# 여러개의 값 무시
x, *_, y = (1, 2, 3, 4, 5) # x = 1, y = 5

# 인덱스 무시
for _ in range(10):
    do_something()

# 특정 위치의 값 무시
for _, val in list_of_tuple:
    do_something()
```



# 3. 특별한 의미의 네이밍을 하는 경우

파이썬에서 \_가 가장 많이 사용되는 곳은 아마 네이밍일 것이다. 파이썬 컨벤션 가이드라인인 PEP8에는 다음과 같은 4가지의 언더스코어를 활용한 네이밍 컨벤션을 소개하고 있다.

* `_single_leading_underscore`: 주로 한 모듈 내부에서만 사용하는 private 클래스/함수/변수/메서드를 선언할 때 사용하는 컨벤션이다. 이 컨벤션으로 선언하게 되면 `from module import *`시 \_로 시작하는 것들은 모두 임포트에서 무시된다. 그러나, 파이썬은 진정한 의미의 private을 지원하고 있지는 않기 때문에 private을 완전히 강제할 수는 없다. 즉, 위와 같은 임포트문에서는 무시되지만 직접 가져다 쓰거나 호출을 할 경우엔 사용이 가능하다. 그래서 "weak internal use indicator"라고 부르기도 한다.

  ```python
  _internal_name = 'one_module' # private 변수
  _internal_version = '1.0' # private 변수
  ```


  class _Base: # private 클래스
      _hidden_factor = 2 # private 변수

      def __init__(self, price):
          self._price = price
    
      def _double_price(self): # private 메서드
          return self._price * self._hidden_factor
    
      def get_double_price(self):
          return self._double_price()
  ```

* `single_trailing_underscore_`: 파이썬 키워드와의 충돌을 피하기 위해 사용하는 컨벤션이다. 그리 많이 사용하지는 않을 것이다.

  ```python
  Tkinter.Toplevel(master, class_='ClassName') # class와의 충돌을 피함

  list_ = List.objects.get(1) # list와의 충돌을 피함
  ```

* `__double_leading_underscores`: 이는 컨벤션이라기보단 하나의 문법적인 요소이다. 더블 언더스코어는 클래스 속성명을 맹글링하여 클래스간 속성명의 충돌을 방지하기 위한 용도로 사용된다. (맹글링이란, 컴파일러나 인터프리터가 변수/함수명을 그대로 사용하지 않고 일정한 규칙에 의해 변형시키는 것을 말한다.) 파이썬의 맹글링 규칙은 더블 언더스코어로 지정된 속성명 앞에 _ClassName을 결합하는 방식이다. 즉, ClassName이라는 클래스에서 __method라는 메서드를 선언했다면 이는 _ClassName__method로 맹글링 된다.

  ```python
  class A:
      def _single_method(self):
          pass

      def __double_method(self): # 맹글링을 위한 메서드
          pass

  class B(A):
      def __double_method(self): # 맹글링을 위한 메서드
          pass

  print(dir(A())) # ['_A__double_method', ..., '_single_method']
  print(dir(B())) # ['_A__double_method', '_B__double_method', ..., '_single_method']

  # 서로 같은 이름의 메서드를 가지지만 오버라이드가 되지 않는다.
  ```

  더블 언더스코어로 지정된 속성명은 위와 같이 맹글링이 되기 때문에 일반적인 속성 접근인 `ClassName.__method`으로 접근이 불가능하다. 간혹, 이러한 특징으로 더블 언더스코어를 사용해 진짜 private처럼 보이게 하는 경우가 있는데 이는 private을 위한 것이 아니며 private으로의 사용도 권장되지 않는다. 이에 대한 자세한 내용은 [Python Naming](http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#naming)을 참고하면 좋을 것 같다.

* `__double_leading_and_trailing_underscores__`: 스페셜 변수나 메서드(매직 메서드라고도 부른다.)에 사용되는 컨벤션이며, `__init__`, `__len__`과 같은 메서드들이 있다. 이런 형태의 메서드들은 어떤 특정한 문법적 기능을 제공하거나 특정한 일을 수행한다. 가령, `__file__`은 현재 파이썬 파일의 위치를 나타내는 스페셜 변수이며, `__eq__`은  `a == b`라는 식이 수행될 때 실행되는 스페셜 메서드이다. 물론 사용자가 직접 만들 수도 있지만 그런 경우는 정말 거의 없으며, 일부 스페셜 메서드의 경우 직접 수정하거나 하는 일은 빈번히 있을 수 있다. `__init__`의 경우 클래스의 인스턴스가 생성될 때 처음으로 실행되는 메서드인데 인스턴스의 초기화 작업을 이 메서드의 내용으로 작성할 수 있다.

  ```python
  class A:
      def __init__(self, a): # 스페셜 메서드 __init__에서 초기화 작업을 한다.
          self.a = a

      def __custom__(self): # 커스텀 스페셜 메서드. 이런건 거의 쓸 일이 없다.
          pass
  ```

<br>

# 4. 국제화(i18n)/지역화(l10n) 함수로 사용되는 경우

이는 어떤 특정한 문법적 규칙이라기보단 말 그대로 컨벤션이다. 즉, \_가 국제화/지역화 함수라는 의미는 아니며, i18n/l10n 함수를 \_로 바인딩하는 C 컨벤션에서 유래된 컨벤션이다. i18n/l10n 라이브러리인 `gettext`라는 파이썬 내장 라이브러리 API 문서에서도 이 컨벤션을 사용하고 있으며, i18n과 l10n을 지원하는 파이썬 웹 프레임워크인 Django의 공식 문서에서도 이 컨벤션을 소개하면서 사용하고 있다.

```python
# gettext 공식 문서 참고 : https://docs.python.org/3/library/gettext.html
import gettext

gettext.bindtextdomain('myapplication', '/path/to/my/language/directory')
gettext.textdomain('myapplication')
_ = gettext.gettext

# ...

print(_('This is a translatable string.'))
```

```python
from django.utils.translation import ugettext as _
 from django.http import HttpResponse

def translate_view(request):
  translated = _('This string will be translated.')
  return HttpResponse(translated)
```

<br>

# 5. 숫자 리터럴값의 자릿수 구분을 위한 구분자로써 사용할 때

Python 3.6에 추가된 문법으로 언더스코어로 숫자값을 좀 더 읽기 쉽도록 자릿수를 구분할 수 있게 되었다.

```python
dec_base = 1_000_000
bin_base = 0b_1111_0000
hex_base = 0x_1234_abcd

print(dec_base) # 1000000
print(bin_base) # 240
print(hex_base) # 305441741
```



이상으로 파이썬 언더스코어(\_)에 대해 다뤄보았다. 나도 파이썬으로 개발을 해오며 위와 같은 상황을 많이 만나봤었는데 이 포스트를 다 작성하고보니 몇몇은 내가 잘못 알고 있었던것도 있어서 또 하나 배울 수 있었다. 특히 4번의 i18n/l10n은 포스트를 작성하기 전까지는 몰랐었는데 이번 포스팅을 위한 자료를 찾으면서 알게되었다. 그래도 나름 얻은게 꽤 있는 것 같다. 나 뿐만 아니라 여러분도 이 포스트에서 얻어가는게 있었으면 좋겠다.

다음에는 파이썬의 더욱 흥미로운 내용들을 다뤄보겠다.



# Update

Python 3.6의 ***PEP 515 -- Underscores in Numeric Literals*** 추가

---

## File: content/post/2016-11-09-translation-go-walkthrough-encoding-package.md

---
categories:
- translation
- go
comments: true
date: 2016-11-09T00:00:00Z
tags:
- go walkthrough
- encoding
title: '[번역] Go 둘러보기 - encoding 패키지'
url: /translation-go-walkthrough-encoding-package
---

> [Go Walkthrough](https://medium.com/go-walkthrough) 시리즈의 [Go Walkthrough: encoding package](https://medium.com/go-walkthrough/go-walkthrough-encoding-package-bc5e912232d#.hgvug2knk)를 번역한 글입니다.

우리는 이제까지 [로우(raw) 바이트 스트림](https://mingrammer.com/translation-go-walkthrough-io-package)과 [제한된 바이트 슬라이스](https://mingrammer.com/translation-go-walkthrough-bytes-strings-packages)를 다뤄봤지만 단순히 바이트만을 사용하는 애플리케이션은 별로 없다. 바이트 자체는 많은 의미를 전달해주지 않지만 바이트 위에서 데이터 구조를 인코딩 한다면 우리는 진정으로 유용한 애플리케이션을 구축할 수 있다.

이 포스트는 표준 라이브러리를  이해하는데 도움을 주기위한 Go 둘러보기 시리즈의 일부이다. 기존에 생성된 문서(자동으로 생성된 Go 문서)는 많은 정보를 제공하지만, 이는 패키지를 실제 상황에서 이해하기에는 어려울 수 있다.
이 시리즈는 일상적으로 사용되는 애플리케이션에서 표준 패키지들이 어떻게 사용되는지에 대한 컨텍스트를 제공할 수 있도록 도와준다. 질문이나 코멘트가 있다면 트위터에서 [@benbjohnson](https://twitter.com/benbjohnson)로 찾아오면 된다.



# 인코딩이란 정확히 무엇인가?

컴퓨터 과학에서는 간단한 개념에 대한 팬시한 단어들이 있다. 이 뿐만 아니라 많은 경우 하나의 개념을 지칭하는 많은 팬시한 단어들이 존재하기도 한다. *인코딩(Encoding)* 은 그 중 하나이다. 가끔 이는 *시리얼라이제이션(serialization)* 또는 *마샬링(marshaling)* 으로 불리기도 하며 이는 모두 로우(raw) 바이트에 논리적 구조체를 더한다는 것을 의미한다.

Go 표준 라이브러리에서, 우리는 두 가지의 분리되었지만 서로 관련이 있는 아이디어를 위해 *encoding*과 *marshaling* 라는 용어를 사용한다. Go에서 *encoder* 는 구조체를 바이트 스트림에 적용하는 객체인 반면 *marshaling* 은 구조체를 제한된 인메모리 바이트에 적용하는 것을 말한다.

예를 들면, *encoding/json* 패키지는 각각의 io.[Writer](https://golang.org/pkg/io/#Writer)와 io.[Reader](https://golang.org/pkg/io/#Reader)로 작업을 하기 위한 json.[Encoder](https://golang.org/pkg/encoding/json/#Encoder)와 json.[Decoder](https://golang.org/pkg/encoding/json/#Decoder)를 가지고있다. 이 패키지는 또한 바이트 슬라이스에 데이터를 쓰고 읽기 위한 json.[Marshaler](https://golang.org/pkg/encoding/json/#Marshaler)과 json.[Unmarshaler](https://golang.org/pkg/encoding/json/#Unmarshaler)를 가지고 있다.



## 두 가지 타입의 인코딩

인코딩간에는 또 다른 중요한 차이점이 있다. 몇몇의 인코딩 패키지는 문자열, 정수등의 프리미티브에서 동작한다. 문자열은 아스키나 [유니코드](https://golang.org/pkg/unicode/) 또는 다른 [언어별 인코딩](https://godoc.org/golang.org/x/text)과 같은 문자 인코딩을 가지고 인코딩이 된다. 정수는 [엔디안(endianness)](https://en.wikipedia.org/wiki/Endianness)기반 또는 가변 길이 인코딩에 따라 조금 다르게 인코딩 될 수 있다. 심지어 바이트자체는 종종 출력가능한 문자들로 변환 하기위해 [Base64](https://golang.org/pkg/encoding/base64/)와 같은 방식으로 인코딩된다.

그러나 우리는 종종 인코딩이라고 하면, 객체 인코딩을 떠올린다. 이는 *구조체(structs)*, *맵(maps)*, 그리고 슬라이스(slices) 같은 복잡한 구조체들을 바이트열로 변환하는것을 지칭한다. 이런 변환을 하는데 있어선 많은 트레이드 오프가 있으며 오랜 시간에 걸쳐 [많은 사람들이 서로 다른 객체 인코딩 방식을 개발해왔다](https://en.wikipedia.org/wiki/Comparison_of_data_serialization_formats).



## 타협(Trade-offs)하기

이 구조체들은 이미 내부적으로 바이트 형태의 인메모리로 표현되기 때문에 처음엔 논리적 구조체를 바이트로 변환하는게 충분히 간단해 보일 수 있다. 그냥 이 포맷을 사용하면 되지 않나?

왜 Go의 인메모리 포맷이 바이트를 변환해서 디스크에 저장하거나 네트워크를 통해 전송하는데에 적합하지 않은지에 대한 이유는 다양하다. 첫번째로 호환성이다. Go의 내부 데이터 구조 포맷은 Java의 내부 포맷과 맞지 않기에 우리는 서로 다른 시스템간 통신을 할 수가 없다. 가끔 우리는 프로그래밍 언어가 아닌 사람과의 호환성이 필요하다. [CSV](https://en.wikipedia.org/wiki/Comma-separated_values), [JSON](http://www.json.org/), 그리고 [XML](https://en.wikipedia.org/wiki/XML)은 모두 사람이 읽을 수 있는 포맷이며 보거나 수정하기가 쉽다.

사람이 읽기 가능한 포맷을 만드는것은 트레이드 오프를 이끌어낸다. 사람이 분석하기 쉬운 포맷은 컴퓨터가 분석하기에는 느리다. 정수가 좋은 예이다. 사람은 정수를 10진법으로 읽지만 컴퓨터는 2진법으로 동작한다. 사람은 또한 1또는 1,000과 같은 가변 길이의 숫자를 읽을 수 있지만 컴퓨터는 32비트나 64비트의 정수와 같은 고정 크기의 숫자를 가지고 동작한다. 성능이 숫자 하나에 대해선 별로 차이가 없어 보일 수 있지만 수백만, 수십억개의 숫자를 분석할 때에는 그 차이가 빠르게 벌어진다.

또한 우리가 처음에 생각하지 않은 다른 트레이드 오프도 있다. 데이터 구조는 시간이 지나면서 변하지만 우리는 여전히 오래전에 인코딩된 바이트 위에서 동작을 시켜야 할 필요가 있다. [프로토콜 버퍼(Protocol Buffers)](https://developers.google.com/protocol-buffers/)와 같은 몇몇 인코딩은 여러분의 데이터와 필드의 버전(새로운 필드가 추가될 수 있는 반면, 이전 필드가 더 이상 사용되지 않을 수 있다)에 대한 스키마를 작성할 수 있도록 해준다. 이것의 단점은 객체를 인코딩하고 디코딩 하기위해선 스키마 정의가 필요하다는 것이다. Go의 자체적인 [gob](https://golang.org/pkg/encoding/gob/) 포맷은 다른 방법을 택하는데 실제로 인코딩시 스키마 포맷을 포함한다. 그러나, 이 방법의 단점은 인코딩 사이즈가 매우 커질 수 있다는 것이다.

일부 포맷은 전적으로 주의를 기울여야 하고 유연한 스키마를 지향한다. [JSON](http://www.json.org/)과 [MessagePack](http://msgpack.org/index.html)는 여러분이 바로 구조체를 인코딩 하도록 해주지만 기존의 포맷에서 구조체를 안전하게 디코딩 하는것에 대한 보장을 제공해주진 않는다.

우리는 또한 따로 인코딩을 생각하지 않아도 우리를 위해 인코딩을 대신 수행해주는 시스템을 사용한다. 예를 들면, 데이터베이스는 우리의 논리적 데이터 구조를 가지고 이들을 디스크에 바이트로 영구 저장하는 우회적인 방법이다. 이는 네트워크 호출, SQL 파싱, 그리고 쿼리 계획을 포함 할 수 있지만 이들은 모두 기본적으로 인코딩이다.

마지막으로, 만약 여러분이 다른 것들보다 정말로 속도가 필요하다면, 데이터 저장을 위해 Go의 내부 포맷을 사용할 수 있다. 심지어 나는 이를 위해 [raw](https://github.com/boltdb/raw)라는 라이브러리를 만들기도했다. 이것의 인코딩과 디코딩 시간은 말그대로 0초다. 여러분은 이걸 프로덕션 환경에서 사용해야 하는가? 아마 아닐 것이다.



# 4가지 인코딩 인터페이스

만약 여러분이 [encoding](https://golang.org/pkg/encoding/) 패키지를 들여다본 몇 안되는 사람들 중 한 명이라면, 조금 실망했을 것이다. 이는 [errors](https://golang.org/pkg/errors/) 패키지 다음으로 두번째로 가장 작은 패키지이며 단 4개의 인터페이스만 가지고있다.

처음에 살펴볼 두 개의 인터페이스는 [BinaryMarshaler](https://golang.org/pkg/encoding/#BinaryMarshaler)와 [BinaryUnmashaler](https://golang.org/pkg/encoding/#BinaryUnmarshaler)이다:

```go
type BinaryMarshaler interface {
        MarshalBinary() (data []byte, err error)   
}

type BinaryUnmashaler interface {
        UnmarshalBinary (data []byte) error
}
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), error : [error](https://golang.org/pkg/errors/)

이들은 객체를 바이너리 포맷으로 변환하거나 역변환하는 방법을 제공한다. 이는 time.[Time](https://golang.org/pkg/time/#Time).[MarshalBinary](https://golang.org/pkg/time/#Time.MarshalBinary)와 같은 표준 라이브러리에서 몇군데 사용된다. 여러분은 이를 많이 보진 못할텐데 보통 객체를 바이너리 포맷으로 마샬링하는 방법이 단일하게 정의되어 있지 않기 때문이다. 보다시피, 수많은 serialization 포맷이 있다.

그러나, 애플리케이션 레벨에서 여러분은 아마 마샬링을 위해 하나의 포맷만 선택할 것이다. 예를 들면, 여러분은 여러분의 모든 데이터에 대해 프로토콜 버퍼를 선택 했을 수 있다. 일반적으로 애플리케이션 데이터를 위해 여러개의 바이너리 포맷을 지원할 이유가 없으므로 [BinaryMarshaler](https://golang.org/pkg/encoding/#BinaryMarshaler)를 구현하는건 의미가 있다.

다음으로 살펴볼 두 인터페이스는 [TextMarshaler](https://golang.org/pkg/encoding/#TextMarshaler)와 [TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler)이다:

```go
type TextMarshaler interface {
        MarshalText() (text []byte, err error)
}

type TextUnmarshaler interface {
        UnmarshalText(text []byte) error
}
```
> byte : [byte](https://golang.org/pkg/builtin/#byte), error : [error](https://golang.org/pkg/errors/)

이 두 인터페이스는 출력값이 UTF-8 포맷인걸 제외하고는 바이너리 마샬링 인터페이스와 유사하다.

몇몇 포맷은 json.[Marshaler](https://golang.org/pkg/encoding/json/#Marshaler)와 같이 동일한 네이밍 스타일을 따르는 자체적인 마샬링 인터페이스 가지고 있다.



# 인코딩 패키지 개요

표준 라이브러리에는 많은 유용한 인코딩 패키지들이 있다. 우리는 이들의 자세한 내용들은 나중 포스트에서 다룰 것이지만 일단 개요를 살펴보고자 한다. 이들 중 몇몇은 *encoding* 의 서브패키지인 반면 그 외에는 다른 곳에 뿔뿔이 흩어져 있다.



## 프리미티브 인코딩

Go를 시작할 때 처음으로 사용하게될 패키지는 아마 [fmt](https://golang.org/pkg/fmt/) 패키지일 것이다. ("fumpt"라 발음한다.) 이는 숫자, 문자열, 바이트, 그리고 일부 지원되는 객체 인코딩까지 포함한 것들을 인코딩 및 디코딩 하기 위해 C 스타일의 [printf](http://pubs.opengroup.org/onlinepubs/009695399/functions/fprintf.html)() 컨벤션을 사용한다. [fmt](https://golang.org/pkg/fmt/) 패키지는 템플릿으로부터 사람이 쉽게 읽을 수 있는 문자열을 훌륭하고 쉽게 만들 수 있는 방법을 제공하지만 템플릿 파싱은 추가적인 오버헤드를 발생시킬 수 있다.

만약 더 나은 성능이 필요하다면 문자열 변환 패키지인 [strconv](https://golang.org/pkg/strconv/)을 사용함으로써 템플레이팅을 피할 수 있다. 이는 기본적인 포맷팅 그리고 문자열, 정수, 실수, 그리고 부울을 위한 스캐닝을 제공하는 로우 레벨 패키지이며 매우 빠르다.

Go 자체와 함께 이 패키지들은 여러분이 UTF-8의 문자열을 인코딩함을 가정한다. 표준 라이브러리에서 유니코드가 아닌 문자의 인코딩 지원이  미약한건 지난 수년간에 걸쳐 UTF-8의 표준이 인터넷의 많은 부분을 빠르게 지배하고 있기 때문에 가능하거나 [Rob Pike](https://en.wikipedia.org/wiki/Rob_Pike)가 Go와 UTF-8의 공동개발자이기 때문에 가능한일 일 것이다. 누가 아는가? 나는 운좋게도 여태까지 Go에서 비 UTF-8의 인코딩을 처리할 필요가 전혀 없었다. 그러나, [unicode/utf16](https://golang.org/pkg/unicode/utf16/), [encoding/ascii85](https://golang.org/pkg/encoding/ascii85/), 그리고 [golang.org/x/text](https://godoc.org/golang.org/x/text) 패키지 트리에 몇가지 인코딩 지원이 있긴하다. *"x"* 패키지 트리는 Go 프로젝트의 일부로 엄청난 패키지들을 많이 포함하고 있지만 [Go 1 호환성 요건](https://golang.org/doc/go1compat)으로는 적용되지 않는다. 

정수 인코딩을 위해, [encoding/binary](https://golang.org/pkg/encoding/binary/) 패키지는 큰 엔디안과 작은 엔디안 인코딩뿐만 아니라 가변 길이의 인코딩도 제공한다. 엔디안이란 바이트가 디스크에 쓰여지는 순서를 가리키는 말이다. 예를 들면, *1,000* (16진법으로는 *0x03E8*) 의 *uint16* 표현식은 두 바이트 *03* 과*E8* 의 조합으로 이루어진다. 큰 엔디안 인코딩에서는 바이트가 "03 E8"의 순서로 쓰여진다. 작은 엔디안에서는 "E8 03"으로 순서가 뒤바뀐다. 많은 일반적인 CPU 아키텍처는 작은 엔디안을 사용한다. 그러나, 큰 엔디안은 보통 네트워크를 통해 바이트를 전송할 때 사용된다. 큰 엔디안은 그래서 *(네트워크 바이트 순서) network byte order* 라고도 한다.

마지막으로, 바이트 인코딩을 위해 사용할 수 있는 패키지 쌍이 있다. 바이트 인코딩은 보통 바이트를 출력가능한 포맷으로 변환하는데 사용된다. 예를 들면, [encoding/hex](https://golang.org/pkg/encoding/hex/) 패키지는 바이너리 데이터를 16진법으로 볼 필요가 있을때 사용될 수 있다. 나는 개인적으로 디버깅 목적으로만 사용해봤다. 반면, 가끔은 역사적으로 제한된 바이너리 지원 (예로 이메일이 있다.)을 가지고 프로토콜 위에서 데이터를 전송해야하기 때문에 출력가능한 포맷이 필요할 때도 있다. [encoding/base32](https://golang.org/pkg/encoding/base32/)과 [encoding/base64](https://golang.org/pkg/encoding/base64/) 패키지는 이의 한 예이다. 또 다른 예시는 TLS 인증서를 인코딩 하기 위해 사용되는 [encoding/pem](https://golang.org/pkg/encoding/pem/) 패키지가 있다.



## 객체 인코딩

우리는 표준 라이브러리에서 객체 인코딩을 위한 몇개의 패키지들을 찾았다. 그러나 실제로 이 패키지들은 우리가 필요로 하는 모든 것들을 가지고 있다.

만약 여러분이 지난 10년간 세상을 등지고 살아왔다면, 아마 [JSON](http://www.json.org/)이 인터넷의 기본 객체 인코딩이 되었다는 것을 알아챘을 것이다. 위에서도 언급했듯이, JSON은 결점을 가지고 있지만 사용하기가 쉬우며 모든 언어에서 라이브러리가 지원 되기 때문에 채택이 급증했다. [encoding/json](https://golang.org/pkg/encoding/json/) 패키지는 이 프로토콜을 위한 훌륭한 지원을 제공하며 [ffjson](https://github.com/pquerna/ffjson)과 같은 빠른 파서를 생성하기 위한 서드파티 구현체들 또한 존재한다.

JSON이 머신 사이의 프로토콜로서 지배하고 있는 동안, [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) 포맷은 사람들에게 데이터를 내보내기 위한 더 일반적인 프로토콜이다. [encoding/csv](https://golang.org/pkg/encoding/csv/) 패키지는 테이블 형식의 데이터를 이 포맷으로 내보내기 위한 좋은 인터페이스를 제공한다.

만약 여러분이 약 2000년경에 구축된 시스템과 상호 작용을 하는 경우엔 아마 [XML](https://en.wikipedia.org/wiki/XML)을 사용해야 할 것이다. [encoding/xml](https://golang.org/pkg/encoding/xml/) 패키지는 [json](https://golang.org/pkg/encoding/json/) 패키지와 유사한 추가적인 태그 기반 marshaler/unmarshaler를 가진 [SAX](https://en.wikipedia.org/wiki/Simple_API_for_XML) 스타일의 인터페이스를 제공한다. 만약 DOM, XPath, XSD, 또는 XSLT같은 좀 더 복잡한 기능들을 찾고 있다면 아마 cgo를 통한 [libxml2](http://xmlsoft.org/)을 사용해야 할 것이다.

Go는 또한 [gob](https://golang.org/pkg/encoding/gob/)라고 하는 자체적인 스트림 인코딩을 가지고 있다. 이 패키지는 두 Go 서비스간의 원격 프로시저 호출을 구현하기 위한 [net/rpc](https://golang.org/pkg/net/rpc/) 패키지에의해 사용된다. Gob은 사용하기가 쉽다. 그러나 이는 그 어떤 언어 크로스도 지원하지 않는다. 만약 서로 다른 언어간 통신이 필요하다면 [gRPC](http://www.grpc.io/)가 인기있는 대안이 될 수 있을 것 같다.

마지막으로, [encding/asn1](https://golang.org/pkg/encoding/asn1/)라는 패키지가 있다. 문서에는 제한된 정보만 있고 패키지에는 오직 25페이지의 텍스트로 이루어진 [layman의 ANS.1 가이드](http://luca.ntop.org/Teaching/Appunti/asn1.html)에 대한 링크만 있다. ANS.1은 특히 SSL/TLS의 X.509 인증서에 많이 사용되는 복잡한 객체 인코딩 스키마이다.



# 결론

인코딩은 바이트 위에서 정보를 레이어링 하기위한 기초적인 기반을 제공한다. 이게 없다면 우린 문자열이나 데이터 구조나 데이터베이스 또는 그 어떤 유용한 애플리케이션도 가질 수 없을 것이다. 상대적으로 간단한 개념처럼 보이는것이 많은 구현의 역사와 다양한 트레이드 오프를 가지고 있다.

이 포스트에서 우리는 표준 라이브러리에 있는 다양한 인코딩 구현의 개요와 그것들의 트레이드 오프들을 살펴봤다. 우리는 이러한 프리미티브와 객체 인코딩 패키지가 우리의 바이트 스트림과 슬라이스의 지식선상에서 어떻게 구현되는지를 보았다. 다음 몇 개의 포스트에선 실제 상황에서 이들을 어떻게 사용할 수 있는지를 알아보기 위해 이 패키지들을 좀 더 깊이 파헤쳐 볼 것이다.

---

## File: content/post/2016-12-03-translation-go-walkthrough-encoding-json-package.md

[Binary file]

---

## File: content/post/2017-01-01-review-2016.md

[Binary file]

---

## File: content/post/2017-01-13-presentation-awskrug-review-first-aws-experience.md

---
categories:
- presentation
- aws
comments: true
date: 2017-01-13T00:00:00Z
tags:
- aws
- review
title: '[발표] AWS 회고'
url: /presentation-awskrug-review-first-aws-experience
---

지난 11일 AWS Korea User Group 강남 SE 모임에서 "AWS 회고"라는 주제로 발표를 했다.

<script async class="speakerdeck-embed" data-id="4e5f0719844f49138674ba12ca02c2f5" data-ratio="1.33333333333333" src="//speakerdeck.com/assets/embed.js"></script>

처음 AWS를 사용해 아키텍쳐링을 해본 경험을 회고하며 부족했던점과 개선방법 그리고 아키텍쳐링 전에 알았으면 좋았을 것들에 대해 간략하게 정리하여 발표를 했다.

발표날까지 여유를 부리다 그만 발표 당일날 자료를 만들어 즉석 발표를 했지만 발표 후 반응을 들어보니 생각보다 잘 마쳤던 것 같다.

운좋게도 함께 모임에 참석했던 삼성에서 근무하시는 데브옵스 개발자분과도 인연이 생겨(?) 많은 조언도 들을 수 있었고 여러가지로 좋은 경험이었다.

---

## File: content/post/2017-01-16-first-publishing-english-post.md

[Binary file]

---

## File: content/post/2017-01-25-translation-iterators-vs-generators.md

[Binary file]

---

## File: content/post/2017-02-02-side-by-side-reference-sheet-of-python-and-go.md

---
categories:
- reference
comments: true
date: 2017-02-02T00:00:00Z
tags:
- python
- go
title: Python vs Go 비교 레퍼런스
url: /side-by-side-reference-sheet-of-python-and-go
---

> Python과 Go의 비교 레퍼런스 (각기 버전은 Python 3.5+와 Go 1.7+)
> 
> 각 코드 블록은 Python/Go 순서



# <a name='version'>버전</a>



## 버전 확인

```shell
// Python
$ python --version
3.5
```
```shell
// Go
$ go version
1.7
```





# <a name='invocation'>실행</a>



## Hello World

```python
print("Hello, World")
```

```go
import "fmt"

func main() {
  fmt.Println("Hello, World")
}
```

## 프로그램 실행

```shell
$ python3 hello.py
```

```shell
$ go build hello.go
$ ./hello

# 또는 다음과 같이 직접 실행할 수 있음
$ go run hello.go
```

## 파일 확장자

```
.py
```


```
.go
```





# <a name='grammar'>문법</a>



## 블록 구분자

```python
# 콜론(:)과 들여쓰기(indentation)로 구분   
def main():
    # body
```


```go
// 중괄호로 구분
func main() {
    // body
}
```

## 주석

```python
# 주석

"""
멀티라인 주석
"""
```


```go
// 주석

/*
멀티라인 주석
*/
```

## 컨벤션

```python
# 보통 변수나 함수, 메서드는 스네이크 케이스 사용
variables_use_snake_case = 10
def some_func()

# 클래스는 파스칼 케이스 사용
class SomeClass
```


```go
// 보통 카멜 케이스 사용
variablesUseCamelCase := 10
func someFunc()

// 노출되는 변수나 함수, 타입은 파스칼 케이스 사용
ExportedVariablesUseCapitanCase := 10
type ExportedType struct
```





# <a name='variables-and-expressions'>변수와 표현식</a>



## 변수

```python
i = 1
s = "string"
```


```go
var i int
i = 1

// var s string = "string"과 동일
s := "string"
```

## 전역 변수

```python
# foo.py:
# 최상위 스코프에 있으면 전역변수로 사용 가능
x = 1

# bar.py:
import foo

print(foo.x)
```


```go
// foo.go:
package foo

// 대문자로 시작하면 타 패키지에서도 참조 가능
var X = 1
// 소문자로 시작하는 변수가 최상위 스코프에 있으면
// 한 패키지에서만 전역 변수로 사용 가능
var y = 1

// bar.go:
package bar

import (
    "fmt"
    "foo"
)

func main() {
    fmt.Println(foo.X)
    // fmt.Println(foo.y)는 불가능
}
```

## 상수

```python
// Python엔 상수라는 개념이 없으며
// 관례적으로만 상수임을 나타내기위해 대문자로된 변수를 사용
PI = 3.14159
```


```go
const PI = 3.14159
```

## 할당

```python
i = 2
```


```go
// 선언과 동시에 할당
i := 2

// 변수 i가 미리 선언되어 있어야함
i = 2
```

## 병렬 할당

```python
x, y = 1, 2
```


```go
x, y := 1, 2

// z, w가 미리 선언되어 있어야함
z, w = 3, 4
```

## 변수값 교환 (Swap)

```python
x, y = y, x
```


```go
x, y = y, x
```

## 증감

```python
i += 1
i -= 1
```


```go
i++
i--
```

## 포인터

```python
# none
```


```go
i := 1

var ptr *int
ptr = &i

// 역참조
j := *ptr
```





# <a name='types-and-operators'>타입 및 연산자</a>



## 부울타입

```python
True False
```


```go
true false
```

## 정수타입

```python
int
```


```go
int
int8
int16
int32 (rune)
int64

uint8 (byte)
uint16
uint32
uint64
```

## 실수타입

```python
float
```


```go
float32
float64
```


## 널타입

```python
None
```


```go
nil
```

## 타입 사이즈

```python
x = 1
x.__sizeof__()

# sys 모듈을 사용할 때
import sys
sys.getsizeof(x)
```


```go
import "unsafe"

i := 1
unsafe.Sizeof(i)
```


## 논리 연산자

```python
and or not
```


```go
&& || !
```

## 관계 연산자

```python
== != < > <= >=
```


```go
== != < > <= >=
```

## 산술 연산자

```python
+ - * / %
```


```go
+ - * / %
```

## 비트 연산자

```python
<< >> & | ^ ~
```


```go
<< >> & | ^
```

## 삼항연산자

```python
y = 1 if x > 0 else 0
```


```go
// none
```

## 나눗셈 연산

```python
int(1 / 3)  # 정수 나누기
1 / 3  # 실수 나누기

# 참고로 Python2에선 다음과 같다
1 / 3  # 정수 나누기
1 / float(3)  # 실수 나누기  
```


```go
1 / 3 // 정수 나누기
1/ float32(3) // 실수 나누기
```




# <a name='strings'>문자열</a>


## 타입

```python
str
```


```go
string
```

## 리터럴

```python
"string"
'string'
```


```go
"string"
`string`
```

## 리터럴 개행

```python
s = 'first \n second'
```


```go
s := `first
second`
```

## 문자열 비교

```python
'first' < 'second'
'fisrt' == 'second'
```


```go
"first" < "second"
"first" == "second"
```

## 문자열을 숫자로 변환

```python
i = int('13')
f = float('3.14')
```


```go
import "strconv"

// 두번째 인자는 밑수(base), 세번째 인자는 비트 크기
i, _ := strconv.ParseInt("13", 10, 32)

// 두번째 인자는 비트 크기
f, _ := strconv.ParseFloat("3.14", 32)
```

## 숫자를 문자열로 변환

```python
str(13)
str(3.14)
```


```go
import "strconv"

// 두번째 인자는 밑수(base)
strconv.FormatInt(13, 10)

// 세번째 인자는 소숫점 아래 정확도, 네번째 인자는 비트 크기 
strconv.FormatFloat(3.14, 'f', 4, 32)
```

## Join과 Split

```python
# join
'-'.join('ab', 'cd', 'ef')

# split
'ab-cd-ed'.split('-')
```


```go
import "strings"

// join
parts := []string{"ab", "cd", "ef"}
s := strings.Join(parse, "-")

// split
parts = strings.Split(s, "-")
```

## 문자 인덱스

```python
"hello"[1]  # e
```


```go
import "strings"

strings.Index("hello", "1") // 3
```

## 케이스 변환

```python
"hello".upper()
"hello".lower()
```


```go
import "strings"

strings.ToUpper("hello")
strings.ToLower("HELLO")
```

## 문자열 길이

```python
len("hello")
```


```go
len("hello")
```

## 문자 타입

```python
str
```


```go
rune
```



# <a name='fixed-arrays'>고정 배열</a>



## 선언 및 초기화

```python
# Python은 고정 배열을 네이티브로 지원하지 않음

# 명시적으로 표기할 순 있지만 길이 변경이 가능함
arr = [0] * 10

# collections 내장 패키지를 사용해 흉내낼 순 있음
# 길이는 보장되지만 일반적인 고정 배열처럼 동작하진 않음
import collections
arr = collections.deque([0] * 10, maxlen=10)
```

```go
// 배열(array)이라고 부름

// 선언
var arr [10]int

// 초기화 리터럴 선언
arr := []int{1, 2, 3}
```



# <a name='resizable-arrays'>동적 배열</a>



## 선언 및 초기화

```python
# 선언
arr = [] # 또는 list()

# 초기화 선언
arr = [1, 2, 3]
```


```go
// 슬라이스(slice)라고 부름

// 선언. 5는 길이, 10은 수용크기 (capacity)
arr = make([]int, 5, 10)

// 초기화 리터럴 선언
arr := []int{1, 2, 3}
```

## 사이즈

```python
len(arr)
```


```go
// 길이. array도 동일
len(arr)

// capacity 크기. 미리 할당된 메모리로 추가적인 값을 저장할 수 있음
cap(arr)
```

## lookup과 update

```python
# lookup
a = arr[0]

# update
arr[0] = 1
```


```go
// array도 동일

// lookup
a = arr[0]

// update
arr[0] = 1
```

## 배열 순회

```python
# 값 순회
for e in arr:
    print(e)

# 인덱스, 값 순회
for i, e in enumerate(arr):
    print("index : {0}, value : {1}".format(i, e))
```


```go
// array도 동일

// 값 순회
for _, e := range arr {
    fmt.Println(e)
}

// 인덱스, 값 순회
for i, e := range arr {
    fmt.Printf("index : %d, value : %d\n", i, e)
}
```

## 슬라이싱

```python
arr = [1, 2, 3, 4 5]

# [2, 3]
arr[1:3]

# [3, 4, 5]
arr[2:]
```


```go
// array도 동일

arr := []int{1, 2, 3, 4, 5}
# {2, 3}
arr[1:3]

// {3, 4, 5}
arr[2:]
```

## 값 추가

```python
arr = [1, 2, 3]
arr.append(4)
```


```go
arr := []int{1, 2, 3}
arr = append(arr, 4)
```

## 확장

```python
arr = [1, 2]
arr2 = [3, 4]

# [1, 2, 3, 4]
arr3 = arr + arr2

# arr = [1, 2, 3, 4]
arr.extend(arr2)
```


```go
// array도 동일

arr = [1, 2]
arr2 = [3, 4]

// [1, 2, 3, 4]
arr3 := append(a, a2...)
```

## 복사

```python
from copy import deepcopy

arr = [1, 2, 3]

# 얕은 복사 (Shallow copy)
arr2 = arr

# 깊은 복사 (Deep copy)
arr3 = deepcopy(arr)
```


```go
arr := []int{1, 2, 3}

// 얕은 복사 (Shallow copy)
arr2 := arr

// 깊은 복사 (Deep copy)
arr3 := make([]int, len(arr))
copy(arr3, arr)
```





# <a name='dictionaries'>딕셔너리(맵)</a>



## 선언 

```python
# 선언
d = {} # 또는 dict()

# 초기화 선언
d = {"a": 1, "b": 2}
```


```go
// 선언
d := make(map[string]int)

// 초기화 리터럴 선언
d := map[string]int{"a": 1, "b": 2}
```

## 사이즈

```python
len(d)
```


```go
len(d)
```

## lookup과 update

```python
# lookup
d["a"]

# update
d["a"] = 2
```


```go
// lookup
d["a"]

// update
d["a"] = 2
```

## 키값 확인

```python
# True
'a' in d

# False
'c' in d
```


```go
// ok = true
val, ok = d["a"]

// ok = false
val, ok = d["c"]
```

## 키 삭제

```python
del d['a']
```


```go
delete(d, "a")
```

## 딕셔너리(맵) 순회

```python
# 키만 순회
for k in d.keys(): # 또는 for k in a:
    print(k)

# 값만 순회
for v in d.values():
    print(v)
    
# 키,값 순회
for i, v in d.items():
    print("key : {0}, value : {1}", i, v)
```


```go
// 키만 순회
for i, _ := range d {
    print(k)
}

// 값만 순회
for _, v := range d {
    print(v)
}

// 키,값 순회
for i, v := range d {
    fmt.Printf("key : %s, value : %d", i, v)
}
```



# <a name='functions'>함수</a>



## 선언

```python
def add(a, b):
    return a + b
```


```go
func add(a int, b int) int {
    return a + b
}

// 같은 타입은 묶을 수도 있음
func add(a, b int) int {
    return a + b
}
```

## 가변 길이 인자

```python
def concat(*args):
    ret = ""
    for s in args:
        ret += s
    return ret       
```


```go
func concat(strs ...string) string {
    var ret = ""
    for _, str := range strs {
        ret += str
    }
    return ret
}
```

## 이름 있는 인자

```python
def add(first=0, second=0):
    return first + second
    
val = add(first=1, second=2)

# 생략하면 위치에 따라 값이 들어감
val = add(1, 2)
```


```go
// none
```


## 다중 반환

```python
def divmod(m, n):
    return m / n, m % n
    
q, r = divmod(7, 3)
```


```go
func divmod(m, n int) (int, int) {
    return m / n, m % n
}

q, r := divmod(7, 3)
```

## 이름 있는 반환값

```python
# none
```


```go
func divmod(m, n int) (q, r int) {
    q = m / n
    r = m % n
    return
}

q, r := divmod(7, 3)
```



# <a name='conditional-expression'>조건문</a>



## if문

```python
x = 2

if x == 1:
    print("x is 1")
elif x == 2:
    print("x is 2")
else:
    print("x is neither 1 or 2")
```


```go
x := 2

if x == 1 {
    fmt.Println("x is 1")
} else if x == 2 {
    fmt.Println("x is 2")
} else {
    fmt.Println("x is neither 1 or 2")
}
```

## switch문

```python
# none
```


```go
// switch문은 모든 타입을 받을 수 있음

switch i {
case 0, 1:
    fmt.Println("i is boolean")
default:
    fmt.Println("i is not a boolean")
}

// 조건을 무시하면서 case를 통과하려면 "fallthrough"를 사용
```





# <a name='loops-expression'>반복문</a>



## while문

```python
i = 0

while i < 10:
    i += 1
```


```go
i := 0

for i < 10 {
    i++
}
```

## for문

```python
n = 0

for i in range(10):
    n += i
```


```go
n := 0

for i := 0; i <= 10; i++ {
    n += i
}
```

## for-in(range)문

```python
arr = [1, 2, 3]

for i in arr:
    print(i)
```


```go
arr := []int{1, 2, 3}

for _, v := range arr {
    fmt.Println(v)
}
```





# <a name='libraries-and-namespaces'>라이브러리 및 네임스페이스</a>



## 라이브러리 로드

```python
import foo

bar = foo.something()
```


```go
import "foo"

bar := foo.Something()
```

## 서브디렉토리에 있는 라이브러리 로드 

```python
import foo.bar
```


```go
import "foo/bar"
```

## 네임스페이스

```python
# 모듈 네임스페이스는 보통 "import"로 불러온 모듈명을 따라감
import os # os
from os import path # path
```


```go
// 모든 소스파일 최상단에 네임스페이스를 선언해야함
package foo
```

## 네임스페이스 별명붙이기 (aliasing)

```python
import foo as fu
```


```go
import fu "foo"
```

## 미사용 임포트

```python
# none
```


```go
import _ "foo"
```





# <a name='user-defined-types'>사용자 정의 타입</a>



## 정의

```python
# 타입으로써의 정의는 없으며 클래스로 대체
class Exam:
    name = 'example'
    avg = 0
    
    def __init__(self, math, physics, sports, name=""):
        self.math = math
        self.physics = physics
        self.sports = sports
        if name:
            self.name = name
```


```go
type Exam struct {
    name    string
    avg     int
    math    int
    physics int
    sports  int
} 
```

## 초기화

```python
ricky_exam = Exam(100, 92, 93, name="Ricky")
```


```go
rickyExam := Exam{"Ricky", 100, 92, 93}

risaExam := Exam{
    name: "Risa",
    math: 89,
    physics: 78,
    sports: 88,
}
```

## 속성(멤버)값 할당

```python
ricky_exam.math = 97
ricky_exam.sports = 95
```


```go
rickyExam.math = 97
rickyExam.sports = 95
```





# <a name='error-handling'>에러 처리</a>



## 에러 처리

```python
def divide(n, m):
    if m == 0:
        raise ValueError("divisor is zero")
    return n / m

try:
    q = divide(10, 0)
except Exception as e:
    print(e)
```


```go
import (
    "errors"
    "fmt"
)

func divide(n, m int) (float32, error) {
    if m == 0 {
        return 0,0, errors.New("divisor is zero")
    }
    return n / float32(m), nil
}

q, err := divide(10, 0)
if err != nil {
    fmt.Println(err)
}
```

---

## File: content/post/2017-02-28-translation-go-and-oop.md

[Binary file]

---

## File: content/post/2017-03-05-ways-to-manage-the-configuration-in-python.md

[Binary file]

---

## File: content/post/2017-03-20-understanding-the-asterisk-of-python.md

---
categories:
- python
comments: true
date: 2017-03-20T00:00:00Z
tags:
- deep dive
- asterisk
title: 파이썬의 Asterisk(*) 이해하기
url: /understanding-the-asterisk-of-python
---

파이썬은 타 언어에 비해 비교적 연산자 및 연산의 종류가 풍부한 편이다.

특히 파이썬이 지원하는 많은 연산자중 하나인 **Asterisk(\*)**는 단순히 곱셈 이상의 여러 의미를 갖는 연산들을 가능케한다. 이번 포스트에서는 파이썬을 좀 더 파이썬스럽게 (보통 **Pythonic하다**라고 표현한다) 쓰기 위해 이 **Asterisk(\*)**로 할 수 있는 여러 연산들을 살펴보고자 한다.

파이썬에서 **Asterisk(\*)**는 다음과 같은 상황에서 사용되는데 크게 4가지의 경우가 있다.

* 곱셈 및 거듭제곱 연산으로 사용할 때
* 리스트형 컨테이너 타입의 데이터를 반복 확장하고자 할 때
* 가변인자 (Variadic Arguments)를 사용하고자 할 때
* 컨테이너 타입의 데이터를 Unpacking 할 때

그럼 이제 각 경우에 대해서 **Asterisk(\*)**가 어떻게 쓰이는지 살펴보자.



# 1. 곱셈 및 거듭제곱 연산으로 사용할 때

이미 다들 알고 있는 곱셈 연산으로 사용할 수 있으며, 파이썬은 곱셈 뿐만 아니라 거듭제곱 연산까지 내장 기능으로 지원하고 있다.

```shell
>>> 2 * 3
6
>>> 2 ** 3
8
>>> 1.414 * 1.414
1.9993959999999997
>>> 1.414 ** 1.414
1.6320575353248798
```



# 2. 리스트형 컨테이너 타입의 데이터를 반복 확장하고자 할 때

파이썬에서는 **\***을 숫자형 데이터 뿐만 아니라 리스트형 컨테이너 타입에서 데이터를 반복적으로 확장하기 위해 사용할 수도 있다.

```python
# 길이 100의 제로값 리스트 초기화
zeros_list = [0] * 100

# 길이 100의 제로값 튜플 선언
zeros_tuple = (0,) * 100

# 리스트 3배 확장 후 연산
vector_list = [[1, 2, 3]]
for i, vector in enumerate(vector_list * 3):
    print("{0} scalar product of vector: {1}".format((i + 1), [(i + 1) * e for e in vector]))
# 1 scalar product of vector: [1, 2, 3]
# 2 scalar product of vector: [2, 4, 6]
# 3 scalar product of vector: [3, 6, 9]
```





# 3. 가변인자 (Variadic Parameters)를 사용하고자 할 때

우리는 종종 어떤 함수에서 가변인자를 필요로 할 때가 있다. 예를 들어, 들어오는 인자의 갯수를 모른다거나, 그 어떤 인자라도 모두 받아서 처리를 해야하는때가 있다.

파이썬에서는 인자의 종류가 2가지가 있는데 하나는 **positional arguments**이고, 하나는 **keyword arguments**이다. 전자는 말그대로 위치에 따라 정해지는 인자이며, 후자는 키워드를 가진 즉, 이름을 가진 인자를 말한다.

variadic positional/keyword arguments를 살펴보기 전에 간단하게 positional arguments과 keyword arguments에 대해 살펴보겠다.

```python
# 2~4명의 주자로 이루어진 달리기 대회 랭킹을 보여주는 함수
def save_ranking(first, second, third=None, fourth=None):
    rank = {}
    rank[1], rank[2] = first, second
    rank[3] = third if third is not None else 'Nobody'
    rank[4] = fourth if fourth is not None else 'Nobody'
    print(rank)

# positional arguments 2개 전달
save_ranking('ming', 'alice')
# positional arguments 2개와 keyword argument 1개 전달
save_ranking('alice', 'ming', third='mike')
# positional arguments 2개와 keyword arguments 2개 전달 (단, 하나는 positional argument 형태로 전달)
save_ranking('alice', 'ming', 'mike', fourth='jim')
```

위의 함수는 `first`, `second`라는 두 개의 positional arguments를 받으며 `third`, `fourth`라는 두 개의 keyword arguments를 받고 있다. positional arguments의 경우 생략이 불가능하며 갯수대로 정해진 위치에 인자를 전달해야한다. 그러나 keyword arguments의 경우 함수 선언시 디폴트값을 설정할 수 있으며, 만약 인자를 생략할 시 해당 디폴트값이 인자의 값으로 들어간다. 즉, 이 형태의 인자는 생략이 가능하다. 따라서, 여기서 알 수 있는건 keyword arguments의 경우 생략이 가능하기 때문에, positional arguments 이전에 선언될 수는 없다. 즉, 다음의 코드는 에러를 발생시킨다.

```python
def save_ranking(first, second=None, third, fourth=None):
    ...
```

그런데 세 번째를 보면 positional arguments가 3개, keyword argument가 1개 전달되고 있다. 눈치가 빠른 사람을 알겠지만, keyword arguments의 경우 선언된 위치만 동일할 경우 키워드를 제외하고 positional arguments 형태로 전달이 가능하다. 즉, 위에서 `mike`는 자동적으로 `third`라는 키로 전달이 된다.

여기까지가 파이썬의 arguments에 관한 기본적인 설명이다. 그런데, 여기서 한 가지 문제를 맞닥뜨릴 수 있다. 만약, 최대 4명의 주자가 아닌 10명 또는 그 이상의 정해지지 않은 주자가 있다고 해보자. 이 경우엔 10개의 인자를 선언하기도 번거로우며, 특히, 주자의 수가 미정일 경우 위와 같은 형태로는 처리가 불가능하다. 이 때 사용하는게 바로 **가변인자 (Variadic Arguments)**이다. 가변인자는 좀 전에 위에서 설명한 positional arguments와 keyword arguments에 모두 사용할 수 있으며, 사용 방법은 다음과 같다.

## positional arguments만 받을 때

```python
def save_ranking(*args):
    print(args)
save_ranking('ming', 'alice', 'tom', 'wilson', 'roy')
# ('ming', 'alice', 'tom', 'wilson', 'roy')
```

## keyword arguments만 받을 때

```python
def save_ranking(**kwargs):
    print(kwargs)
save_ranking(first='ming', second='alice', fourth='wilson', third='tom', fifth='roy')
# {'first': 'ming', 'second': 'alice', 'fourth': 'wilson', 'third': 'tom', 'fifth': 'roy'}
```

## positional arguments와 keyword arguments를 모두 받을 때

```python
def save_ranking(*args, **kwargs):
    print(args)
    print(kwargs)
save_ranking('ming', 'alice', 'tom', fourth='wilson', fifth='roy')
# ('ming', 'alice', 'tom')
# {'fourth': 'wilson', 'fifth': 'roy'}
```

위에서 `*args`는 임의의 갯수의 positional arguments를 받음을 의미하며, `**kwargs`는 임의의 갯수의 keyword arguments를 받음을 의미한다. 이 때 `*args`, `**kwargs` 형태로 가변인자를 받는걸 **packing**이라고 한다.

위의 예시에서 볼 수 있듯이, 임의의 갯수와 임의의 키값을 갖는 인자들을 전달하고 있다. positional 형태로 전달되는 인자들은 `args`라는 **tuple**에 저장되며, keyword 형태로 전달되는 인자들은 `kwargs`라는 **dict**에 저장된다.

아까 positional과 keyword의 선언 순서를 언급했었는데, keyword는 positional보다 앞에 선언할 수 없기 때문에 다음의 코드는 에러를 발생시킨다.

```python
def save_ranking(**kwargs, *args):
    ...
```

이 가변인자는 매우 일반적으로 사용되는 기능으로 많은 오픈소스 코드에서도 쉽게 찾아볼 수 있다. 보통 오픈소스의 경우 코드의 일관성을 위해 `*args`이나 `**kwargs`와 같이 관례적으로 사용되는 인자명을 사용하지만, `*required`나 `**optional`과 같이 인자명은 일반 변수와 같이 원하는대로 지정이 가능하다. (단, 만약 오픈소스 프로젝트를 하고 있고, 인자에 특별한 의미가 있지 않은 일반적인 가변인자라면 `*args`와 `**kwargs`와 같이 관례를 따르는게 좋다.)



# 4. 컨테이너 타입의 데이터를 Unpacking 할 때

**\***는 컨테이너 타입의 데이터를 unpacking 하는 경우에도 사용될 수 있다. 이는 3번과 유사한 원리로, 종종 사용할만한 기능(연산)이다. 가장 쉬운 예로, 다음과 같이 우리가 *list*나 *tuple* 또는 *dict* 형태의 데이터를 가지고 있고 어떤 함수가 가변인자를 받는 경우에 사용할 수 있다.

```python
from functools import reduce

primes = [2, 3, 5, 7, 11, 13]

def product(*numbers):
    p = reduce(lambda x, y: x * y, numbers)
    return p

product(*primes)
# 30030

product(primes)
# [2, 3, 5, 7, 11, 13]
```

`product()` 함수가 가변인자를 받고 있기 때문에 우리는 리스트의 데이터를 모두 unpacking하여 함수에 전달해야한다. 이 경우 함수에 값을 전달할 때 `*primes`와 같이 전달하면 `primes` 리스트의 모든 값들이 unpacking되어 `numbers`라는 리스트에 저장된다. 만약 이를 `primes` 그대로 전달한다면 이 자체가 하나의 값으로 쓰여 `numbers`에는 `primes`라는 원소가 하나 존재하게 된다.

*tuple*도 *list*와 정확히 동일하게 동작하며 *dict*의 경우 **\*** 대신 **\****을 사용하여 동일한 형태로 사용할 수 있다.

```python
headers = {
    'Accept': 'text/plain',
    'Content-Length': 348,
    'Host': 'http://mingrammer.com'
}

def pre_process(**headers):
    content_length = headers['Content-Length']
    print('content length: ', content_length)

    host = headers['Host']
    if 'https' not in host:
        raise ValueError('You must use SSL for http communication')

pre_process(**headers)
# content length:  348
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 7, in pre_process
# ValueError: You must use SSL for http communication
```

또 다른 형태의 unpacking이 한 가지 더 존재하는데, 이는 함수의 인자로써 사용하는게 아닌 리스트나 튜플 데이터를 다른 변수에 가변적으로 unpacking 하여 사용하는 형태이다.

```python
numbers = [1, 2, 3, 4, 5, 6]

# unpacking의 좌변은 리스트 또는 튜플의 형태를 가져야하므로 단일 unpacking의 경우 *a가 아닌 *a,를 사용
*a, = numbers
# a = [1, 2, 3, 4, 5, 6]

*a, b = numbers
# a = [1, 2, 3, 4, 5]
# b = 6

a, *b, = numbers
# a = 1
# b = [2, 3, 4, 5, 6]

a, *b, c = numbers
# a = 1
# b = [2, 3, 4, 5]
# c = 6
```

여기서 `*a`, `*b`로 받는 부분들은 우변의 리스트 또는 튜플이 unpacking된 후, 다른 변수들에 할당된 값 외의 나머지 값들을 다시 하나의 리스트로 packing한다. 이는 3번에서 살펴본 가변인자 packing과 동일한 개념이다.



# 결론

이상으로 크게 4가지의 **Asterisk(*)**를 활용한 연산들을 살펴보았다. 하나의 연산자로 여러가지 연산들을 할 수 있다는 점이 흥미로웠으며, 위의 대부분은 Pythonic한 코드를 짜기 위한 기본적인 내용들이다. 이 중 특히 3번의 경우 매우 자주 사용되는 중요한 기능이자 파이썬 초보자들이 자주 헷갈려하는 부분이기에 초보자라면 더더욱 잘 숙지하였으면 좋겠다.

다음에도 파이썬의 다른 흥미로운 내용들을 다뤄보겠다.

---

## File: content/post/2017-04-02-translation-13-simple-rules-for-good-coding.md

[Binary file]

---

## File: content/post/2017-04-24-translation-the-mathematics-of-machine-learning.md

---
categories:
- translation
- machine learning
comments: true
date: 2017-04-24T00:00:00Z
tags:
- mathematics
title: '[번역] 머신러닝 속 수학'
url: /translation-the-mathematics-of-machine-learning
---

> [The Mathematics of Machine Learning](https://medium.com/towards-data-science/the-mathematics-of-machine-learning-894f046c568)을 번역한 글입니다.

지난 몇 달 간, 나는 데이터 과학의 세계로 모험하고 머신러닝 기술을 사용하여 통계적 규칙성을 조사하고 완벽한 데이터 기반 제품을 개발하고 싶어하는 여러 사람들에게 연락을 받아왔다. 그러나, 나는 실제로 유용한 결과를 얻기 위해 필요한 수학적 직관력과 기초가 부족하다는 것을 관찰할 수 있었다. 이는 내가 이 포스트를 작성하게된 주된 이유이다. 최근들어 [scikit-learn](http://scikit-learn.org/), [Weka](http://www.cs.waikato.ac.nz/ml/weka/), [Tensorflow](https://www.tensorflow.org/), [R-caret](http://topepo.github.io/caret/index.html) 등과 같은 쉽게 사용할 수 있는 머신러닝/딥러닝 패키지들이 급증하고 있다. 머신러닝 이론은 반복적으로 데이터를 학습하고 지능형 애플리케이션을 구축하는 데 사용될 수있는 숨겨진 인사이트를 발견하기 위한 통계학, 확률론, 컴퓨터 과학 및 알고리즘이 교차하는 분야이다. 머신러닝 및 딥러닝의 엄청난 가능성에도 불구하고, 알고리즘의 내부 작동을 잘 파악하고 좋은 결과를 얻으려면 이러한 기술 중 많은 부분을 수학적으로 완전히 이해해야한다. 



# 왜 수학을 두려워 하는가?

머신러닝에서 수학이 중요한 이유는 다양하며 그 중 몇 가지는 다음과 같다:

1. 정확도, 트레이닝 시간, 모델 복잡도, 파라미터 수 및 피쳐 (features) 수에 대한 고려를 포함하는 올바른 알고리즘 선택
2. 파라미터 설정과 검증 (validation) 전략 선택
3. 편향 분산 (bias-variance)의 트레이드오프의 이해를 기반으로한 언더피팅 (underfitting)과 오버피팅 (overfitting)의 식별
4. 올바른 신뢰 구간과 불확실성 추정




# 어느 수준의 수학적 지식이 필요한가?

머신러닝과 같은 학제 간 분야를 이해하려고 할 때 가장 중요한 질문은 필요한 수학의 양과 이러한 기술을 이해하는 데 필요한 수학의 수준이다. 이 질문의 답은 다차원적이며 개개인의 수준과 관심도에 달렸다. 머신러닝의 수학적 공식화 및 이론적 진보에 대한 연구가 진행 중이며 일부 연구원들은 보다 진보된 기술을 연구하고 있다. 나는 머신러닝 과학자 및 엔지니어가 되기 위해 필요한 최소 수준의 수학과 각 개념의 중요성에 대한 나의 생각을 말하려고 한다.



1. **선형대수학 (Linear Algebra)**: 나의 동료중 한 명인 [Skyler Speakman](https://ke.linkedin.com/in/skyler-speakman-9a61415)은 최근에 "선형대수학은 21세기의 수학이다"라고 말한적이 있는데 나는 이 말에 전적으로 동의한다. 머신러닝에서 선형대수학은 모든 곳에서 나타난다. 

   * 주성분 분석 (Principal Component Analysis, PCA)
   * 단일값 분해 (Singular Value Decomposition, SVD)
   * 행렬의 고유분해 (Eigendecomposition of a matrix)
   * LU 분해 (LU Decomposition)
   * QR 분해 (QR Decomposition/Factorization)
   * 대칭 행렬 (Symmetric Matrices)
   * 고유값 & 고유벡터 (Eigenvalues & Eigenvectors)
   * 벡터 공간과 노름 (Vector Spaces and Norms)

     이상 위의 것들은 머신러닝에서 사용되는 최적화 방법들을 이해하기 위해 필요한 개념들이다.

2. **확률론 및 통계 (Probability Theory and Statistics)**: 머신러닝과 통계는 서로 다른 분야가 아니다. 실제로, 누군가는 최근에 머신러닝을 "Mac에서 통계하기"라고 정의했다. 머신러닝에 필요한 기초 통계학 및 확률론 개념들은 다음과 같다.

   * 조합 (Combinatorics)
   * 확률 규칙 및 공리 (Probability Rules & Axioms)
   * 베이지안 이론 (Bayes' Theorem)
   * 랜덤 변수 (Random Variables)
   * 분산과 기댓값 (Variance and Expectation)
   * 조건부 확률 및 결합 확률 분포 (Conditional and Joint Distributions)
   * 표준 분포 (Standard Distributions)
     * Bernouil
     * Binomial
     * Multinomial
     * Uniform
     * Gaussian
   * 모멘트 생성 함수 (Moment Generating Functions)
   * 최대 우도 추정 (Maximum Likelihood Estimation, MLE)
   * 사전 및 사후 확률 (Prior and Posterior)
   * 최대 사후 추정 (Maximum a Posteriori Estimation, MAP)
   * 샘플링 방식 (Sampling Methods)

3. **다변수 미적분학 (Multivariate Calculus)**: 필수 주제중 일부는 다음을 포함한다.

   * 미분 및 적분 (Differential and Integral Calculus)
   * 편미분 (Partial Derivatives)
   * 벡터값 함수 (Vector-Values Functions)
   * 방향 그라디언트 (Directional Gradient)
   * Hessian
   * Jacobian
   * Laplacian
   * Lagrangian 분포 (Lagrangian Distribution)

4. **알고리즘 및 복잡도 최적화 (Algorithms and Complex Optimizations)**: 이는 머신러닝 알고리즘의 계산 효율성과 확장성을 이해하고 데이터셋의 희소성을 활용하는데 있어 중요하다.

   * 데이터 구조에 대한 지식 (이진 트리, 해싱, 힙, 스택 등)
   * 동적 계획법 (Dynamic Programming)
   * 랜덤 및 선형 시간 이하 알고리즘 (Randomized & Sublinear Algorithm)
   * 그래프 (Graphs)
   * 그라디언트/확률론적 하강 (Gradient/Stochastic Descents)
   * 원 및 쌍대문제 해결 (Primal-Dual methods)

5. **기타**: 이는 위에서 설명한 4가지 주요 영역에서 다루지 않은 다른 수학 분야로 구성된다.

   * 복소해석학 (Real and Complex Analysis)
     * 집합과 수열  (Sets and Sequences)
     * 토폴로지 (Topology)
     * 거리 공간 (Metric Spaces)
     * 일가 함수 및 연속 함수 (Single-Valued and Continuous Functions)
     * 극한 (Limits)
     * 코시 커널 (Cauchy Kernel)
     * 푸리에 변환 (Fourier Transforms)
   * 정보 이론 (Information Theory)
     * 엔트로피 (Entropy)
     * 정보 이득 (Information Gain)
   * 함수 공간 (Function Spaces)
   * 다양체/매니폴드 (Manifolds)

다음은 머신러닝에 필요한 수학을 공부할 수 있는 온라인 MOOC와 자료들이다:

* 칸아카데미의 [선형대수학](https://www.khanacademy.org/math/linear-algebra), [확률과 통계](https://www.khanacademy.org/math/probability), [다변수 미적분학](https://www.khanacademy.org/math/calculus-home/multivariable-calculus), [최적화](https://www.khanacademy.org/math/differential-calculus/derivative-applications/calc-optimization/e/optimization)
* 브라운 대학교의 Philip Klein이 쓴 [Coding the Matrix: Linear Algebra through Computer Science Applications](http://codingthematrix.com/)
* 텍사스 대학교의 Robert van de Gejin이 쓴 [Linear Algebra — Foundations to Frontiers](https://www.edx.org/course/linear-algebra-foundations-frontiers-utaustinx-ut-5-04x)
* 데이비드슨 대학교의 Tim Chartier 강의: [Applications of Linear Algebra, Part 1](https://www.edx.org/course/applications-linear-algebra-part-1-davidsonx-d003x-1)과 [Part 2](https://www.edx.org/course/applications-linear-algebra-part-2-davidsonx-d003x-2)
* Joseph Blitzstein의 [Havard Stat 110 lectures](http://projects.iq.harvard.edu/stat110/youtube)
* Larry Wasserman의 [All of statistics: A Concise Course in Statistical Inference](http://read.pudn.com/downloads158/ebook/702714/Larry%20Wasserman_ALL%20OF%20Statistics.pdf)
* Boyd와 Vandenberghe의 강의: [Convex optimization from Standford](http://stanford.edu/~boyd/cvxbook/)
* 선형대수학 — [Foundations to Frontiers on edX](https://www.edx.org/course/linear-algebra-foundations-frontiers-utaustinx-ut-5-04x)
* Udacity의 [Introduction to Statistics](https://www.udacity.com/course/intro-to-statistics--st101)

마지막으로, 이 포스트의 주된 목적은 여러분에게 머신러닝에서의 수학의 중요성에 대한 선의의 조언과 필수 주제 및 이 주제들에 대한 유용한 자료들을 주기 위함이다. 그러나, 일부 머신러닝 애호가들은 수학에 초보자이며 이 포스트가 마음에 들지 않을 것이다. (진지하게, 이는 내 목적이 아니다) 초보자들은 머신러닝을 시작하는데 있어 많은 수학적 지식이 필요하진 않다. 기본적인 선행 조건은 이 포스트에서 설명한대로 데이터 분석이며 여러분들은 더 많은 기술과 알고리즘을 습득하면서 수학을 배울 수 있다.

---

## File: content/post/2017-04-26-tips-of-using-comma-in-python.md

---
categories:
- python
comments: true
date: 2017-04-26T00:00:00Z
tags:
- comma
- tip
title: 파이썬의 Comma(,) 사용팁
url: /tips-of-using-comma-in-python
---

파이썬에서 **Comma(,)**를 활용할 수 있는 몇가지 팁을 정리해보고자 한다. 아마 많은 사람들이 이미 다 알고있을 내용이기 때문에, 이 또한 파이썬 초보자들을 대상으로 한다.



파이썬에서 **Comma(,)**는 다음과 같은 상황에서 사용되는데 크게 4가지의 경우가 있다.

* 다중값을 사용할 때
* 단일 튜플을 사용할 때
* Trailing comma를 사용할 때
* (Python2) 한 줄 출력이 필요할 때



# 1. 다중값을 사용할 때 

일반적인 문법으로 파이썬 개발자라면 이미 모두가 사용하고 있는 케이스이다. 동시에 다중값 할당을 하거나, 값을 여러개 반환할 때 사용된다.

```python
# 동시 선언 및 초기화
a, b = 1, 2

# 값 교환 (Swap)
a, b = b, a

# 다중값 반환
def add_and_mul(a, b):
    add = a + b
    mul = a * b
    return add, mul
```



# 2. 단일 튜플을 사용할 때

파이썬엔 튜플(tuple)이라는 Immutable한 리스트형 타입이 있다. `list` 타입의 경우 `[1]`와 같이 하나의 원소를 가진 리스트를 선언하면 원소가 하나인 `list`타입의 값을 갖게 되지만, `tuple`의 경우에는 위와 동일하게 하나의 원소만 선언할 경우, 우리가 원하는 원소가 하나인 `tuple`타입의 값을 얻을 수 없다. 이 때, **Comma**를 사용하면 단일 튜플을 만들 수 있다.

```bash
>>> type((1))
<class 'int'>
>>> type((1,))
<class 'tuple'>
```

`(1)`과 같이 선언하면 튜플처럼 보이지만 이는 그냥 하나의 정수값을 나타내는 expression으로 처리되어 `int`형의 `1`의 값을 갖게된다. 따라서, 이 표현식을 `tuple` 타입의 값으로 사용하려면 `(1,)`와 같이 원소 뒤에 **Comma**를 붙여 실제 `tuple` 타입의 값을 갖게 해야한다.

여기서 한 가지 의문이 들 수도 있다. 원소가 하나인 튜플을 언제 사용하는가? 말 그대로 원소가 하나인 리스트를 사용하는것과 같이 원소가 하나인 튜플이 필요한 경우가 분명히 있을 것이다. 특히, 어떤 특정 함수가 튜플을 인자로 받는 경우에 튜플값을 하나만 전달하고자 할 때 단순히 `(1)`을 써서 타입 에러를 겪어본 적이 한번쯤은 있을 것이다. 이 때 바로 위와 같이 실제 튜플 타입의 데이터를 인자로 전달하면 해당 문제를 해결할 수 있다.

```python
# 타입 에러 발생
execute_query('...', (1))

# 정상적으로 처리
execute_query('...', (1,))
```



# 3. Trailing comma를 사용할 때

사실 위에서 본 팁이라고 하는것들은 일종의 파이썬 문법 자체이므로 팁이라고는 하기 어려우며, 팁이라고하면 이 부분이 팁이 될 수 있겠다. `Trailing comma`란 리스트형 데이터를 다음과 같은 형태로 선언할 때 맨 뒤에 붙이는 `Comma`를 뜻한다.

```python
scores = [
    100,
    93,
    82,
    97,
]
```

위에서 `Trailing comma`는 `97,`에서 보이는 Comma를 가리키는데, 얼핏보면 이걸 왜 쓰는지 의문이 들것이다. 결론부터 말하면, 목적은 실수 방지와 Git과 같은 VCS를 통한 소스코드 관리에서의 용이함이 있다.



## 실수 방지

이는 사람의 실수를 방지하기 위한 것이며, Trailing comma를 쓰는 주된 이유라기보단 부수적인 효과 및 목적이다.

```python
scores = [
    100,
    93,
    82,
    97
    76
]
```

`scores`에 새로운 값을 추가하는 경우, 리스트 최하단에 데이터를 추가하게 되는데 간혹 실수로 윗 라인에 Comma를 빼먹고 바로 쓰는 경우가 있다. 이 경우, Trailing comma를 사용하면 Comma를 빼먹는 실수를 방지할 수 있게된다.



## VCS를 통한 소스코드 관리에서의 용이함

Trailing comma를 쓰는 주된 목적이다. 우선 Trailing comma를 사용하지 않는 경우를 살펴보자.

```python
names = [
    'James',
    'Smith',
    'Ming',
    'John'
]
```

이제 위 데이터에 새로운 이름을 하나 추가한다고 해보자.

```python
names = [
    'James',
    'Smith',
    'Ming',
    'John',
    'Torvalds'
]
```

위와 같이 추가가 되었으며, 별 문제 없어보인다. 하지만, 이를 `git diff`로 살펴보면 다음과 같은 결과를 볼 수 있다.

```diff
@@ -2,5 +2,6 @@ names = [
     'James',
     'Smith',
     'Ming',
-    'John'
+    'John',
+    'Torvalds'
 ]
```

우리는 분명히 하나의 데이터만 추가를 했을뿐인데, 하나의 데이터가 삭제되고 두 개의 데이터가 추가된 것처럼 나온다. 코드상으로는 문제가 없지만, 이는 실제 커밋 히스토리 변경사항을 파악하는데에는 별로 좋지 않다. Trailing comma를 사용하면 위와 같은 문제를 해결할 수 있다.

```python
names = [
    'James',
    'Smith',
    'Ming',
    'John',
]
```

처음부터 위와 같이 선언을 하고 추후에 데이터를 추가하는 경우 실제로 변경되는 코드도 한 줄로 표시되기 때문에 변경사항을 파악하기가 훨씬 수월해진다.

```diff
@@ -3,4 +3,5 @@ mes = [
     'Smith',
     'Ming',
     'John',
+    'Torvalds',
 ]
```

우리의 의도가 `diff`에도 그대로 반영 되었으며, 훨씬 보기가 수월해졌다. 이는 데이터 추가 뿐만 아니라 삭제를 하는 경우에도 동일하다.



# 4. 한 줄 출력이 필요할 때

이는 Python2에서만 유효한 팁으로 데이터들을 한 줄로 출력하고 싶은 경우 사용할 수 있다.

```python
#  print를 그대로 사용하는 경우
for i in range(5):
    print i
"""
0
1
2
3
4
"""

# print에 comma를 붙여 사용하는 경우
for i in range(5):
    print i,
"""
0 1 2 3 4
"""
```



이상 파이썬에서 **Comma(,)**를 사용하는 케이스와 팁을 살펴보았다. 특히, 3번의 `Trailing comma`는 자주 사용되는 팁이니 숙지해두면 도음이 많이 될 것이다.

---

## File: content/post/2017-05-07-translation-the-truth-is-in-the-code.md

[Binary file]

---

## File: content/post/2017-06-14-introduce-comprehension-of-python.md

---
categories:
- python
comments: true
date: 2017-06-14T00:00:00Z
tags:
- comprehension
title: 파이썬의 Comprehension 소개
url: /introduce-comprehension-of-python
---

**Comprehension**이란 iterable한 오브젝트를 생성하기 위한 방법중 하나로 파이썬에서 사용할 수 있는 유용한 기능중 하나이다.

파이썬에는 다음과 같은 크게 네 가지 종류의 Comprehension이 있다.

* List Comprehension (LC)
* Set Comprehension (SC)
* Dict Comprehension (DC)
* Generator Expression (GE)

Generator의 경우 comprehension과 형태는 동일하지만 특별히 **expression**이라고 부른다.

그럼 이제 각각의 경우에 대해 간단히 살펴보자. (iterable과 generator에 대해서는 [이 포스팅](https://mingrammer.com/translation-iterators-vs-generators)을 참고하길 바란다.)



# List Comprehension (LC)

**List comprehension**은 리스트를 쉽게 생성하기 위한 방법이다. 이는 파이썬에서 보편적으로 사용되는 기능으로 조금만 응용하면 다양한 조건으로 리스트를 생성할 수 있는 강력한 기능중 하나이다.

우선 간단한 예제를 하나 살펴보자.

```python
# 20까지의 짝수를 출력하기 위해 다음과 같은 LC를 사용할 수 있다
evens = [x * 2 for x in range(11)]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# 리스트의 모든 원소값을 정규화 시킨 후 상수값을 더하는 LC
vals = [32, 12, 96, 42, 32, 93, 31, 23, 65, 43, 76]
amount = sum(vals)
norm_and_move = [(x / amount) + 1 for x in vals]
# [1.0587155963302752, 1.0220183486238532, 1.1761467889908257, 1.0770642201834861, 1.0587155963302752, 1.1706422018348623, 1.0568807339449542, 1.0422018348623854, 1.1192660550458715, 1.0788990825688074, 1.1394495412844037]
```

조금 더 복잡한 예제를 살펴보자. (조건을 지닌 LC와 Nested LC)

```python
# 100 이하의 제곱수가 아닌 수를 찾는 LC
from math import sqrt
non_squars = [x for x in range(101) if sqrt(x)**2 != x]
# [2, 3, 5, 6, 7, 8, 10, 12, 13, 15, 18, 19, 20, 23, 24, 26, 28, 29, 31, 32, 37, 38, 40, 43, 45, 48, 50, 51, 52, 58, 59, 60, 61, 63, 65, 66, 72, 73, 75, 76, 77, 78, 80, 82, 87, 89, 92, 94, 95, 96, 97]

# 두 리스트의 원소들의 모든 조합을 찾는 LC
epithets = ['sweet', 'annoying', 'cool', 'grey-eyed']
names = ['john', 'alice', 'james']
epithet_names = [(e, n) for e in epithets for n in names]
# [('sweet', 'john'), ('sweet', 'alice'), ('sweet', 'james'), ('annoying', 'john'), ('annoying', 'alice'), ('annoying', 'james'), ('cool', 'john'), ('cool', 'alice'), ('cool', 'james'), ('grey-eyed', 'john'), ('grey-eyed', 'alice'), ('grey-eyed', 'james')]
```

조금 더 Practical한 예제를 살펴보자.

```python
# a^2 + b^2 = c^2 (a < b < c)를 만족하는 피타고라스 방정식의 해를 찾는 LC
solutions = [(x, y, z) for x in range(1, 30) for y in range(x, 30) for z in range(y, 30) if x**2 + y**2 == z**2]
# [(3, 4, 5), (5, 12, 13), (6, 8, 10), (7, 24, 25), (8, 15, 17), (9, 12, 15), (10, 24, 26), (12, 16, 20), (15, 20, 25), (20, 21, 29)]

# 단어에서 모음을 제거하는 LC
word = 'mathematics'
without_vowels = ''.join([c for c in word if c not in ['a', 'e', 'i', 'o', 'u']])
# 'mthmtcs'

# 행렬을 일차원화 시키는 LC
matrix = [
  [1, 2, 3, 4],
  [5, 6, 7, 8],
  [9, 10, 11, 12],
]
flatten = [e for r in matrix for e in r]
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
```



# Set Comprehension (SC)

**Set comprehension**은 LC와 정확히 동일하며 단지 list가 아닌 set을 생성한다는 것만 다르다.

```python
# 다음의 LC는 중복된 값들을 포함한다
no_primes = [j for i in range(2, 9) for j in range(i * 2, 50, i)]
# [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 10, 15, 20, 25, 30, 35, 40, 45, 12, 18, 24, 30, 36, 42, 48, 14, 21, 28, 35, 42, 49, 16, 24, 32, 40, 48]

# SC를 사용하면 중복값이 없는 집합을 얻을 수 있다
no_primes = {j for i in range(2, 9) for j in range(i * 2, 50, i)}
# {4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 22, 24, 25, 26, 27, 28, 30, 32, 33, 34, 35, 36, 38, 39, 40, 42, 44, 45, 46, 48, 49}
```



# Dict Comprehension (DC)

**Dict comprehension** 또한 LC와 동일하며 dict를 생성한다.

기본 예제.

```python
# 두 리스트를 하나의 dict로 합치는 DC. 하나는 key, 또 다른 하나는 value로 사용한다
subjects = ['math', 'history', 'english', 'computer engineering']
scores = [90, 80, 95, 100]
score_dict = {key: value for key, value in zip(subjects, scores)}
# {'math': 90, 'history': 80, 'english': 95, 'computer engineering': 100}

# 튜플 리스트를 dict 형태로 변환하는 DC
score_tuples = [('math', 90), ('history', 80), ('english', 95), ('computer engineering', 100)]
score_dict = {t[0]: t[1] for t in score_tuples}
# {'math': 90, 'history': 80, 'english': 95, 'computer engineering': 100}
```



# Generator Expression (GE)

**Generator expression**은 특별한 형태의 comprehension이다. 이는 한 번에 모든 원소를 반환하지 않고 한 번에 하나의 원소만 반환하는 **generator**를 생성한다.

GE 또한 다른 Comprehension과 동일한 형태로 쉽게 사용할 수 있다.

```python
# 다음 Generator는 제곱수를 만들어낸다
gen = (x**2 for x in range(10))
print(gen)
# <generator object <genexpr> at 0x105bde5c8>
print(next(gen)) # call 1
# 0
print(next(gen)) # call 2
# 1
# 'next' 함수 호출을 10번 반복
print(next(gen)) # call 10
# 81
print(next(gen)) # call 11
"""
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
"""

# Yes, it is an just generator. You can sum the yielding values.
# GE로 생성한 Generator도 yield를 가진 함수로 생성한 것과 동일한 Generator이기 때문에, 똑같이 sum을 사용할 수 있다. (iterable 객체)
gen = (x**2 for x in range(10))
sum_of_squares = sum(gen)
# 285
```



이상으로 Comprehension과 Expression에 대해 간단히 살펴보았다. 이는 매우 강력한 기능중 하나로 잘만 활용하면 높은 생산성을 낼 수 있다.

---

## File: content/post/2017-07-14-setup-the-python-development-environment-with-pycharm-and-docker.md

[Binary file]

---

## File: content/post/2017-08-28-translation-python-practices-for-efficient-code-performance-memory-and-usability.md

[Empty file]

---

## File: content/post/2017-09-10-translation-10-common-software-architectural-patterns-in-a-nutshell.md

[Binary file]

---

## File: content/post/2017-10-10-translation-cpython-internals-arbitrary-precision-integer-implementation.md

[Binary file]

---

## File: content/post/2017-12-11-cgo-cross-compile-for-mac-for-linux.md

[Binary file]

---

## File: content/post/2018-01-19-translation-go-best-practices.md

[Binary file]

---

## File: content/post/2018-02-01-getting-started-with-oauth2-in-go.md

---
categories:
- go
comments: true
date: 2018-02-01T00:00:00Z
tags:
- oauth2
- tutorial
title: Go에서 OAuth2 인증하기
url: /getting-started-with-oauth2-in-go
---

[OAuth2](https://oauth.net/2/)란, 일종의 인증 프로토콜로써 소셜 인증을 통한 로그인 및 권한 제어를 위해 사용된다. Google, Twitter, Github 등 대부분의 소셜 인증 기능을 지닌 프로바이더들은 표준 인증 방식으로 OAuth2를 채택하고 있으며 그에 따른 OAuth2 API들을 제공하고 있다.

이번 포스팅에서는 Go에서 OAuth2 인증을 처리하는 방법을 다루고자 한다.

Go는 다양한 기능의 내장 라이브러리와 공식 서드파티 라이브러리의 지원이 꽤 잘되어있는 편인데 OAuth2 또한 Go 공식 서드파티 라이브러리가 이미 존재한다. 따라서 별도의 비공식 서드파티 라이브러리를 사용할 필요가 없으며 사실상 이미 준비는 다 되어있다고 보면된다.

이 포스팅에서는 대표적인 OAuth2 프로바이더인 Google을 기준으로 설명을 진행할 것이다. 이 외의 프로바이더에 대해서도 OAuth2 동작방식은 동일하니 사용하려는 프로바이더가 다를 경우엔 일부 설정 및 인증 정보만 바꿔주면 된다. (이 부분은 **인증 정보 설정**을 참고)

우선 코드를 살펴보기 전에 OAuth2의 인증 플로우부터 살펴보도록 하겠다.



# OAuth2 플로우

OAuth2의 추상적인 플로우를 살펴보면 다음과 같다.

 

*출처: [Digital Ocean](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2)*

1. 유저가 로그인 페이지에 접속을 한다.
2. 로그인 페이지 접속시 유저를 식별하기 위해 생성한 랜덤한 `state`값을 사용해 구글 로그인 링크를 생성한다.
3. 유저는 반환된 구글 로그인 링크를 클릭해 소셜 로그인을 진행한다.
4. 소셜 로그인 후에 구글 인증 서버는 토큰 발급을 위한 임시 `code` 값과 이전에 전송했던 `state` 값을 미리 등록했던 콜백 URL에 붙여 리다이렉트 한다.
5. 콜백 URL로 호출되는 인증 처리 핸들러에서는 `state` 값이 이전값과 같은지 확인한 뒤, 받은 `code` 값을 사용해 실제 리소스 사용 권한이 담긴 **엑세스 토큰** 을 발급 받기 위해 구글 인증 서버로 요청을 보낸다.
6. 인증 서버로부터 **엑세스 토큰**을 받으면 필요한 리소스를 요청할 수 있게 된다.



# 준비사항

코드를 작성하기 전에 두 가지 준비해야할 사항이 있다. 그 중 하나는 인증 처리를 위한 의존 라이브러리 설치이며 나머지 하나는 사용할 프로바이더의 OAuth2 API를 사용하기 위한 키값 발급 및 콜백 URL 지정이다.

## 의존성 설치

```bash
go get golang.org/x/oauth2

# (선택사항) 세션 관리는 원하는 형태로 해도 되며, 여기선 gorilla의 sessions를 사용하여 세션을 관리
go get github.com/gorilla/sessions
```

## 키값 발급

OAuth2 API를 사용하기 위해선 API를 제공하는 프로바이더에 애플리케이션을 등록한 뒤 OAuth2 API 사용 권한 인증을 위한 키값들을 발급받아야한다. 구글의 경우 [Google API Console](https://console.developers.google.com)에서 애플리케이션을 등록할 수 있다. 등록 후, **사용자 인증 정보** 페이지에 접속하면 **사용자 인증 정보 만들기**의 **OAuth 클라이언트 ID** 메뉴를 통해 클라이언트 ID와 시크릿 키값을 발급 받을 수 있다.

이렇게 발급받은 클라이언트 ID와 시크릿 키값은 **인증 정보 설정**에서 사용할 것이다.

## 콜백 URL 지정

사용자가 프로바이더 인증 서버에 권한을 요청하면 인증 서버는 미리 등록된 콜백 URL을 통해 토큰 발급시 사용될 임시 코드값을 클라이언트에 전달하게 된다. (위 그림으로 보면 **3**번에서 이 과정이 이루어진다) 따라서 우리는 이 때 사용할 콜백 URL을 좀 전에 만든 애플리케이션에 미리 등록해야한다. 키값을 발급했던 페이지에 있는 **승인된 리디렉션 URI** 칸에 콜백 URL을 지정할 수 있다. 여기에서는 `http://127.0.0.1:1333/auth/callback`로 지정하도록 하자.

준비는 다 끝났다. 그럼 이제 OAuth2 인증을 실제로 구현해보도록 하자. 



# 예제용 애플리케이션 구성

이 포스팅의 목적은 하나의 완전한 애플리케이션의 구현이 아닌 OAuth2 인증에 초점이 맞춰져 있기 때문에 아주 단순한 구조의 예제용 애플리케이션을 구성해보도록 하겠다.

예제로 사용할 애플리케이션 구조는 다음과 같다.

```
├─ auth.go
├─ auth.html
├─ main.go
└─ main.html
```

* **auth.go** : 인증 관련 정보 설정
* **auth.html** : 인증 시작 페이지
* **main.go** : 핸들러 관리
* **main.html** : 메인 페이지

구조는 매우 단순하며 상세한 인증 플로우는 잠시 후 살펴보도록 하겠다. **main.html**과 **auth.html**는 단순히 인증 링크 접속을 위한 페이지라 아주 단순하다.

```html
<!-- main.html -->
<html>
<head></head>
<body>
    <a href="/auth">Sign In</a>
</body>
</html>
```

```html
<!-- auth.html -->
<html>
<head></head>
<body>
    <!-- href는 로그인 핸들러로부터 반환 받은 Google 로그인 링크가 담긴다 -->
    <a href="{{ . }}">Google Login</a> 
</body>
</html>
```

**main.go**는 페이지 렌더링과 인증을 처리하는 핸들러를 관리한다.

```go
func main() {
    http.HandleFunc("/", RenderMainView)
    http.HandleFunc("/auth", RenderAuthView)
    http.HandleFunc("/auth/callback", Authenticate)

    log.Fatal(http.ListenAndServe(":1333", nil))
}

// 메인 뷰 핸들러
func RenderMainView(w http.ResponseWriter, r *http.Request) {
}

// 랜덤 state 값을 가진 구글 로그인 링크를 렌더링 해주는 뷰 핸들러
// 랜덤 state는 유저를 식별하는 용도로 사용된다
func RenderAuthView(w http.ResponseWriter, r *http.Request) {
}

// Google OAuth 인증 콜백 핸들러
func Authenticate(w http.ResponseWriter, r *http.Request) {
}
```



# 인증 정보 설정

`auth.go`는 위에서 살펴봤던 OAuth2 인증을 위한 설정값과 인증에 필요한 데이터들을 독립적으로 관리하기위한 용도이다.

```go
// auth.go
const (
    CallBackURL = "http://localhost:1333/auth/callback"
  
    // 인증 후 유저 정보를 가져오기 위한 API
    UserInfoAPIEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo"
  
    // 인증 권한 범위. 여기에서는 프로필 정보 권한만 사용
    ScopeEmail          = "https://www.googleapis.com/auth/userinfo.email"
    ScopeProfile        = "https://www.googleapis.com/auth/userinfo.profile"
)
```

`CallBackURL`은 인증 서버가 권한 요청을 받은 후 리다이렉트할 URL이며, 프로바이더에 등록한 애플리케이션에서 지정한 콜백 URL과 일치해야한다. 

아래 나머지 URL들은 인증 후 발급된 토큰으로 리소스를 요청할 때 사용하는 URL들이다. (여기선, 인증 후 유저 정보를 가져오기 위함)

```go
// auth.go
var OAuthConf *oauth2.Config

func init() {
    OAuthConf = &oauth2.Config{
        ClientID:     "google client id",
        ClientSecret: "google client secret",
        RedirectURL:  CallBackURL,
        Scopes:       []string{ScopeEmail, ScopeProfile},
        Endpoint:     google.Endpoint,
    }
}

// state 값과 함께 Google 로그인 링크 생성
func GetLoginURL(state string) string {
    return OAuthConf.AuthCodeURL(state)
}

// 랜덤 state 생성기
func RandToken() string {
    b := make([]byte, 32)
    rand.Read(b)
    return base64.StdEncoding.EncodeToString(b)
}
```

**oauth2.Config**는 인증 처리를 위한 설정값들을 관리하는 구조체이다. 클라이언트 ID, 시크릿 키값 그리고 콜백 URL등을  지정할 수 있다. 위에서 발급받은 키값들로 설정하면된다. 더 좋은 방법은 키값을 코드에 바로 넣지 않고 환경 변수로 설정해 `os.GetEnv()`로 가져오는 방법이 있다. 실제 프로덕션에서 사용한다면 이 방법을 추천한다.

인증 처리에 필요한 설정값들을 모두 가지고 있어, 사실상 OAuth2 인증 처리를 위한 URL 생성, 토큰 교환과 같은 대부분의 기능들이 이 구조체의 메서드들로 이루어진다.

**GetLoginURL**은 `state` 값을 사용하여 생성한 구글 로그인 링크를 반환한다. 이 때, 이 로그인 링크에는 `OAuthConf`에서 설정한 `RedirectURL`이 따라 붙는다.



# 인증 처리

인증에 필요한 정보들을 다 설정했으니 이제 실제 인증 처리 과정을 살펴보자.

우선 Google 인증을 하기 전에 로그인 페이지에 접속하는 과정부터 살펴보자.

```go
// main.go
func RenderAuthView(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session")
    session.Options = &sessions.Options{
        Path:   "/auth",
        MaxAge: 300,
     }
    state := RandToken()
    session.Values["state"] = state
    session.Save(r, w)
    RenderTemplate(w, "auth.html", GetLoginURL(state))
}
```

로그인 페이지에 접속하는 순간 `state` 값을 생성해 세션에 저장한 후, 이를 사용해 생성한 구글 로그인 링크를 반환한다. 세션에 저장한 `state` 값은 추후 콜백 인증 핸들러에서 `state` 값을 비교하기 위해 사용된다.

이후 유저는 **auth.html**에 렌더링된 구글 로그인 링크를 통해 구글 로그인을 시도할 것이다. 구글 로그인을 시도하면 위에서 생성된 `state` 값 및 `RedirectURL `과 함께 인증 서버에 권한 요청을 하게 된다. 인증 서버는 요청을 받고 `code` 값을 생성한 뒤 이 값을 `RedirectURL`에 붙여 리다이렉트를 한다.

`RedirectURL`인 `http://127.0.0.1:1333/auth/callback`로 리다이렉트가 되면 `Authenticate` 핸들러가 호출되고 토큰 인증 작업이 시작된다.

```go
// main.go
func Authenticate(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session")
    state := session.Values["state"]

    delete(session.Values, "state")
    session.Save(r, w)

    if state != r.FormValue("state") {
        http.Error(w, "Invalid session state", http.StatusUnauthorized)
        return
    }
  
    ...
}
```

콜백 핸들러가 호출되면 제일 먼저 `state` 값이 유효한지 체크한다. 세션에 저장되어 있는 `state` 값과 비교를 진행하며 한 번 사용된 `state`는 세션에서 삭제한다.

```go
// main.go
func Authenticate(w http.ResponseWriter, r *http.Request) {
    ...
  
    token, err := OAuthConf.Exchange(oauth2.NoContext, r.FormValue("code"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
  
    ...
}
```

`state` 값이 유효하면 이제 전달받은 `code`를 사용해 인증 서버에 **엑세스 토큰**을 요청한다. `code` 값이 유효하다면 토큰을 정상적으로 받을 수 있다.

```go
// main.go
func Authenticate(w http.ResponseWriter, r *http.Request) {
    ...
  
    client := OAuthConf.Client(oauth2.NoContext, token)
    // UserInfoAPIEndpoint는 유저 정보 API URL을 담고 있음
    userInfoResp, err := client.Get(UserInfoAPIEndpoint)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    defer userInfoResp.Body.Close()
    userInfo, err := ioutil.ReadAll(userInfoResp.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    var authUser User
    json.Unmarshal(userInfo, &authUser)
  
    ...
}
```

토큰을 받은 클라이언트는 이제 이 토큰을 가지고 유저 정보 리소스를 요청할 수 있다. 이 때, 요청 가능 데이터에 대한 범위는 아까 **인증 정보 설정**에서 살펴본 `OAuthConf`의 `Scopes`에서 지정한 권한 범위와 일치한다.

요청 데이터가 권한 범위 안에 속한다면 요청 데이터를 정상적으로 받아올 것이다.

```go
// main.go
func Authenticate(w http.ResponseWriter, r *http.Request) {
    ...

    session.Options = &sessions.Options{
        Path:   "/",
        MaxAge: 86400,
    }
    session.Values["user"] = authUser.Email
    session.Values["username"] = authUser.Name
    session.Save(r, w)

    http.Redirect(w, r, "/", http.StatusFound)
}
```

마지막으로 필수는 아니지만 받아온 데이터를 로그인 유지등의 목적으로 계속 사용하고자 한다면 세션에 넣어 관리할 수도 있다.



# 전체 코드

예제 테스트를 해볼 수 있도록 전체 코드를 공개하겠다. **auth.html**과 **main.html** 코드는 위에서 볼 수 있다.

## auth.go

```go
package main

import (
    "crypto/rand"
    "encoding/base64"

    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

const (
    CallBackURL = "http://localhost:1333/auth/callback"
  
    UserInfoAPIEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo"
    ScopeEmail          = "https://www.googleapis.com/auth/userinfo.email"
    ScopeProfile        = "https://www.googleapis.com/auth/userinfo.profile"
)

var OAuthConf *oauth2.Config

func init() {
    OAuthConf = &oauth2.Config{
        ClientID:     "google client id",
        ClientSecret: "google client secret",
        RedirectURL:  CallBackURL,
        Scopes:       []string{ScopeEmail, ScopeProfile},
        Endpoint:     google.Endpoint,
    }
}

func GetLoginURL(state string) string {
    return OAuthConf.AuthCodeURL(state)
}

func RandToken() string {
    b := make([]byte, 32)
    rand.Read(b)
    return base64.StdEncoding.EncodeToString(b)
}
```

## main.go

```go
package main

import (
    "encoding/json"
    "html/template"
    "io/ioutil"
    "log"
    "net/http"

    "github.com/gorilla/sessions"
    "golang.org/x/oauth2"
)

var store = sessions.NewCookieStore([]byte("secret"))

func main() {
    http.HandleFunc("/", RenderMainView)
    http.HandleFunc("/auth", RenderAuthView)
    http.HandleFunc("/auth/callback", Authenticate)

    log.Fatal(http.ListenAndServe(":1333", nil))
}

func RenderTemplate(w http.ResponseWriter, name string, data interface{}) {
    tmpl, _ := template.ParseFiles(name)
    tmpl.Execute(w, data)
}

func RenderMainView(w http.ResponseWriter, r *http.Request) {
    RenderTemplate(w, "main.html", nil)
}

func RenderAuthView(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session")
    session.Options = &sessions.Options{
        Path:   "/auth",
        MaxAge: 300,
	}
    state := RandToken()
    session.Values["state"] = state
    session.Save(r, w)
    RenderTemplate(w, "auth.html", GetLoginURL(state))
}

func Authenticate(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session")
    state := session.Values["state"]

    delete(session.Values, "state")
    session.Save(r, w)

    if state != r.FormValue("state") {
        http.Error(w, "Invalid session state", http.StatusUnauthorized)
        return
    }

    token, err := OAuthConf.Exchange(oauth2.NoContext, r.FormValue("code"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    client := OAuthConf.Client(oauth2.NoContext, token)
    userInfoResp, err := client.Get(UserInfoAPIEndpoint)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    defer userInfoResp.Body.Close()
    userInfo, err := ioutil.ReadAll(userInfoResp.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    var authUser User
    json.Unmarshal(userInfo, &authUser)

    session.Options = &sessions.Options{
        Path:   "/",
        MaxAge: 86400,
    }
    session.Values["user"] = authUser.Email
    session.Values["username"] = authUser.Name
    session.Save(r, w)

    http.Redirect(w, r, "/", http.StatusFound)
}
```



# 마무리

이제까지 아주 보편적으로 널리 사용되고 있는 OAuth2 인증을 Go로 구현하는 방법을 살펴보았다. OAuth2에 친숙하지 않은 사람이라면 다소 복잡해 보일 수 있지만 OAuth2 플로우만 잘 이해한다면 코드 또한 쉽게 이해할 수 있을 것이다.

예제 코드 수준이라 코드 정리가 덜 되었지만 실제 프로덕션에서는 위 코드중 인증 부분만 잘 떼어내 인증 패키지로 묶어 모듈화 시키는게 좋을 것 같다.

기회가 된다면 다양한 프로바이더의 OAuth2를 인증 체계로 사용하는 하나의 완전한 웹 애플리케이션 튜토리얼을 만들어 보는것도 재밌을 것 같다.

---

## File: content/post/2018-02-18-translation-avoiding-memory-leak-in-golang-api.md

---
categories:
- translation
comments: true
date: 2018-02-18T00:00:00Z
tags:
- memory leak
- go
title: '[번역] Go API에서 메모리 누수 예방하기'
url: /translation-avoiding-memory-leak-in-golang-api
---

> [Avoiding Memory Leak in Golang API](https://hackernoon.com/avoiding-memory-leak-in-golang-api-1843ef45fca8)를 번역한 글입니다.

*여러분은 Go API를 프로덕션 환경에 배포하기 전에 먼저 이 글을 읽어야합니다. 이 글은 Kurio에서의 실제 경험에 기반한 것이며, 우리가 올바른 방식으로 개발하고 있지 않아서 모든 배포를 위해 얼마나 고군분투 했었는지에 대한 경험담입니다.*

몇 주 전, 우리는 [Kurio](https://kurio.co/article-feed)의 메인 서비스에서 발생한 이상하고 감지도 되지 않는 버그들을 고치고 있었다. 우리는 다양한 방법으로 디버깅과 버그 수정을 시도했었다. 서비스는 몇 주 동안 프로덕션 환경에서 동작해왔기 때문에 비즈니스 로직에서 발생한 버그는 아니었다. 그러나 오토 스케일링으로 서비스를 항상 살렸기 때문에 이는 잘 동작하는 것처럼 보였다.

나중에 우리팀은 이 문제가 코드의 문제임을 발견했고 우리가 제대로 하고 있지 않았음을 알아냈다.



# 아키텍처

참고로, 우리는 아키텍처에 마이크로서비스 패턴을 사용하고있다. 우리는 유저(모바일 및 웹)에게 API를 제공하는 `메인 API`라고 하는 하나의 게이트웨이 API를 가지고있는데, 이는 API 게이트웨이와 유사한 역할을 하기 때문에 단순히 유저로부터 받은 요청을 처리한 다음 요청에 맞는 필요한 서비스를 호출하고 유저에게 응답을 반환한다. 이 `메인 API`는 100% Go로 작성되었다. Go를 선택한 이유는 다른 얘기이므로 여기서 그 이유를 설명하진 않겠다.

우리의 시스템을 그림으로 그려보면 다음과 같다. 





# 문제점

우리는 한동안 항상 다운되고 모바일앱에 긴 응답 시간을 보내며 가끔은 접속 장애까지 일으키는 메인 API로 인해 많은 고생을 했다. API 대시보드 모니터는 어느새 빨간색으로 바뀌었다. 솔직히 말하면 API 대시보드 모니터가 빨간색으로 바뀐다는건 위험한 일이었고 이는 엔지니어에게 스트레스와 패닉을 안겨주었다.

또 다른 문제는 CPU와 메모리 사용량이 점점 더 높아지고 있었다는 것이다. 이런일이 발생하면 우리는 서비스를 수동으로 다시 시작하고 재실행될 때까지 기다렸다.



단일 요청에 대한 API 응답 시간이 86초까지 치솟음



수동으로 재시작한 후의 API 응답 시간 그래프

이 버그는 버그에 대한 그 어떠한 특별한 정보도 로깅하고 있지 않았기 때문에 매우 난처했다. 응답시간이 매우 길어지고 CPU와 메모리 사용량이 증가했다. 정말 악몽 같았다.



# 1단계 : http.Client 커스터마이징

서비스를 개발하면서 배운것중 하나는 디폴트 설정은 신뢰할 수 없다는 것이다.

우리는 http 패키지의 디폴트 설정을 사용하는 대신 직접 커스터마이징한 `http.Client`를 사용했다.

```go
client := http.Client{} // default
```

우리는 몇가지 필요한 설정을 추가했다. 커넥션 재사용을 위해 전송 (Transport) 정보를 설정하고 재사용 가능한 커넥션에 대한 최대 유휴 (Idle) 시간을 조절했다.

```go
keepAliveTimeout:= 600 * time.Second
timeout:= 2 * time.Second
defaultTransport := &http.Transport{
    Dial: (&net.Dialer{
               KeepAlive: keepAliveTimeout,
          }).Dial,
    MaxIdleConns: 100,
    MaxIdleConnsPerHost: 100,
}
client:= &http.Client{
    Transport: defaultTransport,
    Timeout:   timeout,
}
```

이 설정을 통해 다른 서비스를 호출하는데 소요되는 최대 시간을 줄일 수 있었다.



# 2단계 : 닫히지 않은 응답 바디에서의 메모리 누수 방지

이 단계에서 우리가 배운 것은 커넥션 풀을 다른 서비스에서 재사용하기 위해선 반드시 응답 바디를 모두 읽고 종료 해야한다는 것이다.

`메인 API`는 단지 다른 서비스들을 호출하고 있었기 때문에, 우리는 치명적인 실수를 저질렀다. `메인 API`는 `http.Client`에서 사용가능한 커넥션을 재사용한다고 가정하고 있기 때문에 어떤 일이 생기면 설령 필요없다고 하더라도 반드시 응답 바디를 읽어야한다. 또한 반드시 응답 바디를 닫아야한다. 이 두 가지는 우리 서버에서 메모리 누수를 방지하는데 도움이 되었다.

우리는 코드에서 응답 바디를 닫는다는걸 잊고 있었다. 그리고 이는 후에 우리의 프로덕션에 큰 재앙을 일으켰다.

해결책은 응답 바디는 반드시 닫고, 반환되는 데이터가 필요없을지라도 응답 바디를 읽는 것이었다.

```go
req, err := http.NewRequest("GET", "http://example.com?q=one", nil)
if err != nil {
    return err
}
resp, err := client.Do(req)
//=================================================
// 응답 바디를 닫음
//=================================================
if resp != nil {
    defer resp.Body.Close() // 반드시 닫아야함
}
if err != nil {
    return err
}
//=================================================
// HTTP 커넥션을 재사용할때 메모리 누수를 피하기 위해선 데이터가 필요없더라도 응답 바디를 읽어야함
//=================================================
_, err = io.Copy(ioutil.Discard, resp.Body) // 응답 바디를 읽음. (데이터가 필요 없으므로 devnull에 write)
if err != nil { 
     return err
}
```

우리는 [이 훌륭한 글](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/)을 읽고 위 문제를 해결했다.

1단계와 2단계 및 오토 스케일링의 도움으로 이 버그를 줄일 수 있었으며, 이는 작년 (2017) 이후 3개월간은 다시 발생하지 않았다.



# 3단계 : Go 채널의 타임아웃 제어

잘 동작하다가 몇 개월 후, 버그가 재발하였다. 2018년 1월 첫 주, `메인 API`가 호출하는 서비스중 하나인  `콘텐츠 서비스`가 다운되었는데, 어떤 이유로 접속 불능 상태가 되었다.

따라서 `콘텐츠 서비스`가 다운되면서 `메인 API`는 다시 중단되었다. API 대시보드는 다시 빨간색으로 바뀌고, API 응답 시간이 길어지면서 느려졌다. 오토 스케일링을 사용중임에도 불구하고 CPU와 메모리 사용량이 매우 높아졌다.

또 다시 우리는 문제의 근원을 찾기 시작했다. `콘텐츠 서비스`를 재실행하니 다시 잘 동작하였다.

우리는 왜 이런일이 발생했는지 의아해했다. 생각해보니, 우리는 `http.Client`에 타임아웃 기한을 설정했기 때문에 이런 일은 절대 발생하지 않을 것이다.

그런 다음, 코드에서 잠재적인 문제를 찾다가 몇가지 위험한 코드를 발견했다.

빠른 이해를 위해 바로 코드를 살펴보자.

> 아래 함수는 예시 함수이긴하나, 우리가 마주한 패턴과 유사한 코드이다.

```go
// 고치기전 코드
type sampleChannel struct{
    Data *Sample
    Err error
}

func (u *usecase) GetSample(id int64, someparam string, anotherParam string) ([]*Sample, error) {
    chanSample := make(chan sampleChannel, 3)
    wg := sync.WaitGroup{}

    wg.Add(1)
    go func() {
        defer wg.Done()
        chanSample <- u.getDataFromGoogle(id, anotherParam) // 함수의 예시
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()
        chanSample <- u.getDataFromFacebook(id, anotherParam)
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()
        chanSample <- u.getDataFromTwitter(id,anotherParam)
    }()

    wg.Wait()
    close(chanSample)

    result := make([]*Sample, 0)

    for sampleItem := range chanSample {
        if sampleItem.Error != nil {
            logrus.Error(sampleItem.Err)
        }
        if sampleItem.Data == nil { 
            continue
        }
        result = append(result, sampleItem.Data)
    }
  
    return result
}
```

위 코드를 보면 아무 문제가 없어보인다. 그러나 이 함수는 우리의 `메인 API`에서 가장 많이 접근하는 함수이며, 가장 무거운 호출이다. 왜냐하면 이 함수는 큰 작업을 처리하는 3개의 API를 호출하기 때문이다.

이를 개선하기 위해선 채널에서 타임아웃을 사용하는 새로운 접근법을 도입해야한다. 위와 같은 형태의 코드에서는 `WaitGroup`이 모든 프로세스가 끝날 때까지 대기하고 있기 때문에 모든 응답을 처리하고 반환하기 위해서는 모든 API 호출이 끝날때까지 기다려야한다.

이는 우리가 저지른 큰 실수중 하나이다. 이 코드는 하나의 서비스가 죽었을 때 큰 문제를 일으킬 수 있다. 죽은 서비스가 복구될때까지 계속 기다릴 것이기 때문이다. 물론 초당 5K개의 호출이 들어오면 이는 재앙이 된다.

## 첫 시도 해결책

우리는 타임아웃을 추가하여 코드를 수정했다. 따라서 유저는 오랫동안 기다리지 않아도 되며 대신 `Internal Server Error`를 받게될 것이다.

```go
func (u *usecase) GetSample(id int64, someparam string, anotherParam string) ([]*Sample, error) {
    chanSample := make(chan sampleChannel, 3)
    defer close(chanSample)

    go func() {
        chanSample <- u.getDataFromGoogle(id, anotherParam) // 함수의 예시
    }()

    go func() {
        chanSample <- u.getDataFromFacebook(id, anotherParam)
    }()
   
    go func() {
        chanSample <- u.getDataFromTwitter(id,anotherParam)
    }()
 
    result := make([]*feed.Feed, 0)
    timeout := time.After(time.Second * 2)
  
    for loop := 0; loop < 3; loop++ {
        select {
        case sampleItem := <-chanSample:
            if sampleItem.Err != nil {
                logrus.Error(sampleItem.Err)
                continue
            }
            if feedItem.Data == nil {
                continue
            }
            result = append(result,sampleItem.Data)
        case <-timeout:
            err := fmt.Errorf("Timeout to get sample id: %d. ", id)
            result = make([]*sample, 0)
            return result, err
        }
    }

    return result, nil;
}
```



# 4단계 : 컨텍스트를 활용한 타임아웃 제어

3단계까지 시도 했음에도, 우리의 문제는 여전히 완벽하게 해결되지는 않았다. 우리의 `메인 API`는 여전히 높은 CPU와 메모리를 점유하고 있었다.

이는 유저에게 이미 `Internal Server Error`를 반환하고 있음에도 불구하고 고루틴이 여전히 살아있었기 때문이다. 우리가 원하던건 응답이 반환되면 백그라운드에서 돌아가고있는 고루틴 및 API 호출을 포함한 모든 리소스가 예외없이 지워지는 것이다.

나중에 [이 글](http://dahernan.github.io/2015/02/04/context-and-cancellation-of-goroutines/)을 읽은 뒤, 우리는 Go에서 알지 못했던 흥미로운 특징을 발견했다. 바로  `컨텍스트 (Context)`를 활용한 고루틴의 캔슬레이션 (Cancellation)이다.

타임아웃을 위해 `time.After`를 사용하는 대신 `context.Context`를 사용하였다. 이 새로운 접근법으로 우리의 서비스는 보다 더 안정적이게 되었다.

그 다음 관련 함수에 컨텍스트를 추가하여 코드 구조를 다시 변경하였다.

```go
// 컨텍스트를 사용한 최종 수정 코드
func (u *usecase) GetSample(c context.Context, id int64, someparam string, anotherParam string) ([]*Sample, error) {
    if c== nil {
        c= context.Background()
    }
  
    ctx, cancel := context.WithTimeout(c, time.Second * 2)
    defer cancel()
  
    chanSample := make(chan sampleChannel, 3)
    defer close(chanSample)
    
    go func() {
        chanSample <- u.getDataFromGoogle(ctx, id, anotherParam) // 함수의 예시
    }()

    go func() {
        chanSample <- u.getDataFromFacebook(ctx, id, anotherParam)
    }()

    go func() {
        chanSample <- u.getDataFromTwitter(ctx, id,anotherParam)
    }()

    result := make([]*feed.Feed, 0)
  
    for loop := 0; loop < 3; loop++ {
        select {
            case sampleItem := <-chanSample:
                if sampleItem.Err != nil {
                    continue
                }
                if feedItem.Data == nil {
                    continue
                }
                result = append(result,sampleItem.Data)
                // ============================================================
                // 일관성 없는 데이터를 방지하기 위해 컨텍스트가 타임아웃을 초과하는 경우
                // 유저에게 빈 배열과 에러 메시지를 보낸다
                // ============================================================
            case <-ctx.Done(): // 컨텍스트가 타임아웃을 초과했다는 알림 시그널을 받음
                err := fmt.Errorf("Timeout to get sample id: %d. ", id)
                result = make([]*sample, 0)
                return result, err
        }
    }

    return result, nil;
}
```

우리는 코드에서 모든 고루틴 호출에 `컨텍스트`를 사용한다. 이는 메모리를 해제하고 고루틴 호출을 취소하는데 도움이 된다.

추가적으로, 보다 더 통제 가능하고 신뢰성 있는 서비스를 위해 HTTP 요청에 컨텍스트를 전달한다.

```go
func ( u *usecase) getDataFromFacebook(ctx context.Context, id int64, param string) sampleChanel{
    req,err := http.NewRequest("GET", "https://facebook.com", nil)
    if err != nil {
        return sampleChannel{
            Err: err,
        }
    }
    // ============================================================
    // 요청에 컨텍스트를 전달한다
    // 이 기능은 Go 1.7부터 사용할 수 있다
    // ============================================================
    if ctx != nil {
        req = req.WithContext(ctx) // HTTP 호출 요청에 컨텍스트를 사용하고있다.
    }
  
    resp, err:= u.httpClient.Do(req)
    if err != nil {
        return sampleChannel{
            Err: err,
        }
    }
  
    body,err:= ioutils.ReadAll(resp.Body)
    if err!= nil {
        return sampleChannel{
            Err:err,
        }
    }
    
    sample:= new(Sample)
    err:= json.Unmarshall(body,&sample)
    if err != nil {
        return sampleChannle{
            Err:err,
        }
    }
    
    return sampleChannel{
        Err:nil,
        Data:sample,
    }
}

```

이 모든 설정과 타임아웃 제어로 인해, 우리의 시스템은 보다 더 안전하고 통제 가능한 시스템이 되었다.

 

# 배운점

* 프로덕션 환경에서는 절대 디폴트 옵션을 사용하지 말자.
  디폴트 옵션을 사용하지 않으며 큰 프로젝트를 만들고 있다면 절대 디폴트 옵션을 사용하지 말라.
* 많이 읽고, 많이 시도해보고, 많이 실패해보며, 많은 얻을 것.
  우리는 이 경험으로부터 많은걸 배울 수 있었고, 이 경험은 오직 실제 사례와 실제 사용자가 있는 환경에서만 얻을 수 있다. 나는 이 버그를 수정했던 팀의 일원으로서 아주 기쁘다.

---

## File: content/post/2018-03-06-how-to-implement-the-scroll-and-paging-in-python-curses.md

---
categories:
- python
comments: true
date: 2018-03-06T00:00:00Z
tags:
- curses
- paging
title: 파이썬 curses에서 스크롤 및 페이징 기능 구현하기
url: /how-to-implement-the-scroll-and-paging-in-python-curses
---

TUI 애플리케이션을 개발하다보면 마주치는 어렵지는 않지만 다소 까다로운 문제가 하나 있는데, 바로 TUI 환경에서 화면을 동적으로 제어하는 것이다. 특히 한정된 화면에서 **리스트**를 다루게 된다면 **스크롤(Scroll)** 기능은 필수이며 리스트가 아주 길어질 수 있는 경우라면 **페이징(Paging)** 기능 또한 거의 필수적이게 된다. 이 포스팅에서는 파이썬의 **curses**라는 라이브러리에서 이 두 기능을 구현하는 방법에 대해서 다룰 것이다.

그럼 바로 본론으로 들어가서, **curses**에서 스크롤과 페이징을 구현하는 방법에 대해서 알아보겠다. 참고로 이 포스팅은 curses 튜토리얼은 아니며, 따라서 curses를 이미 사용해봤던 사람들한테 더욱 유용할 것이다.

이 포스팅에서는 전체 소스코드가 아닌 스크롤 및 페이징의 구현부 소스코드만 살펴볼 것이며, 전체 소스코드 및 실행 방법은 [Python Curses Scroll Example](https://github.com/mingrammer/python-curses-scroll-example)을 참고하면 된다. (전체 소스코드 : [tui.py](https://github.com/mingrammer/python-curses-scroll-example/blob/master/tui.py))



# 스크롤 (Scroll)

스크롤 구현의 아이디어는 간단하다. 전체 리스트에서의 현재 보여지는 윈도우의 최상단 위치와 현재 커서 위치를 기준으로 다음 커서의 위치를 계산하여 커서를 이동시키는 것이다. 이해하기 쉽게 실제 구현부 코드를 살펴보자.

## 구현

다음은 TUI 애플리케이션 실행 후 사용자로부터 키보드 입력을 받는 부분인데 `KEY_UP`과 `KEY_DOWN` 입력을 받게 되면 스크롤을 수행하는 `scroll` 메서드를 실행하게된다. (참고로, `UP=1`, `DOWN=-1`)

```python
def input_stream(self):
    """사용자 입력을 대기하며 입력값에 따라 해당되는 메서드를 실행함"""
    while True:
        self.display()

        ch = self.window.getch()
        if ch == curses.KEY_UP:
            self.scroll(self.UP)
        elif ch == curses.KEY_DOWN:
            self.scroll(self.DOWN)
        elif ch == curses.KEY_LEFT:
            self.paging(self.UP)
        elif ch == curses.KEY_RIGHT:
            self.paging(self.DOWN)
        elif ch == curses.ascii.ESC:
            break
```

스크롤을 구현할 때에는 다음 두 가지를 고려해야한다.

* 현재 커서가 현재 윈도우의 최상단 혹은 최하단에 위치하면서 커서와 윈도우가 모두 움직이는 경우
* 현재 커서가 현재 윈도우의 중간에 위치해 커서만 움직이는 경우

```python
# top: 리스트에서의 현재 윈도우의 최상단 라인의 위치
# current: 현재 보여지는 윈도우 기준 현재 커서 위치
# max_lines: 한 번에 볼 수 있는 최대 항목의 갯수
# bottom: 커서가 위치할 수 있는 최하단 라인의 위치
#
# ┌--------------------------------------┐
# |1. Item                               |
# |--------------------------------------| <- top = 1
# |2. Item                               |
# |3. Item                               |
# |4./Item///////////////////////////////| <- current = 3
# |5. Item                               |
# |6. Item                               |
# |7. Item                               |
# |8. Item                               | <- max_lines = 7
# |--------------------------------------|
# |9. Item                               |
# |10. Item                              | <- bottom = 10
# |                                      |
# |                                      | <- page = 1 (0 and 1)
# └--------------------------------------┘


def scroll(self, direction):
    # 방향에 따른 다음 라인 커서 위치 계산
    next_line = self.current + direction

    # 윈도우 스크롤 업
    # 현재 커서가 윈도우의 상단에 위치하나, 윈도우의 상단 라인이 최상단에 닿지 않았으므로 윈도우 스크롤 업이 가능하다
    if (direction == self.UP) and (self.top > 0 and self.current == 0):
        self.top += direction
        return

    # 윈도우 스크롤 다운
    # 다음 커서가 현재 윈도우의 하단에 위치하나, 커서의 절대 위치가 아직 최하단까지 도달하진 않았으므로 윈도우 스크롤 다운이 가능하다
    if (direction == self.DOWN) and (next_line == self.max_lines) and (self.top + self.max_lines < self.bottom):
        self.top += direction
        return

    # 스크롤 업
    # 현재 커서가 최상단보다 아래에 있으므로 스크롤 업이 가능하다
    if (direction == self.UP) and (self.top > 0 or self.current > 0):
        self.current = next_line
        return

    # 스크롤 다운
    # 다음 커서가 현재 윈도우의 하단보다 위에 있으며, 커서의 절대 위치가 아직 최하단까지 도달하진 않았으므로 스크롤 다운이 가능하다
    if (direction == self.DOWN) and (next_line < self.max_lines) and (self.top + next_line < self.bottom):
        self.current = next_line
```

## 스크롤 데모

* 스크롤 업 : **↑**
* 스크롤 다운 : **↓**

[](https://asciinema.org/a/166994)



# 페이징 (Paging)

스크롤은 커서 위치를 조정하면서 동작하는 반면, 페이징은 윈도우의 상단 라인 (`top` 변수)의 위치를 조정하면서 동작한다. 그렇기 때문에 페이징을 구현할 때에는 한가지 유의 해야할 부분이 있는데, 페이징을 하다가 마지막 페이지에 도달했을 때 현재 커서가 마지막 페이지에 나타나는 항목의 리스트보다 아래에 위치하는 경우, 이 커서 위치를 재조정해줘야 한다는 것이다.

## 구현

마찬가지로 키보드 입력을 받는 부분에서 `KEY_LEFT`와 `KEY_RIGHT` 입력을 받게 되면 페이징을 수행하는 `paging` 메서드를 실행하게된다.

 ```python
def paging(self, direction):
    # 윈도우의 상단 위치값과 현재 커서 위치로 현재 페이지와 다음 페이지를 계산
    current_page = (self.top + self.current) // self.max_lines
    next_page = current_page + direction

    # 마지막 페이지에 도달 했을 때 현재 커서가 마지막 페이지에 나타나는 항목의 리스트보다 아래에 있는 경우,
    # 현재 커서를 마지막 페이지 리스트의 마지막 항목 위치로 조정
    if next_page == self.page:
        self.current = min(self.current, self.bottom % self.max_lines - 1)

    # 페이지 업
    # 현재 페이지가 첫 페이지가 아닌 경우, 페이지 업이 가능하다
    # 윈도우 상단의 위치는 음수가 될 수 없으므로, 음수가 될 경우 0으로 조정
    if (direction == self.UP) and (current_page > 0):
        self.top = max(0, self.top - self.max_lines)
        return

    # 페이지 다운
    # 현재 페이지가 마지막 페이지가 아닌 경우, 페이지 다운이 가능하다
    if (direction == self.DOWN) and (current_page < self.page):
        self.top += self.max_lines
        return
 ```

## 페이징 데모

* 페이지 업 : **←**
* 페이지 다운 : **→**

[](https://asciinema.org/a/166995)



# 마무리

이제까지 **curses**를 사용해 TUI 환경에서 스크롤과 페이징을 구현하는 기법에 대해서 살펴보았다. 비단 curses가 아니더라도 다른 언어 및 라이브러리를 활용해 TUI 스크롤/페이징을 구현하고자 한다면 기본적인 아이디어는 그대로 차용할 수 있을 것이다. 다음엔 TUI 환경에서 나타날 수 있는 다른 다양한 동작들의 구현에 대해서 포스팅을 해보겠다.

---

## File: content/post/2018-04-07-translation-the-hidden-components-of-web-caching.md

---
categories:
- translation
comments: true
date: 2018-04-07T00:00:00Z
tags:
- caching
- system
title: '[번역] 웹 캐싱의 숨겨진 요소들'
url: /translation-the-hidden-components-of-web-caching
---

> [The hidden components of Web Caching](https://medium.freecodecamp.org/the-hidden-components-of-web-caching-970854fe2c49)을 번역한 글입니다.

캐싱(Caching)은 애플리케이션의 처리 속도를 높여준다. 이미 가져온 데이터나 계산된 결과값의 복사본을 저장함으로써 처리 속도를 향상시키며, 이를 통해 향후 요청을 더 빠르게 처리할 수 있다. 대부분의 프로그램이 동일한 데이터나 명령어에 반복해서 엑세스하기 때문에 캐싱은 효율적인 아키텍처 패턴이다.

캐싱은 웹 브라우저부터 웹서버와 하드디스크 및 CPU에 이르기까지 모든 곳에 적용된다. 그럼 이제 하위 레이어부터 상위 레이어까지의 순서대로 다양한 캐싱 레이어에 대해서 알아보자. 이 포스팅은 캐싱의 동작 **방식**보단 데이터가 캐싱되는 **위치**에 초점을 두고있다.



# CPU 캐시

캐시 메모리는 RAM과 CPU 사이의 버퍼 역할을 하는 매우 빠른 메모리 타입이다. 이는 필요시 CPU가 즉시 사용할 수 있도록 자주 요청되는 데이터와 명령어들을 저장한다.

CPU는 적은 양의 고속 스토리지로 구성된 **레지스터(Register)**라고 하는 특수한 온칩 메모리(On-chip memory)를 내장하고있다. 레지스터는 가장 가깝고, 가장 작으며  가장 빠른 메모리이다. 때때로 이 레지스터를 **L0 캐시**라고 부르기도 한다.

또한 CPU는 **L1 캐시** (레벨 1)에서 **L4 캐시** (레벨 4)까지 최대 4개의 캐시 레벨에 엑세스할 수 있다. CPU 및 마더보드의 아키텍처는 레지스터가 L0 캐시인지 L1 캐시인지를 결정하며, 또한 여러 캐시 레이어가 CPU와 마더보드중 어디에 위치하는지도 결정한다.



캐시 메모리 구조



# 디스크

HDD는 RAM에 비해 느리지만, SSD로 인해 점점 빨라지고 있다.

컴퓨터 스토리지에서, 디스크 버퍼 또는 캐시 버퍼라고도 불리는 디스크 캐시는 CPU와 물리적인 하드 디스크 사이의 버퍼 역할을 하는 HDD에 내장된 메모리이다.



디스크 캐시는 디스크에 내용을 읽거나 쓸 때, 그 데이터를 곧 다시 읽게된다는 것을 전제로 한다.



# 램 (RAM)

임시 데이터를 RAM과 HDD에 저장하는 것의 차이점은 성능, 비용 및 CPU 근접성이다.

램은 응답 시간이 수십 나노초인 반면에 HDD는 수십 밀리초의 응답 시간을 갖는다. 무려 여섯 자릿수나 차이 난다!



캐싱이 어떻게 동작하는지 더 깊이 이해하면 매우 효율적이고 비용이 저렴하며 유지 보수가 쉬운 애플리케이션을 설계할 수 있다.



# 간단한 웹서버

웹에 요청을 날리면, 요청은 웹 브라우저로부터 하드 디스크의 파일 시스템에 있는 정적 리소스를 제공하는 웹 서버로 전달된다.



웹 사이트 요청시 정적 파일을 제공한다고 가정해보자. 일반적인 워크플로우에서 인입 요청은 웹 서버에 의해 처리된다. 웹 서버는 하드 디스크로부터 파일을 가져와 파일의 내용을 반환하며 응답한다.

첫 번째 요청에서, 하드 디스크는 캐시를 확인하여 "캐시 미스(Cache miss)"를 발생시킨다. 그리고 하드 드라이브로부터 데이터를 가져와 추후에 다시 요청 받을 수 있음을 가정하고 캐시에 저장한다.

이후 요청부터는 캐시 조회시 "캐시 히트(Cache hit)"를 발생시킨다. 이 데이터는 덮어씌워져서 캐시 미스를 일으키기 전까지 버퍼에서 제공된다.



# 데이터베이스 캐싱

데이터베이스 쿼리는 데이터베이스 서버에서 수행되기 때문에 속도가 느려지고 부하가 몰릴 수도 있다. 이러한 쿼리들이 반복되면 결과값을 데이터베이스에 캐싱함으로써 응답 시간을 향상 시킬 수 있다. 이는 다수의 머신이 동일한 데이터베이스에 동일한 쿼리를 사용하는 경우에 더욱 유용하다.



대다수의 데이터베이스 서버는 최적화된 캐싱을 위한 기능을 기본적으로 지원하나, 여러분의 애플리케이션의 요구사항에 맞게 수정할 수 있는 많은 파라미터들이 존재한다.



# 응답 캐싱

웹 서버는 응답을 캐싱하도록 구성할 수 있어 유사한 요청이 애플리케이션 호스트로 전달되지 않도록 할 수 있다. 이와 유사하게, 애플리케이션 호스트는 비용이 높은 데이터베이스 쿼리나 자주 요청되는 파일들에 대한 응답의 일부를 캐시할 수 있다.



웹 서버의 응답은 메모리에 캐싱된다. 애플리케이션 캐시는 로컬 인메모리에 저장되거나 캐시 서버 위에서 실행되는 레디스와 같은 인메모리 데이터베이스에 저장할 수 있다.

> *웹 서버와 애플리케이션 호스트는 애플리케이션 아키텍처에 따라 동일한 서비스 또는 서로 다른 서비스에 존재할 수 있다*



# 함수 메모이제이션 (Memoization)

메모이제이션은 비용이 높은 함수 호출을 최적화하여 특정 입력에 대해 해당 함수를 한 번만 실행되도록하는 캐싱의 한 형태이다. 이는 함수의 입력 파라미터에 해당하는 키와 결과값에 해당하는 값을 매칭시켜둔 조회 테이블을 통해 구현할 수 있다.



메모이제이션은 프로그램의 성능을 높이는데 사용되는 일반적인 기술이다. 그러나 이는 드물게 요청되는 함수나 빠른 응답 속도를 가진 함수에 대해서는 적합하지 않을 수도 있다.



# HTTP 헤더를 통한 브라우저 캐싱

모든 브라우저는 HTML 페이지, 자바스크립트 파일 및 이미지와 같은 웹 문서의 임시 저장을 위해 HTTP 캐시 (웹 캐시)의 구현을 제공하고있다.

이는 서버 응답이 올바른 HTTP 헤더 지시자를 제공하여 브라우저가 응답을 캐싱할 수 있는 시기와 지속 기간을 지시할 때 사용된다.

이는 아주 강력한 기능이며 다음과 같은 이점을 지녔다:

* 리소스가 로컬 캐시로부터 빠르게 로드되기 때문에 사용자 경험이 향상된다. 요청이 네트워크를 통해 전송되지 않기 때문에 왕복 시간(RTT, Round Trip Time)이 발생하지 않는다
* 애플리케이션 서버 및 파이프라인의 다른 구성 요소에 대한 부하가 줄어든다
* 불필요한 대역폭에 대한 지불 비용이 줄어든다





# 프록시 서버

컴퓨터 네트워크에서 프록시 서버는 컴퓨터 시스템, 하드웨어 어플라이언스 또는 애플리케이션 형태로 나타난다. 이는 다른 서버의 리소스를 찾는 클라이언트의 요청에 대한 중개 역할을 하며 그 반대의 경우도 마찬가지이다.

다양한 형태의 프록시 서버가 존재한다. 사용자의 로컬 컴퓨터, 네트워크 라우터 또는 클라이언트와 대상 호스트 사이의 다양한 중개 서버에 상주할 수 있다. 모든 프록시 서버는 캐싱이 가능하다.

일반적인 타입의 프록시 서버들을 살펴보자.

## 게이트 웨이

나가는 요청 또는 들어오는 응답을 변형없이 단순히 포워딩해주는 프록시 서버로 **게이트웨이**, **터널링 프록시**, **웹 프록시**, **프록시** 또는 **애플리케이션 수준 프록시**라고도 한다. 이러한 프록시는 일반적으로 방화벽 내부의 모든 클라이언트가 공유하므로 요청 캐싱에 적합한 후보가 된다.

## 포워드 프록시

포워드 프록시 (프록시 서버)는 일반적으로 클라이언트측 인프라에 설치된다. 포워드 프록시를 사용하도록 설정된 웹 브라우저는 나가는 요청을 프록시로 전달한다. 다음은 전달된 요청을 인터넷을 통해 대상 서버로 포워딩한다. 포워드 프록시의 장점중 하나는 클라이언트의 식별 아이디를 숨긴다는 것이다. (그러나 VPN이 익명성에 더 안전하다)

## 웹 가속기

웹 가속기는 웹 사이트의 엑세스 시간을 줄여주는 프록시 서버이다. 이는 가까운 미래에 엑세스할 가능성이 있는 문서를 미리 페치한다. 이는 또한 문서 압축, 암호화 속도 향상, 이미지 품질을 낮추는 작업등을 수행할 수 있다.

## 리버스 프록시

리버스 프록시는 일반적으로 사설 네트워크의 서버에 직접 엑세스하지 못하도록하는 내부 프록시이다. 이는 내부 서버들간의 요청을 로드 밸런싱하거나 SSL 인증을 제공하거나 요청을 캐싱하는데 사용된다. 서버측 호스트는 캐시를 하고 많은 수의 요청을 관리하도록 도울 수 있다.

## 엣지 캐싱

한편, 엣지 캐싱 (콘텐츠 전송 네트워크 (CDN))은 콘텐츠를 최종 사용자에게 더 가까이 저장하기 위해 캐싱 서버를 사용하는 것을 의미한다. 예를 들어, 인기있는 웹사이트를 방문하여 캐싱될 정적 콘텐츠를 다운로드하는 경우가 있다. 각 후속 사용자들은 캐시가 만료될 때까지 캐싱 서버로부터 직접 콘텐츠를 제공받는다. 



원본 서버는 콘텐츠의 실제 원본이며 CDN에서 사용할 수있는 모든 콘텐츠를 제공할 수 있다.



# 마무리

캐싱은 하드웨어와 소프트웨어부터 네트워킹 장비와 서비스에까지 이르는 파이프라인의 모든 단계에서 발생한다. 이는 본래 서버의 전체적인 성능 향상에 있어 중요한 역할을 한다. 모든 머신은 HDD와 CPU에서 유사한 캐싱 메커니즘을 가지고 있다.

캐시는 레이턴시와 네트워크 트래픽을 줄여 리소스를 표시하는데 필요한 시간을 단축시킨다. 모든 웹 애플리케이션은 CPU 연산과 관련된 응답 지연이 있다. 예를 들어 디스크 조회, 네트워크 레이턴시, 요청 큐잉, 네트워크 쓰로틀링등이 있다. 파이프라인에 있는 다양한 시스템에 걸쳐 이러한 여러 조합을 감안하면 왕복 시간은 빠르게 늘어날 것이다.

마지막으로, 다음은 캐싱으로부터 얻을 수 있는 몇 가지 이점들이다:

* 레이턴시가 단축되어 응답 시간이 단축된다.
* 웹 트래픽이 줄어들어 왕복 시간(RTT)이 향상된다.
* 더 높은 처리량으로 본래 서버가 더 많은 요청을 처리 할 수 있다.
* 대역폭 소모가 줄어들어 네트워크 트래픽과 네트워크 정체가 줄어든다. 즉, 캐시되지 않은 실제 콘텐츠는 훨씬 빠르게 검색됨을 의미한다.
* 가까운 프록시 캐시에서 문서를 검색하면 전송 지연이 최소화된다.
* 인터넷상의 다양한 캐시가 데이터를 처리함으로써 본래 웹 서버의 작업 부하가 줄어든다.
* 크래시나 네트워크 문제로 인해 원격 서버를 사용할 수 없는 경우, 프록시를 통해 리소스의 캐시된 사본을 가져올 수 있다.

---

## File: content/post/2018-04-29-writing-unit-test-for-time-dependent-code.md

---
categories:
- testing
comments: true
date: 2018-04-29T00:00:00Z
tags:
- unittest
- time
- random
title: 시간에 의존하는 코드를 위한 테스트 작성하기
url: /writing-unit-test-for-time-dependent-code
---

그동안 귀찮고 번거롭다는 이유로 테스트 코드 작성을 매번 미루고 있다가 최근 들어 테스트 코드 작성에 습관을 들이고자 조금씩 테스트 코드 작성 비중을 높이고 있다. 며칠 전에도 필요한 커맨드라인 툴을 만들면서 테스트 코드를 작성하고 있었는데 한 가지 문제에 봉착했다.

보통 유닛 테스트를 하게 되면 어떤 특정한 조건하에서 혹은 특정한 정적인 데이터셋을 가지고 우리가 작성한 함수나 메서드를 테스트하곤한다. 그러나 내가 테스트하려던 코드는 정적인 데이터가 아닌 시시각각 변하는 즉, 시간에 의존하는 데이터를 가지고 있었기에, 어떤 특정한 테스트 케이스들을 미리 정해놓고 테스트하기가 어려운 상황이었다.

그리하여 이 포스팅에서는 이처럼 시간에 의존하는 데이터들을 다루는 코드를 테스트하는 방법에 대해서 소개하려고한다. 물론, 여기서 소개하는 내용들은 이미 잘 알려진 방법들이며 특별한 내용은 없으나 혹여나 여러분이 필자처럼 테스트 코드 작성에 조금 서툴다면 유익할 수도 있다.

> 참고로 이 포스팅에서는 Go 언어로 작성된 코드를 사용한다. 아이디어는 동일하므로 각자 언어에 맞게 적용하면 된다.



# 랜덤 시드 고정

예측할 수 없는 데이터의 대표적인 예는 바로 랜덤 데이터이다. 컴퓨터에서의 랜덤은 보통 시드(Seed)라는 초기값에 따라 일정한 규칙으로 값을 생성해내는 방식으로 동작한다. 따라서, 시드가 고정되어 있으면 랜덤은 동일한 값을 생성해내는데, 그렇기 때문에 보통은 유닉스 타임스탬프를 랜덤의 시드값으로 두어 시간에 따라 값이 변하게 만들어 실제 랜덤처럼 보이도록 코드를 작성하곤한다. 이 때문에 일반적인 경우, 랜덤 데이터는 시간에 의존적이라고 해도 무방하다.

그럼 이제 테스트 코드를 작성하기 전에 테스트를 위한 함수 하나를 작성해보자.

```go
// main.go
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func RandFruit() string {
    set := []string{"apple", "melon", "banana", "mango"}
    index := rand.Intn(4) // 0 ~ 3 사이의 임의의 정수값 생성
    return set[index]
}

func main() {
    rand.Seed(time.Now().UnixNano())
    fmt.Println(RandFruit())
}
```

위 코드는 문자열 리스트에서 임의의 원소를 추출하는 코드이다. 이 코드를 실행하게 되면 `RandFruit` 함수는 무작위로 한 원소를 뽑아 반환한다. 그렇다면 이런 함수에 대한 테스트 코드는 어떻게 작성할 수 있을까?

사실 답은 이미 위에 적혀있다. 컴퓨터에서의 랜덤은 시드값을 기준으로 생성되기 때문에 단순히 시드값을 고정 시켜주면 된다. 즉, 테스트 함수 바디 안에서 시드값을 고정시키면 테스트하려는 함수는 동일한 값만을 반환하게 되어 Example 테스트가 가능해진다. (Go에서는 `// Output:`이라는 특별한 형태의 주석으로 Example 테스트를 지원한다)

> Example 테스트는 Go가 아니더라도 단순히 출력이 예상 Output과 같은지 검사하는 테스트이기 때문에 타 언어에서도 충분히 작성할 수 있는 형태이다.

```go
// main_test.go
package main

import (
    "fmt"
    "math/rand"
)

func ExampleRandFruit() {
    rand.Seed(11)
    fmt.Print(RandFruit())
    // Output: apple
}
```



# 몽키 패치를 통한 시간 고정

어느 정도 예측은 가능하나 정확한 예측이 어렵고 제어하기 어려운 데이터중 하나가 시간 데이터이다. 예측이 어느 정도 가능하다고는 했으나 테스트가 언제 실행될지는 알 수 없기 때문에 사실상 이 또한 예측이 불능하다고 볼 수 있다. 그렇다면 시간에 의존하는 코드를 위한 테스트 작성 또한 랜덤 테스트처럼 그 시간을 고정하면 해결되는 문제이다. 그러나 시간의 경우 랜덤 시드값과 달리 개발자가 제어하기 어려운 부분이다. 시간은 시스템 레벨에서 운영체제가 관리하고 있는데 시스템을 건드려서 시간을 조작한다한들 단순히 애플리케이션 테스트를 위해 시스템을 건드는건 그리 좋은 방법은 아니다. 또한 이는 시스템 시간을 조작한 머신이 아니면 무용지물이다.

따라서, 시스템 시간을 조작하는 대신 프로그래밍 언어 레벨에서 시간 정보를 가져오는 내장 패키지나 외부 패키지를 오버라이딩 혹은 래핑하는 방식으로 시간을 제어하는 방법을 사용하는게 깔끔할 것이다. 보통 이런 방식을 [몽키 패치](https://en.wikipedia.org/wiki/Monkey_patch)라고 한다. 몽키 패치 혹은 몽키 패칭이란 런타임중에 프로그램의 메모리를 직접 건드려 소스를 변형하는 행위를 말한다. 따라서, 몽키 패칭은 일반적인 경우에는 안티패턴으로 여겨진다. 실제 애플리케이션 로직 코드에서 몽키 패칭을 사용하는건 필자 또한 매우 지양하는바이며 테스트 코드와 같이 일부 제한된 환경에서만 사용하는걸 권한다.

그럼 우리는 이제 시간 데이터를 다루는 패키지 또는 해당 패키지의 특정 함수를 몽키 패칭하여 시간을 고정하여 테스트를 수행할 것이다. 설명은 장황했으나 사실 코드 레벨에서 보면 아주 간단하다.

그럼 이제 테스트 코드를 작성하기 전에 테스트를 위한 함수 몇개를 작성해보자.

```go
// example.go
package example

import (
    "fmt"
    "time"
)

func NewFakeLog(delta time.Duration) string {
    return fmt.Sprintf(
        RandIPv4Address(),
        RandUsername(),
        RandNumber(0, 1000),
        time.Now().Add(delta).Format(time.RFC3339),
        RandHTTPMethod(),
        RandResourceURI(),
        RandStatusCode(),
        RandNumber(0, 30000),
    )
}

func CheckUserType(user *User) string {
    now := time.Now()
    mod := user.Modified
    switch {
    // 7일 이내 접속 유저
    case mod.After(now.AddDate(0, 0, -7)):
        return "active"
    // 14일 이내 접속 유저
    case mod.After(now.AddDate(0, 0, -14)):
        return "returnable"
    // 14일 이내 미접속 유저
    default:
        return "human"
    }
}

func GetLuckySeven() (string, bool) {
    now := time.Now()
    // 일수가 7의 배수이면 경험치 2배 아이템을 지급한다
    if (now.Day() % 7 == 0) {
        return "lucky_double_exp", true
    }
    return "", false
}
```

위 함수들은 각기 다른 형태로 시간 데이터에 의존하고 있다. 따라서 함수의 실행 시점에 따라 그 결과가 달라질 수 있기 때문에 일반적인 방법으로는 테스트 코드를 작성하기가 어렵다. 따라서, 조금 전에 소개한 몽키 패치를 활용하여 위 함수들에 대한 테스트 코드를 작성해보고자 한다.

테스트 코드 작성은 단순히 테스트 함수 내에서 테스트 대상 함수를 실행하기전에 미리 시간 함수를 몽키 패칭 해두면 된다. 그리고 몽키 패칭을 한 뒤 함수 테스트까지 마치면 몽키 패치를 해제(몽키 언패치) 해주도록 하자.

여기서는 [`monkey`](github.com/bouk/monkey)라는 몽키 패치 라이브러리를 사용할 것이다. (Python 유저라면 [`freezegun`](https://github.com/spulec/freezegun)이라는 시간을 프리징해주는 라이브러리가 있다. 이 또한 내장 `datetime` 패키지를 래핑하는 몽키 패치용 라이브러리이다)

```go
// example_test.go
package example

import (
    "fmt"
    "github.com/bouk/monkey"
    "github.com/stretchr/testify/assert"
    "math/rand"
    "testing"
    "time"
)

func ExampleNewFakeLog() {
    // NewFakeLog의 경우 랜덤/시간 데이터를 모두 사용하고 있으므로 랜덤 시드 또한 고정해준다
    rand.Seed(11)
    
    // 고정된 시간만 나오도록 time.Now 메서드를 몽키 패치한다
    // 현재 시간을 2018년 4월 22일 9시 30분으로 고정
    monkey.Patch(time.Now, func() time.Time {return time.Date(2018, 04, 22, 9, 30, 0, 0, time.UTC)})
    // 테스트 메서드가 끝나면 time.Now를 몽키 언패치한다
    defer monkey.Unpatch(time.Now)
    
    fmt.Println(NewFakeLog(0))
    // Output: 222.83.191.222 - Kozey7157 697 [2018-04-22T09:30:00Z] "DELETE /innovate/next-generation" 302 24570
}

func TestCheckUserType(t *testing.T) {
    user := &User{
        Name: "mingrammer",
        Github: "https://github.com/mingrammer",
        Avatar: "https://mingrammer.com/images/avatar@2x.png",
        ...
        Modified: time.Date(2018, 04, 04, 12, 30, 0, 0, time.UTC),
    }
    
    // 현재 시간을 2018년 4월 10일 9시 30분으로 고정
    monkey.Patch(time.Now, func() time.Time {return time.Date(2018, 04, 10, 9, 30, 0, 0, time.UTC)})
    assert.Equal(t, "active", CheckUserType(user), "Given user is active user")
    
    // 현재 시간을 2018년 4월 17일 9시 30분으로 고정
    monkey.Patch(time.Now, func() time.Time {return time.Date(2018, 04, 17, 9, 30, 0, 0, time.UTC)})
    assert.Equal(t, "returnable", CheckUserType(user), "Given user is returnable")
    
    // 현재 시간을 2018년 4월 24일 9시 30분으로 고정
    monkey.Patch(time.Now, func() time.Time {return time.Date(2018, 04, 24, 9, 30, 0, 0, time.UTC)})
    assert.Equal(t, "human", CheckUserType(user), "Given user is human user")
    
    // 테스트 메서드가 끝나면 time.Now를 몽키 언패치한다
    monkey.Unpatch(time.Now)
}

func TestGetLuckySeven(t *testing.T) {
    // 현재 시간을 2018년 4월 7일 9시 30분으로 고정
    monkey.Patch(time.Now, func() time.Time {return time.Date(2018, 04, 07, 9, 30, 0, 0, time.UTC)})
    ls, ok := GetLuckySeven()
    assert.True(t, ok, "Today should be lucky day")
    assert.Equal(t, "lucky_double_exp", ls, "Today should be lucky day")
    
    // 현재 시간을 2018년 4월 9일 9시 30분으로 고정
    monkey.Patch(time.Now, func() time.Time {return time.Date(2018, 04, 09, 9, 30, 0, 0, time.UTC)})
    ls, ok = GetLuckySeven()
    assert.False(t, ok, "Today shouldn't be lucky day")
    assert.Equal(t, "", ls, "Today shouldn't be lucky day")
    
    // 테스트 메서드가 끝나면 time.Now를 몽키 언패치한다
    monkey.Unpatch(time.Now)
}
```

몽키 패치를 통해 현재 시간을 원하는값으로 설정할 수 있게되어 시간에 의존적인 코드를 아주 쉽고 편하게 테스트할 수 있게 되었다. 일반적으로 시간 데이터를 다루는 코드는 이처럼 몽키 패치를 사용해서 테스트 하는걸 추천한다. 다만, 위에서도 언급했지만 테스트와 같이 제한된 환경 외에서 몽키 패칭을 남용하는건 지양하길 바란다.



# 매개변수화

매개변수화는 랜덤과 시간 데이터를 사용하는 모든 코드에 활용할 수 있는 방법이다. 말 그대로 랜덤이나 시간 데이터를 내부에서 선언하거나 생성하지 않고 매개변수를 통해 받아 사용하는 방식이다. 따라서 꼭 시드를 고정하거나 몽키 패칭을 하지 않더라도 제한된 조건 하에서 테스트를 보다 쉽게 할 수 있다.

처음에 살펴본 코드에 매개변수화를 적용하면 다음과 같이 변형할 수 있다.

```go
func RandFruit(index int) string {
    set := []string{"apple", "melon", "banana", "mango"}
    return set[index]
}

func main() {
    rand.Seed(time.Now().UnixNano())
    index := rand.Intn(4) // 0 ~ 3 사이의 임의의 정수값 생성
    fmt.Println(RandFruit(index))
}
```

이렇게 매개변수화를 하면 다음과 같이 테스트가 가능해진다.

```go
func ExampleRandFruit() {
    fmt.Print(RandFruit(0))
    // Output: apple
}
```

시간 데이터도 마찬가지로 시간 파라미터를 추가하면된다.

```go
func GetLuckySeven(date time.Time) (string, bool) {
    if (date.Day() % 7 == 0) {
        return "lucky_double_exp", true
    }
    return "", false
}
```

```go
func TestGetLuckySeven(t *testing.T) {
    now := time.Date(2018, 04, 07, 9, 30, 0, 0, time.UTC)}
    ls, ok := GetLuckySeven(now)
    assert.True(t, ok, "Today should be lucky day")
    assert.Equal(t, "lucky_double_exp", ls, "Today should be lucky day")
    
    now = time.Date(2018, 04, 09, 9, 30, 0, 0, time.UTC)})
    ls, ok = GetLuckySeven(now)
    assert.False(t, ok, "Today shouldn't be lucky day")
    assert.Equal(t, "", ls, "Today shouldn't be lucky day")
}
```

이렇게 랜덤 및 시간 데이터를 매개변수화하면 별다른 시드 고정이나 몽키 패치를 하지 않아도 몽키 패치를 한 것처럼 테스트 코드 작성이 가능하기 때문에 테스트 코드 작성에 대한 공수를 줄일 수 있다.

물론 이 또한 완벽한 방법은 아니다. 처음부터 코드를 위와 같이 작성하면 모르겠으나 그렇지 않은 경우에는 테스트의 편리함을 위해 모든 랜덤 및 시간 데이터를 파라미터로 만들어야하는 문제가 생길 수 있다. 또한, 랜덤 및 시간 데이터의 매개변수화 자체가 불필요한 작업이거나 이로 인해 유지보수하기 어려운 형태가 만들어질수도 있기 때문에 매개변수화를 해도 괜찮은 상황이 아니라면 이보단 몽키 패치를 활용하는게 더 나을 것 같다.

이상으로 랜덤 및 시간 데이터를 다루는 코드를 위한 테스트 코드 작성 방법에 대해서 살펴보았다. 아주 흔하고 난이도 있는 내용도 아니지만 테스트 초보인 누군가에게는 도움이 되었으면 한다.

다음에도 기회가 된다면 다른 여러 테스트 방법에 대해서 다뤄보도록 하겠다.

---

## File: content/post/2018-05-01-project-euler-maximum-path-sum.md

---
categories:
- algorithm
comments: true
date: 2018-05-01T00:00:00Z
tags:
- project-euler
- maximum-sum
title: "[프로젝트 오일러] 최대 경로합 문제 (18/67)"
url: /project-euler-maximum-path-sum
---

> [프로젝트 오일러](https://projecteuler.net) [18](https://projecteuler.net/problem=18), [67](https://projecteuler.net/problem=67)번 문제 풀이

# 문제



18번 문제는 위와 같이 숫자로 이루어진 삼각형의 꼭대기부터 바닥까지 이어진 경로중 가장 합이 큰 경로를 구하는 문제이다. 단, 아래로 이동할 때에는 아래로 인접한 양 옆의 숫자로만 이동할 수 있다. 67번 문제도 동일한 문제이며 차이점은 삼각형의 높이가 100으로 커졌다는 것이다.

18번 문제에서는 삼각형의 높이가 15이므로 모든 경로의 수가 16,384(2^14)개밖에 되지 않아 Brute force로 풀어도 무방하나, 67번 문제에서는 그 경로가 무려 633,825,300,114,114,700,748,351,602,688(2^99)개나 존재하기 때문에 Brute force로는 불가능하다.

# 풀이

알고리즘의 아이디어는 간단하다. 위에서부터 아래로 내려갈 때에는 바닥까지의 경로중 어떤 경로의 합이 가장 클지 알 수 없기 때문에 아래로 인접한 숫자중 큰 숫자로 계속 이동하는 경로가 가장 합이 큰지는 보장할 수 없다.

그러나 이를 반대로 생각하여 바닥에서 시작하여 위로 이동하면서 그 합이 더 큰 경로로만 이동하면 합이 가장 큰 경로를 구할 수 있다. 왜냐하면 아래에서부터 합이 가장 큰 일부 경로를 고정시키면 삼각형의 꼭대기부터 좀 전에 구한 일부 경로의 꼭대기까지 이어진 경로중 최대 경로만 구하여 합하면 되기 때문이다. 이렇게 되면 합이 가장 큰 일부 경로 외의 모든 경로는 계산 대상에서 제외된다.

```
1.       75
2.     95  64
3.   17  47  82
4. 18  35  87  10
```

3번 라인에서 시작해보자. 3번의 17에서 4번 라인까지의 경로는 (17, 18)과 (17, 35)가 존재한다. 이 두 경로중 더 큰 경로는 (17, 35)이다. 따라서 꼭대기부터 17까지의 경로들은 항상 4번 라인의 35로만 이동하면된다. 마찬가지로 3번 라인의 모든 숫자들에 대해서 이 계산을 수행할 수 있다. 47에 대해서는 (47-87), 82에 대해서는 (82-87)이 가장 크다. 계산을 수행한 후에는 상위 라인의 숫자들을 아래 라인에서부터의 더 큰 경로의 합으로 대체한다.

```
1.       75
2.     95  64
3.   17  47  82
       \
4. 18  35  87  10
```

```
1.       75
2.     95  64
3.   17  47  82
           \
4. 18  35  87  10
```

```
1.       75
2.     95  64
3.   17  47  82
            /
4. 18  35  87  10
```

이렇게 최대 경로를 계산한 뒤에 트리는 다음과 같이 바뀐다.

```
1.          075
2.       095   064
3.    052   134   169
         \     \ /
4. 018   035   087   010
```

가장 꼭대기까지 동일한 계산을 수행해보자.

```
1.          075
2.       229   233
            \    \
3.    052   134   169
         \     \ /
4. 018   035   087   010
```

```
1.          308
              \
2.       229   233
            \    \
3.    052   134   169
         \     \ /
4. 018   035   087   010
```

최대 경로의 합은 `308`이며 그 경로는 (75, 64, 82, 87)이 된다.

즉, 가장 바닥의 바로 위 라인에서 시작하여 각 숫자를 아래 라인의 양 옆의 인접한 숫자중 더 큰 숫자와 합하면서 위로 올라가다보면 합이 가장 큰 경로를 구할 수 있게 된다.

아래에서부터 각 라인간 인접한 숫자의 합을 계산하면서 올라가기 때문에 한 연산당 계산은 2번씩 이루어지며 연산은 삼각형에 존재하는 숫자들의 개수만큼 수행된다. 따라서  `n`을 삼각형의 높이라고 할 때 총 연산수는 `n(n-1)`가 되며 알고리즘의 복잡도는 `O(n^2)`이 된다.

## 18번 솔루션

```python
triangle = """
75
95 64
17 47 82
18 35 87 10
20 04 82 47 65
19 01 23 75 03 34
88 02 77 73 07 63 67
99 65 04 28 06 16 70 92
41 41 26 56 83 40 80 70 33
41 48 72 33 47 32 37 16 94 29
53 71 44 65 25 43 91 52 97 51 14
70 11 33 28 77 73 17 78 39 68 17 57
91 71 52 38 17 14 91 43 58 50 27 29 48
63 66 04 68 89 53 67 30 73 16 69 87 40 31
04 62 98 27 23 09 70 98 73 93 38 53 60 04 23
"""

triangle_leaves = []
for line in triangle.strip().splitlines():
    triangle_leaves.append([int(i) for i in line.split()])

n = len(triangle_leaves)
for i, leaves in enumerate(triangle_leaves[-2::-1]):
    for j, leaf in enumerate(leaves):
        left = triangle_leaves[n-i-1][j]
        right = triangle_leaves[n-i-1][j+1]
        triangle_leaves[n-i-2][j] += max(left, right)

print(triangle_leaves[0][0])
```

> 최대 경로합 = 1074

## 67번 솔루션

> 코드는 동일하며 삼각형 데이터를 파일에서 가져옴

```python
with open('data/p067_triangle.txt', 'r') as f:
    triangle = f.read()

    triangle_leaves = []
    for line in triangle.strip().splitlines():
        triangle_leaves.append([int(i) for i in line.split()])

    n = len(triangle_leaves)
    for i, leaves in enumerate(triangle_leaves[-2::-1]):
        for j, leaf in enumerate(leaves):
            left = triangle_leaves[n-i-1][j]
            right = triangle_leaves[n-i-1][j+1]
            triangle_leaves[n-i-2][j] += max(left, right)

    print(triangle_leaves[0][0])
```

> 최대 경로합 = 7273

---

## File: content/post/2018-05-17-translation-building-blockchain-in-go-part-1.md

---
categories:
- translation
- blockchain
- tutorial
comments: true
date: 2018-05-17T00:00:00Z
tags:
- blockchain
- prototype
title: "[Go로 구현하는 블록체인] Part 1: 기본 프로토타입"
url: /building-blockchain-in-go-part-1
---

> [Building Blockchain in Go](https://jeiwan.cc/posts/building-blockchain-in-go-part-1) 시리즈를 번역한 글입니다.
>
> Go 블록체인 구현체 저장소: [Blockchain Go](https://github.com/Jeiwan/blockchain_go)



# 소개

블록체인은 21세기의 가장 혁명적인 기술중 하나로 현재는 성숙 단계이며 아직 완전히 실현되지는 않았다. 블록체인은 본질적으로 분산화된 데이터베이스이다. 그러나 블록체인을 독보적으로 만든건 블록체인이 프라이빗이 아닌 퍼블릭 데이터베이스라는 것이다. 즉, 블록체인을 사용하는 모든 사람들이 블록체인의 일부 복제본을 가지고 있다는 얘기이다. 그리고 새로운 레코드는 데이터베이스를 지니고 있는 사람들의 합의하에서만 추가될 수 있다. 또한 블록체인을 활용하면 암호화 화폐 및 스마트 컨트랙트를 만드는 것도 가능하다.

이 튜토리얼 시리즈에서는 블록체인에 기반한 간단한 암호화 화폐를 구현해볼 것이다.

# 블록 (Block)

"블록체인"을 구성하는 "블록"부터 살펴보자. 블록체인에서 블록이란 가치있는 정보를 저장하는 데이터 구조이다. 한 예로, 비트코인의 블록은 모든 암호화 화폐에 있어 필수적인 정보인 트랜잭션 정보를 저장한다. 또한 블록은 블록의 버전, 현재 시간의 타임스탬프 및 이전 블록의 해시값등의 기술적인 정보 또한 포함하고 있다. 이 튜토리얼에서는 블록체인 또는 비트코인의 스펙을 그대로 구현하지는 않을거지만 대신 중요한 정보만을 포함하는 간단한 버전의 블록을 구현할 것이다. 구현할 블록의 구조체는 다음과 같다.

```go
type Block struct {
        Timestamp     int64
        Data          []byte
        PrevBlockHash []byte
        Hash          []byte
}
```

**Timestamp**는 현재 시간의 타임스탬프 (블록 생성 시간), **Data**는 블록에 포함된 실제 가치를 지닌 정보, **PrevBlockHash**는 이전 블록의 해시값 그리고 **Hash**는 블록의 해시값이다. 비트코인 스펙에서는 **Timestamp**, **PrevBlockHash** 및 **Hash**가 블록 헤더라는 하나의 독립된 데이터 구조를 이루며 트랜잭션 (여기서는 **Data**)이 또 다른 독립된 데이터 구조를 갖는다. 우리는 구현을 단순히 하기위해 이 둘을 합쳤다.

그렇다면 해시는 어떻게 계산할 수 있을까? 해시 계산법은 블록체인에서 아주 중요한 부분이며, 블록체인을 안전하게 만드는 핵심 기능이다. 해시 계산은 계산적으로 아주 어려운 작업이며, 빠른 컴퓨터에서조차 시간이 다소 걸리는 작업이다 (이게 바로 사람들이 비트코인을 채굴하기 위해 강력한 GPU를 구매하는 이유이다). 이는 의도적인 설계로, 새로운 블록을 추가하기 어렵게 만들어 블록이 추가된 후에 블록의 수정을 방지하기 위함이다. 이 메커니즘에 대한 논의와 구현은 조금 뒤에서 살펴보겠다.

지금은 블록을 구성하는 필드들을 하나로 이은 뒤 이어진 문자열에 대해 SHA-256 해시를 계산할 것이다. 다음과 같이 **SetHash** 메서드를 작성한다.

```go
func (b *Block) SetHash() {
        timestamp := []byte(strconv.FormatInt(b.Timestamp, 10))
        headers := bytes.Join([][]byte{b.PrevBlockHash, b.Data, timestamp}, []byte{})
        hash := sha256.Sum256(headers)
        b.Hash = hash[:]
}
```

다음은 Go의 컨벤션을 따라, 블록을 생성하는 함수를 작성한다.

```go
func NewBlock(data string, prevBlockHash []byte) *Block {
        block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}}
        block.SetHash()
        return block
}
```

블록을 위한 코드 작성은 끝났다!

# 블록체인 (Blockchain)

이제 블록체인을 구현해보자. 블록체인은 본질적으로 특정한 구조를 지닌 데이터베이스일 뿐이며 순서가 지정된 링크드 리스트이다. 즉, 블록은 삽입 순서대로 저장되며 각 블록은 이전 블록과 연결된다. 이러한 구조 덕분에 최신 블록을 빠르게 가져올 수 있고 해시로 블록을 (효율적으로) 검색할 수 있다.

Go에서는 배열과 맵을 활용해 이 구조를 구현할 수 있다. 배열은 정렬된 해시를 유지하고 맵은 해시-블록쌍을 유지한다. 그러나 지금 당장은 해시 검색 기능이 필요하지 않기 때문에 프로토타입 구현에서는 배열만 사용할 것이다.

```go
type Blockchain struct {
        blocks []*Block
}
```

우리의 첫 블록체인이 완성되었다! 아주 쉽다.

이제 블록 추가 기능을 만들어보자.

```go
func (bc *BlockChain) AddBlock(data string) {
        prevBlock := bc.blocks[len(bc.blocks)-1]
        newBlock := NewBlock(data, prevBlock.Hash)
        bc.blocks = append(bc.blocks, newBlock)
}
```

그럼 이제 끝인가? 과연..

새 블록을 추가하려면 기존 블록이 필요하다. 그러나 우리 블록체인에는 아무 블록도 없다! 따라서 모든 블록체인에는 적어도 하나의 블록이 있어야하며, 블록체인의 첫번째 블록을 **제네시스 블록**이라고 한다. 이 블록을 생성하는 메서드를 구현해보자.

```go
func NewGenesisBlock() *Block {
        return NewBlock("Genesis Block", []byte{})
}
```

이제 제네시스 블록을 가지고 블록체인을 생성하는 함수를 구현해보자.

```go
func NewBlockchain() *Blockchain {
        return &Blockchain{[]*Block{NewGenesisBlock()}}
}
```

블록체인이 제대로 동작하는지 확인해보자.


```go
func main() {
        bc := NewBlockchain()

        bc.AddBlock("Send 1 BTC to Ivan")
        bc.AddBlock("Send 2 more BTC to Ivan")

        for _, block := range bc.blocks {
                fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
                fmt.Printf("Data: %s\n", block.Data)
                fmt.Printf("Hash: %x\n", block.Hash)
                fmt.Println()
        }
}
```

출력:

```
Prev. hash:
Data: Genesis Block
Hash: aff955a50dc6cd2abfe81b8849eab15f99ed1dc333d38487024223b5fe0f1168

Prev. hash: aff955a50dc6cd2abfe81b8849eab15f99ed1dc333d38487024223b5fe0f1168
Data: Send 1 BTC to Ivan
Hash: d75ce22a840abb9b4e8fc3b60767c4ba3f46a0432d3ea15b71aef9fde6a314e1

Prev. hash: d75ce22a840abb9b4e8fc3b60767c4ba3f46a0432d3ea15b71aef9fde6a314e1
Data: Send 2 more BTC to Ivan
Hash: 561237522bb7fcfbccbc6fe0e98bbbde7427ffe01c6fb223f7562288ca2295d1
```

잘 동작한다!

# 결론

이제까지 매우 간단한 블록체인 프로토타입을 만들어보았다. 이는 단지 블록들의 배열로 각 블록은 이전 블록과 연결되어있다. 실제 블록체인은 이보다 훨씬 더 복잡하다. 우리가 만든 블록체인에서 새로운 블록을 추가하는 것은 쉽고 빠르지만 실제 블록체인에서 새로운 블록을 추가하는데는 몇 가지 작업이 필요하다: 블록 추가 권한을 얻기 위해 아주 무거운 계산을 수행해야한다 (이 메커니즘을 작업 증명 (Proof of Work)이라고 한다). 또한 블록체인은 단일 의사결정자가 없는 분산화된 데이터베이스이다. 따라서, 하나의 새로운 블록은 반드시 네트워크의 참여자들로부터 확인과 승인을 받아야한다.  (이 메커니즘을 합의 또는 컨센서스 (consensus)라고 한다)). 그리고 우리의 블록체인에는 아직 트랜잭션이 없다!

다음 파트에서는 위의 각 기능들을 살펴볼 것이다.

---

## File: content/post/2018-05-18-translation-building-blockchain-in-go-part-2.md

---
categories:
- translation
- blockchain
- tutorial
comments: true
date: 2018-05-18T00:00:00Z
tags:
- blockchain
- pow
title: "[Go로 구현하는 블록체인] Part 2: 작업 증명"
url: /building-blockchain-in-go-part-2
---

> [Building Blockchain in Go](https://jeiwan.cc/posts/building-blockchain-in-go-part-2) 시리즈를 번역한 글입니다.

# 서론

[이전 파트](/building-blockchain-in-go-part-1)에서 블록체인 데이터베이스의 핵심인 매우 간단한 데이터 구조를 만들어보았다. 그리고 각 블록을 이전 블록에 연결함으로써 체인에 블록을 추가할 수 있도록 만들었다. 하지만 우리가 구현한 블록체인 구현체에는 심각한 결함이 하나 있는데 바로 체인에 새 블록을 추가하기가 쉬우며 비용이 낮다는 것이다. 블록체인과 비트코인의 핵심은 새 블록을 추가하는 작업이 어렵다는 것이다. 이번 파트에서는 이 결함을 개선해보도록 하자.

# 작업 증명 (Proof of Work)

블록체인의 핵심 아이디어는 데이터를 추가하기 위해선 어떤 어려운 작업을 수행해야 한다는 것이다. 이 작업이 블록체인을 안전하고 일관성있게 만들어준다. 또한 이 어려운 작업을 수행한 참여자에게는 보상이 주어진다 (사람들이 채굴을 통해 코인을 얻는 방식이다).

이 메커니즘은 실생활과 아주 흡사한데 보상을 받으며 생계를 유지하기위해 열심히 일을 하는것과 유사하다. 블록체인에서 네트워크의 일부 참여자들 (채굴자)은 네트워크를 유지하기 위해 블록을 생성하고 이에 대한 보상을 받는다. 이 작업의 결과로 블록은 블록체인에 안전하게 추가되어 전체 블록체인 데이터베이스의 안정성을 유지한다. 작업을 마친 사람이 이 작업을 증명해야 한다는것에 주목할 필요가 있다.

"어려운 작업을 수행하고 이를 증명한다"의 전체 메커니즘을 작업 증명 (Proof of Work, PoW)이라고 부른다. 이는 엄청난 컴퓨팅 파워를 요구하기 때문에 작업이 무거우며 고성능 컴퓨터에서조차 빠르게 수행하기 어렵다. 또한, 블록 생성 속도를 시간당 약 6 블록으로 유지하기 위해 작업의 난이도는 시간이 지남에따라 증가한다. 비트코인에서 이 작업의 목표는 특정 요구사항을 충족하는 블록의 해시를 찾는 것이다. 그리고 이 해시가 증명 역할을 하기 때문에 증명을 찾는 작업이 실질적인 작업이다.

마지막으로 주의해야할 점이 있다. 작업 증명 알고리즘은 작업 수행은 어렵지만 증명 검증은 쉬워야한다는 요구사항을 충족해야한다. 증명은 대개 다른 사람에게 넘어가기 때문에 검증 작업에 많은 시간이 소요되서는 안된다.

# 해싱

이 절에서는 해싱에 대해 다룰 것이다. 해싱이라는 개념에 익숙하다면 이 부분은 넘어가도 좋다.

해싱이란 어떤 특정한 데이터에 대한 해시를 얻는 과정이다. 해시란 계산된 데이터의 고유한 표현값이다. 해시 함수는 임의의 길이를 가진 데이터를 입력으로 받아 고정된 길이의 해시값을 생성하는 함수이다. 다음은 해싱의 핵심 기능이다.

1. 원본 데이터는 해시에서 복원할 수 없다. 즉, 해싱은 암호화가 아니다.
2. 특정 데이터는 단 하나의 해시값만 가지며 해시는 고유하다.
3. 입력 데이터에서 하나의 바이트만 수정해도 완전히 다른 해시값이 생성된다.



해시 함수는 데이터의 일관성을 검사하는데 널리 사용된다. 일부 소프트웨어 제공 업체는 소프트웨어 패키지와 함께 체크섬을 퍼블리싱한다. 파일을 다운로드한 후 이를 해싱 함수에 넣어 생성한 해시를 소프트웨어 개발자가 제공한 해시와 비교할 수 있다.

블록체인에서 해싱은 블록의 일관성을 보장하는데 사용된다. 해싱 알고리즘의 입력 데이터에는 이전 블록의 해시값도 포함되어 있기 때문에 체인상의 어떤 한 블록을 변경하는건 불가능하다 (또는 아주 어렵다). 하나의 블록을 변경하면 해당 블록에 대한 해시와 그 이후의 모든 블록들에 대한 해시를 다시 계산해야하기 때문이다.

# 해시캐시 (Hashcash)

비트코인은 초기에 이메일 스팸을 방지하기 위해 개발된 작업 증명 알고리즘인 [해시캐시](https://en.wikipedia.org/wiki/Hashcash)를 사용한다. 알고리즘은 다음 스텝들로 나눌 수 있다.

1. 공개적으로 알려진 데이터를 가져온다 (이메일의 경우 수신자의 이메일 주소, 비트코인의 경우 블록의 헤더를 들 수 있다)
2. 여기에 카운터를 더한다. 카운터는 0부터 시작한다.
3. **data + counter**의 해시를 구한다.
4. 해시가 특정 요구사항을 충족하는지 확인한다.
    1. 만족한다면 알고리즘을 끝낸다.
    2. 그렇지 않다면 카운터를 증가시켜 3번과 4번 스텝을 반복한다.

즉, 이는 무차별 대입 (Brute-force) 알고리즘으로 카운터를 늘리고 새로운 해시를 계산하고 검증하는 과정을 반복한다. 이게 바로 작업 증명의 계산 비용이 높은 이유이다.

이제 해시가 충족해야하는 요구사항이 무엇인지 자세히 살펴보도록 하자. 원래의 해시캐시 구현체에서의 요구사항은 "첫 20비트가 모두 0이어야함"이다. 비트코인의 요구사항은 종종 조정되는데, 시간이 지남에 따라 연산 능력이 증가하고 네트워크에 참여하는 채굴자들이 늘어남에도 불구하고 설계상 10분마다 하나의 블록이 생성되어야하기 때문이다.

알고리즘 시연을 위해, 이전 예제의 데이터 ("I like donuts")를 가지고 3개의 제로 바이트로 시작하는 해시를 찾았다.



**ca07ca**는 10진수로 13240266인 카운터의 16진수값이다.

# 구현

이론은 다 살펴보았으니 이제 코드를 작성해보자! 먼저 채굴 난이도를 정의하자.

```go
const targetBits = 24
```

비트코인에서 "목표 비트 (target bits)"란 블록이 채굴되는 난이도를 저장하고 있는 블록 헤더이다. 현재는 목표 조정 알고리즘을 구현하진 않을 것이므로 난이도를 전역 상수로 정의할 수 있다.

24는 임의의 숫자이고, 우리의 목표은 256비트 이하의 메모리를 차지하는 타겟을 갖는 것이다. 그리고 차이가 커질수록 적절한 해시를 찾는 것이 어려워지기 때문에 차이가 충분히 크지만 너무 크지는 않길 바란다.

```go
type ProofOfWork struct {
        block *Block
        target *big.Int
}

func NewProofOfWork(b *Block) *ProofOfWork {
        target := big.NewInt(1)
        target.Lsh(target, uint(256-targetBits))

        pow := &ProofOfWork{b, target}
        return pow
}
```

블록 포인터와 타겟 포인터를 가진 **ProofOfWork** 구조체를 만들었다. "타겟 (target)"이란 이전절에서 언급했던 요구사항의 또 다른 말이다. 해시와 타겟을 비교할 수 있도록 [큰 (Big)](https://golang.org/pkg/math/big/) 정수를 사용했다. 우리는 해시를 큰 정수로 변환하여 이 값이 타겟보다 작은지 검사할 것이다.

**NewProofOfWork** 함수에서 **bit.Int**을 1로 초기화하고 **256 - targetBits** 비트만큼 좌측 시프트 연산을 취했다. 256은 SHA-256 해시의 비트 길이로 SHA-256이 우리가 사용할 해시 알고리즘이다. **target**의 16진수값은 다음과 같다.

```
0x10000000000000000000000000000000000000000000000000000000000
```

이 값은 메모리에서 29바이트를 차지한다. 이전 예제의 해시값을 가져와 시각적으로 비교해보자.

```
0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca
```

첫 번째 해시값 ("I like donuts"의 해시)은 타겟보다 크기 때문에 유효한 작업 증명이 아니다. 두번째 해시값 ("I like donutsca07ca")은 타겟보다 작기 때문에 유효한 증명이다.

그렇다면 이 타겟값을 유효 범위의 상한선으로 생각할 수 있다. 해시가 이 경계값보다 작으면 유효하고 그 역도 마찬가지이다. 경계값을 낮추면 유효한 해시값의 범위가 줄어들어 이 값을 찾는 작업이 더욱 어려워진다.

이제 해시 계산을 위한 데이터가 필요하므로 데이터를 준비하도록 하자.

```go
func (pow *ProofOfWork) prepareData(nonce int) []byte {
        data := bytes.Join(
                [][]byte{
                        pow.block.PrevBlockHash,
                        pow.block.Data,
                        IntToHex(pow.block.Timestamp),
                        IntToHex(int64(targetBits)),
                        IntToHex(int64(nonce)),
                },
                []byte{},
        )
        return data
}
```

단순히 블록의 필드값들과 타겟 및 논스값을 병합하는 직관적인 코드다. **논스 (nonce)**란 해시캐시에서의 카운터와 동일한 역할을 하는 암호학 용어이다.

모든 준비는 끝났다. 이제 PoW 알고리즘의 핵심 코드를 구현해보자.

```go
func (pow *ProofOfWork) Run() (int, []byte) {
        var hashInt big.Int
        var hash [32]byte
        nonce := 0

        fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)

        for nonce < maxNonce {
                data := pow.prepareData(nonce)
                hash = sha256.Sum256(data)
                fmt.Printf("\r%x", hash)

                hashInt.SetBytes(hash[:])
                if hashInt.Cmp(pow.target) == -1 {
                        break
                } else {
                        nonce++
                }
        }
        fmt.Print("\n\n")

        return nonce, hash[:]
}
```

먼저 변수들을 초기화한다. **hashInt**는 **hash**의 정수 표현값이며 **nonce**는 카운터이다. 다음으로 "무한" 루프를 실행한다. 이는 최대 **maxNonce**만큼만 실행되며 이 값은 **math.MaxInt64**와 같다. 루프 횟수를 제한하는 이유는 **nonce**의 오버플로우를 막기 위해서이다. 물론 우리의 PoW 구현체의 난이도는 카운터가 오버플로우 나기에는 아주 낮지만 경우에 따라서는 오버플로우 검사를 하는게 더 좋다.

루프에서는 다음의 작업들이 수행된다.

1. 데이터 준비 (생성)
2. SHA-256 해싱
3. 해시값의 큰 정수로의 변환
4. 정수값과 타겟값 비교

이전에 설명된것처럼 쉽다. 우리는 이제 **Block**의 **SetHash** 메서드를 제거하고 **NewBlock** 함수를 수정할 수 있다.

```go
func NewBlock(data string, prevBlockHash []byte) *Block {
        block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
        pow := NewProofOfWork(block)
        nonce, hash := pow.Run()

        block.Hash = hash[:]
        block.Nonce = nonce
        return block
}
```

**nonce**가 **Block**의 프로퍼티로 저장되고 있음을 볼 수 있다. 증명 검증에 **nonce**가 필요하기 때문에 이는 필수적이다. **Block** 구조체는 이제 다음과 같이 작성할 수 있다.

```go
type Block struct {
        Timestamp     int64
        Data          []byte
        PrevBlockHash []byte
        Hash          []byte
        Nonce         int
}
```

완성되었다! 잘 동작하는지 확인하기 위해 한 번 실행해보자.

```
Mining the block containing "Genesis Block"
00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Mining the block containing "Send 1 BTC to Ivan"
00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Mining the block containing "Send 2 more BTC to Ivan"
000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

Prev. hash:
Data: Genesis Block
Hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Prev. hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1
Data: Send 1 BTC to Ivan
Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804
Data: Send 2 more BTC to Ivan
Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe
```

와우! 모든 해시가 3개의 제로 바이트로 시작하고 있음을 볼 수 있다. 또한 해시값들을 얻는데 일정 시간이 소요되었음을 확인할 수 있다.

마지막으로 할 일이 하나 남았다. 작업 증명을 검증할 수 있는 기능을 만들어보자.

```go
func (pow *ProofOfWork) Validate() bool {
        var hashInt big.Int

        data := pow.prepareData(pow.block.Nonce)
        hash := sha256.Sum256(data)
        hashInt.SetBytes(hash[:])

        isValid = hashInt.Cmp(pow.target) == -1
        return isValid
}
```

위에서 저장한 논스는 바로 이 검증 로직에서 필요하다.

잘 동작하는지 한 번 더 확인해보자.

```go
func main() {
        ...
        for _, block := range bc.blocks {
                ...
                pow := NewProofOfWork(block)
                fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
                fmt.Println()
        }
}
```

출력:

```
...

Prev. hash:
Data: Genesis Block
Hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
PoW: true

Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
Data: Send 1 BTC to Ivan
Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
PoW: true

Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
Data: Send 2 more BTC to Ivan
Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a
PoW: true
```

# 결론

우리의 블록체인은 실제 아키텍처에 한 발 더 가까이 다가갔다. 블록 생성은 이제 어려운 작업을 요구하고 있으며 채굴이 가능해졌다. 그러나 아직도 일부 중요한 기능들이 빠져있다. 블록체인 데이터베이스가 영구적이지 않으며 지갑, 주소, 트랜잭션 및 합의 메커니즘 또한 없다. 이 모든건 이후의 파트들을 통해 구현해 나갈 것이다.

---

## File: content/post/2018-05-22-translation-building-blockchain-in-go-part-3.md

[Binary file]

---

## File: content/post/2018-05-26-translation-building-blockchain-in-go-part-4.md

[Binary file]

---

## File: content/post/2018-06-06-translation-building-blockchain-in-go-part-5.md

---
categories:
- translation
- blockchain
- tutorial
comments: true
date: 2018-06-06T00:00:00Z
tags:
- blockchain
- address
- cryptography
title: "[Go로 구현하는 블록체인] Part 5: 주소"
url: /building-blockchain-in-go-part-5
---

> [Building Blockchain in Go](https://jeiwan.cc/posts/building-blockchain-in-go-part-5) 시리즈를 번역한 글입니다.

# 서론

[이전 파트](/building-blockchain-in-go-part-4)에서는 트랜잭션을 구현했었다. 또한 비트코인에는 사용자 계정이 없고 개인 정보 (이름, 여권 번호 및 SSN등)는 필요하지 않으며 어디에도 저장되지 않는다는 트랜잭션의 비개인적인 본질적인 특성에 대해서도 알아보았다. 하지만 트랜잭션 출력의 소유자를 식별하기 위한 무언가가 필요하긴하다 (이 출력들은 코인의 소유자가 잠궜기 때문에). 그리고 이게 비트코인 주소가 필요한 이유이다. 지금까지 우리는 임의의 사용자 정의 문자열을 주소로 사용해왔으며 비트코인에서 구현된 실제 주소를 구현할 때가 왔다.

> 이 파트에서는 중요한 코드 변경사항을 소개하기 때문에 모든 코드를 설명하는건 의미가 없다. 모든 변경사항은 [여기](https://github.com/Jeiwan/blockchain_go/compare/part_4...part_5#files_bucket)를 참조하라.

# 비트코인 주소

다음은 비트코인 주소의 예이다: [1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa](https://blockchain.info/address/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa). 이는 사토시 나카모토의 것으로 알려진 최초의 비트코인 주소이다. 비트코인 주소는 모두 공개되어있다. 다른 사람에게 코인을 전송하기 위해선 받는 사람의 주소를 알아야한다. 그러나 주소는 고유한 값임에도 불구하고 여러분을 "지갑"의 소유자로 식별하는건 아니다. 사실 이 주소는 사람이 읽기 쉬운 형태의 공개키 표현이다. 비트코인에서 식별자는 여러분의 컴퓨터에 저장된 개인키와 공개키의 쌍 (혹은 쌍들)이다. 비트코인은 암호화 알고리즘을 조합하여 이러한 키들을 생성하는데 전세계의 그 어느 누구도 키에 물리적으로 접근해서 탈취하지 않는 이상 여러분의 코인에 접근할 수 없음을 보장한다. 그럼 이제 어떤 알고리즘들이 사용되는지 살펴보자.

# 공개키 암호

공개키 암호화 알고리즘에서는 공개키와 개인키의 쌍을 사용한다. 공개키는 민감하지 않으며 누구에게나 공개될 수 있다. 반대로 개인키는 소유자의 식별자로 사용되므로 소유자 이외에는 아무도 접근하면 안되기 때문에 공개되어서는 안된다. 여러분이 바로 개인키이다.

본질적으로 비트코인 지갑은 이 키들의 쌍일뿐이다. 지갑 애플리케이션을 설치하거나 비트코인 클라이언트를 사용해 새로운 주소를 생성하면 키 쌍이 생성된다. 비트코인에선 개인키를 제어하는 사람이 이 키로 전송되는 모든 코인을 제어한다.

개인키와 공개키는 임의의 바이트 시퀀스이기 때문에 화면에 출력할 수 없으며 사람이 읽을 수도 없다. 이게 비트코인이 공개키를 사람이 읽을 수 있는 문자열로 변환하는 알고리즘을 사용하는 이유이다.

> 비트코인 지갑 애플리케이션을 사용해 본 적이 있다면 기억하기 쉬운 문구가 생성되었을 가능성이 높다. 이 문구는 개인키 대신 사용되며 키를 생성하는데 사용할 수 있다. 이 메커니즘은 [BIP-039](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)에 구현되어있다.

우리는 이제 비트코인에서 사용자를 어떻게 식별하는지 알게되었다. 그러나 비트코인은 트랜잭션 출력 및 코인에 대한 소유권을 어떻게 확인할까?

# 디지털 서명

수학과 암호학에는 디지털 서명이라는 개념이 있는데 이 알고리즘은 다음을 보장한다.

1. 송신자로부터 수신자에게 데이터가 전송되는 동안에는 데이터가 변경되지 않는다.
2. 데이터는 특정 송신자에 의해 생성된다.
3. 송신자는 데이터 전송을 거부할 수 없다.

데이터에 서명 알고리즘을 적용하면 (즉, 데이터 서명) 서명이 얻어지며, 이는 나중에 검증될 수 있다. 디지털 서명에서는 개인키를 사용하며 검증시에는 공개키가 필요하다.

데이터 서명을 위해서는 다음의 것들이 필요하다.

1. 서명할 데이터
2. 개인키

서명 작업은 트랜잭션 입력에 저장될 서명을 생성한다. 서명 검증을 위해선 다음의 것들이 필요하다.

1. 서명된 데이터
2. 서명
3. 공개키

간단히 말하면 검증 과정은 다음과 같다: 이 데이터에서 얻은 서명이 공개키 생성에 사용된 개인키로 서명되었는지를 확인한다.

> 디지털 서명은 암호화가 아니기 때문에 서명에서 데이터를 재구성할 수 없다. 이는 해싱 알고리즘에 데이터를 넣어 데이터의 고유한 표현을 얻어내는 해싱과 유사하다. 서명과 해시의 차이점은 키 쌍으로 이는 서명 검증을 가능하게 만든다.
> 그러나 키 쌍은 데이터 암호화에도 사용될 수 있는데 개인키는 암호화, 공개키는 복호화에 사용된다. 하지만 비트코인은 암호화 알고리즘을 사용하지 않는다.

비트코인에서 모든 트랜잭션 입력은 트랜잭션을 생성한 사람에 의해 서명된다. 비트코인의 모든 트랜잭션은 블록에 추가되기 전에 반드시 검증되어야한다. 검증이란 다음을 의미한다.

1. 입력이 이전 트랜잭션의 출력을 사용할 권한을 가지고 있는지 확인
2. 트랜잭션 서명이 올바른지 확인

데이터 서명과 서명 검증을 도식으로 나타내면 다음과 같다.



이제 트랜잭션의 전체 생명주기를 살펴보자.

1. 초기에는 코인베이스 트랜잭션을 가진 제네시스 블록이 있다. 코인베이스 트랜잭션에는 입력이 없기 때문에 서명이 필요하지않다. 코인베이스 트랜잭션의 출력은 해싱된 공개키가 포함되어있다. (**RIPEMD16(SHA256(PubKey)** 알고리즘이 사용됨)
2. 누군가 코인을 전송하면 트랜잭션이 생성된다. 트랜잭션의 입력은 이전 트랜잭션의 출력을 참조한다. 모든 입력은 해싱되지 않은 공개키와 전체 트랜잭션의 서명을 저장한다.
3. 트랜잭션을 받는 비트코인 네트워크상의 다른 노드들이 이를 검증한다. 이 노드들은 다른 것들 외에도 입력의 공개키 해시가 참조된 출력의 해시와 같은지 (송신자가 본인 소유의 코인을 사용했는지를 보장)와 서명이 올바른지 (트랜잭션이 코인의 실소유자에 의해 생성되었는지를 보장)를 확인한다.
4. 채굴 노드가 새로운 블록을 채굴할 준비가 되면, 이 노드는 트랜잭션을 블록에 넣은 뒤 채굴을 시작한다.
5. 블록이 채굴되면 네트워크상의 모든 노드는 블록이 채굴되었다는 메시지를 받으며 블록체인에 블록을 추가한다.
6. 블록이 블록체인에 추가되면 트랜잭션이 완료되고 출력은 새로운 트랜잭션에서 참조될 수 있다.

# 타원 곡선 암호

위에서 설명한 것처럼 공개키와 개인키는 임의의 바이트 시퀀스이다. 개인키는 코인의 소유자를 식별하기 위해 사용되기 때문에 랜덤 알고리즘은 완전히 랜덤한 바이트를 생성할 수 있어야한다. 다른 사람이 소유하고 있는 개인키를 생성함으로써 충돌이 발생해선 안된다.

비트코인은 타원 곡선을 사용해 개인키를 생성한다. 타원 곡선은 복잡한 수학적 개념으로 여기서는 자세히 다루진 않는다. (궁금하다면 [타원 곡선 소개글](http://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction/)을 참고하라) 우리가 알아야 할 것은 이 곡선을 사용하면 아주 크고 랜덤한 숫자를 생성할 수 있다는 것이다. 비트코인에서 사용되는 곡선은 0부터 2^(2^56) (이는 10^77에 근사하는 값이며 우주에는 10^78개에서 10^82개 사이의 원자가 존재한다) 사이의 숫자를 무작위로 선택할 수 있다. 이렇게 높은 상한을 사용하면 동일한 개인키를 두 번 생성하기는 거의 불가능하다.

또한, 비트코인은 ECDSA (Elliptic Curve Digital Signature Algorithm, 타원 곡선 디지털 서명 알고리즘) 알고리즘을 사용해 트랜잭션에 서명을 한다. 우리도 이 알고리즘을 사용할 것이다.

# Base58

이제 다시 위에서 언급했던 비트코인 주소로 돌아가자: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa. 우리는 이제 이 값이 사람이 읽을 수 있는 형태의 공개키임을 알고있다. 이를 디코딩해 나온 공개키를 16진법의 바이트 시퀀스로 나타내면 다음과 같다.

```
0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
```

비트코인은 공개키를 사람이 읽을 수 있는 형태로 변환하기 위해 Base58 알고리즘을 사용한다. 이 알고리즘은 잘 알려진 Base64 알고리즘과 아주 유사하지만 문자 유사성을 활용한 공격을 피하기 위해 알파벳에서 일부 문자를 제거한 버전을 사용한다. 그래서 Base58에는 유사한 문자들인 0 (제로), O (대문자 o), I (대문자 i), l (소문자 L)가 없다. 그리고 + 및 / 문자 또한 없다.

공개키로부터 주소를 얻는 과정을 도식화하면 다음과 같다.



즉, 좀 전에 언급한 디코딩된 공개키는 다음과 같이 세 부분으로 구성되어있다.

```
Version  Public key hash                           Checksum
00       62E907B15CBF27D5425399EBF6F0FB50EBB88F18  C29B7D93
```

해싱 함수는 단방향이기 때문에 해시로부터 공개키를 추출하는건 불가능하다. 그러나 해시 함수에 키를 넣어 해시를 만든 뒤 두 해시를 비교함으로써 해시에 공개키가 사용되었는지를 확인할 수 있다.

이론은 다 살펴보았으니 이제 코드를 작성할 시간이다. 코드를 작성하다보면 개념들을 좀 더 이해하기 쉬울 것이다.

# 주소 구현

**Wallet** 구조체부터 작성해보자.

```go
type Wallet struct {
        PrivateKey scdsa.PrivateKey
        PublicKey  []byte
}

type Wallets struct {
        Wallets map[string]*Wallet
}

func NewWallet() *Wallet {
        private, public = newKeyPair()
        wallet := Wallet{private, public}
        return &wallet
}

func newKeyPair() (ecdsa.PrivateKey, []byte) {
        curve := elliptic.P256()
        private, err := ecdsa.GenerateKey(curve, rand.Reader)
        pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)
        return *private, pubKey
}
```

지갑은 단순히 키의 쌍이다. 지갑들을 하나의 파일로 저장하고 불러올 수 있도록 **Wallets** 타입을 만든다. **Wallet**의 생성자에서 새로운 키 쌍이 생성된다. **newKeyPair** 함수는 직관적인데 ECDSA는 타원 곡선 기반이기 때문에 타원 곡선이 하나 필요하다. 다음으로, 곡선을 이용해 개인키가 생성되고 개인키로부터 공개키가 생성된다. 여기서 주목해야할 것은 타원 곡선 기반 알고리즘에서 공개키들은 곡선 위의 점들이라는 것이다. 따라서 공개키는 X, Y 좌표의 조합이다. 비트코인에서 이 좌표들은 하나로 연결되어 공개키를 형성한다.

이제 주소를 생성해보자.

```go
func (w Wallet) GetAddress() []byte {
        pubKeyHash := HashPubKey(w.PublicKey)
        versionedPayload := append([]byte{version}, pubKeyHash...)
        checksum := checksum(versionedPayload)

        fullPayload := append(versionedPayload, checksum...)
        address := Base58Encode(fullPayload)
        return address
}

func HashPubKey(pubKey []byte) []byte {
        publicSHA256 := sha256.Sum256(pubKey)

        RIPEMD160Hasher := ripemd160.New()
        _, err := RIPEMD160Hasher.Write(publicSHA256[:])
        publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)
        return publicRIPEMD160
}

func checksum(payload []byte) []byte {
        firstSHA := sha256.Sum256(payload)
        secondSHA := sha256.Sum256(firstSHA[:])
        return secondSHA[:addressChecksumLen]
}
```

공개키는 다음 단계들을 통해 Base58 주소로 변환된다.

1. 공개키를 **RIPEMD(SHA256(PubKey))** 해싱 알고리즘으로 두 번 해싱한다.
2. 해시 접두에 주소 생성 알고리즘의 버전을 추가한다.
3. 위 단계에서 나온 해시를 **SHA256(SHA256(payload))**로 해싱하여 체크섬을 계산한다.
4. 체크섬을 **version + PubKeyHash** 조합의 접미에 추가한다.
5. **version + PubKeyHash + checksum** 조합을 Base58로 해싱한다.

이 결과로 여러분은 **실제 비트코인 주소**를 얻을 수 있으며 [blockchain.info](https://blockchain.info/)에서 잔고도 확인할 수 있다. 하지만 필자는 여러분이 새 주소를 생성하고 잔고를 몇번이고 확인해봐도 잔고가 항상 0임을 확신할 수 있다. 이게 바로 적절한 공개키 암호화 알고리즘의 선택이 중요한 이유이다. 개인키를 임의의 수라고 생각하면 동일한 숫자를 생성할 확률은 가능한한 낮아야하며 이상적으로는 절대 발생해서는 안된다.

또한 주소를 얻기 위해 비트코인 노드에 연결할 필요는 없다는 점에 유의하라. 주소 생성 알고리즘은 많은 프로그래밍 언어와 라이브러리에 구현되어있는 공개된 알고리즘들을 조합하여 사용하고있다.

이제 트랜잭션이 주소를 사용할 수 있도록 입력과 출력을 수정해야한다.

```go
type TXIntput struct {
        Txid      []byte
        Vout      int
        Signature []byte
        PubKey    []byte
}

func (in *TXInput) UsesKey(pubKeyHash []byte) bool {
        lockingHash := HashPubKey(in.PubKey)
        return bytes.Compare(lockingHash, pubKeyHash) == 0
}

type TXOutput struct {
        Value      int
        PubKeyHash []byte
}

func (out *TXOutput) Lock(address []byte) {
        pubKeyHash := Base58Decode(address)
        pubKeyHash := pubKeyHash(1 : len(pubKeyHash)-4)
        out.PubKeyHash = pubKeyHash
}

func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool {
        return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0
}
```

스크립팅 언어를 구현하지는 않으므로 더 이상 **ScriptPubKey**와 **ScriptSig** 필드는 사용하지 않을 것이다. 대신 **ScriptSig**는 **Signature**와 **PubKey** 필드로 분할되고 **ScriptPubKey**는 **PubKeyHash**로 바뀐다. 우리는 비트코인과 동일한 출력 잠금/해제 및 입력 서명 로직을 구현할 것이다. 다만 우리는 메서드를 사용할 것이다.

**UsesKey** 메서드는 입력이 출력을 해제할 수 있는 특정한 키를 사용하고 있는지 검사한다. 입력은 해싱되지 않은 공개키를 저장하고 있는데 메서드는 해싱된 값을 받고 있음을 보라. **IsLockedWithKey**는 제공된 공개키 해시가 출력을 잠그는데 사용되었는지를 검사한다. 이는 **UsesKey**와는 대조적이며 이 두 함수 모두 **FindUnspentTransaction**에서 트랜잭션간의 연결을 구성하는데 사용된다.

**Lock**은 출력을 잠그기만 한다. 코인을 다른 사람에게 전송할 때 우리는 주소밖에 모르기 때문에 함수는 인자로 주소만 받는다. 주소가 디코딩되고 공개키 해시가 추출되면 **PubKeyHash** 필드에 저장된다.

잘 동작하는지 확인해보자.

```
$ blockchain_go createwallet
Your new address: 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt

$ blockchain_go createwallet
Your new address: 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h

$ blockchain_go createwallet
Your new address: 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy

$ blockchain_go createblockchain -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
0000005420fbfdafa00c093f56e033903ba43599fa7cd9df40458e373eee724d

Done!

$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
Balance of '13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt': 10

$ blockchain_go send -from 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -to 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -amount 5
2017/09/12 13:08:56 ERROR: Not enough funds

$ blockchain_go send -from 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -to 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -amount 6
00000019afa909094193f64ca06e9039849709f5948fbac56cae7b1b8f0ff162

Success!

$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
Balance of '13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt': 4

$ blockchain_go getbalance -address 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h
Balance of '15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h': 6

$ blockchain_go getbalance -address 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy
Balance of '1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy': 0
```

잘 동작한다! 이제 트랜잭션 서명을 구현해보자.

# 서명 구현

트랜잭션은 반드시 서명되어야하는데 이는 비트코인에서 특정인이 다른 사람의 코인을 소비할 수 없도록 보장하는 유일한 방법이기 때문이다. 서명이 유효하지 않으면 트랜잭션 또한 유효하지 않다고 판단되므로 따라서 블록체인에 추가될 수 없다.

우리는 하나를 제외하고 트랜잭션 서명을 구현하는데 필요한 것들을 모두 갖췄다. 그 하나는 바로 서명을 위한 데이터이다. 트랜잭션에서 실제로 서명되는건 무엇일까? 혹은 트랜잭션 전체가 서명되는것인가? 서명할 데이터를 선택하는건 아주 중요한 문제이다. 서명해야할 데이터에는 고유한 방식으로 데이터를 식별하는 정보가 포함되어야한다. 예를 들어, 단순히 출력값을 서명하는건 의미가 없는데 이 서명에는 전송 및 수신자에 대한 정보가 없기 때문이다.

트랜잭션이 이전 출력의 잠금을 해제하고 값을 재분배하고 새로운 출력을 잠궈야함을 고려한다면 다음의 데이터들이 서명되어야한다.

1. 잠금 해제된 출력에 저장된 공개키 해시. 이는 트랜잭션의 "전송자"를 식별한다.
2. 새롭게 잠긴 출력에 저장된 공개키 해시. 이는 트랜잭션의 "수신자"를 식별한다.
3. 새로운 출력의 값

> 비트코인에서 잠금/해제 로직은 스크립트 형태로 저장되며 이는 각각 입력과 출력의 **ScriptSig**와 **ScriptPubKey** 필드에 저장된다. 비트코인은 여러가지 유형의 스크립트를 허용하기 때문에 **ScriptPubKey** 전체를 서명한다.

보시다시피 입력에 저장된 공개키는 서명할 필요가 없다. 이 때문에 비트코인에서 서명되는건 트랜잭션이 아니라 참조하고있는 출력들의 **ScriptPubKey**를 저장하고 있는 입력의 데이터가 포함된 잘려진 (Trimmed) 복사본이다.

> 잘려진 트랜잭션 복사본을 얻는 자세한 과정은 [여기](https://en.bitcoin.it/wiki/File:Bitcoin_OpCheckSig_InDetail.png)를 참조하라. 오래된 문서이지만 더 신뢰할 수 있는 정보원을 찾을 수 없었다.

복잡해보이니 코드를 한 번 작성해보자. **Sign** 메서드부터 구현해보겠다.

```go
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
        if tx.IsCoinbase() {
                return
        }

        txCopy := tx.TrimmedCopy()
        for inID, vin := range txCopy.Vin {
                prevTx := prevTxs[hex.EncodeToString(vin.Txid)]
                txCopy.Vin[inID].Signature = nil
                txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
                txCopy.ID = txCopy.Hash()
                txCopy.Vin[inID].PubKey = nil

                r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
                signature := append(r.Bytes(), s.Bytes()...)
                tx.Vin[inID].Signature = signature
        }
}
```

이 메서드는 개인키와 이전 트랜잭션들의 집합을 인자로 받는다. 이전에도 언급했듯이 트랜잭션을 서명하기 위해선 트랜잭션 입력이 참조하고 있는 이전 출력의 값들이 필요하기 때문에 이 출력들을 가지고 있는 트랜잭션 집합이 필요하다.

한 줄씩 살펴보자.

```go
if tx.IsCoinbase() {
        return
}
```

코인베이스 트랜잭션에는 실제 입력값이 없기 때문에 서명하지 않는다.

```go
txCopy := tx.TrimmedCopy()
```

전체 트랜잭션이 아닌 잘려진 복사본이 서명된다.

```go
func (tx *Transaction) TrimmedCopy() Transaction {
        var inputs []TXInput
        var outputs []TXOutput

        for _, vin := range tx.Vin {
                inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil})
        }

        for _, vout := range tx.Vout {
                outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash})
        }

        txCopy := Transaction{tx.ID, inputs, outputs}
        return txCopy
}
```

복사본에는 모든 입력과 출력이 포함되지만 **TXInput.Signature**와 **TXInput.PubKey**는 nil로 설정된다.

다음으로 복사본의 각 입력을 순회한다.

```go
for inID, vin := range txCopy.Vin {
        prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
        txCopy.Vin[inID].Signature = nil
        txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
```

각 입력을 순회할때마다 **Signature**는 **nil**로 **PubKey**는 참조하고있는 출력의 **PubKeyHash**로 설정된다. 이 순간, 현재 트랜잭션을 제외한 모든 트랜잭션은 **Signature**와 **PubKey** 필드가 nil로 설정된 "빈" 상태이다. 따라서 **입력은 별도로 서명된다**. 우리가 만들 애플리케이션에서는 필요없지만 비트코인에서는 트랜잭션이 다른 주소를 참조하는 입력을 포함하는 트랜잭션을 허용한다.

```go
        txCopy.ID = txCopy.Hash()
        txCopy.Vin[inID].PubKey = nil
```

**Hash** 메서드는 트랜잭션을 직렬화하여 SHA-256 알고리즘으로 해싱한다. 해싱된 값이 우리가 서명할 데이터이다. 해시를 얻고 나면 이후 순회에 영향이 없도록 **PubKey** 필드를 초기화 해야한다.

이제 핵심 로직을 살펴보자.

```go
        r, s, err := ecdsa.Sign(rand.Reader, *privKey, txCopy.ID)
        signature := append(r.Bytes(), s.Bytes()...)
        tx.Vin[inID].Signature = signature
```

**privKey**로 **txCopy.ID**를 서명하고있다. ECDSA 서명은 한 쌍의 숫자로 입력의 **Signature** 필드에 조합하여 저장한다.

이제 검증 함수를 작성해보자.

```go
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
        txCopy := tx.TrimmedCopy()
        curve := elliptic.P256()

        for inID, vin := range tx.Vin {
                prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
                txCopy.Vin[inID].Signature = nil
                txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
                txCopy.ID = txCopy.Hash()
                txCopy.Vin[inID].PubKey = nil

                r := big.Int{}
                s := big.Int{}
                sigLen := len(vin.Signature)
                r.SetBytes(vin.Signature[:(sigLen / 2)])
                s.SetBytes(vin.Signature[(sigLen / 2):])

                x := big.Int{}
                y := big.Int{}
                keyLen := len(vin.PubKey)
                x.setBytes(vin.PubKey[:(keyLen / 2)])
                y.setBytes(vin.PubKey[(keyLen / 2):])

                rawPubKey := ecdsa.PublicKey{curve, &x, &y}
                if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
                        return false
                }
        }
        return false
}
```

메서드는 아주 직관적이다. 우선, 동일한 트랜잭션 복사본이 필요하다.

```go
txCopy := tx.TrimmedCopy()
```

다음으로 키 쌍을 생성할 때 사용된 것과 동일한 곡선이 필요하다.

```go
curse := elliptic.P256()
```

그 다음 각 입력의 서명을 검사한다.

```go
for inID, vin := range tx.Vin {
        prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
        txCopy.Vin[inID].Signature = nil
        txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
        txCopy.ID = txCopy.Hash()
        txCopy.Vin[inID].PubKey = nil
```

이 부분은 **Sign** 메서드와 동일한데 검증을 위해선 동일하게 서명된 데이터가 필요하기 때문이다.

```go
        r := big.Int{}
        s := big.Int{}
        sigLen := len(vin.Signature)
        r.SetBytes(vin.Signature[:(sigLen / 2)])
        s.SetBytes(vin.Signature[(sigLen / 2):])

        x := big.Int{}
        y := big.Int{}
        keyLen := len(vin.PubKey)
        x.SetBytes(vin.PubKey[:(keyLen / 2)])
        y.SetBytes(vin.PubKey[(keyLen / 2):])
```

서명은 한 쌍의 숫자이며 공개키는 한 쌍의 좌표이므로 **TXInput.Signature**와 **TXInput.PubKey**에 저장된 값들을 풀어내자. 우리는 값을 저장하기 위해 이전에 이 쌍의 값들을 조합했다. 이제는 **crypto/ecdsa** 함수에서 사용하기 위해 이 값들을 풀어내야한다.

```go
        rawPubKey := ecdsa.PublicKey{curve, &x, &y}
        if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
                return false
        }
}

return true
```

입력에서 추출한 공개키를 사용해 **ecdsa.PublicKey**을 생성하고 추출한 서명을 파라미터로 전달해 **ecdsa.Verify**를 실행한다. 모든 입력이 검증되면 true를 반환하며 하나라도 실패하면 false를 반환한다.

이제 우리는 이전 트랜잭션들을 가져오는 함수가 필요하다.이 함수는 블록체인과의 인터랙션이 필요하기 때문에 **Blockchain**의 메서드로 만들 것이다.

```go
func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {
        bci := bc.Iteractor()
        for {
                block := bci.Next()
                for _, tx := block.Transactions {
                        if bytes.Compare(tx.ID, ID) == 0 {
                                return *tx, nil
                        }

                        if (block.PrevBlockHash) == 0 {
                                break
                        }
                }
        }
        return Transaction{}, errors.New("Transaction is not found")
}

func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
        prevTXs := make(map[string]Transaction)

        for _, vin := range tx.Vin {
                prevTX, err := bc.FindTransaction(vin.Txid)
                prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
        }

        tx.Sign(privKey, prevTXs)
}

func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
        prevTXs := make(map[string]Transaction)

        for _, vin := range tx.Vin {
                prevTX, err := bc.FindTransaction(vin.Txid)
                prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
        }

        tx.Sign(privKey, prevTXs)
}
```

이 함수들은 아주 간단하다. **FindTransaction**은 ID로 트랜잭션을 검색한다 (블록체인의 모든 블록을 순회해야함). **SignTransaction**은 트랜잭션을 받아 참조하고 있는 트랜잭션들을 찾아 서명한다. **VerifyTransaction**은 동일한 일을 하나 서명이 아닌 검증 작업을 수행한다.

이제 실제로 트랜잭션에 서명을 하고 검증을 해봐야한다. 서명은 **NewUTXOTransaction**에서 이루어진다.

```go
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
        ...
        tx := Transaction{nil, inputs, outputs}
        tx.ID = tx.Hash()
        bc.SignTransaction(&tx, wallet.PrivateKey)
        return &tx
}
```

검증은 트랜잭션이 블록에 추가되기 전에 수행된다.

```go
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
        var lastHash []byte

        for _, tx := range transactions {
                if bc.VerifyTransaction(tx) != true {
                        log.Panic("ERROR: Invalid transaction")
                }
        }
}
```

끝났다! 잘 동작하는지 한 번 더 확인해보자.

```
$ blockchain_go createwallet
Your new address: 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR

$ blockchain_go createwallet
Your new address: 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab

$ blockchain_go createblockchain -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008

Done!

$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 6
0000000f3dbb0ab6d56c4e4b9f7479afe8d5a5dad4d2a8823345a1a16cf3347b

Success!

$ blockchain_go getbalance -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
Balance of '1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR': 4

$ blockchain_go getbalance -address 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab
Balance of '1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab': 6
```

문제없이 잘 동작한다!

서명되지 않은 트랜잭션은 채굴할 수 없음을 확인해보기 위해 **NewUTXOTransaction**에서 **bc.SignTransaction(&tx, wallet.PrivateKey)** 호출을 주석 처리 해보자.

```go
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
        ...
        tx := Transaction{nil, inputs, outputs}
        tx.ID = tx.Hash()
        // bc.SignTransaction(&tx, wallet.PrivateKey)

        return &tx
}
```

```
$ go install
$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 1
2017/09/12 16:28:15 ERROR: Invalid transaction
```

# 결론

우리는 지금까지 비트코인의 많은 핵심 기능들을 구현했다. 이제 네트워킹을 제외한 거의 모든것이 구현되었다. 다음 파트에서는 트랜잭션을 마무리 할 것이다.

---

## File: content/post/2018-06-14-translation-building-blockchain-in-go-part-6.md

---
categories:
- translation
- blockchain
- tutorial
comments: true
date: 2018-06-14T00:00:00Z
tags:
- blockchain
- transaction
title: "[Go로 구현하는 블록체인] Part 6: 트랜잭션 2"
url: /building-blockchain-in-go-part-6
---

> [Building Blockchain in Go](https://jeiwan.cc/posts/building-blockchain-in-go-part-6) 시리즈를 번역한 글입니다.

# 서론

시리즈 초반부에 필자는 블록체인은 분산된 데이터베이스라고 말한 적이 있다. 우리는 지금까지 "분산" 부분은 건너뛰고 "데이터베이스" 부분에 집중했었다. 또한 여태 블록체인 데이터베이스를 이루는 대부분의 것들을 구현해왔다. 이 파트에서는 이전 파트에서 건너뛴 메커니즘들을 다루고, 다음 파트에서는 블록체인의 분산된 특성에 대해 다뤄볼 것이다.

이전 파트들:

1. [기본 프로토타입](/building-blockchain-in-go-part-1)
2. [작업 증명](/building-blockchain-in-go-part-2)
3. [영속성 및 CLI](/building-blockchain-in-go-part-3)
4. [트랜잭션 1](/building-blockchain-in-go-part-4)
5. [주소](/building-blockchain-in-go-part-5)

> 이 파트에서는 중요한 코드 변경사항을 소개하기 때문에 모든 코드를 설명하는건 의미가 없다. 모든 변경사항은 [여기](https://github.com/Jeiwan/blockchain_go/compare/part_5...part_6#files_bucket)를 참조하라.

# 보상

이전 글에서 생략한 것중 하나는 바로 채굴에 대한 보상이다. 우리는 보상을 구현하기 위해 필요한 모든것들을 이미 구현했다.

보상은 단순히 코인베이스 트랜잭션일뿐이다. 노드를 채굴하면 새로운 블록의 채굴이 시작되고, 큐에 있는 트랜잭션을 가져와 그 앞에 코인베이스 트랜잭션을 붙인다. 코인베이스 트랜잭션의 유일한 출력에는 채굴자의 공개키 해시가 포함된다.

보상 구현은 **send** 커맨드를 수정하는 정도로 아주 쉽다.

```go
func (cli *CLI) send(from, to string, amount int) {
        ...
        bc := NewBlockchain()
        UTXOSet := UTXOSet{bc}
        defer bc.db.Close()

        tx := NewUTXOTransaction(from, to, amount, &UTXOSet)
        cbTx := NewCoinbaseTX(from, "")
        txs := []*Transaction{cbTx, tx}

        newBlock := bc.MineBlock(txs)
        fmt.Println("Success!")
}
```

우리가 구현한 블록체인에선 트랜잭션을 생성한 사람이 새로운 블록을 채굴하고 보상을 받는다.

# UTXO 집합

[Part 3. 영속성 및 CLI](/building-blockchain-in-go-part-3)에서는 비트코인 코어가 데이터베이스에 블록을 저장하는 방법에 대해 알아보았다. 블록들은 **blocks** 데이터베이스에, 트랜잭션 출력은 **chainstate** 데이터베이스에 저장된다고 했었다. **chainstate**의 구조를 다시 한 번 살펴보자.

1. **'c' + 32바이트 트랜잭션 해시 -> 해당 트랜잭션에 대한 미사용 트랜잭션 출력 레코드**
2. **'B' -> 32바이트 블록 해시: 데이터베이스가 미사용 트랜잭션 출력을 나타내는 블록 해시**

우리는 이미 트랜잭션을 구현했지만, 출력을 저장하기 위해 **chainstate**를 사용하지 않았다. 이것이 지금 우리가 하려는 것이다.

**chainstate**는 트랜잭션을 저장하지 않는다. 대신 UTXO 집합 또는 미사용 트랜잭션 출력의 집합을 저장한다. 이 외에도 "데이터베이스가 미사용 트랜잭션 출력을 나타내는 블록 해시"를 저장하지만, 우리는 지금 블록 높이를 사용하고 있지 않기 때문에 이 부분은 생략할 것이다. (다음 파트에서 구현한다)

그렇다면 UTXO 집합은 왜 저장하려는걸까?

이전에 구현했던 **Blockchain.FindUnspentTransactions** 메서드를 생각해보자.

```go
func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction {
        ...
        bci := bc.Iterator()
        for {
                block := bci.Next()
                for tx := range block.Transactions {
                        ...
                }

                if len(block.PrevBlockHash) == 0 {
                        break
                }
        }
}
```

이 함수는 미사용 출력을 갖는 트랜잭션을 검색한다. 트랜잭션은 블록에 저장되어 있기 때문에 블록체인의 블록들을 순회하면서 블록 안에 있는 모든 트랜잭션을 확인한다. 2017년 9월 18일 기준으로 비트코인에는 485,860개의 블록들이 있으며 전체 데이터베이스의 크기는 약 140+ Gb에 달한다. 이는 트랜잭션 검증을 위해선 모든 노드를 실행시켜야함을 의미한다. 게다가 트랜잭션 검증은 많은 블록을 순회해야할 수도 있다.

이 문제에 대한 해결책은 미사용 출력만을 저장하는 인덱스를 만드는 것이다. 이게 바로 UTXO 집합이 필요한 이유이다. 이는 모든 블록체인 트랜잭션으로부터 만들어진 캐시인 셈이다 (물론 처음에는 블록들을 순회해야하지만, 이는 딱 한 번만 이루어진다). 그리고 이 인덱스는 나중에 잔고 계산 및 새로운 트랜잭션의 검증에서도 사용된다. UTXO 집합은 2017년 9월 기준으로 약 2.7 Gb정도 된다.

이제 UTXO 집합을 구현하기 위해 어떤걸 수정해야할지 생각해보자. 현재 우리는 트랜잭션 검색을 위해 다음 메서드들을 사용하고 있다.

1. **Blockchain.FindUnspentTransactions** - 미사용 출력을 가진 트랜잭션을 검색하는 주요 함수. 모든 블록에 대한 순회가 일어나는 곳.
2. **Blockchain.FindSpendableOutputs** - 새로운 트랜잭션이 생성될 때 사용되는 함수. 필요한 수량을 채울 수 있을만큼의 출력을 검색. **Blockchain.FindUnspentTransactions**를 사용한다.
3. **Blockchain.FindUTXO** - 주어진 공개키 해시에 대 한 미사용 출력을 검색하며 잔고를 계산할 때 사용됨. **Blockchain.FindUnspentTransactions**를 사용한다.
4. **Blockchain.FindTransaction** - ID로 블록체인에서 트랜잭션을 검색. 찾을 때까지 모든 블록을 순회한다.

보다시피, 모든 메서드가 데이터베이스상의 모든 블록을 순회하고 있다. 하지만 지금은 UTXO 집합이 모든 트랜잭션을 저장하고 있지는 않기 때문에 위의 모든 메서드들을 개선할 수는 없고 미사용 출력을 사용하는 메서드만 개선이 가능하다. 따라서 **Blockchain.FindTransaction**는 예외로 한다.

우리는 이제 다음 메서드들을 구현할 것이다.

1. **Blockchain.FindUTXO** - 블록들을 순회하면서 모든 미사용 출력들을 검색한다.
2. **UTXOSet.Reindex** - 미사용 출력을 찾기 위해 **FindUTXO**를 사용하며 검색 결과를 데이터베이스에 저장한다. 캐싱이 발생하는 곳이다.
3. **UTXOSet.FindSpendableOutputs** - **Blockchain.FindSpendableOutputs**과 유사하나 UTXO 집합을 사용한다.
4. **Blockchain.FindTransaction** - 기존과 동일하다.

가장 많이 사용되는 두 함수는 지금부터 캐시를 사용할 것이다. 이제 코드를 작성해보자.

```go
type UTXOSet struct {
        Blockchain *Blockchain
}
```

우리는 단일 데이터베이스를 사용할거지만 UTXO 집합은 다른 버킷에 저장할 것이다. **UTXOSet**은 **Blockchain**과 결합되어 있다.

```go
func (u UTXOSet) Reindex() {
        db := u.Blockchain.db
        bucketName := []byte(utxoBucket)

        err := db.Update(func(tx *bolt.Tx) error {
                err := tx.DeleteBucket(bucketName)
                _, err = tx.CreateBucket(bucketName)
        })

        UTXO := u.Blockchain.FindUTXO()

        err = db.Update(func(tx *bolt.Tx) error {
                b := tx.Bucket(bucketName)

                for txID, outs := range UTXO {
                        key, err := hex.DecodeString(txID)
                        err = b.Put(key, outs.Serialize())
                }
        })
}
```

이 메서드는 처음에 UTXO 집합을 생성한다. 버킷이 이미 존재하면 삭제후, 블록체인에서 모든 미사용 출력을 가져와 버킷에 저장한다.

**Blockchain.FindUTXO**는 **Blockchain.FindUnspentTransactions**와 거의 유사하나 이 메서드는 **트랜잭션 아이디 -> 트랜잭션 출력** 쌍들의 맵을 반환한다.

이제 UTXO 집합을 사용해 코인을 전송할 수 있다.

```go
func (u UTXOSet) FindSpendableOutputs(pubKeyHash []byte, amount int) (int, map[string][]int) {
    unspentOutputs := make(map[string][]int)
    accumulated := 0
    db := u.Blockchain.db

    err := db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(utxoBucket))
        c := b.Cursor()

        for k, v := c.First(); k != nil; k, v = c.Next() {
            txID := hex.EncodeToString(k)
            outs := DeserializeOutputs(v)

            for outIdx, out := outs.Outputs {
                if out.IsLockedWithKey(pubKeyHash) && accumulated < amount {
                    accumulated += out.Value
                    unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)
                }
            }
        }
    })
    return accumulated, unspentOutputs
}
```

또는 잔고를 확인할 수도 있다.

```go
func (u UTXOSet) FindUTXO(pubKeyHash []byte) []TXOutput {
        var UTXOs []TXOutput
        db := u.Blockchain.db

        err := db.View(func(tx *bolt.Tx) error {
                b := tx.Bucket([]byte(utxoBucket))
                c := b.Cursor()

                for k, v := c.First(); k != nil; k, v = c.Next() {
                        outs := DeserializeOutputs(v)

                        for _, out := range outs.Outputs {
                                if out.IsLockedWithKey(pubKeyHash) {
                                        UTXOs = append(UTXOs, out)
                                }
                        }
                }
                return nil
        })
        return UTXOs
}
```

이들은 대응되는 **Blockchain** 메서드들의 약간 수정된 버전이다. **Blockchain**의 해당 메서드들은 더이상 필요없다.

UTXO 집합을 따로 둔다는건 트랜잭션 데이터가 두 개의 저장소로 나뉘어졌음을 의미한다. 실제 트랜잭션은 블록체인에 저장되고, 미사용 출력은 UTXO 집합에 저장된다. 이러한 분리에는 견고한 동기화 메커니즘이 필요한데 UTXO 집합을 항상 업데이트하고 최신 트랜잭션의 출력도 저장해야하기 때문이다. 그러나 빈번한 블록체인 스캔은 피하고싶기 때문에 새로운 블록이 채굴될 때마다 재색인을 하고싶지는 않다. 따라서 UTXO 집합을 업데이트하는 메커니즘이 필요하다.

```go
func (u UTXOSet) Update(block *Block) {
    db := u.Blockchain.db

    err := db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(utxoBucket))

        for _, tx := range block.Transactions {
            if tx.IsCoinbase() == false {
                for _, vin := range tx.Vin {
                    updatedOuts := TXOutputs{}
                    outsBytes := b.Get(vin.Txid)
                    outs := DeserializeOutputs(outsBytes)

                    for outIdx, out := range outs.Outputs {
                        if outIdx != vin.Vout {
                            updatedOuts.Outputs = append(updatedOuts.Outputs, out)
                        }
                    }

                    if len(updatedOuts.Outputs) == 0 {
                        err := b.Delete(vin.Txid)
                    } else {
                        err := b.Put(vin.Txid, updatedOuts.Serialize())
                    }
                }
            }

            newOutputs := TXOutputs{}
            for _, out := range tx.Vout {
                newOutputs.Outputs = append(newOutputs.Outputs, out)
            }

            err := b.Put(tx.ID, newOutputs.Serialize())
        }
    })
}
```

복잡해보이지만 직관적인 코드이다. 새로운 블록이 채굴되면 UTXO 집합은 갱신되어야한다. 갱신이란 사용된 출력을 제거하고 새로 채굴된 트랜잭션의 미사용 출력을 추가하는 것을 의미한다. 어떤 트랜잭션의 출력이 모두 제거되고 더 이상의 미사용 출력이 없다면 트랜잭션은 제거된다.

이제 필요한 곳에서 UTXO 집합을 사용해보자.

```go
func (cli *CLI) createBlockchain(address string) {
        ...
        bc := CreateBlockchain(address)
        defer bc.db.Close()

        UTXOSet := UTXOSet{bc}
        UTXOSet.Reindex()
        ...
}
```

재색인은 새로운 블록체인이 생성된 직후에 수행된다. 지금은 이 메서드가 **Reindex**가 사용되는 유일한 위치이다. 블록체인의 초반에는 하나의 트랜잭션만을 갖는 하나의 블록밖에 없기 때문에 재색인을 사용하는건 과해보이고 대신 **Update**를 사용할 수도 있으나, 나중에 가면 재색인 메커니즘이 필요할 것이다.

```go
func (cli *CLI) send(from, to string, amount int) {
        ...
        NewBlock := bc.MineBlock(txs)
        UTXOSet.Update(newBlock)
}
```

그리고 UTXO 집합은 새로운 블록이 채굴되면 갱신된다.

잘 동작하는지 확인해보자.

```
$ blockchain_go createblockchain -address 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1
00000086a725e18ed7e9e06f1051651a4fc46a315a9d298e59e57aeacbe0bf73

Done!

$ blockchain_go send -from 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1 -to 12DkLzLQ4B3gnQt62EPRJGZ38n3zF4Hzt5 -amount 6
0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b

Success!

$ blockchain_go send -from 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1 -to 12ncZhA5mFTTnTmHq1aTPYBri4jAK8TacL -amount 4
000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433

Success!

$ blockchain_go getbalance -address 1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1
Balance of '1F4MbuqjcuJGymjcuYQMUVYB37AWKkSLif': 20

$ blockchain_go getbalance -address 12DkLzLQ4B3gnQt62EPRJGZ38n3zF4Hzt5
Balance of '1XWu6nitBWe6J6v6MXmd5rhdP7dZsExbx': 6

$ blockchain_go getbalance -address 12ncZhA5mFTTnTmHq1aTPYBri4jAK8TacL
Balance of '13UASQpCR8Nr41PojH8Bz4K6cmTCqweskL': 4
```

잘 동작한다! **1JnMDSqVoHi4TEFXNw5wJ8skPsPf4LHkQ1** 주소의 소유자는 총 3번의 보상을 받았다.

1. 제네시스 블록 채굴시
2. **0000001f75cb3a5033aeecbf6a8d378e15b25d026fb0a665c7721a5bb0faa21b** 블록 채굴시
3. **000000cc51e665d53c78af5e65774a72fc7b864140a8224bf4e7709d8e0fa433** 블록 채굴시

# 머클트리

이 파트에서 다루고자하는 또 하나의 최적화 메커니즘이 있다.

이전에도 언급했듯이, 전체 비트코인 데이터베이스 (블록체인)의 크기는 140 Gb를 웃돈다. 비트코인의 분산된 특성 때문에 네트워크 상의 모든 노드는 독립적이고 자체적이어야 한다. 즉, 모든 노드는 블록체인의 전체 복사본을 가지고 있어야한다. 많은 사람들이 비트코인을 사용하기 시작하면서 이 규칙은 이행하기가 어려워졌다. 모든 사람이 전체 노드를 실행하지는 않는다. 또한 노드들은 네트워크의 자격있는 참여자이므로 트랜잭션과 블록을 검증해야한다는 책임이 있다. 그리고 다른 노드와 상호 작용하고 새로운 블록을 다운로드하기 위해 필요한 특정 인터넷 트래픽이 필요하다.

사토시 나카모토가 발행한 [비트코인 논문의 원문](https://bitcoin.org/bitcoin.pdf)에서는 이 문제를 해결하기 위한 간소화된 지불 검증 (Simplified Payment Verification, SPV)이라는 해결책을 제시하고 있다. SPV는 전체 블록체인을 다운로드 받지 않으며 **블록과 트랜잭션을 검증하지 않는** 가벼운 비트코인 노드이다. 대신 이는 지불 검증을 위해 블록에서 트랜잭션을 검색하고 꼭 필요한 데이터를 얻기 위해 전체 노드와 연결된다. 이 메커니즘을 사용하면 하나의 전체 노드를 실행하면서 여러개의 경량 노드를 사용할 수 있다.

SPV가 가능하려면 전체 블록을 다운로드 받지 않고도 어떤 블록이 특정 트랜잭션을 포함하고 있는지 확인할 수 있는 방법이 필요하다. 머클트리를 사용할 때가 왔다.

머클트리는 비트코인에서 트랜잭션 해시를 얻기 위해 사용되며, 블록 헤더에 저장되고 작업 증명 시스템에서 고려된다. 지금까지 우리는 단순히 한 블록의 각 트랜잭션의 해시를 이어붙여 **SHA-256** 알고리즘을 적용해왔다. 물론 이 방식도 블록 트랜잭션들의 유일한 표현값을 얻기 위한 좋은 방식이지만, 머클트리의 이점을 지니고 있지는 않다.

그럼 이제 머클트리에 대해서 살펴보자.



머클트리는 각 블록마다 생성되며, 트랜잭션 해시 (비트코인은 이중 **SHA256** 해싱을 사용함)가 저장된 리프 (트리의 바닥)로부터 시작된다. 리프의 개수는 반드시 짝수여야만 하지만, 모든 블록이 짝수개의 트랜잭션을 가지고 있지는 않다. 트랜잭션의 개수가 홀수개인 경우, 마지막 트랜잭션이 중복되어 들어간다 (머클트리에만 중복되어 들어가며 블록에 실제로 중복된 트랜잭션이 들어가지는 않음).

아래에서 위로 올라가면서 리프들은 쌍으로 그룹핑된다. 해시는 하나로 연결되며, 연결된 해시값으로부터 새로운 해시를 얻는다. 새로운 해시는 두 트리 노드의 해시로 구성된다. 이 과정은 트리의 루트 노드라고 불리는 단일 노드에 도달할때까지 반복된다.

루트 해시는 트랜잭션들의 유일한 표현값으로 사용되며 블록 헤더에 저장되어 작업 증명 시스템에서 사용된다.

머클트리의 이점은 노드가 전체 블록을 다운로드 받지 않고도 특정 트랜잭션의 포함 여부를 검증할 수 있다는 것이다. 검증 작업에는 트랜잭션의 해시와 머클트리의 루트 해시 그리고 머클 경로만 있으면 된다.

이제 코드를 한 번 작성해보자.

```go
type MerkleTree struct {
        RootNode *MerkleNode
}

type MerkleNode struct {
        Left  *MerkleNode
        Right *MerkleNode
        Data  []byte
}
```

구조체부터 작성한다. 모든 **MerkleNode**는 데이터를 가지며 브랜치 (Left/Right)로 연결된다. **MerkleTree**는 실제로 다음 노드와 연결되어 있는 루트 노드이며, 노드들은 다른 노드와 차례대로 연결된다.

먼저 새로운 노드를 생성해보자.

```go

func NewMerkleNode(left, right *MerkleNode, data []byte) *MerkleNode {
        mNode := MerkleNode{}

        if left == nil && right == nil {
                hash := sha256.Sum256(data)
                mNode.Data = hash[:]
        } else {
                prevHashes := append(left.Data, right.Data...)
                hash := sha256.Sum256(prevHashes)
                mNode.Data = hash[:]
        }

        mNode.left = left
        mNode.right = right
        return &mNode
}
```

모든 노드는 데이터를 가진다. 노드가 리프이면 데이터는 외부에서 전달된다 (우리의 경우 직렬화된 트랜잭션이 외부 데이터에 해당한다). 노드가 다른 노드와 연결되어 있으면, 연결된 노드의 데이터를 가져와 합친 뒤 해싱한다.

```go
func NewMerkleTree(data [][]byte) *MerkleTree {
        var nodes []MerkleNode

        if len(data)%2 != 0 {
                data = append(data, data[len(data)-1])
        }

        for _, datum := range data {
                node := NewMerkleNode(nil, nil, datum)
                nodes = append(nodes, *node)
        }

        for i := 0; i < len(data)/2; i++ {
                var newLevel []MerkleNode

                for j := 0; j < len(nodes); j += 2 {
                        node := NewMerkleNode(&nodes[j], *nodes[j+1], nil)
                        newLevel = append(newLevel, *node)
                }

                nodes = newLevel
        }

        mTree := MerkleTree{&nodes[0]}
        return &mTree
}
```

새 트리가 생성될 때 가장 먼저 확인 해야할 일은 리프의 개수가 짝수개인지 확인하는 것이다. 그 다음엔 **data** (직렬화된 트랜잭션의 배열)를 트리의 리프로 변환한 뒤 리프로부터 트리를 만들어나간다.

이제 작업 증명 시스템에서 트랜잭션 해시를 얻는데 사용되는 **Block.HashTransactions**를 수정해보자.

```go
func (b *Block) HashTransactions() []byte {
        var transactions [][]byte

        for _, tx := range b.Transactions {
                transactions = append(transactions, tx.Serialize())
        }
        mTree := NewMerkleTree(transactions)
        return mTree.RootNode.Data
}
```

트랜잭션은 먼저 직렬화된 다음 (**encoding/gob**을 사용) 머클트리를 만드는데 사용된다. 트리의 루트는 블록 트랜잭션들의 식별자로 사용된다.

# P2PKH

좀 더 자세히 다루고자하는 주제가 하나 더 있다.

여러분도 기억하겠지만, 비트코인에는 트랜잭션 출력을 잠그고 트랜잭션 입력이 출력을 해제하기 위한 데이터를 제공하는데 사용되는 *Script*라는 프로그래밍 언어가 있다. 이 언어는 단순하며 코드는 단순히 데이터와 연산자의 시퀀스로 이루어져있다. 예제를 하나 보자.

```
5 2 OP_ADD 7 OP_EQUAL
```

**5**, **2** 그리고 **7**이 데이터이고, **OP_ADD**와 **OP_EQUAL**이 연산자이다. *Script* 코드는 좌에서 우로 실행된다. 모든 데이터를 스택에 넣고 다음 연산자를 스택의 맨위 원소에 적용한다. *Script*의 스택은 단순한 FILO (First Input Last Output) 메모리 스토리지로 스택의 첫번째 원소는 가장 마지막에 사용되고 모든 나중에 들어온 원소는 이전 원소 위에 추가된다.

위 스크립트의 실행을 단계별로 살펴보자.

1. 스택: 비어있음. 스크립트: **5 2 OP_ADD 7 OP_EQUAL**
2. 스택: **5**. 스크립트: **2 OP_ADD 7 OP_EQUAL**
3. 스택: **5 2**. 스크립트: **OP_ADD 7 OP_EQUAL**
4. 스택: **7**. 스크립트: **7 OP_EQUAL**
5. 스택: **7 7**. 스크립트: **OP_EQUAL**
6. 스택: **true**. 스크립트: 비어있음.

**OP_ADD**는 스택에서 두 개의 원소를 취해 합한 다음 그 합을 스택에 넣는다. **OP_EQUAL**은 스택에서 두 원소를 취해 비교한 다음 같으면 **true**를, 아니면 **false**를 스택에 넣는다. 스크립트 실행 결과는 스택의 맨위 원소의 값이된다. 위 예제의 경우 **true**가 실행 결과가 되며 스크립트가 성공적으로 수행되었음을 알 수 있다.

그럼 이제 비트코인에서 지불을 수행하기 위해 사용되는 스크립트를 살펴보자.

```
<signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
```

이 스크립트를 *Pay to Public Key Hash (P2PKH)*라고 부르며 비트코인에서 가장 흔하게 사용되는 스크립트이다. 이는 말그대로 공개키 해시를 지불한다 (특정한 공개키로 코인을 잠금). 이게 바로 **비트코인 결제의 핵심**이다. 계좌도 없고 계좌간 송금도 없다. 그저 제공된 서명과 공개키가 올바른지를 확인하는 스크립트만 있을뿐이다.

위 스크립트는 실제로 두 부분으로 나뉘어 저장된다.

1. 앞 부분의 **&lt;signature&gt; &lt;pubKey&gt;**는 입력의 **ScriptSig** 필드에 저장된다.
2. 뒷 부분의 **OP_DUP OP_HASH160 &lt;pubKeyHash&gt; OP_EQUALVERIFY OP_CHECKSIG**는 입력의 **ScriptPubKey**에 저장된다.

따라서 출력은 로직 해제를 정의하고 입력은 출력 해제를 위한 데이터를 제공하는 것이다. 스크립트를 실행해보자.

1. 스택: 비어있음. 스크립트: **&lt;signature&gt; &lt;pubKey&gt; OP_DUP OP_HASH160 &lt;pubKeyHash&gt; OP_EQUALVERIFY OP_CHECKSIG**.
2. 스택: **&lt;signature>**. 스크립트: **&lt;pubKey&gt; OP_DUP OP_HASH160 &lt;pubKeyHash&gt; OP_EQUALVERIFY OP_CHECKSIG**.
3. 스택: **&lt;signature&gt; &lt;pubKey&gt;**. 스크립트: **OP_DUP OP_HASH160 &lt;pubKeyHash&gt; OP_EQUALVERIFY OP_CHECKSIG**.
4. 스택: **&lt;signature&gt; &lt;pubKey&gt; &lt;pubKey&gt;**. 스크립트: **OP_HASH160 &lt;pubKeyHash&gt; OP_EQUALVERIFY OP_CHECKSIG**.
5. 스택: **&lt;signature&gt; &lt;pubKey&gt; &lt;pubKeyHash&gt;**. 스크립트: **&lt;pubKeyHash&gt; OP_EQUALVERIFY OP_CHECKSIG**.
6. 스택: **&lt;signature&gt; &lt;pubKey&gt; &lt;pubKeyHash&gt; &lt;pubKeyHash&gt;**. 스크립트: **OP_EQUALVERIFY OP_CHECKSIG**.
7. 스택: **&lt;signature&gt; &lt;pubKey&gt;**. 스크립트: **OP_CHECKSIG**.
8. 스택: **true** 또는 **false**. 스크립트: 비어있음.

**OP_DUP**은 최상위 스택 원소를 복제한다. **OP_HASH160**은 최상위 스택 원소를 가져와 **RIPEMD160**으로 해싱한다. 해싱 결과는 스택에 저장된다. **OP_EQUALVERIFY**는 스택의 두 최상위 원소를 비교하며, 만약 같지 않으면 스크립트에 인터럽트를 건다. **OP_CHECKSIG**는 트랜잭션을 해싱하고 **&lt;signature&gt;**와 **&lt;pubKey&gt;**를 사용해 트랜잭션의 서명을 검증한다. 마지막 연산자는 아주 복잡한데, 이는 트랜잭션의 잘려진 복사본을 만들어 해싱한 뒤 (이 해시가 서명된 트랜잭션의 해시이므로), **&lt;signature&gt;**와 **&lt;pubKey&gt;**를 사용해 서명이 올바른지 검사한다.

이러한 스크립팅 언어를 사용하면 비트코인을 스마트 컨트랙트 플랫폼으로도 사용할 수 있다. 예를 들어, 스크립팅 언어를 사용해 단일키 전송 방법 외에 다른 지불 방법을 구현할 수도 있다.

# 결론

이제 거의 끝났다! 우리는 블록체인 기반 암호화의 거의 모든 핵심 기능들을 구현했다. 그리고 이제 블록체인, 주소, 채굴 그리고 트랜잭션을 갖게되었다. 그러나 이러한 메커니즘들에 생명을 불어넣고 비트코인을 글로벌 시스템으로 만든 또 하나의 기능이 있다. 바로 합의 (consensus)이다. 다음 파트에서는 블록체인의 "분산" 부분을 구현해보도록 하겠다.

---

## File: content/post/2018-06-18-translation-building-blockchain-in-go-part-7.md

[Binary file]

---

## File: content/post/2019-02-01-redis-migration.md

---
categories:
- migration
comments: true
date: 2019-02-01T00:00:00Z
tags:
- infrastructure
- architecture
- migration
- redis
- aws
title: IDC에서 AWS로 Redis 데이터 이전하기
url: /redis-migration
---

작년 3분기 즈음에 내가 맡고 있는 두 개의 게임중 한 게임의 서버 인프라를 IDC에서 AWS로 이전해야하는 일이 생겼다.

기존 인프라 구성은 LB (Load Balancer) 뒤에 게임 서버 (웹서버와 애플리케이션 서버가 공존)가 있고 게임 서버 뒤에는 DB (MySQL) 및 캐시 (Memcached, Redis) 서버가 있는 간단한 3 티어 구조였다. 기존 인프라 구성이라고는 했지만 이전 후의 인프라 구성도 Managed 서비스를 사용한다는 점을 제외하면 전과 동일하다.

중규모 이상의 인프라를 이전해보는건 처음이었는데, 총 세 번의 이전 시도 끝에 전체 인프라 이전을 마칠 수 있었다. 물론 나 혼자 진행한건 아니고 TD 한 분과 함께 작업을 진행했다.

> *TD (Technical Director): 사내 게임 클라이언트/서버의 전반적인 기술 지원과 기술 리딩을 담당하는 시니어 개발자*

우리가 이전해야할 데이터는 다수의 MySQL (~200GB, 로그 제외)과 Redis (~20GB, 33M keys) 서버에 저장된 데이터였는데 이중에 나는 Redis 데이터 이전을 맡게되었다. 이 글에서는 IDC의 Redis 데이터를 어떻게 AWS로 이전했는지에 대한 방법을 소개한 뒤 그에 대한 간단한 회고를 남겨보려고 한다.

# 상황분석

여느 이전 작업들과 마찬가지로 가장 골치 아팠던 부분은 데이터 이전 작업이었다. 그나마 다행이었던건 별도의 데이터 스키마 변환 작업이나 추가적인 동기화 프로세스가 필요한 무중단 이전이 아니었기에 좀 더 수월하게 진행할 수 있었다.

기존 IDC 인프라 구성에서 MySQL과 Redis 서버들은 여느 인프라 구성과 마찬가지로 웹서버에서만 접근할 수 있는 프라이빗 서브넷에 위치해 있었고 이전 해야할 Redis 서버 정보는 다음과 같았다.

* Redis 서버: 37대 (Redis 2.6 / 2.8 혼용)
* 오버헤드를 제외한 총 순수 데이터 용량: 20GB 전후
* 총 키 개수: 33M (33,000,000) 전후

가장 쉬운 방법은 모든 Redis 서버들을 대상으로 AWS ElastiCache 인스턴스로 레플리케이션을 구성하는 것이다. 그러나 IDC에서 Redis가 위치한 프라이빗 서브넷에서는 아웃바운드 또한 막혀 있었기 때문에 위 방법은 사용할 수 없었다. 또한, ElastiCache는 퍼블릭 인바운드 접근이 불가능하며 가능하다해도 레플리케이션을 구성할 때 multi-master를 갖는 slave 서버 구성은 불가능하기 때문에 별도의 데이터 병합 과정이 필요하기도 했다.

> 데이터 병합 과정이 필요했던 이유는 사용중인 서버는 37대였지만 각 서버가 역할이 달랐을뿐 서버별로 실제로 차지하고 있는 메모리 용량 자체는 크지 않았기 때문에 이전시에는 비용/관리적인 측면을 고려해 하나의 서버로 통합하려고 했었기 때문이다.

결론적으로 1) 프라이빗 서브넷과 AWS 네트워크망을 연결해줄 일종의 프록시를 구성하는 작업과 2) Redis 데이터를 병합하는 작업이 필요했고 다음의 아키텍처를 구성할 수 있었다.

# 아키텍처

어쨌든간에 데이터를 옮기려면 프라이빗 서브넷에서 데이터를 끌어올 수 있어야하는데 다행히 IDC 프로바이더에서 VPN 서비스를 제공하고 있었기에 VPN을 통해 사내 네트워크망에서도 Redis 서버들에 직접 접근이 가능했다. 따라서 1번에서의 프록시는 VPN 클라이언트를 사용할 수 있으며 외부 네트워크 접속이 가능한 개발용 데스크탑인 iMac을 대신 사용하기로 했다. (iMac 5K with 24GB RAM)

2번의 데이터 병합은 두 가지 정도의 선택지가 있었는데 첫 번째는 모든 Redis 서버들의 rdb 스냅샷을 하나로 병합해 S3에 올린 뒤 ElastiCache에서 복원하는 방법이었고, 두 번째는 모든 키에 대해 dump/restore를 수행해 서버간에 키를 직접 이동시키는 방법이었다.

첫 번째 방식은 rdb 병합후 데이터 정확성만 보장할 수 있다면 가장 쉽고 빠른 방법이었을테지만 official한 rdb 병합툴도 없고 서드파티도 딱히 보이지 않았으며 rdb에 대해 아주 잘 알고있는 상태는 아니었기 때문에 직접 병합툴을 작성하는 일은 시간적 여유도 없을뿐더러 데이터 정확성 보장이 불확실했기 때문에 채택하지 않았다. 따라서 아주 빠르진 않지만 데이터 정확성은 보장할 수 있는 dump/restore 방식을 채택하기로 했다.

또한 이전에도 언급했지만 ElastiCache의 경우 퍼블릭 접근이 제한되어있기 때문에 ElastiCache에 접근이 가능한 EC2를 한 대 띄워 해당 EC2의 로컬 Redis 서버로 데이터를 이전 시켜놓고 EC2에서 ElastiCache로 한 번 더 이전하기로 했다.

그래서 최종적으로 다음과 같은 전체 데이터 이전 아키텍처를 구성할 수 있었다.



# 구현

데이터 이전 프로세스는 다음과 같다.

- 레플리케이션으로 데이터를 끌어오는 부분과 EC2로 데이터를 이전하는 부분을 하나의 작업으로 묶음
- 위 작업을 수행하는 프로세스를 8개 띄움
- 37개의 로컬 포트를 준비한 뒤 각 작업별로 포트를 할당해 로컬 Redis 서버를 띄움 (`localhost:64xx`)
- 최대 8개의 로컬 Redis 서버는 각각 IDC Redis 서버와 레플리케이션을 맺음 (`slaveof 192.x.x.x 6379`)
- 레플리케이션으로 데이터 동기화가 완료되면 레플리케이션을 끊고 EC2로 데이터 이전을 시작
- EC2로의 데이터 이전이 완료되면 로컬 Redis 서버 셧다운 및 덤프 파일 삭제후 프로세스 반환

>  프로세스 수를 8개로 제한한건 데스크탑에서는 가용 램 용량에 한계가 있기 때문에 동시에 많은 수의 로컬 Redis 서버를 띄워서 레플리케이션을 구성하면 램이 고갈되는 문제가 발생할 수 있다. 실제로 테스트시 프로세스 수를 좀 더 큰 값으로 올렸을 때 램이 고갈되어 프리징 직전까지 간 적이 있다.

코드로 보면 다음과 같다.

```python
def run(port, server):
    local_server = 'localhost:{}'.format(port)
    fetch(server, local_server)
    migrate_all(local_server, 'ec2 server ip')
    shutdown(local_server)
    clear_dump(port)
    
def main(start, end):
    ...
    pool = Pool(processes=8)
    pool.starmap(run, zip(local_ports, legacy_servers))
```

스크립트를 실행하고 중간에 작업 디렉터리를 확인해 보면 다음과 같이 **temp rdb**와 **dump rdb**가 동시에 최대 8개까지 존재하는걸 볼 수 있다.

```shell
$ ls -l
... dump6400.rdb # 동기화 완료된 로컬 서버 포트. EC2 이전까지 완료되면 rdb 삭제
... dump6409.rdb
... dump6405.rdb
... dump6406.rdb
... temp_6401.rdb # 동기화 진행중인 로컬 서버 포트.
... temp_6408.rdb
... temp_6403.rdb
... temp_6402.rdb
```

## 레플리케이션을 통한 데이터 동기화

```python
# src: IDC 서버
# dst: 로컬 서버
def fetch(src, dst):
    ...
    r = redis.StrictRedis(host=dsthost, port=dstport, charset='utf8')
    try:
        r.ping()
    except redis.exceptions.ConnectionError:
        subprocess.call([
            'redis-server',
            '--port', dstport,
            '--dbfilename', 'dump{}.rdb'.format(dstport),
            '--daemonize', 'yes',
        ], stdout=subprocess.DEVNULL)
    print('[{}] [{}|{}] Slave status: {}'.format(now(), src, dst, r.slaveof(srchost, srcport)))
    ...
```

특정 로컬 포트로 ping 테스트를 해보고 실패하면 해당 로컬 포트로 Redis 서버를 띄운 다음 IDC의 Redis 서버와 레플리케이션을 맺는다. (`r.slaveof(srchost, srcport)`)

```python
def fetch(src, dst):
    ...
    while True:
        master_link_status = r.info('replication')['master_link_status']
        master_sync_in_progress = r.info('replication')['master_sync_in_progress']
        if master_link_status == 'up' and master_sync_in_progress == 0:
            r.slaveof()
            break
    print('[{}] [{}|{}] All keys is fetched.'.format(now(), src, dst))
```

레플리케이션을 맺으면 slave는 master와 데이터 동기화를 시작한다. 데이터 정확성을 보장하기 위해선 모든 데이터의 동기화가 잘 이루어졌는지를 반드시 체크해야한다. 그렇다면 레플리케이션 도중 데이터 동기화가 완료되는 시점은 어떻게 알아낼 수 있을까?

Redis slave 서버의 레플리케이션 정보에는 master와의 연결 상태를 알 수 있는 `master_link_status`라는 플래그가 있다. 연결이 비정상적이거나 최초로 master와 full sync를 하는 동안에는 `down`으로 설정되며, full sync가 완료되고 정상적인 레플리케이션 연결이 수립되면 `up`으로 설정된다. 또한, 현재 master와의 동기화 진행 여부를 알려주는 `master_sync_in_progress`라는 플래그가 있는데, 동기화가 진행중이면  `1`, 완료되면 `0`으로 설정된다.

따라서 `master_link_status`와 `master_sync_in_progress`의 값이 각각 `up`과 `0`이 되는 시점을 동기화 완료 시점이라고 판단할 수 있으며, 이 때 레플리케이션을 끊으면 된다. (`r.slaveof()` = `slaveof no one`)

> 일반적인 상황에서는 저 두 값이 각각 `up`과 `0`인 경우를 정상적인 레플레케이션 "상태"라고 판단하지, 동기화 완료 "시점"이라고 보는건 조금 애매할 수 있다. 왜냐하면 레플리케이션에서 동기화는 지속적으로 이루어지기 때문인데,  다만, 나의 경우는 서버 점검 시간이었기 때문에 더 이상의 추가 데이터 업데이트가 이루어지지 않는 상황이었으므로 이를 동기화 완료 "시점"이라고 표현한 것이다.
>
> 더 덧붙여서 좀 더 정확히는 저 두 값만 가지고는 레플리케이션 상태가 정말 정상인지를 판단하기는 어려우며 `master_last_io_seconds_ago` 등의 다른 동기화 시간과 관련된 정보도 부가적으로 확인해 볼 필요가 있다.

## 데이터 이전

데이터 이전은 단순한 `dump`와 `restore`의 조합이다.

```python
def migrate_all(src, dst):
    ...
    r = redis.StrictRedis(host=srchost, port=srcport, charset='utf8')
    keyspace = r.info('keyspace')

    print('[{}] [{}|{}] Started migrating.'.format(now(), src, dst))
    jobs = [gevent.spawn(migrate, src, dst, int(k[2:])) for k in keyspace.keys()]
    gevent.joinall(jobs)
    print('[{}] [{}|{}] Migration was done.'.format(now(), src, dst))
```

데이터 이전 또한 병렬로 처리하기 위해 각 keyspace 별로 gevent job을 만들어 동시에 키 이전을 수행했다.

```python
def migrate(src, dst, db):
    count = 2500
    cursor = 0
    ...
    while True:
        # 2,500개 키 순회
        cursor, keys = srcr.scan(cursor, count=count)
        pipeline = srcr.pipeline(transaction=False)

        # dump
        for key in keys:
            pipeline.pttl(key)
            pipeline.dump(key)
        result = pipeline.execute()

        # restore
        pipeline = dstr.pipeline(transaction=False)
        for key, ttl, data in zip(keys, result[::2], result[1::2]):
            if data != None:
                pipeline.restore(key, ttl + 10800000 if ttl > 0 else 0, data)
        pipeline.execute(False)
        ...

        if cursor == 0:
            break
```

각 단일 키에 대해서 하나씩 `pttl`, `dump` 및 `restore`를 실행하면 Redis 서버로의 네트워크 I/O가 매번 발생하기 때문에 속도가 극도로 느려진다. 그래서 파이프라이닝을 통해 여러개의 커맨드를 동시에 전송함으로써 네트워크 I/O를 대폭 줄이고 이전 속도를 향상시켰다. (여기서는 요청당 2,500개)

또한 ttl 값에는 약 3시간 정도의 시간을 더하고 있는데 이는 ttl이 걸린 모든 키에 대해 서버 점검 시간만큼의 시간을 더해 expire time을 지연시키려고 한 것이다.

# 결과

서버 점검 후 약 00시 22분에 시작해 01시 00분쯤에 모든 Redis 데이터 이전을 마쳤다. 데스크탑과 EC2 인스턴스는 서로간 컴퓨팅 스펙 및 네트워크 환경이 다르고 데스크탑에서는 37개중 동시에 최대 8개의 서버 데이터만을 이전했기 때문에 두 구간에서 속도차가 발생하긴 했지만 도합해서 40분이 조금 안걸리게 마칠 수 있었다.

* **데스크탑 → EC2**: 23분 (약 24,000 keys/s)
* **EC2 → ElastiCache**: 14분 (약 40,000 keys/s)

**데스크탑 → EC2 이전 시작** (00:22)



**데스크탑 → EC2 이전 완료** (00:45) 및 **EC2 → ElastiCache 이전 시작** (00:46)



**EC2 → ElastiCache 이전 완료** (01:00)





# 회고

회고는 이전을 다하고 난 뒤 스스로에게 가진 질문들을 기반으로 답변을 하는 형태로 풀어나가겠다.

**데스크탑 프록시밖에 선택지가 없었나?**

이전해야했던 데이터 용량이 20GB 전후로 작았기 때문에 큰 고민없이 데스크탑을 프록시로 선택할 수 있었다. 물론, 만약 데스크탑으로 감당할 수 없는 데이터 용량이었다면 퍼블릭망 접근이 가능한 별도의 IDC 서버를 따로 요청을 하는 등의 다른 방법을 더 고려해봤을 것 같다.

**EC2에서 rdb 백업으로 바로 옮기지 않은 이유는?**

EC2를 생성하기 전에 이미 ElastiCache 인스턴스를 생성해놓았고 ElastiCache 인스턴스 생성시간이 생각보다 길어서 rdb 백업부터해서 ElastiCache 인스턴스 재생성 및 데이터 복원까지의 시간을 생각하면 원래 방식으로 옮기는것과 비교해서 시간적인 이득이 크지는 않을 것 같아 그냥 진행했었다. 이것 또한 데이터 용량이 작아서 그렇지 컸다면 백업 방식을 사용하지 않았을까 한다.

**EC2 ssh 터널링은 고려하지 않았는지?**

이전후에 되돌아보니 가장 아쉬웠던 부분이다. 평소에도 자주 사용하던 ssh 터널링이었는데 데이터 이전 코드에만 신경 쓰다가 이전 당시에는 고려하지 못하고 끝나고나서야 생각이 났기 때문에 아예 시도조차 못해봤다. 터널링을 했다면 절반의 시간으로 끝낼 수도 있었을 것 같다.

**성능이 조금 아쉽다**

결론적으로 보면 같이 작업을 진행하던 TD분이 MySQL 데이터를 옮기는 시간보다는 일찍 끝났기 때문에 Redis 데이터 이전 속도를 빠르게한들 전체 데이터 이전 작업 시간을 앞당기지는 못했을 것 같지만, 그래도 개인적으로 조금 아쉬운 부분이기는 하다. 크게 두 가지 부분에서 아쉬웠는데 하나는 비대칭 병렬성, 다른 하나는 언어이다. 

비대칭 병렬성이라고 하면, 위 아키텍처에서는 총 최대 8개의 프로세스를 띄우고 안에서 다시 한 번 keyspace별로 gevent job을 나누는데 어떤 특정 Redis 서버에서는 keyspace별로 키 개수의 차이가 아주 커서 특정 keyspace가 다른 keyspace보다 훨씬 빠르게 끝나 결국 키가 많은 keyspace를 맡은 job은 병렬성의 이점을 얻지 못하는 상황을 생각하면 된다. 키가 많은 keyspace 안에서도 일정 개수마다 구간을 나눠 다시 한 번 병렬처리를 할 수 있었을테지만 당시에는 키 개수가 막대한 수준의 양은 아니었기 때문에 어느 정도 이전 시간이 길어지는건 허용했었다. 그래도 지금 생각해보니 그래도 적용해보지 못한 아쉬움이 없진않다.

언어적인 측면에서는 이전시 사용한 Python도 좋지만 동시성을 내장하고 있으면서 컴파일이 가능한 Go와 같은 언어를 사용했으면 더 좋았을 것 같다라는 생각이 든다. 어느 정도 차이가 날까 궁금해서 비록 로컬 환경이지만 한 번에 2,000개의 명령어를 실행하는 파이프라인 코드를 두 언어로 작성한 뒤 30만개의 키를 넣고 성능 비교를 해보았다.



단순한 벤치마크 테스트를 돌려보니 Go로 작성한 스크립트가 Python으로 작성한 스크립트보다 약 8.15배 정도 빠르게 측정되었다. 물론 이건 언어뿐만 아니라 언어별로 지원하는 Redis 라이브러리의 구현체별로도 다를 수 있기 때문에 아주 정확한 언어 대 언어 벤치마킹이라고 하기는 어렵다. 하지만 그걸 감안하더라도 위에서 보이는 성능차는 꽤나 크기 때문에 실제로 스크립트를 Go로 작성했다면 좀 더 빠르게 처리할 수 있지 않았을까라는 생각이 든다. 물론 네트워크 대역폭의 한계같은 외부적인 요인도 고려해야하기 때문에 실제로 해보기 전까진 얼마나 차이가 날지는 모르겠으나 고루틴으로 비대칭 병렬성까지 개선했다면 기존보다 훨씬 빠르게 처리할 수 있었을 것 같다.

---

## File: content/post/2019-06-02-dynamodb-batch-retry-with-exponential-backoff.md

---
categories:
- algorithm
comments: true
date: 2019-06-02T00:00:00Z
tags:
- aws
- dynamodb
- batchjob
- pattern
- cloud
title: DynamoDB 배치 작업 재시도 구현 (Exponential Backoff)
url: /dynamodb-batch-retry-with-exponential-backoff
---

약 두 달 전에 게임 서버 인프라를 이전하면서 일부 유저 데이터를 RDB와 캐시 디비에서 AWS DynamoDB로 변환 및 이전하는 작업을 진행했었다.

> TMI: 이전에 작성한 [IDC에서 AWS로 데이터 이전하기](/redis-migration)라는 포스팅에서는 내가 담당하고 있는 두 개의 게임중 한 게임의 데이터 이전에 대한 후기를 공유했었다. 당시 이전을 진행한 게임은 작년 11월에 이전이 완료되었고, 그 이후로도 다른 한 게임의 인프라 이전 또한 진행이되어 올해 4월 초에 이전이 완료되었다.
>
> 같은 인프라 이전이지만 두 게임은 서로 마이그레이션 요구사항이 달랐기 때문에 많은 부분이 서로 다른 방식으로 진행되었다. 그 중 특히 데이터 이전의 경우, 첫 번째 게임은 데이터 스키마와 데이터 스토어가 이전과 동일했던 반면, 두 번째 게임은 거의 대부분의 유저 데이터 스키마가 변경되었고 그에 따라 사용하게될 데이터 스토어 또한 달라졌다.

수억건에 달하는 많은 양의 데이터를 최대한 빠른 시간안에 이전해야했기 때문에 프로비저닝 모드가 아닌 온디맨드 모드로 배치 작업을 수행했다. 실행 후 얼마 지나지 않아 예상대로 배치 작업의 속도에 비례해서 다량의 스로틀링이 발생하고 있었다. DynamoDB는 온디맨드 모드여도 최대 **WCU (Write Capacity Unit)**에 한계치가 있어서 그 이상을 넘어가면 스로틀링이 발생하는건 자연스러웠기 때문에 스로틀링 자체는 문제가 되지 않았다. 다만, 스로틀링이 발생하고 있음에도 불구하고 데이터가 예상보다 훨씬 빠른 속도로 들어가고 있는게 조금 의아했었다.

모니터링중 초당 삽입되는 개수와 현재 처리중인 데이터의 인덱스가 안맞는게 이상해서 중간에 작업을 중단하고 속도 대비 예상 데이터 개수와 실제로 저장된 데이터 개수를 비교해봤는데, 역시나 실제로 들어간 데이터 개수는 예상치보다 훨씬 부족한 상태였다. 즉, 중간에 다량의 데이터 쓰기 작업이 실패한셈이다.

디버깅을 해보니 쓰기 요청 속도가 일정 수준을 넘어가는 순간 위와 같은 문제가 발생하기 시작했다. 배치 작업을 요청하는 부분에서 문제가 발생하고 있어 관련 문서를 찾아보니 DynamoDB는 배치 작업 처리시 하나의 요청이라도 성공하면 일부 요청이 실패해도 에러를 반환하지 않고 실패한 요청에 대한 키 또는 값을 다시 반환해준다는 내용을 볼 수 있었다.

요청이 아예 실패하는 케이스에 대한 에러 처리는 하고 있었으나 부분 실패에 대한 처리가 빠져있어 일부 실패한 쓰기 요청들이 모두 무시되면서 데이터가 누락되는 문제가 발생했던 것이다. (결국엔 문서를 제대로 안본게 문제였다..)

# DynamoDB Exponential Backoff 재시도 구현하기

따라서 부분 실패가 발생하면 반환된 키 또는 값으로 다시 배치 작업을 요청하는 재시도 로직을 구현하기로했다. 재시도 전략으로는 공식 문서에서도 권장하고 있는 지수 백오프 (Exponential Backoff) 알고리즘을 사용했다. 지수 백오프 알고리즘은 매우 간단한 알고리즘으로 실패할때마다 (또는 특정 피드백을 받을때마다) 다음 요청까지의 유휴시간 간격을 n배씩 늘리면서 재요청을 지연시키는 알고리즘이다. 이 알고리즘은 TCP 전송 재시도에서부터 다양한 요청 실패의 가능성을 내재하고있는 서비스에서 재시도 전략으로 채택하고 있는 알고리즘이다. (AWS의 다양한 서비스 또는 SDK에서 내부적으로 구현하고 있는 재시도 로직에서도 같은 알고리즘을 사용한다)

일반적으로 **n=2** (Binary Exponential Backoff)를 사용하며 내 경우도 같은 값을 사용했다. 그럼 간단한 지수 백오프를 사용한 DynamoDB 재시도 로직을 구현해보자. 이 포스팅에서는 Go를 기준으로 설명한다.

# 세션 및 DynamoDB 구조체 생성

우선 AWS 자격 인증을 통해 세션을 생성하고 DynamoDB 서비스 객체를 초기화한다.

```go
package main

import (
    "errors"
    "fmt"
    "math"
    "sync"
    "time"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

// DynamoDB 구조체는 테이블명, 키 리스트, 서비스 객체 (service/dynamodb)를 관리
type DynamoDB struct {
    table string
    conn  *dynamodb.DynamoDB
}

// AWS 인증키 및 설정값 (실사용시 인증키는 하드코딩하지 않는다)
const (
    awsAccessKeyID     = "AKXXXXXXXXXXXXXXXXXX"
    awsSecretAccessKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    awsRegion          = "ap-northeast-2"
)

// 자격 인증 및 세션 생성
func NewSession() (*session.Session, error) {
    sess, err := session.NewSession(&aws.Config{
        Region:      aws.String(awsRegion),
        Credentials: credentials.NewStaticCredentials(awsAccessKeyID, awsSecretAccessKey, ""),
    })
    if err != nil {
        return nil, errors.New("failed to create an aws session")
    }
    return sess, nil
}

// 서비스 객체 생성 및 DynamoDB 구조체 초기화
func NewDynamoDB(table string) (*DynamoDB, error) {
    sess, err := NewSession()
    if err != nil {
        return nil, err
    }
    ddb := &DynamoDB{
        table: table,
        conn: dynamodb.New(sess, nil),
    }
    return ddb, nil
}
```

그럼 이제 DynamoDB 구조체에 부분 실패시 재시도를 수행해주는 배치 작업 메서드를 작성해보자.

# 재시도 가능한 쓰기 배치 메서드 작성

재시도 가능한 쓰기 배치 작업은 다음과 같은 플로우로 진행된다.

- 빌트인 자료구조로 표현된 값을 DynamoDB 속성값으로 변환
- 변환된 값으로 쓰기 요청 생성
- 쓰기 요청값으로 배치 작업 수행
- 부분 요청 실패시 일정 시간 대기하고 반환된 값으로 배치 작업 재수행
- 모든 요청이 성공하면 종료

`RetryableBatchWrite`는 쓰기 배치 메서드로 **키-값** 리스트를 인자로 받고 에러를 반환한다. DynamoDB가 지원하는 쓰기 배치는 한 번에 요청할 수 있는 아이템의 개수를 최대 25개로 제한하기 때문에 길이부터 검증한다.

```go
// items = [
//     {
//         "k1": "v1",
//         "k2": "v2",
//     },
//     {
//         "k3": "v3",
//         "k4": "v4",
//     },
// ]
func (ddb *DynamoDB) RetryableBatchWrite(items []map[string]interface{}) error {
    if len(items) == 0 {
        return errors.New("you must pass at least one item")
    }
    if len(items) > 25 {
        return errors.New("you must pass items less than or equal to number of 25")
    }
    ...
}
```

인자로 받은 `키-값`의 `값`을 DynamoDB 속성값으로 인코딩 (마샬링)한 뒤, DynamoDB API로 넘길 쓰기 요청을 만든다. (`dynamodb.WriteRequest`)

```go
func (ddb *DynamoDB) RetryableBatchWrite(items []map[string]interface{}) error {
    ...
    // 쓰기 요청용 구조체
    req := []*dynamodb.WriteRequest{}
    for _, item := range items {
        putItem := make(map[string]*dynamodb.AttributeValue)
        for k, v := range item {
            // 값을 DynamoDB 속성값으로 인코딩
            attr, err := dynamodbattribute.Marshal(v)
            if err != nil {
                return err
            }
            putItem[k] = attr
        }
        // 쓰기 요청 목록에 "키-값"을 PutRequest로 추가 (쓰기에는 Put/Delete 두 종류의 타입이 있음)
        req = append(req, &dynamodb.WriteRequest{
            PutRequest: &dynamodb.PutRequest{
                Item: putItem,
            },
        })
    }
    // 인코딩이 완료되면 {테이블명: 쓰기 요청 객체} 맵을 만든다
    // 배치 작업 실패시 반환되는 값과 동일한 포맷이다
    unprocessed := map[string][]*dynamodb.WriteRequest{
        ddb.table: req,
    }
    ...
}
```

다음으로, 위에서 만든 쓰기 요청으로 실제 배치 작업을 수행한다. 배치 작업을 요청하기 전에 재시도 횟수 카운터 변수인 `attempts`를 선언한다. `attempts`가 0보다 커지면 재시도를 수행하고 최대 재시도 횟수 안에 모든 요청을 처리하지 못하면 에러를 반환하도록 한다.

재시도 관련 상수값은 애플리케이션에 맞게 적절한 값으로 설정하면 된다.

```go
const (
    maxRetries      = 10
    minRetryBackoff = 5 * time.Millisecond
    maxRetryBackoff = 1 * time.Second
)

func (ddb *DynamoDB) RetryableBatchWrite(items []map[string]interface{}) error {
    ...
    attempts := 0
    for {
        // 재시도 수행시 재시도 횟수만큼 대기시간을 늘리면서 요청을 지연시킨다
        if attempts > 0 {
            time.Sleep(RetryBackoff(attempts))
        }
        attempts++
        // 배치 작업 수행
        output, err := ddb.conn.BatchWriteItem(&dynamodb.BatchWriteItemInput{
            RequestItems: unprocessed,
        })
        if err != nil {
            return err
        }
        // 일부 요청이 실패한 경우 unprocessed의 테이블키에 쓰기 요청을 담아서 반환해준다
        // 모든 요청이 성공하면 empty map을 반환한다
        // if _, ok := unprocessed[ddb.table]; !ok를 사용해도 된다
        unprocessed = output.UnprocessedItems
        if len(unprocessed) == 0 {
            break
        }
        // 최대 재시도 횟수를 넘기면 에러 반환
        if attempts > maxRetries {
            return errors.New("reached maximum retry attempts")
        }
        // 재시도 수행
    }
    return nil
}
```

`RetryBackoff` 함수는 다음과 같이 구현할 수 있다. 재시도 횟수만큼 최대 시간에 도달할 때까지 2배씩 늘린다.

```go
func RetryBackoff(n int) time.Duration {
    rb := time.Duration(math.Pow(2, float64(n-1))) * minRetryBackoff
    if rb > maxRetryBackoff {
        rb = maxRetryBackoff
    }
    return rb
}
```

이제 위에서 구현한 재시도 가능한 쓰기 배치 메서드가 잘 동작하는지 확인해보기 위해 테스트용 테이블로 `id`와 `stage`를 각각 파티션키, 정렬키로 갖는 `user` 테이블을 만들고 **2,500**개의 데이터를 배치로 넣는 코드를 작성해보자. 테스트는 `50 RCU / 50 WCU` 환경에서 진행되었다.

```go
func main() {
    // DynamoDB 초기화
    ddb, err := NewDynamoDB("user")
    if err != nil {
        panic(err)
    }
    wg := sync.WaitGroup{}
    vals := []map[string]interface{}{}
    // 2,500개 데이터 생성
    for i := 1; i < 2501; i++ {
        vals = append(vals, map[string]interface{}{
            "id":    1,
            "stage": i,
        })
        // 25개씩 배치 처리
        if i%25 == 0 {
            wg.Add(1)
            // 배치 고루틴 생성
            go func(v []map[string]interface{}) {
                defer wg.Done()
                // 배치 작업 수행
                // 테스트 코드에서는 에러를 무시하고 있지만 실사용시에는 적절한 에러처리가 필요하다
                _ = ddb.RetryableBatchWrite(vals)
            }(vals)
            vals = []map[string]interface{}{}
        }
    }
    wg.Wait()
}
```

재시도가 정상적으로 동작하는지 확인하기 위해 요청 간격을 지연시키는 부분에 간단한 로깅을 추가하자.

```
if attempts > 0 {
    time.Sleep(RetryBackoff(attempts))
    fmt.Println(fmt.Sprintf("attempts: %d, left: %d", attempts, len(unprocessed[ddb.table])))
}
```

이제 위 코드를 돌려보면 다음과 같은 재시도 요청들을 볼 수 있으며, 데이터도 실제로 잘 들어감을 확인할 수 있다. (고루틴과 DynamoDB에서의 쓰기 요청이 실제로 어떻게 처리되느냐에 따라 재시도 동작 및 출력값은 매번 다를 수 있다)

```console
attempts: 1, left: 11
attempts: 1, left: 23
attempts: 1, left: 6
attempts: 1, left: 10
attempts: 1, left: 10
...
attempts: 3, left: 9
attempts: 5, left: 7
attempts: 4, left: 5
attempts: 4, left: 6
attempts: 5, left: 4
attempts: 4, left: 5
attempts: 6, left: 5
```

# 재시도 가능한 읽기 배치 메서드 작성

재시도 가능한 읽기 배치 코드는 다음과 같으며 플로우는 쓰기 배치와 동일하므로 과정은 생략하고 차이점만 나열하겠다.

- `키-값` 리스트 대신 `키` 리스트를 인자로 받음
- 에러와 함께 받아온 데이터를 반환
- 한 번에 요청할 수 있는 키의 개수가 100개임
- 쓰기의 경우 인코딩한 데이터를 각각 `WriteRequest`에 `PutRequest` 형태로 담고, 읽기의 경우 인코딩한 키들을 `KeysAndAttribute`의 `Keys`에 한 번에 담는다
- `UnprocessedItems` 대신 `UnprocessedKeys`로 실패값(키) 처리
- `unprocessed[ddb.table]` 대신 `unprocessed[ddb.table].Keys`에 실패값(키)가 담겨져 있음

```go
// keys = [
//     {
//         "k1": "v1",
//         "k2": "v2",
//     },
//     {
//         "k3": "v3",
//         "k4": "v4",
//     },
// ]
func (ddb *DynamoDB) RetryableBatchGet(keys []map[string]interface{}) ([]map[string]interface{}, error) {
    if len(keys) < 1 {
        return nil, errors.New("you must pass at least one key")
    }
    if len(keys) > 100 {
        return nil, errors.New("you must pass keys less than or equal to number of 100")
    }

    // 키 저장용 구조체
    attrKeys := []map[string]*dynamodb.AttributeValue{}
    for _, key := range keys {
        attrKey := map[string]*dynamodb.AttributeValue{}
        for k, v := range key {
            // 값을 DynamoDB 속성값으로 인코딩
            marshaled, err := dynamodbattribute.Marshal(v)
            if err != nil {
                return nil, err
            }
            attrKey[k] = marshaled
        }
        attrKeys = append(attrKeys, attrKey)
    }
    // 키 목록 추가
    unprocessed := map[string]*dynamodb.KeysAndAttributes{
        ddb.table: &dynamodb.KeysAndAttributes{
            Keys: attrKeys,
        },
    }

    // 받아온 데이터 저장용 변수
    items := []map[string]interface{}{}
    attempts := 0
    for {
         // 재시도 수행시 재시도 횟수만큼 대기시간을 늘리면서 요청을 지연시킨다
        if attempts > 0 {
            time.Sleep(RetryBackoff(attempts))
        }
        attempts++
        // 배치 작업 수행
        output, err := ddb.conn.BatchGetItem(&dynamodb.BatchGetItemInput{
            RequestItems: unprocessed,
        })
        if err != nil {
            return nil, err
        }
        // 받아온 데이터 디코딩
        for _, r := range output.Responses[ddb.table] {
            item := map[string]interface{}{}
            for k, v := range r {
                var val interface{}
                // DynamoDB 속성값 디코딩
                dynamodbattribute.Unmarshal(v, &val)
                item[k] = val
            }
            items = append(items, item)
        }
        // 일부 요청이 실패한 경우 unprocessed의 테이블키에 실패한 키를 담아서 반환해준다
        // 모든 요청이 성공하면 empty map을 반환한다
        // if _, ok := unprocessed[ddb.table]; !ok를 사용해도 된다
        unprocessed = output.UnprocessedKeys
        if len(unprocessed) == 0 {
            break
        }
        // 최대 재시도 횟수를 넘기면 에러 반환
        if attempts > maxRetries {
            return nil, errors.New("reached maximum retry attempts")
        }
         // 재시도 수행
    }
    return items, nil
}
```

읽기 배치의 재시도 테스트도 해보자. 읽기의 경우 쓰기보다 빠르게 처리되므로 스로틀링이 잘 걸리도록 **RCU**와 **WCU**를 의도적으로 `10`으로 낮췄다. 실제 상황에서 데이터 요청이 매우 많은 경우에는 **RCU**, **WCU**가 충분히 높아도 스로틀링이 걸릴 수 있다.

```go
func main() {
    // DynamoDB 초기화
    ddb, err := NewDynamoDB("user")
    if err != nil {
        panic(err)
    }
    wg := sync.WaitGroup{}
    keys := []map[string]interface{}{}
    for i := 1; i < 2501; i++ {
        keys = append(keys, map[string]interface{}{
            "id":    1,
            "stage": i,
        })
        // 100개씩 배치 처리
        if i%100 == 0 {
            wg.Add(1)
            // 배치 고루틴 생성
            go func(k []map[string]interface{}) {
                defer wg.Done()
                // 배치 작업 수행
                // 배치 데이터를 한 번에 처리하려면 items를 고루틴 밖에서 취합하는 별도의 작업이 필요하다
                items, _ := ddb.RetryableBatchGet(k)
                // 테스트 로깅
                fmt.Println(len(items))
            }(keys)
            keys = []map[string]interface{}{}
        }
    }
    wg.Wait()
}
```

쓰기 배치에서와 동일하게 로깅을 심어보면 다음과 같은 재시도 요청들을 볼 수 있다. 받아온 데이터는 위의 쓰기 배치 테스트시 넣어둔 데이터이다.

```console
...
100
100
attempts: 1, unprocessed: 18
100
100
100
100
100
attempts: 1, unprocessed: 18
100
100
...
```

읽기 배치 또한 잘 동작하는걸 확인할 수 있다.

배치 메서드를 사용하는 부분을 보면 개수 제한과 병렬 처리 때문에 다소 복잡해보이는데 이 부분은 한 메서드로 처리할 수 있도록 한 번 더 래핑해서 사용하면 좀 더 깔끔하게 사용할 수 있다.

---

## File: content/post/2019-08-03-scheduling-cron-job-in-seconds-interval.md

---
categories:
- cron
comments: true
date: 2019-08-03T00:00:00Z
tags:
- cron
- scheduling
title: Cron에서 초단위로 스케줄링하기
url: /scheduling-cron-job-in-seconds-interval
---

최근에 2초에 한 번씩 특정 태스크를 수행하는 스크립트를 돌려야하는 일이 생겨서 cron으로 2초 단위 스케줄을 등록하려는데 cron은 최소 스케줄링 단위가 `분`이었기 때문에 cron의 구문을 활용한 일반적인 방식으로는 작업 등록을 할 수 없었다.

이 문제는 `sleep`을 사용해 하나의 스케줄 잡에서 특정 태스크가 일정 간격마다 실행되도록 하는 간단한 편법으로 해결할 수 있다. 즉, 가령 10초에 한 번씩 돌려야하는 태스크가 있다면 해당 태스크를 10초에 한 번씩 총 6번을 수행하는 스크립트를 1분에 한 번씩 돌도록 스케줄링 해주는 것이다.

위 방법을 사용하려면 스크립트를 약간 수정해줘야 하는데, 수정하고 싶지 않거나 수정할 수 없는 경우 또는 스크립트가 아닌 명령어 실행이라면 스케줄을 등록할때 여러개의 같은 잡을 등록하고 스크립트나 명령어 앞에 sleep만 넣어주면 된다.

# 스크립트에서 스케줄링하기

셸 스크립트의 경우 기존 태스크를 루프로 감싸고 안에서 sleep만 걸어주면 된다. 다른 언어를 사용중이라면 각 언어별로 내장된 sleep 함수를 사용하면 된다.

```sh
# original.
task;

# 2 seconds interval.
for i in {1..30}; do
    task;
    sleep 2;
done
```

그 다음 crontab에 1분 스케줄링을 걸어준다.

```sh
# crontab
* * * * * /bin/sh /path/to/script.sh
```

sleep 시간을 동적으로 설정하려면 sleep 시간을 받을 수 있도록 할 수도 있다.

```sh
n=$1
# n seconds interval.
for i in $(seq $((60/$n))); do
    task;
    sleep $n;
done
```

```sh
# crontab
* * * * * /bin/sh /path/to/script.sh 2
```

그런데 만약 task 실행 시간이 충분히 짧지 않다면 실질적인 sleep 시간은 task 실행시간 + sleep 시간이 되기 때문에 스케줄링에 오차가 생길 수 있다. 이 경우는 task를 백그라운드로 실행하면된다.

```sh
n=$1
# n seconds interval.
for i in $(seq $((60/$n))); do
    task &;
    sleep $n;
done
```

# Crontab에서 스케줄링하기

crontab 등록시 스크립트 실행 이전에 sleep을 걸어주면 된다. 단, 첫 스케줄잡은 제외한다. 스크립트를 수정하지 않아도 되기 때문에 가장 편하게 사용할 수 있는 방법이다.

```sh
# crontab
* * * * * /bin/sh /path/to/script.sh
* * * * * sleep 10; /bin/sh /path/to/script.sh
* * * * * sleep 20; /bin/sh /path/to/script.sh
* * * * * sleep 30; /bin/sh /path/to/script.sh
* * * * * sleep 40; /bin/sh /path/to/script.sh
* * * * * sleep 50; /bin/sh /path/to/script.sh
```

1분마다 모두 동시에 실행되지만 sleep이 있어 작업이 지연되므로 10초 간격으로 스케줄링된 것처럼 동작한다.

---

## File: content/post/2019-11-06-debug-memory-leak-with-node-heapdump.md

[Binary file]

---

## File: content/post/2020-02-13-debugging-containerized-go-app.md

---
categories:
- debugging
- go
comments: true
date: 2020-02-13T00:00:00Z
tags:
- debugging
- docker
- go
- delve
title: 컨테이너 내부 Go 애플리케이션 디버깅하기
url: /debugging-containerized-go-app
---

회사에서 Go를 메인으로 서비스를 개발하다보니 당연하게도 Go 애플리케이션을 디버깅 해야하는 상황이 생기게 마련이다. 서버 애플리케이션을 개발하게 되면 디버깅하는 방식이 크게 두 가지로 나뉘는데, 하나는 로컬 서버 디버깅이고 또 하나는 원격 서버 디버깅이다.

서버 애플리케이션은 보통 DB나 캐시 서버 같은 외부 서비스 의존성을 갖기 때문에 로컬 환경 구성시 VM이나 컨테이너를 통해 의존 서비스들을 함께 띄우곤 한다. 우리는 기본적으로 ECS 및 EKS를 기반으로 서비스를 하고 있기 때문에 로컬 환경에서도 도커를 사용하고 있다. 이 때 외부 서비스만 도커로 띄우는 방법이 있고, 서버 애플리케이션까지 모두 도커로 띄우는 방법이 있는데, 나 같은 경우는 로컬 환경과 프로덕션 환경이 (설정을 제외한 모든 것들이) 완전히 일치하는 것을 선호하는 편이라 후자의 방식을 사용하고 있으며 따라서 컨테이너 원격 서버 디버깅을 주로 사용하고 있다.

> 물론 서버 애플리케이션이 아닌 CLI를 개발할 때에는 (당연히) 로컬 디버깅을 하고 있다.

따라서 이번 포스팅에서는 컨테이너 내부의 Go 애플리케이션을 디버깅하는 방법에 대해 소개하고자 한다. 

# 예제 코드 작성

예제로 사용할 코드는 다음과 같은 n번째 피보나치수를 계산해주는 간단한 서버 애플리케이션이다.

**./main.go**

```go
package main

import (
    "log"
    "net/http"
    "strconv"
)

const port = "8000"

func main() {
    http.HandleFunc("/fib", fibHandler)

    log.Printf("listening on port %s", port)
    log.Fatalln(http.ListenAndServe(":"+port, nil))
}

func fibHandler(w http.ResponseWriter, r *http.Request) {
    ns, ok := r.URL.Query()["num"]
    if !ok || len(ns) < 0 {
        log.Println("param 'num' is missing")
        return
    }
    n0, err := strconv.Atoi(ns[0])
    if err != nil {
        log.Printf("invalid number: %v", err)
        return
    }
    fb := strconv.Itoa(fib(n0))
    w.Write([]byte(fb + "\n"))
}

func fib(n int) int {
    a, b := 1, 1
    for i := 0; i < n-2; i++ {
        a, b = b, a+b
    }
    return b
}
```

위 코드를 `main.go`로 저장한 다음 같은 디렉토리에서 `go mod init calc`로 모듈을 초기화 해준다. 예제 코드 준비는 모두 끝났으니 이제 이 애플리케이션을 컨테이너로 띄워보자.

# 도커 파일 작성

Go 디버깅에는 [`delve`](https://github.com/go-delve/delve)라는 디버거를 사용하며 원격 디버깅을 위해서는 컨테이너 내부에서 디버깅 요청을 받을 수 있는 delve 디버거 서버를 띄워줘야 한다. 또한 디버깅의 정확성을 위해 보통 디버깅용 빌드에서는 컴파일러 최적화 옵션을 끄기 때문에 디버깅 환경으로 구성된 이미지는 프로덕션 환경에서 사용할 수 없다. 따라서 나 같은 경우는 실제 배포용 이미지와 디버깅용 이미지를 분리하여 사용하고 있다.

> 좀 전에 프로덕션 환경과 완전히 일치하는 환경을 선호한다고 했기 때문에 디버깅용 이미지가 따로 있으면 환경이 달라지는거 아닌가하는 의문이 들 수도 있지만, 이 두 이미지는 (컴파일러 최적화 옵션을 끔으로써 발생하는) 성능 차이를 제외한 다른 모든 시스템 환경은 완전히 동일하기 때문에 내가 우려했던 OS 및 시스템 레벨에서의 환경 및 의존성이 일치하지 않음으로써 발생할 수 있는 문제는 없다.

## 배포용 이미지

**./Dockerfile**

```dockerfile
# Build binary
FROM golang:1.13 AS builder
WORKDIR /build

ENV CGO_ENABLED=0

COPY go.mod ./
RUN go mod download

COPY . ./
RUN go build .

# Run on scratch
FROM alpine:3.10
WORKDIR /bin

COPY --from=builder /build/calc /bin/

EXPOSE 8000
ENTRYPOINT ["/bin/calc"]
```

기본적인 Go 바이너리 배포용 이미지이며 이 이미지를 기반으로 디버깅 환경을 갖춘 도커 파일을 작성해보자.

## 디버깅 이미지

**./debug/Dockerfile**

```dockerfile
# Build binary
FROM golang:1.13 AS builder
WORKDIR /build

ENV CGO_ENABLED=0

COPY go.mod ./
RUN go mod download

COPY . ./
RUN go build -gcflags "all=-N -l" . # -gcflags "all=-N -l"로 컴파일러 최적화 옵션을 끈다.

# Install delve
RUN go get -u github.com/go-delve/delve/cmd/dlv # delve 설치

# Run on scratch
FROM alpine:3.10
WORKDIR /bin

COPY --from=builder /build/calc /bin/
COPY --from=builder /go/bin/dlv /bin/ # delve 가져오기

EXPOSE 8000
EXPOSE 2345 # 디버거 서버 포트
ENTRYPOINT ["/bin/dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/bin/calc", "--continue"] # 디버깅 시작
```

디버깅 이미지에서는 `delve` 디버거를 설치한 후 바이너리를 직접 실행하는 대신 헤드리스 디버거 서버가 실행하도록 변경 해주면 된다. 다음 명령어는 2345번 포트에서 디버거 서버를 헤드리스 모드로 실행한 뒤 `/bin/calc` 바이너리를 실행하여 디버깅 세션을 시작하라는 의미이다.

`ENTRYPOINT ["/bin/dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/bin/calc", "--continue"]`

`--continue` 옵션은 디버깅 프로세스를 곧바로 시작하라는 의미이다. 이 옵션을 주지 않으면 디버깅 요청을 보내줘야만 디버깅 프로세스가 시작된다. 즉, 이 옵션이 없으면 컨테이너가 띄워져도 웹 서버는 실행되지 않으며 2345 포트로 디버깅 요청을 보내줘야만 디버깅 프로세스와 함께 웹 서버가 시작된다.

배포용 이미지와 디버깅 이미지를 함께 띄워두기 위해 직접 빌드하고 실행하는 대신 docker compose를 사용한다.

> 함께 띄워두는 이유는 실제 서비스 개발 환경과 디버깅 환경을 동시에 사용하기 위해서이다.

## 컨테이너 실행

**./docker-compose.yml**

```yaml
version: "3"
services:
  calc:
    build:
      context: .
    ports:
      - "8000:8000"
  calc-debug:
    build:
      context: .
      dockerfile: ./debug/Dockerfile
    ports:
      - "8001:8000"
      - "2345:2345"
```

`docker-compose up --build -d` 명령어로 컨테이너를 띄우고 나면 모든 준비가 끝나며, 최종 디렉토리는 구조는 다음과 같다.

```
├── Dockerfile
├── debug
│   └── Dockerfile
├── docker-compose.yml
├── go.mod
└── main.go
```

# 컨테이너 디버깅

나는 현재 IntelliJ + Go 플러그인 및 Goland를 주 에디터로 사용하고 있으며 서브 에디터로는 Vscode를 사용하고 있기 때문에 이 두 에디터에 대해 각각 원격 디버깅을 하는 방법을 소개하려고 한다.

## Goland

먼저 `Run > Edit Configuration` 메뉴로 디버깅 설정 창을 열어 `+`에서 `Go Remote`를 선택한다.



설정 화면이 나오면 `Name`은 **Docker**, `Host`는 **localhost** 그리고 `Port`는 **2345**로 설정해준다.



그 다음 `Rnu > Debug 'Docker'`를 선택하여 디버깅 창을 연 다음 원하는 라인에 브레이크 포인트를 찍는다.



이제 `curl`을 사용하여 웹 서버에 요청을 날려보자.

```shell
$ curl 'localhost:8001/fib?num=1000'
```

요청을 날리면 디버깅이 시작되며 디버깅 창에서 각 변수의 값을 확인할 수 있다. 다음은 두 번째 브레이크 포인트까지 진행한 결과이다. (`n0`: 1000, `fib`: 817770325994397771)



디버깅 프로세스를 끝내면 요청이 잘 처리되었음을 확인할 수 있다.

```shell
$ curl 'localhost:8001/fib?num=1000'
817770325994397771
```

웹 서버가 띄워져 있기 때문에 원할 때마다 같은 방법으로 디버깅을 반복해서 진행할 수 있다.


## Vscode

Vscode에서는 설정이 좀 더 간단하다. 좌측에서 디버깅 메뉴로 들어가면 좌측 상단에 톱니바퀴 모양이 보이는데 해당 버튼을 누르면 `launch.json`이라는 설정 파일이 열리며 설정값은 다음과 같이 작성해주면 된다.

```json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch remote",
            "type": "go",
            "request": "launch",
            "mode": "remote",
            "program": "${workspaceFolder}",
            "remotePath": "/build",
            "host": "127.0.0.1",
            "port": 2345,
            "env": {},
            "args": [],
            "showLog": true,
            "trace": "verbose"
        }
    ]
}
```



설정이 끝났으면 에디터 창으로 돌아가 브레이크 포인트를 찍은 뒤 `Debug > Start Debugging`으로 디버깅을 시작한다. 마찬가지로 `curl`로 요청을 날려보면 다음과 같은 창이 열리며 디버깅이 시작된다.

```shell
$ curl 'localhost:8001/fib?num=1000'
```



# 마무리

이상으로 컨테이너 내부의 Go 애플리케이션을 디버깅하는 방법을 살펴보았다. 물론 아직 로컬 디버깅 대비 단점이 하나 있는데, 그건 바로 코드 변경시 도커 이미지를 다시 컴파일 해줘야 한다는 것이다. 이 부분은 live reload 테크닉을 통해 개선할 수 있으며 이에 대해서는 다음에 소개해보려고 한다.

---

## File: content/post/2020-04-18-go-modules-private-repo.md

[Binary file]

---

## File: content/resume/_index.md

+++
date = "2019-06-03T00:00:00"
title = "Resume"
hidden = true
+++

# MinJae Kwon

## mingrammer

---

### Profile

###### Contact

[mingrammer@gmail.com](mingrammer@gmail.com) or [(+82) 10-5020-4597](sms:821050204597)

###### Websites

[Homepage](https://mingrammer.com), [GitHub](https://github.com/mingrammer), [Medium](https://medium.com/@mingrammer)

---

### Work Experience

###### Server Engineer

##### [Daangn Market](https://www.daangn.com/), Dec 2019 -

- Working on Platform Team

###### Server Engineer
##### [SundayToz](http://corp.sundaytoz.com/), Jul 2017 - Dec 2019

- Developed and maintained a [mobile puzzle game](http://corp.sundaytoz.com/sachunsung-) server with 180k and 25k rpm (requests per minute) at peak and normal time respectively.
- Migrated the game server infrastructure from IDC to AWS with 8 hours downtime.
- Migrated 3.4 billion user data from the MySQL and Redis on IDC to the RDS and DynamoDB on AWS using Golang.
- Built a reproducible local development environment with Vagrant.
- Built an HTTP sniffer to inspect encrypted data flowing between client and game servers with Golang.
- Built a tool for syncing static game data from spreadsheet to backed database with Python.

###### Server Engineer
##### [Townus](https://www.townus.co.kr/), Nov 2016 - Feb 2017

- Developed and maintained the townus API servers.
- Wrote unit tests for almost existing APIs that have never been tested.
- Built an on-demand image resizing server to generate dynamic-sized images with Python.

---

### Skills

###### Programming Languages

Go, Python

###### Backend Development

Linux, AWS, Docker, MySQL, PostgreSQL, Redis, Memcached, Kafka, gRPC

---

### Interests

- Designing system architectures
- Build something for developers
- Automating the boring tasks
- Open source software

---

### Open Source Experience

###### [diagrams](https://github.com/mingrammer/diagrams)
##### Diagram as Code for prototyping cloud system architectures

Diagrams lets you draw the cloud system architecture in Python code. It was born for prototyping a new system architecture design without any design tools. You can also describe or visualize the existing system architecture as well.

###### [flog](https://github.com/mingrammer/flog)
##### A fake log generator for common log formats

Flog provides a log generator for 6 often used log formats (for now). It was created for testing some tasks which require log data like amazon kinesis log stream test. Its docker image was pulled 212k times from docker hub by other developers.

###### [pyreportcard](https://github.com/mingrammer/pyreportcard)
##### A report card web service for Python applications

Pyreportcard analyzes the source code quality of the Python projects which are hosted on GitHub, checks for license and readme files, and provides some statistics.

###### [casec](https://github.com/mingrammer/casec)
##### Text case converter for programmers

Casec is a text case converter for programmers. casec now supports upper, lower, title, camel, pascal, snake, kebab (or lisp) cases, providing also case conversion library.


##### Contributions

[go#24074](https://github.com/golang/go/pull/24074), [go-redis#952](https://github.com/go-redis/redis/pull/952), [airflow#5749](https://github.com/apache/airflow/pull/6418), [airflow#5758](https://github.com/apache/airflow/pull/6432), [airflow#5769](https://github.com/apache/airflow/pull/6443), [dd-trace-go#647](https://github.com/DataDog/dd-trace-go/pull/647), [cli/cli#532](https://github.com/cli/cli/pull/532), [mvdan/sh#388](https://github.com/mvdan/sh/pull/388), [tqdm#615 (wip)](https://github.com/tqdm/tqdm/pull/615) and etc.

---

### Translations

- Korean translator of [Head First Go](https://www.hanbit.co.kr/store/books/look.php?p_code=B1938330682), 2019 

---

### Presentation

- [Redis data migration from IDC to AWS](https://speakerdeck.com/mingrammer/idceseo-awsro-redis-deiteo-ijeonhagi) at AWSKRUG Architecture, Fab 2019
- [Build reproducible infrastructure on legacy](https://speakerdeck.com/mingrammer/regeosi-wieseo-jaehyeon-ganeunghan-hwangyeong-gucughagi) at HashiCorp Korea User Group, 2018
- [Serverless on-demand image resizing server](https://speakerdeck.com/mingrammer/building-a-serverless-application-with-aws) at AWSKRUG Architecture, Aug 2017
- [On-demand image server with Python](https://speakerdeck.com/mingrammer/on-demand-image-server-with-python) at Python Seminar Seoul, May 2017

---

### Education

###### Software Engineering
##### [Ajou University](http://www.ajou.ac.kr), Mar 2014 -

---

## File: static/ads.txt

google.com, pub-9436560391944255, DIRECT, f08c47fec0942fa0

---

## File: themes/hugo-theme-cactus-plus/README.md

# Hogu Theme Cactus Plus

[English Document](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/README.md) | [中文文档](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/README_zh-cn.md)

Cactus Plus is a minimalistic theme for bloggers based on the theme named [Cactus](https://github.com/digitalcraftsman/hugo-cactus-theme).

**Features**

+ **More beautiful style**
+ **Tags page**
+ **Archive page**
+ **[Disqus](https://disqus.com/) comment-system**
+ **Both Google Analytics and Baidu Analytics for site analytics.**
+ **Table of Content**
+ **Twitter Card**

Here's the link to online demo: [http://nodejh.com](http://nodejh.com).




- [Hogu Theme Cactus Plus](#hogu-theme-cactus-plus)
	- [Screenshot](#screenshot)
	- [Installation](#installation)
	- [Example Site](#example-site)
	- [Posts Summary](#posts-summary)
	- [About page](#about-page)
	- [Avatar](#avatar)
	- [Hide pages](#hide-pages)
	- [Disqus](#disqus)
	- [Social link icons](#social-link-icons)
	- [Nearly finished](#nearly-finished)
	- [License](#license)



## Screenshot

**home page**



**tags page**


**archive page**



## Installation

First clone the theme to your themes directory:

```
$ cd themes
$ git clone https://github.com/nodejh/hugo-theme-cactus-plus.git
```

Take a look inside the [`exampleSite`](https://github.com/nodejh/hugo-theme-cactus-plus/tree/master/exampleSite) folder of this theme. You'll find a file called [`config.toml`](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/exampleSite/config.toml).

To use it, copy the [`config.toml`](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/exampleSite/config.toml) in the root folder of your Hugo site. Feel free to change strings as you like to customize your website.

And you may replace your site `archetypes/default.md` with [`hugo-theme-cactus-plus/exampleSite/archetypes/default.md`](https://github.com/nodejh/hugo-theme-cactus-plus/tree/master/exampleSite/archetypes/default.md)

For more information read the official [setup guide](//gohugo.io/overview/installing/) of Hugo.

## Example Site

There is an example site with config file and markdown files in [`exampleSite`](https://github.com/nodejh/hugo-theme-cactus-plus/tree/master/exampleSite) directory.

## Posts Summary

Summary will show follow the post title title in home page.

And if you want to use description property that in post's fromt matter to instated of post's content summary, set `useDescriptionReplaceSummary = true`.

To use the content summary, you may add the `` summary divider where you want to split the article alternatively. For org content, use # more where you want to split the article. Content that comes before the summary divider will be used as that content’s summary.By default, Hugo automatically takes the first 70 words of your content as its summary. See also [https://gohugo.io/content-management/summaries/](https://gohugo.io/content-management/summaries/).

## About page

Use the about page to introduce yourself to your visitors. You can customize the content as you like in the `/content/about/_index.md`.

## Avatar

Furthermore, you should replace the [avatar placeholder](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/static/images/) with a great image of yourself.

## Hide pages

Pages can be hidden and don't appear in the post list on the homepage if you add the variable `hidden = true` to the frontmatter. This allows you to link from elsewhere, i.e. just the menu.

## Disqus

This theme features a comment system powered by Disqus. Just add your Disqus-shortname to the [`config.toml`](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/exampleSite/config.toml) and set `enableDisqus = true`, then let readers respond to your blog posts.


## Social link icons

You can add a social link panel in the footer by adding entries to the `social` block in the [`config.toml`](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/exampleSite/config.toml). You can choose between two icon fonts:

- [Font awesome](https://fortawesome.github.io/Font-Awesome/) or
- [Mono social icons](https://github.com/drinchev/monosocialiconsfont)

Assign either `font-awesome` or `mono-social` to the `iconFont` variable. The Mono social icons offer three styles of icons: -circle, rounded, or default (empty).


## Nearly finished

In order to see your site in action, run Hugo's built-in local server.

```
$ hugo server
```

Now enter [`localhost:1313`](http://localhost:1313) in the address bar of your browser.


## License

This theme is released under the MIT license. For more information read the [license](https://github.com/nodejh/hugo-theme-cactus-plus/blob/master/LICENSE.md).

---

## File: themes/hugo-theme-cactus-plus/LICENSE.md

The MIT License (MIT)

Copyright (c) 201-present [nodejh](http://nodejh.com)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---

## File: themes/hugo-theme-cactus-plus/README_zh-cn.md

[Binary file]

---

## File: themes/hugo-theme-cactus-plus/archetypes/default.md

+++
title = "{{ replace .TranslationBaseName "-" " " | title }}"
date = {{ .Date }}
tags = [""]
categories = [""]
draft = false
+++

---

## File: themes/hugo-theme-cactus-plus/exampleSite/content/about/_index.md

+++
date = "2015-06-20T14:02:37+02:00"
title = "About"
hidden = true
+++

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsa ullam earum dolorum! Sed, perspiciatis.

Lorem ipsum dolor sit amet, consectetur adipisicing elit.

***

### Lorem ipsum dolor.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea dicta corporis ad inventore itaque impedit dolor atque amet exercitationem! Veniam qui voluptas maiores vel laudantium necessitatibus, velit ducimus! Iste hic facere, accusamus fugiat enim facilis.