객체지향 프로그래밍(OOP)에서는 더 나은 코드 구조와 유지 보수를 위해 SOLID 원칙을 따른다.
SOLID 원칙의 세 번째, L - 리스코프 치환 원칙(LSP) 을 알아보자.
“자식 클래스는 언제나 부모 클래스의 역할을 대체할 수 있어야 한다.”
📌 리스코프 치환 원칙(LSP)이란?
Liskov Substitution Principle
“하위 클래스는 상위 클래스로 교체해도 프로그램의 정확성이 유지되어야 한다.”
즉, 하위 타입은 상위 타입을 대체할 수 있어야 한다는 의미이다.
상속받은 자식 클래스는 부모 클래스의 기능을 대체하거나 확장할 수 있어야지, 변경하거나 위반해서는 안 된다.
💡 핵심
- 서브클래스가 부모 클래스의 동작을 변경해서는 안 된다.
- 하위 클래스가 상위 클래스의 규약(계약)을 위반하면 LSP 위반이다.
- "is-a 관계"가 성립해야 하며, 상속을 잘못 사용하면 오히려 LSP를 어기게 된다.
Java의 컬렉션 프레임워크 (Collection Framework)
LSP의 원칙을 잘 적용한 예제이다.
만약 변수를 `LinkedList`를 쓰다가, `HashSet`으로 자료형을 바꿔도 `add()`나 `remove()` 의 메소드의 동작을 보장받을 수 있다. 이렇게 하기 위해서는 `Collection`이라는 인터페이스 타입으로 변수를 선언하여 할당받으면 된다.
인터페이스 `Collection`의 추상 메서드를 각기 하위 자료형 클래스에 `implements` 하여 인터페이스 구현 규약을 지키도록 미리 설계되어있기 때문이다.
void testData() {
// Collection 인터페이스 타입으로 변수 선언
Collection data = new LinkedList();
data = new HashSet(); // 중간에 전혀 다른 자료형 클래스를 할당해도 호환됨
modify(data); // 메소드 실행
}
void modify(Collection data){
list.add(1); // 인터페이스 구현 구조가 잘 잡혀있기 때문에 add 메소드 동작이 각기 자료형에 맞게 보장됨
// ...
}
“리스코프 치환 원칙”은 어려워보이지만, 자바를 쓰면서 사용한 다형성을 지키기 위한 원칙이라고 볼 수 있다.
📝 LSP 예제
“하위 클래스는 상위 클래스로 교체해도 프로그램의 정확성이 유지되어야 한다.”
예시1. 사각형 vs 정사각형
❌ LSP를 위반한 예시
class Rectangle {
protected int width;
protected int height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
width = w;
height = w;
}
@Override
public void setHeight(int h) {
width = h;
height = h;
}
}
- 위 코드처럼 `Rectangle` 객체를 상속 받은 `Square` 클래스에서는 정사각형의 너비와 높이가 같다는 특징을 구현했다.
- 정사각형은 직사각형 범주에 포함되므로 정상적으로 동작해야 한다. 이떄 리스코프 치환 원칙은 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체 할 수 있다는 원칙이었다.
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
System.out.println(r.getArea()); // 기대값: 50, 실제값: 100
- 기대한 값인 50이 아닌, 100이 출력된다.
- 정사각형의 넓이는 잘 구현되었으나 `Rectangle` 클래스의 동작과 그를 상속 받은 `Square` 클래스의 동작이 전혀 다르기 때문이다.
- 이는 정사각형이 직사각형을 상속 받는 것이 올바른 상속 관계가 아니라는 것을 의미한다. 즉, 자식 객체가 부모 객체의 역할을 완전히 대체하지 못한다는 의미이다.
- 때문에 이 코드는 리스코프 치환원칙을 위배한다.
✅ LSP를 지킨 예시 (인터페이스 분리)
public class Shape {
public int width;
public int height;
// 너비 반환, Width Getter
public int getWidth() {
return width;
}
// 너비 할당, Width Setter
public void setWidth(int width) {
this.width = width;
}
// 높이 반환, Height Getter
public int getHeight() {
return height;
}
// 높이 할당, Height Setter
public void setHeight(int height) {
this.height = height;
}
// 사각형 넓이 반환
public int getArea() {
return width * height;
}
}
//직사각형 클래스
public class Rectangle extends Shape {
public Rectangle(int width, int height) {
setWidth(width);
setHeight(height);
}
}
//정사각형 클래스
public class Square extends Shape{
public Square(int length) {
setWidth(length);
setHeight(length);
}
}
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(10, 5);
Shape square = new Square(5);
System.out.println(rectangle.getArea());
System.out.println(square.getArea());
}
}
- 더이상 `Rectangle` 객체와 `Square` 객체는 상속 관계가 아니므로, 리스코프 치환 원칙을 준수한다.
예시 2: Bird → Ostrich (날 수 없는 새)
❌ LSP를 위반한 예시
class Bird {
public void fly() {
System.out.println("Flying...");
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich can't fly");
}
}
Bird bird = new Ostrich();
bird.fly(); // 런타임 예외 발생
- `Bird`는 모두 날 수 있다(`fly`)는 전제를 깔고 있다.
- `Ostrich`는 `Bird`지만 날지 못하는 새로 예외가 발생한다. → 부모 클래스의 계약(Contract)을 깨뜨림
- 이로 인해 프로그램이 `Bird`에 기대하던 행동을 할 수 없게 됨 → LSP 위반
✅ LSP를 지킨 예시 (인터페이스 분리)
interface Bird {
void layEgg();
}
interface FlyingBird extends Bird {
void fly();
}
class Ostrich implements Bird {
public void layEgg() { ... }
}
class Sparrow implements FlyingBird {
public void layEgg() { ... }
public void fly() { ... }
}
- 알을 낳는 새 `Bird`를 `FlyingBird`가 상속
- `Bird`는 이제 날 수 있다고 가정하지 않고, 알을 낳는 것만 가정한다.
- `Ostrich`는 `Bird`를 상속하기에 날지 않는다는 것을 알 수 있다.
- `FlyingBird` 인터페이스를 구현한 새만 `fly()`를 사용할 수 있다.
- 타입 안전성 보장 → 리스코프 치환 원칙 준수
🧠 LSP 원칙 적용 주의점
리스코프 치환 원칙의 핵심은 상속(Inheritance)이다.
하지만 여기서 주의할 점은, 객체지향 프로그래밍에서 상속은 기반 클래스와 서브 클래스 사이에 is-a 관계가 있을 경우로만 하도록 제한되어야 한다!
그 외의 경우 (has-a)는 Composition(합성)을 이용하도록 권고되어있다.
📘 참고자료
- 코드잇 SB 강의 자료
- https://inpa.tistory.com/entry/OOP-💠-아주-쉽게-이해하는-LSP-리스코프-치환-원칙
- https://velog.io/@harinnnnn/OOP-객체지향-5대-원칙SOLID-리스코프-치환-원칙-LSP
- https://levelup.gitconnected.com/java-collections-framework-class-hierarchy-latest-2024-51f9154f1f57
Java Collections Framework — Class Hierarchy
Know everything that can be asked on the Collection Hierarchy in the Java Interviews
levelup.gitconnected.com
[OOP] 객체지향 5대 원칙(SOLID) - 리스코프 치환 원칙 (LSP)
이번 글에서는 객체지향의 5대 원칙 중, 리스코프 치환 원칙 (LSP)에 대해 알아봅니다!
velog.io
💠 완벽하게 이해하는 LSP (리스코프 치환 원칙)
리스코프 치환 원칙 - LSP (Liskov Substitution Principle) 리스코프 치환 원칙은 1988년 바바라 리스코프(Barbara Liskov)가 올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로, 서브 타입은 언제나 기반
inpa.tistory.com
✏️ 마무리
리스코프 치환 원칙은 상속을 쓸 때 반드시 고려해야 하는 원칙이다.
“하위 클래스는 상위 클래스로 교체해도 프로그램의 정확성이 유지되어야 한다.”
- 다형성을 이용하고 싶다면 `extends` 대신 인터페이스로 `implements` 하기
- `is-a` 관계가 아닐 경우, 상속(inheritance)보다는 합성(composition)을 고려하기
다음 포스팅에서는 I - 인터페이스 분리 원칙 (Interface Segregation Principle) 을 다뤄보겠습니다.
SOLID - I: 인터페이스 분리 원칙 (Interface Segregation Principle)
객체지향 프로그래밍(OOP)에서는 더 나은 코드 구조와 유지 보수를 위해 SOLID 원칙을 따른다.SOLID 원칙 중 네 번째인 I -인터페이스 분리 원칙 (ISP)를 알아보자.📌 인터페이스 분리 원칙(ISP)이란?Int
minsllogg.tistory.com
'Architecture & Design' 카테고리의 다른 글
SOLID - D: 의존 역전 원칙 (Dependency Inversion Principle) (0) | 2025.06.15 |
---|---|
SOLID - I: 인터페이스 분리 원칙 (Interface Segregation Principle) (1) | 2025.06.15 |
SOLID - O: 개방/폐쇄 원칙 (Open/Closed Principle) (1) | 2025.06.15 |
SOLID 원칙 - S: 단일 책임 원칙(SRP) (0) | 2025.06.08 |
객체지향 프로그래밍(OOP)의 5가지 핵심 원칙 - SOLID (1) | 2025.06.08 |