레https://product.kyobobook.co.kr/detail/S000001033116

 

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | 알렉스 쉬 - 교보문고

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | “페이스북의 뉴스 피드나 메신저,유튜브, 구글 드라이브 같은 대규모 시스템은 어떻게 설계할까?” IT 경력자라도 느닷없이 대규모 시스템

product.kyobobook.co.kr

1장 사용자 수에 따른 규모 확장성 : https://inhyeok-blog.tistory.com/40
2장 개략적인규모추정 : https://inhyeok-blog.tistory.com/41
3장 시스템 설계 면접 공략법 : https://inhyeok-blog.tistory.com/42
4장 처리율제한장치의설계 : https://inhyeok-blog.tistory.com/43
5장 안정해시설계 : https://inhyeok-blog.tistory.com/44
6장 키-값저장소설계 : https://inhyeok-blog.tistory.com/45
7장 분산시스템을위한유일 ID 생성기설계 : https://inhyeok-blog.tistory.com/46
8장 URL 단축기설계 : https://inhyeok-blog.tistory.com/47
9장 웹크롤러설계 : https://inhyeok-blog.tistory.com/48
10장알림시스템설계 : https://inhyeok-blog.tistory.com/49
11장뉴스피드시스템설계 : https://inhyeok-blog.tistory.com/50
12장채팅시스템설계 : https://inhyeok-blog.tistory.com/51
13장검색어자동완성시스템 : https://inhyeok-blog.tistory.com/52
14장유튜브설계 : https://inhyeok-blog.tistory.com/53
15장구글드라이브설계 : https://inhyeok-blog.tistory.com/54

 

처리율 제한 장치는 클라이언트 또는 서비스가 보내는 트래픽의 처리율을 제어하는 장치를 의미한다. 처리율 제한 장치를 두면 DoS 공격을 막고, 비용을 절감하며, 서버 과부하를 막을 수 있다.

 

이 장에서 작가는 지원자와 면접관의 대화를 통해 문제 이해 및 설계 범위를 확정하는 과정을 보여준다. 우리도 실제 설계를 할 때 면접관이 있다는 생각으로 문제를 명확하게 정의하고 한정할 필요가 있지만, 이 과정은 이 글에서는 제외한다.

 

요구사항

- 설정된 처리율을 초과하는 요청은 정확하게 제한한다.

- 낮은 응답시간: 처리율 제한 장치는 HTTP 응답시간에 영향을 주지 않아야 한다.

- 가능한 적은 메모리 사용

- 분산형 처리율 제한:하나의 처리율 제한 장치를 여러 서버나 프로세스에서 공유할 수 있어야 한다.

- 예외 처리: 요청이 제한되었을 때는 그 사실을 사용자에게 분명하게 보여줘야 한다.

- 높은 결함 남내성: 제한 장치에 장애가 생기더라도 전체 시스템에 영향을 주어서는 안된다.

 

처리율 제한장치는 서버와 클라이언트 사이에서 요청을 제한해야한다. 이는 서버측에 둬야한다. 클라우드 마이크로 서비스의 경우 처리율 제한 장치는 보통 API 게트트웨이에 구현한다. API 게이트웨이는 다양한 기능을 제공하는 완전 위탁관리형 서비스이다. 하지만 일단  API는 처리율 제한을 지원하는 미들웨어라는 점에만 주목하자.

 

 

처리율 제한은 5가지의 알고리즘이 유명하다.

- 토큰 버킷

토큰 버킷은 일정한 시간마다 토큰이 채워지는 버킷(양동이)에서 요청이 발생할 때마다 토큰을 소비해서 요청을 처리하는 것이다. 만약 토큰이 부족하다면 요청은 버려진다. 일반적으로 API 엔드포인트마다 별도의 버킷을 둔다.

장점 : 이 방식은 구현이 쉽고, 메모리 사용이 효율적이며, 짧은 시간에 집중되는 트레픽을 처리할 수 있다.

단점 : 버킷 크기와 토큰 공급률을 조정하기 까다롭다. 

 

- 누출 버킷

요청을 큐에 넣고 고정 속도로 서버에서 읽어와서 처리하는 방식이다. 

장점 : 큐의 크기가 제한되어있어서 메모리 사용이 효율적이며, 고정처리율때문에 안정적 출력이 필요한 경우 적합하다.

단점 : 단시간에 트래픽이 몰리는 경우 큐에 요청이 쌓이고, 최신 요청이 버려질 수 있다.(큐 오버플로) 또한 버킷 크기와 처리율을 조정하기 어렵다.

 

- 고정 윈도 카운터

타임라인을 고정된 간격의 윈도로 나누고(예컨데 1초마다 새로운 윈도가 기다리고 있다), 각 윈도마다 카운터를 붙인다. 요청이 들어올 때마다 윈도 카운터를 증가시키고, 임계치가 넘으면 다음 윈도우가 열릴때까지 요청은 대기한다. 이 알고리즘은 예상한 요청보다 많은 요청을 받을 수 있는 위험이 존재한다. 예컨데 5초마다 임계치가 10인 윈도우가 있는데, 4초에 10개가 들어오고 6초에 10개가 들어오면 3초에서 8초 사이에 20개의 요청을 받게 되는 것이다. 

장점 : 메모리 효율이 좋다. 이해가 쉽다. 특정한 트래픽 패턴을 처리하기 좋다.(매 분마다 요청이 발생하는 경우 등)

단점 : 기대보다 많은 요청을 받을 수 있다(앞서 언급한 사례 참조)

 

- 이동 원도 로그

고정 윈도 카운터 방식의 단점을 보안한 방식으로, 요청은 들어올 때마다 log에 담긴다. 이때 윈도 크기에 해당하는 시간(예컨데 5초)보다 오래된 요청 log는 삭제한다. 오래된 요청을 삭제한 이후에 남아있는 요청의 크기가 임계값보다 작으면 요청을 처리한다.

장점 : 정확하게 처리율 한도를 제한한다.

단점: 다량의 메모리를 사용한다.

 

- 이동 윈도 카운터

이동 윈도 카운터는 고정 윈도 카운터와 이동 윈도 로그 방식을 결합한 방식이다. 고정 윈도와 동일하게 요청이 윈도우마다 카운터를 증가시키는데, 현재 시간을 끝으로 하는 윈도우가 걸치고 있는 2개의 윈도우(정확히 윈도우가 끝나는 지점에 있으면 1개)를 현재 윈도우가 몇 퍼센트씩 걸치고 있냐에 따라서 처리율을 계산한다. 예컨데 윈도우 크기가 1분이고, 현재 시간은 1분 20초라면 앞의 윈도우의 67%, 뒤의 윈도우의 33%를 걸치고 있다. 따라서 (직전 윈도에서 요청의 수) * 67% + (이번 윈도에서 쌓인 요청의 수) > (윈도 카운터 크기) 라면 요청을 버린다.

장점 : 짧은 시간에 몰리는 트래픽에도 잘 대응한다. 또한 메모리 효율도 좋다.

단점: 직전 시간대에 도착한 요청이 균동하게 분포되어 있다고 가정하므로 다소 느슨하다.(하지만 치명적이지 않음)

 

처리율 한도 초과 트래픽 처리

요청을 메시지 큐에 보관할 수도 있을 것이고, 사용자에게 429응답(too many requests)를 반환할 수도 있을 것이다.

 

 

분산 환경에서 처리율 제한 장치의 구현

Redis에서 카운터의 값을 읽고, counter+1의 값이 임계치를 넘는지 본 후에 넘지 않는다면 레디스에 보관된 카운터 값을 1만큼 증가시키는 방식은 경쟁 조건과 동기화 문제를 해결해야한다. 작가는 경쟁조건을 해결하는 2개의 블로그를 보여준다.

1. https://medium.com/@saisandeepmopuri/system-design-rate-limiter-and-data-modelling-9304b0d18250 (Redis)

 

System Design — Rate limiter and Data modelling

More often than not, the design interview rounds start with basic questions such as “design a rate limiter” or “design a circuit breaker”…

medium.com

2. https://stripe.com/blog/rate-limiters(Lua Script)

 

Scaling your API with rate limiters

Online payment processing for internet businesses. Stripe is a suite of payment APIs that powers commerce for businesses of all sizes.

stripe.com

동기화 문제는 중앙 집중형 데이터 저장소를 사용하는 것인데, 이는 처리율 제한장치의 토큰과 같은 상태를 유지해야하는 요소를 외부에 두는 것이다. 이는 앞선 1장에서 무상태 서버를 설명한 내용을 참조하길 바란다.

 

https://product.kyobobook.co.kr/detail/S000001033116

 

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | 알렉스 쉬 - 교보문고

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | “페이스북의 뉴스 피드나 메신저,유튜브, 구글 드라이브 같은 대규모 시스템은 어떻게 설계할까?” IT 경력자라도 느닷없이 대규모 시스템

product.kyobobook.co.kr

1장 사용자 수에 따른 규모 확장성 : https://inhyeok-blog.tistory.com/40
2장 개략적인규모추정 : https://inhyeok-blog.tistory.com/41
3장 시스템 설계 면접 공략법 : https://inhyeok-blog.tistory.com/42
4장 처리율제한장치의설계 : https://inhyeok-blog.tistory.com/43
5장 안정해시설계 : https://inhyeok-blog.tistory.com/44
6장 키-값저장소설계 : https://inhyeok-blog.tistory.com/45
7장 분산시스템을위한유일 ID 생성기설계 : https://inhyeok-blog.tistory.com/46
8장 URL 단축기설계 : https://inhyeok-blog.tistory.com/47
9장 웹크롤러설계 : https://inhyeok-blog.tistory.com/48
10장알림시스템설계 : https://inhyeok-blog.tistory.com/49
11장뉴스피드시스템설계 : https://inhyeok-blog.tistory.com/50
12장채팅시스템설계 : https://inhyeok-blog.tistory.com/51
13장검색어자동완성시스템 : https://inhyeok-blog.tistory.com/52
14장유튜브설계 : https://inhyeok-blog.tistory.com/53
15장구글드라이브설계 : https://inhyeok-blog.tistory.com/54

 

해당 장은 시스템 설계 면접에 접근하는 방법을 전하고 있다. 필자는 이 책을 시스템 설계 면접을 위해 읽은 것은 아니지만, 이 장을 통해 시스템 설계를 하면서 놓치면 안되는 부분을 다시한번 되짚을 수 있었다. 3장은 간단하게 언급하고 넘어가겠다.

 

효과적 면접을 위한 4단계 접근법

1. 문제 이해 및 설계 범위 확정

2. 개략적인 설계안 제시 및 동의 구하기

3. 상세 설계

4. 마무리(추가 논의)

 

작가는 4단계로 면접에 접근하라고 조언한다. 여기서 필자는 개략적인 설계안 제시 및 동의를 구하고 상세 설계를 마무리 한다는 지점이 정말 좋았다. 실제 설계를 하다보면 자신있는 부분에 대한 설계는 상세하게 하고, 잘 알지 못하는 부분에 대한 설계는 문제를 인지하지도 못한채 넘어가곤 한다. 그래서 커다란 시스템 설계를 단계별로 진행하면서 동의를 구하는 부분이 마음에 들었다. 아주 간단한 설계에서 시작해서 계속해서 다음 Step을 밟아가면서 동료에게 또는 선배/상사에게 조언을 받는다면 조금더 꼼꼼한 설계안을 만들 수 있지 않을까?

https://product.kyobobook.co.kr/detail/S000001033116

 

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | 알렉스 쉬 - 교보문고

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | “페이스북의 뉴스 피드나 메신저,유튜브, 구글 드라이브 같은 대규모 시스템은 어떻게 설계할까?” IT 경력자라도 느닷없이 대규모 시스템

product.kyobobook.co.kr

1장 사용자 수에 따른 규모 확장성 : https://inhyeok-blog.tistory.com/40
2장 개략적인규모추정 : https://inhyeok-blog.tistory.com/41
3장 시스템 설계 면접 공략법 : https://inhyeok-blog.tistory.com/42
4장 처리율제한장치의설계 : https://inhyeok-blog.tistory.com/43
5장 안정해시설계 : https://inhyeok-blog.tistory.com/44
6장 키-값저장소설계 : https://inhyeok-blog.tistory.com/45
7장 분산시스템을위한유일 ID 생성기설계 : https://inhyeok-blog.tistory.com/46
8장 URL 단축기설계 : https://inhyeok-blog.tistory.com/47
9장 웹크롤러설계 : https://inhyeok-blog.tistory.com/48
10장알림시스템설계 : https://inhyeok-blog.tistory.com/49
11장뉴스피드시스템설계 : https://inhyeok-blog.tistory.com/50
12장채팅시스템설계 : https://inhyeok-blog.tistory.com/51
13장검색어자동완성시스템 : https://inhyeok-blog.tistory.com/52
14장유튜브설계 : https://inhyeok-blog.tistory.com/53
15장구글드라이브설계 : https://inhyeok-blog.tistory.com/54

 

개략적인 규모추정은 보편적으로 통용되는 성능 수치상에서 사고 실험을 행하여 추정치를 계산하는 행위로, 어떤 설계가 요구사항에 부합할 것인지 보기 위한 것이다.

 

실제 시스템을 설계할 때 숫자를 확인하면서 설계를 하는 것은 아주 중요한 일이다. 예컨데, RabbitMQ을 사용해야 할 상황과 Kafka를 사용해야 할 상황을 구분하기 위해서는 초당 몇건의 요청이 들어오는지에 달렸다. 또한 유일ID 생성기를 설계할 때도, DB다중화 전략을 잡을 때도 모두 예상하는(또는 목표하는)요청의 숫자에서 시작해야 한다. 또한 규모 추정을 통해서 비용을 계산할 수도 있다. 비용 계산은 비즈니스의 일부로서의 개발을 바라봤을 때 개발보다 중요한 요소일수도 있다.  

 

세부적인 데이터에 관한 내용은  블로그에 작성할 내용은 아닌 것 같으므로, 책에 있는 예시만 언급하고 넘어가겠다.

 

다음과 같은 상황의 개략적인 규모를 추정해보자.

가정

- 월간 능동 사용자(MAU) = 3억

- 50%는 매일 트위터를 사용한다.

- 평균적으로 각 사용자는 매일 2건의 트윗을 올린다.

- 미디어를 포함하는 트윗은 10% 정도이다.

- 데이터는 5년간 보관된다.

 

추정 

QPS(Query Per Second) 추정치

- 일간 능동 사용자(DAU) = 3억 * 50% = 1.5억

- QPS=1.5억 * 2트윗 / 24시간 / 3600초 = 3500

- 최대 QPS(Peek QPS) = 2 * QPS = 약 7000

미디어 저장을 위한 저장소 요구량

- 평균 트윗 크기

   - tweet_id에 64바이트

   - 텍스트에 140바이트

   - 미디어에 1MB

- 미디어 저장소 요구량: 1.5억 * 2 * 10% * 1MB = 30TB/일

- 5년간 미디어를 보관하기 위한 저장소 요구량: 30TB * 365 * 5 = 55PB

https://product.kyobobook.co.kr/detail/S000001033116

 

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | 알렉스 쉬 - 교보문고

가상 면접 사례로 배우는 대규모 시스템 설계 기초 | “페이스북의 뉴스 피드나 메신저,유튜브, 구글 드라이브 같은 대규모 시스템은 어떻게 설계할까?” IT 경력자라도 느닷없이 대규모 시스템

product.kyobobook.co.kr

1장 사용자 수에 따른 규모 확장성 : https://inhyeok-blog.tistory.com/40
2장 개략적인규모추정 : https://inhyeok-blog.tistory.com/41
3장 시스템 설계 면접 공략법 : https://inhyeok-blog.tistory.com/42
4장 처리율제한장치의설계 : https://inhyeok-blog.tistory.com/43
5장 안정해시설계 : https://inhyeok-blog.tistory.com/44
6장 키-값저장소설계 : https://inhyeok-blog.tistory.com/45
7장 분산시스템을위한유일 ID 생성기설계 : https://inhyeok-blog.tistory.com/46
8장 URL 단축기설계 : https://inhyeok-blog.tistory.com/47
9장 웹크롤러설계 : https://inhyeok-blog.tistory.com/48
10장알림시스템설계 : https://inhyeok-blog.tistory.com/49
11장뉴스피드시스템설계 : https://inhyeok-blog.tistory.com/50
12장채팅시스템설계 : https://inhyeok-blog.tistory.com/51
13장검색어자동완성시스템 : https://inhyeok-blog.tistory.com/52
14장유튜브설계 : https://inhyeok-blog.tistory.com/53
15장구글드라이브설계 : https://inhyeok-blog.tistory.com/54

 

이 책은 대규모 시스템을 설계하기 위해서 해야할 접근방식과, 모범답안을 알려준다. 이 문제들과 답안을 통해서 대규모시스템의 문제를 도출해 낼 수 있는 힘과 이를 해결하기 위한 사고방식을 배울 수 있었다. 평소에 시스템 설계를 고민하면서 헤메이던 기억이 많은데, 가려운 곳을 시원하게 긁어주는 책이었다. 개인적으로 이 책은 가볍게 읽어도 좋을 것 같다. 각각의 컴포넌트가 아닌 시스템 전체를 설명하고 있어서 생각보다 어렵지 않고, 심지어 작가가 나열하는 문제를 어떻게 해결해 나가는지가 기대되기도 한다.

 

1장 사용자 수에 따른 규모 확장성

1장에서는 단일 서버에서부터 대규모 시스템을 설계하기 까지의 Step을 하나하나 알려준다. 각 Step이 어떤 문제에서 시작해서 어떤 형태로 변화하는지 아주 설득력 있게 다룬다

 

단일서버

단일서버는 웹서버 한대만으로 사용자 단말의 요청을 처리하는 구조다. 여기엔 DB도 없고, 캐시도 없으며, 정말 단일 웹서버만으로 요청을 처리한다. 아마도 데이터를 저장하는 위치는 해당 웹서버의 파일 시스템일 것으로 보인다.

 

데이터베이스

작가는 첫번째로 서버에서 데이터베이스를 분리한다. 단일 서버는 사용자가 증가하면 트레픽을 버틸만한 컴퓨팅 파워가 부족하기 때문이다. (자세한 언급은 없었지만, 당연하게도 단일버서는 조금만 트레픽이 증가해도 버틸 수 없다.) 데이터 베이스는 RDB와 NoSQL을 간단하게 비교해서 설명해준다. 그리고 각각의 DB를 선택해야하는 기준도 간단하게 언급한다.

 

수직적 규모 확장 vs 수평적 규모확장

수직적 규모 확장(이하 Scale up)은 사용하고 서버에 고사양 자원을 추가하는 방식을 의미하고, 수평적 규모 확장(이하 Scale out)은 서버를 추가해 주는 방식이다. 작가는 Scale up의 단점을 언급하면서 Scale out을 추천하고, 이를 위한 로드 벨런서까지 자연스럽게 이어지게 한다. 작가는 scale up의 단점을 확장에 한계가 있다는 점과, 자동복구나 다중화 방안을 제시하지 않는다는 것을 언급한다. 하지만 서버에 유입되는 트래픽의 양이 적을 경우에는 scale up도 좋은 선택이며, 단순한 솔루션이기 때문에 상황에 따라 선택할 수 있다고도 잠깐 언급한다.

 

로드밸런서

scale out을 하기 위해서는 하나의 엔포인트로 오는 사용자 단말의 요청을 여러개의 서버로 분산해 줘야한다. 그 역할을 하는 것이 로드 밸런서이다. 또한 로드 밸런서는 장애를 자동으로 복구할 수 있으며, 가용성(웹서버 하나가 죽더라도 다른 하나가 사용 가능한 것)이 향상된다. 로드 밸런러는 외부로 공개 IP를 Open해두고, 사설 IP 주소를 가지고 서버들에게 요청을 분산해 주는 방식을 택하고있다. 이제 웹 계층에 대해서 살펴봤으니 데이터 계층에 대해서 살펴본다.

 

데이터베이스 다중화

데이터베이스 다중화는 데이터베이스 서버를 여러개 만들어서 master 또는 slave의 역할을 수행하도록 한다. 이때 쓰기 연산은 master에서만 지원하고, slave는 master에서 그 사본을 전달받아서 읽기 전산을 지원한다. 일반적인 어플리케이션은 쓰기보다 읽기연산이 많으므로, slave 서버가 더 많다. 이렇게 데이터베이스 다중화를 수행하므로서 병렬 읽기로 인한 더 나은 성능과 서버가 분산되므로 인한 안정성(데이터 다중화로 인한 자연 재해로 인한 데이터 손실 최소화), 그리고 가용성(하나의 서버에 장애가 발생해도 다른 서버에서 서비스 가능)을 보장받는다. 

 

캐시

이제 웹 계층과 데이터 계층에 대해 논의했으니, 응답시간을 개선하기 위한 캐시에 대해서 말해보자. 캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고 뒤이은 요청이 보다 빨리 처리 되도록 하는 저장소이다. 캐시 계층은 메모리를 활용하므로 DB에 비해 조회 속도가 빠르고, DB에 요청을 적게 주므로서 부하를 줄일 수 있다. 또한 캐시를 확장시키는 것도 가능하다.

작가는 캐시를 사용하기전에 해야할 질문을 친절하게도 대신 하고, 답변해주고 있다. 캐시가 바람직한 상황, 캐싱할 데이터의 종류, 캐시 만료 전략, 일관성 유지방법, 장애 대응, 캐시 메모리 크기, 데이터 방출 정책에 대한 언급을 하고 있다.(해당 내용은 필요하다면 책을 확인해보자)

 

CDN

CDN은 정적 콘텐츠 전송을 위한 지리적으로 분산된 서버의 네트워크이다. CDN을 통해서 정적파일을 캐싱해두면 지리적 이점을 바탕으로 성능을 향상시킬 수 있다. 하지만 비용, 만료 시한 설정, 장애 대응, 콘텐츠 무효과 방법 등을 고려해서 CDN을 도입해야 한다. 

 

무상태 웹계층

웹 계층을 수평적으로 확장하려면 웹 계층은  무상태를 유지해야 한다. 이는 각각의 요청이 같은 서버로 들어올 것이라는 보장이 없기 때문에(이를 실현할 수 있지만 로드 밸런서에게 큰 부하를 준다) 사용자 단말의 상태를 유지해둬도 재조회가 불가능 하기 때문이다. 이를 위해서 상태는 NoSql과 같은 지속성 저장소에 보관하고, 필요할 때 가져와야 한다.

 

데이터 센터

데이터 센터를 다중화 하므로서 지리적 라우팅을 통한 성능 향상, 그리고 가용성 향상, 안정성 향상을 꾀할 수 있다. 하지만 데이터 센터 다중화는 몇가지 기술적 난제를 해결해야 한다. 이는 트레픽 우회(지리적 라우팅)과 데이터 동기화, 테스트와 배포이다. 

 

메시지큐

메시지 큐는 무손실을 보장하는 비동기 통신을 지원하는 컴포넌트이다. 메시지 큐를 이용하면 서비스 또는 서버 간 결합이 느슨해져서, 규모의 확장성이 보장되어야 하는 안정적인 어플리케이션을 구성하기 좋다. 예컨데 서버와 서버간의 통신의 결합을 느슨하게 해서 두 서버중 누군가 시간이 성능이 부족해져도 독립적으로 확장할 수 있는 것이다. 

 

로그, 메트릭 그리고 자동화

웹사이트와 함계 사업 규모가 커지고 나면 위의 도구는 필수적이다.

 

데이터베이스의 규모확장

DB는 scale up과 scale out이 모두 가능하다. 하지만 앞서 언급한바와 같이 scale up은 한계가 존재하며, SPOF(Single Point Of Failure)의 위한이 크다. 또한 비용도 많이 든다. 반면 수평적 확장은 DB 샤딩을 통해서 가능하다. 샤딩은 데이터베이스를 샤드(shard)라고 부르는 작은 단위로 분할하는 기술을 말한다. 이는 간단하게 (user_id%4)번 DB에 데이터를 저장하는 방식이 있다. DB 샤딩은 데이터가 너무 많아지면 재샤딩 하기도 하고, celebrity문제를 해결해야 하며(이후 안정해시를 이야기하며 힌트를 얻을 수 있다), 조인이 불가능해지기 때문에 비정규화를 통해 하나의 샤드에서 질의가 수행하도록 해야한다. 

https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr

책 목차:

1장 객체, 설계 : https://inhyeok-blog.tistory.com/32
2장 객체지향 프로그래밍 : https://inhyeok-blog.tistory.com/33
3장 역할, 책임, 협력 : https://inhyeok-blog.tistory.com/34
4장 설계 품질과 트레이드오프 : https://inhyeok-blog.tistory.com/36
5장 책임 할당하기 : https://inhyeok-blog.tistory.com/37
6장 메시지와 인터페이스 :
7장 객체 분해 :
8장 의존성 관리하기 :
9장 유연한 설계 :
10장 상속과 코드 재사용 :
11장 합성과 유연한 설계 :
12장 다형성 :
13장 서브클래싱과 서브타이핑 :
14장 일관성 있는 협력 :
15장 디자인 패턴과 프레임워크 :


앞서 살펴본 데이터 중심의 설계가 가지는 여러 문제를 해결할 수 있는 가장 좋은 방법은 책이 중심의 설계를 하는 것이다. 하지만 책임 중심의 설계는 어떤 객체에게 어떠 책임을 할당할지 결정하기 어렵다. 이를 해결하기 위해서 이번장은 GRASP 패턴을 확인해 본다. 그리고 책임 주도 설계의 과정을 한 걸음씩 따라가 보면서 객체에 책임을 할당하는 기본적인 원리를 살펴본다.

01. 책임 주도 설계를 향해

책임 주도 설계는 2가지 원칙을 따라야 한다.

  • 데이터보다 행동을 먼저 결정하라.
  • 협력이라는 문맥 안에서 책임을 결정하라.

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

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다. 객체는 협력에 참여하기 위해 존재하며, 협력 안에서 수행하는 책임이 객체의 존재가치를 증명한다.

데이터는 행동에 필요한 재료일 뿐이다. 따라서 우리는 객체의 데이터에서 행동으로 무게중심을 옮겨야 한다. 이 목표는 객체의 행동, 즉 책임을 먼저 결정한 후에 객체의 상태를 결정하므로써 달성할 수 있다.

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

객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다. 이때 협력에서 책임의 적합도는 메시지를 보내는 클라이언트의 의도에 적합한 책임을 말한다. 이를 위해서 메시지를 결정한 후에 객체를 선택해야 한다. 메시지를 결정한 후에 객체를 선택하게 된다면 메시지 송신자(클라이언트 객체)는 수신자에 대해 어떤 가정도 할 수 없다. 메시지 전송자의 관점에서 메시지 수신자가 캡슐화 되는 것이다. 

책임 주도 설계

이제 책임 주도 설계를 더 자세히 살펴 볼 것이다. 이를 위해서 책임 주도 설계의 흐름을 한번 더 확인해보자.

  1. 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  2. 시스템 책임을 더 작은 책임으로 분할한다.
  3.  분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  4. 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  5. 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 된다.

02. 책임 할당을 위한 GRASP 패턴

책임 할당 기법중에 가장 유명한 것은 GRASP패턴이다. 이를 통해서 책임 할당을 살펴보자.

도메인 개념에서 출발하기

설계 전에 도메인에대한 개략적인 모습을 그려보는 것은 유용하다. 예컨데 요구사항을 달성하는데 필요한 요소들을 간단하게 모델링 해 보는 것이다. 이때 중요한 것은 도메인 모델에 시간을 오래 쓰지 말아야 한다. 도메인 개념들의 의미와 관계는 설계를 하기전에 유용한 정보를 제공할 수 있다는 것으로 충분하다.

완벽한 도메인 모델은 존재하지 않는다. 설계를 고도화 할수록, 그리고 코드를 작성할 수록 더 나은 설계가 나오기 마련이다. 다시한번 강조한다. 도메인 개념은 설계를 시작하기전에 유용한 정보를 제공하는 수준에서 멈춰야한다. 더 이상의 노력을 들이지 말자

정보 전문가에게 책임을 할당하라

애플리케이션이 제공해야 하는 기능을 애플리케[이션의 책임으로 생각하자. 그리고 이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.

 

우린 앞서 책임 주도 설계에서 계속해서 책임을 나누고, 객체에 할당하는 과정을 반복해야 한다는 사실을 확인했다. 그럼 구체적으로 "책임을 나누는 방법""객체에 할당하는 방법"은 무엇일까?

  1. 메시지를 전송할 객체는 무엇을 원하는가?
    메시지(책임)은 메시지를 전송할 객체(클라이언트 객체)의 의도를 반영해서 결정해야 한다. 그래야 캡슐화를 성공적으로 구현할 수 있다.
    메시지를 클라이언트 입장에서 정의 해야한다는 사실은 최범균님의 HOW보단 WHAT을 드러내라라는 조언과 일맥상통한다.
  2. 메시지를 수신할 적합한 객체는 누구인가?
    객체는 자율적인 존재여야 한다. 따라서 책임을 수행할 정보(상태; 데이터)를 알고 있는 객체에게 책임을 할당해야 한다. 이를 GRASP에서는 INFORMATION EXPERT(정보 전문가) 패턴이라고 부른다. 이때 정보 전문가는 다른 객체에게 메시지를 보내서 얻을 수 있는 정보도 본인 소유의 정보라고 생각하는 것 이다. 그리고 객체가 메시지를 수행하기 위해서 메소드를 구현 할 때 필요에 따라서 메시지를 정의하고, 다른 정보 전문가를 찾는 것이다.

높은 응집도와 낮은 결합도

사실 동일한 기능을 구현하는 설계는 무수히 많다. 설계는 트레이드오프 활동이라는 것을 기억하라. 그럼 수많은 설계 중에 어떤 설계를 선택해야 할까?

높은 응집도와 낮은 결합도는 객체에 책임을 할당할 때 항상 고려해야 하는 기본 원리다. 이를 GRASP에서는 LOW COUPLING(낮은 결합도) 패턴, HIGH COHESION(높은 응집도) 패턴 이라고 부른다.

낮은 결합도를 달성하기 위해서는 이미 결합되어 있는 객체에게 메시지를 보내면 된다. 즉, 결합되는 객체의 수를 최소한으로 유지하라는 것이다. 

높은 응집도를 달성하기 위해서는 객체의 주된 책임을 확인해서 더 적합한 객체에게 메시지를 보내라. 이는 변경할 이유를 하나로 유지하는 방법에 대한 논의로, 이미 해당 객체가 변경될 이유에서 새로운 이유가 추가되지 않는다면 높은 응집도를 유지한 상태로 메시지를 처리할 수 있게 되는 것이다.

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

객체 A를 생성해야할 책임은 아래 조건을 최대한 많이 만족하는 B에게 할당해야 한다.

  • B가 A 객체를 포함하거나 참조한다.
  • B가 A 객체를 기록한다.
  • B가 A객체를 긴밀하게 사용한다.
  • B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다.(이 경우 B는 A에 대한 정보 전문가다.)

이러한 방식을 GRASP에서는 CREATOR(창조자) 패턴이라 칭한다. 이는 이미 결합돼 있는 객체에게 생성 책임을 할당하는 것으로 설계의 전체적인 결합도에 영향을 미치지 않도록 하는 것이다.

03. 구현을 통한 검증

DiscountCondition 개선하기

설계를 개선하는 작업은 변경의 이유가 하나 이상인 클래스를 찾는 것으로부터 시작하는 것이 좋다. 이는 클래스의 응집도가 낮은 것인데, 클래스의 응집도를 판단할 수 있는 세 가지 방법이 있다.

  • 클래스가 하나 이상의 이유로 변경돼야 한다면 응집도가 낮은 것이다. 변경의 이유를 기준으로 클래스를 분리하라
  • 클래스의 인스턴스를 초기화하는 시점에 경우에 따라 서로 다른 속성들을 초기화하고 있다면 응집도가 낮은 것이다. 초기화되는 속성의 그룹을 기준으로 클래스를 분리하라.
  • 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은 것이다. 이들 그룹을 기준으로 클래스를 분리하라.

타입 분리하기

DiscountCondition는 순번조건과 기간조건이라는 두개의 타입을 하나의 클래스에서 처리한다. 이는 앞서 말한 개선번으로, SequenceCondition과 PeriodCondition으로 분리하면 된다. 그리고 Movie가 기존에 DiscountCondition만 의존하고 있던 상황을 SequenceCondition과 PeriodCondition을 동시에 의존하도록 코드를 변경하면 된다. 그런데 문제가 생겼다. 응집도를 높히려다 보니까 덩달아 시스템의 전체적인 결합도가 높아져 버린 것이다. 이는 다음과 같은 문제를 발생시킨다.

  • Movie클래스가 PeriodCondition과 SequenceCondition 클래스 양쪽 모두에게 결합된다는 것
  • 새로운 할인 조건을 추가하기 어려워졌다. 새로운 할인조건은 Movie 클래스에게 새로운 의존성은 물론이고, 새로운 메서드까지 추가하게 만든다.

다형성을 통해 분리하기

사실 Movie입장에서 PeriodCondition과 SequenceCondition에게 보내는 메시지는 동일하다. 즉, 동일한 책임들을 수행하는 것이고, 같은 역할을 수행한다는 것이다. 협력안에서 역할은 대체 가능성을 의미하기 때문에, Movie는 구체적인 클래스는 알지 못한 채 오직 역할(Interface or Abstract)에 대해서만 결합되도록 의존성을 제한할 수 있는 것이다.

이 사례에서 알 수 있듯, 객체의 구체적인 타입에 따라 행동을 분기해야 한다면 명시적인 클래스를 정의해서 메시지를 수신하고 행동을 나눔으로써 응집도 문제를 해결할 수 있다. 이를 GRASP에서는 POLYMORPHISM(다형성) 패턴이라고 부른다.

프로그램을 if ~ else 또는 switch ~ case 등의 조건 논리를 사용해서 설계한다면 새로운 변화가 일어난 경우 조건 논리를 수정해야한다. 이럴 때 POLYMORPHISM 패턴은 객체의 타입을 검사해서 타입에 따라 여러 대안들을 수행하는 조건적인 논리를 사용하지 말라고 경고한다.

변경으로부터 보호하기

새로운 할인 조건을 추가하는 경우에는 어떻게 해야할까? 그냥 DiscountCondition을 상속받는 새로운 조건 객체를 만드는 것 만으로 충분하다. 어차피 Movie는 DiscountCondition이라는 인터페이스에 의존하기 때문에 전혀 문제가 없다.이처럼 변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서는 PREOTECTED VARIATIONS(변경 보호) 패턴이라고 부른다.

변화가 예상되는 지점을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당하라. 우리가 캡슐화해야 하는 것은 변경이다.

Movie 클래스 개선하기

Movie 클래스도 응집도가 낮다. 금액 할인 정책과 비율 할인 정책 두 가지 타입을 한 클래스에서 구현하고 있기 때문이다. 이를 개선하기 위해서 POLYMORPHSM 패턴을 적용하자. 할인 정책을 타입별로 분리하고, 메시지로만 협력하게 하는 것이다. 그리고 우리는 이를통해 PROTECTED VARIATIONS 패턴의 목표도 달성할 수 있다.

책에서는 Movie를 추상클래스로 만들고 AmountDiscountMovie, PercentDiscountMovie, NoneDiscountMovie 3가지 구체클래스로 상속받아 구현했다. 이 방식을 통해서 Movie의 공통되는 구현을 재사용하고 타입을 나눠서 POLYMORPHISM 패턴을 구현했다.
이 방식은 흔히 말하는 Template Method Pattern(템플릿 메소드 패턴)이다. 이 방식을 통해서 특정 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체적인 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내용을 바꿀 수 있다. 자주 사용하는 패턴이기도 하고, 다형성을 효과적으로 수행하는 방식이다(물론 코드 재사용을 위한 상속은 좋지 않다. 상속보다는 합성임을 기억하자). 단적인 예로 Spring Framework에서 AOP를 구현하기 위해서 Template Method Pattern을 변형해서 사용했다. 

위의 그림은 최종 결과물이다. 그런데 이 그림은 앞서 만든 도메인 모델과 유사하다. 우린 도메인 모델을 만들 때 할인 정책과 할인 조건이 변경될 수 있다는 직관을 포함했다. 그리고 이 직관은 우리의 설계를 유연하게 만들었다. 도메인의 역할은 구현을 가이드하는 것이다. 객체지향은 도메인의 개념과 구조를 반영한 코드를 가능하게 만들기 때문에 도메인의 구조가 코드의 구조를 이끌어 내도록 해야 하는 것이다.

변경과 유연성

설계를 주도하는 것은 변경이다. 변경을 대비할 수 있는 방법은 2가지가 있다.

  • 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계 하는 것
  • 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것

앞선 그림에서 Movie를 상속해서 할인 정책을 구현현했다. 그런데 실행중 할인 정책을 변경해야 한다면 문제가 생긴다. Movie를 상속받은 객체를 다시 생성해서 필요한 정보를 복사해야한다. 또한 새로 생성된 객체는 물리적으로 다를 객체이기 때문에 모든 식별자를 갱신해 줘야하는 어려움도 있다.

2장에서 코드를 재사용하기 위해서는 합성을 사용하라고 조언했다. 이처럼 DiscountPolicy 인터페이스를 만들어서 할인정책을 캡슐화해보자.

이제 Movie에 연결된 DiscountPolicy를 교체하는 것만으로 할인 정책을 변경할 수 있다. 객체를 복사할 필요도, 식별자를 갱신할 필요도 없다.

우리는 변경을 대비하기 위해 코드의 구조가 바뀌는 모습을 지켜봤다. 그럼 도메인에 대한 관점도 변경되어야 한다. 

도메인 모델은 구현과 밀접한 관계를 맺어야 한다. 도메인 모델은 코드에 대한 가이드를 제공할 수 있어야 하며 코드의 변화에 발맞춰 함께 변화해야 한다.

하지만 여전히 책임을 올바르게 할당하는 것은 어려운 일이다. 이를 위해 책임 주도 설계가 아닌 두번째 대안을 제시한다. 바로 절차형 코드로 실행되는 프로그램을 빠르게 작성한 후 완성된 코드를 객체지향적인 코드로 변경하는 것이다.(어쩌면 실무에서 더 많이 사용되고 있는 방법이다)

04. 책임 주도 설계의 대안

숙련된 설계자도 적절한 책임과 객체를 선택하기는 어렵다. 아무것도 없는 상태에서 책임과 협력에 관해 고민하기 보다는 빠르게 실행되는 코드를 얻고 리팩토링 하는 방법도 좋은 방법이다. 이때 주의할 점은 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌면 안된다는 것이다. 동작을 유지한 채 캡슐화를 향상시키고, 응집도를 높이고, 결합도를 낮춰야한다.

이 방식을 확인하기 위해서 4장에서 만든 데이터 중심 설계를 리팩토링 해보자.

메서드 응집도

데이터 중심의 설계에서는 객체는 단지 데이터의 집합이며, 모든 절차는 ReservationAgency에 집중되어있다.

처음으로 할일은 응집도 높은 메소드로 ReservationAgency를 나누는 것이다. 긴 메서드는 다양한 단점을 가진다.

  • 코드를 이해하는 데 시간이 오래 걸린다.
  • 변경이 발생하면 수정할 부분을 찾기 어렵다.
  • 일부 로직을 수정하면 나머지에서 버그가 발생한다.
  • 로직의 일부를 재사용할 수 없다.
  • 재사용을 위해서 코드를 복붙한다.(코드의 중복이 발생한다.)

이런 긴 메소드를 몬스터 메서드(monster method)라고 부른다.

메서드의 응집도를 높힌다는 것은 메서드가 단 하나의 이유로 변경된다는 것이다. 메서드 응집도를 높히면 각 메서드를 적절한 클래스로 이동시키기 쉬워진다.

 평소에 내가 줄기차게 주장하던 하나의 메서드는 같은 수준의 지시문(private method 등)을 나열해야한다는 내용과 일맥상통한다. 이를 통해 public 메서드는 상위 수준의 명세를 읽는 것 같은 느낌이 들 수 있도록 하는 것이다.

메서드의 응집도를 높히므로서 이제 ReservationAgency 클래스는 오직 하나의 작업만 수행하고, 하나의 변경이유만 가지는 작고, 명확하고, 응집도가 높은 메서드들로 구성돼 있다. 이를 통해 앞서 언급한 긴 메스드의 단점을 모두 해결할 수 있었다. 그리고 이제 메서드를 적절한 위치로 이동시키자. 적절한 위치는 바로 메서드가 사용하는 데이터를 정의하고 있는 클래스이다.

객체를 자율적으로 만들자

자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는 것으로 자율적인 객체를 만들 수 있다. 이를 위해 메서드 안에서 어떤 클래스의 접근자 메서드를 사용하는지 파악하고, 그 객체로 메서드를 옮겨보자. 이를 통해서 캡슐화와 높은 응집도, 낮은 결합도를 가지는 설계를 얻게된다. 여기에 POLYMORPHISM 패턴과 PROTECTED VARIATIONS 패턴을 차례대로 적용하면 우리가 원하는 설계를 얻을 수 있다. 

https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr

책 목차:

1장 객체, 설계 : https://inhyeok-blog.tistory.com/32
2장 객체지향 프로그래밍 : https://inhyeok-blog.tistory.com/33
3장 역할, 책임, 협력 : https://inhyeok-blog.tistory.com/34
4장 설계 품질과 트레이드오프 : https://inhyeok-blog.tistory.com/36
5장 책임 할당하기 : https://inhyeok-blog.tistory.com/37
6장 메시지와 인터페이스 :
7장 객체 분해 :
8장 의존성 관리하기 :
9장 유연한 설계 :
10장 상속과 코드 재사용 :
11장 합성과 유연한 설계 :
12장 다형성 :
13장 서브클래싱과 서브타이핑 :
14장 일관성 있는 협력 :
15장 디자인 패턴과 프레임워크 :


 

객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다. 이는 객체지향 설계의 핵심이 책임이라는 것과 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 연관이 깊다는 것을 의미한다. 이번 장에서는 데이터 중심의 설계를 살펴보고 객체지향적으로 설계한 구조와 어떤 차이점이 있는지 살펴보자.

01. 데이터 중심의 영화 예매 시스템

앞으로 객체의 상태는 데이터와 동일한 용어로 사용하겠다.

객체지향 설계는 크게 2가지 방법으로 시스템을 객체로 분할할 수 있다.

  • 상태를 분할의 중심축으로 삼는 방법 
     상태는 구현에 속한다. 구현은 변경되기 쉽고, 상태를 중심으로 삼으면 캡슐화의 원칙이 무너진다.
    -> 변경에 취약해질 수 밖에 없다
  • 책임을 분할의 중심축으로 삼는 방법
    상태는 인터페이스에 속한다. 변경에 안정적이고, 캡슐화를 성공적으로 수행한다.

이번에는 데이터 중심의 분할 방법을 먼저 살펴보자

데이터를 준비하자

앞선 영화 예메 프로그램을 데이터 중심으로 설계해봤다. 각 객체가 가져야하는 데이터를 먼저 정의하고, Getter와 Setter만 정의했다.

영화를 예매하자

영화를 예매하는 로직은 ReservationAgency라는 객체가 담당하도록 했고, 여기에 reserve라는 메소드가 앞서 구현한 데이터를 가져와서 영화를 예매하는 로직을 수행하도록 했다.

02. 설계 트레이드오프

여기서는 데이터 중심 설계와 책임 중심 설계의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도를 사용한다. 각각에 대해서 알아보자.

캡슐화

상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로 부터 감추기 위해서다. 이를 통해서 변경에 의한 파급효과를 적절하게 조절할 수 있다. 여기서 변경가능성이 높은 부분을 구현이라 부르고, 상대적으로 안정적인 부분을 인터페이스라 부른다.

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

응집도와 결합도

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

변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도를 통해 알 수 있다. 하나의 변경에 의한 내부 변경이 같은 모듈에 모여있으면 응집도가 높은 것이고, 하나의 변경에 의해 다양한 모듈 내부에 변화가 있다면 응집도가 낮은 것이다.

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

결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정된다. 예컨데 내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우에는 두 모듈 사이의 결합도가 높다고 표현할 수 있고, 퍼블릭 인터페이스를 수정했을 때만 다른 모듈에 영향을 미치는 경우에는 결합도가 낮다고 표현할 수 있다.

 

03. 데이터 중심의 영화 예매 시스템의 문제점

앞서 데이터 중심의 설계로 만든 영화 예매 시스템은 객체 내부 구현을 인터페이스의 일부로 만든다. 캡슐화의 정도가 객체의 응집도와 결합도를 결정한다는 사실을 기억하라.

앞선 설계는 크게 3가지의 문제를 가진다.

  • 캡슐화 위반
  • 높은 결합도
  • 낮은 응집도

캡슐화 위반

언듯 보기에 코드가 캡슐화를 지킨 것 같지만, Getter와 Setter를 사용하므로서 내부 구현을 인터페이스로 드러냈다.(이는 인스턴스 변수의 가시성을 public으로 만드는 것과 같은 것이다.) 이처럼 설계할 때 협력에 관해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지게 되는 경향이 있다.

앨린 홀립은 접근자와 수정자에 과도하게 의존하는 설계 방식을 추측에 의한 설계 전략이라고 부른다. 이는 결과적으로 변경에 취약한 설계를 만든다.

높은 결합도

앞서 접근자와 수정자를 때문에 캡슐화를 위반한 결과는 높은 결합도를 가져왔는데, 클라이언트 객체가 서버 객체의 구현(데이터)를 알고 있어야 하는 것이 문제인 것이다. 이는 서버 객체의 내부 구현을 변경했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 하는 결과를 만들었다. 

이뿐만이 아니다. 앞선 설계에서는 예약 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다. 따라서 시스템 안의 어떤 변경도 ReservationAgency의 변경을 유발한다.

낮은 응집도

서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 한다. 이는 코드를 수정하는 이유가 여러가지인지 보면 되는데, 앞선 설계의 ReservationAgency코드를 수정하는 이유가 아주 많다.(제어로직이 모여있기 때문)  

  • 할인 정책이 추가될 경우
  • 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
  • 할인 조건이 추가되는 경우
  • 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
  • 예매 요금을 계산하는 방법이 변경될 경우

응집도가 낮을 때 두가지 측면의 문제가 있다.

  • 변경과 아무 상관이 없는 코드들이 영향을 받게 된다.
  • 하나의 요규사항을 반영하기 위해 동시에 여러 모듈을 수정해야한다.

단일 책임 원칙은 응집도가 변경과 연관이 있다는 사실을 강조한다.

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

단일 책임의 원칙에서 "책임""변경의 이유"라는 의미로 사용되고 있다. 지금까지 만한 역할, 책임, 협력과는 다른 것이다.

04. 자율적인 객체를 향해

캡슐화를 지켜라

캡슐화는 설계의 제1원리다. 데이터 중심의 설계가 낮은 응집도와 높은 결합도라는 문제를 가지게 된 것도 다 캡슐화의 원칙을 위반했기 때문이다. 앞서 언급한바와 같이 접근자나 수정자를 통해 속성을 외부로 제공하는 것 역시 캡슐화 위반이다. 그럼 접근자와 수정자를 남발하면 어떤 문제가 생길까?

  1. 코드중복
    특정 데이터를 가지고 작업을 수행하는 로직이 여러곳에 중복 될 것이다. 예컨데, 사각형의 너비를 구하는 로직이 getter를 통해서 높이와 넓이를 곱하는 방식이라면 이곳저곳에서 이 로직을 중복 구현하게 되는 것 이다.
  2. 변경에 취약함
    데이터의 변수명이 바뀌거나, 타입이 바뀌면 의존하는 모든 객체가 변경된다.

해결 방법은 캡슐화를 강화 시키는 것이다. 이는 자신의 데이터를 스스로 변경하도록, 스스로 계산하도록 '책임을 이동'시킨 것이다. 이것이 바로 객체가 스스로를 책임진다는 말의 의미다.

스스로 자신의 데이터를 책임지는 객체

우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다. 객체는 데이터보다 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

영화 예매 시스템의 자율성을 보장한 결과는 아래의 그림과 같다. 이는 객체 스스로 구현하고 있다.

05. 하지만 여전히 부족하다

앞선 개선은 분명 처음 설계보다 나아졌지만, 여전히 데이터 중심의 설계이며 같은 문제가 발생한다. 

캡슐화 위반

기간 조건을 판단하는 isDiscountable(DayOfWeek dayOfWeek, LocalTime time)메서드의 시그니처는 구현을 알아야만 하며, 오버로딩된 isDiscountable(int sequence)역시 구현을 알아야 한다는 사실을 내포하고있다. 이는 여전히 구현이 수정되면 인터페이스가 변하는 문제점을 가지고 있으며, 캡슐화가 부족하다는 증거인 파급효과에서 자유로울 수 없다.

또한 Movie역시 내부 구현을 인터페이스에 노출시키고 있다. calculateAmountDiscountFee, calculatePercentDiscountFee, calculateNoneDiscountFee와 같은 메서드는 내부 구현을 만천하에 드러낸다.

 

캡슐화의 진정한 의미

캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다. 설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화해야 한다.

높은 결합도/낮은 응집도

캡슐화를 실패한 대가로 높은 결합도와 낮은 응집도를 가지게 되었다. 

06. 데이터 중심 설계의 문제점

캡슐화를 위반하므로서 결과적으로 변경에 유연하지 못하게 만들었다. 왜일까?

  • 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
  • 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다

데이터 중심의 설계는 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 된다. 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미치기 때문에 변경에 취약한 코드를 낳게 된다.

데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다

객체지향 설계는 협력하는 객체들의 공동체를 구축한다는 것을 의미한다. 따라서 협력이라는 문맥 안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 가장 중요하다. 올바른 객체지향 설계는 객체의 외부에 맞춰져 있어야 한다. 객체가 어떤 상태와 구현을 가지는지는 중요하지 않다. 중요한 것은 객체가 다른 객체와 협력하는 방법이다.(즉, 메시지가 우선되어야 하는 것이다.)

https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr

책 목차:

1장 객체, 설계 : https://inhyeok-blog.tistory.com/32
2장 객체지향 프로그래밍 : https://inhyeok-blog.tistory.com/33
3장 역할, 책임, 협력 : https://inhyeok-blog.tistory.com/34
4장 설계 품질과 트레이드오프 : https://inhyeok-blog.tistory.com/36
5장 책임 할당하기 : https://inhyeok-blog.tistory.com/37
6장 메시지와 인터페이스 :
7장 객체 분해 :
8장 의존성 관리하기 :
9장 유연한 설계 :
10장 상속과 코드 재사용 :
11장 합성과 유연한 설계 :
12장 다형성 :
13장 서브클래싱과 서브타이핑 :
14장 일관성 있는 협력 :
15장 디자인 패턴과 프레임워크 :


2장에서 우리는 객체지향 프로그램을 구조화하는 기본적인 방법과 상속을 이용해 다형성을 구현하는 기법을 소개했다. 하지만 객제지향의 본질은 협력하는 개체들의 공동체를 창조하는 것이다.

01. 협력

영화 예매 시스템 돌아보기

 협력 : 객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용

 책임 : 객체가 협력에 참여하기 위해 수행하는 로직

 역할 : 협력 안에서 수행하는 책임들이 모여 역할을 구성함

협력

 메시지 전송은 객체 사이에 협력을 하는 유일한 수단이다. 메시지를 받은 객체는 메시지를 처리할 메소드를 결정하고, 이는 객체가 자율적인 존재임을 보장한다. 객체는 내부 구현을 캡슐화 하는 방식으로 자율적인 존재가 되었고, 이로 인해 파급효과를 제한할 수 있게 되어서 변경에 용이해 질 수 있는 것이다.

 객체는 자신에게 할당된 책임을 수행하던 중에 도움이 필요한 경우 다른 객체에게 메시지를 보냄으로서 협력을 하게 되는 것이다. 

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

"어떤 객체도 섬이 아니다" : 애플리케이션 안에 어떤 객체도 협력하지 않는 객체는 없다.

 객체는 앞서 상태와 행동을 하나로 묶어둔 것이라고 말한바 있다. 그럼 앞선 예제에서 Movie는 어떤 행동과 상태를 가졌을까? 우리는 주로 '상영하기'와 같은 행동을 상상할 것이다. 하지만 그렇지 않았다. Movie는 요금을 계산하는 행동과 연관되어있다. 이는 Movie가 참여하고 있는 협력이 모두 영화 예매를 위해서였기 때문이다. 이렇게 객체의 행동은 협력을 통해 정해진다. 그렇다면 상태는 어떨까?

 상태는 행동에 의해서 결정된다. 객체의 행동이 정의되고 나면 필요한 상태들이 정의된다. 결국 상태 역시 협력에 의해서 결정되는 것이다. 결론적으로 협력은 객체를 설계하는 데 필요한 일종의 문맥(context)이다.

 

02. 책임

책임이란 무엇인가

 객체가 수행하는 행동을 책임이라고 부른다. 책임은 크게 하는 것아는 것으로 나누어 진다. 여기서 메시지와 협력은 다르다. 책임은 객체가 수행할 수 있는 행동을 종합적이고 간략하게 서술하는 것으로, 메시지 보다 추상적이고 크기도 더 크다. 따라서 개발을 하면서 책임이 여러개의 메시지로 구현되거나, 여러 객체로 분할되여 협력하게 되기도 한다. 

책임 할당

 INFORMATION EXPERT(정보 전문가) 패턴: 책임을 수행하는 데 필요한 정보를 가장 잘 알고있는 전문가에게 그 책임을 할당하는 것.

 Infromation Expert Pattern을 구현하기 위해서 우선 협력이라는 문맥을 정의해야한다. 협력을 설계하는 출발점은 시스템이 사용자에게 제공하는 기능을 시스템이 담당할 하나의 책임으로 바라보는 것이다. 그 이후 책임을 계속해서 나눠가는 과정이 바로 협력을 설계하는 방법이다. 정보 전문가에게 책임을 할당하는 것만으로도 상태와 행동을 함께 가지는 자율적인 객체를 만들 가능성이 높아진다.(어떤 경우에는 응집도와 결합도의 관점에서 정보 전문가가 아닌 다른 객체에게 책임을 할당하는 것이 더 적절한 경우도 있다.)

책임 주도 설계

 책임 주도 설계: 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법

책임 주도 설계의 과정
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

메시지가 객체를 결정한다

 메시지가 객체를 선택하게 했다는 것은 두가지 이유에서 중요하다.

첫째, 객체가 최소한의 인터페이스를 가질 수 있게 된다.

둘째, 객체는 충분히 추상적인 인터페이스를 가질 수 있게 된다.

-> 객체의 인터페이스는 무엇을 하는지 표현해야 하지만 어떻게 수행하는지를 노출해서는 안된다.

행동이 상태를 결정한다

 중요한 것은 상태가 아니라 행동이다.

 객체는 협력속에서 책임을 할당받아서 존재한다. 여기서 협력은 행동(메시지)으로만 가능하며, 상태는 이런 행동의 구현을 돕기 위한 재료에 불가하다.

 상태를 먼저 정의하고, 적당한 행동을 정하는 방식의 (즉, 구현에 초점을 맞춘) 설계 방법을 데이터-주도 설계라고 하는데, 이는 객체의 캡슐화를 저해한다.  

03. 역할

역할과 협력

 역할 : 객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합(객체의 목적)

우리가 앞서 책임 주도 설계를 했을 때에도 최초에 객체를 선택하는 것 보다는, 역할을 찾고 그 역할을 수행하는 객체를 선택하는 방식으로 설계가 진행된 것이다.

 그런데 객체만으로도 충분히 협력을 설계할 수 있을 것 같은데, 왜 굳이 역할이라는 번거러운 과정을 추가한 것일까?

유연하고 재사용 가능한 협력

 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있다.

 우리가 만약 객체만으로 협력을 설계 했다면,DiscountPolicy는 어떻게 설계 할 수 있을까? 아마도 2가지 할인 정책을 따로따로 구현해야 했을 것이다. 코드 중복은 만악의 근원이며, 경직된 일회성 코드를 생산한다. 앞서 역할은 책임의 집합으로 정의한다 서술한 바 있다. 역할은 Interface, Abstract Class로 표현되는데, 이를 통해서 외부 인터페이스만 공개 하므로서 하나의 설계에서 다양한 구현을 사용할 수 있게 하며, 기능을 추가하기도 용이하게 만든다.

객체 대 역할

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

 협력을 설계할 때 단 하나의 객체만 있는 역할도 역할로 설계해야 할까? 아니다. 하나의 객체만 가진 역할은 그저 객체로 간주한다.

 트리그비 린스카우는 역할을 가리켜 실행되는 동안 협력 안에서 각자의 위치를 가지는 객체들에 대한 별칭이라고 정의한다. 그는 협력은 역할들의 상호작용으로 구성되고, 협력을 구성하기 위해 역할에 적합한 객체가 선택되는 것이라고 설명한다.

 하지만 초반 설계 단계에서는 어떤 것이 객체이고, 어떤 것이 역할인지 뚜렵하지 않은 경우가 많다. 이에 저자는 초반 설계 과정에서는 애매하면 전부 객체로 설계한 후에, 비슷한 방식의 협력이 존재한다면 이를 역할로 뽑아내는 방식을 추천한다. 

역할과 추상화

 앞서 언급한 추상화의 두 가지 장점은 역할에서도 동일하게 적용된다.(중요한 정랙을 상위 수준에서 단순화 할 수 있다는 것과 설계가 유연해 진다는 장점) 

배우와 배역

 영한님의 강의에서 역할과 객체를 배우와 배역에 비유해서 설명 했던 것이 기억 날 것이다.

https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr

책 목차:

1장 객체, 설계 : https://inhyeok-blog.tistory.com/32
2장 객체지향 프로그래밍 : https://inhyeok-blog.tistory.com/33
3장 역할, 책임, 협력 : https://inhyeok-blog.tistory.com/34
4장 설계 품질과 트레이드오프 : https://inhyeok-blog.tistory.com/36
5장 책임 할당하기 : https://inhyeok-blog.tistory.com/37
6장 메시지와 인터페이스 :
7장 객체 분해 :
8장 의존성 관리하기 :
9장 유연한 설계 :
10장 상속과 코드 재사용 :
11장 합성과 유연한 설계 :
12장 다형성 :
13장 서브클래싱과 서브타이핑 :
14장 일관성 있는 협력 :
15장 디자인 패턴과 프레임워크 :

 


 이번 챕터는 이 책을 읽으면서 보게 될 다양한 주제를 얃은 수준으로 훑어보는 것이다. 따라서 혹여 현실적이지 않더라도 간단한 예제를 기반으로 진행할 것이다. 이제 가벼운 마음으로 2챕터를 읽어보자.

1. 영화 예매 시스템

 이번 채챕터는 영화 예매 시스템으로 예시로 설명 할 것이다.

요구사항 살펴보기

 영화 예매 시스템은 크게 4가지 요소가 있다.

  • 영화 정보
  • 상영 정보
  • 할인 조건
  • 할인 정책

특정 영화는 여러번 상영될 수 있으며, 이때 다양한 할인 조건(특정 시간대에 시작하는 영화, 또는 상영 번호[예컨데 조조할인])에 따라서 할인을 하게 되며, 이때 할인되는 금액은 할인 정책(10% 또는 1000원)에 따라 결정된다.

2. 객체지향 프로그래밍을 향해

협력, 객체, 클래스

 객체지향은 클래스를 지향하는 것이 아니라 객체를 지향하는 것이다. 이를 위해서 프로그래밍 하는동안 2가지에 집중해야 한다.

  • 필요한 클래스가 아닌 필요한 객체를 고민하라.

클래스는 상태와 행동을 공유하는 객체를 추상화 한 것이다. 따라서 객체의 상태와 행동을 먼저 결정하고, 클래스의 윤곽을 잡자.

  • 객체를 협력하는 공동체의 일원으로 봐라.

객체를 협력하는 공동체의 일원으로 바라보면 설계를 유연하고 확장 가능하게 만든다.

도메인의 구조를 따르는 프로그램 구조

도메인(Domain): 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야 

객체 지향의 장점은 요구사항 분석부터 마지막 구현까지 객체라는 동일한 추상화 기법을 사용할 수 있다는 것이다.

 

현실 세계에서 영화 예메 도메인의 개념과 관계를 나타내 보면 아래와 같다.

 

이제 이 도메인을 그대로 객체로 가져오면 아래와 같다.

이렇듯 도메인 개념의 구조를 따라서 클래스 구조를 만들면 프로그램의 구조를 이해하고 예상하기 쉽게 만들 수 있다.

클래스 구현하기

 클래스를 구현할 때 private과 public 접근 제어자를 사용해서 클래스의 내부와 외부를 구분해야한다. 그 이유를 저자는 2가지로 설명한다.

자율적인 객체

 객체는 상태와 행동을 함께 가지는 복합적인 존재이다. 이렇듯 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 부른다.

 또한 객체는 자율적인 존재이다. 객체지향 언어들은 캡슐화에서 한걸음 더 나아가 접근 제어 메커니즘을 제공한다. 이를 통해서 객체는 외부의 간섭을 최소화할 수 있다. 외부에서는 객체가 어떤 상태인지 알 수 없으며, 직접 개인하려고 해도 안된다. 단지 요청할 뿐이다.

 캡슐화의 접근 제어는 객체를 퍼블릭 인터페이스구현으로 나눈다. 일반적으로 객체는 상태는 숨기고 행동만 외부에 공개해야한다. 이를 통해서 퍼블릭 인터페이스와 구현을 철저히 구분하는 것 이다. 

프로그래머의 자유

 프로그래머의 역할은 클래스 작성자와 클라이언트 프로그래머로 구분해보자. 클래스 작성자는 클라이언트 프로그래머가 필요한 인터페이스만 공개하고, 내부 구현은 숨겨 둔다. 이를 구현 은닉(implementation hiding)이라고 부른다.

 구현 은닉을 통해서 클라이언트 프로그래머는 내부 구현을 모르고 인터페이스만 알아도 클래스를 사용할 수 있다. 또한 클래스 프로그래머는 인터페이스를 바꾸지 않는 한 외부에 미치는 영향을 걱정하지 않고 내부 구현을 변경할 수 있다. 

 설계가 필요한 이유는 변경을 관리하기 위해서 이다. 객체의 변경을 관리할 수 있는 기법중에 가장 대표적인 것이 바로 접근 제어다.

협력하는 객체들의 공동체

 객체들은 서로에게 Message를 보내서 원하는 정보를 얻어오거나, 서로에게 행동을 요청한다. 여기서 주목할 만한 점은 바로 Money타입인데, 금액정도는 그냥 long 타입으로 지정할 수 도 있을 것을 Money라는 클래스로 만들었다. 저자는 이렇듯 의미를 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현하라고 조언한다. Money클래스를 구현 하므로서 값이 금액과 관련되어 있다는 사실을 명시적으로 클라이언트 프로그래머에게 알릴 수 있었고, 금액과 관련된 로직이 서로 다른 곳에서 중복되어 구현되는 것을 막을 수 있었다. 이렇듯 개념을 명시적으로 표현하는 것은 전체적인 설계의 명확성과 유연성을 높히는 첫걸음이 된다.

 시스템이 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 협력(Collaboration)이라고 부른다. 앞서 구현한 코드는 수많은 객체들이 서로 메소드를 호출하며 협력했다.

협력에 관한 짧은 이야기

 객체가 서로 상호작용 할 수 있는 방법은 메시지를 전송/수신하는 것 뿐이다. 이는 메시지를 처리하는 메소드(Method)와는 다르다. 이 둘은 구분하는 것은 객체지향 패러다임을 유연하고, 확장 가능하며, 재사용 가능할 설계로 만드는데 아주 중요한데, 다형성(polymorphism)의 개념이 메시지와 메소드의 구분에서 출발한다.

 우리가 메소드를 호출한다고 믿었던 것은 단지 메시지를 전송하는 것이다. 왜냐면 클라이언트 객체는 메시지를 전송하면서 내부에 어떤 메소드가 있는지 알지 못하기 때문이다. 루비와 같은 언더들은 calculateMovieFee라는 메시지를 다른 메소드가 처리하게 할 수도 있다. 즉, 메시지를 처리하는 방법(Method)를 결정하는 것은 오롯이 객체의 몫인 것이다.

3. 할인 요금 구하기

할인 요금 계산을 위한 협력 시작하기

 이번에는 상속과 다형성을 이용해서 discountPolicy를 추상화 해 보자.

할인 정책과 할인 조건

 TEMPLATE METHOD 패턴(GOF94) : 부모 클래스에 기본적인 알고리즘을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴

 DiscountPolicy를 추상 클래스로 만들고, 정책의 종류에 따라 달라지는 로직을 추상 메서드로 만드므로서, Template Method패턴을 구현했다. DiscountCondiction은 인터페이스로 만들었다. 이렇게 구현을 하므로서 객체는 구현이 아니라 설계에 의존할 수 있게 되었다.

할인 정책 구성하기

 하나의 영화에 단 하나의 할인 정책만 설정 할 수 있다는 사실을 생성자에 명시해서 강제했다.반면 DiscountPolicy는 여러 DiscountCondition을 받을 수 있는 생성자를 만들었따.이처럼 생성자의 파라미터를 통해 초기화에 필요한 정보를 전달하도록 강제하면 올바른 상태를 가진 객체의 생성을 보장할 수 있다.

4. 상속과 다형성

Movie Class를 보면 할인 정책과 할인 조건에 대해서 아무런 정보도 가지고 있지않아. 그런데 Movie Class는 어떻게 할인에 대한 결정을 내릴 수 있었을까? 이를 의존성과 상속과 다형성을 이용한 선택적 조건 실행하는 방법으로 나누어서 알아본다.

컴파일 시간 의존성과 실행 시간 의존성

 의존성이 있다 : 어떤 클래스가 다른 클래스에 접근 할 수 있는 경로를 가지고 있거나, 해당 클래스의 객체의 메서드를 호출할 경우

 우리는 앞서 작성한 코드에서 Movie가 DiscountPolicy에만 의존하고 있는 것을 알고 있다. 이건 코드의 의존성을 의미한다. 하지만 실행시간에선 어떨까? Movie 클래스는 우리가 생성자에서 넣어준 구체 클래스를 의존하게 된다.

 여기서 중요한 사실은 코드의 의존성과 실생 서짐의 의존성이 서로 다를 수 있다는 것이다. 그리고 코드의 의존성과 실행 시점의 의존성이 달라서 유연하고, 확장 가능하며, 재사용할 수 있는 객체지향 설계를 할 수 있는 것이다.

 하지만 이런식의 코드는 가독성이 떨어지는 측면도 분명히 존재한다. 다시 말해서 이런 코드가 언제나 능사는 아닌 것이다. 우리는 훌륭한 객체지향 설계를 하기 위해서 유연성과 가독성 사이에서 고민해야 하는 것이다. 

차이에 의한 프로그래밍

 상속은 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 상속은 슈퍼클래스의 속성과 메서드를 받을 수 있으며, 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가할 수 있는 간편한 방법을 제공한다. 또한 부모크래스의 구현은 공유하면서도, 행동이 다른 자식클래스를 추가할 수 있다. 예컨데, DiscountPolicy에서 getDiscountAmount를 변경해서 Tamplate Method패턴을 구현한 것 처럼 말이다.

 이처럼 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference) 이라고 한다. 

상속과 인터페이스

 상속이 가치있는 이유는 부모클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 즉, 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에, 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.

 이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 부른다.

다형성

 Movie는 DiscountPolicy Type의 객체에게 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라 부른다.

다형성은 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력이다. 이때 다형적인 협력에 참여하는 객체는 모두 같은 메시지를 이해할 수 있어야 하고, 결국 같은 인터페이스를 가져야 한다. 다형성을 구현하기 위해서 기본적으로 메시지와 메서드를 실행 시점에 바인딩 해야하는데, 이를 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)이라고 한다.

 상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다. 이런 이유로 다형성을 이야기할 때 상속이 빠지지 않는다. 하지만, 다형성을 구현하는 방법은 이보다 많고 앞으로 설명할 것이다.

인터페이스와 다형성

인터페이스는 구현은 고유할 필요가 없고 외부 인터페이스만 구현하고 싶을 때 사용한다.

5. 추상화와 유연성

추상화의 힘

 지금까지의 예시에서 DiscountPolicy와 DiscountCondition은 자식 클래스보다 추상적이다. 이는 구현의 일부 또는 전체를 자식 클래스가 결정 할 수 있도록 결정권을 위임한다.

 이는 2가지 장점을 가져다 준다.

 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다는 것

 추상화를  사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다. 예컨데 "영화 예매 요금은 최대 하나의 할인 정책과 다수의 할인 조건을 이용해 계산할 수 있다."로 표현하게 되는 것이다. 

 추상화를 이용해상위 정책을 기술한다는 것은 애플리케이션의 협력 흐름을 기술하다는 것이다. 이는 아주 중요한데, 디자인 패턴이나 프레임워크 모두 추상화를 이용해서 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있기 때문이다.

 추상화를 이용하면 설계가 좀 더 유연해 진다는 것

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

유연한 설계

 여기서 DiscountPolicy에 할인하지 않는 정책을 추가하려면 어떻게 해야할까? 이때 Movie Class에서 DiscountPolicy가 null이면 할인되지 않은 금액을 반환 하는 것은 좋지 않다. 이는 할인 금액이 0원 이라는 사실을 결정하는 책임이 DiscountPolicy가 아닌 Movie쪽에 있기 때문이다. 책임의 위치를 결정하지 위해 조건문을 사용하는 것은 일반적으로 나쁜 설계이다.

 우린 기존의 Movie와 DiscountPolicy를 수정하지 않고 NoneDiscountPolicy라는 새로운 클래스를 추가해서 기능을 확장했다. 이로서 추상화를 중심으로 코드를 유연하고 확장 가능하도록 했다.

추상 클래스와 인터페이스 트레이드오프

추상 클래스 DiscountPolicy를 상속받는 NoneDiscountPolicy는 사실 getDiscountAmount()라는 메소드가 필요하지 않다. 이는 템플릿 메소드 패턴을 위해서 존재하는 것인데, NoneDiscountPolicy는 그냥 calculateDiscountAmount()를 호출할 때 Money.ZERO를 반환하면 되기 때문이다. 이를 위해서 DiscountPolicy를 interface로 선언하고, 기존 DiscountPolicy의 이름을 DefaultDiscountPolicy로 변경했다. 이는 NoneDiscountPolicy가 필요 이상의 인터페이스를 가지지 않아서 좋아보이지만, 설계가 복잡하고 과해 보이기도 한다.

 저자는 이를 통해 구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다는 사실을 보여준다. 더불어 모든 코드에는 합당한 이유가 있어야 한다고 첨언한다. 

코드 재사용

 코드를 재사용 하는 방법은 상속합성이 있다. 여기서 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용 하는 방법이다. 지금 Movie가 DiscountPolicy를 이용하는 방식이 바로 합성이다. 그럼데, 아래와 같이 Movie를 직접 상속 받아서 DiscountPolicy를 구현하면 안되는 걸까?

상속

 상속을 코드 재사용을 위해 사용한다면 2가지 관점에서 설계에 안 좋은 영향을 미친다. 하나는 캡슐화를 위반한다는 것이고, 하나는 설계를 유연하지 못하게 만든다는 것이다. 

 상속을 이용하려면 부모클래스의 내부를 알아야한다. 이는 결합도를 높히고, 결과적으로 상속을 과하게 사용한 코드는 변경에 취약해진다. 상속은 설계가 유연하지 않다. 상속은 부모와 자식 클래스의 관계를 컴파일 시점에 결정한다.

 만약 Movie를 상속받은 PercentDiscountMovie를 AmountDiscountMovie 객체로 변경해보자. 그럼 AmountDiscountMovie를 생성하고, PercentDiscountMovie 상태를 복사해야 할 것이다.

 이와같이 경직된 상속 보다는 Movie객체가 DiscountMovie를 가지는 형태로 코드를 재사용할 수 있다. 이를 합성이라고 부른다.

 

합성

 Movie는 DiscountPolicy의 코드를 재사용한다. 이는 상속과 다르게 인터페이스를 통해 약하게 결합한다. 이렇듯 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다. 합성은 상속의 2가지 문제를 모두 해결한다.

 

1. 인터페이스에 정의된 메시지를 통해서 재사용이 가능하기 때문에 구현을 캡슐화 한다.

2. 의존하는 인스턴스를 교체하는 것이 쉽기 때문에, 설계가 유연해진다.

 

그렇다고 해서 상속을 절대 금지하자는 것이 아니다. 코드를 재사용하는 경우에는 합성을, 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 조합해서사용하는 것이 당연하다.

 

 

+ Recent posts