무엇인가
책임을 층으로 쌓고, 호출을 한 방향으로만 흐르게 하는 구조다. 위층은 바로 아래층만 알고, 아래층은 위층의 존재를 모른다.
핵심은 층을 나누는 것이 아니라 의존 방향을 한쪽으로 고정하는 것이다. 아래층이 위층을 부르기 시작하면 층은 이름만 남고 그냥 한 덩어리가 된다.
구조
graph TD P[표현 계층<br/>Presentation] --> D[도메인 계층<br/>Domain] D --> A[데이터 접근 계층<br/>Data Access] A --> S[(저장소)]
C++로 보기
각 층을 인터페이스로 끊어 두면 방향이 코드에 강제된다.
아래 예제에서 UserRepository는 위층 타입을 하나도 모른다.
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
// ── 데이터 접근 계층 ─────────────────────────────
struct User {
int id;
std::string name;
};
class UserRepository {
public:
virtual ~UserRepository() = default;
virtual std::optional<User> findById(int id) const = 0;
};
class InMemoryUserRepository : public UserRepository {
std::unordered_map<int, User> rows_{{1, {1, "수원"}}, {2, {2, "지훈"}}};
public:
std::optional<User> findById(int id) const override {
auto it = rows_.find(id);
if (it == rows_.end()) return std::nullopt;
return it->second;
}
};
// ── 도메인 계층 ─────────────────────────────────
// 저장소를 인터페이스로만 안다. 구현이 무엇인지 모른다.
class GreetingService {
const UserRepository& repo_;
public:
explicit GreetingService(const UserRepository& repo) : repo_(repo) {}
std::string greet(int userId) const {
auto user = repo_.findById(userId);
if (!user) return "누구세요?";
return user->name + "님 반갑습니다";
}
};
// ── 표현 계층 ───────────────────────────────────
// 도메인만 안다. 저장소를 직접 만지지 않는다.
class ConsoleView {
const GreetingService& service_;
public:
explicit ConsoleView(const GreetingService& s) : service_(s) {}
void show(int userId) const { std::cout << service_.greet(userId) << '\n'; }
};
int main() {
InMemoryUserRepository repo; // 조립은 가장 바깥에서 한다
GreetingService service{repo};
ConsoleView view{service};
view.show(1);
view.show(99);
}층을 끊어 두면 저장소를 바꿔 끼울 수 있다. InMemoryUserRepository 자리에
DB 구현을 넣어도 도메인·표현 계층은 한 줄도 안 바뀐다. 테스트에서 가짜 저장소를 쓰는 것도 같은 이유다.
언제 쓰나
- 업무 규칙과 저장 방식이 서로 다른 속도로 변할 때. 층이 변경을 가둔다.
- 팀이 나뉘어 일할 때. 층 경계가 그대로 작업 경계가 된다.
- 무엇으로 시작할지 모르겠을 때의 기본값. 가장 익숙하고 설명이 필요 없다.
대가
- 층을 지나가기만 하는 코드가 생긴다. 필드 하나 추가하는 데 세 층을 다 고치는 일이 흔하다.
- 호출이 층마다 한 번씩 튀므로 성능이 중요한 경로에서는 부담이 된다.
- 층을 건너뛰고 싶은 유혹이 계속 생긴다. 한 번 허용하면 규칙이 무너진다.
실제로 만나는 곳
전형적인 웹 애플리케이션(컨트롤러 → 서비스 → 리포지터리), OS의 네트워크 스택, 그리고 OSI 7계층 자체가 이 패턴의 교과서적 사례다.