티스토리 뷰

읽은책

오브젝트 - 조영호

승가비 2020. 7. 12. 03:42
728x90

 

 

객체를 바라보는 것

공동체의 존재

적절한 역할과 책임을 부여할 수 있느냐

적절한 역할과 책임을 수행하며 협력하는 공동체의 일원이라는 사실을 설명하는 것입니다.

 

객체지향의 세계는 이 책이 들려주는 이야기보다 더 깊고 광활합니다.

이 책에서 멈추지 말고 무한한 가능성의 영역으로 나아가서

기존의 원칙들을 찢고, 부수고, 분해한 후 재조립하시기 바랍니다.

 

캡슐화

응집도

결합도

역할

책임

협력

 

주변의 긍정적인 피드백은 큰 도움이 된다.

 

### 이일민님 추천사

개발자는 커피를 주입 받아 코드로 바꾸는 기계라는 농담이 있다.

자칫하면 기능을 변경하다 잘 동작하던 기존 코드에 버그를 심을 수도 있다는 두려움도 따른다. 커피가 점점 더 많이 필요해진다.

 

시스템을 효과적으로 분해하고 구성할 수 있고,

손쉽게 이해하고 효율적으로 다룰 수 있게 도와주는 방법으로 인정받아 많은 프로그래밍 언어에 적용돼 왔고

지금은 가장 인기 있는 프로그래밍 패러다임으로 자리 잡았다.

 

애노테이션을 이용한 메타 프로그래밍,

설정보다는 관례를 우선하는 자동 구성,

함수형 프로그래밍 기법

 

객체에 다시 관심을 가지는 것, 시스템을 자율적인 객체가 서로 협력해서 일을 하는 공동체라는 시각으로 바라보고 설계하고,

개발하는 방법을 학습하고 익히는 건 개발자로서 평생의 경력에 많은 유익함을 안겨줄 것이다.

 

 

========================================================================

 

 

## 프로그래밍 패러다임

과학의 발전이란 이미 달성된 과학적 성취를 기반으로 새로운 발견을 누적시키거나

기존의 오류를 수정하면서 단계적으로 진보해 나가는 과정이다.

기존의 과학적 견해를 붕괴시키는 혁명적인 과정을 거쳐 발전해왔다고 주장했다.

이를 '과학혁명'이라고 불렀다.

 

패러다임 전환(Paradigm Shift)

개발자는 코드를 통해 패러다임을 이해하고 적용할 수 있는 기술을 습득해야만 한다.

 

오히려 서로 다른 패러다임이 하나의 언어 안에서 공존함으로써 서로의 장단점을 보완하는 경향을 보인다.

스칼라(Scala), 다중패러다임 언어(Multiparadigm Language)

 

프로그래밍 패러다임은 혁명적(revolutionary)이 아니라 발전적(evolutionary)이다.

다른 패러다임을 배우는 것이 도움이 될 것이라는 사실을 암시한다.

객체지향이 적합하지 않은 상황에서는 언제라도 다른 패러다임을 적용할 수 있는 시야를 기르고 지식을 갈고 닦아야 한다.

 

 

========================================================================

 

 

## 객체, 설계

"이론이 먼저일까, 실무가 먼저일까?"

글래스에 따르면 어떤 분야를 막론하고 이론을 정립할 수 없는 초기에는 실무가 먼저 급속한 발전을 이룬다고 한다.

(실무 -> 정리 -> 이론)

실무를 관찰한 결과를 바탕으로 이론을 정립하는 것이 최선이다.

 

소프트웨어 설계 & 소프트웨어 유지보수

기법 & 도구: 초기부터 성공적으로 적용하고 발전시켜 왔다.

반복적으로 적용되던 기법들을 이론화한 것들이 대부분이다.

실무에 초점을 맞추는 것이 효과적이다.

'코드' 그 자체다.

 

추상적인 개념과 이론은 훌륭한 코드를 작성하는 데 필요한 도구

개발자는 구체적인 코드를 만지며 손을 더럽힐 때 가장 많은 것을 얻어가는 존재다.

 

로버트 마틴(Robert C. Martin): 클린 소프트웨어: 애자일 원칙과 패턴, 그리고 실천 방법

1. 실행 중에 제대로 동작

2. 변경을 위해 존재

3. 코드를 읽는 사람과 의사소통하는 것

 

더 큰 문제는 변경에 취약하다는 것

객체 사이의 의존성(dependency)와 관련된 문제

의존성은 변경에 대한 영향을 암시

의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼 있다.

 

의존성을 완전히 없애는 것이 정답은 아니다.

객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이다.

최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것이다.

 

결합도(coupling): 객체 사이의 의존성이 과한 경우 높다고 한다.

객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이어야 한다.

 

자율적인 존재로 만들면 되는 것

마음대로 접근할 수 있기 때문이다.

자율적인 존재가 되도록 설계를 변경

 

캡슐화(encapsulation)

캡슐화를 통해 객체 내부로의 접근을 제한하면 객체와 객체 사이의 결합도를 낮출 수 있기 때문에 설계를 좀 더 쉽게 변경할 수 있게 된다.

 

인터페이스(interface)

구현(implementation)

인터페이스만을 공개하는 것은 객체사이의 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙이다.

의존성 제거: 내부 구현이 성공적으로 캡슐화됨

변경 용이성의 측면에서도 확실히 개선됐다고 말할 수 있다.

 

스스로 해결하도록 코드를 변경

직관을 따랐고 그 결과로 코드는 변경이 용이하고 이해 가능하도록 수정됐다.

스스로, 직관, 이해하기 쉽다.

객체의 자율성을 높이는 방향으로 설계를 개선했다.

응집도(cohesion): 밀접하게 연관된 작업만을 수행하고 연관성이 없는 작업은 다른 객체에게 위임하는 개체는 응집도가 높다.

 

프로세스(Process)

데이터(Data)

절차적 프로그래밍(Procedural Programming): 별도의 모듈에 위치시키는 방식

객체지향 프로그래밍(Object-Oriented Programming): 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식

 

적절한 트레이드 오프

캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것

 

책임의 이동(shift of responsibility)

자신을 스스로 책임

코드가 더 이해하기 쉬워졌다는 것이다.

설계를 어렵게 만드는 것은 의존성이라는 것을 기억하라.

객체 사이의 결합도를 낮춰야 한다.

높은 결합도는 변경하기 어려운 설계를 의미한다.

 

캡슐화 == 자율성 == 응집도 (같은 말임 ㅎㅎ)

어떤 경우에도 모든 사람을 만족시킬 수 있는 설계를 만들 수는 없다.

균형의 예술; 적잘한 트레이드오프의 결과물이라는 사실을 명심하자!

 

존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화(anthropomorphism)

이해하기 쉬운 코드를 작성하는 것이라고 설명했다.

모든 객체들이 자율적으로 행동하는 설계를 가리킨다.

 

기능구현: 쉽게 변경할 수 있는 코드

모든 요구사항을 수집하는 것은 불가능에 가깝다.

요구사항은 바뀔 수밖에 없다.

 

코드를 변경할 때 버그가 추가될 가능성이 높기 때문이다.

코드를 수정하지 않는다면 버그는 발생하지 않는다.

코드 수정은 버그가 발생할 가능성을 높인다.

수정하려는 의지를 꺾는다는 것이다.

코드 수정을 회피하려는 가장 큰 원인은 두려움이다.

요구사항 변경으로 인해 버그를 추가할지도 모른다는 불확실성에 기인한다.

 

우리가 진정으로 원하는 것은 변경에 유연하게 대응할 수 있는 코드다.

적어도 앞의 예제를 통해 코드 변경이라는 측면에서는 객체지향이 과거의 다른 방법보다 안정감을 준다는 사실에 공감했으면 좋겠다.

 

변경 가능한 코드란, 이해하기 쉬운 코드다.

객체지향 패러다임은 여러분이 세상을 바라보는 방식대로 코드를 작성할 수 있게 돕는다.

상호작용은 객체 사이에 주고 받는 메시지로 표현된다.

 

메시지를 전송하기 위한 이런 지식이 두 객체를 결합시키고 이 결합이 객체 사이의 의존성을 만든다.

의존성을 적절하게 관리하는 설계 == 의존성을 적절하게 조절함으로 써

 

 

========================================================================

 

 

## 객체지향 프로그래밍

협력, 객체, 클래스

 

클래스(class): 속성 & 메서드

어떤 객체, 어떤 상태, 어떤 행동

공동체의 일원

객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진  객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라.

훌륭한 협력 -> 훌륭한 객체 -> 훌륭한 클래스

 

도메인(domain): 분야

class를 확장할 수 있게 한다.

클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다. (캡슐화)

그 이유는 경계의 명확성이 경계의 자율성을 보장하기 때문이다. (<-> 의존성)

 

자율적인 객체

상태(state) & 행동(behavior)

복합적인 존재, 스스로 판단하고 행동하는 자율적인 존재

 

데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화

접근을 통제할 수 있는 접근 제어(access control)

접근 수정자(access modifier)를 제공한다.

 

퍼블릭 인터페이스(public interface)

구현(implementation)

인터페이스와 구현의 분리(separation of interface and implementation)

 

클래스 작성자(class creator)

클라이언트 프로그래머(client programmer)

내부 구현을 마음대로 변경할 수 있다.

구현은닉(implementation hiding)이라고 부른다.

 

분리하기 위해 노력해야 한다.

private 영역 안에 감춤으로써 변경으로 인한 혼란을 최소화할 수 있다.

 

의미를 좀 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현하라.

그 개념이 비록 하나의 인스턴스 변수만 포함하더라도

개념을 명시적으로 표현하는 것은 전체적인 설계의 명확성과 유연성을 높이는 첫걸음이다.

 

상호작용을 협력(Collaboration)이라고 부른다.

요청(request) & 응답(response)

메시지를 전송(send a message)

메시지를 수신(receive a message)

메서드(method)

메시지와 메서드를 구분하는 것은 매우 중요하다.

다형성(polymorphism)

 

상속(inheritance)

추상화(abstration)

추상화시켜 상속을 구현한다.

상속을 하면 다형성이 생긴다.

인스턴스를 생성할 필요가 없기 때문에 추상 클래스(abstract class)로 구현했다.

추상 메서드(abstract method)

 

자식 클래스에서 오버라이딩한 메서드가 실행될 것이다.

부모 클래스에 기본적인 알고리즘의 흐름을 구현하고

중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 `TEMPLATE METHOD 패턴`이라고 부른다.

 

오버라이딩(overriding): 재정의

오버로딩(overloading): 동명 메소드, 분리 동작, 공존한다.

 

정적: 컴파일 시간 의존성

동적: 실행 시간 의존성

 

### 동적으로 구현하는 경우

context 유지능력 == 집중력

코드의 의존성, 실행 시점의 의존성이 서로 다를 수 있다는 것이다.

단점: 다르면 다를수록 코드를 이해하기 어려워진다는 것이다. 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다. 

장점: 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다.

트레이드 오프의 산물

 

설계가 유연해질수록 코드를 이해하고 디버깅하기에는 점점 더 어려워진다는 사실을 기억하라.

반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장 가능성은 낮아진다는 사실도 기억하라.

이해, 디버깅 vs 재사용성, 확장가능성

 

약간만 추가하거나 수정해서 새로운 클래스를 만들 수 있다면 좋을 것이다.

재사용하는 것일 것이다. 상속이다.

모든 속성과 메서드를 그대로 물려받는, 상속의 강력함을 잘 보여주는 예라고 할 수 있다.

 

차이에 의한 프로그래밍(programming by difference)

코드 중복을 제거하고 여러 클래스 사이에서 동일한 코드를 공유할 수 있게 된다.

슈퍼클래스(super class)

부모클래스(parent class)

부모(parent)

직계조상(immediate ancestor)

자식(child)

직계 자손(immediate descendant)

직접적인 자손(direct descendant)

조상(ancestors)

자손(descendants)

 

부모 클래스이 인터페이스를 포함하게 된다.

외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.

자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)

자식 클래스가 위에 위치한 부모 클래스로 자동적으로 타입 캐스팅되는 것처럼 보이기 때문에 업캐스팅이라는 용어를 사용한다.

 

다형성

동일한 메시지를 전송하지만,

실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.

시간 의존성을 다르게 만들 수 있는 객체지향의 특성을 이용해 서로 다른 메서드를 실행할 수 있게 된다.

추상적인 개념이다.

(유연하다..)

 

동일한 인터페이스를 물려받았기 때문이다.

인터페이스를 통일하기 위해 사용한 구현 방법이 바로 상속인 것이다.

하나의 타입 계층으로 묶을 수 있다.

 

실행 시점에 결정한다는 공통점

지연 바인딩(lazy binding) == 동적 바인딩(dynamic binding)

초기 바인딩(early binding) == 정적 바인딩(static binding)

 

구현 상속(implementation inheritance) == 서브클래싱(subclassing)

코드 재사용 목적

 

인터페이스 상속(interface inheritance) == 서브타이핑(subtyping)

다형적인 협력을 위해 인터페이스 공유

 

추상 파생 클래스(Abstract Base Class, ABC)

대신해서 사용될 수 있다.

업캐스팅이 적용되며 협력은 다형적이다.

 

추상화의 계층만 따로 떼어 놓고 살펴보면 

요구사항의 정책을 높은 수준에서 서술할 수 있다는 것이다.

설계가 좀 더 유연해진다는 것이다.

 

상위 개념만으로도 도메인의 중요한 개념을 설명

추상화를 이용한 설계는 필요에 따라 표현의 수준을 조정하는 것을 가능하게 해준다.

협력 흐름을 기술

디자인 패턴(design pattern)

프레임워크(framework)

객체지향의 메커니즘을 활용

상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.

 

책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분의 경우 좋지 않은 선택이다.

(다형성으로 해결해야함)

항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택하라.

 

컨텍스트 독립(context independency)

모든 것들이 트레이드 오프의 대상이 될 수 있다는 사실이다.

 

상속은 코드를 재상용하기 위해 널리 사용되는 방법이다.

상속보다는 합성(composition): 인스턴스 변수로 포함해서 재사용하는 방법

기능적인 관점에서 완벽히 동일하다.

 

합성을 선호화는 이유는?

상속은 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.

자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다.

설계가 유연하지 않다는 것이다.

따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다. (Adaptor Pattern)

 

합성: 인터페이스에 정의된 메시지를 통해서 코드를 재사용하는 방법

효과적인 캡슐화

인스턴스 교체하는 것이 비교적으로 쉽기 때문에 설계를 유연하게 만듬

재사용을 위해서는 상속보다는 합성을 선호하는 것이 더 좋은 방법

 

상속을 절대 사용하지 말라는 것은 아니다.

다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함꼐 조합해서 사용할 수 밖에 없다.

객체들 사이의 상호작용, 적절한 객체에게 적절한 책임을 할당하는 것이다.

 

 

========================================================================

 

 

## 역할, 책임, 협력 (구현 매커니즘)

역할(role): 적절한 객체

책임(responsibility): 적절한 책임

협력(collaboration): 공동체를 창조

 

### 협력

메시지 전송(message sending)

메서드를 실행해 요청에 응답한다.

스스로의 결정에 따라 행동하는 객체다.

객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화 하는 것이다.

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

협력이 바뀌면 객체가 제공해야 하는 행동 역시 바뀌어야 한다.

객체를 구성하는 행동과 상태 모두를 결정한다.

문맥(context)을 제공한다.

 

### 책임

무엇을 할 수 있는가? 하는 것(doing)

- 객체를 생성하거나 계산을 수행

- 다른 객체의 행동을 시작

- 다른 객체의 활동을 제어하고 조절

무엇을 알고 있는가? 아는 것(knowing)

사적인 정보

- 관련된 객체

- 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것

 

여러 개의 메시지로 분할

커다란 책임으로 자라는 것

visibility: private, public

 

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

적절한 책임을 제공하고, 적절한 책임을 적절한 객체에게 할당

객체 지향 설계에서 가장 중요한 것은 책임이다. (캡슐화)

 

후보(Candidate)

책임(Responsibility)

협력자(Collaborator)

RRC(Role-Responsibility-Collaboration)

효과적으로 일하는 사람들은 추상적이고 가상적인 것보다는 구체적이고 실재적인 것을 사용하는 경향이 있다고 설명한다.

 

`INFORMATION EXPEERT (정보 전문가) 패턴`

일상 생활에서 도움을 요청하는 방식

객체들 역시 협력에 필요한 지식과 방법을 가장 잘 알고 있는 객체에게 도움을 요청한다.

시스템의 책임을 완료하는 데 필요한 더 작은 책임을 찾아내고 이를 객체들에게 할당하는 반복적인 과정을 통해 모양을 갖춰간다.

 

정보 전문가가 아닌 다른 객체에게 책임을 할당하는 것이 더 적절한 경우도 있다.

하지만 기본적인 전략은 책임을 수행할 정보 전문가를 찾는 것이다.

자율적인 책임을 갖는 객체를 만들 가능성이 높아지기 때문이다.

 

책임 주도 설계(Responsibility-Driven Design, RDD)

유연하고 견고한 객체지향시스템을 위해 가장 중요한 재료가 바로 책임

어떤 클래스에 구현할 것인가?

메시지가 객체를 결정

행동이 상태를 결정

 

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

최소한의 인터페이스(minimal interface): 크지도, 작지도 않은 꼭 필요한 크기의 퍼블릭 인터페이스

추상적인 인터페이스(abstract interface): 무엇(what)을 하는지만 표현

추상적인 동시에 최소한의 크기를 유지할 수 있었다.

미니멀리즘 인터페이스를 가지게 하고 싶다면 메시지가 객체를 선택하게 하라.

name + params = method

 

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

때문에, 캡슐화를 저해한다.

 

문맥 안에서 객체를 생각해야 한다.

다른 객체에게 무엇을 제공하고 다른 객체로부터 무엇을 얻어야 하는지를 고민해야만 훌륭한 책임을 수확

협력에 초점을 맞춰야만 응집도가 높고,

결합도가 낮은 객체들을 창조할 수 있다.

상태는 단지 객체가 행동을 정상적으로 수행하기 위한 재료일 뿐이다.

 

행동이 중요하다.

행동이 바로 객체의 책임이 된다.

 

코드 중복은 모든 문제의 근원

역할을 이용하면 불필요한 중복 코드를 제거할 수 있다.

따라서 책임과 역할을 중심으로 협력을 바라보는 것이 바로 변경과 확장에 용이한 유연한 설계로 나아가는 첫걸음이다.

 

추상클래스 & 인터페이스

협력의 관점에서는 둘 모두 역할을 정의할 수 있는 구현 방법이라는 공통점을 공유한다.

일종의 슬롯, 구체적인 객체들의 타입을 캡슐화하는 추상화라는 것

객체에게 중요한 행동

객체 자체가 아닌 협력에 초점을 맞출 수

유연하고 재사용 가능한 협력을 만들 수 있는가?

 

역할은 객체가 참여할 수 있는 일종의 슬롯

만약 동일한 종류의 객체가 하나의 역할을 항상 수행한다면 둘은 동일한 것이다.

서로 다른 방법으로 실행할 수 있는 책임의 집합

역할: 하나의 슬롯

 

한 종류라면 간단하게 객체로 간주

협력(Collaboration) -> 역할(Role) -> 객체(Object) -> 클래스(Class)

- 다형성

- 인터페이스

- 추상화, 상속

 

협력을 지속적으로 정제하다 보면 두 협력이 거의 유사한 구조를 보인다는 것을 발견하게 될 것

역할 모델링(Role Modeling)

협력 패턴으로 추상화

유연하고 재사용 가능한 시스템

역할의 가장 큰 장점은 설계의 구성 요소를 추상화할 수 있다는 것

 

협력의 관점에서 역할에도 동일하게 적용될 수 있다.

세부 사항에 억눌리지 않고도 상위 수준의 정책을 쉽고 간단하게 표현할 수 있다는 것이다.

불필요한 세부 사항을 생략하고, 핵심적인 개념을 강조할 수 있다.

상황을 추상화 할 수 있다.

메시지를 전송하며 협력

 

설계를 유연하게 만들 수 있다는 것이다. 대체 가능하다.

다양한 객체들을 수용할 수 있게 해주므로 협력을 유연하게 만든다.

다양한 종류의 할인 정책과 할인 조건에도 적용될 수 있는 협력을 만들었다는 것을 의미한다.

기존 코드를 수정하지 않고도 새로운 행동을 추가할 수 있다.

추상화: 은유는 협력 안에서 역할을 수행하는 객체라는 관점이 가진 입체적인 측면들을 훌륭하게 담아낸다.

 

객체는 다양한 역할을 가질 수 있다.

context 일시적으로 오직 하나의 역할만이 보여진다는 점에 주의하라.

특정 협력은 객체의 한 가지 역할만 바라 볼 수 있다.

협력의 관점에서 동일한 역할을 수행하는 객체들은 서로 대체 가능하다.

캡슐화 & 다형성

 

 

========================================================================

 

 

## 설계 품질과 트레이드오프

가장 중요한 것은 책임이다.

책임이 적절하지 못하면 역할 역시 협력과 조화를 이루지 못한다.

책임이 객체지향 애플리케이션 전체의 품질을 결정하는 것이다.

 

책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 깊이 연관돼 있다는 것이다.

설계는 변경을 위해 존재, 어떤 식으로든 비용

훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것이다.

응집도가 높고 서로 느슨하게 결합

 

가끔씩은 좋은 설계보다는 나쁜 설계를 살펴보는 과정에서 통찰을 얻기도 한다.

 

구현은 불안정하기 때문에 변하기 쉽다.

상태 변경은 인터페이스의 변경을 초래하며 이 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼지게 된다.

따라서 데이터에 초점을 맞추는 설계는 변경에 취약할 수밖에 없다.

 

책임이 무엇인가?

접근자(accessor) & 수정자(mutator)

캡슐화의 원칙에 따라 이 속성들을 클래스 외부로 노출해서는 안 된다.

 

### 캡슐화

내부 구현을 외부로부터 감추기 위해서

나중에 변경될 가능성이 높은 어떤 것

파급효과를 적절하게 조절

상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제

 

구현: 변경될 가능성이 높은 부분

인터페이스: 상대적으로 안정적인 부분을 인터페이스

 

캡슐화

가장 중요한 원리, 대상을 단순화하는 추상화의 종류

불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화

 

복잡성을 다루기 위한 갖아 효과적인 도구는 추상화

복잡성을 취급하는 주요한 추상화 방법은 캡슐화

복잡성이 잘 캡슐화될 것이라고 보장할 수는 없다.

 

요구사항이 변경

불안정한 부분과 안정적인 부분을 분리해서 변경의 영향을 통제할 수 있기 때문이다.

변경될 수 있는 어떤 것이라도 캡슐화

 

유지보수성이 목표다.

여기서 유지보수성이란 두려움 없이, 주저함 없이, 저항감 없이 코들르 변경할 수 있는 능력을 말한다.

가장 중요한 동료는 캡슐화다. (보험)

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

우리는 시스템의 한 부분을 다른 부분으로부터 감춤으로써 뜻밖에 피해가 발생할 수 있는 가능성을 사전에 방지할 수 있다.

만약 시스템이 완전히 캡슐화 된다면 우리는 변경으로부터 완전히 자유로워질 것이다.

만약 시스템의 캡슐화가 크게 부족하다면, 우리는 변경으로부터 자유로울 수 없고, 결과적으로 시스템은 진화할 수 없을 것이다.

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

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

 

응집도

모듈에 포함된 내부 요소들이 연관돼 있는 정도

긴밀하게 협력한다면 그 모듈은 높은 응집도

 

결합도

의존성의 정도를 나타내며

협력에 필요한 적절한 수준의 관계만

 

좋은 설계

높은 응집도 & 낮은 결합도 -> 모듈로 구성된 설계

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

응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정

모듈의 일부만 변경된다면 응집도가 낮은 것이다.

 

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

결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도

 

변경의 원인

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

인터페이스에 대해 프로그래밍하라.

 

어떤 설계를 쉽게 변경할 수 있다면 높은 응집도를 가진 요소들로 구성돼 있고 요소들 사이의 결합도가 낮을 확률이 높다.

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

 

접근자와 수정자에 과도하게 의존하는 설계 방식을 추측에 의한 설계 전략(design-by-guessing strategy)

취약한 설계 & 캡슐화를 위반

데이터 중심 설계는 객체의 캡슐화를 약화시키기 때문에 클라이언트가 객체의 구현에 강하게 결합된다.

시스템 전체가 요동친다....

 

서로 다른 이유로, 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다.

변경과 아무 상관이 없는 코드들이 영향을 받게된다.

1. 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것은, 모듈의 응집도가 낮을 때 발생하는 대표적인 증상이다.

2. 어떤 요구사항 변경을 수용하기 위해, 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거

 

단일 책임 원칙(Single Responsibility Principle, SRP)

클래스는 단 한 가지의 변경 이유만 가져야 한다는 것

책임이라는 말이 변경의 이유라는 의미로 사용

 

캡슐화의 원칙을 위반했기 때문이다.

가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면

캡슐화를 위반하는 것이다.

 

코드 중복은 악의 근원이다.

따라서 코드 중복을 초래할 수 있는 모든 원인을 제거하는 것이 중요하다. (변경에 취약)

해결책은 캡슐화를 강화시키는 것이다.

스스로 증가시키도록 책임을 이동시킨 것이다.

 

상태와 행동을 객체라는 하나의 단위로 묶는 이유?

스스로 자신의 상태를 처리할 수 있게 하기 위해서다.

이 객체가 어떤 데이터를 포함해야 하는가?

데이터에 대해 수행해야 하는 오퍼레이션은?

 

파급효과(ripple effect): 캡슐화가 부족하다는 명백한 이유다...

내부 속성을 외부로 감추는 것은 데이터의 캡슐화

변할 수 있는 어떤 것이라도 감추는 것

 

결합도가 높을 경우, 다른 객체에게 변경의 영향이 높다.

유연한 설계를창조하기 위해서는 캡슐화를 설계의 첫 번째 목표로 삼아야 한다.

하나의 변경을 수용하기 위해 코드의 여러 곳을 동시에 변경해야 한다는 것은 설계의 응집도가 낮다는 증거다.

 

 

========================================================================

 

 

## 책임 할당하기

책임에 초점을 맞추는 것이다.

어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것

동일한 문제를 해결할 수 있는 다양한 책임 할당 방법이 존재하며,

어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다.

다양한 관점에서 설계를 평가

결합도, 캡슐화, 다양한 기준에 다라 책임을 할당하고 결과를 트레이드오프할 수 있는 기준을 배우게 될 것이다.

 

데이터 < 행동

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

메시지가 객체를 선택하게 해야 한다.

메시지가 클라이언트의 의도를 표현한다는 사실에 주목

메시지를 수신하기로 결정된 객체는 메시지를 처리할 책임을 할당 받게 된다.

 

문맥을 고려 -> 적절한 책임

메시지를 결정한 -> 객체의 상태를 저장

- 시스템 책임을  파악

- 더 작은 책임으로 분할

- 적절한 객체, 역할을 찾아 책임을 할당

- 책임질 적절한 객체

- 역할에게 책임을 할당함으로써 두 객체가 협력

 

GRASP(General Responsibility Assignment Software Pattern)

일반적인 책임 할당을 위한소프트웨어 패턴

 

 

도메인에 대략적인 모습

실용적이면서도 유용한 모델

자신의 상태를 스스로 처리하는 자율적인 존재

어떻게 수행할지 스스로 결정할 수 있기 때문이다.

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

 

자율성이 높은 객체들로 구성된 협력 공동체를 구축할 가능성이 높아지는 것이다.

- LOW COUPLING(낮은 결합도) 패턴

- HIGH COHESION(높은 응집도) 패턴

 

변경의 이유에 따라 클래스를 분리해야 한다.

인스턴스 변수가 초기화되는 시점

함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.

메서드들이 인스턴스 변수를 사용하는 방식

메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 크래스의 응집도가 낮다고 볼 수 있다.

속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리해야 한다.

(초기화 & 사용에 따라 묶는다.)

 

하나 이상의 이유로 변경돼야 한다면 응집도가 낮은 것이다.

다른 속성들을 초기화 하고

속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은 것이다.

메서드를 응집도 높은 작은 메서드로 잘게 분해해 나가면 숨겨져 있던 문제점이 명확하게 드러나는 경우가 많다.

 

응집도를 높이면, 캡슐화가 낮아질 수 있다는 점을 명심하자.

모든 것은 Trade-off 이다...

 

역할을 대체할 클래스들 사이에서 구현을 공유해야 할 필요가 있다면 추상 클래스를 사용하면 된다.

`POLYMORPHISM(다형성) 패턴`

프로그램을 수정하기 어렵고 변경에 취약하다면,

다형성을 이용해 새로운 변화를 다루기 쉽게 확장하라고 권고한다.

(context 가 중요해진다.)

 

`PROTECTED VARIATION 패턴`

인터페이스 뒤로 캡슐화할 수 있다는 것

 

책임을 중심으로 설계

객체지향은 도메인의 개념과 구조를 반영한 코드를 가능하게 만들기 때문에,

도메인의 구조가 코드의 구조를 이끌어 내는 것은 자연스러울뿐만 아니라 바람직한 것이다.

도메인 분석 -> 코드 개발

 

설계를 주도하는 것은 변경 (for 유지보수성)

이해하고 수정하기 쉽도록 최대한 단순하게 설계하는 것

코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것

 

상속을 이용하고 있기 때문에

해결 방법은 상속 대신 합성을 사용하는 것

유연성의 정도에 따라 결합도를 조절할 수 있는 능력은 객체지향 개발자가 갖춰야 하는 중요한 기술 중 하나다.

 

내부 구조를 변경하는 것을 리팩터링(Refactoring)이라고 부른다.

- 코드를 전체적으로 이해하는 데 너무 많은 시간이 걸린다.

- 하나의 메서드 안에서 너무 많은 작업을

- 메서드 내부의 일부 로직만 수정하더라도, 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.

- 로직의 일부만 재사용하는 것이 불가능하다.

- 코드를 복사해서 붙여넣는 것뿐이므로 코드 중복을 초래하기 쉽다.

 

몬스터 메서드(monster method)

응집도가 낮은 메서드는 로직의 흐름을 이해하기 위해 주석이 필요한 경우가 대부분,

메서드가 명령문들의 그룹으로 구성되고 각 그룹에 주석을 달아야 할 필요가 있다면 그 메서드의 응집도는 낮은 것

주석을 추가하는 대신 메서드를 작게 분해해서 각 메서드의 응집도를 높여라.

작은 메서드들로 조합된 메서드는 마치 주석들을 나열한 것처럼 보이기 때문에 코드를 이해하기도 쉽다.

메서드를 응집도 있는 수준으로 분해!!

 

코드를 작은 메서드들로 분해하면 전체적인 흐름을 이해하기도 쉬워진다.

동시에 너무 많은 세부사항을 기억하도록 강요하는 코드는 이해하기도 어렵다.

큰 메서드를 작은 메서드들로 나누면 한 번에 기억해야 하는 정보를 줄 일 수 있다.

더 세부적인 정보가 필요하다면 그때 각 메서드의 세부적인 구현을 확인하면 되기 때문이다.

 

캡슐화, 결합도, 응집도를 이해하고,

훌륭한 객체지향 원칙을 적용하기 위해 노력한다면,

책임 주도 설계 방법을 단계적으로 따르지 않더라도 유연하고 깔끔한 코드를 얻을 수 있을 것이다.

 

 

========================================================================

 

 

## 메시지와 인터페이스

훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 객체를 지향해야 한다.

객체가 수행하는 책임에 초점을 맞춰야 한다.

가장 중요한 재료는 메시지다. (interface)

퍼블릭 인터페이스를 구성한다.

 

클라이언트-서버(Client-Server) 모델

메시지(message)

메시지 전송(message sending)

메시지 패싱(message passing)

메시지 전송자(message sender)

메시지 수신자(message receiver)

오퍼레이션명(operation name)

인자(argument): overload

 

컴파일 시점과 실행 시점에 동일하다는 것

메시지와 메서드의 구분은 느슨하게 결합될 수 있게 한다.

스스로 결정할 수 있는 자율권

메시지라는 얇고 가는 끈을 통해 연결된다.

유연하고 확장 가능한 코드를 작성할 수 있게 만든다.

 

외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스

client -> 메시지 전송 -> server -> 오프레이션(opration) -> 메서드 실행

시그니처(signature): 오퍼레이션이나 메서드의 명세, 이름과 인자의 목록

 

최소한의 인터페이스 & 추상적인 인터페이스

최소주의, 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다.

 

디미터 법칙(Law of Demeter)

강하게 결합되지 않도록 협력 경로를 제한하라는 것

"낯선 자에게 말하지 말라(don't talk to strangers)"

"오직 인접한 이웃하고만 말하라(only talk to your immeditate neighbors)"

Chaining

부끄럼 타는 코드(shy code)

 

기차 충돌(train wreck)

캡슐화는 무너지고, 내부 구현에 강하게 결합

단지 자신이 원하는 것이 무엇인지를 명시하고 단순히 수행하도록 요청한다.

원칙의 함정

 

묻지 말고 시켜라(Tell, Don't Ask)

내부에서 처리 (Exception)

로직은 메시지 수신자가 담당해야 할 책임일 것이다.

상태를 기반으로 결정을 내리는 것은 객체의 캡슐화를 위반한다.

 

연관된 정보와 행동을 함께 가지는 객체를 만들 수 있다.

함께 변경될 확률이 높은 정보와 행동을 하나의 단위로 통합하자.

내부의 상태를 묻는 오퍼레이션을 인터페이스에 포함

객체가 책임져야 하는 어떤 행동이 객체 외부로 누수

오퍼레이션으로 대체 -> 인터페이스를 향상

퍼블릭 인터페이스의 품질을 향상

무엇을 하는지를 서술하자!

 

의도를 드러내는 인터페이스

메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것이다.

어떻게가 아니라 무엇을 하는지를 드러내는 것이다.

코드를 읽고 이해하기 쉽게 만들뿐만 아니라, 유연한 코드를 낳는 지름 길이다.

 

메서드의 내부 구현을 설명하는 이름

수행해야 하는 책임에 관해 고민

Message와 연관된

클라이언트의 관점에서 협력

 

이름을 짓는 것이 설계를 유연하게 만드는 이유

무엇을 하느냐에 초점을 맞추면 클라이언트의 관점에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층

차원을 나눈다.

 

선택자(Intention Revealing Selector)

여러분이 할 수 있는 한 가장 추상적인 이름을 메서드에 붙일 것이다.

추상화 == 유연성 == 확장성 == 유지보수성 == 난이도 높다 (개인역량 중요)

 

타입, 메서드, 인자 이름 모두 중요.

모두 결합되어 의도를 드러내는 이터페이스를 형성하자!

객체에게 묻지 말고 시키되, 구현 방법이 아닌 클라이언트의 의도를 드러내야 한다.

 

클라이언트의 의도를 표현하는 이름

객체의 퍼블릭 인터페이스에 어떤 이름이 드러나야 하는지에 대한 지침을 제공함으로써

코드의 목적을 명확하게 커뮤니케이션할 수 있게 해준디다.

암시적으로 두지 않고 좀 더 명시적으로 만들고 이름을 가지도록 강요한다.

 

하지만 절대적인 법칙은 아니다.

설계가 트레이드오프의 산물

초보자는 원칙을 맹목적으로 추종한다...

원칙에 정당성을 부여하고 억지로 끼워 맞추려고 노력한다...

 

현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하라.

원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지를 판단할 수 있는 능력을 기르는 것이다.

 

객체를 다른 객체로 변환하는 작업

객체의 내부 구조를 노출하고 있는가?

 

자료 구조라면 당연히 내부를 노출해야

원칙을 맹신하지 마라.

트레이드오프

경우에 따라 다르다.

 

명령-쿼리 분리(Command-Query Separation) 원칙

루틴(routine)

프로시저(procedure)

함수(function)

 

- 프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.

- 함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.

질문이 답변을 수정해서는 안된다...

 

버그를 찾기 어려운 이유는, 명령과 쿼리의 두 가지 역할을 동시에 수행하고 있었기 때문이다.

명령과 쿼리를 뒤섞으면 실행 결과를 예측하기가 어려워질 수 있다. (부수효과 - side effect)

가장 깔끔한 해결책은 명령과 쿼리를 명확하게 분리하는 것이다.

이러한 이유로 pure function 순수함수가 강조되는듯..!

 

분리함으로써 얻는 이점이 더 크다.

예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해질 것이다.

정적 분석 가능...

 

참조 투명성(referential transparency)

부수효과(side effect)

불변성(immutability)

객체지향 패러다임이 객체의 상태 변경이라는 부수효과를 기반으로 하기 때문에 참조 투명성은 예외에 가깝다.

명령-쿼리 분리 원칙을 사용하면 이 균열을 조금이나마 줄일 수 있다.

 

명령형 프로그래밍(imperative programming)

객체의 상태 변경에 집중하기 때문에

 

합수형 프로그래밍(functional programming)

참조 투명성의 장점

병렬 처리 예측하기 더 쉽다.

 

계약에 의한 설계(Design By Contract)

- 디미터 법칙

- 묻지 말고 시켜라

- 의도를 드러내는 인터페이스

- 명령-쿼리 분리 원칙

 

 

========================================================================

 

## 객체 분해

문제를 해결하기 위해서는 필요한 정보들을 먼저 단기 기억 안으로 불러들여야 한다.

용량을 초과하는 순간 문제 해결 능력은 급격하게 떨어지고 만다.

인지 과부하(cognitive overload)

 

정보의 수를 줄이기 위해 본질적인 정보만 남기고 불필요한 세부 사항을 걸러내면 문제를 단순화할 수 있을 것이다.

불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업을 추상화라고 부른다.

 

한 번에 다뤄야 하는 문제의 크기를 줄이는 것이다.

사람들은 한 번에 해결하기 어려운 커다란 문제에 맞닥뜨릴 경우 해결 가능한 작은 문제로 나누는 경향이 있다.

다시 더 작은 문제로 나눌 수 있다.

큰 문제를 해결 가능한 작은 문제로 나누는  작업을 분해(decomposition)라고 부른다.

 

더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초월할 수 있다.

추상화와 분해는 인간이 세계를 인식하고 반응하기 위해 사용하는 가장 기본적인 사고 도구라고 할 수 있다.

따라서 추상화와 분해가 인류가 창조한 가장 복잡한 분야의 문제를 해결하기 위해 사용돼 왔다고 해도 놀랍지 않을 것이다.

그 분야는 바로 소프트웨어 개발 영역이다.

 

프로시저 추상화(procedure abstraction)

데이터 추상화(data abstraction)

기능 분해(functional decomposition)

알고리즘 분해(algorithmic decomposition)

타입을 추상화(type abstraction)

프로시저를 추상화(procedure abstraction)

 

추상화 데이터 타입(Abstract Data Type)

객체지향(Object-Oriented)

협력 공동체: 역할과 책임을 수행하는 객체

 

복잡성(인지과부하)을 극복하는 

알고리즘 분해, 기능 분해

추상화의 단위는 프로시저이며 시스템은 프로시저 단위로 분해된다.

상세한 구현 내용을 모르더라도 인터페이스만 알면 프로시저를 사용할 수 있기 때문이다.

 

하향식 접근법(Top-Down Approach)

최상위(topmost) 기능

작은 단계의 하위 기능으로 분해해 나가는 방법을 말한다.

각 세분화 단계는 바로 위 단계보다 더 구체적, 덜 추상적 더 구체적이며 덜 추상적인 하위 기능

하향식 기능 분해 방식이 가지는 문제점을 이해하는 것은 유지보수 관점에서 객체지향의 장점을 이해할 수 있는 좋은 출발점

 

의사 코드(pseudo  code)

트리(tree)

하향식 기능 분해는  논리적이고 체계적인 시스템 개발 절차를 제시한다.

 

하나의 메인 함수

함수를 빈번하게 수정

사용자 인터페이스와 강하게 결합

하향식 분해는 너무 이른 시기에 실행순서를 고정, 유연성과 재사용성이 저하된다.

데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.

 

하지만 불행하게도 소프트웨어는 항상 변경된다.

하향식 접근법, 기능분해 -> 변경에 취약한 설계...?

 

최초에 릴리스됐던 당시의 모습을 그대로 유지하지는 않는다.

기존 코드의 빈번한 수정으로 인한 버그 발생 확률이 높아지기 때문에 시스템은 변경에 취약해질 수밖에 없다.

 

하향식 접근은 근본적으로 변경에 불안정한 아키텍쳐를 낳는다.

따라서 사용자 인터페이스를 변경하는 유일한 방법은 전체 구조를 재설계하는 것뿐이다.

관심사의 분리: 축(차원)을 나눈다.

 

하향식 기능 분해의 가장 큰 문제점

추적하기 어렵다는 것이다.

지역변수, 인자, 전역 변수

의존성과 결합도의 문제다.

테스트의 문제

 

데이터와 함께 변경되는 부분과 그렇지 않은 부분을 명확하게 분리해야 한다.

잘 정의된 퍼블릭 인터페이스

의존성 관리의 핵심

정보은닉 & 모듈

 

이미 해결된 알고리즘을 문서화하고 서술하는 데는 훌륭한 기법이다.

커다란 소프트웨어를 설계하는 데 적합한 방법은 아니다.

 

정보은닉(information hiding) & 모듈

변경의 방향에 맞춰 시스템을 분해하는 것이다.

변경되는 부분, 감춰야 한다는 것이 핵심이다.

 

시스템을 분할하는 모듈 분할 원리다.

비밀을 내부로 감추고,

비밀을 결정하고 모듈을 분해한 후에는 기능 분해를 이용해 모듈에 필요한 퍼블릭 인터페이스

감춰야 하는 비밀을 찾아라.

 

복잡성 == 변경 가능성

 

모듈 내부에만 영향을 미친다.

수정하고 디버깅하기 더 용이하다.

 

비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.

전역변수와 전역함수를 제거

변수와 함수를 모듈 내부에 포함

네임스페이스의 오염을 방지하는 동시에 이름 충돌(name collision)의 위험을 완화한다.

 

모듈화의 핵심은 데이터다.

데이터와 함수가 통합된 한 차원 높은 추상화를 제공하는 설계 단위다.

좀 더 높은수준의 추상화 추상화 메커니즘이 필요한, 추상 데이터 타입

 

타입(type)

내장 타입(built-in type) 제공

데이터 추상화(data abstraction)의 개념 제안

오퍼레이션의 집합을 정의

제공된 오퍼레이션을 통해서만 조작

외부로부터 보호

여러 개의 인스턴스를 생성

 

클래스는 추상 데이터 타입인가?

객체지향 프로그래밍(Object-Oriented Programming)

타입 추상화는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법이다.

즉, 동일한 메시지에 대해 서로 다르게 반응한다. (다형성)

절차 추상화(procedural abstarction)

 

새로운 로직을 추가하기 위해 클라이언트 코드를 수정할 필요가 없다는 것을 의미한다.

개방-폐쇄 원칙(Open-Closed Principle, OCP)

 

참여할 협력을 결정하고 협력에 필요한 책임을 수행하기 위해, 어떤 객체가 필요한지에 관해 고민하라.

그 책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화하라. (다형성)

 

 

========================================================================

 

 

## 의존성 관리하기

응집도 높은 객체들로 구성된다.

초점이 명확하고 한 가지 일만 잘하는 객체를 의미한다.

과도한 협력은 설계를 곤경에 빠트릴 수 있다.

협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다.

 

UML(Unified Modeling Language)

실체화 관계(realization)

연관 관계(association)

의존 관계(dependency)

일반화/특수화 관계(generalization/specialization)

합성 관계(composition)

집합 관계(aggregation)

 

의존성 전이(transitive dependency)

모든 경우에 의존성이 전이되는 것은 아니다.

변경의 방향, 캡슐화 정도에 따라

 

직접 의존성(direct dependency)

간접 의존성(indirect dependency)

 

런타임 의존성(run-time dependency): 객체

컴파일타임 의존성(compile-time dependency): 클래스

유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 한다.

동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 한다.

 

실제로 협력할 객체가 어떤 것인지는 런타임에 해결

따라서 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해진다.

독립성, 객체와 타입 간의 관계를 잘 정의해야 좋은 실행 구조를 만들어낼 수 있다.

 

특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다.

컨텍스트의 독립성

더 유연해지고 변경에 탄력적으로 대응할 수 있게 될 것이다.

변경하기 더 쉽다.

다양한 컨텍스트에 적용할 수 있는 응집력 있는 객체를 만들 수 있고

객체 구성 방법을 재설정해서 변경 가능한 시스템으로 나아갈 수 있다.

 

컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다.

적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 한다.

- constructor

- setter

- method argument

 

의존성이 객체 사이의 협력을 가능하게 만들기 때문에 존재 자체는 바람직한 것이다.

재사용할 수 있는 가능성을 없애 버렸다.

의존성을 바람직하게 만드는 것이다.

의존성으로 대체할 수 있다.

 

의존성 자체가 나쁜 것이 아니라는 사실을 잘 보여준다.

의존성은 협력을 위해 반드시 필요한 것이다.

단지 바람직하지 못한 의존성이 문제일 뿐이다.

 

바람직한 의존성은 재사용성과 관련이 있다.

어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것이다.

의존성이 다양한 환경에서 재사용할 수 있다면 그 의존성은 바람직한 것이다.

 

독립적인 의존성은 바람직한 의존성이고,

특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성이다.

 

O: 느슨한 결합도(loose coupling), 약한 결합도(weak coupling)

X: 단단한 결합도(tight coupling), 강한 결합도(strong coupling)

 

재사용하기 쉽게 만드는 의존성

재사용을 방해한다면 결합도가 강하다고 표현한다.

 

한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다.

반대로 한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 약하게 결합된다.

필요한 정보 외에는 최대한 감추는 것이 중요하다.

 

추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해

특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복

알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지

 

구체 클래스

추상 클래스

인터페이스

 

지식의 양이 더 적기 때문에

인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해진다.

어떤 메시지를 수신할 수 있는지에 대한 지식

 

실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아진다는 것이다.

의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다는 것이다. 

 

명시적인 의존성(explicit dependency)

숨겨진 의존성(hidden dependency)

 

의존성이 명시적이지 않으면

직접 변경해야 한다는 것이다.

명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있다.

의존성은 명시적으로 표현돼야 한다. 의존성을 구현 내부에 숨기지 마라.

 

런타임 의존성으로 교체

클래스가 다른 클래스에 의존하는 것은 부끄러운 일이 아니다.

경계해야 할 것은 의존성 자체가 아니라, 의존성을 감추는 것이다.

숨겨져 있는 의존성을 밝은 곳으로 드러내서 널리 알려라.

그렇게 하면 설계가 유연하고 재사용 가능해질 것이다.

 

new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다.

결합도가 높으면 변경에 의해 영향을 받기 쉬워진다.

생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리

 

사용과 생성의 책임을 분리해서 -> 설계가 유연

사용과 생성의 책임을 분리

의존성을 생성자에 명시적으로

구체 클래스가 아닌 추상 클래스에 의존

클라이언트로 옮기는 것

 

의존성이 불편한 이유는, 항상 변경에 대한 영향을 암시

따라서 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다.

수정될 확률은 0에 가깝기 때문에

 

유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써

애플리케이션의 기능을 쉽게 확장할 수 있다.

 

객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다.

의존성을 관리하는 것이다.

 

객체들의 네트워크로 구성돼 있다.

메시지를 주고 받을 수 있게 조립하는 과정을 거쳐 만들어진다.

객체의 조합(객체의 선택과 연결방식)

목적(what)에 집중할 수 있어 시스템의 행위를 변경하기가 더 쉽다.

 

 

========================================================================

 

 

## 유연한 설계

개방-폐쇄 원칙(Open-Closed Principle, OCP)

소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.

 

기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있는 설계

어떻게 코드를 수정하지 않고도 새로운 동작을 추가할 수 있단 말인가?

컴파일 의존성을 고정시키고 런타임 의존성을 변경하라.

 

새로운 할인 정책을 추가해서 기능을 확장할 수 있도록 허용한다.

확장에 대해서는 열려 있다.

 

추상화에 의존하는 것

핵심적인 부분만 남기고 불필요한 부분은 생략

 

공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다.

다시 말해서 수정할 필요가 없어야 한다.

따라서 추상화 부분은 수정에 닫혀 있다.

개방-폐쇄 원칙을 가능하게 만드는 이유다.

(컨텍스트 주입)

새로운 문맥에 맞게 기능을 확장할 수 있다. 따라서 추상화는 설계의 확장을 가능하게 한다.

 

생성과 사용을 분리(separation use from creation)

GRASP 패턴

도메인 모델은 INFORMATION EXPERT

표현적 분해(representational decomposition)

행위적 분해(behavioral decomposition)

 

도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물)

인공적인 객체를 창조하라.

객체지향이 실세계를 모방해야 한다는 헛된 주장에 현혹될 필요가 없다.

우리가 애플리케이션을 구축하는 것은 사용자들이 원하는 기능을 제공하기 위해서지

실세계를 모방하거나 시뮬레이션하기 위한 것이 아니다.

 

의존성 주입(Dependency Injection)

외부에서 의존성의 대상을 해결한 후 이를 사용하는 객체 쪽으로 주입

메서드 호출 주입(method call injection)

 

의존성을 구현 내부로 감출 경우 의존성과 관련된 문제가 컴파일타입이 아닌 런타임에 가서야 발견된다는 사실을 알 수 있다.

숨겨진 의존성이 이해하기 어렵고 디버깅하기 어려운 이유는 문제점을 발견할 수 있는 시점을

코드 작성 시점이 아니라 실행 시점으로 미루기 때문이다.

 

추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.

의존성 역전 원칙(Dependency Inversion Principle, DIP)

불필요한 클래스들을 같은 패키지에 두는 것은 전체적인 빌드 시간을 가파르게 상승시킨다.

의존성을 역전시켜야만 유연하고 재사용 가능한 설계를 얻을 수 있다.

(의존성을 외부로 옮겨 주입 받는다.)

 

유연하고 재사용 가능한 설계가 항상 좋은 것은 아니다.

단순함 & 명확함, 단순하고 명확한 설계를 가진 코드는 읽기 쉽고 이해하기도 편하다.

변경하기 쉽고 확장하기 쉬운 구조를 만들어 내기 위해서는 단순함과 명확함의 미덕을 버리게될 가능성이 높다.

 

이 설계가 복잡한 이유는 무엇인가?

어떤 변경에 대비하기 위해 설계를 복잡하게 만들었는가?

정말 유연성이 필요한가?

심리학에 가깝다.

변경은 예상이 아니라 현실이어야 한다.

미래에 변경이 일어날지도 모른다는 막연한 불안감은 불필요하게 복잡한 설계를 낳는다.

아직 일어나지 않은 변경은 변경이 아니다.

 

긴밀한 커뮤니케이션

복잡성이 필요한 이유와 합리적인 근거를 제시하지 않는다면,

어느 누구도 설계를 만족스러운 해법으로 받아들이지 않을 것이다. 

 

협력과 책임이 중요하다는 것이다.

협력을 재사용할 필요가 없다면 설계를 유연하게 만들 당위성도 함께 사라진다.

 

 

========================================================================

 

 

## 상속과 코드 재사용

객체지향 프로그래밍의 장점 중 하나는 코드를 재사용하기가 용이하다는 것이다.

상속: 인스턴스 변수 & 메서드

 

중복 코드는 사람들의 마음속에 의심과 불신의 씨앗을 뿌린다.

혼란 속으로 내던져진다.

한쪽 코드만 수정하는 게 더 안전한 방법이 아닐까?

 

### DRY 원칙 반복하지 마라 (Don't Repeat Yourself)

중복 코드는 변경을 방해한다.

이것이 중복 코드를 제거해야 하는 가장 큰 이유다.

지식은 항상 변한다.

코드 역시 변경해야 한다.

수정하는 데 필요한 노력을 몇 배로 증가시킨다는 것이다.

수정과 테스트에 드는 비용을 증가시킬뿐만 아니라 시스템과 여러분을 공황상태로 몰아넣을 수도 있다.

모양이 유사하다는 것은 단지 중복의 징후일 뿐이다.

 

한 번, 단 한번(Once and Only Once)

단일 지점 제어(Single-Point Control) 원칙

중복 코드가 존재하기 때문에 언제 터질지 모르는 시한폭탄

 

상속은 결합도를 높인다.

그리고 상속이 초래하는 부모 클래스와 자식 클래스 사이의 강한 결합이 코드를 수정하기 어렵게 만든다.

super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라.

자식 클래스가 영향, 커다란 대가

 

부모 클래스의 작은 변경에도 자식 클래스는 컴파일 오류와 실행 에러라는 고통에 시달려야 할 수도 있다.

취약한 기반 클래스 문제(Fragile Base Class Problem, Brittle Base Class Problem)

단순한 변경이 전체 프로그램을 불안정한 상태로 만들어버릴 수도 있다.

 

부모 클래스를 점진적으로 개선하는 것은 어렵게 만든다.

캡슐화를 약화시키고 결합도를 높인다.

모든 자손들이 급격하게 요동칠 수 있다.

 

불필요한 인터페이스 상속 문제

메서드 오버라이딩의 오작용 문제

 

문서화해야 한다.

public 이나 protected 메서드 및 생성자가 어떤 오버라이딩 가능한 메서드를 호출하는지,

]어떤 순서로 하는지, 호출한 결과가 다음 처리에 어떤 영향을 주는지에 대해서도 반드시 문서화 해야한다.

 

내부 구현을 공개하고 문서화하는 것이 옳은가?

무슨 일(what)을 하는지를 기술해야 하고,

어떻게 하는지(how)를 설명해서는 안 된다는 통념을 어기는 것이 아닐까?

 

설계는 트레이드오프 활동이라는 사실을 기억하라.

상속은 코드 재사용을 위해 캡슐화를 희생한다.

완벽한 캡슐화를 원한다면 코드 재사용을 포기하거나 상속 이외의 다른 방법을 사용해야 한다.

 

두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하라.

메서드 추출을 통해 메서드를 동일한 형태로 보이도록 만들 수 있다.

 

부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라.

부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것보다

자식 클래스의 추상적인 메서드를 부모 클래스로 올리는 것이 재사용성과 응집도 측면에서 더 뛰어난 결과를 얻을 수 있다.

 

"변하는 것으로부터 변하지 않는 것을 분리하라"

"변하는 부분을 찾고 이를 캡슐화하라"

추상 메서드로 선언하고 자식 클래스에서 오버라이딩할 수 있도록 protected로 선언하자.

 

하나의 변경 이유

단일 책임 원칙을 준수하기 때문에 응집도가 높다.

내부 구현이 변경되더라도 자식 클래스는 영향을 받지 않는다.

낮은 결합도

의존성 역전 원칙도 준수

확장에는 열려 있고 수정에는 닫혀 있기 때문에

추상화에 의존하기 때문에 얻어지는 장점

 

따라서 객체 생성 로직에 대한 변경을 막기보다는 핵심 로직의 중복을 막아라.

핵심 로직은 한 곳에 모아 놓고 조심스럽게 캡슐화해야 한다.

공통적인 핵심 로직은 최대한 추상화해야 한다.

 

상속으로 인한 클래스 사이의 결합을 피할 수 있는 방법은 없다.

부작용이 퍼지지 않게 막는 것이다.

 

차이에 의한 프로그래밍(programming by difference)

점진적인 정의(incremental definition)

중복 코드를 제거하고 코드를 재사용

중복을 제거하기 위해서는 코드를 재사용 가능한 단위로 분해하고 재구성

중복 코드는 악의 근원

 

타이핑하는 수고를 덜어주는 수준의 문제가 아니다.

심각한 버그가 존재하지 않는 코드다.

노력과 테스트는 줄일 수 있다.

 

맹목적으로 상속을 사용하는 것이 위험하다는 사실을 깨닫기 시작했다.

정말로 필요한 경우에만 상속을 사용하라.

 

 

========================================================================

 

 

## 합성과 유연한 설계

합성에서 두 객체 사이의 의존성은 런타임에 해결된다.

상속: is-a

합성: has-a

 

상속: 내부 구현에 대해 상세하게 알아야 하기 때문에, 우아한 방법이라고 할 수 없다.

합성: 포함된 객체의 내부 구현이 변경되더라도 영향을 최소화할 수 있기 때문에 변경에 더 안정적인 코드를 얻을 수 있게 된다. 

 

합성은 유연한 설계를 얻을 수 있다.

코드 재사용을 위해서는 객체 합성이 클래스 상속보다 더 좋은 방법이다.

낮은 결합도로 대체할 수 있는 것이다.

서브클래싱에 의한 재사용을 화이트박스 재사용(white-box reuse)

블랙박스 재사용(black-box reuse)

 

- 불필요한 인터페이스 상속 문제

- 메서드 오버라이딩의 오작용 문제

- 부모 클래스와 자식 클래스의 동시 수정 문제

 

포워딩(forwarding): 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩 메서드(forwarding method)

 

대부분의 경우 구현에 대한 결합보다는 인터페이스에 대한 결합이 더 좋다는 사실을 기억하라.

몽키 패치(Monkey Patch)란 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것

 

안정성 & 유연성

인터페이스에 의존하면 설계가 유연해진다는 것

합성: 클래스의 증가, 중복코드의 문제를 간단하게 해결할 수 있다.

 

기본 구현을 제공하는 메서드를 훅 메서드(hook method)

 

객체지향 언어는 단일 상속만 지원하기 때문에, 중복 코드 문제를 해결하기가 쉽지 않다.

- 클래스 폭발(class explosion)

- 조합의 폭발(combinational explosion)

강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생하는 문제

유일한 해결 방법은 조합의 수만큼 새로운 클래스를 추가하는 것 뿐이다.

 

최선의 방법은 상속을 포기하는 것이다.

런타임 관계로 변경, 런타임에 객체의 관계를 변경

Context!!

 

설계는 변경과 유지보수를 위해 존재한다는 사실을 기억하라.

설계는 트레이드오프의 산물이다.

대부분의 경우에는 단순한 설계가 정답이지만 변경에 따르는 고통이 복잡성으로 인한 혼란을 넘어서고 있다면

유연성의 손을 들어주는 것이 현명한 판단일 확률이 높다.

 

객체를 조합하고 사용하는 방식이 상속을 사용한 방식보다 더 예측 가능하고 일관성이 있다는 사실을 알게 될 것이다.

코드의 진화를 방해한다.

건전한 결합도를 유지할 수 있는 더 좋은 방법은 합성을 이용하는 것이다.

 

### 믹스인(mixin)

재사용하면서도 낮은 결합도를 유지할 수 있는 유일한 방법은 재사용에 적합한 추상화를 도입하는 것이다.

객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법

(합성이 실행 시점에 객체를 조합하는 재사용 방법이라면)

믹스인은 컴파일 시점에 피룡한 코드 조각을 조합하는 재사용 방법이다.

스칼라(Scala) - 트레이트(trait)

 

선형화(linearization)

재사용 가능한 코드를 독립적으로 작성한 후 필요한 곳에서 쉽게 조합할 수 있게 해준다.

추상 서브클래스(abstract subclass)

쌓을 수 있는 변경(stackable modification)

탐색 과정을 이해하면 메시지와 다형성 사이의 관계를 이해하기가 좀 더 수월해질 것이다.

 

 

========================================================================

 

 

## 다형성(Polymorphism)

1. 유니버셜(Universal) 다형성

    1.1 매개변수(Parametric) 다형성

    1.2 포함(Inclusion) 다형성 == 서브타입(Subtype) 다형성

2. 임시(Ad Hoc) 다형성

    2.1 오버로딩(Overloading) 다형성

    2.2 강제(Coercion) 다형성

 

데이터 & 행동

상속이란 부모 클래스에서 정의한 데이터와 행동을 자식 클래스에서 자동적으로 공유할 수 있는 재사용 메커니즘

상속의 목적은 코드 재사용이 아니다.

- 업캐스팅

- 동적 메서드 탐색: 우선순위 & 가시성

- 동적 바인딩

- self 참조

- super 참조

 

메서드 오버라이딩: 상속받은 메서드와 동일한 시그니처의 메서드를 재정해서

메서드 오버로딩: 이름은 동일하지만 시그니처는 다른 메서드를 자식 클래스에 추가하는 것

 

실제 클래스를 기반으로 실행될 메서드

따라서 코드를 변경하지 않고도 실행되는 메서드를 변경

업캐스팅 -> 동적 메서드 탐색

다운캐스팅(downcasting)

 

정적 바인딩(static binding)

초기 바인딩(early binding)

컴파일타임 바인딩(compile-time binding)

동적 바인딩(dynamic binding)

지연 바인딩(late binding)

 

self 참조(self reference)

개발자들은 전통을 존중해서 self라는 이름을 사용한다.

자동적인 메시지 위임

동적인 문맥

부모 클래스에서 자동으로 위임된다.

 

super 참조(super reference)

위임(delegation)

다형성을 구현

 

자바스크립트의 모든 객체들은 prototype으로 링크된다.

prototype 체인

 

 

========================================================================

 

 

## 서브클래싱과 서브타이핑

타입 계층

일반화(generalization)

특수화(specialization)

코드 재사용

객체기반 프로그래밍(Object-Based Programming)

상속 & 다형성

프로토타입 기반 언어(Prototype-Based Language)

 

심볼(symbol)

내연(intension): 공통적인 속성이나 행동을 가리킨다.

외연(extension): 타입에 속하는 객체들의 집합

 

유효한 오퍼레이션의 집합을 정의

모든 객체지향 언어들은 객체의 타입에 따라 적용 가능한 연산자의 종류를 제한함으로써 프로그래머의 실수를 막아준다는 것이다.

약속된 문맥을 제공

 

공통의 특징을 공유하는 대상들의 분류다.

동일한 오퍼레이션을 적용할 수 있는 인스턴스들의 집합

퍼블릭 인터페이스

외부에 제공하는 행동에 초점

내부의 속성이 아니라 객체가 외부에 제공하는 행동이라는 사실을 기억하라.

 

행동 호환성

클라이언트의 관점

동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있다.

동일하게 행동하지 않을 것이라고 기대한다면 두 타입을 타입 계층으로 묶어서는 안된다.

 

새로운 타입을 추가할 때마다 코드 수정을 요구하기 때문에 개방-폐쇄 원칙을 위반한다.

계층을 분리하는 것뿐이다.

인터페이스 분리 원칙(Interface Segregation Principle, ISP)

두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안 된다.

 

서브클래싱(subclassing)

인스턴스를 대체할 수 없다.

구현 상속(implementation inheritance)

클래스 상속(class inheritance)

 

서브타이핑(subtyping)

인터페이스 상속(interface inheritance)

 

자식 클래스의 인스턴스를 사용할 목적으로 상속을 사용했다면 그것은 서브타이핑

구현을 재사용하기 위해 사용된 서브클래싱

(코드 공유의 방법)

 

행동 호환성(behavioral. substitution)

대체 가능성(substitutability)

 

리스코프 치환 원칙(Liskov Substitution Principle, LSP)

동작이 변하지 않으면

대체 가능

행동 호환성을 유지

 

계약에 의한 설계 (Design By Contract, DBC)

사전조건(precondition)

사후조건(postcondition)

클래스 불변식(class invariant)

 

부모 클래스가 클라이언트와 맺고 있는 계약에 관해 깊이 있게 고민하기를 바란다.

(도메인, 로직이해)

 

 

========================================================================

 

 

## 일관성 있는 협력

구현의 가장 큰 문제점은 이 클래스들이 유사한 문제를 해결하고 있음에도 불구하고 설계에 일관성이 없다는 것이다.

비일관성은 두 가지 상황에서 발목을 잡는다.

새로운 구현을 추가해야 하는 상황이고,

또 다른 하나는 기존의 구현을 이해해야 하는 상황이다.

코드를 추가하고 이해하는 일과 깊숙히 연관돼 있기 때문이다.

 

새로운 기본 정책을 추가하면 추가할수록 코드 사이의 일관성은 점점 더 어긋나게 되는 것

일관성 없는 코드가 가지는 두 번째 문제점은 코드를 이해하기 어렵다는 것

오히려 방해가 될 뿐

 

유사한 기능은 유사한 방식으로 구현

객체 사이의 협력은 만드는 것 뿐

두 클래스 사이의 강한 결합도는 설계 개선과 새로운 기능의 추가를 방해한다.

 

일관성 있는 설계를 만드는 데 가장 훌륭한 조언은 다양한 설계 경험을 익히라는 것이다.

어떤 변경이 중요한지

어떻게 다뤄야 하는지

설계 경험이 풍부하면 풍부할수록 어떤 위치에서 일관성을 보장

 

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

변하는 개념을 캡슐화하라.

 

협력에 참여하는 주체는 구체적인 객체

메시지를 수신한 객체의 책임

훌륭한 추상화를 찾아 추상화에 의존하도록 만드는 것이다.

 

시스템을 책임을 캡슐화한 섬

결합도를 제한

결합도가 느슨해질 수 있도록

추상적인 기반 타입

 

데이터 은닉(data hiding)

캡슐화하는 것

캡슐화란 변하는 어떤 것이든 감추는 것이다.

 

협력을 일관성 있게 만들면 상황이 달라진다.

변하지 않는 부분은 모든 기본 정책에서 공통적이라는 것을 기억하라

 

개념적 무결성(Conceptual Integrity)

관성과 동일한 뜻으로 간주해도 무방하다.

시스템이 일관성 있는 몇 개의 협력 패턴으로 구성된다면

시스템을 이해하고, 수정하고, 확장하는 데 필요한 노력과 시간을 아낄 수 있다.

 

협력을 설계하고 있다면 항상 기존의 협력 패턴을 따를 수는 없는지 고민하라.

그것이 시스템의 개념적 무결성을 지키는 최선의 방법일 것이다.

 

지속적으로 개선하라

새로운 요구사항이 추가되는 과정에서 인관성의 벽에 조금씩 금이 가는 경우를 자주 보게 된다.

협력은 고정된 것이 아니다.

과감하게 리팩터링하라.

설계에 맹목적으로 일관성을 맞추는 것이 아니라 달라지는 변경의 방향에 맞춰 지속적으로 코드를 개선하려는 의지다.

 

패턴을 찾아라

유사한 기능을 구현하기 위해 반복적으로 적용할 수 있는 협력의 구조를 찾아가는 기나긴 여정이다.

수정해 나가는 작업을 반복해 나가면서 다듬어진다.

객체, 책임, 책임은 계속 진화해 나가는 것이다.

새로 만들어내면 나눠줄 책임이 바뀌게 된다.

과정을 거치면서 객체들이 자주 통신하는 경로는 더욱 효과적이게 되고, 주어진 작업을 수행하는 표준 방안이 정착된다.

 

 

========================================================================

 

 

## 디자인패턴과 프레임워크: 협력을 일관성 있게 만들기 위한 방법

디자인패턴: 소프트웨어 설계에서 반복적으로 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결 방법

설계를 재사용하는 것,

재사용할 수 있는 설계의 묶음

 

프레임워크: 애플리케이션의 아키텍처를 구현 코드의 형태로 제공

요구에 따라 적절하게 커스터마이징할 수 있는 확장 포인트

 

패턴은 반복적으로 발생하는 문제와 해법의 쌍으로 정의

이미 알려진 문제

다른 사람과 의사소통할 수 있다.

추상적인 원칙

실제 코드 작성 사이의 간극을 메워주며 실질적인 코드 작성을 돕는다.

패턴의 요점은 패턴이 실무에서 탄생했다는 점

 

다양한 각도에서 패턴의 개념을 정립

실무 컨텍스트(practical context)에서 유용하게 사용해 왔고 다른 실무 컨텍스트에서도 유용할 것이라고 예상되는 아이디어

 

발견했다

널리 받아들여지는 경우에만 패턴으로 인정할 수 있기 때문에 이 말은 타당하다.

모든 아이디어가 패턴인 것은 아니다.

패턴은 개발자들이 다른 컨텍스트에서도 유용할 것이라고 생각하는 어떤 것이다.

 

다른 컨텍스트에서도 유용한 아이디어

3의 규칙(Rule of Three)

익히고 반복적으로 적용하는 과정 속에서 유연하고 품질 높은 소프트웨어를 개발하는 방법을 익힐 수 있게 된다.

 

STRATEGY 패턴을 적용하자는 단순한 대화로 바뀐다.

패턴은 홀로 존재하지 않는다.

패턴 언어(Pattern Language)

패턴 시스템(Pattern System)

 

아키텍처 패턴(Architecture Pattern)

분석 패턴(Analysis Pattern)

디자인 패턴(Design Pattern)

이디엄(Idiom)

협력하는 컴포넌트들 사이에서 반복적으로 발생하는 구조

 

아키텍처 패턴

서브시스템들을 제공

조직화하는 규칙과 가이드라인을 포함한다.

프로그래밍 패러다임에 독립적이다.

 

분석 패턴

도메인 내의 개념적인 문제를 해결하는 데 초점을 맞춘다.

도메인에 대해서만 적절할 수도 있고 여러 도메인에 걸쳐 적용할 수도 있다.

 

캡슐화, 크기, 의존성, 유연성, 성능, 확장 가능성, 재사용성 등의 다양한 요소들의 트레이드오프를 통해 결정된다.

훌륭한 품질의 설계를 얻기 위해 많은 시간과 노력을 들여야만 한다.

의존성이 존재해야 하는가?

 

STRATEGY 패턴은 다양한 알고리즘을 동적으로 교체할 수 있는 역할과 책임의 집합을 제공한다.

캡슐화 & 합성

 

BRIDGE 패턴은 추상화의 조합으로 인한 클래스의 폭발적인 증가 문제를 해결하기 위해

역할과 책임을 추상화와 구현의 두 개의 커다란 집합으로 분해함으로써 설계를 확장 가능하게 만든다.

 

COMPOSITE 패턴

합성을 통해 기본적인 역할과 책임, 협력 관계를 준수한다.

 

TEMPLATE METHOD 패턴

알고리즘을 캡슐화하기 위해 합성관계가 아닌, 상속관계를 사용하는 것

 

런타임에 객체의 알고리즘을 변경하는 것은 불가능하다.

복잡도를 낮출 수 있다는 면에서는 장점이라고 할 수 있다.

 

DECORATOR 패턴

행동을 동적으로 추가할 수 있게 해주는 패턴으로서 기본적으로 객체의 행동을 결합하기 위해 객체 합성을 이용한다.

 

특정한 변경을 캡슐화함으로써 유연하고 일관성 있는 협력을 설계할 수 있는 경험을 공유하는 것이다.

어떤 방법을 사용하는지를 이해하는 것이 더 중요하다.

 

패턴은 도구일 뿐이다!! (교과서)

패턴 만능주의

해결하려는 문제가 아니라 패턴이 제시하는 구조를 맹목적으로 따르는 것은 불필요하게 복잡하고,

난해하며, 유지보수하기 어려운 시스템을 낳는다.

엔트로피가 증가하는 부작용

트레이드오프 관계 속에서 패턴을 적용하고 사용해 본 경험이 필요하다.

 

어떤 패턴을 적용해야 하는지

어떤 패턴을 적용해서는 안되는지

명확한 트레이드오프 없이 패턴을 남용하면 설계가 불필요하게 복잡해지게 된다.

패턴을 알지 못하는 사람들은 불필요하게 복잡한 설계를 따라가느라 시간을 낭비하게 된다.

정단한 이유..!

 

패턴을 지향하거나 패턴을 목표로 리팩터링하는 것

모범적인 역할과 책임의 집합을 알고 있는 것은 큰 도움이 될 것이다.

 

설계 재사용보다 더 좋은 방법은 코드 재사용이다.

프레임워크(flow) 

추상 클래스나 인터페이스를 정의하고 인스턴스 사이의 상호작용을 통해

시스템 전체 혹은 일부를 구현해 놓은 재사용 가능한 설계,

애플리케이션 개발자가 현재의 요구사항에 맞게 커스터마이징할 수 있는 애플리케이션의 골격

설계의 재사용..!

 

추상 클래스와 인터페이스가 일관성 있는 협력을 만드는 핵심 재료라는 것을 기억하라.

협력을 일관성 있고 유연하게 만들기 위해서는 추상화를 이용해 변경을 캡슐화해야 한다.

 

상위 정책은 상대적으로 변경에 안정적이지만 세부사항은 자주 변경된다.

상위 정책이 자주 변하는 세부 사항에 의존한다면 변경에 대한 파급효과로 인해 상위 정책이 불안정해질 것이다.

 

가장 좋은 방법은 의존성 역전 원칙에 맞게 상위 정책과 세부 사항 모두 추상화에 의존하게 만드는 것이다.

세부 사항은 변경

변하는 것과 변하지 않는 것을 서로 분리해야 한다.

변하는 것과 변하지 않는 것들을 서로 다른 주기로 배포할 수 있도록 별도의 배포 단위로 분리해야 한다.

추상화에만 의존하도록 의존성의 방향을 조정하고 (상위 부모를 닮는다.)

 

제어 역전 원리

상위 정책을 재사용한다는 것

협력 관계를 재사용

항상 의존성 역전 원리라는 강력한 지원군이 존재한다.

설계 방법과 객체지향을 구분하는 가장 핵심적인 원리다.

프레임워크의 가장 기본적인 설계 메커니즘

의존성을 역전시키면 제어 흐름의 주체 역시 역전된다.

 

제어 역전(Inversion of Control) 원리

할리우드(Hollywood) 원리

서브타입만 개발하면 프레임워크에 정의된 플로우에 따라...

 

해결책만 제공하고, 특정한 동작은 비워둔다.

완성되지 않은 채로 남겨진 동작을 훅(hook)

우리의 코드는 수동적인 존재다.

(몸을 맡긴다. 제어의 역전 수동)

프레임워크는 자신을 찾지 말라고 이야기 한다.

 

애플리케이션 자체가 언제 어떤 라이브러리를 사용할 것인지를 스스로 제어한다.

따라서 언제 자신이 작성한 코드가 호출될 것인지를 스스로 제어할 수 없다.

제어 주체는 자신이 아닌 프레임워크로 넘긴 것이다.

하지만 결정해야 하는 설계 개념은 줄어들고 애플리케이션 별로 구체적인 오퍼레이션의 구현만 남게 된다.

 

제어의 역전이 프레임워크의 핵심 개념인 동시에 코드의 재사용을 가능하게 하는 힘이라는 사실을 이해해야 한다.

 

 

========================================================================

 

 

## 나아가기

 

### 따라하는 수준

모든 절차를 한번에 습득하는 것은 불가능하기 때문에 가장 쉽게 배울 수 있는 단 한 가지 절차를 학습하고 그대로 모방한다.

학습자에게 주어질 수 있는 최고의 보상은, 주어진 절차를 따르면, 만족할 만한 결과물을 얻을 수 있다는 안정감

 

### 분리 수준

다양한 절차를 학습하고 트레이드오프한다.

모든 절차를 배우게 되고, 어떤 절차가 어떤 상황에 적절하고, 어떤 상황에는 적절하지 않은지를 배운다.

각 상황에 따라 적절한 절차를 적용할 수 있는 판단력과 유연함을 익힌다.

 

### 거침없는 수준

많은 학습과 경험을 통해 즉시 적절한 해법을 직관적으로 떠올릴 수 있을뿐만 아니라,

때로는 자신의 방법에 따라 문제를 해결하기도 한다.

과거에 익혔던 모든 절차를 트레이드오프하는 수준을 벗어나 자신에게 가장 적합한 절차를 정하게 된다.

 

 

다양한 상황을 트레이드오프할 수 있는 능력을 길러야 한다.

이 책에서 설명한 기법과 원칙들을 이해하고 다양한 실무 프로젝트에서 적용해본 후 피드백을 통해 여러분만의 설계 기준을 잡기 바란다.

 

설계 방법이 다양할뿐만 아니라, 설계 절차를 정형화된 단계로 설명하는 것이 어렵기 때문이다.

고민하고, 스케치하고, 결과를 옮기는 논리적인 단계를 따르지 않는다.

실타래를 풀기 위해 다양한 방법을 시도한다.

다양한 시행착오와 실패가 누적된 결과물이다.

 

 

디자인 패턴(Design Pattern)

테스트-주도개발(Test-Driven Development): 반복일뿐...;;

 

리팩터링(Refactoring)

코드의 구조를 개선한다는 것을 곧 설계를 개선한다는 것을 의미한다.

리팩터링은 구현 전에 설계를 완성해야 한다는 절차에 대한 강방관념에서 우리를 해방시켜준다.

리팩터링에 익숙해지고 나면 일단 동작하는 코드를 완성한 후에 리팩터링을 통해 코드를 개선해가며

원하는 설계에 이를 수 있는 융통성을 얻게 된다.

 

어떤 기법도 홀로 존재하지 않는다. (모든 것은 조합이다.)

다양한 기법들을 혼합하라.

매순간마다 끊임없이 고민하고 트레이드오프하라.

캡슐화, 응집도, 결합도

역할, 책임, 협력

원칙이 적합하지 않은 지점을 만났을 때 이상이 현실 안에 안전하게 정착하도록 여러분의 설계를 약간 비트는 것뿐이다.

 

 

========================================================================

 

 

## 계약에 의한 설계(Design By Contract, DBC)

계약은 실행 가능하기 때문에 구현에 동기화돼 있는지 여부를 런타임에 검증할 수 있다.

이익(benefit)

의무(obligation)

문서화: 의무와 이익을 문서화

한쪽의 의무가 반대쪽의 권리가 된다는 것이다.

명확하게 정의하고 커뮤니케이션할 수 있는 범용적인 아이디어다.

 

"인터페이스에 대해 프로그래밍하라" (오퍼레이션 시그니처에 명시된 제약 조건)

- 가시성: public

- 반환 타입: String

- 메서드 이름: toString

 

의도를 드러내는 인터페이스

사전조건(precondition)

사후조건(postcondition)

불변식(invariant)

 

### 일찍 실패하기 (Fail Fast)

처리를 종료하는 것이 올바른 선택

되도록 빨리 종료해야 한다.

프로그램이 입히는 피해는 절름발이 프로그램이 끼치는 것보다 훨씬 덜한 법이다.

 

모든 인스턴스 변수의 가시성은 private으로 제한되어야 한다.

서브타입은 슈퍼타입이 발생시키는 예외와 다른 타입의 예외를 발생시켜서는 안 된다.

 

공변성(covariance)

반공변성(contravariance)

무공변성(invariance)

 

함수(anonymous function)

함수 리터럴(function literal)

람다 표현식(lambda expression)

 

슈퍼타입의 불변식을 유지하기 위해 항상 노력해야 한다.

또한 서브타입에서 슈펴타입에서 정의하지 않은 예외를 던져서는 안된다.

 

 

========================================================================

 

 

## 타입 계층의 구현

클래스는 타입을 구현하는 한 가지 방법일 뿐이다.

동일한 메시지에 대한 행동 호환성을 전제로 하기 때문에

타입 계층을 구현하는 방법

다형성을 구현하는 방법

동적 메서드 탐색과 유사한 방식을 이용해 적절한 메서드를 검색

 

타입 계층을 구현한다고 해서 서브타이핑 관계가 보장되는 것은 아니다.

대체할 수 있도록 리스코프 치환 원칙을 준수

행동 호환성을 보장하는 것은 전적으로 우리의 책임

책임은 우리 자신에게 있다는 사실을 기억하라.

 

사용자 정의 타입(user-defined data type)

타입의 구현 방법이 단 한 가지

가장 간단한 방법은 상속

 

구현뿐만 아니라 퍼블릭 인터페이스도 물려받을 수 있기 때문에 타입 계층을 쉽게 구현할 수 있다.

구체 클래스를 상속받는 것은 피해야 한다.

가급적 추상 클래스를 상속받거나 인터페이스를 구현하는 방법을 사용하기 바란다.

 

인터페이스

상속으로 인한 결합도 문제를 피하고 다중 상속이라는 구현 제약도 해결할 수 있는 방법

인터페이스와 클래스를 함께 조합하면 다중 상속의 딜레마에 빠지지 않을 수 있고 단일 상속 계층으로 인한 결합도 문제도 피할 수 있다.

 

추상클래스: 클래스 상속을 이용해 구현을 공유하면서도 결합도로 인한 부작용을 피하는 방법

 

단일 상속 계층만으로도 타입 계층을 구현하는 데 무리가 없다면 클래스나 추상 클래스를 이용해 타입을 정의하는 것이 더 좋다.

그 외의 상황이라면 인터페이스를 이용하는 것을 고려하라.

 

### 덕 테스트(duck test)

걷고, 헤엄치며, 꽥꽥소리를 낸다면 오리다.

행동이 오리와 같다면 그것을 오리라는 타입으로 취급해도 무방

동일 시그니처

 

컨텍스트 독립성(context independence)

인터페이스가 클래스보다 더 유연한 설계를 가능하게 해주는 이유는

클래스가 정의하는 구현이라는 컨텍스트에 독립적인 코드를 작성할 수 있게 해주기 때문이다.

 

덕 타이핑의 타입 안전성을 보장하는 대가로 거대한 크기의 프로그램이라는 비효율을 감수해야만 한다.

설계뿐만 아니라 프로그래밍 언어 역시 트레이드오프의 산물인 것이다. (중복구현...ㅎ)

 

### 믹스인(mixin)과 타입계층

행동을 중복 코드 없이 재사용할 수 있게 만드는 것

퍼블릭 인터페이스를 공유하게 되는 것

간결한 인터페이스(thin interface)를 풍부한 인터페이스(rich interface)로 만들 때는 트레이트를 사용할 수 있다.

디폴트 메서드(default method)

기본 구현을 가지고 있는 메서드를 구현할 필요가 없다.

디폴트 메서드를 사용해 추상 클래스를 대체할 경우 인터페이스가 불필요하게 비대해지고 캡슐화가 약화될 수 있다는 사실을 인지

코드 중복을 완벽하게 제거해 주지도 못한다.

 

코드재사용 vs 서브타이핑 (혼동하지 말기를...)

 

 

========================================================================

 

 

## 동적인 협력, 정적인 코드

협력을 구성하기 위해서는 살아 움직이는 객체가 필요하다.

객체는 태어나고, 협력하고, 책임을 다하고 나면 소멸한다.

지속적으로 변하고, 외부의 자극에 따라 다양한 방식으로 행동한다.

간단히 말해 객체는 동적이다.

살아 움직이는 존재인 것이다.

 

객체는 동적이다.

프로그램은 정적이다.

동적 모델(dynamic model): 객체와 협력

정적 모델(static model): 타입과 관계

 

조화롭게 버무릴 수 있는 능력이 필요하다.

 

응집도, 결합도, 변경가능성

설계 품질은 클래스와 클래스 사이의 관계를 얼마나 잘 설계하느냐에 좌우되는 것이 사실이지 않는가?

객체 사이의 협력에 기반해야 한다.

 

동적 모델을 기반으로 정적 모델을 구상할 때 고려해야 하는 중요한 요소는 변경이다.

변경을 수용할 수 있는 코드를 만들기 위해서다.

단순하고 결합도가 낮으며, 중복 코드가 없는 코드

 

중복 코드가 많을수록 하나의 개념을 변경하기 위해 여러 곳의 코드를 한꺼번에 수정해야 한다.

커그가 발생할 수 있는 확률이 높아지기 때문에 중복코드는 언제 터질지 모르는 시한폭탄과도 같다.

유연한 코드를 향해 나아가다 보면 서로 다른 컨텍스트 사이의 공통점을 하나의 코드로 모아야 하는 상황에 직면한다.

중복 코드를 제거하고 새로운 추상화를 도입하게 만든다.

 

### 변경을 고려하라

하지만 변경이라는 측면에서는 좋은 설계가 아니다.

다양한 정책을 조합하면 조합 할수록 중복 코드가 기하급수적으로 증가한다.

게다가 유연하지도 않다.

 

### 도메인(domain)

사용자가 프로그램을 사용하는 대상 영역을 가리킨다. 모델(model)

대상 영역에 대한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태다.

소프트웨어를 구축하라는 것이다.

도메인 모델을 작성하는 것이 목표가 아니라 출발점이라는 것이다.

 

모델은 옳거나 틀린 것이 아니다.

모델은 유용하거나 유용하지 않은 정도의 차이만 있을 뿐이다.

 

기능과 객체의 책임

협력을 지원하는 코드 구조를 만드는 것이다.

코드의 구조를 주도하는 것은 구조가 아니라 행동이라는 사실을 기억하라.

 

상속 대신 합성을 사용하라는 설계 지침을 따르는 또 다른 예다.

구현하거나 변경하기 더 쉬운 모델이 떠올랐다면 과감하게 초기 아이디어를 버려라.

 

분석 모델이 사용할 구현 기술을 지향하는 것은 매우 합리적이며,

그 결과 모델은 작업해야 할 문제를 이해하는 데 더 유용해진다는 것

 

모델링 툴에 저장된 다이어그램이 코드와 상관이 없다면 당장 다이어그램을 파기하고

모델링 툴을 쓰레기통에 집어 던진 후 프로젝트 관리자에게 라이선스 비용이 더 이상 필요하지 않다는 희소식을 전하자.

 

많은 시간을 소비하고 있다면 설계 모델을 도메인을 반영하도록 수정하고 분석 모델을 폐기처분하자.

이런 프로젝트는 소프트웨어 자체가 아니라 프로세스를 위한 맹목적인 작업에 개발자들의 소중한 시간과 체력을 낭비하고 있을 뿐이다.

 

도메인과 코드 간의 차이가 적어야 한다.

분석모델, 설계모델, 구현모델이 다르다는 생각을 버려라.

잘 버무려 코드에 반영

 

대응하지 않는다면 그 모델은 그다지 가치가 없으며 소프트웨어의 정확성도 의심스러워진다.

동시에 모델과 설계 기능 사이의 복잡한 대응은 이해하기 힘들고,

실제로 설계가 변경되면 유지보수가 불가능해진다.

 

새로운 모델을 찾아내야만 한다.

모델링과 설계 프로세스가 단 하나의 반복고리를 형성할 수 있다.

모든 단계에 걸쳐 행동과 변경에 초점을 맞추라는 것이다.!!

728x90
댓글