
서론
요즘은 프로그래밍하면 화두가 의도(Intent)이다. 항상 유행하는 키워드가 늘 그렇듯 의도가 중요하다면서 아키텍쳐(MVI)에도 의도를 붙이고 프로그래밍에도 의도를 붙이고(Intent-Driven-Programming) 아주 난리가 아니다.
내용은 좋다. 의도를 중시해야한다. 소비자의 의도를 파악해야한다. 엔드 유저의 의도를 파악해서, 프로그래밍에 접목해야한다.
등등 말은 정말 으리으리하다.
문제는 실제로 게시글들을 보다보면 결국 의도라는 것이 다층적이라는 것이 드러난다.
우리는 흔히 “좋은 추상화”라는 말을 쉽게 사용한다.
하지만 실제로 코드를 작성하다 보면, 처음에는 깔끔해 보이던 설계가
시간이 지나면서 점점 무너지는 경험을 반복하게 된다.
이 현상은 단순한 실수나 숙련도의 문제가 아니다.
문제는 더 근본적이다.
의도(intent)가 실제 시스템으로 변환되는 과정 자체가
여러 층의 추상화 레이어를 거치며, 그 사이에서 발생하는
정보 손실과 맥락 의존성 때문이다.

(토마스 쿤의 과학혁명의 구조)
문제를 뭉개는 방식
개인적으로 이 의도(Intent)를 보는 관점은
토마스 쿤의 “패러다임”과 유사하다 생각한다.
문제는 이 단어가 너무 많이 쓰였다는 데 있다.
쿤 자신도 『과학혁명의 구조』에서 “패러다임”이라는 용어를
20가지가 넘는 의미로 사용했다는 비판을 받는다.
(Masterman의 고전적 비판)
즉, 이 개념은 이미 하나의 명확한 이론 용어라기보다
맥락에 따라 의미가 바뀌는 메타 개념에 가깝다.
그 결과, 현대의 소프트웨어 논의에서 “패러다임”이라는 단어는
설명력을 가지기보다 오히려 설명을 회피하는 용어로 사용되는 경우가 많다.
이 점은 최근 유행하는 “Intent” 담론에서도 동일하게 나타난다.
의도(intent)라는 개념 역시 명확하게 정의되지 않은 채,
여러 레이어의 의미를 동시에 가리키는 용어로 사용된다.
- 사용자 요구
- 비즈니스 목표
- 도메인 모델
- 코드 레벨의 의도
이 모든 것이 하나의 “intent”라는 단어로 묶이면서,
문제는 단순화되는 것이 아니라 오히려 흐려진다.
따라서 intent를 하나의 단일 개념으로 다루는 대신,
레이어별로 분해해서 볼 필요가 있다.
그런 지점에서 Intent는 하나가 아니라,
서로 다른 레벨에서 정의되는 여러 개의 intent의 집합이다.
저 위의 예시만 봐도 그렇다.
- 사용자 intent (UX 레벨)
- 도메인 intent (비즈니스 규칙)
- 시스템 intent (아키텍처 결정)
- 구현 intent (코드 레벨 선택)
이들은 동일한 단어를 공유하지만,
서로 다른 제약과 최적화 기준을 가진다.
이 지점에서 중요한 전환이 하나 더 필요하다.
문제를 “개발자의 숙련도”나 “설계의 완성도”로 보는 대신,
의도(intent)가 어떻게 시스템으로 변환되는지 자체를 봐야한다.
의도는 곧바로 코드로 번역되지 않는다.
그 사이에는 여러 단계가 존재한다.
Intent
→ 도메인 모델
→ 추상화 (아키텍처, 인터페이스)
→ 코드
→ 컴파일
→ 실행 환경
→ 하드웨어 변환
이 과정은 단순한 변환이 아니라,
각 단계에서 정보를 버리고 특정 방향으로 해석하는 과정이다.
인텐트에서 도메인 모델로, 그리고 추상화로 주요 비즈니스 로직을 추출할때도, 그리고 코드로 쓸때도 모두 정보의 손실이 존재한다.
그리고 이때 발생하는 정보 손실은 단순히 “정밀도가 떨어진다”는 문제가 아니라, 이후의 선택지를 제한한다는 점에서 더 중요하다.
그리고
한 번 선택된 추상화는 이후의 모든 표현을 제약한다.
추상화는 왜 항상 현장에서 망가지는가
추상화가 "설계의 완성도" 문제라면, 숙련된 개발자가 설계하면 해결된다. 그런데 현실은 그렇지 않다. 숙련된 개발자가 설계한 추상화도 시간이 지나면 망가진다. 이유는 추상화 선택 자체가 이후의 선택지를 제한하기 때문이다.
케이스 1: Repository 패턴과 Transaction Boundary
Repository 패턴의 의도는 명확하다. 데이터 접근 로직을 도메인 레이어에서 분리한다. 교과서대로 설계하면 이렇게 된다.
public interface IOrderRepository
{
Order GetById(int id);
void Save(Order order);
}
public interface IInventoryRepository
{
Inventory GetByProductId(int productId);
void Save(Inventory inventory);
}서비스 레이어에서 두 Repository를 조합한다.
public class OrderService
{
private readonly IOrderRepository orderRepo;
private readonly IInventoryRepository inventoryRepo;
public Result<OrderId, OrderError> PlaceOrder(PlaceOrderCommand command)
{
var inventory = inventoryRepo.GetByProductId(command.ProductId);
if (inventory.Stock < command.Quantity)
return Result.Failure(OrderError.OutOfStock);
var order = Order.Create(command);
inventory.Decrease(command.Quantity);
orderRepo.Save(order); // DB hit 1
inventoryRepo.Save(inventory); // DB hit 2
return Result.Success(order.Id);
}
}설계는 깔끔하다. 문제는 orderRepo.Save와 inventoryRepo.Save 사이에서 프로세스가 죽으면 주문은 생성됐는데 재고는 그대로인 상태가 된다.
Transaction을 걸려면 어떻게 해야 하는가. 가장 흔한 선택은 IUnitOfWork를 추가하는 것이다.
public interface IUnitOfWork
{
void BeginTransaction();
void Commit();
void Rollback();
}이 순간 추상화가 무너지기 시작한다. 그러나 무너지는 이유는 추상화 자체에 있지 않다.
처음부터 인터페이스에 묵시적으로 박혀 있던 가정이, 새 환경에서 더 이상 성립하지 않게 된 것이다.
IOrderRepository와 IInventoryRepository는 'DB 접근을 분리한다'는 의도만 표현하는 것처럼 보였지만, 실제로는 더 강한 가정이 함께 들어가 있었다. 두 Repository가 같은 트랜잭션 컨텍스트 안에 산다는 가정. 이 가정은 인터페이스 시그니처 어디에도 적혀 있지 않다. 메서드 이름에도, 반환 타입에도 없다. 그러나 Save라는 메서드를 호출 가능한 단위로 만든 순간, '같은 커밋 단위 안에서 호출된다'는 가정이 묵시적으로 박혔다.
IUnitOfWork 도입은 이 묵시적 가정을 명시화하는 작업이다. 새로 가정을 추가한 것이 아니라, 처음부터 있던 가정을 인터페이스로 끌어올린 것이다
MSA로 전환하면 이 가정이 깨진다. OrderRepository와 InventoryRepository가 서로 다른 서비스에 있으면 공유 DbContext 자체가 불가능하다.
이때 선택지는 Saga 패턴이나 2PC인데, 이 패턴들은 'Save를 호출하면 영속화된다'가 아니라 '의도를 발행하면 결과적으로 정합성이 맞춰진다'를 전제로 한다. 메서드 시그니처가 같아 보여도, 그 뒤에 깔린 일관성 모델이 다르다.
즉, 초기 Repository 인터페이스에 박혀 있던 '단일 트랜잭션 컨텍스트' 가정이 새 환경의 일관성 모델과 호환되지 않는 것이다. 인터페이스를 그대로 둔 채 구현만 바꾸는 것으로는 해결되지 않는다. 의미론 자체가 달라졌기 때문에, 호출 측의 코드 흐름까지 함께 바뀌어야 한다
Repository 경계를 어디서 나누느냐는 의도 레벨의 선택이다.
그런데 그 선택은 Transaction Boundary라는 런타임 제약과 충돌한다. 이 두 레이어의 intent는 처음부터 다른 최적화 기준을 가졌다. 설계 시점에서는 이 충돌이 보이지 않는다.
케이스 2: 이벤트 기반 설계와 동기 응답 요구
이벤트 기반 아키텍처의 의도는 결합도를 낮추는 것이다.
어차피 C#을 쓰는 사람들은 이벤트 기반으로 코딩하는데 익숙하기때문에 별로 어렵지 않게, 주문 생성 시 재고 차감, 알림 발송, 포인트 적립을 직접 호출하지 않고 이벤트로 분리한다.
public class OrderCreatedEvent
{
public int OrderId { get; init; }
public int ProductId { get; init; }
public int Quantity { get; init; }
public DateTime OccurredAt { get; init; }
}
public class OrderService
{
private readonly IEventBus eventBus;
public OrderId PlaceOrder(PlaceOrderCommand command)
{
var order = Order.Create(command);
// ... persist order
eventBus.Publish(new OrderCreatedEvent
{
OrderId = order.Id,
ProductId = command.ProductId,
Quantity = command.Quantity,
OccurredAt = DateTime.UtcNow
});
return order.Id;
}
}InventoryHandler, NotificationHandler, PointHandler가 각각 이벤트를 구독한다. 결합도가 낮고, 새 핸들러를 추가할 때 OrderService를 건드리지 않아도 된다. 의도가 명확하게 실현된 것처럼 보인다.
3개월 쯤 지나면 사장이 연락을 해오고, 요구사항이 들어온다.
"주문 완료 시 재고 차감 결과를 실시간으로 화면에 뿌려 주세요. 재고 부족이면 주문 자체를 실패 처리해야 합니다."
이 요구사항은 이벤트 기반 설계의 전제를 부순다. 이벤트는 fire-and-forget이다. Publisher는 subscriber의 처리 결과를 모른다. 재고 차감 결과를 응답에 포함시키려면 동기 호출이 필요하다.
선택지는 세 가지인데, 모두 비용이 있다.
선택 A: 재고 체크를 이벤트 발행 전에 동기 호출로 추가
public Result<OrderId, OrderError> PlaceOrder(PlaceOrderCommand command)
{
var stockResult = inventoryService.CheckAndReserve(command.ProductId, command.Quantity);
if (stockResult.IsFailure)
return Result.Failure(OrderError.OutOfStock);
var order = Order.Create(command);
eventBus.Publish(new OrderCreatedEvent { ... });
return Result.Success(order.Id);
}이벤트 기반과 직접 호출이 혼재된다. OrderService가 InventoryService를 직접 안다. 처음에 이벤트로 분리했던 결합도가 다시 생긴다.
선택 B: Request/Reply 패턴으로 이벤트 버스를 확장
var reservationResult = await eventBus.Request<ReserveStockCommand, StockReservationResult>(
new ReserveStockCommand { ProductId = command.ProductId, Quantity = command.Quantity }
);이벤트 버스가 동기 응답을 지원해야 한다. 인프라 복잡도가 올라간다. 이건 이미 이벤트 버스가 아니라 메시지 브로커에 가깝다. RabbitMQ의 RPC 패턴이나 MediatR의 Request/Response를 쓰는 구조인데, 처음 이벤트 기반으로 설계한 의도와 점점 멀어진다.
선택 C: 처음부터 다시 설계
이 선택지는 형식적으로 존재하지만 거의 채택되지 않는다. 이유는 명확하다. 이미 작성된 핸들러, 이미 발행되고 있는 이벤트, 이미 의존하는 다른 팀의 컨슈머가 재설계 비용을 비대칭적으로 비싸게 만든다. EventBus라는 추상화를 선택한 시점에는 이 비용이 존재하지 않았다. 그러나 6개월 동안 이 추상화 위에 코드가 쌓이는 동안, 재설계 비용은 계속 증가한다.
A는 작은 패치로 끝나고, B는 인프라 확장이고, C는 누적된 코드 전부의 재작성이다.
세 선택지의 비용은 동일한 차원에 있지 않다. 특히 나같은 프리랜서 개발자에게는 거의 불가능한 선택지다. 더 좋은 선택지가 C라는 걸 안다. 하지만 나는 시간이 돈이다.
그리고 이 비용 비대칭 자체가 초기 추상화 선택의 결과다. 현실에서는 대부분 선택 A로 간다. 그리고 서비스는 이벤트 기반과 직접 호출이 섞인 상태로 굳어진다.
이게 우연한 사고가 아니라 반복되는 패턴이라는 점이 중요하다. 같은 형태의 충돌은 다른 팀, 다른 도메인에서도 같은 방식으로 일어난다.
이유는 EventBus라는 추상화의 뿌리에 있다.

These paths of communication cannot be dynamically created and discovered at runtime; they need to be agreed upon at design time so that the application knows where its data is coming from and where the data is going to.
통신 경로는 런타임에 동적으로 만들어지거나 발견되는 것이 아니다. 어플리케이션이 자기 데이터가 어디서 오고 어디로 가는지를 알 수 있으려면, 설계 시점에 미리 합의되어 있어야 한다.
Hohpe & Woolf, Enterprise Integration Patterns (2003), p. 108, 4장 "Messaging Channels" 도입부
Hohpe와 Woolf의 Enterprise Integration Patterns(2003)는 메시지 채널을 두 축으로 분리한다.
한 축은 단일 수신자에게 가는 Point-to-Point Channel과 모든 수신자에게 복제되는 Publish-Subscribe Channel이고, 다른 축은 단방향 발행과 양방향 대화를 요구하는 Request-Reply의 구분이다. EventBus의 표준 구현은 Publish-Subscribe Channel + 단방향 발행, 흔히 fire-and-forget으로 부르는 조합 위에 만들어진다.
이 프리미티브 선택은 결합도 분리의 메커니즘 자체를 결정한다. fire-and-forget의 정의상 publisher는 subscriber 목록을 알 필요가 없다. 알 필요가 없으니 새 subscriber가 추가되어도 publisher 코드는 변경되지 않는다. 이것이 결합도 분리의 정확한 의미다. 단순히 'OrderService가 InventoryHandler를 직접 호출하지 않는다'가 아니라, 'OrderService가 InventoryHandler의 존재 자체를 모른다'.
이 익명성이 깨지는 순간 결합도는 다시 생긴다. publisher가 응답을 기다려야 한다면,
publisher는
(a) 응답을 누가 보내는지, 또는 누구에게서 응답이 오기를 기대하는지 알아야 하고,
(b) 응답 형식을 알아야 하고,
(c) 응답이 오지 않을 때의 타임아웃을 결정해야 한다.
이 셋 중 어느 것도 fire-and-forget에는 존재하지 않는다. 시그니처를 비슷하게 흉내 낼 수는 있어도(Request<T, R> 같은 메서드를 추가하는 식으로), 그 메서드의 실패 모드, 타임아웃 의미론, 멱등성 요구는 fire-and-forget의 그것과 다르다. 익명성이 무너지면, 그 익명성 위에 세워진 결합도 분리도 함께 무너진다.
즉, EventBus의 결합도 분리는 fire-and-forget이라는 프리미티브에 조건부로 성립한다. 이 조건은 인터페이스 시그니처 어디에도 명시되지 않은 채, EventBus라는 이름과 Publish라는 메서드 안에 묵시적으로 들어가 있다.
이 충돌은 '처음부터 내재해 있던 모순'이 아니다. 설계 시점에는 동기 응답 요구사항이 존재하지 않았으므로 충돌도 없었다. 그러나 EventBus라는 추상화를 선택한 순간 'publisher는 응답을 모른다'는 가정이 인터페이스에 못 박혔고, 이 못이 새 요구사항이 들어올 자리를 좁혔다.
두 케이스의 공통 구조는 다음과 같다.
설계 시점의 intent (결합도 분리 / 데이터 접근 분리)
-> 추상화 선택 (Repository / EventBus)
-> 추상화가 인코딩한 묵시적 가정 (단일 트랜잭션 컨텍스트 / fire-and-forget 프리미티브)
-> 새 요구사항이 다른 가정을 요구
-> 시그니처는 유지 가능, 의미론은 충돌
-> 패치 또는 전면 재설계이 과정은 설계가 나빠서 발생하지 않는다. 추상화가 맥락적이기 때문에 발생한다.
설계 시점의 맥락과 요구사항 추가 시점의 맥락이 다르고, 그 간극을 추상화가 흡수하지 못한다.
"좋은 추상화"가 존재한다면, 그것은 미래의 모든 맥락을 예측한 설계가 아니라, 맥락이 바뀔 때 마찰 비용을 감당할 수 있는 설계다.
그리고 그 기준은 설계 시점에서 정의할 수 없고, 모든 걸 해결 할 수 있는 프로그래머는 없다.
여기서 중요한 것은,
이 문제가 특정 방법론의 실패가 아니라는 점이다.
Repository도, EventBus도, DDD도,
모두 특정 시점의 intent를 구조화하려는 시도다.
문제는 그렇게 분리기준이 정착되는 순간,
미래의 다른 intent가 들어올 자리가 줄어든다는 데 있다.

(바이브 코딩이 최초로 쓰인 카파시의 게시글)
Karpathy는 이를 '프롬프트로 그때그때 코드를 굴리는' 즉흥적 워크플로우로 긍정적으로 묘사했다. 그러나 그 즉흥성이야말로 본 글이 짚는 문제와 정확히 맞물린다.
LLM은 현재 프롬프트에 드러난 intent를 기준으로
그럴듯한 추상화를 빠르게 생성하지만,
그 추상화가 이후의 요구사항 공간을 어떻게 제한하는지는 거의 고려하지 못한다.
이 문제는 단순히 “LLM이 과하게 많이 짠다”는 수준이 아니다.
더 정확히 말하면, LLM은 현재 주어진 intent를 기준으로
패턴을 국소적으로 최적화한다.
즉, 지금 보이는 요구사항,
지금 프롬프트에 드러난 제약,
지금 당장 그럴듯해 보이는 분리 기준을 중심으로
추상화를 빠르게 조립한다.
문제는 실제 시스템의 intent가 거기서 끝나지 않는다는 점이다.
현실의 intent는 항상 지연되어 도착한다.
오늘의 요구사항은 명시적으로 주어지지만,
내일의 요구사항은 아직 언어화되지 않은 채 잠복해 있다.
인간 개발자는 이 잠복한 요구사항의 존재를 완전히 예측하지는 못해도,
적어도 그것이 다시 들어올 것이라는 사실 자체는 경험적으로 안다.
그래서 아키텍쳐를 잡을 때
“지금 맞는가”만 보지 않고,
“나중에 덜 깨지겠는가”를 함께 본다.
반면 LLM은 대체로 현재 프롬프트에 드러난 intent를
언어적 표면으로 받아들인다.
그 결과,
현재 요구사항에는 잘 맞지만
미래 변경 비용은 큰 설계를 아주 높은 확률로 생성한다.
이것이 흔히 말하는 over-engineering의 한 형태다.
중요한 것은 여기서의 over-engineering이
단순히 클래스가 많고 파일이 많다는 뜻이 아니라는 점이다.
진짜 문제는,
현재 intent를 과도하게 구조화함으로써
아직 도착하지 않은 다른 intent가 들어올 경로를
오히려 좁혀 버린다는 데 있다.
즉, LLM은 추상화를 만든다.
하지만 그 추상화가 미래의 선택지를 얼마나 잠가 버리는지는
거의 감각하지 못한다.
이 점에서 LLM의 발산은 우연한 실수가 아니라 어쩌면 당연한 것일지도 모른다.
LLM은 실패 비용을 장기적으로 부담하지 않는다.
코드가 6개월 뒤, 진상 고객의 요구사항과 충돌할지,
어떤 경계가 transaction semantics와 부딪힐지,
어떤 인터페이스가 분산 환경에서 병목이 될지,
그 마찰은 생성 시점의 언어 표면에 충분히 드러나지 않는다.
결국 LLM은 현재 보이는 intent를 기준으로
가장 그럴듯한 아키텍쳐 설계 만든다.
그러나 내 개인적 경험에서 얻은 좋은 아키텍쳐란
현재 intent를 가장 잘 표현하는 아키텍쳐가 아니라,
미래의 다른 intent가 들어와도
마찰 비용을 감당할 수 있는 아키텍쳐다.
그리고 바로 이 차이가,
인간 개발자의 설계 경험과
LLM의 생성 능력 사이에 존재하는 본질적 간극이라는 생각을 한다.
이 때문에 LLM이 만들어내는 코드는
겉으로 보면 오히려 인간 개발자보다 더 “잘 설계된 것처럼” 보일 때가 많다.
인터페이스는 잘 분리되어 있고,
계층은 명확하며,
이름은 그럴듯하고,
패턴도 익숙하다.
문제는 그 경계 분리와 패턴이 맥락을 해결한 결과가 아니라,
학습 분포에서 자주 등장한 패턴 재조합한 결과라는 점이다.
학습 신호의 구조 자체가 이 실패 비용을 학습할 수 없게 되어 있다.
코드가 깃허브에 push될 때, 그 코드는 작성 시점의 모습이다. 18개월 뒤 transaction semantics와 충돌해서 retrofit된 흔적은 그 시점에는 존재하지 않는다. 학습 코퍼스에 들어가는 스냅샷에도 그 미래의 마찰은 포함될 수 없다. 다음 세대 모델의 학습 데이터로 들어갈 때도 마찬가지다. 들어가는 것은 리포지토리의 현재 상태지, 시간적 궤적이 아니다.
포스트모템과 장애 리포트는 학습 데이터에 분명히 존재한다. 그러나 원인이 된 원본 코드와 텍스트적으로 분리되어 있다. "Repository 패턴이 분산 환경에서 무너진 사례"라는 블로그 글은 있지만, 그 글 안에 18개월 전 누군가가 처음 깔끔하게 작성한 Repository 인터페이스 원본이 함께 들어 있지는 않다. 원본 패턴 -> 18개월 뒤의 실패라는 결합 신호는, 학습 데이터 안에서 한 단위로 묶여 있지 않다.
RLHF 단계도 같은 한계를 가진다. 평가자는 지금 화면에 떠 있는 코드의 미관을 평가한다. 18개월 뒤 어디가 깨질지는 평가 대상이 아니다. 결과적으로 모델이 받는 보상 신호 자체가 작성 시점의 미관에 편향되어 있다.
따라서 LLM이 마찰을 모른다는 것은 비유가 아니라, 학습 메커니즘의 직접적 귀결이다.
즉, 전반적인 패턴 선택의 결과물의 형태는 있어도
선택이 감당해야 할 마찰의 역사성은 비어 있다.
인간이 실패를 통해 배우는 것은 문법이 아니라 마찰이다.
어디서 경계를 나누면 나중에 transaction이 꼬이는지,
어떤 이벤트 분리가 결국 동기 호출로 되돌아오는지,
어떤 추상화가 처음에는 우아해 보여도 요구사항이 두 번만 바뀌면 독이 되는지, 이런 감각은 대부분 성공 사례가 아니라
실패 비용을 치르면서 생긴다. LLM은 바로 이 실패 비용의 기억을 갖지 못한다.
따라서 문제는 LLM이 패턴을 모르기 때문이 아니다.
오히려 패턴을 너무 잘 알기 때문에,
그 패턴이 작동하지 않는 맥락의 마찰을 지워 버린다.
이 점에서 방법론의 교조화와 LLM의 발산은 닮아 있다.
둘 다 살아 있는 맥락을 고정된 구조 환원하려는 충동을 가진다.

(Refactoring To Patterns)

패턴을 배우는 길에서 패턴에 심취하게 되는 것은 아마도 피할 수 없을 것입니다.
사실, 우리 대부분은 실수를 통해 배우니까요. 저 또한 여러 번 패턴에 심취했던 경험이 있습니다.
패턴의 진정한 즐거움은 그것들을 현명하게 사용하는 데서 비롯됩니다.
리팩토링은 중복 제거, 코드 단순화, 그리고 코드의 의도를 명확히 전달하는 데 집중하도록 도와줌으로써 우리가 그렇게 할 수 있도록 돕습니다.
패턴이 리팩토링을 통해 시스템 속으로 자연스럽게 녹아들 때, 패턴을 과도하게 사용하는 과잉 설계의 위험은 줄어듭니다.
리팩토링 실력이 향상될수록, 패턴의 진정한 즐거움을 발견할 가능성은 더욱 커질 것입니다
맥락의 고정
인간은 복잡한 것을 본질적으로 싫어한다. 그래서 '패턴'을 발견하고, 복잡계를 단순화해서 설명하려고 한다.
문제는 그러한 단순화 된 부분은 어디까지나 맥락과 상황에 의존적임에도 불구하고, 이것이 정답인냥 심취하는 경우가 있다. 패턴 프로그래밍 책등지에서도 저자가 패턴 프로그래밍에 심취했던 경험을 나열한 것을 보듯 말이다.
그런면에서 의도(Intent)의 사용 양태는 복잡한 것을 단순 한것으로 뭉개는데 쓰는 깨끗한 상위개념처럼 쓰인다.
TDD나 패턴 프로그래밍과 같이 그 방법론이 나왔던 시기와 맥락을 파악치 않고,
본질적 함의보다는 방법론의 교조화가 되서 반드시 지켜져야하는 일종의 종교적 교리처럼 사용되어진다.
개인적으로 TDD는 평균적으로는 매우 좋은 방법론이다. 하지만 잘못사용하면 최악의 방법론 중 하나가 될 수 있다 생각한다.
추상화가 잘못되었다면 테스트는 무슨 의미가 있을까?
아키텍처를 점진적으로 파괴하면서까지 통과 테스트만 쫓아가는 사례를 많이 봤다.
또 예를 들어 SOLID 원칙은 항상 준수할 필요는 없지만, 일부 조직에서는 거의 종교적 교리처럼 여겨진다.
UI 컴포넌트 디자인에서 LSP를 지나치게 엄격하게 적용하면 UI의 다양성과 유연성을 저해할 수 있다.
결국 의도(Intent)는 단일한 개념이 아니다.
사용자 레벨, 도메인 레벨, 시스템 레벨, 구현 레벨에서 각각 다른 제약과 최적화 기준을 가지는 여러 개의 intent가 공존하고, 그것들은 서로 충돌한다.
좋은 설계란 이 충돌을 없애는 것이 아니다. 충돌이 표면으로 드러나는 시점을 늦추거나, 드러났을 때 마찰 비용을 감당할 수 있는 분리 기준을 만드는 것이다.
그 감각은 패턴을 아는 것에서 오지 않는다. 패턴이 작동하지 않는 맥락을 겪어본 것에서 온다.
의도를 말하는 것은 쉽다. 의도가 레이어를 거칠 때마다 무엇을 잃는지를 아는 것이 어렵다
그리고 그것을 아는 것이 결국 나의 목표인 것 같다.