10.Nginx Ingress Controller on Google Kubernetes Engine

이번 과정에서는 “Ingress”에 대해서 알아 볼 수 있었다.

“Ingress”는 resource와 controller로 구성되어 있다.

resource는 “Ingress”의 동작에 대한 규칙을 정해 놓은 yaml이며,

controller는 layer7에 해당하는 로드밸런서의 역할을 제공한다. 즉 http 요청에 대한 처리와 부하 분산을 제공한다.

controller로는 gce, nginx, envoy, haproxy, istio, kong, traefik 을 이용할 수 있다.

과정에서는 “hello-app” 이라는 “Service”를 노출한다. 그리고 helm을 이용해서 nginx-ingress를 kubernetes cluster에 설치한다.

kind가 “Ingress”인 yaml 파일을 만든다. 이때 “path: /hello”로 지정하며, backend를 “serviceName: hello-app”과 “servicePort: 8080” 으로 지정을 한다.

이렇게 해서 “nginx-ingress-controller”가 제공하는 “EXTERNAL-IP”에 “/hello”로 웹브라우저에서 연결을 해보면 서비스가 제공되는 것을 확인해 볼 수 있다.

“Ingress”를 통해서 로드밸런서를 제공하기 위해서는 “Service” 생성 과정에서 type의 지정이 필요하다.

“Pod”는 죽고 사는 과정이 계속해서 반복되고 ip가 계속해서 변동된다.

이러한 “Pod”를 선택적으로 동일한 ip로 접근하기 위해서 “Service”를 사용한다.

이 “Service”들이 제공하는 타입에 따라서 서비스가 제공되는 형태가 변경이 된다.

clusterip는 클러스터 내부에서만 접근 가능한 형태의 service 이다.

그리고 nodeport는 “node이름:port”로 외부에서도 접근이 가능하도록 서비스를 제공하는 형태이다.

loadbalancer는 클라우드 서비스를 제공하는 벤더들에서 제공하는 로드밸런서를 사용한다.

마지막으로 externalname은 외부의 서비스를 클러스터 내부에서 사용하고자 할 때 사용하는 형태이다.

이와 같이 클라우드 서비스 벤더에서 제공하는 로드밸런서를 사용할때는 “Service”의 타입을 loadbalancer로 지정하고, “Ingress”를 통해서 제공하는 로드밸런서를 사용할때는 nodeport를 사용해서 “Service”를 생성한다.

9.Helm Package Management

이번 과정은 kubernetes에서 사용할 수 있는 패키지 관리자에 대해서 알아 볼 수 있었다.

Helm은 클라이언트 역할을 하는 helm, 서버 역할을 하는 tiller 그리고 설정 정보들의 관리를 위한 chart로 이루어져 있다.

여기에서 클라이언트(helm)라고 하는건 클러스터의 외부에서 작업을 지시하기 위한 도구이고, 실제로 클러스터 안쪽에서 동작 하는건 서버(tiller)라고 보면 된다.

helm의 설치는 간단합니다.

$ curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh

$ chmod 700 get_helm.sh

$ ./get_helm.sh

근데 이 과정 다음에 갑자기 tiller를 위한 계정을 만들고 해당 계정을 무언가에 바인딩을 한다.

이 과정은 tiller가 클러스터 전체에서 권한을 갖기 위한 방법으로 여기면 될 것 같다.

비교가 적절할지 모르겠지만, 리눅스에서는 “sudo” 로 권한을 얻고, 윈도우에서 “관리자 권한”을 얻는 것 처럼 tiller에게도 더 높은 권한을 갖을 수 있도록 해주는 과정이라고 이해를 했다.

(권한에 대한 내용은 “http://bcho.tistory.com/1272" 를 참고하면 좋을것 같다.)

계정을 생성하고 권한을 설정하는 과정은 다음과 같다.

$ kubectl -n kube-system create serviceaccount tiller

$ kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller

그리고 권한을 제거하고 계정을 삭제하는 과정은 다음과 같이한다.

$ kubectl delete clusterrolebinding tiller

$ kubectl -n kube-system delete serviceaccount tiller

이러한 과정은 kubernetes의 권한 관리 방식을 rbac를 이용할 경우에 이렇게 하고, rbac 이외에 abac라는 방식은 다른 방법이 존재하는것 같다.

계정을 생성했으면 tiller를 설치한다.

$ helm init --service-account=tiller

tiller가 설치된 후에 “kubectl get po –namespace kube-system” 로 확인해 보면 “tiller-deploy-blahblah”가 실행되고 있는걸 확인 할 수 있다.

그리고 tiller를 제거하기 위해서는

$ kubectl -n kube-system delete deployment tiller-deploy

를 해주고 위에서 이야기했던 권한 제거하는 작업을 해주면 된다.

그리고 chart를 업데이트하고 chart를 이용해서 설치하는 과정은 아무런 고민없이 사용이 가능할 정도로 간단다.

8.Setting Up a Private Kubernetes Cluster

이번 과정의 실습을 진행하면서 클러스터에 대한 접근을 제한 할 수 있도록 하기 위한 방법에 대해서 알 수 있었다. 하지만 전체 내용에 대해서 정확하게 이해를 하지 못했다.

해당 내용은 이후에 비공개 클러스터 설정(https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters) 에 대한 내용이 실습 내용과 동일하므로 추후에 이해가 가능한 시점이 되면 다시 한번 시도해 보려고 한다.

아래는 내용을 이해하기 위해서 학습했던 내용들의 링크이다.

그리고 구글 클라우드 플랫폼 서비스에서 제공하는 GKE 관련 문서(https://cloud.google.com/kubernetes-engine/docs/) 들 중에서 함께 보면 도움이 되는 부분들이다.

그 외에도 “CIDR” 에 대해서 학습을 할 수 있었다.

7.Build Slack Bot With Node.js on Kubernetes

이번 과정은 Secret 객체에 대해서 좀 더 자세하게 알아볼 수 있는 과정이었다.

서비스를 제공하는 프로그램(node.js 코드)을 Docker image로 만든 다음 Registry Server에 Push를 한다.

제공하는 서비스에서 외부의 서비스(slack)를 사용하기 위해서 민감한 정보인 token이 존재한다. 이 내용이 코드상 또는 image 상에 존재하지 않게 Image를 만들고자 한다.

그래서 별도로 “slack-token” 파일을 만들어서 파일을 통해서 token 정보를 제공하도록 만들어 놓았다. 해당 파일을 경로와 파일명을 포함해서 환경변수 “slack_token_path”에 등록해 놓고, 코드상에서는 환경 변수인 “slack_token_path”에 지정되어 있는 경로의 파일로 부터 token 정보를 읽어서 사용한다.

실습 과정에서 보여지는 Docker image를 만드는 Dockerfile에는 token 정보를 다루고 있지 않다. 이를 통해서 해당 정보는 배포가 되지 않는다는걸 알 수 있다.

이제 Kubernetes 클러스터에서 배포를 하기 위해서 token 정보를 container들이 인식할 수 있도록 해주어야 한다.

그래서 token 정보를 Secret으로 생성해서 클러스터에 배포한다.

다시 반복해서 확인을 해보면, container에서 제공되는 서비스는 “slack_token_path” 라는 이름의 환경 변수를 읽어 파일 경로를 구할 수 있다. 이 경로를 통해서 token 정보를 알아낸다. 그리고 구해진 token 정보를 이용해서 slack이 제공하는 기능을 이용할 수 있다.

이렇게 container에서 token 정보를 구할 수 있도록 하기 위해서, Deployment의 yaml 파일에서 container가 사용할 환경 변수를 등록할 수 있는 “env”를 이용해서 “slack_token_path”를 등록한다. 이 환경 변수(slack_token_path)는 token 정보가 존재하는 “경로와 파일명”의 조합이다.

이 경로에 해당 파일을 만들기 위해서 volumeMounts를 이용해서 지정된 경로에 volume을 mount 한다.

이때 사용되어질 “secret” 타입의 volume은 token 정보를 Pod에서 사용할 수 있도록 하는 “Secret” 객체이다.

token은 민감한 정보이기 때문에 별도의 과정을 통해서 전달되어 진다고 상상해 볼 수 있다. 어떠한 방식으로든지 전달된 token을 이용해서 “Secret”을 생성하는 과정을 거칠 수 있다.

Secret을 생성하는 인자 중 에 “generic”을 사용한다. generic은 로컬의 파일등을 이용해서 Secret을 만들겠다는 의미로 type에 따라서 Secret을 생성하는 방식을 달리 할 수도 있다.

6.Running Mongodb Database in Kubernetes With Statefulsets

지금까지의 과정 중에서 가장 어려웠다.;;; 그리고 제대로 이해를 한건지도 잘 모르겠다.

일단을 이해했다고 생각되는 정도만 정리를 해봤다.

이번 과정은 StatefulSet에 대한 이해를 목표로 하고 있다.

StatefulSet에 대한 이해를 위해서는 Headless Service에 대한 이해와 StorageClass에 대한 이해가 필요했다.

StorageClass는 상태를 갖는걸 확인하기 위한 실습이 필요하다보니, Volume을 사용해야 하고 StatefulSet의 scale을 조정하게 되는 것에 따라서 Volume도 같이 조정이 되어야 해서 StorageClass 컨트롤러를 통해서 Volume을 동적으로 조정할 수 있도록 하려고 사용 된 것 같다.

(StorageClass는 다음에 기회가 되면 다시 좀 알아 봐야 할 것 같다.)

StatefulSet의 목적은 기존에 Deployment에서 상태가 없는 Pod들을 관리하기 위한 목적이 포함되어 있었다고 하면, 이 컨트롤러는 상태를 갖는 Pod를 관리하기 위한 목적이라고 이해를 했다. 데이터라는 상태를 갖는 mongodb를 실습에서 사용을 한 것 같다.

그리고 Deployment같은 컨트롤러의 경우에 Service 컨트롤러를 통해서 노출을 시켜줄 수 있었던 것 처럼 StatefulSet도 Service 컨트롤러를 이용해서 노출을 한다. 여기에서 StatefulSet은 Headless Service라고 불리는 설정으로 노출이 되게 된다. Service를 정의하고 있는 yaml파일을 보시면 “clusterIP: None”으로 지정이 되어 있는걸 볼 수 있다. 이는 로드밸런서의 영향을 받지 않는 서비스를 정의할 때 사용한다고 한다.

StatefulSet 컨트롤러를 통해서 관리되는 Pod는 생성되는 순서도 중요하기 때문에 Pod를 조회해보면 이름이 mongo-0, mongo-1… 과 같이 순서를 확인할 수 있는 숫자가 붙어 있다.

생성되는 순서도 0부터 차례대로 생성이 되고 scale을 통해서 replicas를 조정하면 차례대로 늘어나고 역순으로 줄어드는걸 확인 할 수 있었다.

Volume에 대한 이해가 없이 StatefuleSet을 바로 이해를 하려고 한다는게 무리가 있어 보인다. 그래서 이후에 꼭 Volume에 대한 이해를 해보는 기회를 갖어야 할 것 같다.

5.Continuous Delivery With Jenkins in Kubernetes Engine

이번 과정은 Jenkins와 Kubernetes를 이용한 배포 자동화를 실습해 볼 수 있었다.

분량은 많지만 이해가 어렵지는 않은 내용이었다.

실습을 해보면서 Namespace라는 객체와 Helm이라는 패키지 관리 도구를 사용해 볼 수 있다. Namespace는 일반적으로 우리가 알고 있는 용도인데, 논리적으로 무언가를 구분지어서 사용하고 싶을때 사용하는게 목적이라고 보면 될 것 같다.

이전 과정에서 배웠던 배포를 위한 전략에서는 label을 사용했었다면, 이번에는 Namespace를 사용해서 비슷한 문제 상황을 해결하는걸 경험해 볼 수 있다.

그리고 helm은 Kubernetes의 클러스터에 올라갈 수 있도록 미리 정의해서 배포해 놓은걸 사용할 수 있게 해주는 도구였다. 이번 실습 과정 중에는 Jenkins를 helm을 이용해서 설치를 했다.

(뒤에 있는 과정 중에서 helm을 좀 더 자세하게 다루는것 같으니 여기서는 이렇다는 정도만 확인하고 넘어가도 될 것 같다.)

이제 Jenkins를 통해서 배포가 자동화 되는 시나리오를 학습해 보기 위해서 서비스가 제공되는 과정을 만들어 놓는다.

실제 서비스와 Canary 배포를 위한 Namespace를 “production”, “canary”로 만들어서 배포를 하고 Service를 적용해 놓았다.

그리고 Jenkins에서 Pipeline을 만들어 놓는다.

이 과정중에서 가장 중요한건 “Scan Multibranch Pipeline Trigger”가 아닐까 싶다.

이 설정으로 인해서 지정한 git 경로의 branch들에 변화가 생기는걸 지속적으로 확인하고 변화가 있을때 배포 작업을 시작한다. branch에 변화가 생긴걸 확인하게 되면 build 및 기타등등의 Jenkins가 목적으로 하는 작업들을 수행 후, Jenkins에서 Kubernetes의 클러스터에 해당하는 branch 명으로 Namespace를 만들어서 배포하게 된다.

개발하는데 사용이 되었던 branch가 “new-feature”라면 “new-feature”라는 이름의 Namespace가 생성되고 해당 Namespace에 배포를 하게 된다. Canary 배포를 하고자 한다면, branch를 “canary”로 만들어서 배포를 하면 된다. 그리고 master branch의 경우에는 Jenkinsfile에 보면, master branch의 경우 “production” Namespace를 이용하도록 지정되어 있다.

이번 과정에서는 Namespace를 활용하면, 하나의 Kubernetes 클러스터 상에서도 각자 목적이 다른 솔루션들이 영역 구분을 하고 배치해도 흐름이 끊기지 않고 계속해서 작업이 이어지도록 할 수 있다는걸 학습했다.

각 객체들(Deployment, Service, Namespace …)의 정의와 특성 그리고 한계점에 대해서 명확하게 파악해 놓으면 필요한 상황별로 설계를 하는데 도움이 많이 될 것 같다.

4.Managing Deployments Using Kubernetes

이번 장에서는 “Deployment”가 어떤 역할을 할 수 있으며, 이를 이용해서 취할 수 있는 배포 전략에 대해서 알아볼 수 있었다.

(이번 장에서 실습을 위해서는 필수적으로 compute/zone 설정을 us-central1-a로 지정해 놓아야 cluster 생성을 정상적으로 할 수 있다.)

Deployment 뿐만이 아니라 다른 객체들을 사용하기 위해서도 yaml을 작성할때 각 필드에 대한 정보를 어디에서 확인해야 하는지가 궁금했다.

$ kubectl explain deployment

$ kubectl explain --recursive

$ kubectl explain deployment.metadata.name

등과 같이 확인을 할 수 있다.

Deployment는 ReplicaSet으로 Pod를 관리할 수 있도록 한다. 그래서 “replicas”를 조정해서 서비스를 확장하거나 축소할 수 있다.

Deployment가 수정되면 Rolling 업데이트 방식으로 기존의 Pod가 줄어들면서 새로운 Deployment가 적용된 Pod가 늘어나는 방식으로 교체가 된다.

ReplicaSet을 조회해보고 rollout의 history를 조회해서 업데이트의 상태를 확인해 볼 수 있었다. 이렇게 Deployment가 적용되는 방식에 대해서는 “rollout”의 pause, status, resume, undo 등으로 상태등을 조정할 수 도 있었다.

배포를 관리하는 방식으로 Canary, Blue-Green 과 같은 방식들이 있다.

처음에 Canary 배포에 대한 부분은 실습을 하다만 느낌이 들었는데, 그런 느낌은 Canary 배포의 목적에 대해서 알지 못 해서 그랬던것 같다.

실제 배포를 하기 전에 서비스의 실사용 환경에서 제한된 사용자를 선택해서 테스트 해보기 위한 정도라고 생각을 하고 실습을 다시 해보니 괜찮은 방법 같아 보였다. 실 서비스로 사용되어지는 Deployment와 배포되기 전에 확인을 위해서 사용되어질 Deployment(Canary)를 구분 지었을때, 문제가 발생하거나 하면 Deployment(Canary)만 조정을 하면 되니 신속한 관리가 가능할것 같았다.

Blue-Green 전략은 두 개의 Deployment(Blue/Green)를 적용해 놓고 Service를 원하는 Deployment에 적용시켜 놓는 방식으로 클러스터의 자원을 충분히 확보할 수 있을때 사용하면 좋은 전략인것 같다.

배포를 위한 전략들을 하나씩 실습해보면서 Deployment, Service 등의 각 객체의 특성을 잘 파악하고 있어야만, 어떻게 배치 할 수 있는지 그리고 어떤 식으로 운영할지에 대해서 계획 세울수 있겠다 싶었다.

3.Orchestrating the Cloud With Kubernetes

이번 장은 우선 “nginx”와 “monolith”라는 단어에 현혹되지 않도록 주의를 해야 할 것 같다.

최초에 nginx가 실행되는 Pod는 그거대로 Kubnernetes 작동에 대한 내용이고, Pod에 대한 내용에서 언급되는 nginx와 monolith는 또 그것대로의 내용을 설명한다.

그리고 다음에 나오는 port-forward에 대한 내용은 또 완전히 별개의 내용이다.

port-forward는 “Service”로 노출하지 않는 Pod에 대해서 테스트등을 해보기 위해서 아주 유용한 도구인 것 같다.

port-forward로 “monolith” Pod의 80 포트를 10080을 지정하면 local의 10080 포트를 사용하고 있는것 처럼 이용이 가능하다.

“Service”는 Pod가 수시로 죽고 살아나는 과정 중에서 외부에 노출될 수 있는 일정한 IP를 제공하기 위한 목적이다.

“Service”는 “selector”를 이용해서 Pod에 지정된 “label” 별로 Pod들을 선택할 수 있다. 그리고 “Service”의 형태로는 “ClusterIP”, “NodePort”, “LoadBalancer”가 있다.

“secure-monolith” Pod를 생성하는 과정중에 “secret”와 “configmap”을 생성하는게 나오는데 아직은 크게 신경쓰지 않아도 될 것 같다.

둘 다 비슷한 성격을 갖는데, “secret”는 패스워드 처럼 민감 데이터를 다루는 성격이고, “configmap”은 환경 설정과 같은 성격의 정보를 다룬다. 특성 또는 상태를 갖는 정보를 포함하지 않고 Pod가 관리될 수 있도록 해준다.

“Deployment”는 “replicas” 항목에 지정된 만큼의 Pod를 관리하기 위한 용도이다.

앞부분에서 약간의 착각을 할 수 있는 부분이 있기는 했었지만 전체적으로 간단하게라도 kubernetes를 이용한 관리방법에 대해서 이해를 할 수 있는 장 이었던것 같다.

2.Hello Node Kubernetes

2번째 과정에서는 Kubernetes의 기본적인 동작 방식에 대해서 알아 볼 수 있었다.

Kubernetes의 클러스터를 생성하는 과정은 Google Cloud Platform에서 기능을 제공하고 있어서 아주 간편했다.

책을 보면서 Kubernetes 공부를 처음 시작하는 과정에서 가장 시간을 많이 소비하게 되는 부분이었는데, 플랫폼에서 제공되는 기능을 glcoud를 이용해서 node와 각 node의 기본적인 설정을 지정후 cluster를 생성하는 명령 하나로 모든 과정이 생략될 수 있게 되었다.

그리고 Registry Server에 등록해 놓았던 Image를 이용해서 직접 “Pod”를 만들어보고, “Pod”의 개념과 “Deployment”의 개념이 무엇인지에 대해서 대략적으로 알아 볼 수 있었다.

다음으로 “Service”를 이용하면, 배포된 “Pod”들이 어떻게 외부에 노출이 될 수 있는지에 대해서 알아 볼 수 있었다.

기존에는 로컬 머신에서 VM들을 생성해서 학습을 하다 보니 외부에 정말 노출이 되는건가? 라는 의문이 들었었는데, “LoadBalancer”를 통해서 IP를 할당 받아서 해보니, 제공하고자 하는 기능이 “Service”를 통해서 외부에 노출이 될 수 있다는걸 확인 할 수 있었다.

서비스의 규모를 조정하기 위해서 scale을 이용해서 Pod를 관리하는걸 확인해 볼 수 있기도 했다.

마지막으로 서비스가 업데이트되었을때 배포를 하기 위한 과정에 대해서 알아보면서 Image를 새로 만들고 Registry Server에 재 등록을 하고 Deployment를 수정하는 과정을 실습해볼 수 있었다. 과정중에 약간 아쉬웠던 점은 Deployment가 수정되고 난 다음에 기존에 Pod가 종료되고 새로운 버전의 Pod가 활성화 되는 과정에 대해서도 확인을 해보면 과정을 이해하는데 더 좋을것 같았다.

(Deployment를 edit하는 과정을 완료하고, 바로 “kubectl get pods”를 해보면 확인이 가능하다.)

추가 사항으로 Kubernetes 대시보드를 보기 위한 방법을 학습하는 부분에서는 해당 url 뒤에 “ui”로 변경을 해보았으나 대시보드가 보여지지는 않았다.

1.Introduction to Docker

구글에서 지원해주는 “2019 클라우드 스터디잼 입문반” 스터디에 참여하기 시작했다. 이 스터디는 어떠한 형태로든 모인 멤버들이 일정 기간(17~127) 동안에 실습이 겸해진 과정(QWIKLABS - Kubernetes in the Google Cloud)을 완수하는 방식이다.

첫번째 과정으로 “Introduction to Docker” 을 진행 했다. 다음은 과정을 진행하면서 학습할 수 있었던 내용의 요약이다.

처음 해보는 과정이라서 그런지 익숙해지기 위한 시간이 약간 필요했지만 그다지 어렵지는 않았다.

Docker의 Image와 Container 그리고 Dockerfile의 이해를 위한 몇 가지의 실습이 진행되었다. 실습들을 통해서 Image와 Container를 어떻게 구분지어서 생각해야 하는지를 학습할 수 있었다.

그리고 만들어진 Image는 어떻게 배포하고 관리 되어져야 할 지에 대한 이해를 위해서, 구글에서 서비스하고 있는 플랫폼(Google Cloud Platform)이 제공하는 Registry Server에 gcloud라는 도구을 이용해서 Image를 등록하고 등록된 이미지를 이용하는 실습을 진행되었다.

이렇게 해서 첫번째 과정이 완료 되었다.

간단한 실습과 설명이었지만 Docker 관련 책에서의 분량으로 가늠해 보면 절반 정도의 내용을 압축해 놓은 과정이지 않았나 싶다.

cscope 사용(Go)

Go언어 개발 환경(추가)

Go언어로 작성한 코드를 분석하기 위해서 cscope가 필요할 때가 있다.
vim에서 cscope를 사용하기 위한 과정을 정리해 보려고 한다.

cscope를 설치한다.

$ sudo apt-get install cscope

mkscope.sh 파일을 만든다.

mkscope.sh의 내용은 다음과 같다.
이 스크립트는 https://www.cnblogs.com/shaohef/p/7358111.html 의 내용을 수정해서 사용했다.

#!/bin/bash

# 기존에 존재하던 files와 out 파일을 제거한다
rm cscope.files cscope.out

# $GOROOT가 존재하지 않으면 설정한다.
if ["$GOROOT" = ""]; then
    echo "GOROOT is not set"
    GOROOT=`go env | grep "GOROOT" | cut -d "=" -f2`
    GOROOT=${GOROOT#\"}
    GOROOT=${GOROOT%\"}
fi
echo $GOROOT

# Go 표준 패키지
go_src=$GOROOT/src

# Go언어 표준 패키지로 제공되는 go 파일의 경로를 cscope.files로 만든다.
find $go_src -name "*.go" -print > cscope.files
# 현재 디렉토리 존재하는 go 파일의 경로를 cscope.files로 만든다.
find . -name "*.go" -print > cscope.files


# cscope 옵션으로 -b 옵션을 사용해서 데이터 베이스 생성 후에 심볼 찾기 화면이 안보이게 하고, 
# -k 옵션을 사용해서 default include(/usr/include) 경로의 내용을 사용하지 않도록 한다.
if cscope -b -k; then
    echo "Done"
else
    echo "Failed"
    exit 1
fi

mkscope.sh 의 사용권한을 변경한다.

$ sudo chmod 755 mkscope.sh

mkscope.sh를 사용하기 편리하게 경로를 이동한다.(/usr/local/bin)

$ sudo mv mkscope.sh /usr/local/bin

cscope를 편리하게 사용하기 위해서 quickr-cscope.vim을 설치

vimrc.local에 vundle을 추가

Plugin 'ronakg/quickr-cscope.vim'

quickr-cscope의 단축키는 다음과 같다.

<leader>g : 커서 아래의 함수가 선언되어 있는 곳으로 이동한다.
<leader>s: 커서 아래의 단어를 검색을 한다.
<leader>c: 커서 아래의 함수를 호출하는 함수들을 찾는다.
<leader>f: 커서 아래의 단어로된 파일을 검색한다.
<leader>i: 커서 아래의 함수 또는 키워드의 정의 내용을 출력한다.
<leader>t: 커서 아래의 단어로된 문자열을 검색한다.
<leader>d: 커서 아래의 함수가 호출하는 함수들을 출력한다.
<leader>e: egrep를 사용한 검색을 한다.

그리고 다음과 같이 직접 사용할 수 도 있다.

:cs find {g|s|c|f|i|t|d|e} keyword 

분석하기

vi를 시작하기 전에 항상 mkscope.sh를 실행해서 수정된 내용이 cscope에서 사용하는 데이터베이스인 cscope.out에 반영될 수 있도록 한다.

git push된 내용을 특정한 commit으로 되돌리기

“iss-123” 브랜치에서 작업하던 내용을 실수로 “iss-133” 브랜치로 push 하는 어이없는 실수를 했을때 “iss-133”에 push 된 내용을 이전으로 되돌려 놓아야 한다.

git log로 이전 commit의 id를 확인한다. (ex, d50fd96d2d7e5e5cf689b0943f2b2d20d4c2dda4)

[iss-133] $ git reset --hard d50fd96d2d7e5e5cf689b0943f2b2d20d4c2dda4

이제 변경된 내용을 push 한다.

[iss-13] $ git push --force origin prj

map에 key가 존재하는지 확인

map을 사용할때 해당하는 키가 map에 존재하는지 확인이 필요할 때가 있다.

var target[string]*item
...
target["apple"] = itm1
target["orange"] = itm2
...

value, ok := target["kiwi"]

Go언어 프로젝트에서 테스트 코드 작성 경험

주의, 아래의 내용은 학습을 하면서 이해한 정도에서의 정리라서 틀린 내용이 많이 있을 수 있습니다. 이제 테스트 코드 작성을 시도해보기 시작하는 뉴비라서… 잘못된 부분은 지적을 해주시면 감사한 마음으로 배우겠습니다.

테스트 코드는 왜?

현재 프로젝트를 진행 중 코딩을 할 때면 계속해서 의심과 두려움이 들었다.

“내가 제대로 작성하고 있는 게 맞나?”
“이렇게 작성하면 다른 데에서 문제가 생기는 건 아닌가?”

그리고 이런 걱정들은 다음의 2가지 원인 때문이지 않을까 라고 생각하게 되었다.
첫 번째, 코드 작성 후 실행해서 결과를 확인하기 위해서 개인 개발 환경을 MessageQueue와 내가 보낸 요청에 대해 기대하는 응답을 전달해줄 MessageQueue 반대편의 모듈을 구성하기가 쉽지 않다는 것이었다.
두 번째, 기존에 코딩하면서 세워 놓았던 원칙을 기억하지 못하고 그 원칙에 어긋나는 코드를 작성하게 되었을 때, 전체 시스템에서 동작 중 알 수 없는 순간에 오동작하는 경우 때문이었다.

“테스트 주도 개발 TDD 실천법과 도구” 라는 책과 함께 몇 가지의 블로그 포스팅 그리고 몇 권의 책을 더 읽어 보면서 테스트 코드를 작성해 보면 문제의 원인을 해결할 수 있지 않을까 싶었다.
그래서 진행중인 프로젝트들에 테스트 코드를 작성해 보기로 했다.

테스트 코드는 뭘?

테스트 코드를 작성할 때에 주의해야 할 점은 다음과 같다.

  1. “질문 -> 응답 -> 정제 -> 반복” 의 과정을 거쳐서, 테스트를 작성하고 실패하고 정리하고 이걸 반복한다.
  2. 테스트 작성의 최소 단위는 함수이다. 하나의 함수를 기준으로 테스트 코드를 작성할 수 있다.
  3. 설계할 때 동작을 먼저 정의하고 그 동작에 필요한 속성을 고려한다.

테스트 코드는 어떻게?

Go 언어에서 테스트 코드의 작성은 표준으로 제공하는 “testing” 패키지만으로 가능하다.

테스트 코드 작성과 확인을 좀 더 편리하게 도와주는 다음과 같은 도구들의 도움을 받을 수도 있다.

  • testify ( go get -u github.com/stretchr/testify )
  • goconvey ( go get -u github.com/smartystreets/goconvey )

testify는 assert, http, mock, require, suite 와 같은 패키지를 제공해서 좀 더 편리한 검증을 할 수 있도록 도와준다. 그리고 goconvey는 터미널(go test)에서 테스트 결과를 확인하던걸 브라우저에서 편리하게 확인할 수 있게 해주는 도구이다.

goconvey의 사용은 테스트를 진행할 디렉토리에서 goconvey를 실행해 주면 웹브라우저가 실행되면서 다음과 같은 화면으로 현재 프로젝트의 테스트 상황을 한 눈에 볼 수 있게 해준다.

또한, 현재 coverage 상태를 다음과 같이 볼 수도 있다.

테스트 코드 작성

테스트 함수 작성 요령

  • 테스트 대상 함수와 이름을 1:1로 일치시킨다.

    대상: Getbalance() {...}  
    테스트: TestGetbalance(...) {...}
  • 테스트 대상 메소드의 이름을 메소드 단위로 작성을 하면서 뒤에 추가적인 케이스에 대한 내용을 추가한다.

    func Test_Withdraw_마이너스통장인출(...) {...}
  • 특정한 메소드가 아닌 테스트 시나리오를 대상으로 하는 테스트 메소드를 작성하는 경우도 있다.

    func Test_VIP고객이_인출할때_수수료계산(...) {...}
  • 테스트 케이스 시나리오는 정상적인 흐름에 대한 결과를 확인하거나, 예외나 에러 상황에 대한 결과를 확인하는 방법이 있을 수 있다. 그리고 a+b의 동작을 하는 메소드에 a와 b를 넣고 실제로 a+b를 한 결과와 일치하는지 확인해 보는 식의 경우가 있다.

Go 언어로 만들어진 프로젝트에서 테스트 코드를 작성하기 위해서는 몇 가지 규칙이 있다.

  • 파일명이 “xxx_test.go” 와 같이 파일명 뒤에 _test라고 지정한다.
  • 테스트를 진행할 함수의 이름은 “Test” 로 시작되어야 한다.

    func Test_Minus_작은_수에서_큰_수_빼기(t *testing.T) { }
  • 위의 ex에서 보이는 것 처럼 testing.T 를 인자로 받는다. testing.T 타입은 Error, Fail, Fatal, Log 등과 같은 테스트에 필요한 요소들을 갖고 있다. (자세한 내용은 https://golang.org/pkg/testing/ 을 참고)

테스트 코드 작성 예(1)

간단한 더하기 계산을 하는 패키지를 만들면서 필요한 테스트 코드를 작성하는 예를 한번 보자.
테스트 코드는 다음과 같다.

// add_test.go
package add

import (
	"testing"
)

func TestAdd(t *testing.T) {
	//arrange
	var x, y, res int
	x = 2
	y = 3

	//act
	res = Add(x, y)

	//assert
	if res != 5 {
		t.Fatal("Add의 결과가 옳바르지 않습니다")
	}
}

add 패키지의 구현은 다음과 같다.

// add.go
package add

func Add(x, y int) int {
	return x + y
}

이제 테스트를 진행해서 결과를 보기 위해서 다음과 같이 실행한다.

$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)

테스트 코드 작성 예(2)

이번에는 testify/assert 패키지를 이용한 테스트 코드 작성 예를 한번 보도록 하겠다. 그리고 현재 프로젝트 디렉토리에서 goconvey를 실행해서 브라우저를 통해서 테스트 함수들의 성공 또는 실패를 계속해서 확인해보자.

sample_1$ goconvey

2018/02/20 17:02:39 goconvey.go:61: Initial configuration: [host: 127.0.0.1] [port: 8080] [poll: 250ms] [cover: true]
2018/02/20 17:02:42 tester.go:19: Now configured to test 10 packages concurrently.
2018/02/20 17:02:42 goconvey.go:192: Serving HTTP at: http://127.0.0.1:8080
2018/02/20 17:02:42 integration.go:122: File system state modified, publishing current folders... 0 3038222316
2018/02/20 17:02:42 goconvey.go:118: Received request from watcher to execute tests...
2018/02/20 17:02:42 executor.go:69: Executor status: 'executing'
2018/02/20 17:02:42 coordinator.go:46: Executing concurrent tests: sample_1
2018/02/20 17:02:42 goconvey.go:105: Launching browser on 127.0.0.1:8080
2018/02/20 17:02:46 goconvey.go:113: ATTENTION: default value of option force_s3tc_enable overridden by environment.
기존 브라우저 세션에 새 창을 생성했습니다.
// oprcfg.go

type CfgItem struct {
  Name        string
  Value       string
  Owner       string
}

type Config struct {
  Cfg map[string][]CfgItem
}
 
func NewConfig() (*Config, error) {
  oc := &Config{}
  oc.Cfg = make(map[string][]CfgItem)
  return oc, nil
}
 
func (oc *Config) GetOprCfg(owner string) []OprCfg {
  if owner == "" {
    return nil
  }
  
  globalCategory := "global"
  srchCategory := owner
 
  globalConfigItem := oc.Cfg[globalCategory]
  srchConfigItem := oc.Cfg[srchCategory]
 
  var resOpcf []OprCfg
 
  for _, cf := range globalConfigItem {
    opcf := OprCfg{Name: cf.Name, Value: cf.Value, Owner: cf.Owner}
    resOpcf = append(resOpcf, opcf)
  }
 
  for _, cf := range srchConfigItem {
    opcf := OprCfg{Name: cf.Name, Value: cf.Value, Owner: cf.Owner}
    resOpcf = append(resOpcf, opcf)
  }
 
  return resOpcf
}

Config 객체를 생성하는 NewConfig() 함수가 있고, Config에서 owner에 해당하는 OprCfg를 반환하는 GetOprCfg() 함수가 있다.
이 두 함수에 대한 테스트 코드를 다음과 같이 작성했다.

// oprcfg_test.go

func Test_Config_New(t *testing.T) {
  // arrange
 
  // act
  oc, _ := NewConfig()
 
  // assert
  assert.NotNil(t, oc)
}
 
func Test_GetOprCfg(t *testing.T) {
  // arrange
  oc, _ := NewConfig()
 
  oc.SetCfgItem("key1", "value1", "key1-value1", "owner1")
  oc.SetCfgItem("key2", "value2", "key2-value2", "owner2")
  oc.SetCfgItem("key3", "value3", "key3-value3", "owner1")
  oc.SetCfgItem("key4", "value4", "key4-value4", "owner1")
  oc.SetCfgItem("key5", "value5", "key5-value5", "owner2")
  oc.SetCfgItem("key6", "value6", "key6-value6", "owner3")
  oc.SetCfgItem("key7", "value7", "key7-value7", "owner1")
  oc.SetCfgItem("key8", "value8", "key8-value8", "global")
 
  // act
  opcfs := oc.GetOprCfg("owner1")
  t.Log(opcfs)
 
  // assert
  var res []OprCfg
  res = make([]OprCfg, 5)
 
  // res에는 global이 먼저 쌓이고, 타겟 owner가 쌓인다
  res[0] = OprCfg{Name: "key8", Value: "value8", Owner: "global"}
  res[1] = OprCfg{Name: "key1", Value: "value1", Owner: "owner1"}
  res[2] = OprCfg{Name: "key3", Value: "value3", Owner: "owener1"}
  res[3] = OprCfg{Name: "key4", Value: "value4", Owner: "owner1"}
  res[4] = OprCfg{Name: "key7", Value: "value7", Owner: "owner1"}
  assert.True(t, assert.ObjectsAreEqual(&res, &opcfs))
}

테스트 코드를 작성하기 위해서 “Test” 로 시작하는 함수명을 갖도록 했다.
func Test_Config_New(t *testing.T) 함수에서 보면, 다음과 같은 주석을 볼 수 있다.

// arrange
...
// act
...
// assert
...

이는 테스트 코드를 작성하기 위한 일종의 템플릿 같은 것이다.
arrange: 테스트를 진행하기 전에 필요한 준비 작업 정도의 코드들이 위치할 수 있다. act: 실제로 테스트 되어서 결과를 확인하고자 하는 동작을 구현한다.
assert: act에서 진행한 동작에 대한 결과를 확인하는 용도이다.

첫 번째 테스트 코드인 Test_Config_New는 Config 객체의 생성이 잘 되는지 확인하기 위한 테스트이다.

arrange 절은 준비할 작업이 별도로 없기 때문에 생략되었다.
act 절에서 확인하고자 하는 NewConfig() 함수를 사용해서 Config 객체를 생성한다. assert 절에서 생성된 객체가 생성이 잘 되었는지 확인하는 과정으로 객체가 nil인지를 확인한다. 여기에서 testify/assert 패키지를 사용한다.

assert 패키지에는 다양한 검증을 위한 기능을 제공하고 있다. 간단하게 assert.NotNil은 주어진 객체가 nil이 아닌지 확인하는 함수이다. assert의 다양한 기능에 대해서는 godoc( https://godoc.org/github.com/stretchr/testify/assert ) 에서 확인해볼 수 있다.

두 번째 테스트 코드인 Test_GetOprCfg는 Config에 다양한 owner의 CfgItem들이 섞여 있는 중에서, 특정 owner와 onwer가 global로 지정된 CfgItem을 찾아서 OprCfg 만들어서 반환해주는걸 확인하기 위한 용도의 테스트이다.

arrange 절에서 GetOprCfg 함수를 테스트하기 위해서 Config 객체를 생성해서 임의의 설정(CfgItem) 값들을 owner를 달리해서 입력(SetCfgItem)한다.
act 절에서 GetOprCfg 함수에 owner로 “owner1” 를 전달해서 OprCfg 슬라이스를 반환값으로 받는다.
assert 절에서는 결과 값으로 받은 OperCfg 슬라이스가 global과 owner1이 owner인 값들만을 전달받았는지를 확인한다.

assert 패키지의 ObjectsAreEqual은 두 Object가 일치하는지를 확인한다.
assert 패키지의 True는 전달받은 값이 true인지를 확인한다.

이렇게 기본적인 테스트 코드를 작성할 수 있다. 그렇지만 모든 상황에 대해서 테스트 코드를 작성할 수 있는 것은 아니며, 다음과 같은 경우에 대해서는 테스트 코드를 작성하는 데 한계가 있다.

  • 동시성 문제
  • 접근제한자
  • GUI
  • 의존성 모듈 테스트

위의 경우는 고민하지 말고 테스트 코드 만들기를 포기하자…; (왜? 난 테스트 코드 작성 초보이니까!)

테스트 코드 작성 예(3)

의존성의 경우는 테스트더블을 이용해서 어느 정도는 해결(?) 할 수 있다. 예를 들어 다음과 같은 경우이다.

패스워드 저장의 구현을 위해서 암호화 모듈을 제공 받아야 한다. 
암호화를 위한 인터페이스와 암호화 방식이 미리 정해져 있다면, 암호화 모듈을 Mock으로 제작할 수 있다. 
미리 약속된 인터페이스 기능들을 Cipher라는 인터페이스로 정의하고 
그 인터페이스를 구현한 MockMD5Cipher를 만들어서 encrypt와 decrypt 내부에서 
“potato”와 potato의 md5 hash 값을 무조건 반환해준다.

이러한 경우를 우리 프로젝트에서 생각해 보면 MessageQueue에 의존적인 환경에서 개발하고 있는걸 생각해 볼 수 있다. MessageQueue의 동작을 테스트더블 객체로 만들어서 테스트 코드를 작성해 볼 수 있다.

“테스트더블” 이라는 용어는 대역, 스턴트맨을 의미하는 스턴트 더블이라는 단어에서 유래되었다.
데스트더블의 분류에는 “Dummy”, “Stub”, “Fake”, “Spy”, “Mock” 이 있다. 이들의 정의 또는 쓰임새는 다음과 같다.

  • Dummy: 객체의 겉모양만 만들어서 테스트를 진행할 수 있도록 하는 목적의 객체이다. (객체가 생성만 되면 되는 경우에 사용한다)
  • Stub: 특정 메소드가 호출되어야 정상적인 동작이 가능한 경우에 사용하는 객체이다.(객체가 특정 상태나 모습을 갖추면 되기 때문에 필요한 메소드에 하드 코딩으로 결과 값 등을 작성한다.)
  • Fake: 여러 상황을 대표하기 위해 Stub보다 좀 더 복잡한 구현이 들어간 객체이다.
  • Spy: 특정한 메소드가 호출되었는지 확인하기 위한 용도로 호출된 내역을 내부에서 기록하는 로직이 들어간다.
  • Mock: 위의 테스트더블 객체들은 상태에 대한 확인이 주된 용도였다면 Mock은 행위를 검증하는 용도로 많이 사용된다.

이제 MessageQueue에 의존적인 부분을 테스트더블 객체로 만들어서 테스트 코드를 작성해 보려고 한다.
변경된 이벤트 정보를 확인하는 GetChangeEventInfo 함수에 대한 테스트 코드를 작성해 보자.
테스트 코드를 작성하고자 하는 전체 로직은 다음과 같다.

  1. GetChangeEventInfo 함수가 호출되는 경우는 외부에서 이벤트가 변경되었음을 알리는 요청이 들어 온다.
  2. 전달받은 eventID 값을 이용해서 GetChangeEventInfo 함수가 MessageQueue에 연결되어 있는 Agent에게 변경된 이벤트 정보를 전달해 달라고 요청한다.
  3. Agent는 Database에서 조회해서 MessageQueue에 결과를 전달할 것이다.
  4. sample_1은 MessageQueue를 통해서 전달된 응답을 수집할 것이다.
  5. 결과로 받은 값을 확인한다.

테스트 코드를 작성하기 이전의 코드 상태는 다음과 같았다.

func (d *Agent) GetChangeEventInfo(eventID string) (*EventInfo, error) {
  *msg := MakeChangeMsg(blah-blah-chg-key, agent)*
 
  eventUUID := UUID_Data { UUID: eventID }
  any, _ := Any.Encoding(&eventUUID)
  *msg.Payload(any)
 
  /* message queue 관련 기타 등등의 구현 들
      ...
  */
 
  SendMessage(msg)
  val := RecvMessage()

  data := val.GetPayload()
  var ei EventInfo
  Any.Decoding(data[0], &ei)
 
  return &ei, nil
}

위의 코드 중에서 굵게 표시된 부분은 sample_1 이 아닌 외부 요소(MessageQueue)들에 대한 동작을 구현한 부분이다.
그리고 이 부분은 MessageQueue에 무언가를 요청하는 과정에서 매번 반복되기도 한다. 그래서 이 부분을 별도의 함수로 구현하기로 했다.

func (d *Agent) GetChangeEventInfo(eventID string) (*EventInfo, error) { 
  sendInfo := &MqSendInfo{
    key:     blah-blah-chg-key,
    recv: agent,
  }
 
  eventUUID := UUID_Data{ UUID: eventID }
  any, _ := Any.Encoding(&eventUUID)
 
  anyData, err := MqSend(sendInfo, any)
  if err != nil { return nil, err }
 
  var ei EventInfo
  Any.Decoding(anyData[0], &ei)

  return &ei, nil
}

위와 같이 코드를 별도의 함수(MqSend)로 빼놓고, MqSendInfo라는 struct를 인자로 받도록 변경을 했다. 함수와 구조체는 다음과 같이 작성되었다.

type MqSendInfo struct {
  key     string
  recv string
}
 
func MqSend(info *MqSendInfo, any *Any) ([]*Any, error) {
  msg := MakeChangeMsg(info.key, info.recv)
  
  msg.Payload(any)
 
  /* message queue 관련 기타 등등의 구현 들 
     ...
  */
 
  SendMessage(msg) 
  val := RecvMessage()
 
  return val.Msgs, nil
}

이렇게 해서 중복된 코드를 많이 줄일 수는 있었다. 그렇지만 MessageQueue의 의존성은 여전히 남아있다.

테스트 코드를 작성하는 입장에서는 우리가 전달한 eventID의 값을 이용해서 MqSend 함수가 EventInfo 값을 돌려주면된다. MqSend가 호출되었는지와 그 함수의 결과 값을 이용해서 GetChangeEventInfo가 우리가 원하는 결과 값을 만들어서 돌려주면 성공적으로 동작한 거로 간주할 수가 있게 된다.

그래서 MqSend 함수를 가짜로 구현하고 있는 테스트더블 객체를 만들려고 한다. + 우선 위의 MqSendInfo 구조체와 MqSend 함수의 이름을 MqMsg 구조체와 구조체의 메소드 Send로 변경하였다.

type Messenger interface {
  Send(any *Any) ([]*Any, error)
}
 
type MqMsg struct {
  key     string
  recv string
}

func (m *MqMsg) Send(any *Any) ([]*Any, error) {
  msg := MakeChangeMsg(m.key, m.recv)
 
  msg.Payload(any)
 
  /* message queue 관련 기타 등등의 구현 들 
     ...
  */
 
  SendMessage(msg)
  val := RecvMessage()

  return val.Msgs, nil
}

이제 테스트 코드를 다음과 같이 작성해 볼 수 있다.

type MockAgentMsgr struct {
  mock.Mock
}

func (m *MockAgentMsgr) Send(any *Any) ([]*Any, error) {
  args := m.Called(any, wait)
  return args.Get(0).([]*Any), args.Error(1)
}

func Test_GetChangeEventInfo(t *testing.T) {
  // arrange
 
  // 입력 값
  eventID := "event--uuid-1"
 
  UUID := UUID_Data { UUID: eventID }
  par1, _ := Any.Encoding(&UUID)
 
  // 출력 값
  var resAnytypes []*Any
  res1 := EventInfo { UUID: eventID, Name: "event1", Type: ET_START }
 
  resAnytype1, _ := Any.Encoding(&res1)
  resAnytypes = append(resAnytypes, resAnytype1)
 
  m := &MockAgentMsgr{}
  m.On("Send", par1).Return(resAnytypes, nil)
 
  agentMg := NewAgent(m)
 
  // act
  event, err := agentMg.GetChangeEventInfo(eventID)
 
  // assert
  assert.Equal(t, nil, err)
  assert.True(t, assert.ObjectsAreEqual(&res1, event))
  assert.True(t, m.AssertCalled(t, "Send", par1, true))
}

MqMsg의 테스트더블을 구현하기 위해서 Messenger 인터페이스에 Send 메소드를 정의했다.
MockAgentMsgr 구조체는 Messenger 인터페이스를 구현한 테스트더블이다.
MockAgentMsgr의 Send 메소드의 구현에서는 입력된 값과 반환되어야 할 값에 대해서 지정을 하고 있다.
이제 Test_GetChangeEventInfo 함수에서 입력값을 만들고 Send 메소드가 호출되었을 때 반환해야 할 출력값도 지정한다. MessageQueue에서 어떤 동작을 하든지 상관없이 입력된 값에 의해서 정해진 결과 값을 받는다는 가정이다. 그래서 작성된 코드 중에 다음과 같은 내용이 있다.

m := &MockAgentMsgr{}
m.On("Send", par1).Return(resAnytypes, nil)

Send 메소드가 호출될 때 입력값에 대해서 출력값을 정의해 놓은 것이다.

그런데 현재의 실행 코드상에서는 정해진 형태(MqMsg의 Send 메소드)만을 호출하는 구조이다. 이를 변경해야 할 필요가 있다. 그래서 우리는 Agent 객체를 생성하는 시점에 MessageQueue에 대한 정보를 지정해서 받는 형식으로 변경했다.

type Agent struct {
  Msgr Messenger
}
 
func NewAgent(msgr Messenger) *Agent {
  return &Agent{Msgr: msgr}
}

func (d *Agent) GetChangeEventInfo(eventID string) (*EventInfo, error) {
  eventUUID := UUID_Data{ UUID: evnetID }
  any, _ := Any.Encoding(&eventUUID)
 
  anyData, err := d.Msgr.Send(any)
  if err != nil { return nil, err }
 
  var ei EventInfo
  Any.Decoding(anyData[0], &ei)
 
  return &ei, nil
}

이제 테스트 코드에서처럼 NewAgent로 Agent 객체를 생성하기 전에 Messenger를 생성해서 전달받을 수 있게 되었다. (단, 실제로 Agent를 사용하는 시점에서는 매번 중복 코드가 발생하게 되었다. 이 부분은 다음에 다시 개선하기로 한다.)

이제 GetChangeEventInfo 함수를 구현하기 위해서 MessageQueue에 연결해서 디버깅하기 위한 과정 대신에 테스트 코드가 sample_1 에서 구현되어야 할 내용에 대해서 보장을 해줄 수 있게 되었다.

결론

이제 입력되는 값과 MessageQueue 건너편의 대상으로부터 받기를 원하는 값에 대한 정의만 정확하다면, MessageQueue에 직접 연결되지 않아도 우리는 sample_1 의 기능을 구현할 수 있게 되었다.
그리고 코딩 중에 세워졌던 원칙에 어긋나는 경우에 대해서 별도의 문서나 히스토리를 확인하지 않고 작성된 테스트 코드로 확인할 수 있게 되었다.

KVM 스터디(with go)

주의, 이 글은 KVM이 뭔지도 제대로 모르면서 학습한 내용을 정리한거라서, 정확하지 않은 내용이 다수 포함되어 있을 수 있습니다.
틀린 내용은 지적해 주시면 수정해서 반영하도록 하겠습니다.

준비물

노트북(우분투 16.04)

KVM이 뭔가요?

하이퍼바이저?

위키백과에 나와 있는 정의( https://ko.wikipedia.org/wiki/%ED%95%98%EC%9D%B4%ED%8D%BC%EB%B0%94%EC%9D%B4%EC%A0%80 ) 를 보면 이렇게 나와 있다.

“다수의 운영 체제를 동시에 실행하기 위한 논리적 플랫폼”

여기에 우리가 잘 알고 있는 VmWare, VirtualBox와 같은것도 있고, Xen, KVM 같이 좀 낯선 것들도 있다.

내용을 읽다보면, 왠지 중요해 보이는 단어 Type1, Type2, 전가상화, 반가상화 뭐 이런 단어들이 나온다.
Type1은 호스트에 하이퍼바이저가 존재하고, Type2는 호스트 OS에 설치된 응용 프로그램 형태로 하이퍼바이저가 존재하는거라고 보면 될 거같다.
그리고 전/반 가상화는 게스트에서 하이퍼바이저에 어떤 방식으로 접근을 하는지에 대한 구분 정도로 보면 어떨까 싶다…;
전가상화는 게스트OS가 본인이 가상화된 상태라는걸 모르게 모든 하드웨어가 가상화 되어 제공되는 상태이며, 반가상화는 게스트 OS가 가상화되고 있음을 인지하고 특정한 하드웨어를 사용하기 위해서 게스트 OS의 커널의 변경이 필요한 상태이다.
전가상화와 반가상화에 대해서 가장 이해하기 쉽게 설명된 글을 참고해보면 좋을것 같다. ( https://knowcitrixx.wordpress.com/2014/11/12/hypervisor-full-para-virtualization/ )

이건 공부하면서 이런거 같다라고 정리한 내용이니 100% 믿지는 말기 바란다.

이 글에서는 여러가지의 하이퍼바이저들 중에서 KVM(Kernel-based Virtual Machine)을 사용해 보려고 한다.

어떻게 쓰는거지?

설치와 VM 생성

우리는 QEMU(KVM) 이라는 하이퍼바이저를 설치할거다.
우선 현재 CPU가 가상화를 지원하는지 확인을 해야한다.

$ egrep -c ‘(vmx|svm)’ /proc/cpuinfo

결과가 1보다 크면 가상화를 지원하는 CPU 이다.
그럼 KVM 관련 패키지들을 설치해 보자.
설치하기전에 os관련 업데이트를 하자.

$ sudo apt-get update
$ sudo apt-get upgrade

이제 진짜로 설치… qemu-kvm을 설치한다.
qemu-kvm은 qemu/kvm을 관리할 수 있는 기본적인 프로그램들이 설치된다.

$ sudo apt install qemu-kvm

뭔가 한참을 설치한다.

설치된 항목들은 확인 하기 위해서 터미널창에서 qemu까지 입력하고 탭을 몇 번 입력해보자.
qemu로 시작하는 command가 몇 개가 보여질거다.
qemu-img(qemu의 디스크 이미지 관리를 위한 명령어),
qemu-io(qemu용 저장 장치를 위한 진단, 조작 프로그램),
qemu-make-debian-root(데비안 루트 이미지를 생성),
qemu-nbd(network block device: 호스트의 블록 디바이스에 연결하여 사용하는 장치, qemu 이미지를 자체적으로 제공하도록 한다.)
qemu-system-i386, qemu-system-x86_64, qemu-system-x86_64-spice(qemu pc 시스템 에뮬레이터)
그리고 kv까지 입력하고 탭을 몇 번 입력해 보면 kvm으로 시작하는 command가 몇 개 보인다.
kvm, kvm-ok, kvm-spice

마지막으로 vir까지 입력하고 탭을 몇 번 입력해 보면,
virtfs-proxy-helper
가 보인다.

이제 KVM이 설치되었는지 확인을 해보자.

$ lsmod | grep kvm

을 하면 …

kvm_intel    192512    0
kvm        598016    1    kvm_intel
irqbypass    16384        1    kvm

위의 결과와 비슷한 화면이 보인다. 그럼 잘 설치 된거다.

그럼 이제 VM을 생성해보자.

우선 ubuntu 이미지를 하나 받아 놓자.
( https://help.ubuntu.com/community/Installation/MinimalCD )

$ wget http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/mini.iso

그리고 qemu-img로 디스크를 미리 만들어 놓는다.

$ qemu-img create ubuntu.qcow2 10G

생성한 디스크에 os 설치를 한다.

$ qemu-system-x86_64 -localtime -net  user -net nic -m 256 -cdrom mini.iso -hda ubuntu.qcow2 -boot d

이렇게 kvm을 이용해서 가상머신상에 ubuntu를 설치해 봤다.

Go언어는 뭐죠?

Go언어란?

Go언어에 대한 정의도 위키백과를 참고해 보자.( https://ko.wikipedia.org/wiki/Go_(%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%EC%96%B8%EC%96%B4) ) 구글에서 유명한 아저씨들(로버트 그리즈머, 롭 파이크, 켄 톰슨)이 만들기 시작했다.
2009년 11월에 세상에 공개가 되었다.

문법은 C랑 비슷하고…(라고 하지만 Go 코드에 익숙해지고 난 다음에 C코드 보면 정말 헷갈린다..;)

여러가지 편리한 도구들도 제공한다.
go build
go test
go fmt
go get
go vet
go run
godoc
gorename
go generate

그래서?

설치

설치는 다음과 같이 하면 된다.

  1. 기존에 설치된 버전이 있다면 삭제한다.

    $ rm -r /usr/local/go
  2. http://golang.org/doc/install 에서 다운로드 (또는$ wget –no-check-certificate https://storage.googleapis.com/golang/go1.7.1.linux-amd64.tar.gz 와 같이 원하는 OS 및 버전에 맞는 Go 바이너리를 wget으로 받을 수도 있다.)

  3. /usr/local/go에 압축을 푼다.

    $ sudo tar -C /usr/local -xzf gox.x.x.linux-xxx.tar.gz
  4. $HOME/.profile 에 다음을 추가한다.

    PATH=$PATH:/usr/local/go/bin  
    GOPATH=$HOME/work
  5. .profile을 적용한다.

    $ source ./.profile
  6. 설치가 되었으면 제대로 설치되었는지 확인 해보자.

    $ go version
  7. 결과가 다음이랑 비슷하게 나오면 성공한거다.

    go version go1.8.1 linux/amd64

Hello,World

이제 Hello, World 한번 찍어보자.

  1. 에디터(utf-8 편집 가능한걸로)로 다음과 같이,

    package main
    
    import fmt
    
    func main() {
        fmt.Println(Hello, World)
    }

main.go 파일을 하나 만들어서 작성해보자. 2. 이제 저장하고 빌드해보자.

   $ go run main.go
   $ go build
   $ go install

빌드하는 방법은 위와 같이 3가지가 있는데, run은 결과물을 굳이 만들지 않고 결과를 바로 확인 할 수 있는거, build는 현재 작업하고 있는 경로에 결과물을 만들어 놓는거다. 그리고 install은 만들어진 결과물을 $GOPATH의 bin 디렉토리 아래에 옮겨주기까지 한다.
3. 결과를 확인한다.

libvirt는 또 뭐에요?

뭐하는거?

KVM같은 하이퍼바이저를 관리(?)를 하기 위한 라이브러리 정도(?)

이건 또 어떻게 쓰나요?

우선 설치를 해보자.

$ sudo apt install libvirt-bin ubuntu-vm-builder bridge-utils

여기에서 libvirt-bin 은 libvirt 관련한 것들(virsh, virt-admin, virt-host-validate, virt-login-shell, virt-pki-validate, virt-xml-validate, virtlockd, virtlogd)이 설치된다. 그리고 /var/lib/libvirt라는 디렉토리가 생긴다.
ubuntu-vm-builder는 깨끗한 테스트환경, 가상 시스템 설치 프로세스를 자동화하거나 하는 VM을 빠르게 만들어 주는게 설치된다고 하는데 뭐가 설치되는지는 아직 잘 모르겠다.
그리고 bridge-utils는 브릿지 네트워크를 설정하기 위한 용도의 무언가가 설치된다고 하는데, 설치되는게 없는거 같다.

KVM에 대한 사용 권한은 루트 사용자와 현재 사용자만이 갖게 된다.
다른 계정을 추가하기 위해서는 다음과 같이 추가한다.

$ adduser user1 libvirtd

이제 virsh 이라는 libvirt를 다루기 위한 쉘 프로그램이 추가 되어 있는걸 확인 할 수 있다.
이게 나중에 유용하게 사용이 될 거다.

그리고 이제는 virt-manager라는 GUI기반의 VM 관리 도구를 설치한다.

$ sudo apt install virt-manager

virt-manager를 설치하게 되면 virt-install, virt-viewer, virt-clone, virt-xml, virt-convert 같은걸 추가로 사용할 수 있게된다.

virt-manager를 설치하지 않고 virtinst 를 설치해도 가능할거다…;

이제 virt-manager를 사용해서 vm을 생성하고 시작하고 등등의 관리를 GUI 상에서 할 수 있는데, 동일한 작업을 virsh에서 해보기도 하고…virsh로 해본걸 다시 코드로 작성해 보기도 하자.

libvirt-go 패키지를 설치한다.
우선 libvirt-dev 라이브러리의 설치가 필요하다.

$ sudo apt-get install libvirt-dev

그리고

$go get github.com/libvirt/libvirt-go

이제 libvirt-go 패키지까지 설치를 마쳤다.

그럼 이제 뭘 하면 되죠?

TODO(libvirt-go를 사용한 예제들)

core파일 만들기

Go언어에서도 core dump를 떨구는 방법이 있었다.

데이브체니님이 정리해 놓은 무려 2015년도의 글이다.
https://dave.cheney.net/2015/11/29/a-whirlwind-tour-of-gos-runtime-environment-variables

환경변수로 GOTRACEBACK 을 crash로 설정만 해주면 된다.

이런것도 모르고 Go언어에서는 dump를 생성할 수 없다고 말하고 다녔다…;

근데 위에 처럼 환경변수 설정하고도 core 파일이 안생긴다면, 다음 블로그의 글을 참고해 보면 좋을것 같다.

http://lapan.tistory.com/68

블로그의 내용처럼 ulimit -a 로 확인 해보니 core file size가 0으로 되어 있다.
ulimit -c unlimited 로 해주었다.
이제 panic을 발생시켜 보면 core 파일이 잘 만들어져 있는걸 확인 할 수 있다.

docker 시작하기

docker ce 설치

  • 오래된 버전 제거

    $ sudo apt-get remove docker docker-engine docker.io
  • 패키지 인덱스 업데이트

    $ sudo apt-get update
  • apt가 https를 통해서 저장소를 사용할 수 있도록 패키지를 설치

    $ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
  • docker의 공식 GPG 키를 추가

    $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  • fingerprint를 확인

    $ sudo apt-key fingerprint 0EBFCD88
  • 안정화 버전의 저장소를 추가

    $ sudo add-apt-repository \
    "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) \
    stable"
  • apt 패키지 인덱스 업데이트

    $ sudo apt-get update
  • 최근 버전의 docker ce를 설치

    $ sudo apt-get install docker-ce
  • 특정 버전의 docker ce를 설치하기 위해서 사용 가능한 버전을 나열

    $ apt-cache madison docker-ce
  • docker-ce의 특정 버전 설치(은 위에서 나열된 리스트 중에서 선택(2번째 컬럼)

    $ sudo apt-get install docker-ce=<VERSION>

delve를 사용한 Go언어 디버깅

소개

delve는 Go언어를 위한 debugger 이다.

delve설치

설치를 하기위해서는 Go 1.5 이상의 버전이 필요하다.
지금 시점에 Go 1.5를 사용하지 않을테니…

다음의 명령으로 설치한다.

$ go get github.com/derekparker/delve/cmd/dlv

이제 설치 끝! (Windows랑 MAC은 모르겠다.)

delve사용

프로세스 실행 제어, 변수, 스레드/goroutine 상태, CPU 레지스터 상태 등을 확인 할 수 있다.

  • Commands

    • attach: 실행중인 프로세스에 연결해서 디버깅을 한다.

      $ dlv attach pid [executable]

      실행중인 프로세스에 디버그 세션을 연결해서 프로세스를 제어한다.
      디버그 세션을 종료할때 프로세스도 종료할 수 있다.

    • connect: headless debug server(?)에 연결

      $ dlv connect addr  

      실행중인 헤드리스 디버그 서버에 연결 한다고 하는데 무슨 소리인지 모르겠다.

    • core: core dump를 확인?

      $ dlv core `<executable>` `<core>`  

      코어 파일과 실행 파일을 열어 코어 덤프가 발생되었을때의 상태를 검사한다.

    • debug: 현재 디렉토리의 main 패키지를 컴파일하고 디버깅을 시작한다. 패키지명을 입력하면 된다고 하지만 안되는거 같은데..

      $ dlv debug [package]  

      최적화 되지 않게 컴파일을 해서 시작하고 연결을 한다.

    • exec: 미리 컴파일 해 놓은 바이너리를 실행하고 디버그 세션을 시작

      $ dlv exec [./path/to/binary]

      미리 컴파일한 바이너리를 delve가 실행하고 바로 디버그 세션을 시작하기 위해서 연결(? attach) 한다.
      최적화되지 않은 디버깅을 위한 바이너리로 실행을 해야한다. (-gcflags=“-N -I”)

    • replay: rr trace를 재실행?

      $ dlv replay [trace directory]

      mozilla rr이 반드시 설치되어 있어야 한다.
      rr로 생성된 trace를 열어서 확인한다.(?)

    • test: test 바이너리를 컴파일하고 디버깅 프로그램을 시작한다.

      $ dlv test [package]

      새로운 디버그 세션에서 단위 테스트를 실행
      현재 디렉토리의 테스트를 디버그한다.
      패키지 이름을 지정하면 해당 패키지의 테스트를 디버깅한다.

    • trace: 컴파일하고 trace하는 프로그램을 시작한다?

      $ dlv trace [package] regexp

      -p(–pid) : 해당 Pid에 연결한다.
      -s(–stack) : stack trace를 보여준다.
      trace의 sub-command로 입력된 정규표현식과 일치하는 함수들에 trace-point를 설정하고 해당 함수에서 정보를 출력한다.
      디버그 세션을 시작하지 않고 어떤 기능을 하는지 확인하고자 할 때 유용하다.

    • version: 버전 정보를 출력한다.

      $ dlv version
  • CLI Commands

    • args: 함수의 인자들을 출력한다
    • break(b): 브레이크포인트를 지정

      $ break [name] <linespec>  

      ex) $ break main.go:10

    • breakpoints: 활성화되어 있는 프레이크포인트 정보를 출력

    • check: 현재 위치에 체크포인트를 설정한다

    • checkpoints: 체크포인트들을 출력한다.

    • clear: 브레이크포인트를 제거한다.

    • clear-checkpoint: 체크포인트를 제거한다

    • clearall: 다중의 브레이크포인트를 제거한다

    • condition: 브레이크포인트이 조건을 지정한다.

    • continue: 브레이크포인트전까지 실행하거나 프로그램을 종료한다.

    • disassemble: Disassembler(?)

    • exit(q): 디버거를 종료한다.

    • frame

    • funcs

    • goroutine

    • goroutines

    • help

    • list

    • locals

    • next(n) 다음 라인으로 넘어간다.

    • on: 브레이크포인트에 도달했을때 실행할 명령을 지정(print, stack, goroutine)

      $ on <breakpoint name or id> <command>
    • print(p):

      $ [goroutine <n>] [frame <m>] print <expression>  
    • regs

    • restart®: 체크포인트나 이벤트에서 프로세스를 다시 시작한다.

      $ restart [event number or checkpoint id]
    • rewind(rw): 브레이크포인트나 프로그램 종료되는 지점까지 뒤로 돌아간다.

    • set: 변수의 값을 변경한다.

      $ [goroutine <n>] [frame <m>] set <variable> = <value>
    • source

    • stack(bt): stack trace를 출력한다.

      $ [goroutine <n>] [frame <m>] stack [<depth>] [-full]  
    • step: 함수에 진입한다.

    • step-instruction

    • stepout: 현재 함수에서 나간다.

    • thread

    • threads

    • trace

    • types

    • vars

build

소개

Go언어로 개발을 하다보면 debug모드 release모드 같이 별도의 빌드 환경을 구성하고 싶을때가 있다.
그래서 build 옵션으로 특정한 환경별로 결과를 달리 할 수 있는 방법에 대해서 테스트한 내용을 정리해본다.

빌드

go 파일들을 빌드하기 위해서는 일반적으로 다음과 같이 한다.

$ go build

Go언어 문서중에 보면 https://golang.org/pkg/go/build/#hdr-Build_Constraints[Build Constraints] 에 관한 내용이 있다.
이 내용은 build 할 때 조건을 줄 수 있다는 내용이고 방법은 다음과 같이 하면 된다고 한다.

코드의 상단에 “// +build linux” 하고 한 칸을 띄우고 “package blah” 를 시작한다.

빌드를 하기 위해서는 “go build -tags linux” 와 같이 하면 “// +build linux”가 명시되어 있는 파일들을 빌드한다.
그리고 “// +build !linux” 라고 되어 있는 파일의 경우 tags 플래그로 “linux” 가 들어오면 빌드에서 제외된다.

이렇게 build 조건을 주는 방법을 이용해서 특정한 테스트 또는 디버깅을 위한 코드를 별도로 작성하고 확인 할 수 있을것 같다.

그래서 다음과 같은 예제를 작성해 봤다.

기본 코드를 다음과 같이 작성을 했다.

main.go 파일은 다음과 같다.

package main

func main() {
    Render()
}

이제 Render() 함수가 존재하는 render.go 파일을 다음과 같이 작성해 본다.
Render() 함수는 화면에 “release render”라는 내용을 보여주는 기능을 한다.

render.go 파일은 다음과 같다.

package main

import "fmt"

func Render() {
    fmt.Println("release render")
}

이제 빌드하고 실행을 해보자.

$ go build
$ ./build_constraint
release render

화면에 “release render”라는 내용이 보여지는걸 확인 할 수 있다.

이제 Render() 함수의 내용을 debug 용도로 “debug render” 라고 보여주려고 한다.

그래서 render_debug.go 파일을 다음과 같이 작성했다.

package main

import "fmt"

func Render() {
    fmt.Println("debug render")
}

이 상태로 그냥 build를 하게 되면 다음과 같은 메세지를 보게 된다. +

$ go build
# build_constraint
./render_debug.go:5: Render redeclared in this block
	previous declaration at ./render.go:5

Render() 함수가 중복해서 선언되어 있으니 당연한 결과이다.

이제 위의 코드에 build 옵션을 지정해 보자.

// +build debug

package main

import "fmt"

func Render() {
    fmt.Println("debug render")
}

코드 상단에 “// +build debug” 를 추가하고 반드시 한 칸을 띄워야 한다.

이제 build를 실행하면 build가 성공적으로 된다.
결과를 확인해 보자.

$ ./build_constraint
release render

화면에 보여지는건 여전히 “release render” 이다.
build 옵션을 “debug”로 해서 build를 시도해 보자.

$ go build -tags debug
# build_constraint 
./render_debug.go:7: Render redeclared in this block
	previous declaration at ./render.go:5

Render() 함수가 중복되어 선언 되었다고 한다.
render.go 파일과 render_debug.go 파일에 Render() 함수가 2번 선언된 것으로 확인이 되는것이다.

그래서 render.go에도 “debug”라는 빌드 옵션이 지정될때는 build에서 제외가 되도록 “// +build !debug” 를 추가한다.

// +build !debug

package main

import "fmt"

func Render() {
    fmt.Println("release render")
}

이렇게 추가한 다음 build를 하고 내용을 확인해 본다.

$ go build -tags debug
$ ./build_constraint
debug render

디버그용 메세지인 “debug render”가 화면에 보여진다.

일반적인 빌드를 시도해 본다.

$ go build
$ ./build_constraint
release render

이제 아무런 옵션 없이 build를 하고 확인을 해보면 “release render”가 화면에 보여지는걸 확인 할 수 있다.

위 내용의 예제는 다음의 경로에서 볼 수 있다.
https://github.com/sabzil/build_constraint

RabbitMQ 스터디(with go)

Go언어로 RabbitMQ를 어떻게 이용할 수 있는지 스터디하는 내용들을 대충 대충 정리하는 중…

RabbitMQ라는게 뭐지?

AMQP를 구현한 메세지 브로커
여기 저기에 있는 클라이언트들(Producer/Consumer)이 메세지를 서로 주고 받을 수 있도록 해주는 그런거?

설치

  • RabbitMQ 설치

    sudo apt-get install rabbitmq-server
  • 관리 플러그인 설치

    sudo rabbitmq-plugins enable rabbitmq_management   
    sudo service rabbitmq-server restart
  • 실행

    service rabbitmq-server start
  • 계정 등록(id:sabzil, pw:1234)

    rabbimqctl add_user sabzil 1234
  • 등록한 계정을 관리자 계정으로 변경

    rabbitmqctl set_user_tags sabzil administrator
  • 웹브라우저로 관리 플러그인에 접속

    localhost:15672  

용어

대충 요런것들에 대해서 찾아 보면 될 것 같은데…

  • 브로커(broker): 미들웨어, 메세지를 받고 전달
  • 가상호스트(virtual host): 가상 영역
  • 연결(connection): 물리적인 네트워크 연결(?)
  • 채널(channel): 논리적인 네트워크 연결(?)
  • 익스체인지(exchange): 생산된 새로운 메세지를 큐에 전달
  • 큐(queue): RabbitMQ 에 존재하는 우편함으로 메세지를 저장
  • 결합(binding): 익스체인지와 큐를 연결
  • 큐(queue)의 속성
    • durable: 브로커가 재시작되어도 큐를 선언한 상태로 유지 여부
    • autoDelete: 큐에서 소비할 대상이 없을때 큐의 유지 여부
    • exclusive: 다른 연결에서 큐를 사용 가능 여부
    • arguments: 사용자 정의 큐를 설정

Go언어에서 RabbitMQ 사용

amqp 패키지

  • Type은 다음과 같은 것들이 있다.

    • Acknowledger
    • Authentication
    • Blocking
    • Channel
    • Config
    • Configuration
    • Connection
    • Decimal
    • Delivery
    • Error
    • PlainAuth
    • Publishing
    • Queue
    • Return
    • Table
    • URI
  • 여기에서 Acknowledger와 Authentication은 인터페이스 이므로 패스,
    패키지 내부에서 사용하는 타입(?)들인 Blocking, Configuration, Decimal, Error, Publishing, Queue, Table도 패스
    Config, URI, Delivery는 DialXXX, Open 함수, Connection, Channel 타입에서 사용하는 용도이므로 대충 이런거구나 하고 패스
    Connection, Channel 타입에 대해서만 좀 알아 보면 좋을것 같다.

  • Connection의 메소드들에 대해서 알아보자

    • Channel()
    • Close()
    • ConnectionState()
    • LocalAddr()
    • Notify(Blocked, Close)
  • Channel의 메소드들에 대해서 알아보자

    • Acknowledger 인터페이스의 구현 메소드
      Ack(), Nack(), Reject()
    • Channel 기능에 대한 메소드 나열
      Cancel(), Close(), Confirm(), Consume(), Flow(), Get(), Publish(), Qos(), Recover()
    • Exchange, Notify, Queue, Tx 관리 메소드
      Exchange(Bind, Declare, DeclarePassive, Delete, Unbind)
      Notify(Cancel, Close, Confirm, Flow, Publish, Return)
      Queue(Bind, Declare, DeclarePassive, Delete, Inspect, Purge, Unbind)
      Tx(, Commit, Rollback)

tutorial

  1. “Hello World” (http://www.rabbitmq.com/tutorials/tutorial-one-go.html)
  2. Work Queues (http://www.rabbitmq.com/tutorials/tutorial-two-go.html)
  3. Publish/Subscribe (http://www.rabbitmq.com/tutorials/tutorial-three-go.html)
  4. Routing(http://www.rabbitmq.com/tutorials/tutorial-four-go.html)
  5. Topics(http://www.rabbitmq.com/tutorials/tutorial-five-go.html)
  6. Remote Procedure Call(http://www.rabbitmq.com/tutorials/tutorial-six-go.html)

“Hello World”

  • 응용프로그램에서 RabbitMQ로 전달되는 메세지는 큐에 저장된다.
  • 여러개의 Porducer에서 메세지를 큐에 전달을 하며, 여러개의 Consumer에서 데이터를 수신하려고 시도할 수 있다.
  • RaabbitMQ 클라이언트 라이브러리를 설치

    go get github.com/streadway/amqp
  • mq 서버에 연결

    conn, err := amqp.Dial("amqp://sabzil:1234@localhost:5672")  

    연결 포트는 5672번을 사용 (관리 플러그인은 15672번 포트를 사용)

  • Connection
    Dial(…) 에서 Connection 타입을 만든다.

    ch, err := conn.Channel()

    Connection은 소켓 연결 관리, 프로토콜 버전 협상(?), 인증을 처리
    Connection을 통해서 Channel을 생성한다.

  • Channel
    채널을 통해서 큐를 생성

    q, err := ch.QueueDeclare(...)

    큐에 대한 설정을 할 수 있을것 같다.
    name: 큐의 이름
    durable: 브로커 재시작 후에도 큐의 상태를 유지(true)
    autoDelete: 소비자가 없으면 유지 되지 않는다(true)
    exclusive: 다른 연결에서 큐를 사용하지 못함(true)
    noWait: 대기열이 서버에서 선언(true)
    args: 사용자 정의 큐를 구성할때 사용
    참고) 결합된 큐가 없을때, 목적지 큐를 찾지 못하면 그냥 사라진다.

  • 보내기/받기

    • 보내기(Publish)
      exchange: exchange 이름
      key: routing key의 이름(단일 큐일 경우에는 큐의 이름)
      mandatory: 전달할 수 없는 메세지일 경우에 처리
      immediate: 전달할 수 없는 메세지일 경우에 처리
      msg:
    • 받기(Consume)
      queue:
      consumer:
      autoAck:
      exclusive: true이면 서버가 메세지의 소비자, false이면 다수의 소비자가 존재
      noLocal: 동일한 연결에 대해서 한번 정송한 메세지를 다시 전송하지 않는다
      noWait: 요청이 들어오면 바로 전송한다.
      args:
  • Work Queues

CreateFormFile()을 사용하면 Content-Type이 고정되는 현상

mime/multipart를 사용해서 파일을 업로드 하려 할때 파일을 업로드할 필드를 생성하기 위해서 CreateFormFile()을 사용한다.
이를 이용해서 파일을 업로드하면 “Content-Type”이 “application/octet-stream” 으로 고정되어진다.

src/mime/multipart/writer.go의 CreateFormFile을 확인해 보면 다음과 같이 고정되어 있는걸 확인 할 수 있다.

h.Set("Content-Type", "application/octet-stream")

관련한 이슈가 올라온게 혹시 있지 않을까 해서 찾아보니.
이런 이슈가 있었다.
https://github.com/golang/go/issues/16425

bradfitz는 다음과 같이 답변을 하고 있다.

Use CreatePart. CreateFormFile is a very thin wrapper around CreatePart. Click https://golang.org/pkg/mime/multipart/#Writer.CreateFormField[] and then click the CreateFormFile heading to see its source code.

그래서 난 CreateFormFile() 함수의 구현을 수정해서 CreateFormImage()함수를 만들어서 사용하기로 했다.

file, fileHeader, _ := r.FormFile("image")
...
var b bytes.Buffer
wr := multipart.NewWriter(&b)

h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",  fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes("image"), escapeQuotes(fileHeader.Filename)))
h.Set("Content-Type", "image/jpeg")
fw, err := wr.CreatePart(h)

CreateFormFile()을 사용하던 부분을 위의 코드로 교체해서 전송 하면 Content Type이 원하는 형태(image/jpeg”)로 전달이 되어진것을 확인 할 수 있다.
escapeQuotes() 함수는 …/multipart/writer.go의 구현을 참고하면 된다.

Go코드로 HTML Form 전송

HTML, Javascript를 사용해서 Post방식으로 Form을 전송하는 동작을 Go 코드를 작성해 보자.
우선 POST로 전송된 Form을 받아서 처리를 하는 서버쪽을 다음과 같은 구성을 갖는 코드로 만든다.

package main

import {
    "net/http"

    "github.com/gorilla/mux"
}

func main() {
   r := mux.Newrouter().StrictSlash(false)
   r.HandleFunc("/upload", upload).Methods("POST")

   http.ListenAndServe(":8080", r)
}

func upload(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {

    }
}

이렇게 “http://localhost:8080/upload" 를 통해서 POST 방식으로 전달되는 요청을 받아서 처리하기 위한 큰틀을 만들었다.

이제 upload() 를 구현해 보자.
우선 Form의 필드로 “user”를 받아오는 부분을 구현해 보면 다음과 같다.

func upload(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        user := r.FormValue("user")
    }
}

위의 코드는 다음과 같은 HTML의 Form을 받아서 처리 하기 위한 구현이다.

<html>
    <body>
        <form action="/upload" method="POST">
            user: <input type="text" name="user">
            <input type="submit"  name="submit" value="submit">
        </form>
    </body>
</html>

이 HTML을 Go 코드로 구현을 하기 위해서는 http 패키지의 PostForm() 함수를 사용할 수 있다.

resp, err := http.PostForm("localhost:8080/upload", url.Values{"user", {"sabzil"}})

그럼 HTML의 Form을 통해서 파일을 업로드를 할 경우에는 어떻게 해야 할까?
그냥 url.Values를 사용해서 파일을 업로드 할 수 있을까? 안되는것 같다.

http 통신에서 파일을 전송하기 위해서는 mime/multipart 패키지를 사용할 수 있다.

우선 Form을 받아서 각 필드를 처리하기 위한 부분을 구현해 보면 다음과 같다.

func upload(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        user := r.FormValue("user")
        file, handler, _ := r.FormFile("image")
        defer file.Close()

        f, err := os.OpenFile("./" + handler.Filename, os.O_WRONLY | os.O_CREATE, 0666)
        if err != nil {
            return
        }
        defer f.Close()
        io.Copy(f, file)
    }
}

위의 코드는 http.Request를 통해 전달된 Form의 필드 중 “image” 필드의 파일을 FormFile() 함수를 사용해서 구한 다음 전달된 파일이름으로 서버에서 파일을 생성하도록 하고 있다.

이제 File을 전달하는 HTML 코드를 살펴보자.

<html>
    <body>
        <form action="/upload" method="POST" enctype="multipart/form-data">
            user: <input type="text" name="user"><br>
            file: <input type="file" name="image">
            <input type="submit"  name="submit" value="submit">
        </form>
    </body>
</html>

이렇게 하면 user라는 문자열 필드와 image라는 파일 필드를 갖는 Form이 전송된다.

이제 이 HTML의 내용을 Go코드로 구현을 하면 다음과 같다.

func main() {
    url := "http://localhost:8080/upload"

    var b bytes.Buffer
    w := multipart.NewWriter(&b)
    f, err := os.Open("./sample.jpg")
    if err != nil {
        return err
    }
    defer f.Close()
    
    fw, err := w.CreateFormFile("image", "./sample.jpg")
    if err != nil {
        return
    }
    if _, err = io.Copy(fw, f); err != nil {
        return
    }
    if fw, err = w.CreateFormField("user"); err != nil {
        return
    }
    if _, err = fw.Write([]byte("sabzil")); err != nil {
        return
    }

    w.Close()

    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return
    }

    req.Header.Set("Content-Type", w.FormDataContentType())

    client := &http.Client{}
    res, err := client.Do(req)
    if err != nil {
        return
    }
}

위의 코드는 로컬의 현재 위치에 있는 “sample.jpg” 파일과 “sabzil”이라는 이름을 전송하려고 한다.

mime/multipart의 Writer를 생성해서 File 필드를 하나 만들고(CreateFormFile), 일반 필드를 하나 만든다.(CreateFormField)
HTML의 Form에서 action에 해당하는 url과 파일 버퍼를 사용해서 request를 만든다(NewRequest)
만들어진 request와 http.Client를 이용해서 POST방식의 Form을 전달한다.

인코딩된 텍스트 디코딩하기

Javascript에서 encodeURIComponent() 함수를 사용해서 전달받은 문자열을 디코딩해서 보기 위해서는 “net/url” 패키지의 QueryUnescape() 함수를 사용할 수 있다.

email, err := url.QueryUnescape(user.Email)

vendor 관리

vendoring되어 있는 패키지를 관리해야 할 이유가 무엇인지에 대해서 찾아보다가 좋은 글을 발견했다.
https://gocodecloud.com/blog/2016/03/29/go-vendoring-beginner-tutorial/

이 글을 읽고 이해한 정도만을 요약


vendoring을 왜 하나?

Project A에서 사용하는 Pkg X의 리비전 1을 사용하고 있다. 그런데 Project B에서는 Pkg X의 리비전 2를 사용하려 한다.
그런데 $GOPATH 안에는 같은 패키지를 하나만 갖고 있을 수 있다.
Pkg X의 리비전을 Project A에 맞추면 Project B가 빌드를 실패하고, Project B에 맞추면 Project A가 빌드에 실패하게 된다.
그래서 패키지들을 vendor 아래에 위치 시킨다.

프로젝트를 진행하면서 vendoring 되어 있는 패키지들의 리비전을 공유, 관리하기 위해서 godep, govendor 등과 같은 걸 사용할 수 있다.
다른 도구들은 무시하고 여기에서는 govendor의 사용법만 알아본다.
govendor를 사용하면, $GOPATH에는 무조건 최신 리비전의 패키지를 받아 놓고, 프로젝트에서 필요한 리비전의 패키지만을 vendor에 복사해 놓고 사용할 수 있다.

govendor 사용

govendor를 설치한다.

$ go get -u github.com/kardianos/govendor

프로젝트 폴더로 이동한다.

$ cd yourproject

govendor를 사용하기 위한 초기화를 한다.

$ govendor init

이제 vendor 폴더가 생기고 그 안에 vednor.json이 생기게 된다.

사용할 패키지들을 github 등에서 clone 하거나 go get을 한다.
$GOPATH/src 아래에 받아져 있게 된다.

이제 yourproject 폴더안에서 “govendor add +external” 을 한다.
+external 은 src 아래 받았던 패키지이다.
govendor add 를 하면 yourproject의 vendor 폴더로 옮겨지게 된다.
그리고 vendor.json에 현재 add 한 패키지의 리비전등의 정보를 기록한다.

이 후에 필요에 의해서 패키지를 업데이하기 위해서는

$ govendor update

를 하면 최신의 패키지로 갱신된다.

그리고 현재 사용중이었던 패키지를 다시 받아오고 싶으면,

$ govendor sync 

를 사용하면 vendor.json에 기록되어 있는 패키지를 다시 받아온다.

GitHub 페이지에 Hugo 올리기

Blogger에서 Hugo( http://gohugo.io )로 갈아타는 과정을 정리해 본다.

Hugo 를 설치한다

https://github.com/spf13/hugo/releases 에서 본인의 환경에 맞는걸 찾아서 설치한다.
Linux는 deb를 제공하고 있지만, Windows는 exe파일을 제공하고 있어서 환경변수(PATH)를 잡아주던가, 환경변수가 잡혀있는곳에 복사를 해준다.
(이름도 hugo.exe로 바꿔주는게 사용하기에 더 편리한것 같다.)
MAC은 안 써봐서 잘 모르겠다…;;;

github에 저장소를 만든다

github에서 호스팅을 받아서 hugo를 사용하기 위해서는 2개의 저장소를 만들어야 한다.
hugo의 컨텐츠를 관리하기 위한 저장소가 필요하다.(ex: https://github.com/sabzil/blog)
그리고 컨텐츠를 보여주기 위한 github의 페이지 저장소(ex: https://github.com/sabzil/sabzil.github.io)

로컬에서 기초 작업을 한다

이제 로컬에서 hugo를 이용하여 컨텐츠를 관리하기 위한 워크 스페이스를 만들어 보자.
다음과 같이 blog이라는 새로운 사이트를 만든다.

$ hugo new site blog 

blog 디렉토리로 들어가 보면, themes가 있다.

$ cd blog   

여기에 원하는 테마(ex: hemingway)를 받는다.
테마는 http://themes.gohugo.io/ 에서 골라보면 된다.

$ cd themes  
$ git clone https://github.com/tanksuzuki/hemingway.git  

hemingway의 리모트를 제거한다.(이건 해도 그만 안해도 그만이다.)

$ cd hemingway    
$ git remote rm origin

이제 /themes/hemingway 에서 blog로 돌아온다.

$ cd ../..

config.toml을 수정해서 사이트의 기본 설정을 한다.
이 부분은 개인 취향과 테마에서 필요한 설정들을 하도록 한다.

이제 로컬의 blog 디렉토리에 처음에 만들었던 github의 blog( github.com/sabzil/blog ) 을 리모트로 등록한다.

$ git init  
$ git remote add origin git@github.com:sabzil/blog.git

그리고 github.com/sabzil/sabzi.github.io를 blog의 서브모듈로 등록한다.

$ git submodule add -b master git@github.com:sabzil/sabzil.github.io.git public

컨텐츠를 만든다

컨텐츠를 만들기 위한 md 파일을 생성한다.

$ hugo new post/blah.md

위와 같이 하면 /content/post/blah.md 파일이 생성되어 있다.
blah.md 파일의 상단 부분을 적당하게 수정하고, 아래쪽의 “+++” 다음 부터 내용을 작성한다.

컨텐츠 블로그에 반영

Linux의 경우 http://gohugo.io/tutorials/github-pages-blog/ 의 제일 하단에서 제공하는 deploy.sh를 사용하면 된다.
Windows는 배치파일을 만들어서 사용해도 되겠지만, 귀찮으면 deploy.sh의 내용이 별로 어렵지 않은 내용이니, 하나씩 따라하면 된다.

내용을 간략하게 보면, 다음과 같다.

로컬의 /blog 경로에서 시작한다.

$ hugo -t hemingway   

를 실행해서 테마가 적용된 블로그 내용을 public에 생성한다.
(혹시 개인 도메인을 사용하고 싶으면 CNAME 파일도 여기에서 등록을 해주면 된다.)

$ cd public

으로 public 디랙토리로 이동한다.

$ git add -A

public 디렉토리의 모든 파일을 add 한다.

$ git commit -m "blah"

이제 변경된 내용을 커밋한다.

$ git push origin master

지금 까지 작업한 내용을 github.com/sabzil/sabzil.github.io에 반영한다.

$ cd ..

blog 디렉토리로 돌아온다.

이제 blog의 변경된 내용을 github.com/sabzil/blog 에 반영을 한다.

$ git add -A  
$ git commit -m "blahblah"  
$ git push origin master

새로운 환경에서 컨텐츠 관리

항상 동일한 장소에서 컨텐츠를 만들고 있다면 모르겠지만,
회사, 집 그리고 노트북에서 사용을 하고 있다거나 OS를 새로설치하고 다시 사용하기 위해서는,
“github.com/sabzil/blog”을 새로 clone 받아서 사용해야 한다.
이때 필요한 절차는 다음과 같다.

blog을 clone 받는다.

$ git clone git@github.com:sabzil/blog.git

submodule로 등록해 놓은 public의 내용을 업데이트 한다.

$ git submodule init  
$ git submodule update

테마(themes/hemingway)의 리모트를 추가하고 테마를 갖고 온다.
(themes도 submodule로 등록해 놓고 사용하면 편리하지 않을까 싶다.)

$ cd themes  
$ cd hemingway    
$ git init  
$ git remote add origin https://github.com/masa0221/hemingway  
$ git pull origin master  

이렇게 하고 글을 작성하고 deploy.sh 를 실행하면, 잘 안된다.
원인은 public으로 들어가서 확인을 해보면,

$ cd public  
$ git status

다음과 같은 메세지를 보게 된다.

HEAD detached from 185cdaf  
nothing to commit, working tree clean

이게 무슨 상황인지는 다음의 링크를 확인해 보면 된다.
서브모듈 사용할 때 주의할 점들

그래서 링크에 나와 있듯이…

$ git checkout master

이제 다시 “blog/” 로 돌아가서, deploy를 하면 잘 올라간다.

tmux

tmux 시작

$ tmux  
$ tmux new-session(or new) -s [세션 이름] -n [윈도우 이름]

tmux 종료

$ ctrl-d  
$ exit

tmux detach(현재 클라이언트)

$ ctrl-b, d

tmux attach

$ tmux attach -t [세션 이름]

window 생성

$ ctrl-b, c

window 리스트

$ ctrl-b, w

window 이동(현재의 이전(다음, 이전)

$ ctrl-b, l(n, p)

window 종료

$ ctrl-b, &

window 이름 변경

$ ctrl-b, ,

pane 가로 나누기

$ ctrl-b, %

pane 세로 나누기

$ ctrl-b, "

pane 의 번호를 화면에 출력

$ ctrl-b, q

pane 크기 조절

$ ctrl-b, (ctrl + 방향키)

모든 pane의 크기 동일하게 만들기

$ ctrl-b alt-1 # 모든 vertical split 넓이를 동일하게 변경
$ ctrl-b alt-2 # 모든 horizontal split의 넓이를 동일하게 변경

pane 이동하기

$ ctrl-b, o

pane 이동하기

$ ctrl-b, 방향키  
$ ctrl-b {(})

pane 삭제하기

$ ctrl-d

pane 화면 스크롤 모드 시작

$ ctrl-b, [

pane 화면 스크롤 모드 종료

$ q

입력되는 내용을 모든 pane에 동일하게 적용

$ ctrl-b

: setw synchronize-panes

http://nodeqa.com/nodejs_ref/99
http://nodeqa.com/nodejs_ref/100
http://nodeqa.com/nodejs_ref/101
http://superuser.com/questions/209437/how-do-i-scroll-in-tmux

vi 팁

현재 작업중인 파일의 경로와 이름 확인

현재 작업중인 파일의 경로 또는 이름이 궁금할 경우가 있다.

ctrl + g  

또는

: f

80컬럼에 버티컬 라인

vi에서 80컬럼의 버티컬 라인을 생성하려면 다음과 같이 한다.

set colorcolumn=80

을 적용하면, 빨간색 라인이 생긴다.

파일 관리

NerdTree를 사용하지 않고 vi에서 파일과 디렉토리를 관리하는 방법

파일 네비게이션

  1. 표준모드(ex: i, o, a 같은걸 입력해서 입력모드로 전환되기 전 상태)
  2. “e.” (설명: “.” 는 현재의 디렉토리)

디렉토리 만들기

  1. 명령라인 모드 (ex: “:wq” 해서 현재 작업중인 내용을 저장하고 종료 하기 위한 명령을 입력하기 위한 모드)
  2. “:!mkidir foo” (ex: “foo” 라는 디렉토리 생성)

또는 “:!mkdir -p foo/bar” (ex: foo의 하위 디렉토리인 bar까지 생성)

buffer이동

vi를 사용중에 열려있는 buffer를 이동해야 할때가 있다.

다음과 같이 하면 열려있는 1번 bufffer로 이동한다.

:buffer 1  

열려 있는 buffer전체를 보려면,

:buffers  

와 같이 한다.

화면 분리

분리는 가로(vsplit), 세로(split)으로 나눌 수 있다.

:[vsp]lit [파일명]  
:[sp]lit [파일명]  

화면간의 이동은

ctrl + w [h j k l]

현재 화면 닫기는

:q

화면을 모두 닫기 위해서는

:qa

현재 커서가 있는 화면을 제외하고 모두 닫기

ctrl + w, o

vimgrep

현재 디렉토리내에서 “.go” 파일들중에서 문자(“TEXT”)에 대한 검색이 필요할 경우

:vimgrep /TEXT/ ./*.go

여러개의 결과들을 이동

:cfirst
:cnext
:cprev
:clast

검색된 결과를 리스트로 보기

:cw

vim-go 기본 템플릿 비활성화

vim-go가 언제부터인가 main.go 를 만들면 템플릿으로 fmt.Println(“vim-go”) 를 찍어주는 코드를 자동으로 생성해 주기 시작했다.

나는 아무것도 나오지 않기를 바랬는데, 이런게 나와서 매번 지우는 작업을 하는게 싫었다. 그래서 설정중에 go_template_autocreate 를 조절했다.

let g:go_template_autocreate = 0

와 같이 하면 이제 아무것도 없는 빈 화면이 나온다.

혹시라도 반복적으로 나오는게 좋은데 “vim-go”를 찍는게 아닌 특정한 코드가 필요하다면, “.vim/bundle/vim-go/templates/hello_world.go” 파일을 수정해도 된다. 또는 특정한 템플릿을 지정하고 싶다면, 다음과 같이,

let g:go_template_file = "hello_world.go"

로 해도 된다. 여기에서 hello_world.go 대신에 템플릿으로 지정하고 싶은 파일을 입력 하면된다.

template을 생성할때 왜 이름을 지정할까?

Go언어의 Template 엔진을 사용할때, 왜 New()의 파라미터로 이름을 만들어서 넘겨야 하는지가 궁금했었다.

Template and Associated templates라는 글을 읽고 어느 정도 이해를 하게 되었다.

다음은 위 글의 내용을 요약한 것이다.

template.New() 로 template을 만들때, 이름을 정해서 만든다.
이 때 FuncMap과 템플릿 리스트를 갖는 그룹(그룹의 이 글에서 사용하는 용어로 Associated Templates를 가르킨다)이 생성된다.
FuncMap에 대해서는,
https://golang.org/pkg/html/template/#FuncMap
http://goinbigdata.com/example-of-using-templates-in-golang/
http://technosophos.com/2013/11/23/using-custom-template-functions-in-go.html
를 확인해본다.

New()를 했어도 아직 템플릿 리스트에서 관리되는 대상은 아니다.
Parse(…)를 하면 템플릿 리스트에 추가가 된다.

처음에 생성한 template “t1”으로 New()를 하면 연관된 템플릿을 생성할 수 있다.
“t1”으로 생성한 template들은 동일한 템플릿 리스트에서 관리되어진다.

현재 템플릿 리스트에서는 “t1” 템플릿을 더 이상 만들 수 없다.

Lookup()으로 현재 템플릿 리스트에서 관리되고 있는 template을 찾을 수 있다.

Templates() 함수로 템플릿 리스트의 template들을 모두 구한다.

Execute() 와 ExecuteTemplate() 의 차이
Execute() 는 호출한 template 객체를 실행한다.
ExecuteTemplate() 는 호출한 template 객체가 관리되는 템플릿 리스트에서 인자로 넘겨진 이름의 template 객체를 찾아서 실행한다.

ParseFiles() 는 파라미터로 넘겨진 파일 이름들을 템플릿 리스트에서 관리하는 리스트의 New()로 template을 생성할때 인자로 넘기는 name의 역할로 사용된다.

ifconfig 사용

임시로 사용할 개발환경을 만들기 위해서 ubuntu 이미지를 사용하는데,
ifconfig 명령이 동작하지 않아서 보니, net-tools가 온전하게 설치되었지가 않은것 같다.

$ apt-get install --reinstall net-tools

css와 같은 정적 컨텐츠를 html에서 사용하기 위한 팁

Go의 “net/http”와 “html/template”를 이용해서 html과 css로 만들어진 웹페이지를 만들려고 할때, html 파일이 보여질때 css가 적용되지 않은 상태로 보여진다.
이 때 어떻게 해야 하는지 방법을 찾아가던 과정을 기록으로 남겨 놓는다.

프로젝트의 디렉토리 구조는 다음과 같다.

src\
 + prj
    + main.go
    + static
        style.css
    + template
        index.html

그래서 이렇게…

func main() {
  http.HandleFunc("/", indexHandler)
  http.ListenAndServe(":8080", nil)
}

func indexHandler(...) {
  // index.html
}

와 같이 하면 될 줄 알았다.
그런데 안된다.

그래서 어떻게 해야 하는지 찾아보니, FileServer로 css파일을 제공해 주면 된다고 한다. 그래서 다음과 같이 했다.

func main() {
  fs := http.FileServer(http.Dir("/static"))
  http.Handle("/static/, fs)
  http.HandleFunc("/", indexHandler)
  http.ListenAndServe(":8080", nil)
}

func indexHandler(...) {
  // index.html
}

근데 안된다.
http.Dir에서 만들어 낸 경로는 “/static”이 존재한다.
그래서 좀 더 찾아보니 StripPrefix를 사용해서 경로를 만들어 줘야 한다.
fs 핸들러에서 “/static”를 제거해준다.

func main() {
  fs := http.FileServer(http.Dir("/static"))
  http.Handle("/static/, http.StripPrefix("/static", fs))
  http.HandleFunc("/", indexHandler)
  http.ListenAndServe(":8080", nil)
}

func indexHandler(...) {
  // index.html
}

이제 잘된다!!!

css와 같은 정적 컨텐츠를 제공하기 위해서 사용할 수 있는 팁인것 같다.

http 패키지의 HandleFunc과 Handle

HandleFunc

  • http.ResponseWriter 와 *http.Request를 인자로 받는 함수를 전달받는다.
  • 내부적으로 ServeHTTP가 구현된 ServeMux를 사용한다.

    package main
    import (
    "net/http"
    )
    func indexHandler(w http.ResponseWriter, r *http.Request) {
    .... }
    func main() {
    http.HandleFunc("/", indexHandler)
    http.ListenAndServe(":8080", nil)
    }

Handle

  • http.Handler 인터페이스를 구현한 객체를 전달받는다.
  • 인자로 전달받은 객체는 http.Handler의 ServeHTTP(http.ResponseWriter, *http.Request)가 구현되어 있어야 한다.

    package main
    import (
    "net/http"
    )
    type indexHandler struct {
    }
    func (index *AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ...
    }
    func main() {
    index := new(indexHandler)
    http.Handle("/", indexHandler)
    http.ListenAndServe(":8080", nil)
    }

zsh(oh-my-zsh) 사용

zsh 설치하고 기본 쉘 변경

$ sudo apt-get install zsh  
$ which zsh  
/usr/bin/zsh  
$ chsh -s /usr/bin/zsh  

oh-my-zsh 설치

$ curl -L https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh | sh

설정(개인 취향)

.zshrc 의 ZSH_THEME를 minimal 로 변경

terminator 사용

설치

$sudo apt-get install terminator

단축키

분할(수직): ctrl + shift + e  
분할(수평): ctrl + shift + o  
닫기(현재 창): ctrl + shift + w  
닫기(전체): ctrl + shift + q  
새 탭: ctrl + shift + t  
분할된 화면간 이동: alt + 방향키  

vendor 사용 팁(내부 패키지)

프로젝트 내부에서 별도의 패키지를 만들어서 사용을 하려고 할때면,
import 경로를 “github.com/blah/project/internalpkg” 처럼 다 써야 했었다.
이제 vendor를 사용하면 pkg 이름만 import 해서 사용할 수 있게 되었다.

샘플 프로젝트는 다음과 같다.
https://github.com/sabzil/lotto

샘플 프로젝트에 대해서 약간의 설명을 더하면,
기존에는 command 패키지를 commands.go 에서 import 하기 위해서,
import “github.com/sabzil/lotto/command” 와 같이 사용해야 했었다.

이렇게 되면 lotto 프로젝트를 다른 사람이 fork 해서 사용하려고 하면 일일이 import 경로를 변경해줘야 하는 문제가 생기게 된다.

그래서 go1.6 부터는 vendor를 사용하면 이러한 문제를 해결할 수 있게 되었다.

src
  + github.com
     + sabzil
        + lotto
           + vendor
               + command

와 같이 command 패키지를 vendor 디렉토리 아래에 배치를 한다.
이제 commands.go 에서 command를 사용하기 위해서,
import “command” 와 같이 변경이 될 수도 있는 경로들은 제외하고, pkg 이름만 지정해주면 된다.

pkg 원격저장소 연결하기

최근에 go소스들을 분석하면서 특이한 pkg import 경로들을 보게되었다.

import "rsc.io/pdf"

와 같은 “github.com/rsc/pdf” 와 동일한 내용인데 그리고 rsc는 russ cox가 사용하는 닉네임인데…그럼 같은 pkg인가?
그래서 웹브라우저에서 rsc.io/pdf 를 입력하면 godoc로 이동을 하네?
어떻게 한걸까!? 궁금했다. 그리고 왠지 멋있어 보였다…;

그러다가 우연히 rob pike의 github page(https://github.com/robpike/robpike.github.io)를 보게 되었다.
index.html 에 이렇게만 코드가 되어 있었다.

<meta name="go-import" content="robpike.io/cmd hg https://code.google.com/p/rspace.cmd">
This is a git repository holding a redirect for the Go repositories referenced by robpike.io.

이게 뭐지? 그리고 roppike.github.io/cmd 안에는 여러 폴더들이 존재하고 그 안에는 index.html 파일 하나씩만 덩그러니 있었다.
그 내용들은 다음과 같았다.

<meta name="go-import" content="robpike.io/cmd hg https://code.google.com/p/rspace.cmd">

흠…meta tag를 사용하면 뭔가가 되나 보다 하면서 자료를 찾아봤다.
단서는 의외로 간단하게 나왔다.

https://golang.org/cmd/go/#hdr-Remote_import_paths  

에 보면 meta tag를 사용해서 원격저장소의 경로를 커스텀 도메인으로 사용할 수 있는 방법이 있었다.
그래서 도메인을 하나 구입했다.
io도메인(rsc.io, robpike.io 가 왠지 멋있어 보여서..;)을 따라하고 싶어…
나도 jaehoon.io 구매했었지만…너무 비싸서 연장을 못했다…;(지금은 sabzil.org 로…)

그리고 github page(sabzil.github.io)를 만들어 놓고, 거기에 내가 만든 pkg중에 하나를 골라서 한번 만들어봤다.

sabzil.github.io/cubrid

에 index.html 을 다음과 같이 만들었다.

<meta name="go-import" content="sabzil.org/cubrid git https://github.com/sabzil/cubrid">

이제 내 도메인(sabzil.org)과 연결을 했다.(github page를 커스컴 도메인에 연결하는 방법은 검색해보니 많이 나와서 쉽게했다.)

그리고 커맨드 창에서…

$ go get gonuts.kr/cubrid  

명령을 내리고 잠시 기다리니 받아진다!!! (오류가 나온다면 cgo관련한 설정 때문인것 같다.) + + 그리고 godoc.org에도 등록도 잘된다. + cubrid로 검색을 해보면, sabzil.org/cubrid 와 github.com/sabzil/cubrid 두개가 나온다.

russ cox는 웹브라우저에서 rsc.io/pdf 를 입력하면 godoc/rsc.io/pdf 로 redirect 가 되도록 해놓은것 같다.
그래서 나도 meta tag를 하나 더 추가했다.

<meta http-equiv="refresh" content="3;url=http://www.godoc.org/sabzil.org/cubrid">

이렇게 하니 godoc.org로 이동을 한다.
음! 왠지 멋있어 보인다..;

이제 열심히 코딩해서 pkg 많이 만들면 되는건가?;

html 코드는 https://github.com/sabzil/sabzil.github.io/blob/master/ringbuffer/index.html 을 참고하면 된다.

github page에 커스텀 도메인을 연결하면, https에 문제가 좀 있다.;

개발환경 설정

업데이트

$ sudo apt-get update  
$ sudo apt-get install build-essential

vim 설치

$ sudo apt-get install vim

vim 8.0

$ sudo add-apt-repository ppa:jonathonf/vim  
$ sudo apt-get update

xclip 설치

$ sudo apt-get install xclip

git 설치

  1. git 설치와 설정

    $ sudo apt-get install git  
    $ git config –-global user.name “blah”  
    $ git config –-global user.email “blah@blah.com”  
    $ git config --global core.editor vim  
    $ git config --global core.commentchar "*"  
    $ ssh-keygen -t rsa -C “blah@blah.com”  
    $ xclip -sel clip < ~/.ssh/id_rsa.pub
  2. github/bitbucket에 ssh key 값을 등록

GOPATH로 사용할 위치 생성

$HOME/go  
bin  
pkg  
src

Go 설치

  1. 기존에 설치된 버전이 있다면 삭제한다.

    $ rm -r /usr/local/go
  2. http://golang.org/doc/install 에서 다운로드

  3. /usr/loca/go에 압축을 푼다.

    $ sudo tar -C /usr/local -xzf gox.x.x.linux-xxx.tar.gz
  4. $HOME/.profile 에 다음을 추가한다.

    export GOPATH=$HOME/go  
    PATH=$PATH:/usr/local/go/bin:$GOPATH/bin  
  5. 적용

    $ source ~/.profile

ctags 설치

  1. 설치

    $ sudo apt-get install ctags
  2. go가 설치된 위치로 이동(/usr/local/go)해서 tag 생성

    $ sudo ctags -R
  3. vimrc 파일에 다음을 추가한다.(아래의 vimrc를 사용 할 예정이라면 안해도 됨)

    set tags +=/usr/local/go/tags

python과 cmake 설치

  • YouCompleteMe라는 vim 플러그인의 정상적인 설치를 위해서 python과 cmake를 설치한다.

    $ sudo apt-get install python-dev python3-dev  
    $ sudo apt-get install cmake

Vundle.vim을 이용한 NERDTree와 fatih/vim-go 설치

  • Vundle.vim 설치

    $ git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim
  • ~/.vimrc 파일을 생성해서 아래의 내용을 붙여 넣는다.

    set nocompatible " be iMproved, required
    filetype off " required
    
    set autoindent
    set smartindent
    set shiftwidth=4
    set number
    set expandtab
    set tabstop=4
    
    " set the runtime path to include Vundle and initialize
    set rtp+=~/.vim/bundle/Vundle.vim
    call vundle#begin()
    " alternatively, pass a path where Vundle should install plugins
    "call vundle#begin('~/some/path/here')
    
    " let Vundle manage Vundle, required
    Plugin 'gmarik/Vundle.vim'
    
    " The following are examples of different formats supported.
    " Keep Plugin commands between vundle#begin/end.
    " plugin on GitHub repo
    Plugin 'tpope/vim-fugitive'
    " plugin from http://vim-scripts.org/vim/scripts.html
    Plugin 'L9'
    " Git plugin not hosted on GitHub
    Plugin 'git://git.wincent.com/command-t.git'
    " git repos on your local machine (i.e. when working on your own plugin)
    "Plugin 'file:///home/gmarik/path/to/plugin'
    " The sparkup vim script is in a subdirectory of this repo called vim.
    " Pass the path to set the runtimepath properly.
    Plugin 'rstacruz/sparkup', {'rtp': 'vim/'}
    " Avoid a name conflict with L9
    "Plugin 'user/L9', {'name': 'newL9'}
    
    Plugin 'The-NERD-tree'
    Plugin 'bling/vim-airline'
    Plugin 'fatih/vim-go'
    Plugin 'nsf/gocode'
    Plugin 'Tagbar'
    Plugin 'Valloric/YouCompleteMe'
    
    " All of your Plugins must be added before the following line
    call vundle#end() " required
    filetype plugin indent on " required
    " To ignore plugin indent changes, instead use:
    "filetype plugin on
    "
    " Brief help
    " :PluginList - lists configured plugins
    " :PluginInstall - installs plugins; append `!` to update or just :PluginUpdate
    " :PluginSearch foo - searches for foo; append `!` to refresh local cache
    " :PluginClean - confirms removal of unused plugins; append `!` to auto-approve removal
    "
    " see :h vundle for more details or wiki for FAQ
    " Put your non-Plugin stuff after this line
  • vim 실행해서 “ :PluginInstall “ 을 해서 지정된 플러그인들을 설치한다.

  • NERDTree의 설정을 한다.

    $ sudo vim ~/.vimrc  

을 열어서 적당한 곳에 아래의 내용을 붙여넣는다.

nmap <F2> :NERDTreeToggle<CR>
let NERDTreeWinPos="left"
  • Tagbar 설정
    위의 NERDTree 설정 부분 아래에 추가한다.

    nmap <F3> :Tagbar<CR>
    let Tagbar="right"
    let g:tagbar_type_go = {
    \ 'ctagstype' : 'go',
    \ 'kinds'     : [
        \ 'p:package',
        \ 'i:imports:1',
        \ 'c:constants',
        \ 'v:variables',
        \ 't:types',
        \ 'n:interfaces',
        \ 'w:fields',
        \ 'e:embedded',
        \ 'm:methods',
        \ 'r:constructor',
        \ 'f:functions'
    \ ],
    \ 'sro' : '.',
    \ 'kind2scope' : {
        \ 't' : 'ctype',
        \ 'n' : 'ntype'
    \ },
    \ 'scope2kind' : {
        \ 'ctype' : 't',
        \ 'ntype' : 'n'
    \ },
    \ 'ctagsbin'  : 'gotags',
    \ 'ctagsargs' : '-sort -silent'
    \ }

추가) vim-go 설치가 완료되고 난 다음에,
$GOPATH/bin 에 생성되어 있는, errcheck, gocode, gofmt, golint, gotags, goimports, gorename, oracle 를 /usr/local/go/bin 에 복사/붙여넣기 한다.

[참고] https://gist.github.com/sabzil/47d81a29b620a0d64f80

pkg-config 사용

Go언어를 사용하다보면 C로 만들어진 라이브러리를 사용하기 위해서 cgo가 필요할 때 가 있다. 라이브러리를 링크하고 하는 몇 가지 절차를 거치게 되는데, 이때 pkg-config라는걸 사용하면 편리하다.

pkg-config를 사용하면 컴파일하는데 필요한 라이브러리와 헤더정보를 Go코드상에서 동일한 경로로 제공해줄 수 있게 된다.

pkg-config가 설치되어 있지 않을 경우에는 다음과 같이 설치를 해준다.

$ sudo apt-get install pkg-config

이제 설치되어 있는 라이브러리 목록을 한번 보자.

$ pkg-config --list-all

현재 설치된 라이브러리들의 정보가 나오게 된다.

그런데 내가 원하는 라이브러리의 정보가 나타나지 않는다.

라이브러리 정보의 관리는 /usr/lib/pkgconfig 경로에 있는 .pc 파일들을 통해서 되어진다. 직접 소스를 컴파일해서 사용하는 라이이브러리의 경우 /usr/local/lib/pkgconfig 에 있는 경우도 있다. 또는 Qt와 같은 경우에는 /home/{user}/Qt5.4.15.4/gcc_64/lib/pkgconfig 의 경로에 pc파일이 존재한다.

이렇게 기본경로가 아닌곳에 존재하는 pc파일들을 관리하기 위해서 PKG_CONFIG_PATH라는 환경변수를 사용할 수 있다.

$ export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/home/{user}/Qt5.4.1/5.4/gcc_64/lib/pkgconfig”

와 같이 해준다. 구분은 “:” 로 해준다.

[참고] http://ko.wikipedia.org/wiki/Pkg-config
[참고] http://tjcplpllog.blogspot.kr/2014/08/pkg-config.html
[참고] http://blog.daum.net/heyjun/15705389
[참고] http://mwmw7.tistory.com/160
[참고] http://ubuntuforums.org/showthread.php?t=751456
[참고] http://manual-archive.blogspot.kr/2011/08/pkg-config-pc-pkgconfigpath.html
[참고] http://ubuntuforums.org/showthread.php?t=1091717
[참고] http://blog.naver.com/supsup5642/60161284657

Golang channels tutorial

최근에 goroutine과 channel에 대해서 학습하다가 설명이 잘 되어 있는 글을 찾아서 번역(이라고 하기에는 뭐하고 정리?) 를 해봤다. 제대로 이해를 못하고 잘 못 번역을 한 부분이 있을 수 도 있으니…꼭 원본을 보길 권장한다.;;;

원본 : http://guzalexander.com/2013/12/06/golang-channels-tutorial.html


Go언어는 병렬 프로그램 작성을 위해서 내장 도구를 갖고 있다. go를 함수 호출 앞에 놓으면 동일한 주소에 위치하는 코드를 독립적인 병렬 스레드로 실행을 시작한다. 이런 쓰레드를 Go언어에서는 goroutine이라고 부른다. 여기에서 말하는 concurrently는 parallel을 의미하지는 않는다. (역주: 그렇지만 번역은 “병렬” 이라고 칭하겠다.;;;) Goroutines은 하드웨어에서 병렬(parallel) 실행이 가능할때 병렬(concurrent)아키텍처를 생성하는 것을 의미한다. Concurrency is not parallelism 에 대한 토크가 있다.
goroutine 예제를 실행해 보자:

func main() {
     // Start a goroutine and execute println concurrently
     go println("goroutine message")
     println("main function message")
}

이 프로그램은 “main function message” 와 아마도 “goroutine message” 를 찍을 것이다. 아마도 라고한건 goroutine 몇 가지 특징이 나타날 수 있기 때문이다 코드를 호출하는 goroutine이 시작될때, main함수는 goroutine이 끝나기를 기다리지 않고, 계속해서 실행이 된다. main함수는 println을 호출하고 나서 실행을 종료한다 그리고 Go언어에서 그건 생성된 모든 goroutine들과 전체 프로그램의 실행 중지를 말한다. 그렇지만 그러한 일이 발생하기전에 우리의 goroutine은 “goroutine message” 문자를 인쇄하고 코드 실행을 완료 할 수 있을거다.

알다시피 이런 상황을 피할 수 있는 어떤 방법이 있어야만 한다. 그래서 Go언어에는 channel 이란게 있다.

Channels basics

Channel은 동시에 동작하는 함수들의 실행을 동기화하고 특정한 값을 넘겨서 그들이 대화할 수 있는 방법을 제공한다. Channel은 몇 가지의 특징이 있다: channel을 통해서 보낼 수 있는 타입의 요소는, 용량(capacity:버퍼의 사이즈) 그리고 “<-” 연산자로 지정된 커뮤니케이션의 방향이 있다. make 함수를 사용해서 channel을 할당할 수 있다:

i := make(chan int)       // by default the capacity is 0
s := make(chan string, 3) // non-zero capacity

r := make(<-chan bool)          // can only read from
w := make(chan<- []os.FileInfo) // can only write to

Channel은 first-class values(역주: first-class object의 의미로 생각했음) 이고 다른 변수들처럼 어디에서나 사용될 수 있다: 구조체의 요소, 함수의 인자로, 함수에서 반환되는 값으로 그리고 다른 channel을 위한 타입으로도 사용될 수 있다:

// a channel which:
//  - you can only write to
//  - holds another channel as its value
c := make(chan<- chan bool)

// function accepts a channel as a parameter
func readFromChannel(input <-chan string) {}

// function returns a channel
func getChannel() chan bool {
     b := make(chan bool)
     return b
}

channel을 쓰고 읽기 위해서 “<-” 연산자가 있다. channel 변수에 대해서 상대적인 연산자의 위치에 따라서 읽기 또는 쓰기 동작이 정해진다. 다음 예제에서 그 사용법을 보여주고 있지만, 이 코드는 나중에 설명할 몇 가지 원인 때문에 동작하지 않는다.

func main() {
     c := make(chan int)
     c <- 42    // write to a channel
     val := <-c // read from a channel
     println(val)
}

이제 channel이 무엇인지, 어떻게 만들고 기본 동작의 수행을 어떻게 하는지 알았으니, 첫번째 예제로 돌아가서 channel이 어떻게 도움을 주는지 알아보자.

func main() {
     // Create a channel to synchronize goroutines
     done := make(chan bool)

     // Execute println in goroutine
     go func() {
          println("goroutine message")

          // Tell the main function everything is done.
          // This channel is visible inside this goroutine because
          // it is executed in the same address space.
          done <- true
     }()

     println("main function message")
     <-done // Wait for the goroutine to finish
}

이 프로그램은 두 메세지를 인쇄 할 것이다. 왜? “done” channel은 버퍼 없기 때문이다. unbuffered channel에서 모든 동작은 수신자와 송신자 모두가 통신 준비가 되기 전까지 실행이 멈춰있게 된다. 그게 unbuffered channel을 동기식이라고 부르는 이유이다. “<-done” 으로 읽는 작업을 하는 main 함수는 goroutine에서 channel에 data를 쓰기 전까지 동작이 블락될 것이다. 그래서 읽는 작업이 성공한 후에만 프로그램이 끝난다.

다음의 경우 버퍼가 완전히 차있지 않으면 쓰는 동작을 할 수 있고, 버퍼가 비어 있지 않으면 블럭없이 성공적으로 모두 읽을 수 있는 버퍼를 갖고 있다. 이러한 channel을 비동기식이라고 부른다. 동기와 비동기의 차이를 보여주는 예는 다음과 같다:

func main() {
     message := make(chan string) // no buffer
     count := 3

     go func() {
          for i := 1; i <= count; i++ {
               fmt.Println("send message")
               message<- fmt.Sprintf("message %d", i)
          }
     }()

     time.Sleep(time.Second * 3)

     for i := 1; i <= count; i++ {
          fmt.Println(<-message)
     }
}

이 예에서 “message” 는 동기식 channel이고 그 결과이다:

send message
// wait for 3 seconds
message 1
send message
send message
message 2
message 3

goroutine에서 channel에 첫번째 쓰는 동작을 하고 난 후에 channel에 모든 쓰기 동작은 첫번째 읽기 동작이 수행되기전 (대략 3초 후)까지 블락되었다.

이제 “message” channel에서 버퍼를 제공해보자. 즉 새로 작성한 라인은 “message := make(chan string, 2)” 처럼 보일것이다. 이번에는 결과과 다음과 같이 보일거다:

send message
send message
send message
// wait for 3 seconds
message 1
message 2
message 3

이제 3개의 message를 저장할 수 있도록 한 channel의 버퍼에서 첫 번째 읽기를 위한 대기 없이 모든 쓰기 동작을 할 수 있는 걸 볼 수 있다. channel의 capacity를 변경해서 시스템의 처리량을 제한하고 정보량을 제어할 수 있게 되었다.

Deadlock

이제 실행할 수 없었던 읽고/쓰기 동작 예제로 돌아가 보자.

func main() {
     c := make(chan int)
     c <- 42    // write to a channel
     val := <-c // read from a channel
     println(val)
}

실행해 보면 이런 에러들을 볼 수 있다.(상세한건 다를 수 있다.)

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
     /fullpathtofile/channelsio.go:5 +0x54
exit status 2

이 에러를 deadlock이라고 부른다. 이 상황은 두 goroutine이 서로를 기다리면서 둘 다 실행을 할 수 없는 것이다. Go언어는 런타임에서 deadlock을 찾을 수 있기때문에 이런 에러를 볼 수 있다. 커뮤니케이션 동작이 블락되었기 때문에 에러가 발생한 것이다.

이 코드는 싱글쓰레드에서 한줄 씩, 차례대로 실행된다. 프로그램 실행이 channel에 쓰기 동작( c <- 42 ) 에서 블락되어 있는건, 우리가 알고 있는 것 처럼, 동기 channel에서 쓰기 동작은 그걸 받는 쪽에서 데이터를 받을 준비가 되어야만 성공할 수 있기 때문이다. 그래서 다음 라인에 리시버를 만들어 주기만 하면 된다.

이 코드를 동작하게 하기 위해 다음과 같이 작성하면 된다:

func main() {
     c := make(chan int)
     
     // Make the writing operation be performed in
     // another goroutine.
     go func() { 
        c <- 42 
     }()
     val := <-c
     println(val)
}

Range channels and closing

앞의 예제들중에 하나에서 channel에 여러 메세지를 보내고 그걸 읽었다. 받는 부분의 코드이다:

for i := 1; i <= count; i++ {
     fmt.Println(<-message)
}

읽는 동작에서 deadlock이 없게 하기 위해서, 보낸 메세지의 수를 정확하게 알고 있어야 한다. 더 많이 보내게 되면 읽을 수가 없다.이건 아주 불편하다. 이걸 좀 더 일반적인 코드로 작성하면 좋을것 같다.

Go언어에는 array, string, slice, map 그리고 channel의 반복자를 사용할 수 있는 range expression 이 있다. channel을 close 하기전까지 반복처리를 한다. 다음 예제를 보자(지금 동작하지 않는다.):

func main() {
     message := make(chan string)
     count := 3

     go func() {
          for i := 1; i <= count; i++ {
               message <- fmt.Sprintf("message %d", i)
          }
     }()

     for msg := range message {
          fmt.Println(msg)
     }
}

이 코드는 동작하지 않는다. 위에서 말한대로 range는 channel이 close되기 전까지 작업을 계속하게 된다. close 함수로 channel을 닫아줘야만 한다. goroutine 부분은 이처럼 보여질거다:

go func() {
     for i := 1; i <= count; i++ {
          message <- fmt.Sprintf("message %d", i)
     }
     close(message)
}()

channel을 close 하게 되면 한가지 더 유용한 기능이 있다. - 닫힌 channel 에 읽기 동작을 하면 블락이 되지 않고 channel 타입의 기본값을 반환한다.

done := make(chan bool)
close(done)

// Will not block and will print false twice 
// because it’s the default value for bool type
println(<-done)
println(<-done)

이 방법은 goroutine의 동기화에 사용할 수도 있다. 예제중에 하나를 동기화 해보자 :

func main() {
     done := make(chan bool)

     go func() {
          println("goroutine message")

          // We are only interested in the fact of sending itself, 
          // but not in data being sent.
          done <- true
     }()

     println("main function message")
     <-done 
}

여기에서 “done” channel은 오직 실행을 동기화 하는데만 쓰이고 데이터를 전송하지는 못한다. 이럴때 사용하는 패턴이 있다:

func main() {
     // Data is irrelevant
     done := make(chan struct{})

     go func() {
          println("goroutine message")

          // Just send a signal "I'm done"
          close(done)
     }()

     println("main function message")
     <-done
}

goroutine에서 close한 channel에 읽기 동작을 하면 블럭되지도 않고 main 함수도 계속 실행이 된다.

Multiple channels and select

실제 프로그램에서는 더 많은 goroutine과 channel이 필요할 거다. 독립적인 부분이 많아 지면 더 효과적인 동기화가 필요할거다. 복잡한 예제를 보자:

func getMessagesChannel(msg string, delay time.Duration) <-chan string {
     c := make(chan string)
     go func() {
          for i := 1; i <= 3; i++ {
               c <- fmt.Sprintf("%s %d", msg, i)
               // Wait before sending next message
               time.Sleep(time.Millisecond * delay)
          }
     }()
     return c
}

func main() {
     c1 := getMessagesChannel("first", 300)
     c2 := getMessagesChannel("second", 150)
     c3 := getMessagesChannel("third", 10)

     for i := 1; i <= 3; i++ {
          println(<-c1)
          println(<-c2)
          println(<-c3)
     }
}

우리는 channel을 만들고 일정한 간격으로 3개의 메세지로 channel을 채우는 goroutine을 생성하는 함수를 갖고 있다. 3번째 channel c3는 제일 짧은 간격이라서 다른것들보다 먼저 메세지가 보여지게 될 것 같다. (역주: except 가 아니라 expect 인것 같다.) 그렇지만 결과는 다음과 같다:

first 1
second 1
third 1
first 2
second 2
third 2
first 3
second 3
third 3

분명히 연속해서 출력되었다.300밀리초단위로 루프 반복과 다른 작업을 하는 첫번쨰 channel에 대한 읽기 동작은 기다려야하기 때문이다. 우리는 실제로 모든 channel에서 어떤것이든 바로 메세지를 읽기를 원한다.

여러 channel에서 커뮤니케이션 작업을 위해서 Go언어에는 “select” 가 있다. “switch”와 비슷하지만, 모든 case에는 커뮤니케이션 동작(read, write)만 있다. “case”의 동작이 수행되면 그에 맞는 코드 블록이 실행된다. 그래서 우리가 원하는걸 하도록 작성하면된다:

for i := 1; i <= 9; i++ {
     select {
     case msg := <-c1:
          println(msg)
     case msg := <-c2:
          println(msg)
     case msg := <-c3:
          println(msg)
     }
}

숫자 9는 channel에서 3번 쓰는 동작을 한다. 그래서 9번 루프를 돌면서 select를 했다. 프로그램에서 무한 루프로 “select”를 실행하는건 보통 데몬으로서 실행되는걸 의미한다. 그렇지만 여기에서는 한번 실행이 되고나면 deadlock에 걸리게 될 것이다.

이제 예상한 결과를 얻었다. 그리고 읽기 작업중에 다른 작업을 차단하지 않는다. 결과이다:

first 1
second 1
third 1 // this channel does not wait for others
third 2
third 3
second 2
first 2
second 3
first 3

Conclusion

Channel은 Go언어에서 아주 강력하고 흥미로운 구조이다. 그렇지만 그걸 효과적으로 사용하기 위해서는 어떻게 동작하는지 꼭 이해를 해야한다. 여기에서는 꼭 필요한 기본들에 대해서만 설명을 했다. 더 학습하기 위해서 다음들을 보면 좋을 것 같다.

md5 패키지 사용예

Qt로 만드는 클라이언트에서 이미지 파일에 대해서 만들어진 md5 hash를 검증 해보기 위해서, 같은 이미지 파일에 대해서 Go언어로 md5 hash를 만들었을때 동일한 값이 생성되면 제대로 만들어진거 아닐까 해서, 만들어 봄.
(사실은 일하기 싫어서 괜히 한번 해 본…)

package main

import (
  "io/ioutil"
  "crypto/md5"
  "fmt"
)
 
func main() {
  data, err := ioutil.ReadFile("1.jpg")
  if err != nil {
    return;
  }
 
  hash := md5.New() 
  hash.Write(data)
  hashData := hash.Sum(nil)
 
  fmt.Printf("%x", hashData)
}

C언어의 배열을 Go언어의 slices로 변환

cgo를 이용하다보면, C언어로 작성된 라이브러리의 리턴값으로 포인터에 구조체가 할당되어서 넘어올때가 있다.
C언어에서 넘어오는 구조체 배열이 포인터 형태로 넘어오니, Go의 포인터에 할당을 해서 사용 해봤다.
당연히 제대로 동작하지 않는다. C언어로 작성된 라이브러리에서 넘어오는 값은 배열이니, Go언어에서는 slices에 할당을 하는게 맞는것 같다.

다음의 내용을 보면, array를 어떻게 slices에 할당을 해야 할지에 대해서 잘 설명이 되어 있다.
“Turning C arrays into Go slcies” ( https://code.google.com/p/go-wiki/wiki/cgo )

아래는 cubrid driver를 만들면서 사용한 코드이다.

slcieHeader := (*reflect.SliceHeader)(unsafe.Pointer(&go_col_info)))
sliceHeader.Cap = int(col_count)
sliceHeader.Len = int(col_count)
sliceHeader.Data = uintptr(unsafe.Pointer(c_col_info))

type으로 이름 붙여진 구조체의 필드를 사용하기위한 방법

c로 만들어져 있던 라이브러리를 cgo를 이용해서 포팅(?)을 하는 작업을 하다보면 난감한 부분들이 종종 나타난다.

그중에 한가지가 c에서 사용하던 구조체의 파라미터명이 go 에서 미리정의된 명칭일 경우가 있다.
대표적인 예로 “type”이라는 파라미터명이다.
c에서는 T_CCI_COL_INFO(cubrid의 cci에서 사용되는 구조체중에 하나) 의 필드중에 하나가 type이라는 필드가 하나 있다.
이걸 go에서 사용을 하려고 하니,,,
”…expected selector or type assertion, found ‘type’ “ 이라는 결과를 만나게 된다.

이걸 도대체 어떻게 사용해야 하나 고민을 하던중, golang.org 에 있는 문서 http://golang.org/cmd/cgo/ 에서 해결을 할 수 있었다.

Within the Go file, C identifiers or field names that are keywords in Go can be accessed by prefixing them with an underscore: if x points at a C struct with a field named “type”, x._type accesses the field.

패키지 만들때 testing 패키지 사용하기

개요

Go 언어용 패키지를 만들려면, 테스트코드를 해야 개발이 진행이 좀 더 원활하게 되는 것 같다.
그래서 간단하게 테스트 코드 작성하는 방법을 정리해봤다.

abc 패키지를 만들어서 테스트하기

src\abc\abc.go
src\abc\abc_test.go 와 같이 파일을 만들어 놓는다.

abc.go를 다음과 같이 작성한다.

// abc.go

package abc

import (
    "fmt"
)

func A_Method() {
    fmt.Println("test A Method")
}

그리고 abc_test.go를 다음과 같이 작성한다.

// abc_test.go

package abc

import (
    "testing"
)

func TestA_Method(t *testing.T) {
    A_Method()
}

그리고 $GOPATH/src/abc/ 에서

$ go test

를 하면 테스트가 진행된다.

about

Jaehoon Kim
jaehoon@shrinklabs.com