Swift/Design_Pattern

[Swift 디자인 패턴] Composite Pattern (컴포지트) - 디자인 패턴 공부 9

Dev_Pingu 2021. 5. 18. 21:58
반응형

안녕하세요 Pingu입니다.🐧

 

지난 글에서는 구조 패턴 중 Bridge Pattern(브리지)에 대해 알아봤는데요, 이번 글에서는 또 다른 구조 패턴 중 하나인 Composite Pattern(컴포지트)에 대해 알아보도록 하겠습니다.

컴포지트 패턴이란?

컴포지트 패턴은 객체를 트리 구조로 구성하여 마치 하나의 객체인 것처럼 작업할 수 있는 구조 패턴입니다. 

  • Component
    • 컴포지트 패턴에 사용할 객체 모두에 사용가능한 인터페이스를 선언합니다.
    • 하위 객체에 접근하고 관리하기 위한 인터페이스를 선언합니다.
  • Leaf
    • 하위 객체가 없는 가장 기본 단위의 객체입니다.
    • 기본 객체에 대한 작업을 구현합니다.
  • Composite (Container)
    • Container라고도 하며 Leaf나 다른 Composite를 하위 객체로 갖는 객체입니다.
    • 하위 객체의 구체적인 클래스는 모르지만 모든 객체는 Component에서 정의한 인터페이스를 따르니까 상관없습니다.
    • 하위 객체에 접근하고 뭔갈 요청할 작업들을 구현합니다.
  • Client
    • Component 인터페이스를 통해 컴포지트 패턴에 존재하는 모든 객체를 사용합니다.

컴포지트 패턴은 언제 쓰나요?

  • 객체들을 part-whole 계층으로 나타내고 싶을 때, 즉 트리 구조와 같이 나타내고 싶을 때 사용합니다.
  • 클라이언트가 객체 구성과 개별 객체 간 차이를 무시하고 싶을 때 사용합니다.
  • 즉 클라이언트는 모든 객체를 똑같이 취급하고 싶은 거예요.

예를 들어 파일 시스템의 디렉토리 구조를 생각해보겠습니다. 파일 시스템은 디렉토리와 파일을 가지고 모든 파일들을 계층화 시킵니다. 어떤 디렉토리 안에는 다른 디렉토리가 존재할 수도 있고 파일이 존재할 수도 있죠?

 

이러한 상황에서 어떤 디렉토리가 차지하는 디스크 용량을 구하고자 한다면 어떻게 하면 될까요? 자신이 가지고 있는 모든 디렉토리와 파일의 용량을 더하면 자신의 용량이 될 거예요. 이때 자신의 모든 하위 디렉토리에도 똑같이 용량을 구하는 작업을 하고 그 결과를 모두 더하면 될 거 같은데요, 이러 때 컴포지트 패턴을 사용할 수 있습니다.

 

컴포지트 패턴을 사용해서 용량을 구하는 메서드를 인터페이스로 만들면 됩니다. 파일의 경우 자신의 용량만 반환하면 되고 디렉토리의 경우 해당 디렉토리의 하위에 존재하는 모든 디렉토리와 파일의 크기를 구하면 되겠죠? 이렇게 하면 어떤 디렉토리나 파일에 대해서도 크기를 구할 때 전체 구조를 생각할 필요가 없게 됩니다. 하위에 뭔가 존재한다면 걔네한테도 똑같은 작업을 시켜버리면 되기 때문이에요.

컴포지트 패턴의 결과

  • Leaf, Composite로 구성된 클래스들의 계층 구조를 만들 수 있습니다.
  • 클라이언트의 코드가 단순해집니다.
  • Component 인터페이스를 따르는 새로운 구성 요소는 쉽게 추가할 수 있습니다.
  • 완전히 새로운 객체들은 추가하기 어렵습니다.

예제

그럼 이제 Composite Pattern을 Swift로 구현해보겠습니다.

아까 예로 들었던 파일 시스템에서 디렉토리나 파일의 용량을 구하는 예제로 구현해볼게요.

가장 먼저 Component부터 만들어 보겠습니다.

protocol FileComponent {
    var size: Int { get set }
    var name: String { get set }
    func open()
    func getSize() -> Int
}

파일이든 디렉토리든 반드시 가지고 있어야 할 요소들을 FileComponent 프로토콜로 정의했습니다.

그럼 이걸 채택한 Directory를 하나 만들어볼게요.

final class Directory: FileComponent {
    var size: Int
    var name: String
    var files: [FileComponent] = []
    func open() {
        print("\(self.name) Directory의 모든 File Open")
        for file in files {
            file.open()
        }
        print("\(self.name) Directory의 모든 File Open 완료\n")
    }
    
    func getSize() -> Int {
        var sum: Int = 0
        for file in files {
            sum += file.getSize()
        }
        return sum
    }
    
    func addFile(file: FileComponent) {
        self.files.append(file)
        self.size += file.size
    }
    
    init(size: Int, name: String) {
        self.size = size
        self.name = name
    }
}

디렉토리이니까 File들을 모을 수 있는 배열을 하나 가지고 있어야 합니다.

또한 size는 자신이 가지고 있는 모든 파일, 디렉토리의 size의 합을 반환해줘야 합니다.

 

파일은 여러 종류가 존재할 수 있겠죠?

final class MusicFile: FileComponent {
    var size: Int
    var name: String
    var artist: String
    
    func open() {
        print("\(self.name) Music Artist  : \(self.artist)")
    }
    
    func getSize() -> Int {
        return self.size
    }
    
    init(size: Int, name: String, artist: String) {
        self.size = size
        self.name = name
        self.artist = artist
    }
}
final class CodeFile: FileComponent {
    var size: Int
    var name: String
    var language: String
    
    func open() {
        print("\(self.name) Code Language : \(self.language)")
    }
    
    func getSize() -> Int {
        return self.size
    }
    
    init(size: Int, name: String, language: String) {
        self.size = size
        self.name = name
        self.language = language
    }
}

파일은 여러 개의 종류가 존재할 수 있고 각각의 종류마다 갖는 프로퍼티도 다를 수 있습니다.

저는 Music, Code 파일을 만들어봤어요.

 

이렇게 하면 컴포지트 패턴에 필요한 모든 요소를 구현한 거예요.

바로 한 번 사용해볼게요!

 

위와 같이 구현한 파일과 디렉토리의 계층 구조를 만들 수 있습니다.

그럼 가장 하고 싶었던 각 디렉토리의 크기를 구해보는 것도 해볼게요.

각각의 디렉토리에서도 자신에 속한 모든 FileComponent를 채택한 객체들의 size 합을 잘 보여주는 것을 볼 수 있습니다.

 

이렇게 구조 패턴 중 하나인 Composite Pattern에 대해 알아보고 간단하게 구현도 해봤습니다.

혹시라도 틀린 부분이 있다면 알려주시면 감사하겠습니다.

 

전체 코드는 여기에서 볼 수 있습니다.

 

감사합니다.

반응형