LSP(Liscov Substitution Principle)에서의 행위 일관성

이재원·2025년 5월 12일
0

아키텍처

목록 보기
3/4
post-thumbnail

리스코프 치환 원칙(LSP)

LSP는 SOLID원칙 중 하나로, 다음과 같은 문장으로 정리할 수 있습니다.

상위 타입의 객체를 사용하는 코드에서, 하위 타입으로 교체하더라도 프로그램의 의미가 변하지 않아야 한다.

즉, 어떤 클래스 A가 있고 이 클래스를 사용하는 코드가 있을 때, A를 상속받은 클래스 B로 교체해도 그 코드는 문제없이 동작해야 하는 것을 말하는데요. 이를 만족하려면 상속 이상의 조건이 필요한데, 바로 행위의 일관성입니다. 행위의 일관성은 메서드의 전제 조건(선조건)과 결과 조건(후조건)에 대한 정합성이 지켜져야 합니다.

LSP에서의 선조건 관계

선조건(precondition)이란, 메서드가 호출되기 전에 반드시 만족해야 하는 조건을 말합니다. LSP에 따르면 하위 클래스는 절대 상위 클래스보다 더 강한(까다로운) 선조건을 요구해서는 안 됩니다.

이유는 간단합니다. 클라이언트는 상위 클래스의 규칙만 알고 있고, 그것만 믿고 코드를 짜고 있는데, 하위 클래스가 더 까다로운 조건을 요구하면 클라이언트의 기대를 배신하게 되죠. 이로 인해 런타임 오류나 비정상적인 동작이 발생할 수 있습니다.

**class Printer {
	// 선조건: page >= 5
	public void printDocument(int pages) {
		if (pages < 5) {
			throw new IllegalArgumentException("5페이지 이상이어야 합니다.")
		}
		System.out.println("Printing " + pages + " pages...");
	}
}

class SecurePrinter extends Printer {
	// 선조건: pages >= 10 (더 강화됨 -> LSP 위반)
	@Override
	public void printDocument(int pages) {
		if (pages < 10) {
			throw new IllegalArgumentException("10페이지 이상이어야 합니다.")
		}
		System.out.println("Securely Printing " + pages + " pages...");
	}
}**

위 예제에서 Printer 클래스는 5페이지 이상이면 인쇄 가능하다고 했지만, 하위 클래스인 SecurePrinter는 10페이지 이상이라는 더 강한 조건을 요구하고 있습니다.

// 클라이언트 코드 - 다형성 기반
public class Client {
    public static void processPrintJob(Printer printer) {
        // 클라이언트는 "5페이지 이상이면 된다"는 계약을 믿고 호출
        printer.printDocument(7); // SecurePrinter는 이 입력을 거부함
    }

    public static void main(String[] args) {
        Printer printer = new SecurePrinter(); // 다형성
        processPrintJob(printer); // 런타임 예외 발생 → LSP 위반!
    }
}

이런 상황에서 클라이언트는 Printer를 기대하고 인스턴스를 넘겼는데, 실제로는 SecurePrinter가 들어오면 오류가 발생할 수 있습니다. 즉, LSP 위반입니다.

선조건은 완화하거나 유지해야 한다.

LSP를 만족시키려면 하위 클래스는 상위 클래스의 선조건보다 같거나 더 느슨한 조건만 요구해야 합니다. 즉, 더 많은 상황에서도 동작할 수 있어야 하는 것이죠.

class GenerousPrinter extends Printer {
	// 선조건: pages >= 1 ( 더 완화됨 -> LSP 만족)
	@Override
	public void printDocument(int pages) {
		if (pages < 1) {
			throw new **IllegalArgumentException("1페이지 이상이어야 합니다.");
		}
		System.out.println("Even Printing " + pages + " pages...");
	}
}**

이 경우, 상위 클래스가 요구한 조건보다 더 많은 입력을 받아들이므로 클라이언트의 기대를 벗어나지 않으며, 오히려 더 유연하게 대응할 수 있습니다.

후조건은 강화하거나 유지해야 한다.

후조건은 메서드 실행 이후 반드시 만족되어야 하는 결과 조건입니다. LSP에서는 하위 클래스가 상위 클래스보다 더 약한 결과(후조건 약화)를 반환하면 안되고, 동일하거나 더 구체적인 결과(후조건 강화)를 제공해야 합니다.

특히 메서드의 선조건을 강화하면 하위 클래스는 더 이상 상위 클래스의 대체물이 될 수 없게 됩니다. 따라서 하위 클래스는 선조건을 완화하거나 유지해야 하며, 이 원칙을 지켜야만 확장성과 안정성을 얻을 수 있게 됩니다.

아래 예제는 후조건 약화로 인한 LSP 위반한 코드입니다.

class Printer {
    // 후조건: 반드시 "Printed X pages" 메시지를 출력해야 함
    public String printDocument(int pages) {
        if (pages < 5) {
            throw new IllegalArgumentException("5페이지 이상이어야 합니다.");
        }
        return "Printed " + pages + " pages";
    }
}

class BrokenPrinter extends Printer {
    @Override
    public String printDocument(int pages) {
        if (pages < 5) {
            throw new IllegalArgumentException("5페이지 이상이어야 합니다.");
        }
        return null; // 후조건 약화 → 아무 결과도 보장하지 않음 → LSP 위반
    }
}

예제 코드를 보면, Printer는 항상 문자열을 반환하고, 특정 형식을 유지한다는 결과를 암시하고 있습니다. 그런데 BorkenPrinter는 null을 반화하거나 결과 형식을 지키고 있지 않습니다.

이로 인해 후조건이 약화되었고, 클라이언트는 “Printed X pages”를 기대했다가 예외나 null을 받게 됩니다.

이것이 후조건 약화로 인한 LSP위반입니다. 다음은 후조건을 강화하고 유지한 예제입니다.

class SecurePrinter extends Printer {
    @Override
    public String printDocument(int pages) {
        if (pages < 5) {
            throw new IllegalArgumentException("5페이지 이상이어야 합니다.");
        }
        // 후조건 강화: 결과에 추가 정보 포함
        return "[SECURE MODE] Printed " + pages + " pages successfully.";
    }
}

public class Client {
    public static void main(String[] args) {
        Printer printer = new SecurePrinter(); // 다형성
        String result = printer.printDocument(15);
        System.out.println(result); // 여전히 유효한 결과 출력 → LSP 만족
    }
}

마무리

리스코프 치환 원칙을 지킨다는 건 단순히 문법적으로 상속만 하는 것이 아니라, 행위적으로 상위 클래스와 하위 클래스가 일관된 동작을 보여야 한다는 의미입니다.

SOLID 원칙은 간단한 예제 코드를 보면 이해하기는 쉽지만, 실제로 프로젝트를 하면 지키지 못하는 경우가 많은 것 같습니다. 그렇기 때문에 이러한 특성들을 의식적으로 기억하면서 코딩을 해야 되겠습니다.

또한, SwiftUI처럼 함수형 프로그래밍을 지향하는 환경에서는 전통적인 방식으로 SOLID 원칙을 온전히 적용하기는 어렵습니다. 하지만 SOLID가 강조하는 핵심 철학인 역할 분리, 유연한 확장, 인터페이스 기반 설계은 충분히 적용 가능합니다.

즉, 꼭 객체지향 언어를 사용하지 않더라도 이러한 설계 원칙을 의식하며 설계하고 구현하는 것 자체가 중요하다고 생각합니다. 이러한 사고방식은 결과적으로 코드의 유지보수성과 확장성을 높이는 데 큰 도움이 될 것기 때문이죠.

참고자료

소프트웨어공학 수업 강의 내용 참고(2025년 1학기)
https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-LSP-%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84-%EC%B9%98%ED%99%98-%EC%9B%90%EC%B9%99

profile
20학번 새내기^^(였음..)

0개의 댓글