Edsger Dijkstra의 구조적 프로그래밍 관점은 규율 있고 단순한 코드가 팀, 기능, 시스템이 커져도 왜 올바르고 유지보수 가능한지를 설명한다.

소프트웨어가 실패하는 이유는 보통 ‘작성할 수 없어서’가 아니다. 일 년 뒤에 아무도 안전하게 변경할 수 없어서 실패한다.
코드베이스가 커질수록 ‘작은’ 수정도 영향을 확산시킨다: 버그 수정이 먼 기능을 깨뜨리고, 새로운 요구가 재작성으로 이어지며, 단순한 리팩터링이 일주일간의 면밀한 조정으로 바뀐다. 어려운 부분은 코드를 추가하는 것이 아니라—주변 모든 것이 바뀌어도 동작을 예측 가능하게 유지하는 것이다.
Edsger Dijkstra는 정확성과 단순성을 일급 목표로 삼아야 한다고 주장했다. 이는 학술적 관점이 아니라 실제 이득이다. 시스템을 추론하기 쉬우면 팀은 불난 집 끄기보다 새로운 것을 구축하는 데 더 많은 시간을 쓸 수 있다.
사람들이 ‘소프트웨어가 확장되어야 한다’고 할 때 보통 성능을 의미한다. 다익스트라의 요점은 다르다: 복잡성도 함께 확장된다.
규모는 다음과 같이 나타난다:
구조적 프로그래밍은 자신이 엄격해지려는 목적이 아니다. 제어 흐름과 분해 방식을 선택해 두 가지 질문에 답하기 쉽게 만드는 것이다:
동작이 예측 가능하면 변경은 위험한 일이 아니라 일상 작업이 된다. 그래서 다익스트라는 여전히 중요하다: 그의 규율은 커져 가는 소프트웨어의 진짜 병목—충분히 이해해서 개선할 수 있는 능력—을 겨냥한다.
Edsger W. Dijkstra(1930–2002)는 신뢰할 수 있는 소프트웨어를 만드는 방법에 대해 프로그래머들의 사고방식을 형성한 네덜란드의 컴퓨터 과학자다. 그는 초기 운영체제 작업을 했고, 그의 이름을 딴 최단 경로 알고리즘을 포함한 알고리즘에 기여했으며—일상 개발자에게 가장 중요한 점으로—프로그래밍은 단지 ‘동작하게 해보는 것’이 아니라 우리가 추론할 수 있는 무언가여야 한다고 주장했다.
다익스트라는 몇몇 예제에 대해 올바른 출력을 만들 수 있는지보다, 우리가 중요한 경우에 대해 올바름을 설명할 수 있는지에 더 관심을 가졌다.
코드가 무엇을 해야 하는지 진술할 수 있다면, 단계별로 그것이 실제로 그렇게 동작한다는 것을 주장할 수 있어야 한다. 이런 사고방식은 자연스럽게 따라가기 쉽고, 리뷰하기 쉽고, 영웅적 디버깅에 덜 의존하는 코드를 만든다.
다익스트라의 글 중 일부는 타협이 없어 보인다. 그는 ‘영리한’ 트릭, 엉성한 제어 흐름, 추론을 어렵게 만드는 코딩 관행을 비판했다. 그 엄격함은 스타일을 규제하려는 것이 아니라 모호함을 줄이려는 것이다. 코드 의미가 명확하면 의도를 두고 논쟁하는 시간이 줄고 동작을 검증하는 시간은 늘어난다.
구조적 프로그래밍은 프로그램을 순차, 선택(if/else), 반복(루프) 같은 소수의 명확한 제어 구조로 구축하는 관행이다. 목적은 간단하다: 프로그램의 경로를 이해하기 쉽게 만들어 자신 있게 설명하고 유지보수하며 변경할 수 있게 하는 것이다.
사람들은 소프트웨어 품질을 ‘빠르다’, ‘아름답다’, ‘기능이 풍부하다’고 표현하지만, 사용자는 정확성을 다르게 경험한다: 앱이 그들을 놀라게 하지 않을 것이라는 조용한 신뢰감이다. 정확성이 있으면 아무도 신경 쓰지 않는다. 없으면 다른 모든 것이 무의미해진다.
“지금 동작한다”는 보통 몇 가지 경로를 시도해 기대한 결과를 얻었다는 의미다. “계속 동작한다”는 리팩터, 새로운 통합, 더 높은 트래픽, 새 팀원이 코드를 건드려도 의도한 대로 동작한다는 의미다.
기능이 ‘지금 동작’하더라도 여전히 취약할 수 있다:
정확성은 이러한 숨은 가정을 제거하거나 명시하는 것이다.
작은 버그는 소프트웨어가 커지면 거의 그대로 작게 남지 않는다. 잘못된 상태 하나, 한 칸의 오프바이원, 명확하지 않은 오류 처리 규칙 하나가 새로운 모듈로 복제되고, 다른 서비스에 래핑되고, 캐시되고, 재시도되거나 ‘우회’로 덮인다. 시간이 지나면 팀은 “무엇이 참인가?” 대신 “보통은 어떻게 되나?”를 묻기 시작한다. 그때 사고 대응은 고고학이 된다.
증폭의 원인은 의존성이다: 작은 잘못 동작이 많은 다운스트림 잘못 동작으로 번지고, 각각은 부분적 수정으로 덮인다.
명확한 코드는 의사소통을 개선하므로 정확성을 높인다:
정확성은: 우리가 지원한다고 주장하는 입력과 상황에 대해 시스템이 일관되게 약속한 결과를 만들어내고, 불가능한 경우에는 예측 가능하고 설명 가능한 방식으로 실패하는 것이다.
단순성은 코드를 ‘귀엽게’ 또는 극단적으로 최소화하거나 영리한 한 줄로 만들려는 게 아니다. 그보다는 동작을 예측하고 설명하며 두려움 없이 수정할 수 있게 만드는 것이다. 다익스트라는 단순성이 프로그램을 추론하기 쉽게 하기 때문에 중요하다고 보았다—특히 코드베이스와 팀이 커질 때.
단순한 코드는 동시에 적은 수의 아이디어만 움직이게 한다: 명확한 데이터 흐름, 명확한 제어 흐름, 명확한 책임. 독자가 많은 대체 경로를 머릿속으로 시뮬레이션하게 만들지 않는다.
단순성은 다음이 아니다:
많은 시스템이 도메인 자체의 복잡성 때문이 아니라 우발적 복잡성 때문에 변경하기 어려워진다: 상호작용하는 플래그들, 제거되지 않는 특수 패치, 이전 결정을 우회하기 위해 생긴 레이어들.
각 예외는 이해에 대한 세금이다. 그 비용은 누군가 버그를 고치려 할 때 한 영역의 변경이 여러 다른 영역을 미묘하게 깨뜨린다는 것을 발견할 때 드러난다.
설계가 단순하면 진전은 꾸준한 작업에서 온다: 리뷰 가능한 변경, 작은 diff, 적은 긴급 수정. 팀은 모든 역사적 엣지 케이스를 기억하거나 새벽 2시에 스트레스 속에서 디버깅할 수 있는 ‘영웅’ 개발자가 필요 없다. 대신 시스템이 정상적인 인간의 주의 한도를 지원한다.
실용적 테스트: 예외(“단, …”, “다만 …인 경우에만”, “특정 고객에만…”)를 계속 추가하고 있다면 우발적 복잡성을 쌓고 있을 가능성이 높다. 행동 분기를 줄이는 해결책을 선호하라—한 가지 일관된 규칙이 처음 생각보다 조금 더 일반적이더라도 다섯 개의 특수 사례보다 낫다.
구조적 프로그래밍은 실행 경로를 따라가기 쉽게 코드를 작성하라는 단순한 아이디어지만 결과는 크다: 대부분 프로그램은 순차(sequence), 선택(selection), 반복(repetition) 세 가지 구성요소로 만들 수 있고 얽힌 점프에 의존할 필요가 없다.
if/else, switch).for, while).제어 흐름이 이러한 구조로 구성되면 보통 파일을 ‘위에서 아래로’ 읽으면서 프로그램이 무엇을 하는지 설명할 수 있다.
구조적 프로그래밍이 보편화되기 전에는 많은 코드베이스가 임의의 점프(goto 스타일)에 크게 의존했다. 문제는 점프 자체가 항상 악이라는 게 아니라 제한 없는 점프가 예측하기 어려운 실행 경로를 만든다는 것이다. 그러면 “우리는 어떻게 여기로 왔나?” “이 변수의 상태는 무엇인가?” 같은 질문이 생기는데 코드가 명확히 답해주지 않는다.
명확한 제어 흐름은 사람이 올바른 정신 모델을 만들도록 도와준다. 그 모델은 디버깅, 풀 리퀘스트 리뷰, 시간 압박 속에서 동작 변경 등에 의존하는 것이다.
구조가 일관되면 수정이 더 안전해진다: 한 분기를 변경해도 다른 분기에 영향을 주지 않거나, 루프를 리팩터해도 숨은 종료 경로를 놓치지 않는다. 가독성은 단지 미적 취향이 아니라 기존 동작을 깨뜨리지 않고 자신 있게 변경할 수 있게 하는 기반이다.
다익스트라는 간단한 아이디어를 강조했다: 코드가 왜 올바른지 설명할 수 있으면 두려움 없이 변경할 수 있다. 세 가지 작은 추론 도구는 팀을 수학자처럼 만들지 않고도 실용적으로 적용할 수 있다.
불변은 코드가 실행되는 동안, 특히 루프 내부에서 항상 참인 사실이다.
예: 장바구니의 가격을 합산한다고 하자. 유용한 불변은 “total은 지금까지 처리한 모든 항목의 합과 같다.” 루프가 끝날 때 이 불변이 유지되면 결과를 신뢰할 수 있다.
불변은 무엇이 결코 깨져서는 안 되는지에 주의를 집중시키기 때문에 강력하다.
전제조건은 함수가 실행되기 전에 참이어야 하는 조건이다. 후조건은 함수가 종료된 후 보장하는 것이다.
일상적 예:
코드에서는 전제조건이 “입력 리스트가 정렬되어 있다”일 수 있고, 후조건은 “출력 리스트가 정렬되어 있고 삽입된 요소를 포함한다”일 수 있다.
이것들을(비록 비공식적으로라도) 적어두면 설계가 더 날카로워진다: 함수가 무엇을 기대하는지와 무엇을 약속하는지를 결정하게 되어 자연스럽게 더 작고 집중된 단위가 된다.
리뷰에서는 논쟁이 스타일(“나는 다르게 쓰겠다”)에서 정확성(“이 불변을 유지하나?” “우리는 전제조건을 강제할 것인가 아니면 문서화할 것인가?”)으로 이동한다.
형식적 증명이 없어도 이득을 얻을 수 있다. 가장 버그가 많은 루프나 까다로운 상태 업데이트를 골라 그 위에 한 줄짜리 불변 주석을 추가하라. 누군가 나중에 코드를 편집하면 그 주석은 안전장치처럼 작동한다: 변경이 이 사실을 깨뜨리면 코드가 더 이상 안전하지 않다는 신호다.
테스트와 추론은 동일한 목표—의도한 대로 동작하는 소프트웨어—를 향하지만 작동 방식은 다르다. 테스트는 예제를 통해 문제를 발견하고, 추론은 논리를 명시하고 점검함으로써 전체 범주의 문제를 예방한다.
테스트는 실용적 안전망이다. 회귀를 잡고 실제 시나리오를 검증하며 팀 전체가 실행할 수 있는 방식으로 기대 동작을 문서화한다.
하지만 테스트는 버그의 존재는 보여줄 수 있지만 부재를 증명하지는 못한다. 어떤 테스트 스위트도 모든 입력, 모든 타이밍 변형, 모든 기능 상호작용을 커버하지 못한다. 많은 “내 머신에서는 동작한다” 실패는 테스트되지 않은 조합에서 비롯된다: 희귀한 입력, 특정 작업 순서, 여러 단계 후에만 나타나는 미묘한 상태 등.
추론은 코드의 성질을 증명하는 것이다: “이 루프는 항상 종료한다”, “이 변수는 절대 음수가 되지 않는다”, “이 함수는 유효하지 않은 객체를 반환하지 않는다.” 잘하면 경계와 엣지 케이스 관련 결함 범주를 배제할 수 있다.
한계는 노력과 범위다. 전체 제품에 대한 완전한 형식 증명은 거의 경제적이지 않다. 추론은 핵심 알고리즘, 보안 민감 흐름, 금전·청구 로직, 동시성처럼 실패 비용이 큰 곳에 선택적으로 적용할 때 가장 효과적이다.
광범위하게는 테스트를 사용하고, 실패 비용이 큰 곳에는 더 깊은 추론을 적용하라.
테스트와 추론을 잇는 실용적 다리:
이 기법들은 테스트를 대체하지 않는다—오히려 그 그물을 조여준다. 모호한 기대를 점검 가능한 규칙으로 바꾸어 버그를 쓰기 어렵게 하고 진단을 쉽게 만든다.
‘영리한’ 코드는 순간에는 이점처럼 느껴진다: 라인 수가 적고, 깔끔한 트릭, 자신을 똑똑하다고 느끼게 하는 한 줄. 문제는 그런 영리함이 시간이나 사람을 넘어서 확장되지 않는다는 것이다. 6개월 후 작성자는 트릭을 잊고, 새로운 팀원은 글자 그대로 읽고 숨은 가정을 놓쳐 동작을 깨뜨린다. 이것이 ‘영리함 빚’이다: 단기 속도는 장기 혼란으로 산정된다.
다익스트라의 요점은 ‘지루한 코드’를 쓰라는 것이 아니라—규칙적인 제약이 프로그램을 추론하기 쉽게 만든다는 것이다. 팀에서는 제약이 의사결정 피로를 줄인다. 모두가 기본값(이름 짓기, 함수 구조, ‘완료’의 기준)을 알고 있으면 풀 리퀘스트마다 기본을 다시 논의하는 시간이 줄어든다. 그 시간은 제품 작업으로 돌아간다.
규율은 일상적 관행으로 나타난다:
몇 가지 습관이 영리함 빚의 축적을 막는다:
calculate_total()을 do_it()보다 선호).규율은 완벽함이 아니라 다음 변경을 예측 가능하게 만드는 것이다.
모듈화는 단순히 ‘파일을 나눈다’는 것이 아니다. 결정들을 명확한 경계 뒤에 숨겨 나머지 시스템이 내부 세부사항을 알 필요가 없게 만드는 것이다. 모듈은 지저분한 부분—데이터 구조, 엣지 케이스, 성능 트릭—을 숨기고 작고 안정적인 인터페이스를 드러낸다.
변경 요청이 오면 이상적인 결과는: 한 모듈만 변경되고 나머지는 건드리지 않는 것이다. 이것이 ‘변경을 국지화한다’는 실용적 의미다. 경계는 우발적 결합을 방지한다—한 기능을 업데이트하면 세 개가 조용히 깨지는 상황을 막는다.
좋은 경계는 추론을 쉽게 만든다. 모듈이 무엇을 보장하는지 서술할 수 있다면 전체 프로그램을 매번 재확인하지 않고도 추론할 수 있다.
인터페이스는 약속이다: “이 입력을 주면 이 출력을 제공하고 이 규칙을 유지하겠다.” 약속이 명확하면 팀은 병렬로 작업할 수 있다:
이것은 관료적 절차가 아니라 성장하는 코드베이스에서 안전한 조정 점을 만드는 것이다.
거대한 아키텍처 검토 없이도 모듈화를 개선할 수 있다. 가벼운 점검:
잘 그어진 경계는 ‘변경’을 시스템 전체 이벤트에서 국지적 편집으로 바꾼다.
소프트웨어가 작을 때는 모든 것을 머릿속에 넣고 관리할 수 있다. 규모가 커지면 그게 더 이상 통하지 않으며 실패 모드는 익숙해진다.
공통 증상은 다음과 같다:
다익스트라의 핵심 베팅은 인간이 병목이라는 것이다. 명확한 제어 흐름, 작고 잘 정의된 단위, 추론 가능한 코드가 미적 선택이 아니라 용량 증폭기라는 점이다.
큰 코드베이스에서 구조는 이해를 압축한다. 함수가 명시적 입력/출력을 가지면, 모듈에 이름을 붙일 수 있는 경계가 있으면, ‘행복 경로’가 모든 엣지 케이스와 얽혀 있지 않으면 개발자는 의도를 재구성하는 데 드는 시간을 줄이고 의도적인 변경에 더 많은 시간을 쓸 수 있다.
팀이 커지면 커뮤니케이션 비용은 코드 라인 수보다 더 빨리 증가한다. 규율 있고 읽기 쉬운 코드는 기여에 필요한 부족한 부족한(tribal) 지식을 줄인다.
이는 온보딩에서 즉시 드러난다: 새 엔지니어가 예측 가능한 패턴을 따라 배우고 소수의 규약을 익혀 긴 ‘골칫거리’ 투어 없이도 변경할 수 있다. 코드 자체가 시스템을 가르친다.
사건 중에는 시간이 부족하고 확신이 약하다. 전제조건, 의미 있는 검사, 단순한 제어 흐름으로 작성된 코드는 압박 상태에서 추적하기 더 쉽다.
동시에 규율 있는 변경은 롤백도 쉽다. 경계가 명확한 작은 국지 편집은 롤백이 새로운 실패를 유발할 가능성을 줄인다. 결과는 완벽함이 아니라 놀라움 감소, 빠른 복구, 그리고 여러 해와 기여자에도 유지보수 가능한 시스템이다.
다익스트라의 요지는 “옛날 방식으로 코드를 쓰라”가 아니다. “설명할 수 있는 코드”를 쓰라는 것이다. 모든 기능을 형식적 증명 연습으로 만들지 않고도 그 사고방식을 채택할 수 있다.
추론을 저렴하게 만드는 선택으로 시작하라:
좋은 휴리스틱: 함수가 보장하는 것을 한 문장으로 요약할 수 없으면 아마 너무 많은 일을 하고 있다.
대대적 리팩터가 필요 없다. 접합점에서 구조를 추가하라:
isEligibleForRefund)로 바꾼다.이 업그레이드는 점진적이다: 다음 변경의 인지 부하를 줄인다.
변경을 리뷰하거나 작성할 때 묻자:
리뷰어가 빠르게 답하지 못하면 코드가 숨은 의존성을 신호한다.
코드를 그대로 되풀이하는 주석은 금세 오래된다. 대신 코드가 왜 올바른지—핵심 가정, 당신이 지키는 엣지 케이스, 가정이 변하면 무엇이 깨지는지—를 적어라. “불변: total은 항상 처리된 항목의 합이다” 같은 짧은 메모는 장황한 설명보다 유용할 수 있다.
이 습관을 담을 가벼운 장소가 필요하면 공유 체크리스트(참조: /blog/practical-checklist-for-disciplined-code)에 모아라.
현대 팀은 점점 AI를 사용해 전달 속도를 높인다. 위험은 익숙하다: 오늘의 속도가 내일의 혼란이 될 수 있다면 생성된 코드가 설명하기 어려워진다.
다익스트라 친화적 방식으로 AI를 사용하는 법은 AI를 ‘구조적 사고’의 가속기로 대하는 것이다, 그 자체의 대체물로 보지 말라. 예를 들어 Koder.ai와 같이 채팅으로 웹·백엔드·모바일 앱을 만드는 플랫폼에서 작업할 때는 다음을 명시하라:
최종적으로 소스 코드를 내보내 다른 곳에서 실행하더라도 동일한 원칙이 적용된다: 생성된 코드는 설명할 수 있는 코드여야 한다.
리뷰, 리팩터, 머지 전에 쓸 수 있는 가벼운 ‘다익스트라 친화적’ 체크리스트다. 하루 종일 증명을 쓰라는 것이 아니라 정확성과 명확성을 기본값으로 만드는 것이다.
total은 처리된 항목의 합”)가 미묘한 버그를 예방한다.한 덩어리의 지저분한 모듈을 골라 제어 흐름을 먼저 재구조화하라:
그다음 새로운 경계를 중심으로 몇 가지 집중 테스트를 추가하라. 자세한 패턴을 더 보고 싶으면 관련 게시물들을 /blog 에서 찾아보라.
사유 가능한 제어 흐름, 명확한 계약, 그리고 정확성에 대한 강조는 시간이 지나도 "작은 변경"이 다른 곳에서 예기치 않은 동작을 유발하지 않게 한다는 점에서 팀과 코드베이스가 커질수록 생기는 병목을 줄여준다. 즉, 다익스트라의 아이디어는 대형 소프트웨어 팀이 마주치는 실전 문제에 직접 대응한다.
여기서 “확장(scale)”은 성능만을 뜻하지 않는다. 오히려 복잡성이 증대되는 현상을 뜻한다.
이런 요인들이 합쳐져서 시스템을 이해하고 예측 가능하게 유지하는 것이 더 중요한 자산이 된다.
실무적으로는 소수의 명확한 제어 구조를 선호한다는 의미다:
if/else, switch)for, while)목표는 경직성이 아니라 실행 경로를 따라가기 쉽게 만들어 동작을 설명하고 리뷰하며 디버깅할 수 있게 하는 것이다.
문제는 제어 흐름이 제한 없이 뛰어넘을 때 발생한다. 무분별한 점프는 예측하기 힘든 경로와 불명확한 상태를 만들고, 결과적으로 "우리가 어떻게 여기까지 왔나?" 또는 "이 변수의 상태는 무엇인가?" 같은 기본 질문에 답하기 어렵게 만든다.
현대에서는 깊게 중첩된 분기, 흩어진 조기 반환, 암묵적 상태 변경 등이 같은 문제를 일으킨다.
사용자가 기대하는 조용한 신뢰성이다. 시스템이 약속한 입력과 상황에 대해 일관되게 결과를 내고, 실패할 때는 예측 가능하고 설명 가능한 방식으로 실패하는 것이다. 즉 "몇 가지 예에서는 동작한다"와 "리팩터, 통합, 엣지 케이스가 생겨도 계속 동작한다"의 차이다.
의존성이 오류를 증폭시키기 때문이다. 작은 잘못된 상태나 경계 버그는 모듈과 서비스 사이에서 복사되고 캐시되고 재시도되며 우회 패치로 덮인다. 시간이 지나면 팀은 "무엇이 참인가?" 대신에 "보통은 어떻게 되나?"라는 질문을 하게 되고, 이는 사고 대응을 고고학처럼 복잡하게 만든다.
동시에 작동하는 아이디어 수가 적다는 의미다: 책임이 명확하고 데이터 흐름과 제어 흐름이 선명하며 특수 사례가 적다. 이는 라인 수를 줄이거나 영리한 한 줄짜리 트릭을 의미하지 않는다.
좋은 테스트는 요구사항이 바뀔 때 행동을 예측하기 쉬운지로 판단할 수 있다. 만약 새로운 경우마다 “단, …” 규칙이 늘어난다면 우발적 복잡성을 쌓는 것이다.
불변은 루프나 상태 전환 동안 항상 참이어야 하는 사실이다. 가벼운 실무 방식은 다음과 같다:
total은 지금까지 처리한 항목의 합과 같다”).이렇게 하면 이후 변경이 안전해진다. 다음 사람이 무엇을 망가뜨리면 안 되는지 분명히 알 수 있기 때문이다.
테스트는 예제를 실행해 문제를 찾아내고 회귀를 잡아주는 실용적 안전망이다. 하지만 모든 입력, 모든 타이밍 변형, 모든 상호작용을 커버하지 못하므로 결함의 부재를 증명하지는 못한다.
추론(Reasoning)은 ‘이 루프는 항상 종료한다’, ‘이 변수는 음수가 될 수 없다’ 같은 성질을 증명해 특정 결함 범주를 배제한다. 비용이 큰 영역(금전 처리, 보안, 동시성)에는 추론을 더 적용하고, 전체 제품에는 광범위한 테스트를 유지하는 균형이 실용적이다.
다익스트라의 핵심은 ‘코드를 설명할 수 있어야 한다’는 것이다. 다음과 같은 작은 실천으로 시작하라:
좋은 휴리스틱: 함수가 무엇을 보장하는지 한 문장으로 요약할 수 없다면 아마 한 일이 너무 많다.