객체, 설계1️⃣ 티켓 판매 애플리케이션 구현하기[배경][정책] [구현코드]2️⃣ 무엇이 문제일까?소프트웨어의 모듈의 세가지 목적예상을 빗나가는 코드변경에 취약한 코드3️⃣ 설계 개선하기자율성을 높이자무엇이 개선됐는가어떻게 한 것인가캡슐화와 응집도절차지향과 객체지향책임의 이동그래 거짓말이다!4️⃣ 설계가 왜 필요한가객체지향 설계결론
객체, 설계
이론이 먼저일까? 실무가 먼저일까?
- 대부분의 사람들은 이론이 먼저 정립 된 후 실무가 그 뒤를 따른다고 생각한다.
- 하지만 로버트 글래스는 어떤 분야를 막론하고 이론을 정립할 수 없는 초기에는 실무가 먼저 급속한 발전을 이룬다고 한다.
- 즉 이론보다는 실무가 먼저이며 초기 단계에서는 아무것도 없는 상태에서 이론을 정립하기보다는 실무를 관찰한 결과를 바탕으로 이론을 정립하는 것이 최선이다.
1️⃣ 티켓 판매 애플리케이션 구현하기
[배경]
작은 소극장을 경영하고 있다. 규모는 그리 크지 않으며 시설도 낡은 감이 있지만 실험적이면서도 재미있는 공연을 지속적으로 기획하고 발굴한 덕분에 조금씩 입소문을 타고 매출이 오르고 있는 상황이다.
[정책]
이벤트에 당첨된 관람객은 초대장을 티켓으로 교환한 후에 입장이 가능하다.
이벤트에 당첨되지 않은 관람객은 티켓을 구매해야만 입장이 가능하다.
즉 관람객을 입장시키기 전에 이벤트 당첨 여부를 확인해야 하고 이벤트 당첨자가 아닌 경우에는 티켓을 판매한 후에 입장시켜야함
[구현코드]
// 초대장 public class Invitation { private LocalDateTime when; } // 티켓 public class Ticket { private Long fee; public Long getFee() { return fee; } } // 가방 public class Bag { private Invitation invitation; private Long amount; private Ticket ticket; public Bag(Long amount) { this(null, amount); } public Bag(Invitation invitation, Long amount) { this.invitation = invitation; this.amount = amount; } public boolean hasInvitation() { return invitation != null; } public boolean hasTicket() { return ticket != null; } public void setTicket(Ticket ticket) { this.ticket = ticket; } public void minusAmount(Long amount) { this.amount -= amount; } public void plusAmount(Long amount) { this.amount += amount; } } // 티켓오피스 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) ); } public Long getAmount() { return amount; } public Ticket getTicket() { return tickets.remove(0); } public void minusAmount(Long amount) { this.amount -= amount; } public void plusAmount(Long amount) { this.amount += amount; } } // 티켓셀러 public class TicketSeller { private TicketOffice ticketOffice; public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; } public TicketOffice getTicketOffice() { return ticketOffice; } } // 손님 public class Audience { private Bag bag; public Audience(Bag bag) { this.bag = bag; } public Bag getBag() { return bag; } } // 소극장 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); } } }
2️⃣ 무엇이 문제일까?
소프트웨어의 모듈의 세가지 목적
- 실행 중에 제대로 동작해야 하며 이것은 모듈의 존재 이유라고 할 수 있다.
- 변경을 위해 존재한다. 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경이 가능해야함
- 코드를 읽는 사람과 의사소통 하는 것이다. 모듈은 특별한 훈련 없이도 개발자가 쉽게 읽고 이해할 수 있어야 한다.
로버트 마틴에 따르면 모듈은 제대로 실행되어야 하고, 변경에 용이해야 하며, 이해하기 쉬워야한다.
예상을 빗나가는 코드
Theater
클래스의enter
메서드의 책임은 무엇이고 무엇이 문제일까?- 소극장은 관람객의 가방을 열어 그 안에 초대장이 들어있는지 확인한다.
- 가방 안에 초대장이 들어 있으면 판매원은 매표소에 보관되어 있는 티켓을 관람객의 가방 안으로 옮긴다.
- 가방 안에 초대장이 들어있지 않다면 관람객의 가방에서 티켓 금액만큼 현금을 꺼내 메표소에 적립한 후에 메표소에 보관되어 있는 티켓을 관람객의 가방 안으로 옮긴다.
관람객과 판매원이 소극장의 통제를 받는 수동적인 존재라는 점..!
- 소극장이 담당하는 일이 너무 많으며 남의 가방을 뒤져서 확인하는 셈이다.
- 이해 가능한 코드란?
- 우리의 예상에서 크게 벗어나지 않는 코드이다.
- 현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건네며 티켓을 구매하는 관람객은 가방안에서 돈을 직접 꺼내 판매원에게 지불한다.
- 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고, 관람객에게서 직접 돈을 받아 매표소에 보관한다.
- 가장 심각한 문제는
Audience
와TicketSaller
를 변경할 경우에Theater
가 함께 변경되어야 한다는 점이다.
변경에 취약한 코드
- 처음에 작성한 코드의 가장 큰 문제는 변경에 매우 취약하다는 것이다.
- 정책이 바뀔때 코드들이 흔들리게 되버린다.
- 객체 사이의 의존성(dependency)과 관련된 문제다. 문제는 의존성이 변경과 관련돼 있다는 점이다.
- 의존성은 변경에 대한 영향을 암시한다.
- 의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에 의존하는 다른 객체도 함께 변경될 수 있다는 사실을 내포하고 있다.
- 객체 사이의 의존성을 완전히 없애는 것은 정답이 아니다. 객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이다.
- 객체 사이의 의존성이 강한 경우를 가리켜 결합도가 높다고 말한다. 반대로 합리적인 수준으로 의존하는 경우에는 결합도가 낮다고 말한다.
- 결합도는 의존성과 관련되어 있기 때문에 결합도 역시 변경과 관련이 있다.
- 두 객체 사이의 결합도가 높으면 높을수록 함께 변경될 확률도 높아지기 때문에 변경하기 어려워 진다.
우리의 목표는 애플리케이션의 기능을 구현하는데 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것이다.
설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이어야 한다.
3️⃣ 설계 개선하기
- 처음 코드는 로버트 마틴이 이야기한 세 가지 목적 중 한 가지는 만족시키지만 두 조건은 만족시키지 못한다.
- 기능은 제대로 수행하지만 이해하기 어렵고 변경하기 쉽지 않다.
- 코드를 이해하기 어려운 이유는
Theater
가 관람객의 가방과 판매원의 매표소에 직접 접근하기 때문이다. - 이것은 관람객과 판매원이 자신의 일을 스스로 처리해야 한다는 우리의 직관을 벗어난다.
- 즉 의도를 정확하게 의사소통하지 못하기 때문에 코드가 이해하기 어려워 진 것이다.
- 해결 방법은
Theater
가Audience
와TicketSaller
에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하면 된다.
자율성을 높이자
- 설계를 변경하기 어려운 이유는 Theater가 관람객, 판매원이 관리하는 모든 부분에 접근할 수 있기 때문이다.
- Audience와 TicketSaller가 직접 Bag과 TicketOffice를 처리하는 자율적인 존재가 되도록 설계를 변경해야 한다.
변경된 코드
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(); } } } public class TicketSeller { private TicketOffice ticketOffice; public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; } public void sellTo(Audience audience) { ticketOffice.plusAmount( audience.buy(ticketOffice.getTicket()) ); } }
TicketSaller
에서만 매표소에만 접근이 가능하도록 수정되었으며 이처럼 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 캡슐화라고 부른다.
Theater
클래스는 어디서도 매표소에 접근할 수 없으며 매표소가 판매원 내부에 존재한다는 사실도 모르게된다. 단지sellTo
메시지를 이해하고 응답할 수 있다는 사실만 알고있다.
무엇이 개선됐는가
- 수정된 예제 역시 첫번째 예제외 마찬가지로 관람객들을 입장시키는 데 필요한 기능을 오류 없이 수행한다. 따라서 동작을 수행해야한다는 로버트 마틴의 첫번째 목적을 만족시킨다.
- 수정된 관람객과 판매원은 자신이 가지고 있는 소지품을 스스로 관리한다.
- 이것은 우리의 예상과 정확하게 일치하며 읽는 사람과의 의사소통이라는 관점에서 이 코드는 확실히 개선되었다.
중요한 점은 관람객과 판매원이 변경되어도
Theater
를 함께 변경할 필요가 없어졌다는 것이다.어떻게 한 것인가
- 판매자가 티켓을 판매하기 위해
TicketOffice
를 사용하는 모든 부분을TicketSaller
내부로 옮기고, 관람객이 티켓을 구매하기 위해Bag
을 사용하는 모든 부분을Audience
내부로 옮긴 것이다.
- 자기 자신의 문제를 스스로 해결하도록 코드를 변경하여 변경에 용이하고 이해 가능하도록 수정했다.
캡슐화와 응집도
- 핵심은 객체 내부의 상태를 캡슐화하고 객체 간에 오직 메시지를 통해서만 상호작용 하도록 만든것이다.
Theater
는TicketSaller
의 내부에 대해서 전혀 알지 못한다. 단지sellTo
메시지를 이해하고 응답할 수 있다는 사실만을 알고 있을 뿐이다.
TicketSaller
역시Audience
의 내부에 대해서 전혀 알지 못한다.
- 밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도가 높다고 말한다.
- 자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 결합도를 낮추면서 응집도를 높일 수 있다.
객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임지도록 자율적인 존재로 만들어야 한다.
절차지향과 객체지향
- 수정하기 전의 코드는
Theater
의enter
메서드 안에서 관람객, 판매원, 매표소를 가져와 관람객을 입장시키는 절차를 구현했다. 모든 처리는enter
메서드안에 존재했다.
- 이 관점에서
enter
메서드는 프로세스(proceess)이며 관람객, 판매원, 매표소, 가방은 데이터(Data)이다. - 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍이라고 부른다.
- 이러한 절차적 프로그래밍은 우리의 직관에 위배되며 우리의 예상을 벗어나기 때문에 코드를 읽는 사람과 원활하게 의사소통 하지 못한다.
- 더큰 문제는 유지보수가 어렵게 만든다. 변경에 취약하기 때문에
- 수정된 코드는 자신의 데이터를 스스로 처리하도록 프로세스의 적절한 단계를 관람객과 판매원에게 이동시켰다.
- 이처럼 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍 하는 방식을 객체지향 프로그래밍 이라고 부른다.
훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다.
일반적으로 객체지향에 비해 변경에 좀 더 유연하다고 말하는 이유가 바로 이것이다.
책임의 이동
- 두 방식 사이에 근본적인 차이를 만드는 것은 책임의 이동이다.
- 수정 되기 전 코드는 주로
Theater
에 의해 제어된다는 사실을 알 수 있다. - 객체지향 언어로 표현하면 책임이
Theater
에 집중되어 있다.
- 이러한 책임을 이동하여 각 객체가 자신을 스스로 책임질 수 있도록 수정했다.
설계를 어렵게 만드는 것은 의존성 이라는 것을 기억하자.
해결 방법은 불필요한 의존성을 제거함으로써 객체 사이의 결합도를 낮추는 것이다.
결합도를 낮추기 위해서는 다른 객체가 몰라도 되는 세부 사항을 캡슐화 하는 것이다.
불필요한 세부사항을 객체 내부로 캡슐화하는 것은 객체의 자율성을 높이고 응집도 높은 객체들의 공동체를 만들 수 있다.
그래 거짓말이다!
- 앞에서 실생활의 관람객과 판매자가 스스로 자신의 일을 처리하기 때문에 코드에서의
Audience
와TicketSaller
역시 스스로 자신을 책임져야 한다고 말햇다. - 이것은 우리가 세상을 바라보는 직관과도 일치하며 이 직관에 따르는 코드는 이해하기 쉬운 경향이 있다.
- 그러나
Theater
,Bag
,TicketOffice
는 실세계에서는 자율적인 존재가 아니다.
비록 현실에서는 수동적인 존재라고 하더라도 일단 객체지향 세계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀐다.
자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화라고 부른다.
4️⃣ 설계가 왜 필요한가
- 설계란 코드를 배치하는 것이다.
- 좋은 설계란 무엇일까?
- 우리는 오늘 완성해야 하는 기능을 구현하는 코드를 짜야하는 동시에 내일 쉽게 변경할 수 있는 코드를 짜야한다.
- 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계다.
- 변경을 수용할 수 있는 설계가 중요한 이유는 요구사항이 항상 변경되기 때문이다.
- 모든 요구사항을 수집하는 것은 불가능에 가깝다.
- 코드를 변경할 때 버그가 추가될 가능성이 높기 때문인 이유도 존재한다.
객체지향 설계
- 우리가 진정으로 원하는 것은 변경에 유연하게 대응할 수 있는 코드이다.
- 객체지향 프로그래밍은 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공함으로써 요구사항 변경에 좀 더 수월하게 대응할 수 있는 가능성을 높여준다.
결론
- 훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다.
- 진정한 객체지향 설계로 나아가는 길은 협력하는 객체들 사이의 의존성을 적절하게 조절함으로써 변경이 용이한 설계를 만드는 것이다.