Menu
Coddy logo textTech

컴포지트 패턴

Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 96번째.

컴포지트 패턴(Composite pattern)은 개별 객체와 객체 그룹을 동일하게 다룰 수 있도록 해줍니다. 상태(State) 패턴이 내부 상태에 따라 동작을 변경하는 반면, 컴포지트(Composite)는 리프(leaf)와 컨테이너(container)가 동일한 인터페이스를 공유하는 트리 구조를 구축합니다.

이 패턴은 파일 시스템, 조직도 또는 UI 컴포넌트와 같은 계층적 구조에 적합합니다. Go에서는 개별 항목과 컨테이너가 모두 구현하는 공통 인터페이스를 정의합니다:

type Component interface {
    GetSize() int
}

type File struct {
    Name string
    Size int
}

func (f File) GetSize() int {
    return f.Size
}

컴포지트(컨테이너)는 자식들을 포함하며 그들의 결과를 집계함으로써 동일한 인터페이스를 구현합니다:

type Folder struct {
    Name     string
    Children []Component
}

func (f *Folder) Add(c Component) {
    f.Children = append(f.Children, c)
}

func (f Folder) GetSize() int {
    total := 0
    for _, child := range f.Children {
        total += child.GetSize()
    }
    return total
}

이제 폴더 안에 폴더를 중첩할 수 있으며, GetSize()를 호출하는 것은 단일 파일이든 전체 디렉토리 트리이든 상관없이 동일하게 작동합니다:

docs := &Folder{Name: "docs"}
docs.Add(File{Name: "readme.txt", Size: 100})
docs.Add(File{Name: "notes.txt", Size: 50})

root := &Folder{Name: "root"}
root.Add(docs)
root.Add(File{Name: "config.json", Size: 25})

fmt.Println(root.GetSize())  // 175

컴포지트 패턴은 부분-전체 계층 구조를 표현해야 하고, 클라이언트가 개별 객체와 복합 객체를 동일하게 다루기를 원할 때 이상적입니다.

challenge icon

챌린지

쉬움

Composite 패턴을 사용하여 조직도 시스템을 구축해 봅시다! 개별 직원과 부서(직원 또는 다른 부서를 포함하는)를 동일하게 취급할 수 있는 구조를 만들게 됩니다. 이는 조직의 어느 부분에서든 총 급여를 계산하는 데 매우 적합합니다.

코드는 세 개의 파일로 구성됩니다:

  • component.go: 개인과 그룹이 모두 구현할 공통 인터페이스를 정의합니다.

    두 개의 메서드를 가진 OrgComponent 인터페이스를 생성하세요:

    • GetSalary() int — 이 컴포넌트의 총 급여를 반환합니다.
    • GetName() string — 직원 또는 부서의 이름을 반환합니다.
  • organization.go: 리프(개별 직원) 및 컴포지트(부서) 타입을 모두 구현합니다.

    Name (string)과 Salary (int) 필드를 가진 Employee 구조체를 생성하세요. 이 구조체의 GetSalary()는 자신의 급여를 반환하고, GetName()은 자신의 이름을 반환합니다.

    Name (string) 필드와 OrgComponent 항목을 담는 Members 슬라이스를 가진 Department 구조체를 생성하세요. 멤버를 추가하기 위한 Add(c OrgComponent) 메서드를 추가하세요. 이 구조체의 GetSalary()는 모든 멤버의 급여 합계를 반환해야 하며, GetName()은 부서 이름을 반환합니다.

  • main.go: 조직 구조를 구축하고 급여를 계산합니다.

    직원 수를 읽습니다. 각 직원에 대해 이름과 급여를 읽고 Employee 인스턴스를 생성합니다. 그런 다음 부서 수를 읽습니다. 각 부서에 대해 이름과 멤버 인덱스 개수를 읽은 다음, 해당 인덱스들(생성된 순서대로 이전의 직원 또는 부서를 참조하는 0부터 시작하는 인덱스)을 읽습니다.

    구조를 구축한 후, 마지막 인덱스를 읽고 해당 컴포넌트의 이름과 총 급여를 [name]: [salary] 형식으로 출력합니다.

다음 입력이 제공됩니다:

  • 직원 수, 그 다음 각 직원의 이름과 급여가 별도의 줄에 제공됩니다.
  • 부서 수, 그 다음 각 부서의 이름, 멤버 수, 멤버 인덱스가 제공됩니다.
  • 조회할 마지막 인덱스

예를 들어, 다음과 같이 입력되면:

3
Alice
50000
Bob
60000
Carol
55000
1
Engineering
3
0
1
2
3

출력은 다음과 같아야 합니다:

Engineering: 165000

그리고 다음과 같이 입력되면:

4
Alice
50000
Bob
60000
Carol
55000
Dave
70000
2
Dev
2
0
1
QA
2
2
3
4

출력은 다음과 같아야 합니다:

QA: 125000

그리고 다음과 같이 입력되면:

2
Alice
50000
Bob
60000
0
1

출력은 다음과 같아야 합니다:

Bob: 60000

단일 직원에 대해 호출하든 전체 부서에 대해 호출하든 GetSalary()가 동일하게 작동하는 방식에 주목하세요. Composite 패턴을 사용하면 동일한 인터페이스를 통해 개인과 그룹을 균일하게 처리할 수 있습니다!

치트 시트

컴포지트 패턴(Composite pattern)은 개별 객체와 객체 그룹을 공통 인터페이스를 통해 동일하게 다룹니다. 파일 시스템, 조직도 또는 UI 컴포넌트와 같은 계층 구조에 이상적입니다.

개별 항목과 컨테이너가 모두 구현하는 공통 인터페이스를 정의합니다:

type Component interface {
    GetSize() int
}

리프(leaf, 개별 항목)를 구현합니다:

type File struct {
    Name string
    Size int
}

func (f File) GetSize() int {
    return f.Size
}

자식 요소를 포함하고 그 결과를 집계하는 컴포지트(composite, 컨테이너)를 구현합니다:

type Folder struct {
    Name     string
    Children []Component
}

func (f *Folder) Add(c Component) {
    f.Children = append(f.Children, c)
}

func (f Folder) GetSize() int {
    total := 0
    for _, child := range f.Children {
        total += child.GetSize()
    }
    return total
}

개별 항목과 복합체를 동일하게 처리하는 사용 예시입니다:

docs := &Folder{Name: "docs"}
docs.Add(File{Name: "readme.txt", Size: 100})
docs.Add(File{Name: "notes.txt", Size: 50})

root := &Folder{Name: "root"}
root.Add(docs)
root.Add(File{Name: "config.json", Size: 25})

fmt.Println(root.GetSize())  // 175

직접 해보기

package main

import "fmt"

func main() {
	// 직원 수 읽기
	var numEmployees int
	fmt.Scanln(&numEmployees)

	// 모든 구성 요소(직원 및 부서)를 생성 순서대로 저장
	var components []OrgComponent

	// TODO: 각 직원의 이름과 급여 읽기
	// Employee 인스턴스를 생성하고 components 슬라이스에 추가
	for i := 0; i < numEmployees; i++ {
		var name string
		var salary int
		fmt.Scanln(&name)
		fmt.Scanln(&salary)
		// TODO: 직원을 생성하고 components에 추가
	}

	// 부서 수 읽기
	var numDepartments int
	fmt.Scanln(&numDepartments)

	// TODO: 각 부서의 이름, 구성원 수 및 구성원 인덱스 읽기
	// Department 인스턴스를 생성하고, 인덱스로 구성원을 추가한 뒤 components에 추가
	for i := 0; i < numDepartments; i++ {
		var deptName string
		var memberCount int
		fmt.Scanln(&deptName)
		fmt.Scanln(&memberCount)
		// TODO: 부서를 생성하고, 구성원 인덱스를 읽어 구성원을 추가한 뒤 components에 추가
	}

	// 조회할 최종 인덱스 읽기
	var queryIndex int
	fmt.Scanln(&queryIndex)

	// TODO: 구성 요소의 이름과 총 급여를 다음 형식으로 출력: [name]: [salary]
	// 예시: fmt.Printf("%s: %d\n", name, salary)
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨