DB 조회는 왜 아웃바운드일까요?

이 글은 직무 통합 과정에서 백엔드 실무를 맡게 된 프론트엔드 개발자가 실제 작업을 부딪히며 배운 것들을 주제 단위로 정리하는 시리즈입니다. 낯선 개념을 이미 아는 개념에 대응시키는 방식으로 씁니다.
처음 어댑터 분류를 접했을 때 DB 리포지토리가 아웃바운드 어댑터라는 게 영 어색했습니다. DB에서 데이터를 받아오는 코드인데, 데이터가 들어오면 인(in) 아닌가? 알고 보니 제가 기준을 잘못 짚고 있었습니다. 그리고 그 기준을 바로잡고 나니 지난 글 끝에 남겨둔 모순 — 호출은 밖으로 나가는데 의존은 안으로만 향한다 — 도 같이 풀렸습니다.
인바운드와 아웃바운드를 가르는 기준은 무엇이고, 호출의 방향과 의존의 방향은 어떻게 서로 갈라질 수 있을까요?
이 글에서 다루는 내용
지난 글에서 도메인 모델을 중심에 놓는 구조를 봤다면, 이번 글은 그 구조를 관통하는 방향의 이야기입니다. 어댑터를 인바운드/아웃바운드로 가르는 기준을 잡고, 의존이 왜 항상 안쪽으로만 향해야 하는지, 그리고 호출과 의존이 갈라지는 지점에서 의존성 역전(DIP)이 무슨 일을 하는지 순서대로 풀어보겠습니다. 이번에도 프론트엔드에서 매일 쓰는 개념 — 이벤트 리스너, fetch, props 콜백 — 이 그대로 대응됩니다.
1. 누가 전화를 거는가
어댑터는 두 종류로 나뉩니다. 그런데 그 기준이 데이터가 흐르는 방향이 아니라 호출(제어)의 방향입니다. 제가 처음에 헷갈린 이유가 정확히 여기 있었습니다 — 데이터의 방향으로 읽고 있었던 것입니다.
| 구분 | 표준 명칭 | 정의 | 예 |
|---|---|---|---|
| 인바운드 | driving / primary | 외부가 나를 호출 | REST 컨트롤러, 메시지 컨슈머, 스케줄러 |
| 아웃바운드 | driven / secondary | 내가 외부를 호출 | DB 리포지토리, 외부 API 클라이언트, 메시지 프로듀서 |
DB에서 데이터를 끌어와도 DB 어댑터는 아웃바운드입니다. 데이터는 들어오지만 전화를 거는 쪽이 나이기 때문입니다. 표준 명칭이 방향(in/out)이 아니라 driving(나를 모는)/driven(내가 모는) 인 것도 같은 이유입니다.
사실 이 구분은 프론트엔드에서 이미 매일 쓰고 있습니다.
addEventListener('click', ...)— 세상이 나를 부릅니다 → 인바운드fetch('/api/...')— 내가 세상을 부릅니다 → 아웃바운드
fetch가 응답 데이터를 받아와도 아웃바운드 호출이라는 데에 아무 혼란이 없습니다. DB 조회도 똑같습니다.

애플리케이션 전체로 보면 이렇게 됩니다.
Recap
어댑터를 가르는 기준은 데이터의 방향이 아니라 호출의 방향입니다. 외부가 나를 부르면 인바운드(driving), 내가 외부를 부르면 아웃바운드(driven)입니다. fetch가 데이터를 받아와도 아웃바운드 호출이듯 DB 조회도 아웃바운드입니다 — 전화를 거는 쪽이 나이기 때문입니다.
2. 의존은 안쪽으로만 향합니다
방향 감각이 잡혔으니 이제 이 구조의 제1 규칙을 말할 수 있습니다. 클린 아키텍처에서 의존성 규칙(Dependency Rule) 이라고 부르는 것입니다.
소스 코드 의존은 항상 바깥(어댑터)에서 안(모델)으로만 향한다. 모델은 어댑터를 import할 수 없다.
지난 글에서 봤듯이 이 규칙은 매너나 컨벤션이 아니라 빌드 모듈 분리로 강제됩니다. 프론트엔드 모노레포에서 디자인시스템 패키지가 앱 코드를 import할 수 없는 것과 같은 방향 그래프인데, lint 규칙이 아니라 컴파일 자체가 안 되는 수준의 강제입니다.
그런데 왜 하필 안쪽일까요? 변경의 빈도 때문입니다. 어댑터가 다루는 것들 — 웹 프레임워크, DB 라이브러리, 외부 API — 은 자주 바뀝니다. 반면 업무 규칙은 상대적으로 오래갑니다. 의존이 안쪽으로 향하면 잦은 변경이 경계 밖에서 멈춥니다. 반대로 향하면 DB 라이브러리 업그레이드가 업무 규칙 코드를 흔드는, 꼬리가 몸통을 흔드는 상황이 됩니다. 덜 바뀌는 쪽에 의존하라 — 규칙의 실체는 이 한 문장입니다.
Recap
의존성 규칙은 소스 코드 의존이 항상 바깥에서 안으로만 향한다는 것이고, 빌드 모듈 분리가 이를 컴파일 수준에서 강제합니다. 방향이 안쪽인 이유는 변경의 빈도입니다 — 자주 바뀌는 기술이 오래가는 업무 규칙에 의존해야 변경이 경계 밖에서 멈춥니다.
3. 호출과 의존이 갈라지는 지점, 의존성 역전
그런데 1장과 2장을 나란히 놓으면 모순이 보입니다. 1장에서 코어는 아웃바운드 어댑터를 호출해서 DB를 씁니다. 2장에서 코어는 어댑터에 의존할 수 없습니다. 호출은 밖으로 나가는데 의존은 안으로만 — 어떻게 의존하지 않는 코드를 호출한다는 걸까요?
이 모순을 푸는 것이 의존성 역전 원칙(DIP, Dependency Inversion Principle) 입니다. 방법은 지난 글에서 이미 봤습니다. 코어가 인터페이스(포트)를 선언하고 바깥이 그것을 구현합니다. 그러면 호출의 방향과 의존의 방향이 갈라집니다.
런타임에는 유즈케이스가 리포지토리 구현을 호출하지만, 컴파일 타임에는 리포지토리 구현이 코어의 인터페이스를 의존합니다. 화살표가 뒤집혀 있습니다. "역전(inversion)"이라는 이름은 여기서 왔습니다 — 전통적인 레이어링에서는 정책(업무 규칙)이 세부사항(DB)에 의존했는데, 그 방향을 뒤집은 것입니다.
코드로 보면 유즈케이스는 구현이 누구인지 끝까지 모릅니다.
// 코어 모듈 — 유즈케이스는 포트만 안다. 구현이 누구인지 모른다
class GetUserProfile(
private val repository: UserProfileRepository // 인터페이스 타입으로 주입받는다
) {
fun execute(userId: UserId): UserProfile? = repository.find(userId)
}
props 콜백이 바로 이것입니다
프론트엔드에서 DIP를 가장 정확하게 번역해 주는 것은 props 콜백입니다.
// 자식 — "제출되면 알려준다"는 계약(시그니처)을 선언한다
function ProfileForm({ onSubmit }: { onSubmit: (v: Form) => void }) {
// 자식은 부모를 모른 채 부모의 코드를 실행한다
return <form onSubmit={() => onSubmit(values)}>…</form>;
}
// 부모 — 구현을 꽂아준다
<ProfileForm onSubmit={(v) => api.save(v)} />;
자식 컴포넌트가 onSubmit이라는 시그니처를 선언하고 부모가 구현을 꽂아줍니다. 런타임에 실행되는 것은 부모의 코드지만 자식은 부모를 import한 적이 없습니다. 코어가 어댑터를 모른 채 어댑터의 코드를 실행하는 것과 같은 구조입니다. 계약은 안쪽(자식)이 소유하고 구현은 바깥(부모)이 가져옵니다.
의존성 주입(DI, Dependency Injection)은 구현체를 밖에서 꽂아 넣는 기법이고, 의존성 역전(DIP)은 계약의 소유권을 안쪽으로 옮기는 원칙입니다. 주입은 역전 없이도 쓸 수 있습니다 — 인터페이스가 어댑터 쪽에 있어도 주입은 됩니다. 역전의 핵심은 "누가 꽂아주느냐"가 아니라 "계약을 누가 소유하느냐" 입니다.
이것으로 지난 글에서 남겨둔 모순은 풀렸습니다. 구조와 방향이 잡혔으니 다음 질문은 자연스럽게 "그래서 어떤 코드가 어디에 사는가"입니다 — 모델 안에 로직을 얼마나 둘 것인지, 서비스는 왜 두 종류인지는 다음 글에서 다루겠습니다.
Recap
코어는 어댑터를 호출하지만 의존하지 않습니다. 코어가 인터페이스를 선언하고 바깥이 구현하기 때문에 런타임 호출은 밖으로, 컴파일 의존은 안으로 — 두 방향이 갈라집니다. props 콜백에서 자식이 시그니처를 선언하고 부모가 구현을 꽂아주는 것과 같은 구조이며, 핵심은 계약의 소유권이 안쪽에 있다는 것입니다.
FE ↔ BE 대응표
이 글에서 다룬 대응입니다. 지난 글의 표에 이어집니다.
| BE 개념 | FE에서 가장 가까운 것 | 대응의 핵심 |
|---|---|---|
| 인바운드 어댑터 | 이벤트 리스너 | 세상이 나를 부른다 |
| 아웃바운드 어댑터 | fetch / API 클라이언트 | 내가 세상을 부른다 |
| 의존성 규칙 | 모노레포 import 방향 | 덜 바뀌는 쪽으로만 의존한다 |
| 의존성 역전 (DIP) | props 콜백 | 안쪽이 시그니처를 선언하고 바깥이 구현을 꽂는다 |
References
포트와 어댑터, 방향
의존성 규칙과 DIP
- Robert C. Martin — The Clean Architecture
- Brett L. Schuchert — DIP in the Wild (martinfowler.com)
- Martin Fowler — InversionOfControl
