코드로 이해하는 객체지향 패러다임!
복잡함을 어떻게 효과적으로 관리할 수 있을까?
객체지향 프로그램을 설계하고 유지 보수하기 위한 책.
실제 코드를 통해 개념을 이해할 수 있어서 좋다.

1주일 한 챕터씩 책을 읽고 질문과 내용을 정리합니다.
<1장: 객체, 설계>ㅡ2022.10.15
- 객체 지향 패러다임이란 무엇인가?
- 스스로 상태를 관리하고, 판단하고, 행동하는 자율적인 객체들의 공동체를 구성하여 문제 해결하는 방식?
- 적절한 협력을 식별하고 협력에 필요한 역할을 수행할 수 있는 적절한 객체에게 적절한 책임을 할당하여 문제를 해결하는 방식?
- 언제 객체지향 프로그래밍이 절차 지향 프로그래밍보다 유리한가? 효율적인가?
- 절차 지향 프로그램은 프로세스와 데이터를 별도의 모듈에 위치시키고 프로세스가 모든 데이터에 의존하는 구조이기 때문에 변경하기 어렵다.
- 객체지향 프로그램은 변경의 전파를 적절히 통제할 수 있으며 데이터와 그 데이터를 사용하는 프로세스를 모두 동일한 모듈 안에 위치시킨다. 객체 내부의 변경은 캡슐화되어 있기 때문에 외부로 전파되지 않아 변경이 더 쉽다.
- 좋은 모듈이란?
- 제대로 동작하는 모듈에서 더 나아가 변경하기 쉽고 이해하기 쉬운 모듈을 만들어야 한다.
실제로 실무를 하면서 느낀 건데 변경하기 어렵고 이해하기 어려운 모듈은 확실히 시간을 많이 잡아먹는 것 같다. 좋은 모듈은 모두의 시간을 절약해 줄 수 있는 좋은 방법이지 않을까? 내가 만든 모듈은 과연 좋은 모듈일까? 책을 읽으면서 생각해 보니 고칠게 많아 보인다. 어떻게 더 좋게 고칠 수 있는지 이 책을 보면서 알 수 있지 않을까?
<2장: 객체지향 프로그래밍>ㅡ2022.10.22
- 객체 지향 패러다임으로 전환하기 위해 집중해야 하는 부분은?
- 클래스 이전에 어떤 객체가 필요하지 고민해 보자. 객체를 먼저 생각하고 그 후 클래스를 구현하자.
- 객체는 독립적인 존재가 아니다. 기능을 구현하기 위해 협력하는 공동체의 일원으로 생각해야 한다.
- 도메인의 구조를 따르는 프로그램 구조는 어떤 의미인가?
- 도메인 구조는 문제 해결을 위해 도메인을 구성하는 개념들을 추상화하여 설명한다.
- 객체라는 동일한 관점으로 요구사항과 프로그램을 바라본다면 도메인 개념이 프로그램의 객체와 클래스로 자연스럽게 연결될 수 있다.
- 도메인 구조를 따르는 프로그램의 구조를 만들었다면 언어의 적절한 기능을 이용해 구현하면 된다.
- 프로그램 구현 시 유의할 점?
- 클래스의 내부와 외부 경계를 분명히 정한다. => 객체의 자율성을 보장하고 구현의 자유로움을 제공함.
- 상태와 행동을 가진 자율적인 존재로 객체를 디자인해야 한다. => 캡슐화, 접근제어 활용
- 인터페이스와 구현의 분리
- 변경을 관리할 수 있는 구조인가?
- 이해하기 쉬운가?
- 미래에 발생할 문제를 해결하는데 유리한가?
- 인터페이스와 같은 추상적인 요소에 초점을 맞추자
- 객체 간 협력한다는 것은?
- 메시지와 메서드를 구분해야 한다.
- 상대 객체가 나의 메시지를 이해하고 응답할 거라는 기대를 가지고 믿고 메시지를 전송한다.
- 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것뿐이다.
- 객체는 전송받은 메시지를 스스로 처리하여 자신만의 방법으로 응답한다.
- 디형성은 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다. 동일한 인터페이스를 갖고 있고 그렇기에 동일한 메시지를 이해할 수 있다.
- 코드 재사용 시: 상속을 사용할까? 합성을 사용할까?
- 상속의 장단점
- 장점: 부모 클래스의 변수와 코드를 재사용할 수 있다. 클래스를 통해 강하게 결합
- 단점: 캡슐화를 위반, 설계가 유연하지 못하다. 실행 시점에 객체의 종류를 변경할 수 없음.
- 합성의 장단점
- 장점: 캡슐화 유지, 실행 시점에 객체의 종류를 변경할 수 있음, 유연한 설계, 인터페이스를 통해 약하게 결합
- 주의할 점 : 합성을 사용 시 객체나 메서드명, 인터페이스 설계가 명확하고 분명하게 잘 이루어지지 않으면 이해하기 어렵고 가독성이 떨어진다.
- 상속의 장단점
<3장: 역할, 책임, 협력>ㅡ2022.10.29
- 어떻게 협력하는 객체들의 공동체를 창조할 것인가?
- 객체지향 프로그래밍의 핵심은 역할, 책임, 협력
- 협력을 구성하기 위해 적절한 객체를 찾고 적절한 책임을 할당한다.
- 객체가 어떤 협력에 참여하고 있는지에 따라 객체의 행동(책임)을 결정하고, 객체의 행동에 따라 객체의 상태를 결정해야 한다.
- 그렇다면 어떻게 객체에게 적절한 책임이 할당되는가?
- 우선 처리해야 할 메시지(요청)를 식별한다.
- 요청을 처리할 책임을 수행하는데 필요한 정보를 가장 잘 아는 객체에게 메시지(요청)를 보낸다.
- 메시지를 수신한 객체는 응답하기 위해 필요한 행동을 하게 되는데, 이 행동이 객체가 수행할 책임으로 이어진다.
- 요청을 받은 객체는 요청에 응답하기 위해 필요한 행동(책임)을 수행한다. 이 과정에서 도움을 요청해서 처리해야 할 메시지를 식별하고 다시 요청을 처리하는데 필요한 정보를 가장 잘 아는 객체에게 책임을 할당한다.
- 이렇게 협력에 필요한 메시지를 찾고 메시지에 적절한 객체를 선택하는 반복적인 과정을 통해 이루어진다.
책임 할당 시 고려해야 할 2가지 요소
1. 메시지가 객체를 결정한다.
2. 행동이 상태를 결정한다. 협력이라는 문맥 아래에서 객체의 행동과 그 행동에 필요한 상태를 결정해야 한다.
- 배우라는 구체적인 객체에 책임을 할당하지 않고 배역이라는 역할 자체에 책임을 할당한다면?
- 역할은 다양한 종류의 객체를 수용할 수 있는 일종의 슬롯이자 구체적인 객체(배우)들의 타입을 캡슐화하는 추상화이다.
- 협력을 구체적인 객체(배우)가 아니라 추상적인 역할의 관점에서 설계하면 협력이 유연하고 재사용 가능해진다.
- 추상 클래스와 인터페이스를 통해 역할을 구현할 수 있다.
- 역할은 객체를 추상화해서 객체 자체가 아닌 협력에 초점을 맞출 수 있게 한다.
<4장: 설계 품질과 트레이드오프>ㅡ2022.11.05 (토)
설계는 변경을 위해 존재하고 변경에는 어떤 식으로든 비용이 발생한다. 훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것이다. 적절한 비용 안에서 쉽게 변경할 수 있는 설계는 응집도가 높고 서로 느슨하게 결합돼 있는 요소로 구성된다. (p.97)
설계 품질을 결정하는 요소는?
- 캡슐화
- 변경 가능성이 높은 부분을 내부로 숨기고 외부에는 상대적으로 안정적인 부분만 공개하여 변경의 전파를 통제하고 최소화할 수 있는가?
- 캡슐화는 유지보수성과 아주 밀접하게 관련되어 있다. 캡슐화에 실패하면 변경이 모든 시스템으로 전파될 수도 있다.
- 캡슐화의 정도가 응집도와 결합도를 결정한다.
- 응집도
- 모듈에 포함된 내부 요소들이 연관되어 있는 정도를 의미한다.
- 하나의 목적을 위해 내부 요소들이 긴밀하게 협력하고 있는가?
- 객체에 관련 있는 책임들이 할당되었나?
- 응집도가 높을수록 변경의 대상과 범위가 명확해지므로 코드를 변경하기 쉬워진다.
- 결합도
- 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지의 정도를 의미한다.
- 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 결합도가 낮다고 할 수 있다.
- 객체가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는가?
- 한 모듈이 변경되기 위해서 다른 모듈의 변경을 얼마나 요구하는가?
좋은 설계란?
- 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계가 좋은 설계다.
- 높은 응집도와 낮은 결합도를 추구해야 한다.
- 객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다.
| 데이터 중심 설계 (변경에 유연하지 못한 설계) | 책임 중심 설계 (변경에 유연한 설계) |
| 데이터 중심 | 책임 중심 |
| 상태를 중심으로 객체를 분할한다. | 책임을 중심으로 객체를 분할한다. |
| 객체는 독립된 데이터 덩어리일뿐이다. | 객체는 협력하는 공동체의 일원이다. |
| 변경에 취약하다. | 상대적으로 변경에 유리하다. |
| 객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의한다. | 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다. |
| 캡슐화 원칙을 위반한다. 상태변경은 인터페이스의 변경을 초래하고 인터페이스에 의존하는 모든 객체에게 영향이 전파된다. | 캡슐화 원칙을 준수한다. 객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는데 필요한 상태를 캡슐화하여 구현 변경에 대한 파장이 외부로 전파되는 것을 방지한다. |
| 높은 결합도 | 낮은 결합도 |
| 낮은 응집도 | 높은 응집도 |
< 5장: 책임 할당하기 >ㅡ2022.11.12 (토)
4장에서 살펴본 데이터 중심 설계는 행동보다 데이터를 먼저 결정하고 협력이라는 문맥을 벗어나 고립된 상태의 객체에 초점을 맞추기 때문에 캡슐화를 위반하기 쉽고, 결합도가 높아지며, 응집도가 낮아지며 코드를 변경하기 어려워진다.
그래서 우리는 책임에 초점을 맞추어야한다. 5장에서는 책임 할당을 위한 일반적인 패턴을 소개하고 있다.
책임 할당을 위한 일반적인 패턴
1. Information Pattern : 정보 전문가에게 책임을 할당하라.
2. Low Coupling Pattern : 낮은 결합도를 유지하도록 책임을 할당하라.
3. High Cohesion Pattern : 높은 응집도를 유지하도록 책임을 할당하라.
4. Creator Pattern 창조자에게 객체 생성 책임을 할당하라.
5. Polymorphism Pattern : 인터페이스를 활용해 역할이 책임을 수행하게 하라.
6. Protected Variations Pattern: 변경을 캡슐화하도록 책임을 할당하라.
변경에 대처하는 2가지 방법
1. 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계한다.
2. 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것이다.
변경이 반복적으로 발생하는 경우 복잡성이 조금 상승하더라도 코드를 더 유연하게 만드는 2번째 방법이 더 좋다고 한다. 번거로움과 실수를 줄일 수 있어 오류 발생을 줄일 수도 있다.
책임 할당은 실제로는 쉽지 않다. 도메인에 대한 이해가 필수적으로 요구되고, 책임 할당도 정답이 없기에 난해하고 어려울 수 있기 때문이다. 처음부터 객체지향 프로그램을 작성하지 않더라도 절차형 프로그램을 작성한 후에 객체지향적인 코드로 변경하는 리팩터링을 하면 기존 동작은 유지하되 내부 구조를 유연하게 변경할 수 있다.
간단한 리팩터링 원칙
1. 변경을 처리하기 위해 어떤 부분을 수정해야하는지 쉽게 파악하기 위해 우선 작고, 목적이 명확한 메서드로 분리한다.
2. 최대한 한 눈에 알아볼 수 있게 이해하기 구조를 만들어야 한다. 동시에 너무 많은 세부사항을 기억하도록 강요하는 코드는 좋지 않다.
3. 분해한 메서드를 각 메서드가 사용하는 데이터를 정의하는 클래스로 이동하여 내부 구현을 캡슐화할 수 있게 하고 불필요하게 내부 구현을 노출시키는 모든 setter, getter 메서드를 제거하라.
< 6장: 메시지와 인터페이스 >ㅡ2022.11.19 (토)
6장에서는 협력과 메시지의 개념을 다시 한번 살펴보고 유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는 데 도움이 되는 설계 원칙과 기법을 소개한다.
좋은 인터페이스란?
- 꼭 필요한 최소한의 오퍼레이션만을 인터페이스에 포함해야 한다.
- 어떻게가 아니라 무엇을 하는지를 추상적으로 표현하고 있어야 한다.
좋은 인터페이스를 만들기 위한 원칙은?
- 디미터 법칙(Law of Demeter)
- 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하여 결합도를 낮추는 원칙.
- 메시지 전송 객체는 메시지 수신 객체의 내부 구조에 관해 묻지 않고 원하는 것을 정확히 명시하여 단순히 수행하도록 요청한다.
- 묻지 말고 시켜라
- 객체의 외부에서 해당 객체의 상태를 기반으로 결정을 내리는 것은 객체의 캡슐화를 위반한다.
- 상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라.
- 의도를 드러내는 인터페이스
- 메서드의 이름으로 내부 구현 방법을 드러내지 않아야 한다.
- 무엇을 하려고 하는지 그 의도를 메서드명으로 표현하라.
- 객체가 협력안에서 수행해야 하는 책임에 관해 고민해 보고 메시지를 전송하는 목적을 먼저 생각해야 한다.
- 명령-쿼리 분리
- 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 한다.
- 오퍼레이션은 명령인 동시에 쿼리여서는 안 된다.
원칙의 함정을 조심하자: 손가락이 아닌 달을 보자!
- 소프트웨어 설계에 법칙은 존재하지 않는다.
- 모든 설계는 트레이드오프의 산물이므로 상황에 맞게 원칙을 적용해야 일관성 있는 설계와 질서 있는 코드를 작성할 수 있다.
- 원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지를 판단할 수 있는 능력을 기르는 것이다.
< 7장: 객체 분해 >ㅡ2022.11.26 (토)
프로그래밍 언어의 발전은 좀 더 효과적인 추상화를 이용해 복잡성을 극복하려는 개발자들의 노력에서 출발했다.
어떻게 추상화를 통해 복잡성을 극복하고 인지 과부하 문제를 해결할 것인가?
필요했기에 발명했다.
소프트웨어가 발전하고 좀 더 많은 요구사항을 처리하기 위해 점점 더 복잡해졌다.
그에 따라 개발자들은 새로운 기능을 추가하고 유지 보수하기 위해 복잡성을 관리해야 할 필요가 있었다.
기존에 사용하던 추상화 방법이 실무에서 한계를 느끼면서 더 나은 방법을 찾기 위해 계속 발전해 온 것 같다.
<추상화 및 분해 방법>
# 하향식 기능 분해
- 메인 함수 중심의 기능 분해
- 기본적으로 복잡한 문제를 해결 가능한 수준까지 기능 단위로 분해한다.
- 설계 시 시스템이 무엇을 해야 하는지가 아니라 어떻게 동작해야 하는지 집중하는 방식이다.
- 잦은 변경에 취약하고 파급효과 예측이 어렵다.
- 작은 프로그램과 개별 알고리즘 수준에서는 유용하다. 이미 해결된 알고리즘을 문서화하고 서술하는데 훌륭하다.
- 재사용하기 어렵다.
# 모듈
- 모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 한다.
- 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감추는 방식이다.
- 정보 은닉이라는 개념을 통해 데이터라는 존재를 설계의 중심 요소로 부각시켰다.
- 감춰야 할 데이터를 결정하고 이 데이터를 조작하는데 필요한 함수를 결정한다.
# 추상 데이터 타입
- 데이터를 추상화
- 추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정된다.
- 오퍼레이션을 기준으로 타입을 추상화한다.
- 오퍼레이션이 자주 추가되고 변경될 경우 유용한 방식.
- 전체 타입에 대한 구현 코드가 하나의 구현체 내에 포함되어 있기 때문에 새로운 오퍼레이션을 추가하는 작업이 상대적으로 간단하다.
- 클래스는 추상 데이터 타입인가? 다르다. 가장 핵심적인 차이로 클래스는 상속과 다형성을 지원한다.
# 객체지향
- 타입을 기준으로 절차를 추상화하는 방식.
- 타입이 자주 변경되고 추가될 경우 유용한 방식.
- 타입 변수를 이용한 조건문 대신 다형성을 활용한다. 조건문은 변경에 취약하다
- 클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라 객체가 메시지를 처리할 적절한 메서드를 선택하여 자율적으로 응답한다.
- 협력이라는 문맥을 고려해서 객체들이 협력하는 방식에 집중한다.
< 8장: 의존성 관리하기 >ㅡ2022.12. 3 (토)
객체지향적으로 프로그래밍을 하다 보면 협력은 필수적이다.
어떻게 협력을 위해 필요한 최소한의 의존성만을 유지하면서 변경을 어렵게 하는 의존성을 제거할 것인가?
8장을 읽으면서 의존성을 관리해야 하는 이유와 방법에 대해서 생각해 볼 수 있었다.
의존성이란?
- 두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미한다.
- 변경에 의한 영향의 전파 가능성을 암시한다.
- 의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가진다.
- 런타임 의존성과 컴파일타임 의존성을 서로 다르게 만들어야 유연하고 재사용 가능한 구조를 만들 수 있다.
- 런타임 의존성: 실행 시점의 의존성을 의미
- 컴파일타임 의존성: 코드 그 자체가 가지는 의존성을 의미. 코드를 작성하는 시점을 의미(구현 시점)
의존성을 관리해야 하는 이유?
- 의존성은 객체들의 협력을 가능하게 만드는 필수적인 요소다.
- 하지만 의존성이 과하면 문제가 될 수 있다.
- 바람직하지 않음 == 강한 결합도 == 다른 환경에서 재사용하기 위해서 내부 구현을 변경하게 만드는 의존성
- 바람직함 == 느슨한 결합도 == 다양한 환경에서 의존성을 재사용할 수 있다.
결합도를 느슨하게 유지하기 위한 방법은?
- 추상화에 의존하라.
- 클래스 내부에서 구체 클래스에 대한 의존성을 가급적이면 모두 제거하라.
- 생성자, setter, 메서드의 인자를 이용해 의존성을 해결
- 명시적으로 의존성을 퍼블릭 인터페이스로 표현하자.
- 의존성을 파악하기 위해 내부 구현을 살펴보는 고생을 하지 않아도 된다.
- 생성자 체이닝을 활용하라.
< 9장: 유연한 설계 >ㅡ2022.12. 10 (토)
다양한 컨텍스트에서 협력을 재사용할 필요가 항상 있는가?
그렇지 않다. 유연성과 복잡성은 트레이드오프 관계다.
유연성이 떨어지더라도 단순하고 명확한 설계는 이해하기 좋다.
복잡한 코드는 커뮤니케이션 비용을 추가로 부담해야 한다.
9장에서는 조금 복잡해지더라도 유연한 설계를 만들고자 할 때 생각해 볼 원칙에 대하여 소개한다.
- 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
- 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려있어야 하고, 수정에 대해서는 닫혀있어야 한다.
- 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있어야 한다.
- 그렇다면 어떻게 코드를 수정하지 않고 새로운 동작을 추가할 수 있을까?
- 컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하면 된다.
- OCP원칙의 핵심은 올바른 추상화에 의존해야 한다는 것이다.
- 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다.
- 올바른 추상화란 변경에 의한 파급효과를 최대한 피하기 위한 추상화다.
- 올바른 추상화를 얻기 위해서는 변하는 것과 변하지 않는 것을 고민해 보고 변하지 않는 것을 추상화의 목적으로 삼아야 한다.
- 객체 생성과 사용 분리 원칙
- 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다.
- 객체를 잘못된 위치에서 생성할 경우 결합도가 높아진다.
- 객체의 생성과 사용을 분리하기 위한 임의의 객체를 만들 수 있다.
- 어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 없으면 도메인과 무관한 인공적인 객체를 추가하고 이 객체에게 책임을 할당하자.
- 의존성 주입
- 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입이라고 부른다.
- 숨겨진 의존성이 안 좋은 이유는?
- 숨겨진 의존성은 코드를 읽기 어렵게 하고 내부 구현을 파악할 것을 강요한다.
- 단위 테스트 작성도 어렵다.
- 퍼블릭 인터페이스로 의존성에 대한 정보를 명확히 명시해야 한다.
- 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 모두 추상화에 의존해야 좋다.
- 추상화는 구체적인 사항에 의존해서는 안 된다.
- 협력에서 중요한 정책이나 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스이다.
- 하위 수준의 변경에 의해 상위 수준의 클래스가 영향을 받으면 안 된다.
- 상위 수준의 클래스가 하위 수준에 의존하면 재사용도 어렵다.
- 다양한 컨텍스트에서 재사용 가능할 수 있게 추상화에 의존하는 것이 좋다.
< 10장: 상속과 코드 재사용 >ㅡ2022.12. 17 (토)
상속은 코드를 재사용하는 강력한 방법이다.
하지만 잘못 사용할 경우 애플리케이션을 이해하고 확장하기 어렵게 만든다.
상속은 정말 꼭 필요한 곳에만 잘 사용해야 한다.
10장에서는 상속의 문제점과 잘 사용하는 방법을 살펴본다.
- 중복 코드가 위험한 이유는?
- 요구사항이 변경됐을 때 두 코드를 함께 수정해야 한다면 이 코드는 중복 코드다.
- 과연 중복 코드를 모두 찾아내서 수정했는지 확신할 수 있는가? => 확인에 많은 노력이 필요하다.
- 과연 그렇게 수정한 코드가 이전과 동일한 결과를 보장하는가? => 역시 확인에 많은 노력이 필요하다.
- 중복 코드는 수정과 테스트에 드는 비용을 증가시키고 변경을 방해한다.
- 기회가 생길 때마다 코드를 DRY 하게 정리해야 한다.
- DRY 원칙이란? (Don't Repeat Yourself) : 중복 코드 없이 명확하게 작성해야 한다.
- 그렇다면 상속으로 중복 코드를 효과적으로 다룰 수 있을까?
- 상속으로 중복 코드를 효과적으로 다루기 어렵다.
- 상속을 염두에 두고 설계되지 않은 클래스에는 적용하기가 어렵다.
- 상속은 결합도를 높이고 슈퍼클래스와 서브클래스가 강하게 결합되어 코드 수정을 어렵게 만든다.
- 상속은 서브 클래스가 슈퍼 클래스의 메서드뿐만 아니라 인스턴수 변수에 대해서도 결합되게 만든다.
- 상속 사용 시 취약한 기반 클래스 문제가 발생한다. 슈퍼 클래스의 변경에 의해 서브 클래스가 영향을 받는 현상으로 상속을 사용한다면 피할 수 없는 근본적인 취약점이다.
- 불필요한 인터페이스 상속 문제
- 메서드 오버라이딩의 오작용 문제
- 슈퍼클래스와 서브클래스의 동시 수정 문제
- 상속을 더 안전하게 사용하는 방법은?
- 추상화에 의존해야 한다. 슈퍼클래스와 서브클래스가 모두 추상화에 의존하면 결합도를 낮출 수 있다.
- 차이를 메서드로 추출하기: 중복 코드 안에서 변하는 부분을 찾아서 메서드로 추출하여 캡슐화한다.
- 중복 코드 위로 올리기: 중복 코드를 추상화된 슈퍼클래스로 올려서 추상화한다.
- 이렇게 중복 코드를 이동시킨 후 각 클래스는 서로 다른 변경의 이유를 가지게 되어 응집도가 높아진다.
- 추상화에 의존해야 한다. 슈퍼클래스와 서브클래스가 모두 추상화에 의존하면 결합도를 낮출 수 있다.
< 11장: 합성과 유연한 설계 >ㅡ2022.12. 24 (토)
11장에서는 코드 재사용 방법 중 유연한 설계에 도움이 되는 합성에 대해 살펴보았다.
1. 합성이 상속보다 유연한 설계에 유리한 이유는?
2. 왜 결합도를 낮게 유지해야 하는가?
합성이 상속보다 유연한 설계에 유리한 이유는?
- 유연한 설계라는 것은 변경의 영향을 덜 받는 설계라고도 할 수 있다.
- 변경의 영향을 덜 받기 위해서는 변경이 잦은 내부 구현에 의존하기보다 추상화된 인터페이스에 의존해야 한다.
- 합성은 상속과 다르게 구현에 의존하지 않는다.
- 합성은 퍼블릭 인터페이스에 의존하기 때문에 변경의 영향을 최소화할 수 있다. 내부 구현을 알지 못하기 때문에 내부 구현이 변경돼도 영향을 받지 않는다.
- 합성에서 두 객체 사이의 의존성은 런타임에 결정되기 때문에 동적으로 특정 객체에 책임을 할당할 수 있어서 유리하다.
왜 결합도를 낮게 유지해야 하는가?
- 일반적으로 결합도가 높을수록 수정하기 위해 살펴봐야 할 코드가 많아지고, 변경의 영향이 전파되어서 사소한 수정에도 이곳저곳 고치다 보면 시간이 오래 걸릴 수 있다.
- 상속은 코드를 간단하게 재사용할 수 있지만 슈퍼 클래스와 서브 클래스의 결합도가 높다.
- 책에서 나오는 간단한 요구사항의 조합 예제에서도 하나의 기능을 추가하기 위해서 중복코드도 많이 생기고 많은 작업이 필요했는데 실제로 더 복잡한 실무라면...? 시간도 오래 걸리고 관리하기 어려워 보인다.
- 기존에 상속 관계로 구현된 코드를 합성 관계로 변경하여 결합도를 낮추고 요구사항의 조합을 유연하게 다룰 수 있다.
- 합성은 조합을 구성하는 요소들을 개별 클래스로 구현한 후 실행 시점에 인스턴스를 조립하는 방법을 사용한다.
- 무작정 결합도를 낮게 유지하기 위해서 불필요하게 합성관계를 적용하려고 할 필요는 없을 것 같다.
< 12장: 다형성 >ㅡ2022.12. 31 (토)
상속은 다형성의 기반이 되는 타입 계층을 구조화하기 위해서 사용해야 한다.
12장에서 다형성과 다형성이 어떻게 구현되는지 살펴본다.
다형성이란?
- 하나의 추상 인터페이스에 대해 코드를 작성하고 이 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력이다.
- 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력을 의미한다.
어떻게 이렇게 여러 타입을 대상으로 코드가 동작할 수 있는가?
- 상속과 합성(위임)을 통해 타입 계층이 구조화되어있어야 한다.
- 업캐스팅과 동적 바인딩 메커니즘을 기반으로 동작한다.
- 업캐스팅: 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것이 가능하다.
- 동적 바인딩: 선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정된다. 이것은 객체지향 시스템이 메시지를 처리할 적절한 메서드를 컴파일 시점이 아니라 실행시점에 결정하기 때문에 가능하다.
- self 참조변수(this): 처음에 메시지를 수신한 객체를 가리킨다. self 참조가 가리키는 클래스에서 시작해서 상속 계층의 역방향으로 메서드를 탐색한다.
- super 참조변수(super): 부모클래스의 인스턴스 변수나 메서드에 접근하기 위해 사용할 수 있는 내부 변수. super 참조를 이용해서 메시지를 전송하면 부모클래스부터 메서드 탐색을 시작한다.
상속의 기본적인 동작 원리
- 객체에게는 각기 다른 상태를 저장할 수 있도록 각 인스턴스별로 독립적인 메모리를 할당한다.
- 메서드는 동일한 클래스의 인스턴스끼리 공유가 가능하기 때문에 클래스는 한 번만 메모리에 로드하고 각 인스턴스 별로 클래스를 가리키는 포인터를 갖게 한다.
- 이 포인터를 이용해 클래스 정보에 접근할 수 있고 접근한 클래스가 부모클래스를 가지고 있다면 부모 클래스를 가리키는 포인터를 이용해 부모클래스에도 접근이 가능하다.
- 메시지를 수신한 자식 클래스의 인스턴스는 클래스 포인터로 연결된 자신의 클래스에서 적절한 메서드가 존재하는지 찾고 없으면 부모클래스의 포인터를 따라 부모 클래스를 차례대로 아래에서 위로 훑어 나가면서 적절한 메서드가 존재하는지 검색한다.
우리는 이러한 상속의 동작 원리를 모방하여 부모 클래스 대신 메시지를 처리할 수 있는 다른 객체에게 처리를 요청하는 위임을 통해 합성관계로 다형성을 구현할 수도 있다.
동적 타입언어인 자바스크립트에서는 동적인 객체 사이의 위임을 통해 다형성을 구현한다.
< 13장: 서브클래싱과 서브타이핑 >ㅡ 2023.1. 7 (토)
서브클래스와 서브타입은 분명하게 구분해서 사용해야 한다.
상속을 올바르게 사용하기 위해서는 타입 계층을 구현하기 위한 목적으로만 사용하는 것이 좋다.
객체지향 패러다임 관점에서 타입이란?
- 객체의 타입은 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이라고 할 수 있다.
- 객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다고 볼 수 있다.
- 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류할 수 있다.
타입계층이란?
- 슈퍼타입: 더 일반적인 퍼블릭 인터페이스를 가지는 객체들의 타입을 의미한다.
- 서브타입: 더 특수한 퍼블릭 인터페이스를 가지는 객체들의 타입을 의미한다.
상속을 사용하기 전 생각해 볼 질문
- 클라이언트의 관점에서 슈퍼클래스와 서브 클래스가 is-a 관계를 만족하는가?
- 클라이언트의 관점에서 부모 클래스의 타입으로 자식클래스를 사용해도 괜찮은가?
- 클라이언트의 관점에서 부모클래스와 자식클래스의 타입이 동일하게 행동할 것이라고 기대할 수 있는가?
- 질문을 만족하지 않으면 상속을 사용하지 않는 게 좋다.
| 서브클래싱 | 서브타이핑 | |
| 목적 | 단순 코드 재사용 | 타입계층 구성 |
| 자식클래스와 부모 클래스간 행동 이 호환되는가? | 호환되지 않음 | 호환됨 |
| 부모클래스를 자식 클래스가 대체할 수 있는가? | 대체할 수 없음 | 대체할 수 있음 |
| 구현방법 | 상속, 합성 | |
< 14장: 일관성 있는 협력 >ㅡ 2023.1. 14 (토)
협력을 일관성 있게 조직하는 과정은 유사한 기능을 구현하기 위해 반복적으로 적용할 수 있는 협력의 구조를 찾아가는 긴 여정이다.
일관성 있는 협력이란? 왜 중요한가?
- 잘 설계된 애플리케이션은 이해하기 쉽고, 수정에 용이하며, 재사용 가능한 협력의 모임이다.
- 이렇게 잘 설계된 애플리케이션을 만들기 위해서는 유사한 기능을 구현시 일관성 있는 협력 패턴을 사용해서 객체들의 협력 방식을 일관성 있게 만들어야 한다.
- 이렇게 일관성 있게 만들어야 재사용이 쉽고 이해하기 쉽다.
- 일관성 있는 협력의 핵심은 변경을 분리하고 캡슐화하는 것이다.
어떻게 일관성 있게 만들 수 있을까?
- 책임을 일관된 방식으로 할당하면 객체끼리 일관된 방식으로 협력할 수 있다.
- 디자인 패턴을 참고해도 좋다. 디자인 패턴은 특정한 변경에 대해 일관성 있는 설계를 만들 수 있는 일종의 설계 템플릿이다.
- 변하는 개념을 변하지 않는 개념으로부터 분리해야 한다.
- 변하는 개념의 공통점을 추상화하고 캡슐화해야 한다.
- 캡슐화 기법은 데이터뿐만 아니라 변할 수 있는 모든 개념을 감추고, 코드 수정으로 인한 파급효과를 제어할 수 있는 기법이다.
- 캡슐화의 종류
- 데이터 캡슐화
- 메서드 캡슐화
- 객체 캡슐화(합성)
- 서브타입 캡슐화(인터페이스 상속)
< 15장: 디자인 패턴과 프레임워크 >ㅡ 2023.1. 28 (토)
디자인 패턴과 프레임워크는 모두 협력을 일관성있게 만들기 위한 방법이다.
- 디자인 패턴
- 소프트웨어 설계에서 반복적으로 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결방법이 디자인 패턴이다.
- 디자인 패턴은 특정한 변경을 일관성 있게 다룰 수 있는 협력 템플릿을 제공한다. 역할, 책임, 협력의 템플릿이다.
- 각 디자인 패턴은 특정한 변경을 캡슐화하기 위한 독자적인 방법을 정의하고 있다.
- 디자인 패턴의 목적은 특정한 변경을 캡슐화함으로써 유연하고 일관성 있는 협력을 설계할 수 있는 경험을 공유하는 것이다.
- 디자인 패턴에서 중요한 것은 구현방법이나 구조가 아니라 어떤 변경을 캡슐화하는 것인지 이해하는 것이다. 변경을 캡슐화하기 위해 어떤 방법을 사용하는지 이해해야 한다.
- 디자인 패턴은 프로그래밍 언어에 독립적으로 재사용 가능한 설계 아이디어를 제공하는 것을 목적으로 한다.
- 디자인 패턴을 적용하기 위해서는 설계 아이디어를 프로그래밍 언어의 특성에 맞춰 가공해야 하고 매번 구현 코드를 재작성해야 한다.
- 프레임워크
- 프레임 워크는 설계와 코드를 함께 재사용하기 위해 사용할 수 있다.
- 프레임워크는 특정한 변경을 일관성 있게 다룰 수 있는 확장 가능한 코드 템플릿을 제공한다.
- 프레임 워크는 애플리케이션의 아키텍처를 구현 코드의 형태로 제공하고, 제공되는 아키텍처가 요구사항에 적합하다면 다양한 환경에서 테스트를 거친 견고한 구현 코드를 쉽게 빠르게 재사용할 수 있다.
- 프레임워크는 애플리케이션을 확장할 수 있도록 부분적으로 구현된 추상 클래스와 인터페이스 집합뿐만 아니라 추가적인 작업 없이도 재사용 가능한 다양한 컴포넌트도 함께 제공한다.
- 전체적인 협력의 흐름은 프레임워크에 이미 정의되어 있고 개발자는 비어있는 훅만 구현하면 이미 정의된 일반적인 흐름에 원하는 특정한 동작을 구현하기만 하면 된다.
- 개발자가 작성한 코드가 호출될지 개발자 스스로 제어할 수 없다. 프레임워크가 제어한다. 그렇지만 개발자는 설계 코드를 재사용하게 되어 구체적인 로직의 개발에 집중할 수 있다.
- 어떻게 프레임워크로 재사용성을 향상할 수 있게 되었나?
- 프레임워크의 핵심은 추상화다.
- 추상 클래스와 인터페이스는 일관성 있는 협력을 만드는 핵심 재료다.
- 협력을 일관성 있고 유연하게 만들기 위해서는 추상화를 이용해 변경을 캡슐화하고, 의존성은 추상화를 향하도록 작성해야 한다.
< 계약에 의한 설계 >ㅡ 2023. 2. 4 (토)
명령 쿼리 분리 원칙
명령(프로시저)은 상태를 변경할 수 있지만 상태를 반환해서는 안된다.
쿼리(함수)는 객체의 상태를 반환할 수 있지만 상태를 변경해서는 안된다.
인터페이스만으로는 객체의 행동에 관한 다양한 관점을 전달하기 어렵다.
명령의 부수효과를 쉽고 명확하게 표현할 수 있을까?
계약에 의한 설계(Design By Contract, DBC)를 사용하면 협력에 필요한 다양한 제약과 부수효과를 명시적으로 정의하고 문서화할 수 있다.
- 프로그래밍 언어로 작성된 인터페이스는 객체가 수신할 수 있는 메시지는 정의할 수 있지만 객체 사이의 의사소통 방식은 명확하게 정의할 수 없다.
- 계약은 협력을 명확하게 정의하고 커뮤니케이션할 수 있는 범용적인 아이디어다.
- 협력에 참여하는 각 객체는 계약으로부터 이익을 기대하고 이익을 얻기 위해 의무를 이행한다.
- 협력에 참여하는 각 객체의 이익과 의무는 객체의 인터페이스 상에 문서화된다.
- 계약에 의한 설계를 이용하면 오퍼레이션의 시그니처를 구성하는 다양한 요소들을 이용해 협력에 참여하는 객체들이 지켜야 하는 제약 조건을 명시할 수 있다. 이 제약 조건을 인터페이스의 일부로 만들어서 인터페이스의 사용법을 쉽게 이해하게 할 수 있다. 사전 조건, 사후 조건, 불변식을 기술할 때는 실행 절차를 기술할 필요 없이 상태변경만을 명시하기 때문에 코드를 이해하고 분석하기 쉬워진다.
- 사전 조건
- 메서드가 정상적으로 호출되기 위해 만족돼야 하는 조건으로 메서드의 요구 사항을 명시한다. 사전조건이 만족되지 않을 경우 메서드가 실행되면 안 된다. 사전 조건을 만족시키는 것은 메서드를 실행하는 클라이언트의 의무다.
- 일반적으로 사전 조건은 메서드에 전달된 인자의 정합성을 체크하기 위해 사용된다.
- 사후 조건
- 메서드가 실행된 후에 클라이언트에게 보장해야 하는 조건이다. 클라이언트가 사전조건을 만족시켰다면 메서드는 사후 조건에 명시된 조건을 만족시켜야 한다. 만약 클라이언트가 사전조건을 만족시켰는데도 사후 조건을 만족시키지 못한 경우에는 클라이언트에게 예외를 던져야 한다. 사후 조건을 만족시키는 것은 서버의 의무다.
- 사후 조건은 메서드의 실행결과가 올바른지를 검사하고 실행 후에 객체가 유효한 상태로 남아 있는지를 검증한다.
- 불변식
- 항상 참이라고 보장되는 서버의 조건이다. 메서드가 실행되는 도중에는 불변식을 만족시키지 못할 수 있지만 메서드를 실행하기 전이나 종료된 후에 불변식은 항상 참이어야 한다.
- 일반적으로 객체의 내부 상태와 관련이 있으며 인스턴스 생명주기 전반에 걸쳐 지켜져야 하는 규칙을 명세한다.
- 불변식은 클래스의 모든 인스턴스가 생성된 후에 만족돼야 한다. 이것은 클래스에 정의된 모든 생성자는 불변식을 준수해야 한다는 것을 의미한다.
- 불변식은 모든 메서드의 사전 조건과 사후 조건에 추가되는 공통의 조건으로 생각할 수 있다.
- 사전 조건
계약에 의한 설계의 핵심은 클라이언트와 서버 사이의 견고한 협력을 위해 준수해야 하는 규약을 정의하는 것이다. 리스코프 치환 원칙은 슈퍼 타입의 인스턴스와 협력하는 클라이언트의 관점에서 서브타입의 인스턴스가 슈퍼타입을 대체하더라도 협력에 지장이 없어야 한다는 것을 의미한다. 즉 서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 계약을 준수해야 한다.
리스코프 치환 원칙
- 계약 규칙
- 계약 규칙은 협력에 참여하는 객체에 대한 기대를 표현하는 규칙이다. 슈퍼 타입과 서브 타입 사이의 사전조건, 사후조건, 불변식에 대해 서술할 수 있는 제약에 관한 규칙이다.
- 서브타입에 더 강력한 사전조건을 정의할 수 없다.
- 서브타입에 더 완화된 사후조건을 정의할 수 없다.
- 슈퍼타입의 불변식은 서브타입에서도 반드시 유지돼야 한다.
- 가변성 규칙
- 가변성 규칙은 교체 가능한 타입과 관련된 규칙이다. 파라미터와 리턴 타입의 변형과 관련된 규칙이다.
- 서브타입의 메서드 파라미터는 반공변성을 가져야 한다.
- 서브타입의 리턴 타입은 공변성을 가져야 한다.
- 서브타입은 슈퍼타입이 발생시키는 예외와 다른 타입의 예외를 발생시켜서는 안 된다.
< 타입 계층의 구현 >ㅡ 2023. 2. 11 (토)
- 타입은 개념의 분류를 의미하고 클래스는 타입을 구현하는 한 가지 방법일 뿐이다.
- 타입 계층은 동일한 메시지에 대한 행동 호환성을 전제로 한다.
- 타입 계층을 구현하더라도 리스코프 치환 원칙을 준수하지 않는다면 올바른 타입 계층을 구축한 게 아니다.
- 코드 재사용과 서브타이핑을 혼동하면 안 된다.
- 클래스와 타입의 차이
- 객체의 클래스는 객체의 구현을 정의한다.
- 클래스는 객체의 내부 상태와 오퍼레이션 구현 방법을 정의하는 것이고 객체의 타입은 인터페이스만을 정의하는 것으로 객체가 반응할 수 있는 오퍼레이션의 집합을 정의한다.
- 하나의 객체가 여러 타입을 가질 수 있고 서로 다른 클래스의 객체들이 동일한 타입을 가질 수 있다. 즉, 객체의 구현은 다를지라도 인터페이스는 같을 수 있다는 의미다.
- 클래스와 타입 간에는 밀접한 관련이 있다. 클래스도 객체가 만족할 수 있는 오퍼레이션을 정의하고 있으므로 타입을 정의하는 것이기도 하다. 그래서 객체가 클래스의 인터페이스라고 말할 때 객체는 클래스가 정의하고 있는 인터페이스를 지원한다는 뜻을 내포한다.
- 타입은 동일한 퍼블릭 인터페이스를 가진 객체의 범주다.
- 클래스는 타입에 속하는 객체들을 구현하기 위한 구현 메커니즘이다.
- 타입을 중심으로 객체들의 계층을 설계해야 한다.
- 자바 8에서 도입된 디폴트 메서드
- 인터페이스에 메서드의 기본 구현을 추가하는 것을 허용한다.
- 그러나 디폴트 메서드로 사용하기 위해서는 public 접근을 허용해야 한다. 외부에 노출할 필요가 없는 메서드를 불필요하게 추가하는 결과가 생긴다. 캡슐화를 약화시킨다.
- 사실 디폴트 메서드를 도입한 이유는 인터페이스로 추상 클래스의 역할을 대체하려는 것이 아니다. 기존에 사용되고 있는 인터페이스에 오퍼레이션을 추가해야만 하는 하위 호환성 문제를 해결하기 위해서 도입된 것이다.
< 동적인 협력, 정적인 코드 >ㅡ 2023. 2. 11 (토)
- 협력하는 객체는 동적이다. 프로그램은 정적이다.
- 프로그램은 고정된 텍스트라는 형식 안에 갇혀 있으면서도 객체의 모든 변화 가능성을 담아야 한다.
- 프로그램의 실행구조를 동적으로 표현하는 움직이는 모델과 코드의 구조를 담는 고정된 모델을 동시에 생각해야 한다.
- 객체지향 세계에서 동적 모델은 객체와 협력으로 구성된다. 객체는 다른 객체와 협력하면서 애플리케이션의 기능을 수행한다.
- 객체지향 세계에서 정적 모델은 타입과 관계로 구성된다. 타입은 객체를 분류하기 위한 틀로서 동일한 타입에 속하는 객체들이 수행할 수 있는 모든 행동들을 압축해서 표현한 것이다. 타입을 구현한 클래스로 구성된 모델을 의미한다.
- 정적 모델은 동적 모델의 토대 위에서 존재해야 한다. 정적 모델은 객체사이의 협력에 기반해야 한다는 의미이다. 가장 중요하게 고려해야 하는 요소는 변경이고 협력에 초점을 맞춰야 한다.
- 변경을 수용할 수 있는 코드란?
- 단순하다.
- 응집도가 높고 결합도가 낮다.
- 중복 코드가 없다.
동적 모델과 정적 모델
- 행동이 코드를 결정한다.
- 객체가 외부에 제공하는 행동은 타입 계층을 구성하는 방식에 영향을 미친다.
- 정적 모델을 설계하는 이유는 단지 행동과 변경을 적절하게 수용할 수 있는 코드 구조를 찾는 것이어야 한다.
- 동적 모델이 정적 모델을 결정해야한다.
- 변경을 고려하라.
- 동일한 행동을 제공하는 정적 모델이 있다면 항상 현재의 설계에서 요구되는 변경을 부드럽게 수용할 수 있는 설계를 선택해라.
도메인 모델과 구현
- 도메인 모델이란 사용자가 프로그램을 사용하는 대상 영역에 대한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태다.
- 도메인 모델을 기반으로 소프트웨어를 구축하면 개념과 소프트웨어 사이의 표현적 차이를 줄일 수 있기 때문에 이해하고 수정하기 쉬운 소프트웨어를 만들 수 있다.
- 유용한 도메인 모델은 소프트웨어를 만드는데 휼륭한 출발점을 제공한다.
- 도메인 모델을 참고하고 수정하면서 객체들의 협력을 지원하는 코드 구조를 만드는게 중요하다.
- 초기 도메인 모델은 시작일뿐 계속해서 행동과 변경을 고려해서 수정되어야만 한다.
- 도메인 모델을 봤을때 도메인의 개념뿐만 아니라 코드도 함께 이해될 수 있는 구조를 찾으려고 해보자.
- 그래서 도메인 모델과 코드와 동일한 형태를 가질 수 있다.
- 객체지향 패러다임의 가장 큰 힘은 도메인을 표현하는 방법과 프로그램 코드를 표현하는 방법이 동일하다는 것이다. 전체 개발 주기에 걸쳐 행동과 변경에 초점을 맞추어야 한다.
'Book' 카테고리의 다른 글
| 헤드 퍼스트 디자인 패턴 (0) | 2022.11.27 |
|---|
댓글