프레임워크는 편리하다. 그 편리함이 함정이다. 새 프로젝트를 시작할 때 우리는 좋은 프레임워크를 만나면 곧장 그 위에 집을 짓고 싶어진다. 문서가 권하는 대로 기반 클래스를 상속하고, 애너테이션을 붙이고, 규약에 코드를 맞춘다. 30장이 데이터베이스를, 31장이 웹을 세부사항으로 밀어냈다면, 32장은 그중에서도 가장 밀어내기 어려운 세부사항을 다룬다. 프레임워크는 우리를 안에서부터 끌어안기 때문이다. DB나 웹은 시스템의 가장자리에 있지만, 프레임워크는 처음부터 우리더러 자기 품 안으로 들어오라고 요구한다.
마틴의 경고는 한 문장으로 요약된다 — 프레임워크와 결혼하지 마라. 데이트는 해도 좋다. 그러나 혼인 서약을 하고 핵심 업무 규칙을 그 품에 통째로 맡기는 순간, 프레임워크의 운명이 곧 내 시스템의 운명이 된다.
관계는 처음부터 비대칭이다
프레임워크 저자와 나 사이의 관계를 냉정하게 보자. 그것은 대등한 계약이 아니다. 비대칭이다.
프레임워크 저자는 자기 자신의 문제를 풀기 위해, 혹은 자기가 상상한 수많은 사용자의 공통 문제를 풀기 위해 프레임워크를 만들었다. 그는 나를 알지 못하고, 내 도메인을 모르며, 내가 3년 뒤에 어떤 방향으로 시스템을 틀어야 하는지에는 아무 관심도 없다. 그는 나에게 아무것도 약속하지 않았다. 반면 내가 그의 프레임워크를 깊이 끌어안으면, 나는 그에게 많은 것을 약속하게 된다 — 그의 규약을 따르겠다, 그의 기반 클래스를 상속하겠다, 그의 버전 정책과 방향 전환을 감내하겠다.
이 관계에서 헌신하는 쪽은 언제나 나다. 그는 자신의 이유로 다음 메이저 버전에서 API를 갈아엎을 수 있고, 특정 기능을 폐기(deprecate)할 수 있고, 아예 프로젝트를 접을 수도 있다. 그가 방향을 틀면 나는 따라가야 한다. 내가 깊이 결합했을수록 그 추적 비용은 커진다. 그는 나를 배려할 의무가 없고, 나는 그에게 붙잡혀 있다. 이것이 비대칭의 실체다.
판단 기준: 어떤 라이브러리를 도입할 때, “이 저자가 다음 버전에서 내 코드를 깨뜨리기로 결정한다면 나는 어디까지 끌려가는가”를 먼저 그려 본다. 그 영향 반경이 UI나 진입점 몇 군데에 갇히면 안전한 데이트이고, 엔티티와 유스케이스까지 번지면 그건 결혼이다. 함정: 프레임워크가 성숙하고 인기 있고 문서가 훌륭하다는 사실을 관계의 대칭성으로 착각하는 것 — 인기와 완성도는 저자가 나를 배려한다는 뜻이 아니라, 더 많은 사람을 끌어안은 채 언제든 방향을 틀 수 있다는 뜻일 뿐이다.
저자가 파는 것과 내가 사는 것은 다르다
프레임워크 문서는 대개 “이렇게 우리 프레임워크와 최대한 밀착하라”고 권한다. 우리 기반 클래스를 상속하고, 우리 객체를 당신의 핵심 업무 객체에 심고, 우리 규약을 당신의 도메인 깊숙이 들이라고. 저자로서는 자연스러운 조언이다. 그가 파는 것은 결합이고, 그 결합이 깊을수록 그의 프레임워크는 당신 시스템에서 뽑아내기 어려워지므로 그에게 유리하다.
그러나 내가 사려는 것은 결합이 아니다. 나는 그 프레임워크가 지금 잘 풀어 주는 문제 — 요청 라우팅이든, ORM이든, 의존성 주입이든 — 를 값싸게 해결하고 싶을 뿐이다. 나는 그 편의를 쓰되, 내 시스템의 심장을 담보로 내주고 싶지는 않다. 여기서 저자의 이익과 나의 이익이 갈라진다. 2장의 언어로 말하면, 저자는 오늘의 편의(행위)를 팔고, 나는 내일의 변경 가능성(구조)을 지켜야 한다. 문서가 권하는 밀착을 곧이곧대로 따르면, 나는 저자의 장부에 유리한 거래를 내 장부의 손해로 떠안는 셈이다.
판단 기준: 프레임워크 문서가 “당신의 도메인 객체가 우리 기반 클래스를 상속하게 하라”고 말하면, 그 조언은 저자의 이익에서 나온 것임을 기억하고 반대 방향으로 저항해 본다 — 상속 대신 감싸기(wrapping), 심기 대신 밀어내기가 가능한지 먼저 묻는다. 함정: 튜토리얼이 보여주는 “가장 빠른 시작”을 아키텍처의 권장 형태로 오해하는 것. 튜토리얼은 5분 만에 화면에 무언가를 띄우도록 최적화돼 있지, 3년 뒤 프레임워크를 갈아 끼우도록 설계돼 있지 않다.
결혼의 증거 — 엔티티가 프레임워크를 상속할 때
비대칭이 가장 위험하게 드러나는 장면은, 시스템의 가장 안쪽 원에 있어야 할 핵심 업무 객체가 프레임워크의 기반 클래스를 상속하는 순간이다. 22장의 동심원을 떠올려 보라. 엔티티는 의존성 규칙에 따라 어떤 바깥 원도 향해서는 안 된다. 그런데 다음과 같은 코드는 그 규칙을 정면으로 위반한다.
// 안티패턴 — 핵심 업무 객체가 프레임워크에 결혼했다
public class Loan extends FrameworkEntityBase {
@FrameworkColumn(name = "principal")
@FrameworkValidated
private Money principal;
// 대출 이자 계산 — 이건 순수한 업무 규칙이다.
// 그런데 이 클래스는 FrameworkEntityBase 없이는 존재조차 못 한다.
public Money interestFor(Period period) { ... }
}Loan은 은행의 핵심 업무 규칙이다. 이자를 계산하는 로직은 어떤 프레임워크가 오고 가든 변하지 않아야 할, 시스템에서 가장 오래 살아남을 자산이다. 그런데 이 Loan은 FrameworkEntityBase를 상속하고 애너테이션으로 온몸을 덮고 있어서, 그 프레임워크 없이는 컴파일조차 되지 않는다. 이자 계산 규칙을 테스트하려면 프레임워크 전체를 띄워야 하고, 프레임워크가 메이저 버전에서 기반 클래스의 계약을 바꾸면 은행의 심장이 함께 흔들린다. 가장 안쪽 원이 가장 바깥 원에 의존하는, 의존성 규칙의 완전한 도착(倒錯)이다.
해법의 방향은 밀어내기다. 업무 규칙을 담은 Loan은 프레임워크를 전혀 모르는 순수한 객체로 두고, 프레임워크가 요구하는 형태는 경계 밖의 별도 객체로 분리한다.
// 핵심 업무 객체 — 아무것도 상속하지 않는다. 프레임워크의 이름조차 등장하지 않는다.
public class Loan {
private final Money principal;
public Money interestFor(Period period) { ... }
}
// 프레임워크와의 결합은 바깥 원의 이 객체가 홀로 짊어진다.
@FrameworkTable(name = "loans")
public class LoanRecord {
@FrameworkColumn(name = "principal") private Money principal;
Loan toDomain() { ... } // 규약을 넘나드는 번역은 여기서만 일어난다
}이제 프레임워크가 방향을 틀어도 그 충격은 LoanRecord라는 경계면에서 멈춘다. Loan은 프레임워크가 존재하는지도 모른 채 자기 일만 한다. 데이트는 LoanRecord가 하고, Loan은 독신을 지킨다.
판단 기준: 어떤 도메인 클래스든 extends나 프레임워크 애너테이션이 붙어 있다면, 그 클래스는 이미 프레임워크와 결혼한 것이다 — 그 결합을 바깥 원의 어댑터 객체로 밀어낼 수 있는지 물어라. 함정: “이 프레임워크의 ORM을 쓰려면 엔티티가 기반 클래스를 상속하는 수밖에 없다”는 체념 — 상속을 감싸기로 바꾸는 데는 번역 코드라는 대가가 들지만, 그 대가는 심장을 담보로 내주는 것보다 언제나 싸다.
결정을 미룬다는 것
15장은 좋은 아키텍처가 선택지를 열어 두고 세부사항의 결정을 최대한 미룬다고 했다. 프레임워크야말로 그렇게 미뤄야 할 결정이다. “우리는 이 웹 프레임워크를 쓴다”는 문장은 아키텍처의 첫 페이지가 아니라 각주에 있어야 한다.
프레임워크를 경계 밖으로 밀어내면, 그 선택은 되돌릴 수 있는 결정이 된다. 유스케이스와 엔티티가 프레임워크를 모르므로, 나중에 더 나은 프레임워크가 나타나거나 지금 것이 우리 요구를 배신했을 때, 우리는 어댑터 층만 갈아 끼우면 된다. 반대로 프레임워크와 결혼해 두면 그 결정은 비가역이 된다. 프레임워크를 바꾸는 일이 곧 시스템을 다시 쓰는 일과 같아지기 때문이다. 결혼은 선택지를 닫고, 데이트는 선택지를 연다.
물론 모든 결합을 0으로 만들 수는 없다. 어느 지점에서는 프레임워크의 규약을 실제로 호출해야 하고, main 컴포넌트(26장)나 어댑터 층은 프레임워크를 정면으로 끌어안는다. 요점은 그 끌어안음을 시스템의 바깥 껍질에 가두는 것이다. DIP 위반을 완전히 없앨 수 없듯(11장), 프레임워크 의존도 완전히 없앨 수는 없다. 다만 그것을 심장에서 멀리 떼어 놓을 수는 있다.
판단 기준: 프레임워크 도입을 아직 되돌릴 수 있는 결정으로 유지하고 싶다면, 프레임워크의 이름이 등장하는 파일을 세어 본다 — 그 목록이 어댑터·설정·진입점에 갇혀 있으면 건강하고, 도메인 패키지 전역에 흩어져 있으면 이미 비가역에 가깝다. 함정: “우리는 절대 이 프레임워크를 안 바꾼다”는 확신으로 밀어내기를 생략하는 것 — 바꾸지 않는 것은 선택이지만, 바꿀 수 없게 만드는 것은 선택지의 상실이다. 미래는 그 확신을 자주 배신한다.
예외 — 어떤 결합은 감수한다
그렇다고 모든 프레임워크를 똑같이 경계 밖으로 밀어내야 하는 것은 아니다. 마틴도 여기서 균형을 요구한다. 어떤 결합은 리스크가 낮아 감수할 만하다.
판단의 축은 두 가지다. 첫째, 그 프레임워크가 얼마나 안정적인가. 언어의 표준 라이브러리처럼 사실상 바뀌지 않고 방향을 틀 일이 없는 것이라면, 그것과의 결합은 리스크가 거의 없다(11장에서 String 같은 안정적 구체는 DIP의 예외였던 것과 같은 논리다). 둘째, 결합이 시스템의 어느 원까지 침투하는가. 진입점과 설정에만 닿는 프레임워크라면 크게 걱정할 것이 없다. 위험한 것은 안정성이 낮은 프레임워크가 가장 안쪽 원까지 파고들 때다.
그러니 규칙은 “프레임워크를 쓰지 마라”가 아니다. 프레임워크는 써라 — 다만 결혼이 아니라 데이트로. 편의는 취하되 심장은 내주지 않는 거리 두기, 그것이 이 장의 처방이다.
판단 기준: 결합을 감수할지 말지는 “프레임워크의 안정성 × 침투 깊이”로 저울질한다 — 안정적이고 얕으면 그냥 쓰고, 불안정하고 깊으면 반드시 밀어낸다. 함정: 이 균형을 “그냥 편한 대로 밀착하자”는 게으름의 면허로 오독하는 것 — 예외는 안정성이 확실히 검증된 소수에만 허락되며, 대부분의 애플리케이션 프레임워크는 그 예외에 들지 못한다.
- 프레임워크 저자와 나의 관계는 비대칭이다. 그는 내 사정을 신경 쓰지 않고, 헌신하는 쪽은 언제나 나다.
- 문서가 권하는 밀착은 저자의 이익에서 나온 조언이다 — 저자는 결합(오늘의 편의)을 팔고, 나는 변경 가능성(내일의 구조)을 지켜야 한다.
- 결혼의 증거는 핵심 업무 객체가 프레임워크 기반 클래스를 상속하거나 애너테이션으로 뒤덮이는 것 — 엔티티는 순수하게 두고 프레임워크 결합은 바깥 원의 어댑터로 밀어낸다.
- 밀어내면 프레임워크 선택은 되돌릴 수 있는 결정이 되고, 결혼하면 비가역이 된다. 프레임워크와는 결혼하지 말고 데이트하라.
프레임워크는 세부사항이다. DB가, 웹이 그러했듯 시스템의 가장자리에 세워 두어야 할 도구이지, 아키텍처의 중심에 앉힐 주인이 아니다. 그러나 지금까지의 원칙들은 여전히 추상의 지도 위에 있다. 다음 장은 이 모든 규칙 — 경계, 의존성 규칙, 험블 객체, 프레임워크 밀어내기 — 을 하나의 구체적인 시스템 위에서 처음부터 조립해 보인다.
다음장으로 33장