1. 추상화는 알지만 치환 가능성은 모른다.
객체지향 프로그래밍(OOP, Object Oriented Programming)에서 상속과 인터페이스는 더 이상 낯선 개념이 아니다. 많은 개발자들이 재사용성과 확장성을 위해 추상화하고, 새로운 기능을 위해 하위 클래스나 구현 클래스로 분리해 나간다. 문제는 여기서부터 발생한다.
분명히 상위 타입에 맞게 코드를 작성했고, 컴파일도 문제없이 완료되었는데 런타임 시 기존 코드가 깨진다.
클래스를 새로 하나 추가했을 뿐인데 테스트가 실패하거나 의외의 장애가 발생하는 상황은 생각보다 흔하다. 이는 단순한 실수가 아니라, 추상화를 사용하는 방식 자체에 근본적인 결함이 있었을 가능성을 의미한다.
많은 개발자들은 '상속'을 문법적으로 잘 이해하지만, 상속이 발생시키는 추상 타입과 구현 타입 간의 '계약'까지 고려하는 경우는 드물다. 컴파일러는 타입 계층이 올바른지만 확인할 뿐, 그 타입이 실제로 기대한 방식으로 동작하는지는 보장하지 않는다.
즉, 타입이 맞는다고 해서 곧바로 치환 가능하다고 볼 수는 없다.
이처럼 타입의 모양이 아닌, 행동의 신뢰성까지 보장하려는 원칙이 바로 리스코프 치환 원칙(LSP, Liskov Substitution Principle)이다.
2. LSP란 무엇인가? 정의 및 조건
리스코프 치환 원칙(LSP, Liskov Substitution Principle)은 다음과 같이 정의된다.
"프로그램의 각 부분에서 상위 타입의 인스턴스를 사용하는 모든 경우에, 하위 타입의 인스턴스로 대체하더라도 프로그램의 동작이 바뀌지 않아야 한다."
해당 정의는 단순한 문법적 상속이나 타입 호환성을 넘어, 행동적인 호환성까지 보장되어야 한다는 의미이다. 즉, 어떤 코드가 상위 타입을 기준으로 작성되어 있다면, 해당 코드를 변경하지 않고 하위 타입의 인스턴스를 넣었을 때도 기대하는 방식으로 동일하게 동작해야 한다.
만약 이러한 행동적 호환성이 지켜지지 않으면, 컴파일 시에서는 문제가 없어 보이더라도 런타임에서 예기치 않은 오류나 오동작이 발생할 수 있다. 이는 타입 체계로는 검출할 수 없는 설계적 결함이라 말할 수 있다.
2-1. LSP가 요구하는 두 가지 조건
LSP에서 말하는 치환 가능성은 다음 두 가지 조건을 모두 충족할 때만 진정한 의미의 치환이 가능하다.
- 1️⃣ 기능적 계약
기능적 계약이란, 특정 메서드가 무엇을 입력받고, 무엇을 반환하며, 어떤 예외를 발생시킬 수 있는지에 대한 약속이다. 하위 타입은 상위 타입이 정의한 이 계약을 그대로 지키거나, 더 완화된 형태로만 변경할 수 있다. 이 계약을 어기면 치환은 불가능하며, 이는 곧 LSP 위반이다.
기능적 계약은 다음 네 가지 측면을 모두 충족해야 한다.
* 사전 조건 (Preconditions)
- 하위 타입은 상위 타입보다 더 까다로운 입력 조건을 요구해서는 안된다.
- 예) 상위는 "모든 정수 허용", 하위는 "양수만 허용"이라면 계약 위반
* 사후 조건 (Postconditions)
- 하위 타입은 상위 타입이 보장한 결과를 그대로 제공하거나, 더 강화된 보장을 제공해야 한다.
- 예) 상위는 "항상 양수를 반환"한다고 명시했다면, 하위가 음수를 반환하면 계약 위반
* 불변 조건 (Invariants)
- 상위 타입에서 보장하던 내부 상태의 일관성은 하위 타입에서도 그대로 유지되어야 한다.
- 예) 상위는 "List는 절대 null이 아님"을 보장, 하위가 이를 null로 만든다면 계약 위반
* 예외 처리 조건 (Exception Behavior)
- 상위 타입이 예외를 발생시키지 않던 상황에서, 하위 타입이 새롭게 예외를 발생시키면 계약 위반
- 예외 처리도 계약의 일환으로 간주되며, 그 동작은 예측 가능해야 한다.
- 2️⃣ 동작 의미
기능이 언제, 왜, 어떻게 사용되는지에 대한 의미적 맥락까지 일관되어야 한다. 외형상 동일한 메서드 이름, 매개변수 타입 및 순서를 갖고 있더라도, 실제로 동작 방식이나 결과가 기대와 다르다면 이는 LSP를 위반한 것이다.
예를 들어, 작성한 사각형 클래스의 setWidth( )는 너비만 설정하는 동작으로 이해되지만, 이를 상속한 정사각형 클래스에서 setWidth( )가 너비와 높이를 동시에 변경한다면 사용자는 예상치 못한 동작을 경험하게 된다. 이러한 차이는 코드의 안정성을 해칠 수 있으며, 치환 가능성을 깨뜨리는 요인이 된다.
정리하자면, LSP가 말하는 치환 가능성은 다음 질문으로 확인할 수 있다. 만약 아래 질문에 "네"라고 대답할 수 없다면, 아무리 타입 계층상 호환되더라도 그 설계는 LSP를 위반한 것이다.
하위 타입이 상위 타입과 동일한 기능적 계약을 이행하며,
그 기능이 사용되는 의미와 맥락까지 동일하게 유지되는가?
2-2. LSP의 이해를 돕기 위한 현실 예시
LSP를 보다 직관적으로 이해하기 위해 현실에서 접할 수 있는 비유를 하나 들어본다. 한 회사의 재무팀에는 일반적으로 다음과 같은 역할이 존재한다.
- 자금 관리
- 재무 계획 수립
- 투자 판단 지원
이제 이 재무팀에 새로운 신입 사원이 배치되었다고 가정하자. 신입 사원은 재무 관련 자격증은 갖췄지만, 실무 경험이 부족하여 위 세 가지 업무를 아직 독립적으로는 수행할 수 없다. 즉, 신입 사원은 형식적으로 재무팀 소속이지만, 실제로는 해당 팀의 기존 인력을 대체할 수 없는 상태인 것이다.
이와 같은 상황은 객체지향 설계에서도 동일하게 발생할 수 있다. 어떤 하위 타입이 상위 타입을 상속하거나 인터페이스를 구현했더라도, 그 타입이 상위 타입이 보장하던 기능적 계약과 동작 의미를 따르지 못한다면, 해당 하위 타입은 치환 가능한 객체가 아니다.
조금 더 구체적으로 신입 사원이 LSP를 위반한 것으로 간주되는 이유는 다음 두 가지 측면에서 설명할 수 있다.
- ❌ 기능적 계약 위반
재무팀 구성원이라면 자금 관리, 재무 계획, 투자 판단 등의 역할을 수행할 수 있다는 것이 조직 내에서 암묵적으로 전제되는 계약이다. 그러나 신입 사원이 어떤 이유로든 해당 기능들을 실제로 수행하지 못한다면, 이는 계약을 충족하지 못한 상태이다. 특히 업무를 아예 수행하지 못하거나 실질적으로 예외 상황을 유발하는 경우라면, 이는 사후 조건이나 예외 처리 조건을 위반한 것과 같다.
- ❌ 동작 의미 위반
외형적으로는 신입 사원이 '재무팀 구성원'이라는 역할을 갖고 있지만, 해당 역할이 사용자 또는 조직이 기대한 방식으로 수행되지 않는다면, 이는 동작 의미 측면에서 일관성을 잃은 것이다. 즉, 직함은 같지만, 실제로는 같은 기능을 기대할 수 없기 때문에 치환 가능성이 성립하지 않는다.
3. LSP 위반 예제와 코드 리팩토링
앞서 살펴본 바와 같이, LSP의 핵심은 상위 타입이 정의한 '기능적 계약과 동작 의미'를 하위 타입이 일관되게 이행할 수 있는가에 있다. 이번에는 이 개념을 코드로 확장하여 LSP를 위반한 설계와 이를 어떻게 리팩터링 했는지를 단계적으로 살펴본다. 준비된 예제는 개념 전달을 위해 단순화된 구조이며, 세부적인 요소는 생략되었음을 참고해 주길 바란다.
3-1. 초보 개발자의 잘못된 상속 구조
초보 개발자는 재무팀 구성원이라면 누구나 자금 관리, 재무 분석, 투자 판단 업무를 수행할 수 있다고 가정하였다. 이 전제에 따라, 모든 재무 팀원이 상속받을 수 있도록 다음과 같은 상위 타입을 정의한다.
class FinanceTeamMember {
void manageFunds() {
System.out.println("[재무팀] - 자금 관리 업무를 수행한다.");
}
void analyzeFinancials() {
System.out.println("[재무팀] - 재무 계획을 수립한다.");
}
void supportInvestmentDecisions() {
System.out.println("[재무팀] - 투자 판단 지원 업무를 수행한다.");
}
}
재무팀의 숙련된 구성원은 아래와 같이 FinanceTeamMember을 상속받아 하위 타입으로 설계하여 기능 수행에 아무런 문제가 발생하지 않았다.
class ExperiencedAccountant extends FinanceTeamMember {
// 기본 구현 그대로 사용
}
하지만 실무 경험이 부족한 신입 사원을 이와 동일한 구조로 상속시키는 순간 문제가 발생한다. 신입 사원은 실제로는 자금 관리나 투자 판단을 수행할 수 없음에도 불구하고, 상속을 통해 해당 기능을 포함하게 된다.
초보 개발자는 이 문제를 해결하기 위해 해당 기능들을 오버라이드 한 뒤 예외를 던지는 방식으로 회피했다.
class NewAccountant extends FinanceTeamMember {
@Override
void manageFunds() {
throw new UnsupportedOperationException("[재무팀] - 신입: 자금 관리 불가");
}
@Override
void analyzeFinancials() {
throw new UnsupportedOperationException("[재무팀] - 신입: 재무 분석 불가");
}
@Override
void supportInvestmentDecisions() {
throw new UnsupportedOperationException("[재무팀] - 신입: 투자 판단 불가");
}
}
이 설계를 바탕으로 FinanceTeamMember 타입을 사용하는 측의 코드는 다음과 같은 형태가 될 수 있다.
void assignTasks(FinanceTeamMember member) {
member.manageFunds();
member.analyzeFinancials();
member.supportInvestmentDecisions();
}
이제 assignTasks( new NewAccountant( ) )와 같이 신입 사원을 인자로 넘기게 되면, 모든 메서드가 예외를 발생시키며 실패한다. 컴파일 시점에는 전혀 문제가 없지만, 런타임에 예기치 못한 동작이 발생하게 된다.
이와 같이 초보 개발자의 설계 사례는 LSP가 요구하는 두 가지 조건 모두 위반하고 있다.
- ❌ 기능적 계약 위반
상위 타입 FinanceTeamMember는 모든 구성원이 세 가지 핵심 업무를 수행할 수 있다는 계약을 내포한다. 하지만 NewAccountant는 실제로 해당 기능을 이행하지 못하며, 호출 시 예외를 던지므로 사후 조건과 예외 처리 조건을 모두 위반하고 있다.
- ❌ 동작 의미 위반
외형상 NewAccountant는 FinanceTeamMember의 모든 메서드를 갖추고 있지만, 이 메서드들이 사용자의 기대와 전혀 다르게 동작한다. 예를 들어, manageFunds( )를 호출하면 자금을 관리하는 것이 아니라 예외가 발생하므로, 이는 동작 의미의 불일치를 의미한다.
3-2. 능숙한 개발자의 인터페이스 분리로 LSP 지키기
3-1의 구조를 살펴본 후 문제를 인지한 능숙한 개발자는 재무팀 시스템을 재설계하면서, 모든 팀원이 동일한 역할을 수행하는 것은 아니라는 점에 주목한다. 현실에서도 각 구성원이 담당하는 업무가 다르듯이, 시스템 내 객체 역시 역할 기반으로 책임을 분리해야 한다고 생각했다.
능숙한 개발자는 먼저 재무팀의 각 기능을 별도의 인터페이스로 분리한다. 이렇게 하면, 특정 기능이 필요한 경우에만 해당 인터페이스를 구현하도록 강제할 수 있다.
interface FundManager {
void manageFunds();
}
interface FinancialAnalyst {
void analyzeFinancials();
}
interface InvestmentAdvisor {
void supportInvestmentDecisions();
}
이후 실제 팀원들의 특성을 반영하여 필요한 역할만 구현하도록 설계한다.
class ExperiencedAccountant implements FundManager, FinancialAnalyst, InvestmentAdvisor {
@Override
public void manageFunds() {
System.out.println("[재무팀] - 자금 관리 업무를 수행한다.");
}
@Override
public void analyzeFinancials() {
System.out.println("[재무팀] - 재무 계획을 수립한다.");
}
@Override
public void supportInvestmentDecisions() {
System.out.println("[재무팀] - 투자 판단 지원 업무를 수행한다.");
}
}
반면, 신입 사원은 실무에 투입되지 않고 교육 중심의 역할만 수행하므로, 어떠한 재무 기능도 구현하지 않는다.
class NewAccountant {
public void attendTraining() {
System.out.println("[재무팀] - 신입: 실무 교육 수강한다.");
}
}
이 구조를 사용하는 측의 코드도 더 이상 FinanceTeamMember라는 하나의 추상 타입에 의존하지 않고, 필요한 역할에 따라 정확한 인터페이스 타입으로 의존할 수 있다.
void assignFundTask(FundManager member) {
member.manageFunds();
}
void assignAnalysisTask(FinancialAnalyst member) {
member.analyzeFinancials();
}
void assignInvestmentTask(InvestmentAdvisor member) {
member.supportInvestmentDecisions();
}
초보 개발자의 설계가 단일 타입에 모든 기능을 몰아넣음으로써 LSP를 위반한 반면, 능숙한 개발자의 설계는 현실의 역할 분담을 기반으로 책임을 분리하여, LSP가 요구하는 기능적 계약과 동작 의미를 정확히 만족시켰다. 이 구조는 해당 타입은 어떤 역할을 수행할 수 있어야 하는가?라는 질문의 설계 차원에서 명확하게 답하고 있다.
4. LSP를 지키면 얻을 수 있는 이점
앞서 살펴본 초보 개발자의 예제에서는 타입은 맞지만 행동이 맞지 않는 하위 객체가 설계에 포함되면서 예외 발생, 기능 누락, 테스트 실패 등 다양한 문제가 발생한다. 이는 결국 전체 시스템의 신뢰도와 유지보수성까지 저해하는 결과로 이어진다. 반면, LSP를 충실히 따르는 구조에서는 하위 타입이 상위 타입의 역할을 기대 가능한 방식으로 안전하게 대체할 수 있기 때문에, 시스템 전체가 더 탄탄하고 유연하게 작동한다.
- ✅ 예측 가능한 동작
LSP가 지켜진 시스템에서는 상위 타입으로 작성된 클라이언트 코드가, 어떤 하위 타입으로 대체되더라도 동일하게 동작할 것이라는 신뢰를 전제로 설계할 수 있다. 이러한 구조는 사용자가 타입만 보고도 해당 객체의 동작을 예측할 수 있게 만들어 주며, 코드의 사용 부담을 크게 줄인다.
예를 들어, assignFundTask 함수는 FundManager 인터페이스만 바라보기 때문에 어떤 구현체의 인스턴스가 전달되든 항상 정상적으로 수행된다는 보장이 있다. 신입 사원은 아예 해당 인터페이스를 구현하지 않으므로, 잘못된 객체가 전달될 가능성도 없다.
- ✅ 테스트 용이성
LSP를 만족하는 구조에서는 상위 타입 기준으로 테스트 코드를 작성해도, 이를 구현하는 모든 하위 타입에 대해 동일한 테스트를 반복해서 재사용할 수 있다. 이것은 테스트 코드의 중복을 줄이고, 새로운 구현체가 추가되더라도 테스트 케이스를 변경할 필요가 없도록 만들어 준다.
예를 들어, FundManagerTest라는 테스트 클래스에서 FundManager 인터페이스만을 대상으로 단위 테스트를 구성했다면, ExperiencedAccountant와 같은 실제 구현체를 주입해 테스트를 돌려볼 수 있다. 향후 다른 구현체가 등장해도, 같은 테스트를 그대로 재사용할 수 있어 검증 효율이 올라간다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class FundManagerTest {
@Test
@DisplayName("자금 관리 기능은 예외 없이 정상 동작해야 한다")
void testManageFunds() {
FundManager fundManager = new ExperiencedAccountant(); // 다른 구현체로 대체 가능
assertDoesNotThrow(fundManager::manageFunds, "manageFunds() 실행 시 예외가 발생해서는 안 된다.");
}
}
- ✅ 확장에 강한 구조
하위 타입이 상위 타입을 안전하게 대체할 수 있다면, 새로운 기능이 추가되더라도 기존 코드를 수정하지 않고 확장만으로 시스템을 발전시킬 수 있다. 이 구조는 개방-폐쇄 원칙(OCP, Open Closed Principle)과도 자연스럽게 연결되며, 안정성과 유연성을 동시에 확보할 수 있다.
예를 들어, 재무팀에 세무사를 고용하여 '세금 전략 자문' 업무가 추가되었다면, 기존 클래스는 건드리지 않고 아래와 같이 새로운 역할의 인터페이스와 구현체를 작성하여 확장할 수도 있다.
public interface TaxAdvisor {
void adviseOnTaxStrategy();
}
public class CertifiedTaxAdvisor implements TaxAdvisor {
@Override
public void adviseOnTaxStrategy() {
System.out.println("[재무팀] - 세금 전략 자문을 수행한다.");
}
}
- ✅ 유지보수성과 리팩토링 효율 증가
LSP가 지켜진 구조는 타입 간의 역할과 책임이 명확하게 분리되어 있기 때문에, 특정 기능을 변경하거나 제거할 때 그 변경이 다른 객체에 미치는 영향이 최소화된다. 이는 리팩토링 시 안정성과 예측 가능성을 크게 높여준다.
- ✅ 설계의 명확성과 일관성
LSP를 고려한 설계는 단순히 코드 재사용에 그치지 않고, 각 타입이 왜 존재하는지, 어떤 역할을 수행해야 하는지를 명확히 드러내는 구조로 이어진다. 인터페이스나 추상 클래스는 계약(contract)을 정의하고 그것을 충실히 이행하는 구조로 해석된다.
예를 들어, ExperiencedAccountant는 명확히 세 가지 역할을 수행할 수 있는 인물이라는 사실이, FundManager, FinancialAnalyst, InvestmentAdvisor를 동시에 구현하고 있는 구조만으로 명확하게 표현된다. 반대로 NewAccountant는 교육 중이라는 특성을 attendTraining( )이라는 메서드 하나로 드러내며, 실무 기능을 강요받지 않는 것을 확인할 수 있다.
정리하자면, LSP는 객체 간 신뢰 가능한 계약과 일관된 행동을 설계에 강제하는 원칙이다. 이 원칙을 지키면 시스템은 더 예측 가능하고, 더 안전하며, 더 잘 확장 가능한 구조로 나아갈 수 있다. 또한 개발자는 타입을 보는 것만으로도 그 객체의 책임과 한계를 명확히 이해할 수 있게 되고, 이는 결국 개발 속도, 품질, 유지보수 효율을 모두 향상하는 핵심 설계 역량으로 이어진다.
5. LSP 적용 기준 - 언제 추상화하고 분리할 것인가?
LSP는 객체지향 설계의 품질을 좌우하는 중요한 기준이다. 앞서 살펴본 것처럼, LSP를 준수하면 시스템은 더욱 예측 가능하고, 안정적이며, 변화에 유연하게 대응할 수 있다. 그러나 모든 상황에서 LSP를 반드시 적용해야 하는 것은 아니다. 오히려 LSP를 맹목적으로 지키려는 시도는 설계를 더 불편하게 만들 수도 있다.
현실적으로 다음과 같은 사례에서는 오히려 LSP를 억지로 적용하는 것이 설계를 더 복잡하게 만들 수도 있다.
- 실제로 교체될 일 없는 내부 객체에까지 추상화를 강제하는 경우
- 간단한 유틸 클래스나 일회성 객체에 대해서도 인터페이스를 도입하는 경우
- 모든 기능을 쪼개서 의미 없는 인터페이스들이 양산되는 경우
그러면 어떤 경우에 LSP를 적용하는 것이 타당할까? 설계를 진행하면서 다음과 같은 질문에 스스로 판단해 보자.
- 💡 이 타입은 다른 타입으로 대체될 가능성이 있는가?
다양한 구현체가 등장할 수 있고, 그 구현체들이 동일한 클라이언트 코드에서 사용될 가능성이 있다면, LSP가 고려돼야 한다.
- 💡 이 타입을 사용하는 쪽에서 어떤 기능을 기대하고 있는가?
클라이언트 코드가 해당 타입에 대해 특정 동작을 기대하고 있다면, 그 기대를 충족하지 않는 구현체는 LSP를 위반하게 된다.
- 💡 정의한 계약은 실제로 지켜져야 할 만큼 중요한가?
메서드의 입력, 출력, 예외 발생 등 계약 수준의 규칙이 실제 동작 신뢰성에 영향을 준다면, 하위 타입도 이를 엄격히 따라야 한다.
- 💡 하위 타입도 진심으로 이 계약을 지킬 수 있는가?
단순히 타입만 맞춘 후 하위 타입이 상위 타입의 계약을 완전히 이행할 수 없다면, 설계 자체를 재검토해야 한다. 이 경우에는 역할을 분리하거나 기능 단위로 쪼개는 방식이 적절하다.
LSP는 결국 "치환 가능한 구조인가?"라는 질문에 설계자의 감각을 훈련시키는 원칙이다. 그 감각이 없다면 구조는 쉽게 무너지지만, 반대로 감각이 있다면 원칙은 자연스럽게 실천된다. 따라서 중요한 것은 LSP를 무조건 적용하는 것이 아니라, 지켜야 할 때 지키고, 그렇지 않은 상황은 명확하게 구분하는 판단 기준을 갖추는 것이다.
6. 마무리 - 객체지향 설계 감각을 기르는 핵심 원칙 LSP
지금까지의 내용을 정리하면 다음과 같다.
객체지향 설계에서 추상화와 상속은 재사용성과 확장성을 높이는 핵심 도구이다. 그러나 타입이 맞는다고 해서 동작까지 맞을 것이라 기대하는 것은 설계의 큰 착각이 될 수 있다. 이러한 오류를 방지하기 위해 존재하는 원칙이 리스코프 치환 원칙(LSP, Liskov Substitution Principle)이다.
LSP는 "프로그램의 각 부분에서 상위 타입의 인스턴스를 사용하는 모든 경우에, 그 자리에 하위 타입의 인스턴스를 대체하더라도 프로그램의 동작이 바뀌지 않아야 한다."라는 정의를 갖는다. 즉, 하위 타입이 상위 타입을 행동적으로 완전히 대체할 수 있어야 하며, 단순히 타입 계층이 맞는 것만으로는 충분하지 않다. 이 원칙은 두 가지 기준을 충족할 때 성립한다.
- 기능적 계약 : 입력, 출력, 예외 처리, 내부 상태 보존 등의 계약을 하위 타입도 동일하게 이행
- 동작 의미 일관성 : 외형이 같더라도 동작이 기대와 다르면 LSP 위반
LSP를 지키지 않으면 다음과 같은 문제가 발생한다.
- 타입은 맞지만 기능은 동작하지 않는 객체
- 예상치 못한 예외 발생
- 테스트 실패 또는 누락
- 클라이언트 코드의 복잡성 증가
- 시스템 전체의 신뢰도 하락
LSP를 충실히 따르는 구조는 다음과 같은 실질적인 이점을 제공한다.
- 예측 가능한 동작 : 타입만 보더라도 해당 객체의 동작을 신뢰할 수 있음
- 테스트 재사용성 : 상위 타입 기준으로 작성된 테스트를 모든 구현체에 적용 가능
- 확장성 강화 : 기존 코드를 수정하지 않고도 새로운 기능을 추가 가능
- 유지보수 용이성 : 변경의 파급 범위가 작고, 역할이 명확히 분리됨
- 설계 명확성 : 객체의 존재 이유와 책임이 코드 구조로 자연스럽게 드러남
모든 경우에 맹목적으로 LSP를 적용할 필요는 없다. 다음과 같은 질문을 통해 LSP 적용 여부를 판단할 수 있다.
- 이 타입이 실제로 다른 구현체로 대체될 가능성이 있는가?
- 이 타입이 제공해야 하는 동작은 계약 수준으로 중요한가?
- 하위 타입이 해당 계약을 온전히 이행할 수 있는가?
LSP는 객체지향 설계의 기반이 되는 강력한 원칙이지만, 적용 시점과 범위를 스스로 판단할 수 있어야 비로소 실용적인 원칙이 된다. 명확한 판단 기준 없이 원칙만 따르려는 시도는 불필요한 복잡성을 낳는다. 반면, 상황에 맞게 LSP를 적용하는 설계자는 시스템의 안정성과 확장성 사이의 균형을 아는 사람이다. 따라서, 해당 원칙을 이해하고 적절히 적용하는 것으로 설계의 역량을 키울 수 있다.
'설계 > 설계 원칙' 카테고리의 다른 글
SOLID의 DIP : 변화에 강한 구조를 만드는 의존성 역전 원칙 (0) | 2025.05.12 |
---|---|
SOLID의 ISP : 역할 중심 설계를 도와주는 인터페이스 분리 원칙 (0) | 2025.05.07 |
SOLID의 OCP : 조건문 없이 유연하게 확장하는 개방-폐쇄 원칙 (0) | 2025.05.01 |
SOLID의 SRP : 클래스 책임 분리로 복잡도를 낮추는 단일 책임 원칙 (0) | 2025.04.29 |