좋은 객체지향설계의 5가지 원칙 : SOLID
- SRP : 단일 책임 원칙(single responsibility principle)
- OCP : 개방-폐쇄 원칙(Open/closed principle)
- LSP : 리스코프 치환 원칙(Liskov substitution principle)
- ISP : 인터페이스 분리 원칙(Interface segregation principle)
- DIP : 의존관계 역전 원칙(Dependency inversion principle)
LSP : 리스코프 치환 원칙
서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.
로버트 C. 마틴(밥아저씨)
- 하위 클래스의 인스턴스는 상위형 객체 참조변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
- 프로그램의 객체는 프로그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 단순히 컴파일에 성공하는 것을 넘어서서, 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 그래서 기능적으로 보장이 되어야 한다는 것을 말한다.
- 하위 클래스의 인스턴스는 상위형 객체 참조변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
자바에서는 Collection Framework라는게 있다.
Collection 안에는 Map, List, Set이 각각의 장단점과 특징을 가지고 존재하고 있고,
Map 안에는 HashMap, TreeMap 등이
List 안에는 ArrayList, LinkedList 등이
Set 안에는 TreeSet, HashSet 등이 있어 설계의 방향성을 가지고 내가 원하는 대로 구현할 수 있다.
Map, List, Set은 Collection의 공통 메서드들을 일관되게 수행할 수 있어야 하고,
Map map = new HashMap();과 같이 하위 인스턴스는 상위형 인스턴스의 역할을 하는 데 문제가 없다.
다시 돌아가서, 이번에는 LSP 원칙을 지키지 못한 계산기를 살펴보자.
public class Calculator {
public int calculate(AbstractOperation operation, int firstNumber, int secondNumber) {
if(operation instanceof DivideOperation){
if(secondNumber == 0){return -99999;}
}
return operation.operate(firstNumber, secondNumber);
}
계산기 클래스
public class DivideOperation extends AbstractOperation {
public int operate(int firstNumber, int secondNumber) {
return firstNumber / secondNumber;
}
}
나누기 연산 클래스
문제점
무슨 문제가 있을까?
Calculator 안에는 사칙연산을 할 수 있는 연산이 모두 들어가 있는데, 한가지 문제점이 있다.
'0'으로 나누면 ArithmeticException이 터진다는 것.
그래서 계산기 클래스에서 instanceOf로 일일히 타입을 확인하고 있고, 자식이 완전히 부모클래스의 인스턴스 역할을 수행한다고 볼 수 없는 것이다.
해설
9조에 있는 성민이, 선연이, 성재, 영준이가 있다.
사람이니까 무조건 밥은 먹고 살아야 하지만, 그중 제일 악질인 영준이는 편식을 심하게 해서 고기가 아니면 안 먹는다고 한다. 그래서 영준이를 위한 편식 코드를 9조 전체에 넣어 다음과 같이 나타냈다.
이를 DIP를 위반한 코드로 나타내면 다음과 같다.
Abstract class 9조{
abstract void eatVegetable(Person p){
if (p instanceOf '영준') System.out.println("현기증 나요, 빨리 고기 주세요");
else System.out.println("맛있게 먹겠습니다.");
}
}
애꿎은 편식충 영준이때문에 불행하게도 성민, 선연, 성재는 밥을 먹을때마다 편식을 하는지 검사를 당해야 한다.
혹은 영준이만 고기를 먹는다고 영준이에 맞춰서 코드를 짜버리면(추후 업데이트 예정) 계속 다른 사람들은 의도하지 않았던 예외처리를 당하게 된다.
다시 돌아가서
LSP를 위반한 계산기에서는 무엇을 해 줘야 할까? 답은 다음과 같다.
- 부모 와 자식 클래스 사이의 행위가 일관성이 있도록 추상화를 좀 더 정교하게 구현한다.
- 연산 기능을 추상화한 부모 클래스에 피연산자 값의 유효성 검사를 진행하는 메서드를 추가해 준다.
- 계산기 클래스에서는 이 메서드를 사용하여 유효성 검사를 진행하고 이 유효성 검사가 필요한 자식 클래스에서는 이 추가된 유효성 검사 조건을 구체화한다.
TROUBLESHOOTING
불효자 나눗셈 계산기에는 불행하게도 0으로 나누면 ArithmeticException이 터지는 특징이 존재한다.
이를 방지하고자 isValid 메서드를 만들어주기로 했다. 그러나 고민했던 부분은 대체 이 isValid 메서드를 어디서 구현하고, 어디서 사용해야하는지가 문제였다.
1) Calculater 자체에서 isValid 메서드로 구현하기
2) AbstractOperation에서 isValid 메서드 구현하고 하위 클래스에서 isValid 체킹해주기
3) 어짜피 DivideOperation에서만 사용하니 DivideOperation에서 isValid 구현해~버리기
정말 오래 고심한 결과 다음과 같은 해결방법이 나왔다.
- AbstractOperation에서 isValid를 true로만 return하기
- DivideOperation에서 오버라이딩으로 isValid 구현하기
- Calculater에서 isValid 메서드로 유효성 검사하기
그래서 다음과 같은 결과물이 나왔다.
abstract class AbstractOperation {
public boolean isValid(int firstNumber, int secondNumber){return true;}
public abstract int operate(int firstNumber, int secondNumber);
}
AbstractOperation에서는 추상메서드인 operate와 true값을 반환하는 isValid메서드를 구현해놓는다.
isValid는 왜 추상화 안시켰죠? 라고 할 수 있는데, isValid를 추상화시키면 AbstractOperation을 상속받는 모든 메서드에서 무조건적으로 오버라이딩 해서 구현해줘야 하기 때문에 번거롭다.
class DivideOperation extends AbstractOperation {
@Override
public int operate(int firstNumber, int secondNumber) {
return firstNumber/secondNumber;
}
@Override
public boolean isValid(int firstNumber, int secondNumber) {
if (secondNumber==0) return false;
return true;
}
}
DivideOperation에서는 isValid를 오버라이딩하여 메서드를 구체화시킨다.
isValid에서
class Calculator {
// 연산 기능을 추상화된 부모클래스에 의존하여 처리한다.
public int calculate(AbstractOperation operation, int firstNumber, int secondNumber) {
if (!operation.isValid(firstNumber, secondNumber)) return -99999;
return operation.operate(firstNumber, secondNumber);
}
}
마지막으로 Calculator에서는 isValid 조건만을 이용하여 유효성 검사를 진행해준다.
이렇게 되면 새로운 연산 클래스를 만들어도 isValid를 구현해서 새로 만들어 주기만 하면 되니 만사OK다.
이 구현방법을 생각하고 너무나 기뻤다. 이래서 개발을 하는가 싶을 정도로.
인터넷에서는 대부분 Interface만을 이용하는 방식을 사용하기도 했고,
Abstract -> Divide -> Calculator가 아닌 2단 방식을 이용해 어떻게 해야 하나 정말 고민했는데
정말 오래 기억나는 순간이 될 것 같다.