소프트웨어 엔지니어링은 복잡성을 관리하는 것에 관한 것이다
요약
본 글은 코드를 작성하는 행위와 소프트웨어를 구축(building)하는 과정을 명확히 구분합니다. 코딩이 아이디어를 지침으로 번역하는 것이라면, 소프트웨어 구축은 시스템의 상호작용 방식, 제약 조건, 트레이드오프를 결정하는 고차원적인 과정입니다. 진정한 어려움은 구문이나 언어 선택이 아닌, 아키텍처와 근본적인 설계 질문에 답하는 데 있습니다.
핵심 포인트
- 소프트웨어 구축은 코딩을 넘어선 시스템 설계의 영역이다.
- 진짜 문제는 '무엇을 구현할지'가 아니라 '어떻게 설계할지'를 결정하는 것이다.
- 시스템 요구사항(사용자 규모, 일관성 등)에 따라 최적의 아키텍처는 달라진다.
- 기술적으로 멋진 해결책이 모든 상황에서 정답일 수는 없다.
AI가 점점 더 명확하게 드러내고 있는 소프트웨어 엔지니어링에 대한 오해가 있습니다. 우리는 코드를 작성하는 것과 소프트웨어를 구축하는 것을 혼동하는 경향이 있습니다.
특정 측면에서는 일부 겹치는 부분이 있지만, 둘은 같은 것이 아닙니다.
코드를 작성한다는 것은 아이디어를 컴퓨터가 실행할 수 있는 지침으로 번역하는 것을 의미합니다. 소프트웨어를 구축한다는 것은 애초에 어떤 지침이 존재해야 하는지, 그것들이 어떻게 상호 작용해야 하는지, 어떤 제약 조건이 중요한지, 결정의 비용은 얼마인지, 허용 가능한 트레이드오프(tradeoffs)는 무엇인지, 그리고 결과 시스템이 자체적인 제약과 한계 속에서 붕괴되지 않고 어떻게 진화할 수 있는지를 결정하는 것을 의미합니다.
기본 전제부터 시작하겠습니다. AI는 첫 번째 문제에 매우 뛰어나기 때문에 필수적인 도구입니다.
두 번째 부분이 바로 소프트웨어 엔지니어링이 실제로 시작되는 곳입니다.
어려운 부분은 코드를 타이핑하는 것이 아니었다
비교적 평범한 엔지니어링 요구사항을 생각해 봅시다.
들어오는 이벤트를 처리하고 일부 데이터를 업데이트해야 합니다.
그리고 기술적인 논의 과정에서 제기되는 첫 번째 질문들 중 일부는 다음과 같습니다:
- 동기적으로(synchronously) 처리해야 할까요?
- 큐(queue)에 넣어야 할까요?
- 정확히 한 번만 처리되어야 하나요, 아니면 최소 한 번 이상이면 충분한가요?
- 시스템이 최종적 일관성(eventual consistency)을 허용할 수 있나요?
- 처리가 중간에 실패하면 어떻게 되나요?
- 재시도해야 할까요? 몇 번이나요?
- 소비자가 3시간 동안 사용할 수 없다면 어떻게 될까요?
- 이벤트는 순서를 유지해야 하나요?
- 오늘 예상되는 트래픽은 얼마나 되나요? 2년 후는요?
- 이벤트가 두 번 처리된다면 어떤 결과가 따를까요?
... 그리고 계속됩니다.
이러한 질문들은 구문(syntax)과는 거의 관련이 없습니다.
프로그래밍 언어의 선택은 팀의 숙련도, 팀의 성능, 시스템의 성능, 안전성, 유지보수성, 툴링(tooling), 운영 특성에 영향을 미치기 때문에 중요하지만, 근본적인 질문들에 답하지는 못합니다.
어려운 부분은 올바른 타협점들의 집합을 나타내는 아키텍처를 선택하는 것입니다.
그리고 보편적으로 올바른 답은 거의 없습니다.
1 문제는 N개의 완전히 다른 올바른 해결책을 가질 수 있습니다
이것은 특히 소프트웨어가 비즈니스 내부에 존재할 때 명확합니다.
두 회사가 자신들의 엔지니어링 팀에게 정확히 똑같은 기능처럼 보이는 것을 만들라고 요청하는 상황을 상상해 보세요.
그들의 요구사항은 종이 위에서 동일하게 보일 수 있습니다.
하지만:
- 회사 A는 500명의 사용자를 가지고 있는 반면, 회사 B는 2천만 명의 사용자를 가집니다.
- 회사 A는 시스템을 유지 관리하는 엔지니어가 세 명일 수 있습니다. 회사 B는 200명일 수 있습니다.
- 한 회사는 실수가 심각한 재정적 결과를 초래하기 때문에 강한 일관성(strong consistency)이 필요할 수 있습니다. 다른 회사는 가용성과 처리량(throughput)을 확보하는 대신 최종적인 일관성(eventual consistency)을 기꺼이 받아들일 수 있습니다.
- 한 회사는 3주 안에 배포해야 할 수도 있습니다.
- 또 다른 회사는 시스템이 15년 동안 운영 상태를 유지할 것으로 기대할 수 있습니다.
- 한 회사는 이미 Kafka, Kubernetes, PostgreSQL, 관측 가능성(observability) 인프라와 분산 시스템 경험이 풍부한 엔지니어들을 보유하고 있을 수 있습니다. 다른 회사는 네 명의 개발자가 관리하는 단일 애플리케이션 서버와 PostgreSQL 데이터베이스를 가지고 있을 수 있습니다.
한 회사에게 기술적으로 인상적인 해결책이 다른 회사에게는 무책임한 해결책일 수 있습니다.
이것이 바로 아키텍처가 다음 질문으로 축소될 수 없는 이유입니다:
X를 구현하는 가장 좋은 방법은 무엇인가?
올바른 질문은 보통 다음과에 더 가깝습니다:
이러한 제약 조건, 이 팀, 이 비즈니스, 이 인프라, 이 예산, 이러한 위험 요소들, 그리고 제품의 예상되는 진화 과정을 고려할 때, 오늘날 X를 구현하는 가장 적절한 방법은 무엇인가?
이는 근본적으로 다른 질문입니다.
AI가 해결책을 생성하고, 엔지니어가 트레이드오프(tradeoffs)를 소유합니다.
AI가 소프트웨어 조직에 채택되는 방식 때문에 이 구분이 점점 더 중요해지고 있습니다.
AI는 소프트웨어 개발에 매우 유용합니다. 우리는 이를 가속기(accelerator)로 사용합니다. 즉, 상용구 코드 생성, API 탐색, 구현 제안, 잠재적 버그 찾기, 생소한 코드 설명, 테스트 생성, 접근 방식 비교, 또는 단순히 아이디어를 작동하는 코드로 전환하는 데 필요한 기계적인 작업량을 줄이는 방식으로 사용하는 것입니다.
하지만 이 능력을 훨씬 더 광범위한 영역으로 확장하려는 위험한 경향이 있습니다. 바로 엔지니어링 판단 자체를 위임하는 것입니다.
여러분은 다음과 같은 일을 할 수 있습니다:
- AI 모델에게 요구사항을 주고 시스템 설계를 요청하며, “실수 없이” 하도록 합니다: 모델은 하나를 설계할 것입니다.
- 데이터베이스 선택을 요청합니다: 모델은 하나를 선택할 것입니다.
- 큐(queue), 마이크로서비스(microservices), 캐싱(caching), CQRS, 이벤트 소싱(event sourcing), Kubernetes, Redis 또는 다른 추상화 계층 도입 여부를 물어봅니다: 모델은 답변을 줄 것입니다.
하지만 답변이 존재한다는 것이 근본적인 엔지니어링 문제가 해결되었다는 것을 의미하지는 않습니다.
진짜 문제는 올바른 결정이 컨텍스트(context)에 달려 있다는 것입니다. 그리고 그 컨텍스트는 종종 엄청난 양의 정보이며, 분석하고 이해하며 평가하는 데 몇 시간, 며칠, 심지어 몇 주가 걸릴 수 있습니다. 이 과정에는 종종 같은 조직 내 여러 부서가 관련되며, 공식화하기 어려운 회색 영역을 남기기도 하고, 미래에 변경이 필요할 때 문제가 될 수 있습니다.
실제 세상은 이런 방식으로 작동합니다: 이러한 컨텍스트 중 일부는 문서에 존재합니다. 하지만 대부분은 그렇지 않습니다.
이는 고객과의 대화 속에 존재합니다. 제품의 이력 속에서 존재합니다. 엔지니어링 팀의 기술 속에 존재합니다. 3년 전 운영상의 사고 기록 속에 존재합니다. 예산 제약 조건 속에 존재합니다. 마감 기한 속에 존재합니다. 계약상 의무 사항 속에 존재합니다. 아무도 건드리고 싶어 하지 않는 레거시 시스템의 이상한 동작 속에서 존재합니다. 그리고 고객이 6개월 후에 특정 기능을 요청할 가능성을 아는 것 같은 곳에 존재합니다.
때로는 단순히 경험 속에만 존재하는 경우도 있습니다: 이론적으로 우아한 아키텍처가 그것을 유지해야 하는 팀에게 운영상의 악몽이 될 것이라는 것을 인식하는 것입니다.
이 모든 것을 AI 프롬프트에 단락을 추가하는 것으로 어찌어찌 담아낼 수 있는 작은 세부 사항으로 취급할 수는 없습니다.
아키텍처에는 트레이드오프가 존재한다
엔지니어링은 본질적으로 어떤 문제를 감수할 것인지를 결정하는 학문입니다. FAANG 규모에서 일하지 않더라도, 여러분의 프로젝트는 여전히 충분한 데이터를 처리해야 할 수 있으며, 적절한 성능을 유지하기 위해 올바른 엔지니어링 결정을 내리는 것이 필수적입니다. 그렇지 않으면 시스템적인 비효율성을 해결하기 위해 값비싼 하드웨어를 투입해야 합니다.
심지어 “간단한” 선택에도 고려해야 할 요소들이 있습니다:
- 데이터베이스를 공격적으로 정규화(Normalize)하면 무결성(integrity)을 얻을 수 있지만, 특정 쿼리를 더 복잡하게 만들 수 있습니다.
- 비정규화(Denormalize)하면 읽기 성능(read performance)을 개선할 수 있지만, 동기화 복잡성(synchronization complexity)이 증가할 수 있습니다.
- 캐싱(caching)을 추가하면 지연 시간(latency)을 줄일 수 있지만, 무효화 문제(invalidation problems)를 야기합니다.
- 비동기 처리(asynchronous processing)를 도입하면 컴포넌트를 분리할 수 있지만, 새로운 장애 모드(failure modes)를 만듭니다.
- 모놀리스(monolith)를 마이크로서비스(microservices)로 분할하면 독립적인 배포 경계(deployment boundaries)를 얻지만, 분산 시스템 복잡성(distributed-system complexity)을 도입합니다.
- 공격적으로 최적화(Optimize aggressively)하면 성능을 얻을 수 있지만, 가독성(readability)을 희생할 수 있습니다.
- 공격적으로 추상화(Abstract aggressively)하면 중복(duplication)을 줄일 수 있지만, 시스템 이해도를 떨어뜨릴 수 있습니다.
- 추상화를 피하면 결국 반대 문제가 나타납니다.
이러한 것들 중 어느 것도 독립적으로 평가될 수 없습니다. 모든 최적화는 어딘가에 복잡성을 소모합니다. 따라서 엔지니어의 임무는 단순히 시스템을 작동하게 만드는 것이 아닙니다.
복잡성이 어디에 속해야 할지를 결정하는 것입니다.
알고리즘적 사고(Algorithmic thinking)가 덜 중요해지는 것이 아니라 더 중요하다
이것이 제가 AI 시대에 알고리즘적 사고가 덜 중요한 것이 아니라 오히려 더 중요해진다고 생각하는 이유입니다.
여기서 말하는 알고리즘적 사고란 단순히 교과서적인 알고리즘을 아는 것, 혹은 레드-블랙 트리(red-black tree)를 암기해서 구현할 수 있다는 것을 의미하지 않습니다.
알고리즘적 사고란 따를 기본적인 규칙들을 정의하고 일상적으로 적용하는 것을 의미합니다. 저는 10가지 원칙 세트를 정리했습니다:
- 문제를 분해하라.
- 불변량(invariants)을 식별하라.
- 데이터 흐름을 이해하라.
- 적절한 자료 구조를 선택하라.
- 시간 및 공간 복잡도에 대해 추론하라.
- 경합(contention)을 식별하라.
- 실패 모드를 이해하라.
- 어떤 연산이 비용이 많이 들고, 실제로 중요한 것이 무엇인지 인식하라.
- 본질적인 복잡성과 우발적인 복잡성을 분리하라.
- 가정이 더 이상 사실이 아닐 때 무슨 일이 일어나야 하는지 이해하라.
알다시피, 생산성 향상 시대에는 이것이 이상하게 들릴 수 있다. AI는 이전에 엔지니어가 하나를 작성하는 데 걸렸던 시간 동안 10개의 구현을 생성할 수 있다. 도전 과제는 인간이 루프(loop) 안에 있으면서, 목표 지향적이고 통제된 반복을 통해 이러한 원칙들을 적용하기 위해 AI를 도구로 사용하는 것이다.
그것은 올바른 구현을 선택하는 것이 10배 더 중요해진다는 것을 의미한다. 이것이 진정한 생산성 향상이 의미하는 바이다.
당신의 프로그래밍 언어는 정체성이 아니라 도구입니다
이는 프로그래밍 언어에도 동일하게 적용된다.
작업에 맞는 올바른 도구를 선택하는 데 가치가 있다. 언어들은 서로 다른 강점, 생태계, 성능 특성, 타입 시스템, 동시성 모델(concurrency models), 배포 모델, 그리고 안전 보장 기능을 가지고 있다.
이러한 차이점들이 중요하지만, 엔지니어링 논의에서는 때때로 프로그래밍 언어 선택에 '과대평가'를 한다. 많은 비즈니스 시스템에서 두 가지 합리적인 언어 중 하나를 선택하는 것이 잘못된 아키텍처, 잘못된 데이터 모델, 또는 잘못된 운영 전략을 선택하는 것보다 훨씬 덜 중요하다.
시나리오에 적합한 언어를 선택하라. 여러 언어가 적절하다면, 팀이 깊이 이해하고 있는 언어를 선택하는 데 상당한 가치가 있다. LLM(대규모 언어 모델)이 특정 PL(프로그래밍 언어)에서 더 잘 작동할 것이라는 가정에 근거하여 결정을 내리지 마라. 항상 팀을 최우선으로 생각하라.
5가지 중요한 원칙은 다음과 같다:
- 숙련도(Fluency)가 중요하다.
- 생태계 성숙도가 중요하다.
- 유지보수성(Maintainability)이 중요하다.
- 디버깅이 중요하다.
- 배포가 중요하다.
이것이 바로 오늘날, 그 어느 때보다도 LLM을 활용할 수 있게 되면서, 저는 엔지니어들에게 단일 PL(Product Line)에 종교적으로 충성하기보다는 가능한 한 다재다능해지고, AI 어시스턴트를 단순한 코드 생성기가 아니라 학습 도구로 사용하고, 항상 공식 문서를 재확인하도록 권장합니다.
진정한 최적화 목표는 시스템의 수명이다
또 다른 실수는 소프트웨어를 현재 요구사항에만 맞춰서 최적화하는 것입니다.
현대적인 소프트웨어는 정적일 수 없으며, 다음과 같은 변화에 대응하도록 구축되어야 합니다:
- 사용자(Users)
- 고객(Customers)
- 트래픽(Traffic)
- 비즈니스 모델(Business models)
- 규제(Regulations)
- 팀(Teams)
- 인프라스트럭처(Infrastructures)
모든 지점에는 시간이 지나면서 가중치가 변할 수 있습니다. 따라서 오늘날 사소해 보이는 기능이 2년 후 핵심 워크플로우가 될 수 있습니다.
그러므로 좋은 엔지니어링은, 팀들이 어떤 코덱스(codex) 출력을 너무 오래 검토하지 않아 받아들여서 발생할 수도 있는 가상의 미래를 위해 정교한 추상화를 설계하는 반대 함정에 빠지지 않으면서도 변화를 예측하는 능력이 필요합니다. 이 균형은 어렵기 때문입니다:
- 이 요구사항이 변경될 가능성은 얼마나 되는가?
- 어디에 의도적으로 유연성을 도입해야 하는가?
- 어디는 단순하게 유지해야 하는가?
- 어떤 기술 부채(technical debt)가 허용 가능한가?
- 어떤 기술 부채가 구조적으로 비용이 많이 들 수 있는가?
- 지금 무엇을 최적화해야 하는가?
- 측정 결과 그것이 중요하다는 것이 증명될 때까지 의도적으로 비최적화 상태로 남겨두어야 할 것은 무엇인가?
이것이 엔지니어링 판단력입니다.
이러한 질문들과 다른 많은 질문들에 올바르게 답할 수 있는 AI는 없으며, 단순히 AI 에이전트를 투입하여 신뢰할 수 있는 결과를 기대하는 것도 불가능합니다. 이러한 질문들을 사라지게 만드는 프롬프트도 없습니다.
AI가 생성한 코드는 새로운 소유권 문제를 만든다
저는 소프트웨어 조직들이 과소평가하고 있다고 생각하는 또 다른 문제가 있습니다: 코드베이스의 소유권(codebase ownership)입니다.
AI는 코드를 생성하는 것을 극도로 저렴하게 만듭니다. 이는 완전히 긍정적으로 들리지만, 코드 생성과 코드 유지보수는 서로 다른 경제 활동이라는 점을 기억할 때까지는 그렇지 않습니다. 엔지니어는 이제 겉보기에는 합리적인 수백 또는 수천 줄의 코드를 매우 빠르게 생성할 수 있습니다.
팀은 이를 병합하고, 테스트를 통과시키고, 기능이 작동하며, 생산성이 증가한 것처럼 보이지만, 결정적으로 무언가가 손실되었을 수 있습니다. 무엇인지 이해하고 싶다면 스스로에게 물어보세요:
- 팀이 실제로 일반적인 아키텍처를 이해하는가?
- 그들은 데이터 구조를 이해하는가?
- 특정 추상화(abstraction)가 존재하는 이유를 이해하는가?
- 실패 동작(failure behavior)을 이해하는가?
- 생성된 구현이 어떤 가정에 의존하는지 알고 있는가?
- 6개월 후에 다른 AI에게 이전에 AI가 생성한 코드를 설명해 달라고 요청하지 않고도 자신감 있게 수정할 수 있다고 가정할 수 있는가?
만약 답이 '아니요'라면, 팀은 복잡성을 제거하지 못한 것입니다. 오히려 시간이 지남에 따라 점점 더 중요해지는 다른 복잡성들을 도입했을 가능성이 높습니다.
코드 라인은 싸다. 이해하는 것이 비싸하다.
이것이야말로 AI가 소프트웨어 경제학에 가져오는 가장 큰 변화일 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 HN Claude Code Search의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기