본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 컴포지트 패턴 (Composite Pattern)

컴포지트 패턴은 동일 기능을 수행하는 부분-전체 관계를 정의할 때 유용하다.

Context

보통 컴퓨터의 가격과 전력을 산정할 때는 중요하다. 어떤 CPU와 어떤 램 그리고 그래픽카드를 장착했는지에 따라 컴퓨터의 가격과 소비전력을 천차만별이다.

흔히 우리는 기존에 배운대로 설계하여 코드를 작성할 것이다. 일부 눈엣가시인 듯한 코드들이 보이지만 가볍게 무시하자.

class Computer {
    private CPU cpu;
    private Ram ram;
    private HardDisk disk;

    public Computer(CPU cpu, Ram ram, HardDisk disk) {
        this.cpu = cpu;
        this.ram = ram;
        this.disk = disk;
    }

    public int getPrice () {
        return cpu.getPrice() + ram.getPrice() + disk.getPrice();
    }

    public int getPower() {
        return cpu.getPower() + ram.getPower() + disk.getPower();
    }
}

class CPU {
    public int getPrice () {
        return 100;
    }

    public int getPower() {
        return 95;
    }
}

class Ram {
    public int getPrice () {
        return 20;
    }

    public int getPower () {
        return 5;
    }
}

class HardDisk {
    public int getPrice() {
        return 10;
    }

    public int getPower() {
        return 20;
    }
}

Problem

역시나 요구사항은 피할 수 없었다. 게임을 하고싶은데 서버용 컴퓨터처럼 그래픽카드가 장착되어 있지 않았다.

위 다이어그램처럼 VGA를 추가해주자.

class VGA {
    public int getPrice() {
        return 15;
    }

    public int getPower() {
        return 60;
    }
}

VGA클래스가 추가되었으므로 Computer에 장착하려면 수정이 불가피하다.

class Computer {
    private CPU cpu;
    private Ram ram;
    private HardDisk disk;
    private VGA vga;

    public Computer(CPU cpu, Ram ram, HardDisk disk, VGA vga) {
        this.cpu = cpu;
        this.ram = ram;
        this.disk = disk;
        this.vga = vga;
    }

    public int getPrice () {
        return cpu.getPrice() + ram.getPrice() + disk.getPrice() + vga.getPrice();
    }

    public int getPower() {
        return cpu.getPower() + ram.getPower() + disk.getPower() + vga.getPower();
    }
}

필드로 vga:VGA를 넣고 각 메서드에 해당 내용을 수정해주었다. 확장에 대해서 수정이 되었으니 역시 OCP 위반이다. 더불어 이러한 단순 코드들은 별로 좋지 않은 코드들이다.

Solution

Computer은 각 부품에 대해서 포함관계를 가지고 있으며 각 부품과 동일하게 getPower, getPrice등의 기능을 가지고 있다. 이런 경우 아래와 같은 컴포지트 패턴을 사용하면 유용하게 처리할 수 있다.

전체-부분 관계는 사실 부분과 전체는 모두 동일한 기능을 수행할 수 있다. 부분들이 모두 같은 기능을 수행한다는 것은 전체가 그 기능을 수행한 것과 같은 의미이기 떄문이다.

abstract class Component {
    public abstract int getPrice();

    public abstract int getPower();
}

class Computer extends Component {
    private List<Component> components = new ArrayList<>();

    @Override
    public int getPrice() {
        return components.stream().mapToInt(Component::getPrice).sum();
    }

    @Override
    public int getPower() {
        return components.stream().mapToInt(Component::getPower).sum();
    }

    public void addComponent(Component component) {
        components.add(component);
    }
}

class CPU extends Component {
    @Override
    public int getPrice() {
        return 100;
    }

    @Override
    public int getPower() {
        return 95;
    }
}

class Ram extends Component {
    @Override
    public int getPrice() {
        return 20;
    }

    @Override
    public int getPower() {
        return 5;
    }
}

class HardDisk extends Component {
    @Override
    public int getPrice() {
        return 10;
    }

    @Override
    public int getPower() {
        return 20;
    }
}

class VGA extends Component {
    @Override
    public int getPrice() {
        return 15;
    }

    @Override
    public int getPower() {
        return 60;
    }
}

코드를 작성한 후 아래와 같이 테스트해보면 잘 수행하는 것을 알 수 있으며 VGA 추가에 따른 기존 코드의 변화는 없는 것을 알 수 있다.

class Client{
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.addComponent(new CPU());
        computer.addComponent(new HardDisk());
        computer.addComponent(new Ram());
        computer.addComponent(new VGA());

        System.out.println(computer.getPower());
        System.out.println(computer.getPrice());
    }
}

Summary

컴포지트 패턴은 부분-전체 관계를 정의할 때 유용하며, 클라이언트는 전체와 부분을 구분하지 않고 사용할 수 있다.

구성 요소

  • Component : 전체와 부분에 해당하는 공통 인터페이스를 정의한다.
  • Composite : 전체 클래스로 복수 개의 Component를 가질 수 있다.
  • Leaf : 구체적인 부분 클래스로 Composite 객체의 부품으로 사용된다.