PUBLISHED
마약 조직으로 이해하는 객체지향 설계 원칙 SOLID
작성일: 2025.12.07

SBS에서 제작한 다큐멘터리 <갱단과의 전쟁>을 보다가 흥미로운 생각이 들었다. 마약 조직이라는 대상이 도덕적으로는 비난받아 마땅하지만, 구조적인 관점에서 보면 놀라울 정도로 객체지향적인 특성을 가지고 있다는 점이다.
상위 조직은 세부 구현에 관여하지 않는다. 전체적인 전략과 역할만 정의하고, 실제 실행은 하위 조직에 위임한다. 보스는 누가 어떤 차량으로 어떻게 운반하는지에는 관심이 없고, 오직 “운반이 제대로 이루어졌는가”만을 문제 삼는다. 이는 고수준 모듈이 저수준 구현을 직접 알지 않아야 한다는 객체지향 설계의 핵심 원칙과 정확히 맞닿아 있다.
각 조직은 자신이 맡은 책임만 알고 움직인다. 판매 조직은 판매에만 집중하고, 운송 조직은 물류만 책임지며, 생산 조직은 생산 과정 외의 일에는 관여하지 않는다. 다른 조직의 내부 사정이나 구현 방식은 알 필요도 없고, 알려고 해서도 안 된다. 책임이 명확히 분리되어 있고, 내부 구현은 외부로 드러나지 않는다. 이는 캡슐화와 단일 책임 원칙이 자연스럽게 작동하고 있는 구조다.
문제가 발생했을 때의 대응 방식도 인상적이다. 특정 유통 라인이 무너지면 조직 전체를 재설계하는 대신, 같은 역할을 수행하는 다른 조직으로 교체한다. 외부에서 바라보는 계약은 그대로 유지한 채, 내부 구현만 갈아끼우는 방식이다. 인터페이스는 고정되어 있고 구현체만 교체되는 이 구조는 개방-폐쇄 원칙과 의존성 역전 원칙을 동시에 떠올리게 한다.
결국 오래 살아남는 조직은 느슨한 결합과 높은 응집도를 유지한다. 역할 간 의존은 최소화하고, 각 조직 내부는 자신이 맡은 책임에만 집중한다. 이 구조가 무너지기 시작하면 조직은 빠르게 내부 갈등과 붕괴로 이어진다. 이 모습은 규모가 커진 소프트웨어 시스템이 잘못된 설계로 인해 유지보수 불가능한 상태에 빠지는 과정과 크게 다르지 않다.
이 다큐멘터리를 보며, 객체지향 설계 원칙이 단순한 코드 스타일이나 이론이 아니라 규모가 커진 시스템이 살아남기 위한 구조적 조건이라는 생각이 들었다. 이런 문제의식에서 출발해, 마약 조직이라는 극단적인 예시를 통해 객체지향 설계 원칙인 SOLID를 다시 살펴보고자 한다.
도메인 명확히 하기
설계를 설명하기 전에 먼저 도메인을 명확히 해야 한다. 객체지향에서 가장 위험한 출발은 역할과 책임이 모호한 상태에서 클래스를 정의하는 것이다. 책임이 불분명하면 의존은 쉽게 얽히고, 변경은 예상치 못한 방향으로 전파된다.
이 글에서는 다음과 같은 조직 구조를 가정한다. 조직에는 전체 전략과 방향을 결정하는 보스가 있고, 현장을 관리하는 중간 관리자가 존재한다. 판매를 담당하는 딜러 조직, 물류를 담당하는 운송 조직, 제품을 생산하는 제조 조직은 서로 분리되어 있으며, 각 조직은 자신이 맡은 역할 외의 세부 사항을 알지 못한다.
단일 책임 원칙(SRP): 보스가 직접 마약을 팔기 시작하면 조직은 망한다
단일 책임 원칙은 “하나의 클래스는 하나의 책임만 가져야 한다”는 문장으로 자주 요약된다. 하지만 여기서 말하는 책임은 단순한 기능 단위가 아니라, 변경의 이유에 가깝다. 하나의 클래스는 하나의 이유로만 변경되어야 한다는 의미다.
조직을 예로 들면, 보스의 책임은 전략을 결정하는 것이다. 그런데 보스가 전략 수립뿐만 아니라 직접 제품을 만들고, 운송 경로를 관리하고, 길거리에서 판매까지 하기 시작한다면 어떤 일이 벌어질까. 전략이 바뀔 때마다 판매 방식이 흔들리고, 물류 문제가 생길 때마다 의사결정 구조가 영향을 받는다. 책임이 섞인 조직은 작은 변화에도 전체가 불안정해진다.
class DrugOrganization {
decideStrategy() {
console.log("조직의 전략을 결정한다");
}
sell() {
console.log("판매를 진행한다");
}
transport() {
console.log("물류를 담당한다");
}
}이 클래스는 조직 전체를 대표하는 것처럼 보이지만, 실제로는 서로 다른 책임을 하나로 묶은 전형적인 God Object다. 전략 변경, 판매 정책 변경, 물류 방식 변경이라는 서로 다른 이유로 이 클래스는 계속 수정될 수밖에 없다. 이를 역할 단위로 분리하면 구조는 훨씬 명확해진다.
class Boss {
decideStrategy() {
console.log("조직의 전략을 결정한다");
}
}
class Dealer {
sell() {
console.log("판매를 진행한다");
}
}
class Logistician {
transport() {
console.log("물류를 담당한다");
}
}이 구조에서는 각 클래스가 하나의 이유로만 변경된다. SRP는 코드의 가독성을 넘어서, 변경의 전파 범위를 통제하기 위한 설계 원칙이다.
개방-폐쇄 원칙(OCP): 새로운 유통 경로가 생겨도 조직 규칙은 바꾸지 않는다
개방-폐쇄 원칙은 소프트웨어 엔티티는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 원칙이다. 변화가 불가피한 시스템일수록 이 원칙의 중요성은 더 커진다.
기존에는 길거리 판매만 하던 조직이 온라인 판매를 시작해야 하는 상황을 떠올려보자. 이때 기존 판매 로직 내부에 조건문을 추가하는 방식은 단기적으로는 간단해 보이지만, 장기적으로는 규칙을 훼손한다.
class Dealer {
sell(type: "street" | "online") {
if (type === "street") {
console.log("길거리에서 판매");
}
if (type === "online") {
console.log("온라인으로 판매");
}
}
}판매 방식이 늘어날수록 이 메서드는 계속 수정된다. 이는 기존 코드가 변경에 열려 있다는 뜻이며, 새로운 기능이 기존 기능에 영향을 미칠 가능성도 함께 커진다.
판매라는 개념을 추상화하면 접근 방식이 달라진다.
interface SalesChannel {
sell(): void;
}
class StreetDealer implements SalesChannel {
sell() {
console.log("길거리에서 판매");
}
}
class OnlineDealer implements SalesChannel {
sell() {
console.log("온라인으로 판매");
}
}
class SalesManager {
constructor(private channel: SalesChannel) {}
executeSale() {
this.channel.sell();
}
}이 구조에서는 새로운 판매 채널이 추가되더라도 기존 코드는 수정되지 않는다. 단지 새로운 구현체를 추가할 뿐이다. OCP는 다형성을 통해 변화를 기존 코드 바깥으로 밀어내는 설계 전략이다.
리스코프 치환 원칙(LSP): 간부라고 다 같은 간부는 아니다
리스코프 치환 원칙은 상속 관계가 성립하기 위한 조건을 정의한다. 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 하며, 부모 클래스가 약속한 행동을 깨뜨려서는 안 된다.
조직에서 “간부”라는 역할은 지시를 받고 이를 수행할 수 있어야 한다는 암묵적인 계약을 가진다. 그런데 어떤 간부는 특정 지시를 받으면 예외를 던지고, 어떤 간부는 명령을 거부한다면, 그들은 더 이상 같은 역할이라고 볼 수 없다.
class Officer {
giveOrder() {
console.log("지시를 내린다");
}
}
class ReliableOfficer extends Officer {
giveOrder() {
console.log("지시를 정확히 수행한다");
}
}
class UnreliableOfficer extends Officer {
giveOrder() {
throw new Error("이 지시는 수행할 수 없다");
}
}UnreliableOfficer는 타입 시스템상으로는 Officer이지만, 행동 측면에서는 부모가 보장한 계약을 위반한다. 이 객체를 Officer로 취급하는 순간 시스템은 예외에 취약해진다. LSP는 상속을 제한하는 원칙이 아니라, 의미적으로 대체 가능한 경우에만 상속을 허용하라는 경고다.
인터페이스 분리 원칙(ISP): 조직원에게 불필요한 규칙을 강요하지 마라
인터페이스 분리 원칙은 하나의 큰 인터페이스보다, 작고 목적이 명확한 인터페이스 여러 개가 낫다는 원칙이다. 이는 클래스가 자신이 사용하지 않는 기능에 의존하지 않도록 하기 위함이다.
운송 담당자에게 판매 기술이나 가격 협상 규칙까지 모두 숙지하라고 요구한다면, 이는 불필요할 뿐 아니라 위험하다. 역할 경계가 흐려지고, 책임이 섞이기 때문이다.
interface OrganizationMember {
sell(): void;
transport(): void;
negotiate(): void;
}이 구조에서는 모든 조직원이 필요하지 않은 메서드까지 구현해야 한다. 이를 역할 단위로 나누면 구조는 훨씬 명확해진다.
interface Seller {
sell(): void;
}
interface Transporter {
transport(): void;
}
class Dealer implements Seller {
sell() {
console.log("판매를 진행한다");
}
}
class Logistician implements Transporter {
transport() {
console.log("물류를 담당한다");
}
}ISP는 클래스 설계의 문제이자, 의존성을 어디까지 허용할 것인가에 대한 설계 선택이다.
의존성 역전 원칙(DIP): 보스는 사람에게 명령하지 않고 역할에 명령한다
의존성 역전 원칙은 고수준 모듈이 저수준 구현에 의존하지 말고, 추상화에 의존해야 한다는 원칙이다. 이는 시스템의 중심부가 세부 구현에 끌려다니지 않도록 하기 위한 장치다.
조직에서 보스가 특정 개인에게 직접 명령을 내리는 구조는 취약하다. 그 사람이 사라지는 순간 조직은 멈춘다. 대신 보스는 “운송 책임자”라는 역할에 명령한다.
interface TransportService {
move(): void;
}
class TruckTransport implements TransportService {
move() {
console.log("트럭으로 운송");
}
}
class BikeTransport implements TransportService {
move() {
console.log("오토바이로 운송");
}
}
class Boss {
constructor(private transport: TransportService) {}
command() {
this.transport.move();
}
}보스는 운송 방식이 무엇인지 알 필요가 없다. 오직 “운송한다”는 계약만 알면 된다. DIP는 결합도를 낮추는 원칙이 아니라, 의사결정이 의존하는 방향을 통제하는 원칙이다.
마무리: 좋은 설계는 도덕이 아니라 생존의 문제다
이 글에서 살펴본 마약 조직의 구조는 도덕적으로 정당화될 수는 없지만, 설계라는 관점에서는 분명한 교훈을 준다. 객체지향 설계 원칙은 코드를 깔끔하게 보이게 만들기 위한 규칙이 아니다. 사람이 늘고, 역할이 늘고, 변경이 반복되는 시스템이 무너지지 않고 버티기 위해 필요한 구조적 조건이다.
SOLID 원칙은 외워서 적용하는 체크리스트가 아니라, 시스템을 바라보는 사고방식에 가깝다. 누가 무엇을 책임지는지, 그 책임이 어디까지 노출되는지, 그리고 변화가 어디까지 전파되어야 하는지를 끊임없이 고민하게 만든다. 결국 좋은 설계란, 오래 살아남는 구조를 만드는 일이다.