PUBLISHED
마약 조직으로 이해하는 책임 할당 패턴 GRASP
작성일: 2025.12.07

<갱단과의 전쟁>을 보다가 또 하나의 흥미로운 생각이 들었다. 마약 조직이라는 대상은 도덕적으로 정당화될 수 없지만, 구조적인 관점에서 보면 규모를 키우고 오래 버티기 위해 꽤 일관된 설계를 갖추고 있다는 점이다. 이들의 설계는 멋있어서가 아니라 생존을 위해 필연적으로 그렇게 된 것처럼 보인다. 조직이 커질수록 사람은 늘고, 역할은 분화되고, 실수와 배신 가능성은 커지며, 외부 환경은 끊임없이 변한다. 이런 조건에서 조직이 무너지지 않으려면 결국 구조가 받쳐줘야 한다.
객체지향 설계도 마찬가지다. 우리가 만드는 시스템이 커질수록 책임은 늘어나고, 협력하는 객체는 많아지고, 변경 요청은 반복된다. 이때 가장 중요한 질문은 화려한 아키텍처가 아니라 늘 같은 질문이다. “이 책임은 누구에게 줘야 할까?” 어떤 객체가 어떤 행동을 수행해야 하는지, 어떤 데이터를 알고 있어야 하는지, 다른 객체와 어떤 관계로 연결되어야 하는지를 결정하는 과정이 결국 유지보수성, 확장성, 테스트 용이성을 좌우한다. 설계에서 자주 실패하는 지점은 상속 계층이나 패턴 이름이 아니라, 책임을 잘못 놓아 변경이 엉뚱한 곳으로 전파되는 순간이다.
이 질문을 체계적으로 다루기 위한 사고 도구가 GRASP(General Responsibility Assignment Software Patterns)다. GRASP는 “정답”을 강요하지 않는다. 대신 책임을 할당할 때 흔히 필요한 판단 기준을 9가지 패턴 형태로 제공한다. 이 패턴들을 제대로 이해하면, 설계는 감이나 취향이 아니라 근거를 가진 결정이 된다. 이 글에서는 GRASP의 9가지 패턴을, 마약 조직이라는 극단적인 비유를 통해 “실제로 어떤 구조가 책임을 안정적으로 지탱하는가”라는 관점에서 풀어본다.
정보 전문가 (Information Expert): 정보를 가진 쪽이 책임을 가진다
Information Expert는 책임 할당의 가장 기본적인 출발점이다. 어떤 행동을 수행하기 위해 필요한 정보를 가장 잘 알고 있는 객체에게 그 책임을 부여하라는 원칙이다. 이는 단순히 “그 객체가 데이터를 가지고 있으니 그 객체가 처리해라”가 아니라, 책임과 데이터가 분리될 때 발생하는 비용을 줄이기 위한 판단이다. 정보가 없는 객체가 책임을 수행하려면, 어딘가에서 데이터를 끌어오거나 다른 객체에 계속 질문해야 한다. 이 과정에서 의존이 늘고 결합이 높아지며, 변경 시 영향 범위가 커진다.
조직 비유로 보면 매우 직관적이다. 거래 금액을 계산하거나 수익을 집계하는 일을, 현장을 모르는 상위 조직이 직접 맡으면 보고 체계가 과도하게 커진다. 판매 현장에서 가장 많은 거래 정보를 가진 곳이 집계 책임을 지는 편이 효율적이고 안전하다. 정보가 있는 곳에 책임이 붙으면, 그 책임은 자연스럽게 응집도를 얻는다.
class OrderItem {
constructor(private price: number, private quantity: number) {}
getTotal(): number {
return this.price * this.quantity;
}
}
class Order {
private items: OrderItem[] = [];
addItem(item: OrderItem): void {
this.items.push(item);
}
calculateTotal(): number {
return this.items.reduce((acc, item) => acc + item.getTotal(), 0);
}
}총액 계산은 Order가 맡는다. Order가 항목 목록을 알고 있기 때문이다. 여기서 중요한 포인트는 Order가 모든 계산을 독점하는 것이 아니라, 각 항목의 합산을 항목(OrderItem)의 getTotal과 협력해 수행한다는 점이다. 책임은 한 객체에 몰아주기보다, 정보가 가까운 객체들이 협력하는 형태로 설계될 때 가장 안정적이다.
생성자 책임 (Creator): 소유하거나 관리하는 쪽이 만든다
Creator 패턴은 “누가 객체를 생성해야 하는가”를 판단하기 위한 기준이다. 객체 생성은 흔히 단순한 new 호출처럼 보이지만, 실제로는 구조를 결정하는 중요한 행위다. 생성 책임이 이곳저곳 흩어지면 객체 그래프는 예측 불가능해지고, 생성 규칙을 바꾸는 일은 시스템 전반을 건드리는 일이 된다. Creator는 생성과 소유의 관계를 묶어, 생성 규칙을 한곳에 모아두게 한다.
조직에서도 비슷하다. 조직원을 뽑거나 새로운 팀을 만드는 책임은, 그 사람이나 팀을 실제로 관리할 조직이 가진다. 외부에서 무작위로 사람을 만들어 배치하는 구조는 관리 비용과 실패 비용이 폭증한다. 생성은 곧 책임의 시작이기 때문에, 관리 주체가 생성까지 함께 가져가는 편이 논리적이다.
class Order {
private items: OrderItem[] = [];
createItem(price: number, quantity: number): OrderItem {
const item = new OrderItem(price, quantity);
this.items.push(item);
return item;
}
}Order가 OrderItem을 만들고 내부 컬렉션에 추가한다. 이 구조의 장점은 외부가 OrderItem의 생성 규칙을 알 필요가 없다는 점이다. 추후 OrderItem 생성 시 검증이나 정책이 추가되어도 변경은 Order 내부에 머문다. Creator는 종종 Factory 패턴과 혼동되지만, GRASP의 Creator는 특정 패턴을 강요하기보다 “생성을 어느 책임 묶음에 둘 것인가”를 사고하게 만든다.
컨트롤러 (Controller): 흐름은 조정하되, 일을 대신하지 않는다
Controller 패턴은 시스템 이벤트(사용자 요청, 외부 입력 등)를 받아 적절한 도메인 객체에 위임하는 객체를 둔다는 원칙이다. 여기서 핵심은 “컨트롤러가 도메인 로직을 수행하면 안 된다”는 점이다. 컨트롤러는 흐름을 조율하는 역할이지, 실무를 처리하는 역할이 아니다. 컨트롤러가 비즈니스 규칙을 담기 시작하면, 입력 채널(UI, API, CLI)이 바뀔 때마다 도메인 로직이 같이 흔들리고, 테스트도 어려워진다.
조직에서도 외부 접점이 되는 연락책이나 중간 관리자는 실제 거래를 수행하지 않는다. 대신 요청을 분류하고, 적절한 하위 조직에 전달하며, 결과를 다시 취합한다. 이 역할이 없으면 말단 조직이 외부에 직접 노출되고, 조직 내부의 규칙이 외부 채널과 뒤섞인다.
class OrderService {
createOrder(items: [number, number][]): Order {
const order = new Order();
items.forEach(([p, q]) => order.createItem(p, q));
return order;
}
}
class OrderController {
constructor(private service: OrderService) {}
handleCreateOrder(items: [number, number][]): void {
const order = this.service.createOrder(items);
console.log("주문 완료:", order.calculateTotal());
}
}이 구조에서 컨트롤러는 입력을 받고 서비스 호출을 조정한다. 도메인 로직의 중심은 Order와 OrderService에 남는다. Controller 패턴을 잘 쓰면 “어디서 들어오는 요청이든 도메인은 동일하게 동작한다”는 설계가 가능해진다.
낮은 결합도 (Low Coupling): 서로를 몰라도 협력할 수 있어야 한다
Low Coupling은 변경의 파급 효과를 줄이기 위한 핵심 원칙이다. 결합도가 높으면 한 객체의 변경이 다른 객체에 연쇄적으로 영향을 주고, 결국 작은 수정이 큰 리스크가 된다. 조직이 오래 버티려면 각 부서가 서로의 내부 사정을 덜 알아야 한다. 내부를 알아야만 협력이 가능한 구조는, 곧 내부가 바뀔 때 협력도 같이 무너진다.
마약 조직 비유로 보면, 판매 조직이 운송 방식의 상세를 알고 있어야만 판매가 가능한 구조는 매우 취약하다. 운송 방식이 바뀔 때마다 판매 방식도 같이 수정되어야 하기 때문이다. 결합을 낮춘다는 것은 곧 “연결은 유지하되 의존은 줄이는 것”이다.
interface PaymentMethod {
pay(amount: number): void;
}
class CreditCardPayment implements PaymentMethod {
pay(amount: number): void {
console.log(`${amount}원을 신용카드로 결제`);
}
}
class OrderService {
constructor(private payment: PaymentMethod) {}
checkout(order: Order): void {
this.payment.pay(order.calculateTotal());
}
}OrderService는 결제 수단의 구체 구현을 알지 못한다. 이 구조는 테스트에서도 강력하다. 결제 모듈을 모킹할 수 있고, 결제 방식이 바뀌어도 주문 처리 로직은 유지된다. Low Coupling은 단지 “인터페이스를 쓰자”가 아니라, 책임을 배치할 때 의존의 방향과 범위를 최소화하자는 설계 판단이다.
높은 응집도 (High Cohesion): 한 객체는 하나의 역할에 집중한다
High Cohesion은 객체 내부의 책임들이 서로 관련성이 높아야 한다는 원칙이다. 응집도가 높으면 객체는 이해하기 쉽고, 변경이 있을 때 수정 지점이 예측 가능해진다. 반대로 응집도가 낮으면 “이 객체가 왜 이것까지 하지?”라는 의문이 쌓이고, 코드 베이스는 점점 해석하기 어려워진다.
조직에서 판매 조직이 판매뿐 아니라 운송 일정까지 관리하고, 생산량까지 조절하기 시작하면 결국 모든 일이 서로 얽히며 장애가 발생한다. 책임이 늘어날수록 조정 비용이 폭증하고, 실수는 시스템 전체로 번진다. 응집도는 단순히 “작게 나누자”가 아니라, “하나의 역할을 선명하게 만들자”에 가깝다.
class OrderService {
constructor(private payment: PaymentMethod) {}
processOrder(order: Order): void {
const total = order.calculateTotal();
this.payment.pay(total);
}
}이 서비스는 주문 처리라는 흐름에만 집중한다. 계산은 Order가, 결제는 PaymentMethod가 맡는다. 객체 간 협력은 존재하지만, 각자의 역할은 흐려지지 않는다.
다형성 (Polymorphism): 조건문 대신 역할을 교체한다
Polymorphism 패턴은 분기 로직을 늘리는 대신, 역할을 추상화하고 구현을 교체하는 방식으로 확장성을 얻는 방법이다. 조직 비유로 보면, 상황이 바뀔 때 규칙을 자꾸 덧대는 조직은 결국 운영 불가능해진다. 대신 “역할은 유지하되 담당 조직을 바꾸는 방식”이 더 안정적이다.
interface DiscountPolicy {
apply(amount: number): number;
}
class ChristmasDiscount implements DiscountPolicy {
apply(amount: number): number {
return amount * 0.9;
}
}
class NoDiscount implements DiscountPolicy {
apply(amount: number): number {
return amount;
}
}
class Checkout {
constructor(private discount: DiscountPolicy) {}
calculate(amount: number): number {
return this.discount.apply(amount);
}
}할인 정책이 늘어날 때 if 문이 늘어나지 않는다. 역할(DiscountPolicy)은 고정되고 구현만 늘어난다. 이 구조는 OCP와도 자연스럽게 이어지며, 변화가 한곳에 집중되는 대신 외곽으로 분산된다.
인공 객체 (Pure Fabrication): 설계를 위해 만들어진 역할
Pure Fabrication은 현실 도메인에 직접 대응되지 않는 객체를 의도적으로 만들어 설계 품질을 높이는 패턴이다. “현실에 없으니 만들면 안 된다”는 태도는 도메인 모델을 과도하게 비대하게 만들고, 결국 응집도를 깨뜨린다. 실무에서 필요한 것은 현실 그대로의 모델이 아니라, 변경에 강한 소프트웨어 구조다.
조직에서도 회계 담당, 연락책, 브로커 같은 역할은 현실의 ‘업무 단위’이기도 하지만, 동시에 조직을 유지하기 위한 ‘구조적 장치’다. 이런 역할이 없으면 도메인 핵심 역할이 부수 책임까지 떠안게 된다.
class OrderRepository {
save(order: Order): void {
console.log("DB에 주문 저장:", order.calculateTotal());
}
}Order가 직접 DB 저장을 하지 않도록 분리했다. 저장은 도메인의 핵심 책임이 아니며, 기술적 변화가 잦은 영역이기 때문이다. Pure Fabrication은 “가짜 객체”가 아니라 “설계 품질을 위해 만든 의도적 객체”라고 보는 편이 정확하다.
간접화 (Indirection): 직접 연결하지 않는다
Indirection은 두 객체가 직접 의존하지 않도록 중간 객체를 두어 결합을 낮추는 패턴이다. 직접 연결은 단순해 보이지만, 시간이 지나면 변경 전파 경로가 되며 시스템을 취약하게 만든다. 중간 계층은 비용처럼 보이지만, 규모가 커질수록 안전 장치가 된다.
interface MessageSender {
send(message: string): void;
}
class EmailSender implements MessageSender {
send(message: string): void {
console.log("이메일 전송:", message);
}
}
class NotificationService {
constructor(private sender: MessageSender) {}
sendNotification(msg: string): void {
this.sender.send(msg);
}
}알림을 보내는 쪽은 전송 방식의 상세를 모른다. 조직 비유로 보면, 상위 조직이 말단 조직에 직접 연락하지 않고 연락책을 통해 전달하는 구조에 가깝다. 간접화는 결합을 낮추는 동시에, 책임 경계를 더 선명하게 만든다.
변화 보호 (Protected Variations): 변하는 것을 격리한다
Protected Variations는 변할 가능성이 높은 요소를 캡슐화하고, 안정적인 인터페이스 뒤로 숨겨 변화의 영향을 최소화하는 패턴이다. 현실 조직도 외부 환경이 바뀔 때 조직 전체를 바꾸지 않는다. 변하는 부분을 경계 밖으로 밀어내고, 내부는 안정적으로 유지하려 한다.
interface PaymentMethod {
pay(amount: number): void;
}
class KakaoPay implements PaymentMethod {
pay(amount: number): void {
console.log(`${amount}원을 카카오페이로 결제`);
}
}
class OrderService {
constructor(private payment: PaymentMethod) {}
checkout(order: Order): void {
this.payment.pay(order.calculateTotal());
}
}결제 방식은 바뀔 수 있다. 하지만 OrderService는 바뀌지 않는다. 변화를 보호한다는 것은 “변화를 없애자”가 아니라, “변화가 전파되는 경로를 설계로 제어하자”는 의미다.
마무리: GRASP는 책임을 설계하는 기준이다
GRASP 패턴은 단순한 설계 팁 모음이 아니다. 책임을 배치하는 판단을 체계화한 도구다. 마약 조직이라는 극단적인 사례에서도 보이듯, 책임이 잘못 배분되면 조직은 빠르게 붕괴한다. 역할이 흐려지고, 내부 구현이 밖으로 새고, 변화가 전체로 전파되면 결국 구조는 버티지 못한다. 소프트웨어도 마찬가지다.
SOLID가 설계의 제약 조건이라면, GRASP는 설계의 판단 기준이다. 좋은 설계는 패턴 이름을 외우는 것보다, “이 책임은 누구의 것인가?”라는 질문을 끝까지 밀고 가는 데서 시작한다. 책임을 어디에 둘지, 협력은 어떻게 만들지, 변화는 어디에 격리할지에 대한 결정이 쌓여 시스템의 생존 가능성을 만든다. 결국 오래 살아남는 구조는, 책임이 올바르게 배치된 구조다.