Uploaded avatar of rpalo

Bash Grains에서 의도적으로 코딩하기

@rpalo
7년 초과 전

스포일러 주의: 이 글에는 곡물 연습 문제 전반, 특히 Bash 트랙의 곡물 연습 문제에 대한 스포일러가 담겨 있어요. 아직 직접 끝내지 않았고 풀이를 미리 보고 싶지 않다면, 다 끝낸 뒤에 다시 돌아와 주세요!

새 회사에서의 첫날이에요. 서류 작업을 다 마쳤고, 팀원들과도 인사를 나눴고, 이제 드디어 자리에 앉아서 앞으로 작업하게 될 코드를 읽기 시작할 시간이에요. 여러 함수와 클래스, 모듈을 차례로 읽어 나가다 보면, 어느새 고개를 갸웃하며 화면을 다시 들여다보고 있는 자신을 발견하게 돼요. 계속 읽다 보면, 입 밖으로 거의 소리도 내지 않고 숨처럼 새어 나오는 한마디가 있어요: "뭐어어어어..."1 읽으면 읽을수록 점점 더 어리둥절해지고, 심지어 조금 화가 나기도 하면서 이런 일이 자꾸 생겨요.

이 코드에서 대체 무슨 일이 벌어지고 있는 걸까요?

한 사람 이상이 같은 코드를 다루기 시작하면, 코드를 관리 가능한 상태로 유지하기 위해 필요한 세심함과 의도적인 고민이 훨씬 많아져요. 더 이상 개념은 머릿속에만 있고 코드는 그 개념을 실현하기만 하면 되는 대상이 아니에요. 이제 개념은 코드 안에 살아야 하고, 함께 작업하는 모든 사람이 그것을 보고 필요하면 바꿀 수 있어야 해요.

어떤 기능을 어떻게 구현하는지는 최종 사용자에게는 별로 중요하지 않아요. 하지만 언젠가 그 설계를 다루게 될 모든 엔지니어에게는 아주 많은 것을 말해 줘야 해요. 같은 기능을 구현하는 방법은 흔히 여러 가지가 있고, 어느 것을 골라도 일을 끝내는 데에는 충분해 보일 수 있어요. 하지만 저는 결정 하나하나에 이유가 있어야 한다고 믿어요(작은 결정에 작은 이유라도요). 그리고 그 이유는 목표나 요구 사항을 전달해야 해요.

구현 세부 사항이 코드를 읽는 사람으로 하여금 사고 과정과 목표, 우선순위를 파악하도록 도와야 한다는 생각을 설계 의도라고 해요. 변수 이름을 어떻게 짓는지, 함수가 어떤 매개변수를 받는지, 대상을 어떻게 추상화하는지는 모두 설계 의도를 드러낼 수 있는 지점이에요. 잘 드러낼 수도 있고, 엉망으로 드러낼 수도 있죠.

저는 설계 의도가 엔지니어링 설계를 구현할 때 가장 중요하게 고려해야 할 것 중 하나라고 굳게 믿어요. 소프트웨어 공학을 프로그래밍과 구별 짓는 것 중 하나가 바로 이것이에요.

소프트웨어 공학은 프로그래밍에 시간과 다른 프로그래머들을 더했을 때 벌어지는 일이에요.

Russ Cox

설계 의도는 분야를 가리지 않아요

저는 기계 엔지니어로 일하면서, 주로 의료 기기에 들어가는 사출 금형을 설계해요. 제 설계는 완성되면 곧바로 기계 가공 작업장으로 넘어가고, 거기서 사람들이 부품을 만들고 조립하기 시작해요. 그 사람들은 제가 각 설계를 만들면서 머릿속에 무슨 생각을 했는지 전부 알지 못하니까, 저는 설계 자체를 통해 제 의도를 보여 줄 방법을 찾아야 해요.

자주 있는 일인데, 어떤 형상은 특히 중요해요. 고객이 그 부분에 특별히 정밀한 공차가 필요하다고 말했거나, 금형이 조립되는 방식 때문에 어떤 이유로든 극도로 정확해야 하는 경우예요. 그래서 기계공이 중요한 부분의 정확도를 우선시하도록 부품을 만들 수 있게 돕기 위해, 저는 일부러 완전한 직각으로 만들거나, 바이스에 특정한 방식으로 물리기 쉬운 자리를 남겨 둬요. 그렇게 하면 그들이 가장 손쉽게 가는 길이 제게는 가장 좋은 결과를 만들어 줘요.

치수가 그렇게까지 중요하지 않은 부분도 있어요. 예를 들어 공기 배출구 용도로 설계에 구멍을 낼 때는, 6mm처럼 흔하고 깔끔한 크기로 만들어요.

이 구멍을 가공하고 나서 결과가 어떻게 나왔는지 측정했을 때 5.99mm 같은 숫자가 나오면, "아, 이건 원래 6mm였겠구나, 꽤 가깝네" 하고 생각하고, CAD나 사양 도면의 치수를 다시 확인할 필요조차 없어요. 반대로 5.87mm처럼 흔치 않은 크기로 만들었다면, 그 사람들은 그것을 보고 처음에 이렇게 반응해요:

  1. 이런, 내가 너무 작게 깎았나 봐요. 원래 6mm였어야 했나요?
  2. (CAD를 확인해 보니 구멍은 제대로 나왔고, 그냥 흔치 않은 크기일 뿐이에요.)
  3. 흠. 이 구멍이 흔치 않은 크기인 데에는 분명 이유가 있을 거예요. 어쩌면 정말 중요한 부분이거나, 고객이 여기에 특별한 구멍을 요청한 걸지도 몰라요. Ryan에게 가서 이 구멍이 뭐가 그렇게 중요한지 물어봐야겠어요.
  4. (쾅! 그 사람들이 알루미늄 덩어리를 제 책상 위에 살포시 내려놓아요.)
  5. (이 구멍에는 별로 중요한 게 없다는 걸, 제가 그냥 이상한 크기를 골랐다는 걸 알게 돼요. 이 모든 추가 작업과 걱정이 아무 이유 없이 일어난 거예요.)
  6. Ryan이라는 사람, 정말 별난 사람이네요. (투덜투덜, 욕설, 투덜투덜)

이런 일이 벌어지는 이유는, 제가 의도하든 하지 않든 제 설계의 모든 결정이 그것을 보고 작업하는 다른 사람들에게 무언가를 전달하기 때문이에요. 그 사람들은 그 안에서 의미를 찾을 수밖에 없어요. 그것이 그들이 의지할 수 있는 유일한 정보니까요! 그러니 시간을 들여 제 설계에 의미 있는, 의도적인 정보를 담을 수 있다면 훨씬 좋아요.

곡물: 들어가며

이제 Exercism 연습 문제 중 하나를 예로 들어, 코드에서 설계 의도를 어떻게 전달할 수 있는지 이야기해 볼까요. 저는 최근에 한 학생과 함께 Bash 트랙의 곡물 연습 문제 풀이를 작업했어요. 곡물은 밀과 체스판 문제를 다루는 연습 문제예요. 간단히 말해, 체스판의 첫 번째 칸에 밀 한 톨을 놓아요. 다음 칸에는 두 톨을 놓아요. 그다음 칸에는 네 톨을 놓아요. 이렇게 각 칸이 이전 칸의 두 배가 되도록 계속해요. 학생들은 각 칸의 값과 체스판 전체의 밀알 총개수를 계산하는 방법을 찾아야 해요.

이번 학생은 총합을 계산하는 꽤 영리한 방법을 떠올렸어요.

bc <<< 'ibase=16;FFFFFFFFFFFFFFFF'

bc는 명령줄 계산기예요. 산술 식을 문자열로 넘겨주면, 아주 큰 정수나 실수에 대해서도 계산해 줘요. Bash에서 bc를 쓰지 않고 계산하는 다른 방법도 있지만, 간단하게 하기 위해 여기서는 bc를 사용할 때 의도를 어떻게 전달할 수 있는지(혹은 없는지)를 살펴볼게요.

이 풀이가 통하는 이유는 연습 문제 전체가 2의 거듭제곱을 중심으로 돌아가기 때문이에요. 2의 거듭제곱이 있는 곳에 이진법이 있고, 이진법이 있는 곳에 십육진법이 있으니까요2!

영리한 풀이이긴 하지만, 이 코드는 우리에게 무엇을 말해 주나요? 여기서 십육진법이 중요하다는 걸까요? 문제가 근본적으로 16을 중심으로 돌아간다는 걸까요? 문제 설명을 다시 읽어 보면, 둘 다 아니라는 게 꽤 분명해요. 학생과 저는 의도를 더 명확하게 전달할 방법을 함께 머리를 맞대고 고민했어요. 그중 몇 가지를 소개할게요:

첫 번째 방법: 이진법

두 배가 되는 것들이 잔뜩 있으니(그러니까 2의 거듭제곱도 잔뜩 있으니), 이진법으로 보면 어떤지 한번 살펴봐요. 도움이 될지도 몰라요.


첫 번째 칸에는 밀알이 1톨 있어요. 이진법으로는 0b1이에요. (0b는 "이것은 이진수"라는 뜻이고, 실제 숫자는 1이에요.)

두 번째 칸에는 2톨이 있어요. 이진법으로는 0b10이에요. 지금까지의 합계는 3(0b11)이에요.

세 번째 칸에는 4톨(0b100)이 있어요. 지금까지의 합계: 7(0b111)이에요.

네 번째 칸에는 8톨(0b1000)이 있어요. 지금까지의 합계: 15(0b1111)이에요.


규칙이 보이나요?

각 칸은 이진수 자릿수 하나를 나타내고, 그것들을 모두 더하면 그냥 1이 잔뜩 모인 수가 돼요.

학생의 풀이에서 F들을 1 64개(칸마다 하나씩)로 바꿀 수도 있어요!

bc <<< "ibase=2;1111111111111111111111111111111111111111111111111111111111111111"

더 의도적이에요. 문제가 우리에게 주는 것에 더 가깝기 때문이에요. 하지만 우리는 로봇 말을 하지 않아요. 사실상 셀 수도 없는 길다란 1의 나열은 그다지 나아진 것 같지 않아요.

두 번째 방법: 무식하게 계산하기

좋아요, 그럼 십진법이 아닌 진법은 아예 포기해 봐요. 체스판의 밀알 개수를 손으로 세어 합계를 낼 때처럼, 칸마다 밀알을 세는 방식으로 코드를 짜면 어떨까요?

total=0
current_grains=1
for square in {1..64}; do
  total=$( bc <<< "$total + $current_grains" )
  current_grains=$( bc <<< "$current_grains * 2" )
done
echo "$total"

훨씬 읽기 쉽고 이해하기 쉬워요. 이 코드는 체스판의 칸 수가 핵심 요소라는 것과, 칸마다 두 배가 된다는 점을 분명히 보여 줘요. 처음 풀이보다 낫다고 생각해요.

그런데.

느려요. 반복하고, 더하고, 외부 명령을 계속 불러내는 것? 이 모든 게 겹쳐서 실행 시간이 제법 느려져요. 그게 그렇게 큰일일까요? 아니에요. Bash로 이런 스크립트를 짠다면, 이미 속도 제약은 신경 쓰지 않기로 한 셈이니까요. 그래도 더 나아질 수 있을까요? 네.

세 번째 방법: 곧바로 계산하기

그럼 반복하지 않고 이걸 전부 더하려면 어떻게 해야 할까요?

같은 문제의 더 작은 버전을 생각해 봐요. 칸이 5개인 체스판이에요3.

다섯 칸에 들어가는 밀알 개수는 이래요:

---------------------
| 1 | 2 | 4 | 8 |16 |
---------------------

그리고 여기서 총합은 1 + 2 + 4 + 8 + 16 = 31이에요. 흠. 31만 보고는 아직 뭐가 뚜렷하게 눈에 들어오지 않아요. 조금 더 키워 봐요.

좋아요, 그럼 칸이 6개인 체스판은 어떨까요? 이번에는 더하기 쉽게 각 칸 아래에 누적 합계를 보여 줄게요.

-------------------------
| 1 | 2 | 4 | 8 |16 |32 |
|   | 3 | 7 |15 |31 |63 |
-------------------------

그리고 합은 1 + 2 + 4 + 8 + 16 + 32 = 63이에요. 흠... 사실 이제 규칙이 어렴풋이 보이기 시작하네요. 그래도 확실히 하기 위해 하나만 더 해 볼게요.

7개 칸:

-----------------------------
| 1 | 2 | 4 | 8 |16 |32 |64 |
|   | 3 | 7 |15 |31 |63 |127|
-----------------------------

1 + 2 + 4 + 8 + 16 + 32 + 64 = 127이에요. 보이나요? 31, 63, 127이라는 값에서 뭔가 느껴지는 게 있나요?

이 수들은 2의 거듭제곱에 거의 가까워요. 사실 다음 2의 거듭제곱보다 1 작아요.

확실히 하기 위해 예를 하나 더 들어 볼게요. 칸이 12개인 체스판을 상상해 봐요. 이건 1을 11번 두 배로 늘린 값이고(수학계에서는 2^11이라고 하죠), 2048이에요. 다시 두 배로 하면 4096(2^12)이 돼요. 그러니까... 규칙을 제대로 파악했다면 누적 합계는 4096보다 1 작은 수, 즉 4095여야 해요. 실제로 세어 보면 정확히 그렇게 나와요: 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256 + 512 + 1024 + 2048 = 4095.

다르게 말하면, n개의 칸 전체의 총합을 구하려면 2의 거듭제곱을 하나 올리고 그 결과에서 1을 빼면 돼요.

64번째 칸에 있는 밀알 개수는 2^63이에요(0부터 세는 거, 기억하죠?). 그러니까... 64번째 칸까지 포함해서 모든 칸의 밀알 총합을 구하려면, 2^64를 계산하고 1을 빼면 돼요.

짜잔.

Bash로 쓰면 이렇게 돼요:

bc <<< "2^64 - 1"

이진법으로 무슨 일이 일어나는지 확인해 보면 이게 말이 돼요. 이진법으로 하면 64개 칸 전체의 합계는 무엇이었죠?

0b1111...  # 64 ones

이론상 65번째 칸에 있는 밀알 개수는 얼마예요?

0b10000... # 1 and 64 zeros

1과 0 64개에서 1 64개로 어떻게 갈 수 있을까요? 1을 빼면 돼요.

그리고 이것이 우리에게 어떤 추가 이점을 줄까요? 우선 총합을 나타내는 읽기 좋은 식이 생겼어요. 반복하지 않으니 성능도 좋아요. 그리고 체스판의 칸 수인 64라는 숫자가 들어 있어요. 이건 설계 의도가 잘 드러난 좋은 예예요. 만약 1000년 뒤에 세계가 7x7 체스판으로 표준화된다면, 그 미래의 엔지니어(아마 Bash 6.1을 쓰고 있겠죠)가 스크립트를 확인하고 여러분이 무엇을 하려 했는지 알아낸 뒤 64를 49로 바꾸면 돼요. 문제없어요!

여러분, 의도를 잃지 마세요

구현 방법을 고민할 때는 이것저것 던져 보고 처음으로 작동하는 풀이에 매달리기 쉬워요. 문제를 탐색하는 동안에는 그래도 괜찮아요. 하지만 핵심 요소를 완전히 이해하고 나면(그리고 제대로 다듬을 시간이 있다면), 모든 알고리즘과 변수 이름, 심지어 공백까지 문제와 핵심 요구 사항, 그리고 모든 조각이 어떻게 맞물리는지를 그림처럼 보여 주도록 해요.

  1. Thom Holwerda의 만화도 참고해 보세요. ↩

  2. 이진법과 십육진법 세는 법이 좀 가물가물하다면, @kytrinyx가 How to Count 책을 추천해요. 뻔뻔한 홍보를 하나 하자면, 저도 최근에 이진법과 십육진법에 관한 블로그 글을 몇 편 썼어요. ↩

  3. 그게 어떻게 될지는 저도 몰라요. 그냥 폰끼리 창을 들고 겨루게 하면 되지 않을까요? ↩

2019년 02월 14일 · 유용했나요?