학부 시절이나 취업 준비를 할 때 우리는 객체 지향 프로그래밍(OOP)을 “현실 세계를 모델링하는 기술”, “부모 클래스를 물려받는 상속(Inheritance)” 정도로 배웠습니다.
하지만 실무에서 코드를 수만 줄씩 쌓아가다 보면, 부모 클래스 하나 잘못 건드렸다가 자식 클래스 수십 개가 동시에 깨져나가는 참사를 겪게 됩니다. 그제서야 왜 대선배 개발자들이 “상속보다는 합성(Composition)을 써라”, **“인터페이스에 의존해라”**라고 신신당부했는지 뼈저리게 깨닫게 되죠.
이번 글에서는 교과서적인 설명은 걷어내고, 유지보수하기 좋은 코드를 작성하기 위해 실무에서 꼭 기억해야 할 핵심 원칙과 자주 쓰는 실전 디자인 패턴을 정리해 보겠습니다.
1. 실무 관점으로 다시 보는 SOLID 5대 원칙
이름만 외우는 SOLID는 아무 도움이 안 됩니다. “이 원칙을 어기면 실무에서 어떤 피를 보는지”를 알아야 합니다.
1) SRP (단일 책임 원칙)
- 한 줄 요약: 하나의 클래스나 모듈은 단 하나의 변경 이유만 가져야 합니다.
- 실전 예시:
UserService클래스 안에 회원 가입 로직, 이메일 템플릿 생성, 결제 카드 검증, PDF 영수증 발급 코드가 한데 섞여 있다면 명백한 위반입니다. 이메일 문구 바꾸려고UserService를 건드리다가 회원 가입 버그를 내는 일이 벌어지기 때문이죠.
2) OCP (개방-폐쇄 원칙)
- 한 줄 요약: 기존 코드를 수정하지 않고도 새로운 기능을 쉽게 확장할 수 있어야 합니다.
- 실전 팁: 새로운 결제 수단(카카오페이, 토스페이, 애플페이)이 추가될 때마다
if-else나switch문을 찾아다니며 코드를 뜯어고치고 있다면 OCP가 깨진 상태입니다. 공통 결제 인터페이스를 정의하고 각 수단을 별도 클래스로 꽂아 넣는 구조로 바꿔야 합니다.
3) LSP (리스코프 치환 원칙)
- 한 줄 요약: 자식 클래스는 부모 클래스의 계약(규약)을 배신하지 않고 완전히 대체할 수 있어야 합니다.
- 실전 함정:
Rectangle(직사각형)을 상속받아Square(정사각형)를 만들었는데, 가로 길이를 바꿨더니 세로 길이까지 멋대로 바뀌어 부모의 전제 조건을 깨뜨리는 식의 설계는 피해야 합니다.
4) ISP (인터페이스 분리 원칙)
- 한 줄 요약: 자신이 쓰지도 않는 메서드에 의존하도록 강제당하지 않아야 합니다.
- 실전 팁: 10개짜리 거대한 인터페이스 하나보다는, 2~3개씩 역할별로 쪼갠 작은 인터페이스 여러 개를 만드는 편이 훨씬 깔끔합니다.
5) DIP (의존성 역전 원칙)
- 한 줄 요약: 구체적인 구현 클래스에 기대지 말고, 추상화된 인터페이스에 기대세요.
- 실전 예시: 비즈니스 로직이
MySqlDatabase라는 구체 클래스를 직접new해서 쓰지 않고,DatabaseRepository라는 인터페이스를 주입(DI)받아 쓰는 구조입니다. 그래야 테스트할 때 가짜(Mock) DB로 손쉽게 갈아 끼울 수 있습니다.
2. 왜 상속보다 합성(Composition)일까?
객체 지향의 꽃이 상속이라고 배웠지만, 실무에서는 상속이 오히려 코드 결합도를 극단적으로 높이는 독이 되곤 합니다.
[상속 (Inheritance) - 강한 결합: 부모 변경 시 자식이 전부 영향받음]
CustomList ──상속(is-a)──> ArrayList
[합성 (Composition) - 느슨한 결합: 필요한 부품을 레고처럼 갈아 끼움]
OrderService ──소유(has-a)──> PaymentProcessor (Interface)
- 깨지기 쉬운 기반 클래스 문제: 부모 클래스의 내부 구현이 아주 조금만 바뀌어도 이를 상속받은 자식들의 동작이 예측 불가능하게 망가질 수 있습니다.
- 합성의 우아함: 클래스 내부의 필드로 인터페이스 인스턴스를 소유(has-a)하게 만들면, 런타임에 언제든 부품을 레고처럼 갈아 끼울 수 있고 단위 테스트도 믿을 수 없을 만큼 쉬워집니다.
3. 실무에서 가장 자주 써먹는 3대 디자인 패턴
💡 1. 전략 패턴 (Strategy Pattern)
알고리즘군을 정의하고 각각을 캡슐화하여 교체 가능하게 만듭니다.
// 결제 전략 인터페이스
interface PaymentStrategy {
pay(amount: number): Promise<void>;
}
class KakaoPayStrategy implements PaymentStrategy {
async pay(amount: number) { console.log(`카카오페이로 ${amount}원 결제`); }
}
class TossPayStrategy implements PaymentStrategy {
async pay(amount: number) { console.log(`토스페이로 ${amount}원 결제`); }
}
// 결제 프로세서는 전략만 갈아 끼우면 새로운 결제 수단이 추가되어도 코드 수정이 없습니다 (OCP 준수!)
class PaymentService {
constructor(private strategy: PaymentStrategy) {}
async checkout(amount: number) {
await this.strategy.pay(amount);
}
}
💡 2. 팩토리 패턴 (Factory Pattern)
어떤 객체를 생성할지 결정하는 책임을 한곳으로 몰아주는 패턴입니다. 객체 생성 로직이 복잡하거나 조건에 따라 다른 객체를 만들어야 할 때 클라이언트 코드를 깔끔하게 지켜줍니다.
💡 3. 옵저버 패턴 (Observer / Pub-Sub)
한 객체의 상태가 바뀌었을 때 이를 구독하고 있는 다른 객체들에게 자동으로 알림을 날려주는 패턴입니다. 회원 가입 완료 후 “환영 이메일 발송”, “가입 축하 쿠폰 지급”, “슬랙 알림” 같은 후속 작업들을 메인 가입 로직과 완벽히 분리할 수 있습니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- Redis, RabbitMQ, Kafka 운영할 때 꼭 겪게 되는 병목과 트러블슈팅 미들웨어가 뻗으면 백엔드 전체가 마비됩니다. Redis 빅키(BigKey)와 메모리 방출 정책, RabbitMQ 데드레터(DLX)와 쿼럼 큐, Kafka 컨슈머 랙과 리밸런싱 지옥 탈출법을 정리했습니다.
- Vercel 배포하다가 빌드 터졌을 때: OOM부터 504 타임아웃까지 실전 해결법 로컬에선 잘 되던 프로젝트가 Vercel에 올리기만 하면 터지는 이유와 해결책. 빌드 메모리 초과(OOM), 504 게이트웨이 타임아웃, 환경 변수 누락, 빠른 롤백 팁을 정리했습니다.
- 실무에서 써본 AI 에이전트 & RAG 아키텍처: 삽질 줄이는 핵심 설계 팁 단순 챗봇을 넘어 스스로 생각하고 행동하는 AI 에이전트 만들기. LLM 파라미터(Temperature/Top-P) 튜닝, 프롬프트 캐싱, RAG vs 파인튜닝 선택 기준, LangGraph 상태 관리까지 실전 정리.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.