SOLID 원칙, 리스코프 치환의 원칙(Liskov Substitution Principle)
리스코프 치환의 원칙을 깊게 공부하고 Solid 원칙에 대해 공부해봅시다! 이미 알고 계신 분들은 한번 리프레쉬 해보는 건 어떨까요?
이전 시간에 단일 책임의 원칙과 개방 폐쇄의 원칙을 알아 보았습니다.
리스코프 치환 원칙(LSP)은 (강한) 행동 하위 유형이라고 불리는 하위 유형 관계에 대한 정의를 나타냅니다. 객체 S가 객체 T의 하위 유형이라고 가정하면, T의 본질적인 특성을 변경하지 않고 T형의 객체들을 S형 객체로 대체할 수 있다는 것을 나타냅니다.
해당 내용을 글로 이해하긴 어려우니 코드로 볼까요?
Employee 클래스가 있다고 가정해보겠습니다.
package com.blackdog.solid.liskov;
public class Employee {
public String getTitle() {
return "The employee's title";
}
public void work() {
System.out.println("Employee is working");
}
}
Employee 클래스를 상속하는 하나의 클래스가 있다고 가정해보겠습니다. EmployeeOnVacation 클래스
package com.blackdog.solid.liskov;
public class EmployeeOnVacation extends Employee {
@Override
public void work() {
throw new IllegalArgumentException("Employees on vacation should not work");
}
}
그리고 이러한 Employee가 work 해야 진행할 수 있는 Project 클래스가 있다고 가정해보겠습니다.
ackage com.blackdog.solid.liskov;
import java.util.List;
public class Project {
public void start(List<Employee> employees) {
for(Employee employee:employees) {
employee.work();
}
}
}
Employee에게 Project를 진행할 수 있도록 일을 아래와 같이 시킵니다.
List<Employee> employees = new ArrayList<>();
employees.add(new EmployeeOnVacation());
employees.add(new Employee());
Project project = new Project();
project.start(employees);
해당 코드를 실행했을 때의 결과는 EmployeeOnVacation 클래스의 work 메소드에 의해서 프로젝트가 완료되지 못하여 Exception이 발생할 것입니다. EmployeOnVacation은 휴가중인 직원이지만(일을 안하는) 직원이긴 직원일 것입니다. 비록 해당 클래스의 메소드가 Exception을 발생시키지 않고 아무일도 일어나지 않게 변경하더라도 팀의 프로젝트 속도는 처음 생각했을 때의 속도만큼 빠르게 진행되지 못할 것입니다.
원칙 위반을 피하기 위해 클래스 상속 구조를 변경하고 다른 접근 방식으로 코드를 수정해보겠습니다.
기존의 Employee 클래스를 아래와 같이 변경해보겠습니다.
package com.blackdog.solid.liskov;
public class Employee {
public String getTitle() {
return "The employee's title";
}
}
그리고 두개의 인터페이스를 설계해보도록 하겠습니다.(WorkingEmployee, NonWorkingEmployee)
public interface WorkingEmployee {
public void work();
}
public interface NonWorkingEmployee {
void relax();
}
package com.blackdog.solid.liskov;
public class WorkingEmployeeImpl extends Employee implements WorkingEmployee {
@Override
public void work() {
// 무슨일을 한다..
}
}
package com.blackdog.solid.liskov;
import java.util.List;
public class Project {
public void start(List<WorkingEmployee> workingEmployees) {
for(WorkingEmployee workingEmployee : workingEmployees) {
workingEmployee.work();
}
}
}
List<WorkingEmployee> employees = new ArrayList<>();
employees.add(new WorkingEmployeeImpl());
Project project = new Project();
project.start(employees);
위의 예제에서 본대로 Employee는 일을 하던 안하던 Employee입니다. 하지만 Project에서 활용할 수 있는 Employee는 WorkingEmployee가 됩니다. 일을 하는 특성과 일을 하지 않는 특성을 인터페이스로 분리하였고, 인터페이스의 다형성을 통해 자식타입인 WorkingEmployeeImpl로 인해 프로젝트를 진행하는 work 메소드에는 문제가 없음을 보장합니다. 왜냐하면 Project를 하는 타입은 WorkingEmployee 인터페이스를 구현해야 하니까요.