객체, 설계에 대한 주제로 내용이다.
자세한 내용이 궁금하면 오브젝트를 펼쳐보는 걸 추천드립니다.📖
티켓 판매 어플리케이션 구현해보기
이벤트로 티켓권 소유하면 무료관람을 하는 작은 소극장을 구현하면 다음과 같다.
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);
}
}
}
핵심 클래스 코드는 더보기로 확인 바란다.
class Invitation {
private LocalDateTime when;
}
class Ticket {
private Long fee;
public Long getFee() {
return fee;
}
}
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.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;
}
}
class Audience {
private Bag bag;
public Audience(Bag bag) {
this.bag = bag;
}
public Bag getBag() {
return bag;
}
}
class TicketOffice {
private Long amount;
private List<Ticket> tickets = new ArrayList<>();
public TicketOffice(Long amount, List<Ticket> tickets) {
this.amount = amount;
this.tickets = tickets;
}
public Ticket getTicket() {
return tickets.remove(0);
}
public void minusAmount(Long amount) {
this.amount -= amount;
}
public void plusAmount(Long amount) {
this.amount += amount;
}
}
class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public TicketOffice getTicketOffice() {
return ticketOffice;
}
}
무엇이 문제인가
로버트 마틴은 소프트웨어 모듈이 가져야하는 세 가지 기능에 관해 다음과 같이 설명한다.
1. 실행 중에 제대로 동작
2. 변경을 위해 존재
3. 코드를 읽는 사람과 의사소통
위의 코드를 보면 1번만 만족하고 2,3번은 불만족하는 상태다.
예상을 빗나가는 코드
이해 가능한 코드란 그 동작이 우리의 예상에서 크게 벗어나지 않는 코드다. 현실에서 극장표를 구매할 때 고객이 스스로 가방을 열고 확인한다.
하지만 현재의 코드를 보면 티켓판매소가 고객의 가방을 직접 열고 확인한다. (현실이라면 절도행위다)
우리의 상식과는 너무도 다르게 행동하기 때문에 코드를 읽는 사람과 의사소통하지 못한다.
또한, Theater에서는 핵심 클래스의 모든 메서드를 직접 다 호출한다. 이 코드를 이해하기 위해선 핵심 클래스 내용을 모두를 알고 있어야 한다는 점이다.
변경에 취약한 코드
핵심 클래스 중 하나라도 변경이 생기면 Theater까지 수정이 필요한 구조다. 변경에 매우 취약하다.
이는 객체 사이의 의존성(dependency) 문제다. 의존성은 어떤 객체가 변경될 때 그 객체에 의존하는 다른 객체도 변경될 수 있다는 사실을 내포한다.
객체 사이의 의존성이 과한 경우를 가리켜 결합도(coupling)가 높다고 말한다.
의존성을 없애는 것이 답은 아니다. 객체지향 설계는 협력하는 객체들의 공동체를 구축하는 것이다.
따라서 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이다.
설계 개선하기
해결 방안은 간단하다 자율적인 존재로 만드면 되는 것이다. Theater는 각 클래스의 세부사항을 알고 사용했다. 이제는 각 클래스가 자율적으로 처리할 수 있고 외부에서는 접근 못하게 숨겨보자.
class Invitation {
private LocalDateTime when;
}
class Ticket {
private Long fee;
public Long getFee() {
return fee;
}
}
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.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;
}
}
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.plusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
class TicketOffice {
private Long amount;
private List<Ticket> tickets = new ArrayList<>();
public TicketOffice(Long amount, List<Ticket> tickets) {
this.amount = amount;
this.tickets = tickets;
}
public Ticket getTicket() {
return tickets.remove(0);
}
public void minusAmount(Long amount) {
this.amount -= amount;
}
public void plusAmount(Long amount) {
this.amount += amount;
}
}
class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
이처럼 개념적이나 물리적으로 객체 내부의 세부사항을 숨기는 것을 캡슐화(encapsulation)라고 부른다. 캡슐화하는 이유는 변경하기 쉬운 객체로 만드는 것이며, 접근을 제한하기 때문에 객체와 객체 사이의 결합도를 낮추고 설계를 유연하게 만들 수 있다.
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
이전과는 다르게 세부적인 사항을 몰라도 상관 없게 되었다.
캡슐화와 응집도
밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 개체를 가리켜 응집도(cohesion)가 높다고 한다.
응집도를 높게 하기 위해선 객체 스스로 자신의 데이터를 책임지고 처리하며, 외부의 간섭을 배제하고 메시지로만 협력해야 한다.
절차지향과 객체지향
처음의 Theater 코드를 보면, 데이터(data)와 프로세스(process)가 별도로 분리 되어 있다. 이 방식을 절차적 프로그래밍(Procedural Programming)이라고 부른다. 절차적 프로그래밍은 우리의 상식에 위배되기에 의사소통이 원활하지가 못하고 변경에 취약하고 어려운 코드를 양산하는 경향이 있다.
이후 자율적인 존재가 된 Theater 코드를 보면 데이터와 프로세스가 같은 객체안에 위치 한다. 이 방식을 객체지향 프로그래밍(Object-Oriented Programming)이라고 부른다. OOP는 의존성을 적절히 통제하고 변경의 여파를 최소화한다.
책임의 이동
두 방식의 차이를 만드는 것은 책임의 이동(shift of responsibility)이다. '책임'을 기능을 가리키는 객체지향 용어다.
절차지향 방식인 Theater를 보면 책임이 집중되어 있고, 객체지향 방식인 Theater를 보면 책임이 객체마다 분산되어 객체 스스로 책임을 진다.
객체지향 설계의 핵심은 객체가 어떤 데이터를 가지는 것보다는 객체에 어떤 책임을 할당하는 것인지가 중요하다.
* 객체지향 세상에서는 모든 것이 능동적이고 자율적인 존재로 바뀐다. 가방, 돌멩이 같은 무생물도 자율적인 존재로 활동한다. 이처럼 소프트웨어 객체를 설계하는 원칙을 의인화(anthropomorphism)라고 부른다
객체지향 설계
설계란 코드를 배치하는 것이다.[Metz12]
좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계다.
훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다.
데이터와 프로세스를 하나의 덩어리로 모으는 것은 첫걸음일 뿐이다. 진정한 길은 협력하는 객체들 사이의 의존성을 적절하게 조절하고 변경에 유연한 설계를 만드는 것이다.
'서적 > 오브젝트: 코드로 이해하는 객체지향 설계' 카테고리의 다른 글
오브젝트 05_ 책임 할당하기 (0) | 2021.06.09 |
---|---|
오브젝트 04_ 설계 품질과 트레이드 오프 (0) | 2021.06.08 |
오브젝트 03_ 역할, 책임, 협력 (0) | 2021.06.08 |
오브젝트 02_ 객체지향 프로그래밍 (0) | 2021.06.08 |
오브젝트 포스팅을 시작하며 (0) | 2021.06.04 |