#7 While You Are Coding
코딩하는 동안
Topic 37. 파충류의 뇌에 귀 기울이기
요약
이 항목은 프로그래머가 업무 중 느끼는 불안·초조·의심 같은 감정이 단순한 감정이 아니라 파충류의 뇌(lizard brain) 가 보내는 경고 신호라는 점에 주목합니다. 이러한 감정은 문제를 조기에 감지하고, 설계를 개선하고, 작업 방향을 수정하는 데 중요한 역할을 합니다. 핵심은 이 신호를 억누르지 말고, 외부화·관찰·실험·프로토타이핑을 통해 실제 행동으로 전환하는 것입니다.
파충류의 뇌란 무엇인가
게빈 드 베커는 사람들이 자신의 안전을 지킬 수 있는 본능적 감각을 무시하도록 사회적으로 학습돼 왔다고 설명합니다. 프로그래머 역시 일상적으로 이러한 신호를 받지만, “감정적이다”, “불필요하다”는 이유로 무시할 때가 많습니다.
파충류의 뇌는 아래와 같은 형태로 반응합니다.
원인을 설명하기 어려운 불편함
코드나 설계에서 느껴지는 이상한 감각
새로운 프로젝트에서 느껴지는 막연한 두려움
대화 중 뭔가 맞지 않는 느낌
이러한 반응은 대부분 정확한 위험 감지 신호입니다.
개발자가 느끼는 불안의 원인
파충류의 뇌가 주는 직관적 의심
경험이 쌓일수록 우리는 코드나 설계에서 “이상하다”는 느낌을 더 자주 받습니다. 그 이유를 나중에서야 깨닫는 경우도 많습니다.
합리적 두려움
새로운 프로젝트, 리팩토링, 난해한 버그는 누구에게나 두려움을 줍니다. 이는 실력이 부족해서 오는 감정이 아니라, 과거의 경험을 통해 형성된 학습된 경계심입니다.
가면 증후군
개발자가 흔히 겪는 “내가 이 일을 해도 되는가?”라는 감정 역시 자연스러운 현상입니다. 프로젝트가 복잡할수록 파충류의 뇌는 경고를 더 크게 보냅니다.
본능적 감정과 싸우는 법
일단 멈춰서 관찰하기
막연한 불안이 생겼다면 잠시 손을 멈추고
자리 이동
산책
커피 한 잔
화면에서 눈 떼기 와 같은 방식으로 뇌에 정리할 시간을 주는 것이 좋습니다.
불안을 외부화하기
머릿속에서만 고민하면 불편함은 더 커집니다. 아래와 같이 외부로 꺼내는 것이 효과적입니다.
그림 그리기
말로 설명하기
개발자가 아닌 사람에게 설명한다고 가정하기
포스트잇으로 정리하기
설명 과정에서 문제가 무엇인지 구조적으로 드러납니다.
프로토타이핑의 힘
놀이처럼 접근하기
엔디 와이어는 프로그래머들이 에디터 앞에서만 문제를 해결하려 하면 비효율적이라고 말합니다. 눈앞에서 코드를 붙잡기보다
적어보기
그려보기
가볍게 실험해보기 가 훨씬 빠를 때가 많습니다.
프로토타입을 만드는 절차
포스트잇에 “프로토타이핑 중”이라고 적어 책상 옆에 붙인다.
실패를 가정하고 작게 실험한다.
실험하면서 배운 점을 문장으로 정리한다.
정리한 내용을 기반으로 코드를 다시 작성한다.
이 방법이 효과적인 이유
두려움을 줄인다.
복잡성을 작은 조각으로 나눈다.
설계를 가볍게 검증할 수 있다.
모호한 감정을 구체적 행동으로 바꾼다.
여러분의 코드만의 문제가 아니다
우리 업무 대부분은 기존 코드 읽기입니다. 다른 개발자가 작성한 코드가 이상하고 불편하게 느껴지는 것은 자연스러운 일이며, 그 불편함은 리팩토링이 필요하다는 신호일 뿐입니다. 자책할 필요가 없습니다.
파충류의 뇌에 귀 기울이는 실천적 방법
잠시 멈추기
불안하거나 찜찜한 감정이 들면 즉시 멈추고 상황을 관찰한다.
문제를 외부화하기
그림, 설명, 포스트잇, 메모 등으로 생각을 밖으로 끌어낸다.
불편한 감정을 가볍게 해석하지 않기
감정은 실력 부족의 증거가 아니라 신호다.
불안의 이유를 문장으로 표현하기
예시:
“이 함수는 책임이 너무 많다.”
“이 API는 예외 상황을 설명하지 않는다.”
“이 설계는 확장성을 고려하지 않았다.”
문장으로 정리하면 해결 방향이 보입니다.
Kotlin 실무 관점의 활용 팁
불안한 영역을 샌드박스로 분리해 실험하기
이 방식은 본 코드에 손대기 전, 불안 요소를 안전하게 탐색하고 문제의 구조를 명확히 파악하는 데 도움이 됩니다.
도전해 볼 것
미뤄두고 있던 일 중 약간 무섭고 어렵게 느껴지는 것을 하나 고른다.
시간을 1~2시간으로 제한하고 이번 항목의 기법을 적용한다.
프로토타입 방식으로 가볍게 실험한다.
타이머가 울리면 결과물을 지워도 좋다.
중요한 것은 시작했고, 배웠다는 사실이다.
Topic 38. 우연에 맡기는 프로그래밍
요약
이 항목은 프로그래밍 과정에서 우리가 얼마나 자주 우연에 의존해 성공했다고 착각하는지를 지적합니다. 전쟁터에서 지뢰밭을 건너는 병사의 비유처럼, 우리는 코드가 “운 좋게” 작동한 것을 실력으로 오해하고, 이를 기반으로 의사결정을 반복하곤 합니다. 핵심 메시지는 명확합니다. 개발자는 우연에 맡기는 프로그래밍을 멈추고, 의도적으로 프로그래밍해야 한다는 것입니다.
지뢰밭을 통과한 병사의 비유
한 병사가 지뢰밭을 지나 아무 일도 없었다고 해서 그 길에 지뢰가 없었다는 뜻은 아닙니다. 그 병사는 단지 운이 좋았을 뿐, 그 경험으로 확신을 가지면 다음에는 큰 위험으로 이어질 수 있습니다.
프로그래머도 마찬가지입니다. 코드가 “돌아갔다”고 해서 그것이 맞는 코드, 올바른 설계, 안전한 구조라는 의미는 아닙니다.
그래서 우리는
우연히 성공한 코드
이유를 모르는 결과
명확히 설명되지 않는 동작 을 경계해야 합니다.
우연에 맡기는 프로그래밍의 사례
프레드가 겪는 코드 문제
프레드는 프로그래밍을 하다 보면 어느 날 갑자기 문제가 해결되거나, “이제 되는 것 같다”는 느낌을 받습니다. 하지만 그 이유를 설명하지 못한 채 넘어간다면, 그것은 실력이 아니라 운의 결과입니다.
프레드가 코드를 작성할 때 반복되는 패턴은 다음과 같습니다.
조금 수정한다.
실행해본다.
잘 되는 것 같다.
돌아가는 것 같으니 넘어간다.
하지만 코드가 망가지는 이유를 모른다면 해결은 근본적일 수 없습니다.
구현 과정에서 생기는 우연
잘못된 입력과 잘못된 반응
예를 들어 잘못된 데이터를 던졌을 때 프로그램이 문제 없이 실행되는 경우가 있습니다. 이는 실제로 “문제 없다”가 아니라, 우연히 예외 상황이 드러나지 않았을 뿐입니다.
이런 우연이 반복되면 개발자는 잘못된 확신을 가지게 되고, 나중에 환경이 변하면 같은 루틴이 더 이상 작동하지 않게 됩니다.
GUI 관련 우연과 잘못된 호출
프레드가 GUI 렌더링 코드를 작성하면서 다음과 같이 호출한다고 가정합니다.
프레드는 무엇인가를 고치려는 마음으로 여러 호출을 섞어 사용하지만, 실제로는 프레임워크가 의도한 방식과 전혀 다르게 호출한 것입니다. 지금 화면이 우연히 그려졌다고 해서 올바른 호출 순서라는 의미는 아닙니다.
이러한 우연들이 쌓이면 문제는 더욱 복잡해집니다.
화면이 이상하게 보임
일부 시스템에서는 정상, 다른 시스템에서는 오류
환경 차이로 동작 변경
CPU 사용량 증가
버그 추적 불가
비슷하다고 괜찮은 게 아니다
프로젝트에서 외부 장치의 데이터 수집 타이밍이 우연히 잘 맞아 보였다고 해서, 그것이 정확한 설계라는 뜻은 아닙니다.
프레드는 데이터를 받아들이는 타이밍이 “대체로 맞아 보이니 괜찮다”고 생각했지만, 실제로는 설계 자체가 부정확했습니다. 결국 프로젝트는 폐기되었습니다.
상관관계와 유럽 패턴
인간은 인지적으로 패턴과 연관성을 찾고 싶어 합니다. 그래서 우연한 결과를 원인으로 착각하는 오류를 범하곤 합니다.
예를 들어 러시아에서 머리카락이 많은 사람이 대통령이 된다는 우스갯소리가 있다고 가정하면, 이는 상관관계가 인과관계로 오해된 사례입니다.
코드에서도 같은 착각을 하기 쉽습니다.
특정 버전에서만 나는 오류
특정 조건에서만 작동하는 기능
새로운 환경에서 나타나는 비정상 동작
이를 모두 우연으로 치부하면 더 큰 문제를 낳습니다.
상황에서 생기는 우연
예를 들어 특정 OS에서는 화면에 글자가 잘 보이는 GUI 코드가 다른 환경에서는 흐릿하게 보일 수 있습니다.
프레드는 이를 “환경이 다르니까 어쩔 수 없다”고 받아들이지만, 실제로는
DPI 설정
렌더러 차이
서버의 파일 접근 방식 차이 등이 원인일 수 있습니다.
문제를 명확히 파악하지 않으면, 작동하는 것처럼 보이는 코드에 우연히 의존하게 됩니다.
의도적으로 프로그래밍하기
“돌아가니까 괜찮다”는 태도는 위험합니다. 개발자는 다음을 항상 고려해야 합니다.
정확한 원인 파악
왜 이 코드가 작동하는가? 지금의 동작이 정말 의도된 결과인가?
이해하지 못한 기술에 기대지 않기
알지 못하는 기술 위에서 가정을 세우는 것은 위험합니다. 우연한 동작은 언제든 신뢰를 무너뜨립니다.
계획을 세우고 기록하기
계획 없이 코드를 작성하면 수정, 테스트, 배포 과정에서 우연의 영향을 크게 받습니다.
코드를 시험하고 결과를 기록하기
테스트를 통해 확인된 사실과 가정을 분리해야 합니다. 테스트 결과가 맞다면 왜 맞는지, 틀렸다면 왜 틀렸는지 기록하는 것이 중요합니다.
우선순위와 비용
가정이 틀렸음에도 코드가 우연히 돌아가면 나중에 훨씬 큰 비용이 발생할 수 있습니다.
변경 비용 증가
일정 지연
QA 비용 증가
버그 유출
설계 전체의 붕괴
따라서 “왜 되는지” 또는 “왜 안 되는지”를 분석하는 데 시간을 투자해야 합니다.
Topic 39. 알고리즘의 속도
요약
이 항목은 개발자가 프로그램의 실행 시간 및 복잡도를 정확하게 이해해야 한다는 점을 강조합니다. 프로그램이 얼마나 빠르게 실행될지는 입력 크기에 따라 달라지며, 이를 분석하기 위해 대문자 O 표기법(Big-O notation) 을 사용합니다. Big-O는 실제 시간이나 정확한 수치를 알려주지는 않지만, 입력 크기가 커질 때 성능이 어떻게 변화하는지 예측하게 해줍니다. 또한 단순 반복문, 중첩 루프, 분할 정복, 재귀, 조합적 경우 등을 통해 알고리즘이 얼마나 다른 속도로 증가하는지를 설명하며, 개발자는 “최고의 알고리즘”이 아니라 “상황에 적합한 알고리즘”을 선택해야 한다고 조언합니다.
알고리즘 속도를 추정한다는 의미
대부분 알고리즘의 실행 시간은 입력 크기(n) 의 영향을 받습니다. 입력 데이터가 커지면 실행 시간이 거의 항상 증가합니다. 예를 들어
문자열 n개의 정렬
m×n 행렬 구성
n비트 암호화 등은 모두 입력 크기에 비례해 시간이 늘어납니다.
입력이 커질수록 알고리즘의 실행 시간은 기하급수적으로 증가하는 경우가 있으며, 이는 코드 성능 최적화에서 중요한 고려사항입니다.
대문자 O 표기법(Big-O)
대문자 O 표기법은 프로그램이 최악의 경우(worst-case) 에 얼마나 오래 걸리는지를 알려주는 수학적 표기입니다. 예를 들어
어떤 작업이
O(n²)이라고 하면 입력 n이 두 배가 되면 작업 시간은 네 배가 됩니다.O(n)은 선형 증가,O(log n)은 느린 증가,O(1)은 입력이 늘어도 시간이 거의 변하지 않음을 의미합니다.
Big-O는
정확한 시간이나 메모리 값
실제 기계 환경
언어 차이 등을 고려하지 않지만, 전체적인 증가 추세를 파악하는 데 필수적입니다.
다양한 알고리즘의 시간 복잡도 비교
이미지 속 그래프에서 몇 가지 복잡도 곡선을 비교하고 있습니다.
O(1) — 상수 시간
작업이 입력 크기와 무관합니다. 예: 배열의 원소 1개 접근.
O(log n) — 로그 시간
이진 탐색처럼 입력을 계속 절반으로 줄이는 알고리즘.
O(n) — 선형 시간
입력 전체를 한 번 훑는 작업.
O(n log n) — 로그가 붙은 선형
대부분의 정렬 알고리즘이 해당됩니다(merge sort, quicksort 평균).
O(n²) — 이차 시간
중첩 반복문 형태. 큰 입력에서는 급격히 느려짐.
O(2ⁿ) — 지수 시간
여행자 문제 등 폭발적 증가. 입력이 조금만 커져도 실행 불가.
O(n!) — 팩토리얼
조합 문제. 사실상 계산 불가능.
상식으로 추정하기
알고리즘이 어떤 복잡도를 갖는지 상식적으로 추측할 수 있습니다.
단순 반복문 → O(n)
하나의 반복문이 1부터 n까지 순회.
중첩 반복문 → O(m×n) 혹은 O(n²)
이중 루프 구조.
반복+반복 → O(n log n) 가능
예: 이진 트리 탐색 반복.
분할 정복(divide & conquer) → O(n log n)
입력을 반씩 나누며 정렬하는 알고리즘.
조합적 경우(permutation) → O(n!)
입력이 조금만 커져도 대폭 증가.
실제 개발 환경에서의 고려
회사의 대부분 코드는 이미 빠른 라이브러리를 사용하므로 알고리즘 구현 자체에 시간을 쓰지 않는 경우가 많습니다. 그러나 중요한 점은 다음과 같습니다.
단순 반복문도 O(n)이다
입력 크기가 커지면 충분히 느려질 수 있습니다.
의도치 않은 중첩 비용(O(m×n), O(n³))이 숨어 있을 수 있다
데이터 구조나 조건문 안에서 발생하는 추가 연산이 성능을 크게 떨어뜨릴 수 있습니다.
최적의 알고리즘이 항상 최선은 아니다
입력 크기가 매우 작다면 O(n²) 이 O(n log n) 보다 성능이 나을 때도 있습니다.
알고리즘은 사용되는 데이터의 규모와 상황에 따라 선택해야 합니다.
알고리즘 성능 검증: 직접 테스트하기
책에서는 “추정을 테스트하라”고 강조합니다.
정확한 시간 측정 도구 사용
코드 프로파일러
시간 측정 그래프
메모리 사용량 기록
예를 들어, 아래처럼 간단한 반복문을 실제로 측정할 수 있습니다.
이런 방식으로 반복횟수·배열 크기·정렬 대상 등 다양한 조건에서 직접 속도 차이를 측정하면 Big-O 개념이 훨씬 명확해집니다.
최고라고 언제나 최고는 아니다
가장 빠른 알고리즘이 항상 가장 좋은 알고리즘은 아닙니다. 데이터의 구조와 입력 규모가 다르면 실행 시간은 완전히 달라지고, 실제 상황에서는
메모리 비용
구현 복잡도
하드웨어 환경
캐시 적중률 등이 모두 영향을 줍니다.
따라서 “성능 최적화”라는 명목으로 가장 빠른 알고리즘만 선택하는 것은 위험합니다. 선택의 기준은 문제 상황에 따라 달라져야 합니다.
Topic 40. 리팩터링
요약
프로그램이 발전할수록 초기 결정은 더 이상 적합하지 않게 되고, 코드의 일부를 다시 작성해야 하는 순간이 찾아옵니다. 이는 잘못된 것이 아니라 자연스러운 현상이며, 코드가 성장하고 발전해야 한다는 뜻입니다. 리팩터링은 코드의 동작은 유지하되 내부 구조를 변경하여 더 좋은 설계로 만드는 과정입니다. 정원을 가꾸듯 조금씩 구조를 손보고, 낡은 부분을 제거하고, 환경 변화에 맞춰 형태를 조정하는 일이 리팩터링입니다. 핵심은 “지금 당장 잘못되었다고 판단되면 주저하지 말고 개선해야 한다”는 것입니다.
건축 메타포 vs 정원 가꾸기 메타포
소프트웨어 개발을 오래 설명해 온 가장 유명한 메타포는 건축이지만, 실제 개발 과정은 오히려 정원 가꾸기에 더 가깝습니다.
건축 메타포의 단계
건축가가 설계도를 그린다.
건축업자가 기초를 놓고 구조를 세운다.
입주자가 들어와 문제 발생 시 건물 관리실에 연락한다.
하지만 소프트웨어 개발은 이렇게 한 번 지으면 끝나는 구조가 아닙니다. 요구사항은 지속적으로 변하고, 운영 환경도 시간이 지날수록 달라집니다.
정원 가꾸기 메타포
정원은
기후와 계절에 따라 달라지고
지나치게 자라난 가지를 잘라내고
배치가 어울리지 않으면 옮겨 심고
전체적인 건강 상태를 꾸준히 유지해야 합니다.
소프트웨어도 마찬가지입니다. 지난달에는 괜찮았던 구조가 새로운 요구사항 때문에 더 이상 맞지 않을 수 있습니다. 이때는 과감히 가지치기를 하고, 뽑아 옮기고, 다시 정비해야 합니다. 이 과정이 바로 리팩터링입니다.
리팩터링의 핵심
정원 전체를 뒤엎는 것이 아니라, 작고 점진적이고 안전한 변화를 누적해 나가는 활동입니다. 중요한 핵심은 두 가지입니다.
1. 리팩터링은 체계적이다.
무작정 하는 작업이 아니다. 목표가 명확해야 하며 증상과 원인을 구분해야 한다.
2. 바깥의 동작은 그대로 유지된다.
기능을 바꾸지 않은 상태로 내부 구조만 개선하는 작업이다. 명확한 테스트가 매우 중요하다.
리팩터링을 언제 해야 하는가
리팩터링을 해야 하는 순간은 다음과 같습니다.
코드가 어제보다 더 나빠 보이는 순간
10분 전과 비교해서 더욱 이해하기 어려워졌다면 리팩터링해야 합니다.
코드가 맞지 않아 보일 때
두 가지 기능이 실제로는 하나로 묶여야 한다는 사실을 발견했을 때.
잘못되었다는 느낌이 들 때
감정이 아니라 실제 냄새(code smell)를 의미합니다.
대표적인 리팩터링 트리거
중복된 코드 발견(DRY 위반)
읽기 어려운 설계
지나치게 복잡한 로직
더 나은 구조가 보이는 경우
이때에는 지금 바로 수정하는 편이 장기적으로 이득입니다.
리팩터링을 해야 하는 이유
더 이상 유효하지 않은 지식 때문
요구는 변합니다. 그 순간까지 유효했던 코드가 더 이상 문제를 해결하지 못할 수 있습니다.
실제 사용 사례와의 괴리
사람들이 시스템을 실제로 사용하는 방법은 예상과 다를 수 있습니다. 필요 없는 기능이 되고, 반대로 예상보다 중요해지는 기능도 생깁니다.
성능 개선
전체 시스템의 성능 병목을 해결하기 위해 구조 개선이 필요할 수 있습니다.
테스트 통과 여부
리팩터링은 테스트가 있는 환경에서 더욱 안전합니다. 리팩터링 전/후 비교를 통해 코드가 망가지지 않았음을 검증할 수 있습니다.
현실 세계에서의 복잡한 문제들
많은 개발자들이 “코드가 돌아가니 리팩터링할 필요 없다”라고 말합니다. 그러나 이는 “증상이 나쁘지 않으니 치료가 필요 없다”와 같은 오류입니다.
코드의 품질 저하는 시간이 지나면서
유지보수 비용 증가
일정 지연
장애 가능성 증가
팀 생산성 하락 으로 이어집니다.
의사에게 종양을 제거하라고 조언받았을 때 바로 수술하는 것처럼, 소프트웨어에서도 증상이 심각해지기 전에 조기에 리팩터링해야 합니다.
리팩터링은 어떻게 하는가
리팩터링의 본질은 재설계입니다. 코드 전체를 다시 작성하는 것이 아니라, 더 나은 구조를 향해 작은 단계를 쌓아 나갑니다.
1. 기능 추가와 리팩터링은 동시에 하지 않는다
두 작업을 동시에 하면 위험하다.
2. 단단한 테스트가 있는지 먼저 확인한다
테스트 없이는 리팩터링의 안전성을 보장할 수 없다.
3. 작은 단계로 나누어 신중하게 진행한다
클래스 하나씩, 메서드 하나씩 분리하고 개선한다. 큰 변화는 위험하다. 작은 단위에서 변화를 누적해야 한다.
이 방식은 마틴 파울러의 리팩터링 원칙과도 일치합니다.
자동 리팩터링
IDE의 자동 리팩터링 기능은
이름 변경
메서드 추출
클래스 이동 등을 매우 안전하게 수행합니다. 이 도구들을 활용하면 리팩터링을 실수 위험 없이 진행할 수 있습니다.
리팩터링의 주의점
외부 API 형태를 함부로 바꾸지 말 것
빌드를 깨뜨리지 말 것
작은 냄새라도 방치하지 말 것
지금 당장 하지 않으면 나중에 더 큰 비용으로 돌아온다는 점을 기억할 것
Topic 41. 테스트로 코딩하기
요약
이 장은 “테스트는 버그를 찾기 위한 것이 아니다”라는 선언에서 출발합니다. 테스트는 코드를 더 잘 이해하고, 설계를 개선하며, API를 명확하게 드러내고, 우리가 진짜 원하는 기능을 파악하게 하는 핵심 도구입니다. 단순한 검증 행위가 아니라 더 좋은 코드를 만들기 위한 사고 과정이라는 점을 강조합니다.
또한 테스트를 작성하기 전에 “테스트를 어떻게 만들지, 무엇을 테스트할지”를 충분히 고민해야 함을 설명합니다. 테스트가 코드를 이끌기도 하고, 테스트가 API 설계를 유도하기도 하며, 테스트 우선 개발(TDD)의 목적을 오해하지 말아야 한다는 메시지를 제공합니다.
테스트는 버그를 찾기 위한 것이 아니다
테스트의 목적은 단순히 버그를 잡는 것이 아닙니다. 테스트는 다음을 가능하게 합니다.
생각을 정리하고 요구사항을 명확하게 이해한다.
코드가 진짜 해야 하는 일을 정의한다.
API가 어떤 식으로 사용될지 외부 시각을 통해 점검한다.
코드 구조를 재설계할 기회를 제공한다.
테스트에 대해 생각하기
새로운 코드를 작성할 때, 당장 구현부터 들어가는 것이 아니라 “무엇을 테스트해야 하는지” 먼저 고민해야 합니다.
예를 들어 ‘열렬한 시청자 목록 조회’ 기능이 있다고 할 때, 단순히 DB에서 조회하는 코드부터 작성하는 것이 아니라,
어떤 테스트 데이터가 필요한가?
어떤 필드를 기준으로 조회해야 하는가?
API 사용자는 어떤 인터페이스를 기대하는가?
이런 질문들이 코드 작성 이전에 존재해야 합니다.
테스트가 코딩을 이끈다
테스트를 작성하다 보면 API 설계가 자연스럽게 바뀝니다. 이는 매우 긍정적인 신호입니다.
예시로 제시된 코드는 테스트를 작성하면서 메서드 시그니처가 변경되었습니다.
Kotlin 변환 예시
테스트가 API를 바깥에서 바라보게 하므로, 어색한 인터페이스는 자연스럽게 개선됩니다.
테스트 우도 개발(TDD)
TDD의 핵심은 “테스트를 먼저 작성한다”가 아니라 한 번에 한 걸음씩 문제를 해결하는 사고 방식입니다.
TDD 사이클은 다음과 같습니다.
추가하고 싶은 작은 기능을 정한다.
그 기능을 구현하기 위한 실패하는 테스트를 만든다.
테스트를 통과하는 최소한의 코드를 작성한다.
필요하면 리팩터링한다.
테스트가 문제 해결을 한 단계씩 유도하도록 만드는 것이 TDD의 본질입니다.
TDD의 목적을 이해해야 한다
TDD는 모든 상황에서 최고의 방법이 아니며, “전체 코드를 한 번에 정확히 이해하기 어려울 때” 특히 유용합니다.
테스트를 먼저 작성하면 다음을 얻습니다.
문제를 작은 단위로 쪼개는 능력
코드의 의도를 명확하게 표현
불필요한 구현을 피함
리팩터링 시 신뢰할 수 있는 안전망 확보
계약을 지키는지 테스트하기
테스트는 함수의 “계약(Contract)”을 검증하는 역할도 합니다.
예시: 제곱근 함수 sqrt(x)의 계약
입력 조건
x ≥ 0
출력 조건
반환된 숫자의 제곱이 입력값과 오차 ε 이내여야 한다
Kotlin 예시
이렇게 테스트는 “함수가 약속한 바를 지키는지” 검증하고, 이를 통해 모듈 간 관계를 안정적으로 유지할 수 있습니다.
테스트 문화
결국 우리가 만들 모든 소프트웨어는 테스트됩니다. 문제는 누가 테스트하느냐입니다.
개발자가 테스트하면 빠르고, 의도한 방향으로 테스트할 수 있다.
사용자가 테스트하게 되면 버그가 서비스에 그대로 노출된다.
따라서 팀은 테스트를 “선행되는 작업”으로 바라봐야 하며, “나중에 테스트한다”는 말은 결국 “테스트하지 않는다”와 같습니다.
테스트는 프로그래밍의 일부다
테스트는 개발 과정의 별도 활동이 아니라 설계, 코딩, 리팩터링과 동등한 프로그래밍의 한 조각입니다.
이 장의 핵심 메시지는 다음과 같습니다.
테스트는 코드 품질을 위해 하는 “추가 작업”이 아니라 개발의 일부다.
테스트는 사고를 정리하고 더 나은 구조로 안내한다.
테스트를 하지 않으면 최종적으로 사용자들이 테스트하게 된다.
마무리: 여러분의 테스트는 여러분의 사고를 드러낸다
테스트는 단순한 검증 도구가 아니라 코드를 이해하고 설계하며 발전시키는 가장 강력한 수단입니다.
좋은 테스트는 좋은 사고에서 나오고, 좋은 사고는 결국 좋은 코드를 만듭니다.
Topic 42. 속성 기반 테스트
요약
속성 기반 테스트(Property-Based Testing)는 단순한 예시 입력을 넣고 결과를 확인하는 기존 단위 테스트와 달리, 코드가 지켜야 하는 계약(계약, 불변식, 속성)을 다양한 입력에 대해 자동으로 검증하는 방식입니다. 이 방식은 특정한 잘못된 가정이나 경계 상황을 놓쳤을 때, 인간이 미처 예상하지 못한 오류를 발견하는 데 매우 효과적입니다.
속성 기반 테스트는 우리가 작성한 코드가 단순히 "예상한 몇 가지 경우"만 통과하는 것이 아니라, 전체적인 규칙을 만족하는지 자동으로 탐색하고 검증합니다. 또한 설계 측면에서도 코드가 가져야 할 속성을 명확하게 정의하도록 도와주므로 더 단단한 구조를 만들게 해줍니다.
믿으라, 하지만 확인하라
러시아 속담처럼, 속성 기반 테스트는 주어진 입력 몇 개만 믿지 말고 **“코드가 가져야 할 성질을 확인하라”**는 원칙으로 작동합니다.
단위 테스트만으로는 우리가 만든 코드의 잘못된 가정이 그대로 테스트 코드에도 스며들 수 있습니다. 그러나 속성 기반 테스트는 다양한 무작위 데이터로 코드의 일반적인 성질을 검증합니다.
계약, 불변식, 속성
속성 기반 테스트의 중심 개념은 아래와 같습니다.
계약(Contract) 함수가 지켜야 할 전제조건(pre-condition)과 후조건(post-condition).
불변식(Invariant) 항상 참이어야 하는 조건. 예: 리스트를 정렬한 뒤 길이는 동일해야 한다.
속성(Property) 계약과 불변식을 테스트에서 활용 가능한 형태로 정의한 것.
예를 들어 정렬 함수의 속성은 다음과 같습니다.
정렬 후 길이는 동일해야 한다.
모든 원소는 원래 리스트에 존재해야 한다.
정렬된 리스트는 항상 오름차순이어야 한다.
Kotlin 예시
속성 기반 테스트 프레임워크 개념
책에서는 Python의 Hypothesis 예시를 사용하지만, Kotlin에서는 다음과 같은 대표적인 프레임워크가 있습니다.
KotlinTest / Kotest property testing
jqwik (JUnit 기반)
핵심은 같으며, 테스트 프레임워크가 자동으로 다양한 입력을 생성해 속성을 검증한다는 점입니다.
Kotest 예시로 다시 구성하면:
잘못된 가정 찾기
책의 예시는 창고(Warehouse) 재고 시스템으로 오류를 찾아내는 과정을 보여줍니다.
전통적 단위 테스트는 다음처럼 몇 가지 경우만 확인합니다.
단위 테스트는 잘 통과하지만, 속성 기반 테스트는 문제를 드러냅니다.
예: 재고를 가져간 수량 + 남은 재고 = 원래 재고 이어야 한다는 속성.
속성 기반 테스트가 오류를 드러내는 방식
속성 기반 테스트는 다음과 같은 상황을 조합하며 문제를 드러냅니다.
다양한 아이템 이름
다양한 수량
재고보다 많은 수량을 요청
존재하지 않는 아이템 입력
반복된 요청 시 재고 수량 누적 문제
결국 테스트는 “모자(item='모자') 수량=3”이라는 특정 조합에서 버그를 찾아내고, 코드가 재고 감소를 잘못 처리했다는 사실을 알려줍니다.
이를 바탕으로 코드를 수정하면 모든 속성 기반 테스트가 통과합니다.
속성 기반 테스트는 설계에도 도움을 준다
속성 기반 테스트는 코드의 본질적인 규칙을 찾게 해줍니다.
예를 들어:
어떤 입력에서도 변하지 않아야 하는 조건이 무엇인가?
어떤 경우는 반드시 예외가 발생해야 하는가?
결과의 일관성을 어떻게 보장해야 하는가?
이러한 사고 과정은 코드의 설계를 더 단단하게 만들어주고, API의 계약을 명확하게 하는 데 크게 기여합니다.
단위 테스트와 속성 기반 테스트 비교
테스트 범위
특정 입력
매우 다양한 입력
관점
예시 기반
규칙 기반
목표
특정 케이스 검증
코드의 보편적 성질 증명
유지보수
더 많은 테스트 필요
적은 테스트로 더 많은 경우 커버
설계 개선
제한적
구조적 개선 유도
둘 중 하나가 더 우월한 것은 아니며 서로 보완적인 도구입니다.
Topic 43. 바깥에서는 안전에 주의하라
우리는 코드 내부의 완결성과 안정성에 대해 이야기할 때 종종 이렇게 생각한다. “스파이나 해커를 상대할 일은 거의 없으니까, 검증은 적당히 해도 되지 않을까?” 하지만 실제 세계는 전혀 그렇지 않다. 외부 환경은 우리가 예상하는 것보다 훨씬 더 위험하고, 훨씬 더 교묘하다.
최근 뉴스만 봐도 대규모 데이터 유출, API 공격, 인증 우회, 피싱, 랜섬웨어 등 도처에서 사고가 터지고 있다. 공격자들은 특별히 똑똑하거나 천재가 아니다. 그저 우리가 실수하길 기다릴 뿐이다. 따라서 개발자는 기능을 만드는 것만큼이나 “위험을 줄이는 노력”을 꾸준히 해야 한다.
이 글에서는 실용주의 개발자가 반드시 고려해야 하는 보안 관점의 사고 방식과 실제 프로덕션 개발에서 적용할 수 있는 핵심 원칙들을 정리한다.
나머지 90%를 고려하라
개발을 하다 보면 “된다!”와 “왜 안 되지?” 사이를 끊임없이 오간다. 문제가 해결되고 나면 ‘이제 거의 완성된 것 같네’라는 생각이 들기도 한다.
하지만 진짜 중요한 건 그 이후다.
코드가 동작하기 시작했다고 해서 모든 문제가 끝난 것이 아니다. 남은 10%가 아니라, 남은 90%를 고민해야 한다.
그 90%는 다음과 같은 질문들이다.
외부 입력이 들어올 때 어떤 위험이 있을까?
잘못된 값이 들어오면 어떤 피해가 발생할까?
단위 테스트는 충분한가? 경계값은 확인됐는가?
이 기능이 다른 시스템과 연결될 때 어떤 취약점이 생길까?
즉, 정상 동작만이 아니라 비정상 시나리오 전체를 고려해야 한다.
실용적 보안의 다섯 가지 원칙
실무에서 쓸 수 있는 가장 중요한 보안 원칙 다섯 가지는 다음과 같다.
공격 표면을 최소화하라.
최소 권한 원칙을 적용하라.
민감 정보를 암호화하라.
출력 데이터를 안전하게 처리하라.
보안 업데이트를 즉시 반영하라.
각 항목을 실제 개발 시 어떻게 적용할 수 있는지 풀어보자.
공격 표면을 최소화하라
공격 표면(Attack Surface) 이란, 공격자가 시스템에 접근하거나 데이터를 넘겨줄 수 있는 모든 통로다.
노출된 API
불필요하게 열린 포트
외부와 직접 통신하는 기능
인증 없이 접근 가능한 엔드포인트
복잡한 시스템일수록 공격 표면은 넓어진다.
코드가 단순하고 작을수록 해킹은 어려워진다. 개발 과정에서 기능을 줄이거나, 리팩터링으로 경로를 단순화하는 것만으로도 취약점을 크게 줄일 수 있다.
입력 데이터는 항상 “적대적”이라고 가정하라
외부에서 들어오는 데이터는 어떤 것도 믿어서는 안 된다. 입력값이 웹 화면에서 온다고 해도, API 클라이언트에서 왔다고 해도, 심지어 내부 서버 간 통신이라 해도 모두 의심하는 것이 기본값이다.
다음은 Ruby 예제지만 원칙은 언어에 상관없이 동일하다.
단 한 번의 문자열 삽입만으로도 시스템 명령어가 실행될 수 있다. 입력값을 Sanitizing(정제)하는 것은 선택이 아니라 필수이다.
인증되지 않은 사용자에게 시스템을 절대 노출하지 말라
인증 없이 누구나 호출할 수 있는 API는 “나를 공격해 주세요”라고 직접 말하는 것과 같다.
관리자 기능
내부 데이터 조회 기능
디버그용 엔드포인트
파일 업로드 기능
이런 것들이 인증 없이 외부에 노출될 경우, 몇 분 만에 시스템 전체를 장악당할 수 있다.
출력을 안전하게 처리하라
입력만 위험한 것이 아니다. 출력도 공격 매개체가 될 수 있다.
서버 로그에 민감 정보 출력
브라우저로 전달되는 HTML에 사용자 입력을 그대로 반영
외부 시스템에게 전달되는 데이터에 검증 누락
예를 들어 사용자의 비밀번호를 에러 메시지나 로그에 담아 버리면 의도하지 않은 형태로 확산되며 보안 사고를 유발할 수 있다.
최소 권한 원칙
“필요한 만큼만 권한을 부여하라.”
이 규칙 하나만 제대로 지켜도 대부분의 보안 사고는 예방할 수 있다.
root/administrator로 실행하지 않기
서비스별 계정 분리
DB 권한 최소화(READ ONLY, 특정 Column 제한 등)
IAM Role 최소 권한 정책 적용
권한이 많은 서비스는 공격자가 장악했을 때 피해가 크다. 따라서 권한은 최소한, 가장 제한된 수준이어야 한다.
민감 정보는 반드시 암호화하라
평문 비밀번호 저장은 더 이상 실수라고 부를 수 없는 수준의 치명적 행위다.
비밀번호, 토큰, 세션ID, API Key
금융 정보, 주민번호 등 PII
SSH 키, OAuth Secret
이 모든 데이터는 저장 시 반드시 암호화하거나 해싱해야 한다.
또한 해시 알고리즘은 bcrypt, scrypt, Argon2 등 현대적인 Key Stretching 알고리즘을 사용해야 한다.
보안 업데이트를 제때 적용하라
많은 보안 사고는 “이미 패치된 취약점” 때문에 발생한다.
오래된 라이브러리
패치되지 않은 OS
EOL된 프레임워크
보안 이슈가 존재하는 오픈소스
이런 요소들은 공격자에게 너무나 쉬운 진입점이 된다.
업데이트는 비용이 아니라 보험이다.
바깥에서는 항상 안전에 주의하라
개발 환경은 점점 복잡해지고 있다. 우리는 이제 더 이상 코드만 잘 작성한다고 안전한 시스템을 만들 수 없다.
API는 인터넷에 연결되고
데이터는 외부로 이동하며
사용자 입력은 예측할 수 없고
시스템은 크고 복잡하며
공격자는 우리의 실수를 기다린다
이 모든 현실을 고려할 때, 개발자가 보안을 신경 쓰지 않는다는 것은 말이 되지 않는다.
“바깥에서는 안전에 주의하라”는 말은 외부 세계는 항상 위험하고, 개발자는 그 위험을 가정한 채 코드를 작성해야 한다는 뜻이다.
Topic 44. 이름 짓기
좋은 이름은 코드를 이해하는 시간을 줄이고, 협업을 매끄럽게 만드는 가장 강력한 개발 도구 중 하나입니다. 실용주의 프로그래머는 이름을 “모든 것의 시작”이라고 말합니다. 이름은 단순한 라벨이 아니라, 코드의 역할·의도·맥락을 전달하는 중요한 인터페이스입니다.
이 글에서는 이름이 왜 중요한지, 이름을 어떻게 잘 지을 수 있는지, 그리고 실무에서 이름을 개선할 때 어떤 전략을 사용해야 하는지 정리합니다.
이름이 갖는 힘
프로그래밍에서는 새로운 개념을 만들 때마다 이름을 붙입니다.
애플리케이션
서비스
도메인 객체
함수와 변수
DTO, 이벤트, 모듈
이때 이름이 명확하지 않으면 개념 자체가 모호해집니다. 이름이 불명확한 코드는 개발자에게 혼란을 주고, 논의를 어렵게 만들고, 마침내 시스템 전체 품질을 떨어뜨립니다.
이름을 짓는 일은 문제를 해결하는 사고 과정의 일부입니다. 좋은 이름을 만들려면, 먼저 “이 개념이 무엇을 하는가”를 명확히 해야 합니다.
인지적 관점에서 본 이름
우리 뇌는 단어를 읽는 활동보다 훨씬 더 빠르게 색과 형태를 인지합니다. 색상 Stroop Test는 이를 잘 보여줍니다.
하얀색 회색 검은색 하얀색
위 단어를 읽을 때, 색과 글자가 일치하지 않으면 인지적 혼란이 생깁니다. 이처럼 이름이 하는 일과 이름이 의미하는 바가 다르면 코드 이해에 큰 장애가 됩니다.
나쁜 이름의 예: user? amount? percent?
다음 변수 이름을 보겠습니다.
여기서 user는 실제로 “사용자 객체”인가요?
혹은 “인증 결과”인가요? “토큰”? “자격 검증 여부”?
이런 애매함은 코드 해석에 부담을 줍니다.
함수 이름도 마찬가지입니다.
이 함수는 “퍼센트만큼 차감한다”는 의미를 갖지만,
코드만 봐서는 amount가 금액인지 할인율인지 모호합니다.
다음처럼 바꾸는 것이 훨씬 명확합니다.
이름은 도메인을 반영해야 한다
도메인 모델을 정확히 이해하면 이름은 자연스럽게 좋아집니다.
예를 들어 이커머스에서 “고객(user)”보다 “구매자(buyer)”가 더 정확할 수 있습니다. “문자열(data)”보다 “주문ID(orderId)”는 더 구체적입니다.
즉, 오래된 습관적 용어가 아니라 도메인의 실제 언어를 코드에 반영해야 합니다.
함수 이름은 역할을 드러내야 한다
예를 들어 피보나치 수열을 계산하는 함수를 구현한다고 할 때,
이라는 이름은 짐작은 가능하지만 대놓고 의도를 드러내지는 않습니다.
다음은 더 명확한 형태입니다.
사용하는 사람은 함수의 목적을 더 명확히 이해할 수 있습니다.
규칙은 없지만 원칙은 존재한다
이름 짓기에는 절대적 규칙이 없지만, 다음 원칙은 늘 유효합니다.
좋은 이름은 다음의 특징을 갖는다.
짧지만 의미가 충분해야 한다.
역할과 의도를 드러낸다.
팀의 용어와 일관성을 유지한다.
모호한 줄임말을 피한다.
코드 맥락에서 자연스럽다.
반대로, 피해야 할 이름은 다음과 같다.
data, info, item 같은 범용 이름
i, j, k 같은 의미 없는 임시 변수(특수한 맥락 제외)
value, obj 같은 모호한 용어
내부 구현을 드러내는 이름
문화적 컨텍스트를 존중하라
언어와 환경은 이름 짓기에 영향을 준다. 예를 들어 C 언어에서는 한 시대 동안 반복문의 변수로 i, j, k를 널리 사용했다. 반면 현대적인 언어에서는 더 명확한 이름을 선호하는 문화가 자리 잡았다.
Python은 snake_case, Java/Kotlin은 camelCase를 선호한다. 심지어 JSON은 유니코드를 포함한 필드명을 사용할 수 있지만, 실제로는 대부분 사람이 읽기 쉬운 형태로 제한한다.
결론적으로, 이름은 사용하는 언어와 팀의 문화에 맞추어야 한다.
일관성: 이름의 질서를 만든다
이름 짓기의 가장 중요한 원칙 중 하나는 일관성이다. 한 코드베이스에서 같은 의미를 갖는 단어는 항상 같은 이름으로 표현해야 한다.
예를 들어,
get → fetch → retrieve → load 등 섞어 쓰지 말 것
user → buyer → customer를 혼동하지 말 것
주문ID → orderId로 통일할 것
일관되지 않으면 개발자가 코드를 읽으며 계속 다시 추론해야 한다. 이는 업무 효율을 크게 떨어뜨린다.
이름 바꾸기는 어렵지만 반드시 해야 한다
코드에서 가장 고치기 어려운 문제 두 가지가 있다.
캐시 무효화
이름 짓기
하나 차이 오류
이름을 잘못 지으면 리팩터링할 때 고통을 겪는다. 특히 다음과 같은 경험은 많은 개발자가 겪는다.
기존 코드에
getData()라는 함수가 있다.아무도 이 함수가 무엇을 반환하는지 모른다.
이름 때문에 사용처마다 의미를 다시 유추해야 한다.
이럴 때는 과감하게 이름을 바꿔야 한다. 이름 변경은 리팩터링의 핵심 중 하나다.
다행히 현대 IDE는 안전한 rename 기능을 제공한다. 따라서 필요하다면 이름을 자주, 적극적으로 수정해야 한다.
결론: 이름을 잘 지어라. 필요하면 바꿔라
이름은 코드의 품질을 결정하는 가장 강력한 요소다. 잘못된 이름은 버그보다 더 큰 문제를 만든다.
그러므로:
이름이 모호하면 즉시 개선하라.
코드를 읽는 사람의 입장에서 이름을 검토하라.
도메인 언어를 반영하라.
팀 전체에서 용어를 통일하라.
이름이 좋아지면 코드가 좋아지고, 코드가 좋아지면 팀이 좋아진다.
Last updated