장기 지속 가능한 소프트웨어 시스템 구축 방법
Source: Dev.to
대부분의 소프트웨어 시스템은 잘못된 기술 선택 때문에 실패하지 않는다.
그들은 학습을 멈추기 때문에 실패한다. 프레임워크는 시간이 지나면서 노후하고, 아키텍처는 유행을 타며, 팀은 변한다. 치명적인 문제는 시스템이 비즈니스에 대한 내부 이해가 현실과 서서히 괴리될 때 발생한다—작은 변경조차 위험하고, 비용이 많이 들며, 예측 불가능해진다.
시스템이 실패하는 이유
도메인 모델이 비즈니스를 더 이상 반영하지 않을 때, 재작성, 새로운 아키텍처, 최신 프레임워크가 제안되지만 근본적인 문제는 기술과 무관하게 남는다. 오래 살아남는 시스템은 변하지 않는 시스템이 아니라, 스스로를 불안정하게 만들지 않으면서 지속적으로 점진적으로 진화할 수 있는 시스템이다.
오래 살아남는 시스템의 특징
- 새로운 기능은 보통 로컬에서 추가할 수 있다.
- 기존 동작은 기능이 도입될 때 거의 깨지지 않는다.
- 개발자는 도메인 용어로 시스템을 사고하며, 기술 용어가 아니다.
- 수년간 진화한 뒤에도 대규모 재작성은 필요하지 않다.
- 코드베이스는 비즈니스가 실제로 어떻게 운영되는지를 반영하고 비즈니스 언어로 “말한다”.
이러한 특성은 개념 모델이 비즈니스와 같은 속도로 진화할 때만 나타난다. 진화가 멈추면 개념적 긴장이 쌓이고, 책임이 깔끔하게 정렬되지 않으며, 점진적인 변화가 불가능해진다.
사용자 스토리를 도메인 검증으로 활용하기
사용자 스토리는 시스템이 지원해야 할 기능 조각을 평면적으로 묘사한 것으로, 무엇을 원하는지를 포착하고 어떻게 내부 책임을 구조화해야 하는지는 다루지 않는다. 스토리를 단순 작업 지시서로만 취급하면 도메인 모델이 약화된다.
구현 전에 물어볼 질문
- 이 스토리는 현재 도메인 모델에 어떻게 맞는가?
- 원래 정의된 기존 객체 중 어떤 것이 참여해야 하는가?
- 이 스토리가 누락된 개념이나 책임을 드러내는가?
- 기존 책임 경계는 여전히 유효한가?
- 모델이 스토리를 자연스럽게 흡수한다면 구현은 직관적이다.
- 흡수되지 못한다면 마찰은 모델링 신호이며, 구현 문제는 아니다. 이 단계를 건너뛰면 책임 결정이 서비스, 워크플로, 오케스트레이션 코드로 옮겨져 모델을 은밀히 약화시킨다. 시간이 지나면 도메인 모델은 행동을 지배하지 못하고, 수동적인 데이터 스키마로 전락한다.
“모델 우선” 원칙
“모델 우선”은 모든 것을 미리 설계하거나 현실을 추상화한다는 뜻이 아니다. 의미는 다음과 같다:
- 도메인 모델이 가장 중요한 산출물이다.
- 구현은 모델을 지원하기 위해 존재한다.
- 책임 경계가 코드 구조를 이끈다.
- 기술적 편의가 개념적 정확성을 압도해서는 안 된다.
프레임워크, 라이브러리, 아키텍처는 시간이 지나면서 교체 가능해야 하며, 비즈니스를 정확히 반영하는 도메인 모델은 교체되지 않아야 한다.
빈약한 도메인 모델의 위험
빈약한 도메인 모델은 흔히 “로직이 없는 엔티티”라고 설명되지만, 이 정의는 불완전하다. 도메인 모델은 책임의 구조이며, 상태는 책임의 결과일 뿐 정의가 아니다. 책임이 모델에서 다른 코드 영역으로 이동할 때 빈약한 모델이 나타난다.
다음과 같은 경우에도 객체에 로직이 존재하면서 빈약한 모델이 될 수 있다:
- 관련 책임을 가진 객체 외부에서 결정이 내려질 때, 상태와 무관하게.
- 개념적 경계를 정의하는 객체 외부에서 로직이 구현될 때, 해당 객체가 모델에 존재하더라도.
- 불변 조건이 절차적으로(워크플로 또는 서비스) 강제될 때, 구조적으로(모델을 통해) 강제되지 않을 때.
- 도메인 객체가 단순히 조정용 도구가 되고, 의미 있는 행동은 외부 오케스트레이터에 밀려날 때.
예시: 도메인 인터랙션 객체
도메인 인터랙션 객체는 다음을 정의할 수 있다:
- 도메인에 언제, 어떻게 진입하는가.
- 어떤 일관성 또는 트랜잭션 범위가 적용되는가.
- 인터랙션 전반에 걸쳐 어떤 불변 조건이 유지되어야 하는가.
이 객체는 영속 상태가 거의 없거나 없을 수도 있지만, 핵심 책임을 소유한다. 트랜잭션 관리나 일관성 경계가 그 인터랙션 외부(예: 서비스나 인프라 수준)에서 도입되면, 객체는 존재하지만 의미를 잃게 된다.
프레임워크 영향 – Spring Boot 예시
프레임워크는 특히 엔터프라이즈 환경에서 시스템 형태에 강력한 중력처럼 작용한다. Spring Boot를 선택하면 팀은 다음을 감수한다:
- 연간 프레임워크 업데이트 주기.
- 지속적인 라이선스 또는 지원 비용.
이 강제적인 변동은 비즈니스 도메인이 변했는지와 무관하게 엔지니어링 주의를 소모한다. 안정적인 도메인이라 할지라도 팀은 지속적으로:
- 프레임워크 진화에 적응한다.
- 폐기(deprecation)를 처리한다.
- 인프라 관련 사항을 재검증한다.
Spring의 기본 설정과 생태계는 절차적이고 서비스 중심적인 설계를 강하게 유도한다:
- 무상태 서비스.
- 의존성 주입 기반 오케스트레이션.
- 트랜잭션 워크플로.
- 수동적인 도메인 객체.
그 결과 책임이 파편화되고 모델이 빈약해진다.
트랜잭션 관리와 도메인 관점
트랜잭션은 종종 순수 기술적 관점—서비스 레벨에서 설정하거나 어노테이션을 붙이는 것—으로 다뤄진다. 실제로 트랜잭션은 비즈니스 수준의 일관성 경계를 정의하며 다음과 같은 질문에 답한다:
- 의미 있는 작업 단위는 무엇인가?
- 어떤 변경이 함께 성공하거나 실패해야 하는가?
- 언제 중간 상태가 관찰될 수 있는가?
- “전부 혹은 전무”는 이 도메인에서 실제로 무엇을 의미하는가?
트랜잭션 경계가 도메인 인터랙션을 정의하는 객체 외부에 도입되면, 모델은 핵심 책임을 잃는다. 시스템은 기술적으로는 올바를 수 있지만 개념적으로는 불안정해진다.
지속적인 모델링이 전달 속도를 높인다
지속적인 모델링이 전달을 늦춘다는 믿음이 있다. 실제로는 그 반대가 종종 사실이다. 각 스토리를 모델링 기회로 삼아 진화적 도메인 개발을 의도적으로 실천하는 팀은 기능을 훨씬 빠르게 제공한다.
도메인 모델을 비즈니스와 일치시킴으로써 팀은 우발적인 복잡성을 줄이고, 대규모 재작성을 피하며, 안전하고 점진적으로 변할 수 있는 시스템을 유지한다.