설계
[오브젝트] 객체 지향 설계
mint*
2024. 12. 1. 19:36
728x90
절차지향 vs 객체지향
절차지향
- 프로세스와 데이터를 별도의 모듈에 위치시키는 방식
- 모든 처리가 하나의 클래스 안에 위치하고 나머지 클래스는 단지 데이터의 역할만 수행
public class Theater {
public void enter(Audience audience, TicketSeller seller) {
if (audience.getBag().hasInvitation()) { // 초대장이 있으면
Ticket ticket = seller.getTicketOffice().getTicket();
audience.getBag().setTicket(ticket); // Theater가 Audience의 가방을 직접 확인
} else { // 초대장이 없으면 구매
Ticket ticket = seller.getTicketOffice().getTicket();
audience.getBag().minusAmount(ticket.getFee());
seller.getTicketOffice().plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket);
}
}
}
// 단순 데이터 보관
class Audience {
private Bag bag;
public Bag getBag() { return bag; }
}
// 단순 데이터 보관
class TicketSeller {
private TicketOffice ticketOffice;
public TicketOffice getTicketOffice() { return ticketOffice; }
}
문제점
- Theater가 TicketSeller, TicketOffice, Audience, Bag 모두에 의존하고 있다.
자율적으로 객체가 행동한다
는 직관에 위배된다.- 데이터의 변경으로 인한 영향을 지역적으로 고립시킬 수 없다.
- Audience와 TicketSeller의 외부 구현을 변경하려면 Theater의 enter 메서드를 함께 변경해야한다.
- 변경하기 어려운 코드를 양산한다.
객체지향 프로그래밍
- 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식
- 자신의 데이터를 스스로 처리하도록 한다.
- 데이터를 사용하는 프로세스가 데이터를 소유하고 있는 클래스 내부로 옮겨진다.
- 책임을 이동한다.
- 하나의 변경으로 인한 여파가 다른 클래스로 전파되는 것을 억제한다.
- 변경하기 수월하다.
코드
/** 극장. */
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
/**
* 관람객을 맞이한다.
*
* @param audience 관람객
*/
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
/** 티켓 판매원. */
public class TicketSeller {
private TicketOffice ticketOffice;
/** 티켓 판매원은 자신이 일하는 매표소를 알고 있어야 한다. */
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
/** 티켓을 관람객에게 판매한다. */
public void sellTo(Audience audience) {
ticketOffice.sellTicketTo(audience);
}
}
/** 매표소. */
public class TicketOffice {
/** 매표소에서 판매한 모든 티켓의 금액. */
private Long amount;
/** 매표소에서 판매하거나 초대장과 교환해 줄 티켓의 목록. */
private List<Ticket> tickets = new ArrayList<>();
public TicketOffice(Long amount, List<Ticket> tickets) {
this.amount = amount;
this.tickets = tickets;
}
/** 판매할 티켓을 꺼내준다. */
private Ticket getTicket() {
// 편의상 티켓 컬렉션에서 첫 번째 위치에 저장된 티켓을 리턴한다.
return tickets.remove(0);
}
/** 판매 금액을 차감한다. */
private void minusAmount(Long amount) {
this.amount -= amount;
}
/** 판매 금액을 더한다. */
private void plusAmount(Long amount) {
this.amount += amount;
}
public void sellTicketTo(Audience audience) {
plusAmount(audience.buy(getTicket()));
}
}
/** 관람객. */
public class Audience {
private Bag bag;
public Audience(Bag bag) {
this.bag = bag;
}
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
}
/** 관람객이 소지품을 보관할 가방. */
public class Bag {
@Getter
private Long amount;
private Invitation invitation;
private Ticket ticket;
/**
* 이벤트에 당첨되지 않은 관람객. 초대장이 없다.
*
* @param amount 현금
*/
public Bag(Long amount) {
this.amount = amount;
}
/**
* 이벤트에 당첨된 관람객. 현금과 초대장이 있다.
*
* @param amount 현금
* @param invitation 초대장
*/
public Bag(Long amount, Invitation invitation) {
this.amount = amount;
this.invitation = invitation;
}
/** 관람객이 초대장을 갖고 있다면 true 를 리턴한다. */
private boolean hasInvitation() {
return invitation != null;
}
/** 관람객이 티켓을 갖고 있다면 true 를 리턴한다. */
public boolean hasTicket() {
return ticket != null;
}
private void setTicket(Ticket ticket) {
this.ticket = ticket;
}
/** 현금을 감소시킨다. */
private void minusAmount(Long amount) {
this.amount -= amount;
}
/** 현금을 증가시킨다. */
private void plusAmount(Long amount) {
this.amount += amount;
}
public Long hold(Ticket ticket) {
if (hasInvitation()) {
setTicket(ticket);
return 0L;
} else {
setTicket(ticket);
minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
결합도
- 객체 사이의 의존성이 강한 것
- 설계를 어렵게 만드는 것은 의존성이다.
- 불필요한 의존성을 제거함으로써 객체 사이의 결합도를 낮춘다.
- 결합도를 낮추기 위해 선택한 방법은 Theater가 몰라도 되는 세부사항을 Audience와 TicketSeller 내부로 감춰 캡슐화하는 것이다.
- 불필요한 세부사항을 객체 내부로 캡슐화하는 것은 객체의 자율성을 높이고 응집도 높은 객체들의 공동체를 창조한다.
객체지향 설계의 핵심 : 데이터를 가진 객체가 행동을 결정한다
분리 전 (TicketSeller 클래스)
public void sellTo(Audience audience) {
if (audience.getBag().hasInvitation()) { // audience의 bag을 들춰서 초대장을 확인
Ticket ticket = ticketOffice.getTicket();
audience.getBag().setTicket(ticket);
} else {
Ticket ticket = ticketOffice.getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketOffice.plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket);
}
}
문제점
- TicketSeller가 Audience의 내부 구조(Bag)를 너무 자세히 알고 있음
- Audience의 캡슐화가 깨짐 (내부 상태에 직접 접근)
- 티켓 판매와 관련된 모든 책임이 TicketSeller에 집중됨
분리 후
// TicketSeller 클래스
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
// 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();
}
}
- Audience가 자신의 Bag을
직접 관리
- TicketSeller는 단순히 티켓을 판매하고 금액을 받는 역할만 수행한다.
- TicketSeller가 Audience의 내부 구조를 알 필요가 없어짐
- 객체 간 의존성이 줄어듦
객체지향적 메서드 분리
1. 작업 단위로 분리 ❌
- 잘못된 방식: 흐름(if/else) 기준으로 메서드 분리
public class TicketSeller {
private TicketOffice ticketOffice;
// 흐름(if/else)에 따라 메서드를 분리 - 좋지 않은 방식
public void sellTo(Audience audience) {
if (audience.getBag().hasInvitation()) {
processWithInvitation(audience);
} else {
processWithoutInvitation(audience);
}
}
private void processWithInvitation(Audience audience) {
Ticket ticket = ticketOffice.getTicket();
audience.getBag().setTicket(ticket);
}
private void processWithoutInvitation(Audience audience) {
Ticket ticket = ticketOffice.getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketOffice.plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket);
}
}
public class Audience {
private Bag bag;
public Bag getBag() {
return bag;
}
}
하나의 완결된 작업 단위로 분리 ⭕
public class TicketSeller {
private TicketOffice ticketOffice;
// 티켓 판매라는 하나의 완결된 작업
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
public class Audience {
private Bag bag;
// 티켓 구매라는 하나의 완결된 작업
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
}
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 boolean hasInvitation() {
return invitation != null;
}
private void setTicket(Ticket ticket) {
this.ticket = ticket;
}
private void minusAmount(Long amount) {
this.amount -= amount;
}
}
public class TicketOffice {
private Long amount;
private List<Ticket> tickets = new ArrayList<>();
// 티켓 발행이라는 하나의 완결된 작업
public Ticket getTicket() {
return tickets.remove(0);
}
public void plusAmount(Long amount) {
this.amount += amount;
}
}
- 데이터를 외부에서 조작하는 것이 아닌, 객체 스스로 책임지고 처리한다.
- 데이터를 가진 객체가 관련 행동도 함께 수행해야 한다.
- 코드의 응집도가 높아진다.
- 변경이 필요할때 관련 객체만 수정하면 된다.
- 다른 객체에 미치는 영향이 최소화된다.
- 데이터를 가진 객체가 관련 행동도 함께 수행해야 한다.
- 메서드를 분리할 때는 "이 객체가 무슨 작업을 해야 하는가?" 를 기준으로 삼아야 한다.
- 단순히 프로그램의 흐름(if/else)을 기준으로 분리하면 안된다.
Reference
https://wikibook.co.kr/object/
https://github.com/johngrib/study-objects
728x90