[번역] Do the simplest thing that could possibly work

[번역] Do the simplest thing that could possibly work

2026년 2월 1일

Sean GoedeckeDo the simplest thing that could possibly work를 번역한 글입니다.

소프트웨어 시스템을 설계할 때, 가능한 한 가장 간단한 방법을 사용하세요.

이 조언이 얼마나 유용한지 놀라울 정도입니다. 저는 진심으로 당신이 항상 이렇게 할 수 있다고 생각합니다. 이 접근 방식을 버그 수정, 기존 시스템 유지 관리, 새로운 시스템 아키텍처 설계에 적용할 수 있습니다.

많은 엔지니어는 “이상적인” 시스템, 즉 잘 구성되어 있고 거의 무한대로 확장 가능하며 우아하게 분산된 시스템 등을 생각하면서 설계합니다. 저는 이것이 소프트웨어 설계에 있어서 완전히 잘못된 방법이라고 생각합니다. 대신, 그 시간을 들여 현재 시스템을 깊이 이해하고 가능한 가장 간단한 일을 하세요.

단순한 것은 압도적일 수 있다

시스템 설계에는 앱 서버, 프록시, 데이터베이스, 캐시, 대기열 등 다양한 도구에 대한 역량이 필요합니다. 이러한 도구에 익숙해지면 주니어 엔지니어들은 자연스럽게 이를 사용하고 싶어하게 됩니다. 다양한 구성요소로 시스템을 구성하는 것은 재미있습니다! 실제 엔지니어링 작업을 하는 것처럼 화이트보드에 상자와 화살표를 그리는 것은 매우 만족스러운 일입니다.

그러나 많은 기술과 마찬가지로 진정한 숙달에는 더 많은 일이 아니라 더 적은 일을 해야 할 때를 배우는 것이 포함됩니다. 야심찬 초보자와 마스터 사이의 싸움은 무술 영화에서 진부한 진부한 클리셰입니다. 초보자는 움직임이 흐릿하고 뒤집히고 회전하는 모습입니다. 마스터은 대부분 가만히 있습니다. 하지만 어쩐지 초보자의 공격은 결코 연결되지 않는 것 같고, 마스터의 최종 공격이 결정적입니다.

소프트웨어에서 이는 훌륭한 설계일수록 오히려 평범해 보인다는 것을 의미합니다. 겉으로 보기엔 대단한 일이 벌어지고 있는 것처럼 보이지 않기 때문이죠. “오, 문제가 그렇게 쉬운 줄은 몰랐네요.” 또는 “아 다행이네요. 실제로는 어려운 작업을 수행할 필요가 없습니다.”와 같은 생각이 들기 시작하기 때문에 훌륭한 소프트웨어 설계가 존재한다고 말할 수 있습니다.

Unicorn은 훌륭한 소프트웨어 설계의 본보기입니다.유닉스 프리미티브(Unix primitives)라는 기본 요소들을 활용해 웹 서버에서 가장 중요한 보장 요소들(요청 격리, 수평적 확장, 크래시 복구)을 모두 구현해냈기 때문입니다. 업계 표준인 Rails REST API 역시 훌륭한 설계라 할 수 있는데, 이는 CRUD 애플리케이션에 필요한 기능을 가장 지루해 보일 만큼 단순한 방식으로 제공하기 때문입니다. 저는 이들이 소프트웨어로서 대단히 ‘강렬한 인상’을 준다고 생각하지는 않습니다. 하지만 **‘작동 가능한 가장 단순한 설계’**를 구현해냈다는 점에서, 설계 측면으로는 경이로운 성과라고 생각합니다.

여러분도 그렇게 해야 합니다! 예를 들어, Go로 만든 애플리케이션에 처리율 제한(Rate Limiting) 기능을 추가하고 싶다고 가정해 봅시다. 이때 ‘작동 가능한 가장 단순한 방법’은 무엇일까요?

아마 처음에는 사용자별 요청 횟수를 기록하기 위해 리디스(Redis) 같은 외부 저장소를 도입하고 ‘리키 버킷(Leaky-bucket)’ 알고리즘을 구현하는 방법을 떠올릴 겁니다. 물론 잘 작동하겠지요! 하지만 정말로 새로운 인프라가 통째로 필요할까요?

대신 사용자별 요청 횟수를 그냥 메모리(In-memory)에 저장하면 어떨까요? 물론 서버를 재시작할 때마다 데이터가 일부 사라지겠지만, 그게 정말 큰 문제가 될까요? 아니면, 혹시 사용 중인 에지 프록시(Edge Proxy)에서 이미 처리율 제한 기능을 지원하고 있지는 않나요? 직접 기능을 구현하는 대신 설정 파일 몇 줄만 고치면 끝날 일일지도 모릅니다.

물론 에지 프록시가 처리율 제한을 지원하지 않을 수도 있습니다. 혹은 서버 인스턴스가 너무 많이 병렬로 실행되고 있어서, 메모리 기반으로만 관리하면 제한 범위가 너무 넓어져 실효성이 없을지도 모릅니다. 아니면 서비스에 가해지는 공격이 너무 거세서 처리율 데이터를 단 조금이라도 잃는 것이 치명적인 문제가 될 수도 있죠.

그런 경우라면 외부 저장소를 추가하는 것이 ‘작동 가능한 가장 단순한 방법’이 되며, 당연히 그렇게 구현해야 합니다. 하지만 만약 더 쉬운 접근법 중 하나로도 문제를 해결할 수 있다면, 굳이 더 쉬운 길을 택하지 않을 이유가 있을까요?

실제로 애플리케이션 전체를 이런 방식으로 밑바닥부터 구축할 수 있습니다. 지독할 정도로 단순한 것부터 시작하고, 새로운 요구사항 때문에 어쩔 수 없는 상황이 닥칠 때만 기능을 확장하는 것이죠. 바보 같은 소리처럼 들리겠지만, 이 방법은 통합니다.

YAGNI(당장 필요하지 않은 기능은 만들지 마라)를 궁극의 설계 원칙으로 삼아보세요. ‘단일 책임 원칙’보다도, ‘적재적소에 맞는 최고의 도구 선택’보다도, 그리고 소위 말하는 ‘좋은 설계’라는 관념보다도 YAGNI를 최우선 순위에 두는 것입니다.

가장 간단한 일을 하는 것이 무엇이 잘못되었나요?

“물론, ‘작동 가능한 가장 단순한 방법’만을 고집하는 데에는 세 가지 커다란 문제점이 있습니다.

첫째, 미래의 요구사항을 예측하지 않음으로써 결국 유연성이 떨어지는 시스템이 되거나, 소위 말하는 ‘거대한 진흙탕(Big ball of mud)’ 같은 스파게티 코드가 되어버릴 수 있다는 점입니다.

둘째, 무엇이 ‘가장 단순한지’ 그 정의가 모호하다는 점입니다. 자칫하면 제가 ‘설계를 잘하려면 항상 좋은 설계를 해라’라는 식의 뻔한 소리를 하는 것으로 보일 수도 있습니다.

셋째, 지금 당장 작동하는 시스템이 아니라 확장 가능한(Scale) 시스템을 구축해야 한다는 책임론입니다.

이제 이러한 반론들을 하나씩 차례대로 짚어보겠습니다.

거대한 진흙탕

어떤 엔지니어들에게 ‘작동 가능한 가장 단순한 일을 하라’는 말은 마치 엔지니어링을 그만두라는 소리처럼 들릴 수도 있습니다. 만약 ‘가장 단순한 방법’이 대개 임시방편으로 대충 때우는 식(kludge)이라면, 이 조언은 필연적으로 코드를 엉망진창으로 만들게 되지 않을까요?

우리 모두는 임시방편(hacks) 위에 또 다른 임시방편이 층층이 쌓여 있는 코드베이스를 본 적이 있습니다. 그리고 그런 코드는 결코 훌륭한 설계라고 부를 수 없는 모습이었죠.

하지만 임시방편(hacks)이 과연 단순할까요? 저는 그렇게 생각하지 않습니다. 임시방편이나 조잡한 해결책의 진짜 문제는 그것이 단순하지 않다는 데 있습니다. 오히려 항상 기억해야 할 짐을 하나 더 얹음으로써 코드베이스에 복잡성을 더할 뿐이죠.

임시방편은 그저 떠올리기가 더 쉬울 뿐입니다. 제대로 된 해결책을 찾는 것이 어려운 이유는 코드베이스 전체(혹은 아주 넓은 범위)를 깊이 이해해야 하기 때문입니다. 사실, 제대로 된 해결책은 거의 언제나 임시방편보다 훨씬 더 단순합니다.

작동 가능한 가장 단순한 방법을 찾는 것은 결코 쉬운 일이 아닙니다. 어떤 문제를 마주했을 때 처음 떠오르는 몇 가지 해결책이 가장 단순한 방법일 가능성은 희박합니다.

가장 단순한 해결책을 찾아내려면 수많은 다양한 접근 방식을 깊이 고민해야 합니다. 다시 말해, 그 과정 자체가 바로 ‘엔지니어링’을 하는 것입니다.

단순함이란 무엇일까요?

엔지니어들 사이에서도 무엇이 ‘단순한 코드’인가에 대해서는 의견이 크게 갈립니다. 만약 ‘가장 단순한 것’이 이미 ‘좋은 설계’라는 뜻을 내포하고 있다면, ‘작동 가능한 가장 단순한 일을 하라’는 말은 그저 ‘설계를 잘해라’라는 식의 동어반복(Tautology)에 불과한 것 아닐까요?

다시 말해, 유니콘(Unicorn)이 푸마(Puma)보다 정말 더 단순하다고 할 수 있을까요? 인메모리 처리율 제한을 구현하는 것이 리디스(Redis)를 사용하는 것보다 정말 더 단순할까요?

여기, 단순함에 대한 대략적이고 직관적인 정의를 내려보겠습니다.

  1. 단순한 시스템은 “움직이는 부품(moving pieces)“의 수가 적습니다. : 즉, 시스템을 다룰 때 머릿속으로 고려해야 할 요소 자체가 적다는 뜻입니다.

  2. 단순한 시스템은 내부적인 연결성이 낮습니다. : 시스템을 구성하는 컴포넌트들이 명확하고 직관적인 인터페이스로 이루어져 있습니다.

유닉스 프로세스는 스레드보다 더 단순합니다. 따라서 유니콘(Unicorn)이 푸마(Puma)보다 더 단순하다고 할 수 있죠. 프로세스는 서로 메모리를 공유하지 않기에 연결성(Connectedness)이 더 낮기 때문입니다.

이는 매우 일리 있는 설명이라고 생각합니다! 하지만 이 기준이 모든 상황에서 무엇이 더 단순한지 판가름할 수 있는 만능 도구가 되어준다고 보지는 않습니다.

인메모리 처리율 제한과 Redis를 사용하는 방법은 어떨까요?

한편으로 보면 인메모리 방식이 더 단순합니다. 영속성 저장소를 가진 별도의 서비스를 구축하고 관리할 때 고려해야 할 그 수많은 일들을 신경 쓰지 않아도 되기 때문입니다.

하지만 다른 한편으로 보면 리디스가 더 단순할 수도 있습니다. 리디스가 제공하는 처리율 제한의 보장 범위가 훨씬 더 명확하기 때문이죠. 즉, 특정 서버 인스턴스는 사용자가 제한 대상이라고 판단하는데, 다른 인스턴스는 그렇지 않다고 판단하는 식의 복잡한 예외 상황을 걱정할 필요가 없습니다.

무엇이 더 단순해 보이는지 확신이 서지 않을 때, 저는 이런 결정적인 기준(tiebreaker)을 사용하곤 합니다. 바로 **‘단순한 시스템은 안정적이다’**라는 점입니다.

소프트웨어 시스템의 두 상태를 비교할 때, 만약 새로운 요구사항이 전혀 없는 상태에서도 관리해야 할 일이 더 많이 생기는 쪽이 있다면, 그 반대편에 있는 시스템이 더 단순한 것입니다.

Redis는 배포하고 유지 관리해야 합니다. 그 자체로 장애가 발생할 수도 있고, 별도의 모니터링도 필요하며, 서비스가 확장되는 모든 새로운 환경마다 별도로 설치해줘야 합니다. 따라서 이런 관점에서 보면, 인메모리 기반의 처리율 제한이 Redis보다 더 단순합니다.

확장성을 원하지 않는 이유는 무엇입니까?

지금쯤 어떤 부류의 엔지니어들은 속으로 비명을 지르고 있을지도 모릅니다. ‘하지만 인메모리 방식은 확장성이 없잖아!’라고 말이죠.

작동 가능한 가장 단순한 방법을 선택하는 것은 단언컨대 ‘웹 스케일(Web-scale, 대규모 확장성)‘에 가장 최적화된 시스템을 만들어주지는 않습니다. 그저 현재의 규모에서 잘 작동하는 시스템을 만들어줄 뿐입니다.

그렇다면 이것은 무책임한 엔지니어링일까요?

아니요, 그렇지 않습니다. 제 관점에서 볼 때, 거대 기술 기업(Big Tech) SaaS 엔지니어링이 저지르는 가장 치명적인 죄악은 바로 ‘확장에 대한 집착’입니다.

현재 규모보다 몇 단계(orders of magnitude)나 더 큰 규모에 대비하겠다며 시스템을 과잉 설계(over-engineering)하는 바람에, 피할 수 있었던 고통을 겪는 사례를 저는 너무나도 많이 보아왔습니다.

확장성을 미리 준비하지 말아야 할 가장 큰 이유는 그게 생각처럼 작동하지 않기 때문입니다. 제 경험상, 어느 정도 규모가 있는 코드베이스라면 트래픽이 지금보다 몇 단계씩 늘어났을 때 시스템이 어떻게 작동할지 미리 예측하는 것은 불가능합니다. 어디가 병목 지점(bottleneck)이 될지 미리 알 수 없기 때문입니다.

기껏해야 현재 트래픽의 2배나 5배 정도를 감당할 수 있는지 확인하는 게 최선이며, 그 이상의 문제는 실제로 상황이 닥쳤을 때 해결할 수 있도록 대기하는 수밖에 없습니다.

이런 시도(미래를 대비한 설계)를 하지 말아야 할 또 다른 이유는 코드베이스가 경직되기 때문입니다. 서비스를 두 개로 분리해서 각각 독립적으로 확장할 수 있게 만드는 건 분명 즐거운 작업일 겁니다. (저는 이런 광경을 열 번 정도 목격했지만, 실제로 그렇게 분리해서 유의미하게 독립적 확장을 해낸 사례는 아마 한 번 정도밖에 보지 못했습니다.)

하지만 이렇게 분리하면 특정 기능을 구현하기가 매우 어려워집니다. 이제 네트워크를 사이에 두고 서로 정보를 주고받으며 합을 맞춰야(coordination) 하기 때문입니다. 최악의 경우에는 네트워크상에서 **트랜잭션(transactions over the wire)**을 처리해야 할 수도 있는데, 이는 정말로 난이도가 높은 공학적 문제입니다.

사실 대부분의 상황에서는 이런 고생을 할 필요가 전혀 없습니다!

최종 생각

기술 업계에서 오래 일하면 할수록, 저는 우리가 시스템의 미래를 예측할 수 있다는 낙관적인 믿음을 버리게 됩니다. 시스템의 현재 상태를 제대로 파악하는 것만으로도 충분히 벅찬 일이기 때문입니다.

사실, 좋은 설계를 하는 데 있어 실질적으로 가장 큰 걸림돌은 바로 이것입니다. 시스템의 전체적인 그림(big-picture)을 정확하게 파악하는 것 말이죠. 대부분의 설계는 이러한 이해 없이 이루어지며, 그렇기 때문에 대부분의 설계가 엉망인 것입니다.

소프트웨어를 개발하는 방식에는 크게 두 가지가 있습니다.

첫째는 요구사항이 6개월이나 1년 뒤에 어떤 모습일지 미리 예측하고, 그 목적에 맞는 최적의 시스템을 설계하는 것입니다.

둘째는 지금 당장 필요한 요구사항에 딱 맞는 최적의 시스템을 설계하는 것입니다. 다시 말해, ‘작동 가능한 가장 단순한 일’을 하는 것이죠.

추가: 이 글이 해커 뉴스에서 몇몇 댓글을 받았습니다.

그중 흥미로운 댓글 하나는, 대규모 시스템에서는 아키텍처의 단순함이 중요하지 않다고 주장합니다. 왜냐하면 ‘구현 시 상태 공간 탐색(state space exploration in implementation)‘의 복잡성(제 생각에는 제가 여기에 썼던 내용과 비슷한 의미인 것 같습니다)이 다른 모든 복잡성을 압도하기 때문이라는 것이죠.

저는 동의하지 않습니다. 기능 간의 상호작용이 복잡해질수록 단순한 아키텍처는 훨씬 더 중요해집니다. 왜냐하면 당신에게 주어진 ‘복잡도 예산(complexity budget)‘은 이미 거의 바닥나기 직전이기 때문입니다.

또한 이 표현을 처음 만드신 워드 커닝햄(Ward Cunningham)과 켄트 벡(Kent Beck)께 감사의 마음을 전합니다. 솔직히 말하자면 제가 직접 생각해낸 문구라고 진심으로 믿고 있었는데, 아마 예전에 읽었던 것을 기억해낸 것이 거의 확실해 보입니다. 이런! 이 점을 지적해주신 해커 뉴스 사용자 ‘ternaryoperator’님께 감사드립니다.