코딩을 지탱하는 기술

지은이 / 니시오 히로카즈 | 옮긴이 / 김완섭

by Joong

코딩을 지탱하는 기술
: 원리로 깨우치는 프로그래밍 기법
초판 1쇄 발행 2013년 10월 17일
지은이 / 니시오 히로카즈
옮긴이 / 김완섭
발행처 / 비제이퍼블릭(서울시 종로구 내수동 73 경희궁의 아침 4단지 오피스텔 #1004)


1장 효율적으로 언어 배우기


프로그래밍 언어의 교과서에는 다양한 규약이 기술되어 있지만, 그것은 절대적인 약속이 아니다. ‘지금은 이렇게 약속하는 게 보다 수월하다고 생각하니 그렇게 합시다’라는 의미일 뿐이다. [2p]


지금도 수많은 프로그래밍 언어가 ‘배우는 편이 좋아’라고 말하고 있다. 그러나 개별 언어 지식이 5년 후, 10년 후에도 도움이 될지는 아무도 모른다. [5p]


이 책은 특정 언어에 국한된 지식이 아니라, 보다 보편적인 지식을 습득할 수 있도록 하고 있다. 이를 위해 ‘비교를 통한 배움’과 ‘역사를 통한 배움’이라는 2가지 방법을 사용한다.
‘비교를 통한 배움’이란 특정 언어로 프로그래밍을 배우는 것이 아니라, 다수의 언어를 비교해 가면서 학습하는 것을 의미한다. 이를 통해, 무엇이 언어에 따라 다르고 무엇이 공통적인지 배울 수 있다.
‘역사를 통한 배움’이란 언어가 어떻게 바뀌었고 바뀌기 전에는 어떤 의문점이 존재했는지 학습하는 것을 의미한다. 이를 통해, 언어가 가지고 있는 다양한 기능이 ‘왜’ 탄생했는지 배울 수 있다. [6p]


2장 프로그래밍 언어를 조감하다.


프로그램은 어떤 것을 편하게 하기 위해 고안된 것이다. 편하게 하는 것은 부실하게 하는 것과는 다르다. 부실하게 만들어서 나중에 고생케 하는 것은 편하게 만들었다고 볼 수 없다. [11p]


프로그래밍 언어의 목적은 ‘편리함’이라고 말했지만, 그럼 세상에는 왜 수많은 언어가 존재하는 것일까? 그것은 편하다는 의미가 사람에 따라 다르기 때문이다. 어떤 ‘편리함’을 목표로 했는지, 언어 설계자의 의도를 파악해보도록 하자. [12p]


3장 문법의 탄생


연산자는 연산 대상의 사이에 배치된다. 이와 같이 연산자를 연산 대상 뒤에 두는 것을 후위 표기법, 앞에 두는 것을 전위 표시법, 사이에 두는 것을 중위 표기법이라고 한다. [25p]


FORTH나 LISP은 규칙이 적은 것을 중시했다. 그러나 시장이 요구한 것은 규칙이 적거나 간단한 것이 아니었다. FORTRAN은 ‘*가 +보다 우선순위가 높다’등의 정해진 규칙을 대량으로 도입해서 ‘다가가기 쉬운 작성법’을 중시했다. 그리고 그런 설계 방침의 성공으로, FORTRAN 방식이 LISP나 FORTH보다 많은 사람에게 사랑 받게 되었다. [28p]


if-else 구문을 도입했을 때의 이점이 바로 여기에 있다. ‘조건이 참인 경우와 거짓인 겨웅의 처리 흐름을 분배한다’는 패턴은 프로그래밍에 빈번히 사용된다. 이것을 읽기 쉬운 형태로 쓰기 위해 if-else 구문이 새롭게 도입된 것이다.
프로그래밍을 하면서 else가 반드시 필요한 건 아니다. 하지만 저자는 else를 사용하고 싶다. 그게 더 편하기 때문이다. 또한 다른 사람도 else를 사용했으면 좋겠다. 그게 더 편하게 읽을 수 있기 때문이다. [35p]
이와 같이, while문도 break문도 ‘goto문만 있으면 가능한 것’을 하고 있는 것이다. while문이 가져온 편리함은 ‘새로운 것’아 아니라, ‘읽기 쉽게함’, ‘쓰기 쉽게 함’인 것이다.
goto는 강력하고 이해하기 쉬운 개념이다. 그러나 너무 원시적이다. 아무생각 없이 goto를 남용하면 프로그램이 엉망진창이 되어버린다. 말에 고삐를 채우듯이, goto에도 어느 정도 제한을 두는 것이 코드를 이해하기 쉽도록 한다. if-else나 while, break는 ‘제한이 붙은 goto’라고 생각하면 된다. [37p]


while은 조건식으로 반복을 제어한다. for문은 횟수로 반복을 제어한다. foreach구문은 처리 대상으로 반복을 제어한다. [39p]


for문에서는 ‘0이상 items 크기 미만의 범위에서 i를 1씩 증가시키면서 items의 I번째를 표시’라고 표현하고 있지만, foreach 구문을 사용하면 ‘items의 각 요소를 표시’라고 간단하게 쓰고 있다. [39~40p]


5장 함수


코드가 함수로 나눠져 있는 것은 큰 조직이 부서로 나눠져 있는 것과 닮았다. 작은 프로그램에서 함수를 쓰는 이점을 잘 모르는 것은 몇 명의 친한 사람끼리 부서를 만들 필요성을 못 느끼는 것과 같다. 사람 수가 적으면 그룹 전원의 얼굴과 이름, 특기 등을 쉽게 파악할 수 있다. 그와 마찬가지로, 코드도 행수가 적으면 어디서 무엇을 하고 있는지 간단히 파악할 수 있다.
문제는 사람 수가 늘었을 때다. 이렇게 되면 모든 것을 파악하는 것이 어려워진다. 그래서 몇 명을 하나의 그룹으로 묶어서 이름을 붙이게 된다. ‘경리과’ 라든지 ‘00개발팀’ 등과 같이 말이다.
프로그래밍도 동일하다. 소스 코드의 행수가 많아지면 전체를 파악하기 어렵게 된다. 그래서 몇 개의 행을 하나의 그룹으로 묶어서 거기에 이름을 붙이는 것이다. 이것이 함수다. [42p]


같은 처리를 한 군데에 정리하여 생기는 이점은 단순히 프로그램이 짧아지는 데에만 있지 않다. 소스 코드를 읽는 사람이 몇 번이고 같은 내용의 소스 코드를 읽을 필요도 없어진다. 길게 늘려있는 명령들을 자주 사용되는 단위로 잘라내어 정리함으로써 프로그램을 보다 쉽게 이해할 수 있게 된다. [44p]


함수 호출 전으로 돌아가도록 하는 명령의 점프 목적지를 변경하기 위해, 함수를 호출하는 사람이 ‘점프 목적지가 어딘지’, ‘돌아가는 명령이 있는 곳은 어디인지’를 상호간에 파악해둬야만 했다. 예를 들어, 함수 내용을 조금 바꿔서 돌아가는 명령의 위치가 뒤로 옮겨졌다고 치자. 그러면 그 함수를 호출하고 있는 코드를 전부 수정해야만 한다.
이후에 조금 더 개선된 방법이 발명되었다. 돌아갈 목적지를 기록해두는 전용 메모리를 만들어 ‘돌아갈 목적지 메모리에 적어둔 번지로 점프하는 명령’을 준비해두는 방법이다. 이것으로 호출처가 ‘돌아가는 명령의 위치’를 파악해둘 필요가 없어졌다. -중략- 그러나 이 방법에는 문제가 있다. 함수 X를 호출하고 있는 중에 다른 함수 Y를 호출하면, 돌아갈 목적지 메모리에 덮어 씌워져서 함수 X가 돌아갈 목적지를 잊어버리게 된다. 어떻게 하면 좋을까?
스택
그래서 등장하는 것이 스택(stack)이다. 스택은 복수의 값을 저장해두는 데이터 구조로, 마지막에 넣은 것을 가장 먼저 꺼내는 경우에 적합하다. [46~48p]


내포 구조를 다루는 방법
물리적인 물건을 만들 때는 ‘어떤 물품이 그 물품 자신을 사용해서 만들어졌다’고 생각하기 어렵기 때문에, 처음 프로그램을 접하고 재귀 호출을 만나게 되면 당황하는 사람도 많다. 여기에서는 내포된 처리를 예로 해서, 내포된 리스트의 수치를 전부 합산하는 문제를 생각해보도록 한다.
예를 들어 [1, 2, [3, 4], 5]라는 리스트를 생각해보자. 이것은 [1, 2, ?, 5]라는 리스트의 ? 부분에 [3, 4]라는 별도의 리스트가 내포되어 있다. 이 내포 리스트에 있는 숫자를 전부 합산하고 싶을 때는 어떻게 하면 될까?
다음의 Python코드에서는 for문으로 리스트에서 하나씩 꺼내, 그것이 정수이면 합산해가는 처리를 하고 있다. -중략- 우선 ‘1’과 ‘2’가 아노면 정수이기 때문에 result(결과)에 더한다. 여기까지의 합은 3이다. 여기까진 간단하지만 다음엔 정수가 아닌 [3, 4]가 나온다. 이것은 어떻게 하면 좋을까?
for로 구현할 수 없다.
‘이것은 리스트이기 때문에 for문을 써서 안에 있는 값을 별도 합산하면 되지 않나?’라고 생각하는 사람도 있을지 모르겠다. 이번 입력 데이터의 경우에 한해선, 그 방법도 잘 동작한다. 입력 데이터의 리스트가 가장 깊은 곳이 2중 내포이기 때문에 2중 for 문으로 처리할 수 있다. 하지만 3중 내포의 리스트가 입력되면 어떻게 될까? 두 번째 for문을 처리하고 있을 때 다시 리스트가 출현한다. 이 리스트는 어떻게 하면 좋을까?
여기에 추가로 for문을 써도 ‘3중 내포까지 처리 가능한 코드’ 밖에 되지 않는다. 만약 데이터가 4중, 5중 내포 관계가 되면 처리할 수 없다.
이런 ‘수많은 내포 관계로 된 데이터 구조’는 결코 놀라운 것이 아니다. 예를 들어, HTML에서는 태그가 수십 개의 내포 관계로 되어 있다. 이런 데이터 구조를 다루기 위해선 다중 내포 관계에서도 사용할 수 있는 코드여야 된다. for문을 몇 개의 내포 관계로 구성한다고 해도 해결할 수 없다.
재귀 호출을 사용
그래서 존재하는 것이 재귀 호출이다. [51~53p]


재귀함수는 재귀되는 함수 호출마다 개별적으로 xs나 result 등의 값을 기억하는 장소가 있다는 것과 두 번째 total 처리가 완료됐을 때 첫 번째 total 처리는 아직 계속 진행되고 있는 것 등은 자칫하면 놓치기 쉬운 포인트다. [56p]


만일 shippai 함수가 주로 실패하지 않는다면, 이 코드는 대부분의 경우 잘 동작할 것이다. 잘 동작하기 때문에 프로그래머는 ‘이 코드에 문제는 없어’라고 생각해버린다. 코드에 문제가 있는 것은 shuppai 함수가 실패하여 프로그래머의 예사오가 다른 움직임을 보일 때 발견하게 된다. 그것이 언제인지는 아무도 모른다. 제품을 출시한 이후일 수도 있다.
이런 경우에는 코드를 쓴 타이밍과 문제를 발견하는 타이밍이 어긋나기 때문에, 코드의 문제점을 찾기 위해 많은 고생을 하기 십상이다. 또한 shippai 함수가 실패함으로 어떤 값이 기대와 다른 상태가 되며, 그것을 계기로 다른 함수도 실패하는 연쇄 반응이 발생할 수 있다. shippai 함수와 관계가 없어 보이는 곳에서 문제를 발견할 수도 있다. [62p]


원래 하고 싶은 것은 ‘3개의 처리를 실행한다’였지만, 실제로 코드를 쓰고보면 ‘원래 하고 싶은 것을 기술한 코드’ 사이에 많은 ‘실패했을 경우의 코드’가 채워져 버려서 흐름을 읽기 어려워진다.
좀 더 읽기 쉽도록 할 방법은 없을까? 이 에러 처리가 동일한 처리라면, 한 곳에 정리해두고 싶다. 어떻게 하면 될까?
점프로 에러 처리를 정리한다. -중략- 코드 상에선ㄴ ‘실패했을 때의 처리’가 ‘원래 하고 싶은 것을 기술한 코드’와 분리되어 있다. [63~64p]


1964년까지, ‘실패했을 때 처리를 등록할 수 있다’, ‘새로운 실패 종류를 추가할 수 있다’, ‘실패를 발생시킬 수 있다’ 등 현대 예외 처리에서도 중요한 역할을 하고 있는 다양한 기능이 발명됐다는 것을 배웠다.
그러나 Java나 C++, Python 등에서 채용하고 있는 예외 처리 구문과 큰 차이점이 있다. PL/I 구문은 ‘미리 실패했을 때 처리를 등록해둔 후 실패할 것 같은 코드를 쓰는’ 형식이다. 하지만 Java 등의 구문에서는 ‘실패할 것 같은 코드를 미리 try{...}로 묶어둔 후 실패했을 때 처리를 쓰는’ 형식이다. 이 구문은 언제, 왜 만들어진 것일까?
John Good enough의 주장
1975년에 John Goodenough는 자신의 논문에서 보다 좋은 예외 처리 방법을 제안했다. 그 내용은 다음과 같다. “프로그래머는 ‘명령’이 예외를 던질 가능성이 있다는 것을 잊어버리고 최적이 아닌 장소 또는 최적이 아닌 종류의 예외 처리를 사용하는 등 실수를 할 가능성이 있다. 이 가능성을 줄이고 프로그래머가 한 실수를 컴파일러가 경고하도록 하기 위해서는 2 가지가 필요하다. 하나는 명령이 어떤 예외를 던질 가능성이 있는지를 명시적으로 선언하는 것이다. 다른 하나는 자발적으로 ‘실패할 것 같은 처리’를 묶는 구문이다.” [68p]
C 언어에서 SEH(Structured Exception Handling)를 사용하는 코드
__try{
__try{
/* 실패할 것 같은 코드 */
}
__finally{
/* 실패해도 성공해도 실행하고 싶은 코드 */
}
__except(.....){
/* 에러 처리 코드 */
}
[70p]


왜 finally를 도입한 것일까?
구조화 예외 처리를 채용함으로 코드의 신뢰성을 높일 수 있었다. 예를 들어, 프로그래머가 예측하지 못한 종료가 발생했을 시 메모리 블록이나 파일 등의 리소스를 잘 닫을 수 있게 된다. 또한 메모리 부족 등의 특정 문제에 대해서도 goto 구문이나 반환값 개념을 사용하지 않고 간단한 구조화 코드로 대응할 수 있다. [70~71p]


미술관을 예로 들어보자. 입구에서 빌린 음성 안내기를 나중에 전부 회수하려고 한다. 출구가 하나라면 제대로 회수할 수 있다. 사람들이 출구를 통해 나갈 때 회수하면 되기 때문이다. 하지만 출구가 여러 개 있다면, 출구마다 담당 직원을 두지 않으면 회수에 문제가 생길 수 있다. 만약 벽이 없어서 아무 곳으로 나갈 수 있다고 하면 회수는 더욱 어려워진다. [71p]


그러면 예외적인 상황이란 무엇일까?
함수 호출 시 인수가 부족한 경우
배열 범위 밖에 있는 것을 취득하려고 했을 때 [75~77p]


혼자서 만들고 있는 작은 규모의 프로그램의 경우에는 Python과 같이 바로 예외를 던지는 쪽이 JavaScript 같이 undefined 처리하는 것보다 좋다. -중략-
예외의 이점은 ‘실패를 놓치지 않는 것’이다. [78p]


Java에서는 ‘throw에서 던질 수 있는 것’, 즉 다른 수많은 언어들이 ‘예외’라고 부르고 있는 것을 다음과 같이 더 세분화하고 있다. ‘예외 처리를 하지 않아도 되는 중요한 문제‘와 ’예외 처리를 해도 좋은 실행 시 예외‘ 그리고 ’예외 처리를 해도 좋은 기타 예외‘가 그 3가지다. 그리고 이 ’기타 예외‘는 검사 예외라고도 불리며, 만약 메소드 밖으로 던지는 것이면 그것을 메소드 정의 시에 선언해줄 필요가 있다.
그것을 위해 준비되어 있는 것이 throw절이다. 다음 코드에는 void shippai() throws MyException이라고 쓰여있다. 이것은 ‘이 메소드는 MyException 예외를 던질 가능성이 있다’는 선언이다. [79~80p]


검사 예외는 매우 좋은 기능처럼 보인다. 하지만 다른 언어에서는 잘 채용되고 있지 않다. 왜 그런 걸까?
한 마디로 말하면, 귀찮기 때문이다. throws나 try/catch에 기술하는 예외 개수가 너무 방대해 지거나, 어떤 메소드에서 던질 예외를 하나 추가하면 그 메소드를 호출하고 있는 모든 메소드를 수정해야만 한다. [81p]


이 장에서는 프로그램도 실패하는 때가 있다는 것, 그리고 그 실패를 어떻게 전달할지에 대해 배웠다. 실패를 전달하는 방법은 크게 2 가지로 나눌 수 있다. ‘반환값으로 알린다’와 ‘실패하면 점프한다’이다. [83p]


책이나 자료 전체가 동일한 정도로 중요하다고 말할 수 없다. 목적이 명확하고, 목적 달성을 위해서 어디를 읽어야 할지 알고 있다면 다른 페이지는 신경 쓰지 말고 바로 그곳을 읽도록 한다.
전체 모두 읽지 않은 것이 께름칙한가? 하지만 좌절하고 전혀 읽지 않는 것보다는 낫다. ‘전부 읽지 않으면’이라는 완벽 주의가 배우고자 하는 동기를 짓누르고 있다면, 버려버리는 것이 낫다. 동기는 매우 중요하다. [84p]


7장. 이름과 스코프


변수명이 중복되는 것, 즉 이름이 충돌하는 것을 방지하기 위해선 어떻게 하면 좋을까?
긴 변수명을 사용한다.
스코프를 이용한다.
[89p]


어떻게 스코프를 표현할 수 있을까?
동적 스코프
어떤 방식으로 동작하는가
해결 방법 중 하나는 함수 입구에 원래의 값을 기록해두고 출구에서 원래의 값으로 되될리는 것이다. -중략-
문제점
동적 스코프는 조금 다루기 힘든 특징이 있는데, ‘변수를 변경한 후 다른 함수를 호출한 경우 호출된 함수에 영향을 미친다’는 것이다. [90~91p]


동적 스코프를 도입함으로, 함수 yobu 안에서 변수 x를 변경해도 전역 변수 x에는 영향을 주지 않게 되었다. 하지만 함수 yobu 안에서 일어난 변경은 거기서 호출하는 함수 yobareru에 영향을 미치게 되는 것이다.
동적 스코프에서는 변경값이 호출되는 곳에 파급되기 때문에, 변수값을 참조했을 때 어떤 값이 될지는 호출처의 코드를 보지 않고선 알 수 없다. 그렇다고 호출처까지 찾아가는 것은 귀찮은 일이다. -중략- 이 문제는 어떻게 해결할 수 있을까?
정적 스코프
중략-
함수가 호출될 때마다 새로운 대응표를 만들면 되지 않을까? 공유하고 있는 화이트 보드에 쓰는 대신, 한 명 한 명이 자신의 책상에 메모 용지를 가지고 있는 것이다. [92~93p]


동적 스코프로 지역 변수 x를 만든다는 것은 다음 3 가지 처리를 의미한다.
함수에 들어갔을 때 새로운 대응표를 준비한다.
함수내에서 변수 x에 대입하는 것은 대응표에 기록한다.
함수를 벗어날 때 그 대응표를 제거한다. [93p]


다수의 함수에서 대응표를 공유하고 있는 것이 동적 스코프의 문제점이다. 그러면 함수별로 대응표를 나눠보도록 하자. 즉 언어 처리계는 다음 3가지 처리를 한다.
함수에 들어갔을 때 함수 전용의 새로운 대응표를 준비한다.
함수내에서 변수 x에 대입하는 것은 대응표에 기록한다.
함수를 빠져나갈 때 해당 대응표를 제거한다. [95p]


한편 동적 스코프는 많이 사용되고 있지는 않지만, 아직까지 무용지물이 된 건 아니다. 정적 스코프에서는 스코프가 소스 코드 상에 한 묶음의 영역으로 존재하지만, 동적 스코프에서는 스코프에 들어가서 나갈 때까지 시간축 상에 존재하는 범위다. 이와 같은 특징을 가지고 있는 것이 몇 가지 있다. 예를 들어 ‘어떤 처리를 하고 있는 중간에 일시적으로 변수값을 바꾸고 나중에 되돌린다’는 처리를 하고 있다면 사전에 동적 스코프를 만들고 있는 것과 같다. 또한 예외 처리도 동적 스코프와 닮았다. 어떤 함수내에서 예외를 던질 때의 처리가 호출처에 있는 함수의 try/catch로 인해 영향을 받는다. [104p]


예를 들어 Java는 정적 스코프 언어이지만 클래스는 소스 코드 어디서든 참조할 수 있다. 즉, 클래스는 전역 스코프라고 할 수 있다. 클래스는 이름이 계층적으로 이루어져있고, 임포트하지 않으면 사용할 수 없어서 전역 변수가 가지고 있던 ‘충돌’ 문제를 피하고 있다. 그러나 전역 변수도 클래스의 static 멤버도 ‘소스 코드 어디서든지 바꿀 수 있다’는 점에서는 같다. 편리하게 사용할 수 있지만, 남용하면 이해하기 어려운 코드가 돼버린다. [104~105p]


8장 형


대부분의 언어에서 실수는 부동 소수점(IEEE 754)으로 표현된다. 대부분의 경우는 별 문제가 없다. 하지만 이 방법으로 ‘3 / 10’의 결과를 표현하려고 하면, 10진수로는 0.3을 사용해 표현할 수 있지만 2 진수의 경우 0.0100110011001100110011...로 무한 소수가 되어버린다. 이로 인해 0.3을 10회 더한 후 소수 자리를 버려버리면 2가 돼버리는 현상이 발생한다. -중략- 은행 등 돈을 다루는 분야에서는 이런 계산법이 용납되지 않기에, 고정 소수점수나 Excess-3과 같은 10진수 계산법이 사용되고 있다. [120p]


우선 ‘언어가 가지고 있는 기본적인 형을 조합해서 새로운 형을 만드는 기능’이 발명됐다. C언어의 구조체 등이 대표적인 예다. 사용자 정의형이라고도 불린다.
다음으로, 정수 등 ‘데이터’뿐만 아니라 함수 등 ‘데이터를 처리하는’ 기능도 형으로 정리되었다. C++ 설계자인 Bjarne Stroustrup은 ‘사용자가 정의할 수 있는 형’이야 말로 프로그램을 구축하기 위한 기본적 요소라고 생각했으며, 이런 형에 ‘클래서(class)’라는 이름을 붙였다. [126~127p]


‘형은 사양이다’라는 개념이 등장했다. 구조체나 클래스를 구성하는 형을 전부 공개하지 않고 최소한만을 공개한다는 것이다. 형이 맞는지 틀린지는 컴파일러가 체크해주기 때문에, 사양을 형으로 표현하면 사양과 일치하는지를 컴파일러가 체크해준다. 그래서 외부와 작업하는 부분만을 형으로 공개하고, 상세 구현 방법은 숨긴다는 발상이다. 이렇게 해서 형에 공개 부분과 비공개 부분이 생기게 되었다. C++이나 Java를 배운 사람이면 public이나 private등의 접근 권한에 대해 알고 있을 것이다. [127p]


다양한 형을 조합해서 만든 복잡한 형이 사용되면서 ‘일부만을 바꾸고 싶은데 전부 다시 정의해야 하는 것은 이상하다. 재사용하고 싶다!’는 필요가 생겨났다.
그래서 ‘구성 요소의 형을 일부만 바꾸는 형’, 즉 총칭형이 탄생했다. 다르게 설명하면 ‘형이 인수를 가지고 형을 만드는 함수’가 탄생한 것이다. C++의 템플릿(template), Java의 제네릭(generics), Haskell의 형 생성자 등이 그런 구조다. [128~129p]
*C++
#include<iostream>
template<typename T>
struct person {
int age;
char *name;
T something;
};
int main() {
person<int>x;
x.something = 1;
person<<const char*>y;
y.something = “hoge”;
std::cout<<x.something<<std::endl; // ->1
std;;cout<<y.something<<std::endl; // ->hoge
}


이 something의 형은 지금 정하지 않는다. template<typename T>.......; 이라고 정의해서 ‘T는 나중에 구체적인 형을 넣기 위한 형 인수’라고 선언하고, ‘something의 형은 나중에 정할 T라는 형’이라고 정의하고 있다. [129p]


당초의 형에는 값의 종류 정보만 기록했지만, 이후 다양한 정보를 저장하게 되었다. 예를 들어 해당 값에 대해 어떤 조작이 가능한지, 이 함수는 어떤 예외를 던지는지 등의 정보를 형에 넣을 수 있게 되었다.
현재는 정적 형결정과 동적 형결정과 같이 정보 저장 장소나 사용하는 타이밍이 다른 것까지 포함해서 ‘형’이라 부른다. 이 때문에 형이 무엇인지 더욱 이해하기 어렵게 되었다. 어떤 정보가 어디에 있고 어떤 타이밍에 사용되는지의 관점에서 보면 조금은 이해하기 쉬울 것이다. [138p]


소스 코드를 읽을 때는 우선 디렉토리 구조와 파일명을 본다. 그리고 파일을 속독으로 읽고 거기서 정의하고 있는 함수나 클래스 이름, 자주 호출되는 함수명 등을 본다.
이 방법들에는 ‘우선 대략적인 구조를 잡고, 조금씩 상세한 정보로 접근한다’는 공통점이 있다. 이것이 기본 원칙이다.
소스 코드에는 다른 방식의 독해 방법이 있다. 디버거(debugger)의 과정을 사용해서, 실행되는 순서나 호출 계층으로 읽는 방법이다. 이 경우도 동일하게 우선은 대략적인 처리 흐름을 따라가고, 조금씩 깊이를 더해서 함수안의 처리를 따라가는 것이 중요하다. [139p]


9장 컨테이너와 문자열


얼핏 보면, 연결 리스트가 사용하는 저장 방법은 메모리를 2배 사용하는 것처럼 보일 수 있다. 하지만 이것이 장점이 되기도 한다. 그 중 하나는 요소를 삽입하는 시간이 빠르다는 것이다. 예를 들어 A와 B 사이에 Z를 삽입하고 싶을 때는 어떻게 하면 될까?
배열에서는 ‘값을 순서대로 넣는’ 방법으로 저장한다. 배열에 요소를 삽입할 때는 삽입된 위치보다 뒤에 있는 요소를 전부 다른 위치로 옮겨야 한다.(복사한다) -중략-
반면, 연결리스트에서는 메모리에 순서대로 정렬해있을 필요가 없다. ‘다음 요소가 들어있는 위치’를 메모리 상에 넣어두기 때문이다. 연결 리스트 삽입에서는 우선 적당한 위치에 Z를 넣는다. 그림에서는 106에 들어있지만, 빈 공간이라면 어디든 상관없다. Z 다음 상자에는 ‘Z 다음 값이 어디에 있는지’의 정보를 넣는다. 다음 값 B는 102에 있기 때문에 102라고 넣는다. 그리고 마지막에 101번 상자에 쓰여있는 ‘A 다음 값은 어디에 있는지’의 정보를 변경한다. A 다음 값 Z는 106에 있기 때문에 106이라고 바꾼다.
연결 리스트에서는 두 개 상자에 새로운 값을 넣고 한 개 상자의 내용물을 바꿔주기만 하면 요소를 추가할 수 있다. [144~146p]


배열은 정수와 값을 대응시킨 것이었다. ‘10번째 값을 무엇인가’라고 물으면 ‘10번재 값은 3이다’라고 가르쳐준다.
반면, 사전은 문자열과 값을 대응시킨 것이다. ‘“age”의 값은 무엇인가?‘라고 물으면 ’“age”의 값은 31이다‘라고 가르쳐준다. 여기서 문자열을 ’키(key)’라고 부른다. 저자는 사전이라고 부르는 것이 ‘문자열과 값의 대응’을 제일 잘 표현하고 있다고 생각한다. [149p]


해쉬 테이블(Hash table)은 문자열을 인수로 받아서 정수를 반환하는 ‘해쉬 함수’를 사용해서 문자열과 값을 대응 관계를 표현하는 방법이다. 값을 넣기 위해서 우선 큰 배열을 준비한다. 그리고 해쉬 함수를 사용해서 문자열을 ‘적당한 정수’로 변환한 후 해당 배열 어디에 넣을지를 결정한다.
해쉬 함수를 사용해서 먼저 키를 정수 n으로 변환한다. 예를 들어 ‘Lee’는 434로 변환한다. 그리고 배열의 434번째 위치에 저장한다. [150p]


해쉬 테이블의 경우는 어떨까? 키에 대응하는 값을 꺼내기 위해선 ‘키를 해쉬 함수로 변환’, ‘배열의 해당 장소에 있는 값을 읽는’ 작업이 필요하다. 이 작업은 데이터량과 관계없다. 즉 O(1)이다.
이처럼 해쉬 테이블의 오더가 가장 작다. 이것이 많은 언어가 사전을 만들기 위해 해쉬 테이블을 ᅟ갓용하고 있는 이유다. 한편, 메모리 소비량으로 보면 ‘배열에 넣는’ 방법이 가장 작다. 해쉬 테이블은 값을 넣기 위해 큰 배열을 사용하고 있기 때문에 메모리 소비량이 매우 크다. [155p]


후반부에서는 ‘문자들이 들어있는 것’인 ‘문자열’에 대해 배웠다. 문자열에도 여러 가지 차이가 있다. 우선 ‘무엇이 문자인지’(문자 집합)의 차이, 다음으로 ‘어떻게 문자를 비트열로 표현하는지’(문자 부호화 방식)의 차이, 그리고 ‘어떤 정보를 어떤 메모리에 저장하는지’(문자열 구현)의 차이다. 많은 언어가 문자열을 지원하고 있지만, 서로 동일하다고 말할 순 없다. [170p]


10장 병행처리


하나의 실행 회로를 사용해서 복수의 처리를 실행하는 것을 사람에 비유하면, 하나의 일인용 게임기를 형제가 가지고 놀고 있는 상태라고 할 수 있다. 어느 쪽도 불만을 가지지 않는 정도에서 교대 간격을 정해서 논다면 일인용 게임기이지만 둘이서 가지고 놀 수 있다. 여기서 ‘언제 교대할 것인가’를 정하는 방법은 크게 2 가지로 나눌 수 있다.
협력적 멀티태스크
하나는 ‘타이밍이 좋은 시점에서 교대’하는 방법이다. 처리가 일단락되는 시점에 자발적으로 처리 교대를 하는 방법이다. 이 방법으로 구현된 멀티 태스크(multi-task, 병행 처리)를 협렵적 멀티태스크라고 한다.
이 방법에서는 어떤 처리가 ‘교대해도 좋아’라고 말하지 않고 계속 실행한면 다른 처리는 계속 기다려야 하는 문제점이 있다. 어디까지나 ‘모든 처리가 최적의 간격으로 교대한다’는 신뢰 관계를 기반으로 성립하는 시스템이다.
-중략-
선점적 멀티태스크 – 일정 시간에 교대한다
다른 한 가지 방법은 ‘일정 시간에 교대’하는 것이다. 이 방법에서는 개별프로그램과 입장이 다른 프로그램(태스크 스케줄러)이 존재한다. 이 프로그램이 일정 시간마다 지금 실행하고 있는 처리를 강제적으로 중단시켜서 다른 프로그램이 실행될 수 있도록 한다.
-중략-
선점적이란 ‘타인의 행동을 막기 위한’이란 의미다. 이 기법은 협력적 멀티태스크와 달리 ‘처리를 멈출 수 있는 프로그램의 협력’ 없이도 강제적으로 처리를 중지시킬 수 있는 것이 특징이다. [173~174p]


경합 상태는 어떤 겨웅에 발생하는 것일까? 평행해서 동작하고 있는 2가지 처리 간에 경합 상태가 발생하기 위해서는 다음 3가지 조건을 모두 만족해야 한다.
2가지 처리가 변수를 공유하고 있다.
적어도 하나의 처리가 그 변수를 변경한다.
한쪽 처리가 한 단락 마무리 되기 전에, 다른 한쪽의 처리가 끼어들 가능성이 있다.
역으로 말하면, 이 3가지 조건 중 하나라도 제거할 수 있다면 병행 실행 시에도 안정된 프로그램을 만들 수 있다. [175~176p]


UNIX 출시 후 약 10년 후 ‘경량 프로세스’가 만들어진다. 이것은 메모리를 공유하는 UNIX 이전 방식의 프로세스다. 메모리를 공유하지 않는 것은 너무 엄격한 구조였다. 그래서 이것이 나중에 ‘스레드’라고 불리게 되었다. [177p]


Java에서는 Mark Grand가 제안한 디자인 패턴의 하나인 Immutable 패턴이 자주 사용된다. 클래스에 private 필드를 만들어서 그것을 읽어내기 위해 getter 메소드는 만들지만, 변경하기 위한 setter 메소드는 만들지 않는다는 패턴이다. 변경하기 위한 방법이 마련되어 있지 않아서 ‘읽는 것은 가능하나 변경은 안 된다’는 상황을 구현할 수 있다. [179p]


락을 거는 방법에는 여러 가지 기법이 있어서, 락(Lock), 뮤텍스(Mutex), 세마포어(Semaphore)등 다양한 명칭이 있지만 핵심 개념은 ‘사용중’ 표식과 같다. ‘락’이라는 이름 때문에 ‘락을 걸어 두면 다른 사람이 들어올 수 없다’고 착각하기 쉽다. 하지만 실제는 ‘사용중’이라는 표식을 붙여둘 뿐, 표식을 확인하지 않고 실행하는 스레드가 있으면 아무 소용이 없다. 처리 흐름의 일부만을 협력적으로 양보하는 구조라고 볼 수 있다. [180p]


‘안에 들어갈 때 사용중이란 표식이 있는지 확인, 있으면 댁, 없으면 표식을 걸고 안으로 들어간다’는 일련의 처리를 바르게 구현하는 것은 쉽지 않은 일이다. 예를 들어 if문 등으로 만들면 ‘값을 확인한다’와 ‘0이면 1로 변환한다’ 사이에 별도 처리가 끼어들 가능성이 발생한다. 다른 처리가 끼어들지 못하게 하기 위해 기계어의 ‘값 확인과 변경을 한 번에 실행하는 명령’ 등을 사용할 필요가 생긴다. Java의 언어 처리계는 이 기능을 자체적으로 구현해서 채용했다. 이로 인해 Java 언어 사용자는 자신이 직접 고생하지 않고도 ‘synchronized 블록’으로 감싸기만 하면 손쉽게 락을 걸 수 있게 되었다. [180p]


‘락을 프로그래머가 신경 쓰지 않도록 하자’. 좋은 방법은 없는 것일까?
이 문제를 해결하려고 하는 것이 트랜잭션 메모리(Transactional memory)라는 접근법이다. 데이터베이스의 트랜잭션 기법을 메모리에 적용한 것이다. 개념은 ‘실험적으로 해보고, 실패하면 처음부터 다시 고쳐서 하고, 성공하면 변경을 공유한다’이다. X나 Y를 직접 변경하는 것이 아니라, 일시적으로 별도 버전을 만들어서 그것을 변경하고 하나의 묶음 처리가 끝나면 반영하는 것이 포인트다. [182p]


11장 객체와 클래스
객체 지향을 배우려고 한 사람이 제일 처음 만나는 개념은 ‘클래스(Class)’다. 클래스란 무엇일까? 이 질문도 일반적으로 대답해버리면 모순에 빠져버리는 위험한 질문이다. 언어마다 다른 의미로 사용되고 있기 때문이다. 적어도 C++에서는 ‘클래스는 사용자가 정의할 수 있는 형’이라고 할 수 있다. 하지만 8장에서 배운 것처럼, C++은 정적 형결정 언어이고 Ruby나 Python등은 동적 형결정 언어다. 즉 Ruby나 Python에서는 ‘형’이란 용어가 가리키는 것이 C++과 다르다. 당연히 클래스가 가리키는 것도 달라진다.
‘클래스가 필요한가?’라는 의문을 가진 사람이 많을 것이다. 클래스는 필요한 걸까? 대부분의 언어에서 프로그램을 만들 때 클래스는 필수 조건이 아니다. 단, Java는 예외다. Java는 ‘클래스라는 부품을 정의하고, 그것을 조립해나가는 것이 프로그래밍이다’라고 말하는 언어다. 때문에 Java에서는 클래스가 반드시 필요한 조건이다. [190p]


이와 같이 프로그램 안에는 함수나 변수가 대등한 관계로 여기 저기 흩어져있다. 어떤 변수도 어떤 함수도 어디에서든 동일하게 접근할 수 있다. 하지만 이해하기 쉽게 프로그램을 설계하면 몇 개의 ‘상호간의 연결성이 강한 그룹’이 생기는 경향이 있다. 요소 전부가 다른 요소와 동일하게 상호 작용하는 것보다 관련성이 강한 것을 ‘몇 개로 묶어서’ 나누는 것이 이해하기 쉽다.
1978년경에 개발된 Modula-2에서는 이와 같은 ‘관련성이 높은 함수나 변수의 묶음’을 명시하기 위해 ‘모듈(Module)’이라는 개념을 도입했다. 지금도 많은 언어가 이 기능을 계승하고 있다. Python이나 Ruby는 ‘모듈’이라하고, Java나 Perl에서는 ‘패키지’라는 이름으로 부른다. [192~193p]


함수나 모듈은 하나의 정의에 하나의 컴퓨터상의 실체가 대응하고 있다. 하지만 현실에서는 ‘비슷한 사물이 복수 개 있다’고 하는 상황이 존재한다. 어떻게 하면 이런 현실 세계 구조를 컴퓨터상의 모형으로 표현할 수 있을까? 즉, 한마디로 말하면 ‘복수 개의 인스턴스(instance)를 만들고 싶다’이다. [194p]


함수 x에 new를 붙여서 호출하면 다음 4가지 처리가 이루어진다.
새로운 객체를 만든다.
만들어진 객체 프로토타입을 함수 x 프로토타입으로 변경한다.
만들어진 객체를 this에 넣어서 함수 x 본체를 실행한다.
해당 객체를 반환한다. [205~206p]


클로저란?
클로저(Closure)도 많은 사람들이 이해하기 어려운 개념 중 하나다. 이것은 객체적인 것을 만들기 위한 기술이다.
많은 언어에서는 상태 정보를 가지고 있는 함수를 만들 수 있다. 예를 들어, 호출할 때마다 숫자가 1씩 증가하는 카운터 같은 함수를 만들 수 있다. JavaScript로 시험해보자.
JavaScript
function makeCounter(){
var count = 0;
function push() {
count++;
console.long(count);
}
return push;
}
c = makeCounter();
c(); // ->1
c(); // ->2
c(); // ->3
함수 makeCounter 안에 변수 count와 함수 push를 만들고, 함수 push를 반환값으로 반환하고 있다. 그리고 함수 makeCounter의 반환값을 c에 넣어두고, 그것을 3회 호출하고 있다. 호출할 때마다 출력값이 증가한다. 어떻게 이런 동작이 가능한 것일까? 함수 makeCounter는 우선 이름과 값 대응표를 만들어 변수 count 값을 0으로 한다. 그리고 함수 push를 정의하고 그것을 반환한다. 함수 push는 해당 함수가 정의된 때에 대응표를 물고서 밖으로 나온다. 그리고 호출할 때마다 ‘push가 정의된 때의 대응표’ 값이 1씩 증가한다.
클로저라는 특수한 구문이 있는 것은 아니다. 함수를 함수 안에 정의하고, 내포할 수 있는 정적 스코프가 있어서 함수를 반환값으로 사용하거나 변수에 대입하여 사용한다는 개념이다. 즉, 간단한 내포 구조를 사용함으로 상태 정보를 가진 함수를 만들 수 있는 것이다. [207~208p]


클래스는 분류였다. -중략- 현재 C++이나 Java 등에서 사용하는 ‘클래스’라는 개념에는 여러 가지 의미가 추가되어서 복잡하지만, 처음 시작은 ‘분류’였던 것이다. 현재 사용되고 있는 ‘클래스를 상속한다’, ‘클래스는 인스턴스다’라는 개념이 등장한 것은 보다 나중의 이야기다. [209p]


클래스 개념이 복잡하다고 생각하지 않는가? 그렇다. 클래스가 몇 가지 역할을 가지고 있기 때문이다.
결합체를 만드는 생성기
어떤 조작이 가능한지에 대한 사양
코드를 재사용하는 단위
[211p]


12장 상속을 통한 재사용


클래스의 가장 기본적인 의미는 분류다. 같은 분류는 같은 속성을 공통으로 가지고 있다. 분류를 보다 상세히 나눈다고 해도, 그 속성은 계속 남는다. [214p]


상속은 크게 3가지 측면으로 접근할 수 있다.
일반화/특수화
첫 번째는 ‘부모 클래스로 일반적인 기능을 구현하고, 자식 클래스로 목적에 특화된 기능을 구현한다’는 접근이다. 즉 ‘자식 클래스는 부모 클래스를 특수화한다’는 설계 방침이다. 특수화와 분류는 둘 다 세분화한다는 측면에서 의미가 가깝다. 즉, ‘클래스 = 분류’라는 접근과 일치한다. 이 경우 ‘자식 클래스는 부모 클래스의 일종입니까?’라고 물으면 그 대답은 ‘예’이다.
공통 부분을 추출
두 번째는 ‘복수 클래스의 공통부분을 부모 클래스로서 추출하면 좋다’는 접근법이다. 이것은 일반화/특수화와는 매우 다르다. 이 경우 ‘자식 클래스는 부모 클래스의 일종입니까?’라고 묻는다면 대답은 ‘아니오’가 된다. ‘공통부분을 추출한다’는 설계 방침은 함수 등에서 이미 익숙하게 사용되는 개념이다.
차분 구현
그리고 세 번째는 ‘상속 후 변경된 부분만을 구현하면 효율이 좋다’는 접근법이다. 상속을 재사용을 위해 사용함으로 구현이 편해질 수 있다는 발상이다. 확실히 기존 코드를 돌려쓰면 편해질 수 있는 겨웅가 많다. 이 경우 ‘자식 클래스는 부모 클래스의 일종입니까?’라고 묻는다면 대부분의 경우 대답은 ‘아니오’이다. [215~216p]


‘상속을 많이 사용하면 코드가 복잡해진다. 제어를 추가해야 한다’는 의견이 나오고 있다. 특히 세 번째의 ‘클래스를 상속해서 차분을 구현한다’는 코딩 방식은 깊은 상속 트리를 만들어내서 코드가 복잡해지는 경향이 강하다.
깊은 상속 트리는 어째서 프로그램을 이해하기 어렵게 만드는 것일까? 어떤 객체가 어떤 메소드 X를 가지고 있다고 하자. 이 메소드 X의 정의는 어디에 있을까? 이 클래스일까, 부모 클래스일까, 아니면 한 단계 더 위에 있는 부모 클래스일가? 상속 관계를 따라가면서 수많은 코드를 확인해야만 한다. 또한 어떤 메소드를 바꾸면 그 영향이 모든 자식 클래스까지 이르게 된다. 자식 클래스의 자식 클래스에게도 미친다. 영향 범위가 넓어질수록 해당 변경이 문제를 일으킬지 않을지 자신을 잃게 된다. [217p]


자식 클래스 S의 객체는 모두 부모 클래스 T의 객체라고 간주하여 문제가 발생하지 않도록 해야 한다는 의미다.
이 제약은 매우 엄격하다. 어떤 클래스를 상속하려고 할 때 정말 상속해도 되는 걸가? 해당 시점에서 고려하고 있는 속성이 리스코프의 치환 원칙이 성립하고 있다고 하자. 하지만 이후 프로그램을 만들어가면서 고려해야 할 속성이 늘어날 수도 있다. 속성이 증가하면 치환 원칙이 깨질 수도 있다. 설계 단계에서 확실히 속성을 목록화하고, 절대 치환 원칙이 깨지지 않는 경우에만 상속해야 하는 것일까? 아니면 개발 단계에서는 새로운 속성이 발견되면 상속을 멈추어야 하는것일까? 어느 쪽이든 매우 큰일이다. [219p]


한 사람이 프로그래머로서의 역할과 영업 사원으로서의 역할을 모두 가지는 경우는 충분히 있을 수 있는 일이다. 그러면 클래스가 복수 클래스의 자식이 되는 것이 자연스럽다. 즉, 현실 세계에서 하나의 사물이 복수의 분류에 해당하는 경우가 있기 때문에 그것을 모델로 하는 프로그래밍 언어가 복수의 클래스를 상속할 수 있어야 한다. 이것이 다중 상속이다. [220p]


Java는 클래스 다중 상속을 금지하기로 했다. 클래스 다중 상속을 인정하지 않으면 앞의 경우와 같은 문제가 발생하지 않는다. 다중 상속을 버림으로 문제를 해결했지만, 대신 다중 상속의 편리함 또한 버리게 된다.
이를 포함해서, Java에서는 ‘코딩 시 재상용을 목적으로 한 상속’을 피하려고 하는 움직임을 보이고 있다. -중략-
대신해서 발달한 것이 ‘위임(delegation)’개념이다. 사용하고 싶은 코드를 가지고 있는 클래스 객체를 만들고, 필요에 따라 해당 클래스에 처리를 맡기는 방법이다. 상속을 사용해서 형이나 이름 공간까지 함께 계승하는 것이 문제의 원인이기 때문에, 단순히 객체를 보유하기만 하면 문제를 막을 수 있다. [223p]


Java는 다중 상속을 금지하고 있다고 설명했지만, Java에도 다중 상속이 가능한 것이 있다. 바로 인터페이스(Interface)다.
인터페이스는 ‘코드를 가지고 있지 않는 클래스’다. ‘인터페이스를 상속한 클래스는 반드시 OO라는 이름의 메소드를 가지고 있다’라는 ‘사양’만 가지고 있다. 다중 상속 시 발생하는 문제는 ‘복수의 코드가 충돌하면 어느쪽을 선택하면 좋을지 모른다’는 것이었다. 인터페이스의 다중 상속으로 ‘가지고 있다’는 정보가 복수 개 있다고 해도, 그것은 ‘가지고 있다’는 사실 자체이기 때문에 아무 문제도 발생하지 않는다. 다음 코드에서는 같은 이름의 메소드를 가지고 있는 두 개의 인터페이스에서 상속하고 있지만 컴파일 에러가 발생하지 않는다. [225p]


Java는 사양만 다중 계승될 수 있도록 인터페이스를 도입했다. [226p]


어떤 클래스는 선조 클래스까지 도달하는 경로가 여러 개 있는 것이 문제다. 그렇다면 재사용하고 싶은 기능만을 모은 작은 클래스를 만들어서 해당 기능을 추가하고 싶은 클래스에 섞어 넣으면 된다. 이런 설계 방침이나 섞어 넣는 것, 그리고 섞기 위한 작은 클래스를 믹스-인(Mix-in)이라고 부른다. Mix-in이란 용어의 기원은 C++의 설계자인 Bjarne Stroustrup에 의하면 ‘MIT 근처에 있는 아이스크림 집에서 땅콩이나 건포도를 아이스크림에 섞어서 주는 것’에서 왔다고 한다.
-중략- 이 방법으로 마름모 상속이 없어질 뿐 아니라, 상속 트리의 깊이도 한 단계 줄어든다. [230~231p]


왜 다중 상속은 이런 문제를 겪고 있는 것일까? 2002년에 발표된 트레이트(trait)에 관한 논문에서는 문제점이 매우 잘 정리되어 있다.
클래스에는 2가지 상반되는 역할이 있다. 하나는 ‘인스턴스를 만들기 위한 것’이고, 이를 위해선 ‘필요한 모든 것을 가지고 완결된 형태의 큰 클래스’여야 할 필요가 있다. 두 번째는 ‘재사용 단위’라는 역할로, 이를 위해서는 ‘필요 없는 기능을 가지고 있지 않은 작은 클래스’여야 한다.
클래스가 ‘인스턴스를 만들기 위한 것’으로 사용될 때는 재사용 단위로 너무 크다. 그러면 재사용 단위라는 역할에 특화된 보다 작은 구조(트레이트 = 메소드 묶음)를 만드는 것이 좋다. 이것이 트레이트 개념이다. [233p]


명확히 ‘하고 싶은 것’, ‘조사하고 싶은 것’이 없이 ‘대충 읽으면’ 읽은 내용이 뇌를 그냥 스쳐 지나갈 뿐이다. 이런 상태에서 어떻게 배울까를 고민한다고 해도, 판단을 위한 지식 자체가 없기 때문에 무의미하다.
그래서 지식의 밑바탕을 만들기 위해서 교과서를 그대로 베껴 쓴다. 이것이 ‘베끼기’라 불리는 기술이다. 지식이 없는 상태에서 고민하는 것은 무익하기 때문에 우선 아무것도 생각하지 않고 지식을 복사하는 것이다.
이 이상의 방법은 없다. 저자는 시간을 정해서 ‘25분간 어디까지 베낄 수 있는지’ 도전하는 것을 좋아한다. 분량으로 나누는 것도 좋은 방법이다. 중요한 것은 간격을 적절히 해서 목표를 이루었다는 만족감을 얻을 수 있도록 하는 것이다. [237p]

keyword
매거진의 이전글패턴 랭귀지