아이에게 프로그래밍 가르치기

예전부터 아이들에게 프로그래밍을 가르치는 일에 흥미가 있었다. 특히 올해는 우리집 멍멍이가 초등학교에 입학할 나이가 되었기 때문에 개발자 아빠로서 뭐라도 알려주어야겠다는 생각이 든다. 이참에 아이에게 프로그래밍을 가르칠 때 무엇을 염두에 두어야 하는지 글로 정리해 보려고 한다.

 

코딩 교육 의무화

2019년도부터 초등학생에게 코딩교육을 의무화하였다 한다. 사실 코딩 교육 의무화는 2015년부터 계획되어 있던 것이라 놀랄 일은 아니다. 다만 코딩이 공교육과 입시의 영역으로 들어오게 되었을 때 생길지 모를 부작용 때문에 각계에서 우려가 많은 것으로 안다.

본인은 현업 교사도 아니고 입시 전문가도 아니기 때문에 현 코딩 교육 실태에 대한 평가가 맞지 않을 수도 있음을 감안하고 들어달라. 코딩 교육은 2019년부터 의무화가 되었지만 현장 교육은 학교마다 차이가 있다. 어떤 곳은 저학년부터, 어떤 곳은 5/6학년부터 코딩 교육을 실시한다. 왜 이렇게 차이가 나는가 하면 아직 공교육의 영역에 들어올 만큼 커리큘럼이 자리를 잡지 못한 상태에서 의무화를 서둘렀기 때문이라고 생각한다. 이런 경우 예상할 수 있는건 코딩 교육의 난이도가 많이 낮을 것이라는거다. 실제로 교재들을 찾아보면 부교재의 품질(로봇을 움직인다던지, 악기를 연주한다던지)에 차이가 있을 뿐 코딩의 난이도가 높은 과정은 찾아볼 수 없었다. 허면, 앞으로도 그럴 것인가 하면 그건 알 수 없다. 향후 코딩 교육 난이도의 향방은 입시 포함 여부가 결정할 듯 하다. 입시에 코딩이 포함되려면 변별력을 두어야 할텐데, 사실 코딩문제를 어렵게 내려면 그 난이도는 끝도 없이 높일 수도 있다.

개인적인 의견을 말하자면, 본인은 그다지 주입식 교육을 좋아하지는 않는다. 코딩 교육도 주입식 교육과 그다지 어울리지는 않는 영역으로 보인다. 허나 만약 코딩을 현 입시제도 하에서 주입식으로 가르친다 가정한다면 다른 어떤 과목보다도 실용적이기는 할 것이라 본다. 12년 입시 공교육 체제에서 배운 지식 중 실용적인 학문이 얼마나 될까? 기껏해야 영어 / 제2외국어 / 한문 정도? 본인은 수학도 좋아했고 과학도 좋아했고 철학 / 문학 / 역사도 즐겁게 배웠다. 공교육 체제 아래 배운 이러한 지식들이 본인의 관점과 지평을 넓혀주었다는 점을 부인할 수는 없으나, 그 지식들은 실용성과는 정말 거리가 멀었다. 심지어 변별력을 위해 무의미하게 난이도를 엄청나게 올린 입시 스킬은 정말 쓰잘데기 없는 죽은 지식들이다.  80평생 동안 ‘반데르발스 힘’ 같은걸 써먹을 일이 단 한 번이라도 생길까?

반면 코딩은 조금 상황이 다르다. 예를 들어 대입 시험에 임의의 숫자 배열을 merge sort하는 방법과 그때의 시간복잡도를 증명하는 문제가 출제된다면 이것도 죽은 지식일까? 물론 실용성 측면이라면 좀 전에 예로 들은 ‘반데르발스 힘’과 큰 차이가 없을 지도 모른다. 허나 merge sort는 컴퓨터나 핸드폰을 사용하는 사람이라면 알던 모르던 매 순간 마주하는 알고리즘일만큼 우리 일상에 가까운 것이다. 심지어 입시 공부로 merge sort를 배운 학생이 어느 날 갑자기 영감이 떠올라 개선된 탐색 알고리즘을 제안할 수 있을지도 모른다. 왜냐하면 생각은 누구나 할 수 있고, 그 생각의 결과물을 제안하는 방법도 누구에게나 열려 있기 때문이다.

기존의 입시과목의 실용성이 0이라면 단언컨대 코딩은 살면서 반드시 쓰일 일이 생긴다. 이 글을 읽는 독자가 설령 비개발자여도 엑셀 함수나 받은 편지함 분류 따위의 간단한 자동화는 직장에서 필수적이다. 현재에도 이런데 미래에 12년간 코딩 의무교육을 이수한 사람이라면 얼마나 많은 일들을 자동화 해낼 수 있을까.

코딩 교육 의무화는 본인이 다루고자 하는 주제의 본질과는 살짝 거리가 있으므로 ‘코딩 교육은 본질적으로 주입식과는 맞지 않으나, 만약 주입식으로 가르친다면 그나마 다른 학문에 비해 실용적일 것이다.’ 정도로 정리해본다.

 

Computational thinking

입시 코딩은 일단 제껴두자. 입시를 제외한다면 코딩 교육은 무엇을 목적으로 하여야 할까? 필자는 computational thinking 이어야 한다고 생각한다. 다른 말로 하면 ‘문제 해결 능력’인데, 그냥 문제를 해결하는 것이 아니라 문제 해결의 절차를 실행 가능한 작은 단위로 나누어 서술하는 능력을 말한다. 문제해결의 절차는 논리적으로 서술할 수만 있으면 도구는 무엇이던 크게 상관이 없다. 아래 링크한 두 영상은 컴퓨터 없이 코딩 교육을 진행하는 좋은 예제다. 영상에서 작성한 문제 해결의 절차를 컴퓨터가 제공하는 명령어로 치환하면 바로 코딩이 된다.

EBS 소프트웨어야 놀자 – 쓱싹쓱싹 로봇 청소기

스크린샷 2020-02-16 오후 9.10.29

[한글] 샌드위치 만들기

[원본] Exact Instructions Challenge PB&J Classroom Friendly

스크린샷 2020-02-16 오후 9.26.02

코딩학원이나 학교의 교재를 살펴보면 엔트리나 스크래치 류의 블록코딩 언어부터 시작하는 경향이 있다. 이후에 라즈베리파이나 아두이노, 마이크로비트 같은 IoT 하드웨어를 제어하는 방법을 배우고, 파이썬이나 c언어 등의 프로그래밍 언어를 배우는 식으로 커리큘럼이 만들어져 있다.

블록코딩이나 IoT 기기들로 코딩을 시작하는 것이 나쁘다는건 아니다. 아이들은 쉽게 접할 수 있으면서 결과가 눈에 보이는 것들에 쉽게 흥미를 보인다. 허나 이런 것들은 본질이 아니므로 아이가 충분히 우수하다면 굳이 흥미 본위의 교육에 시간과 비용을 뺏길 필요는 없다. 가끔 블록코딩으로 엄청나게 복잡한 제어로직을 작성하거나 IoT 교재에 많은 비용을 들이는 경우가 있는데 굳이 그럴 필요가 없다는거다.

 

Structured programming

현대의 거의 대부분의 프로그래밍 언어는 구조적이다. 왜냐하면 프로그래밍 언어가 구조적이지 않을 경우, 그 복잡도가 인간이 제어할 수 없는 수준으로 높아지기 때문이다. 아래 링크한 아티클을 굳이 읽어볼 필요는 없으나 업계에서 전설처럼 회자되는 ‘~considered harmful’ 의 시조이자 비구조적 언어의 한계를 명백히 하여 현대의 프로그램 언어가 구조적으로 자리잡는 분기점이 된 사건이기에 링크해 보았다.

GOTO considered harmful(전설의 레전드의 시작…)

각설하고, 아이들에게는 구조적 프로그래밍의 구성단위만을 알려주면 된다. 컴퓨터에 특별한 소질을 보이면서 진짜 내부 동작원리를 궁금해 하는 아이라면 실제 컴퓨터의 명령어 수행은 비구조적이라는 사실을 알려줄 수도 있겠으나, 보통의 아이들에게 필요한 교육은 1) 순차 2) 분기 3) 반복 4) 함수 의 4가지 구성단위이다. 구조적 프로그래밍의 자세한 내용은 기회가 되면 다뤄보도록 하자.

 

그밖에 주제들

다음 주제들을 아이 가르칠 때 염두에 두어야 할 듯 하다. 허나 저학년이 이해하기에는 내용이 쉽지 않으므로 염두에만 두도록 한다. 혹시라도 고학년이나 우수한 학생들을 가르칠 기회가 생긴다면 아래 주제들을 다뤄보도록 하자.

  • 기계의 한계
  • 알고리즘
  • NAND to Tetris
  • No silver bullet – programming paradigm

OSX 의 exception handling 순서

오랜만에 올리는 블로그 글의 주제는 OSX 의 exception handling 순서이다.

OSX kernel 구조

애플 개발자 문서 링크 – Kernel Architecture Overview

Signal handling 순서를 설명하기 전에 알아둘 사전 지식, OSX 의 커널 구조를 알아보자.

C436D54C-9BDD-4FC0-B225-DAFF54156938

OSX 커널은 BSD와 Mach 의 디자인을 둘 다 따르고 있다.

맥 os 의 발전사를 깊이 알지 못하는 필자로선 이런 디자인이 캘베로스나 히드라의 ‘한 몸 – 두 머리’처럼 꽤나 기괴한 설계로 보인다. 운전석과 조수석에 각각 핸들이 달려있는 자동차를 상상해보자. 서로 핸들을 반대 방향으로 꺾을 경우, 이 자동차는 어디로 가야하는가?

다행히(?) OSX 에서 BSD와 Mach 사이에 이런 문제는 발생하지 않는다. 두 커널은 담당하는 레이어가 분리되어 있다. 아래 발췌해 온 문서를 참고해 보자.

Mach

Mach manages processor resources such as CPU usage and memory, handles scheduling, provides memory protection, and provides a messaging-centered infrastructure to the rest of the operating-system layers. The Mach component provides

  • untyped interprocess communication (IPC)
  • remote procedure calls (RPC)
  • scheduler support for symmetric multiprocessing (SMP)
  • support for real-time services
  • virtual memory support
  • support for pagers
  • modular architecture

General information about Mach may be found in the chapter Mach Overview. Information about scheduling can be found in the chapter Mach Scheduling and Thread Interfaces. Information about the VM system can be found in Memory and Virtual Memory.

BSD

Above the Mach layer, the BSD layer provides “OS personality” APIs and services. The BSD layer is based on the BSD kernel, primarily FreeBSD. The BSD component provides

  • file systems
  • networking (except for the hardware device level)
  • UNIX security model
  • syscall support
  • the BSD process model, including process IDs and signals
  • FreeBSD kernel APIs
  • many of the POSIX APIs
  • kernel support for pthreads (POSIX threads)

The BSD component is described in more detail in the chapter BSD Overview.

주저리 주저리 설명되어 있지만 거칠게 요약하면 Mach 커널은 리소스에 대한 보다 추상화된 영역을 관리하는 반면, POSIX 커널은 상대적으로 하드웨어에 밀접한 영역을 관리한다고 볼 수 있다.

 

Exception?

Exception 이란 ‘프로그램에서 정의하지 않은 동작을 처리하기 위한 추상화’ 이다. 텍스트 뷰어 프로그램을 예로 들면, 파일을 열고 안의 내용을 볼 수 있게 화면에 띄워주는 기능은 프로그램에서 정의한 동작이다. 허나 파일을 열려고 시도했는데 해당 파일이 지워져서 찾을 수 없다면? 더 이상 프로그램은 정의된 동작을 수행할 수가 없다. 이러한 ‘파일을 찾을 수 없는 상태’를 추상화하여 이를 제어하는 메커니즘을 제공하는 것이 Exception handling 이다.

 

Signal?

Signal 이란 OS에서 발생한 이벤트를 프로세스에게 전달하기 위한 software interrupt 이다. Signal은 경찰관이 부는 호루라기 소리와 같다. 운전자가 술을 먹고 중앙선을 침범하였을 때, 운전자를 멈춰세우고 면허를 취소시키기 위해 호루라기를 부는 것이다. 이때 보통의 운전자들은 경찰의 지시에 따라 모든 것을 체념하고 갓길에 차를 댄 후 면허증을 반납한다. 헌데 가끔씩 있다. 경찰의 부름에 제멋대로 GTA 를 찍는 돌+I 들이 말이다. 이와 유사하게, Signal이 발생했을 때 프로세스는 default action(종료, 덤프 생성 등)을 수행한다. 허나 default action을 따르지 않고 Signal handler 를 등록하여 동작을 바꿀 수도 있다.

 

Handling 순서

Exception 과 Signal 모두 하드웨어 리소스에 대한 프로세스의 잘못된 접근에 의해 발생할 수 있다. OSX의 경우, Mach exception handler 와 POSIX signal handler 를 둘 다 제공한다. OSX 프로그램에서 Exception handler 와 Signal handler 를 둘 다 정의한 경우 프로그램은 아래의 순서로 동작한다.

  1. 제일 먼저 Mach exception 이 발생한다.
  2. 발생한 Mach exception handler 가 있는지 찾아본다. 있으면 handler 수행 후 종료.
  3. exception handler 가 없으면 해당하는 POSIX signal 이 발생한다.
  4. 발생한 POSIX signal handler 가 있는지 찾아본다. 있으면 handler 수행 후 종료.
  5. Signal handler 가 없으면 EXC_CRASH exception 이 발생한다.
  6. Apple crash report server 로 관련 crash 를 전송한다.

 

한국에서는 시스템 프로그래밍을 할 기회가 많지 않은데, 하물며 OSX 계열의 시스템 api 라면 더더욱 관련 경험을 얻기가 쉽지 않다. 필자 역시도 일이 아니었더라면 OSX의 커널 아키텍처는 아마도 평생 찾아볼 일이 없었을 것이다. 혹시라도 누군가 OSX 의 하드웨어 exception을 처리하면서 어려움을 겪고 있다면 이 글이 도움이 되길 바란다. 참고로 iOS 도 구조가 유사하다.

도서 리뷰 – 알고리즘 산책: 수학에서 제네릭 프로그래밍까지

알고리즘 산책 수학에서 제네릭 프로그래밍까지에 대한 이미지 검색결과

길벗 출판사 도서 소개

프로그래밍은 추상화(abstraction)의 연속이다. 데이터를 추상화하고 흐름을 추상화하고 모듈과 빌딩블록을 추상화하며 심지어는 서비스와 어플리케이션도 추상화한다.

헌데 추상화라는 단어만큼 추상적인 것도 없다. 누군가는 추상화라는 단어를 듣고 타입 추상화를 떠올릴 것이며 누군가는 자료구조나 객체를 떠올릴 것이다. 컴퓨터 과학에서 누군가가 추상화라는 단어를 사용한다면 이 단어가 다양한 계층에서 서로 다른 의미로 쓰인다는 것을 미리 알고 있어야 이해에 도움이 될 것이다.

알고리즘 산책에서 다루는 추상화는 크게 두가지이다. 수학적 추상화와 제네릭 프로그래밍이다.  이 책은 제네릭 프로그래밍이 어떻게 데이터 타입을 추상화 하는지에 대하여 여러 수학적 사례를 들어 설명한다. 이를테면, 고대 이집트인들의 곱셈하는 방식으로부터 곱셈 알고리즘을 도출한 후 이를 점진적으로 개선한다. 이후 이를 일반화하여 덧셈 연산으로부터 닫혀있는 타입에서 동작하는 템플릿으로 알고리즘을 재구현한다. 이 과정은 매우 흥미롭다. 알고리즘을 개선하는 시도와 이를 어떻게 일반화하는지를 보여주면서 제네릭 프로그래밍의 목적이 무엇인지 독자에게 자연스럽게 알려준다.

꽤 많은 수식과 증명이 나온다. 때문에 수학과 친하지 않다면 쉽게 읽히는 책은 아니다. 허나 저자의 친절한 설명과 함께 코드 구현이 수반하기 때문에 고교 수학 과정을 성실히 이수하였다면 이해하는데 큰 무리는 없을 것이다. 틈틈이 수학과 수학자의 역사를 읽을거리로 제공하는 것도 이 책의 가치를 높인다.

c++처럼 type define이 자유로운 강타입 언어에서 제네릭 프로그래밍이 갖는 의미는 매우 크다. 문제 해결하는 방법을 일반화하고, 이 일반화된 문제해결 방법이 어떤 타입에 제한되는지(또는 어떤 타입에 적용 가능한지) 정의하는 방법이 어떻게 수학의 문제해결 방식과 맞닿아 있는지에 대한 설명이 탁월하다. 누군가가 제네릭 프로그래밍을 주제로 책을 추천해 달라 한다면 알고리즘 산책은 단연 선순위이다.

주개발 언어가 c++ 가 아닌 프로그래머라면 type에 조건을 주는 concepts 부분이 잘 이해가 가지 않을 수도 있겠다. 이번 기회에 이 책을 읽으면서 인식의 지평을 넓히는 계기가 되었으면 한다.

concepts 소개

마지막으로 이런 훌륭한 책을 리뷰할 수 있도록 출간해 주신 길벗 출판사에게 감사드린다.

kubernetes multi-node cluster 구성하기

kubernetes 공식 문서에서는 다양한 kubernetes cluster 구성 전략 을 제공하고 있다.

그 중 docker를 이용한 cluster 구성 방식(portable multi-node)으로 multi-node cluster 를 구성하는 방법을 기술한다.

설치 후 알게 된 사실

portable multi-node 는 kubernetes developer community 에서 업데이트가 뜸하다.  대신 kubeadm 활동이 활발하다.(관련링크) 아직 alpha release 이므로 좀 더 안정화될 때까지 기다렸다가(얼마 안남은 듯) kubeadm 으로 cluster 구성하는 것을 추천한다.

prerequisite
docker 만 있으면 됨
필자는 centos 7 에 설치하였다.

docker 설치
docker 설치는 이 사이트에서 하란대로 하면 쉽게 설치가 가능하다.

환경에 따라 docker daemon 이 올라오지 않는 문제가 있다.
필자의 경우에는 docker0 라는 가상 bridge 가 생성되지 않아서 강제로 docker0 브릿지를 생성하였다.

brctl addbr docker0
ip addr add 192.168.5.1/24 dev docker0
ip link set dev docker0 up
iptables -t nat -L -n

gnome gvfs 에서 io 를 엄청나게 점유해 버리는 문제가 있다.
관련 app / service 를 모조리 uninstall 하고 reboot 하자.

kubernetes docker image 설치

이 글을 쓰는 도중 kubernetes documentation 에서 docker multi-node 구성이 지워졌다.

앞으로는 kubeadm 으로 multi node 구성하면 된다. (링크 : https://kubernetes.io/docs/getting-started-guides/kubeadm/)

내가 이 글을 쓴 이유가 공홈 다큐멘테이션 대로 docker multi node 구성하면 설치가 안되기 때문이었는데 글 쓸 이유가 사라졌다.(ㅠㅠ)

글은 여기서 줄인다. 아마도 kubeadm 으로 설치하면 한방에 잘 설치 될거다.

 

kubernetes 로 flask web app 배포하기

kubernetes?

고대 그리스어로 조타수? 키잡이? 라는 의미라고 한다.
구글에서 개발한 container orchestration tool(vm 또는 container 관리를 자동화해주는 도구)이다.

 

why kubernetes?

사실 이 부분은 충분한 경험이 없어서 자신있게 말을 못하겠다.
온라인에서 이것 저것 찾아보면 몇 가지 후보군이 더 있다.

  • swarm
  • mesos / marathon
  • 기타(kotena, nomad …)

이 중에서 kubernetes 를 선택한 이유는
기본적으로 제공하는 기능과 설계구조가 전반적으로 낫다는 평(카더라)
업계의 선택 – 실무 활용 사례가 많다는 점

때문이다. 다른 오케스트레이션 툴 중에 더 나은 것이 있다면 얼마든지 변경할 수 있다.

 

설치

는 생략… 공식 홈페이지에 tutorial 이 잘 나와 있다.

클러스터링 전략을 어떻게 가져가느냐에 따라 다양한 방법으로 설치가 가능하므로
문서를 잘 읽어보고 본인의 환경에 맞게 설치하길 바란다.

처음 시작하는 사람들은 로컬 개발환경에 docker / minikube / kubectl 설치하면 된다.
https://kubernetes.io/docs/getting-started-guides/minikube/

 

flask app 만들기

아래와 같이 간단하게 동작하는 flask app을 만들자.

중요!
host 설정을 0.0.0.0 으로 하여야 가상 컨테이너 외부에서 별도 proxy 없이 접근 가능하다.

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello!!"

if __name__ == "__main__":
    app.run(host='0.0.0.0')

특별한 설정 없이 실행하는 것 만으로 웹 서비스를 제공한다.

$ python3 hello.py
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [11/Apr/2017 08:40:53] "GET / HTTP/1.1" 200 -

이 flask app 을 docker image 로 만들어 보자.

docker image / container 만들기

다음과 같이 Dockerfile 을 만들자.
기존에 제공된 python image 기반으로 조금 전에 만든 flask app 을 복사/실행하도록 하자.

FROM python:3.6
EXPOSE 5000
COPY hello.py .
RUN pip install flask
CMD ["python",  "./hello.py"]

만든 Dockerfile로부터 docker image 를 만들자.

$ docker build -t hello-flask:v1 .

이 docker image 가 잘 동작하는지 확인해 보자.

docker run -it --rm -p 5000:5000 hello-flask:v1
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
172.17.0.1 - - [10/Apr/2017 23:54:50] "GET / HTTP/1.1" 200 -

 

 

kubernetes 로 docker container(pod 또는 deployment) 만들기

다음 2가지를 주의한다.
kubernetes 는 Dockerfile 로부터 container를 생성하는 기능은 제공하지 않는다. docker image를 미리 만들어 두어야 한다.
kubernetes 하위에서 실행되는 docker 환경은 시스템에 설치된 docker 환경과는 완전히 독립적이다. docker image / container 를 공유하지 않으므로 image를 다시 만들어야 한다.

$ eval $(minikube docker-env) //kubernetes docker 실행환경 전환
$ eval $(minikube docker-env -u) //kubernetes docker 실행환경 원복

kubernetes docker 실행환경으로 전환하여 docker image 를 만든 후, kubectl을 이용하여 다음과 같이 deployment(pod) 를 생성한다.

$ kubectl run hello-flask --image=hello-flask:v1 --port=5000
deployment "hello-flask" created

kubectl 을 이용해 deployment / pod 정보를 보면 아래와 같다.

$ kubectl get deployments
NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hello-flask      1         1         1            1           24s

$ kubectl get pods
NAME                              READY     STATUS    RESTARTS   AGE
hello-flask-2475082718-xw1s3      1/1       Running   0          3m

 

 

pod vs deployment ?

kubernetes 는 container / container group 의 단위로 pod 를 사용한다.
반면 deployment 는 pod 의 상태를 체크하여 pod 생성/재시작/scaling 을 담당한다.
pod 가 Object 라면 deployment 는 Factory 나 Builder 와 같은 관계랄까.

위와 같은 이유로 보통 kubectl 로 container 를 생성/삭제할 때에는 deployment 로 호칭하므로 참고하기 바란다.

 

deployment 로 service 만들기

deployment 만 생성하여서는 아직 서비스를 할 수가 없다.
다음과 같이 kubectl 명령을 사용하여 deployment 를 service 로 실행한다.

kubectl expose deployment hello-flask --type=LoadBalancer --name=hello-flask

kubectl 을 이용해 service 정보를 확인해 보자

$ kubectl get service hello-flask
NAME          CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
hello-flask   10.0.0.245   <pending>     5000:30276/TCP   <invalid>

$ kubectl describe service hello-flask
Name:                   hello-flask
Namespace:              default
Labels:                 run=hello-flask
Selector:               run=hello-flask
Type:                   LoadBalancer
IP:                     10.0.0.245
Port:                   <unset> 5000/TCP
NodePort:               <unset> 30276/TCP
Endpoints:              172.17.0.2:5000
Session Affinity:       None
No events.

다음 minikube 명령을 이용하면 flask app 이 어느 external ip 와 mapping 되어 있는지 브라우저로 확인할 수 있다. 현재 별도로 kubernetes 에게 external ip/port 할당 관련한 설정을 하지 않았으므로 아무렇게나 할 것이다.

$ minikube service hello-flask
Opening kubernetes service default/hello-flask in default browser...

다음과 같이 stdout log 도 확인 가능하다.

$ kubectl logs -f hello-flask-2475082718-xw1s3
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
172.17.0.1 - - [11/Apr/2017 00:36:00] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [11/Apr/2017 00:36:00] "GET /favicon.ico HTTP/1.1" 404 -
172.17.0.1 - - [11/Apr/2017 00:38:15] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [11/Apr/2017 00:40:47] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [11/Apr/2017 00:42:22] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [11/Apr/2017 00:47:26] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [11/Apr/2017 00:47:27] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [11/Apr/2017 00:47:27] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [11/Apr/2017 00:47:27] "GET / HTTP/1.1" 200 -

중요!
container 기반 app 을 작성할 때에는 log를 반드시 stdout 으로 남기도록 한다.
그래야 log 를 container 와 별도로 수집 가능하다.

코딩소림사 스터디 – 알고리즘 문제해결전략 #1

금번 스터디는 구종만 님의 매우 명저 “알고리즘 문제해결 전략” 으로 8주간 진행합니다.

개인적으로는 분량도 짧고(!) 내용도 알찬 한주영 님의 “개미수열을 푸는 10가지 방법”으로 진행하길 원했으나 다수결에 밀려(ㅜㅜ) “알고리즘 문제해결 전략” 으로 선정되었습니다.

사실 “알고리즘 문제해결전략”은 매우 명저이고 저도 참 좋아하는 책입니다. 저자분이 그 유명한 알고스팟 운영자시기도 하구요. 많은 분들께서 아주 의욕적으로 알고리즘 주제를 깊이 있게 공부하길 원한다는 의미인 듯 하여 매우 기쁩니다.

아래는 스터디 ppt 입니다.

[rxJava] Flowable 과 Observable 의 차이

rxjava 가 메이저 버전 업(1->2)을 하면서 몇 가지 변경점이 생겼다.

변경점에 대한 자세한 내용은 아래 링크를 참조하기 바란다.

Flowable 이라는 base reactive class 가 추가 되었다. Observable 과의 차이는 backpressure buffer의 기본 탑재 유무이다.

backpressure?

우리말로 번역하면 ‘등 뒤에서 떠밀리는 압박’ 정도가 될 듯 하다.

이런 상황을 가정해보자. 콘서트장을 사람들이 가득 메웠다. 콘서트장에 들어오려는 사람들은 저글링 개떼처럼 밀려드는데 나가는 사람은 별로 없다. 콘서트장 출입구를 통제하는 요원이 없다면? 콘서트장이 터지던지 안에 있던 사람들이 짜부러지던지 아무튼 대형 사고가 발생할거다.

publish / subscribe 모델에서도 이런 비극적인 시나리오가 발생할 수 있다. 생산자는 미친듯이 element 를 생산해 내는데 소비자가 처리하는 속도가 이를 따라가지 못한다면

  1. busy waiting 또는
  2. out of memory exception 이 발생할 것이다.

‘등 뒤에서 떠밀리는 압박’ 에 대한 흐름제어를 위한 버퍼가 바로 backpressure buffer 다. 버퍼가 가득 차면 어차피 소비자는 element 를 처리할 여유가 없는 상태이므로 더 이상 publish 를 하지 않는다.

기존에 없던 개념이 새로 추가된 것은 아니다. 기존 rxJava 1.xx 의 경우 Observable 에 backpressure buffer 를 직접 생성해 주면 사용이 가능하다. 허나 rxJava 개발자는 초보자들이 미처 알아채지 못하는 영역에서 기대하지 않는 동작이 일어날 가능성이 있다며 Flowable 을 추가하였다.

다음 예제코드를 보자. 생산자의 생산 속도를 소비자가 따라가지 못하는 시나리오다.
Flowable 을 사용하면 default buffer size(128) 이상 backpressure buffer 에 element 가 쌓일 경우 흐름제어를 한다.

public class example01 {

    public static void main(String... args) throws InterruptedException {

        final String tmpStr = Arrays.stream(new String[10_000_000]).map(x->"*").collect(Collectors.joining());
        Flowable foo = Flowable.range(0, 1000_000_000)
                .map(x-> {
                    System.out.println("[very fast sender] i'm fast. very fast.");
                    System.out.println(String.format("sending id: %s %d%50.50s", Thread.currentThread().getName(), x, tmpStr));
                    return x+tmpStr;
                });

        foo.observeOn(Schedulers.computation()).subscribe(x->{
            Thread.sleep(1000);
            System.out.println("[very busy receiver] i'm busy. very busy.");
            System.out.println(String.format("receiving id: %s %50.50s", Thread.currentThread().getName(), x));
        });

        while (true) {
            Thread.sleep(1000);
        }
    }
}
[very fast sender] i'm fast. very fast.
sending id: main 0**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 1**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 2**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 3**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 4**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 5**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 6**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 7**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 8**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 9**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 10**************************************************

... 중략 ...

[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 0*************************************************
receiving id: RxComputationThreadPool-1 1*************************************************
receiving id: RxComputationThreadPool-1 2*************************************************
[very fast sender] i'm fast. very fast.
sending id: main 117**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 118**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 119**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 120**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 121**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 122**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 123**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 124**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 125**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 126**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 127**************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 3*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 4*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 5*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 6*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 7*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 8*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 9*************************************************

반면, 같은 시나리오를 Observable 을 backpressure buffer 생성 없이 사용하면 OutOfMemoryException 이 발생한다.

public class example02 {

    public static void main(String... args) throws InterruptedException {

        final String tmpStr = Arrays.stream(new String[10_000_000]).map(x->"*").collect(Collectors.joining());
        Observable foo = Observable.range(0, 1000_000_000)
                .map(x-> {
                    System.out.println("[very fast sender] i'm fast. very fast.");
                    System.out.println(String.format("sending id: %s %d%50.50s", Thread.currentThread().getName(), x, tmpStr));
                    return x+tmpStr;
                });

        foo.observeOn(Schedulers.computation()).subscribe(x->{
            Thread.sleep(1000);
            System.out.println("[very busy receiver] i'm busy. very busy.");
            System.out.println(String.format("receiving id: %s %50.50s", Thread.currentThread().getName(), x));
        });

        while (true) {
            Thread.sleep(1000);
        }
    }
}
[very fast sender] i'm fast. very fast.
sending id: main 0**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 1**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 2**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 3**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 4**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 5**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 6**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 7**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 8**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 9**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 10**************************************************
[very fast sender] i'm fast. very fast.

...중략...

sending id: main 198**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 199**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 200**************************************************
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.<init>(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at example02.lambda$main$1(example02.java:24)
	at example02$$Lambda$6/123961122.apply(Unknown Source)
	at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59)
	at io.reactivex.internal.operators.observable.ObservableRange$RangeDisposable.run(ObservableRange.java:64)
	at io.reactivex.internal.operators.observable.ObservableRange.subscribeActual(ObservableRange.java:35)
	at io.reactivex.Observable.subscribe(Observable.java:10700)
	at io.reactivex.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
	at io.reactivex.Observable.subscribe(Observable.java:10700)
	at io.reactivex.internal.operators.observable.ObservableObserveOn.subscribeActual(ObservableObserveOn.java:45)
	at io.reactivex.Observable.subscribe(Observable.java:10700)
	at io.reactivex.Observable.subscribe(Observable.java:10686)
	at io.reactivex.Observable.subscribe(Observable.java:10589)
	at example02.main(example02.java:27)
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 5*************************************************

참고로, Flowable 은 FlowableCreate 라는 builder 에서 생성되며, 특별한 설정이 없을 경우 buffer size 는 최소 16, 기본 128 로 설정한다.

//FlowableCreate.java line:44

    @Override
    public void subscribeActual(Subscriber<? super T> t) {
        BaseEmitter<T> emitter;

        switch (backpressure) {
        case MISSING: {
            emitter = new MissingEmitter<T>(t);
            break;
        }
        case ERROR: {
            emitter = new ErrorAsyncEmitter<T>(t);
            break;
        }
        case DROP: {
            emitter = new DropAsyncEmitter<T>(t);
            break;
        }
        case LATEST: {
            emitter = new LatestAsyncEmitter<T>(t);
            break;
        }
        default: {
            emitter = new BufferAsyncEmitter<T>(t, bufferSize());
            break;
        }
        }

        t.onSubscribe(emitter);
        try {
            source.subscribe(emitter);
        } catch (Throwable ex) {
            Exceptions.throwIfFatal(ex);
            emitter.onError(ex);
        }
    }
//Flowable.java line:61
    static final int BUFFER_SIZE;
    static {
        BUFFER_SIZE = Math.max(16, Integer.getInteger("rx2.buffer-size", 128));
    }