[Book] 객체지향의 사실과 오해

J.Noma·2021년 12월 26일
0

내용전반: 객체지향의 사실과 오해


⚙️ 객체지향적 설계 가이드라인

🔸 가장 먼저, 객체의 책임과 역할을 결정하라

객체지향 설계에 있어 핵심은 클래스를 어떻게 구현할까가 아니라 객체가 협력 안에서 어떤 책임과 역할을 수행할 것인지를 결정하는 것이다. (ex. 재판이라는 협력에서 중요한 것은 '왕'이라는 겉모습이 아니라 '판사'라는 역할이다)

설계순서
1. 협력 : 먼저 견고하고 깔끔한 협력을 설계한다. 이는 설계에 참여하는 '객체들이 주고 받을 요청과 응답의 흐름'을 결정한다는 것을 의미한다
2. 책임/행동 : 이렇게 결정된 요청과 응답의 흐름은 객체가 협력에 참여하기 위해 수행될 '책임'이 된다. 그리고 책임은 객체가 외부에 제공하게 될 '행동'이 된다
3. 상태/데이터 : 행동을 결정한 후에 그 행동을 수행하는데 필요한 데이터를 고민한다
4. 클래스 : 이렇게 객체가 협력에 참여하기 위해 필요한 데이터와 행동이 결정된 후에 클래스의 구현 방법을 결정한다

장점
-- 깔끔하게 캡슐화될 수 있다. (무엇을 공개해야 하는지에 대해 '책임'이라는 명확한 기준을 이미 파악했으므로)
-- 객체를 고립된 섬이 아닌 협력자로 만든다. 책임을 먼저 생각하면 자연스럽게 어플리케이션 문맥 내에서 협력에 적합한 객체를 만들게 된다
-- 자연스레 재사용성이 좋아진다. 어떤 객체가 다양한 협력에 참여할 수 있느냐는 특정 책임을 수행할 수 있느냐에 달렸다. 책임이 아닌 상태에 초점을 맞추면 동일한 책임을 요구하는 다양한 협력에 참여하기 어려워진다

🔸 역할은 책임으로부터 정의되어야 한다

역할은 '이 자리는 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있습니다'라고 말하는 것과 같다. 이게 가능하도록 하려면, 역할이란 것은 어떤 협력 안에서 객체가 수행해야 할 책임들로부터 정의되어야 같은 책임을 가진(=행동을 할 수 있는) 다른 객체로 대체할 수 있다.

어떤 역할을 대체하려면 그 역할을 위해 수행가능해야 할 책임,행동,메시지를 동일하게 이해하고 처리할 수 있어야 한다 (코드로 치면, 같은 메서드를 가져야 한다)

🔸 책임은 그 책임을 수행하는데 필요한 정보(상태)를 가진 객체에게 할당한다

어떤 책임을 누구에게 할당할지는 그 책임을 수행하는데 필요한 정보(상태)를 가진 객체에게 할당해야 한다. 관련된 상태와 행동을 함께 캡슐화하여 자율적인 객체를 만드는 중요한 기본 원칙이다


🔹 사용자의 도메인 모델을 기반으로 소프트웨어를 설계해야 한다

사용자의 도메인 모델이란, 사용자가 생각/기대하는 App의 구조를 말한다. 예로, 은행 업무를 대신해주는 App에 대해 사용자들은 실제 은행의 구조를 생각하며 접근한다 (대상이 은행처럼 현실 세계에 있는 것은 그것과 유사할 것이라 기대하고, 현실 세계에 없는 것은 자신에게 익숙한 다른 무언가와 유사할 것이라 기대한다)

장점
-- 사용자의 요구사항들은 이런 도메인을 기반으로 발생/변경되므로, 소프트웨어 구조에 이를 적극적으로 반영하면 미래의 요구사항 변경에 대비하는 설계가 자연스레 만들어질 가능성이 높기 때문에 개발을 쉽게 만든다
-- 도메인 모델은 대게 쉽게 바뀌지 않는 개념이므로 상대적으로 안정적인 구조이다. 예로, 은행에 대한 도메인 모델(예금,적금,이자 등)은 현실에서 은행 자체가 크게 바뀌지 않는 한 변경되지 않는다
-- 이런 설계는 코드의 구조가 도메인 모델의 구조를 반영하기 때문에, 개발자가 그 코드의 도메인을 이해하고 있다면 코드를 이해하기 훨씬 수월해진다

참고로, 객체지향은 이런 사용자 도메인 모델 기반 설계를 만족시킬 수 있는 거의 유일한 패러다임이다


🔸 캡슐화: 공용 인터페이스(직접적인 책임) 외에는 감출 것

꼭 필요한 공용 인터페이스에 해당하지 않는 구현은 최대한 감춰야 한다. 왜냐하면 소프트웨어는 항상 변경되고 수많은 객체들이 물고 물리며 돌아가는 객체지향 공동체에서 어떤 객체를 수정했을 때 다른 어떤 객체들이 영향을 받는지 판단하는 것은 거의 곡예에 가깝다. 객체의 많은 부분이 외부에 공개될수록 아무리 작은 부분을 수정하더라도 변경에 의한 파급효과가 객체 공동체에 파고들기 쉽다.

즉, 외부에 많은 것을 공개할수록 변경을 어렵게 만드므로, 결국 객체가 책임에 대해 구현을 자유롭게 할 '자율성'을 해치는 것이다. 캡슐화는 객체의 주어진 책임에 대한 구체적인 구현을 변경하기 쉽게 만들고, 협력을 단순하고 유연하게 만든다

🔸 객체는 다른 객체의 상태를 묻지 말아야 한다

이것도 캡슐화와 관련된 가이드이다. 객체가 다른 객체의 상태를 묻는다는 것은 메시지를 전송하기 이전에 해당 객체에 관해 너무 많이 고민하고 있었다는 증거다. 다른 객체에 대해선 너무 관심갖지 말고 자율성을 존중해주자

🔸 인터페이스는 단순해야 한다

인터페이스는 해당 객체의 책임으로부터 만들어진다. 만약 객체의 책임이 '이건 이렇게 하고 저건 저렇게 해야 한다'라는 식으로 구체적이라면 그 책임을 수행하기 어려워지므로 다른 객체가 대체하기 어려워진다. 즉, 객체의 책임은 협력의 의도를 명확하게 표현할 수 있는 선에서 추상적일수록 유연하고 단순한 설계를 할 수 있다. ('의존성'을 낮추는 길이라고도 할 수 있겠다)

책임/인터페이스는 구체적인 구현에 대한 객체의 자율성을 보장할 수 있을 정도로 충분히 추상적인 동시에 협력의 의도를 뚜렷하게 표현할 수 있을 정도로 충분히 구체적이어야 한다. 이에 대한 기준은 설계 중인 협력이 무엇이냐와 같은 문맥에 따라 다르다는 사실에 유의한다

책임/인터페이스를 단순화하는 방법 중 하나는 객체의 책임을 '어떻게(how)'가 아니라 '무엇(what)'을 해야 하는가로 정의하는 것이다


⚙️ 객체지향 용어정의

🔹 객체지향

객체지향적인 소프트웨어란, 적절한 객체에게 적절한 책임을 할당함으로써, 동일한 목적을 달성하기 위해 협력하는 객체들의 공동체를 구성하는 것

객체지향은 인간의 기본적인 인지 능력에 기반을 두고 있어 직관적이고 이해하기 쉽다. 동시에 유연하고 재사용 가능한 협력 관계를 구축을 목적으로 한다

🔹 메시지

객체가 다른 객체에게 주어진 책임을 수행하도록 요청하는 것을 '메시지 전송'이라고 한다. 메시지는 협력을 위해 한 객체가 다른 객체로 접근할 수 있는 유일한 방법이다. 결국 메시지는 '책임', '공용 인터페이스'와 같은 것을 가리키게 되는 듯하다

🔹 책임

객체지향의 세계에서는 어떤 객체가 어떤 요청(메시지)에 대해 대답해 줄 수 있거나, 적절한 행동을 할 의무가 있는 경우 해당 객체가 책임을 가진다고 말한다

책임에는 2가지 분류가 있다

  • 행위(doing) : (객체생성, 계산, 다른객체의 행동을 시작/제어)를 하는 것
  • 정보(knowing) : (개인적인 정보, 관련된 객체)를 아는 것

객체의 책임을 이야기할 때는, 일반적으로 외부에서 접근 가능한 공용 서비스의 관점에서 이야기하므로 책임은 객체의 '공용 인터페이스'를 구성한다

🔹 역할

역할은 어떤 협력 안에서 객체가 수행해야 할 책임의 집합을 말한다. 즉, 역할이 같다는 것은 관련된 모든 책임들을 동일하게 수행할 수 있다는 것으로 '이 자리는 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있습니다'라는 말하는 것과 같다

역할의 개념을 사용하면 유사한 협력을 추상화해서 인지 과부하를 줄일 수 있다. 또한 다양한 객체들이 역할만 지녔다면 협력에 참여할 수 있기 때문에 협력이 좀 더 유연해지며 다양한 객체들이 동일한 협력에 재사용성이 높아진다. 역할은 객체지향 설계의 '단순성', '유연성', '재사용성'을 뒷받침하는 핵심 개념이다

한 객체는 여러 협력에 참여할 수 있고, 여러 역할을 맡을 수 있다

🔹 메서드

객체가 수신하는 메시지를 처리하기 위해 내부적으로 선택할 수 있는 방법(구현)을 메서드라고 한다. 따라서 어떤 객체에게 메시지를 전송하면 메시지에 대응되는 특정 메서드가 실행된다

🔹 개념

개념은 객체들의 복잡성을 극복하기 위한 추상화 도구다. 공통적인 특성을 기준으로 객체를 '여러 그룹으로 묶어' 동시에 다뤄야 하는 가짓수를 줄임으로써 상황을 '단순화'할 수 있다. 이렇게 공통점을 기반으로 객체들을 묶기 위한 그릇을 개념이라고 한다. ('타입'과 동일)

🔹 타입

타입이란, 우리가 인식하고 있는 다양한 사물이나 객체에 적용할 수 있는 아이디어나 관념을 말한다. 타입을 사용하는 이유는 시간에 따라 동적으로 변하는 객체의 상태 변경이라는 복잡성을 극복하기 위함이다. 어떤 객체에 타입을 적용할 수 있을 때 그 객체를 타입의 인스턴스라고 한다

🔹 인터페이스

일반적으로 인터페이스란 두 사물이 서로 상호작용하는 방법/장치를 말한다. 아래는 일반적인 인터페이스의 특징/장점이며 이는 객체지향에서도 동일하게 적용된다

  1. 인터페이스는 사용법만 익히면 내부 구조/방식을 몰라도 쉽게 조작하거나 의사를 전달할 수 있다
  2. 내부 구조/방식을 아무리 바꿔도 인터페이스가 바뀌지 않는 한 사용자(메시지 송신자)에게는 영항을 미치지 않는다
  3. 동일한 인터페이스를 제공할 수만 있다면 대상(메시지 수신자)이 달라져도 사용자(메시지 송신자)가 문제없이 상호작용할 수 있다

🔸 추상화

추상화는 2가지 차원에서 이뤄진다. 첫 째로, 구체적인 사물 간의 공통점은 취하고 차이점은 버리는 일반화를 통해 단순화하는 것. 둘 째로, 중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거해 단순화하는 것. 아무튼 객체들을 단순화함으로써 복잡성을 극복하는 것

🔸 캡슐화

객체지향의 세계에서 캡슐화는 두 가지 관점에서 사용된다

1. 상태와 책임을 한 덩어리로
객체를 '상태와 행동을 묶어' 자신의 상태를 스스로 관리하도록 만든다는 관점을 데이터 캡슐화라고 한다. 과거의 전통적인 개발 방법은 데이터(상태)와 프로세스(행동)을 엄격하게 구분하지만 객체지향에서는 객체라는 하나의 틀 안으로 함께 묶어 놓음으로써 객체의 자율성을 보장한다.

2. 외부로부터의 방어
외부의 객체가 자신의 내부 상태를 직접 관찰하거나 제어할 수 없도록 막고 외부로 제공해야 할 필요가 있는 메시지(책임)만 공용 인터페이스에 포함시키는 것. 개인적인 비밀은 감춤으로써 외부의 불필요한 간섭으로부터 내부 상태를 격리하여 구현 변경에 대한 자율성을 보장받을 수 있다

🔸 일반화/특수화

일반화와 특수화는 동시에 일어난다. 두 타입 간에 일반화/특수화 관계가 성립한다는 것은 한 타입이 다른 타입보다 '더 특수하게 행동'해야 하고, 반대로 한 타입은 다른 타입보다 '더 일반적으로 행동'해야 한다. 상속 관계에서 superclass는 subclass보다 '일반적인 타입'이고, subclass는 superclass보다 '특수한 타입'이다

🔸 다형성

동일한 요청(메시지)에 대해 서로 다른 방식으로 응답할 수 있는 능력. 결과적으로 다형적인 객체들은 동일한 타입/타입계층에 속하게 된다

다형성은 역할,책임,협력과 깊은 관련이 있다. 서로 다른 객체들이 다형성을 만족시킨다는 것은 객체들이 동일한 책임을 공유한다는 것을 의미한다. 메시지 수신자들이 서로 다른 방식으로 처리하더라도 메시지 송신자 입장에서는 이 객체들은 동일한 책임을 수행하는 것이다

다형성을 사용하면 심지어 메시지 송신자가 수신자의 종류를 모르더라도 메시지를 전송할 수 있다. 단지, 수신자가 메시지에 대한 책임만 수행할 수 있으면 되는 것이다. 결국 '타입 - 타입'의 결합을 '메시지송신자 - 책임자'의 결합으로 낮춤으로써 필요한 책임을 수행할 수 있는 어떤 객체로든 대체 가능하게 된다. 이는 설계를 유연하고 재사용 가능하게 만든다.


🔹 도메인

소프트웨어를 사용하는 사람들은 자신이 관심을 가지고 있는 특정한 분야의 문제를 해결하기 위해 소프트웨어를 사용한다. 이처럼 사용자가 프로그램을 사용하는 대상 분야를 '도메인'이라고 한다

🔹 도메인 모델

사용자가 프로그램을 사용하는 대상 영역에 관한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태

🔹 합성관계

두 객체가 전체-부분 관계를 가질 때 합성관계라 한다

🔹 연관관계

한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않지만 서로 알고 있어야 하는 경우를 연관관계라고 한다

🔹 패키지 / 모듈

타입의 수가 많아질수록 타입 간 의존성을 관리하기 어려워지므로 구조에 관한 큰 그림을 안내해줄 지도 개념이 필요하다. 구조를 단순화하기 위해 서로 관련성이 높은 타입 집합을 논리적인 단위로 통합한 것을 패지키 혹은 모듈이라고 한다

패키지를 이용하면 시스템의 전반적인 구조를 이해하기 위해 한 번에 고려해야 하는 요소의 수를 줄일 수 있다. 또한, 패키지 내부에 포함된 타입들을 감춤으로써 "타입 그룹"단위의 캡슐화를 이룰 수도 있다


🔸 디자인 패턴

패턴은 특정한 상황에서 설계를 돕기 위해 모방하고 수정할 수 있는 과거의 설계 경험이다. 패턴은 해결하려고 하는 문제가 무엇인지를 명확하게 서술하고, 패턴을 적용할 수 있는 상황과 적용할 수 없는 상황을 함께 설명한다. 그리고 어떤 설계가 왜(why) 더 효과적인지에 대한 이유를 설명한다. 디자인 패턴은 유사한 상황에서 반복적으로 적용할 수 있는 책임-주도 설계의 결과물이라고 할 수 있다

만약 특정한 상황에 적용 가능한 디자인 패턴을 잘 알고 있다면 책임-주도 설계의 절차를 순차적으로 따르지 않고도 시스템 안에 구현할 객체들의 역할,책임,협력관계를 빠르고 손쉽게 포착할 수 있다

🔸 책임-주도 설계(RDD)

현재 가장 널리 알려진 객체지향 설계 방법으로, 말 그대로 객체의 책임을 중심으로 시스템을 구축하는 것.

시스템의 기능을 더 작은 규모의 책임으로 분할하고 각 책임은 책임을 수행할 적절한 객체에게 할당된다. 객체가 스스로 처리할 수 없는 정보나 기능이 필요한 경우 적절한 객체를 찾아 필요한 작업을 요청하게 된다. 이런 요청 행위를 통해 결과적으로 객체들 간의 협력 관계가 만들어진다

협력이라는 문맥 안에서 객체의 행동을 생각하도록 도움으로써 응집도 높고 재사용 가능한 객체를 만들 수 있게 한다

🔸 테스트-주도 개발(TDD)

테스트-주도 개발의 기본 흐름은 다음과 같다
1. 실패하는 테스트 작성 (테스트를 테스트하는 개념)
2. 테스트를 통과하는 가장 간단한 코드 작성
3. 리팩터링

테스트-주도 개발은 테스트를 작성하는 것이 아니라, 객체가 어떤 역할에 대한 메시지를 수신할 때 기대하는 결과를 반환하는지 그리고 기대하는 객체와 협력하는지를 코드로 작성하는 것이다

테스트-주도 개발은 객체지향에 대한 깊이 있는 지식을 요구한다. 테스트를 작성하기 위해 객체의 메서드를 호출하고 반환값을 검증하는 것은 해당 객체가 수행하는 책임에 관해 생각한 것이다

profile
노션으로 이사갑니다 https://tungsten-run-778.notion.site/Study-Archive-98e51c3793684d428070695d5722d1fe

0개의 댓글