이 글에서는 오브젝트에서 나온 내용을 기반으로 내가 이해한 객체지향설계에 대해 이야기해보려 한다.
소프트웨어는 하드웨어와는 달리, 요청한 기능을 구현하는 것에서 끝나지 않는다.
끊임없이 요구사항이 추가되고 개발자는 그에 맞추어 계속해서 소프트웨어를 변경해 나가야 한다.
그리고 많은 개발자들이 그 변경때문에 복잡한 코드를 읽고, 수정하고, 에러를 경험한다.
단순한 요구사항에 의해 코드 한줄을 수정했을 뿐인데 예상치못한 곳에서 에러가 발생한다거나, 하나의 기능을 추가하기 위해 수백 수천줄의 코드를 읽고 여러곳을 수정해야하는 경험은 개발자라면 한번쯤은 해보았을 것이다.
그러한 경험을 일찍이 해본 개발자들은 변경에 유연한 설계의 필요성을 절실히 깨달았고, 다양한 프로그래밍 패러다임이 생겨나고 발전해오게 되었다.
그리고 객체지향설계는 변경에 유연한 설계를 위한 다양한 프로그래밍 패러다임 중 하나이다.
"설계는 트레이드 오프다."
오브젝트의 저자가 지속적으로 전달하는 메시지이다.
상황에 따라서 절차적 프로그래밍이나 데이터 주도 프로그래밍이 적합할 수도 있다.
당분간 변경 소요가 없을 것으로 예상되고 빠르게 기능 구현이 필요하다면 절차적 프로그래밍 또한 나쁜 선택이 아니다.
때로는 응집도나 결합도를 서로 트레이드 오프해야 할 수도 있으며
무조건적으로 모든 객체에 대한 추상화를 만드는 것 또한 옳은 방법이 아니다.
다양한 프로그래밍 패러다임을 이해하고, 설계경험을 쌓음으로서 상황에 맞는 적절한 설계를 하는 것이 중요하다.
다만 변경이 잦게 일어나는 부분을 발견했다면 그 즉시 변경에 유연한 설계를 항상 고민하고 이행해야 한다.
객체지향설계는 메시지를 식별하고, 그 메시지를 처리할 책임을 가질 객체를 식별하여 부여하는 것으로부터 시작된다.
주문시스템을 만들 때, 무작정 주문 객체를 만들고 객체가 가져야할 상태와 행동을 고민하는 것이 아니라 '주문하라' 라는 메시지를 식별하고 그 메시지를 처리하기에 가장 적합한 객체를 찾아가야 한다.
흔히 프로그래밍을 할 때 Setter 를 쓰지 말라는 이야기를 많이 듣는다.
Setter의 무분별한 사용은 메시지의 식별이 우선되지 않고 객체의 상태에 기반한 설계 방식이다.
객체의 상태를 먼저 고민하고 그에 따른 행동을 뒤늦게 만들다보면 하나의 객체가 여러개의 책임을 수행하는 상황이 발생할 수 있다.
메시지를 먼저 찾고 그 뒤에 이를 수행할 책임에 적합한 객체를 찾는 방식은 자연스레 단일 책임 원칙을 따르게 한다.
단일 책임 원칙을 따르면 변경의 연쇄작용으로부터 보다 자유로워질 수 있다.
메시지를 식별하고 그 책임을 수행할 객체를 식별하다보면 여러 객체들간의 협력이 발생한다.
객체가 책임을 수행하기 위해 다른 객체와의 협력은 불가피하지만, 무분별한 협력은 변경을 힘들게 한다.
객체가 너무 많은 협력, 결합도를 가지고 있다면 이는 그 객체가 너무 많은 책임을 가지고 있다는 신호가 될 수 있다.
상황에 따라 해당 책임을 다른 객체에게 적절히 분배함으로서 결합도를 낮추고 응집도를 높이는 것이 필요하다.
추상화는 그저 추상 클래스 혹은 인터페이스를 분리하는 작업이 아니다.
변경이 잦고 불안정한 부분을 변경이 적고 안정적인 부분으로부터 분리하고 이를 퍼블릭 인터페이스로 추출하며 그 외의 것들을 캡슐화 하는 것이다.
퍼블릭 메서드/추상 클래스/인터페이스를 상황에 맞게 적절하게 활용하는 것이 중요하다.
본질은 추상 클래스를 만들고 인터페이스를 만드는 것이 아닌 변경에 유연한 설계를 지향하는 것이다.
추상화를 설명한 객체지향 원칙중의 하나가 개방 폐쇄 원칙이다.
개방 폐쇄 원칙은 추상화와 구현을 분리한 설계를 통해 기존의 추상화된 부분을 건드리지 않고 구현만을 추가하여 기능을 추가하거나 변경할 수 있음을 말한다.
추상화와 관련된 또 하나의 객체지향 원칙은 의존성 역전 원칙이다.
각 객체간의 협력을 설계할 때, 구현이 아닌 추상화에 의존하게 함으로서 객체간의 관계를 느슨하게 하는 것을 말한다.
코드의 재사용을 목적으로한 합성은 자식 클래스가 부모 클래스에 과하게 의존하게 함으로서 변경에 유연한 설계를 방해한다.
상속은 오로지 타입계층을 구현해야 할 때만 사용되어야 한다.
객체의 행동을 기반으로 일반화된 객체와 특수화된 객체의 행동을 추출할 수 있는 경우에 상속을 사용해야 한다.
리스코프 치환 원칙은 올바른 상속의 방법에 대해서 설명한다.
부모 클래스에 대해 클라이언트가 기대하는 모든 행동을 자식 클래스 또한 수행이 가능하도록 하는 설계를 통해 다형성을 얻을 수 있다.
인터페이스 분리 원칙은 잘못된 상속을 피하고 인터페이스의 분리를 통해 최소한의 인터페이스를 갖도록 한다.
클래스의 특정 인터페이스만을 필요로 한다면, 상속이 아닌 인터페이스의 분리를 통해 필요한 인터페이스만 상속받도록 하여 불필요한 결합을 피할 수 있다.
상속의 경우, 부모 클래스에 자식 클래스가 강하게 결합되어 변경에 유연하지 않지만
합성은 퍼블릭 인터페이스만 노출하기에 느슨한 결합으로 인해 변경에 유연한 설계에 용이하다.
물론 이는 합성 대상 객체의 캡슐화가 적절히 선행되어야 한다.
상속이 아닌 합성을 사용했다 하더라도, 해당 객체의 모든 상태와 메서드가 노출되어있다면 상속의 단점들을 제대로 보완할 수 없다.