////
Search

1장 - 객체, 설계

Date
2024/01/23 12:28
Tags

1. 티켓 판매 애플리케이션 구현하기

[오브젝트] 13p
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); } } } public class Bag { private Long amount; private Invitation invitation; private Ticket ticket; public Bag(Long amount) { this(null,amount); } public Bag(Invitation invitation, long amount) { this.amount = amount; this.invitation = invitation; } 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 Invitation { private LocalDateTime when; } public class Ticket { private Long fee; public Long getFee() { return fee; } } 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 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; } }
Java
복사
극장 어플리케이션
소극장은 관람객 가방 안에 티켓이 있는지 확인한다.
초대장이 있다면 티켓판매원을 통해 티켓을 가져오고 가방에 티켓을 넣어준다.
초대장이 없다면 티켓판매원을 통해 티켓을 가져오고
가방에서 금액을 차감하고
티켓셀러는 매출을 올린다.
가져온 티켓을 가방에 넣어준다.

2. 무엇이 문제인가

[클린 소프트웨어]를 작성한 마틴의 말에 따르면 다음의 조건을 만족해야지 좋은 모듈이라 말할 수 있다.
모든 모듈은 제대로 실행되어야 한다.
변경이 용이해야 한다.
이해하기 쉬어야 한다.
위 코드에서는 관람객 입장을 위한 기능을 오류없이 수행했지만 나머지 두 개의 조건은 만족시키지 못했다.

2.1. 예상을 빗나가는 코드

위 코드에서 문제점은 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재라는 점이다.
소극장이라는 제 3자가 초대장 확인을 위해 관람객의 가방을 마음대로 열어본다.
마찬가지로 판매원의 허락 없이 소극장이 티켓과 현금을 마음대로 접근할 수 있다.
이해 가능한 코드란 우리의 예상에서 크게 벗어나지 않는 코드를 의미한다.
때문에 앞서 본 코드는 우리의 예상을 빗나간다.
코드를 이해하기 어려운 또 다른 이유로는 코드를 이해하기 위해 여러 세부적 내용들을 한번에 기억하고 있어한다는 점이다.

2.2. 변경에 취약한 코드

[오브젝트] p17
더 심각한 문제는 위 코드는 변경에 취약하다는 점이다.
관람객이 가방을 들고 있지않다면?
관락객이 현금이 아닌 신용카드를 사용한다면?
판매원이 매표소 밖에서 티켓을 판매해야한다면?
이런 가정이 깨지는 순간 모든 코드가 일시에 흔들리게 된다.
위 코드에서는 특정 객체가 수정된다면 해당 객체를 의존하고 있는 객체도 함께 수정되어야 한다.
다른 클래스마다 직접적으로 특정 클래스와 관계가 많아질 수록 점점 더 수정은 어려워진다.
객체 사이의 의존성(Dependency)의 문제로 이는 변경과 관련있다는 점이다.
의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포되어있다.
그렇다면 의존성을 완전히 없애는 것이 답인가?
그건 아니다. 객체지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이다.
따라서 애플리케이션 구현에 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것을 목표로 한다.
객체사이 의존성이 과한 경우를 가리켜 결합도(coupling)가 높다고 말한다.
객체 사이 결합도가 높다면 함께 변경될 확률이 높아진다.
따라서 설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것

3. 설계 개선하기

코드를 이해하기 어려운 이유는 Theater가 관람객의 가방과 판매원의 매표소에 직접 접근하기 때문이다.
의도를 정확히 소통하지 못하기 때문에 코드가 이해하기 어려워진 것이다.
해결 방법은 간단한데, TheaterAudienceTicketSeller에 관해 너무 세세히 알지 못하도록 정보를 차단하면 된다.
다시 말해 관람객과 판매원을 자율적인 존재로 만들면 된다.

3.1. 자율성을 높이자

설계를 변경하기 어려운 이유?
TheaterAudienceTicketSeller뿐만이 아닌 객체 내의 멤버 변수까지 맘대로 접근할 수 있기때문이다.
즉 상위의 객체가 하위의 객체를 너무 자세히 알고있어 발생하는 문제이다.
public class Audience { private Bag bag; public Audience(Bag bag) { this.bag = bag; } public Long buy(Ticket ticket) { if(bag.hasInvitation()) { bag.getTicket(ticket); return 0; } else { bag.minusAmount(ticket.getFee()); bag.setTicket(ticket); return ticket.getFee(); } } } public class TheaterSeller { private TicketOffice ticketOffice; public Theater(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; } public void sellTo(Audience audience) { iticketOffice.plusAmount(audience.buy(ticketOffice.getTicket())); } } public class Theater { private TicketSeller ticketSeller; public Theater(TicketSeller ticketSeller) { this.ticketSeller = ticketSeller; } public void enter(Audience audience) { ticketSeller.sellTo(audience); } }
Java
복사
우선 Theater가 담당하던 기능을 TicketSeller에서 sellTo메서드로 상세한 행동을 감춰 캡슐화한다.
개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 캡슐화라고 부른다.
캡슐화를 통해 객체 내부로의 접근을 제한하면 객체와 객체 사이의 결합도를 낮출 수 있기 때문에 설계를 좀 더 쉽게 변경할 수 있게 된다.
Theater는 오직 인터페이스에만 의존하며 TicketSeller내부에 TicketOffice 인스턴스를 포함하고 있다는 구현에 영역에 속한다.
인터페이스와 구현으로 나누고 인터페이스만 공개하는 방식은 결합도를 낮추고 쉬운 코드를 작성하는 기본적인 설계 원칙이다.
buy메서드는 인자로 전달된 TicketBag에 넣은 후 지불된 금액을 반환한다.
Audience는 자신의 가방 안에 초대장이 있는지 스스로 확인하여 제 3자의 접근을 허락하지 않도록 한다.
[오브젝트] p24

3.2. 무엇이 개선됐는가

수정된 코드도 필요한 기능을 오류 없이 수행한다.
수정된 AudienceTicketSeller는 소지품을 스스로 관리한다.
우리의 예상과 정확하게 일치하며 사람과 의사소통이라는 관점에서 확실해 개선되었다.
모든 변경은 각각의 객체로 제한되어 추가적인 기능을 만들고 싶을 때 변경할 객체만 집중할 수 있게되었다.
따라서 편의성과 변경 용이성 측면에서 확실히 개선되었다.

3.3. 어떻게 한 것인가

TicketOffice를 사용하는 모든 부분을 TicketSeller로 옮기고, Bag를 사용하는 모든 부분을 Audience로 옮긴것이다.
Theater가 다른 클래스를 너무 자세히 알고있어 발생한 문제였다고 말할 수 있다.
클래스가 자신의 일을 수행하기 위한 기능의 응집도를 높히고 객체간의 결합도는 낮춰 유연하고 이해하기 쉬운 코드가 되었다.

3.4. 캡슐화와 응집도

캡슐화 : 객체 내부 상태를 알 수 없고 오직 메세지를 통해서만 상호작용하도록 만드는 것
응집도 : 밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에 위임하는 행위

3.5. 철차지향과 객체지향

수정 전 코드의 경우 Theaterenter메서드는 프로세스이며 Audience, TicketSeller, Bag, TicketOffice데이터, 프로세스와 데이터를 별도로 모듈에 위치시키는 방식을 절차적 프로그래밍이라 한다.
절차적 프로그래밍은 우리의 직관에 위배된다.
데이터의 변경으로 인한 영향이 지역적으로 고립시키기 어렵다.
수정 후 코드처럼 데이터를 사용하는 프로세스가 데이터를 소유하도록 프로그래밍 하는 방식이 객체지향 프로그래밍이라 부른다.
변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계다.
훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것
객체 내부의 변경은 객체 외부에 파급되지 않도록 제어할 수 있기 때문이다.

3.6. 책임의 이동

두 객체 사이의 근본적인 차이를 만드는 것은 책임의 이동(shith of responsibility)이다.
[오브젝트] p28
[오브젝트] p28
절차지향 설계는 책임이 몰려있는 반면 객체지향 설계는 각 객체가 스스로 책임을 지니고 작동한다.
몰려있던 책임을 개별 객체로 이동하는 것 그것을 책임의 이동이라 한다.
의존성은 설계를 어렵게 만들기 때문에 불필요한 의존성을 제거하여 결합도를 낮춰야 한다.

3.7. 더 개선할 수 있다.

public class Bag { private Long amount; private Ticket ticket; private Invitation invitation; public Long hold(Ticket ticket) { if(hasInvitation()) { setTicket(ticket); return 0; } else { setTicket(ticket); minusAmount(ticket.getFee()); return ticket.getFee(); } } ... } public class Audience { public Long buy(Ticket ticket) { return bag.hold(ticket); } } public class TicketOffice { public void sellTicketTo(Audience audience) { plusAmount(audience.buy(getTicket())); } ... } public class TicketSeller { public void sellTo(Audience audience) { ticketOffice.sellTicketTo(audience)); } }
Java
복사
Audience는 스스로 티켓을 구매하고 가방안의 내용물을 직접 관리한다.
하지만 Bag은 과거의 Audience처럼 스스로 자기 자신을 책임지지 않고 Audience에 의해 끌려다니는 수동적인 존재다.
Bag 객체를 자율적인 존재로 바꿔 상태와 관련된 행위를 함께 가지는 응집도 높은 클래스로 만들 수 있다.

4. 객체지향 설계

4.1. 설계가 왜 필요한가

설계는 코드를 작성하는 매 순간 코드를 어떻게 배치할 것인지를 결정하는 과정에서 나온다.
설계는 코드 작성의 일부이며 코드를 작성하지 않고서는 검증할 수 없다.
앞서 첫 번째로 작성해본 소극장의 코드는 데이터와 프로세스를 나눠 별도로 배치한 반면 두 번째 코드는 필요한 데이터를 보유한 클래스 안에 프로세스를 함께 배치했다.
좋은 설계란 무엇인가?
오늘의 요구하는 기능을 온전히 수행하며 내일의 변경을 매끄럽게 수용할 수 있는 코드

4.2. 객체지향 설계

따라서 우리가 진정으로 원하는 것은 변경에 유연하게 대응할 수 있는 코드다.
OOP는 의존성을 효율적으로 통제할 다양한 방법을 제공하여 요구사항 변경에 더 수월하게 대응할 수 있는 가능성을 높여준다.
훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다.
객체간 의존성은 어플리케이션을 수정하기 어렵게 만드는 주범이다.