객체는 협력을 위해 존재한다. 협력은 객체가 존재하는 이유를 설명하고 문맥을 제공한다.

유사한 요구사항을 계속 추가하는 상황에서 협력이 서로 다른 패턴을 따른다면 설계의 일관성이 무너지게 된다.

가능하면 유사한 기능을 구현하기 위해 유사한 협력 패턴을 사용하자. 약간의 부조화가 생겨도 시스템 전반적으로 일관성이 지켜진다면 이를 이해하고 확장하기 훨씬 편해진다.

이러한 관점에서 객체지향 패러다임의 장점은 아래와 같다.

  • 설계의 재사용
  • 일관성 있는 설계
    • 코드 가독성이 좋아지고 재사용성이 높아진다.

객체지향에서 일관성을 유지하는 방법을 예시 코드를 통해 살펴보자.

요구 사항


휴대폰 과금 시스템의 요금 정책을 구현한다. 이는 크게 2가지로 구분된다.

  • 기본 정책
  • 부가 정책

요금 정책은 기본 정책 1개와 부가 정책 여러 개로 이루어진다.

기본 정책

아래 4가지의 기본 정책이 있다.

조합 가능한 모든 경우의 수는 아래와 같다.

시간대/요일 별 방식 구현하기

e.g. 시간대별 정책

  • 00시 ~ 18시 : 10초당 18원
  • 18시 ~ 24시 : 10초당 15원
  • 18시 ~ 20시 통화한 경우

e.g. 여러 날에 걸쳐 통화한 경우

  • 이 경우 통화 시작/종료 시간만 알아선 안되고 시작/종료 일자도 알아야 한다.
  • 따라서, DateTimeInterval 이라는 VO 객체를 추가하자.

기존 Call 클래스의 from, to 필드를 DateTimeInterval로 대체 가능하다. 요금 계산 로직은 다음 2가지로 나뉘어야 한다.

  • 통화 기간을 날짜 별로 구분
  • 구분된 기간을 다시 시간대(또는 요일) 규칙에 따라 분리후 각자 계산

이렇게 설계를 하면 정보 전문가를 아래와 같이 나타낼 수 있다.

  • 통화 기간을 잘 아는 정보 전문가 : Call
  • 통화 기간 처리하는 방법을 아는 정보 전문가 : DateTimeInterval

시간대/요일 정책은 날짜 별로 분리된 List<DateTimeInterval>에 대해 루프를 돌면서 요금을 계산한다.

비일관성 설계 문제점

앞서 시간/요일별 구현은 아래 2가지 문제점이 있다.

  • 개념적으로는 연관되있으나 구현 방식은 제각각
  • 유사한 문제를 해결하고 있음에도 설계의 일관성이 없음

이처럼 비일관적인 설계는 다양한 문제를 야기한다. 일단, 새로운 구현 추가를 어렵게 한다. 비슷한 개념의 정책이 추가되는 경우 점점 더 일관성이 깨질 것이며 이는 곧 유연하지 못한 설계가 된다. 일관성이 깨지면 깨질수록 이해하기 어려운 코드가 된다. 결과적으로 유지보수성과 생산성이 떨어질 것이다.

따라서, 유사한 기능을 서로 다른 방식으로 구현해서는 안된다.

설계에 일관성 부여하기


협력을 일관성 있게 만들기 위해선 다음 지침을 따라야 한다.

  • 변하는 개념을 변하지 않는 개념으로부터 분리하라.
  • 변하는 개념을 캡슐화하라.

조건 로직 vs 객체 탐색

영화마다 여러 개의 할인 정책이 적용될 수 있다. 이를 if문을 통해 표현할 수 있지만 객체지향에선 아니다.객체지향 코드는 다형성 패턴을 사용하여 이를 구현하여 조건을 판단하지 않는다. 단지 다음 객체로 이동할 뿐이다.

각 조건문을 개별적인 객체로 분리했고 이 객체들과 일관성 있게 협력하기 위해 타입 계층을 구성했다. 변하는 개념을 변하지 않는 개념으로부터 분리했다고 볼 수 있다.

Movie는 협력하는 DiscountPolicy의 구체적인 타입을 알지 못한다. 단지, DiscountPolicy의 퍼블릭 인터페이스를 이해할 수 있을 뿐이다. 이는 변하는 개념을 캡슐화했다고 볼 수 있다.

참고 타입 캡슐화 + 낮은 응집도 유지 기법들
인터페이스 설계 원칙 : 효과적인 캡슐화
의존성 관리 기법 : 타입을 캡슐화하기 위해 낮은 결합도 유지
상속을 재사용 목적으로 사용 X : 캡슐화 저해 + 높은 응집도
LSP : 상속을 타입 계층 구현 목적으로 사용

이처럼 변경에 초점을 두고 캡슐화 관점에서 설계를 바라보면 일관성 있는 협력 패턴을 얻을 수 있다.

캡슐화


변하는 어떤 것이든 감추는 것. 정확히 말하면 소프트웨어 안에서 변할 수 있는 어떤 개념이라도 감추는 것이다. 설계에서 무엇이 변화될 수 있는지 고려하자.

우리는 필드에 private 접근 지정자를 적용하는걸 캡슐화라고 말한다. 이는 데이터 은닉일 뿐 진정한 의미의 캡슐화는 아니다. (데이터 은닉 ⊂ 캡슐화)

설계의 맥락에서 캡슐화를 논의할 때는 그 안에 항상 변경이라는 주제가 녹아있다.

종류

  • 데이터 캡슐화 : 인스턴스 변수의 가시성이 private
  • 메서드 캡슐화 : DiscountPolicygetDiscountAmount의 가시성이 protected
  • 객체 캡슐화 : 객체와 객체 사이의 관계를 캡슐화. 합성을 의미
  • 서브타입 캡슐화 : 서브타입의 종류를 캡슐화. 다형성의 기반

코드 수정으로 인한 파급효과를 제어할 수 있는 모든 기법이 캡슐화라고 할 수 있다.

서브타입/객체 캡슐화를 구현하는 방법

  1. 변하는 부분을 분리해서 타입 계층을 만든다.
  • 변하지 않는 부분으로부터 변하는 부분을 분리하자.
  • 변하는 부분의 공통적인 행동을 추상화 (변하는 부분은 이를 상속받아 구현)
  1. 변하지 않는 부분의 일부도 타입 계층을 합성
  • 타입 계층을 변하지 않는 부분에 합성하자 (e.g. Movie, DiscountPolicy)
  • 변하지 않는 부분에서는 변경되는 구체적인 사항에 결합되서는 안된다. (e.g. Movie, XXXDiscountPolicy)

변경 분리


  • 시간대별, 요일별, 구간별 방식은 기본 정책을 구성하는 방식이 유사하다.

  • 기본 정책은 1개 이상의 규칙으로 구성된다.
  • 규칙적용 조건단위 요금의 조합이다.

우리는 공통점(변하지 않는 부분)과 차이점(변하는 부분)을 분리해야 한다. 여기선 규칙이 공통점이고 적용 조건이 차이점이라 할 수 있으니 이 둘을 분리하자.

변경 캡슐화

  • 변하지 않는 부분에서 변하는 부분을 분리
    • FeeRuleFeeCondition
  • 타입 계층에 변하지 않는 부분 합성
    • BasicRatePolicyFeeRole
  • 변하는 부분에서 공통적인 행동을 추상화
    • FeeCondition

결론


일관성있는 협력을 이해하면 변하는 부분을 분리해도 전체적인 구조를 쉽게 이해할 수 있다. 이처럼 유사한 기능에 대해 유사한 협력 패턴을 적용하는 것은 객체지향 시스템에서 개념적 무결성을 유지할 수 있는 가장 효과적인 방법이다.

협력을 설계하고 있다면 항상 기존 협력 패턴을 따를 수는 없는지 고민하자. 가급적 기존의 협력 패턴에 맞추는게 가장 좋다. 개념적 무결성을 무너뜨리는 것보다 약간의 부조화를 수용하는게 낫다.

물론, 이러한 일관성은 설계 초기엔 잘 안지켜진다. 요구사항이 바뀔 때 마다 지속적으로 리펙토링을 해야 한다.

0개의 댓글