목차
- ☆ 표시가 붙은 부분은 스터디 중 나온 얘기로 책에 나오지 않는 내용입니다.
- 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.
들어가며 - 프로그래밍 패러다임
01 패러다임의 시대
- 현대인들에게 패러다임 → 한 시대의 사회 전체가 공유하는 이론이나 방법, 문제의식 등의 체계 (e.g. 천동설 → 지동설 로의 패러다임의 전환)
- 이 책에서 얘기하는 패러다임 전환 - 절차형 패러다임에서 절차형 패러다임으로의 변화
02 프로그래밍 패러다임
- ☆P.S 좋아하는 알고리즘 중 하나인 플로이드 와샬 알고리즘의 그 플로이드 와샬이 프로그래밍 패러다임 용어 처음 사용해서 신기했음!
- 프로그래밍 패러다임 - 특정 시대의 어느 성숙한 개발자 공동체에 의해 수용된 프로그래밍 방법과 문제 해결 방법, 프로그래밍 스타일
- 어떤 패러다임을 쓰느냐에 따라 해결할 문제를 바라보는 방식과 프로그램을 작성하는 방법이 달라진다.
- 프로그래밍 언어와 프로그래밍 패러다임을 분리해서 설명할 수 없다.
- 이책과 객사오와 다른 점 → 코드를 통해 패러다임을 이해하고 적용할 기술을 습득할 수 있음.
- 프로그래밍 패러다임은 과거의 패러다임의 단점을 보완함. 혁명적이 아니라 발전적.
CHAPTER 01. 객체, 설계
‘소프트웨어 설계’와 ‘소프트웨어 유지보수’ 분야에서는 실무가 이론보다 앞서 있다. → 소프트웨어 생명주기(계획>분석>설계>구현>테스트>유지보수(가장 김))를 동안 유지보수가 차지하는 비중을 보면 실망스러운 수준 → 소프트웨어 설계와 유지보수에 중점을 두려면 이론이 아닌 실무에 초점을 맞추는 것이 효과적이다.
01 티켓 판매 애플리케이션 구현하기 - step1 (github)
- ☆ TicketOffice의 getTicket()은 getter인데 로직이 들어가 있어서 인터페이스만 보고 유추할 수 없다.
public Ticket getTicket() {
return tickets.remove(0);
}
- step1의 경우 메인 로직인 Theater의 의존성이 높다.
- 절차지향적인 Theater class
package org.eternity.theater.step01;
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
public void enter(Audience audience) {
if (audience.getBag().hasInvitation()) {
Ticket ticket = ticketSeller.getTicketOffice().getTicket();
audience.getBag().setTicket(ticket);
} else {
Ticket ticket = ticketSeller.getTicketOffice().getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket);
}
}
}
- 작성된 프로그램의 로직은 예상대로 동작한다. 하지만 몇 가지 문제점이 있다.
02 무엇이 문제인가
- 소프트웨어 모듈이 가져야 하는 세 가지 기능 (로버트 마틴)
- 실행 중에 제대로 동작
- 간단한 작업만으로도 변경이 가능해야 함
- 특별한 훈련 없이도 개발자가 쉽게 읽고 이해할 수 있어야 함
- ☆ 변경하기 어렵고 이해하기 어렵지만 명백히 효율적인 코드가 있다며 어떻게 해야 할까? → 고객에게 영향을 안끼칠 정도라면 이해하기 쉬운걸 선택하는게 좋다. (예를들어 이하의 코드에서 N이 1000만 이하 정도라면 크게 영향을 끼치는 수준은 아닐 것이다.)
private int getSequenceSum1(int N) {
int sum = 0;
for (int i = 1; i <= N; i++) {
sum += i;
}
return sum;
}
private int getSequenceSum2(int N) {
return (N+N*N)/2;
}
- step 1의 문제A - 이해하기 어렵다.
- 관람객과 판매원이 소극장의 통제를 받는 수동적 존재이다. → 소극장이 관람객의 가방을 마음대로 열어본다. 매표소의 티켓과 현금도 마음대로 접근한다.
- 이해 가능한 코드란 그 동작이 우리의 예상에서 크게 벗어나지 않는 코드 → 현재의 코드는 우리의 상식과는 너무나도 다르게 동작하므로 코드를 읽는 사람과 제대로 의사소통하지 못한다.
- step 1의 문제B - 변경에 취약하다.
- Audience와 TicketSeller를 변경할 경우 Theater도 함께 변경해야 한다. → 관람객이 가방을 들고 있지 않다면?, 관람객이 현금이 아니라 신용카드를 사용한다면?, 판매원이 매표소 밖에서 티켓을 판매해야 한다면?
- 다른 클래스가 Audience의 내부에 대해 더 많이 알면 알수록 Audience를 변경하기 어려워진다.
- 객체 사이의 의존성(dependency)
- 어떤 객체가 변경될 때 그 객체에 의존하는 다른 객체도 함께 변경해야 함.
- 의존성이 과한 경우 = 결합도(coupling)이 높다
- 우리의 목표
- 애플리케이션의 기능을 구현하는 데 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거 (완전히 없애는 것이 정답은 아님. 객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것)
- 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만들어야 함.
03 설계 개선하기 - step 2 (github)
- 관람객과 판매원을 자율적인 존재로 만들자
- 관람객이 스스로 가방 안의 현금과 초대장을 처리
- 판매원이 스스로 매표소의 티켓과 판매 요금을 다루자
- Theater가 Audience와 TicketSeller에 관해 너무 세세히 알지 못하도록 정보를 차단
- 캡슐화 (encapsulation)
- 객체 내부의 세부적인 사항을 감추는 것
- 캡슐화를 통해 변경하기 쉬운 객체를 만들 수 있다.
- 결합도 낮춤 → 자율성 높힘
- 캡슐화 → 결합도 낮춤, 자율성 높힘, 응집도 높힘 (29page 내용)
- 개선A. Theater 클래스의 enter()에 있던 메인 로직을 TicketSeller의 sellTo() 함수로 가져가 캡슐화 하자.
- 캡슐화 후 간단해진 Theater
package org.eternity.theater.step02;
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
- TicketSeller의 인터페이스에만 의존하는 Theater
- 이제 Theater는 TicketSeller 내부에 TicketOffice가 있는걸 알지 못한다. (의존성이 제거됨)
- 개선B. 기존 Theater의 enter()에 있다가 TicketSeller의 sellTo()로 넘어간 로직에 있던 Audience도 buy() 함수를 만들어 캡슐화할 수 있다.
package org.eternity.theater.step02;
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
---
package org.eternity.theater.step02;
public class Audience {
private Bag bag;
public Audience(Bag bag) {
this.bag = bag;
}
public Long buy(Ticket ticket) {
if (bag.hasInvitation()) {
bag.setTicket(ticket);
return 0L;
} else {
bag.setTicket(ticket);
bag.minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
- 마찬가지로 TicketSeller는 Audience의 인터페이스에만 의존한다.
- 외부에서는 더이상 Audience가 Bag을 소유하고 있다는 사실을 알 필요가 없다.
- Audience의 구현을 수정하더라도 TicketSeller에는 영향을 미치지 않는다.
- 자신의 문제를 스스로 책임지고 해결하는 자율적인 존재가 된 Audience와 TicketSeller
- Audience가 가방이 아니라 작은 지갑을 소지하도록 변경 → 이제 Audience만 변경하면 된다.
- TicketSeller가 매표소가 아니라 은행에 돈을 보관하도록 변경 → 이제 TicketSeller 내부만 변경하면 된다.
- 객체의 자율성을 높이는 방향으로 설계를 개선 → 이해하기 쉽고 유연한 설계를 얻을 수 있다.
- 핵심
- 객체 내부의 상태를 캡슐화
- 객체 간에 오직 메시지를 통해서만 상호작용하도록 함
- 외부의 간섭을 최대한 배제하고 메시지를 통해서만 협력하는 자율적인 객체들의 공동체를 만드는 것이 훌륭한 객체지향 설계를 얻을 수 있는 지름길!
- 절차지향
- step 1의 코드에서 모든 처리는 Theater의 enter() 내에 존재했다.
- Theater의 enter()는 프로세스(Process)이고, Audience, TicketSeller, Bag, TicketOffice는 데이터(Data)이다.
- 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍이라 한다.
- 모든 처리가 하나의 클래스 안에 위치하고 나머지 클래스는 단지 데이터의 역할만 수행 ㅜ
- “변경은 버그를 부르고 버그에 대한 두려움은 코드를 변경하기 어렵게 만든다.”
- 객체지향
- 변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계다.
- 자신의 데이터를 스스로 처리하도록 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍이라 한다.
- 훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다.
- 책임의 이동! Theater에 몰려 있던 책임이 개별 객체로 이동했다.
- 위의 객체지향 프로그래밍의 정의는 구현 관점에서만 바라본 지극히 편협한 시각이지만, 갓 입문한 사람에게 도움이 되는 실용적인 조언이다. → 사실 객체지향 설계의 핵심은 적절한 객체에 적절한 책임을 할당하는 것이다. 따라서 객체가 어떤 데이터를 가지느냐보다는 객체에 어떤 책임을 할당할 것이냐에 초점을 맞춰야 한다. (☆클래스 이름에 맞는 행위를 줘야한다.)
트레이드 오프 - step 3 (github)
- 좀 더 개선해보자. Audience에게 끌려다니는 Bag도 자율적인 존재로 만들 수 있다. 이건 별 문제 없이 개선할 수 있다.
package org.eternity.theater.step03;
public class Bag {
private Long amount;
private Ticket ticket;
private Invitation invitation;
public Long hold(Ticket ticket) {
if (hasInvitation()) {
setTicket(ticket);
return 0L;
} else {
setTicket(ticket);
minusAmount(ticket.getFee());
return ticket.getFee();
}
}
private void setTicket(Ticket ticket) {
this.ticket = ticket;
}
private boolean hasInvitation() {
return invitation != null;
}
private void minusAmount(Long amount) {
this.amount -= amount;
}
}
---
package org.eternity.theater.step03;
public class Audience {
private Bag bag;
public Audience(Bag bag) {
this.bag = bag;
}
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
}
- 이번엔 TicketSeller에게 끌려다니는 TicketOffice도 자율적인 존재로 만들어보자. 이번에도 문제가 없을까?
package org.eternity.theater.step03;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TicketOffice {
private Long amount;
private List<Ticket> tickets = new ArrayList<>();
public TicketOffice(Long amount, Ticket... tickets) {
this.amount = amount;
this.tickets.addAll(Arrays.asList(tickets));
}
// 아래 코드는 책에서 설명한 것처럼 트레이드오프 후에 원래의 step02의 구현으로 복구해야 합니다.
public void sellTicketTo(Audience audience) {
plusAmount(audience.buy(getTicket()));
}
private Ticket getTicket() {
return tickets.remove(0);
}
private void plusAmount(Long amount) {
this.amount += amount;
}
}
---
package org.eternity.theater.step03;
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
// 아래 코드는 책에서 설명한 것처럼 트레이드오프 후에 원래의 step02의 구현으로 복구해야 합니다.
public void sellTo(Audience audience) {
ticketOffice.sellTicketTo(audience);
}
}
- 이제 TicketSeller는 TicketOffice의 인터페이스에만 의존하게 되었는데, 문제는 기존에 없던 TicketOffice와 Audience 사이의 의존성이 추가되어 버렸다.
- TicketOffice의 자율성은 높였지만, 전체 설계의 관점에서 결합도가 상승해버림.
- 현재로서는 결합도와 자율성 모두를 만족시키는 방법이 떠오르지 않는다. → 트레이드 오프! → 선택해야 한다.
- 어떤 기능을 설계하는 방법은 한가지 이상일 수 있다.
- 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있으므로 결국 설계는 트레이드오프의 산물이다.
- 어떤 경우에도 모든 사람들을 만족시킬 수 있는 설계를 만들 수는 없다.
- “설계는 균형의 예술이다. 훌륭한 설계는 적절한 트레이드오프의 결과물이다.”
- 의인화 (anthropomorphism)
- 사실 Theater, Bag, TicketOffice는 실세계에서 자율적인 존재가 아니다. 따라서 우리가 세상을 바라보는 직관과 일치하지 않는다.
- 현실에서는 수동적인 존재라도 객체지향의 세계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀐다.
- 훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가리킨다.
04 객체지향 설계
- “설계란 코드를 배치하는 것이다.”
- “설계는 코드 작성의 일부이며 코드를 작성하지 않고서는 검증할 수 없다.”
- 좋은 설계
- 우리가 짜는 프로그램의 두 가지 요구사항
- 오늘 완성해야 하는 기능을 구현하는 코드를 짜야한다.
- 내일 쉽게 변경할 수 있는 코드를 짜야 한다.
- 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계이다.
- 개발을 시작하는 시점에 구현에 필요한 모든 요구사항을 수집하는 것은 불가능에 가깝다. → 개발이 진행되는 동안 요구사항은 바뀔 수밖에 없다.
- 코드를 수정하지 않는다면 버그는 발생하지 않는다. 코드를 변경할 때 버그가 추가될 가능성이 높기 때문에 설계가 중요하다.
- 객체지향 설계
- 우리가 진정으로 원하는 것은 변경에 유연하게 대응할 수 있는 코드이다.
- 객체지향 프로그래밍은 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공한다. → 요구사항 변경에 좀 더 수월하게 대응할 수 있는 가능성을 높혀준다.
- 1장에서 나온 얘기인 데이터와 프로세스를 하나의 덩어리로 모으는 것은 훌륭한 객체지향 설계로 가는 첫걸음일 뿐이다. 진정한 객체지향 설계로 나아가는 길은 협력하는 객체들 사이의 의존성을 적절하게 조절함으로써 변경에 용이한 설계를 만드는 것이다.
'Study > 오브젝트' 카테고리의 다른 글
[오브젝트] 6장. 메시지와 인터페이스 (2) | 2022.12.08 |
---|---|
[오브젝트] 5장. 책임 할당하기 (0) | 2022.12.02 |
[오브젝트] 4장. 설계 품질과 트레이드오프 (0) | 2022.12.02 |
[오브젝트] 3장. 역할, 책임, 협력 (0) | 2022.11.24 |
[오브젝트] 2장. 객체지향 프로그래밍 (2) | 2022.11.23 |
댓글