추상화, 결합도, 실전
- 7.1 정보 은닉과 추상화는 한몸이다
- 7.2 '큰 진흙탕'이 된 코드의 원인을 찾아서
- 7.3 추상화를 높이려면 테스트 코드부터 작성하라
- 7.4 좋은 추상화가 핵심이다
- 7.5 구멍 난 추상화
- 7.6 세계 지도와 지하철 노선도의 비유로 배우는 추상화 기법
- 7.7 이벤트 스토밍으로 추상화를 달성하자
- 7.8 추상화된 우발적인 복잡성
- 7.9 타사 시스템과 타사 코드를 격리하자
- 7.10 추상화와 구상화 사이의 트레이드오프
- 7.11 너무 느슨해도 너무 긴밀해도 문제
- 7.12 수직 확장을 위해서는 결합도가 필수
- 7.13 마이크로서비스: 결합도를 분리하기 위한 효과적인 방법
- 7.14 느슨한 결합도의 대가: 더 크고 많아진 코드
- 7.15 결합도 모델은 한 종류만이 아니다
- 7.16 어쨌든 느슨한 결합이 긴밀한 결합보다는 좋다
- 7.17 느슨한 결합과 관심사 분리의 상관관계
- 7.18 DRY는 너무 단순하다
- 7.19 느슨한 결합을 위한 비동기식 구현 방법
- 7.20 느슨한 결합을 위한 설계
- 7.21 강건한 결합도로 영원히 고통받는 대규모 조직
- 7.22 소프트웨어 개발 그 진실에 대하여
- 7.23 테스트 가능한 코드 사례 1
- 7.24 시스템은 반드시 측정 가능해야 한다
- 7.25 테스트 가능한 코드 사례 2
- 7.26 테스트: 시스템의 시작
- 7.27 배포: 시스템의 완성
- 7.28 피드백의 속도: 더 나은 품질과 결과물을 위한 필수 요소
- 7.29 시스템의 전 과정에서 변수를 통제하자
- 7.30 지속적인 배포를 잊지 말자
- 7.31 소프트웨어에서 고려해야 할 질문들
- 7.32 팀이나 조직도 복잡성의 관리대상임을 잊지 말자
- 7.33 디지털적으로 파괴적인 조직을 추구하자
- 7.34 결과 vs 메커니즘 무엇이 더 중요할까
- 7.35 소프트웨어 공학 원칙은 머신러닝 시스템에도 유효하다
- 7.36 모던 소프트웨어 엔지니어링의 핵심 아이디어
추상화와 결합도를 다루고 나면, 남은 건 이 원칙들을 실전에서 어떻게 적용하고 조직 전체로 확장할 것인가야.
좋은 추상화는 지하철 노선도야. 세계 지도가 아니라. 세계 지도는 지형의 실제 모습을 최대한 정확하게 보여주려고 해. 정확하지만 복잡하지. 반면 지하철 노선도는 지리적 정확성을 완전히 무시하고 역과 역 사이의 연결 관계만 보여줘. 승객은 터널이 어떻게 구불구불 뚫려 있는지에 관심 없잖아. "몇 정거장을 가고, 어디서 갈아타느냐"만 알면 돼. 사용자의 목적에 맞게 불필요한 정보를 제거한 거야. 이게 추상화의 본질이야.
코드에서 userRepository.findById(id)라고 쓰면, SQL 쿼리의 세부사항은 숨겨지고 "ID로 사용자를 찾는다"라는 핵심만 남아. 추상화를 잘 하면 정보 은닉이 자연스럽게 따라오는 거야. 이 둘은 한몸이야.
근데 이 원칙이 무너지면 어떻게 될까? **큰 진흙탕(Big Ball of Mud)**이 돼. 처음에는 작고 깔끔했던 코드가, 기능이 추가되면서 "빨리 이 버그 고쳐야 하니까 일단 내부 변수를 직접 바꾸자" 같은 결정이 쌓이면 모든 것이 모든 것에 의존하는 상태가 돼. 내부 구현에 직접 접근하는 지름길은 단기적으로는 빠르지만, 장기적으로는 시스템을 진흙탕으로 만들어.
그래서 Farley는 외부 라이브러리나 서드파티 API도 안티코럽션 레이어로 감싸라고 해. Stripe 결제 API를 코드 전체에서 직접 쓰지 말고, PaymentGateway라는 인터페이스를 만들어서 StripePaymentGateway가 실제 호출을 담당하게 하는 거야. 나중에 PayPal로 바꾸더라도 비즈니스 로직은 한 줄도 안 고쳐도 돼.
물론 추상화가 항상 좋은 건 아니야. 클래스 하나의 실제 동작을 파악하려고 인터페이스 5개를 따라가야 한다면 — 그건 과도한 추상화야. 반대로 구체적인 구현에 전부 엮여서 변경이 불가능하면 — 그건 추상화 부족이야. Farley의 기준은 명쾌해: "변경될 가능성이 있는 것은 추상화하고, 변경될 가능성이 없는 것은 구체적으로 둬라." DB를 바꿀 가능성이 있다면 리포지토리 패턴으로 감싸고, 문자열 연결을 추상화할 필요는 없어.
조엘 스폴스키가 말했듯이 모든 비자명적 추상화는 어딘가 구멍이 나. TCP가 네트워크 끊김을 완전히 숨기지 못하고, ORM이 N+1 쿼리 문제를 만드는 것처럼. 구멍을 완전히 피할 수는 없지만, 구멍이 있다는 사실을 인식하고 아래 계층에 대한 이해도를 갖추는 게 중요해.
추상화의 다음 단계로, 결합도 이야기에서 가장 위험한 함정은 **"결합도는 무조건 낮출수록 좋다"**는 생각이야. 결합도가 0이면 시스템이 동작하지 않아. 모듈이 서로 전혀 연결되지 않으면 협력이 안 되니까. 반대로, 모든 걸 메시지 큐를 통해 비동기로 통신하게 만들면 디버깅이 지옥이 돼 — 요청이 어디서 시작해서 어디서 끝나는지 추적이 안 되거든. 결합도에는 스위트 스팟이 있어. "필요한 만큼만 결합하라"가 원칙이야.
마이크로서비스를 보면 이게 명확해져. 프로세스를 분리했다고 결합도가 자동으로 낮아지는 게 아니야. 서비스 A가 서비스 B의 DB를 직접 읽고 있다면 — 프로세스만 분리된 분산 모놀리스, 최악의 상황이지. 마이크로서비스의 핵심은 프로세스 분리가 아니라 독립적인 배포 가능성이야. 다른 서비스의 변경 없이 혼자 배포할 수 있어야 진짜야.
여기서 Farley가 정말 날카로운 지적을 하나 해. DRY를 맹목적으로 따르면 결합도가 올라간다는 거야. 주문 서비스와 반품 서비스에 비슷한 검증 로직이 있으면 개발자는 본능적으로 공통 모듈로 추출하잖아. 근데 그 "비슷한 코드"가 서로 다른 이유로 변경되는 거라면? 주문 검증 규칙이 바뀔 때 반품 서비스에도 영향이 가. 이런 경우 코드 중복이 차라리 나아. 기준은 — "같은 이유로 변경되는 코드만 통합하라." 우연히 비슷해 보이는 코드는 각자 두는 게 맞아.
느슨한 결합을 추구하면 코드량은 늘어나. 인터페이스 정의하고, 어댑터 만들고, 팩토리 구현하고. 직접 함수 호출하면 한 줄이면 될 걸 5-10배 코드가 필요해질 수 있어. 하지만 시스템이 성장하면서 이 투자는 반드시 회수돼. 변경 비용이 낮아지거든. 물론 변경 가능성이 높은 부분에 집중적으로 적용하고, 안정적인 부분은 직접 결합을 허용하는 게 실용적이야.
대규모 조직에서 강한 결합도가 뭘 만드는지 알아? 릴리스 열차야. 하나의 기능을 배포하려고 5개 팀 승인을 받고, 릴리스 맞추려고 전체가 동시에 코드 프리즈하는 거. 한 팀이 늦으면 전체가 늦어져. 느슨하게 결합된 아키텍처를 가진 조직은 각 팀이 독립적인 택시를 타는 것과 같아 — 자기 속도로 자기 목적지로 갈 수 있지. 결합도를 해결하지 않으면, 인원을 두 배로 늘려도 산출물은 두 배가 안 돼. 브룩스의 법칙이 바로 이 현상이야.
이 모든 원칙을 실전에 적용하면 결국 피드백 속도가 핵심이야. 이 책의 핵심 메시지를 한 문장으로 줄이면 — 피드백이 빠를수록 소프트웨어가 좋아진다. 이건 단순한 의견이 아니라 DORA 연구로 뒷받침된 사실이야. Farley는 빌드 5분 이내, 단위 테스트 밀리초 단위, 커밋 후 1시간 이내에 프로덕션 도달을 목표로 하라고 해.
이걸 가능하게 만드는 전제 조건이 테스트 가능한 코드야. 주문 처리에서 유효성 검증, 재고 확인, 결제 처리, 이메일 발송이 하나의 메서드에 뒤엉켜 있으면 DB, 결제 게이트웨이, 이메일 서버를 전부 띄워야 테스트할 수 있어. 의존성 주입으로 관심사를 분리하면 각 외부 시스템에 테스트 더블을 주입해서 비즈니스 로직만 순수하게 검증할 수 있지. 비동기 시스템도 마찬가지야 — 이벤트 핸들러는 메시지를 파싱해서 비즈니스 로직 모듈에 넘기기만 하고, 로직은 동기적인 순수 함수로 만들면 밀리초 만에 테스트가 끝나.
피드백 속도를 높이려면 변수를 통제해야 해. 한 번에 100개 파일을 수정해서 배포하면 뭐가 문제인지 알 수 없어. 한 번에 하나의 작은 변경만 배포하면 원인을 즉시 파악할 수 있지. 피처 플래그로 배포와 릴리스를 분리하면 문제가 생겼을 때 플래그만 끄면 돼. 과학 실험과 똑같아 — 한 번에 하나의 변수만 바꿔야 그 변수의 효과를 알 수 있어.
이 모든 게 향하는 궁극적 목표가 **지속적 배포(CD)**야. 모든 커밋이 자동으로 프로덕션까지 가는 것. CD가 가능하려면 지금까지 다룬 모든 원칙이 필요해 — 모듈성, 응집성, 관심사 분리, 느슨한 결합, 좋은 추상화. 그리고 충분한 자동화 테스트가 있어야 수동 확인 없이 프로덕션에 내보낼 자신감이 생기지. CD는 도구의 문제가 아니라 문화와 설계의 문제야.
마지막으로, 코드의 복잡성만 관리하면 되는 게 아니라 조직의 복잡성도 관리 대상이야. 콘웨이의 법칙은 이 책에서 계속 등장했지. 4개 팀이 컴파일러를 만들면 4-패스 컴파일러가 나와. 소프트웨어 아키텍처를 바꾸고 싶으면 조직 구조를 먼저 바꿔야 할 수도 있어.
많은 조직이 결과에만 집중해. "이번 분기 매출 20% 성장." 근데 Farley는 메커니즘이 더 중요하다고 해. 메커니즘은 결과를 만들어내는 반복 가능한 프로세스야. CI는 코드 품질을 유지하는 메커니즘, 배포 파이프라인은 안전한 릴리스를 보장하는 메커니즘, TDD는 설계 품질을 유지하는 메커니즘. 좋은 메커니즘이 있으면 좋은 결과는 자연스럽게 따라와. 메커니즘 없이 결과만 추구하면 — 이번에는 운 좋게 되더라도 다음에는 보장이 없지.
ML 시스템도 예외가 아니야. "ML은 실험적이니까 소프트웨어 공학 원칙을 적용할 수 없어"라는 말에 Farley는 강하게 반대해. 오히려 ML이야말로 재현성, 다층적 테스트, CI/CD 파이프라인, 관심사 분리가 더 절실해. "실험적"이라는 건 더 빠른 피드백 루프가 필요하다는 뜻이지, 원칙을 무시해도 된다는 뜻이 아니거든.
결국 Farley가 말하는 모던 소프트웨어 엔지니어란 — 학습의 전문가이자 복잡성 관리의 전문가야. 가설을 세우고, 실험을 설계하고, 결과를 측정하고, 결론을 도출하는 사람. "이 방법이 좋다고 들었어"가 아니라 "이 방법을 적용했더니 배포 빈도가 2배 늘고 장애율이 반으로 줄었어"라고 말할 수 있는 사람. 이것이 공학과 코딩의 차이야.
정리
7장 읽고 기억할 거 세 가지:
- 추상화는 "변경될 가능성이 있는 것"에 집중하고, 결합도는 "필요한 만큼만" 허용하라. DRY를 맹목적으로 따르면 오히려 결합도가 올라가고, 서드파티 코드는 안티코럽션 레이어로 격리해야 해.
- CD는 도구가 아니라 문화와 설계의 문제야. 모듈성, 응집성, 관심사 분리, 느슨한 결합이 모두 갖춰져야 모든 커밋을 자신 있게 프로덕션에 보낼 수 있어.
- 결과보다 메커니즘에 투자해. CI, TDD, 배포 파이프라인 같은 반복 가능한 프로세스를 확립하면 좋은 결과는 자연스럽게 따라와. 이게 모던 소프트웨어 엔지니어의 핵심이야.