[디자인 패턴] Composite Pattern

전성훈·2023년 11월 8일
1

DesignPattern

목록 보기
14/18
post-thumbnail

주제: 일괄 명령 또는 네트워크 데이터 처리


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

Composite Pattern이란?

  • Composite 패턴은 트리 구조를 이용해 전체-부분의 관계를 표현하는 패턴이다. 이 패턴을 사용하면 클라이언트는 개별 객체와 복합 객체를 동일하게 취급할 수 있다.
  • 이는 공통적으로 수행할 수 있는 기능(메서드)을 가진 프로토콜이 있고, 모든 객체들은 해당 프로토콜(인터페이스)을 준수하고 있는 패턴이다.
  • Composite 패턴은 트리 구조로 되어있는 프로젝트에서 한 번의 실행으로 해당 실행을 최하단까지 재귀적으로 사용하고 싶을때 효과적으로 사용할 수 있다.
    • 군대에서 대대장이 중대장에게 명령하고, 중대장이 소대장들에게 명령을 하고, 소대장이 소대원들에게 모두 같은 명령을 하달해서 전 부대원이 해당 명령을 수행하게 되는 것
    • 사용하는 폴더의 용량을 알고 싶을 때 du -h 커맨드 입력을 통해 파일정보를 알 수 있다. 이때 최상위 폴더에서 해당 옵션을 입력한다면 하위에 있는 모든 폴더, 파일들의 정보를 한 번에 확인할 수 있기 때문에 우리는 최하위 폴더까지 일일이 커맨드를 입력하지 않아도 되는 것

컴포지트 패턴의 구조

컴포지트

  • Component
    • 공통적으로 수행할 수 있는 기능을 가진 프로토콜
  • Leaf
    • Component를 채택하고 있고, 하위 트리가 없는 객체
  • Composite
    • Component를 채택하고 있고, Component 타입의 리스트를 가진 객체로서 해당 리스트에는 Leaf 및 Composite가 들어 올 수 있다.

장단점

장점

  • 클라이언트는 복합 객체와 개별 객체를 동일하게 취급할 수 있다.
  • 객체의 계층 구조를 쉽게 정의할 수 있다.
  • 새로운 종류의 Component를 쉽게 추가할 수 있다.

단점

  • Composite 패턴을 사용하면 설계가 과도하게 일반화될 수 있다.
  • Component 인터페이스가 너무 많은 책임을 가질 수 있다.

결론

  • Composite Pattern은 클라이언트가 단순한 구조로 명령을 내릴 수 있다는 장점이 있다. 또한 새로운 객체를 만들기에도 까다롭지 않다. 하지만 객체 간의 관계가 다소 복잡해지는 경향이 있고, 트리구조가 아니면 사용할 수 없다는 한계가 존재한다.

예시 코드

import UIKit

// Component
protocol Shape: AnyObject {
    func draw()
}

// Leaf
class Circle: Shape {
    func draw() {
        print("Draw a circle")
    }
}

class Square: Shape {
    func draw() {
        print("Draw a Square")
    }
}

// Composite
class Drawin: Shape {
    private var shapes: [Shape] = []
    
    func draw() {
        for shape in shapes {
            shape.draw()
        }
    }
    
    func addShape(shape: Shape) {
        shapes.append(shape)
    }
    
    func removeShape(shape: Shape) {
        shapes = shapes.filter { $0 !== shape }
    }
}
let drawin = Drawin()

drawin.addShape(shape: Circle())
drawin.addShape(shape: Circle())
drawin.addShape(shape: Square())
drawin.addShape(shape: Square())
drawin.addShape(shape: Circle())

drawin.draw()

Composite를 활용한 데이터 소스 처리

  • Composite 패턴은 일반적으로 객체의 계층 구조를 관리하는 데 사용되지만, 네트워크 데이터 캐싱과 같은 문제에도 적용할 수 있다.
  • 각 데이터 소스(예: 네트워크, 캐시)를 Component로 간주하고, 이 들을 관리하는 Composite를 만들 수 있다.
import Foundation

// Component
protocol DataSource { 
	func fetchData(completion: @escaping (Data?) -> Void)
}

// Leaf
class NetworkDataSource: DataSource { 
	let url: URL
	
	init(url: URL) { 
		self.url = url
	}
	
	func fetchData(completion: @escaping (Data?) -> Void) { 
		let task = URLSession.shared.dataTask(with: url) { (data, response, error) in 
			completion(data)
		}
		task.resume()
	}
}

class CacheDataSource: DataSource { 
	var data: Data?
	
	func fetchData(completion: @escaping (Data?) -> Void) { 
		completion(Data)
	}
}

// Composite
class CompositeDataSource: DataSource { 
	let networkDataSource: NetworkDataSource 
	let cacheDataSource: CacheDataSource
	 
	init(networkDataSource: NetworkDataSource, cacheDataSource: CacheDataSource) { 
		cacheDataSource.fetchData { [weak self] data in 
			if let data = data { 
				completion(data)
			} else { 
				self?.networkDataSource.fetchData { data in 
					self?.cacheDataSource.data = data
					completion(data)
				}
			}
		}
	}
}

// Usage 
let url = URL(string: "https://example.com")! 

let networkDataSource = NetworkDataSource(url: url)
let cacheDataSource = CahceDataSource()

let compositeDataSource = CompositeDataSource(
	networkDataSource: networkDatasource, 
	cahceDataSource: cacheDataSource
)
compositeDataSource.fetchData { data in 
	// Handle Data
}

데이터 소스 처리 심화

  • 위 다이어그램에서 FeedLoader 인터페이스는 Composite 패턴의 Component 역할을 합니다. 이 인터페이스는 RemoteFeedLoaderLocalFeedLoader (Leaf 역할) 그리고 RemoteWithLocalFallbackFeedLoader (Composite 역할)에 의해 구현된다.
  • RemoteWithLocalFallbackFeedLoaderFeedLoader 인터페이스를 채택하면서 동시에 RemoteFeedLoaderLocalFeedLoader를 포함하고 있다.
  • 이는 Composite 패턴의 특징인데, 즉 RemoteWithLocalFallbackFeedLoader는 단일 FeedLoader이면서 동시에 여러 FeedLoader를 포함하고 있는 복합 객체이다.
  • 이를 통해 클라이언트는 FeedLoader 인터페이스를 통해 단일 객체와 복합 객체를 동일하게 다룰 수 있다.
  • 이를 구현할 때 concrete type은 최대한 밖으로 빼서 ViewController를 최대한 가볍게 해주는 것이 중요하다.
  • 또한 if문과 같은 분기처리는 변수를 생성해서 변수를 통해 최대한 간단하게 코드를 설계하는 것이 중요하다.
import UIKit

//typealias FeedLoader = (([String]) -> Void) -> Void

// Interface
protocol FeedLoader {
    func loadFeed(completion: @escaping ([String]) -> Void)
}

class FeedViewController: UIViewController {
    var loader: FeedLoader!
    
    convenience init(loader: FeedLoader) {
        self.init()
        self.loader = loader
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        loader.loadFeed { loadedItems in
            // update UI
        }
    }
}
 
// leaf
class RemoteFeedLoader: FeedLoader {
    func loadFeed(completion: @escaping ([String]) -> Void) {
        print("remote")
    }
}

class LocalFeedLoader: FeedLoader {
    func loadFeed(completion: @escaping ([String]) -> Void) {
        print("local")
    }
}

struct Reachability {
    static let networkAvailable = false
}

// Composite
// concrete type은 최대한 밖으로 빼기
class RemoteWithLocalFallbackFeedLoader: FeedLoader {
    let remote: RemoteFeedLoader
    let local: LocalFeedLoader
    
    init(remote: RemoteFeedLoader, local: LocalFeedLoader) {
        self.remote = remote
        self.local = local
    }
    
    func loadFeed(completion: @escaping ([String]) -> Void) {
        // if문을 위 struct 변수를 활용하여 훨씬 더 간단하게 코드 작성
//        if Reachability.networkAvailable {
//            remote.loadFeed { loadedItems in
//                // do something
//            }
//        } else {
//            local.loadFeed { loadedItems in
//                // do something
//            }
//        }
        
        let load = Reachability.networkAvailable ? remote.loadFeed : local.loadFeed
        
        load(completion)
    }
}

let vc = FeedViewController(loader: RemoteFeedLoader())
let vc2  = FeedViewController(loader: LocalFeedLoader())
let vc3 = FeedViewController(loader: RemoteWithLocalFallbackFeedLoader(remote: RemoteFeedLoader(), local: LocalFeedLoader()))
vc3.loader.loadFeed { items in
    
}

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다

0개의 댓글