이번 글에서는 데이터 중심 설계를 통해 '왜 객체를 지향하여 설계해야 하는지'에 대해 알아봅시다.
카페 예제를 데이터 중심 설계해보겠습니다. 그리고 왜 객체지향 설계가 되어야하는지에 대해 알아보겠습니다.
[!INFO]
'객체지향 제대로 파고들기 1'에서 진행했던 카페 예제를 참고해주세요!
카페 예제를 데이터 중심적으로 설계하기 위해 필요한 데이터를 만들어보겠습니다.
작성해야 할 데이터의 집합은 다음과 같습니다.
객체 | 이름 | 설명 |
---|---|---|
Espresso | 에스프레소 | 에스프레소는 판매하는 음료입니다. |
CouponBook | 쿠폰 | 쿠폰 10개를 모으면 에스프레소 1개를 공짜로 받습니다. |
Customer | 고객 | 고객은 음료를 구매합니다. |
Barista | 바리스타 | 바리스타는 음료를 제조합니다. |
Cafe | 카페 | 카페는 정산 금액을 관리합니다. |
Order | 주문서 | 음료를 판매하고 주문서를 생성합니다. |
OrderAgency | 주문 로직 | 주문을 처리합니다. |
먼저 Espresso를 아래와 같이 작성해보겠습니다.
public class Espresso {
public final String name = "Espresso";
// price 데이터를 캡슐화하기 위해 private으로 설정합니다.
private Long price;
// Getter 메서드를 통해 price 데이터를 가져옵니다.
public Long getPrice() {
return price;
}
// Setter 메서드를 통해 price를 설정합니다.
public void setPrice(Long price) {
this.price = price;
}
}
나머지 요소들도 Espresso와 비슷한 맥락으로 작성해 보겠습니다.
먼저 CouponBook을 작성합니다. CouponBook은 음료를 구매하고 받은 쿠폰의 횟수를 기록합니다.
public class CouponBook {
private int count;
public CouponBook(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
Customer를 작성해보겠습니다.
Customer는 CouponBook과 Espresso를 가지고있습니다.(Espresso는 초기에 가지고 있지 않습니다.)
public class Customer {
private Long balance;
private CouponBook couponBook;
public Customer(Long balance, CouponBook couponBook) {
this.balance = balance;
this.couponBook = couponBook;
}
}
Customer의 변수들을 위한 getter와 setter를 정의해줍니다.
public class Customer {
public Long getBalance() {
return balance;
}
public void setBalance(Long balance) {
this.balance = balance;
}
public CouponBook getCouponBook() {
return couponBook;
}
public void setCouponBook(CouponBook couponBook) {
this.couponBook = couponBook;
}
}
Cafe와 Barista의 데이터 집합들도 작성해보겠습니다.
먼저 Cafe는 다음과 같습니다.
public class Cafe {
private Long balance;
private Barista barista;
public Cafe(Long balance, Barista barista) {
this.balance = balance;
this.barista = barista;
}
public Long getBalance() {
return balance;
}
public void setBalance(Long balance) {
this.balance = balance;
}
public Barista getBarista() {
return barista;
}
public void setBarista(Barista barista) {
this.barista = barista;
}
}
Cafe는 정산 금액(balance)와 Barista를 가지고 있습니다.
Barista는 단순히 음료를 제조하기만 합니다.
public class Barista {
public Espresso createEspresso() {
return new Espresso();
}
}
[!INFO]
Barista는 특별히 데이터 가지고 있지 않습니다. 이는 여러분들의 이해를 돕기위해 현실세계의 고증을 조금 반영한 부분일 뿐입니다.
마지막으로 Order와 OrderAgency입니다.
먼저 Order를 작성해보겠습니다.
Order는 주문서이기 때문에 에스프레소와 고객 그리고 구매 수량(quantity)이 있습니다.
public class Order {
private Customer customer;
private Espresso espresso;
private int quantity;
public Order(Customer customer, Espresso espresso, int quantity) {
this.customer = customer;
this.espresso = espresso;
this.quantity = quantity;
}
}
OrderAgency는 주문을 처리하는 프로세스를 담당합니다. 주문이 성공적으로 처리된다면 Order를 생성하고 종료됩니다.
public class OrderAgency {
public Order order(Cafe cafe, Customer customer, int quantity) {
Espresso espresso = cafe.getBarista().createEspresso();
// 쿠폰이 10개면 에스프레소 한 잔을 무료로 제공합니다.
if (customer.getCouponBook().getCount() == 10 && quantity == 1) {
customer.setCouponBook(new CouponBook(0));
return new Order(customer, espresso, quantity);
}
long totalAmount = espresso.getPrice() * quantity;
// 고객의 잔액보다 에스프레소 가격이 더 크다면 예외처리합니다.
if (totalAmount > customer.getBalance()) {
return null;
}
// 고객의 잔액을 차감합니다.
customer.setBalance(customer.getBalance() - totalAmount);
// 차감한 만큼 카페의 잔액을 증가시킵니다.
cafe.setBalance(cafe.getBalance() + totalAmount);
// 쿠폰은 에스프레소를 구매한 횟수(quantity) 만큼 증가합니다.
customer.getCouponBook().setCount(customer.getCouponBook().getCount() + quantity);
return new Order(customer, espresso, quantity);
}
}
데이터 중심적 설계한 카페 예제의 문제점은 다음과 같습니다.
각각의 문제점들을 살펴볼 때 '변경'이라는 관점에서 주의 깊게 살펴보시길 바랍니다.
대부분의 객체는 '접근자와 수정자 비슷한 메서드'를 통해 대부분의 내부 상태를 캡슐화하지 못하고 있습니다.
예를 들면, Cafe의 getBalance()와 setBalance()가 있습니다.
public class Cafe {
private Long balance;
public Long getBalance() {
return balance;
}
public void setBalance(Long balance) {
this.balance = balance;
}
}
상태(balance)의 가시성을 private으로 부여한다고 해도, getBalance()와 setBalance()를 통해 balance의 존재를 퍼블릭 인터페이스에 노골적으로 드러냅니다.
캡슐화를 하지 못한다는 것은 모듈들이 서로 강하게 결합되어 있을 확률이 높습니다. 이는 시스템 전체가 변화에 민감하게 반응한다는 것을 의미합니다.
OrderAgency는 '주문하기(order)'라는 행위를 위해 모든 객체를 관여하고 있습니다.
public class OrderAgency {
public Order order(Cafe cafe, Customer customer, int quantity) {
Espresso espresso = cafe.getBarista().createEspresso();
// 쿠폰이 10개면 에스프레소 한 잔을 무료로 제공합니다.
if (customer.getCouponBook().getCount() == 10 && quantity == 1) {
customer.setCouponBook(new CouponBook(0));
return new Order(customer, espresso, quantity);
}
long totalAmount = espresso.getPrice() * quantity;
// 고객의 잔액보다 에스프레소 가격이 더 크다면 예외처리합니다.
if (totalAmount > customer.getBalance()) {
return null;
}
// 고객의 잔액을 차감합니다.
customer.setBalance(customer.getBalance() - totalAmount);
// 차감한 만큼 카페의 잔액을 증가시킵니다.
cafe.setBalance(cafe.getBalance() + totalAmount);
// 쿠폰은 에스프레소를 구매한 횟수(quantity) 만큼 증가합니다.
customer.getCouponBook().setCount(customer.getCouponBook().getCount() + quantity);
return new Order(customer, espresso, quantity);
}
}
CouponBook.count, balance, price, amount 등 여러 데이터가 OrderAgency.order라는 하나의 제어 로직에 집중되어 있습니다. 이러한 형태는 결합도 측면에서 데이터 중심 설계가 가지는 단점 중 하나입니다.
즉, Customer, CouponBook, Cafe, Barista, Order 등 어떠한 객체에 변경이 발생하면 OrderAgency도 어쩔 수 없이 변경될 수밖에 없음을 의미합니다.
(이는 OrderAgency가 거의 모든 객체에 의존한다는 것을 알 수 있습니다.)
서로 다른 이유로 변경되는 코드들이 모여있을 경우 '응집도가 낮다'라고 이야기합니다.
먼저 응집도를 이해하기 위해서는 변화의 이유가 무엇인지 알아야 합니다. 예를 들어, 다음과 같은 수정사항이 발생한다면 어떻게 될까요?
이를 위해 OrderAgency는 물론이고 Order, Customer, CouponBook 등이 수정될 수 있습니다.
현재 카페 예제가 변경에 취약한 이유는 캡슐화를 지키지 않았기 때문에 입니다.
따라서 다음 글에서는 캡슐화를 지키면서 카페 예제의 문제점을 차근차근 해결해보겠습니다.
이번 글을 정리하자면..