티스토리 뷰

728x90
반응형

 


정말 정말 정말 좋은 책이다,,

좋은 예제와 쉽고 재밌는 글로 객체지향을 제대로 이해시켜주신다. (🥺💜)

저의 개발도서 TOP1 🏆🏅입니다. 

 

기억하고 싶은 것을 기록합니다 ✏️

좋은 부분이 너무 많아서 책을 많이 옮겨왔는데 혹시 문제가 된다면 말씀해주세요-!

(예제도 함께 있는 책을 꼭 읽어보시는 것을 추천드립니다!)

 

 


[1] 좋은 설계란?

 

좋은 설계란 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계다. 

좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계다.

 

[2] 좋은 객체지향 설계란?

 

좋은 객체지향 설계는 협력하는 객체들 사이의 의존성을 적절하게 조절함으로써 변경에 용이한 설계를 만드는 것이다.

 

구체적으로 말하면.. 

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

객체지향에서 가장 중요한 것은 애플리케이션의 기능을 구현하기 위해 협력에 참여하는 객체들 사이의 상호작용이다.

객체지향 설계의 핵심은 적절한 협력을 식별하고 / 협력에 필요하는 역할을 정의 한 후 / 역할을 수행할 수 있는 적절한 객체에게 적절한 책임을 할당하는 것이다. 

 

객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다. 

 

[3] 좋은 설계인지 판단하는 척도 - 캡슐화, 응집도, 결합도

 

1. 캡슐화 (encapsulation)

 

캡슐화란 어떤 것을 숨긴다는 것을 의미한다.

캡슐화는 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다. 

 

객체지향 설계의 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다.

상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다.

캡슐화의 가장 대표적인 예는 객체의 퍼블릭 인터페이스와 구현을 분리하는 것이다.

자주 변경되는 내부 구현을 안정적인 퍼블릭 인터페이스(메시지) 뒤로 숨겨야한다. 

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

 

캡슐화가 중요한 이유는 불안정한 부분과 안정적인 부분을 분리해서 변경의 영향을 통제할 수 있기때문이다.

변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.

변경될 수 있는 어떤 것이라도 캡슐화해야한다

 

응집도, 결합도, 중복 역시 훌륭한(변경가능한) 코드를 규정하는 데 핵심적인 품질인 것이 사실이지만

캡슐화는 우리를 좋은 코드로 안내하기 때문에 가장 중요한 제 1원리다.

 

 

2. 응집도 (cohesion)

 

모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.

모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.

반면 모듈 내의 요소들이 서로 다른 목적을 추구한다면 그 모듈은 낮은 응집도를 가진다.

객체지향 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다. 

 

음영으로 칠해진 부분은 변경이 발생했을 때 수정되는 영역을 표현한 것이다. 

응집도가 높은 설계에서는 하나의 요구사항 변경을 반영하기 위해 오직 하나의 모듈만 수정하면 된다.

반면 응집도가 낮은 설계에서는 하나의 원인에 의해 변경해야 하는 부분이 다수의 모듈에 분산돼 있기 때문에 여러 모듈을 동시에 수정해야한다. 

 

 

 

 

응집도가 높을 수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.

 

 

3. 결합도 (coupling)

 

의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나태내는 척도이다. 

어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다.

어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다.

객체지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는 지를 나타낸다. 

 

결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.

 

결합도가 낮은 설계에서는 모듈 A를 변경했을 때 오직 하나의 모듈만 영향을 받는다는 것을 알 수 있다.

반면 높은 결합도를 가진 설계에서는 모듈 A를 변경했을 때 4개의 모듈을 동시에 변경해야한다.  

 

 

 

 

영향을 받는 모듈 수 외에도 변경의 원인을 이용해 결합도의 개념을 설명할 수도 있다.

내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우에는 두 모듈 사이의 결합도가 높다고 표현한다.

반면 퍼블릭 인터페이스를 수정했을 때만 다른 모듈에 영향을 미치는 경우에는 결합도가 낮다고 표현한다.

따라서 클래스의 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 얻을 수 있다.

 

 

마지막으로 캡슐화의 정도가 응집도와 결합도에 영향을 미친다는 사실을 강조하고 싶다.

캡슐화를 지키면 모듈 안의 응집도는 높아지고 모듈 사이의 결합도는 낮아진다.

 

 

[4] 데이터 주도 설계 

 


바로 우리가 대학생 시절 자바 배울 때 했던,

클래스에 필요한 프로퍼티들 쭉 정의해두고

이클립스 툴을 이용해서 자동으로 getter, setter 만들고,, 

했던 것을 데이터 주도 설계라고 합니다. 😭


 

데이터 중심 설계를 시작할 때  던지는 첫번째 질문은
"이 객체가 포함해야하는 데이터가 무엇인가? "다. 


데이터 주도 설계는 설계를 시작하는 처음부터 데이터에 관해 결정하도록 강요하기 때문에 

너무 이른 시기에 내부 구현에 초점을 맞추게 한다. 

데이터 중심의 관점에서 객체는 그저 단순한 데이터 집합체일 뿐이다.
이로 인해 getter, setter를 과도하게 추가하게 되고 이 데이터 객체를 사용하는 절차를 분리된 별도의 객체 안에 구현하게 된다.
getter, setter는 public 속성과 큰 차이가 없기 때문에 객체의 캡슐화는 완전히 무너질 수 밖에 없다. 

객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미치기 때문에
변경에 취약한 코드를 낳게 된다. 

또한 객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없다. 

 

               

< 캡슐화 위반 예제 >

public class Movie {
   private Money fee;
   
   public Money getFee() {
      return fee;
   }

   public void setFee(Money fee) {
      this.fee = fee;
   }
}

 

데이터 중심으로 설계한 Movie 클래스는 오직 메서드를 통해서만 객체의 내부 상태에 접근할 수 있다.

위 코드는 직접 객체의 내부에 접근할 수 없기 때문에 캡슐화의 원칙을 지키고 있는 것 처럼 보인다.

getter와 setter는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.

getFee 메서드와 setFee 메서드는 Movie 내부에 Money 타입의 fee라는 이름의 인스턴스 변수가 존재한다는 사실을 퍼블릭 인터페이스에 노골적으로 드러낸다. 

속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다. 

Movie가 캡슐화의 원칙을 어기게 된 근본 원인은 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞췄기 때문이다. 

구현을 캡슐화할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때만 얻을 수 있다. 

 

 

데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현을 인터페이스의 일부로 만든다.

반면 책임 중심의 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화한다. 

 

 

[5] 책임 주도 설계

 

데이터 중심 설계 -> 책임 중심의 설계로 전환하기 위해서는 다음의 두가지 원칙을 따라야한다.

 

1. 데이터보다 행동을 먼저 결정하라

 

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다. 

객체는 협력에 참여하기 위해 존재하며 협력 안에서 수행하는 책임이 객체의 존재가치를 증명한다. 

데이터 중심의 설계는 "이 객체가 포함해야하는 데이터가 무엇인가? 를 질문한다.

반면 책임 중심 설계에서는 "이 객체가 수행해야하는 책임은 무엇인가" 를 결정한 후에 "이 책임을 수행하는데 필요한 데이터는 무엇인가" 를 결정한다. 

즉 책임을 먼저 결정한 후에 객체의 상태를 결정한다는 것이다. 

 

2. 협력이라는 문맥 안에서 책임을 결정하라 

 

책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야한다.

협력을 시작하는 주체는 메세지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 

메세지 전송자에게 적합한 책임을 의미한다. 

 

객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야한다. 

 

메시지를 먼저 결정하기 때문에 메시지 전송자는 수신자에 대한 어떠한 가정도 할 수 없다.

메시지 전송자의 관점에서 메시지 수신자가 깔끔하게 캡슐화되는 것이다. 

책임 중심의 설계가 응집도가 높고 결합도가 낮으며 변경하기 쉽다고 말하는 이유가 여기에 있다. 

 

 

------ 

훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 객체가 훌륭한 클래스를 낳는다.

어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라. 

-----

 

[6] 책임 주도 설계의 흐름

 

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

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

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

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

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

 

책임 주도 설계의 핵심은 책임을 정한 후, 책임을 수행할 객체를 결정하는 것이다.

그리고 협력에 참여하는 객체들의 책임이 어느 정도 정리될 때까지는 객체의 내부 상태에 대해 관심을 가지지 않는 것이다. 

 

 

[7] 책임 주도 설계 예제 - 영화 예매 시스템

 

1. 도메인 개념 개략적으로 그려보기

 

설계를 시작하기전 도메인에 대한 개략적인 모습을 그려보는 것이 유용하다.

(중요한 것인 설계를 시작하는 것이지 도메인 개념들을 완벽하게 정리하는 것이 아니다.

또한 도메인을 그대로 코드에 옮겨야하는 것도 아니다.

실제 코드를 구현하면서 역으로 도메인 개념을 바꿀 수 도있다. )

 

 

 

 

2. "예매하라" 메시지

 

책임 주도 설계의 첫 단계는 애플리케이션이 제공해야하는 기능(영화예매)을 애플리케이션의 책임으로 생각하는 것이다. 

이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.

 

 

 

 

메세지를 결정했으므로 메시지를 수신하기에 적합한 객체를 찾아야한다.

이 질문에 답하기 위해서는 객체가 상태와 행동을 통합한 캡슐화의 단위라는 사실에 집중해야한다.

객체는 자신의 상태를 스스로 처리하는 자율적인 존재여야한다.

따라서 객체에게 책임을 할당하는 첫번째 원칙은 책임을 수행할 정보를 알고 있는 '정보전문가' 객체에게 책임을 할당하는 것이다. 

 

(여기서 이야기하는 정보는 데이터와는 다르다. 책임을 수행하는 객체가 정보를 '알고' 있다고 해서 그 정보를 '저장'하고 있을 필요는 없다. 객체는 해당 정보를 제공할 수는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수 도 있다. 어떤 방식이건 정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다.) 

 

 

상영은 영화에 대한 정보, 상영시간, 상영순번 처럼 영화예매에 필요한 다양한 정보를 알고 있는 정보전문가이다.

따라서 상영(Screening)에게 예매를 위한 책임을 Screening에게 할당한다.

 

 

 

 

이제 예매하라 메시지를 수신했을 때 Screening이 수행해야하는 작업의 흐름을 개략적으로 생각해보자.

만약 스스로 처리할 수 없는 작업이 있다면 외부에 도움을 요청해야한다.

이 요청이 외부로 전송해야하는 새로운 메시지가 되고, 최종적으로는 이 메시지가 새로운 객체의 책임으로 할당된다.

이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다. 

 

예메하라 메시지를 완료하기 위해서는 예매 가격을(영화 한편의 가격 x 예매 인원수) 계산하는 작업이 필요하다. 

따라서 영화 한편 가격을 알아야한다.

Screening은 가격을 계산하는데 필요한 정보를 모르기 때문에 외부의 객체에게 도움을 요청해서 가격을 얻어야한다.

외부에 대한 이 요청이 새로운 메시지가 된다. 메시지의 이름으로는 "가격을 계산하라" 로 하자.

 

3. "가격을 계산하라" 메시지

 

 

 

 

이 메시지를 책임질 객체는

영화 가격을 계산하는데 필요한 정보를 알고 있는 전문가 영화(Movie) 로 한다.

 

 

 

 

가격 계산을 위해 Movie가 어떤 작업을 해야하는지 생각해보자

요금을 계산하기 위해서는 먼저 영화가 할인 가능한지를 판단 후, 할인 정책에 따라 할인 요금을 제외한 금액을 계산하면 된다.

할인 조건에 따라 영화가 할인 가능한지 판단하는 것은 Movie가 스스로 처리할 수 없는 일이다.

따라서 Movie는 "할인 여부를 판단하라" 메시지를 전송해서 외부의 도움을 요청한다.

 

 

4. "할인 여부를 판단하라" 메시지

 

 

 

 

할인여부를 판단하는데 필요한 정보를 가장 많이 알고 있는 객체는 할인 조건(DiscountCondition)이다. 여기에 책임을 할당하자.

 

 

 

 

 

DiscountCondition은 자체적으로 할인 여부를 판단하는데 필요한 모든 정보를 알고 있기 때문에 외부의 도움 없이도

스스로 할인 여부를 판단할 수 있다. 따라서 DiscountCondition은 외부에 메시지를 전송하지 않는다.

 

Movie는 "할인 여부를 판단하라" 메시지의 결과로 할인 가능 여부를 반환받는다. 

DiscountCondition 중에서 할인 가능한 조건이 있다면 정책에 따라 요금을 계산한 후 반환하고

없다면 영화의 기본금액을 반환한다. 

 

5. 창조자에게 객체 생성 책임을 할당하라

 

영화 예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것이다.

협력에 참여하는 객체들 중 하나에게 Reservation 인스턴스를 생성할 책임을 할당해야한다.

Reservation을 잘알고 있거나 긴밀하게 사용하거나 초기화에 필요한 데이터를 가지고 있는 객체는 Screening이다. Screnning은 예매정보를 생성하는데 필요한 정보 전문가이기때문이다. (영화, 상영시간, 상영순번 알고 있음 / 예매 요금을 계산하는데 필수적인 Movie도 알고 있음)

그러므로 Screening을 Reservation의 창조자(CREATOR)로 선택한다. 

 

 

 

 

6. 코드로 검증

 

협력이 제대로 동작하는 지 확인할 수 있는 유일한 방법은 코드작성이다.

(코드는 책으로 확인!)

728x90
반응형
댓글
댓글쓰기 폼