-
SOLID - LSP (리스코프 치환 원칙)Clean Architecture 2025. 12. 14. 14:10

안녕하세요~ 오늘은 SOLID 원칙 중 세 번째, LSP 원칙에 대해 알아보겠습니다.
1. 정의와 예시
리스코프 치환 원칙(Liskov Substitution Principle)은 리스코프 교수님이 주장한 원칙으로, 하위 클래스는 언제나 상위 클래스를 대체할 수 있어야 한다는 것을 의미합니다. 좀 더 풀어서 설명하자면, 하위 클래스는 상위 클래스가 제공하는 모든 기능을 동일하게 수행해야 하며, 상위 클래스의 동작을 변경하거나 제한해서는 안된다는 것으로, 안정적이고 예측 가능한 코드를 설계하는 데 중요합니다.
2. 코틀린 위반/준수 예제
// LSP 원칙 위반 open class Bird { open fun fly() { println("Bird can fly") } } class Sparrow: Bird() { override fun fly() { println("Sparrow can fly") } } class Penguin: Bird() { override fun fly() { throw UnsupportedOperationException("Penguin can't fly") } }코틀린 언어 예시를 살펴보겠습니다. 참새 하위 클래스는 Bird 상위 클래스가 제공하는 비행 기능을 올바르게 수행하지만, 펭귄 하위 클래스는 해당 기능을 수행하지 못하고 예외를 던지고 있습니다. 일부 하위 클래스가 상위 클래스의 기능을 동일하게 수행하지 못하고, 동작을 변경하거나 제한하기 때문에 LSP 원칙을 위배하였습니다. 어디에서 잘못되었을까요? 새가 모두 날 수 있을거라고 가정한 부모 클래스의 비행 메소드가 그 원인입니다.

LSP 원칙을 위반한 경우 // LSP 원칙 준수 open class Bird open class FlyingBird: Bird() { open fun fly() { println("I can fly") } } class Penguin: Bird() { fun swim() { println("Penguin can swim") } } class Sparrow: FlyingBird() { override fun fly() { println("Sparrow can fly") } }LSP 위반을 해결하기 위해 상위 클래스를 새, 날 수 있는 새 2가지로 분리하였습니다. 이렇게 하면 펭귄이나 타조와 같이 날지 못하는 경우에는 Bird 클래스를 상속받으면 되고, 참새나 비둘기와 같이 날 수 있는 경우는 FlyingBird 클래스를 상속받으면 됩니다. 이제 상위 클래스를 위반하는 하위 클래스가 존재하지 않게 되었습니다.

LSP 원칙을 준수한 경우 3. 안드로이드 위반/준수 예제
// LSP 위반 open class BaseViewHolder(view: View): RecyclerView.ViewHolder(view) { open fun bind(data: String) { // String 데이터 바인딩 } } class ImageViewHolder(view: View): BaseViewHolder(view) { override fun bind(data: String) { throw UnsupportedOperationException("ImageViewHolder는 String 데이터를 바인딩 할 수 없습니다.") } }안드로이드 예시를 살펴보겠습니다. 상위 클래스가 제공하는 기능을 수행하지 못하고 예외를 발생시키는 하위 클래스는 LSP 원칙 을 위반합니다. 이를 어떻게 해결할 수 있을까요?
// LSP 준수 open class BaseViewHolder(view: View): RecyclerView.ViewHolder(view) { open fun bind(data: T) { // 모든 데이터 바인딩 } } class ImageViewHolder(view: View): BaseViewHolder(view) { override fun bind(data: Image) { showUI(data) // 이미지 데이터 바인딩 } } class TextViewHolder(view: View): BaseViewHolder(view) { override fun bind(data: String) { showUI(data) // 텍스트 데이터 바인딩 } }상위 클래스의 데이터 타입을 제네릭 T로 변경하면 범위를 한정하지 않게 됩니다. 모든 하위 클래스에서 예외없이 상위 클래스의 제공 메소드를 수행할 수 있게 되면서 LSP 원칙을 준수하게 되었습니다.
// LSP를 준수한 경우 class CustomMyButton: Button { override fun setOnClickListener(l: OnClickListener?) { super.setOnClickListener(l) showUI() } } // LSP를 위반한 경우 1 class DisabledOneButton: Button { override fun setOnClickListener(l: OnClickListener?) { super.setOnClickListener(null) } } // LSP를 위반한 경우 2 class DisabledTwoButton: Button { override fun setOnClickListener(l: OnClickListener?) { // super.setOnClickListener(l) } }하위 클래스인 Button은 상위 클래스인 View가 정의한 기본적인 동작을 예상대로 수행해야 합니다. 그렇게 해야 클라이언트는 View 객체를 다룰 때, 그것이 Button이든 ImageView든 관계없이 View의 공통 동작을 믿고 로직을 작성할 수 있습니다. 하지만, 위의 예시에서는 오버라이드 메소드에서 super.on 부모 클래스의 구현을 호출하지 않게 되면서 LSP 원칙을 위반합니다. 메소드 내의super.on 호출은 부모 클래스가 제공하는 기능 수행은 최소한 약속한다는 것을 의미하고, 그 이후에 기능을 확장하는 방향으로 가야 합니다.
4. LSP와 클린 아키텍처의 연관성
LSP 원칙에서 강조하는 상위 클래스와 하위 클래스의 관계를 보면, 추상체와 구현체를 떠올릴 수 있는데요. 클린 아키텍처와 무슨 연관성이 있을까요?
추상체 레이어 추상체 구현체 레이어 구현체 Domain UseCase Interface Domain UseCase Impl Domain Repository Interface Data Repository Impl Data DataSource Interface Remote, Local DataSoure Impl 클린 아키텍처 내에서는 UseCase, Repository, DataSource 3개의 요소에 대해 추상체와 구현체가 존재합니다. 모든 구현체(서브 클래스)는 추상체(상위 클래스)가 제공하는 기능을 일관되게 준수해야 계층 간 구조에서 안정성이 높아집니다.
5. OCP와 LSP의 차이와 관계성
이번에도 인터페이스, 추상메소드를 언급하여 아마 저번 시간에 다룬 개방 폐쇄 원칙과 헷갈릴 수 있습니다. 인터페이스, 추상 클래스를 여전히 다루고 있지만 개방 폐쇄원칙과 리스코프 치환 원칙은 이를 서로 다른 관점에서 보고 있습니다.

LSP 먼저, 리스코프 치환 원칙(LSP)는 추상체(상위 클래스)를 위반하는 구현체(하위 클래스)는 없어야 한다는 점에서 추상체, 구현체의 치환 가능한 관계에 집중합니다. 예시로, 클린 아키텍처에서 추상체는 DataSource 인터페이스, 구현체는 RemoteDataSource, LocalDataSoure가 있습니다. 기반 타입인 DataSource 인터페이스에서 유저를 가져오는 메소드 getUser()를 제공한다면, 서브 타입인 Remote와 Local은 해당 메소드를 위반하지 않고 동일한 방식으로 동작하도록 구현해야합니다. 그렇게해야 클라이언트는 데이터 소스가 변경되더라도 로컬 DB에서 가져오든 서버에서 가져오든 결과값이 같다고 기대할 수 있습니다.

OCP 반면, 개방폐쇄 원칙(OCP)은 추상체를 이용해서 기존 코드의 수정을 막고 확장할 수 있다는 점에 의미를 두며, 구현체에 대해 신경쓰지 않습니다. 예를 들어, Repository는 DataSource 인터페이스의 메서드만 호출할 뿐 어떤 구체적인 데이터 소스를 사용하는지 모릅니다.
두 개의 원칙을 비교해보았을 때, LSP는 OCP를 달성하기 위한 필수 조건입니다. Repository가 데이터 소스의 변경과 상관없이 닫혀 있으려면(OCP), RemoteDataSource에서 LocalDataSource로 문제없이 치환되어야 합니다(LSP). 만약 치환된 서브타입이 기반 타입인 DataSource 인터페이스의 메소드 기능을 위반한다면(LSP), Repository 또한 관심사 분리를 하지 못하고 변경되어야 합니다(OCP).
오늘은 인터페이스 타입으로 선언하고 실제 구현체를 자유롭게 바꿀 수 있어야 하는 원칙, 리스코프 치환 원칙에 대해 알아보았습니다. 다음 시간에는 네 번째 원칙, 인터페이스 분리 원칙에 대해 알아보겠습니다!
'Clean Architecture' 카테고리의 다른 글
SOLID - ISP (인터페이스 분리 원칙) (1) 2026.01.11 SOLID - OCP (개방 폐쇄 원칙) (0) 2025.11.30 SOLID - SRP (단일 책임 원칙) (0) 2025.10.05 UI 레이어란? (3) 2025.08.24 Presentation 레이어란? (8) 2025.08.17