객체지향 #2부 - 객체지향의 심층 탐구: 역할, 책임, 협력의 중요성

객체지향 #2부 - 객체지향의 심층 탐구: 역할, 책임, 협력의 중요성

객체지향 프로그래밍의 근본적인 원리들을 탐구하고, 현실 세계의 문제를 객체지향의 관점에서 어떻게 코드로 해결할 수 있는지에 대해 설명합니다. 다형성, 상속, 책임, 협력 등 객체지향의 핵심 개념들을 심층적으로 다루며, 좋은 객체지향 설계를 위한 전략을 제시합니다.

다형성은 메세지와 메서드가 실행시점 (Runtime)에 매칭되는 지연 바인딩 으로 구현된다.
상속은 코드를 재사용한다는 이점은 있지만, 컴파일 시점에 특정 객체를 사용할지 결정해야하고, 캡슐화의 측면에서 합성이 더 좋다.
컴파일 시간 의존성과 런타임 시간 의존성이 달라야 더 유연한 객체지향 프로그램이 된다.
런타임 시간 의존성에 객체가 결정된다는 의미는, 기본 골격 설계에 대한 추상화가 잘 이뤄져 변경이 없는 안정적인 것에 의존했다는 의미와 같다.

객체지향 패러다임에서 다형성, 상속, 합성 등보다 더 중요한 것이 있다.
바로 역할(role), 책임(responsibility), 협력(collaboration) 이다.
다형성, 상속, 합성 등은 다분히 구현레벨에 치우쳐져 본질과 거리가 멀기 때문이다.

본질협력하는 객체들의 공동체를 창조하는 것이다.

초기 설계가 제대로 되지 못하면 아무리 응집도 높은 클래스와 중복 없는 상속 계층을 구현해도 변경시에 침몰된다.

지금부터 객체지향 패러다임의 협력, 책임, 역할이 왜 핵심인지 알아보자.


협력은 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용이다.
이 협력에 참여하기 위해 수행하는 로직은 책임이다.
협력 안에서 수행하는 책임들이 모여 역할이 만들어진다.

협력

협력은 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용이다.

객체 사이의 협력메세지 전송 을 통한 커뮤니케이션으로 시작된다.
이 때, 캡슐화를 통해 상세한 내부 구현(메서드)에 접근할 수 없다.

객체는 자율적으로 자신의 상태를 관리할 수 있어야 하고, 자신이 할 수 없는 일은 다른 객체에게 부탁해서 결과를 받아야 한다.

Q : 객체는 상태와 행동을 캡슐화하는 실행 단위이다. 그렇다면 객체가 가질 수 있는 상태와 행동을 무슨 기준으로 결정할까?

객체의 행동을 결정하는 것은 객체가 참여하고 있는 협력이다.
협력은 객체가 필요한 이유와 객체가 수행하는 행동의 동기를 제공한다.

상태를 결정하는 것은 행동이다. 객체의 상태는 행동을 수행하는 데 필요한 정보가 무엇인지로 결정된다.
객체 스스로 자신의 상태를 결정하고 관리하는 자율적인 존재이기 때문에, 협력에서 필요한 자신의 행동에 필요한 상태를 스스로 가지고 있어야 한다.

예를 들어 Movie라는 객체가 있다고 쳤을 때, 일반적으로 영화를 보여주는 것이 Movie의 행동이라고 생각된다.
그러나, 협력 관계에 있어서 Movie가 금액 계산의 역할을 맡게 되면 Movie의 행동은 금액을 계산하는 행동을 하게 될 것이다.
그리고 금액 계산에 필요한 상태인 요금 정책, 할인 정책 등의 상태를 가지고 있게 된다.

Q : 반대로 이런 협력이 깨져버리게 된다면?

만약 Movie가 금액 계산을 하기로 설계된 협력관계에서 다른 객체가 요금을 계산하는 작업을 수행한다면,
Movie의 상태 값을 꺼내서 구현을 하게 될 것이고, 그렇게 되면 캡슐화가 깨져 Movie의 자율성이 깨져버리게 되고, Movie의 내부 구현에 결합된다.
곧, Movie 내부의 상태 값에 대한 의존이 생겨 쉽사리 내부 구현을 바꾸지 못한다.

결론적으로 객체가 참여하는 협력상태와 행동에 대한 문맥(context) 설계를 제공해준다.


책임

협력에 참여하기 위해 수행하는 로직은 책임이다.

협력 설계가 완료가 되면, 협력으로 형성된 문맥에 참여하기 위해 객체가 수행해야만 하는 행동을 책임이라고 한다.

객체에 대한 책임의 구성 요소

  • 무엇을 하는가 (행동)
    • 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
    • 다른 객체의 행동을 시작시키는것
    • 다른 객체의 활동을 제어하고 조절하는 것
  • 무엇을 아는가 (상태)
    • 사적인 정보에 관해 아는 것
    • 관련된 객체에 관해 아는 것
    • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것

객체에 할당된 책임은 꼭 한 메세지에 매칭되는 것이 아니라 여러 메세지를 만들기도 한다.
객체에 책임이 할당되면, 그 책임을 수행할 정보나 기능을 요청할 객체 대상을 결정해야 한다.

Screening

할당된 책임 : 개별적인 상영 정보를 나타내며, 사용자들의 실제 예매 대상이 된다.

관련된 객체에 대해 안다 : 상영 정보를 알고 있다. - Movie 객체
책임 수행 메세지 : 예매 정보를 생성한다.

책임 주도적인 설계를 진행하는 방법

  • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  • 시스템 책임을 더 작은 책임으로 분할한다.
  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 겨웅 이를 책임질 적절한 객체 또는 역할을 찾는다.
  • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

협력 관계에서 필요한 메세지를 먼저 만들고, 추후에 메세지를 처리할 객체를 선택하는 것이 중요하다.

WHY?

  1. 오버엔지니어링을 방지해준다. 꼭 필요한 기능만 추가된다.
  2. 추상적인 인터페이스를 가지게 되어 객체의 내부 구현에 대한 의존을 줄여준다.

Q : 객체의 상태보다 행동이 주요시되는 이유는 무엇일까?

객체의 협력관계에서 행동이 결정이 되고, 그 협력을 위한 행동을 수행하기 위해 상태가 뒤따라오기 때문이다.
상태에 집중하게 되면 내부 구현이 노출되게 만들기 때문에 캡슐화를 저해한다.
결국 내부 구현을 바꿀 때 인터페이스가 함께 변경이 되어 부작용이 전파된다.

객체는 협력을 하기 위해 태어난다. 객체간 협력은 메세지를 통해 가능하고,
메세지를 처리하기 위해 자기 자신의 상태를 가지거나 상태로 가진 다른 객체에게 부탁할 수 있다.
상태는 협력의 부수적인 도우미일 뿐이다.


역할

협력 안에서 수행하는 책임들이 모여 역할이 만들어진다.

객체끼리 협력관계가 만들어지고, 메세지가 생기게 되면 메세지를 처리하기 위한 역할이 생긴다.
협력 네트워크에서 수행되어져야 할 역할은 이미 정해져 있고, 어떤 인스턴스가 그 역할을 수행할지 고르는것에 가깝다.

Q : 역할 설정이 설계에서 도움되는 이유는 무엇일까?

역할이란 추상화를 통해 재사용 가능한 협력을 얻을 수 있기 때문이다.
역할은 주어진 책임을 수행하는 인터페이스이기 때문에 추상체로 구현이 가능하다.
한 역할을 맡을 인스턴스가 여러개라면, 어차피 같은 책임을 가지고 수행하는 인터페이스는 동일하기 때문이다.

역할의 추상체로써의 interface vs abstract class

추상화의 종류는 abstract class, interface 두 가지가 있는데,
추상 클래스는 역할을 수행할 수 있는 모든 객체들이 공유하는 상태와 행동의 기본 구현이 존재할 때 사용하면 좋다.
인터페이스는 그냥 공통의 구현이 필요 없고, 책임의 목록만 정의하면 될 때 사용하면 좋다.

협력은 역할들의 상호작용으로 구성되고, 협력을 구성하기 위해 역할에 적합한 객체가 선택되며, 객체는 클래스를 이용해 구현되고 생성된다.

Q : 역할과 객체의 구분점을 어떻게 지어야 하나요?

설계 자체도 성장한다. 여러 객체가 동일한 역할에 참여하게 되면 역할로 시작하면 된다.
하지만 초기에 불분명하다면 객체로 시작해도 된다.
추후에 시스템이 발전되면서 유사한 협력들을 단순화하고 합치다보면 자연스럽게 역할이 그 모습을 드러낼 것이다.

결국 역할의 존재 의미는 협력이 유연해지고 재사용이 가능해진다는 것이다.


책임 주도 설계

좋은 객체지향 설계에 있어서 가장 중요한 핵심은 올바른 객체에게 올바른 책임을 분배하느냐다.
올바른 책임 분배는 객체의 상태가 아닌 행동을 중심으로 구분을 했을 때 가능하다.
그로 인해 높은 응집도와 낮은 결합도가 결정이 되고, 유연한 변경이 가능해진다.

상태 중심으로 설계를 하게 되면 어떤 차이점이 있는지 알아보자.

데이터(상태) 중심 설계 vs 책임 주도 설계

객체의 상태는 구현레벨에 속한다. 구현은 변하기 쉽고 불안정하다.
상태중심은 구현 세부사항이 객체의 인터페이스에 스며들고, 객체의 캡슐화를 깨뜨린다.
해당 인터페이스를 사용하고 있는 다른 객체들에게도 영향을 미치게 되고, 결국 변경에 취약한 설계가 된다.

협력하는 객체들의 공동체를 생각하기 이전에 객체가 관리할 데이터의 세부 정보를 먼저 설정하게 된다.
그로 인해 내부 상태가 외부 객체와의 협력에서 결합이 된다.
무슨 상태를 가지고 무슨 값을 가지게 되는 것은 역할과 책임 분배가 이뤄진 이후, 맡은 역할을 수행하기 위한 행동을 수행하면서 결정되어져야 한다.

public class Movie{
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;
    
    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;
}

할인 조건의 목록, 할인 정책에 사용될 할인 비율과 할인액 등이 멤버변수로 포함된다.

객체의 책임은 인터페이스에 속한다. 객체 자신의 책임을 수행하는 추상화된 인터페이스의 결과가 상태에 영향을 미친다.
여기서 내부 구현은 캡슐화되므로 상태의 변화가 인터페이스에 영향을 미치지 않게 된다.
결국 책임 중심의 설계는 변경에 유연하고, 응집도가 높고, 결합도가 낮은 설계를 만들어낸다.

좋은 객체지향 설계의 척도

캡슐화, 응집도, 결합도는 좋은 객체지향 설계의 척도로 여겨진다.

캡슐화

불안정한 구체적인 구현부분을 감추고 외부에 상대적으로 변화에 안정적인 인터페이스만을 보여준다.
해당 인터페이스를 참조하는 다른 협력관계의 객체들에게 변화에 대한 전파를 통제하는 것이 목적이다.

캡슐화는 추상화의 한 기법인데, 복잡한 현실을 객체지향으로 모델링하여 단단한 인터페이스를 설정하고
그 추상화된 인터페이스를 통해 내부 구현을 숨긴다면, 내부 구현을 거부감 없이 수정을 할 수 있게 된다.

응집도

모듈에 포함도니 내부 요소들이 얼마나 연관성을 가지고 있는가이다.
하나의 목표를 위해 긴밀하게 협력한다면 응집도가 높다고 말할 수 있다.
반면에 서로 다른 목표를 추구한다면 낮은 응집도라고 말한다.

객체지향에서는 객체에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.

결합도

의존성의 정도이며 한 모듈이 다른 모듈을 너무 많이 알게되면 결합도가 높다고 말한다.
한 모듈이 다른 모듈의 필요한 지식만 가지게 되면 결합도가 낮다고 말한다.
적절한 수준을 유지하는 것이 중요하다.

유지보수 관점에서의 높은 응집도와 낮은 결합도

유지보수에서 높은 응집도가 갖는 의미는 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도이다.
객체 내부의 상태, 인터페이스가 하나의 목표만 가지고 구성되어 있다면, 해당 객체의 변경 발생할 이유가 그 목표 하나이다.
반대로 말하면 한 객체 내부 구성이 오만가지 목표를 가지고 있다면, 해당 객체의 변경 발생, 영향도 오만가지이다.


유지보수에서 낮은 결합도가 갖는 의미는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도이다.
객체 내부의 책임을 중점으로 말하는 응집도와는 다르게 외부 협력관계에서의 의존성을 말한다.
그리고 내부 구현을 변경하였을 때, 다른 모듈에 영향을 미치는 정도도 의미한다.
절대적으로 안정성을 가진 String 과 같은 객체와 강한 결합을 맺는 것은 상관이 없다.
그러나 매우 불안정한 객체와의 강한 결합은 변경에 취약한 설계를 만들어낸다.

객체끼리 협력관계에서 역할에 대한 단일한 목표(변경의 이유)를 지정하고(높은 응집), 구현이 아닌 인터페이스에 의존해 설계를 진행해야 한다.(낮은 결합)

진정한 의미의 캡슐화

진정한 캡슐화는 단순히 객체 내부의 private 변수를 감추는 것에 그치지 않는다.
내부 구현의 변경으로 인해 외부 객체가 영향 받을 수 있는 어떤 것이라도 감추는 것을 의미한다.

파라미터로 내부 변수 캡슐화 실패

public class Discountcondition{
    private int sequence;
    DayOfWeek dayOfWeek;
    LocalTime startTime;
    LocalTime endTime;
    
    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time){...}
    public boolean isDiscountable(int sequence){...}
}

isDiscountable 의 파라미터가 객체 내부에 DayOfWeek, LocalTime, int 타입의 순번 정보를 변수로 가지고 있다는 사실을 노출하고 있다.
-> 내부 변수의 타입을 바꾸면 파라미터로 엮여서 isDiscountable 메서드를 사용하고 있는 외부 객체들도 수정해야 한다.

추상화되지 못한 메서드구현으로 캡슐화 실패

public class Movie{
    private List<DiscountCondition> discountConditions;
    
    public Money calculateAmountDiscountedFee() {...}
    public Money calculatePercentDiscountedFee() {...}
    public Money calculateNoneDiscountedFee() {...}
    
}

파라미터로 내부 변수 타입을 노출 시키지 않더라도, 추상화되지 못한 메서드로 내부 구현을 드러내고 있다.
할인 조건이 금액, 비율, 논 할인 정책이란것을 외부에 드러내고 있다.
-> 할인 정책이 바뀌는 경우, 그것을 사용하고 있던 모든 객체가 수정되어야 한다.