Skip to content

Latest commit

 

History

History
102 lines (92 loc) · 63.9 KB

File metadata and controls

102 lines (92 loc) · 63.9 KB

3부. 소프트웨어 복잡성 관리를 위한 기본 원칙 5가지

9장. 모듈성: 분리와 재조합을 위한 기준

  • 모듈성(Modularity)의 전형적 특징: 모듈성이란 큰 시스템을 작고 이해하기 쉬운 구성 요소(모듈)들로 분리하는 설계 철학이다. 이상적인 모듈은 자신의 기능을 독립적으로 수행하며, 잘 정의된 인터페이스를 통해서만 다른 모듈과 상호작용한다. 예를 들어 전자제품의 모듈화처럼, 소프트웨어에서도 UI, 비즈니스 로직, 데이터 접근 등을 분리하면 각각 교체나 수정이 쉬워진다. 모듈별로 개발과 테스트가 가능하기 때문에 전체 개발 속도와 품질이 향상된다.
  • 설계는 언제나 중요하다: “코드 몇 줄이면 되는데 굳이 설계가 필요해?”라고 생각하기 쉽지만, 모든 소프트웨어에는 적절한 수준의 설계가 필요하다. 특히 모듈 경계를 어떻게 정할지는 시스템 성공의 관건이 된다. 설계를 전혀 안 할 수는 없으며, 모듈 간 관계를 제대로 정의하지 않으면 금방 스파게티 얽힌 구조가 되어버린다. 좋은 설계란 필요한 곳에 적절한 추상화와 분리를 적용하는 것이다. 초기 설계가 부족하면 나중에 구조적 문제를 해결하느라 더 큰 비용을 치른다. 반대로 초기부터 모듈 경계를 잘 설정해두면, 규모가 커져도 구조가 유지되어 복잡성을 제어할 수 있다.
  • TDD의 교훈: 테스트가 어렵다면 설계도 문제다: 만약 작성한 코드에 대해 단위 테스트를 만들기가 힘들다면, 이는 모듈화가 부족하거나 결합도가 높다는 신호일 수 있다. 예를 들어 함수가 너무 많은 일을 하거나, 여기저기 전역 상태에 의존하면 테스트하기 어렵다. TDD 관점에서 “테스트하기 쉬운 설계가 좋은 설계” 인 경우가 많다. 실제로 개발자들은 테스트 작성이 난해할 때 코드를 리팩토링하여 모듈 경계를 수정하고, 의존성을 분리하면서 설계를 개선한다. 모듈성이 높고 응집된 코드일수록 테스트 작성이 쉽고, 이는 곧 그 설계가 건강함을 나타낸다.
  • TDD로 모듈성을 강화하자: TDD(Test-Driven Development)를 하면 자연스럽게 모듈 단위의 사고를 하게 된다. 왜냐하면 테스트는 작은 단위(함수 또는 클래스)의 기능을 검증하므로, 코드를 그 단위로 쪼개 작성하게 된다. 또한 테스트 통과를 위해 결합도를 낮추고 의존성을 주입하는 등의 작업이 필요해지므로 결과적으로 코드 구조가 더 모듈화된다. 예를 들어, 어느 클래스가 I/O와 연산 로직을 모두 담고 있다면 TDD로 테스트하기 어렵기에, 로직 부분을 별도 모듈로 분리하는 식이다. 이렇게 테스트 친화적 개발 습관이 모듈성을 향상시킨다.
  • REST API로 모듈성을 강화하자: 시스템을 서비스화하거나 분산시킬 때, RESTful API와 같은 명확한 통신 인터페이스를 활용하면 모듈성을 높일 수 있다. 하나의 거대한 애플리케이션을 여러 마이크로서비스로 나누고 REST API로 소통하게 하면, 각 서비스가 독립적인 모듈 역할을 한다. REST API는 엄격한 인터페이스(HTTP 요청/응답) 를 통해 결합도를 낮추며, 언어나 플랫폼이 달라도 모듈 간 협력이 가능하도록 해준다. 물론 모든 경우에 서비스를 나누는 것이 옳진 않지만, 시스템 경계를 API로 명확히 정의하는 것은 높은 모듈성을 얻는 효과적인 수단이다.
  • 배포 파이프라인으로 모듈성을 강화하자: CI/CD 환경에서 모듈 단위로 빌드하고 배포할 수 있다면, 그 자체로 아키텍처 모듈성이 개선된 것이다. 예를 들어 독립적인 라이브러리나 서비스는 그 부분만 따로 배포 가능해야 한다. 모놀리식 시스템에서는 작은 변경도 전체를 재빌드/재배포해야 하므로 비효율적이다. 반대로 모듈별 파이프라인이 있으면, 한 모듈 업데이트가 다른 부분에 영향 없이 바로 사용자에게 전달될 수 있다. 이런 모듈별 배포 독립성을 확보하려면, 사전에 코드 구조를 느슨히 결합시키고 경계를 명확히 설계해야 한다.
  • 모듈성의 규모는 크고 작음이 없다: 모듈화 원칙은 거대한 엔터프라이즈 시스템부터 간단한 학년별 프로젝트까지 적용 범위에 상관없이 유효하다. 예를 들어 간단한 파이썬 프로그램이라도 파일별/클래스별 역할 구분을 명확히 하면 이해하기 쉬워진다. 반대로 대규모 시스템도 결국 모듈들의 집합이므로, 작은 모듈 하나하나가 건강해야 전체 시스템의 복잡성에 먹혀들지 않는다. 또한 모듈성은 코드뿐 아니라 하드웨어 구성, 팀 조직, 배포 패키지 구조 등 여러 차원에서 고려될 수 있다. 중요한 건 크기에 관계없이 “응집도 있게 묶고, 관련 없는 것은 분리한다”는 기본 원칙이다.
  • 고성과 개발 조직의 특징: 모듈형: 업계 연구를 보면 높은 성과를 내는 IT조직은 제품 구조와 조직 구조 모두 모듈화되어 있는 경우가 많다. Conway의 법칙에 따라, 조직이 기능별로 나뉘어 협력하면 소프트웨어도 그 경계에 따라 모듈화된다. 아마존이나 넷플릭스 같은 기업은 소규모 팀들이 각기 서비스를 책임지는 마이크로서비스 + DevOps 모델을 활용해 빠른 개선과 확장을 이룬 것으로 유명하다. 이는 결국 모듈성 원칙을 조직 운영에까지 적용한 사례라 볼 수 있다. 이러한 구조에서는 한 팀이 자신의 모듈을 독립적으로 개발/배포하므로 전체 속도가 빨라지고, 문제도 국지화되어 복구가 신속하다.
  • 정리: 모듈성은 복잡한 문제를 다루는 기본 전략이다. 분리할 것은 분리하고, 재조합할 때 명확한 계약에 따라 연결하는 것이다. 모듈성이 높으면 한 부분을 수정할 때 다른 부분에 미치는 영향이 적고, 여러 팀이 병렬로 작업할 수 있어 개발이 스케일 업된다. 다만 지나치게 잘게 나누면 관리 포인트가 늘어나하는 만큼 적정 수준의 분리가 필요하다. 결국 경험을 통해 어디까지 모듈로 뗄지 결정하는 것이 엔지니어의 몫이며, 이는 앞으로 배울 다른 원칙들(응집도, 결합도 등)과 균형을 이뤄야 한다.

10장. 응집성: 소프트웨어의 관련 요소들은 한곳에

  • 모듈성과 응집성의 관계: 응집성(cohesion)이란 모듈 내부 구성 요소들이 서로 얼마나 밀접하게 관련되어 있는가를 나타낸다. 높은 응집도란 한 모듈이 하나의 책임이나 기능에 집중하고 있음을 의미하며, 이는 곧 모듈성이 잘 설계되었음을 뜻한다. 모듈성은 시스템을 나누는 기준이고, 응집성은 나눈 각 조각의 내부 품질이다. 이상적인 설계에서는 모듈 간에는 느슨하게, 모듈 내부는 굳게 뭉쳐 있게 된다. 두 원칙 모두 중요하며, 보통 높은 응집도는 낮은 결합도와 함께 간다 (관련 기능을 한곳에 모으면 다른 부분과 상호작용이 줄어드므로).
  • 응집성 개선을 위한 리팩토링 사례: 예를 들어 어떤 클래스 UserManager가 사용자 인증, 이메일 발송, 포인트 계산 등 여러 기능을 다 담당하고 있다면 응집도가 낮다. 이를 리팩토링하여 인증 전담 클래스, 메일 전담 클래스, 포인트 전담 클래스로 분리하면 각각의 응집도가 높아진다. 이 과정에서 UserManager는 실제로 인증/메일/포인트를 관리하는 여러 모듈을 조합하는 역할로 단순화된다. 결과적으로 각 모듈은 자신의 목적에 충실해져 이해하기 쉬워지고, 한 모듈의 변경이 다른 기능에 영향 주지 않게 된다. 이렇게 리팩토링으로 응집도를 높이면 유지보수성이 극적으로 향상된다.
  • DDD의 컨텍스트로 응집성 개선: 도메인 주도 설계(DDD)에서는 Bounded Context(경계 컨텍스트) 개념으로 대규모 시스템을 여러 도메인 영역으로 나눈다. 예를 들어 전자상거래 시스템이라면 주문, 결제, 배송 등이 각각 별도 컨텍스트가 된다. 이는 해당 컨텍스트 내서는 모든 관련 개념이 한 곳에 모이도록 해 응집도를 높이는 효과가 있다. 하나의 컨텍스트 안에서는 용어의 의미가 일관되고, 그 컨텍스트에 속한 기능들은 긴밀히 연관되어 동작한다. DDD를 따르면 도메인 경계를 따라 시스템이 모듈화되고, 각 부분의 응집도가 자연히 올라간다. 이는 비즈니스 개념 단위로 코드가 조직화되는 것이므로, 이해하기 쉽고 변경 영향 범위가 제한되는 장점이 있다.
  • 소프트웨어에서 ‘고성능’의 의미: 물리 기계와 달리, 소프트웨어에서 성능 좋은(High-performance) 시스템이란 꼭 처리 속도만 의미하지는 않는다. 변화에 빠르게 대응할 수 있는 시스템도 일종의 고성능 시스템이라 할 수 있다. 이러한 유연성의 뿌리에는 높은 응집도와 낮은 결합도가 있다. 응집도가 높으면 특정 기능을 개선하거나 수정할 때 그 부분만 고려하면 되므로 개발 속도가 빠른다. 반면 응집도가 낮은 시스템은 한 기능 바꾸려다 엉뚱한 부분까지 영향이 미쳐서 수정에 오래 걸린다. 그러므로 소프트웨어의 성능(팀 생산성 관점에서)은 설계 품질, 특히 응집성과 결합도 같은 속성과 깊이 연관되어 있다.
  • 결합도와 응집성 사이의 관계: 일반적으로 높은 응집도(high cohesion) 를 유지하면 낮은 결합도(low coupling) 를 자연스럽게 얻는 경우가 많다. 예컨대 한 모듈에 관련 기능을 몰아넣으면, 그 모듈이 다른 모듈과 통신해야 할 필요성이 줄어들기 때문이다. 반대로 응집도가 낮아서 이곳저곳 흩어진 기능들을 서로 호출하게 되면 모듈 간 결합도가 높아진다. 따라서 시스템을 설계할 때는 응집도와 결합도를 동시에 고려해야 한다. “한 모듈의 내부는 단단히 뭉치고, 모듈 사이 연결은 최소화” 가 최상의 시나리오이다. 이런 설계에서는 변경이 모듈 내부에서 흡수되고 외부로 파급되지 않으므로 유지보수가 쉬워진다.
  • TDD로 응집성을 높이자: 앞서 TDD가 모듈화에 도움된다고 언급했듯이, 테스트는 응집도에도 지표 역할을 한다. 어떤 클래스를 단위테스트하려는데 준비해야 할 다른 객체가 너무 많다면, 그 클래스가 여러 역할을 섞어 수행하고 있을 확률이 높다. TDD를 통해 이런 부분을 감지하면, 자연스럽게 코드를 SRP(단일 책임 원칙) 에 맞계 리팩토링하게 된다. 예컨대 한 클래스에서 두 가지 일이 이루어진다면, 두 테스트 케이스를 작성해야 하고 이는 불편하니 클래스를 둘로 나누는 식이다. 이런 식으로 TDD는 코드의 응집도가 낮을 때 개발자로 하여금 냄새를 맡게 해주고, 개선 방향을 제시해준다.
  • 응집성 있는 소프트웨어를 만들려면: 먼저 명확한 책임 분리 원칙을 세우고 코드를 작성해야 한다. 각 클래스나 모듈이 “하나의 액터(actor)” 처럼 행동하도록 설계하는 것이다. 함수도 마찬가지로, 가능한 한 하나의 일만 수행하도록 한다. 또 네이밍(이름짓기)을 응집도 판단에 활용할 수 있다. 만약 클래스 이름을 지었는데 “~Manager”처럼 포괄적인 이름이 된다면, 그 안에 여러 기능이 섞여 있을 확률이 있다. 차라리 좀 더 구체적인 이름을 지을 수 있도록 클래스를 쪼개보는 것이 좋다. 마지막으로, 코드 구조를 기능별/계층별로 폴더링하여 관련 파일끼리 모아주는 것도 응집도 향상에 도움 된다 (예: MVC 패턴에서는 Model, View, Controller 코드를 각각 디렉토리로 구분).
  • 응집성이 부족할 때 치러야 할 대가: 낮은 응집도의 대표적 결과물이 “Big Ball of Mud”(큰 진흙덩어리) 아키텍처다. 여기서는 모든 것이 모든 것과 얽혀 있어, 작은 변경 하나도 시스템 곳곳에 예상치 못한 영향을 끼친다. 개발자는 특정 기능을 수정하려면 전혀 관련 없어 보이는 다른 부분까지 일일이 확인해야 하고, 버그는 수정하면 다른 곳에서 재발한다. 새로운 기능 추가 시 기존 코드와 뒤엉켜 복잡도가 기하급수적으로 증가한다. 이러한 상황에서는 개발 속도가 갈수록 느려지고, 품질은 떨어지며, 개발자도 지쳐 간다. 결국 시스템을 새로 만드는 것 외엔 답이 없다는 말까지 나오곤 한다. 응집성 부족은 기술 부채를 키우고 팀 생산성을 갉아먹는 주범이므로 초기부터 경계해야 한다.
  • 개발 조직 관점에서 응집성의 중요성: 응집도가 높은 코드베이스는 팀의 업무 분담과 협업에도 유리하다. 예를 들어 모듈 A와 B가 잘 분리돼 있고 각각 응집도가 높다면, 팀원 한 명이 모듈 A를 전담하고 다른 한 명이 B를 맡아 병행 개발하기가 쉽다. 그러나 모듈 경계가 엉망이면 여러 사람이 동시에 같은 파일을 수정해야 하므로 충돌이 잦고 비효율적이다. 또한 응집도가 높으면 신입이나 다른 팀원이 특정 부분을 이해하는 데 걸리는 시간도 단축된다. “이 모듈은 X기능만 한다”고 명확히 말할 수 있으면 온보딩이 수월하다. 요컨대 응집성은 코드의 이해도와 협업 효율을 높여주는, 조직 생산성 측면에서도 중요한 속성이다.
  • 정리: 응집성은 “연관된 것은 함께, 관련 없는 것은 분리” 라는 간단한 원칙이지만, 지키지 않으면 큰 혼란에 빠진다. 높은 응집도의 모듈은 이해하기 쉽고 변경 영향이 모듈 내부에 국한되며, 팀원 간 작업 충돌도 적다. 이를 달성하려면 책임을 단일화하고, 기능별로 코드 구조를 정리하는 노력이 필요하다. 결국 모듈성, 응집성, 결합도는 한 세트로 고려해야 하는데, 이중에서도 “한 곳에 모아놓을 것은 확실히 모아놓는 것”이 응집성의 요체다.

11장. 관심사 분리: 고품질 코드의 가장 중요한 속성

  • 의존성 주입(Dependency Injection): 관심사 분리(SoC, Separation of Concerns)를 구현하는 실용적 기법 중 하나가 의존성 주입이다. 예를 들어 클래스 A가 클래스 B를 직접 생성해서 쓰는 대신, 외부에서 B의 인스턴스를 주입받게 하면 A와 B의 결합이 느슨해진다. 이는 A가 “자신의 로직”과 “필요한 외부자원 획득”이라는 두 관심사를 분리한 셈이다. DI를 활용하면 나중에 B를 교체하기도 쉽고, 특히 테스트 시에 가짜(Mock) 객체를 주입하여 A를 독립적으로 검증할 수 있다. 결과적으로 코드가 유연하고 테스트 용이해지며, 이는 유지보수성을 높여준다.
  • 본질적인 복잡성과 우발적인 복잡성: 프레드 브룩스는 소프트웨어 복잡성을 본질적인(essential) 것과 부수적인(accidental) 것으로 구분했다. 본질적 복잡성은 해결하려는 문제 자체의 어려움에서 기인하며, 이것은 아무리 해도 없앨 수 없다. 반면 우발적 복잡성은 사용한 기술나 구현 방식 때문에 생긴 불필요한 복잡함이다. 예를 들어 비즈니스 로직 자체는 간단한데, 개발자가 언어 사용을 잘못해 메모리 관리 버그로 고생한다면 이는 우발적 복잡성이다. 관심사 분리란 곧 우발적 복잡성을 줄이는 활동이다. 핵심 문제(본질)에 집중하고, 부가적인 것들은 추상화나 자동화를 통해 단순화하는 것이 목표다. 이렇게 하면 개발자는 중요한 로직에 집중할 수 있고 전체 시스템 이해도도 높아진다.
  • DDD는 중요하다: 경계 컨텍스트를 활용한 상향식 관심사 분리: 도메인 주도 설계(DDD)의 경계 컨텍스트(Bounded Context)는 관심사 분리의 한 형태다. 거대한 도메인을 context별로 분리하여, 각 컨텍스트 안에서는 그 분야의 언어와 모델에 집중하고, 다른 컨텍스트와는 명시적인 인터페이스(API)로 소통하게 한다. 이는 팀 조직과 코드베이스를 관심사(도메인 영역)별로 분할하는 효과가 있다. DDD 구현에서는 Context Map을 그려 컨텍스트 간 관계를 관리하는데, 이 역시 관심사들을 명확히 분리하고 통합하는 청사진이라 볼 수 있다. DDD를 제대로 적용하면, 비즈니스 로직, 인프라 구현, UI 등 서로 다른 관심사가 섞이지 않고 각자 맡은 역할에 충실한 시스템이 만들어진다.
  • 테스트하기 쉬운 코드 = 관심사가 분리된 코드: 단일 책임 원칙(SRP)과 관심사 분리는 코드의 테스타빌리티(testability) 와 직결된다. 예를 들어 데이터베이스에 접속하여 계산도 하는 함수라면, 테스트하려면 DB 세팅이 필요해 복잡해진다. 이를 관심사별로 둘로 나누어 계산 로직은 순수 함수로 만들고, DB 접근은 별도 계층으로 분리하면, 계산 함수는 테스트가 간단해진다. 즉, 어떤 코드를 독립적으로 테스트할 수 있다는 것은 그 코드가 다른 것들과 적게 얽혀 있다는 증거다. 잘 설계된 모듈은 자신의 관심사만 다루기에, 외부 세계와의 접점을 모의(Mock)하거나 차단하여 검증할 수 있다. 그러므로 테스트가 쉽도록 코드를 구조화하다 보면 자연스럽게 관심사별 분리가 이뤄진다.
  • 육각형 아키텍처: 포트와 어댑터: Hexagonal Architecture (허니콤 또는 육각형 아키텍처, Ports & Adapters 패턴)는 관심사 분리의 아키텍처적 접근이다. 핵심 아이디어는 애플리케이션을 안쪽 도메인 로직과 바깥 세계(I/O) 로 나누고, 이 둘을 포트(인터페이스)와 어댑터(구현체)를 통해 연결하는 것이다. 예를 들어 도메인 로직은 DB나 UI에 대해 전혀 모르며, 단지 “Repository”라는 인터페이스(포트)를 통해 데이터 접근을 한다. 실제 DB 연동은 Repository 인터페이스를 구현한 어댑터 클래스가 담당한다. 이 구조에서는 도메인(핵심 관심사)과 기술세부(부차 관심사)가 철저히 분리된다. 테스트 시에는 어댑터 대신 가짜 구현으로 대체할 수 있고, DB종류를 바꾸거나 UI를 웹에서 CLI로 바꿀 때도 도메인 로직은 손댈 필요가 없다. 결국 육각형 아키텍처는 코어 비즈니스 로직과 주변 기술 관심사를 깨끗이 분리하는 강력한 방법이다.
  • 포트와 어댑터는 언제 채택해야 할까: 이상적으로는 모든 프로젝트에 좋은 아키텍처를 적용하면 좋겠지만, 현실적으로 작은 규모 앱에까지 복잡한 분리 구조를 만들 필요는 없다. 애플리케이션이 여러 종류의 입출력 채널을 가져야 하거나, 오래 유지보수되어 변경이 잦을 것으로 예상된다면 포트-어댑터 패턴을 고려해볼 만하다. 예컨대 동일한 도메인 로직을 두 가지 UI (웹/모바일)로 제공하거나, DB를 추후 교체할 여지가 있는 경우이다. 반면 단기간에 끝날 간단한 스크립트나 1인 개발 프로젝트라면, 너무 구조를 분리하면 개발 속도만 늦출 수 있다. 핵심은 변화의 가능성이 높은 부분을 사전에 추상화해두는 것이 관심사 분리의 목적이므로, 요구 변화나 시스템 확장 가능성을 판단해 적절한 수준으로 적용하면 된다.
  • API가 단순한 함수 호출이 아닌 이유: 모듈 간 상호작용을 위해 API(Application Programming Interface) 를 설계할 때도 관심사 분리를 고려해야 한다. 예를 들어 클래스 내부 함수 호출은 같은 프로세스 내에서 즉각 일어나지만, 서비스 A의 API를 서비스 B가 호출하는 경우 네트워크 지연, 장애 대비, 호환성 유지 등 추가 관심사가 생긴다. API는 두 모듈(또는 시스템) 사이의 계약이므로 버전 관리, 오류 처리, 보안, 데이터 호환 등 신경 써야 할 부분이 많다. 따라서 단순히 함수를 노출하는 것과 API 제공은 차원이 다르다. API 설계시 이러한 부가 관심사(통신 프로토콜, 인증 등) 를 적절히 캡슐화하여, API 소비자는 내부 구현이나 통신 세부를 몰라도 자신의 로직에 집중할 수 있게 해야 한다. 좋은 API는 내부 복잡성을 숨기고 클라이언트가 자신의 관심사(비즈니스 로직)에만 집중하도록 돕는다.
  • TDD를 이용한 상향식 관심사 분리: 관심사 분리는 큰 그림 아키텍처에서도 중요하지만, 실제 코딩 단계의 작은 선택에서도 실현된다. TDD 같은 bottom-up 접근으로 코드를 만들다 보면, 자연스럽게 느슨한 결합의 작은 단위 함수/클래스들이 만들어진다. 이후 이들을 조립하면서 전체 구조가 형성되는데, 이때 각 단위가 서로 역할이 겹치지 않고 깔끔히 분리되도록 유지하면 된다. 예를 들어 TDD로 특정 기능의 도메인 로직을 개발하고, 나중에 I/O 부분을 붙이는 식이면, 도메인 로직과 I/O가 섞이지 않은 구조가 된다. 이러한 상향식 방법은 처음부터 거대한 레이어 아키텍처를 계획하기 어려운 상황에서, 코드를 작성하면서 자연스럽게 관심사를 분리하는 효과를 준다.
  • 정리: 관심사 분리는 코드의 구조화 원칙 중에서도 가장 핵심이라 할 수 있다. 이것이 잘 된 시스템은 이해하기 쉽고, 변경 시 국부적인 수정으로 충분하며, 재사용이 용이하다. 이를 실현하기 위해 의존성 주입, 계층화, 아키텍처 패턴 등 여러 기법을 활용할 수 있다. 중요한 점은 개발 단계마다 “이 코드에 여러 가지 다른 수준의 일이 섞여 있지는 않은가?” 를 자문하고, 가능하면 분리하는 것이다. 모던 소프트웨어 엔지니어링에선 작은 모듈 하나에도 관심사 분리를 적용하고, 나아가 팀과 서비스 수준에서도 이 원칙을 지켜 복잡성을 누그러뜨리는 디자인을 추구한다.

12장. 정보 은닉과 추상화: 우리의 적인가 친구인가

  • 정보 은닉과 추상화는 한몸이다: 정보 은닉(information hiding)은 모듈 내부의 구현 상세를 외부에 숨기는 것이고, 추상화(abstraction)는 복잡한 현실에서 핵심 개념만 드러내는 것이다. 둘은 사실 뗄 수 없는 관계로, 어떤 것을 감추느냐에 따라 무엇이 드러나는지가 결정된다. 예를 들어 클래스 내부 구현(자료구조, 알고리즘)을 숨기고 메서드 시그니처만 노출하면, 사용자에게는 그 메서드 명세(추상화된 기능)만 보인다. 이런 식으로 불필요한 세부사항을 가려서 개념적인 단순함을 이끌어내는 것이 정보은닉+추상화의 콤비네이션이다. 잘만 활용하면 복잡성을 크게 줄여주는 친구 같은 존재지만, 반대로 잘못된 추상화는 문제를 더 꼬이게 하는 적이 될 수도 있다.
  • ‘큰 진흙탕’(Big Ball of Mud)이 된 코드의 원인을 찾아서: 소프트웨어 아키텍처의 안티패턴인 Big Ball of Mud는 설계 원칙이 무시되고 모든 것이 얽힌 구조를 말한다. 이런 상태가 되는 원인은 다양하다:
    • 조직적/문화적 문제: 일정 압박으로 빨리빨리 기능만 추가하고 정리는 못하는 문화, 개발자들이 공통된 설계 원칙 없이 각자 코딩하는 상황, 또는 아키트 부재 등이 해당된다. 이 경우 정보 은닉이나 추상화 원칙을 따를 시간적/심리적 여유가 없으니, 시스템이 무계획적으로 커진다.
    • 기술적인 문제와 설계 부족: 처음에 작은 규모라 설계를 대충 했는데 폭발적으로 기능이 늘어난 경우, 초기의 단순구조가 누더기가 된다. 초반에 정보 은닉이나 모듈 경계 설정을 소홀히 하면, 나중에는 수정이 어려워져 억지로 이어붙인 코드가 양산된다. 또한 개발자들이 추상화보다는 복붙(copy-paste)에 의존하면 중복과 모순이 쌓여 코드가 진흙탕이 된다.
    • 과도하게 공들인 공학의 우려: 한편으론 추상화를 지나치게 추구해 불필요한 복잡 계층을 만든 경우도 있다. 예를 들어 간단한 기능에도 디자인 패턴을 남발해 클래스가 수십 개가 되면, 정작 개발자는 핵심 로직을 따라가기 힘들어진다. 추상화를 잘못 적용하면 오히려 추상층과 현실 구현 간의 괴리가 커져 버그 찾기가 어려워질 수 있다. 이러한 over-engineering 역시 결국 시스템을 이해하기 어렵게 만들어 진흙탕을 초래한다.
  • 추상화를 높이려면 테스트 코드부터 작성하라: 정보 은닉과 추상화 수준을 적절히 하려면 사용자 입장에서 생각하는 훈련이 필요하다. TDD를 하면 클라이언트 관점에서 API를 먼저 쓰게 되므로, 자연스럽게 “어떤 인터페이스가 편리할까?”를 고민하게 된다. 이는 필요한 추상화 수준을 결정하는 데 큰 도움을 준다. 예를 들어 어떤 객체를 사용할 때 테스트 코드에서 “이렇게 이렇게 호출하고 결과가 ~~ 나와야지”라고 쓰다 보면, 그 객체는 그런 동작을 추상적으로 제공해야 함을 알게 된다. 구현은 잠시 잊고 사용 시나리오에 맞춰 추상적인 인터페이스를 설계하면, 불필요한 세부 노출 없이도 충분히 기능을 쓸 수 있는 구조가 나온다. 이렇게 테스트 퍼스트 접근이 좋은 추상화의 가이드가 된다.
  • 좋은 추상화가 핵심이다: 모든 프로그래밍의 진보는 더 좋은 추상화를 만들어온 역사라고 해도 과언이 아니다. 절차적 언어 -> 객체지향 -> 함수형, 혹은 어셈블리 -> 고급언어 -> 라이브러리/프레임워크 -> 클라우드 서비스에 이르기까지, 계속 반복되는 테마는 “개발자가 신경 써야 할 사항을 줄여주는 추상화” 였다. 따라서 엔지니어는 현재 사용 중인 추상화들이 (언어, 프레임워크, 아키텍처 등) 제 역할을 잘 하고 있는지 늘 평가해야 한다. 좋은 추상화는 필요한 것만 보여주고 나머지는 완벽히 감춘다. 그 덕분에 우리는 세부사항에 정신 팔리지 않고 더 중요한 문제 해결에 집중할 수 있다. 이런 추상화의 힘 덕에 오늘날 복잡한 시스템도 쓸 수 있는 수준으로 관리되고 있는 것이다.
  • 구멍 난 추상화(Leaky Abstraction): 문제는 모든 추상화가 완벽하지 않다는 데 있다. Joel Spolsky의 “Leaky Abstraction 법칙”에 따르면 “모든 비트 trivial(자명)하지 않은 추상화는 어느 정도 누수를 가진다”. 즉, 추상화 계층이 아래 상세를 완벽히 숨기지 못하고 일부 속성이 드러나버린다는 것이다. 예를 들어 ORMs(Object-Relational Mapping)은 DB SQL 쿼리의 복잡성을 추상화해주지만, 개발자는 결국 성능 이슈가 생기면 SQL 튜닝 같은 낮은 단계 지식을 알아야 한다. 이런 추상화의 누수는 개발자에게 혼란과 추가 학습 부담을 준다. 추상화가 원래 의도와 다르게 동작하면 디버깅이 어려워지고, 숨겨진 부분까지 이해해야 하므로 인지부하가 커진다. 따라서 추상화 층을 설계할 때 의도치 않은 내부 정보가 외부로 새어나오지 않도록 면밀히 검토해야 한다. 그리고 외부에 공개된 부분(API, 문서)에 대해서는 향후 구현이 바뀌어도 일관되게 유지해야 추상화의 신뢰성이 확보된다.
  • 세계 지도와 지하철 노선도의 비유로 배우는 추상화 기법: 추상화란 목적에 맞게 정보를 취사선택하는 것이다. 세계 지도는 지형과 경계를 정확히 표현하지만, 지하철 노선도는 노선 연결관계에 집중하고 지리적 비율은 무시한다. 두 다이어그램 모두 현실의 추상화인데, 세계 지도는 여행자가 도시 간 거리를 가늠할 때 유용하고, 지하철 지도는 승객이 환승 경로를 파악하기에 유용하다. 소프트웨어도 마찬가지로, 사용 목적에 맞게 상세를 생략하거나 강조해야 한다. 예컨대 네트워크 토폴로지를 보여주는 다이어그램에서는 클래스 다이어그램 같은 세부는 생략하고 서비스 간 관계만 보여주는 식이다. 이러한 관점에서, 어떤 모듈의 인터페이스도 그 모듈을 사용하는 사람이 필요로 하는 기능만 드러내고 내부 구현은 감춘다. 결국 추상화는 지도를 그리는 작업과 같아서, 보는 사람이 필요한 정보에 집중할 수 있게 해주는 것이 핵심이다.
  • 이벤트 스토밍으로 추상화를 달성하자: 이벤트 스토밍(Event Storming)은 DDD 문맥에서 쓰이는 도메인 모델링 기법으로, 도메인에서 일어나는 사건(event)을 추출해내어 전체 그림을 파악하는 워크숍 방식이다. 여기서 엔지니어와 도메인 전문가들은 부착식 메모지 등에 중요한 이벤트 (“주문 생성됨”, “결제 승인됨” 등)을 적어 시간 흐름에 따라 배치한다. 상세 구현은 전혀 언급되지 않고, 오직 도메인 핵심 이벤트(업무 흐름의 추상화) 들만 다룬다. 이 과정을 통해 팀은 복잡한 비즈니스 로직을 추상화된 이벤트 흐름으로 이해하게 된다. 나아가, 이러한 이벤트들을 기준으로 Bounded Context를 나누거나 서비스 경계를 설정하기도 한다. 이벤트 스토밍은 추상화를 단체로 수행하는 예라 할 수 있으며, 개발 초기 아이디어 수집 및 모델 도출에 큰 도움이 된다.
  • 추상화된 우발적인 복잡성: 재밌는 모순처럼 들리지만, 잘못된 추상화는 그것 자체가 부수적인 복잡성을 만들어낸다. 예를 들어 지나치게 일반화된 프레임워크를 도입했는데 우리 프로젝트엔 오히려 복잡도만 더해주는 경우다. 또는 간단한 기능을 추상화 계층 여러 개를 통해 호출하도록 설계하면, 계층간 전달로 프로세스가 복잡해진다. 이처럼 필요 이상의 추상화는 개발을 어렵게 한다. 우발적 복잡성을 줄이려다 도리어 늘려버린 셈이다. 그러므로 추상화 수준은 요구에 딱 맞게, KISS(Keep It Simple, Stupid) 원칙에 따라 최소한으로 설정하는 게 중요하다. 추상화는 강력한 도구지만 남용하면 해가 될 수 있음을 항상 경계해야 한다.
  • 타사 시스템과 타사 코드를 격리하자: 현실적으로 완전히 우리의 통제 하에 있지 않은 코드나 시스템을 사용해야 할 때가 많다 (서드파티 라이브러리, 외부 API 등). 이때는 안정적인 추상화 계층을 우리 쪽에 만들어 외부 것을 감싸는 것이 좋다. 예를 들어 특정 서드파티 결제 시스템을 호출하는 코드를 여기저기서 직접 쓰지 말고, PaymentService라는 추상 인터페이스와 PaymentServiceImpl 어댑터를 한 곳에 만들어 놓고 사용한다. 이렇게 하면 나중에 서드파티가 변경되어도 그 어댑터만 수정하면 되고, 다른 코드들은 영향을 받지 않는다. 또한 외부 API 특유의 에러 처리나 데이터 형식도 우리 쪽 표준으로 변환해주면, 내부 로직은 일관된 추상화로 개발할 수 있어 편해진다. 요컨대 외부 요소로 인한 복잡성은 경계면에서 격리해두어야 우리 시스템 전체의 복잡도를 낮출 수 있다.
  • 추상화와 구상화 사이의 트레이드오프: 모든 것을 추상화하면 현실과 동떨어진 모델이 되고, 반대로 너무 구체적이면 범용성이 떨어진다. 이 균형을 맞추는 것이 엔지니어의 예술이다. 추상화 수준을 결정할 때는 YAGNI (필요 없을 것 같으면 넣지 마라) 원칙이 도움이 된다. 너무 미래까지 대비한 일반화는 불필요할 수 있으니, 현재 문제를 충분히 깨끗하게 해결할 정도까지만 추상화하는 것이 좋다. 그리고 구체 구현은 추상화 뒤에 숨기되 완전히 감추지 말고 문서화 등을 통해 “어떤 일이 벌어지는지” 힌트를 남겨두는 것도 필요하다 (특히 성능이나 중요한 부작용 관련). 이러한 세심한 접근이 추상화의 이점(단순성 유지)과 구체성의 이점(실제 동작 통제)을 모두 챙길 수 있게 해준다.
  • 정리: 정보 은닉과 추상화는 복잡성을 길들이는 무기다. 둘을 현명하게 활용하면 개발자는 꼭 필요한 사항에 집중하고 나머지는 신경 쓰지 않아도 되며, 시스템 변경도 국지적으로 일어난다. 그러나 잘못된 추상화는 오히려 혼란과 오류를 낳을 수 있으므로, 적정 수준의 추상화를 목표로 해야 한다. 모던 소프트웨어 엔지니어링에서는 “필요한 것만 노출하고, 나머지는 숨긴다”는 원칙 아래, 객체 지향의 캡슐화, 모듈 간 인터페이스, 계층 아키텍처, API 설계 등을 신중하게 다룬다. 이는 궁극적으로 대규모 복잡한 시스템도 이해 가능하고 유지 가능한 구조로 만드는 토대가 된다.

13장. 결합도: 소프트웨어 모듈 간의 상호 연관 수준

  • 너무 느슨해도 너무 긴밀해도 문제: 결합도(coupling)는 한 모듈이 다른 모듈에 얼마나 의존하고 연결되었는지를 나타낸다. 긴밀한 결합(tight coupling) 은 모듈들이 떼려야 뗄 수 없을 정도로 밀접한 상태로, 한쪽 변경 시 다른 쪽도 변경해야 하는 상황이다. 반대로 느슨한 결합(loose coupling) 은 모듈들이 최소한의 연결만 유지하는 상태다. 이상적으로는 결합도가 낮을수록 좋지만, 너무 지나치게 분리하면 모듈 간 의사소통 비용이 커지고 중복이 늘어날 수 있다. 예를 들어 서비스를 극단적으로 쪼개면 API 통신 오버헤드가 증가하고 관리 포인트도 많아진다. 따라서 결합도는 낮추되, 필요한 곳에서는 일정 수준 유지라는 균형점이 존재한다. “각각 독립적이지만 함께 유기적으로 동작” 하는 상태를 목표로 해야 한다.
  • 너무 느슨한 결합의 사례: 만약 시스템을 구성하는 컴포넌트들이 서로 거의 상호작용하지 않는다면, 기능 구현에 중복이 많아지거나 일관성 유지가 어려워질 수 있다. 예를 들어 데이터 일관성을 유지해야 하는 두 모듈이 너무 분리돼서 각각 별도 DB를 쓰며 소통이 없다면, 같은 데이터가 두 곳에 존재해 동기화 문제가 생길 수 있다. 또한 한 기능을 위해 여러 모듈이 연쇄 호출을 해야 할 때 인터페이스가 지나치게 추상적이면, 개발자가 전체 흐름 파악이 어렵고 디버깅도 힘들어진다. 이처럼 필요 이상으로 결합도를 낮추면 오히려 시스템 복잡성이 다른 형태로 증가한다 (ex: 더 많은 코드와 설정이 필요). 따라서 업무적으로 강한 연관이 있는 부분은 적절히 모아두는 편이 낫다.
  • 너무 긴밀한 결합의 폐해: 앞서 여러 번 다룬 대로, 모듈 간 결합도가 높으면 변경 파급이 커서 개발이 어려워진다. 한 모듈을 수정하려면 연관된 여러 모듈을 다 고쳐야 하고, 인터페이스 하나 바꾸면 사용하는 모든 곳을 업데이트해야 한다. 특히 거대한 모놀리식(monolithic) 애플리케이션은 구성 요소 간 결합도가 높기 쉬운데, 여기서는 배포도 전체를 한 번에 해야 하고, 한 부분 장애가 전체 시스템을 마비시킬 수도 있다. 또한 새로운 기술 도입이나 확장이 어려워 조직이 민첩성을 잃는다. 큰 조직일수록 이 강한 결합의 덫에 빠지면, 변화에 매우 둔감하고 유지보수 인력/시간이 기하급수적으로 필요해진다. 이는 궁극적으로 엔지니어링 생산성의 심각한 저하로 이어진다.
  • 수직 확장을 위해서는 결합도가 필수: 결합도도 완전히 나쁜 것은 아니다. 때로는 성능이나 통합 관리상의 이유로 일부러 결합도를 높이는 결정도 필요하다. 예를 들어 실시간 고성능이 요구되는 시스템에서는 함수 호출이나 메모리 공유를 통해 직접 결합된 통신을 사용하고, 이는 속도를 높여준다. 분산 시스템보다 하나의 프로세스 내에서 동작하도록 구성하면 레이턴시가 낮아지는 식이다. 또 다른 예로, 강한 트랜잭션 일관성이 필요하면 모듈들을 하나의 데이터베이스에 묶어 ACID 특성을 이용할 수 있다. 이처럼 제품 목표(고성능, 일관성 등)에 따라 일부 영역은 의도적으로 결합시키기도 한다. 이러한 수직적 통합은 성능은 좋지만 유연성이 희생되므로, 핵심 경로 외에는 남용하지 말아야 한다.
  • 마이크로서비스: 결합도를 분리하기 위한 효과적인 방법: 최근 소프트웨어 아키텍처의 대세 중 하나는 마이크로서비스(microservices) 이다. 이는 시스템을 작고 자족적인 서비스들로 분할하고, 각 서비스가 독립적으로 배포/운영되도록 하는 패턴이다. 마이크로서비스의 주요 목표는 서비스 간 결합도를 낮추는 것이다. 예를 들어 한 서비스가 다른 서비스 DB에 직접 접근하지 않고, 반드시 API를 통해 통신하도록 강제한다. 서비스들은 각자 독립된 기술 스택을 써도 되고, 장애가 나도 다른 서비스에 영향을 덜 준다. Netflix나 Amazon 등의 사례에서, 마이크로서비스로 전환하여 개발 팀별로 서비스를 맡아 결합도를 조직 단위로 분산시켰고, 그 결과 배포 속도와 신뢰성이 크게 향상되었다고 알려져 있다. 마이크로서비스는 결합도 완화를 극한까지 적용한 사례이며, 성공적으로 구현하면 대규모 시스템도 작은 서비스들의 모음으로 이해하고 관리할 수 있게 된다.
  • 느슨한 결합도의 대가: 더 크고 많아진 코드: 마이크로서비스 같은 접근에는 트레이드오프가 있다. 모듈 간 결합도를 낮추면, 모듈 간 연결을 위한 추가 코드가 많이 필요해진다. 네트워크 호출, 데이터 변환, 에러 재시도 등의 인프라 코드가 증가하고, 각 서비스마다 중복되는 유틸리티나 모델이 생길 수 있다. 또 데이터가 분산되면 중앙에서 한번 처리할 것을 각 서비스에서 중복 처리해야 하는 경우도 있다. 요컨대, 느슨한 결합을 이루는 대가로 시스템 전체 코드량과 복잡도는 어느 정도 증가한다. 이는 자동화 및 관리 도구의 힘으로 완화할 수 있지만, 너무 많은 서비스는 오히려 개발자에게 부담이 될 수 있다. 따라서 팀 규모와 능력에 맞게 적정 수준으로 분리하는 것이 중요하다.
  • 결합도 모델은 한 종류만이 아니다: 결합도에는 여러 유형이 있다. 예를 들어 내용 결합(content coupling): 한 모듈이 직접 다른 모듈 내부를 건드는 최악의 결합(전역 변수 공유 등), 제어 결합(control coupling): 모듈 A가 모듈 B의 동작을 제어하기 위해 플래그 등을 전달하는 것, 데이터 결합(data coupling): 모듈 간 순수한 데이터만 교환하는 가장 낮은 형태의 결합 등으로 분류한다. 또한 시간적 결합(temporal coupling) 같은 개념도 있는데, 이는 “두 기능이 항상 동시에 수정되어야 한다”처럼 변경 시점이 연동되는 관계를 뜻한다. 이런 다양한 결합 형태를 인지하면, 단순히 결합도 수치만 볼 게 아니라 무슨 종류의 결합이 문제인지 파악할 수 있다. 예컨대 내용 결합은 즉시 개선해야 하지만, 데이터 결합은 허용된다. 이런 식으로 결합도를 세분화해 진단하면 더 정교한 설계 개선이 가능하다.
  • 어쨌든, 느슨한 결합이 긴밀한 결합보다는 좋다: 이상적인 소프트웨어는 부품처럼 서로 독립적이면서 필요할 때만 소통하는 구조다. 현실적으로 100% 독립은 불가능하지만, 대부분 시스템에서 아직 불필요하게 엉켜있는 부분이 있기 마련이다. 예를 들어 UI 코드가 비즈니스 로직과 섞여 있다든지, 서비스 간에 중복 기능을 라이브러리로 공유하느라 강하게 결합되어 있다든지 하는 경우다. 이러한 부분을 찾아 약한 연결고리로 변경하면 많은 이점이 발생한다. 코드 변경 영향 범위가 줄고, 새로운 요구사항에 유연해지며, 성능이 조금 희생되더라도 유지보수 용이성이 높아진다. 특히 현대의 클라우드/분산 환경에서는 느슨한 결합 아키텍처가 확장성(scalability)과 지속적인 배포를 가능하게 해준다. 결합도를 완화하는 일은 초기에는 추가작업처럼 보여도, 장기적으로 비용을 아끼는 투자인 경우가 많다.
  • 느슨한 결합과 관심사 분리의 상관관계: 관심사 분리는 결합도와 응집도 개념을 포괄하는 상위 원칙이다. 자연스럽게 관심사가 잘 분리된 시스템은 모듈 간 결합도가 낮을 수밖에 없다, 왜냐하면 각 관심사가 독립 모듈로 존재하니까. 예를 들어 UI와 로직을 MVC 패턴으로 분리하면, UI 모듈과 로직 모듈은 명확한 인터페이스(모델)로 소통하며 결합도가 낮아진다. 반대로 한 화면 클래스에 UI와 데이터처리 로직이 다 들어있다면, 그 화면과 다른 부분은 긴밀히 얽힐 것이다. 그러므로 결합도를 낮추고 싶다면 먼저 관심사 분리를 점검해야 한다. 한 모듈이 여러 관심사를 다루고 있다면 응집도를 높이고 결합도를 낮추는 리팩토링 기회를 찾은 것이다.
  • DRY는 너무 단순하다: “Don’t Repeat Yourself”(코드 중복을 피하라)는 좋은 개발 원칙이지만, 결합도 관점에서 맹목적으로 따를 일은 아니다. 중복을 없애기 위해 모든 곳에서 공통으로 쓰는 유틸을 만들면, 그 유틸에 모든 곳이 결합된다. 변경 시 파급효과가 커지는 것이다. 그래서 마이크로서비스 설계에서는 의도적인 중복 허용이 나온다 – 서비스 간 코드를 공유 라이브러리로 뽑지 않고 각각 복사해서 갖도록 하는 것이다. 언뜻 비효율 같지만, 이렇게 하면 서비스들이 서로 독립적이어서 결합도가 내려간다. DRY 원칙만 추구하다 보면 오히려 결합도가 높아질 수 있으므로, 적절한 중복은 감수하고 모듈 독립성을 지키는 편이 좋다. 요컨대 실무에서는 “중복을 줄여 유지보수성 향상” vs “결합도 감소로 서비스 독립성 향상” 사이에서 균형 잡기가 필요하며, 경우에 따라 DRY보다 모듈 격리를 우선시해야 할 때가 있다.
  • 느슨한 결합을 위한 비동기식 구현 방법: 시스템 간 결합도를 낮추는 방법으로 비동기 메시징/이벤트 기반 아키텍처가 널리 쓰인다. 동기 방식(API 호출 등)은 호출자가 응답을 기다려야 해서 서로의 가용성에 영향을 받지만, 비동기 큐나 Pub/Sub를 사용하면 한 시스템이 메시지를 보내놓고 바로 다음 일을 할 수 있고, 수신 시스템은 자신의 페이스대로 처리한다. 이는 시간적 결합(temporal coupling)을 완화시킨다. 예를 들어 주문 시스템이 결제 시스템을 동기 호출하면 결제가 느릴 때 주문 흐름이 막히지만, 메시지 큐에 “주문 발생” 이벤트를 발행하면 결제 서비스는 알아서 처리하고 오류나 완료 상태만 나중에 업데이트하면 된다. 이러한 이벤트 중심 설계는 서비스들 간 느슨한 연결과 확장성을 제공하며, 새로운 서비스가 이벤트만 구독하면 기능 추가도 쉬워지는 효과가 있다.
  • 느슨한 결합을 위한 설계: 인터페이스와 추상화를 활용해 의존성 방향을 역전(DIP 원칙) 시키면 결합도를 크게 줄일 수 있다. 예를 들어 상위 모듈이 하위 모듈 구현에 직접 의존하지 않고, 추상 인터페이스에 의존하게 만드는 것이다. (하위 모듈이 그 인터페이스 구현체를 제공). 이러면 상위-하위 모듈 간 결합이 추상 인터페이스 하나로 약화된다. 또 다른 디자인 원칙은 팩토리 패턴이나 이벤트 디스패처 등을 써서 모듈 간 직접 호출을 줄이는 것이다. 콜백이나 이벤트 핸들러는 느슨한 연결을 가능케 하는 도구다. 그리고 표준 프로토콜 활용도 중요하다. 예를 들어 파일 저장 모듈을 로컬이든 클라우드든 POSIX 파일 I/O 인터페이스로 추상화하면, 나중에 구현 바뀌어도 호출부는 영향이 없다. 이처럼 설계 단계에서부터 모듈 간 계약을 잘 정의하면, 각 모듈은 계약만 지키면 되므로 서로 내부에 무슨 일이 있는지 몰라도 되는 관계가 된다.
  • 강건한 결합도로 영원히 고통받는 대규모 조직: 흔히 전통적 대기업들의 레거시 시스템이 이런 상태에 놓여 있다. 수십 년에 걸쳐 개발된 거대한 모놀리식 어플리케이션, 혹은 분산되어 있다고는 하지만 사실상 데이터와 로직이 얽혀있는 느슨하지 않은 SOA 등이 그렇다. 이런 환경에서는 작은 변화도 수많은 팀과 승인 절차를 거쳐야 하고, 하나 수정하면 예상치 못한 다른 부서 시스템이 오류나는 일이 발생한다. 조직 구조 역시 부서 간 결합도가 높아 서로 의존적인 프로젝트들이 발목을 잡는다. 이런 상황에서 탈피하려면 기술적 리팩토링뿐 아니라 조직 개편, 운영 프로세스 변화 등 큰 결단이 필요하다. 다행히 현대에는 클라우드 네이티브, DevOps 문화가 퍼지면서 많은 조직이 느슨한 결합 아키텍처 + 자율적인 팀 구조로 전환하는 추세다. 이는 단순한 기술 변경이 아니라 엔지니어링 생산성을 위한 필수 진화라 볼 수 있다.
  • 정리: 결합도는 시스템 구성 요소들이 얼만큼 서로 엮여 있나를 나타내는 지표로, 소프트웨어 설계의 난이도를 좌우한다. 낮은 결합도를 추구하면 모듈 독립성이 높아져 변경 용이성과 재사용성이 향상된다. 그러나 지나치게 분리하면 복잡도가 다른 형태로 나타날 수 있으므로, 응집도를 유지하면서 적절히 결합도를 관리하는 것이 중요하다. 모던 소프트웨어 엔지니어링에서는 마이크로서비스, 이벤트 드리븐, DI, 인터페이스 설계 등 다양한 기법으로 결합도를 제어하며, 그 목표는 변화에 민첩하고 확장 가능한 시스템을 만드는 것이다.

4부. 소프트웨어 엔지니어를 위한 아이디어

14장. 실제 사례로 되짚어 본 소프트웨어 공학

  • 소프트웨어 개발, 그 진실에 대하여: 이론과 원칙을 모두 적용하면 언제나 순탄할 것 같지만, 현실 프로젝트는 늘 예상치 못한 난관과 절충의 연속이다. 요구사항은 변하고, 완벽했던 설계에도 버그가 생긴다. 여기서 소프트웨어 공학의 가치는 빛난다. 공학 원칙은 문제가 발생하지 않게 하는 것뿐 아니라, 문제가 발생해도 효과적으로 대처할 수 있게 해주는 안전망을 제공한다. 실제 경험을 통해 “왜 테스트가 중요하고, 왜 자동화가 필요한지” 절감한 사례들을 살펴보자. 이는 앞서 배운 원칙들이 현장에서 어떻게 적용되는지 보여줄 것이다.
  • 테스트 가능한 코드 사례 1: 한 금융 시스템 팀에서 복잡한 이자 계산 모듈을 개발했는데, 처음에는 단위 테스트 없이 수동 테스트로 검증했다. 시간이 지나 유지보수 인력이 바뀌자, 겁나서 코드를 못 고칠 지경이 됐다. 나중에 들어온 시니어 엔지니어가 해당 모듈을 리팩토링하여 함수별 단위 테스트를 촘촘히 작성했고, 정보 은닉을 철저히 하여 모듈 경계를 명확히 했다. 그 결과 새로운 요구사항(복리 계산 방식 변경)이 들어와도 테스트를 돌려가며 안정적으로 수정할 수 있었다. 이 사례는 “테스트가 어려운 코드는 문제가 있다” 는 교훈과 함께, 테스트가 가능해지면 리팩토링이 용이해져 결과적으로 코드 품질이 올라간다는 것을 보여준다.
  • 시스템은 반드시 측정 가능해야 한다: 대규모 서비스 운영 사례에서 흔히 듣는 말이다. 예를 들어 어느 날 트래픽 폭주로 시스템이 느려졌는데, 정작 모니터링과 로깅이 미흡하여 원인을 못 찾는다면 엔지니어는 깜깜한 상태에서 추측만 하게 된다. 반면 잘 구축된 모니터링 시스템은 CPU, 메모리, 요청수, 오류율 등을 시각화해주고, 어느 모듈에서 병목이 생겼는지 데이터로 알려준다. 한 e커머스 회사는 모든 주요 기능에 대한 성능 지표와 비즈니스 지표(장바구니 추가율 등)를 실시간으로 수집한 덕분에, 문제 발생 시 신속히 근본 원인을 파악하고 대응할 수 있었다. 이는 경험주의와 피드백 원칙이 운영 단계에서도 똑같이 중요함을 일깨운다 – “보이는 소프트웨어” 를 만들어야 지속 개선이 가능하다.
  • 테스트 가능한 코드 사례 2: 한 웹 서비스 팀은 레거시 코드베이스에 자동 테스트가 거의 없었다. 새로운 기능 추가 시 기존 것들이 종종 망가졌고, 이를 발견하는 건 QA나 사용자 몫이었다. 이를 개선하기 위해 팀장은 캠페인 식 리팩토링을 진행했다: 매주 시간이 날 때마다 특정 컴포넌트를 선택해 단위 테스트를 추가하고, 테스트하기 어렵게 설계된 부분은 구조를 개선했다. 예를 들어 글로벌 상태에 의존하던 함수들을 객체로 묶고 DI를 적용하여 상태를 주입받게 변경하고, I/O 코드와 로직 코드를 분리했다. 몇 개월에 걸친 노력 끝에 테스트 커버리지가 0%대에서 60%대로 상승했고, 배포 시 사고가 현저히 줄었다. 개발자들은 이제 부담 없이 리팩토링할 수 있게 되었고 속도도 빨라졌다. 이 사례는 “빚을 갚듯 기술 부채(낮은 테스트 커버리지)를 갚아나가면 공학적 품질이 향상된다” 는 사실을 잘 보여준다.
  • 테스트: 시스템의 시작: 많은 초보 개발자는 개발 다 하고 마지막에 테스트한다고 생각하지만, 실제 뛰어난 팀은 테스트 작성으로 일을 시작한다. 요구사항을 테스트 시나리오로 풀어내면서 무엇을 만들지 명확히 정의하고, 개발 중간중간 돌려보며 바른 길로 가고 있는지 확인한다. 이러한 접근은 결국 개발의 전체 사이클에서 테스트가 중심에 있음을 뜻한다. CI 파이프라인에서 빌드보다 테스트가 오래 걸릴 정도로 수백, 수천 개의 케이스가 돌고, 이는 곧 시스템의 안전망이 된다. 한 금융업체는 규제가 엄격해 새로운 릴리스 전에 수만 개의 자동화 테스트를 통과해야만 배포를 승인하는 프로세스를 운영한다. 이처럼 테스트는 프로젝트의 마지막 체크리스트 항목이 아니라, 처음부터 끝까지 개발을 견인하는 엔진 역할을 한다.
  • 배포: 시스템의 완성: “컴파일되었다고 코드가 완성된 게 아니다. 프로덕션에 안정적으로 돌아가야 끝난 것이다.” 모던 소프트웨어 엔지니어링에서는 배포(Deployment)를 개발 라이프사이클의 필수 단계로 포함시킨다. 그래서 CI 다음에는 CD(Continuous Deployment)까지 이어지는 것이다. 어떤 팀은 매일 수십 회 자동 배포가 이뤄지는데, 이는 배포를 사람 수동이 아닌 시스템이 하도록 엔지니어링한 덕분이다. 배포 자동화 스크립트, IaC(Infrastructure as Code), 컨테이너 기술 등을 활용해, 개발자가 코드를 커밋하면 짧은 시간 내에 프로덕션 반영까지 되게 한다. 배포 파이프라인에 장애 감지와 롤백 메커니즘도 넣어 문제가 생기면 자동 되돌리기까지 처리한다. 이러한 배포 자동화는 개발팀이 큰 변화에도 두려움 없이 시도해볼 수 있게 해주며, 결국 더 빠른 피드백 루프를 완성한다.
  • 피드백의 속도: 더 나은 품질과 결과물을 위한 필수 요소: 앞서 여러 번 강조했듯, 피드백은 빠를수록 가치가 크다. 실무 예로, 어떤 모바일 앱 팀은 사용자 피드백 반영 주기를 기존 3개월에서 2주로 단축하자 앱 평점이 크게 올랐다고 한다. 사용자의 불만 사항이나 제안이 금세 다음 버전에 반영되니, 앱 품질이 개선되고 고객 만족도가 향상된 것이다. 또 다른 예로, A/B 테스트 플랫폼을 구축한 팀은 기능별 실험 결과(전환율 변화 등)를 며칠 내로 확인하여, 가설이 틀린 기능은 과감히 폐기하고 유효한 개선만 채택했다. 그 결과 개발 효율과 제품 품질이 동시에 상승했다. 결국 짧은 피드백 주기 = 고속 학습 주기이며, 이는 곧 품질과 가치를 높이는 지름길이다.
  • 시스템의 전 과정에서 변수를 통제하자: 앞서 실험주의에서 변수 통제에 대해 이야기했는데, 운영 환경에서도 적용된다. 예를 들어 장애가 발생하면 한꺼번에 여러 변경을 롤백하지 말고 순차적으로 하나씩 롤백하면서 원인을 특정해야 한다. 또는 성능 튜닝 시도도 한 가지 설정씩 바꿔가며 효과를 측정해야 한다. 복잡한 시스템일수록 동시에 많은 일이 일어나기 때문에, 문제 분석을 쉽게 하려면 가급적 변동 요인을 줄이는 운영 습관이 중요하다. 카나리아 배포(일부 트래픽에만 새 버전 적용)도 비슷한 맥락인데, 전체에 영향 주지 않고 작은 통제 실험으로 시작함으로써 위험을 줄인다. 결국 개발부터 운영까지 “무엇이 달라졌기에 결과가 달라졌는가” 를 추적할 수 있게 하는 것이 공학적 관리의 핵심이며, 이를 위한 원칙이 변수 통제다.
  • 지속적인 배포를 잊지 말자: 지속적 배포(Continuous Delivery/Deployment)는 현대 소프트웨어 엔지니어링의 총아(寵兒)라 할 만하다. 기술적으로 자동화된 배포와 테스트가 받쳐줘야 가능하지만, 문화적으로는 “항상 배포 가능 상태로 작업” 하는 습관이 중요하다. 기능이 완벽하지 않아도 뒤에 숨겨놓고(main 브랜치에 머지하지만 feature toggle로 비활성) 점진적으로 개발하거나, 작은 개선이라도 나오면 바로 배포하여 피드백을 받는다. 이렇게 하면 위험이 분산되고, 배포를 특별한 이벤트가 아니라 일상으로 만듦으로써 팀 스트레스도 준다. 실제로 구글이나 아마존 등의 엔지니어는 “배포”를 긴장되는 일이 아니라 하루에도 몇 번씩 하는 당연한 일로 받아들인다. 이는 시스템이 언제나 업데이트될 수 있음을 의미하고, 따라서 코드도 항상 정돈되고 테스트된 상태로 유지하려는 노력이 뒤따른다. 지속적 배포는 앞서 말한 대부분의 원칙(반복, 피드백, 점진적 변화 등)을 아우르는 실천이기도 하다.
  • 소프트웨어에서 고려해야 할 질문들: 마지막으로, 실제 사례들을 통해 떠오른 자문자답 리스트를 개발자들은 프로젝트마다 가져볼 필요가 있다. 예를 들면:
    • “우리 시스템의 가장 복잡한 부분은 어디이고, 이를 어떻게 관리하고 있는가?” – (복잡성 방치 X, 모듈화/추상화 적용 O)
    • “문제가 생기면 어떻게 알 수 있으며, 얼마나 빨리 대응할 수 있는가?” – (모니터링/로그, 알람 체계, On-call 절차 등)
    • “팀원 누구라도 새로운 코드를 안전하게 배포할 수 있는가?” – (자동 테스트/배포, 코드리뷰 등으로 배포 안정성 확보)
    • “기술 부채를 관리하고 있는가?” – (정기적 리팩토링, 개선 항목 백로그 관리)
    • “우리 시스템은 확장/변경에 얼마나 열려 있는가?” – (새 기능 추가 시 얼마나 영향 범위가 좁은지, 아키텍처 여유는 있는지)
  • 정리: 실제 사례들을 통해 배운 것은, 원칙은 현장에서 구체적 실천으로 이어져야 효과가 있다는 점이다. 자동화된 테스트 한 줄, 모니터링 대시보드 하나 같은 작은 실천들이 모여 공학적 품질 향상을 만들어낸다. 또한 문제가 드러났을 때 공학 원칙으로 돌아가 해법을 찾는 것이 중요하다. 모던 소프트웨어 엔지니어는 “문제가 생기면 근본 원인을 찾아 프로세스를 개선”하는 자세로 임해야 하며, 이는 곧 지속적 개선(Continuous Improvement) 문화와도 상통한다. 결국 사례의 교훈은: 원칙들을 머리로만 아는 것을 넘어, 팀과 시스템에 체화시킬 때 진정한 가치가 발현된다는 것이다.

15장. 모던 소프트웨어 엔지니어가 되려면

  • 팀이나 조직도 복잡성의 관리 대상임을 잊지 말자: 소프트웨어 공학 원칙은 코드에만 국한되지 않는다. 잘게 나뉜 마이크로서비스 아키텍처가 효과적이려면 그에 맞게 조직 구조도 소규모 자율팀들로 나뉘어야 한다. 큰 팀 하나에 모든 것이 얽혀 있으면 인적 결합도가 높아 협업이 어려워진다. 따라서 Conway의 법칙을 의식하여, 조직을 설계할 때 시스템 모듈 경계와 팀 책임을 정렬시키는 것이 좋다. 또한 지식의 버스 팩터(bus factor) 를 높이기 위해 문서화, 페어프로그래밍, 코드리뷰 등으로 개인에게 과도하게 의존하지 않도록 한다. 복잡한 프로젝트일수록 프로젝트 관리, 커뮤니케이션 구조 등 소프트웨어 개발을 둘러싼 사람과 프로세스의 영역도 엔지니어링 마인드로 최적화해야 한다. 결국 조직 전체를 하나의 시스템으로 보고 지속 개선하는 자세가 필요하다.
  • 디지털적으로 파괴적인 조직을 추구하자: “Digital disruptive”란 혁신적으로 기존 방식을 깨고 나아간다는 의미다. 모던 소프트웨어 엔지니어는 자기 조직(혹은 자기 자신)이 구태의연한 비효율에 안주하지 않고 새로운 기술과 방법을 과감히 시도할 수 있어야 한다. 예를 들어 수작업 배포를 자동화하거나, 온프레미스 시스템을 클라우드로 옮기는 변화가 필요할 때 이를 주도하는 자세다. 때론 기존의 성공 방정식도 시대에 뒤떨어질 수 있으므로, 성공을 방해하는 요소라면 과감히 혁신하는 용기가 필요하다. 엔지니어 개인도 마찬가지로, 익숙한 기술만 고집하면 성장 정체에 빠진다. 학습의 전문가로서 새로운 언어, 새로운 아키텍처를 꾸준히 접하고 배우면서 자기 혁신을 해야 한다. 이러한 파괴적 혁신 마인드가 결과적으로 조직과 개인을 한 단계 도약시킨다.
  • 결과 vs 메커니즘, 무엇이 더 중요할까: 흔히 성과(Outcome)활동(Activity) 를 구분한다. 소프트웨어 엔지니어링에서도 마찬가지인데, 어떤 프로세스를 따르는 것 자체가 목적이 되어선 안 되고, 그를 통해 얻는 결과가 중요하다. 예컨대 “우리 애자일 스프린트 철저히 지켰어”가 중요한 게 아니라, 정말로 고객에게 가치가 빨리 전달되고 있는지가 핵심이다. 테스트 커버리지 100%여도 버그투성이면 의미가 없다. 따라서 메커니즘(과정)은 어디까지나 결과를 좋게 내기 위한 수단으로서 유연하게 활용해야 한다. 엔지니어는 성공 지표(예: 시스템 가동시간, 사용자 수, 성능 수치 등) 를 염두에 두고 일해야 하며, 상황이 바뀌면 프로세스도 조정할 수 있어야 한다. 결과 중심으로 사고하면 불필요한 형식주의에 빠지지 않게 되고, 진짜 가치를 창출하는데 집중할 수 있다.
  • 소프트웨어 공학 원칙은 머신러닝 시스템에도 유효하다: 최근 많은 개발이 머신러닝(ML)을 포함하고 있다. ML에서는 데이터 수집, 모델 학습 등 특유의 파이프라인이 있지만, 결국 그것도 소프트웨어 시스템이다. 따라서 지금까지 다룬 원칙들이 똑같이 적용된다. 예를 들어 재현 가능한 실험(실험주의) – 머신러닝 모델도 실험 설정(하이퍼파라미터)을 관리하고 결과를 측정해야 한다. 모듈성 – 데이터 전처리, 모델 학습, 예측 서빙을 모듈화하면 파이프라인 변경이 수월해진다. 피드백 루프 – 모델의 예측 오류율 등을 모니터링하고 주기적으로 재학습(Continuous Training)하는 피드백이 필요하다. 이처럼 ML도 하나의 소프트웨어니까, 데이터와 모델이라는 새로운 개념을 다룰 뿐 공학적 관리가 본질이다. 실제로 ML 시스템 구축 가이드들을 보면 버전 관리, 테스트(데이터 검증), 모니터링 등 일반 소프트웨어 엔지니어링과 겹치는 부분이 많다. 결론적으로, AI 시대에도 기본기(공학 원리)가 중요하다는 것을 명심해야 한다.
  • 모던 소프트웨어 엔지니어링의 핵심 아이디어: 끝으로, 현대 소프트웨어 개발을 관통하는 아이디어들을 간단히 정리하면 다음과 같다.
    • 작게 나누고 빨리 돌려라 – 반복, 피드백, 점진적 개선을 통해 리스크를 통제하고 빠르게 진화한다.
    • 근거 기반으로 결정하라 – 측정, 실험, 데이터에 기반해 객관적으로 판단한다.
    • 복잡성은 적극적으로 다뤄라 – 방치하지 말고 모듈화, 추상화, 자동화로 복잡성을 관리한다.
    • 품질은 내재된 것이다 – 품질을 나중에 붙이는게 아니라 (테스트/디버그), 처음부터 내재화시킨다. 예: 클린코드, TDD, CI/CD 등으로 개발 과정 속에 품질 확보.
    • 끊임없이 배우고 개선하라 – 기술도 방법론도 계속 변하므로, 학습과 적응이 궁극의 무기이다. 특히 실패로부터 배우는 조직 문화, 지속적 개선(Kaizen) 정신이 중요하다.
  • 정리: 모던 소프트웨어 엔지니어란, 과학자처럼 호기심을 갖고 실험하고, 엔지니어처럼 체계화하고, 장인처럼 열정을 갖고 품질을 추구하는 사람이다. 오늘날 우리가 논의한 원칙들(반복, 피드백, 원리들)은 결국 “더 나은 소프트웨어를 더 빠르게(Better Software Faster)” 만들기 위함이며, 이를 통해 사용자와 사회에 가치를 전달하는 것이 궁극적 목표다. 비전공자 1학년이라도 이 핵심 개념들을 이해한다면, 앞으로 어떤 역할이든 소프트웨어와 관련된 일을 할 때 큰 그림을 보고 올바른 판단을 하는 데 도움 될 것이다. 소프트웨어 엔지니어링은 계속 발전중인 분야이니만큼, 여러분도 앞으로 배움과 경험을 통해 이 원칙들을 자기 것으로 만들고 현장에서 응용하길 바란다. 이는 단지 개발의 효율을 높이는 것을 넘어, 더 창의적이고 신뢰성 있는 디지털 세상을 만드는 밑거름이 될 것이다.