Composite 디자인 패턴 활용하기

chiyongs·2024년 2월 15일
2

Composite 디자인 패턴

Composite 패턴은 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 하는 패턴입니다.


이미지출처 : https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%8F%AC%EC%A7%80%ED%8A%B8_%ED%8C%A8%ED%84%B4

구성 요소

  • Component : 모든 component 들을 추상화한 개념, Leaf와 Composite 클래스의 인터페이스
  • Leaf : Component 인터페이스를 구현한 구현체 클래스
  • Composite : Component 인터페이스를 구현하고, 구현체들을 가지며, 이런 구현체들을 관리하기 위한 메소드들을 구현하는 클래스

코드로 알아보는 Composite 패턴

여러분은 아래 예시 요구사항을 만족하는 코드를 작성해야 합니다.

예시상황

현재 강아지멤버십, 고양이멤버십 이렇게 2가지 멤버십이 존재하는 상황입니다.
각각의 멤버십들은 적립액과 적립율이 존재합니다.
두 가지 멤버십은 동시에 적용할 수 없으며, 가장 높은 적립율을 가진 멤버십을 적용합니다.

  • 기존 멤버십 코드
public class Membership {
	double dogMembershipSaveRate;
    long dogMembershipSaveAmount;
    
    double catMembershipSaveRate;
    long catMembershipSaveAmount;
}

이런 상황에서 우리는 토끼멤버십을 추가해야합니다.

가장 간단한 방법은 기존 멤버십 코드에 토끼멤버십 관련 필드들을 추가하는 방법입니다.

public class Membership {
	double dogMembershipSaveRate;
    long dogMembershipSaveAmount;
    
    double catMembershipSaveRate;
    long catMembershipSaveAmount;
    
    double rabbitMembershipSaveRate;
    long rabbitMembershipSaveAmount;
}

하지만, 이 방법은 확장에 유연하지 않습니다.

첫 번째로, 토끼멤버십이 아닌 기린멤버십, 햄스터멤버십이 추가된다면 각 멤버십의 적립액, 적립율에 대한 필드가 한 클래스 내에서 계속해서 늘어나야 하죠.
두 번째로, 멤버십 개수가 늘어날수록 여러 멤버십을 동시에 적용할 수 없기 때문에 어떤 멤버십을 적용해야하는지 계산하기 더더욱 복잡해집니다.
현재는 단순히 가장 높은 적립율을 가진 멤버십을 적용하지만, 추후에 적립율과 적립액 모두를 보며 계산해야 할 수도 있기 때문입니다.

이런 상황은 Composite 디자인 패턴을 활용해 좀 더 유연하게 코드를 작성할 수 있습니다.

public interface MembershipType {
	double getSaveRate();
    long getSaveAmount();
}

public class DogMembership implements Membership {
	private final double saveRate;
    private final long saveAmount;
    
	@Override
    public double getSaveRate() { return saveRate; }
    @Override
    public long getSaveAmount() { return saveAmount; }
}

public class CatMembership implements Membership {
	private final double saveRate;
    private final long saveAmount;
    
	@Override
    public double getSaveRate() { return saveRate; }
    @Override
    public long getSaveAmount() { return saveAmount; }
}

public class RabbitMembership implements Membership {
	private final double saveRate;
    private final long saveAmount;
    
	@Override
    public double getSaveRate() { return saveRate; }
    @Override
    public long getSaveAmount() { return saveAmount; }
}

위처럼 MembershipType 인터페이스(Component)를 만들고 각각의 멤버십들을 Leaf 클래스로 만들 수 있습니다.
그런 후 Composite 클래스를 아래와 같이 만들 수 있습니다.

public class CompositeMembership implements MembershipType {
	private List<MembershipType> memberships;
    ...
    
    @Override
    public double getSaveRate() {
      var maxSaveRateMembership = memberships.stream()
							      	   .max(saveRateComparator);
      return maxSaveRateMembership.map(MembershipType::getSaveRate)
      							  .orElse(0.0);
    }
    ...
}

이렇게 만든 Composite 클래스를 통해 모든 멤버십이 유효한 적립액, 적립율을 가지고 있는지 확인하는 것도 쉬워집니다.
MembershipType 인터페이스에 hasSaveRate 메서드를 추가하면 각 멤버십(Leaf 클래스)에서 유효한 적립율을 가지고 있는지 검사하는 메서드를 구현해야하고 이를 Composite 클래스에서도 hasSaveRate 메서드를 멤버십들이 담겨있는 types를 순회하며 판단할 수 있습니다.

이처럼 동일한 방식을 가진 개체들과 그 개체들을 처리하기 위해 동일한 코드를 사용하는 경우에 Composite 디자인 패턴을 사용하면 좀 더 유연한 방식으로 풀어낼 수 있습니다.

0개의 댓글