리스코프 치환 원칙(LSP)

박시시·2022년 9월 17일
0

SOLID

목록 보기
2/5

리스코프 치환 원칙에 대해 로버트 C.마틴은 아래와 같이 정의했다.

Subtypes must be substitutable for their base types.

즉 서브타입 객체는 반드시 상위 타입 객체를 대체 가능해야 한다는 것이 리스코프 치환 원칙의 핵심이다.

모바일페이 애플리케이션 예제를 통해 리스코프 치환 원칙을 지키지 않는 예를 살펴보고 어떻게 해당 원칙을 준수할 수 있도록 설계해야 하는지 알아보자.

리스코프 치환 원칙을 준수하지 않은 케이스

모바일페이 애플리케이션에는 MobilePayService라는 클래스가 있고 페이 기능을 제공한다. 아래의 그림을 살펴보자.

Samsung과 Apple은 추상 클래스인 Device를 extend하였다. 새로운 디바이스가 추가된다면 코드 상에서 수정 없이 새로운 디바이스 타입을 확장하면 된다는 점에서 이 설계는 개방/폐쇄 원칙을 준수한다.

코드를 살펴보자.

// Device.java
public abstract class Device {
	protected abstract void deposit(BigDecimal amount);

	protected abstract void pay(BigDecimal amount);
}
// MobilePayService.java
public class MobilePayService {
	private Device device;

	public MobilePayService(Device device) {
		this.device = device;
	}	

	public void pay(BigDecimal amount) {
		device.pay(amount)
	}
}

이제 새로운 디바이스 타입을 추가해보자. 바로 LG이다.

public class Lg extends Device {
	// overriden methods
}

하지만 안타깝게도 LG는 디파짓 기능만 제공하고 아직 페이서비스를 제공하지 않는 상태이다. Device 추상클래스를 확장했으니 pay() 메서드 역시 오버라이드 해야 한다. 해결책으로 pay() 메서드 사용시 에러를 발생시키는 코드를 추가해보자.

public class Lg extends Device {
	@Override
	protected void deposit(BigDecimal amount) {
		// 코드 작성
	}

	@Override
	protected void pay(BigDecimal amount) {
		throw new UnsupportedOperationException("pay service is not supported")
	}
}

이 새로운 클래스를 MobilePayService와 함께 사용해보자.

Device myLg = new Lg();
myLg.deposit(new BigDecimal(1000.00));

MobilePayService payService = new MobilePayService(myLg);
payService.pay(new BigDecimal(100.00));

위 코드는 "pay service is not supported" 라는 메시지와 함께 에러를 던진다.

MobilePayService에서는 Device와 서브타입 모두에서 pay 메서드가 원활히 작동할 것이라 예상한다. 하지만 Lg 디바이스에서는 pay 메서드를 지원하지 않음으로써 이 메서드 명세를 위반한다. 즉 Lg 클래스는 Device를 확실히 대체할 수 없다. 즉 리스코프 치환 원칙을 위반한 셈이다.

리팩토링

Device의 설계는 모든 Device 타입이 pay 기능을 지원한다고 잘못 가정했다. Device 타입은 pay 기능을 지원하는 타입과 그렇지 않은 타입으로 재설계 되어야 한다.

이러한 재설계를 통해 MobilePayService, 즉 클라이언트 코드에서 PayableDevice를 구현한 객체를 예상에 어긋남없이 신뢰를 갖고 사용할 수 있게 되었다. 또한, 더욱 중요한 건, 각 서브타입 객체는 상위 객체를 완벽히 대체할 수 있게 되며 리스코프 치환 원칙을 준수하게 되었다는 점이다.

코드로 살펴보자.

public class MobilePayService {
    private PayableDevice payableDevice;

    public MobilePayService(PayableDevice payableDevice) {
        this.payableDevice = payableDevice;
    }

    public void pay(BigDecimal amount) {
        payableDevice.pay(amount);
    }
}

이러한 설계를 통해 좀 더 객체지향적인 코드를 작성할 수 있게 되었다.

0개의 댓글