객체 사이의 의존성이 과한 경우 결합도(coupling)이 높다고 말한다.
의존성이 있을 경우, 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포되어 있다.
결합도를 낮추는 방법으로(객체의 자율성을 높이는 방식으로) 캡슐화
가 있다. 객체 내부에 세부적인 사항을 감추는 것을 캡슐화라고 한다. 즉, 외부에 존재하는 프로세스를 내부로 감추는 것이다. 간단한 예시로 티켓을 판매하는 프로세스를 들 수 있다. Theather에서 필드로 가지는 TicketSeller를 통해 티켓을 꺼내고, Audience의 돈을 꺼낼 수도 있다. 하지만 이러한 일련의 과정을 TicketSeller의 sellTo 메서드에 넣어서 캡슐화를 할 수 있다. 이러면 Theather은 TicketSeller의 sellTo 메서드만 호출하면 되지, Audience가 무슨 함수를 가지고 있는지, TicketSeller은 어떤 방식으로 Ticket을 판매하는지 몰라도 된다.
이를 Theather은 오직 TicketSeller의 인터페이스
에만 의존한다고 한다. 이처럼 객체를 인터페이스
와 구현(implementation)
으로 나누고 인터페이스만을 공개하는 것은 객체 사이의 결합도를 낮춘다.
데이터: 객체 안의 필드
프로세스: 데이터를 다루는 행위
예를 들어서 티켓을 사는 것은 프로세스고, 티켓은 데이터이다. 티켓 구매 프로세스를 객체 바깥에서 한다면, 이는 책임을 다른 객체에게 넘기는 것이고 결합도가 높아진다.
도메인이란, 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 뜻한다. 예를 들어 영화를 쉽고 빠르게 예매하려는 문제가 있다. 이를 위해 영화 예매 분야(도메인)을 이용할 수 있다.
도메인의 구조를 정하고, 이와 유사하게 클래스의 구조를 정해야 한다. 그 다음 설계 과정을 거친 뒤 코드를 짜기 시작한다.
필드는 private으로, 메서드는 public으로 두어야 한다. 그를 통해 외부에서는 객체의 속성에 직접 접근할 수 없도록 막고, 적절한 public 메서드를 통해서만 내부 상태를 변경할 수 있게 해야 한다.
이처럼 클래스의 내부와 외부를 구분해야 객체의 자율성이 보장된다. 즉, 캡슐화를 넘어서 접근 제어를 통해 내부(구현)
과 외부(인터페이스)
를 구분해야 객체의 자율성을 지킬 수 있다. 이를 구현 은닉
이라고도 한다.
객체들 사이의 상호작용을 협력
이라고 한다. 객체지향에서의 협력은 객체들 사이의 요청
과 응답
으로 이루어진다. 요청의 방법은 메시지
를 보내는 것 뿐이다. 도착한 메시지를 처리하는 방법을 메서드
라고 한다. 즉, 객체에게 인터페이스
를 통해 메시지를 보내면, 객체 내부에서는 메서드를 선택해서 처리한다. 여기서 어떤 메서드로 처리할지 컴파일 시점이 아니라 실행 시점에 결정하는 것을 동적 바인딩
이라고 한다. 이와 같이 메시지와 메서드가 구분되어 있을 때, 다형성
이 성립한다. (다형성은 동적 바인딩을 통해 구현된다.)
어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나, 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다.
코드의 의존성과 실행 시점의 의존성은 다를 수 있다. DiscountPolicy라는 추상 클래스가 존재하고, 이 클래스를 상속받는 PercentDiscountPolicy가 있다. 코드 상에서 DiscountPolicy에게 의존할 수 있지만, 실행 시점에는 PercentDiscountPolicy에 의존할 수 있다. 이처럼 코드의 의존성과 실행 시점의 의존성이 다를 경우 확장성은 높아지지만, 코드를 이해하기는 어려워진다.
어떻게 이처럼 시점에 따라 의존성이 다를 수 있을까? 오직 calculateDiscountAmount라는 메시지를 받을 수만 있다면 객체가 어떤 클래스의 인스턴스인지는 상관하지 않기 때문이다. 이렇게 인터페이스에 의존하는 방식을 업캐스팅
이라고 한다.
상속은 코드를 재사용할 수 있다는 장점을 가진다. 하지만 단점이 있다.
캡슐화를 위반하는 이유는, 상속을 위해서 부모 클래스의 내부 구조를 잘 알고있어야 하기 때문이다. 즉, 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다. 캡슐화가 약화되면 부모 클래스가 변경될 때, 자식 클래스도 변경될 가능성이 높아진다.
설계를 유연하지 못하게 하는 이유는, 컴파일 시점에 어떤 객체가 사용될지 결정되기 때문이다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.
합성은 객체 간에 인터페이스를 통해 정의된 메시지를 통해서만 통신하는 것을 뜻한다. 실제적인 구현은 객체 내부에 담겨지므로 캡슐화를 이룰 수 있고, 인터페이스에만 의존하니 실행 시점에 객체를 변경할 수 있어 설계가 유연해진다.
객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용을 협력
이라고 한다. 협력의 유일한 방법은 메시지 전송 뿐이다. 메시지를 수신하면 메서드를 실행해 요청에 응답한다.
협력은 객체의 행동을 결정하고, 행동은 객체의 상태를 결정한다. 예를 들어서 영화 예매라는 협력을 위해 Movie는 요금을 계산하는 행동을 한다. 이 행동을 위해 Movie는 기본 요금인 fee와 할인 정책인 discountPolicy라는 상태를 가진다.
즉, 협력은 객체의 행동과 상태를 결정하는데 필요한 문맥
을 제공한다.
협력에 참여하기 위해 객체가 수행하는 행동을 책임
이라고 한다. 책임은 객체가 유지해야하는 정보와 수행할 수 있는 행동에 대해 개략적으로 서술한 문장이다. 책임은 메시지보다 개념적으로 크고 추상적이다. 객체에게 얼마나 적절한 책임을 할당하느냐가 설계의 전체적인 품질을 결정한다.
책임을 객체에게 나누기 위한 하나의 방법이다. 협력이라는 문맥을 정의하고, 그를 하나의 책임으로 본다. 그 다음 더 작은 책임과 책임을 맡을 수 있는 적절한 정보 전문가를 찾아내가는 방식을 말한다.
이렇게 책임을 찾아나가면서, 인터페이스와 오퍼레이션의 목록을 구할 수 있다. 이러한 책임에 초점을 맞춘 설계 방식을 책임 주도 설계라고 한다.
책임을 할당할 때는 두 가지 요소를 고려해야 한다.
정보 전문가 패턴은 객체의 상태(정보)를 고려하고, 책임에 맞는 객체를 선택하는 방법이다. 그렇다면 상태가 객체를 결정하지 메시지가 객체를 결정하는 방법이 아니지 않나?
객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할
이라고 한다. 책임을 통해 객체를 결정할 때, 하나의 책임을 여러 객체들이 수행할 수 있을 경우 이는 역할이 된다. 역할을 구현하는 가장 일반적인 방법은 인터페이스 또는 추상 클래스이다. 이 둘은 책임의 집합을 서술해 놓은 것인데, 추상 클래스는 그 일부를 구현해 둔 것이고 인터페이스는 책임의 집합만을 나열해 둔 것이다.
여러 시나리오를 고려해보고, 협력을 지속적으로 정제해 보면 그 협력들이 거의 유사한 구조를 보이게 된다. 그때 협력들을 합치면서 두 객체를 역할로 포괄할 수 있다.