모든 글

UI 모양을 넘어서는 도메인 모델 설계

트리 모양의 API가 트리 모양의 도메인을 요구하지는 않습니다. 클라이언트 계약은 프레젠테이션 경계에 두고 내부 개념을 조합해 응답합니다.

출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다. gpt-5.6-sol 모델을 사용해 생성·편집했습니다.

클라이언트가 책을 트리로 보여 달라고 요청합니다. 가장 빠른 백엔드 설계는 모든 것에 트리라는 이름을 붙이는 것입니다. TreeController, TreeService, TreeItem을 만들고 저장 구조까지 트리 모양으로 맞출 수 있습니다. 특히 초기에는 이 방식도 동작합니다. 문제는 현재 화면의 모양이 서비스 안의 중요한 객체를 전부 결정하기 시작할 때 생깁니다.

저는 ‘트리’를 클라이언트와의 계약으로 보고, 백엔드가 실제로 무엇을 모델링하는지는 따로 묻는 편을 선호합니다. 이 예제의 내부 개념은 선반입니다. 선반에는 아이템이 놓이고, 각 아이템은 자신의 깊이와 우선순위를 압니다. 프레젠테이션 계층은 이 선반과 아이템을 클라이언트가 기대하는 트리로 바꿉니다.

각 경계가 소유한 언어를 씁니다

API에서는 트리라는 용어를 일관되게 사용할 수 있습니다. findTree 엔드포인트는 인자를 받은 뒤 선반 서비스에서 선반을 조회하고 TreeResponse를 만듭니다. 아이템까지 포함한 응답이 필요하다면 선반과 선반 아이템을 각각 조회한 뒤 요구된 표현으로 조합할 수 있습니다.

처음 이 호출 흐름을 보면 어색할 수 있습니다. findTree라는 이름의 메서드가 갑자기 선반 서비스로 들어가기 때문입니다. 이 불일치는 변환 지점을 눈에 보이게 합니다. 컨트롤러 패키지는 클라이언트가 보는 트리의 언어를 쓰고, 도메인 영역은 선반과 아이템의 언어를 씁니다. 모든 곳에서 같은 명사를 쓰면 따라가기는 쉽지만, 그 명사를 어느 쪽이 소유하는지는 가려질 수 있습니다.

두 영역의 이름이 다른 것은 의도적인 선택입니다. 프레젠테이션 계층은 클라이언트가 사용하는 명세와 백엔드 내부 개념을 서로 변환하는 다리입니다. “책을 트리로 보여 달라”는 요구사항은 정보가 시스템 밖으로 나가는 모양을 설명합니다. 도메인 객체의 가장 적절한 이름이 Tree라거나 데이터베이스도 같은 구조여야 한다는 뜻은 아닙니다.

이렇게 나누면 양쪽이 서로 다른 이유로 바뀔 여지도 생깁니다. API는 계속 트리를 약속하지만 내부 비유는 선반에서 상자나 다른 컨테이너로 바뀔 수 있습니다. 내부 이름 자체보다 중요한 것은 외부 계약을 지키고, 그 변환을 경계에서 분명하게 드러내는 일입니다.

계약을 아는 곳에서 응답을 조합합니다

지금 단계에서는 한 엔드포인트가 선반과 선반 아이템을 함께 반환한다는 이유만으로 둘을 하나의 영구적인 도메인 객체로 만들 필요가 없습니다. 두 객체의 결합은 클라이언트 응답을 만들기 위해 필요하므로 프레젠테이션 계층에서 조합할 수 있습니다.

흐름은 단순합니다.

선반 서비스 -> 선반 목록 -----------+
                                      +-> 트리 응답
아이템 조회 -> 선반 아이템 목록 -----+

모든 조합을 컨트롤러나 응답 객체에 두라는 규칙은 아닙니다. 두 객체를 함께 다루는 행위가 반복되거나 그 결합이 비즈니스 안에서 독자적인 의미를 얻는다면, 둘을 아우르는 개념이 생길 수 있습니다. 그러나 현재처럼 단순한 모델에서 미리 그런 객체를 만들면 구현으로 확인하지 못한 지식을 안다고 가정하게 됩니다.

따라서 경계에서는 양쪽을 보호할 만큼 변환하되, 실제로 나타난 도메인 개념까지 프레젠테이션 조합으로 대신해서는 안 됩니다.

저장 모델이 화면을 그대로 베끼지 않게 합니다

같은 구분은 데이터 모델에도 이어집니다. 선반에는 이름과 설명이 있을 수 있습니다. 각 선반 아이템이 깊이와 우선순위를 가지면 트리 응답을 만들 때 아이템을 층별로 배치할 수 있습니다. 썸네일, 링크, 이름 같은 필드는 현재 명세의 영향을 받지만 트리 UI에서만 쓸 수 있는 값이라고 단정할 필요는 없습니다.

이 모델도 변할 수 있습니다. 지금은 각 아이템의 값으로 표현한 선반 층을 나중에는 명시적인 데이터로 만들 수도 있습니다. 첫 스키마가 최종 답이라는 뜻이 아닙니다. 첫 화면을 기계적으로 복사하기보다 백엔드 개념에서 데이터 설계를 시작한다는 점이 중요합니다.

선반은 현재 트리에서 드러나지 않는 다른 쓰임을 얻을 수도 있습니다. 지금은 책을 놓지만 이후에는 다른 아이템을 담거나 같은 내용을 다른 모양으로 보여줄 수 있습니다. 그렇다고 가능한 미래를 오늘 모두 설계해야 한다는 뜻은 아닙니다. 재사용할 수 있는 내부 개념과 그 개념의 첫 표현을 구분해 둘 이유가 있다는 뜻입니다.

이 예제가 모든 영속 엔티티에 별도의 도메인 클래스를 요구하는 것도 아닙니다. JPA 엔티티가 애플리케이션에서 충분한 개념 객체라면 그대로 사용하는 선택도 가능합니다. 행위와 응집이 다른 객체를 요구하기 시작할 때 분리하는 선택 역시 가능합니다. 계층마다 객체가 하나씩 있어야 한다는 규칙이 아니라 모델에 필요한 것이 무엇인지 보고 결정해야 합니다.

변화가 분명해질 때만 분리 비용을 냅니다

API에서는 ‘트리’, 서비스 안에서는 ‘선반’이라는 이름을 오가면 헷갈릴 수 있습니다. 개발자는 둘 사이의 대응 관계를 익혀야 하고, 모든 클래스가 같은 명사를 쓸 때보다 코드를 따라가기가 직관적이지 않습니다. 이것은 실제 비용입니다.

처음에는 모든 곳에 트리라는 이름을 쓰고, 모델이 분명해진 다음 리팩터링해도 괜찮습니다. 더 의도적인 분리는 표현 요구사항이 내부의 고정된 전제가 되는 것을 막고 싶을 때 가치가 있습니다. 그러면 “클라이언트와 무엇을 약속했는가”와 “백엔드를 가장 잘 설명하는 개념은 무엇인가”를 따로 물을 자리가 생깁니다.

이 예제의 답은 크지 않습니다. 트리는 API 계약으로 유지하고, 내부에서는 선반과 선반 아이템을 모델링하며, 응답을 만들 때 둘을 조합합니다. 이후의 행위가 더 강한 개념을 드러내면 그때 모델을 바꾸면 됩니다. 목표는 최대한 많이 분리하는 데 있지 않습니다. UI와 도메인이 서로를 조용히 규정하지 않은 채 각자의 이유로 변할 수 있을 만큼만 독립시키는 것이 목표입니다.