객체 지향의 기본은 객체에게 할당하는 데 있다. 객체를 객체로 존재하게 하는 이유가 책임인데, 단익 책임 원칙은 책임과 관련된 원칙이다. 단익 책임 원칙은 다음과 같은 간단한 규칙이다.
클래스는 단 한 개의 책임을 가져야 한다.
클래스가 여러 책임을 갖게 되면 그 클래스는 각 책임 마다 변경되는 이유가 발생하기 때문에 클래스가 한 개의 이유로만 변경되려면 클래스는 한 개의 책임만 가져야 한다. 그런데 단일 책임 원칙은 가장 어려운 원칙이기도 하다. 한 개의 책임에 대한 정의가 명확하지 않고, 책임을 도출하기 위해 서는 다양한 경험이 필요하기 때문이다.
책임의 개수가 많아 질수록 한 책임의 기능 변화가 다른 책임에 주는 영향은 비례해서 증가하게 디는데, 이는 결국 코드를 절차 지향적으로 만들어 변경을 어렵게 만든다.
확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다. 기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않는다.
추상화와 다형성을 이용해서 개방 폐쇄 원칙을 구현하기 때문에, 추상화와 다형성이 제대로 지켜지지 않은 코드는 개발 폐쇄 원칙을 어기게 된다. OCP를 어기는 코드는 전형적인 특징은 다음과 같다
- 다운 캐스틍을 한다.
- 비슷한 if - else 블럭이 존재한다.
앞서 OCP 원친은 추상화와 다형성을 이용해서 구현 했는데, 리스코프 치환 원칙은 개방 폐쇄 원칙을 받쳐 주는 다형성에 관한 원칙을 제공한다.
상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상작동 해야한다.
// Sueper 상위 클래스, Sub 하위 클래스
public void someMethod(Super sc){
sc.someMethod();
}
someMethod()는 상위 타입인 Super 타입의 객체를 사용 하고 있는데, 이 메서드에 다음과 같이 하위 타입의 객체를 전달해도 someMethod()가 정상적으로 동작해야 한다는 것이 리스코프 치환 원칙이다.
someMethod.(new Sub());
리스코프 치환 원칙이 제대로 지키지 않으면 다형성에 기반한 개방 폐쇄 원칙 역시 지키지 않기 때문에, 리스코프 치환 원칙을 지키는 것은 매중 중요하다.
리스코프 치환 원칙은 확장에 대한 것이다. 리스코프 치환 원칙을 어기면 개방 폐쇄 원칙을 어길 가능성이 높아진다.
- 명시된 명세에서 벗어난 값을 리턴한다.
- 명시된 명세에서 벗어난 익셉션을 발생한다.
- 명시된 명세에서 벗어난 기능을 수행한다.
리턴 값은 0이나 또는 그 이상을 리턴하도록 정의되어 있는데 하위 타입에서 음수 값을 리턴한다거나, IOException만 발생시킨다고 정의했는데 IllegalAgumentException을 발생 시킨다든가 하는 식이다. 하위 타입이 이렇게 명세에서 벗어난 동작을하게 되면, 이 명세에서 기반해서 구현한 코드는 비정상적으로 동작할 수 있기 때문에, 하위 타입은 상위 타엡에서 정의한 명세를 벗어나지 않는 범위에서 구현 해야한다.
리스코프 치환 원칙을 어기게 되면 개방 폐쇄 원칙을 어길 사능성이 높아진다.
class Coupon {
public int calculaeDiscountAmount(Item item){
return item.getPrice() * discountRate;
}
}
- 이 코드는 Coupon 클래스의 calculaeDiscountAmount() 메서드 Item 클래스의 getPrice()메서드를 이용해서 할인될 값을 구하고 있음.
- 특수 Item이 생겼고 Item은 무조건 할인을 해주지 않는 정책이 추가되어, 이를 Item 클래스를 상속받는 SpecialItem 클래스를 추가했함
class Coupon {
public int calculaeDiscountAmount(Item item){
if(item instanceof Speialitem) return 0; //LSP 위반 발생
return item.getPrice() * discountRate;
}
}
- 해당 코드는 아주 흔한 리스코프 치환 원칙 위반 사례
- Item 타입을 사용하는 SpeicialItem 타입이 존재하는지 알 필요 없이 오직 Item 타입만 사용 해야한다.
- 위의 코드 instanceof 연산자를 사용해서 SpeicialItem 여부를 확인하고 있다는 것은 하위 타입인 SpeicialItem이 상위 타입인 Item을 완벽하게 대처하지 못하는 상황이 발생하고 있다는 것이다.
- instanceof는 리스코프 치환 원칙을 위반할 때 발생하는 증상이다.
- instanceof 연산자를 사용했다는 것은 상위 타입(Item 클래스)만을 사용해서 프로그래밍 할 수 없다는 것을 뜻하며
- 이는 하위 타입이 상위 타입을 대체할 수 없다는 것을 의미한다.
- 이는 SpeicialItem과 같은 새로운 종류의 하위 타입이 생길 때마다 상위 타입을 사용하는 코드를 수정해줘야 할 가능성이 높아지고 이것은 OCP를 준수하지 못한다.
- Item을 확장장한 SpeicialItem을 추가 하는 과정에서 Coupon의 수정은 닫혀 있어야 하는데 item을 확장하면서 coupon을 함께 수정, 이는 리스코프 치환 원칙을 지키지 않게 되면, 향후 가능을 변경하거나 확장할 때 더 많은 코드를 수정할 가능성이 높아지게 되는 것이다.
리스코프 치환 원칙을 어기게 되는 이유는 Item에 대한 추상화가 덜 되었기 때문이다. 할인되지 않은 상품 타입이 추가되었다는 것은 이후에 비슷한 요구가 발생할 수 있는 가능성이 높음을 뜻한다.
class Item {
// 변화가되는 기능을 상위 타입에 추가
public boolean isDiscountAmount(){
return true;
}
}
class SpeicialItem {
@Override
public boolean isDiscountAmount(){
return false;
}
}
class Coupon {
public int calculaeDiscountAmount(Item item){
if(item.isDiscountAvailable()) return 0; // instanceof 연산제 제거
return item.getPrice() * discountRate;
}
}
위 코드에서 Item 클래스에 가격 할인 가능 여부를 판단하는 기능을 추가하고 SpeicialItem 클래스는 이 기능을 알맞게 재정의 했다. 이렇게 변화되는 부분을 상위 타입에 추가함으로써 instanceof 연산자를 사용 하던 코드를 Item 클래스만 사용 하도록 구현 할 수 있게 되었다.
LSP 원칙이 지켜지지 않으면 쿠폰 예제에서 봤듯이 OCP 원칙을 지킬 수 없게 된다.
용도에 맞게 인터페이스를 분리하는 것은 단일 책임 원칙과도 연결 된다. 단일 책임 원칙에서 봤듯이 하나이상의 타입에 여러 기능이 섞여 있을 경우 한 기능의 변화로 인해 다른 기능이 영향을 받을 가능성이 높아진다. 따라서 클라이언트 입장에서 사용하는 기능만 제공 하도록 인터페이스를 분리 함으로써 한 기능에 대한 변경의 여파를 최소화 할 수 있게 된다.
인터페이스는 그 인터페이스를 사용하는 클라언트를 기준을 분리 해야한다.
용도에 맞게 인터페이스를 분리하는 것은 단일 책임 원칙과도 연결된다. 단일 책임 원칙에서 봤듯이 하나의 타입에 여러 기능이 섞여 있을 경우 한 기능의 변화로 인해 다른 기능이 영향을 받을 가능성이 높아진다. 따라서 클라이언트 입장에서 사용하는 기능만 제공하도록 인터페이스를 분리함으로 써 한 기능에 대한 변경의 여파를 최소화할 수 있게 된다.
인터페이스를 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하라는 원칙이다. 우리는 2장 의존성의 양면성 에서 A가 B를 의존할 경우 B의 변화로 인해 A가 변경되지만, 반대로 A의 요구사항에 의해서 B가 변경됨을 알 수 있었다.
이는 인터페이스를 분리하는 기준이 클라이언트(엑터)가 된다는 것을 뜻한다. 각 클라이언트가 사용하는 기능을 중심으로 인테페이스를 분리함으로써, 클라이언트로 부터 발생하는 인터페이스의 변경의 여파가 다른 클라이언트에 미치는 영향을 촤소화할 수 있게 된다.
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준의 모듈이 고수준 모듈에 정의한 추상 타입에 의존 해야한다.
고수준 모듈은 상대적으로 큰틀 (상위 수준)에서 플그램을 다룬다면, 저수준 모듈은 각 개별 요소(상세)가 어떻게 구현될지 에대해서 다룬다. 프로젝트 초기에 요구사항이 어느정도 안정화되면 이후부터는 큰틀에서 프로그램이 변경되기 보다는 상세 수준의 변경이 발생항 가능성이 높아진다.
단칙 책임 원칙과 인터페이스 분리 원칙은 객체가 커지지 않도록 막아 준다. 객체가 많은 기능을 가지게되면, 객체가 가진 기능의 변경 여파가 그 객체의 다른 기능에까지 번지게되고 이는 다시 다른 기능을 사용하는 클라이언트에까지 영향을 준다. 객체가 단일 책임을 갖게 하고 클라이언트 마다 다른 인터페이스를 사용하게 함으로써 한 기능의 변경이 다른 곳까지 미치는 영향을 최소화할 수 있고, 이는 결국 기능 변경 보다 쉽게 할 수 있도록 만들어 준다.
리스코프 치환 원칙과 의존 역전 원칙은 개방 폐쇄 원칙을 지원한다. 개방 폐쇄 원칙은 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능 확장을 하면서 기존 코드를 수정하지 않도록 만들어준다. 여기서, 변화되는 부분을 추상화할 수 있도록 도와주는 원칙이 바로 의존 역전 원칙이고, 다형성을 도와주는 원칙이 리스코프 치환 원칙인 것이다.
또한, SOLID 원칙은 사용자 입장에서의 기능을 사용을 중시한다. 인터페이스 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하고 있으며, 의존 역전 원칙 역시 저수준 모듈을 사용하고 고수준 모듈입장에서 추상화 타입을 도축하도록 유도한다. 또 리스코프 치환 원칙은 사용자에게 기능 명세를 제공하고, 그 명세에 따라 기능을 구현할 것을 약속한다. 이 처럼 SOLID 원칙은 사용자 관점에서 설계를 지향하고 있다.