스파게티 코드 방지: 기능별 책임 명확히 나누기
회원, 결제, 상품 등 기능(도메인)별로 코드의 책임을 나누고, 서로의 코드를 함부로 침범하지 않도록 경계를 설정하는 법을 배웁니다.
이전 에피소드에서 우리는 모듈러 모놀리스의 핵심이 서버가 아닌 '코드의 논리적 분리'에 있다는 것을 알았습니다. 그렇다면 코드를 대체 어떤 기준으로, 어떻게 나누어야 할까요? 아무렇게나 폴더만 나눈다고 해서 좋은 모듈러 모놀리스가 되는 것은 아닙니다. 이번 시간에는 도메인(Domain)을 기준으로 책임을 나누고 단단한 경계를 치는 방법에 대해 알아보겠습니다.
왜 폴더만 나누면 실패할까?
전통적으로 많은 프레임워크들은 기술적인 역할에 따라 코드를 분류하도록 가이드했습니다. MVC 패턴이 대표적입니다. `controllers`, `services`, `models` 같은 폴더를 만들고 그 안에 모든 기능을 밀어넣는 방식입니다.
이러한 기술 주도 계층형 아키텍처(Technical Layered Architecture)는 프로젝트가 커지면 큰 문제를 일으킵니다. 만약 '결제' 기능에 수정이 필요하다면, 개발자는 `controllers` 폴더에서 결제 컨트롤러를 찾고, `services` 폴더에서 결제 서비스를 찾고, `models` 폴더에서 결제 모델을 찾아 헤매야 합니다.
더 심각한 문제는 '결제 모델'을 '배송 서비스'나 '장바구니 컨트롤러'에서 아무런 제약 없이 가져다 쓸 수 있다는 점입니다. 결국 기술적으로는 분리되어 보일지라도, 비즈니스 로직은 거미줄처럼 얽힌 스파게티가 됩니다.
코드 구조화 방식
권장
- 비즈니스 기능(도메인) 단위로 폴더를 구성하세요.
- 관련된 컨트롤러, 서비스, 모델을 하나의 도메인 폴더에 모으세요.
- 다른 도메인의 내부 코드를 직접 참조하지 않도록 접근을 제한하세요.
주의
- MVC(Model-View-Controller) 같은 기술적 역할로만 최상위 폴더를 나누지 마세요.
- 모든 서비스에서 접근할 수 있는 거대한 전역 유틸리티 폴더를 남용하지 마세요.
- 다른 도메인의 데이터베이스 모델을 마음대로 임포트하여 수정하지 마세요.
도메인별로 책임 나누기
진정한 모듈러 모놀리스를 위해서는 코드를 기술이 아닌 비즈니스 기능(도메인, Domain)을 기준으로 나누어야 합니다.
예를 들어 이커머스 시스템이라면 `users(회원)`, `products(상품)`, `orders(주문)`, `payments(결제)` 등으로 최상위 모듈을 나눕니다. 그리고 각 도메인 폴더 안에는 그 기능이 동작하는 데 필요한 컨트롤러, 서비스, 데이터 모델이 모두 포함되어야 합니다.
이렇게 구성하면 하나의 기능(예: 결제 로직 수정)을 작업할 때 하나의 폴더(payments 모듈) 안에서만 작업하면 되므로 인지적 부담이 확연히 줄어듭니다.
바운디드 컨텍스트 (Bounded Context)
이러한 개념을 심화한 것이 도메인 주도 설계(DDD, Domain-Driven Design)에서 말하는 바운디드 컨텍스트(Bounded Context)입니다. 바운디드 컨텍스트는 특정한 비즈니스 모델이 유효하게 적용되는 명확한 경계를 의미합니다.
예를 들어 '상품(Product)'이라는 단어는 문맥에 따라 의미가 다릅니다.
- 상품 카탈로그 모듈: 보여주기 위한 정보 (이름, 이미지, 상세 설명)가 중요합니다.
- 재고 모듈: 창고 위치, 남은 수량, 입고 예정일이 중요합니다.
- 배송 모듈: 상품의 무게, 부피, 파손 위험 여부가 중요합니다.
과거에는 이 모든 정보를 담은 거대한 `Product` 클래스를 하나 만들어 모든 곳에서 돌려 썼습니다. 하지만 모듈러 모놀리스에서는 각 모듈(바운디드 컨텍스트)에 맞게 상품의 개념을 작게 분리하여 따로 정의합니다.
거대 모델과 바운디드 컨텍스트
거대한 Product 모델
분리됨 ➡️
카탈로그 모듈 (이름, 설명)
거대한 Product 모델
분리됨 ➡️
재고 모듈 (수량, 위치)
거대한 Product 모델
분리됨 ➡️
배송 모듈 (무게, 부피)
넘어오지 마! 단단한 경계 설정하기
모듈을 도메인별로 나누었다면, 다음은 모듈 간의 '규칙'을 강제할 차례입니다. 개발자들은 급하다는 핑계로 이웃 모듈의 코드를 몰래 가져다 쓰는(Import) 유혹에 빠지기 쉽습니다.
경계를 지키는 방법들
모듈 간의 독립성을 지키기 위해 다음과 같은 규칙들을 강제해야 합니다.
- 내부 구현 숨기기: 자바스크립트/타입스크립트 환경이라면 모듈 내부에서만 사용하는 클래스나 함수는 절대 `export` 하지 않습니다. 오직 외부와 소통하기 위해 약속된 퍼블릭 인터페이스(API) 파일 하나만 밖으로 노출(export)합니다.
- 린트(Lint) 도구 활용: ESLint나 Nx 같은 도구를 사용하여, 특정 모듈이 다른 모듈의 내부 코드를 `import` 하려 할 때 빌드 에러를 내도록 설정합니다.
- 직접 변경 금지: '주문' 모듈은 절대로 '결제' 모듈의 상태를 직접 변경해서는 안 됩니다. 반드시 결제 모듈이 제공하는 공식 함수(예: `processPayment()`)를 호출하여 결제 모듈 스스로 상태를 변경하도록 요청해야 합니다.
의도적인 불편함 만들기
좋은 아키텍처는 가끔 개발자를 귀찮게 만듭니다. 옆 모듈의 데이터를 살짝 고치면 5분 만에 끝날 일을, 인터페이스를 새로 만들고 안전하게 호출하도록 강제하기 때문입니다. 하지만 이런 '의도적인 불편함'이 누적될 때, 비로소 시스템은 스파게티 코드가 되는 것을 면하고 튼튼하게 성장할 수 있습니다. 경계를 허무는 타협은 결국 기술 부채로 돌아옵니다.
바이브코딩과 명확한 경계
AI 코딩 어시스턴트에게 코드를 작성해 달라고 요청할 때, 모듈의 경계가 명확할수록 AI는 더 훌륭한 코드를 만들어냅니다.
스파게티 시스템에서는 AI에게 결제 로직 수정을 지시했을 때, AI가 주문이나 배송 로직까지 실수로 엉망으로 만들 위험이 있습니다. 하지만 도메인별로 폴더가 격리되어 있고 엄격한 린트 룰이 적용된 모듈러 모놀리스 환경에서는, AI 역시 정해진 폴더 내에서만 제한적으로 작업하게 되므로 훨씬 안전하고 예측 가능한 결과를 얻을 수 있습니다.
개념 퀴즈
퀴즈 답을 맞춰야 학습 완료가 됩니다.모듈러 모놀리스에서 코드를 분리하는 가장 바람직한 기준은 무엇입니까?
3줄 요약
- 1코드를 나눌 때는 컨트롤러나 서비스 같은 기술적 계층이 아닌, 비즈니스 도메인을 기준으로 모듈을 구성해야 합니다.
- 2거대한 범용 데이터 모델을 하나 만드는 대신, 각 바운디드 컨텍스트의 목적에 맞는 작고 독립적인 모델을 설계하세요.
- 3모듈 간의 내부 코드 직접 참조를 엄격히 금지하고, 잘 정의된 인터페이스를 통해서만 상호작용하도록 강제해야 합니다.