‘오브젝트: 코드로 이해하는 객체지향 설계’를 읽고 공부한 내용입니다.
공부하며 작성한 내용이기 때문에 오류 사항이 있을 수 있습니다. 잘못된 부분은 피드백 부탁드립니다.
1장 내용
의존성 : 다른 객체 내부를 알면 알수록 변경에 취약해진다
초기의 코드는 Theater (극장) 객체에서 관객, 관객의 가방, 초대장, 티켓, 티켓 판매원, 티켓 판매소를 알고 있는 상태입니다.
그리고 티켓 구매 로직까지 모두 책임지고 있습니다.
전형적인 절차지향적 프로그래밍이라고 할 수 있습니다.
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 Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
// ticketSeller에게 티켓 판매 책임을 위임
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
public class TicketSeller {
// ...
// 관객의 구매 책임은 Audience 에게 위임, 판매소 재산 관리 책임은 판매소에게 위임
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
public class Audience {
// ...
public Long buy(Ticket ticket) {
if (bag.hasInvitation()) {
bag.setTicket(ticket);
return 0L;
} else {
bag.setTicket(ticket);
bag.minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
객체들의 응집도가 높아졌고 다른 객체들에 대한 Theater의 의존성이 낮아졌습니다.
티켓을 구매해 가방에 넣는 책임은 Audience로 이동했고, 티켓 판매소의 티켓을 파는 책임은 TicketSeller로 이동했습니다.
캡슐화를 통해 자신의 데이터를 스스로 책임지는 모습이 되었다고 할 수 있습니다.
캡슐화와 응집도
핵심은 객체 내부의 상태를 캡슐화하고 객체간에 오직 메세지를 통해서만 상호작용하도록 만드는 것이다. …
밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도가 높다고 말한다.
객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 한다.
이처럼 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍이라고 부른다. …
두 방식 사이에 근본적인 차이를 만드는 것은 책임(기능)의 이동이다.
리팩토링 이전에는 작업 흐름이 주로 Theater에 의해 제어된다는 사실을 알 수 있다.
객체들간의 메세지를 통한 상호작용이 생각보다 정말 중요한 개념이었다는걸 느끼게 됐습니다.
단순히 데이터나 프로세스를 담는 대상으로써 객체를 바라보는게 아니라, 추후 변경을 용이하게 하도록 하려면 객체들의 책임을 어떻게 적절하게 분배할지 고민하는게 객체지향이라고 생각합니다.
디미터 법칙
다른 객체가 어떠한 자료를 갖고 있는지 속사정을 몰라야 한다. (여러 개의 . 도트를 사용하지 말라)
객체 지향 프로그래밍에서 가장 중요한 것은 "객체가 어떤 데이터를 가지고 있는가?"가 아니라, "객체가 어떤 메세지를 주고 받는가?" 이다.
1장의 교훈은 디미터 법칙과 일맥상통한다고 생각합니다.
프로젝트에 적용해보기
이번 장에서 배운 내용을 현재 진행중인 프로젝트 코드에 적용해 리팩토링했습니다.
기능 명세
시간표와 시간표의 셀 객체가 있습니다.
시간표는 이름 필드를 가집니다(막학기).
시간표에는 셀들이 있고, 셀들은 과목명, 교수명, 요일, 시작 교시와 끝 교시 필드들을 가집니다.
시간표의 셀들끼리 겹쳐선 안 됩니다.
즉 서로 요일이 같고, 교시가 하나라도 겹치게 되면 유효한 시간표 셀이라고 할 수 없습니다.
리팩토링 대상은 그 유효성 검사 코드입니다.
리팩토링
시간표 객체는 셀 리스트를 갖고 있고, 자신의 셀 리스트를 순회하면서 인자로 받은 셀에 대해 하나씩 요일과 교시를 비교합니다.
리팩토링 이전:
public class Cell {
// 요일, 시작 교시, 끝 교시
private TimetableDay day;
private Integer startPeriod;
private Integer endPeriod;
}
public class Timetable {
private String name;
private List<Cell> cellList = new ArrayList<>();
public void validateOverlap(Cell otherCell) {
this.cellList.stream()
// 셀 객체의 책임을 대신 지고 있음
.filter(cell -> cell.getDay().equals(otherCell.getDay()) &&
Math.max(cell.getStartPeriod, otherCell.startPeriod)
<= Math.min(cell.getEndPeriod, otherCell.getEndPeriod())
.findAny()
.ifPresent(it -> {
throw new Exception("No!!");
});
}
}
중요한 점은 한 객체에서 다른 객체의 내부 정보를 깊이 알고 있었다는 점, 다른 객체의 책임을 대신 지고 있었다는 점입니다.
즉 결합도가 높고 응집도가 낮은 상태였습니다.
처음에 코드를 짰을 땐 "한번밖에 안 만드는 메서드니까 이대로 냅두자" 라고 생각했습니다.
"결국 시간표 객체의 메서드가 호출되니까.. 여기서 다 해버리면 되지 않나?" 라고도 생각했습니다.
지금 생각해보면 매우 절차지향적이었던것 같습니다.
지금 당장은 한번밖에 안 쓰인다고 하더라도, 언젠가 요구사항이 변경되어 셀 객체가 여기저기 쓰일지 아무도 모릅니다.
중복성 여부 확인의 책임을 셀로 이동했습니다.
즉, 셀 객체가 스스로 자신의 데이터를 책임지도록 했습니다.
리팩토링 이후:
public class Timetable {
private String name;
private List<Cell> cellList = new ArrayList<>();
public void validateOverlap(Cell otherCell) {
// 중복 여부 확인 책임은 Cell로 이동
boolean isOverlapped = cellList.stream()
.anyMatch(cell -> cell.isOverlapped(otherCell))
if (isOverlapped) {
throw new Exception("No!!");
}
}
}
public class Cell {
private TimetableDay day;
private Integer startPeriod;
private Integer endPeriod;
public boolean isOverlapped(Cell otherCell) {
return this.day.equals(otherCell.day) &&
Math.max(this.startPeriod, otherCell.startPeriod)
<= Math.min(this.endPeriod, otherCell.endPeriod);
}
}
리팩토링함으로써 셀 객체의 응집도를 높이고, 셀 객체에 대한 의존성을 낮출 수 있었습니다.
다른 코드들도 비슷하게 리팩토링중인데, 조금씩 객체지향과 친해지고 있는 것 같아 뿌듯합니다.
'CS > OOP' 카테고리의 다른 글
[오브젝트] 6장 - 메시지와 인터페이스 (0) | 2024.01.22 |
---|---|
[오브젝트] 5장 : 책임 할당과 책임 중심 설계 (0) | 2024.01.18 |
[오브젝트] 4장 - 데이터 중심 설계의 문제점과 캡슐화 (0) | 2024.01.15 |
[오브젝트] 3장 - 역할, 책임, 협력 (0) | 2024.01.11 |
[오브젝트 스터디] 2장 - 상속과 다형성 (0) | 2024.01.10 |