리스코프 치환 원칙(Liskov Substitution Principle, LSP)은 객체 지향 프로그래밍에서 하위 클래스는 언제나 상위 클래스를 대체할 수 있어야 한다는 원칙을 의미. 이 원칙은 객체 지향 설계의 SOLID 원칙 중 하나로, 상속과 다형성을 올바르게 사용하는데 중요한 기준을 제공
프로그램에서 상위 클래스의 객체를 하위 클래스의 객체로 치환하더라도 프로그램의 동작은 일관되게 유지되어야 한다.
위반의 경우
class Bird {
public void fly() {
System.out.println("새가 날아갑니다.");
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("타조는 날 수 없습니다!");
}
}
// 클라이언트 코드
public void letBirdFly(Bird bird) {
bird.fly();
}
// 실행
Bird ostrich = new Ostrich();
letBirdFly(ostrich); // Exception 발생
위반하지 않는 경우
interface Bird {
void sound();
}
class FlyingBird implements Bird {
public void sound() {
System.out.println("새가 소리를 냅니다.");
}
public void fly() {
System.out.println("새가 날아갑니다.");
}
}
class Ostrich implements Bird {
public void sound() {
System.out.println("타조가 소리를 냅니다.");
}
}
// 클라이언트 코드
public void letBirdSound(Bird bird) {
bird.sound();
}
// 실행
Bird ostrich = new Ostrich();
Bird sparrow = new FlyingBird();
letBirdSound(ostrich); // "타조가 소리를 냅니다."
letBirdSound(sparrow); // "새가 소리를 냅니다."
Bird를 인터페이스로 분리하고, FlyingBird와 Ostrich를 개별적으로 설계하여 날 수 있는 새와 날지 못하는 새를 구분.
-> 하위 클래스가 상위 클래스를 치환해도 문제 발생x
Liskov Substitution Principle를 통하여 하위 클래스가 상위 클래스의 계약을 준수하므로, 코드를 재사용하기 쉽다. 변경 사항이 상위 클래스와 상위 클래스와 하위 클래스 간에 일관되게 작동하므로, 코드 수정이 덜 복잡해집니다.
상위 클래스 기반의 테스트 코드가 하위 클래스에서도 동일하게 작동하므로, 테스트 케이스를 재사용할 수 있다.
다형성을 활용해 상위 클래스와 하위 클래스를 자유롭게 교체하며 동작의 일관성을 확인.Payment라는 상위 인터페이스를 작성하세요.
public interface Payment {
void pay(int amount);
}
두 개 이상의 구현체를 작성합니다.
CardPayment (카드 결제)CashPayment (현금 결제)클라이언트 코드는 Payment 타입만 사용하도록 작성하세요.
public class PaymentProcessor {
public void processPayment(Payment payment, int amount) {
payment.pay(amount);
}
}
다양한 구현체를 사용해 클라이언트 코드의 동작을 검증합니다.
Spring 프로젝트에서도 리스코프 치환 원칙을 준수하도록 설계 연습을 할 수 있습니다.
인터페이스 기반 서비스 설계
NotificationService 인터페이스를 작성.public interface NotificationService {
void send(String message);
}다양한 구현체 작성
EmailNotificationService (이메일 발송)SmsNotificationService (SMS 발송)Spring Bean으로 등록
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String message) {
System.out.println("Email sent: " + message);
}
}
@Service
public class SmsNotificationService implements NotificationService {
@Override
public void send(String message) {
System.out.println("SMS sent: " + message);
}
}
클라이언트 코드에서 인터페이스로 주입받기
NotificationService만 사용해 동작 확인.하위 클래스 교체를 통해 동작 검증
@Primary 또는 프로파일을 사용하여 환경에 따라 구현체를 교체.JUnit을 사용해 상위 클래스와 하위 클래스 간의 동작 일관성을 확인하는 테스트 작성.
@Test
void testNotificationService() {
NotificationService service = new EmailNotificationService();
service.send("Hello via Email");
service = new SmsNotificationService();
service.send("Hello via SMS");
}
하위 클래스에서 새로운 동작을 추가해도 상위 클래스의 동작을 위반하지 않는지 검증.
@Qualifier나 @Primary를 활용하면 여러 구현체 간의 동작 교체를 쉽게 실습할 수 있습니다.위 실습을 통해 LSP 준수 설계 경험과 함께 Spring 환경에서의 활용 방법까지 익힐 수 있습니다.