실용주의 프로그래머
The Pragmatic Programmer
7장. 코딩하는 동안
While You Are Coding (p.273)
일단 코딩에 들어가면 대부분 기계적인 작업,
즉 설계 내용을 컴퓨터가 실행 할 수 있는 문장으로 바꾸는 일만 하면 된다고들 많이 생각한다.
이런 태도가 소프트웨어 프로젝트가 실패하는 가장 큰 원인으로 볼 수 있다.
코딩은 기계적인 작업이 아니다.
코딩할 때는 매 순간 결정을 내려야 하는데,
프로그램이 정확하게 생산적으로 작동하면서 천수를 누리도록 하려면
사려 깊은 생각과 판단으로 결정을 내려야 한다.
코딩도 대부분은 반복적인 일이지만 정신을 늘 기민하게 유지하면 재앙을 막을 수 있다.
Topic 37. 파충류의 뇌에 귀 기울이기 (p.275)
오직 인간만이 무언가를 직접 보고, 정확한 예측에 필요한 모든 정보를 획득하고,
심지어 순간적으로는 정확한 예측을 한 후에도, 그런데 그것이 아니라고 말할 수 있다.
- 개빈 드 베커(Gavin de Becker), <서늘한 신호(The Gift of Fear)>
프로그래머로서 경험이 늘어 갈수록 여러분의 뇌에는 암묵적인 지식이 켜켜이 쌓인다.
여러분은 자신의 뇌가 하는 일을 자각하지 못하더라도 뇌는 계속 저장을 하고 있는 것이다.
어디에서 왔는지에 상관업싱 모든 본능에는 공통점이 있다.
* 백지의 공포
첫번째 원인,
파충류의 뇌가 여러분에게 무언가 할 말이 있어서다.
인식의 지평 바로 밑에 도사리고 있는 모종의 의심이 있다. 이런 의심은 중요하다.
어떤 작업을 앞두고 마음속에 의심이 계속 남아 있거나 왠지 꺼림칙하다면,
여러분의 경험치 여러분에게 말을 거는 중일지도 모른다.
직감이 여러분의 역량에 일조하도록 하라.
두번째 다른 원인은,
여러분은 그저 실수할까 봐 두려운 것일 수 있다.
합리적인 두려움이다.
우리 개발자들은 코드에 많은 것을 투자한다. 그 때문이다.
* 자신과 싸우기
코딩이 어려운 날이 있다.
여러분의 코드가 무언가 말하려는 것이다.
지금 하려는 작업이 필요 이상으로 힘들다고 말이다.
이유가 무엇이든 코드가 보내는 피드백을 파충류의 뇌가 느끼고 있다.
그래서 여러분의 주의를 끌기 위해 필사적으로 노력하는 것이다.
* 파충류와 이야기하는 법
여러분의 본능, 여러분의 무의식, 파충류의 뇌에게 귀 기울이고 또 기울이기 바란다.
비법은 언제나 동일하다.
Tip 61. 여러분 내면의 파충류에게 귀 기울여라.
코드가 반발하는 것처럼 느껴진다면,
사실은 여러분의 무의식이 무엇인가 잘못되었다고 말하려는 것이다.
1. 하고 있는 일을 멈춰라.
2. 문제를 표면으로 끄집어내 보라.
위와 관련하여 여러 방법들을 시도해 보았는데도 여전히 막혀 있을 수도 있다.
여러분의 뇌에게 여러분이 하려는 일은 별 문제가 없다고 알려줘야 한다.
바로 프로토타이핑을 하면 된다.
* 놀이 시간이다!
이미 존재하는 코드 위에서 작업하고 있어서 기존 코드 때문에 문제 해결이 여의치 않다면,
기존 코드를 잠시 다른 곳으로 밀어 두고 비슷한 것을 대신 프로토타이핑으로 만들어라.
1. 포스트잇에 "프로토타이핑 중" 이라고 써서 모니터 옆에 붙여라.
2. 프로토타이핑은 원래 실패한다고, 실패하지 않더라고 버리는 것이라는 점도 함께 상기시켜라. 프로토타이핑으로 손해 볼 일은 없다.
3. 텅 빈 에디터 화면에 여러분이 배우고 싶은 것 혹은 하고 싶은 것을 한 문장의 주석으로 표현해 보라.
4. 코딩을 시작하라.
포스틑잇을 보고, 명확한 문제로 구체화되면 즉각 해결하라.
마친 후 여전히 불안한 마음이 들면 다시 처음부터 시작하라.
첫 단계는 산책과 수다, 그리고 휴식이다.
단계를 반복하다 보면 무엇을 해야 하는지 알게 될 것이다.
모든 프로토타입 코드를 지우고, 포스트잇도 떼고, 비워진 에디터 창에 멋지고 빛나느 새 코드를 채워 넣어라.
* '여러분'의 코드뿐이 아니다
다른 사람의 코드를 실험하고 정리 작업 하면서 패턴을 찾아보라.
그런 식으로 코드를 작성해야만 했던 원인을 찾아낼 수 있다면 코드를 이해하는 일이 훨씬 더 쉬워질지도 모른다.
다른 사람들이 은연중에 적용한 패턴을 여러분은 의식적으로 적용할 수도 있다.
그 과정에서 여러분이 새로운 것을 배울 수도 있다.
* 코드뿐이 아니다
본능에 귀를 기울이고 문제가 여러분 앞에 튀어나오기 전에 미리 대처하라.
Topic 38. 우연에 맡기는 프로그래밍 (p.282)
우리는 우연에 맡기는 프로그래밍, 곧 행운과 우연한 성공에 의존하는 프로그래밍을 하지 않아야 한다.
대신 '의도적으로 프로그래밍'해야 한다.
* 우연에 맡기는 프로그래밍 하기
애초에 코드가 왜 잘 돌아가는지도 모른다면,
왜 코드가 망가졌는지도 모를 것이다.
우리도 우연에 맡길 때가 있다.
우연한 행운과 주도면밀한 계획을 착각하기 쉬운 경우도 종종 있다.
1. 구현에서 생기는 우연
단순히 코드가 지금 작성된 방식이 그렇기 때문에 생기는 우연한 일들이 있다.
이런 우연에 기대다 보면 결국 문서화되지 않은 에러나 예외적인 경우의 동작에 의존하게 되고 만다.
예시) 어떤 루틴을 잘못된 데이터를 가지고 호출 !
> 예상 못한 데이터에 특정한 방식으로 반응
> 그 반응을 기반으로 코드 작성
>> 하지만! 그 루틴을 만든 사람의 의도는 그 루틴이 그런 식으로 작동하는 것이 아니었다..
가장 극단적인 경우는 여러분이 호출한 루틴이 실제로는 그렇게 설계된 루틴이 아닌데도
여러분이 원하는 효과를 내는 것처럼 보일 수도 있다.
잘못된 순서로 호출하거나, 잘못된 맥락에서 호출하는 것도 이와 관련된 문제다.
"이제 돌아는 가니까, 그대로 놔두는 편이 더 나을 거야......"
이런 함정에 빠지기 쉽다.
잘 작동하는데 더 괜히 건드려서 일을 만들 필요가 있을까?
우리가 보기에는 그래야 할 이유가 몇 가지 있다.
- 정말로 제대로 돌아가는 게 아닐지도 모른다. 그저 그렇게 보이는 것일수도..
- 여러분이 의존하는 조건이 단지 우연인 경우도 있다. 화면 해상도, CPU 코어 수 등등
- 문서화되지 않은 동작은 라이브러리의 다음 릴리스에서 변경될 수도 있다.
- 불필요한 추가 호출은 코드를 더 느리게 만든다.
- 추가로 호출한 루틴에 새로운 버그가 생길 수도 있다.
다른 루틴을 호출할 때도 문서화된 동작에만 의존하라.
어떤 이유로든 그럴 수 없다면 추측을 문서로 상세히 남겨라.
2. 비슷하다고 괜찮을 리는 없다
특정한 경우에만 '딱' 특정한 현상이 발생하는 것은 그저 우연이다.
사실은 더 깊고 근본적인 문제가 있었을 것이다.
3. 유령 패턴
그저 우연에 불과한 것들 속에서, 인간은 언제나 패턴과 인과 관계를 찾으려고 한다.
테스트가 여러분의 장비에서는 통과했던 것 같은데 서버에서는 통과하지 못하는 이유는
두 환경의 차이 때문일 수도 있지만 어쩌면 그저 우연일 수도 있다.
가정하지 말라. 증명하라.
4. 상황에서 생기는 우연
잘 되는 듯한 답을 찾는 것과 올바른 답을 찾는 것은 다르다.
Tip 62. 우연에 맡기는 프로그래밍을 하지 말라.
믿을 만한 것만 믿어야 한다.
우발적인 복잡성에 주의하고,
우연한 행운을 목적을 가지고 세운 계획으로 착각하지 말라.
6. 암묵적인 가정
우연은 여러 단계에서 우리를 오도할 수 있다.
요구 사항을 만들어내는 단계부터 테스팅에 이르기까지 이 모든 단계에서 말이다.
가정하지 말라. 증명하라.
확고한 사실에 근거하지 않은 가정은
어떤 프로젝트에서든 재앙의 근원이 된다.
* 의도적으로 프로그래밍하기
우리는 코드를 마구 찍어 내는 데에 드는 시간을 줄이고 싶고,
또 가능한 한 개발 주기 초기에 오류를 잡아서 고치고 싶고,
애초부터 오류를 더 적게 만들고 싶어 한다.
> 의도적으로 프로그래밍해야 한다!
- 언제나 여러분이 지금 무엇을 하고 있는지 알아야 한다.
- 더 경험이 적은 프로그래머에게 코드를 상세히 설명할 수 있는가? 그렇지 않다면 아마 우연에 기대고 있는 것일 터..
- 자신도 잘 모르는 코드를 만들지 말라.
- 계획을 세우고 그것을 바탕으로 진행하라.
- 신뢰할 수 있는 것에만 기대라.
- 가정을 기록으로 남겨라.
- 코드뿐 아니라 여러분이 세운 가정도 테스트해 보아야 한다. 어떤 일이든 추측만 하지 말고 실제로 시험해 보라.
- 노력을 기울일 대상의 우선순위를 정하라.
- 과거의 노예가 되지 말라. 기존 코드가 앞으로 짤 코드를 지배하도록 놓아 두지 말라.
언제나 리팩터링할 자세가 되어 있어야 한다.
그러므로 앞으로 어떤 것이 잘 돌아가는 듯이 보이기는 하는데
여러분이 그 이유를 모를 경우
그것이 우연은 아닌지 반드시 확인하라.
Topic 39. 알고리즘의 속도 (p.291)
실용주의 프로그래머가 거의 날마다 하는 또 다른 종류의 추정이 있다.
바로 알고리즘이 사용하는 자원, 곧 시간, 프로세서, 메모리 등을 추정하는 것이다.
상식과 약간의 분석, 그리고 '대문자 O 표기법(Big-O notation)'이라고 부르는
근삿값을 기록하는 방식을 알아보자.
* 알고리즘을 추정한다는 말의 의미
일반적으로 입력의 크기는 알고리즘에 영향을 준다.
입력의 크기가 클수록 알고리즘의 수행 시간이 길어지거나 사용하는 메모리 양이 늘어난다.
우리는 반복문이나 재구 호출을 담고 있는 코드를 작성할 때면
언제나 무의식적으로 수행 시간과 필요한 메모리 양을 계산한다.
생각보다 훨씬 상세한 분석을 해야 하는 경우도 종종 실제로 있다.
이럴 때 대문자 O 표기법이 유용하다.
* 대문자 O 표기법
대문자 O 표기법은 근삿값을 다루는 수학적 방법으로
O()
와 같이 표기한다.
어떤 정렬 루틴이 원소 n개를 정렬하는 데 O(n²) 시간이 걸린다고 말할 때,
이는 그저 최악의 경우에 걸리는 시간이 n의 제곱에 비례하여 늘어난다고 얘기하는 것이다.
O가 '... 차수로(order of)'를 뜻한다고 생각하면 된다.
O() 표기법은 우리가 측정하는 값-시간, 메모리 등-의 상한을 기술하는 표기법이다.
n이 커질수록 가장 큰 차수에 비하면 다른 차수는 무시해도 될 정도이기 때문에,
관습적으로 최상위 차수를 제외한 다른 모든 차수는 제거하며,
상수인 계수도 표기하지 않는다.
O(n²/2) 는 O(n²)과도 같다.
대문자 O 표기법은 수행 시간이든 메모리든, 아니면 다른 무엇을 나타내든 실제 숫자를 알려주지 않는다.
그저 입력의 크기가 바뀜에 따라 이 값이 어떻게 바뀔지를 알려줄 뿐이다.
* 상식으로 추정하기
상식을 이용해서 간단한 알고리즘들의 차수를 대부분 추정할 수 있다.
단순 반복문
O(n) - 소진 탐색(exhaustive search), 배열에서 최댓값 찾기, 체크섬 생성하기 등
중첩 반복문
O(m x n) or O(n²) - 버블 정렬
반씩 자르기
반복문을 돌 때마다 작업 대상의 수를 반으로 줄여 나가는 알고리즘
O(log n) - 정렬된 목록의 이진 검색이나 이진 트리의 탐색, 정수의 2진수 표현에서 첫 번째 1인 비트를 찾는 문제 등
분할 정복(divide and conquer)
입력 데이터를 둘로 나눠서 각각 독립적으로 작업한 다음, 결과를 합치는 알고리즘
O(n log n) - 퀵 정렬(quicksort)
조합적(combinatoric)
알고리즘이 항목의 순열(permutation)을 다루기 시작하면 대부분의 경우 수행 시간은 걷잡을 수 없이 늘어난다.
순열에는 계승(factorial)이 따라오기 때문이다.
'난해(hard)'하다고 분류되는 문제를 푸는 알고리즘이 대개 여기에 속한다.
예시) 여행하는 외판원 문제(traveling salesman problem),
상자에 물건을 최적으로 집어넣는 문제(bin packing problem),
숫자 집합을 분할해서 각 부분 집합의 원소 합을 모두 같게 만드는 문제(partition problem) 등
종종 한정된 문제 도메인에서 이런 알고리즘의 수행 시간을 줄이기 위해 휴리스틱을 동원하기도 한다.
* 실전에서의 알고리즘 속도
들어올 수 있는 최댓값이 정해져 있다면 그 코드를 실행하는 데 시간이 얼마나 걸릴지 알 수 있다.
숫자가 외부 요인에 따라 달라진다면 잠시 작업을 멈추고 커다란 수가 들어왔을 경우
수행 시간이나 메모리 소모에 어떤 영향을 미칠지 생각해 보는 것이 좋다
Tip 63. 사용하는 알고리즘의 차수를 추정하라.
코드를 작성하기 전에 실행 시간이 얼마나 걸릴지 대략이라도 감을 잡아라.
코드의 실행 시간이 얼마나 될지 또는 메모리를 얼마나 사용할지 확실하지 않다면 직접 실행해 보라.
어떤 일을 하는 코드인지 코드 자체에 대해서도 생각해 보라.
이론적 요인과 실무적 요인을 모두 고려하려고 노력하라.
Tip 64. 여러분의 추정을 테스트하라.
알고리즘의 수학적 분석이 모든 것을 다 알려주지는 않는다.
실제 환경에서 코드의 수행 시간을 측정해 보라.
정확하게 시간을 재는 일이 어렵다면 '코드 프로파일러(code profiler)'를 사용하여
알고리즘이 돌아갈 때 각 단계의 실행 횟수를 센 다음 입력값 크기별 실행 횟수를 그래프로 그려 보라.
최고라고 언제나 최고는 아니다
적당한 알고리즘을 선택할 때도 실용적이어야 할 필요가 있다.
가장 빠른 알고리즘이 언제나 가장 좋은 알고리즘은 아니다.
'성급한 최적화(premature optimization)'를 조심하라.
언제나 어떤 알고리즘을 개선하느라 여러분의 귀중한 시간을 투자하기 전에
그 알고리즘이 정말로 병목인지 먼저 확인하는 것이 좋다.
Topic 40. 리팩터링 (p.300)
이 천지 만물 모두 변하나 ......
- H. F. 라이트(H.F. Lyte), <Abide with me(함께 하소서)>
코드는 정적인 존재가 아니다.
코드는 발전해야 한다.
소프트웨어 개발은 건축보다 정원 가꾸기에 더 가깝다.
딱딱하기보다는 유기적인 활동이다.
소프트웨어 개발의 현실은 정원 가꾸기 메타포에 훨씬 더 가깝다.
어떤 루틴이 너무 크게 자라거나 너무 많은 것을 하려고 할지도 모른다.
계획한 대로 잘 되지 않는 것들은 잡초 제거하듯 뽑아내거나 가지치기를 해야 한다.
코드 고쳐쓰기, 다시 작업하기, 다시 아키텍처 만들기는 모두 아울러서 '재구성(restructuring)'이라고 부른다.
그런 활동 중 일부를 따로 떼어 '리팩터링(refactoring)'이라는 이름으로 실천하기도 한다.
<리팩터링> 에서 마틴 파울러는 '리팩터링'을 다음과 같이 정의한다.
밖으로 드러나는 동작은 그대로 유지한 채 내부 구조를 변경함으로써
이미 존재하는 코드를 재구성하는 체계적 기법.
여기서 핵심 두 가지는 아래와 같다.
1. 이 활동은 체계적이다. 아무렇게나 하는 것이 아니다.
2. 밖으로 드러나는 동작은 바뀌지 않는다. 기능을 추가하는 작업이 아니다.
무질서하게 대규모로 코드를 다시 쓰는 것이 아니라, 정확한 목적을 가지고 정밀하게 접근하는 활동이다.
그래서 코드를 바꾸기 쉽게 유지하는 것이다.
밖으로 드러나는 동작이 바뀌지 않는다는 것을 보장하려면
코드의 동작을 검증하는 좋은 자동화된 단위 테스트가 필요하다.
* 리팩터링은 언제 하는가?
리팩터링은 여러분이 무언가를 알게 되었을 때 한다.
주저하지 말고 변경하라.
언제나 바로 지금이 최적기다.
코드를 리팩터링할 이유는 아주 많다.
중복
DRY 원칙 위반을 발견했다.
직교적이지 않은 설계
더 직교적으로 바꿀 수 있는 무언가를 발견했다.
더 이상 유효하지 않은 지식
코드는 지금 상황에 뒤떨어지지 않아야 한다.
사용 사례
진짜 사람들이 실제 상황에서 시스템을 사용하게 되면,
여러분은 어떤 기능은 예전에 생각했던 것보다 더 중요하고,
"꼭 필요하다"고 생각했던 기능은 그렇지 않은 경우도 있다는 것을 깨닫게 될 것이다.
성능
성능을 개선하려면 시스템의 한 영역에서 다른 영역으로 기능을 옮겨야 한다.
테스트통과
코드를 조금 추가한 후 추가한 테스트가 통과했을 때가,
방금 추가한 코드로 다시 뛰어들어 깔끔하게 정리하기에 최고의 타이밍이다.
여러분의 코드를 리팩터링하는 것 - 기능을 이리저리 옮기고 이전에 내린 결정을 바꾸는 것 - 은
사실 '고통 관리(pain management)'를 실천하는 것이다.
많은 개발자들이 코드에 조금 개선할 부분이 있다는 이유만으로는 다시 돌아가서 코드 열기를 주저한다.
현실 세계의 복잡한 문제들
일정의 압박은 리팩터링을 하지 않는 단골 핑계다.
지금 하지 않으면 일이 더 진척 되었을 때 훨씬 더 많은 시간을 투자하게 될 것이다.
그때가 되면 일정에 더 여유가 생길까? ... 그럴리가!
Tip 65. 일찍 리팩터링하고, 자주 리팩터링하라.
정원의 잡초를 뽑고 식물 배치를 바꾸는 것과 같이
코드도 필요할 때마다 다시 작성하고, 다시 작업하고, 아키텍처를 다시 만들라.
근본적인 문제를 해결하라.
일정에 리팩터링할 시간을 확실히 포함시켜 두도록 하라.
그 코드를 사용하는 사람들이 코드가 조만간 재작성될 것이라는 사실과
재작성이 그들의 코드에 미칠 영향을 인지하도록 해야 한다.
* 리팩터링은 어떻게 하는가?
리팩터링의 본질은 재설계다.
리팩터링은 천천히, 신중하게, 조심스럽게 진행해야 하는 작업이다.
1. 리팩터링과 기능 추가를 동시에 하지 말라.
2. 리팩터링을 시작하기 전 든든한 테스트가 있는지 먼저 확인하라.
3. 단계를 작게 나누어 신중하게 작업하라.
단계를 작게 나누고, 한 단계가 끝날 때마다 테스트를 돌린다면 기난긴 디버깅 작업을 피할 수 있다.
탄탄한 회귀 테스트를 유지하는 것이야말로 안전한 리팩터링의 비결이라는 것이다.
다음에 여러분이 기대하는 수준에 못 미치는 코드를 발견하면, 고쳐라.
고통을 관리하라.
지금은 고통스러울지라도 앞으로 더욱 고통스러워질 것 같으면 지금 고치는 편이 낫다.
깨진 창문을 그대로 놓아두지 말라.
Topic 41. 테스트로 코딩하기 (p.307)
Tip 66. 테스트는 버그를 찾기 위한 것이 아니다.
테스트는 여러분의 코드를 보는 한 관점이다.
코드의 설계, API, 결합에 대한 피드백을 준다.
우리는 테스트의 주요한 이득이 테스트를 실행할 때가 아니라
테스트에 대해 생각하고, 테스트를 작성할 때 생긴다고 믿는다.
* 테스트에 대해 생각하기
특정한 데이터베이스 조회 코드를 작성해야 한다고 생각해보자.
테스트 데이터는 어디서 가져다 사용해야 할까? 데이터베이스에서 연결한다.
테스트 데이터를 어떻게 채워야 할까? 데이터베이스 스키마를 확인하여 필요한 테이블/필드를 찾는다.
테스트에 대해 생각하는 것으로 시작했는데 코드 한 줄 쓰지 않고도 두 가지 발견을 했다.
* 테스트가 코딩을 주도한다.
이전 예에서 테스트에 대해 생각함으로써
우리 코드의 결합도는 낮추고 (전역 DB 연결을 쓰지 않고 DB 연결을 넘겨줌)
유연성은 올릴 수 있었다. (테스트하는 필드의 이름을 매개 변수로 지정)
우리 메서드의 테스트 작성에 대해 생각함으로써 코드의 작성자가 아니라
사용자인 것처럼 메서드를 외부의 시선으로 보게 되었다.
Tip 67. 테스트가 코드의 첫 번째 사용자다.
테스트가 주는 피드백으로 코드의 방향을 잡아라.
테스트는 우리의 코딩을 인도하는 필수 피드백이다.
다른 코드와 긴밀하게 결합된 함수나 메서드는 테스트하기 힘들다.
무언가를 테스트하려면 그것을 이해해야만 한다.
코드에 테스트의 빛을 비추면 모든 것이 명확해진다.
코딩을 시작하기 전에 경계 조건의 테스트와 경계 조건에서 어떻게 동작해야 하는지를 먼저 생각해 본다면,
아마 함수를 단순하게 만드는 코드 패턴을 찾을 수 있을 것이다.
테스트해야 하는 오류 조건에 대해 생각해 본다면 그에 맞게 함수 구조를 잡을 것이다.
테스트 주도 개발
테스트 주도 개발(test-driven development, TDD)
테스트 우선 개발(test-first development)
TDD의 기본 주기는 다음과 같다.
1. 추가하고 싶은 작은 기능 하나를 결정한다.
2. 그 기능이 구현되었을 때 통과하게 될 테스트를 하나 작성한다.
3. 테스트를 실행한다.
4. 실패하는 테스트를 통과시킬 수 있는 최소한의 코드만 작성한다. 그리고 확인한다.
5. 코드를 리팩터링한다. 개선점을 찾고 개선 후에도 테스트&수정을 반복 확인한다.
어떻게든 TDD를 실천하라.
하지만 도중에 이따금 멈추어 큰 그림을 살피는 것을 잊지 말라.
* TDD: 목표가 어디인지 알아야 한다.
전체 문제를 완전히 파악하기 힘들 때 한 번에 테스트 하나씩 작은 단계들을 밟아라.
Tip 68. 상향식이나 하향식이 아니라 끝에서 끝까지(end-to-end) 만들어라!
끝에서 끝을 잇는 조그만 기능 조각들을 만들고, 그 과정에서 문제에 대하여 배워라.
코드를 채워 나가면서 배운 것을 적용하고, 각 단계마다 고객을 참여시켜서 전체 과정을 안내하도록 하라.
* 다시 코드로
소프트웨어를 만들 때 맨 처음부터 테스트가 가능하도록 만들고,
코드들을 서로 연결하기 전에 코드를 하나하나 철저하게 테스트해야만 한다.
* 단위 테스트(unit test)
모듈을 통제된 (심지어는 인위적으로 만들어진) 환경에서 철저하게 테스트하고 나면,
넓은 바깥세상에서 그 모듈이 어떻게 행동할지 더 잘 알게 될 것이다.
소프트웨어 단위 테스트랑 어떤 모듈에게 이것저것을 시켜보는 코드를 가리킨다.
동일한 테스트를 코드 수정 후 다시 돌려보는 것을 회귀 테스트(regression testing)라고 한다.
* 계약을 지키는지 테스트하기
우리는 단위 테스트를 계약을 잘 지키는지 보는 테스트라고 여긴다.
1. 계약을 지키는지 여부 확인
2. 코드로 표현된 계약의 의미가 우리가 생각한 것과 일치하는지 여부 확인
다양한 종류의 테스트 케이스와 경계 조건에서도 모듈이 약속한 대로 기능을 잘 수행하는지 테스트 해보자.
계약을 잘 지키는지 확인하는 테스트를 강조함으로써 프로젝트에서 이후에 벌어질지 모를 재앙을 피하려고 노력하자.
Tip 69. 테스트할 수 있도록 설계하라.
코드를 쓰기 전 테스트에 대해 먼저 생각하라.
* 임시 테스트
'임시(Ad-hoc) 테스트'는 우리가 직접 코드를 이리저리 찔러보는 것이다.
디버깅 작업이 끝나면 이런 임시 테스트를 정식 테스트의 형태로 만들어 두어야 한다.
여러분이 만든 테스트를 그냥 버리지 말고 기존의 단위 테스트 군단에 합류시켜라.
* 테스트 접점 만들기
아무리 테스트를 잘 갖추었어도 모든 버그를 발견할 수는 없다.
규칙적이고 일관된 형식의 로그를 확인 가능한 모듈을 사용하거나,
상태 정보와 그 외의 것들이 들어 있는 진단 제어 도구를 사용하자.
* 테스트 문화
여러분이 작성하는 모든 소프트웨어는 언젠가 테스트된다.
여러분이나 여러분의 팀이 테스트하지 않으면 결과적으로 사용자들이 테스트하게 된다.
소프트웨어를 철저하게 테스트할 계획을 세우는 것이 좋다.
때에 따라 테스트를 먼저 쓰기가 어렵거나 의미가 없을 수도 있다.
코드를 조금 작성하고, 테스트를 작성하고 다시 코드로 넘어가라.
제대로 된 테스트 문화를 가졌다면 모든 테스트가 언제나 통과해야 한다.
불량 테스트를 무시하다 보면 모든 테스트를 무시하게 되기 쉽고 악순환의 고리가 시작된다.
테스트 코드를 다른 제품 코드와 마찬가지로 다뤄라.
결합도를 낮추고, 깨끗하고 견고하게 유지하라.
Tip 70. 여러분의 소프트웨어를 테스트하라. 그러지 않으면 사용자가 테스트하게 된다.
가차 없이 테스트하라. 여러분 대신 사용자가 버그를 찾도록 하지 말라.
Topic 42. 속성 기반 테스트 (p.321)
믿으라, 하지만 확인하라
- 러시아 속담
함수를 작성할 때 단위 테스트도 작성하기를 추천한다.
코드를 작성하고 직접 테스트를 작성한다면 잘못된 가정이 둘 다에 들어갈 수도 있지 않을까?
자기 자신의 생각에 비추어 보면 제대로 동작하므로 코드는 테스트를 통과한다.
>> 대안은 컴퓨터에게 테스트를 맡기는 것이다. 컴퓨터는 여러분과 달리 선입견이 없다.
* 계약, 불변식, 속성
'불변식(invariant)' - 함수 실행 전후로 계속 어떤 부분의 상태에 대하여 참이 되는 조건을 말한다.
예시) 리스트를 정렬했을 때의 결과는 정렬하기 전 리스트와 원소의 수가 같을 것이다.
그렇다면 리스트의 길이가 불변식에 해당한다.
코드에 존재하는 계약과 불변식을 뭉뚱그려서 '속성(porperty)'이라고 부른다.
코드에서 속성을 찾아내서 테스트 자동화에 사용할 수 있는데,
이것을 '속성 기반 테스트(property-based testing)'라 한다.
Tip 71. 속성 기반 테스트로 가정을 검증하라.
속성 기반 테스트는 여러분이 절대 시도해 보지 않을 만한 것들을 시도하고,
여러분이 의도하지 않은 방식으로 여러분의 코드를 사용할 것이다.
* 테스트 데이터 생성
예시) Hypothesis 의 hypothesis.strategies 모듈의 함수들
@given(some.integers())
> 여러 번 실행되면서 매번 다른 정수(integer)를 넘길 것이다.
@given(some.integers(min_value=5, max_value=10).map(lambda x: x * 2))
> 10에서 20 사이의 짝수가 생성될 것이다.
@given(some.lists(some.integers(min_value=1), max_size=100))
> 길이가 최대 100인 자연수 리스트가 만들어질 것이다.
* 잘못된 가정 찾기
(책 예제 참고)
* 속성 기반 테스트는 우리를 자주 놀래킨다
속성 기반 테스트가 강력한 까닭은 그저 입력을 생성하는 규칙과 출력을 검증하는 단정문만 설정한 채 제멋대로 작동하도록 놔두기 때문이다.
정확히 어떤 일이 일어날지 절대 알 수 없다.
속성 기반 테스트가 실패했다면 테스트 함수가 어떤 매개 변수를 사용했는지 알아낸 다음 그 값을 이용하여 별도의 단위 테스트를 정식으로 추가하는 것이 좋다.
이 단위 테스트의 두 가지 역할.
1. 속성 기반 테스트의 여러 가지 다른 수행 결과와 상관없이 문제가 발생하는 상황에 집중할 수 있게 해 준다.
2. 단위 테스트가 '회귀 테스트(regression test)' 역할을 한다.
속성 기반 테스트는 임의의 값을 생성하여 사용하기 때문에 다음번에 실행했을 때 똑같은 값을 테스트 함수에 넘긴다는 보장이 없다. 위와 같이 단위 테스트를 만들어 두면 버그가 완전히 해결되었음을 보장할 수 있다.
* 속성 기반 테스트는 설계에도 도움을 준다.
속성 기반 테스트는 여러분이 코드를 불변식과 계약이라는 관점으로 바라보게 한다.
여러분은 무엇이 변하지 않아야 하고, 어떤 조건을 만족해야 하는지 생각하게 된다.
Topic 43. 바깥에서는 안전에 주의하라 (p.331)
좋은 울타리가 좋은 이웃을 만든다.
- 로버트 프로스트(Robert Frost), <담장 고치기>
우리는 지나칠 정도로 의심을 해야 한다. 매일.
근래 발생하는 보안 관련 사건, 사고들은 모두 개발자가 부주의한 탓이다.
* 나머지 90%
잘 돌아가는 코드를 완성 했다고 생각해도, 그것은 90% 완성한 것이다.
이제는 나머지 90%를 고려해야 한다.
다음으로 해야 하는 일은 코드가 잘못될 수 있는 경우를 찾아보고, 각 경우에 대한 단위 테스트를 추가하는 것이다.
잘못된 매개 변수를 넘기거나 리소르를 흘리거나 리소스가 모자라는 경우 따위를 생각해 보아야 한다.
* 기본 보안 원칙
개발하고 배포하는 환경에 따라 제각각 보안을 위해 해야 할 일들이 있겠지만,
우리가 언제나 명심해야 하는 기본 원칙이 몇 가지 있다.
1. 공격 표면을 최소화하라.
2. 최소 권한 원칙.
3. 안전한 기본값.
4. 민감 정보를 암호화하라.
5. 보안 업데이트를 적용하라.
공격 표면을 최소화하라.
시스템의 '공격 표면(attack surface)' 영역은 공격자가 데이터를 입력하거나,
데이터를 추출하거나 서비스를 실행시킬 수 있는 모든 접근 지점을 합한 것이다.
1. 코드의 복잡성은 공격 매개체(attack vector)를 유발한다.
2. 입력 데이터는 공격 매개체다. 외부 데이터를 절대 신뢰하지 말라.
3. 인증이 없는 서비스는 공격 매개체다.
4. 인증을 요구하는 서비스도 공격 매개체다.
5. 출력 데이터는 공격 매개체다.
6. 디버깅 정보는 공격 매개체다.
Tip 72. 단순함을 유지하고 공격 표면을 최소화하라.
복잡한 코드는 버그의 온상이고, 공격자가 뚫고 들어올 여지도 높인다.
최소 권한 원칙
최소한의 권한만을 꼭 필요한 시간만큼만 제일 짧게 부여하라는 게 또다른 핵심 원칙이다.
시스템의 모든 프로그램과 모든 특수 권한 사용자는
과업을 마치기 위해 필요한 최소한의 권한만을 사용하여 운용해야 한다.
- 제롬 살처(Jerome Saltzer), Communications of the ACM, 1974.
시간과 권한 차원에서 공격 매개체의 유효 범위를 줄이자.
권한이야말로 '적을수록 낫다(less is more)'.
안전한 기본값
여러분의 애플리케이션 혹은 웹 사이트 사용자의 기본 설정은 가장 안전한 값이어야 한다.
가장 안전한 값이 가장 사용자 친화적인 값이나 편리한 값은 아닐 수 있겠지만,
각 사용자가 직접 보안과 편리함 사이에서 고르도록 하는 편이 낫다.
민감 정보를 암호화하라
개인 식별 정보나 금융 데이터, 비밀번호, 다른 인증 정보를 일반 텍스트로 남기지 말라.
데이터베이스든 다른 파일이든 동일하다.
설사 데이터가 유출 되더라도 암호화가 안전장치 역할을 할 수 있어야 한다.
키나 암호는 보통 빌드나 배포 프로세스 내에서 설정 파일이나 환경 변수로 관리한다.
보안 업데이트를 적용하라
Tip 73. 보안 패치를 신속히 적용하라.
공격자들은 최대한 빠르게 취약점을 공격한다. 여러분은 더 빨라야 한다.
이 팁은 인터넷에 연결된 모든 장비에 해당한다.
* 상식 대 암호
암호학(cryptography)에 있어서는 여러분의 상식이 맞지 않을 수 있다는 점을 명심해야 한다.
암호화에 있어서 첫 번째 규칙이자 가장 중요한 규칙은 절대 직접 만들지 말라는 것이다.
신뢰할 수 있는 것에만 의지하라.
간단한 암호화 작업 외에도 여러분의 웹 사이트나 애플리케이션의 보안 관련 기능을 주의 깊게 검토하라.
제대로 구현했다 하더라도 계속해서 데이터를 관리하고 안전하게 유지할 책임은 여전히 여러분에게 있다.
새로운 법령이나 법적 의무에 대한 대응도 여러분 몫이다.
외부의 인증 서비스를 사용하라.
여러분의 조직 내에서 운영하는 인증 서비스를 함께 사용할 수도 있고,
외부 클라우드가 제공하는 것을 사용할 수도 있다.
바깥에서는 안전에 주의하라.
Topic 44. 이름 짓기 (p.341)
올바른 이름으로 부르는 것이 지혜의 시작이다.
- 공자
이름이란게 무슨 의미가 있나?? 프로그래밍에서는 이름이 "모든 것!!"이다.
이름은 아주 중요하다.
이름은 여러분의 의도와 믿음을 잔뜩 드러내기 때문이다.
이름을 지을 때 왜 만드는지 생각하는 것은 아주 효과적이다.
여러분이 문제 풀이 사고방식에서 벗어나 더 큰 그림을 보도록 하기 때문이다.
이름을 지을 때는 여러분이 표현하고 싶은 것을 더 명확하게 다듬기 위해 끊임없이 노력해야 한다.
이렇게 명확하게 다듬는 작업이 여러분이 코드를 작성할 때 코드를 더 잘 이해할 수 있도록 도울 것이다.
* 문화를 존중하라
컴퓨터 과학에는 어려운 문제가 딱 두 개 있다.
캐시 무효화와 이름 짓기.
관습은 각 프로그래밍 언어나 환경의 문화에 달린 것이다.
관습이 없는 환경이라면 당연히 한 글자 변수명을 사용하면 안된다.
그 분야의 문화를 존중하라.
* 일관성
반드시 팀의 모든 사람이 각 단어의 뜻을 알고 일관성 있게 사용해야 한다.
의사 소통을 장려하자. 용어의 의미를 자연스럽게 퍼져 나갈 수 있도록 만들자.
팀에게 특별한 의미가 있는 단어를 모두 모은 프로젝트 용어 사전을 만들어보자.
시간이 흐르다 보면 프로젝트 용어들은 자리를 잡아 나갈 것이다.
* 이름 바꾸기는 더 어렵다
컴퓨터 과학에는 어려운 문제가 딱 두개 있다.
캐시 무효화, 이름 짓기, 그리고 하나 차이(off-by-one) 오류.
아무리 좋은 이름을 짓기 위해 노력하더라도 모든 것은 변한다.
부지런히 이름을 계속 바꾸지 않으면 악몽 같은 상황에 빠지게 된다.
문제를 발견했으면 고쳐라.
당장 바로 그 자리에서.
Tip 74. 이름을 잘 지어라. 필요하면 이름을 바꿔라.
읽는 사람에게 의도를 표현할 수 있는 이름을 붙이고, 의도가 변하자마자 이름을 바꿔라.
잘못된 이름을 바꿀 수 없는 상황이라면 더 큰 문제가 있는 것이다.
바로 ETC(easier to change) 위반이다.
그 문제를 고치고 나서 잘못된 이름을 바꿔라.
이름을 바꾸기 쉽게 만들고, 자주 이름을 바꿔라.