정의
1988년 바바라 리스코프는 다음과 같은 치환 원칙을 제시했다.
S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다.
쉽게 말하면, 부모 클래스의 객체가 사용되는 자리에 자식 클래스의 객체를 넣거나, 인터페이스를 구현한 객체가 사용되거나, REST API에서 계약된 포맷을 따르는 구현체가 사용되거나 하는 등, 특정 계약이나 역할을 정의하는 상위 존재를 따라 구현한 경우에는 그 치환이 시스템 동작에 영향을 주지 않아야 한다는 것이다.
원칙을 지키지 않은 구조: 알림 전송기 예제
아래는 알림을 전송하는 Notifier 클래스와 이를 확장한 EmailNotifier, SlackNotifier의 구조이다. 그러나 SlackNotifier는 상위 타입이 기대하는 계약을 깨뜨리고 있다.
abstract class Notifier {
abstract send(message: string): void;
}
class EmailNotifier extends Notifier {
send(message: string): void {
console.log(`Email: ${message}`);
}
}
class SlackNotifier extends Notifier {
send(message: string): void {
if (message.length > 100) {
throw new Error("Slack message too long");
}
console.log(`Slack: ${message}`);
}
}
이때 클라이언트는 Notifier를 사용한다고 가정하고 아래처럼 작성할 수 있다.
function notifyUser(notifier: Notifier) {
notifier.send("긴 공지사항 내용입니다. 내용이 매우 길고, 여러 줄로 구성되어 있습니다.");
}
이 코드는 EmailNotifier에서는 정상 동작하지만, SlackNotifier에서는 예외가 발생한다.
만약 이 구조에서 LSP를 지키고자 했다면, 다음과 같이 수정할 수 있다:
class SlackNotifier extends Notifier {
send(message: string): void {
const safeMessage = message.slice(0, 100); // 길이를 자르거나
console.log(`Slack: ${safeMessage}`); // 내부적으로 처리
}
}
이처럼 하위 클래스가 제한사항을 강제하기보다, 문제를 내부에서 해결하도록 구현해야 상위 타입이 기대하는 동작(모든 메시지를 안전하게 전송 가능함)을 그대로 유지할 수 있다.
그럴 때 비로소 하위 타입은 상위 타입을 대체할 수 있으며, 리스코프 치환 원칙을 지켰다고 할 수 있다.
즉, 하위 타입인 SlackNotifier가 상위 타입의 기대(모든 메시지를 전송 가능하다는 계약)를 어겼기 때문에, 리스코프 치환 원칙을 위반한 것이다.
아키텍처와의 연결
LSP는 단순히 클래스 상속 수준의 문제가 아니라, 인터페이스나 API 등의 외부와의 계약 전체에 영향을 준다.
예를 들어, 여러 결제 서비스가 동일한 결제 요청 API 포맷을 따르기로 했다고 가정하자.
기본 포맷은 다음과 같다:
POST /pay
{
"amount": 10000,
"currency": "KRW",
"method": "card"
}
여기서 method는 결제 방식을 나타내는 필수 필드이다.
그런데 어떤 하위 시스템(예: LegacyPay)은 이 필드를 payment_type이라는 이름으로 바꾸어 사용한다고 하자.
if (request.vendor === "LegacyPay") {
body.payment_type = body.method;
delete body.method;
}
이처럼 하위 시스템이 상위 계약을 따르지 않으면, 상위 시스템은 각 하위 구현체를 인식하고 조건 분기를 해야 한다.
이는 객체지향에서 if (x instanceof Y) 와 같은 조건 분기와 동일한 문제를 일으키며, 시스템을 취약하게 만든다.
결과적으로 REST API와 같은 설계에서도 '하위 구현이 상위 규약을 따를 것'이라는 신뢰가 무너지면, 분기 처리, 조건문, 예외처리 코드가 늘어나며 유지보수가 어려워진다.
결론
LSP는 단순한 객체지향 원칙을 넘어서 아키텍처 수준까지 영향을 미치는 원칙이다.
치환 가능한 하위 타입을 만들지 않으면, 시스템은 해당 타입에 따라 분기 로직과 예외 처리가 곳곳에 생기게 되고, 결과적으로 유지보수가 어려운 구조가 된다.
※ 본 글은 『Clean Architecture』(로버트 C. 마틴 저) 3부의 9장을 기반으로 학습 목적으로 요약한 글입니다.
※ 이 글은 책의 내용을 요약한 것으로, 원문 없이 읽을 경우 오해의 여지가 있을 수 있습니다. 정확한 이해를 위해 원서의 정독을 권장합니다.
'아키텍처' 카테고리의 다른 글
Clean Architecture 정리 - 3부 11장 의존성 역전 원칙 (1) | 2025.06.15 |
---|---|
Clean Architecture 정리 - 3부 10장 인터페이스 분리 법칙 (0) | 2025.06.14 |
Clean Architecture 정리 - 3부 8장 개방-폐쇄 원칙 (3) | 2025.06.12 |
Clean Architecture 정리 - 3부 7장 단일 책임 원칙 (0) | 2025.06.10 |
Clean Architecture 정리 - 2부 6장 함수형 프로그래밍 (1) | 2025.06.09 |