-
Notifications
You must be signed in to change notification settings - Fork 0
Description
파충류의 뇌에 귀 기울이기
본능이란 우리 뇌의 무의식 속에 채워져 있는 패턴에 대한 단순한 반응이다. 프로그래머로서 경험이 늘어 갈수록 우리의 뇌에는 암묵적인 지식이 쌓여가게 된다. 잘되는 방법, 잘 안되는 방법, 오류 형태별로 가능한 원인 등 일하는 동안 보고 느끼는 것들이 쌓이게 된다는 의미이다.
이게 무슨 의미일까, 기억에 남기는 할까? 싶은 내용들도 무의식 속에 뇌는 계속 저장을 하고 있다는 뜻이다. 그렇다면 어떻게 무의식 속에 잠재된 기억들을 꺼낼 수 있을지가 관건이다.
백지의 공포
무에서 유로 새로운 프로젝트를 시작하는 것은 두렵다. 이 원인은 두 가지가 있고, 한 가지 방법으로 두 가지를 모두 해결할 수 있다.
- 파충류의 뇌가 우리에게 무언가 할 말이 있다.
이 말은 무의식속에서 무언가 의심이 피어오르는 것을 의미한다. 이런 의심은 중요하다. 프로그래밍을 하다가 뭔가 꺼림칙하다면, 파충류의 뇌가 말을 걸어오는걸지도 모른다.
이 느낌을 그냥 넘기지 말고, 시간을 준다면 대응책을 생각할 수 있는 무언가로 변할 수 있을 것이다.
- 합리적인 두려움
개발자들은 코드에 많은 것을 투자한다. 그래서 오류가 발생하면 코드의 오류를 자신의 실력 부족이라고 받아들인다. 흔히 자신의 실력이 과대평가 되었다는 두려움을 의미한다.
자신과 싸우기
코딩을 하다보면 잘 되는 날도, 잘 되지 않는 날도 있다. 그러나 전문가가 되려면 끊임 없이 나아가야 한다고 생각한다. 그러나 진짜로 우리가 해야하는 일은 정 반대다.
끊임 없이 나아갈 것이 아니라, 현재 작업이 필요 이상으로 힘들지는 않은지 구조나 설계 자체가 잘못되지는 않았는지, 엉뚱한 문제를 붙들고 있는건 아닌지 파충류의 뇌를 느껴야한다.
파충류와 이야기 하는 방법
일단, 하고 있던 일을 멈추고 뇌가 정리를 할 수 있는 시간과 공간을 확보해라. 산책을 하고 수다를 떨어라. 그러다 보면 생각이 저절로 뇌 층층이 스며들고, 언젠가 떠오르는 순간이 찾아올 것이다.
이 방법이 잘 통하지 않는다면, 작성하는 코드를 그림으로 그려보고 동료에게 설명해 보아라. 사람이 없다면 인형에게 해도 좋다.
아마 이미 이전에 이런 경험이 있다면, 누군가에게 자신의 문제를 설명하다가 ‘아하!’ 하고 뛰쳐나간 경험이 있을 것이다. 이 내용이 바로 그것이다.
위 방법들도 통하지 않는다면, 행동할 시간이다. 우리의 뇌에게 우리가 하려는 일은 아무 문제가 없다고 프로토타이핑을 통해 알려주어야 한다.
놀이 시간이다!
- 포스트잇에 “프로토타이핑 중” 이라고 모니터에 써서 붙여라
- 프로토타이핑은 원래 실패하는 것이라고 상기 시켜라. 실패하지 않더라도 프로토타이핑은 버리는 것이다.
- 텅 빈 에디터에 배우고 싶거나, 하고 싶은 것을 주석으로 적어라
- 코딩을 시작해라
꺼림칙한 느낌이 명확한 문제로 구체화 되면 즉각 해결해라. 실험을 마쳤는데도 불안하다면, 다시 처음부터 시작하되 첫 단계는 산책과, 수다 그리고 휴식이 되어야 한다.
이 과정을 반복하면 놀이처럼 느껴질 것이다.
‘여러분’의 코드뿐이 아니다
다른 사람의 코드를 보다가 처리 방식이 이상해 보인다면 적어 놓아라. 그리고 작업하며 패턴을 찾아보아라. 그 사람이 그런식으로 코드를 작성할 수 밖에 없었던 이유를 발견하게 된다면, 코드를 이해하는 일이 더 쉬워질 것이며 은연중에 새로운 것을 배울 수도 있다.
우연에 맡기는 프로그래밍
우연에 맡기는 프로그래밍 하기
종종 시험 삼아 프로그램을 작성하였는데, 잘 작동하여서 이 다음에 이어 붙여서 작성하고 작성하다가 어느 순간 오류가 발생하면, 문제를 해결하기 어려운 경험을 해 본 적이 있을 것이다. 애초에 코드가 왜 잘 돌아가는지도 몰랐기 때문이다.
그것은 몇번 운이 좋았기 때문이고, 근거 없는 확신을 가지고 계속 나아갔기 때문에 결국 실패를 맛 본 것이다. 그래도 우리 또한 종종 프로그래밍을 하며 우연에 맡기는 경우가 있다.
구현에서 생기는 우연
예를 들어서 어떤 루틴이 잘못된 데이터를 가지고 호출하는 경우이다. 루틴은 예상하지 못한 데이터에 특정 방식으로 반응을 하고, 우리는 그 반응을 기반으로 코드를 작성한다. 하지만 루틴을 만든 사람의 의도는 이 루틴이 이대로 작동하는 것이 아니었던것이다.
만약 우리가 이 루틴을 고치면 프로그램 전체가 멈추게 될 지도 모른다. 실제로 설계가 올바르지 않은데도, 원하는 동작대로 작동하는 경우가 있을 수도 있다.
이 때 우리는 잘 작동하는데 굳이 건드려서 일을 만들 필요가 있을까? 생각해볼 수 있다. 그러나 그럴 이유가 있다.
- 정말로 제대로 돌아가는게 아닐수도 있다.
- 다른 하드웨어 환경에서는 완전히 다르게 동작할 가능성이 있다.
- 문서화 되지 않은 동작은 다음 릴리스에서 변경될 가능성이 있다.
- 불필요한 추가 호출은 코드를 느리게 만든다.
- 추가 호출한 루틴에 버그가 생길 수도 있다.
이러한 문제를 막기 위해서는 모듈화와 문서화를 잘 하고, 문서화된 동작에만 의존해야한다.
비슷하다고 괜찮을리 없다
대형 프로젝트의 경우에는 딱 숫자 1만큼 차이가 났을지라도, 서비스가 운영되면서 이 값은 점점 차이가 벌어지게 되고 겉잡을 수 없을 만큼 차이가 벌어지게 될 가능성이 있다. 그리고 최악의 경우 프로젝트가 폐기될 가능성이 있다.
유령 패턴
우리는 언제나 패턴과 인과 관계를 찾으려고 노력한다. 이러한 행동은 로또나, 주사위 숫자, 룰렛 번호에서 패턴을 찾으려고 하는 것과 같다.
이러한 행동은 좋지 못하다. 가정하지 말고, 증명하라.
상황에서 생기는 우연
특정 상황에서 빚어지는 우연들이 있다. 가장 대표적인 예가 인터넷 검색으로 찾은 첫 번째 답에서 코드를 복사해올 때이다. 이 코드가 당장 긁어와서 잘 작동하더라도, 이 코드의 작성자와 나의 환경이 동일하다고 확신할 수 없기 때문에 추후에 문제가 발생할 수도 있다.
항상 의미를 신경쓰고 그냥 따라해서는 안된다. 잘 되는 듯한 답을 찾는 것과 올바른 답을 찾는 것은 다르다.
의도적으로 프로그래밍하기
오류를 줄이기 위해서는 의도적으로 프로그래밍을 해야 한다.
- 무엇을 하고 있는지 알아야 한다.
- 더 경험이 적은 프로그래머에게 상세하게 설명할 수 있어야한다.
- 자신도 잘 모르는 코드를 만들지 말라
- 계획을 세우고 그것을 바탕으로 코드를 작성하라
- 신뢰할 수 있는 것에만 기대라. 가정에 의존해서는 안된다. 만약 판단하기 어렵다면 최악의 상황을 가정하라.
- 가정들은 기록으로 남겨라
- 코드뿐만 아니라 우리가 세운 가정도 테스트를 해보아야 한다. 추측만 하기보다는 실제로 시험을 해보아야 한다.
- 우선순위를 정해서 중요한것에 시간을 투자해라
- 과거의 노예가 되서는 안된다. 언제든지 변경될 가능성을 염두에 두어야한다.
알고리즘의 속도
실용주의 프로그래머가 거의 날마다 하는 추정이 하나가 있다. 바로 알고리즘이 사용하는 자원, 시간, 프로세서, 메모리 등을 추정하는 것이다. 보통 이런 추정을 할 때는 상식과 약간의 분석, Big-O 표기법이라고 부르는 근삿값 기록 방식을 사용한다.
알고리즘을 추정한다는 말의 의미
알고리즘은 대부분 선형적이지 않다. 이진 탐색은 일치하는 항목을 찾을 때 모든 후보를 다 살펴볼 필요가 없기 때문에 증가폭이 선형보다 작다. 그러나 나머지 알고리즘들은 증가 폭이 선형보다 훨씬 크다. 즉, 수행 시간이나 메모리 요구량이 n보다 훨씬 빠르게 늘어난다.
그 예로 10개의 원소를 처리하는데 1분도 걸리지 않는 알고리즘이 100개를 처리하는데 처리가 끝나지 않는 경우도 있다.
우리는 알고리즘을 풀 때 의식적으로 수행 시간과 필요한 메모리 양을 반복문 개수 등을 통해서 가볍게 추정한다. 이보다 더 상세한 분석이 필요한 경우에는 Big-O 표기법이라고 불리는 표기법을 사용한다.
Big-O 표기법
근삿값을 다루는 수학적 방법으로 O() 와 같이 표기한다. 어떤 정렬 루틴이 원소 n개를 정렬하는데 O(n^2) 시간이 걸린다고 할 때, 이는 최악의 경우 걸리는 시간이 n 제곱에 비례해 늘어난다는 의미이다.
O() 표기법은 우리가 측정하는 값-시간, 메모리 등의 상한을 기술하는 표기법이다. 예를 들어 O(n^2)이면 함수가 실행되는데 걸리는 시간이 n^2보다는 더 빠르게 늘어나지 않는다는 의미이다.
n이 커질수록 사실상 계수가 미치는 영향이 작아지기 때문에, 상수인 계수는 표기하지 않는 것이 관례이다.
- O(1)
- 상수
- 배열 원소 접근, 단순 명령문
- O(lgN)
- 로그
- 이진 검색
- O(n)
- 선형
- 순차 검색
- O(NlgN)
- 선형보다는 좋지 않음
- 퀵 정렬, 힙 정렬 평균 수행 시간
- O(n^2)
- 제곱
- 선택 정렬과 삽입 정렬
- O(n^3)
- 세제곱
- 두 n x n 행렬의 곱
- O(C^m)
- 지수
- 여행하는 외판원 문제, 집합 분할 문제
상식으로 추정하기
단순 반복문
- O(n)
중첩 반복문
- O(m * n)
- O(n^2) 이 되기 쉽다
- 버블 정렬
반씩 자르기
- O(lgN)
- 이진 트리 탐색, 2진수 표현에서 1인 비트 찾기 문제
분할 정복
- O(NlgN) - 평균
- O(n^2) - 최악
- 퀵 정렬
리팩터링
소프트웨어개발은 처음 설계한대로 지어지고 계속 유지되는 건축보다는 정원 가꾸기에 가깝다. 최소 계획에 맞추어 식물을 심고 햇빛, 그림자, 비와 바람의 상호작용에 따라 그 위치를 바꾸어 심기도 하고, 가지를 치기도 한다.
사업 분야에 있는 사람들은 변하지 않는 건축과 같은 메타포를 더 선호한다. 하지만 소프트웨어는 현실적으로 정원 가꾸기에 더 가깝다.
프로그래밍에 있어서 계속해서 위치를 바꾸어 심고, 가지를 치는 등 코드를 다듬는 활동을 리팩터링이라고 한다.
<리팩터링>의 저자 마틴 파울러는 리팩터링을 다음과 같이 정의했다
밖으로 드러나는 동작은 그대로 유지한 채 내부 구조를 변경함으로써 이미 존재하는 코드를 재구성하는 체계적 기법
- 활동은 체계적이다. 아무렇게나 하는 것이 아니다.
- 밖으로 드러나는 동작은 바뀌지 않는다. 기능이 추가 되어서는 안된다.
리팩터링은 언제하는가?
프로그래밍을 하다가 무엇인가 잘못되었음을 깨달았을 때 변경하기 위해 한다.
- 중복
- 직교적이지 않은 설계
- 더 이상 유효하지 않은 지식
- 필요성
- 이전에 필요하다고 생각했던 것이 이제는 필요하지 않을 때
- 성능
- 성능 개선을 위해서는 다른 위치로 코드를 바꾸어야하는 경우가 생긴다.
- 테스트 통과
- 코드를 조금 추가한 후 추가된 테스트가 통과 되었을 때
리팩터링은 어떻게 하는가?
- 리팩터링과 기능 추가를 동시에 하지 말라
- 리팩터링을 시작하기 전 든든한 테스트가 있는지 확인하라
- 단계를 작게 나누어서 신중하게 작업하라
테스트로 코딩하기
테스트에 대해 생각하기
데이터베이스와 연동되는 함수를 테스트한다고 하자. 이를 테스트하기 위해서 어떻게 테스트 데이터를 채울지 고민해야한다. 이 과정에서 어떤 데이터베이스 스키마가 필요할지 고민하게 되고, 어떤 필드가 필요한지 고민해보게 된다.
테스트 코드에 대해 생각만 하는 것으로도 많은 것을 깨달을 수 있다.
테스트가 코딩을 주도한다
기능 단위로 테스트를 고민하게 되면, 자연스럽게 결합도는 낮아지는 코드를 작성할 수 있게 된다. 즉 무언가를 테스트하기 좋게 만들면 결합도도 낮아지는것이다.
그리고 무언가를 테스트하기 위해서는 그것을 잘 이해해야만 한다. 테스트를 진행하지 않을 경우에는 확신이 들지 않지만, 우연에 기대어 코드를 작성했던 경우도 종종 있을 것이다. 이런 경우에 계속 프로젝트를 진행하며, 발생하는 문제들을 그때 그때 수정하면서, 테스트를 먼저 진행할때보다 몇 배는 시간이 더 소요되게 된다.
테스트 주도 개발
흔히 TDD라고 말하며, 테스트 우선 개발이라고 칭하기도 한다.
TDD의 기본 주기는 다음과 같다.
- 추가하고 싶은 작은 기능 하나를 결정한다.
- 그 기능이 구현되었을 때 통과하게 될 테스트를 하나 작성한다.
- 테스트를 실행한다. 다른 테스트는 통과하고 방금 추가한 테스트 딱 하나만 실패해야 한다.
- 실패하는 테스트를 통과시킬 수 있는 최소한의 코드만 작성한다. 그리고 이제는 모든 테스트가 통과하는지 확인한다.
- 코드를 리팩터링한다. 방금 작성한 테스트나 함수를 개선할 수 있는 부분이 없는지 살펴본다. 개선한 후에도 테스트가 계속 통과하는지 확인한다.
만약 테스트 작성을 이제 막 배우기 시작했다면 TDD가 매우 효과적이다. TDD 작업 방식을 따르면 언제나 우리 코드에 테스트 코드가 함께 있을 수밖에 없기 때문이다.
TDD: 목표가 어디인지 알아야 한다
“코끼리를 먹는 방법은” 이라는 농담의 답은 “한 번에 한입씩”이다. 이는 TDD의 장점으로 흔히 언급되곤 한다. 전체 문제를 완전히 파악하기 어려울 경우에는 한 번에 테스트 하나씩 작은 단계들을 밟는다는 의미이다.
그러나 이러한 접근 방법이 테스트의 진짜 의미는 간과한 채 쉬운 문제들만 만지작거리도록 유도할 수 있다. 반짝이는 테스트 메시지에 현혹되어, 정작 중요한 핵심 로직 작성에는 힘을 쏟지 않고 테스트를 통과시키기 쉬운 부분에 시간을 쏟는다는 의미이다.
테스트를 할 때는 이 테스트를 하는 궁극적인 목표가 무엇인지 놓쳐서는 안된다.
단위 테스트
단위 테스트란 어떤 모듈에서 이것저것을 시켜보는 코드를 가리킨다. 일반적으로 단위 테스트는 일종의 인위적인 환경을 구축한 다음, 테스트할 모듈의 루틴들을 호출한다. 그런 다음 반환된 결과들을 이미 알고 있는 값과 비교해 보거나 똑같은 테스트를 이전에 돌렸을 때 나온 값과 비교하여 올바른지 검사한다.
이와 같이 동일한 코드를 수정하고 다시 돌려보는 것을 회귀 테스트(regression testing)이라고 한다.
임시 테스트
직접 코드를 이리저리 찔러보는 것을 의미한다. console.log() 한 줄 이거나, 디버거나 IDE를 직접 실행하며 입력하는 코드 조각일 수도 있다.
이러한 작업 이후에는 임시 테스트를 정식의 테스트 형태로 만들어 두어야 한다. 임시 테스트도 버리지 말고 단위 테스트로 만들어라.
테스트 접점 만들기
아무리 잘 작성된 테스트라도 모든 버그를 발견할 수는 없다. 이 말은 소프트웨어를 배포한 후에도 테스트할 일이 자주 생긴다는 의미이다.
이럴때 로그 파일에 쌓이는 추적 메시지가 도움이 될 수 있다. 로그 메시지는 반드시 규칙적이고 일관된 형식이어야 한다. 일관적이지 않다면, 메시지는 읽기도 어렵고 현실적으로 파싱하기도 힘든 그냥 ‘내뱉은 것’이 된다.
이름 짓기
이름이란게 무슨 의미가 있나? 싶겠지만 프로그래밍에서는 이름이 모든 것이다. 우리는 코드에서 하는 역할에 따라 이름을 지어야 한다고 믿는다. 이 말은 우리가 무언가를 만들 때마다 ‘이걸 왜 만드는 거지?’ 라는 생각을 해야한다는 뜻이다.
이 질문은 아주 효과적이다. 우리가 문제 풀이 사고방식에서 벗어나 더 큰 그림을 볼 수 있도록 해주기 때문이다.
문화를 존중하라
대부분 컴퓨터 입문용 교재에는 의미 파악이 어려운 한 글자 변수명을 사용 짜지 말라고 한다. 그러나 i, j, k의 경우 관례적으로 반복에서 증가하는 변수로 쓰여 왔다. s는 문자열을 의미하며, 이 외에도 여러가지 관습들이 있다. 한 가지 더 예시를 들면, 어떤 언어에서는 카멜케이스를 사용하고 어떤 언어에서는 스네이크케이스를 사용한다.
때로는 우리가 알고 있는 규칙과 달라보여도, 해당 분야의 문화를 존중하라.
일관성
Order가 누군가에게는 ‘주문’이라는 의미일 수 있지만, 종교 단체의 입장에서는 ‘교단’의 의미가 될 수 있다. 원활한 커뮤니케이션을 위해서는 단어의 뜻을 모두가 알고 일관성 있게 사용해야 한다.
이 방법으로는 많은 의사소통을 장려하는 것이다. 대표적인 프로그래밍 기법으로는 페어프로그래밍이 있다.