[오브젝트] 설계 품질과 트레이드 오프

설계 품질과 트레이드 오프

객체지향 설계는 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다. 설계는 변경을 위해 존재하고 변경에는 어떤 식으로든 비용이 발생한다. 그리고 훌륭한 설계는 합리적인 비용안에서 변경 가능한 구조를 만드는 것이다.

올바른 객체를 만들때는 객체의 상태가 아니라 객체의 행동에 초점을 두어야 한다. 그리고 이를 위해서는 객체의 책임을 기준으로 객체를 분할해야 한다. 가령 객체의 상태(데이터)를 중심으로 분할을 해본다고 가정을 해보자.

다음은 상태를 중심으로 분할한 커피주문하는 어플리케이션의 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Coffee {
private String beverage;
private long fee;

private DiscountType discountType;
private long discountAmount;
private double discountPercent;

public String getBeverage() {
return beverage;
}

public long getFee() {
return fee;
}

public DiscountType getDiscountType() {
return discountType;
}

public long getDiscountAmount() {
return discountAmount;
}

public double getDiscountPercent() {
return discountPercent;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CoffeeStore {
private List<Coffee> coffees = new ArrayList<>();

public CoffeeStore(Coffee ... coffees) {
this.coffees.addAll(Arrays.asList(coffees));
}

public List<Coffee> getCoffees() {
return coffees;
}

public void setCoffees(List<Coffee> coffees) {
this.coffees = coffees;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Customer {
private long money;

public long getMoney() {
return money;
}

public void setMoney(long money) {
this.money = money;
}
}
1
2
3
4
5
public enum DiscountType {
NONE_DISCOUNT,
PROMOTION_DISCOUNT,
COUPON_DISCOUNT
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Order {
private CoffeeStore coffeeStore;

public Receipt orderCoffee(Customer customer, String beverage) {
long money = customer.getMoney();

List<Coffee> coffees = this.coffeeStore.getCoffees();
for(Coffee coffee : coffees) {
if(coffee.getBeverage().equals(beverage)) {
long fee = coffee.getFee();

if(coffee.getDiscountType() == DiscountType.PROMOTION_DISCOUNT) {
fee = fee - coffee.getDiscountAmount();
} else if(coffee.getDiscountType() == DiscountType.COUPON_DISCOUNT) {
fee = (long)(fee * coffee.getDiscountPercent());
}

if(money < fee)
return new Receipt(0);
else
return new Receipt(fee);
}
}

return new Receipt(0);
}
}
1
2
3
4
5
6
7
public class Receipt {
private long fee;

public Receipt(long fee) {
this.fee = fee;
}
}

위 코드의 첫번째 문제점은 캡슐화를 위반한다는 점이다. 객체가 가지는 데이터의 access modifier를 private로 선언하였다고 캡슐화가 된다는것은 아니다. 자신이 가지는 데이터의 접근을 getter와 setter를 통해 허용하고 있으니 이는 캡슐화를 위반한다. 또한 그 데이터를 통해 커뮤니케이션을 하니 문제가 유발된다. 즉 높은 결합도를 가지고 있다.

두번째 문제는 어느 부분에 수정이 일어나도 Order 클래스를 수정해야 한다는 점이다. 가령 새로운 할인 정책이 추가된다고 가정해보자. 먼저, DiscountType을 수정해야 할 것이다. 그 이후엔 정책에 의한 추가로 Order에도 수정이 가해져야 한다. 이런 현상은 응집도가 낮다는 것을 의미한다.

결국 데이터 중심의 설계는 변경에 취약해진다. 이유는 다음과 같다.

  • 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
  • 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

데이터에 초점을 맞추고 이를 처리하는데 필요한 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 정보가 객체의 인터페이스에 고스란히 드러나게 된다. 결국 너무 이른시기에 데이터에 대해 고민하여 캡슐화를 실패하는데 영향을 준다. 또한 객체지향 설계는 객체간의 협력을 어떻게 풀어나갈까에 대한 고민을 하여야한다. 하지만 데이터 중심 설계에서의 초점은 객체 외부가 아닌 내부로 향하게 된다. 객체를 먼저 구현하고 다른 객체와의 협력을 고민하기 때문에 만들어진 틀에 억지로 끼워 맞추게 된다. 이는 결국 유연하지 못한 설계로 이어지게 된다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2021/01/10/Object/object4/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.