본문으로 건너뛰기
2026. 6. 22
© WONKOOK LEE

도메인 모델은 왜 DB를 몰라야 할까요?

육각형 경계 안의 순수한 코어와 바깥의 어댑터들

이 글은 직무 통합 과정에서 백엔드 실무를 맡게 된 프론트엔드 개발자가 실제 작업을 부딪히며 배운 것들을 주제 단위로 정리하는 시리즈입니다. 낯선 개념을 이미 아는 개념에 대응시키는 방식으로 씁니다.

  1. 도메인 모델은 왜 DB를 몰라야 할까요? — 이 글
  2. DB 조회는 왜 아웃바운드일까요?
  3. 그 로직은 어디에 살아야 할까요?
  4. 이 API는 누가 부르나요?
  5. Response는 모델을 닮아야 할까요?
  6. null은 비우기일까요, 건드리지 않기일까요?
  7. 컬럼일까요, JSON 컬럼일까요?
  8. DB 스키마에도 git이 필요할까요?
  9. 백엔드 도구들, FE로 치면 무엇일까요?

처음 백엔드 레포를 열었을 때 가장 먼저 든 생각은 "필드 하나 추가하는데 왜 파일을 열 개나 고치지?"였습니다. 모델, 엔티티, DTO, Request, Response — 비슷하게 생긴 클래스가 모듈마다 하나씩 있고 같은 필드가 곳곳에 반복됩니다. 처음엔 중복으로 보였는데 작업을 끝내고 나니 그 반복이 실수가 아니라 이 구조의 의도였습니다. 그 의도를 이해하는 열쇠가 바로 "모델이 중심"이라는 말이었습니다.

백엔드는 왜 모델을 중심에 놓고, 모델은 왜 DB를 모르는 채로 두는 걸까요?


이 글에서 다루는 내용

이 글은 헥사고날 아키텍처(hexagonal architecture)의 가장 기초가 되는 아이디어 — 도메인 모델을 시스템의 중심에 놓고 순수하게 유지하는 것 — 를 다룹니다. 백엔드를 처음 접하는 분, 특히 저처럼 프론트엔드에서 넘어온 분을 독자로 상정하고, 새 개념이 나올 때마다 프론트엔드에서 이미 쓰고 있는 개념에 대응시켜 설명하겠습니다. JPA가 뭔지 몰라도 따라올 수 있습니다. 저도 이 작업을 시작할 때 몰랐습니다.





1. 모델은 순수합니다

백엔드 아키텍처 자료를 읽다 보면 헥사고날 아키텍처, 포트와 어댑터(ports & adapters), 클린 아키텍처(clean architecture) 같은 이름이 계속 나옵니다. 이름은 여러 개지만 핵심 아이디어는 하나입니다.

시스템의 중심에 도메인 모델(domain model)을 놓는다. 모델은 업무의 개념과 규칙만 표현하고, DB·프레임워크·프로토콜을 전혀 모른다.

여기서 "모른다"는 문자 그대로입니다. 모델 코드 어디에도 SQL이 없고, HTTP가 없고, 어떤 데이터베이스 라이브러리의 import도 없습니다. 처음엔 이 말이 이상하게 들렸습니다. 데이터를 저장하고 꺼내는 게 백엔드의 일 아닌가? 그런데 이 구조에서 영속성(persistence)은 모델의 속성이 아니라 모델 바깥에 나중에 붙이는 부품입니다. JPA를 쓰든 JDBC를 쓰든 아예 다른 저장소로 갈아타든, 모델은 한 줄도 바뀌지 않습니다. 갈아끼울 수 있는 이유는 갈아끼움의 기준 틀인 모델이 어느 쪽에도 물려 있지 않기 때문입니다.

다만 이 구조의 값어치를 "언젠가 DB를 갈아끼울 수 있다"에서만 찾으면 반쪽입니다. 솔직히 실무에서 DB를 통째로 교체하는 일은 드뭅니다. 더 일상적인 이점은 테스트 가능성입니다 — 진짜 DB를 띄우지 않고도 코어의 업무 규칙을 검증할 수 있고, 바깥 기술의 유행이 바뀌어도 도메인 코드가 흔들리지 않습니다. 이 구조를 제안한 Alistair Cockburn의 원래 동기도 교체가 아니라 이쪽 — 바깥세상과 격리된 채 개발하고 테스트할 수 있는 애플리케이션 — 이었습니다.


React 코어와 렌더러

프론트엔드 개발자에게 이 구조를 가장 정확하게 번역해 주는 것은 React입니다.

React 코어(reconciler)는 DOM을 모릅니다. 어떤 엘리먼트를 그려야 하는지 계산할 뿐, 그것이 브라우저 DOM이 될지 네이티브 뷰가 될지 3D 오브젝트가 될지는 관심 밖입니다. react-dom, react-native, @react-three/fiber가 각각의 세계에 붙는 어댑터고 코어는 순수하게 유지됩니다. 그래서 같은 코어로 웹도 그리고 네이티브도 그립니다.

React 코어와 렌더러, 도메인 모델과 어댑터의 평행 구조

모델 = React 코어, JPA = 렌더러. 이 대응 하나를 잡고 나니 백엔드 코드가 갑자기 읽히기 시작했습니다. 렌더러가 코어를 의존하지 코어가 렌더러를 의존하지 않듯이, 저장소 코드가 모델을 의존하지 모델이 저장소를 의존하지 않습니다.

전체 그림을 그려보면 이렇습니다.

다이어그램의 화살표는 호출(제어)의 방향입니다. 바깥을 모른다면서 코어에서 어댑터로 화살표가 나가는 게 모순처럼 보일 수 있는데, 호출의 방향과 의존의 방향은 다른 것입니다. 이 갈라짐은 4장에서 다시 다룹니다.

가운데에 모델과 유즈케이스(use case)가 있고, REST API·DB·메시지 큐 같은 바깥세상과의 접점은 전부 어댑터(adapter)로 밀려나 있습니다. 헥사고날이라는 이름의 육각형은 별 뜻이 없습니다 — 어댑터가 여러 면에 붙을 수 있다는 것을 그림으로 표현하다 보니 육각형이 됐을 뿐이라고 Cockburn 본인이 밝히고 있습니다.


Recap

헥사고날 아키텍처의 핵심은 도메인 모델을 시스템의 중심에 놓고 순수하게 유지하는 것입니다. 모델은 업무의 개념과 규칙만 알고 DB·프레임워크·프로토콜은 전혀 모릅니다. React 코어가 DOM을 모른 채 react-dom 같은 렌더러에게 실제 그리기를 맡기는 것과 같은 구조로, 영속성은 모델 바깥에 나중에 붙이는 부품입니다. 이 순수성의 일상적인 이점은 DB 교체가 아니라 테스트 가능성입니다.




2. 갈아끼움을 가능하게 하는 것, 인터페이스

그런데 모델이 DB를 모른다면 저장은 대체 누가, 어떻게 할까요? 여기서 등장하는 메커니즘이 인터페이스입니다. 헥사고날 용어로는 포트(port)라고 부릅니다.

코어 쪽은 "저장할 수 있다"는 계약만 선언합니다. 구현은 바깥에 삽니다.

// 코어 모듈 — 모델은 인터페이스(포트)만 안다
interface UserProfileRepository {
fun find(userId: UserId): UserProfile?
fun save(profile: UserProfile)
}
// 어댑터 모듈 — JPA 구현. 갈아끼우는 부품
class UserProfileJpaRepository(...) : UserProfileRepository { ... }

이 패턴은 프론트엔드에서 늘 하던 것과 같은 결입니다.

// 소비하는 쪽은 "저장할 수 있다"는 계약만 안다
interface ProfileStorage {
load(): Profile;
save(p: Profile): void;
}
// localStorage 구현이든 IndexedDB 구현이든 소비하는 쪽은 모른다

왜 모델부터 설계할까

포트가 갈아끼움을 가능하게 한다면, 설계의 순서가 모델을 지켜줍니다. 영속성부터 붙이고 시작하면 자연스럽게 "모델 = DB 로우(row)"라고 생각하게 됩니다. 테이블 구조가 먼저 있고 그 위에 코드를 얹는 식이 되는 것입니다.

프론트엔드로 번역하면 이것은 서버 응답 shape을 그대로 뷰모델로 쓰는 것과 같은 함정입니다. 화면의 필요가 아니라 남의 사정 — 서버 응답이든 테이블 구조든 — 이 설계를 지배하게 됩니다. 응답 구조가 바뀔 때마다 컴포넌트가 흔들리는 코드를 겪어본 분이라면 이 함정의 비용을 이미 알고 계실 것입니다.

그래서 순서가 중요합니다. 영속성을 일단 잊고 모델부터 설계합니다. 예를 들어 사용자 프로필에 병역 정보를 추가한다면, "병역이란 사항·종류·복무 기간으로 이루어진 하나의 개념 묶음이다"라는 정의가 먼저고, 그것이 개별 컬럼에 저장될지 JSON 컬럼 하나에 저장될지는 별도의 — 그리고 나중의 — 결정입니다. 저장 형태는 어댑터의 사정이고 주(主)는 모델입니다.


Recap

모델이 DB를 모른 채 저장을 시키는 메커니즘은 인터페이스(포트)입니다. 코어가 계약을 선언하고 어댑터가 그것을 구현하므로 구현체는 언제든 갈아끼울 수 있습니다. 그리고 설계의 순서가 이 구조를 지켜줍니다 — 영속성부터 시작하면 테이블 구조가 모델을 지배하는, 서버 응답 shape을 그대로 뷰모델로 쓰는 것과 같은 함정에 빠집니다.




3. 요청 하나가 지나가는 길

이제 구조의 정적인 그림은 잡혔으니, 실제 요청 하나가 이 구조를 어떻게 통과하는지 따라가 보겠습니다. 모든 요청은 어댑터로 들어와서 유즈케이스를 지나 모델을 만나고, 다시 유즈케이스와 어댑터를 지나 나갑니다.

API ── 어댑터 ── 유즈케이스 ── (모델) ── 유즈케이스 ── 어댑터 ── DB

조회 요청을 예로 풀면 이렇게 됩니다.

눈여겨볼 것은 DB에서 나온 row가 그대로 흘러가지 않고 어댑터에서 모델로 복원된 뒤에야 안쪽으로 들어간다는 점, 그리고 나갈 때도 모델이 그대로 나가지 않고 Response 전용 표현(DTO) 으로 바뀌어 나간다는 점입니다. 층마다 자기 표현이 있습니다.

프론트엔드의 레이어 통과와 정확히 평행합니다. 이벤트 → 핸들러 → 상태 → 파생 계산 → 렌더. 방향과 층이 있고 층마다 자기 표현이 있다는 점이 같습니다. 모델이 항상 원형 그대로 흐르는 것도 아닙니다 — 다른 모델과 합쳐지거나(combine) 찢어져서(split) 계층 경계를 건넙니다. 그래도 기준은 항상 모델입니다.


Recap

요청은 어댑터로 들어와 유즈케이스를 지나 모델을 만나고 같은 길을 되짚어 나갑니다. 경계를 건널 때마다 표현이 바뀝니다 — row는 모델로 복원되어 들어오고 모델은 DTO로 변환되어 나갑니다. 프론트엔드의 이벤트 → 핸들러 → 상태 → 렌더 흐름처럼 방향과 층이 있고, 층마다 자기 표현이 있습니다.




4. 경계를 지키는 것은 폴더가 아니라 빌드입니다

마지막으로 서두의 의문 — "필드 하나 추가하는데 왜 파일을 열 개나 고치지?" — 으로 돌아가겠습니다.

이런 구조의 백엔드 레포를 열어보면 한 도메인이 여러 개의 Gradle 모듈로 쪼개져 있습니다. 예를 들면 이런 식입니다.

모듈역할FE로 치면
:model순수 도메인 모델·값객체·enum. 외부 의존 없음타입 + 헤드리스 도메인 로직 패키지
:protocol서비스 인터페이스와 DTO 계약API 계약 (인터페이스)
:repositoryJPA 엔티티와 컨버터 — DB 어댑터ORM·직렬화 계층
:service비즈니스 로직 구현체상태 관리·오케스트레이션 계층
:apiREST 컨트롤러와 Request/Response라우트 핸들러 + 응답 타입

폴더로 나눈 게 아니라 빌드 단위로 나눈 것이 핵심입니다. 모듈 간 의존은 빌드 설정에 명시되고 방향은 안쪽(:model)으로만 향합니다. 프론트엔드 모노레포에서 디자인시스템 패키지가 앱 코드를 import할 수 없는 것과 같은 방향 그래프인데, 이걸 lint 규칙이 아니라 컴파일 자체가 안 되는 수준에서 강제합니다. 모델이 어댑터를 참조하는 코드는 리뷰어가 잡아내기 전에 빌드가 먼저 거부합니다.

그리고 여기에 서두의 답이 있습니다. 필드 하나를 추가할 때 파일을 열 개 고치게 되는 것은 같은 코드가 열 벌 복사되어 있어서가 아니라, 각 레이어가 자기 경계의 계약을 따로 갖기 때문입니다. 모델의 필드, DB 저장 형태, API 응답 형태는 서로 다른 관심사고 각자의 속도로 진화합니다. 그 독립성의 대가가 반복처럼 보이는 명시적 변환들입니다. 반복이 아니라 경계의 표현인 셈입니다.

한 가지 의문이 남습니다. 코어는 런타임에 분명히 DB를 쓰는데 의존은 안쪽으로만 향한다니, 호출의 방향과 의존의 방향이 어긋나 있습니다. 이 모순을 푸는 것이 의존성 역전(DIP, Dependency Inversion Principle)인데, 인바운드/아웃바운드 어댑터의 구분과 함께 다음 글에서 다루겠습니다.


Recap

헥사고날 구조의 백엔드는 레이어를 폴더가 아니라 빌드 모듈로 나누고, 의존 방향을 컴파일 수준에서 강제합니다. 필드 하나에 여러 파일을 고치게 되는 것은 중복이 아니라 각 레이어가 자기 계약을 따로 갖는 데서 오는 경계의 표현입니다. 호출은 밖으로 나가는데 의존은 안으로만 향하는 모순은 다음 글의 주제인 의존성 역전으로 풀립니다.




FE ↔ BE 대응표

이 글에서 다룬 대응만 추려 정리합니다. 시리즈가 진행되면서 이 표는 계속 늘어날 예정입니다.

BE 개념FE에서 가장 가까운 것대응의 핵심
도메인 모델React 코어 / 헤드리스 로직순수한 중심, 바깥을 모름
어댑터렌더러 (react-dom 등)갈아끼울 수 있는 바깥 부품
포트 (인터페이스)스토리지 인터페이스 계약계약은 안쪽이 소유한다
DTO / ResponseAPI 응답 타입 · 뷰모델경계를 건너는 전용 표현
모듈 의존 방향모노레포 import 방향빌드가 강제하는 방향 그래프



References

헥사고날 아키텍처

  1. Alistair Cockburn — Hexagonal Architecture
  2. Herberto Graça — Ports & Adapters Architecture
  3. Netflix Tech Blog — Ready for changes with Hexagonal Architecture

클린 아키텍처와 레이어링

  1. Robert C. Martin — The Clean Architecture
  2. Martin Fowler — PresentationDomainDataLayering

React 코어와 렌더러

  1. react-reconciler — React 공식 커스텀 렌더러 패키지


좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자