위와 같이 추후 기능이 확장된다 하더라도 다운캐스팅 및 코드의 변경 없이도 의도한 대로 동작할 수 있어야 LSP 및 OCP를 지킬 수 있다.
인터페이스 분리 원칙 (Interpace Segregation Principle, ISP)
클라이언트는 자신이 사용하는 메서드에만 의존해야 한다.
= 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.
인터페이스 분리 원칙은 클라이언트에 대한 것이다.
C/C++같이 컴파일과 링크를 직접 해주는 언어를 사용할 때 장점이 잘 드러난다.
예를 들어 ArticleListUI, ArticleWriteUI, ArticleDeleteUI가 ArticleService 클래스의 헤더를 참조하고 있을 때, ArticleService 클래스에서 읽기와 관련된 메소드가 변경된다고 하면 재컴파일 후에 목적 파일이 재생성 된다. 여기서 읽기 기능만 변경되었음에도 불구하고 전혀 상관없는 ListUI, WriteUI도 ArticleService클래스의 헤더 파일을 참조하고 있기 때문에 다시 컴파일되며 목적 파일이 재생성 된다.
이런 경우 ArticleService클래스를 각 기능으로 나누어서 분리하면 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않는다.
UML로 그리면 위와 같다
직관적으로 보이듯이 단일 책임 원칙과도 연결된다.
의존 역전 원칙 (Dependency Inversion Principle, DIP)
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
추상화를 이용해서 DIP를 달성할 수 있다.
고수준 모듈과 저수준 모듈의 예를 들면 아래와 같다.
◾ 고수준 모듈
- 바이트 데이터를 읽어와 암호화 하고 결과 바이트 데이터를 쓴다.
◾ 저수준 모듈
- 파일에서 바이트 데이터를 읽어온다.
- AES 알고리즘으로 암호화한다.
파일에 바이트 데이터를 쓴다.
고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈이라고 정의할 수 있고 저수준 모듈은 고수준 모듈을 구현하기 위해 필요한 하위 기능의 실제 구현으로 정의할 수 있다.
즉, 고수준 모듈은 상위 수준에서 프로그램을 다룬다면 저수준 모듈은 상세가 어떻게 구현될지에 대해서 다루는 것이다.
프로젝트가 어느정도 안정화가 되면 상위 수준보다는 상세 수준에서 변경이 발생할 가능성이 높아진다.
한 가지 예시를 들어보자.
어떤 상품의 가격을 결정하는 정책이 있을 때 상위 수준(고수준 모듈)에서는 아래와 같은 결정이 내려질 수 있다.
◾ 쿠폰을 적용해서 가격 할인을 받을 수 있다.
◾ 쿠폰은 동시에 한 개만 적용 가능하다.
상위 수준에서의 쿠폰 정책은 한 번 안정화되면 쉽게 변하지 않지만 쿠폰은 상황에 따라 다양한 종류가 추가될 수 있다. 여기서 쿠폰을 이용한 가격 계산 모듈이 개별적인 쿠폰 구현에 의존하게 되면 새로운 쿠폰 구현이 추가되거나 변경될 때마다 가격 계산 모듈이 변경되는 상황이 발생된다.
저수준 모듈의 변경이 고수준 모듈의 변경을 초래하게 되는 것이다.
특정 클래스에 직접 의존하는 것이 아니라 추상화를 거쳐서 각 클래스들이 추상 타입에 의존하도록 만들면 저수준 모듈이 변경되더라도 고수준 모듈의 변경이 발생하지 않는다.
즉, DIP는 LSP와 OCP를 따르는 설계를 만들어 주는 기반이 된다.
추가로 DIP는 런타임에서의 의존이 아닌 소스 코드의 의존을 역전시킴으로써 변경의 유연함을 확보할 수 있도록 만들어주는 원칙이다.
정리
SOLID 원칙을 한 마디로 정의하자면 변화에 유연하게 대처할 수 있는 설계 원칙이다.
SRP와 ISP는 객체가 커지지 않도록 막아준다.
OCP는 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능 확장을 하면서 동시에 기존 코드를 수정하지 않도록 만들어준다. 여기서 변화되는 부분을 추상화할 수 있도록 도와주는 것이 DIP이고 다형성을 도와주는 것이 LSP이다.
절차 지향 프로그램은 다수의 프로시저들이 데이터를 공유하는 방식으로 만들어지기 때문에 자연스럽게 데이터를 중심으로 구현하게 된다. 그러다보니 프로그램의 규모가 커졌을 때, 프로시저간에 서로 공유되는 데이터의 타입이 변경되거나 데이터를 서로 다른 의미로 사용하고 있다면 코드의 유지 보수가 매우 어려워지기 시작한다.
객체 지향 프로그램은 절차 지향 프로그램과 다르게 데이터 뿐만 아니라 관련된 프로시저를 묶어서 객체라는 단위로 만든다. 객체는 자신만의 데이터와 프로시저를 가지고 자신만의 기능을 제공한다. 각 객체들은 서로 연결되어 다른 객체가 제공하는 기능을 사용할 수 있고 이때 프로시저는 자신이 속한 객체의 데이터에만 접근할 수 있으며 다른 객체에 속한 데이터에는 접근할 수 없다. (캡슐화)
객체 지향 프로그래밍에서 가장 중요한 것은 객체가 어떤 데이터를 가지고 있는지가 아니라 객체가 어떤 메세지를 주고 받는가이다.
객체 지향 프로그래밍의 4가지 특징
캡슐화
객체가 기능을 어떻게 구현하는지를 감추는 것이다.
내부의 기능 구현이 변경되더라도 그 기능을 사용하는 코드는 영향을 받지 않도록 만들어준다. 즉, 내부 구현 변경의 유연함을 주는 기법인 셈이다.
캡슐화는 하고싶다고 해서 자동으로 이뤄지는 것이 아니다.
그래서 캡슐화를 위한 2가지 규칙을 숙지하면 캡슐화된 코드를 작성하는 데 도움이 된다.
◾ Tell, Don't Ask. 데이터를 요구하지 말고 기능을 처리해 달라고 요청한다.
if (REGULAR == acc.getMembership()) { ... }
위와 같이 데이터를 받아서 처리하면 동일한 코드가 많아질 수록 기능변경시 문제가 된다.
if (acc.hasRegularPermission()) { ... }
데이터를 받아서 처리하는 대신에 판단을 요청해서 처리하면 기능을 변경하더라도 해당 함수의 구현부만 수정하면 되기 때문에 유지보수에 매우 용이해진다.
◾ Demeter's Law.해당 객체의 메소드만 호출해야 한다. 해당 객체가 어떤 데이터를 가지고 있는지 몰라야 한다.
(메서드에서 생성한 객체, 파라미터로 받은 객체, 멤버로 참조하는 객체)
acc.getExpData().isAfter(now);
위와 같은 코드는 호출하는 객체의 내부 데이터 구조를 알기때문에 작성할 수 있는 코드이다.
acc.isExpired();
대신 이렇게 수정해서 해당 객체의 메소드만 호출하도록 한다.
디미터 법칙을 어기는 전형적인 증상은 연속된 get 메서드를 호출하거나 임시 변수의 get 호출이 많은 경우이다. 이 증상들이 보인다면 디미터 법칙을 어기고 있을 가능성이 높고 캡슐화를 약화시켜서 코드 변경을 어렵게 만드는 원인이 될 수 있다.
참고로 컨테이너와 같은 자료 구조의 경우에는 적용할 필요가 없다.
상속
상위 클래스의 기능을 물려받으면서 기능을 변경하거나 추가할 수 있도록 해주는 방법이다. 재사용이 쉽지만 대신 몇 가지 단점이 존재한다.
◾ 상위 클래스의 변경이 어렵다
상위 클래스를 변경하면 의존하는 하위 클래스들도 영향을 받기 때문에 시간이 지날수록 하나의 거대한 단일 구조처럼 만들어지는 결과가 발생할 수 있다.
◾ 클래스의 불필요한 증가가 발생한다
단순한 기능이더라도 경우의 수가 많아지거나 조합이 다양해질수록 파생 클래스가 계속 생성된다.
◾ 상속의 오용이 발생할 수 있다
예를 들어 STL 컨테이너를 상속 받아서 구현하는 경우에 필요하지 않은 메소드들까지 모두 상속될 뿐만 아니라 외부에서 잘못 사용할 여지가 생긴다.
상속 대신 컴포지션으로 구현하면 단점을 많이 상쇄시킬 수 있다.
또한 상속은 컴파일 타임에만 알고리즘을 변경할 수 있지만 컴포지션은 런타임에 교체가 가능해진다.
그렇다면 상속은 언제 사용하는게 좋을까?
재사용이라는 관점이 아니라 기능의 확장이라는 관점에서 사용하는 것이 좋다. 그리고 명확한 IS-A 관계일 때 적용하는 것이 옳다. 즉, 명확한 IS-A 관계에서 기능을 점진적으로 확장시켜 나가고 싶을 때 사용하면 된다.
다형성
한 객체가 여러 가지 타입을 가지는 것을 의미한다. 다형성을 구현하려면 상속을 이용해야 한다.
크게 구현 상속, 인터페이스 상속으로 나눌 수 있다.
구현 상속은 보통 상위 클래스에 정의된 기능을 재사용하기 위한 목적으로 사용되고 인터페이스 상속은 기능의 구현을 강제하기 위한 목적으로 사용된다.
추상화
데이터나 프로세스 등을 의미가 비슷한 개념이나 표현으로 정의하는 과정이다. 좀 더 쉽게 얘기하면 공통 분모를 찾아서 하나로 묶는 것이다. 그렇기에 상속 및 다형성의 개념이 자연스럽게 포함된다.
뿐만 아니라 많은 책임을 가진 객체로부터 책임을 분리하는 촉매제가 되기도 한다.
요구 사항이 바뀔 때 변화하는 부분을 추상화시키면 향후 유연하게 대처할 수 있는 가능성이 높아진다.