티스토리 뷰

IT/SOLID

LSP - 리스코프 치환 원칙 (Liskov Substitution Principle) : "부모를 대체해도 문제가 없어야 한다"

Stv 2025. 4. 25. 17:25

목차


    1. LSP란?

    리스코프 치환 원칙은 "서브타입은 언제나 자신의 기반 타입(부모 타입)으로 교체할 수 있어야 한다"는 원칙이다.

     

    📌 원칙의 핵심

    "자식 클래스는 부모 클래스를 대체해도 동작에 문제가 없어야 한다"
    • LSP를 만족하면 클라이언트는 부모 타입(추상 타입)만 보고도 구현체(자식 클래스)에 상관없이 동일한 방식으로 사용할 수 있어야 한다.

    2. LSP를 이해하는 질문

    • 이 자식 클래스는 진짜 is-a 관계인가?
    • 부모 클래스를 사용하는 코드에서 자식 클래스를 넣었을 때, 기능이 깨지지 않는가?
    • 다형성을 깨지 않고 안전하게 확장할 수 있는가?

    3. LSP 위반 예시 (고전적인 Rectangle vs Square)

    public class Rectangle {
        protected int width;
        protected int height;
    
        public void setWidth(int width) { this.width = width; }
        public void setHeight(int height) { this.height = height; }
    
        public int getArea() { return width * height; }
    }
    public class Square extends Rectangle {
        @Override
        public void setWidth(int width) {
            this.width = width;
            this.height = width; // 정사각형이므로 height도 함께 설정
        }
    
        @Override
        public void setHeight(int height) {
            this.width = height; // height 변경 시 width도 같이 변경
            this.height = height;
        }
    }

     

    📉 LSP 위반 상황

    Rectangle rect = new Square();
    rect.setWidth(5);
    rect.setHeight(10);
    System.out.println(rect.getArea()); // 예상? 50 → 실제? 100

     

    ➡️ Square는 Rectangle을 상속했지만, 동작이 완전히 달라져서 치환이 불가능함

    ➡️ LSP 위반 -> 이는 상속을 잘못 쓴 설계의 대표적인 예이다.

     

    🔁 4. LSP를 지키는 구조로 리팩토링

    public interface Shape {
        int getArea();
    }
    public class Rectangle implements Shape {
        private int width;
        private int height;
    
        // 생성자 기반으로 설정
        public Rectangle(int width, int height) {
            this.width = width;
            this.height = height;
        }
    
        public int getArea() {
            return width * height;
        }
    }
    
    public class Square implements Shape {
        private int length;
    
        public Square(int length) {
            this.length = length;
        }
    
        public int getArea() {
            return length * length;
        }
    }

     

    ➡️ 이제는 서로 다른 규칙을 가진 Rectangle, Square를 "공통된 인터페이스(Shape)로 묶었기 때문에, 서로 간섭 없이 다형성 유지 + 책임 분리 

     

    5. 실무에서 자주 보는 LSP 위반 사례

     

    위반 사례 문제
    자식 클래스가 부모 메서드의 동작 계약(contract)을 변경 호출자 입장에서 기능이 다르게 동작함
    상속 후 일부 메서드만 의미 없는 형태로 override 호출은 가능하지만, 행동이 예상과 다름
    자식 클래스가 부모의 메서드를 오히려 무력화하거나 예외 throw 인터페이스를 따르지 않는 치환 불가능 상태

     

    6. 실무에서 LSP를 지키기 위한 팁

     

    ✔️자식 클래스가 부모의 "계약"을 무시하지 않게 하기

    • 부모 클래스의 사전 조건(precondition)은 자식이 더 약해야 한다.
    • 부모 클래스의 사후 조건(postcondition)은 자식이 더 강해야 한다.

    📌 쉽게 말하면 :

    "자식은 덜 까다롭게 입력 받고, 더 많이 책임지고 반환하라"

     

    ✔️ 진짜 "is-a" 관계일 때만 상속 사용

    • 상속은 강력한 결합을 만들어내는 위험한 구조
    • 가능하면 상속보다 "구현(implements), 합성(composition)이 더 유연

    7. LSP는 OOP와 어떤 관계가 있을까?

    원칙 설명
    다형성 부모 타입으로 자식 객체를 대입할 수 있어야 함
    캡슐화 자식 클래스가 부모 구조를 잘못 변경하지 않도록 보호
    추상화 LSP가 보장될 때, 진짜 의미 있는 추상화가 가능해짐

     

    ✅ 마무리 요약

    • LSP는 자식 클래스가 부모 클래스를 대체해도 무방해야 한다는 원칙
    • 동작이 다르거나 기능이 일부만 동작하면 치환 불가능 -> 설계 위반
    • 진짜 is-a 관계일 때만 상속을 쓰고, 그 외에는 합성(Composition) + 인터페이스 기반 설계를 지향
    • LSP는 다형성, 추상화, 테스트 가능성, 유연한 아키텍처의 핵심 기반이다.

    🔜 다음 포스팅 예고 : ISP - 인터페이스 분리 원칙 : "클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다" 라는 주제를 다룰 예정이다.