AI 시대에 풍부한 도메인 모델 (Rich Domain Model)을 사용하는 이유는 무엇인가?
요약
소프트웨어 아키텍처 설계 시 본질적 복잡성과 우연적 복잡성을 구분하는 것의 중요성을 다룹니다. 특히 AI가 코드 구현 비용을 낮추는 시대에는 도메인 모델에 대한 의도적인 설계가 더욱 중요해짐을 강조합니다.
핵심 포인트
- 본질적 복잡성과 우연적 복잡성을 분리하여 설계해야 함
- 기술적 제약이 도메인 모델을 규정하게 되면 시스템이 부패함
- AI로 인해 구현 비용이 낮아지면서 도메인 모델링의 중요성이 증대됨
- 풍부한 도메인 모델은 단순한 미학적 선택이 아닌 필수적인 도구임
대부분의 소프트웨어 아키텍처 논쟁은 실제로 해결될 수 없습니다. 모든 시스템은 단 한 번 구축됩니다. 선택되지 않은 대안적 접근 방식은 동일한 조건, 동일한 팀, 동일한 시장 상황 속에서 그 시스템과 나란히 구축된 적이 결코 없습니다. 따라서 시스템이 잘 작동하면 "그 방식이 옳았다"라고 조용히 격상되고, 시스템이 부패하면 그 부패의 원인을 도메인이 본질적으로 복잡하거나, 요구사항이 너무 많이 변했거나, 이전 개발자들이 부주의했기 때문이라고 돌립니다. 아키텍처 자체가 중요한 변수였다고 결론 내리는 사람은 거의 없는데, 왜냐하면 비교할 대조군(control group)이 없기 때문입니다.
이것이 바로 반증 불가능성(unfalsifiability) 문제이며, 아키텍처 논의가 생산적이지 못한 경향이 있는 이유입니다. 모든 사람은 팀의 기술 수준, 도메인의 난이도, 그리고 통제되지 않은 변수인 순전한 운이 작용하는 상황 속에서, 단 하나의 사례(n of one) 또는 몇 개의 고립된 사례로부터 일반화를 시도합니다. 유능한 두 엔지니어가 각각 10년의 경력을 가지고 자신의 결론에 완전한 확신을 가질 수 있지만, 서로에게 전수할 수 있는 것은 아무것도 배우지 못할 수도 있습니다. 왜냐하면 두 사람 모두 자신의 신념이 대안과 비교되어 검증되는 것을 본 적이 없기 때문입니다.
통제된 실험을 수행할 수 없다면, 우리는 대체물이 필요합니다. Fred Brooks는 수십 년 전, 본질적 복잡성(essential complexity) — 문제 자체가 가진 실제 어려움 — 과 우연적 복잡성(accidental complexity) — 도구, 프로세스, 표현 방식을 통해 우리 스스로가 도입하는 어려움 — 을 분리함으로써 그 대체물의 대부분을 우리에게 제시했습니다. Brooks의 요점은 소프트웨어 개발에서 발생하는 많은 고통이, 처음에는 그리 어렵지 않았던 문제 위에 스스로 덧씌운 자가 초래된 것(self-inflicted)이라는 점이었습니다.
Brooks가 우리에게 주지 않은 것은 운영 가능한 테스트(operational test)입니다. 즉, 실제 설계 결정 과정에서 자신이 마주한 복잡성이 어떤 종류인지 판별하기 위해 던질 수 있는 질문 말입니다. 이 글이 제공하고자 하는 테스트가 바로 그것입니다. 이 결정이 도메인(domain)에 대한 진정하고 현재적인 이해에 의해 강제된 것인가, 아니면 그러한 이해가 생기기 전에 존재했던 제약 사항에 의해 강제된 것인가? 본질적 복잡성 (Essential complexity)은 항상 주도적인 역할을 해야 합니다. 우연적 복잡성 (Accidental complexity)은 항상 그 뒤를 따르며 본질적 복잡성을 보조해야 합니다. 이 순서가 뒤바뀌는 순간 — 즉, 기술적 선택, 배포 토폴로지 (deployment topology), 또는 프로세스 게이트 (process gate)가 도메인이 어떤 모습이어야 하는지를 규정하기 시작하는 순간 — 당신은 우연적 복잡성이 주도권을 쥐게 된 것이며, 시스템은 결국 그 대가를 치르게 할 것입니다.
이 문제는 이전의 그 어느 때보다 지금 더 시급합니다. 그 이유는 이 글의 마지막 부분에서 다시 다루겠지만, AI가 구현(implementation) — 즉, 실제 코드를 작성하는 것 — 을 거의 무료로 만들었기 때문입니다. 구현이 무료가 된다는 것은, 개발자들이 자신들이 무엇을 하고 있는지 이름을 붙일 수 있었든 없었든 간에, 거의 우연히라도 올바른 모델을 향하도록 유도했던 바로 그 마찰(friction)을 제거해 버린다는 것을 의미합니다. 그 마찰이 사라진 후 남는 것은, 오직 누군가가 여전히 의도적으로 그 질문을 던지고 있는가 하는 문제뿐입니다.
도구, 그리고 그 도구가 작동하기 위해 필요한 것
풍부한 도메인 모델 (Rich Domain Model)은 스타일의 선호도나 클래스 대 함수(classes versus functions)에 대한 미학적 선택이 아니라, 세 가지 특정한 작업을 위해 만들어진 도구라고 저는 주장합니다. 첫째, 그것은 도메인이 실제로 무엇인지 배우는 방법입니다. 개념에 깔끔한 형태를 부여하려고 시도하는 행위 자체가, 당신이 애초에 그 개념을 제대로 이해했는지를 드러내기 때문입니다. 둘째, 그것은 "무엇을 만들 것인가"가 취향이나 기억의 문제가 되지 않을 만큼 도메인을 정밀하게 정의하는 방법입니다. 셋째, 그것은 계속해서 작동해야 하는 형태로 도메인을 문서화하는 방법입니다. 아무도 알아차리지 못한 채 수년간 조용히 시대에 뒤떨어질 수 있는 다이어그램이나 위키(wiki) 페이지와 달리, 잘못된 도메인 모델은 그 오류를 드러내는 경향이 있습니다. 그것은 실체화된 필수 복잡성 (essential complexity) — 즉, 실제로 가리킬 수 있는 무언가 — 이며, 믿음에 의존해야 하는 것이 아니라 잘못되었을 때 이를 알려주는 테스트 가능한 (testable) 무언가입니다.
이 글의 나머지 부분은 바로 이 주장을 옹호하는 데 할애될 것입니다. 다만 여기에는 조건이 하나 붙습니다. 도구는 특정한 상황에서만 제 역할을 하기 때문입니다. 그리고 소프트웨어 산업의 익숙한 습관들 — 비대한 서비스 레이어 (fat service layers), 너무 이른 경계 컨텍스트 (bounded contexts) 또는 마이크로서비스 (microservices) 분리, 새로운 사용자 스토리 (user story)를 증거가 아닌 작업 지시서로 취급하는 것 — 은 대부분 그 조건을 끊임없이 위반하며, 대개 아무도 자신이 그렇게 하고 있다는 사실을 알아차리지 못한 채 이루어집니다.
이 조건은 세 가지 부분으로 구성됩니다. 첫째, 본질적 복잡성 (Essential complexity)은 온전하게 유지되어야 합니다. 즉, 한 곳에 머물러 있어야 하며, 한 번에 한 명의 정신이 도달할 수 있어야 합니다. 단일 코드베이스 내의 수백 개 서비스로 분산되어서도 안 되며, 팀이나 배포 사이에 그어진 경계 너머로 흩어져서도 안 됩니다. 둘째, 모델은 당신의 이해가 불완전하다는 것이 드러날 때 피드백을 주어야 합니다. 즉, 갈 곳 없는 동작이 나타나거나, 이전 형태를 가정했던 모든 지점에서 컴파일 에러가 발생하거나, 가정이 틀린 바로 그 순간 제약 조건 (Constraint)이 위반되는 식이어야 합니다. 셋째, 새로운 통찰을 얻었을 때 이를 모델에 반영하는 비용이 저렴하게 유지되어야 합니다. 즉, 이전의 이해를 인코딩했던 수많은 곳을 찾아다니는 대신, 단 한 번, 한 곳에서만 비용을 지불하면 되어야 합니다. 이 세 가지 중 하나라도 잃게 되면 그 도구는 더 이상 도구로서의 기능을 상실합니다. 코드는 여전히 실행될 수도 있고, 어딘가 슬라이드 위에 모델이 존재할 수도 있습니다. 하지만 이 작업을 수행하기로 되어 있던 실체는 더 이상 작동하지 않으며, 오직 그 겉모습만이 남아 있을 뿐입니다.
첫 번째 조건부터 살펴보겠습니다. 왜냐하면 이 조건은 대개 선의를 가지고 가장 조용히 위반되기 때문입니다. 본질적 복잡성이 하나의 핵심 모델 내에 존재할 때, 당신은 모델을 보고 비즈니스를 볼 수 있습니다. 하지만 그렇지 않을 때 — 즉, 비대한 서비스들에 퍼져 있거나, 저장소(Repository)에 파묻혀 있거나, 분산된 컴포넌트들에 흩어져 있거나, 당시에는 당연해 보였던 부서 간의 경계에 따라 나뉘어 있을 때 — 가독성 (Legibility)이 가장 먼저 희생되며, 이와 함께 이 글의 서두에서 던진 질문조차 던질 수 없게 됩니다. 즉, 애플리케이션이 도메인을 서빙하고 있는가, 아니면 도메인이 자신을 보조해야 했던 우연적 복잡성 (Accidental complexity)을 조용히 서빙하기 시작했는가 하는 질문 말입니다. 본질적 복잡성이 더 이상 하나의 가시적이고 일관된 거처를 갖지 못하게 되면, 더 이상 살펴볼 단일 지점이 존재하지 않기 때문에 그 누구도 이러한 관찰을 수행할 수 없습니다. 이어지는 내용은 사실상 이 세 가지 조건을 하나씩 잃었을 때 어떤 대가를 치르게 되는지, 그리고 그것을 잃어가는 과정 중에 그것이 실수라는 사실이 얼마나 드물게 알려지는지에 대한 확장된 입증입니다.
모델은 한 번 그리는 것이 아니라, 이를 통해 학습해 나가는 것입니다
의도적으로 단순한 예시를 들어보겠습니다. 물건을 빌려주는 도서관입니다. 주제가 중요한 것이 아니라 논리적 추론이 핵심이 되도록 일부러 오래되고 익숙한 소재를 선택했습니다.
도메인 전문가(Domain Expert)와의 첫 대화는 예상 가능한 대로 흘러갑니다. 도서관은 책을 빌려주고 싶어 합니다. 그들은 각 책이 어디에 있는지 — 선반 위에 있는지, 아니면 누구에게 언제부터 언제까지 대출 중인지 — 알고 싶어 합니다.
가장 저항이 적은 경로는 대출 날짜를 Book 엔티티에 직접 넣는 것입니다. 책은 자신이 어디에 있는지 압니다. 만약 대출 중이라면, 누구에게 언제까지 대출되었는지도 압니다. 대부분의 개발자라면 별다른 의심 없이 자연스럽게 받아들일 만큼 충분히 자연스러워 보입니다.
하지만 그럼에도 불구하고 잠시 멈춰 생각해보십시오. 왜냐하면 이 결정이 이후의 모든 과정(downstream)을 조용히 제약하기 때문입니다. 순수한 도메인 질문을 던져보십시오. 책이 언제, 누구에게 빌려졌는지 아는 것이 책이라는 존재의 일부인가요? 책은 제목, 저자, 물리적 객체입니다. 대출(Loan)은 이벤트(Event)입니다. 즉, 특정 시점에 그 책을 두고 도서관과 개인 사이에 맺어진 계약입니다. 이것들은 편의를 위해 하나로 꿰매진 서로 다른 것들이며, 누군가의 고용 기록을 여권 안에 저장하는 것과 같은 범주의 오류입니다.
개념적인 문제 뒤에는 구조적인 문제도 숨어 있습니다. 책은 서로 다른 시점에 서로 다른 사람들에 의해 여러 번 대출됩니다. Book에 있는 단일 대출 필드 세트로는 매번 새로운 대출이 발생할 때마다 기존 정보를 덮어쓰지 않고는 그 이력을 표현할 수 없습니다. 이것은 단순한 스타일의 문제가 아닙니다. 이 모델은 비즈니스가 결국 던지게 될 질문들에 답할 수 없는 구조적 불능 상태에 빠져 있습니다.
따라서 Loan 엔티티가 도입됩니다. 이 엔티티는 책과 대출자(Borrower)를 가리키며, 시작일, 종료일, 반납일과 같은 자체 데이터를 가집니다. Book은 다시 단지 책으로서의 역할로 돌아갑니다. 각 개념은 자신이 실제로 무엇인지에 대해 책임을 집니다.
아무도 이러한 정교화(refinement)를 요청하지 않았습니다. 사용자 스토리(user story)는 "책을 대여하고 싶다"였지, "대여(loan)라는 개념을 책(book)이라는 개념과 분리해 주세요"가 아니었습니다. 하지만 그 스토리는 결코 명세(specification)가 아니었습니다. 그것은 도메인(domain)에 관한 하나의 정보였으며, 우리의 역할은 그 정보가 무엇을 드러내는지 질문하는 것이었지, 그것을 Book 클래스에 직접 타이핑해 넣고 티켓을 닫아버리는 것이 아니었습니다.
Loan이 그 자체로 존재하는 독립적인 존재가 되자, 아무도 요청하지 않았던 것들이 보이기 시작했습니다. 올해 책이 몇 번이나 대여되었는지, 두 번째 복사본을 마련할 만큼 빈번하게 연속 대여가 발생하는지, 현재 어떤 대여 건이 연체되었는지, 어떤 대여자가 가장 많은 품목을 빌려갔는지와 같은 것들 말입니다. 이 중 그 어떤 것도 모델을 다시 수정할 필요를 요구하지 않았습니다. 처음부터 책임을 올바른 곳에 두었기에 자연스럽게 도출된 결과였습니다. 올바른 추상화(abstraction)는 명시된 문제만을 해결하는 것이 아니라, 아직 아무도 묻지 않은 다음 열 가지 질문에 저항하지 않도록 해줍니다.
두 번째 수정
새로운 요구사항이 도착합니다: 도서관에서 DVD도 대여하고 싶어 합니다.
여기서 가장 저항이 적은 경로는 예측하기 매우 쉽습니다. 바로 DVD 엔티티(entity)를 추가하는 것입니다. 제목, 감독, 상영 시간. 그리고 티켓을 닫습니다. 이것이 바로 이 글 전체가 다루고자 하는 실패를 축소해 놓은 모습입니다. "DVD도 대여하고 싶다"라는 요청은, 자신에 대해 무언가를 방금 드러낸 도메인에 관한 새로운 정보로 취급되는 대신, DVD 클래스를 추가하라는 지시로 취급되었습니다.
실제 질문은 "어떻게 DVD를 추가할 것인가"가 아닙니다. 질문은 이것입니다: "애초에 Book이 이 도메인에 적합한 개념이었는가?" 대여 시스템은 책에 페이지가 있는지 또는 DVD에 상영 시간이 있는지에는 관심이 없습니다. 시스템은 두 가지 모두 대여되고, 추적되고, 반납될 수 있는 대상이라는 점에 관심이 있습니다. Book과 DVD를 형제 관계로 모델링하면, 다음 스토리에서는 잡지가 등장하고, 그다음에는 도구가 등장하며, 그다음에는 패턴을 완전히 깨뜨리는 무언가가 등장하게 됩니다. 결국 네 개의 병렬적인 엔티티 유형이 서비스 로직을 중복시키고 모든 보고서를 복잡하게 만들게 됩니다.
결과적으로 도메인이 실제로 필요로 했던 개념은 Book이 아니었습니다. 그것은 LendableItem — 즉, 물리적으로 무엇인지와 상관없이 대여할 수 있는 무언가였습니다. Book은 LendableItem이 됩니다. 어떤 종류의 아이템인지는 클래스가 아닌 데이터(ItemType)가 되며, 특정 유형에 특화된 속성들(책의 경우 ISBN과 저자, DVD의 경우 상영 시간과 감독)은 해당 ItemType에 의해 형성된 작은 타입 컬렉션(typed collection) 안에 존재하게 됩니다. 새로운 대여 가능 항목은 별도의 릴리스 없이 설정(configuration)을 통해 정의될 수 있습니다.
이것은 추상화를 위한 추상화가 아닙니다. 책만 존재했을 때는 Book으로 시작하는 것이 옳은 결정이었습니다. 알려진 유일한 인스턴스를 바탕으로 개념의 이름을 짓는 것은 합리적인 일이지, 순진한 것이 아닙니다. 핵심은 두 번째 인스턴스가 등장했을 때 그것이 '증거(evidence)'가 되었다는 점이며, 모델은 그 증거를 원래의 추측 옆에 덧붙여진 특수 사례로 흡수하는 대신 그에 대응해야 할 의무가 있었다는 것입니다.
여기서 깊이 생각해 볼 만한 부분이 있습니다: 두 가지 수정 모두에서, 틀렸을 때의 비용은 정확히 한 번, 정확히 한 곳에서 지불되었으며, 컴파일러는 변경이 필요한 다른 모든 곳을 당신에게 알려주었습니다. Book을 LendableItem으로 바꾸면 Book을 가정했던 모든 호출 지점(call site)에서 컴파일 에러의 파도가 발생합니다. 이 에러들은 하나하나가 추적해야 할 대상이 아니라, 이미 완료된 체크리스트와 같습니다. 14개의 서비스 중 어떤 것이 이전의 가정을 건드리고 있었는지 기억해내야 하는 단계는 없습니다. 타입 시스템(type system)이 이미 알고 있기 때문입니다.
그 대안을 상상해 보십시오. 수년에 걸쳐 축적되어, 그중 몇몇은 이미 퇴사한 사람들이 작성한 200개의 서비스 메서드(service methods)가 있는 코드베이스를 말입니다. 어떤 서비스들은 Book의 플래그(flag)를 통해 책의 대출 상태를 읽어옵니다. 어떤 것들은 "이 물건이 현재 대출 중인가"라는 체크 로직을 인라인(inline)으로 중복 구현합니다. 어떤 것들은 올바르게 처리하는 공유된 BookService를 호출하지만, 어떤 것들은 제대로 처리하지 못하는 오래된 서비스를 호출합니다. DVD 관련 요구사항이 내려왔을 때, 책에 대한 가정을 인코딩(encoding)한 모든 곳을 찾아내는 작업은 이제 기억력과 grep에 의존하여 수행되는 하나의 연구 프로젝트가 되어버립니다. 모든 곳을 다 찾았는지 확인해 줄 도구도 없이 말입니다. 만약 두 명의 서로 다른 개발자가 그 서비스들 중 두 개를 작성했다면, 그들은 '책이란 무엇인가'에 대해 미묘하게 다른 두 가지 멘탈 모델(mental models)을 인코딩했을 수도 있습니다. 아키텍처(architecture) 상에서 그 모델들이 충돌하도록 강제하는 것이 아무것도 없었기 때문에, 두 모델 중 어느 것도 서로 일치하도록 조정될 필요가 없었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기