전작주의(全作主義) 독서법

강원국 교수님의 마음을 움직이는 글쓰기 강연에서 전작주의(全作主義) 독서법에 대해 들었다. 한 작가의 작품들을 모두 읽어보는 방법이다. 작가의 문체를 여러 번 접하면서 문형을 익힐 수 있는 좋은 독서법이라고 소개해주셨다. 나는 수집욕이 강한 편이라 마음에 드는 작가-뿐만 아니라, 뮤지션, 배우, 감독-의 모든 작품을 섭렵하기를 즐긴다. 문학적 취향이 생겼을 무렵부터 지금껏 암묵적으로 전작주의 독서를 해왔다고 할 수 있다.

99129170_7d542023a6_z

내가 좋아하는 작가들을 꼽는다면 무라카미 하루키, 정유정, 오지은, 박민규, 김애란, 김미월, 김금희 같은 작가들을 이야기할 수 있다. 그들이 쓴 작품 대부분을 읽었다. 특히 -다른 작가에 비해 작품이 많은 이유로- 하루키의 글을 많이 읽었는데, 몇 년 전 프롤로그만 쓰고 그만둔 습작을 읽어 본 친구는 파스타와 재즈만 등장하면 하루키의 소설일 것이라 평하기도 했다. 전작주의 독서법을 통해 나도 모르게 하루키의 문체를 습득한 것이다.

창조는 모방에서부터 시작된다. 내가 새롭게 만들어 낼 수 있는 것은 이미 거의 없다. 악기를 배울 때는 고전들을 먼저 익힌다. 고전에 있는 패턴을 익히기 위함이다. 프로그래밍도 마찬가지다. 오래된 알고리즘과 패턴들을 먼저 익힌다. 글쓰기 역시 그런 훈련을 통해 좋은 패턴들을 익히면 자연스레 좋은 문장을 만들어 낼 수 있을 것이다.

pens

그래서 또 한 작가를 파 보기로 했다. 유시민 작가다. 그의 문체가 간결하고 논리적인 글을 쓰고 싶은 내 욕구를 채워줄 수 있을 것이라 기대한다.

쉘 스크립트를 만드는 당신, ShellCheck을 써라!

정적 분석 (Static Analysis)

프로그램이 복잡해질수록, 코드 길이가 늘어날수록 정적 분석의 중요성은 커진다. 정적 분석은 런타임에 쉽게 발견할 수 없는 결함(defects)을 조기에 검출하여 더욱 정교하고 완성도 높은 코드를 생산하는 데 큰 도움이 된다.

C/C++ World에는 Coverity와 같은 값 비싼 기업용 도구도 있고, llvm과 같은 오픈 소스 범용 컴파일러에 포함되어 있기도 하다. Java는 findbug, 파이썬은 pep8을 지원하는 flake8, 루비는 RuboCop과 같은 도구들을 이용할 수 있다. 그렇다면 쉘 스크립트는 어떨까?

사실 쉘 스크립트를 만들면서 정적 분석을 해야겠다는 생각은 한 번도 해 본 적이 없다. 쉘 스크립트를 작성하는 목적은 대부분 실험 자동화, 결과 분석 등 이었기 때문에 해당 케이스에 의존적이었고, 한번 쓰고 버릴 코드라 생각했다. 그래서 대충대충 원하는 결과만 뽑을 수 있으면 되는 코드를 만들었다. 그런데 얼마 전 쉘 스크립트 정적 분석 도구를 발견했다. 사용해보니 신세계가 아닐 수 없었다.

그렇다. 오늘 팔아볼 약 소개할 도구는 쉘 스크립트 정적 분석기이다.

 

ShellCheck

깔끔한 쉘 스크립트를 만들고 싶으십니까?
최신 문법을 몰라 아재향 물씬한 스크립트를 만들고 계시다고요?
ShellCheck이 여러분의 코드를 아름답게 만들 수 있도록 도와드립니다.
지금 바로 설치하세요!

use_it

구구절절 설명하는 것 보다 써보면 좋다는 것을 확실히 알 수 있다. 다음 예제를 사용해 확인해 보자.

#!/bin/bash
T0=`date +%s`
sleep 1
T1=`date +%s`
ELAPSED_TIME=$((T1-T0))

echo "START_TIME: " ${T0}
echo "END_TIME: " ${T1}
echo "ELAPSED_TIME: ${ELAPSES_TIME} sec"

ShellCheck의 웹페이지에는 Ace(Ajax Cloud Editor)를 제공하고 있다. 이 에디터에서 스크립트를 작성하거나 이미 작성한 스크립트를 붙여넣으면 즉시 피드백을 준다.

shellcheck_01

에디터에 예제 코드를 붙여넣자 위 그림과 같이 2건의 결함과 2건의 제안(각각 2건씩 중복)이 출력되었다. 하나하나 살펴보면 다음과 같다.

SC2006: Use $(..) instead of legacy `..`.
  # 레거시 스타일이다. 아재 코드라 할 수 있다.
  # `...`와 같이 backtick(또는 backquote)로 감싼 명령은
  # 중세이전 Bourne Shell에서 사용하던 형식이다.
  # Bash에서는 $( ... ) 형태로 변경되었다.
  # 가능하면 최신 문법을 사용하지 않겠는가!
  # Bash가 나온지도 거의 30년이 지나 이제 최신이라 하기 어렵지만
  # Bourne Shell 보다는 최신이다.

SC2034: ELAPSED_TIME appears unused. Verify it or export it.
  # 변수 ELAPSED_TIME은 선언만하고 사용되지 않는다.
  # 확인해보고 필요 없다면 제거하자.

SC2086: Double quote to prevent globbing and word splitting.
  # 변수를 사용할 때, 쌍따옴표로 감싸주는 것을 추천한다.
  # "${VAR}" 이렇게 하면 글로빙 또는 단어가 분리되는 문제를 막을 수 있다.

SC2153: Possible misspelling: ELAPSES_TIME may not be assigned, but ELAPSED_TIME is.
  # 오타가 있는 것 같다.
  # 여기서 사용한 ELAPSES_TIME이라는 변수는 할당되어 있지 않다.
  # 아마도 앞에서 선언만하고 사용하지 않은 ELAPSED_TIME인 것 같다.

이처럼 레거시 코드도 지적해주고, 오타도 잡아주고, 런타임에 발생할지도 모르는 문제에 대해 경고도 해준다. ShellCheck을 쓰기만 하면 완벽한 스크립트 코드를 만들 수 있을 것 같은 기분이 든다. 어서, 지금, 당장 스크립트를 만들고 싶다.

그런데 불편한 점이 있다. 스크립트를 만들 때마다 ShellCheck 웹페이지에 접속하고, 쉘 스크립트를 붙여넣고, 검사 결과를 반영하고, 다시 붙여넣어 확인하자니 너무 번거롭다. 웹 브라우저가 아닌 터미널에서 사용하고 싶다. 그럼 로컬 PC에 설치해야 하는데…

터미널에서 사용

ShellCheck은 대부분의 패키지 관리자를 지원하는 패키지들을 배포하고 있다. OS의 패키지 매니저에 따라 골라 설치하시라.

  • On Debian / Ubuntu
# apt-get install shellcheck
  • On REHL / CentOS
# yum -y install epel-release
# yum install ShellCheck
  • On Fedora
# dnf install ShellCheck
  • On macOS
$ brew install shellcheck

설치 후 앞에서 사용했던 예제를 파일로 저장하고 터미널에서 검사하면 웹페이지와 동일한 결과를 얻을 수 있다.

$ shellcheck test.sh

In test.sh line 2:
T0=`date +%s`
   ^-- SC2006: Use $(..) instead of legacy `..`.

In test.sh line 4:
T1=`date +%s`
   ^-- SC2006: Use $(..) instead of legacy `..`.

In test.sh line 5:
ELAPSED_TIME=$((T1-T0))
^-- SC2034: ELAPSED_TIME appears unused. Verify it or export it.

In test.sh line 7:
echo "START_TIME: " ${T0}
                    ^-- SC2086: Double quote to prevent globbing and word splitting.

In test.sh line 8:
echo "END_TIME: " ${T1}
                  ^-- SC2086: Double quote to prevent globbing and word splitting.

In test.sh line 9:
echo "ELAPSED_TIME: ${ELAPSES_TIME} sec"
                    ^-- SC2153: Possible misspelling: ELAPSES_TIME may not be assigned, but ELAPSED_TIME is.

한결 편리해졌다. 그러나 개발자는 게으른 짐승. 에디터에서 바로 확인할 수 있으면 더 편리할 것 같다. 그래서 플러그인을 검색해보았다.

플러그인

vim에 syntastic이라는 플러그인을 설치하면, 파일로 저장할 때 ShellCheck이 동작하여 정적 분석을 해준다.
설치 과정은 다음과 같다.

  • syntastic 설치를 쉽게 하기 위해 pathogen 설치
$ mkdir -p ~/.vim/autoload ~/.vim/bundle && curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim
  • vim이 pathogen을 실행하도록 ~/.vimrc에 다음과 같은 명령 추가
execute pathogen#infect()
  • syntastic 설치
$ cd ~/.vim/bundle && git clone --depth=1 https://github.com/vim-syntastic/syntastic.git

이제 vim에서 ShellCheck을 사용할 준비가 끝났다. 앞의 예제를 입력하고 저장해보자.

shellcheck_02

위와 같이 라인 왼쪽에 결함(>>) 또는 제안(S>)이 있음을 표시해준다. 해당 라인으로 커서를 이동하면 하단에 에러 메시지가 출력된다. 바로 수정해 반영하고 피드백을 받아 볼 수 있다.

아래 표와 같이 vim 뿐만 아니라 많이 사용되는 에디터들을 위한 플러그인이 제공되고 있다. 익숙한 도구용 플러그인을 골라 사용하면 된다.

에디터 플러그인
vim syntastic
emacs flycheck
sublime SublimeLinter
atom linter

사용해보니…

서버 진단 스크립트를 만들면서 처음 사용해보고 결과가 너무 만족스러웠다. 습관적으로 사용하던 레거시 코드들을 깔끔하게 고칠 수 있었고, 디버깅에 들어가는 시간과 노력을 대폭 줄일 수 있었다. 고수에게 코드 리뷰를 받은 기분이랄까?

만족감을 느끼며 커밋 메시지에 이렇게 새겼다.

do_shellcheck

ShellCheck을 써라! 두번 써라!

복잡한 확률 통계를 손쉽게 – R

시스템 인프라 관련 개발을 진행하다 보니 수많은 성능 측정 데이터들을 어떻게 하면 보기 좋게 정리할 수 있을지 고민이 많다. 일반적으로 이런 일에 가장 많이 사용하는 도구는 당연히 스프레드시트다. 스프레드시트의 대명사 엑셀은 응용범위가 넓고, 간단한 수식과 함수를 이용해 다양한 결과물을 도출해 낼 수 있는 강력한 데이터 처리 도구이다. 많은 회사에서 사용하고 있어 손쉽게 접할 수 있기도 하다. 그러나 범용이기 때문에 원하는 결과를 만들기 위해서는 품이 많이 들 수밖에 없다.

 

최근 진행한 디스크의 성능을 측정하는 업무를 복기해 보았다. 결과의 정확도를 높이기 위해 반복 실험을 진행하여 1,500개의 결과를 얻었다. 결과 파일에는 필요한 메트릭만 있는 것이 아니므로 원하는 값을 선택해 엑셀 시트에 입력해야 했다. 1,500개의 결과 파일을 일일이 열어 원하는 메트릭을 뽑아 시트에 입력하는 것은 말도 안 되는 중노동이었기 때문에, bash script를 이용해 필요한 메트릭만을 추출해 tsv 파일을 만들어냈다. 이 파일을 엑셀에서 읽어 들여 시트에 입력하고, 각 메트릭의 통계를 위한 수식들을 만들었다. 그리고 별도로 그래프를 생성했다. 어렵지 않은 작업이지만 반복되는 일들이 있어 생각보다 많은 시간이 걸렸다. 필요한 통계치를 도출하기 위해 알고 있어야 하는 함수와 옵션들도 많고, 확률분포 그래프라도 그리려면 더 많은 작업을 해야 한다.

 

이런 일을 쉽고 빠르게 하는 방법이 없을까? SAS나 SPSS와 같은 통계 전용 프로그램을 사용하면 어떨까? 아니면 수치 해석용 프로그램인 MATLAB은? 모두 진입장벽이 높은 도구들이다. 게다가 비싸기까지 하다.

 
 

하지만 R이 출동하면 어떨까? r_logo_300

R은 통계와 그래프를 위한 오픈소스 프로그래밍 언어이다. 다양한 통계 기법과 수치 해석 기법을 제공하며 사용자 제작 패키지를 추가하여 기능 확장도 가능하다.

일단 R의 프로젝트 사이트(https://cran.r-project.org)에서 제공하는 문서를 찾아보았다. 문서를 열자 영문 스크립트가 방대하게 펼쳐진다. 당장 필요한 것은 최대/최솟값과 중간값, 평균값 그리고 테스트 조건별 확률분포 그래프 정도인데 전체 문서를 정독하며 학습할 필요는 없을 것 같다. 필요한 내용만 체리피킹 해보자.

 

설치

R은 Windows, Mac, Linux를 모두 지원한다. 웹페이지를 참조하면 쉽게 설치할 수 있다.

  • Windows
    • 웹페이지에서 제공하는 인스톨러를 다운로드하여 설치한다.
  • Mac
    • 역시 웹페이지에서 인스톨러를 제공한다.
    • 그래프를 그리기 위해 XQuartz가 필요하니 설치되어 있지 않다면 먼저 설치하고 진행하자.
    • brew를 이용해 설치할 수도 있다.
    $ brew tap homebrew/science
    $ brew install Caskroom/cask/xquartz
    $ brew install r
    
  • Linux (Ubuntu)
    • apt를 이용해 쉽게 설치할 수 있다.
    $ sudo apt-get install r-base
    

 

실행

Unix 계열에서는 터미널을 열고 R을 입력하면 인터프리터가 실행된다. 윈도에서는 설치 후 생성된 R 아이콘을 클릭하면 GUI 콘솔이 실행된다. Mac에서도 GUI 콘솔을 사용할 수 있다.

$ R

R version 3.3.2 (2016-10-31) -- "Sincere Pumpkin Patch"
Copyright (C) 2016 The R Foundation for Statistical Computing
Platform: x86_64-apple-darwin13.4.0 (64-bit)

R은 자유 소프트웨어이며, 어떠한 형태의 보증없이 배포됩니다.
또한, 일정한 조건하에서 이것을 재배포 할 수 있습니다.
배포와 관련된 상세한 내용은 'license()' 또는 'licence()'을 통하여 확인할 수 있습니다.

R은 많은 기여자들이 참여하는 공동프로젝트입니다.
'contributors()'라고 입력하시면 이에 대한 더 많은 정보를 확인하실 수 있습니다.
그리고, R 또는 R 패키지들을 출판물에 인용하는 방법에 대해서는 'citation()'을 통해 확인하시길 부탁드립니다.

'demo()'를 입력하신다면 몇가지 데모를 보실 수 있으며, 'help()'를 입력하시면 온라인 도움말을 이용하실 수 있습니다.
또한, 'help.start()'의 입력을 통하여 HTML 브라우저에 의한 도움말을 사용하실수 있습니다
R의 종료를 원하시면 'q()'을 입력해주세요.

> 

 

데이터 읽어들이기

사용할 데이터는 worker의 개수를 10에서 250까지 변경하며 각 케이스의 bandwidth를 30회 반복 측정한 결과이다. bash script를 이용해 다음과 같은 포맷의 tsv 파일을 만들었다.

$ cat result.tsv
worker  bandwidth
010     370.4
020     356.4
030     291.2
040     162.4
050     166.9
060     160.0
070     156.2
080     243.3
090     275.0
100     258.6
...

이 파일을 R에서 읽기 위해서는 read.table() 함수를 사용하면 된다.

> result <- read.table('./result.tsv', header=TRUE, sep='\t')
> result
    worker bandwidth
1       10     370.4
2       20     356.4
3       30     291.2
4       40     162.4
5       50     166.9
6       60     160.0
7       70     156.2
8       80     243.3
9       90     275.0
10     100     258.6
...

read.table() 함수에는 수 많은 옵션이 있지만, 예제에서 사용한 정도만 알면 csv, tsv 파일은 쉽게 읽을 수 있다.

  • header=TRUE
    • TRUE이면 파일의 첫 줄을 header로 사용한다.
    • FALSE이면 자동으로 V1, V2… 형태의 컬럼 이름이 붙는다.
    • default 값은 FALSE이다.
  • sep='\t'
    • 컬럼 구분할 구분자를 설정한다.
    • csv의 경우 tab 문자 대신 comma를 지정해주면 된다.
    • default 값은 하나 이상의 white space이다. space와 tab의 경우 구분자를 지정하지 않아도 된다.

이제 result라는 데이터셋 객체에 tsv 파일의 데이터가 로드되었다. read.table() 함수 외에도 읽을 파일 포맷에 따라 read.csv(), read.delim() 등의 함수를 사용할 수도 있다. read.csv()는 header=TRUE, sep=',', read.delim()는 header=TRUE, sep='\t'이 default 옵션이다.

 

통계치 얻기

전술했지만 필요한 통계치는 최대/최솟값과 중간값, 평균값이다. 엑셀에서는 각각 수식을 작성하고 범위를 지정하면 얻을 수 있는 값들이다. R을 이용하면 더 간단하고 빠르게 모든 값을 얻을 수 있다. 요약 통계를 출력하는 summary() 함수를 제공하기 때문이다.

> bw <- result$bandwidth  # result 데이터셋의 bandwidth 컬럼 데이터를 bw 객체에 할당

> summary(bw)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  156.2   263.2   280.3   284.3   297.3   383.3 

예제는 result 데이터셋의 bandwidth 컬럼이 가진 모든 값을 대상으로 최대/최솟값, 1사/3사분위값, 중간값, 평균값을 계산해 출력해준다. 엑셀처럼 각각의 함수를 이용해 계산할 수도 있다. min(), max(), median(), mean() 등의 함수를 이용하면 된다.

 

summary()와 유사한 fivenum(), quantile() 함수도 제공되는데, 평균값을 제외한 나머지 값들을 출력해준다. 다만 quantile() 함수는 확률적으로 균등하게 자른 값을 출력해주기 때문에 summary()나 fivenum()의 출력값과 결과가 다를 수 있다.

> fivenum(bw)
[1] 156.20 263.15 280.30 297.35 383.30

> quantile(bw)
     0%     25%     50%     75%    100%
156.200 263.225 280.300 297.325 383.300

 

이 밖에도 통계하면 떠오르는 분산, 표준편차를 계산해주는 함수도 제공한다. 각각 var()sd() 함수다.

> var(bw)
[1] 1451.567

> sd(bw)
[1] 38.09944

이처럼 R이 기본 제공하는 함수들을 이용하여 간단하게 필요한 통계치들을 모두 얻을 수 있다.

 

그래프 그리기

통계치를 얻었으니 이제 확률분포 그래프를 그릴 차례다. 확률분포 그래프는 셀을 잘게 잘게 쪼갠 히스토그램을 이용해 그린다고 배웠던 것 같은 느낌적 느낌이 기억 저편에 흐릿하다. 일단 그려보자. 히스토그램은 hist() 함수를 이용하여 그릴 수 있다.

> hist(bw, breaks=100, probability=TRUE)
  • breaks=100
    • 히스토그램 셀의 개수
    • 숫자가 커질수록 선형에 가까워진다.
    • default 값은 10
  • probability=TRUE
    • Y축을 출현 확률로 지정
    • FALSE인 경우, Y축은 출현 빈도를 나타낸다.
    • default 값은 FALSE

histogram

가운데가 불룩한 정규분포도에 정확히 부합하지는 않지만 비슷한 결과가 나오기는 했다. 정규분포도와 다른 이유는 데이터셋의 문제다. worker의 개수를 10에서 250까지 변경하며 bandwidth를 반복 측정했던 데이터이기 때문에 worker 개수 별 분포가 섞여 있는 결과가 나온 것이다. 정확한 그래프를 얻기 위해서는 worker의 개수를 각각의 집단으로 하는 집단별 히스토그램을 그려야 한다.

25개의 히스토그램을 그리자니 몹시 귀찮다. 한꺼번에 그릴 수는 없을까? 물론 방법은 있다.

 

박스플롯(boxplot)

박스플롯은 fivenum() 또는 quantile() 함수로 얻을 수 있는 5가지 통계치(최솟값, 1사분위값, 중간값, 3사분위값, 최댓값)을 이용해 그리는 상자 형태의 그래프다. 히스토그램과는 다르게 집단이 여러 개인 경우에도 하나의 그래프에 표현할 수 있다. R에서는 박스플롯 그래프를 그리기 위해 boxplot() 함수를 제공한다.

> worker <- report$worker  # result 데이터셋의 worker 컬럼 데이터를 worker 객체에 할당
>
> boxplot(bw~worker, ylim=c(0, 400), xlab=~Workers, ylab=Bandwidth~MB/s)
>
> grid()  # 그래프에 grid 표시
  • bw~worker
    • y~grp, 그룹별 numeric vector를 나타내는 수식
    • worker의 개수 별 그룹을 만들고 각각의 bandwidth를 입력하겠다는 의미
  • ylim=c(0, 400)
    • y축의 범위를 0에서 400으로 설정
  • xlab, ylab
    • x축과 y축의 label 설정

boxplot

이상한 그래프가 그려졌다. 그래프만 봐서는 무슨 의미인지 잘 모르겠다. 좀 더 자세히 알아보자.

boxplot_singlebox

박스는 1사분위값과 3사분위값을 경계로 그려지며 박스 안의 선은 중간값이다. 박스는 전체 분포의 50%에 해당한다. LIF와 UIF는 각각 하한값 상한값을 나타내며 두 값의 사이는 전체 분포의 약 99%에 해당한다. LIF와 UIF 밖의 값들은 이상 수치로 간주하고 통계에서는 제외한다.

이를 정규분포도와 비교해보면 각각의 박스플롯은 그룹별 정규분포를 나타낸다는 것을 알 수 있다.

boxplot_vs_norm_dist

 

이런 그래프를 그려야 하는 이유가 무엇일까? 앞서 설명한 바와 같이, 이 그림은 worker 갯수 별 bandwidth의 정규분포를 박스플롯으로 나타낸 것이다. 즉, multi process 환경에서 디스크의 대략적인 bandwidth를 한눈에 쉽게 알아보기 위함이다.

앞의 박스플롯 그래프를 다시 보면 대부분의 박스가 250~300MB/s 사이에 분포한다는 것을 쉽게 알 수 있다. 따라서 이 디스크의 bandwidth는 대략 250~300 MB/s라 할 수 있다.

 
 

마치며…

… 확실히 확률과 통계는 어렵다. 배운지 20년이 다 되어가니 다시 공부해도 잘 모르겠다. 손으로 계산하는 것은 상상도 못 할 것 같다. 하물며 엑셀을 이용한다 하더라도 쉽지 않은 작업일 것이다. R은 이런 복잡한 계산을 순식간에 처리해준다. 필요한 내용만 빠르게 학습한 수준이지만, 앞으로 있을 여러 가지 성능측정 시험을 정리하는 데 유용하게 사용할 수 있을 것 같다.