한 줄 정의
소프트웨어 엔지니어링은 흐르는 시간 위에서 순간순간의 프로그래밍을 모두 합산한 것입니다(Software engineering is programming integrated over time).
쉽게 말하면
프로그래밍이 “지금 이 코드를 동작하게 만드는 일”이라면, 소프트웨어 엔지니어링은 “이 코드가 수명이 다할 때까지 동작하게 유지하는 일”입니다.
정육면체는 정사각형이 아니고 거리는 속도가 아니듯, 소프트웨어 엔지니어링은 프로그래밍이 아닙니다.
프로그래밍에 시간(time) 이라는 차원이 하나 더해지면서, 평면이던 문제가 입체로 바뀝니다. 코드를 작성하는 행위는 같아 보여도, 그 코드가 1시간 살다 사라질지 20년을 살아남을지에 따라 완전히 다른 게임이 됩니다.
왜 중요한가?
프로그래밍과 소프트웨어 엔지니어링을 가르는 핵심 축은 세 가지입니다.
- 시간(time) 과 변경(change) — 코드가 살아 있는 동안 변경을 감당할 수 있는가
- 규모 확장(scale) 과 효율성 — 조직과 코드베이스가 커질 때 비용이 선형 이하로 증가하는가
- 실전에서의 트레이드오프(trade-offs at play) — 불완전한 정보 위에서 결과에 큰 영향을 주는 결정을 내리는 능력
이 차이를 인식하지 못하면 한쪽 영역에서 훌륭한 도구가 다른 영역에서는 독이 된다는 사실을 놓치게 됩니다. 며칠만 쓸 스크립트에 지속적 배포(CD)나 유의적 버전(SemVer)을 붙이는 것은 과잉이고, 반대로 20년 갈 시스템을 “당장 돌아가니까” 식으로 짜는 것은 미래의 자신에게 빚을 지우는 일입니다.
백엔드 관점에서 이 구분은 곧바로 실무 의사결정으로 연결됩니다. 일회성 마이그레이션 배치 스크립트와 수년간 운영할 결제 서비스에 같은 잣대를 들이대면 안 된다는 것, 이게 1장의 출발점입니다.
핵심 내용
시간과 변경 — 코드의 예상 수명
코드의 성격을 판단하는 가장 좋은 질문은 “이 코드의 예상 수명은?” 입니다. 여기서 수명은 단순 구동 수명(execution lifetime)이 아니라 유지보수 수명(maintenance lifetime), 즉 이 코드가 언제까지 개발·유지보수될지를 말합니다.
수명이 짧은 코드는 시간의 영향을 거의 받지 않습니다. 초심자의 연습 코드, 한 번 실행하고 버리는 스크립트가 여기에 속합니다. 하지만 십 년 이상 살아남는다면 직간접적으로 의존하는 거의 모든 것(외부 라이브러리, 기반 프레임워크, 운영체제, 컴파일러)이 처음과 달라집니다.
지속 가능성(sustainability)
소프트웨어의 기대 생애 동안 요구되는 모든 가치 있는 변경에 대응할 역량 이 있다면 그 프로젝트는 지속 가능(sustainable) 하다고 말합니다.
중요한 것은 “실제로 모든 변경을 다 한다”가 아니라 “할 수 있는 역량을 갖췄다”는 점입니다. 가치가 충분치 않거나 더 중요한 일이 있어서 특정 변경을 미루기로 선택하는 것은 괜찮습니다. 문제는 그 변경을 할 능력 자체가 없는 경우입니다. 그건 “그 변경이 영영 필요 없기를 바라는 위험한 베팅”입니다.
그림 1-1: 수명과 업그레이드 중요도의 관계
업그레이드 중요도는 기대 수명에 따라 완만하게 가다가 어느 지점에서 급격히 솟구치는 계단형 전환 을 보입니다. 이 전환은 일회성 프로그램과 수십 년 가는 프로젝트 사이 어딘가에서 일어나며, 구글의 경험상 대체로 5~10년 사이입니다.
전환점을 넘긴 뒤 “첫 대규모 업그레이드”는 특히 고통스럽습니다. 이유는 세 가지입니다.
- 해본 적 없는 작업이라 숨어 있던 가정들이 수면 위로 드러납니다.
- 담당 엔지니어가 이런 작업을 경험해본 적이 없을 가능성이 큽니다.
- 점진적 업그레이드가 아니라 수년 치 업그레이드를 한 번에 몰아서 해야 하므로 작업 규모가 큽니다.
그래서 많은 회사가 첫 업그레이드의 고통을 겪은 뒤 “다시는 시도하지 않겠다”며 기존 코드를 버리거나 업그레이드를 포기합니다. 하지만 고통을 회피하는 것보다 고통을 덜어줄 방법 을 찾는 편이 합리적일 때가 많습니다.
하이럼의 법칙(Hyrum’s Law)
API 사용자가 충분히 많다면 API 명세에 적힌 내용은 중요하지 않습니다. 시스템에서 눈에 보이는 모든 행위(동작)를 누군가는 이용하게 될 것이기 때문입니다.
이것이 “동작한다(it works)“와 “유지보수 가능하다”를 가르는 가장 중요한 요인입니다. 정식 명칭은 ‘암시적 의존성 법칙(The Law of Implicit Dependencies)‘입니다.
API 소유자가 인터페이스를 아무리 명확하게 명세해도, 사용자는 명세에 없는 관찰 가능한 동작까지 의존하게 됩니다. 그 의존이 널리 퍼지면 명세상으로는 자유로워야 할 변경조차 막히게 됩니다. 개념적으로 엔트로피 와 유사합니다. 모든 관측 가능한 행위는 시간이 흐르면 누군가의 의존 대상이 됩니다.
핵심 함의는 이것입니다. 엔트로피가 절대 줄지 않는다고 해서 효율을 포기하지 않듯, 하이럼의 법칙이 적용된다고 해서 명세를 잘 쓰려는 노력이나 계획을 포기해서는 안 됩니다. 문제를 완벽히 제거할 수는 없어도 완화할 수는 있습니다.
사례: 해시 순서
해시 기반 집합에 원소 다섯 개를 넣고 출력하면 순서는 삽입 순서와 무관하게 나옵니다.
>>> for i in {"apple", "banana", "carrot", "durian", "eggplant"}: print(i)
...
durian
carrot
apple
eggplant
banana해시 테이블의 순서는 명세되지 않은 구현 세부사항입니다. 그런데 누군가는 “내 컨테이너가 항상 같은 순서로 나온다”고 가정한 코드를 작성합니다. 심지어 직렬화한 값을 RPC로 주고받으면서 역직렬화 순서가 일정하다고 가정한 라이브러리를 만들기도 합니다.
이것이 “동작한다(it works)“와 “옳다(it is correct)” 의 차이를 보여주는 기초적인 예입니다. 수명이 짧으면 이런 의존은 문제가 안 됩니다. 하지만 긴 프로젝트에서는 위험 요인입니다. 해시 플러딩(hash flooding, DoS의 일종) 방어나 효율 개선을 위해 해시 순서를 일부러 바꾸는 일이 충분히 일어날 수 있기 때문입니다. 실제로 어떤 언어는 이런 의존을 원천봉쇄하려고 실행할 때마다 해시 순서를 무작위로 바꿉니다.
코드 스타일의 분류
코드를 “수명에 대한 요구사항”으로 분류하면 두 부류로 나뉩니다.
| 스타일 | 의존 대상 | 표현 |
|---|---|---|
| 임시방편적(hacky) / 기발한(clever) | 명세에 없는, 언제든 변할 수 있는 동작 | ”당장 돌아가야 한다” |
| 클린(clean) / 유지보수 가능 | 모범 사례를 따르고 미래에 대비 | ”언제까지고 작동해야 한다” |
어느 쪽을 택할지는 코드의 기대 수명에 크게 좌우됩니다. ‘기발한’이 칭찬으로 느껴지면 프로그래밍이고, 질책으로 느껴지면 소프트웨어 엔지니어링입니다.
‘변하지 않기’를 목표로 하지 않는 이유
“변경은 피할 수 없다”는 가정이 이 모든 논의의 밑바탕입니다. 외부 의존성 없이 순수 C로 짠 코드라면 리팩터링 없이 오래 버틸 수도 있고, 이런 안정성이 C를 택하는 이유 중 하나이기도 합니다.
하지만 대부분의 프로젝트는 기반 기술의 변화를 훨씬 많이 겪습니다. 보안 문제는 모든 기술 제품에서 발견됩니다. 하트블리드(Heartbleed) 패치를 적용하지 않거나 멜트다운·스펙터(Meltdown/Spectre) 같은 추측 실행 취약점을 완화하지 않으면 위험천만한 도박입니다.
목표는 변경을 위한 변경을 삼가되, 변화에 대응할 수 있는 역량은 갖추는 것 입니다. 시스템 관리자가 백업 테이프가 있다는 사실뿐 아니라 실제로 복구를 수행하는 방법과 비용까지 알고 있어야 하는 것과 같습니다. 연습하여 숙달시켜야만 효율과 안정성을 높일 수 있습니다.
규모 확장과 효율성
조직과 코드베이스가 커질 때 비용이 어떻게 증가하느냐가 두 번째 축입니다. 여기서 확장 가능(scalable) 하다는 것은 “규모가 커져도 비용이 선형보다 완만하게 증가한다”는 비공식적 의미입니다. 100의 작업을 반복하는 데 매번 100의 노력이 든다면 그것은 확장 가능하지 않은 것입니다.
핵심 질문은 단순합니다. “조직이 10배 커지면 이 작업도 10배로 많아지는가? 자동화하거나 최적화할 수단이 있는가?” 마지막 답이 ‘아니오’라면 확장성에 문제가 있습니다.
확장하기 어려운 정책 vs 확장 가능한 정책
확장성이 낮은 정책은 인력 충원으로만 버티다가 어느 순간 무너집니다. 반대로 확장 가능한 정책은 책임 소재를 옮기거나 전문가에게 위임하여 규모의 경제를 만듭니다.
| 확장하기 어려운 정책 | 확장 가능한 정책 |
|---|---|
| 마이그레이션 부담을 사용자 각자에게 떠넘김 | 마이그레이션을 담당하는 전문가 그룹이 처리(갈아타기 규칙, Churn Rule) |
| 폐기 시 모든 사용자가 직접 코드를 고침 | 인프라팀이 하위 호환성을 유지하며 직접 업데이트 |
| 개발 브랜치를 늘려 머지를 회피 | 트렁크(trunk) 기반으로 자주 통합 |
| 영향받는 모든 팀을 일일이 찾아다니며 테스트 확인 | 비욘세 규칙(The Beyoncé Rule) |
비욘세 규칙(The Beyoncé Rule)
“인프라를 변경해 서비스가 중단되더라도, 그 문제가 CI 시스템의 자동 테스트에서 발견되지 않는다면 인프라팀의 책임이 아니다.”
친근하게 표현하면 “네가 좋아했다면 CI 테스트를 준비해뒀어야지(If you liked it then you should have put a ring on it)” 입니다. 공통 CI에 추가되지 않은 테스트는 인프라팀이 책임지지 않는다는 뜻입니다.
이 규칙이 없다면 인프라 엔지니어는 코드가 조금이라도 영향받는 모든 팀을 찾아다니며 테스트 방식을 물어봐야 합니다. 엔지니어가 100명일 때는 가능해도 수천 명 규모에서는 감당할 수 없습니다. 백엔드 관점에서 이건 “공유 라이브러리/플랫폼을 운영하는 팀이 소비자 측 회귀를 책임지지 않으려면, 소비자가 자기 테스트를 공통 파이프라인에 올려야 한다”는 운영 원칙으로 직결됩니다.
사례: 컴파일러 업그레이드(2006년)
2006년 구글의 컴파일러 업그레이드는 악몽이었습니다. 앞선 5년간 업그레이드하지 않아 대부분의 엔지니어가 컴파일러를 바꿔본 경험이 없었고, 코드들도 단 하나의 컴파일러 버전만 겪은 상태였습니다. 크고 작은 하이럼의 법칙 문제가 터져 나와 특정 컴파일러 버전에 깊이 의존하게 만들었고, 당시엔 비욘세 규칙도 CI도 없어 변경 영향을 사전에 알기 어려웠습니다.
여기서 코드베이스의 유연성에 영향을 주는 다섯 요인을 발견했습니다.
| 요인 | 내용 |
|---|---|
| 전문성(expertise) | 여러 플랫폼에서 수백 번 업그레이드해본 지식 축적 |
| 안정성(stability) | 더 규칙적으로 릴리스해 릴리스 간 변경량을 줄임 |
| 순응(conformity) | 업그레이드를 겪지 않은 코드가 적도록 규칙적으로 업그레이드 |
| 익숙함(familiarity) | 정기 수행으로 중복 작업을 찾아 자동화(SRE의 삽질 줄이기와 일맥상통) |
| 정책(policy) | 비욘세 규칙처럼 미지의 사용법을 걱정하지 않게 해주는 절차 |
핵심 교훈은 “인프라는 자주 변경할수록 변경하기가 오히려 쉬워진다” 입니다. 코드를 한번 수정해두면 하부 구현의 미묘한 차이에 의존하는 일이 없어지고, 대신 언어나 OS가 보장하는 추상 개념에 기대게 됩니다. 첫 업그레이드 때의 수정량이 압도적으로 가장 많습니다.
원점 회귀(shift left)
개발 과정에서 문제를 일찍 발견할수록 비용이 적게 듭니다.
기능의 워크플로는 시간순으로 개념잡기 → 설계 → 구현 → 리뷰 → 테스트 → 커밋 → 카나리(canary) → 배포 로 흐릅니다. 이 타임라인에서 문제 발견 시점을 왼쪽으로 옮길수록(shift left) 수정 비용이 줄어듭니다.
비용은 배포 단계로 갈수록 지수적으로 치솟습니다. 그래서 정적 검사, 코드 리뷰, CI처럼 그래프 왼쪽에서 버그를 잡는 도구에 투자하는 다층 방어(defense-in-depth) 전략을 구사합니다.
트레이드오프와 비용
세 번째 축은 의사결정입니다. 구글은 “내가 시켰으니까(because I said so)” 방식을 싫어하며, 잘못된 결정이라 판단될 때 찾아가는 에스컬레이션 경로(escalation path) 를 명확히 정의합니다. 목표는 독재가 아니라 합의 도출입니다.
비용은 금액만이 아니다
| 비용 종류 | 예 |
|---|---|
| 금융 비용 | 돈 |
| 리소스 비용 | CPU 시간 |
| 인적 비용 | 엔지니어링 노력 |
| 거래 비용 | 조치를 취하는 비용 |
| 기회 비용 | 조치를 취하지 않는 비용 |
| 사회적 비용 | 선택이 사회 전체에 미치는 영향 |
여기에 현상 유지 편향(status quo bias)과 손실 회피(loss aversion) 같은 인지적 치우침(bias)도 더해집니다. 소프트웨어 엔지니어링처럼 창의적이고 수익성 높은 분야에서는 금융 비용보다 인적 비용이 제한 요소 일 가능성이 큽니다. 엔지니어가 행복하게 일에 집중하도록 해주면 효율이 10~20% 달라집니다.
의사결정의 두 유형
| 유형 | 상황 | 접근 |
|---|---|---|
| 정량화 가능 | CPU·메모리·금액 트레이드오프처럼 측정/추정 가능 | 합의된 환산표로 계산. “메모리 5GB 더 쓰고 CPU 2,000개 줄이기, 인건비·기회비용까지 고려” |
| 정량화 어려움 | ”엔지니어 시간이 얼마나 들지 모르겠다”, 사회적 영향 | 경험·리더십·선례에 의존. 똑같이 중요하지만 관리가 더 어려움 |
핵심은 “모든 것이 측정 가능하거나 예측 가능하지는 않다는 사실을 인정하되, 그런 결정에도 똑같은 우선순위와 관심을 두라” 는 것입니다.
사례로 보는 트레이드오프
- 화이트보드 마커 — 마커를 엄격히 관리하면 막상 회의 때 쓸 마커가 없습니다. 구글은 사물함에 다양한 마커를 비치해 “끊김 없는 브레인스토밍”이 “마커 찾기”보다 중요하다는 균형점을 잡았습니다. 사무용품부터 글로벌 서비스 운영까지 동일한 수준의 비용/이윤 분석을 적용한다는 상징입니다.
- 분산 빌드와 제번스의 역설(Jevons Paradox) — 분산 빌드로 생산성을 끌어올렸지만, 로컬 빌드 시절에는 각자가 신경 쓰던 빌드 최적화에 아무도 관심을 두지 않게 되면서 빌드가 비대해지고 의존성이 넘쳐났습니다. “효율이 좋아지면 자원 소비가 오히려 늘어난다”는 역설입니다. 사소한 트레이드오프조차 예기치 못한 부작용을 낳을 수 있다는 교훈입니다.
- 시간 vs 규모 확장 — “이 라이브러리를 쓸까, 포크(fork)하거나 다시 구현할까?” 수명이 짧으면 포크가 안전하지만, 데이터 구조·직렬화 포맷·네트워크 프로토콜처럼 수명에 종속되지 않는 인터페이스는 포크를 피하는 게 좋습니다.
결정 재고하기와 잘못 인정하기
데이터에 기초해 결정하되, 그 데이터 자체가 시간이 지나면 변한다는 점을 명심해야 합니다. 결정 시점에 가용한 데이터만 쓸 수 있다는 한계가 있으므로, 새 데이터가 나오거나 가정이 무너지면 기존 결정을 재고해야 합니다.
가장 중요한 덕목은 결정권자에게 “잘못을 인정할 권리”가 있다는 점 입니다. 잘못을 인정할 줄 아는 리더가 더 존경받습니다.
비교 / 트레이드오프
프로그래밍 vs 소프트웨어 엔지니어링
graph LR P["프로그래밍<br/>(코드 생산)"] -->|시간| SE1 P -->|협업/규모| SE2 P -->|트레이드오프| SE3 SE1["변경 대응·지속 가능성"] --> SE["소프트웨어 엔지니어링"] SE2["확장 가능한 정책·전문성"] --> SE SE3["불완전한 정보 위의 의사결정"] --> SE
| 구분 | 프로그래밍 | 소프트웨어 엔지니어링 |
|---|---|---|
| 본질 | 코드를 생산하는 즉각적 행위 | 코드를 수명이 다할 때까지 유용하게 관리 |
| 시간 | 단명, 변경 거의 없음 | 장수, 의존성·기술·요구사항 변경에 대응 |
| 참여 인원 | 개인 창작 | 여러 사람·여러 버전이 참여하는 팀 업무 |
| 핵심 관심 | ”지금 동작하는가" | "언제까지고 동작하게 유지 가능한가“ |
| ‘기발함’의 의미 | 칭찬 | 질책 |
단명하는 코드와 장수하는 코드의 수명 차이는 적어도 10만 배 입니다. 수명 축의 양 끝에 똑같은 모범 사례를 적용하는 것은 너무 안일한 생각입니다.
내 생각
-
“이 코드의 예상 수명은?”은 PR마다 던질 질문 입니다. 일회성 백필 스크립트에 CI·SemVer를 붙이면 오버엔지니어링이고, 5년 갈 결제 도메인을 “일단 돌아가니까”로 짜면 미래 비용을 폭증시키는 베팅입니다. 같은 코드도 수명에 따라 정답이 정반대입니다.
-
비욘세 규칙은 플랫폼 팀의 책임 경계 도구 입니다. 사내 공통 라이브러리 운영 시 “소비자가 깨졌다”는 항의에 무한 책임을 지는 대신 “공통 CI에 테스트를 올려라”로 계약을 그으면, 변경 속도와 운영 부담이 동시에 풀립니다.
-
하이럼의 법칙은 곧 API 호환성 비용의 예고 입니다. 응답 필드 순서·에러 메시지 문구·암묵적 정렬처럼 “명세엔 없지만 관찰 가능한 모든 것”이 의존 대상이 되므로, 깨고 싶지 않은 동작은 명세로 못 박고 나머지는 일부러 흔들어(예: 무작위화) 의존을 차단하는 게 현실적입니다.
-
제번스의 역설은 인프라 효율화의 단골 함정 입니다. 빌드·CI를 빠르고 싸게 만들수록 사람들이 더 분별없이 돌려 총 자원 소비가 오히려 늘어납니다. 효율화는 반드시 사용량 변화 모니터링과 함께 가야 합니다.
-
원점 회귀(shift left)는 정적 검사·코드 리뷰 투자 근거 입니다. 배포 후 버그 수정 비용이 지수적으로 치솟으므로, 린트·타입 체크·PR 리뷰·CI는 “있으면 좋은 것”이 아니라 비용 곡선 왼쪽으로 버그를 끌어오는 다층 방어입니다.