[오브젝트] 합성과 유연한 설계

합성과 유연한 설계

상속과 합성은 객체지향 프로그래밍에서 널리 사용되는 코드 재사용 기법이다. 상속은 컴파일 타임에 해결되며 합성은 런타임에 해결된다. 상속은 is-a 관계라 하고 합성은 has-a관계가 된다.

상속을 통해 구현하는 경우에는 다음과 같은 문제를 수반한다.

  • 불필요한 인터페이스 상속 문제
  • 메소드 오버라이딩의 오작용 문제
  • 부모 클래스와 자식 클래스의 동시 수정 문제

위 세 문제는 모두 합성으로 해결이 가능하다. 상속은 또 다른 문제를 낳는다. 상속과 코드 재사용에서 보았던 코드에서 부가적인 부분을 추가할 때 생긴다. 앞에서는 세금을 추가하는 부가 서비스만 존재했지만, 전체 요금에서 일정 금액을 공제하는 부가 서비스와 같은 여타 수 많은 부가 서비스가 추가 된다면 해당 구현 클래스마다 부가 서비스들에 맞는 상속을 해야하고, 순서가 변경되기라도 하면 이에 따라 구현을 해주어야 한다. 결과적으로 클래스의 수가 폭발적으로 늘어난다. 다음은 상속을 합성으로 바꾸고 부가 서비스를 데코레이터 패턴으로 변경한 코드이다.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public interface Order {
long calculateFee();
}

public class RegularOrder implements Order {
public List<Product> products;

public RegularOrder(List<Product> products) {
this.products = products;
}

@Override
public long calculateFee() {
long fee = 0;

for(Product product : products) {
fee += product.getFee();
}

return fee;
}
}

public class DiscountOrder implements Order {
public List<Product> products;

public DiscountOrder(List<Product> products) {
this.products = products;
}

@Override
public long calculateFee() {
long fee = 0;

for(Product product : products) {
if(product.getOrderedDay().equals(DayOfWeek.MONDAY))
fee += product.getFee() * 0.9;
else
fee += product.getFee();
}

return fee;
}
}
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
29
public abstract class AdditionalPolicy implements Order{
protected Order order;

public AdditionalPolicy(Order order) {
this.order = order;
}
}

public class TaxRatePolicy extends AdditionalPolicy{
public TaxRatePolicy(Order order) {
super(order);
}

@Override
public long calculateFee() {
return (long)(order.calculateFee() * 1.1);
}
}

public class TotalDiscountPolicy extends AdditionalPolicy{
public TotalDiscountPolicy(Order order) {
super(order);
}

@Override
public long calculateFee() {
return Math.max(0, order.calculateFee() - 1000);
}
}
1
2
3
4
Order totalDiscountTaxOrder = new TotalDiscountPolicy(
new TaxRatePolicy(
new RegularOrder(Arrays.asList(new Product(DayOfWeek.MONDAY, 1000),new Product(DayOfWeek.FRIDAY, 1000)))));
totalDiscountTaxOrder.calculateFee();

위 코드는 전체 주문 금액에서 10%의 세금을 추가한 총 금액에서 1000원을 할인하는 주문이다. 만약 전체 주문 금액에서 1000원을 할인하고 10%의 세금을 더하고 싶다면 다음과 같이 변경하면 된다.

1
2
3
4
Order totalDiscountTaxOrder = new TaxRatePolicy(
new TotalDiscountPolicy(
new RegularOrder(Arrays.asList(new Product(DayOfWeek.MONDAY, 1000),new Product(DayOfWeek.FRIDAY, 1000)))));
totalDiscountTaxOrder.calculateFee();

믹스인

믹스인은 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 가리킨다. 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다. 상속과 차이점은 상속은 is-a관계로 묶어버린다면 믹스인은 코드를 다른 코드 안에 섞어 넣기 위한 방법이다. 주로 언어레벨에서 지원하는데 스칼라의 트레이트(trait)가 믹스인을 잘 설명한다.

트레이드에서 extends 키워드는 상속의 의미가 아닌 해당 클래스의 자손에 해당될 경우에만 믹스인을 허용하겠다는 제약 조건이다. 즉, 해당 클래스로 트레이트를 사용할 문맥을 제한시킬 수 있다. 트레이트에서의 상속은 부모 클래스를 고정시키는 것이 아니다. 이를 통해 제약을 코드로 표현하고 개발자의 실수를 막을 수 있다. 또한 super 참조자를 통해 상위에 연결된 코드를 호출한다. 트레이트가 부모 클래스를 고정시키는게 아닌것과 마찬가지로 super로 참조되는 코드 또한 고정되지 않는다. super로 참조될 코드가 결정되는 시점은 실제로 트레이트가 믹스인되는 시점이다. 즉, 스칼라의 트레이트에서 super 참조는 this와 같이 동적으로 결정된다. 결과적으로 트레이트는 상속과 달리 재사용 가능한 문맥을 확장 가능하도록 열어둔다. 이런 측면에서 믹스인과 합성은 유사하다.

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