개발자는 기록이 답이다

오브젝트 3장 - 역할, 책임, 협력(1) 본문

기술 서적/OOP

오브젝트 3장 - 역할, 책임, 협력(1)

slow-walker 2023. 11. 28. 07:51

 

 

2장에서는 클래스, 추상 클래스, 인터페이스를 조합해서 객체 지향 프로그램을 구조화하는 기본적인 방법과 상속을 이용해 다형성을 구현하는 기법을 소개했다. 다형성이 지연 바인딩이라는 매커니즘을 통해 구현된다는 사실도 설명했고, 상속은 코드를 재사용할 수 있는 가장 널리 알려진 방법이지만 캡슐화의 측면에서 합성이 더 좋은 방법이라는 사실을 이해했다. 또한, 유연한 객체지향 프로그램을 위해서는 컴파일 시간 의존성과 실행 시간 의존성이 달라야 한다는 사실 역시 알게 됐다.

 

객체지향 패러다임의 관점에서 핵심은 역할(role), 책임(responsibility), 협력(collaboration)이다.

클래스, 상속, 지연바인딩이 중요하지 않은것은 아니지만 구현 측면에 치우쳐 있으며 객체지향 패러다임의 본질과 거리가 멀다.

 

객체지향 설계의 핵심은 협력을 구성하기 위해 적절한 객체를 찾고 적절한 책임을 할당하는 과정에서 드러난다.

클래스와 상속은 객체들의 책임과 협력이 어느정도 자리 잡은 후에 사용할 수 있는 구현 매커니즘일 뿐이다.

애플리케이션의 기능을 구현하기 위해 어떤 협력이 필요하고 협력을 위해 어떤 역할과 책임이 필요한지를 고민하지 않은 채

너무 이른 시기에 구현에 초점을 맞추는 것은 변경하기 어렵고 유연하지 못한 코드를 낳는 원인이 된다.

 

역할, 책임, 협력이 제자리를 찾지 못한 상태라면 응집도 높은 클래스와 중복 없는 상속 계층을 구현한다고 해도 애플리케이션이 침몰하는 것을 구현하지 못할 것이다. 3장은 그 이유에 대해서 알아보는 시간이다.

 

1. 협력

 

영화 예매 시스템 돌아보기

 

사용자가 영화 예매 시스템을 통해 영화를 예매할 수 잇게 하려면 다양한 객체들이 참여하는 협력을 구축해야 한다.

 

그림에서 알 수 있는것 처럼 객체지향 원칙을 따르는 애플리케이션의 제어 흐름은 어떤 하나의 객체에 의해 통제되지 않고, 다양한 객체들 사이에 균형있게 분배되는것이 일반적이다. 객체들은 요청의 흐름에 따라 자신에게 분배된 로직을 실행하면서 애플리케이션의 전체 기능을 완성한다.

 

💡 다양한 객체들이 영화 예매라는 기능을 구현하기 위해 메시지를 주고 받으면서 상호작용한다는 것이 중요하다.
이러한 상호작용을 협력이라고 하고, 객체가 협력에 참여하기 위해 수행하는 로직은 책임이라고 부른다. 객체들이 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할을 구성한다.

 

협력

 

객체지향 시스템은 자율적인 객체들의 공동체다 고립된 존재가 아니라 시스템의 기능이라는 더 큰 목표를 달성하기 위해 다른 객체와 협력하는 사회적인 존재다. 협력은 객체지향의 세계에서 기능을 구현할 수 있는 유일한 방법이다.

 

두 객체 사이의 협력은 하나의 객체가 다른 객체에게 도움을 요청할 때 시작된다.

객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단은 메세지 전송(message sending)이다.

객체는 다른 객체의 상세한 내부 구현에 직접 접근할 수 없기 때문에 오직 메세지 전송을 통해서만 자신의 요청을 전달할 수 있다.

 

메시지를 수신한 객체는 메서드를 실행해 요청에 응답한다. 객체가 메시지를 처리할 방법을 스스로 선택한다는 점이 중요하다

외부 객체는 오직 메세지만 전송할 수 있을 뿐, 메세지를 수신한 객체가 메세지를 어떻게 처리할지 직접 결졍한다.

이것은 객체가 자신의 일을 스스로 처리할 수 있는 자율적인 존재라는것을 의미한다.

 

 

Screening이 Movie에게 처리를 위임하는 이유는 요금을 계산하는 데 필요한 기본 요금과 할인 정책을 Movie가 가장 잘 알기 때문이다.

만일 이와 다르게 Screening이 요금계산하는 작업을 직접 수행한다면 Movie의 인스턴수변수인 fee와 discountPolicy에 접근해야하기 때문에 Movie의 자율성이 훼손된다.

 

💡 자율적 객체 ? 자신의 상태를 직접 관리하고 스스로의 결정에 따라 행동하는 객체다.
객체의 자율성을 보장하기 위해 필요한 정보와 정보에 기반한 행동을 같은 객체 안에 모아놓아야 한다.
객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화 하는 것이다.

자율적인 객체는 자신에게 할당된 책임을 수행하던 중에 필요한 정보를 알지 못하거나 외부의 도움이 필요한 경우 적절한 객체에게 메세지를 전송해서 협력을 요청한다. 메세지를 수신한 객체 역시 메세지를 처리하던 중에 직접 처리할 수 없는 정보나 행동이 필요한 경우 또 다른 객체에게 도움을 요청한다.

이처럼 객체들 사이의 협력을 구성하는 일련의 요청과 응답의 흐름을 통해 애플리케이션의 기능이 구현된다.

 

 

협력이 설계를 위한 문맥을 결정한다

 

🚩 객체란? 상태와 행동을 함께 캡슐화하는 실행 단위다

 

그렇다면 객체가 가질 수 있는 상태와 행동을 어떤 기준으로 결졍해야 할까?

애플리케이션 안에 어떤 객체가 필요하다면 그 이유는 단 하나여야 한다. 그 객체가 어떤 협력에 참여하고 있기 때문이다.

그리고 객체가 협력에 참여할 수 있는 이유는 협력에 필요한 적절한 행동을 보유하고 있기 때문이다.

협력이 바뀌면 객체가 제공해야 하는 행동 역시 바뀌어야 한다. 협력은 객체가 필요한 이유와 객체가 수행하는 행동의 동기를 제공한다.

 

 

Movie객체는 어떤 행동을 수행할 수 있어야 할까?

public class Movie {

	private final Money fee;
	private DiscountPolicy discountPolicy;
    
	public Money calculateMovieFee(Screening screening) {
		return this.fee.minus(this.discountPolicy.calculateDiscountAmount(screening));
	}
}

 

온라인 영화 예매 시스템에서의 Movie는 영화를 상영하기 위한 어떤 코드도 포함돼있지 않다. 대부분의 메서드는 요금을 계산하는 행동과 관련되어 있는데, Movie가 영화를 예매하기 위한 협력에 참여하고 있고, 그 안에서 요금을 계산하는 책임을 지고 있기 때문이다.

 

Movie의 행동을 결정하는 것은 영화 예매를 위한 협력이다. 협력이라는 문맥을 고려하지 않고 Movie의 행동을 결정하는 것은 아무런 의미가 없다.

 

객체의 행동을 결정하는 것이 협력이라면, 객체의 상태를 결정하는 행동이다. 객체의 사태는 그 객체가 행동을 수행하는데 필요한 정보가 무엇인지로 결정된다.

 

Movie가 fee와 discountPolicy를 포함하는 이유는 요금 계산이라는 행동을 수행하기에 앞서 이 정보들이 필요하기 때문이다.

 

상태는 객체가 행동하는데 필요한 정보에 의해 결정되고 행동은 협력 안에서 객체가 처리할 메시지로 결정된다.

객체가 참여하는 협력이 객체를 구성하는 행동과 상태 모두를 결정한다. 따라서 협력은 객체를 설계하는 데 필요한 일종의 문맥(context)을 제공한다.

 

2.책임

 

책임이란 무엇인가

 

객체를 설계하기 위해 필요한 문맥인 협력을 갖춘 후에는 협럭에 필요한 행동을 수행할 수 있는 적절한 객체를 찾는 것이다.

협력에 참여하기 위해 객체가 수행하는 행동을 책임이라고 부른다.

 

🚩 책임이란? 
객체에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 유지해야 하는 정보와 수행할 수 있는 행동에 대한 것이다.

 

책임은 객체가 '무엇을 알고 있는가'와 '무엇을 할 수 있는가'로 구성된다.

 

하는 것(doing)

  • 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
  • 다른 객체의 행동을 시작시키는 것
  • 다른 객체의 활동을 제어하고 조절하는 것

 

아는 것(knowing)

  • 사적인 정보에 관해 아는 것
  • 관련된 객체에 대해 아는 것
  • 자신이 유도하거나 계산할 수 있는것에 관해 아는 것

 

Movie의 책임 : 요금을 계산하는 것

  • 아는 것 : 가격과 어떤 할인 정책이 적용됐는지 알아야 한다.
  • 하는 것 : 예매 가격을 계산한다.

Screening의 책임 : 영화를 예매하는 것

  • 아는 것 : 상영 정보를 알고 있다.
  • 하는 것 : 예매 정보를 생성 한다.
public class Screening {
  private Movie movie;
  private int sequence;
  private LocalDateTime whenScreened;

  public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
    this.movie = movie;
    this.sequence = sequence;
    this.whenScreened = whenScreened;
  }

  public LocalDateTime getStartTime() {
   return whenScreened;
  }

  public boolean isSequence() {
    return this.sequence == sequence;
  }

  public Money getMovieFee() {
    return movie.getFee();
  }

  public Reservation reserve(Customer customer, int audienceCount) {
    return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
  }

  private Money calculateFee(int audienceCount) {
    return movie.calculateMovieFee(this).times(audienceCount);
  }
}

 

Screening이 reserve메시지를 수신하고 movie를 인스턴스 변수로 포함하는 이유는 협력 안에서 영화를 예매할 책임을 수행해야 하기 때문이다. Movie 또한 같은 이유로 calculateMovieFee 메시지를 수신할 수 있으며 fee와 discountPolicy를 속성으로 가진다.

 

즉, 협력 안에서 객체에게 할당한 책임이 외부의 인터페이스와 내부의 속성을 결정한다.

 

 

일반적으로 책임과 메시지의 크기는 다르다.

책임은 객체가 수행할 수 있는 행동을 종합적이고 간략하게 서술하기 때문에 메시지보다 추상적이고 개념적으로도 더 크다

처음에는 단순한 책임이라고 생각했던 것이 여러개의 메시지로 분할되기도 하고 하나의 객체가 수행할 수 있다고 생각했던 책임이 나중에는 여러 객체들이 협력해야만 하는 커다란 책임으로 자라는 것이 일반적이다.

 

책임의 관점에서 아는 것과 하는 것이 밀접하게 연관돼 있다는게 중요하다.

객체는 자신이 맡은 책임을 수행하는 데 필요한 정보를 알고 있을책임이 있다. 또한 객체는 자신이 할수없는 작업을 도와줄 객체를 알고 있을 책임이 있고, 어떤 책임을 수행하기 위해서는 그 책임을 수행하는데 필요한 정보도 함께 알아야 한다.

 

객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 소프트웨어 객체에 할당하는 것

 

협력이 중요한 이유는 객체에게 할당할 책임을 결정할 수 있는 문맥을 제공하기 때문이다.

객체 지향 설계에서 가장 중요한 것은 책임이며, 객체에게 얼마나 적절한 책임을 할당하느냐가 설계의 전체적인 품질을 결정한다.

단순하고 유연한 설계 : 적절한 협력 -> 적절한 책임 -> 적절한 객체에게 할당

 

책임 할당

 

자율적인 객체를 만드는 가장 기본적인 방법은 책임을 수행하는 데 필요한 정보를 가장 잘 알고 있는 전문가에게 그 책임을 할당하는 것이다.

이를 책임 할당을 위한 INFORMATION EXPERT 패턴이라고 부른다.

 

객체에게 책임을 할당하기 위해 먼저 협력이라는 문맥을 정의해야 한다. 협력을 설계하는 출발점은 시스템이 사용자에게 제공하는 기능을 시스템이 담당할 하나의 책임으로 바라보는 것이다. 객체지향 설계는 시스템의 책임을 완료하는 데 필요한 더 작은 책임을 찾아내고 이를 객체들에게 할당하는 반복적인 과정을 통해 모양을 갖춰간다.

 

 

 

영화 예매 시스템이 사용자에게 제공해야 할 기능은 영화를 예매하는 것이고, 이 기능을 시스템이 제공할 책임으로 할당하는 것이다.

객체가 책임을 수행하게 하는 유일한 방법은 메시지를 전송하는 것이므로 책임을 할당한다는 것은 메시지의 이름을 결정하는 것과 같다

 

 

"예매하라"라는 메시지로 협력을 시작할때 해당 메시지를 선택했으면 메시지를 처리할 적절한 객체를 선택해야 한다.

따라서 영화 예매와 관련된 정보를 가장 많이 알고 있는 객체에게 책임을 할당하는 것이 바람직 하다.

영화를 예매하기 위해서는 상영 시간과 기본 요금을 알아야 한다. 이 정보를 소유하고 있거나 해당 정보의 소유자를 가장 잘 알고 있는 전문가는 Screening이다.

 

영화를 예매하기 위해서는 예매 가격을 계산해야 한다. Screening은 예매에 대해서 정보 전문가 일지 몰라도 가격 자체에 대해서는 정보 전문가가 아니다. 이것은 Screening이 외부의 객체에게 가격 계산을 요청해야 한다는 것을 의미하고 "가격을 계산하라"라는 메시지가 필요하다. 마찬가지로 가격을 계산하는데 필요한 정보를 가장 많이 알고 있는 정보 전문가를 선택해야 하는데, 가격과 할인 정책이 있어야 가격을 계산할 수 있다. 이 모든 정보를 가장 잘 알고 있는 건 Movie이므로 가격을 계산할 책임을 할당한다.

 

가격을 계산하기 위해 할인 요금이 필요하지만 Movie는 할인 요금을 계산하는 데 적절한 정보를 갖고 있지 않으므로 필요한 요청을 외부에 전송해야 한다. 그러면 "할인 요금을 계산하라"라는 새로운 메시지가 생길 것이다.

 

💡 이처럼 객체 지향 설계는 협력에 필요한 메시지를 찾고 메시지에 적절한 객체를 선택하는 반복적인 과정을 통해 이뤄진다. 그리고 이런 메시지가 메시지를 수신할 객체의 책임을 결정한다.

이렇게 결정된 메시지가 객체의 퍼블릭 인터페이스를 구성한다는 것 역시 눈여겨 보기 바란다.
협력을 설계하면서 객체의 책임을 식별해 나가는 과정에서 최종적으로 얻게 되는 결과물은 시스템을 구성하는 객체들의 인터페이스와 오퍼레이션의 목록이다.

물론 모든 책임 할당 과정이 이렇게 단순하진 않다. 어떤 경우에는 응집도와 결합도의 관점에서 정보 전문가가 아닌 다른 객체에게 책임을 할당하는 것이 더 적절한 경우도 있다. 하지만 기본적인 전략은 책임을 수행할 정보 전문가를 찾는 것이다. 정보 전문가에게 책임을 할당하는 것만으로도 상태와 행동을 함께 가지는 자율적인 객체를 만들 가능성이 높아지기 때문이다.

 

책임 주도 설계

 

지금까지 살펴본 내용은 협력을 설계하기 위해서는 책임체 초점을 맞춰야 한다는 것이다. 

어떤 책임을 선택하느냐가 전체적인 설계 방향과 흐름을 결정한다.

 

🚩 책임 주도 설계(Responsibility-Driven Design, RDD) :  책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법

 

[ 책임 주도 설계 방법의 과정 ]

 

- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.

- 시스템 책임을 더 작은 책임으로 분할한다.

- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.

- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.

- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

 

🚩 책임을 할당할때 고려해야할 2가지 요소

1. 메시지가 객체를 결정한다
2. 행동이 상태를 결정한다.

 

 

1. 메시지가 객체를 결정한다

 

객체에게 책임을 할당하는데 필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택했다는 것이 중요하다

즉, 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택한다.

 

메세지가 객체를 선택해야 중요한 2가지 이유

  • 객체가 최소한의 인터페이스(minimal interface)를 가질 수 있게 된다.
    • 필요한 메시지가 식별될 때까지 객체의 퍼블릭 인터페이스에 어떤 것도 추가하지 않기 때문에 객체는 애플리케이션에 크지도, 작지도 않은 꼭 필요한 크기의 퍼브릭 인터페이스를 가질 수 있다.
  • 객체는 충분히 추상적인 인터페이스(abstract interface)를 가질 수 있게 된다.
    • 객체의 인터페이스는 무엇(what)을 하는지 표현해야 하지만 어떻게 (how) 수행하는지를 노출해선 안된다.
    • 메시지는 외부의 객체가 요청하는 무언가를 의미하기 때문에 메시지를 먼저 식별하면 무엇을 수행할지에 초점을 맞추는 인터페이스를 얻을 수 있다.
🤔 궁금한점 : 객체의 인터페이스란?

객체 지향 설계에서 "객체의 인터페이스"는 객체가 외부 세계와 상호 작용하는 방식을 정의한 것을 의미합니다. 이는 객체가 제공하는 메서드와 그 메서드의 시그니처(매개변수의 타입, 반환 값의 타입 등)로 구성됩니다. 이러한 인터페이스는 객체가 외부에서 어떻게 상호 작용할 수 있는지를 정의하며, 객체 지향 프로그래밍의 중요한 원칙 중 하나인 "캡슐화"를 지원합니다.


🤔 궁금한점 : 최소한의 인터페이스와 추상적인 인터페이스가 잘 와닿지 않는다.

 

 

2. 행동이 상태를 결정한다.

 

객체가 존재하는 이유는 협력에 참가하기 위해서인 것을 기억하는가?

객체는 협력에 필요한 행동을 제공해야 한다. 객체를 객체답게 만드는 것은 객체의 상태가 아니라 객체가 다른 객체에게 제공하는 행동이다.

 

객체지향 패러다임에 갓 입문한 사람들이 가장 쉽게 빠지는 실수는 객체의 행동이 아니라 상태에 초점을 맞춘다.

초보자들은 먼저 객체에 필요한 상태가 무엇인지를 결정하고, 그 후에 상태에 필요한 행동을 결정한다.

이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해한다.

 

객체의 내부 구현을 변경하면 퍼블릭 인터페이스도 함께 변경되고, 결국 개체에 의존하는 클라이언트로 변경의 영향이 전파된다.

객체의 내부 구조에 초점을 맞춘 설계 방법데이터 구조 설계(Data-Driven Design)이라고 부른다.

 

캡슐화를 위반하지 않도록 구현에 대한 결정을 뒤로 미루면서 객체의 행위를 고려하기 위해서는 항상 협력이라는 문맥 안에서 객체를 생각해야 한다. 협력 관계 속에서 다른 객체에게 무엇을 제공해야 하고 다른 객체로부터 무엇을 얻어야 하는지를 고민해야한 훌륭한 책임을 수확할 수 있다. 개별 객체의 상태와 행동이 아닌 시스템의 기능을 구현하기 위한 협력에 초점을 맞춰야만 응집도가 높고 결합도가 낮은 객체들을 창조할 수 있다. 상태는 단지 객체가 행동을 정상적으로 수행하기 위해 필요한 재료일 뿐이다.

 

🤔 궁금한점 : OOP와 DDD는 구현은 비슷하지만, 설계하는 관점이 포괄적인지 협소한지에 대한 차이인 것인가?

1. 객체지향 프로그래밍 (OOP):
- 협력 중심 접근: 객체지향 프로그래밍은 주로 객체 간의 협력에 중점을 둡니다. 객체는 메시지를 주고받으며 협력을 통해 문제를 해결합니다.
- 행동 우선: 객체는 자신의 상태를 캡슐화하고, 상태를 조작하는 행동을 통해 협력에 참여합니다. 객체의 상태는 내부적으로 유지되며, 이 상태를 행동을 통해 변경하고 외부에 노출하지 않습니다.


2. 도메인 주도 설계 (DDD):
- 도메인 중심 접근: DDD는 비즈니스 도메인에 중점을 두며, 도메인 모델을 통해 현실 세계의 문제를 해결합니다. 도메인 모델은 엔터티, 밸류, 애그리게이트 등의 개념을 사용하여 도메인을 표현합니다.
- 상태 우선: DDD에서는 먼저 객체의 필요한 상태를 결정하고, 그 상태에 필요한 행동을 정의합니다. 상태를 중심으로 모델을 구축하고, 해당 상태를 조작하기 위한 행동을 결정합니다.

이로 인해 두 접근 방식은 설계하는 초점과 관점에서 차이가 있습니다. OOP는 주로 협력과 행동에 초점을 맞추고, DDD는 도메인과 상태에 초점을 맞춥니다. 결국 이러한 차이는 설계자가 어떤 측면을 더 중요하게 여기느냐에 따라 다를 수 있습니다. 어느 접근 방식이 더 나은지는 문제의 복잡성과 해결해야 하는 도메인의 특성에 따라 다를 수 있습니다.

 

코드 확인할 수 있는 Github 링크

 

GitHub - codesejin/OOP-Object: [오브젝트] - 코드로 이해하는 객체지향 설계

[오브젝트] - 코드로 이해하는 객체지향 설계. Contribute to codesejin/OOP-Object development by creating an account on GitHub.

github.com