1장: 객체, 설계

프로그래밍 이론

  • 이론보다 실무가 먼저

    • 어떤 분야든 초기 단계에서는 아무것도 없는 상태에서 이론을 정립하기보단 실무를 관찰한 결과를 바탕으로 이론을 정립하는 것이 최선이다.

  • 추상적인 개념과 이론은 훌륭한 코드를 작성하는 데 필요한 도구일 뿐 프로그래밍을 통해 개념과 이론을 배우는 것이 개념과 이론을 통해 프로그래밍을 배우는 것보다 더 좋다.

티켓 판매 애플리케이션

아래 요구사항에 맞게 프로그램을 객체지향적으로 작성하도록 한다.

  • 극장에는 이벤트 당첨자와 표를 구매하는 관람객이 입장할 수 있다.

  • 관람객

    • 이벤트에 당첨되었다면 초대장을 티켓으로 교환해야만 입장 가능하다.

    • 이벤트에 당첨되지 않았다면 티켓을 구매해야만 입장 가능하다.

  • 판매원

    • 매표소에서 초대장을 티켓으로 교환해주거나 티켓을 판매한다.

    • 자신이 일하는 매표소를 알고 있어야 한다.

객체로 표현하기

  • 이벤트 당첨자가 가지는 초대장 클래스이다.

public class Invitation {
	private LocalDateTime when;
}
  • 초대장을 통해 교환하거나 구매가 가능한 티켓 클래스이다.

public class Ticket { 
	private Long fee;
	public Long getFee() {
		return fee;
	}
}
  • 현금, 초대장, 티켓 등을 보관할 수 있는 가방 클래스이다.

public class Bag {
	private Long amount;
	private Invitation invitation;
	private Ticket ticket;
	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 Audience { 
	private Bag bag;
	public Audience(Bag bag) { 
		this.bag = bag;
	}
	public Bag getBag() {
		return bag;
	} 
}
  • 매표소를 구현하는 클래스이다. 판매하거나 교환해줄 티켓의 목록과 판매 금액을 포함한다.

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;
	}
}
  • 극장 클래스에서는 관람객을 입장시키는 역할을 한다.

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);
		} 
	}
}

설계 개선하기

  • 소프트웨어 모듈의 필수 기능

    • 정상적으로 동작해야 한다.

    • 변경을 위해 존재해야 하며 변경이 어려운 모듈은 변경이 용이하도록 개선해야 한다.

    • 코드를 읽는 사람과 의사소통 가능해야 한다.

  • 애플리케이션의 기능을 구현하는 데에 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거해야 한다. 즉, 결합도를 낮춰 변경이 용이한 설계를 만들어야 한다.

  • 기존 객체의 경우 관람객과 판매원이 소극장인 Theater 클래스에 의해 동작하는 수동적인 존재로 표현된다. 심지어 관람객의 가방을 직접 확인해 돈을 계산해 다시 넣어두므로, 결합도가 강하다.

  • 또한 Audience, TicketSeller 클래스를 변경하면 Theater에도 같이 변경해주어야 한다.

  • 이를 아래와 같이 TicketSeller 클래스가 직접 티켓 판매 역할을 담당하도록 하고, Audience 클래스가 직접 티켓 구매 역할을 담당하도록 하여 해결할 수 있다.

  • 객체 내부의 세부적인 사항을 캡슐화를 통해 감추어 변경하기 쉬운 객체를 만든다.

  • 이를 통해 Audience와 TicketSeller는 자신이 가진 소지품을 스스로 관리하므로 이해하기 용이하다.

public class TicketSeller {
	private TicketOffice ticketOffice;
	public TicketSeller(TicketOffice ticketOffice) {
		this.ticketOffice = ticketOffice;
	}
	public void sellTo(Audience audience) {
		ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
	}
}
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();
		}
	}
}
  • 밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 응집도(cohesion)가 높다고 한다. 객체 스스로 자신의 데이터를 처리하는 자율적인 존재여야 한다.

절차지향 vs 객체지향

  • 절차지향적인 방식

    • 기존의 설계는 모든 처리가 하나의 클래스 내에 위치하고 나머지 클래스는 단지 데이터의 역할만 수행하는 절차지향적인 방식이었다.

    • 하지만 이는 인간의 직관적인 생각과 반대되며 변경하기 어려운 코드를 양산한다.

  • 객체지향적 방식

    • 자신의 데이터는 스스로 처리하도록 동일한 클래스 내부에 데이터와 이를 제어하는 프로세스를 위치하도록 한다.

    • 즉, 캡슐화를 이용해 의존성을 적절히 관리하여 객체 사이의 결합도를 낮추어 변화에 용이하게 만든다.

    • 책임을 개별 객체로 이동시켜 스스로 수행하도록 한다. 적절한 객체에 적절한 책임을 할당하면 이해하기 쉬운 구조와 가독성있는 코드를 만들 수 있다.

책임 분리하기

  • 기존 Audience 클래스는 Bag 객체의 메서드들을 호출해 초대권이 있는지 확인하고 티켓으로 교환받거나 구매하였다. 이 로직을 bag의 hold 메서드 내부로 캡슐화하여 Bag 클래스에 책임을 주었다.

public class Audience {
	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();
		}
	}
	// ...
}

트레이드오프

  • 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 결국 설계는 트레이드오프의 산물이다. 어떤 경우에도 모든 사람들을 만족시킬 수 있는 설계를 만들 수는 없다.

  • TicketSeller는 TicketOffice 클래스의 티켓을 꺼내 판매하지만 이는 TicketOffice의 자율권을 침해한다. 따라서 sellTicketTo 메서드를 추가하여 직접 관람객에게 티켓을 팔도록 한다.

public class TicketOffice {
	public void sellTicketTo(Audience audience) {
		plusAmount(audience.buy(getTicket()));
	}
	private Ticket getTicket() {
		return tickets.remove(0);
	}
	private void plusAmount(Long amount) {
		this.amount += amount;
	} 
}
public class TicketSeller {
	public void sellTo(Audience audience) {
		ticketOffice.sellTicketTo(audience);
	}
}
  • 하지만 위와 같은 변경은 TicketOffice와 Audience 클래스 간의 의존성을 초래하였다. TicketOffice의 자율성과 Audience에 대한 결합도 제거 중 더 중요한 것을 선택해야만 한다.

의인화

  • 현실에서는 수동적인 존재라고 하더라도 객체지향의 세계에서는 모든 것을 능동적이고 자율적인 존재로 설계하는 원칙을 의미한다.

설계의 필요성

  • 좋은 설계란? 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용 할 수 있는 설계다.

  • 요구사항은 항상 변경되기 때문에 개발을 시작하는 시점에 모든 요구사항을 만족시키는 것은 불가능에 가깝다.

  • 코드 수정으로 인해 버그가 발생하지 않도록 하려면 쉽게 이해 가능한 코드와 변경을 수용할 수 있는 설계가 중요하다.

  • 훌륭한 객체지향 설계란? 객체 사이의 의존성을 적절하게 (강결합되지 않도록) 관리하는 설계다.

Last updated